<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>PSYCHONAUTICA</title>
	<atom:link href="https://psychonautica.net/feed" rel="self" type="application/rss+xml" />
	<link>https://psychonautica.net</link>
	<description></description>
	<lastBuildDate>Thu, 15 May 2025 02:15:20 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.9.4</generator>

<image>
	<url>https://psychonautica.net/wp-content/uploads/2025/05/1338-8c81141a-65c7-11ef-9809-4e31bf70f0f5-150x150.png</url>
	<title>PSYCHONAUTICA</title>
	<link>https://psychonautica.net</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title></title>
		<link>https://psychonautica.net/archives/3926</link>
					<comments>https://psychonautica.net/archives/3926#respond</comments>
		
		<dc:creator><![CDATA[psychonautica]]></dc:creator>
		<pubDate>Thu, 15 May 2025 02:15:19 +0000</pubDate>
				<category><![CDATA[Uncategorized]]></category>
		<guid isPermaLink="false">https://psychonautica.net/?p=3926</guid>

					<description><![CDATA[]]></description>
										<content:encoded><![CDATA[
<p></p>


<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <!-- Version: Unified Context + Advanced PostFX (DEBUGGING VISIBILITY) -->
    <title>Psychonautica! - v14.3 DEBUGGING</title>
    <style>
        /* Font import (Kept for fallback/UI elements if needed, but main title is 3D) */
        @import url('https://fonts.googleapis.com/css2?family=Rationale&display=swap');
        /* CHANGED: Import Bungee Shade font */
        @import url('https://fonts.googleapis.com/css2?family=Bungee+Shade&display=swap');

        :root {
            /* --- NEON PALETTE --- */
            --neon-pink: #ff00ff;
            --neon-cyan: #00ffff;
            --neon-lime: #39ff14;
            --neon-yellow: #ffff00;
            --neon-blue: #0077ff;
            --neon-orange: #ff8800;
            --neon-white: #f0f0f0;

            --dark-bg: #050208;
            --darker-bg: #000000;
            --cursor-x: 50vw;
            --cursor-y: 50vh;
            --overlay-rotate-x: 0deg;
            --overlay-rotate-y: 0deg;
            /* --- Added for Title Interaction --- */
            --title-glow-intensity: 1;
            --title-perspective-shift: 0deg;
        }

        *, *::before, *::after {
            box-sizing: border-box;
        }

        html { height: 100%; }

        body {
            margin: 0; height: 100%; overflow: hidden;
            background-color: var(--darker-bg); /* Start with black */
            font-family: 'Audiowide', sans-serif; color: var(--neon-white);
            cursor: none; position: relative;
            transition: background-color 1s ease-in;
        }

        /* --- State Control --- */
        body.state-initial #main-content-wrapper,
        body.state-initial #neon-cursor {
            opacity: 0; visibility: hidden; pointer-events: none;
        }
        body.state-initial #reveal-container { /* Reveal container for logo */
            opacity: 1; visibility: visible;
        }

        body.state-revealing #main-content-wrapper,
        body.state-revealing #neon-cursor {
            opacity: 0; visibility: hidden; pointer-events: none;
        }
        body.state-revealing #reveal-container { /* Reveal container for logo */
            opacity: 1; visibility: visible;
            /* GSAP will handle logo fade, so direct transition might not be needed here */
        }

        body.state-revealed {
             background-color: var(--darker-bg);
        }
        body.state-revealed #main-content-wrapper,
        body.state-revealed #neon-cursor {
            opacity: 1; visibility: visible; pointer-events: auto;
            transition: opacity 0.8s 0.5s ease-in; /* Fade in main content, slight delay after reveal 3D fades */
        }
        body.state-revealed #reveal-container { /* Reveal container for logo */
            opacity: 0; visibility: hidden; pointer-events: none;
            transition: opacity 0.4s ease-out;
        }
        body.state-revealed #overlay { pointer-events: none; }
        body.state-revealed #navigation,
        body.state-revealed #navigation a,
        body.state-revealed #thank-you-text { pointer-events: auto; }
        body.state-revealed #neon-cursor { pointer-events: none; }


        /* --- Reveal HTML Container (for Logo) --- */
        #reveal-container {
            position: fixed;
            top: 0; left: 0; width: 100%; height: 100%;
            z-index: 100; /* Above 3D canvas initially */
            background-color: transparent;
            display: flex;
            justify-content: center;
            align-items: center;
            pointer-events: none; /* Allow 3D canvas interaction if logo is small/faded */
        }
        /* NO canvas#reveal-effect anymore */

        #reveal-logo {
            position: relative;
            z-index: 102; /* Ensure logo is above potential 3D elements during its phase */
            max-width: 90vw; max-height: 90vh;
            opacity: 0; transform: scale(0.8);
            will-change: opacity, transform;
            filter: drop-shadow(0 0 15px rgba(255, 255, 255, 0.7)) drop-shadow(0 0 25px rgba(0, 255, 255, 0.5)) drop-shadow(0 0 40px var(--neon-pink));
        }

        /* --- Main Content Wrapper (HTML overlay) --- */
        #main-content-wrapper {
            position: absolute;
            top: 0; left: 0; width: 100%; height: 100%;
            z-index: 5; /* Will be above the 3D canvas */
            perspective: 2000px;
            transform-style: preserve-3d;
        }

        /* Single Main Psychonautica Canvas */
        canvas#webgl-canvas {
            display: block; position: fixed;
            top: 0; left: 0; width: 100%; height: 100%;
            z-index: 1; /* Behind the HTML overlay */
        }

        /* Overlay (Nav + Thank You Text) */
        #overlay {
            position: absolute; top: 0; left: 0; width: 100%; height: 100%;
            display: flex; flex-direction: column;
            justify-content: space-between;
            align-items: center; text-align: center;
            z-index: 10; /* Inside main-content-wrapper, above canvas */
            padding: clamp(4vh, 8vh, 12vh) clamp(2vw, 4vw, 6vw);
            transform: translateZ(150px) rotateY(var(--overlay-rotate-y, 0deg)) rotateX(var(--overlay-rotate-x, 0deg));
            transition: transform 0.25s ease-out;
            will-change: transform;
        }

        /* --- Title Styling (Enhanced 3D) --- */
        #title-container {
             perspective: 1900px; /* Local perspective for title tilt */
             position: relative;
             animation: subtlePulseAndColorShift 10s infinite ease-in-out;
             will-change: filter, transform;
             transform: rotateY(var(--title-perspective-shift)); /* Local perspective shift */
             transition: transform 0.3s ease-out, filter 0.3s ease-out;
             margin-bottom: clamp(3vh, 6vh, 10vh);
             pointer-events: none; /* Pass through pointer events */
        }
        #title {
            font-family: 'Bungee Shade', sans-serif;
            font-size: clamp(3rem, 8vw, 6rem); font-weight: 400;
            color: var(--neon-white); margin: 0; line-height: 1;
            position: relative; z-index: 1;
            text-shadow:
                 -1px -1px 0 #000,  1px -1px 0 #000, -1px  1px 0 #000,  1px  1px 0 #000,
                 -2px 0 0 #000, 2px 0 0 #000, 0 -2px 0 #000, 0 2px 0 #000,
                 -3px -3px 1px rgba(0,0,0,0.6), 3px -3px 1px rgba(0,0,0,0.6),
                 -3px 3px 1px rgba(0,0,0,0.6), 3px 3px 1px rgba(0,0,0,0.6),
                 0 0 4px #eee, 0 0 8px #eee;
            filter:
                drop-shadow(0 0 calc(12px * var(--title-glow-intensity)) var(--neon-pink))
                drop-shadow(0 0 calc(22px * var(--title-glow-intensity)) var(--neon-cyan))
                drop-shadow(0 0 calc(35px * var(--title-glow-intensity)) var(--neon-lime));
            transition: filter 0.3s ease-out, text-shadow 0.3s ease-out;
            will-change: filter, text-shadow;
        }
        #title span {
             display: inline-block;
             animation: letterDance 1s infinite ease-in-out alternate;
             position: relative;
             will-change: transform;
        }
        @keyframes letterDance {
             0%, 100% { transform: translateY(0) rotate(0deg) scale(1); }
             50% { transform: translateY(-8%) rotate(3deg) scale(1.05); }
        }
        @keyframes subtlePulseAndColorShift {
            0%, 100% { filter: hue-rotate(0deg) brightness(1); }
            33% { filter: hue-rotate(-10deg) brightness(1.05); }
            66% { filter: hue-rotate(5deg) brightness(0.98); }
        }

        /* --- Navigation --- */
        #navigation {
            display: flex; flex-wrap: wrap; justify-content: center;
            align-items: center; gap: clamp(15px, 2vw, 25px) clamp(20px, 3vw, 35px);
            padding: 15px; width: 100%; max-width: 1000px;
            margin-bottom: clamp(2vh, 4vh, 6vh);
            opacity: 1; will-change: opacity;
        }
        #navigation a {
            font-size: clamp(0.85rem, 1.6vw, 1.2rem);
            color: var(--neon-blue); text-decoration: none;
            padding: clamp(9px, 1.3vh, 13px) clamp(14px, 2.2vw, 22px);
            border: 1.5px solid var(--neon-blue); border-radius: 4px;
            transition: all 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275);
            text-shadow: 0 0 3px #fff, 0 0 6px var(--neon-blue);
            background-color: rgba(5, 10, 25, 0.8); backdrop-filter: blur(3px);
            position: relative; overflow: hidden;
            transform: skewX(-6deg) perspective(300px);
            box-shadow: 0 2px 5px rgba(0, 0, 0, 0.4), inset 0 1px 1px rgba(255, 255, 255, 0.1), inset 0 -1px 1px rgba(0, 0, 0, 0.3);
            filter: drop-shadow(0 0 5px rgba(0, 119, 255, 0.4));
            cursor: none;
            will-change: transform, background-color, color, box-shadow, border-color, filter, text-shadow;
        }
         #navigation a::before {
            content: ''; position: absolute; top: 0; left: -100%; width: 10px; height: 100%;
            background-color: var(--neon-cyan);
            transform: skewX(-30deg); transition: left 0.4s ease-out;
            box-shadow: 0 0 12px 3px var(--neon-cyan); opacity: 0.8;
            pointer-events: none; z-index: -1;
         }
        #navigation a:hover, #navigation a:focus {
            color: var(--darker-bg); background-color: var(--neon-pink);
            border-color: var(--neon-white);
            box-shadow: 0 6px 15px rgba(0, 0, 0, 0.6), inset 0 1px 1px rgba(255, 255, 255, 0.3), inset 0 -1px 1px rgba(0, 0, 0, 0.5), 0 0 9px var(--neon-pink), 0 0 18px var(--neon-pink), 0 0 25px var(--neon-cyan);
            filter: drop-shadow(0 0 12px rgba(255, 0, 255, 0.9)) drop-shadow(0 0 18px rgba(0, 255, 255, 0.6));
            text-shadow: none;
            transform: scale(1.12) skewX(-6deg) rotateY(5deg) rotateX(2deg) translateZ(10px);
            outline: none;
        }
         #navigation a:hover::before { left: 150%; transition: left 0.3s ease-in; }

        /* --- Thank You Text --- */
        #thank-you-text {
            font-size: clamp(0.7rem, 1.2vw, 0.9rem);
            color: rgba(240, 240, 240, 0.6); margin-bottom: clamp(1vh, 1.5vh, 2vh);
            text-shadow: 0 0 3px rgba(0, 119, 255, 0.3); transition: color 0.3s ease;
        }
        #thank-you-text:hover { color: rgba(240, 240, 240, 0.9); text-shadow: 0 0 5px rgba(0, 255, 255, 0.5); }

        /* --- NEON CURSOR --- */
        #neon-cursor {
            position: fixed; left: var(--cursor-x); top: var(--cursor-y);
            width: 50px; height: 50px; z-index: 9999;
            transform: translate(-50%, -50%);
        }
        .cursor-core {
             position: absolute; top: 50%; left: 50%; width: 10px; height: 10px;
             background-color: var(--neon-cyan); border-radius: 50%;
             box-shadow: 0 0 8px var(--neon-cyan), 0 0 16px var(--neon-cyan);
             transform: translate(-50%, -50%);
             animation: corePulse .6s infinite ease-in-out alternate;
             will-change: transform, box-shadow;
        }
        @keyframes corePulse {
            from { transform: translate(-50%, -50%) scale(0.9); box-shadow: 0 0 6px var(--neon-cyan), 0 0 12px var(--neon-cyan); }
            to { transform: translate(-50%, -50%) scale(1.1); box-shadow: 0 0 10px var(--neon-cyan), 0 0 20px var(--neon-cyan); }
        }
        .cursor-blade {
             position: absolute; top: 50%; left: 50%; width: 4px; height: 25px;
             background-color: var(--neon-pink); border-radius: 2px;
             box-shadow: 0 0 5px var(--neon-pink), 0 0 10px var(--neon-pink);
             transform-origin: 50% 0%;
             animation: bladeRotate 1.5s infinite linear;
             will-change: transform;
        }
        .cursor-blade:nth-child(2) { animation-delay: -0.75s; }
        @keyframes bladeRotate {
            from { transform: translate(-50%, 0) rotate(0deg); }
            to { transform: translate(-50%, 0) rotate(360deg); }
        }

        /* --- Centered GIF --- */
         #center-gif-container {
             transform: translateZ(50px);
             width: clamp(300px, 55vw, 700px);
             max-height: 55vh;
             display: flex; justify-content: center; align-items: center;
             pointer-events: none;
             filter: brightness(0.95) contrast(1.05);
             margin: clamp(1vh, 3vh, 5vh) 0;
             will-change: transform;
         }
         #center-gif-container img {
             display: block; width: 100%; height: 100%;
             object-fit: contain;
             border-radius: 8px;
             box-shadow: 0 0 20px rgba(0, 255, 255, 0.2), 0 0 35px rgba(255, 0, 255, 0.15);
         }

        /* --- Responsive adjustments --- */
        @media (max-width: 768px) {
            #overlay { justify-content: flex-end; gap: 0; padding: 2vh 2vw; }
            #navigation { gap: 10px 15px; max-width: 95%; padding: 10px; margin-bottom: 2vh;}
            #navigation a { padding: 7px 12px; transform: skewX(-6deg) perspective(300px); }
            #navigation a:hover { transform: scale(1.08) skewX(-6deg) rotateY(3deg) rotateX(1deg) translateZ(5px); }
            #thank-you-text { margin-bottom: 1vh; }
        }
         @media (max-width: 480px) {
            #title { font-size: clamp(2rem, 12vw, 3.5rem); }
         }
    </style>
