#include <SDL2/SDL.h>
#include <GL/glew.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <vector>
#include <cmath>
#include <cstdio>
#include <cstddef>
#include <algorithm>
#include <numeric>
#include <mutex>
#include <string>
#include "mp3_player.hpp"
#include "shredblob_live.hpp"

int windowWidth = 800, windowHeight = 600;
float globalTime = 0.0f;
float narrativeClock = 0.0f;

static const float DENDRITE_ONSET = 8.0f;
// Network emergence: one ramp (networkBlend 0→1), sub-stages as offsets on that curve.
static const float NET_E_SHELL_ON   = 0.30f;
static const float NET_E_SHELL_FULL = 0.48f;
static const float NET_E_CORE_ON    = 0.48f;
static const float NET_E_LEARN_ON   = 0.50f;
static const float NET_E_ELECT_ON   = 0.52f;
static const float NET_E_ELECT_FULL = 0.66f;
static bool networkCoresSeeded = false;
static const char* DEFAULT_MP3 = "contemporaneo-psy-matthsammcore-243522.mp3";

enum ShapeMode { BLOB = 0, THREADS = 1, SPIKED = 2 };
enum NeuronRole { ROLE_SOLO = 0, ROLE_UP_R, ROLE_UP_G, ROLE_UP_B, ROLE_MIDDLE, ROLE_DOWNSTREAM };

ShapeMode manualTarget = BLOB;
bool manualOverride = false;
glm::vec3 shapeBlend(1.0f, 0.0f, 0.0f);

float bassEnv = 0, midEnv = 0, trebleEnv = 0, beatAccent = 0;
float lowMidEnv = 0, highMidEnv = 0, energyEnv = 0, centroidEnv = 0;
float bassTarget = 0, midTarget = 0, trebleTarget = 0;
float lowMidTarget = 0, highMidTarget = 0, energyTarget = 0, centroidTarget = 0;
float prevBeatPhase = 0;
bool useMp3Audio = false;
static float gLastSyncedAudioPos = -1.0f;

glm::vec2 blobCenter(0.0f);
float blobDepth = 0.3f;
float blobScale = 1.35f;
float networkBlend = 0.0f;
bool networkViewActive = false;

float lightCharge = 0, dendriteGrowth = 0, lightningPulse = 0, lightExposure = 0;
float musicDrive = 0.0f;
float impulseTravel = 0.0f;
glm::vec3 networkShake(0.0f);
int learnedNodeCount = 3;
float learnCooldown = 0.0f;
static const int SHELL_NODE_COUNT = 48;
static const float NETWORK_SHELL_RADIUS = 7.2f;
float coreBlobReveal = 0.0f;
glm::vec3 beatMorphBlend(1.0f, 0.0f, 0.0f);
int beatMorphMode = 0;
float beatMorphRotKick = 0.0f;
float beatMorphCooldown = 0.0f;
bool beatMorphWasHigh = false;
enum CagePhase {
    CAGE_INTACT = 0, CAGE_STRUGGLE = 1, CAGE_BREAKING = 2,
    CAGE_FREED = 3, CAGE_TUNNEL = 4, CAGE_LAUNCH = 5
};
int cagePhase = CAGE_INTACT;
float cageStruggle = 0.0f;
float cageBreak = 0.0f;
float shellRupture = 0.0f;
float cubeReveal = 0.0f;
float cubeHalfExtent = 9.5f;
glm::vec3 corePos(0.0f);
glm::vec3 coreVel(0.0f);
glm::vec3 cagePushDir(1.0f, 0.0f, 0.0f);
float cagePushAmt = 0.0f;
float cageStruggleTime = 0.0f;
float coreColorPhase = 0.0f;
float freedPhaseTime = 0.0f;
float tunnelPhaseTime = 0.0f;
float tunnelProgress = 0.0f;
float tunnelPathDist = 0.0f;
float tunnelSpeed = 0.0f;
float launchPhaseTime = 0.0f;
float launchProgress = 0.0f;
float starFieldReveal = 0.0f;
float tunnelVisualFade = 1.0f;
float tunnelDrawMorph = 0.0f;
float tunnelEntrySpeed = 0.0f;
float tunnelEntryBlend = 0.0f;
bool vacuumSuck = false;
glm::vec3 preTunnelPos(0.0f);
glm::vec3 preTunnelVel(0.0f);
float finaleWhiteFade = 0.0f;
float finaleFadeTime = 0.0f;
bool narrativeFinished = false;
bool liveControlActive = false;
struct StarPoint { glm::vec3 dir; float dist; float bright; float twinkle; };
std::vector<StarPoint> starField;
std::vector<glm::vec3> blobTrailPos;
std::vector<float> blobTrailAge;
float blobTrailTimer = 0.0f;
struct ImpactWave {
    glm::vec3 point;
    glm::vec3 normal;
    float age;
    float power;
    float contactRadius = 0.0f;
    float hitSpeed = 0.0f;
};
std::vector<ImpactWave> impactWaves;
std::vector<glm::vec3> shellExtraOff;
std::vector<glm::vec3> shellDodgeOff;
std::vector<glm::vec3> shellBurstVel;
std::vector<std::vector<int>> shellNeighbors;
int coreR = 0, coreG = 0, coreB = 0, coreM0 = 0, coreM1 = 0, coreD0 = 0, coreD1 = 0;

struct NeuronNode {
    glm::vec3 pos;
    glm::vec3 shellDir;
    NeuronRole role;
    glm::vec3 color;
    glm::vec3 signal;
    glm::vec3 displaySignal;
    float charge;
    float pulse;
    float flowPhase;
    float growth;
    int spawnOrder;
    bool active;
    float dendritePhase;
    float rotSpeed;
    glm::vec3 rotAxis;
    glm::vec3 drift;
    std::vector<int> inputs;
    std::vector<int> outputs;
};

std::vector<NeuronNode> network;
std::vector<glm::vec3> dendriteLines;
std::vector<glm::vec3> dendriteColors;
struct DendriteBatch {
    std::vector<glm::vec3> verts;
    std::vector<glm::vec3> cols;
    glm::mat4 model{1.0f};
};
std::vector<DendriteBatch> dendriteBatches;
DendriteBatch soloElectDendBatch;
std::vector<glm::vec3> linkLines;
std::vector<glm::vec3> linkColors;
std::vector<glm::vec3> boltLines;
std::vector<glm::vec3> boltColors;

int cascadeStep = 0;
float cascadeTimer = 0.0f;
float boltProgress = 0.0f;

struct ActiveBolt {
    int from, to;
    glm::vec3 color;
};
std::vector<ActiveBolt> activeBolts;

AudioAnalysis::MP3Player* audioPlayer = nullptr;
std::mutex audioMutex;

GLuint blobProgram = 0, lineProgram = 0, cubeProgram = 0, tunnelProgram = 0, starProgram = 0, fadeProgram = 0;
GLuint vao = 0, vbo = 0, ebo = 0;
GLuint dendVao = 0, dendVbo = 0, dendCbo = 0;
GLuint linkVao = 0, linkVbo = 0, linkCbo = 0;
GLuint boltVao = 0, boltVbo = 0, boltCbo = 0;
GLuint cubeVao = 0, cubeVbo = 0;
GLuint tunnelVao = 0, tunnelVbo = 0;
GLuint starVao = 0, starVbo = 0, starCbo = 0, starDbo = 0;
GLuint fadeVao = 0, fadeVbo = 0;
std::vector<glm::vec3> baseVertices;
std::vector<unsigned int> sphereIndices;
int sphereResolution = 40;

static float clamp01(float x) { return x < 0 ? 0 : (x > 1 ? 1 : x); }

static float networkNodeAppearEstimate();
static float soloElectrocuteBlend(float nodeAppear, bool applyShock);
static bool soloDrainPhaseActive(float electrocute, float soloFade);
static float drainLayerMix(float electrocute, float soloAlpha);

static float smootherstep(float e0, float e1, float x) {
    float t = clamp01((x - e0) / (e1 - e0));
    return t * t * t * (t * (t * 6.0f - 15.0f) + 10.0f);
}

static float networkEmergence() {
    return networkViewActive ? 1.0f : networkBlend;
}

static float networkEmergenceStage(float onset, float full) {
    return smootherstep(onset, full, networkEmergence());
}

static float networkShellStage() {
    return networkEmergenceStage(NET_E_SHELL_ON, NET_E_SHELL_FULL);
}

static float networkShellRetireFade() {
    if (networkViewActive) return 1.0f;
    if (cagePhase >= CAGE_TUNNEL || cagePhase == CAGE_LAUNCH) return 0.0f;
    if (vacuumSuck)
        return 1.0f - smootherstep(0.06f, 0.62f, tunnelEntryBlend);
    if (cagePhase == CAGE_FREED)
        return 1.0f - smootherstep(0.45f, 3.0f, freedPhaseTime);
    if (cagePhase == CAGE_BREAKING)
        return 1.0f - smootherstep(0.55f, 1.0f, cageBreak);
    return 1.0f;
}

static float networkElectrocuteStage() {
    return networkEmergenceStage(NET_E_ELECT_ON, NET_E_ELECT_FULL);
}

static float hashf(float x) {
    return fmodf(sinf(x * 127.1f) * 43758.5453f, 1.0f);
}

static glm::vec3 safeNorm(glm::vec3 v) {
    float l = glm::length(v);
    return l > 0.0001f ? v / l : glm::vec3(0, 1, 0);
}

// Pure RGB components of white light — not tinted
static glm::vec3 roleBaseColor(NeuronRole r) {
    switch (r) {
        case ROLE_UP_R: return gLive.colorFire;
        case ROLE_UP_G: return gLive.colorAmber;
        case ROLE_UP_B: return gLive.colorChrome;
        case ROLE_MIDDLE: return glm::vec3(0.18f, 0.12f, 0.10f);
        case ROLE_DOWNSTREAM: return glm::vec3(0.14f, 0.10f, 0.12f);
        default: return glm::vec3(0.55f, 0.42f, 0.38f);
    }
}

static glm::vec3 additiveCombine(const glm::vec3& a, const glm::vec3& b) {
    return glm::min(a + b, glm::vec3(1.0f));
}

static glm::vec3 additiveCombine3(const glm::vec3& a, const glm::vec3& b, const glm::vec3& c) {
    return glm::min(a + b + c, glm::vec3(1.0f));
}

// Subtract white (luminance), disperse remaining hue like a prism
static glm::vec3 prismSubtract(const glm::vec3& c) {
    float w = (c.r + c.g + c.b) / 3.0f;
    glm::vec3 minusWhite = glm::max(c - glm::vec3(w * 0.72f), glm::vec3(0.0f));
    glm::vec3 complement = glm::max(glm::vec3(1.0f) - c, glm::vec3(0.0f));
    return glm::clamp(glm::vec3(
        minusWhite.r * 1.15f + complement.b * 0.20f + 0.06f,
        minusWhite.g * 0.70f + complement.r * 0.10f,
        minusWhite.b * 1.35f + complement.g * 0.26f + 0.08f), glm::vec3(0.0f), glm::vec3(1.0f));
}

static glm::vec3 prismDendriteColor(const glm::vec3& signal, float branchAngle) {
    float spread = 0.35f;
    return glm::clamp(glm::vec3(
        signal.r * (1.0f + spread * sinf(branchAngle)),
        signal.g * (1.0f + spread * sinf(branchAngle + 2.094f)),
        signal.b * (1.0f + spread * sinf(branchAngle + 4.188f))), glm::vec3(0.0f), glm::vec3(1.0f));
}

static glm::vec3 neuronTint(const NeuronNode& n) {
    if (glm::length(n.signal) > 0.02f)
        return n.signal;
    return roleBaseColor(n.role);
}

static float neuronColorMix(const NeuronNode& n) {
    switch (n.role) {
        case ROLE_UP_R:
        case ROLE_UP_G:
        case ROLE_UP_B:
            return 0.78f + n.pulse * 0.18f;
        case ROLE_MIDDLE:
            return glm::length(n.signal) > 0.02f ? (0.82f + n.pulse * 0.15f) : 0.22f;
        case ROLE_DOWNSTREAM:
            return glm::length(n.signal) > 0.02f ? (0.78f + n.pulse * 0.12f) : 0.18f;
        default:
            return 0.0f;
    }
}

static float networkBlobScale() {
    float cover = network.empty() ? 0.0f
        : float(learnedNodeCount) / float(std::max(1, (int)network.size()));
    return glm::mix(0.58f, 0.40f, smootherstep(0.25f, 0.85f, cover));
}

static float coreBlobInnerScale() {
    float shellInner = NETWORK_SHELL_RADIUS - networkBlobScale() * 1.55f;
    shellInner = std::max(shellInner, 3.0f);
    return shellInner * 0.68f / 1.25f;
}

static glm::vec3 coreBlobWorldCenter() {
    if (cagePhase >= CAGE_TUNNEL) return corePos;
    return cagePhase == CAGE_FREED ? corePos : glm::vec3(0.0f);
}

static float coreLaunchScale() {
    if (cagePhase != CAGE_LAUNCH) return 1.0f;
    return glm::mix(1.0f, 0.022f, smootherstep(0.42f, 0.96f, launchProgress));
}

static float cubeMinBounceTime() {
    return std::max(0.5f, gLive.freedBounceDuration);
}

static float cubeVacuumFallbackTime() {
    return cubeMinBounceTime() + std::max(1.5f, gLive.freedBounceDuration * 0.35f);
}

static void beginVacuumSuck() {
    vacuumSuck = true;
    tunnelEntryBlend = 0.0f;
    tunnelPathDist = 0.0f;
    preTunnelPos = corePos;
    preTunnelVel = coreVel;
    tunnelEntrySpeed = std::max(glm::length(coreVel), 22.0f);
}

struct NarrativePhaseBounds {
    float network, freed, tunnel, launch, finale, end;
};

static NarrativePhaseBounds narrativePhaseBounds() {
    NarrativePhaseBounds b;
    b.end = gLive.narrativeDuration;
    b.finale = std::max(0.0f, b.end - gLive.finaleWhiteDuration);
    b.launch = std::max(0.0f, b.finale - gLive.launchDuration);
    b.tunnel = std::max(0.0f, b.launch - gLive.tunnelDuration);
    b.freed = std::max(0.0f, b.tunnel - gLive.freedBounceDuration);
    b.network = gLive.networkStart;
    return b;
}

static bool audioTimelineActive() {
    return useMp3Audio && audioPlayer != nullptr;
}

static void syncTimelineFromAudio() {
    if (!audioTimelineActive()) return;
    double pos = audioPlayer->get_position();
    double dur = audioPlayer->get_duration();
    float t = float(std::clamp(pos, 0.0, double(gLive.narrativeDuration)));
    narrativeClock = t;
    globalTime = t;
    if (!narrativeFinished && (pos >= dur - 0.03 || t >= gLive.narrativeDuration - 0.03f))
        narrativeFinished = true;
}

static float tunnelCoreVisualScale() {
    if (cagePhase == CAGE_TUNNEL) {
        float mid = glm::mix(1.0f, 0.68f, smootherstep(0.14f, 0.52f, tunnelProgress));
        float exit = glm::mix(1.0f, 0.62f, smootherstep(0.72f, 1.0f, tunnelProgress));
        return mid * exit;
    }
    if (cagePhase == CAGE_LAUNCH) {
        float grow = glm::mix(0.38f, 1.0f, smootherstep(0.0f, 0.42f, launchProgress));
        if (tunnelVisualFade > 0.18f)
            grow = std::min(grow, glm::mix(0.38f, grow, 1.0f - tunnelVisualFade));
        return grow;
    }
    if (vacuumSuck)
        return glm::mix(1.0f, 0.92f, tunnelEntryBlend);
    return 1.0f;
}

static float coreLaunchAlpha() {
    if (cagePhase != CAGE_LAUNCH) return 1.0f;
    return glm::mix(1.0f, 0.35f, smootherstep(0.70f, 1.0f, launchProgress));
}

struct ProtagonistDraw {
    float soloAlpha = 0.0f;
    float coreAlpha = 0.0f;
    float handoff = 0.0f;
    float vitality = 0.0f;
    float layerMix = 1.0f;
    glm::vec3 drawPos{0.0f};
    float soloScale = 1.0f;
};

static float drainLayerMix(float electrocute, float soloAlpha) {
    if (!soloDrainPhaseActive(electrocute, soloAlpha)) return 1.0f;
    return smootherstep(0.04f, 0.72f, coreBlobReveal);
}

static ProtagonistDraw computeProtagonistDraw(float zoomEase, float electrocute, float greyDrain) {
    ProtagonistDraw p;
    if (networkViewActive) return p;

    float zoomVit = 1.0f - zoomEase * 0.38f;
    float netStage = networkShellStage();
    p.handoff = netStage * smootherstep(0.18f, 0.74f, coreBlobReveal);

    p.vitality = (networkEmergence() < 0.02f)
        ? zoomVit
        : glm::mix(zoomVit, 1.0f - smootherstep(0.0f, 0.92f, coreBlobReveal), netStage);
    p.vitality = clamp01(p.vitality);

    p.soloAlpha = clamp01(p.vitality * (1.0f - p.handoff * 0.98f));
    p.coreAlpha = clamp01(coreBlobReveal * smootherstep(0.12f, 0.58f, coreBlobReveal)
                        * (0.12f + p.handoff * 0.88f));

    float drainHold = clamp01(greyDrain * 1.05f);
    if (drainHold > 0.04f && cagePhase <= CAGE_STRUGGLE) {
        p.handoff *= (1.0f - drainHold * 0.98f);
        p.soloAlpha = clamp01(std::max(p.soloAlpha, 0.94f * smootherstep(0.06f, 0.42f, drainHold)));
        p.coreAlpha *= (1.0f - drainHold * 0.98f);
    }

    p.layerMix = drainLayerMix(electrocute, p.soloAlpha);
    p.coreAlpha *= p.layerMix;

    if (cagePhase >= CAGE_BREAKING && cagePhase <= CAGE_LAUNCH) {
        p.handoff = 1.0f;
        p.soloAlpha = 0.0f;
        p.coreAlpha = clamp01(coreBlobReveal);
        p.layerMix = 1.0f;
        p.drawPos = coreBlobWorldCenter();
        p.soloScale = blobScale;
    } else {
        glm::vec3 soloPos(blobCenter.x, blobCenter.y, blobDepth);
        p.drawPos = glm::mix(soloPos, coreBlobWorldCenter(), p.handoff);
        p.soloScale = blobScale * (drainHold > 0.08f ? 1.0f : std::max(0.85f, p.soloAlpha));
    }
    return p;
}

static glm::vec3 nodeDrawSignal(const NeuronNode& n, float colorTheft) {
    if (colorTheft < 0.02f) return n.displaySignal;
    glm::vec3 base = roleBaseColor(n.role);
    glm::vec3 sig = glm::length(n.displaySignal) > 0.04f ? n.displaySignal : base * 0.35f;
    glm::vec3 stolen = base * (0.42f + colorTheft * 0.95f) + sig * (0.25f + n.pulse * 0.35f);
    return glm::mix(sig, stolen, colorTheft * 0.72f);
}

static float starBlobReachPulse() {
    if (cagePhase != CAGE_LAUNCH) return 0.0f;
    float rise = smootherstep(0.58f, 0.82f, launchProgress);
    float hold = 1.0f - smootherstep(0.96f, 1.0f, launchProgress);
    return rise * hold * 2.4f;
}

static float starSwirlAmount() {
    if (cagePhase != CAGE_LAUNCH) return 0.0f;
    return smootherstep(0.55f, 1.0f, launchProgress) * 2.8f * gLive.starSwirlGain;
}

static void networkDrawAppears(float netAppearBase, float& nodeAppear, float& linkAppear, float& scatterAmt) {
    float retire = networkShellRetireFade();
    float intact = netAppearBase * (1.0f - smootherstep(0.0f, 0.45f, shellRupture));
    scatterAmt = 0.0f;
    if (cagePhase >= CAGE_BREAKING && cagePhase <= CAGE_FREED)
        scatterAmt = smootherstep(0.08f, 0.55f, shellRupture) * retire;
    float scatterNodes = scatterAmt * 0.98f;
    if (scatterNodes > 0.15f && shellExtraOff.size() == network.size()) {
        float keep = 0.0f;
        for (size_t i = 0; i < network.size(); ++i) {
            float d = glm::length(shellExtraOff[i]);
            if (d < 55.0f) keep = std::max(keep, scatterNodes);
            else if (d < 120.0f) keep = std::max(keep, scatterNodes * (1.0f - (d - 55.0f) / 65.0f));
        }
        scatterNodes = keep;
    }
    nodeAppear = std::max(intact, scatterNodes) * retire;
    float linkIntact = intact * (1.0f - smootherstep(0.12f, 0.50f, shellRupture));
    linkAppear = std::max(linkIntact, scatterAmt * 0.88f) * retire;
    linkAppear *= (1.0f + networkBlend * 0.55f + musicDrive * 0.45f + beatAccent * 0.35f) * gLive.linkBrightness;
    if (cagePhase <= CAGE_STRUGGLE && shellRupture < 0.08f)
        nodeAppear = std::min(1.0f, nodeAppear * 1.22f);
}

static glm::mat4 coreBlobOrientMatrix(const glm::vec3& pos) {
    glm::mat4 model = glm::translate(glm::mat4(1.0f), pos);
    if (cagePhase >= CAGE_TUNNEL) {
        glm::vec3 fwd = safeNorm(coreVel);
        if (glm::length(fwd) > 0.01f) {
            glm::vec3 upRef(0.0f, 1.0f, 0.0f);
            glm::vec3 right = safeNorm(glm::cross(fwd, upRef));
            if (glm::length(right) < 0.01f) right = glm::vec3(1.0f, 0.0f, 0.0f);
            glm::vec3 up = safeNorm(glm::cross(right, fwd));
            glm::mat4 basis(1.0f);
            basis[0] = glm::vec4(right, 0.0f);
            basis[1] = glm::vec4(up, 0.0f);
            basis[2] = glm::vec4(-fwd, 0.0f);
            model = model * basis;
        }
    }
    return model;
}

static float coreBlobWorldRadius() {
    float swell = 1.0f + cagePushAmt * 0.12f;
    return coreBlobInnerScale() * coreBlobReveal * swell * 1.22f;
}

static glm::vec3 shellBaseWorldPos(const NeuronNode& n, int nodeIdx) {
    float spin = globalTime * (0.06f + musicDrive * 0.04f);
    glm::mat3 spinMat = glm::mat3(glm::rotate(glm::mat4(1), spin, glm::vec3(0, 1, 0)));
    float shellR = NETWORK_SHELL_RADIUS * (1.0f + cagePushAmt * 0.04f);
    glm::vec3 pos = spinMat * (n.shellDir * shellR);
    if (nodeIdx >= 0 && nodeIdx < (int)shellExtraOff.size())
        pos += shellExtraOff[nodeIdx];
    return pos;
}

static void updateShellDodge(float dt) {
    if (shellRupture > 0.88f || coreBlobReveal < 0.04f) {
        for (auto& d : shellDodgeOff) d *= expf(-dt * 8.0f);
        return;
    }
    if (shellDodgeOff.size() != network.size())
        shellDodgeOff.assign(network.size(), glm::vec3(0.0f));

    glm::vec3 coreC = coreBlobWorldCenter();
    float coreR = coreBlobWorldRadius();
    float nodeR = networkBlobScale() * 1.30f;
    float minDist = coreR + nodeR + 0.55f;

    for (size_t i = 0; i < network.size(); ++i) {
        if (!network[i].active || network[i].growth < 0.05f) {
            shellDodgeOff[i] *= expf(-dt * 10.0f);
            continue;
        }
        glm::vec3 nodePos = shellBaseWorldPos(network[i], (int)i) + shellDodgeOff[i];
        glm::vec3 toNode = nodePos - coreC;
        float dist = glm::length(toNode);
        glm::vec3 target(0.0f);
        if (dist < minDist && dist > 0.001f)
            target = safeNorm(toNode) * (minDist - dist) * 1.35f;
        shellDodgeOff[i] += (target - shellDodgeOff[i]) * std::min(1.0f, 10.0f * dt);
    }
}

static glm::vec3 dendriteColorFor(const NeuronNode& n) {
    glm::vec3 grey(0.46f, 0.50f, 0.58f);
    if (n.spawnOrder >= 7)
        return grey;
    glm::vec3 hint = roleBaseColor(n.role) * (0.22f + glm::length(n.displaySignal) * 0.10f);
    return glm::min(grey + hint, glm::vec3(0.62f));
}

static glm::vec3 fibonacciSphereDir(int i, int n) {
    const float ga = float(M_PI) * (3.0f - sqrtf(5.0f));
    float y = 1.0f - (2.0f * i + 1.0f) / float(n);
    float r = sqrtf(std::max(0.0f, 1.0f - y * y));
    float phi = ga * i;
    return glm::vec3(cosf(phi) * r, y, sinf(phi) * r);
}

static glm::vec3 shellWorldPos(const NeuronNode& n, int nodeIdx = -1) {
    glm::vec3 pos = shellBaseWorldPos(n, nodeIdx);
    if (nodeIdx >= 0 && nodeIdx < (int)shellDodgeOff.size())
        pos += shellDodgeOff[nodeIdx];
    return pos;
}

static void buildShellNeighborGraph() {
    shellNeighbors.assign(network.size(), {});
    const int K = 4;
    for (size_t i = 0; i < network.size(); ++i) {
        std::vector<std::pair<float, int>> dists;
        for (size_t j = 0; j < network.size(); ++j) {
            if (i == j) continue;
            float dot = glm::dot(network[i].shellDir, network[j].shellDir);
            dists.emplace_back(-dot, (int)j);
        }
        std::sort(dists.begin(), dists.end());
        for (int k = 0; k < K && k < (int)dists.size(); ++k)
            shellNeighbors[i].push_back(dists[k].second);
    }
}

struct BlobDeformParams {
    float t, nodeTime, narrativeT;
    glm::vec3 shapeBlend;
    float audioBass, audioMid, audioTreble, beatAccent;
};

