<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>THE MOTION CATALOG — Web Animation Systems</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Space+Mono:ital,wght@0,400;0,700;1,400&family=Instrument+Serif:ital@0;1&display=swap" rel="stylesheet">
<style>
  :root {
    --ink: #0a0a0f;
    --paper: #f0ede6;
    --acid: #c8ff00;
    --electric: #00f5ff;
    --blood: #ff2d55;
    --gold: #ffd700;
    --dim: #1a1a2e;
  }

  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

  html { scroll-behavior: smooth; }

  body {
    background: var(--ink);
    color: var(--paper);
    font-family: 'Space Mono', monospace;
    overflow-x: hidden;
    cursor: none;
  }

  /* ── CUSTOM CURSOR ── */
  #cursor-dot {
    position: fixed; width: 8px; height: 8px;
    background: var(--acid); border-radius: 50%;
    pointer-events: none; z-index: 10000;
    transform: translate(-50%, -50%);
    transition: transform 0.05s;
    mix-blend-mode: difference;
  }
  #cursor-ring {
    position: fixed; width: 40px; height: 40px;
    border: 1px solid var(--acid); border-radius: 50%;
    pointer-events: none; z-index: 9999;
    transform: translate(-50%, -50%);
    transition: transform 0.12s ease, width 0.2s, height 0.2s, opacity 0.2s;
    opacity: 0.6;
    mix-blend-mode: difference;
  }
  body:has(a:hover) #cursor-ring,
  body:has(button:hover) #cursor-ring,
  body:has(.clickable:hover) #cursor-ring {
    width: 60px; height: 60px; opacity: 1;
  }

  /* ── NOISE OVERLAY ── */
  body::before {
    content: '';
    position: fixed; inset: 0;
    background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.04'/%3E%3C/svg%3E");
    pointer-events: none; z-index: 9998; opacity: 0.35;
  }

  /* ── HERO ── */
  #hero {
    position: relative; height: 100vh;
    display: flex; flex-direction: column;
    justify-content: center; align-items: center;
    overflow: hidden;
  }

  #hero-canvas {
    position: absolute; inset: 0;
    width: 100%; height: 100%;
  }

  .hero-content {
    position: relative; z-index: 2;
    text-align: center;
  }

  .hero-eyebrow {
    font-family: 'Space Mono', monospace;
    font-size: 0.7rem; letter-spacing: 0.4em;
    color: var(--acid); text-transform: uppercase;
    opacity: 0;
    animation: fadeUp 0.8s ease 0.5s forwards;
  }

  .hero-title {
    font-family: 'Bebas Neue', sans-serif;
    font-size: clamp(5rem, 18vw, 16rem);
    line-height: 0.85;
    letter-spacing: -0.02em;
    background: linear-gradient(135deg, var(--paper) 0%, var(--acid) 50%, var(--electric) 100%);
    background-size: 300% 300%;
    -webkit-background-clip: text;
    -webkit-text-fill-color: transparent;
    background-clip: text;
    animation: gradShift 6s ease infinite, fadeUp 1s ease 0.3s both;
  }

  @keyframes gradShift {
    0%,100% { background-position: 0% 50%; }
    50%      { background-position: 100% 50%; }
  }

  .hero-subtitle {
    font-family: 'Instrument Serif', serif;
    font-style: italic;
    font-size: clamp(1rem, 2.5vw, 1.6rem);
    color: rgba(240,237,230,0.6);
    margin-top: 1rem;
    opacity: 0;
    animation: fadeUp 0.8s ease 0.9s forwards;
  }

  .hero-scroll {
    position: absolute; bottom: 2rem; left: 50%;
    transform: translateX(-50%);
    font-size: 0.6rem; letter-spacing: 0.3em;
    color: var(--acid); text-transform: uppercase;
    display: flex; flex-direction: column; align-items: center; gap: 0.5rem;
    opacity: 0; animation: fadeUp 0.8s ease 1.5s forwards;
  }
  .scroll-line {
    width: 1px; height: 40px;
    background: linear-gradient(to bottom, var(--acid), transparent);
    animation: scrollPulse 2s ease-in-out infinite;
  }
  @keyframes scrollPulse {
    0%,100% { opacity: 1; transform: scaleY(1); }
    50%      { opacity: 0.3; transform: scaleY(0.5); }
  }

  @keyframes fadeUp {
    from { opacity: 0; transform: translateY(30px); }
    to   { opacity: 1; transform: translateY(0); }
  }

  /* ── SECTION BASE ── */
  .section {
    position: relative;
    padding: 8rem 4vw;
    overflow: hidden;
  }

  .section-number {
    font-family: 'Bebas Neue', monospace;
    font-size: 0.65rem;
    letter-spacing: 0.3em;
    color: var(--acid);
    text-transform: uppercase;
    margin-bottom: 0.5rem;
    display: flex; align-items: center; gap: 1rem;
  }
  .section-number::after {
    content: '';
    flex: 1; max-width: 60px;
    height: 1px; background: var(--acid); opacity: 0.4;
  }

  .section-title {
    font-family: 'Bebas Neue', sans-serif;
    font-size: clamp(2.5rem, 7vw, 6rem);
    line-height: 0.9;
    letter-spacing: -0.01em;
    margin-bottom: 1.5rem;
  }

  .section-desc {
    font-family: 'Instrument Serif', serif;
    font-style: italic;
    font-size: 1.1rem;
    color: rgba(240,237,230,0.55);
    max-width: 480px;
    line-height: 1.7;
  }

  .tag {
    display: inline-block;
    font-size: 0.6rem; letter-spacing: 0.2em;
    padding: 0.25rem 0.6rem;
    border: 1px solid rgba(200,255,0,0.3);
    color: var(--acid);
    text-transform: uppercase;
    border-radius: 2px;
    margin: 0.2rem;
  }

  /* ── DEMO GRID ── */
  .demo-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
    gap: 1.5px;
    margin-top: 3rem;
    background: rgba(200,255,0,0.08);
  }

  .demo-cell {
    background: var(--ink);
    aspect-ratio: 4/3;
    position: relative;
    overflow: hidden;
    display: flex; align-items: center; justify-content: center;
  }

  .demo-cell canvas {
    position: absolute; inset: 0;
    width: 100%; height: 100%;
  }

  .demo-label {
    position: absolute; bottom: 0; left: 0; right: 0;
    padding: 0.75rem 1rem;
    font-size: 0.6rem; letter-spacing: 0.15em;
    text-transform: uppercase;
    background: linear-gradient(to top, rgba(10,10,15,0.9), transparent);
    color: rgba(240,237,230,0.6);
    z-index: 2;
  }

  /* ── TRANSFORM SHOWCASE ── */
  #transforms {
    background: var(--ink);
    border-top: 1px solid rgba(200,255,0,0.1);
  }

  .transform-stage {
    margin-top: 3rem;
    display: grid;
    grid-template-columns: repeat(6, 1fr);
    gap: 2rem;
    align-items: center;
  }

  .transform-item {
    display: flex; flex-direction: column; align-items: center; gap: 1rem;
  }

  .t-box {
    width: 60px; height: 60px;
    background: var(--acid);
    display: flex; align-items: center; justify-content: center;
    font-size: 0.5rem; letter-spacing: 0.1em;
    color: var(--ink); font-weight: 700;
    text-transform: uppercase;
  }
  .t-label {
    font-size: 0.55rem; letter-spacing: 0.15em;
    text-transform: uppercase;
    color: rgba(240,237,230,0.4);
  }

  .t-translate { animation: demo-translate 2s ease-in-out infinite alternate; }
  .t-scale     { animation: demo-scale 1.5s ease-in-out infinite alternate; }
  .t-rotate    { animation: demo-rotate 3s linear infinite; }
  .t-skew      { animation: demo-skew 2s ease-in-out infinite alternate; }
  .t-flip      { animation: demo-flip 2s ease-in-out infinite; }
  .t-elastic   { animation: demo-elastic 2.5s cubic-bezier(0.34,1.56,0.64,1) infinite alternate; }

  @keyframes demo-translate { to { transform: translateY(-20px); } }
  @keyframes demo-scale     { to { transform: scale(1.4); } }
  @keyframes demo-rotate    { to { transform: rotate(360deg); } }
  @keyframes demo-skew      { to { transform: skewX(-20deg); } }
  @keyframes demo-flip      { 50% { transform: rotateY(180deg); } }
  @keyframes demo-elastic   { to { transform: scale(1.3) rotate(10deg); } }

  /* ── TEXT SECTION ── */
  #text-section { background: #0d0d14; }

  .text-demos {
    margin-top: 3rem;
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 2px;
  }

  .text-demo-cell {
    background: var(--ink);
    padding: 3rem 2rem;
    min-height: 180px;
    display: flex; align-items: center; justify-content: center;
    position: relative; overflow: hidden;
  }
  .text-demo-cell .demo-label { z-index: 2; }

  /* Typewriter */
  .typewriter-text {
    font-family: 'Space Mono', monospace;
    font-size: 1.4rem;
    color: var(--acid);
    overflow: hidden;
    white-space: nowrap;
    border-right: 2px solid var(--acid);
    width: 0;
    animation: typing 3s steps(20) 0.5s infinite alternate, blink 0.7s step-end infinite;
  }
  @keyframes typing { to { width: 100%; } }
  @keyframes blink  { 50% { border-color: transparent; } }

  /* Gradient text */
  .gradient-text-demo {
    font-family: 'Bebas Neue', sans-serif;
    font-size: 3rem;
    background: linear-gradient(90deg, #ff2d55, #ffd700, #00f5ff, #c8ff00, #ff2d55);
    background-size: 300% 100%;
    -webkit-background-clip: text;
    -webkit-text-fill-color: transparent;
    background-clip: text;
    animation: gradMove 3s linear infinite;
  }
  @keyframes gradMove { to { background-position: -300% 0; } }

  /* Neon */
  .neon-text {
    font-family: 'Bebas Neue', sans-serif;
    font-size: 2.5rem;
    color: #fff;
    text-shadow:
      0 0 5px #fff,
      0 0 15px #fff,
      0 0 30px #00f5ff,
      0 0 60px #00f5ff;
    animation: neonFlicker 3s infinite alternate;
  }
  @keyframes neonFlicker {
    0%,18%,22%,25%,53%,57%,100% {
      text-shadow: 0 0 5px #fff, 0 0 15px #fff, 0 0 30px #00f5ff, 0 0 60px #00f5ff;
    }
    20%,24%,55% { text-shadow: none; color: rgba(255,255,255,0.4); }
  }

  /* Wave text */
  .wave-text-container {
    font-family: 'Bebas Neue', sans-serif;
    font-size: 2.5rem;
    display: flex; overflow: hidden;
  }
  .wave-char {
    display: inline-block;
    animation: waveChar 1.5s ease-in-out infinite;
  }
  @keyframes waveChar {
    0%,100% { transform: translateY(0); }
    50%      { transform: translateY(-12px); }
  }

  /* ── PARTICLES SECTION ── */
  #particles-section {
    background: var(--ink);
    border-top: 1px solid rgba(0,245,255,0.1);
  }

  #particle-canvas {
    width: 100%; height: 500px;
    display: block; margin-top: 3rem;
    border: 1px solid rgba(200,255,0,0.08);
  }

  .particle-controls {
    display: flex; gap: 1rem; margin-top: 1.5rem; flex-wrap: wrap;
  }
  .ctrl-btn {
    font-family: 'Space Mono', monospace;
    font-size: 0.6rem; letter-spacing: 0.15em;
    text-transform: uppercase;
    padding: 0.5rem 1.2rem;
    background: transparent;
    border: 1px solid rgba(200,255,0,0.4);
    color: var(--acid);
    cursor: none;
    transition: background 0.2s, color 0.2s;
  }
  .ctrl-btn:hover, .ctrl-btn.active {
    background: var(--acid); color: var(--ink);
  }

  /* ── SVG MORPH SECTION ── */
  #svg-section {
    background: #080810;
    border-top: 1px solid rgba(255,45,85,0.1);
  }

  .svg-demo-grid {
    margin-top: 3rem;
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 2px;
  }

  .svg-cell {
    background: var(--ink);
    aspect-ratio: 1;
    display: flex; align-items: center; justify-content: center;
    position: relative; overflow: hidden;
  }

  /* Blob */
  .blob-svg {
    width: 140px; height: 140px;
    animation: blobPath 6s ease-in-out infinite;
    fill: var(--acid);
    opacity: 0.8;
    filter: blur(0px);
  }

  /* Path draw */
  .path-draw { stroke-dasharray: 400; stroke-dashoffset: 400;
    animation: drawPath 2s ease-in-out infinite alternate; }
  @keyframes drawPath { to { stroke-dashoffset: 0; } }

  /* Clip path morph */
  .clip-morph {
    width: 120px; height: 120px;
    background: var(--electric);
    animation: clipMorph 3s ease-in-out infinite;
  }
  @keyframes clipMorph {
    0%   { clip-path: polygon(50% 0%, 100% 100%, 0% 100%); }
    33%  { clip-path: polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%); }
    66%  { clip-path: circle(50% at 50% 50%); }
    100% { clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%); }
  }

  /* ── SHADER SHOWCASE ── */
  #shaders-section {
    background: var(--ink);
    border-top: 1px solid rgba(255,215,0,0.1);
  }

  #shader-canvas {
    width: 100%; height: 480px;
    display: block; margin-top: 3rem;
    border: 1px solid rgba(255,215,0,0.1);
  }

  .shader-nav {
    display: flex; gap: 1px; margin-top: 1.5rem; flex-wrap: wrap;
  }
  .shader-btn {
    font-family: 'Space Mono', monospace;
    font-size: 0.55rem; letter-spacing: 0.12em; text-transform: uppercase;
    padding: 0.4rem 0.8rem;
    background: rgba(255,215,0,0.05);
    border: 1px solid rgba(255,215,0,0.15);
    color: rgba(255,215,0,0.5); cursor: none;
    transition: all 0.2s;
  }
  .shader-btn:hover, .shader-btn.active {
    background: rgba(255,215,0,0.15);
    color: var(--gold); border-color: var(--gold);
  }

  /* ── PHYSICS SECTION ── */
  #physics-section {
    background: #09090f;
    border-top: 1px solid rgba(0,245,255,0.08);
  }

  #physics-canvas {
    width: 100%; height: 500px;
    display: block; margin-top: 3rem;
    border: 1px solid rgba(0,245,255,0.08);
  }

  /* ── SCROLL SECTION ── */
  #scroll-section {
    background: var(--ink);
    border-top: 1px solid rgba(200,255,0,0.1);
  }

  .scroll-demo-track {
    margin-top: 3rem;
    height: 300px;
    overflow-x: auto;
    overflow-y: hidden;
    display: flex;
    gap: 2px;
    scrollbar-width: thin;
    scrollbar-color: var(--acid) transparent;
  }

  .scroll-card {
    flex: 0 0 300px;
    height: 100%;
    display: flex; flex-direction: column;
    justify-content: flex-end;
    padding: 1.5rem;
    position: relative; overflow: hidden;
  }

  .scroll-card-bg {
    position: absolute; inset: 0;
    background-size: cover; background-position: center;
    transform-origin: center;
    transition: transform 0.6s ease;
  }
  .scroll-card:hover .scroll-card-bg { transform: scale(1.08); }

  .scroll-card-label {
    position: relative; z-index: 1;
    font-family: 'Bebas Neue', sans-serif;
    font-size: 1.6rem;
    color: var(--paper);
    text-shadow: 0 2px 20px rgba(0,0,0,0.8);
  }

  /* ── FINAL MANIFESTO ── */
  #manifesto {
    background: var(--ink);
    min-height: 100vh;
    display: flex; align-items: center; justify-content: center;
    position: relative; overflow: hidden;
    border-top: 1px solid rgba(200,255,0,0.1);
  }

  #manifesto-canvas {
    position: absolute; inset: 0; width: 100%; height: 100%;
  }

  .manifesto-content {
    position: relative; z-index: 2;
    text-align: center; max-width: 900px; padding: 4rem 2rem;
  }

  .manifesto-title {
    font-family: 'Bebas Neue', sans-serif;
    font-size: clamp(3rem, 10vw, 8rem);
    line-height: 0.9;
    letter-spacing: -0.02em;
    color: var(--paper);
    mix-blend-mode: difference;
  }

  .manifesto-title span {
    display: block;
    opacity: 0;
    transform: translateY(40px);
    transition: opacity 0.7s ease, transform 0.7s ease;
  }
  .manifesto-title span.visible {
    opacity: 1; transform: translateY(0);
  }

  .stat-grid {
    display: grid; grid-template-columns: repeat(4, 1fr);
    gap: 2px; margin: 4rem 0;
  }
  .stat-cell {
    background: rgba(10,10,15,0.8);
    padding: 2rem 1rem; text-align: center;
    border: 1px solid rgba(200,255,0,0.08);
    backdrop-filter: blur(10px);
  }
  .stat-num {
    font-family: 'Bebas Neue', sans-serif;
    font-size: 3.5rem; color: var(--acid);
    display: block; line-height: 1;
  }
  .stat-label {
    font-size: 0.55rem; letter-spacing: 0.2em; text-transform: uppercase;
    color: rgba(240,237,230,0.4); margin-top: 0.5rem;
  }

  /* ── DIVIDERS ── */
  .divider {
    height: 1px;
    background: linear-gradient(90deg, transparent, var(--acid), transparent);
    opacity: 0.2;
  }

  /* ── SCROLLBAR ── */
  ::-webkit-scrollbar { width: 2px; }
  ::-webkit-scrollbar-thumb { background: var(--acid); }
  ::-webkit-scrollbar-track { background: transparent; }

  /* ── INTERSECTION ANIMATIONS ── */
  .reveal { opacity: 0; transform: translateY(50px); transition: opacity 0.7s ease, transform 0.7s ease; }
  .reveal.in { opacity: 1; transform: none; }
  .reveal-left { opacity: 0; transform: translateX(-60px); transition: opacity 0.7s ease, transform 0.7s ease; }
  .reveal-left.in { opacity: 1; transform: none; }

  /* ── FLOATING LABEL ── */
  .floating-tags {
    display: flex; flex-wrap: wrap; gap: 0.5rem; margin-top: 2rem;
  }

  /* ── PROGRESS BAR ── */
  .progress-ring {
    position: fixed; top: 0; left: 0; right: 0; height: 2px;
    background: rgba(200,255,0,0.1); z-index: 9997;
  }
  .progress-fill {
    height: 100%; background: var(--acid);
    width: 0%; transition: width 0.1s linear;
  }

  /* ── CHAPTER MARKER ── */
  .chapter-marker {
    position: fixed; right: 2rem; top: 50%;
    transform: translateY(-50%);
    display: flex; flex-direction: column; gap: 0.5rem;
    z-index: 1000;
  }
  .chapter-dot {
    width: 4px; height: 4px; border-radius: 50%;
    background: rgba(200,255,0,0.3);
    transition: all 0.3s; cursor: none;
  }
  .chapter-dot.active { background: var(--acid); transform: scale(2); }

  /* ── STAGGER CHARS ── */
  .stagger-title .char {
    display: inline-block;
    opacity: 0; transform: translateY(60px) rotateX(-90deg);
    transition: opacity 0.5s ease, transform 0.5s ease;
  }
  .stagger-title.in .char {
    opacity: 1; transform: none;
  }

  /* ── FPS COUNTER ── */
  #fps {
    position: fixed; bottom: 1rem; right: 1rem;
    font-size: 0.55rem; color: rgba(200,255,0,0.4);
    letter-spacing: 0.1em; z-index: 9999;
    font-family: 'Space Mono', monospace;
  }

  @media (max-width: 768px) {
    .transform-stage { grid-template-columns: repeat(3, 1fr); }
    .text-demos { grid-template-columns: 1fr; }
    .svg-demo-grid { grid-template-columns: 1fr; }
    .stat-grid { grid-template-columns: repeat(2, 1fr); }
    .chapter-marker { display: none; }
  }