</head>
<body class="state-initial">

    <!-- Reveal container only for the HTML logo now -->
    <div id="reveal-container">
        <img decoding="async" id="reveal-logo" src="https://psychonautica.net/wp-content/uploads/2024/12/cropped-replicate-prediction-k3h8xbk17nrm80ckqmzrmkmytc.png" alt="Psychonautica Logo Reveal">
    </div>

    <!-- Single 3D Canvas -->
    <canvas id="webgl-canvas"></canvas>

    <!-- Main HTML Overlay Content -->
    <div id="main-content-wrapper">
        <div id="overlay">
             <div id="title-container">
                 <h1 id="title">Psychonautica!</h1>
             </div>
             <div id="center-gif-container">
                 <img decoding="async" src="https://psychonautica.net/wp-content/uploads/2024/12/replicate-prediction-k3h8xbk17nrm80ckqmzrmkmytc.png" alt="Psychedelic Center Image">
             </div>
             <div id="navigation-container" style="width: 100%; display: flex; flex-direction: column; align-items: center;">
                 <nav id="navigation">
                     <a href="#">AI ART GALLERY!</a>
                     <a href="#">TO THE ELVES!</a>
                     <a href="#">Coding Stuff!</a>
                     <a href="#">Dope Shit</a>
                     <a href="#">Mushroom Love!</a>
                 </nav>
                 <div id="thank-you-text">Thank you for exploring.</div>
             </div>
        </div>
    </div>

    <div id="neon-cursor">
        <div class="cursor-core"></div>
        <div class="cursor-blade"></div>
        <div class="cursor-blade"></div>
    </div>

    <script type="importmap">
        {
            "imports": {
                "three": "https://unpkg.com/three@0.161.0/build/three.module.js",
                "three/addons/": "https://unpkg.com/three@0.161.0/examples/jsm/"
            }
        }
    </script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.11.5/gsap.min.js"></script>

    <script type="module">
        import * as THREE from 'three';
        import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
        import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
        import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
        import { AfterimagePass } from 'three/addons/postprocessing/AfterimagePass.js';
        import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js';
        import { FilmShader } from 'three/addons/shaders/FilmShader.js'; // For Film Grain

        // --- Core Three.js Components (Unified) ---
        let scene, camera, renderer, composer, clock;
        let animationFrameId = null;
        const rootStyle = document.documentElement.style;
        const TMath = THREE.MathUtils;

        // --- Scene Groups ---
        let revealGroup, mainGroup;

        // --- HTML Elements ---
        let revealLogoElement, titleElement, titleRect;

        // --- Interaction ---
        let mouse = new THREE.Vector2();
        let targetCameraPos = new THREE.Vector3(); // For main scene camera movement
        let initialCameraPosReveal = new THREE.Vector3(); // Store initial camera pos for reveal
        let raycaster = new THREE.Raycaster();
        let interactableObjects = [];
        let currentIntersected = null;

        // === CONFIGURATION PARAMETERS ===

        // --- Reveal Effect Config ---
        const confReveal = {
            particleCount: 20000, // Reduced a bit for unified scene
            particleBaseSize: 0.8,
            particleColorStart: 0x00ffff,
            particleColorEnd: 0xff00ff,
            ambientColor: 0xaaaaaa,
            light1Color: 0xffffff,
            perspective: 100, // Wider FOV for reveal particles to spread
            cameraZ: 75,     // Camera closer for reveal particles
            bloomStrength: 0.8, // Bloom for reveal phase (can be different)
            bloomRadius: 0.5,
            bloomThreshold: 0.7,
            animationDuration: 6.0, // Slightly shorter reveal
            fadeOutDelayFactor: 0.6, // Start fading reveal particles at 80% of anim duration
            rotationAmount: Math.PI * 2.5,
            cameraPulseAmount: 120
        };
        let revealParticles = null; // Specific to revealGroup

        // --- Main Scene Visuals Config ---
        const mainSceneConfig = {
            particleCount: 19000, // Reduced a bit
            particleSpread: 160,
            maxParticleSize: 1.2,
            hueShiftSpeed: 0.3,
            lightAnimSpeed: 0.6,
            cameraFollowSpeed: 2.0,
            cameraInitialZ: 45, // Default Z for main scene
            cameraXInfluence: 6.0,
            cameraYInfluence: 4.0,
            cameraZInfluence: 8.0,
            overlayTiltXMultiplier: -28,
            overlayTiltYMultiplier: 28,
            titlePerspectiveShiftMultiplier: -30,
            nebulaLineCount: 580,
            nebulaSpread: 180,
            nebulaColor: 0xaa00ff,
            nebulaOpacityBase: 0.2,
            nebulaOpacityHover: 0.48,
            nebulaUndulationSpeed: 0.9,
            nebulaUndulationAmount: 0.35,
            bloomStrengthBase: 0.15, // Main scene bloom
            bloomStrengthHoverMult: 1.3,
            afterImageDamp: 0.8,
            interactionLerpSpeed: 5.0,
            // New PostFX
            chromaticAberrationAmount: 0.0005, // Subtle
            filmNoiseIntensity: 0.03,
            filmScanlinesIntensity: 0.05,
            filmScanlinesCount: 268,
        };
        let mainParticles, nebulaStructure; // Specific to mainGroup
        let pointLight1, pointLight2, pointLight3, pointLight4; // Main scene lights

        // --- Main Particle Shaders (Vertex and Fragment) ---
        const mainVertexShader=`attribute float size;attribute vec3 customColor;attribute float speed;attribute float baseHue;varying vec3 vColor;varying float vAlpha;varying float vBaseHue;uniform float time;float rand(vec2 co){return fract(sin(dot(co.xy,vec2(12.9898,78.233)))*43758.5453);}vec3 curlNoise(vec3 p){const float e=0.1;float n1,n2,n3,n4,n5,n6;vec3 p_x=p-vec3(e,0.,0.);vec3 pX=p+vec3(e,0.,0.);vec3 p_y=p-vec3(0.,e,0.);vec3 pY=p+vec3(0.,e,0.);vec3 p_z=p-vec3(0.,0.,e);vec3 pZ=p+vec3(0.,0.,e);n1=rand(p_x.xy+p_x.z);n2=rand(pX.xy+pX.z);n3=rand(p_y.xy+p_y.z);n4=rand(pY.xy+pY.z);n5=rand(p_z.xy+p_z.z);n6=rand(pZ.xy+pZ.z);float x=n4-n3;float y=n6-n5;float z=n2-n1;const float noise_strength=3.5;return normalize(vec3(x,y,z))*noise_strength/(2.*e);}void main(){vColor=customColor;vBaseHue=baseHue;vec3 pos=position;float R=length(pos);float timeScaled=time*speed*0.3;vec3 noise=curlNoise(pos*0.015+vec3(0.,0.,timeScaled*0.18));pos+=noise*0.7;float spiralPhase=timeScaled*1.4+R*0.05;float spiralAmount=sin(spiralPhase)*1.8*(1.-smoothstep(0.,${mainSceneConfig.particleSpread.toFixed(1)}*0.6,R));pos+=normalize(pos)*spiralAmount;float boundaryFactor=smoothstep(${mainSceneConfig.particleSpread.toFixed(1)}*0.98,${mainSceneConfig.particleSpread.toFixed(1)},R);if(boundaryFactor>0.){pos*=mix(1.,(${mainSceneConfig.particleSpread.toFixed(1)}*0.95)/R,boundaryFactor*boundaryFactor);}vec4 mvPosition=modelViewMatrix*vec4(pos,1.);gl_Position=projectionMatrix*mvPosition;float perspectiveSize=size*(400./-mvPosition.z);float sizePulse=sin(time*speed*3.5+position.x*0.3)*0.25+0.75;gl_PointSize=clamp(perspectiveSize*sizePulse,0.5,18.);vAlpha=smoothstep(0.,${mainSceneConfig.particleSpread.toFixed(1)}*0.15,R);vAlpha*=(1.-smoothstep(${mainSceneConfig.particleSpread.toFixed(1)}*0.85,${mainSceneConfig.particleSpread.toFixed(1)},R));float alphaPulse=cos(time*speed*2.5+position.y*0.2)*0.2+0.8;vAlpha*=alphaPulse;vAlpha=clamp(vAlpha,0.,1.);}`;
        const mainFragmentShader=`varying vec3 vColor;varying float vAlpha;varying float vBaseHue;uniform float time;uniform float hueShift;vec3 rgb2hsl(vec3 color){float maxC=max(color.r,max(color.g,color.b));float minC=min(color.r,min(color.g,color.b));float delta=maxC-minC;float L=(maxC+minC)/2.;float H=0.;float S=0.;if(delta==0.){H=0.;S=0.;}else{if(L<0.5)S=delta/(maxC+minC);else S=delta/(2.-maxC-minC);if(color.r==maxC)H=(color.g-color.b)/delta;else if(color.g==maxC)H=2.+(color.b-color.r)/delta;else H=4.+(color.r-color.g)/delta;H/=6.;if(H<0.)H+=1.;}return vec3(H,S,L);}float hue2rgb(float p,float q,float t){if(t<0.)t+=1.;if(t>1.)t-=1.;if(t<1./6.)return p+(q-p)*6.*t;if(t<1./2.)return q;if(t<2./3.)return p+(q-p)*(2./3.-t)*6.;return p;}vec3 hsl2rgb(vec3 hsl){float H=hsl.x;float S=hsl.y;float L=hsl.z;float R,G,B;if(S==0.){R=G=B=L;}else{float q=L<0.5?L*(1.+S):L+S-L*S;float p=2.*L-q;R=hue2rgb(p,q,H+1./3.);G=hue2rgb(p,q,H);B=hue2rgb(p,q,H-1./3.);}return vec3(R,G,B);}void main(){float dist=length(gl_PointCoord-vec2(0.5));float strength=1.-smoothstep(0.35,0.5,dist);if(strength<=0.)discard;float shimmer=sin(time*15.+gl_FragCoord.x*0.07+gl_FragCoord.y*0.05)*0.15+0.9;vec3 hslColor=rgb2hsl(vColor);hslColor.x=mod(hslColor.x+hueShift,1.);vec3 finalColor=hsl2rgb(hslColor);finalColor*=shimmer;gl_FragColor=vec4(finalColor,vAlpha*strength);}`;

        // --- Chromatic Aberration Shader (Simple version) ---
        const ChromaticAberrationShader = {
            uniforms: {
                'tDiffuse': { value: null },
                'amount': { value: 0.005 } // Default amount
            },
            vertexShader: /*glsl*/`
                varying vec2 vUv;
                void main() {
                    vUv = uv;
                    gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
                }`,
            fragmentShader: /*glsl*/`
                uniform sampler2D tDiffuse;
                uniform float amount;
                varying vec2 vUv;
                void main() {
                    vec2 offsetR = vec2(amount, 0.0);
                    vec2 offsetB = vec2(-amount, 0.0);
                    float r = texture2D(tDiffuse, vUv + offsetR).r;
                    float g = texture2D(tDiffuse, vUv).g;
                    float b = texture2D(tDiffuse, vUv + offsetB).b;
                    gl_FragColor = vec4(r, g, b, texture2D(tDiffuse, vUv).a);
                }`
        };
        let chromaticAberrationPass, filmPass; // To store the passes

        // === INITIALIZATION ===
        function init() {
            console.log("Psychonautica Initializing - Unified Context Mode - DEBUGGING VISIBILITY");
            initCoreThreeJS();
            initRevealAssets();
            initMainSceneAssets();
            setupPostProcessing(); // Setup after core components and assets


            // --- TEMPORARY DEBUG ---
            console.log("FORCING DEBUG STATE: Forcing revealGroup and mainGroup visible, setting camera Z, forcing body state");
            if (revealGroup) {
                revealGroup.visible = true;
                console.log("Reveal group visibility set to true");
                if (revealParticles && revealParticles.material) {
                    revealParticles.material.opacity = 1.0; // Ensure particles are opaque
                    console.log("Reveal particles opacity set to 1.0");
                }
            } else {
                console.log("Reveal group is undefined during DEBUG setup");
            }
            if (mainGroup) {
                mainGroup.visible = true;
                console.log("Main group visibility set to true");
                 if (mainParticles && mainParticles.material) {
                    mainParticles.material.opacity = 1.0;
                    console.log("Main particles opacity set to 1.0");
                } else {
                    console.log("Main particles or material undefined during DEBUG setup");
                }
                if (nebulaStructure && nebulaStructure.material) {
                    nebulaStructure.material.opacity = mainSceneConfig.nebulaOpacityBase;
                     console.log("Nebula opacity set to base");
                } else {
                    console.log("Nebula structure or material undefined during DEBUG setup");
                }
            } else {
                console.log("Main group is undefined during DEBUG setup");
            }

            if (camera) {
                camera.position.set(0, 0, 60); // A reasonable Z position for viewing
                camera.lookAt(0,0,0);
                camera.fov = 130; // A standard FOV
                camera.updateProjectionMatrix();
                console.log("Camera position and FOV reset for debug", camera.position.toArray(), camera.fov);
            } else {
                console.log("Camera is undefined during DEBUG setup");
            }

            document.body.className = 'state-revealed'; // Force the HTML overlay to be visible
            console.log("Body class forced to 'state-revealed'");

            // Force enable post-processing passes that might have been disabled
            const bloomPassInstance = composer.passes.find(pass => pass instanceof UnrealBloomPass);
            if (bloomPassInstance) {
                bloomPassInstance.strength = mainSceneConfig.bloomStrengthBase; // Use main scene bloom
                console.log("Bloom pass strength set for debug");
            } else {
                console.log("Bloom pass instance not found during DEBUG setup");
            }
            if (filmPass) { // filmPass is globally defined
                filmPass.enabled = true;
                console.log("Film pass enabled for debug");
            } else {
                console.log("Film pass (global var) is undefined/null during DEBUG setup");
            }
            if (chromaticAberrationPass) { // chromaticAberrationPass is globally defined
                chromaticAberrationPass.enabled = true;
                console.log("Chromatic aberration pass enabled for debug");
            } else {
                console.log("Chromatic aberration pass (global var) is undefined/null during DEBUG setup");
            }
            // --- END TEMPORARY DEBUG ---


            revealLogoElement = document.getElementById('reveal-logo');
            titleElement = document.getElementById('title');
            if(titleElement) {
                 wrapTitleLetters();
                 setTimeout(() => { if (titleElement) titleRect = titleElement.getBoundingClientRect(); }, 500);
            }

            addEventListeners();

            // Start with reveal sequence - THIS WILL BE OVERRIDDEN BY DEBUG FOR NOW
            if (document.body.classList.contains('state-initial') && !document.body.classList.contains('state-revealed')) { // Check we haven't forced state
                console.log("Body is 'state-initial', scheduling startRevealSequence.");
                setTimeout(startRevealSequence, 100);
            } else {
                console.log("Body is NOT 'state-initial' (or was forced). Current class:", document.body.className);
            }
            animate();
        }

        function initCoreThreeJS() {
            clock = new THREE.Clock();
            scene = new THREE.Scene();
            scene.background = null; // Transparent to see CSS body background

            camera = new THREE.PerspectiveCamera(confReveal.perspective, window.innerWidth / window.innerHeight, 0.1, 3000);
            camera.position.z = confReveal.cameraZ;
            initialCameraPosReveal.copy(camera.position); // Save for reveal camera anim
            targetCameraPos.set(0,0, mainSceneConfig.cameraInitialZ); // Default for main scene

            const canvasElement = document.getElementById('webgl-canvas');
            renderer = new THREE.WebGLRenderer({
                canvas: canvasElement,
                antialias: false, // Performance
                alpha: true,      // For transparency
                powerPreference: 'high-performance'
            });
            renderer.setSize(window.innerWidth, window.innerHeight);
            renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.5));
            renderer.toneMapping = THREE.ACESFilmicToneMapping;
            renderer.outputColorSpace = THREE.SRGBColorSpace;
            scene.environment = null;
            console.log("Core ThreeJS Initialized. Renderer, Scene, Camera created.");
        }

        function setupPostProcessing() {
            composer = new EffectComposer(renderer);
            composer.addPass(new RenderPass(scene, camera));
            console.log("RenderPass added to composer.");

            const afterimagePass = new AfterimagePass();
            afterimagePass.uniforms['damp'].value = mainSceneConfig.afterImageDamp;
            composer.addPass(afterimagePass);
            console.log("AfterimagePass added.");


            const bloomPass = new UnrealBloomPass(
                new THREE.Vector2(window.innerWidth, window.innerHeight),
                confReveal.bloomStrength, confReveal.bloomRadius, confReveal.bloomThreshold
            );
            composer.addPass(bloomPass);
            console.log("UnrealBloomPass added.");

            chromaticAberrationPass = new ShaderPass(ChromaticAberrationShader);
            chromaticAberrationPass.uniforms['amount'].value = mainSceneConfig.chromaticAberrationAmount;
            composer.addPass(chromaticAberrationPass);
            console.log("ChromaticAberrationPass added.");

            if (typeof FilmShader !== 'undefined' && FilmShader.uniforms) {
                filmPass = new ShaderPass(FilmShader);
                if (filmPass.uniforms.nIntensity) filmPass.uniforms.nIntensity.value = mainSceneConfig.filmNoiseIntensity;
                if (filmPass.uniforms.sIntensity) filmPass.uniforms.sIntensity.value = mainSceneConfig.filmScanlinesIntensity;
                if (filmPass.uniforms.sCount) filmPass.uniforms.sCount.value = mainSceneConfig.filmScanlinesCount;
                if (filmPass.uniforms.grayscale) filmPass.uniforms.grayscale.value = 0;
                if (filmPass.uniforms.time) filmPass.uniforms.time.value = 0.0;
                composer.addPass(filmPass);
                console.log("FilmPass added.");
            } else {
                console.error("FilmShader is not defined or not structured as expected. Film grain effect will be disabled.");
                filmPass = null;
            }
            console.log("Post Processing Setup Complete.");
        }

        // --- REVEAL ASSETS ---
        function initRevealAssets() {
            revealGroup = new THREE.Group();
            revealGroup.name = "RevealGroup";
            scene.add(revealGroup);

            revealGroup.add(new THREE.AmbientLight(confReveal.ambientColor, 0.9));
            let light = new THREE.PointLight(confReveal.light1Color, 1.5, 900, 1.8);
            light.position.set(0, 50, 180);
            revealGroup.add(light);

            const geometry = new THREE.BufferGeometry();
            const positions = new Float32Array(confReveal.particleCount * 3);
            const targetPositions = new Float32Array(confReveal.particleCount * 3);
            const delays = new Float32Array(confReveal.particleCount);
            const scales = new Float32Array(confReveal.particleCount);
            const startRadius = 1; const endRadius = confReveal.cameraZ * 4.5;
            for (let i = 0; i < confReveal.particleCount; i++) {
                const i3 = i * 3;
                const phiStart = Math.acos((Math.random() * 2) - 1);
                const thetaStart = Math.random() * Math.PI * 2;
                positions[i3] = startRadius * Math.sin(phiStart) * Math.cos(thetaStart);
                positions[i3 + 1] = startRadius * Math.sin(phiStart) * Math.sin(thetaStart);
                positions[i3 + 2] = startRadius * Math.cos(phiStart);
                const angle = Math.random() * Math.PI * 2;
                const burstRadius = endRadius * (0.8 + Math.random() * 0.9);
                const noiseFactor = endRadius * 0.25;
                targetPositions[i3] = burstRadius * Math.cos(angle) + TMath.randFloatSpread(noiseFactor);
                targetPositions[i3 + 1] = burstRadius * Math.sin(angle) + TMath.randFloatSpread(noiseFactor);
                targetPositions[i3 + 2] = TMath.randFloatSpread(endRadius * 0.9);
                delays[i] = Math.random() * (confReveal.animationDuration * 0.7);
                scales[i] = TMath.randFloat(0.3, 1.1);
            }
            geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
            geometry.setAttribute('targetPosition', new THREE.BufferAttribute(targetPositions, 3));
            geometry.setAttribute('delay', new THREE.BufferAttribute(delays, 1));
            geometry.setAttribute('scale', new THREE.BufferAttribute(scales, 1));
            const material = new THREE.PointsMaterial({
                color: confReveal.particleColorStart, size: confReveal.particleBaseSize,
                sizeAttenuation: true, transparent: true, opacity: 1.0,
                blending: THREE.AdditiveBlending, depthWrite: false
            });
            revealParticles = new THREE.Points(geometry, material);
            revealGroup.add(revealParticles);
            revealGroup.visible = false;
            console.log("Reveal Assets Initialized. revealGroup created.");
        }

        // --- MAIN SCENE ASSETS ---
        function initMainSceneAssets() {
            mainGroup = new THREE.Group();
            mainGroup.name = "MainGroup";
            scene.add(mainGroup);

            const ambientLight = new THREE.AmbientLight(0xffffff, 0.5); mainGroup.add(ambientLight);
            pointLight1 = new THREE.PointLight(0xff00ff, 400, 500, 1.5); mainGroup.add(pointLight1);
            pointLight2 = new THREE.PointLight(0x00ffff, 350, 450, 1.6); mainGroup.add(pointLight2);
            pointLight3 = new THREE.PointLight(0x39ff14, 300, 420, 1.7); mainGroup.add(pointLight3);
            pointLight4 = new THREE.PointLight(0xff8800, 320, 480, 1.6); mainGroup.add(pointLight4);
            pointLight1.position.set(0, 0, 30); pointLight2.position.set(25, 20, -20); pointLight3.position.set(-20, -15, 15); pointLight4.position.set(10, -25, -10);

            const particlesGeometry = new THREE.BufferGeometry();
            const positions = new Float32Array(mainSceneConfig.particleCount * 3);
            const colors = new Float32Array(mainSceneConfig.particleCount * 3);
            const baseHues = new Float32Array(mainSceneConfig.particleCount);
            const sizes = new Float32Array(mainSceneConfig.particleCount);
            const speeds = new Float32Array(mainSceneConfig.particleCount);
            const color = new THREE.Color();
            const overallBaseHue = Math.random();
            for (let i = 0; i < mainSceneConfig.particleCount; i++) {
                const i3 = i * 3;
                const radius = mainSceneConfig.particleSpread * Math.cbrt(Math.random());
                const phi = Math.acos((Math.random() * 2) - 1);
                const theta = Math.random() * Math.PI * 2;
                positions[i3] = radius * Math.sin(phi) * Math.cos(theta);
                positions[i3 + 1] = radius * Math.sin(phi) * Math.sin(theta);
                positions[i3 + 2] = radius * Math.cos(phi);
                const particleBaseHue = (overallBaseHue + (Math.random() - 0.5) * 0.5) % 1.0;
                baseHues[i] = particleBaseHue;
                color.setHSL(particleBaseHue, 1.0, 0.6 + Math.random() * 0.15);
                colors[i3] = color.r; colors[i3 + 1] = color.g; colors[i3 + 2] = color.b;
                sizes[i] = (Math.random() * 0.8 + 0.2) * mainSceneConfig.maxParticleSize;
                speeds[i] = Math.random() * 0.6 + 0.5;
            }
            particlesGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
            particlesGeometry.setAttribute('customColor', new THREE.BufferAttribute(colors, 3));
            particlesGeometry.setAttribute('baseHue', new THREE.BufferAttribute(baseHues, 1));
            particlesGeometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
            particlesGeometry.setAttribute('speed', new THREE.BufferAttribute(speeds, 1));
            const particlesMaterial = new THREE.ShaderMaterial({
                uniforms: { time: { value: 0.0 }, hueShift: { value: 0.0 } },
                vertexShader: mainVertexShader, fragmentShader: mainFragmentShader,
                blending: THREE.AdditiveBlending, depthWrite: false, transparent: true, opacity: 0
            });
            mainParticles = new THREE.Points(particlesGeometry, particlesMaterial);
            mainParticles.rotation.x = Math.random() * 0.1; mainParticles.rotation.y = Math.random() * 0.1;
            mainGroup.add(mainParticles);

            createNebulaStructure();
            mainGroup.visible = false;
            console.log("Main Scene Assets Initialized. mainGroup created.");
        }

        function createNebulaStructure() {
            const points = [];
            const numSegmentsPerLine = 60;
            const lineSpread = mainSceneConfig.nebulaSpread * 0.95;
            for (let i = 0; i < mainSceneConfig.nebulaLineCount; i++) {
                const startRadius = TMath.randFloat(mainSceneConfig.particleSpread * 0.6, lineSpread);
                const startPhi = Math.acos((Math.random() * 2) - 1);
                const startTheta = Math.random() * Math.PI * 2;
                let currentPos = new THREE.Vector3( startRadius * Math.sin(startPhi) * Math.cos(startTheta), startRadius * Math.sin(startPhi) * Math.sin(startTheta), startRadius * Math.cos(startPhi) );
                let velocity = new THREE.Vector3().randomDirection().multiplyScalar(TMath.randFloat(1.0, 2.5));
                for (let j = 0; j < numSegmentsPerLine; j++) {
                    points.push(currentPos.clone());
                    velocity.x += TMath.randFloatSpread(0.38); velocity.y += TMath.randFloatSpread(0.2); velocity.z += TMath.randFloatSpread(0.2);
                    velocity.normalize().multiplyScalar(TMath.randFloat(1.5, 3.5));
                    currentPos.add(velocity);
                     const R = currentPos.length(); const boundaryFactor = TMath.smoothstep(mainSceneConfig.nebulaSpread * 0.9, mainSceneConfig.nebulaSpread, R);
                     if (boundaryFactor > 0.0) { currentPos.multiplyScalar(1.0 - boundaryFactor * 0.1); }
                     if (j < numSegmentsPerLine - 1) { points.push(currentPos.clone()); }
                }
            }
            const geometry = new THREE.BufferGeometry().setFromPoints(points);
            geometry.setAttribute('originalPosition', geometry.getAttribute('position').clone());
            const material = new THREE.LineBasicMaterial({
                color: mainSceneConfig.nebulaColor, linewidth: 1, transparent: true,
                opacity: 0, blending: THREE.AdditiveBlending, depthWrite: false
            });
            nebulaStructure = new THREE.LineSegments(geometry, material);
            nebulaStructure.name = "Nebula"; nebulaStructure.renderOrder = 0;
            mainGroup.add(nebulaStructure);
            interactableObjects.push(nebulaStructure);
            console.log("Nebula Structure Created.");
        }


        // === ANIMATION & TRANSITION LOGIC ===

        function startRevealSequence() {
            if (!revealGroup || !revealParticles || !revealLogoElement || document.body.classList.contains('state-revealing')) {
                console.warn("startRevealSequence preconditions not met or already revealing.");
                return;
            }
            console.log("Starting Reveal Sequence...");

            document.body.classList.remove('state-initial');
            document.body.classList.add('state-revealing');
            revealGroup.visible = true;
            mainGroup.visible = false;

            camera.fov = confReveal.perspective;
            camera.position.copy(initialCameraPosReveal);
            camera.updateProjectionMatrix();

            const bloomPass = composer.passes.find(pass => pass instanceof UnrealBloomPass);
            if (bloomPass) {
                bloomPass.strength = confReveal.bloomStrength;
                bloomPass.radius = confReveal.bloomRadius;
                bloomPass.threshold = confReveal.bloomThreshold;
            }
            if(filmPass) filmPass.enabled = false;
            if(chromaticAberrationPass) chromaticAberrationPass.enabled = false;

            const pGeom = revealParticles.geometry;
            const pMat = revealParticles.material;
            pMat.opacity = 1.0; pMat.size = confReveal.particleBaseSize;
            pMat.color.setHex(confReveal.particleColorStart);
            const posAttr = pGeom.getAttribute('position');
            const targetPosAttr = pGeom.getAttribute('targetPosition');
            const delayAttr = pGeom.getAttribute('delay');

            const tl = gsap.timeline({
                onStart: () => console.log("Reveal GSAP Timeline STARTING"),
                onComplete: () => {
                    console.log("Reveal GSAP Timeline Complete. Transitioning to main scene.");
                    transitionToMainScene();
                }
            });
            const logoFadeInStart = 0.3;
            const logoFadeInDuration = confReveal.animationDuration * 0.15;
            const logoVisibleDuration = confReveal.animationDuration * 0.2;
            const logoFadeOutStart = logoFadeInStart + logoFadeInDuration + logoVisibleDuration;
            const logoFadeOutDuration = confReveal.animationDuration - logoFadeOutStart - 0.5;
            tl.to(revealLogoElement, { opacity: 1, scale: 1, duration: logoFadeInDuration, ease: "power2.out" }, logoFadeInStart);
            let breathTween = gsap.to(revealLogoElement, { scale: 1.15, duration: 1.1, ease: "sine.inOut", repeat: -1, yoyo: true, paused: true });
            tl.call(() => breathTween.play(), null, logoFadeInStart + logoFadeInDuration);
            tl.call(() => { if (breathTween) breathTween.kill(); gsap.set(revealLogoElement, { scale: 1 }); }, null, logoFadeOutStart);
            tl.to(revealLogoElement, { opacity: 0, duration: logoFadeOutDuration, ease: "power1.in" }, logoFadeOutStart);

            for (let i = 0; i < confReveal.particleCount; i++) {
                const delay = delayAttr.getX(i);
                const duration = confReveal.animationDuration * TMath.randFloat(0.6, 0.9);
                let proxy = { x: posAttr.getX(i), y: posAttr.getY(i), z: posAttr.getZ(i) };
                tl.to(proxy, {
                    x: targetPosAttr.getX(i), y: targetPosAttr.getY(i), z: targetPosAttr.getZ(i),
                    duration: duration, ease: "expo.out",
                    onUpdate: function() {
                        posAttr.setXYZ(i, this.targets()[0].x, this.targets()[0].y, this.targets()[0].z);
                        posAttr.needsUpdate = true;
                    }
                }, delay);
            }
            tl.to(revealParticles.rotation, { y: confReveal.rotationAmount, x: TMath.randFloatSpread(Math.PI * 0.8), duration: confReveal.animationDuration * 0.8, ease: "power3.out" }, 0.2);
            tl.to(pMat.color, {
                r: ((confReveal.particleColorEnd >> 16) & 255) / 255, g: ((confReveal.particleColorEnd >> 8) & 255) / 255, b: (confReveal.particleColorEnd & 255) / 255,
                duration: confReveal.animationDuration * 0.3, ease: "sine.inOut"
            }, confReveal.animationDuration * 0.15);
            const fadeOutDelay = confReveal.animationDuration * confReveal.fadeOutDelayFactor;
            const fadeDuration = confReveal.animationDuration - fadeOutDelay;
            tl.to(pMat, { opacity: 0, duration: fadeDuration, ease: "power2.in" }, fadeOutDelay);
            tl.to(pMat, { size: confReveal.particleBaseSize * 0.06, duration: fadeDuration, ease: "power2.in" }, fadeOutDelay);
            const camPulseDur = confReveal.animationDuration * 0.3;
            tl.to(camera.position, { z: initialCameraPosReveal.z + confReveal.cameraPulseAmount, duration: camPulseDur, ease: "expo.out" }, confReveal.animationDuration * 0.1)
              .to(camera.position, { z: initialCameraPosReveal.z, duration: camPulseDur * 1.2, ease: "elastic.out(1, 0.6)" }, confReveal.animationDuration * 0.1 + camPulseDur);
            tl.set(revealLogoElement, { opacity: 0 }, `>${confReveal.animationDuration}`);
        }

        function transitionToMainScene() {
            console.log("Transitioning to Main Scene...");
            document.body.classList.remove('state-revealing');
            document.body.classList.add('state-revealed');

            gsap.to(revealGroup, {
                duration: 0.5, // Dummy tween for onComplete, actual fade handled by particle material
                onComplete: () => {
                    revealGroup.visible = false;
                    console.log("Reveal group set to invisible.");
                }
            });
             setTimeout(() => { if(revealGroup) revealGroup.visible = false; }, 1000); // Backup hide

            mainGroup.visible = true;
            console.log("Main group set to visible.");

            gsap.to(camera, { fov: 70, duration: 1.5, ease: "power2.inOut", onUpdate: () => camera.updateProjectionMatrix() });
            gsap.to(camera.position, { x: targetCameraPos.x, y: targetCameraPos.y, z: targetCameraPos.z, duration: 2.0, ease: "power3.inOut" });

            const bloomPass = composer.passes.find(pass => pass instanceof UnrealBloomPass);
            if (bloomPass) {
                gsap.to(bloomPass, { strength: mainSceneConfig.bloomStrengthBase, radius: 0.5, threshold: 0.2, duration: 1.5, ease: "power2.inOut" });
            }
            if(filmPass) filmPass.enabled = true;
            if(chromaticAberrationPass) chromaticAberrationPass.enabled = true;
            console.log("Main scene post-processing (bloom, film, chromatic) adjusted/enabled.");

            if (mainParticles && mainParticles.material) {
                gsap.to(mainParticles.material, { opacity: 1.0, duration: 2.0, delay: 0.5, ease: "power2.out" });
            }
            if (nebulaStructure && nebulaStructure.material) {
                gsap.to(nebulaStructure.material, { opacity: mainSceneConfig.nebulaOpacityBase, duration: 2.5, delay: 0.8, ease: "power2.out" });
            }
            console.log("Main scene elements (particles, nebula) fading in.");
        }

        function cleanupRevealAssets() {
            if (!revealGroup) return;
            gsap.killTweensOf(revealParticles?.rotation);
            gsap.killTweensOf(revealParticles?.material?.color);
            gsap.killTweensOf(revealParticles?.material);
            if (revealParticles) {
                revealParticles.geometry.dispose();
                revealParticles.material.dispose();
                revealGroup.remove(revealParticles); revealParticles = null;
            }
            const revealLights = revealGroup.children.filter(obj => obj.isLight);
            revealLights.forEach(light => revealGroup.remove(light));
            scene.remove(revealGroup); revealGroup = null;
            console.log("Reveal assets cleaned up.");
        }

        // === EVENT HANDLERS & UTILS ===
        function addEventListeners() {
            window.addEventListener('resize', onWindowResize);
            document.addEventListener('mousemove', updateMouseAndCursor);
        }

        function onWindowResize() {
            const width = window.innerWidth; const height = window.innerHeight;
            camera.aspect = width / height; camera.updateProjectionMatrix();
            renderer.setSize(width, height); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.5));
            composer.setSize(width, height);
            if(titleElement) { titleRect = titleElement.getBoundingClientRect(); }
        }

        function wrapTitleLetters() {
            const text = titleElement.innerText; titleElement.innerHTML='';
            text.split('').forEach((char,index)=>{
                const span=document.createElement('span');
                span.innerText=char===' '?'\\u00A0':char;
                span.style.animationDelay=`${index*0.02}s`;
                titleElement.appendChild(span);
            });
        }

        function updateMouseAndCursor(event) {
            const mouseX = event.clientX; const mouseY = event.clientY;
            rootStyle.setProperty('--cursor-x', `${mouseX}px`);
            rootStyle.setProperty('--cursor-y', `${mouseY}px`);
            if (!document.body.classList.contains('state-revealed')) return;
            mouse.x = (mouseX / window.innerWidth) * 2 - 1;
            mouse.y = - (mouseY / window.innerHeight) * 2 + 1;
            if (titleRect) {
                 const titleCenterX = titleRect.left + titleRect.width / 2;
                 const titleCenterY = titleRect.top + titleRect.height / 2;
                 const distX = mouseX - titleCenterX; const distY = mouseY - titleCenterY;
                 const distance = Math.sqrt(distX * distX + distY * distY);
                 const maxDist = Math.sqrt(Math.pow(titleRect.width / 1.5, 2) + Math.pow(titleRect.height / 1.5, 2));
                 const proximity = Math.max(0, 1 - Math.min(1, distance / maxDist));
                 const glowIntensity = 1 + proximity * 0.2;
                 const perspectiveShift = (distX / (titleRect.width / 2)) * proximity * mainSceneConfig.titlePerspectiveShiftMultiplier;
                 rootStyle.setProperty('--title-glow-intensity', glowIntensity.toFixed(3));
                 rootStyle.setProperty('--title-perspective-shift', `${perspectiveShift.toFixed(2)}deg`);
            }
            const rotateX = mouse.y * mainSceneConfig.overlayTiltXMultiplier;
            const rotateY = mouse.x * mainSceneConfig.overlayTiltYMultiplier;
            rootStyle.setProperty('--overlay-rotate-x', `${rotateX.toFixed(2)}deg`);
            rootStyle.setProperty('--overlay-rotate-y', `${rotateY.toFixed(2)}deg`);
        }

        function handleInteractions(delta) {
            if (!mainGroup || !mainGroup.visible || !camera || !raycaster) return;
            raycaster.setFromCamera(mouse, camera);
            const intersects = raycaster.intersectObjects(interactableObjects, false);
            const bloomPass = composer.passes.find(pass => pass instanceof UnrealBloomPass);
            let targetBloomStrength = mainSceneConfig.bloomStrengthBase;
            if (intersects.length > 0) {
                const firstIntersected = intersects[0].object;
                if (currentIntersected !== firstIntersected) {
                    resetInteractionStates();
                    currentIntersected = firstIntersected;
                    if (currentIntersected === nebulaStructure && nebulaStructure.material) {
                        gsap.to(nebulaStructure.material, { opacity: mainSceneConfig.nebulaOpacityHover, duration: 0.25, ease: "power1.inOut" });
                    }
                }
                targetBloomStrength = mainSceneConfig.bloomStrengthBase * mainSceneConfig.bloomStrengthHoverMult;
            } else {
                if (currentIntersected) { resetInteractionStates(); }
            }
            if (bloomPass) {
                bloomPass.strength = TMath.lerp(bloomPass.strength, targetBloomStrength, delta * mainSceneConfig.interactionLerpSpeed);
            }
        }

        function resetInteractionStates() {
            if (currentIntersected) {
                 if (currentIntersected === nebulaStructure && nebulaStructure.material) {
                     gsap.to(nebulaStructure.material, { opacity: mainSceneConfig.nebulaOpacityBase, duration: 0.4, ease: "power1.out" });
                 }
                currentIntersected = null;
            }
        }

        // === MAIN ANIMATION LOOP ===
        let frameCount = 0;
        function animate() {
            animationFrameId = requestAnimationFrame(animate);
            const delta = clock.getDelta();
            const elapsedTime = clock.getElapsedTime();

            // DEBUG LOGGING (reduce frequency after initial checks)
            // if (frameCount % 120 === 0) { // Log every 2 seconds approx
            //     console.log(`Animate loop running. Delta: ${delta.toFixed(4)}, Elapsed: ${elapsedTime.toFixed(2)}, Body Class: ${document.body.className}`);
            //     if(camera) console.log("Camera pos:", camera.position.toArray());
            //     if(renderer) console.log("Render calls:", renderer.info.render.calls);
            // }
            // frameCount++;


            if (document.body.classList.contains('state-revealing')) {
                // Most reveal logic is GSAP driven
            } else if (document.body.classList.contains('state-revealed')) {
                if (mainParticles && mainParticles.material?.uniforms.time) {
                    mainParticles.material.uniforms.time.value = elapsedTime;
                    mainParticles.material.uniforms.hueShift.value = (elapsedTime * mainSceneConfig.hueShiftSpeed) % 1.0;
                }
                if(mainParticles) {
                    mainParticles.rotation.y += delta * 0.08;
                    mainParticles.rotation.x += delta * 0.115;
                }
                if (nebulaStructure?.geometry?.attributes?.position) {
                    nebulaStructure.rotation.y += delta * 0.015;
                    nebulaStructure.rotation.z += delta * 0.008;
                    const positions = nebulaStructure.geometry.attributes.position;
                    const originals = nebulaStructure.geometry.attributes.originalPosition;
                    const count = positions.count;
                    const timeFactor = elapsedTime * mainSceneConfig.nebulaUndulationSpeed;
                    for (let i = 0; i < count; i++) {
                        const ox = originals.getX(i); const oy = originals.getY(i); const oz = originals.getZ(i);
                        const undulation = Math.sin(ox * 0.1 + timeFactor) * Math.cos(oy * 0.08 + timeFactor * 0.7) * mainSceneConfig.nebulaUndulationAmount;
                        const undulationZ = Math.cos(oz * 0.09 - timeFactor * 1.1) * mainSceneConfig.nebulaUndulationAmount * 0.8;
                        positions.setXYZ(i, ox + undulation, oy + undulation, oz + undulationZ);
                    }
                    positions.needsUpdate = true;
                }
                const lightTime = elapsedTime * mainSceneConfig.lightAnimSpeed;
                if (pointLight1) { pointLight1.position.x=Math.sin(lightTime*0.7+1)*35; pointLight1.position.y=Math.cos(lightTime*0.5+2)*35; pointLight1.position.z=Math.cos(lightTime*0.3+3)*30+15; pointLight1.color.setHSL((lightTime*0.05+0.8)%1.,1.,.65); }
                if (pointLight2) { pointLight2.position.x=Math.cos(lightTime*0.4+4)*38; pointLight2.position.y=Math.sin(lightTime*0.6+5)*38; pointLight2.position.z=Math.sin(lightTime*0.8+6)*28-10; pointLight2.color.setHSL((lightTime*0.05+0.5)%1.,1.,.65); }
                if (pointLight3) { pointLight3.position.x=Math.sin(lightTime*0.5+7)*32; pointLight3.position.y=Math.cos(lightTime*0.3+8)*32; pointLight3.position.z=Math.cos(lightTime*0.9+9)*25; pointLight3.color.setHSL((lightTime*0.05+0.1)%1.,1.,.65); }
                if (pointLight4) { pointLight4.position.x=Math.cos(lightTime*0.6+10)*36; pointLight4.position.y=Math.sin(lightTime*0.4+11)*36; pointLight4.position.z=Math.sin(lightTime*0.7+12)*26+5; pointLight4.color.setHSL((lightTime*0.05+0.0)%1.,1.,.65); }

                targetCameraPos.x = mouse.x * mainSceneConfig.cameraXInfluence;
                targetCameraPos.y = mouse.y * mainSceneConfig.cameraYInfluence;
                targetCameraPos.z = mainSceneConfig.cameraInitialZ - mouse.y * mainSceneConfig.cameraZInfluence;
                if(camera && scene) { // Add null check for camera
                    camera.position.lerp(targetCameraPos, delta * mainSceneConfig.cameraFollowSpeed);
                    camera.lookAt(scene.position);
                }
                handleInteractions(delta);
                if (filmPass && filmPass.enabled && filmPass.uniforms.time) {
                    filmPass.uniforms.time.value = elapsedTime;
                }
            } else if (document.body.classList.contains('state-initial')) {
                 // If stuck in initial, perhaps do nothing or just render a clear for now
            }


            if (composer) {
                try {
                    composer.render(delta);
                } catch (e) {
                    console.error("Error during composer.render:", e);
                    if (animationFrameId) cancelAnimationFrame(animationFrameId); // Stop loop on critical render error
                }
            } else if (renderer && scene && camera) { // Fallback if composer is missing but core exists
                 console.warn("Composer not found, attempting direct render.");
                 renderer.render(scene, camera);
            }
        }

        // --- START EVERYTHING ---
        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', init);
        } else {
            init();
        }

    </script>

