<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <title>GenMotion v2.0 — Multi-Model AI Motion Pipeline</title>
  <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=JetBrains+Mono:ital,wght@0,100..800;1,100..800&family=Bebas+Neue&display=swap" rel="stylesheet">
  <style>
    /* ===== RESET & ROOT ===== */
    *{margin:0;padding:0;box-sizing:border-box}
    :root{
      --void:#08080c;
      --chalk:#e8e4dd;
      --acid:#c8ff00;
      --acid-dim:rgba(200,255,0,0.15);
      --mono:'JetBrains Mono',monospace;
      --display:'Bebas Neue',sans-serif;
      --chalk-rgb:232,228,221;
      --acid-rgb:200,255,0;
    }
    html,body{width:100vw;height:100vh;overflow:hidden;background:var(--void);color:var(--chalk);cursor:none;font-family:var(--mono);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}
    body::before{content:'';position:fixed;inset:0;background:url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48ZGVmcz48ZmlsdGVyIGlkPSJub2lzZSIgeD0iMCUiIHk9IjAlIiB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIj48ZmVUdXJidWxlbmNlIHR5cGU9ImZyYWN0YWxOb2lzZSIgYmFzZUZyZXF1ZW5jeT0iLjk4IiBudW1PY3RhdmVzPSI0IiByZXN1bHQ9Im5vaXNlIi8+PGZlQ29sb3JNYXRyaXggdHlwZT0ibWF0cml4IiB2YWx1ZXM9IjAgMCAwIDAgMSAwIDAgMCAwIDEgMCAwIDAgMCAxIDAgMCAwIC4wMTUgMCIgLz48L2ZpbHRlcj48L2RlZnM+PHJlY3Qgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgZmlsdGVyPSJ1cmwoI25vaXNlKSIvPjwvc3ZnPg==');opacity:0.015;pointer-events:none;z-index:0}

    /* ===== CURSOR =====
       FIX: use position:fixed with left/top updated via JS.
       Do NOT use transform for positioning — it conflicts with
       the translate(-50%,-50%) centering trick. Use left/top instead. */
    .cursor{
      position:fixed;
      width:6px;height:6px;
      background:var(--acid);
      border-radius:0;
      /* FIX: centering offset via margin, not transform, so JS can freely set left/top */
      margin-left:-3px;
      margin-top:-3px;
      pointer-events:none;
      z-index:9999;
      /* keep scale pulse via a separate animation so JS left/top is never overridden */
      animation: cursorPulse 2s ease infinite;
    }
    @keyframes cursorPulse {
      0%,100%{transform:scale(1)}
      50%{transform:scale(1.4)}
    }

    /* ===== GRID OVERLAY ===== */
    .grid-overlay{position:fixed;inset:0;background-image:repeating-linear-gradient(0deg,transparent,transparent 59px,rgba(var(--chalk-rgb),.04) 59px,rgba(var(--chalk-rgb),.04) 60px),repeating-linear-gradient(90deg,transparent,transparent 59px,rgba(var(--chalk-rgb),.04) 59px,rgba(var(--chalk-rgb),.04) 60px);opacity:1;pointer-events:none;z-index:2}

    /* ===== HEADER ===== */
    .header{position:fixed;top:0;left:0;right:0;display:flex;justify-content:space-between;align-items:center;padding:30px 40px;z-index:20}
    .header .logo{font-family:var(--display);font-size:24px;letter-spacing:4px;color:var(--acid)}
    .header .version{font-family:var(--mono);font-size:11px;color:var(--chalk);opacity:.4}

    /* ===== HERO ===== */
    .hero{position:relative;display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:100vh;z-index:10;text-align:center}
    .title{font-family:var(--display);font-size:clamp(60px,8vw,140px);line-height:.9;color:var(--chalk);opacity:0;transform:translateY(60px);transition:opacity .8s ease-out,transform .8s cubic-bezier(.165,.84,.44,1)}
    .title .accent{color:var(--acid)}
    .title.visible{opacity:1;transform:translateY(0)}

    /* ===== STATS ROW ===== */
    .stats-row{display:flex;gap:60px;margin-top:40px;opacity:0;transform:translateY(30px);transition:opacity .7s ease-out .2s,transform .7s cubic-bezier(.165,.84,.44,1) .2s}
    .stats-row.visible{opacity:1;transform:translateY(0)}
    .stat{text-align:center}
    .stat-number{font-family:var(--display);font-size:64px;color:var(--acid);font-variant-numeric:tabular-nums}
    .stat-label{font-family:var(--mono);font-size:10px;color:var(--chalk);opacity:.5;letter-spacing:2px;margin-top:4px;text-transform:uppercase}

    /* ===== PIPELINE SECTION ===== */
    .pipeline-section{margin-top:60px;opacity:0;transform:translateY(30px);transition:opacity .6s ease-out .4s,transform .6s cubic-bezier(.165,.84,.44,1) .4s}
    .pipeline-section.visible{opacity:1;transform:translateY(0)}
    .pipeline-label{font-family:var(--mono);font-size:10px;color:var(--acid);opacity:.6;letter-spacing:3px;text-transform:uppercase;margin-bottom:16px}
    .pipeline-flow{display:flex;align-items:center;gap:12px;justify-content:center;flex-wrap:wrap}
    .pipeline-node{padding:12px 20px;border:1px solid rgba(var(--chalk-rgb),.15);transition:border-color .4s,background .4s;border-radius:0}
    .pipeline-node .model-name{font-family:var(--mono);font-size:11px;font-weight:700;color:var(--acid)}
    .pipeline-node .model-role{font-family:var(--mono);font-size:9px;color:var(--chalk);opacity:.5;text-transform:uppercase;margin-top:4px}
    .pipeline-arrow{font-family:var(--mono);font-size:16px;color:var(--chalk);opacity:.2}
    .pipeline-node.active{border-color:rgba(var(--acid-rgb),.4);background:var(--acid-dim)}

    /* ===== MODELS ROW ===== */
    .models-row{display:flex;gap:20px;margin-top:50px;max-width:1200px;width:100%;justify-content:center;opacity:0;transition:opacity .8s ease-out .6s}
    .models-row.visible{opacity:1}
    .model-card{flex:1;max-width:360px;padding:24px;border:1px solid rgba(var(--chalk-rgb),.1);position:relative;overflow:hidden;transition:border-color .4s,opacity .6s ease-out,transform .6s cubic-bezier(.165,.84,.44,1);opacity:0;transform:translateY(20px) scale(0.98)}
    .model-card.visible{opacity:1;transform:translateY(0) scale(1)}
    .model-card::before{content:'';position:absolute;top:0;left:0;width:100%;height:2px;background:var(--acid);transform:scaleX(0);transform-origin:left;transition:transform .6s cubic-bezier(.165,.84,.44,1)}
    /* FIX: cards use .visible for reveal AND .active for the top border — both are needed */
    .model-card.active::before{transform:scaleX(1)}
    .model-card .card-model{font-family:var(--display);font-size:28px;color:var(--acid);line-height:1}
    .model-card .card-role{font-family:var(--mono);font-size:10px;color:var(--chalk);opacity:.5;letter-spacing:2px;text-transform:uppercase;margin-top:4px}
    .model-card .card-desc{font-family:var(--mono);font-size:12px;color:var(--chalk);opacity:.7;line-height:1.6;margin-top:16px}
    .card-stats{display:flex;gap:16px;margin-top:16px;padding-top:12px;border-top:1px solid rgba(var(--chalk-rgb),.08)}
    .card-stat-val{font-family:var(--mono);font-size:14px;color:var(--acid);font-variant-numeric:tabular-nums}
    .card-stat-key{font-family:var(--mono);font-size:9px;color:var(--chalk);opacity:.4;text-transform:uppercase}

    /* ===== TECH BADGES ===== */
    .tech-badges{display:flex;gap:10px;margin-top:40px;opacity:0;transition:opacity .6s ease-out .8s}
    .tech-badges.visible{opacity:1}
    /* FIX: badge starts invisible, becomes visible via JS stagger, then active for color */
    .badge{padding:6px 14px;font-family:var(--mono);font-size:10px;color:var(--chalk);border:1px solid rgba(var(--chalk-rgb),.12);text-transform:uppercase;letter-spacing:1px;opacity:0;transform:translateY(10px) scale(0.95);transition:border-color .4s,color .4s,opacity .4s ease-out,transform .4s cubic-bezier(.165,.84,.44,1)}
    .badge.visible{opacity:.6;transform:translateY(0) scale(1)}
    .badge.active{border-color:rgba(var(--acid-rgb),.3);color:var(--acid);opacity:1}

    /* ===== FOOTER BAR ===== */
    .footer-bar{position:fixed;bottom:0;left:0;right:0;display:flex;justify-content:space-between;align-items:center;padding:16px 40px;font-family:var(--mono);font-size:10px;color:var(--chalk);opacity:.3;z-index:20}
    .footer-bar .status{display:flex;align-items:center;gap:8px}
    .footer-bar .status::before{content:'';width:6px;height:6px;border-radius:50%;background:var(--acid)}
    .footer-bar .fps{font-variant-numeric:tabular-nums}

    /* ===== ANIMATIONS ===== */
    @keyframes scanSweep{0%{transform:translateX(-120%)}100%{transform:translateX(120%)}}
    @keyframes fadeSlideUp{from{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}
    @keyframes pulseGlow{0%,100%{opacity:.4}50%{opacity:1}}
    @keyframes borderDraw{from{transform:scaleX(0)}to{transform:scaleX(1)}}
    @keyframes pulse{0%,100%{transform:scale(1)}50%{transform:scale(1.05)}}
  </style>
</head>
<body>
  <canvas id="bgCanvas"></canvas>
  <div class="grid-overlay"></div>
  <!-- FIX: cursor element cached in JS — never queried inside RAF -->
  <div class="cursor" id="cursor"></div>

  <header class="header">
    <div class="logo">GENMOTION</div>
    <div class="version">v2.0 — multi-model pipeline</div>
  </header>

  <main class="hero" id="hero">
    <h1 class="title" id="title">
      <span class="accent">173</span> Animation Systems.<br>
      <span class="accent">3</span> Models.<br>
      One Pipeline.
    </h1>

    <div class="stats-row" id="statsRow">
      <div class="stat">
        <div class="stat-number" id="statSystems">0</div>
        <div class="stat-label">Animation Systems</div>
      </div>
      <div class="stat">
        <div class="stat-number" id="statCategories">0</div>
        <div class="stat-label">Categories</div>
      </div>
      <div class="stat">
        <div class="stat-number" id="statModels">0</div>
        <div class="stat-label">AI Models</div>
      </div>
      <div class="stat">
        <div class="stat-number" id="statLines">0</div>
        <div class="stat-label">Lines of Reference</div>
      </div>
    </div>

    <div class="pipeline-section" id="pipelineSection">
      <div class="pipeline-label">Pipeline Architecture</div>
      <div class="pipeline-flow">
        <div class="pipeline-node" id="pNode1">
          <div class="model-name">K2 Thinking</div>
          <div class="model-role">Creative Director</div>
        </div>
        <div class="pipeline-arrow">→</div>
        <div class="pipeline-node" id="pNode2">
          <div class="model-name">K2 Instruct</div>
          <div class="model-role">Architect</div>
        </div>
        <div class="pipeline-arrow">→</div>
        <div class="pipeline-node" id="pNode3">
          <div class="model-name">Minimax M2</div>
          <div class="model-role">Builder × 4</div>
        </div>
        <div class="pipeline-arrow">→</div>
        <div class="pipeline-node" id="pNode4">
          <div class="model-name">Minimax M2</div>
          <div class="model-role">Critic / QA</div>
        </div>
      </div>
    </div>

    <div class="models-row" id="modelsRow">
      <div class="model-card" id="card1">
        <div class="card-model">Kimi K2</div>
        <div class="card-role">Thinking — Creative Director</div>
        <div class="card-desc">Deep reasoning for creative decisions. Produces the design document — visual concept, timeline, color system, hero moment. Never writes code.</div>
        <div class="card-stats">
          <div>
            <div class="card-stat-val">256K</div>
            <div class="card-stat-key">Context</div>
          </div>
          <div>
            <div class="card-stat-val">Design</div>
            <div class="card-stat-key">Output</div>
          </div>
        </div>
      </div>
      <div class="model-card" id="card2">
        <div class="card-model">Kimi K2</div>
        <div class="card-role">Instruct — Architect</div>
        <div class="card-desc">Translates creative vision into technical specification. Module breakdown, globals, interfaces, trimmed reference catalog. Fast and surgical.</div>
        <div class="card-stats">
          <div>
            <div class="card-stat-val">16K</div>
            <div class="card-stat-key">Tokens</div>
          </div>
          <div>
            <div class="card-stat-val">Spec</div>
            <div class="card-stat-key">Output</div>
          </div>
        </div>
      </div>
      <div class="model-card" id="card3">
        <div class="card-model">Minimax M2</div>
        <div class="card-role">Builder + Critic</div>
        <div class="card-desc">Cross-context consistency across 4-part code generation. Maintains variable names, data structures, and interfaces. Then audits its own output.</div>
        <div class="card-stats">
          <div>
            <div class="card-stat-val">64K</div>
            <div class="card-stat-key">Output</div>
          </div>
          <div>
            <div class="card-stat-val">Code</div>
            <div class="card-stat-key">Output</div>
          </div>
        </div>
      </div>
    </div>

    <div class="tech-badges" id="techBadges">
      <div class="badge">CSS</div>
      <div class="badge">Canvas 2D</div>
      <div class="badge">SVG</div>
      <div class="badge">WebGL</div>
      <div class="badge">GLSL</div>
    </div>
  </main>

  <footer class="footer-bar">
    <div class="status" id="status">Pipeline Active</div>
    <div class="fps" id="fps">-- FPS</div>
  </footer>

  <script>
    /* ===== MODULE GLOBALS ===== */
    const canvas = document.getElementById('bgCanvas');
    // FIX: guard context acquisition
    const ctx = canvas.getContext('2d');
    if (!ctx) console.error('Canvas 2D context unavailable');

    // FIX: cache cursor element once — never query it inside the RAF loop
    const cursorEl = document.getElementById('cursor');

    let W = window.innerWidth;
    let H = window.innerHeight;
    let mouse = { x: W / 2, y: H / 2 };
    let phase = 'intro';
    let startTime = 0;
    let particles = [];

    // FPS tracking
    let lastFrameTime = 0;
    let fpsSmoothed = 0;       // FIX: start at 0, not 60 — computed honestly from first frame
    let fpsUpdateTimer = 0;
    const fpsEl = document.getElementById('fps');
    const statusEl = document.getElementById('status');

    /* ===== RESIZE HANDLER =====
       FIX: set canvas dimensions BEFORE calling initParticles,
       so W/H are correct when particles reference them */
    function resizeHandler() {
      W = window.innerWidth;
      H = window.innerHeight;
      canvas.width  = W;
      canvas.height = H;
      initParticles(); // safe to call now — canvas is properly sized
    }

    /* ===== PARTICLE SYSTEM ===== */
    function initParticles() {
      particles.length = 0;
      const spacing = 40;
      const cols = Math.ceil(W / spacing);
      const rows = Math.ceil(H / spacing);

      for (let gy = 0; gy < rows; gy++) {
        for (let gx = 0; gx < cols; gx++) {
          const gridX = gx * spacing;
          const gridY = gy * spacing;
          particles.push({
            gx: gridX,
            gy: gridY,
            x:  gridX,
            y:  gridY,
            baseAlpha: 0.04 + Math.random() * 0.06,
            phase:     Math.random() * Math.PI * 2,
            alpha:     0
          });
        }
      }
    }

    function updateParticles(ts) {
      // FIX: use ts (milliseconds) consistently throughout — don't mix ms and seconds
      const breatheSpeed  = 0.0008;  // cycles per ms → comfortable ~1.3s period
      const breatheAmp    = 2;
      const mouseRadius   = 150;
      const mouseRepel    = 3;
      const alphaSpeed    = 0.0015;

      for (const p of particles) {
        let targetX = p.gx;
        let targetY = p.gy;

        // Gentle breathe using raw ts (milliseconds)
        const breatheOffset = Math.sin(p.phase + ts * breatheSpeed) * breatheAmp;
        targetX += breatheOffset;
        targetY += breatheOffset;

        // Mouse repulsion
        const dx = mouse.x - p.gx;
        const dy = mouse.y - p.gy;
        const dist = Math.sqrt(dx * dx + dy * dy);
        if (dist < mouseRadius && dist > 0) {
          const force = (1 - dist / mouseRadius) * mouseRepel;
          const angle = Math.atan2(dy, dx);
          targetX -= Math.cos(angle) * force;
          targetY -= Math.sin(angle) * force;
        }

        p.x = targetX;
        p.y = targetY;

        // Alpha flicker
        const alphaMod = Math.sin(ts * alphaSpeed + p.phase) * 0.02;
        p.alpha = Math.max(0, Math.min(1, p.baseAlpha + alphaMod));
      }
    }

    function drawParticles() {
      if (!ctx) return;
      ctx.clearRect(0, 0, W, H);
      for (const p of particles) {
        ctx.fillStyle = `rgba(200,255,0,${p.alpha})`;
        ctx.fillRect(p.x - 1, p.y - 1, 2, 2);
      }
    }

    /* FIX: drawScanLine received raw ts (milliseconds) but divided by a constant
       designed for seconds. Corrected to seconds via ts*0.001 internally. */
    function drawScanLine(ts) {
      if (!ctx) return;
      const timeSec = ts * 0.001;               // convert ms → seconds
      const scanProgress = (timeSec * 0.125) % 1; // one sweep every 8 seconds
      const scanY = scanProgress * H;
      ctx.fillStyle = 'rgba(200,255,0,0.06)';
      ctx.fillRect(0, scanY, W, 1);
    }

    /* FIX: FPS calculated properly — skip the very first frame (delta would be 0)
       and smooth from real measurements rather than seeding at 60 */
    function updateFPS(ts) {
      if (lastFrameTime === 0) {
        lastFrameTime = ts;
        return;
      }
      const delta = ts - lastFrameTime;
      lastFrameTime = ts;
      if (delta <= 0) return;

      const instantFPS = 1000 / delta;
      // Smooth with exponential moving average — starts from real first value
      fpsSmoothed = fpsSmoothed === 0
        ? instantFPS
        : fpsSmoothed * 0.9 + instantFPS * 0.1;

      fpsUpdateTimer += delta;
      if (fpsUpdateTimer >= 500) {
        if (fpsEl) fpsEl.textContent = Math.round(fpsSmoothed) + ' FPS';
        fpsUpdateTimer = 0;
      }
    }

    /* ===== COUNTER ANIMATION ===== */
    function animateCounter(id, target, duration) {
      const el = document.getElementById(id);
      if (!el) return;
      const t0 = performance.now();
      function step(now) {
        const progress = Math.min((now - t0) / duration, 1);
        const eased    = 1 - Math.pow(1 - progress, 3);
        el.textContent = Math.floor(eased * target).toLocaleString();
        if (progress < 1) requestAnimationFrame(step);
        else el.textContent = target.toLocaleString();
      }
      requestAnimationFrame(step);
    }

    /* ===== CARD STAGGER =====
       FIX: adds both .visible (opacity reveal) AND .active (top border)
       with the same stagger — previously .active was never added to cards */
    function staggerCards(selector, offset) {
      document.querySelectorAll(selector).forEach((card, idx) => {
        setTimeout(() => {
          card.classList.add('visible');
          card.classList.add('active');
        }, idx * 120 + offset);
      });
    }

    function activatePipelineNodes() {
      document.querySelectorAll('.pipeline-node').forEach((node, idx) => {
        setTimeout(() => node.classList.add('active'), idx * 200);
      });
    }

    /* FIX: badge stagger correctly separates visible (opacity) from active (color).
       Previously both were applied in the same timeout, so the stagger was correct
       but the opacity animation had no base state to transition from because
       .badge had opacity:0 and neither class alone gave it a visible state.
       Now: visible → fades in at 60% opacity, then active → full acid color */
    function activateBadges() {
      document.querySelectorAll('.badge').forEach((badge, idx) => {
        setTimeout(() => {
          badge.classList.add('visible');
          // Slight extra delay before accent color kicks in
          setTimeout(() => badge.classList.add('active'), 200);
        }, idx * 100);
      });
    }

    function typeStatus(message) {
      if (!statusEl) return;
      statusEl.textContent = '';
      let idx = 0;
      function tick() {
        if (idx < message.length) {
          statusEl.textContent += message.charAt(idx++);
          setTimeout(tick, 40);
        }
      }
      tick();
    }

    /* ===== INTRO SEQUENCE ===== */
    function animateIn() {
      setTimeout(() => document.getElementById('title').classList.add('visible'),         800);
      setTimeout(() => document.getElementById('statsRow').classList.add('visible'),      1400);
      setTimeout(() => {
        animateCounter('statSystems',    173,  1800);
        animateCounter('statCategories',   5,  1200);
        animateCounter('statModels',        3,   800);
        animateCounter('statLines',      3350,  2000);
      }, 1500);
      setTimeout(() => document.getElementById('pipelineSection').classList.add('visible'), 2200);
      setTimeout(() => activatePipelineNodes(),                                              2400);
      setTimeout(() => document.getElementById('modelsRow').classList.add('visible'),       3000);
      setTimeout(() => staggerCards('.model-card', 0),                                      3200);
      setTimeout(() => {
        document.getElementById('techBadges').classList.add('visible');
        activateBadges();
      }, 4000);
      setTimeout(() => {
        phase = 'idle';
        typeStatus('Pipeline Active — All Systems Nominal');
      }, 4500);
    }

    /* ===== MASTER RAF LOOP =====
       FIX: cursor positioned via left/top (set once per frame from cached element),
       never via style.transform which would override the CSS centering offset.
       FIX: cursorEl is the cached reference — no querySelector inside the loop. */
    function masterLoop(ts) {
      updateParticles(ts);
      drawParticles();
      drawScanLine(ts);
      updateFPS(ts);

      // FIX: position cursor via left/top using the cached element reference
      if (cursorEl) {
        cursorEl.style.left = mouse.x + 'px';
        cursorEl.style.top  = mouse.y + 'px';
      }

      requestAnimationFrame(masterLoop);
    }

    /* ===== EVENTS ===== */
    document.addEventListener('mousemove', e => {
      mouse.x = e.clientX;
      mouse.y = e.clientY;
    });

    /* ===== INIT =====
       FIX: resizeHandler() must run before anything else — it sets canvas.width/height
       AND calls initParticles(). Only after that do we start the loop and sequence. */
    document.addEventListener('DOMContentLoaded', () => {
      startTime = performance.now();
      resizeHandler();
      window.addEventListener('resize', resizeHandler);
      requestAnimationFrame(masterLoop);
      animateIn();
    });
  </script>
</body>
</html>