</style>
</head>
<body>

<div id="cursor-dot"></div>
<div id="cursor-ring"></div>
<div id="fps">-- FPS</div>
<div class="progress-ring"><div class="progress-fill" id="progress"></div></div>

<nav class="chapter-marker" id="chapters">
  <div class="chapter-dot active" data-target="hero"></div>
  <div class="chapter-dot" data-target="transforms"></div>
  <div class="chapter-dot" data-target="text-section"></div>
  <div class="chapter-dot" data-target="particles-section"></div>
  <div class="chapter-dot" data-target="svg-section"></div>
  <div class="chapter-dot" data-target="shaders-section"></div>
  <div class="chapter-dot" data-target="physics-section"></div>
  <div class="chapter-dot" data-target="manifesto"></div>
</nav>

<!-- ══════════════════════════════════════════════════
     HERO
══════════════════════════════════════════════════ -->
<section id="hero">
  <canvas id="hero-canvas"></canvas>
  <div class="hero-content">
    <p class="hero-eyebrow">A Living Catalog of</p>
    <h1 class="hero-title">THE<br>MOTION<br>CATALOG</h1>
    <p class="hero-subtitle">173 animation systems demonstrated in realtime</p>
  </div>
  <div class="hero-scroll">
    <span>scroll</span>
    <div class="scroll-line"></div>
  </div>
</section>

<!-- ══════════════════════════════════════════════════
     TRANSFORMS
