// V4c — Signal Terrain II
// Revision of v4a: calm pins (label → card on hover, edge-flip), project index rail,
// ←/→ gradient-descent tour, basin depth shading, contact links, compact NOW panel,
// personal ticker (live GitHub activity w/ project-stat fallback), paper/night themes.
// Mobile: forced-landscape "game map" mode — CSS-rotated wrapper in portrait,
// HUD corner chips (identity / index / now / contacts) instead of fixed panels, no ticker.

const V4C_THEMES = {
  paper: {
    bg: "#fafaf7",
    ink: "#111111",
    sub: "#222222",
    faint: "#777770",
    cardBg: "rgba(250,250,247,0.97)",
    panelBg: "rgba(250,250,247,0.92)",
    contourMinor: "rgba(0,0,0,0.14)",
    kwBorder: "#999999",
    kwInk: "#444444",
    tickerBg: "#111111",
    tickerFg: "#fafaf7",
    tickerDim: "#888888",
    halo: "#fafaf7",
    texture: true,
  },
  night: {
    bg: "#131412",
    ink: "#ecebe4",
    sub: "#d2d1ca",
    faint: "#8d8d86",
    cardBg: "rgba(24,25,23,0.97)",
    panelBg: "rgba(19,20,18,0.92)",
    contourMinor: "rgba(255,255,255,0.13)",
    kwBorder: "#56564f",
    kwInk: "#b5b4ac",
    tickerBg: "#0b0b0a",
    tickerFg: "#ecebe4",
    tickerDim: "#84847d",
    halo: "#131412",
    texture: false,
  },
};

function v4cRgba(hex, a) {
  const r = parseInt(hex.slice(1, 3), 16);
  const g = parseInt(hex.slice(3, 5), 16);
  const b = parseInt(hex.slice(5, 7), 16);
  return `rgba(${r},${g},${b},${a})`;
}

// Viewport: phones get a forced-landscape "game map" mode.
// isMobile = coarse pointer + small screen; rotated = mobile held in portrait.
function v4cUseViewport() {
  const get = () => {
    const w = window.innerWidth, h = window.innerHeight;
    const coarse = window.matchMedia && window.matchMedia("(pointer: coarse)").matches;
    const force = /[?&]mobile=1/.test(window.location.search); // preview/debug override
    const isMobile = force || (coarse && Math.min(w, h) < 720);
    return { vw: w, vh: h, isMobile, rotated: isMobile && h > w };
  };
  const [vp, setVp] = React.useState(get);
  React.useEffect(() => {
    const on = () => setVp(get());
    window.addEventListener("resize", on);
    window.addEventListener("orientationchange", on);
    return () => {
      window.removeEventListener("resize", on);
      window.removeEventListener("orientationchange", on);
    };
  }, []);
  return vp;
}

