// SPDX-FileCopyrightText: 2026 fuddlesworth
// SPDX-License-Identifier: GPL-3.0-or-later

#version 450

layout(location = 0) in vec2 vTexCoord;
layout(location = 1) in float vMouseInfluence;
layout(location = 2) in float vDistortAmount;
layout(location = 3) in vec2 vDisplacement;
layout(location = 4) in vec2 vFragCoord;

layout(location = 0) out vec4 fragColor;

#include <common.glsl>
#include <audio.glsl>


/**
 * MAGNETIC FIELD - Mouse-reactive shader
 *
 * Creates a magnetic/gravitational field effect that reacts to mouse position.
 * The field lines bend toward the cursor, creating an interactive visual.
 *
 * Parameters are declared in metadata.json and read through the generated
 * p_<id> accessors. The customParams slot tables that used to sit here
 * listed 8 of the 19 parameters the pack now reads, and the slot framing
 * no longer matches how they are accessed.
 */

// Kept local rather than calling common.glsl's fbm(): the shared one rotates
// each octave and offsets by vec2(180.0) with a 0.55 gain, which is a
// different field from this pack's plain doubling at 0.5. The noise() this
// used to carry WAS byte-identical to noise2D() and is gone.
float fbm(vec2 p) {
    float f = 0.0;
    float amp = 0.5;
    for (int i = 0; i < 4; i++) {
        f += amp * noise2D(p);
        p *= 2.0;
        amp *= 0.5;
    }
    return f;
}

// Calculate field vector influenced by mouse
// polarityFlip: -1.0 to 1.0, where negative values reverse tangent direction
vec2 fieldVector(vec2 pos, vec2 mousePos, float strength, float time, float polarityFlip) {
    vec2 toMouse = mousePos - pos;
    float dist = length(toMouse);

    // Guard against zero-length vector at exact mouse position
    if (dist < 0.001) return vec2(0.0);

    // Magnetic field lines curve around the mouse
    // Polarity flip reverses the tangent direction on strong bass hits
    // polarityFlip smoothly transitions through ~0 during reversal; use mix
    // instead of sign() to avoid a zero tangent (which breaks normalize)
    float polarity = polarityFlip >= 0.0 ? max(polarityFlip, 0.001) : min(polarityFlip, -0.001);
    vec2 tangent = vec2(-toMouse.y, toMouse.x) * polarity;
    float influence = strength / (dist * dist + 0.01);
    influence = clamp(influence, 0.0, 2.0);

    // Turbulence from noise (baseline, no longer audio-driven here)
    float turbulence = fbm(pos * 3.0 + time * 0.5) * 0.3;

    // Combine radial and tangential components
    // During polarity transition (~0), radial component wobbles creating chaotic field
    float radialSign = mix(1.0, -0.5, smoothstep(0.3, 0.0, abs(polarityFlip)));
    vec2 field = normalize(tangent) * influence + normalize(toMouse) * influence * 0.3 * radialSign;
    field += vec2(cos(turbulence * 6.28), sin(turbulence * 6.28)) * 0.1;

    return field;
}

// Draw field lines
float fieldLines(vec2 pos, vec2 mousePos, float strength, float time, float speed, float polarityFlip) {
    float lines = 0.0;

    // Field vector is independent of loop iteration — compute once
    vec2 field = fieldVector(pos, mousePos, strength, time, polarityFlip);
    float angle = atan(field.y, field.x);

    // Multiple field line layers
    for (int i = 0; i < 3; i++) {
        float phase = float(i) * 2.094; // 2π/3

        // Create flowing field lines
        float flow = sin(angle * 8.0 + length(pos - mousePos) * 10.0 - time * speed * 2.0 + phase);
        flow = pow(abs(flow), 3.0);
        
        // Distance-based fade
        float dist = length(pos - mousePos);
        float fade = exp(-dist * 2.0);
        
        lines += flow * fade * 0.4;
    }
    
    return lines;
}

// Ripple effect from mouse
float mouseRipple(vec2 pos, vec2 mousePos, float time, float size) {
    float dist = length(pos - mousePos);
    float ripple = sin(dist * 30.0 / size - time * 5.0);
    ripple = pow(max(ripple, 0.0), 2.0);
    float fade = exp(-dist * 3.0);
    return ripple * fade;
}