══════════════════════════════════════════════════ -->
<section class="section" id="transforms">
  <div class="section-number">01 — Basic Motion & Transforms</div>
  <h2 class="section-title stagger-title reveal">THE<br>FUNDAMENTALS</h2>
  <p class="section-desc reveal">Every animation is a composition of primitive transforms. Translate. Scale. Rotate. Skew. Mastered separately, combined into anything.</p>

  <div class="floating-tags reveal">
    <span class="tag">Translate XYZ</span>
    <span class="tag">Scale</span>
    <span class="tag">Rotate</span>
    <span class="tag">Skew</span>
    <span class="tag">Flip</span>
    <span class="tag">Elastic</span>
    <span class="tag">Spring</span>
    <span class="tag">Bounce</span>
    <span class="tag">Stagger</span>
    <span class="tag">Yoyo</span>
    <span class="tag">Path Follow</span>
    <span class="tag">Cubic-Bezier</span>
  </div>

  <div class="transform-stage reveal">
    <div class="transform-item">
      <div class="t-box t-translate">T</div>
      <span class="t-label">Translate</span>
    </div>
    <div class="transform-item">
      <div class="t-box t-scale" style="background:var(--electric); color:var(--ink)">S</div>
      <span class="t-label">Scale</span>
    </div>
    <div class="transform-item">
      <div class="t-box t-rotate" style="background:var(--blood); color:var(--paper)">R</div>
      <span class="t-label">Rotate</span>
    </div>
    <div class="transform-item">
      <div class="t-box t-skew" style="background:var(--gold); color:var(--ink)">SK</div>
      <span class="t-label">Skew</span>
    </div>
    <div class="transform-item">
      <div class="t-box t-flip" style="background:var(--paper); color:var(--ink)">FL</div>
      <span class="t-label">Flip</span>
    </div>
    <div class="transform-item">
      <div class="t-box t-elastic" style="background:linear-gradient(135deg,var(--acid),var(--electric)); color:var(--ink)">EL</div>
      <span class="t-label">Elastic</span>
    </div>
  </div>

  <div class="demo-grid" style="margin-top:2px;">
    <div class="demo-cell"><canvas id="orbit-canvas"></canvas><span class="demo-label">Circular / Orbit</span></div>
    <div class="demo-cell"><canvas id="spiral-canvas"></canvas><span class="demo-label">Spiral Motion</span></div>
    <div class="demo-cell"><canvas id="stagger-canvas"></canvas><span class="demo-label">Staggered Animation</span></div>
    <div class="demo-cell"><canvas id="bounce-canvas"></canvas><span class="demo-label">Physics Bounce</span></div>
  </div>
</section>

<div class="divider"></div>

<!-- ══════════════════════════════════════════════════
     TEXT
══════════════════════════════════════════════════ -->
<section class="section" id="text-section">
  <div class="section-number">02 — Text Motion Graphics</div>
  <h2 class="section-title stagger-title reveal">TYPE<br>IN MOTION</h2>
  <p class="section-desc reveal">Letters are not static. They can breathe, scatter, reform, glow, drip, and race across the page. Every character is a particle waiting to move.</p>

  <div class="floating-tags reveal">
    <span class="tag">Typewriter</span>
    <span class="tag">Letter Reveal</span>
    <span class="tag">Word Stagger</span>
    <span class="tag">Scramble</span>
    <span class="tag">Wave</span>
    <span class="tag">Gradient</span>
    <span class="tag">Neon Flicker</span>
    <span class="tag">Matrix Rain</span>
    <span class="tag">Kinetic</span>
    <span class="tag">SVG Draw</span>
  </div>

  <div class="text-demos reveal">
    <div class="text-demo-cell">
      <div class="typewriter-text">MOTION_GRAPHICS.exe</div>
      <span class="demo-label">Typewriter Effect</span>
    </div>
    <div class="text-demo-cell">
      <div class="gradient-text-demo">GRADIENT</div>
      <span class="demo-label">Animated Gradient Text</span>
    </div>
    <div class="text-demo-cell">
      <div class="neon-text">NEON</div>
      <span class="demo-label">Neon Flicker</span>
    </div>
    <div class="text-demo-cell">
      <div class="wave-text-container" id="wave-text"></div>
      <span class="demo-label">Wave Distortion</span>
    </div>
    <div class="text-demo-cell" style="grid-column: span 2">
      <canvas id="matrix-canvas" style="position:absolute;inset:0;width:100%;height:100%"></canvas>
      <span class="demo-label">Matrix Text Rain</span>
    </div>
    <div class="text-demo-cell">
      <div id="scramble-text" style="font-family:'Space Mono';font-size:1.1rem;color:var(--acid);letter-spacing:0.1em;"></div>
      <span class="demo-label">Text Scramble</span>
    </div>
    <div class="text-demo-cell">
      <canvas id="text-particle-canvas" style="position:absolute;inset:0;width:100%;height:100%"></canvas>
      <span class="demo-label">Text Particle Breakup</span>
    </div>
  </div>
</section>

<div class="divider"></div>

<!-- ══════════════════════════════════════════════════
     PARTICLES
══════════════════════════════════════════════════ -->
<section class="section" id="particles-section">
  <div class="section-number">03 — Particle Systems</div>
  <h2 class="section-title stagger-title reveal">PARTICLES<br>UNLEASHED</h2>
  <p class="section-desc reveal">From fire to galaxies. From rain to flow fields. A particle is a single point of possibility — multiply it by fifty thousand and a universe emerges.</p>

  <div class="floating-tags reveal">
    <span class="tag">Emitter</span>
    <span class="tag">Fire</span>
    <span class="tag">Snow</span>
    <span class="tag">Rain</span>
    <span class="tag">Confetti</span>
    <span class="tag">Starfield</span>
    <span class="tag">Flow Field</span>
    <span class="tag">Mouse Reactive</span>
    <span class="tag">GPU Points</span>
    <span class="tag">Boids</span>
  </div>

  <canvas id="particle-canvas"></canvas>
  <div class="particle-controls reveal">
    <button class="ctrl-btn active clickable" data-mode="flow">Flow Field</button>
    <button class="ctrl-btn clickable" data-mode="fire">Fire</button>
    <button class="ctrl-btn clickable" data-mode="snow">Snow</button>
    <button class="ctrl-btn clickable" data-mode="galaxy">Galaxy</button>
    <button class="ctrl-btn clickable" data-mode="boids">Boids</button>
    <button class="ctrl-btn clickable" data-mode="confetti">Confetti</button>
  </div>
</section>

<div class="divider"></div>

<!-- ══════════════════════════════════════════════════
     SVG / SHAPE
══════════════════════════════════════════════════ -->
<section class="section" id="svg-section">
  <div class="section-number">04 — Shape & SVG Motion</div>
  <h2 class="section-title stagger-title reveal">SHAPE<br>SHIFTING</h2>
  <p class="section-desc reveal">SVG paths are infinitely elastic. A circle can become a star, a letter, a map. Clip-paths reveal worlds. Strokes draw themselves into existence.</p>

  <div class="floating-tags reveal">
    <span class="tag">Path Draw</span>
    <span class="tag">Blob Morph</span>
    <span class="tag">Clip-Path</span>
    <span class="tag">Shape Morph</span>
    <span class="tag">Stroke Dash</span>
    <span class="tag">Lissajous</span>
    <span class="tag">Wave Shape</span>
    <span class="tag">SVG Mask</span>
  </div>

  <div class="svg-demo-grid reveal">
    <div class="svg-cell">
      <svg width="160" height="160" viewBox="0 0 160 160">
        <path id="blob-path" fill="var(--acid)" opacity="0.85">
          <animate attributeName="d"
            values="M80,30 C110,30 130,50 130,80 C130,110 110,130 80,130 C50,130 30,110 30,80 C30,50 50,30 80,30 Z;
                    M80,20 C120,25 140,55 135,80 C130,108 108,138 80,135 C52,132 22,105 25,80 C28,55 40,15 80,20 Z;
                    M80,30 C115,20 145,45 140,80 C135,115 112,140 80,135 C48,130 20,112 22,80 C24,48 45,40 80,30 Z;
                    M80,25 C108,22 132,48 132,80 C132,112 108,135 80,138 C52,141 25,115 25,80 C25,45 52,28 80,25 Z;
                    M80,30 C110,30 130,50 130,80 C130,110 110,130 80,130 C50,130 30,110 30,80 C30,50 50,30 80,30 Z"
            dur="6s" repeatCount="indefinite"/>
        </path>
      </svg>
      <span class="demo-label">Blob Morphing</span>
    </div>
    <div class="svg-cell">
      <svg width="200" height="160" viewBox="0 0 200 160" fill="none">
        <path class="path-draw" d="M20,80 C60,20 140,140 180,80" stroke="var(--electric)" stroke-width="2"/>
        <path class="path-draw" d="M20,100 C70,30 130,150 180,60" stroke="var(--blood)" stroke-width="2" style="animation-delay:0.3s"/>
        <path class="path-draw" d="M20,60 C80,140 120,10 180,100" stroke="var(--gold)" stroke-width="2" style="animation-delay:0.6s"/>
      </svg>
      <span class="demo-label">SVG Path Draw</span>
    </div>
    <div class="svg-cell">
      <div class="clip-morph"></div>
      <span class="demo-label">Clip-Path Morphing</span>
    </div>
    <div class="svg-cell">
      <canvas id="lissajous-canvas" width="300" height="240" style="width:100%;height:100%;position:absolute;inset:0"></canvas>
      <span class="demo-label">Lissajous Curves</span>
    </div>
    <div class="svg-cell">
      <canvas id="wave-canvas" width="300" height="240" style="width:100%;height:100%;position:absolute;inset:0"></canvas>
      <span class="demo-label">Wave Shapes</span>
    </div>
    <div class="svg-cell">
      <svg width="180" height="180" viewBox="0 0 180 180">
        <g transform="translate(90,90)">
          <circle r="60" fill="none" stroke="rgba(200,255,0,0.15)" stroke-width="1"/>
          <circle r="40" fill="none" stroke="rgba(0,245,255,0.2)" stroke-width="1"/>
          <circle r="20" fill="none" stroke="rgba(255,45,85,0.3)" stroke-width="1"/>
          <circle id="orbit1" r="5" fill="var(--acid)"><animateTransform attributeName="transform" type="rotate" from="0" to="360" dur="3s" repeatCount="indefinite"/><animateMotion dur="3s" repeatCount="indefinite"><mpath href="#orbit-path1"/></animateMotion></circle>
          <circle id="orbit2" r="4" fill="var(--electric)"><animateMotion dur="5s" repeatCount="indefinite" rotate="auto"><mpath href="#orbit-path2"/></animateMotion></circle>
          <circle id="orbit3" r="3" fill="var(--blood)"><animateMotion dur="2s" repeatCount="indefinite"><mpath href="#orbit-path3"/></animateMotion></circle>
          <path id="orbit-path1" d="M60,0 A60,60 0 1,1 59.99,0.01" fill="none"/>
          <path id="orbit-path2" d="M40,0 A40,40 0 1,1 39.99,0.01" fill="none"/>
          <path id="orbit-path3" d="M20,0 A20,20 0 1,1 19.99,0.01" fill="none"/>
        </g>
      </svg>
      <span class="demo-label">Orbital Motion</span>
    </div>
  </div>
</section>

<div class="divider"></div>

<!-- ══════════════════════════════════════════════════
     SHADERS