</body>
</html>
]]></content:encoded>
					
					<wfw:commentRss>https://psychonautica.net/archives/3926/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>MUSIC VISZZ</title>
		<link>https://psychonautica.net/archives/3607</link>
					<comments>https://psychonautica.net/archives/3607#respond</comments>
		
		<dc:creator><![CDATA[psychonautica]]></dc:creator>
		<pubDate>Sat, 03 May 2025 10:25:10 +0000</pubDate>
				<category><![CDATA[Photography]]></category>
		<guid isPermaLink="false">https://psychonautica.net/?p=3607</guid>

					<description><![CDATA[]]></description>
										<content:encoded><![CDATA[<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Hyper-Psychedelic Fractals & Ico Sphere V8.0 (Presets, Mic Input)</title> {/* Updated Title V8.0 */}
    <style>
        body { margin: 0; overflow: hidden; background-color: #000; color: #fff; font-family: 'Arial', sans-serif; }
        #container { width: 100%; height: 100%; display: block; cursor: grab; } /* Default cursor */
        #container:active { cursor: grabbing; } /* Cursor while dragging */

        #loading { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); font-size: 28px; color: #ff00ff; text-shadow: 0 0 15px #ff00ff, 0 0 30px #ff00ff, 0 0 5px #fff; z-index: 1000; text-align: center; transition: opacity 0.8s ease-out; opacity: 1;}
        #loading.hidden { opacity: 0; pointer-events: none; }
        canvas { display: block; }

        /* UI Styling & Hiding */
        .ui-element-h {
            transition: opacity 0.5s ease-out, transform 0.5s ease-out;
            opacity: 1;
            transform: translateY(0);
            will-change: opacity, transform;
            box-sizing: border-box;
        }
        .ui-element-h.hidden {
            opacity: 0 !important;
            pointer-events: none;
            transform: translateY(20px);
        }

        /* --- UPDATED: Help Panel --- */
        #helpPanel {
            position: absolute;
            top: 15px; /* Adjusted */
            left: 15px; /* Moved to left */
            width: 330px; /* Slightly narrower */
            max-height: calc(100vh - 30px); /* Adjusted */
            background: linear-gradient(135deg, rgba(15, 0, 30, 0.92), rgba(5, 0, 15, 0.94)); /* Enhanced background */
            border: 1px solid rgba(200, 80, 255, 0.7); /* Enhanced border */
            border-radius: 10px; /* Smoother */
            padding: 18px; /* Adjusted */
            color: #e8e8e8; /* Slightly brighter base text */
            font-size: 12.5px; /* Slightly larger */
            line-height: 1.6; /* Adjusted */
            overflow-y: auto;
            z-index: 15;
            box-shadow: 0 0 25px rgba(180, 60, 250, 0.65), 0 0 8px rgba(255, 150, 255, 0.4); /* Enhanced glow */
            transition: opacity 0.5s ease-out, transform 0.5s ease-out;
            opacity: 0;
            pointer-events: none;
            transform: translateY(20px);
            will-change: opacity, transform;
        }
        #helpPanel.visible {
            opacity: 1;
            pointer-events: auto;
            transform: translateY(0);
        }
        /* --- END UPDATED: Help Panel --- */

        /* Apply ui-element-h class */
        .dg.main { z-index: 20 !important; }

        /* --- UPDATED: Audio Controls --- */
        #audioControls {
            position: absolute;
            bottom: 15px;
            left: 50%;
            transform: translateX(-50%);
            background: linear-gradient(180deg, rgba(5, 0, 15, 0.88), rgba(20, 0, 40, 0.9)); /* Enhanced gradient */
            padding: 12px 20px; /* Adjusted */
            border-radius: 12px; /* Smoother */
            z-index: 50;
            display: flex;
            align-items: center;
            flex-wrap: wrap;
            justify-content: center;
            gap: 12px; /* Adjusted */
            box-shadow: 0 0 22px rgba(180, 50, 255, 0.7), 0 0 30px rgba(80, 180, 255, 0.6); /* Enhanced glow */
        }
        /* --- END UPDATED: Audio Controls --- */

         /* --- UPDATED: Info Text --- */
         #infoText {
             position: absolute;
             bottom: 15px; /* Moved to bottom-left */
             left: 15px; /* Moved to bottom-left */
             font-size: 13.5px; /* Slightly larger */
             color: #d8d8d8; /* Slightly brighter */
             z-index: 10;
             background: rgba(10, 0, 25, 0.85); /* Match theme */
             padding: 10px 14px; /* Adjusted */
             border-radius: 8px; /* Smoother */
             max-width: 450px;
             text-shadow: 0 0 4px #000, 0 0 8px rgba(200, 100, 255, 0.5); /* Subtle glow */
        }
        /* --- END UPDATED: Info Text --- */

        /* --- UPDATED: Standard button styling --- */
        #audioControls label, #audioControls button, #audioControls select { /* Added select */
            color: #f0f0f0; /* Brighter text */
            background: linear-gradient(45deg, #b040ff, #6080ff); /* More vibrant gradient */
            border: none;
            padding: 9px 16px; /* Adjusted */
            border-radius: 8px; /* Smoother */
            cursor: pointer;
            font-size: 13.5px; /* Slightly larger */
            font-weight: bold;
            transition: all 0.3s ease;
            box-shadow: 0 0 10px rgba(180, 60, 250, 0.8), inset 0 0 5px rgba(255, 255, 255, 0.15); /* Enhanced shadow */
            text-shadow: 0 0 3px rgba(255, 255, 255, 0.5); /* Adjusted text shadow */
            flex-shrink: 0;
            appearance: none; /* For select */
            text-align: center;
        }
         #audioControls input[type="file"] { display: none; }
        #audioControls label:hover, #audioControls button:hover, #audioControls select:hover {
            background: linear-gradient(45deg, #9d2cfc, #4d6ff1); /* Slightly brighter hover */
            box-shadow: 0 0 18px rgba(180, 60, 250, 1), 0 0 8px rgba(255, 255, 255, 0.6), inset 0 0 8px rgba(255, 255, 255, 0.2); /* Enhanced hover shadow */
            transform: scale(1.06); /* Slightly more pop */
        }
        #audioControls button:disabled, #audioControls select:disabled {
            background: linear-gradient(45deg, #5a5a5a, #777); /* Slightly adjusted disabled */
            cursor: not-allowed;
            box-shadow: inset 0 0 4px rgba(0, 0, 0, 0.3); /* Subtle inset disabled */
            transform: scale(1);
            color: #a0a0a0; /* Adjusted disabled text */
            text-shadow: none;
        }
        /* --- END UPDATED: Standard button styling --- */

        #audioElement { display: none; }

        /* Help Panel Content Styling */
        #helpPanel h4 {
            margin-top: 16px; margin-bottom: 8px; color: #ffaaff; font-size: 14.5px; /* Enhanced color/size */
            border-bottom: 1px solid rgba(180, 80, 250, 0.5); padding-bottom: 5px; /* Enhanced border */
            text-shadow: 0 0 5px rgba(255, 150, 255, 0.6); /* Subtle heading glow */
        }
        #helpPanel h4:first-of-type { margin-top: 0; }
        #helpPanel strong { color: #a0e0ff; font-weight: 600;} /* Brighter strong text */
        #helpPanel p { margin-top: 4px; margin-bottom: 10px; }
        #helpPanel code { background-color: rgba(255,255,255,0.15); padding: 2px 5px; border-radius: 4px; font-family: Consolas, Monaco, monospace; color: #f5b5f5;} /* Enhanced code */

        /* --- NEW: dat.GUI Custom Styling --- */
        .dg.main {
            background-color: rgba(10, 0, 25, 0.9) !important; /* Match help/audio theme */
            border: 1px solid rgba(180, 70, 255, 0.6) !important;
            box-shadow: 0 0 20px rgba(180, 60, 250, 0.5) !important;
            border-radius: 8px;
           /* Add overflow hidden to help contain floated elements */
            overflow: hidden !important;
        }
        /* CLOSE BUTTON REMOVED */
        .dg .close-button {
            display: none !important;
        }

        .dg li:not(.folder) {
            background: transparent !important; /* Remove default list item background */
            border-bottom: 1px solid rgba(120, 50, 180, 0.3) !important; /* Subtle separator */
            border-radius: 4px;
        }
        .dg li.title {
            background: linear-gradient(90deg, rgba(60, 20, 100, 0.7), rgba(30, 10, 60, 0.7)) !important; /* Gradient title bg */
            color: #f0f0f0 !important;
            text-shadow: 0 0 4px rgba(220, 150, 255, 0.7) !important;
            border-bottom: 1px solid rgba(150, 60, 220, 0.6) !important;
        }
        .dg .cr.function, .dg .cr.string, .dg .cr.number, .dg .cr.boolean {
            color: #e8e8e8 !important; /* Brighter controller text */
            border-left: 3px solid transparent !important; /* Remove default left border */
            transition: background-color 0.2s ease;
        }
         .dg .cr:hover {
            background-color: rgba(255, 255, 255, 0.05) !important; /* Subtle hover background */
         }
        .dg .property-name {
            color: #b0d0ff !important; /* Lighter property name */
            font-weight: 500 !important;
        }
        .dg .c input[type=text] {
            background: rgba(0, 0, 0, 0.4) !important;
            color: #ffffff !important;
            border: 1px solid rgba(150, 100, 200, 0.6) !important;
            border-radius: 4px;
            padding: 3px 5px;
            box-shadow: inset 0 0 5px rgba(0,0,0,0.3);
        }
        .dg .c input[type=checkbox] {
            margin-top: 5px !important; /* Adjust alignment */
            accent-color: #ff80ff !important; /* Checkbox color */
            transform: scale(1.1);
        }
        .dg select {
            background: rgba(0, 0, 0, 0.4) !important;
            color: #e0e0e0 !important;
            border: 1px solid rgba(150, 100, 200, 0.6) !important;
            border-radius: 4px;
        }
        .dg .slider {
            background: rgba(0, 0, 0, 0.3) !important;
            border-radius: 5px;
            border: 1px solid rgba(100, 50, 150, 0.5);
            height: 5px !important; /* Slightly thicker */
            margin-top: 7px !important; /* Adjust alignment */
            position: relative !important; /* Added for absolute child positioning */
        }
        .dg .slider-fg {
            background: linear-gradient(90deg, #a050ff, #ff80ff) !important; /* Vibrant slider fill */
            height: 100% !important;
            position: absolute !important; /* Added */
            left: 0 !important; /* Added */
            top: 0 !important; /* Added */
            display: block !important; /* Ensure element is displayed */
        }
         /* Color picker adjustments */
        .dg .cr.color .c .button {
            border: 1px solid rgba(150, 100, 200, 0.6) !important;
            border-radius: 4px;
            box-shadow: none !important;
        }
        .dg .color .picker {
            background: rgba(10, 0, 25, 0.95) !important; /* Match main bg */
            border: 1px solid rgba(180, 70, 255, 0.7) !important;
            box-shadow: 0 5px 15px rgba(0,0,0,0.4) !important;
        }
        /* --- END NEW: dat.GUI Custom Styling --- */

        /* --- NEW: Position GUI --- */
        .dg.main {
            /* Keep initial position centered vertically on the right */
            position: fixed !important; /* Start as fixed */
            top: 50% !important;
            left: auto !important;
            right: 20px !important;
            transform: translateY(-50%) !important; /* Center vertically */
            max-height: 85vh !important; /* Allow taller GUI */
            overflow-y: auto !important; /* Add scrollbar if needed */
            overflow-x: hidden !important; /* Prevent horizontal scroll */
            width: 340px !important; /* Explicit width */
        }
        /* --- END NEW: Position GUI --- */

        /* --- NEW: GUI Footer Style for Instructions --- */
        .gui-footer-info {
            font-size: 10px;
            color: #a0b0ff; /* Lighter blue/purple */
            text-align: center;
            padding: 6px 0 8px 0; /* Add some padding */
            margin-top: 5px;
            border-top: 1px solid rgba(120, 50, 180, 0.3); /* Separator line */
            background-color: rgba(15, 5, 35, 0.7); /* Subtle background match */
            user-select: none; /* Prevent text selection */
            cursor: default; /* Standard cursor over instructions */
        }
        /* --- END NEW: GUI Footer Style --- */

    </style>
</head>
<body>
    <div id="container"></div>
    <div id="loading">Loading Hyper-Psychedelic Experience V8.0...</div>
    {/* Updated Info Text V8.0 */}
    {/* <div id="infoText" class="ui-element-h">Load music/mic below. Drag scene. H=UI, I=Help. Presets available.</div> */}
    {/* Info Text now at bottom left via CSS */ }
    <div id="infoText" class="ui-element-h">Load music/mic below. Drag scene. H=UI, I=Help. Presets available.</div>

    {/* --- Help Panel (Updated V8.0 - Moved to Left) --- */}
    <div id="helpPanel">
        <h4>General</h4>
        <p>Press 'H' to toggle the visibility of the GUI controls, audio/settings buttons, and the bottom info text.</p>
        <p>Press 'I' to toggle this Help panel.</p>
        <p>Click and drag the scene to rotate the camera around the center.</p>
        <p>Use the mouse wheel to zoom in and out.</p>

        <h4>Audio Controls / Settings</h4>
        <p><strong>Input:</strong> Select <code>File</code> to load music, or <code>Microphone</code> to use mic input (requires permission).</p>
        <p><strong>Load Music:</strong> (Only if Input is File) Click to select an audio file (MP3, WAV, OGG, etc.).</p>
        <p><strong>Play/Pause:</strong> Controls audio file playback.</p>
        <p><strong>Save Settings:</strong> Saves all current visual parameters (from the GUI panels) to a JSON file (e.g., `fractal-settings-v8.0.json`) in your downloads folder.</p>
        <p><strong>Load Settings:</strong> Opens a file dialog to select a previously saved JSON settings file.</p>
        <p><strong>Presets:</strong> Save current settings to browser storage (<code>Save Slot 1-5</code>), load a saved slot (<code>Load Slot 1-5</code>), or delete a slot (<code>Delete Slot 1-5</code>).</p>

        <h4>Fractal</h4>
        <p><strong>Type:</strong> Selects the algorithm (<code>Symmetric</code>, <code>Asymmetric</code>, <code>3D</code>, <code>Koch 3D</code>).</p>
        <p><strong>Count:</strong> Number of independent fractal trees (1-9).</p>
        <p><strong>Depth:</strong> Maximum recursion level. Higher = more detail, lower performance.</p>

        <h4>Fractal Style</h4>
        <p><strong>Color Scheme:</strong> Selects a predefined color palette (<code>Gradient Map</code> uses audio intensity to map a gradient).</p>
        <p><strong>Flow Speed:</strong> Speed of the flowing light effect.</p>
        <p><strong>Pulse Freq:</strong> Spatial density of light pulses.</p>
        <p><strong>Base Emission:</strong> Constant minimum brightness/glow.</p>
        <p><strong>Branch Warp:</strong> Sine-wave distortion on branches, influenced by audio.</p>
        <p><strong>Surface Noise Type:</strong> Choose between <code>FBM</code> (Fractional Brownian Motion) or <code>Worley</code> (Cellular) noise.</p>
        <p><strong>Surface Noise Scale:</strong> Size/frequency of the surface noise.</p>
        <p><strong>Surface Noise Intensity:</strong> Strength of the surface noise effect.</p>
        <p><strong>Audio Intensity:</strong> Multiplier for audio effect on brightness/emission.</p>

        <h4>Icosphere</h4> {/* Updated V8.0 */}
        <p><strong>Show:</strong> Toggles the wireframe icosphere visibility.</p>
        <p><strong>Radius:</strong> Overall radius of the icosphere.</p>
        <p><strong>Detail:</strong> Subdivision level (0=Icosahedron, higher=smoother sphere). Affects performance.</p>
        <p><strong>Rotate Speed:</strong> Base automatic rotation speed.</p>
        <p><strong>Line Color:</strong> Base color of the wireframe lines.</p>
        <p><strong>Line Width:</strong> Thickness of the wireframe lines.</p>
        <p><strong>Audio Scale Boost:</strong> How much audio (bass) makes the sphere scale up.</p>

        <h4>Background & Fog</h4>
        <p><strong>Fog Color:</strong> Color of the volumetric fog.</p>
        <p><strong>Fog Noise Scale:</strong> Size/frequency of the fog noise pattern.</p>
        <p><strong>Fog Density:</strong> Overall base density/opacity of the fog.</p>
        <p><strong>Fog Audio Reactivity:</strong> Multiplier for audio influence on fog density/animation.</p>

        <h4>Core Effects</h4>
        <p><strong>Bloom Strength:</strong> Overall intensity of the bloom (glow).</p>
        <p><strong>Bloom Threshold:</strong> Brightness level above which pixels bloom.</p>
        <p><strong>Bloom Audio Boost:</strong> Multiplier for audio (bass/mid) increasing bloom.</p>
        <p><strong>Afterimage Damp:</strong> How quickly the trail fades (0.7=fast, 0.99=long).</p>
        <p><strong>Afterimage Audio:</strong> How much audio (bass) reduces damp factor (shorter trails on beat).</p>
        <p><strong>RGB Shift Amount:</strong> Base chromatic aberration amount.</p>
        <p><strong>RGB Shift Audio Boost:</strong> Multiplier for audio (treble) increasing RGB shift.</p>
        <p><strong>Film Noise:</strong> Intensity of flickering noise overlay.</p>
        <p><strong>Film Scanlines:</strong> Intensity of horizontal scanline overlay.</p>
        <p><strong>Film Grayscale:</strong> Makes film overlay grayscale.</p>
        <p><strong>Film Audio Boost:</strong> Multiplier for audio increasing film noise/scanlines.</p>
        <p><strong>Vignette:</strong> Enables/disables edge darkening.</p>
        <p><strong>Vignette Offset:</strong> How far from center vignette starts (1.0=edge).</p>
        <p><strong>Vignette Darkness:</strong> How dark the vignette is at edges (1.0=black).</p>

        <h4>Advanced Effects</h4>
        <p><strong>Kaleido Sides:</strong> Number of kaleidoscope reflection segments (2-24).</p>
        <p><strong>Kaleido Distortion:</strong> Base warping/swirling distortion.</p>
        <p><strong>Kaleido Audio Distort:</strong> Multiplier for audio increasing distortion.</p>
        <p><strong>Kaleido Audio Speed:</strong> Multiplier for audio (treble) increasing rotation speed.</p>
        <p><strong>Kaleido Mouse Distort:</strong> How much mouse position influences distortion.</p>
        <p><strong>Mirror Mode:</strong> Screen reflections (Off, Vertical, Quad, Oct, Hex).</p>
        <p><strong>Feedback Mix:</strong> Amount of previous frame blended (0.75=less, 0.99=strong).</p>
        <p><strong>Feedback Color Shift:</strong> Amount of hue shift applied in the feedback loop.</p> {/* New V8.0 */}
        <p><strong>Feedback Audio:</strong> How much audio influences feedback scale/rotation/mix.</p>
        <p><strong>Glitch Audio Trigger:</strong> Sensitivity for audio transients (treble spikes) triggering digital glitch.</p>
        <p><strong>Lens Distortion:</strong> Amount of barrel/pincushion distortion.</p> {/* New V8.0 */}
        <p><strong>Lens Dist Audio:</strong> How much audio (bass) influences lens distortion.</p> {/* New V8.0 */}
        <p><strong>Edge Detect:</strong> Enables/disables edge detection overlay.</p> {/* New V8.0 */}
        <p><strong>Edge Color:</strong> Color of the detected edges.</p> {/* New V8.0 */}
        <p><strong>Edge Threshold:</strong> Sensitivity of the edge detection.</p> {/* New V8.0 */}

        <h4>Camera & Scene Rotation</h4>
        <p><strong>Auto Rotate Scene:</strong> Toggles automatic rotation of scene content.</p>
        <p><strong>Rotate Speed X/Y/Z:</strong> Speed of automatic rotation on each axis.</p>

        <h4>Particles</h4> {/* New Section V8.0 */}
        <p><strong>Turbulence Amount:</strong> Strength of the noise-based random movement.</p>
        <p><strong>Turbulence Speed:</strong> Speed of the noise evolution for movement.</p>
        <p><strong>Audio Size Boost:</strong> How much audio (mid/treble) increases particle size.</p>
        <p><strong>Audio Speed Boost:</strong> How much audio (overall) increases particle movement speed.</p>
    </div>

    {/* --- Audio Controls (Updated V8.0) --- */}
    <div id="audioControls" class="ui-element-h">
        <select id="audioInputSelect">
            <option value="file">Input: File</option>
            <option value="mic">Input: Microphone</option>
        </select>
        <label for="audioFile" id="loadMusicLabel">Load Music</label>
        <input type="file" id="audioFile" accept="audio/*">
        <button id="playButton" disabled>Play</button>
        <button id="pauseButton" disabled>Pause</button>
        <button id="saveSettingsButton">Save Settings</button>
        <button id="loadSettingsButton">Load Settings</button>
        <input type="file" id="settingsFile" accept=".json" style="display: none;">
        <select id="presetSelect">
            <option value="">Presets...</option>
            <option value="save1">Save Slot 1</option>
            <option value="save2">Save Slot 2</option>
            <option value="save3">Save Slot 3</option>
            <option value="save4">Save Slot 4</option>
            <option value="save5">Save Slot 5</option>
            <option value="load1">Load Slot 1</option>
            <option value="load2">Load Slot 2</option>
            <option value="load3">Load Slot 3</option>
            <option value="load4">Load Slot 4</option>
            <option value="load5">Load Slot 5</option>
            <option value="delete1">Delete Slot 1</option>
            <option value="delete2">Delete Slot 2</option>
            <option value="delete3">Delete Slot 3</option>
            <option value="delete4">Delete Slot 4</option>
            <option value="delete5">Delete Slot 5</option>
        </select>
        <audio id="audioElement" controls></audio>
    </div>

    <!-- 3JS Core & Addons -->
    <script type="importmap">
        {
            "imports": {
                "three": "https://unpkg.com/three@0.160.0/build/three.module.js",
                "three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/",
                "dat.gui": "https://unpkg.com/dat.gui@0.7.9/build/dat.gui.module.js"
            }
        }
    </script>
    <!-- Keep dat.gui JS for fallback if module fails -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.7.9/dat.gui.min.js"></script>


    <script type="module">
        // Imports
        import * as THREE from 'three';
        import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
        import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
        import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
        import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
        import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js';
        import { AfterimagePass } from 'three/addons/postprocessing/AfterimagePass.js';
        import { FilmPass } from 'three/addons/postprocessing/FilmPass.js';
        import { RGBShiftShader } from 'three/addons/shaders/RGBShiftShader.js';
        // No LineMaterial needed for Icosphere wireframe
        import { DigitalGlitch } from 'three/addons/shaders/DigitalGlitch.js';
        import { GlitchPass } from 'three/addons/postprocessing/GlitchPass.js';
        import { SavePass } from 'three/addons/postprocessing/SavePass.js';
        import { CopyShader } from 'three/addons/shaders/CopyShader.js';
        import { SobelOperatorShader } from 'three/addons/shaders/SobelOperatorShader.js'; // V8 Enhancement: Edge Detection

        // Import GUI
        let GUI;
        try {
           GUI = (await import('dat.gui')).GUI;
        } catch (e) {
           console.warn("dat.gui module import failed, falling back to global.");
           GUI = window.dat.GUI;
        }

        // Variables
        let scene, camera, renderer, composer, controls;
        let clock, loadingManager;
        let masterGroup; // Group for auto-rotation content
        let particleSystem, backgroundMesh, icoSphereMesh; // V8: Renamed cubeSphereGroup
        let pointLight1, pointLight2, pointLight3, pointLight4;
        let mouse = new THREE.Vector2();
        let fractalRoots = [];
        let dynamicBranchNodes = []; // Nodes whose rotation is animated
        let targetAngle = Math.PI / 6; // For fractal branching animation
        let currentAngle = Math.PI / 6;

        // Effect Passes
        let bloomPass, afterimagePass, rgbShiftPass, kaleidoscopePass, filmPass, mirrorPass;
        let feedbackRenderTarget, feedbackPass, glitchPass, vignettePass;
        let feedbackSavePass;
        let lensDistortionPass; // V8 Enhancement
        let edgeDetectionPass; // V8 Enhancement

        // Volumetric Fog
        let fogMesh;

        // Audio
        let audioContext;
        let analyser;
        let audioDataArray;
        let audioSourceNode = null; // For file buffer source
        let micSourceNode = null; // V8 Enhancement: For microphone source
        let audioElement;
        let audioInitialized = false;
        let musicLoaded = false; // Specifically for file loading state
        let isMicActive = false; // V8 Enhancement: Mic state
        // V8 Enhancement: Refined audio bands
        let smoothedSubBass = 0, smoothedBass = 0, smoothedMid = 0, smoothedTreble = 0, smoothedOverall = 0;
        let currentSubBass = 0, currentBass = 0, currentMid = 0, currentTreble = 0, currentOverall = 0;
        const AUDIO_LERP_FACTOR = 0.18; // Slightly faster smoothing

        // Parameters & Constants
        const initialBranchLength = 4;
        const lengthFactor = 0.68;
        const maxAngle = Math.PI / 2.6; // Base max angle for branching
        const angleLerpFactor = 0.07;
        const particleCount = 29000; // Increased slightly
        const particleBaseSize = 0.042; // Adjusted base size
        const fractalSpreadRadius = 16;
        const baseBranchRadius = 0.08;
        const radiusFactor = 0.75;

        // GUI Controls Object (Updated V8.0)
         const guiControls = {
            // Fractal
            fractalType: 'Symmetric Tree', numFractals: 1, maxDepth: 7,
            // Fractal Style
            colorScheme: 'Rainbow Rave', branchFlowSpeed: 4.0, branchPulseFrequency: 10.0, fractalAudioIntensity: 5.0, branchBaseEmission: 0.20, branchDistortionFactor: 0.08,
            fractalSurfaceNoiseType: 'FBM', // V8 Enhancement
            fractalSurfaceNoiseScale: 2.5, fractalSurfaceNoiseIntensity: 0.15,
            // Icosphere (V8 Renamed/Updated)
            showIcoSphere: true, icoSphereRadius: 18, icoSphereDetail: 3, icoSphereRotateSpeed: 0.15, icoSphereLineColor: 0x7040E0, // Adjusted color
            icoSphereLineWidth: 1.5, icoSphereAudioScaleBoost: 1.45, // V8 Enhancement
            // Background & Fog
            fogColor: 0x1a0a2a, fogNoiseScale: 0.8, fogDensity: 0.15, fogAudioInfluence: 1.0,
            // Core Effects
            bloomBaseStrength: 1.3, bloomThreshold: 0.03, bloomAudioBoost: 20.0, // Increased bloomAudioBoost max
            afterimageDamp: 0.93, afterimageAudioInfluence: 0.5, // Increased afterimageAudioInfluence max
            rgbShiftBaseAmount: 0.0055, rgbShiftAudioBoost: 20.0, // Increased rgbShiftAudioBoost max
            filmNoiseIntensity: 0.25, filmScanlineIntensity: 0.3, filmGrayscale: false, filmAudioBoost: 8.0, // Reduced base, Reduced filmAudioBoost
            vignetteEnabled: true, vignetteOffset: 0.95, vignetteDarkness: 0.85,
            // Advanced Effects
            kaleidoscopeSides: 10, kaleidoscopeBaseDistortion: 0.25, kaleidoscopeAudioBoost: 10.0, kaleidoscopeAngleAudioBoost: 20.0, kaleidoscopeMouseInfluence: 0.65, // Increased kaleidoscope boosts max
            mirrorCount: 4,
            feedbackMixAmount: 0.96, feedbackColorShift: 0.005, // V8 Enhancement
            feedbackAudioInfluence: 15.0, // Increased feedbackAudioInfluence max
            glitchAudioTrigger: 25.0, // Increased glitchAudioTrigger max
            lensDistortionAmount: 0.15, lensDistortionAudioBoost: 10.0, // V8 Enhancement, Increased lensDistortionAudioBoost max
            edgeDetectEnabled: false, edgeDetectThreshold: 0.15, edgeDetectColor: 0xff00ff, // V8 Enhancement
            // Camera & Scene
            autoRotate: true, autoRotateSpeedX: 0.0, autoRotateSpeedY: 0.08, autoRotateSpeedZ: 0.01,
            cameraFovAudioBoost: 8.0, // CORRECTED default for camera FOV audio reaction
            autoRotateAudioBoost: 1.0, // NEW for auto-rotate audio reaction
            edgeDetectAudioThresholdBoost: 0.8, // CORRECTED default for edge detection audio reaction
            // Particles (V8 Enhancement)
            particleTurbulence: 0.8, particleTurbulenceSpeed: 0.3, particleAudioSizeBoost: 5.0, particleAudioSpeedBoost: 2.5, particleColorAudioBoost: 2.0, // V8.1 Added missing property
            // Internal state (not saved/loaded directly via file)
            dynamicMaxAngle: maxAngle, baseMaxAngle: maxAngle,
            pointLightAudioIntensity: 1.5, // CORRECTED default for point light audio reaction
        };

        // Color Schemes (Added Gradient Map V8.0)
        const colorSchemes = {
            'Rainbow Rave': { baseHue: 0.0, hueSpread: 0.0, saturation: 1.0, lightness: 0.65, dynamicHue: true, hueSpeed: 0.15 },
            'Galaxy Ember': { baseHue: 0.65, hueSpread: 0.50, saturation: 1.0, lightness: 0.62 },
            'Electric Dream': { baseHue: 0.5, hueSpread: 0.15, saturation: 1.0, lightness: 0.6, secondaryHue: 0.85 },
            'Acid Wash': { baseHue: 0.3, hueSpread: 0.7, saturation: 1.0, lightness: 0.6, dynamicHue: true, hueSpeed: 0.08 },
            'Iridescent': { baseHue: 0.0, hueSpread: 1.0, saturation: 0.95, lightness: 0.68 },
            'Plasma Flow': { baseHue: 0.3, hueSpread: 0.65, saturation: 1.0, lightness: 0.58 },
            'Psychedelic Cycle': { baseHue: 0.3, hueSpread: 0.85, saturation: 1.0, lightness: 0.62 },
            'Forest': { baseHue: 0.25, hueSpread: 0.15, saturation: 0.85, lightness: 0.5 },
            'Oceanic': { baseHue: 0.55, hueSpread: 0.1, saturation: 0.95, lightness: 0.58 },
            'Fire': { baseHue: 0.0, hueSpread: 0.1, saturation: 1.0, lightness: 0.58 },
            'Monochrome': { baseHue: 0, hueSpread: 0, saturation: 0, lightness: 0.75 },
            'Neon Night': { baseHue: 0.65, hueSpread: 0.45, saturation: 1.0, lightness: 0.65 },
            'Pastel Dream': { baseHue: 0.8, hueSpread: 0.35, saturation: 0.75, lightness: 0.8 },
            'Gradient Map': { // V8 Enhancement
                isGradient: true,
                gradient: [ // Example: Cool to Warm based on intensity
                    { stop: 0.0, color: new THREE.Color(0x00ffff) }, // Low intensity = Cyan
                    { stop: 0.3, color: new THREE.Color(0x8000ff) }, // Mid-low = Purple
                    { stop: 0.7, color: new THREE.Color(0xff8000) }, // Mid-high = Orange
                    { stop: 1.0, color: new THREE.Color(0xffff00) }  // High intensity = Yellow
                ]
            },
        };
        let currentBaseHue = 0.0; // Used for dynamic schemes

        // --- Shaders (Updated V8.0) ---

        // Fractal Shader (Added Worley Noise option V8.0)
        const fractalVertexShader = `
            varying vec2 vUv;
            varying float vLevelRatio; // Pass level ratio for fragment shader
            varying vec3 vNormal;
            varying vec3 vViewPosition;
            varying vec3 vWorldPosition; // Pass world position

            uniform float time;
            uniform float audioLevel; // Smoothed overall audio level
            uniform float distortionFactor; // Branch warp amount

            // Simple 3D noise function (replace with Simplex if needed)
            float random (vec3 st) {
                return fract(sin(dot(st.xyz, vec3(12.9898,78.233, 151.7182))) * 43758.5453123);
            }
            float noise (vec3 st) {
                vec3 i = floor(st);
                vec3 f = fract(st);
                float a = random(i);
                float b = random(i + vec3(1.0, 0.0, 0.0));
                float c = random(i + vec3(0.0, 1.0, 0.0));
                float d = random(i + vec3(1.0, 1.0, 0.0));
                float e = random(i + vec3(0.0, 0.0, 1.0));
                float f_ = random(i + vec3(1.0, 0.0, 1.0));
                float g = random(i + vec3(0.0, 1.0, 1.0));
                float h = random(i + vec3(1.0, 1.0, 1.0));
                vec3 u = f*f*(3.0-2.0*f); // Smoothstep interpolation
                return mix(a, b, u.x) +
                       (c - a)* u.y * (1.0 - u.x) +
                       (d - b) * u.x * u.y +
                       (e - a) * u.z * (1.0 - u.x) * (1.0 - u.y) +
                       (f_ - b) * u.z * u.x * (1.0 - u.y) +
                       (g - c) * u.z * (1.0 - u.x) * u.y +
                       (h - d) * u.z * u.x * u.y;
            }

            void main() {
                vUv = uv;
                vNormal = normalize(normalMatrix * normal);
                vec4 worldPos = modelMatrix * vec4(position, 1.0);
                vWorldPosition = worldPos.xyz; // Pass world position

                // Branch Warping / Distortion
                float timeScaled = time * 0.6;
                float noiseVal = noise(worldPos.xyz * 0.8 + vec3(0.0, 0.0, timeScaled * 0.5));
                float distortAmount = distortionFactor * (0.5 + audioLevel * 2.5) * (0.7 + noiseVal * 0.6);
                vec3 distortedPosition = position + normal * sin(worldPos.y * 5.0 + timeScaled) * distortAmount;

                vec4 mvPosition = modelViewMatrix * vec4(distortedPosition, 1.0);
                vViewPosition = -mvPosition.xyz; // Vector from vertex to camera in view space

                // Pass level ratio (assuming it's set correctly in JS)
                // We need to get levelRatio from an attribute if it varies per vertex,
                // or keep it uniform if it's per-mesh. Assuming uniform for now.
                // vLevelRatio = levelRatio; // This needs to be passed if varying

                gl_Position = projectionMatrix * mvPosition;
            }`;

        const fractalFragmentShader = `
            uniform float time;
            uniform float levelRatio; // Ratio of current depth to max depth (0 to 1)
            uniform float audioLevel; // Smoothed overall audio level
            // Color Scheme Uniforms
            uniform float baseHue;
            uniform float hueSpread;
            uniform float saturation;
            uniform float lightness;
            uniform bool isDynamicHue;
            uniform float secondaryHue; // For 'Electric Dream'
            uniform bool useSecondaryHue;
            uniform bool isGradientMap; // V8 Enhancement: Gradient Map flag
            uniform vec3 gradientColor0; uniform float gradientStop0;
            uniform vec3 gradientColor1; uniform float gradientStop1;
            uniform vec3 gradientColor2; uniform float gradientStop2;
            uniform vec3 gradientColor3; uniform float gradientStop3;
            // Style Uniforms
            uniform float flowSpeed;
            uniform float pulseFrequency;
            uniform float audioIntensity; // How much audio affects emission pulse
            uniform float baseEmission; // Constant emission
            // Surface Noise Uniforms (V8 Enhancement: Added type)
            uniform int noiseType; // 0 for FBM, 1 for Worley
            uniform float surfaceNoiseScale;
            uniform float surfaceNoiseIntensity;

            varying vec2 vUv;
            varying vec3 vNormal;
            varying vec3 vViewPosition; // Vector from vertex to camera in view space
            varying vec3 vWorldPosition; // World position from vertex shader

            // --- Utility Functions ---
            vec3 hsl2rgb(vec3 c) {
                vec3 rgb = clamp(abs(mod(c.x*6.0+vec3(0.0,4.0,2.0), 6.0)-3.0)-1.0, 0.0, 1.0);
                return c.z + c.y * (rgb-0.5)*(1.0-abs(2.0*c.z-1.0));
            }

            float random (vec2 st) {
                return fract(sin(dot(st.xy, vec2(12.9898,78.233))) * 43758.5453123);
            }
            float random (vec3 st) {
                return fract(sin(dot(st.xyz, vec3(12.9898,78.233, 151.7182))) * 43758.5453123);
            }

            // --- Noise Functions ---
            // FBM Noise
            float noise3D (vec3 p) {
                vec3 i = floor(p); vec3 f = fract(p);
                f = f*f*(3.0-2.0*f); // Smoothstep
                return mix(mix(mix( random(i + vec3(0,0,0)), random(i + vec3(1,0,0)),f.x),
                           mix( random(i + vec3(0,1,0)), random(i + vec3(1,1,0)),f.x),f.y),
                       mix(mix( random(i + vec3(0,0,1)), random(i + vec3(1,0,1)),f.x),
                           mix( random(i + vec3(0,1,1)), random(i + vec3(1,1,1)),f.x),f.y),f.z);
            }
            float fbm3D(vec3 p, int octaves, float persistence) {
                float total = 0.0; float frequency = 1.0; float amplitude = 1.0; float maxValue = 0.0;
                for(int i = 0; i < octaves; i++) {
                    total += amplitude * noise3D(p * frequency);
                    maxValue += amplitude;
                    amplitude *= persistence;
                    frequency *= 2.0;
                }
                return total / maxValue;
            }

            // Worley Noise (Cellular) - V8 Enhancement
            vec3 hash33(vec3 p) { // Hash function for Worley
                p = fract(p * vec3(0.1031, 0.1030, 0.0973));
                p += dot(p, p.yzx + 33.33);
                return fract((p.xxy + p.yyz) * p.zyx);
            }
            float worleyNoise(vec3 p) {
                vec3 i = floor(p);
                vec3 f = fract(p);
                float minDist1 = 1.0; // F1
                // float minDist2 = 1.0; // F2 (optional)

                for (int x = -1; x <= 1; x++) {
                    for (int y = -1; y <= 1; y++) {
                        for (int z = -1; z <= 1; z++) {
                            vec3 neighbor = vec3(float(x), float(y), float(z));
                            vec3 pointPos = hash33(i + neighbor); // Random point in neighbor cell
                            vec3 diff = neighbor + pointPos - f;
                            float dist = length(diff);

                            // Store closest distance (F1)
                            minDist1 = min(minDist1, dist);

                            // Optional: Store second closest (F2)
                            // if (dist < minDist1) {
                            //     minDist2 = minDist1;
                            //     minDist1 = dist;
                            // } else if (dist < minDist2) {
                            //     minDist2 = dist;
                            // }
                        }
                    }
                }
                // return minDist2 - minDist1; // F2 - F1 (ridges)
                return minDist1; // F1 (standard cells)
            }

            // --- Gradient Mapping Function --- V8 Enhancement
            vec3 getColorFromGradient(float t) {
                 t = clamp(t, 0.0, 1.0);
                 if (t <= gradientStop0) return gradientColor0;
                 if (t <= gradientStop1) return mix(gradientColor0, gradientColor1, smoothstep(gradientStop0, gradientStop1, t));
                 if (t <= gradientStop2) return mix(gradientColor1, gradientColor2, smoothstep(gradientStop1, gradientStop2, t));
                 if (t <= gradientStop3) return mix(gradientColor2, gradientColor3, smoothstep(gradientStop2, gradientStop3, t));
                 return gradientColor3; // Should be unreachable if gradientStop3 is 1.0
            }


            void main() {
                vec3 baseColorRGB;

                // --- Determine Base Color ---
                if (isGradientMap) { // V8 Enhancement: Gradient Logic
                    float intensity = clamp(audioLevel * 1.5 + levelRatio * 0.5, 0.0, 1.0); // Map audio/level to 0-1
                    baseColorRGB = getColorFromGradient(intensity);
                } else { // Original HSL Logic
                    float hue;
                    float sat = saturation;
                    float light = lightness;
                    float randomFactor = random(vWorldPosition.xz + vec2(time * 0.1, levelRatio)) - 0.5;
                    float schemeHueSpeed = 0.0;
                    if (isDynamicHue) { // Determine speed based on scheme base hue (quirk from original)
                         schemeHueSpeed = (baseHue < 0.15 || (baseHue > 0.25 && baseHue < 0.35)) ? 0.15 : 0.08;
                         hue = mod(baseHue + levelRatio * 0.3 + randomFactor * 0.15 + time * schemeHueSpeed, 1.0);
                    } else {
                         hue = mod(baseHue + levelRatio * hueSpread + randomFactor * 0.1, 1.0);
                         // Special case for Galaxy Ember (add yellow sparks)
                         if (baseHue > 0.6 && baseHue < 0.7 && hueSpread > 0.4) { // Galaxy Ember heuristic
                            if (random(vUv * 5.0 + vec2(levelRatio, time * 0.25)) < 0.12) {
                                hue = 0.08 + random(vUv + vec2(0.1, 0.1)) * 0.05; // Yellowish hue
                                sat = 1.0; light = 0.75;
                            }
                         }
                         // Special case for Electric Dream (mix secondary hue)
                         if (useSecondaryHue && random(vWorldPosition.xy + vec2(time * 0.3, levelRatio)) > 0.6) {
                            hue = mod(secondaryHue + randomFactor * 0.05, 1.0);
                            sat = 1.0; light = 0.65;
                         }
                    }
                    // Apply some random variation to saturation and lightness
                    sat *= (0.8 + randomFactor * 0.4);
                    light *= (0.7 + randomFactor * 0.6);
                    vec3 baseColorHSL = vec3(hue, clamp(sat, 0.6, 1.0), clamp(light, 0.45, 0.85));
                    baseColorRGB = hsl2rgb(baseColorHSL);
                }


                // --- Calculate Emission ---
                float flow = sin(vUv.y * pulseFrequency - time * flowSpeed + vWorldPosition.x * 0.5);
                flow = smoothstep(-0.2, 1.0, flow * 0.6 + 0.4); // Remap to 0-1 range smoothly
                float emission = baseEmission + flow * audioLevel * audioIntensity * (1.0 + levelRatio * 0.8);

                // --- Calculate Surface Noise --- V8 Enhancement: Noise Type Choice
                float noiseVal = 0.0;
                vec3 noisePos = vWorldPosition * surfaceNoiseScale + vec3(time * 0.1, 0.0, 0.0);
                if (noiseType == 1) { // Worley Noise
                    noiseVal = worleyNoise(noisePos);
                    noiseVal = smoothstep(0.05, 0.5, noiseVal); // Adjust range for visual effect
                    noiseVal = (noiseVal * 2.0 - 1.0); // Map to -1 to 1 roughly
                } else { // FBM Noise (Default)
                    noiseVal = fbm3D(noisePos, 3, 0.6);
                    noiseVal = (noiseVal - 0.5) * 2.0; // Map to -1 to 1
                }
                vec3 noiseEffect = baseColorRGB * noiseVal * surfaceNoiseIntensity * (1.0 - levelRatio * 0.5);


                // --- Combine Color, Emission, Noise ---
                vec3 finalColor = baseColorRGB + baseColorRGB * emission + noiseEffect;

                // --- Fresnel Effect (Rim Lighting) ---
                vec3 viewDir = normalize(vViewPosition);
                float fresnel = pow(1.0 - abs(dot(viewDir, vNormal)), 3.0); // Rim effect
                finalColor += vec3(0.8) * fresnel * 0.5 * (0.5 + audioLevel * 1.5); // Add white rim light based on audio

                float opacity = 1.0; // Could be adjusted later if needed

                // Clamp final color to avoid excessive brightness before bloom
                gl_FragColor = vec4(clamp(finalColor, 0.0, 3.0), opacity);
            }`;

        // Mirror Shader (Unchanged)
        const MirrorShader = { uniforms: { 'tDiffuse': { value: null }, 'sides': { value: guiControls.mirrorCount } }, vertexShader: `varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); }`, fragmentShader: `uniform sampler2D tDiffuse; uniform float sides; varying vec2 vUv; const float PI = 3.14159265359; const float TAU = PI * 2.0; void main() { if (sides <= 1.0) { gl_FragColor = texture2D(tDiffuse, vUv); return; } vec2 p = vUv - 0.5; float r = length(p); float a = atan(p.y, p.x); float segment = TAU / sides; a = mod(a, segment); a = segment - abs(a - segment * 0.5) * 2.0; a = a * 0.5; vec2 uv = r * vec2(cos(a), sin(a)) + 0.5; gl_FragColor = texture2D(tDiffuse, clamp(uv, 0.0, 1.0)); }` };

        // Background Shader (Unchanged)
        const organicNoiseBackgroundFragmentShader = `uniform float time; uniform vec2 resolution; uniform vec3 color1; uniform vec3 color2; uniform vec3 color3; uniform float noiseScale; uniform float timeScale; uniform float audioInfluence; varying vec2 vUv; float hash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); } float noise(vec2 p) { vec2 i = floor(p); vec2 f = fract(p); f = f * f * (3.0 - 2.0 * f); float a = hash(i + vec2(0.0, 0.0)); float b = hash(i + vec2(1.0, 0.0)); float c = hash(i + vec2(0.0, 1.0)); float d = hash(i + vec2(1.0, 1.0)); return mix(mix(a, b, f.x), mix(c, d, f.x), f.y); } float fbm(vec2 p, int octaves, float persistence) { float total = 0.0; float frequency = 1.0; float amplitude = 1.0; float maxValue = 0.0; for(int i = 0; i < octaves; i++) { total += amplitude * noise(p * frequency); maxValue += amplitude; amplitude *= persistence; frequency *= 2.0; } return total / maxValue; } vec2 warp(vec2 p, float warpAmount) { vec2 q = vec2(fbm(p + vec2(0.0, 0.0), 3, 0.6), fbm(p + vec2(5.2, 1.3), 3, 0.6)); vec2 r = vec2(fbm(p + q * warpAmount + vec2(1.7, 9.2), 3, 0.6), fbm(p + q * warpAmount + vec2(8.3, 2.8), 3, 0.6)); return r * warpAmount; } void main() { float dynamicTime = time * timeScale * (1.0 + audioInfluence * 2.5); float dynamicNoiseScale = noiseScale * (1.0 + sin(dynamicTime * 0.2) * 0.3 + audioInfluence * 1.5); float warpAmount = 0.6 + audioInfluence * 1.8 + sin(dynamicTime * 0.15) * 0.3; vec2 uv = vUv * dynamicNoiseScale; vec2 warpedUv = uv + warp(uv, warpAmount); float n = fbm(warpedUv + vec2(sin(dynamicTime * 0.3), cos(dynamicTime * 0.4)), 5, 0.5); n = smoothstep(0.3, 0.7, n); vec3 col = mix(color1, color2, smoothstep(0.0, 0.6, n + sin(dynamicTime * 0.5 + vUv.x * 5.0 + audioInfluence * 5.0) * 0.2)); col = mix(col, color3, smoothstep(0.4, 0.9, n + cos(dynamicTime * 0.6 + vUv.y * 6.0 - audioInfluence * 6.0) * 0.25)); col *= (0.7 + hash(vUv * 150.0 + dynamicTime * 0.05) * 0.3); float edgeFade = smoothstep(0.0, 0.3, length(vUv - 0.5) * 2.0); col *= (0.4 + edgeFade * 0.6); gl_FragColor = vec4(col, 1.0); }`;

        // Feedback Shader (Added Color Shift V8.0)
        const FeedbackShader = {
            uniforms: {
                'tDiffuse': { value: null },
                'tFeedback': { value: null },
                'mixAmount': { value: guiControls.feedbackMixAmount },
                'time': { value: 0.0 },
                'audioInfluence': { value: 0.0 }, // Overall audio level
                'mouse': { value: new THREE.Vector2() },
                'colorShiftAmount': { value: guiControls.feedbackColorShift } // V8 Enhancement
            },
            vertexShader: `varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); }`,
            fragmentShader: `
                uniform sampler2D tDiffuse;
                uniform sampler2D tFeedback;
                uniform float mixAmount;
                uniform float time;
                uniform float audioInfluence;
                uniform vec2 mouse;
                uniform float colorShiftAmount; // V8 Enhancement

                varying vec2 vUv;

                // HSL <-> RGB Conversion functions (needed for color shift)
                vec3 rgb2hsl( vec3 c ){
                    float maxC = max( c.r, max( c.g, c.b ) );
                    float minC = min( c.r, min( c.g, c.b ) );
                    vec3 hsl = vec3( 0.0, 0.0, ( maxC + minC ) / 2.0 );
                    if ( maxC == minC ){ hsl.x = hsl.y = 0.0; } // achromatic
                    else {
                        float d = maxC - minC;
                        hsl.y = hsl.z > 0.5 ? d / ( 2.0 - maxC - minC ) : d / ( maxC + minC );
                        if ( maxC == c.r ) { hsl.x = ( c.g - c.b ) / d + ( c.g < c.b ? 6.0 : 0.0 ); }
                        else if ( maxC == c.g ) { hsl.x = ( c.b - c.r ) / d + 2.0; }
                        else { hsl.x = ( c.r - c.g ) / d + 4.0; }
                        hsl.x /= 6.0;
                    } return hsl;
                }
                float hue2rgb( float f1, float f2, float hue ) {
                    if ( hue < 0.0 ) hue += 1.0;
                    else if ( hue > 1.0 ) hue -= 1.0;
                    float res;
                    if ( ( 6.0 * hue ) < 1.0 ) res = f1 + ( f2 - f1 ) * 6.0 * hue;
                    else if ( ( 2.0 * hue ) < 1.0 ) res = f2;
                    else if ( ( 3.0 * hue ) < 2.0 ) res = f1 + ( f2 - f1 ) * ( ( 2.0 / 3.0 ) - hue ) * 6.0;
                    else res = f1;
                    return res;
                }
                vec3 hsl2rgb( vec3 hsl ) {
                    vec3 rgb;
                    if ( hsl.y == 0.0 ) { rgb = vec3( hsl.z ); } // achromatic
                    else {
                        float f2 = hsl.z < 0.5 ? hsl.z * ( 1.0 + hsl.y ) : hsl.z + hsl.y - hsl.z * hsl.y;
                        float f1 = 2.0 * hsl.z - f2;
                        rgb.r = hue2rgb( f1, f2, hsl.x + 1.0/3.0 );
                        rgb.g = hue2rgb( f1, f2, hsl.x );
                        rgb.b = hue2rgb( f1, f2, hsl.x - 1.0/3.0 );
                    } return rgb;
                }


                void main() {
                    vec2 center = vec2(0.5);
                    vec2 uv = vUv;
                    vec2 offset = uv - center;

                    // Scale/Rotate feedback UV based on audio
                    float audioScaleEffect = 1.015 + audioInfluence * 0.05 * (sin(time*0.5)*0.5+0.5);
                    offset *= audioScaleEffect;
                    float audioAngleEffect = audioInfluence * 0.08 * sin(time * 0.3);
                    float s = sin(audioAngleEffect);
                    float c = cos(audioAngleEffect);
                    mat2 rotMat = mat2(c, -s, s, c);
                    offset = rotMat * offset;

                    // Mouse influence on feedback UV shift
                    vec2 mouseShift = mouse * 0.004 * (0.5 + audioInfluence * 0.8);
                    offset += mouseShift;

                    vec2 feedbackUv = offset + center;

                    // Sample current frame and feedback frame
                    vec4 currentFrameColor = texture2D(tDiffuse, vUv);
                    vec4 feedbackColor = texture2D(tFeedback, clamp(feedbackUv, 0.0, 1.0));

                    // V8 Enhancement: Apply color shift to feedback color
                    if (colorShiftAmount > 0.0001) {
                        vec3 feedbackHSL = rgb2hsl(feedbackColor.rgb);
                        feedbackHSL.x = mod(feedbackHSL.x + colorShiftAmount * (0.5 + audioInfluence * 0.5), 1.0); // Shift hue
                        feedbackColor.rgb = hsl2rgb(feedbackHSL);
                    }

                    // Mix based on mixAmount and audio
                    float dynamicMix = mixAmount - audioInfluence * 0.12;
                    vec4 finalColor = mix(currentFrameColor, feedbackColor, clamp(dynamicMix, 0.75, 0.995));

                    gl_FragColor = finalColor;
                }`
        };

        // Fog Shader (Unchanged)
        const FogShader = { uniforms: { time: { value: 0.0 }, fogColor: { value: new THREE.Color(guiControls.fogColor) }, noiseScale: { value: guiControls.fogNoiseScale }, density: { value: guiControls.fogDensity }, audioInfluence: { value: 0.0 }, cameraPos: { value: new THREE.Vector3() } }, vertexShader: `varying vec3 vWorldPosition; void main() { vec4 worldPosition = modelMatrix * vec4(position, 1.0); vWorldPosition = worldPosition.xyz; gl_Position = projectionMatrix * viewMatrix * worldPosition; }`, fragmentShader: `uniform float time; uniform vec3 fogColor; uniform float noiseScale; uniform float density; uniform float audioInfluence; uniform vec3 cameraPos; varying vec3 vWorldPosition; float random (vec3 st) { return fract(sin(dot(st.xyz, vec3(12.9898,78.233, 151.7182))) * 43758.5453123); } float noise (vec3 p) { vec3 i = floor(p); vec3 f = fract(p); f = f*f*(3.0-2.0*f); return mix(mix(mix( random(i + vec3(0,0,0)), random(i + vec3(1,0,0)),f.x), mix( random(i + vec3(0,1,0)), random(i + vec3(1,1,0)),f.x),f.y), mix(mix( random(i + vec3(0,0,1)), random(i + vec3(1,0,1)),f.x), mix( random(i + vec3(0,1,1)), random(i + vec3(1,1,1)),f.x),f.y),f.z); } float fbm(vec3 p, int octaves, float persistence) { float total = 0.0; float frequency = 1.0; float amplitude = 1.0; float maxValue = 0.0; for(int i = 0; i < octaves; i++) { total += amplitude * noise(p * frequency); maxValue += amplitude; amplitude *= persistence; frequency *= 2.0; } return total / maxValue; } void main() { float dynamicNoiseScale = noiseScale * (1.0 + audioInfluence * 0.5); float dynamicTime = time * 0.05 * (1.0 + audioInfluence * 1.5); float noiseVal = fbm(vWorldPosition * dynamicNoiseScale + vec3(0.0, 0.0, dynamicTime), 4, 0.5); noiseVal = smoothstep(0.4, 0.65, noiseVal); float camDist = length(vWorldPosition - cameraPos); float distFade = smoothstep(50.0, 300.0, camDist); float finalDensity = density * noiseVal * (1.0 + audioInfluence * 3.0) * (1.0 - distFade); finalDensity = clamp(finalDensity, 0.0, 0.8); gl_FragColor = vec4(fogColor, finalDensity); }`, transparent: true, blending: THREE.AdditiveBlending, depthWrite: false, side: THREE.DoubleSide };

        // Vignette Shader (Unchanged)
        const VignetteShader = { uniforms: { tDiffuse: { value: null }, offset: { value: guiControls.vignetteOffset }, darkness: { value: guiControls.vignetteDarkness } }, vertexShader: `varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); }`, fragmentShader: `uniform sampler2D tDiffuse; uniform float offset; uniform float darkness; varying vec2 vUv; void main() { vec4 texel = texture2D( tDiffuse, vUv ); vec2 uv = ( vUv - vec2( 0.5 ) ) * vec2( offset ); float vig = smoothstep(0.8, 0.25, length( uv ) ); float finalDarkness = 1.0 - darkness * (1.0 - vig); gl_FragColor = vec4( mix( texel.rgb * finalDarkness, texel.rgb, 1.0 - darkness), texel.a ); }` };

        // V8 Enhancement: Lens Distortion Shader
        const LensDistortionShader = {
            uniforms: {
                'tDiffuse': { value: null },
                'amount': { value: guiControls.lensDistortionAmount }, // Distortion amount (-1 to 1, typically -0.5 to 0.5)
                'audioBoost': { value: 0.0 } // Audio influence on amount
            },
            vertexShader: `varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); }`,
            fragmentShader: `
                uniform sampler2D tDiffuse;
                uniform float amount;
                uniform float audioBoost;
                varying vec2 vUv;

                void main() {
                    vec2 uv = vUv;
                    vec2 center = vec2(0.5, 0.5);
                    vec2 texCoord = uv - center;
                    float dist = length(texCoord);
                    float currentAmount = amount + audioBoost * amount * 3.0; // Audio increases effect

                    // Apply distortion based on distance from center
                    // Positive amount = barrel, Negative amount = pincushion
                    texCoord = texCoord * (1.0 + currentAmount * dist * dist);

                    vec2 finalUv = texCoord + center;

                    // Clamp UVs or handle borders
                    if (finalUv.x < 0.0 || finalUv.x > 1.0 || finalUv.y < 0.0 || finalUv.y > 1.0) {
                       // Option 1: Discard fragment (hard edge)
                       // discard;
                       // Option 2: Clamp (stretches edge pixels)
                       finalUv = clamp(finalUv, 0.0, 1.0);
                       // Option 3: Black border
                       // gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);
                       // return;
                    }

                    gl_FragColor = texture2D(tDiffuse, finalUv);
                }`
        };

        // V8 Enhancement: Particle Shader (Vertex only for now)
        const particleVertexShader = `
            attribute float baseSize; // Base size for each particle
            attribute float initialHue; // Store initial hue variation
            attribute vec3 initialPos; // Store initial position for turbulence calculation

            uniform float time;
            uniform float size; // Global size from PointsMaterial
            uniform float scale; // Camera scale factor
            uniform float audioSizeBoost; // Audio influence on size
            uniform float audioSpeedBoost; // Audio influence on speed/turbulence
            uniform float turbulenceAmount;
            uniform float turbulenceSpeed;

            varying vec3 vColor; // Pass color to fragment shader

            // Noise functions (can reuse from fractal shader or simplify)
            float random (vec3 st) { return fract(sin(dot(st.xyz, vec3(12.9898,78.233, 151.7182))) * 43758.5453123); }
            float noise (vec3 p) { vec3 i = floor(p); vec3 f = fract(p); f = f*f*(3.0-2.0*f); return mix(mix(mix( random(i + vec3(0,0,0)), random(i + vec3(1,0,0)),f.x), mix( random(i + vec3(0,1,0)), random(i + vec3(1,1,0)),f.x),f.y), mix(mix( random(i + vec3(0,0,1)), random(i + vec3(1,0,1)),f.x), mix( random(i + vec3(0,1,1)), random(i + vec3(1,1,1)),f.x),f.y),f.z); }
            vec3 fbmVec3(vec3 p, float time) { // Vector noise for displacement
                float speed = turbulenceSpeed * (1.0 + audioSpeedBoost * 0.7);
                return vec3(
                    noise(p + vec3(1.3, 4.7, 2.9) + time * speed * 0.8),
                    noise(p + vec3(7.9, 9.1, 0.7) + time * speed * 0.9),
                    noise(p + vec3(5.2, 0.4, 6.3) + time * speed * 1.0)
                ) * 2.0 - 1.0; // Map to -1 to 1 range
            }


            void main() {
                vColor = color; // Pass vertex color attribute through

                // --- Turbulence Calculation ---
                vec3 turbulenceOffset = vec3(0.0);
                if (turbulenceAmount > 0.01) {
                    float effectiveTurbulence = turbulenceAmount * (1.0 + audioSpeedBoost * 2.5);
                    turbulenceOffset = fbmVec3(initialPos * 0.05, time) * effectiveTurbulence;
                }

                // Apply turbulence to the original position
                vec3 displacedPosition = position + turbulenceOffset;

                // Standard point projection
                vec4 mvPosition = modelViewMatrix * vec4(displacedPosition, 1.0);
                gl_Position = projectionMatrix * mvPosition;

                // Point size calculation
                float pointScale = scale / -mvPosition.z; // Perspective scaling
                float audioSizeFactor = 1.0 + audioSizeBoost * (vColor.r + vColor.g + vColor.b)/3.0; // Size boost based on audio and maybe color brightness
                gl_PointSize = size * baseSize * pointScale * audioSizeFactor;
            }`;

        const particleFragmentShader = `
            varying vec3 vColor;
            uniform sampler2D pointTexture; // Use default point texture

            void main() {
                // Simple circular point, colored by varying vColor
                float dist = length(gl_PointCoord - vec2(0.5));
                if (dist > 0.5) discard; // Make it circular

                gl_FragColor = vec4(vColor, 1.0 - smoothstep(0.45, 0.5, dist)); // Color + alpha fade at edge
                // gl_FragColor = gl_FragColor * texture2D( pointTexture, gl_PointCoord ); // Optional: Use texture
            }`;


        // Globals
        let baseFractalShaderMaterial = null;
        let guiInstance = null;
        let uiControlsVisible = true;
        let helpPanelVisible = false;
        const PRESET_STORAGE_KEY = 'fractalVisualizerPresets_v8'; // V8 Enhancement: Preset key


        init();
        animate();

        function init() {
            console.log("Initializing Hyper-Psychedelic Fractals V8.0...");
            clock = new THREE.Clock();

            // Loading Manager
            loadingManager = new THREE.LoadingManager( () => { const loadingScreen = document.getElementById('loading'); if (loadingScreen) loadingScreen.classList.add('hidden'); console.log("Hyper-Psychedelic Experience V8.0 Loaded!"); }, (url, itemsLoaded, itemsTotal) => {}, (url) => { console.error('Error loading ' + url); } );
            new THREE.TextureLoader(loadingManager).load('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=', () => {}); // Dummy texture load

            // Scene, Camera, Renderer, Controls
            scene = new THREE.Scene();
            masterGroup = new THREE.Group(); // Group for things affected by auto-rotate
            scene.add(masterGroup);
            camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 2000);
            camera.position.set(0, 0, 40); // Start further back
            const container = document.getElementById('container');
            try {
                renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false });
                renderer.setSize(window.innerWidth, window.innerHeight);
                renderer.setPixelRatio(window.devicePixelRatio);
                renderer.setClearColor(0x000000, 1);
                container.appendChild(renderer.domElement);
            } catch (e) {
                 console.error("WebGL Initialization failed:", e);
                 document.getElementById('loading').textContent = "Error: WebGL Failed to Initialize. Browser/GPU Unsupported?";
                 document.getElementById('loading').style.color = 'red';
                 return;
            }
            controls = new OrbitControls(camera, renderer.domElement);
            controls.enableDamping = true; controls.dampingFactor = 0.05; controls.screenSpacePanning = false;
            controls.minDistance = 1; controls.maxDistance = 350; // Increased max distance
            controls.autoRotate = false; // We control rotation via masterGroup
            controls.target.set(0, 0, 0); controls.zoomSpeed = 1.8; controls.enableRotate = true; controls.enablePan = true;

            // Lighting (Slightly adjusted intensities/colors)
            scene.add(new THREE.AmbientLight(0xffffff, 0.40));
            pointLight1 = new THREE.PointLight(0xff40ff, 3.0, 200, 1.6); pointLight1.position.set(35, 35, 35); scene.add(pointLight1);
            pointLight2 = new THREE.PointLight(0x40ffff, 3.0, 200, 1.6); pointLight2.position.set(-35, -35, 35); scene.add(pointLight2);
            pointLight3 = new THREE.PointLight(0xffff40, 2.5, 180, 1.6); pointLight3.position.set(0, 40, 25); scene.add(pointLight3);
            pointLight4 = new THREE.PointLight(0x40ff40, 2.8, 190, 1.6); pointLight4.position.set(40, -25, -40); scene.add(pointLight4);
            const backLight = new THREE.PointLight(0xffaa66, 2.0, 250, 1.8); backLight.position.set(0, 0, -80); scene.add(backLight);

            // Base Fractal Shader Material (V8 Updated Uniforms)
             baseFractalShaderMaterial = new THREE.ShaderMaterial({
                uniforms: THREE.UniformsUtils.clone({
                    time: { value: 0.0 }, levelRatio: { value: 0.0 }, audioLevel: { value: 0.0 },
                    baseHue: { value: 0.0 }, hueSpread: { value: 0.0 }, saturation: { value: 1.0 }, lightness: { value: 0.6 },
                    isDynamicHue: { value: false }, secondaryHue: { value: 0.0 }, useSecondaryHue: { value: false },
                    isGradientMap: { value: false }, // V8
                    gradientColor0: { value: new THREE.Color(0x000000) }, gradientStop0: { value: 0.0 }, // V8
                    gradientColor1: { value: new THREE.Color(0x000000) }, gradientStop1: { value: 0.0 }, // V8
                    gradientColor2: { value: new THREE.Color(0x000000) }, gradientStop2: { value: 0.0 }, // V8
                    gradientColor3: { value: new THREE.Color(0x000000) }, gradientStop3: { value: 0.0 }, // V8
                    flowSpeed: { value: guiControls.branchFlowSpeed }, pulseFrequency: { value: guiControls.branchPulseFrequency },
                    audioIntensity: { value: guiControls.fractalAudioIntensity }, baseEmission: { value: guiControls.branchBaseEmission },
                    distortionFactor: { value: guiControls.branchDistortionFactor },
                    noiseType: { value: (guiControls.fractalSurfaceNoiseType === 'Worley' ? 1 : 0) }, // V8
                    surfaceNoiseScale: { value: guiControls.fractalSurfaceNoiseScale },
                    surfaceNoiseIntensity: { value: guiControls.fractalSurfaceNoiseIntensity },
                }),
                vertexShader: fractalVertexShader,
                fragmentShader: fractalFragmentShader,
                side: THREE.DoubleSide // Render inside of tubes too
            });

            // Scene Objects
            createParticleSystem(); // V8 Enhanced particles
            createBackground();
            icoSphereMesh = createIcoSphere(guiControls.icoSphereRadius, guiControls.icoSphereDetail); // V8: Icosphere
            icoSphereMesh.visible = guiControls.showIcoSphere;
            masterGroup.add(icoSphereMesh);

            // Fog
            const fogGeometry = new THREE.SphereGeometry(500, 48, 48); // Larger fog sphere
            const fogMaterial = new THREE.ShaderMaterial(FogShader);
            fogMesh = new THREE.Mesh(fogGeometry, fogMaterial);
            fogMesh.renderOrder = -0.5; // Render behind transparent objects but before background
            scene.add(fogMesh);

            // Post Processing setup (V8 Enhanced)
            setupPostProcessing();

            // Initial Fractal Generation
            regenerateFractal();

            // Setup GUI (V8 Enhanced)
            guiInstance = setupGUI();
            if (guiInstance && guiInstance.domElement) {
                guiInstance.domElement.classList.add('ui-element-h');
            }

             // Audio Setup (V8 Enhanced: Mic Input)
             setupAudio();

            // Settings Load/Save Setup
            setupSettingsHandlers();

            // V8 Enhancement: Preset System Setup
            setupPresetSystem();

            // V8 Enhancement: Make GUI Draggable
            setupGuiDragging();

            // Event Listeners
            window.addEventListener('resize', onWindowResize, false);
            window.addEventListener('mousemove', onMouseMove, false);
            window.addEventListener('keydown', onKeyDown, false);
            controls.addEventListener('start', () => { guiControls.autoRotate = false; }); // Stop auto-rotate on manual interaction
        }

        // --- Helper Functions ---

        // V8 Enhancement: Updated Particle System
        function createParticleSystem() {
            const particlesGeometry = new THREE.BufferGeometry();
            const positions = [];
            const colors = [];
            const initialHues = [];
            const baseSizes = [];
            const initialPositions = []; // V8: Store initial positions for noise

            for (let i = 0; i < particleCount; i++) {
                // More distributed initial positions - less spherical shell, more volume
                const radius = 20 + Math.random() * 150; // Wider range
                const theta = Math.random() * Math.PI * 2;
                const phi = Math.acos((Math.random() * 2) - 1);
                const x = radius * Math.sin(phi) * Math.cos(theta);
                const y = radius * Math.sin(phi) * Math.sin(theta);
                const z = radius * Math.cos(phi);

                positions.push(x, y, z);
                initialPositions.push(x, y, z); // Store initial

                colors.push(1, 1, 1); // Initial color (will be updated)
                initialHues.push(Math.random()); // Store hue variation factor
                baseSizes.push(particleBaseSize * (0.7 + Math.random() * 0.6)); // Store size variation
            }

            particlesGeometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
            particlesGeometry.setAttribute('initialPos', new THREE.Float32BufferAttribute(initialPositions, 3)); // V8 Attribute
            particlesGeometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3));
            particlesGeometry.setAttribute('baseSize', new THREE.Float32BufferAttribute(baseSizes, 1));
            particlesGeometry.setAttribute('initialHue', new THREE.Float32BufferAttribute(initialHues, 1)); // V8 Attribute

            // V8: Use ShaderMaterial for custom vertex shader
            const particlesMaterial = new THREE.ShaderMaterial({
                uniforms: {
                    pointTexture: { value: new THREE.TextureLoader().load( 'https://threejs.org/examples/textures/sprites/disc.png' ) }, // Default point texture
                    size: { value: particleBaseSize }, // Global size factor (can be animated)
                    scale: { value: window.innerHeight / 2.0 }, // For perspective size correction
                    time: { value: 0.0 },
                    audioSizeBoost: { value: guiControls.particleAudioSizeBoost },
                    audioSpeedBoost: { value: guiControls.particleAudioSpeedBoost },
                    turbulenceAmount: { value: guiControls.particleTurbulence },
                    turbulenceSpeed: { value: guiControls.particleTurbulenceSpeed },
                },
                vertexShader: particleVertexShader,
                fragmentShader: particleFragmentShader,
                blending: THREE.AdditiveBlending,
                depthWrite: false,
                transparent: true,
                vertexColors: true // Use the 'color' attribute
            });


            if (particleSystem) {
                particleSystem.geometry?.dispose();
                particleSystem.material?.dispose();
                masterGroup.remove(particleSystem);
            }
            particleSystem = new THREE.Points(particlesGeometry, particlesMaterial);
            particleSystem.renderOrder = 1; // Render after opaque objects
            masterGroup.add(particleSystem);
            updateParticleColors(); // Initial color update
        }

        // V8 Enhancement: Update Particle Colors (factored out)
        function updateParticleColors(elapsedTime = clock?.getElapsedTime() ?? 0) {
            if (!particleSystem?.geometry?.attributes?.color || !particleSystem?.geometry?.attributes?.initialHue) return;

            const schemeName = guiControls.colorScheme;
            const scheme = colorSchemes[schemeName];
            if (!scheme || scheme.isGradient) return; // Don't apply HSL logic to gradient maps

            const colors = particleSystem.geometry.attributes.color;
            const initialHues = particleSystem.geometry.attributes.initialHue;
            const baseColor = new THREE.Color();
            const count = colors.count;
            const hueSpeed = scheme.hueSpeed || 0.035;

            if (scheme.dynamicHue) {
                currentBaseHue = (elapsedTime * hueSpeed) % 1.0;
            } else {
                currentBaseHue = scheme.baseHue;
            }

            for (let i = 0; i < count; i++) {
                let particleHue;
                const initialHueFactor = initialHues.getX(i);

                if (schemeName === 'Electric Dream') {
                    particleHue = (initialHueFactor < 0.5) ? currentBaseHue : scheme.secondaryHue;
                    particleHue = (particleHue + (initialHueFactor - 0.5) * 0.1) % 1.0;
                } else {
                    particleHue = (currentBaseHue + (initialHueFactor - 0.5) * scheme.hueSpread * 1.0) % 1.0;
                }

                const finalHue = (particleHue + elapsedTime * hueSpeed * 0.05) % 1.0;
                const sat = THREE.MathUtils.clamp(scheme.saturation * (0.7 + Math.random() * 0.5), 0.7, 1.0);
                const light = THREE.MathUtils.clamp(scheme.lightness * (0.6 + Math.random() * 0.7), 0.55, 1.0);

                baseColor.setHSL(finalHue < 0 ? finalHue + 1 : finalHue, sat, light);
                colors.setXYZ(i, baseColor.r, baseColor.g, baseColor.b);
            }
            colors.needsUpdate = true;
        }


        // Create Background (Unchanged)
        function createBackground() {
            const backgroundGeometry = new THREE.SphereGeometry(1600, 64, 64);
            const backgroundMaterial = new THREE.ShaderMaterial({
                uniforms: {
                    time: { value: 1.0 },
                    resolution: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) },
                    color1: { value: new THREE.Color(0x0a001a) }, // Dark blue/purple
                    color2: { value: new THREE.Color(0x003040) }, // Dark cyan/teal
                    color3: { value: new THREE.Color(0x501030) }, // Dark magenta/red
                    noiseScale: { value: 3.0 },
                    timeScale: { value: 0.12 },
                    audioInfluence: { value: 0.0 } // Driven by smoothedOverall
                },
                vertexShader: `varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`,
                fragmentShader: organicNoiseBackgroundFragmentShader,
                side: THREE.BackSide // Render on the inside
            });
            if (backgroundMesh) { backgroundMesh.geometry?.dispose(); backgroundMesh.material?.dispose(); scene.remove(backgroundMesh); }
            backgroundMesh = new THREE.Mesh(backgroundGeometry, backgroundMaterial);
            backgroundMesh.renderOrder = -1; // Render last
            scene.add(backgroundMesh);
        }

        // V8 Enhancement: Create Icosphere Wireframe
        function createIcoSphere(radius, detail) {
            const geometry = new THREE.IcosahedronGeometry(radius, detail);
            const wireframeGeometry = new THREE.WireframeGeometry(geometry);
            const material = new THREE.LineBasicMaterial({
                color: guiControls.icoSphereLineColor,
                linewidth: guiControls.icoSphereLineWidth, // Note: linewidth > 1 may not work on all platforms/drivers
                opacity: 0.85,
                transparent: true,
                blending: THREE.AdditiveBlending // Glowy effect
            });

            const wireframe = new THREE.LineSegments(wireframeGeometry, material);
            wireframe.userData = { geometry, material }; // Store refs for updates
            wireframe.renderOrder = 2; // Render after fractals
            return wireframe;
        }

        // V8 Enhancement: Regenerate Icosphere
        function regenerateIcoSphere() {
            if (icoSphereMesh) {
                icoSphereMesh.geometry?.dispose();
                // Material is reused or updated, no need to dispose typically unless shader changes
                masterGroup.remove(icoSphereMesh);
                icoSphereMesh = null;
            }
            icoSphereMesh = createIcoSphere(guiControls.icoSphereRadius, guiControls.icoSphereDetail);
            icoSphereMesh.visible = guiControls.showIcoSphere;
            // Update material properties that might have changed in guiControls but don't require full regen
            if (icoSphereMesh.material) {
                 icoSphereMesh.material.color.set(guiControls.icoSphereLineColor);
                 icoSphereMesh.material.linewidth = guiControls.icoSphereLineWidth;
            }
            masterGroup.add(icoSphereMesh);
        }

        // Setup Post Processing (V8 Enhanced: Added Lens Distortion, Edge Detection)
        function setupPostProcessing() {
            const rtOptions = { minFilter: THREE.LinearFilter, magFilter: THREE.LinearFilter, format: THREE.RGBAFormat, type: THREE.HalfFloatType };
            feedbackRenderTarget?.dispose();
            feedbackRenderTarget = new THREE.WebGLRenderTarget(window.innerWidth, window.innerHeight, rtOptions);

            composer = new EffectComposer(renderer);
            composer.addPass(new RenderPass(scene, camera));

            // 1. Bloom
            bloomPass = new UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 1.5, 0.4, 0.85);
            bloomPass.threshold = guiControls.bloomThreshold;
            bloomPass.strength = guiControls.bloomBaseStrength;
            bloomPass.radius = 0.5;
            composer.addPass(bloomPass);

            // 2. Afterimage
            afterimagePass = new AfterimagePass(guiControls.afterimageDamp);
            composer.addPass(afterimagePass);

            // 3. RGB Shift
            rgbShiftPass = new ShaderPass(RGBShiftShader);
            rgbShiftPass.uniforms['amount'].value = guiControls.rgbShiftBaseAmount;
            composer.addPass(rgbShiftPass);

            // 4. Kaleidoscope
            const kaleidoscopeShader = { uniforms: { 'tDiffuse': { value: null }, 'sides': { value: guiControls.kaleidoscopeSides }, 'angle': { value: 0.0 }, 'time': { value: 0.0 }, 'distortion': { value: guiControls.kaleidoscopeBaseDistortion }, 'distortionScale': { value: 8.5 }, 'mouseInfluence': { value: guiControls.kaleidoscopeMouseInfluence }, 'audioInfluence': { value: 0.0 } }, vertexShader: `varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); }`, fragmentShader: `uniform sampler2D tDiffuse; uniform float sides; uniform float angle; uniform float time; uniform float distortion; uniform float distortionScale; uniform float mouseInfluence; uniform float audioInfluence; varying vec2 vUv; const float PI = 3.14159265359; const float TAU = PI * 2.0; void main() { vec2 p = vUv - 0.5; float r = length(p); float a = atan(p.y, p.x) + angle; float segment = TAU / sides; a = mod(a, segment); a = abs(a - segment * 0.5); vec2 baseP = r * vec2(cos(a), sin(a)); float dynamicDistort = distortion + (sin(r * distortionScale + time * 2.1) * 0.5 + 0.5) * distortion * 1.5; dynamicDistort += mouseInfluence * distortion * (sin(time*0.8 + r * 2.5)*0.5+0.5) * 1.8; dynamicDistort += audioInfluence * distortion * 3.5 * (0.5 + sin(r * 5.0 - time * 1.5) * 0.5); float distFactor = 1.0 + dynamicDistort * (1.0 - r * 0.5); vec2 distortedP = baseP * distFactor; vec2 uv = distortedP + 0.5; vec4 color = texture2D(tDiffuse, clamp(uv, 0.0, 1.0)); gl_FragColor = color; }` };
            kaleidoscopePass = new ShaderPass(kaleidoscopeShader);
            composer.addPass(kaleidoscopePass);

            // 5. Mirror
            mirrorPass = new ShaderPass(MirrorShader);
            mirrorPass.uniforms.sides.value = guiControls.mirrorCount;
            composer.addPass(mirrorPass);

            // 6. V8 Enhancement: Lens Distortion
            lensDistortionPass = new ShaderPass(LensDistortionShader);
            lensDistortionPass.uniforms['amount'].value = guiControls.lensDistortionAmount;
            composer.addPass(lensDistortionPass);

            // 7. Glitch (Triggered by audio)
            glitchPass = new GlitchPass(64);
            glitchPass.enabled = false;
            glitchPass.goWild = false;
            composer.addPass(glitchPass);

            // --- Feedback Loop ---
            // 8. Save current frame for feedback
            feedbackSavePass?.renderTarget?.dispose();
            feedbackSavePass = new SavePass(feedbackRenderTarget);
            composer.addPass(feedbackSavePass);

            // 9. Apply feedback (using saved frame)
            feedbackPass = new ShaderPass(FeedbackShader);
            feedbackPass.uniforms.tFeedback.value = feedbackRenderTarget.texture;
            feedbackPass.uniforms.mixAmount.value = guiControls.feedbackMixAmount;
            feedbackPass.uniforms.colorShiftAmount.value = guiControls.feedbackColorShift; // V8
            composer.addPass(feedbackPass);
            // --- End Feedback Loop ---

            // 10. Vignette
            vignettePass = new ShaderPass(VignetteShader);
            vignettePass.uniforms['offset'].value = guiControls.vignetteOffset;
            vignettePass.uniforms['darkness'].value = guiControls.vignetteDarkness;
            vignettePass.enabled = guiControls.vignetteEnabled;
            composer.addPass(vignettePass);

            // 11. V8 Enhancement: Edge Detection (Sobel)
            edgeDetectionPass = new ShaderPass(SobelOperatorShader);
            edgeDetectionPass.uniforms['resolution'].value.x = window.innerWidth * window.devicePixelRatio;
            edgeDetectionPass.uniforms['resolution'].value.y = window.innerHeight * window.devicePixelRatio;
            edgeDetectionPass.uniforms['tDiffuse'].value = null; // Will be set by composer
            edgeDetectionPass.uniforms['uThreshold'] = { value: guiControls.edgeDetectThreshold }; // Custom uniform name for threshold
            edgeDetectionPass.uniforms['uEdgeColor'] = { value: new THREE.Color(guiControls.edgeDetectColor) }; // Custom uniform for color
            // Modify the Sobel shader slightly to use threshold and color
            edgeDetectionPass.material.fragmentShader = `
                uniform sampler2D tDiffuse;
                uniform vec2 resolution;
                uniform float uThreshold; // V8
                uniform vec3 uEdgeColor; // V8
                varying vec2 vUv;

                void main() {
                    vec2 texel = vec2( 1.0 / resolution.x, 1.0 / resolution.y );
                    // kernel definition (in glsl matrices are filled column first)
                    const mat3 Gx = mat3( -1, -2, -1, 0, 0, 0, 1, 2, 1 ); // x direction kernel
                    const mat3 Gy = mat3( -1, 0, 1, -2, 0, 2, -1, 0, 1 ); // y direction kernel
                    // fetch the 3x3 neighbourhood of a fragment
                    float tx0y0 = texture2D( tDiffuse, vUv + texel * vec2( -1, -1 ) ).r;
                    float tx0y1 = texture2D( tDiffuse, vUv + texel * vec2( -1,  0 ) ).r;
                    float tx0y2 = texture2D( tDiffuse, vUv + texel * vec2( -1,  1 ) ).r;
                    float tx1y0 = texture2D( tDiffuse, vUv + texel * vec2(  0, -1 ) ).r;
                    float tx1y1 = texture2D( tDiffuse, vUv + texel * vec2(  0,  0 ) ).r;
                    float tx1y2 = texture2D( tDiffuse, vUv + texel * vec2(  0,  1 ) ).r;
                    float tx2y0 = texture2D( tDiffuse, vUv + texel * vec2(  1, -1 ) ).r;
                    float tx2y1 = texture2D( tDiffuse, vUv + texel * vec2(  1,  0 ) ).r;
                    float tx2y2 = texture2D( tDiffuse, vUv + texel * vec2(  1,  1 ) ).r;
                    // calculate the gradient length
                    mat3 I;
                    I[0] = vec3( tx0y0, tx1y0, tx2y0 );
                    I[1] = vec3( tx0y1, tx1y1, tx2y1 );
                    I[2] = vec3( tx0y2, tx1y2, tx2y2 );
                    float Gx_ = dot( Gx[0], I[0] ) + dot( Gx[1], I[1] ) + dot( Gx[2], I[2] );
                    float Gy_ = dot( Gy[0], I[0] ) + dot( Gy[1], I[1] ) + dot( Gy[2], I[2] );
                    float gradient_length = sqrt( Gx_ * Gx_ + Gy_ * Gy_ );

                    // V8: Apply threshold and color
                    if (gradient_length > uThreshold) {
                        gl_FragColor = vec4(uEdgeColor, 1.0);
                    } else {
                        // Option 1: Show original image where no edge
                        // gl_FragColor = texture2D( tDiffuse, vUv );
                        // Option 2: Make non-edges transparent (better for overlay)
                        discard; // Or gl_FragColor = vec4(0.0);
                    }
                }`;
            edgeDetectionPass.enabled = guiControls.edgeDetectEnabled;
            composer.addPass(edgeDetectionPass);


            // 12. Film Grain / Scanlines
            filmPass = new FilmPass( guiControls.filmNoiseIntensity, guiControls.filmScanlineIntensity, 1280, guiControls.filmGrayscale );
            composer.addPass(filmPass);
        }

        // Setup GUI (V8 Enhanced: Added controls for new features)
        function setupGUI() {
            if (!GUI) { console.error("dat.GUI not available."); return null; }
            if (guiInstance) { guiInstance.destroy(); guiInstance = null; }

            const gui = new GUI({ width: 340 }); // Wider GUI
            gui.domElement.style.zIndex = "20";

            // --- Fractal ---
            const fractalFolder = gui.addFolder('Fractal');
            fractalFolder.add(guiControls, 'fractalType', ['Symmetric Tree', 'Asymmetric', '3D Branching', 'Koch 3D']).name('Type').onChange(regenerateFractal); // V8: Added Koch
            fractalFolder.add(guiControls, 'numFractals', 1, 9, 1).name('Count').onChange(regenerateFractal);
            fractalFolder.add(guiControls, 'maxDepth', 4, 9, 1).name('Depth').onChange(regenerateFractal);
            fractalFolder.open();

            // --- Fractal Style ---
            const fractalStyleFolder = gui.addFolder('Fractal Style');
            fractalStyleFolder.add(guiControls, 'colorScheme', Object.keys(colorSchemes)).name('Color Scheme').onChange(() => { updateColorSchemeUniforms(); updateParticleColors(); });
            fractalStyleFolder.add(guiControls, 'branchFlowSpeed', 0.1, 20.0, 0.1).name('Flow Speed').onChange(updateFractalShaderUniforms);
            fractalStyleFolder.add(guiControls, 'branchPulseFrequency', 1.0, 30.0, 0.5).name('Pulse Freq').onChange(updateFractalShaderUniforms);
            fractalStyleFolder.add(guiControls, 'branchBaseEmission', 0.0, 2.0, 0.05).name('Base Emission').onChange(updateFractalShaderUniforms);
            fractalStyleFolder.add(guiControls, 'branchDistortionFactor', 0.0, 0.3, 0.005).name('Branch Warp').onChange(updateFractalShaderUniforms);
            fractalStyleFolder.add(guiControls, 'fractalSurfaceNoiseType', ['FBM', 'Worley']).name('Surface Noise Type').onChange(updateFractalShaderUniforms); // V8
            fractalStyleFolder.add(guiControls, 'fractalSurfaceNoiseScale', 0.1, 10.0, 0.1).name('Surface Noise Scale').onChange(updateFractalShaderUniforms);
            fractalStyleFolder.add(guiControls, 'fractalSurfaceNoiseIntensity', 0.0, 0.5, 0.01).name('Surface Noise Intensity').onChange(updateFractalShaderUniforms);
            fractalStyleFolder.add(guiControls, 'fractalAudioIntensity', 0.0, guiControls.fractalAudioIntensity, 0.2).name('Audio Intensity').onChange(updateFractalShaderUniforms); // Reflects guiControls max

            // --- Icosphere --- (V8 Updated)
            const sphereFolder = gui.addFolder('Icosphere');
            sphereFolder.add(guiControls, 'showIcoSphere').name('Show').onChange(v => { if(icoSphereMesh) icoSphereMesh.visible = v; });
            sphereFolder.add(guiControls, 'icoSphereRadius', 5, 80, 0.5).name('Radius').onChange(regenerateIcoSphere);
            sphereFolder.add(guiControls, 'icoSphereDetail', 0, 6, 1).name('Detail').onChange(regenerateIcoSphere); // Detail is subdivision level
            sphereFolder.add(guiControls, 'icoSphereRotateSpeed', 0, 1.5, 0.01).name('Rotate Speed');
            sphereFolder.addColor(guiControls, 'icoSphereLineColor').name('Line Color').onChange(v => { if (icoSphereMesh?.material) icoSphereMesh.material.color.set(v);});
            sphereFolder.add(guiControls, 'icoSphereLineWidth', 0.5, 5.0, 0.1).name('Line Width').onChange(v => { if (icoSphereMesh?.material) icoSphereMesh.material.linewidth = v;}); // Note: May not work > 1
            sphereFolder.add(guiControls, 'icoSphereAudioScaleBoost', 0.0, guiControls.icoSphereAudioScaleBoost, 0.05).name('Audio Scale Boost'); // Reflects guiControls max

            // --- Background & Fog ---
            const backgroundFolder = gui.addFolder('Background & Fog');
            backgroundFolder.addColor(guiControls, 'fogColor').name('Fog Color').onChange(v => { if(fogMesh?.material?.uniforms?.fogColor) fogMesh.material.uniforms.fogColor.value.set(v); });
            backgroundFolder.add(guiControls, 'fogNoiseScale', 0.1, 5.0, 0.05).name('Fog Noise Scale').onChange(v => { if(fogMesh?.material?.uniforms?.noiseScale) fogMesh.material.uniforms.noiseScale.value = v; });
            backgroundFolder.add(guiControls, 'fogDensity', 0.0, 0.5, 0.005).name('Fog Density').onChange(v => { if(fogMesh?.material?.uniforms?.density) fogMesh.material.uniforms.density.value = v; });
            backgroundFolder.add(guiControls, 'fogAudioInfluence', 0.0, 5.0, 0.1).name('Fog Audio Reactivity');

            // --- Core Effects ---
            const coreEffectsFolder = gui.addFolder('Core Effects');
            coreEffectsFolder.add(guiControls, 'bloomBaseStrength', 0.0, 6.0, 0.05).name('Bloom Strength');
            coreEffectsFolder.add(guiControls, 'bloomThreshold', 0.0, 0.5, 0.01).name('Bloom Threshold').onChange(v => { if(bloomPass) bloomPass.threshold = v; });
            coreEffectsFolder.add(guiControls, 'bloomAudioBoost', 0.0, guiControls.bloomAudioBoost, 0.1).name('Bloom Audio Boost'); // Reflects guiControls max
            coreEffectsFolder.add(guiControls, 'afterimageDamp', 0.7, 0.995, 0.005).name('Afterimage Damp').onChange(v => { if(afterimagePass) afterimagePass.uniforms["damp"].value = v; });
            coreEffectsFolder.add(guiControls, 'afterimageAudioInfluence', 0.0, guiControls.afterimageAudioInfluence, 0.005).name('Afterimage Audio'); // Reflects guiControls max
            coreEffectsFolder.add(guiControls, 'rgbShiftBaseAmount', 0.0, 0.03, 0.0005).name('RGB Shift Amount');
            coreEffectsFolder.add(guiControls, 'rgbShiftAudioBoost', 0.0, guiControls.rgbShiftAudioBoost, 0.1).name('RGB Shift Audio Boost'); // Reflects guiControls max
            coreEffectsFolder.add(guiControls, 'filmNoiseIntensity', 0.0, 2.0, 0.01).name('Film Noise').onChange(v => { if(filmPass?.uniforms.nIntensity) filmPass.uniforms.nIntensity.value = v; });
            coreEffectsFolder.add(guiControls, 'filmScanlineIntensity', 0.0, 2.5, 0.02).name('Film Scanlines').onChange(v => { if(filmPass?.uniforms.sIntensity) filmPass.uniforms.sIntensity.value = v; });
            coreEffectsFolder.add(guiControls, 'filmGrayscale').name('Film Grayscale').onChange(v => { if(filmPass?.uniforms.grayscale) filmPass.uniforms.grayscale.value = v; });
            coreEffectsFolder.add(guiControls, 'filmAudioBoost', 0.0, guiControls.filmAudioBoost, 0.1).name('Film Audio Boost'); // Reflects guiControls max
            coreEffectsFolder.add(guiControls, 'vignetteEnabled').name('Vignette').onChange(v => { if(vignettePass) vignettePass.enabled = v; });
            coreEffectsFolder.add(guiControls, 'vignetteOffset', 0.0, 1.5, 0.01).name('Vignette Offset').onChange(v => { if(vignettePass) vignettePass.uniforms['offset'].value = v; });
            coreEffectsFolder.add(guiControls, 'vignetteDarkness', 0.0, 1.0, 0.01).name('Vignette Darkness').onChange(v => { if(vignettePass) vignettePass.uniforms['darkness'].value = v; });
            coreEffectsFolder.add(guiControls, 'pointLightAudioIntensity', 0.0, guiControls.pointLightAudioIntensity, 0.1).name('Light Audio Intensity'); // Reflects guiControls max

            // --- Advanced Effects --- (V8 Enhanced)
            const advancedEffectsFolder = gui.addFolder('Advanced Effects');
            advancedEffectsFolder.add(guiControls, 'kaleidoscopeSides', 2, 24, 1).name('Kaleido Sides').onChange(v => { if(kaleidoscopePass) kaleidoscopePass.uniforms.sides.value = v; });
            advancedEffectsFolder.add(guiControls, 'kaleidoscopeBaseDistortion', 0.0, 1.0, 0.01).name('Kaleido Distortion');
            advancedEffectsFolder.add(guiControls, 'kaleidoscopeAudioBoost', 0.0, guiControls.kaleidoscopeAudioBoost, 0.1).name('Kaleido Audio Distort'); // Reflects guiControls max
            advancedEffectsFolder.add(guiControls, 'kaleidoscopeAngleAudioBoost', 0.0, guiControls.kaleidoscopeAngleAudioBoost, 0.1).name('Kaleido Audio Speed'); // Reflects guiControls max
            advancedEffectsFolder.add(guiControls, 'kaleidoscopeMouseInfluence', 0.0, 1.0, 0.01).name('Kaleido Mouse Distort').onChange(v => { if(kaleidoscopePass) kaleidoscopePass.uniforms.mouseInfluence.value = v; });
            advancedEffectsFolder.add(guiControls, 'mirrorCount', { Off: 1, Vertical: 2, Quad: 4, Oct: 8, Hex: 16 } ).name('Mirror Mode').onChange(v => { if(mirrorPass) mirrorPass.uniforms.sides.value = parseFloat(v); });
            advancedEffectsFolder.add(guiControls, 'feedbackMixAmount', 0.75, 0.995, 0.005).name('Feedback Mix').onChange(v => { if(feedbackPass) feedbackPass.uniforms.mixAmount.value = v; });
            advancedEffectsFolder.add(guiControls, 'feedbackColorShift', 0.0, 0.1, 0.001).name('Feedback Color Shift').onChange(v => { if(feedbackPass) feedbackPass.uniforms.colorShiftAmount.value = v; }); // V8
            advancedEffectsFolder.add(guiControls, 'feedbackAudioInfluence', 0.0, guiControls.feedbackAudioInfluence, 0.1).name('Feedback Audio'); // Reflects guiControls max
            advancedEffectsFolder.add(guiControls, 'glitchAudioTrigger', 0.5, guiControls.glitchAudioTrigger, 0.1).name('Glitch Audio Trigger'); // Reflects guiControls max
            advancedEffectsFolder.add(guiControls, 'lensDistortionAmount', -0.5, 0.5, 0.01).name('Lens Distortion').onChange(v => { if(lensDistortionPass) lensDistortionPass.uniforms.amount.value = v; }); // V8
            advancedEffectsFolder.add(guiControls, 'lensDistortionAudioBoost', 0.0, guiControls.lensDistortionAudioBoost, 0.1).name('Lens Dist Audio'); // Reflects guiControls max
            advancedEffectsFolder.add(guiControls, 'edgeDetectEnabled').name('Edge Detect').onChange(v => { if(edgeDetectionPass) edgeDetectionPass.enabled = v; }); // V8
            advancedEffectsFolder.addColor(guiControls, 'edgeDetectColor').name('Edge Color').onChange(v => { if(edgeDetectionPass?.uniforms?.uEdgeColor) edgeDetectionPass.uniforms.uEdgeColor.value.set(v); }); // V8
            advancedEffectsFolder.add(guiControls, 'edgeDetectThreshold', 0.01, 0.5, 0.01).name('Edge Threshold').onChange(v => { if(edgeDetectionPass?.uniforms?.uThreshold) edgeDetectionPass.uniforms.uThreshold.value = v; }); // V8
            advancedEffectsFolder.add(guiControls, 'edgeDetectAudioThresholdBoost', 0.0, guiControls.edgeDetectAudioThresholdBoost, 0.05).name('Edge Audio Boost'); // Reflects guiControls max

            // --- Particles --- (V8 New Folder)
            const particleFolder = gui.addFolder('Particles');
            particleFolder.add(guiControls, 'particleTurbulence', 0.0, 5.0, 0.05).name('Turbulence Amount').onChange(v => { if(particleSystem?.material.uniforms.turbulenceAmount) particleSystem.material.uniforms.turbulenceAmount.value = v; });
            particleFolder.add(guiControls, 'particleTurbulenceSpeed', 0.0, 1.0, 0.01).name('Turbulence Speed').onChange(v => { if(particleSystem?.material.uniforms.turbulenceSpeed) particleSystem.material.uniforms.turbulenceSpeed.value = v; });
            particleFolder.add(guiControls, 'particleAudioSizeBoost', 0.0, guiControls.particleAudioSizeBoost, 0.1).name('Audio Size Boost').onChange(v => { if(particleSystem?.material.uniforms.audioSizeBoost) particleSystem.material.uniforms.audioSizeBoost.value = v; }); // Reflects guiControls max
            particleFolder.add(guiControls, 'particleAudioSpeedBoost', 0.0, guiControls.particleAudioSpeedBoost, 0.1).name('Audio Speed Boost').onChange(v => { if(particleSystem?.material.uniforms.audioSpeedBoost) particleSystem.material.uniforms.audioSpeedBoost.value = v; }); // Reflects guiControls max
            particleFolder.add(guiControls, 'particleColorAudioBoost', 0.0, guiControls.particleColorAudioBoost, 0.1).name('Color Audio Boost'); // Reflects guiControls max

            // --- Camera & Scene Rotation ---
            const interactionFolder = gui.addFolder('Camera & Scene Rotation');
            interactionFolder.add(guiControls, 'autoRotate').name('Auto Rotate Scene');
            interactionFolder.add(guiControls, 'autoRotateSpeedX', -0.5, 0.5, 0.005).name('Rotate Speed X');
            interactionFolder.add(guiControls, 'autoRotateSpeedY', -0.5, 0.5, 0.005).name('Rotate Speed Y');
            interactionFolder.add(guiControls, 'autoRotateSpeedZ', -0.5, 0.5, 0.005).name('Rotate Speed Z');
            interactionFolder.add(guiControls, 'cameraFovAudioBoost', 0.0, guiControls.cameraFovAudioBoost, 0.5).name('FOV Audio Boost'); // Reflects guiControls max
            interactionFolder.add(guiControls, 'autoRotateAudioBoost', 0.0, guiControls.autoRotateAudioBoost, 0.1).name('Rotate Audio Boost'); // Reflects guiControls max
            interactionFolder.open();

            // --- Add Footer Instructions ---
            const footerInfo = document.createElement('div');
            footerInfo.classList.add('gui-footer-info');
            footerInfo.textContent = 'H: Toggle UI | I: Toggle Help | Drag Panel';
            gui.domElement.appendChild(footerInfo);
            // Prevent dragging when clicking title/folder names
            gui.domElement.querySelectorAll('.dg li.title').forEach(title => {
                title.style.cursor = 'default';
            });
            // --- End Footer Instructions ---

            return gui;
        }

        // Update Fractal Shader Uniforms (V8 Updated for noise type)
        function updateFractalShaderUniforms() {
             if (!fractalRoots || fractalRoots.length === 0) return;
             const noiseTypeVal = guiControls.fractalSurfaceNoiseType === 'Worley' ? 1 : 0; // V8
             fractalRoots.forEach(root => {
                 root.traverse((o) => {
                     if (o.isMesh && o.material?.isShaderMaterial && o.userData.level !== undefined) {
                         const u = o.material.uniforms;
                         if(u.flowSpeed) u.flowSpeed.value = guiControls.branchFlowSpeed;
                         if(u.pulseFrequency) u.pulseFrequency.value = guiControls.branchPulseFrequency;
                         if(u.audioIntensity) u.audioIntensity.value = guiControls.fractalAudioIntensity;
                         if(u.baseEmission) u.baseEmission.value = guiControls.branchBaseEmission;
                         if(u.distortionFactor) u.distortionFactor.value = guiControls.branchDistortionFactor;
                         if(u.noiseType) u.noiseType.value = noiseTypeVal; // V8
                         if(u.surfaceNoiseScale) u.surfaceNoiseScale.value = guiControls.fractalSurfaceNoiseScale;
                         if(u.surfaceNoiseIntensity) u.surfaceNoiseIntensity.value = guiControls.fractalSurfaceNoiseIntensity;
                     }
                 });
             });
        }

        // Update Color Scheme Uniforms (V8 Updated for Gradient Map)
         function updateColorSchemeUniforms() {
             const schemeName = guiControls.colorScheme;
             const scheme = colorSchemes[schemeName];
             if (!scheme || !baseFractalShaderMaterial || !fractalRoots || fractalRoots.length === 0) return;

             const isDynamic = !!scheme.dynamicHue;
             const useSecondary = schemeName === 'Electric Dream';
             const isGradient = !!scheme.isGradient; // V8

             fractalRoots.forEach(root => {
                 root.traverse((o) => {
                     if (o.isMesh && o.material?.isShaderMaterial && o.userData.level !== undefined) {
                         const u = o.material.uniforms;
                         if(u.isGradientMap) u.isGradientMap.value = isGradient; // V8

                         if (isGradient) { // V8: Set gradient uniforms
                             if (scheme.gradient && scheme.gradient.length >= 4) { // Ensure 4 stops defined
                                 if(u.gradientColor0) u.gradientColor0.value.copy(scheme.gradient[0].color);
                                 if(u.gradientStop0) u.gradientStop0.value = scheme.gradient[0].stop;
                                 if(u.gradientColor1) u.gradientColor1.value.copy(scheme.gradient[1].color);
                                 if(u.gradientStop1) u.gradientStop1.value = scheme.gradient[1].stop;
                                 if(u.gradientColor2) u.gradientColor2.value.copy(scheme.gradient[2].color);
                                 if(u.gradientStop2) u.gradientStop2.value = scheme.gradient[2].stop;
                                 if(u.gradientColor3) u.gradientColor3.value.copy(scheme.gradient[3].color);
                                 if(u.gradientStop3) u.gradientStop3.value = scheme.gradient[3].stop;
                             }
                         } else { // Original HSL logic
                             if(u.baseHue) u.baseHue.value = scheme.baseHue;
                             if(u.hueSpread) u.hueSpread.value = scheme.hueSpread;
                             if(u.saturation) u.saturation.value = scheme.saturation;
                             if(u.lightness) u.lightness.value = scheme.lightness;
                             if(u.isDynamicHue) u.isDynamicHue.value = isDynamic;
                             if(u.secondaryHue) u.secondaryHue.value = scheme.secondaryHue || 0.0;
                             if(u.useSecondaryHue) u.useSecondaryHue.value = useSecondary;
                         }
                     }
                 });
             });

             // Update currentBaseHue for dynamic HSL schemes (not used by gradient)
             if (!isGradient && isDynamic) {
                 const speed = scheme.hueSpeed || 0.05;
                 currentBaseHue = (clock.getElapsedTime() * speed) % 1.0;
             } else if (!isGradient) {
                 currentBaseHue = scheme.baseHue;
             }

             updateParticleColors(); // Update particle colors based on the new scheme (if not gradient)
         }

        // --- Fractal Building (V8: Added Koch 3D type) ---
         function regenerateFractal() {
            console.log(`Regenerating ${guiControls.numFractals} fractal(s) type ${guiControls.fractalType} depth ${guiControls.maxDepth}`);
            // Cleanup old fractals
            fractalRoots.forEach(root => {
                root.traverse((o) => {
                    if (o.isMesh) {
                        o.geometry?.dispose();
                        // Dispose material only if it's a clone, not the base
                        if (o.material && o.material !== baseFractalShaderMaterial) o.material.dispose();
                    }
                });
                masterGroup.remove(root);
            });
            fractalRoots = [];
            dynamicBranchNodes = []; // Reset animated nodes

            if (!baseFractalShaderMaterial) {
                console.error("Base fractal shader material not initialized!");
                return;
            }
            // Ensure the base material has the latest color scheme info before cloning
            updateColorSchemeUniforms();

            const count = guiControls.numFractals;
            const depth = guiControls.maxDepth;
            const type = guiControls.fractalType;

            for (let i = 0; i < count; i++) {
                const root = new THREE.Object3D();
                // Position multiple fractals
                if (count > 1) {
                    const angle = (i / count) * Math.PI * 2;
                    const radius = fractalSpreadRadius * (1.0 + (count - 1) * 0.18);
                    root.position.set(Math.cos(angle) * radius, -initialBranchLength * 1.5, Math.sin(angle) * radius); // Start slightly higher
                    root.rotation.y = -angle + Math.PI / 2 + (Math.random() - 0.5) * 0.3;
                } else {
                    root.position.set(0, -initialBranchLength * 1.5, 0); // Start slightly higher
                }
                masterGroup.add(root);
                fractalRoots.push(root);

                // Create Trunk
                const trunkLength = initialBranchLength;
                const trunkRadius = baseBranchRadius;
                const trunkPath = new THREE.LineCurve3(new THREE.Vector3(0, 0, 0), new THREE.Vector3(0, trunkLength, 0));
                const trunkGeo = new THREE.TubeGeometry(trunkPath, 3, trunkRadius, 9, false);
                const trunkMat = baseFractalShaderMaterial.clone(); // Clone for unique uniforms if needed later
                trunkMat.uniforms.levelRatio.value = 0.0 / depth;
                // Ensure cloned uniforms exist
                trunkMat.uniforms.time = trunkMat.uniforms.time || { value: 0.0 };
                trunkMat.uniforms.audioLevel = trunkMat.uniforms.audioLevel || { value: 0.0 };
                const trunkMesh = new THREE.Mesh(trunkGeo, trunkMat);
                trunkMesh.userData.level = 0;
                trunkMesh.renderOrder = 2; // Render after background/fog
                root.add(trunkMesh);

                // Starting point for branches
                const firstBranchNode = new THREE.Object3D();
                firstBranchNode.position.set(0, trunkLength, 0);
                root.add(firstBranchNode);

                const startLength = initialBranchLength * lengthFactor;
                const startRadius = baseBranchRadius * radiusFactor;

                // Build based on type
                if (type === 'Symmetric Tree') buildBranchSymmetric(firstBranchNode, startLength, startRadius, 1, depth);
                else if (type === 'Asymmetric') buildBranchAsymmetric(firstBranchNode, startLength, startRadius, 1, depth);
                else if (type === '3D Branching') buildBranch3D(firstBranchNode, startLength, startRadius, 1, depth);
                else if (type === 'Koch 3D') buildBranchKoch(firstBranchNode, startLength, startRadius, 1, depth); // V8
            }
        }

        // Create Branch Mesh (Helper)
        function createBranchMesh(length, radius, level, maxDepth) {
            if (radius < 0.001) radius = 0.001; // Prevent zero radius
            const path = new THREE.LineCurve3(new THREE.Vector3(0, 0, 0), new THREE.Vector3(0, length, 0));
            // Reduce segments for higher levels (performance)
            const radialSegments = Math.max(3, 8 - Math.floor(level * 1.3));
            const tubularSegments = 1; // Keep low for tubes
            const branchGeo = new THREE.TubeGeometry(path, tubularSegments, radius, radialSegments, false);

            // Clone material for each branch to allow individual uniform values if needed later
            // (Currently only levelRatio differs significantly, but good practice)
            const branchMat = baseFractalShaderMaterial.clone();
            const levelRatio = THREE.MathUtils.clamp(level / maxDepth, 0, 1);
            // Ensure uniforms exist on the clone and set levelRatio
            branchMat.uniforms.levelRatio = { value: levelRatio };
            branchMat.uniforms.time = branchMat.uniforms.time || { value: 0.0 };
            branchMat.uniforms.audioLevel = branchMat.uniforms.audioLevel || { value: 0.0 };
            // Copy gradient uniforms if applicable (they are set on the base material)
             if (branchMat.uniforms.isGradientMap?.value) {
                 branchMat.uniforms.gradientColor0 = baseFractalShaderMaterial.uniforms.gradientColor0;
                 branchMat.uniforms.gradientStop0 = baseFractalShaderMaterial.uniforms.gradientStop0;
                 branchMat.uniforms.gradientColor1 = baseFractalShaderMaterial.uniforms.gradientColor1;
                 branchMat.uniforms.gradientStop1 = baseFractalShaderMaterial.uniforms.gradientStop1;
                 branchMat.uniforms.gradientColor2 = baseFractalShaderMaterial.uniforms.gradientColor2;
                 branchMat.uniforms.gradientStop2 = baseFractalShaderMaterial.uniforms.gradientStop2;
                 branchMat.uniforms.gradientColor3 = baseFractalShaderMaterial.uniforms.gradientColor3;
                 branchMat.uniforms.gradientStop3 = baseFractalShaderMaterial.uniforms.gradientStop3;
             }


            const branchMesh = new THREE.Mesh(branchGeo, branchMat);
            branchMesh.userData.level = level;
            branchMesh.renderOrder = 2;
            return branchMesh;
        }

        // Build Symmetric Branch
        function buildBranchSymmetric(parentObject, length, radius, level, maxDepth) {
            if (level > maxDepth || length < 0.02 || radius < 0.002) return;

            const branchNode = new THREE.Object3D(); // Node to apply rotation to both children
            parentObject.add(branchNode);
            dynamicBranchNodes.push(branchNode); // Add to list for animation

            const nextLength = length * lengthFactor;
            const nextRadius = radius * radiusFactor;

            // Right Branch
            const rightBranchHolder = new THREE.Object3D(); // Holder for mesh + next node
            const rightMesh = createBranchMesh(length, radius, level, maxDepth);
            rightBranchHolder.add(rightMesh);
            // Position the holder (rotation will be applied to branchNode)
            branchNode.add(rightBranchHolder);
            // Add node for next level recursion at the end of this branch
            const nextRightNode = new THREE.Object3D();
            nextRightNode.position.set(0, length, 0);
            rightBranchHolder.add(nextRightNode);
            buildBranchSymmetric(nextRightNode, nextLength, nextRadius, level + 1, maxDepth);

            // Left Branch (Mirror of Right)
            const leftBranchHolder = new THREE.Object3D();
            const leftMesh = createBranchMesh(length, radius, level, maxDepth);
            leftBranchHolder.add(leftMesh);
            branchNode.add(leftBranchHolder);
            const nextLeftNode = new THREE.Object3D();
            nextLeftNode.position.set(0, length, 0);
            leftBranchHolder.add(nextLeftNode);
            buildBranchSymmetric(nextLeftNode, nextLength, nextRadius, level + 1, maxDepth);
        }

        // Build Asymmetric Branch
        function buildBranchAsymmetric(parentObject, length, radius, level, maxDepth) {
             if (level > maxDepth || length < 0.02 || radius < 0.002) return;

             const branchNode = new THREE.Object3D();
             parentObject.add(branchNode);
             dynamicBranchNodes.push(branchNode);

             const lenFactorL = 0.80; // Longer branch factor
             const lenFactorR = 0.55; // Shorter branch factor
             const angleVariation = Math.PI / 12; // Random angle variation

             // Right (Shorter) Branch
             const rightBranchHolder = new THREE.Object3D();
             const rightLength = length * lenFactorR;
             const rightRadius = radius * Math.sqrt(lenFactorR); // Scale radius by sqrt of length factor
             const rightMesh = createBranchMesh(rightLength, rightRadius, level, maxDepth);
             rightBranchHolder.userData = { baseAngle: -1, angleVar: angleVariation * (Math.random() - 0.5) }; // Store angle info
             rightBranchHolder.add(rightMesh);
             branchNode.add(rightBranchHolder);
             const nextRightNode = new THREE.Object3D();
             nextRightNode.position.set(0, rightLength, 0);
             rightBranchHolder.add(nextRightNode);
             buildBranchAsymmetric(nextRightNode, rightLength * lengthFactor, rightRadius * radiusFactor, level + 1, maxDepth);

             // Left (Longer) Branch
             const leftBranchHolder = new THREE.Object3D();
             const leftLength = length * lenFactorL;
             const leftRadius = radius * Math.sqrt(lenFactorL);
             const leftMesh = createBranchMesh(leftLength, leftRadius, level, maxDepth);
             leftBranchHolder.userData = { baseAngle: 1, angleVar: angleVariation * (Math.random() - 0.5) };
             leftBranchHolder.add(leftMesh);
             branchNode.add(leftBranchHolder);
             const nextLeftNode = new THREE.Object3D();
             nextLeftNode.position.set(0, leftLength, 0);
             leftBranchHolder.add(nextLeftNode);
             buildBranchAsymmetric(nextLeftNode, leftLength * lengthFactor, leftRadius * radiusFactor, level + 1, maxDepth);
        }

        // Build 3D Branching
         function buildBranch3D(parentObject, length, radius, level, maxDepth) {
             if (level > maxDepth || length < 0.03 || radius < 0.002) return;

             const branchNode = new THREE.Object3D(); // This node itself won't be animated directly usually
             parentObject.add(branchNode);
             // dynamicBranchNodes.push(branchNode); // Don't animate the base node, animate holders

             const nextLengthFactor = lengthFactor * (0.75 + Math.random()*0.35);
             const numBranches = Math.random() < 0.4 ? 3 : 2; // 2 or 3 branches
             const baseAngleZ = Math.PI / (numBranches + 0.7); // Base angle between branches around Y

             for(let i = 0; i < numBranches; i++) {
                 const branchHolder = new THREE.Object3D(); // This holder WILL be animated
                 dynamicBranchNodes.push(branchHolder); // Add holder to animation list

                 const branchLength = length * (0.8 + Math.random() * 0.3);
                 const branchRadius = radius * (0.85 + Math.random() * 0.15);
                 const branchMesh = createBranchMesh(branchLength, branchRadius, level, maxDepth);

                 // Calculate random angles for 3D spread
                 const angleZ = (i + 1) * baseAngleZ * (Math.random() * 0.5 + 0.75) - baseAngleZ * (numBranches+1)/2 * 1.1; // Spread around Y
                 const angleX = (Math.random() - 0.5) * Math.PI / 2.2; // Tilt forward/back
                 const angleY = (Math.random() - 0.5) * Math.PI / 2.5; // Twist (less common)

                 branchHolder.rotation.set(angleX, angleY, angleZ);
                 branchHolder.userData.baseAngleZ = angleZ; // Store base Z angle for animation
                 branchHolder.userData.baseAngleX = angleX; // Store base X angle
                 branchHolder.add(branchMesh);
                 branchNode.add(branchHolder); // Add holder to the parent node

                 // Add next recursion node at the end of this branch
                 const nextNode = new THREE.Object3D();
                 nextNode.position.set(0, branchLength, 0);
                 branchHolder.add(nextNode);
                 buildBranch3D(nextNode, length * nextLengthFactor, radius * radiusFactor, level + 1, maxDepth);
             }
        }

        // V8 Enhancement: Build Koch Snowflake inspired 3D fractal
        function buildBranchKoch(parentObject, length, radius, level, maxDepth) {
            if (level > maxDepth || length < 0.05 || radius < 0.003) return;

            const kochAngle = Math.PI / 3.0; // 60 degrees
            const scaleFactor = 1.0 / 3.0;
            const nextLength = length * scaleFactor;
            const nextRadius = radius * Math.sqrt(scaleFactor); // Keep thickness reasonable

            // Create 4 segments in a Koch-like pattern
            const segments = [
                { angle: 0, lengthScale: scaleFactor },          // Straight
                { angle: kochAngle, lengthScale: scaleFactor },   // Turn left 60 deg
                { angle: -kochAngle * 2, lengthScale: scaleFactor }, // Turn right 120 deg
                { angle: kochAngle, lengthScale: scaleFactor }    // Turn left 60 deg
            ];

            let currentPosition = new THREE.Vector3(0, 0, 0);
            let currentRotation = new THREE.Quaternion();

            for (let i = 0; i < segments.length; i++) {
                const seg = segments[i];
                const segLength = length * seg.lengthScale;
                const segRadius = radius * Math.sqrt(seg.lengthScale);

                const segmentHolder = new THREE.Object3D();
                // Apply rotation relative to the previous segment's end rotation
                const rotation = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 0, 1), seg.angle);
                currentRotation.multiply(rotation);

                segmentHolder.position.copy(currentPosition);
                segmentHolder.quaternion.copy(currentRotation);

                parentObject.add(segmentHolder);
                // dynamicBranchNodes.push(segmentHolder); // Optional: Animate these segments? Might get chaotic.

                const mesh = createBranchMesh(segLength, segRadius, level, maxDepth);
                segmentHolder.add(mesh);

                // Node for next level recursion
                const nextNode = new THREE.Object3D();
                nextNode.position.set(0, segLength, 0); // Position at the end of this segment mesh
                segmentHolder.add(nextNode);

                // Recurse
                buildBranchKoch(nextNode, nextLength, nextRadius, level + 1, maxDepth);

                // Update position and rotation for the next segment *within this level*
                const deltaPos = new THREE.Vector3(0, segLength, 0).applyQuaternion(currentRotation);
                currentPosition.add(deltaPos);
                // Rotation is already updated for the holder, keep it for next segment start
            }
        }


        // Update Particle Colors (Factored out and called from animate/init) - See createParticleSystem section

        // Setup Audio (V8 Enhanced: Mic Input, Refined Bands)
        function setupAudio() {
            audioElement = document.getElementById('audioElement');
            const fileInput = document.getElementById('audioFile');
            const playButton = document.getElementById('playButton');
            const pauseButton = document.getElementById('pauseButton');
            const infoText = document.getElementById('infoText');
            const audioInputSelect = document.getElementById('audioInputSelect'); // V8
            const loadMusicLabel = document.getElementById('loadMusicLabel'); // V8

            if (!audioElement || !fileInput || !playButton || !pauseButton || !infoText || !audioInputSelect || !loadMusicLabel) {
                console.error("Audio control elements not found!");
                if(infoText) infoText.textContent = "Error: UI elements missing.";
                return;
            }

            try {
                audioContext = new (window.AudioContext || window.webkitAudioContext)();
                analyser = audioContext.createAnalyser();
                analyser.fftSize = 256; // Lower resolution is fine for broad bands
                analyser.smoothingTimeConstant = 0.65; // Slightly more smoothing
                const bufferLength = analyser.frequencyBinCount;
                audioDataArray = new Uint8Array(bufferLength);
                audioInitialized = true;
                console.log("AudioContext Initialized.");
            } catch (e) {
                console.error("Web Audio API Error:", e);
                alert('Web Audio API is not supported or enabled in your browser.');
                infoText.textContent = "Error: Web Audio API failed.";
                audioInputSelect.disabled = true;
                fileInput.disabled = true;
                loadMusicLabel.style.display = 'none';
                playButton.disabled = true;
                pauseButton.disabled = true;
                return;
            }

            // V8: Handle Input Source Change
            audioInputSelect.addEventListener('change', (event) => {
                const selectedSource = event.target.value;
                if (selectedSource === 'mic') {
                    activateMicInput();
                    loadMusicLabel.style.display = 'none';
                    fileInput.style.display = 'none';
                    playButton.style.display = 'none';
                    pauseButton.style.display = 'none';
                    audioElement.pause(); // Stop file playback if any
                    audioElement.src = ""; // Clear file source
                    musicLoaded = false;
                } else { // 'file'
                    deactivateMicInput();
                    loadMusicLabel.style.display = 'inline-block';
                    // fileInput doesn't need display change (it's hidden anyway)
                    playButton.style.display = 'inline-block';
                    pauseButton.style.display = 'inline-block';
                    playButton.disabled = !musicLoaded; // Re-enable if music was loaded
                    pauseButton.disabled = true;
                    infoText.textContent = musicLoaded ? `File loaded. Press Play.` : `Select audio file. H=UI, I=Help.`;
                }
            });

            // File Input Handling (Largely unchanged, but respects mic state)
            fileInput.addEventListener('change', (event) => {
                if (!audioInitialized || isMicActive) return; // Don't process if mic is active

                const file = event.target.files[0];
                if (file) {
                    infoText.textContent = "Loading audio...";
                    playButton.disabled = true;
                    pauseButton.disabled = true;
                    musicLoaded = false;

                    // Disconnect previous file source if exists
                    if (audioSourceNode) {
                        try { audioSourceNode.stop(); } catch (e) { /* ignore */ }
                        try { audioSourceNode.disconnect(); } catch (e) { /* ignore */ }
                        audioSourceNode = null;
                    }
                    // Ensure analyser is disconnected from any old source before connecting new
                    try { analyser.disconnect(); console.log("Analyser disconnected from old source."); } catch(e) { /* Ignore if not connected */ }

                    audioElement.src = URL.createObjectURL(file);
                    audioElement.load();

                    // Use FileReader to decode for BufferSourceNode (more reliable timing)
                    const reader = new FileReader();
                    reader.onload = (e_reader) => {
                        audioContext.decodeAudioData(e_reader.target.result)
                            .then(buffer => {
                                audioSourceNode = audioContext.createBufferSource();
                                audioSourceNode.buffer = buffer;
                                try {
                                    audioSourceNode.connect(analyser);
                                    // Only connect analyser to destination if mic isn't active
                                    if (!isMicActive) {
                                        analyser.connect(audioContext.destination);
                                    }
                                    console.log("New audio file node connected to analyser.");
                                } catch(err_connect) {
                                    console.error("Error connecting new audio node:", err_connect);
                                    infoText.textContent = "Error connecting audio.";
                                    return;
                                }
                                playButton.disabled = false;
                                pauseButton.disabled = true;
                                musicLoaded = true;
                                infoText.textContent = `Loaded: ${file.name.substring(0, 30)}${file.name.length > 30 ? '...' : ''}. H=UI, I=Help.`;
                                console.log("Audio decoded and ready.");
                                // Reset smoothing on new track
                                smoothedSubBass = smoothedBass = smoothedMid = smoothedTreble = smoothedOverall = 0;
                                currentSubBass = currentBass = currentMid = currentTreble = currentOverall = 0;
                                audioSourceNode.onended = () => {
                                    console.log("BufferSourceNode playback finished.");
                                    // We rely on the audioElement 'ended' event now
                                };
                            })
                            .catch(err_decode => {
                                console.error("Error decoding audio:", err_decode);
                                alert(`Error decoding audio file: ${err_decode.message}. Try different file/format.`);
                                infoText.textContent = "Error decoding audio.";
                                playButton.disabled = true;
                                pauseButton.disabled = true;
                                musicLoaded = false;
                                audioSourceNode = null;
                                audioElement.src = "";
                            });
                    };
                    reader.onerror = (err_read) => {
                        console.error("FileReader error:", err_read);
                        alert("Error reading file.");
                        infoText.textContent = "Error reading file.";
                        musicLoaded = false;
                        audioElement.src = "";
                    };
                    reader.readAsArrayBuffer(file);
                }
                event.target.value = null; // Reset file input
            });

            // Play/Pause using <audio> element controls
            playButton.addEventListener('click', () => {
                if (!musicLoaded || playButton.disabled || isMicActive) return;
                if (audioContext.state === 'suspended') {
                    audioContext.resume().then(playAudioElement).catch(err => console.error("AudioContext resume failed:", err));
                } else {
                    playAudioElement();
                }
            });

            function playAudioElement() {
                if (!audioElement || !musicLoaded) return;
                audioElement.play().catch(err => {
                    console.error("Error playing audio element:", err);
                    infoText.textContent = `Playback error: ${err.message}`;
                    playButton.disabled = false; // Allow retry maybe
                    pauseButton.disabled = true;
                });
            }

            pauseButton.addEventListener('click', () => {
                if (!pauseButton.disabled && audioElement && !audioElement.paused) {
                    audioElement.pause();
                }
            });

            // Audio Element Event Listeners
            audioElement.onplay = () => {
                if (isMicActive) return; // Ignore if mic is source
                if (audioContext.state === 'suspended') audioContext.resume();
                playButton.disabled = true;
                pauseButton.disabled = false;
                console.log("AudioElement event: play");
            };
            audioElement.onpause = () => {
                if (isMicActive) return;
                playButton.disabled = false;
                pauseButton.disabled = true;
                console.log("AudioElement event: pause");
            };
            audioElement.onended = () => {
                if (isMicActive) return;
                playButton.disabled = false;
                pauseButton.disabled = true;
                musicLoaded = false; // Allow loading new file
                infoText.textContent = "Track ended. Load music/mic. H=UI, I=Help.";
                console.log("AudioElement event: ended");
                // BufferSourceNode might finish slightly before/after, cleanup just in case
                if (audioSourceNode) {
                    try { audioSourceNode.disconnect(); } catch(e) {}
                    audioSourceNode = null;
                }
            };
            audioElement.onerror = (e) => {
                console.error("Audio Element Error:", audioElement.error);
                infoText.textContent = `Audio Error: ${audioElement.error?.message || 'Unknown'}`;
                playButton.disabled = true;
                pauseButton.disabled = true;
                musicLoaded = false;
                if (audioSourceNode) {
                     try { audioSourceNode.disconnect(); } catch(e) {}
                     audioSourceNode = null;
                }
            }
        }

        // V8 Enhancement: Activate Microphone Input
        async function activateMicInput() {
            if (!audioContext || isMicActive) return;
            const infoText = document.getElementById('infoText');

            // Disconnect file source if connected
            if (audioSourceNode) {
                try { audioSourceNode.stop(); } catch (e) {}
                try { audioSourceNode.disconnect(); } catch (e) {}
                audioSourceNode = null;
            }
             // Disconnect analyser from destination if connected (will reconnect via mic)
            try { analyser.disconnect(audioContext.destination); } catch(e) {}


            try {
                infoText.textContent = "Requesting Mic Access...";
                const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false });
                micSourceNode = audioContext.createMediaStreamSource(stream);
                micSourceNode.connect(analyser);
                // DO NOT connect analyser to destination for mic input to avoid feedback loop!
                isMicActive = true;
                infoText.textContent = "Microphone Active. H=UI, I=Help.";
                console.log("Microphone input connected to analyser.");
                 // Reset smoothing on new source
                smoothedSubBass = smoothedBass = smoothedMid = smoothedTreble = smoothedOverall = 0;
                currentSubBass = currentBass = currentMid = currentTreble = currentOverall = 0;
            } catch (err) {
                console.error("Error accessing microphone:", err);
                infoText.textContent = `Mic Error: ${err.message}. Select File input.`;
                alert(`Could not access microphone: ${err.message}`);
                isMicActive = false;
                micSourceNode = null;
                // Revert UI selection back to file
                document.getElementById('audioInputSelect').value = 'file';
                document.getElementById('audioInputSelect').dispatchEvent(new Event('change')); // Trigger change handler
            }
        }

        // V8 Enhancement: Deactivate Microphone Input
        function deactivateMicInput() {
            if (!isMicActive || !micSourceNode) return;
            try {
                micSourceNode.disconnect();
                // Stop the tracks on the stream to turn off the mic indicator
                micSourceNode.mediaStream.getTracks().forEach(track => track.stop());
                micSourceNode = null;
                isMicActive = false;
                console.log("Microphone input disconnected.");
                // Reconnect analyser to destination if file source exists and is playing?
                // Or just let the file loading logic handle reconnection.
                // For simplicity, let file loading handle it.
                // If a file was previously loaded and ready, connect analyser to destination
                if (musicLoaded && audioSourceNode && !audioElement.paused) {
                     try { analyser.connect(audioContext.destination); } catch(e) {}
                }

            } catch (err) {
                console.error("Error disconnecting microphone:", err);
            }
        }


        // Settings Load/Save (Largely unchanged, uses new updateGuiDisplay)
        function setupSettingsHandlers() {
            const saveButton = document.getElementById('saveSettingsButton');
            const loadButton = document.getElementById('loadSettingsButton');
            const settingsFileInput = document.getElementById('settingsFile');
            const infoText = document.getElementById('infoText');
            if (!saveButton || !loadButton || !settingsFileInput || !infoText) {
                console.error("Settings buttons or file input not found!"); return;
            }

            saveButton.addEventListener('click', saveSettings);
            loadButton.addEventListener('click', () => settingsFileInput.click());

            settingsFileInput.addEventListener('change', (event) => {
                const file = event.target.files[0];
                if (!file) return;
                const reader = new FileReader();
                reader.onload = (e) => {
                    try {
                        const loadedSettings = JSON.parse(e.target.result);
                        if (typeof loadedSettings !== 'object' || loadedSettings === null) {
                            throw new Error("Invalid file format - not a JSON object.");
                        }
                        console.log("Loaded settings data:", loadedSettings);
                        applyLoadedSettings(loadedSettings); // Apply the settings
                        infoText.textContent = `Settings loaded from ${file.name}. H=UI, I=Help.`;
                        console.log("Settings successfully loaded and applied.");
                    } catch (error) {
                        console.error("Error loading or parsing settings file:", error);
                        alert(`Error loading settings: ${error.message}`);
                        infoText.textContent = `Error loading settings: ${error.message}`;
                    } finally {
                        event.target.value = null; // Reset file input
                    }
                };
                reader.onerror = (err) => {
                    console.error("FileReader error while loading settings:", err);
                    alert("Error reading settings file.");
                    infoText.textContent = "Error reading settings file.";
                    event.target.value = null;
                };
                reader.readAsText(file);
            });
        }

        // Save Settings (Updated filename V8.0)
        function saveSettings() {
            const infoText = document.getElementById('infoText');
            try {
                const settingsToSave = {};
                // Iterate through guiControls and save relevant properties
                for (const key in guiControls) {
                    // Exclude internal state variables
                    if (key !== 'dynamicMaxAngle' && key !== 'baseMaxAngle') {
                        // Handle color objects specifically if needed (dat.gui usually stores them correctly as hex numbers/strings)
                        // if (guiControls[key] instanceof THREE.Color) {
                        //     settingsToSave[key] = guiControls[key].getHex();
                        // } else {
                            settingsToSave[key] = guiControls[key];
                        // }
                    }
                }

                const settingsJSON = JSON.stringify(settingsToSave, null, 2);
                const blob = new Blob([settingsJSON], { type: 'application/json' });
                const url = URL.createObjectURL(blob);
                const link = document.createElement('a');
                link.href = url;
                const timestamp = new Date().toISOString().slice(0, 19).replace(/[:T]/g, '-');
                link.download = `fractal-settings-v8.0-${timestamp}.json`; // Updated filename
                document.body.appendChild(link);
                link.click();
                document.body.removeChild(link);
                URL.revokeObjectURL(url);
                infoText.textContent = "Settings saved successfully! Check downloads.";
                console.log("Settings saved.");
            } catch (error) {
                console.error("Error saving settings:", error);
                alert(`Could not save settings: ${error.message}`);
                infoText.textContent = `Error saving settings: ${error.message}`;
            }
        }

        // Apply Loaded Settings (V8: Handles new controls)
        function applyLoadedSettings(loadedSettings) {
             console.log("Applying loaded settings...");
             let needsFractalRegen = false;
             let needsIcoSphereRegen = false; // V8 Renamed
             let needsColorUpdate = false;
             let needsFractalUniformUpdate = false;
             let needsParticleUpdate = false; // V8

             // Apply settings from loaded object to guiControls
             for (const key in loadedSettings) {
                 if (guiControls.hasOwnProperty(key)) {
                     // Check for actual change to avoid unnecessary updates
                     // Need careful comparison for objects/arrays if they were used
                     if (JSON.stringify(guiControls[key]) !== JSON.stringify(loadedSettings[key])) {
                         guiControls[key] = loadedSettings[key];
                         console.log(`  Applied ${key}:`, loadedSettings[key]);

                         // Set flags for regeneration/updates based on changed key
                         if (['fractalType', 'numFractals', 'maxDepth'].includes(key)) needsFractalRegen = true;
                         if (['icoSphereRadius', 'icoSphereDetail'].includes(key)) needsIcoSphereRegen = true; // V8
                         if (key === 'colorScheme') needsColorUpdate = true;
                         if (['branchFlowSpeed', 'branchPulseFrequency', 'fractalAudioIntensity', 'branchBaseEmission', 'branchDistortionFactor', 'fractalSurfaceNoiseType', 'fractalSurfaceNoiseScale', 'fractalSurfaceNoiseIntensity'].includes(key)) needsFractalUniformUpdate = true;
                         if (['particleTurbulence', 'particleTurbulenceSpeed', 'particleAudioSizeBoost', 'particleAudioSpeedBoost'].includes(key)) needsParticleUpdate = true; // V8
                     }
                 } else {
                     console.warn(`Loaded setting "${key}" does not exist in current guiControls.`);
                 }
             }

             // Update the GUI display to reflect loaded values
             if (guiInstance) {
                 updateGuiDisplay(guiInstance, loadedSettings); // Use the robust update function
             } else {
                 console.warn("GUI instance not found, cannot update display.");
             }

             // Trigger visual updates based on flags
             console.log("Triggering visual updates...");
             if (needsFractalRegen) {
                 console.log("-> Regenerating Fractals");
                 regenerateFractal(); // This implicitly handles color and uniforms for new fractals
                 needsColorUpdate = false; // Reset flags handled by regen
                 needsFractalUniformUpdate = false;
             } else {
                 // If no fractal regen, update colors/uniforms if needed
                 if (needsColorUpdate) {
                     console.log("-> Updating Color Scheme");
                     updateColorSchemeUniforms(); // Updates fractal colors and particles
                     needsFractalUniformUpdate = false; // Color scheme update handles base uniforms
                 }
                 if (needsFractalUniformUpdate) {
                     console.log("-> Updating Fractal Style Uniforms");
                     updateFractalShaderUniforms();
                 }
             }

             // Update Icosphere (V8)
             if (needsIcoSphereRegen) {
                 console.log("-> Regenerating Icosphere");
                 regenerateIcoSphere();
             } else {
                 // Update existing icosphere properties if needed
                 if (icoSphereMesh?.material) {
                     if (loadedSettings.hasOwnProperty('icoSphereLineColor')) icoSphereMesh.material.color.set(guiControls.icoSphereLineColor);
                     if (loadedSettings.hasOwnProperty('icoSphereLineWidth')) icoSphereMesh.material.linewidth = guiControls.icoSphereLineWidth;
                 }
                 if (icoSphereMesh && loadedSettings.hasOwnProperty('showIcoSphere')) icoSphereMesh.visible = guiControls.showIcoSphere;
             }

             // Update Particles (V8)
             if (needsParticleUpdate && particleSystem?.material?.uniforms) {
                 console.log("-> Updating Particle Uniforms");
                 const u = particleSystem.material.uniforms;
                 if(u.turbulenceAmount) u.turbulenceAmount.value = guiControls.particleTurbulence;
                 if(u.turbulenceSpeed) u.turbulenceSpeed.value = guiControls.particleTurbulenceSpeed;
                 if(u.audioSizeBoost) u.audioSizeBoost.value = guiControls.particleAudioSizeBoost;
                 if(u.audioSpeedBoost) u.audioSpeedBoost.value = guiControls.particleAudioSpeedBoost;
             }

             // Update Fog
             if (fogMesh?.material?.uniforms) {
                 if (loadedSettings.hasOwnProperty('fogColor')) fogMesh.material.uniforms.fogColor.value.set(guiControls.fogColor);
                 if (loadedSettings.hasOwnProperty('fogNoiseScale')) fogMesh.material.uniforms.noiseScale.value = guiControls.fogNoiseScale;
                 if (loadedSettings.hasOwnProperty('fogDensity')) fogMesh.material.uniforms.density.value = guiControls.fogDensity;
             }

             // Update Post-Processing Passes directly
             if (bloomPass && loadedSettings.hasOwnProperty('bloomThreshold')) bloomPass.threshold = guiControls.bloomThreshold;
             // bloomBaseStrength is handled in animate loop

             if (afterimagePass && loadedSettings.hasOwnProperty('afterimageDamp')) afterimagePass.uniforms["damp"].value = guiControls.afterimageDamp;
             // afterimageAudioInfluence handled in animate loop

             // rgbShiftBaseAmount handled in animate loop

             if (filmPass) {
                 if (loadedSettings.hasOwnProperty('filmNoiseIntensity') && filmPass.uniforms.nIntensity) filmPass.uniforms.nIntensity.value = guiControls.filmNoiseIntensity;
                 if (loadedSettings.hasOwnProperty('filmScanlineIntensity') && filmPass.uniforms.sIntensity) filmPass.uniforms.sIntensity.value = guiControls.filmScanlineIntensity;
                 if (loadedSettings.hasOwnProperty('filmGrayscale') && filmPass.uniforms.grayscale) filmPass.uniforms.grayscale.value = guiControls.filmGrayscale;
             }
             // filmAudioBoost handled in animate loop

             if (vignettePass) {
                 if (loadedSettings.hasOwnProperty('vignetteEnabled')) vignettePass.enabled = guiControls.vignetteEnabled;
                 if (loadedSettings.hasOwnProperty('vignetteOffset')) vignettePass.uniforms['offset'].value = guiControls.vignetteOffset;
                 if (loadedSettings.hasOwnProperty('vignetteDarkness')) vignettePass.uniforms['darkness'].value = guiControls.vignetteDarkness;
             }

             if (kaleidoscopePass) {
                 if (loadedSettings.hasOwnProperty('kaleidoscopeSides')) kaleidoscopePass.uniforms.sides.value = guiControls.kaleidoscopeSides;
                 if (loadedSettings.hasOwnProperty('kaleidoscopeMouseInfluence')) kaleidoscopePass.uniforms.mouseInfluence.value = guiControls.kaleidoscopeMouseInfluence;
             }
             // kaleidoscopeBaseDistortion, AudioBoost, AngleAudioBoost handled in animate loop

             if (mirrorPass && loadedSettings.hasOwnProperty('mirrorCount')) mirrorPass.uniforms.sides.value = parseFloat(guiControls.mirrorCount);

             if (feedbackPass) {
                 if (loadedSettings.hasOwnProperty('feedbackMixAmount')) feedbackPass.uniforms.mixAmount.value = guiControls.feedbackMixAmount;
                 if (loadedSettings.hasOwnProperty('feedbackColorShift')) feedbackPass.uniforms.colorShiftAmount.value = guiControls.feedbackColorShift; // V8
             }
             // feedbackAudioInfluence handled in animate loop

             // glitchAudioTrigger handled in animate loop

             if (lensDistortionPass && loadedSettings.hasOwnProperty('lensDistortionAmount')) { // V8
                 lensDistortionPass.uniforms.amount.value = guiControls.lensDistortionAmount;
             }
             // lensDistortionAudioBoost handled in animate loop

             if (edgeDetectionPass) { // V8
                 if (loadedSettings.hasOwnProperty('edgeDetectEnabled')) edgeDetectionPass.enabled = guiControls.edgeDetectEnabled;
                 if (loadedSettings.hasOwnProperty('edgeDetectThreshold') && edgeDetectionPass.uniforms.uThreshold) edgeDetectionPass.uniforms.uThreshold.value = guiControls.edgeDetectThreshold;
                 if (loadedSettings.hasOwnProperty('edgeDetectColor') && edgeDetectionPass.uniforms.uEdgeColor) edgeDetectionPass.uniforms.uEdgeColor.value.set(guiControls.edgeDetectColor);
             }


             console.log("Settings application finished.");
         }

         // Update GUI Display (V8: Robust version without instanceof)
         function updateGuiDisplay(gui, loadedSettings) {
            // Helper to check if a value might be a color representation (heuristic)
            function looksLikeColor(value) {
                if (typeof value === 'number') return true; // Could be hex number
                if (typeof value === 'string') {
                    // Basic checks for hex (# RRGGBB, #RGB) or css color names/functions
                    return value.startsWith('#') || value.includes('rgb') || /^[a-zA-Z]+$/.test(value);
                }
                if (Array.isArray(value) && (value.length === 3 || value.length === 4)) {
                   return value.every(n => typeof n === 'number'); // [r,g,b] or [r,g,b,a]
                }
                 if (typeof value === 'object' && value !== null && ('r' in value || 'h' in value)) {
                    return true; // {r,g,b} or {h,s,v/l} objects
                 }
                return false;
            }

            // Update controllers within folders
            for (const folderName in gui.__folders) {
                const folder = gui.__folders[folderName];
                folder.__controllers.forEach(controller => {
                    const propertyName = controller.property;
                    if (loadedSettings.hasOwnProperty(propertyName)) {
                        const newValue = loadedSettings[propertyName];
                        try {
                            // For number controllers (sliders), update their min/max if newValue is outside current range
                            if (typeof newValue === 'number' && !controller.constructor.name.toLowerCase().includes('color')) {
                                if (typeof controller.__max === 'number' && newValue > controller.__max) {
                                    controller.max(newValue); 
                                }
                                if (typeof controller.__min === 'number' && newValue < controller.__min) {
                                    controller.min(newValue);
                                }
                            }

                            // Heuristic check for color controllers
                            const isLikelyColorController = controller.constructor.name.toLowerCase().includes('color');

                            if (isLikelyColorController && !looksLikeColor(newValue)) {
                                console.warn(`Attempting to load non-standard color value '${newValue}' (type: ${typeof newValue}) into color controller for '${propertyName}'. May not display correctly.`);
                                controller.setValue(newValue);
                            } else {
                                controller.setValue(newValue);
                            }

                            // Ensure the display reflects the change immediately
                            if (typeof controller.updateDisplay === 'function') {
                                controller.updateDisplay();
                            }

                        } catch (e) {
                            console.error(`Error setting controller value for '${propertyName}' with value:`, newValue, e);
                        }
                    }
                });
            }

            // Update root controllers (if any)
             gui.__controllers.forEach(controller => {
                 const propertyName = controller.property;
                 if (loadedSettings.hasOwnProperty(propertyName)) {
                      const newValue = loadedSettings[propertyName];
                       try {
                            // For number controllers (sliders), update their min/max if newValue is outside current range
                            if (typeof newValue === 'number' && !controller.constructor.name.toLowerCase().includes('color')) {
                                if (typeof controller.__max === 'number' && newValue > controller.__max) {
                                    controller.max(newValue);
                                }
                                if (typeof controller.__min === 'number' && newValue < controller.__min) {
                                    controller.min(newValue);
                                }
                            }
                           controller.setValue(newValue);
                           if (typeof controller.updateDisplay === 'function') {
                               controller.updateDisplay();
                           }
                       } catch (e) {
                           console.error(`Error setting root controller value for '${propertyName}' with value:`, newValue, e);
                       }
                 }
             });
            console.log("GUI display update finished.");
         }


        // V8 Enhancement: Preset System using localStorage
        function setupPresetSystem() {
            const presetSelect = document.getElementById('presetSelect');
            const infoText = document.getElementById('infoText');
            if (!presetSelect || !infoText) {
                console.error("Preset select element not found!");
                return;
            }

            presetSelect.addEventListener('change', (event) => {
                const action = event.target.value;
                if (!action) return; // Ignore placeholder

                const match = action.match(/^(save|load|delete)(\d)$/);
                if (match) {
                    const operation = match[1];
                    const slot = match[2];
                    const key = `${PRESET_STORAGE_KEY}_slot_${slot}`;

                    if (operation === 'save') {
                        try {
                            const settingsToSave = {};
                            for (const propKey in guiControls) {
                                if (propKey !== 'dynamicMaxAngle' && propKey !== 'baseMaxAngle') {
                                    settingsToSave[propKey] = guiControls[propKey];
                                }
                            }
                            localStorage.setItem(key, JSON.stringify(settingsToSave));
                            infoText.textContent = `Preset saved to Slot ${slot}.`;
                            console.log(`Preset saved to ${key}`);
                        } catch (e) {
                            console.error(`Error saving preset to slot ${slot}:`, e);
                            infoText.textContent = `Error saving preset ${slot}: Storage might be full.`;
                            alert(`Error saving preset: ${e.message}`);
                        }
                    } else if (operation === 'load') {
                        const savedData = localStorage.getItem(key);
                        if (savedData) {
                            try {
                                const loadedSettings = JSON.parse(savedData);
                                applyLoadedSettings(loadedSettings);
                                infoText.textContent = `Preset loaded from Slot ${slot}.`;
                                console.log(`Preset loaded from ${key}`);
                            } catch (e) {
                                console.error(`Error parsing or applying preset from slot ${slot}:`, e);
                                infoText.textContent = `Error loading preset ${slot}: Invalid data.`;
                                alert(`Error loading preset: ${e.message}`);
                            }
                        } else {
                            infoText.textContent = `Preset Slot ${slot} is empty.`;
                        }
                    } else if (operation === 'delete') {
                        if (localStorage.getItem(key)) {
                            localStorage.removeItem(key);
                            infoText.textContent = `Preset Slot ${slot} deleted.`;
                            console.log(`Preset deleted from ${key}`);
                        } else {
                            infoText.textContent = `Preset Slot ${slot} was already empty.`;
                        }
                    }
                }
                // Reset dropdown after action
                presetSelect.value = "";
            });
        }


        // --- Event Handlers ---
        function onWindowResize() {
            const container = document.getElementById('container');
            if (!container) return; // Exit if container not found

            const width = container.clientWidth;
            const height = container.clientHeight;

            camera.aspect = width / height;
            camera.updateProjectionMatrix();

            renderer.setSize(width, height);
            composer.setSize(width, height);

            if (feedbackRenderTarget) {
                feedbackRenderTarget.setSize(width, height);
                if (feedbackPass) feedbackPass.uniforms.tFeedback.value = feedbackRenderTarget.texture;
            }
            // V8: No LineMaterial resolution to update for Icosphere
            // if (icoSphereMesh?.material) { // Basic LineMaterial doesn't have resolution
            //     // icoSphereMesh.material.resolution.set(width, height);
            // }
            if (backgroundMesh?.material?.uniforms?.resolution) {
                backgroundMesh.material.uniforms.resolution.value.set(width, height);
            }
            if (fogMesh?.material?.uniforms?.resolution) { // Fog shader doesn't use resolution currently
                // fogMesh.material.uniforms.resolution.value.set(width, height);
            }
            if (edgeDetectionPass?.uniforms?.resolution) { // V8
                 edgeDetectionPass.uniforms.resolution.value.x = width * window.devicePixelRatio;
                 edgeDetectionPass.uniforms.resolution.value.y = height * window.devicePixelRatio;
            }
             if (particleSystem?.material?.uniforms?.scale) { // V8 Update particle scale uniform
                 particleSystem.material.uniforms.scale.value = height / 2.0;
             }
        }

        function onMouseMove(event) {
            mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
            mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;

            // Update target angle for fractal sway (less influence than before)
            targetAngle = (mouse.x * 0.4) * guiControls.dynamicMaxAngle * 0.6 + (Math.PI / 8);

            // Update feedback mouse uniform
            if (feedbackPass) {
                feedbackPass.uniforms.mouse.value.set(mouse.x, mouse.y);
            }
            // Update kaleidoscope mouse uniform (handled in animate loop now)
        }

        function onKeyDown(event) {
            const key = event.key.toLowerCase();

            if (key === 'h') { // Toggle main UI Controls
                uiControlsVisible = !uiControlsVisible;
                const uiElements = document.querySelectorAll('.ui-element-h');

                uiElements.forEach(el => {
                     el.classList.toggle('hidden', !uiControlsVisible);
                });

                if (guiInstance && guiInstance.domElement) {
                     guiInstance.domElement.classList.toggle('hidden', !uiControlsVisible);
                }

                 // Toggle cursor visibility
                 const bodyCursor = uiControlsVisible ? 'auto' : 'none';
                 document.body.style.cursor = bodyCursor;
                 renderer.domElement.style.cursor = uiControlsVisible ? 'grab' : 'none';

            } else if (key === 'i') { // Toggle Help Panel
                helpPanelVisible = !helpPanelVisible;
                const helpPanel = document.getElementById('helpPanel');
                if (helpPanel) {
                    helpPanel.classList.toggle('visible', helpPanelVisible);
                }
            }
        }

        // --- Animate Loop (V8 Enhanced Audio Analysis & Effects) ---
        function animate() {
            requestAnimationFrame(animate);
            const delta = clock.getDelta();
            const elapsedTime = clock.getElapsedTime();

            // --- Audio Analysis ---
            let isAudioActive = (musicLoaded && !audioElement.paused) || isMicActive;
            if (isAudioActive && audioContext?.state === 'running' && analyser && audioDataArray) {
                analyser.getByteFrequencyData(audioDataArray);
                const bufferLength = analyser.frequencyBinCount; // e.g., 128

                // Define frequency band indices (adjust based on fftSize and sample rate)
                // Example for fftSize=256, sampleRate=44100 -> bin width ~172Hz
                const subBassEnd = Math.floor(bufferLength * 0.05); // ~0-86 Hz
                const bassEnd = Math.floor(bufferLength * 0.15);    // ~86-258 Hz
                const midEnd = Math.floor(bufferLength * 0.60);     // ~258-1033 Hz
                // Treble starts after mid
                const trebleStart = midEnd + 1;                     // ~1033 Hz+

                let subBassSum = 0, bassSum = 0, midSum = 0, trebleSum = 0, totalSum = 0;
                for (let i = 0; i < bufferLength; i++) {
                    // Power curve for better sensitivity to dynamics
                    const val = Math.pow(audioDataArray[i] / 255.0, 2.5);
                    totalSum += val;
                    if (i <= subBassEnd) subBassSum += val;
                    else if (i <= bassEnd) bassSum += val;
                    else if (i <= midEnd) midSum += val;
                    // else if (i >= trebleStart) trebleSum += val; // Corrected: treble is the rest
                    else trebleSum += val;
                }

                // Calculate normalized band levels (sqrt helps normalize energy)
                // Add small epsilon to avoid division by zero if band width is 0
                const epsilon = 1e-6;
                currentSubBass = Math.min(1.0, Math.sqrt(subBassSum / (subBassEnd + 1 + epsilon)) * 1.50); // Boost sub more
                currentBass = Math.min(1.0, Math.sqrt(bassSum / (bassEnd - subBassEnd + epsilon)) * 1.35); // Boost bass more
                currentMid = Math.min(1.0, Math.sqrt(midSum / (midEnd - bassEnd + epsilon)) * 1.25); // Boost mid more
                currentTreble = Math.min(1.0, Math.sqrt(trebleSum / (bufferLength - trebleStart + epsilon)) * 1.40); // Boost treble more
                currentOverall = Math.min(1.0, Math.sqrt(totalSum / (bufferLength + epsilon)) * 1.20); // Boost overall slightly more

                // Clamp just in case
                currentSubBass = Math.max(0, Math.min(1.0, currentSubBass));
                currentBass = Math.max(0, Math.min(1.0, currentBass));
                currentMid = Math.max(0, Math.min(1.0, currentMid));
                currentTreble = Math.max(0, Math.min(1.0, currentTreble));
                currentOverall = Math.max(0, Math.min(1.0, currentOverall));

                // Smooth the values
                smoothedSubBass += (currentSubBass - smoothedSubBass) * AUDIO_LERP_FACTOR;
                smoothedBass += (currentBass - smoothedBass) * AUDIO_LERP_FACTOR;
                smoothedMid += (currentMid - smoothedMid) * AUDIO_LERP_FACTOR;
                smoothedTreble += (currentTreble - smoothedTreble) * AUDIO_LERP_FACTOR;
                smoothedOverall += (currentOverall - smoothedOverall) * AUDIO_LERP_FACTOR;

                // --- Apply Audio to Effects ---
                // Bloom
                if (bloomPass) {
                    const bloomMultiplier = 1.0 + (smoothedBass * 3.0 + smoothedMid * 2.5) * guiControls.bloomAudioBoost; // Further Increased multipliers
                    const targetBloom = guiControls.bloomBaseStrength * bloomMultiplier;
                    bloomPass.strength += (targetBloom - bloomPass.strength) * 0.20; 
                }
                // Kaleidoscope
                if (kaleidoscopePass?.uniforms) {
                    const targetKaleidoDistortion = guiControls.kaleidoscopeBaseDistortion + smoothedOverall * 2.5 * guiControls.kaleidoscopeAudioBoost; // Further Increased multiplier
                    kaleidoscopePass.uniforms.distortion.value += (targetKaleidoDistortion - kaleidoscopePass.uniforms.distortion.value) * 0.20;
                    kaleidoscopePass.uniforms.audioInfluence.value += ((smoothedMid * 1.2 + smoothedTreble * 1.5) - kaleidoscopePass.uniforms.audioInfluence.value) * 0.18; // Further Increased multipliers
                     const angleUpdate = delta * 0.10 + smoothedTreble * 2.5 * delta * guiControls.kaleidoscopeAngleAudioBoost; // Further Increased multiplier
                     kaleidoscopePass.uniforms.angle.value = (kaleidoscopePass.uniforms.angle.value + angleUpdate) % (Math.PI * 2);
                }
                // Fractal Branch Angle
                const angleOscillation = Math.sin(elapsedTime * 0.4) * 0.08;
                const currentBaseMaxAngle = guiControls.baseMaxAngle + angleOscillation;
                const targetMaxAngle = currentBaseMaxAngle * (1.0 + smoothedTreble * 3.0 + smoothedMid * 0.7); // Further Increased multipliers
                guiControls.dynamicMaxAngle += (targetMaxAngle - guiControls.dynamicMaxAngle) * 0.15; 

                // RGB Shift
                if (rgbShiftPass) {
                    const targetRgbShift = guiControls.rgbShiftBaseAmount * (1.0 + smoothedTreble * 3.0 * guiControls.rgbShiftAudioBoost); // Further Increased multiplier
                    rgbShiftPass.uniforms.amount.value += (targetRgbShift - rgbShiftPass.uniforms.amount.value) * 0.20;
                }
                // Afterimage
                if (afterimagePass) {
                    const targetDamp = guiControls.afterimageDamp * (1.0 - (smoothedBass * 2.2 + smoothedSubBass * 0.7) * guiControls.afterimageAudioInfluence); // Further Increased effect
                    afterimagePass.uniforms.damp.value += (targetDamp - afterimagePass.uniforms.damp.value) * 0.12;
                }
                // Film Pass (Base intensities reduced, boost kept relative)
                if (filmPass?.uniforms) {
                    const filmBoost = (smoothedOverall * 1.8 + smoothedTreble * 0.5) * guiControls.filmAudioBoost; 
                    if(filmPass.uniforms.nIntensity) {
                        const targetNoise = guiControls.filmNoiseIntensity * (1.0 + filmBoost);
                        filmPass.uniforms.nIntensity.value += (targetNoise - filmPass.uniforms.nIntensity.value) * 0.20;
                    }
                    if(filmPass.uniforms.sIntensity) {
                        const targetScan = guiControls.filmScanlineIntensity * (1.0 + filmBoost * 0.8);
                        filmPass.uniforms.sIntensity.value += (targetScan - filmPass.uniforms.sIntensity.value) * 0.20;
                    }
                }
                // Background Noise
                if (backgroundMesh?.material?.uniforms?.audioInfluence) {
                    const targetBgAudio = (smoothedOverall * 2.2 + smoothedBass * 1.3 + smoothedSubBass * 0.5) * 1.5; // Further Increased multipliers
                    backgroundMesh.material.uniforms.audioInfluence.value += (targetBgAudio - backgroundMesh.material.uniforms.audioInfluence.value) * 0.12;
                }
                // Glitch Trigger
                if (glitchPass) {
                    const trebleTransient = Math.max(0, currentTreble - smoothedTreble);
                    const glitchTriggerLevel = trebleTransient * guiControls.glitchAudioTrigger * 2.5; // Further Increased sensitivity
                    const randomChance = 0.007 * (1.0 + smoothedOverall * 3.0); 
                    if ((glitchTriggerLevel > 0.25 || Math.random() < randomChance) && !glitchPass.enabled) { // Further Lowered threshold
                        glitchPass.goWild = (Math.random() < 0.70); 
                        glitchPass.enabled = true;
                        setTimeout(() => { if(glitchPass) { glitchPass.enabled = false; } }, 45 + Math.random() * 80); 
                    }
                }
                // Feedback
                if (feedbackPass) {
                    feedbackPass.uniforms.audioInfluence.value += ((smoothedOverall * 2.0 + smoothedBass * 0.7) * guiControls.feedbackAudioInfluence - feedbackPass.uniforms.audioInfluence.value) * 0.12; // Further Increased multiplier
                }
                // Fog
                if (fogMesh?.material?.uniforms?.audioInfluence) {
                    const fogAudioTarget = (smoothedOverall * 2.0 + smoothedSubBass * 1.0) * guiControls.fogAudioInfluence; // Further Increased multiplier
                    fogMesh.material.uniforms.audioInfluence.value += (fogAudioTarget - fogMesh.material.uniforms.audioInfluence.value) * 0.1;
                }
                // Lens Distortion (V8)
                if (lensDistortionPass) {
                    const targetBoost = (smoothedSubBass * 2.5 + smoothedBass * 0.8) * guiControls.lensDistortionAudioBoost; // Further Increased effect
                    lensDistortionPass.uniforms.audioBoost.value += (targetBoost - lensDistortionPass.uniforms.audioBoost.value) * 0.15;
                }

                // --- NEW/ENHANCED AUDIO REACTIONS ---
                // Point Lights Intensity
                if (pointLight1?.userData?.baseIntensity) {
                    const p1Intensity = pointLight1.userData.baseIntensity * (1.0 + (smoothedMid * 1.8 + smoothedTreble * 0.8) * guiControls.pointLightAudioIntensity);
                    pointLight1.intensity += (p1Intensity - pointLight1.intensity) * 0.18;
                }
                if (pointLight2?.userData?.baseIntensity) {
                    const p2Intensity = pointLight2.userData.baseIntensity * (1.0 + (smoothedTreble * 1.8 + smoothedOverall * 0.8) * guiControls.pointLightAudioIntensity);
                    pointLight2.intensity += (p2Intensity - pointLight2.intensity) * 0.18;
                }
                if (pointLight3?.userData?.baseIntensity) {
                    const p3Intensity = pointLight3.userData.baseIntensity * (1.0 + (smoothedBass * 2.1 + smoothedSubBass * 1.1) * guiControls.pointLightAudioIntensity);
                    pointLight3.intensity += (p3Intensity - pointLight3.intensity) * 0.18;
                }
                if (pointLight4?.userData?.baseIntensity) {
                    const p4Intensity = pointLight4.userData.baseIntensity * (1.0 + (smoothedOverall * 1.5 + smoothedMid * 0.7) * guiControls.pointLightAudioIntensity);
                    pointLight4.intensity += (p4Intensity - pointLight4.intensity) * 0.18;
                }

                // Camera FOV
                if (camera?.userData?.initialFov) {
                    const fovTarget = camera.userData.initialFov + (smoothedBass * 1.3 + smoothedSubBass * 1.8) * guiControls.cameraFovAudioBoost;
                    camera.fov += (fovTarget - camera.fov) * 0.10; // Adjusted LERP
                    camera.updateProjectionMatrix();
                }

                // Edge Detection Threshold
                if (edgeDetectionPass?.enabled && edgeDetectionPass?.uniforms?.uThreshold) {
                    const edgeThresholdTarget = Math.max(0.001, guiControls.edgeDetectThreshold * (1.0 - (smoothedTreble * 1.0 + smoothedMid * 0.7) * guiControls.edgeDetectAudioThresholdBoost));
                    edgeDetectionPass.uniforms.uThreshold.value += (edgeThresholdTarget - edgeDetectionPass.uniforms.uThreshold.value) * 0.18;
                }

            } else { // --- No Audio Input ---
                // Smoothly return effects to base values
                const baseLerpFactor = 0.025; // Slower return to base
                if(bloomPass && Math.abs(bloomPass.strength - guiControls.bloomBaseStrength) > 0.01) bloomPass.strength += (guiControls.bloomBaseStrength - bloomPass.strength) * baseLerpFactor; else if (bloomPass) bloomPass.strength = guiControls.bloomBaseStrength;
                if(kaleidoscopePass?.uniforms?.distortion && Math.abs(kaleidoscopePass.uniforms.distortion.value - guiControls.kaleidoscopeBaseDistortion) > 0.005) kaleidoscopePass.uniforms.distortion.value += (guiControls.kaleidoscopeBaseDistortion - kaleidoscopePass.uniforms.distortion.value) * baseLerpFactor; else if (kaleidoscopePass?.uniforms?.distortion) kaleidoscopePass.uniforms.distortion.value = guiControls.kaleidoscopeBaseDistortion;
                if(kaleidoscopePass?.uniforms?.audioInfluence && kaleidoscopePass.uniforms.audioInfluence.value > 0.001) kaleidoscopePass.uniforms.audioInfluence.value += (0.0 - kaleidoscopePass.uniforms.audioInfluence.value) * baseLerpFactor; else if (kaleidoscopePass?.uniforms?.audioInfluence) kaleidoscopePass.uniforms.audioInfluence.value = 0;
                // Angle speed returns to base (very slow)
                if (kaleidoscopePass?.uniforms) {
                     const angleUpdate = delta * 0.10; // Base rotation only
                     kaleidoscopePass.uniforms.angle.value = (kaleidoscopePass.uniforms.angle.value + angleUpdate) % (Math.PI * 2);
                }
                // Fractal angle returns to base
                const angleOscillation = Math.sin(elapsedTime * 0.4) * 0.08;
                const currentBaseMaxAngle = guiControls.baseMaxAngle + angleOscillation;
                if (Math.abs(guiControls.dynamicMaxAngle - currentBaseMaxAngle) > 0.01) guiControls.dynamicMaxAngle += (currentBaseMaxAngle - guiControls.dynamicMaxAngle) * baseLerpFactor * 1.8; else guiControls.dynamicMaxAngle = currentBaseMaxAngle;
                if (rgbShiftPass?.uniforms?.amount && Math.abs(rgbShiftPass.uniforms.amount.value - guiControls.rgbShiftBaseAmount) > 0.0001) rgbShiftPass.uniforms.amount.value += (guiControls.rgbShiftBaseAmount - rgbShiftPass.uniforms.amount.value) * baseLerpFactor; else if (rgbShiftPass?.uniforms?.amount) rgbShiftPass.uniforms.amount.value = guiControls.rgbShiftBaseAmount;
                if (afterimagePass && Math.abs(afterimagePass.uniforms.damp.value - guiControls.afterimageDamp) > 0.001) afterimagePass.uniforms.damp.value += (guiControls.afterimageDamp - afterimagePass.uniforms.damp.value) * baseLerpFactor; else if (afterimagePass) afterimagePass.uniforms.damp.value = guiControls.afterimageDamp;
                if (filmPass?.uniforms) {
                    if (filmPass.uniforms.nIntensity && Math.abs(filmPass.uniforms.nIntensity.value - guiControls.filmNoiseIntensity) > 0.001) filmPass.uniforms.nIntensity.value += (guiControls.filmNoiseIntensity - filmPass.uniforms.nIntensity.value) * baseLerpFactor; else if (filmPass.uniforms.nIntensity) filmPass.uniforms.nIntensity.value = guiControls.filmNoiseIntensity;
                    if (filmPass.uniforms.sIntensity && Math.abs(filmPass.uniforms.sIntensity.value - guiControls.filmScanlineIntensity) > 0.001) filmPass.uniforms.sIntensity.value += (guiControls.filmScanlineIntensity - filmPass.uniforms.sIntensity.value) * baseLerpFactor; else if (filmPass.uniforms.sIntensity) filmPass.uniforms.sIntensity.value = guiControls.filmScanlineIntensity;
                }
                if (backgroundMesh?.material?.uniforms?.audioInfluence && backgroundMesh.material.uniforms.audioInfluence.value > 0.001) backgroundMesh.material.uniforms.audioInfluence.value += (0.0 - backgroundMesh.material.uniforms.audioInfluence.value) * baseLerpFactor; else if (backgroundMesh?.material?.uniforms?.audioInfluence) backgroundMesh.material.uniforms.audioInfluence.value = 0;
                if (glitchPass && glitchPass.enabled) { glitchPass.enabled = false; }
                if (feedbackPass && feedbackPass.uniforms.audioInfluence.value > 0.001) feedbackPass.uniforms.audioInfluence.value += (0.0 - feedbackPass.uniforms.audioInfluence.value) * 0.05; else if (feedbackPass) feedbackPass.uniforms.audioInfluence.value = 0;
                if (fogMesh?.material?.uniforms?.audioInfluence && fogMesh.material.uniforms.audioInfluence.value > 0.001) fogMesh.material.uniforms.audioInfluence.value += (0.0 - fogMesh.material.uniforms.audioInfluence.value) * 0.05; else if (fogMesh?.material?.uniforms?.audioInfluence) fogMesh.material.uniforms.audioInfluence.value = 0;
                if (lensDistortionPass && lensDistortionPass.uniforms.audioBoost.value > 0.001) lensDistortionPass.uniforms.audioBoost.value += (0.0 - lensDistortionPass.uniforms.audioBoost.value) * 0.05; else if (lensDistortionPass) lensDistortionPass.uniforms.audioBoost.value = 0;

                // --- NEW/ENHANCED: Reset to base when no audio ---
                if (pointLight1?.userData?.baseIntensity && Math.abs(pointLight1.intensity - pointLight1.userData.baseIntensity) > 0.01) pointLight1.intensity += (pointLight1.userData.baseIntensity - pointLight1.intensity) * baseLerpFactor; else if (pointLight1?.userData?.baseIntensity) pointLight1.intensity = pointLight1.userData.baseIntensity;
                if (pointLight2?.userData?.baseIntensity && Math.abs(pointLight2.intensity - pointLight2.userData.baseIntensity) > 0.01) pointLight2.intensity += (pointLight2.userData.baseIntensity - pointLight2.intensity) * baseLerpFactor; else if (pointLight2?.userData?.baseIntensity) pointLight2.intensity = pointLight2.userData.baseIntensity;
                if (pointLight3?.userData?.baseIntensity && Math.abs(pointLight3.intensity - pointLight3.userData.baseIntensity) > 0.01) pointLight3.intensity += (pointLight3.userData.baseIntensity - pointLight3.intensity) * baseLerpFactor; else if (pointLight3?.userData?.baseIntensity) pointLight3.intensity = pointLight3.userData.baseIntensity;
                if (pointLight4?.userData?.baseIntensity && Math.abs(pointLight4.intensity - pointLight4.userData.baseIntensity) > 0.01) pointLight4.intensity += (pointLight4.userData.baseIntensity - pointLight4.intensity) * baseLerpFactor; else if (pointLight4?.userData?.baseIntensity) pointLight4.intensity = pointLight4.userData.baseIntensity;

                if (camera?.userData?.initialFov && Math.abs(camera.fov - camera.userData.initialFov) > 0.01) {
                    camera.fov += (camera.userData.initialFov - camera.fov) * baseLerpFactor;
                    camera.updateProjectionMatrix();
                } else if (camera?.userData?.initialFov) { // Corrected condition here
                    camera.fov = camera.userData.initialFov;
                    camera.updateProjectionMatrix();
                }

                if (edgeDetectionPass?.enabled && edgeDetectionPass?.uniforms?.uThreshold && Math.abs(edgeDetectionPass.uniforms.uThreshold.value - guiControls.edgeDetectThreshold) > 0.001) {
                    edgeDetectionPass.uniforms.uThreshold.value += (guiControls.edgeDetectThreshold - edgeDetectionPass.uniforms.uThreshold.value) * baseLerpFactor;
                } else if (edgeDetectionPass?.enabled && edgeDetectionPass?.uniforms?.uThreshold) {
                    edgeDetectionPass.uniforms.uThreshold.value = guiControls.edgeDetectThreshold;
                }

                // Reset smoothed audio values
                const audioResetLerp = 0.06;
                smoothedSubBass += (0 - smoothedSubBass) * audioResetLerp;
                smoothedBass += (0 - smoothedBass) * audioResetLerp;
                smoothedMid += (0 - smoothedMid) * audioResetLerp;
                smoothedTreble += (0 - smoothedTreble) * audioResetLerp;
                smoothedOverall += (0 - smoothedOverall) * audioResetLerp;
                // Reset current values as well for consistency
                currentSubBass = smoothedSubBass;
                currentBass = smoothedBass;
                currentMid = smoothedMid;
                currentTreble = smoothedTreble;
                currentOverall = smoothedOverall;
            }

            // --- Update Scene Objects ---
            controls.update(); // Update orbit controls (damping)

            // Fractal Branch Animation
            currentAngle += (targetAngle - currentAngle) * angleLerpFactor; // Smooth target angle
            const fractalType = guiControls.fractalType;
            const wiggleSpeed = 3.5;
            const wiggleAmount = 0.06 + smoothedTreble * 0.25; // Treble adds more wiggle

            dynamicBranchNodes.forEach(node => {
                // Add slight random wiggle independent of type
                const wiggleX = Math.sin(elapsedTime * wiggleSpeed + node.id * 0.5) * wiggleAmount;
                const wiggleY = Math.cos(elapsedTime * wiggleSpeed * 0.8 + node.id * 0.3) * wiggleAmount * 0.8;

                if (fractalType === 'Symmetric Tree' && node.children.length >= 2) {
                    // Apply rotation to the holders (children of the dynamic node)
                    if(node.children[0]) node.children[0].rotation.set(wiggleX, wiggleY, -currentAngle);
                    if(node.children[1]) node.children[1].rotation.set(-wiggleX, -wiggleY, currentAngle);
                } else if (fractalType === 'Asymmetric' && node.children.length >= 2) {
                    const rightHolder = node.children[0];
                    const leftHolder = node.children[1];
                    if(rightHolder?.userData?.baseAngle !== undefined) rightHolder.rotation.set(wiggleX, wiggleY, rightHolder.userData.baseAngle * currentAngle * 1.2 + rightHolder.userData.angleVar);
                    if(leftHolder?.userData?.baseAngle !== undefined) leftHolder.rotation.set(-wiggleX, -wiggleY, leftHolder.userData.baseAngle * currentAngle * 1.2 + leftHolder.userData.angleVar);
                } else if (fractalType === '3D Branching') {
                    // Here, the dynamic node *is* the holder
                    if (node.userData.baseAngleZ !== undefined) {
                        const targetX = node.userData.baseAngleX + wiggleX;
                        const targetY = node.rotation.y; // Keep Y twist minimal for now
                        const targetZ = node.userData.baseAngleZ + currentAngle * 0.9 + Math.sin(elapsedTime * 5 + node.id * 0.7) * smoothedBass * 0.15; // Bass influences Z angle slightly
                        // Smooth rotation towards target
                        node.rotation.x += (targetX - node.rotation.x) * 0.15;
                        // node.rotation.y += (targetY - node.rotation.y) * 0.15;
                        node.rotation.z += (targetZ - node.rotation.z) * 0.18;
                    }
                }
                // Koch 3D currently not animated via dynamicBranchNodes
            });

            // Update Fractal Shader Time/Audio Uniforms
            const fractalAudioLevel = smoothedMid * 0.6 + smoothedBass * 0.7 + smoothedSubBass * 0.4; // Include sub-bass influence
            fractalRoots.forEach(root => {
                root.traverse((o) => {
                    if (o.isMesh && o.material?.isShaderMaterial && o.material.uniforms) {
                        const u = o.material.uniforms;
                        if(u.time) u.time.value = elapsedTime;
                        if(u.audioLevel) u.audioLevel.value = fractalAudioLevel;
                        // LevelRatio is static per branch, set during creation
                    }
                });
            });

            // Update Particles (V8 Enhanced)
            if (particleSystem?.material?.uniforms) {
                const u = particleSystem.material.uniforms;
                u.time.value = elapsedTime; // Update time for noise

                // Update size/opacity based on audio (handled in vertex shader now)
                // We just need to ensure the base size uniform is correct if it changes
                // u.size.value = particleBaseSize;

                // Update colors unless using gradient map fractals
                 if (!colorSchemes[guiControls.colorScheme]?.isGradient) {
                    updateParticleColors(elapsedTime);
                 } else {
                    // Optional: Could implement a separate particle gradient logic here
                    // For now, particles might keep their last HSL color when gradient is active
                 }
            }

            // Update Icosphere (V8)
            if (icoSphereMesh && icoSphereMesh.visible) {
                // Rotation
                icoSphereMesh.rotation.y += delta * guiControls.icoSphereRotateSpeed * (1.0 + smoothedMid * 1.5); // Further Increased mid influence
                icoSphereMesh.rotation.x += delta * guiControls.icoSphereRotateSpeed * 0.5 * (1.0 + smoothedTreble * 1.3); // Further Increased treble influence
                icoSphereMesh.rotation.z += delta * guiControls.icoSphereRotateSpeed * 0.3 * (1.0 + smoothedBass * 1.6); // Further Increased bass influence

                // Scale and Color/Width based on audio
                const baseScale = 1.0 + Math.sin(elapsedTime * 1.1) * 0.015; 
                const lineMat = icoSphereMesh.material;

                if (isAudioActive && lineMat) {
                    // Scale
                    const scaleTarget = baseScale + (smoothedBass * 1.8 + smoothedSubBass * 1.1) * guiControls.icoSphereAudioScaleBoost; // Further Increased effect
                    icoSphereMesh.scale.lerp(new THREE.Vector3(scaleTarget, scaleTarget, scaleTarget), 0.15);

                    // Color
                    const baseColor = new THREE.Color(guiControls.icoSphereLineColor);
                    const targetColor = baseColor.clone();
                    const targetHSL = {}; targetColor.getHSL(targetHSL);
                    targetHSL.l = THREE.MathUtils.clamp(targetHSL.l * (1.0 + smoothedMid * 2.2 + smoothedOverall * 0.7), 0.1, 0.95); // Further Increased brightening
                    targetHSL.s = THREE.MathUtils.clamp(targetHSL.s * (1.0 + smoothedTreble * 1.1 + smoothedOverall * 0.5), 0.4, 1.0); // Further Increased saturation
                    targetColor.setHSL(targetHSL.h, targetHSL.s, targetHSL.l);
                    lineMat.color.lerp(targetColor, 0.18);

                    // Line Width 
                    const targetWidth = guiControls.icoSphereLineWidth * (1.0 + (smoothedTreble * 2.5 + smoothedMid * 0.8) * 0.5 ); // Further Increased thickening
                    lineMat.linewidth += (targetWidth - lineMat.linewidth) * 0.18;

                } else { // No audio
                    icoSphereMesh.scale.lerp(new THREE.Vector3(baseScale, baseScale, baseScale), 0.05); 
                    if (lineMat){
                        const targetColor = new THREE.Color(guiControls.icoSphereLineColor);
                        lineMat.color.lerp(targetColor, 0.05); // Return to base color
                        lineMat.linewidth += (guiControls.icoSphereLineWidth - lineMat.linewidth) * 0.05; // Return to base width
                    }
                }
            }

            // Auto Rotate Scene Content
            if (guiControls.autoRotate && !controls.manualRotate) { // Check if user is dragging
                const audioRotateBoost = 1.0 + (smoothedOverall * 1.5 + smoothedBass * 0.7) * guiControls.autoRotateAudioBoost; // Further Increased boost
                masterGroup.rotation.x += delta * guiControls.autoRotateSpeedX * audioRotateBoost;
                masterGroup.rotation.y += delta * guiControls.autoRotateSpeedY * audioRotateBoost;
                masterGroup.rotation.z += delta * guiControls.autoRotateSpeedZ * audioRotateBoost;
            }

            // Update Lights
            const lightSpeed = 0.3; const colorSpeed = 0.15;
            if (pointLight1) { pointLight1.position.x = 60 * Math.cos(elapsedTime * lightSpeed * 0.8); pointLight1.position.z = 60 * Math.sin(elapsedTime * lightSpeed * 0.8); pointLight1.position.y = 45 + 15 * Math.sin(elapsedTime * lightSpeed * 1.1); pointLight1.color.setHSL((elapsedTime * colorSpeed + 0.8) % 1.0, 1.0, 0.6); }
            if (pointLight2) { pointLight2.position.x = -60 * Math.cos(elapsedTime * lightSpeed * 0.9 + Math.PI); pointLight2.position.y = -60 * Math.sin(elapsedTime * lightSpeed * 0.9 + Math.PI); pointLight2.position.z = 40 + 20 * Math.cos(elapsedTime * lightSpeed * 1.2); pointLight2.color.setHSL((elapsedTime * colorSpeed + 0.5) % 1.0, 1.0, 0.6); }
            if (pointLight3) { pointLight3.position.y = 50 + Math.sin(elapsedTime * lightSpeed * 1.15) * 18; pointLight3.position.x = Math.cos(elapsedTime * lightSpeed * 0.7) * 15; pointLight3.color.setHSL((elapsedTime * colorSpeed + 0.15) % 1.0, 1.0, 0.65); }
            if (pointLight4) { pointLight4.position.z = -60 + Math.cos(elapsedTime * lightSpeed * 0.75) * 25; pointLight4.position.x = 55 + Math.sin(elapsedTime * lightSpeed * 0.85) * 12; pointLight4.color.setHSL((elapsedTime * colorSpeed + 0.3) % 1.0, 1.0, 0.6); }

            // Update Background Shader Time
            if (backgroundMesh?.material?.uniforms?.time) backgroundMesh.material.uniforms.time.value = elapsedTime;

            // Update Kaleidoscope Mouse Influence
            if (kaleidoscopePass?.uniforms) {
                kaleidoscopePass.uniforms['time'].value = elapsedTime;
                const mouseDist = Math.sqrt(mouse.x*mouse.x + mouse.y*mouse.y);
                const targetMouseInfluence = mouseDist * guiControls.kaleidoscopeMouseInfluence;
                // Smooth mouse influence
                kaleidoscopePass.uniforms[ 'mouseInfluence' ].value += (targetMouseInfluence - kaleidoscopePass.uniforms[ 'mouseInfluence' ].value) * 0.1;
            }

            // Update Feedback Shader Time
            if (feedbackPass) { feedbackPass.uniforms.time.value = elapsedTime; }

            // Update Fog Shader Time & Camera Position
            if (fogMesh?.material?.uniforms) {
                fogMesh.material.uniforms.time.value = elapsedTime;
                fogMesh.material.uniforms.cameraPos.value.copy(camera.position);
            }

            // --- Render ---
            try {
                if (composer) composer.render(delta);
                else if (renderer) renderer.render(scene, camera); // Fallback if composer fails
            } catch (e) {
                console.error("Render loop error:", e);
                // Display error message if loading screen is hidden
                const loadingDiv = document.getElementById('loading');
                if(loadingDiv && loadingDiv.classList.contains('hidden')) {
                    loadingDiv.textContent = "Render Error! Check Console (F12).";
                    loadingDiv.style.color = 'red';
                    loadingDiv.classList.remove('hidden');
                }
                // Optionally, stop the animation loop to prevent repeated errors
                // cancelAnimationFrame(animate);
            }
        } // End animate function

        // V8 Enhancement: Setup GUI Dragging Functionality
        function setupGuiDragging() {
            // Wait a brief moment for dat.GUI to potentially finish rendering/positioning
            setTimeout(() => {
                if (!guiInstance || !guiInstance.domElement) {
                    console.warn("Cannot setup GUI dragging: GUI instance not found.");
                    return;
                }

                const guiElement = guiInstance.domElement;
                const dragHandle = guiElement; // Drag the whole panel

                if (!dragHandle) {
                    console.warn("Cannot setup GUI dragging: Drag handle (GUI element) not found.");
                    return;
                }

                let isDragging = false;
                // Remove startX, startY, initialLeft, initialTop from here, define in mousedown

                dragHandle.style.cursor = 'move'; // Indicate it's draggable

                dragHandle.addEventListener('mousedown', (e) => {
                    // Prevent dragging if clicking on an input/slider/button/control row/close button inside the GUI
                    if (e.target.closest('input, select, button, .slider, .cr, .close-button, .title')) {
                        return;
                    }
                    // Only allow left mouse button drag
                    if (e.button !== 0) {
                        return;
                    }

                    isDragging = true;

                    // Get the initial position using getBoundingClientRect BEFORE changing styles
                    const rect = guiElement.getBoundingClientRect();
                    const initialOffsetX = e.clientX - rect.left; // Offset within the element
                    const initialOffsetY = e.clientY - rect.top;

                    // Apply fixed positioning and initial placement based on bounding rect
                    // This prevents the jump when transform/right are removed
                    guiElement.style.position = 'fixed';
                    guiElement.style.left = `${rect.left}px`;
                    guiElement.style.top = `${rect.top}px`;
                    guiElement.style.right = 'auto';
                    guiElement.style.transform = 'none'; // Remove transform centering
                    guiElement.style.cursor = 'grabbing'; // Indicate dragging state


                    // Define move/end listeners HERE to capture the correct offsetX/Y
                    function onDragMove(e_move) {
                        if (!isDragging) return;

                        // Calculate new top-left based on mouse position and initial offset
                        const newLeft = e_move.clientX - initialOffsetX;
                        const newTop = e_move.clientY - initialOffsetY;

                        // Optional: Clamp position to viewport boundaries
                        const maxWidth = window.innerWidth - guiElement.offsetWidth;
                        const maxHeight = window.innerHeight - guiElement.offsetHeight;

                        guiElement.style.left = `${Math.max(0, Math.min(newLeft, maxWidth))}px`;
                        guiElement.style.top = `${Math.max(0, Math.min(newTop, maxHeight))}px`;
                    }

                    function onDragEnd(e_up) {
                        if (!isDragging) return;
                        // Only react to left mouse button up
                        if (e_up.button !== 0) {
                            return;
                        }

                        isDragging = false;
                        document.removeEventListener('mousemove', onDragMove);
                        document.removeEventListener('mouseup', onDragEnd);
                        dragHandle.style.cursor = 'move'; // Restore move cursor
                        // Keep position: fixed and the calculated top/left where the user dropped it
                    }

                    document.addEventListener('mousemove', onDragMove);
                    document.addEventListener('mouseup', onDragEnd);

                    // Prevent text selection during drag
                    e.preventDefault();
                });

                 // Remove the old, outer-scoped onDragMove and onDragEnd functions if they exist
                 // function onDragMove(e) { ... } // DELETE THIS if it exists outside
                 // function onDragEnd() { ... } // DELETE THIS if it exists outside

                console.log("GUI dragging enabled.");
            }, 150); // Slightly longer timeout just in case
        }

    </script>
