/* global React, Stage, Sprite, useTime, useSprite, Easing, clamp */
// ============================================================
// Inframind — Workflow Animation (light enterprise theme)
// Story arc: Capture → Upload → Reconstruct → Detect → Quantify
//   → Report → Compare → Collaborate → Outro
// ============================================================

const { useMemo: useMemoAnim, useRef: useRefAnim } = React;

// Palette mirroring the rest of the site
const C = {
  paper:    "#FFFFFF",
  bg:       "#FAFAFA",
  panel:    "#FFFFFF",
  ink:      "#0A0A0A",
  ink2:     "#404040",
  muted:    "#737373",
  dim:      "#A3A3A3",
  border:   "#E5E5E5",
  borderSoft: "#ECECEC",
  hairline: "#F5F5F5",
  crit:     "#DC2626",
  high:     "#EA580C",
  med:      "#EAB308",
  low:      "#A3A3A3",
  ok:       "#16A34A",
  info:     "#2563EB",
  scan:     "#2563EB",
  scanLite: "#3B82F6",
  scanFade: "#DBEAFE",
};

// Scene schedule (start, end, num, title, sub)
const SCENES = [
  { id: "capture",     start: 0,    end: 4.2,  num: "01", title: "Capture",      sub: "LiDAR + imagery field survey" },
  { id: "upload",      start: 4.2,  end: 7.6,  num: "02", title: "Upload",       sub: "Multi-modal data to cloud" },
  { id: "reconstruct", start: 7.6,  end: 12.8, num: "03", title: "Reconstruct",  sub: "Point cloud to surface twin" },
  { id: "detect",      start: 12.8, end: 17.0, num: "04", title: "Detect",       sub: "AI defect detection" },
  { id: "quantify",    start: 17.0, end: 21.0, num: "05", title: "Quantify",     sub: "Risk-ranked insights" },
  { id: "report",      start: 21.0, end: 25.0, num: "06", title: "Report",       sub: "One-click evidence pack" },
  { id: "compare",     start: 25.0, end: 29.0, num: "07", title: "Compare",      sub: "Cross-campaign change" },
  { id: "collab",      start: 29.0, end: 33.0, num: "08", title: "Collaborate",  sub: "Engineer review & action" },
  { id: "outro",       start: 33.0, end: 36.5, num: "—",  title: "",             sub: "" },
];
const TOTAL = 36.5;

// ============================================================
// Persistent chrome: scene caption, wordmark, timecode, grid
// ============================================================

function FrameChrome() {
  const t = useTime();
  return (
    <>
      <BackgroundGrid/>
      <CornerTicks/>
      <Wordmark/>
      <SceneCaption/>
      <Timecode t={t}/>
      <Tagline/>
    </>
  );
}

function BackgroundGrid() {
  const t = useTime();
  const drift = (t * 3) % 80;
  return (
    <>
      <div style={{
        position: "absolute", inset: 0,
        background: C.bg,
      }}/>
      <div style={{
        position: "absolute", inset: 0,
        backgroundImage: `
          linear-gradient(to right, rgba(10,10,10,0.04) 1px, transparent 1px),
          linear-gradient(to bottom, rgba(10,10,10,0.04) 1px, transparent 1px)
        `,
        backgroundSize: "80px 80px",
        backgroundPosition: `${drift}px ${drift}px`,
        maskImage: "radial-gradient(ellipse 90% 80% at 50% 50%, #000 30%, transparent 100%)",
      }}/>
    </>
  );
}

function CornerTicks() {
  const ticks = [
    { x: 0,    y: 0,    a: 0 },
    { x: 1920, y: 0,    a: 90 },
    { x: 1920, y: 1080, a: 180 },
    { x: 0,    y: 1080, a: 270 },
  ];
  return ticks.map((tk, i) => (
    <svg key={i} width="40" height="40" viewBox="0 0 40 40" style={{
      position: "absolute",
      left: tk.x === 0 ? 28 : "auto",
      right: tk.x !== 0 ? 28 : "auto",
      top: tk.y === 0 ? 28 : "auto",
      bottom: tk.y !== 0 ? 28 : "auto",
      transform: `rotate(${tk.a}deg)`,
      zIndex: 9,
    }}>
      <path d="M2 14 L2 2 L14 2" stroke={C.ink} strokeWidth="1" fill="none" opacity="0.4"/>
    </svg>
  ));
}

