// CDJ-900NXS Mods — small helpers: theme toggle, before/after slider, docs TOC highlight. (function () { var root = document.documentElement; function storedTheme() { try { return localStorage.getItem('theme'); } catch (e) { return null; } } var t = storedTheme(); if (t === 'light' || t === 'dark') root.setAttribute('data-theme', t); document.addEventListener('DOMContentLoaded', function () { var btn = document.querySelector('.theme-btn'); function label() { if (btn) btn.textContent = root.getAttribute('data-theme') === 'light' ? '☾' : '☀'; } label(); if (btn) btn.addEventListener('click', function () { var next = root.getAttribute('data-theme') === 'light' ? 'dark' : 'light'; root.setAttribute('data-theme', next); try { localStorage.setItem('theme', next); } catch (e) { /* private mode */ } label(); }); // version badge and changelog (assets/versions.js, made by tools/make_builder_data.py) var V = window.CDJ_VERSIONS; var head = document.querySelector('.top .wrap'); if (V && head) { var esc = function (t) { return String(t).replace(/[&<>]/g, function (c) { return { '&': '&', '<': '<', '>': '>' }[c]; }); }; var vb = document.createElement('button'); vb.type = 'button'; vb.className = 'ver-btn'; vb.textContent = 'v' + V.site; vb.setAttribute('aria-haspopup', 'dialog'); vb.title = 'Changelog'; var brand = head.querySelector('.brand'); if (brand && brand.nextSibling) head.insertBefore(vb, brand.nextSibling); else head.appendChild(vb); var pop = document.createElement('div'); pop.className = 'ver-pop'; pop.setAttribute('role', 'dialog'); pop.setAttribute('aria-label', 'Changelog'); var html = '
' + '

Changelog

Site ' + esc(V.site) + ' · ' + esc(V.date) + ' · Semantic Versioning, dates ISO 8601

'; V.log.forEach(function (e) { html += '

' + esc(e.version) + '' + esc(e.date) + '

'; }); html += '

Mods