static float blobDeformCpu(glm::vec3 pos, float t) {
    return 0.07f * sinf(t * 1.2f + pos.x * 6.0f + pos.y * 4.0f)
         + 0.055f * cosf(t * 1.7f + pos.y * 5.0f + pos.z * 3.0f)
         + 0.04f * sinf(t * 0.9f + pos.z * 7.0f);
}
static float threadDeformCpu(glm::vec3 pos, float t) {
    float fiber = sinf(t * 2.4f + pos.x * 9.0f) * cosf(t * 1.8f + pos.y * 6.0f);
    float axis = sinf(pos.x * 11.0f + pos.y * 7.0f + pos.z * 5.0f);
    return 0.12f + 0.40f * fabsf(fiber) * (0.45f + 0.55f * fabsf(axis));
}
static float spikeDeformCpu(glm::vec3 pos, float t) {
    float radial = pos.x * 10.0f + pos.y * 10.0f + pos.z * 8.0f;
    return 0.45f * powf(fabsf(sinf(t * 2.2f + radial)), 5.0f);
}
static float evalSurfaceIntensity(glm::vec3 unitPos, const BlobDeformParams& p) {
    float t = p.t + p.nodeTime;
    float breathe = 0.028f * sinf(t * 0.55f) + 0.018f * p.audioBass;
    float midWobble = 0.02f * p.audioMid * sinf(t * 1.1f + glm::dot(unitPos, glm::vec3(3, 5, 2)));
    float shimmer = 0.01f * p.audioTreble * sinf(t * 8.0f + glm::dot(unitPos, glm::vec3(12, 9, 11)));
    float base = 1.0f + breathe + midWobble + shimmer + p.beatAccent * 0.022f;
    float deform = p.shapeBlend.x * blobDeformCpu(unitPos, t)
                 + p.shapeBlend.y * threadDeformCpu(unitPos, t)
                 + p.shapeBlend.z * spikeDeformCpu(unitPos, t);
    float narrativePulse = 0.06f * sinf(p.narrativeT * 6.28318f) * (1.0f - p.narrativeT);
    deform *= 0.85f + 0.15f * sinf(t * 0.4f + p.narrativeT * 3.14159f);
    return std::max(0.72f, std::min(2.15f, base + deform + narrativePulse * p.shapeBlend.x));
}
static glm::vec3 deformedSurface(glm::vec3 unitPos, const BlobDeformParams& p) {
    return unitPos * evalSurfaceIntensity(unitPos, p);
}
static int nearestMeshVertex(glm::vec3 dir) {
    dir = safeNorm(dir);
    int best = 0;
    float bestDot = -1e9f;
    for (size_t i = 0; i < baseVertices.size(); i += 2) {
        float d = glm::dot(baseVertices[i], dir);
        if (d > bestDot) { bestDot = d; best = (int)i; }
    }
    return best;
}

static glm::vec3 xformPoint(const glm::mat4& m, glm::vec3 local) {
    return glm::vec3(m * glm::vec4(local, 1.0f));
}

static glm::mat4 neuronModelMatrix(const NeuronNode& n, float netAppear, float netScale, int nodeIdx = -1) {
    glm::vec3 shellPos = shellWorldPos(n, nodeIdx);
    glm::vec3 normal = safeNorm(shellPos);
    glm::vec3 tangentDrift = n.drift - normal * glm::dot(n.drift, normal);
    glm::vec3 shake = networkShake * (0.55f + n.pulse * 0.45f);
    glm::vec3 pos = shellPos + tangentDrift + shake;
    glm::mat4 model = glm::translate(glm::mat4(1), pos);
    float audioTilt = sinf(globalTime * (2.5f + n.rotSpeed * 3.0f) + n.dendritePhase)
                    * (0.08f + bassEnv * 0.10f + midEnv * 0.06f);
    model = glm::rotate(model, globalTime * n.rotSpeed + n.dendritePhase + midEnv * 0.08f, n.rotAxis);
    model = glm::rotate(model, audioTilt + sinf(globalTime * 0.19f + n.dendritePhase) * 0.10f, glm::vec3(1, 0, 0.25f));
    float pulseS = 1.0f + n.pulse * 0.12f + bassEnv * 0.05f * sinf(globalTime * 4.0f + n.dendritePhase);
    float grow = smootherstep(0.0f, 1.0f, n.growth);
    model = glm::scale(model, glm::vec3(netScale * netAppear * grow * pulseS));
    return model;
}

static glm::vec3 worldSurfaceToward(const glm::mat4& modelFrom, const glm::mat4& modelTo,
                                    const BlobDeformParams& pFrom, const BlobDeformParams& pTo) {
    (void)pTo;
    glm::vec3 cFrom = xformPoint(modelFrom, glm::vec3(0));
    glm::vec3 cTo = xformPoint(modelTo, glm::vec3(0));
    glm::vec3 wDir = safeNorm(cTo - cFrom);
    glm::mat3 inv = glm::inverse(glm::mat3(modelFrom));
    glm::vec3 unitDir = safeNorm(inv * wDir);
    int vi = nearestMeshVertex(unitDir);
    return xformPoint(modelFrom, deformedSurface(baseVertices[vi], pFrom));
}

static float networkZoomEase(float t) {
    return smootherstep(gLive.networkStart - 2.0f, gLive.networkStart + 6.0f, t);
}

static glm::vec3 modeUnitVector(int mode) {
    glm::vec3 v(0);
    v[mode % 3] = 1.0f;
    return v;
}

static glm::vec3 bigBlobShapeBlend() {
    if (manualOverride)
        return modeUnitVector(int(manualTarget));
    return beatMorphBlend;
}

void updateBeatMorph(float dt) {
    beatMorphCooldown -= dt;
    beatMorphRotKick *= expf(-dt * 3.8f);

    bool beatHigh = beatAccent > 0.40f || bassEnv > 0.68f;
    if (beatHigh && !beatMorphWasHigh && beatMorphCooldown <= 0.0f) {
        beatMorphMode = (beatMorphMode + 1) % 3;
        beatMorphCooldown = 0.18f;
        beatMorphRotKick = 0.55f;
    }
    beatMorphWasHigh = beatHigh;

    glm::vec3 target = modeUnitVector(beatMorphMode);
    float snap = 2.0f + beatAccent * 3.0f + beatMorphRotKick * 1.2f;
    beatMorphBlend += (target - beatMorphBlend) * std::min(1.0f, snap * dt);
    float len = glm::length(beatMorphBlend);
    if (len > 0.001f) beatMorphBlend /= len;
}

static void resetCageState() {
    cagePhase = CAGE_INTACT;
    cageStruggle = cageBreak = shellRupture = cubeReveal = cagePushAmt = 0.0f;
    cageStruggleTime = 0.0f;
    freedPhaseTime = tunnelPhaseTime = tunnelProgress = tunnelPathDist = tunnelSpeed = 0.0f;
    launchPhaseTime = launchProgress = starFieldReveal = 0.0f;
    tunnelVisualFade = 1.0f;
    tunnelDrawMorph = 0.0f;
    tunnelEntrySpeed = 0.0f;
    tunnelEntryBlend = 0.0f;
    vacuumSuck = false;
    preTunnelPos = preTunnelVel = glm::vec3(0.0f);
    finaleWhiteFade = finaleFadeTime = 0.0f;
    blobTrailPos.clear();
    blobTrailAge.clear();
    blobTrailTimer = 0.0f;
    corePos = coreVel = glm::vec3(0.0f);
    if (!shellDodgeOff.empty()) std::fill(shellDodgeOff.begin(), shellDodgeOff.end(), glm::vec3(0.0f));
    cagePushDir = glm::vec3(1.0f, 0.0f, 0.0f);
    impactWaves.clear();
    if (!shellExtraOff.empty()) std::fill(shellExtraOff.begin(), shellExtraOff.end(), glm::vec3(0.0f));
    shellBurstVel.clear();
}

static float tunnelBendAmount(float pathS) {
    float pathRamp = smootherstep(0.0f, 22.0f, pathS);
    float timeRamp = (cagePhase >= CAGE_TUNNEL) ? smootherstep(0.04f, 0.40f, tunnelProgress) : 0.0f;
    float blend = std::max(timeRamp, pathRamp);
    return blend * 5.8f;
}

static glm::vec3 tunnelPathPoint(float s, float bendAmt) {
    float entranceZ = -cubeHalfExtent;
    float bx = sinf(s * 0.0075f) * bendAmt + sinf(s * 0.012f + 1.1f) * bendAmt * 0.40f;
    float by = sinf(s * 0.0065f + 0.7f) * bendAmt * 0.62f + cosf(s * 0.0095f + 0.4f) * bendAmt * 0.36f;
    float bz = sinf(s * 0.0055f + 2.0f) * bendAmt * 0.10f;
    return glm::vec3(bx, by, entranceZ - s + bz);
}

static glm::vec3 tunnelPathTangent(float s, float bendAmt) {
    return safeNorm(tunnelPathPoint(s + 0.45f, bendAmt)
                  - tunnelPathPoint(s - 0.45f, bendAmt));
}

/** Path distance along tunnel curve for a given phase progress (0–1). */
static float tunnelPathDistForProgress(float progress) {
    progress = clamp01(progress);
    float launchGate = 145.0f + 35.0f * progress;
    return launchGate * smootherstep(0.0f, 1.0f, progress);
}

static void tunnelFrameAt(float s,
                          glm::vec3& pos, glm::vec3& tangent, glm::vec3& normal, glm::vec3& binormal) {
    float bend = tunnelBendAmount(s);
    pos = tunnelPathPoint(s, bend);
    tangent = tunnelPathTangent(s, bend);
    glm::vec3 upRef(0.0f, 1.0f, 0.0f);
    normal = safeNorm(glm::cross(upRef, tangent));
    if (glm::length(normal) < 0.01f) normal = glm::vec3(1.0f, 0.0f, 0.0f);
    binormal = glm::cross(tangent, normal);
    float twistRamp = smootherstep(4.0f, 40.0f, s);
    float twist = s * 0.0065f + sinf(s * 0.0095f + 0.85f) * 0.16f * twistRamp;
    float ct = cosf(twist), st = sinf(twist);
    glm::vec3 n2 = normal * ct + binormal * st;
    binormal = safeNorm(glm::cross(tangent, n2));
    normal = n2;
}

static float tunnelTubeRadius() {
    float coreR = coreBlobInnerScale() * coreBlobReveal * (0.92f + 0.05f * bassEnv)
                * tunnelCoreVisualScale() * 2.05f;
    float clearance = coreR + 3.2f;
    float base = cubeHalfExtent * 0.82f;
    float pathS = tunnelPathDist;
    float bend = tunnelBendAmount(pathS);
    base *= 1.0f + 0.06f * smootherstep(4.0f, 11.5f, bend);
    return std::max(base, clearance * 1.22f);
}

static void updateBlobTrail(float dt) {
    if (cagePhase < CAGE_TUNNEL) return;
    blobTrailTimer += dt;
    const float interval = 0.032f;
    while (blobTrailTimer >= interval) {
        blobTrailTimer -= interval;
        blobTrailPos.insert(blobTrailPos.begin(), corePos);
        blobTrailAge.insert(blobTrailAge.begin(), 0.0f);
        if (blobTrailPos.size() > 52) {
            blobTrailPos.pop_back();
            blobTrailAge.pop_back();
        }
    }
    for (size_t i = 0; i < blobTrailAge.size(); ++i)
        blobTrailAge[i] += dt;
    while (!blobTrailAge.empty() && blobTrailAge.back() > 1.35f) {
        blobTrailPos.pop_back();
        blobTrailAge.pop_back();
    }
}

static void initStarField() {
    starField.clear();
    starField.reserve(26000);
    for (int i = 0; i < 26000; ++i) {
        float u = hashf(float(i) * 1.73f);
        float v = hashf(float(i) * 2.41f);
        float theta = u * 6.28318f;
        float phi = acosf(2.0f * v - 1.0f);
        glm::vec3 dir(cosf(theta) * sinf(phi), sinf(theta) * sinf(phi), cosf(phi));
        StarPoint sp;
        sp.dir = dir;
        sp.dist = 750.0f + hashf(float(i) * 4.91f) * 2250.0f;
        sp.bright = 0.30f + hashf(float(i) * 5.33f) * 0.70f;
        sp.twinkle = hashf(float(i) * 6.77f) * 6.28318f;
        starField.push_back(sp);
    }
    std::vector<glm::vec3> starDirs;
    std::vector<float> starDists;
    std::vector<glm::vec3> starCol;
    starDirs.reserve(starField.size());
    starDists.reserve(starField.size());
    starCol.reserve(starField.size());
    for (const auto& s : starField) {
        starDirs.push_back(s.dir);
        starDists.push_back(s.dist);
        float b = s.bright;
        float tint = hashf(s.twinkle);
        glm::vec3 white = glm::mix(glm::vec3(0.94f, 0.95f, 1.0f), gLive.starWarm, 0.15f);
        starCol.push_back(white * (0.82f + b * 0.38f) * (0.96f + tint * 0.06f));
    }
    glBindVertexArray(starVao);
    glBindBuffer(GL_ARRAY_BUFFER, starVbo);
    glBufferData(GL_ARRAY_BUFFER, starDirs.size() * sizeof(glm::vec3), starDirs.data(), GL_STATIC_DRAW);
    glBindBuffer(GL_ARRAY_BUFFER, starDbo);
    glBufferData(GL_ARRAY_BUFFER, starDists.size() * sizeof(float), starDists.data(), GL_STATIC_DRAW);
    glBindBuffer(GL_ARRAY_BUFFER, starCbo);
    glBufferData(GL_ARRAY_BUFFER, starCol.size() * sizeof(glm::vec3), starCol.data(), GL_STATIC_DRAW);
    glBindBuffer(GL_ARRAY_BUFFER, starVbo);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(glm::vec3), (void*)0);
    glEnableVertexAttribArray(0);
    glBindBuffer(GL_ARRAY_BUFFER, starCbo);
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(glm::vec3), (void*)0);
    glEnableVertexAttribArray(1);
    glBindBuffer(GL_ARRAY_BUFFER, starDbo);
    glVertexAttribPointer(2, 1, GL_FLOAT, GL_FALSE, sizeof(float), (void*)0);
    glEnableVertexAttribArray(2);
    glBindVertexArray(0);
}

static void spawnImpactWave(const glm::vec3& point, const glm::vec3& normal, float power,
                            float contactRadius = 0.0f, float hitSpeed = 0.0f) {
    ImpactWave w;
    w.point = point;
    w.normal = safeNorm(normal);
    w.age = 0.0f;
    w.power = power;
    w.contactRadius = contactRadius;
    w.hitSpeed = hitSpeed;
    impactWaves.push_back(w);
    if (impactWaves.size() > 24) impactWaves.erase(impactWaves.begin());
    lightningPulse = std::max(lightningPulse, std::min(0.85f, power * 0.45f));
}

static glm::vec3 cubeWallContactPoint(int axis, float wallCoord, const glm::vec3& blobCenter) {
    glm::vec3 p = blobCenter;
    p[axis] = wallCoord;
    return p;
}

static glm::vec3 cubeWallInwardNormal(int axis, float wallCoord) {
    glm::vec3 n(0.0f);
    n[axis] = wallCoord >= 0.0f ? -1.0f : 1.0f;
    return n;
}

static void spawnCubeWallImpact(int axis, float wallCoord, const glm::vec3& blobCenter,
                                float contactRadius, float hitSpeed, float powerScale = 1.0f) {
    float power = std::min(2.5f, hitSpeed * 0.06f + 0.55f) * powerScale;
    spawnImpactWave(cubeWallContactPoint(axis, wallCoord, blobCenter),
                    cubeWallInwardNormal(axis, wallCoord),
                    power, contactRadius, hitSpeed);
}

static float coreBlobRadiusEstimate() {
    return coreBlobInnerScale() * coreBlobReveal * 1.05f;
}

static float coreCollisionRadius() {
    return coreBlobInnerScale() * coreBlobReveal * 0.82f;
}

static void initShellBurst();

static void enterFreedFromTimeline(float t) {
    if (cagePhase >= CAGE_FREED) return;
    initShellBurst();
    cagePhase = CAGE_FREED;
    cageStruggle = cageBreak = 1.0f;
    shellRupture = 1.0f;
    cubeHalfExtent = NETWORK_SHELL_RADIUS * 1.38f;
    corePos = glm::vec3(0.0f);
    glm::vec3 launch(
        hashf(t * 3.1f) - 0.5f,
        hashf(t * 3.1f + 2.0f) - 0.5f,
        hashf(t * 3.1f + 4.0f) - 0.5f);
    coreVel = safeNorm(launch) * 11.0f + launch * 4.0f;
    beatMorphRotKick = 0.55f;
    vacuumSuck = false;
    tunnelEntryBlend = 0.0f;
}

static void updateFreedCubePhysics(float dt) {
    cubeReveal = std::min(1.0f, cubeReveal + dt * 1.4f);
    float coreR = coreCollisionRadius();

    if (vacuumSuck) {
        tunnelEntryBlend = std::min(1.0f, tunnelEntryBlend + dt * 1.05f);
        float tv = smootherstep(0.0f, 1.0f, tunnelEntryBlend);
        float pullSpd = tunnelEntrySpeed + tv * (38.0f + tunnelEntrySpeed * 0.42f);
        tunnelPathDist += pullSpd * dt;
        float pathS = tunnelPathDist;
        float pathBend = tunnelBendAmount(pathS);
        glm::vec3 onPath = tunnelPathPoint(pathS, pathBend);
        glm::vec3 tangent = tunnelPathTangent(pathS, pathBend);
        if (tv < 0.20f)
            corePos = glm::mix(preTunnelPos, onPath, smootherstep(0.04f, 0.20f, tv));
        else
            corePos = onPath;
        coreVel = tangent * pullSpd;
        cubeReveal = std::max(0.0f, cubeReveal - dt * 2.8f);
        tunnelVisualFade = std::max(tunnelVisualFade, smootherstep(0.04f, 0.55f, tv));
        tunnelDrawMorph = std::max(tunnelDrawMorph, smootherstep(0.04f, 0.50f, tv));
        starFieldReveal = std::max(starFieldReveal, smootherstep(0.18f, 0.75f, tv) * 0.42f);
        return;
    }

    if (beatAccent > 0.38f) {
        glm::vec3 kick(
            (hashf(globalTime * 8.1f) - 0.5f) * beatAccent * 5.5f,
            (hashf(globalTime * 8.1f + 1.7f) - 0.5f) * beatAccent * 5.5f,
            (hashf(globalTime * 8.1f + 3.3f) - 0.5f) * beatAccent * 4.5f);
        coreVel += kick;
    }

    glm::vec3 chaos(
        sinf(globalTime * 2.4f) * midEnv * 14.0f + sinf(globalTime * 5.1f) * musicDrive * 8.0f,
        cosf(globalTime * 1.8f) * bassEnv * 14.0f + cosf(globalTime * 4.7f) * musicDrive * 8.0f,
        sinf(globalTime * 2.9f) * trebleEnv * 11.0f + sinf(globalTime * 6.3f) * beatAccent * 6.0f);
    coreVel += chaos * dt * 2.8f;
    coreVel *= expf(-dt * 0.28f);

    float spd = glm::length(coreVel);
    if (spd < 6.5f)
        coreVel = (spd > 0.01f ? coreVel / spd : safeNorm(chaos)) * 6.5f;
    else if (spd > 24.0f)
        coreVel = coreVel / spd * 24.0f;

    corePos += coreVel * dt;

    auto bounceAxis = [&](int ax) {
        float limit = cubeHalfExtent - coreR;
        if (limit < 1.2f) limit = 1.2f;
        if (corePos[ax] > limit) {
            float hitSpeed = glm::length(coreVel);
            glm::vec3 hitPos = corePos;
            corePos[ax] = limit;
            if (coreVel[ax] > 0.0f) {
                coreVel[ax] *= -0.88f;
                int o1 = (ax + 1) % 3, o2 = (ax + 2) % 3;
                coreVel[o1] += (hashf(globalTime + ax * 1.3f) - 0.5f) * 4.5f;
                coreVel[o2] += (hashf(globalTime + ax * 2.7f) - 0.5f) * 4.5f;
                spawnCubeWallImpact(ax, cubeHalfExtent, hitPos, coreR, hitSpeed);
            }
        } else if (corePos[ax] < -limit) {
            float hitSpeed = glm::length(coreVel);
            glm::vec3 hitPos = corePos;
            corePos[ax] = -limit;
            if (coreVel[ax] < 0.0f) {
                coreVel[ax] *= -0.88f;
                int o1 = (ax + 1) % 3, o2 = (ax + 2) % 3;
                coreVel[o1] += (hashf(globalTime + ax * 1.9f) - 0.5f) * 4.5f;
                coreVel[o2] += (hashf(globalTime + ax * 3.1f) - 0.5f) * 4.5f;
                spawnCubeWallImpact(ax, -cubeHalfExtent, hitPos, coreR, hitSpeed);
            }
        }
    };
    bounceAxis(0);
    bounceAxis(1);
    bounceAxis(2);
}

static void updateCageEscapeAudioSynced(float dt) {
    NarrativePhaseBounds b = narrativePhaseBounds();
    float t = narrativeClock;
    float net = networkViewActive ? 1.0f : networkBlend;
    if (net < 0.30f || coreBlobReveal < 0.45f || t < b.network) return;

    cagePushDir = safeNorm(glm::vec3(
        sinf(globalTime * 0.73f) * 0.65f + sinf(globalTime * 1.13f + 1.2f) * 0.35f,
        cosf(globalTime * 0.61f) * 0.55f + sinf(globalTime * 0.97f) * 0.40f,
        sinf(globalTime * 0.83f) * 0.50f + cosf(globalTime * 1.07f) * 0.35f));

    float preFreed = std::max(0.001f, b.freed - b.network);
    float escapeU = clamp01((t - b.network) / preFreed);

    if (t < b.freed) {
        if (escapeU < 0.30f) {
            cagePhase = CAGE_INTACT;
        } else if (escapeU < 0.58f) {
            cagePhase = CAGE_STRUGGLE;
            cageStruggle = smootherstep(0.30f, 0.58f, escapeU);
            cageStruggleTime = (escapeU - 0.30f) * preFreed;
            cagePushAmt = cageStruggle * (0.50f + 0.40f * (0.5f + 0.5f * sinf(globalTime * 4.2f)));
            networkShake += cagePushDir * cagePushAmt * 0.28f;
            if (shellExtraOff.size() != network.size())
                shellExtraOff.assign(network.size(), glm::vec3(0.0f));
            for (size_t i = 0; i < network.size(); ++i) {
                glm::vec3 sp = shellBaseWorldPos(network[i], (int)i);
                float align = std::max(0.0f, glm::dot(safeNorm(sp), cagePushDir));
                shellExtraOff[i] = safeNorm(sp) * cagePushAmt * align * 0.22f;
            }
        } else if (escapeU < 0.82f) {
            if (cagePhase < CAGE_BREAKING) {
                initShellBurst();
                lightningPulse = std::max(lightningPulse, 0.90f);
            }
            cagePhase = CAGE_BREAKING;
            cageStruggle = 1.0f;
            cageBreak = smootherstep(0.58f, 0.82f, escapeU);
            shellRupture = smootherstep(0.0f, 1.0f, cageBreak);
            cagePushAmt = (1.0f - cageBreak) * 1.15f;
            if (shellExtraOff.size() == network.size() && shellBurstVel.size() == network.size()) {
                for (size_t i = 0; i < network.size(); ++i)
                    shellExtraOff[i] = shellBurstVel[i] * cageBreak * 2.8f;
            }
        } else {
            enterFreedFromTimeline(t);
            cubeReveal = std::max(cubeReveal, smootherstep(0.82f, 0.95f, escapeU));
            freedPhaseTime = std::max(0.0f, (escapeU - 0.82f) * preFreed);
        }
    }

    if (t >= b.freed && t < b.tunnel) {
        enterFreedFromTimeline(t);
        freedPhaseTime = t - b.freed;
        updateFreedCubePhysics(dt);
        float vacuumAt = b.tunnel - std::min(1.1f, gLive.freedBounceDuration * 0.22f);
        if (!vacuumSuck && t >= vacuumAt && cubeReveal > 0.35f)
            beginVacuumSuck();
    }

    if (t >= b.tunnel && t < b.launch) {
        if (cagePhase < CAGE_TUNNEL) {
            cagePhase = CAGE_TUNNEL;
            vacuumSuck = false;
            tunnelDrawMorph = 1.0f;
            tunnelVisualFade = 1.0f;
            blobTrailPos.clear();
            blobTrailAge.clear();
            blobTrailTimer = 0.0f;
            tunnelPhaseTime = t - b.tunnel;
            tunnelProgress = clamp01(tunnelPhaseTime / std::max(0.001f, gLive.tunnelDuration));
            tunnelPathDist = std::max(tunnelPathDist, tunnelPathDistForProgress(tunnelProgress));
            tunnelSpeed = std::max({tunnelSpeed, tunnelEntrySpeed, 28.0f});
        }
        tunnelPhaseTime = t - b.tunnel;
        tunnelProgress = clamp01(tunnelPhaseTime / std::max(0.001f, gLive.tunnelDuration));
        float stepDt = std::max(dt, 1.0f / 120.0f);
        float pathBend = tunnelBendAmount(tunnelPathDist);
        float accel = (18.0f + tunnelProgress * 58.0f + beatAccent * 16.0f + bassEnv * 10.0f)
            * gLive.tunnelSpeedGain;
        accel *= 1.0f + smootherstep(0.0f, 0.10f, tunnelProgress) * 0.65f;
        tunnelSpeed += accel * stepDt;
        if (tunnelEntrySpeed > 0.0f)
            tunnelSpeed = std::max(tunnelSpeed, tunnelEntrySpeed);
        tunnelPathDist += tunnelSpeed * stepDt;
        tunnelPathDist = std::max(tunnelPathDist, tunnelPathDistForProgress(tunnelProgress));
        pathBend = tunnelBendAmount(tunnelPathDist);
        corePos = tunnelPathPoint(tunnelPathDist, pathBend);
        coreVel = tunnelPathTangent(tunnelPathDist, pathBend) * tunnelSpeed;
        cubeReveal = std::max(0.0f, cubeReveal - stepDt * 2.2f);
        starFieldReveal = std::max(starFieldReveal,
            smootherstep(0.32f, 0.88f, tunnelProgress) * 0.78f);
    }

    if (t >= b.launch) {
        if (cagePhase < CAGE_LAUNCH) {
            cagePhase = CAGE_LAUNCH;
            tunnelVisualFade = 0.65f;
            beatMorphRotKick = 0.0f;
            blobTrailPos.clear();
            blobTrailAge.clear();
            blobTrailTimer = 0.0f;
        }
        launchPhaseTime = t - b.launch;
        launchProgress = clamp01(launchPhaseTime / std::max(0.001f, gLive.launchDuration));
        starFieldReveal = std::max(starFieldReveal,
            smootherstep(0.05f, 0.55f, launchProgress) * 0.98f);
        tunnelVisualFade = 1.0f - smootherstep(0.0f, 0.55f, launchProgress);
        glm::vec3 fwd = safeNorm(coreVel);
        if (glm::length(fwd) < 0.01f) fwd = glm::vec3(0.0f, 0.0f, -1.0f);
        float thrust = (18.0f + launchProgress * 95.0f + beatAccent * 22.0f) * gLive.launchThrustGain;
        coreVel += fwd * thrust * dt;
        coreVel += coreVel * dt * (0.12f + launchProgress * 0.28f);
        float pathHold = 1.0f - smootherstep(0.0f, 0.38f, launchProgress);
        glm::vec3 nextPos = corePos + coreVel * dt;
        if (pathHold > 0.001f) {
            float pathBend = tunnelBendAmount(tunnelPathDist);
            tunnelPathDist += glm::length(coreVel) * dt;
            glm::vec3 onPath = tunnelPathPoint(tunnelPathDist, pathBend);
            glm::vec3 tangent = tunnelPathTangent(tunnelPathDist, pathBend);
            float spd = glm::length(coreVel);
            coreVel = glm::mix(coreVel, tangent * spd, pathHold * 0.92f);
            corePos = glm::mix(nextPos, onPath, pathHold);
        } else {
            corePos = nextPos;
        }
        if (t >= b.finale) {
            finaleFadeTime = t - b.finale;
            finaleWhiteFade = smootherstep(0.0f, gLive.finaleWhiteDuration, finaleFadeTime);
            if (t >= b.end - 0.02f)
                narrativeFinished = true;
        }
    }

    if (cagePhase >= CAGE_BREAKING && cagePhase <= CAGE_LAUNCH
        && shellBurstVel.size() == network.size()
        && shellExtraOff.size() == network.size()) {
        for (size_t i = 0; i < network.size(); ++i) {
            glm::vec3 off = shellExtraOff[i];
            float dist = glm::length(off);
            if (dist > 0.01f) {
                glm::vec3 out = safeNorm(off);
                shellExtraOff[i] += out * dt * (4.0f + shellRupture * 14.0f);
                shellBurstVel[i] += out * dt * (1.8f + shellRupture * 4.5f);
            }
        }
    }

    for (auto it = impactWaves.begin(); it != impactWaves.end(); ) {
        it->age += dt;
        if (it->age > 3.6f) it = impactWaves.erase(it);
        else ++it;
    }
}