function Wordmark() {
  return (
    <div style={{
      position: "absolute", top: 56, right: 64,
      display: "flex", alignItems: "center",
      zIndex: 10,
    }}>
      <img src="assets/inframind-logo.png" alt="Inframind"
           style={{height: 28, width: "auto", display: "block"}}/>
    </div>
  );
}

function SceneCaption() {
  const t = useTime();
  const scene = SCENES.find(s => t >= s.start && t < s.end) || SCENES[SCENES.length - 1];
  const elapsedIn = t - scene.start;
  const op = Math.min(1, elapsedIn / 0.35) * Math.min(1, (scene.end - t) / 0.35);
  return (
    <div style={{
      position: "absolute", top: 56, left: 64,
      fontFamily: "Inter, sans-serif",
      color: C.ink,
      opacity: scene.id === "outro" ? 0 : op,
      zIndex: 10,
    }}>
      <div style={{
        fontFamily: "JetBrains Mono, monospace",
        fontSize: 13,
        color: C.muted,
        letterSpacing: "0.18em",
        marginBottom: 14,
        display: "flex", alignItems: "center", gap: 12,
      }}>
        <span style={{width: 6, height: 6, background: C.ink, borderRadius: "50%"}}></span>
        SCENE {scene.num} / 08
      </div>
      <div style={{
        fontSize: 72,
        fontWeight: 700,
        letterSpacing: "-0.035em",
        lineHeight: 0.95,
        color: C.ink,
      }}>{scene.title}</div>
      <div style={{
        marginTop: 16,
        fontSize: 22,
        color: C.muted,
        letterSpacing: "-0.005em",
        fontWeight: 400,
      }}>{scene.sub}</div>
    </div>
  );
}

function Timecode({ t }) {
  const fmt = (x) => {
    const s = Math.floor(x);
    const cs = Math.floor((x * 100) % 100);
    return `00:${String(s).padStart(2, "0")}.${String(cs).padStart(2, "0")}`;
  };
  return (
    <div style={{
      position: "absolute", bottom: 56, right: 64,
      fontFamily: "JetBrains Mono, monospace",
      fontSize: 13,
      color: C.dim,
      letterSpacing: "0.08em",
      zIndex: 10,
    }}>
      {fmt(t)} <span style={{color: C.dim, opacity: 0.5}}>/ {fmt(TOTAL)}</span>
    </div>
  );
}

function Tagline() {
  return (
    <div style={{
      position: "absolute", bottom: 56, left: 64,
      fontFamily: "JetBrains Mono, monospace",
      fontSize: 12,
      color: C.muted,
      letterSpacing: "0.14em",
      textTransform: "uppercase",
      zIndex: 10,
      maxWidth: 540,
    }}>
      From raw infrastructure data to risk-ranked engineering decisions
    </div>
  );
}

// ============================================================
// Shared UI primitives
// ============================================================

function ReadoutPanel({ x, y, width = 360, title, items }) {
  return (
    <div style={{
      position: "absolute", left: x, top: y,
      width,
      background: C.paper,
      border: `1px solid ${C.border}`,
      borderRadius: 8,
      padding: "20px 22px",
      fontFamily: "Inter, sans-serif",
      boxShadow: "0 1px 2px rgba(0,0,0,0.04), 0 4px 12px -4px rgba(0,0,0,0.08)",
    }}>
      <div style={{
        fontFamily: "JetBrains Mono, monospace",
        fontSize: 11, color: C.muted,
        letterSpacing: "0.12em", textTransform: "uppercase",
        marginBottom: 16, paddingBottom: 12,
        borderBottom: `1px solid ${C.borderSoft}`,
      }}>{title}</div>
      <div style={{display: "flex", flexDirection: "column", gap: 12}}>
        {items.map((it, i) => (
          <div key={i} style={{
            display: "grid", gridTemplateColumns: "10px 1fr auto",
            gap: 12, alignItems: "center",
          }}>
            <span style={{width: 8, height: 8, background: it.color, borderRadius: "50%", flex: "none"}}/>
            <span style={{fontSize: 13, color: C.ink2}}>{it.k}</span>
            <span style={{
              fontFamily: "JetBrains Mono, monospace",
              fontSize: 13,
              color: C.ink,
              fontVariantNumeric: "tabular-nums",
              fontWeight: 500,
            }}>{it.v}</span>
          </div>
        ))}
      </div>
    </div>
  );
}

