BACK TO EXPERIMENTS

Heading 1

Heading 2

Heading 3

Button Text

Falling Text

Inclusions
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Baskervville:ital,wght@1,600&family=Montserrat:wght@400;700&display=swap" rel="stylesheet">

<script src="https://cdn.tailwindcss.com"></script>
<script>
  tailwind.config = {
    theme: {
      extend: {
        fontFamily: {
          sans: ['Montserrat', 'sans-serif'],
          serif: ['Baskervville', 'serif'],
        }
      }
    }
  }
</script>
CSS
<style>
  /* Ensure the body doesn't show scrollbars during the experience */
  body {
    background-color: #000000;
    color: #ffffff;
    font-family: 'Montserrat', sans-serif;
    /* We use important to override Webflow's default resets if necessary */
    overflow-x: hidden !important; 
    margin: 0; 
  }

  canvas {
    display: block;
    outline: none;
  }

  /* Hide scrollbar for Chrome/Safari/Opera */
  ::-webkit-scrollbar {
    display: none;
  }

  /* Custom Selection Color */
  ::selection {
      background: #D4AF37;
      color: #000;
  }
  
  /* --- NEW: SCROLLYTELLING CLASSES --- */

  /* The "Track" creates the physical height for the scroll duration (400vh) */
  .scroll-track {
    height: 400vh; 
    position: relative;
    z-index: 1; 
  }

  /* The "Viewport" sticks to the top of the browser window */
  .sticky-viewport {
    position: -webkit-sticky;
    position: sticky;
    top: 0;
    height: 100vh;
    width: 100%;
    overflow: hidden;
  }

  /* Updated Root to fill the sticky container */
  #root {
    position: relative;
    z-index: 1;
    width: 100%;
    height: 100%;
  }
</style>
JS
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>

<script type="importmap">
{
  "imports": {
    "react": "https://esm.sh/react@18.2.0",
    "react/": "https://esm.sh/react@18.2.0/",
    "react-dom": "https://esm.sh/react-dom@18.2.0",
    "react-dom/client": "https://esm.sh/react-dom@18.2.0/client",
    "three": "https://esm.sh/three@0.160.0",
    "@react-three/fiber": "https://esm.sh/@react-three/fiber@8.15.16?external=react,react-dom,three",
    "@react-three/drei": "https://esm.sh/@react-three/drei@9.99.0?external=react,react-dom,three,@react-three/fiber",
    "@react-three/postprocessing": "https://esm.sh/@react-three/postprocessing@2.16.0?external=react,react-dom,three,@react-three/fiber",
    "postprocessing": "https://esm.sh/postprocessing@6.35.0?external=three",
    "uuid": "https://esm.sh/uuid@9.0.1"
  }
}
</script>