void updateCageEscape(float dt) {
    if (audioTimelineActive()) {
        updateCageEscapeAudioSynced(dt);
        return;
    }
    float net = networkViewActive ? 1.0f : networkBlend;
    if (net < 0.30f || coreBlobReveal < 0.45f) return;

    float cover = network.empty() ? 0.0f
        : float(learnedNodeCount) / float(std::max(1, (int)network.size()));
    float musicBoost = smootherstep(0.32f, 0.78f, musicDrive);

    cagePushDir = safeNorm(glm::vec3(
        sinf(globalTime * 0.73f) * 0.65f + sinf(globalTime * 1.13f + 1.2f) * 0.35f,
        cosf(globalTime * 0.61f) * 0.55f + sinf(globalTime * 0.97f) * 0.40f,
        sinf(globalTime * 0.83f) * 0.50f + cosf(globalTime * 1.07f) * 0.35f));

    if (cagePhase == CAGE_INTACT
        && coreBlobReveal > glm::mix(0.88f, 0.82f, musicBoost)
        && cover > glm::mix(0.72f, 0.66f, musicBoost)
        && learnedNodeCount >= int(glm::mix(30.0f, 26.0f, musicBoost)))
        cagePhase = CAGE_STRUGGLE;

    if (cagePhase == CAGE_STRUGGLE) {
        cageStruggleTime += dt;
        float drive = (0.04f + bassEnv * 0.06f + beatAccent * 0.10f + musicDrive * 0.07f) * dt;
        cageStruggle = std::min(1.0f, cageStruggle + drive);
        if (beatAccent > 0.38f) cageStruggle = std::min(1.0f, cageStruggle + 0.014f * (0.6f + musicBoost));
        cagePushAmt = cageStruggle * (0.50f + 0.40f * (0.5f + 0.5f * sinf(globalTime * 4.2f)));
        networkShake += cagePushDir * cagePushAmt * 0.28f;

        if (shellExtraOff.size() != network.size())
            shellExtraOff.assign(network.size(), glm::vec3(0.0f));
        for (size_t i = 0; i < network.size(); ++i) {
            glm::vec3 sp = shellBaseWorldPos(network[i], (int)i);
            float align = std::max(0.0f, glm::dot(safeNorm(sp), cagePushDir));
            shellExtraOff[i] = safeNorm(sp) * cagePushAmt * align * 0.22f;
        }

        float breakCover = glm::mix(0.68f, 0.62f, musicBoost);
        float breakTime = glm::mix(11.0f, 6.0f, musicBoost * clamp01(cover * 1.15f));
        if (cageStruggle >= 1.0f && cageStruggleTime > breakTime && cover > breakCover) {
            cagePhase = CAGE_BREAKING;
            shellBurstVel.resize(network.size());
            for (size_t i = 0; i < network.size(); ++i) {
                glm::vec3 dir = network[i].shellDir;
                shellBurstVel[i] = dir * (9.0f + hashf(float(i) * 3.7f) * 14.0f);
                shellBurstVel[i] += glm::vec3(hashf(i * 1.1f) - 0.5f, hashf(i * 2.3f) - 0.5f,
                                              hashf(i * 3.9f) - 0.5f) * 4.0f;
            }
            lightningPulse = std::max(lightningPulse, 0.90f);
        }
    }

    if (cagePhase == CAGE_BREAKING) {
        cageBreak = std::min(1.0f, cageBreak + dt * 0.62f);
        shellRupture = smootherstep(0.0f, 1.0f, cageBreak);
        cagePushAmt = (1.0f - cageBreak) * 1.15f;
        if (shellExtraOff.size() == network.size() && shellBurstVel.size() == network.size()) {
            for (size_t i = 0; i < network.size(); ++i)
                shellExtraOff[i] = shellBurstVel[i] * cageBreak * 2.8f;
        }
        if (cageBreak >= 1.0f) {
            cagePhase = CAGE_FREED;
            cubeHalfExtent = NETWORK_SHELL_RADIUS * 1.38f;
            glm::vec3 launch(
                hashf(globalTime * 3.1f) - 0.5f,
                hashf(globalTime * 3.1f + 2.0f) - 0.5f,
                hashf(globalTime * 3.1f + 4.0f) - 0.5f);
            coreVel = safeNorm(launch) * 11.0f + launch * 4.0f;
            corePos = glm::vec3(0.0f);
            beatMorphRotKick = 0.55f;
        }
    }

    if (cagePhase == CAGE_FREED && shellBurstVel.size() == network.size()
        && shellExtraOff.size() == network.size()) {
        for (size_t i = 0; i < network.size(); ++i) {
            shellExtraOff[i] += shellBurstVel[i] * dt * 0.85f;
            shellBurstVel[i] *= 1.0f + dt * 0.28f;
        }
    }

    if (cagePhase >= CAGE_BREAKING && cagePhase <= CAGE_LAUNCH
        && shellBurstVel.size() == network.size()
        && shellExtraOff.size() == network.size()) {
        for (size_t i = 0; i < network.size(); ++i) {
            glm::vec3 off = shellExtraOff[i];
            float dist = glm::length(off);
            if (dist > 0.01f) {
                glm::vec3 out = safeNorm(off);
                shellExtraOff[i] += out * dt * (4.0f + shellRupture * 14.0f);
                shellBurstVel[i] += out * dt * (1.8f + shellRupture * 4.5f);
            }
        }
    }

    if (cagePhase == CAGE_FREED) {
        cubeReveal = std::min(1.0f, cubeReveal + dt * 1.4f);
        freedPhaseTime += dt;
        float coreR = coreCollisionRadius();

        if (vacuumSuck) {
            tunnelEntryBlend = std::min(1.0f, tunnelEntryBlend + dt * 1.05f);
            float t = smootherstep(0.0f, 1.0f, tunnelEntryBlend);
            float pullSpd = tunnelEntrySpeed + t * (38.0f + tunnelEntrySpeed * 0.42f);
            tunnelPathDist += pullSpd * dt;
            float pathS = tunnelPathDist;
            float pathBend = tunnelBendAmount(pathS);
            glm::vec3 onPath = tunnelPathPoint(pathS, pathBend);
            glm::vec3 tangent = tunnelPathTangent(pathS, pathBend);
            if (t < 0.20f)
                corePos = glm::mix(preTunnelPos, onPath, smootherstep(0.04f, 0.20f, t));
            else
                corePos = onPath;
            coreVel = tangent * pullSpd;
            cubeReveal = std::max(0.0f, cubeReveal - dt * 2.8f);
            tunnelVisualFade = std::max(tunnelVisualFade, smootherstep(0.04f, 0.55f, t));
            tunnelDrawMorph = std::max(tunnelDrawMorph, smootherstep(0.04f, 0.50f, t));
            starFieldReveal = std::max(starFieldReveal, smootherstep(0.18f, 0.75f, t) * 0.42f);
            if (tunnelEntryBlend >= 1.0f) {
                cagePhase = CAGE_TUNNEL;
                tunnelPhaseTime = 0.0f;
                tunnelProgress = 0.0f;
                tunnelSpeed = pullSpd;
                tunnelEntrySpeed = pullSpd;
                tunnelDrawMorph = 1.0f;
                tunnelVisualFade = 1.0f;
                vacuumSuck = false;
                blobTrailPos.clear();
                blobTrailAge.clear();
                blobTrailTimer = 0.0f;
                beatMorphRotKick = 0.0f;
            }
        } else {
        if (beatAccent > 0.38f) {
            glm::vec3 kick(
                (hashf(globalTime * 8.1f) - 0.5f) * beatAccent * 5.5f,
                (hashf(globalTime * 8.1f + 1.7f) - 0.5f) * beatAccent * 5.5f,
                (hashf(globalTime * 8.1f + 3.3f) - 0.5f) * beatAccent * 4.5f);
            coreVel += kick;
        }

        glm::vec3 chaos(
            sinf(globalTime * 2.4f) * midEnv * 14.0f + sinf(globalTime * 5.1f) * musicDrive * 8.0f,
            cosf(globalTime * 1.8f) * bassEnv * 14.0f + cosf(globalTime * 4.7f) * musicDrive * 8.0f,
            sinf(globalTime * 2.9f) * trebleEnv * 11.0f + sinf(globalTime * 6.3f) * beatAccent * 6.0f);
        coreVel += chaos * dt * 2.8f;
        coreVel *= expf(-dt * 0.28f);

        float spd = glm::length(coreVel);
        if (spd < 6.5f)
            coreVel = (spd > 0.01f ? coreVel / spd : safeNorm(chaos)) * 6.5f;
        else if (spd > 24.0f)
            coreVel = coreVel / spd * 24.0f;

        corePos += coreVel * dt;

        auto bounceAxis = [&](int ax) {
            float limit = cubeHalfExtent - coreR;
            if (limit < 1.2f) limit = 1.2f;
            if (corePos[ax] > limit) {
                float hitSpeed = glm::length(coreVel);
                glm::vec3 hitPos = corePos;
                corePos[ax] = limit;
                if (coreVel[ax] > 0.0f) {
                    coreVel[ax] *= -0.88f;
                    int o1 = (ax + 1) % 3, o2 = (ax + 2) % 3;
                    coreVel[o1] += (hashf(globalTime + ax * 1.3f) - 0.5f) * 4.5f;
                    coreVel[o2] += (hashf(globalTime + ax * 2.7f) - 0.5f) * 4.5f;
                    spawnCubeWallImpact(ax, cubeHalfExtent, hitPos, coreR, hitSpeed);
                }
            } else if (corePos[ax] < -limit) {
                float hitSpeed = glm::length(coreVel);
                glm::vec3 hitPos = corePos;
                if (ax == 2 && coreVel[ax] < -2.5f && cubeReveal > 0.5f
                    && freedPhaseTime >= cubeMinBounceTime()) {
                    beginVacuumSuck();
                    spawnCubeWallImpact(ax, -cubeHalfExtent, hitPos, coreR, hitSpeed, 1.35f);
                    return;
                }
                corePos[ax] = -limit;
                if (coreVel[ax] < 0.0f) {
                    coreVel[ax] *= -0.88f;
                    int o1 = (ax + 1) % 3, o2 = (ax + 2) % 3;
                    coreVel[o1] += (hashf(globalTime + ax * 1.9f) - 0.5f) * 4.5f;
                    coreVel[o2] += (hashf(globalTime + ax * 3.1f) - 0.5f) * 4.5f;
                    spawnCubeWallImpact(ax, -cubeHalfExtent, hitPos, coreR, hitSpeed);
                }
            }
        };
        bounceAxis(0);
        bounceAxis(1);
        bounceAxis(2);

        if (!vacuumSuck && freedPhaseTime >= cubeMinBounceTime()
            && corePos.z < -(cubeHalfExtent - coreR * 0.6f) && coreVel.z < -3.5f && cubeReveal > 0.45f) {
            beginVacuumSuck();
            spawnCubeWallImpact(2, -cubeHalfExtent, corePos, coreR, glm::length(coreVel), 1.2f);
        }

        if (!vacuumSuck && freedPhaseTime >= cubeVacuumFallbackTime() && cubeReveal > 0.75f) {
            beginVacuumSuck();
            preTunnelVel = safeNorm(glm::vec3(0.1f, 0.05f, -1.0f)) * std::max(22.0f, glm::length(coreVel));
            tunnelEntrySpeed = std::max(glm::length(preTunnelVel), 22.0f);
        }
        }
    }

    if (cagePhase == CAGE_TUNNEL) {
        tunnelPhaseTime += dt;
        tunnelProgress = clamp01(tunnelPhaseTime / gLive.tunnelDuration);
        float pathBend = tunnelBendAmount(tunnelPathDist);
        float accel = (18.0f + tunnelProgress * 58.0f + beatAccent * 16.0f + bassEnv * 10.0f) * gLive.tunnelSpeedGain;
        accel *= 1.0f + smootherstep(0.0f, 0.10f, tunnelProgress) * 0.65f;
        tunnelSpeed += accel * dt;
        if (tunnelEntrySpeed > 0.0f)
            tunnelSpeed = std::max(tunnelSpeed, tunnelEntrySpeed);
        tunnelPathDist += tunnelSpeed * dt;
        corePos = tunnelPathPoint(tunnelPathDist, pathBend);
        coreVel = tunnelPathTangent(tunnelPathDist, pathBend) * tunnelSpeed;
        cubeReveal = std::max(0.0f, cubeReveal - dt * 2.2f);
        starFieldReveal = std::max(starFieldReveal,
            smootherstep(0.32f, 0.88f, tunnelProgress) * 0.78f);
        float launchGate = 145.0f + tunnelProgress * 35.0f;
        if (tunnelProgress > 0.92f && tunnelPathDist > launchGate) {
            cagePhase = CAGE_LAUNCH;
            launchPhaseTime = 0.0f;
            launchProgress = 0.0f;
            tunnelVisualFade = 0.65f;
            beatMorphRotKick = 0.0f;
            blobTrailPos.clear();
            blobTrailAge.clear();
            blobTrailTimer = 0.0f;
            coreVel = tunnelPathTangent(tunnelPathDist, tunnelBendAmount(tunnelPathDist)) * std::max(tunnelSpeed, 48.0f);
        } else if (tunnelProgress >= 1.0f) {
            cagePhase = CAGE_LAUNCH;
            launchPhaseTime = 0.0f;
            launchProgress = 0.0f;
            tunnelVisualFade = 1.0f;
            beatMorphRotKick = 0.0f;
            blobTrailPos.clear();
            blobTrailAge.clear();
            blobTrailTimer = 0.0f;
        }
    }

    if (cagePhase == CAGE_LAUNCH) {
        launchPhaseTime += dt;
        launchProgress = clamp01(launchPhaseTime / gLive.launchDuration);
        starFieldReveal = std::max(starFieldReveal,
            smootherstep(0.05f, 0.55f, launchProgress) * 0.98f);
        tunnelVisualFade = 1.0f - smootherstep(0.0f, 0.55f, launchProgress);
        glm::vec3 fwd = safeNorm(coreVel);
        if (glm::length(fwd) < 0.01f) fwd = glm::vec3(0.0f, 0.0f, -1.0f);
        float thrust = (18.0f + launchProgress * 95.0f + beatAccent * 22.0f) * gLive.launchThrustGain;
        coreVel += fwd * thrust * dt;
        coreVel += coreVel * dt * (0.12f + launchProgress * 0.28f);
        float pathHold = 1.0f - smootherstep(0.0f, 0.38f, launchProgress);
        glm::vec3 nextPos = corePos + coreVel * dt;
        if (pathHold > 0.001f) {
            float pathBend = tunnelBendAmount(tunnelPathDist);
            tunnelPathDist += glm::length(coreVel) * dt;
            glm::vec3 onPath = tunnelPathPoint(tunnelPathDist, pathBend);
            glm::vec3 tangent = tunnelPathTangent(tunnelPathDist, pathBend);
            float spd = glm::length(coreVel);
            coreVel = glm::mix(coreVel, tangent * spd, pathHold * 0.92f);
            corePos = glm::mix(nextPos, onPath, pathHold);
        } else {
            corePos = nextPos;
        }
        if (launchProgress >= 1.0f) {
            finaleFadeTime += dt;
            finaleWhiteFade = smootherstep(0.0f, gLive.finaleWhiteDuration, finaleFadeTime);
            if (finaleFadeTime >= gLive.finaleWhiteDuration)
                narrativeFinished = true;
        }
    }

    for (auto it = impactWaves.begin(); it != impactWaves.end(); ) {
        it->age += dt;
        if (it->age > 3.6f) it = impactWaves.erase(it);
        else ++it;
    }
}

static glm::vec3 neuronShapeBlend(const NeuronNode& n, float t) {
    if (n.spawnOrder >= 7) {
        glm::vec3 base(0.44f, 0.48f, 0.55f);
        float beatMorph = clamp01(beatAccent * 1.2f + musicDrive * 0.3f);
        base += glm::vec3(0.06f, 0.04f, -0.03f) * sinf(t * 0.8f + n.dendritePhase) * beatMorph;
        float kick = clamp01(beatMorphRotKick);
        if (kick > 0.02f)
            base = glm::mix(base, beatMorphBlend, kick * 0.22f);
        float sum = base.x + base.y + base.z;
        return sum > 0.001f ? base / sum : base;
    }
    glm::vec3 base(0.5f);
    switch (n.role) {
        case ROLE_UP_R: base = glm::vec3(0.64f, 0.20f, 0.16f); break;
        case ROLE_UP_G: base = glm::vec3(0.34f, 0.52f, 0.14f); break;
        case ROLE_UP_B: base = glm::vec3(0.34f, 0.16f, 0.50f); break;
        case ROLE_MIDDLE: base = glm::vec3(0.36f, 0.46f, 0.18f); break;
        case ROLE_DOWNSTREAM: base = glm::vec3(0.26f, 0.22f, 0.52f); break;
        default: return shapeBlend;
    }
    float beatMorph = clamp01(beatAccent * 1.5f + musicDrive * 0.35f);
    float phase = sinf(t * (0.65f + n.rotSpeed * 2.0f) + n.dendritePhase);
    glm::vec3 modeKick;
    switch (n.spawnOrder % 3) {
        case 0: modeKick = glm::vec3(0.18f, -0.05f, 0.16f); break; // spiked
        case 1: modeKick = glm::vec3(-0.05f, 0.24f, -0.02f); break; // thread
        default: modeKick = glm::vec3(0.18f, 0.10f, -0.08f); break; // blob swell
    }
    float w = 0.06f * phase;
    base += glm::vec3(w, w * 0.55f, -w * 0.35f) + modeKick * beatMorph;
    float kick = clamp01(beatMorphRotKick);
    if (kick > 0.02f)
        base = glm::mix(base, beatMorphBlend, kick * 0.30f);
    float sum = base.x + base.y + base.z;
    return sum > 0.001f ? base / sum : base;
}

static BlobDeformParams deformParamsFor(const glm::vec3& localShape, float timeOff, float narrativeT) {
    BlobDeformParams p;
    p.t = globalTime;
    p.nodeTime = timeOff;
    p.narrativeT = narrativeT;
    p.shapeBlend = localShape;
    p.audioBass = bassEnv;
    p.audioMid = midEnv;
    p.audioTreble = trebleEnv;
    p.beatAccent = beatAccent;
    return p;
}

static BlobDeformParams deformParamsForNeuron(const NeuronNode& n, float narrativeT) {
    return deformParamsFor(neuronShapeBlend(n, globalTime), n.dendritePhase * 0.35f, narrativeT);
}

static void seedNetworkColors() {
    if (network.size() < 7) return;
    if (networkCoresSeeded) {
        network[coreR].signal = roleBaseColor(ROLE_UP_R);
        network[coreR].displaySignal = network[coreR].signal;
        network[coreR].color = network[coreR].signal;
        network[coreG].signal = roleBaseColor(ROLE_UP_G);
        network[coreG].displaySignal = network[coreG].signal;
        network[coreG].color = network[coreG].signal;
        network[coreB].signal = roleBaseColor(ROLE_UP_B);
        network[coreB].displaySignal = network[coreB].signal;
        network[coreB].color = network[coreB].signal;
        return;
    }
    networkCoresSeeded = true;
    learnedNodeCount = 3;
    learnCooldown = 0.25f;
    for (auto& n : network) {
        n.signal = glm::vec3(0);
        n.displaySignal = glm::vec3(0);
        n.charge = 0;
        n.pulse = 0;
        n.color = roleBaseColor(n.role);
        n.active = n.spawnOrder < learnedNodeCount;
        n.growth = n.active ? 1.0f : 0.0f;
    }
    network[coreR].signal = roleBaseColor(ROLE_UP_R);
    network[coreR].displaySignal = network[coreR].signal;
    network[coreR].color = network[coreR].signal;
    network[coreR].pulse = 0.85f;
    network[coreG].signal = roleBaseColor(ROLE_UP_G);
    network[coreG].displaySignal = network[coreG].signal;
    network[coreG].color = network[coreG].signal;
    network[coreG].pulse = 0.85f;
    network[coreB].signal = roleBaseColor(ROLE_UP_B);
    network[coreB].displaySignal = network[coreB].signal;
    network[coreB].color = network[coreB].signal;
    network[coreB].pulse = 0.85f;
    cascadeStep = 0;
    cascadeTimer = 0;
    activeBolts.clear();
    if (!networkCoresSeeded)
        coreBlobReveal = 0.0f;
    resetCageState();
    narrativeFinished = false;
    freedPhaseTime = 0.0f;
}

static void activateNetworkView() {
    networkViewActive = true;
    narrativeClock = gLive.networkStart + 2.0f;
    globalTime = std::max(globalTime, narrativeClock);
    blobCenter = glm::vec2(0);
    blobDepth = 0.0f;
    dendriteGrowth = 0.55f;
    networkBlend = 1.0f;
    seedNetworkColors();
    if (audioPlayer) audioPlayer->seek(narrativeClock);
    printf("Network view ON — shell network grows on a sphere; core blob returns inside. Press R for solo.\n");
}

static void ensureFullNetwork() {
    networkViewActive = true;
    networkBlend = 1.0f;
    narrativeClock = std::max(narrativeClock, gLive.networkStart + 4.0f);
    globalTime = std::max(globalTime, narrativeClock);
    blobCenter = glm::vec2(0);
    blobDepth = 0.0f;
    dendriteGrowth = 1.0f;
    lightCharge = 0.5f;
    learnedNodeCount = (int)network.size();
    learnCooldown = 0.0f;
    coreBlobReveal = 1.0f;
    for (auto& n : network) {
        n.active = true;
        n.growth = 1.0f;
    }
}

static void initShellBurst() {
    shellBurstVel.resize(network.size());
    for (size_t i = 0; i < network.size(); ++i) {
        glm::vec3 dir = network[i].shellDir;
        shellBurstVel[i] = dir * (9.0f + hashf(float(i) * 3.7f) * 14.0f);
        shellBurstVel[i] += glm::vec3(hashf(i * 1.1f) - 0.5f, hashf(i * 2.3f) - 0.5f,
                                      hashf(i * 3.9f) - 0.5f) * 4.0f;
    }
}

static int narrativePhaseIndex() {
    if (cagePhase == CAGE_LAUNCH) return 7;
    if (cagePhase == CAGE_TUNNEL) return 6;
    if (cagePhase == CAGE_FREED) return 5;
    if (cagePhase == CAGE_BREAKING) return 4;
    if (cagePhase == CAGE_STRUGGLE) return 3;
    if (narrativeClock >= gLive.networkStart) return 2;
    if (narrativeClock >= DENDRITE_ONSET) return 1;
    return 0;
}

static void restartDemo();