══════════════════════════════════════════════════ -->
<section class="section" id="shaders-section">
  <div class="section-number">05 — Shader & GPU Effects</div>
  <h2 class="section-title stagger-title reveal">RAW<br>GPU POWER</h2>
  <p class="section-desc reveal">GLSL shaders execute millions of pixels simultaneously. Plasma, glitch, noise displacement, chromatic aberration, fractals — GPU-rendered in realtime.</p>

  <div class="floating-tags reveal">
    <span class="tag">Plasma</span>
    <span class="tag">Perlin Noise</span>
    <span class="tag">Glitch</span>
    <span class="tag">Chromatic Aberration</span>
    <span class="tag">Water Ripple</span>
    <span class="tag">VHS</span>
    <span class="tag">Fractal</span>
    <span class="tag">Kaleidoscope</span>
    <span class="tag">Color Grade</span>
    <span class="tag">Bloom</span>
  </div>

  <canvas id="shader-canvas"></canvas>
  <div class="shader-nav reveal">
    <button class="shader-btn active clickable" data-shader="plasma">Plasma</button>
    <button class="shader-btn clickable" data-shader="noise">Noise</button>
    <button class="shader-btn clickable" data-shader="glitch">Glitch</button>
    <button class="shader-btn clickable" data-shader="ripple">Ripple</button>
    <button class="shader-btn clickable" data-shader="fractal">Fractal</button>
    <button class="shader-btn clickable" data-shader="vhs">VHS</button>
    <button class="shader-btn clickable" data-shader="kaleidoscope">Kaleidoscope</button>
    <button class="shader-btn clickable" data-shader="godray">God Rays</button>
  </div>
</section>

<div class="divider"></div>

<!-- ══════════════════════════════════════════════════
     PHYSICS
══════════════════════════════════════════════════ -->
<section class="section" id="physics-section">
  <div class="section-number">06 — Physics-Based Animation</div>
  <h2 class="section-title stagger-title reveal">LAWS OF<br>MOTION</h2>
  <p class="section-desc reveal">Gravity. Friction. Collision. Springs. The browser becomes a physics engine. Verlet integration, constraint solving, and emergence from simple rules.</p>

  <div class="floating-tags reveal">
    <span class="tag">Gravity</span>
    <span class="tag">Collision</span>
    <span class="tag">Spring</span>
    <span class="tag">Cloth</span>
    <span class="tag">Rope / Verlet</span>
    <span class="tag">Boids</span>
    <span class="tag">Magnetic</span>
    <span class="tag">Friction</span>
  </div>

  <canvas id="physics-canvas"></canvas>
</section>

<div class="divider"></div>

<!-- ══════════════════════════════════════════════════
     SCROLL DEMO
══════════════════════════════════════════════════ -->
<section class="section" id="scroll-section">
  <div class="section-number">07 — Scroll-Driven Motion</div>
  <h2 class="section-title stagger-title reveal">SCROLL<br>AS INPUT</h2>
  <p class="section-desc reveal">Scroll position becomes a timeline scrubber. The user's hand controls the animation. Parallax, pinning, horizontal tracks, and 3D camera motion.</p>

  <div class="floating-tags reveal">
    <span class="tag">ScrollTrigger</span>
    <span class="tag">Scrub</span>
    <span class="tag">Parallax</span>
    <span class="tag">Pinning</span>
    <span class="tag">Horizontal Track</span>
    <span class="tag">Ken Burns</span>
    <span class="tag">Reveal</span>
  </div>

  <div class="scroll-demo-track reveal" id="scroll-track">
    <div class="scroll-card">
      <div class="scroll-card-bg" style="background: linear-gradient(135deg, #0a0a0f 0%, #1a1a3e 100%);"></div>
      <div class="scroll-card-label">Parallax BG</div>
    </div>
    <div class="scroll-card">
      <div class="scroll-card-bg" style="background: linear-gradient(135deg, #0a0a0f 0%, #1e0a20 100%);"></div>
      <div class="scroll-card-label">Section Pin</div>
    </div>
    <div class="scroll-card">
      <div class="scroll-card-bg" style="background: linear-gradient(135deg, #001a14 0%, #0a1a2e 100%);"></div>
      <div class="scroll-card-label">Scrub Timeline</div>
    </div>
    <div class="scroll-card">
      <div class="scroll-card-bg" style="background: linear-gradient(135deg, #1a0a0a 0%, #2e1a00 100%);"></div>
      <div class="scroll-card-label">Horizontal Scroll</div>
    </div>
    <div class="scroll-card">
      <div class="scroll-card-bg" style="background: linear-gradient(135deg, #0a1a0a 0%, #1a3e10 100%);"></div>
      <div class="scroll-card-label">Depth Parallax</div>
    </div>
    <div class="scroll-card">
      <div class="scroll-card-bg" style="background: linear-gradient(135deg, #1a1a00 0%, #3e2a00 100%);"></div>
      <div class="scroll-card-label">3D Camera</div>
    </div>
  </div>
  <!-- Auto-scroll the track -->
</section>

<div class="divider"></div>

<!-- ══════════════════════════════════════════════════
     MANIFESTO / FINALE
══════════════════════════════════════════════════ -->
<section id="manifesto">
  <canvas id="manifesto-canvas"></canvas>
  <div class="manifesto-content">
    <div class="stat-grid">
      <div class="stat-cell reveal">
        <span class="stat-num" data-count="173">0</span>
        <span class="stat-label">Animation Systems</span>
      </div>
      <div class="stat-cell reveal" style="transition-delay:0.1s">
        <span class="stat-num" data-count="17">0</span>
        <span class="stat-label">Categories</span>
      </div>
      <div class="stat-cell reveal" style="transition-delay:0.2s">
        <span class="stat-num" data-count="95">0</span>
        <span class="stat-label">% AE Parity</span>
      </div>
      <div class="stat-cell reveal" style="transition-delay:0.3s">
        <span class="stat-num" data-count="0">∞</span>
        <span class="stat-label">Possibilities</span>
      </div>
    </div>

    <div class="manifesto-title">
      <span id="m1">THE WEB</span>
      <span id="m2">IS YOUR</span>
      <span id="m3">CANVAS</span>
    </div>

    <p style="font-family:'Instrument Serif',serif;font-style:italic;font-size:1.1rem;color:rgba(240,237,230,0.45);margin-top:2rem;line-height:1.8;">
      CSS. Canvas. WebGL. GLSL. The full motion stack lives in the browser.<br>
      Every effect in this catalog runs at 60fps, no plugin required.
    </p>

    <div class="floating-tags" style="justify-content:center;margin-top:2rem;">
      <span class="tag">CSS Transforms</span>
      <span class="tag">GSAP</span>
      <span class="tag">Canvas 2D</span>
      <span class="tag">SVG</span>
      <span class="tag">WebGL</span>
      <span class="tag">GLSL Shaders</span>
      <span class="tag">Three.js</span>
      <span class="tag">PHP</span>
      <span class="tag">Verlet Physics</span>
      <span class="tag">GPU Particles</span>
    </div>
  </div>
</section>

<script>
// ════════════════════════════════════════════════
//  UTILITIES
// ════════════════════════════════════════════════

const $ = id => document.getElementById(id);
const qs = sel => document.querySelector(sel);
const qsa = sel => [...document.querySelectorAll(sel)];

let mouseX = window.innerWidth / 2, mouseY = window.innerHeight / 2;
let cursorX = mouseX, cursorY = mouseY;
let ringX = mouseX, ringY = mouseY;

// FPS
let fps = 0, frameCount = 0, lastFPSTime = performance.now();
function trackFPS() {
  frameCount++;
  const now = performance.now();
  if (now - lastFPSTime >= 500) {
    fps = Math.round(frameCount / ((now - lastFPSTime) / 1000));
    $('fps').textContent = fps + ' FPS';
    frameCount = 0; lastFPSTime = now;
  }
}

// Cursor
document.addEventListener('mousemove', e => { mouseX = e.clientX; mouseY = e.clientY; });
const cursorDot = $('cursor-dot');
const cursorRing = $('cursor-ring');
function animateCursor() {
  cursorX += (mouseX - cursorX) * 0.25;
  cursorY += (mouseY - cursorY) * 0.25;
  ringX += (mouseX - ringX) * 0.1;
  ringY += (mouseY - ringY) * 0.1;
  cursorDot.style.left = cursorX + 'px';
  cursorDot.style.top  = cursorY + 'px';
  cursorRing.style.left = ringX + 'px';
  cursorRing.style.top  = ringY + 'px';
  requestAnimationFrame(animateCursor);
}
animateCursor();

// Progress bar
window.addEventListener('scroll', () => {
  const pct = window.scrollY / (document.body.scrollHeight - window.innerHeight) * 100;
  $('progress').style.width = pct + '%';
});

// Chapter dots
const sections = ['hero','transforms','text-section','particles-section','svg-section','shaders-section','physics-section','manifesto'];
const dots = qsa('.chapter-dot');
dots.forEach((d, i) => {
  d.addEventListener('click', () => {
    document.getElementById(sections[i]).scrollIntoView({ behavior: 'smooth' });
  });
});
const sectObserver = new IntersectionObserver(entries => {
  entries.forEach(e => {
    if (e.isIntersecting) {
      const idx = sections.indexOf(e.target.id);
      if (idx >= 0) { dots.forEach(d => d.classList.remove('active')); dots[idx]?.classList.add('active'); }
    }
  });
}, { threshold: 0.4 });
sections.forEach(id => { const el = document.getElementById(id); if (el) sectObserver.observe(el); });

// Reveal on scroll
const revObserver = new IntersectionObserver(entries => {
  entries.forEach(e => { if (e.isIntersecting) e.target.classList.add('in'); });
}, { threshold: 0.1 });
qsa('.reveal, .reveal-left, .stagger-title').forEach(el => {
  // Stagger chars
  if (el.classList.contains('stagger-title')) {
    el.innerHTML = el.innerHTML.replace(/([A-Z]|<br>)/g, m => m === '<br>' ? '<br>' : `<span class="char">${m}</span>`);
    qsa('.char', el).forEach((c, i) => c.style.transitionDelay = (i * 0.04) + 's');
  }
  revObserver.observe(el);
});

// Stat counters
const statObserver = new IntersectionObserver(entries => {
  entries.forEach(e => {
    if (e.isIntersecting) {
      qsa('.stat-num').forEach(el => {
        const target = parseInt(el.dataset.count);
        if (isNaN(target)) return;
        let val = 0;
        const step = target / 60;
        const timer = setInterval(() => {
          val = Math.min(val + step, target);
          el.textContent = Math.round(val);
          if (val >= target) clearInterval(timer);
        }, 16);
      });
    }
  });
}, { threshold: 0.5 });
const statGrid = qs('.stat-grid');
if (statGrid) statObserver.observe(statGrid);

// Manifesto text reveal
const manifObserver = new IntersectionObserver(entries => {
  entries.forEach(e => {
    if (e.isIntersecting) {
      ['m1','m2','m3'].forEach((id, i) => {
        setTimeout(() => document.getElementById(id)?.classList.add('visible'), i * 200);
      });
    }
  });
}, { threshold: 0.3 });
const manifTitle = qs('.manifesto-title');
if (manifTitle) manifObserver.observe(manifTitle);

// Auto-scroll the scroll track
const track = $('scroll-track');
if (track) {
  let tOffset = 0;
  setInterval(() => {
    tOffset += 1;
    if (tOffset > track.scrollWidth - track.clientWidth) tOffset = 0;
    track.scrollLeft = tOffset;
  }, 16);
}