<script type="text/babel" data-type="module" data-presets="react, typescript">
    import React, { useRef, useState, useMemo, useEffect, Suspense, useLayoutEffect } from 'react';
    import ReactDOM from 'react-dom/client';
    import { Canvas, useFrame, useThree } from '@react-three/fiber';
    import { EffectComposer, Bloom } from '@react-three/postprocessing';
    import * as THREE from 'three';

    // ------------------------------------------------------------------
    // COMPONENT: Fireflies (Unchanged)
    // ------------------------------------------------------------------
    const fireflyVertexShader = `
      uniform float uTime;
      attribute float aScale;
      attribute vec3 aOffset;
      varying float vAlpha;
      varying float vPhase;
      void main() {
        vec3 pos = position;
        float t = uTime * 0.5;
        pos.x += sin(t + aOffset.x) * 3.0 + sin(t * 0.3 + aOffset.y) * 1.5;
        pos.y += cos(t * 0.8 + aOffset.y) * 1.5 + sin(t * 0.2 + aOffset.x) * 0.5;
        pos.z += sin(t * 0.6 + aOffset.z) * 3.0;
        vec4 mvPosition = modelViewMatrix * vec4(pos, 1.0);
        gl_Position = projectionMatrix * mvPosition;
        gl_PointSize = (300.0 * aScale) / -mvPosition.z;
        vPhase = aOffset.x;
        vAlpha = 1.0;
      }
    `;

    const fireflyFragmentShader = `
      uniform float uTime;
      varying float vAlpha;
      varying float vPhase;
      void main() {
        vec2 uv = gl_PointCoord.xy - 0.5;
        float r = length(uv);
        if (r > 0.5) discard;
        float glow = 1.0 - smoothstep(0.0, 0.5, r);
        glow = pow(glow, 2.0);
        float flicker = 0.6 + 0.4 * sin(uTime * 3.0 + vPhase * 10.0);
        float flash = smoothstep(0.8, 1.0, sin(uTime * 1.0 + vPhase * 20.0));
        float intensity = flicker + flash;
        vec3 color = vec3(1.0, 0.9, 0.4);
        gl_FragColor = vec4(color, glow * vAlpha * intensity);
      }
    `;

    const Fireflies = ({ count = 15 }) => {
      const points = useRef(null);
      const uniforms = useRef({ uTime: { value: 0 } });

      const { positions, scales, offsets } = useMemo(() => {
        const p = new Float32Array(count * 3);
        const s = new Float32Array(count);
        const o = new Float32Array(count * 3);
        for (let i = 0; i < count; i++) {
          p[i * 3] = (Math.random() - 0.5) * 20 - 5;
          p[i * 3 + 1] = Math.random() * 8 + 1;
          p[i * 3 + 2] = (Math.random() - 0.5) * 40 - 5;
          s[i] = Math.random() * 0.5 + 0.5;
          o[i * 3] = Math.random() * 100;
          o[i * 3 + 1] = Math.random() * 100;
          o[i * 3 + 2] = Math.random() * 100;
        }
        return { positions: p, scales: s, offsets: o };
      }, [count]);

      useFrame((state) => {
        uniforms.current.uTime.value = state.clock.getElapsedTime();
      });

      return (
        <points ref={points}>
          <bufferGeometry>
            <bufferAttribute attach="attributes-position" count={count} array={positions} itemSize={3} />
            <bufferAttribute attach="attributes-aScale" count={count} array={scales} itemSize={1} />
            <bufferAttribute attach="attributes-aOffset" count={count} array={offsets} itemSize={3} />
          </bufferGeometry>
          <shaderMaterial
            vertexShader={fireflyVertexShader}
            fragmentShader={fireflyFragmentShader}
            uniforms={uniforms.current}
            transparent={true}
            depthWrite={false}
            depthTest={true}
            blending={THREE.AdditiveBlending}
          />
        </points>
      );
    };

    // ------------------------------------------------------------------
    // COMPONENT: Dust (Unchanged)
    // ------------------------------------------------------------------
    const dustVertexShader = `
      uniform float uTime;
      attribute float aScale;
      attribute vec3 aRandom; 
      varying float vAlpha;
      void main() {
        vec3 pos = position;
        float speed = 0.2 + aRandom.x * 0.3;
        float yOffset = mod(pos.y + uTime * speed + aRandom.y * 10.0, 25.0) - 2.0;
        pos.y = yOffset;
        pos.x += sin(uTime * 0.3 + aRandom.z * 10.0) * 0.5;
        pos.z += cos(uTime * 0.2 + aRandom.x * 10.0) * 0.5;
        vec4 mvPosition = modelViewMatrix * vec4(pos, 1.0);
        gl_Position = projectionMatrix * mvPosition;
        gl_PointSize = (150.0 * aScale) / -mvPosition.z;
        float dist = length(mvPosition.xyz);
        float nearFade = smoothstep(0.5, 3.0, dist);
        float farFade = 1.0 - smoothstep(30.0, 50.0, dist);
        vAlpha = nearFade * farFade * 0.15; 
      }
    `;

    const dustFragmentShader = `
      varying float vAlpha;
      void main() {
        vec2 uv = gl_PointCoord.xy - 0.5;
        float r = length(uv);
        if (r > 0.5) discard;
        float glow = 1.0 - smoothstep(0.0, 0.5, r);
        vec3 dustColor = vec3(0.8, 0.75, 0.6); 
        gl_FragColor = vec4(dustColor, glow * vAlpha);
      }
    `;

    const Dust = ({ count }) => {
      const points = useRef(null);
      const uniforms = useRef({ uTime: { value: 0 } });

      const { positions, scales, randoms } = useMemo(() => {
        const p = new Float32Array(count * 3);
        const s = new Float32Array(count);
        const r = new Float32Array(count * 3);
        for (let i = 0; i < count; i++) {
          p[i * 3] = (Math.random() - 0.5) * 60;
          p[i * 3 + 1] = Math.random() * 20;
          p[i * 3 + 2] = (Math.random() - 0.5) * 60 - 10;
          s[i] = Math.random() * 0.5 + 0.5;
          r[i * 3] = Math.random();
          r[i * 3 + 1] = Math.random();
          r[i * 3 + 2] = Math.random();
        }
        return { positions: p, scales: s, randoms: r };
      }, [count]);

      useFrame((state) => {
        uniforms.current.uTime.value = state.clock.getElapsedTime();
      });

      return (
        <points ref={points}>
          <bufferGeometry>
            <bufferAttribute attach="attributes-position" count={count} array={positions} itemSize={3} />
            <bufferAttribute attach="attributes-aScale" count={count} array={scales} itemSize={1} />
            <bufferAttribute attach="attributes-aRandom" count={count} array={randoms} itemSize={3} />
          </bufferGeometry>
          <shaderMaterial
            vertexShader={dustVertexShader}
            fragmentShader={dustFragmentShader}
            uniforms={uniforms.current}
            transparent={true}
            depthWrite={false}
            blending={THREE.AdditiveBlending}
          />
        </points>
      );
    };

    // ------------------------------------------------------------------
    // COMPONENT: OrganicGrass (Unchanged)
    // ------------------------------------------------------------------
    const grassVertexShader = `
      uniform float uTime;
      uniform vec3 uLionPos;
      uniform float uScroll;
      attribute float aScale;
      attribute float aCurve; 
      attribute float aLean;
      varying vec2 vUv;
      varying float vHeight;
      varying float vVariation;
      varying float vDist; 
      varying vec3 vWorldPos;

      vec3 mod289(vec3 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
      vec2 mod289(vec2 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
      vec3 permute(vec3 x) { return mod289(((x*34.0)+1.0)*x); }
      float snoise(vec2 v) {
        const vec4 C = vec4(0.211324865405187, 0.366025403784439, -0.577350269189626, 0.024390243902439);
        vec2 i  = floor(v + dot(v, C.yy) );
        vec2 x0 = v - i + dot(i, C.xx);
        vec2 i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
        vec4 x12 = x0.xyxy + C.xxzz;
        x12.xy -= i1;
        i = mod289(i);
        vec3 p = permute( permute( i.y + vec3(0.0, i1.y, 1.0 )) + i.x + vec3(0.0, i1.x, 1.0 ));
        vec3 m = max(0.5 - vec3(dot(x0,x0), dot(x12.xy,x12.xy), dot(x12.zw,x12.zw)), 0.0);
        m = m*m ; m = m*m ;
        vec3 x = 2.0 * fract(p * C.www) - 1.0;
        vec3 h = abs(x) - 0.5;
        vec3 ox = floor(x + 0.5);
        vec3 a0 = x - ox;
        m *= 1.79284291400159 - 0.85373472095314 * ( a0*a0 + h*h );
        vec3 g;
        g.x  = a0.x  * x0.x  + h.x  * x0.y;
        g.yz = a0.yz * x12.xz + h.yz * x12.yw;
        return 130.0 * dot(m, g);
      }

      void main() {
        vUv = uv;
        vHeight = uv.y;
        vVariation = aCurve; 
        
        vec3 pos = position;
        float widthTaper = smoothstep(1.0, 0.0, uv.y); 
        pos.x *= widthTaper; 

        vec4 instancePos = instanceMatrix * vec4(0.0, 0.0, 0.0, 1.0);
        vec4 worldPos = modelMatrix * instancePos;
        vWorldPos = worldPos.xyz;
        float dist = distance(cameraPosition, worldPos.xyz);
        vDist = dist;

        float noiseFreq = 0.05;
        float timeSpeed = 0.2; 
        float turbulence = snoise(instancePos.xz * noiseFreq + uTime * timeSpeed);
        float leanAmount = (aLean + turbulence) * uv.y * uv.y;
        
        pos.x += leanAmount;
        pos.z += leanAmount * 0.3; 

        float distToLion = distance(worldPos.xyz, uLionPos);
        float presenceRadius = 3.5; 
        float presence = smoothstep(presenceRadius, 0.0, distToLion) * smoothstep(0.1, 0.6, uScroll);
        float randomStatic = fract(sin(dot(instancePos.xz, vec2(12.9898, 78.233))) * 43758.5453) - 0.5;
        
        pos.x += randomStatic * 1.0 * presence * uv.y;
        pos.z += randomStatic * 1.0 * presence * uv.y;

        float partRadius = 2.5; 
        float partProgress = smoothstep(0.6, 0.95, uScroll);
        float partStrength = smoothstep(partRadius, 0.0, distToLion) * partProgress;
        vec2 dirFromLion = normalize(worldPos.xz - uLionPos.xz);
        float pushDistance = 2.0; 
        pos.x += dirFromLion.x * partStrength * pushDistance * uv.y; 
        pos.z += dirFromLion.y * partStrength * pushDistance * uv.y;
        
        float flatten = 1.0 - (partStrength * 0.8);
        pos.y *= flatten;
        pos.y *= aScale;

        float thicken = 1.0 + smoothstep(10.0, 50.0, dist) * 1.5;
        pos.x *= thicken;

        vec4 mvPosition = modelViewMatrix * instanceMatrix * vec4(pos, 1.0);
        gl_Position = projectionMatrix * mvPosition;
      }
    `;

    const grassFragmentShader = `
      varying vec2 vUv;
      varying float vHeight;
      varying float vVariation;
      varying float vDist;
      varying vec3 vWorldPos;

      uniform float uDensityForeground;
      uniform float uDensityMidground;
      uniform float uDensityBackground;
      
      uniform vec3 uColorDeep;
      uniform vec3 uColorVibrant;
      uniform vec3 uColorGrey;
      
      uniform vec3 uLionPos;
      uniform float uScroll;
      uniform float uTime;

      float rand(vec2 co){
          return fract(sin(dot(co.xy ,vec2(12.9898,78.233))) * 43758.5453);
      }

      void main() {
        float density = 1.0;
        if (vDist < 20.0) {
           density = mix(uDensityForeground, uDensityMidground, vDist / 20.0);
        } else if (vDist < 60.0) {
           density = mix(uDensityMidground, uDensityBackground, (vDist - 20.0) / 40.0);
        } else {
           density = uDensityBackground;
        }

        float randomSeed = rand(vec2(vVariation, 1.0));
        if (randomSeed > density) discard;

        float shape = 1.0 - abs(vUv.x - 0.5) * 2.0;
        if (shape < 0.2) discard;

        vec3 tipColor = mix(uColorVibrant, uColorGrey, vVariation);
        float brightness = 0.8 + 0.4 * fract(vVariation * 43.21);
        tipColor *= brightness;

        float rootDarkness = 0.5 + 0.5 * fract(vVariation * 12.34);
        vec3 baseColor = uColorDeep * rootDarkness;

        vec3 finalColor = mix(baseColor, tipColor, smoothstep(0.0, 1.0, vHeight));
        float fakeLight = 0.8 + 0.4 * smoothstep(0.0, 1.0, vUv.x);
        finalColor *= fakeLight;
        
        float distToLion = distance(vWorldPos, uLionPos);
        
        float hintRadius = 3.5;
        float hintStrength = smoothstep(hintRadius, 0.0, distToLion) * smoothstep(0.1, 0.6, uScroll);
        vec3 hintColor = vec3(0.55, 0.45, 0.25); 
        
        float dangerRadius = 2.2;
        float dangerStrength = smoothstep(dangerRadius, 0.0, distToLion) * smoothstep(0.6, 1.0, uScroll);
        vec3 dangerColor = vec3(0.9, 0.3, 0.0); 
        
        finalColor = mix(finalColor, hintColor, hintStrength * 0.7);
        finalColor = mix(finalColor, dangerColor, dangerStrength * 0.9);

        float darkness = smoothstep(20.0, 70.0, vDist); 
        finalColor = mix(finalColor, vec3(0.0), darkness);

        gl_FragColor = vec4(finalColor, 1.0);
      }
    `;

    const groundPlane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);
    const targetLionPos = new THREE.Vector3();
    const tempVec = new THREE.Vector3();

    const OrganicGrass = ({ count, densities, colors, scrollRef }) => {
      const meshRef = useRef(null);
      const { camera, raycaster, pointer } = useThree();
      
      const uniforms = useRef({ 
        uTime: { value: 0 },
        uScroll: { value: 0 },
        uLionPos: { value: new THREE.Vector3(0, 0, -25) },
        uDensityForeground: { value: 1.0 },
        uDensityMidground: { value: 0.5 },
        uDensityBackground: { value: 0.1 },
        uColorDeep: { value: new THREE.Color('#1b5605') },
        uColorVibrant: { value: new THREE.Color('#386a25') },
        uColorGrey: { value: new THREE.Color('#656b63') }
      });

      const { scales, leans, curves } = useMemo(() => {
        const s = new Float32Array(count);
        const l = new Float32Array(count);
        const c = new Float32Array(count);

        for (let i = 0; i < count; i++) {
          s[i] = 0.5 + Math.random() * 2.0; 
          l[i] = (Math.random() - 0.5) * 2.0; 
          c[i] = Math.random(); 
        }
        return { scales: s, leans: l, curves: c };
      }, [count]);

      useLayoutEffect(() => {
        if (!meshRef.current) return;
        const temp = new THREE.Object3D();

        for (let i = 0; i < count; i++) {
          let x;
          const rDist = Math.random();
          if (rDist < 0.5) {
            x = (Math.random() - 0.5) * 30; 
          } else if (rDist < 0.8) {
            x = (Math.random() - 0.5) * 60; 
          } else {
            x = (Math.random() - 0.5) * 120; 
          }
          const z = 15 - Math.random() * 60; 
          temp.position.set(x, 0, z);
          temp.rotation.y = Math.random() * Math.PI * 2;
          temp.updateMatrix();
          meshRef.current.setMatrixAt(i, temp.matrix);
        }
        meshRef.current.instanceMatrix.needsUpdate = true;
      }, [count]);

      useFrame((state) => {
        uniforms.current.uTime.value = state.clock.getElapsedTime();
        uniforms.current.uScroll.value = scrollRef.current;
        if (scrollRef.current > 0.75) {
          raycaster.setFromCamera(state.pointer, camera);
          const hit = raycaster.ray.intersectPlane(groundPlane, tempVec);
          if (hit) {
            targetLionPos.copy(hit);
          }
        } else {
          targetLionPos.set(0, 0, -25);
        }
        uniforms.current.uLionPos.value.lerp(targetLionPos, 0.04);
        uniforms.current.uDensityForeground.value = densities.f;
        uniforms.current.uDensityMidground.value = densities.m;
        uniforms.current.uDensityBackground.value = densities.b;
        uniforms.current.uColorDeep.value.set(colors.deep);
        uniforms.current.uColorVibrant.value.set(colors.vibrant);
        uniforms.current.uColorGrey.value.set(colors.grey);
      });

      return (
        <instancedMesh ref={meshRef} args={[null, null, count]} frustumCulled={false}>
          <planeGeometry args={[0.06, 1.0, 1, 4]}>
            <instancedBufferAttribute attach="attributes-aScale" args={[scales, 1]} />
            <instancedBufferAttribute attach="attributes-aLean" args={[leans, 1]} />
            <instancedBufferAttribute attach="attributes-aCurve" args={[curves, 1]} />
          </planeGeometry>
          <shaderMaterial
            vertexShader={grassVertexShader}
            fragmentShader={grassFragmentShader}
            uniforms={uniforms.current}
            side={THREE.DoubleSide}
            depthWrite={true} 
            transparent={false} 
          />
        </instancedMesh>
      );
    };

    // ------------------------------------------------------------------
    // COMPONENT: VignetteOverlay
    // ------------------------------------------------------------------
    const VignetteOverlay = ({ strength, radius, noise, darkness }) => {
      const innerStop = Math.max(0, radius * 100); 
      const outerStop = Math.min(150, (radius + 0.5) * 100); 

      const blurStyle = {
        WebkitBackdropFilter: `blur(${strength * 4}px)`,
        backdropFilter: `blur(${strength * 4}px)`,
        WebkitMaskImage: `radial-gradient(circle, transparent ${innerStop}%, black ${outerStop}%)`,
        maskImage: `radial-gradient(circle, transparent ${innerStop}%, black ${outerStop}%)`,
      };

      const darkStyle = {
        background: `radial-gradient(circle, transparent ${innerStop}%, rgba(0,0,0, ${darkness}) ${outerStop}%)`,
      };
      
      const noiseBg = `url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)' opacity='1'/%3E%3C/svg%3E")`;

      return (
        <React.Fragment>
          <div className="fixed inset-0 pointer-events-none z-[5] transition-all duration-100 ease-linear" style={blurStyle} />
          <div className="fixed inset-0 pointer-events-none z-[6] transition-all duration-100 ease-linear" style={darkStyle} />
          <div className="fixed inset-0 pointer-events-none z-[7] mix-blend-overlay opacity-50 transition-opacity duration-100 ease-linear" style={{ backgroundImage: noiseBg, opacity: noise, backgroundSize: '100px 100px' }} />
        </React.Fragment>
      );
    };

    // ------------------------------------------------------------------
    // UPDATED: ScrollManager (Logic Only)
    // ------------------------------------------------------------------
    const ScrollManager = ({ scrollRef }) => {
      useEffect(() => {
        const handleScroll = () => {
          // This must match the height of your Webflow 'Scrolly Track' (400vh)
          const trackHeight = window.innerHeight * 4;
          const scrollableDistance = trackHeight - window.innerHeight;
          const currentScroll = window.scrollY;
          
          let progress = currentScroll / scrollableDistance;
          progress = Math.min(Math.max(progress, 0), 1);
          
          scrollRef.current = progress;
        };
        
        window.addEventListener('scroll', handleScroll);
        handleScroll();
        return () => window.removeEventListener('scroll', handleScroll);
      }, [scrollRef]);
      return null;
    };

    const CameraRig = ({ scrollRef }) => {
      const { camera } = useThree();
      const currentPos = useRef(new THREE.Vector3(-4, 4, 14));
      const currentLook = useRef(new THREE.Vector3(0, 0, 0));

      useFrame((state) => {
        const r = scrollRef.current;
        const t = state.clock.getElapsedTime();
        const p1 = new THREE.Vector3(-4, 4, 14);
        const p2 = new THREE.Vector3(-7, 2, -5);
        const p3 = new THREE.Vector3(-10, 20, -20);
        const l1 = new THREE.Vector3(5, 0, -5);
        const l2 = new THREE.Vector3(0, 0.5, -25);
        const l3 = new THREE.Vector3(0, -10, -20);

        let targetPos = new THREE.Vector3();
        let targetLook = new THREE.Vector3();

        if (r < 0.5) {
          const n = r * 2;
          const ease = n * n;
          targetPos.lerpVectors(p1, p2, ease);
          targetLook.lerpVectors(l1, l2, ease);
        } else {
          const n = (r - 0.5) * 2;
          const ease = 1 - Math.pow(1 - n, 3);
          targetPos.lerpVectors(p2, p3, ease);
          targetLook.lerpVectors(l2, l3, ease);
        }
        targetPos.y += Math.sin(t * 0.5) * 0.2;
        currentPos.current.lerp(targetPos, 0.05);
        currentLook.current.lerp(targetLook, 0.05);
        camera.position.copy(currentPos.current);
        camera.lookAt(currentLook.current);
      });
      return null;
    };

    // ------------------------------------------------------------------
    // UPDATED: App Component (Clean)
    // ------------------------------------------------------------------
    const App = () => {
      const scrollRef = useRef(0);
      const [lomoConfig] = useState({ strength: 5.8, radius: 0.47, noise: 0.3, darkness: 0.35 });
      const densities = { f: 1.0, m: 0.5, b: 0.1 };
      const colors = { deep: '#061400', vibrant: '#7fa446', grey: '#663915' };

      return (
        <div className="w-full h-full relative bg-black text-white selection:bg-amber-500 selection:text-black">
          <ScrollManager scrollRef={scrollRef} />
          
          <div className="absolute inset-0 w-full h-full z-0">
            <Canvas flat dpr={[1, 1.5]} gl={{ antialias: false, powerPreference: "high-performance", depth: true }} camera={{ fov: 45, near: 0.1, far: 100 }}>
              <color attach="background" args={['#000000']} />
              <Suspense fallback={null}>
                <CameraRig scrollRef={scrollRef} />
                <ambientLight intensity={0.1} />
                <spotLight position={[10, 20, 10]} angle={0.5} penumbra={1} intensity={2} color="#ffffff" />
                <pointLight position={[-10, 2, -10]} intensity={0.5} color="#D4AF37" />
                <OrganicGrass count={100000} densities={densities} colors={colors} scrollRef={scrollRef} />
                <Dust count={500} />
                <Fireflies count={15} />
              </Suspense>
              <EffectComposer enableNormalPass={false}>
                <Bloom luminanceThreshold={0.2} mipmapBlur intensity={1.0} radius={0.6} />
              </EffectComposer>
            </Canvas>
          </div>
          
          <VignetteOverlay strength={lomoConfig.strength} radius={lomoConfig.radius} noise={lomoConfig.noise} darkness={lomoConfig.darkness} />
        </div>
      );
    };

    // ------------------------------------------------------------------
    // RENDER
    // ------------------------------------------------------------------
    const root = ReactDOM.createRoot(document.getElementById('root'));
    root.render(<App />);

</script>