static void jumpToPhase(int target) {
    static const char* phaseNames[] = {
        "solo", "dendrites", "network", "struggle", "breaking",
        "cube bounce", "tunnel", "launch", "finale"
    };
    if (target < 0 || target > 8) return;
    narrativeFinished = false;
    finaleWhiteFade = finaleFadeTime = 0.0f;
    vacuumSuck = false;
    tunnelEntryBlend = 0.0f;
    tunnelDrawMorph = 0.0f;
    tunnelEntrySpeed = 0.0f;

    if (target == 0) {
        restartDemo();
        printf("Jump -> solo\n");
        return;
    }

    switch (target) {
    case 1:
        networkViewActive = false;
        resetCageState();
        narrativeClock = DENDRITE_ONSET;
        globalTime = std::max(globalTime, narrativeClock);
        networkBlend = smootherstep(gLive.networkStart - 2.0f, gLive.networkStart + 2.0f, narrativeClock);
        dendriteGrowth = 0.55f;
        lightCharge = 0.35f;
        lightExposure = 0.2f;
        break;
    case 2:
        if (learnedNodeCount < 7) seedNetworkColors();
        ensureFullNetwork();
        resetCageState();
        break;
    case 3:
        ensureFullNetwork();
        resetCageState();
        cagePhase = CAGE_STRUGGLE;
        cageStruggle = 0.85f;
        cageStruggleTime = 12.0f;
        shellExtraOff.assign(network.size(), glm::vec3(0.0f));
        break;
    case 4:
        ensureFullNetwork();
        cagePhase = CAGE_BREAKING;
        cageStruggle = 1.0f;
        cageStruggleTime = 12.0f;
        cageBreak = 0.6f;
        shellRupture = smootherstep(0.0f, 1.0f, cageBreak);
        initShellBurst();
        shellExtraOff.assign(network.size(), glm::vec3(0.0f));
        for (size_t i = 0; i < network.size(); ++i)
            shellExtraOff[i] = shellBurstVel[i] * cageBreak * 2.8f;
        break;
    case 5:
        ensureFullNetwork();
        cagePhase = CAGE_FREED;
        cageStruggle = cageBreak = 1.0f;
        shellRupture = 1.0f;
        cubeHalfExtent = gLive.cubeHalfExtent;
        cubeReveal = 1.0f;
        freedPhaseTime = 0.0f;
        corePos = glm::vec3(0.0f);
        coreVel = safeNorm(glm::vec3(0.4f, 0.3f, -0.55f)) * 11.0f;
        impactWaves.clear();
        break;
    case 6:
        ensureFullNetwork();
        cagePhase = CAGE_TUNNEL;
        cageStruggle = cageBreak = shellRupture = 1.0f;
        cubeReveal = 0.0f;
        cubeHalfExtent = gLive.cubeHalfExtent;
        freedPhaseTime = gLive.freedBounceDuration;
        tunnelPhaseTime = gLive.tunnelDuration * 0.18f;
        tunnelProgress = clamp01(tunnelPhaseTime / gLive.tunnelDuration);
        tunnelPathDist = 22.0f;
        tunnelSpeed = 48.0f;
        tunnelEntrySpeed = 48.0f;
        tunnelVisualFade = 1.0f;
        tunnelDrawMorph = 1.0f;
        launchPhaseTime = launchProgress = 0.0f;
        starFieldReveal = 0.12f;
        {
            float bend = tunnelBendAmount(tunnelPathDist);
            corePos = tunnelPathPoint(tunnelPathDist, bend);
            coreVel = tunnelPathTangent(tunnelPathDist, bend) * tunnelSpeed;
        }
        blobTrailPos.clear();
        blobTrailAge.clear();
        blobTrailTimer = 0.0f;
        beatMorphRotKick = 0.0f;
        impactWaves.clear();
        break;
    case 7:
        ensureFullNetwork();
        cagePhase = CAGE_LAUNCH;
        cageStruggle = cageBreak = shellRupture = 1.0f;
        cubeReveal = 0.0f;
        tunnelPhaseTime = gLive.tunnelDuration;
        tunnelProgress = 1.0f;
        tunnelVisualFade = 0.35f;
        launchPhaseTime = 0.0f;
        launchProgress = 0.0f;
        starFieldReveal = 0.45f;
        blobTrailPos.clear();
        blobTrailAge.clear();
        blobTrailTimer = 0.0f;
        {
            float s = 130.0f;
            float bend = tunnelBendAmount(s);
            corePos = tunnelPathPoint(s, bend);
            coreVel = tunnelPathTangent(s, bend) * 45.0f;
        }
        break;
    case 8:
        ensureFullNetwork();
        cagePhase = CAGE_LAUNCH;
        cubeReveal = 0.0f;
        tunnelPhaseTime = gLive.tunnelDuration;
        tunnelProgress = 1.0f;
        tunnelVisualFade = 0.08f;
        launchPhaseTime = gLive.launchDuration * 0.88f;
        launchProgress = clamp01(launchPhaseTime / gLive.launchDuration);
        starFieldReveal = 0.92f;
        {
            float s = 165.0f;
            float bend = tunnelBendAmount(s);
            corePos = tunnelPathPoint(s, bend);
            coreVel = tunnelPathTangent(s, bend) * 62.0f;
        }
        break;
    default:
        return;
    }

    if (audioPlayer) audioPlayer->seek(narrativeClock);
    printf("Jump -> %s\n", phaseNames[target]);
}

static void skipToNextPhase() {
    int next = narrativePhaseIndex() + 1;
    if (next > 8) {
        narrativeFinished = true;
        printf("Skip -> finale (done)\n");
        return;
    }
    jumpToPhase(next);
}

static void fitNetworkCamera(float& camDist, float& fovDeg) {
    float r = networkBlobScale() * 1.35f;
    float maxExt = NETWORK_SHELL_RADIUS + r;
    fovDeg = 52.0f;
    float fovRad = fovDeg * float(M_PI) / 180.0f;
    camDist = (maxExt * 1.35f) / tanf(fovRad * 0.5f);
    camDist = std::max(camDist, 18.0f);
}

void generateSphere(int res) {
    baseVertices.clear();
    sphereIndices.clear();
    for (int y = 0; y <= res; ++y) {
        float v = float(y) / res, theta = v * float(M_PI);
        for (int x = 0; x <= res; ++x) {
            float u = float(x) / res, phi = u * 2.0f * float(M_PI);
            baseVertices.emplace_back(sinf(theta)*cosf(phi), cosf(theta), sinf(theta)*sinf(phi));
        }
    }
    for (int y = 0; y < res; ++y)
        for (int x = 0; x < res; ++x) {
            unsigned i0 = y*(res+1)+x, i1 = i0+1, i2 = i0+(res+1), i3 = i2+1;
            sphereIndices.insert(sphereIndices.end(), {i0,i2,i1,i1,i2,i3});
        }
}

void appendBatchSeg(DendriteBatch& batch, glm::vec3 a, glm::vec3 b, glm::vec3 colA, glm::vec3 colB) {
    batch.verts.push_back(a);
    batch.verts.push_back(b);
    batch.cols.push_back(colA);
    batch.cols.push_back(colB);
}

void growDendriteBranchDeformed(DendriteBatch& batch, glm::vec3 start, glm::vec3 dir,
                                float len, int depth, float seed, const glm::vec3& col, float taper) {
    if (depth <= 0 || len < 0.04f) return;
    dir = safeNorm(dir);
    glm::vec3 end = start + dir * len;
    appendBatchSeg(batch, start, end, col * (0.70f + 0.30f * taper), col * (0.45f + 0.20f * taper));
    if (depth <= 1) return;
    float h = hashf(seed + depth * 9.0f);
    glm::vec3 axis = safeNorm(glm::cross(dir, glm::vec3(0, 1, 0)) + glm::vec3(h - 0.5f, 0, hashf(seed) - 0.5f));
    growDendriteBranchDeformed(batch, end, safeNorm(dir + axis * 0.35f), len * 0.55f, depth - 1,
                               seed + 1.0f, col, taper * 0.8f);
}

void buildBlobDendrites(DendriteBatch& batch, NeuronRole role, float growth, float seed,
                        const glm::vec3& col, const BlobDeformParams& deform) {
    int count = (role == ROLE_SOLO) ? 6 : 10;
    float branchLen = 0.42f + growth * 0.72f;
    glm::vec3 dendCol = col;
    if (glm::length(dendCol) < 0.1f)
        dendCol = glm::vec3(0.50f, 0.54f, 0.62f);

    for (int i = 0; i < count; ++i) {
        int vi = (int)(baseVertices.size() * (float(i) + seed * 0.07f) / float(count)) % (int)baseVertices.size();
        glm::vec3 unitPos = baseVertices[vi];
        glm::vec3 start = deformedSurface(unitPos, deform);
        glm::vec3 outward = safeNorm(start);
        start += outward * 0.04f;
        glm::vec3 jitter = safeNorm(outward + glm::vec3(
            hashf(seed + i * 2.1f) - 0.5f, hashf(seed + i * 3.7f) - 0.5f, hashf(seed + i * 5.3f) - 0.5f) * 0.22f);
        growDendriteBranchDeformed(batch, start, jitter, branchLen, 2, seed + i * 13.0f, dendCol, 1.0f);
    }
}

void buildSignalLink(glm::vec3 from, glm::vec3 to, const glm::vec3& col, float flowPhase, float strength,
                     float fatScale = 1.0f) {
    if (strength < 0.025f) return;
    glm::vec3 delta = to - from;
    float len = glm::length(delta);
    if (len < 0.08f) return;

    glm::vec3 dir = delta / len;
    glm::vec3 side = safeNorm(glm::cross(dir, glm::vec3(0, 0, 1)));
    if (glm::length(side) < 0.1f)
        side = safeNorm(glm::cross(dir, glm::vec3(0, 1, 0)));
    glm::vec3 up = safeNorm(glm::cross(side, dir));

    int segs = 28;
    float wobbleAmp = std::min(0.42f, 0.07f * len) * (0.55f + 0.45f * strength) * (0.75f + musicDrive * 0.35f);
    float crawl = globalTime * (0.35f + musicDrive * 0.65f) + flowPhase * 1.7f;
    float impulse = fmodf(impulseTravel + flowPhase * 0.13f, 1.0f);
    float beatBoost = 1.0f + beatAccent * 1.8f;
    float brightBoost = fatScale > 1.05f ? (1.35f + fatScale * 0.22f) : 1.0f;
    glm::vec3 prev = from;
    auto emitSeg = [&](const glm::vec3& a, const glm::vec3& b, const glm::vec3& c) {
        linkLines.push_back(a);
        linkLines.push_back(b);
        linkColors.push_back(c * 1.05f);
        linkColors.push_back(c * 1.55f);
    };
    for (int s = 1; s <= segs; ++s) {
        float t = float(s) / segs;

        float envelope = sinf(t * float(M_PI)); // pinned at both blob surfaces
        float primary = sinf(t * 9.0f + crawl * 2.0f);
        float secondary = sinf(t * 17.0f - crawl * 1.35f + flowPhase * 3.1f);
        float twitch = sinf(globalTime * (2.4f + musicDrive * 4.0f) + t * 31.0f + flowPhase * 5.0f);
        glm::vec3 offset = side * (primary * 0.70f + secondary * 0.30f)
                         + up * (cosf(t * 11.0f - crawl) * 0.55f + twitch * 0.20f);
        offset += networkShake * sinf(t * float(M_PI)) * (0.35f + bassEnv * 0.45f);
        glm::vec3 p = glm::mix(from, to, t) + offset * wobbleAmp * envelope;

        float d = fabsf(t - impulse);
        d = std::min(d, 1.0f - d);
        float travelingFront = expf(-d * d * (55.0f + musicDrive * 45.0f));
        float afterglow = 0.15f + 0.28f * (0.5f + 0.5f * sinf(t * 18.0f - globalTime * (0.8f + musicDrive) + flowPhase));
        float vibration = 0.65f + 0.35f * sinf(globalTime * (4.0f + musicDrive * 6.0f) + t * 23.0f + flowPhase);
        glm::vec3 c = col * strength * brightBoost
                    * (afterglow + travelingFront * (1.35f + beatAccent * 1.2f)) * vibration * beatBoost;
        emitSeg(prev, p, c);
        if (fatScale > 1.05f) {
            float off = 0.026f * fatScale;
            emitSeg(prev + side * off, p + side * off, c * 0.94f);
            emitSeg(prev - side * off, p - side * off, c * 0.94f);
            emitSeg(prev + up * off * 0.62f, p + up * off * 0.62f, c * 0.82f);
        }

        if (travelingFront > 0.55f) {
            float bead = (travelingFront - 0.55f) / 0.45f;
            glm::vec3 beadCol = col * strength * brightBoost * (2.4f + beatAccent * 2.0f) * bead;
            glm::vec3 tickA = p - side * (0.045f + 0.055f * bead) - up * 0.020f;
            glm::vec3 tickB = p + side * (0.045f + 0.055f * bead) + up * 0.020f;
            emitSeg(tickA, tickB, beadCol);
        }
        prev = p;
    }
}

static void addNetworkLink(int fromIdx, int toIdx, float netAppear, float netScale, float narrativeT,
                           const glm::vec3& col, float phase, float strength) {
    if (fromIdx < 0 || toIdx < 0 || fromIdx >= (int)network.size() || toIdx >= (int)network.size()) return;
    const NeuronNode& a = network[fromIdx];
    const NeuronNode& b = network[toIdx];
    glm::mat4 modelA = neuronModelMatrix(a, netAppear, netScale, fromIdx);
    glm::mat4 modelB = neuronModelMatrix(b, netAppear, netScale, toIdx);
    BlobDeformParams pA = deformParamsForNeuron(a, narrativeT);
    BlobDeformParams pB = deformParamsForNeuron(b, narrativeT);
    glm::vec3 from = worldSurfaceToward(modelA, modelB, pA, pB);
    glm::vec3 to = worldSurfaceToward(modelB, modelA, pB, pA);
    buildSignalLink(from, to, col, phase, strength);
}

void updateMusicBrain(float dt) {
    float net = networkViewActive ? 1.0f : networkBlend;
    if (audioTimelineActive()) {
        NarrativePhaseBounds b = narrativePhaseBounds();
        if (narrativeClock >= b.network) {
            float u = clamp01((narrativeClock - b.network)
                / std::max(0.001f, b.freed - b.network));
            coreBlobReveal = std::max(coreBlobReveal, smootherstep(0.06f, 0.72f, u));
            int targetLearn = int(glm::mix(5.0f, float(network.size()),
                smootherstep(0.12f, 0.80f, u)));
            if (targetLearn > learnedNodeCount)
                learnedNodeCount = std::min(targetLearn, (int)network.size());
        }
    }
    float beatSens = gLive.beatSensitivity;
    float beat = std::max(beatAccent * beatSens, bassEnv * 0.55f);

    musicDrive += (energyEnv * 0.65f + bassEnv * 0.25f + midEnv * 0.10f - musicDrive) * std::min(1.0f, 4.0f * dt);
    float drive = musicDrive * gLive.musicDriveGain;
    float impulseSpeed = 0.10f + drive * 0.50f + beat * 0.95f + trebleEnv * 0.15f;
    impulseTravel = fmodf(impulseTravel + dt * impulseSpeed, 1.0f);

    if (beatAccent * beatSens > 0.45f)
        lightningPulse = std::max(lightningPulse, beatAccent * beatSens * 0.55f);

    learnCooldown -= dt;
    float cover = network.empty() ? 0.0f
        : float(learnedNodeCount) / float(std::max(1, (int)network.size()));
    if (net > NET_E_LEARN_ON && learnedNodeCount < (int)network.size()
        && (beatAccent * beatSens > 0.24f || bassEnv > 0.48f) && learnCooldown <= 0.0f) {
        int add = (beatAccent * beatSens > 0.48f && cover < 0.88f) ? 3 : 2;
        for (int k = 0; k < add && learnedNodeCount < (int)network.size(); ++k) {
            learnedNodeCount++;
            learnCooldown = (cover < 0.70f ? 0.045f : 0.07f) / std::max(0.15f, gLive.learnRate);
            for (auto& born : network) {
                if (born.spawnOrder != learnedNodeCount - 1) continue;
                born.active = true;
                born.growth = 0.82f;
                born.pulse = 1.5f;
                born.flowPhase += 1.3f;
                lightningPulse = std::max(lightningPulse, 0.45f);
                break;
            }
        }
    }

    for (auto& n : network) {
        float targetGrowth = (n.spawnOrder < learnedNodeCount) ? 1.0f : 0.0f;
        n.active = targetGrowth > 0.0f || n.growth > 0.03f;
        n.growth += (targetGrowth - n.growth) * std::min(1.0f, (targetGrowth > n.growth ? 10.0f : 4.0f) * dt);
    }

    float targetCore = (learnedNodeCount >= 5 && net > 0.30f && cover > 0.12f)
        ? smootherstep(0.10f, 0.58f, cover) : 0.0f;
    coreBlobReveal += (targetCore - coreBlobReveal) * std::min(1.0f, 4.2f * dt);

    updateShellDodge(dt);

    coreColorPhase += dt * (0.40f + midEnv * 0.22f + musicDrive * 0.08f);

    networkShake = glm::vec3(
        sinf(globalTime * (2.6f + bassEnv * 2.0f)) * bassEnv * 0.16f,
        cosf(globalTime * (3.1f + midEnv * 1.5f) + 0.6f) * midEnv * 0.14f,
        sinf(globalTime * (4.8f + trebleEnv * 2.5f)) * trebleEnv * 0.08f) * net;
}

void initNetwork() {
    network.clear();
    shellNeighbors.clear();
    for (int i = 0; i < SHELL_NODE_COUNT; ++i) {
        glm::vec3 dir = fibonacciSphereDir(i, SHELL_NODE_COUNT);
        NeuronNode n;
        n.shellDir = dir;
        n.pos = dir * NETWORK_SHELL_RADIUS;
        n.role = ROLE_MIDDLE;
        n.color = roleBaseColor(ROLE_MIDDLE);
        n.signal = glm::vec3(0);
        n.displaySignal = glm::vec3(0);
        n.charge = 0;
        n.pulse = 0;
        n.flowPhase = hashf(dir.x * 2.1f + dir.y * 3.7f);
        n.growth = 0.0f;
        n.spawnOrder = i;
        n.active = false;
        n.dendritePhase = hashf(dir.x + dir.z) * 6.28318f;
        n.rotSpeed = 0.06f + hashf(dir.x * 1.7f + dir.y) * 0.12f;
        n.rotAxis = safeNorm(glm::cross(dir, glm::vec3(0, 1, 0)));
        if (glm::length(n.rotAxis) < 0.01f)
            n.rotAxis = glm::vec3(1, 0, 0);
        n.drift = glm::vec3(0);
        network.push_back(n);
    }

    std::vector<int> byY(SHELL_NODE_COUNT);
    std::iota(byY.begin(), byY.end(), 0);
    std::sort(byY.begin(), byY.end(), [](int a, int b) {
        return network[a].shellDir.y > network[b].shellDir.y;
    });

    int r = byY[0], g = byY[2], b = byY[4];
    int m0 = byY[SHELL_NODE_COUNT / 2], m1 = byY[SHELL_NODE_COUNT / 2 + 1];
    int d0 = byY[SHELL_NODE_COUNT - 2], d1 = byY[SHELL_NODE_COUNT - 1];
    coreR = r; coreG = g; coreB = b; coreM0 = m0; coreM1 = m1; coreD0 = d0; coreD1 = d1;
    int coreIdx[] = {r, g, b, m0, m1, d0, d1};
    NeuronRole coreRoles[] = {
        ROLE_UP_R, ROLE_UP_G, ROLE_UP_B,
        ROLE_MIDDLE, ROLE_MIDDLE, ROLE_DOWNSTREAM, ROLE_DOWNSTREAM
    };
    bool isCore[SHELL_NODE_COUNT] = {};
    for (int k = 0; k < 7; ++k) {
        isCore[coreIdx[k]] = true;
        network[coreIdx[k]].spawnOrder = k;
        network[coreIdx[k]].role = coreRoles[k];
        network[coreIdx[k]].color = roleBaseColor(coreRoles[k]);
    }
    int order = 7;
    for (int i = 0; i < SHELL_NODE_COUNT; ++i) {
        if (isCore[i]) continue;
        network[i].spawnOrder = order++;
    }

    network[m0].inputs = {r, g};
    network[m1].inputs = {g, b};
    network[d0].inputs = {m0};
    network[d1].inputs = {m1};
    network[r].outputs = {m0};
    network[g].outputs = {m0, m1};
    network[b].outputs = {m1};
    network[m0].outputs = {d0};
    network[m1].outputs = {d1};

    buildShellNeighborGraph();
    shellExtraOff.assign(SHELL_NODE_COUNT, glm::vec3(0.0f));
    shellDodgeOff.assign(SHELL_NODE_COUNT, glm::vec3(0.0f));

    for (auto& n : network) {
        n.active = n.spawnOrder < learnedNodeCount;
        n.growth = n.active ? 1.0f : 0.0f;
    }
}

const char* blobVS = R"(
#version 330 core
layout(location=0) in vec3 aPos;
uniform mat4 model, view, projection;
uniform float time, nodeTime, audioBass, audioMid, audioTreble, beatAccent, narrativeT;
uniform vec3 shapeBlend;
uniform float cagePushAmt;
uniform vec3 cagePushDir;
out float vIntensity;
out vec3 vNormal;
out vec3 vUnitPos;
out vec3 vWorldPos;
out vec3 vWorldNormal;

float blobDeform(vec3 pos, float t) {
    return 0.10*sin(t*1.2+pos.x*6.0+pos.y*4.0) + 0.08*cos(t*1.7+pos.y*5.0+pos.z*3.0)
         + 0.06*sin(t*0.9+pos.z*7.0);
}
float threadDeform(vec3 pos, float t) {
    float fiber = sin(t*2.4+pos.x*9.0)*cos(t*1.8+pos.y*6.0);
    float axis = sin(pos.x*11.0+pos.y*7.0+pos.z*5.0);
    return 0.12 + 0.40*abs(fiber)*(0.45+0.55*abs(axis));
}
float spikeDeform(vec3 pos, float t) {
    float radial = pos.x*10.0+pos.y*10.0+pos.z*8.0;
    return 0.45*pow(abs(sin(t*2.2+radial)), 5.0);
}

void main() {
    vec3 pos = aPos;
    float t = time + nodeTime;
    float breathe = 0.028*sin(t*0.55) + 0.018*audioBass;
    float midWobble = 0.02*audioMid*sin(t*1.1+dot(pos,vec3(3,5,2)));
    float shimmer = 0.01*audioTreble*sin(t*8.0+dot(pos,vec3(12,9,11)));
    float base = 1.0 + breathe + midWobble + shimmer + beatAccent*0.022;
    float deform = shapeBlend.x*blobDeform(pos,t) + shapeBlend.y*threadDeform(pos,t) + shapeBlend.z*spikeDeform(pos,t);
    float narrativePulse = 0.06*sin(narrativeT*6.28318)*(1.0-narrativeT);
    deform *= 0.85 + 0.15*sin(t*0.4+narrativeT*3.14159);
    float pushBulge = cagePushAmt * max(0.0, dot(normalize(pos), normalize(cagePushDir)));
    float intensity = clamp(base + deform + narrativePulse*shapeBlend.x + pushBulge*0.28, 0.72, 2.15);
    vIntensity = intensity;
    vUnitPos = pos;
    vec3 displaced = pos * intensity;
    vNormal = normalize(displaced);
    vec4 worldPos = model * vec4(displaced, 1.0);
    vWorldPos = worldPos.xyz;
    vWorldNormal = normalize(mat3(model) * vNormal);
    gl_Position = projection * view * worldPos;
}
)";

const char* blobFS = R"(
#version 330 core
in float vIntensity;
in vec3 vNormal;
in vec3 vUnitPos;
in vec3 vWorldPos;
in vec3 vWorldNormal;
uniform vec3 shapeBlend;
uniform vec3 signalColor;
uniform vec3 cameraPos;
uniform float signalAmp;
uniform float wavePhase;
uniform float nodeTime;
uniform int neuronRole;
uniform float audioBass, audioMid, audioTreble, lightningPulse, time, musicDrive, beatAccent;
uniform float colorSeed;
uniform float blobAlpha;
uniform float soloGreyAmt;
out vec4 FragColor;

vec3 prismDispersion(vec3 c) {
    float w = (c.r + c.g + c.b) / 3.0;
    vec3 m = max(c - vec3(w * 0.88), vec3(0.0));
    return vec3(m.r * 1.22 + 0.03, m.g * 0.82, m.b * 1.28 + 0.03);
}

float plasmaNoise(vec3 p, float t) {
    float a = sin(p.x * 5.8 + t * 1.05);
    float b = sin(p.y * 6.4 - t * 0.88);
    float c = sin(p.z * 4.9 + t * 0.95);
    float d = sin(dot(p, vec3(1.6, 1.05, 1.35)) * 3.6 + t * 1.35);
    float e = sin(p.x * 11.0 + p.y * 8.5 - t * 2.1) * 0.55;
    float f = sin(p.z * 14.0 + p.x * 9.0 + t * 1.7) * 0.35;
    return (a + b + c + d + e + f) * 0.28;
}

