1// Reveal-on-scroll for elements with .reveal â no library, IntersectionObserver only. 2(function () { 3 const els = document.querySelectorAll('.reveal'); 4 if (!els.length) return; 5 const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches; 6 if (!('IntersectionObserver' in window) || reduce) { 7 els.forEach((el) => el.classList.add('in')); 8 return; 9 } 10 const io = new IntersectionObserver((entries) => { 11 entries.forEach((e) => { 12 if (e.isIntersecting) { 13 e.target.classList.add('in'); 14 io.unobserve(e.target); 15 } 16 }); 17 }, { rootMargin: '0px 0px -10% 0px', threshold: 0.05 }); 18 els.forEach((el) => io.observe(el)); 19})(); 20 21// Countly analytics â async loader from jsDelivr. 22(function () { 23 var Countly = window.Countly = window.Countly || {}; 24 Countly.q = Countly.q || []; 25 Countly.app_key = 'b9730c43befcc41a8d78c398b083174e0ac32fd9'; 26 Countly.url = 'https://master.count.ly'; 27 Countly.q.push(['track_sessions']); 28 Countly.q.push(['track_pageview']); 29 Countly.q.push(['track_clicks']); 30 Countly.q.push(['track_scrolls']); 31 Countly.q.push(['track_links']); 32 Countly.q.push(['track_errors']); 33 Countly.q.push(['track_forms']); 34 Countly.q.push(['enable_apm']); 35 var s = document.createElement('script'); 36 s.type = 'text/javascript'; 37 s.async = true; 38 s.src = 'https://cdn.jsdelivr.net/npm/countly-sdk-web/lib/countly.min.js'; 39 s.onload = function () { window.Countly.init(); }; 40 document.head.appendChild(s); 41})(); 42 43// Tab switcher â progressive enhancement. 44// Without JS, all [role="tabpanel"] panels stay visible (crawlers index every word). 45// With JS, the first tab's panel is shown; others are `hidden`. Click and â/â 46// arrow keys switch panels. 47(function () { 48 document.querySelectorAll('[role="tablist"]').forEach(function (list) { 49 var tabs = Array.from(list.querySelectorAll('[role="tab"]')); 50 var panels = tabs 51 .map(function (t) { return document.getElementById(t.getAttribute('aria-controls')); }) 52 .filter(Boolean); 53 if (!tabs.length || tabs.length !== panels.length) return; 54 55 // Derive a URL-friendly slug from each tab id (e.g. "tab-lyrics" -> "lyrics"). 56 var slugs = tabs.map(function (t) { 57 var id = t.id || ''; 58 return id.replace(/^tab-/, ''); 59 }); 60 61 function show(idx, updateHash) {
62 tabs.forEach(function (t, i) { 63 var sel = i === idx; 64 t.setAttribute('aria-selected', sel ? 'true' : 'false'); 65 t.setAttribute('tabindex', sel ? '0' : '-1'); 66 }); 67 panels.forEach(function (p, i) { p.hidden = (i !== idx); }); 68 if (updateHash && history && history.replaceState) { 69 // First tab is the default â drop the hash for a clean URL. 70 var newUrl = idx === 0 71 ? location.pathname + location.search 72 : location.pathname + location.search + '#' + slugs[idx]; 73 history.replaceState(null, '', newUrl); 74 } 75 } 76 77 function indexForHash() { 78 var h = (location.hash || '').replace(/^#/, ''); 79 if (!h) return -1; 80 return slugs.indexOf(h); 81 } 82 83 var fromHash = indexForHash(); 84 var initial = fromHash >= 0 85 ? fromHash 86 : tabs.findIndex(function (t) { return t.getAttribute('aria-selected') === 'true'; }); 87 if (initial < 0) initial = 0; 88 show(initial, false); 89 90 tabs.forEach(function (t, i) { 91 t.addEventListener('click', function () { show(i, true); }); 92 t.addEventListener('keydown', function (e) { 93 var next = i; 94 if (e.key === 'ArrowRight') next = (i + 1) % tabs.length; 95 else if (e.key === 'ArrowLeft') next = (i - 1 + tabs.length) % tabs.length; 96 else if (e.key === 'Home') next = 0; 97 else if (e.key === 'End') next = tabs.length - 1; 98 else return; 99 e.preventDefault(); 100 show(next, true); 101 tabs[next].focus(); 102 }); 103 }); 104 105 // React to back/forward or to a URL pasted directly into the address bar. 106 window.addEventListener('hashchange', function () { 107 var idx = indexForHash(); 108 if (idx >= 0) show(idx, false); 109 }); 110 }); 111})(); 112 113// Diptych toggle (mobile-only segmented control on /songs/my-idol/). 114// Flips body class diptych-active-neon â diptych-active-shadow which the CSS 115// uses to show/hide the inactive half on narrow viewports. On desktop the 116// toggle is hidden and both halves are always visible â no JS effect needed. 117(function () { 118 var toggle = document.querySelector('.dip-toggle'); 119 if (!toggle) return; 120 var buttons = Array.from(toggle.querySelectorAll('button[data-side]')); 121 if (!buttons.length) return; 122 123 function setActive(side) { 124 document.body.classList.remove('diptych-active-neon', 'diptych-active-shadow'); 125 document.body.classList.add('diptych-active-' + side); 126 buttons.forEach(function (b) { 127 b.setAttribute('aria-pressed', b.dataset.side === side ? 'true' : 'false'); 128 }); 129 } 130 131 // Default to neon on load. 132 setActive('neon'); 133 134 buttons.forEach(function (b) { 135 b.addEventListener('click', function () { setActive(b.dataset.side); }); 136 b.addEventListener('keydown', function (e) { 137 if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') return; 138 e.preventDefault(); 139 var idx = buttons.indexOf(b); 140 var next = (idx + (e.key === 'ArrowRight' ? 1 : -1) + buttons.length) % buttons.length; 141 setActive(buttons[next].dataset.side); 142 buttons[next].focus(); 143 }); 144 }); 145})(); 146 147// Share button â `[data-action="share"]` tries the Web Share API first 148// (native OS share sheet on mobile and many desktops). On user-dismissed: 149// silent (they chose). On any other failure, or if Web Share isn't here, 150// falls back to clipboard copy with a "Link copied" confirmation. One 151// button, always does something visible if the OS sheet doesn't appear. 152(function () { 153 document.addEventListener("click", function (e) { 154 var btn = e.target.closest && e.target.closest('[data-action="share"]'); 155 if (!btn) return; 156 e.preventDefault(); 157 var url = location.href; 158 var title = document.title; 159 160 function flashCopied() { 161 var orig = btn.textContent; 162 btn.textContent = "Link copied"; 163 btn.classList.add("copied"); 164 setTimeout(function () { 165 btn.textContent = orig; 166 btn.classList.remove("copied"); 167 }, 1500); 168 } 169 170 function copyLink() { 171 if (navigator.clipboard && navigator.clipboard.writeText) { 172 navigator.clipboard.writeText(url).then(flashCopied, function () {}); 173 return; 174 } 175 // very old fallback 176 var ta = document.createElement("textarea"); 177 ta.value = url; 178 ta.style.position = "fixed"; 179 ta.style.opacity = "0"; 180 document.body.appendChild(ta); 181 ta.select();
182 try { document.execCommand("copy"); flashCopied(); } catch (_) {} 183 document.body.removeChild(ta); 184 } 185 186 if (typeof navigator.share === "function") { 187 navigator.share({ title: title, url: url }).then( 188 function () { /* user completed share â sheet did its thing */ }, 189 function (err) { 190 // AbortError = user dismissed. Anything else: fall back to copy. 191 if (err && err.name === "AbortError") return; 192 copyLink(); 193 } 194 ); 195 return; 196 } 197 copyLink(); 198 }); 199})(); 200 201// Copy-link buttons â `.btn-share[data-action="copy"]` copies the page URL to clipboard. 202(function () { 203 document.addEventListener("click", function (e) { 204 var btn = e.target.closest && e.target.closest('.btn-share[data-action="copy"]'); 205 if (!btn) return; 206 e.preventDefault(); 207 var url = location.href; 208 var orig = btn.textContent; 209 var done = function () { 210 btn.textContent = "Copied"; 211 btn.classList.add("copied"); 212 setTimeout(function () { 213 btn.textContent = orig; 214 btn.classList.remove("copied"); 215 }, 1500); 216 }; 217 if (navigator.clipboard && navigator.clipboard.writeText) { 218 navigator.clipboard.writeText(url).then(done, function () {}); 219 } else { 220 // very old fallback 221 var ta = document.createElement("textarea"); 222 ta.value = url; 223 ta.style.position = "fixed"; 224 ta.style.opacity = "0"; 225 document.body.appendChild(ta); 226 ta.select(); 227 try { document.execCommand("copy"); done(); } catch (_) {} 228 document.body.removeChild(ta); 229 } 230 }); 231})(); 232 233// Click-to-load YouTube embeds â swap thumbnail for iframe on first click. 234// Avoids loading the YouTube player until the user wants to watch, and lets us 235// use the high-resolution maxresdefault thumbnail (the iframe-served preview is grainy). 236(function () { 237 function activate(box) { 238 if (!box || box.classList.contains('is-playing')) return; 239 var vid = box.getAttribute('data-video'); 240 if (!vid) return; 241 var title = box.getAttribute('data-title') || 'Music video'; 242 var iframe = document.createElement('iframe'); 243 iframe.src = 'https://www.youtube-nocookie.com/embed/' + encodeURIComponent(vid) + '?autoplay=1&rel=0'; 244 iframe.title = title; 245 iframe.allow = 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share'; 246 iframe.setAttribute('allowfullscreen', ''); 247 iframe.referrerPolicy = 'strict-origin-when-cross-origin'; 248 box.appendChild(iframe); 249 box.classList.add('is-playing'); 250 } 251 document.addEventListener('click', function (e) { 252 activate(e.target.closest && e.target.closest('.video-embed[data-video]')); 253 }); 254 document.addEventListener('keydown', function (e) { 255 if (e.key !== 'Enter' && e.key !== ' ') return; 256 var box = e.target.closest && e.target.closest('.video-embed[data-video]'); 257 if (!box) return; 258 e.preventDefault(); 259 activate(box); 260 }); 261})(); 262 263// Custom-event tracking for outbound links (streaming, social, listen, press kit, medium). 264// Fires Countly add_event with segmentation, on top of Countly.track_links auto-tracking. 265(function () { 266 if (!('addEventListener' in document)) return; 267 268 var TONEDEN_TO_SLUG = { 269 's4a': 'smile-for-the-algorithm', 270 'fp_ol': 'one-life', 271 'fp-nmw': 'not-my-war', 272 'fp_idc': 'i-dont-care' 273 }; 274 275 function track(key, segmentation) { 276 var Countly = window.Countly = window.Countly || {}; 277 Countly.q = Countly.q || []; 278 Countly.q.push(['add_event', { key: key, count: 1, segmentation: segmentation }]); 279 } 280 281 function classify(url) { 282 var u; 283 try { u = new URL(url); } catch (e) { return null; } 284 var host = u.hostname.replace(/^www\./, ''); 285 var page = window.location.pathname; 286 287 // Toneden listen buttons (per-song). 288 if (host === 'fanlink.tv' || host === 'fp.fanlink.tv') { 289 var slug = u.pathname.split('/').filter(Boolean)[0] || ''; 290 return { event: 'listen_click', segmentation: { song: TONEDEN_TO_SLUG[slug] || slug, page: page } }; 291 } 292 293 // Streaming platforms. 294 if (host === 'open.spotify.com') return { event: 'streaming_click', segmentation: { platform: 'spotify', page: page } }; 295 if (host === 'music.apple.com') return { event: 'streaming_click', segmentation: { platform: 'apple_music', page: page } }; 296 if (host === 'music.youtube.com') return { event: 'streaming_click', segmentation: { platform: 'youtube_music', page: page } }; 297 if (host === 'music.amazon.com') return { event: 'streaming_click', segmentation: { platform: 'amazon_music', page: page } }; 298 if (host === 'deezer.com') return { event: 'streaming_click', segmentation: { platform: 'deezer', page: page } }; 299 if (host === 'soundcloud.com') return { event: 'streaming_click', segmentation: { platform: 'soundcloud', page: page } }; 300 301 // Social. 302 if (host === 'tiktok.com') return { event: 'social_click', segmentation: { platform: 'tiktok', page: page } }; 303 if (host === 'instagram.com') return { event: 'social_click', segmentation: { platform: 'instagram', page: page } }; 304 if (host === 'youtube.com') return { event: 'social_click', segmentation: { platform: 'youtube', page: page } }; 305 if (host === 'x.com') return { event: 'social_click', segmentation: { platform: 'x', page: page } }; 306 if (host === 'facebook.com') return { event: 'social_click', segmentation: { platform: 'facebook', page: page } }; 307 308 // Medium "Read the full story" buttons on song pages. 309 if (host === 'medium.com') return { event: 'medium_click', segmentation: { page: page } }; 310 311 // Press kit Drive folder. 312 if (host === 'drive.google.com') return { event: 'press_kit_click', segmentation: { page: page } }; 313 314 return null; 315 } 316 317 document.addEventListener('click', function (e) { 318 var a = e.target.closest && e.target.closest('a[href]'); 319 if (!a) return; 320 var href = a.href; 321 if (!href || !/^https?:\/\//.test(href)) return; 322 // Press-mention links carry their own outlet slug for clean segmentation. 323 var press = a.getAttribute('data-press-mention'); 324 if (press) { 325 track('press_mention_click', { outlet: press, page: window.location.pathname }); 326 return; 327 } 328 var classified = classify(href); 329 if (classified) track(classified.event, classified.segmentation); 330 }, true); 331})();
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.