</body>
</html>



<p></p>
]]></content:encoded>
					
					<wfw:commentRss>https://psychonautica.net/archives/3607/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title></title>
		<link>https://psychonautica.net/archives/3055</link>
					<comments>https://psychonautica.net/archives/3055#respond</comments>
		
		<dc:creator><![CDATA[psychonautica]]></dc:creator>
		<pubDate>Sat, 26 Apr 2025 04:27:47 +0000</pubDate>
				<category><![CDATA[Photography]]></category>
		<category><![CDATA[ai art gallery]]></category>
		<category><![CDATA[ai artwork]]></category>
		<category><![CDATA[ai image]]></category>
		<category><![CDATA[aiartcommunity]]></category>
		<category><![CDATA[aiartdaily]]></category>
		<category><![CDATA[apes]]></category>
		<category><![CDATA[generativeart]]></category>
		<category><![CDATA[midjourney]]></category>
		<category><![CDATA[war]]></category>
		<guid isPermaLink="false">https://psychonautica.net/archives/3055</guid>

					<description><![CDATA[APE CHAOS GUNS DEATH APES! JellyFishArcade &#60;3]]></description>
										<content:encoded><![CDATA[<div class="npf_row">
<figure class="tmblr-full" data-orig-height="1134" data-orig-width="2024"><img decoding="async" src="https://64.media.tumblr.com/1d6b2c031d1c4648d898aa409026438e/0f193218e8ac79c2-32/s640x960/cf24d3aeb9fe3af738bf01a40a50cfa241af997c.png" data-orig-height="1134" data-orig-width="2024" /></figure>
</div>
<div class="npf_row">
<figure class="tmblr-full" data-orig-height="1134" data-orig-width="2024"><img decoding="async" src="https://64.media.tumblr.com/36dd0c12e58667dd712f3b73bb5be5b6/0f193218e8ac79c2-ac/s640x960/aca713cbbbedb729c4d1f89ca0b1c6818f46faa2.png" data-orig-height="1134" data-orig-width="2024" /></figure>
</div>
<div class="npf_row">
<figure class="tmblr-full" data-orig-height="1121" data-orig-width="2000"><img decoding="async" src="https://64.media.tumblr.com/09eaf51930ee9412cf91f5874e09754b/0f193218e8ac79c2-14/s640x960/3289a54aeecd7219a91140a5c70a5372495458d6.png" data-orig-height="1121" data-orig-width="2000" /></figure>
</div>
<p></p>
<p class="npf_quirky"><span class="npf_color_chandler">APE</span> <span style="color: #ff4930">CHAOS </span>GUNS <span class="npf_color_ross">DEATH </span><span class="npf_color_monica">APES!</span><span class="npf_color_ross"> </span></p>
<p><span class="npf_color_niles">Jelly</span><span class="npf_color_rachel">Fish</span>Arcade <span style="color: #ff4930">&lt;3</span></p>
]]></content:encoded>
					
					<wfw:commentRss>https://psychonautica.net/archives/3055/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title></title>
		<link>https://psychonautica.net/archives/3056</link>
					<comments>https://psychonautica.net/archives/3056#respond</comments>
		
		<dc:creator><![CDATA[psychonautica]]></dc:creator>
		<pubDate>Sat, 26 Apr 2025 01:46:46 +0000</pubDate>
				<category><![CDATA[Photography]]></category>
		<category><![CDATA[ai art gallery]]></category>
		<category><![CDATA[ai artwork]]></category>
		<category><![CDATA[ai image]]></category>
		<category><![CDATA[aiartcommunity]]></category>
		<category><![CDATA[aiartdaily]]></category>
		<category><![CDATA[aiartwork]]></category>
		<category><![CDATA[generativeart]]></category>
		<category><![CDATA[midjourney]]></category>
		<category><![CDATA[sci fi]]></category>
		<category><![CDATA[stable diffusion]]></category>
		<guid isPermaLink="false">https://psychonautica.net/archives/3056</guid>

					<description><![CDATA[Midjourney V6.1 + Clarity Upscaler JellyfishArcade!!!]]></description>
										<content:encoded><![CDATA[<div class="npf_row">
<figure class="tmblr-full" data-orig-height="4212" data-orig-width="2000"><img decoding="async" src="https://64.media.tumblr.com/c2386050683b1822de864876dcb1f0d2/fba8c6800d2cc2c2-63/s640x960/38720570391e71e097c0af72680266981157d187.png" data-orig-height="4212" data-orig-width="2000" /></figure>
</div>
<p></p>
<p>Midjourney V6.1 + Clarity Upscaler </p>
<p class="npf_quirky"><span class="npf_color_niles">JellyfishArcade!!!</span></p>
]]></content:encoded>
					
					<wfw:commentRss>https://psychonautica.net/archives/3056/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title></title>
		<link>https://psychonautica.net/archives/3057</link>
					<comments>https://psychonautica.net/archives/3057#respond</comments>
		
		<dc:creator><![CDATA[psychonautica]]></dc:creator>
		<pubDate>Sat, 26 Apr 2025 00:33:37 +0000</pubDate>
				<category><![CDATA[Photography]]></category>
		<category><![CDATA[ai art gallery]]></category>
		<category><![CDATA[ai image]]></category>
		<category><![CDATA[aiartcommunity]]></category>
		<category><![CDATA[aiartdaily]]></category>
		<category><![CDATA[aiartwork]]></category>
		<category><![CDATA[midjourney]]></category>
		<guid isPermaLink="false">https://psychonautica.net/archives/3057</guid>

					<description><![CDATA[See if this shows up without getting downsized&#8230;..Tumblr seriously needs to up their game when it comes to upload size, 2048 wide Max is just silly these days. But yeah, Sunflower from Midjourney + Upscaler and Lightroom JELLYFISH ARCADE!]]></description>
										<content:encoded><![CDATA[<div class="npf_row">
<figure class="tmblr-full" data-orig-height="3200" data-orig-width="1472"><img decoding="async" src="https://64.media.tumblr.com/47c9059bc94b5cd59c6f4241ac8d437f/81d2e2fa791ff543-42/s640x960/25b7ee74c32abc897efbc72e5fb360a0d89e6aa6.png" data-orig-height="3200" data-orig-width="1472" /></figure>
</div>
<p class="npf_quirky">See if this shows up without getting downsized&hellip;..Tumblr seriously needs to up their game when it comes to upload size, 2048 wide Max is just silly these days. But yeah, Sunflower from Midjourney + Upscaler and Lightroom</p>
<p><span class="npf_color_niles">JELLYFISH ARCADE!</span></p>
]]></content:encoded>
					
					<wfw:commentRss>https://psychonautica.net/archives/3057/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title></title>
		<link>https://psychonautica.net/archives/3058</link>
					<comments>https://psychonautica.net/archives/3058#respond</comments>
		
		<dc:creator><![CDATA[psychonautica]]></dc:creator>
		<pubDate>Thu, 24 Apr 2025 22:46:44 +0000</pubDate>
				<category><![CDATA[Photography]]></category>
		<category><![CDATA[ai art gallery]]></category>
		<category><![CDATA[ai artwork]]></category>
		<category><![CDATA[ai image]]></category>
		<category><![CDATA[aiartcommunity]]></category>
		<category><![CDATA[aiartdaily]]></category>
		<category><![CDATA[aiartwork]]></category>
		<category><![CDATA[generativeart]]></category>
		<category><![CDATA[midjourney]]></category>
		<category><![CDATA[sci fi]]></category>
		<category><![CDATA[stable diffusion]]></category>
		<guid isPermaLink="false">https://psychonautica.net/archives/3058</guid>

					<description><![CDATA[Midjourney V7 + Clarity Upscaler + Lightroom + Love JELLYFISH ARCADE IS BACK YET AGAIN! 4 REAL THIS TIME!!!]]></description>
										<content:encoded><![CDATA[<div class="npf_row">
<figure class="tmblr-full" data-orig-height="1148" data-orig-width="2048"><img decoding="async" src="https://64.media.tumblr.com/42ddc6ec954d97bed53b02079df437ce/a33b784238ff0df0-8b/s640x960/a6837831c565e561ae21127a5856173c59717665.png" data-orig-height="1148" data-orig-width="2048" /></figure>
</div>
<p class="npf_chat">Midjourney V7 + Clarity Upscaler + Lightroom + Love <br /><b><span class="npf_color_niles"><small>JELLYFISH ARCADE IS BACK YET AGAIN! 4 REAL THIS TIME!!! </small></span></b></p>
]]></content:encoded>
					
					<wfw:commentRss>https://psychonautica.net/archives/3058/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title></title>
		<link>https://psychonautica.net/archives/3059</link>
					<comments>https://psychonautica.net/archives/3059#respond</comments>
		
		<dc:creator><![CDATA[psychonautica]]></dc:creator>
		<pubDate>Fri, 18 Apr 2025 07:08:08 +0000</pubDate>
				<category><![CDATA[Photography]]></category>
		<category><![CDATA[ai art gallery]]></category>
		<category><![CDATA[ai artwork]]></category>
		<category><![CDATA[ai image]]></category>
		<category><![CDATA[aiartcommunity]]></category>
		<category><![CDATA[aiartdaily]]></category>
		<category><![CDATA[aiartwork]]></category>
		<category><![CDATA[frogs]]></category>
		<category><![CDATA[generativeart]]></category>
		<category><![CDATA[midjourney]]></category>
		<category><![CDATA[sci fi]]></category>
		<category><![CDATA[stable diffusion]]></category>
		<guid isPermaLink="false">https://psychonautica.net/archives/3059</guid>

					<description><![CDATA[frogz JELLYFISHARCADE]]></description>
										<content:encoded><![CDATA[<div class="npf_row">
<figure class="tmblr-full" data-orig-height="2164" data-orig-width="3860"><img decoding="async" src="https://64.media.tumblr.com/3335ee99b957e364cbae9c9e0e10ed74/cf2d78dfc6886992-71/s640x960/e3eee26d94225f3b2d88d000c5703c0b0a97850f.png" data-orig-height="2164" data-orig-width="3860" /></figure>
</div>
<p class="npf_quirky"><span class="npf_color_niles">frogz JELLYFISHARCADE</span></p>
]]></content:encoded>
					
					<wfw:commentRss>https://psychonautica.net/archives/3059/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title></title>
		<link>https://psychonautica.net/archives/3060</link>
					<comments>https://psychonautica.net/archives/3060#respond</comments>
		
		<dc:creator><![CDATA[psychonautica]]></dc:creator>
		<pubDate>Sun, 13 Apr 2025 01:35:45 +0000</pubDate>
				<category><![CDATA[Photography]]></category>
		<guid isPermaLink="false">https://psychonautica.net/archives/3060</guid>

					<description><![CDATA[Centipede, mech, thing, enjoy. Jellyfish Arcade &#8211; Mid-journey plus upscaler plus half-assed Java on Lightroom. That red is way too saturated. I&#8217;m too lazy to change it right now.]]></description>
										<content:encoded><![CDATA[<div class="npf_row">
<figure class="tmblr-full" data-orig-height="1722" data-orig-width="3072"><img decoding="async" src="https://64.media.tumblr.com/767415f0a2cf9c7eca95ef39f6464427/0ef1986e7c27e60d-20/s640x960/cafee3eab5ada994bf371da40f065a4339361d3a.png" data-orig-height="1722" data-orig-width="3072" /></figure>
</div>
<p>Centipede, mech, thing, enjoy. <b>Jellyfish Arcade</b> &#8211; Mid-journey plus upscaler plus half-assed Java on Lightroom. That red is way too saturated. I&rsquo;m too lazy to change it right now. </p>
]]></content:encoded>
					
					<wfw:commentRss>https://psychonautica.net/archives/3060/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title></title>
		<link>https://psychonautica.net/archives/3061</link>
					<comments>https://psychonautica.net/archives/3061#respond</comments>
		
		<dc:creator><![CDATA[psychonautica]]></dc:creator>
		<pubDate>Fri, 11 Apr 2025 04:41:02 +0000</pubDate>
				<category><![CDATA[Photography]]></category>
		<category><![CDATA[ai art gallery]]></category>
		<category><![CDATA[ai artwork]]></category>
		<category><![CDATA[ai image]]></category>
		<category><![CDATA[aiartcommunity]]></category>
		<category><![CDATA[aiartdaily]]></category>
		<category><![CDATA[aiartwork]]></category>
		<category><![CDATA[generativeart]]></category>
		<category><![CDATA[midjourney]]></category>
		<category><![CDATA[sci fi]]></category>
		<category><![CDATA[stable diffusion]]></category>
		<guid isPermaLink="false">https://psychonautica.net/archives/3061</guid>

					<description><![CDATA[Lively Bustling Midjourney And Creative Upscaler and Lightroom JELLYFISHARCADE!]]></description>
										<content:encoded><![CDATA[<div class="npf_row">
<figure class="tmblr-full" data-orig-height="2350" data-orig-width="4193"><img decoding="async" src="https://64.media.tumblr.com/31be174379120815e5f5d24080fa892b/569df4a955980b6f-5a/s640x960/0deba2cc1d2741393cc0013ded9a1a5ec2193056.png" data-orig-height="2350" data-orig-width="4193" /></figure>
</div>
<p class="npf_chat">Lively Bustling Midjourney And Creative Upscaler and Lightroom </p>
<p><span class="npf_color_niles">JELL</span>YFISHA<span class="npf_color_monica">RCADE!</span></p>
]]></content:encoded>
					
					<wfw:commentRss>https://psychonautica.net/archives/3061/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title></title>
		<link>https://psychonautica.net/archives/3062</link>
					<comments>https://psychonautica.net/archives/3062#respond</comments>
		
		<dc:creator><![CDATA[psychonautica]]></dc:creator>
		<pubDate>Fri, 11 Apr 2025 04:39:17 +0000</pubDate>
				<category><![CDATA[Photography]]></category>
		<category><![CDATA[ai art gallery]]></category>
		<category><![CDATA[ai artwork]]></category>
		<category><![CDATA[ai image]]></category>
		<category><![CDATA[aiartcommunity]]></category>
		<category><![CDATA[aiartdaily]]></category>
		<category><![CDATA[aiartwork]]></category>
		<category><![CDATA[generativeart]]></category>
		<category><![CDATA[midjourney]]></category>
		<category><![CDATA[sci fi]]></category>
		<category><![CDATA[stable diffusion]]></category>
		<guid isPermaLink="false">https://psychonautica.net/archives/3062</guid>

					<description><![CDATA[JELLYFISSH ARCADE MIDJOURNEY V7 &#60;3]]></description>
										<content:encoded><![CDATA[<div class="npf_row">
<figure class="tmblr-full" data-orig-height="4591" data-orig-width="8192"><img decoding="async" src="https://64.media.tumblr.com/211050f48c49475782fb917636cd738d/66527d053cc5835a-ff/s640x960/1cdad37e91248f6180424a894147130d7a6299a6.jpg" data-orig-height="4591" data-orig-width="8192" /></figure>
</div>
<p>JELLYFISSH ARCADE MIDJOURNEY V7 &lt;3 </p>
]]></content:encoded>
					
					<wfw:commentRss>https://psychonautica.net/archives/3062/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