void main() {
    vec3 n = normalize(vWorldNormal);
    vec3 eye = normalize(cameraPos - vWorldPos);
    vec3 lightDir = normalize(vec3(0.30, 0.82, 0.48));
    vec3 halfDir = normalize(lightDir + eye);

    float diff = 0.18 + 0.82 * max(dot(n, lightDir), 0.0);
    float rim = pow(1.0 - max(dot(n, eye), 0.0), 2.2);
    float spec = pow(max(dot(n, halfDir), 0.0), 38.0);
    float backLight = pow(max(dot(n, -lightDir), 0.0), 3.0) * 0.22;

    float t = time + nodeTime * 0.15;
    float pt = t + colorSeed * 0.18;
    float drive = 0.85 + musicDrive * 0.65 + beatAccent * 0.45 + audioBass * 0.25;
    vec3 plasmaPos = vUnitPos * 1.35 + vWorldPos * 0.08;
    float p1 = plasmaNoise(plasmaPos, pt * drive);
    float p2 = plasmaNoise(plasmaPos * 1.85 + vec3(sin(pt * 0.41), cos(pt * 0.33), sin(pt * 0.27)), pt * 1.35 * drive);
    float p3 = plasmaNoise(plasmaPos * 0.62 + vec3(0.0, pt * 0.11, 0.0), pt * 0.88);
    float p4 = plasmaNoise(plasmaPos * 3.2 + vec3(pt * 0.05), pt * 2.2 * drive);
    float field = clamp(0.32 + p1 + p2 * 0.78 + p3 * 0.42 + p4 * 0.28, 0.0, 1.0);
    field = pow(field, 0.95 + shapeBlend.z * 0.25);
    field *= 0.78 + 0.22 * sin(pt * 3.4 + dot(vUnitPos, vec3(7.0, 4.5, 5.5)));

    vec3 plasmaHue;
    if (neuronRole == 0) {
        plasmaHue = vec3(
            0.58 + 0.32 * sin(vUnitPos.x * 4.2 + vUnitPos.y * 3.1 + pt * 1.1),
            0.44 + 0.28 * sin(vUnitPos.y * 3.8 + vUnitPos.z * 2.9 + pt * 0.95 + 2.3),
            0.36 + 0.24 * sin(vUnitPos.z * 4.5 + vUnitPos.x * 2.6 + pt * 1.05 + 4.1));
        plasmaHue = mix(plasmaHue, vec3(0.92, 0.48, 0.18), shapeBlend.x * 0.48 * (1.0 - soloGreyAmt * 0.92));
        plasmaHue = mix(plasmaHue, vec3(0.55, 0.78, 0.42), shapeBlend.y * 0.26 * (1.0 - soloGreyAmt * 0.92));
        plasmaHue = mix(plasmaHue, vec3(0.38, 0.55, 0.95), shapeBlend.z * 0.24 * (1.0 - soloGreyAmt * 0.92));
        plasmaHue = mix(plasmaHue, vec3(0.44, 0.47, 0.52), soloGreyAmt * 0.98);
        field *= (1.0 - soloGreyAmt * 0.82);
    } else {
        plasmaHue = vec3(
            0.42 + 0.44 * sin(vUnitPos.x * 3.5 + pt * 0.95 + float(neuronRole) * 0.9),
            0.28 + 0.38 * sin(vUnitPos.y * 3.2 + pt * 0.88 + 2.094 + float(neuronRole) * 0.7),
            0.48 + 0.46 * sin(vUnitPos.z * 3.8 + pt * 0.92 + 4.189 + float(neuronRole) * 1.1));
        plasmaHue = mix(plasmaHue, vec3(0.62, 0.28, 0.82), shapeBlend.z * 0.16);
        plasmaHue = mix(plasmaHue, vec3(0.28, 0.72, 0.58), shapeBlend.y * 0.14);
        plasmaHue = mix(plasmaHue, vec3(0.72, 0.55, 0.38), shapeBlend.x * 0.12);
    }

    vec3 deep = plasmaHue * 0.12;
    vec3 mid = plasmaHue * (0.32 + 0.52 * field);
    vec3 hot = mix(plasmaHue, vec3(1.0, 0.94, 0.88), 0.62) * (0.55 + 0.45 * pow(field, 2.2));
    vec3 col = mix(deep, mix(mid, hot, pow(field, 1.35)), diff);
    col += plasmaHue * rim * (0.65 + audioBass * 0.50);
    col += vec3(0.95, 0.98, 1.0) * spec * (0.42 + field * 0.55);
    col += plasmaHue * backLight * (0.85 + field * 0.35);
    col += vec3(0.10, 0.12, 0.16) * (vIntensity - 1.0);

    vec3 flowDir = normalize(vec3(
        sin(wavePhase * 2.1 + float(neuronRole)),
        cos(wavePhase * 1.7 + float(neuronRole) * 0.6),
        sin(wavePhase * 1.3 + 1.2)));
    vec3 tangentDir = normalize(cross(flowDir, vec3(0.2, 0.9, 0.35)));
    float travel = dot(vUnitPos, flowDir);
    float crossTravel = dot(vUnitPos, tangentDir);
    float slowFront = sin(travel * 10.5 - time * 1.05 + wavePhase * 6.28318);
    float branchFront = sin(crossTravel * 13.0 + travel * 4.5 - time * 0.72 - wavePhase * 3.0);
    float filament = pow(max(0.0, 0.5 + 0.5 * slowFront), 3.2);
    float branching = pow(max(0.0, 0.5 + 0.5 * branchFront), 5.5);
    float crawl = 0.35 + 0.65 * pow(max(0.0, sin(travel * 5.0 - time * 0.38 + wavePhase * 2.0)), 2.0);
    float ripple = clamp(filament * 0.85 + branching * 0.65, 0.0, 1.35);
    float pulse = crawl * (0.82 + 0.18 * sin(time * 5.0 + crossTravel * 12.0 + wavePhase));

    vec3 sig = signalColor * signalAmp;

    if (neuronRole == 4) {
        col += sig * ripple * pulse * 1.05;
        col = mix(col, col * vec3(1.15, 1.08, 0.75), field * 0.35);
    } else if (neuronRole == 5) {
        vec3 add = sig * ripple * pulse;
        col += prismDispersion(add);
        col = mix(col, col * vec3(0.85, 0.75, 1.25), field * 0.40);
    } else if (neuronRole >= 1 && neuronRole <= 3) {
        col = mix(col, col * (0.55 + signalColor * 1.35) + signalColor * field * 0.28, 0.55 + signalAmp * 0.35);
        col += sig * ripple * (0.30 + 0.70 * pulse) * 0.85;
    } else if (neuronRole == 0) {
        float colorKeep = 1.0 - soloGreyAmt;
        col += hot * filament * (0.55 + pulse * 0.45) * colorKeep;
        col += plasmaHue * ripple * pulse * 0.95 * colorKeep;
        col += vec3(1.0, 0.96, 0.90) * pow(field, 3.5) * 0.42 * colorKeep;
        col = mix(col, hot, pow(field, 1.8) * 0.38 * colorKeep);
    } else {
        col += sig * ripple * 0.30;
        col = mix(col, col + plasmaHue * field * 0.22, 0.45);
    }

    if (neuronRole == 0 && soloGreyAmt > 0.001) {
        float luma = dot(col, vec3(0.299, 0.587, 0.114));
        col = mix(col, vec3(luma), soloGreyAmt * 0.72);
        vec3 grey = vec3(0.44, 0.47, 0.52);
        float sculpt = 0.48 + 0.52 * field;
        float relief = clamp(vIntensity - 0.88, -0.12, 0.38);
        vec3 greyCol = grey * sculpt * (0.18 + 0.82 * diff);
        greyCol += grey * rim * 0.48;
        greyCol += vec3(0.97, 0.98, 1.0) * spec * 0.62;
        greyCol += vec3(0.10, 0.11, 0.13) * relief;
        greyCol -= vec3(0.06) * (1.0 - diff);
        col = mix(col, clamp(greyCol, 0.0, 1.0), soloGreyAmt);
    }

    col = mix(col, vec3(0.92), lightningPulse * 0.22 * (1.0 - soloGreyAmt));

    FragColor = vec4(clamp(col, 0.0, 1.0), blobAlpha);
}
)";

const char* tunnelVS = R"(
#version 330 core
layout(location=0) in vec3 aPos;
layout(location=1) in vec2 aTA;
uniform mat4 view, projection;
out vec3 vWorldPos;
out vec2 vTA;
void main() {
    vWorldPos = aPos;
    vTA = aTA;
    gl_Position = projection * view * vec4(aPos, 1.0);
}
)";

const char* tunnelFS = R"(
#version 330 core
in vec3 vWorldPos;
in vec2 vTA;
uniform float time, tunnelProgress, fade;
out vec4 FragColor;
void main() {
    float t = vTA.x;
    float ang = vTA.y;
    float grid = sin(ang * 12.0 + t * 0.35 - time * 1.8)
               * sin(t * 0.22 - time * 2.4);
    vec3 base = mix(vec3(0.10, 0.14, 0.22), vec3(0.18, 0.28, 0.48), grid * 0.5 + 0.5);
    float rim = pow(1.0 - abs(sin(ang * 6.0 + time * 0.5)), 3.0);
    base += vec3(0.12, 0.22, 0.38) * rim * 0.35;
    float kaleidoW = smoothstep(0.68, 0.92, t) * tunnelProgress;
    float sectors = 7.0 + floor(tunnelProgress * 5.0);
    float ka = mod(ang * sectors + sin(t * 0.8 + time * 1.6) * 2.0, 6.28318);
    float kb = sin(ka * 3.0 + time * 3.5 + t * 4.0);
    vec3 kaleido = vec3(
        0.55 + 0.45 * sin(kb * 1.7 + 0.3),
        0.35 + 0.55 * sin(kb * 2.1 + 2.0),
        0.65 + 0.35 * sin(kb * 1.3 + 4.2));
    kaleido *= 0.85 + 0.15 * sin(time * 4.0 + ang * 8.0);
    base = mix(base, kaleido, kaleidoW);
    float endCap = smoothstep(0.94, 1.0, t);
    base = mix(base, kaleido * 1.35, endCap * tunnelProgress);
    float alpha = fade * (0.38 + 0.22 * rim) * (0.55 + 0.45 * tunnelProgress);
    FragColor = vec4(base, alpha);
}
)";

const char* starVS = R"(
#version 330 core
layout(location=0) in vec3 aDir;
layout(location=1) in vec3 aCol;
layout(location=2) in float aDist;
uniform mat4 view, projection;
uniform float time;
uniform vec3 starAnchor;
uniform float blobReachPulse;
uniform float starSwirl;
out vec3 vCol;
out float vBright;
void main() {
    vCol = aCol;
    vBright = aCol.r;
    vec3 dir = aDir;
    if (starSwirl > 0.001) {
        float w = starSwirl * (0.55 + aDist * 0.00042);
        float cy = cos(w + time * 0.24);
        float sy = sin(w + time * 0.24);
        dir = vec3(cy * dir.x + sy * dir.z, dir.y, -sy * dir.x + cy * dir.z);
        float cx = cos(w * 0.72 - time * 0.19);
        float sx = sin(w * 0.72 - time * 0.19);
        dir = vec3(dir.x, cx * dir.y - sx * dir.z, sx * dir.y + cx * dir.z);
        dir = normalize(dir);
    }
    vec3 worldPos = starAnchor + dir * aDist;
    vec4 viewPos = view * vec4(worldPos, 1.0);
    gl_Position = projection * viewPos;
    float dist = max(length(viewPos.xyz), 50.0);
    float reach = blobReachPulse * (0.5 + 0.5 * sin(time * 6.2 + vBright * 22.0));
    gl_PointSize = clamp((920.0 + reach * 780.0) * vBright / dist, 1.4, 8.5);
}
)";

const char* starFS = R"(
#version 330 core
in vec3 vCol;
in float vBright;
uniform float time, reveal, blobReachPulse;
out vec4 FragColor;
void main() {
    vec2 uv = gl_PointCoord * 2.0 - 1.0;
    float d = dot(uv, uv);
    if (d > 1.0) discard;
    float core = exp(-d * 3.6);
    float halo = exp(-d * 0.95) * 0.58;
    float tw = 0.80 + 0.20 * sin(time * 3.1 + vBright * 14.0);
    float reach = blobReachPulse * (0.55 + 0.45 * sin(time * 6.2 + vBright * 22.0));
    tw *= 1.0 + reach * 0.65;
    vec3 white = mix(vCol, vec3(0.98, 0.97, 1.0), reach * 0.42);
    vec3 c = white * tw * (core * (1.75 + reach * 0.55) + halo * (0.88 + reach * 0.35));
    c += vec3(1.0, 0.98, 0.95) * reach * core * 0.14;
    float a = reveal * (core * (1.30 + reach * 0.55) + halo * (0.78 + reach * 0.32));
    FragColor = vec4(c, clamp(a, 0.0, 1.0));
}
)";

const char* fadeVS = R"(
#version 330 core
layout(location=0) in vec2 aPos;
void main() {
    gl_Position = vec4(aPos, 0.0, 1.0);
}
)";

const char* fadeFS = R"(
#version 330 core
uniform float fade;
out vec4 FragColor;
void main() {
    FragColor = vec4(1.0, 0.98, 0.94, fade);
}
)";

const char* cubeVS = R"(
#version 330 core
layout(location=0) in vec3 aPos;
uniform mat4 view, projection;
out vec3 vPos;
void main() {
    vPos = aPos;
    gl_Position = projection * view * vec4(aPos, 1.0);
}
)";

const char* cubeFS = R"(
#version 330 core
in vec3 vPos;
uniform float alpha, time;
out vec4 FragColor;
void main() {
    float grid = sin(vPos.x * 1.8 + time * 0.6) * sin(vPos.y * 1.8 - time * 0.45)
               * sin(vPos.z * 1.8 + time * 0.52);
    float pulse = 0.82 + 0.18 * grid;
    vec3 col = vec3(0.42, 0.28, 0.18) * pulse;
    FragColor = vec4(col, alpha * 0.40 * pulse);
}
)";

const char* lineVS = R"(
#version 330 core
layout(location=0) in vec3 aPos;
layout(location=1) in vec3 aCol;
uniform mat4 view, projection, model;
uniform float time, linePulse, boltT;
uniform vec3 boltColor;
uniform int isBolt;
uniform int worldSpace;
out vec3 vCol;
out float vBright;
void main() {
    vCol = aCol;
    vBright = linePulse;
    if (isBolt == 1) {
        vCol = mix(aCol, boltColor, boltT);
        vBright = max(linePulse, boltT);
    }
    vec4 wp = worldSpace == 1 ? vec4(aPos, 1.0) : model * vec4(aPos, 1.0);
    gl_Position = projection * view * wp;
}
)";

const char* lineFS = R"(
#version 330 core
in vec3 vCol;
in float vBright;
uniform float time;
out vec4 FragColor;
void main() {
    float breathe = 0.93 + 0.07 * sin(time * 2.4 + vCol.r * 3.2 + vCol.b * 2.1);
    float warmAmt = clamp(0.12 - (vBright - 1.0) * 0.06, 0.02, 0.12);
    vec3 warm = mix(vCol, vec3(1.0, 0.98, 0.94), warmAmt);
    vec3 c = warm * (0.78 + 0.52 * vBright) * breathe;
    FragColor = vec4(clamp(c, 0.0, 1.45), 0.72 + 0.38 * clamp(vBright, 0.0, 2.2));
}
)";

GLuint compileShader(GLenum type, const char* src) {
    GLuint s = glCreateShader(type);
    glShaderSource(s, 1, &src, nullptr);
    glCompileShader(s);
    GLint ok; glGetShaderiv(s, GL_COMPILE_STATUS, &ok);
    if (!ok) { char log[512]; glGetShaderInfoLog(s,512,nullptr,log); printf("Shader: %s\n", log); }
    return s;
}

GLuint linkProgram(const char* vs, const char* fs) {
    GLuint p = glCreateProgram();
    glAttachShader(p, compileShader(GL_VERTEX_SHADER, vs));
    glAttachShader(p, compileShader(GL_FRAGMENT_SHADER, fs));
    glLinkProgram(p);
    return p;
}

void uploadDendriteBatch(const DendriteBatch& batch) {
    if (batch.verts.empty()) return;
    glBindVertexArray(dendVao);
    glBindBuffer(GL_ARRAY_BUFFER, dendVbo);
    glBufferData(GL_ARRAY_BUFFER, batch.verts.size() * sizeof(glm::vec3), batch.verts.data(), GL_DYNAMIC_DRAW);
    glBindBuffer(GL_ARRAY_BUFFER, dendCbo);
    glBufferData(GL_ARRAY_BUFFER, batch.cols.size() * sizeof(glm::vec3), batch.cols.data(), GL_DYNAMIC_DRAW);
    glBindBuffer(GL_ARRAY_BUFFER, dendVbo);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(glm::vec3), (void*)0);
    glEnableVertexAttribArray(0);
    glBindBuffer(GL_ARRAY_BUFFER, dendCbo);
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(glm::vec3), (void*)0);
    glEnableVertexAttribArray(1);
}

void uploadLinks() {
    glBindVertexArray(linkVao);
    if (linkLines.empty()) {
        glBindBuffer(GL_ARRAY_BUFFER, linkVbo);
        glBufferData(GL_ARRAY_BUFFER, 0, nullptr, GL_DYNAMIC_DRAW);
        glBindVertexArray(0);
        return;
    }
    glBindBuffer(GL_ARRAY_BUFFER, linkVbo);
    glBufferData(GL_ARRAY_BUFFER, linkLines.size() * sizeof(glm::vec3), linkLines.data(), GL_DYNAMIC_DRAW);
    glBindBuffer(GL_ARRAY_BUFFER, linkCbo);
    glBufferData(GL_ARRAY_BUFFER, linkColors.size() * sizeof(glm::vec3), linkColors.data(), GL_DYNAMIC_DRAW);
    glBindBuffer(GL_ARRAY_BUFFER, linkVbo);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(glm::vec3), (void*)0);
    glEnableVertexAttribArray(0);
    glBindBuffer(GL_ARRAY_BUFFER, linkCbo);
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(glm::vec3), (void*)0);
    glEnableVertexAttribArray(1);
    glBindVertexArray(0);
}

void drawDendriteBatch(const DendriteBatch& batch, const glm::mat4& view, const glm::mat4& proj, float pulse) {
    if (batch.verts.empty()) return;
    uploadDendriteBatch(batch);
    glUseProgram(lineProgram);
    glUniformMatrix4fv(glGetUniformLocation(lineProgram, "model"), 1, GL_FALSE, glm::value_ptr(batch.model));
    glUniformMatrix4fv(glGetUniformLocation(lineProgram, "view"), 1, GL_FALSE, glm::value_ptr(view));
    glUniformMatrix4fv(glGetUniformLocation(lineProgram, "projection"), 1, GL_FALSE, glm::value_ptr(proj));
    glUniform1f(glGetUniformLocation(lineProgram, "time"), globalTime);
    glUniform1f(glGetUniformLocation(lineProgram, "linePulse"), pulse);
    glUniform1i(glGetUniformLocation(lineProgram, "isBolt"), 0);
    glUniform1i(glGetUniformLocation(lineProgram, "worldSpace"), 0);
    glDepthMask(GL_FALSE);
    glBindVertexArray(dendVao);
    glDrawArrays(GL_LINES, 0, (GLsizei)batch.verts.size());
    glBindVertexArray(0);
    glDepthMask(GL_TRUE);
}

void uploadLines() {
    if (dendriteLines.empty()) {
        glBindBuffer(GL_ARRAY_BUFFER, dendVbo);
        glBufferData(GL_ARRAY_BUFFER, 0, nullptr, GL_DYNAMIC_DRAW);
        return;
    }
    glBindVertexArray(dendVao);
    glBindBuffer(GL_ARRAY_BUFFER, dendVbo);
    glBufferData(GL_ARRAY_BUFFER, dendriteLines.size()*sizeof(glm::vec3), dendriteLines.data(), GL_DYNAMIC_DRAW);
    glBindBuffer(GL_ARRAY_BUFFER, dendCbo);
    glBufferData(GL_ARRAY_BUFFER, dendriteColors.size()*sizeof(glm::vec3), dendriteColors.data(), GL_DYNAMIC_DRAW);
    glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,sizeof(glm::vec3),(void*)0);
    glEnableVertexAttribArray(0);
    glBindBuffer(GL_ARRAY_BUFFER, dendCbo);
    glVertexAttribPointer(1,3,GL_FLOAT,GL_FALSE,sizeof(glm::vec3),(void*)0);
    glEnableVertexAttribArray(1);
    glBindVertexArray(0);
}

void uploadBolts() {
    glBindVertexArray(boltVao);
    if (boltLines.empty()) {
        glBindBuffer(GL_ARRAY_BUFFER, boltVbo);
        glBufferData(GL_ARRAY_BUFFER, 0, nullptr, GL_DYNAMIC_DRAW);
        glBindVertexArray(0);
        return;
    }
    glBindBuffer(GL_ARRAY_BUFFER, boltVbo);
    glBufferData(GL_ARRAY_BUFFER, boltLines.size()*sizeof(glm::vec3), boltLines.data(), GL_DYNAMIC_DRAW);
    glBindBuffer(GL_ARRAY_BUFFER, boltCbo);
    glBufferData(GL_ARRAY_BUFFER, boltColors.size()*sizeof(glm::vec3), boltColors.data(), GL_DYNAMIC_DRAW);
    glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,sizeof(glm::vec3),(void*)0);
    glEnableVertexAttribArray(0);
    glBindBuffer(GL_ARRAY_BUFFER, boltCbo);
    glVertexAttribPointer(1,3,GL_FLOAT,GL_FALSE,sizeof(glm::vec3),(void*)0);
    glEnableVertexAttribArray(1);
    glBindVertexArray(0);
}

void initGL() {
    glewInit();
    glEnable(GL_DEPTH_TEST);
    glEnable(GL_BLEND);
    glEnable(GL_PROGRAM_POINT_SIZE);
    blobProgram = linkProgram(blobVS, blobFS);
    lineProgram = linkProgram(lineVS, lineFS);
    cubeProgram = linkProgram(cubeVS, cubeFS);
    tunnelProgram = linkProgram(tunnelVS, tunnelFS);
    starProgram = linkProgram(starVS, starFS);
    fadeProgram = linkProgram(fadeVS, fadeFS);
    generateSphere(sphereResolution);
    glGenVertexArrays(1,&vao); glGenBuffers(1,&vbo); glGenBuffers(1,&ebo);
    glBindVertexArray(vao);
    glBindBuffer(GL_ARRAY_BUFFER,vbo);
    glBufferData(GL_ARRAY_BUFFER,baseVertices.size()*sizeof(glm::vec3),baseVertices.data(),GL_STATIC_DRAW);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,ebo);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER,sphereIndices.size()*sizeof(unsigned),sphereIndices.data(),GL_STATIC_DRAW);
    glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,sizeof(glm::vec3),(void*)0);
    glEnableVertexAttribArray(0);
    glBindVertexArray(0);
    glGenVertexArrays(1,&dendVao); glGenBuffers(1,&dendVbo); glGenBuffers(1,&dendCbo);
    glGenVertexArrays(1,&linkVao); glGenBuffers(1,&linkVbo); glGenBuffers(1,&linkCbo);
    glGenVertexArrays(1,&boltVao); glGenBuffers(1,&boltVbo); glGenBuffers(1,&boltCbo);
    glGenVertexArrays(1,&cubeVao); glGenBuffers(1,&cubeVbo);
    glGenVertexArrays(1,&tunnelVao); glGenBuffers(1,&tunnelVbo);
    glGenVertexArrays(1,&starVao); glGenBuffers(1,&starVbo); glGenBuffers(1,&starCbo); glGenBuffers(1,&starDbo);
    glGenVertexArrays(1,&fadeVao); glGenBuffers(1,&fadeVbo);
    const float fadeQuad[] = { -1.f, -1.f, 1.f, -1.f, -1.f, 1.f, 1.f, 1.f };
    glBindVertexArray(fadeVao);
    glBindBuffer(GL_ARRAY_BUFFER, fadeVbo);
    glBufferData(GL_ARRAY_BUFFER, sizeof(fadeQuad), fadeQuad, GL_STATIC_DRAW);
    glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);
    glBindVertexArray(0);
    initNetwork();
    initStarField();
}

static float maxImpactRadiusOnFace(const glm::vec3& point, const glm::vec3& normal, float h) {
    glm::vec3 n = safeNorm(normal);
    int ax = fabsf(n.x) > 0.5f ? 0 : (fabsf(n.y) > 0.5f ? 1 : 2);
    int ay = (ax + 1) % 3;
    int az = (ax + 2) % 3;
    float margin = std::min(h - fabsf(point[ay]), h - fabsf(point[az]));
    return std::max(0.8f, margin * 0.92f);
}

static void buildCubeGeometry(float h, float stretchZ, std::vector<glm::vec3>& tris, std::vector<glm::vec3>& edges) {
    tris.clear();
    edges.clear();
    float backZ = -h - std::max(0.0f, stretchZ - 1.0f) * h * 2.4f;
    glm::vec3 c[8] = {
        {-h,-h,backZ},{-h,-h,h},{-h,h,backZ},{-h,h,h},
        { h,-h,backZ},{ h,-h,h},{ h,h,backZ},{ h,h,h}
    };
    auto quad = [&](int a, int b, int c0, int d) {
        tris.push_back(c[a]); tris.push_back(c[b]); tris.push_back(c[c0]);
        tris.push_back(c[a]); tris.push_back(c[c0]); tris.push_back(c[d]);
    };
    quad(4,5,7,6); quad(1,0,2,3); quad(5,1,3,7); quad(0,4,6,2); quad(3,2,6,7); quad(1,5,4,0);
    int ePairs[][2] = {{0,1},{1,3},{3,2},{2,0},{4,5},{5,7},{7,6},{6,4},{0,4},{1,5},{2,6},{3,7}};
    for (auto& e : ePairs) { edges.push_back(c[e[0]]); edges.push_back(c[e[1]]); }
}

static void drawTranslucentCube(const glm::mat4& view, const glm::mat4& proj, float h, float alpha, float stretchZ = 1.0f) {
    if (alpha < 0.02f) return;
    std::vector<glm::vec3> tris, edges;
    buildCubeGeometry(h, stretchZ, tris, edges);

    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    glEnable(GL_CULL_FACE);
    glCullFace(GL_BACK);
    glUseProgram(cubeProgram);
    glUniformMatrix4fv(glGetUniformLocation(cubeProgram, "view"), 1, GL_FALSE, glm::value_ptr(view));
    glUniformMatrix4fv(glGetUniformLocation(cubeProgram, "projection"), 1, GL_FALSE, glm::value_ptr(proj));
    glUniform1f(glGetUniformLocation(cubeProgram, "alpha"), alpha);
    glUniform1f(glGetUniformLocation(cubeProgram, "time"), globalTime);
    glDepthMask(GL_FALSE);
    glBindVertexArray(cubeVao);
    glBindBuffer(GL_ARRAY_BUFFER, cubeVbo);
    glBufferData(GL_ARRAY_BUFFER, tris.size() * sizeof(glm::vec3), tris.data(), GL_DYNAMIC_DRAW);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(glm::vec3), (void*)0);
    glEnableVertexAttribArray(0);
    glDrawArrays(GL_TRIANGLES, 0, (GLsizei)tris.size());
    glDisable(GL_CULL_FACE);

    glUseProgram(lineProgram);
    glUniformMatrix4fv(glGetUniformLocation(lineProgram, "view"), 1, GL_FALSE, glm::value_ptr(view));
    glUniformMatrix4fv(glGetUniformLocation(lineProgram, "projection"), 1, GL_FALSE, glm::value_ptr(proj));
    glUniform1f(glGetUniformLocation(lineProgram, "time"), globalTime);
    glUniform1f(glGetUniformLocation(lineProgram, "linePulse"), 1.35f);
    glUniform1i(glGetUniformLocation(lineProgram, "isBolt"), 0);
    glUniform1i(glGetUniformLocation(lineProgram, "worldSpace"), 1);
    std::vector<glm::vec3> ecols(edges.size(), glm::vec3(0.62f, 0.78f, 1.05f));
    glBindVertexArray(linkVao);
    glBindBuffer(GL_ARRAY_BUFFER, linkVbo);
    glBufferData(GL_ARRAY_BUFFER, edges.size() * sizeof(glm::vec3), edges.data(), GL_DYNAMIC_DRAW);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(glm::vec3), (void*)0);
    glEnableVertexAttribArray(0);
    glBindBuffer(GL_ARRAY_BUFFER, linkCbo);
    glBufferData(GL_ARRAY_BUFFER, ecols.size() * sizeof(glm::vec3), ecols.data(), GL_DYNAMIC_DRAW);
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(glm::vec3), (void*)0);
    glEnableVertexAttribArray(1);
    glLineWidth(2.8f);
    glDrawArrays(GL_LINES, 0, (GLsizei)edges.size());
    glBindVertexArray(0);
    glDepthMask(GL_TRUE);
}

