Plugin Directory

Changeset 3771537


Ignore:
Timestamp:
10/28/2026 08:11:59 PM (3 days ago)
Author:
zoobu
Message:

Release 5.4.0: optional Decline all, GPC to California rules, Do Not Sell choice, simpler monitoring settings, plus listing screenshots

Location:
cookieflame
Files:
11 added
16 edited
1 copied

Legend:

Unmodified
Added
Removed
  • cookieflame/tags/5.4.0/assets/css/admin.css

    r3769546 r3771537  
    625625  border: 1px solid #dcdcde;
    626626  border-radius: 4px;
     627}
     628.cf-admin-manual-row select {
     629  padding-right: 28px;
    627630}
    628631.cf-admin-manual-row .button-link-delete {
    … …  
    10171020  color: #c2410c;
    10181021}
     1022.cf-admin-upgrade-banner-dismiss {
     1023  margin: 0;
     1024  flex-shrink: 0;
     1025  align-self: flex-start;
     1026}
     1027.cf-admin-upgrade-banner-close {
     1028  background: none;
     1029  border: none;
     1030  color: #8c8f94;
     1031  font-size: 20px;
     1032  line-height: 1;
     1033  padding: 2px 4px;
     1034  cursor: pointer;
     1035  border-radius: 4px;
     1036}
     1037.cf-admin-upgrade-banner-close:hover,
     1038.cf-admin-upgrade-banner-close:focus {
     1039  color: #1d2327;
     1040  background: rgba(0, 0, 0, 0.05);
     1041}
    10191042
    10201043/* Licence tab Pro features card */
    … …  
    14751498  transition: border-color .15s;
    14761499}
     1500/* Room for WordPress's select arrow, drawn about 5-21px from the right edge. */
     1501.cf-admin-wrap .form-table select {
     1502  padding-right: 32px;
     1503}
    14771504.cf-admin-wrap .form-table input:focus,
    14781505.cf-admin-wrap .form-table select:focus,
  • cookieflame/tags/5.4.0/assets/css/cookieconsent.css

    r3766806 r3771537  
    11/**
    2  * CookieFlame v4.41.0 — Consent Banner Stylesheet
     2 * CookieFlame v5.4.0 — Consent Banner Stylesheet
    33 * ─────────────────────────────────────────────────────────────
    44 * Loaded on every front-end page via cookieflame_enqueue_assets().
    5  * Before v4.41.0 these rules were injected inline by
    6  * cookieconsent.js at runtime; they are now an external
    7  * stylesheet so the main JS bundle is ~5.6 KB smaller and
    8  * the CSS can be cached independently by the browser.
    95 *
    106 * Dark mode: JS adds `.cc-dark` to <html> when theme=dark.
    117 * Icon position: JS adds `.cc-icon-right` to <html> when
    128 * iconPosition=right (default is left).
     9 *
     10 * Custom properties (v5.4.0): the banner's colours, corner radius and
     11 * font come from the --cc-* properties below. Override them on :root
     12 * (and :root.cc-dark for the dark theme) to restyle the banner.
    1313 */
     14
     15:root{--cc-font:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;--cc-radius:20px;--cc-btn-radius:12px;--cc-bg:#fff;--cc-text:#1d1d1f;--cc-text-muted:#555;--cc-link:#0071e3;--cc-focus:#0071e3;--cc-banner-shadow:rgba(0,0,0,.15);--cc-accent:#0071e3;--cc-accent-hover:#005bb5;--cc-accent-text:#fff;--cc-accent-shadow:rgba(0,113,227,.3);--cc-accent-shadow-hover:rgba(0,113,227,.35);--cc-secondary-bg:#e8e8ed;--cc-secondary-hover:#d2d2d7;--cc-secondary-text:#1d1d1f;--cc-save-bg:#34c759;--cc-save-hover:#28a745;--cc-save-text:#fff;--cc-save-shadow:rgba(52,199,89,.3);--cc-save-shadow-hover:rgba(52,199,89,.35);--cc-details:#555;--cc-details-hover:#555;--cc-border:#e8e8ed;--cc-divider:#f0f0f2;--cc-cat-desc:#646970;--cc-always-on:#767676;--cc-toggle-off:#ccc;--cc-toggle-on:#0071e3;--cc-knob:#fff;--cc-powered:#999;--cc-powered-hover:#666;--cc-icon-bg:#1d1d1f;--cc-icon-text:#fff;--cc-icon-shadow:rgba(0,0,0,.25);--cc-icon-shadow-hover:rgba(0,0,0,.35)}
     16:root.cc-dark{--cc-bg:#1c1c1e;--cc-text:#f5f5f7;--cc-text-muted:#98989d;--cc-link:#2997ff;--cc-banner-shadow:rgba(0,0,0,.5);--cc-accent:#0a84ff;--cc-accent-hover:#0071e3;--cc-accent-shadow:rgba(10,132,255,.35);--cc-accent-shadow-hover:rgba(10,132,255,.4);--cc-secondary-bg:#2c2c2e;--cc-secondary-hover:#3a3a3c;--cc-secondary-text:#f5f5f7;--cc-save-bg:#30d158;--cc-save-hover:#28a745;--cc-save-shadow:rgba(48,209,88,.3);--cc-save-shadow-hover:rgba(48,209,88,.35);--cc-details:#98989d;--cc-details-hover:#c7c7cc;--cc-border:#2c2c2e;--cc-divider:#2c2c2e;--cc-cat-desc:#8e8e93;--cc-always-on:#8e8e93;--cc-toggle-off:#3a3a3c;--cc-toggle-on:#0a84ff;--cc-knob:#f5f5f7;--cc-powered:#aaa;--cc-powered-hover:#ddd;--cc-icon-bg:#f5f5f7;--cc-icon-text:#1d1d1f;--cc-icon-shadow:rgba(255,255,255,.15);--cc-icon-shadow-hover:rgba(255,255,255,.25)}
    1417
    1518#cc-overlay{position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:999998;opacity:0;pointer-events:none;transition:opacity .3s;backdrop-filter:blur(2px);-webkit-backdrop-filter:blur(2px)}
    1619#cc-overlay.cc-show{opacity:1;pointer-events:auto}
    17 #cc-banner{position:fixed;bottom:0;left:0;right:0;z-index:999999;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;font-size:14px;line-height:1.5;color:#1d1d1f;background:#fff;box-shadow:0 -8px 40px rgba(0,0,0,.15);transform:translateY(100%);transition:transform .4s cubic-bezier(.4,0,.2,1);border-radius:20px 20px 0 0;max-height:90vh;overflow-y:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}
     20#cc-banner{position:fixed;bottom:0;left:0;right:0;z-index:999999;font-family:var(--cc-font);font-size:14px;line-height:1.5;color:var(--cc-text);background:var(--cc-bg);box-shadow:0 -8px 40px var(--cc-banner-shadow);transform:translateY(100%);transition:transform .4s cubic-bezier(.4,0,.2,1);border-radius:var(--cc-radius) var(--cc-radius) 0 0;max-height:90vh;overflow-y:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}
    1821#cc-banner.cc-show{transform:translateY(0)}
    1922#cc-banner,#cc-banner *{box-sizing:border-box;-webkit-tap-highlight-color:transparent}
    … …  
    2124.cc-inner{max-width:720px;margin:0 auto;padding:32px 32px 20px}
    2225.cc-title{font-size:20px !important;font-weight:800 !important;margin:0 0 8px !important;letter-spacing:-.3px;line-height:1.3 !important;color:inherit !important;background:none !important;border:none !important;padding:0 !important;text-transform:none !important}
    23 .cc-text{margin:0 0 20px !important;color:#555 !important;font-size:14px !important;font-weight:400 !important;line-height:1.6 !important;padding:0 !important;border:none !important;background:none !important;text-transform:none !important}
    24 .cc-text a{color:#0071e3 !important;text-decoration:none !important;font-weight:500 !important;background:none !important;border:none !important;box-shadow:none !important}
     26.cc-text{margin:0 0 20px !important;color:var(--cc-text-muted) !important;font-size:14px !important;font-weight:400 !important;line-height:1.6 !important;padding:0 !important;border:none !important;background:none !important;text-transform:none !important}
     27.cc-text a{color:var(--cc-link) !important;text-decoration:none !important;font-weight:500 !important;background:none !important;border:none !important;box-shadow:none !important}
    2528.cc-text a:hover{text-decoration:underline !important}
    2629.cc-buttons{display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin-bottom:16px}
    27 .cc-btn{display:inline-flex !important;align-items:center !important;justify-content:center !important;border-radius:12px !important;border:none !important;font-weight:700 !important;cursor:pointer !important;letter-spacing:-.1px !important;text-transform:none !important;text-decoration:none !important;line-height:1 !important;white-space:nowrap !important;-webkit-appearance:none !important;appearance:none !important;outline:none !important;box-shadow:none !important;text-shadow:none !important;float:none !important;margin:0 !important}
    28 .cc-btn:focus{outline:2px solid #0071e3 !important;outline-offset:2px !important}
     30.cc-btn{display:inline-flex !important;align-items:center !important;justify-content:center !important;border-radius:var(--cc-btn-radius) !important;border:none !important;font-weight:700 !important;cursor:pointer !important;letter-spacing:-.1px !important;text-transform:none !important;text-decoration:none !important;line-height:1 !important;white-space:nowrap !important;-webkit-appearance:none !important;appearance:none !important;outline:none !important;box-shadow:none !important;text-shadow:none !important;float:none !important;margin:0 !important}
     31.cc-btn:focus{outline:2px solid var(--cc-focus) !important;outline-offset:2px !important}
    2932.cc-btn:active{transform:scale(.97) !important}
    30 .cc-btn-accept{background:#0071e3 !important;color:#fff !important;font-size:15px !important;padding:14px 32px !important;box-shadow:0 2px 8px rgba(0,113,227,.3) !important;transition:background .15s,box-shadow .15s,transform .1s !important}
    31 .cc-btn-accept:hover{background:#005bb5 !important;box-shadow:0 4px 16px rgba(0,113,227,.35) !important;color:#fff !important}
    32 .cc-btn-decline{background:#e8e8ed !important;color:#1d1d1f !important;font-size:13px !important;padding:11px 22px !important;box-shadow:none !important;transition:background .15s,transform .1s !important}
    33 .cc-btn-decline:hover{background:#d2d2d7 !important;color:#1d1d1f !important}
    34 .cc-btn-save{background:#34c759 !important;color:#fff !important;font-size:14px !important;padding:13px 28px !important;box-shadow:0 2px 8px rgba(52,199,89,.3) !important;transition:background .15s,box-shadow .15s,transform .1s !important}
    35 .cc-btn-save:hover{background:#28a745 !important;box-shadow:0 4px 16px rgba(52,199,89,.35) !important;color:#fff !important}
    36 .cc-btn-details{background:transparent !important;color:#555 !important;padding:11px 4px;font-weight:500;font-size:13px;text-decoration:none !important;border:none !important;box-shadow:none !important;cursor:pointer;transition:color .15s !important}
    37 .cc-btn-details:hover{color:#555 !important;text-decoration:none !important;background:transparent !important;box-shadow:none !important}
    38 .cc-categories{margin:0 0 16px !important;border:1px solid #e8e8ed !important;border-radius:14px !important;overflow:hidden !important;display:none}
     33.cc-btn-accept{background:var(--cc-accent) !important;color:var(--cc-accent-text) !important;font-size:15px !important;padding:14px 32px !important;box-shadow:0 2px 8px var(--cc-accent-shadow) !important;transition:background .15s,box-shadow .15s,transform .1s !important}
     34.cc-btn-accept:hover{background:var(--cc-accent-hover) !important;box-shadow:0 4px 16px var(--cc-accent-shadow-hover) !important;color:var(--cc-accent-text) !important}
     35.cc-btn-decline{background:var(--cc-secondary-bg) !important;color:var(--cc-secondary-text) !important;font-size:13px !important;padding:11px 22px !important;box-shadow:none !important;transition:background .15s,transform .1s !important}
     36.cc-btn-decline:hover{background:var(--cc-secondary-hover) !important;color:var(--cc-secondary-text) !important}
     37.cc-btn-reject{background:var(--cc-secondary-bg) !important;color:var(--cc-secondary-text) !important;font-size:15px !important;padding:14px 32px !important;transition:background .15s,transform .1s !important}
     38.cc-btn-reject:hover{background:var(--cc-secondary-hover) !important;color:var(--cc-secondary-text) !important}
     39.cc-btn-save{background:var(--cc-save-bg) !important;color:var(--cc-save-text) !important;font-size:14px !important;padding:13px 28px !important;box-shadow:0 2px 8px var(--cc-save-shadow) !important;transition:background .15s,box-shadow .15s,transform .1s !important}
     40.cc-btn-save:hover{background:var(--cc-save-hover) !important;box-shadow:0 4px 16px var(--cc-save-shadow-hover) !important;color:var(--cc-save-text) !important}
     41.cc-btn-details{background:transparent !important;color:var(--cc-details) !important;padding:11px 4px;font-weight:500;font-size:13px;text-decoration:none !important;border:none !important;box-shadow:none !important;cursor:pointer;transition:color .15s !important}
     42.cc-btn-details:hover{color:var(--cc-details-hover) !important;text-decoration:none !important;background:transparent !important;box-shadow:none !important}
     43.cc-categories{margin:0 0 16px !important;border:1px solid var(--cc-border) !important;border-radius:14px !important;overflow:hidden !important;display:none}
    3944.cc-categories.cc-open{display:block !important}
    40 .cc-cat{padding:16px 20px !important;border-bottom:1px solid #f0f0f2 !important;display:flex !important;justify-content:space-between !important;align-items:center !important;gap:16px !important;background:none !important}
     45.cc-cat{padding:16px 20px !important;border-bottom:1px solid var(--cc-divider) !important;display:flex !important;justify-content:space-between !important;align-items:center !important;gap:16px !important;background:none !important}
    4146.cc-cat:last-child{border-bottom:none !important}
    4247.cc-cat-label{font-weight:700 !important;font-size:14px !important;line-height:1.3 !important;color:inherit !important;text-transform:none !important}
    43 .cc-cat-desc{font-size:12px !important;color:#646970 !important;margin-top:3px !important;font-weight:400 !important;line-height:1.4 !important;text-transform:none !important}
    44 .cc-always-on{font-weight:400 !important;font-size:11px !important;color:#767676 !important}
     48.cc-cat-desc{font-size:12px !important;color:var(--cc-cat-desc) !important;margin-top:3px !important;font-weight:400 !important;line-height:1.4 !important;text-transform:none !important}
     49.cc-always-on{font-weight:400 !important;font-size:11px !important;color:var(--cc-always-on) !important}
     50.cc-gpc-note{font-size:12px !important;color:var(--cc-text) !important;margin-top:6px !important;font-weight:600 !important;line-height:1.4 !important;text-transform:none !important}
    4551.cc-toggle{position:relative !important;width:46px !important;height:26px !important;flex-shrink:0 !important;display:block !important}
    4652.cc-toggle input{opacity:0 !important;width:0 !important;height:0 !important;position:absolute !important}
    47 .cc-toggle .cc-slider{position:absolute !important;inset:0 !important;background:#ccc !important;border-radius:26px !important;cursor:pointer !important;transition:background .2s !important;border:none !important;box-shadow:none !important}
    48 .cc-toggle .cc-slider:before{content:"" !important;position:absolute !important;width:20px !important;height:20px !important;left:3px !important;bottom:3px !important;background:#fff !important;border-radius:50% !important;transition:transform .2s !important;box-shadow:0 1px 3px rgba(0,0,0,.15) !important}
    49 .cc-toggle input:checked+.cc-slider{background:#0071e3 !important}
     53.cc-toggle .cc-slider{position:absolute !important;inset:0 !important;background:var(--cc-toggle-off) !important;border-radius:26px !important;cursor:pointer !important;transition:background .2s !important;border:none !important;box-shadow:none !important}
     54.cc-toggle .cc-slider:before{content:"" !important;position:absolute !important;width:20px !important;height:20px !important;left:3px !important;bottom:3px !important;background:var(--cc-knob) !important;border-radius:50% !important;transition:transform .2s !important;box-shadow:0 1px 3px rgba(0,0,0,.15) !important}
     55.cc-toggle input:checked+.cc-slider{background:var(--cc-toggle-on) !important}
    5056.cc-toggle input:checked+.cc-slider:before{transform:translateX(20px) !important}
    51 .cc-toggle input:disabled+.cc-slider{background:#0071e3 !important;opacity:.5 !important;cursor:default !important}
    52 .cc-toggle input:focus-visible+.cc-slider{outline:2px solid #0071e3 !important;outline-offset:2px !important}
    53 .cc-powered{text-align:center !important;padding:0 0 16px !important;font-size:11px !important;color:#999 !important;font-weight:400 !important;background:none !important;border:none !important;margin:0 !important}
     57.cc-toggle input:disabled+.cc-slider{background:var(--cc-toggle-on) !important;opacity:.5 !important;cursor:default !important}
     58.cc-toggle input:focus-visible+.cc-slider{outline:2px solid var(--cc-focus) !important;outline-offset:2px !important}
     59.cc-powered{text-align:center !important;padding:0 0 16px !important;font-size:11px !important;color:var(--cc-powered) !important;font-weight:400 !important;background:none !important;border:none !important;margin:0 !important}
    5460.cc-pref-buttons{display:none !important;gap:10px;flex-wrap:wrap;align-items:center;margin-bottom:8px}
    5561.cc-pref-buttons.cc-open{display:flex !important}
    56 .cc-powered a{color:#999 !important;text-decoration:none !important;font-weight:400 !important;background:none !important;border:none !important;box-shadow:none !important}
    57 .cc-powered a:hover{color:#666 !important;text-decoration:none !important}
    58 #cc-icon{position:fixed !important;bottom:18px !important;left:18px !important;z-index:999997 !important;width:48px !important;height:48px !important;border-radius:50% !important;background:#1d1d1f !important;color:#fff !important;border:none !important;cursor:pointer !important;box-shadow:0 2px 12px rgba(0,0,0,.25) !important;display:none;align-items:center !important;justify-content:center !important;padding:0 !important;transition:transform .15s,box-shadow .15s !important;-webkit-appearance:none !important;appearance:none !important;margin:0 !important;text-decoration:none !important;outline:none !important}
    59 #cc-icon:hover{transform:scale(1.1) !important;box-shadow:0 4px 20px rgba(0,0,0,.35) !important}
    60 #cc-icon:focus-visible{outline:2px solid #0071e3 !important;outline-offset:2px !important}
     62.cc-powered a{color:var(--cc-powered) !important;text-decoration:none !important;font-weight:400 !important;background:none !important;border:none !important;box-shadow:none !important}
     63.cc-powered a:hover{color:var(--cc-powered-hover) !important;text-decoration:none !important}
     64#cc-icon{position:fixed !important;bottom:18px !important;left:18px !important;z-index:999997 !important;width:48px !important;height:48px !important;border-radius:50% !important;background:var(--cc-icon-bg) !important;color:var(--cc-icon-text) !important;border:none !important;cursor:pointer !important;box-shadow:0 2px 12px var(--cc-icon-shadow) !important;display:none;align-items:center !important;justify-content:center !important;padding:0 !important;transition:transform .15s,box-shadow .15s !important;-webkit-appearance:none !important;appearance:none !important;margin:0 !important;text-decoration:none !important;outline:none !important}
     65#cc-icon:hover{transform:scale(1.1) !important;box-shadow:0 4px 20px var(--cc-icon-shadow-hover) !important}
     66#cc-icon:focus-visible{outline:2px solid var(--cc-focus) !important;outline-offset:2px !important}
    6167#cc-icon.cc-show{display:flex !important}
    6268.cc-icon-right #cc-icon{left:auto !important;right:18px !important}
    63 #cc-ccpa-banner{position:fixed;bottom:0;left:0;right:0;z-index:999999;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;font-size:14px;line-height:1.5;color:#1d1d1f;background:#fff;box-shadow:0 -8px 40px rgba(0,0,0,.15);transform:translateY(100%);transition:transform .4s cubic-bezier(.4,0,.2,1);border-radius:20px 20px 0 0}
     69#cc-ccpa-banner{position:fixed;bottom:0;left:0;right:0;z-index:999999;font-family:var(--cc-font);font-size:14px;line-height:1.5;color:var(--cc-text);background:var(--cc-bg);box-shadow:0 -8px 40px var(--cc-banner-shadow);transform:translateY(100%);transition:transform .4s cubic-bezier(.4,0,.2,1);border-radius:var(--cc-radius) var(--cc-radius) 0 0;max-height:90vh;overflow-y:auto}
    6470#cc-ccpa-banner.cc-show{transform:translateY(0)}
    6571#cc-ccpa-banner *{box-sizing:border-box}
    6672.cc-ccpa-inner{max-width:720px;margin:0 auto;padding:20px 32px;display:flex;align-items:center;gap:20px;flex-wrap:wrap}
    6773.cc-ccpa-text{flex:1;min-width:280px}
    68 .cc-ccpa-text p{margin:0;color:#555}
     74.cc-ccpa-text p{margin:0;color:var(--cc-text-muted)}
     75.cc-ccpa-text p.cc-ccpa-gpc{margin:8px 0 0;color:var(--cc-text);font-weight:600}
     76.cc-ccpa-gpc:before{content:"\2713\00a0"}
    6977.cc-ccpa-actions{display:flex;gap:10px;align-items:center;flex-shrink:0}
    70 .cc-ccpa-link{color:#0071e3;text-decoration:underline;font-size:13px;font-weight:500;white-space:nowrap}
    71 .cc-dark #cc-banner,.cc-dark #cc-ccpa-banner{background:#1c1c1e !important;color:#f5f5f7 !important;box-shadow:0 -8px 40px rgba(0,0,0,.5) !important}
    72 .cc-dark .cc-text{color:#98989d !important}
    73 .cc-dark .cc-text a,.cc-dark .cc-ccpa-link{color:#2997ff !important}
    74 .cc-dark .cc-btn-details{color:#98989d !important}
    75 .cc-dark .cc-btn-details:hover{color:#c7c7cc !important}
    76 .cc-dark .cc-btn-decline{background:#2c2c2e !important;color:#f5f5f7 !important}
    77 .cc-dark .cc-btn-decline:hover{background:#3a3a3c !important;color:#f5f5f7 !important}
    78 .cc-dark .cc-categories{border-color:#2c2c2e !important}
    79 .cc-dark .cc-cat{border-bottom-color:#2c2c2e !important}
    80 .cc-dark .cc-cat-desc{color:#8e8e93 !important}
    81 .cc-dark .cc-always-on{color:#8e8e93 !important}
    82 .cc-dark .cc-toggle .cc-slider{background:#3a3a3c !important}
    83 .cc-dark .cc-toggle .cc-slider:before{background:#f5f5f7 !important}
    84 .cc-dark .cc-toggle input:checked+.cc-slider{background:#0a84ff !important}
    85 .cc-dark .cc-toggle input:disabled+.cc-slider{background:#0a84ff !important}
    86 .cc-dark .cc-btn-accept{background:#0a84ff !important;box-shadow:0 2px 8px rgba(10,132,255,.35) !important}
    87 .cc-dark .cc-btn-accept:hover{background:#0071e3 !important;box-shadow:0 4px 16px rgba(10,132,255,.4) !important}
    88 .cc-dark .cc-btn-save{background:#30d158 !important;box-shadow:0 2px 8px rgba(48,209,88,.3) !important}
    89 .cc-dark .cc-btn-save:hover{background:#28a745 !important;box-shadow:0 4px 16px rgba(48,209,88,.35) !important}
    90 .cc-dark .cc-ccpa-text p{color:#98989d !important}
    91 .cc-dark .cc-powered{color:#aaa !important}
    92 .cc-dark .cc-powered a{color:#aaa !important}
    93 .cc-dark .cc-powered a:hover{color:#ddd !important}
    94 .cc-dark .cc-cat-label{color:#f5f5f7 !important}
    95 .cc-dark .cc-title{color:#f5f5f7 !important}
    96 .cc-dark #cc-icon{background:#f5f5f7 !important;color:#1d1d1f !important;box-shadow:0 2px 12px rgba(255,255,255,.15) !important}
    97 .cc-dark #cc-icon:hover{box-shadow:0 4px 20px rgba(255,255,255,.25) !important}
    98 @media(max-width:480px){.cc-inner{padding:24px 20px 16px}.cc-title{font-size:18px}.cc-buttons{gap:8px}.cc-btn-accept{font-size:14px;padding:12px 24px}.cc-btn-decline{font-size:12px;padding:10px 18px}}
     78.cc-ccpa-link{color:var(--cc-link);text-decoration:underline;font-size:13px;font-weight:500;white-space:nowrap}
     79#cc-ccpa-banner .cc-btn-details{padding:11px 0}
     80.cc-ccpa-choice{display:none}
     81.cc-ccpa-choice.cc-open{display:block}
     82.cc-ccpa-choice-inner{max-width:720px;margin:0 auto;padding:20px 32px}
     83.cc-ccpa-choice-title{font-size:16px;font-weight:800;line-height:1.3;margin:0 0 6px;color:inherit}
     84.cc-ccpa-choice-text,.cc-ccpa-choice-note{margin:0 0 14px;color:var(--cc-text-muted)}
     85.cc-ccpa-choice-status{margin:0 0 14px;font-weight:600;color:var(--cc-text)}
     86.cc-ccpa-choice-note{font-size:12px;margin:10px 0 0}
     87.cc-ccpa-choice-buttons{display:flex;gap:10px;flex-wrap:wrap;align-items:center}
     88@media(max-width:480px){.cc-inner{padding:24px 20px 16px}.cc-title{font-size:18px}.cc-buttons{gap:8px}.cc-btn-accept{font-size:14px;padding:12px 24px}.cc-btn-decline{font-size:12px;padding:10px 18px}.cc-ccpa-inner{padding:16px 20px;gap:12px}.cc-ccpa-text{min-width:0;flex:1 1 100%}.cc-ccpa-actions{flex-wrap:wrap;flex-shrink:1;width:100%;justify-content:space-between}.cc-ccpa-link{white-space:normal}.cc-ccpa-choice-inner{padding:16px 20px}}
  • cookieflame/tags/5.4.0/assets/js/admin.js

    r3769546 r3771537  
    1010    cc_anon_id:     { vendor: "CookieFlame",     category: "necessary",   purpose: "Anonymous ID for consent logging" },
    1111    cookieflame_dnt:       { vendor: "CookieFlame",     category: "necessary",   purpose: "Do-not-track opt-out flag" },
    12     "wp-settings-": { vendor: "WordPress",         category: "necessary",   purpose: "Admin interface preferences", prefix: true },
    13     "wp-settings-time-": { vendor: "WordPress",    category: "necessary",   purpose: "Admin settings timestamp", prefix: true },
    14     wordpress_logged_in_: { vendor: "WordPress",   category: "necessary",   purpose: "Login session", prefix: true },
    15     wordpress_sec_: { vendor: "WordPress",         category: "necessary",   purpose: "Secure login session", prefix: true },
    16     wordpress_test_cookie: { vendor: "WordPress",  category: "necessary",   purpose: "Checks cookie support" },
    17     wp_lang:        { vendor: "WordPress",         category: "necessary",   purpose: "Language preference" },
     12    "wp-settings-": { vendor: "WordPress",         category: "necessary",   purpose: "Remembers dashboard layout settings", prefix: true },
     13    "wp-settings-time-": { vendor: "WordPress",    category: "necessary",   purpose: "Records when dashboard settings were last saved", prefix: true },
     14    wordpress_logged_in_: { vendor: "WordPress",   category: "necessary",   purpose: "Keeps you logged in", prefix: true },
     15    wordpress_sec_: { vendor: "WordPress",         category: "necessary",   purpose: "Confirms your login when you open the dashboard", prefix: true },
     16    wordpress_rec_: { vendor: "WordPress",         category: "necessary",   purpose: "Recovery mode after a site error (administrators only)", prefix: true },
     17    wordpress_test_cookie: { vendor: "WordPress",  category: "necessary",   purpose: "Checks your browser accepts cookies before you log in" },
     18    wp_lang:        { vendor: "WordPress",         category: "necessary",   purpose: "Remembers the language picked on the login page" },
     19    comment_author_: { vendor: "WordPress",        category: "necessary",   purpose: "Remembers your name for comments, if you ask it to", prefix: true },
     20    comment_author_email_: { vendor: "WordPress",  category: "necessary",   purpose: "Remembers your email for comments, if you ask it to", prefix: true },
     21    comment_author_url_: { vendor: "WordPress",    category: "necessary",   purpose: "Remembers your website for comments, if you ask it to", prefix: true },
     22    "wp-postpass_": { vendor: "WordPress",         category: "necessary",   purpose: "Remembers the password entered for a protected page", prefix: true },
    1823    woocommerce_cart_hash: { vendor: "WooCommerce", category: "necessary",  purpose: "Shopping cart contents" },
    1924    woocommerce_items_in_cart: { vendor: "WooCommerce", category: "necessary", purpose: "Cart item count" },
    … …  
    181186  function identifyBuiltIn(name) {
    182187    if (KNOWN[name]) return KNOWN[name];
    183     var match = null;
     188    // Longest prefix wins (wp-settings-time- over wp-settings-).
     189    var match = null, matchLen = 0;
    184190    var keys = Object.keys(KNOWN);
    185191    for (var i = 0; i < keys.length; i++) {
    186       if (KNOWN[keys[i]].prefix && name.indexOf(keys[i]) === 0) { match = KNOWN[keys[i]]; break; }
     192      if (KNOWN[keys[i]].prefix && keys[i].length > matchLen && name.indexOf(keys[i]) === 0) { match = KNOWN[keys[i]]; matchLen = keys[i].length; }
    187193    }
    188194    return match;
    … …  
    320326  }
    321327
    322   // WordPress core vendors that are always necessary
    323   var WP_CORE_VENDORS = ["WordPress", "WooCommerce", "PHP"];
     328  // Vendors listed under WordPress Core Cookies (PHP sessions and
     329  // WooCommerce are not core, so they appear in the main table).
     330  var WP_CORE_VENDORS = ["WordPress"];
    324331  var PLUGIN_VENDOR = "CookieFlame";
    325332
    … …  
    23542361    .then(function (data) {
    23552362      if (!data.success) {
    2356         if (startBtn) { startBtn.disabled = false; startBtn.textContent = "Start Cookie Discovery"; }
     2363        if (startBtn) { startBtn.disabled = false; startBtn.textContent = "Start Cookie Discovery (uses your own browser)"; }
    23572364        return;
    23582365      }
    … …  
    24052412    })
    24062413    .catch(function (err) {
    2407       if (startBtn) { startBtn.disabled = false; startBtn.textContent = "Start Cookie Discovery"; }
     2414      if (startBtn) { startBtn.disabled = false; startBtn.textContent = "Start Cookie Discovery (uses your own browser)"; }
    24082415      cookieflameNotice("Error: " + err.message, "error");
    24092416    });
  • cookieflame/tags/5.4.0/assets/js/cookieconsent.js

    r3769546 r3771537  
    11/**
    2  * CookieFlame v5.3.0
     2 * CookieFlame v5.4.0
    33 * ─────────────────────────────────────────────────────────────
    44 * Self-hosted cookie consent manager with Google Consent Mode v2,
    5  * IAB TCF v2.2, automatic script blocking, iframe facades with
     5 * automatic script blocking, iframe facades with
    66 * thumbnails, Elementor video widget support, multi-region
    77 * compliance (GDPR/CCPA), and passive cookie monitoring.
    … …  
    4040  } catch (e) {}
    4141
    42   var PLUGIN_VERSION = "5.3.0";
     42  var PLUGIN_VERSION = "5.4.0";
    4343
    4444  /* ═══════════════════════════════════════════
    … …  
    4949    cookieExpiry:   365,
    5050    privacyUrl:     "/privacy-policy",
    51     ccpaUrl:        "/privacy-policy#do-not-sell",
    5251    geoApi:         "",
    5352    logEndpoint:    "",
    5453    reconsentDays:  365,
    5554    cspSafe:        false,
    56     tcfEnabled:     true,
     55    // IAB TCF v2.2 stub. Off since v5.4.0 (it never carried a TC string);
     56    // the code is kept and can be switched on through the
     57    // cookieflame_frontend_config filter (tcfEnabled: true).
     58    tcfEnabled:     false,
    5759    cmpId:          1,
    5860    cmpVersion:     1,
    … …  
    6567    iconPosition:   "left",
    6668    consentVersion: "",
     69    // Show "Decline All" next to "Accept All" on the GDPR banner's first
     70    // screen (Settings > Show "Decline all" on the first screen).
     71    rejectFirstScreen: false,
     72    // List WordPress login cookies in the declaration under "If you log
     73    // in" (Settings > Visitor logins). Login cookies are never listed
     74    // with the others, so the declaration is the same for every visitor.
     75    visitorLogins: false,
    6776
    6877    autoBlockPatterns: [
    … …  
    122131      description:        'We use cookies to improve your experience, analyse traffic and personalise content. You can choose which categories to allow.',
    123132      acceptAllButton:    "Accept All",
     133      rejectAllButton:    "Decline All",
    124134      declineAllButton:   "Decline All",
    125135      saveButton:         "Save Preferences",
    … …  
    128138      alwaysOnLabel:      "(always on)",
    129139      ccpaTitle:          "Your Privacy Choices",
    130       ccpaDescription:    'We use cookies and similar technologies. Under California law, you have the right to opt out of the sale or sharing of your personal information.',
     140      ccpaDescription:    "We use cookies and similar technologies. Under US state privacy laws, such as California's, you have the right to opt out of the sale or sharing of your personal information.",
    131141      ccpaAcceptButton:   "OK",
    132       ccpaDnsmpiLink:     "Do Not Sell My Personal Information"
     142      ccpaDnsmpiLink:     "Do Not Sell or Share My Personal Information",
     143      ccpaGpcNotice:      "Your browser asked us not to sell or share your information. We're honouring that.",
     144      ccpaChoiceTitle:    "Do Not Sell or Share My Personal Information",
     145      ccpaChoiceText:     "Information about your visit may be used by advertising partners whose cookies run on this site. Under US state privacy laws that can count as selling or sharing. You can opt out in this browser.",
     146      ccpaOptOutButton:   "Opt out",
     147      ccpaCancelButton:   "Cancel",
     148      ccpaOptedOut:       "You've opted out of the sale or sharing of your personal information in this browser.",
     149      ccpaGpcOptedOut:    "Your browser's Global Privacy Control signal asked us not to sell or share your information, so you're opted out in this browser.",
     150      ccpaDoneButton:     "Done",
     151      ccpaOptInButton:    "Allow selling or sharing",
     152      ccpaOptInGpcNote:   "Allowing it overrides your browser's Global Privacy Control signal on this site only.",
     153      ccpaMoreSettings:   "More cookie settings",
     154      gpcPrefNote:        "Your browser's Global Privacy Control signal is on, so Marketing stays off unless you switch it on here.",
     155      changePreferences:  "Change cookie preferences",
     156      loginCookiesTitle:  "If you log in",
     157      loginCookiesIntro:  "These cookies are only set if you log in to an account on this site. They keep you signed in and remember your account settings. They're needed for logging in to work, so they don't ask for your consent."
    133158    },
    134159
    … …  
    181206    var ext = window.__ccConfig;
    182207    // shallow merge top-level primitives
    183     var keys = ["cookieName","cookieExpiry","privacyUrl","ccpaUrl","geoApi",
     208    var keys = ["cookieName","cookieExpiry","privacyUrl","geoApi",
    184209      "logEndpoint","reconsentDays","cspSafe","tcfEnabled","cmpId",
    185       "cmpVersion","vendorListVersion","storageMethod","cookieDomain","shareSubdomains","theme","showPoweredBy","iconPosition","consentVersion"];
     210      "cmpVersion","vendorListVersion","storageMethod","cookieDomain","shareSubdomains","theme","showPoweredBy","iconPosition","consentVersion",
     211      "rejectFirstScreen","visitorLogins"];
    186212    keys.forEach(function (k) {
    187213      if (ext[k] !== undefined) CONFIG[k] = ext[k];
    … …  
    207233  // visitor's browser is sending the GPC signal.
    208234  var gpcActive = !!window.__ccGPC;
     235
     236  /* GPC rules (Cal. Code Regs. tit. 11 §7025, applied in every region):
     237     - A GPC signal is a valid opt-out of selling and sharing: Marketing
     238       stays off and Google's ad signals are denied, with no extra steps.
     239     - The opt-out is saved in the consent cookie (state.gpc), so a later
     240       visit without GPC keeps it.
     241     - The visitor can opt back in only through a separate choice: the
     242       US notice's "Allow selling or sharing" button, or switching
     243       Marketing on and saving in the preferences. That sets
     244       state.gpcOptIn. OK and Accept All never opt back in. */
     245  function applyGpc(state) {
     246    if (gpcActive && state && !state.gpcOptIn) {
     247      state.marketing = false;
     248      state.gpc = 1;
     249    }
     250    return state;
     251  }
    209252
    210253  // Merged blocking rules from PHP (built-in defaults + auto-generated
    … …  
    227270    cc_anon_id:     { vendor: "CookieFlame",     category: "necessary",   purpose: "Anonymous ID for consent logging", expiry: "1 year" },
    228271    cookieflame_dnt:       { vendor: "CookieFlame",     category: "necessary",   purpose: "Do-not-track opt-out flag", expiry: "1 year" },
     272    // PHP sessions (started by some plugins and hosts, not by WordPress)
     273    PHPSESSID:      { vendor: "PHP",              category: "necessary",   purpose: "Server-side session identifier", expiry: "Session" },
    229274    // WordPress core
    230     PHPSESSID:      { vendor: "PHP",              category: "necessary",   purpose: "Server-side session identifier", expiry: "Session" },
    231     wp_settings_:   { vendor: "WordPress",         category: "necessary",   purpose: "Admin interface preferences", prefix: true, expiry: "1 year" },
    232     "wp-settings-": { vendor: "WordPress",         category: "necessary",   purpose: "Admin interface preferences", prefix: true, expiry: "1 year" },
    233     "wp-settings-time-": { vendor: "WordPress",    category: "necessary",   purpose: "Admin settings timestamp", prefix: true, expiry: "1 year" },
    234     wordpress_logged_in_: { vendor: "WordPress",   category: "necessary",   purpose: "Login session", prefix: true, expiry: "Session" },
    235     wordpress_sec_: { vendor: "WordPress",         category: "necessary",   purpose: "Secure login session", prefix: true, expiry: "Session" },
    236     wordpress_test_cookie: { vendor: "WordPress",  category: "necessary",   purpose: "Checks cookie support", expiry: "Session" },
    237     wp_lang:        { vendor: "WordPress",         category: "necessary",   purpose: "Language preference", expiry: "Session" },
     275    "wp-settings-": { vendor: "WordPress",         category: "necessary",   purpose: "Remembers dashboard layout settings", prefix: true, expiry: "1 year" },
     276    "wp-settings-time-": { vendor: "WordPress",    category: "necessary",   purpose: "Records when dashboard settings were last saved", prefix: true, expiry: "1 year" },
     277    wordpress_logged_in_: { vendor: "WordPress",   category: "necessary",   purpose: "Keeps you logged in", prefix: true, expiry: "Session, or 14 days with Remember Me" },
     278    wordpress_sec_: { vendor: "WordPress",         category: "necessary",   purpose: "Confirms your login when you open the dashboard", prefix: true, expiry: "Session, or 14 days with Remember Me" },
     279    wordpress_rec_: { vendor: "WordPress",         category: "necessary",   purpose: "Recovery mode after a site error (administrators only)", prefix: true, expiry: "1 week" },
     280    wordpress_test_cookie: { vendor: "WordPress",  category: "necessary",   purpose: "Checks your browser accepts cookies before you log in", expiry: "Session" },
     281    wp_lang:        { vendor: "WordPress",         category: "necessary",   purpose: "Remembers the language picked on the login page", expiry: "Session" },
     282    comment_author_: { vendor: "WordPress",        category: "necessary",   purpose: "Remembers your name for comments, if you ask it to", prefix: true, expiry: "1 year" },
     283    comment_author_email_: { vendor: "WordPress",  category: "necessary",   purpose: "Remembers your email for comments, if you ask it to", prefix: true, expiry: "1 year" },
     284    comment_author_url_: { vendor: "WordPress",    category: "necessary",   purpose: "Remembers your website for comments, if you ask it to", prefix: true, expiry: "1 year" },
     285    "wp-postpass_": { vendor: "WordPress",         category: "necessary",   purpose: "Remembers the password entered for a protected page", prefix: true, expiry: "10 days" },
    238286    // WooCommerce
    239287    woocommerce_cart_hash: { vendor: "WooCommerce", category: "necessary",  purpose: "Shopping cart contents", expiry: "Session" },
    … …  
    272320    if (match) return match;
    273321
    274     // Prefix match in built-in database
     322    // Prefix match in built-in database: the longest prefix wins, so
     323    // wp-settings-time-1 is not taken for a wp-settings- cookie.
     324    var matchLen = 0;
    275325    Object.keys(KNOWN_COOKIES).forEach(function (key) {
    276       if (!match && KNOWN_COOKIES[key].prefix && name.indexOf(key) === 0) {
     326      if (KNOWN_COOKIES[key].prefix && key.length > matchLen && name.indexOf(key) === 0) {
    277327        match = KNOWN_COOKIES[key];
     328        matchLen = key.length;
    278329      }
    279330    });
    280331
    281332    return match || null;
     333  }
     334
     335  // Cookies WordPress sets when someone logs in. The declaration never
     336  // lists the ones in the viewer's own browser; with visitorLogins on it
     337  // lists all of them under "If you log in" instead. wordpress_[hash] is
     338  // the login cookie on sites without HTTPS, wordpress_rec_ is admin-only.
     339  var LOGIN_COOKIE_RE = /^(?:wordpress_(?:logged_in_|sec_|rec_|test_cookie$|[0-9a-f]{32}$)|wp-settings-|wp_lang$)/;
     340
     341  function isLoginCookie(name) {
     342    return LOGIN_COOKIE_RE.test(name);
     343  }
     344
     345  function loginCookieRows() {
     346    var keep = "Session, or 14 days with Remember Me";
     347    return [
     348      { name: "wordpress_logged_in_[hash]", purpose: "Keeps you logged in", expiry: keep },
     349      { name: location.protocol === "https:" ? "wordpress_sec_[hash]" : "wordpress_[hash]", purpose: "Confirms your login when you open your account dashboard", expiry: keep },
     350      { name: "wordpress_test_cookie", purpose: "Checks your browser accepts cookies before you log in", expiry: "Session" },
     351      { name: "wp_lang", purpose: "Remembers the language you picked on the login page", expiry: "Session" },
     352      { name: "wp-settings-[user ID]", purpose: "Remembers your dashboard layout settings", expiry: "1 year" },
     353      { name: "wp-settings-time-[user ID]", purpose: "Records when your dashboard settings were last saved", expiry: "1 year" }
     354    ];
    282355  }
    283356
    … …  
    410483    // Connecticut, Montana, Oregon, Texas, Delaware, Iowa, Nebraska,
    411484    // New Hampshire, New Jersey, Minnesota, plus others pending).
    412     if (gpcActive) {
     485    if (gpcActive && !state.gpcOptIn) {
    413486      update.ad_storage = "denied";
    414487      update.ad_user_data = "denied";
    … …  
    666739  ];
    667740
     741  // Patterns match whole hostnames (the host itself or a subdomain of
     742  // it), plus an optional path prefix: "x.com" matches x.com and
     743  // www.x.com but not netflix.com or box.com.
     744  function iframePatternMatches(src, pattern) {
     745    var host, path;
     746    try {
     747      var u = new URL(src, location.href);
     748      host = u.hostname.toLowerCase();
     749      path = u.pathname.toLowerCase();
     750    } catch (e) { return false; }
     751    var slash = pattern.indexOf("/");
     752    var pHost = slash === -1 ? pattern : pattern.slice(0, slash);
     753    var pPath = slash === -1 ? "" : pattern.slice(slash);
     754    if (host !== pHost && host.slice(-pHost.length - 1) !== "." + pHost) return false;
     755    return !pPath || path.indexOf(pPath) === 0;
     756  }
     757
    668758  function getIframeBlockCategory(src) {
    669759    if (!src) return null;
    670     var lower = src.toLowerCase();
    671760    for (var i = 0; i < IFRAME_BLOCK_PATTERNS.length; i++) {
    672       if (lower.indexOf(IFRAME_BLOCK_PATTERNS[i].pattern) !== -1) {
     761      if (iframePatternMatches(src, IFRAME_BLOCK_PATTERNS[i].pattern)) {
    673762        return IFRAME_BLOCK_PATTERNS[i].category;
    674763      }
    … …  
    838927      var src = iframe.src || "";
    839928      if (!src) continue;
    840       for (var j = 0; j < IFRAME_BLOCK_PATTERNS.length; j++) {
    841         if (src.indexOf(IFRAME_BLOCK_PATTERNS[j].pattern) !== -1) {
    842           iframe.src = src;
    843           break;
    844         }
    845       }
     929      if (getIframeBlockCategory(src)) iframe.src = src;
    846930    }
    847931  }
    … …  
    896980  function saveState(state, action) {
    897981    var prev = consentState;
     982    // A click on an embed's placeholder loads that embed now; the GPC
     983    // opt-out is saved again on the next page view (boot()).
     984    if (!(action && action.indexOf("facade_accept_") === 0)) applyGpc(state);
    898985    consentState = state;
    899986    storageSet(CONFIG.cookieName, JSON.stringify(state), CONFIG.cookieExpiry);
    … …  
    9581045      if (!s || typeof s !== "object" || Array.isArray(s)) return null;
    9591046      if (!s.ts || isNaN(new Date(s.ts).getTime())) return null;
    960       // Check re-consent expiry
     1047      // Check re-consent expiry. A period below 1 day means the default.
     1048      var days = Number(CONFIG.reconsentDays);
     1049      if (!(days >= 1)) days = 365;
    9611050      var age = (Date.now() - new Date(s.ts).getTime()) / 864e5;
    962       if (age > CONFIG.reconsentDays) return null;
     1051      if (age > days) return null;
    9631052      // Consent version check: if the site owner has clicked
    9641053      // "Invalidate All Consent", the version string will not
    … …  
    11831272      var alwaysOn = t.required
    11841273        ? ' <span class="cc-always-on">' + escHtml(txt.alwaysOnLabel) + '</span>' : "";
     1274      var gpcNote = gpcActive && t.id === "marketing"
     1275        ? '<div class="cc-gpc-note">' + escHtml(txt.gpcPrefNote) + '</div>' : "";
    11851276      return '<div class="cc-cat">' +
    11861277        '<div><div class="cc-cat-label">' + escHtml(t.name) + alwaysOn + '</div>' +
    1187         '<div class="cc-cat-desc">' + escHtml(t.description || "") + '</div></div>' +
     1278        '<div class="cc-cat-desc">' + escHtml(t.description || "") + '</div>' + gpcNote + '</div>' +
    11881279        '<label class="cc-toggle"><input type="checkbox" id="cc-cb-' + escHtml(t.id) + '"' +
    11891280          ' aria-label="' + escHtml(t.name) + '"' +
    … …  
    12001291        '<div class="cc-buttons">' +
    12011292          '<button class="cc-btn cc-btn-accept" id="cc-accept">' + escHtml(txt.acceptAllButton) + '</button>' +
     1293          (CONFIG.rejectFirstScreen ? '<button class="cc-btn cc-btn-reject" id="cc-reject">' + escHtml(txt.rejectAllButton) + '</button>' : '') +
    12021294          '<button class="cc-btn cc-btn-details" id="cc-details" aria-expanded="false">' + escHtml(txt.showDetailsButton) + '</button>' +
    12031295        '</div>' +
    … …  
    12211313    document.getElementById("cc-accept").addEventListener("click", acceptAll);
    12221314    document.getElementById("cc-decline").addEventListener("click", declineAll);
     1315    var rejectBtn = document.getElementById("cc-reject");
     1316    if (rejectBtn) rejectBtn.addEventListener("click", declineAll);
    12231317    document.getElementById("cc-save").addEventListener("click", savePrefs);
    12241318    document.getElementById("cc-details").addEventListener("click", toggleDetails);
    12251319    icon.addEventListener("click", function () { showBanner("gdpr"); });
    12261320    // No overlay click handler — banner must be dismissed via buttons only
     1321    emitRendered("gdpr", banner);
     1322  }
     1323
     1324  // cc:banner-rendered fires once each banner's markup is in the page
     1325  // (before it is shown), so other code can adjust it.
     1326  // detail: { banner: "gdpr" | "ccpa", element, state }
     1327  function emitRendered(type, el) {
     1328    emit("cc:banner-rendered", { banner: type, element: el, state: getPublicState() });
    12271329  }
    12281330
    … …  
    12311333  ═══════════════════════════════════════════ */
    12321334  function buildCcpaBanner() {
     1335    if (document.getElementById("cc-ccpa-banner")) return;
    12331336    var txt = CONFIG.text;
    12341337    var banner = document.createElement("div");
    … …  
    12401343
    12411344    banner.innerHTML =
    1242       '<div class="cc-ccpa-inner">' +
    1243         '<div class="cc-ccpa-text"><p>' + escHtml(txt.ccpaDescription) + '</p></div>' +
     1345      '<div class="cc-ccpa-inner" id="cc-ccpa-notice">' +
     1346        '<div class="cc-ccpa-text"><p>' + escHtml(txt.ccpaDescription) + '</p>' +
     1347          (gpcActive ? '<p class="cc-ccpa-gpc">' + escHtml(txt.ccpaGpcNotice) + '</p>' : '') +
     1348        '</div>' +
    12441349        '<div class="cc-ccpa-actions">' +
    1245           '<a href="' + escHtml(CONFIG.ccpaUrl) + '" class="cc-ccpa-link">' + escHtml(txt.ccpaDnsmpiLink) + '</a>' +
     1350          '<a href="#" class="cc-ccpa-link" id="cc-ccpa-dns" role="button">' + escHtml(txt.ccpaDnsmpiLink) + '</a>' +
    12461351          '<button class="cc-btn cc-btn-accept" id="cc-ccpa-ok">' + escHtml(txt.ccpaAcceptButton) + '</button>' +
    12471352        '</div>' +
    1248       '</div>';
     1353      '</div>' +
     1354      '<div class="cc-ccpa-choice" id="cc-ccpa-choice"></div>' +
     1355      (CONFIG.showPoweredBy ? '<div class="cc-powered"><a href="https://www.cookieflame.com" target="_blank" rel="noopener">Powered by CookieFlame</a></div>' : '');
    12491356
    12501357    document.body.appendChild(banner);
    12511358
     1359    // OK only closes the notice. It never changes the visitor's choices:
     1360    // US visitors start with everything allowed, and a GPC visitor's
     1361    // opt-out was saved before the notice appeared.
    12521362    document.getElementById("cc-ccpa-ok").addEventListener("click", function () {
    1253       var s = buildStateAllGranted();
    1254       saveState(s, "ccpa_acknowledge");
    1255       banner.classList.remove("cc-show");
     1363      if (consentState) logConsent("ccpa_acknowledge", consentState);
     1364      closeCcpaBanner();
     1365    });
     1366    document.getElementById("cc-ccpa-dns").addEventListener("click", function (e) {
     1367      e.preventDefault();
     1368      showCcpaChoice(true);
     1369    });
     1370    emitRendered("ccpa", banner);
     1371  }
     1372
     1373  // The "Do Not Sell or Share" choice, drawn from the current state.
     1374  function renderCcpaChoice() {
     1375    var el = document.getElementById("cc-ccpa-choice");
     1376    if (!el) return;
     1377    var txt = CONFIG.text;
     1378    var optedOut = !(consentState && consentState.marketing);
     1379    var html = '<div class="cc-ccpa-choice-inner">' +
     1380      '<p class="cc-ccpa-choice-title">' + escHtml(txt.ccpaChoiceTitle) + '</p>';
     1381    if (optedOut) {
     1382      html += '<p class="cc-ccpa-choice-status">' + escHtml(gpcActive ? txt.ccpaGpcOptedOut : txt.ccpaOptedOut) + '</p>' +
     1383        '<div class="cc-ccpa-choice-buttons">' +
     1384          '<button class="cc-btn cc-btn-accept" id="cc-ccpa-done">' + escHtml(txt.ccpaDoneButton) + '</button>' +
     1385          '<button class="cc-btn cc-btn-decline" id="cc-ccpa-optin">' + escHtml(txt.ccpaOptInButton) + '</button>' +
     1386        '</div>' +
     1387        (gpcActive ? '<p class="cc-ccpa-choice-note">' + escHtml(txt.ccpaOptInGpcNote) + '</p>' : '');
     1388    } else {
     1389      html += '<p class="cc-ccpa-choice-text">' + escHtml(txt.ccpaChoiceText) + '</p>' +
     1390        '<div class="cc-ccpa-choice-buttons">' +
     1391          '<button class="cc-btn cc-btn-accept" id="cc-ccpa-optout">' + escHtml(txt.ccpaOptOutButton) + '</button>' +
     1392          '<button class="cc-btn cc-btn-decline" id="cc-ccpa-cancel">' + escHtml(txt.ccpaCancelButton) + '</button>' +
     1393        '</div>';
     1394    }
     1395    html += '<button class="cc-btn cc-btn-details" id="cc-ccpa-more">' + escHtml(txt.ccpaMoreSettings) + '</button></div>';
     1396    el.innerHTML = html;
     1397
     1398    var bind = function (id, fn) { var b = document.getElementById(id); if (b) b.addEventListener("click", fn); };
     1399    bind("cc-ccpa-done", closeCcpaBanner);
     1400    bind("cc-ccpa-cancel", closeCcpaBanner);
     1401    bind("cc-ccpa-optout", function () {
     1402      var s = copyState(consentState);
     1403      s.marketing = false;
     1404      saveState(s, "ccpa_opt_out");
     1405      renderCcpaChoice();
     1406      focusFirst("cc-ccpa-choice");
    12561407      emit("cc:update");
    12571408    });
     1409    bind("cc-ccpa-optin", function () {
     1410      var s = copyState(consentState);
     1411      s.marketing = true;
     1412      if (gpcActive) s.gpcOptIn = 1;
     1413      delete s.gpc;
     1414      saveState(s, "ccpa_opt_in");
     1415      renderCcpaChoice();
     1416      focusFirst("cc-ccpa-choice");
     1417      emit("cc:update");
     1418    });
     1419    bind("cc-ccpa-more", function () {
     1420      closeCcpaBanner();
     1421      openPreferences();
     1422    });
     1423  }
     1424
     1425  function showCcpaChoice(open) {
     1426    var notice = document.getElementById("cc-ccpa-notice");
     1427    var choice = document.getElementById("cc-ccpa-choice");
     1428    if (!notice || !choice) return;
     1429    if (open) renderCcpaChoice();
     1430    notice.style.display = open ? "none" : "";
     1431    choice.classList.toggle("cc-open", !!open);
     1432    if (open) focusFirst("cc-ccpa-choice");
     1433  }
     1434
     1435  function closeCcpaBanner() {
     1436    var b = document.getElementById("cc-ccpa-banner");
     1437    if (b) b.classList.remove("cc-show");
     1438    showCcpaChoice(false);
     1439    document.removeEventListener("keydown", _trapFocus);
     1440    if (_preFocusEl && typeof _preFocusEl.focus === "function") {
     1441      try { _preFocusEl.focus(); } catch (e) {}
     1442    }
     1443    _preFocusEl = null;
     1444  }
     1445
     1446  function focusFirst(id) {
     1447    var el = document.getElementById(id);
     1448    var b = el && el.querySelector("button");
     1449    if (b) { try { b.focus(); } catch (e) {} }
     1450  }
     1451
     1452  // A fresh state with the same choices as src (everything allowed when
     1453  // there is none yet), keeping an earlier GPC opt-in.
     1454  function copyState(src) {
     1455    var s = { ts: new Date().toISOString(), cv: CONFIG.consentVersion, cc: detectedCountry };
     1456    CONFIG.cookieTypes.forEach(function (t) { s[t.id] = src ? !!src[t.id] : true; });
     1457    if (src && src.gpcOptIn) s.gpcOptIn = 1;
     1458    return s;
    12581459  }
    12591460
    … …  
    12681469  function _trapFocus(e) {
    12691470    if (e.key !== "Tab") return;
    1270     var banner = document.getElementById("cc-banner") || document.getElementById("cc-ccpa-banner");
    1271     if (!banner || !banner.classList.contains("cc-show")) return;
     1471    var banner = document.querySelector("#cc-banner.cc-show, #cc-ccpa-banner.cc-show");
     1472    if (!banner) return;
    12721473    var focusable = [];
    12731474    var all = banner.querySelectorAll('button, a[href], input:not([disabled]), select, textarea, [tabindex]:not([tabindex="-1"])');
    … …  
    12781479      var p = el.parentElement;
    12791480      while (p && p !== banner) {
    1280         if (p.style && window.getComputedStyle(p).display === "none") { hidden = true; break; }
     1481        if (window.getComputedStyle(p).display === "none") { hidden = true; break; }
    12811482        p = p.parentElement;
    12821483      }
    … …  
    12951496  function showBanner(mode) {
    12961497    if (mode === "ccpa") {
     1498      _preFocusEl = document.activeElement;
    12971499      requestAnimationFrame(function () {
    12981500        var b = document.getElementById("cc-ccpa-banner");
    12991501        if (b) {
    13001502          b.classList.add("cc-show");
    1301           _preFocusEl = document.activeElement;
    1302           b.focus();
     1503          var choice = document.getElementById("cc-ccpa-choice");
     1504          if (choice && choice.classList.contains("cc-open")) focusFirst("cc-ccpa-choice");
     1505          else b.focus();
    13031506          document.addEventListener("keydown", _trapFocus);
    13041507        }
    … …  
    13071510    }
    13081511    _preFocusEl = document.activeElement;
     1512    // "Decline All" on the first screen is for the GDPR banner only, never
     1513    // when the preferences are opened by a US or other visitor.
     1514    var rj = document.getElementById("cc-reject");
     1515    if (rj) {
     1516      if (complianceMode === "gdpr") rj.style.removeProperty("display");
     1517      else rj.style.setProperty("display", "none", "important"); // beats .cc-btn's !important
     1518    }
    13091519    requestAnimationFrame(function () {
    13101520      var ov = document.getElementById("cc-overlay");
    … …  
    13541564  ═══════════════════════════════════════════ */
    13551565  function acceptAll() {
    1356     saveState(buildStateAllGranted(), "accept_all");
     1566    var s = buildStateAllGranted();
     1567    if (consentState && consentState.gpcOptIn) s.gpcOptIn = 1;
     1568    saveState(s, "accept_all");
    13571569    closeBanner();
    13581570    emit("cc:accept-all"); emit("cc:update");
    … …  
    13661578
    13671579  function savePrefs() {
    1368     saveState(buildStateFromCheckboxes(), "save_preferences");
     1580    var s = buildStateFromCheckboxes();
     1581    // Switching Marketing on here is the separate choice that opts a GPC
     1582    // visitor back in.
     1583    if (s.marketing && (gpcActive || (consentState && consentState.gpcOptIn))) s.gpcOptIn = 1;
     1584    saveState(s, "save_preferences");
    13691585    closeBanner();
    13701586    emit("cc:save"); emit("cc:update");
    … …  
    13831599    }
    13841600  }
     1601
     1602  /* ═══════════════════════════════════════════
     1603     COOKIE SETTINGS LINK
     1604     [cookieflame_settings_link], the declaration's button and any
     1605     element with the class cc-open-settings reopen the visitor's
     1606     choices: the "Do Not Sell or Share" choice for US visitors, the
     1607     preferences for everyone else.
     1608  ═══════════════════════════════════════════ */
     1609  function openPreferences() {
     1610    if (!document.getElementById("cc-banner")) {
     1611      applyThemeClasses();
     1612      buildGdprBanner();
     1613      hideAllBanners();
     1614      if (consentState) syncCheckboxes(consentState);
     1615    }
     1616    showBanner("gdpr");
     1617  }
     1618
     1619  function openUsChoices() {
     1620    applyThemeClasses();
     1621    buildCcpaBanner();
     1622    showBanner("ccpa");
     1623    showCcpaChoice(true);
     1624  }
     1625
     1626  function openSettings() {
     1627    if (complianceMode === "ccpa") openUsChoices();
     1628    else openPreferences();
     1629  }
     1630
     1631  document.addEventListener("click", function (e) {
     1632    var t = e.target && e.target.closest ? e.target.closest(".cc-open-settings") : null;
     1633    if (!t) return;
     1634    e.preventDefault();
     1635    openSettings();
     1636  });
    13851637
    13861638  /* ═══════════════════════════════════════════
    … …  
    14351687    getComplianceMode:  function () { return complianceMode; },
    14361688    getCountry:         function () { return detectedCountry; },
    1437     show:               function () { showBanner("gdpr"); }, // always open preference manager
     1689    show:               openPreferences, // always open preference manager
     1690    showSettings:       openSettings,    // what the Cookie settings link opens
    14381691    acceptAll:          acceptAll,
    14391692    declineAll:         declineAll,
    … …  
    14611714    // Add browser cookies
    14621715    var browserCookies = document.cookie.split(";").map(function (c) { return c.trim().split("=")[0]; }).filter(Boolean);
     1716    browserCookies = browserCookies.filter(function (n) { return !isLoginCookie(n); });
    14631717    for (var b = 0; b < browserCookies.length; b++) {
    14641718      var bInfo = identifyCookie(browserCookies[b]);
    … …  
    14731727    for (var c = 0; c < customCookies.length; c++) {
    14741728      var cc = customCookies[c];
    1475       if (cc.name && cc.vendor && cc.category && cc.category !== "unclassified" && !allCookies[cc.name]) {
     1729      if (cc.name && cc.vendor && cc.category && cc.category !== "unclassified" && !allCookies[cc.name] && !isLoginCookie(cc.name)) {
    14761730        allCookies[cc.name] = {
    14771731          name: cc.name, vendor: cc.vendor, category: cc.category,
    … …  
    15621816    }
    15631817
     1818    if (CONFIG.visitorLogins) {
     1819      var loginRows = loginCookieRows();
     1820      html += '<div class="cc-declaration-login">';
     1821      html += '<p><b>' + escHtml(CONFIG.text.loginCookiesTitle) + '</b></p>';
     1822      html += '<p>' + escHtml(CONFIG.text.loginCookiesIntro) + '</p>';
     1823      html += '<table><thead><tr>';
     1824      html += '<th>Name</th><th>Provider</th><th>Purpose</th><th>Expiry</th>';
     1825      html += '</tr></thead><tbody>';
     1826      for (var li = 0; li < loginRows.length; li++) {
     1827        html += '<tr>';
     1828        html += '<td><code>' + escHtml(loginRows[li].name) + '</code></td>';
     1829        html += '<td>WordPress</td>';
     1830        html += '<td>' + escHtml(loginRows[li].purpose) + '</td>';
     1831        html += '<td>' + escHtml(loginRows[li].expiry) + '</td>';
     1832        html += '</tr>';
     1833      }
     1834      html += '</tbody></table></div>';
     1835    }
     1836
     1837    html += '<p><button type="button" class="cc-open-settings cc-declaration-change">' + escHtml(CONFIG.text.changePreferences) + '</button></p>';
    15641838    html += '</div>';
    15651839    el.innerHTML = html;
    … …  
    15811855  var preloadedConsent = loadState();
    15821856  if (preloadedConsent) {
     1857    // Marketing scripts created before boot() runs stay blocked for GPC.
     1858    if (gpcActive && !preloadedConsent.gpcOptIn) preloadedConsent.marketing = false;
    15831859    consentState = preloadedConsent;
    15841860    updateConsentMode(preloadedConsent);
    … …  
    16491925    var existing = loadState();
    16501926
     1927    var gpcNewOptOut = false;
    16511928    if (existing) {
    1652       // GPC override: when Global Privacy Control is active, suppress
    1653       // marketing consent in the effective state. The stored cookie
    1654       // is not modified — if the visitor later disables GPC, their
    1655       // original consent choice is restored. This ensures marketing
    1656       // scripts and ad signals stay blocked while GPC is active.
    1657       if (gpcActive) existing.marketing = false;
     1929      // GPC: a signal that arrives after the visitor's choice is
     1930      // processed as an opt-out and saved, so it holds on later visits
     1931      // without GPC (§7025(c)(5)). An earlier opt-in made while GPC was
     1932      // on (gpcOptIn) is kept.
     1933      if (gpcActive && !existing.gpcOptIn && (existing.marketing || !existing.gpc)) {
     1934        gpcNewOptOut = !!existing.marketing;
     1935        applyGpc(existing);
     1936        storageSet(CONFIG.cookieName, JSON.stringify(existing), CONFIG.cookieExpiry);
     1937      }
    16581938      consentState = existing;
    16591939      updateConsentMode(existing);
    … …  
    16811961          if (ic) ic.classList.add("cc-show");
    16821962        }
     1963        if (gpcNewOptOut) logConsent("gpc_optout", existing);
    16831964        emit("cc:ready");
    16841965        renderDeclaration();
    … …  
    16911972            if (ic) ic.classList.add("cc-show");
    16921973          }
     1974          if (gpcNewOptOut) logConsent("gpc_optout", existing);
    16931975          emit("cc:ready");
    16941976          renderDeclaration();
    … …  
    17141996        startIframeObserver();
    17151997        if (gpcActive) {
    1716           // GPC active: grant analytics and preferences but deny marketing.
    1717           // This fulfills the CCPA/CPRA requirement to honor Global Privacy
    1718           // Control as a valid opt-out of the sale or sharing of personal
    1719           // information. Fines have been issued for ignoring GPC (Sephora
    1720           // $1.2M, DoorDash $375K, Tractor Supply $1.3M).
    1721           var gpcState = buildStateAllGranted();
    1722           gpcState.marketing = false;
    1723           saveState(gpcState, "ccpa_gpc_optout");
     1998          // GPC active: allow analytics and preferences but opt out of
     1999          // selling and sharing (Marketing off), saved before the notice
     2000          // appears. The notice says the signal was honoured.
     2001          saveState(applyGpc(buildStateAllGranted()), "ccpa_gpc_optout");
    17242002        } else {
    17252003          saveState(buildStateAllGranted(), "ccpa_auto_grant");
  • cookieflame/tags/5.4.0/assets/js/cookieconsent.min.js

    r3769546 r3771537  
    11/**
    2  * CookieFlame v5.3.0
     2 * CookieFlame v5.4.0
    33 * ─────────────────────────────────────────────────────────────
    44 * Self-hosted cookie consent manager with Google Consent Mode v2,
    5  * IAB TCF v2.2, automatic script blocking, iframe facades with
     5 * automatic script blocking, iframe facades with
    66 * thumbnails, Elementor video widget support, multi-region
    77 * compliance (GDPR/CCPA), and passive cookie monitoring.
    … …  
    1717 *   • Timezone-based geo heuristic with server-side fallback
    1818 */
    19 !function(){"use strict";var e={};try{for(var t=document.cookie.split(";"),n=0;n<t.length;n++){var o=t[n].trim().split("=")[0];o&&(e[o]=!0)}}catch(e){}var c="5.3.0",r={cookieName:"cc_consent",cookieExpiry:365,privacyUrl:"/privacy-policy",ccpaUrl:"/privacy-policy#do-not-sell",geoApi:"",logEndpoint:"",reconsentDays:365,cspSafe:!1,tcfEnabled:!0,cmpId:1,cmpVersion:1,vendorListVersion:2,storageMethod:"cookie",cookieDomain:"",shareSubdomains:!1,theme:"light",showPoweredBy:!1,iconPosition:"left",consentVersion:"",autoBlockPatterns:[{pattern:"googletagmanager.com/gtag",category:"analytics"},{pattern:"google-analytics.com",category:"analytics"},{pattern:"analytics.google.com",category:"analytics"},{pattern:"hotjar.com",category:"analytics"},{pattern:"mixpanel.com",category:"analytics"},{pattern:"segment.com/analytics",category:"analytics"},{pattern:"segment.io",category:"analytics"},{pattern:"clarity.ms",category:"analytics"},{pattern:"plausible.io",category:"analytics"},{pattern:"js.hs-analytics.net",category:"analytics"},{pattern:"connect.facebook.net",category:"marketing"},{pattern:"snap.licdn.com",category:"marketing"},{pattern:"ads.linkedin.com",category:"marketing"},{pattern:"googleads.g.doubleclick.net",category:"marketing"},{pattern:"googlesyndication.com",category:"marketing"},{pattern:"googleadservices.com",category:"marketing"},{pattern:"ads-twitter.com",category:"marketing"},{pattern:"analytics.tiktok.com",category:"marketing"},{pattern:"pintrk",category:"marketing"},{pattern:"s.pinimg.com/ct",category:"marketing"},{pattern:"widget.intercom.io",category:"preferences"},{pattern:"js.intercomcdn.com",category:"preferences"}],regionRules:{gdpr:["AT","BE","BG","CY","CZ","DE","DK","EE","ES","FI","FR","GR","HR","HU","IE","IT","LT","LU","LV","MT","NL","PL","PT","RO","SE","SI","SK","IS","LI","NO","GB","GG","JE","IM","CH","BR","ZA"],ccpa:["US"]},text:{title:"We value your privacy",description:"We use cookies to improve your experience, analyse traffic and personalise content. You can choose which categories to allow.",acceptAllButton:"Accept All",declineAllButton:"Decline All",saveButton:"Save Preferences",showDetailsButton:"Manage preferences",hideDetailsButton:"Hide preferences",alwaysOnLabel:"(always on)",ccpaTitle:"Your Privacy Choices",ccpaDescription:"We use cookies and similar technologies. Under California law, you have the right to opt out of the sale or sharing of your personal information.",ccpaAcceptButton:"OK",ccpaDnsmpiLink:"Do Not Sell My Personal Information"},cookieTypes:[{id:"necessary",name:"Necessary",description:"Required for the site to function. Cannot be disabled.",required:!0,consentMode:["security_storage","functionality_storage"],tcfPurposes:[1],onAccept:function(){}},{id:"preferences",name:"Preferences",description:"Remember your settings and personalisation choices.",required:!1,defaultValue:!1,consentMode:["functionality_storage","personalization_storage"],tcfPurposes:[2,3],onAccept:function(){},onReject:function(){}},{id:"analytics",name:"Analytics",description:"Help us understand how visitors use the site.",required:!1,defaultValue:!1,consentMode:["analytics_storage"],tcfPurposes:[4,5,6],onAccept:function(){},onReject:function(){}},{id:"marketing",name:"Marketing",description:"Used to deliver relevant ads and track campaigns.",required:!1,defaultValue:!1,consentMode:["ad_storage","ad_user_data","ad_personalization"],tcfPurposes:[7,8,9,10],onAccept:function(){},onReject:function(){}}]};if(window.__ccConfig&&"object"==typeof window.__ccConfig){var a=window.__ccConfig;["cookieName","cookieExpiry","privacyUrl","ccpaUrl","geoApi","logEndpoint","reconsentDays","cspSafe","tcfEnabled","cmpId","cmpVersion","vendorListVersion","storageMethod","cookieDomain","shareSubdomains","theme","showPoweredBy","iconPosition","consentVersion"].forEach(function(e){void 0!==a[e]&&(r[e]=a[e])}),a.text&&"object"==typeof a.text&&Object.keys(a.text).forEach(function(e){r.text[e]=a.text[e]}),Array.isArray(a.cookieTypes)&&(r.cookieTypes=a.cookieTypes),Array.isArray(a.autoBlockPatterns)&&(r.autoBlockPatterns=a.autoBlockPatterns),a.regionRules&&(r.regionRules=a.regionRules)}window.__ccConsentVersion&&(r.consentVersion=window.__ccConsentVersion);var i=!!window.__ccGPC;window.__ccBlockRules&&Array.isArray(window.__ccBlockRules)&&window.__ccBlockRules.length&&(r.autoBlockPatterns=window.__ccBlockRules);var s={cc_consent:{vendor:"CookieFlame",category:"necessary",purpose:"Stores your consent preferences",expiry:"1 year"},cc_anon_id:{vendor:"CookieFlame",category:"necessary",purpose:"Anonymous ID for consent logging",expiry:"1 year"},cookieflame_dnt:{vendor:"CookieFlame",category:"necessary",purpose:"Do-not-track opt-out flag",expiry:"1 year"},PHPSESSID:{vendor:"PHP",category:"necessary",purpose:"Server-side session identifier",expiry:"Session"},wp_settings_:{vendor:"WordPress",category:"necessary",purpose:"Admin interface preferences",prefix:!0,expiry:"1 year"},"wp-settings-":{vendor:"WordPress",category:"necessary",purpose:"Admin interface preferences",prefix:!0,expiry:"1 year"},"wp-settings-time-":{vendor:"WordPress",category:"necessary",purpose:"Admin settings timestamp",prefix:!0,expiry:"1 year"},wordpress_logged_in_:{vendor:"WordPress",category:"necessary",purpose:"Login session",prefix:!0,expiry:"Session"},wordpress_sec_:{vendor:"WordPress",category:"necessary",purpose:"Secure login session",prefix:!0,expiry:"Session"},wordpress_test_cookie:{vendor:"WordPress",category:"necessary",purpose:"Checks cookie support",expiry:"Session"},wp_lang:{vendor:"WordPress",category:"necessary",purpose:"Language preference",expiry:"Session"},woocommerce_cart_hash:{vendor:"WooCommerce",category:"necessary",purpose:"Shopping cart contents",expiry:"Session"},woocommerce_items_in_cart:{vendor:"WooCommerce",category:"necessary",purpose:"Cart item count",expiry:"Session"},wp_woocommerce_session_:{vendor:"WooCommerce",category:"necessary",purpose:"Shopping session",prefix:!0,expiry:"2 days"},_ga:{vendor:"Google Analytics",category:"analytics",purpose:"Distinguishes visitors",expiry:"2 years"},_ga_:{vendor:"Google Analytics",category:"analytics",purpose:"Stores session state",prefix:!0,expiry:"2 years"},_gid:{vendor:"Google Analytics",category:"analytics",purpose:"Distinguishes visitors (24h)",expiry:"24 hours"},_gat:{vendor:"Google Analytics",category:"analytics",purpose:"Throttles request rate",expiry:"1 minute"},wp_wk_ga_untrack_:{vendor:"Google Analytics",category:"analytics",purpose:"GA opt-out flag",prefix:!0,expiry:"Persistent"},_gcl_au:{vendor:"Google Ads",category:"marketing",purpose:"Conversion linker",expiry:"90 days"},_gcl_aw:{vendor:"Google Ads",category:"marketing",purpose:"Stores ad click info",expiry:"90 days"},_gac_:{vendor:"Google Ads",category:"marketing",purpose:"Campaign information",prefix:!0,expiry:"90 days"},IDE:{vendor:"Google DoubleClick",category:"marketing",purpose:"Ad serving and frequency",expiry:"1 year"},test_cookie:{vendor:"Google DoubleClick",category:"marketing",purpose:"Checks cookie support",expiry:"15 minutes"},NID:{vendor:"Google",category:"preferences",purpose:"Stores preferences",expiry:"6 months"}},d=window.__ccCustomCookies||[];function u(e){for(var t=0;t<d.length;t++){var n=d[t];if("exact"===n.match&&e===n.name)return n;if("prefix"===n.match&&0===e.indexOf(n.name))return n;if("contains"===n.match&&-1!==e.indexOf(n.name))return n}var o=s[e];return o||(Object.keys(s).forEach(function(t){!o&&s[t].prefix&&0===e.indexOf(t)&&(o=s[t])}),o||null)}var l=null,p="none",f=null,m=null,g=!1,y=0;function v(e){return"string"!=typeof e&&(e=String(e)),e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;")}function h(e){if("localStorage"===r.storageMethod)try{return localStorage.getItem(e)}catch(e){return null}var t=document.cookie.match(new RegExp("(?:^|;\\s*)"+e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")+"=([^;]*)"));if(!t)return null;try{return decodeURIComponent(t[1])}catch(e){return null}}function w(e,t,n){if("localStorage"!==r.storageMethod){var o=new Date;o.setTime(o.getTime()+864e5*n);var c=e+"="+encodeURIComponent(t)+";expires="+o.toUTCString()+";path=/;SameSite=Lax;Secure";r.cookieDomain&&(c+=";domain="+r.cookieDomain),document.cookie=c}else try{localStorage.setItem(e,t)}catch(e){}}function b(){if(m)return m;var e=h("cc_anon_id");return e?(m=e,e):(w("cc_anon_id",m="cc_"+function(){if("undefined"!=typeof crypto&&"function"==typeof crypto.randomUUID)try{return crypto.randomUUID()}catch(e){}return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,function(e){var t=16*Math.random()|0;return("x"===e?t:3&t|8).toString(16)})}(),730),m)}function k(){var e={timestamp:l?l.ts:null,complianceMode:p,country:f,gpc:i,consentVersion:r.consentVersion};return r.cookieTypes.forEach(function(t){e[t.id]=l?!!l[t.id]:!!t.required}),e}function _(e,t){window.dispatchEvent(new CustomEvent(e,{detail:t||k()}))}function x(){window.dataLayer=window.dataLayer||[],window.dataLayer.push(arguments)}function E(e){var t={};r.cookieTypes.forEach(function(n){n.consentMode&&n.consentMode.forEach(function(o){t[o]=e[n.id]?"granted":"denied"})}),i&&(t.ad_storage="denied",t.ad_user_data="denied",t.ad_personalization="denied"),x("consent","update",t)}var A=new Map,S=!1;function L(e){var t={};return r.cookieTypes.forEach(function(n){n.tcfPurposes&&n.tcfPurposes.forEach(function(o){t[o]=!!e[n.id]})}),{tcString:"",tcfPolicyVersion:4,cmpId:r.cmpId,cmpVersion:r.cmpVersion,gdprApplies:"gdpr"===p,eventStatus:"useractioncomplete",cmpStatus:"loaded",listenerId:void 0,isServiceSpecific:!0,useNonStandardTexts:!1,publisherCC:"GB",purposeOneTreatment:!1,purpose:{consents:t,legitimateInterests:{}},vendor:{consents:{},legitimateInterests:{}},specialFeatureOptins:{},publisher:{consents:{},legitimateInterests:{},customPurpose:{consents:{},legitimateInterests:{}},restrictions:{}}}}function C(e,t,n,o){if("function"==typeof n)if("addEventListener"===e){var c=++y;if(A.set(c,n),l){var a=L(l);a.listenerId=c,n(a,!0)}}else"removeEventListener"===e?n({listenerId:o},A.delete(o)):"getTCData"===e?n(l?L(l):null,!!l):"ping"===e?n({gdprApplies:"gdpr"===p,cmpLoaded:S,cmpStatus:S?"loaded":"stub",displayStatus:"hidden",apiVersion:"2.2",cmpVersion:r.cmpVersion,cmpId:r.cmpId,gvlVersion:r.vendorListVersion,tcfPolicyVersion:4},!0):n(null,!1)}function B(e){A.forEach(function(t,n){var o=L(e);o.listenerId=n;try{t(o,!0)}catch(e){}})}function I(e){if(!e)return null;for(var t=e.toLowerCase(),n=0;n<r.autoBlockPatterns.length;n++){var o=r.autoBlockPatterns[n];if(o.pattern instanceof RegExp){if(o.pattern.test(t))return o.category}else if(-1!==t.indexOf(o.pattern.toLowerCase()))return o.category}return null}function T(e){document.querySelectorAll('script[type="text/plain"][data-cc]').forEach(function(t){var n=(t.getAttribute("data-cc")||"").toLowerCase();if(e[n]&&!t.getAttribute("data-cc-activated")){t.setAttribute("data-cc-activated","1");var o=document.createElement("script");o.isWPRocket=!0,Array.from(t.attributes).forEach(function(e){"type"!==e.name&&"data-cc"!==e.name&&"data-cc-activated"!==e.name&&"data-cc-auto"!==e.name&&o.setAttribute(e.name,e.value)}),!t.src&&t.textContent&&(o.textContent=t.textContent),t.parentNode.insertBefore(o,t.nextSibling),t.parentNode.removeChild(t)}}),["img","iframe","link"].forEach(function(t){document.querySelectorAll(t+"[data-cc][data-cc-src]").forEach(function(n){var o=(n.getAttribute("data-cc")||"").toLowerCase();e[o]&&!n.getAttribute("data-cc-activated")&&(n.setAttribute("data-cc-activated","1"),"link"===t?n.href=n.getAttribute("data-cc-src"):n.src=n.getAttribute("data-cc-src"))})})}var O=[{pattern:"youtube.com",category:"marketing"},{pattern:"youtube-nocookie.com",category:"marketing"},{pattern:"youtu.be",category:"marketing"},{pattern:"player.vimeo.com",category:"marketing"},{pattern:"vimeo.com",category:"marketing"},{pattern:"dailymotion.com",category:"marketing"},{pattern:"open.spotify.com",category:"preferences"},{pattern:"soundcloud.com",category:"preferences"},{pattern:"google.com/maps",category:"preferences"},{pattern:"maps.google.com",category:"preferences"},{pattern:"facebook.com/plugins",category:"marketing"},{pattern:"instagram.com",category:"marketing"},{pattern:"tiktok.com/embed",category:"marketing"},{pattern:"twitter.com",category:"marketing"},{pattern:"x.com",category:"marketing"}];function D(){return!(!document.body||!(document.body.classList.contains("elementor-editor-active")||document.body.classList.contains("fl-builder-edit")||document.body.classList.contains("et-fb-root-ancestor")))}var P=[],M=0,R=null,q=!1;function V(){if(R&&P.length){var e=P;P=[];try{R(e)}catch(e){}}}function N(){0!==P.length&&(2!==M?1!==M&&3!==M&&(M=1,function(){var e=window.__ccConfig&&window.__ccConfig.facadesUrl||null;if(!e)return void(M=3);var t=document.createElement("script");t.src=e,t.async=!0,t.onload=function(){M=2,V()},t.onerror=function(){M=3},(document.head||document.documentElement).appendChild(t)}()):V())}function U(e){var t=e.src||e.getAttribute("data-src")||"";if(t&&!e.getAttribute("data-cc-facade")&&!D()){var n=function(e){if(!e)return null;for(var t=e.toLowerCase(),n=0;n<O.length;n++)if(-1!==t.indexOf(O[n].pattern))return O[n].category;return null}(t);if(n){var o=l||Z();o&&o[n]||(e.setAttribute("data-cc-facade","1"),e.setAttribute("data-cc-iframe-cat",n),e.setAttribute("data-cc-original-src",t),e.removeAttribute("src"),e.style.display="none",P.push({type:"iframe",node:e,category:n,src:t}),N())}}}function G(e){if(!e.getAttribute("data-cc-facade")&&!D()){var t=e.getAttribute("data-settings");if(t){var n;try{n=JSON.parse(t)}catch(e){return}var o=n.youtube_url||n.vimeo_url||"";if(o){var c=o.toLowerCase(),r=null;if((-1!==c.indexOf("youtube")||-1!==c.indexOf("youtu.be")||-1!==c.indexOf("vimeo")||-1!==c.indexOf("dailymotion"))&&(r="marketing"),r&&(!l||!l[r])){if(!l){var a=Z();if(a&&a[r])return}e.setAttribute("data-cc-facade","1"),q=!0,P.push({type:"elementor",node:e,category:r,url:o}),N()}}}}}function j(){for(var e=document.querySelectorAll("iframe[src]"),t=0;t<e.length;t++)U(e[t]);for(var n=document.querySelectorAll(".elementor-widget-video"),o=0;o<n.length;o++)G(n[o]);for(var c=document.querySelectorAll(".wp-block-embed__wrapper iframe[src]"),r=0;r<c.length;r++)U(c[r])}function J(){new MutationObserver(function(e){e.forEach(function(e){e.addedNodes.forEach(function(e){if(1===e.nodeType){if("IFRAME"===e.tagName&&U(e),e.querySelectorAll){for(var t=e.querySelectorAll("iframe[src]"),n=0;n<t.length;n++)U(t[n]);for(var o=e.querySelectorAll(".elementor-widget-video"),c=0;c<o.length;c++)G(o[c])}e.classList&&e.classList.contains("elementor-widget-video")&&G(e)}})})}).observe(document.documentElement,{childList:!0,subtree:!0})}function W(){for(var e=document.querySelectorAll("iframe[src]"),t=0;t<e.length;t++){var n=e[t];if(!n.getAttribute("data-cc-facade")){var o=n.src||"";if(o)for(var c=0;c<O.length;c++)if(-1!==o.indexOf(O[c].pattern)){n.src=o;break}}}}function H(e,t){r.cookieTypes.forEach(function(n){var o=!!e[n.id];if((t?!!t[n.id]:null)!==o||null===t)if(o&&"function"==typeof n.onAccept)try{n.onAccept()}catch(e){console.warn("[CookieConsent] onAccept error for "+n.id,e)}else if(!o&&"function"==typeof n.onReject)try{n.onReject()}catch(e){console.warn("[CookieConsent] onReject error for "+n.id,e)}})}function F(e,t){var n=l;if(l=e,w(r.cookieName,JSON.stringify(e),r.cookieExpiry),E(e),q&&t)for(var o=document.querySelectorAll(".cc-iframe-facade[data-cc-elementor-widget]"),a=0;a<o.length;a++){var s=o[a].getAttribute("data-cc-iframe-cat");if(s&&e[s])return void window.location.reload()}K(e,n),T(e),function(e){for(var t=document.querySelectorAll(".cc-iframe-facade"),n=0;n<t.length;n++)if(!t[n].getAttribute("data-cc-elementor-widget")&&e[t[n].getAttribute("data-cc-iframe-cat")]){var o=t[n].nextElementSibling;if(o&&"IFRAME"===o.tagName&&o.getAttribute("data-cc-facade")){var c=o.getAttribute("data-cc-original-src");c&&(o.src=c,o.style.display="",o.removeAttribute("data-cc-facade"))}t[n].parentNode.removeChild(t[n])}}(e),t&&"auto_grant_non_eu"!==t&&"ccpa_auto_grant"!==t&&setTimeout(W,300),H(e,n),$(e),r.tcfEnabled&&B(e),t&&function(e,t){if(r.logEndpoint){var n={};r.cookieTypes.forEach(function(e){n[e.id]=!!t[e.id]});var o={anonymousId:b(),timestamp:(new Date).toISOString(),action:e,consent:n,complianceMode:p,country:f,url:location.href,userAgent:navigator.userAgent,tcfEnabled:r.tcfEnabled,gpc:i,consentVersion:r.consentVersion,version:c};try{var a=JSON.stringify(o);navigator.sendBeacon?navigator.sendBeacon(r.logEndpoint,new Blob([a],{type:"application/json"})):fetch(r.logEndpoint,{method:"POST",body:a,headers:{"Content-Type":"application/json"},keepalive:!0})}catch(e){}}}(t,e)}function $(e){r.cookieTypes.forEach(function(t){if(!t.required){var n=document.getElementById("cc-cb-"+t.id);n&&(n.checked=!!e[t.id])}});var t=document.getElementById("cc-cats"),n=document.getElementById("cc-details"),o=document.getElementById("cc-pref-buttons");t&&t.classList.remove("cc-open"),n&&(n.textContent=r.text.showDetailsButton),o&&o.classList.remove("cc-open")}function Z(){var e=h(r.cookieName);if(!e)return null;try{var t=JSON.parse(e);return!t||"object"!=typeof t||Array.isArray(t)||(!t.ts||isNaN(new Date(t.ts).getTime())||(Date.now()-new Date(t.ts).getTime())/864e5>r.reconsentDays||r.consentVersion&&t.cv!==r.consentVersion)?null:t}catch(e){return null}}function Y(){var e={ts:(new Date).toISOString(),cv:r.consentVersion,cc:f};return r.cookieTypes.forEach(function(t){e[t.id]=!0}),e}function z(){var e={ts:(new Date).toISOString(),cv:r.consentVersion,cc:f};return r.cookieTypes.forEach(function(t){e[t.id]=!!t.required}),e}function K(e,t){if(t){var n=[];if(r.cookieTypes.forEach(function(o){o.required||t[o.id]&&!e[o.id]&&n.push(o.id)}),n.length)document.cookie.split(";").map(function(e){return e.trim().split("=")[0]}).filter(Boolean).forEach(function(e){var t=u(e);if(t&&-1!==n.indexOf(t.category)){var o=location.hostname,c="."+o;document.cookie=e+"=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/",document.cookie=e+"=; expires=Thu, 01 Jan 1970 00:00:00 GMT",document.cookie=e+"=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain="+c,document.cookie=e+"=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain="+o,document.cookie=e+"=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; Secure",r.cookieDomain&&(document.cookie=e+"=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain="+r.cookieDomain,document.cookie=e+"=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain="+r.cookieDomain+"; Secure");var a=o.split(".");if(a.length>2){var i="."+a.slice(1).join(".");document.cookie=e+"=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain="+i,document.cookie=e+"=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain="+i+"; Secure"}}})}}function Q(e){return(f=e?e.toUpperCase():null)?r.regionRules.gdpr&&-1!==r.regionRules.gdpr.indexOf(f)?"gdpr":r.regionRules.ccpa&&-1!==r.regionRules.ccpa.indexOf(f)?"ccpa":"none":"gdpr"}function X(e){if(window.__ccGeo)return e(Q(window.__ccGeo));r.geoApi?fetch(r.geoApi).then(function(e){return e.json()}).then(function(t){var n=t.country_code||t.country||"";n?e(Q(n)):ee(e)}).catch(function(){ee(e)}):ee(e)}function ee(e){var t="";try{t=Intl.DateTimeFormat().resolvedOptions().timeZone||""}catch(e){}return 0===t.indexOf("Europe/")||"Atlantic/Reykjavik"===t?(f=null,e("gdpr")):0===t.indexOf("US/")||0===t.indexOf("America/")&&["New_York","Chicago","Denver","Los_Angeles","Phoenix","Anchorage","Boise","Detroit","Indianapolis","Louisville","Menominee","Nome","Adak","Juneau","Sitka","Yakutat","Metlakatla","Kentucky","Indiana","North_Dakota"].some(function(e){return-1!==t.indexOf(e)})||"Pacific/Honolulu"===t?(f="US",e("ccpa")):(f=null,void e("gdpr"))}function te(){"dark"===r.theme&&document.documentElement.classList.add("cc-dark"),"right"===r.iconPosition&&document.documentElement.classList.add("cc-icon-right")}function ne(){var e=r.text,t=document.createElement("div");t.id="cc-overlay",document.body.appendChild(t);var n=document.createElement("div");n.id="cc-banner",n.setAttribute("role","dialog"),n.setAttribute("aria-modal","true"),n.setAttribute("aria-label","Cookie consent"),n.setAttribute("tabindex","-1");var o=r.cookieTypes.map(function(t){var n=t.required?' <span class="cc-always-on">'+v(e.alwaysOnLabel)+"</span>":"";return'<div class="cc-cat"><div><div class="cc-cat-label">'+v(t.name)+n+'</div><div class="cc-cat-desc">'+v(t.description||"")+'</div></div><label class="cc-toggle"><input type="checkbox" id="cc-cb-'+v(t.id)+'" aria-label="'+v(t.name)+'"'+(t.required||t.defaultValue?" checked":"")+(t.required?" disabled":"")+'><span class="cc-slider"></span></label></div>'}).join("");n.innerHTML='<div class="cc-inner"><p class="cc-title">'+v(e.title)+'</p><p class="cc-text">'+v(e.description)+' <a href="'+v(r.privacyUrl)+'">Privacy Policy</a></p><div class="cc-buttons"><button class="cc-btn cc-btn-accept" id="cc-accept">'+v(e.acceptAllButton)+'</button><button class="cc-btn cc-btn-details" id="cc-details" aria-expanded="false">'+v(e.showDetailsButton)+'</button></div><div class="cc-categories" id="cc-cats">'+o+'</div><div class="cc-pref-buttons" id="cc-pref-buttons"><button class="cc-btn cc-btn-save" id="cc-save">'+v(e.saveButton)+'</button><button class="cc-btn cc-btn-decline" id="cc-decline">'+v(e.declineAllButton)+"</button></div></div>"+(r.showPoweredBy?'<div class="cc-powered"><a href="https://www.cookieflame.com" target="_blank" rel="noopener">Powered by CookieFlame</a></div>':""),document.body.appendChild(n);var c=document.createElement("button");c.id="cc-icon",c.setAttribute("aria-label","Cookie settings"),c.innerHTML='<svg width="24" height="24" viewBox="0 0 38 38" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M19.1 1.16c-.06-.42-.38-.77-.8-.83A11.8 11.8 0 0 0 13.28 1.2L7.5 4.3A11.8 11.8 0 0 0 3.95 7.98L1.09 13.9A11.8 11.8 0 0 0 .38 18.96l1.13 6.47a11.8 11.8 0 0 0 3.53 4.99l4.71 4.56a11.8 11.8 0 0 0 4.59 2.24l6.48.91a11.8 11.8 0 0 0 5.52-.89l5.78-3.09a11.8 11.8 0 0 0 3.55-3.69l2.87-5.9a11.8 11.8 0 0 0 .71-5.08c-.07-.41-.42-.73-.83-.79a14.8 14.8 0 0 1-8.16-7.61c-.14-.59-.63-1.05-1.23-1.14A14.8 14.8 0 0 1 19.1 1.16ZM15.25 10.25a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5Zm-5 15a2.5 2.5 0 1 1 5 0 2.5 2.5 0 0 1-5 0Zm17.5-5a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5Z" fill="currentColor"/></svg>',document.body.appendChild(c),document.getElementById("cc-accept").addEventListener("click",se),document.getElementById("cc-decline").addEventListener("click",de),document.getElementById("cc-save").addEventListener("click",ue),document.getElementById("cc-details").addEventListener("click",le),c.addEventListener("click",function(){re("gdpr")})}var oe=null;function ce(e){if("Tab"===e.key){var t=document.getElementById("cc-banner")||document.getElementById("cc-ccpa-banner");if(t&&t.classList.contains("cc-show")){for(var n=[],o=t.querySelectorAll('button, a[href], input:not([disabled]), select, textarea, [tabindex]:not([tabindex="-1"])'),c=0;c<o.length;c++){for(var r=o[c],a=!1,i=r.parentElement;i&&i!==t;){if(i.style&&"none"===window.getComputedStyle(i).display){a=!0;break}i=i.parentElement}a||n.push(r)}if(0!==n.length){var s=n[0],d=n[n.length-1];e.shiftKey?document.activeElement===s&&(e.preventDefault(),d.focus()):document.activeElement===d&&(e.preventDefault(),s.focus())}}}}function re(e){"ccpa"!==e?(oe=document.activeElement,requestAnimationFrame(function(){var e=document.getElementById("cc-overlay"),t=document.getElementById("cc-banner"),n=document.getElementById("cc-icon");if(e&&e.classList.add("cc-show"),t){t.classList.add("cc-show");var o=t.querySelector(".cc-btn-accept");o&&o.focus(),document.addEventListener("keydown",ce)}n&&n.classList.remove("cc-show")})):requestAnimationFrame(function(){var e=document.getElementById("cc-ccpa-banner");e&&(e.classList.add("cc-show"),oe=document.activeElement,e.focus(),document.addEventListener("keydown",ce))})}function ae(){var e=document.getElementById("cc-overlay"),t=document.getElementById("cc-banner"),n=document.getElementById("cc-icon");if(e&&e.classList.remove("cc-show"),t&&t.classList.remove("cc-show"),n&&"gdpr"===p&&n.classList.add("cc-show"),document.removeEventListener("keydown",ce),oe&&"function"==typeof oe.focus)try{oe.focus()}catch(e){}oe=null}function ie(){var e=document.getElementById("cc-overlay"),t=document.getElementById("cc-banner"),n=document.getElementById("cc-icon");e&&e.classList.remove("cc-show"),t&&t.classList.remove("cc-show"),n&&n.classList.remove("cc-show"),document.removeEventListener("keydown",ce)}function se(){F(Y(),"accept_all"),ae(),_("cc:accept-all"),_("cc:update")}function de(){F(z(),"decline_all"),ae(),_("cc:decline-all"),_("cc:update")}function ue(){var e;F((e={ts:(new Date).toISOString(),cv:r.consentVersion,cc:f},r.cookieTypes.forEach(function(t){if(t.required)e[t.id]=!0;else{var n=document.getElementById("cc-cb-"+t.id);e[t.id]=!!n&&n.checked}}),e),"save_preferences"),ae(),_("cc:save"),_("cc:update")}function le(){var e=document.getElementById("cc-cats"),t=document.getElementById("cc-details"),n=document.getElementById("cc-pref-buttons"),o=e.classList.toggle("cc-open");t.textContent=o?r.text.hideDetailsButton:r.text.showDetailsButton,t.setAttribute("aria-expanded",o?"true":"false"),n&&(o?n.classList.add("cc-open"):n.classList.remove("cc-open"))}function pe(){var e=document.getElementById("cc-declaration");if(e){for(var t=k(),n={},o=document.cookie.split(";").map(function(e){return e.trim().split("=")[0]}).filter(Boolean),c=0;c<o.length;c++){var a=u(o[c]);a&&(n[o[c]]={name:o[c],vendor:a.vendor,category:a.category,purpose:a.purpose,expiry:a.expiry||"",active:!0})}for(var i=0;i<d.length;i++){var s=d[i];s.name&&s.vendor&&s.category&&"unclassified"!==s.category&&!n[s.name]&&(n[s.name]={name:s.name,vendor:s.vendor,category:s.category,purpose:s.purpose||"",expiry:s.expiry||"",active:!1})}for(var l=0;l<o.length;l++)n[o[l]]||(n[o[l]]={name:o[l],vendor:"",category:"unclassified",purpose:"",expiry:"",active:!0});var p=["necessary","preferences","analytics","marketing"],f={};r.cookieTypes.forEach(function(e){f[e.id]=e.name});var m={};p.forEach(function(e){m[e]=[]}),Object.keys(n).forEach(function(e){var t=n[e],o=t.category;m[o]||(m[o]=[]),m[o].push(t)}),p.forEach(function(e){m[e]&&m[e].sort(function(e,t){return e.name.localeCompare(t.name)})});var g="<div>";if(t.timestamp){var y=new Date(t.timestamp);g+="<p>"+v("Your consent was last updated on "+y.toLocaleDateString(void 0,{year:"numeric",month:"long",day:"numeric"})+" at "+y.toLocaleTimeString(void 0,{hour:"2-digit",minute:"2-digit"})+".")+"</p>"}else g+="<p>"+v("You have not yet made a cookie preference choice on this site.")+"</p>";for(var h=0;h<p.length;h++){var w=p[h],b=m[w];if(b&&b.length){var _,x=f[w]||w,E=r.cookieTypes.filter(function(e){return e.id===w})[0],A=E&&E.description||"";_=!!E&&E.required?"Always active":t[w]?"Active":"Inactive",g+="<div>",g+="<p><b>"+v(x)+"</b> ("+v(_)+")</p>",A&&(g+="<p>"+v(A)+"</p>"),g+="<table><thead><tr>",g+="<th>Name</th><th>Provider</th><th>Purpose</th><th>Expiry</th>",g+="</tr></thead><tbody>";for(var S=0;S<b.length;S++){var L=b[S];g+="<tr>",g+="<td><code>"+v(L.name)+"</code></td>",g+="<td>"+v(L.vendor||"Unknown")+"</td>",g+="<td>"+v(L.purpose||"Not specified")+"</td>",g+="<td>"+v(L.expiry||"—")+"</td>",g+="</tr>"}g+="</tbody></table></div>"}}g+="</div>",e.innerHTML=g}}window.__ccInternal={getResourceSnapshot:ye,scriptLoaded:function(e){for(var t=ge||ye(),n=0;n<t.allUrls.length;n++)if(-1!==t.allUrls[n].indexOf(e))return!0;return!1},scriptLoadedUrls:function(e){for(var t=ge||ye(),n=[],o=0;o<t.allUrls.length;o++)-1!==t.allUrls[o].indexOf(e)&&n.push(t.allUrls[o]);return n},hasCookie:function(e){return document.cookie.split(";").some(function(t){return t.trim().split("=")[0]===e})},hasCookiePrefix:function(e){return document.cookie.split(";").some(function(t){return 0===t.trim().split("=")[0].indexOf(e)})},setResourceSnapshot:function(e){ge=e},visitorCookies:function(e){for(var t=ye().thirdPartyDomains,n={},o=0;o<e.length;o++)we(e[o],t,he(e[o]))&&(n[e[o]]=!0);return n},facadeBridge:{getConfig:function(){return r},getConsentState:function(){return l},loadConsent:Z,saveConsent:function(e,t){F(e,t)},hideAllBanners:ie,registerRenderer:function(e){R=e,M=2,V()}}},window.CookieConsent={getState:k,getConfig:function(){return r},getComplianceMode:function(){return p},getCountry:function(){return f},show:function(){re("gdpr")},acceptAll:se,declineAll:de,getGPC:function(){return i},getConsentVersion:function(){return r.consentVersion},on:function(e,t){window.addEventListener(e,t)},off:function(e,t){window.removeEventListener(e,t)},VERSION:c},window.addEventListener("cc:ready",pe),window.addEventListener("cc:update",pe),function(){if(!window.__ccDefaultsSet){var e={wait_for_update:500};r.cookieTypes.forEach(function(t){t.consentMode&&t.consentMode.forEach(function(n){e[n]=t.required?"granted":"denied"})}),x("consent","default",e)}}(),function(){if(r.tcfEnabled){var e=window.__tcfapi&&window.__tcfapi.queue?window.__tcfapi.queue:[];window.__tcfapi=C,e.forEach(function(e){C.apply(null,e)}),S=!0,window===window.top&&window.addEventListener("message",function(e){var t;try{t="string"==typeof e.data?JSON.parse(e.data):e.data}catch(e){return}if(t&&t.__tcfapiCall){var n=t.__tcfapiCall;C(n.command,n.version,function(t,o){var c={__tcfapiReturn:{returnValue:t,success:o,callId:n.callId}};e.source&&e.source.postMessage("string"==typeof e.data?JSON.stringify(c):c,"*")},n.parameter)}})}}();var fe=Z();function me(){var e=window.__ccDiscovery&&window.__ccDiscovery.active;if(!e&&(window.__ccDNT||document.cookie.split(";").some(function(e){return 0===e.trim().indexOf("cookieflame_dnt=")})))return E(l=z()),K(l,Y()),J(),j(),p="dnt",te(),ne(),ie(),$(l),_("cc:ready"),void pe();if(e){E(l=Y()),T(l),J(),H(l,null),r.tcfEnabled&&B(l),window.dataLayer=window.dataLayer||[];for(var t=["consent_accepted_analytics","consent_accepted_marketing","consent_accepted_preferences","consent_accepted_necessary","consent_update","consent_given","cookie_consent_given","cookie_consent_update","consent_accepted"],n=0;n<t.length;n++)window.dataLayer.push({event:t[n]});return p="discovery",te(),ne(),ie(),_("cc:ready"),_("cc:update"),void pe()}var o=Z();if(o)if(i&&(o.marketing=!1),l=o,E(o),T(o),J(),j(),H(o,null),r.tcfEnabled&&B(o),te(),ne(),ie(),$(o),o.cc){if("gdpr"===(p=Q(o.cc))){var c=document.getElementById("cc-icon");c&&c.classList.add("cc-show")}_("cc:ready"),pe()}else X(function(e){if(p=e,"gdpr"===e){var t=document.getElementById("cc-icon");t&&t.classList.add("cc-show")}_("cc:ready"),pe()});else X(function(e){if(p=e,"none"===e)return J(),F(Y(),"auto_grant_non_eu"),_("cc:ready"),_("cc:update"),void pe();if("ccpa"===e){if(J(),i){var t=Y();t.marketing=!1,F(t,"ccpa_gpc_optout")}else F(Y(),"ccpa_auto_grant");return te(),n=r.text,(o=document.createElement("div")).id="cc-ccpa-banner",o.setAttribute("role","dialog"),o.setAttribute("aria-modal","true"),o.setAttribute("aria-label","Privacy notice"),o.setAttribute("tabindex","-1"),o.innerHTML='<div class="cc-ccpa-inner"><div class="cc-ccpa-text"><p>'+v(n.ccpaDescription)+'</p></div><div class="cc-ccpa-actions"><a href="'+v(r.ccpaUrl)+'" class="cc-ccpa-link">'+v(n.ccpaDnsmpiLink)+'</a><button class="cc-btn cc-btn-accept" id="cc-ccpa-ok">'+v(n.ccpaAcceptButton)+"</button></div></div>",document.body.appendChild(o),document.getElementById("cc-ccpa-ok").addEventListener("click",function(){F(Y(),"ccpa_acknowledge"),o.classList.remove("cc-show"),_("cc:update")}),re("ccpa"),_("cc:ready"),_("cc:update"),void pe()}var n,o;te(),J(),j(),ne(),re("gdpr"),_("cc:ready"),pe()})}fe&&(l=fe,E(fe)),function(){if(!g&&r.autoBlockPatterns&&r.autoBlockPatterns.length){g=!0;var e=document.createElement.bind(document),t=Object.getOwnPropertyDescriptor(HTMLScriptElement.prototype,"src");t&&t.set&&(document.createElement=function(){var n=e.apply(document,arguments);if("script"!==(arguments[0]||"").toLowerCase())return n;function o(e,t){if(t&&"string"==typeof t&&!e.getAttribute("data-cc")&&!e.getAttribute("data-cc-auto")&&-1===t.indexOf("cookieconsent")){var n=I(t);!n||l&&l[n]||(e.type="text/plain",e.setAttribute("data-cc",n),e.setAttribute("data-cc-auto","1"))}}Object.defineProperty(n,"src",{configurable:!0,enumerable:!0,get:function(){return t.get.call(this)},set:function(e){return o(this,e),t.set.call(this,e)}});var c=n.setAttribute.bind(n);return n.setAttribute=function(e,t){return"src"===e.toLowerCase()&&o(n,t),c(e,t)},n}),new MutationObserver(function(e){e.forEach(function(e){e.addedNodes.forEach(function(e){if(1===e.nodeType){var t=[];"SCRIPT"===e.tagName&&t.push(e),e.querySelectorAll&&e.querySelectorAll("script[src]").forEach(function(e){t.push(e)}),t.forEach(function(e){if(!(e.getAttribute("data-cc-auto")||e.getAttribute("data-cc")||e.src&&-1!==e.src.indexOf("cookieconsent"))){var t=I(e.src);t&&(l&&l[t]||(e.type="text/plain",e.setAttribute("data-cc",t),e.setAttribute("data-cc-auto","1")))}})}})})}).observe(document.documentElement,{childList:!0,subtree:!0})}}(),"loading"===document.readyState?document.addEventListener("DOMContentLoaded",me):me();var ge=null;function ye(){for(var e=[],t=[],n=[],o={},c={},r=document.querySelectorAll("script[src]"),a=0;a<r.length;a++)r[a].src&&e.push(r[a].src);for(var i=document.querySelectorAll("iframe[src]"),s=0;s<i.length;s++)i[s].src&&e.push(i[s].src);if(window.performance&&performance.getEntriesByType)for(var d=performance.getEntriesByType("resource"),u=0;u<d.length;u++){var l=d[u].name||"";if(l){-1===e.indexOf(l)&&e.push(l);var p=d[u].initiatorType||"";if("script"===p||"iframe"===p||"xmlhttprequest"===p||"fetch"===p||-1!==l.indexOf(".js"))try{var f=new URL(l),m=f.hostname;if(m===location.hostname||m==="www."+location.hostname||"www."+m===location.hostname){var g=f.pathname.match(/\/wp-content\/plugins\/([^\/]+)\//);g&&!c[g[1]]&&(c[g[1]]=!0,n.push({slug:g[1],url:l}))}else o[m]||(o[m]=!0,t.push(m))}catch(e){}}}return{allUrls:e,thirdPartyDomains:t,pluginScripts:n}}var ve={};function he(e){var t={set_by:"",set_by_plugin:""},n=window.__ccCookieSetters;if(!n||!Object.prototype.hasOwnProperty.call(n,e)||!n[e])return t;try{var o=new URL(String(n[e]).replace(/:\d+(:\d+)?$/,""));if(/^(chrome-extension|moz-extension|safari-web-extension|safari-extension):$/.test(o.protocol))return t.set_by="browser-extension",t;var c=o.hostname;if(c===location.hostname||c==="www."+location.hostname||"www."+c===location.hostname){t.set_by="first-party";var r=o.pathname.match(/\/wp-content\/(?:plugins|themes)\/([^\/]+)\//);r&&(t.set_by_plugin=r[1].toLowerCase())}else t.set_by=c.toLowerCase()}catch(e){}return t}function we(e,t,n){if("browser-extension"===n.set_by)return!0;var o=window.__ccVisitorCookieRules;if(!o||!o.tools)return!1;if(o.allow&&-1!==o.allow.indexOf(e))return!1;if(n.set_by)return!1;for(var c=0;c<o.tools.length;c++){for(var r=o.tools[c],a=-1!==(r.n||[]).indexOf(e),i=0;!a&&i<(r.p||[]).length;i++)0===e.indexOf(r.p[i])&&(a=!0);if(a){for(var s=0;s<t.length;s++)for(var d=0;d<(r.d||[]).length;d++){var u=String(t[s]).toLowerCase(),l=r.d[d];if(u===l||u.slice(-l.length-1)==="."+l)return!1}return!0}}return!1}function be(){var t=window.__ccClassified||[],n=window.__ccUnclassified||[],o=window.__ccBeaconUrl;if(o){var c,r={};for(c=0;c<t.length;c++)r[t[c]]=!0;for(c=0;c<n.length;c++)r[n[c]]=!0;var a=Object.keys(ve);for(c=0;c<a.length;c++)r[a[c]]=!0;var i=Object.keys(s);for(c=0;c<i.length;c++)r[i[c]]=!0;var d={},u=[],l=document.cookie.split(";");for(c=0;c<l.length;c++){var p=l[c].indexOf("="),f=(p>-1?l[c].substring(0,p):l[c]).trim(),m=p>-1?l[c].substring(p+1).trim():"";f&&(u.push(f),d[f]=m)}var g=[];for(c=0;c<u.length;c++)if(!r[u[c]]){for(var y=!1,v=Object.keys(s),h=0;h<v.length;h++)if(s[v[h]].prefix&&0===u[c].indexOf(v[h])){y=!0;break}y||g.push(u[c])}if(0!==g.length){for(var w=ye(),b=w.thirdPartyDomains,k=[],_=0;_<w.pluginScripts.length;_++)k.push(w.pluginScripts[_].slug);for(var x=location.pathname,E=0;E<g.length;E++){var A=g[E],S=!!e[A]&&!r[A],L=ke(d[A]||""),C=he(A);if(!we(A,b,C)||window.__ccDiscovery&&window.__ccDiscovery.active){var B=JSON.stringify({cookie_name:A,script_domains:b,plugin_scripts:k,page_path:x,pre_existing:S,value_shape:L,set_by:C.set_by,set_by_plugin:C.set_by_plugin});ve[A]=!0,navigator.sendBeacon&&navigator.sendBeacon(o,new Blob([B],{type:"application/json"}))}else ve[A]=!0}}}}function ke(e){if(!e||0===e.length)return"empty";if("true"===e||"false"===e||"1"===e||"0"===e)return"boolean";if(/^\d+$/.test(e))return e.length>12?"long-numeric":"numeric";if(/^\d+\.\d+$/.test(e))return"decimal";if(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(e))return"uuid";if(/^[A-Za-z0-9+\/]{20,}={0,2}$/.test(e))return"base64";if(/^(GA\d+\.\d+\.\d+\.\d+|G-[A-Z0-9]+)/.test(e))return"ga-id";try{if("object"==typeof JSON.parse(decodeURIComponent(e)))return"json"}catch(e){}return/^[a-f0-9]{24,}$/i.test(e)?"hex-id":/^\d{10,13}$/.test(e)?"timestamp":e.length<=20?"short-string":e.length<=100?"medium-string":"long-string"}setTimeout(be,3e3),setTimeout(be,15e3),setTimeout(be,45e3),window.addEventListener("cc:update",function(){setTimeout(be,3e3)}),window.addEventListener("pagehide",be)}();
     19!function(){"use strict";var e={};try{for(var t=document.cookie.split(";"),n=0;n<t.length;n++){var c=t[n].trim().split("=")[0];c&&(e[c]=!0)}}catch(e){}var o="5.4.0",r={cookieName:"cc_consent",cookieExpiry:365,privacyUrl:"/privacy-policy",geoApi:"",logEndpoint:"",reconsentDays:365,cspSafe:!1,tcfEnabled:!1,cmpId:1,cmpVersion:1,vendorListVersion:2,storageMethod:"cookie",cookieDomain:"",shareSubdomains:!1,theme:"light",showPoweredBy:!1,iconPosition:"left",consentVersion:"",rejectFirstScreen:!1,visitorLogins:!1,autoBlockPatterns:[{pattern:"googletagmanager.com/gtag",category:"analytics"},{pattern:"google-analytics.com",category:"analytics"},{pattern:"analytics.google.com",category:"analytics"},{pattern:"hotjar.com",category:"analytics"},{pattern:"mixpanel.com",category:"analytics"},{pattern:"segment.com/analytics",category:"analytics"},{pattern:"segment.io",category:"analytics"},{pattern:"clarity.ms",category:"analytics"},{pattern:"plausible.io",category:"analytics"},{pattern:"js.hs-analytics.net",category:"analytics"},{pattern:"connect.facebook.net",category:"marketing"},{pattern:"snap.licdn.com",category:"marketing"},{pattern:"ads.linkedin.com",category:"marketing"},{pattern:"googleads.g.doubleclick.net",category:"marketing"},{pattern:"googlesyndication.com",category:"marketing"},{pattern:"googleadservices.com",category:"marketing"},{pattern:"ads-twitter.com",category:"marketing"},{pattern:"analytics.tiktok.com",category:"marketing"},{pattern:"pintrk",category:"marketing"},{pattern:"s.pinimg.com/ct",category:"marketing"},{pattern:"widget.intercom.io",category:"preferences"},{pattern:"js.intercomcdn.com",category:"preferences"}],regionRules:{gdpr:["AT","BE","BG","CY","CZ","DE","DK","EE","ES","FI","FR","GR","HR","HU","IE","IT","LT","LU","LV","MT","NL","PL","PT","RO","SE","SI","SK","IS","LI","NO","GB","GG","JE","IM","CH","BR","ZA"],ccpa:["US"]},text:{title:"We value your privacy",description:"We use cookies to improve your experience, analyse traffic and personalise content. You can choose which categories to allow.",acceptAllButton:"Accept All",rejectAllButton:"Decline All",declineAllButton:"Decline All",saveButton:"Save Preferences",showDetailsButton:"Manage preferences",hideDetailsButton:"Hide preferences",alwaysOnLabel:"(always on)",ccpaTitle:"Your Privacy Choices",ccpaDescription:"We use cookies and similar technologies. Under US state privacy laws, such as California's, you have the right to opt out of the sale or sharing of your personal information.",ccpaAcceptButton:"OK",ccpaDnsmpiLink:"Do Not Sell or Share My Personal Information",ccpaGpcNotice:"Your browser asked us not to sell or share your information. We're honouring that.",ccpaChoiceTitle:"Do Not Sell or Share My Personal Information",ccpaChoiceText:"Information about your visit may be used by advertising partners whose cookies run on this site. Under US state privacy laws that can count as selling or sharing. You can opt out in this browser.",ccpaOptOutButton:"Opt out",ccpaCancelButton:"Cancel",ccpaOptedOut:"You've opted out of the sale or sharing of your personal information in this browser.",ccpaGpcOptedOut:"Your browser's Global Privacy Control signal asked us not to sell or share your information, so you're opted out in this browser.",ccpaDoneButton:"Done",ccpaOptInButton:"Allow selling or sharing",ccpaOptInGpcNote:"Allowing it overrides your browser's Global Privacy Control signal on this site only.",ccpaMoreSettings:"More cookie settings",gpcPrefNote:"Your browser's Global Privacy Control signal is on, so Marketing stays off unless you switch it on here.",changePreferences:"Change cookie preferences",loginCookiesTitle:"If you log in",loginCookiesIntro:"These cookies are only set if you log in to an account on this site. They keep you signed in and remember your account settings. They're needed for logging in to work, so they don't ask for your consent."},cookieTypes:[{id:"necessary",name:"Necessary",description:"Required for the site to function. Cannot be disabled.",required:!0,consentMode:["security_storage","functionality_storage"],tcfPurposes:[1],onAccept:function(){}},{id:"preferences",name:"Preferences",description:"Remember your settings and personalisation choices.",required:!1,defaultValue:!1,consentMode:["functionality_storage","personalization_storage"],tcfPurposes:[2,3],onAccept:function(){},onReject:function(){}},{id:"analytics",name:"Analytics",description:"Help us understand how visitors use the site.",required:!1,defaultValue:!1,consentMode:["analytics_storage"],tcfPurposes:[4,5,6],onAccept:function(){},onReject:function(){}},{id:"marketing",name:"Marketing",description:"Used to deliver relevant ads and track campaigns.",required:!1,defaultValue:!1,consentMode:["ad_storage","ad_user_data","ad_personalization"],tcfPurposes:[7,8,9,10],onAccept:function(){},onReject:function(){}}]};if(window.__ccConfig&&"object"==typeof window.__ccConfig){var a=window.__ccConfig;["cookieName","cookieExpiry","privacyUrl","geoApi","logEndpoint","reconsentDays","cspSafe","tcfEnabled","cmpId","cmpVersion","vendorListVersion","storageMethod","cookieDomain","shareSubdomains","theme","showPoweredBy","iconPosition","consentVersion","rejectFirstScreen","visitorLogins"].forEach(function(e){void 0!==a[e]&&(r[e]=a[e])}),a.text&&"object"==typeof a.text&&Object.keys(a.text).forEach(function(e){r.text[e]=a.text[e]}),Array.isArray(a.cookieTypes)&&(r.cookieTypes=a.cookieTypes),Array.isArray(a.autoBlockPatterns)&&(r.autoBlockPatterns=a.autoBlockPatterns),a.regionRules&&(r.regionRules=a.regionRules)}window.__ccConsentVersion&&(r.consentVersion=window.__ccConsentVersion);var i=!!window.__ccGPC;function s(e){return i&&e&&!e.gpcOptIn&&(e.marketing=!1,e.gpc=1),e}window.__ccBlockRules&&Array.isArray(window.__ccBlockRules)&&window.__ccBlockRules.length&&(r.autoBlockPatterns=window.__ccBlockRules);var d={cc_consent:{vendor:"CookieFlame",category:"necessary",purpose:"Stores your consent preferences",expiry:"1 year"},cc_anon_id:{vendor:"CookieFlame",category:"necessary",purpose:"Anonymous ID for consent logging",expiry:"1 year"},cookieflame_dnt:{vendor:"CookieFlame",category:"necessary",purpose:"Do-not-track opt-out flag",expiry:"1 year"},PHPSESSID:{vendor:"PHP",category:"necessary",purpose:"Server-side session identifier",expiry:"Session"},"wp-settings-":{vendor:"WordPress",category:"necessary",purpose:"Remembers dashboard layout settings",prefix:!0,expiry:"1 year"},"wp-settings-time-":{vendor:"WordPress",category:"necessary",purpose:"Records when dashboard settings were last saved",prefix:!0,expiry:"1 year"},wordpress_logged_in_:{vendor:"WordPress",category:"necessary",purpose:"Keeps you logged in",prefix:!0,expiry:"Session, or 14 days with Remember Me"},wordpress_sec_:{vendor:"WordPress",category:"necessary",purpose:"Confirms your login when you open the dashboard",prefix:!0,expiry:"Session, or 14 days with Remember Me"},wordpress_rec_:{vendor:"WordPress",category:"necessary",purpose:"Recovery mode after a site error (administrators only)",prefix:!0,expiry:"1 week"},wordpress_test_cookie:{vendor:"WordPress",category:"necessary",purpose:"Checks your browser accepts cookies before you log in",expiry:"Session"},wp_lang:{vendor:"WordPress",category:"necessary",purpose:"Remembers the language picked on the login page",expiry:"Session"},comment_author_:{vendor:"WordPress",category:"necessary",purpose:"Remembers your name for comments, if you ask it to",prefix:!0,expiry:"1 year"},comment_author_email_:{vendor:"WordPress",category:"necessary",purpose:"Remembers your email for comments, if you ask it to",prefix:!0,expiry:"1 year"},comment_author_url_:{vendor:"WordPress",category:"necessary",purpose:"Remembers your website for comments, if you ask it to",prefix:!0,expiry:"1 year"},"wp-postpass_":{vendor:"WordPress",category:"necessary",purpose:"Remembers the password entered for a protected page",prefix:!0,expiry:"10 days"},woocommerce_cart_hash:{vendor:"WooCommerce",category:"necessary",purpose:"Shopping cart contents",expiry:"Session"},woocommerce_items_in_cart:{vendor:"WooCommerce",category:"necessary",purpose:"Cart item count",expiry:"Session"},wp_woocommerce_session_:{vendor:"WooCommerce",category:"necessary",purpose:"Shopping session",prefix:!0,expiry:"2 days"},_ga:{vendor:"Google Analytics",category:"analytics",purpose:"Distinguishes visitors",expiry:"2 years"},_ga_:{vendor:"Google Analytics",category:"analytics",purpose:"Stores session state",prefix:!0,expiry:"2 years"},_gid:{vendor:"Google Analytics",category:"analytics",purpose:"Distinguishes visitors (24h)",expiry:"24 hours"},_gat:{vendor:"Google Analytics",category:"analytics",purpose:"Throttles request rate",expiry:"1 minute"},wp_wk_ga_untrack_:{vendor:"Google Analytics",category:"analytics",purpose:"GA opt-out flag",prefix:!0,expiry:"Persistent"},_gcl_au:{vendor:"Google Ads",category:"marketing",purpose:"Conversion linker",expiry:"90 days"},_gcl_aw:{vendor:"Google Ads",category:"marketing",purpose:"Stores ad click info",expiry:"90 days"},_gac_:{vendor:"Google Ads",category:"marketing",purpose:"Campaign information",prefix:!0,expiry:"90 days"},IDE:{vendor:"Google DoubleClick",category:"marketing",purpose:"Ad serving and frequency",expiry:"1 year"},test_cookie:{vendor:"Google DoubleClick",category:"marketing",purpose:"Checks cookie support",expiry:"15 minutes"},NID:{vendor:"Google",category:"preferences",purpose:"Stores preferences",expiry:"6 months"}},u=window.__ccCustomCookies||[];function l(e){for(var t=0;t<u.length;t++){var n=u[t];if("exact"===n.match&&e===n.name)return n;if("prefix"===n.match&&0===e.indexOf(n.name))return n;if("contains"===n.match&&-1!==e.indexOf(n.name))return n}var c=d[e];if(c)return c;var o=0;return Object.keys(d).forEach(function(t){d[t].prefix&&t.length>o&&0===e.indexOf(t)&&(c=d[t],o=t.length)}),c||null}var p=/^(?:wordpress_(?:logged_in_|sec_|rec_|test_cookie$|[0-9a-f]{32}$)|wp-settings-|wp_lang$)/;function f(e){return p.test(e)}var g=null,m="none",y=null,v=null,h=!1,w=0;function b(e){return"string"!=typeof e&&(e=String(e)),e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;")}function k(e){if("localStorage"===r.storageMethod)try{return localStorage.getItem(e)}catch(e){return null}var t=document.cookie.match(new RegExp("(?:^|;\\s*)"+e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")+"=([^;]*)"));if(!t)return null;try{return decodeURIComponent(t[1])}catch(e){return null}}function _(e,t,n){if("localStorage"!==r.storageMethod){var c=new Date;c.setTime(c.getTime()+864e5*n);var o=e+"="+encodeURIComponent(t)+";expires="+c.toUTCString()+";path=/;SameSite=Lax;Secure";r.cookieDomain&&(o+=";domain="+r.cookieDomain),document.cookie=o}else try{localStorage.setItem(e,t)}catch(e){}}function x(){if(v)return v;var e=k("cc_anon_id");return e?(v=e,e):(_("cc_anon_id",v="cc_"+function(){if("undefined"!=typeof crypto&&"function"==typeof crypto.randomUUID)try{return crypto.randomUUID()}catch(e){}return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,function(e){var t=16*Math.random()|0;return("x"===e?t:3&t|8).toString(16)})}(),730),v)}function E(){var e={timestamp:g?g.ts:null,complianceMode:m,country:y,gpc:i,consentVersion:r.consentVersion};return r.cookieTypes.forEach(function(t){e[t.id]=g?!!g[t.id]:!!t.required}),e}function A(e,t){window.dispatchEvent(new CustomEvent(e,{detail:t||E()}))}function S(){window.dataLayer=window.dataLayer||[],window.dataLayer.push(arguments)}function C(e){var t={};r.cookieTypes.forEach(function(n){n.consentMode&&n.consentMode.forEach(function(c){t[c]=e[n.id]?"granted":"denied"})}),i&&!e.gpcOptIn&&(t.ad_storage="denied",t.ad_user_data="denied",t.ad_personalization="denied"),S("consent","update",t)}var I=new Map,L=!1;function B(e){var t={};return r.cookieTypes.forEach(function(n){n.tcfPurposes&&n.tcfPurposes.forEach(function(c){t[c]=!!e[n.id]})}),{tcString:"",tcfPolicyVersion:4,cmpId:r.cmpId,cmpVersion:r.cmpVersion,gdprApplies:"gdpr"===m,eventStatus:"useractioncomplete",cmpStatus:"loaded",listenerId:void 0,isServiceSpecific:!0,useNonStandardTexts:!1,publisherCC:"GB",purposeOneTreatment:!1,purpose:{consents:t,legitimateInterests:{}},vendor:{consents:{},legitimateInterests:{}},specialFeatureOptins:{},publisher:{consents:{},legitimateInterests:{},customPurpose:{consents:{},legitimateInterests:{}},restrictions:{}}}}function O(e,t,n,c){if("function"==typeof n)if("addEventListener"===e){var o=++w;if(I.set(o,n),g){var a=B(g);a.listenerId=o,n(a,!0)}}else"removeEventListener"===e?n({listenerId:c},I.delete(c)):"getTCData"===e?n(g?B(g):null,!!g):"ping"===e?n({gdprApplies:"gdpr"===m,cmpLoaded:L,cmpStatus:L?"loaded":"stub",displayStatus:"hidden",apiVersion:"2.2",cmpVersion:r.cmpVersion,cmpId:r.cmpId,gvlVersion:r.vendorListVersion,tcfPolicyVersion:4},!0):n(null,!1)}function T(e){I.forEach(function(t,n){var c=B(e);c.listenerId=n;try{t(c,!0)}catch(e){}})}function P(e){if(!e)return null;for(var t=e.toLowerCase(),n=0;n<r.autoBlockPatterns.length;n++){var c=r.autoBlockPatterns[n];if(c.pattern instanceof RegExp){if(c.pattern.test(t))return c.category}else if(-1!==t.indexOf(c.pattern.toLowerCase()))return c.category}return null}function D(e){document.querySelectorAll('script[type="text/plain"][data-cc]').forEach(function(t){var n=(t.getAttribute("data-cc")||"").toLowerCase();if(e[n]&&!t.getAttribute("data-cc-activated")){t.setAttribute("data-cc-activated","1");var c=document.createElement("script");c.isWPRocket=!0,Array.from(t.attributes).forEach(function(e){"type"!==e.name&&"data-cc"!==e.name&&"data-cc-activated"!==e.name&&"data-cc-auto"!==e.name&&c.setAttribute(e.name,e.value)}),!t.src&&t.textContent&&(c.textContent=t.textContent),t.parentNode.insertBefore(c,t.nextSibling),t.parentNode.removeChild(t)}}),["img","iframe","link"].forEach(function(t){document.querySelectorAll(t+"[data-cc][data-cc-src]").forEach(function(n){var c=(n.getAttribute("data-cc")||"").toLowerCase();e[c]&&!n.getAttribute("data-cc-activated")&&(n.setAttribute("data-cc-activated","1"),"link"===t?n.href=n.getAttribute("data-cc-src"):n.src=n.getAttribute("data-cc-src"))})})}var R=[{pattern:"youtube.com",category:"marketing"},{pattern:"youtube-nocookie.com",category:"marketing"},{pattern:"youtu.be",category:"marketing"},{pattern:"player.vimeo.com",category:"marketing"},{pattern:"vimeo.com",category:"marketing"},{pattern:"dailymotion.com",category:"marketing"},{pattern:"open.spotify.com",category:"preferences"},{pattern:"soundcloud.com",category:"preferences"},{pattern:"google.com/maps",category:"preferences"},{pattern:"maps.google.com",category:"preferences"},{pattern:"facebook.com/plugins",category:"marketing"},{pattern:"instagram.com",category:"marketing"},{pattern:"tiktok.com/embed",category:"marketing"},{pattern:"twitter.com",category:"marketing"},{pattern:"x.com",category:"marketing"}];function M(e,t){var n,c;try{var o=new URL(e,location.href);n=o.hostname.toLowerCase(),c=o.pathname.toLowerCase()}catch(e){return!1}var r=t.indexOf("/"),a=-1===r?t:t.slice(0,r),i=-1===r?"":t.slice(r);return(n===a||n.slice(-a.length-1)==="."+a)&&(!i||0===c.indexOf(i))}function N(e){if(!e)return null;for(var t=0;t<R.length;t++)if(M(e,R[t].pattern))return R[t].category;return null}function q(){return!(!document.body||!(document.body.classList.contains("elementor-editor-active")||document.body.classList.contains("fl-builder-edit")||document.body.classList.contains("et-fb-root-ancestor")))}var G=[],j=0,V=null,U=!1;function J(){if(V&&G.length){var e=G;G=[];try{V(e)}catch(e){}}}function W(){0!==G.length&&(2!==j?1!==j&&3!==j&&(j=1,function(){var e=window.__ccConfig&&window.__ccConfig.facadesUrl||null;if(!e)return void(j=3);var t=document.createElement("script");t.src=e,t.async=!0,t.onload=function(){j=2,J()},t.onerror=function(){j=3},(document.head||document.documentElement).appendChild(t)}()):J())}function F(e){var t=e.src||e.getAttribute("data-src")||"";if(t&&!e.getAttribute("data-cc-facade")&&!q()){var n=N(t);if(n){var c=g||ee();c&&c[n]||(e.setAttribute("data-cc-facade","1"),e.setAttribute("data-cc-iframe-cat",n),e.setAttribute("data-cc-original-src",t),e.removeAttribute("src"),e.style.display="none",G.push({type:"iframe",node:e,category:n,src:t}),W())}}}function H(e){if(!e.getAttribute("data-cc-facade")&&!q()){var t=e.getAttribute("data-settings");if(t){var n;try{n=JSON.parse(t)}catch(e){return}var c=n.youtube_url||n.vimeo_url||"";if(c){var o=c.toLowerCase(),r=null;if((-1!==o.indexOf("youtube")||-1!==o.indexOf("youtu.be")||-1!==o.indexOf("vimeo")||-1!==o.indexOf("dailymotion"))&&(r="marketing"),r&&(!g||!g[r])){if(!g){var a=ee();if(a&&a[r])return}e.setAttribute("data-cc-facade","1"),U=!0,G.push({type:"elementor",node:e,category:r,url:c}),W()}}}}}function $(){for(var e=document.querySelectorAll("iframe[src]"),t=0;t<e.length;t++)F(e[t]);for(var n=document.querySelectorAll(".elementor-widget-video"),c=0;c<n.length;c++)H(n[c]);for(var o=document.querySelectorAll(".wp-block-embed__wrapper iframe[src]"),r=0;r<o.length;r++)F(o[r])}function Y(){new MutationObserver(function(e){e.forEach(function(e){e.addedNodes.forEach(function(e){if(1===e.nodeType){if("IFRAME"===e.tagName&&F(e),e.querySelectorAll){for(var t=e.querySelectorAll("iframe[src]"),n=0;n<t.length;n++)F(t[n]);for(var c=e.querySelectorAll(".elementor-widget-video"),o=0;o<c.length;o++)H(c[o])}e.classList&&e.classList.contains("elementor-widget-video")&&H(e)}})})}).observe(document.documentElement,{childList:!0,subtree:!0})}function Z(){for(var e=document.querySelectorAll("iframe[src]"),t=0;t<e.length;t++){var n=e[t];if(!n.getAttribute("data-cc-facade")){var c=n.src||"";c&&N(c)&&(n.src=c)}}}function K(e,t){r.cookieTypes.forEach(function(n){var c=!!e[n.id];if((t?!!t[n.id]:null)!==c||null===t)if(c&&"function"==typeof n.onAccept)try{n.onAccept()}catch(e){console.warn("[CookieConsent] onAccept error for "+n.id,e)}else if(!c&&"function"==typeof n.onReject)try{n.onReject()}catch(e){console.warn("[CookieConsent] onReject error for "+n.id,e)}})}function z(e,t){if(r.logEndpoint){var n={};r.cookieTypes.forEach(function(e){n[e.id]=!!t[e.id]});var c={anonymousId:x(),timestamp:(new Date).toISOString(),action:e,consent:n,complianceMode:m,country:y,url:location.href,userAgent:navigator.userAgent,tcfEnabled:r.tcfEnabled,gpc:i,consentVersion:r.consentVersion,version:o};try{var a=JSON.stringify(c);navigator.sendBeacon?navigator.sendBeacon(r.logEndpoint,new Blob([a],{type:"application/json"})):fetch(r.logEndpoint,{method:"POST",body:a,headers:{"Content-Type":"application/json"},keepalive:!0})}catch(e){}}}function Q(e,t){var n=g;if(t&&0===t.indexOf("facade_accept_")||s(e),g=e,_(r.cookieName,JSON.stringify(e),r.cookieExpiry),C(e),U&&t)for(var c=document.querySelectorAll(".cc-iframe-facade[data-cc-elementor-widget]"),o=0;o<c.length;o++){var a=c[o].getAttribute("data-cc-iframe-cat");if(a&&e[a])return void window.location.reload()}ce(e,n),D(e),function(e){for(var t=document.querySelectorAll(".cc-iframe-facade"),n=0;n<t.length;n++)if(!t[n].getAttribute("data-cc-elementor-widget")&&e[t[n].getAttribute("data-cc-iframe-cat")]){var c=t[n].nextElementSibling;if(c&&"IFRAME"===c.tagName&&c.getAttribute("data-cc-facade")){var o=c.getAttribute("data-cc-original-src");o&&(c.src=o,c.style.display="",c.removeAttribute("data-cc-facade"))}t[n].parentNode.removeChild(t[n])}}(e),t&&"auto_grant_non_eu"!==t&&"ccpa_auto_grant"!==t&&setTimeout(Z,300),K(e,n),X(e),r.tcfEnabled&&T(e),t&&z(t,e)}function X(e){r.cookieTypes.forEach(function(t){if(!t.required){var n=document.getElementById("cc-cb-"+t.id);n&&(n.checked=!!e[t.id])}});var t=document.getElementById("cc-cats"),n=document.getElementById("cc-details"),c=document.getElementById("cc-pref-buttons");t&&t.classList.remove("cc-open"),n&&(n.textContent=r.text.showDetailsButton),c&&c.classList.remove("cc-open")}function ee(){var e=k(r.cookieName);if(!e)return null;try{var t=JSON.parse(e);if(!t||"object"!=typeof t||Array.isArray(t))return null;if(!t.ts||isNaN(new Date(t.ts).getTime()))return null;var n=Number(r.reconsentDays);return n>=1||(n=365),(Date.now()-new Date(t.ts).getTime())/864e5>n||r.consentVersion&&t.cv!==r.consentVersion?null:t}catch(e){return null}}function te(){var e={ts:(new Date).toISOString(),cv:r.consentVersion,cc:y};return r.cookieTypes.forEach(function(t){e[t.id]=!0}),e}function ne(){var e={ts:(new Date).toISOString(),cv:r.consentVersion,cc:y};return r.cookieTypes.forEach(function(t){e[t.id]=!!t.required}),e}function ce(e,t){if(t){var n=[];if(r.cookieTypes.forEach(function(c){c.required||t[c.id]&&!e[c.id]&&n.push(c.id)}),n.length)document.cookie.split(";").map(function(e){return e.trim().split("=")[0]}).filter(Boolean).forEach(function(e){var t=l(e);if(t&&-1!==n.indexOf(t.category)){var c=location.hostname,o="."+c;document.cookie=e+"=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/",document.cookie=e+"=; expires=Thu, 01 Jan 1970 00:00:00 GMT",document.cookie=e+"=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain="+o,document.cookie=e+"=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain="+c,document.cookie=e+"=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; Secure",r.cookieDomain&&(document.cookie=e+"=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain="+r.cookieDomain,document.cookie=e+"=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain="+r.cookieDomain+"; Secure");var a=c.split(".");if(a.length>2){var i="."+a.slice(1).join(".");document.cookie=e+"=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain="+i,document.cookie=e+"=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain="+i+"; Secure"}}})}}function oe(e){return(y=e?e.toUpperCase():null)?r.regionRules.gdpr&&-1!==r.regionRules.gdpr.indexOf(y)?"gdpr":r.regionRules.ccpa&&-1!==r.regionRules.ccpa.indexOf(y)?"ccpa":"none":"gdpr"}function re(e){if(window.__ccGeo)return e(oe(window.__ccGeo));r.geoApi?fetch(r.geoApi).then(function(e){return e.json()}).then(function(t){var n=t.country_code||t.country||"";n?e(oe(n)):ae(e)}).catch(function(){ae(e)}):ae(e)}function ae(e){var t="";try{t=Intl.DateTimeFormat().resolvedOptions().timeZone||""}catch(e){}return 0===t.indexOf("Europe/")||"Atlantic/Reykjavik"===t?(y=null,e("gdpr")):0===t.indexOf("US/")||0===t.indexOf("America/")&&["New_York","Chicago","Denver","Los_Angeles","Phoenix","Anchorage","Boise","Detroit","Indianapolis","Louisville","Menominee","Nome","Adak","Juneau","Sitka","Yakutat","Metlakatla","Kentucky","Indiana","North_Dakota"].some(function(e){return-1!==t.indexOf(e)})||"Pacific/Honolulu"===t?(y="US",e("ccpa")):(y=null,void e("gdpr"))}function ie(){"dark"===r.theme&&document.documentElement.classList.add("cc-dark"),"right"===r.iconPosition&&document.documentElement.classList.add("cc-icon-right")}function se(){var e=r.text,t=document.createElement("div");t.id="cc-overlay",document.body.appendChild(t);var n=document.createElement("div");n.id="cc-banner",n.setAttribute("role","dialog"),n.setAttribute("aria-modal","true"),n.setAttribute("aria-label","Cookie consent"),n.setAttribute("tabindex","-1");var c=r.cookieTypes.map(function(t){var n=t.required?' <span class="cc-always-on">'+b(e.alwaysOnLabel)+"</span>":"",c=i&&"marketing"===t.id?'<div class="cc-gpc-note">'+b(e.gpcPrefNote)+"</div>":"";return'<div class="cc-cat"><div><div class="cc-cat-label">'+b(t.name)+n+'</div><div class="cc-cat-desc">'+b(t.description||"")+"</div>"+c+'</div><label class="cc-toggle"><input type="checkbox" id="cc-cb-'+b(t.id)+'" aria-label="'+b(t.name)+'"'+(t.required||t.defaultValue?" checked":"")+(t.required?" disabled":"")+'><span class="cc-slider"></span></label></div>'}).join("");n.innerHTML='<div class="cc-inner"><p class="cc-title">'+b(e.title)+'</p><p class="cc-text">'+b(e.description)+' <a href="'+b(r.privacyUrl)+'">Privacy Policy</a></p><div class="cc-buttons"><button class="cc-btn cc-btn-accept" id="cc-accept">'+b(e.acceptAllButton)+"</button>"+(r.rejectFirstScreen?'<button class="cc-btn cc-btn-reject" id="cc-reject">'+b(e.rejectAllButton)+"</button>":"")+'<button class="cc-btn cc-btn-details" id="cc-details" aria-expanded="false">'+b(e.showDetailsButton)+'</button></div><div class="cc-categories" id="cc-cats">'+c+'</div><div class="cc-pref-buttons" id="cc-pref-buttons"><button class="cc-btn cc-btn-save" id="cc-save">'+b(e.saveButton)+'</button><button class="cc-btn cc-btn-decline" id="cc-decline">'+b(e.declineAllButton)+"</button></div></div>"+(r.showPoweredBy?'<div class="cc-powered"><a href="https://www.cookieflame.com" target="_blank" rel="noopener">Powered by CookieFlame</a></div>':""),document.body.appendChild(n);var o=document.createElement("button");o.id="cc-icon",o.setAttribute("aria-label","Cookie settings"),o.innerHTML='<svg width="24" height="24" viewBox="0 0 38 38" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M19.1 1.16c-.06-.42-.38-.77-.8-.83A11.8 11.8 0 0 0 13.28 1.2L7.5 4.3A11.8 11.8 0 0 0 3.95 7.98L1.09 13.9A11.8 11.8 0 0 0 .38 18.96l1.13 6.47a11.8 11.8 0 0 0 3.53 4.99l4.71 4.56a11.8 11.8 0 0 0 4.59 2.24l6.48.91a11.8 11.8 0 0 0 5.52-.89l5.78-3.09a11.8 11.8 0 0 0 3.55-3.69l2.87-5.9a11.8 11.8 0 0 0 .71-5.08c-.07-.41-.42-.73-.83-.79a14.8 14.8 0 0 1-8.16-7.61c-.14-.59-.63-1.05-1.23-1.14A14.8 14.8 0 0 1 19.1 1.16ZM15.25 10.25a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5Zm-5 15a2.5 2.5 0 1 1 5 0 2.5 2.5 0 0 1-5 0Zm17.5-5a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5Z" fill="currentColor"/></svg>',document.body.appendChild(o),document.getElementById("cc-accept").addEventListener("click",ke),document.getElementById("cc-decline").addEventListener("click",_e);var a=document.getElementById("cc-reject");a&&a.addEventListener("click",_e),document.getElementById("cc-save").addEventListener("click",xe),document.getElementById("cc-details").addEventListener("click",Ee),o.addEventListener("click",function(){he("gdpr")}),de("gdpr",n)}function de(e,t){A("cc:banner-rendered",{banner:e,element:t,state:E()})}function ue(){if(!document.getElementById("cc-ccpa-banner")){var e=r.text,t=document.createElement("div");t.id="cc-ccpa-banner",t.setAttribute("role","dialog"),t.setAttribute("aria-modal","true"),t.setAttribute("aria-label","Privacy notice"),t.setAttribute("tabindex","-1"),t.innerHTML='<div class="cc-ccpa-inner" id="cc-ccpa-notice"><div class="cc-ccpa-text"><p>'+b(e.ccpaDescription)+"</p>"+(i?'<p class="cc-ccpa-gpc">'+b(e.ccpaGpcNotice)+"</p>":"")+'</div><div class="cc-ccpa-actions"><a href="#" class="cc-ccpa-link" id="cc-ccpa-dns" role="button">'+b(e.ccpaDnsmpiLink)+'</a><button class="cc-btn cc-btn-accept" id="cc-ccpa-ok">'+b(e.ccpaAcceptButton)+'</button></div></div><div class="cc-ccpa-choice" id="cc-ccpa-choice"></div>'+(r.showPoweredBy?'<div class="cc-powered"><a href="https://www.cookieflame.com" target="_blank" rel="noopener">Powered by CookieFlame</a></div>':""),document.body.appendChild(t),document.getElementById("cc-ccpa-ok").addEventListener("click",function(){g&&z("ccpa_acknowledge",g),fe()}),document.getElementById("cc-ccpa-dns").addEventListener("click",function(e){e.preventDefault(),pe(!0)}),de("ccpa",t)}}function le(){var e=document.getElementById("cc-ccpa-choice");if(e){var t=r.text,n=!(g&&g.marketing),c='<div class="cc-ccpa-choice-inner"><p class="cc-ccpa-choice-title">'+b(t.ccpaChoiceTitle)+"</p>";c+=n?'<p class="cc-ccpa-choice-status">'+b(i?t.ccpaGpcOptedOut:t.ccpaOptedOut)+'</p><div class="cc-ccpa-choice-buttons"><button class="cc-btn cc-btn-accept" id="cc-ccpa-done">'+b(t.ccpaDoneButton)+'</button><button class="cc-btn cc-btn-decline" id="cc-ccpa-optin">'+b(t.ccpaOptInButton)+"</button></div>"+(i?'<p class="cc-ccpa-choice-note">'+b(t.ccpaOptInGpcNote)+"</p>":""):'<p class="cc-ccpa-choice-text">'+b(t.ccpaChoiceText)+'</p><div class="cc-ccpa-choice-buttons"><button class="cc-btn cc-btn-accept" id="cc-ccpa-optout">'+b(t.ccpaOptOutButton)+'</button><button class="cc-btn cc-btn-decline" id="cc-ccpa-cancel">'+b(t.ccpaCancelButton)+"</button></div>",c+='<button class="cc-btn cc-btn-details" id="cc-ccpa-more">'+b(t.ccpaMoreSettings)+"</button></div>",e.innerHTML=c;var o=function(e,t){var n=document.getElementById(e);n&&n.addEventListener("click",t)};o("cc-ccpa-done",fe),o("cc-ccpa-cancel",fe),o("cc-ccpa-optout",function(){var e=me(g);e.marketing=!1,Q(e,"ccpa_opt_out"),le(),ge("cc-ccpa-choice"),A("cc:update")}),o("cc-ccpa-optin",function(){var e=me(g);e.marketing=!0,i&&(e.gpcOptIn=1),delete e.gpc,Q(e,"ccpa_opt_in"),le(),ge("cc-ccpa-choice"),A("cc:update")}),o("cc-ccpa-more",function(){fe(),Ae()})}}function pe(e){var t=document.getElementById("cc-ccpa-notice"),n=document.getElementById("cc-ccpa-choice");t&&n&&(e&&le(),t.style.display=e?"none":"",n.classList.toggle("cc-open",!!e),e&&ge("cc-ccpa-choice"))}function fe(){var e=document.getElementById("cc-ccpa-banner");if(e&&e.classList.remove("cc-show"),pe(!1),document.removeEventListener("keydown",ve),ye&&"function"==typeof ye.focus)try{ye.focus()}catch(e){}ye=null}function ge(e){var t=document.getElementById(e),n=t&&t.querySelector("button");if(n)try{n.focus()}catch(e){}}function me(e){var t={ts:(new Date).toISOString(),cv:r.consentVersion,cc:y};return r.cookieTypes.forEach(function(n){t[n.id]=!e||!!e[n.id]}),e&&e.gpcOptIn&&(t.gpcOptIn=1),t}var ye=null;function ve(e){if("Tab"===e.key){var t=document.querySelector("#cc-banner.cc-show, #cc-ccpa-banner.cc-show");if(t){for(var n=[],c=t.querySelectorAll('button, a[href], input:not([disabled]), select, textarea, [tabindex]:not([tabindex="-1"])'),o=0;o<c.length;o++){for(var r=c[o],a=!1,i=r.parentElement;i&&i!==t;){if("none"===window.getComputedStyle(i).display){a=!0;break}i=i.parentElement}a||n.push(r)}if(0!==n.length){var s=n[0],d=n[n.length-1];e.shiftKey?document.activeElement===s&&(e.preventDefault(),d.focus()):document.activeElement===d&&(e.preventDefault(),s.focus())}}}}function he(e){if("ccpa"===e)return ye=document.activeElement,void requestAnimationFrame(function(){var e=document.getElementById("cc-ccpa-banner");if(e){e.classList.add("cc-show");var t=document.getElementById("cc-ccpa-choice");t&&t.classList.contains("cc-open")?ge("cc-ccpa-choice"):e.focus(),document.addEventListener("keydown",ve)}});ye=document.activeElement;var t=document.getElementById("cc-reject");t&&("gdpr"===m?t.style.removeProperty("display"):t.style.setProperty("display","none","important")),requestAnimationFrame(function(){var e=document.getElementById("cc-overlay"),t=document.getElementById("cc-banner"),n=document.getElementById("cc-icon");if(e&&e.classList.add("cc-show"),t){t.classList.add("cc-show");var c=t.querySelector(".cc-btn-accept");c&&c.focus(),document.addEventListener("keydown",ve)}n&&n.classList.remove("cc-show")})}function we(){var e=document.getElementById("cc-overlay"),t=document.getElementById("cc-banner"),n=document.getElementById("cc-icon");if(e&&e.classList.remove("cc-show"),t&&t.classList.remove("cc-show"),n&&"gdpr"===m&&n.classList.add("cc-show"),document.removeEventListener("keydown",ve),ye&&"function"==typeof ye.focus)try{ye.focus()}catch(e){}ye=null}function be(){var e=document.getElementById("cc-overlay"),t=document.getElementById("cc-banner"),n=document.getElementById("cc-icon");e&&e.classList.remove("cc-show"),t&&t.classList.remove("cc-show"),n&&n.classList.remove("cc-show"),document.removeEventListener("keydown",ve)}function ke(){var e=te();g&&g.gpcOptIn&&(e.gpcOptIn=1),Q(e,"accept_all"),we(),A("cc:accept-all"),A("cc:update")}function _e(){Q(ne(),"decline_all"),we(),A("cc:decline-all"),A("cc:update")}function xe(){var e=function(){var e={ts:(new Date).toISOString(),cv:r.consentVersion,cc:y};return r.cookieTypes.forEach(function(t){if(t.required)e[t.id]=!0;else{var n=document.getElementById("cc-cb-"+t.id);e[t.id]=!!n&&n.checked}}),e}();e.marketing&&(i||g&&g.gpcOptIn)&&(e.gpcOptIn=1),Q(e,"save_preferences"),we(),A("cc:save"),A("cc:update")}function Ee(){var e=document.getElementById("cc-cats"),t=document.getElementById("cc-details"),n=document.getElementById("cc-pref-buttons"),c=e.classList.toggle("cc-open");t.textContent=c?r.text.hideDetailsButton:r.text.showDetailsButton,t.setAttribute("aria-expanded",c?"true":"false"),n&&(c?n.classList.add("cc-open"):n.classList.remove("cc-open"))}function Ae(){document.getElementById("cc-banner")||(ie(),se(),be(),g&&X(g)),he("gdpr")}function Se(){"ccpa"===m?(ie(),ue(),he("ccpa"),pe(!0)):Ae()}function Ce(){var e=document.getElementById("cc-declaration");if(e){var t=E(),n={},c=document.cookie.split(";").map(function(e){return e.trim().split("=")[0]}).filter(Boolean);c=c.filter(function(e){return!f(e)});for(var o=0;o<c.length;o++){var a=l(c[o]);a&&(n[c[o]]={name:c[o],vendor:a.vendor,category:a.category,purpose:a.purpose,expiry:a.expiry||"",active:!0})}for(var i=0;i<u.length;i++){var s=u[i];s.name&&s.vendor&&s.category&&"unclassified"!==s.category&&!n[s.name]&&!f(s.name)&&(n[s.name]={name:s.name,vendor:s.vendor,category:s.category,purpose:s.purpose||"",expiry:s.expiry||"",active:!1})}for(var d=0;d<c.length;d++)n[c[d]]||(n[c[d]]={name:c[d],vendor:"",category:"unclassified",purpose:"",expiry:"",active:!0});var p=["necessary","preferences","analytics","marketing"],g={};r.cookieTypes.forEach(function(e){g[e.id]=e.name});var m={};p.forEach(function(e){m[e]=[]}),Object.keys(n).forEach(function(e){var t=n[e],c=t.category;m[c]||(m[c]=[]),m[c].push(t)}),p.forEach(function(e){m[e]&&m[e].sort(function(e,t){return e.name.localeCompare(t.name)})});var y,v="<div>";if(t.timestamp){var h=new Date(t.timestamp);v+="<p>"+b("Your consent was last updated on "+h.toLocaleDateString(void 0,{year:"numeric",month:"long",day:"numeric"})+" at "+h.toLocaleTimeString(void 0,{hour:"2-digit",minute:"2-digit"})+".")+"</p>"}else v+="<p>"+b("You have not yet made a cookie preference choice on this site.")+"</p>";for(var w=0;w<p.length;w++){var k=p[w],_=m[k];if(_&&_.length){var x,A=g[k]||k,S=r.cookieTypes.filter(function(e){return e.id===k})[0],C=S&&S.description||"";x=!!S&&S.required?"Always active":t[k]?"Active":"Inactive",v+="<div>",v+="<p><b>"+b(A)+"</b> ("+b(x)+")</p>",C&&(v+="<p>"+b(C)+"</p>"),v+="<table><thead><tr>",v+="<th>Name</th><th>Provider</th><th>Purpose</th><th>Expiry</th>",v+="</tr></thead><tbody>";for(var I=0;I<_.length;I++){var L=_[I];v+="<tr>",v+="<td><code>"+b(L.name)+"</code></td>",v+="<td>"+b(L.vendor||"Unknown")+"</td>",v+="<td>"+b(L.purpose||"Not specified")+"</td>",v+="<td>"+b(L.expiry||"—")+"</td>",v+="</tr>"}v+="</tbody></table></div>"}}if(r.visitorLogins){var B=[{name:"wordpress_logged_in_[hash]",purpose:"Keeps you logged in",expiry:y="Session, or 14 days with Remember Me"},{name:"https:"===location.protocol?"wordpress_sec_[hash]":"wordpress_[hash]",purpose:"Confirms your login when you open your account dashboard",expiry:y},{name:"wordpress_test_cookie",purpose:"Checks your browser accepts cookies before you log in",expiry:"Session"},{name:"wp_lang",purpose:"Remembers the language you picked on the login page",expiry:"Session"},{name:"wp-settings-[user ID]",purpose:"Remembers your dashboard layout settings",expiry:"1 year"},{name:"wp-settings-time-[user ID]",purpose:"Records when your dashboard settings were last saved",expiry:"1 year"}];v+='<div class="cc-declaration-login">',v+="<p><b>"+b(r.text.loginCookiesTitle)+"</b></p>",v+="<p>"+b(r.text.loginCookiesIntro)+"</p>",v+="<table><thead><tr>",v+="<th>Name</th><th>Provider</th><th>Purpose</th><th>Expiry</th>",v+="</tr></thead><tbody>";for(var O=0;O<B.length;O++)v+="<tr>",v+="<td><code>"+b(B[O].name)+"</code></td>",v+="<td>WordPress</td>",v+="<td>"+b(B[O].purpose)+"</td>",v+="<td>"+b(B[O].expiry)+"</td>",v+="</tr>";v+="</tbody></table></div>"}v+='<p><button type="button" class="cc-open-settings cc-declaration-change">'+b(r.text.changePreferences)+"</button></p>",v+="</div>",e.innerHTML=v}}document.addEventListener("click",function(e){(e.target&&e.target.closest?e.target.closest(".cc-open-settings"):null)&&(e.preventDefault(),Se())}),window.__ccInternal={getResourceSnapshot:Oe,scriptLoaded:function(e){for(var t=Be||Oe(),n=0;n<t.allUrls.length;n++)if(-1!==t.allUrls[n].indexOf(e))return!0;return!1},scriptLoadedUrls:function(e){for(var t=Be||Oe(),n=[],c=0;c<t.allUrls.length;c++)-1!==t.allUrls[c].indexOf(e)&&n.push(t.allUrls[c]);return n},hasCookie:function(e){return document.cookie.split(";").some(function(t){return t.trim().split("=")[0]===e})},hasCookiePrefix:function(e){return document.cookie.split(";").some(function(t){return 0===t.trim().split("=")[0].indexOf(e)})},setResourceSnapshot:function(e){Be=e},visitorCookies:function(e){for(var t=Oe().thirdPartyDomains,n={},c=0;c<e.length;c++)De(e[c],t,Pe(e[c]))&&(n[e[c]]=!0);return n},facadeBridge:{getConfig:function(){return r},getConsentState:function(){return g},loadConsent:ee,saveConsent:function(e,t){Q(e,t)},hideAllBanners:be,registerRenderer:function(e){V=e,j=2,J()}}},window.CookieConsent={getState:E,getConfig:function(){return r},getComplianceMode:function(){return m},getCountry:function(){return y},show:Ae,showSettings:Se,acceptAll:ke,declineAll:_e,getGPC:function(){return i},getConsentVersion:function(){return r.consentVersion},on:function(e,t){window.addEventListener(e,t)},off:function(e,t){window.removeEventListener(e,t)},VERSION:o},window.addEventListener("cc:ready",Ce),window.addEventListener("cc:update",Ce),function(){if(!window.__ccDefaultsSet){var e={wait_for_update:500};r.cookieTypes.forEach(function(t){t.consentMode&&t.consentMode.forEach(function(n){e[n]=t.required?"granted":"denied"})}),S("consent","default",e)}}(),function(){if(r.tcfEnabled){var e=window.__tcfapi&&window.__tcfapi.queue?window.__tcfapi.queue:[];window.__tcfapi=O,e.forEach(function(e){O.apply(null,e)}),L=!0,window===window.top&&window.addEventListener("message",function(e){var t;try{t="string"==typeof e.data?JSON.parse(e.data):e.data}catch(e){return}if(t&&t.__tcfapiCall){var n=t.__tcfapiCall;O(n.command,n.version,function(t,c){var o={__tcfapiReturn:{returnValue:t,success:c,callId:n.callId}};e.source&&e.source.postMessage("string"==typeof e.data?JSON.stringify(o):o,"*")},n.parameter)}})}}();var Ie=ee();function Le(){var e=window.__ccDiscovery&&window.__ccDiscovery.active;if(!e&&(window.__ccDNT||document.cookie.split(";").some(function(e){return 0===e.trim().indexOf("cookieflame_dnt=")})))return C(g=ne()),ce(g,te()),Y(),$(),m="dnt",ie(),se(),be(),X(g),A("cc:ready"),void Ce();if(e){C(g=te()),D(g),Y(),K(g,null),r.tcfEnabled&&T(g),window.dataLayer=window.dataLayer||[];for(var t=["consent_accepted_analytics","consent_accepted_marketing","consent_accepted_preferences","consent_accepted_necessary","consent_update","consent_given","cookie_consent_given","cookie_consent_update","consent_accepted"],n=0;n<t.length;n++)window.dataLayer.push({event:t[n]});return m="discovery",ie(),se(),be(),A("cc:ready"),A("cc:update"),void Ce()}var c=ee(),o=!1;if(c)if(!i||c.gpcOptIn||!c.marketing&&c.gpc||(o=!!c.marketing,s(c),_(r.cookieName,JSON.stringify(c),r.cookieExpiry)),g=c,C(c),D(c),Y(),$(),K(c,null),r.tcfEnabled&&T(c),ie(),se(),be(),X(c),c.cc){if("gdpr"===(m=oe(c.cc))){var a=document.getElementById("cc-icon");a&&a.classList.add("cc-show")}o&&z("gpc_optout",c),A("cc:ready"),Ce()}else re(function(e){if(m=e,"gdpr"===e){var t=document.getElementById("cc-icon");t&&t.classList.add("cc-show")}o&&z("gpc_optout",c),A("cc:ready"),Ce()});else re(function(e){return m=e,"none"===e?(Y(),Q(te(),"auto_grant_non_eu"),A("cc:ready"),A("cc:update"),void Ce()):"ccpa"===e?(Y(),i?Q(s(te()),"ccpa_gpc_optout"):Q(te(),"ccpa_auto_grant"),ie(),ue(),he("ccpa"),A("cc:ready"),A("cc:update"),void Ce()):(ie(),Y(),$(),se(),he("gdpr"),A("cc:ready"),void Ce())})}Ie&&(i&&!Ie.gpcOptIn&&(Ie.marketing=!1),g=Ie,C(Ie)),function(){if(!h&&r.autoBlockPatterns&&r.autoBlockPatterns.length){h=!0;var e=document.createElement.bind(document),t=Object.getOwnPropertyDescriptor(HTMLScriptElement.prototype,"src");t&&t.set&&(document.createElement=function(){var n=e.apply(document,arguments);if("script"!==(arguments[0]||"").toLowerCase())return n;function c(e,t){if(t&&"string"==typeof t&&!e.getAttribute("data-cc")&&!e.getAttribute("data-cc-auto")&&-1===t.indexOf("cookieconsent")){var n=P(t);!n||g&&g[n]||(e.type="text/plain",e.setAttribute("data-cc",n),e.setAttribute("data-cc-auto","1"))}}Object.defineProperty(n,"src",{configurable:!0,enumerable:!0,get:function(){return t.get.call(this)},set:function(e){return c(this,e),t.set.call(this,e)}});var o=n.setAttribute.bind(n);return n.setAttribute=function(e,t){return"src"===e.toLowerCase()&&c(n,t),o(e,t)},n}),new MutationObserver(function(e){e.forEach(function(e){e.addedNodes.forEach(function(e){if(1===e.nodeType){var t=[];"SCRIPT"===e.tagName&&t.push(e),e.querySelectorAll&&e.querySelectorAll("script[src]").forEach(function(e){t.push(e)}),t.forEach(function(e){if(!(e.getAttribute("data-cc-auto")||e.getAttribute("data-cc")||e.src&&-1!==e.src.indexOf("cookieconsent"))){var t=P(e.src);t&&(g&&g[t]||(e.type="text/plain",e.setAttribute("data-cc",t),e.setAttribute("data-cc-auto","1")))}})}})})}).observe(document.documentElement,{childList:!0,subtree:!0})}}(),"loading"===document.readyState?document.addEventListener("DOMContentLoaded",Le):Le();var Be=null;function Oe(){for(var e=[],t=[],n=[],c={},o={},r=document.querySelectorAll("script[src]"),a=0;a<r.length;a++)r[a].src&&e.push(r[a].src);for(var i=document.querySelectorAll("iframe[src]"),s=0;s<i.length;s++)i[s].src&&e.push(i[s].src);if(window.performance&&performance.getEntriesByType)for(var d=performance.getEntriesByType("resource"),u=0;u<d.length;u++){var l=d[u].name||"";if(l){-1===e.indexOf(l)&&e.push(l);var p=d[u].initiatorType||"";if("script"===p||"iframe"===p||"xmlhttprequest"===p||"fetch"===p||-1!==l.indexOf(".js"))try{var f=new URL(l),g=f.hostname;if(g===location.hostname||g==="www."+location.hostname||"www."+g===location.hostname){var m=f.pathname.match(/\/wp-content\/plugins\/([^\/]+)\//);m&&!o[m[1]]&&(o[m[1]]=!0,n.push({slug:m[1],url:l}))}else c[g]||(c[g]=!0,t.push(g))}catch(e){}}}return{allUrls:e,thirdPartyDomains:t,pluginScripts:n}}var Te={};function Pe(e){var t={set_by:"",set_by_plugin:""},n=window.__ccCookieSetters;if(!n||!Object.prototype.hasOwnProperty.call(n,e)||!n[e])return t;try{var c=new URL(String(n[e]).replace(/:\d+(:\d+)?$/,""));if(/^(chrome-extension|moz-extension|safari-web-extension|safari-extension):$/.test(c.protocol))return t.set_by="browser-extension",t;var o=c.hostname;if(o===location.hostname||o==="www."+location.hostname||"www."+o===location.hostname){t.set_by="first-party";var r=c.pathname.match(/\/wp-content\/(?:plugins|themes)\/([^\/]+)\//);r&&(t.set_by_plugin=r[1].toLowerCase())}else t.set_by=o.toLowerCase()}catch(e){}return t}function De(e,t,n){if("browser-extension"===n.set_by)return!0;var c=window.__ccVisitorCookieRules;if(!c||!c.tools)return!1;if(c.allow&&-1!==c.allow.indexOf(e))return!1;if(n.set_by)return!1;for(var o=0;o<c.tools.length;o++){for(var r=c.tools[o],a=-1!==(r.n||[]).indexOf(e),i=0;!a&&i<(r.p||[]).length;i++)0===e.indexOf(r.p[i])&&(a=!0);if(a){for(var s=0;s<t.length;s++)for(var d=0;d<(r.d||[]).length;d++){var u=String(t[s]).toLowerCase(),l=r.d[d];if(u===l||u.slice(-l.length-1)==="."+l)return!1}return!0}}return!1}function Re(){var t=window.__ccClassified||[],n=window.__ccUnclassified||[],c=window.__ccBeaconUrl;if(c){var o,r={};for(o=0;o<t.length;o++)r[t[o]]=!0;for(o=0;o<n.length;o++)r[n[o]]=!0;var a=Object.keys(Te);for(o=0;o<a.length;o++)r[a[o]]=!0;var i=Object.keys(d);for(o=0;o<i.length;o++)r[i[o]]=!0;var s={},u=[],l=document.cookie.split(";");for(o=0;o<l.length;o++){var p=l[o].indexOf("="),f=(p>-1?l[o].substring(0,p):l[o]).trim(),g=p>-1?l[o].substring(p+1).trim():"";f&&(u.push(f),s[f]=g)}var m=[];for(o=0;o<u.length;o++)if(!r[u[o]]){for(var y=!1,v=Object.keys(d),h=0;h<v.length;h++)if(d[v[h]].prefix&&0===u[o].indexOf(v[h])){y=!0;break}y||m.push(u[o])}if(0!==m.length){for(var w=Oe(),b=w.thirdPartyDomains,k=[],_=0;_<w.pluginScripts.length;_++)k.push(w.pluginScripts[_].slug);for(var x=location.pathname,E=0;E<m.length;E++){var A=m[E],S=!!e[A]&&!r[A],C=Me(s[A]||""),I=Pe(A);if(!De(A,b,I)||window.__ccDiscovery&&window.__ccDiscovery.active){var L=JSON.stringify({cookie_name:A,script_domains:b,plugin_scripts:k,page_path:x,pre_existing:S,value_shape:C,set_by:I.set_by,set_by_plugin:I.set_by_plugin});Te[A]=!0,navigator.sendBeacon&&navigator.sendBeacon(c,new Blob([L],{type:"application/json"}))}else Te[A]=!0}}}}function Me(e){if(!e||0===e.length)return"empty";if("true"===e||"false"===e||"1"===e||"0"===e)return"boolean";if(/^\d+$/.test(e))return e.length>12?"long-numeric":"numeric";if(/^\d+\.\d+$/.test(e))return"decimal";if(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(e))return"uuid";if(/^[A-Za-z0-9+\/]{20,}={0,2}$/.test(e))return"base64";if(/^(GA\d+\.\d+\.\d+\.\d+|G-[A-Z0-9]+)/.test(e))return"ga-id";try{if("object"==typeof JSON.parse(decodeURIComponent(e)))return"json"}catch(e){}return/^[a-f0-9]{24,}$/i.test(e)?"hex-id":/^\d{10,13}$/.test(e)?"timestamp":e.length<=20?"short-string":e.length<=100?"medium-string":"long-string"}setTimeout(Re,3e3),setTimeout(Re,15e3),setTimeout(Re,45e3),window.addEventListener("cc:update",function(){setTimeout(Re,3e3)}),window.addEventListener("pagehide",Re)}();
  • cookieflame/tags/5.4.0/cookieflame.php

    r3769546 r3771537  
    33 * Plugin Name:       CookieFlame
    44 * Plugin URI:        https://cookieflame.com
    5  * Description:       A lightweight, self-hosted cookie consent manager with Google Consent Mode v2, IAB TCF v2.2, automatic script blocking, multi-region compliance (GDPR/CCPA/LGPD/POPIA), passive cookie monitoring, and server-side geo detection via Cloudflare.
    6  * Version:           5.3.0
     5 * Description:       A lightweight, self-hosted cookie consent manager with Google Consent Mode v2, automatic script blocking, multi-region compliance (GDPR/CCPA/LGPD/POPIA), passive cookie monitoring, and server-side geo detection via Cloudflare.
     6 * Version:           5.4.0
    77 * Requires at least: 6.0
    88 * Requires PHP:      8.0
    … …  
    1919}
    2020
    21 define( 'COOKIEFLAME_VERSION', '5.3.0' );
     21define( 'COOKIEFLAME_VERSION', '5.4.0' );
    2222define( 'COOKIEFLAME_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
    2323define( 'COOKIEFLAME_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
    … …  
    2727
    2828/* ═══════════════════════════════════════════════════════════
    29    MONITORING INTERVAL BUCKETS (in minutes)
    30    A single 'digest_interval' option drives both beacon
    31    processing cadence and email digest cadence. Value 0 = off.
    32    Free tier: off, daily, weekly.
    33    Pro tier:  free buckets + 15m, 1h, 4h, monthly (added via the
    34               cookieflame_interval_buckets filter by the Pro plugin).
     29   COOKIE MONITORING SETTINGS (v5.4.0)
     30   Two separate settings on the Cookie Monitoring tab:
     31     - Watch for new cookies: On/Off. Visitors' browsers report
     32       new cookies as they browse, processed straight away.
     33       Stored as the inverse of 'monitoring_paused'.
     34     - Email me about changes: 'weekly' (default), 'daily',
     35       'instant' or 'off'. Stored as 'email_frequency'.
     36   Before v5.4.0 a single 'digest_interval' (minutes, 0 = off)
     37   drove both. cookieflame_migrate_monitoring_settings() converts
     38   it once; until then the getters derive the same answer from it.
     39
     40   The cookieflame_interval_buckets filter is kept: a plugin that
     41   adds a bucket shorter than a day (Pro 1.5.x adds 15 minutes)
     42   makes "Instantly" emails possible, sent after that delay.
    3543═══════════════════════════════════════════════════════════ */
    3644
    … …  
    5361 */
    5462function cookieflame_interval_buckets_free() {
     63    // v5.4.0: the 15-minute, 1-hour and 4-hour entries are no longer
     64    // declared here. Pro 1.5.x still contributes them through the filter.
    5565    return array(
    5666        array( 'minutes' => 0,     'tier' => 'free' ),
    57         array( 'minutes' => 15,    'tier' => 'pro' ),
    58         array( 'minutes' => 60,    'tier' => 'pro' ),
    59         array( 'minutes' => 240,   'tier' => 'pro' ),
    6067        array( 'minutes' => 1440,  'tier' => 'free' ),
    6168        array( 'minutes' => 10080, 'tier' => 'free' ),
    … …  
    159166
    160167/**
    161  * Return the currently configured monitoring interval in minutes,
    162  * normalised to a valid bucket for the current tier. Defaults to
    163  * daily (1440) on new installs with no value set.
     168 * Delay before the next email summary, in minutes (0 = no email).
     169 * Kept under its pre-5.4.0 name for any code that still calls it.
    164170 */
    165171function cookieflame_current_interval_minutes() {
    166     $settings = get_option( 'cookieflame_settings', array() );
    167     $raw      = isset( $settings['digest_interval'] ) ? intval( $settings['digest_interval'] ) : 1440;
    168     return cookieflame_normalise_interval( $raw );
    169 }
    170 
     172    return cookieflame_email_delay_minutes();
     173}
     174
     175/**
     176 * Whether "Watch for new cookies" is off. Every beacon, queue and
     177 * digest path gates on this, so turning it off silences them all.
     178 */
    171179function cookieflame_monitoring_is_off() {
    172     if ( cookieflame_monitoring_is_paused() ) {
    173         return true;
    174     }
    175     return 0 === cookieflame_current_interval_minutes();
    176 }
    177 
    178 /**
    179  * Whether monitoring is currently paused. Paused is a temporary
    180  * suppression state — distinct from "Off" — that keeps the chosen
    181  * update interval configured but silences beacons and digests while
    182  * the admin does site surgery. Admin toggles this from the Cookie
    183  * Monitoring tab. cookieflame_monitoring_is_off() folds this into the same
    184  * gate so every downstream path inherits pause automatically.
     180    return cookieflame_monitoring_is_paused();
     181}
     182
     183/**
     184 * Whether "Watch for new cookies" is off (stored as 'monitoring_paused',
     185 * the flag behind the On/Off switch on the Cookie Monitoring tab). A
     186 * pre-5.4.0 'digest_interval' of 0 also means off until migrated.
    185187 */
    186188function cookieflame_monitoring_is_paused() {
    187189    $settings = get_option( 'cookieflame_settings', array() );
    188     return ! empty( $settings['monitoring_paused'] );
    189 }
     190    if ( ! empty( $settings['monitoring_paused'] ) ) {
     191        return true;
     192    }
     193    if ( ! isset( $settings['email_frequency'] ) && isset( $settings['digest_interval'] ) && intval( $settings['digest_interval'] ) <= 0 ) {
     194        return true;
     195    }
     196    return false;
     197}
     198
     199/**
     200 * The saved "Email me about changes" choice: 'weekly', 'daily',
     201 * 'instant' or 'off'. Sites not yet migrated get the answer their old
     202 * 'digest_interval' maps to (15/60/240 minutes map to 'instant').
     203 */
     204function cookieflame_email_frequency() {
     205    $settings = get_option( 'cookieflame_settings', array() );
     206    if ( isset( $settings['email_frequency'] ) ) {
     207        return cookieflame_sanitize_email_frequency( $settings['email_frequency'] );
     208    }
     209    if ( ! isset( $settings['digest_interval'] ) ) {
     210        return 'weekly';
     211    }
     212    return cookieflame_email_frequency_from_interval( $settings['digest_interval'], true );
     213}
     214
     215/**
     216 * Map a pre-5.4.0 'digest_interval' to an email frequency. Intervals
     217 * under a day become 'instant' when $instant_ok, otherwise 'daily'.
     218 */
     219function cookieflame_email_frequency_from_interval( $minutes, $instant_ok ) {
     220    $minutes = intval( $minutes );
     221    if ( $minutes <= 0 ) {
     222        return 'off';
     223    }
     224    if ( 10080 === $minutes ) {
     225        return 'weekly';
     226    }
     227    if ( $minutes < 1440 ) {
     228        return $instant_ok ? 'instant' : 'daily';
     229    }
     230    return 'daily';
     231}
     232
     233function cookieflame_sanitize_email_frequency( $value ) {
     234    $value = is_string( $value ) ? $value : '';
     235    return in_array( $value, array( 'weekly', 'daily', 'instant', 'off' ), true ) ? $value : 'weekly';
     236}
     237
     238/**
     239 * Shortest sub-daily bucket another plugin added through the
     240 * cookieflame_interval_buckets filter, in minutes, or 0 when none.
     241 */
     242function cookieflame_instant_bucket_minutes() {
     243    $shortest = 0;
     244    foreach ( cookieflame_interval_buckets() as $bucket ) {
     245        $minutes = intval( $bucket['minutes'] );
     246        if ( $minutes > 0 && $minutes < 1440 && ( 0 === $shortest || $minutes < $shortest ) ) {
     247            $shortest = $minutes;
     248        }
     249    }
     250    return $shortest;
     251}
     252
     253/**
     254 * Whether "Instantly" can be chosen on this site: CookieFlame Pro is
     255 * active and a plugin supplies a delay shorter than a day.
     256 */
     257function cookieflame_instant_email_available() {
     258    return cookieflame_is_premium() && cookieflame_instant_bucket_minutes() > 0;
     259}
     260
     261/**
     262 * Minutes from the first new cookie to the email summary, or 0 when
     263 * emails are off. 'instant' uses the shortest contributed bucket and
     264 * falls back to daily when there is none.
     265 *
     266 * Filter: cookieflame_email_delay_minutes ( int $minutes, string $frequency ).
     267 */
     268function cookieflame_email_delay_minutes() {
     269    $frequency = cookieflame_email_frequency();
     270    $map       = array( 'off' => 0, 'daily' => 1440, 'weekly' => 10080 );
     271    if ( 'instant' === $frequency ) {
     272        $minutes = cookieflame_instant_bucket_minutes();
     273        $minutes = $minutes > 0 ? $minutes : 1440;
     274    } else {
     275        $minutes = $map[ $frequency ];
     276    }
     277    return max( 0, intval( apply_filters( 'cookieflame_email_delay_minutes', $minutes, $frequency ) ) );
     278}
     279
     280/**
     281 * One-time conversion of the pre-5.4.0 'digest_interval' setting:
     282 *   0             -> watch Off, email Off
     283 *   daily, weekly -> watch On, email unchanged
     284 *   15m, 1h, 4h   -> 'instant' when Pro is active, 'daily' otherwise
     285 * 'digest_interval' itself is left in place.
     286 */
     287function cookieflame_migrate_monitoring_settings() {
     288    $settings = get_option( 'cookieflame_settings', false );
     289    if ( ! is_array( $settings ) || isset( $settings['email_frequency'] ) ) {
     290        return;
     291    }
     292    if ( isset( $settings['digest_interval'] ) ) {
     293        $minutes = intval( $settings['digest_interval'] );
     294        $settings['email_frequency'] = cookieflame_email_frequency_from_interval( $minutes, $minutes > 0 && $minutes < 1440 && cookieflame_is_premium() );
     295        if ( $minutes <= 0 ) {
     296            $settings['monitoring_paused'] = 1;
     297        }
     298    } else {
     299        $settings['email_frequency'] = 'weekly';
     300    }
     301    update_option( 'cookieflame_settings', $settings );
     302}
     303add_action( 'admin_init', 'cookieflame_migrate_monitoring_settings', 5 );
    190304
    191305/**
    … …  
    209323    $host      = is_string( $host ) ? strtolower( $host ) : '';
    210324    return substr( sha1( $host . '|' . $page_path ), 0, 16 );
     325}
     326
     327/**
     328 * Version string for one of the plugin's own scripts or stylesheets: the
     329 * plugin version plus the file's modification time, so a changed file
     330 * always gets a new URL and no browser or CDN cache serves an old copy.
     331 */
     332function cookieflame_asset_version( $relative_path ) {
     333    $file  = COOKIEFLAME_PLUGIN_DIR . $relative_path;
     334    $mtime = file_exists( $file ) ? filemtime( $file ) : false;
     335    return $mtime ? COOKIEFLAME_VERSION . '.' . $mtime : COOKIEFLAME_VERSION;
    211336}
    212337
    … …  
    337462add_action( 'admin_post_cookieflame_save_notifications', 'cookieflame_save_notifications' );
    338463add_action( 'admin_post_cookieflame_toggle_pause',        'cookieflame_toggle_monitoring_pause' );
     464add_action( 'admin_post_cookieflame_dismiss_upgrade_banner', 'cookieflame_dismiss_upgrade_banner' );
     465
     466/**
     467 * Hide the "Upgrade to Pro" banner at the top of the CookieFlame tabs
     468 * for the current user.
     469 */
     470function cookieflame_dismiss_upgrade_banner() {
     471    if ( ! current_user_can( 'manage_options' ) ) {
     472        wp_die( esc_html__( 'You do not have permission to do this.', 'cookieflame' ) );
     473    }
     474    check_admin_referer( 'cookieflame_dismiss_upgrade_banner' );
     475    update_user_meta( get_current_user_id(), 'cookieflame_upgrade_banner_dismissed', time() );
     476    $tab = sanitize_key( wp_unslash( $_POST['tab'] ?? 'cookies' ) );
     477    wp_safe_redirect( add_query_arg( array( 'page' => 'cookieflame', 'tab' => $tab ? $tab : 'cookies' ), admin_url( 'admin.php' ) ) );
     478    exit;
     479}
    339480
    340481/**
    … …  
    11931334    //   - The beacon carries a script_domain or plugin_script not yet
    11941335    //     recorded for this (URL, cookie) pair
    1195     //   - The configured update interval has elapsed since last_processed
     1336    //   - A day has passed since last_processed (heartbeat; this only
     1337    //     refreshes "last seen", which Tidy up relies on)
    11961338    //
    11971339    // Otherwise we silently drop the beacon.
    11981340    //
    1199     // Monitoring off (interval = 0) is handled at enqueue time — the
     1341    // Watch for new cookies Off is handled at enqueue time — the
    12001342    // beacon URL is not injected so nothing fires. We defensively drop
    12011343    // here too in case a stale page serves the URL from cache.
    … …  
    12081350    $url_hash    = cookieflame_url_hash_for_path( $page_path );
    12091351    $now         = time();
    1210     $interval_s  = cookieflame_current_interval_minutes() * 60;
     1352    $interval_s  = DAY_IN_SECONDS; // Heartbeat: fixed daily since v5.4.0, independent of the email setting.
    12111353
    12121354    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom plugin table; high-write-rate beacon storage where the object cache adds no value (writes invalidate cache on every visitor request, reads are admin-only and infrequent).
    … …  
    14041546 */
    14051547function cookieflame_queue_notification( $cookie_name, $script_domains, $page_path, $pre_existing = false, $value_shape = '' ) {
    1406     // Monitoring off -> do not queue and do not schedule a digest.
    1407     if ( cookieflame_monitoring_is_off() ) {
     1548    // Watching off, or emails off -> do not queue and do not schedule a digest.
     1549    if ( cookieflame_monitoring_is_off() || 0 === cookieflame_email_delay_minutes() ) {
    14081550        return;
    14091551    }
    … …  
    14221564    update_option( 'cookieflame_notification_queue', $queue );
    14231565
    1424     // Start digest timer if not already running. The same update_interval
    1425     // setting drives both beacon filtering and digest cadence.
     1566    // Start digest timer if not already running, from the
     1567    // "Email me about changes" setting.
    14261568    $scheduled = get_option( 'cookieflame_digest_scheduled', 0 );
    14271569    if ( empty( $scheduled ) ) {
    1428         $interval = cookieflame_current_interval_minutes() * 60;
     1570        $interval = cookieflame_email_delay_minutes() * 60;
    14291571        if ( $interval <= 0 ) {
    14301572            return;
    … …  
    16011743    // invoking the hook directly. Silently clean up and return — the
    16021744    // queue is preserved for resume.
    1603     if ( cookieflame_monitoring_is_off() ) {
     1745    if ( cookieflame_monitoring_is_off() || 0 === cookieflame_email_delay_minutes() ) {
    16041746        delete_option( 'cookieflame_digest_scheduled' );
    16051747        return;
    … …  
    16851827
    16861828    // Header
    1687     $body  = "CookieFlame Cookie Discovery Report\n";
     1829    $body  = "CookieFlame: new cookies found by Watch for new cookies\n";
    16881830    $body .= $site_name . " (" . $site_url . ")\n";
    16891831    $body .= wp_date( 'j M Y, H:i' ) . "\n";
    … …  
    18131955    $body .= "-- \n";
    18141956    $body .= "CookieFlame v" . COOKIEFLAME_VERSION . " on " . $site_name . "\n";
    1815     $body .= "You are receiving this because Cookie Monitoring is enabled.\n";
     1957    $body .= "You are receiving this because \"Email me about changes\" is on in Cookie Monitoring.\n";
    18161958    $body .= "Change settings: " . admin_url( 'admin.php?page=cookieflame&tab=notifications' ) . "\n";
    18171959
    1818     wp_mail( $to, $subject, $body );
     1960    /**
     1961     * Filter the email summary before it is sent.
     1962     *
     1963     * Return an array with 'to', 'subject', 'body' and 'headers' to change
     1964     * the email, or false to skip sending it (the queue is still cleared).
     1965     *
     1966     * @param array $email      { to, subject, body, headers }.
     1967     * @param array $actionable Queue entries that need a decision.
     1968     * @param array $residual   Queue entries from plugins no longer active.
     1969     */
     1970    $email = apply_filters( 'cookieflame_digest_email', array(
     1971        'to'      => $to,
     1972        'subject' => $subject,
     1973        'body'    => $body,
     1974        'headers' => array(),
     1975    ), $actionable, $residual );
     1976
     1977    if ( is_array( $email ) && ! empty( $email['to'] ) ) {
     1978        wp_mail( $email['to'], (string) ( $email['subject'] ?? $subject ), (string) ( $email['body'] ?? $body ), $email['headers'] ?? array() );
     1979    }
    18191980
    18201981    // Clear the queue and timer
    … …  
    22182379        COOKIEFLAME_PLUGIN_URL . 'assets/css/admin.css',
    22192380        array(),
    2220         COOKIEFLAME_VERSION
     2381        cookieflame_asset_version( 'assets/css/admin.css' )
    22212382    );
    22222383    wp_enqueue_script(
    … …  
    22242385        COOKIEFLAME_PLUGIN_URL . 'assets/js/admin.js',
    22252386        array(),
    2226         COOKIEFLAME_VERSION,
     2387        cookieflame_asset_version( 'assets/js/admin.js' ),
    22272388        true
    22282389    );
    … …  
    22532414    $cookieflame_js_data = apply_filters( 'cookieflame_admin_js_data', $cookieflame_js_data );
    22542415    wp_localize_script( 'cookieflame-admin', 'cookieflameAdmin', $cookieflame_js_data );
     2416
     2417    // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Tab navigation, no data modification.
     2418    if ( isset( $_GET['tab'] ) && 'preview' === sanitize_key( wp_unslash( $_GET['tab'] ) ) ) {
     2419        wp_enqueue_script(
     2420            'cookieflame-admin-preview',
     2421            COOKIEFLAME_PLUGIN_URL . 'assets/js/admin-preview.js',
     2422            array(),
     2423            cookieflame_asset_version( 'assets/js/admin-preview.js' ),
     2424            true
     2425        );
     2426        wp_localize_script( 'cookieflame-admin-preview', 'cookieflamePreview', array(
     2427            'privacyUrl'        => $opts['privacy_url'] ?? '/privacy-policy',
     2428            'theme'             => $opts['theme'] ?? 'light',
     2429            'rejectFirstScreen' => ! empty( $opts['show_reject_first'] ),
     2430            'showPoweredBy'     => ! empty( $opts['show_powered_by'] ),
     2431        ) );
     2432    }
    22552433}
    22562434
    … …  
    23052483}
    23062484
     2485/**
     2486 * [cookieflame_settings_link text="Cookie settings"]: a link that reopens
     2487 * the visitor's cookie choices (the US "Do Not Sell or Share" choice for
     2488 * US visitors). Any element with the class cc-open-settings does the same,
     2489 * for example a menu item.
     2490 */
     2491add_shortcode( 'cookieflame_settings_link', 'cookieflame_settings_link_shortcode' );
     2492
     2493function cookieflame_settings_link_shortcode( $atts ) {
     2494    $atts = shortcode_atts( array(
     2495        'text'  => __( 'Cookie settings', 'cookieflame' ),
     2496        'class' => '',
     2497    ), $atts, 'cookieflame_settings_link' );
     2498    $class = trim( 'cc-open-settings ' . implode( ' ', array_map( 'sanitize_html_class', preg_split( '/\s+/', (string) $atts['class'], -1, PREG_SPLIT_NO_EMPTY ) ) ) );
     2499    return '<a href="#" class="' . esc_attr( $class ) . '" role="button">' . esc_html( $atts['text'] ) . '</a>';
     2500}
     2501
    23072502/* ═══════════════════════════════════════════════════════════
    23082503   REST API — GEO ENDPOINT
    … …  
    23392534        update_option( 'cookieflame_settings', array(
    23402535            'privacy_url'    => '/privacy-policy',
    2341             'ccpa_url'       => '/privacy-policy#do-not-sell',
    23422536            'log_endpoint'   => '',
    23432537            'gtm_id'         => '',
    … …  
    23502544            'track_logged_in' => 1,
    23512545            'extra_patterns' => '',
    2352             'digest_interval' => 1440, // daily
     2546            'email_frequency' => 'weekly',
    23532547            'monitoring_paused' => 0,
    23542548        ) );
    … …  
    25402734        . "if(raw.cv!=={$cv_js})return;"
    25412735        . "var gpc=window.__ccGPC;"
    2542         . "var mkt=raw.marketing&&!gpc;"
     2736        . "var mkt=raw.marketing&&(!gpc||raw.gpcOptIn);"
    25432737        . "gtag('consent','update',{"
    25442738        . "analytics_storage:raw.analytics?'granted':'denied',"
    … …  
    26292823    // blockable iframe or Elementor video widget is present on the
    26302824    // page). Versioned so cache busts cleanly on plugin update.
    2631     $overrides['facadesUrl'] = COOKIEFLAME_PLUGIN_URL . 'assets/js/cookieconsent-facades.min.js?ver=' . COOKIEFLAME_VERSION;
     2825    $overrides['facadesUrl'] = COOKIEFLAME_PLUGIN_URL . 'assets/js/cookieconsent-facades.min.js?ver=' . cookieflame_asset_version( 'assets/js/cookieconsent-facades.min.js' );
    26322826
    26332827    if ( ! empty( $options['privacy_url'] ) )    $overrides['privacyUrl']    = $options['privacy_url'];
    2634     // CCPA "Do Not Sell" link points to the same privacy policy page with an anchor.
    2635     $cookieflame_privacy = ! empty( $options['privacy_url'] ) ? $options['privacy_url'] : '/privacy-policy';
    2636     $overrides['ccpaUrl'] = $cookieflame_privacy . '#do-not-sell';
    26372828    if ( ! empty( $options['log_endpoint'] ) )    $overrides['logEndpoint']   = $options['log_endpoint'];
    26382829    if ( ! empty( $options['storage_method'] ) )  $overrides['storageMethod'] = $options['storage_method'];
    … …  
    26482839    $overrides['shareSubdomains'] = ! empty( $options['share_subdomains'] ) || ! empty( $options['cookie_domain'] );
    26492840    if ( isset( $options['reconsent_days'] ) && $options['reconsent_days'] !== '' ) {
    2650         $overrides['reconsentDays'] = intval( $options['reconsent_days'] );
     2841        $overrides['reconsentDays'] = cookieflame_reconsent_days( $options['reconsent_days'] );
     2842    }
     2843    if ( ! empty( $options['show_reject_first'] ) ) {
     2844        $overrides['rejectFirstScreen'] = true;
     2845    }
     2846    if ( ! empty( $options['visitor_logins'] ) ) {
     2847        $overrides['visitorLogins'] = true;
    26512848    }
    26522849    if ( ! empty( $options['show_powered_by'] ) ) {
    … …  
    26552852    if ( ! empty( $options['icon_position'] ) && 'right' === $options['icon_position'] ) {
    26562853        $overrides['iconPosition'] = 'right';
     2854    }
     2855
     2856    /**
     2857     * Filter the banner configuration sent to the front end as
     2858     * window.__ccConfig. Keys the banner script knows are applied over
     2859     * its defaults; any other keys are left on window.__ccConfig for the
     2860     * code that added them.
     2861     *
     2862     * @param array $overrides Front-end config.
     2863     * @param array $options   The cookieflame_settings option.
     2864     */
     2865    $filtered = apply_filters( 'cookieflame_frontend_config', $overrides, $options );
     2866    if ( is_array( $filtered ) ) {
     2867        $overrides = $filtered;
    26572868    }
    26582869
    … …  
    27973008        COOKIEFLAME_PLUGIN_URL . 'assets/js/cookieconsent' . $suffix . '.js',
    27983009        array(),
    2799         COOKIEFLAME_VERSION,
     3010        cookieflame_asset_version( 'assets/js/cookieconsent' . $suffix . '.js' ),
    28003011        false // Load in head, not footer.
    28013012    );
    … …  
    28113022            COOKIEFLAME_PLUGIN_URL . 'assets/js/cookieconsent-discovery' . $suffix . '.js',
    28123023            array( 'cookieflame-consent' ),
    2813             COOKIEFLAME_VERSION,
     3024            cookieflame_asset_version( 'assets/js/cookieconsent-discovery' . $suffix . '.js' ),
    28143025            true // Footer is fine -- discovery runs after DOM is ready.
    28153026        );
    … …  
    28473058        COOKIEFLAME_PLUGIN_URL . 'assets/css/cookieconsent.css',
    28483059        array(),
    2849         COOKIEFLAME_VERSION
     3060        cookieflame_asset_version( 'assets/css/cookieconsent.css' )
    28503061    );
    28513062}
    … …  
    33783589        array( 'key' => 'privacy_url',
    33793590               'wp_default_page' => 'wp_page_for_privacy_policy',
    3380                'desc' => __( 'The page linked from the consent banner and the CCPA "Do Not Sell My Personal Information" notice. Defaults to the page set under Settings > Privacy. Add the [cookieflame_declaration] shortcode to this page to show visitors their consent choices.', 'cookieflame' ) ) );
     3591               'desc' => __( 'The page linked from the consent banner. Defaults to the page set under Settings > Privacy. Add the [cookieflame_declaration] shortcode to this page to show visitors their consent choices.', 'cookieflame' ) ) );
     3592
     3593    add_settings_field( 'cookieflame_visitor_logins', __( 'Visitor logins', 'cookieflame' ), 'cookieflame_field_checkbox', 'cookieflame', 'cookieflame_consent',
     3594        array( 'key' => 'visitor_logins',
     3595               'desc' => __( 'Visitors can log in to this site (for example, customer accounts or a members area)', 'cookieflame' ),
     3596               'help' => __( 'Tick this if people other than you and your team can log in, for example to see their orders, download purchases or read members-only pages. Your cookie declaration will then also list the cookies WordPress sets when someone logs in. Leave it unticked if only you and your team log in.', 'cookieflame' ) ) );
    33813597
    33823598    add_settings_field( 'cookieflame_reconsent', __( 'Re-consent Period', 'cookieflame' ), 'cookieflame_field_number', 'cookieflame', 'cookieflame_consent',
    … …  
    33863602    add_settings_field( 'cookieflame_theme', __( 'Banner Theme', 'cookieflame' ), 'cookieflame_field_theme', 'cookieflame', 'cookieflame_consent' );
    33873603
     3604    add_settings_field( 'cookieflame_show_reject_first', __( 'Show "Decline all" on the first screen', 'cookieflame' ), 'cookieflame_field_checkbox', 'cookieflame', 'cookieflame_consent',
     3605        array( 'key' => 'show_reject_first',
     3606               'desc' => __( 'Adds a Decline All button next to Accept All on the banner for visitors in the UK, EU/EEA, Switzerland, Brazil and South Africa. Off by default: visitors can always decline everything under Manage preferences. Never shown on the US notice.', 'cookieflame' ) ) );
     3607
    33883608    add_settings_field( 'cookieflame_share_subdomains', __( 'Share consent across subdomains', 'cookieflame' ), 'cookieflame_field_checkbox', 'cookieflame', 'cookieflame_consent',
    33893609        array( 'key' => 'share_subdomains',
    … …  
    34283648}
    34293649
     3650/**
     3651 * Re-consent period in days. Anything below 1 (blank, 0, negative or
     3652 * junk) means the default of 365, so consent never expires on load.
     3653 */
     3654function cookieflame_reconsent_days( $value ) {
     3655    $days = intval( $value );
     3656    return $days >= 1 ? $days : 365;
     3657}
     3658
    34303659function cookieflame_sanitize_settings( $input ) {
    3431     // Handle the DNT device cookie (not stored in DB, stored as browser cookie)
    3432     cookieflame_handle_dnt_cookie( ! empty( $input['dnt_device'] ) );
     3660    $input = is_array( $input ) ? $input : array();
    34333661
    34343662    // Preserve consent_bump from existing settings (managed by the
    34353663    // Force Re-consent button, not by this form)
    34363664    $existing = get_option( 'cookieflame_settings', array() );
    3437 
    3438     return array(
    3439         'privacy_url'        => esc_url_raw( $input['privacy_url'] ?? '' ),
    3440         'ccpa_url'           => esc_url_raw( $input['ccpa_url'] ?? '' ),
    3441         'log_endpoint'       => esc_url_raw( $input['log_endpoint'] ?? '' ),
     3665    $existing = is_array( $existing ) ? $existing : array();
     3666
     3667    // The Settings tab form posts a '_form' marker. Other saves (the
     3668    // Cookie Monitoring and Tag Managers handlers, the pause switch, the
     3669    // migration) pass the whole stored array back through this callback,
     3670    // so an unticked checkbox there means "not in this form", not "off".
     3671    $main_form = ! empty( $input['_form'] );
     3672    $flag = function ( $key, $default = 0 ) use ( $input, $existing, $main_form ) {
     3673        if ( $main_form ) {
     3674            return ! empty( $input[ $key ] ) ? 1 : 0;
     3675        }
     3676        $source = array_key_exists( $key, $input ) ? $input : $existing;
     3677        return isset( $source[ $key ] ) ? ( ! empty( $source[ $key ] ) ? 1 : 0 ) : $default;
     3678    };
     3679
     3680    if ( $main_form ) {
     3681        // Handle the DNT device cookie (not stored in DB, stored as browser cookie)
     3682        cookieflame_handle_dnt_cookie( ! empty( $input['dnt_device'] ) );
     3683        $track_logged_in = empty( $input['exclude_staff'] ) ? 1 : 0;
     3684    } else {
     3685        $track_logged_in = $flag( 'track_logged_in', 1 );
     3686    }
     3687
     3688    $email_frequency = cookieflame_sanitize_email_frequency( $input['email_frequency'] ?? ( $existing['email_frequency'] ?? 'weekly' ) );
     3689
     3690    $clean = array(
     3691        'privacy_url'        => esc_url_raw( $input['privacy_url'] ?? ( $main_form ? '' : ( $existing['privacy_url'] ?? '' ) ) ),
     3692        'log_endpoint'       => esc_url_raw( $input['log_endpoint'] ?? ( $main_form ? '' : ( $existing['log_endpoint'] ?? '' ) ) ),
    34423693        'storage_method'     => 'cookie', // hardcoded — localStorage removed
    3443         'reconsent_days'     => intval( $input['reconsent_days'] ?? 365 ),
    3444         'cookie_domain'      => cookieflame_sanitize_cookie_domain( $input['cookie_domain'] ?? '' ),
    3445         'share_subdomains'   => ! empty( $input['share_subdomains'] ) ? 1 : 0,
    3446         'csp_safe'           => ! empty( $input['csp_safe'] ) ? 1 : 0,
    3447         'theme'              => in_array( $input['theme'] ?? '', array( 'light', 'dark' ), true ) ? $input['theme'] : 'light',
    3448         'show_powered_by'    => ! empty( $input['show_powered_by'] ) ? 1 : 0,
    3449         'icon_position'      => in_array( $input['icon_position'] ?? '', array( 'left', 'right' ), true ) ? $input['icon_position'] : 'left',
    3450         'track_logged_in'    => empty( $input['exclude_staff'] ) ? 1 : 0,
    3451         'extra_patterns'     => sanitize_textarea_field( $input['extra_patterns'] ?? '' ),
     3694        'reconsent_days'     => cookieflame_reconsent_days( $input['reconsent_days'] ?? ( $existing['reconsent_days'] ?? 365 ) ),
     3695        'cookie_domain'      => cookieflame_sanitize_cookie_domain( $input['cookie_domain'] ?? ( $existing['cookie_domain'] ?? '' ) ),
     3696        'share_subdomains'   => $flag( 'share_subdomains' ),
     3697        'csp_safe'           => $flag( 'csp_safe' ),
     3698        'theme'              => in_array( $input['theme'] ?? ( $existing['theme'] ?? '' ), array( 'light', 'dark' ), true ) ? ( $input['theme'] ?? $existing['theme'] ) : 'light',
     3699        'show_reject_first'  => $flag( 'show_reject_first' ),
     3700        'visitor_logins'     => $flag( 'visitor_logins' ),
     3701        'show_powered_by'    => $flag( 'show_powered_by' ),
     3702        'icon_position'      => in_array( $input['icon_position'] ?? ( $existing['icon_position'] ?? '' ), array( 'left', 'right' ), true ) ? ( $input['icon_position'] ?? $existing['icon_position'] ) : 'left',
     3703        'track_logged_in'    => $track_logged_in,
     3704        'extra_patterns'     => sanitize_textarea_field( $input['extra_patterns'] ?? ( $main_form ? '' : ( $existing['extra_patterns'] ?? '' ) ) ),
    34523705        'consent_bump'       => intval( $existing['consent_bump'] ?? 0 ),
    3453         'delete_on_uninstall' => ! empty( $input['delete_on_uninstall'] ) ? 1 : 0,
     3706        'delete_on_uninstall' => $flag( 'delete_on_uninstall' ),
    34543707        // Separate-tab fields: use $input if present (handler included it), else preserve $existing
    34553708        'gtm_id'             => isset( $input['gtm_id'] )             ? cookieflame_sanitize_gtm_id( $input['gtm_id'] )                : ( $existing['gtm_id'] ?? '' ),
    34563709        'uet_id'             => isset( $input['uet_id'] )             ? cookieflame_sanitize_uet_id( $input['uet_id'] )                : ( $existing['uet_id'] ?? '' ),
    34573710        'notification_email' => isset( $input['notification_email'] ) ? sanitize_email( $input['notification_email'] )           : ( $existing['notification_email'] ?? '' ),
    3458         'digest_interval'    => isset( $input['digest_interval'] )    ? cookieflame_normalise_interval( $input['digest_interval'] )    : cookieflame_normalise_interval( $existing['digest_interval'] ?? 1440 ),
    3459         // monitoring_paused is toggled only via the dedicated
    3460         // admin-post handler (cookieflame_toggle_monitoring_pause). When the
    3461         // handler's update_option() fires, the sanitize_option filter
    3462         // runs with $input containing the new value the handler wants
    3463         // to commit. When the main Settings tab saves, $input does not
    3464         // include this field, so we fall back to $existing to preserve
    3465         // whatever pause state was previously set.
     3711        'email_frequency'    => $email_frequency,
     3712        // "Watch for new cookies" Off. Toggled only via the dedicated
     3713        // admin-post handler (cookieflame_toggle_monitoring_pause) or
     3714        // the migration; the Settings tab preserves it.
    34663715        'monitoring_paused'  => isset( $input['monitoring_paused'] )  ? ( ! empty( $input['monitoring_paused'] ) ? 1 : 0 ) : ( ! empty( $existing['monitoring_paused'] ) ? 1 : 0 ),
    34673716    );
     3717    // Pre-5.4.0 setting, kept as-is so a downgrade still finds it.
     3718    if ( isset( $input['digest_interval'] ) || isset( $existing['digest_interval'] ) ) {
     3719        $clean['digest_interval'] = intval( $input['digest_interval'] ?? $existing['digest_interval'] );
     3720    }
     3721    return $clean;
    34683722}
    34693723
    … …  
    35323786    check_admin_referer( 'cookieflame_save_notifications' );
    35333787
    3534     $opts            = get_option( 'cookieflame_settings', array() );
    3535     $previous        = cookieflame_normalise_interval( $opts['digest_interval'] ?? 1440 );
     3788    $opts      = get_option( 'cookieflame_settings', array() );
     3789    $previous  = cookieflame_email_frequency();
     3790    $frequency = cookieflame_sanitize_email_frequency( sanitize_key( wp_unslash( $_POST['email_frequency'] ?? 'weekly' ) ) );
     3791    if ( 'instant' === $frequency && ! cookieflame_instant_email_available() ) {
     3792        $frequency = 'daily';
     3793    }
    35363794    $opts['notification_email'] = sanitize_email( wp_unslash( $_POST['notification_email'] ?? '' ) );
    3537     $opts['digest_interval']    = cookieflame_normalise_interval( intval( $_POST['digest_interval'] ?? 1440 ) );
     3795    $opts['email_frequency']    = $frequency;
    35383796    update_option( 'cookieflame_settings', $opts );
    35393797
    3540     // If the interval changed, clear any pending single-event digest so
    3541     // the next queued cookie reschedules against the new cadence. If the
    3542     // admin has just turned monitoring off, drop any pending digest too.
    3543     if ( $previous !== $opts['digest_interval'] || 0 === $opts['digest_interval'] ) {
     3798    // If the frequency changed, clear any pending single-event digest so
     3799    // the next queued cookie reschedules against the new setting. Emails
     3800    // turned off also drops the waiting list, so nothing stale is sent
     3801    // when they are turned back on.
     3802    if ( $previous !== $frequency ) {
    35443803        wp_clear_scheduled_hook( 'cookieflame_send_digest' );
    35453804        delete_option( 'cookieflame_digest_scheduled' );
     3805        if ( 'off' === $frequency ) {
     3806            update_option( 'cookieflame_notification_queue', array() );
     3807        }
    35463808    }
    35473809
    … …  
    35543816
    35553817/**
    3556  * Toggle the monitoring_paused flag from the Cookie Monitoring tab.
    3557  * Pause suppresses beacons, beacon handling, digest queueing, and
     3818 * Toggle "Watch for new cookies" (the monitoring_paused flag) from the
     3819 * Cookie Monitoring tab. Off suppresses beacons, beacon handling, digest queueing, and
    35583820 * digest sending — all three paths gate on cookieflame_monitoring_is_off(),
    35593821 * which now returns true when paused. On pause we also clear any
    … …  
    35713833
    35723834    $opts       = get_option( 'cookieflame_settings', array() );
    3573     $was_paused = ! empty( $opts['monitoring_paused'] );
     3835    $was_paused = cookieflame_monitoring_is_paused();
    35743836    $now_paused = ! $was_paused;
    35753837
    … …  
    35883850        // next cadence window rather than immediately.
    35893851        $queue    = get_option( 'cookieflame_notification_queue', array() );
    3590         $interval = cookieflame_current_interval_minutes();
     3852        $interval = cookieflame_email_delay_minutes();
    35913853        if ( ! empty( $queue ) && $interval > 0 && ! wp_next_scheduled( 'cookieflame_send_digest' ) ) {
    35923854            wp_schedule_single_event( time() + ( $interval * MINUTE_IN_SECONDS ), 'cookieflame_send_digest' );
    … …  
    39304192        esc_html( $a['desc'] ?? '' )
    39314193    );
     4194    if ( ! empty( $a['help'] ) ) {
     4195        printf( '<p class="description">%s</p>', esc_html( $a['help'] ) );
     4196    }
    39324197}
    39334198
    … …  
    41924457        </div>
    41934458
    4194         <?php if ( ! cookieflame_is_premium() ) : ?>
     4459        <?php if ( ! cookieflame_is_premium() && ! get_user_meta( get_current_user_id(), 'cookieflame_upgrade_banner_dismissed', true ) ) : ?>
    41954460        <div class="cf-admin-upgrade-banner">
    41964461            <div class="cf-admin-upgrade-banner-icon">&#x2728;</div>
    … …  
    42004465            </div>
    42014466            <a href="https://cookieflame.com/pricing" target="_blank" class="cf-admin-btn-upgrade"><?php esc_html_e( 'Upgrade to Pro', 'cookieflame' ); ?> &rarr;</a>
     4467            <form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" class="cf-admin-upgrade-banner-dismiss">
     4468                <?php wp_nonce_field( 'cookieflame_dismiss_upgrade_banner' ); ?>
     4469                <input type="hidden" name="action" value="cookieflame_dismiss_upgrade_banner" />
     4470                <input type="hidden" name="tab" value="<?php echo esc_attr( $active_tab ); ?>" />
     4471                <button type="submit" class="cf-admin-upgrade-banner-close" aria-label="<?php esc_attr_e( 'Dismiss this message', 'cookieflame' ); ?>" title="<?php esc_attr_e( 'Dismiss this message', 'cookieflame' ); ?>">&times;</button>
     4472            </form>
    42024473        </div>
    42034474        <?php endif; ?>
    … …  
    42094480            <p><?php esc_html_e( 'Add this shortcode to the page you have set as your Privacy Policy page (under Settings > Privacy). It shows visitors which cookie categories are active, what each cookie does, and a button to change their consent choices.', 'cookieflame' ); ?></p>
    42104481            <pre class="cf-admin-code-block">[cookieflame_declaration]</pre>
     4482            <h3><?php esc_html_e( 'Cookie Settings Link', 'cookieflame' ); ?></h3>
     4483            <p><?php esc_html_e( 'Put this shortcode in your footer or anywhere else to give visitors a "Cookie settings" link that reopens their choices. US visitors get the "Do Not Sell or Share My Personal Information" choice. You can change the wording with text="...". To use a menu item or your own button instead, give it the CSS class cc-open-settings.', 'cookieflame' ); ?></p>
     4484            <pre class="cf-admin-code-block">[cookieflame_settings_link]</pre>
    42114485        </div>
    42124486
    … …  
    42554529            <?php
    42564530            settings_fields( 'cookieflame_settings_group' );
     4531            ?>
     4532            <input type="hidden" name="cookieflame_settings[_form]" value="main" />
     4533            <?php
    42574534            do_settings_sections( 'cookieflame' );
    42584535            submit_button();
    42594536            ?>
    42604537        </form>
     4538
     4539        <?php
     4540        /**
     4541         * Fires on the Settings tab after the main settings form, so other
     4542         * plugins can add their own section (with its own form and option).
     4543         */
     4544        do_action( 'cookieflame_settings_sections' );
     4545        ?>
    42614546
    42624547        <div class="cf-admin-card cf-admin-card-warning">
    … …  
    44544739
    44554740                <h4 style="margin-top:16px;margin-bottom:8px;"><?php esc_html_e( 'How the scan works', 'cookieflame' ); ?></h4>
    4456                 <p style="margin-bottom:12px;"><?php esc_html_e( 'The scan reads cookies from this browser, right now. It does not crawl your website or check other visitors. If a cookie does not exist in your browser at this moment, it will not appear in the scan. That means you need to visit your own website first and trigger all the scripts that set cookies.', 'cookieflame' ); ?></p>
     4741                <p style="margin-bottom:12px;"><?php esc_html_e( 'Cookie Discovery uses your own browser. It records the cookies your browser picks up while you browse your site with every cookie allowed. It does not crawl your website or check other visitors, so a cookie only appears if a page you visit sets it. Cookies your visitors pick up are found separately by Watch for new cookies on the Cookie Monitoring tab.', 'cookieflame' ); ?></p>
    44574742
    44584743                <h4 style="margin-bottom:8px;"><?php esc_html_e( 'Before you start', 'cookieflame' ); ?></h4>
    4459                 <p style="margin-bottom:4px;color:#d63638;"><strong><?php esc_html_e( 'Check your tracking settings first.', 'cookieflame' ); ?></strong></p>
    4460                 <p style="margin-bottom:12px;"><?php esc_html_e( 'Go to the Settings tab. Make sure "Track logged-in users" is checked and "Do not track this device" is unchecked. If either of these is blocking tracking, scripts will be suppressed for you even after you accept cookies, and no third-party cookies will appear in the scan. Save the settings and clear your site cache if you make changes. You can turn these back on after you have finished scanning.', 'cookieflame' ); ?></p>
     4744                <p style="margin-bottom:4px;"><strong><?php esc_html_e( 'Clear your page cache if you run one.', 'cookieflame' ); ?></strong></p>
     4745                <p style="margin-bottom:12px;"><?php esc_html_e( 'You do not need to change any CookieFlame settings. While Cookie Discovery runs, "Exclude staff from tracking" and "Do not track this device" are paused for your browser, and they are put back when you click Finish Discovery.', 'cookieflame' ); ?></p>
    44614746
    44624747                <h4 style="margin-bottom:8px;"><?php esc_html_e( 'Step by step', 'cookieflame' ); ?></h4>
    … …  
    44644749                    <li>
    44654750                        <strong><?php esc_html_e( 'Start Cookie Discovery.', 'cookieflame' ); ?></strong>
    4466                         <br /><span style="color:#666;font-size:13px;"><?php esc_html_e( 'When you click the Start Cookie Discovery button above, your website opens in this tab with all cookies temporarily accepted, so every tracking script can run and set its cookies. You need to be in the same browser you are using right now for wp-admin, because cookies are stored per browser.', 'cookieflame' ); ?></span>
     4751                        <br /><span style="color:#666;font-size:13px;"><?php esc_html_e( 'Click Start Cookie Discovery above, then Open my site. Your site opens in this tab with every cookie allowed for your browser only, and a green bar at the top counts what is found. Use the browser you are using for wp-admin right now, because cookies are stored per browser.', 'cookieflame' ); ?></span>
    44674752                    </li>
    44684753                    <li>
    4469                         <strong><?php esc_html_e( 'Click "Accept All" on the consent banner.', 'cookieflame' ); ?></strong>
    4470                         <br /><span style="color:#666;font-size:13px;"><?php esc_html_e( 'This is essential. The plugin blocks all tracking scripts until consent is given. If you only accept some categories, only those scripts will run. You need to accept everything so every script fires and every cookie gets created.', 'cookieflame' ); ?></span>
     4754                        <strong><?php esc_html_e( 'You will not see the consent banner.', 'cookieflame' ); ?></strong>
     4755                        <br /><span style="color:#666;font-size:13px;"><?php esc_html_e( 'Cookie Discovery allows every cookie for your browser while it runs, so every script on your site can run and set its cookies. Your visitors still see the banner as normal.', 'cookieflame' ); ?></span>
    44714756                    </li>
    44724757                    <li>
    … …  
    44844769                    </li>
    44854770                    <li>
    4486                         <strong><?php esc_html_e( 'Come back to this tab to see your results.', 'cookieflame' ); ?></strong>
    4487                         <br /><span style="color:#666;font-size:13px;"><?php esc_html_e( 'The table will update with every cookie now sitting in your browser. Known cookies will have their details filled in automatically. Unknown cookies will be highlighted in yellow.', 'cookieflame' ); ?></span>
     4771                        <strong><?php esc_html_e( 'Click Finish Discovery on the green bar.', 'cookieflame' ); ?></strong>
     4772                        <br /><span style="color:#666;font-size:13px;"><?php esc_html_e( 'You come back here, and the table lists every cookie found. Known cookies have their details filled in automatically. Unknown cookies are highlighted in yellow.', 'cookieflame' ); ?></span>
    44884773                    </li>
    44894774                    <li>
    … …  
    45004785                    </li>
    45014786                    <li>
    4502                         <strong><?php esc_html_e( 'Re-enable your tracking protection.', 'cookieflame' ); ?></strong>
    4503                         <br /><span style="color:#666;font-size:13px;"><?php esc_html_e( 'If you turned off "Do not track this device" in step 1, go back to the Settings tab and turn it back on. You can also click "Delete Non-Essential Cookies" on the Settings tab to clean up the tracking cookies that were just created during your scan.', 'cookieflame' ); ?></span>
     4787                        <strong><?php esc_html_e( 'Tidy up your browser if you like.', 'cookieflame' ); ?></strong>
     4788                        <br /><span style="color:#666;font-size:13px;"><?php esc_html_e( 'Your tracking settings are put back automatically. To remove the tracking cookies your browser picked up during the scan, click "Delete Non-Essential Cookies from This Browser" on the Settings tab.', 'cookieflame' ); ?></span>
    45044789                    </li>
    45054790                </ol>
    … …  
    45104795                <h4 style="margin-bottom:4px;"><?php esc_html_e( 'Why are some cookies not showing up?', 'cookieflame' ); ?></h4>
    45114796                <ul style="margin:0 0 12px 20px;line-height:1.8;">
    4512                     <li><strong><?php esc_html_e( 'You did not click Accept All:', 'cookieflame' ); ?></strong> <?php esc_html_e( 'If you only accepted some categories, scripts in the other categories never ran. Their cookies do not exist. Go back and accept all categories.', 'cookieflame' ); ?></li>
    4513                     <li><strong><?php esc_html_e( 'Your tracking protection is still on:', 'cookieflame' ); ?></strong> <?php esc_html_e( 'If "Do not track this device" is enabled or "Track logged-in users" is unchecked on the Settings tab, tracking is suppressed for your browser even after you accept cookies. Temporarily disable these before scanning.', 'cookieflame' ); ?></li>
     4797                    <li><strong><?php esc_html_e( 'Your page cache served an old page:', 'cookieflame' ); ?></strong> <?php esc_html_e( 'Clear your page cache and run Cookie Discovery again.', 'cookieflame' ); ?></li>
    45144798                    <li><strong><?php esc_html_e( 'The cookie is page-specific:', 'cookieflame' ); ?></strong> <?php esc_html_e( 'Some cookies are only set on specific pages (e.g. Stripe only on checkout, a chat widget only on contact pages). Visit those pages before re-scanning.', 'cookieflame' ); ?></li>
    45154799                    <li><strong><?php esc_html_e( 'The cookie is on a subdomain:', 'cookieflame' ); ?></strong> <?php esc_html_e( 'If you have subdomains (like app.yoursite.com), cookies locked to that subdomain are invisible from your main site. Enable "Share consent across subdomains" in Settings to share cookies, or use the "Add Cookie Manually" section below to register them by name.', 'cookieflame' ); ?></li>
    … …  
    45874871                <div id="cf-admin-discovery-idle">
    45884872                    <div style="display:flex;align-items:center;justify-content:space-between;gap:16px;flex-wrap:wrap;">
    4589                         <button type="button" id="cf-admin-discovery-start" class="cf-admin-btn-primary"><?php esc_html_e( 'Start Cookie Discovery', 'cookieflame' ); ?></button>
    4590                         <span style="font-size:13px;color:#50575e;flex:1;min-width:200px;"><?php esc_html_e( 'Temporarily disables tracking protection, opens your site in a new tab with all cookies accepted, and lets you browse a few pages. When you finish, it scans for cookies and restores your settings.', 'cookieflame' ); ?></span>
     4873                        <button type="button" id="cf-admin-discovery-start" class="cf-admin-btn-primary"><?php esc_html_e( 'Start Cookie Discovery (uses your own browser)', 'cookieflame' ); ?></button>
     4874                        <span style="font-size:13px;color:#50575e;flex:1;min-width:200px;"><?php esc_html_e( 'Cookie Discovery uses your own browser: it allows every cookie for you only while you browse a few pages of your site in this tab. When you finish, it lists the cookies found and puts your settings back.', 'cookieflame' ); ?></span>
    45914875                    </div>
    45924876                    <p style="font-size:12px;color:#646970;margin:10px 0 0;"><?php esc_html_e( 'If you notice cookies you cannot explain after scanning, try running discovery in an incognito or private browser window. Browser extensions can occasionally inject tracking cookies that do not belong to your site, and incognito mode disables them by default. This is uncommon.', 'cookieflame' ); ?></p>
    … …  
    50025286            <?php if ( $cookieflame_paused ) : ?>
    50035287            <div class="notice notice-warning is-dismissible" style="margin:0 0 16px;">
    5004                 <p><strong><?php esc_html_e( 'Monitoring is now Off.', 'cookieflame' ); ?></strong> <?php esc_html_e( 'Cookie beacons and alert emails are suppressed. Your update interval is preserved for when you switch monitoring back on.', 'cookieflame' ); ?></p>
     5288                <p><strong><?php esc_html_e( 'Watch for new cookies is now Off.', 'cookieflame' ); ?></strong> <?php esc_html_e( 'Visitors\' browsers no longer report new cookies, and no emails are sent. Your email setting is kept for when you turn it back on.', 'cookieflame' ); ?></p>
    50055289            </div>
    50065290            <?php endif; ?>
    … …  
    50085292            <?php if ( $cookieflame_resumed ) : ?>
    50095293            <div class="notice notice-success is-dismissible" style="margin:0 0 16px;">
    5010                 <p><strong><?php esc_html_e( 'Monitoring is now On.', 'cookieflame' ); ?></strong> <?php esc_html_e( 'New cookies will be detected on the cadence you have configured.', 'cookieflame' ); ?></p>
     5294                <p><strong><?php esc_html_e( 'Watch for new cookies is now On.', 'cookieflame' ); ?></strong> <?php esc_html_e( 'New cookies appear in the Cookie Manager as soon as a visitor\'s browser reports them.', 'cookieflame' ); ?></p>
    50115295            </div>
    50125296            <?php endif; ?>
    50135297
    5014             <p class="cf-admin-tab-intro"><?php esc_html_e( 'Watch real visitor sessions in the background and surface new cookies as they appear.', 'cookieflame' ); ?></p>
     5298            <p class="cf-admin-tab-intro"><?php esc_html_e( 'Your visitors\' browsers report cookies as they browse your site, so new ones show up in the Cookie Manager without you scanning.', 'cookieflame' ); ?></p>
    50155299
    50165300            <?php $cookieflame_is_paused = cookieflame_monitoring_is_paused(); ?>
    50175301            <div class="cf-admin-pause-card <?php echo $cookieflame_is_paused ? 'cf-admin-pause-card-paused' : 'cf-admin-pause-card-running'; ?>">
    50185302                <div class="cf-admin-pause-card-body">
    5019                     <h3 class="cf-admin-pause-card-title"><?php esc_html_e( 'Visitor Monitoring', 'cookieflame' ); ?></h3>
    5020                     <p class="cf-admin-pause-card-desc"><?php esc_html_e( 'Pause during site surgery (plugin swaps, theme changes, migrations). Your update interval is preserved.', 'cookieflame' ); ?></p>
     5303                    <h3 class="cf-admin-pause-card-title"><?php esc_html_e( 'Watch for new cookies', 'cookieflame' ); ?></h3>
     5304                    <p class="cf-admin-pause-card-desc"><?php esc_html_e( 'Uses your visitors\' browsers. New cookies are recorded the moment a visitor\'s browser reports them. Turn it off for a while during big changes (plugin swaps, theme changes, migrations) if you prefer. Clear your page cache after switching, so cached pages pick up the change.', 'cookieflame' ); ?></p>
    50215305                </div>
    50225306                <form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" class="cf-admin-pause-card-action">
    … …  
    50275311                        <span class="cf-admin-toggle-label"><?php
    50285312                            echo $cookieflame_is_paused
    5029                                 ? esc_html__( 'Monitoring: Off', 'cookieflame' )
    5030                                 : esc_html__( 'Monitoring: On', 'cookieflame' );
     5313                                ? esc_html__( 'Watch for new cookies: Off', 'cookieflame' )
     5314                                : esc_html__( 'Watch for new cookies: On', 'cookieflame' );
    50315315                        ?></span>
    50325316                    </button>
    … …  
    50355319
    50365320            <?php
    5037             $notif_opts   = get_option( 'cookieflame_settings', array() );
    5038             $notif_email  = $notif_opts['notification_email'] ?? '';
    5039             $current_mins = cookieflame_normalise_interval( $notif_opts['digest_interval'] ?? 1440 );
    5040             $buckets      = cookieflame_interval_buckets();
    5041             $bucket_label = array(
    5042                 0     => __( 'Off', 'cookieflame' ),
    5043                 15    => __( '15 minutes', 'cookieflame' ),
    5044                 60    => __( '1 hour', 'cookieflame' ),
    5045                 240   => __( '4 hours', 'cookieflame' ),
    5046                 1440  => __( 'Daily', 'cookieflame' ),
    5047                 10080 => __( 'Weekly', 'cookieflame' ),
     5321            $notif_opts        = get_option( 'cookieflame_settings', array() );
     5322            $notif_email       = $notif_opts['notification_email'] ?? '';
     5323            $cf_frequency      = cookieflame_email_frequency();
     5324            $cf_instant_ok     = cookieflame_instant_email_available();
     5325            $cf_is_premium     = cookieflame_is_premium();
     5326            if ( 'instant' === $cf_frequency && ! $cf_instant_ok ) {
     5327                $cf_frequency = 'daily'; // What the site actually gets without Pro.
     5328            }
     5329            $cf_frequencies = array(
     5330                'weekly'  => __( 'Weekly', 'cookieflame' ),
     5331                'daily'   => __( 'Daily', 'cookieflame' ),
     5332                'instant' => $cf_instant_ok ? __( 'Instantly', 'cookieflame' ) : __( 'Instantly (Pro)', 'cookieflame' ),
     5333                'off'     => __( 'Off', 'cookieflame' ),
    50485334            );
    5049             // Dropdown ordering: fastest at top, then slower cadences,
    5050             // with Off at the very bottom as the "I don't want this"
    5051             // answer rather than a cadence choice. Off is excluded from
    5052             // the ordered list and re-appended at the end.
    5053             $cf_dropdown_order = array( 15, 60, 240, 1440, 10080 );
    5054             $cf_ordered_buckets = array();
    5055             $cf_buckets_by_minutes = array();
    5056             foreach ( $buckets as $cf_b ) {
    5057                 $cf_buckets_by_minutes[ $cf_b['minutes'] ] = $cf_b;
    5058             }
    5059             foreach ( $cf_dropdown_order as $cf_m ) {
    5060                 if ( isset( $cf_buckets_by_minutes[ $cf_m ] ) ) {
    5061                     $cf_ordered_buckets[] = $cf_buckets_by_minutes[ $cf_m ];
    5062                 }
    5063             }
    5064             if ( isset( $cf_buckets_by_minutes[ 0 ] ) ) {
    5065                 $cf_ordered_buckets[] = $cf_buckets_by_minutes[ 0 ];
    5066             }
    5067             $cf_is_premium = cookieflame_is_premium();
    50685335            ?>
    50695336
    … …  
    50755342                    <tr>
    50765343                        <th scope="row">
    5077                             <label for="cf-admin-digest-interval" title="<?php esc_attr_e( 'How often CookieFlame refreshes the cookie list and sends digest alerts. Shorter cadences surface new cookies sooner; longer cadences group changes into a single summary.', 'cookieflame' ); ?>"><?php esc_html_e( 'Update interval', 'cookieflame' ); ?></label>
     5344                            <label for="cf-admin-email-frequency"><?php esc_html_e( 'Email me about changes', 'cookieflame' ); ?></label>
    50785345                        </th>
    50795346                        <td>
    5080                             <select id="cf-admin-digest-interval" name="digest_interval">
    5081                                 <?php foreach ( $cf_ordered_buckets as $cf_b ) :
    5082                                     $cf_min   = intval( $cf_b['minutes'] );
    5083                                     $cf_tier  = (string) ( $cf_b['tier'] ?? 'free' );
    5084                                     $cf_label = $bucket_label[ $cf_min ] ?? ( $cf_min . ' min' );
    5085                                     $cf_is_pro_tier = ( 'pro' === $cf_tier );
    5086                                     $cf_disabled    = ( $cf_is_pro_tier && ! $cf_is_premium );
    5087                                     if ( $cf_is_pro_tier ) {
    5088                                         /* translators: %s: human-readable interval label, e.g. "15 minutes" */
    5089                                         $cf_label = sprintf( __( '%s (Pro)', 'cookieflame' ), $cf_label );
    5090                                     }
    5091                                     ?>
    5092                                     <option value="<?php echo esc_attr( $cf_min ); ?>"<?php
    5093                                         selected( $current_mins, $cf_min );
    5094                                         disabled( $cf_disabled, true );
     5347                            <select id="cf-admin-email-frequency" name="email_frequency">
     5348                                <?php foreach ( $cf_frequencies as $cf_value => $cf_label ) : ?>
     5349                                    <option value="<?php echo esc_attr( $cf_value ); ?>"<?php
     5350                                        selected( $cf_frequency, $cf_value );
     5351                                        disabled( 'instant' === $cf_value && ! $cf_instant_ok, true );
    50955352                                    ?>><?php echo esc_html( $cf_label ); ?></option>
    50965353                                <?php endforeach; ?>
    50975354                            </select>
    5098                             <p class="description"><?php esc_html_e( 'Pro intervals are visible above so you can see what an upgrade adds. They unlock when CookieFlame Pro is active.', 'cookieflame' ); ?></p>
     5355                            <p class="description"><?php esc_html_e( 'One email lists every new cookie found since the last one. Nothing is sent when there is nothing new.', 'cookieflame' ); ?></p>
    50995356                            <?php if ( ! $cf_is_premium ) : ?>
    5100                             <p class="description"><a href="https://cookieflame.com/pricing" target="_blank"><?php esc_html_e( 'Upgrade to CookieFlame Pro', 'cookieflame' ); ?></a></p>
     5357                            <p class="description"><?php esc_html_e( 'Instant emails come with CookieFlame Pro.', 'cookieflame' ); ?> <a href="https://cookieflame.com/pricing" target="_blank"><?php esc_html_e( 'Upgrade to CookieFlame Pro', 'cookieflame' ); ?></a></p>
    51015358                            <?php endif; ?>
    51025359                        </td>
    … …  
    51135370                <p style="display:flex;align-items:center;gap:14px;flex-wrap:wrap;">
    51145371                    <?php submit_button( __( 'Save Cookie Monitoring Settings', 'cookieflame' ), 'primary', 'submit', false ); ?>
    5115                     <span class="description" style="margin:0;"><?php esc_html_e( 'Clear your page cache after saving so visitors pick up the new beacon configuration.', 'cookieflame' ); ?></span>
    51165372                </p>
    51175373            </form>
    … …  
    51235379            <div class="cf-admin-card">
    51245380                <h2><?php esc_html_e( 'Banner Preview', 'cookieflame' ); ?></h2>
    5125                 <p><?php esc_html_e( 'Preview how the consent banner appears to visitors in different regions. Select a region and theme to see the exact banner they would see.', 'cookieflame' ); ?></p>
     5381                <p><?php esc_html_e( 'Choose a region and theme to see the banner those visitors get.', 'cookieflame' ); ?></p>
    51265382
    51275383                <div style="display:flex;gap:16px;flex-wrap:wrap;margin:20px 0;">
    51285384                    <div>
    51295385                        <label style="font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;color:#50575e;display:block;margin-bottom:6px;"><?php esc_html_e( 'Region', 'cookieflame' ); ?></label>
    5130                         <select id="cf-admin-preview-region" style="min-width:200px;padding:8px 12px;border-radius:6px;border:1px solid #dcdcde;">
     5386                        <select id="cf-admin-preview-region" style="min-width:200px;padding:8px 36px 8px 12px;border-radius:6px;border:1px solid #dcdcde;">
    51315387                            <option value="gdpr"><?php esc_html_e( 'GDPR / Opt-in (EU, EEA, UK, Brazil, South Africa, Switzerland)', 'cookieflame' ); ?></option>
    51325388                            <option value="ccpa"><?php esc_html_e( 'CCPA / Opt-out (United States)', 'cookieflame' ); ?></option>
    … …  
    51365392                    <div>
    51375393                        <label style="font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;color:#50575e;display:block;margin-bottom:6px;"><?php esc_html_e( 'Theme', 'cookieflame' ); ?></label>
    5138                         <select id="cf-admin-preview-theme" style="min-width:200px;padding:8px 12px;border-radius:6px;border:1px solid #dcdcde;">
     5394                        <select id="cf-admin-preview-theme" style="min-width:200px;padding:8px 36px 8px 12px;border-radius:6px;border:1px solid #dcdcde;">
    51395395                            <option value="light"><?php esc_html_e( 'Light', 'cookieflame' ); ?></option>
    51405396                            <option value="dark"><?php esc_html_e( 'Dark', 'cookieflame' ); ?></option>
    51415397                        </select>
    51425398                    </div>
     5399                    <div id="cf-admin-preview-reject-wrap" style="align-self:flex-end;padding-bottom:8px;">
     5400                        <label><input type="checkbox" id="cf-admin-preview-reject" /> <?php esc_html_e( 'Show "Decline all" on the first screen', 'cookieflame' ); ?></label>
     5401                    </div>
     5402                    <div id="cf-admin-preview-gpc-wrap" style="align-self:flex-end;padding-bottom:8px;display:none;">
     5403                        <label><input type="checkbox" id="cf-admin-preview-gpc" /> <?php esc_html_e( 'Visitor\'s browser sends Global Privacy Control', 'cookieflame' ); ?></label>
     5404                    </div>
    51435405                </div>
    5144 
    5145                 <div style="margin-top:8px;font-size:12px;color:#888;line-height:1.8;">
    5146                     <strong><?php esc_html_e( 'GDPR / Opt-in:', 'cookieflame' ); ?></strong>
    5147                     <?php esc_html_e( 'Full consent banner with Accept All, Decline All, and Manage Preferences. Visitors must opt in before non-essential cookies are set.', 'cookieflame' ); ?><br />
    5148                     <span style="color:#aaa;"><?php esc_html_e( 'Countries: Austria, Belgium, Bulgaria, Croatia, Cyprus, Czech Republic, Denmark, Estonia, Finland, France, Germany, Greece, Hungary, Ireland, Italy, Latvia, Lithuania, Luxembourg, Malta, Netherlands, Poland, Portugal, Romania, Slovakia, Slovenia, Spain, Sweden (EU); Iceland, Liechtenstein, Norway (EEA); United Kingdom, Guernsey, Jersey, Isle of Man (UK); Switzerland (revFADP); Brazil (LGPD); South Africa (POPIA).', 'cookieflame' ); ?></span><br /><br />
    5149                     <strong><?php esc_html_e( 'CCPA / Opt-out:', 'cookieflame' ); ?></strong>
    5150                     <?php esc_html_e( 'Compact notice bar with a "Do Not Sell My Personal Information" link. All cookies are granted by default. The visitor can opt out of the sale or sharing of personal data. If Global Privacy Control (GPC) is detected in the browser, marketing cookies are automatically suppressed.', 'cookieflame' ); ?><br />
    5151                     <span style="color:#aaa;"><?php esc_html_e( 'Countries: United States (CCPA/CPRA applies to California residents; GPC is legally binding in 12+ states including California, Colorado, Connecticut, Texas, Montana, Oregon, and others).', 'cookieflame' ); ?></span><br /><br />
    5152                     <strong><?php esc_html_e( 'No banner:', 'cookieflame' ); ?></strong>
    5153                     <?php esc_html_e( 'No consent banner is shown. All cookies are auto-granted. The cookie icon appears in the bottom-left corner so visitors can manage preferences if they choose. This applies to visitors in countries not covered by the regions above.', 'cookieflame' ); ?>
     5406                <p class="description" id="cf-admin-preview-reject-note" style="margin-top:-8px;"><?php esc_html_e( 'The "Decline all" box here only changes the preview. To change your banner, use the setting on the Settings tab.', 'cookieflame' ); ?></p>
     5407
     5408                <div id="cf-admin-preview-container" style="position:relative;border-radius:12px;overflow:hidden;margin-top:16px;"></div>
     5409
     5410                <div id="cf-admin-preview-notes" style="margin-top:16px;font-size:12px;color:#888;line-height:1.8;">
     5411                    <p data-region="gdpr" style="margin:0;"><strong><?php esc_html_e( 'GDPR / Opt-in:', 'cookieflame' ); ?></strong>
     5412                    <?php esc_html_e( 'Full consent banner with Accept All and Manage preferences. Decline All is under Manage preferences, and you can also show it next to Accept All with the setting on the Settings tab. Visitors must opt in before non-essential cookies are set.', 'cookieflame' ); ?><br />
     5413                    <span style="color:#aaa;"><?php esc_html_e( 'Countries: Austria, Belgium, Bulgaria, Croatia, Cyprus, Czech Republic, Denmark, Estonia, Finland, France, Germany, Greece, Hungary, Ireland, Italy, Latvia, Lithuania, Luxembourg, Malta, Netherlands, Poland, Portugal, Romania, Slovakia, Slovenia, Spain, Sweden (EU); Iceland, Liechtenstein, Norway (EEA); United Kingdom, Guernsey, Jersey, Isle of Man (UK); Switzerland (revFADP); Brazil (LGPD); South Africa (POPIA).', 'cookieflame' ); ?></span></p>
     5414                    <p data-region="ccpa" style="margin:0;display:none;"><strong><?php esc_html_e( 'CCPA / Opt-out:', 'cookieflame' ); ?></strong>
     5415                    <?php esc_html_e( 'Compact notice with a "Do Not Sell or Share My Personal Information" link, which opens a real opt-out choice. Cookies are allowed by default. If the visitor\'s browser sends Global Privacy Control (GPC), they are opted out of selling and sharing straight away: Marketing stays off, the notice says the signal was honoured, and OK only closes the notice. They can opt back in only through a separate choice, and a later visit without GPC keeps the opt-out.', 'cookieflame' ); ?><br />
     5416                    <span style="color:#aaa;"><?php esc_html_e( 'Countries: United States. The same notice covers California (CCPA/CPRA) and the other states with similar laws.', 'cookieflame' ); ?></span></p>
     5417                    <p data-region="none" style="margin:0;display:none;"><strong><?php esc_html_e( 'No banner:', 'cookieflame' ); ?></strong>
     5418                    <?php esc_html_e( 'No consent banner is shown and all cookies are allowed. Visitors can still change their choices from a [cookieflame_settings_link] link or your [cookieflame_declaration] page. This applies to visitors in countries not covered by the regions above.', 'cookieflame' ); ?></p>
    51545419                </div>
    51555420            </div>
    5156 
    5157             <div id="cf-admin-preview-container" style="position:relative;border-radius:12px;overflow:hidden;min-height:400px;margin-top:4px;">
    5158             </div>
    51595421        </div>
    51605422
    5161         <?php
    5162         $opts = get_option( 'cookieflame_settings', array() );
    5163         $preview_config = array(
    5164             'privacyUrl' => $opts['privacy_url'] ?? '/privacy-policy',
    5165             'ccpaUrl'    => ( $opts['privacy_url'] ?? '/privacy-policy' ) . '#do-not-sell',
    5166         );
    5167         $preview_cfg_json   = wp_json_encode( $preview_config );
    5168         $preview_theme_json = wp_json_encode( $opts['theme'] ?? 'light' );
    5169         $preview_js = <<<'JS'
    5170 (function () {
    5171     var regionEl = document.getElementById("cf-admin-preview-region");
    5172     var themeEl  = document.getElementById("cf-admin-preview-theme");
    5173     var container = document.getElementById("cf-admin-preview-container");
    5174     if (!regionEl || !themeEl || !container) return;
    5175 
    5176     var cfg = __COOKIEFLAME_PREVIEW_CFG__;
    5177 
    5178     var txt = {
    5179         title: "We value your privacy",
    5180         description: "We use cookies to improve your experience, analyse traffic and personalise content. You can choose which categories to allow.",
    5181         acceptAll: "Accept All",
    5182         declineAll: "Decline All",
    5183         savePrefs: "Save Preferences",
    5184         managePrefs: "Manage preferences",
    5185         hidePrefs: "Hide preferences",
    5186         ccpaText: "We use cookies and similar technologies. Under California law, you have the right to opt out of the sale of your personal information.",
    5187         ccpaDnsmpi: "Do Not Sell My Personal Information",
    5188         ccpaOk: "OK"
    5189     };
    5190 
    5191     var categories = [
    5192         { id: "necessary", name: "Necessary", desc: "Required for the site to function. Cannot be disabled.", required: true },
    5193         { id: "preferences", name: "Preferences", desc: "Remember your settings and personalisation choices.", required: false },
    5194         { id: "analytics", name: "Analytics", desc: "Help us understand how visitors use the site.", required: false },
    5195         { id: "marketing", name: "Marketing", desc: "Used to deliver relevant ads and track campaigns.", required: false }
    5196     ];
    5197 
    5198     function esc(s) { var d = document.createElement("div"); d.textContent = s; return d.innerHTML; }
    5199 
    5200     function render() {
    5201         var region = regionEl.value;
    5202         var theme  = themeEl.value;
    5203         var isDark = theme === "dark";
    5204         var bg = isDark ? "#121212" : "#f5f5f5";
    5205         var bannerBg = isDark ? "#1c1c1e" : "#fff";
    5206         var textColor = isDark ? "#f5f5f7" : "#1d1d1f";
    5207         var mutedColor = isDark ? "#98989d" : "#555";
    5208         var borderColor = isDark ? "#2c2c2e" : "#e8e8ed";
    5209         var catDescColor = isDark ? "#636366" : "#888";
    5210         var acceptBg = isDark ? "#0a84ff" : "#0071e3";
    5211         var declineBg = isDark ? "#2c2c2e" : "#e8e8ed";
    5212         var declineColor = isDark ? "#f5f5f7" : "#1d1d1f";
    5213         var saveBg = isDark ? "#30d158" : "#34c759";
    5214         var detailsColor = isDark ? "#636366" : "#888";
    5215         var linkColor = isDark ? "#2997ff" : "#0071e3";
    5216         var toggleBg = isDark ? "#3a3a3c" : "#ccc";
    5217         var toggleActiveBg = isDark ? "#0a84ff" : "#0071e3";
    5218         var sliderBg = isDark ? "#f5f5f7" : "#fff";
    5219         var poweredColor = isDark ? "#888" : "#ccc";
    5220 
    5221         var html = '<div style="background:' + bg + ';padding:60px 20px 20px;min-height:400px;display:flex;align-items:flex-end;justify-content:center;border-radius:12px;">';
    5222 
    5223         if (region === "none") {
    5224             html += '<div style="text-align:center;padding:40px;color:' + mutedColor + ';font-family:-apple-system,BlinkMacSystemFont,\'Segoe UI\',Roboto,sans-serif;font-size:14px;align-self:center;">';
    5225             html += '<div style="font-size:48px;margin-bottom:16px;">&#x1F30D;</div>';
    5226             html += '<div style="font-weight:700;font-size:16px;color:' + textColor + ';margin-bottom:8px;">No banner shown</div>';
    5227             html += '<div>Visitors outside the regulated regions (EU/EEA, UK, US,<br>Switzerland, Brazil, South Africa) see no consent banner.<br>All cookies are auto-granted. The cookie icon appears in the<br>bottom-left corner so they can manage preferences if they choose.</div>';
    5228             html += '<div style="margin-top:24px;width:48px;height:48px;border-radius:50%;background:' + (isDark ? "#f5f5f7" : "#1d1d1f") + ';display:inline-flex;align-items:center;justify-content:center;">';
    5229             html += '<svg width="24" height="24" viewBox="0 0 38 38" fill="none"><path d="M19.1 1.16c-.06-.42-.38-.77-.8-.83A11.8 11.8 0 0 0 13.28 1.2L7.5 4.3A11.8 11.8 0 0 0 3.95 7.98L1.09 13.9A11.8 11.8 0 0 0 .38 18.96l1.13 6.47a11.8 11.8 0 0 0 3.53 4.99l4.71 4.56a11.8 11.8 0 0 0 4.59 2.24l6.48.91a11.8 11.8 0 0 0 5.52-.89l5.78-3.09a11.8 11.8 0 0 0 3.55-3.69l2.87-5.9a11.8 11.8 0 0 0 .71-5.08c-.07-.41-.42-.73-.83-.79a14.8 14.8 0 0 1-8.16-7.61c-.14-.59-.63-1.05-1.23-1.14A14.8 14.8 0 0 1 19.1 1.16ZM15.25 10.25a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5Zm-5 15a2.5 2.5 0 1 1 5 0 2.5 2.5 0 0 1-5 0Zm17.5-5a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5Z" fill="' + (isDark ? "#1d1d1f" : "#fff") + '"/></svg>';
    5230             html += '</div>';
    5231             html += '</div>';
    5232             html += '</div>';
    5233             container.innerHTML = html;
    5234             return;
    5235         }
    5236 
    5237         if (region === "ccpa") {
    5238             html += '<div style="position:relative;bottom:0;left:0;right:0;width:100%;background:' + bannerBg + ';box-shadow:0 -8px 40px rgba(0,0,0,.15);border-radius:20px 20px 0 0;font-family:-apple-system,BlinkMacSystemFont,\'Segoe UI\',Roboto,sans-serif;font-size:14px;color:' + textColor + ';">';
    5239             html += '<div style="max-width:720px;margin:0 auto;padding:20px 32px;display:flex;align-items:center;gap:20px;flex-wrap:wrap;">';
    5240             html += '<div style="flex:1;min-width:280px;"><p style="margin:0;color:' + mutedColor + ';">' + esc(txt.ccpaText) + '</p></div>';
    5241             html += '<div style="display:flex;gap:10px;align-items:center;flex-shrink:0;">';
    5242             html += '<a href="' + esc(cfg.ccpaUrl) + '" style="color:' + linkColor + ';text-decoration:underline;font-size:13px;font-weight:500;white-space:nowrap;">' + esc(txt.ccpaDnsmpi) + '</a>';
    5243             html += '<button style="display:inline-flex;align-items:center;justify-content:center;border-radius:12px;border:none;font-weight:700;cursor:pointer;background:' + acceptBg + ';color:#fff;font-size:15px;padding:14px 32px;box-shadow:0 2px 8px rgba(0,113,227,.3);">' + esc(txt.ccpaOk) + '</button>';
    5244             html += '</div></div>';
    5245             html += '<div style="text-align:center;padding:0 0 12px;font-size:11px;color:' + poweredColor + ';"><a href="https://www.cookieflame.com" target="_blank" rel="noopener" style="color:' + poweredColor + ';text-decoration:none;">Powered by CookieFlame</a></div>';
    5246             html += '</div>';
    5247             html += '</div>';
    5248             container.innerHTML = html;
    5249             return;
    5250         }
    5251 
    5252         html += '<div style="position:relative;bottom:0;left:0;right:0;width:100%;background:' + bannerBg + ';box-shadow:0 -8px 40px rgba(0,0,0,.15);border-radius:20px 20px 0 0;font-family:-apple-system,BlinkMacSystemFont,\'Segoe UI\',Roboto,sans-serif;font-size:14px;color:' + textColor + ';">';
    5253         html += '<div style="max-width:720px;margin:0 auto;padding:32px 32px 20px;">';
    5254         html += '<p style="font-size:20px;font-weight:800;margin:0 0 8px;letter-spacing:-.3px;">' + esc(txt.title) + '</p>';
    5255         html += '<p style="margin:0 0 20px;color:' + mutedColor + ';font-size:14px;line-height:1.6;">' + esc(txt.description) + ' <a href="' + esc(cfg.privacyUrl) + '" style="color:' + linkColor + ';text-decoration:none;font-weight:500;">Privacy Policy</a></p>';
    5256 
    5257         html += '<div style="display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin-bottom:16px;">';
    5258         html += '<button style="display:inline-flex;align-items:center;justify-content:center;border-radius:12px;border:none;font-weight:700;cursor:pointer;background:' + acceptBg + ';color:#fff;font-size:15px;padding:14px 32px;box-shadow:0 2px 8px rgba(0,113,227,.3);">' + esc(txt.acceptAll) + '</button>';
    5259         html += '<button id="cf-admin-preview-manage" style="background:transparent;color:' + detailsColor + ';padding:11px 4px;font-weight:500;font-size:13px;border:none;cursor:pointer;">' + esc(txt.managePrefs) + '</button>';
    5260         html += '</div>';
    5261 
    5262         html += '<div id="cf-admin-preview-cats" style="display:none;margin:0 0 16px;border:1px solid ' + borderColor + ';border-radius:14px;overflow:hidden;">';
    5263         for (var i = 0; i < categories.length; i++) {
    5264             var cat = categories[i];
    5265             var isLast = i === categories.length - 1;
    5266             var checkedAttr = cat.required ? " checked disabled" : "";
    5267             var alwaysOn = cat.required ? ' <span style="font-weight:400;font-size:11px;color:' + (isDark ? "#48484a" : "#aaa") + ';">(always on)</span>' : "";
    5268 
    5269             html += '<div style="padding:16px 20px;' + (isLast ? "" : "border-bottom:1px solid " + borderColor + ";") + 'display:flex;justify-content:space-between;align-items:center;gap:16px;">';
    5270             html += '<div><div style="font-weight:700;font-size:14px;">' + esc(cat.name) + alwaysOn + '</div>';
    5271             html += '<div style="font-size:12px;color:' + catDescColor + ';margin-top:3px;">' + esc(cat.desc) + '</div></div>';
    5272             html += '<label style="position:relative;width:46px;height:26px;flex-shrink:0;display:block;">';
    5273             html += '<input type="checkbox"' + checkedAttr + ' style="opacity:0;width:0;height:0;position:absolute;" />';
    5274             html += '<span style="position:absolute;inset:0;background:' + (cat.required ? toggleActiveBg + ";opacity:.5" : toggleBg) + ';border-radius:26px;cursor:' + (cat.required ? "default" : "pointer") + ';transition:background .2s;"></span>';
    5275             html += '<span style="position:absolute;width:20px;height:20px;left:' + (cat.required ? "23px" : "3px") + ';bottom:3px;background:' + sliderBg + ';border-radius:50%;box-shadow:0 1px 3px rgba(0,0,0,.15);transition:transform .2s;"></span>';
    5276             html += '</label></div>';
    5277         }
    5278         html += '</div>';
    5279 
    5280         html += '<div id="cf-admin-preview-pref-btns" style="display:none;gap:10px;flex-wrap:wrap;align-items:center;margin-bottom:8px;">';
    5281         html += '<button style="display:inline-flex;align-items:center;justify-content:center;border-radius:12px;border:none;font-weight:700;cursor:pointer;background:' + saveBg + ';color:#fff;font-size:14px;padding:13px 28px;box-shadow:0 2px 8px rgba(52,199,89,.3);">' + esc(txt.savePrefs) + '</button>';
    5282         html += '<button style="display:inline-flex;align-items:center;justify-content:center;border-radius:12px;border:none;font-weight:700;cursor:pointer;background:' + declineBg + ';color:' + declineColor + ';font-size:13px;padding:11px 22px;">' + esc(txt.declineAll) + '</button>';
    5283         html += '</div>';
    5284 
    5285         html += '</div>';
    5286         html += '<div style="text-align:center;padding:0 0 16px;font-size:11px;color:' + poweredColor + ';"><a href="https://www.cookieflame.com" target="_blank" rel="noopener" style="color:' + poweredColor + ';text-decoration:none;">Powered by CookieFlame</a></div>';
    5287         html += '</div>';
    5288         html += '</div>';
    5289 
    5290         container.innerHTML = html;
    5291 
    5292         var manageBtn = document.getElementById("cf-admin-preview-manage");
    5293         var catsEl = document.getElementById("cf-admin-preview-cats");
    5294         var prefBtnsEl = document.getElementById("cf-admin-preview-pref-btns");
    5295         if (manageBtn && catsEl) {
    5296             manageBtn.addEventListener("click", function () {
    5297                 var open = catsEl.style.display === "none";
    5298                 catsEl.style.display = open ? "block" : "none";
    5299                 if (prefBtnsEl) prefBtnsEl.style.display = open ? "flex" : "none";
    5300                 manageBtn.textContent = open ? txt.hidePrefs : txt.managePrefs;
    5301             });
    5302         }
    5303     }
    5304 
    5305     render();
    5306     regionEl.addEventListener("change", render);
    5307     themeEl.addEventListener("change", render);
    5308 
    5309     themeEl.value = __COOKIEFLAME_PREVIEW_THEME__;
    5310     render();
    5311 })();
    5312 JS;
    5313         $preview_js = str_replace(
    5314             array( '__COOKIEFLAME_PREVIEW_CFG__', '__COOKIEFLAME_PREVIEW_THEME__' ),
    5315             array( $preview_cfg_json, $preview_theme_json ),
    5316             $preview_js
    5317         );
    5318         wp_print_inline_script_tag( $preview_js );
    5319         ?>
    53205423
    53215424        <?php endif; ?>
  • cookieflame/tags/5.4.0/readme.txt

    r3769546 r3771537  
    55Tested up to: 7.1
    66Requires PHP: 8.0
    7 Stable tag: 5.3.0
     7Stable tag: 5.4.0
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
    1010
    11 Cookie consent for WordPress that finds cookies automatically. Runs continuously, no external service, correct banner for GDPR, CCPA, LGPD, POPIA.
     11Cookie consent for WordPress that finds cookies automatically. Self-hosted, with the right banner for GDPR, CCPA, LGPD and POPIA visitors.
    1212
    1313== Description ==
    1414
    15 CookieFlame is a cookie consent plugin for WordPress that does three things most other free plugins do not. It finds the cookies on your site automatically by watching real visitor sessions. It runs entirely on your own server with no outside connections. And it shows the right banner for each visitor based on where they are, rather than showing a single GDPR banner to the whole world.
     15CookieFlame is a cookie consent plugin that finds your site's cookies for you, runs on your own server, and shows each visitor the banner their region requires. Full details are at [cookieflame.com](https://cookieflame.com).
    1616
    17 It is built by [Zoobu] and the full product site is at [cookieflame.com](https://cookieflame.com).
     17= Finds cookies for you =
    1818
    19 = 🔍 Cookie discovery, continuously =
     19* **Watch for new cookies:** your visitors' browsers report cookies as they browse, so a new plugin's or tag's cookies appear in the Cookie Manager without you scanning. Get an email about changes weekly, daily, or not at all.
     20* **Cookie Discovery:** for a full list now, browse your own site with every cookie allowed for your browser only. It uses your own browser.
    2021
    21 Most consent plugins expect you to know which cookies your site sets. CookieFlame does not. A lightweight beacon watches real visitor sessions in the background and reports any cookies it has not seen before. New plugin installed? Its cookies surface automatically. Tracking tag changed? Picked up next time a visitor triggers it. No scheduled scans, no headless browsers, no external scanning service.
     22= The right banner for each region =
    2223
    23 For a one-off inventory you can also run an on-demand Discovery scan from the admin. Browse your own site as logged in, watch the discovery bar accumulate cookies and detected services, click Finish when done.
     24* UK, EU/EEA, Switzerland, Brazil and South Africa: an opt-in banner with Accept All and Manage preferences. You can also show Decline All on the first screen (off by default).
     25* United States: a short notice with a "Do Not Sell or Share My Personal Information" link that opens a real opt-out choice.
     26* Everywhere else: no banner.
    2427
    25 = 🌍 Correct behaviour per region =
     28Country comes from CDN headers such as Cloudflare's, or a free MaxMind database you upload, with the time zone as a fallback. If none works, the opt-in banner is shown.
    2629
    27 CookieFlame detects visitor country and shows the banner that region actually requires:
     30= Global Privacy Control =
    2831
    29 * EU, UK, EEA, Switzerland: full two-step GDPR opt-in banner with Accept, Manage Preferences, and Decline
    30 * California and other US states with similar laws: compact CCPA opt-out notice with a Do Not Sell link
    31 * Brazil (LGPD) and South Africa (POPIA): GDPR-style opt-in banner
    32 * Everywhere else: no banner, all cookies auto-granted
     32A browser that sends Global Privacy Control (GPC) is opted out of selling and sharing straight away: marketing cookies stay off and Google's ad signals are denied. US visitors see that the signal was honoured, and OK only closes the notice. The opt-out is saved, so a later visit without GPC keeps it. A visitor can opt back in only through a separate, clear choice.
    3333
    34 Detection uses three layers in order: Cloudflare and other CDN country headers first (zero latency, works with every caching setup), then a local MaxMind GeoLite2 database if uploaded, then a browser timezone heuristic as a last-resort fallback. If all three fail, the plugin defaults to GDPR opt-in for safety rather than silently granting consent.
     34= Blocks trackers before consent =
    3535
    36 = 🔒 No outside connections =
     36Tracking scripts are rewritten on your server before the page is sent, so blocking works with WP Rocket, LiteSpeed, Cloudflare APO and other caches. Built-in patterns cover Google Analytics, Google Ads, Google Tag Manager, Microsoft Ads, Meta, X (Twitter), LinkedIn, Pinterest, Reddit and TikTok. Cookies you classify get matching rules automatically.
    3737
    38 CookieFlame does not load JavaScript from any content delivery network, does not call any geo API, does not phone home. Every part of the consent flow runs on your server and in the visitor's browser. The only optional outbound request is Google Tag Manager itself, if you enter a GTM container ID in the settings (see the External services section below).
     38YouTube, Vimeo, Google Maps, Spotify, SoundCloud, Dailymotion and Elementor videos show a click-to-load placeholder until the visitor consents.
    3939
    40 This matters because a consent tool that itself sets third-party cookies is an awkward contradiction, and because self-hosted plugins keep working during outages of services you did not choose to depend on.
     40= Google Consent Mode v2 =
    4141
    42 = 🚫 Server-side script blocking =
     42All Google consent signals start as denied before any tag runs and update the moment a visitor chooses. Returning visitors' choices are restored before Google Tag Manager loads.
    4343
    44 When a visitor has not granted consent, CookieFlame rewrites tracking scripts in the HTML output so the browser does not execute them. This happens before the page leaves your server, which means it works with any caching stack: WP Rocket, LiteSpeed, Cloudflare APO, W3 Total Cache, and others all hold the correct pre-modified HTML. A lighter JavaScript layer handles scripts injected later by GTM or other plugins.
     44= For developers =
    4545
    46 Known trackers are blocked from the moment you install the plugin. Ten services ship with built-in patterns: Google Analytics, Google Ads, Google Tag Manager, Microsoft Ads, Meta and Facebook, Twitter and X, LinkedIn, Pinterest, Reddit, TikTok. Anything else you classify in the Cookie Manager gets a matching block rule generated automatically.
     46* Events: cc:ready, cc:update, cc:accept-all, cc:decline-all, cc:save and cc:banner-rendered.
     47* JavaScript API: CookieConsent.getState(), show(), showSettings() and more.
     48* [cookieflame_settings_link] shortcode, or the class cc-open-settings on any link, to reopen a visitor's choices.
     49* CSS custom properties (--cc-accent, --cc-bg, --cc-radius, --cc-font and others) to restyle the banner.
     50* Filters cookieflame_frontend_config and cookieflame_digest_email, and the action cookieflame_settings_sections.
     51* An optional consent log endpoint for your own audit trail.
    4752
    48 = 🪟 Iframe facades =
     53= CookieFlame Pro =
    4954
    50 YouTube, Vimeo, Google Maps, Spotify, SoundCloud, Dailymotion, and Elementor video widgets are replaced with click-to-accept placeholders until the visitor grants consent. The iframe source is stripped synchronously before the embed has a chance to contact its origin.
     55Pro identifies and classifies cookies automatically, creates the blocking rules, and adds instant emails. See [cookieflame.com](https://cookieflame.com).
    5156
    52 = 📊 Google Consent Mode v2 =
     57= Compliance note =
    5358
    54 All seven Google consent signals are managed automatically and set to denied by default before any tag runs. When a visitor grants consent, the correct signals update immediately. Returning visitors keep their stored choices, with the signals restored before GTM loads (so the first dataLayer push is already correct).
    55 
    56 Works with a GTM container ID you paste into the settings. No separate GTM consent template, no additional configuration. The plugin also injects the body noscript tag in the correct position.
    57 
    58 = 🛡️ Global Privacy Control =
    59 
    60 GPC is detected automatically and suppresses marketing consent for CCPA visitors. Required by California, Colorado, Connecticut, Texas, and nine other US states. Turning GPC off later restores the visitor's original stored choice rather than overwriting it.
    61 
    62 = 📜 IAB TCF v2.2 =
    63 
    64 A TCF v2.2 stub is registered on every page so ad-tech vendors that need TCF signals can read them.
    65 
    66 = 🚀 Lightweight and framework-free =
    67 
    68 The frontend JavaScript is a single 37 KB file (12 KB gzipped) with no jQuery and no framework dependencies. A separate facades bundle is only loaded on pages that actually contain a blockable embed. The Discovery bundle only loads for administrators during an active scan.
    69 
    70 = 👨‍💻 Developer-friendly =
    71 
    72 * Event API with CustomEvents: cc:ready, cc:update, cc:accept-all, cc:decline-all, cc:save
    73 * Public JavaScript API for reading consent state and showing the banner on demand
    74 * Per-category onAccept and onReject callbacks
    75 * Extension points via WordPress filters and actions
    76 * Optional consent log endpoint for audit trails
    77 
    78 = ✨ Want automatic classification? =
    79 
    80 CookieFlame Pro adds automatic cookie classification using a central database and AI. You click Auto-Identify All, and the plugin classifies every unclassified cookie with vendor, category, and purpose, then generates the blocking rules automatically. Flat annual pricing by number of sites, no pageview limits. Learn more at [cookieflame.com](https://cookieflame.com).
    81 
    82 = 📋 Compliance note =
    83 
    84 CookieFlame provides tools that help support compliance with GDPR, CCPA, LGPD, POPIA and similar regulations. Compliance itself is the site owner's responsibility and depends on how the plugin is configured. For legal advice, consult a qualified professional.
     59CookieFlame helps with GDPR, CCPA, LGPD, POPIA and similar laws; how you set it up is your responsibility. For legal advice, ask a qualified professional.
    8560
    8661== Installation ==
    8762
    88 1. Upload the `cookieflame` folder to `/wp-content/plugins/`, or install via Plugins > Add New in the WordPress admin
    89 2. Activate the plugin through the Plugins menu
    90 3. Go to **CookieFlame** in the admin menu to review settings
     631. Install from Plugins > Add New, or upload the `cookieflame` folder to `/wp-content/plugins/`.
     642. Activate the plugin.
     653. Open **CookieFlame** in the admin menu to check the settings.
    9166
    92 That is it. The plugin works immediately with sensible defaults. The ten built-in tracking services are blocked before any visitor can trigger them, and the banner shows correctly for each region.
    93 
    94 For best geo detection, either run Cloudflare (the plugin reads the CF-IPCountry header automatically) or upload a free MaxMind GeoLite2 database on the Geo Detection tab. Neither is required.
     67It works straight away. For the best country detection, use Cloudflare or upload a free MaxMind database on the Geo Detection tab.
    9568
    9669== Frequently Asked Questions ==
    … …  
    9871= Do I need to know which cookies my site sets? =
    9972
    100 No. CookieFlame watches real visitor sessions and reports new cookies to the Cookie Manager automatically. You classify them when you are ready, and the plugin generates the blocking rules for you.
     73No. Watch for new cookies lists them as your visitors' browsers report them, and Cookie Discovery finds them on demand. You classify them when you are ready.
    10174
    102 = Does this work with Google Tag Manager? =
     75= Does it work with Google Tag Manager and caching plugins? =
    10376
    104 Yes. Enter your GTM container ID on the Tag Managers tab and the plugin injects both the head script and body noscript tag in the correct positions. All GTM tags that respect Google Consent Mode will see the denied state on load and wait for consent.
     77Yes. Blocking happens in the HTML before it is cached. Turn off Cloudflare Rocket Loader; the plugin warns you if it is on.
    10578
    106 = Does this work with Cloudflare, WP Rocket, and other caching setups? =
     79= What do US visitors see? =
    10780
    108 Yes. Script blocking runs server-side in the PHP output buffer, so the cached HTML already has the correct tags rewritten. Cloudflare is specifically supported for geo detection via the CF-IPCountry header on all plans including free.
     81A short notice. Cookies are allowed by default because US laws use an opt-out model. The "Do Not Sell or Share My Personal Information" link opens a choice to opt out. Put [cookieflame_settings_link] in your footer so visitors can reach it again later.
    10982
    110 One exception: Cloudflare Rocket Loader rewrites scripts at the edge after PHP has run, which defeats the blocking layer. Rocket Loader must be disabled when using CookieFlame. The plugin detects this and shows a warning if it is active.
     83= Can I show "Decline all" on the first screen? =
    11184
    112 = What happens for US visitors? =
     85Yes. Turn on "Show 'Decline all' on the first screen" on the Settings tab. It appears only on the opt-in banner, never on the US notice.
    11386
    114 US visitors see a compact CCPA opt-out notice at the bottom of the page with a Do Not Sell My Personal Information link. Cookies are auto-granted by default because CCPA uses an opt-out model, and the visitor can revoke consent through the notice.
     87= Can I change the banner text? =
    11588
    116 = What happens for visitors outside the regulated regions? =
    117 
    118 No banner is shown and all cookies are auto-granted. CookieFlame does not show GDPR banners to visitors whose region does not require them.
    119 
    120 = Does the plugin contact any external service? =
    121 
    122 By default, no. Geo detection reads CDN headers first, then a local MaxMind database if you have uploaded one, then the browser timezone as a last resort. No external geo API is called. The plugin does not load scripts from any CDN and does not phone home.
    123 
    124 The one optional external request is Google Tag Manager, but only if you have entered a GTM container ID in the settings. See the External services section for full details.
    125 
    126 = Does it handle Global Privacy Control (GPC)? =
    127 
    128 Yes. GPC is required by twelve US states and the plugin detects the browser signal automatically. For CCPA visitors with GPC active, marketing consent is suppressed before any tag runs. Turning GPC off later restores the visitor's original choice.
    129 
    130 = Can I customise the banner text and colours? =
    131 
    132 Yes. Every string shown to visitors is editable through the Settings tab, and the banner supports both light and dark themes. For per-language versions, use WPML or Polylang with the standard WordPress translation pipeline.
    133 
    134 = What is CookieFlame Pro? =
    135 
    136 Pro adds automatic cookie classification. Instead of classifying each cookie manually, you click Auto-Identify All and the plugin uses a central database of known cookies plus AI to classify them with vendor, category, and purpose. Pro also adds tighter monitoring intervals, a more detailed digest email, and automatic MaxMind database updates. Pricing is flat annual by site count with no pageview limits. See [cookieflame.com](https://cookieflame.com) for details.
     89Not yet from the settings. The banner is in English, with light and dark themes. Developers can change the text and colours with the cookieflame_frontend_config filter and the --cc-* CSS properties.
    13790
    13891= Is my visitors' data safe? =
    13992
    140 CookieFlame does not collect visitor data. IP addresses are hashed for rate-limiting and never stored raw. Cookie values are never transmitted, only their name and a shape classification (uuid, timestamp, and similar) used for identification. The plugin stores consent decisions in a first-party cookie on the visitor's own browser.
     93CookieFlame collects no visitor data. Cookie values are never sent, only names. IP addresses are hashed for rate limiting and never stored. Choices are kept in a first-party cookie in the visitor's browser.
    14194
    14295== External services ==
    14396
    144 CookieFlame is a self-hosted plugin. By default it does not contact any external service from the visitor's browser, and it does not phone home.
     97CookieFlame does not phone home. These requests happen only in the cases described.
    14598
    146 There is one optional external interaction, only if the site owner configures it.
     99**Google Tag Manager (optional).** If you enter a container ID on the Tag Managers tab, Google's script (https://www.googletagmanager.com/gtm.js) loads on every page, except for visitors you exclude from tracking. Tags inside it follow the visitor's Consent Mode choices. Sent: the container ID and the browser's standard request data (IP address, user agent, page URL). [Terms](https://marketingplatform.google.com/about/analytics/tag-manager/use-policy/), [privacy policy](https://policies.google.com/privacy).
    147100
    148 **Google Tag Manager (optional)**
     101**Microsoft UET (optional).** If you enter a UET tag ID, Microsoft's loader (https://bat.bing.com/bat.js) loads on every page, except for visitors you exclude from tracking. It records nothing until the visitor allows Marketing. Sent: the tag ID and the browser's standard request data. [Microsoft Advertising agreement](https://about.ads.microsoft.com/en/resources/policies/microsoft-advertising-agreement), [privacy statement](https://privacy.microsoft.com/en-gb/privacystatement).
    149102
    150 If the site owner enters a Google Tag Manager container ID on the CookieFlame Tag Managers settings tab, CookieFlame injects the standard Google Tag Manager script (https://www.googletagmanager.com/gtm.js) into the site front-end under the visitor's consent. This script is provided by Google and is loaded from Google's servers when the page is viewed.
     103**YouTube thumbnails.** When a page has a YouTube embed that is waiting for consent, its placeholder shows the video's thumbnail from img.youtube.com. Sent: the video ID and the browser's standard request data. [YouTube terms](https://www.youtube.com/t/terms), [Google privacy policy](https://policies.google.com/privacy).
    151104
    152 * What is sent: standard Google Tag Manager request, including the container ID and whatever browser request data Google receives for any embed.
    153 * When: only if the admin has set a GTM container ID, and only when a visitor loads a page and has consented.
    154 * Google Tag Manager terms: https://marketingplatform.google.com/about/analytics/tag-manager/use-policy/
    155 * Google privacy policy: https://policies.google.com/privacy
     105**Consent log endpoint (optional).** If you enter a URL in the Consent Log Endpoint setting, each consent choice is sent there from the visitor's browser as JSON: an anonymous ID, time, action, the choice per category, region, country code, page URL, user agent, the GPC flag, consent version and plugin version. No cookie values or IP address are in it. You choose the destination and are responsible for it.
    156106
    157 If no GTM container ID is set, no request is made.
    158 
    159 **Microsoft UET (Bing Ads tag, optional)**
    160 
    161 If the site owner enters a Microsoft UET tag ID on the CookieFlame Tag Managers settings tab, CookieFlame injects the standard Microsoft UET loader script (https://bat.bing.com/bat.js) into the site front-end. The loader is provided by Microsoft and is loaded from Microsoft's servers when the page is viewed.
    162 
    163 The UET runtime reads Microsoft Consent Mode v2 signals from the dataLayer that CookieFlame already manages, so the loader honours visitor consent internally: tracking events do not record until the visitor has granted Marketing consent. The loader script itself is fetched on every page load while a UET tag ID is configured.
    164 
    165 * What is sent: the UET tag ID, plus the standard browser request data Microsoft receives for any third-party script load (visitor IP, user agent, page URL, referer).
    166 * When: every page load while a UET tag ID is configured, except for visitors whose browser sends the Do Not Track header in which case CookieFlame suppresses the injection entirely.
    167 * Microsoft Advertising agreement: https://about.ads.microsoft.com/en/resources/policies/microsoft-advertising-agreement
    168 * Microsoft privacy statement: https://privacy.microsoft.com/en-gb/privacystatement
    169 
    170 If no UET tag ID is set, no request is made.
    171 
    172 **Consent log endpoint (optional, site-owner-nominated webhook)**
    173 
    174 If the site owner enters a URL in the Consent Log endpoint setting, CookieFlame sends a JSON record of each consent decision to that URL from the visitor's browser via navigator.sendBeacon (with a fetch fallback for browsers without sendBeacon). The default for this setting is empty, in which case nothing is sent. The destination URL is nominated by the site owner; terms and privacy obligations for that URL are the site owner's responsibility, not CookieFlame's.
    175 
    176 * What is sent: a JSON payload containing an anonymous ID, an ISO-8601 timestamp, the action that produced the record (accept-all, decline-all, save, or one of the region-based auto-grants), the per-category consent decision (necessary, preferences, analytics, marketing), the compliance mode (gdpr, ccpa, none), the visitor country code, the page URL, the user agent string, the TCF v2.2 flag, the Global Privacy Control flag, the consent policy version, and the plugin version. No cookie values are included. No IP address is included in the payload, although the receiving endpoint will of course see the visitor IP via the underlying HTTP request, the same as any other request.
    177 * When: every time the visitor's consent state is set. This includes banner actions (accept-all, decline-all, save) and region-based auto-grants for visitors outside GDPR regions and for CCPA opt-out visitors.
    178 
    179 If no Consent Log endpoint URL is set, no request is made.
    180 
    181 **Domain strings in the plugin code**
    182 
    183 The plugin code contains many third-party domain names (for example, connect.facebook.net, googletagmanager.com, and similar). These are pattern strings used for local matching only. They identify scripts that may already be present on the admin's site so CookieFlame can detect them, show them to the admin in the Cookie Manager, and block them until the visitor has given consent. CookieFlame does not fetch from these domains.
     107Third-party domain names in the code (such as connect.facebook.net) are only used to recognise and block scripts; they are never contacted.
    184108
    185109== Screenshots ==
    186110
    187 1. GDPR consent banner with category toggles (two-step flow)
    188 2. CCPA opt-out notice for US visitors
    189 3. Cookie Manager showing discovered cookies with their origin
    190 4. Cookie Discovery in progress: the discovery bar accumulates services
    191 5. Admin Settings tab with theme and regional behaviour controls
    192 6. Geo Detection tab with CDN status and MaxMind upload
    193 7. Tag Managers tab for GTM and Microsoft UET
     1111. Opt-in banner with category toggles
     1122. US notice with the "Do Not Sell or Share" link
     1133. Cookie Manager listing your site's cookies
     1144. Cookie Discovery in progress
     1155. Settings tab
     1166. Geo Detection tab
     1177. Tag Managers tab
    194118
    195119== Changelog ==
    196120
    197 = 5.3.0 =
    198 * Cookie monitoring now ignores cookies that come with a visitor's browser rather than from your site. Some browser extensions write the "already accepted" cookies of other consent tools (such as Complianz, CookieYes, Cookiebot, OneTrust or Didomi) onto every site they visit, and these were being reported as your site's cookies. A consent tool's cookie is now ignored when that tool doesn't run on your site: its plugin isn't active, its script isn't on the page, and no script on the page wrote the cookie.
    199 * Cookies written by a browser extension's own code are ignored too.
    200 * Cookies ignored this way go on the Permanently Ignored list with the reason, and the list now shows why each cookie was ignored. Restoring one keeps it monitored from then on.
    201 * The Cookie Manager and the Cookie Discovery count leave these cookies out too, including ones your own browser extensions add. Cookies that came with your browser during Cookie Discovery go on the Permanently Ignored list with the reason. Cookies on the Permanently Ignored list no longer show in the Cookie Manager just because they are in your own browser.
    202 * On update, other consent tools' cookies that were already reported are moved to the Permanently Ignored list. Cookies you classified yourself are left alone.
    203 * Fixed: the "consent plugin detected" conflict warning could be set off by CookieFlame's own list of cookie names rather than by another consent plugin on the page.
    204 * Cookie Manager: the last column is now "Status". Each cookie shows one label for where it stands (Known, Auto-Identified, Classified, Added by you, Changed by you, or Not identified yet), and cookies that aren't identified yet say where they came from: found by your scan, reported by visitors, or seen in your own browser.
    205 * Cookie Manager: Remove and Ignore permanently are now one "Remove…" link that asks why. "My site doesn't use it any more" takes the cookie off your list, and it comes back if it's seen again. "It's not from my site" puts it on the Permanently Ignored list for good.
    206 * Cookie Manager: with CookieFlame Pro, the padlock is now a "Lock as correct" button. A locked cookie keeps its vendor, category and purpose, even when you re-identify.
    207 * Cookie Manager: the small help buttons beside each link are gone. Each link explains itself when you hover over it, and the Status column's help button explains every label and action.
    208 * Improved: when another consent tool's code is on your pages, the warning now says so in plain words ("Cookiebot found on your pages"), names the page and the text to look for, and lists where such code usually lives (plugins, page builder custom code such as Elementor > Custom Code, the theme's header, or a header and footer scripts plugin). It used to say a plugin was installed even when the code came from somewhere else.
    209 
    210 = 5.2.0 =
    211 * Cookie monitoring now records which script first wrote each new cookie: a third-party script's domain, or the WordPress plugin or theme that set it. Previously only the full list of third-party scripts on the page was known. Cookie values are never read or sent.
    212 * Cookie monitoring now checks again later in the visit, a few seconds after the visitor changes their consent choice, and when they leave the page, so cookies set after consent or after the first few seconds are found.
    213 * Fixed: when a page reported several new cookies at the same moment, some could be lost, because the reports overwrote each other when saving. Reports are now saved one at a time, so every new cookie is kept.
    214 * The cookieflame_new_cookie_discovered action now fires after the cookie's details are saved and passes them as a fourth argument (pre-existing flag, value type, writer). Listeners using three arguments are unaffected.
    215 * Tested up to WordPress 7.1.
    216 
    217 = 5.1.1 =
    218 * Cookie declaration shortcode tag is now [cookieflame_declaration] (previously [cookieconsent_declaration]). If you embed the cookie declaration on your Privacy Policy page, update the shortcode tag.
    219 * URL fields on the Settings page (Privacy URL, CCPA URL, Consent log endpoint) now use esc_url_raw, the WordPress URL sanitiser, in place of sanitize_text_field.
    220 
    221 = 5.1.0 =
    222 * Cookie Monitoring tab redesigned for clarity. The page now leads with the two controls that matter (Monitoring on/off and Update interval) and trims the long-form intro and guidance copy that buried them.
    223 * New: the Update interval dropdown shows CookieFlame Pro intervals (15 minutes, 1 hour, 4 hours) inline as disabled options labelled "(Pro)", so you can see at a glance what an upgrade adds without leaving the page. They unlock when CookieFlame Pro is active.
    224 * Removed the Monthly interval. It was producing digests so far apart that newly discovered cookies were already weeks stale by the time the email arrived. Existing sites set to Monthly will move to Daily on update.
    225 * Removed the in-page "Clear monitoring history" button. The 30-day auto-purge handles staleness without intervention; for the rare support case where a full reset is needed, the new WP-CLI command "wp cookieflame purge-beacon-seen" replaces the button.
    226 * Filter contract change for developers extending the interval list: cookieflame_interval_buckets now passes tier-aware entries (each entry shaped ['minutes' => N, 'tier' => 'free'|'pro']). Plain integers from third-party filter contributors are still accepted and default to the free tier for back-compatibility.
    227 
    228 = 5.0.2 =
    229 * Fixed: Geo Detection now correctly recognises the Cloudflare CF-IPCountry header again. Sites fronted by Cloudflare without a MaxMind database installed were falling through to the GDPR opt-in safety default for every visitor, which meant CCPA-eligible US visitors saw the GDPR banner instead of the CCPA notice, and visitors in regions where no banner is required also saw the GDPR banner. The Geo Detection admin panel was reporting "Not configured" on Cloudflare-fronted sites for the same reason. No setting changes are required; geo detection resumes automatically on update.
    230 
    231 = 5.0.1 =
    232 * Hardened consent boot: the five wp_head injection hooks (consent defaults, config, frontend script, Google Tag Manager, Microsoft UET) now register at deep-negative priorities so other plugins cannot hook ahead of CookieFlame and push tags before consent signals are set.
    233 * Beacon endpoint refactored: the per-IP rate limit now runs as a REST permission_callback, rejecting bursts at the routing layer above the request handler. The same check still runs inside the handler as a belt-and-braces guard.
    234 * Defensive shutdown handler added for the output buffer: walks the buffer stack at end of request and flushes only CookieFlame's own buffer if it remains open, leaving any caching or optimisation plugin buffers above it untouched.
    235 * Database queries against the beacon-seen table reworked to inline the WordPress table prefix directly in each SQL literal, with rationale comments on the remaining advisory warnings.
    236 * wp_unslash applied consistently to display-only query parameters on the Cookie Monitoring tab.
    237 * Readme: added an Upgrade Notice section.
    238 
    239 = 5.0.0 =
    240 * Initial public release.
     121= 5.4.0 =
     122* New: an option to show "Decline All" on the banner's first screen, next to Accept All. Off by default, and only on the opt-in banner (UK, EU/EEA, Switzerland, Brazil, South Africa), never on the US notice.
     123* Global Privacy Control now follows California's rules (§7025): OK only closes the US notice, the notice says the signal was honoured, the opt-out is saved, and visitors can opt back in only through a separate choice.
     124* "Do Not Sell or Share My Personal Information" now opens a real opt-out choice on the page, instead of linking to your privacy page.
     125* Cookie Monitoring is now two settings: "Watch for new cookies" On/Off, and "Email me about changes": Weekly (the default for new sites), Daily, Instantly (Pro) or Off. Your old setting carries over; 15 minutes, 1 hour and 4 hours become Instantly with Pro and Daily without it.
     126* New: [cookieflame_settings_link] shortcode and a cc-open-settings class for a "Cookie settings" link. The cookie declaration now has a "Change cookie preferences" button.
     127* New: "Visitor logins" setting. When ticked, the cookie declaration lists the WordPress login cookies under "If you log in". Otherwise they are left out, so every visitor sees the same declaration.
     128* The upgrade message at the top of the CookieFlame pages can now be dismissed.
     129* For developers: --cc-* CSS properties, two filters, an action and the cc:banner-rendered event (see For developers).
     130* The IAB TCF placeholder, which never carried a consent string, is now off by default.
     131* Fixed: a Re-consent Period below 1 day made the banner appear on every page. It now means the default of 365 days.
     132* Fixed: the US notice could run off the screen on phones.
     133* Fixed: the X (Twitter) embed placeholder also caught other sites whose names end in "x.com", such as netflix.com and box.com.
     134* Fixed: the WordPress cookie list: login cookie expiry, comment and protected-page cookies, the wp-settings-time description, and PHP and WooCommerce cookies listed as WordPress core.
     135* Fixed: saving Cookie Monitoring settings, or switching monitoring on or off, turned "Exclude staff from tracking" off.
     136* Older changes: changelog.txt.
    241137
    242138== Upgrade Notice ==
    243139
    244 = 5.3.0 =
    245 Cookie monitoring now ignores other consent tools' cookies that visitors' browser extensions add, so they no longer appear as your site's cookies. The Cookie Manager's last column is tidier and easier to understand.
    246 
    247 = 5.2.0 =
    248 Better cookie detection: CookieFlame now records which script set each new cookie and keeps checking after consent is given.
    249 
    250 = 5.1.0 =
    251 Cookie Monitoring tab redesigned and the Monthly interval removed. If you were on Monthly, your setting will move to Daily on update. The "Clear monitoring history" button has been replaced with a WP-CLI command for the rare cases where it is needed.
    252 
    253 = 5.0.2 =
    254 Bug fix release. Restores Cloudflare CF-IPCountry geo detection on sites fronted by Cloudflare without MaxMind. Recommended for all users on Cloudflare.
    255 
    256 = 5.0.1 =
    257 Compatibility and hardening release. Refactors a public endpoint's permission check, hardens consent boot priority, and addresses static-analysis warnings. No setting or visitor-facing behaviour changes.
    258 
    259 = 5.0.0 =
    260 Initial public release.
     140= 5.4.0 =
     141Optional "Decline all" button, Global Privacy Control handled to California's rules, a real "Do Not Sell or Share" choice, and simpler Cookie Monitoring settings. Your settings carry over.
  • cookieflame/tags/5.4.0/uninstall.php

    r3769546 r3771537  
    3737delete_option( 'cookieflame_maxmind_last_error' );
    3838delete_option( 'cookieflame_beacon_seen_schema' );
     39delete_metadata( 'user', 0, 'cookieflame_upgrade_banner_dismissed', '', true );
    3940
    4041// Drop the beacon-seen custom table (only on opt-in deletion).
  • cookieflame/trunk/assets/css/admin.css

    r3769546 r3771537  
    625625  border: 1px solid #dcdcde;
    626626  border-radius: 4px;
     627}
     628.cf-admin-manual-row select {
     629  padding-right: 28px;
    627630}
    628631.cf-admin-manual-row .button-link-delete {
    … …  
    10171020  color: #c2410c;
    10181021}
     1022.cf-admin-upgrade-banner-dismiss {
     1023  margin: 0;
     1024  flex-shrink: 0;
     1025  align-self: flex-start;
     1026}
     1027.cf-admin-upgrade-banner-close {
     1028  background: none;
     1029  border: none;
     1030  color: #8c8f94;
     1031  font-size: 20px;
     1032  line-height: 1;
     1033  padding: 2px 4px;
     1034  cursor: pointer;
     1035  border-radius: 4px;
     1036}
     1037.cf-admin-upgrade-banner-close:hover,
     1038.cf-admin-upgrade-banner-close:focus {
     1039  color: #1d2327;
     1040  background: rgba(0, 0, 0, 0.05);
     1041}
    10191042
    10201043/* Licence tab Pro features card */
    … …  
    14751498  transition: border-color .15s;
    14761499}
     1500/* Room for WordPress's select arrow, drawn about 5-21px from the right edge. */
     1501.cf-admin-wrap .form-table select {
     1502  padding-right: 32px;
     1503}
    14771504.cf-admin-wrap .form-table input:focus,
    14781505.cf-admin-wrap .form-table select:focus,
  • cookieflame/trunk/assets/css/cookieconsent.css

    r3766806 r3771537  
    11/**
    2  * CookieFlame v4.41.0 — Consent Banner Stylesheet
     2 * CookieFlame v5.4.0 — Consent Banner Stylesheet
    33 * ─────────────────────────────────────────────────────────────
    44 * Loaded on every front-end page via cookieflame_enqueue_assets().
    5  * Before v4.41.0 these rules were injected inline by
    6  * cookieconsent.js at runtime; they are now an external
    7  * stylesheet so the main JS bundle is ~5.6 KB smaller and
    8  * the CSS can be cached independently by the browser.
    95 *
    106 * Dark mode: JS adds `.cc-dark` to <html> when theme=dark.
    117 * Icon position: JS adds `.cc-icon-right` to <html> when
    128 * iconPosition=right (default is left).
     9 *
     10 * Custom properties (v5.4.0): the banner's colours, corner radius and
     11 * font come from the --cc-* properties below. Override them on :root
     12 * (and :root.cc-dark for the dark theme) to restyle the banner.
    1313 */
     14
     15:root{--cc-font:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;--cc-radius:20px;--cc-btn-radius:12px;--cc-bg:#fff;--cc-text:#1d1d1f;--cc-text-muted:#555;--cc-link:#0071e3;--cc-focus:#0071e3;--cc-banner-shadow:rgba(0,0,0,.15);--cc-accent:#0071e3;--cc-accent-hover:#005bb5;--cc-accent-text:#fff;--cc-accent-shadow:rgba(0,113,227,.3);--cc-accent-shadow-hover:rgba(0,113,227,.35);--cc-secondary-bg:#e8e8ed;--cc-secondary-hover:#d2d2d7;--cc-secondary-text:#1d1d1f;--cc-save-bg:#34c759;--cc-save-hover:#28a745;--cc-save-text:#fff;--cc-save-shadow:rgba(52,199,89,.3);--cc-save-shadow-hover:rgba(52,199,89,.35);--cc-details:#555;--cc-details-hover:#555;--cc-border:#e8e8ed;--cc-divider:#f0f0f2;--cc-cat-desc:#646970;--cc-always-on:#767676;--cc-toggle-off:#ccc;--cc-toggle-on:#0071e3;--cc-knob:#fff;--cc-powered:#999;--cc-powered-hover:#666;--cc-icon-bg:#1d1d1f;--cc-icon-text:#fff;--cc-icon-shadow:rgba(0,0,0,.25);--cc-icon-shadow-hover:rgba(0,0,0,.35)}
     16:root.cc-dark{--cc-bg:#1c1c1e;--cc-text:#f5f5f7;--cc-text-muted:#98989d;--cc-link:#2997ff;--cc-banner-shadow:rgba(0,0,0,.5);--cc-accent:#0a84ff;--cc-accent-hover:#0071e3;--cc-accent-shadow:rgba(10,132,255,.35);--cc-accent-shadow-hover:rgba(10,132,255,.4);--cc-secondary-bg:#2c2c2e;--cc-secondary-hover:#3a3a3c;--cc-secondary-text:#f5f5f7;--cc-save-bg:#30d158;--cc-save-hover:#28a745;--cc-save-shadow:rgba(48,209,88,.3);--cc-save-shadow-hover:rgba(48,209,88,.35);--cc-details:#98989d;--cc-details-hover:#c7c7cc;--cc-border:#2c2c2e;--cc-divider:#2c2c2e;--cc-cat-desc:#8e8e93;--cc-always-on:#8e8e93;--cc-toggle-off:#3a3a3c;--cc-toggle-on:#0a84ff;--cc-knob:#f5f5f7;--cc-powered:#aaa;--cc-powered-hover:#ddd;--cc-icon-bg:#f5f5f7;--cc-icon-text:#1d1d1f;--cc-icon-shadow:rgba(255,255,255,.15);--cc-icon-shadow-hover:rgba(255,255,255,.25)}
    1417
    1518#cc-overlay{position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:999998;opacity:0;pointer-events:none;transition:opacity .3s;backdrop-filter:blur(2px);-webkit-backdrop-filter:blur(2px)}
    1619#cc-overlay.cc-show{opacity:1;pointer-events:auto}
    17 #cc-banner{position:fixed;bottom:0;left:0;right:0;z-index:999999;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;font-size:14px;line-height:1.5;color:#1d1d1f;background:#fff;box-shadow:0 -8px 40px rgba(0,0,0,.15);transform:translateY(100%);transition:transform .4s cubic-bezier(.4,0,.2,1);border-radius:20px 20px 0 0;max-height:90vh;overflow-y:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}
     20#cc-banner{position:fixed;bottom:0;left:0;right:0;z-index:999999;font-family:var(--cc-font);font-size:14px;line-height:1.5;color:var(--cc-text);background:var(--cc-bg);box-shadow:0 -8px 40px var(--cc-banner-shadow);transform:translateY(100%);transition:transform .4s cubic-bezier(.4,0,.2,1);border-radius:var(--cc-radius) var(--cc-radius) 0 0;max-height:90vh;overflow-y:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}
    1821#cc-banner.cc-show{transform:translateY(0)}
    1922#cc-banner,#cc-banner *{box-sizing:border-box;-webkit-tap-highlight-color:transparent}
    … …  
    2124.cc-inner{max-width:720px;margin:0 auto;padding:32px 32px 20px}
    2225.cc-title{font-size:20px !important;font-weight:800 !important;margin:0 0 8px !important;letter-spacing:-.3px;line-height:1.3 !important;color:inherit !important;background:none !important;border:none !important;padding:0 !important;text-transform:none !important}
    23 .cc-text{margin:0 0 20px !important;color:#555 !important;font-size:14px !important;font-weight:400 !important;line-height:1.6 !important;padding:0 !important;border:none !important;background:none !important;text-transform:none !important}
    24 .cc-text a{color:#0071e3 !important;text-decoration:none !important;font-weight:500 !important;background:none !important;border:none !important;box-shadow:none !important}
     26.cc-text{margin:0 0 20px !important;color:var(--cc-text-muted) !important;font-size:14px !important;font-weight:400 !important;line-height:1.6 !important;padding:0 !important;border:none !important;background:none !important;text-transform:none !important}
     27.cc-text a{color:var(--cc-link) !important;text-decoration:none !important;font-weight:500 !important;background:none !important;border:none !important;box-shadow:none !important}
    2528.cc-text a:hover{text-decoration:underline !important}
    2629.cc-buttons{display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin-bottom:16px}
    27 .cc-btn{display:inline-flex !important;align-items:center !important;justify-content:center !important;border-radius:12px !important;border:none !important;font-weight:700 !important;cursor:pointer !important;letter-spacing:-.1px !important;text-transform:none !important;text-decoration:none !important;line-height:1 !important;white-space:nowrap !important;-webkit-appearance:none !important;appearance:none !important;outline:none !important;box-shadow:none !important;text-shadow:none !important;float:none !important;margin:0 !important}
    28 .cc-btn:focus{outline:2px solid #0071e3 !important;outline-offset:2px !important}
     30.cc-btn{display:inline-flex !important;align-items:center !important;justify-content:center !important;border-radius:var(--cc-btn-radius) !important;border:none !important;font-weight:700 !important;cursor:pointer !important;letter-spacing:-.1px !important;text-transform:none !important;text-decoration:none !important;line-height:1 !important;white-space:nowrap !important;-webkit-appearance:none !important;appearance:none !important;outline:none !important;box-shadow:none !important;text-shadow:none !important;float:none !important;margin:0 !important}
     31.cc-btn:focus{outline:2px solid var(--cc-focus) !important;outline-offset:2px !important}
    2932.cc-btn:active{transform:scale(.97) !important}
    30 .cc-btn-accept{background:#0071e3 !important;color:#fff !important;font-size:15px !important;padding:14px 32px !important;box-shadow:0 2px 8px rgba(0,113,227,.3) !important;transition:background .15s,box-shadow .15s,transform .1s !important}
    31 .cc-btn-accept:hover{background:#005bb5 !important;box-shadow:0 4px 16px rgba(0,113,227,.35) !important;color:#fff !important}
    32 .cc-btn-decline{background:#e8e8ed !important;color:#1d1d1f !important;font-size:13px !important;padding:11px 22px !important;box-shadow:none !important;transition:background .15s,transform .1s !important}
    33 .cc-btn-decline:hover{background:#d2d2d7 !important;color:#1d1d1f !important}
    34 .cc-btn-save{background:#34c759 !important;color:#fff !important;font-size:14px !important;padding:13px 28px !important;box-shadow:0 2px 8px rgba(52,199,89,.3) !important;transition:background .15s,box-shadow .15s,transform .1s !important}
    35 .cc-btn-save:hover{background:#28a745 !important;box-shadow:0 4px 16px rgba(52,199,89,.35) !important;color:#fff !important}
    36 .cc-btn-details{background:transparent !important;color:#555 !important;padding:11px 4px;font-weight:500;font-size:13px;text-decoration:none !important;border:none !important;box-shadow:none !important;cursor:pointer;transition:color .15s !important}
    37 .cc-btn-details:hover{color:#555 !important;text-decoration:none !important;background:transparent !important;box-shadow:none !important}
    38 .cc-categories{margin:0 0 16px !important;border:1px solid #e8e8ed !important;border-radius:14px !important;overflow:hidden !important;display:none}
     33.cc-btn-accept{background:var(--cc-accent) !important;color:var(--cc-accent-text) !important;font-size:15px !important;padding:14px 32px !important;box-shadow:0 2px 8px var(--cc-accent-shadow) !important;transition:background .15s,box-shadow .15s,transform .1s !important}
     34.cc-btn-accept:hover{background:var(--cc-accent-hover) !important;box-shadow:0 4px 16px var(--cc-accent-shadow-hover) !important;color:var(--cc-accent-text) !important}
     35.cc-btn-decline{background:var(--cc-secondary-bg) !important;color:var(--cc-secondary-text) !important;font-size:13px !important;padding:11px 22px !important;box-shadow:none !important;transition:background .15s,transform .1s !important}
     36.cc-btn-decline:hover{background:var(--cc-secondary-hover) !important;color:var(--cc-secondary-text) !important}
     37.cc-btn-reject{background:var(--cc-secondary-bg) !important;color:var(--cc-secondary-text) !important;font-size:15px !important;padding:14px 32px !important;transition:background .15s,transform .1s !important}
     38.cc-btn-reject:hover{background:var(--cc-secondary-hover) !important;color:var(--cc-secondary-text) !important}
     39.cc-btn-save{background:var(--cc-save-bg) !important;color:var(--cc-save-text) !important;font-size:14px !important;padding:13px 28px !important;box-shadow:0 2px 8px var(--cc-save-shadow) !important;transition:background .15s,box-shadow .15s,transform .1s !important}
     40.cc-btn-save:hover{background:var(--cc-save-hover) !important;box-shadow:0 4px 16px var(--cc-save-shadow-hover) !important;color:var(--cc-save-text) !important}
     41.cc-btn-details{background:transparent !important;color:var(--cc-details) !important;padding:11px 4px;font-weight:500;font-size:13px;text-decoration:none !important;border:none !important;box-shadow:none !important;cursor:pointer;transition:color .15s !important}
     42.cc-btn-details:hover{color:var(--cc-details-hover) !important;text-decoration:none !important;background:transparent !important;box-shadow:none !important}
     43.cc-categories{margin:0 0 16px !important;border:1px solid var(--cc-border) !important;border-radius:14px !important;overflow:hidden !important;display:none}
    3944.cc-categories.cc-open{display:block !important}
    40 .cc-cat{padding:16px 20px !important;border-bottom:1px solid #f0f0f2 !important;display:flex !important;justify-content:space-between !important;align-items:center !important;gap:16px !important;background:none !important}
     45.cc-cat{padding:16px 20px !important;border-bottom:1px solid var(--cc-divider) !important;display:flex !important;justify-content:space-between !important;align-items:center !important;gap:16px !important;background:none !important}
    4146.cc-cat:last-child{border-bottom:none !important}
    4247.cc-cat-label{font-weight:700 !important;font-size:14px !important;line-height:1.3 !important;color:inherit !important;text-transform:none !important}
    43 .cc-cat-desc{font-size:12px !important;color:#646970 !important;margin-top:3px !important;font-weight:400 !important;line-height:1.4 !important;text-transform:none !important}
    44 .cc-always-on{font-weight:400 !important;font-size:11px !important;color:#767676 !important}
     48.cc-cat-desc{font-size:12px !important;color:var(--cc-cat-desc) !important;margin-top:3px !important;font-weight:400 !important;line-height:1.4 !important;text-transform:none !important}
     49.cc-always-on{font-weight:400 !important;font-size:11px !important;color:var(--cc-always-on) !important}
     50.cc-gpc-note{font-size:12px !important;color:var(--cc-text) !important;margin-top:6px !important;font-weight:600 !important;line-height:1.4 !important;text-transform:none !important}
    4551.cc-toggle{position:relative !important;width:46px !important;height:26px !important;flex-shrink:0 !important;display:block !important}
    4652.cc-toggle input{opacity:0 !important;width:0 !important;height:0 !important;position:absolute !important}
    47 .cc-toggle .cc-slider{position:absolute !important;inset:0 !important;background:#ccc !important;border-radius:26px !important;cursor:pointer !important;transition:background .2s !important;border:none !important;box-shadow:none !important}
    48 .cc-toggle .cc-slider:before{content:"" !important;position:absolute !important;width:20px !important;height:20px !important;left:3px !important;bottom:3px !important;background:#fff !important;border-radius:50% !important;transition:transform .2s !important;box-shadow:0 1px 3px rgba(0,0,0,.15) !important}
    49 .cc-toggle input:checked+.cc-slider{background:#0071e3 !important}
     53.cc-toggle .cc-slider{position:absolute !important;inset:0 !important;background:var(--cc-toggle-off) !important;border-radius:26px !important;cursor:pointer !important;transition:background .2s !important;border:none !important;box-shadow:none !important}
     54.cc-toggle .cc-slider:before{content:"" !important;position:absolute !important;width:20px !important;height:20px !important;left:3px !important;bottom:3px !important;background:var(--cc-knob) !important;border-radius:50% !important;transition:transform .2s !important;box-shadow:0 1px 3px rgba(0,0,0,.15) !important}
     55.cc-toggle input:checked+.cc-slider{background:var(--cc-toggle-on) !important}
    5056.cc-toggle input:checked+.cc-slider:before{transform:translateX(20px) !important}
    51 .cc-toggle input:disabled+.cc-slider{background:#0071e3 !important;opacity:.5 !important;cursor:default !important}
    52 .cc-toggle input:focus-visible+.cc-slider{outline:2px solid #0071e3 !important;outline-offset:2px !important}
    53 .cc-powered{text-align:center !important;padding:0 0 16px !important;font-size:11px !important;color:#999 !important;font-weight:400 !important;background:none !important;border:none !important;margin:0 !important}
     57.cc-toggle input:disabled+.cc-slider{background:var(--cc-toggle-on) !important;opacity:.5 !important;cursor:default !important}
     58.cc-toggle input:focus-visible+.cc-slider{outline:2px solid var(--cc-focus) !important;outline-offset:2px !important}
     59.cc-powered{text-align:center !important;padding:0 0 16px !important;font-size:11px !important;color:var(--cc-powered) !important;font-weight:400 !important;background:none !important;border:none !important;margin:0 !important}
    5460.cc-pref-buttons{display:none !important;gap:10px;flex-wrap:wrap;align-items:center;margin-bottom:8px}
    5561.cc-pref-buttons.cc-open{display:flex !important}
    56 .cc-powered a{color:#999 !important;text-decoration:none !important;font-weight:400 !important;background:none !important;border:none !important;box-shadow:none !important}
    57 .cc-powered a:hover{color:#666 !important;text-decoration:none !important}
    58 #cc-icon{position:fixed !important;bottom:18px !important;left:18px !important;z-index:999997 !important;width:48px !important;height:48px !important;border-radius:50% !important;background:#1d1d1f !important;color:#fff !important;border:none !important;cursor:pointer !important;box-shadow:0 2px 12px rgba(0,0,0,.25) !important;display:none;align-items:center !important;justify-content:center !important;padding:0 !important;transition:transform .15s,box-shadow .15s !important;-webkit-appearance:none !important;appearance:none !important;margin:0 !important;text-decoration:none !important;outline:none !important}
    59 #cc-icon:hover{transform:scale(1.1) !important;box-shadow:0 4px 20px rgba(0,0,0,.35) !important}
    60 #cc-icon:focus-visible{outline:2px solid #0071e3 !important;outline-offset:2px !important}
     62.cc-powered a{color:var(--cc-powered) !important;text-decoration:none !important;font-weight:400 !important;background:none !important;border:none !important;box-shadow:none !important}
     63.cc-powered a:hover{color:var(--cc-powered-hover) !important;text-decoration:none !important}
     64#cc-icon{position:fixed !important;bottom:18px !important;left:18px !important;z-index:999997 !important;width:48px !important;height:48px !important;border-radius:50% !important;background:var(--cc-icon-bg) !important;color:var(--cc-icon-text) !important;border:none !important;cursor:pointer !important;box-shadow:0 2px 12px var(--cc-icon-shadow) !important;display:none;align-items:center !important;justify-content:center !important;padding:0 !important;transition:transform .15s,box-shadow .15s !important;-webkit-appearance:none !important;appearance:none !important;margin:0 !important;text-decoration:none !important;outline:none !important}
     65#cc-icon:hover{transform:scale(1.1) !important;box-shadow:0 4px 20px var(--cc-icon-shadow-hover) !important}
     66#cc-icon:focus-visible{outline:2px solid var(--cc-focus) !important;outline-offset:2px !important}
    6167#cc-icon.cc-show{display:flex !important}
    6268.cc-icon-right #cc-icon{left:auto !important;right:18px !important}
    63 #cc-ccpa-banner{position:fixed;bottom:0;left:0;right:0;z-index:999999;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;font-size:14px;line-height:1.5;color:#1d1d1f;background:#fff;box-shadow:0 -8px 40px rgba(0,0,0,.15);transform:translateY(100%);transition:transform .4s cubic-bezier(.4,0,.2,1);border-radius:20px 20px 0 0}
     69#cc-ccpa-banner{position:fixed;bottom:0;left:0;right:0;z-index:999999;font-family:var(--cc-font);font-size:14px;line-height:1.5;color:var(--cc-text);background:var(--cc-bg);box-shadow:0 -8px 40px var(--cc-banner-shadow);transform:translateY(100%);transition:transform .4s cubic-bezier(.4,0,.2,1);border-radius:var(--cc-radius) var(--cc-radius) 0 0;max-height:90vh;overflow-y:auto}
    6470#cc-ccpa-banner.cc-show{transform:translateY(0)}
    6571#cc-ccpa-banner *{box-sizing:border-box}
    6672.cc-ccpa-inner{max-width:720px;margin:0 auto;padding:20px 32px;display:flex;align-items:center;gap:20px;flex-wrap:wrap}
    6773.cc-ccpa-text{flex:1;min-width:280px}
    68 .cc-ccpa-text p{margin:0;color:#555}
     74.cc-ccpa-text p{margin:0;color:var(--cc-text-muted)}
     75.cc-ccpa-text p.cc-ccpa-gpc{margin:8px 0 0;color:var(--cc-text);font-weight:600}
     76.cc-ccpa-gpc:before{content:"\2713\00a0"}
    6977.cc-ccpa-actions{display:flex;gap:10px;align-items:center;flex-shrink:0}
    70 .cc-ccpa-link{color:#0071e3;text-decoration:underline;font-size:13px;font-weight:500;white-space:nowrap}
    71 .cc-dark #cc-banner,.cc-dark #cc-ccpa-banner{background:#1c1c1e !important;color:#f5f5f7 !important;box-shadow:0 -8px 40px rgba(0,0,0,.5) !important}
    72 .cc-dark .cc-text{color:#98989d !important}
    73 .cc-dark .cc-text a,.cc-dark .cc-ccpa-link{color:#2997ff !important}
    74 .cc-dark .cc-btn-details{color:#98989d !important}
    75 .cc-dark .cc-btn-details:hover{color:#c7c7cc !important}
    76 .cc-dark .cc-btn-decline{background:#2c2c2e !important;color:#f5f5f7 !important}
    77 .cc-dark .cc-btn-decline:hover{background:#3a3a3c !important;color:#f5f5f7 !important}
    78 .cc-dark .cc-categories{border-color:#2c2c2e !important}
    79 .cc-dark .cc-cat{border-bottom-color:#2c2c2e !important}
    80 .cc-dark .cc-cat-desc{color:#8e8e93 !important}
    81 .cc-dark .cc-always-on{color:#8e8e93 !important}
    82 .cc-dark .cc-toggle .cc-slider{background:#3a3a3c !important}
    83 .cc-dark .cc-toggle .cc-slider:before{background:#f5f5f7 !important}
    84 .cc-dark .cc-toggle input:checked+.cc-slider{background:#0a84ff !important}
    85 .cc-dark .cc-toggle input:disabled+.cc-slider{background:#0a84ff !important}
    86 .cc-dark .cc-btn-accept{background:#0a84ff !important;box-shadow:0 2px 8px rgba(10,132,255,.35) !important}
    87 .cc-dark .cc-btn-accept:hover{background:#0071e3 !important;box-shadow:0 4px 16px rgba(10,132,255,.4) !important}
    88 .cc-dark .cc-btn-save{background:#30d158 !important;box-shadow:0 2px 8px rgba(48,209,88,.3) !important}
    89 .cc-dark .cc-btn-save:hover{background:#28a745 !important;box-shadow:0 4px 16px rgba(48,209,88,.35) !important}
    90 .cc-dark .cc-ccpa-text p{color:#98989d !important}
    91 .cc-dark .cc-powered{color:#aaa !important}
    92 .cc-dark .cc-powered a{color:#aaa !important}
    93 .cc-dark .cc-powered a:hover{color:#ddd !important}
    94 .cc-dark .cc-cat-label{color:#f5f5f7 !important}
    95 .cc-dark .cc-title{color:#f5f5f7 !important}
    96 .cc-dark #cc-icon{background:#f5f5f7 !important;color:#1d1d1f !important;box-shadow:0 2px 12px rgba(255,255,255,.15) !important}
    97 .cc-dark #cc-icon:hover{box-shadow:0 4px 20px rgba(255,255,255,.25) !important}
    98 @media(max-width:480px){.cc-inner{padding:24px 20px 16px}.cc-title{font-size:18px}.cc-buttons{gap:8px}.cc-btn-accept{font-size:14px;padding:12px 24px}.cc-btn-decline{font-size:12px;padding:10px 18px}}
     78.cc-ccpa-link{color:var(--cc-link);text-decoration:underline;font-size:13px;font-weight:500;white-space:nowrap}
     79#cc-ccpa-banner .cc-btn-details{padding:11px 0}
     80.cc-ccpa-choice{display:none}
     81.cc-ccpa-choice.cc-open{display:block}
     82.cc-ccpa-choice-inner{max-width:720px;margin:0 auto;padding:20px 32px}
     83.cc-ccpa-choice-title{font-size:16px;font-weight:800;line-height:1.3;margin:0 0 6px;color:inherit}
     84.cc-ccpa-choice-text,.cc-ccpa-choice-note{margin:0 0 14px;color:var(--cc-text-muted)}
     85.cc-ccpa-choice-status{margin:0 0 14px;font-weight:600;color:var(--cc-text)}
     86.cc-ccpa-choice-note{font-size:12px;margin:10px 0 0}
     87.cc-ccpa-choice-buttons{display:flex;gap:10px;flex-wrap:wrap;align-items:center}
     88@media(max-width:480px){.cc-inner{padding:24px 20px 16px}.cc-title{font-size:18px}.cc-buttons{gap:8px}.cc-btn-accept{font-size:14px;padding:12px 24px}.cc-btn-decline{font-size:12px;padding:10px 18px}.cc-ccpa-inner{padding:16px 20px;gap:12px}.cc-ccpa-text{min-width:0;flex:1 1 100%}.cc-ccpa-actions{flex-wrap:wrap;flex-shrink:1;width:100%;justify-content:space-between}.cc-ccpa-link{white-space:normal}.cc-ccpa-choice-inner{padding:16px 20px}}
  • cookieflame/trunk/assets/js/admin.js

    r3769546 r3771537  
    1010    cc_anon_id:     { vendor: "CookieFlame",     category: "necessary",   purpose: "Anonymous ID for consent logging" },
    1111    cookieflame_dnt:       { vendor: "CookieFlame",     category: "necessary",   purpose: "Do-not-track opt-out flag" },
    12     "wp-settings-": { vendor: "WordPress",         category: "necessary",   purpose: "Admin interface preferences", prefix: true },
    13     "wp-settings-time-": { vendor: "WordPress",    category: "necessary",   purpose: "Admin settings timestamp", prefix: true },
    14     wordpress_logged_in_: { vendor: "WordPress",   category: "necessary",   purpose: "Login session", prefix: true },
    15     wordpress_sec_: { vendor: "WordPress",         category: "necessary",   purpose: "Secure login session", prefix: true },
    16     wordpress_test_cookie: { vendor: "WordPress",  category: "necessary",   purpose: "Checks cookie support" },
    17     wp_lang:        { vendor: "WordPress",         category: "necessary",   purpose: "Language preference" },
     12    "wp-settings-": { vendor: "WordPress",         category: "necessary",   purpose: "Remembers dashboard layout settings", prefix: true },
     13    "wp-settings-time-": { vendor: "WordPress",    category: "necessary",   purpose: "Records when dashboard settings were last saved", prefix: true },
     14    wordpress_logged_in_: { vendor: "WordPress",   category: "necessary",   purpose: "Keeps you logged in", prefix: true },
     15    wordpress_sec_: { vendor: "WordPress",         category: "necessary",   purpose: "Confirms your login when you open the dashboard", prefix: true },
     16    wordpress_rec_: { vendor: "WordPress",         category: "necessary",   purpose: "Recovery mode after a site error (administrators only)", prefix: true },
     17    wordpress_test_cookie: { vendor: "WordPress",  category: "necessary",   purpose: "Checks your browser accepts cookies before you log in" },
     18    wp_lang:        { vendor: "WordPress",         category: "necessary",   purpose: "Remembers the language picked on the login page" },
     19    comment_author_: { vendor: "WordPress",        category: "necessary",   purpose: "Remembers your name for comments, if you ask it to", prefix: true },
     20    comment_author_email_: { vendor: "WordPress",  category: "necessary",   purpose: "Remembers your email for comments, if you ask it to", prefix: true },
     21    comment_author_url_: { vendor: "WordPress",    category: "necessary",   purpose: "Remembers your website for comments, if you ask it to", prefix: true },
     22    "wp-postpass_": { vendor: "WordPress",         category: "necessary",   purpose: "Remembers the password entered for a protected page", prefix: true },
    1823    woocommerce_cart_hash: { vendor: "WooCommerce", category: "necessary",  purpose: "Shopping cart contents" },
    1924    woocommerce_items_in_cart: { vendor: "WooCommerce", category: "necessary", purpose: "Cart item count" },
    … …  
    181186  function identifyBuiltIn(name) {
    182187    if (KNOWN[name]) return KNOWN[name];
    183     var match = null;
     188    // Longest prefix wins (wp-settings-time- over wp-settings-).
     189    var match = null, matchLen = 0;
    184190    var keys = Object.keys(KNOWN);
    185191    for (var i = 0; i < keys.length; i++) {
    186       if (KNOWN[keys[i]].prefix && name.indexOf(keys[i]) === 0) { match = KNOWN[keys[i]]; break; }
     192      if (KNOWN[keys[i]].prefix && keys[i].length > matchLen && name.indexOf(keys[i]) === 0) { match = KNOWN[keys[i]]; matchLen = keys[i].length; }
    187193    }
    188194    return match;
    … …  
    320326  }
    321327
    322   // WordPress core vendors that are always necessary
    323   var WP_CORE_VENDORS = ["WordPress", "WooCommerce", "PHP"];
     328  // Vendors listed under WordPress Core Cookies (PHP sessions and
     329  // WooCommerce are not core, so they appear in the main table).
     330  var WP_CORE_VENDORS = ["WordPress"];
    324331  var PLUGIN_VENDOR = "CookieFlame";
    325332
    … …  
    23542361    .then(function (data) {
    23552362      if (!data.success) {
    2356         if (startBtn) { startBtn.disabled = false; startBtn.textContent = "Start Cookie Discovery"; }
     2363        if (startBtn) { startBtn.disabled = false; startBtn.textContent = "Start Cookie Discovery (uses your own browser)"; }
    23572364        return;
    23582365      }
    … …  
    24052412    })
    24062413    .catch(function (err) {
    2407       if (startBtn) { startBtn.disabled = false; startBtn.textContent = "Start Cookie Discovery"; }
     2414      if (startBtn) { startBtn.disabled = false; startBtn.textContent = "Start Cookie Discovery (uses your own browser)"; }
    24082415      cookieflameNotice("Error: " + err.message, "error");
    24092416    });
  • cookieflame/trunk/assets/js/cookieconsent.js

    r3769546 r3771537  
    11/**
    2  * CookieFlame v5.3.0
     2 * CookieFlame v5.4.0
    33 * ─────────────────────────────────────────────────────────────
    44 * Self-hosted cookie consent manager with Google Consent Mode v2,
    5  * IAB TCF v2.2, automatic script blocking, iframe facades with
     5 * automatic script blocking, iframe facades with
    66 * thumbnails, Elementor video widget support, multi-region
    77 * compliance (GDPR/CCPA), and passive cookie monitoring.
    … …  
    4040  } catch (e) {}
    4141
    42   var PLUGIN_VERSION = "5.3.0";
     42  var PLUGIN_VERSION = "5.4.0";
    4343
    4444  /* ═══════════════════════════════════════════
    … …  
    4949    cookieExpiry:   365,
    5050    privacyUrl:     "/privacy-policy",
    51     ccpaUrl:        "/privacy-policy#do-not-sell",
    5251    geoApi:         "",
    5352    logEndpoint:    "",
    5453    reconsentDays:  365,
    5554    cspSafe:        false,
    56     tcfEnabled:     true,
     55    // IAB TCF v2.2 stub. Off since v5.4.0 (it never carried a TC string);
     56    // the code is kept and can be switched on through the
     57    // cookieflame_frontend_config filter (tcfEnabled: true).
     58    tcfEnabled:     false,
    5759    cmpId:          1,
    5860    cmpVersion:     1,
    … …  
    6567    iconPosition:   "left",
    6668    consentVersion: "",
     69    // Show "Decline All" next to "Accept All" on the GDPR banner's first
     70    // screen (Settings > Show "Decline all" on the first screen).
     71    rejectFirstScreen: false,
     72    // List WordPress login cookies in the declaration under "If you log
     73    // in" (Settings > Visitor logins). Login cookies are never listed
     74    // with the others, so the declaration is the same for every visitor.
     75    visitorLogins: false,
    6776
    6877    autoBlockPatterns: [
    … …  
    122131      description:        'We use cookies to improve your experience, analyse traffic and personalise content. You can choose which categories to allow.',
    123132      acceptAllButton:    "Accept All",
     133      rejectAllButton:    "Decline All",
    124134      declineAllButton:   "Decline All",
    125135      saveButton:         "Save Preferences",
    … …  
    128138      alwaysOnLabel:      "(always on)",
    129139      ccpaTitle:          "Your Privacy Choices",
    130       ccpaDescription:    'We use cookies and similar technologies. Under California law, you have the right to opt out of the sale or sharing of your personal information.',
     140      ccpaDescription:    "We use cookies and similar technologies. Under US state privacy laws, such as California's, you have the right to opt out of the sale or sharing of your personal information.",
    131141      ccpaAcceptButton:   "OK",
    132       ccpaDnsmpiLink:     "Do Not Sell My Personal Information"
     142      ccpaDnsmpiLink:     "Do Not Sell or Share My Personal Information",
     143      ccpaGpcNotice:      "Your browser asked us not to sell or share your information. We're honouring that.",
     144      ccpaChoiceTitle:    "Do Not Sell or Share My Personal Information",
     145      ccpaChoiceText:     "Information about your visit may be used by advertising partners whose cookies run on this site. Under US state privacy laws that can count as selling or sharing. You can opt out in this browser.",
     146      ccpaOptOutButton:   "Opt out",
     147      ccpaCancelButton:   "Cancel",
     148      ccpaOptedOut:       "You've opted out of the sale or sharing of your personal information in this browser.",
     149      ccpaGpcOptedOut:    "Your browser's Global Privacy Control signal asked us not to sell or share your information, so you're opted out in this browser.",
     150      ccpaDoneButton:     "Done",
     151      ccpaOptInButton:    "Allow selling or sharing",
     152      ccpaOptInGpcNote:   "Allowing it overrides your browser's Global Privacy Control signal on this site only.",
     153      ccpaMoreSettings:   "More cookie settings",
     154      gpcPrefNote:        "Your browser's Global Privacy Control signal is on, so Marketing stays off unless you switch it on here.",
     155      changePreferences:  "Change cookie preferences",
     156      loginCookiesTitle:  "If you log in",
     157      loginCookiesIntro:  "These cookies are only set if you log in to an account on this site. They keep you signed in and remember your account settings. They're needed for logging in to work, so they don't ask for your consent."
    133158    },
    134159
    … …  
    181206    var ext = window.__ccConfig;
    182207    // shallow merge top-level primitives
    183     var keys = ["cookieName","cookieExpiry","privacyUrl","ccpaUrl","geoApi",
     208    var keys = ["cookieName","cookieExpiry","privacyUrl","geoApi",
    184209      "logEndpoint","reconsentDays","cspSafe","tcfEnabled","cmpId",
    185       "cmpVersion","vendorListVersion","storageMethod","cookieDomain","shareSubdomains","theme","showPoweredBy","iconPosition","consentVersion"];
     210      "cmpVersion","vendorListVersion","storageMethod","cookieDomain","shareSubdomains","theme","showPoweredBy","iconPosition","consentVersion",
     211      "rejectFirstScreen","visitorLogins"];
    186212    keys.forEach(function (k) {
    187213      if (ext[k] !== undefined) CONFIG[k] = ext[k];
    … …  
    207233  // visitor's browser is sending the GPC signal.
    208234  var gpcActive = !!window.__ccGPC;
     235
     236  /* GPC rules (Cal. Code Regs. tit. 11 §7025, applied in every region):
     237     - A GPC signal is a valid opt-out of selling and sharing: Marketing
     238       stays off and Google's ad signals are denied, with no extra steps.
     239     - The opt-out is saved in the consent cookie (state.gpc), so a later
     240       visit without GPC keeps it.
     241     - The visitor can opt back in only through a separate choice: the
     242       US notice's "Allow selling or sharing" button, or switching
     243       Marketing on and saving in the preferences. That sets
     244       state.gpcOptIn. OK and Accept All never opt back in. */
     245  function applyGpc(state) {
     246    if (gpcActive && state && !state.gpcOptIn) {
     247      state.marketing = false;
     248      state.gpc = 1;
     249    }
     250    return state;
     251  }
    209252
    210253  // Merged blocking rules from PHP (built-in defaults + auto-generated
    … …  
    227270    cc_anon_id:     { vendor: "CookieFlame",     category: "necessary",   purpose: "Anonymous ID for consent logging", expiry: "1 year" },
    228271    cookieflame_dnt:       { vendor: "CookieFlame",     category: "necessary",   purpose: "Do-not-track opt-out flag", expiry: "1 year" },
     272    // PHP sessions (started by some plugins and hosts, not by WordPress)
     273    PHPSESSID:      { vendor: "PHP",              category: "necessary",   purpose: "Server-side session identifier", expiry: "Session" },
    229274    // WordPress core
    230     PHPSESSID:      { vendor: "PHP",              category: "necessary",   purpose: "Server-side session identifier", expiry: "Session" },
    231     wp_settings_:   { vendor: "WordPress",         category: "necessary",   purpose: "Admin interface preferences", prefix: true, expiry: "1 year" },
    232     "wp-settings-": { vendor: "WordPress",         category: "necessary",   purpose: "Admin interface preferences", prefix: true, expiry: "1 year" },
    233     "wp-settings-time-": { vendor: "WordPress",    category: "necessary",   purpose: "Admin settings timestamp", prefix: true, expiry: "1 year" },
    234     wordpress_logged_in_: { vendor: "WordPress",   category: "necessary",   purpose: "Login session", prefix: true, expiry: "Session" },
    235     wordpress_sec_: { vendor: "WordPress",         category: "necessary",   purpose: "Secure login session", prefix: true, expiry: "Session" },
    236     wordpress_test_cookie: { vendor: "WordPress",  category: "necessary",   purpose: "Checks cookie support", expiry: "Session" },
    237     wp_lang:        { vendor: "WordPress",         category: "necessary",   purpose: "Language preference", expiry: "Session" },
     275    "wp-settings-": { vendor: "WordPress",         category: "necessary",   purpose: "Remembers dashboard layout settings", prefix: true, expiry: "1 year" },
     276    "wp-settings-time-": { vendor: "WordPress",    category: "necessary",   purpose: "Records when dashboard settings were last saved", prefix: true, expiry: "1 year" },
     277    wordpress_logged_in_: { vendor: "WordPress",   category: "necessary",   purpose: "Keeps you logged in", prefix: true, expiry: "Session, or 14 days with Remember Me" },
     278    wordpress_sec_: { vendor: "WordPress",         category: "necessary",   purpose: "Confirms your login when you open the dashboard", prefix: true, expiry: "Session, or 14 days with Remember Me" },
     279    wordpress_rec_: { vendor: "WordPress",         category: "necessary",   purpose: "Recovery mode after a site error (administrators only)", prefix: true, expiry: "1 week" },
     280    wordpress_test_cookie: { vendor: "WordPress",  category: "necessary",   purpose: "Checks your browser accepts cookies before you log in", expiry: "Session" },
     281    wp_lang:        { vendor: "WordPress",         category: "necessary",   purpose: "Remembers the language picked on the login page", expiry: "Session" },
     282    comment_author_: { vendor: "WordPress",        category: "necessary",   purpose: "Remembers your name for comments, if you ask it to", prefix: true, expiry: "1 year" },
     283    comment_author_email_: { vendor: "WordPress",  category: "necessary",   purpose: "Remembers your email for comments, if you ask it to", prefix: true, expiry: "1 year" },
     284    comment_author_url_: { vendor: "WordPress",    category: "necessary",   purpose: "Remembers your website for comments, if you ask it to", prefix: true, expiry: "1 year" },
     285    "wp-postpass_": { vendor: "WordPress",         category: "necessary",   purpose: "Remembers the password entered for a protected page", prefix: true, expiry: "10 days" },
    238286    // WooCommerce
    239287    woocommerce_cart_hash: { vendor: "WooCommerce", category: "necessary",  purpose: "Shopping cart contents", expiry: "Session" },
    … …  
    272320    if (match) return match;
    273321
    274     // Prefix match in built-in database
     322    // Prefix match in built-in database: the longest prefix wins, so
     323    // wp-settings-time-1 is not taken for a wp-settings- cookie.
     324    var matchLen = 0;
    275325    Object.keys(KNOWN_COOKIES).forEach(function (key) {
    276       if (!match && KNOWN_COOKIES[key].prefix && name.indexOf(key) === 0) {
     326      if (KNOWN_COOKIES[key].prefix && key.length > matchLen && name.indexOf(key) === 0) {
    277327        match = KNOWN_COOKIES[key];
     328        matchLen = key.length;
    278329      }
    279330    });
    280331
    281332    return match || null;
     333  }
     334
     335  // Cookies WordPress sets when someone logs in. The declaration never
     336  // lists the ones in the viewer's own browser; with visitorLogins on it
     337  // lists all of them under "If you log in" instead. wordpress_[hash] is
     338  // the login cookie on sites without HTTPS, wordpress_rec_ is admin-only.
     339  var LOGIN_COOKIE_RE = /^(?:wordpress_(?:logged_in_|sec_|rec_|test_cookie$|[0-9a-f]{32}$)|wp-settings-|wp_lang$)/;
     340
     341  function isLoginCookie(name) {
     342    return LOGIN_COOKIE_RE.test(name);
     343  }
     344
     345  function loginCookieRows() {
     346    var keep = "Session, or 14 days with Remember Me";
     347    return [
     348      { name: "wordpress_logged_in_[hash]", purpose: "Keeps you logged in", expiry: keep },
     349      { name: location.protocol === "https:" ? "wordpress_sec_[hash]" : "wordpress_[hash]", purpose: "Confirms your login when you open your account dashboard", expiry: keep },
     350      { name: "wordpress_test_cookie", purpose: "Checks your browser accepts cookies before you log in", expiry: "Session" },
     351      { name: "wp_lang", purpose: "Remembers the language you picked on the login page", expiry: "Session" },
     352      { name: "wp-settings-[user ID]", purpose: "Remembers your dashboard layout settings", expiry: "1 year" },
     353      { name: "wp-settings-time-[user ID]", purpose: "Records when your dashboard settings were last saved", expiry: "1 year" }
     354    ];
    282355  }
    283356
    … …  
    410483    // Connecticut, Montana, Oregon, Texas, Delaware, Iowa, Nebraska,
    411484    // New Hampshire, New Jersey, Minnesota, plus others pending).
    412     if (gpcActive) {
     485    if (gpcActive && !state.gpcOptIn) {
    413486      update.ad_storage = "denied";
    414487      update.ad_user_data = "denied";
    … …  
    666739  ];
    667740
     741  // Patterns match whole hostnames (the host itself or a subdomain of
     742  // it), plus an optional path prefix: "x.com" matches x.com and
     743  // www.x.com but not netflix.com or box.com.
     744  function iframePatternMatches(src, pattern) {
     745    var host, path;
     746    try {
     747      var u = new URL(src, location.href);
     748      host = u.hostname.toLowerCase();
     749      path = u.pathname.toLowerCase();
     750    } catch (e) { return false; }
     751    var slash = pattern.indexOf("/");
     752    var pHost = slash === -1 ? pattern : pattern.slice(0, slash);
     753    var pPath = slash === -1 ? "" : pattern.slice(slash);
     754    if (host !== pHost && host.slice(-pHost.length - 1) !== "." + pHost) return false;
     755    return !pPath || path.indexOf(pPath) === 0;
     756  }
     757
    668758  function getIframeBlockCategory(src) {
    669759    if (!src) return null;
    670     var lower = src.toLowerCase();
    671760    for (var i = 0; i < IFRAME_BLOCK_PATTERNS.length; i++) {
    672       if (lower.indexOf(IFRAME_BLOCK_PATTERNS[i].pattern) !== -1) {
     761      if (iframePatternMatches(src, IFRAME_BLOCK_PATTERNS[i].pattern)) {
    673762        return IFRAME_BLOCK_PATTERNS[i].category;
    674763      }
    … …  
    838927      var src = iframe.src || "";
    839928      if (!src) continue;
    840       for (var j = 0; j < IFRAME_BLOCK_PATTERNS.length; j++) {
    841         if (src.indexOf(IFRAME_BLOCK_PATTERNS[j].pattern) !== -1) {
    842           iframe.src = src;
    843           break;
    844         }
    845       }
     929      if (getIframeBlockCategory(src)) iframe.src = src;
    846930    }
    847931  }
    … …  
    896980  function saveState(state, action) {
    897981    var prev = consentState;
     982    // A click on an embed's placeholder loads that embed now; the GPC
     983    // opt-out is saved again on the next page view (boot()).
     984    if (!(action && action.indexOf("facade_accept_") === 0)) applyGpc(state);
    898985    consentState = state;
    899986    storageSet(CONFIG.cookieName, JSON.stringify(state), CONFIG.cookieExpiry);
    … …  
    9581045      if (!s || typeof s !== "object" || Array.isArray(s)) return null;
    9591046      if (!s.ts || isNaN(new Date(s.ts).getTime())) return null;
    960       // Check re-consent expiry
     1047      // Check re-consent expiry. A period below 1 day means the default.
     1048      var days = Number(CONFIG.reconsentDays);
     1049      if (!(days >= 1)) days = 365;
    9611050      var age = (Date.now() - new Date(s.ts).getTime()) / 864e5;
    962       if (age > CONFIG.reconsentDays) return null;
     1051      if (age > days) return null;
    9631052      // Consent version check: if the site owner has clicked
    9641053      // "Invalidate All Consent", the version string will not
    … …  
    11831272      var alwaysOn = t.required
    11841273        ? ' <span class="cc-always-on">' + escHtml(txt.alwaysOnLabel) + '</span>' : "";
     1274      var gpcNote = gpcActive && t.id === "marketing"
     1275        ? '<div class="cc-gpc-note">' + escHtml(txt.gpcPrefNote) + '</div>' : "";
    11851276      return '<div class="cc-cat">' +
    11861277        '<div><div class="cc-cat-label">' + escHtml(t.name) + alwaysOn + '</div>' +
    1187         '<div class="cc-cat-desc">' + escHtml(t.description || "") + '</div></div>' +
     1278        '<div class="cc-cat-desc">' + escHtml(t.description || "") + '</div>' + gpcNote + '</div>' +
    11881279        '<label class="cc-toggle"><input type="checkbox" id="cc-cb-' + escHtml(t.id) + '"' +
    11891280          ' aria-label="' + escHtml(t.name) + '"' +
    … …  
    12001291        '<div class="cc-buttons">' +
    12011292          '<button class="cc-btn cc-btn-accept" id="cc-accept">' + escHtml(txt.acceptAllButton) + '</button>' +
     1293          (CONFIG.rejectFirstScreen ? '<button class="cc-btn cc-btn-reject" id="cc-reject">' + escHtml(txt.rejectAllButton) + '</button>' : '') +
    12021294          '<button class="cc-btn cc-btn-details" id="cc-details" aria-expanded="false">' + escHtml(txt.showDetailsButton) + '</button>' +
    12031295        '</div>' +
    … …  
    12211313    document.getElementById("cc-accept").addEventListener("click", acceptAll);
    12221314    document.getElementById("cc-decline").addEventListener("click", declineAll);
     1315    var rejectBtn = document.getElementById("cc-reject");
     1316    if (rejectBtn) rejectBtn.addEventListener("click", declineAll);
    12231317    document.getElementById("cc-save").addEventListener("click", savePrefs);
    12241318    document.getElementById("cc-details").addEventListener("click", toggleDetails);
    12251319    icon.addEventListener("click", function () { showBanner("gdpr"); });
    12261320    // No overlay click handler — banner must be dismissed via buttons only
     1321    emitRendered("gdpr", banner);
     1322  }
     1323
     1324  // cc:banner-rendered fires once each banner's markup is in the page
     1325  // (before it is shown), so other code can adjust it.
     1326  // detail: { banner: "gdpr" | "ccpa", element, state }
     1327  function emitRendered(type, el) {
     1328    emit("cc:banner-rendered", { banner: type, element: el, state: getPublicState() });
    12271329  }
    12281330
    … …  
    12311333  ═══════════════════════════════════════════ */
    12321334  function buildCcpaBanner() {
     1335    if (document.getElementById("cc-ccpa-banner")) return;
    12331336    var txt = CONFIG.text;
    12341337    var banner = document.createElement("div");
    … …  
    12401343
    12411344    banner.innerHTML =
    1242       '<div class="cc-ccpa-inner">' +
    1243         '<div class="cc-ccpa-text"><p>' + escHtml(txt.ccpaDescription) + '</p></div>' +
     1345      '<div class="cc-ccpa-inner" id="cc-ccpa-notice">' +
     1346        '<div class="cc-ccpa-text"><p>' + escHtml(txt.ccpaDescription) + '</p>' +
     1347          (gpcActive ? '<p class="cc-ccpa-gpc">' + escHtml(txt.ccpaGpcNotice) + '</p>' : '') +
     1348        '</div>' +
    12441349        '<div class="cc-ccpa-actions">' +
    1245           '<a href="' + escHtml(CONFIG.ccpaUrl) + '" class="cc-ccpa-link">' + escHtml(txt.ccpaDnsmpiLink) + '</a>' +
     1350          '<a href="#" class="cc-ccpa-link" id="cc-ccpa-dns" role="button">' + escHtml(txt.ccpaDnsmpiLink) + '</a>' +
    12461351          '<button class="cc-btn cc-btn-accept" id="cc-ccpa-ok">' + escHtml(txt.ccpaAcceptButton) + '</button>' +
    12471352        '</div>' +
    1248       '</div>';
     1353      '</div>' +
     1354      '<div class="cc-ccpa-choice" id="cc-ccpa-choice"></div>' +
     1355      (CONFIG.showPoweredBy ? '<div class="cc-powered"><a href="https://www.cookieflame.com" target="_blank" rel="noopener">Powered by CookieFlame</a></div>' : '');
    12491356
    12501357    document.body.appendChild(banner);
    12511358
     1359    // OK only closes the notice. It never changes the visitor's choices:
     1360    // US visitors start with everything allowed, and a GPC visitor's
     1361    // opt-out was saved before the notice appeared.
    12521362    document.getElementById("cc-ccpa-ok").addEventListener("click", function () {
    1253       var s = buildStateAllGranted();
    1254       saveState(s, "ccpa_acknowledge");
    1255       banner.classList.remove("cc-show");
     1363      if (consentState) logConsent("ccpa_acknowledge", consentState);
     1364      closeCcpaBanner();
     1365    });
     1366    document.getElementById("cc-ccpa-dns").addEventListener("click", function (e) {
     1367      e.preventDefault();
     1368      showCcpaChoice(true);
     1369    });
     1370    emitRendered("ccpa", banner);
     1371  }
     1372
     1373  // The "Do Not Sell or Share" choice, drawn from the current state.
     1374  function renderCcpaChoice() {
     1375    var el = document.getElementById("cc-ccpa-choice");
     1376    if (!el) return;
     1377    var txt = CONFIG.text;
     1378    var optedOut = !(consentState && consentState.marketing);
     1379    var html = '<div class="cc-ccpa-choice-inner">' +
     1380      '<p class="cc-ccpa-choice-title">' + escHtml(txt.ccpaChoiceTitle) + '</p>';
     1381    if (optedOut) {
     1382      html += '<p class="cc-ccpa-choice-status">' + escHtml(gpcActive ? txt.ccpaGpcOptedOut : txt.ccpaOptedOut) + '</p>' +
     1383        '<div class="cc-ccpa-choice-buttons">' +
     1384          '<button class="cc-btn cc-btn-accept" id="cc-ccpa-done">' + escHtml(txt.ccpaDoneButton) + '</button>' +
     1385          '<button class="cc-btn cc-btn-decline" id="cc-ccpa-optin">' + escHtml(txt.ccpaOptInButton) + '</button>' +
     1386        '</div>' +
     1387        (gpcActive ? '<p class="cc-ccpa-choice-note">' + escHtml(txt.ccpaOptInGpcNote) + '</p>' : '');
     1388    } else {
     1389      html += '<p class="cc-ccpa-choice-text">' + escHtml(txt.ccpaChoiceText) + '</p>' +
     1390        '<div class="cc-ccpa-choice-buttons">' +
     1391          '<button class="cc-btn cc-btn-accept" id="cc-ccpa-optout">' + escHtml(txt.ccpaOptOutButton) + '</button>' +
     1392          '<button class="cc-btn cc-btn-decline" id="cc-ccpa-cancel">' + escHtml(txt.ccpaCancelButton) + '</button>' +
     1393        '</div>';
     1394    }
     1395    html += '<button class="cc-btn cc-btn-details" id="cc-ccpa-more">' + escHtml(txt.ccpaMoreSettings) + '</button></div>';
     1396    el.innerHTML = html;
     1397
     1398    var bind = function (id, fn) { var b = document.getElementById(id); if (b) b.addEventListener("click", fn); };
     1399    bind("cc-ccpa-done", closeCcpaBanner);
     1400    bind("cc-ccpa-cancel", closeCcpaBanner);
     1401    bind("cc-ccpa-optout", function () {
     1402      var s = copyState(consentState);
     1403      s.marketing = false;
     1404      saveState(s, "ccpa_opt_out");
     1405      renderCcpaChoice();
     1406      focusFirst("cc-ccpa-choice");
    12561407      emit("cc:update");
    12571408    });
     1409    bind("cc-ccpa-optin", function () {
     1410      var s = copyState(consentState);
     1411      s.marketing = true;
     1412      if (gpcActive) s.gpcOptIn = 1;
     1413      delete s.gpc;
     1414      saveState(s, "ccpa_opt_in");
     1415      renderCcpaChoice();
     1416      focusFirst("cc-ccpa-choice");
     1417      emit("cc:update");
     1418    });
     1419    bind("cc-ccpa-more", function () {
     1420      closeCcpaBanner();
     1421      openPreferences();
     1422    });
     1423  }
     1424
     1425  function showCcpaChoice(open) {
     1426    var notice = document.getElementById("cc-ccpa-notice");
     1427    var choice = document.getElementById("cc-ccpa-choice");
     1428    if (!notice || !choice) return;
     1429    if (open) renderCcpaChoice();
     1430    notice.style.display = open ? "none" : "";
     1431    choice.classList.toggle("cc-open", !!open);
     1432    if (open) focusFirst("cc-ccpa-choice");
     1433  }
     1434
     1435  function closeCcpaBanner() {
     1436    var b = document.getElementById("cc-ccpa-banner");
     1437    if (b) b.classList.remove("cc-show");
     1438    showCcpaChoice(false);
     1439    document.removeEventListener("keydown", _trapFocus);
     1440    if (_preFocusEl && typeof _preFocusEl.focus === "function") {
     1441      try { _preFocusEl.focus(); } catch (e) {}
     1442    }
     1443    _preFocusEl = null;
     1444  }
     1445
     1446  function focusFirst(id) {
     1447    var el = document.getElementById(id);
     1448    var b = el && el.querySelector("button");
     1449    if (b) { try { b.focus(); } catch (e) {} }
     1450  }
     1451
     1452  // A fresh state with the same choices as src (everything allowed when
     1453  // there is none yet), keeping an earlier GPC opt-in.
     1454  function copyState(src) {
     1455    var s = { ts: new Date().toISOString(), cv: CONFIG.consentVersion, cc: detectedCountry };
     1456    CONFIG.cookieTypes.forEach(function (t) { s[t.id] = src ? !!src[t.id] : true; });
     1457    if (src && src.gpcOptIn) s.gpcOptIn = 1;
     1458    return s;
    12581459  }
    12591460
    … …  
    12681469  function _trapFocus(e) {
    12691470    if (e.key !== "Tab") return;
    1270     var banner = document.getElementById("cc-banner") || document.getElementById("cc-ccpa-banner");
    1271     if (!banner || !banner.classList.contains("cc-show")) return;
     1471    var banner = document.querySelector("#cc-banner.cc-show, #cc-ccpa-banner.cc-show");
     1472    if (!banner) return;
    12721473    var focusable = [];
    12731474    var all = banner.querySelectorAll('button, a[href], input:not([disabled]), select, textarea, [tabindex]:not([tabindex="-1"])');
    … …  
    12781479      var p = el.parentElement;
    12791480      while (p && p !== banner) {
    1280         if (p.style && window.getComputedStyle(p).display === "none") { hidden = true; break; }
     1481        if (window.getComputedStyle(p).display === "none") { hidden = true; break; }
    12811482        p = p.parentElement;
    12821483      }
    … …  
    12951496  function showBanner(mode) {
    12961497    if (mode === "ccpa") {
     1498      _preFocusEl = document.activeElement;
    12971499      requestAnimationFrame(function () {
    12981500        var b = document.getElementById("cc-ccpa-banner");
    12991501        if (b) {
    13001502          b.classList.add("cc-show");
    1301           _preFocusEl = document.activeElement;
    1302           b.focus();
     1503          var choice = document.getElementById("cc-ccpa-choice");
     1504          if (choice && choice.classList.contains("cc-open")) focusFirst("cc-ccpa-choice");
     1505          else b.focus();
    13031506          document.addEventListener("keydown", _trapFocus);
    13041507        }
    … …  
    13071510    }
    13081511    _preFocusEl = document.activeElement;
     1512    // "Decline All" on the first screen is for the GDPR banner only, never
     1513    // when the preferences are opened by a US or other visitor.
     1514    var rj = document.getElementById("cc-reject");
     1515    if (rj) {
     1516      if (complianceMode === "gdpr") rj.style.removeProperty("display");
     1517      else rj.style.setProperty("display", "none", "important"); // beats .cc-btn's !important
     1518    }
    13091519    requestAnimationFrame(function () {
    13101520      var ov = document.getElementById("cc-overlay");
    … …  
    13541564  ═══════════════════════════════════════════ */
    13551565  function acceptAll() {
    1356     saveState(buildStateAllGranted(), "accept_all");
     1566    var s = buildStateAllGranted();
     1567    if (consentState && consentState.gpcOptIn) s.gpcOptIn = 1;
     1568    saveState(s, "accept_all");
    13571569    closeBanner();
    13581570    emit("cc:accept-all"); emit("cc:update");
    … …  
    13661578
    13671579  function savePrefs() {
    1368     saveState(buildStateFromCheckboxes(), "save_preferences");
     1580    var s = buildStateFromCheckboxes();
     1581    // Switching Marketing on here is the separate choice that opts a GPC
     1582    // visitor back in.
     1583    if (s.marketing && (gpcActive || (consentState && consentState.gpcOptIn))) s.gpcOptIn = 1;
     1584    saveState(s, "save_preferences");
    13691585    closeBanner();
    13701586    emit("cc:save"); emit("cc:update");
    … …  
    13831599    }
    13841600  }
     1601
     1602  /* ═══════════════════════════════════════════
     1603     COOKIE SETTINGS LINK
     1604     [cookieflame_settings_link], the declaration's button and any
     1605     element with the class cc-open-settings reopen the visitor's
     1606     choices: the "Do Not Sell or Share" choice for US visitors, the
     1607     preferences for everyone else.
     1608  ═══════════════════════════════════════════ */
     1609  function openPreferences() {
     1610    if (!document.getElementById("cc-banner")) {
     1611      applyThemeClasses();
     1612      buildGdprBanner();
     1613      hideAllBanners();
     1614      if (consentState) syncCheckboxes(consentState);
     1615    }
     1616    showBanner("gdpr");
     1617  }
     1618
     1619  function openUsChoices() {
     1620    applyThemeClasses();
     1621    buildCcpaBanner();
     1622    showBanner("ccpa");
     1623    showCcpaChoice(true);
     1624  }
     1625
     1626  function openSettings() {
     1627    if (complianceMode === "ccpa") openUsChoices();
     1628    else openPreferences();
     1629  }
     1630
     1631  document.addEventListener("click", function (e) {
     1632    var t = e.target && e.target.closest ? e.target.closest(".cc-open-settings") : null;
     1633    if (!t) return;
     1634    e.preventDefault();
     1635    openSettings();
     1636  });
    13851637
    13861638  /* ═══════════════════════════════════════════
    … …  
    14351687    getComplianceMode:  function () { return complianceMode; },
    14361688    getCountry:         function () { return detectedCountry; },
    1437     show:               function () { showBanner("gdpr"); }, // always open preference manager
     1689    show:               openPreferences, // always open preference manager
     1690    showSettings:       openSettings,    // what the Cookie settings link opens
    14381691    acceptAll:          acceptAll,
    14391692    declineAll:         declineAll,
    … …  
    14611714    // Add browser cookies
    14621715    var browserCookies = document.cookie.split(";").map(function (c) { return c.trim().split("=")[0]; }).filter(Boolean);
     1716    browserCookies = browserCookies.filter(function (n) { return !isLoginCookie(n); });
    14631717    for (var b = 0; b < browserCookies.length; b++) {
    14641718      var bInfo = identifyCookie(browserCookies[b]);
    … …  
    14731727    for (var c = 0; c < customCookies.length; c++) {
    14741728      var cc = customCookies[c];
    1475       if (cc.name && cc.vendor && cc.category && cc.category !== "unclassified" && !allCookies[cc.name]) {
     1729      if (cc.name && cc.vendor && cc.category && cc.category !== "unclassified" && !allCookies[cc.name] && !isLoginCookie(cc.name)) {
    14761730        allCookies[cc.name] = {
    14771731          name: cc.name, vendor: cc.vendor, category: cc.category,
    … …  
    15621816    }
    15631817
     1818    if (CONFIG.visitorLogins) {
     1819      var loginRows = loginCookieRows();
     1820      html += '<div class="cc-declaration-login">';
     1821      html += '<p><b>' + escHtml(CONFIG.text.loginCookiesTitle) + '</b></p>';
     1822      html += '<p>' + escHtml(CONFIG.text.loginCookiesIntro) + '</p>';
     1823      html += '<table><thead><tr>';
     1824      html += '<th>Name</th><th>Provider</th><th>Purpose</th><th>Expiry</th>';
     1825      html += '</tr></thead><tbody>';
     1826      for (var li = 0; li < loginRows.length; li++) {
     1827        html += '<tr>';
     1828        html += '<td><code>' + escHtml(loginRows[li].name) + '</code></td>';
     1829        html += '<td>WordPress</td>';
     1830        html += '<td>' + escHtml(loginRows[li].purpose) + '</td>';
     1831        html += '<td>' + escHtml(loginRows[li].expiry) + '</td>';
     1832        html += '</tr>';
     1833      }
     1834      html += '</tbody></table></div>';
     1835    }
     1836
     1837    html += '<p><button type="button" class="cc-open-settings cc-declaration-change">' + escHtml(CONFIG.text.changePreferences) + '</button></p>';
    15641838    html += '</div>';
    15651839    el.innerHTML = html;
    … …  
    15811855  var preloadedConsent = loadState();
    15821856  if (preloadedConsent) {
     1857    // Marketing scripts created before boot() runs stay blocked for GPC.
     1858    if (gpcActive && !preloadedConsent.gpcOptIn) preloadedConsent.marketing = false;
    15831859    consentState = preloadedConsent;
    15841860    updateConsentMode(preloadedConsent);
    … …  
    16491925    var existing = loadState();
    16501926
     1927    var gpcNewOptOut = false;
    16511928    if (existing) {
    1652       // GPC override: when Global Privacy Control is active, suppress
    1653       // marketing consent in the effective state. The stored cookie
    1654       // is not modified — if the visitor later disables GPC, their
    1655       // original consent choice is restored. This ensures marketing
    1656       // scripts and ad signals stay blocked while GPC is active.
    1657       if (gpcActive) existing.marketing = false;
     1929      // GPC: a signal that arrives after the visitor's choice is
     1930      // processed as an opt-out and saved, so it holds on later visits
     1931      // without GPC (§7025(c)(5)). An earlier opt-in made while GPC was
     1932      // on (gpcOptIn) is kept.
     1933      if (gpcActive && !existing.gpcOptIn && (existing.marketing || !existing.gpc)) {
     1934        gpcNewOptOut = !!existing.marketing;
     1935        applyGpc(existing);
     1936        storageSet(CONFIG.cookieName, JSON.stringify(existing), CONFIG.cookieExpiry);
     1937      }
    16581938      consentState = existing;
    16591939      updateConsentMode(existing);
    … …  
    16811961          if (ic) ic.classList.add("cc-show");
    16821962        }
     1963        if (gpcNewOptOut) logConsent("gpc_optout", existing);
    16831964        emit("cc:ready");
    16841965        renderDeclaration();
    … …  
    16911972            if (ic) ic.classList.add("cc-show");
    16921973          }
     1974          if (gpcNewOptOut) logConsent("gpc_optout", existing);
    16931975          emit("cc:ready");
    16941976          renderDeclaration();
    … …  
    17141996        startIframeObserver();
    17151997        if (gpcActive) {
    1716           // GPC active: grant analytics and preferences but deny marketing.
    1717           // This fulfills the CCPA/CPRA requirement to honor Global Privacy
    1718           // Control as a valid opt-out of the sale or sharing of personal
    1719           // information. Fines have been issued for ignoring GPC (Sephora
    1720           // $1.2M, DoorDash $375K, Tractor Supply $1.3M).
    1721           var gpcState = buildStateAllGranted();
    1722           gpcState.marketing = false;
    1723           saveState(gpcState, "ccpa_gpc_optout");
     1998          // GPC active: allow analytics and preferences but opt out of
     1999          // selling and sharing (Marketing off), saved before the notice
     2000          // appears. The notice says the signal was honoured.
     2001          saveState(applyGpc(buildStateAllGranted()), "ccpa_gpc_optout");
    17242002        } else {
    17252003          saveState(buildStateAllGranted(), "ccpa_auto_grant");
  • cookieflame/trunk/assets/js/cookieconsent.min.js

    r3769546 r3771537  
    11/**
    2  * CookieFlame v5.3.0
     2 * CookieFlame v5.4.0
    33 * ─────────────────────────────────────────────────────────────
    44 * Self-hosted cookie consent manager with Google Consent Mode v2,
    5  * IAB TCF v2.2, automatic script blocking, iframe facades with
     5 * automatic script blocking, iframe facades with
    66 * thumbnails, Elementor video widget support, multi-region
    77 * compliance (GDPR/CCPA), and passive cookie monitoring.
    … …  
    1717 *   • Timezone-based geo heuristic with server-side fallback
    1818 */
    19 !function(){"use strict";var e={};try{for(var t=document.cookie.split(";"),n=0;n<t.length;n++){var o=t[n].trim().split("=")[0];o&&(e[o]=!0)}}catch(e){}var c="5.3.0",r={cookieName:"cc_consent",cookieExpiry:365,privacyUrl:"/privacy-policy",ccpaUrl:"/privacy-policy#do-not-sell",geoApi:"",logEndpoint:"",reconsentDays:365,cspSafe:!1,tcfEnabled:!0,cmpId:1,cmpVersion:1,vendorListVersion:2,storageMethod:"cookie",cookieDomain:"",shareSubdomains:!1,theme:"light",showPoweredBy:!1,iconPosition:"left",consentVersion:"",autoBlockPatterns:[{pattern:"googletagmanager.com/gtag",category:"analytics"},{pattern:"google-analytics.com",category:"analytics"},{pattern:"analytics.google.com",category:"analytics"},{pattern:"hotjar.com",category:"analytics"},{pattern:"mixpanel.com",category:"analytics"},{pattern:"segment.com/analytics",category:"analytics"},{pattern:"segment.io",category:"analytics"},{pattern:"clarity.ms",category:"analytics"},{pattern:"plausible.io",category:"analytics"},{pattern:"js.hs-analytics.net",category:"analytics"},{pattern:"connect.facebook.net",category:"marketing"},{pattern:"snap.licdn.com",category:"marketing"},{pattern:"ads.linkedin.com",category:"marketing"},{pattern:"googleads.g.doubleclick.net",category:"marketing"},{pattern:"googlesyndication.com",category:"marketing"},{pattern:"googleadservices.com",category:"marketing"},{pattern:"ads-twitter.com",category:"marketing"},{pattern:"analytics.tiktok.com",category:"marketing"},{pattern:"pintrk",category:"marketing"},{pattern:"s.pinimg.com/ct",category:"marketing"},{pattern:"widget.intercom.io",category:"preferences"},{pattern:"js.intercomcdn.com",category:"preferences"}],regionRules:{gdpr:["AT","BE","BG","CY","CZ","DE","DK","EE","ES","FI","FR","GR","HR","HU","IE","IT","LT","LU","LV","MT","NL","PL","PT","RO","SE","SI","SK","IS","LI","NO","GB","GG","JE","IM","CH","BR","ZA"],ccpa:["US"]},text:{title:"We value your privacy",description:"We use cookies to improve your experience, analyse traffic and personalise content. You can choose which categories to allow.",acceptAllButton:"Accept All",declineAllButton:"Decline All",saveButton:"Save Preferences",showDetailsButton:"Manage preferences",hideDetailsButton:"Hide preferences",alwaysOnLabel:"(always on)",ccpaTitle:"Your Privacy Choices",ccpaDescription:"We use cookies and similar technologies. Under California law, you have the right to opt out of the sale or sharing of your personal information.",ccpaAcceptButton:"OK",ccpaDnsmpiLink:"Do Not Sell My Personal Information"},cookieTypes:[{id:"necessary",name:"Necessary",description:"Required for the site to function. Cannot be disabled.",required:!0,consentMode:["security_storage","functionality_storage"],tcfPurposes:[1],onAccept:function(){}},{id:"preferences",name:"Preferences",description:"Remember your settings and personalisation choices.",required:!1,defaultValue:!1,consentMode:["functionality_storage","personalization_storage"],tcfPurposes:[2,3],onAccept:function(){},onReject:function(){}},{id:"analytics",name:"Analytics",description:"Help us understand how visitors use the site.",required:!1,defaultValue:!1,consentMode:["analytics_storage"],tcfPurposes:[4,5,6],onAccept:function(){},onReject:function(){}},{id:"marketing",name:"Marketing",description:"Used to deliver relevant ads and track campaigns.",required:!1,defaultValue:!1,consentMode:["ad_storage","ad_user_data","ad_personalization"],tcfPurposes:[7,8,9,10],onAccept:function(){},onReject:function(){}}]};if(window.__ccConfig&&"object"==typeof window.__ccConfig){var a=window.__ccConfig;["cookieName","cookieExpiry","privacyUrl","ccpaUrl","geoApi","logEndpoint","reconsentDays","cspSafe","tcfEnabled","cmpId","cmpVersion","vendorListVersion","storageMethod","cookieDomain","shareSubdomains","theme","showPoweredBy","iconPosition","consentVersion"].forEach(function(e){void 0!==a[e]&&(r[e]=a[e])}),a.text&&"object"==typeof a.text&&Object.keys(a.text).forEach(function(e){r.text[e]=a.text[e]}),Array.isArray(a.cookieTypes)&&(r.cookieTypes=a.cookieTypes),Array.isArray(a.autoBlockPatterns)&&(r.autoBlockPatterns=a.autoBlockPatterns),a.regionRules&&(r.regionRules=a.regionRules)}window.__ccConsentVersion&&(r.consentVersion=window.__ccConsentVersion);var i=!!window.__ccGPC;window.__ccBlockRules&&Array.isArray(window.__ccBlockRules)&&window.__ccBlockRules.length&&(r.autoBlockPatterns=window.__ccBlockRules);var s={cc_consent:{vendor:"CookieFlame",category:"necessary",purpose:"Stores your consent preferences",expiry:"1 year"},cc_anon_id:{vendor:"CookieFlame",category:"necessary",purpose:"Anonymous ID for consent logging",expiry:"1 year"},cookieflame_dnt:{vendor:"CookieFlame",category:"necessary",purpose:"Do-not-track opt-out flag",expiry:"1 year"},PHPSESSID:{vendor:"PHP",category:"necessary",purpose:"Server-side session identifier",expiry:"Session"},wp_settings_:{vendor:"WordPress",category:"necessary",purpose:"Admin interface preferences",prefix:!0,expiry:"1 year"},"wp-settings-":{vendor:"WordPress",category:"necessary",purpose:"Admin interface preferences",prefix:!0,expiry:"1 year"},"wp-settings-time-":{vendor:"WordPress",category:"necessary",purpose:"Admin settings timestamp",prefix:!0,expiry:"1 year"},wordpress_logged_in_:{vendor:"WordPress",category:"necessary",purpose:"Login session",prefix:!0,expiry:"Session"},wordpress_sec_:{vendor:"WordPress",category:"necessary",purpose:"Secure login session",prefix:!0,expiry:"Session"},wordpress_test_cookie:{vendor:"WordPress",category:"necessary",purpose:"Checks cookie support",expiry:"Session"},wp_lang:{vendor:"WordPress",category:"necessary",purpose:"Language preference",expiry:"Session"},woocommerce_cart_hash:{vendor:"WooCommerce",category:"necessary",purpose:"Shopping cart contents",expiry:"Session"},woocommerce_items_in_cart:{vendor:"WooCommerce",category:"necessary",purpose:"Cart item count",expiry:"Session"},wp_woocommerce_session_:{vendor:"WooCommerce",category:"necessary",purpose:"Shopping session",prefix:!0,expiry:"2 days"},_ga:{vendor:"Google Analytics",category:"analytics",purpose:"Distinguishes visitors",expiry:"2 years"},_ga_:{vendor:"Google Analytics",category:"analytics",purpose:"Stores session state",prefix:!0,expiry:"2 years"},_gid:{vendor:"Google Analytics",category:"analytics",purpose:"Distinguishes visitors (24h)",expiry:"24 hours"},_gat:{vendor:"Google Analytics",category:"analytics",purpose:"Throttles request rate",expiry:"1 minute"},wp_wk_ga_untrack_:{vendor:"Google Analytics",category:"analytics",purpose:"GA opt-out flag",prefix:!0,expiry:"Persistent"},_gcl_au:{vendor:"Google Ads",category:"marketing",purpose:"Conversion linker",expiry:"90 days"},_gcl_aw:{vendor:"Google Ads",category:"marketing",purpose:"Stores ad click info",expiry:"90 days"},_gac_:{vendor:"Google Ads",category:"marketing",purpose:"Campaign information",prefix:!0,expiry:"90 days"},IDE:{vendor:"Google DoubleClick",category:"marketing",purpose:"Ad serving and frequency",expiry:"1 year"},test_cookie:{vendor:"Google DoubleClick",category:"marketing",purpose:"Checks cookie support",expiry:"15 minutes"},NID:{vendor:"Google",category:"preferences",purpose:"Stores preferences",expiry:"6 months"}},d=window.__ccCustomCookies||[];function u(e){for(var t=0;t<d.length;t++){var n=d[t];if("exact"===n.match&&e===n.name)return n;if("prefix"===n.match&&0===e.indexOf(n.name))return n;if("contains"===n.match&&-1!==e.indexOf(n.name))return n}var o=s[e];return o||(Object.keys(s).forEach(function(t){!o&&s[t].prefix&&0===e.indexOf(t)&&(o=s[t])}),o||null)}var l=null,p="none",f=null,m=null,g=!1,y=0;function v(e){return"string"!=typeof e&&(e=String(e)),e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;")}function h(e){if("localStorage"===r.storageMethod)try{return localStorage.getItem(e)}catch(e){return null}var t=document.cookie.match(new RegExp("(?:^|;\\s*)"+e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")+"=([^;]*)"));if(!t)return null;try{return decodeURIComponent(t[1])}catch(e){return null}}function w(e,t,n){if("localStorage"!==r.storageMethod){var o=new Date;o.setTime(o.getTime()+864e5*n);var c=e+"="+encodeURIComponent(t)+";expires="+o.toUTCString()+";path=/;SameSite=Lax;Secure";r.cookieDomain&&(c+=";domain="+r.cookieDomain),document.cookie=c}else try{localStorage.setItem(e,t)}catch(e){}}function b(){if(m)return m;var e=h("cc_anon_id");return e?(m=e,e):(w("cc_anon_id",m="cc_"+function(){if("undefined"!=typeof crypto&&"function"==typeof crypto.randomUUID)try{return crypto.randomUUID()}catch(e){}return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,function(e){var t=16*Math.random()|0;return("x"===e?t:3&t|8).toString(16)})}(),730),m)}function k(){var e={timestamp:l?l.ts:null,complianceMode:p,country:f,gpc:i,consentVersion:r.consentVersion};return r.cookieTypes.forEach(function(t){e[t.id]=l?!!l[t.id]:!!t.required}),e}function _(e,t){window.dispatchEvent(new CustomEvent(e,{detail:t||k()}))}function x(){window.dataLayer=window.dataLayer||[],window.dataLayer.push(arguments)}function E(e){var t={};r.cookieTypes.forEach(function(n){n.consentMode&&n.consentMode.forEach(function(o){t[o]=e[n.id]?"granted":"denied"})}),i&&(t.ad_storage="denied",t.ad_user_data="denied",t.ad_personalization="denied"),x("consent","update",t)}var A=new Map,S=!1;function L(e){var t={};return r.cookieTypes.forEach(function(n){n.tcfPurposes&&n.tcfPurposes.forEach(function(o){t[o]=!!e[n.id]})}),{tcString:"",tcfPolicyVersion:4,cmpId:r.cmpId,cmpVersion:r.cmpVersion,gdprApplies:"gdpr"===p,eventStatus:"useractioncomplete",cmpStatus:"loaded",listenerId:void 0,isServiceSpecific:!0,useNonStandardTexts:!1,publisherCC:"GB",purposeOneTreatment:!1,purpose:{consents:t,legitimateInterests:{}},vendor:{consents:{},legitimateInterests:{}},specialFeatureOptins:{},publisher:{consents:{},legitimateInterests:{},customPurpose:{consents:{},legitimateInterests:{}},restrictions:{}}}}function C(e,t,n,o){if("function"==typeof n)if("addEventListener"===e){var c=++y;if(A.set(c,n),l){var a=L(l);a.listenerId=c,n(a,!0)}}else"removeEventListener"===e?n({listenerId:o},A.delete(o)):"getTCData"===e?n(l?L(l):null,!!l):"ping"===e?n({gdprApplies:"gdpr"===p,cmpLoaded:S,cmpStatus:S?"loaded":"stub",displayStatus:"hidden",apiVersion:"2.2",cmpVersion:r.cmpVersion,cmpId:r.cmpId,gvlVersion:r.vendorListVersion,tcfPolicyVersion:4},!0):n(null,!1)}function B(e){A.forEach(function(t,n){var o=L(e);o.listenerId=n;try{t(o,!0)}catch(e){}})}function I(e){if(!e)return null;for(var t=e.toLowerCase(),n=0;n<r.autoBlockPatterns.length;n++){var o=r.autoBlockPatterns[n];if(o.pattern instanceof RegExp){if(o.pattern.test(t))return o.category}else if(-1!==t.indexOf(o.pattern.toLowerCase()))return o.category}return null}function T(e){document.querySelectorAll('script[type="text/plain"][data-cc]').forEach(function(t){var n=(t.getAttribute("data-cc")||"").toLowerCase();if(e[n]&&!t.getAttribute("data-cc-activated")){t.setAttribute("data-cc-activated","1");var o=document.createElement("script");o.isWPRocket=!0,Array.from(t.attributes).forEach(function(e){"type"!==e.name&&"data-cc"!==e.name&&"data-cc-activated"!==e.name&&"data-cc-auto"!==e.name&&o.setAttribute(e.name,e.value)}),!t.src&&t.textContent&&(o.textContent=t.textContent),t.parentNode.insertBefore(o,t.nextSibling),t.parentNode.removeChild(t)}}),["img","iframe","link"].forEach(function(t){document.querySelectorAll(t+"[data-cc][data-cc-src]").forEach(function(n){var o=(n.getAttribute("data-cc")||"").toLowerCase();e[o]&&!n.getAttribute("data-cc-activated")&&(n.setAttribute("data-cc-activated","1"),"link"===t?n.href=n.getAttribute("data-cc-src"):n.src=n.getAttribute("data-cc-src"))})})}var O=[{pattern:"youtube.com",category:"marketing"},{pattern:"youtube-nocookie.com",category:"marketing"},{pattern:"youtu.be",category:"marketing"},{pattern:"player.vimeo.com",category:"marketing"},{pattern:"vimeo.com",category:"marketing"},{pattern:"dailymotion.com",category:"marketing"},{pattern:"open.spotify.com",category:"preferences"},{pattern:"soundcloud.com",category:"preferences"},{pattern:"google.com/maps",category:"preferences"},{pattern:"maps.google.com",category:"preferences"},{pattern:"facebook.com/plugins",category:"marketing"},{pattern:"instagram.com",category:"marketing"},{pattern:"tiktok.com/embed",category:"marketing"},{pattern:"twitter.com",category:"marketing"},{pattern:"x.com",category:"marketing"}];function D(){return!(!document.body||!(document.body.classList.contains("elementor-editor-active")||document.body.classList.contains("fl-builder-edit")||document.body.classList.contains("et-fb-root-ancestor")))}var P=[],M=0,R=null,q=!1;function V(){if(R&&P.length){var e=P;P=[];try{R(e)}catch(e){}}}function N(){0!==P.length&&(2!==M?1!==M&&3!==M&&(M=1,function(){var e=window.__ccConfig&&window.__ccConfig.facadesUrl||null;if(!e)return void(M=3);var t=document.createElement("script");t.src=e,t.async=!0,t.onload=function(){M=2,V()},t.onerror=function(){M=3},(document.head||document.documentElement).appendChild(t)}()):V())}function U(e){var t=e.src||e.getAttribute("data-src")||"";if(t&&!e.getAttribute("data-cc-facade")&&!D()){var n=function(e){if(!e)return null;for(var t=e.toLowerCase(),n=0;n<O.length;n++)if(-1!==t.indexOf(O[n].pattern))return O[n].category;return null}(t);if(n){var o=l||Z();o&&o[n]||(e.setAttribute("data-cc-facade","1"),e.setAttribute("data-cc-iframe-cat",n),e.setAttribute("data-cc-original-src",t),e.removeAttribute("src"),e.style.display="none",P.push({type:"iframe",node:e,category:n,src:t}),N())}}}function G(e){if(!e.getAttribute("data-cc-facade")&&!D()){var t=e.getAttribute("data-settings");if(t){var n;try{n=JSON.parse(t)}catch(e){return}var o=n.youtube_url||n.vimeo_url||"";if(o){var c=o.toLowerCase(),r=null;if((-1!==c.indexOf("youtube")||-1!==c.indexOf("youtu.be")||-1!==c.indexOf("vimeo")||-1!==c.indexOf("dailymotion"))&&(r="marketing"),r&&(!l||!l[r])){if(!l){var a=Z();if(a&&a[r])return}e.setAttribute("data-cc-facade","1"),q=!0,P.push({type:"elementor",node:e,category:r,url:o}),N()}}}}}function j(){for(var e=document.querySelectorAll("iframe[src]"),t=0;t<e.length;t++)U(e[t]);for(var n=document.querySelectorAll(".elementor-widget-video"),o=0;o<n.length;o++)G(n[o]);for(var c=document.querySelectorAll(".wp-block-embed__wrapper iframe[src]"),r=0;r<c.length;r++)U(c[r])}function J(){new MutationObserver(function(e){e.forEach(function(e){e.addedNodes.forEach(function(e){if(1===e.nodeType){if("IFRAME"===e.tagName&&U(e),e.querySelectorAll){for(var t=e.querySelectorAll("iframe[src]"),n=0;n<t.length;n++)U(t[n]);for(var o=e.querySelectorAll(".elementor-widget-video"),c=0;c<o.length;c++)G(o[c])}e.classList&&e.classList.contains("elementor-widget-video")&&G(e)}})})}).observe(document.documentElement,{childList:!0,subtree:!0})}function W(){for(var e=document.querySelectorAll("iframe[src]"),t=0;t<e.length;t++){var n=e[t];if(!n.getAttribute("data-cc-facade")){var o=n.src||"";if(o)for(var c=0;c<O.length;c++)if(-1!==o.indexOf(O[c].pattern)){n.src=o;break}}}}function H(e,t){r.cookieTypes.forEach(function(n){var o=!!e[n.id];if((t?!!t[n.id]:null)!==o||null===t)if(o&&"function"==typeof n.onAccept)try{n.onAccept()}catch(e){console.warn("[CookieConsent] onAccept error for "+n.id,e)}else if(!o&&"function"==typeof n.onReject)try{n.onReject()}catch(e){console.warn("[CookieConsent] onReject error for "+n.id,e)}})}function F(e,t){var n=l;if(l=e,w(r.cookieName,JSON.stringify(e),r.cookieExpiry),E(e),q&&t)for(var o=document.querySelectorAll(".cc-iframe-facade[data-cc-elementor-widget]"),a=0;a<o.length;a++){var s=o[a].getAttribute("data-cc-iframe-cat");if(s&&e[s])return void window.location.reload()}K(e,n),T(e),function(e){for(var t=document.querySelectorAll(".cc-iframe-facade"),n=0;n<t.length;n++)if(!t[n].getAttribute("data-cc-elementor-widget")&&e[t[n].getAttribute("data-cc-iframe-cat")]){var o=t[n].nextElementSibling;if(o&&"IFRAME"===o.tagName&&o.getAttribute("data-cc-facade")){var c=o.getAttribute("data-cc-original-src");c&&(o.src=c,o.style.display="",o.removeAttribute("data-cc-facade"))}t[n].parentNode.removeChild(t[n])}}(e),t&&"auto_grant_non_eu"!==t&&"ccpa_auto_grant"!==t&&setTimeout(W,300),H(e,n),$(e),r.tcfEnabled&&B(e),t&&function(e,t){if(r.logEndpoint){var n={};r.cookieTypes.forEach(function(e){n[e.id]=!!t[e.id]});var o={anonymousId:b(),timestamp:(new Date).toISOString(),action:e,consent:n,complianceMode:p,country:f,url:location.href,userAgent:navigator.userAgent,tcfEnabled:r.tcfEnabled,gpc:i,consentVersion:r.consentVersion,version:c};try{var a=JSON.stringify(o);navigator.sendBeacon?navigator.sendBeacon(r.logEndpoint,new Blob([a],{type:"application/json"})):fetch(r.logEndpoint,{method:"POST",body:a,headers:{"Content-Type":"application/json"},keepalive:!0})}catch(e){}}}(t,e)}function $(e){r.cookieTypes.forEach(function(t){if(!t.required){var n=document.getElementById("cc-cb-"+t.id);n&&(n.checked=!!e[t.id])}});var t=document.getElementById("cc-cats"),n=document.getElementById("cc-details"),o=document.getElementById("cc-pref-buttons");t&&t.classList.remove("cc-open"),n&&(n.textContent=r.text.showDetailsButton),o&&o.classList.remove("cc-open")}function Z(){var e=h(r.cookieName);if(!e)return null;try{var t=JSON.parse(e);return!t||"object"!=typeof t||Array.isArray(t)||(!t.ts||isNaN(new Date(t.ts).getTime())||(Date.now()-new Date(t.ts).getTime())/864e5>r.reconsentDays||r.consentVersion&&t.cv!==r.consentVersion)?null:t}catch(e){return null}}function Y(){var e={ts:(new Date).toISOString(),cv:r.consentVersion,cc:f};return r.cookieTypes.forEach(function(t){e[t.id]=!0}),e}function z(){var e={ts:(new Date).toISOString(),cv:r.consentVersion,cc:f};return r.cookieTypes.forEach(function(t){e[t.id]=!!t.required}),e}function K(e,t){if(t){var n=[];if(r.cookieTypes.forEach(function(o){o.required||t[o.id]&&!e[o.id]&&n.push(o.id)}),n.length)document.cookie.split(";").map(function(e){return e.trim().split("=")[0]}).filter(Boolean).forEach(function(e){var t=u(e);if(t&&-1!==n.indexOf(t.category)){var o=location.hostname,c="."+o;document.cookie=e+"=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/",document.cookie=e+"=; expires=Thu, 01 Jan 1970 00:00:00 GMT",document.cookie=e+"=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain="+c,document.cookie=e+"=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain="+o,document.cookie=e+"=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; Secure",r.cookieDomain&&(document.cookie=e+"=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain="+r.cookieDomain,document.cookie=e+"=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain="+r.cookieDomain+"; Secure");var a=o.split(".");if(a.length>2){var i="."+a.slice(1).join(".");document.cookie=e+"=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain="+i,document.cookie=e+"=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain="+i+"; Secure"}}})}}function Q(e){return(f=e?e.toUpperCase():null)?r.regionRules.gdpr&&-1!==r.regionRules.gdpr.indexOf(f)?"gdpr":r.regionRules.ccpa&&-1!==r.regionRules.ccpa.indexOf(f)?"ccpa":"none":"gdpr"}function X(e){if(window.__ccGeo)return e(Q(window.__ccGeo));r.geoApi?fetch(r.geoApi).then(function(e){return e.json()}).then(function(t){var n=t.country_code||t.country||"";n?e(Q(n)):ee(e)}).catch(function(){ee(e)}):ee(e)}function ee(e){var t="";try{t=Intl.DateTimeFormat().resolvedOptions().timeZone||""}catch(e){}return 0===t.indexOf("Europe/")||"Atlantic/Reykjavik"===t?(f=null,e("gdpr")):0===t.indexOf("US/")||0===t.indexOf("America/")&&["New_York","Chicago","Denver","Los_Angeles","Phoenix","Anchorage","Boise","Detroit","Indianapolis","Louisville","Menominee","Nome","Adak","Juneau","Sitka","Yakutat","Metlakatla","Kentucky","Indiana","North_Dakota"].some(function(e){return-1!==t.indexOf(e)})||"Pacific/Honolulu"===t?(f="US",e("ccpa")):(f=null,void e("gdpr"))}function te(){"dark"===r.theme&&document.documentElement.classList.add("cc-dark"),"right"===r.iconPosition&&document.documentElement.classList.add("cc-icon-right")}function ne(){var e=r.text,t=document.createElement("div");t.id="cc-overlay",document.body.appendChild(t);var n=document.createElement("div");n.id="cc-banner",n.setAttribute("role","dialog"),n.setAttribute("aria-modal","true"),n.setAttribute("aria-label","Cookie consent"),n.setAttribute("tabindex","-1");var o=r.cookieTypes.map(function(t){var n=t.required?' <span class="cc-always-on">'+v(e.alwaysOnLabel)+"</span>":"";return'<div class="cc-cat"><div><div class="cc-cat-label">'+v(t.name)+n+'</div><div class="cc-cat-desc">'+v(t.description||"")+'</div></div><label class="cc-toggle"><input type="checkbox" id="cc-cb-'+v(t.id)+'" aria-label="'+v(t.name)+'"'+(t.required||t.defaultValue?" checked":"")+(t.required?" disabled":"")+'><span class="cc-slider"></span></label></div>'}).join("");n.innerHTML='<div class="cc-inner"><p class="cc-title">'+v(e.title)+'</p><p class="cc-text">'+v(e.description)+' <a href="'+v(r.privacyUrl)+'">Privacy Policy</a></p><div class="cc-buttons"><button class="cc-btn cc-btn-accept" id="cc-accept">'+v(e.acceptAllButton)+'</button><button class="cc-btn cc-btn-details" id="cc-details" aria-expanded="false">'+v(e.showDetailsButton)+'</button></div><div class="cc-categories" id="cc-cats">'+o+'</div><div class="cc-pref-buttons" id="cc-pref-buttons"><button class="cc-btn cc-btn-save" id="cc-save">'+v(e.saveButton)+'</button><button class="cc-btn cc-btn-decline" id="cc-decline">'+v(e.declineAllButton)+"</button></div></div>"+(r.showPoweredBy?'<div class="cc-powered"><a href="https://www.cookieflame.com" target="_blank" rel="noopener">Powered by CookieFlame</a></div>':""),document.body.appendChild(n);var c=document.createElement("button");c.id="cc-icon",c.setAttribute("aria-label","Cookie settings"),c.innerHTML='<svg width="24" height="24" viewBox="0 0 38 38" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M19.1 1.16c-.06-.42-.38-.77-.8-.83A11.8 11.8 0 0 0 13.28 1.2L7.5 4.3A11.8 11.8 0 0 0 3.95 7.98L1.09 13.9A11.8 11.8 0 0 0 .38 18.96l1.13 6.47a11.8 11.8 0 0 0 3.53 4.99l4.71 4.56a11.8 11.8 0 0 0 4.59 2.24l6.48.91a11.8 11.8 0 0 0 5.52-.89l5.78-3.09a11.8 11.8 0 0 0 3.55-3.69l2.87-5.9a11.8 11.8 0 0 0 .71-5.08c-.07-.41-.42-.73-.83-.79a14.8 14.8 0 0 1-8.16-7.61c-.14-.59-.63-1.05-1.23-1.14A14.8 14.8 0 0 1 19.1 1.16ZM15.25 10.25a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5Zm-5 15a2.5 2.5 0 1 1 5 0 2.5 2.5 0 0 1-5 0Zm17.5-5a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5Z" fill="currentColor"/></svg>',document.body.appendChild(c),document.getElementById("cc-accept").addEventListener("click",se),document.getElementById("cc-decline").addEventListener("click",de),document.getElementById("cc-save").addEventListener("click",ue),document.getElementById("cc-details").addEventListener("click",le),c.addEventListener("click",function(){re("gdpr")})}var oe=null;function ce(e){if("Tab"===e.key){var t=document.getElementById("cc-banner")||document.getElementById("cc-ccpa-banner");if(t&&t.classList.contains("cc-show")){for(var n=[],o=t.querySelectorAll('button, a[href], input:not([disabled]), select, textarea, [tabindex]:not([tabindex="-1"])'),c=0;c<o.length;c++){for(var r=o[c],a=!1,i=r.parentElement;i&&i!==t;){if(i.style&&"none"===window.getComputedStyle(i).display){a=!0;break}i=i.parentElement}a||n.push(r)}if(0!==n.length){var s=n[0],d=n[n.length-1];e.shiftKey?document.activeElement===s&&(e.preventDefault(),d.focus()):document.activeElement===d&&(e.preventDefault(),s.focus())}}}}function re(e){"ccpa"!==e?(oe=document.activeElement,requestAnimationFrame(function(){var e=document.getElementById("cc-overlay"),t=document.getElementById("cc-banner"),n=document.getElementById("cc-icon");if(e&&e.classList.add("cc-show"),t){t.classList.add("cc-show");var o=t.querySelector(".cc-btn-accept");o&&o.focus(),document.addEventListener("keydown",ce)}n&&n.classList.remove("cc-show")})):requestAnimationFrame(function(){var e=document.getElementById("cc-ccpa-banner");e&&(e.classList.add("cc-show"),oe=document.activeElement,e.focus(),document.addEventListener("keydown",ce))})}function ae(){var e=document.getElementById("cc-overlay"),t=document.getElementById("cc-banner"),n=document.getElementById("cc-icon");if(e&&e.classList.remove("cc-show"),t&&t.classList.remove("cc-show"),n&&"gdpr"===p&&n.classList.add("cc-show"),document.removeEventListener("keydown",ce),oe&&"function"==typeof oe.focus)try{oe.focus()}catch(e){}oe=null}function ie(){var e=document.getElementById("cc-overlay"),t=document.getElementById("cc-banner"),n=document.getElementById("cc-icon");e&&e.classList.remove("cc-show"),t&&t.classList.remove("cc-show"),n&&n.classList.remove("cc-show"),document.removeEventListener("keydown",ce)}function se(){F(Y(),"accept_all"),ae(),_("cc:accept-all"),_("cc:update")}function de(){F(z(),"decline_all"),ae(),_("cc:decline-all"),_("cc:update")}function ue(){var e;F((e={ts:(new Date).toISOString(),cv:r.consentVersion,cc:f},r.cookieTypes.forEach(function(t){if(t.required)e[t.id]=!0;else{var n=document.getElementById("cc-cb-"+t.id);e[t.id]=!!n&&n.checked}}),e),"save_preferences"),ae(),_("cc:save"),_("cc:update")}function le(){var e=document.getElementById("cc-cats"),t=document.getElementById("cc-details"),n=document.getElementById("cc-pref-buttons"),o=e.classList.toggle("cc-open");t.textContent=o?r.text.hideDetailsButton:r.text.showDetailsButton,t.setAttribute("aria-expanded",o?"true":"false"),n&&(o?n.classList.add("cc-open"):n.classList.remove("cc-open"))}function pe(){var e=document.getElementById("cc-declaration");if(e){for(var t=k(),n={},o=document.cookie.split(";").map(function(e){return e.trim().split("=")[0]}).filter(Boolean),c=0;c<o.length;c++){var a=u(o[c]);a&&(n[o[c]]={name:o[c],vendor:a.vendor,category:a.category,purpose:a.purpose,expiry:a.expiry||"",active:!0})}for(var i=0;i<d.length;i++){var s=d[i];s.name&&s.vendor&&s.category&&"unclassified"!==s.category&&!n[s.name]&&(n[s.name]={name:s.name,vendor:s.vendor,category:s.category,purpose:s.purpose||"",expiry:s.expiry||"",active:!1})}for(var l=0;l<o.length;l++)n[o[l]]||(n[o[l]]={name:o[l],vendor:"",category:"unclassified",purpose:"",expiry:"",active:!0});var p=["necessary","preferences","analytics","marketing"],f={};r.cookieTypes.forEach(function(e){f[e.id]=e.name});var m={};p.forEach(function(e){m[e]=[]}),Object.keys(n).forEach(function(e){var t=n[e],o=t.category;m[o]||(m[o]=[]),m[o].push(t)}),p.forEach(function(e){m[e]&&m[e].sort(function(e,t){return e.name.localeCompare(t.name)})});var g="<div>";if(t.timestamp){var y=new Date(t.timestamp);g+="<p>"+v("Your consent was last updated on "+y.toLocaleDateString(void 0,{year:"numeric",month:"long",day:"numeric"})+" at "+y.toLocaleTimeString(void 0,{hour:"2-digit",minute:"2-digit"})+".")+"</p>"}else g+="<p>"+v("You have not yet made a cookie preference choice on this site.")+"</p>";for(var h=0;h<p.length;h++){var w=p[h],b=m[w];if(b&&b.length){var _,x=f[w]||w,E=r.cookieTypes.filter(function(e){return e.id===w})[0],A=E&&E.description||"";_=!!E&&E.required?"Always active":t[w]?"Active":"Inactive",g+="<div>",g+="<p><b>"+v(x)+"</b> ("+v(_)+")</p>",A&&(g+="<p>"+v(A)+"</p>"),g+="<table><thead><tr>",g+="<th>Name</th><th>Provider</th><th>Purpose</th><th>Expiry</th>",g+="</tr></thead><tbody>";for(var S=0;S<b.length;S++){var L=b[S];g+="<tr>",g+="<td><code>"+v(L.name)+"</code></td>",g+="<td>"+v(L.vendor||"Unknown")+"</td>",g+="<td>"+v(L.purpose||"Not specified")+"</td>",g+="<td>"+v(L.expiry||"—")+"</td>",g+="</tr>"}g+="</tbody></table></div>"}}g+="</div>",e.innerHTML=g}}window.__ccInternal={getResourceSnapshot:ye,scriptLoaded:function(e){for(var t=ge||ye(),n=0;n<t.allUrls.length;n++)if(-1!==t.allUrls[n].indexOf(e))return!0;return!1},scriptLoadedUrls:function(e){for(var t=ge||ye(),n=[],o=0;o<t.allUrls.length;o++)-1!==t.allUrls[o].indexOf(e)&&n.push(t.allUrls[o]);return n},hasCookie:function(e){return document.cookie.split(";").some(function(t){return t.trim().split("=")[0]===e})},hasCookiePrefix:function(e){return document.cookie.split(";").some(function(t){return 0===t.trim().split("=")[0].indexOf(e)})},setResourceSnapshot:function(e){ge=e},visitorCookies:function(e){for(var t=ye().thirdPartyDomains,n={},o=0;o<e.length;o++)we(e[o],t,he(e[o]))&&(n[e[o]]=!0);return n},facadeBridge:{getConfig:function(){return r},getConsentState:function(){return l},loadConsent:Z,saveConsent:function(e,t){F(e,t)},hideAllBanners:ie,registerRenderer:function(e){R=e,M=2,V()}}},window.CookieConsent={getState:k,getConfig:function(){return r},getComplianceMode:function(){return p},getCountry:function(){return f},show:function(){re("gdpr")},acceptAll:se,declineAll:de,getGPC:function(){return i},getConsentVersion:function(){return r.consentVersion},on:function(e,t){window.addEventListener(e,t)},off:function(e,t){window.removeEventListener(e,t)},VERSION:c},window.addEventListener("cc:ready",pe),window.addEventListener("cc:update",pe),function(){if(!window.__ccDefaultsSet){var e={wait_for_update:500};r.cookieTypes.forEach(function(t){t.consentMode&&t.consentMode.forEach(function(n){e[n]=t.required?"granted":"denied"})}),x("consent","default",e)}}(),function(){if(r.tcfEnabled){var e=window.__tcfapi&&window.__tcfapi.queue?window.__tcfapi.queue:[];window.__tcfapi=C,e.forEach(function(e){C.apply(null,e)}),S=!0,window===window.top&&window.addEventListener("message",function(e){var t;try{t="string"==typeof e.data?JSON.parse(e.data):e.data}catch(e){return}if(t&&t.__tcfapiCall){var n=t.__tcfapiCall;C(n.command,n.version,function(t,o){var c={__tcfapiReturn:{returnValue:t,success:o,callId:n.callId}};e.source&&e.source.postMessage("string"==typeof e.data?JSON.stringify(c):c,"*")},n.parameter)}})}}();var fe=Z();function me(){var e=window.__ccDiscovery&&window.__ccDiscovery.active;if(!e&&(window.__ccDNT||document.cookie.split(";").some(function(e){return 0===e.trim().indexOf("cookieflame_dnt=")})))return E(l=z()),K(l,Y()),J(),j(),p="dnt",te(),ne(),ie(),$(l),_("cc:ready"),void pe();if(e){E(l=Y()),T(l),J(),H(l,null),r.tcfEnabled&&B(l),window.dataLayer=window.dataLayer||[];for(var t=["consent_accepted_analytics","consent_accepted_marketing","consent_accepted_preferences","consent_accepted_necessary","consent_update","consent_given","cookie_consent_given","cookie_consent_update","consent_accepted"],n=0;n<t.length;n++)window.dataLayer.push({event:t[n]});return p="discovery",te(),ne(),ie(),_("cc:ready"),_("cc:update"),void pe()}var o=Z();if(o)if(i&&(o.marketing=!1),l=o,E(o),T(o),J(),j(),H(o,null),r.tcfEnabled&&B(o),te(),ne(),ie(),$(o),o.cc){if("gdpr"===(p=Q(o.cc))){var c=document.getElementById("cc-icon");c&&c.classList.add("cc-show")}_("cc:ready"),pe()}else X(function(e){if(p=e,"gdpr"===e){var t=document.getElementById("cc-icon");t&&t.classList.add("cc-show")}_("cc:ready"),pe()});else X(function(e){if(p=e,"none"===e)return J(),F(Y(),"auto_grant_non_eu"),_("cc:ready"),_("cc:update"),void pe();if("ccpa"===e){if(J(),i){var t=Y();t.marketing=!1,F(t,"ccpa_gpc_optout")}else F(Y(),"ccpa_auto_grant");return te(),n=r.text,(o=document.createElement("div")).id="cc-ccpa-banner",o.setAttribute("role","dialog"),o.setAttribute("aria-modal","true"),o.setAttribute("aria-label","Privacy notice"),o.setAttribute("tabindex","-1"),o.innerHTML='<div class="cc-ccpa-inner"><div class="cc-ccpa-text"><p>'+v(n.ccpaDescription)+'</p></div><div class="cc-ccpa-actions"><a href="'+v(r.ccpaUrl)+'" class="cc-ccpa-link">'+v(n.ccpaDnsmpiLink)+'</a><button class="cc-btn cc-btn-accept" id="cc-ccpa-ok">'+v(n.ccpaAcceptButton)+"</button></div></div>",document.body.appendChild(o),document.getElementById("cc-ccpa-ok").addEventListener("click",function(){F(Y(),"ccpa_acknowledge"),o.classList.remove("cc-show"),_("cc:update")}),re("ccpa"),_("cc:ready"),_("cc:update"),void pe()}var n,o;te(),J(),j(),ne(),re("gdpr"),_("cc:ready"),pe()})}fe&&(l=fe,E(fe)),function(){if(!g&&r.autoBlockPatterns&&r.autoBlockPatterns.length){g=!0;var e=document.createElement.bind(document),t=Object.getOwnPropertyDescriptor(HTMLScriptElement.prototype,"src");t&&t.set&&(document.createElement=function(){var n=e.apply(document,arguments);if("script"!==(arguments[0]||"").toLowerCase())return n;function o(e,t){if(t&&"string"==typeof t&&!e.getAttribute("data-cc")&&!e.getAttribute("data-cc-auto")&&-1===t.indexOf("cookieconsent")){var n=I(t);!n||l&&l[n]||(e.type="text/plain",e.setAttribute("data-cc",n),e.setAttribute("data-cc-auto","1"))}}Object.defineProperty(n,"src",{configurable:!0,enumerable:!0,get:function(){return t.get.call(this)},set:function(e){return o(this,e),t.set.call(this,e)}});var c=n.setAttribute.bind(n);return n.setAttribute=function(e,t){return"src"===e.toLowerCase()&&o(n,t),c(e,t)},n}),new MutationObserver(function(e){e.forEach(function(e){e.addedNodes.forEach(function(e){if(1===e.nodeType){var t=[];"SCRIPT"===e.tagName&&t.push(e),e.querySelectorAll&&e.querySelectorAll("script[src]").forEach(function(e){t.push(e)}),t.forEach(function(e){if(!(e.getAttribute("data-cc-auto")||e.getAttribute("data-cc")||e.src&&-1!==e.src.indexOf("cookieconsent"))){var t=I(e.src);t&&(l&&l[t]||(e.type="text/plain",e.setAttribute("data-cc",t),e.setAttribute("data-cc-auto","1")))}})}})})}).observe(document.documentElement,{childList:!0,subtree:!0})}}(),"loading"===document.readyState?document.addEventListener("DOMContentLoaded",me):me();var ge=null;function ye(){for(var e=[],t=[],n=[],o={},c={},r=document.querySelectorAll("script[src]"),a=0;a<r.length;a++)r[a].src&&e.push(r[a].src);for(var i=document.querySelectorAll("iframe[src]"),s=0;s<i.length;s++)i[s].src&&e.push(i[s].src);if(window.performance&&performance.getEntriesByType)for(var d=performance.getEntriesByType("resource"),u=0;u<d.length;u++){var l=d[u].name||"";if(l){-1===e.indexOf(l)&&e.push(l);var p=d[u].initiatorType||"";if("script"===p||"iframe"===p||"xmlhttprequest"===p||"fetch"===p||-1!==l.indexOf(".js"))try{var f=new URL(l),m=f.hostname;if(m===location.hostname||m==="www."+location.hostname||"www."+m===location.hostname){var g=f.pathname.match(/\/wp-content\/plugins\/([^\/]+)\//);g&&!c[g[1]]&&(c[g[1]]=!0,n.push({slug:g[1],url:l}))}else o[m]||(o[m]=!0,t.push(m))}catch(e){}}}return{allUrls:e,thirdPartyDomains:t,pluginScripts:n}}var ve={};function he(e){var t={set_by:"",set_by_plugin:""},n=window.__ccCookieSetters;if(!n||!Object.prototype.hasOwnProperty.call(n,e)||!n[e])return t;try{var o=new URL(String(n[e]).replace(/:\d+(:\d+)?$/,""));if(/^(chrome-extension|moz-extension|safari-web-extension|safari-extension):$/.test(o.protocol))return t.set_by="browser-extension",t;var c=o.hostname;if(c===location.hostname||c==="www."+location.hostname||"www."+c===location.hostname){t.set_by="first-party";var r=o.pathname.match(/\/wp-content\/(?:plugins|themes)\/([^\/]+)\//);r&&(t.set_by_plugin=r[1].toLowerCase())}else t.set_by=c.toLowerCase()}catch(e){}return t}function we(e,t,n){if("browser-extension"===n.set_by)return!0;var o=window.__ccVisitorCookieRules;if(!o||!o.tools)return!1;if(o.allow&&-1!==o.allow.indexOf(e))return!1;if(n.set_by)return!1;for(var c=0;c<o.tools.length;c++){for(var r=o.tools[c],a=-1!==(r.n||[]).indexOf(e),i=0;!a&&i<(r.p||[]).length;i++)0===e.indexOf(r.p[i])&&(a=!0);if(a){for(var s=0;s<t.length;s++)for(var d=0;d<(r.d||[]).length;d++){var u=String(t[s]).toLowerCase(),l=r.d[d];if(u===l||u.slice(-l.length-1)==="."+l)return!1}return!0}}return!1}function be(){var t=window.__ccClassified||[],n=window.__ccUnclassified||[],o=window.__ccBeaconUrl;if(o){var c,r={};for(c=0;c<t.length;c++)r[t[c]]=!0;for(c=0;c<n.length;c++)r[n[c]]=!0;var a=Object.keys(ve);for(c=0;c<a.length;c++)r[a[c]]=!0;var i=Object.keys(s);for(c=0;c<i.length;c++)r[i[c]]=!0;var d={},u=[],l=document.cookie.split(";");for(c=0;c<l.length;c++){var p=l[c].indexOf("="),f=(p>-1?l[c].substring(0,p):l[c]).trim(),m=p>-1?l[c].substring(p+1).trim():"";f&&(u.push(f),d[f]=m)}var g=[];for(c=0;c<u.length;c++)if(!r[u[c]]){for(var y=!1,v=Object.keys(s),h=0;h<v.length;h++)if(s[v[h]].prefix&&0===u[c].indexOf(v[h])){y=!0;break}y||g.push(u[c])}if(0!==g.length){for(var w=ye(),b=w.thirdPartyDomains,k=[],_=0;_<w.pluginScripts.length;_++)k.push(w.pluginScripts[_].slug);for(var x=location.pathname,E=0;E<g.length;E++){var A=g[E],S=!!e[A]&&!r[A],L=ke(d[A]||""),C=he(A);if(!we(A,b,C)||window.__ccDiscovery&&window.__ccDiscovery.active){var B=JSON.stringify({cookie_name:A,script_domains:b,plugin_scripts:k,page_path:x,pre_existing:S,value_shape:L,set_by:C.set_by,set_by_plugin:C.set_by_plugin});ve[A]=!0,navigator.sendBeacon&&navigator.sendBeacon(o,new Blob([B],{type:"application/json"}))}else ve[A]=!0}}}}function ke(e){if(!e||0===e.length)return"empty";if("true"===e||"false"===e||"1"===e||"0"===e)return"boolean";if(/^\d+$/.test(e))return e.length>12?"long-numeric":"numeric";if(/^\d+\.\d+$/.test(e))return"decimal";if(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(e))return"uuid";if(/^[A-Za-z0-9+\/]{20,}={0,2}$/.test(e))return"base64";if(/^(GA\d+\.\d+\.\d+\.\d+|G-[A-Z0-9]+)/.test(e))return"ga-id";try{if("object"==typeof JSON.parse(decodeURIComponent(e)))return"json"}catch(e){}return/^[a-f0-9]{24,}$/i.test(e)?"hex-id":/^\d{10,13}$/.test(e)?"timestamp":e.length<=20?"short-string":e.length<=100?"medium-string":"long-string"}setTimeout(be,3e3),setTimeout(be,15e3),setTimeout(be,45e3),window.addEventListener("cc:update",function(){setTimeout(be,3e3)}),window.addEventListener("pagehide",be)}();
     19!function(){"use strict";var e={};try{for(var t=document.cookie.split(";"),n=0;n<t.length;n++){var c=t[n].trim().split("=")[0];c&&(e[c]=!0)}}catch(e){}var o="5.4.0",r={cookieName:"cc_consent",cookieExpiry:365,privacyUrl:"/privacy-policy",geoApi:"",logEndpoint:"",reconsentDays:365,cspSafe:!1,tcfEnabled:!1,cmpId:1,cmpVersion:1,vendorListVersion:2,storageMethod:"cookie",cookieDomain:"",shareSubdomains:!1,theme:"light",showPoweredBy:!1,iconPosition:"left",consentVersion:"",rejectFirstScreen:!1,visitorLogins:!1,autoBlockPatterns:[{pattern:"googletagmanager.com/gtag",category:"analytics"},{pattern:"google-analytics.com",category:"analytics"},{pattern:"analytics.google.com",category:"analytics"},{pattern:"hotjar.com",category:"analytics"},{pattern:"mixpanel.com",category:"analytics"},{pattern:"segment.com/analytics",category:"analytics"},{pattern:"segment.io",category:"analytics"},{pattern:"clarity.ms",category:"analytics"},{pattern:"plausible.io",category:"analytics"},{pattern:"js.hs-analytics.net",category:"analytics"},{pattern:"connect.facebook.net",category:"marketing"},{pattern:"snap.licdn.com",category:"marketing"},{pattern:"ads.linkedin.com",category:"marketing"},{pattern:"googleads.g.doubleclick.net",category:"marketing"},{pattern:"googlesyndication.com",category:"marketing"},{pattern:"googleadservices.com",category:"marketing"},{pattern:"ads-twitter.com",category:"marketing"},{pattern:"analytics.tiktok.com",category:"marketing"},{pattern:"pintrk",category:"marketing"},{pattern:"s.pinimg.com/ct",category:"marketing"},{pattern:"widget.intercom.io",category:"preferences"},{pattern:"js.intercomcdn.com",category:"preferences"}],regionRules:{gdpr:["AT","BE","BG","CY","CZ","DE","DK","EE","ES","FI","FR","GR","HR","HU","IE","IT","LT","LU","LV","MT","NL","PL","PT","RO","SE","SI","SK","IS","LI","NO","GB","GG","JE","IM","CH","BR","ZA"],ccpa:["US"]},text:{title:"We value your privacy",description:"We use cookies to improve your experience, analyse traffic and personalise content. You can choose which categories to allow.",acceptAllButton:"Accept All",rejectAllButton:"Decline All",declineAllButton:"Decline All",saveButton:"Save Preferences",showDetailsButton:"Manage preferences",hideDetailsButton:"Hide preferences",alwaysOnLabel:"(always on)",ccpaTitle:"Your Privacy Choices",ccpaDescription:"We use cookies and similar technologies. Under US state privacy laws, such as California's, you have the right to opt out of the sale or sharing of your personal information.",ccpaAcceptButton:"OK",ccpaDnsmpiLink:"Do Not Sell or Share My Personal Information",ccpaGpcNotice:"Your browser asked us not to sell or share your information. We're honouring that.",ccpaChoiceTitle:"Do Not Sell or Share My Personal Information",ccpaChoiceText:"Information about your visit may be used by advertising partners whose cookies run on this site. Under US state privacy laws that can count as selling or sharing. You can opt out in this browser.",ccpaOptOutButton:"Opt out",ccpaCancelButton:"Cancel",ccpaOptedOut:"You've opted out of the sale or sharing of your personal information in this browser.",ccpaGpcOptedOut:"Your browser's Global Privacy Control signal asked us not to sell or share your information, so you're opted out in this browser.",ccpaDoneButton:"Done",ccpaOptInButton:"Allow selling or sharing",ccpaOptInGpcNote:"Allowing it overrides your browser's Global Privacy Control signal on this site only.",ccpaMoreSettings:"More cookie settings",gpcPrefNote:"Your browser's Global Privacy Control signal is on, so Marketing stays off unless you switch it on here.",changePreferences:"Change cookie preferences",loginCookiesTitle:"If you log in",loginCookiesIntro:"These cookies are only set if you log in to an account on this site. They keep you signed in and remember your account settings. They're needed for logging in to work, so they don't ask for your consent."},cookieTypes:[{id:"necessary",name:"Necessary",description:"Required for the site to function. Cannot be disabled.",required:!0,consentMode:["security_storage","functionality_storage"],tcfPurposes:[1],onAccept:function(){}},{id:"preferences",name:"Preferences",description:"Remember your settings and personalisation choices.",required:!1,defaultValue:!1,consentMode:["functionality_storage","personalization_storage"],tcfPurposes:[2,3],onAccept:function(){},onReject:function(){}},{id:"analytics",name:"Analytics",description:"Help us understand how visitors use the site.",required:!1,defaultValue:!1,consentMode:["analytics_storage"],tcfPurposes:[4,5,6],onAccept:function(){},onReject:function(){}},{id:"marketing",name:"Marketing",description:"Used to deliver relevant ads and track campaigns.",required:!1,defaultValue:!1,consentMode:["ad_storage","ad_user_data","ad_personalization"],tcfPurposes:[7,8,9,10],onAccept:function(){},onReject:function(){}}]};if(window.__ccConfig&&"object"==typeof window.__ccConfig){var a=window.__ccConfig;["cookieName","cookieExpiry","privacyUrl","geoApi","logEndpoint","reconsentDays","cspSafe","tcfEnabled","cmpId","cmpVersion","vendorListVersion","storageMethod","cookieDomain","shareSubdomains","theme","showPoweredBy","iconPosition","consentVersion","rejectFirstScreen","visitorLogins"].forEach(function(e){void 0!==a[e]&&(r[e]=a[e])}),a.text&&"object"==typeof a.text&&Object.keys(a.text).forEach(function(e){r.text[e]=a.text[e]}),Array.isArray(a.cookieTypes)&&(r.cookieTypes=a.cookieTypes),Array.isArray(a.autoBlockPatterns)&&(r.autoBlockPatterns=a.autoBlockPatterns),a.regionRules&&(r.regionRules=a.regionRules)}window.__ccConsentVersion&&(r.consentVersion=window.__ccConsentVersion);var i=!!window.__ccGPC;function s(e){return i&&e&&!e.gpcOptIn&&(e.marketing=!1,e.gpc=1),e}window.__ccBlockRules&&Array.isArray(window.__ccBlockRules)&&window.__ccBlockRules.length&&(r.autoBlockPatterns=window.__ccBlockRules);var d={cc_consent:{vendor:"CookieFlame",category:"necessary",purpose:"Stores your consent preferences",expiry:"1 year"},cc_anon_id:{vendor:"CookieFlame",category:"necessary",purpose:"Anonymous ID for consent logging",expiry:"1 year"},cookieflame_dnt:{vendor:"CookieFlame",category:"necessary",purpose:"Do-not-track opt-out flag",expiry:"1 year"},PHPSESSID:{vendor:"PHP",category:"necessary",purpose:"Server-side session identifier",expiry:"Session"},"wp-settings-":{vendor:"WordPress",category:"necessary",purpose:"Remembers dashboard layout settings",prefix:!0,expiry:"1 year"},"wp-settings-time-":{vendor:"WordPress",category:"necessary",purpose:"Records when dashboard settings were last saved",prefix:!0,expiry:"1 year"},wordpress_logged_in_:{vendor:"WordPress",category:"necessary",purpose:"Keeps you logged in",prefix:!0,expiry:"Session, or 14 days with Remember Me"},wordpress_sec_:{vendor:"WordPress",category:"necessary",purpose:"Confirms your login when you open the dashboard",prefix:!0,expiry:"Session, or 14 days with Remember Me"},wordpress_rec_:{vendor:"WordPress",category:"necessary",purpose:"Recovery mode after a site error (administrators only)",prefix:!0,expiry:"1 week"},wordpress_test_cookie:{vendor:"WordPress",category:"necessary",purpose:"Checks your browser accepts cookies before you log in",expiry:"Session"},wp_lang:{vendor:"WordPress",category:"necessary",purpose:"Remembers the language picked on the login page",expiry:"Session"},comment_author_:{vendor:"WordPress",category:"necessary",purpose:"Remembers your name for comments, if you ask it to",prefix:!0,expiry:"1 year"},comment_author_email_:{vendor:"WordPress",category:"necessary",purpose:"Remembers your email for comments, if you ask it to",prefix:!0,expiry:"1 year"},comment_author_url_:{vendor:"WordPress",category:"necessary",purpose:"Remembers your website for comments, if you ask it to",prefix:!0,expiry:"1 year"},"wp-postpass_":{vendor:"WordPress",category:"necessary",purpose:"Remembers the password entered for a protected page",prefix:!0,expiry:"10 days"},woocommerce_cart_hash:{vendor:"WooCommerce",category:"necessary",purpose:"Shopping cart contents",expiry:"Session"},woocommerce_items_in_cart:{vendor:"WooCommerce",category:"necessary",purpose:"Cart item count",expiry:"Session"},wp_woocommerce_session_:{vendor:"WooCommerce",category:"necessary",purpose:"Shopping session",prefix:!0,expiry:"2 days"},_ga:{vendor:"Google Analytics",category:"analytics",purpose:"Distinguishes visitors",expiry:"2 years"},_ga_:{vendor:"Google Analytics",category:"analytics",purpose:"Stores session state",prefix:!0,expiry:"2 years"},_gid:{vendor:"Google Analytics",category:"analytics",purpose:"Distinguishes visitors (24h)",expiry:"24 hours"},_gat:{vendor:"Google Analytics",category:"analytics",purpose:"Throttles request rate",expiry:"1 minute"},wp_wk_ga_untrack_:{vendor:"Google Analytics",category:"analytics",purpose:"GA opt-out flag",prefix:!0,expiry:"Persistent"},_gcl_au:{vendor:"Google Ads",category:"marketing",purpose:"Conversion linker",expiry:"90 days"},_gcl_aw:{vendor:"Google Ads",category:"marketing",purpose:"Stores ad click info",expiry:"90 days"},_gac_:{vendor:"Google Ads",category:"marketing",purpose:"Campaign information",prefix:!0,expiry:"90 days"},IDE:{vendor:"Google DoubleClick",category:"marketing",purpose:"Ad serving and frequency",expiry:"1 year"},test_cookie:{vendor:"Google DoubleClick",category:"marketing",purpose:"Checks cookie support",expiry:"15 minutes"},NID:{vendor:"Google",category:"preferences",purpose:"Stores preferences",expiry:"6 months"}},u=window.__ccCustomCookies||[];function l(e){for(var t=0;t<u.length;t++){var n=u[t];if("exact"===n.match&&e===n.name)return n;if("prefix"===n.match&&0===e.indexOf(n.name))return n;if("contains"===n.match&&-1!==e.indexOf(n.name))return n}var c=d[e];if(c)return c;var o=0;return Object.keys(d).forEach(function(t){d[t].prefix&&t.length>o&&0===e.indexOf(t)&&(c=d[t],o=t.length)}),c||null}var p=/^(?:wordpress_(?:logged_in_|sec_|rec_|test_cookie$|[0-9a-f]{32}$)|wp-settings-|wp_lang$)/;function f(e){return p.test(e)}var g=null,m="none",y=null,v=null,h=!1,w=0;function b(e){return"string"!=typeof e&&(e=String(e)),e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;")}function k(e){if("localStorage"===r.storageMethod)try{return localStorage.getItem(e)}catch(e){return null}var t=document.cookie.match(new RegExp("(?:^|;\\s*)"+e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")+"=([^;]*)"));if(!t)return null;try{return decodeURIComponent(t[1])}catch(e){return null}}function _(e,t,n){if("localStorage"!==r.storageMethod){var c=new Date;c.setTime(c.getTime()+864e5*n);var o=e+"="+encodeURIComponent(t)+";expires="+c.toUTCString()+";path=/;SameSite=Lax;Secure";r.cookieDomain&&(o+=";domain="+r.cookieDomain),document.cookie=o}else try{localStorage.setItem(e,t)}catch(e){}}function x(){if(v)return v;var e=k("cc_anon_id");return e?(v=e,e):(_("cc_anon_id",v="cc_"+function(){if("undefined"!=typeof crypto&&"function"==typeof crypto.randomUUID)try{return crypto.randomUUID()}catch(e){}return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,function(e){var t=16*Math.random()|0;return("x"===e?t:3&t|8).toString(16)})}(),730),v)}function E(){var e={timestamp:g?g.ts:null,complianceMode:m,country:y,gpc:i,consentVersion:r.consentVersion};return r.cookieTypes.forEach(function(t){e[t.id]=g?!!g[t.id]:!!t.required}),e}function A(e,t){window.dispatchEvent(new CustomEvent(e,{detail:t||E()}))}function S(){window.dataLayer=window.dataLayer||[],window.dataLayer.push(arguments)}function C(e){var t={};r.cookieTypes.forEach(function(n){n.consentMode&&n.consentMode.forEach(function(c){t[c]=e[n.id]?"granted":"denied"})}),i&&!e.gpcOptIn&&(t.ad_storage="denied",t.ad_user_data="denied",t.ad_personalization="denied"),S("consent","update",t)}var I=new Map,L=!1;function B(e){var t={};return r.cookieTypes.forEach(function(n){n.tcfPurposes&&n.tcfPurposes.forEach(function(c){t[c]=!!e[n.id]})}),{tcString:"",tcfPolicyVersion:4,cmpId:r.cmpId,cmpVersion:r.cmpVersion,gdprApplies:"gdpr"===m,eventStatus:"useractioncomplete",cmpStatus:"loaded",listenerId:void 0,isServiceSpecific:!0,useNonStandardTexts:!1,publisherCC:"GB",purposeOneTreatment:!1,purpose:{consents:t,legitimateInterests:{}},vendor:{consents:{},legitimateInterests:{}},specialFeatureOptins:{},publisher:{consents:{},legitimateInterests:{},customPurpose:{consents:{},legitimateInterests:{}},restrictions:{}}}}function O(e,t,n,c){if("function"==typeof n)if("addEventListener"===e){var o=++w;if(I.set(o,n),g){var a=B(g);a.listenerId=o,n(a,!0)}}else"removeEventListener"===e?n({listenerId:c},I.delete(c)):"getTCData"===e?n(g?B(g):null,!!g):"ping"===e?n({gdprApplies:"gdpr"===m,cmpLoaded:L,cmpStatus:L?"loaded":"stub",displayStatus:"hidden",apiVersion:"2.2",cmpVersion:r.cmpVersion,cmpId:r.cmpId,gvlVersion:r.vendorListVersion,tcfPolicyVersion:4},!0):n(null,!1)}function T(e){I.forEach(function(t,n){var c=B(e);c.listenerId=n;try{t(c,!0)}catch(e){}})}function P(e){if(!e)return null;for(var t=e.toLowerCase(),n=0;n<r.autoBlockPatterns.length;n++){var c=r.autoBlockPatterns[n];if(c.pattern instanceof RegExp){if(c.pattern.test(t))return c.category}else if(-1!==t.indexOf(c.pattern.toLowerCase()))return c.category}return null}function D(e){document.querySelectorAll('script[type="text/plain"][data-cc]').forEach(function(t){var n=(t.getAttribute("data-cc")||"").toLowerCase();if(e[n]&&!t.getAttribute("data-cc-activated")){t.setAttribute("data-cc-activated","1");var c=document.createElement("script");c.isWPRocket=!0,Array.from(t.attributes).forEach(function(e){"type"!==e.name&&"data-cc"!==e.name&&"data-cc-activated"!==e.name&&"data-cc-auto"!==e.name&&c.setAttribute(e.name,e.value)}),!t.src&&t.textContent&&(c.textContent=t.textContent),t.parentNode.insertBefore(c,t.nextSibling),t.parentNode.removeChild(t)}}),["img","iframe","link"].forEach(function(t){document.querySelectorAll(t+"[data-cc][data-cc-src]").forEach(function(n){var c=(n.getAttribute("data-cc")||"").toLowerCase();e[c]&&!n.getAttribute("data-cc-activated")&&(n.setAttribute("data-cc-activated","1"),"link"===t?n.href=n.getAttribute("data-cc-src"):n.src=n.getAttribute("data-cc-src"))})})}var R=[{pattern:"youtube.com",category:"marketing"},{pattern:"youtube-nocookie.com",category:"marketing"},{pattern:"youtu.be",category:"marketing"},{pattern:"player.vimeo.com",category:"marketing"},{pattern:"vimeo.com",category:"marketing"},{pattern:"dailymotion.com",category:"marketing"},{pattern:"open.spotify.com",category:"preferences"},{pattern:"soundcloud.com",category:"preferences"},{pattern:"google.com/maps",category:"preferences"},{pattern:"maps.google.com",category:"preferences"},{pattern:"facebook.com/plugins",category:"marketing"},{pattern:"instagram.com",category:"marketing"},{pattern:"tiktok.com/embed",category:"marketing"},{pattern:"twitter.com",category:"marketing"},{pattern:"x.com",category:"marketing"}];function M(e,t){var n,c;try{var o=new URL(e,location.href);n=o.hostname.toLowerCase(),c=o.pathname.toLowerCase()}catch(e){return!1}var r=t.indexOf("/"),a=-1===r?t:t.slice(0,r),i=-1===r?"":t.slice(r);return(n===a||n.slice(-a.length-1)==="."+a)&&(!i||0===c.indexOf(i))}function N(e){if(!e)return null;for(var t=0;t<R.length;t++)if(M(e,R[t].pattern))return R[t].category;return null}function q(){return!(!document.body||!(document.body.classList.contains("elementor-editor-active")||document.body.classList.contains("fl-builder-edit")||document.body.classList.contains("et-fb-root-ancestor")))}var G=[],j=0,V=null,U=!1;function J(){if(V&&G.length){var e=G;G=[];try{V(e)}catch(e){}}}function W(){0!==G.length&&(2!==j?1!==j&&3!==j&&(j=1,function(){var e=window.__ccConfig&&window.__ccConfig.facadesUrl||null;if(!e)return void(j=3);var t=document.createElement("script");t.src=e,t.async=!0,t.onload=function(){j=2,J()},t.onerror=function(){j=3},(document.head||document.documentElement).appendChild(t)}()):J())}function F(e){var t=e.src||e.getAttribute("data-src")||"";if(t&&!e.getAttribute("data-cc-facade")&&!q()){var n=N(t);if(n){var c=g||ee();c&&c[n]||(e.setAttribute("data-cc-facade","1"),e.setAttribute("data-cc-iframe-cat",n),e.setAttribute("data-cc-original-src",t),e.removeAttribute("src"),e.style.display="none",G.push({type:"iframe",node:e,category:n,src:t}),W())}}}function H(e){if(!e.getAttribute("data-cc-facade")&&!q()){var t=e.getAttribute("data-settings");if(t){var n;try{n=JSON.parse(t)}catch(e){return}var c=n.youtube_url||n.vimeo_url||"";if(c){var o=c.toLowerCase(),r=null;if((-1!==o.indexOf("youtube")||-1!==o.indexOf("youtu.be")||-1!==o.indexOf("vimeo")||-1!==o.indexOf("dailymotion"))&&(r="marketing"),r&&(!g||!g[r])){if(!g){var a=ee();if(a&&a[r])return}e.setAttribute("data-cc-facade","1"),U=!0,G.push({type:"elementor",node:e,category:r,url:c}),W()}}}}}function $(){for(var e=document.querySelectorAll("iframe[src]"),t=0;t<e.length;t++)F(e[t]);for(var n=document.querySelectorAll(".elementor-widget-video"),c=0;c<n.length;c++)H(n[c]);for(var o=document.querySelectorAll(".wp-block-embed__wrapper iframe[src]"),r=0;r<o.length;r++)F(o[r])}function Y(){new MutationObserver(function(e){e.forEach(function(e){e.addedNodes.forEach(function(e){if(1===e.nodeType){if("IFRAME"===e.tagName&&F(e),e.querySelectorAll){for(var t=e.querySelectorAll("iframe[src]"),n=0;n<t.length;n++)F(t[n]);for(var c=e.querySelectorAll(".elementor-widget-video"),o=0;o<c.length;o++)H(c[o])}e.classList&&e.classList.contains("elementor-widget-video")&&H(e)}})})}).observe(document.documentElement,{childList:!0,subtree:!0})}function Z(){for(var e=document.querySelectorAll("iframe[src]"),t=0;t<e.length;t++){var n=e[t];if(!n.getAttribute("data-cc-facade")){var c=n.src||"";c&&N(c)&&(n.src=c)}}}function K(e,t){r.cookieTypes.forEach(function(n){var c=!!e[n.id];if((t?!!t[n.id]:null)!==c||null===t)if(c&&"function"==typeof n.onAccept)try{n.onAccept()}catch(e){console.warn("[CookieConsent] onAccept error for "+n.id,e)}else if(!c&&"function"==typeof n.onReject)try{n.onReject()}catch(e){console.warn("[CookieConsent] onReject error for "+n.id,e)}})}function z(e,t){if(r.logEndpoint){var n={};r.cookieTypes.forEach(function(e){n[e.id]=!!t[e.id]});var c={anonymousId:x(),timestamp:(new Date).toISOString(),action:e,consent:n,complianceMode:m,country:y,url:location.href,userAgent:navigator.userAgent,tcfEnabled:r.tcfEnabled,gpc:i,consentVersion:r.consentVersion,version:o};try{var a=JSON.stringify(c);navigator.sendBeacon?navigator.sendBeacon(r.logEndpoint,new Blob([a],{type:"application/json"})):fetch(r.logEndpoint,{method:"POST",body:a,headers:{"Content-Type":"application/json"},keepalive:!0})}catch(e){}}}function Q(e,t){var n=g;if(t&&0===t.indexOf("facade_accept_")||s(e),g=e,_(r.cookieName,JSON.stringify(e),r.cookieExpiry),C(e),U&&t)for(var c=document.querySelectorAll(".cc-iframe-facade[data-cc-elementor-widget]"),o=0;o<c.length;o++){var a=c[o].getAttribute("data-cc-iframe-cat");if(a&&e[a])return void window.location.reload()}ce(e,n),D(e),function(e){for(var t=document.querySelectorAll(".cc-iframe-facade"),n=0;n<t.length;n++)if(!t[n].getAttribute("data-cc-elementor-widget")&&e[t[n].getAttribute("data-cc-iframe-cat")]){var c=t[n].nextElementSibling;if(c&&"IFRAME"===c.tagName&&c.getAttribute("data-cc-facade")){var o=c.getAttribute("data-cc-original-src");o&&(c.src=o,c.style.display="",c.removeAttribute("data-cc-facade"))}t[n].parentNode.removeChild(t[n])}}(e),t&&"auto_grant_non_eu"!==t&&"ccpa_auto_grant"!==t&&setTimeout(Z,300),K(e,n),X(e),r.tcfEnabled&&T(e),t&&z(t,e)}function X(e){r.cookieTypes.forEach(function(t){if(!t.required){var n=document.getElementById("cc-cb-"+t.id);n&&(n.checked=!!e[t.id])}});var t=document.getElementById("cc-cats"),n=document.getElementById("cc-details"),c=document.getElementById("cc-pref-buttons");t&&t.classList.remove("cc-open"),n&&(n.textContent=r.text.showDetailsButton),c&&c.classList.remove("cc-open")}function ee(){var e=k(r.cookieName);if(!e)return null;try{var t=JSON.parse(e);if(!t||"object"!=typeof t||Array.isArray(t))return null;if(!t.ts||isNaN(new Date(t.ts).getTime()))return null;var n=Number(r.reconsentDays);return n>=1||(n=365),(Date.now()-new Date(t.ts).getTime())/864e5>n||r.consentVersion&&t.cv!==r.consentVersion?null:t}catch(e){return null}}function te(){var e={ts:(new Date).toISOString(),cv:r.consentVersion,cc:y};return r.cookieTypes.forEach(function(t){e[t.id]=!0}),e}function ne(){var e={ts:(new Date).toISOString(),cv:r.consentVersion,cc:y};return r.cookieTypes.forEach(function(t){e[t.id]=!!t.required}),e}function ce(e,t){if(t){var n=[];if(r.cookieTypes.forEach(function(c){c.required||t[c.id]&&!e[c.id]&&n.push(c.id)}),n.length)document.cookie.split(";").map(function(e){return e.trim().split("=")[0]}).filter(Boolean).forEach(function(e){var t=l(e);if(t&&-1!==n.indexOf(t.category)){var c=location.hostname,o="."+c;document.cookie=e+"=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/",document.cookie=e+"=; expires=Thu, 01 Jan 1970 00:00:00 GMT",document.cookie=e+"=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain="+o,document.cookie=e+"=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain="+c,document.cookie=e+"=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; Secure",r.cookieDomain&&(document.cookie=e+"=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain="+r.cookieDomain,document.cookie=e+"=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain="+r.cookieDomain+"; Secure");var a=c.split(".");if(a.length>2){var i="."+a.slice(1).join(".");document.cookie=e+"=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain="+i,document.cookie=e+"=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain="+i+"; Secure"}}})}}function oe(e){return(y=e?e.toUpperCase():null)?r.regionRules.gdpr&&-1!==r.regionRules.gdpr.indexOf(y)?"gdpr":r.regionRules.ccpa&&-1!==r.regionRules.ccpa.indexOf(y)?"ccpa":"none":"gdpr"}function re(e){if(window.__ccGeo)return e(oe(window.__ccGeo));r.geoApi?fetch(r.geoApi).then(function(e){return e.json()}).then(function(t){var n=t.country_code||t.country||"";n?e(oe(n)):ae(e)}).catch(function(){ae(e)}):ae(e)}function ae(e){var t="";try{t=Intl.DateTimeFormat().resolvedOptions().timeZone||""}catch(e){}return 0===t.indexOf("Europe/")||"Atlantic/Reykjavik"===t?(y=null,e("gdpr")):0===t.indexOf("US/")||0===t.indexOf("America/")&&["New_York","Chicago","Denver","Los_Angeles","Phoenix","Anchorage","Boise","Detroit","Indianapolis","Louisville","Menominee","Nome","Adak","Juneau","Sitka","Yakutat","Metlakatla","Kentucky","Indiana","North_Dakota"].some(function(e){return-1!==t.indexOf(e)})||"Pacific/Honolulu"===t?(y="US",e("ccpa")):(y=null,void e("gdpr"))}function ie(){"dark"===r.theme&&document.documentElement.classList.add("cc-dark"),"right"===r.iconPosition&&document.documentElement.classList.add("cc-icon-right")}function se(){var e=r.text,t=document.createElement("div");t.id="cc-overlay",document.body.appendChild(t);var n=document.createElement("div");n.id="cc-banner",n.setAttribute("role","dialog"),n.setAttribute("aria-modal","true"),n.setAttribute("aria-label","Cookie consent"),n.setAttribute("tabindex","-1");var c=r.cookieTypes.map(function(t){var n=t.required?' <span class="cc-always-on">'+b(e.alwaysOnLabel)+"</span>":"",c=i&&"marketing"===t.id?'<div class="cc-gpc-note">'+b(e.gpcPrefNote)+"</div>":"";return'<div class="cc-cat"><div><div class="cc-cat-label">'+b(t.name)+n+'</div><div class="cc-cat-desc">'+b(t.description||"")+"</div>"+c+'</div><label class="cc-toggle"><input type="checkbox" id="cc-cb-'+b(t.id)+'" aria-label="'+b(t.name)+'"'+(t.required||t.defaultValue?" checked":"")+(t.required?" disabled":"")+'><span class="cc-slider"></span></label></div>'}).join("");n.innerHTML='<div class="cc-inner"><p class="cc-title">'+b(e.title)+'</p><p class="cc-text">'+b(e.description)+' <a href="'+b(r.privacyUrl)+'">Privacy Policy</a></p><div class="cc-buttons"><button class="cc-btn cc-btn-accept" id="cc-accept">'+b(e.acceptAllButton)+"</button>"+(r.rejectFirstScreen?'<button class="cc-btn cc-btn-reject" id="cc-reject">'+b(e.rejectAllButton)+"</button>":"")+'<button class="cc-btn cc-btn-details" id="cc-details" aria-expanded="false">'+b(e.showDetailsButton)+'</button></div><div class="cc-categories" id="cc-cats">'+c+'</div><div class="cc-pref-buttons" id="cc-pref-buttons"><button class="cc-btn cc-btn-save" id="cc-save">'+b(e.saveButton)+'</button><button class="cc-btn cc-btn-decline" id="cc-decline">'+b(e.declineAllButton)+"</button></div></div>"+(r.showPoweredBy?'<div class="cc-powered"><a href="https://www.cookieflame.com" target="_blank" rel="noopener">Powered by CookieFlame</a></div>':""),document.body.appendChild(n);var o=document.createElement("button");o.id="cc-icon",o.setAttribute("aria-label","Cookie settings"),o.innerHTML='<svg width="24" height="24" viewBox="0 0 38 38" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M19.1 1.16c-.06-.42-.38-.77-.8-.83A11.8 11.8 0 0 0 13.28 1.2L7.5 4.3A11.8 11.8 0 0 0 3.95 7.98L1.09 13.9A11.8 11.8 0 0 0 .38 18.96l1.13 6.47a11.8 11.8 0 0 0 3.53 4.99l4.71 4.56a11.8 11.8 0 0 0 4.59 2.24l6.48.91a11.8 11.8 0 0 0 5.52-.89l5.78-3.09a11.8 11.8 0 0 0 3.55-3.69l2.87-5.9a11.8 11.8 0 0 0 .71-5.08c-.07-.41-.42-.73-.83-.79a14.8 14.8 0 0 1-8.16-7.61c-.14-.59-.63-1.05-1.23-1.14A14.8 14.8 0 0 1 19.1 1.16ZM15.25 10.25a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5Zm-5 15a2.5 2.5 0 1 1 5 0 2.5 2.5 0 0 1-5 0Zm17.5-5a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5Z" fill="currentColor"/></svg>',document.body.appendChild(o),document.getElementById("cc-accept").addEventListener("click",ke),document.getElementById("cc-decline").addEventListener("click",_e);var a=document.getElementById("cc-reject");a&&a.addEventListener("click",_e),document.getElementById("cc-save").addEventListener("click",xe),document.getElementById("cc-details").addEventListener("click",Ee),o.addEventListener("click",function(){he("gdpr")}),de("gdpr",n)}function de(e,t){A("cc:banner-rendered",{banner:e,element:t,state:E()})}function ue(){if(!document.getElementById("cc-ccpa-banner")){var e=r.text,t=document.createElement("div");t.id="cc-ccpa-banner",t.setAttribute("role","dialog"),t.setAttribute("aria-modal","true"),t.setAttribute("aria-label","Privacy notice"),t.setAttribute("tabindex","-1"),t.innerHTML='<div class="cc-ccpa-inner" id="cc-ccpa-notice"><div class="cc-ccpa-text"><p>'+b(e.ccpaDescription)+"</p>"+(i?'<p class="cc-ccpa-gpc">'+b(e.ccpaGpcNotice)+"</p>":"")+'</div><div class="cc-ccpa-actions"><a href="#" class="cc-ccpa-link" id="cc-ccpa-dns" role="button">'+b(e.ccpaDnsmpiLink)+'</a><button class="cc-btn cc-btn-accept" id="cc-ccpa-ok">'+b(e.ccpaAcceptButton)+'</button></div></div><div class="cc-ccpa-choice" id="cc-ccpa-choice"></div>'+(r.showPoweredBy?'<div class="cc-powered"><a href="https://www.cookieflame.com" target="_blank" rel="noopener">Powered by CookieFlame</a></div>':""),document.body.appendChild(t),document.getElementById("cc-ccpa-ok").addEventListener("click",function(){g&&z("ccpa_acknowledge",g),fe()}),document.getElementById("cc-ccpa-dns").addEventListener("click",function(e){e.preventDefault(),pe(!0)}),de("ccpa",t)}}function le(){var e=document.getElementById("cc-ccpa-choice");if(e){var t=r.text,n=!(g&&g.marketing),c='<div class="cc-ccpa-choice-inner"><p class="cc-ccpa-choice-title">'+b(t.ccpaChoiceTitle)+"</p>";c+=n?'<p class="cc-ccpa-choice-status">'+b(i?t.ccpaGpcOptedOut:t.ccpaOptedOut)+'</p><div class="cc-ccpa-choice-buttons"><button class="cc-btn cc-btn-accept" id="cc-ccpa-done">'+b(t.ccpaDoneButton)+'</button><button class="cc-btn cc-btn-decline" id="cc-ccpa-optin">'+b(t.ccpaOptInButton)+"</button></div>"+(i?'<p class="cc-ccpa-choice-note">'+b(t.ccpaOptInGpcNote)+"</p>":""):'<p class="cc-ccpa-choice-text">'+b(t.ccpaChoiceText)+'</p><div class="cc-ccpa-choice-buttons"><button class="cc-btn cc-btn-accept" id="cc-ccpa-optout">'+b(t.ccpaOptOutButton)+'</button><button class="cc-btn cc-btn-decline" id="cc-ccpa-cancel">'+b(t.ccpaCancelButton)+"</button></div>",c+='<button class="cc-btn cc-btn-details" id="cc-ccpa-more">'+b(t.ccpaMoreSettings)+"</button></div>",e.innerHTML=c;var o=function(e,t){var n=document.getElementById(e);n&&n.addEventListener("click",t)};o("cc-ccpa-done",fe),o("cc-ccpa-cancel",fe),o("cc-ccpa-optout",function(){var e=me(g);e.marketing=!1,Q(e,"ccpa_opt_out"),le(),ge("cc-ccpa-choice"),A("cc:update")}),o("cc-ccpa-optin",function(){var e=me(g);e.marketing=!0,i&&(e.gpcOptIn=1),delete e.gpc,Q(e,"ccpa_opt_in"),le(),ge("cc-ccpa-choice"),A("cc:update")}),o("cc-ccpa-more",function(){fe(),Ae()})}}function pe(e){var t=document.getElementById("cc-ccpa-notice"),n=document.getElementById("cc-ccpa-choice");t&&n&&(e&&le(),t.style.display=e?"none":"",n.classList.toggle("cc-open",!!e),e&&ge("cc-ccpa-choice"))}function fe(){var e=document.getElementById("cc-ccpa-banner");if(e&&e.classList.remove("cc-show"),pe(!1),document.removeEventListener("keydown",ve),ye&&"function"==typeof ye.focus)try{ye.focus()}catch(e){}ye=null}function ge(e){var t=document.getElementById(e),n=t&&t.querySelector("button");if(n)try{n.focus()}catch(e){}}function me(e){var t={ts:(new Date).toISOString(),cv:r.consentVersion,cc:y};return r.cookieTypes.forEach(function(n){t[n.id]=!e||!!e[n.id]}),e&&e.gpcOptIn&&(t.gpcOptIn=1),t}var ye=null;function ve(e){if("Tab"===e.key){var t=document.querySelector("#cc-banner.cc-show, #cc-ccpa-banner.cc-show");if(t){for(var n=[],c=t.querySelectorAll('button, a[href], input:not([disabled]), select, textarea, [tabindex]:not([tabindex="-1"])'),o=0;o<c.length;o++){for(var r=c[o],a=!1,i=r.parentElement;i&&i!==t;){if("none"===window.getComputedStyle(i).display){a=!0;break}i=i.parentElement}a||n.push(r)}if(0!==n.length){var s=n[0],d=n[n.length-1];e.shiftKey?document.activeElement===s&&(e.preventDefault(),d.focus()):document.activeElement===d&&(e.preventDefault(),s.focus())}}}}function he(e){if("ccpa"===e)return ye=document.activeElement,void requestAnimationFrame(function(){var e=document.getElementById("cc-ccpa-banner");if(e){e.classList.add("cc-show");var t=document.getElementById("cc-ccpa-choice");t&&t.classList.contains("cc-open")?ge("cc-ccpa-choice"):e.focus(),document.addEventListener("keydown",ve)}});ye=document.activeElement;var t=document.getElementById("cc-reject");t&&("gdpr"===m?t.style.removeProperty("display"):t.style.setProperty("display","none","important")),requestAnimationFrame(function(){var e=document.getElementById("cc-overlay"),t=document.getElementById("cc-banner"),n=document.getElementById("cc-icon");if(e&&e.classList.add("cc-show"),t){t.classList.add("cc-show");var c=t.querySelector(".cc-btn-accept");c&&c.focus(),document.addEventListener("keydown",ve)}n&&n.classList.remove("cc-show")})}function we(){var e=document.getElementById("cc-overlay"),t=document.getElementById("cc-banner"),n=document.getElementById("cc-icon");if(e&&e.classList.remove("cc-show"),t&&t.classList.remove("cc-show"),n&&"gdpr"===m&&n.classList.add("cc-show"),document.removeEventListener("keydown",ve),ye&&"function"==typeof ye.focus)try{ye.focus()}catch(e){}ye=null}function be(){var e=document.getElementById("cc-overlay"),t=document.getElementById("cc-banner"),n=document.getElementById("cc-icon");e&&e.classList.remove("cc-show"),t&&t.classList.remove("cc-show"),n&&n.classList.remove("cc-show"),document.removeEventListener("keydown",ve)}function ke(){var e=te();g&&g.gpcOptIn&&(e.gpcOptIn=1),Q(e,"accept_all"),we(),A("cc:accept-all"),A("cc:update")}function _e(){Q(ne(),"decline_all"),we(),A("cc:decline-all"),A("cc:update")}function xe(){var e=function(){var e={ts:(new Date).toISOString(),cv:r.consentVersion,cc:y};return r.cookieTypes.forEach(function(t){if(t.required)e[t.id]=!0;else{var n=document.getElementById("cc-cb-"+t.id);e[t.id]=!!n&&n.checked}}),e}();e.marketing&&(i||g&&g.gpcOptIn)&&(e.gpcOptIn=1),Q(e,"save_preferences"),we(),A("cc:save"),A("cc:update")}function Ee(){var e=document.getElementById("cc-cats"),t=document.getElementById("cc-details"),n=document.getElementById("cc-pref-buttons"),c=e.classList.toggle("cc-open");t.textContent=c?r.text.hideDetailsButton:r.text.showDetailsButton,t.setAttribute("aria-expanded",c?"true":"false"),n&&(c?n.classList.add("cc-open"):n.classList.remove("cc-open"))}function Ae(){document.getElementById("cc-banner")||(ie(),se(),be(),g&&X(g)),he("gdpr")}function Se(){"ccpa"===m?(ie(),ue(),he("ccpa"),pe(!0)):Ae()}function Ce(){var e=document.getElementById("cc-declaration");if(e){var t=E(),n={},c=document.cookie.split(";").map(function(e){return e.trim().split("=")[0]}).filter(Boolean);c=c.filter(function(e){return!f(e)});for(var o=0;o<c.length;o++){var a=l(c[o]);a&&(n[c[o]]={name:c[o],vendor:a.vendor,category:a.category,purpose:a.purpose,expiry:a.expiry||"",active:!0})}for(var i=0;i<u.length;i++){var s=u[i];s.name&&s.vendor&&s.category&&"unclassified"!==s.category&&!n[s.name]&&!f(s.name)&&(n[s.name]={name:s.name,vendor:s.vendor,category:s.category,purpose:s.purpose||"",expiry:s.expiry||"",active:!1})}for(var d=0;d<c.length;d++)n[c[d]]||(n[c[d]]={name:c[d],vendor:"",category:"unclassified",purpose:"",expiry:"",active:!0});var p=["necessary","preferences","analytics","marketing"],g={};r.cookieTypes.forEach(function(e){g[e.id]=e.name});var m={};p.forEach(function(e){m[e]=[]}),Object.keys(n).forEach(function(e){var t=n[e],c=t.category;m[c]||(m[c]=[]),m[c].push(t)}),p.forEach(function(e){m[e]&&m[e].sort(function(e,t){return e.name.localeCompare(t.name)})});var y,v="<div>";if(t.timestamp){var h=new Date(t.timestamp);v+="<p>"+b("Your consent was last updated on "+h.toLocaleDateString(void 0,{year:"numeric",month:"long",day:"numeric"})+" at "+h.toLocaleTimeString(void 0,{hour:"2-digit",minute:"2-digit"})+".")+"</p>"}else v+="<p>"+b("You have not yet made a cookie preference choice on this site.")+"</p>";for(var w=0;w<p.length;w++){var k=p[w],_=m[k];if(_&&_.length){var x,A=g[k]||k,S=r.cookieTypes.filter(function(e){return e.id===k})[0],C=S&&S.description||"";x=!!S&&S.required?"Always active":t[k]?"Active":"Inactive",v+="<div>",v+="<p><b>"+b(A)+"</b> ("+b(x)+")</p>",C&&(v+="<p>"+b(C)+"</p>"),v+="<table><thead><tr>",v+="<th>Name</th><th>Provider</th><th>Purpose</th><th>Expiry</th>",v+="</tr></thead><tbody>";for(var I=0;I<_.length;I++){var L=_[I];v+="<tr>",v+="<td><code>"+b(L.name)+"</code></td>",v+="<td>"+b(L.vendor||"Unknown")+"</td>",v+="<td>"+b(L.purpose||"Not specified")+"</td>",v+="<td>"+b(L.expiry||"—")+"</td>",v+="</tr>"}v+="</tbody></table></div>"}}if(r.visitorLogins){var B=[{name:"wordpress_logged_in_[hash]",purpose:"Keeps you logged in",expiry:y="Session, or 14 days with Remember Me"},{name:"https:"===location.protocol?"wordpress_sec_[hash]":"wordpress_[hash]",purpose:"Confirms your login when you open your account dashboard",expiry:y},{name:"wordpress_test_cookie",purpose:"Checks your browser accepts cookies before you log in",expiry:"Session"},{name:"wp_lang",purpose:"Remembers the language you picked on the login page",expiry:"Session"},{name:"wp-settings-[user ID]",purpose:"Remembers your dashboard layout settings",expiry:"1 year"},{name:"wp-settings-time-[user ID]",purpose:"Records when your dashboard settings were last saved",expiry:"1 year"}];v+='<div class="cc-declaration-login">',v+="<p><b>"+b(r.text.loginCookiesTitle)+"</b></p>",v+="<p>"+b(r.text.loginCookiesIntro)+"</p>",v+="<table><thead><tr>",v+="<th>Name</th><th>Provider</th><th>Purpose</th><th>Expiry</th>",v+="</tr></thead><tbody>";for(var O=0;O<B.length;O++)v+="<tr>",v+="<td><code>"+b(B[O].name)+"</code></td>",v+="<td>WordPress</td>",v+="<td>"+b(B[O].purpose)+"</td>",v+="<td>"+b(B[O].expiry)+"</td>",v+="</tr>";v+="</tbody></table></div>"}v+='<p><button type="button" class="cc-open-settings cc-declaration-change">'+b(r.text.changePreferences)+"</button></p>",v+="</div>",e.innerHTML=v}}document.addEventListener("click",function(e){(e.target&&e.target.closest?e.target.closest(".cc-open-settings"):null)&&(e.preventDefault(),Se())}),window.__ccInternal={getResourceSnapshot:Oe,scriptLoaded:function(e){for(var t=Be||Oe(),n=0;n<t.allUrls.length;n++)if(-1!==t.allUrls[n].indexOf(e))return!0;return!1},scriptLoadedUrls:function(e){for(var t=Be||Oe(),n=[],c=0;c<t.allUrls.length;c++)-1!==t.allUrls[c].indexOf(e)&&n.push(t.allUrls[c]);return n},hasCookie:function(e){return document.cookie.split(";").some(function(t){return t.trim().split("=")[0]===e})},hasCookiePrefix:function(e){return document.cookie.split(";").some(function(t){return 0===t.trim().split("=")[0].indexOf(e)})},setResourceSnapshot:function(e){Be=e},visitorCookies:function(e){for(var t=Oe().thirdPartyDomains,n={},c=0;c<e.length;c++)De(e[c],t,Pe(e[c]))&&(n[e[c]]=!0);return n},facadeBridge:{getConfig:function(){return r},getConsentState:function(){return g},loadConsent:ee,saveConsent:function(e,t){Q(e,t)},hideAllBanners:be,registerRenderer:function(e){V=e,j=2,J()}}},window.CookieConsent={getState:E,getConfig:function(){return r},getComplianceMode:function(){return m},getCountry:function(){return y},show:Ae,showSettings:Se,acceptAll:ke,declineAll:_e,getGPC:function(){return i},getConsentVersion:function(){return r.consentVersion},on:function(e,t){window.addEventListener(e,t)},off:function(e,t){window.removeEventListener(e,t)},VERSION:o},window.addEventListener("cc:ready",Ce),window.addEventListener("cc:update",Ce),function(){if(!window.__ccDefaultsSet){var e={wait_for_update:500};r.cookieTypes.forEach(function(t){t.consentMode&&t.consentMode.forEach(function(n){e[n]=t.required?"granted":"denied"})}),S("consent","default",e)}}(),function(){if(r.tcfEnabled){var e=window.__tcfapi&&window.__tcfapi.queue?window.__tcfapi.queue:[];window.__tcfapi=O,e.forEach(function(e){O.apply(null,e)}),L=!0,window===window.top&&window.addEventListener("message",function(e){var t;try{t="string"==typeof e.data?JSON.parse(e.data):e.data}catch(e){return}if(t&&t.__tcfapiCall){var n=t.__tcfapiCall;O(n.command,n.version,function(t,c){var o={__tcfapiReturn:{returnValue:t,success:c,callId:n.callId}};e.source&&e.source.postMessage("string"==typeof e.data?JSON.stringify(o):o,"*")},n.parameter)}})}}();var Ie=ee();function Le(){var e=window.__ccDiscovery&&window.__ccDiscovery.active;if(!e&&(window.__ccDNT||document.cookie.split(";").some(function(e){return 0===e.trim().indexOf("cookieflame_dnt=")})))return C(g=ne()),ce(g,te()),Y(),$(),m="dnt",ie(),se(),be(),X(g),A("cc:ready"),void Ce();if(e){C(g=te()),D(g),Y(),K(g,null),r.tcfEnabled&&T(g),window.dataLayer=window.dataLayer||[];for(var t=["consent_accepted_analytics","consent_accepted_marketing","consent_accepted_preferences","consent_accepted_necessary","consent_update","consent_given","cookie_consent_given","cookie_consent_update","consent_accepted"],n=0;n<t.length;n++)window.dataLayer.push({event:t[n]});return m="discovery",ie(),se(),be(),A("cc:ready"),A("cc:update"),void Ce()}var c=ee(),o=!1;if(c)if(!i||c.gpcOptIn||!c.marketing&&c.gpc||(o=!!c.marketing,s(c),_(r.cookieName,JSON.stringify(c),r.cookieExpiry)),g=c,C(c),D(c),Y(),$(),K(c,null),r.tcfEnabled&&T(c),ie(),se(),be(),X(c),c.cc){if("gdpr"===(m=oe(c.cc))){var a=document.getElementById("cc-icon");a&&a.classList.add("cc-show")}o&&z("gpc_optout",c),A("cc:ready"),Ce()}else re(function(e){if(m=e,"gdpr"===e){var t=document.getElementById("cc-icon");t&&t.classList.add("cc-show")}o&&z("gpc_optout",c),A("cc:ready"),Ce()});else re(function(e){return m=e,"none"===e?(Y(),Q(te(),"auto_grant_non_eu"),A("cc:ready"),A("cc:update"),void Ce()):"ccpa"===e?(Y(),i?Q(s(te()),"ccpa_gpc_optout"):Q(te(),"ccpa_auto_grant"),ie(),ue(),he("ccpa"),A("cc:ready"),A("cc:update"),void Ce()):(ie(),Y(),$(),se(),he("gdpr"),A("cc:ready"),void Ce())})}Ie&&(i&&!Ie.gpcOptIn&&(Ie.marketing=!1),g=Ie,C(Ie)),function(){if(!h&&r.autoBlockPatterns&&r.autoBlockPatterns.length){h=!0;var e=document.createElement.bind(document),t=Object.getOwnPropertyDescriptor(HTMLScriptElement.prototype,"src");t&&t.set&&(document.createElement=function(){var n=e.apply(document,arguments);if("script"!==(arguments[0]||"").toLowerCase())return n;function c(e,t){if(t&&"string"==typeof t&&!e.getAttribute("data-cc")&&!e.getAttribute("data-cc-auto")&&-1===t.indexOf("cookieconsent")){var n=P(t);!n||g&&g[n]||(e.type="text/plain",e.setAttribute("data-cc",n),e.setAttribute("data-cc-auto","1"))}}Object.defineProperty(n,"src",{configurable:!0,enumerable:!0,get:function(){return t.get.call(this)},set:function(e){return c(this,e),t.set.call(this,e)}});var o=n.setAttribute.bind(n);return n.setAttribute=function(e,t){return"src"===e.toLowerCase()&&c(n,t),o(e,t)},n}),new MutationObserver(function(e){e.forEach(function(e){e.addedNodes.forEach(function(e){if(1===e.nodeType){var t=[];"SCRIPT"===e.tagName&&t.push(e),e.querySelectorAll&&e.querySelectorAll("script[src]").forEach(function(e){t.push(e)}),t.forEach(function(e){if(!(e.getAttribute("data-cc-auto")||e.getAttribute("data-cc")||e.src&&-1!==e.src.indexOf("cookieconsent"))){var t=P(e.src);t&&(g&&g[t]||(e.type="text/plain",e.setAttribute("data-cc",t),e.setAttribute("data-cc-auto","1")))}})}})})}).observe(document.documentElement,{childList:!0,subtree:!0})}}(),"loading"===document.readyState?document.addEventListener("DOMContentLoaded",Le):Le();var Be=null;function Oe(){for(var e=[],t=[],n=[],c={},o={},r=document.querySelectorAll("script[src]"),a=0;a<r.length;a++)r[a].src&&e.push(r[a].src);for(var i=document.querySelectorAll("iframe[src]"),s=0;s<i.length;s++)i[s].src&&e.push(i[s].src);if(window.performance&&performance.getEntriesByType)for(var d=performance.getEntriesByType("resource"),u=0;u<d.length;u++){var l=d[u].name||"";if(l){-1===e.indexOf(l)&&e.push(l);var p=d[u].initiatorType||"";if("script"===p||"iframe"===p||"xmlhttprequest"===p||"fetch"===p||-1!==l.indexOf(".js"))try{var f=new URL(l),g=f.hostname;if(g===location.hostname||g==="www."+location.hostname||"www."+g===location.hostname){var m=f.pathname.match(/\/wp-content\/plugins\/([^\/]+)\//);m&&!o[m[1]]&&(o[m[1]]=!0,n.push({slug:m[1],url:l}))}else c[g]||(c[g]=!0,t.push(g))}catch(e){}}}return{allUrls:e,thirdPartyDomains:t,pluginScripts:n}}var Te={};function Pe(e){var t={set_by:"",set_by_plugin:""},n=window.__ccCookieSetters;if(!n||!Object.prototype.hasOwnProperty.call(n,e)||!n[e])return t;try{var c=new URL(String(n[e]).replace(/:\d+(:\d+)?$/,""));if(/^(chrome-extension|moz-extension|safari-web-extension|safari-extension):$/.test(c.protocol))return t.set_by="browser-extension",t;var o=c.hostname;if(o===location.hostname||o==="www."+location.hostname||"www."+o===location.hostname){t.set_by="first-party";var r=c.pathname.match(/\/wp-content\/(?:plugins|themes)\/([^\/]+)\//);r&&(t.set_by_plugin=r[1].toLowerCase())}else t.set_by=o.toLowerCase()}catch(e){}return t}function De(e,t,n){if("browser-extension"===n.set_by)return!0;var c=window.__ccVisitorCookieRules;if(!c||!c.tools)return!1;if(c.allow&&-1!==c.allow.indexOf(e))return!1;if(n.set_by)return!1;for(var o=0;o<c.tools.length;o++){for(var r=c.tools[o],a=-1!==(r.n||[]).indexOf(e),i=0;!a&&i<(r.p||[]).length;i++)0===e.indexOf(r.p[i])&&(a=!0);if(a){for(var s=0;s<t.length;s++)for(var d=0;d<(r.d||[]).length;d++){var u=String(t[s]).toLowerCase(),l=r.d[d];if(u===l||u.slice(-l.length-1)==="."+l)return!1}return!0}}return!1}function Re(){var t=window.__ccClassified||[],n=window.__ccUnclassified||[],c=window.__ccBeaconUrl;if(c){var o,r={};for(o=0;o<t.length;o++)r[t[o]]=!0;for(o=0;o<n.length;o++)r[n[o]]=!0;var a=Object.keys(Te);for(o=0;o<a.length;o++)r[a[o]]=!0;var i=Object.keys(d);for(o=0;o<i.length;o++)r[i[o]]=!0;var s={},u=[],l=document.cookie.split(";");for(o=0;o<l.length;o++){var p=l[o].indexOf("="),f=(p>-1?l[o].substring(0,p):l[o]).trim(),g=p>-1?l[o].substring(p+1).trim():"";f&&(u.push(f),s[f]=g)}var m=[];for(o=0;o<u.length;o++)if(!r[u[o]]){for(var y=!1,v=Object.keys(d),h=0;h<v.length;h++)if(d[v[h]].prefix&&0===u[o].indexOf(v[h])){y=!0;break}y||m.push(u[o])}if(0!==m.length){for(var w=Oe(),b=w.thirdPartyDomains,k=[],_=0;_<w.pluginScripts.length;_++)k.push(w.pluginScripts[_].slug);for(var x=location.pathname,E=0;E<m.length;E++){var A=m[E],S=!!e[A]&&!r[A],C=Me(s[A]||""),I=Pe(A);if(!De(A,b,I)||window.__ccDiscovery&&window.__ccDiscovery.active){var L=JSON.stringify({cookie_name:A,script_domains:b,plugin_scripts:k,page_path:x,pre_existing:S,value_shape:C,set_by:I.set_by,set_by_plugin:I.set_by_plugin});Te[A]=!0,navigator.sendBeacon&&navigator.sendBeacon(c,new Blob([L],{type:"application/json"}))}else Te[A]=!0}}}}function Me(e){if(!e||0===e.length)return"empty";if("true"===e||"false"===e||"1"===e||"0"===e)return"boolean";if(/^\d+$/.test(e))return e.length>12?"long-numeric":"numeric";if(/^\d+\.\d+$/.test(e))return"decimal";if(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(e))return"uuid";if(/^[A-Za-z0-9+\/]{20,}={0,2}$/.test(e))return"base64";if(/^(GA\d+\.\d+\.\d+\.\d+|G-[A-Z0-9]+)/.test(e))return"ga-id";try{if("object"==typeof JSON.parse(decodeURIComponent(e)))return"json"}catch(e){}return/^[a-f0-9]{24,}$/i.test(e)?"hex-id":/^\d{10,13}$/.test(e)?"timestamp":e.length<=20?"short-string":e.length<=100?"medium-string":"long-string"}setTimeout(Re,3e3),setTimeout(Re,15e3),setTimeout(Re,45e3),window.addEventListener("cc:update",function(){setTimeout(Re,3e3)}),window.addEventListener("pagehide",Re)}();
  • cookieflame/trunk/cookieflame.php

    r3769546 r3771537  
    33 * Plugin Name:       CookieFlame
    44 * Plugin URI:        https://cookieflame.com
    5  * Description:       A lightweight, self-hosted cookie consent manager with Google Consent Mode v2, IAB TCF v2.2, automatic script blocking, multi-region compliance (GDPR/CCPA/LGPD/POPIA), passive cookie monitoring, and server-side geo detection via Cloudflare.
    6  * Version:           5.3.0
     5 * Description:       A lightweight, self-hosted cookie consent manager with Google Consent Mode v2, automatic script blocking, multi-region compliance (GDPR/CCPA/LGPD/POPIA), passive cookie monitoring, and server-side geo detection via Cloudflare.
     6 * Version:           5.4.0
    77 * Requires at least: 6.0
    88 * Requires PHP:      8.0
    … …  
    1919}
    2020
    21 define( 'COOKIEFLAME_VERSION', '5.3.0' );
     21define( 'COOKIEFLAME_VERSION', '5.4.0' );
    2222define( 'COOKIEFLAME_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
    2323define( 'COOKIEFLAME_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
    … …  
    2727
    2828/* ═══════════════════════════════════════════════════════════
    29    MONITORING INTERVAL BUCKETS (in minutes)
    30    A single 'digest_interval' option drives both beacon
    31    processing cadence and email digest cadence. Value 0 = off.
    32    Free tier: off, daily, weekly.
    33    Pro tier:  free buckets + 15m, 1h, 4h, monthly (added via the
    34               cookieflame_interval_buckets filter by the Pro plugin).
     29   COOKIE MONITORING SETTINGS (v5.4.0)
     30   Two separate settings on the Cookie Monitoring tab:
     31     - Watch for new cookies: On/Off. Visitors' browsers report
     32       new cookies as they browse, processed straight away.
     33       Stored as the inverse of 'monitoring_paused'.
     34     - Email me about changes: 'weekly' (default), 'daily',
     35       'instant' or 'off'. Stored as 'email_frequency'.
     36   Before v5.4.0 a single 'digest_interval' (minutes, 0 = off)
     37   drove both. cookieflame_migrate_monitoring_settings() converts
     38   it once; until then the getters derive the same answer from it.
     39
     40   The cookieflame_interval_buckets filter is kept: a plugin that
     41   adds a bucket shorter than a day (Pro 1.5.x adds 15 minutes)
     42   makes "Instantly" emails possible, sent after that delay.
    3543═══════════════════════════════════════════════════════════ */
    3644
    … …  
    5361 */
    5462function cookieflame_interval_buckets_free() {
     63    // v5.4.0: the 15-minute, 1-hour and 4-hour entries are no longer
     64    // declared here. Pro 1.5.x still contributes them through the filter.
    5565    return array(
    5666        array( 'minutes' => 0,     'tier' => 'free' ),
    57         array( 'minutes' => 15,    'tier' => 'pro' ),
    58         array( 'minutes' => 60,    'tier' => 'pro' ),
    59         array( 'minutes' => 240,   'tier' => 'pro' ),
    6067        array( 'minutes' => 1440,  'tier' => 'free' ),
    6168        array( 'minutes' => 10080, 'tier' => 'free' ),
    … …  
    159166
    160167/**
    161  * Return the currently configured monitoring interval in minutes,
    162  * normalised to a valid bucket for the current tier. Defaults to
    163  * daily (1440) on new installs with no value set.
     168 * Delay before the next email summary, in minutes (0 = no email).
     169 * Kept under its pre-5.4.0 name for any code that still calls it.
    164170 */
    165171function cookieflame_current_interval_minutes() {
    166     $settings = get_option( 'cookieflame_settings', array() );
    167     $raw      = isset( $settings['digest_interval'] ) ? intval( $settings['digest_interval'] ) : 1440;
    168     return cookieflame_normalise_interval( $raw );
    169 }
    170 
     172    return cookieflame_email_delay_minutes();
     173}
     174
     175/**
     176 * Whether "Watch for new cookies" is off. Every beacon, queue and
     177 * digest path gates on this, so turning it off silences them all.
     178 */
    171179function cookieflame_monitoring_is_off() {
    172     if ( cookieflame_monitoring_is_paused() ) {
    173         return true;
    174     }
    175     return 0 === cookieflame_current_interval_minutes();
    176 }
    177 
    178 /**
    179  * Whether monitoring is currently paused. Paused is a temporary
    180  * suppression state — distinct from "Off" — that keeps the chosen
    181  * update interval configured but silences beacons and digests while
    182  * the admin does site surgery. Admin toggles this from the Cookie
    183  * Monitoring tab. cookieflame_monitoring_is_off() folds this into the same
    184  * gate so every downstream path inherits pause automatically.
     180    return cookieflame_monitoring_is_paused();
     181}
     182
     183/**
     184 * Whether "Watch for new cookies" is off (stored as 'monitoring_paused',
     185 * the flag behind the On/Off switch on the Cookie Monitoring tab). A
     186 * pre-5.4.0 'digest_interval' of 0 also means off until migrated.
    185187 */
    186188function cookieflame_monitoring_is_paused() {
    187189    $settings = get_option( 'cookieflame_settings', array() );
    188     return ! empty( $settings['monitoring_paused'] );
    189 }
     190    if ( ! empty( $settings['monitoring_paused'] ) ) {
     191        return true;
     192    }
     193    if ( ! isset( $settings['email_frequency'] ) && isset( $settings['digest_interval'] ) && intval( $settings['digest_interval'] ) <= 0 ) {
     194        return true;
     195    }
     196    return false;
     197}
     198
     199/**
     200 * The saved "Email me about changes" choice: 'weekly', 'daily',
     201 * 'instant' or 'off'. Sites not yet migrated get the answer their old
     202 * 'digest_interval' maps to (15/60/240 minutes map to 'instant').
     203 */
     204function cookieflame_email_frequency() {
     205    $settings = get_option( 'cookieflame_settings', array() );
     206    if ( isset( $settings['email_frequency'] ) ) {
     207        return cookieflame_sanitize_email_frequency( $settings['email_frequency'] );
     208    }
     209    if ( ! isset( $settings['digest_interval'] ) ) {
     210        return 'weekly';
     211    }
     212    return cookieflame_email_frequency_from_interval( $settings['digest_interval'], true );
     213}
     214
     215/**
     216 * Map a pre-5.4.0 'digest_interval' to an email frequency. Intervals
     217 * under a day become 'instant' when $instant_ok, otherwise 'daily'.
     218 */
     219function cookieflame_email_frequency_from_interval( $minutes, $instant_ok ) {
     220    $minutes = intval( $minutes );
     221    if ( $minutes <= 0 ) {
     222        return 'off';
     223    }
     224    if ( 10080 === $minutes ) {
     225        return 'weekly';
     226    }
     227    if ( $minutes < 1440 ) {
     228        return $instant_ok ? 'instant' : 'daily';
     229    }
     230    return 'daily';
     231}
     232
     233function cookieflame_sanitize_email_frequency( $value ) {
     234    $value = is_string( $value ) ? $value : '';
     235    return in_array( $value, array( 'weekly', 'daily', 'instant', 'off' ), true ) ? $value : 'weekly';
     236}
     237
     238/**
     239 * Shortest sub-daily bucket another plugin added through the
     240 * cookieflame_interval_buckets filter, in minutes, or 0 when none.
     241 */
     242function cookieflame_instant_bucket_minutes() {
     243    $shortest = 0;
     244    foreach ( cookieflame_interval_buckets() as $bucket ) {
     245        $minutes = intval( $bucket['minutes'] );
     246        if ( $minutes > 0 && $minutes < 1440 && ( 0 === $shortest || $minutes < $shortest ) ) {
     247            $shortest = $minutes;
     248        }
     249    }
     250    return $shortest;
     251}
     252
     253/**
     254 * Whether "Instantly" can be chosen on this site: CookieFlame Pro is
     255 * active and a plugin supplies a delay shorter than a day.
     256 */
     257function cookieflame_instant_email_available() {
     258    return cookieflame_is_premium() && cookieflame_instant_bucket_minutes() > 0;
     259}
     260
     261/**
     262 * Minutes from the first new cookie to the email summary, or 0 when
     263 * emails are off. 'instant' uses the shortest contributed bucket and
     264 * falls back to daily when there is none.
     265 *
     266 * Filter: cookieflame_email_delay_minutes ( int $minutes, string $frequency ).
     267 */
     268function cookieflame_email_delay_minutes() {
     269    $frequency = cookieflame_email_frequency();
     270    $map       = array( 'off' => 0, 'daily' => 1440, 'weekly' => 10080 );
     271    if ( 'instant' === $frequency ) {
     272        $minutes = cookieflame_instant_bucket_minutes();
     273        $minutes = $minutes > 0 ? $minutes : 1440;
     274    } else {
     275        $minutes = $map[ $frequency ];
     276    }
     277    return max( 0, intval( apply_filters( 'cookieflame_email_delay_minutes', $minutes, $frequency ) ) );
     278}
     279
     280/**
     281 * One-time conversion of the pre-5.4.0 'digest_interval' setting:
     282 *   0             -> watch Off, email Off
     283 *   daily, weekly -> watch On, email unchanged
     284 *   15m, 1h, 4h   -> 'instant' when Pro is active, 'daily' otherwise
     285 * 'digest_interval' itself is left in place.
     286 */
     287function cookieflame_migrate_monitoring_settings() {
     288    $settings = get_option( 'cookieflame_settings', false );
     289    if ( ! is_array( $settings ) || isset( $settings['email_frequency'] ) ) {
     290        return;
     291    }
     292    if ( isset( $settings['digest_interval'] ) ) {
     293        $minutes = intval( $settings['digest_interval'] );
     294        $settings['email_frequency'] = cookieflame_email_frequency_from_interval( $minutes, $minutes > 0 && $minutes < 1440 && cookieflame_is_premium() );
     295        if ( $minutes <= 0 ) {
     296            $settings['monitoring_paused'] = 1;
     297        }
     298    } else {
     299        $settings['email_frequency'] = 'weekly';
     300    }
     301    update_option( 'cookieflame_settings', $settings );
     302}
     303add_action( 'admin_init', 'cookieflame_migrate_monitoring_settings', 5 );
    190304
    191305/**
    … …  
    209323    $host      = is_string( $host ) ? strtolower( $host ) : '';
    210324    return substr( sha1( $host . '|' . $page_path ), 0, 16 );
     325}
     326
     327/**
     328 * Version string for one of the plugin's own scripts or stylesheets: the
     329 * plugin version plus the file's modification time, so a changed file
     330 * always gets a new URL and no browser or CDN cache serves an old copy.
     331 */
     332function cookieflame_asset_version( $relative_path ) {
     333    $file  = COOKIEFLAME_PLUGIN_DIR . $relative_path;
     334    $mtime = file_exists( $file ) ? filemtime( $file ) : false;
     335    return $mtime ? COOKIEFLAME_VERSION . '.' . $mtime : COOKIEFLAME_VERSION;
    211336}
    212337
    … …  
    337462add_action( 'admin_post_cookieflame_save_notifications', 'cookieflame_save_notifications' );
    338463add_action( 'admin_post_cookieflame_toggle_pause',        'cookieflame_toggle_monitoring_pause' );
     464add_action( 'admin_post_cookieflame_dismiss_upgrade_banner', 'cookieflame_dismiss_upgrade_banner' );
     465
     466/**
     467 * Hide the "Upgrade to Pro" banner at the top of the CookieFlame tabs
     468 * for the current user.
     469 */
     470function cookieflame_dismiss_upgrade_banner() {
     471    if ( ! current_user_can( 'manage_options' ) ) {
     472        wp_die( esc_html__( 'You do not have permission to do this.', 'cookieflame' ) );
     473    }
     474    check_admin_referer( 'cookieflame_dismiss_upgrade_banner' );
     475    update_user_meta( get_current_user_id(), 'cookieflame_upgrade_banner_dismissed', time() );
     476    $tab = sanitize_key( wp_unslash( $_POST['tab'] ?? 'cookies' ) );
     477    wp_safe_redirect( add_query_arg( array( 'page' => 'cookieflame', 'tab' => $tab ? $tab : 'cookies' ), admin_url( 'admin.php' ) ) );
     478    exit;
     479}
    339480
    340481/**
    … …  
    11931334    //   - The beacon carries a script_domain or plugin_script not yet
    11941335    //     recorded for this (URL, cookie) pair
    1195     //   - The configured update interval has elapsed since last_processed
     1336    //   - A day has passed since last_processed (heartbeat; this only
     1337    //     refreshes "last seen", which Tidy up relies on)
    11961338    //
    11971339    // Otherwise we silently drop the beacon.
    11981340    //
    1199     // Monitoring off (interval = 0) is handled at enqueue time — the
     1341    // Watch for new cookies Off is handled at enqueue time — the
    12001342    // beacon URL is not injected so nothing fires. We defensively drop
    12011343    // here too in case a stale page serves the URL from cache.
    … …  
    12081350    $url_hash    = cookieflame_url_hash_for_path( $page_path );
    12091351    $now         = time();
    1210     $interval_s  = cookieflame_current_interval_minutes() * 60;
     1352    $interval_s  = DAY_IN_SECONDS; // Heartbeat: fixed daily since v5.4.0, independent of the email setting.
    12111353
    12121354    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom plugin table; high-write-rate beacon storage where the object cache adds no value (writes invalidate cache on every visitor request, reads are admin-only and infrequent).
    … …  
    14041546 */
    14051547function cookieflame_queue_notification( $cookie_name, $script_domains, $page_path, $pre_existing = false, $value_shape = '' ) {
    1406     // Monitoring off -> do not queue and do not schedule a digest.
    1407     if ( cookieflame_monitoring_is_off() ) {
     1548    // Watching off, or emails off -> do not queue and do not schedule a digest.
     1549    if ( cookieflame_monitoring_is_off() || 0 === cookieflame_email_delay_minutes() ) {
    14081550        return;
    14091551    }
    … …  
    14221564    update_option( 'cookieflame_notification_queue', $queue );
    14231565
    1424     // Start digest timer if not already running. The same update_interval
    1425     // setting drives both beacon filtering and digest cadence.
     1566    // Start digest timer if not already running, from the
     1567    // "Email me about changes" setting.
    14261568    $scheduled = get_option( 'cookieflame_digest_scheduled', 0 );
    14271569    if ( empty( $scheduled ) ) {
    1428         $interval = cookieflame_current_interval_minutes() * 60;
     1570        $interval = cookieflame_email_delay_minutes() * 60;
    14291571        if ( $interval <= 0 ) {
    14301572            return;
    … …  
    16011743    // invoking the hook directly. Silently clean up and return — the
    16021744    // queue is preserved for resume.
    1603     if ( cookieflame_monitoring_is_off() ) {
     1745    if ( cookieflame_monitoring_is_off() || 0 === cookieflame_email_delay_minutes() ) {
    16041746        delete_option( 'cookieflame_digest_scheduled' );
    16051747        return;
    … …  
    16851827
    16861828    // Header
    1687     $body  = "CookieFlame Cookie Discovery Report\n";
     1829    $body  = "CookieFlame: new cookies found by Watch for new cookies\n";
    16881830    $body .= $site_name . " (" . $site_url . ")\n";
    16891831    $body .= wp_date( 'j M Y, H:i' ) . "\n";
    … …  
    18131955    $body .= "-- \n";
    18141956    $body .= "CookieFlame v" . COOKIEFLAME_VERSION . " on " . $site_name . "\n";
    1815     $body .= "You are receiving this because Cookie Monitoring is enabled.\n";
     1957    $body .= "You are receiving this because \"Email me about changes\" is on in Cookie Monitoring.\n";
    18161958    $body .= "Change settings: " . admin_url( 'admin.php?page=cookieflame&tab=notifications' ) . "\n";
    18171959
    1818     wp_mail( $to, $subject, $body );
     1960    /**
     1961     * Filter the email summary before it is sent.
     1962     *
     1963     * Return an array with 'to', 'subject', 'body' and 'headers' to change
     1964     * the email, or false to skip sending it (the queue is still cleared).
     1965     *
     1966     * @param array $email      { to, subject, body, headers }.
     1967     * @param array $actionable Queue entries that need a decision.
     1968     * @param array $residual   Queue entries from plugins no longer active.
     1969     */
     1970    $email = apply_filters( 'cookieflame_digest_email', array(
     1971        'to'      => $to,
     1972        'subject' => $subject,
     1973        'body'    => $body,
     1974        'headers' => array(),
     1975    ), $actionable, $residual );
     1976
     1977    if ( is_array( $email ) && ! empty( $email['to'] ) ) {
     1978        wp_mail( $email['to'], (string) ( $email['subject'] ?? $subject ), (string) ( $email['body'] ?? $body ), $email['headers'] ?? array() );
     1979    }
    18191980
    18201981    // Clear the queue and timer
    … …  
    22182379        COOKIEFLAME_PLUGIN_URL . 'assets/css/admin.css',
    22192380        array(),
    2220         COOKIEFLAME_VERSION
     2381        cookieflame_asset_version( 'assets/css/admin.css' )
    22212382    );
    22222383    wp_enqueue_script(
    … …  
    22242385        COOKIEFLAME_PLUGIN_URL . 'assets/js/admin.js',
    22252386        array(),
    2226         COOKIEFLAME_VERSION,
     2387        cookieflame_asset_version( 'assets/js/admin.js' ),
    22272388        true
    22282389    );
    … …  
    22532414    $cookieflame_js_data = apply_filters( 'cookieflame_admin_js_data', $cookieflame_js_data );
    22542415    wp_localize_script( 'cookieflame-admin', 'cookieflameAdmin', $cookieflame_js_data );
     2416
     2417    // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Tab navigation, no data modification.
     2418    if ( isset( $_GET['tab'] ) && 'preview' === sanitize_key( wp_unslash( $_GET['tab'] ) ) ) {
     2419        wp_enqueue_script(
     2420            'cookieflame-admin-preview',
     2421            COOKIEFLAME_PLUGIN_URL . 'assets/js/admin-preview.js',
     2422            array(),
     2423            cookieflame_asset_version( 'assets/js/admin-preview.js' ),
     2424            true
     2425        );
     2426        wp_localize_script( 'cookieflame-admin-preview', 'cookieflamePreview', array(
     2427            'privacyUrl'        => $opts['privacy_url'] ?? '/privacy-policy',
     2428            'theme'             => $opts['theme'] ?? 'light',
     2429            'rejectFirstScreen' => ! empty( $opts['show_reject_first'] ),
     2430            'showPoweredBy'     => ! empty( $opts['show_powered_by'] ),
     2431        ) );
     2432    }
    22552433}
    22562434
    … …  
    23052483}
    23062484
     2485/**
     2486 * [cookieflame_settings_link text="Cookie settings"]: a link that reopens
     2487 * the visitor's cookie choices (the US "Do Not Sell or Share" choice for
     2488 * US visitors). Any element with the class cc-open-settings does the same,
     2489 * for example a menu item.
     2490 */
     2491add_shortcode( 'cookieflame_settings_link', 'cookieflame_settings_link_shortcode' );
     2492
     2493function cookieflame_settings_link_shortcode( $atts ) {
     2494    $atts = shortcode_atts( array(
     2495        'text'  => __( 'Cookie settings', 'cookieflame' ),
     2496        'class' => '',
     2497    ), $atts, 'cookieflame_settings_link' );
     2498    $class = trim( 'cc-open-settings ' . implode( ' ', array_map( 'sanitize_html_class', preg_split( '/\s+/', (string) $atts['class'], -1, PREG_SPLIT_NO_EMPTY ) ) ) );
     2499    return '<a href="#" class="' . esc_attr( $class ) . '" role="button">' . esc_html( $atts['text'] ) . '</a>';
     2500}
     2501
    23072502/* ═══════════════════════════════════════════════════════════
    23082503   REST API — GEO ENDPOINT
    … …  
    23392534        update_option( 'cookieflame_settings', array(
    23402535            'privacy_url'    => '/privacy-policy',
    2341             'ccpa_url'       => '/privacy-policy#do-not-sell',
    23422536            'log_endpoint'   => '',
    23432537            'gtm_id'         => '',
    … …  
    23502544            'track_logged_in' => 1,
    23512545            'extra_patterns' => '',
    2352             'digest_interval' => 1440, // daily
     2546            'email_frequency' => 'weekly',
    23532547            'monitoring_paused' => 0,
    23542548        ) );
    … …  
    25402734        . "if(raw.cv!=={$cv_js})return;"
    25412735        . "var gpc=window.__ccGPC;"
    2542         . "var mkt=raw.marketing&&!gpc;"
     2736        . "var mkt=raw.marketing&&(!gpc||raw.gpcOptIn);"
    25432737        . "gtag('consent','update',{"
    25442738        . "analytics_storage:raw.analytics?'granted':'denied',"
    … …  
    26292823    // blockable iframe or Elementor video widget is present on the
    26302824    // page). Versioned so cache busts cleanly on plugin update.
    2631     $overrides['facadesUrl'] = COOKIEFLAME_PLUGIN_URL . 'assets/js/cookieconsent-facades.min.js?ver=' . COOKIEFLAME_VERSION;
     2825    $overrides['facadesUrl'] = COOKIEFLAME_PLUGIN_URL . 'assets/js/cookieconsent-facades.min.js?ver=' . cookieflame_asset_version( 'assets/js/cookieconsent-facades.min.js' );
    26322826
    26332827    if ( ! empty( $options['privacy_url'] ) )    $overrides['privacyUrl']    = $options['privacy_url'];
    2634     // CCPA "Do Not Sell" link points to the same privacy policy page with an anchor.
    2635     $cookieflame_privacy = ! empty( $options['privacy_url'] ) ? $options['privacy_url'] : '/privacy-policy';
    2636     $overrides['ccpaUrl'] = $cookieflame_privacy . '#do-not-sell';
    26372828    if ( ! empty( $options['log_endpoint'] ) )    $overrides['logEndpoint']   = $options['log_endpoint'];
    26382829    if ( ! empty( $options['storage_method'] ) )  $overrides['storageMethod'] = $options['storage_method'];
    … …  
    26482839    $overrides['shareSubdomains'] = ! empty( $options['share_subdomains'] ) || ! empty( $options['cookie_domain'] );
    26492840    if ( isset( $options['reconsent_days'] ) && $options['reconsent_days'] !== '' ) {
    2650         $overrides['reconsentDays'] = intval( $options['reconsent_days'] );
     2841        $overrides['reconsentDays'] = cookieflame_reconsent_days( $options['reconsent_days'] );
     2842    }
     2843    if ( ! empty( $options['show_reject_first'] ) ) {
     2844        $overrides['rejectFirstScreen'] = true;
     2845    }
     2846    if ( ! empty( $options['visitor_logins'] ) ) {
     2847        $overrides['visitorLogins'] = true;
    26512848    }
    26522849    if ( ! empty( $options['show_powered_by'] ) ) {
    … …  
    26552852    if ( ! empty( $options['icon_position'] ) && 'right' === $options['icon_position'] ) {
    26562853        $overrides['iconPosition'] = 'right';
     2854    }
     2855
     2856    /**
     2857     * Filter the banner configuration sent to the front end as
     2858     * window.__ccConfig. Keys the banner script knows are applied over
     2859     * its defaults; any other keys are left on window.__ccConfig for the
     2860     * code that added them.
     2861     *
     2862     * @param array $overrides Front-end config.
     2863     * @param array $options   The cookieflame_settings option.
     2864     */
     2865    $filtered = apply_filters( 'cookieflame_frontend_config', $overrides, $options );
     2866    if ( is_array( $filtered ) ) {
     2867        $overrides = $filtered;
    26572868    }
    26582869
    … …  
    27973008        COOKIEFLAME_PLUGIN_URL . 'assets/js/cookieconsent' . $suffix . '.js',
    27983009        array(),
    2799         COOKIEFLAME_VERSION,
     3010        cookieflame_asset_version( 'assets/js/cookieconsent' . $suffix . '.js' ),
    28003011        false // Load in head, not footer.
    28013012    );
    … …  
    28113022            COOKIEFLAME_PLUGIN_URL . 'assets/js/cookieconsent-discovery' . $suffix . '.js',
    28123023            array( 'cookieflame-consent' ),
    2813             COOKIEFLAME_VERSION,
     3024            cookieflame_asset_version( 'assets/js/cookieconsent-discovery' . $suffix . '.js' ),
    28143025            true // Footer is fine -- discovery runs after DOM is ready.
    28153026        );
    … …  
    28473058        COOKIEFLAME_PLUGIN_URL . 'assets/css/cookieconsent.css',
    28483059        array(),
    2849         COOKIEFLAME_VERSION
     3060        cookieflame_asset_version( 'assets/css/cookieconsent.css' )
    28503061    );
    28513062}
    … …  
    33783589        array( 'key' => 'privacy_url',
    33793590               'wp_default_page' => 'wp_page_for_privacy_policy',
    3380                'desc' => __( 'The page linked from the consent banner and the CCPA "Do Not Sell My Personal Information" notice. Defaults to the page set under Settings > Privacy. Add the [cookieflame_declaration] shortcode to this page to show visitors their consent choices.', 'cookieflame' ) ) );
     3591               'desc' => __( 'The page linked from the consent banner. Defaults to the page set under Settings > Privacy. Add the [cookieflame_declaration] shortcode to this page to show visitors their consent choices.', 'cookieflame' ) ) );
     3592
     3593    add_settings_field( 'cookieflame_visitor_logins', __( 'Visitor logins', 'cookieflame' ), 'cookieflame_field_checkbox', 'cookieflame', 'cookieflame_consent',
     3594        array( 'key' => 'visitor_logins',
     3595               'desc' => __( 'Visitors can log in to this site (for example, customer accounts or a members area)', 'cookieflame' ),
     3596               'help' => __( 'Tick this if people other than you and your team can log in, for example to see their orders, download purchases or read members-only pages. Your cookie declaration will then also list the cookies WordPress sets when someone logs in. Leave it unticked if only you and your team log in.', 'cookieflame' ) ) );
    33813597
    33823598    add_settings_field( 'cookieflame_reconsent', __( 'Re-consent Period', 'cookieflame' ), 'cookieflame_field_number', 'cookieflame', 'cookieflame_consent',
    … …  
    33863602    add_settings_field( 'cookieflame_theme', __( 'Banner Theme', 'cookieflame' ), 'cookieflame_field_theme', 'cookieflame', 'cookieflame_consent' );
    33873603
     3604    add_settings_field( 'cookieflame_show_reject_first', __( 'Show "Decline all" on the first screen', 'cookieflame' ), 'cookieflame_field_checkbox', 'cookieflame', 'cookieflame_consent',
     3605        array( 'key' => 'show_reject_first',
     3606               'desc' => __( 'Adds a Decline All button next to Accept All on the banner for visitors in the UK, EU/EEA, Switzerland, Brazil and South Africa. Off by default: visitors can always decline everything under Manage preferences. Never shown on the US notice.', 'cookieflame' ) ) );
     3607
    33883608    add_settings_field( 'cookieflame_share_subdomains', __( 'Share consent across subdomains', 'cookieflame' ), 'cookieflame_field_checkbox', 'cookieflame', 'cookieflame_consent',
    33893609        array( 'key' => 'share_subdomains',
    … …  
    34283648}
    34293649
     3650/**
     3651 * Re-consent period in days. Anything below 1 (blank, 0, negative or
     3652 * junk) means the default of 365, so consent never expires on load.
     3653 */
     3654function cookieflame_reconsent_days( $value ) {
     3655    $days = intval( $value );
     3656    return $days >= 1 ? $days : 365;
     3657}
     3658
    34303659function cookieflame_sanitize_settings( $input ) {
    3431     // Handle the DNT device cookie (not stored in DB, stored as browser cookie)
    3432     cookieflame_handle_dnt_cookie( ! empty( $input['dnt_device'] ) );
     3660    $input = is_array( $input ) ? $input : array();
    34333661
    34343662    // Preserve consent_bump from existing settings (managed by the
    34353663    // Force Re-consent button, not by this form)
    34363664    $existing = get_option( 'cookieflame_settings', array() );
    3437 
    3438     return array(
    3439         'privacy_url'        => esc_url_raw( $input['privacy_url'] ?? '' ),
    3440         'ccpa_url'           => esc_url_raw( $input['ccpa_url'] ?? '' ),
    3441         'log_endpoint'       => esc_url_raw( $input['log_endpoint'] ?? '' ),
     3665    $existing = is_array( $existing ) ? $existing : array();
     3666
     3667    // The Settings tab form posts a '_form' marker. Other saves (the
     3668    // Cookie Monitoring and Tag Managers handlers, the pause switch, the
     3669    // migration) pass the whole stored array back through this callback,
     3670    // so an unticked checkbox there means "not in this form", not "off".
     3671    $main_form = ! empty( $input['_form'] );
     3672    $flag = function ( $key, $default = 0 ) use ( $input, $existing, $main_form ) {
     3673        if ( $main_form ) {
     3674            return ! empty( $input[ $key ] ) ? 1 : 0;
     3675        }
     3676        $source = array_key_exists( $key, $input ) ? $input : $existing;
     3677        return isset( $source[ $key ] ) ? ( ! empty( $source[ $key ] ) ? 1 : 0 ) : $default;
     3678    };
     3679
     3680    if ( $main_form ) {
     3681        // Handle the DNT device cookie (not stored in DB, stored as browser cookie)
     3682        cookieflame_handle_dnt_cookie( ! empty( $input['dnt_device'] ) );
     3683        $track_logged_in = empty( $input['exclude_staff'] ) ? 1 : 0;
     3684    } else {
     3685        $track_logged_in = $flag( 'track_logged_in', 1 );
     3686    }
     3687
     3688    $email_frequency = cookieflame_sanitize_email_frequency( $input['email_frequency'] ?? ( $existing['email_frequency'] ?? 'weekly' ) );
     3689
     3690    $clean = array(
     3691        'privacy_url'        => esc_url_raw( $input['privacy_url'] ?? ( $main_form ? '' : ( $existing['privacy_url'] ?? '' ) ) ),
     3692        'log_endpoint'       => esc_url_raw( $input['log_endpoint'] ?? ( $main_form ? '' : ( $existing['log_endpoint'] ?? '' ) ) ),
    34423693        'storage_method'     => 'cookie', // hardcoded — localStorage removed
    3443         'reconsent_days'     => intval( $input['reconsent_days'] ?? 365 ),
    3444         'cookie_domain'      => cookieflame_sanitize_cookie_domain( $input['cookie_domain'] ?? '' ),
    3445         'share_subdomains'   => ! empty( $input['share_subdomains'] ) ? 1 : 0,
    3446         'csp_safe'           => ! empty( $input['csp_safe'] ) ? 1 : 0,
    3447         'theme'              => in_array( $input['theme'] ?? '', array( 'light', 'dark' ), true ) ? $input['theme'] : 'light',
    3448         'show_powered_by'    => ! empty( $input['show_powered_by'] ) ? 1 : 0,
    3449         'icon_position'      => in_array( $input['icon_position'] ?? '', array( 'left', 'right' ), true ) ? $input['icon_position'] : 'left',
    3450         'track_logged_in'    => empty( $input['exclude_staff'] ) ? 1 : 0,
    3451         'extra_patterns'     => sanitize_textarea_field( $input['extra_patterns'] ?? '' ),
     3694        'reconsent_days'     => cookieflame_reconsent_days( $input['reconsent_days'] ?? ( $existing['reconsent_days'] ?? 365 ) ),
     3695        'cookie_domain'      => cookieflame_sanitize_cookie_domain( $input['cookie_domain'] ?? ( $existing['cookie_domain'] ?? '' ) ),
     3696        'share_subdomains'   => $flag( 'share_subdomains' ),
     3697        'csp_safe'           => $flag( 'csp_safe' ),
     3698        'theme'              => in_array( $input['theme'] ?? ( $existing['theme'] ?? '' ), array( 'light', 'dark' ), true ) ? ( $input['theme'] ?? $existing['theme'] ) : 'light',
     3699        'show_reject_first'  => $flag( 'show_reject_first' ),
     3700        'visitor_logins'     => $flag( 'visitor_logins' ),
     3701        'show_powered_by'    => $flag( 'show_powered_by' ),
     3702        'icon_position'      => in_array( $input['icon_position'] ?? ( $existing['icon_position'] ?? '' ), array( 'left', 'right' ), true ) ? ( $input['icon_position'] ?? $existing['icon_position'] ) : 'left',
     3703        'track_logged_in'    => $track_logged_in,
     3704        'extra_patterns'     => sanitize_textarea_field( $input['extra_patterns'] ?? ( $main_form ? '' : ( $existing['extra_patterns'] ?? '' ) ) ),
    34523705        'consent_bump'       => intval( $existing['consent_bump'] ?? 0 ),
    3453         'delete_on_uninstall' => ! empty( $input['delete_on_uninstall'] ) ? 1 : 0,
     3706        'delete_on_uninstall' => $flag( 'delete_on_uninstall' ),
    34543707        // Separate-tab fields: use $input if present (handler included it), else preserve $existing
    34553708        'gtm_id'             => isset( $input['gtm_id'] )             ? cookieflame_sanitize_gtm_id( $input['gtm_id'] )                : ( $existing['gtm_id'] ?? '' ),
    34563709        'uet_id'             => isset( $input['uet_id'] )             ? cookieflame_sanitize_uet_id( $input['uet_id'] )                : ( $existing['uet_id'] ?? '' ),
    34573710        'notification_email' => isset( $input['notification_email'] ) ? sanitize_email( $input['notification_email'] )           : ( $existing['notification_email'] ?? '' ),
    3458         'digest_interval'    => isset( $input['digest_interval'] )    ? cookieflame_normalise_interval( $input['digest_interval'] )    : cookieflame_normalise_interval( $existing['digest_interval'] ?? 1440 ),
    3459         // monitoring_paused is toggled only via the dedicated
    3460         // admin-post handler (cookieflame_toggle_monitoring_pause). When the
    3461         // handler's update_option() fires, the sanitize_option filter
    3462         // runs with $input containing the new value the handler wants
    3463         // to commit. When the main Settings tab saves, $input does not
    3464         // include this field, so we fall back to $existing to preserve
    3465         // whatever pause state was previously set.
     3711        'email_frequency'    => $email_frequency,
     3712        // "Watch for new cookies" Off. Toggled only via the dedicated
     3713        // admin-post handler (cookieflame_toggle_monitoring_pause) or
     3714        // the migration; the Settings tab preserves it.
    34663715        'monitoring_paused'  => isset( $input['monitoring_paused'] )  ? ( ! empty( $input['monitoring_paused'] ) ? 1 : 0 ) : ( ! empty( $existing['monitoring_paused'] ) ? 1 : 0 ),
    34673716    );
     3717    // Pre-5.4.0 setting, kept as-is so a downgrade still finds it.
     3718    if ( isset( $input['digest_interval'] ) || isset( $existing['digest_interval'] ) ) {
     3719        $clean['digest_interval'] = intval( $input['digest_interval'] ?? $existing['digest_interval'] );
     3720    }
     3721    return $clean;
    34683722}
    34693723
    … …  
    35323786    check_admin_referer( 'cookieflame_save_notifications' );
    35333787
    3534     $opts            = get_option( 'cookieflame_settings', array() );
    3535     $previous        = cookieflame_normalise_interval( $opts['digest_interval'] ?? 1440 );
     3788    $opts      = get_option( 'cookieflame_settings', array() );
     3789    $previous  = cookieflame_email_frequency();
     3790    $frequency = cookieflame_sanitize_email_frequency( sanitize_key( wp_unslash( $_POST['email_frequency'] ?? 'weekly' ) ) );
     3791    if ( 'instant' === $frequency && ! cookieflame_instant_email_available() ) {
     3792        $frequency = 'daily';
     3793    }
    35363794    $opts['notification_email'] = sanitize_email( wp_unslash( $_POST['notification_email'] ?? '' ) );
    3537     $opts['digest_interval']    = cookieflame_normalise_interval( intval( $_POST['digest_interval'] ?? 1440 ) );
     3795    $opts['email_frequency']    = $frequency;
    35383796    update_option( 'cookieflame_settings', $opts );
    35393797
    3540     // If the interval changed, clear any pending single-event digest so
    3541     // the next queued cookie reschedules against the new cadence. If the
    3542     // admin has just turned monitoring off, drop any pending digest too.
    3543     if ( $previous !== $opts['digest_interval'] || 0 === $opts['digest_interval'] ) {
     3798    // If the frequency changed, clear any pending single-event digest so
     3799    // the next queued cookie reschedules against the new setting. Emails
     3800    // turned off also drops the waiting list, so nothing stale is sent
     3801    // when they are turned back on.
     3802    if ( $previous !== $frequency ) {
    35443803        wp_clear_scheduled_hook( 'cookieflame_send_digest' );
    35453804        delete_option( 'cookieflame_digest_scheduled' );
     3805        if ( 'off' === $frequency ) {
     3806            update_option( 'cookieflame_notification_queue', array() );
     3807        }
    35463808    }
    35473809
    … …  
    35543816
    35553817/**
    3556  * Toggle the monitoring_paused flag from the Cookie Monitoring tab.
    3557  * Pause suppresses beacons, beacon handling, digest queueing, and
     3818 * Toggle "Watch for new cookies" (the monitoring_paused flag) from the
     3819 * Cookie Monitoring tab. Off suppresses beacons, beacon handling, digest queueing, and
    35583820 * digest sending — all three paths gate on cookieflame_monitoring_is_off(),
    35593821 * which now returns true when paused. On pause we also clear any
    … …  
    35713833
    35723834    $opts       = get_option( 'cookieflame_settings', array() );
    3573     $was_paused = ! empty( $opts['monitoring_paused'] );
     3835    $was_paused = cookieflame_monitoring_is_paused();
    35743836    $now_paused = ! $was_paused;
    35753837
    … …  
    35883850        // next cadence window rather than immediately.
    35893851        $queue    = get_option( 'cookieflame_notification_queue', array() );
    3590         $interval = cookieflame_current_interval_minutes();
     3852        $interval = cookieflame_email_delay_minutes();
    35913853        if ( ! empty( $queue ) && $interval > 0 && ! wp_next_scheduled( 'cookieflame_send_digest' ) ) {
    35923854            wp_schedule_single_event( time() + ( $interval * MINUTE_IN_SECONDS ), 'cookieflame_send_digest' );
    … …  
    39304192        esc_html( $a['desc'] ?? '' )
    39314193    );
     4194    if ( ! empty( $a['help'] ) ) {
     4195        printf( '<p class="description">%s</p>', esc_html( $a['help'] ) );
     4196    }
    39324197}
    39334198
    … …  
    41924457        </div>
    41934458
    4194         <?php if ( ! cookieflame_is_premium() ) : ?>
     4459        <?php if ( ! cookieflame_is_premium() && ! get_user_meta( get_current_user_id(), 'cookieflame_upgrade_banner_dismissed', true ) ) : ?>
    41954460        <div class="cf-admin-upgrade-banner">
    41964461            <div class="cf-admin-upgrade-banner-icon">&#x2728;</div>
    … …  
    42004465            </div>
    42014466            <a href="https://cookieflame.com/pricing" target="_blank" class="cf-admin-btn-upgrade"><?php esc_html_e( 'Upgrade to Pro', 'cookieflame' ); ?> &rarr;</a>
     4467            <form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" class="cf-admin-upgrade-banner-dismiss">
     4468                <?php wp_nonce_field( 'cookieflame_dismiss_upgrade_banner' ); ?>
     4469                <input type="hidden" name="action" value="cookieflame_dismiss_upgrade_banner" />
     4470                <input type="hidden" name="tab" value="<?php echo esc_attr( $active_tab ); ?>" />
     4471                <button type="submit" class="cf-admin-upgrade-banner-close" aria-label="<?php esc_attr_e( 'Dismiss this message', 'cookieflame' ); ?>" title="<?php esc_attr_e( 'Dismiss this message', 'cookieflame' ); ?>">&times;</button>
     4472            </form>
    42024473        </div>
    42034474        <?php endif; ?>
    … …  
    42094480            <p><?php esc_html_e( 'Add this shortcode to the page you have set as your Privacy Policy page (under Settings > Privacy). It shows visitors which cookie categories are active, what each cookie does, and a button to change their consent choices.', 'cookieflame' ); ?></p>
    42104481            <pre class="cf-admin-code-block">[cookieflame_declaration]</pre>
     4482            <h3><?php esc_html_e( 'Cookie Settings Link', 'cookieflame' ); ?></h3>
     4483            <p><?php esc_html_e( 'Put this shortcode in your footer or anywhere else to give visitors a "Cookie settings" link that reopens their choices. US visitors get the "Do Not Sell or Share My Personal Information" choice. You can change the wording with text="...". To use a menu item or your own button instead, give it the CSS class cc-open-settings.', 'cookieflame' ); ?></p>
     4484            <pre class="cf-admin-code-block">[cookieflame_settings_link]</pre>
    42114485        </div>
    42124486
    … …  
    42554529            <?php
    42564530            settings_fields( 'cookieflame_settings_group' );
     4531            ?>
     4532            <input type="hidden" name="cookieflame_settings[_form]" value="main" />
     4533            <?php
    42574534            do_settings_sections( 'cookieflame' );
    42584535            submit_button();
    42594536            ?>
    42604537        </form>
     4538
     4539        <?php
     4540        /**
     4541         * Fires on the Settings tab after the main settings form, so other
     4542         * plugins can add their own section (with its own form and option).
     4543         */
     4544        do_action( 'cookieflame_settings_sections' );
     4545        ?>
    42614546
    42624547        <div class="cf-admin-card cf-admin-card-warning">
    … …  
    44544739
    44554740                <h4 style="margin-top:16px;margin-bottom:8px;"><?php esc_html_e( 'How the scan works', 'cookieflame' ); ?></h4>
    4456                 <p style="margin-bottom:12px;"><?php esc_html_e( 'The scan reads cookies from this browser, right now. It does not crawl your website or check other visitors. If a cookie does not exist in your browser at this moment, it will not appear in the scan. That means you need to visit your own website first and trigger all the scripts that set cookies.', 'cookieflame' ); ?></p>
     4741                <p style="margin-bottom:12px;"><?php esc_html_e( 'Cookie Discovery uses your own browser. It records the cookies your browser picks up while you browse your site with every cookie allowed. It does not crawl your website or check other visitors, so a cookie only appears if a page you visit sets it. Cookies your visitors pick up are found separately by Watch for new cookies on the Cookie Monitoring tab.', 'cookieflame' ); ?></p>
    44574742
    44584743                <h4 style="margin-bottom:8px;"><?php esc_html_e( 'Before you start', 'cookieflame' ); ?></h4>
    4459                 <p style="margin-bottom:4px;color:#d63638;"><strong><?php esc_html_e( 'Check your tracking settings first.', 'cookieflame' ); ?></strong></p>
    4460                 <p style="margin-bottom:12px;"><?php esc_html_e( 'Go to the Settings tab. Make sure "Track logged-in users" is checked and "Do not track this device" is unchecked. If either of these is blocking tracking, scripts will be suppressed for you even after you accept cookies, and no third-party cookies will appear in the scan. Save the settings and clear your site cache if you make changes. You can turn these back on after you have finished scanning.', 'cookieflame' ); ?></p>
     4744                <p style="margin-bottom:4px;"><strong><?php esc_html_e( 'Clear your page cache if you run one.', 'cookieflame' ); ?></strong></p>
     4745                <p style="margin-bottom:12px;"><?php esc_html_e( 'You do not need to change any CookieFlame settings. While Cookie Discovery runs, "Exclude staff from tracking" and "Do not track this device" are paused for your browser, and they are put back when you click Finish Discovery.', 'cookieflame' ); ?></p>
    44614746
    44624747                <h4 style="margin-bottom:8px;"><?php esc_html_e( 'Step by step', 'cookieflame' ); ?></h4>
    … …  
    44644749                    <li>
    44654750                        <strong><?php esc_html_e( 'Start Cookie Discovery.', 'cookieflame' ); ?></strong>
    4466                         <br /><span style="color:#666;font-size:13px;"><?php esc_html_e( 'When you click the Start Cookie Discovery button above, your website opens in this tab with all cookies temporarily accepted, so every tracking script can run and set its cookies. You need to be in the same browser you are using right now for wp-admin, because cookies are stored per browser.', 'cookieflame' ); ?></span>
     4751                        <br /><span style="color:#666;font-size:13px;"><?php esc_html_e( 'Click Start Cookie Discovery above, then Open my site. Your site opens in this tab with every cookie allowed for your browser only, and a green bar at the top counts what is found. Use the browser you are using for wp-admin right now, because cookies are stored per browser.', 'cookieflame' ); ?></span>
    44674752                    </li>
    44684753                    <li>
    4469                         <strong><?php esc_html_e( 'Click "Accept All" on the consent banner.', 'cookieflame' ); ?></strong>
    4470                         <br /><span style="color:#666;font-size:13px;"><?php esc_html_e( 'This is essential. The plugin blocks all tracking scripts until consent is given. If you only accept some categories, only those scripts will run. You need to accept everything so every script fires and every cookie gets created.', 'cookieflame' ); ?></span>
     4754                        <strong><?php esc_html_e( 'You will not see the consent banner.', 'cookieflame' ); ?></strong>
     4755                        <br /><span style="color:#666;font-size:13px;"><?php esc_html_e( 'Cookie Discovery allows every cookie for your browser while it runs, so every script on your site can run and set its cookies. Your visitors still see the banner as normal.', 'cookieflame' ); ?></span>
    44714756                    </li>
    44724757                    <li>
    … …  
    44844769                    </li>
    44854770                    <li>
    4486                         <strong><?php esc_html_e( 'Come back to this tab to see your results.', 'cookieflame' ); ?></strong>
    4487                         <br /><span style="color:#666;font-size:13px;"><?php esc_html_e( 'The table will update with every cookie now sitting in your browser. Known cookies will have their details filled in automatically. Unknown cookies will be highlighted in yellow.', 'cookieflame' ); ?></span>
     4771                        <strong><?php esc_html_e( 'Click Finish Discovery on the green bar.', 'cookieflame' ); ?></strong>
     4772                        <br /><span style="color:#666;font-size:13px;"><?php esc_html_e( 'You come back here, and the table lists every cookie found. Known cookies have their details filled in automatically. Unknown cookies are highlighted in yellow.', 'cookieflame' ); ?></span>
    44884773                    </li>
    44894774                    <li>
    … …  
    45004785                    </li>
    45014786                    <li>
    4502                         <strong><?php esc_html_e( 'Re-enable your tracking protection.', 'cookieflame' ); ?></strong>
    4503                         <br /><span style="color:#666;font-size:13px;"><?php esc_html_e( 'If you turned off "Do not track this device" in step 1, go back to the Settings tab and turn it back on. You can also click "Delete Non-Essential Cookies" on the Settings tab to clean up the tracking cookies that were just created during your scan.', 'cookieflame' ); ?></span>
     4787                        <strong><?php esc_html_e( 'Tidy up your browser if you like.', 'cookieflame' ); ?></strong>
     4788                        <br /><span style="color:#666;font-size:13px;"><?php esc_html_e( 'Your tracking settings are put back automatically. To remove the tracking cookies your browser picked up during the scan, click "Delete Non-Essential Cookies from This Browser" on the Settings tab.', 'cookieflame' ); ?></span>
    45044789                    </li>
    45054790                </ol>
    … …  
    45104795                <h4 style="margin-bottom:4px;"><?php esc_html_e( 'Why are some cookies not showing up?', 'cookieflame' ); ?></h4>
    45114796                <ul style="margin:0 0 12px 20px;line-height:1.8;">
    4512                     <li><strong><?php esc_html_e( 'You did not click Accept All:', 'cookieflame' ); ?></strong> <?php esc_html_e( 'If you only accepted some categories, scripts in the other categories never ran. Their cookies do not exist. Go back and accept all categories.', 'cookieflame' ); ?></li>
    4513                     <li><strong><?php esc_html_e( 'Your tracking protection is still on:', 'cookieflame' ); ?></strong> <?php esc_html_e( 'If "Do not track this device" is enabled or "Track logged-in users" is unchecked on the Settings tab, tracking is suppressed for your browser even after you accept cookies. Temporarily disable these before scanning.', 'cookieflame' ); ?></li>
     4797                    <li><strong><?php esc_html_e( 'Your page cache served an old page:', 'cookieflame' ); ?></strong> <?php esc_html_e( 'Clear your page cache and run Cookie Discovery again.', 'cookieflame' ); ?></li>
    45144798                    <li><strong><?php esc_html_e( 'The cookie is page-specific:', 'cookieflame' ); ?></strong> <?php esc_html_e( 'Some cookies are only set on specific pages (e.g. Stripe only on checkout, a chat widget only on contact pages). Visit those pages before re-scanning.', 'cookieflame' ); ?></li>
    45154799                    <li><strong><?php esc_html_e( 'The cookie is on a subdomain:', 'cookieflame' ); ?></strong> <?php esc_html_e( 'If you have subdomains (like app.yoursite.com), cookies locked to that subdomain are invisible from your main site. Enable "Share consent across subdomains" in Settings to share cookies, or use the "Add Cookie Manually" section below to register them by name.', 'cookieflame' ); ?></li>
    … …  
    45874871                <div id="cf-admin-discovery-idle">
    45884872                    <div style="display:flex;align-items:center;justify-content:space-between;gap:16px;flex-wrap:wrap;">
    4589                         <button type="button" id="cf-admin-discovery-start" class="cf-admin-btn-primary"><?php esc_html_e( 'Start Cookie Discovery', 'cookieflame' ); ?></button>
    4590                         <span style="font-size:13px;color:#50575e;flex:1;min-width:200px;"><?php esc_html_e( 'Temporarily disables tracking protection, opens your site in a new tab with all cookies accepted, and lets you browse a few pages. When you finish, it scans for cookies and restores your settings.', 'cookieflame' ); ?></span>
     4873                        <button type="button" id="cf-admin-discovery-start" class="cf-admin-btn-primary"><?php esc_html_e( 'Start Cookie Discovery (uses your own browser)', 'cookieflame' ); ?></button>
     4874                        <span style="font-size:13px;color:#50575e;flex:1;min-width:200px;"><?php esc_html_e( 'Cookie Discovery uses your own browser: it allows every cookie for you only while you browse a few pages of your site in this tab. When you finish, it lists the cookies found and puts your settings back.', 'cookieflame' ); ?></span>
    45914875                    </div>
    45924876                    <p style="font-size:12px;color:#646970;margin:10px 0 0;"><?php esc_html_e( 'If you notice cookies you cannot explain after scanning, try running discovery in an incognito or private browser window. Browser extensions can occasionally inject tracking cookies that do not belong to your site, and incognito mode disables them by default. This is uncommon.', 'cookieflame' ); ?></p>
    … …  
    50025286            <?php if ( $cookieflame_paused ) : ?>
    50035287            <div class="notice notice-warning is-dismissible" style="margin:0 0 16px;">
    5004                 <p><strong><?php esc_html_e( 'Monitoring is now Off.', 'cookieflame' ); ?></strong> <?php esc_html_e( 'Cookie beacons and alert emails are suppressed. Your update interval is preserved for when you switch monitoring back on.', 'cookieflame' ); ?></p>
     5288                <p><strong><?php esc_html_e( 'Watch for new cookies is now Off.', 'cookieflame' ); ?></strong> <?php esc_html_e( 'Visitors\' browsers no longer report new cookies, and no emails are sent. Your email setting is kept for when you turn it back on.', 'cookieflame' ); ?></p>
    50055289            </div>
    50065290            <?php endif; ?>
    … …  
    50085292            <?php if ( $cookieflame_resumed ) : ?>
    50095293            <div class="notice notice-success is-dismissible" style="margin:0 0 16px;">
    5010                 <p><strong><?php esc_html_e( 'Monitoring is now On.', 'cookieflame' ); ?></strong> <?php esc_html_e( 'New cookies will be detected on the cadence you have configured.', 'cookieflame' ); ?></p>
     5294                <p><strong><?php esc_html_e( 'Watch for new cookies is now On.', 'cookieflame' ); ?></strong> <?php esc_html_e( 'New cookies appear in the Cookie Manager as soon as a visitor\'s browser reports them.', 'cookieflame' ); ?></p>
    50115295            </div>
    50125296            <?php endif; ?>
    50135297
    5014             <p class="cf-admin-tab-intro"><?php esc_html_e( 'Watch real visitor sessions in the background and surface new cookies as they appear.', 'cookieflame' ); ?></p>
     5298            <p class="cf-admin-tab-intro"><?php esc_html_e( 'Your visitors\' browsers report cookies as they browse your site, so new ones show up in the Cookie Manager without you scanning.', 'cookieflame' ); ?></p>
    50155299
    50165300            <?php $cookieflame_is_paused = cookieflame_monitoring_is_paused(); ?>
    50175301            <div class="cf-admin-pause-card <?php echo $cookieflame_is_paused ? 'cf-admin-pause-card-paused' : 'cf-admin-pause-card-running'; ?>">
    50185302                <div class="cf-admin-pause-card-body">
    5019                     <h3 class="cf-admin-pause-card-title"><?php esc_html_e( 'Visitor Monitoring', 'cookieflame' ); ?></h3>
    5020                     <p class="cf-admin-pause-card-desc"><?php esc_html_e( 'Pause during site surgery (plugin swaps, theme changes, migrations). Your update interval is preserved.', 'cookieflame' ); ?></p>
     5303                    <h3 class="cf-admin-pause-card-title"><?php esc_html_e( 'Watch for new cookies', 'cookieflame' ); ?></h3>
     5304                    <p class="cf-admin-pause-card-desc"><?php esc_html_e( 'Uses your visitors\' browsers. New cookies are recorded the moment a visitor\'s browser reports them. Turn it off for a while during big changes (plugin swaps, theme changes, migrations) if you prefer. Clear your page cache after switching, so cached pages pick up the change.', 'cookieflame' ); ?></p>
    50215305                </div>
    50225306                <form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" class="cf-admin-pause-card-action">
    … …  
    50275311                        <span class="cf-admin-toggle-label"><?php
    50285312                            echo $cookieflame_is_paused
    5029                                 ? esc_html__( 'Monitoring: Off', 'cookieflame' )
    5030                                 : esc_html__( 'Monitoring: On', 'cookieflame' );
     5313                                ? esc_html__( 'Watch for new cookies: Off', 'cookieflame' )
     5314                                : esc_html__( 'Watch for new cookies: On', 'cookieflame' );
    50315315                        ?></span>
    50325316                    </button>
    … …  
    50355319
    50365320            <?php
    5037             $notif_opts   = get_option( 'cookieflame_settings', array() );
    5038             $notif_email  = $notif_opts['notification_email'] ?? '';
    5039             $current_mins = cookieflame_normalise_interval( $notif_opts['digest_interval'] ?? 1440 );
    5040             $buckets      = cookieflame_interval_buckets();
    5041             $bucket_label = array(
    5042                 0     => __( 'Off', 'cookieflame' ),
    5043                 15    => __( '15 minutes', 'cookieflame' ),
    5044                 60    => __( '1 hour', 'cookieflame' ),
    5045                 240   => __( '4 hours', 'cookieflame' ),
    5046                 1440  => __( 'Daily', 'cookieflame' ),
    5047                 10080 => __( 'Weekly', 'cookieflame' ),
     5321            $notif_opts        = get_option( 'cookieflame_settings', array() );
     5322            $notif_email       = $notif_opts['notification_email'] ?? '';
     5323            $cf_frequency      = cookieflame_email_frequency();
     5324            $cf_instant_ok     = cookieflame_instant_email_available();
     5325            $cf_is_premium     = cookieflame_is_premium();
     5326            if ( 'instant' === $cf_frequency && ! $cf_instant_ok ) {
     5327                $cf_frequency = 'daily'; // What the site actually gets without Pro.
     5328            }
     5329            $cf_frequencies = array(
     5330                'weekly'  => __( 'Weekly', 'cookieflame' ),
     5331                'daily'   => __( 'Daily', 'cookieflame' ),
     5332                'instant' => $cf_instant_ok ? __( 'Instantly', 'cookieflame' ) : __( 'Instantly (Pro)', 'cookieflame' ),
     5333                'off'     => __( 'Off', 'cookieflame' ),
    50485334            );
    5049             // Dropdown ordering: fastest at top, then slower cadences,
    5050             // with Off at the very bottom as the "I don't want this"
    5051             // answer rather than a cadence choice. Off is excluded from
    5052             // the ordered list and re-appended at the end.
    5053             $cf_dropdown_order = array( 15, 60, 240, 1440, 10080 );
    5054             $cf_ordered_buckets = array();
    5055             $cf_buckets_by_minutes = array();
    5056             foreach ( $buckets as $cf_b ) {
    5057                 $cf_buckets_by_minutes[ $cf_b['minutes'] ] = $cf_b;
    5058             }
    5059             foreach ( $cf_dropdown_order as $cf_m ) {
    5060                 if ( isset( $cf_buckets_by_minutes[ $cf_m ] ) ) {
    5061                     $cf_ordered_buckets[] = $cf_buckets_by_minutes[ $cf_m ];
    5062                 }
    5063             }
    5064             if ( isset( $cf_buckets_by_minutes[ 0 ] ) ) {
    5065                 $cf_ordered_buckets[] = $cf_buckets_by_minutes[ 0 ];
    5066             }
    5067             $cf_is_premium = cookieflame_is_premium();
    50685335            ?>
    50695336
    … …  
    50755342                    <tr>
    50765343                        <th scope="row">
    5077                             <label for="cf-admin-digest-interval" title="<?php esc_attr_e( 'How often CookieFlame refreshes the cookie list and sends digest alerts. Shorter cadences surface new cookies sooner; longer cadences group changes into a single summary.', 'cookieflame' ); ?>"><?php esc_html_e( 'Update interval', 'cookieflame' ); ?></label>
     5344                            <label for="cf-admin-email-frequency"><?php esc_html_e( 'Email me about changes', 'cookieflame' ); ?></label>
    50785345                        </th>
    50795346                        <td>
    5080                             <select id="cf-admin-digest-interval" name="digest_interval">
    5081                                 <?php foreach ( $cf_ordered_buckets as $cf_b ) :
    5082                                     $cf_min   = intval( $cf_b['minutes'] );
    5083                                     $cf_tier  = (string) ( $cf_b['tier'] ?? 'free' );
    5084                                     $cf_label = $bucket_label[ $cf_min ] ?? ( $cf_min . ' min' );
    5085                                     $cf_is_pro_tier = ( 'pro' === $cf_tier );
    5086                                     $cf_disabled    = ( $cf_is_pro_tier && ! $cf_is_premium );
    5087                                     if ( $cf_is_pro_tier ) {
    5088                                         /* translators: %s: human-readable interval label, e.g. "15 minutes" */
    5089                                         $cf_label = sprintf( __( '%s (Pro)', 'cookieflame' ), $cf_label );
    5090                                     }
    5091                                     ?>
    5092                                     <option value="<?php echo esc_attr( $cf_min ); ?>"<?php
    5093                                         selected( $current_mins, $cf_min );
    5094                                         disabled( $cf_disabled, true );
     5347                            <select id="cf-admin-email-frequency" name="email_frequency">
     5348                                <?php foreach ( $cf_frequencies as $cf_value => $cf_label ) : ?>
     5349                                    <option value="<?php echo esc_attr( $cf_value ); ?>"<?php
     5350                                        selected( $cf_frequency, $cf_value );
     5351                                        disabled( 'instant' === $cf_value && ! $cf_instant_ok, true );
    50955352                                    ?>><?php echo esc_html( $cf_label ); ?></option>
    50965353                                <?php endforeach; ?>
    50975354                            </select>
    5098                             <p class="description"><?php esc_html_e( 'Pro intervals are visible above so you can see what an upgrade adds. They unlock when CookieFlame Pro is active.', 'cookieflame' ); ?></p>
     5355                            <p class="description"><?php esc_html_e( 'One email lists every new cookie found since the last one. Nothing is sent when there is nothing new.', 'cookieflame' ); ?></p>
    50995356                            <?php if ( ! $cf_is_premium ) : ?>
    5100                             <p class="description"><a href="https://cookieflame.com/pricing" target="_blank"><?php esc_html_e( 'Upgrade to CookieFlame Pro', 'cookieflame' ); ?></a></p>
     5357                            <p class="description"><?php esc_html_e( 'Instant emails come with CookieFlame Pro.', 'cookieflame' ); ?> <a href="https://cookieflame.com/pricing" target="_blank"><?php esc_html_e( 'Upgrade to CookieFlame Pro', 'cookieflame' ); ?></a></p>
    51015358                            <?php endif; ?>
    51025359                        </td>
    … …  
    51135370                <p style="display:flex;align-items:center;gap:14px;flex-wrap:wrap;">
    51145371                    <?php submit_button( __( 'Save Cookie Monitoring Settings', 'cookieflame' ), 'primary', 'submit', false ); ?>
    5115                     <span class="description" style="margin:0;"><?php esc_html_e( 'Clear your page cache after saving so visitors pick up the new beacon configuration.', 'cookieflame' ); ?></span>
    51165372                </p>
    51175373            </form>
    … …  
    51235379            <div class="cf-admin-card">
    51245380                <h2><?php esc_html_e( 'Banner Preview', 'cookieflame' ); ?></h2>
    5125                 <p><?php esc_html_e( 'Preview how the consent banner appears to visitors in different regions. Select a region and theme to see the exact banner they would see.', 'cookieflame' ); ?></p>
     5381                <p><?php esc_html_e( 'Choose a region and theme to see the banner those visitors get.', 'cookieflame' ); ?></p>
    51265382
    51275383                <div style="display:flex;gap:16px;flex-wrap:wrap;margin:20px 0;">
    51285384                    <div>
    51295385                        <label style="font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;color:#50575e;display:block;margin-bottom:6px;"><?php esc_html_e( 'Region', 'cookieflame' ); ?></label>
    5130                         <select id="cf-admin-preview-region" style="min-width:200px;padding:8px 12px;border-radius:6px;border:1px solid #dcdcde;">
     5386                        <select id="cf-admin-preview-region" style="min-width:200px;padding:8px 36px 8px 12px;border-radius:6px;border:1px solid #dcdcde;">
    51315387                            <option value="gdpr"><?php esc_html_e( 'GDPR / Opt-in (EU, EEA, UK, Brazil, South Africa, Switzerland)', 'cookieflame' ); ?></option>
    51325388                            <option value="ccpa"><?php esc_html_e( 'CCPA / Opt-out (United States)', 'cookieflame' ); ?></option>
    … …  
    51365392                    <div>
    51375393                        <label style="font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;color:#50575e;display:block;margin-bottom:6px;"><?php esc_html_e( 'Theme', 'cookieflame' ); ?></label>
    5138                         <select id="cf-admin-preview-theme" style="min-width:200px;padding:8px 12px;border-radius:6px;border:1px solid #dcdcde;">
     5394                        <select id="cf-admin-preview-theme" style="min-width:200px;padding:8px 36px 8px 12px;border-radius:6px;border:1px solid #dcdcde;">
    51395395                            <option value="light"><?php esc_html_e( 'Light', 'cookieflame' ); ?></option>
    51405396                            <option value="dark"><?php esc_html_e( 'Dark', 'cookieflame' ); ?></option>
    51415397                        </select>
    51425398                    </div>
     5399                    <div id="cf-admin-preview-reject-wrap" style="align-self:flex-end;padding-bottom:8px;">
     5400                        <label><input type="checkbox" id="cf-admin-preview-reject" /> <?php esc_html_e( 'Show "Decline all" on the first screen', 'cookieflame' ); ?></label>
     5401                    </div>
     5402                    <div id="cf-admin-preview-gpc-wrap" style="align-self:flex-end;padding-bottom:8px;display:none;">
     5403                        <label><input type="checkbox" id="cf-admin-preview-gpc" /> <?php esc_html_e( 'Visitor\'s browser sends Global Privacy Control', 'cookieflame' ); ?></label>
     5404                    </div>
    51435405                </div>
    5144 
    5145                 <div style="margin-top:8px;font-size:12px;color:#888;line-height:1.8;">
    5146                     <strong><?php esc_html_e( 'GDPR / Opt-in:', 'cookieflame' ); ?></strong>
    5147                     <?php esc_html_e( 'Full consent banner with Accept All, Decline All, and Manage Preferences. Visitors must opt in before non-essential cookies are set.', 'cookieflame' ); ?><br />
    5148                     <span style="color:#aaa;"><?php esc_html_e( 'Countries: Austria, Belgium, Bulgaria, Croatia, Cyprus, Czech Republic, Denmark, Estonia, Finland, France, Germany, Greece, Hungary, Ireland, Italy, Latvia, Lithuania, Luxembourg, Malta, Netherlands, Poland, Portugal, Romania, Slovakia, Slovenia, Spain, Sweden (EU); Iceland, Liechtenstein, Norway (EEA); United Kingdom, Guernsey, Jersey, Isle of Man (UK); Switzerland (revFADP); Brazil (LGPD); South Africa (POPIA).', 'cookieflame' ); ?></span><br /><br />
    5149                     <strong><?php esc_html_e( 'CCPA / Opt-out:', 'cookieflame' ); ?></strong>
    5150                     <?php esc_html_e( 'Compact notice bar with a "Do Not Sell My Personal Information" link. All cookies are granted by default. The visitor can opt out of the sale or sharing of personal data. If Global Privacy Control (GPC) is detected in the browser, marketing cookies are automatically suppressed.', 'cookieflame' ); ?><br />
    5151                     <span style="color:#aaa;"><?php esc_html_e( 'Countries: United States (CCPA/CPRA applies to California residents; GPC is legally binding in 12+ states including California, Colorado, Connecticut, Texas, Montana, Oregon, and others).', 'cookieflame' ); ?></span><br /><br />
    5152                     <strong><?php esc_html_e( 'No banner:', 'cookieflame' ); ?></strong>
    5153                     <?php esc_html_e( 'No consent banner is shown. All cookies are auto-granted. The cookie icon appears in the bottom-left corner so visitors can manage preferences if they choose. This applies to visitors in countries not covered by the regions above.', 'cookieflame' ); ?>
     5406                <p class="description" id="cf-admin-preview-reject-note" style="margin-top:-8px;"><?php esc_html_e( 'The "Decline all" box here only changes the preview. To change your banner, use the setting on the Settings tab.', 'cookieflame' ); ?></p>
     5407
     5408                <div id="cf-admin-preview-container" style="position:relative;border-radius:12px;overflow:hidden;margin-top:16px;"></div>
     5409
     5410                <div id="cf-admin-preview-notes" style="margin-top:16px;font-size:12px;color:#888;line-height:1.8;">
     5411                    <p data-region="gdpr" style="margin:0;"><strong><?php esc_html_e( 'GDPR / Opt-in:', 'cookieflame' ); ?></strong>
     5412                    <?php esc_html_e( 'Full consent banner with Accept All and Manage preferences. Decline All is under Manage preferences, and you can also show it next to Accept All with the setting on the Settings tab. Visitors must opt in before non-essential cookies are set.', 'cookieflame' ); ?><br />
     5413                    <span style="color:#aaa;"><?php esc_html_e( 'Countries: Austria, Belgium, Bulgaria, Croatia, Cyprus, Czech Republic, Denmark, Estonia, Finland, France, Germany, Greece, Hungary, Ireland, Italy, Latvia, Lithuania, Luxembourg, Malta, Netherlands, Poland, Portugal, Romania, Slovakia, Slovenia, Spain, Sweden (EU); Iceland, Liechtenstein, Norway (EEA); United Kingdom, Guernsey, Jersey, Isle of Man (UK); Switzerland (revFADP); Brazil (LGPD); South Africa (POPIA).', 'cookieflame' ); ?></span></p>
     5414                    <p data-region="ccpa" style="margin:0;display:none;"><strong><?php esc_html_e( 'CCPA / Opt-out:', 'cookieflame' ); ?></strong>
     5415                    <?php esc_html_e( 'Compact notice with a "Do Not Sell or Share My Personal Information" link, which opens a real opt-out choice. Cookies are allowed by default. If the visitor\'s browser sends Global Privacy Control (GPC), they are opted out of selling and sharing straight away: Marketing stays off, the notice says the signal was honoured, and OK only closes the notice. They can opt back in only through a separate choice, and a later visit without GPC keeps the opt-out.', 'cookieflame' ); ?><br />
     5416                    <span style="color:#aaa;"><?php esc_html_e( 'Countries: United States. The same notice covers California (CCPA/CPRA) and the other states with similar laws.', 'cookieflame' ); ?></span></p>
     5417                    <p data-region="none" style="margin:0;display:none;"><strong><?php esc_html_e( 'No banner:', 'cookieflame' ); ?></strong>
     5418                    <?php esc_html_e( 'No consent banner is shown and all cookies are allowed. Visitors can still change their choices from a [cookieflame_settings_link] link or your [cookieflame_declaration] page. This applies to visitors in countries not covered by the regions above.', 'cookieflame' ); ?></p>
    51545419                </div>
    51555420            </div>
    5156 
    5157             <div id="cf-admin-preview-container" style="position:relative;border-radius:12px;overflow:hidden;min-height:400px;margin-top:4px;">
    5158             </div>
    51595421        </div>
    51605422
    5161         <?php
    5162         $opts = get_option( 'cookieflame_settings', array() );
    5163         $preview_config = array(
    5164             'privacyUrl' => $opts['privacy_url'] ?? '/privacy-policy',
    5165             'ccpaUrl'    => ( $opts['privacy_url'] ?? '/privacy-policy' ) . '#do-not-sell',
    5166         );
    5167         $preview_cfg_json   = wp_json_encode( $preview_config );
    5168         $preview_theme_json = wp_json_encode( $opts['theme'] ?? 'light' );
    5169         $preview_js = <<<'JS'
    5170 (function () {
    5171     var regionEl = document.getElementById("cf-admin-preview-region");
    5172     var themeEl  = document.getElementById("cf-admin-preview-theme");
    5173     var container = document.getElementById("cf-admin-preview-container");
    5174     if (!regionEl || !themeEl || !container) return;
    5175 
    5176     var cfg = __COOKIEFLAME_PREVIEW_CFG__;
    5177 
    5178     var txt = {
    5179         title: "We value your privacy",
    5180         description: "We use cookies to improve your experience, analyse traffic and personalise content. You can choose which categories to allow.",
    5181         acceptAll: "Accept All",
    5182         declineAll: "Decline All",
    5183         savePrefs: "Save Preferences",
    5184         managePrefs: "Manage preferences",
    5185         hidePrefs: "Hide preferences",
    5186         ccpaText: "We use cookies and similar technologies. Under California law, you have the right to opt out of the sale of your personal information.",
    5187         ccpaDnsmpi: "Do Not Sell My Personal Information",
    5188         ccpaOk: "OK"
    5189     };
    5190 
    5191     var categories = [
    5192         { id: "necessary", name: "Necessary", desc: "Required for the site to function. Cannot be disabled.", required: true },
    5193         { id: "preferences", name: "Preferences", desc: "Remember your settings and personalisation choices.", required: false },
    5194         { id: "analytics", name: "Analytics", desc: "Help us understand how visitors use the site.", required: false },
    5195         { id: "marketing", name: "Marketing", desc: "Used to deliver relevant ads and track campaigns.", required: false }
    5196     ];
    5197 
    5198     function esc(s) { var d = document.createElement("div"); d.textContent = s; return d.innerHTML; }
    5199 
    5200     function render() {
    5201         var region = regionEl.value;
    5202         var theme  = themeEl.value;
    5203         var isDark = theme === "dark";
    5204         var bg = isDark ? "#121212" : "#f5f5f5";
    5205         var bannerBg = isDark ? "#1c1c1e" : "#fff";
    5206         var textColor = isDark ? "#f5f5f7" : "#1d1d1f";
    5207         var mutedColor = isDark ? "#98989d" : "#555";
    5208         var borderColor = isDark ? "#2c2c2e" : "#e8e8ed";
    5209         var catDescColor = isDark ? "#636366" : "#888";
    5210         var acceptBg = isDark ? "#0a84ff" : "#0071e3";
    5211         var declineBg = isDark ? "#2c2c2e" : "#e8e8ed";
    5212         var declineColor = isDark ? "#f5f5f7" : "#1d1d1f";
    5213         var saveBg = isDark ? "#30d158" : "#34c759";
    5214         var detailsColor = isDark ? "#636366" : "#888";
    5215         var linkColor = isDark ? "#2997ff" : "#0071e3";
    5216         var toggleBg = isDark ? "#3a3a3c" : "#ccc";
    5217         var toggleActiveBg = isDark ? "#0a84ff" : "#0071e3";
    5218         var sliderBg = isDark ? "#f5f5f7" : "#fff";
    5219         var poweredColor = isDark ? "#888" : "#ccc";
    5220 
    5221         var html = '<div style="background:' + bg + ';padding:60px 20px 20px;min-height:400px;display:flex;align-items:flex-end;justify-content:center;border-radius:12px;">';
    5222 
    5223         if (region === "none") {
    5224             html += '<div style="text-align:center;padding:40px;color:' + mutedColor + ';font-family:-apple-system,BlinkMacSystemFont,\'Segoe UI\',Roboto,sans-serif;font-size:14px;align-self:center;">';
    5225             html += '<div style="font-size:48px;margin-bottom:16px;">&#x1F30D;</div>';
    5226             html += '<div style="font-weight:700;font-size:16px;color:' + textColor + ';margin-bottom:8px;">No banner shown</div>';
    5227             html += '<div>Visitors outside the regulated regions (EU/EEA, UK, US,<br>Switzerland, Brazil, South Africa) see no consent banner.<br>All cookies are auto-granted. The cookie icon appears in the<br>bottom-left corner so they can manage preferences if they choose.</div>';
    5228             html += '<div style="margin-top:24px;width:48px;height:48px;border-radius:50%;background:' + (isDark ? "#f5f5f7" : "#1d1d1f") + ';display:inline-flex;align-items:center;justify-content:center;">';
    5229             html += '<svg width="24" height="24" viewBox="0 0 38 38" fill="none"><path d="M19.1 1.16c-.06-.42-.38-.77-.8-.83A11.8 11.8 0 0 0 13.28 1.2L7.5 4.3A11.8 11.8 0 0 0 3.95 7.98L1.09 13.9A11.8 11.8 0 0 0 .38 18.96l1.13 6.47a11.8 11.8 0 0 0 3.53 4.99l4.71 4.56a11.8 11.8 0 0 0 4.59 2.24l6.48.91a11.8 11.8 0 0 0 5.52-.89l5.78-3.09a11.8 11.8 0 0 0 3.55-3.69l2.87-5.9a11.8 11.8 0 0 0 .71-5.08c-.07-.41-.42-.73-.83-.79a14.8 14.8 0 0 1-8.16-7.61c-.14-.59-.63-1.05-1.23-1.14A14.8 14.8 0 0 1 19.1 1.16ZM15.25 10.25a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5Zm-5 15a2.5 2.5 0 1 1 5 0 2.5 2.5 0 0 1-5 0Zm17.5-5a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5Z" fill="' + (isDark ? "#1d1d1f" : "#fff") + '"/></svg>';
    5230             html += '</div>';
    5231             html += '</div>';
    5232             html += '</div>';
    5233             container.innerHTML = html;
    5234             return;
    5235         }
    5236 
    5237         if (region === "ccpa") {
    5238             html += '<div style="position:relative;bottom:0;left:0;right:0;width:100%;background:' + bannerBg + ';box-shadow:0 -8px 40px rgba(0,0,0,.15);border-radius:20px 20px 0 0;font-family:-apple-system,BlinkMacSystemFont,\'Segoe UI\',Roboto,sans-serif;font-size:14px;color:' + textColor + ';">';
    5239             html += '<div style="max-width:720px;margin:0 auto;padding:20px 32px;display:flex;align-items:center;gap:20px;flex-wrap:wrap;">';
    5240             html += '<div style="flex:1;min-width:280px;"><p style="margin:0;color:' + mutedColor + ';">' + esc(txt.ccpaText) + '</p></div>';
    5241             html += '<div style="display:flex;gap:10px;align-items:center;flex-shrink:0;">';
    5242             html += '<a href="' + esc(cfg.ccpaUrl) + '" style="color:' + linkColor + ';text-decoration:underline;font-size:13px;font-weight:500;white-space:nowrap;">' + esc(txt.ccpaDnsmpi) + '</a>';
    5243             html += '<button style="display:inline-flex;align-items:center;justify-content:center;border-radius:12px;border:none;font-weight:700;cursor:pointer;background:' + acceptBg + ';color:#fff;font-size:15px;padding:14px 32px;box-shadow:0 2px 8px rgba(0,113,227,.3);">' + esc(txt.ccpaOk) + '</button>';
    5244             html += '</div></div>';
    5245             html += '<div style="text-align:center;padding:0 0 12px;font-size:11px;color:' + poweredColor + ';"><a href="https://www.cookieflame.com" target="_blank" rel="noopener" style="color:' + poweredColor + ';text-decoration:none;">Powered by CookieFlame</a></div>';
    5246             html += '</div>';
    5247             html += '</div>';
    5248             container.innerHTML = html;
    5249             return;
    5250         }
    5251 
    5252         html += '<div style="position:relative;bottom:0;left:0;right:0;width:100%;background:' + bannerBg + ';box-shadow:0 -8px 40px rgba(0,0,0,.15);border-radius:20px 20px 0 0;font-family:-apple-system,BlinkMacSystemFont,\'Segoe UI\',Roboto,sans-serif;font-size:14px;color:' + textColor + ';">';
    5253         html += '<div style="max-width:720px;margin:0 auto;padding:32px 32px 20px;">';
    5254         html += '<p style="font-size:20px;font-weight:800;margin:0 0 8px;letter-spacing:-.3px;">' + esc(txt.title) + '</p>';
    5255         html += '<p style="margin:0 0 20px;color:' + mutedColor + ';font-size:14px;line-height:1.6;">' + esc(txt.description) + ' <a href="' + esc(cfg.privacyUrl) + '" style="color:' + linkColor + ';text-decoration:none;font-weight:500;">Privacy Policy</a></p>';
    5256 
    5257         html += '<div style="display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin-bottom:16px;">';
    5258         html += '<button style="display:inline-flex;align-items:center;justify-content:center;border-radius:12px;border:none;font-weight:700;cursor:pointer;background:' + acceptBg + ';color:#fff;font-size:15px;padding:14px 32px;box-shadow:0 2px 8px rgba(0,113,227,.3);">' + esc(txt.acceptAll) + '</button>';
    5259         html += '<button id="cf-admin-preview-manage" style="background:transparent;color:' + detailsColor + ';padding:11px 4px;font-weight:500;font-size:13px;border:none;cursor:pointer;">' + esc(txt.managePrefs) + '</button>';
    5260         html += '</div>';
    5261 
    5262         html += '<div id="cf-admin-preview-cats" style="display:none;margin:0 0 16px;border:1px solid ' + borderColor + ';border-radius:14px;overflow:hidden;">';
    5263         for (var i = 0; i < categories.length; i++) {
    5264             var cat = categories[i];
    5265             var isLast = i === categories.length - 1;
    5266             var checkedAttr = cat.required ? " checked disabled" : "";
    5267             var alwaysOn = cat.required ? ' <span style="font-weight:400;font-size:11px;color:' + (isDark ? "#48484a" : "#aaa") + ';">(always on)</span>' : "";
    5268 
    5269             html += '<div style="padding:16px 20px;' + (isLast ? "" : "border-bottom:1px solid " + borderColor + ";") + 'display:flex;justify-content:space-between;align-items:center;gap:16px;">';
    5270             html += '<div><div style="font-weight:700;font-size:14px;">' + esc(cat.name) + alwaysOn + '</div>';
    5271             html += '<div style="font-size:12px;color:' + catDescColor + ';margin-top:3px;">' + esc(cat.desc) + '</div></div>';
    5272             html += '<label style="position:relative;width:46px;height:26px;flex-shrink:0;display:block;">';
    5273             html += '<input type="checkbox"' + checkedAttr + ' style="opacity:0;width:0;height:0;position:absolute;" />';
    5274             html += '<span style="position:absolute;inset:0;background:' + (cat.required ? toggleActiveBg + ";opacity:.5" : toggleBg) + ';border-radius:26px;cursor:' + (cat.required ? "default" : "pointer") + ';transition:background .2s;"></span>';
    5275             html += '<span style="position:absolute;width:20px;height:20px;left:' + (cat.required ? "23px" : "3px") + ';bottom:3px;background:' + sliderBg + ';border-radius:50%;box-shadow:0 1px 3px rgba(0,0,0,.15);transition:transform .2s;"></span>';
    5276             html += '</label></div>';
    5277         }
    5278         html += '</div>';
    5279 
    5280         html += '<div id="cf-admin-preview-pref-btns" style="display:none;gap:10px;flex-wrap:wrap;align-items:center;margin-bottom:8px;">';
    5281         html += '<button style="display:inline-flex;align-items:center;justify-content:center;border-radius:12px;border:none;font-weight:700;cursor:pointer;background:' + saveBg + ';color:#fff;font-size:14px;padding:13px 28px;box-shadow:0 2px 8px rgba(52,199,89,.3);">' + esc(txt.savePrefs) + '</button>';
    5282         html += '<button style="display:inline-flex;align-items:center;justify-content:center;border-radius:12px;border:none;font-weight:700;cursor:pointer;background:' + declineBg + ';color:' + declineColor + ';font-size:13px;padding:11px 22px;">' + esc(txt.declineAll) + '</button>';
    5283         html += '</div>';
    5284 
    5285         html += '</div>';
    5286         html += '<div style="text-align:center;padding:0 0 16px;font-size:11px;color:' + poweredColor + ';"><a href="https://www.cookieflame.com" target="_blank" rel="noopener" style="color:' + poweredColor + ';text-decoration:none;">Powered by CookieFlame</a></div>';
    5287         html += '</div>';
    5288         html += '</div>';
    5289 
    5290         container.innerHTML = html;
    5291 
    5292         var manageBtn = document.getElementById("cf-admin-preview-manage");
    5293         var catsEl = document.getElementById("cf-admin-preview-cats");
    5294         var prefBtnsEl = document.getElementById("cf-admin-preview-pref-btns");
    5295         if (manageBtn && catsEl) {
    5296             manageBtn.addEventListener("click", function () {
    5297                 var open = catsEl.style.display === "none";
    5298                 catsEl.style.display = open ? "block" : "none";
    5299                 if (prefBtnsEl) prefBtnsEl.style.display = open ? "flex" : "none";
    5300                 manageBtn.textContent = open ? txt.hidePrefs : txt.managePrefs;
    5301             });
    5302         }
    5303     }
    5304 
    5305     render();
    5306     regionEl.addEventListener("change", render);
    5307     themeEl.addEventListener("change", render);
    5308 
    5309     themeEl.value = __COOKIEFLAME_PREVIEW_THEME__;
    5310     render();
    5311 })();
    5312 JS;
    5313         $preview_js = str_replace(
    5314             array( '__COOKIEFLAME_PREVIEW_CFG__', '__COOKIEFLAME_PREVIEW_THEME__' ),
    5315             array( $preview_cfg_json, $preview_theme_json ),
    5316             $preview_js
    5317         );
    5318         wp_print_inline_script_tag( $preview_js );
    5319         ?>
    53205423
    53215424        <?php endif; ?>
  • cookieflame/trunk/readme.txt

    r3769546 r3771537  
    55Tested up to: 7.1
    66Requires PHP: 8.0
    7 Stable tag: 5.3.0
     7Stable tag: 5.4.0
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
    1010
    11 Cookie consent for WordPress that finds cookies automatically. Runs continuously, no external service, correct banner for GDPR, CCPA, LGPD, POPIA.
     11Cookie consent for WordPress that finds cookies automatically. Self-hosted, with the right banner for GDPR, CCPA, LGPD and POPIA visitors.
    1212
    1313== Description ==
    1414
    15 CookieFlame is a cookie consent plugin for WordPress that does three things most other free plugins do not. It finds the cookies on your site automatically by watching real visitor sessions. It runs entirely on your own server with no outside connections. And it shows the right banner for each visitor based on where they are, rather than showing a single GDPR banner to the whole world.
     15CookieFlame is a cookie consent plugin that finds your site's cookies for you, runs on your own server, and shows each visitor the banner their region requires. Full details are at [cookieflame.com](https://cookieflame.com).
    1616
    17 It is built by [Zoobu] and the full product site is at [cookieflame.com](https://cookieflame.com).
     17= Finds cookies for you =
    1818
    19 = 🔍 Cookie discovery, continuously =
     19* **Watch for new cookies:** your visitors' browsers report cookies as they browse, so a new plugin's or tag's cookies appear in the Cookie Manager without you scanning. Get an email about changes weekly, daily, or not at all.
     20* **Cookie Discovery:** for a full list now, browse your own site with every cookie allowed for your browser only. It uses your own browser.
    2021
    21 Most consent plugins expect you to know which cookies your site sets. CookieFlame does not. A lightweight beacon watches real visitor sessions in the background and reports any cookies it has not seen before. New plugin installed? Its cookies surface automatically. Tracking tag changed? Picked up next time a visitor triggers it. No scheduled scans, no headless browsers, no external scanning service.
     22= The right banner for each region =
    2223
    23 For a one-off inventory you can also run an on-demand Discovery scan from the admin. Browse your own site as logged in, watch the discovery bar accumulate cookies and detected services, click Finish when done.
     24* UK, EU/EEA, Switzerland, Brazil and South Africa: an opt-in banner with Accept All and Manage preferences. You can also show Decline All on the first screen (off by default).
     25* United States: a short notice with a "Do Not Sell or Share My Personal Information" link that opens a real opt-out choice.
     26* Everywhere else: no banner.
    2427
    25 = 🌍 Correct behaviour per region =
     28Country comes from CDN headers such as Cloudflare's, or a free MaxMind database you upload, with the time zone as a fallback. If none works, the opt-in banner is shown.
    2629
    27 CookieFlame detects visitor country and shows the banner that region actually requires:
     30= Global Privacy Control =
    2831
    29 * EU, UK, EEA, Switzerland: full two-step GDPR opt-in banner with Accept, Manage Preferences, and Decline
    30 * California and other US states with similar laws: compact CCPA opt-out notice with a Do Not Sell link
    31 * Brazil (LGPD) and South Africa (POPIA): GDPR-style opt-in banner
    32 * Everywhere else: no banner, all cookies auto-granted
     32A browser that sends Global Privacy Control (GPC) is opted out of selling and sharing straight away: marketing cookies stay off and Google's ad signals are denied. US visitors see that the signal was honoured, and OK only closes the notice. The opt-out is saved, so a later visit without GPC keeps it. A visitor can opt back in only through a separate, clear choice.
    3333
    34 Detection uses three layers in order: Cloudflare and other CDN country headers first (zero latency, works with every caching setup), then a local MaxMind GeoLite2 database if uploaded, then a browser timezone heuristic as a last-resort fallback. If all three fail, the plugin defaults to GDPR opt-in for safety rather than silently granting consent.
     34= Blocks trackers before consent =
    3535
    36 = 🔒 No outside connections =
     36Tracking scripts are rewritten on your server before the page is sent, so blocking works with WP Rocket, LiteSpeed, Cloudflare APO and other caches. Built-in patterns cover Google Analytics, Google Ads, Google Tag Manager, Microsoft Ads, Meta, X (Twitter), LinkedIn, Pinterest, Reddit and TikTok. Cookies you classify get matching rules automatically.
    3737
    38 CookieFlame does not load JavaScript from any content delivery network, does not call any geo API, does not phone home. Every part of the consent flow runs on your server and in the visitor's browser. The only optional outbound request is Google Tag Manager itself, if you enter a GTM container ID in the settings (see the External services section below).
     38YouTube, Vimeo, Google Maps, Spotify, SoundCloud, Dailymotion and Elementor videos show a click-to-load placeholder until the visitor consents.
    3939
    40 This matters because a consent tool that itself sets third-party cookies is an awkward contradiction, and because self-hosted plugins keep working during outages of services you did not choose to depend on.
     40= Google Consent Mode v2 =
    4141
    42 = 🚫 Server-side script blocking =
     42All Google consent signals start as denied before any tag runs and update the moment a visitor chooses. Returning visitors' choices are restored before Google Tag Manager loads.
    4343
    44 When a visitor has not granted consent, CookieFlame rewrites tracking scripts in the HTML output so the browser does not execute them. This happens before the page leaves your server, which means it works with any caching stack: WP Rocket, LiteSpeed, Cloudflare APO, W3 Total Cache, and others all hold the correct pre-modified HTML. A lighter JavaScript layer handles scripts injected later by GTM or other plugins.
     44= For developers =
    4545
    46 Known trackers are blocked from the moment you install the plugin. Ten services ship with built-in patterns: Google Analytics, Google Ads, Google Tag Manager, Microsoft Ads, Meta and Facebook, Twitter and X, LinkedIn, Pinterest, Reddit, TikTok. Anything else you classify in the Cookie Manager gets a matching block rule generated automatically.
     46* Events: cc:ready, cc:update, cc:accept-all, cc:decline-all, cc:save and cc:banner-rendered.
     47* JavaScript API: CookieConsent.getState(), show(), showSettings() and more.
     48* [cookieflame_settings_link] shortcode, or the class cc-open-settings on any link, to reopen a visitor's choices.
     49* CSS custom properties (--cc-accent, --cc-bg, --cc-radius, --cc-font and others) to restyle the banner.
     50* Filters cookieflame_frontend_config and cookieflame_digest_email, and the action cookieflame_settings_sections.
     51* An optional consent log endpoint for your own audit trail.
    4752
    48 = 🪟 Iframe facades =
     53= CookieFlame Pro =
    4954
    50 YouTube, Vimeo, Google Maps, Spotify, SoundCloud, Dailymotion, and Elementor video widgets are replaced with click-to-accept placeholders until the visitor grants consent. The iframe source is stripped synchronously before the embed has a chance to contact its origin.
     55Pro identifies and classifies cookies automatically, creates the blocking rules, and adds instant emails. See [cookieflame.com](https://cookieflame.com).
    5156
    52 = 📊 Google Consent Mode v2 =
     57= Compliance note =
    5358
    54 All seven Google consent signals are managed automatically and set to denied by default before any tag runs. When a visitor grants consent, the correct signals update immediately. Returning visitors keep their stored choices, with the signals restored before GTM loads (so the first dataLayer push is already correct).
    55 
    56 Works with a GTM container ID you paste into the settings. No separate GTM consent template, no additional configuration. The plugin also injects the body noscript tag in the correct position.
    57 
    58 = 🛡️ Global Privacy Control =
    59 
    60 GPC is detected automatically and suppresses marketing consent for CCPA visitors. Required by California, Colorado, Connecticut, Texas, and nine other US states. Turning GPC off later restores the visitor's original stored choice rather than overwriting it.
    61 
    62 = 📜 IAB TCF v2.2 =
    63 
    64 A TCF v2.2 stub is registered on every page so ad-tech vendors that need TCF signals can read them.
    65 
    66 = 🚀 Lightweight and framework-free =
    67 
    68 The frontend JavaScript is a single 37 KB file (12 KB gzipped) with no jQuery and no framework dependencies. A separate facades bundle is only loaded on pages that actually contain a blockable embed. The Discovery bundle only loads for administrators during an active scan.
    69 
    70 = 👨‍💻 Developer-friendly =
    71 
    72 * Event API with CustomEvents: cc:ready, cc:update, cc:accept-all, cc:decline-all, cc:save
    73 * Public JavaScript API for reading consent state and showing the banner on demand
    74 * Per-category onAccept and onReject callbacks
    75 * Extension points via WordPress filters and actions
    76 * Optional consent log endpoint for audit trails
    77 
    78 = ✨ Want automatic classification? =
    79 
    80 CookieFlame Pro adds automatic cookie classification using a central database and AI. You click Auto-Identify All, and the plugin classifies every unclassified cookie with vendor, category, and purpose, then generates the blocking rules automatically. Flat annual pricing by number of sites, no pageview limits. Learn more at [cookieflame.com](https://cookieflame.com).
    81 
    82 = 📋 Compliance note =
    83 
    84 CookieFlame provides tools that help support compliance with GDPR, CCPA, LGPD, POPIA and similar regulations. Compliance itself is the site owner's responsibility and depends on how the plugin is configured. For legal advice, consult a qualified professional.
     59CookieFlame helps with GDPR, CCPA, LGPD, POPIA and similar laws; how you set it up is your responsibility. For legal advice, ask a qualified professional.
    8560
    8661== Installation ==
    8762
    88 1. Upload the `cookieflame` folder to `/wp-content/plugins/`, or install via Plugins > Add New in the WordPress admin
    89 2. Activate the plugin through the Plugins menu
    90 3. Go to **CookieFlame** in the admin menu to review settings
     631. Install from Plugins > Add New, or upload the `cookieflame` folder to `/wp-content/plugins/`.
     642. Activate the plugin.
     653. Open **CookieFlame** in the admin menu to check the settings.
    9166
    92 That is it. The plugin works immediately with sensible defaults. The ten built-in tracking services are blocked before any visitor can trigger them, and the banner shows correctly for each region.
    93 
    94 For best geo detection, either run Cloudflare (the plugin reads the CF-IPCountry header automatically) or upload a free MaxMind GeoLite2 database on the Geo Detection tab. Neither is required.
     67It works straight away. For the best country detection, use Cloudflare or upload a free MaxMind database on the Geo Detection tab.
    9568
    9669== Frequently Asked Questions ==
    … …  
    9871= Do I need to know which cookies my site sets? =
    9972
    100 No. CookieFlame watches real visitor sessions and reports new cookies to the Cookie Manager automatically. You classify them when you are ready, and the plugin generates the blocking rules for you.
     73No. Watch for new cookies lists them as your visitors' browsers report them, and Cookie Discovery finds them on demand. You classify them when you are ready.
    10174
    102 = Does this work with Google Tag Manager? =
     75= Does it work with Google Tag Manager and caching plugins? =
    10376
    104 Yes. Enter your GTM container ID on the Tag Managers tab and the plugin injects both the head script and body noscript tag in the correct positions. All GTM tags that respect Google Consent Mode will see the denied state on load and wait for consent.
     77Yes. Blocking happens in the HTML before it is cached. Turn off Cloudflare Rocket Loader; the plugin warns you if it is on.
    10578
    106 = Does this work with Cloudflare, WP Rocket, and other caching setups? =
     79= What do US visitors see? =
    10780
    108 Yes. Script blocking runs server-side in the PHP output buffer, so the cached HTML already has the correct tags rewritten. Cloudflare is specifically supported for geo detection via the CF-IPCountry header on all plans including free.
     81A short notice. Cookies are allowed by default because US laws use an opt-out model. The "Do Not Sell or Share My Personal Information" link opens a choice to opt out. Put [cookieflame_settings_link] in your footer so visitors can reach it again later.
    10982
    110 One exception: Cloudflare Rocket Loader rewrites scripts at the edge after PHP has run, which defeats the blocking layer. Rocket Loader must be disabled when using CookieFlame. The plugin detects this and shows a warning if it is active.
     83= Can I show "Decline all" on the first screen? =
    11184
    112 = What happens for US visitors? =
     85Yes. Turn on "Show 'Decline all' on the first screen" on the Settings tab. It appears only on the opt-in banner, never on the US notice.
    11386
    114 US visitors see a compact CCPA opt-out notice at the bottom of the page with a Do Not Sell My Personal Information link. Cookies are auto-granted by default because CCPA uses an opt-out model, and the visitor can revoke consent through the notice.
     87= Can I change the banner text? =
    11588
    116 = What happens for visitors outside the regulated regions? =
    117 
    118 No banner is shown and all cookies are auto-granted. CookieFlame does not show GDPR banners to visitors whose region does not require them.
    119 
    120 = Does the plugin contact any external service? =
    121 
    122 By default, no. Geo detection reads CDN headers first, then a local MaxMind database if you have uploaded one, then the browser timezone as a last resort. No external geo API is called. The plugin does not load scripts from any CDN and does not phone home.
    123 
    124 The one optional external request is Google Tag Manager, but only if you have entered a GTM container ID in the settings. See the External services section for full details.
    125 
    126 = Does it handle Global Privacy Control (GPC)? =
    127 
    128 Yes. GPC is required by twelve US states and the plugin detects the browser signal automatically. For CCPA visitors with GPC active, marketing consent is suppressed before any tag runs. Turning GPC off later restores the visitor's original choice.
    129 
    130 = Can I customise the banner text and colours? =
    131 
    132 Yes. Every string shown to visitors is editable through the Settings tab, and the banner supports both light and dark themes. For per-language versions, use WPML or Polylang with the standard WordPress translation pipeline.
    133 
    134 = What is CookieFlame Pro? =
    135 
    136 Pro adds automatic cookie classification. Instead of classifying each cookie manually, you click Auto-Identify All and the plugin uses a central database of known cookies plus AI to classify them with vendor, category, and purpose. Pro also adds tighter monitoring intervals, a more detailed digest email, and automatic MaxMind database updates. Pricing is flat annual by site count with no pageview limits. See [cookieflame.com](https://cookieflame.com) for details.
     89Not yet from the settings. The banner is in English, with light and dark themes. Developers can change the text and colours with the cookieflame_frontend_config filter and the --cc-* CSS properties.
    13790
    13891= Is my visitors' data safe? =
    13992
    140 CookieFlame does not collect visitor data. IP addresses are hashed for rate-limiting and never stored raw. Cookie values are never transmitted, only their name and a shape classification (uuid, timestamp, and similar) used for identification. The plugin stores consent decisions in a first-party cookie on the visitor's own browser.
     93CookieFlame collects no visitor data. Cookie values are never sent, only names. IP addresses are hashed for rate limiting and never stored. Choices are kept in a first-party cookie in the visitor's browser.
    14194
    14295== External services ==
    14396
    144 CookieFlame is a self-hosted plugin. By default it does not contact any external service from the visitor's browser, and it does not phone home.
     97CookieFlame does not phone home. These requests happen only in the cases described.
    14598
    146 There is one optional external interaction, only if the site owner configures it.
     99**Google Tag Manager (optional).** If you enter a container ID on the Tag Managers tab, Google's script (https://www.googletagmanager.com/gtm.js) loads on every page, except for visitors you exclude from tracking. Tags inside it follow the visitor's Consent Mode choices. Sent: the container ID and the browser's standard request data (IP address, user agent, page URL). [Terms](https://marketingplatform.google.com/about/analytics/tag-manager/use-policy/), [privacy policy](https://policies.google.com/privacy).
    147100
    148 **Google Tag Manager (optional)**
     101**Microsoft UET (optional).** If you enter a UET tag ID, Microsoft's loader (https://bat.bing.com/bat.js) loads on every page, except for visitors you exclude from tracking. It records nothing until the visitor allows Marketing. Sent: the tag ID and the browser's standard request data. [Microsoft Advertising agreement](https://about.ads.microsoft.com/en/resources/policies/microsoft-advertising-agreement), [privacy statement](https://privacy.microsoft.com/en-gb/privacystatement).
    149102
    150 If the site owner enters a Google Tag Manager container ID on the CookieFlame Tag Managers settings tab, CookieFlame injects the standard Google Tag Manager script (https://www.googletagmanager.com/gtm.js) into the site front-end under the visitor's consent. This script is provided by Google and is loaded from Google's servers when the page is viewed.
     103**YouTube thumbnails.** When a page has a YouTube embed that is waiting for consent, its placeholder shows the video's thumbnail from img.youtube.com. Sent: the video ID and the browser's standard request data. [YouTube terms](https://www.youtube.com/t/terms), [Google privacy policy](https://policies.google.com/privacy).
    151104
    152 * What is sent: standard Google Tag Manager request, including the container ID and whatever browser request data Google receives for any embed.
    153 * When: only if the admin has set a GTM container ID, and only when a visitor loads a page and has consented.
    154 * Google Tag Manager terms: https://marketingplatform.google.com/about/analytics/tag-manager/use-policy/
    155 * Google privacy policy: https://policies.google.com/privacy
     105**Consent log endpoint (optional).** If you enter a URL in the Consent Log Endpoint setting, each consent choice is sent there from the visitor's browser as JSON: an anonymous ID, time, action, the choice per category, region, country code, page URL, user agent, the GPC flag, consent version and plugin version. No cookie values or IP address are in it. You choose the destination and are responsible for it.
    156106
    157 If no GTM container ID is set, no request is made.
    158 
    159 **Microsoft UET (Bing Ads tag, optional)**
    160 
    161 If the site owner enters a Microsoft UET tag ID on the CookieFlame Tag Managers settings tab, CookieFlame injects the standard Microsoft UET loader script (https://bat.bing.com/bat.js) into the site front-end. The loader is provided by Microsoft and is loaded from Microsoft's servers when the page is viewed.
    162 
    163 The UET runtime reads Microsoft Consent Mode v2 signals from the dataLayer that CookieFlame already manages, so the loader honours visitor consent internally: tracking events do not record until the visitor has granted Marketing consent. The loader script itself is fetched on every page load while a UET tag ID is configured.
    164 
    165 * What is sent: the UET tag ID, plus the standard browser request data Microsoft receives for any third-party script load (visitor IP, user agent, page URL, referer).
    166 * When: every page load while a UET tag ID is configured, except for visitors whose browser sends the Do Not Track header in which case CookieFlame suppresses the injection entirely.
    167 * Microsoft Advertising agreement: https://about.ads.microsoft.com/en/resources/policies/microsoft-advertising-agreement
    168 * Microsoft privacy statement: https://privacy.microsoft.com/en-gb/privacystatement
    169 
    170 If no UET tag ID is set, no request is made.
    171 
    172 **Consent log endpoint (optional, site-owner-nominated webhook)**
    173 
    174 If the site owner enters a URL in the Consent Log endpoint setting, CookieFlame sends a JSON record of each consent decision to that URL from the visitor's browser via navigator.sendBeacon (with a fetch fallback for browsers without sendBeacon). The default for this setting is empty, in which case nothing is sent. The destination URL is nominated by the site owner; terms and privacy obligations for that URL are the site owner's responsibility, not CookieFlame's.
    175 
    176 * What is sent: a JSON payload containing an anonymous ID, an ISO-8601 timestamp, the action that produced the record (accept-all, decline-all, save, or one of the region-based auto-grants), the per-category consent decision (necessary, preferences, analytics, marketing), the compliance mode (gdpr, ccpa, none), the visitor country code, the page URL, the user agent string, the TCF v2.2 flag, the Global Privacy Control flag, the consent policy version, and the plugin version. No cookie values are included. No IP address is included in the payload, although the receiving endpoint will of course see the visitor IP via the underlying HTTP request, the same as any other request.
    177 * When: every time the visitor's consent state is set. This includes banner actions (accept-all, decline-all, save) and region-based auto-grants for visitors outside GDPR regions and for CCPA opt-out visitors.
    178 
    179 If no Consent Log endpoint URL is set, no request is made.
    180 
    181 **Domain strings in the plugin code**
    182 
    183 The plugin code contains many third-party domain names (for example, connect.facebook.net, googletagmanager.com, and similar). These are pattern strings used for local matching only. They identify scripts that may already be present on the admin's site so CookieFlame can detect them, show them to the admin in the Cookie Manager, and block them until the visitor has given consent. CookieFlame does not fetch from these domains.
     107Third-party domain names in the code (such as connect.facebook.net) are only used to recognise and block scripts; they are never contacted.
    184108
    185109== Screenshots ==
    186110
    187 1. GDPR consent banner with category toggles (two-step flow)
    188 2. CCPA opt-out notice for US visitors
    189 3. Cookie Manager showing discovered cookies with their origin
    190 4. Cookie Discovery in progress: the discovery bar accumulates services
    191 5. Admin Settings tab with theme and regional behaviour controls
    192 6. Geo Detection tab with CDN status and MaxMind upload
    193 7. Tag Managers tab for GTM and Microsoft UET
     1111. Opt-in banner with category toggles
     1122. US notice with the "Do Not Sell or Share" link
     1133. Cookie Manager listing your site's cookies
     1144. Cookie Discovery in progress
     1155. Settings tab
     1166. Geo Detection tab
     1177. Tag Managers tab
    194118
    195119== Changelog ==
    196120
    197 = 5.3.0 =
    198 * Cookie monitoring now ignores cookies that come with a visitor's browser rather than from your site. Some browser extensions write the "already accepted" cookies of other consent tools (such as Complianz, CookieYes, Cookiebot, OneTrust or Didomi) onto every site they visit, and these were being reported as your site's cookies. A consent tool's cookie is now ignored when that tool doesn't run on your site: its plugin isn't active, its script isn't on the page, and no script on the page wrote the cookie.
    199 * Cookies written by a browser extension's own code are ignored too.
    200 * Cookies ignored this way go on the Permanently Ignored list with the reason, and the list now shows why each cookie was ignored. Restoring one keeps it monitored from then on.
    201 * The Cookie Manager and the Cookie Discovery count leave these cookies out too, including ones your own browser extensions add. Cookies that came with your browser during Cookie Discovery go on the Permanently Ignored list with the reason. Cookies on the Permanently Ignored list no longer show in the Cookie Manager just because they are in your own browser.
    202 * On update, other consent tools' cookies that were already reported are moved to the Permanently Ignored list. Cookies you classified yourself are left alone.
    203 * Fixed: the "consent plugin detected" conflict warning could be set off by CookieFlame's own list of cookie names rather than by another consent plugin on the page.
    204 * Cookie Manager: the last column is now "Status". Each cookie shows one label for where it stands (Known, Auto-Identified, Classified, Added by you, Changed by you, or Not identified yet), and cookies that aren't identified yet say where they came from: found by your scan, reported by visitors, or seen in your own browser.
    205 * Cookie Manager: Remove and Ignore permanently are now one "Remove…" link that asks why. "My site doesn't use it any more" takes the cookie off your list, and it comes back if it's seen again. "It's not from my site" puts it on the Permanently Ignored list for good.
    206 * Cookie Manager: with CookieFlame Pro, the padlock is now a "Lock as correct" button. A locked cookie keeps its vendor, category and purpose, even when you re-identify.
    207 * Cookie Manager: the small help buttons beside each link are gone. Each link explains itself when you hover over it, and the Status column's help button explains every label and action.
    208 * Improved: when another consent tool's code is on your pages, the warning now says so in plain words ("Cookiebot found on your pages"), names the page and the text to look for, and lists where such code usually lives (plugins, page builder custom code such as Elementor > Custom Code, the theme's header, or a header and footer scripts plugin). It used to say a plugin was installed even when the code came from somewhere else.
    209 
    210 = 5.2.0 =
    211 * Cookie monitoring now records which script first wrote each new cookie: a third-party script's domain, or the WordPress plugin or theme that set it. Previously only the full list of third-party scripts on the page was known. Cookie values are never read or sent.
    212 * Cookie monitoring now checks again later in the visit, a few seconds after the visitor changes their consent choice, and when they leave the page, so cookies set after consent or after the first few seconds are found.
    213 * Fixed: when a page reported several new cookies at the same moment, some could be lost, because the reports overwrote each other when saving. Reports are now saved one at a time, so every new cookie is kept.
    214 * The cookieflame_new_cookie_discovered action now fires after the cookie's details are saved and passes them as a fourth argument (pre-existing flag, value type, writer). Listeners using three arguments are unaffected.
    215 * Tested up to WordPress 7.1.
    216 
    217 = 5.1.1 =
    218 * Cookie declaration shortcode tag is now [cookieflame_declaration] (previously [cookieconsent_declaration]). If you embed the cookie declaration on your Privacy Policy page, update the shortcode tag.
    219 * URL fields on the Settings page (Privacy URL, CCPA URL, Consent log endpoint) now use esc_url_raw, the WordPress URL sanitiser, in place of sanitize_text_field.
    220 
    221 = 5.1.0 =
    222 * Cookie Monitoring tab redesigned for clarity. The page now leads with the two controls that matter (Monitoring on/off and Update interval) and trims the long-form intro and guidance copy that buried them.
    223 * New: the Update interval dropdown shows CookieFlame Pro intervals (15 minutes, 1 hour, 4 hours) inline as disabled options labelled "(Pro)", so you can see at a glance what an upgrade adds without leaving the page. They unlock when CookieFlame Pro is active.
    224 * Removed the Monthly interval. It was producing digests so far apart that newly discovered cookies were already weeks stale by the time the email arrived. Existing sites set to Monthly will move to Daily on update.
    225 * Removed the in-page "Clear monitoring history" button. The 30-day auto-purge handles staleness without intervention; for the rare support case where a full reset is needed, the new WP-CLI command "wp cookieflame purge-beacon-seen" replaces the button.
    226 * Filter contract change for developers extending the interval list: cookieflame_interval_buckets now passes tier-aware entries (each entry shaped ['minutes' => N, 'tier' => 'free'|'pro']). Plain integers from third-party filter contributors are still accepted and default to the free tier for back-compatibility.
    227 
    228 = 5.0.2 =
    229 * Fixed: Geo Detection now correctly recognises the Cloudflare CF-IPCountry header again. Sites fronted by Cloudflare without a MaxMind database installed were falling through to the GDPR opt-in safety default for every visitor, which meant CCPA-eligible US visitors saw the GDPR banner instead of the CCPA notice, and visitors in regions where no banner is required also saw the GDPR banner. The Geo Detection admin panel was reporting "Not configured" on Cloudflare-fronted sites for the same reason. No setting changes are required; geo detection resumes automatically on update.
    230 
    231 = 5.0.1 =
    232 * Hardened consent boot: the five wp_head injection hooks (consent defaults, config, frontend script, Google Tag Manager, Microsoft UET) now register at deep-negative priorities so other plugins cannot hook ahead of CookieFlame and push tags before consent signals are set.
    233 * Beacon endpoint refactored: the per-IP rate limit now runs as a REST permission_callback, rejecting bursts at the routing layer above the request handler. The same check still runs inside the handler as a belt-and-braces guard.
    234 * Defensive shutdown handler added for the output buffer: walks the buffer stack at end of request and flushes only CookieFlame's own buffer if it remains open, leaving any caching or optimisation plugin buffers above it untouched.
    235 * Database queries against the beacon-seen table reworked to inline the WordPress table prefix directly in each SQL literal, with rationale comments on the remaining advisory warnings.
    236 * wp_unslash applied consistently to display-only query parameters on the Cookie Monitoring tab.
    237 * Readme: added an Upgrade Notice section.
    238 
    239 = 5.0.0 =
    240 * Initial public release.
     121= 5.4.0 =
     122* New: an option to show "Decline All" on the banner's first screen, next to Accept All. Off by default, and only on the opt-in banner (UK, EU/EEA, Switzerland, Brazil, South Africa), never on the US notice.
     123* Global Privacy Control now follows California's rules (§7025): OK only closes the US notice, the notice says the signal was honoured, the opt-out is saved, and visitors can opt back in only through a separate choice.
     124* "Do Not Sell or Share My Personal Information" now opens a real opt-out choice on the page, instead of linking to your privacy page.
     125* Cookie Monitoring is now two settings: "Watch for new cookies" On/Off, and "Email me about changes": Weekly (the default for new sites), Daily, Instantly (Pro) or Off. Your old setting carries over; 15 minutes, 1 hour and 4 hours become Instantly with Pro and Daily without it.
     126* New: [cookieflame_settings_link] shortcode and a cc-open-settings class for a "Cookie settings" link. The cookie declaration now has a "Change cookie preferences" button.
     127* New: "Visitor logins" setting. When ticked, the cookie declaration lists the WordPress login cookies under "If you log in". Otherwise they are left out, so every visitor sees the same declaration.
     128* The upgrade message at the top of the CookieFlame pages can now be dismissed.
     129* For developers: --cc-* CSS properties, two filters, an action and the cc:banner-rendered event (see For developers).
     130* The IAB TCF placeholder, which never carried a consent string, is now off by default.
     131* Fixed: a Re-consent Period below 1 day made the banner appear on every page. It now means the default of 365 days.
     132* Fixed: the US notice could run off the screen on phones.
     133* Fixed: the X (Twitter) embed placeholder also caught other sites whose names end in "x.com", such as netflix.com and box.com.
     134* Fixed: the WordPress cookie list: login cookie expiry, comment and protected-page cookies, the wp-settings-time description, and PHP and WooCommerce cookies listed as WordPress core.
     135* Fixed: saving Cookie Monitoring settings, or switching monitoring on or off, turned "Exclude staff from tracking" off.
     136* Older changes: changelog.txt.
    241137
    242138== Upgrade Notice ==
    243139
    244 = 5.3.0 =
    245 Cookie monitoring now ignores other consent tools' cookies that visitors' browser extensions add, so they no longer appear as your site's cookies. The Cookie Manager's last column is tidier and easier to understand.
    246 
    247 = 5.2.0 =
    248 Better cookie detection: CookieFlame now records which script set each new cookie and keeps checking after consent is given.
    249 
    250 = 5.1.0 =
    251 Cookie Monitoring tab redesigned and the Monthly interval removed. If you were on Monthly, your setting will move to Daily on update. The "Clear monitoring history" button has been replaced with a WP-CLI command for the rare cases where it is needed.
    252 
    253 = 5.0.2 =
    254 Bug fix release. Restores Cloudflare CF-IPCountry geo detection on sites fronted by Cloudflare without MaxMind. Recommended for all users on Cloudflare.
    255 
    256 = 5.0.1 =
    257 Compatibility and hardening release. Refactors a public endpoint's permission check, hardens consent boot priority, and addresses static-analysis warnings. No setting or visitor-facing behaviour changes.
    258 
    259 = 5.0.0 =
    260 Initial public release.
     140= 5.4.0 =
     141Optional "Decline all" button, Global Privacy Control handled to California's rules, a real "Do Not Sell or Share" choice, and simpler Cookie Monitoring settings. Your settings carry over.
  • cookieflame/trunk/uninstall.php

    r3769546 r3771537  
    3737delete_option( 'cookieflame_maxmind_last_error' );
    3838delete_option( 'cookieflame_beacon_seen_schema' );
     39delete_metadata( 'user', 0, 'cookieflame_upgrade_banner_dismissed', '', true );
    3940
    4041// Drop the beacon-seen custom table (only on opt-in deletion).
Note: See TracChangeset for help on using the changeset viewer.