1// EVXL â site behaviors (theme toggle, mobile drawer, sticky CTA) 2(function () { 3 // ---- Theme (light/dark) ---- 4 var STORAGE_KEY = 'dxl-theme'; 5 function setTheme(t) { 6 document.documentElement.setAttribute('data-theme', t); 7 try { localStorage.setItem(STORAGE_KEY, t); } catch (e) {} 8 } 9 function getTheme() { 10 try { var t = localStorage.getItem(STORAGE_KEY); if (t) return t; } catch (e) {} 11 return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; 12 } 13 // apply ASAP to avoid FOUC (the inline script in <head> usually does this; 14 // we set again here in case the page didn't include one) 15 if (!document.documentElement.getAttribute('data-theme')) setTheme(getTheme()); 16 17 document.addEventListener('click', function (e) { 18 var t = e.target.closest('[data-toggle="theme"]'); 19 if (t) { 20 e.preventDefault(); 21 var cur = document.documentElement.getAttribute('data-theme'); 22 setTheme(cur === 'dark' ? 'light' : 'dark'); 23 } 24 }); 25 26 // ---- Mobile drawer ---- 27 function setDrawer(open) { 28 var d = document.getElementById('drawer'); 29 if (!d) return; 30 if (open) { 31 d.classList.add('open'); 32 d.setAttribute('aria-hidden', 'false'); 33 document.body.style.overflow = 'hidden'; 34 // move focus into the dialog for keyboard/screen-reader users 35 var c = d.querySelector('[data-toggle="drawer-close"]'); 36 if (c) c.focus(); 37 } else { 38 var wasOpen = d.classList.contains('open'); 39 // return focus to the menu button BEFORE hiding, so focus never 40 // sits inside an aria-hidden subtree 41 if (wasOpen && d.contains(document.activeElement)) { 42 var m = document.querySelector('[data-toggle="drawer-open"]'); 43 if (m) m.focus(); 44 } 45 d.classList.remove('open'); 46 d.setAttribute('aria-hidden', 'true'); 47 document.body.style.overflow = ''; 48 } 49 } 50 document.addEventListener('click', function (e) { 51 if (e.target.closest('[data-toggle="drawer-open"]')) { e.preventDefault(); setDrawer(true); } 52 else if (e.target.closest('[data-toggle="drawer-close"]')) { e.preventDefault(); setDrawer(false); } 53 else if (e.target.id === 'drawer') { setDrawer(false); } 54 }); 55 document.addEventListener('keydown', function (e) { 56 if (e.key === 'Escape') setDrawer(false); 57 }); 58 59 // ---- Sticky CTA dismiss ---- 60 var STICKY_KEY = 'dxl-cta-dismissed'; 61 function initCta() { 62 var cta = document.querySelector('.mobile-cta'); 63 if (!cta) return; 64 try { 65 if (sessionStorage.getItem(STICKY_KEY) === '1') { cta.style.display = 'none'; return; } 66 } catch (e) {} 67 document.body.classList.add('has-mobile-cta'); 68 var dismiss = cta.querySelector('.dismiss'); 69 if (dismiss) dismiss.addEventListener('click', function () { 70 cta.style.display = 'none'; 71 document.body.classList.remove('has-mobile-cta');
72 try { sessionStorage.setItem(STICKY_KEY, '1'); } catch (e) {} 73 }); 74 } 75 76 // ---- Image fade-in (real photos overlaying .ph skeletons) ---- 77 function initImages() { 78 var imgs = document.querySelectorAll('.ph img.cover'); 79 imgs.forEach(function (img) { 80 if (img.complete && img.naturalWidth) { 81 img.classList.add('loaded'); 82 } else { 83 img.addEventListener('load', function () { img.classList.add('loaded'); }); 84 img.addEventListener('error', function () { img.remove(); }); 85 } 86 }); 87 } 88 89 // ---- Reading progress bar (article pages) ---- 90 function initReadProgress() { 91 var bar = document.querySelector('.read-progress span'); 92 if (!bar) return; 93 var ticking = false; 94 function update() { 95 var h = document.documentElement; 96 var max = (h.scrollHeight - h.clientHeight); 97 var pct = max > 0 ? Math.min(100, Math.max(0, (h.scrollTop / max) * 100)) : 0; 98 bar.style.width = pct + '%'; 99 ticking = false; 100 } 101 window.addEventListener('scroll', function () { 102 if (!ticking) { requestAnimationFrame(update); ticking = true; } 103 }, { passive: true }); 104 update(); 105 } 106 107 // ---- Ticker: render from data + persist scroll across pages ---- 108 var TICKER_ANCHOR_KEY = 'dxl-ticker-anchor'; 109 var TICKER_DURATION = 90; // seconds (matches CSS .ticker-track animation) 110 function initTicker() { 111 var track = document.querySelector('.ticker-track'); 112 if (!track) return; 113 // Render items from window.DXL_DEALS if present (deals.js loaded before this). 114 var deals = window.DXL_DEALS; 115 if (deals && deals.length) { 116 var html = ''; 117 // duplicate the list for seamless loop 118 var loop = deals.concat(deals); 119 for (var i = 0; i < loop.length; i++) { 120 var d = loop[i]; 121 var tone = d.tone || 'mut'; 122 html += 123 '<a class="ticker-item" href="' + (d.url || '#') + 124 '" target="_blank" rel="sponsored noopener">' + 125 '<span class="sym">' + d.brand + '</span> ' + 126 '<span>' + d.product + '</span> ' + 127 '<span class="' + tone + '">' + d.note + '</span>' + 128 '</a>'; 129 } 130 track.innerHTML = html; 131 } 132 // Persist animation timing across pages via a single anchor timestamp. 133 var anchor; 134 try { anchor = Number(localStorage.getItem(TICKER_ANCHOR_KEY)); } catch (e) {} 135 if (!anchor || isNaN(anchor)) { 136 anchor = Date.now(); 137 try { localStorage.setItem(TICKER_ANCHOR_KEY, String(anchor)); } catch (e) {} 138 } 139 var elapsed = (Date.now() - anchor) / 1000; 140 var offset = elapsed % TICKER_DURATION; 141 track.style.animationDelay = '-' + offset + 's'; 142 } 143 144 function init() { 145 initCta(); 146 initImages(); 147 initReadProgress(); 148 initTicker(); 149 } 150 if (document.readyState !== 'loading') init(); 151 else document.addEventListener('DOMContentLoaded', init); 152})(); 153 154 155// ---- The Wire: beat filter tabs (any page with [data-wire-filter]) ---- 156(function () { 157 var nav = document.querySelector('[data-wire-filter]'); 158 if (!nav) return; 159 var rows = Array.prototype.slice.call(document.querySelectorAll('.wire-row:not(.head)')); 160 nav.addEventListener('click', function (e) { 161 var a = e.target.closest('a[data-filter]'); 162 if (!a) return; 163 e.preventDefault(); 164 Array.prototype.forEach.call(nav.querySelectorAll('a'), function (x) { x.classList.remove('active'); }); 165 a.classList.add('active'); 166 var f = a.getAttribute('data-filter'); 167 var tags = f ? f.split(',') : null; 168 rows.forEach(function (r) { 169 var tagEl = r.querySelector('.tag'); 170 var t = tagEl ? tagEl.textContent.trim() : ''; 171 r.style.display = (!tags || tags.indexOf(t) !== -1) ? '' : 'none'; 172 }); 173 }); 174})(); 175 176// ---- Category: sort control (#cat-sort) ---- 177(function () { 178 var sel = document.getElementById('cat-sort'); 179 var list = document.querySelector('.cat-list'); 180 if (!sel || !list) return; 181 var items = Array.prototype.slice.call(list.querySelectorAll('.item')); 182 function dateOf(item) { 183 // production: items carry an ISO timestamp (works across years / brands) 184 var iso = item.getAttribute('data-date'); 185 if (iso) { 186 var di = new Date(iso); 187 if (!isNaN(di.getTime())) return di.getTime(); 188 } 189 var spans = item.querySelectorAll('.byline span'); 190 var txt = spans.length ? spans[spans.length - 1].textContent : ''; 191 var d = new Date(txt + ' 2026'); 192 return isNaN(d.getTime()) ? 0 : d.getTime(); 193 } 194 function titleOf(item) { 195 var h = item.querySelector('h3'); 196 return h ? h.textContent.trim().toLowerCase() : ''; 197 } 198 sel.addEventListener('change', function () { 199 var mode = sel.value; 200 var sorted = items.slice().sort(function (a, b) { 201 if (mode === 'Oldest') return dateOf(a) - dateOf(b); 202 if (mode.indexOf('Title') === 0) return titleOf(a) < titleOf(b) ? -1 : 1; 203 return dateOf(b) - dateOf(a); 204 }); 205 sorted.forEach(function (el) { list.appendChild(el); }); 206 }); 207})(); 208 209// ---- Category: search preview (?q=) ---- 210(function () { 211 var hero = document.querySelector('.cat-hero'); 212 if (!hero) return; 213 var q = null; 214 try { q = new URLSearchParams(location.search).get('q'); } catch (e) {} 215 if (!q) return; 216 var h1 = hero.querySelector('h1'); 217 var deck = hero.querySelector('.deck'); 218 var crumb = hero.querySelector('.crumbs span'); 219 if (crumb) crumb.textContent = 'Search'; 220 if (h1) h1.textContent = '\u201C' + q + '\u201D'; 221 if (deck) deck.textContent = 'Every story matching your search, ranked by relevance.'; 222})(); 223 224// ---- Newsletter forms (form[data-subscribe] â data-endpoint, e.g. Beehiiv) ---- 225// Brand-agnostic: without an endpoint the button flips to SUBSCRIBED â (the 226// design's placeholder behavior); with one it POSTs { email } as JSON. 227(function () { 228 document.addEventListener('submit', function (e) { 229 var form = e.target && e.target.closest ? e.target.closest('form[data-subscribe]') : null; 230 if (!form) return; 231 e.preventDefault(); 232 var input = form.querySelector('input[type="email"]'); 233 var btn = form.querySelector('button'); 234 var email = input && input.value ? input.value.trim() : ''; 235 if (!email) { if (input) input.focus(); return; } 236 var endpoint = form.getAttribute('data-endpoint'); 237 if (!endpoint) { if (btn) btn.textContent = 'SUBSCRIBED â'; return; } 238 if (btn) { btn.disabled = true; btn.textContent = 'â¦'; } 239 fetch(endpoint, { 240 method: 'POST', 241 headers: { 'Content-Type': 'application/json' }, 242 body: JSON.stringify({ email: email }) 243 }).then(function (res) { 244 if (!res.ok) throw new Error('subscribe failed'); 245 if (btn) { btn.disabled = false; btn.textContent = 'SUBSCRIBED â'; } 246 if (input) input.value = ''; 247 }).catch(function () { 248 if (btn) { btn.disabled = false; btn.textContent = 'TRY AGAIN â»'; } 249 }); 250 }); 251})(); 252 253// ---- Share buttons ([data-share]) â native share sheet, clipboard fallback ---- 254(function () { 255 document.addEventListener('click', function (e) { 256 var b = e.target && e.target.closest ? e.target.closest('[data-share]') : null; 257 if (!b) return; 258 e.preventDefault(); 259 var url = location.href.split('#')[0]; 260 var title = b.getAttribute('data-share-title') || document.title; 261 if (navigator.share) { navigator.share({ title: title, url: url }).catch(function () {}); return; } 262 if (navigator.clipboard && navigator.clipboard.writeText) { 263 navigator.clipboard.writeText(url).then(function () { 264 var t = b.getAttribute('title'); 265 b.setAttribute('title', 'Link copied'); 266 setTimeout(function () { b.setAttribute('title', t || 'Share'); }, 1500); 267 }); 268 } 269 }); 270})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.