static void drawImpactWaves(const glm::mat4& view, const glm::mat4& proj) {
    if (impactWaves.empty() || cagePhase != CAGE_FREED) return;
    std::vector<glm::vec3> verts, cols;
    auto addSeg = [&](const glm::vec3& a, const glm::vec3& b, const glm::vec3& ca, const glm::vec3& cb) {
        verts.push_back(a); verts.push_back(b);
        cols.push_back(ca); cols.push_back(cb);
    };

    for (const auto& w : impactWaves) {
        float life = 1.0f - w.age / 3.6f;
        if (life <= 0.0f) continue;

        glm::vec3 n = safeNorm(w.normal);
        glm::vec3 tangent = safeNorm(glm::cross(n, fabsf(n.y) < 0.85f ? glm::vec3(0, 1, 0) : glm::vec3(1, 0, 0)));
        glm::vec3 bitangent = glm::cross(n, tangent);
        float maxR = maxImpactRadiusOnFace(w.point, w.normal, cubeHalfExtent);
        float speedBoost = 1.0f + w.hitSpeed * 0.035f;
        float ripple = life * life;
        float baseCol = w.power * ripple;
        glm::vec3 hotCol = gLive.impactColor * baseCol;
        glm::vec3 coolCol = glm::vec3(0.55f, 0.78f, 1.08f) * baseCol * 0.72f;
        float surfaceOff = 0.018f;

        float imprintR = w.contactRadius > 0.01f
            ? std::min(w.contactRadius * 1.05f, maxR * 0.85f)
            : std::min(1.6f, maxR * 0.22f);
        float flash = clamp01(1.0f - w.age * 5.5f);
        if (flash > 0.02f) {
            int flashSegs = 14;
            float fr = imprintR * (0.55f + flash * 0.45f);
            glm::vec3 fc = glm::mix(hotCol, glm::vec3(1.0f, 0.96f, 0.88f), flash * 0.65f) * (0.85f + flash);
            for (int i = 0; i < flashSegs; ++i) {
                float ang = float(i) / float(flashSegs) * 6.28318f;
                float angN = float(i + 1) / float(flashSegs) * 6.28318f;
                glm::vec3 p0 = w.point + (cosf(ang) * tangent + sinf(ang) * bitangent) * fr + n * surfaceOff;
                glm::vec3 p1 = w.point + (cosf(angN) * tangent + sinf(angN) * bitangent) * fr + n * surfaceOff;
                addSeg(p0, p1, fc, fc * 0.35f);
            }
            glm::vec3 center = w.point + n * surfaceOff;
            for (int s = 0; s < 6; ++s) {
                float ang = float(s) / 6.0f * 6.28318f + w.age * 2.2f;
                glm::vec3 dir = cosf(ang) * tangent + sinf(ang) * bitangent;
                addSeg(center, center + dir * imprintR * 0.42f * flash,
                       fc * 1.15f, fc * 0.08f);
            }
        }

        int numSpokes = 12;
        float spokeGrow = std::min(w.age * (5.5f + w.hitSpeed * 0.22f) * speedBoost, maxR * 0.92f);
        for (int s = 0; s < numSpokes; ++s) {
            float ang = float(s) / float(numSpokes) * 6.28318f + hashf(float(s) * 2.17f + w.age) * 0.35f;
            glm::vec3 dir = cosf(ang) * tangent + sinf(ang) * bitangent;
            float wobble = 0.78f + 0.22f * sinf(ang * 4.0f + w.age * 9.0f);
            float len = spokeGrow * wobble * (0.35f + 0.65f * ripple);
            glm::vec3 p0 = w.point + n * surfaceOff;
            glm::vec3 p1 = w.point + dir * len + n * surfaceOff;
            glm::vec3 sc = glm::mix(coolCol, hotCol, 0.35f + 0.45f * ripple);
            addSeg(p0, p1, sc * 1.05f, sc * 0.04f);
        }

        for (int ring = 0; ring < 3; ++ring) {
            float ringT = clamp01(w.age * (1.35f + ring * 0.42f) * speedBoost - ring * 0.14f);
            if (ringT <= 0.01f) continue;
            float ringLife = clamp01((life - ring * 0.12f) / (1.0f - ring * 0.12f));
            float radius = std::min(ringT * (2.6f + w.power * 3.2f + ring * 1.1f), maxR)
                         * (0.42f + ringLife * ringLife * 0.58f);
            if (radius > maxR) continue;
            glm::vec3 rc = glm::mix(hotCol, coolCol, ring * 0.38f) * ringLife * (1.0f - ring * 0.22f);
            int segs = 28 + ring * 4;
            for (int i = 0; i < segs; ++i) {
                float ang = float(i) / float(segs) * 6.28318f;
                float angN = float(i + 1) / float(segs) * 6.28318f;
                float wobA = 1.0f + 0.06f * sinf(ang * 5.0f + w.age * 7.0f + ring);
                float wobB = 1.0f + 0.06f * sinf(angN * 5.0f + w.age * 7.0f + ring);
                glm::vec3 p0 = w.point + (cosf(ang) * tangent + sinf(ang) * bitangent) * (radius * wobA) + n * surfaceOff;
                glm::vec3 p1 = w.point + (cosf(angN) * tangent + sinf(angN) * bitangent) * (radius * wobB) + n * surfaceOff;
                addSeg(p0, p1, rc, rc * 0.12f);
            }
        }

        int sparks = 10;
        for (int k = 0; k < sparks; ++k) {
            float seed = hashf(float(k) * 4.91f + w.age * 0.7f);
            float ang = seed * 6.28318f;
            float dist = w.age * (3.8f + seed * 5.5f + w.hitSpeed * 0.18f);
            if (dist > maxR * 1.05f) continue;
            glm::vec3 dir = cosf(ang) * tangent + sinf(ang) * bitangent;
            glm::vec3 p0 = w.point + dir * imprintR * 0.35f + n * surfaceOff;
            glm::vec3 p1 = w.point + dir * dist + n * surfaceOff;
            float sparkLife = clamp01(1.0f - w.age * (1.6f + seed));
            if (sparkLife <= 0.02f) continue;
            glm::vec3 sc = glm::mix(hotCol, glm::vec3(1.0f), sparkLife * 0.45f) * sparkLife;
            addSeg(p0, p1, sc, sc * 0.02f);
        }
    }
    if (verts.empty()) return;
    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE);
    glUseProgram(lineProgram);
    glUniformMatrix4fv(glGetUniformLocation(lineProgram, "view"), 1, GL_FALSE, glm::value_ptr(view));
    glUniformMatrix4fv(glGetUniformLocation(lineProgram, "projection"), 1, GL_FALSE, glm::value_ptr(proj));
    glUniform1f(glGetUniformLocation(lineProgram, "time"), globalTime);
    glUniform1f(glGetUniformLocation(lineProgram, "linePulse"), 2.05f);
    glUniform1i(glGetUniformLocation(lineProgram, "isBolt"), 0);
    glUniform1i(glGetUniformLocation(lineProgram, "worldSpace"), 1);
    glDepthMask(GL_FALSE);
    glBindVertexArray(linkVao);
    glBindBuffer(GL_ARRAY_BUFFER, linkVbo);
    glBufferData(GL_ARRAY_BUFFER, verts.size() * sizeof(glm::vec3), verts.data(), GL_DYNAMIC_DRAW);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(glm::vec3), (void*)0);
    glEnableVertexAttribArray(0);
    glBindBuffer(GL_ARRAY_BUFFER, linkCbo);
    glBufferData(GL_ARRAY_BUFFER, cols.size() * sizeof(glm::vec3), cols.data(), GL_DYNAMIC_DRAW);
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(glm::vec3), (void*)0);
    glEnableVertexAttribArray(1);
    glLineWidth(5.0f);
    glDrawArrays(GL_LINES, 0, (GLsizei)verts.size());
    glBindVertexArray(0);
    glDepthMask(GL_TRUE);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
}

static void drawTunnel(const glm::mat4& view, const glm::mat4& proj, float fade) {
    if (fade < 0.02f) return;
    if (cagePhase < CAGE_TUNNEL && !vacuumSuck) return;
    if (cagePhase == CAGE_TUNNEL && tunnelPathDist < 0.5f && !vacuumSuck) return;
    float tubeRadius = tunnelTubeRadius();
    float tubeClear = std::max(tunnelProgress, cagePhase == CAGE_LAUNCH ? launchProgress * 0.35f : 0.0f);
    tubeRadius *= 1.0f + 0.04f * smootherstep(0.75f, 1.0f, tubeClear);
    const float ringSpacing = 2.75f;
    float camProg = vacuumSuck ? tunnelEntryBlend : tunnelProgress;
    float followDist = glm::mix(gLive.cameraFollowMin, gLive.cameraFollowMax, camProg);
    float pathAnchor = tunnelPathDist;
    float camPathS = pathAnchor - followDist;
    float behindLen = glm::mix(30.0f, 44.0f, tunnelProgress) * gLive.tunnelBehindMul;
    float aheadLen = glm::mix(95.0f, 165.0f, tunnelProgress) * gLive.tunnelAheadMul;
    float startS = floorf((camPathS - behindLen) / ringSpacing) * ringSpacing;
    float endS = ceilf((pathAnchor + aheadLen) / ringSpacing) * ringSpacing;
    if (endS - startS < ringSpacing * 24.0f)
        endS = startS + ringSpacing * 24.0f;
    int rings = int((endS - startS) / ringSpacing + 0.5f);
    rings = std::max(48, std::min(rings, 128));
    endS = startS + rings * ringSpacing;
    int segs = 24;
    std::vector<std::vector<glm::vec3>> ringPts(rings + 1);
    float span = std::max(ringSpacing, endS - startS);
    for (int ri = 0; ri <= rings; ++ri) {
        float s = startS + ri * ringSpacing;
        glm::vec3 center, tangent, normal, binormal;
        tunnelFrameAt(s, center, tangent, normal, binormal);
        float tNorm = (s - startS) / span;
        ringPts[ri].resize(segs);
        for (int si = 0; si < segs; ++si) {
            float ang = float(si) / float(segs) * 6.28318f;
            ringPts[ri][si] = center + (cosf(ang) * normal + sinf(ang) * binormal) * tubeRadius;
        }
    }
    std::vector<glm::vec3> wireVerts, wireCols;
    wireVerts.reserve(rings * segs * 4);
    wireCols.reserve(rings * segs * 4);
    auto addSeg = [&](const glm::vec3& a, const glm::vec3& b, float tNorm) {
        glm::vec3 col = glm::mix(gLive.tunnelWarm, gLive.tunnelHot, tNorm * 0.65f);
        col = glm::mix(col, gLive.tunnelCool, tNorm * 0.45f);
        col *= fade * (0.62f + 0.38f * std::max(tunnelProgress, vacuumSuck ? tunnelEntryBlend : 0.0f));
        wireVerts.push_back(a);
        wireVerts.push_back(b);
        wireCols.push_back(col);
        wireCols.push_back(col * 0.52f);
    };
    for (int ri = 0; ri <= rings; ++ri) {
        float tNorm = float(ri) / float(rings);
        for (int si = 0; si < segs; ++si)
            addSeg(ringPts[ri][si], ringPts[ri][(si + 1) % segs], tNorm);
    }
    const int longStep = 2;
    for (int si = 0; si < segs; si += longStep) {
        for (int ri = 0; ri < rings; ++ri) {
            float tNorm = float(ri) / float(rings);
            addSeg(ringPts[ri][si], ringPts[ri + 1][si], tNorm);
        }
    }
    if (wireVerts.empty()) return;
    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE);
    glUseProgram(lineProgram);
    glUniformMatrix4fv(glGetUniformLocation(lineProgram, "view"), 1, GL_FALSE, glm::value_ptr(view));
    glUniformMatrix4fv(glGetUniformLocation(lineProgram, "projection"), 1, GL_FALSE, glm::value_ptr(proj));
    glUniform1f(glGetUniformLocation(lineProgram, "time"), globalTime);
    glUniform1f(glGetUniformLocation(lineProgram, "linePulse"), 1.45f);
    glUniform1i(glGetUniformLocation(lineProgram, "isBolt"), 0);
    glUniform1i(glGetUniformLocation(lineProgram, "worldSpace"), 1);
    glDepthMask(GL_FALSE);
    glBindVertexArray(linkVao);
    glBindBuffer(GL_ARRAY_BUFFER, linkVbo);
    glBufferData(GL_ARRAY_BUFFER, wireVerts.size() * sizeof(glm::vec3), wireVerts.data(), GL_DYNAMIC_DRAW);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(glm::vec3), (void*)0);
    glEnableVertexAttribArray(0);
    glBindBuffer(GL_ARRAY_BUFFER, linkCbo);
    glBufferData(GL_ARRAY_BUFFER, wireCols.size() * sizeof(glm::vec3), wireCols.data(), GL_DYNAMIC_DRAW);
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(glm::vec3), (void*)0);
    glEnableVertexAttribArray(1);
    glLineWidth(1.6f);
    glDrawArrays(GL_LINES, 0, (GLsizei)wireVerts.size());
    glBindVertexArray(0);
    glDepthMask(GL_TRUE);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
}

static void drawWhiteFade(float fade) {
    if (fade < 0.005f) return;
    glDisable(GL_DEPTH_TEST);
    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    glUseProgram(fadeProgram);
    glUniform1f(glGetUniformLocation(fadeProgram, "fade"), fade);
    glBindVertexArray(fadeVao);
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
    glBindVertexArray(0);
    glEnable(GL_DEPTH_TEST);
}

static void drawStarField(const glm::mat4& view, const glm::mat4& proj, const glm::vec3& anchor) {
    if (starFieldReveal < 0.01f || starField.empty()) return;
    glDisable(GL_DEPTH_TEST);
    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE);
    glDepthMask(GL_FALSE);
    glUseProgram(starProgram);
    glUniformMatrix4fv(glGetUniformLocation(starProgram, "view"), 1, GL_FALSE, glm::value_ptr(view));
    glUniformMatrix4fv(glGetUniformLocation(starProgram, "projection"), 1, GL_FALSE, glm::value_ptr(proj));
    glUniform3fv(glGetUniformLocation(starProgram, "starAnchor"), 1, glm::value_ptr(anchor));
    glUniform1f(glGetUniformLocation(starProgram, "time"), globalTime);
    glUniform1f(glGetUniformLocation(starProgram, "reveal"), starFieldReveal);
    glUniform1f(glGetUniformLocation(starProgram, "blobReachPulse"), starBlobReachPulse());
    glUniform1f(glGetUniformLocation(starProgram, "starSwirl"), starSwirlAmount());
    glBindVertexArray(starVao);
    glDrawArrays(GL_POINTS, 0, (GLsizei)starField.size());
    glBindVertexArray(0);
    glDepthMask(GL_TRUE);
    glEnable(GL_DEPTH_TEST);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
}

glm::vec3 narrativeShapeBlend(float t) {
    if (t >= gLive.networkStart) return glm::vec3(0.88f, 0.08f, 0.04f);
    float ts = (t / gLive.networkStart) * 30.0f;
    ts = fmodf(ts, 30.0f);
    glm::vec3 r(0);
    if (ts < 5) r.x = 1;
    else if (ts < 11) { float p=smootherstep(5,11,ts); r.x=1-p*0.55f; r.y=p*0.75f; }
    else if (ts < 17) { float p=smootherstep(11,17,ts); r.x=0.45f*(1-p*0.6f); r.y=0.75f*(1-p*0.5f)+0.15f; r.z=p*0.85f; }
    else if (ts < 23) { float p=smootherstep(17,23,ts); r.x=0.18f*(1-p); r.y=0.4f*(1-p); r.z=0.85f+0.15f*sinf(p*3.14159f); }
    else { float p=smootherstep(23,30,ts); r.z=(1-p)*0.55f; r.y=(1-p)*0.25f; r.x=0.18f+p*0.82f; }
    r.x = std::max(r.x, 0.22f);
    float sum = r.x + r.y + r.z;
    return sum > 0.001f ? r / sum : glm::vec3(1,0,0);
}

void followEnvelope(float& e, float t, float dt, float atk, float rel) {
    e += (t - e) * std::min(1.0f, ((t > e) ? atk : rel) * dt);
}

void onAudioFeatures(const AudioAnalysis::AudioFeatures& f, double) {
    std::lock_guard<std::mutex> lock(audioMutex);
    bassTarget = clamp01(float(f.freq_bands.bass));
    midTarget = clamp01(float(f.freq_bands.mid));
    lowMidTarget = clamp01(float(f.freq_bands.low_mid));
    highMidTarget = clamp01(float(f.freq_bands.high_mid));
    trebleTarget = clamp01(float(f.freq_bands.treble));
    energyTarget = clamp01(float(f.energy));
    centroidTarget = clamp01(float(f.spectral_centroid / 3000.0));
    if (f.beat_detected) beatAccent = std::max(beatAccent, clamp01(float(f.beat_strength)));
}

void updateProceduralTargets(float t) {
    float beatHz = gLive.bpm / 60, beatPhase = t * beatHz;
    float beatWave = 0.5f+0.5f*sinf(beatPhase*6.28318f);
    bassTarget = clamp01(0.55f*beatWave + 0.25f*(0.5f+0.5f*sinf(t*0.37f)));
    midTarget = clamp01(0.4f*(0.5f+0.5f*sinf(t*2.3f)));
    lowMidTarget = clamp01(0.45f*(0.5f+0.5f*sinf(t*1.6f))+0.25f*midTarget);
    highMidTarget = clamp01(0.4f*(0.5f+0.5f*sinf(t*4.1f))+0.2f*midTarget);
    trebleTarget = clamp01(0.3f*(0.5f+0.5f*sinf(t*13.1f)));
    energyTarget = clamp01(0.35f*bassTarget+0.35f*midTarget+0.3f*trebleTarget);
    centroidTarget = clamp01(0.5f+0.5f*sinf(t*0.9f));
    if (beatPhase-floorf(beatPhase) < prevBeatPhase-floorf(prevBeatPhase)) beatAccent = 1;
    prevBeatPhase = beatPhase;
}

void updateAudioEnvelopes(float dt) {
    if (!useMp3Audio) updateProceduralTargets(globalTime);
    followEnvelope(bassEnv,bassTarget,dt,4,2);
    followEnvelope(midEnv,midTarget,dt,5,3);
    followEnvelope(lowMidEnv,lowMidTarget,dt,5,3);
    followEnvelope(highMidEnv,highMidTarget,dt,6,4);
    followEnvelope(trebleEnv,trebleTarget,dt,8,5);
    followEnvelope(energyEnv,energyTarget,dt,3.5f,2.5f);
    followEnvelope(centroidEnv,centroidTarget,dt,4,3);
    beatAccent *= expf(-dt*7);
}

static void updateAudioFade() {
    if (!audioPlayer || !useMp3Audio) return;

    if (narrativeFinished) {
        if (audioPlayer->get_playback_state())
            audioPlayer->pause();
        audioPlayer->set_volume(0.0);
        return;
    }

    if (finaleWhiteFade > 0.0f)
        audioPlayer->set_volume(double(std::max(0.0f, 1.0f - finaleWhiteFade)));
    else if (audioPlayer->get_volume() < 1.0)
        audioPlayer->set_volume(1.0);
}

void updateBlobMotion(float dt) {
    float zoomPull = networkViewActive ? 1.0f : networkZoomEase(narrativeClock);
    if (zoomPull > 0.01f) {
        blobCenter += (glm::vec2(0) - blobCenter) * std::min(1.0f, 2.5f * dt * zoomPull);
        blobDepth += (0.0f - blobDepth) * std::min(1.0f, 2.5f * dt * zoomPull);
    }
    if (networkViewActive) return;
    if (networkEmergence() > 0.38f && coreBlobReveal > 0.78f) return;
    float t = globalTime;
    float ampX = 1.4f + 1.5f*midEnv + 1.0f*bassEnv;
    float ampY = 1.2f + 1.3f*bassEnv + 1.0f*midEnv;
    blobCenter += (glm::vec2(
        ampX*sinf(t*0.38f) + 0.7f*cosf(t*0.71f),
        ampY*cosf(t*0.31f) + 0.6f*sinf(t*0.64f)) - blobCenter) * std::min(1.0f, 3.0f*dt);
    blobDepth += (0.2f + 0.6f*bassEnv - blobDepth) * std::min(1.0f, 3.0f*dt);
    float scaleTarget = 1.35f + 0.4f*bassEnv + 0.15f*dendriteGrowth;
    blobScale += (scaleTarget - blobScale) * std::min(1.0f, 3.0f*dt);
}

void updateNetworkMotion(float dt) {
    float motionBlend = networkViewActive ? 1.0f : std::max(networkBlend, networkZoomEase(narrativeClock));
    if (motionBlend < 0.02f) return;
    float t = globalTime;
    float musicAmp = 0.65f + musicDrive * 0.85f + beatAccent * 0.55f;
    for (size_t i = 0; i < network.size(); ++i) {
        NeuronNode& n = network[i];
        if (!n.active) continue;
        float ph = n.dendritePhase;
        glm::vec3 target(0);
        switch (n.role) {
            case ROLE_UP_R:
                target = glm::vec3(0.07f * sinf(t * 0.39f + ph), 0.06f * cosf(t * 0.31f + ph), 0.02f * sinf(t * 0.62f));
                target += glm::vec3(0.05f * bassEnv * sinf(t * 1.1f + ph), 0, 0.03f * beatAccent * cosf(t * 2.0f + ph));
                break;
            case ROLE_UP_G:
                target = glm::vec3(0.05f * cosf(t * 0.47f + ph), 0.08f * sinf(t * 0.29f + ph), 0.03f * cosf(t * 0.55f));
                target += glm::vec3(0, 0.06f * midEnv * sinf(t * 0.9f + ph), 0.04f * lowMidEnv * cosf(t * 1.3f + ph));
                break;
            case ROLE_UP_B:
                target = glm::vec3(0.06f * sinf(t * 0.58f + ph), 0.04f * cosf(t * 0.44f + ph), 0.05f * sinf(t * 0.71f));
                target += glm::vec3(0.04f * trebleEnv * cosf(t * 1.8f + ph), 0.03f * highMidEnv * sinf(t * 2.2f + ph), 0);
                break;
            case ROLE_MIDDLE:
                target = glm::vec3(0.04f * sinf(t * 0.25f + ph), 0.03f * cosf(t * 0.33f + ph), 0.025f * sinf(t * 0.41f));
                target += glm::vec3(0.04f * midEnv * cosf(t * 0.7f + ph), 0.03f * bassEnv * sinf(t * 0.55f + ph), 0);
                break;
            case ROLE_DOWNSTREAM:
                target = glm::vec3(0.035f * cosf(t * 0.22f + ph), -0.05f * sinf(t * 0.27f + ph), 0.03f * cosf(t * 0.36f));
                target += glm::vec3(0.03f * trebleEnv, -0.05f * bassEnv * sinf(t * 0.8f + ph), 0.02f * centroidEnv);
                break;
            default: break;
        }
        target *= musicAmp;
        n.drift += (target * motionBlend - n.drift) * std::min(1.0f, 3.5f * dt);
        glm::vec3 normal = safeNorm(shellWorldPos(n, (int)i));
        if (glm::length(normal) > 0.01f)
            n.drift -= normal * glm::dot(n.drift, normal);
    }
}

void updateDendriteLightning(float dt) {
    float lightInput = trebleEnv*0.42f + highMidEnv*0.33f + centroidEnv*0.25f + 0.12f;
    lightExposure += (lightInput - lightExposure) * std::min(1.0f, 6.0f*dt);
    lightCharge += lightExposure * dt * 0.85f;
    lightCharge -= dt * 0.08f;
    lightCharge = clamp01(lightCharge);
    float neuronMorph = smootherstep(DENDRITE_ONSET, gLive.networkStart - 1.0f, narrativeClock);
    float growthTarget = std::max(lightCharge, neuronMorph * 0.85f) * (0.5f + 0.5f*lightExposure);
    dendriteGrowth += (growthTarget - dendriteGrowth) * std::min(1.0f, 4.0f*dt);
    if (networkBlend < 0.3f && lightCharge >= 0.98f) {
        lightningPulse = 1.0f; lightCharge = 0; dendriteGrowth *= 0.35f;
    }
    lightningPulse *= expf(-dt*12);
}

void buildBoltSegment(glm::vec3 a, glm::vec3 b, glm::vec3 col, float wobble) {
    glm::vec3 mid = glm::mix(a, b, 0.5f) + glm::vec3(sinf(wobble)*0.2f, cosf(wobble*1.3f)*0.15f, 0);
    boltLines.push_back(a); boltColors.push_back(col);
    boltLines.push_back(mid); boltColors.push_back(col * 1.2f);
    boltLines.push_back(mid); boltColors.push_back(col * 1.2f);
    boltLines.push_back(b); boltColors.push_back(col);
}

void addBolt(int from, int to, glm::vec3 color) {
    ActiveBolt b;
    b.from = from; b.to = to; b.color = color;
    activeBolts.push_back(b);
}

void updateNetworkSignals(float dt) {
    float net = networkViewActive ? 1.0f : networkBlend;
    if (net < 0.35f || network.size() < 7) return;

    float beat = 0.30f + 0.70f * std::max(beatAccent, bassEnv * 0.65f);
    float spike = 1.0f + beatAccent * 2.2f + musicDrive * 0.6f;

    network[coreR].signal = roleBaseColor(ROLE_UP_R) * (0.25f + bassEnv * 0.75f) * beat * spike;
    network[coreG].signal = roleBaseColor(ROLE_UP_G) * (0.25f + midEnv * 0.65f + lowMidEnv * 0.15f) * beat * spike;
    network[coreB].signal = roleBaseColor(ROLE_UP_B) * (0.25f + trebleEnv * 0.55f + highMidEnv * 0.25f) * beat * spike;

    glm::vec3 m0 = additiveCombine(network[coreR].signal, network[coreG].signal);
    glm::vec3 m1 = additiveCombine(network[coreG].signal, network[coreB].signal);
    network[coreM0].signal = m0 * (0.85f + midEnv * 0.25f);
    network[coreM1].signal = m1 * (0.85f + midEnv * 0.25f);
    network[coreD0].signal = prismSubtract(m0) * (0.80f + trebleEnv * 0.30f);
    network[coreD1].signal = prismSubtract(m1) * (0.80f + trebleEnv * 0.30f);

    float coRG = std::min(glm::length(network[coreR].displaySignal), glm::length(network[coreG].displaySignal));
    float coGB = std::min(glm::length(network[coreG].displaySignal), glm::length(network[coreB].displaySignal));
    if (network[coreM0].active) network[coreM0].pulse += coRG * (0.20f + beatAccent * 0.45f);
    if (network[coreM1].active) network[coreM1].pulse += coGB * (0.20f + beatAccent * 0.45f);
    if (network[coreD0].active) network[coreD0].pulse += glm::length(network[coreM0].displaySignal) * (0.12f + trebleEnv * 0.35f);
    if (network[coreD1].active) network[coreD1].pulse += glm::length(network[coreM1].displaySignal) * (0.12f + trebleEnv * 0.35f);

    for (size_t i = 0; i < network.size(); ++i) {
        if (network[i].spawnOrder < 7 || !network[i].active) continue;
        glm::vec3 blend(0);
        int count = 0;
        for (int nb : shellNeighbors[i]) {
            if (nb < 0 || nb >= (int)network.size() || !network[nb].active) continue;
            blend += network[nb].displaySignal;
            count++;
        }
        if (count > 0)
            network[i].signal = blend / float(count) * (0.55f + musicDrive * 0.45f);
        else
            network[i].signal = glm::vec3(0.35f, 0.40f, 0.48f) * (0.15f + beat * 0.25f);
    }

    float smooth = 3.5f + musicDrive * 4.0f;
    for (auto& n : network) {
        if (!n.active) {
            n.signal = glm::vec3(0);
            n.displaySignal *= std::max(0.0f, 1.0f - dt * 5.0f);
            continue;
        }
        n.displaySignal += (n.signal - n.displaySignal) * std::min(1.0f, smooth * dt);
        float pulseTarget = glm::length(n.displaySignal) * (0.50f + 0.50f * beat);
        n.pulse += (pulseTarget - n.pulse) * std::min(1.0f, 5.0f * dt);
        n.color = n.displaySignal;
        n.flowPhase += dt * (0.4f + musicDrive * 1.2f + beatAccent * 2.5f);
    }
}