// ============================================================
// 3D bridge geometry (light theme)
// ============================================================

const YAW_COS = 0.93, YAW_SIN = 0.37, PITCH = 0.28, CX = 960, CY = 560;
function proj3(x, y, z) {
  const x1 = x * YAW_COS - z * YAW_SIN;
  const z1 = x * YAW_SIN + z * YAW_COS;
  return [CX + x1, CY + y - z1 * PITCH];
}
const P = (p) => proj3(p[0], p[1], p[2]);
const polyPath = (pts3) => "M" + pts3.map(P).map(([x, y]) => `${x.toFixed(1)},${y.toFixed(1)}`).join(" L") + " Z";
const linePath = (a, b) => { const [ax, ay] = P(a), [bx, by] = P(b); return `M${ax.toFixed(1)},${ay.toFixed(1)} L${bx.toFixed(1)},${by.toFixed(1)}`; };

const BR = {
  deck:    { xL: -360, xR: 360, yT: -94, yB: -60, zN: -100, zF: 100 },
  girders: [-78, -26, 26, 78].map(z => ({ xL: -340, xR: 340, yT: -60, yB: -2, zN: z - 7, zF: z + 7 })),
  piers:   [-220, 220].map(x => ({ xL: x - 22, xR: x + 22, yT: 0, yB: 92, zN: -38, zF: 38 })),
  ground:  92,
};

function makeBridgePoints() {
  let s = 13;
  const r = () => { s = (s * 9301 + 49297) % 233280; return s / 233280; };
  const pts = [];
  const onFace = (n, mapper) => { for (let i = 0; i < n; i++) pts.push(mapper(r(), r())); };
  onFace(900, (u, v) => [BR.deck.xL + u * (BR.deck.xR - BR.deck.xL), BR.deck.yT + r() * 0.6 - 0.3, BR.deck.zN + v * (BR.deck.zF - BR.deck.zN)]);
  onFace(220, (u, v) => [BR.deck.xL + u * (BR.deck.xR - BR.deck.xL), BR.deck.yT + v * (BR.deck.yB - BR.deck.yT), BR.deck.zN]);
  onFace(140, (u, v) => [BR.deck.xR, BR.deck.yT + u * (BR.deck.yB - BR.deck.yT), BR.deck.zN + v * (BR.deck.zF - BR.deck.zN)]);
  BR.girders.forEach(g => {
    onFace(120, (u, v) => [g.xL + u * (g.xR - g.xL), g.yB, g.zN + v * (g.zF - g.zN)]);
    onFace(60,  (u, v) => [g.xL + u * (g.xR - g.xL), g.yT + v * (g.yB - g.yT), g.zN]);
  });
  BR.piers.forEach(p => {
    onFace(180, (u, v) => [p.xL + u * (p.xR - p.xL), p.yT + v * (p.yB - p.yT), p.zN]);
    onFace(140, (u, v) => [p.xR, p.yT + u * (p.yB - p.yT), p.zN + v * (p.zF - p.zN)]);
  });
  return pts.map(p => ({ pos: p, jitter: r(), depth: r() }));
}

function BridgeGround({ opacity = 1 }) {
  const corners = [
    [BR.deck.xL - 250, BR.ground, BR.deck.zN - 40],
    [BR.deck.xR + 250, BR.ground, BR.deck.zN - 40],
    [BR.deck.xR + 250, BR.ground, BR.deck.zF + 40],
    [BR.deck.xL - 250, BR.ground, BR.deck.zF + 40],
  ];
  return (
    <g opacity={opacity}>
      <path d={polyPath(corners)} fill={C.hairline} stroke={C.borderSoft} strokeWidth="0.6"/>
      {[-100, -60, -20, 20, 60, 100].map((z, i) => (
        <path key={i} d={linePath([BR.deck.xL - 250, BR.ground, z], [BR.deck.xR + 250, BR.ground, z])}
              stroke="rgba(10,10,10,0.04)" strokeWidth="0.5"/>
      ))}
    </g>
  );
}