// Field particles that orbit mouse
float fieldParticles(vec2 pos, vec2 mousePos, float time, float count, float pSize) {
    float particles = 0.0;
    
    for (float i = 0.0; i < 50.0; i++) {
        if (i >= count) break;
        
        float angle = i * 2.399 + time * (0.5 + hash21(vec2(i, 0.0)) * 0.5);
        float radius = 0.05 + hash21(vec2(i, 1.0)) * 0.15;
        float speed = 0.8 + hash21(vec2(i, 2.0)) * 0.4;
        
        // Orbit around mouse
        vec2 orbit = mousePos + vec2(cos(angle * speed), sin(angle * speed)) * radius;
        
        // Add some wobble
        orbit += vec2(sin(time * 3.0 + i), cos(time * 2.0 + i * 1.5)) * 0.02;
        
        float dist = length(pos - orbit);
        float particle = smoothstep(pSize * 0.015, 0.0, dist);
        
        // Glow
        particle += exp(-dist * 100.0 / pSize) * 0.3;
        
        particles += particle;
    }
    
    return particles;
}

// Energy distortion around mouse
float energyDistortion(vec2 pos, vec2 mousePos, float time, float amount) {
    vec2 toMouse = pos - mousePos;
    float dist = length(toMouse);
    
    // Distortion waves
    float wave1 = sin(dist * 40.0 - time * 4.0) * 0.5 + 0.5;
    float wave2 = sin(dist * 25.0 - time * 3.0 + 1.0) * 0.5 + 0.5;
    
    float distortion = wave1 * wave2;
    distortion *= exp(-dist * 5.0) * amount;
    
    return distortion;
}