function V4CSignalTerrain({ accent = "#c2410c", mode = "paper", feed = true, enableKeys = true, onToggleMode = null }) {
  const data = window.ALI;
  const T = V4C_THEMES[mode] || V4C_THEMES.paper;
  const wrapRef = React.useRef(null);
  const canvasRef = React.useRef(null);

  const [cam, setCam] = React.useState({ x: 0, y: 0, z: 1 });
  const camRef = React.useRef(cam);
  camRef.current = cam;
  const [target, setTarget] = React.useState(null);
  const [hovered, setHovered] = React.useState(null);
  const [activePin, setActivePin] = React.useState(null);
  const [cursorWorld, setCursorWorld] = React.useState({ x: 0, y: 0 });
  const [isDragging, setIsDragging] = React.useState(false);
  const dragRef = React.useRef({ active: false, lastX: 0, lastY: 0, moved: 0 });

  // Theme + accent, readable from the rAF draw loop without re-mounting it
  const lookRef = React.useRef({ accent, T });
  lookRef.current = { accent, T };

  // Mobile "game map" mode: forced landscape + HUD chips instead of fixed panels
  const { vw, vh, isMobile, rotated } = v4cUseViewport();
  const rotRef = React.useRef(rotated);
  rotRef.current = rotated;
  const [openPanel, setOpenPanel] = React.useState(null); // 'id' | 'index' | 'now' | 'contacts'
  const togglePanel = (p) => setOpenPanel(prev => (prev === p ? null : p));

  // Measured wrapper size (local/unrotated coords) for pin placement
  const [size, setSize] = React.useState({ w: 1280, h: 820 });
  React.useEffect(() => {
    const measure = () => {
      const el = wrapRef.current;
      if (el) setSize({ w: el.offsetWidth, h: el.offsetHeight });
    };
    measure();
    window.addEventListener("resize", measure);
    window.addEventListener("orientationchange", measure);
    return () => {
      window.removeEventListener("resize", measure);
      window.removeEventListener("orientationchange", measure);
    };
  }, [rotated, vw, vh]);

  // Pointer → wrapper-local coords, accounting for the forced-landscape rotation
  const toLocal = (clientX, clientY) => {
    if (rotRef.current) return { x: clientY, y: window.innerWidth - clientX };
    const r = wrapRef.current.getBoundingClientRect();
    return { x: clientX - r.left, y: clientY - r.top };
  };

  // Pin positions in world coords. Eararchy re-seated away from the NOW panel.
  const pins = React.useMemo(() => [
    { p: data.projects[0], wx:  120, wy:   80, depth: -3.2 },   // Pythia (hero, center)
    { p: data.projects[1], wx:  300, wy: -260, depth: -2.7 },   // Pytheum (top-right)
    { p: data.projects[2], wx:  500, wy:  160, depth: -2.4 },   // Netforce (right)
    { p: data.projects[3], wx: -340, wy:  -10, depth: -2.0 },   // Eararchy (mid-left)
    { p: data.projects[4], wx:  280, wy:  300, depth: -1.8 },   // MoltShell (bottom-right)
    { p: data.projects[5], wx:  -80, wy:  330, depth: -1.6 },   // Muse2 (bottom)
    { p: data.projects[6], wx:  340, wy:  -50, depth: -2.2 },   // MCOP (right of center)
    { p: data.projects[7], wx:   40, wy: -290, depth: -2.1 },   // WorkstreamBench (top-center)
    { p: data.projects[8], wx: -300, wy:  180, depth: -2.6 },   // Batched QR (left-bottom, deep basin)
  ], [data]);

  // Tour order: deepest minimum first ("descend the gradient")
  const tourOrder = React.useMemo(
    () => [...pins].sort((a, b) => a.depth - b.depth),
    [pins]
  );
  const tourRef = React.useRef(-1);

  // Smooth camera tween toward target
  React.useEffect(() => {
    let raf;
    const tick = () => {
      if (target) {
        setCam(c => {
          const dx = target.x - c.x;
          const dy = target.y - c.y;
          const dz = target.z - c.z;
          const ease = 0.06;
          if (Math.hypot(dx, dy) < 0.4 && Math.abs(dz) < 0.002) {
            return { x: target.x, y: target.y, z: target.z };
          }
          return { x: c.x + dx * ease, y: c.y + dy * ease, z: c.z + dz * ease };
        });
      }
      raf = requestAnimationFrame(tick);
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, [target]);

  // Loss landscape draw — contours + basin depth shading + descent path
  React.useEffect(() => {
    const cv = canvasRef.current;
    const ctx = cv.getContext("2d");
    let raf, t = 0;
    const dpr = window.devicePixelRatio || 1;

    function resize() {
      // offsetWidth/Height = layout size, unaffected by the rotation transform
      cv.width = cv.offsetWidth * dpr;
      cv.height = cv.offsetHeight * dpr;
      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    }
    resize();

    function loss(x, y) {
      let v = 0.0;
      for (const pn of pins) {
        const dx = x - pn.wx;
        const dy = y - pn.wy;
        const r2 = dx * dx + dy * dy;
        v += pn.depth * Math.exp(-r2 / (180 * 180));
      }
      v += (x * x + y * y) / 900000;
      v += 0.18 * Math.sin(x * 0.005 + t * 0.004) * Math.cos(y * 0.005 - t * 0.003);
      return v;
    }

    const draw = () => {
      const { accent: ac, T: th } = lookRef.current;
      const r = { width: cv.offsetWidth, height: cv.offsetHeight };
      if (cv.width !== Math.round(r.width * dpr) || cv.height !== Math.round(r.height * dpr)) resize();
      ctx.clearRect(0, 0, r.width, r.height);
      const cx = r.width / 2;
      const cy = r.height / 2;
      const c = camRef.current;
      const proj = (wx, wy) => [(wx - c.x) * c.z + cx, (wy - c.y) * c.z + cy];

      // Basin depth shading — soft warm pools so minima read as depressions
      for (const pn of pins) {
        const [sx, sy] = proj(pn.wx, pn.wy);
        const rad = 210 * c.z;
        if (sx < -rad || sx > r.width + rad || sy < -rad || sy > r.height + rad) continue;
        const g = ctx.createRadialGradient(sx, sy, 0, sx, sy, rad);
        const peak = 0.11 * (-pn.depth / 3.2);
        g.addColorStop(0, v4cRgba(ac, peak));
        g.addColorStop(0.55, v4cRgba(ac, peak * 0.35));
        g.addColorStop(1, v4cRgba(ac, 0));
        ctx.fillStyle = g;
        ctx.beginPath();
        ctx.arc(sx, sy, rad, 0, Math.PI * 2);
        ctx.fill();
      }

      // Contours via marching squares
      const step = 8;
      const cols = Math.ceil(r.width / step) + 2;
      const rows = Math.ceil(r.height / step) + 2;
      const grid = new Float32Array(cols * rows);
      for (let j = 0; j < rows; j++) {
        for (let i = 0; i < cols; i++) {
          const sx = i * step;
          const sy = j * step;
          const wx = (sx - cx) / c.z + c.x;
          const wy = (sy - cy) / c.z + c.y;
          grid[j * cols + i] = loss(wx, wy);
        }
      }
      let mn = Infinity, mx = -Infinity;
      for (let k = 0; k < grid.length; k++) {
        if (grid[k] < mn) mn = grid[k];
        if (grid[k] > mx) mx = grid[k];
      }

      const levels = 22;
      for (let l = 0; l < levels; l++) {
        const v = mn + ((mx - mn) * l) / (levels - 1);
        const isMajor = l % 5 === 0;
        ctx.strokeStyle = isMajor ? v4cRgba(ac, 0.55) : th.contourMinor;
        ctx.lineWidth = isMajor ? 1.0 : 0.5;
        ctx.beginPath();

        for (let j = 0; j < rows - 1; j++) {
          for (let i = 0; i < cols - 1; i++) {
            const a = grid[j * cols + i];
            const b = grid[j * cols + (i + 1)];
            const cVal = grid[(j + 1) * cols + (i + 1)];
            const d = grid[(j + 1) * cols + i];
            let idx = 0;
            if (a > v) idx |= 1;
            if (b > v) idx |= 2;
            if (cVal > v) idx |= 4;
            if (d > v) idx |= 8;
            if (idx === 0 || idx === 15) continue;

            const x0 = i * step, y0 = j * step;
            const x1 = x0 + step, y1 = y0 + step;
            const lerp = (p1, p2, va, vb) => {
              const tt = (v - va) / (vb - va);
              return [p1[0] + (p2[0] - p1[0]) * tt, p1[1] + (p2[1] - p1[1]) * tt];
            };
            const A = [x0, y0], B = [x1, y0], C = [x1, y1], D = [x0, y1];
            const lerps = {
              ab: lerp(A, B, a, b),
              bc: lerp(B, C, b, cVal),
              cd: lerp(C, D, cVal, d),
              da: lerp(D, A, d, a),
            };
            const seg = (p, q) => { ctx.moveTo(p[0], p[1]); ctx.lineTo(q[0], q[1]); };
            switch (idx) {
              case 1: case 14: seg(lerps.da, lerps.ab); break;
              case 2: case 13: seg(lerps.ab, lerps.bc); break;
              case 3: case 12: seg(lerps.da, lerps.bc); break;
              case 4: case 11: seg(lerps.bc, lerps.cd); break;
              case 6: case 9:  seg(lerps.ab, lerps.cd); break;
              case 7: case 8:  seg(lerps.da, lerps.cd); break;
              case 5: seg(lerps.da, lerps.ab); seg(lerps.bc, lerps.cd); break;
              case 10: seg(lerps.ab, lerps.bc); seg(lerps.cd, lerps.da); break;
            }
          }
        }
        ctx.stroke();
      }

      // Gradient descent path drifting from cursor toward nearest pin
      const cw = cursorWorld;
      let gx = cw.x, gy = cw.y;
      ctx.beginPath();
      ctx.strokeStyle = v4cRgba(ac, 0.7);
      ctx.lineWidth = 1.2;
      const [psx, psy] = proj(gx, gy);
      ctx.moveTo(psx, psy);
      const eps = 1.5;
      for (let s = 0; s < 60; s++) {
        const lx1 = loss(gx + eps, gy);
        const lx0 = loss(gx - eps, gy);
        const ly1 = loss(gx, gy + eps);
        const ly0 = loss(gx, gy - eps);
        const ggx = (lx1 - lx0) / (2 * eps);
        const ggy = (ly1 - ly0) / (2 * eps);
        const norm = Math.hypot(ggx, ggy) + 1e-6;
        const stepSize = 8 / Math.max(norm, 0.001);
        gx -= ggx * stepSize;
        gy -= ggy * stepSize;
        const [sx, sy] = proj(gx, gy);
        ctx.lineTo(sx, sy);
      }
      ctx.stroke();

      t += 1;
      raf = requestAnimationFrame(draw);
    };
    draw();
    window.addEventListener("resize", resize);
    return () => { cancelAnimationFrame(raf); window.removeEventListener("resize", resize); };
  }, [pins, cursorWorld]);

  // Mouse → world coords + drag pan (local coords so forced-landscape stays correct)
  const handleMouse = (e) => {
    const { x: sx, y: sy } = toLocal(e.clientX, e.clientY);
    const el = wrapRef.current;
    const cx = el.offsetWidth / 2, cy = el.offsetHeight / 2;
    const c = camRef.current;
    setCursorWorld({
      x: (sx - cx) / c.z + c.x,
      y: (sy - cy) / c.z + c.y,
    });
    if (dragRef.current.active) {
      const dx = sx - dragRef.current.lastX;
      const dy = sy - dragRef.current.lastY;
      dragRef.current.lastX = sx;
      dragRef.current.lastY = sy;
      dragRef.current.moved += Math.abs(dx) + Math.abs(dy);
      setTarget(null);
      setCam(prev => ({ x: prev.x - dx / prev.z, y: prev.y - dy / prev.z, z: prev.z }));
    }
  };
  const handleMouseDown = (e) => {
    if (e.button !== 0) return;
    const { x, y } = toLocal(e.clientX, e.clientY);
    dragRef.current = { active: true, lastX: x, lastY: y, moved: 0 };
    setIsDragging(true);
  };
  const handleMouseUp = () => {
    dragRef.current.active = false;
    setIsDragging(false);
  };

  // Touch: single-finger pan, two-finger pinch zoom (all math in local coords)
  const touchRef = React.useRef({ mode: null, lastX: 0, lastY: 0, dist: 0, cx: 0, cy: 0, moved: 0 });
  const handleTouchStart = (e) => {
    setTarget(null);
    if (e.touches.length === 1) {
      const t = e.touches[0];
      const p = toLocal(t.clientX, t.clientY);
      touchRef.current = { mode: 'pan', lastX: p.x, lastY: p.y, moved: 0 };
    } else if (e.touches.length === 2) {
      const p1 = toLocal(e.touches[0].clientX, e.touches[0].clientY);
      const p2 = toLocal(e.touches[1].clientX, e.touches[1].clientY);
      touchRef.current = { mode: 'pinch', dist: Math.hypot(p2.x - p1.x, p2.y - p1.y), cx: (p1.x + p2.x)/2, cy: (p1.y + p2.y)/2, moved: 0 };
    }
  };
  const handleTouchMove = (e) => {
    if (touchRef.current.mode === 'pan' && e.touches.length === 1) {
      e.preventDefault();
      const t = e.touches[0];
      const p = toLocal(t.clientX, t.clientY);
      const dx = p.x - touchRef.current.lastX;
      const dy = p.y - touchRef.current.lastY;
      touchRef.current.lastX = p.x;
      touchRef.current.lastY = p.y;
      touchRef.current.moved += Math.abs(dx) + Math.abs(dy);
      setCam(prev => ({ x: prev.x - dx / prev.z, y: prev.y - dy / prev.z, z: prev.z }));
    } else if (touchRef.current.mode === 'pinch' && e.touches.length === 2) {
      e.preventDefault();
      const p1 = toLocal(e.touches[0].clientX, e.touches[0].clientY);
      const p2 = toLocal(e.touches[1].clientX, e.touches[1].clientY);
      const newDist = Math.hypot(p2.x - p1.x, p2.y - p1.y);
      const el = wrapRef.current;
      const sx = touchRef.current.cx, sy = touchRef.current.cy;
      const cx = el.offsetWidth / 2, cy = el.offsetHeight / 2;
      const c = camRef.current;
      const wxBefore = (sx - cx) / c.z + c.x;
      const wyBefore = (sy - cy) / c.z + c.y;
      const factor = newDist / touchRef.current.dist;
      const newZ = Math.max(0.5, Math.min(3.5, c.z * factor));
      const newX = wxBefore - (sx - cx) / newZ;
      const newY = wyBefore - (sy - cy) / newZ;
      setCam({ x: newX, y: newY, z: newZ });
      touchRef.current.dist = newDist;
    }
  };
  const handleTouchEnd = () => { touchRef.current.mode = null; };
  const handleWheel = (e) => {
    e.preventDefault();
    const { x: sx, y: sy } = toLocal(e.clientX, e.clientY);
    const el = wrapRef.current;
    const cx = el.offsetWidth / 2, cy = el.offsetHeight / 2;
    const c = camRef.current;
    const wxBefore = (sx - cx) / c.z + c.x;
    const wyBefore = (sy - cy) / c.z + c.y;
    const factor = Math.exp(-e.deltaY * 0.0015);
    const newZ = Math.max(0.5, Math.min(3.5, c.z * factor));
    const newX = wxBefore - (sx - cx) / newZ;
    const newY = wyBefore - (sy - cy) / newZ;
    setTarget(null);
    setCam({ x: newX, y: newY, z: newZ });
  };
  React.useEffect(() => {
    const el = wrapRef.current;
    if (!el) return;
    const onWheel = (e) => handleWheel(e);
    el.addEventListener('wheel', onWheel, { passive: false });
    window.addEventListener('mouseup', handleMouseUp);
    return () => {
      el.removeEventListener('wheel', onWheel);
      window.removeEventListener('mouseup', handleMouseUp);
    };
  }, []);

  const flyTo = (pin) => {
    setActivePin(pin.p.id);
    tourRef.current = tourOrder.indexOf(pin);
    setTarget({ x: pin.wx, y: pin.wy, z: 1.7 });
  };
  const resetCam = () => {
    setActivePin(null);
    tourRef.current = -1;
    setTarget({ x: 0, y: 0, z: 1 });
  };

  // ←/→ gradient-descent tour, Esc resets
  React.useEffect(() => {
    if (!enableKeys) return;
    const onKey = (e) => {
      if (e.key === 'Escape') { resetCam(); return; }
      if (e.key !== 'ArrowRight' && e.key !== 'ArrowLeft') return;
      e.preventDefault();
      const n = tourOrder.length;
      tourRef.current = e.key === 'ArrowRight'
        ? (tourRef.current + 1) % n
        : (tourRef.current - 1 + n) % n;
      flyTo(tourOrder[tourRef.current]);
    };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [enableKeys, tourOrder]);

  // World coords -> screen fraction (for placement + edge-flip)
  const pinScreenFrac = (pin) => {
    const sx = (pin.wx - cam.x) * cam.z + size.w / 2;
    const sy = (pin.wy - cam.y) * cam.z + size.h / 2;
    return { fx: sx / size.w, fy: sy / size.h };
  };

  const v4c = v4cStyles(T, accent);
  const m = isMobile ? v4cMobileStyles(T, accent) : null;
  const stop = (e) => e.stopPropagation();

  // Forced landscape: in portrait, lay the app out at vh×vw and rotate it 90°
  const rootStyle = rotated
    ? {
        ...v4c.root,
        position: "fixed", top: 0, left: 0,
        width: vh, height: vw,
        transform: "rotate(90deg) translateY(-100%)",
        transformOrigin: "top left",
      }
    : v4c.root;

  return (
    <div
      ref={wrapRef}
      style={{ ...rootStyle, cursor: isDragging ? 'grabbing' : 'grab' }}
      onMouseMove={handleMouse}
      onMouseDown={handleMouseDown}
      onTouchStart={handleTouchStart}
      onTouchMove={handleTouchMove}
      onTouchEnd={handleTouchEnd}
      onClick={() => {
        // Mobile: tapping empty map closes any open HUD panel / pin card
        if (!isMobile) return;
        if (dragRef.current.moved > 8 || touchRef.current.moved > 8) return;
        setOpenPanel(null);
        setActivePin(null);
      }}
    >
      {T.texture && <div style={v4c.paperTex}></div>}
      <canvas ref={canvasRef} style={v4c.canvas} />

      {/* Identity */}
      {!isMobile && (
      <div style={v4c.title} className="v4c-title">
        <h1 style={v4c.h1} className="v4c-h1">{data.name}</h1>
        <div style={v4c.subtitle}>
          {data.edu}<br/>
          <span style={{ color: accent }}>●</span> {data.status} · {data.location}
        </div>
      </div>
      )}

      {/* View controls */}
      {!isMobile && (
      <div style={v4c.topRight} className="v4c-topright">
        {onToggleMode && (
          <button style={v4c.resetBtn} onClick={onToggleMode}>
            {mode === "paper" ? "\u263E night" : "\u2600 paper"}
          </button>
        )}
        <button style={v4c.resetBtn} onClick={resetCam}>↺ reset view</button>
      </div>
      )}

      {/* Project index rail */}
      {!isMobile && (
      <div style={v4c.rail} className="v4c-rail">
        <div style={v4c.railHead}>/ INDEX</div>
        {pins.map((pin, i) => {
          const isActive = activePin === pin.p.id;
          return (
            <div
              key={pin.p.id}
              style={{ ...v4c.railItem, ...(isActive ? v4c.railItemActive : {}) }}
              onMouseEnter={(e) => { e.currentTarget.style.color = accent; }}
              onMouseLeave={(e) => { e.currentTarget.style.color = isActive ? accent : T.sub; }}
              onClick={() => flyTo(pin)}
            >
              <span style={v4c.railNum}>{String(i + 1).padStart(2, "0")}</span>
              <span style={v4c.railName}>{pin.p.name}</span>
              <span style={v4c.railYear}>{pin.p.year}</span>
            </div>
          );
        })}
        <div style={v4c.railHint}>← → tour · esc reset</div>
      </div>
      )}

      {/* Pins: dot + quiet label by default; card on hover/active, flipped near right edge */}
      {pins.map((pin) => {
        const isActive = activePin === pin.p.id;
        const isHover = hovered === pin.p.id;
        const open = isMobile ? isActive : (isHover || isActive); // mobile: closed until tapped
        const { fx } = pinScreenFrac(pin);
        const flip = fx > 0.58;
        return (
          <div
            key={pin.p.id}
            style={{
              ...v4c.pin,
              left: `${fx * 100}%`,
              top: `${(pinScreenFrac(pin).fy) * 100}%`,
              zIndex: open ? 13 : 8,
            }}
            onMouseEnter={() => setHovered(pin.p.id)}
            onMouseLeave={() => setHovered(null)}
            onClick={(e) => {
              if (dragRef.current.moved > 4 || touchRef.current.moved > 8) return;
              e.stopPropagation();
              setOpenPanel(null);
              flyTo(pin);
            }}
          >
            <div style={{ ...v4c.pinDot, ...(isActive ? v4c.pinDotActive : {}) }}></div>

            {!open && (
              <div style={{ ...v4c.pinLabel, ...(flip ? v4c.pinLabelFlip : {}) }} className="v4c-pin-label">
                {pin.p.name} <span style={{ color: T.faint }}>’{pin.p.year.slice(-2)}</span>
              </div>
            )}

            {open && (
              <div
                className="v4c-pin-card"
                style={{ ...v4c.pinCard, ...(flip ? v4c.pinCardFlip : {}), ...(isMobile ? { width: 200, padding: "8px 12px" } : {}) }}
              >
                <div style={v4c.pinHead}>
                  <span style={{ color: accent }}>·</span>
                  <span>{pin.p.year}</span>
                </div>
                <div style={v4c.pinName}>{pin.p.name}</div>
                <div style={v4c.pinTag}>{pin.p.tag}</div>
                <div style={v4c.pinSum}>{pin.p.summary}</div>
                <div style={v4c.pinKw}>
                  {pin.p.keywords.slice(0, 5).map(k => <span key={k} style={v4c.pinKwItem}>{k}</span>)}
                </div>
                <div style={v4c.pinStat}>{pin.p.stat} · {pin.p.stat2}</div>
                {(() => {
                  const links = pin.p.links || (pin.p.url ? [{ label: "open", href: pin.p.url }] : []);
                  if (!links.length) return null;
                  return (
                    <div style={v4c.pinLinks}>
                      {links.map(l => (
                        <a key={l.label}
                           href={l.href.startsWith("http") ? l.href : `https://${l.href}`}
                           target="_blank" rel="noopener noreferrer"
                           style={v4c.pinLinkChip}
                           onClick={(e) => e.stopPropagation()}>
                          {l.label} ↗
                        </a>
                      ))}
                    </div>
                  );
                })()}
              </div>
            )}
          </div>
        );
      })}

      {/* Now / experience — compact strip */}
      {!isMobile && (
      <div style={v4c.bottomLeft} className="v4c-bl">
        <div style={v4c.blHead}>/ NOW</div>
        {data.now.map((n, i) => <div key={i} style={v4c.blLine}>· {n}</div>)}
        <div style={{ ...v4c.blHead, marginTop: 12 }}>/ EXP · EDU</div>
        {data.experience.map((e, i) => <div key={`e${i}`} style={v4c.blLine}>{e.role}, {e.org} · {e.when}</div>)}
        {data.education.map((e, i) => <div key={`d${i}`} style={v4c.blLine}>{e.degree}, {e.school} · {e.when}</div>)}
      </div>
      )}

      {/* Contact links */}
      {!isMobile && (
      <div style={v4c.contacts} className="v4c-contacts">
        <a style={v4c.contactChip} href={`mailto:${data.email}`}>email</a>
        <a style={v4c.contactChip} href={`https://${data.links.github}`} target="_blank" rel="noopener noreferrer">github</a>
        <a style={v4c.contactChip} href={`https://${data.links.linkedin}`} target="_blank" rel="noopener noreferrer">linkedin</a>
        <a style={v4c.contactChip} href={`https://${data.links.x}`} target="_blank" rel="noopener noreferrer">x</a>
        <a style={v4c.contactChip} href={`https://${data.links.site}`} target="_blank" rel="noopener noreferrer">{data.links.site}</a>
      </div>
      )}

      {/* Mobile HUD — game-style corner chips, one panel open at a time */}
      {isMobile && (
        <React.Fragment>
          {/* top-left: identity */}
          <div style={m.tl} onClick={stop} onTouchStart={stop} onMouseDown={stop}>
            <button style={{ ...m.chip, ...(openPanel === 'id' ? m.chipActive : {}) }} onClick={() => togglePanel('id')}>
              {data.name.toUpperCase()}
            </button>
            {openPanel === 'id' && (
              <div style={m.panel}>
                <div style={m.mh1}>{data.name}</div>
                <div style={m.msub}>
                  {data.edu}<br/>
                  <span style={{ color: accent }}>●</span> {data.status} · {data.location}
                </div>
              </div>
            )}
          </div>

          {/* top-right: index + view controls */}
          <div style={m.tr} onClick={stop} onTouchStart={stop} onMouseDown={stop}>
            <button style={{ ...m.chip, ...(openPanel === 'index' ? m.chipActive : {}) }} onClick={() => togglePanel('index')}>
              ☰ index
            </button>
            {onToggleMode && (
              <button style={m.chip} onClick={onToggleMode}>{mode === "paper" ? "☾" : "☀"}</button>
            )}
            <button style={m.chip} onClick={() => { setOpenPanel(null); resetCam(); }}>↺</button>
            {openPanel === 'index' && (
              <div style={{ ...m.panel, ...m.panelRight }} onTouchMove={stop}>
                <div style={v4c.railHead}>/ INDEX</div>
                {pins.map((pin, i) => (
                  <div
                    key={pin.p.id}
                    style={{ ...v4c.railItem, ...(activePin === pin.p.id ? v4c.railItemActive : {}), padding: "5px 0" }}
                    onClick={() => { flyTo(pin); setOpenPanel(null); }}
                  >
                    <span style={v4c.railNum}>{String(i + 1).padStart(2, "0")}</span>
                    <span style={v4c.railName}>{pin.p.name}</span>
                    <span style={v4c.railYear}>{pin.p.year}</span>
                  </div>
                ))}
              </div>
            )}
          </div>

          {/* bottom-left: now / exp / edu */}
          <div style={m.bl} onClick={stop} onTouchStart={stop} onMouseDown={stop}>
            {openPanel === 'now' && (
              <div style={{ ...m.panel, ...m.panelUp }} onTouchMove={stop}>
                <div style={v4c.blHead}>/ NOW</div>
                {data.now.map((n, i) => <div key={i} style={v4c.blLine}>· {n}</div>)}
                <div style={{ ...v4c.blHead, marginTop: 10 }}>/ EXP · EDU</div>
                {data.experience.map((e, i) => <div key={`e${i}`} style={v4c.blLine}>{e.role}, {e.org} · {e.when}</div>)}
                {data.education.map((e, i) => <div key={`d${i}`} style={v4c.blLine}>{e.degree}, {e.school} · {e.when}</div>)}
              </div>
            )}
            <button style={{ ...m.chip, ...(openPanel === 'now' ? m.chipActive : {}) }} onClick={() => togglePanel('now')}>
              / now
            </button>
          </div>

          {/* bottom-right: contacts */}
          <div style={m.br} onClick={stop} onTouchStart={stop} onMouseDown={stop}>
            {openPanel === 'contacts' && (
              <div style={{ ...m.panel, ...m.panelUp, ...m.panelRight, display: "flex", flexDirection: "column", gap: 6, alignItems: "stretch" }}>
                <a style={v4c.contactChip} href={`mailto:${data.email}`}>email</a>
                <a style={v4c.contactChip} href={`https://${data.links.github}`} target="_blank" rel="noopener noreferrer">github</a>
                <a style={v4c.contactChip} href={`https://${data.links.linkedin}`} target="_blank" rel="noopener noreferrer">linkedin</a>
                <a style={v4c.contactChip} href={`https://${data.links.x}`} target="_blank" rel="noopener noreferrer">x</a>
                <a style={v4c.contactChip} href={`https://${data.links.site}`} target="_blank" rel="noopener noreferrer">{data.links.site}</a>
              </div>
            )}
            <button style={{ ...m.chip, ...(openPanel === 'contacts' ? m.chipActive : {}) }} onClick={() => togglePanel('contacts')}>
              @ contact
            </button>
          </div>
        </React.Fragment>
      )}

      {feed && !isMobile && <V4CFeedTicker accent={accent} T={T} data={data} />}
    </div>
  );
}

// Personal feed ticker — live GitHub public activity, project stats as fallback/filler
function V4CFeedTicker({ accent, T, data }) {
  const [gh, setGh] = React.useState(null); // null = loading, [] = failed

  React.useEffect(() => {
    let cancelled = false;
    (async () => {
      try {
        const events = await fetch(`https://api.github.com/users/${data.handle}/events/public`)
          .then(r => { if (!r.ok) throw new Error('http ' + r.status); return r.json(); });
        const rel = (iso) => {
          const s = (Date.now() - new Date(iso).getTime()) / 1000;
          if (s < 3600) return `${Math.max(1, Math.floor(s / 60))}m ago`;
          if (s < 86400) return `${Math.floor(s / 3600)}h ago`;
          return `${Math.floor(s / 86400)}d ago`;
        };
        const mapped = events.slice(0, 12).map(e => {
          const repo = e.repo ? e.repo.name : '';
          const when = rel(e.created_at);
          switch (e.type) {
            case 'PushEvent': {
              const n = (e.payload.commits || []).length || 1;
              return `pushed ${n} commit${n > 1 ? 's' : ''} → ${repo} · ${when}`;
            }
            case 'PullRequestEvent': return `${e.payload.action} PR → ${repo} · ${when}`;
            case 'CreateEvent': return `created ${e.payload.ref_type} → ${repo} · ${when}`;
            case 'WatchEvent': return `starred ${repo} · ${when}`;
            case 'ForkEvent': return `forked ${repo} · ${when}`;
            case 'IssuesEvent': return `${e.payload.action} issue → ${repo} · ${when}`;
            case 'IssueCommentEvent': return `commented → ${repo} · ${when}`;
            default: return `${e.type.replace('Event', '').toLowerCase()} → ${repo} · ${when}`;
          }
        });
        if (!cancelled) setGh(mapped);
      } catch (err) {
        if (!cancelled) setGh([]);
      }
    })();
    return () => { cancelled = true; };
  }, [data.handle]);

  const items = React.useMemo(() => {
    const stats = data.projects.map(p => ({ k: p.name.toUpperCase(), t: `${p.stat} · ${p.stat2}` }));
    const now = data.now.map(n => ({ k: 'NOW', t: n }));
    const ghItems = (gh || []).map(t => ({ k: 'GH', t }));
    // Interleave: github activity first, then now-lines, then project stats
    return [...ghItems, ...now, ...stats];
  }, [gh, data]);

  const s = v4cTickerStyles(T, accent);
  if (items.length === 0) return null;

  return (
    <div style={s.ticker}>
      <div style={s.tickerLabel}>FEED&nbsp;<span style={{ color: accent }}>●</span></div>
      <div style={s.tickerScroll}>
        <div style={s.tickerTrack}>
          {[...items, ...items].map((it, i) => (
            <span key={i} style={s.tickerItem}>
              <span style={{ color: accent, fontWeight: 700 }}>{it.k}</span>
              <span style={{ color: T.tickerFg }}>&nbsp;{it.t}</span>
              <span style={s.tickerSep}>&nbsp;//&nbsp;</span>
            </span>
          ))}
        </div>
      </div>
    </div>
  );
}

function v4cStyles(T, accent) {
  return {
    root: {
      width: "100%", height: "100%",
      background: T.bg,
      color: T.ink,
      position: "relative",
      overflow: "hidden",
      fontFamily: "'Inter', sans-serif",
      transition: "background 300ms ease, color 300ms ease",
    },
    paperTex: {
      position: "absolute", inset: 0,
      backgroundImage: "url('assets/sketch.png')",
      backgroundSize: "100% 100%",
      backgroundPosition: "center",
      backgroundRepeat: "no-repeat",
      opacity: 0.025,
      mixBlendMode: "multiply",
      pointerEvents: "none",
      filter: "blur(1.5px) contrast(1.1)",
    },
    canvas: { position: "absolute", inset: 0, width: "100%", height: "100%", pointerEvents: "none" },

    title: { position: "absolute", top: 48, left: 56, maxWidth: 480, zIndex: 10 },
    h1: {
      fontFamily: "'Newsreader', 'Source Serif 4', 'Georgia', serif",
      fontSize: 64, fontWeight: 400, fontStyle: "italic",
      lineHeight: 0.95, margin: "0 0 6px",
      letterSpacing: "-0.025em",
      color: T.ink,
    },
    subtitle: { fontSize: 13, color: T.sub, lineHeight: 1.55, marginTop: 6 },

    topRight: {
      position: "absolute", top: 40, right: 40, zIndex: 12,
      display: "flex", gap: 8,
    },
    resetBtn: {
      fontFamily: "'JetBrains Mono', monospace", fontSize: 11,
      padding: "6px 12px",
      background: T.panelBg,
      border: `1px solid ${T.ink}`,
      color: T.ink, cursor: "pointer",
      letterSpacing: "0.04em",
      whiteSpace: "nowrap",
    },

    rail: {
      position: "absolute", top: 92, right: 40, zIndex: 11,
      padding: "10px 14px",
      background: T.panelBg,
      border: `1px solid ${T.ink}`,
      minWidth: 180,
    },
    railHead: {
      fontFamily: "'JetBrains Mono', monospace", fontSize: 9,
      letterSpacing: "0.18em", fontWeight: 700, color: accent, marginBottom: 7,
    },
    railItem: {
      display: "flex", alignItems: "baseline", gap: 8,
      fontFamily: "'JetBrains Mono', monospace", fontSize: 11,
      color: T.sub, cursor: "pointer", padding: "2px 0",
      whiteSpace: "nowrap",
    },
    railItemActive: { color: accent, fontWeight: 700 },
    railNum: { fontSize: 9, color: T.faint, width: 16, flexShrink: 0 },
    railName: { flex: 1 },
    railYear: { fontSize: 9, color: T.faint },
    railHint: {
      fontFamily: "'JetBrains Mono', monospace", fontSize: 8.5,
      color: T.faint, marginTop: 8, letterSpacing: "0.06em",
    },

    pin: { position: "absolute", transform: "translate(-50%, -50%)", cursor: "pointer" },
    pinDot: {
      width: 10, height: 10, borderRadius: "50%",
      background: accent, border: `2px solid ${T.bg}`,
      boxShadow: `0 0 0 1px ${accent}, 0 0 18px ${v4cRgba(accent, 0.33)}`,
    },
    pinDotActive: { width: 14, height: 14, boxShadow: `0 0 0 2px ${accent}, 0 0 32px ${v4cRgba(accent, 0.53)}` },

    pinLabel: {
      position: "absolute", left: 16, top: -3,
      fontFamily: "'JetBrains Mono', monospace", fontSize: 10,
      letterSpacing: "0.04em", whiteSpace: "nowrap",
      color: T.ink,
      textShadow: `0 0 6px ${T.halo}, 0 0 2px ${T.halo}`,
      pointerEvents: "none",
    },
    pinLabelFlip: { left: "auto", right: 16, textAlign: "right" },

    pinCard: {
      position: "absolute", left: 18, top: 8,
      width: 270,
      padding: "10px 14px",
      background: T.cardBg,
      border: `1px solid ${accent}`,
      boxShadow: "0 14px 36px rgba(0,0,0,0.18)",
    },
    pinCardFlip: { left: "auto", right: 18 },
    pinHead: { display: "flex", justifyContent: "space-between", fontFamily: "'JetBrains Mono', monospace", fontSize: 9, color: T.faint, marginBottom: 4 },
    pinName: { fontFamily: "'Newsreader', serif", fontStyle: "italic", fontSize: 24, lineHeight: 1.05, letterSpacing: "-0.02em", color: T.ink },
    pinTag: { fontFamily: "'JetBrains Mono', monospace", fontSize: 10, color: T.faint, marginTop: 2 },
    pinSum: { fontSize: 12, color: T.sub, lineHeight: 1.5, marginTop: 8 },
    pinKw: { display: "flex", flexWrap: "wrap", gap: 4, marginTop: 8 },
    pinKwItem: { fontFamily: "'JetBrains Mono', monospace", fontSize: 9, padding: "1px 5px", border: `1px solid ${T.kwBorder}`, color: T.kwInk, whiteSpace: "nowrap" },
    pinStat: { fontFamily: "'JetBrains Mono', monospace", fontSize: 10, color: accent, marginTop: 8, fontWeight: 600 },
    pinLinks: { display: "flex", flexWrap: "wrap", gap: 6, marginTop: 9 },
    pinLinkChip: {
      fontFamily: "'JetBrains Mono', monospace", fontSize: 9,
      padding: "2px 8px", border: `1px solid ${accent}`,
      color: accent, textDecoration: "none", letterSpacing: "0.04em",
      cursor: "pointer", whiteSpace: "nowrap",
    },

    bottomLeft: {
      position: "absolute", bottom: 56, left: 56,
      maxWidth: 330, zIndex: 10,
      padding: "10px 12px",
      background: T.panelBg,
      border: `1px solid ${T.ink}`,
    },
    blHead: { fontFamily: "'JetBrains Mono', monospace", fontSize: 9, letterSpacing: "0.18em", fontWeight: 700, color: accent, marginBottom: 5 },
    blLine: { fontSize: 11, color: T.sub, lineHeight: 1.45, marginTop: 2 },

    contacts: {
      position: "absolute", bottom: 56, right: 40, zIndex: 10,
      display: "flex", gap: 6,
    },
    contactChip: {
      fontFamily: "'JetBrains Mono', monospace", fontSize: 10,
      padding: "4px 10px",
      background: T.panelBg,
      border: `1px solid ${T.ink}`,
      color: T.ink, textDecoration: "none",
      letterSpacing: "0.04em",
      cursor: "pointer",
    },
  };
}

// Mobile HUD: corner chips + pop-over panels, game-style
function v4cMobileStyles(T, accent) {
  const corner = { position: "absolute", zIndex: 30 };
  return {
    tl: { ...corner, top: 10, left: 12 },
    tr: { ...corner, top: 10, right: 12, display: "flex", gap: 6 },
    bl: { ...corner, bottom: 12, left: 12 },
    br: { ...corner, bottom: 12, right: 12 },
    chip: {
      fontFamily: "'JetBrains Mono', monospace", fontSize: 10,
      padding: "7px 11px",
      background: T.panelBg,
      border: `1px solid ${T.ink}`,
      color: T.ink, cursor: "pointer",
      letterSpacing: "0.06em",
      whiteSpace: "nowrap",
    },
    chipActive: { borderColor: accent, color: accent, fontWeight: 700 },
    panel: {
      position: "absolute", top: "calc(100% + 8px)", left: 0,
      width: "max-content",
      maxWidth: "min(72vmax, 420px)",
      maxHeight: "min(62vmin, 380px)",
      overflowY: "auto",
      padding: "10px 14px",
      background: T.cardBg,
      border: `1px solid ${T.ink}`,
      boxShadow: "0 14px 36px rgba(0,0,0,0.18)",
    },
    panelRight: { left: "auto", right: 0 },
    panelUp: { top: "auto", bottom: "calc(100% + 8px)" },
    mh1: {
      fontFamily: "'Newsreader', 'Source Serif 4', 'Georgia', serif",
      fontSize: 26, fontWeight: 400, fontStyle: "italic",
      lineHeight: 1, letterSpacing: "-0.025em", color: T.ink,
    },
    msub: { fontSize: 11, color: T.sub, lineHeight: 1.55, marginTop: 6 },
  };
}

function v4cTickerStyles(T, accent) {
  return {
    ticker: {
      position: "absolute", bottom: 0, left: 0, right: 0,
      height: 36, zIndex: 11,
      background: T.tickerBg, color: T.tickerFg,
      display: "flex", alignItems: "center",
      fontFamily: "'JetBrains Mono', monospace", fontSize: 11,
      overflow: "hidden",
      borderTop: `1px solid ${accent}`,
    },
    tickerLabel: {
      flexShrink: 0,
      padding: "0 14px",
      fontWeight: 700, letterSpacing: "0.08em",
      color: T.tickerFg,
      borderRight: "1px solid rgba(128,128,128,0.35)",
      height: "100%",
      display: "flex", alignItems: "center",
      fontSize: 10,
    },
    tickerScroll: { flex: 1, overflow: "hidden", height: "100%" },
    tickerTrack: {
      display: "inline-flex",
      whiteSpace: "nowrap",
      animation: "v4cTickerScroll 70s linear infinite",
      height: "100%",
      alignItems: "center",
    },
    tickerItem: { padding: "0 8px", color: T.tickerDim },
    tickerSep: { color: "rgba(128,128,128,0.5)", padding: "0 6px" },
  };
}

// Keyframes + responsive rules
(function() {
  if (document.getElementById("v4c-keyframes")) return;
  const s = document.createElement("style");
  s.id = "v4c-keyframes";
  s.textContent = `
    @keyframes v4cTickerScroll { from { transform: translateX(0); } to { transform: translateX(-50%); } }
  `;
  document.head.appendChild(s);
})();

window.V4CSignalTerrain = V4CSignalTerrain;