function BridgeSolid({ opacity = 1 }) {
  const pierShadows = BR.piers.map((p, i) => {
    const [cx, cy] = P([(p.xL + p.xR) / 2, BR.ground - 0.5, (p.zN + p.zF) / 2]);
    return <ellipse key={i} cx={cx} cy={cy + 4} rx="56" ry="6" fill="rgba(10,10,10,0.08)"/>;
  });
  return (
    <g opacity={opacity}>
      <defs>
        <linearGradient id="brTopLt" x1="0" y1="0" x2="0" y2="1">
          <stop offset="0" stopColor="#F5F5F5" stopOpacity="1"/>
          <stop offset="1" stopColor="#E5E5E5" stopOpacity="1"/>
        </linearGradient>
        <linearGradient id="brFrontLt" x1="0" y1="0" x2="0" y2="1">
          <stop offset="0" stopColor="#D4D4D4" stopOpacity="1"/>
          <stop offset="1" stopColor="#A3A3A3" stopOpacity="1"/>
        </linearGradient>
        <linearGradient id="brSideLt" x1="0" y1="0" x2="0" y2="1">
          <stop offset="0" stopColor="#A3A3A3" stopOpacity="1"/>
          <stop offset="1" stopColor="#737373" stopOpacity="1"/>
        </linearGradient>
        <linearGradient id="brBottomLt" x1="0" y1="0" x2="0" y2="1">
          <stop offset="0" stopColor="#737373" stopOpacity="1"/>
          <stop offset="1" stopColor="#525252" stopOpacity="1"/>
        </linearGradient>
      </defs>
      {pierShadows}
      {/* Piers */}
      {BR.piers.map((p, i) => (
        <g key={`pier${i}`}>
          <path d={polyPath([[p.xL, p.yT, p.zN], [p.xR, p.yT, p.zN], [p.xR, p.yB, p.zN], [p.xL, p.yB, p.zN]])}
                fill="url(#brFrontLt)" stroke={C.ink2} strokeOpacity="0.35" strokeWidth="0.6"/>
          <path d={polyPath([[p.xR, p.yT, p.zN], [p.xR, p.yT, p.zF], [p.xR, p.yB, p.zF], [p.xR, p.yB, p.zN]])}
                fill="url(#brSideLt)" stroke={C.ink2} strokeOpacity="0.3" strokeWidth="0.5"/>
        </g>
      ))}
      {/* Girders */}
      {BR.girders.map((g, i) => (
        <g key={`girder${i}`}>
          <path d={polyPath([[g.xL, g.yB, g.zN], [g.xR, g.yB, g.zN], [g.xR, g.yB, g.zF], [g.xL, g.yB, g.zF]])}
                fill="url(#brBottomLt)" stroke={C.ink2} strokeOpacity="0.25" strokeWidth="0.4"/>
          <path d={polyPath([[g.xL, g.yT, g.zN], [g.xR, g.yT, g.zN], [g.xR, g.yB, g.zN], [g.xL, g.yB, g.zN]])}
                fill="url(#brFrontLt)" stroke={C.ink2} strokeOpacity="0.35" strokeWidth="0.5"/>
        </g>
      ))}
      {/* Deck */}
      <path d={polyPath([[BR.deck.xL, BR.deck.yB, BR.deck.zN], [BR.deck.xR, BR.deck.yB, BR.deck.zN], [BR.deck.xR, BR.deck.yB, BR.deck.zF], [BR.deck.xL, BR.deck.yB, BR.deck.zF]])}
            fill="url(#brBottomLt)" stroke={C.ink2} strokeOpacity="0.3" strokeWidth="0.5"/>
      <path d={polyPath([[BR.deck.xL, BR.deck.yT, BR.deck.zN], [BR.deck.xR, BR.deck.yT, BR.deck.zN], [BR.deck.xR, BR.deck.yB, BR.deck.zN], [BR.deck.xL, BR.deck.yB, BR.deck.zN]])}
            fill="url(#brFrontLt)" stroke={C.ink2} strokeOpacity="0.45" strokeWidth="0.7"/>
      <path d={polyPath([[BR.deck.xR, BR.deck.yT, BR.deck.zN], [BR.deck.xR, BR.deck.yT, BR.deck.zF], [BR.deck.xR, BR.deck.yB, BR.deck.zF], [BR.deck.xR, BR.deck.yB, BR.deck.zN]])}
            fill="url(#brSideLt)" stroke={C.ink2} strokeOpacity="0.35" strokeWidth="0.5"/>
      <path d={polyPath([[BR.deck.xL, BR.deck.yT, BR.deck.zN], [BR.deck.xR, BR.deck.yT, BR.deck.zN], [BR.deck.xR, BR.deck.yT, BR.deck.zF], [BR.deck.xL, BR.deck.yT, BR.deck.zF]])}
            fill="url(#brTopLt)" stroke={C.ink2} strokeOpacity="0.4" strokeWidth="0.6"/>
      {/* Deck centerline */}
      <path d={linePath([BR.deck.xL + 14, BR.deck.yT - 0.5, 0], [BR.deck.xR - 14, BR.deck.yT - 0.5, 0])}
            stroke={C.scan} strokeOpacity="0.5" strokeWidth="0.9" strokeDasharray="14 14" fill="none"/>
      {/* Expansion joints */}
      {[-240, -120, 0, 120, 240].map((x, i) => (
        <path key={i} d={linePath([x, BR.deck.yT - 0.3, BR.deck.zN + 4], [x, BR.deck.yT - 0.3, BR.deck.zF - 4])}
              stroke={C.ink2} strokeOpacity="0.3" strokeWidth="0.9" fill="none"/>
      ))}
    </g>
  );
}