void updateNetworkCascade(float dt) {
    if (networkBlend < 0.08f && !networkViewActive) {
        activeBolts.clear();
        boltProgress = 0.0f;
        if (!boltLines.empty()) {
            boltLines.clear();
            boltColors.clear();
            uploadBolts();
        }
        return;
    }

    if (networkBlend < 0.5f) return;

    if (networkShellRetireFade() < 0.08f && !networkViewActive) {
        activeBolts.clear();
        boltProgress = 0.0f;
        if (!boltLines.empty()) {
            boltLines.clear();
            boltColors.clear();
            uploadBolts();
        }
        return;
    }

    float drainNodeAppear = networkNodeAppearEstimate();
    float drainElectrocute = soloElectrocuteBlend(drainNodeAppear, false);
    float drainZoom = networkViewActive ? 1.0f : networkZoomEase(narrativeClock);
    ProtagonistDraw drainProta = computeProtagonistDraw(drainZoom, drainElectrocute, 0.0f);
    if (soloDrainPhaseActive(drainElectrocute, drainProta.soloAlpha) && drainProta.layerMix < 0.38f) {
        activeBolts.clear();
        boltProgress = 0.0f;
        if (!boltLines.empty()) {
            boltLines.clear();
            boltColors.clear();
            uploadBolts();
        }
        return;
    }

    static float beatBoltCooldown = 0.0f;
    beatBoltCooldown -= dt;
    if (beatAccent > 0.58f && beatBoltCooldown <= 0.0f) {
        beatBoltCooldown = 0.12f;
        int edge = int(globalTime * 4.0f) % 6;
        switch (edge) {
            case 0: addBolt(coreR, coreM0, network[coreR].displaySignal); break;
            case 1: addBolt(coreG, coreM0, network[coreG].displaySignal); break;
            case 2: addBolt(coreB, coreM1, network[coreB].displaySignal); break;
            case 3: addBolt(coreM0, coreD0, network[coreM0].displaySignal); break;
            case 4: addBolt(coreM1, coreD1, network[coreM1].displaySignal); break;
            case 5: addBolt(coreR, coreG, additiveCombine(network[coreR].displaySignal, network[coreG].displaySignal)); break;
        }
    }

    cascadeTimer += dt;
    const float stepDur = 2.8f - musicDrive * 0.8f;

    if (cascadeTimer >= stepDur) {
        cascadeTimer = 0;
        cascadeStep = (cascadeStep + 1) % 6;
        activeBolts.clear();
        boltProgress = 0;

        switch (cascadeStep) {
            case 0: addBolt(coreR, coreM0, network[coreR].displaySignal); break;
            case 1: addBolt(coreG, coreM0, network[coreG].displaySignal); addBolt(coreG, coreM1, network[coreG].displaySignal); break;
            case 2: addBolt(coreB, coreM1, network[coreB].displaySignal); break;
            case 3: addBolt(coreM0, coreD0, network[coreM0].displaySignal); break;
            case 4: addBolt(coreM1, coreD1, network[coreM1].displaySignal); break;
            case 5: lightningPulse = 0.55f + musicDrive * 0.35f; break;
        }
    }

    boltProgress = std::min(1.0f, boltProgress + dt * (1.8f + musicDrive * 2.5f + beatAccent * 2.0f));

    boltLines.clear(); boltColors.clear();
    float netScale = networkBlobScale();
    float narrativeT = narrativeClock >= gLive.networkStart ? 1.0f
        : fminf(narrativeClock, gLive.networkStart) / gLive.networkStart;
    for (const auto& bolt : activeBolts) {
        if (bolt.from < 0 || bolt.to >= (int)network.size()) continue;
        if (!network[bolt.from].active || !network[bolt.to].active) continue;
        glm::mat4 modelA = neuronModelMatrix(network[bolt.from], 1.0f, netScale, bolt.from);
        glm::mat4 modelB = neuronModelMatrix(network[bolt.to], 1.0f, netScale, bolt.to);
        BlobDeformParams pA = deformParamsForNeuron(network[bolt.from], narrativeT);
        BlobDeformParams pB = deformParamsForNeuron(network[bolt.to], narrativeT);
        glm::vec3 aSurf = worldSurfaceToward(modelA, modelB, pA, pB);
        glm::vec3 bSurf = worldSurfaceToward(modelB, modelA, pB, pA);
        glm::vec3 end = glm::mix(aSurf, bSurf, boltProgress);
        buildBoltSegment(aSurf, end, bolt.color, globalTime * 9.0f + bolt.from * 3.0f);
    }
    uploadBolts();
}

static void buildTornLinkFragments(float scatterAmt) {
    if (scatterAmt < 0.22f || shellBurstVel.size() != network.size()) return;
    for (size_t i = 0; i < network.size(); ++i) {
        const NeuronNode& n = network[i];
        if (!n.active || n.growth < 0.12f) continue;
        glm::vec3 vel = shellBurstVel[i];
        if (glm::length(vel) < 0.5f) continue;
        glm::vec3 p = shellWorldPos(n, (int)i);
        glm::vec3 dir = safeNorm(vel);
        float len = (2.5f + hashf(float(i) * 2.17f) * 9.0f) * scatterAmt;
        glm::vec3 col = dendriteColorFor(n) * scatterAmt * (0.85f + musicDrive * 0.25f);
        linkLines.push_back(p);
        linkLines.push_back(p + dir * len);
        linkColors.push_back(col * 0.55f);
        linkColors.push_back(col * 0.12f);
    }
}

static float soloGreyDrainBlend(float electrocuteBase) {
    if (electrocuteBase < 0.015f) return 0.0f;
    float cover = network.empty() ? 0.0f
        : float(std::max(0, learnedNodeCount - 2)) / float(std::max(1, (int)network.size() - 2));
    float fromNodes = smootherstep(0.02f, 0.38f, cover);
    float fromShock = smootherstep(0.04f, 0.52f, electrocuteBase);
    return clamp01(fromShock * (0.82f + fromNodes * 0.18f));
}

static float soloGreyBlend(float electrocuteDrain = 0.0f) {
    float early = 0.0f;
    if (!networkViewActive && networkBlend <= 0.05f && cagePhase <= CAGE_INTACT)
        early = 1.0f - smootherstep(DENDRITE_ONSET - 1.5f, DENDRITE_ONSET + 4.5f, narrativeClock);
    return clamp01(std::max(early, soloGreyDrainBlend(electrocuteDrain)));
}

static float networkNodeAppearEstimate() {
    float shellFade = 1.0f - shellRupture * 0.96f;
    if (cagePhase >= CAGE_TUNNEL) shellFade = 0.0f;
    float retireFade = networkShellRetireFade();
    float netAppearBase = (networkViewActive ? 1.0f : networkShellStage()) * shellFade * retireFade;
    float nodeAppear = 0.0f, linkAppear = 0.0f, scatterAmt = 0.0f;
    networkDrawAppears(netAppearBase, nodeAppear, linkAppear, scatterAmt);
    if (!networkViewActive && networkShellStage() < 0.02f && cagePhase <= CAGE_INTACT)
        nodeAppear = 0.0f;
    return nodeAppear;
}

static float soloElectrocuteBlend(float nodeAppear, bool applyShock) {
    if (networkViewActive) return 0.0f;
    if (cagePhase > CAGE_STRUGGLE) return 0.0f;

    if (cagePhase < CAGE_STRUGGLE) {
        if (networkEmergence() < NET_E_ELECT_ON || nodeAppear < 0.14f) return 0.0f;
        if (learnedNodeCount < 3) return 0.0f;
    } else if (nodeAppear < 0.05f) {
        return 0.0f;
    }

    float spawnRise = smootherstep(0.06f, 0.82f, (float(learnedNodeCount) - 2.0f) / 10.0f);
    float appear = (cagePhase == CAGE_STRUGGLE)
        ? std::max(0.78f, smootherstep(0.05f, 0.38f, nodeAppear))
        : smootherstep(0.14f, 0.52f, nodeAppear);
    float netRise = (cagePhase == CAGE_STRUGGLE)
        ? 1.0f
        : networkElectrocuteStage();
    float struggleBoost = (0.88f + cagePushAmt * 0.45f + cageStruggle * 0.18f);
    float shock = applyShock ? (0.88f + beatAccent * 0.42f + bassEnv * 0.22f) : 1.0f;
    float blend = std::max(spawnRise, 0.80f);
    return clamp01(blend * appear * netRise * shock * struggleBoost);
}

static bool soloDrainPhaseActive(float electrocute, float soloFade) {
    return electrocute > 0.04f && soloFade > 0.10f && cagePhase <= CAGE_STRUGGLE;
}

static glm::mat4 soloBlobModelMatrix(const glm::vec3& pos, float drawScale,
                                     float rotSpeed, float tilt, float beatSpin) {
    glm::mat4 model = glm::translate(glm::mat4(1.0f), pos);
    model = glm::rotate(model, globalTime * rotSpeed + beatSpin, glm::vec3(0, 1, 0));
    model = glm::rotate(model, tilt, glm::vec3(1, 0, 0.3f));
    model = glm::scale(model, glm::vec3(drawScale));
    return model;
}

static void buildSoloElectrocuteLinks(float electrocute, float netScale, float nodeAppear,
                                      float narrativeT, float rotSpeed, float tilt, float beatSpin,
                                      const glm::vec3& protaPos, float protaScale) {
    if (electrocute < 0.03f) return;
    float drawScale = protaScale;
    glm::mat4 soloModel = soloBlobModelMatrix(protaPos, drawScale, rotSpeed, tilt, beatSpin);
    BlobDeformParams soloDeform = deformParamsFor(shapeBlend, 0.0f, narrativeT);
    float linkBright = std::min(5.5f, electrocute * 2.45f + 0.85f) * gLive.linkBrightness;
    float shock = 0.22f + beatAccent * 1.05f + bassEnv * 0.55f + trebleEnv * 0.35f;
    const glm::vec3 whiteHot(1.0f, 0.99f, 0.97f);
    const glm::vec3 whiteCore(0.90f, 0.94f, 1.0f);
    float fatScale = 1.65f + electrocute * 0.85f;

    for (size_t ni = 0; ni < network.size(); ++ni) {
        const NeuronNode& n = network[ni];
        if (!n.active && n.growth < 0.04f) continue;
        float nodeFade = nodeAppear * smootherstep(0.0f, 1.0f, n.growth);
        if (nodeFade < 0.04f) continue;

        glm::mat4 nodeModel = neuronModelMatrix(n, nodeFade, netScale, (int)ni);
        BlobDeformParams nodeDeform = deformParamsForNeuron(n, narrativeT);
        glm::vec3 from = worldSurfaceToward(soloModel, nodeModel, soloDeform, nodeDeform);
        glm::vec3 to = worldSurfaceToward(nodeModel, soloModel, nodeDeform, soloDeform);

        glm::vec3 linkCol = n.displaySignal;
        if (glm::length(linkCol) < 0.05f)
            linkCol = roleBaseColor(n.role) * (0.40f + musicDrive * 0.75f);
        linkCol = glm::mix(glm::mix(linkCol, whiteCore, 0.62f), whiteHot, 0.78f + beatAccent * 0.12f);
        linkCol = additiveCombine(linkCol, whiteHot * (0.42f + electrocute * 0.35f + trebleEnv * 0.18f));

        float strength = std::max(glm::length(n.displaySignal), 0.18f + shock * 0.65f)
                       * linkBright * nodeFade * (0.72f + n.pulse * 0.42f) * (1.95f + electrocute * 0.55f);
        if (n.spawnOrder >= 3 && n.growth < 0.98f)
            strength *= 1.0f + (1.0f - n.growth) * 0.45f;
        else if (n.spawnOrder < 7)
            strength *= 1.12f;
        buildSignalLink(from, to, linkCol, n.flowPhase + float(ni) * 0.17f, strength, fatScale);
    }
}

static void buildSoloElectDendrites(float electrocute, float narrativeT,
                                    float rotSpeed, float tilt, float beatSpin,
                                    const glm::vec3& protaPos, float protaScale) {
    soloElectDendBatch.verts.clear();
    soloElectDendBatch.cols.clear();
    if (electrocute < 0.04f || dendriteGrowth < 0.12f) return;
    soloElectDendBatch.model = soloBlobModelMatrix(protaPos, protaScale, rotSpeed, tilt, beatSpin);
    glm::vec3 col(0.45f + lightExposure * 0.14f, 0.52f + lightCharge * 0.12f, 0.60f + lightExposure * 0.18f);
    col = additiveCombine(col, gLive.linkWarm * electrocute * 0.35f);
    BlobDeformParams deform = deformParamsFor(shapeBlend, 0.0f, narrativeT);
    float growth = dendriteGrowth * (0.55f + electrocute * 0.65f);
    buildBlobDendrites(soloElectDendBatch, ROLE_SOLO, growth, 42.0f, col, deform);
}

void rebuildDendriteGeometry(float nodeAppear, float linkAppear, float scatterAmt,
                             float netScale, float soloFade, float layerMix,
                             const glm::vec3& protaPos, float protaScale,
                             float rotSpeed, float tilt, float narrativeT, float beatSpin) {
    dendriteBatches.clear();
    linkLines.clear();
    linkColors.clear();

    float netDend = networkViewActive ? 1.0f : clamp01(networkShellStage() * networkShellRetireFade());
    bool showNetwork = (networkViewActive || networkShellStage() > 0.02f)
        && networkShellRetireFade() > 0.02f;
    float nodeAppearEst = networkNodeAppearEstimate();
    float electrocute = soloElectrocuteBlend(nodeAppearEst, true);
    soloElectDendBatch.verts.clear();
    soloElectDendBatch.cols.clear();

    if (showNetwork && (nodeAppear > 0.02f || linkAppear > 0.02f || electrocute > 0.03f) && netDend > 0.02f) {
        if (nodeAppear > 0.02f && layerMix > 0.18f) {
            float g = (0.45f + dendriteGrowth * 0.55f) * netDend;
            for (size_t ni = 0; ni < network.size(); ++ni) {
                const auto& n = network[ni];
                if (!n.active && n.growth < 0.03f) continue;
                DendriteBatch batch;
                batch.model = neuronModelMatrix(n, nodeAppear, netScale, (int)ni);
                glm::vec3 col = dendriteColorFor(n);
                BlobDeformParams deform = deformParamsForNeuron(n, narrativeT);
                buildBlobDendrites(batch, n.role, g, n.dendritePhase, col, deform);
                dendriteBatches.push_back(std::move(batch));
            }
        }

        if (linkAppear > 0.02f && layerMix > 0.10f) {
            float linkBright = std::min(2.35f, linkAppear);
            for (size_t ni = 0; ni < network.size(); ++ni) {
                const NeuronNode& n = network[ni];
                if (!n.active || n.growth < 0.12f) continue;
                float strength = std::max(glm::length(n.displaySignal), 0.10f + musicDrive * 0.48f) * linkBright;
                for (int outIdx : n.outputs) {
                    if (outIdx < 0 || outIdx >= (int)network.size()) continue;
                    const NeuronNode& tgt = network[outIdx];
                    if (!tgt.active || tgt.growth < 0.12f) continue;
                    glm::vec3 linkCol = n.displaySignal;
                    if (glm::length(linkCol) < 0.05f)
                        linkCol = roleBaseColor(n.role) * (0.25f + musicDrive * 0.5f);
                    if (tgt.role == ROLE_MIDDLE)
                        linkCol = additiveCombine(linkCol, tgt.displaySignal * 0.35f);
                    else if (tgt.role == ROLE_DOWNSTREAM) {
                        linkCol = prismSubtract(linkCol) + glm::vec3(0.12f, 0.02f, 0.18f);
                        strength *= 1.25f;
                    }
                    addNetworkLink((int)ni, outIdx, linkAppear, netScale, narrativeT,
                                   linkCol, n.flowPhase + outIdx * 0.7f, strength);
                }
            }

            float weave = 0.28f + musicDrive * 0.42f + beatAccent * 0.32f;
            for (size_t ni = 0; ni < network.size(); ++ni) {
                const NeuronNode& na = network[ni];
                if (!na.active || na.growth < 0.12f) continue;
                for (int nb : shellNeighbors[ni]) {
                    if (nb <= (int)ni || nb >= (int)network.size()) continue;
                    const NeuronNode& nbNode = network[nb];
                    if (!nbNode.active || nbNode.growth < 0.12f) continue;
                    glm::vec3 col = additiveCombine(na.displaySignal, nbNode.displaySignal) * 0.62f;
                    if (glm::length(col) < 0.04f)
                        col = glm::vec3(0.72f, 0.48f, 0.28f) * weave;
                    float ph = na.flowPhase + nbNode.flowPhase * 0.5f;
                    addNetworkLink((int)ni, nb, linkAppear, netScale, narrativeT,
                                   col, ph + ni * 0.11f + nb * 0.07f, weave * linkBright);
                }
            }
        }

        buildTornLinkFragments(scatterAmt);
        if (soloFade > 0.02f || electrocute > 0.03f) {
            float dendBoost = std::max(electrocute, soloFade * 0.55f);
            if (electrocute > 0.03f)
                buildSoloElectrocuteLinks(electrocute, netScale, nodeAppear, narrativeT,
                                          rotSpeed, tilt, beatSpin, protaPos, protaScale);
            if (dendBoost > 0.04f)
                buildSoloElectDendrites(dendBoost, narrativeT, rotSpeed, tilt, beatSpin,
                                        protaPos, protaScale);
        }
        uploadLinks();
    } else if (dendriteGrowth > 0.15f && soloFade > 0.02f) {
        DendriteBatch batch;
        batch.model = glm::translate(glm::mat4(1), glm::vec3(blobCenter.x, blobCenter.y, blobDepth));
        batch.model = glm::rotate(batch.model, globalTime * rotSpeed, glm::vec3(0, 1, 0));
        batch.model = glm::rotate(batch.model, tilt, glm::vec3(1, 0, 0.3f));
        batch.model = glm::scale(batch.model, glm::vec3(blobScale * soloFade));
        glm::vec3 col(0.45f + lightExposure * 0.12f, 0.52f + lightCharge * 0.10f, 0.60f + lightExposure * 0.15f);
        BlobDeformParams deform = deformParamsFor(shapeBlend, 0.0f, narrativeT);
        buildBlobDendrites(batch, ROLE_SOLO, dendriteGrowth * 0.50f, 42.0f, col, deform);
        dendriteBatches.push_back(std::move(batch));
    }
    uploadLinks();
}

void updateShapeBlend(float dt) {
    updateBeatMorph(dt);
    if (manualOverride) {
        glm::vec3 target = modeUnitVector(int(manualTarget));
        shapeBlend += (target - shapeBlend) * std::min(1.0f, 3.5f * dt);
        float len = glm::length(shapeBlend);
        if (len > 0.001f) shapeBlend /= len;
        beatMorphBlend = shapeBlend;
    } else {
        shapeBlend = beatMorphBlend;
    }

    if (networkViewActive || narrativeClock >= gLive.networkStart + 4.0f)
        networkBlend = 1.0f;
    else if (narrativeClock >= gLive.networkStart)
        networkBlend = smootherstep(gLive.networkStart, gLive.networkStart + 4.0f, narrativeClock);
    else
        networkBlend = 0.0f;

    if (!networkCoresSeeded && networkEmergence() >= NET_E_CORE_ON && !networkViewActive)
        seedNetworkColors();
}

bool initAudio(const std::string& mp3) {
    if (mp3.empty()) return false;
    try {
        audioPlayer = new AudioAnalysis::MP3Player();
        AudioAnalysis::AnalysisParams p;
        p.enable_freq_bands = true;
        p.continuous_update_rate_ms = 30;
        p.enable_continuous_monitoring = true;
        audioPlayer->set_analysis_params(p);
        audioPlayer->add_continuous_callback(onAudioFeatures);
        if (!audioPlayer->load(mp3)) { delete audioPlayer; audioPlayer=nullptr; return false; }
        audioPlayer->set_volume(1.0);
        useMp3Audio = true;
        printf("Loaded MP3: %s\n", mp3.c_str());
        return true;
    } catch (...) { return false; }
}

void drawBlob(const glm::mat4& view, const glm::mat4& proj, const glm::mat4& model,
              const NeuronNode* node, float narrativeT,
              const glm::vec3& localShapeBlend, float timeOffset, const glm::vec3& camPos,
              float blobAlpha = 1.0f, float electrocuteDrain = 0.0f) {
    glm::vec3 sig = node ? node->displaySignal : glm::vec3(0);
    float amp = node ? std::min(1.5f, 0.12f + glm::length(sig) * 1.1f + node->pulse * 0.35f) : 0.0f;
    int role = node ? int(node->role) : 0;
    float phase = node ? node->flowPhase : 0.0f;
    float colorSeed = node
        ? (node->flowPhase * 3.7f + node->dendritePhase * 1.9f + role * 2.13f)
        : coreColorPhase;

    glUseProgram(blobProgram);
    glUniformMatrix4fv(glGetUniformLocation(blobProgram, "model"), 1, GL_FALSE, glm::value_ptr(model));
    glUniformMatrix4fv(glGetUniformLocation(blobProgram, "view"), 1, GL_FALSE, glm::value_ptr(view));
    glUniformMatrix4fv(glGetUniformLocation(blobProgram, "projection"), 1, GL_FALSE, glm::value_ptr(proj));
    glUniform1f(glGetUniformLocation(blobProgram, "time"), globalTime);
    glUniform1f(glGetUniformLocation(blobProgram, "nodeTime"), timeOffset);
    glUniform1f(glGetUniformLocation(blobProgram, "musicDrive"), musicDrive);
    glUniform3fv(glGetUniformLocation(blobProgram, "cameraPos"), 1, glm::value_ptr(camPos));
    glUniform1f(glGetUniformLocation(blobProgram, "audioBass"), bassEnv);
    glUniform1f(glGetUniformLocation(blobProgram, "audioMid"), midEnv);
    glUniform1f(glGetUniformLocation(blobProgram, "audioTreble"), trebleEnv);
    glUniform1f(glGetUniformLocation(blobProgram, "beatAccent"), beatAccent);
    glUniform3fv(glGetUniformLocation(blobProgram, "shapeBlend"), 1, glm::value_ptr(localShapeBlend));
    glUniform3fv(glGetUniformLocation(blobProgram, "signalColor"), 1, glm::value_ptr(sig));
    glUniform1f(glGetUniformLocation(blobProgram, "signalAmp"), amp);
    glUniform1f(glGetUniformLocation(blobProgram, "wavePhase"), phase);
    glUniform1i(glGetUniformLocation(blobProgram, "neuronRole"), role);
    glUniform1f(glGetUniformLocation(blobProgram, "lightningPulse"), lightningPulse);
    glUniform1f(glGetUniformLocation(blobProgram, "narrativeT"), narrativeT);
    glUniform1f(glGetUniformLocation(blobProgram, "colorSeed"), colorSeed);
    glUniform1f(glGetUniformLocation(blobProgram, "blobAlpha"), blobAlpha);
    float soloGrey = (!node) ? clamp01(soloGreyBlend(electrocuteDrain)) : 0.0f;
    glUniform1f(glGetUniformLocation(blobProgram, "soloGreyAmt"), soloGrey);
    float pushAmt = (!node && cagePhase < CAGE_FREED) ? cagePushAmt : 0.0f;
    glUniform1f(glGetUniformLocation(blobProgram, "cagePushAmt"), pushAmt);
    glUniform3fv(glGetUniformLocation(blobProgram, "cagePushDir"), 1, glm::value_ptr(cagePushDir));
    glBindVertexArray(vao);
    glDrawElements(GL_TRIANGLES, (GLsizei)sphereIndices.size(), GL_UNSIGNED_INT, 0);
    if (localShapeBlend.y > 0.08f) {
        glPointSize(2.0f + 4.0f * localShapeBlend.y);
        glDrawArrays(GL_POINTS, 0, (GLsizei)baseVertices.size());
    }
    glBindVertexArray(0);
}

static void drawBlobTrail(const glm::mat4& view, const glm::mat4& proj, const glm::vec3& camPos,
                          float narrativeT, const glm::vec3& bigShape, float rotSpeed, float tilt, float beatSpin) {
    if (blobTrailPos.size() < 2) return;
    std::vector<glm::vec3> lineVerts, lineCols;
    lineVerts.reserve(blobTrailPos.size() * 2);
    lineCols.reserve(blobTrailPos.size() * 2);
    for (size_t i = 0; i + 1 < blobTrailPos.size(); ++i) {
        float age = blobTrailAge[i];
        float life = 1.0f - age / 1.25f;
        if (life <= 0.0f) continue;
        glm::vec3 col = glm::vec3(0.55f, 0.72f, 1.05f) * life * (0.65f + bassEnv * 0.35f);
        lineVerts.push_back(blobTrailPos[i]);
        lineVerts.push_back(blobTrailPos[i + 1]);
        lineCols.push_back(col);
        lineCols.push_back(col * 0.35f);
    }
    if (!lineVerts.empty()) {
        glEnable(GL_BLEND);
        glBlendFunc(GL_SRC_ALPHA, GL_ONE);
        glUseProgram(lineProgram);
        glUniformMatrix4fv(glGetUniformLocation(lineProgram, "view"), 1, GL_FALSE, glm::value_ptr(view));
        glUniformMatrix4fv(glGetUniformLocation(lineProgram, "projection"), 1, GL_FALSE, glm::value_ptr(proj));
        glUniform1f(glGetUniformLocation(lineProgram, "time"), globalTime);
        glUniform1f(glGetUniformLocation(lineProgram, "linePulse"), 1.65f);
        glUniform1i(glGetUniformLocation(lineProgram, "isBolt"), 0);
        glUniform1i(glGetUniformLocation(lineProgram, "worldSpace"), 1);
        glDepthMask(GL_FALSE);
        glBindVertexArray(linkVao);
        glBindBuffer(GL_ARRAY_BUFFER, linkVbo);
        glBufferData(GL_ARRAY_BUFFER, lineVerts.size() * sizeof(glm::vec3), lineVerts.data(), GL_DYNAMIC_DRAW);
        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(glm::vec3), (void*)0);
        glEnableVertexAttribArray(0);
        glBindBuffer(GL_ARRAY_BUFFER, linkCbo);
        glBufferData(GL_ARRAY_BUFFER, lineCols.size() * sizeof(glm::vec3), lineCols.data(), GL_DYNAMIC_DRAW);
        glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(glm::vec3), (void*)0);
        glEnableVertexAttribArray(1);
        glLineWidth(5.5f);
        glDrawArrays(GL_LINES, 0, (GLsizei)lineVerts.size());
        glBindVertexArray(0);
        glDepthMask(GL_TRUE);
    }
    float coreS = coreBlobInnerScale() * coreBlobReveal * (0.92f + 0.05f * bassEnv);
    const glm::vec3 blobOnlyShape(1.0f, 0.0f, 0.0f);
    if (cagePhase == CAGE_TUNNEL) {
        glEnable(GL_BLEND);
        glBlendFunc(GL_SRC_ALPHA, GL_ONE);
        for (size_t i = 0; i < blobTrailPos.size(); i += 2) {
            float age = blobTrailAge[i];
            float life = 1.0f - age / 1.25f;
            if (life <= 0.08f) continue;
            float ghostScale = coreS * (0.22f + 0.48f * life);
            glm::mat4 model = coreBlobOrientMatrix(blobTrailPos[i]);
            model = glm::scale(model, glm::vec3(ghostScale));
            drawBlob(view, proj, model, nullptr, narrativeT, blobOnlyShape, 0.0f, camPos, life * 0.28f);
        }
    }
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
}