// ════════════════════════════════════════════════
//  PERLIN NOISE (inline simplex approx)
// ════════════════════════════════════════════════
const perm = new Uint8Array(512);
const p = new Uint8Array(256);
for (let i = 0; i < 256; i++) p[i] = i;
for (let i = 255; i > 0; i--) { const j = Math.floor(Math.random() * (i+1)); [p[i],p[j]]=[p[j],p[i]]; }
for (let i = 0; i < 512; i++) perm[i] = p[i & 255];
function fade(t) { return t*t*t*(t*(t*6-15)+10); }
function lerp(a,b,t) { return a + t*(b-a); }
function grad(h, x, y) { const u=h<8?x:y, v=h<4?y:h===12||h===14?x:0; return ((h&1)?-u:u)+((h&2)?-v:v); }
function perlin2(x, y) {
  const X=Math.floor(x)&255, Y=Math.floor(y)&255;
  x-=Math.floor(x); y-=Math.floor(y);
  const u=fade(x), v=fade(y);
  const a=perm[X]+Y, aa=perm[a], ab=perm[a+1], b=perm[X+1]+Y, ba=perm[b], bb=perm[b+1];
  return lerp(lerp(grad(perm[aa],x,y),grad(perm[ba],x-1,y),u),
              lerp(grad(perm[ab],x,y-1),grad(perm[bb],x-1,y-1),u),v);
}
function perlin3(x,y,z) {
  const X=Math.floor(x)&255, Y=Math.floor(y)&255, Z=Math.floor(z)&255;
  x-=Math.floor(x); y-=Math.floor(y); z-=Math.floor(z);
  const u=fade(x),v=fade(y),w=fade(z);
  const A=perm[X]+Y,AA=perm[A]+Z,AB=perm[A+1]+Z,B=perm[X+1]+Y,BA=perm[B]+Z,BB=perm[B+1]+Z;
  return lerp(lerp(lerp(grad(perm[AA],x,y,z),grad(perm[BA],x-1,y,z),u),
              lerp(grad(perm[AB],x,y-1,z),grad(perm[BB],x-1,y-1,z),u),v),
              lerp(lerp(grad(perm[AA+1],x,y,z-1),grad(perm[BA+1],x-1,y,z-1),u),
              lerp(grad(perm[AB+1],x,y-1,z-1),grad(perm[BB+1],x-1,y-1,z-1),u),v),w);
}
function grad(h,x,y,z=0){const u=h<8?x:y,v=h<4?y:h===12||h===14?x:z;return((h&1)?-u:u)+((h&2)?-v:v);}

// ════════════════════════════════════════════════
//  HERO CANVAS — flowing particle field
// ════════════════════════════════════════════════
(function() {
  const canvas = $('hero-canvas');
  const ctx = canvas.getContext('2d');
  let W, H, particles = [], time = 0;

  function resize() {
    W = canvas.width  = canvas.offsetWidth;
    H = canvas.height = canvas.offsetHeight;
  }
  resize();
  window.addEventListener('resize', resize);

  function makeParticle() {
    return {
      x: Math.random() * W,
      y: Math.random() * H,
      size: Math.random() * 1.5 + 0.3,
      hue: Math.random() * 60 + 160,
      life: Math.random(),
      speed: Math.random() * 0.8 + 0.3
    };
  }
  for (let i = 0; i < 600; i++) particles.push(makeParticle());

  function loop() {
    ctx.fillStyle = 'rgba(10,10,15,0.15)';
    ctx.fillRect(0, 0, W, H);
    time += 0.001;

    particles.forEach(p => {
      const angle = perlin3(p.x * 0.002, p.y * 0.002, time) * Math.PI * 4;
      p.x += Math.cos(angle) * p.speed;
      p.y += Math.sin(angle) * p.speed;

      // Edge proximity fade
      const edgeDist = Math.min(p.x, p.y, W - p.x, H - p.y) / 80;
      const alpha = Math.min(edgeDist, 0.7);

      ctx.beginPath();
      ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
      ctx.fillStyle = `hsla(${p.hue},100%,70%,${alpha})`;
      ctx.fill();

      if (p.x < 0 || p.x > W || p.y < 0 || p.y > H) {
        p.x = Math.random() * W;
        p.y = Math.random() * H;
      }
    });

    trackFPS();
    requestAnimationFrame(loop);
  }
  loop();
})();

// ════════════════════════════════════════════════
//  ORBIT CANVAS
// ════════════════════════════════════════════════
(function() {
  const c = $('orbit-canvas');
  if (!c) return;
  const ctx = c.getContext('2d');
  let a = 0;
  function resize() { c.width = c.offsetWidth; c.height = c.offsetHeight; }
  resize();
  const orbits = [
    { r: 0.35, speed: 0.025, size: 5, color: '#c8ff00' },
    { r: 0.25, speed: -0.04, size: 4, color: '#00f5ff' },
    { r: 0.15, speed: 0.07,  size: 3, color: '#ff2d55' }
  ];
  function loop() {
    const W = c.width, H = c.height, cx = W/2, cy = H/2;
    ctx.clearRect(0,0,W,H);
    a += 0.01;
    orbits.forEach((o, i) => {
      const rad = Math.min(W,H) * o.r;
      ctx.beginPath();
      ctx.arc(cx, cy, rad, 0, Math.PI*2);
      ctx.strokeStyle = o.color + '22'; ctx.lineWidth = 1; ctx.stroke();
      const ox = cx + rad * Math.cos(a * o.speed / 0.01 + i);
      const oy = cy + rad * Math.sin(a * o.speed / 0.01 + i);
      ctx.beginPath(); ctx.arc(ox, oy, o.size, 0, Math.PI*2);
      ctx.fillStyle = o.color; ctx.fill();
      ctx.beginPath(); ctx.arc(cx, cy, 4, 0, Math.PI*2);
      ctx.fillStyle = 'rgba(240,237,230,0.3)'; ctx.fill();
    });
    requestAnimationFrame(loop);
  }
  loop();
})();

// ════════════════════════════════════════════════
//  SPIRAL CANVAS
// ════════════════════════════════════════════════
(function() {
  const c = $('spiral-canvas');
  if (!c) return;
  const ctx = c.getContext('2d');
  function resize() { c.width = c.offsetWidth; c.height = c.offsetHeight; }
  resize();
  let t = 0, points = [];
  function loop() {
    const W = c.width, H = c.height, cx = W/2, cy = H/2;
    ctx.fillStyle = 'rgba(10,10,15,0.08)';
    ctx.fillRect(0,0,W,H);
    t += 0.03;
    const r = (t % (Math.PI * 6)) * 5;
    const x = cx + r * Math.cos(t * 1.5);
    const y = cy + r * Math.sin(t * 1.5);
    points.push({x,y,age:1});
    if (points.length > 300) points.shift();
    points.forEach((p,i) => {
      p.age -= 0.003;
      ctx.beginPath(); ctx.arc(p.x, p.y, 2, 0, Math.PI*2);
      const hue = (i / points.length) * 120 + 160;
      ctx.fillStyle = `hsla(${hue},100%,70%,${p.age})`;
      ctx.fill();
    });
    if (t > Math.PI * 6) { t = 0; points = []; }
    requestAnimationFrame(loop);
  }
  loop();
})();

// ════════════════════════════════════════════════
//  STAGGER CANVAS
// ════════════════════════════════════════════════
(function() {
  const c = $('stagger-canvas');
  if (!c) return;
  const ctx = c.getContext('2d');
  function resize() { c.width = c.offsetWidth; c.height = c.offsetHeight; }
  resize();
  let phase = 0, phaseT = 0;
  const COLS = 6, ROWS = 4;
  function loop() {
    const W = c.width, H = c.height;
    ctx.clearRect(0,0,W,H);
    phaseT += 0.01;
    const spacing_x = W / (COLS + 1), spacing_y = H / (ROWS + 1);
    for (let r = 0; r < ROWS; r++) {
      for (let col = 0; col < COLS; col++) {
        const delay = (r * COLS + col) * 0.12;
        const t = Math.sin(phaseT - delay);
        const y = spacing_y * (r+1) + t * 15;
        const scale = 0.7 + (t * 0.5 + 0.5) * 0.5;
        const hue = (r * COLS + col) * 20 + 150;
        ctx.beginPath();
        ctx.arc(spacing_x * (col+1), y, 6 * scale, 0, Math.PI*2);
        ctx.fillStyle = `hsl(${hue},100%,70%)`;
        ctx.fill();
      }
    }
    requestAnimationFrame(loop);
  }
  loop();
})();

// ════════════════════════════════════════════════
//  BOUNCE CANVAS
// ════════════════════════════════════════════════
(function() {
  const c = $('bounce-canvas');
  if (!c) return;
  const ctx = c.getContext('2d');
  function resize() { c.width = c.offsetWidth; c.height = c.offsetHeight; }
  resize();
  const balls = Array.from({length:5},(_,i) => ({
    x: (i+1) * (c.offsetWidth/6),
    y: 60, vy: 0, vx: (Math.random()-0.5)*2,
    r: 10 + i*3, phase: i * 0.4,
    color: ['#c8ff00','#00f5ff','#ff2d55','#ffd700','#ff9ff3'][i]
  }));
  function loop() {
    const W = c.width, H = c.height, floor = H - 20;
    ctx.clearRect(0,0,W,H);
    ctx.fillStyle='rgba(200,255,0,0.08)';
    ctx.fillRect(0, floor, W, 2);
    balls.forEach(b => {
      b.vy += 0.5;
      b.x += b.vx; b.y += b.vy;
      if (b.y + b.r > floor) { b.y = floor - b.r; b.vy *= -0.75; b.vx *= 0.97; }
      if (b.x < b.r || b.x > W-b.r) { b.vx *= -1; }
      if (Math.abs(b.vy) < 0.3 && Math.abs(b.y + b.r - floor) < 2) {
        b.y = 60; b.vy = 0;
      }
      ctx.beginPath(); ctx.arc(b.x, b.y, b.r, 0, Math.PI*2);
      ctx.fillStyle = b.color + 'cc'; ctx.fill();
      ctx.beginPath(); ctx.ellipse(b.x, floor+4, b.r*0.8, 3, 0, 0, Math.PI*2);
      ctx.fillStyle = b.color + '44'; ctx.fill();
    });
    requestAnimationFrame(loop);
  }
  loop();
})();

// ════════════════════════════════════════════════
//  WAVE TEXT
// ════════════════════════════════════════════════
(function() {
  const el = $('wave-text');
  if (!el) return;
  const text = 'WAVE  MOTION';
  el.innerHTML = [...text].map(c => `<span style="display:inline-block">${c === ' '?'&nbsp;':c}</span>`).join('');
  const chars = qsa('span', el);
  let t = 0;
  function loop() {
    t += 0.04;
    chars.forEach((c,i) => {
      const y = Math.sin(t + i * 0.5) * 10;
      c.style.transform = `translateY(${y}px)`;
    });
    requestAnimationFrame(loop);
  }
  loop();
})();

