/* ===================================================
   JAESWIFT.XYZ — Sitewide Visual Effects Suite
   27 effects (25 new + matrix + cmatrix migration)
   Exposed as window.__jaeEffects
   Each effect: { enable(), disable(), active: bool }
   =================================================== */
(function () {
    'use strict';

    // ─── Environment gates ─────────────────────────
    const reducedMotion = () => window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
    const isMobile = () => window.matchMedia && window.matchMedia('(max-width: 640px)').matches;
    const shouldDowngrade = () => reducedMotion() || isMobile();

    // ─── Shared audio context (created on demand) ─────────
    let _audioCtx = null;
    function getAudio() {
        if (!_audioCtx) {
            try {
                const AC = window.AudioContext || window.webkitAudioContext;
                if (AC) _audioCtx = new AC();
            } catch (e) {}
        }
        if (_audioCtx && _audioCtx.state === 'suspended') {
            try { _audioCtx.resume(); } catch (e) {}
        }
        return _audioCtx;
    }

    // ─── Helpers ──────────────────────────────────────
    function makeFullCanvas(z, opacity) {
        const c = document.createElement('canvas');
        Object.assign(c.style, {
            position: 'fixed', inset: '0', width: '100%', height: '100%',
            pointerEvents: 'none', zIndex: String(z || 9100),
            opacity: String(opacity != null ? opacity : 1),
        });
        function resize() { c.width = window.innerWidth; c.height = window.innerHeight; }
        resize();
        c._resize = resize;
        window.addEventListener('resize', resize);
        document.body.appendChild(c);
        return c;
    }
    function destroyCanvas(c) {
        if (!c) return;
        if (c._resize) window.removeEventListener('resize', c._resize);
        c.remove();
    }
    function injectStyle(id, css) {
        if (document.getElementById(id)) return;
        const s = document.createElement('style');
        s.id = id;
        s.textContent = css;
        document.head.appendChild(s);
    }
    function ejectStyle(id) {
        const s = document.getElementById(id);
        if (s) s.remove();
    }
    function walkText(root, skipSelector) {
        // Return array of text nodes in visible elements (skipping nav/inputs/scripts)
        const skip = /^(SCRIPT|STYLE|NOSCRIPT|NAV|INPUT|TEXTAREA|CODE|PRE|CANVAS|SVG|IFRAME|BUTTON)$/i;
        const out = [];
        const w = document.createTreeWalker(root || document.body, NodeFilter.SHOW_TEXT, {
            acceptNode: (n) => {
                if (!n.parentElement) return NodeFilter.FILTER_REJECT;
                let p = n.parentElement;
                while (p && p !== document.body) {
                    if (skip.test(p.tagName)) return NodeFilter.FILTER_REJECT;
                    if (p.classList && (p.classList.contains('chat-messages') || p.classList.contains('chat-msg') || p.id === 'chatMessages' || p.id === 'chatInput')) return NodeFilter.FILTER_REJECT;
                    p = p.parentElement;
                }
                if (skipSelector && n.parentElement.closest(skipSelector)) return NodeFilter.FILTER_REJECT;
                if (!/\S/.test(n.nodeValue)) return NodeFilter.FILTER_REJECT;
                return NodeFilter.FILTER_ACCEPT;
            }
        });
        let node;
        while ((node = w.nextNode())) out.push(node);
        return out;
    }
    function snapshotRect(el) {
        const r = el.getBoundingClientRect();
        return { left: r.left, top: r.top, width: r.width, height: r.height };
    }

    // ==================================================
    // EFFECT REGISTRY
    // ==================================================
    const R = {};

    // --- MATRIX (migrated) ---
    R.matrix = (function () {
        let canvas = null, raf = null;
        return {
            active: false,
            enable() {
                if (this.active) return;
                this.active = true;
                canvas = makeFullCanvas(9050, 0.18);
                const ctx = canvas.getContext('2d');
                const chars = 'アイウエオカキクケコサシスセソタチツテトナニヌネノ01JAESWIFT'.split('');
                const size = 16;
                let cols = Math.floor(canvas.width / size);
                let drops = new Array(cols).fill(1);
                const onResize = () => { cols = Math.floor(canvas.width / size); drops = new Array(cols).fill(1); };
                window.addEventListener('resize', onResize);
                canvas._onResize2 = onResize;
                function draw() {
                    ctx.fillStyle = 'rgba(0,0,0,0.06)';
                    ctx.fillRect(0, 0, canvas.width, canvas.height);
                    ctx.fillStyle = '#00cc33';
                    ctx.font = size + 'px JetBrains Mono, monospace';
                    for (let i = 0; i < drops.length; i++) {
                        const ch = chars[Math.floor(Math.random() * chars.length)];
                        ctx.fillText(ch, i * size, drops[i] * size);
                        if (drops[i] * size > canvas.height && Math.random() > 0.975) drops[i] = 0;
                        drops[i]++;
                    }
                    raf = requestAnimationFrame(draw);
                }
                draw();
            },
            disable() {
                if (!this.active) return;
                this.active = false;
                if (raf) cancelAnimationFrame(raf);
                if (canvas && canvas._onResize2) window.removeEventListener('resize', canvas._onResize2);
                destroyCanvas(canvas);
                canvas = null; raf = null;
            }
        };
    })();

    // --- CMATRIX (migrated, enhanced) ---
    R.cmatrix = (function () {
        let canvas = null, raf = null, audio = null, start = 0;
        return {
            active: false,
            enable() {
                if (this.active) return;
                this.active = true;
                canvas = makeFullCanvas(9060, 0.32);
                const ctx = canvas.getContext('2d');
                const chars = 'アイウエオカキクケコサシスセソタチツテトナニヌネノ01JAESWIFT'.split('');
                const size = 16;
                let cols = Math.floor(canvas.width / size);
                let drops = new Array(cols).fill(1);
                const onResize = () => { cols = Math.floor(canvas.width / size); drops = new Array(cols).fill(1); };
                window.addEventListener('resize', onResize);
                canvas._onResize2 = onResize;
                start = Date.now();
                audio = getAudio();
                let tick = 0;
                function draw() {
                    const t = (Date.now() - start) / 1000;
                    const phase = Math.min(1, t / 30);
                    const speed = 1 + phase * 2.5;
                    ctx.fillStyle = 'rgba(0,0,0,0.07)';
                    ctx.fillRect(0, 0, canvas.width, canvas.height);
                    ctx.font = size + 'px JetBrains Mono, monospace';
                    for (let i = 0; i < drops.length; i++) {
                        const ch = chars[Math.floor(Math.random() * chars.length)];
                        ctx.fillStyle = (i % 7 === 0) ? '#33ddff' : '#00ff66';
                        ctx.fillText(ch, i * size, drops[i] * size);
                        if (drops[i] * size > canvas.height && Math.random() > 0.97) drops[i] = 0;
                        drops[i] += speed;
                    }
                    if (audio && tick % 4 === 0) {
                        try {
                            const osc = audio.createOscillator();
                            const g = audio.createGain();
                            osc.type = 'square';
                            osc.frequency.value = 1600 + Math.random() * 800;
                            g.gain.value = 0.0035;
                            osc.connect(g); g.connect(audio.destination);
                            osc.start();
                            osc.stop(audio.currentTime + 0.02);
                        } catch (e) {}
                    }
                    tick++;
                    raf = requestAnimationFrame(draw);
                }
                draw();
            },
            disable() {
                if (!this.active) return;
                this.active = false;
                if (raf) cancelAnimationFrame(raf);
                if (canvas && canvas._onResize2) window.removeEventListener('resize', canvas._onResize2);
                destroyCanvas(canvas);
                canvas = null; raf = null; audio = null;
            }
        };
    })();

    // --- 1. CRT ---
    R.crt = {
        active: false,
        enable() {
            if (this.active) return; this.active = true;
            document.documentElement.classList.add('fx-crt');
            const overlay = document.createElement('div');
            overlay.className = 'fx-crt-overlay';
            overlay.id = 'fx-crt-overlay';
            document.body.appendChild(overlay);
        },
        disable() {
            if (!this.active) return; this.active = false;
            document.documentElement.classList.remove('fx-crt');
            const o = document.getElementById('fx-crt-overlay'); if (o) o.remove();
        }
    };

    // --- 2. VHS ---
    R.vhs = (function () {
        let noiseCanvas = null, noiseRaf = null, glitchInterval = null;
        return {
            active: false,
            enable() {
                if (this.active) return; this.active = true;
                document.documentElement.classList.add('fx-vhs');
                const rollBar = document.createElement('div');
                rollBar.className = 'fx-vhs-roll'; rollBar.id = 'fx-vhs-roll';
                document.body.appendChild(rollBar);
                noiseCanvas = makeFullCanvas(9200, 0.12);
                const ctx = noiseCanvas.getContext('2d');
                function noise() {
                    const w = noiseCanvas.width, h = noiseCanvas.height;
                    const img = ctx.createImageData(w, h);
                    for (let i = 0; i < img.data.length; i += 4) {
                        const v = Math.random() * 255 | 0;
                        img.data[i] = v; img.data[i + 1] = v; img.data[i + 2] = v; img.data[i + 3] = 60;
                    }
                    ctx.putImageData(img, 0, 0);
                    noiseRaf = setTimeout(() => requestAnimationFrame(noise), 80);
                }
                if (!shouldDowngrade()) noise();
                glitchInterval = setInterval(() => {
                    const b = document.createElement('div');
                    b.className = 'fx-vhs-glitch-block';
                    b.style.top = Math.random() * 100 + '%';
                    b.style.height = (4 + Math.random() * 14) + 'px';
                    document.body.appendChild(b);
                    setTimeout(() => b.remove(), 80);
                }, 3000 + Math.random() * 2000);
            },
            disable() {
                if (!this.active) return; this.active = false;
                document.documentElement.classList.remove('fx-vhs');
                const r = document.getElementById('fx-vhs-roll'); if (r) r.remove();
                if (noiseRaf) clearTimeout(noiseRaf);
                destroyCanvas(noiseCanvas); noiseCanvas = null;
                if (glitchInterval) clearInterval(glitchInterval); glitchInterval = null;
                document.querySelectorAll('.fx-vhs-glitch-block').forEach(e => e.remove());
            }
        };
    })();

    // --- 3. GLITCH ---
    R.glitch = (function () {
        let scrambleInterval = null;
        const origValues = new WeakMap();
        return {
            active: false,
            enable() {
                if (this.active) return; this.active = true;
                document.documentElement.classList.add('fx-glitch');
                if (shouldDowngrade()) return;
                scrambleInterval = setInterval(() => {
                    const nodes = walkText();
                    if (!nodes.length) return;
                    const picks = [];
                    for (let i = 0; i < 3; i++) picks.push(nodes[Math.floor(Math.random() * nodes.length)]);
                    picks.forEach(n => {
                        if (!n || !n.nodeValue || n.nodeValue.length < 3) return;
                        if (!origValues.has(n)) origValues.set(n, n.nodeValue);
                        const orig = origValues.get(n);
                        const arr = orig.split('');
                        const idx = Math.floor(Math.random() * (arr.length - 1));
                        const tmp = arr[idx]; arr[idx] = arr[idx + 1]; arr[idx + 1] = tmp;
                        n.nodeValue = arr.join('');
                        setTimeout(() => { try { n.nodeValue = orig; } catch (e) {} }, 80);
                    });
                }, 2000);
            },
            disable() {
                if (!this.active) return; this.active = false;
                document.documentElement.classList.remove('fx-glitch');
                if (scrambleInterval) clearInterval(scrambleInterval); scrambleInterval = null;
            }
        };
    })();

    // --- 4. RED ALERT ---
    R.redalert = (function () {
        let banner = null, oscA = null, gain = null, toggle = null, audio = null;
        return {
            active: false,
            enable() {
                if (this.active) return; this.active = true;
                document.documentElement.classList.add('fx-redalert');
                banner = document.createElement('div');
                banner.id = 'fx-redalert-banner';
                banner.className = 'fx-redalert-banner';
                banner.textContent = '⚠ DEFCON 1 — RED ALERT ⚠';
                document.body.appendChild(banner);
                audio = getAudio();
                if (audio && !shouldDowngrade()) {
                    try {
                        oscA = audio.createOscillator();
                        gain = audio.createGain();
                        gain.gain.value = 0.03;
                        oscA.type = 'sine';
                        oscA.frequency.value = 400;
                        oscA.connect(gain); gain.connect(audio.destination);
                        oscA.start();
                        let hi = false;
                        toggle = setInterval(() => {
                            hi = !hi;
                            try { oscA.frequency.setValueAtTime(hi ? 800 : 400, audio.currentTime); } catch (e) {}
                        }, 500);
                    } catch (e) {}
                }
            },
            disable() {
                if (!this.active) return; this.active = false;
                document.documentElement.classList.remove('fx-redalert');
                if (banner) banner.remove(); banner = null;
                if (toggle) clearInterval(toggle); toggle = null;
                if (oscA) { try { oscA.stop(); } catch (e) {} oscA = null; }
                gain = null;
            }
        };
    })();

    // --- 5. INVERT ---
    R.invert = {
        active: false,
        enable() { if (this.active) return; this.active = true; document.documentElement.classList.add('fx-invert'); },
        disable() { if (!this.active) return; this.active = false; document.documentElement.classList.remove('fx-invert'); }
    };

    // --- 6. BLUEPRINT ---
    R.blueprint = {
        active: false,
        enable() {
            if (this.active) return; this.active = true;
            document.documentElement.classList.add('fx-blueprint');
            const g = document.createElement('div'); g.id = 'fx-blueprint-grid'; g.className = 'fx-blueprint-grid';
            document.body.appendChild(g);
        },
        disable() {
            if (!this.active) return; this.active = false;
            document.documentElement.classList.remove('fx-blueprint');
            const g = document.getElementById('fx-blueprint-grid'); if (g) g.remove();
        }
    };

    // --- 7. TYPEWRITER ---
    R.typewriter = (function () {
        const wrapped = [];
        let running = false;
        return {
            active: false,
            enable() {
                if (this.active) return; this.active = true;
                if (shouldDowngrade()) return;
                const nodes = walkText();
                const audio = getAudio();
                nodes.forEach(n => {
                    const text = n.nodeValue;
                    if (text.length < 2) return;
                    const span = document.createElement('span');
                    span.className = 'fx-typewriter-span';
                    n.parentNode.replaceChild(span, n);
                    wrapped.push({ span, text, parent: span.parentNode });
                    span.textContent = '';
                });
                running = true;
                let idx = 0;
                const self = this;
                function typeNext() {
                    if (!running || idx >= wrapped.length) return;
                    const { span, text } = wrapped[idx];
                    let i = 0;
                    const interval = setInterval(() => {
                        if (!running) { clearInterval(interval); return; }
                        if (i >= text.length) {
                            clearInterval(interval);
                            idx++;
                            setTimeout(typeNext, 40);
                            return;
                        }
                        span.textContent += text[i];
                        if (audio && i % 5 === 0) {
                            try {
                                const osc = audio.createOscillator();
                                const g = audio.createGain();
                                osc.frequency.value = 1200; g.gain.value = 0.004;
                                osc.connect(g); g.connect(audio.destination);
                                osc.start(); osc.stop(audio.currentTime + 0.01);
                            } catch (e) {}
                        }
                        i++;
                    }, 30);
                }
                typeNext();
            },
            disable() {
                if (!this.active) return; this.active = false;
                running = false;
                wrapped.forEach(({ span, text }) => {
                    if (span.parentNode) {
                        const tn = document.createTextNode(text);
                        span.parentNode.replaceChild(tn, span);
                    }
                });
                wrapped.length = 0;
            }
        };
    })();

    // --- 8. GRAVITY ---
    R.gravity = (function () {
        let items = [], raf = null;
        return {
            active: false,
            enable() {
                if (this.active) return; this.active = true;
                const selectors = '.panel, .card, h1, h2, h3';
                const els = Array.from(document.querySelectorAll(selectors)).slice(0, 40);
                items = els.map(el => {
                    const r = snapshotRect(el);
                    const data = {
                        el,
                        origStyle: el.getAttribute('style') || '',
                        x: r.left, y: r.top, w: r.width, h: r.height,
                        vx: (Math.random() - 0.5) * 3, vy: -Math.random() * 4,
                    };
                    el.style.position = 'fixed';
                    el.style.left = r.left + 'px';
                    el.style.top = r.top + 'px';
                    el.style.width = r.width + 'px';
                    el.style.margin = '0';
                    el.style.zIndex = '8000';
                    return data;
                });
                function step() {
                    const floor = window.innerHeight;
                    items.forEach(it => {
                        it.vy += 0.6;
                        it.x += it.vx;
                        it.y += it.vy;
                        if (it.y + it.h >= floor) {
                            it.y = floor - it.h;
                            it.vy *= -0.3;
                            it.vx *= 0.8;
                            if (Math.abs(it.vy) < 0.5) it.vy = 0;
                        }
                        if (it.x < 0) { it.x = 0; it.vx *= -0.5; }
                        if (it.x + it.w > window.innerWidth) { it.x = window.innerWidth - it.w; it.vx *= -0.5; }
                        it.el.style.left = it.x + 'px';
                        it.el.style.top = it.y + 'px';
                    });
                    raf = requestAnimationFrame(step);
                }
                step();
            },
            disable() {
                if (!this.active) return; this.active = false;
                if (raf) cancelAnimationFrame(raf); raf = null;
                items.forEach(it => {
                    if (it.origStyle) it.el.setAttribute('style', it.origStyle);
                    else it.el.removeAttribute('style');
                });
                items = [];
            }
        };
    })();

    // --- 9. EARTHQUAKE ---
    R.earthquake = (function () {
        let interval = null, stopTimer = null;
        return {
            active: false,
            enable() {
                if (this.active) return; this.active = true;
                const self = this;
                interval = setInterval(() => {
                    const x = (Math.random() - 0.5) * 20;
                    const y = (Math.random() - 0.5) * 20;
                    document.body.style.transform = `translate(${x}px, ${y}px)`;
                }, 50);
                stopTimer = setTimeout(() => self.disable(), 10000);
            },
            disable() {
                if (!this.active) return; this.active = false;
                if (interval) clearInterval(interval); interval = null;
                if (stopTimer) clearTimeout(stopTimer); stopTimer = null;
                document.body.style.transform = '';
            }
        };
    })();

    // --- 10. LOW GRAVITY ---
    R.lowgravity = {
        active: false,
        enable() {
            if (this.active) return; this.active = true;
            document.documentElement.classList.add('fx-lowgrav');
            document.querySelectorAll('.panel, .card').forEach((el, i) => {
                el.style.animationDelay = (Math.random() * 4) + 's';
                el.classList.add('fx-lowgrav-bob');
            });
        },
        disable() {
            if (!this.active) return; this.active = false;
            document.documentElement.classList.remove('fx-lowgrav');
            document.querySelectorAll('.fx-lowgrav-bob').forEach(el => {
                el.classList.remove('fx-lowgrav-bob');
                el.style.animationDelay = '';
            });
        }
    };

    // --- 11. MELT ---
    R.melt = (function () {
        let items = [], raf = null, t0 = 0;
        return {
            active: false,
            enable() {
                if (this.active) return; this.active = true;
                const els = Array.from(document.querySelectorAll('p, h1, h2, h3, h4, li, span, a')).filter(el => el.offsetParent !== null).slice(0, 200);
                items = els.map(el => ({ el, origTransform: el.style.transform || '', drop: window.innerHeight - el.getBoundingClientRect().top - 20 + Math.random() * 60, jitter: (Math.random() - 0.5) * 10 }));
                t0 = performance.now();
                const self = this;
                function step(t) {
                    const elapsed = (t - t0) / 15000; // 15s to fully melt
                    const p = Math.min(1, elapsed);
                    items.forEach(it => {
                        const y = it.drop * easeInCubic(p);
                        it.el.style.transform = `translate(${it.jitter * p}px, ${y}px)`;
                        it.el.style.transition = 'transform 0.1s linear';
                    });
                    if (p < 1 && self.active) raf = requestAnimationFrame(step);
                }
                function easeInCubic(x) { return x * x * x; }
                step(t0);
            },
            disable() {
                if (!this.active) return; this.active = false;
                if (raf) cancelAnimationFrame(raf); raf = null;
                items.forEach(it => { it.el.style.transform = it.origTransform; it.el.style.transition = ''; });
                items = [];
            }
        };
    })();

    // --- 12. SHUFFLE ---
    R.shuffle = (function () {
        const saved = [];
        return {
            active: false,
            enable() {
                if (this.active) return; this.active = true;
                const nodes = walkText();
                nodes.forEach(n => {
                    const text = n.nodeValue;
                    if (!/\s/.test(text)) return;
                    saved.push({ node: n, orig: text });
                    const words = text.split(/(\s+)/);
                    const content = words.filter(w => /\S/.test(w));
                    for (let i = content.length - 1; i > 0; i--) {
                        const j = Math.floor(Math.random() * (i + 1));
                        [content[i], content[j]] = [content[j], content[i]];
                    }
                    let ci = 0;
                    const shuffled = words.map(w => /\S/.test(w) ? content[ci++] : w).join('');
                    n.nodeValue = shuffled;
                });
            },
            disable() {
                if (!this.active) return; this.active = false;
                saved.forEach(({ node, orig }) => { try { node.nodeValue = orig; } catch (e) {} });
                saved.length = 0;
            }
        };
    })();

    // --- 13. RAIN ---
    R.rain = (function () {
        let canvas = null, raf = null, drops = [], thunder = null, audio = null, noiseNode = null;
        return {
            active: false,
            enable() {
                if (this.active) return; this.active = true;
                canvas = makeFullCanvas(9100, 0.55);
                const ctx = canvas.getContext('2d');
                const count = shouldDowngrade() ? 60 : 200;
                drops = [];
                for (let i = 0; i < count; i++) {
                    drops.push({ x: Math.random() * canvas.width, y: Math.random() * canvas.height, len: 8 + Math.random() * 12, spd: 10 + Math.random() * 8 });
                }
                function draw() {
                    ctx.clearRect(0, 0, canvas.width, canvas.height);
                    ctx.strokeStyle = 'rgba(180,220,255,0.55)';
                    ctx.lineWidth = 1;
                    drops.forEach(d => {
                        ctx.beginPath();
                        ctx.moveTo(d.x, d.y);
                        ctx.lineTo(d.x - 2, d.y + d.len);
                        ctx.stroke();
                        d.y += d.spd;
                        if (d.y > canvas.height) { d.y = -d.len; d.x = Math.random() * canvas.width; }
                    });
                    raf = requestAnimationFrame(draw);
                }
                draw();
                // thunder flicker
                function flash() {
                    document.documentElement.classList.add('fx-rain-flash');
                    setTimeout(() => document.documentElement.classList.remove('fx-rain-flash'), 80);
                    thunder = setTimeout(flash, 15000 + Math.random() * 15000);
                }
                thunder = setTimeout(flash, 15000);
                // ambient noise
                audio = getAudio();
                if (audio && !shouldDowngrade()) {
                    try {
                        const bufferSize = 2 * audio.sampleRate;
                        const buffer = audio.createBuffer(1, bufferSize, audio.sampleRate);
                        const data = buffer.getChannelData(0);
                        let lastOut = 0;
                        for (let i = 0; i < bufferSize; i++) {
                            const white = Math.random() * 2 - 1;
                            data[i] = (lastOut + (0.02 * white)) / 1.02;
                            lastOut = data[i];
                            data[i] *= 3.5;
                        }
                        noiseNode = audio.createBufferSource();
                        noiseNode.buffer = buffer; noiseNode.loop = true;
                        const filter = audio.createBiquadFilter();
                        filter.type = 'highpass'; filter.frequency.value = 800;
                        const g = audio.createGain(); g.gain.value = 0.04;
                        noiseNode.connect(filter); filter.connect(g); g.connect(audio.destination);
                        noiseNode.start();
                    } catch (e) {}
                }
            },
            disable() {
                if (!this.active) return; this.active = false;
                if (raf) cancelAnimationFrame(raf); raf = null;
                if (thunder) clearTimeout(thunder); thunder = null;
                destroyCanvas(canvas); canvas = null;
                if (noiseNode) { try { noiseNode.stop(); } catch (e) {} noiseNode = null; }
                document.documentElement.classList.remove('fx-rain-flash');
            }
        };
    })();

    // --- 14. SNOW ---
    R.snow = (function () {
        let canvas = null, raf = null, flakes = [];
        return {
            active: false,
            enable() {
                if (this.active) return; this.active = true;
                canvas = makeFullCanvas(9100, 0.75);
                const ctx = canvas.getContext('2d');
                const count = shouldDowngrade() ? 50 : 150;
                flakes = [];
                for (let i = 0; i < count; i++) {
                    flakes.push({ x: Math.random() * canvas.width, y: Math.random() * canvas.height, r: 1 + Math.random() * 3, vy: 0.5 + Math.random() * 1.5, phase: Math.random() * Math.PI * 2 });
                }
                function draw() {
                    ctx.clearRect(0, 0, canvas.width, canvas.height);
                    ctx.fillStyle = 'rgba(255,255,255,0.85)';
                    flakes.forEach(f => {
                        f.phase += 0.02;
                        f.y += f.vy;
                        const x = f.x + Math.sin(f.phase) * 12;
                        ctx.beginPath();
                        ctx.arc(x, f.y, f.r, 0, Math.PI * 2);
                        ctx.fill();
                        if (f.y > canvas.height) { f.y = -5; f.x = Math.random() * canvas.width; }
                    });
                    raf = requestAnimationFrame(draw);
                }
                draw();
            },
            disable() {
                if (!this.active) return; this.active = false;
                if (raf) cancelAnimationFrame(raf); raf = null;
                destroyCanvas(canvas); canvas = null;
            }
        };
    })();

    // --- 15. FOG ---
    R.fog = (function () {
        let overlay = null, onMove = null;
        return {
            active: false,
            enable() {
                if (this.active) return; this.active = true;
                overlay = document.createElement('div');
                overlay.id = 'fx-fog'; overlay.className = 'fx-fog';
                document.body.appendChild(overlay);
                onMove = (e) => {
                    overlay.style.setProperty('--fx-fog-x', e.clientX + 'px');
                    overlay.style.setProperty('--fx-fog-y', e.clientY + 'px');
                };
                window.addEventListener('mousemove', onMove);
            },
            disable() {
                if (!this.active) return; this.active = false;
                if (onMove) window.removeEventListener('mousemove', onMove); onMove = null;
                if (overlay) overlay.remove(); overlay = null;
            }
        };
    })();

    // --- 16. NIGHT ---
    R.night = (function () {
        let overlay = null, onMove = null;
        return {
            active: false,
            enable() {
                if (this.active) return; this.active = true;
                document.documentElement.classList.add('fx-night');
                overlay = document.createElement('div');
                overlay.id = 'fx-night-spot'; overlay.className = 'fx-night-spot';
                document.body.appendChild(overlay);
                onMove = (e) => {
                    overlay.style.setProperty('--fx-spot-x', e.clientX + 'px');
                    overlay.style.setProperty('--fx-spot-y', e.clientY + 'px');
                };
                window.addEventListener('mousemove', onMove);
            },
            disable() {
                if (!this.active) return; this.active = false;
                document.documentElement.classList.remove('fx-night');
                if (onMove) window.removeEventListener('mousemove', onMove); onMove = null;
                if (overlay) overlay.remove(); overlay = null;
            }
        };
    })();

    // --- 17. UNDERWATER ---
    R.underwater = (function () {
        let canvas = null, raf = null, bubbles = [], audio = null, noiseNode = null;
        return {
            active: false,
            enable() {
                if (this.active) return; this.active = true;
                document.documentElement.classList.add('fx-underwater');
                canvas = makeFullCanvas(9100, 0.6);
                const ctx = canvas.getContext('2d');
                const count = shouldDowngrade() ? 15 : 30;
                bubbles = [];
                for (let i = 0; i < count; i++) {
                    bubbles.push({ x: Math.random() * canvas.width, y: canvas.height + Math.random() * canvas.height, r: 3 + Math.random() * 8, vy: 0.5 + Math.random() * 1.2, phase: Math.random() * Math.PI * 2 });
                }
                let t = 0;
                function draw() {
                    t += 0.02;
                    ctx.clearRect(0, 0, canvas.width, canvas.height);
                    // wave overlay
                    ctx.fillStyle = 'rgba(80,140,200,0.08)';
                    for (let y = 0; y < canvas.height; y += 30) {
                        const off = Math.sin(t + y * 0.02) * 10;
                        ctx.fillRect(off, y, canvas.width, 15);
                    }
                    // bubbles
                    ctx.strokeStyle = 'rgba(200,230,255,0.8)';
                    ctx.lineWidth = 1;
                    bubbles.forEach(b => {
                        b.phase += 0.05;
                        b.y -= b.vy;
                        const x = b.x + Math.sin(b.phase) * 8;
                        ctx.beginPath();
                        ctx.arc(x, b.y, b.r, 0, Math.PI * 2);
                        ctx.stroke();
                        if (b.y + b.r < 0) { b.y = canvas.height + 10; b.x = Math.random() * canvas.width; }
                    });
                    raf = requestAnimationFrame(draw);
                }
                draw();
                audio = getAudio();
                if (audio && !shouldDowngrade()) {
                    try {
                        const bufferSize = 2 * audio.sampleRate;
                        const buffer = audio.createBuffer(1, bufferSize, audio.sampleRate);
                        const data = buffer.getChannelData(0);
                        for (let i = 0; i < bufferSize; i++) data[i] = (Math.random() * 2 - 1) * 0.5;
                        noiseNode = audio.createBufferSource();
                        noiseNode.buffer = buffer; noiseNode.loop = true;
                        const filter = audio.createBiquadFilter();
                        filter.type = 'lowpass'; filter.frequency.value = 400;
                        const g = audio.createGain(); g.gain.value = 0.03;
                        noiseNode.connect(filter); filter.connect(g); g.connect(audio.destination);
                        noiseNode.start();
                    } catch (e) {}
                }
            },
            disable() {
                if (!this.active) return; this.active = false;
                document.documentElement.classList.remove('fx-underwater');
                if (raf) cancelAnimationFrame(raf); raf = null;
                destroyCanvas(canvas); canvas = null;
                if (noiseNode) { try { noiseNode.stop(); } catch (e) {} noiseNode = null; }
            }
        };
    })();

    // --- 18. DIMENSIONS ---
    R.dimensions = (function () {
        let onMove = null;
        return {
            active: false,
            enable() {
                if (this.active) return; this.active = true;
                document.documentElement.classList.add('fx-dimensions');
                onMove = (e) => {
                    const rx = ((e.clientY / window.innerHeight) - 0.5) * -10;
                    const ry = ((e.clientX / window.innerWidth) - 0.5) * 10;
                    document.body.style.setProperty('--fx-tilt-x', rx + 'deg');
                    document.body.style.setProperty('--fx-tilt-y', ry + 'deg');
                };
                window.addEventListener('mousemove', onMove);
            },
            disable() {
                if (!this.active) return; this.active = false;
                document.documentElement.classList.remove('fx-dimensions');
                if (onMove) window.removeEventListener('mousemove', onMove); onMove = null;
                document.body.style.removeProperty('--fx-tilt-x');
                document.body.style.removeProperty('--fx-tilt-y');
            }
        };
    })();

    // --- 19. PORTAL ---
    R.portal = (function () {
        let portal = null, onMove = null;
        return {
            active: false,
            enable() {
                if (this.active) return; this.active = true;
                portal = document.createElement('div');
                portal.id = 'fx-portal'; portal.className = 'fx-portal';
                const targets = ['/hq/telemetry', '/depot/contraband', '/recon', '/armoury/lab', '/transmissions/radar', '/hq/leaderboards'];
                const pick = targets[Math.floor(Math.random() * targets.length)];
                portal.innerHTML = '<iframe src="' + pick + '" loading="lazy"></iframe>';
                document.body.appendChild(portal);
                onMove = (e) => {
                    portal.style.left = (e.clientX - 75) + 'px';
                    portal.style.top = (e.clientY - 75) + 'px';
                    const iframe = portal.querySelector('iframe');
                    if (iframe) {
                        const tx = -((e.clientX / window.innerWidth) * 1000) + 400;
                        const ty = -((e.clientY / window.innerHeight) * 800) + 300;
                        iframe.style.transform = `translate(${tx}px, ${ty}px)`;
                    }
                };
                window.addEventListener('mousemove', onMove);
            },
            disable() {
                if (!this.active) return; this.active = false;
                if (onMove) window.removeEventListener('mousemove', onMove); onMove = null;
                if (portal) portal.remove(); portal = null;
            }
        };
    })();

    // --- 20. RETRO ---
    R.retro = (function () {
        let currentTheme = null, banner = null;
        function clearAll() {
            ['retro-1995', 'retro-2005', 'retro-2015'].forEach(c => document.documentElement.classList.remove(c));
            if (banner) { banner.remove(); banner = null; }
        }
        return {
            active: false,
            currentArg: null,
            enable(arg) {
                const year = (arg || 'now').toLowerCase();
                clearAll();
                this.currentArg = year;
                if (year === '1995') {
                    document.documentElement.classList.add('retro-1995');
                    banner = document.createElement('div');
                    banner.className = 'fx-retro-1995-banner';
                    banner.innerHTML = '🚧 UNDER CONSTRUCTION 🚧 &nbsp; VISITORS: <b>000' + (1000 + Math.floor(Math.random() * 9000)) + '</b> &nbsp; <blink>BEST VIEWED IN NETSCAPE</blink>';
                    document.body.appendChild(banner);
                    this.active = true;
                } else if (year === '2005') {
                    document.documentElement.classList.add('retro-2005');
                    this.active = true;
                } else if (year === '2015') {
                    document.documentElement.classList.add('retro-2015');
                    this.active = true;
                } else {
                    this.active = false;
                }
            },
            disable() {
                this.active = false;
                this.currentArg = null;
                clearAll();
            }
        };
    })();

    // --- 21. PARTY MODE ---
    R.partymode = (function () {
        let canvas = null, raf = null, audio = null, confettiTimer = null, flashTimer = null, particles = [], kickInterval = null;
        const palette = ['#ff3366', '#33ffcc', '#ffcc33', '#cc33ff', '#66ff33', '#3366ff'];
        return {
            active: false,
            enable() {
                if (this.active) return; this.active = true;
                document.documentElement.classList.add('fx-party');
                canvas = makeFullCanvas(9300, 0.85);
                const ctx = canvas.getContext('2d');
                function spawn() {
                    if (shouldDowngrade()) return;
                    for (let i = 0; i < 30; i++) {
                        particles.push({
                            x: Math.random() * canvas.width, y: -10,
                            vx: (Math.random() - 0.5) * 4, vy: 1 + Math.random() * 4,
                            size: 3 + Math.random() * 5,
                            color: palette[Math.floor(Math.random() * palette.length)],
                            rot: Math.random() * Math.PI * 2, vr: (Math.random() - 0.5) * 0.2,
                        });
                    }
                }
                spawn();
                confettiTimer = setInterval(spawn, 3000);
                function draw() {
                    ctx.clearRect(0, 0, canvas.width, canvas.height);
                    particles.forEach(p => {
                        p.vy += 0.05;
                        p.x += p.vx; p.y += p.vy; p.rot += p.vr;
                        ctx.save();
                        ctx.translate(p.x, p.y);
                        ctx.rotate(p.rot);
                        ctx.fillStyle = p.color;
                        ctx.fillRect(-p.size / 2, -p.size / 2, p.size, p.size);
                        ctx.restore();
                    });
                    particles = particles.filter(p => p.y < canvas.height + 20);
                    raf = requestAnimationFrame(draw);
                }
                draw();
                flashTimer = setInterval(() => {
                    const panels = document.querySelectorAll('.panel');
                    if (panels.length) {
                        const el = panels[Math.floor(Math.random() * panels.length)];
                        const c = palette[Math.floor(Math.random() * palette.length)];
                        const orig = el.style.backgroundColor;
                        el.style.transition = 'background-color 0.2s';
                        el.style.backgroundColor = c;
                        setTimeout(() => { el.style.backgroundColor = orig; }, 200);
                    }
                }, 500);
                audio = getAudio();
                if (audio && !shouldDowngrade()) {
                    kickInterval = setInterval(() => {
                        try {
                            const osc = audio.createOscillator();
                            const g = audio.createGain();
                            osc.frequency.setValueAtTime(120, audio.currentTime);
                            osc.frequency.exponentialRampToValueAtTime(40, audio.currentTime + 0.15);
                            g.gain.setValueAtTime(0.15, audio.currentTime);
                            g.gain.exponentialRampToValueAtTime(0.001, audio.currentTime + 0.15);
                            osc.connect(g); g.connect(audio.destination);
                            osc.start(); osc.stop(audio.currentTime + 0.15);
                        } catch (e) {}
                    }, 500);
                }
            },
            disable() {
                if (!this.active) return; this.active = false;
                document.documentElement.classList.remove('fx-party');
                if (raf) cancelAnimationFrame(raf); raf = null;
                if (confettiTimer) clearInterval(confettiTimer); confettiTimer = null;
                if (flashTimer) clearInterval(flashTimer); flashTimer = null;
                if (kickInterval) clearInterval(kickInterval); kickInterval = null;
                destroyCanvas(canvas); canvas = null; particles = [];
            }
        };
    })();

    // --- 22. GHOST MODE ---
    R.ghostmode = {
        active: false,
        enable() {
            if (this.active) return; this.active = true;
            document.documentElement.classList.add('fx-ghost');
        },
        disable() {
            if (!this.active) return; this.active = false;
            document.documentElement.classList.remove('fx-ghost');
        }
    };

    // --- 23. QUANTUM ---
    R.quantum = (function () {
        let timer = null, originals = [];
        return {
            active: false,
            enable() {
                if (this.active) return; this.active = true;
                const panels = document.querySelectorAll('.panel');
                panels.forEach(p => {
                    originals.push({ el: p, transform: p.style.transform, transition: p.style.transition });
                    p.style.transition = 'transform 0.5s ease';
                });
                timer = setInterval(() => {
                    panels.forEach(p => {
                        const x = (Math.random() - 0.5) * 60;
                        const y = (Math.random() - 0.5) * 60;
                        p.style.transform = `translate(${x}vw, ${y}vh)`;
                    });
                }, 3000);
            },
            disable() {
                if (!this.active) return; this.active = false;
                if (timer) clearInterval(timer); timer = null;
                originals.forEach(o => { o.el.style.transform = o.transform; o.el.style.transition = o.transition; });
                originals = [];
            }
        };
    })();

    // --- 24. SNEAK ---
    R.sneak = (function () {
        const SNEAKY = ['CLASSIFIED', 'REDACTED', 'ACCESS GRANTED', 'DO NOT CLICK', 'FREE MONEY', 'TOP SECRET', '█████', 'COMPROMISED', 'SUSPECT', 'EYES ONLY', 'TRAP', 'HONEYPOT'];
        const saved = [];
        return {
            active: false,
            enable() {
                if (this.active) return; this.active = true;
                document.querySelectorAll('a, button').forEach(el => {
                    if (!el.childNodes.length) return;
                    const hasOnlyText = Array.from(el.childNodes).every(c => c.nodeType === 3);
                    if (!hasOnlyText) return;
                    const orig = el.textContent;
                    if (!orig.trim()) return;
                    saved.push({ el, orig, title: el.getAttribute('title') });
                    el.setAttribute('title', orig);
                    el.textContent = SNEAKY[Math.floor(Math.random() * SNEAKY.length)];
                });
            },
            disable() {
                if (!this.active) return; this.active = false;
                saved.forEach(({ el, orig, title }) => {
                    el.textContent = orig;
                    if (title == null) el.removeAttribute('title');
                    else el.setAttribute('title', title);
                });
                saved.length = 0;
            }
        };
    })();

    // --- 25. HACKER ---
    R.hacker = (function () {
        let canvas = null, raf = null, lines = [];
        const snippets = [
            '#include <stdio.h>', 'int main(int argc, char** argv) {',
            '    for (int i = 0; i < n; i++) {', '        if (ptr[i] == NULL) return -1;',
            '    }', '    exit(0);', '}',
            'def exploit(target):', '    shellcode = b"\\x31\\xc0\\x50\\x68"',
            '    payload = nop_sled(128) + shellcode', '    return pwn(target, payload)',
            'mov eax, 0x1337', 'push ebp', 'call 0xdeadbeef', 'jmp short $-5',
            'INFO: Scanning 10.0.0.0/24...', 'CRIT: Buffer overflow detected',
            'WARN: Rootkit signature found', 'ALERT: Firewall breach imminent',
            'echo "pwned" > /root/.hacked', 'sudo rm -rf / --no-preserve-root',
            '0x00007fff5fbff7b8: 0x00007fff8b4a3d80', 'SEGFAULT at 0x0',
            'while(1) fork();', 'cat /etc/shadow', 'nmap -p- -sS -T4 target.com',
        ];
        return {
            active: false,
            enable() {
                if (this.active) return; this.active = true;
                canvas = makeFullCanvas(9500, 0.88);
                const ctx = canvas.getContext('2d');
                document.documentElement.classList.add('fx-hacker');
                const size = 13;
                lines = [];
                for (let y = 0; y < canvas.height; y += size) {
                    lines.push({ text: snippets[Math.floor(Math.random() * snippets.length)], y: y });
                }
                function draw() {
                    ctx.fillStyle = 'rgba(0,0,0,0.9)';
                    ctx.fillRect(0, 0, canvas.width, canvas.height);
                    ctx.fillStyle = '#00ff33';
                    ctx.font = size + 'px JetBrains Mono, monospace';
                    lines.forEach(l => {
                        ctx.fillText(l.text, 10, l.y);
                        l.y -= 2;
                        if (l.y < -size) {
                            l.y = canvas.height + size;
                            l.text = snippets[Math.floor(Math.random() * snippets.length)];
                        }
                    });
                    raf = requestAnimationFrame(draw);
                }
                draw();
            },
            disable() {
                if (!this.active) return; this.active = false;
                document.documentElement.classList.remove('fx-hacker');
                if (raf) cancelAnimationFrame(raf); raf = null;
                destroyCanvas(canvas); canvas = null; lines = [];
            }
        };
    })();

    // ==================================================
    // Public API
    // ==================================================
    function toggle(name, arg) {
        const fx = R[name];
        if (!fx) return false;
        if (fx.active) { fx.disable(); return false; }
        fx.enable(arg);
        return true;
    }
    function enable(name, arg) { const fx = R[name]; if (fx && !fx.active) fx.enable(arg); }
    function disable(name) { const fx = R[name]; if (fx && fx.active) fx.disable(); }
    function active() { return Object.keys(R).filter(k => R[k].active); }
    function disableAll() { Object.keys(R).forEach(k => { if (R[k].active) R[k].disable(); }); }

    window.__jaeEffects = {
        toggle, enable, disable, active, disableAll,
        registry: R,
        list: () => Object.keys(R),
    };
})();