' + Object.keys(V.mods).map(function (m) { return '' + esc(m) + '' + esc(V.mods[m]) + ''; }).join('') + '
'; pop.innerHTML = html; document.body.appendChild(pop); var close = function () { pop.classList.remove('open'); vb.focus(); }; vb.addEventListener('click', function () { pop.classList.add('open'); pop.querySelector('.close').focus(); }); pop.querySelector('.close').addEventListener('click', close); pop.addEventListener('click', function (e) { if (e.target === pop) close(); }); document.addEventListener('keydown', function (e) { if (e.key === 'Escape' && pop.classList.contains('open')) close(); }); // every mod card's version beside its name document.querySelectorAll('.card h3 code').forEach(function (c) { var v = V.mods[c.textContent.trim()]; if (v) { var s = document.createElement('span'); s.className = 'ver'; s.textContent = 'v' + v; c.parentNode.appendChild(s); } }); } // before / after document.querySelectorAll('.compare').forEach(function (box) { var range = box.querySelector('input[type="range"]'); if (!range) return; function set() { box.style.setProperty('--pos', range.value + '%'); } range.addEventListener('input', set); set(); }); // docs: highlight the section in view var links = Array.prototype.slice.call(document.querySelectorAll('.toc a[href^="#"]')); if (links.length && 'IntersectionObserver' in window) { var byId = {}; links.forEach(function (a) { byId[a.getAttribute('href').slice(1)] = a; }); var obs = new IntersectionObserver(function (entries) { entries.forEach(function (e) { if (e.isIntersecting && byId[e.target.id]) { links.forEach(function (a) { a.classList.remove('active'); }); byId[e.target.id].classList.add('active'); } }); }, { rootMargin: '-80px 0px -70% 0px' }); Object.keys(byId).forEach(function (id) { var el = document.getElementById(id); if (el) obs.observe(el); }); } }); })(); // Background: a record turning at the top right, a 3-band waveform (lows blue, // mids amber, highs white) running along the bottom at 128 BPM; --beat pulses // with it. Quiet when the system asks for reduced motion or the tab is hidden. (function () { var BPM = 128, COL = 4, SPEED = 0.3, still = window.matchMedia && matchMedia('(prefers-reduced-motion: reduce)').matches; function rnd(k) { var x = Math.sin(k * 127.1 + 311.7) * 43758.5453; return x - Math.floor(x); } // one column of the waveform: k counts columns, 16 to a beat function band(k) { var beat = k % 16, bar = Math.floor(k / 64) % 16, drop = bar % 8 >= 2 && bar % 8 < 7; var kick = drop ? Math.exp(-beat / 3.2) : 0; var low = 0.12 + 0.8 * kick + 0.08 * rnd(k); var mid = (drop ? 0.32 : 0.45) + 0.25 * rnd(k + 9) * (0.6 + 0.4 * Math.sin(k / 40)); var high = (beat >= 7 && beat <= 9 ? 0.45 : 0.08) * (0.5 + rnd(k + 3)); return [low, mid, high]; } document.addEventListener('DOMContentLoaded', function () { var c = document.createElement('canvas'), x = c.getContext('2d'); if (!x) return; c.className = 'bg-fx'; c.setAttribute('aria-hidden', 'true'); document.body.insertBefore(c, document.body.firstChild); var w, h, dpr, last = 0, t0 = performance.now(); function size() { dpr = Math.min(window.devicePixelRatio || 1, 1.5); w = innerWidth; h = innerHeight; c.width = Math.round(w * dpr); c.height = Math.round(h * dpr); } size(); addEventListener('resize', size); function draw(now) { var light = document.documentElement.getAttribute('data-theme') === 'light', a = light ? 0.5 : 1; var t = (now - t0) / 1000 * SPEED, beats = t * BPM / 60; // slowed down: calm, not a real 128 BPM x.setTransform(dpr, 0, 0, dpr, 0, 0); x.clearRect(0, 0, w, h); // the record var r = Math.max(w, h) * 0.32, cx = w - r * 0.35, cy = r * 0.25, turn = still ? 0.6 : t * Math.PI * 2 * 0.09; x.save(); x.translate(cx, cy); for (var g = 0.36; g < 1; g += 0.022) { x.beginPath(); x.arc(0, 0, r * g, 0, Math.PI * 2); x.strokeStyle = light ? 'rgba(20,24,33,' + 0.05 * a + ')' : 'rgba(255,255,255,' + 0.035 + ')'; x.lineWidth = 1; x.stroke(); } x.rotate(turn); var sheen = x.createLinearGradient(-r, 0, r, 0); sheen.addColorStop(0.42, 'rgba(255,255,255,0)'); sheen.addColorStop(0.5, light ? 'rgba(20,24,33,' + 0.06 * a + ')' : 'rgba(255,255,255,0.06)'); sheen.addColorStop(0.58, 'rgba(255,255,255,0)'); x.beginPath(); x.arc(0, 0, r, 0, Math.PI * 2); x.arc(0, 0, r * 0.33, 0, Math.PI * 2, true); x.fillStyle = sheen; x.fill(); x.beginPath(); x.arc(0, 0, r * 0.3, 0, Math.PI * 2); x.fillStyle = 'rgba(0,97,197,' + 0.06 * a + ')'; x.fill(); x.beginPath(); x.arc(r * 0.18, 0, r * 0.012, 0, Math.PI * 2); x.fillStyle = 'rgba(255,255,255,' + 0.25 * a + ')'; x.fill(); x.restore(); // the waveform, 16 columns to a beat, scrolling left var y0 = h - Math.min(110, h * 0.13), amp = Math.min(70, h * 0.085); var shift = still ? 0 : beats * 16 * COL, first = Math.floor(shift / COL), off = shift - first * COL; for (var i = 0; i * COL - off < w + COL; i++) { var b = band(first + i), px = i * COL - off; var hl = b[0] * amp, hm = b[1] * amp * 0.85, hh = b[2] * amp; x.fillStyle = 'rgba(40,110,255,' + 0.22 * a + ')'; x.fillRect(px, y0 - hl, COL - 1, hl * 2); x.fillStyle = 'rgba(255,150,30,' + 0.22 * a + ')'; x.fillRect(px, y0 - hm, COL - 1, hm * 2); x.fillStyle = 'rgba(255,255,255,' + 0.20 * a + ')'; x.fillRect(px, y0 - hh, COL - 1, hh * 2); } document.documentElement.style.setProperty('--beat', still ? 0 : Math.pow(1 - (beats % 1), 3).toFixed(3)); } function loop(now) { if (!document.hidden && now - last > 33) { last = now; draw(now); } if (!still) requestAnimationFrame(loop); } if (still) draw(performance.now()); else requestAnimationFrame(loop); addEventListener('resize', function () { if (still) draw(performance.now()); }); }); })();