function BridgeWireframe({ opacity = 1 }) {
  const boxEdges = (b) => {
    const v = [
      [b.xL, b.yT, b.zN], [b.xR, b.yT, b.zN], [b.xR, b.yT, b.zF], [b.xL, b.yT, b.zF],
      [b.xL, b.yB, b.zN], [b.xR, b.yB, b.zN], [b.xR, b.yB, b.zF], [b.xL, b.yB, b.zF],
    ];
    return [
      [v[0],v[1]],[v[1],v[2]],[v[2],v[3]],[v[3],v[0]],
      [v[4],v[5]],[v[5],v[6]],[v[6],v[7]],[v[7],v[4]],
      [v[0],v[4]],[v[1],v[5]],[v[2],v[6]],[v[3],v[7]],
    ];
  };
  const edges = [...boxEdges(BR.deck), ...BR.girders.flatMap(boxEdges), ...BR.piers.flatMap(boxEdges)];
  return (
    <g opacity={opacity}>
      {edges.map((e, i) => (
        <path key={i} d={linePath(e[0], e[1])} stroke={C.ink} strokeOpacity="0.5" strokeWidth="0.7" fill="none"/>
      ))}
    </g>
  );
}

// ============================================================
// SCENE 01 — CAPTURE: UAV scans bridge, points materialise
// ============================================================
function SceneCapture() {
  const { progress } = useSprite();
  const scanT = clamp((progress - 0.05) / 0.85, 0, 1);
  const scanX = BR.deck.xL + scanT * (BR.deck.xR - BR.deck.xL);
  const points = useMemoAnim(() => makeBridgePoints(), []);
  const wob = Math.sin(progress * 26) * 1.5;

  return (
    <div style={{position: "absolute", inset: 0}}>
      <AssetTitle text="Field survey · A14 Bridge-07 / Span-02 · UAV LiDAR + imagery"/>
      <svg viewBox="0 0 1920 1080" style={{position: "absolute", inset: 0, width: "100%", height: "100%"}}>
        <BridgeGround opacity={0.6}/>
        <BridgeWireframe opacity={0.25}/>
        {/* Captured points (only those whose x has been swept) */}
        <g>
          {points.map((pt, i) => {
            if (pt.pos[0] > scanX) return null;
            const age = clamp((scanX - pt.pos[0]) / 90, 0, 1);
            const [sx, sy] = P(pt.pos);
            return (
              <circle key={i} cx={sx} cy={sy}
                      r={0.6 + pt.jitter * 0.8}
                      fill={C.scan}
                      opacity={(0.5 + (1 - pt.depth) * 0.4) * age}/>
            );
          })}
        </g>
        {scanT < 0.99 && <UAVScanner uavX={scanX} uavY={-200 + wob} uavZ={-10}/>}
      </svg>
      <ReadoutPanel x={1380} y={520} title="Sensor stream" items={[
        { k: "UAV LiDAR",     v: `${Math.floor(scanT * 1.4 * 1000)}k pts/s`,         color: C.scan },
        { k: "GNSS · RTK",    v: scanT > 0.15 ? "locked" : "acquiring",                color: scanT > 0.15 ? C.ok : C.med },
        { k: "Imagery",       v: `${Math.floor(scanT * 1240)} frames`,                 color: C.scanLite },
        { k: "Chainage",      v: `${(scanT * 800).toFixed(1)} m`,                      color: C.muted },
      ]}/>
    </div>
  );
}