// ════════════════════════════════════════════════
//  MATRIX CANVAS
// ════════════════════════════════════════════════
(function() {
  const c = $('matrix-canvas');
  if (!c) return;
  const ctx = c.getContext('2d');
  function resize() { c.width = c.offsetWidth; c.height = c.offsetHeight; }
  resize();
  const cols = Math.floor(c.offsetWidth / 18);
  const drops = Array(cols).fill(1);
  function loop() {
    const W = c.width, H = c.height;
    ctx.fillStyle = 'rgba(10,10,15,0.06)';
    ctx.fillRect(0,0,W,H);
    ctx.fillStyle = '#c8ff00';
    ctx.font = '13px Space Mono, monospace';
    drops.forEach((y,i) => {
      const char = String.fromCharCode(0x30a0 + Math.random() * 96);
      ctx.fillStyle = `rgba(200,255,0,${Math.random()*0.5+0.5})`;
      ctx.fillText(char, i * 18, y * 18);
      if (y * 18 > H && Math.random() > 0.975) drops[i] = 0;
      drops[i]++;
    });
    requestAnimationFrame(loop);
  }
  loop();
})();

// ════════════════════════════════════════════════
//  TEXT SCRAMBLE
// ════════════════════════════════════════════════
(function() {
  const el = $('scramble-text');
  if (!el) return;
  const chars = '!@#$%^&*<>{}[]|/\\+=?~';
  const words = ['TRANSFORM','PARTICLE','PHYSICS','SHADER','ANIMATE','MOTION'];
  let wi = 0, frame = 0, queue = [];

  function setText(word) {
    queue = [...word].map((to,i) => ({
      to, from: chars[Math.floor(Math.random()*chars.length)],
      start: i*2, end: i*2+8
    }));
    frame = 0;
  }

  function update() {
    let out = '', done = 0;
    queue.forEach(q => {
      if (frame >= q.end) { out += q.to; done++; }
      else if (frame >= q.start) out += `<span style="color:rgba(200,255,0,0.4)">${chars[Math.floor(Math.random()*chars.length)]}</span>`;
      else out += q.from;
    });
    el.innerHTML = out;
    frame++;
    if (done < queue.length) requestAnimationFrame(update);
    else setTimeout(() => { wi = (wi+1)%words.length; setText(words[wi]); }, 1200);
  }

  setText(words[0]);
  requestAnimationFrame(update);
})();

// ════════════════════════════════════════════════
//  TEXT PARTICLE CANVAS
// ════════════════════════════════════════════════
(function() {
  const c = $('text-particle-canvas');
  if (!c) return;
  const ctx = c.getContext('2d');
  function resize() { c.width = c.offsetWidth; c.height = c.offsetHeight; }
  resize();

  let particles = [], mode = 'gather', modeT = 0;

  function buildParticles() {
    const off = document.createElement('canvas');
    off.width = c.width; off.height = c.height;
    const octx = off.getContext('2d');
    octx.fillStyle = '#fff';
    octx.font = `bold ${Math.floor(c.width * 0.28)}px Bebas Neue, sans-serif`;
    octx.textAlign = 'center';
    octx.textBaseline = 'middle';
    octx.fillText('TYPE', c.width/2, c.height/2);
    const data = octx.getImageData(0,0,c.width,c.height).data;
    particles = [];
    const step = 5;
    for (let y = 0; y < c.height; y += step) {
      for (let x = 0; x < c.width; x += step) {
        if (data[(y*c.width+x)*4+3] > 128) {
          particles.push({
            ox: x, oy: y,
            x: Math.random()*c.width,
            y: Math.random()*c.height,
            vx: 0, vy: 0,
            color: `hsl(${Math.random()*60+150},100%,70%)`
          });
        }
      }
    }
  }
  buildParticles();

  let t = 0;
  function loop() {
    const W = c.width, H = c.height;
    ctx.fillStyle = 'rgba(10,10,15,0.3)';
    ctx.fillRect(0,0,W,H);
    t += 0.01;
    modeT++;

    if (modeT > 180) { mode = mode === 'gather' ? 'scatter' : 'gather'; modeT = 0; }

    particles.forEach(p => {
      if (mode === 'gather') {
        p.vx += (p.ox - p.x) * 0.05;
        p.vy += (p.oy - p.y) * 0.05;
      } else {
        p.vx += (Math.random()-0.5) * 2;
        p.vy += (Math.random()-0.5) * 2;
      }
      p.vx *= 0.88; p.vy *= 0.88;
      p.x += p.vx; p.y += p.vy;
      ctx.beginPath(); ctx.arc(p.x, p.y, 1.5, 0, Math.PI*2);
      ctx.fillStyle = p.color; ctx.fill();
    });
    requestAnimationFrame(loop);
  }
  loop();
})();

// ════════════════════════════════════════════════
//  LISSAJOUS CANVAS
// ════════════════════════════════════════════════
(function() {
  const c = $('lissajous-canvas');
  if (!c) return;
  const ctx = c.getContext('2d');
  let delta = 0;
  function loop() {
    const W = c.offsetWidth, H = c.offsetHeight;
    c.width = W; c.height = H;
    ctx.clearRect(0,0,W,H);
    delta += 0.008;
    const A = W*0.38, B = H*0.38, cx = W/2, cy = H/2;
    const combos = [[3,2,'#c8ff00'],[2,3,'#00f5ff'],[5,4,'#ff2d55']];
    combos.forEach(([a,b,col], ci) => {
      ctx.beginPath();
      for (let i = 0; i <= Math.PI*2; i += 0.02) {
        const x = cx + A * Math.sin(a * i + delta + ci*0.5);
        const y = cy + B * Math.sin(b * i);
        i === 0 ? ctx.moveTo(x,y) : ctx.lineTo(x,y);
      }
      ctx.strokeStyle = col + '99'; ctx.lineWidth = 1.5; ctx.stroke();
    });
    requestAnimationFrame(loop);
  }
  loop();
})();

// ════════════════════════════════════════════════
//  WAVE SHAPE CANVAS
// ════════════════════════════════════════════════
(function() {
  const c = $('wave-canvas');
  if (!c) return;
  const ctx = c.getContext('2d');
  let t = 0;
  function loop() {
    const W = c.offsetWidth, H = c.offsetHeight;
    c.width = W; c.height = H;
    ctx.clearRect(0,0,W,H);
    t += 0.02;
    const waves = [
      { amp: 22, freq: 0.015, speed: 1,   color: 'rgba(200,255,0,0.35)',  y: H*0.45 },
      { amp: 16, freq: 0.025, speed: 1.5, color: 'rgba(0,245,255,0.25)', y: H*0.52 },
      { amp: 28, freq: 0.01,  speed: 0.7, color: 'rgba(255,45,85,0.2)',  y: H*0.50 }
    ];
    waves.forEach(w => {
      ctx.beginPath();
      ctx.moveTo(0, H);
      for (let x = 0; x <= W; x++) {
        const y = w.y + Math.sin((x * w.freq + t * w.speed)) * w.amp;
        ctx.lineTo(x, y);
      }
      ctx.lineTo(W, H); ctx.closePath();
      ctx.fillStyle = w.color; ctx.fill();
    });
    requestAnimationFrame(loop);
  }
  loop();
})();