vec4 renderMagneticZone(vec2 fragCoord, vec4 rect, vec4 fillColor, vec4 borderColor, vec4 params, bool isHighlighted,
                        float bass, float mids, float treble, float overall, bool hasAudio) {
    // Corner radius: logical px to device px, clamped to half the zone's smaller side.
    // Shared with the decoration side via zoneSdf() in shared/common.glsl.
    ZoneSDF zoneShape = zoneSdf(fragCoord, rect, params.x);
    float borderWidth = zoneBorderWidth(params.y);

    // Parameters
    float fieldStrength = p_fieldStrength >= 0.0 ? p_fieldStrength : 1.0;
    float waveSpeed = p_waveSpeed >= 0.0 ? p_waveSpeed : 1.5;
    float rippleSize = p_rippleSize >= 0.0 ? p_rippleSize : 0.5;
    float glowIntensity = p_glowIntensity >= 0.0 ? p_glowIntensity : 0.6;

    float particleCount = p_particleCount >= 0.0 ? p_particleCount : 30.0;
    float particleSize = p_particleSize >= 0.0 ? p_particleSize : 1.5;
    float distortionAmount = p_distortionAmount >= 0.0 ? p_distortionAmount : 0.4;
    float audioReactivity  = p_audioReactivity >= 0.0 ? p_audioReactivity : 1.0;
    float nebulaIntensity  = p_nebulaIntensity >= 0.0 ? p_nebulaIntensity : 0.15;
    float shockwaveStr     = p_shockwaveStrength >= 0.0 ? p_shockwaveStrength : 1.0;
    float fillOpacity      = p_fillOpacity >= 0.0 ? p_fillOpacity : 0.85;

    float arcThick = p_arcThickness >= 0.0 ? p_arcThickness : 0.006;
    float gridFreq = p_gridFrequency >= 0.0 ? p_gridFrequency : 20.0;

    // ── Identity-native audio: electromagnetic field behavior ─────

    // Bass = Field Polarity Reversal
    // Smooth polarity state: 1.0 = normal, crosses through 0 to -1.0 on bass hits
    // Uses a nonlinear curve so light bass doesn't flip, but strong bass snaps hard
    float bassThreshold = smoothstep(0.25, 0.7, bass);
    float polarityFlip = mix(1.0, -1.0, bassThreshold);

    // Bass shockwave ring expanding from mouse/center
    float shockwaveRadius = fract(iTime * 0.8 + bass * 0.5) * 1.2;
    float shockwaveStrength = bassThreshold * (1.0 - shockwaveRadius) * shockwaveStr;

    // Mids = EM interference band parameters
    float emBandIntensity = smoothstep(0.1, 0.5, mids);
    float emBandSpeed = 0.5 + mids * 3.0;

    // Treble = Corona discharge arc intensity
    float coronaIntensity = smoothstep(0.08, 0.5, treble);
    float coronaReach = zoneLen(20.0 + treble * 60.0); // how far arcs extend from edge (pixels)

    // Overall = Field density/visibility (NOT a simple multiplier)
    // Affects rendering thresholds and particle visibility
    // Clamped: audioReactivity maxes at 2.0, so this reached 2.2 and the mix()
    // below extrapolated fieldLineThreshold to -0.39, where the smoothstep
    // returns 1.0 for every input and the visibility gate it describes stops
    // gating anything.
    float fieldDensity = hasAudio ? clamp(0.4 + overall * 0.9 * audioReactivity, 0.0, 1.3) : 1.0;
    float fieldLineThreshold = mix(0.6, 0.15, fieldDensity); // lower = more lines visible

    vec2 center = zoneShape.center;  // already computed by zoneSdf()
    vec2 p = fragCoord - center;

    // Screen-space UV for continuous field across all zones
    vec2 globalUV = fragCoord / max(iResolution, vec2(1.0));
    vec2 mouseGlobal = iMouse.xy / max(iResolution, vec2(1.0));
    // Shared by the interior radial effects and the outer-glow bound below.
    float mouseDist = length(globalUV - mouseGlobal);

    float d = zoneShape.d;

    // Colors
    vec3 fieldColor = p_fieldColor.rgb;
    if (length(fieldColor) < 0.01) fieldColor = vec3(0.545, 0.361, 0.965);  // Purple #8b5cf6

    vec3 highlightColor = p_highlightColor.rgb;
    if (length(highlightColor) < 0.01) highlightColor = vec3(0.98, 0.8, 0.082);  // Yellow #facc15

    float vitality = zoneVitality(isHighlighted);
    fieldColor = vitalityDesaturate(fieldColor, vitality);
    highlightColor = vitalityDesaturate(highlightColor, vitality);
    fieldStrength *= vitalityScale(0.6, 1.5, vitality);
    glowIntensity *= vitalityScale(0.4, 1.3, vitality);
    particleCount *= vitalityScale(0.5, 1.0, vitality);
    distortionAmount *= vitalityScale(0.5, 1.0, vitality);

    // Derived AFTER the vitality scaling above, not before it. Computed early
    // this was a dead write: the vitality factor landed on particleCount, which
    // nothing read afterwards, so dormant zones never thinned their particle
    // field the way the surrounding three lines intend.
    float visibleParticleCount = particleCount * fieldDensity;

    float t = iTime * waveSpeed;

    vec4 result = vec4(0.0);

    if (d < 0.0) {
        // ── Warped noise nebula background ──────────────────────────
        vec2 nebulaUV = globalUV * 3.0;
        // Warp the UV with a flowing offset
        float warpTime = iTime * 0.15;
        vec2 warp = vec2(
            fbm(nebulaUV + vec2(warpTime, 0.0)),
            fbm(nebulaUV + vec2(0.0, warpTime + 3.7))
        );
        float n1 = fbm(nebulaUV + warp * 1.5);
        float n2 = fbm(nebulaUV * 1.5 + warp + vec2(5.2, 1.3));
        float nebula = n1 * 0.6 + n2 * 0.4;

        // Color the nebula with dim fieldColor and highlightColor blend
        vec3 nebulaColor = mix(fieldColor * 0.4, highlightColor * 0.3, n2);
        float nebBright = (0.06 + nebula * 0.09) * (nebulaIntensity / 0.15);
        // Field density modulates nebula brightness
        nebBright += (fieldDensity - 0.5) * 0.04;

        vec3 bg = vec3(0.02, 0.03, 0.06) + nebulaColor * nebBright;
        bg += fieldColor * vDistortAmount * 0.02;

        // Field lines emanating from mouse position (polarity flip from bass)
        // iTime, not t: t already carries waveSpeed, and fieldLines multiplies
        // its time by the speed argument again, so passing both squared the
        // knob (9x at the slider's max of 3, and slower than the rest of the
        // pack below 1).
        float lines = fieldLines(globalUV, mouseGlobal, fieldStrength * 0.5, iTime, waveSpeed, polarityFlip);
        // Field density controls line visibility threshold
        lines = smoothstep(fieldLineThreshold, fieldLineThreshold + 0.3, lines) * lines;

        // Mouse ripple effect
        float ripple = mouseRipple(globalUV, mouseGlobal, t, rippleSize);

        // Orbiting particles - visible count driven by overall energy
        float particles = fieldParticles(globalUV, mouseGlobal, t, visibleParticleCount, particleSize);

        // ── Energy connections between nearby particles ─────────────
        // Particle positions depend only on the index and the clock, never on
        // globalUV, so they are evaluated ONCE per fragment rather than once per
        // pair. The pair loop below is O(n²) at up to 300 iterations, and
        // re-deriving pj inside it cost ~325 position evaluations (each 3 hash21
        // plus 4 trig) where 25 do. This runs per decorated window per frame on
        // the compositor path, so the difference is real.
        vec2 particlePos[25];
        for (int pi_i = 0; pi_i < 25; pi_i++) {
            if (float(pi_i) >= visibleParticleCount) break;
            float fi = float(pi_i);
            float ai = fi * 2.399 + t * (0.5 + hash21(vec2(fi, 0.0)) * 0.5);
            float ri = 0.05 + hash21(vec2(fi, 1.0)) * 0.15;
            float si = 0.8 + hash21(vec2(fi, 2.0)) * 0.4;
            particlePos[pi_i] = mouseGlobal + vec2(cos(ai * si), sin(ai * si)) * ri
                              + vec2(sin(t * 3.0 + fi), cos(t * 2.0 + fi * 1.5)) * 0.02;
        }

        float connections = 0.0;
        for (int ii = 0; ii < 25; ii++) {
            if (float(ii) >= visibleParticleCount) break;
            vec2 pi = particlePos[ii];

            for (int jj = ii + 1; jj < 25; jj++) {
                if (float(jj) >= visibleParticleCount) break;
                vec2 pj = particlePos[jj];

                float pairDist = length(pi - pj);
                if (pairDist > 0.15) continue; // only connect nearby particles

                // Distance from fragment to the line segment pi->pj
                vec2 pa = globalUV - pi;
                vec2 ba = pj - pi;
                float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
                float lineDist = length(pa - ba * h);

                // Curved arc: offset perpendicular distance with a sine bulge
                float arcBulge = sin(h * PI) * pairDist * 0.3;
                lineDist = max(lineDist - arcBulge, 0.0);

                float arc = smoothstep(arcThick, 0.0, lineDist);
                float proximity = 1.0 - pairDist / 0.15;
                connections += arc * proximity * 0.07;
            }
        }

        // Energy distortion - enhanced by vertex displacement
        float distortion = energyDistortion(globalUV, mouseGlobal, t, distortionAmount);
        distortion += vDistortAmount * 0.3;

        // Central glow around mouse - enhanced by vertex influence
        float mouseGlow = exp(-mouseDist * 8.0) * glowIntensity;
        mouseGlow += vMouseInfluence * 0.2;

        // Pulsing aura
        float pulse = 0.5 + 0.5 * sin(t * 3.0);
        float aura = exp(-mouseDist * 4.0) * pulse * 0.3;

        // Deformation visualization
        vec3 deformColor = highlightColor * length(vDisplacement) * 15.0;

        // Combine effects - modulated by fieldDensity for overall intensity
        vec3 fx = vec3(0.0);
        fx += fieldColor * lines * 0.8 * fieldDensity;
        fx += fieldColor * ripple * 0.6;
        fx += fieldColor * particles * fieldDensity;
        fx += mix(fieldColor, highlightColor, 0.3) * connections * fieldDensity;
        fx += highlightColor * distortion * 0.5;
        fx += fieldColor * mouseGlow;
        fx += highlightColor * aura;
        fx += deformColor;

        // Subtle grid for depth - distorted by vertex displacement
        vec2 gridUV = globalUV + vDisplacement * 0.08;
        vec2 grid = abs(fract(gridUV * gridFreq) - 0.5);
        float gridLine = smoothstep(0.48, 0.5, max(grid.x, grid.y)) * 0.05;
        // max(), not a bare subtraction: mouseDist is a screen-space UV distance
        // reaching ~1.41 at opposite corners, so (1.0 - mouseDist) went negative
        // over the far third of the screen and SUBTRACTED the grid colour there.
        fx += fieldColor * gridLine * max(1.0 - mouseDist, 0.0) * fieldDensity;

        // Strain lines - show direction of pull
        float strainAngle = atan(vDisplacement.y, vDisplacement.x);
        float strainLines = sin(strainAngle * 8.0 + length(vDisplacement) * 100.0) * 0.5 + 0.5;
        strainLines *= length(vDisplacement) * 20.0;
        fx += highlightColor * strainLines * 0.3;

        // ── BASS: Polarity reversal shockwave ring ──────────────────
        if (hasAudio && shockwaveStrength > 0.01) {
            float shockDist = length(globalUV - mouseGlobal);
            // Ring at the expanding shockwave radius
            float ringWidth = 0.015 + bassThreshold * 0.02;
            float ring = exp(-pow(shockDist - shockwaveRadius * 0.4, 2.0) / (ringWidth * ringWidth));
            // Ring color: bright white-blue flash during polarity reversal
            vec3 shockColor = mix(fieldColor, vec3(0.7, 0.85, 1.0), 0.7);
            fx += shockColor * ring * shockwaveStrength * 1.5;
            // Secondary fainter ring trailing behind
            float ring2 = exp(-pow(shockDist - shockwaveRadius * 0.3, 2.0) / (ringWidth * ringWidth * 4.0));
            fx += shockColor * ring2 * shockwaveStrength * 0.4;
        }

        // ── MIDS: EM Interference Bands (field-aligned) ───────────────
        if (hasAudio && emBandIntensity > 0.01) {
            // Compute field direction at this fragment to align bands with field lines.
            // iTime, matching the fieldLines() call above. Passing `t` here (which
            // already carries waveSpeed) advanced this field's turbulence on a
            // different clock from the lines the bands are supposed to align to, so
            // at any waveSpeed other than 1 they aligned to a field nobody drew.
            vec2 localField = fieldVector(globalUV, mouseGlobal, fieldStrength * 0.5, iTime, polarityFlip);
            float fieldAngle = atan(localField.y, localField.x);
            // Project UV perpendicular to the field direction so bands run along field lines
            float bandCoord = globalUV.x * cos(fieldAngle) - globalUV.y * sin(fieldAngle);
            // Multiple band frequencies for EM interference look
            float sweep = iTime * emBandSpeed * 0.3;
            float band1 = sin(bandCoord * 25.0 - sweep) * 0.5 + 0.5;
            float band2 = sin(bandCoord * 40.0 - sweep * 1.7 + 1.5) * 0.5 + 0.5;
            float band3 = sin(bandCoord * 15.0 - sweep * 0.6 + 3.0) * 0.5 + 0.5;
            // Combine for interference pattern
            float emBand = band1 * band2 * 0.7 + band3 * 0.3;
            emBand = pow(emBand, 2.0); // sharpen the bands
            // Color the bands with the field's polarity color (no chromatic split)
            vec3 polarityColor = mix(fieldColor, highlightColor, smoothstep(-0.5, 0.5, polarityFlip) * 0.6);
            fx += polarityColor * emBand * emBandIntensity * 0.35;
        }

        // ── TREBLE: Corona Discharge Arcs at zone edges ─────────────
        float edgeDist = -d; // positive inside, largest at center
        if (hasAudio && coronaIntensity > 0.01) {
            float edgeProx = smoothstep(coronaReach, 0.0, edgeDist);
            if (edgeProx > 0.01) {
                // Angular coordinate in screen space for continuous arc pattern
                vec2 fromCenter = globalUV - 0.5;
                float angle = atan(fromCenter.y, fromCenter.x);

                // Multi-scale angular noise creates branching arc structure
                float arcNoise1 = angularNoise(angle, 3.0, iTime * 4.0) * 0.5;
                float arcNoise2 = angularNoise(angle, 8.0, iTime * 6.0 + 10.0) * 0.3;
                float arcNoise3 = angularNoise(angle, 20.0, iTime * 8.0 + 25.0) * 0.2;
                float arcPattern = arcNoise1 + arcNoise2 + arcNoise3;

                // Arcs are strongest near the edge, branch inward
                // The noise threshold determines which angles get arcs
                float arcThreshold = 0.55 - coronaIntensity * 0.25;
                float arc = smoothstep(arcThreshold, arcThreshold + 0.15, arcPattern);

                // Arc fades with distance from edge (brighter at edge, dims inward)
                float arcFade = pow(edgeProx, 1.5);
                arc *= arcFade;

                // Flickering intensity (electrical discharge is unstable)
                float flicker = 0.7 + 0.3 * noise2D(vec2(angle * 5.0 + iTime * 15.0, iTime * 10.0));
                arc *= flicker;

                // Corona color: bright electric blue-white core with purple fringe
                vec3 arcCore = vec3(0.75, 0.85, 1.0);
                vec3 arcFringe = fieldColor * 1.5;
                vec3 coronaColor = mix(arcFringe, arcCore, arc * 0.8);

                fx += coronaColor * arc * coronaIntensity * 0.8;
            }
        }

        // Vignette - reduced in areas of high deformation
        float vig = 1.0 - length(globalUV - 0.5) * (0.4 - vDistortAmount * 0.1);

        result.rgb = bg + fx * vig;
        // Light identity tint from the zone's configured fill colour, at the
        // sibling packs' weight. Without it this pack discarded the setting.
        result.rgb = zoneTint(result.rgb, fillColor, 0.35);
        // fillOpacity alone, matching the rest of the catalog. The distortion addend was
    // unbounded (vDistortAmount is length(displacement) * 50 from the vert) and
    // relied on clampFragColor to save it.
    result.a = fillOpacity;
    }

    // Border with energy effect - enhanced by vertex displacement
    // The distortion addend only widens a border that exists. Added
    // unconditionally it kept the width non-zero, so this was the one pack in
    // the catalog where setting the border width to 0 did not turn the border
    // off.
    float effectiveBorderWidth = borderWidth <= 0.0 ? 0.0 : borderWidth + vDistortAmount * zoneLen(2.0);
    float border = softBorder(d, effectiveBorderWidth);
    if (border > 0.0) {

        // Energy flowing along border - modulated by vertex influence
        float flow = sin(atan(p.y, p.x) * 8.0 + t * 4.0 + vMouseInfluence * 10.0) * 0.5 + 0.5;

        vec3 borderClr = mix(fieldColor, highlightColor, flow * 0.5 + vMouseInfluence * 0.3);
        // Fold in the zone's configured border colour, as the sibling packs do.
        borderClr = mix(borderClr, borderColor.rgb, 0.3);
        borderClr *= 0.8 + 0.2 * flow + vDistortAmount * 0.5;
        // Corona discharge bleeds into border during treble
        vec3 coronaBorderGlow = vec3(0.6, 0.75, 1.0) * coronaIntensity * 0.4;
        borderClr += coronaBorderGlow;

        result.rgb = mix(result.rgb, borderClr, (border * 0.9) * borderColor.a);
        result.a = max(result.a, border * borderColor.a);
    }

    // Outer glow influenced by mouse proximity and vertex deformation
    // Bound derived from the falloff rather than tuned independently, and both
    // hoisted so they cannot drift. They used to coincide when the cursor sat on
    // the zone (mouseInfluence -> 1), cutting the gradient at exp(-1) = 37% and
    // leaving a hard ring at a fixed radius.
    float mouseInfluence = exp(-mouseDist * 3.0);
    float glowSize = zoneLen(15.0 + mouseInfluence * 10.0 + vDistortAmount * 5.0);
    if (d > 0.0 && d < glowSize * 3.5) {
        float glow = exp(-d / glowSize) * (0.3 + mouseInfluence * 0.4 + vMouseInfluence * 0.2);
        result.rgb += fieldColor * glow * 0.4;
        result.a = max(result.a, glow * 0.5);
    }

    return result;
}