function AssetTitle({ text }) {
  return (
    <div style={{
      position: "absolute", left: 64, top: 320,
      fontFamily: "JetBrains Mono, monospace",
      fontSize: 13, color: C.muted,
      letterSpacing: "0.12em", textTransform: "uppercase",
      zIndex: 5,
    }}>
      {text}
    </div>
  );
}

function UAVScanner({ uavX, uavY, uavZ }) {
  const [ux, uy] = P([uavX, uavY, uavZ]);
  const N = 9;
  const rays = [];
  for (let i = 0; i < N; i++) {
    const t = i / (N - 1);
    const tz = BR.deck.zN + 4 + t * (BR.deck.zF - BR.deck.zN - 8);
    rays.push(P([uavX, BR.deck.yT - 0.5, tz]));
  }
  const fanPath = [
    `M${ux},${uy}`,
    `L${rays[0][0]},${rays[0][1]}`,
    `L${rays[rays.length - 1][0]},${rays[rays.length - 1][1]}`,
    `Z`,
  ].join(" ");
  return (
    <g>
      <path d={fanPath} fill={C.scan} opacity="0.10"/>
      {rays.map((r, i) => {
        const t = i / (N - 1);
        const op = 0.18 + Math.abs(t - 0.5) * 0.5;
        return <line key={i} x1={ux} y1={uy} x2={r[0]} y2={r[1]} stroke={C.scan} strokeOpacity={op} strokeWidth="0.6"/>;
      })}
      <line x1={ux} y1={uy} x2={rays[Math.floor(N / 2)][0]} y2={rays[Math.floor(N / 2)][1]}
            stroke={C.scanLite} strokeOpacity="0.85" strokeWidth="1"/>
      <g transform={`translate(${ux}, ${uy})`}>
        <line x1="-22" y1="-2" x2="22" y2="2" stroke={C.ink} strokeWidth="1.6"/>
        <line x1="-8" y1="-14" x2="8" y2="14" stroke={C.ink} strokeWidth="1.6"/>
        {[[-22, -2], [22, 2], [-8, -14], [8, 14]].map(([x, y], i) => (
          <g key={i}>
            <circle cx={x} cy={y} r="7" fill={C.paper} stroke={C.ink} strokeWidth="1"/>
            <circle cx={x} cy={y} r="6.5" fill="none" stroke={C.scanLite} strokeOpacity="0.5" strokeWidth="0.5"/>
          </g>
        ))}
        <rect x="-7" y="-5" width="14" height="10" fill={C.ink}/>
        <rect x="-4" y="-2" width="8" height="4" fill={C.scan}/>
      </g>
      <g transform={`translate(${ux + 36}, ${uy - 4})`}>
        <rect x="0" y="-12" width="86" height="22" fill={C.paper} stroke={C.scan} strokeOpacity="0.6" strokeWidth="0.7" rx="3"/>
        <text x="8" y="0" fontFamily="JetBrains Mono, monospace" fontSize="11" fill={C.scan}>UAV-01</text>
        <text x="8" y="6" fontFamily="JetBrains Mono, monospace" fontSize="8" fill={C.muted}>ALT 32 m</text>
      </g>
    </g>
  );
}