// ════════════════════════════════════════════════
//  PARTICLE SYSTEM — multi-mode
// ════════════════════════════════════════════════
(function() {
  const c = $('particle-canvas');
  if (!c) return;
  const ctx = c.getContext('2d');
  let W, H, particles = [], time = 0, mode = 'flow';

  function resize() {
    W = c.width = c.offsetWidth;
    H = c.height = Math.max(c.offsetHeight, 400);
  }
  resize();
  window.addEventListener('resize', () => { resize(); initMode(mode); });

  let pmouseX = W/2, pmouseY = H/2;
  c.addEventListener('mousemove', e => {
    const r = c.getBoundingClientRect();
    pmouseX = e.clientX - r.left;
    pmouseY = e.clientY - r.top;
  });

  function initMode(m) {
    mode = m; particles = []; time = 0;
    if (m === 'flow') {
      for (let i = 0; i < 1200; i++) particles.push({
        x: Math.random()*W, y: Math.random()*H, vx:0, vy:0,
        speed: Math.random()*1.5+0.5, hue: Math.random()*60+160
      });
    } else if (m === 'fire') {
      for (let i = 0; i < 400; i++) particles.push(mkFire());
    } else if (m === 'snow') {
      for (let i = 0; i < 500; i++) particles.push(mkSnow(true));
    } else if (m === 'galaxy') {
      for (let i = 0; i < 2000; i++) particles.push(mkStar());
    } else if (m === 'boids') {
      for (let i = 0; i < 200; i++) particles.push({
        x: Math.random()*W, y: Math.random()*H,
        vx:(Math.random()-0.5)*3, vy:(Math.random()-0.5)*3,
        hue: Math.random()*60+160
      });
    } else if (m === 'confetti') {
      for (let i = 0; i < 300; i++) particles.push(mkConfetti());
    }
  }

  function mkFire() {
    return { x: W/2+(Math.random()-0.5)*80, y: H-10,
      vx:(Math.random()-0.5)*1.5, vy:-(Math.random()*3+1),
      life:1, size:Math.random()*8+2, decay:0.015+Math.random()*0.01 };
  }
  function mkSnow(rand=false) {
    return { x:Math.random()*W, y:rand?Math.random()*H:-10,
      size:Math.random()*3+1, speed:Math.random()*1.5+0.5, drift:(Math.random()-0.5)*0.3 };
  }
  function mkStar() {
    const arm = Math.floor(Math.random()*3) * (Math.PI*2/3);
    const r = Math.pow(Math.random(),0.5) * Math.min(W,H)*0.42;
    const spin = r * 1.5;
    const angle = arm + spin*0.1 + (Math.random()-0.5)*0.6;
    return {
      x: W/2 + r*Math.cos(angle), y: H/2 + r*Math.sin(angle),
      size: Math.random()*1.5+0.2, hue: r < Math.min(W,H)*0.1 ? 50 : 200+Math.random()*60,
      twinkle: Math.random()*Math.PI*2
    };
  }
  function mkConfetti() {
    const colors = ['#c8ff00','#00f5ff','#ff2d55','#ffd700','#ff9ff3','#54a0ff'];
    return { x:Math.random()*W, y:-20, vy:Math.random()*3+2, vx:(Math.random()-0.5)*2,
      rotation:Math.random()*Math.PI*2, rotSpeed:(Math.random()-0.5)*0.2,
      color:colors[Math.floor(Math.random()*colors.length)],
      w:Math.random()*10+4, h:Math.random()*5+2 };
  }

  // Boids helpers
  function boidSep(b, all) {
    let sx=0,sy=0,cnt=0;
    all.forEach(o=>{if(o===b)return;const dx=b.x-o.x,dy=b.y-o.y,d=Math.sqrt(dx*dx+dy*dy);if(d<30&&d>0){sx+=dx/d;sy+=dy/d;cnt++;}});
    return cnt?{x:sx/cnt,y:sy/cnt}:{x:0,y:0};
  }
  function boidAlign(b, all) {
    let ax=0,ay=0,cnt=0;
    all.forEach(o=>{if(o===b)return;const dx=b.x-o.x,dy=b.y-o.y;if(Math.sqrt(dx*dx+dy*dy)<80){ax+=o.vx;ay+=o.vy;cnt++;}});
    return cnt?{x:ax/cnt,y:ay/cnt}:{x:0,y:0};
  }
  function boidCoh(b, all) {
    let cx=0,cy=0,cnt=0;
    all.forEach(o=>{if(o===b)return;const dx=b.x-o.x,dy=b.y-o.y;if(Math.sqrt(dx*dx+dy*dy)<100){cx+=o.x;cy+=o.y;cnt++;}});
    return cnt?{x:(cx/cnt-b.x)*0.01,y:(cy/cnt-b.y)*0.01}:{x:0,y:0};
  }

  initMode('flow');

  qsa('.ctrl-btn').forEach(btn => {
    btn.addEventListener('click', () => {
      qsa('.ctrl-btn').forEach(b => b.classList.remove('active'));
      btn.classList.add('active');
      initMode(btn.dataset.mode);
    });
  });

  function loop() {
    ctx.fillStyle = 'rgba(10,10,15,0.2)';
    ctx.fillRect(0,0,W,H);
    time += 0.001;

    if (mode === 'flow') {
      particles.forEach(p => {
        const angle = perlin3(p.x*0.003, p.y*0.003, time) * Math.PI*4;
        p.vx = Math.cos(angle) * p.speed;
        p.vy = Math.sin(angle) * p.speed;
        p.x += p.vx; p.y += p.vy;
        if(p.x<0||p.x>W||p.y<0||p.y>H){p.x=Math.random()*W;p.y=Math.random()*H;}
        ctx.beginPath(); ctx.arc(p.x,p.y,1,0,Math.PI*2);
        ctx.fillStyle=`hsla(${p.hue},100%,65%,0.6)`; ctx.fill();
      });
    } else if (mode === 'fire') {
      particles = particles.filter(p => p.life > 0);
      for (let i = 0; i < 12; i++) particles.push(mkFire());
      particles.forEach(p => {
        p.x+=p.vx; p.y+=p.vy; p.vy-=0.04;
        p.vx+=(Math.random()-0.5)*0.2; p.life-=p.decay;
        const g = ctx.createRadialGradient(p.x,p.y,0,p.x,p.y,p.size*p.life);
        g.addColorStop(0,`rgba(255,255,100,${p.life})`);
        g.addColorStop(0.5,`rgba(255,80,0,${p.life*0.7})`);
        g.addColorStop(1,'rgba(0,0,0,0)');
        ctx.beginPath(); ctx.arc(p.x,p.y,p.size*(p.life+0.3),0,Math.PI*2);
        ctx.fillStyle=g; ctx.fill();
      });
    } else if (mode === 'snow') {
      particles.forEach(p => {
        p.y+=p.speed; p.x+=p.drift;
        if(p.y>H) Object.assign(p, mkSnow());
        ctx.beginPath(); ctx.arc(p.x,p.y,p.size,0,Math.PI*2);
        ctx.fillStyle=`rgba(200,230,255,${0.6+p.size*0.1})`; ctx.fill();
      });
    } else if (mode === 'galaxy') {
      ctx.fillStyle='rgba(10,10,15,0.04)'; ctx.fillRect(0,0,W,H);
      particles.forEach(p => {
        p.twinkle+=0.03;
        const alpha = 0.4+Math.sin(p.twinkle)*0.4;
        ctx.beginPath(); ctx.arc(p.x,p.y,p.size,0,Math.PI*2);
        ctx.fillStyle=`hsla(${p.hue},80%,80%,${alpha})`; ctx.fill();
      });
    } else if (mode === 'boids') {
      particles.forEach(b => {
        const sep=boidSep(b,particles), ali=boidAlign(b,particles), coh=boidCoh(b,particles);
        b.vx+=sep.x*0.15+ali.x*0.05+coh.x;
        b.vy+=sep.y*0.15+ali.y*0.05+coh.y;
        const spd=Math.sqrt(b.vx*b.vx+b.vy*b.vy)||1;
        if(spd>3){b.vx=b.vx/spd*3;b.vy=b.vy/spd*3;}
        b.x+=b.vx; b.y+=b.vy;
        if(b.x<0)b.x=W; if(b.x>W)b.x=0; if(b.y<0)b.y=H; if(b.y>H)b.y=0;
        const angle=Math.atan2(b.vy,b.vx);
        ctx.save(); ctx.translate(b.x,b.y); ctx.rotate(angle);
        ctx.beginPath(); ctx.moveTo(7,0); ctx.lineTo(-4,3); ctx.lineTo(-4,-3); ctx.closePath();
        ctx.fillStyle=`hsla(${b.hue},100%,70%,0.85)`; ctx.fill();
        ctx.restore();
      });
    } else if (mode === 'confetti') {
      particles.forEach(p => {
        p.y+=p.vy; p.x+=p.vx; p.rotation+=p.rotSpeed;
        if(p.y>H){p.y=-20;p.x=Math.random()*W;}
        ctx.save(); ctx.translate(p.x,p.y); ctx.rotate(p.rotation);
        ctx.fillStyle=p.color;
        ctx.fillRect(-p.w/2,-p.h/2,p.w,p.h);
        ctx.restore();
      });
    }

    requestAnimationFrame(loop);
  }
  loop();
})();

// ════════════════════════════════════════════════
//  PHYSICS CANVAS — Verlet rope + bouncing balls
// ════════════════════════════════════════════════
(function() {
  const c = $('physics-canvas');
  if (!c) return;
  const ctx = c.getContext('2d');
  let W, H;
  function resize() { W = c.width = c.offsetWidth; H = c.height = Math.max(c.offsetHeight, 400); }
  resize();

  // Rope
  const SEG = 25, SEG_LEN = 18;
  const rope = Array.from({length:SEG},(_, i)=>({x:W*0.3+i*SEG_LEN,y:H*0.3,px:W*0.3+i*SEG_LEN,py:H*0.3,pinned:i===0}));

  // Balls
  const balls = Array.from({length:8},()=>({
    x:Math.random()*W, y:Math.random()*H*0.5,
    vx:(Math.random()-0.5)*4, vy:Math.random()*3,
    r:8+Math.random()*12,
    color:`hsl(${Math.random()*360},80%,65%)`
  }));

  // Mouse drag on rope endpoint
  let dragging = false;
  c.addEventListener('mousedown', ()=>dragging=true);
  c.addEventListener('mouseup', ()=>dragging=false);
  c.addEventListener('mousemove', e => {
    if (!dragging) return;
    const r = c.getBoundingClientRect();
    rope[rope.length-1].x = e.clientX-r.left;
    rope[rope.length-1].y = e.clientY-r.top;
    rope[rope.length-1].px = rope[rope.length-1].x;
    rope[rope.length-1].py = rope[rope.length-1].y;
  });

  // Swing the anchor
  let t = 0;
  function loop() {
    ctx.fillStyle='rgba(9,9,15,0.25)';
    ctx.fillRect(0,0,W,H);
    t += 0.02;

    // Rope anchor swings
    if (!dragging) {
      rope[0].x = W*0.3 + Math.sin(t)*80;
      rope[0].y = H*0.2;
    }

    // Verlet
    rope.forEach(p => {
      if(p.pinned)return;
      const [vx,vy]=[p.x-p.px,p.y-p.py];
      [p.px,p.py]=[p.x,p.y];
      p.x+=vx*0.99; p.y+=vy*0.99+0.4;
    });
    for(let iter=0;iter<5;iter++){
      for(let i=0;i<rope.length-1;i++){
        const a=rope[i],b=rope[i+1];
        const dx=b.x-a.x,dy=b.y-a.y;
        const d=Math.sqrt(dx*dx+dy*dy)||0.001;
        const corr=(d-SEG_LEN)/d*0.5;
        if(!a.pinned){a.x+=dx*corr;a.y+=dy*corr;}
        if(!b.pinned){b.x-=dx*corr;b.y-=dy*corr;}
      }
    }

    // Draw rope
    ctx.beginPath();
    rope.forEach((p,i)=>i===0?ctx.moveTo(p.x,p.y):ctx.lineTo(p.x,p.y));
    ctx.strokeStyle='rgba(200,255,0,0.7)'; ctx.lineWidth=2.5;
    ctx.lineCap='round'; ctx.stroke();
    // Anchor
    ctx.beginPath(); ctx.arc(rope[0].x,rope[0].y,6,0,Math.PI*2);
    ctx.fillStyle='var(--acid)'; ctx.fill();

    // Balls
    balls.forEach(b => {
      b.vy+=0.35; b.x+=b.vx; b.y+=b.vy;
      if(b.y+b.r>H){b.y=H-b.r;b.vy*=-0.72;b.vx*=0.96;}
      if(b.x-b.r<0){b.x=b.r;b.vx*=-0.85;}
      if(b.x+b.r>W){b.x=W-b.r;b.vx*=-0.85;}
      // Draw shadow
      ctx.beginPath(); ctx.ellipse(b.x,H-2,b.r*0.8,3,0,0,Math.PI*2);
      ctx.fillStyle='rgba(0,0,0,0.4)'; ctx.fill();
      // Draw ball
      const g=ctx.createRadialGradient(b.x-b.r*0.3,b.y-b.r*0.3,1,b.x,b.y,b.r);
      g.addColorStop(0,'rgba(255,255,255,0.4)');
      g.addColorStop(1,b.color);
      ctx.beginPath(); ctx.arc(b.x,b.y,b.r,0,Math.PI*2);
      ctx.fillStyle=g; ctx.fill();
    });

    requestAnimationFrame(loop);
  }
  loop();
})();