void render() {
    float soloNarrT = fminf(narrativeClock, gLive.networkStart) / std::max(0.001f, gLive.networkStart);
    float narrativeT = glm::mix(soloNarrT, 1.0f, networkShellStage());
    float nodeAppearEst = networkNodeAppearEstimate();
    float electrocuteEst = soloElectrocuteBlend(nodeAppearEst, false);
    float greyDrainEst = soloGreyDrainBlend(electrocuteEst);
    float bgFlash = lightningPulse * 0.12f + greyDrainEst * electrocuteEst * 0.045f;
    float finaleDark = 0.0f;
    if (cagePhase >= CAGE_TUNNEL)
        finaleDark = smootherstep(0.0f, 0.55f, tunnelProgress) * 0.03f;
    if (cagePhase == CAGE_LAUNCH)
        finaleDark = glm::mix(finaleDark, 0.10f, launchProgress);
    float launchBg = (cagePhase == CAGE_LAUNCH) ? launchProgress * 0.06f : 0.0f;
    float starBg = std::max(starFieldReveal, vacuumSuck ? tunnelEntryBlend * 0.38f : 0.0f) * 0.88f;
    glm::vec3 spaceCol(0.025f, 0.014f, 0.018f);
    glm::vec3 baseBg = glm::mix(gLive.bgColor, spaceCol, starBg);
    glClearColor(baseBg.r + bgFlash * 0.08f - finaleDark - launchBg,
                 baseBg.g + bgFlash * 0.06f - finaleDark * 0.8f - launchBg * 0.6f,
                 baseBg.b + bgFlash * 0.04f - finaleDark * 0.5f - launchBg * 0.4f, 1);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    float zoomEase = networkViewActive ? 1.0f : networkZoomEase(narrativeClock);
    float swell = 0.78f + 0.22f * bassEnv;
    float soloCam = 7.2f / swell;
    float netCam = 14.0f;
    float fovDeg = 48.0f;
    fitNetworkCamera(netCam, fovDeg);

    float camDist = glm::mix(soloCam, netCam, zoomEase);
    if (cagePhase == CAGE_FREED) {
        float freedCam = std::max(cubeHalfExtent * 2.65f, 24.0f);
        camDist = glm::mix(camDist, freedCam, cubeReveal);
    }
    float farPlane = 200.0f;
    float freedFollow = 0.0f;
    if (cagePhase >= CAGE_FREED || vacuumSuck) {
        freedFollow = (cagePhase == CAGE_FREED && !vacuumSuck)
            ? smootherstep(0.0f, 1.4f, freedPhaseTime) : 1.0f;
        farPlane = glm::mix(200.0f, 900.0f, smootherstep(0.0f, 0.85f, freedFollow));
    }
    if (cagePhase >= CAGE_TUNNEL || vacuumSuck)
        farPlane = 900.0f;
    float fov = glm::radians(glm::mix(48.0f, fovDeg, zoomEase));
    if (cagePhase >= CAGE_TUNNEL || vacuumSuck)
        fov = glm::radians(glm::mix(gLive.fovTunnel, gLive.fovTunnel + 6.0f,
            vacuumSuck ? tunnelEntryBlend : tunnelProgress));
    else if (cagePhase < CAGE_FREED && electrocuteEst > 0.12f)
        fov *= 1.0f + beatAccent * electrocuteEst * 0.016f;
    glm::vec3 lookTarget = glm::mix(glm::vec3(0.0f), corePos, freedFollow);
    glm::vec3 camEye(0.0f, 0.0f, camDist);
    if (cagePhase == CAGE_FREED && cubeReveal > 0.3f && !vacuumSuck)
        camEye.x = sinf(globalTime * 0.11f) * cubeHalfExtent * 0.12f * cubeReveal;
    glm::mat4 view;
    if (cagePhase >= CAGE_TUNNEL || vacuumSuck) {
        glm::vec3 fwd = safeNorm(coreVel);
        if (glm::length(fwd) < 0.01f) fwd = glm::vec3(0.0f, 0.0f, -1.0f);
        float camProg = vacuumSuck ? tunnelEntryBlend : tunnelProgress;
        float followDist = glm::mix(gLive.cameraFollowMin, gLive.cameraFollowMax, camProg);
        if (cagePhase == CAGE_LAUNCH)
            followDist = glm::mix(gLive.cameraFollowMax + 6.0f, gLive.cameraFollowMin + 10.0f, launchProgress);
        float pathS = tunnelPathDist;
        glm::vec3 pathPt, pathTan, pathNorm, pathBin;
        tunnelFrameAt(pathS, pathPt, pathTan, pathNorm, pathBin);
        camEye = corePos - fwd * followDist
               + pathNorm * followDist * 0.08f
               + pathBin * followDist * 0.04f
               + glm::vec3(0.0f, 1.0f, 0.0f);
        lookTarget = corePos + fwd * glm::mix(20.0f, 32.0f, camProg) + pathNorm * 2.5f;
        fov = glm::radians(glm::mix(gLive.fovTunnel + 2.0f, gLive.fovLaunch, launchProgress));
        view = glm::lookAt(camEye, lookTarget, glm::vec3(0, 1, 0));
    } else {
        view = glm::lookAt(camEye, lookTarget, glm::vec3(0, 1, 0));
    }
    glm::mat4 proj = glm::perspective(fov, float(windowWidth) / windowHeight, 0.1f, farPlane);

    glm::vec3 bigShape = bigBlobShapeBlend();
    float tunnelShapeT = 0.0f;
    if (cagePhase >= CAGE_TUNNEL)
        tunnelShapeT = smootherstep(0.0f, 0.45f, tunnelProgress);
    else if (vacuumSuck)
        tunnelShapeT = smootherstep(0.0f, 0.55f, tunnelEntryBlend) * 0.85f;
    bigShape = glm::mix(bigShape, glm::vec3(1.0f, 0.0f, 0.0f), tunnelShapeT);
    float rotSpeed = 0.10f + 0.07f * midEnv + 0.04f * bigShape.z + beatMorphRotKick * 0.10f;
    float tilt = 0.10f * sinf(globalTime * 0.18f) + 0.06f * bigShape.y + beatMorphRotKick * 0.12f;
    float beatSpin = (cagePhase >= CAGE_TUNNEL) ? 0.0f : beatMorphRotKick * 1.2f;
    glm::vec3 camPos = camEye;
    float netScale = networkBlobScale();
    float shellFade = 1.0f - shellRupture * 0.96f;
    if (cagePhase >= CAGE_TUNNEL) shellFade = 0.0f;
    float retireFade = networkShellRetireFade();
    float netAppearBase = (networkViewActive ? 1.0f : networkShellStage()) * shellFade * retireFade;
    float nodeAppear = 0.0f, linkAppear = 0.0f, scatterAmt = 0.0f;
    networkDrawAppears(netAppearBase, nodeAppear, linkAppear, scatterAmt);
    if (!networkViewActive && networkShellStage() < 0.02f && cagePhase <= CAGE_INTACT)
        nodeAppear = linkAppear = scatterAmt = 0.0f;
    float electrocuteBase = soloElectrocuteBlend(nodeAppear, false);
    float electrocute = soloElectrocuteBlend(nodeAppear, true);
    float greyDrain = soloGreyDrainBlend(electrocuteBase);
    ProtagonistDraw prota = computeProtagonistDraw(zoomEase, electrocute, greyDrain);
    linkAppear *= prota.layerMix;
    scatterAmt *= prota.layerMix;
    float colorTheft = greyDrain * 0.92f;
    float tunnelMorph = tunnelDrawMorph;
    if (cagePhase == CAGE_TUNNEL)
        tunnelMorph = std::max(tunnelDrawMorph, smootherstep(0.0f, 2.0f, tunnelPhaseTime));
    else if (vacuumSuck)
        tunnelMorph = std::max(tunnelDrawMorph, smootherstep(0.04f, 0.50f, tunnelEntryBlend));
    float tunnelDrawFade = tunnelVisualFade;
    if (vacuumSuck)
        tunnelDrawFade = std::max(tunnelDrawFade, smootherstep(0.04f, 0.50f, tunnelEntryBlend));
    float tunnelLaunchFade = (cagePhase == CAGE_LAUNCH)
        ? glm::mix(0.35f, 0.0f, smootherstep(0.0f, 0.45f, launchProgress)) : 0.0f;

    if ((cagePhase >= CAGE_TUNNEL || vacuumSuck) && tunnelDrawFade > 0.02f)
        drawTunnel(view, proj, tunnelDrawFade * std::max(tunnelMorph, tunnelLaunchFade));

    rebuildDendriteGeometry(nodeAppear, linkAppear, scatterAmt, netScale,
                            prota.soloAlpha, prota.layerMix, prota.drawPos, prota.soloScale,
                            rotSpeed, tilt, narrativeT, beatSpin);

    float soloDendPulse = std::max(0.65f, dendriteGrowth * 0.8f + lightningPulse * 0.5f + electrocute * 0.85f);
    float netDendPulse = 0.50f + dendriteGrowth * 0.14f;

    bool showShellNetwork = (networkViewActive || networkShellStage() > 0.02f)
        && retireFade > 0.02f;
    size_t batchIdx = 0;
    if (prota.coreAlpha > 0.02f) {
        float swellCore = 1.0f + cagePushAmt * 0.12f;
        float coreS = coreBlobInnerScale() * coreBlobReveal * swellCore * (0.92f + 0.05f * bassEnv);
        coreS *= coreLaunchScale();
        coreS *= tunnelCoreVisualScale();
        glm::vec3 pos = (prota.handoff > 0.05f && cagePhase < CAGE_TUNNEL) ? prota.drawPos : coreBlobWorldCenter();
        glm::mat4 model = coreBlobOrientMatrix(pos);
        if (cagePhase < CAGE_TUNNEL) {
            model = glm::rotate(model, globalTime * rotSpeed * 0.28f + beatSpin, glm::vec3(0, 1, 0));
            model = glm::rotate(model, tilt * 0.5f, glm::vec3(1, 0, 0.3f));
            model = glm::rotate(model, beatSpin * 0.18f, glm::vec3(0.2f, 1.0f, 0.15f));
        }
        model = glm::scale(model, glm::vec3(coreS));
        float coreAlphaDraw = coreLaunchAlpha() * clamp01(prota.coreAlpha / std::max(0.08f, coreBlobReveal));
        drawBlob(view, proj, model, nullptr, narrativeT, bigShape, 0.0f, camPos, coreAlphaDraw);
    }
    if (cagePhase == CAGE_TUNNEL && coreBlobReveal > 0.02f)
        drawBlobTrail(view, proj, camPos, narrativeT, bigShape, rotSpeed, tilt, beatSpin);
    if (showShellNetwork && nodeAppear > 0.02f) {
        for (size_t ni = 0; ni < network.size(); ++ni) {
            const auto& n = network[ni];
            if (!n.active && n.growth < 0.03f) continue;
            float nodeFade = nodeAppear;
            if (scatterAmt > 0.12f && ni < shellExtraOff.size()) {
                float d = glm::length(shellExtraOff[ni]);
                if (d > 35.0f)
                    nodeFade *= 1.0f - smootherstep(35.0f, 105.0f, d);
            }
            if (nodeFade < 0.02f) continue;
            glm::mat4 model = neuronModelMatrix(n, nodeFade, netScale, (int)ni);
            glm::vec3 localShape = neuronShapeBlend(n, globalTime);
            float timeOff = n.dendritePhase * 0.35f;
            NeuronNode drawN = n;
            drawN.displaySignal = nodeDrawSignal(n, colorTheft);
            drawN.color = drawN.displaySignal;
            drawBlob(view, proj, model, &drawN, narrativeT, localShape, timeOff, camPos);
            if (batchIdx < dendriteBatches.size()) {
                glLineWidth(1.6f + dendriteGrowth * 0.6f);
                drawDendriteBatch(dendriteBatches[batchIdx], view, proj, netDendPulse);
                ++batchIdx;
            }
        }
    }

    if (prota.soloAlpha > 0.02f) {
        glm::mat4 model = glm::translate(glm::mat4(1), prota.drawPos);
        model = glm::rotate(model, globalTime * rotSpeed + beatSpin, glm::vec3(0, 1, 0));
        model = glm::rotate(model, tilt, glm::vec3(1, 0, 0.3f));
        model = glm::rotate(model, beatSpin * 0.18f, glm::vec3(0.15f, 1.0f, 0.2f));
        model = glm::scale(model, glm::vec3(prota.soloScale));
        glm::vec3 soloShape = bigShape;
        if (greyDrain > 0.06f)
            soloShape = glm::mix(bigShape, glm::vec3(1.0f, 0.0f, 0.0f), clamp01(greyDrain * 0.94f));
        drawBlob(view, proj, model, nullptr, soloNarrT, soloShape, 0.0f, camPos, prota.soloAlpha, electrocuteBase);
        if (!soloElectDendBatch.verts.empty()) {
            glLineWidth(2.2f + dendriteGrowth * (0.8f + electrocute * 0.6f));
            drawDendriteBatch(soloElectDendBatch, view, proj, soloDendPulse);
        } else if (!showShellNetwork && batchIdx < dendriteBatches.size()) {
            glLineWidth(2.0f + dendriteGrowth);
            drawDendriteBatch(dendriteBatches[batchIdx], view, proj, soloDendPulse);
        }
    }

    glBlendFunc(GL_SRC_ALPHA, GL_ONE);
    if (showShellNetwork && !linkLines.empty() && linkAppear > 0.02f) {
        float linkW = (1.2f + musicDrive * gLive.musicDriveGain * 1.8f) * gLive.dendriteLineWidth;
        float linePulse = 1.0f;
        if (electrocute > 0.05f) {
            linkW *= 1.85f + electrocute * 2.6f;
            linePulse = 2.1f + electrocute * 2.2f + beatAccent * 0.85f;
        }
        glLineWidth(linkW);
        glUseProgram(lineProgram);
        glUniformMatrix4fv(glGetUniformLocation(lineProgram, "view"), 1, GL_FALSE, glm::value_ptr(view));
        glUniformMatrix4fv(glGetUniformLocation(lineProgram, "projection"), 1, GL_FALSE, glm::value_ptr(proj));
        glUniform1f(glGetUniformLocation(lineProgram, "time"), globalTime);
        glUniform1f(glGetUniformLocation(lineProgram, "linePulse"), linePulse);
        glUniform1i(glGetUniformLocation(lineProgram, "isBolt"), 0);
        glUniform1i(glGetUniformLocation(lineProgram, "worldSpace"), 1);
        glDepthMask(GL_FALSE);
        glBindVertexArray(linkVao);
        glDrawArrays(GL_LINES, 0, (GLsizei)linkLines.size());
        glBindVertexArray(0);
        glDepthMask(GL_TRUE);
    }

    if (showShellNetwork && !boltLines.empty() && prota.layerMix > 0.38f && retireFade > 0.08f) {
        glLineWidth(3.0f);
        glUseProgram(lineProgram);
        glUniformMatrix4fv(glGetUniformLocation(lineProgram,"view"),1,GL_FALSE,glm::value_ptr(view));
        glUniformMatrix4fv(glGetUniformLocation(lineProgram,"projection"),1,GL_FALSE,glm::value_ptr(proj));
        glUniform1f(glGetUniformLocation(lineProgram,"time"),globalTime);
        glUniform1f(glGetUniformLocation(lineProgram,"linePulse"),1.0f);
        glUniform1i(glGetUniformLocation(lineProgram,"isBolt"),1);
        glUniform1i(glGetUniformLocation(lineProgram,"worldSpace"),1);
        glUniform1f(glGetUniformLocation(lineProgram,"boltT"),boltProgress);
        glUniform3f(glGetUniformLocation(lineProgram,"boltColor"),1,1,1);
        glBindVertexArray(boltVao);
        glDrawArrays(GL_LINES,0,(GLsizei)boltLines.size());
        glBindVertexArray(0);
    }

    drawImpactWaves(view, proj);
    if (cagePhase == CAGE_FREED && cubeReveal > 0.08f)
        drawTranslucentCube(view, proj, cubeHalfExtent, cubeReveal, 1.0f);
    if (starFieldReveal > 0.01f)
        drawStarField(view, proj, camEye);
    if (finaleWhiteFade > 0.005f)
        drawWhiteFade(finaleWhiteFade);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
}

static void restartDemo() {
    globalTime = narrativeClock = 0;
    networkViewActive = false;
    networkCoresSeeded = false;
    lightCharge = dendriteGrowth = lightningPulse = networkBlend = 0;
    musicDrive = impulseTravel = 0;
    learnedNodeCount = 3;
    learnCooldown = 0;
    coreBlobReveal = 0;
    beatMorphBlend = glm::vec3(1, 0, 0);
    beatMorphMode = 0;
    beatMorphRotKick = 0;
    beatMorphCooldown = 0;
    beatMorphWasHigh = false;
    coreColorPhase = 0.0f;
    narrativeFinished = false;
    freedPhaseTime = 0.0f;
    gLastSyncedAudioPos = -1.0f;
    resetCageState();
    networkShake = glm::vec3(0);
    cascadeStep = 0;
    cascadeTimer = 0;
    activeBolts.clear();
    boltLines.clear();
    boltColors.clear();
    blobCenter = {0, 0};
    blobScale = gLive.blobScale;
    cubeHalfExtent = gLive.cubeHalfExtent;
    if (audioPlayer) {
        audioPlayer->set_volume(1.0);
        audioPlayer->stop();
        audioPlayer->play();
    }
    for (auto& n : network) {
        n.color = roleBaseColor(n.role);
        n.signal = glm::vec3(0);
        n.displaySignal = glm::vec3(0);
        n.charge = n.pulse = 0;
        n.active = n.spawnOrder < learnedNodeCount;
        n.growth = n.active ? 1.0f : 0.0f;
        n.drift = glm::vec3(0);
    }
}

int main(int argc, char** argv) {
    std::string mp3File;
    std::string presetFile;
    bool startInNetwork = false;
    bool windowed = false;
    for (int i = 1; i < argc; ++i) {
        std::string arg = argv[i];
        if (arg == "--network" || arg == "-n")
            startInNetwork = true;
        else if (arg == "--windowed" || arg == "-w")
            windowed = true;
        else if (arg.size() >= 5 && arg.compare(arg.size() - 5, 5, ".json") == 0)
            presetFile = arg;
        else if (mp3File.empty())
            mp3File = arg;
    }

    SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO);
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3);
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);

    Uint32 winFlags = SDL_WINDOW_OPENGL | SDL_WINDOW_SHOWN;
    if (!windowed)
        winFlags |= SDL_WINDOW_FULLSCREEN_DESKTOP;

    if (!windowed) {
        SDL_DisplayMode dm;
        if (SDL_GetCurrentDisplayMode(0, &dm) == 0) {
            windowWidth = dm.w;
            windowHeight = dm.h;
        }
    }

    SDL_Window* window = SDL_CreateWindow("ShredBlob Neuron Network", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
        windowWidth, windowHeight, winFlags);
    if (windowed)
        SDL_SetWindowPosition(window, 40, 40);
    SDL_GLContext ctx = SDL_GL_CreateContext(window);
    SDL_GL_SetSwapInterval(1);
    glViewport(0, 0, windowWidth, windowHeight);
    initGL();

    gLive = LiveParams::defaults();
    std::string loadedPreset;
    if (!presetFile.empty()) {
        if (liveTuneLoadPresetFile(presetFile))
            loadedPreset = presetFile;
        else
            fprintf(stderr, "Failed to load preset: %s\n", presetFile.c_str());
    } else if (liveTuneLoadDefaultPreset(&loadedPreset)) {
        /* default search paths */
    }

    std::string audioPath = mp3File;
    if (audioPath.empty())
        audioPath = liveTunePresetMp3Path();
    if (audioPath.empty()) {
        if (FILE* f = fopen(DEFAULT_MP3, "rb")) { fclose(f); audioPath = DEFAULT_MP3; }
    }

    if (!audioPath.empty()) {
        if (initAudio(audioPath))
            audioPlayer->play();
    }

    if (!loadedPreset.empty())
        printf("Loaded tuning preset: %s\n", loadedPreset.c_str());
    else if (!presetFile.empty())
        fprintf(stderr, "Warning: running without tuning preset\n");

    if (audioPlayer && useMp3Audio) {
        audioPlayer->set_loop(false);
        float trackDur = float(audioPlayer->get_duration());
        if (gLive.narrativeDuration > trackDur + 0.01f)
            gLive.narrativeDuration = trackDur;
        printf("Audio-synced timeline: %.1fs (track %.1fs)\n",
               gLive.narrativeDuration, trackDur);
    }
    liveTuneApplyBlobScale();
    blobScale = gLive.blobScale;
    cubeHalfExtent = gLive.cubeHalfExtent;

    std::string liveSocketPath;
    if (windowed) {
        if (liveTuneStartServer(liveSocketPath)) {
            liveControlActive = true;
            if (!liveTuneSpawnPanel(liveSocketPath))
                printf("Warning: could not launch shredblob_controls.py\n");
        }
    }

    printf("Timeline: solo | dendrites | network | escape | tunnel | launch into stars\n");
    if (windowed)
        printf("Windowed mode: live control panel should open beside the demo.\n"
               "Demo stays open after finale so you can tune and restart.\n");
    else
        printf("Fullscreen by default. --windowed for windowed mode + live GUI. J = skip to next phase\n");

    if (startInNetwork) activateNetworkView();

    bool quit = false, paused = false;
    Uint32 lastTicks = SDL_GetTicks();

    while (!quit) {
        SDL_Event e;
        while (SDL_PollEvent(&e)) {
            if (e.type == SDL_QUIT || (e.type==SDL_KEYDOWN && e.key.keysym.sym==SDLK_ESCAPE)) quit = true;
            if (e.type==SDL_KEYDOWN && e.key.keysym.sym==SDLK_SPACE) {
                paused = !paused;
                if (audioPlayer) paused ? audioPlayer->pause() : audioPlayer->play();
            }
            if (e.type==SDL_KEYDOWN && e.key.keysym.sym==SDLK_r)
                restartDemo();
            if (e.type==SDL_KEYDOWN && e.key.keysym.sym==SDLK_j)
                skipToNextPhase();
            if (e.type==SDL_KEYDOWN && e.key.keysym.sym==SDLK_n) manualOverride = false;
            if (e.type==SDL_KEYDOWN && e.key.keysym.sym==SDLK_1) {
                manualTarget=BLOB; manualOverride=true;
                beatMorphMode=0; beatMorphBlend=modeUnitVector(0); beatMorphRotKick=0.45f;
            }
            if (e.type==SDL_KEYDOWN && e.key.keysym.sym==SDLK_2) {
                manualTarget=THREADS; manualOverride=true;
                beatMorphMode=1; beatMorphBlend=modeUnitVector(1); beatMorphRotKick=0.45f;
            }
            if (e.type==SDL_KEYDOWN && e.key.keysym.sym==SDLK_3) {
                manualTarget=SPIKED; manualOverride=true;
                beatMorphMode=2; beatMorphBlend=modeUnitVector(2); beatMorphRotKick=0.45f;
            }
            if (e.type==SDL_WINDOWEVENT && (e.window.event==SDL_WINDOWEVENT_RESIZED
                || e.window.event==SDL_WINDOWEVENT_SIZE_CHANGED)) {
                windowWidth=e.window.data1; windowHeight=e.window.data2;
                glViewport(0,0,windowWidth,windowHeight);
            }
        }
        Uint32 now = SDL_GetTicks();
        float dt = (now-lastTicks)/1000.0f;
        lastTicks = now;
        if (liveControlActive) {
            liveTunePoll();
            blobScale = gLive.blobScale;
            if (cagePhase <= CAGE_FREED || cagePhase == CAGE_INTACT)
                cubeHalfExtent = gLive.cubeHalfExtent;
            if (gLive.resetDemo) {
                gLive.resetDemo = false;
                restartDemo();
            }
            if (gLive.jumpPhase >= 0.0f) {
                int phase = (int)gLive.jumpPhase;
                gLive.jumpPhase = -1.0f;
                jumpToPhase(phase);
            }
        }
        dt *= gLive.timeScale;
        float simDt = dt;
        if (!paused && !narrativeFinished) {
            if (audioTimelineActive()) {
                syncTimelineFromAudio();
                if (gLastSyncedAudioPos >= 0.0f)
                    simDt = std::max(0.0f, narrativeClock - gLastSyncedAudioPos);
                gLastSyncedAudioPos = narrativeClock;
            } else {
                globalTime += dt;
                narrativeClock += dt;
                if (narrativeClock > gLive.narrativeDuration)
                    narrativeClock = gLive.narrativeDuration;
            }
            updateAudioEnvelopes(simDt);
            updateAudioFade();
            updateMusicBrain(simDt);
            updateCageEscape(simDt);
            updateBlobTrail(simDt);
            updateBlobMotion(simDt);
            updateNetworkMotion(simDt);
            updateNetworkSignals(simDt);
            updateDendriteLightning(simDt);
            updateShapeBlend(simDt);
            updateNetworkCascade(simDt);
        } else if (!paused && windowed && narrativeFinished) {
            if (audioTimelineActive())
                syncTimelineFromAudio();
            globalTime += dt;
            updateAudioEnvelopes(dt);
            updateAudioFade();
        }
        if (narrativeFinished && !windowed)
            quit = true;
        char title[128];
        snprintf(title,sizeof(title),"ShredBlob %.0fs %s net:%.0f%% %s",
            narrativeClock, networkViewActive ? "[NETWORK]" : "",
            networkBlend*100, narrativeFinished ? "[FIN]" : "");
        SDL_SetWindowTitle(window, title);
        render();
        SDL_GL_SwapWindow(window);
    }
    if (audioPlayer) { audioPlayer->stop(); delete audioPlayer; }
    if (liveControlActive)
        liveTuneShutdown();
    SDL_GL_DeleteContext(ctx);
    SDL_DestroyWindow(window);
    SDL_Quit();
    return 0;
}
