// ============================================================
// SoulSync — Page Particle System
// Single canvas, per-page particle behaviors
// ============================================================

(function () {
    'use strict';

    // Disable particles on mobile devices for performance
    if (window.innerWidth <= 768 || /Mobi|Android|iPhone|iPad|iPod/i.test(navigator.userAgent)) return;

    const canvas = document.getElementById('page-particles-canvas');
    if (!canvas) return;
    const ctx = canvas.getContext('2d');

    let animFrame = null;
    let frameCount = 0;
    let extras = {}; // temp ref swapped during initExtras calls

    // Transition: converge → burst between presets

    // ── Helpers ──

    let _cachedAccent = '29, 185, 84';
    let _accentCheckFrame = 0;
    function getAccentRGB() {
        // Only re-read CSS variable every 60 frames (~1s) to avoid getComputedStyle overhead
        if (_accentCheckFrame++ % 60 === 0) {
            const s = getComputedStyle(document.documentElement).getPropertyValue('--accent-rgb').trim();
            if (s) _cachedAccent = s;
        }
        return _cachedAccent;
    }

    // Shift an "r, g, b" accent string by a hue offset (degrees), cached per base color
    let _shiftCache = { base: '', shifts: {} };
    function shiftAccent(rgbStr, hueDeg) {
        if (hueDeg === 0) return rgbStr;
        // Check cache
        if (_shiftCache.base !== rgbStr) _shiftCache = { base: rgbStr, shifts: {} };
        const key = Math.round(hueDeg);
        if (key in _shiftCache.shifts) return _shiftCache.shifts[key];

        const [r, g, b] = rgbStr.split(',').map(s => parseInt(s.trim()));
        const rn = r / 255, gn = g / 255, bn = b / 255;
        const max = Math.max(rn, gn, bn), min = Math.min(rn, gn, bn);
        let h = 0, s = 0;
        const l = (max + min) / 2;
        if (max !== min) {
            const d = max - min;
            s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
            if (max === rn) h = ((gn - bn) / d + (gn < bn ? 6 : 0)) / 6;
            else if (max === gn) h = ((bn - rn) / d + 2) / 6;
            else h = ((rn - gn) / d + 4) / 6;
        }
        h = ((h * 360 + hueDeg) % 360 + 360) % 360;
        const c = (1 - Math.abs(2 * l - 1)) * s;
        const x = c * (1 - Math.abs((h / 60) % 2 - 1));
        const m = l - c / 2;
        let r1, g1, b1;
        if (h < 60) { r1 = c; g1 = x; b1 = 0; }
        else if (h < 120) { r1 = x; g1 = c; b1 = 0; }
        else if (h < 180) { r1 = 0; g1 = c; b1 = x; }
        else if (h < 240) { r1 = 0; g1 = x; b1 = c; }
        else if (h < 300) { r1 = x; g1 = 0; b1 = c; }
        else { r1 = c; g1 = 0; b1 = x; }
        const result = `${Math.round((r1 + m) * 255)}, ${Math.round((g1 + m) * 255)}, ${Math.round((b1 + m) * 255)}`;
        _shiftCache.shifts[key] = result;
        return result;
    }

    function resize() {
        const dpr = 1; // keep 1:1 for performance
        canvas.width = canvas.clientWidth * dpr;
        canvas.height = canvas.clientHeight * dpr;
    }

    window.addEventListener('resize', resize);
    resize();

    // ── Preset Definitions ──
    // Each preset: { count, init(p, i), update(p, time, i), draw(p, ctx, accent, time),
    //   optional: initExtras(), drawGlobal(ctx, particles, accent, time, extras) }

    const PRESETS = {

        // ── DASHBOARD — network nodes, connections, data packets, shooting stars ──
        dashboard: {
            count: 50,
            init(p, i) {
                p.x = Math.random() * canvas.width;
                p.y = Math.random() * canvas.height;
                p.vx = (Math.random() - 0.5) * 0.3;
                p.vy = (Math.random() - 0.5) * 0.3;
                p.phase = Math.random() * Math.PI * 2;
                // Hub nodes — ~10% are larger, brighter, attract more connections
                p.isHub = i < 5;
                p.radius = p.isHub ? (3 + Math.random() * 2) : (1.5 + Math.random() * 1.5);
                // Color offset — hue shift ±30° from accent for variety
                p.hueShift = (Math.random() - 0.5) * 60;
            },
            update(p) {
                p.x += p.vx;
                p.y += p.vy;
                if (p.x < 15 || p.x > canvas.width - 15) p.vx *= -1;
                if (p.y < 15 || p.y > canvas.height - 15) p.vy *= -1;
                p.x = Math.max(5, Math.min(canvas.width - 5, p.x));
                p.y = Math.max(5, Math.min(canvas.height - 5, p.y));
            },
            draw(p, ctx, accent, time) {
                const pulse = 0.5 + 0.5 * Math.sin(time * 1.5 + p.phase);
                const col = shiftAccent(accent, p.hueShift);

                // Glow — hubs get bigger, brighter glow
                const glowSize = p.isHub ? p.radius * 6 : p.radius * 4;
                const glowAlpha = p.isHub ? (0.18 + pulse * 0.12) : (0.10 + pulse * 0.07);
                const glow = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, glowSize);
                glow.addColorStop(0, `rgba(${col}, ${glowAlpha})`);
                glow.addColorStop(1, 'rgba(0,0,0,0)');
                ctx.beginPath();
                ctx.arc(p.x, p.y, glowSize, 0, Math.PI * 2);
                ctx.fillStyle = glow;
                ctx.fill();

                // Core
                const coreAlpha = p.isHub ? (0.5 + pulse * 0.3) : (0.3 + pulse * 0.2);
                ctx.beginPath();
                ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2);
                ctx.fillStyle = `rgba(${col}, ${coreAlpha})`;
                ctx.fill();

                // Hub ring
                if (p.isHub) {
                    ctx.beginPath();
                    ctx.arc(p.x, p.y, p.radius + 2 + pulse * 2, 0, Math.PI * 2);
                    ctx.strokeStyle = `rgba(${col}, ${0.08 + pulse * 0.06})`;
                    ctx.lineWidth = 0.8;
                    ctx.stroke();
                }
            },
            initExtras() {
                extras.packets = [];
                extras.shootingStars = [];
                extras.activeLines = {}; // "i-j" → glow remaining frames
                extras.connectionDist = 180;
            },
            drawGlobal(ctx, parts, accent, time, layerExtras) {
                const ex = layerExtras || extras;
                if (!ex.packets) { ex.packets = []; ex.shootingStars = []; ex.activeLines = {}; ex.connectionDist = 180; }
                const dist = ex.connectionDist;

                // Decay active line glows
                for (const key in ex.activeLines) {
                    ex.activeLines[key] -= 0.02;
                    if (ex.activeLines[key] <= 0) delete ex.activeLines[key];
                }

                // Connections — hubs connect further, active lines glow brighter
                for (let i = 0; i < parts.length; i++) {
                    for (let j = i + 1; j < parts.length; j++) {
                        const maxDist = (parts[i].isHub || parts[j].isHub) ? dist * 1.3 : dist;
                        const dx = parts[j].x - parts[i].x;
                        const dy = parts[j].y - parts[i].y;
                        const d = Math.sqrt(dx * dx + dy * dy);
                        if (d < maxDist) {
                            const key = `${i}-${j}`;
                            const baseAlpha = (1 - d / maxDist) * 0.10;
                            const glowBoost = ex.activeLines[key] || 0;
                            const lineCol = shiftAccent(accent, (parts[i].hueShift + parts[j].hueShift) * 0.5);

                            ctx.beginPath();
                            ctx.moveTo(parts[i].x, parts[i].y);
                            ctx.lineTo(parts[j].x, parts[j].y);
                            ctx.strokeStyle = `rgba(${lineCol}, ${baseAlpha + glowBoost * 0.25})`;
                            ctx.lineWidth = glowBoost > 0 ? 1.2 : 0.7;
                            ctx.stroke();
                        }
                    }
                }

                // Spawn packets — prefer hub connections
                if (frameCount % 60 === 0 && parts.length >= 2) {
                    // Pick a hub if possible, otherwise random
                    const hubs = parts.reduce((arr, p, i) => { if (p.isHub) arr.push(i); return arr; }, []);
                    const a = hubs.length > 0 && Math.random() < 0.6
                        ? hubs[Math.floor(Math.random() * hubs.length)]
                        : Math.floor(Math.random() * parts.length);
                    let b = a, best = dist * 1.5;
                    for (let i = 0; i < parts.length; i++) {
                        if (i === a) continue;
                        const dx = parts[i].x - parts[a].x;
                        const dy = parts[i].y - parts[a].y;
                        const d = Math.sqrt(dx * dx + dy * dy);
                        if (d < best) { best = d; b = i; }
                    }
                    if (b !== a) {
                        ex.packets.push({ from: a, to: b, t: 0, trail: [], hue: parts[a].hueShift });
                        // Light up this connection
                        const key = a < b ? `${a}-${b}` : `${b}-${a}`;
                        ex.activeLines[key] = 1;
                    }
                }

                // Draw packets with comet trail
                for (let i = ex.packets.length - 1; i >= 0; i--) {
                    const pkt = ex.packets[i];
                    pkt.t += 0.02;
                    if (pkt.t >= 1) { ex.packets.splice(i, 1); continue; }
                    const fa = parts[pkt.from], ta = parts[pkt.to];
                    const px = fa.x + (ta.x - fa.x) * pkt.t;
                    const py = fa.y + (ta.y - fa.y) * pkt.t;

                    // Store trail points
                    pkt.trail.push({ x: px, y: py });
                    if (pkt.trail.length > 8) pkt.trail.shift();

                    const col = shiftAccent(accent, pkt.hue);

                    // Draw trail
                    for (let t = 0; t < pkt.trail.length - 1; t++) {
                        const alpha = (t / pkt.trail.length) * 0.3;
                        const width = (t / pkt.trail.length) * 2;
                        ctx.beginPath();
                        ctx.moveTo(pkt.trail[t].x, pkt.trail[t].y);
                        ctx.lineTo(pkt.trail[t + 1].x, pkt.trail[t + 1].y);
                        ctx.strokeStyle = `rgba(${col}, ${alpha})`;
                        ctx.lineWidth = width;
                        ctx.lineCap = 'round';
                        ctx.stroke();
                    }

                    // Head glow
                    const g = ctx.createRadialGradient(px, py, 0, px, py, 8);
                    g.addColorStop(0, `rgba(${col}, 0.6)`);
                    g.addColorStop(0.4, `rgba(${col}, 0.15)`);
                    g.addColorStop(1, 'rgba(0,0,0,0)');
                    ctx.beginPath();
                    ctx.arc(px, py, 8, 0, Math.PI * 2);
                    ctx.fillStyle = g;
                    ctx.fill();

                    // Bright core
                    ctx.beginPath();
                    ctx.arc(px, py, 1.8, 0, Math.PI * 2);
                    ctx.fillStyle = 'rgba(255,255,255,0.9)';
                    ctx.fill();
                }

                // Shooting stars — spawn occasionally
                if (frameCount % 200 === 0 || (frameCount % 120 === 0 && Math.random() < 0.3)) {
                    const edge = Math.random();
                    let sx, sy, angle;
                    if (edge < 0.5) {
                        // From left/right
                        sx = edge < 0.25 ? -10 : canvas.width + 10;
                        sy = Math.random() * canvas.height * 0.6;
                        angle = edge < 0.25 ? (Math.random() * 0.4 + 0.1) : (Math.PI - Math.random() * 0.4 - 0.1);
                    } else {
                        // From top
                        sx = Math.random() * canvas.width;
                        sy = -10;
                        angle = Math.random() * 0.6 + 0.4 + (Math.random() < 0.5 ? 0 : Math.PI * 0.3);
                    }
                    const speed = 3 + Math.random() * 4;
                    ex.shootingStars.push({
                        x: sx, y: sy,
                        vx: Math.cos(angle) * speed,
                        vy: Math.sin(angle) * speed,
                        trail: [],
                        life: 1,
                        hue: (Math.random() - 0.5) * 80
                    });
                }

                // Draw shooting stars
                for (let i = ex.shootingStars.length - 1; i >= 0; i--) {
                    const star = ex.shootingStars[i];
                    star.x += star.vx;
                    star.y += star.vy;
                    star.life -= 0.012;
                    star.trail.push({ x: star.x, y: star.y });
                    if (star.trail.length > 20) star.trail.shift();

                    if (star.life <= 0 || star.x < -50 || star.x > canvas.width + 50 ||
                        star.y < -50 || star.y > canvas.height + 50) {
                        ex.shootingStars.splice(i, 1);
                        continue;
                    }

                    const col = shiftAccent(accent, star.hue);

                    // Trail
                    for (let t = 0; t < star.trail.length - 1; t++) {
                        const frac = t / star.trail.length;
                        const alpha = frac * 0.5 * star.life;
                        ctx.beginPath();
                        ctx.moveTo(star.trail[t].x, star.trail[t].y);
                        ctx.lineTo(star.trail[t + 1].x, star.trail[t + 1].y);
                        ctx.strokeStyle = `rgba(${col}, ${alpha})`;
                        ctx.lineWidth = frac * 2.5;
                        ctx.lineCap = 'round';
                        ctx.stroke();
                    }

                    // Head
                    const sg = ctx.createRadialGradient(star.x, star.y, 0, star.x, star.y, 5);
                    sg.addColorStop(0, `rgba(255, 255, 255, ${0.8 * star.life})`);
                    sg.addColorStop(0.3, `rgba(${col}, ${0.5 * star.life})`);
                    sg.addColorStop(1, 'rgba(0,0,0,0)');
                    ctx.beginPath();
                    ctx.arc(star.x, star.y, 5, 0, Math.PI * 2);
                    ctx.fillStyle = sg;
                    ctx.fill();
                }
            }
        },

        // ── SYNC — orbiting rings with sync arrows ──
        sync: {
            count: 40,
            init(p, i) {
                // Assign each particle to an orbital ring
                const ringCount = 4;
                p.ring = i % ringCount;
                const ringRadii = [0.08, 0.15, 0.23, 0.32]; // fraction of min(w,h)
                p.orbitRadius = ringRadii[p.ring];
                p.angle = (i / Math.ceil(40 / ringCount)) * Math.PI * 2 + Math.random() * 0.3;
                // Alternating directions per ring
                p.speed = (p.ring % 2 === 0 ? 1 : -1) * (0.004 + p.ring * 0.002);
                p.radius = 1.5 + Math.random() * 1.5;
                p.hueShift = (p.ring - 1.5) * 25; // spread hues across rings
                p.phase = Math.random() * Math.PI * 2;
                // Compute initial x/y
                const cx = canvas.width * 0.5, cy = canvas.height * 0.45;
                const r = p.orbitRadius * Math.min(canvas.width, canvas.height);
                p.x = cx + Math.cos(p.angle) * r;
                p.y = cy + Math.sin(p.angle) * r * 0.5; // elliptical
            },
            update(p) {
                p.angle += p.speed;
                const cx = canvas.width * 0.5, cy = canvas.height * 0.45;
                const r = p.orbitRadius * Math.min(canvas.width, canvas.height);
                p.x = cx + Math.cos(p.angle) * r;
                p.y = cy + Math.sin(p.angle) * r * 0.5;
            },
            draw(p, ctx, accent, time) {
                const pulse = 0.5 + 0.5 * Math.sin(time * 2 + p.phase);
                const col = shiftAccent(accent, p.hueShift);
                // Depth — particles "behind" center are dimmer
                const depth = Math.sin(p.angle);
                const depthAlpha = 0.3 + (depth * 0.5 + 0.5) * 0.5;

                const glow = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, p.radius * 4);
                glow.addColorStop(0, `rgba(${col}, ${(0.12 + pulse * 0.08) * depthAlpha})`);
                glow.addColorStop(1, 'rgba(0,0,0,0)');
                ctx.beginPath();
                ctx.arc(p.x, p.y, p.radius * 4, 0, Math.PI * 2);
                ctx.fillStyle = glow;
                ctx.fill();

                ctx.beginPath();
                ctx.arc(p.x, p.y, p.radius * (0.8 + depth * 0.2), 0, Math.PI * 2);
                ctx.fillStyle = `rgba(${col}, ${(0.3 + pulse * 0.25) * depthAlpha})`;
                ctx.fill();
            },
            initExtras() {
                extras.syncArrowAngle = 0;
                extras.transfers = []; // data pulses jumping between rings
                extras.arcs = []; // energy arcs between particles
                extras.transferTimer = 0;
            },
            drawGlobal(ctx, parts, accent, time, layerExtras) {
                const ex = layerExtras || extras;
                if (ex.syncArrowAngle === undefined) {
                    ex.syncArrowAngle = 0; ex.transfers = []; ex.arcs = []; ex.transferTimer = 0;
                }
                const cx = canvas.width * 0.5, cy = canvas.height * 0.45;
                const minDim = Math.min(canvas.width, canvas.height);
                const ringRadii = [0.08, 0.15, 0.23, 0.32];

                // Draw faint orbital paths — pulsing
                for (let r = 0; r < ringRadii.length; r++) {
                    const rx = ringRadii[r] * minDim;
                    const ry = rx * 0.5;
                    const col = shiftAccent(accent, (r - 1.5) * 25);
                    const ringPulse = 0.03 + 0.025 * Math.sin(time * 1.2 + r * 1.5);
                    ctx.beginPath();
                    ctx.ellipse(cx, cy, rx, ry, 0, 0, Math.PI * 2);
                    ctx.strokeStyle = `rgba(${col}, ${ringPulse})`;
                    ctx.lineWidth = 0.8;
                    ctx.stroke();
                }

                // Spawn data transfers between rings
                ex.transferTimer++;
                if (ex.transferTimer >= 50 + Math.floor(Math.random() * 30)) {
                    ex.transferTimer = 0;
                    // Pick two particles on different rings
                    const ringA = Math.floor(Math.random() * 4);
                    let ringB = ringA;
                    while (ringB === ringA) ringB = Math.floor(Math.random() * 4);
                    const fromParts = parts.filter(p => p.ring === ringA);
                    const toParts = parts.filter(p => p.ring === ringB);
                    if (fromParts.length && toParts.length) {
                        const from = fromParts[Math.floor(Math.random() * fromParts.length)];
                        const to = toParts[Math.floor(Math.random() * toParts.length)];
                        ex.transfers.push({
                            fx: from.x, fy: from.y,
                            tx: to.x, ty: to.y,
                            fromRef: from, toRef: to,
                            t: 0,
                            hue: (from.hueShift + to.hueShift) * 0.5,
                            trail: []
                        });
                    }
                }

                // Draw transfers — arc through center
                for (let i = ex.transfers.length - 1; i >= 0; i--) {
                    const tr = ex.transfers[i];
                    tr.t += 0.025;
                    // Update endpoints to track moving particles
                    tr.fx = tr.fromRef.x; tr.fy = tr.fromRef.y;
                    tr.tx = tr.toRef.x; tr.ty = tr.toRef.y;

                    if (tr.t >= 1) { ex.transfers.splice(i, 1); continue; }

                    // Bezier through center
                    const t = tr.t;
                    const mt = 1 - t;
                    const px = mt * mt * tr.fx + 2 * mt * t * cx + t * t * tr.tx;
                    const py = mt * mt * tr.fy + 2 * mt * t * cy + t * t * tr.ty;

                    tr.trail.push({ x: px, y: py });
                    if (tr.trail.length > 8) tr.trail.shift();

                    const col = shiftAccent(accent, tr.hue);

                    // Trail
                    for (let s = 0; s < tr.trail.length - 1; s++) {
                        const frac = s / tr.trail.length;
                        ctx.beginPath();
                        ctx.moveTo(tr.trail[s].x, tr.trail[s].y);
                        ctx.lineTo(tr.trail[s + 1].x, tr.trail[s + 1].y);
                        ctx.strokeStyle = `rgba(${col}, ${frac * 0.3})`;
                        ctx.lineWidth = frac * 2;
                        ctx.lineCap = 'round';
                        ctx.stroke();
                    }

                    // Head
                    const hg = ctx.createRadialGradient(px, py, 0, px, py, 6);
                    hg.addColorStop(0, `rgba(255,255,255, 0.5)`);
                    hg.addColorStop(0.3, `rgba(${col}, 0.3)`);
                    hg.addColorStop(1, 'rgba(0,0,0,0)');
                    ctx.beginPath();
                    ctx.arc(px, py, 6, 0, Math.PI * 2);
                    ctx.fillStyle = hg;
                    ctx.fill();
                }

                // Energy arcs — brief flickers between close particles on adjacent rings
                if (frameCount % 8 === 0) {
                    // Clean old arcs
                    for (let i = ex.arcs.length - 1; i >= 0; i--) {
                        ex.arcs[i].life -= 0.08;
                        if (ex.arcs[i].life <= 0) ex.arcs.splice(i, 1);
                    }
                    // Maybe spawn new arc
                    if (ex.arcs.length < 3 && Math.random() < 0.3) {
                        const a = Math.floor(Math.random() * parts.length);
                        let bestDist = 120, bestIdx = -1;
                        for (let j = 0; j < parts.length; j++) {
                            if (j === a || parts[j].ring === parts[a].ring) continue;
                            if (Math.abs(parts[j].ring - parts[a].ring) > 1) continue;
                            const dx = parts[j].x - parts[a].x, dy = parts[j].y - parts[a].y;
                            const d = Math.sqrt(dx * dx + dy * dy);
                            if (d < bestDist) { bestDist = d; bestIdx = j; }
                        }
                        if (bestIdx >= 0) {
                            ex.arcs.push({ a, b: bestIdx, life: 1 });
                        }
                    }
                }

                // Draw arcs
                for (const arc of ex.arcs) {
                    const pa = parts[arc.a], pb = parts[arc.b];
                    const col = shiftAccent(accent, (pa.hueShift + pb.hueShift) * 0.5);
                    ctx.beginPath();
                    ctx.moveTo(pa.x, pa.y);
                    const mx = (pa.x + pb.x) * 0.5 + (Math.random() - 0.5) * 6;
                    const my = (pa.y + pb.y) * 0.5 + (Math.random() - 0.5) * 6;
                    ctx.quadraticCurveTo(mx, my, pb.x, pb.y);
                    ctx.strokeStyle = `rgba(${col}, ${arc.life * 0.2})`;
                    ctx.lineWidth = arc.life * 1.5;
                    ctx.lineCap = 'round';
                    ctx.stroke();
                    // Bright core
                    ctx.strokeStyle = `rgba(255,255,255, ${arc.life * 0.08})`;
                    ctx.lineWidth = arc.life * 0.5;
                    ctx.stroke();
                }

                // Center sync arrows — two curved arrows rotating
                ex.syncArrowAngle += 0.008;
                const arrowR = minDim * 0.035;
                ctx.save();
                ctx.translate(cx, cy);
                ctx.rotate(ex.syncArrowAngle);
                for (let a = 0; a < 2; a++) {
                    ctx.save();
                    ctx.rotate(a * Math.PI);
                    ctx.beginPath();
                    ctx.arc(0, 0, arrowR, -0.9, 0.9);
                    ctx.strokeStyle = `rgba(${accent}, 0.15)`;
                    ctx.lineWidth = 1.5;
                    ctx.lineCap = 'round';
                    ctx.stroke();
                    // Arrowhead
                    const tx = arrowR * Math.cos(0.9), ty = arrowR * Math.sin(0.9);
                    ctx.beginPath();
                    ctx.moveTo(tx - 4, ty - 3);
                    ctx.lineTo(tx, ty);
                    ctx.lineTo(tx + 1, ty - 5);
                    ctx.stroke();
                    ctx.restore();
                }
                ctx.restore();

                // Center glow — pulses with transfer activity
                const activity = Math.min(1, ex.transfers.length * 0.3);
                const cg = ctx.createRadialGradient(cx, cy, 0, cx, cy, minDim * 0.06);
                cg.addColorStop(0, `rgba(${accent}, ${0.08 + activity * 0.1})`);
                cg.addColorStop(1, 'rgba(0,0,0,0)');
                ctx.beginPath();
                ctx.arc(cx, cy, minDim * 0.06, 0, Math.PI * 2);
                ctx.fillStyle = cg;
                ctx.fill();
            }
        },

        // ── SEARCH — radar/sonar pulse with scanning particles ──
        search: {
            count: 45,
            init(p, i) {
                // Scatter particles across canvas
                p.x = Math.random() * canvas.width;
                p.y = Math.random() * canvas.height;
                p.baseX = p.x;
                p.baseY = p.y;
                p.radius = 1 + Math.random() * 1.5;
                p.phase = Math.random() * Math.PI * 2;
                p.hueShift = (Math.random() - 0.5) * 50;
                // Slow drift
                p.vx = (Math.random() - 0.5) * 0.15;
                p.vy = (Math.random() - 0.5) * 0.15;
                // Lit state — brightens when radar sweeps over
                p.lit = 0;
                // Some particles are "result" nodes — slightly bigger, brighter
                p.isResult = i < 8;
                if (p.isResult) p.radius = 2 + Math.random() * 1.5;
            },
            update(p) {
                // Gentle drift
                p.x += p.vx;
                p.y += p.vy;
                // Soft bounds — bounce
                if (p.x < 10 || p.x > canvas.width - 10) p.vx *= -1;
                if (p.y < 10 || p.y > canvas.height - 10) p.vy *= -1;
                p.x = Math.max(5, Math.min(canvas.width - 5, p.x));
                p.y = Math.max(5, Math.min(canvas.height - 5, p.y));
                // Decay lit state
                if (p.lit > 0) p.lit = Math.max(0, p.lit - 0.015);
            },
            draw(p, ctx, accent, time) {
                const pulse = 0.5 + 0.5 * Math.sin(time * 1.2 + p.phase);
                const col = shiftAccent(accent, p.hueShift);
                const litBoost = p.lit;

                // Glow — bigger when lit
                const glowSize = p.radius * (3 + litBoost * 5);
                const glowAlpha = (p.isResult ? 0.08 : 0.05) + litBoost * 0.25;
                const glow = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, glowSize);
                glow.addColorStop(0, `rgba(${col}, ${glowAlpha})`);
                glow.addColorStop(1, 'rgba(0,0,0,0)');
                ctx.beginPath();
                ctx.arc(p.x, p.y, glowSize, 0, Math.PI * 2);
                ctx.fillStyle = glow;
                ctx.fill();

                // Core
                const coreAlpha = (p.isResult ? 0.25 : 0.15) + pulse * 0.1 + litBoost * 0.45;
                ctx.beginPath();
                ctx.arc(p.x, p.y, p.radius * (1 + litBoost * 0.3), 0, Math.PI * 2);
                ctx.fillStyle = `rgba(${col}, ${coreAlpha})`;
                ctx.fill();

                // Result ring when lit
                if (p.isResult && litBoost > 0.1) {
                    ctx.beginPath();
                    ctx.arc(p.x, p.y, p.radius + 3 + litBoost * 4, 0, Math.PI * 2);
                    ctx.strokeStyle = `rgba(${col}, ${litBoost * 0.2})`;
                    ctx.lineWidth = 0.6;
                    ctx.stroke();
                }
            },
            initExtras() {
                extras.rings = []; // expanding radar rings
                extras.spawnTimer = 0;
                extras.scanAngle = 0;
            },
            drawGlobal(ctx, parts, accent, time, layerExtras) {
                const ex = layerExtras || extras;
                if (!ex.rings) { ex.rings = []; ex.spawnTimer = 0; ex.scanAngle = 0; }

                const cx = canvas.width * 0.5, cy = canvas.height * 0.45;
                const maxRadius = Math.max(canvas.width, canvas.height) * 0.7;

                // Spawn rings periodically
                ex.spawnTimer++;
                if (ex.spawnTimer >= 120) { // every ~2 seconds
                    ex.rings.push({
                        radius: 0,
                        maxRadius: maxRadius,
                        alpha: 0.35,
                        hue: (Math.random() - 0.5) * 40
                    });
                    ex.spawnTimer = 0;
                }

                // Update and draw rings
                for (let i = ex.rings.length - 1; i >= 0; i--) {
                    const ring = ex.rings[i];
                    ring.radius += 2.5;
                    ring.alpha = 0.35 * (1 - ring.radius / ring.maxRadius);

                    if (ring.alpha <= 0 || ring.radius > ring.maxRadius) {
                        ex.rings.splice(i, 1);
                        continue;
                    }

                    const col = shiftAccent(accent, ring.hue);

                    // Ring stroke
                    ctx.beginPath();
                    ctx.arc(cx, cy, ring.radius, 0, Math.PI * 2);
                    ctx.strokeStyle = `rgba(${col}, ${ring.alpha * 0.5})`;
                    ctx.lineWidth = 1.5;
                    ctx.stroke();

                    // Soft inner glow on ring edge
                    const ringGlow = ctx.createRadialGradient(cx, cy, Math.max(0, ring.radius - 15), cx, cy, ring.radius + 5);
                    ringGlow.addColorStop(0, 'rgba(0,0,0,0)');
                    ringGlow.addColorStop(0.7, `rgba(${col}, ${ring.alpha * 0.08})`);
                    ringGlow.addColorStop(1, 'rgba(0,0,0,0)');
                    ctx.beginPath();
                    ctx.arc(cx, cy, ring.radius + 5, 0, Math.PI * 2);
                    ctx.fillStyle = ringGlow;
                    ctx.fill();

                    // Light up particles near this ring's edge
                    for (const p of parts) {
                        const dx = p.x - cx, dy = p.y - cy;
                        const dist = Math.sqrt(dx * dx + dy * dy);
                        const diff = Math.abs(dist - ring.radius);
                        if (diff < 25) {
                            p.lit = Math.max(p.lit, (1 - diff / 25) * ring.alpha * 2.5);
                        }
                    }
                }

                // Rotating scan line — faint sweep
                ex.scanAngle += 0.012;
                const scanLen = maxRadius * 0.6;
                const sx = cx + Math.cos(ex.scanAngle) * scanLen;
                const sy = cy + Math.sin(ex.scanAngle) * scanLen;
                const scanGrad = ctx.createLinearGradient(cx, cy, sx, sy);
                scanGrad.addColorStop(0, `rgba(${accent}, 0.06)`);
                scanGrad.addColorStop(0.7, `rgba(${accent}, 0.02)`);
                scanGrad.addColorStop(1, 'rgba(0,0,0,0)');
                ctx.beginPath();
                ctx.moveTo(cx, cy);
                ctx.lineTo(sx, sy);
                ctx.strokeStyle = scanGrad;
                ctx.lineWidth = 1;
                ctx.stroke();

                // Scan wedge — faint arc trailing the scan line
                ctx.beginPath();
                ctx.moveTo(cx, cy);
                ctx.arc(cx, cy, scanLen, ex.scanAngle - 0.3, ex.scanAngle, false);
                ctx.closePath();
                const wedgeGrad = ctx.createRadialGradient(cx, cy, 0, cx, cy, scanLen);
                wedgeGrad.addColorStop(0, `rgba(${accent}, 0.03)`);
                wedgeGrad.addColorStop(0.5, `rgba(${accent}, 0.01)`);
                wedgeGrad.addColorStop(1, 'rgba(0,0,0,0)');
                ctx.fillStyle = wedgeGrad;
                ctx.fill();

                // Center dot — radar origin
                const cg = ctx.createRadialGradient(cx, cy, 0, cx, cy, 12);
                cg.addColorStop(0, `rgba(${accent}, 0.15)`);
                cg.addColorStop(0.5, `rgba(${accent}, 0.05)`);
                cg.addColorStop(1, 'rgba(0,0,0,0)');
                ctx.beginPath();
                ctx.arc(cx, cy, 12, 0, Math.PI * 2);
                ctx.fillStyle = cg;
                ctx.fill();

                ctx.beginPath();
                ctx.arc(cx, cy, 2, 0, Math.PI * 2);
                ctx.fillStyle = `rgba(${accent}, 0.3)`;
                ctx.fill();

                // Crosshair lines — very subtle
                ctx.strokeStyle = `rgba(${accent}, 0.04)`;
                ctx.lineWidth = 0.5;
                ctx.beginPath();
                ctx.moveTo(cx - scanLen * 0.4, cy);
                ctx.lineTo(cx + scanLen * 0.4, cy);
                ctx.moveTo(cx, cy - scanLen * 0.4);
                ctx.lineTo(cx, cy + scanLen * 0.4);
                ctx.stroke();

                // Range circles — faint concentric guides
                for (let r = 1; r <= 3; r++) {
                    const guideR = scanLen * r * 0.3;
                    ctx.beginPath();
                    ctx.arc(cx, cy, guideR, 0, Math.PI * 2);
                    ctx.strokeStyle = `rgba(${accent}, 0.025)`;
                    ctx.lineWidth = 0.5;
                    ctx.stroke();
                }
            }
        },

        // ── DISCOVER — drifting constellation starfield ──
        discover: {
            count: 60,
            init(p, i) {
                p.x = Math.random() * canvas.width;
                p.y = Math.random() * canvas.height;
                // Slow parallax drift — different layers move at different speeds
                p.layer = Math.random(); // 0 = far/dim, 1 = near/bright
                p.vx = (0.05 + p.layer * 0.15) * (Math.random() < 0.5 ? 1 : -1);
                p.vy = -0.02 - p.layer * 0.08; // gentle upward drift
                p.radius = 0.5 + p.layer * 2;
                p.phase = Math.random() * Math.PI * 2;
                p.hueShift = (Math.random() - 0.5) * 70;
                // Twinkle speed — far stars twinkle faster
                p.twinkleSpeed = 1.5 + (1 - p.layer) * 2;
                // Some are "constellation" anchor stars — brighter, connected
                p.isAnchor = i < 15;
                if (p.isAnchor) {
                    p.radius = 1.5 + p.layer * 2;
                    p.vx *= 0.4;
                    p.vy *= 0.4;
                }
            },
            update(p) {
                p.x += p.vx;
                p.y += p.vy;
                // Wrap around edges
                if (p.x < -20) p.x = canvas.width + 20;
                if (p.x > canvas.width + 20) p.x = -20;
                if (p.y < -20) p.y = canvas.height + 20;
                if (p.y > canvas.height + 20) p.y = -20;
            },
            draw(p, ctx, accent, time) {
                const twinkle = 0.4 + 0.6 * Math.pow(Math.sin(time * p.twinkleSpeed + p.phase), 2);
                const col = shiftAccent(accent, p.hueShift);
                const brightness = (0.3 + p.layer * 0.7) * twinkle;

                // Soft glow
                const glowR = p.radius * (3 + p.layer * 3);
                const glow = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, glowR);
                glow.addColorStop(0, `rgba(${col}, ${brightness * 0.25})`);
                glow.addColorStop(0.5, `rgba(${col}, ${brightness * 0.06})`);
                glow.addColorStop(1, 'rgba(0,0,0,0)');
                ctx.beginPath();
                ctx.arc(p.x, p.y, glowR, 0, Math.PI * 2);
                ctx.fillStyle = glow;
                ctx.fill();

                // Core star
                ctx.beginPath();
                ctx.arc(p.x, p.y, p.radius * (0.6 + twinkle * 0.4), 0, Math.PI * 2);
                ctx.fillStyle = `rgba(${col}, ${brightness})`;
                ctx.fill();

                // Cross-spike on bright anchor stars
                if (p.isAnchor && twinkle > 0.7) {
                    const spikeLen = p.radius * 4 * twinkle;
                    const spikeAlpha = (twinkle - 0.7) * brightness * 0.5;
                    ctx.strokeStyle = `rgba(${col}, ${spikeAlpha})`;
                    ctx.lineWidth = 0.5;
                    ctx.beginPath();
                    ctx.moveTo(p.x - spikeLen, p.y);
                    ctx.lineTo(p.x + spikeLen, p.y);
                    ctx.moveTo(p.x, p.y - spikeLen);
                    ctx.lineTo(p.x, p.y + spikeLen);
                    ctx.stroke();
                }
            },
            initExtras() {
                extras.constellations = []; // groups of connected anchor indices
                extras.shootingStars = [];
                extras.nebulae = [];
                // Create 3 nebula regions
                for (let i = 0; i < 3; i++) {
                    extras.nebulae.push({
                        x: Math.random() * canvas.width,
                        y: Math.random() * canvas.height,
                        radius: 80 + Math.random() * 120,
                        hue: (Math.random() - 0.5) * 80,
                        drift: (Math.random() - 0.5) * 0.15
                    });
                }
            },
            drawGlobal(ctx, parts, accent, time, layerExtras) {
                const ex = layerExtras || extras;
                if (!ex.nebulae) { ex.nebulae = []; ex.shootingStars = []; ex.constellations = []; }

                // Draw nebula clouds — very subtle colored regions
                for (const neb of ex.nebulae) {
                    neb.x += neb.drift;
                    if (neb.x < -200) neb.x = canvas.width + 200;
                    if (neb.x > canvas.width + 200) neb.x = -200;

                    const col = shiftAccent(accent, neb.hue);
                    const pulse = 0.7 + 0.3 * Math.sin(time * 0.3 + neb.hue);
                    const ng = ctx.createRadialGradient(neb.x, neb.y, 0, neb.x, neb.y, neb.radius);
                    ng.addColorStop(0, `rgba(${col}, ${0.035 * pulse})`);
                    ng.addColorStop(0.5, `rgba(${col}, ${0.015 * pulse})`);
                    ng.addColorStop(1, 'rgba(0,0,0,0)');
                    ctx.beginPath();
                    ctx.arc(neb.x, neb.y, neb.radius, 0, Math.PI * 2);
                    ctx.fillStyle = ng;
                    ctx.fill();
                }

                // Constellation lines — connect nearby anchor stars
                const anchors = parts.filter(p => p.isAnchor);
                const conDist = 180;
                for (let i = 0; i < anchors.length; i++) {
                    for (let j = i + 1; j < anchors.length; j++) {
                        const dx = anchors[j].x - anchors[i].x;
                        const dy = anchors[j].y - anchors[i].y;
                        const d = Math.sqrt(dx * dx + dy * dy);
                        if (d < conDist) {
                            const alpha = (1 - d / conDist) * 0.08;
                            const col = shiftAccent(accent, (anchors[i].hueShift + anchors[j].hueShift) * 0.5);
                            ctx.beginPath();
                            ctx.moveTo(anchors[i].x, anchors[i].y);
                            ctx.lineTo(anchors[j].x, anchors[j].y);
                            ctx.strokeStyle = `rgba(${col}, ${alpha})`;
                            ctx.lineWidth = 0.5;
                            ctx.stroke();
                        }
                    }
                }

                // Shooting stars — occasional streaks
                if (frameCount % 180 === 0 || (frameCount % 100 === 0 && Math.random() < 0.25)) {
                    const sx = Math.random() * canvas.width;
                    const angle = Math.PI * 0.6 + Math.random() * 0.4;
                    ex.shootingStars.push({
                        x: sx, y: -10,
                        vx: Math.cos(angle) * (4 + Math.random() * 3),
                        vy: Math.sin(angle) * (4 + Math.random() * 3),
                        trail: [],
                        life: 1,
                        hue: (Math.random() - 0.5) * 60
                    });
                }

                for (let i = ex.shootingStars.length - 1; i >= 0; i--) {
                    const star = ex.shootingStars[i];
                    star.x += star.vx;
                    star.y += star.vy;
                    star.life -= 0.015;
                    star.trail.push({ x: star.x, y: star.y });
                    if (star.trail.length > 15) star.trail.shift();

                    if (star.life <= 0 || star.x < -50 || star.x > canvas.width + 50 ||
                        star.y > canvas.height + 50) {
                        ex.shootingStars.splice(i, 1);
                        continue;
                    }

                    const col = shiftAccent(accent, star.hue);
                    for (let t = 0; t < star.trail.length - 1; t++) {
                        const frac = t / star.trail.length;
                        ctx.beginPath();
                        ctx.moveTo(star.trail[t].x, star.trail[t].y);
                        ctx.lineTo(star.trail[t + 1].x, star.trail[t + 1].y);
                        ctx.strokeStyle = `rgba(${col}, ${frac * 0.4 * star.life})`;
                        ctx.lineWidth = frac * 2;
                        ctx.lineCap = 'round';
                        ctx.stroke();
                    }

                    const sg = ctx.createRadialGradient(star.x, star.y, 0, star.x, star.y, 4);
                    sg.addColorStop(0, `rgba(255,255,255, ${0.7 * star.life})`);
                    sg.addColorStop(0.4, `rgba(${col}, ${0.4 * star.life})`);
                    sg.addColorStop(1, 'rgba(0,0,0,0)');
                    ctx.beginPath();
                    ctx.arc(star.x, star.y, 4, 0, Math.PI * 2);
                    ctx.fillStyle = sg;
                    ctx.fill();
                }
            }
        },

        // ── ARTISTS — sound ripples / audio wave field ──
        artists: {
            count: 50,
            init(p, i) {
                // Grid-ish scatter with jitter
                const cols = 10, rows = 5;
                const col = i % cols, row = Math.floor(i / cols);
                p.baseX = (col + 0.5) / cols * canvas.width + (Math.random() - 0.5) * 40;
                p.baseY = (row + 0.5) / rows * canvas.height + (Math.random() - 0.5) * 40;
                p.x = p.baseX;
                p.y = p.baseY;
                p.radius = 1.2 + Math.random() * 1.3;
                p.phase = Math.random() * Math.PI * 2;
                p.hueShift = (Math.random() - 0.5) * 50;
                p.displacement = 0; // vertical displacement from ripples
            },
            update(p, time) {
                // Reset displacement each frame, drawGlobal will set it
                p.displacement = 0;
                p.x = p.baseX;
                p.y = p.baseY;
            },
            draw(p, ctx, accent, time) {
                const col = shiftAccent(accent, p.hueShift);
                const energy = Math.min(1, Math.abs(p.displacement));
                const alpha = 0.15 + energy * 0.6;
                const r = p.radius * (1 + energy * 0.5);

                // Glow — stronger when displaced
                const glowR = r * (3 + energy * 4);
                const glow = ctx.createRadialGradient(p.x, p.y + p.displacement * 15, 0, p.x, p.y + p.displacement * 15, glowR);
                glow.addColorStop(0, `rgba(${col}, ${0.06 + energy * 0.2})`);
                glow.addColorStop(1, 'rgba(0,0,0,0)');
                ctx.beginPath();
                ctx.arc(p.x, p.y + p.displacement * 15, glowR, 0, Math.PI * 2);
                ctx.fillStyle = glow;
                ctx.fill();

                // Core — offset by displacement
                ctx.beginPath();
                ctx.arc(p.x, p.y + p.displacement * 15, r, 0, Math.PI * 2);
                ctx.fillStyle = `rgba(${col}, ${alpha})`;
                ctx.fill();
            },
            initExtras() {
                extras.emitters = [];
                // 3–4 ripple emitters at random positions
                const count = 3 + Math.floor(Math.random() * 2);
                for (let i = 0; i < count; i++) {
                    extras.emitters.push({
                        x: 0.15 * canvas.width + Math.random() * 0.7 * canvas.width,
                        y: 0.15 * canvas.height + Math.random() * 0.7 * canvas.height,
                        rings: [],
                        timer: Math.floor(Math.random() * 80),
                        interval: 70 + Math.floor(Math.random() * 40),
                        hue: (i - 1.5) * 35
                    });
                }
                extras.waveLines = [];
            },
            drawGlobal(ctx, parts, accent, time, layerExtras) {
                const ex = layerExtras || extras;
                if (!ex.emitters) { ex.emitters = []; ex.waveLines = []; }
                const maxR = Math.max(canvas.width, canvas.height) * 0.45;

                // Update emitters — spawn rings (cap at 3 per emitter)
                for (const em of ex.emitters) {
                    em.timer++;
                    if (em.timer >= em.interval && em.rings.length < 3) {
                        em.rings.push({ radius: 0, alpha: 0.3 });
                        em.timer = 0;
                    }

                    const col = shiftAccent(accent, em.hue);

                    // Draw emitter center — subtle pulsing dot
                    const pulse = 0.5 + 0.5 * Math.sin(time * 2 + em.hue);
                    const eg = ctx.createRadialGradient(em.x, em.y, 0, em.x, em.y, 15);
                    eg.addColorStop(0, `rgba(${col}, ${0.1 + pulse * 0.08})`);
                    eg.addColorStop(1, 'rgba(0,0,0,0)');
                    ctx.beginPath();
                    ctx.arc(em.x, em.y, 15, 0, Math.PI * 2);
                    ctx.fillStyle = eg;
                    ctx.fill();

                    // Update and draw rings
                    for (let i = em.rings.length - 1; i >= 0; i--) {
                        const ring = em.rings[i];
                        ring.radius += 1.8;
                        ring.alpha = 0.25 * (1 - ring.radius / maxR);

                        if (ring.alpha <= 0 || ring.radius > maxR) {
                            em.rings.splice(i, 1);
                            continue;
                        }

                        // Ring arc
                        ctx.beginPath();
                        ctx.arc(em.x, em.y, ring.radius, 0, Math.PI * 2);
                        ctx.strokeStyle = `rgba(${col}, ${ring.alpha * 0.4})`;
                        ctx.lineWidth = 1.5 * (1 - ring.radius / maxR);
                        ctx.stroke();

                        // Displace particles near ring edge
                        for (const p of parts) {
                            const dx = p.baseX - em.x, dy = p.baseY - em.y;
                            const dist = Math.sqrt(dx * dx + dy * dy);
                            const diff = Math.abs(dist - ring.radius);
                            if (diff < 30) {
                                const wave = Math.cos((diff / 30) * Math.PI * 0.5);
                                p.displacement += wave * ring.alpha * 1.8;
                            }
                        }
                    }
                }

                // Horizontal wave lines — subtle sine waves across canvas
                ctx.lineWidth = 0.4;
                for (let w = 0; w < 3; w++) {
                    const yBase = canvas.height * (0.25 + w * 0.25);
                    const col = shiftAccent(accent, (w - 1) * 30);
                    ctx.beginPath();
                    for (let x = 0; x < canvas.width; x += 4) {
                        const y = yBase + Math.sin(x * 0.008 + time * (0.8 + w * 0.3) + w) * 12;
                        if (x === 0) ctx.moveTo(x, y);
                        else ctx.lineTo(x, y);
                    }
                    ctx.strokeStyle = `rgba(${col}, 0.04)`;
                    ctx.stroke();
                }
            }
        },

        // ── AUTOMATIONS — electric / lightning circuit ──
        automations: {
            count: 40,
            init(p, i) {
                p.x = Math.random() * canvas.width;
                p.y = Math.random() * canvas.height;
                p.vx = (Math.random() - 0.5) * 0.25;
                p.vy = (Math.random() - 0.5) * 0.25;
                p.radius = 1 + Math.random() * 1.5;
                p.phase = Math.random() * Math.PI * 2;
                p.hueShift = (Math.random() - 0.5) * 40;
                p.charge = 0; // lights up when struck by lightning
                // Some are relay nodes — bigger, attract bolts
                p.isRelay = i < 6;
                if (p.isRelay) p.radius = 2.5 + Math.random() * 1.5;
            },
            update(p) {
                p.x += p.vx;
                p.y += p.vy;
                if (p.x < 10 || p.x > canvas.width - 10) p.vx *= -1;
                if (p.y < 10 || p.y > canvas.height - 10) p.vy *= -1;
                p.x = Math.max(5, Math.min(canvas.width - 5, p.x));
                p.y = Math.max(5, Math.min(canvas.height - 5, p.y));
                if (p.charge > 0) p.charge = Math.max(0, p.charge - 0.02);
            },
            draw(p, ctx, accent, time) {
                const pulse = 0.5 + 0.5 * Math.sin(time * 2 + p.phase);
                const col = shiftAccent(accent, p.hueShift);
                const energy = p.charge;

                // Electric glow — bigger when charged
                const glowR = p.radius * (3 + energy * 6);
                const glow = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, glowR);
                glow.addColorStop(0, `rgba(${col}, ${(p.isRelay ? 0.12 : 0.06) + energy * 0.35})`);
                glow.addColorStop(0.6, `rgba(${col}, ${energy * 0.08})`);
                glow.addColorStop(1, 'rgba(0,0,0,0)');
                ctx.beginPath();
                ctx.arc(p.x, p.y, glowR, 0, Math.PI * 2);
                ctx.fillStyle = glow;
                ctx.fill();

                // Core
                const coreAlpha = (p.isRelay ? 0.3 : 0.15) + pulse * 0.1 + energy * 0.5;
                ctx.beginPath();
                ctx.arc(p.x, p.y, p.radius * (1 + energy * 0.3), 0, Math.PI * 2);
                ctx.fillStyle = `rgba(${col}, ${coreAlpha})`;
                ctx.fill();

                // Relay ring
                if (p.isRelay) {
                    ctx.beginPath();
                    ctx.arc(p.x, p.y, p.radius + 3 + pulse * 2, 0, Math.PI * 2);
                    ctx.strokeStyle = `rgba(${col}, ${0.06 + energy * 0.15})`;
                    ctx.lineWidth = 0.6;
                    ctx.stroke();
                }
            },
            initExtras() {
                extras.bolts = [];
                extras.sparks = [];
                extras.boltTimer = 0;
            },
            drawGlobal(ctx, parts, accent, time, layerExtras) {
                const ex = layerExtras || extras;
                if (!ex.bolts) { ex.bolts = []; ex.sparks = []; ex.boltTimer = 0; }

                // Spawn lightning bolts between relay nodes
                ex.boltTimer++;
                if (ex.boltTimer >= 45 + Math.floor(Math.random() * 30)) {
                    ex.boltTimer = 0;
                    const relays = [];
                    const normals = [];
                    parts.forEach((p, i) => { if (p.isRelay) relays.push(i); else normals.push(i); });

                    if (relays.length >= 2 || parts.length >= 2) {
                        let a, b;
                        if (relays.length >= 2 && Math.random() < 0.6) {
                            // Relay to relay
                            a = relays[Math.floor(Math.random() * relays.length)];
                            b = relays[Math.floor(Math.random() * relays.length)];
                            if (a === b) b = relays[(relays.indexOf(a) + 1) % relays.length];
                        } else {
                            // Relay to nearest normal
                            a = relays.length > 0 ? relays[Math.floor(Math.random() * relays.length)] : Math.floor(Math.random() * parts.length);
                            let bestD = Infinity;
                            b = a;
                            for (let i = 0; i < parts.length; i++) {
                                if (i === a) continue;
                                const dx = parts[i].x - parts[a].x, dy = parts[i].y - parts[a].y;
                                const d = dx * dx + dy * dy;
                                if (d < bestD && d < 300 * 300) { bestD = d; b = i; }
                            }
                        }
                        if (a !== b) {
                            // Build jagged bolt path
                            const pa = parts[a], pb = parts[b];
                            const segs = 6 + Math.floor(Math.random() * 4);
                            const path = [{ x: pa.x, y: pa.y }];
                            for (let s = 1; s < segs; s++) {
                                const t = s / segs;
                                const mx = pa.x + (pb.x - pa.x) * t;
                                const my = pa.y + (pb.y - pa.y) * t;
                                const jitter = 25 * (1 - Math.abs(t - 0.5) * 2);
                                path.push({
                                    x: mx + (Math.random() - 0.5) * jitter * 2,
                                    y: my + (Math.random() - 0.5) * jitter * 2
                                });
                            }
                            path.push({ x: pb.x, y: pb.y });
                            ex.bolts.push({
                                path,
                                life: 1,
                                hue: (parts[a].hueShift + parts[b].hueShift) * 0.5,
                                branch: Math.random() < 0.4 // some bolts branch
                            });
                            parts[a].charge = 1;
                            parts[b].charge = 1;

                            // Sparks at endpoints
                            for (let s = 0; s < 4; s++) {
                                ex.sparks.push({
                                    x: pb.x, y: pb.y,
                                    vx: (Math.random() - 0.5) * 3,
                                    vy: (Math.random() - 0.5) * 3,
                                    life: 0.6 + Math.random() * 0.4,
                                    hue: parts[b].hueShift
                                });
                            }
                        }
                    }
                }

                // Draw bolts
                for (let i = ex.bolts.length - 1; i >= 0; i--) {
                    const bolt = ex.bolts[i];
                    bolt.life -= 0.04;
                    if (bolt.life <= 0) { ex.bolts.splice(i, 1); continue; }

                    const col = shiftAccent(accent, bolt.hue);
                    const alpha = bolt.life;

                    // Main bolt
                    ctx.beginPath();
                    ctx.moveTo(bolt.path[0].x, bolt.path[0].y);
                    for (let s = 1; s < bolt.path.length; s++) {
                        ctx.lineTo(bolt.path[s].x, bolt.path[s].y);
                    }
                    ctx.strokeStyle = `rgba(${col}, ${alpha * 0.6})`;
                    ctx.lineWidth = 2 * alpha;
                    ctx.lineCap = 'round';
                    ctx.lineJoin = 'round';
                    ctx.stroke();

                    // Bright core
                    ctx.strokeStyle = `rgba(255,255,255, ${alpha * 0.4})`;
                    ctx.lineWidth = 0.8 * alpha;
                    ctx.stroke();

                    // Glow along bolt
                    ctx.strokeStyle = `rgba(${col}, ${alpha * 0.15})`;
                    ctx.lineWidth = 6 * alpha;
                    ctx.stroke();

                    // Branch bolt
                    if (bolt.branch && bolt.path.length > 3) {
                        const branchIdx = 2 + Math.floor(Math.random() * (bolt.path.length - 3));
                        const bp = bolt.path[branchIdx];
                        ctx.beginPath();
                        ctx.moveTo(bp.x, bp.y);
                        let bx = bp.x, by = bp.y;
                        for (let s = 0; s < 3; s++) {
                            bx += (Math.random() - 0.5) * 30;
                            by += (Math.random() - 0.5) * 30;
                            ctx.lineTo(bx, by);
                        }
                        ctx.strokeStyle = `rgba(${col}, ${alpha * 0.3})`;
                        ctx.lineWidth = 1 * alpha;
                        ctx.stroke();
                        bolt.branch = false; // only draw branch once
                    }
                }

                // Draw sparks
                for (let i = ex.sparks.length - 1; i >= 0; i--) {
                    const sp = ex.sparks[i];
                    sp.x += sp.vx;
                    sp.y += sp.vy;
                    sp.vx *= 0.95;
                    sp.vy *= 0.95;
                    sp.life -= 0.03;
                    if (sp.life <= 0) { ex.sparks.splice(i, 1); continue; }

                    const col = shiftAccent(accent, sp.hue);
                    ctx.beginPath();
                    ctx.arc(sp.x, sp.y, 1.2 * sp.life, 0, Math.PI * 2);
                    ctx.fillStyle = `rgba(${col}, ${sp.life * 0.8})`;
                    ctx.fill();
                }

                // Ambient circuit traces — faint grid lines
                ctx.strokeStyle = `rgba(${accent}, 0.015)`;
                ctx.lineWidth = 0.5;
                const spacing = 80;
                for (let x = spacing; x < canvas.width; x += spacing) {
                    const wobble = Math.sin(x * 0.01 + time * 0.5) * 5;
                    ctx.beginPath();
                    ctx.moveTo(x + wobble, 0);
                    ctx.lineTo(x - wobble, canvas.height);
                    ctx.stroke();
                }
                for (let y = spacing; y < canvas.height; y += spacing) {
                    const wobble = Math.cos(y * 0.01 + time * 0.5) * 5;
                    ctx.beginPath();
                    ctx.moveTo(0, y + wobble);
                    ctx.lineTo(canvas.width, y - wobble);
                    ctx.stroke();
                }
            }
        },

        // ── LIBRARY — vinyl turntable grooves ──
        library: {
            count: 50,
            init(p, i) {
                const cx = canvas.width * 0.5, cy = canvas.height * 0.5;
                const minDim = Math.min(canvas.width, canvas.height);
                // Distribute across grooves — 8 concentric rings
                const grooveCount = 8;
                p.groove = i % grooveCount;
                p.grooveRadius = (0.06 + p.groove * 0.045) * minDim;
                p.angle = (i / Math.ceil(50 / grooveCount)) * Math.PI * 2 + Math.random() * 0.5;
                // All rotate same direction (clockwise), outer grooves slightly slower
                p.speed = 0.003 - p.groove * 0.0002;
                p.radius = 1 + Math.random() * 1.2;
                p.phase = Math.random() * Math.PI * 2;
                p.hueShift = (p.groove - 3.5) * 12;
                p.x = cx + Math.cos(p.angle) * p.grooveRadius;
                p.y = cy + Math.sin(p.angle) * p.grooveRadius;
            },
            update(p) {
                p.angle += p.speed;
                const cx = canvas.width * 0.5, cy = canvas.height * 0.5;
                p.x = cx + Math.cos(p.angle) * p.grooveRadius;
                p.y = cy + Math.sin(p.angle) * p.grooveRadius;
            },
            draw(p, ctx, accent, time) {
                const pulse = 0.5 + 0.5 * Math.sin(time * 1.5 + p.phase);
                const col = shiftAccent(accent, p.hueShift);

                const glowR = p.radius * 4;
                const glow = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, glowR);
                glow.addColorStop(0, `rgba(${col}, ${0.1 + pulse * 0.08})`);
                glow.addColorStop(1, 'rgba(0,0,0,0)');
                ctx.beginPath();
                ctx.arc(p.x, p.y, glowR, 0, Math.PI * 2);
                ctx.fillStyle = glow;
                ctx.fill();

                ctx.beginPath();
                ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2);
                ctx.fillStyle = `rgba(${col}, ${0.25 + pulse * 0.2})`;
                ctx.fill();
            },
            initExtras() {
                extras.tonearmAngle = -0.4;
                extras.spinHighlight = 0;
                extras.needleRipples = [];
                extras.rippleTimer = 0;
                extras.noteParticles = []; // tiny notes floating off the needle
            },
            drawGlobal(ctx, parts, accent, time, layerExtras) {
                const ex = layerExtras || extras;
                if (ex.tonearmAngle === undefined) {
                    ex.tonearmAngle = -0.4; ex.spinHighlight = 0;
                    ex.needleRipples = []; ex.rippleTimer = 0; ex.noteParticles = [];
                }
                const cx = canvas.width * 0.5, cy = canvas.height * 0.5;
                const minDim = Math.min(canvas.width, canvas.height);

                // Vinyl grooves — concentric rings with subtle wobble
                const grooveCount = 8;
                for (let g = 0; g < grooveCount; g++) {
                    const r = (0.06 + g * 0.045) * minDim;
                    const col = shiftAccent(accent, (g - 3.5) * 12);
                    // Groove brightness pulses slightly
                    const gPulse = 0.03 + 0.015 * Math.sin(time * 0.8 + g * 0.7);
                    ctx.beginPath();
                    ctx.arc(cx, cy, r, 0, Math.PI * 2);
                    ctx.strokeStyle = `rgba(${col}, ${gPulse})`;
                    ctx.lineWidth = 0.8;
                    ctx.stroke();
                }

                // Outer rim
                const outerR = (0.06 + grooveCount * 0.045 + 0.02) * minDim;
                ctx.beginPath();
                ctx.arc(cx, cy, outerR, 0, Math.PI * 2);
                ctx.strokeStyle = `rgba(${accent}, 0.06)`;
                ctx.lineWidth = 1.5;
                ctx.stroke();

                // Center label — warm glow
                const labelR = 0.04 * minDim;
                const lg = ctx.createRadialGradient(cx, cy, 0, cx, cy, labelR);
                lg.addColorStop(0, `rgba(${accent}, 0.15)`);
                lg.addColorStop(0.6, `rgba(${accent}, 0.06)`);
                lg.addColorStop(1, 'rgba(0,0,0,0)');
                ctx.beginPath();
                ctx.arc(cx, cy, labelR, 0, Math.PI * 2);
                ctx.fillStyle = lg;
                ctx.fill();

                // Spindle dot
                ctx.beginPath();
                ctx.arc(cx, cy, 2.5, 0, Math.PI * 2);
                ctx.fillStyle = `rgba(${accent}, 0.3)`;
                ctx.fill();

                // Light reflection — rotating highlight arc
                ex.spinHighlight += 0.006;
                const hlAngle = ex.spinHighlight;
                const hlR = 0.22 * minDim;
                ctx.beginPath();
                ctx.arc(cx, cy, hlR, hlAngle - 0.4, hlAngle + 0.4);
                const hlGrad = ctx.createRadialGradient(
                    cx + Math.cos(hlAngle) * hlR * 0.5,
                    cy + Math.sin(hlAngle) * hlR * 0.5, 0,
                    cx + Math.cos(hlAngle) * hlR * 0.5,
                    cy + Math.sin(hlAngle) * hlR * 0.5, hlR * 0.6
                );
                hlGrad.addColorStop(0, `rgba(255,255,255, 0.03)`);
                hlGrad.addColorStop(1, 'rgba(0,0,0,0)');
                ctx.strokeStyle = hlGrad;
                ctx.lineWidth = hlR * 0.8;
                ctx.stroke();

                // Tonearm — pivot at top-right, needle rests on groove 5
                const armPivotX = cx + outerR * 0.9;
                const armPivotY = cy - outerR * 0.75;
                const needleGroove = 5;
                const needleR = (0.06 + needleGroove * 0.045) * minDim;
                // Needle sits on the groove at a fixed angle from center
                const needleAngle = -0.6 + Math.sin(time * 0.3) * 0.01; // subtle bob
                const armEndX = cx + Math.cos(needleAngle) * needleR;
                const armEndY = cy + Math.sin(needleAngle) * needleR;

                // Arm line — straight from pivot to needle
                ctx.beginPath();
                ctx.moveTo(armPivotX, armPivotY);
                ctx.lineTo(armEndX, armEndY);
                ctx.strokeStyle = `rgba(${accent}, 0.1)`;
                ctx.lineWidth = 1.8;
                ctx.lineCap = 'round';
                ctx.stroke();

                // Headshell — small wider section at the end
                const hsLen = 12;
                const hsDx = (armEndX - armPivotX), hsDy = (armEndY - armPivotY);
                const hsNorm = Math.sqrt(hsDx * hsDx + hsDy * hsDy);
                const hsX = armEndX - (hsDx / hsNorm) * hsLen;
                const hsY = armEndY - (hsDy / hsNorm) * hsLen;
                ctx.beginPath();
                ctx.moveTo(hsX, hsY);
                ctx.lineTo(armEndX, armEndY);
                ctx.strokeStyle = `rgba(${accent}, 0.15)`;
                ctx.lineWidth = 3;
                ctx.stroke();

                // Pivot dot
                ctx.beginPath();
                ctx.arc(armPivotX, armPivotY, 3, 0, Math.PI * 2);
                ctx.fillStyle = `rgba(${accent}, 0.12)`;
                ctx.fill();

                // Needle glow — on the groove
                const ng = ctx.createRadialGradient(armEndX, armEndY, 0, armEndX, armEndY, 8);
                ng.addColorStop(0, `rgba(${accent}, 0.3)`);
                ng.addColorStop(0.5, `rgba(${accent}, 0.1)`);
                ng.addColorStop(1, 'rgba(0,0,0,0)');
                ctx.beginPath();
                ctx.arc(armEndX, armEndY, 8, 0, Math.PI * 2);
                ctx.fillStyle = ng;
                ctx.fill();

                // Sound wave ripples — concentric arcs expanding FROM the needle outward
                ex.rippleTimer++;
                if (ex.rippleTimer >= 30) {
                    ex.rippleTimer = 0;
                    ex.needleRipples.push({
                        radius: 0, alpha: 0.3,
                        hue: (Math.random() - 0.5) * 30
                    });
                }

                const maxRippleR = outerR * 0.7;
                for (let i = ex.needleRipples.length - 1; i >= 0; i--) {
                    const rip = ex.needleRipples[i];
                    rip.radius += 1.8;
                    rip.alpha = 0.25 * (1 - rip.radius / maxRippleR);
                    if (rip.alpha <= 0 || rip.radius > maxRippleR) {
                        ex.needleRipples.splice(i, 1);
                        continue;
                    }
                    const col = shiftAccent(accent, rip.hue);
                    // Full circle ripple centered on needle
                    ctx.beginPath();
                    ctx.arc(armEndX, armEndY, rip.radius, 0, Math.PI * 2);
                    ctx.strokeStyle = `rgba(${col}, ${rip.alpha * 0.35})`;
                    ctx.lineWidth = 1 * (1 - rip.radius / maxRippleR);
                    ctx.stroke();

                    // Brighten groove particles near ripple edge
                    for (const p of parts) {
                        const dx = p.x - armEndX, dy = p.y - armEndY;
                        const dist = Math.sqrt(dx * dx + dy * dy);
                        if (Math.abs(dist - rip.radius) < 15) {
                            p.phase = time * 1.5;
                        }
                    }
                }

                // Floating note particles — tiny shapes drifting from needle area
                if (frameCount % 60 === 0) {
                    ex.noteParticles.push({
                        x: armEndX + (Math.random() - 0.5) * 15,
                        y: armEndY,
                        vx: (Math.random() - 0.5) * 0.8,
                        vy: -0.5 - Math.random() * 0.8,
                        life: 1,
                        size: 3 + Math.random() * 3,
                        hue: (Math.random() - 0.5) * 40,
                        type: Math.floor(Math.random() * 2) // 0 = eighth note, 1 = beam pair
                    });
                }

                for (let i = ex.noteParticles.length - 1; i >= 0; i--) {
                    const n = ex.noteParticles[i];
                    n.x += n.vx;
                    n.y += n.vy;
                    n.vx += (Math.random() - 0.5) * 0.02;
                    n.life -= 0.008;
                    if (n.life <= 0) { ex.noteParticles.splice(i, 1); continue; }

                    const col = shiftAccent(accent, n.hue);
                    const a = n.life * 0.3;
                    const s = n.size;

                    if (n.type === 0) {
                        // Eighth note — circle head + stem + flag
                        ctx.beginPath();
                        ctx.arc(n.x, n.y, s * 0.35, 0, Math.PI * 2);
                        ctx.fillStyle = `rgba(${col}, ${a})`;
                        ctx.fill();
                        ctx.beginPath();
                        ctx.moveTo(n.x + s * 0.35, n.y);
                        ctx.lineTo(n.x + s * 0.35, n.y - s);
                        ctx.strokeStyle = `rgba(${col}, ${a})`;
                        ctx.lineWidth = 0.8;
                        ctx.stroke();
                        // Flag
                        ctx.beginPath();
                        ctx.moveTo(n.x + s * 0.35, n.y - s);
                        ctx.quadraticCurveTo(n.x + s * 0.8, n.y - s * 0.6, n.x + s * 0.35, n.y - s * 0.4);
                        ctx.strokeStyle = `rgba(${col}, ${a * 0.8})`;
                        ctx.stroke();
                    } else {
                        // Beamed pair — two note heads connected
                        ctx.beginPath();
                        ctx.arc(n.x, n.y, s * 0.3, 0, Math.PI * 2);
                        ctx.arc(n.x + s * 0.6, n.y, s * 0.3, 0, Math.PI * 2);
                        ctx.fillStyle = `rgba(${col}, ${a})`;
                        ctx.fill();
                        // Stems
                        ctx.beginPath();
                        ctx.moveTo(n.x + s * 0.3, n.y);
                        ctx.lineTo(n.x + s * 0.3, n.y - s * 0.9);
                        ctx.moveTo(n.x + s * 0.9, n.y);
                        ctx.lineTo(n.x + s * 0.9, n.y - s * 0.9);
                        ctx.strokeStyle = `rgba(${col}, ${a})`;
                        ctx.lineWidth = 0.8;
                        ctx.stroke();
                        // Beam
                        ctx.beginPath();
                        ctx.moveTo(n.x + s * 0.3, n.y - s * 0.9);
                        ctx.lineTo(n.x + s * 0.9, n.y - s * 0.9);
                        ctx.lineWidth = 1.2;
                        ctx.stroke();
                    }
                }
            }
        },

        // ── IMPORT — data stream flowing into a central vortex ──
        import: {
            count: 55,
            init(p, i) {
                const cx = canvas.width * 0.5, cy = canvas.height * 0.45;
                // Spawn from edges, will flow toward center
                p.radius = 0.8 + Math.random() * 1.3;
                p.hueShift = (Math.random() - 0.5) * 50;
                p.phase = Math.random() * Math.PI * 2;
                p.speed = 0.8 + Math.random() * 1.2;
                p.absorbed = false;
                // Stream column — particles fall in vertical streams
                p.stream = i % 7;
                p.streamX = canvas.width * (0.1 + (p.stream / 6) * 0.8);
                // Stagger vertically
                p.x = p.streamX + (Math.random() - 0.5) * 20;
                p.y = -Math.random() * canvas.height;
                // Pull radius — when close to center, spiral in
                p.pullDist = 120 + Math.random() * 60;
                p.spiralAngle = Math.atan2(p.y - cy, p.x - cx);
                p.trail = [];
            },
            update(p) {
                const cx = canvas.width * 0.5, cy = canvas.height * 0.45;
                const dx = cx - p.x, dy = cy - p.y;
                const dist = Math.sqrt(dx * dx + dy * dy);

                if (dist < p.pullDist) {
                    // Spiral inward
                    p.spiralAngle += 0.08;
                    const pullStrength = 1 - dist / p.pullDist;
                    const spiralR = dist * (1 - pullStrength * 0.04);
                    p.x = cx + Math.cos(p.spiralAngle) * spiralR;
                    p.y = cy + Math.sin(p.spiralAngle) * spiralR;

                    if (dist < 8) {
                        // Absorbed — reset from top
                        p.x = p.streamX + (Math.random() - 0.5) * 20;
                        p.y = -10 - Math.random() * 100;
                        p.spiralAngle = Math.atan2(p.y - cy, p.x - cx);
                        p.trail = [];
                    }
                } else {
                    // Fall downward in stream
                    p.x += Math.sin(p.phase + p.y * 0.005) * 0.3;
                    p.y += p.speed;
                    // Update spiral angle as we approach
                    p.spiralAngle = Math.atan2(p.y - cy, p.x - cx);
                }

                // Trail
                p.trail.push({ x: p.x, y: p.y });
                if (p.trail.length > 6) p.trail.shift();

                // Reset if off screen
                if (p.y > canvas.height + 30 && dist > p.pullDist) {
                    p.x = p.streamX + (Math.random() - 0.5) * 20;
                    p.y = -10 - Math.random() * 50;
                    p.trail = [];
                }
            },
            draw(p, ctx, accent, time) {
                const cx = canvas.width * 0.5, cy = canvas.height * 0.45;
                const dist = Math.sqrt((p.x - cx) ** 2 + (p.y - cy) ** 2);
                const nearCenter = Math.max(0, 1 - dist / p.pullDist);
                const col = shiftAccent(accent, p.hueShift);
                const pulse = 0.5 + 0.5 * Math.sin(time * 2 + p.phase);

                // Trail
                for (let t = 0; t < p.trail.length - 1; t++) {
                    const frac = t / p.trail.length;
                    ctx.beginPath();
                    ctx.moveTo(p.trail[t].x, p.trail[t].y);
                    ctx.lineTo(p.trail[t + 1].x, p.trail[t + 1].y);
                    ctx.strokeStyle = `rgba(${col}, ${frac * 0.15 * (1 + nearCenter)})`;
                    ctx.lineWidth = frac * 1.5;
                    ctx.lineCap = 'round';
                    ctx.stroke();
                }

                // Glow — intensifies near center
                const glowR = p.radius * (3 + nearCenter * 4);
                const glow = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, glowR);
                glow.addColorStop(0, `rgba(${col}, ${0.08 + nearCenter * 0.25 + pulse * 0.05})`);
                glow.addColorStop(1, 'rgba(0,0,0,0)');
                ctx.beginPath();
                ctx.arc(p.x, p.y, glowR, 0, Math.PI * 2);
                ctx.fillStyle = glow;
                ctx.fill();

                // Core
                ctx.beginPath();
                ctx.arc(p.x, p.y, p.radius * (1 + nearCenter * 0.5), 0, Math.PI * 2);
                ctx.fillStyle = `rgba(${col}, ${0.2 + nearCenter * 0.5 + pulse * 0.1})`;
                ctx.fill();
            },
            initExtras() {
                extras.portalPulse = 0;
            },
            drawGlobal(ctx, parts, accent, time, layerExtras) {
                const ex = layerExtras || extras;
                if (ex.portalPulse === undefined) ex.portalPulse = 0;
                const cx = canvas.width * 0.5, cy = canvas.height * 0.45;
                const minDim = Math.min(canvas.width, canvas.height);

                // Portal at center — pulsing vortex
                ex.portalPulse += 0.02;

                // Concentric portal rings
                for (let r = 0; r < 4; r++) {
                    const radius = 10 + r * 12;
                    const rot = time * (1.5 - r * 0.3) * (r % 2 === 0 ? 1 : -1);
                    const alpha = 0.08 - r * 0.015;
                    const col = shiftAccent(accent, r * 20 - 30);

                    ctx.beginPath();
                    ctx.arc(cx, cy, radius, rot, rot + Math.PI * 1.5);
                    ctx.strokeStyle = `rgba(${col}, ${alpha})`;
                    ctx.lineWidth = 1.5 - r * 0.2;
                    ctx.lineCap = 'round';
                    ctx.stroke();
                }

                // Center glow — pulses with absorptions
                const gPulse = 0.8 + 0.2 * Math.sin(ex.portalPulse * 3);
                const cg = ctx.createRadialGradient(cx, cy, 0, cx, cy, 40);
                cg.addColorStop(0, `rgba(${accent}, ${0.15 * gPulse})`);
                cg.addColorStop(0.4, `rgba(${accent}, ${0.05 * gPulse})`);
                cg.addColorStop(1, 'rgba(0,0,0,0)');
                ctx.beginPath();
                ctx.arc(cx, cy, 40, 0, Math.PI * 2);
                ctx.fillStyle = cg;
                ctx.fill();

                // Stream guide lines — faint vertical lines showing data streams
                for (let s = 0; s < 7; s++) {
                    const sx = canvas.width * (0.1 + (s / 6) * 0.8);
                    const col = shiftAccent(accent, (s - 3) * 15);

                    // Fade line from top toward center
                    const grad = ctx.createLinearGradient(sx, 0, sx, cy);
                    grad.addColorStop(0, `rgba(${col}, 0.03)`);
                    grad.addColorStop(0.7, `rgba(${col}, 0.015)`);
                    grad.addColorStop(1, 'rgba(0,0,0,0)');
                    ctx.beginPath();
                    ctx.moveTo(sx, 0);
                    // Curve toward center
                    ctx.quadraticCurveTo(sx, cy * 0.7, cx, cy);
                    ctx.strokeStyle = grad;
                    ctx.lineWidth = 0.5;
                    ctx.stroke();
                }

                // Outer absorption ring — faint
                const absR = 120 + Math.sin(time * 0.8) * 10;
                ctx.beginPath();
                ctx.arc(cx, cy, absR, 0, Math.PI * 2);
                ctx.strokeStyle = `rgba(${accent}, 0.025)`;
                ctx.lineWidth = 0.5;
                ctx.setLineDash([4, 8]);
                ctx.stroke();
                ctx.setLineDash([]);
            }
        },

        // ── SETTINGS — interlocking gears / clockwork ──
        settings: {
            count: 45,
            init(p, i) {
                // Assign particles to gears
                const gearDefs = [
                    { cx: 0.35, cy: 0.4, r: 0.16, teeth: 12, dir: 1 },
                    { cx: 0.62, cy: 0.38, r: 0.13, teeth: 10, dir: -1 },
                    { cx: 0.48, cy: 0.65, r: 0.10, teeth: 8, dir: 1 },
                    { cx: 0.25, cy: 0.68, r: 0.08, teeth: 6, dir: -1 },
                    { cx: 0.72, cy: 0.62, r: 0.09, teeth: 7, dir: 1 },
                ];
                p.gear = i % gearDefs.length;
                const g = gearDefs[p.gear];
                const minDim = Math.min(canvas.width, canvas.height);
                p.gearCx = g.cx * canvas.width;
                p.gearCy = g.cy * canvas.height;
                p.gearR = g.r * minDim;
                p.gearTeeth = g.teeth;
                p.gearDir = g.dir;
                // Position along gear rim
                p.angle = (i / Math.ceil(45 / gearDefs.length)) * Math.PI * 2 + Math.random() * 0.3;
                p.speed = g.dir * (0.003 + (1 / g.teeth) * 0.008);
                p.radius = 1 + Math.random() * 1.2;
                p.phase = Math.random() * Math.PI * 2;
                p.hueShift = (p.gear - 2) * 20;
                p.x = p.gearCx + Math.cos(p.angle) * p.gearR;
                p.y = p.gearCy + Math.sin(p.angle) * p.gearR;
            },
            update(p) {
                p.angle += p.speed;
                // Tooth bump — particle radius wobbles with gear teeth
                const toothPhase = (p.angle * p.gearTeeth) % (Math.PI * 2);
                p.toothBump = Math.max(0, Math.sin(toothPhase)) * 0.15;
                const r = p.gearR * (1 + p.toothBump);
                p.x = p.gearCx + Math.cos(p.angle) * r;
                p.y = p.gearCy + Math.sin(p.angle) * r;
            },
            draw(p, ctx, accent, time) {
                const pulse = 0.5 + 0.5 * Math.sin(time * 1.5 + p.phase);
                const col = shiftAccent(accent, p.hueShift);
                const bump = p.toothBump || 0;

                const glowR = p.radius * (3 + bump * 6);
                const glow = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, glowR);
                glow.addColorStop(0, `rgba(${col}, ${0.1 + bump * 0.2 + pulse * 0.05})`);
                glow.addColorStop(1, 'rgba(0,0,0,0)');
                ctx.beginPath();
                ctx.arc(p.x, p.y, glowR, 0, Math.PI * 2);
                ctx.fillStyle = glow;
                ctx.fill();

                ctx.beginPath();
                ctx.arc(p.x, p.y, p.radius * (1 + bump * 0.5), 0, Math.PI * 2);
                ctx.fillStyle = `rgba(${col}, ${0.2 + bump * 0.35 + pulse * 0.1})`;
                ctx.fill();
            },
            initExtras() {
                extras.gearDefs = [
                    { cx: 0.35, cy: 0.4, r: 0.16, teeth: 12, dir: 1 },
                    { cx: 0.62, cy: 0.38, r: 0.13, teeth: 10, dir: -1 },
                    { cx: 0.48, cy: 0.65, r: 0.10, teeth: 8, dir: 1 },
                    { cx: 0.25, cy: 0.68, r: 0.08, teeth: 6, dir: -1 },
                    { cx: 0.72, cy: 0.62, r: 0.09, teeth: 7, dir: 1 },
                ];
                extras.gearAngles = extras.gearDefs.map(() => 0);
            },
            drawGlobal(ctx, parts, accent, time, layerExtras) {
                const ex = layerExtras || extras;
                if (!ex.gearDefs) {
                    ex.gearDefs = [
                        { cx: 0.35, cy: 0.4, r: 0.16, teeth: 12, dir: 1 },
                        { cx: 0.62, cy: 0.38, r: 0.13, teeth: 10, dir: -1 },
                        { cx: 0.48, cy: 0.65, r: 0.10, teeth: 8, dir: 1 },
                        { cx: 0.25, cy: 0.68, r: 0.08, teeth: 6, dir: -1 },
                        { cx: 0.72, cy: 0.62, r: 0.09, teeth: 7, dir: 1 },
                    ];
                    ex.gearAngles = ex.gearDefs.map(() => 0);
                }
                const minDim = Math.min(canvas.width, canvas.height);

                // Draw each gear
                for (let g = 0; g < ex.gearDefs.length; g++) {
                    const gd = ex.gearDefs[g];
                    const gcx = gd.cx * canvas.width;
                    const gcy = gd.cy * canvas.height;
                    const gr = gd.r * minDim;
                    const col = shiftAccent(accent, (g - 2) * 20);

                    // Rotate gear angle
                    ex.gearAngles[g] += gd.dir * (0.003 + (1 / gd.teeth) * 0.008);
                    const rot = ex.gearAngles[g];

                    // Gear teeth outline
                    const toothH = gr * 0.15;
                    const toothW = (Math.PI * 2) / gd.teeth * 0.35;
                    ctx.beginPath();
                    for (let t = 0; t < gd.teeth; t++) {
                        const a = rot + (t / gd.teeth) * Math.PI * 2;
                        // Outer tooth corners
                        const a1 = a - toothW, a2 = a + toothW;
                        const outerR = gr + toothH;
                        // Valley between teeth
                        const va = a + (1 / gd.teeth) * Math.PI;

                        if (t === 0) {
                            ctx.moveTo(gcx + Math.cos(a1) * outerR, gcy + Math.sin(a1) * outerR);
                        }
                        ctx.lineTo(gcx + Math.cos(a2) * outerR, gcy + Math.sin(a2) * outerR);
                        // Down to rim
                        const nextA = rot + ((t + 1) / gd.teeth) * Math.PI * 2 - toothW;
                        ctx.lineTo(gcx + Math.cos(a2) * gr, gcy + Math.sin(a2) * gr);
                        ctx.lineTo(gcx + Math.cos(nextA) * gr, gcy + Math.sin(nextA) * gr);
                        ctx.lineTo(gcx + Math.cos(nextA) * outerR, gcy + Math.sin(nextA) * outerR);
                    }
                    ctx.closePath();
                    ctx.strokeStyle = `rgba(${col}, 0.06)`;
                    ctx.lineWidth = 0.8;
                    ctx.stroke();

                    // Inner ring
                    ctx.beginPath();
                    ctx.arc(gcx, gcy, gr * 0.4, 0, Math.PI * 2);
                    ctx.strokeStyle = `rgba(${col}, 0.05)`;
                    ctx.lineWidth = 0.8;
                    ctx.stroke();

                    // Spokes
                    for (let s = 0; s < 4; s++) {
                        const sa = rot + s * Math.PI * 0.5;
                        ctx.beginPath();
                        ctx.moveTo(gcx + Math.cos(sa) * gr * 0.15, gcy + Math.sin(sa) * gr * 0.15);
                        ctx.lineTo(gcx + Math.cos(sa) * gr * 0.4, gcy + Math.sin(sa) * gr * 0.4);
                        ctx.strokeStyle = `rgba(${col}, 0.04)`;
                        ctx.lineWidth = 1;
                        ctx.stroke();
                    }

                    // Center axle
                    const ag = ctx.createRadialGradient(gcx, gcy, 0, gcx, gcy, gr * 0.12);
                    ag.addColorStop(0, `rgba(${col}, 0.12)`);
                    ag.addColorStop(1, 'rgba(0,0,0,0)');
                    ctx.beginPath();
                    ctx.arc(gcx, gcy, gr * 0.12, 0, Math.PI * 2);
                    ctx.fillStyle = ag;
                    ctx.fill();

                    // Axle dot
                    ctx.beginPath();
                    ctx.arc(gcx, gcy, 2, 0, Math.PI * 2);
                    ctx.fillStyle = `rgba(${col}, 0.25)`;
                    ctx.fill();
                }
            }
        },

        // ── HELP — fireflies / lanterns illuminating the dark ──
        help: {
            count: 30,
            init(p, i) {
                p.x = Math.random() * canvas.width;
                p.y = Math.random() * canvas.height;
                p.vx = (Math.random() - 0.5) * 0.3;
                p.vy = (Math.random() - 0.5) * 0.3;
                p.phase = Math.random() * Math.PI * 2;
                p.hueShift = (Math.random() - 0.5) * 40;
                // Fireflies blink — each has its own rhythm
                p.blinkSpeed = 0.6 + Math.random() * 1.2;
                p.blinkOffset = Math.random() * Math.PI * 2;
                p.radius = 1.5 + Math.random() * 1.5;
                // Lanterns are bigger, steadier, brighter
                p.isLantern = i < 6;
                if (p.isLantern) {
                    p.radius = 3 + Math.random() * 2;
                    p.blinkSpeed = 0.2 + Math.random() * 0.3; // slower pulse
                    p.vx *= 0.4;
                    p.vy *= 0.4;
                }
                // Wander target — fireflies change direction
                p.wanderAngle = Math.random() * Math.PI * 2;
                p.wanderTimer = 0;
            },
            update(p) {
                // Wander — change direction periodically
                p.wanderTimer++;
                if (p.wanderTimer > 80 + Math.random() * 60) {
                    p.wanderTimer = 0;
                    p.wanderAngle += (Math.random() - 0.5) * 2;
                }
                const wander = p.isLantern ? 0.003 : 0.008;
                p.vx += Math.cos(p.wanderAngle) * wander;
                p.vy += Math.sin(p.wanderAngle) * wander;
                // Dampen
                p.vx *= 0.98;
                p.vy *= 0.98;
                p.x += p.vx;
                p.y += p.vy;
                // Soft bounds
                if (p.x < 30) p.wanderAngle = 0;
                if (p.x > canvas.width - 30) p.wanderAngle = Math.PI;
                if (p.y < 30) p.wanderAngle = Math.PI * 0.5;
                if (p.y > canvas.height - 30) p.wanderAngle = -Math.PI * 0.5;
                p.x = Math.max(10, Math.min(canvas.width - 10, p.x));
                p.y = Math.max(10, Math.min(canvas.height - 10, p.y));
            },
            draw(p, ctx, accent, time) {
                const col = shiftAccent(accent, p.hueShift);

                if (p.isLantern) {
                    // Lantern — large steady warm glow
                    const pulse = 0.7 + 0.3 * Math.sin(time * p.blinkSpeed + p.blinkOffset);

                    // Big light pool
                    const poolR = p.radius * 12;
                    const pool = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, poolR);
                    pool.addColorStop(0, `rgba(${col}, ${0.06 * pulse})`);
                    pool.addColorStop(0.3, `rgba(${col}, ${0.03 * pulse})`);
                    pool.addColorStop(1, 'rgba(0,0,0,0)');
                    ctx.beginPath();
                    ctx.arc(p.x, p.y, poolR, 0, Math.PI * 2);
                    ctx.fillStyle = pool;
                    ctx.fill();

                    // Bright core glow
                    const coreR = p.radius * 4;
                    const core = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, coreR);
                    core.addColorStop(0, `rgba(${col}, ${0.3 * pulse})`);
                    core.addColorStop(0.4, `rgba(${col}, ${0.12 * pulse})`);
                    core.addColorStop(1, 'rgba(0,0,0,0)');
                    ctx.beginPath();
                    ctx.arc(p.x, p.y, coreR, 0, Math.PI * 2);
                    ctx.fillStyle = core;
                    ctx.fill();

                    // Center bright point
                    ctx.beginPath();
                    ctx.arc(p.x, p.y, p.radius * 0.6, 0, Math.PI * 2);
                    ctx.fillStyle = `rgba(255, 255, 255, ${0.15 * pulse})`;
                    ctx.fill();

                    ctx.beginPath();
                    ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2);
                    ctx.fillStyle = `rgba(${col}, ${0.35 * pulse})`;
                    ctx.fill();
                } else {
                    // Firefly — blinks on and off
                    const raw = Math.sin(time * p.blinkSpeed + p.blinkOffset);
                    const blink = Math.pow(Math.max(0, raw), 3); // sharp on, quick off

                    if (blink > 0.01) {
                        // Light pool when lit
                        const poolR = p.radius * 8 * blink;
                        const pool = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, poolR);
                        pool.addColorStop(0, `rgba(${col}, ${0.12 * blink})`);
                        pool.addColorStop(0.5, `rgba(${col}, ${0.04 * blink})`);
                        pool.addColorStop(1, 'rgba(0,0,0,0)');
                        ctx.beginPath();
                        ctx.arc(p.x, p.y, poolR, 0, Math.PI * 2);
                        ctx.fillStyle = pool;
                        ctx.fill();

                        // Bright core
                        ctx.beginPath();
                        ctx.arc(p.x, p.y, p.radius * (0.5 + blink * 0.5), 0, Math.PI * 2);
                        ctx.fillStyle = `rgba(${col}, ${0.2 + blink * 0.5})`;
                        ctx.fill();

                        // White center flash
                        if (blink > 0.5) {
                            ctx.beginPath();
                            ctx.arc(p.x, p.y, p.radius * 0.3, 0, Math.PI * 2);
                            ctx.fillStyle = `rgba(255,255,255, ${(blink - 0.5) * 0.5})`;
                            ctx.fill();
                        }
                    } else {
                        // Barely visible when dark
                        ctx.beginPath();
                        ctx.arc(p.x, p.y, p.radius * 0.5, 0, Math.PI * 2);
                        ctx.fillStyle = `rgba(${col}, 0.04)`;
                        ctx.fill();
                    }
                }
            },
            initExtras() {
                extras.trails = {};
            },
            drawGlobal(ctx, parts, accent, time, layerExtras) {
                const ex = layerExtras || extras;
                if (!ex.trails) ex.trails = {};

                // Faint trails behind fireflies — short luminous paths
                for (let i = 0; i < parts.length; i++) {
                    const p = parts[i];
                    if (p.isLantern) continue;
                    const key = i;
                    if (!ex.trails[key]) ex.trails[key] = [];
                    ex.trails[key].push({ x: p.x, y: p.y });
                    if (ex.trails[key].length > 10) ex.trails[key].shift();

                    const raw = Math.sin(time * p.blinkSpeed + p.blinkOffset);
                    const blink = Math.pow(Math.max(0, raw), 3);
                    if (blink < 0.1) continue;

                    const trail = ex.trails[key];
                    const col = shiftAccent(accent, p.hueShift);
                    for (let t = 0; t < trail.length - 1; t++) {
                        const frac = t / trail.length;
                        ctx.beginPath();
                        ctx.moveTo(trail[t].x, trail[t].y);
                        ctx.lineTo(trail[t + 1].x, trail[t + 1].y);
                        ctx.strokeStyle = `rgba(${col}, ${frac * 0.06 * blink})`;
                        ctx.lineWidth = frac * 1.5;
                        ctx.lineCap = 'round';
                        ctx.stroke();
                    }
                }
            }
        },

        // ── NONE — fallback for pages without a preset ──
        none: {
            count: 0,
            init() {},
            update() {},
            draw() {}
        }
    };

    // ── Transition System ──
    // Phases: 'normal' | 'converge' | 'burst'
    // converge: old particles move toward center, globalDraw fades out
    // burst: new particles move from center to target positions
    // normal: standard preset behavior

    const TRANSITION_SPEED = 0.025; // 0→1 per frame, ~40 frames = ~0.67s

    let currentLayer = null;     // { preset, particles, extras, name }
    let transitionState = null;  // null or { phase, progress, oldParticles, newLayer, cx, cy }

    function initLayer(presetName) {
        const preset = PRESETS[presetName] || PRESETS.none;
        const parts = [];
        const layerExtras = {};
        resize();
        for (let i = 0; i < preset.count; i++) {
            const p = {};
            preset.init(p, i);
            parts.push(p);
        }
        if (preset.initExtras) {
            const saved = extras;
            extras = layerExtras;
            preset.initExtras();
            extras = saved;
        }
        return { preset, particles: parts, extras: layerExtras, name: presetName };
    }

    function loop() {
        animFrame = requestAnimationFrame(loop);

        const w = canvas.width, h = canvas.height;
        if (w === 0 || h === 0) { resize(); return; }

        ctx.clearRect(0, 0, w, h);
        const accent = getAccentRGB();
        const time = Date.now() * 0.001;
        frameCount++;

        const cx = w * 0.5, cy = h * 0.5;

        if (transitionState) {
            const ts = transitionState;
            ts.progress = Math.min(1, ts.progress + TRANSITION_SPEED);

            if (ts.phase === 'converge') {
                // Draw old particles converging to center
                const t = ts.progress;
                const ease = t * t; // ease-in
                const alpha = 1 - t * 0.5; // fade slightly

                ctx.globalAlpha = alpha;
                for (const p of ts.oldParticles) {
                    // Lerp from current position toward center
                    const dx = cx - p.ox;
                    const dy = cy - p.oy;
                    p.x = p.ox + dx * ease;
                    p.y = p.oy + dy * ease;
                }

                // Draw with old preset (no global effects during converge — faded)
                const oldPreset = ts.oldPreset;
                if (oldPreset) {
                    ctx.globalAlpha = alpha * (1 - ease * 0.8);
                    if (oldPreset.drawGlobal && ts.oldExtras) {
                        oldPreset.drawGlobal(ctx, ts.oldParticles, accent, time, ts.oldExtras);
                    }
                    ctx.globalAlpha = alpha;
                    for (const p of ts.oldParticles) {
                        oldPreset.draw(p, ctx, accent, time);
                    }
                }
                ctx.globalAlpha = 1;

                // Center glow builds as particles converge
                const cg = ctx.createRadialGradient(cx, cy, 0, cx, cy, 30 + ease * 20);
                cg.addColorStop(0, `rgba(${accent}, ${ease * 0.4})`);
                cg.addColorStop(0.5, `rgba(${accent}, ${ease * 0.15})`);
                cg.addColorStop(1, 'rgba(0,0,0,0)');
                ctx.beginPath();
                ctx.arc(cx, cy, 30 + ease * 20, 0, Math.PI * 2);
                ctx.fillStyle = cg;
                ctx.fill();

                if (ts.progress >= 1) {
                    // Switch to burst phase
                    ts.phase = 'burst';
                    ts.progress = 0;
                    // Store target positions for new particles
                    currentLayer = ts.newLayer;
                    for (const p of currentLayer.particles) {
                        p.tx = p.x;
                        p.ty = p.y;
                        p.x = cx;
                        p.y = cy;
                    }
                }

            } else if (ts.phase === 'burst') {
                const t = ts.progress;
                const ease = 1 - Math.pow(1 - t, 3); // ease-out

                // Center flash that fades
                const flashAlpha = Math.max(0, (1 - t) * 0.5);
                if (flashAlpha > 0.01) {
                    const fg = ctx.createRadialGradient(cx, cy, 0, cx, cy, 50);
                    fg.addColorStop(0, `rgba(${accent}, ${flashAlpha})`);
                    fg.addColorStop(0.4, `rgba(${accent}, ${flashAlpha * 0.3})`);
                    fg.addColorStop(1, 'rgba(0,0,0,0)');
                    ctx.beginPath();
                    ctx.arc(cx, cy, 50, 0, Math.PI * 2);
                    ctx.fillStyle = fg;
                    ctx.fill();
                }

                // Lerp new particles from center to target
                const layer = currentLayer;
                for (const p of layer.particles) {
                    p.x = cx + (p.tx - cx) * ease;
                    p.y = cy + (p.ty - cy) * ease;
                }

                // Draw new preset (global effects fade in)
                ctx.globalAlpha = ease;
                if (layer.preset.drawGlobal) {
                    layer.preset.drawGlobal(ctx, layer.particles, accent, time, layer.extras);
                }
                ctx.globalAlpha = 0.5 + ease * 0.5; // particles visible earlier
                for (const p of layer.particles) {
                    layer.preset.draw(p, ctx, accent, time);
                }
                ctx.globalAlpha = 1;

                if (ts.progress >= 1) {
                    // Restore true positions and enter normal
                    for (const p of layer.particles) {
                        p.x = p.tx;
                        p.y = p.ty;
                        delete p.tx;
                        delete p.ty;
                    }
                    transitionState = null;
                }
            }

        } else if (currentLayer && currentLayer.preset.count > 0) {
            // Normal rendering
            const layer = currentLayer;
            for (let i = 0; i < layer.particles.length; i++) {
                layer.preset.update(layer.particles[i], time, i);
            }
            if (layer.preset.drawGlobal) {
                layer.preset.drawGlobal(ctx, layer.particles, accent, time, layer.extras);
            }
            for (let i = 0; i < layer.particles.length; i++) {
                layer.preset.draw(layer.particles[i], ctx, accent, time);
            }
        } else {
            // Nothing to draw — stop loop
            cancelAnimationFrame(animFrame);
            animFrame = null;
            ctx.clearRect(0, 0, w, h);
            return;
        }
    }

    // ── Public API ──

    function setPreset(name) {
        // Already showing this preset? Skip.
        if (currentLayer && currentLayer.name === name) return;

        const newLayer = initLayer(name);
        const isNone = (PRESETS[name] || PRESETS.none).count === 0;

        if (currentLayer && currentLayer.preset.count > 0 && !isNone) {
            // Transition: converge old → burst new
            // Snapshot old particle positions
            const oldParticles = currentLayer.particles.map(p => ({ ...p, ox: p.x, oy: p.y }));
            transitionState = {
                phase: 'converge',
                progress: 0,
                oldParticles,
                oldPreset: currentLayer.preset,
                oldExtras: currentLayer.extras,
                newLayer
            };
        } else if (!isNone) {
            // No old layer (first load or from 'none') — burst from center
            currentLayer = newLayer;
            const cx = canvas.width * 0.5, cy = canvas.height * 0.5;
            for (const p of currentLayer.particles) {
                p.tx = p.x;
                p.ty = p.y;
                p.x = cx;
                p.y = cy;
            }
            transitionState = {
                phase: 'burst',
                progress: 0,
                oldParticles: [],
                oldPreset: null,
                oldExtras: null,
                newLayer: currentLayer
            };
        } else {
            // Switching to 'none' — just fade out
            if (currentLayer) {
                // Quick converge to center then stop
                const oldParticles = currentLayer.particles.map(p => ({ ...p, ox: p.x, oy: p.y }));
                transitionState = {
                    phase: 'converge',
                    progress: 0,
                    oldParticles,
                    oldPreset: currentLayer.preset,
                    oldExtras: currentLayer.extras,
                    newLayer: initLayer('none')
                };
            }
            currentLayer = null;
        }

        // Ensure loop is running
        if (!animFrame) loop();
    }

    function stop() {
        if (animFrame) {
            cancelAnimationFrame(animFrame);
            animFrame = null;
        }
        currentLayer = null;
        transitionState = null;
        ctx.clearRect(0, 0, canvas.width, canvas.height);
    }

    // Page ID → preset name mapping
    const PAGE_PRESETS = {
        dashboard: 'dashboard',
        sync: 'sync',
        downloads: 'search',
        discover: 'discover',
        artists: 'artists',
        automations: 'automations',
        library: 'library',
        import: 'import',
        settings: 'settings',
        help: 'help',
    };

    // Listen for page changes from script.js
    window.pageParticles = {
        setPage(pageId) {
            const presetName = PAGE_PRESETS[pageId] || 'none';
            setPreset(presetName);
        },
        stop
    };

    // Auto-start for initial page (respect particles toggle)
    requestAnimationFrame(() => {
        if (window._particlesEnabled === false) return;
        const activePage = document.querySelector('.page.active');
        if (activePage) {
            const pageId = activePage.id.replace('-page', '');
            window.pageParticles.setPage(pageId);
        }
    });

})();