// ════════════════════════════════════════════════
//  WEBGL SHADER SYSTEM
// ════════════════════════════════════════════════
(function() {
  const c = $('shader-canvas');
  if (!c) return;

  const gl = c.getContext('webgl') || c.getContext('experimental-webgl');
  if (!gl) { c.style.background = '#1a1a2e'; return; }

  let W, H;
  function resize() {
    W = c.width = c.offsetWidth;
    H = c.height = Math.max(c.offsetHeight, 300);
    gl.viewport(0, 0, W, H);
  }
  resize();
  window.addEventListener('resize', resize);

  let mx = 0.5, my = 0.5;
  c.addEventListener('mousemove', e => {
    const r = c.getBoundingClientRect();
    mx = (e.clientX - r.left) / W;
    my = 1 - (e.clientY - r.top) / H;
  });

  const VS = `
    attribute vec2 a_pos;
    void main() { gl_Position = vec4(a_pos, 0.0, 1.0); }
  `;

  const SHADERS = {
    plasma: `
      precision highp float;
      uniform float uTime; uniform vec2 uRes; uniform vec2 uMouse;
      void main(){
        vec2 uv = gl_FragCoord.xy/uRes;
        float v=0.;
        v+=sin(uv.x*8.+uTime);
        v+=sin(uv.y*6.+uTime*0.8);
        v+=sin((uv.x+uv.y)*5.+uTime*0.6);
        v+=sin(sqrt(pow(uv.x-0.5,2.)+pow(uv.y-0.5,2.))*10.-uTime*2.);
        float c=sin(v*3.14159);
        gl_FragColor=vec4(0.5+0.5*cos(c*3.14+vec4(0,0.6,1.2,0)));
      }`,
    noise: `
      precision highp float;
      uniform float uTime; uniform vec2 uRes; uniform vec2 uMouse;
      float rand(vec2 c){return fract(sin(dot(c,vec2(12.9898,78.233)))*43758.5453);}
      float noise(vec2 p){vec2 i=floor(p),f=fract(p),u=f*f*(3.-2.*f);
        return mix(mix(rand(i),rand(i+vec2(1,0)),u.x),mix(rand(i+vec2(0,1)),rand(i+vec2(1,1)),u.x),u.y);}
      float fbm(vec2 p){float v=0.,a=0.5;for(int i=0;i<5;i++){v+=a*noise(p);p*=2.1;a*=0.5;}return v;}
      void main(){
        vec2 uv=gl_FragCoord.xy/uRes;
        float n=fbm(uv*3.+vec2(uTime*0.3,uTime*0.1));
        float n2=fbm(uv*6.-vec2(uTime*0.15,uTime*0.2)+n);
        vec3 col=mix(vec3(0.05,0.02,0.15),vec3(0.1,0.8,0.4),n2);
        col=mix(col,vec3(0.8,1.0,0.0),n2*n2*0.5);
        gl_FragColor=vec4(col,1.);
      }`,
    glitch: `
      precision highp float;
      uniform float uTime; uniform vec2 uRes; uniform vec2 uMouse;
      float rand(float x){return fract(sin(x*127.1)*43758.5453);}
      float rand2(vec2 c){return fract(sin(dot(c,vec2(12.9898,78.233)))*43758.5453);}
      void main(){
        vec2 uv=gl_FragCoord.xy/uRes;
        float glitchLine=step(0.97,sin(uTime*8.+uv.y*40.));
        float shift=rand(floor(uTime*30.))*0.06*glitchLine;
        vec2 uvR=vec2(uv.x+shift,uv.y);
        vec2 uvB=vec2(uv.x-shift,uv.y);
        // Gradient base
        float r=sin(uvR.x*10.+uTime)*0.5+0.5;
        float g=cos(uv.y*8.+uTime*0.7)*0.5+0.5;
        float b=sin(uvB.x*6.+uvB.y*4.+uTime*0.5)*0.5+0.5;
        // Block glitch
        vec2 block=floor(uv*vec2(20.,30.))/vec2(20.,30.);
        float blockGlitch=step(0.98,rand2(block+floor(uTime*15.)))*step(0.97,sin(uTime*5.));
        r+=blockGlitch*rand2(block)*0.5;
        g-=blockGlitch*0.3;
        // Scanline
        float scan=0.9+0.1*sin(uv.y*600.);
        gl_FragColor=vec4(r*scan,g*scan,b*scan,1.);
      }`,
    ripple: `
      precision highp float;
      uniform float uTime; uniform vec2 uRes; uniform vec2 uMouse;
      void main(){
        vec2 uv=gl_FragCoord.xy/uRes;
        vec2 center=uMouse;
        float d=length(uv-center);
        float ripple=sin(d*40.-uTime*6.)*0.04/(d+0.1);
        vec2 uv2=uv+normalize(uv-center)*ripple;
        // Checkerboard base to show distortion
        vec2 grid=floor(uv2*12.);
        float check=mod(grid.x+grid.y,2.);
        float ring=sin(d*25.-uTime*4.)*0.5+0.5;
        vec3 col=mix(vec3(0.04,0.04,0.12),vec3(0.0,0.96,1.0),check*0.4+ring*0.3);
        col+=ripple*8.*vec3(0.,0.8,1.);
        gl_FragColor=vec4(col,1.);
      }`,
    fractal: `
      precision highp float;
      uniform float uTime; uniform vec2 uRes; uniform vec2 uMouse;
      void main(){
        vec2 uv=(gl_FragCoord.xy-uRes*0.5)/min(uRes.x,uRes.y)*2.8;
        float zoom=1.5+sin(uTime*0.2)*0.4;
        uv/=zoom;
        uv+=vec2(-0.7+sin(uTime*0.1)*0.2, sin(uTime*0.07)*0.3);
        vec2 z=vec2(0.);
        int iter=0;
        for(int i=0;i<96;i++){
          z=vec2(z.x*z.x-z.y*z.y,2.*z.x*z.y)+uv;
          if(dot(z,z)>4.) break;
          iter=i;
        }
        float t=float(iter)/96.;
        vec3 col=0.5+0.5*cos(3.14159*t*3.+vec3(0.0,0.6,1.0)+uTime*0.2);
        gl_FragColor=vec4(col*(1.-step(95.,float(iter))),1.);
      }`,
    vhs: `
      precision highp float;
      uniform float uTime; uniform vec2 uRes; uniform vec2 uMouse;
      float rand(vec2 c){return fract(sin(dot(c,vec2(12.9898,78.233)))*43758.5);}
      void main(){
        vec2 uv=gl_FragCoord.xy/uRes;
        // Wavy distort
        float wave=sin(uv.y*30.+uTime*8.)*0.003;
        float bandNoise=rand(vec2(floor(uv.y*50.),floor(uTime*12.)));
        float jitter=(bandNoise-0.5)*0.015*step(0.94,bandNoise);
        uv.x+=wave+jitter;
        // Color fringe
        float rr=sin(uv.x*3.+uTime)*0.5+0.5;
        float gg=cos(uv.y*4.+uTime*0.8)*0.5+0.5;
        float bb=sin((uv.x+uv.y)*5.+uTime*0.5)*0.5+0.5;
        // Noise grain
        float grain=rand(uv+mod(uTime,1.))*0.1;
        // Scanlines
        float scan=0.88+0.12*sin(uv.y*uRes.y*0.8);
        // Vignette
        vec2 vig=uv*2.-1.;
        float vignette=1.-dot(vig*0.6,vig*0.6);
        gl_FragColor=vec4((vec3(rr,gg,bb)+grain)*scan*vignette,1.);
      }`,
    kaleidoscope: `
      precision highp float;
      uniform float uTime; uniform vec2 uRes; uniform vec2 uMouse;
      void main(){
        vec2 uv=gl_FragCoord.xy/uRes*2.-1.;
        uv.x*=uRes.x/uRes.y;
        float angle=atan(uv.y,uv.x);
        float radius=length(uv);
        float N=8.;
        float seg=3.14159*2./N;
        angle=mod(angle,seg);
        if(mod(floor(angle/seg+0.5),2.)==1.) angle=seg-angle;
        angle+=uTime*0.3;
        vec2 d=vec2(cos(angle),sin(angle))*radius;
        vec3 col=0.5+0.5*cos(d.xyx*4.+vec3(0,1.5,3)+uTime);
        col*=col; // gamma
        gl_FragColor=vec4(col,1.);
      }`,
    godray: `
      precision highp float;
      uniform float uTime; uniform vec2 uRes; uniform vec2 uMouse;
      void main(){
        vec2 uv=gl_FragCoord.xy/uRes;
        vec2 lpos=vec2(0.5+sin(uTime*0.4)*0.3, 0.85);
        vec2 dir=uv-lpos;
        float SAMPLES=50.;
        dir/=SAMPLES;
        float decay=1.,weight=0.008,acc=0.;
        vec2 p=uv;
        for(float i=0.;i<50.;i++){
          p-=dir;
          float mask=max(0.,1.-length(p-lpos)*6.);
          float streak=pow(max(0.,sin(atan(p.y-lpos.y,p.x-lpos.x)*8.+uTime*2.)),4.);
          acc+=mask*decay*weight*(0.5+streak*0.5);
          decay*=0.96;
        }
        float base=max(0.,1.-length(uv-lpos)*3.);
        vec3 rayCol=vec3(1.0,0.9,0.5)*acc*6.;
        vec3 bg=mix(vec3(0.02,0.02,0.1),vec3(0.1,0.05,0.2),uv.y);
        gl_FragColor=vec4(bg+rayCol,1.);
      }`
  };

  function compile(src) {
    const vs = gl.createShader(gl.VERTEX_SHADER);
    gl.shaderSource(vs, VS); gl.compileShader(vs);
    const fs = gl.createShader(gl.FRAGMENT_SHADER);
    gl.shaderSource(fs, src); gl.compileShader(fs);
    const prog = gl.createProgram();
    gl.attachShader(prog, vs); gl.attachShader(prog, fs);
    gl.linkProgram(prog);
    return prog;
  }

  // Fullscreen quad
  const buf = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, buf);
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1,1,-1,-1,1,1,1]), gl.STATIC_DRAW);

  let programs = {};
  let current = 'plasma';
  Object.keys(SHADERS).forEach(k => { programs[k] = compile(SHADERS[k]); });

  qsa('.shader-btn').forEach(btn => {
    btn.addEventListener('click', () => {
      qsa('.shader-btn').forEach(b => b.classList.remove('active'));
      btn.classList.add('active');
      current = btn.dataset.shader;
    });
  });

  let startTime = performance.now();
  function loop() {
    const t = (performance.now() - startTime) / 1000;
    const prog = programs[current];
    gl.useProgram(prog);
    const posLoc = gl.getAttribLocation(prog, 'a_pos');
    gl.enableVertexAttribArray(posLoc);
    gl.bindBuffer(gl.ARRAY_BUFFER, buf);
    gl.vertexAttribPointer(posLoc, 2, gl.FLOAT, false, 0, 0);
    const uTime = gl.getUniformLocation(prog, 'uTime');
    const uRes  = gl.getUniformLocation(prog, 'uRes');
    const uMouse= gl.getUniformLocation(prog, 'uMouse');
    gl.uniform1f(uTime, t);
    gl.uniform2f(uRes, W, H);
    gl.uniform2f(uMouse, mx, my);
    gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
    requestAnimationFrame(loop);
  }
  loop();
})();

// ════════════════════════════════════════════════
//  MANIFESTO CANVAS — aurora
// ════════════════════════════════════════════════
(function() {
  const c = $('manifesto-canvas');
  if (!c) return;
  const ctx = c.getContext('2d');
  let W, H;
  function resize() { W = c.width = c.offsetWidth; H = c.height = c.offsetHeight; }
  resize();
  window.addEventListener('resize', resize);

  let t = 0;
  function loop() {
    ctx.fillStyle = 'rgba(10,10,15,0.15)';
    ctx.fillRect(0,0,W,H);
    t += 0.005;

    const auroraLines = 5;
    for (let a = 0; a < auroraLines; a++) {
      ctx.beginPath();
      const yBase = H * (0.3 + a * 0.08);
      for (let x = 0; x <= W; x += 4) {
        const noise1 = perlin2(x*0.003 + t + a*2, t*0.5);
        const noise2 = perlin2(x*0.005 - t*0.7 + a, t*0.3);
        const y = yBase + noise1*80 + noise2*40;
        x === 0 ? ctx.moveTo(x,y) : ctx.lineTo(x,y);
      }
      const hue = 150 + a * 30 + Math.sin(t)*20;
      const grad = ctx.createLinearGradient(0,0,W,0);
      grad.addColorStop(0,'transparent');
      grad.addColorStop(0.3,`hsla(${hue},100%,60%,0.06)`);
      grad.addColorStop(0.7,`hsla(${hue+40},100%,70%,0.08)`);
      grad.addColorStop(1,'transparent');
      ctx.strokeStyle=grad; ctx.lineWidth=60; ctx.stroke();
    }
    requestAnimationFrame(loop);
  }
  loop();
})();
</script>
</body>
</html>
EOF