// ============================================================
// SCENE 02 — UPLOAD: files stream to Inframind Cloud
// ============================================================
function SceneUpload() {
  const { progress } = useSprite();
  const sources = [
    { x: 600,  c: C.scan,     label: "LIDAR.E57",    size: "4.2 GB" },
    { x: 840,  c: C.scanLite, label: "HISTORY.ZIP",  size: "1.1 GB" },
    { x: 1080, c: C.ok,       label: "IMAGERY.TIFF", size: "2.6 GB" },
    { x: 1320, c: C.muted,    label: "REPORT.PDF",   size: "84 MB"  },
  ];
  return (
    <div style={{position: "absolute", inset: 0}}>
      <svg viewBox="0 0 1920 1080" style={{position: "absolute", inset: 0, width: "100%", height: "100%"}}>
        {/* Cloud at top */}
        <g transform="translate(960, 240)" opacity={Math.min(1, progress * 2)}>
          <CloudShape/>
        </g>
        {/* Data streams */}
        {sources.map((s, i) => (
          <DataStream key={i} x={s.x} startY={820} endY={300} color={s.c} delay={i * 0.12} progress={progress}/>
        ))}
        {/* Source boxes */}
        {sources.map((s, i) => (
          <g key={i} transform={`translate(${s.x - 80}, 800)`}>
            <rect x="0" y="0" width="160" height="64" fill={C.paper} stroke={s.c} strokeWidth="1.4" rx="6"
                  opacity={Math.min(1, (progress - i * 0.05) * 3)}/>
            <rect x="0" y="0" width="160" height="22" fill={s.c} opacity={Math.min(1, (progress - i * 0.05) * 3) * 0.1} rx="6"/>
            <text x="80" y="30" textAnchor="middle" fontFamily="JetBrains Mono, monospace"
                  fontSize="12" fill={C.ink} opacity={Math.min(1, (progress - i * 0.05) * 3)}
                  fontWeight="600">{s.label}</text>
            <text x="80" y="50" textAnchor="middle" fontFamily="JetBrains Mono, monospace"
                  fontSize="11" fill={s.c} opacity={Math.min(1, (progress - i * 0.05) * 3)}>{s.size}</text>
          </g>
        ))}
        <text x="960" y="282" textAnchor="middle" fontFamily="Inter, sans-serif"
              fontSize="16" fill={C.ink} fontWeight="700" letterSpacing="0.04em"
              opacity={Math.min(1, progress * 2)}>
          INFRAMIND CLOUD
        </text>
      </svg>
      <ReadoutPanel x={1380} y={500} title="Cloud ingest" items={[
        { k: "Total upload",    v: `${(progress * 8.1).toFixed(1)} GB`,           color: C.scan },
        { k: "Throughput",      v: `${Math.floor(180 + progress * 220)} MB/s`,    color: C.ok },
        { k: "Co-registration", v: progress > 0.5 ? "running" : "queued",         color: progress > 0.5 ? C.scan : C.med },
        { k: "Sources",         v: "4 / 4",                                       color: C.muted },
      ]}/>
    </div>
  );
}

function CloudShape() {
  return (
    <g>
      <path
        d="M -160 0 Q -180 -50 -120 -60 Q -110 -110 -40 -90 Q 0 -130 60 -100 Q 130 -110 140 -50 Q 200 -40 180 20 L -160 20 Z"
        fill={C.paper} stroke={C.ink} strokeWidth="1.4"/>
      {[-90, -30, 30, 90].map((x, i) => (
        <g key={i}>
          <rect x={x - 10} y="-30" width="20" height="38" fill={C.hairline} stroke={C.ink2} strokeWidth="0.6" rx="2"/>
          <line x1={x - 6} y1="-22" x2={x + 6} y2="-22" stroke={C.scanLite} strokeWidth="1"/>
          <line x1={x - 6} y1="-14" x2={x + 6} y2="-14" stroke={C.scanLite} strokeWidth="1" opacity="0.6"/>
          <line x1={x - 6} y1="-6"  x2={x + 6} y2="-6"  stroke={C.scanLite} strokeWidth="1" opacity="0.4"/>
        </g>
      ))}
    </g>
  );
}

function DataStream({ x, startY, endY, color, delay = 0, progress }) {
  const pktCount = 5;
  const packets = [];
  for (let i = 0; i < pktCount; i++) {
    const phase = ((progress * 2.4) + i / pktCount + delay) % 1;
    const y = startY - phase * (startY - endY);
    const opacity = phase < 0.1 ? phase * 10 : phase > 0.9 ? (1 - phase) * 10 : 1;
    packets.push({ y, opacity });
  }
  return (
    <g>
      <line x1={x} y1={startY} x2={x} y2={endY} stroke={color} strokeOpacity="0.2" strokeWidth="1.5"/>
      {packets.map((p, i) => (
        <rect key={i} x={x - 8} y={p.y - 3} width="16" height="6" fill={color}
              opacity={p.opacity * Math.min(1, progress * 3)} rx="1"/>
      ))}
    </g>
  );
}

Object.assign(window, {
  C, SCENES, TOTAL, FrameChrome, BackgroundGrid, ReadoutPanel,
  BridgeGround, BridgeSolid, BridgeWireframe,
  BR, P, polyPath, linePath, proj3, makeBridgePoints,
  SceneCapture, SceneUpload, AssetTitle,
});