// ─── Custom Label Composite ───────────────────────────────────────

vec4 compositeMagneticLabels(vec4 color, vec2 fragCoord,
                             float bass, bool hasAudio) {
    vec2 uv = labelsUv(fragCoord);
    vec2 px = 1.0 / max(iResolution, vec2(1.0));
    vec4 labels = texture(uZoneLabels, uv);

    vec3 fieldColor = p_fieldColor.rgb;
    if (length(fieldColor) < 0.01) fieldColor = vec3(0.545, 0.361, 0.965);
    vec3 highlightColor = p_highlightColor.rgb;
    if (length(highlightColor) < 0.01) highlightColor = vec3(0.98, 0.8, 0.082);

    float labelGlowSpread = p_coronaSpread >= 0.0 ? p_coronaSpread : 3.5;
    float labelBrightness = p_fieldBright >= 0.0 ? p_fieldBright : 2.2;
    float labelAudioReact = p_arcReact >= 0.0 ? p_arcReact : 1.0;

    // Gaussian halo with EM field tint
    float halo = 0.0;
    for (int dy = -2; dy <= 2; dy++) {
        for (int dx = -2; dx <= 2; dx++) {
            float w = exp(-float(dx * dx + dy * dy) * 0.25);
            halo += texture(uZoneLabels, uv + vec2(float(dx), float(dy)) * px * labelGlowSpread).a * w;
        }
    }
    halo /= 16.0;

    if (halo > 0.003) {
        float haloEdge = halo * (1.0 - labels.a);

        // Corona arcs: angular noise creates spiky aura
        float angle = atan(uv.y - 0.5, uv.x - 0.5);
        float arc = angularNoise(angle, 8.0, iTime * 0.5);
        arc = pow(arc, 2.0) * 1.5;

        // Polarity shift: top/bottom of label gets different tint
        float polarity = sin(angle) * 0.5 + 0.5;
        vec3 coronaCol = mix(fieldColor, highlightColor, polarity);

        float arcBright = haloEdge * (0.4 + arc * 0.6) * (hasAudio ? 1.0 + bass * 0.2 * labelAudioReact : 1.0);
        color.rgb += coronaCol * arcBright;

        // Mouse proximity: labels near cursor glow brighter
        vec2 mouseUV = iMouse.xy / max(iResolution, vec2(1.0));
        float mouseDist = length(uv - mouseUV);
        float mouseBoost = exp(-mouseDist * 5.0) * 0.4;
        color.rgb += fieldColor * haloEdge * mouseBoost;

        color.a = max(color.a, haloEdge * 0.5);
    }

    // Core: amplified field view through the label
    if (labels.a > 0.01) {
        vec3 core = color.rgb * labelBrightness + fieldColor * 0.15;
        float bassPulse = hasAudio ? 1.0 + bass * 0.6 : 1.0;
        core *= bassPulse;
        color.rgb = mix(color.rgb, core, labels.a);
        color.a = max(color.a, labels.a);
    }

    return color;
}

void main() {
    vec2 fragCoord = vFragCoord;
    vec4 color = vec4(0.0);
    
    if (zoneCount == 0) {
        fragColor = vec4(0.0);
        return;
    }
    
    // Audio analysis (computed once for all zones)
    bool  hasAudio = iAudioSpectrumSize > 0;
    float bass    = getBassSoft();
    float mids    = getMidsSoft();
    float treble  = getTrebleSoft();
    float overall = getOverallSoft();

    for (int i = 0; i < zoneCount && i < 64; i++) {
        vec4 rect = zoneRects[i];
        if (rect.z <= 0.0 || rect.w <= 0.0) continue;

        vec4 zoneColor = renderMagneticZone(fragCoord, rect, zoneFillColors[i],
            zoneBorderColors[i], zoneParams[i], zoneParams[i].z > 0.5,
            bass, mids, treble, overall, hasAudio);
        color = blendOver(color, zoneColor);
    }

    if (p_showLabels > 0.5)
        color = compositeMagneticLabels(color, fragCoord, bass, hasAudio);

    fragColor = clampFragColor(color);
}
