// V4a — Signal Terrain
// The page IS a 2D loss landscape / log-likelihood surface.
// Contours = level sets. Projects sit at local minima ("solutions found").
// Camera flies to pins, compass HUD, mini-map, dota meta ticker.

const V4A_ACCENT = "#c2410c"; // burnt sienna

function V4ASignalTerrain() {
  const data = window.ALI;
  const wrapRef = React.useRef(null);
  const canvasRef = React.useRef(null);
  const minimapRef = React.useRef(null);

  // Camera: pan offset (px) and zoom; world coords map to pin lat/lng.
  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 });

  // World extent (logical units, projected into canvas px in draw)
  const W = 1280, H = 820;

  // Pin positions in world coords (centered around 0)
  // Each pin = a local minimum of an objective function.
  const pins = React.useMemo(() => [
    { p: data.projects[0], wx:  120, wy:   80, depth: -3.2, theta: 0.0 },   // Pythia (center-right, hero pin)
    { p: data.projects[1], wx:  430, wy: -200, depth: -2.7, theta: 0.0 },   // QuaRot (top-right)
    { p: data.projects[2], wx:  500, wy:  160, depth: -2.4, theta: 0.0 },   // Netforce (right)
    { p: data.projects[3], wx: -300, wy:  220, depth: -2.0, theta: 0.0 },   // Eararchy (mid-left, below title)
    { p: data.projects[4], wx:  280, wy:  300, depth: -1.8, theta: 0.0 },   // MoltShell (bottom-right)
    { p: data.projects[5], wx:  -80, wy:  340, depth: -1.6, theta: 0.0 },   // Muse2 (bottom)
    { p: data.projects[6], wx:  340, wy:  -50, depth: -2.2, theta: 0.0 },   // MCOP (right of center)
  ], [data]);

  // 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 — sum of inverted gaussians at each pin
  React.useEffect(() => {
    const cv = canvasRef.current;
    const ctx = cv.getContext("2d");
    let raf, t = 0;
    const dpr = window.devicePixelRatio || 1;

    function resize() {
      const r = cv.getBoundingClientRect();
      cv.width = r.width * dpr;
      cv.height = r.height * dpr;
      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    }
    resize();

    function loss(x, y) {
      // Sum of negative gaussians at pins → bowl-shaped basins
      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));
      }
      // Add gentle global bowl + low-freq noise so contours undulate
      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 r = cv.getBoundingClientRect();
      ctx.clearRect(0, 0, r.width, r.height);
      const cx = r.width / 2;
      const cy = r.height / 2;
      const c = camRef.current;

      // World-to-screen: (wx,wy) -> ((wx - cam.x)*z + cx, (wy - cam.y)*z + cy)

      // Sample contour lines using marching squares on a grid
      const step = 8;                 // grid resolution (px in screen space)
      const cols = Math.ceil(r.width / step) + 2;
      const rows = Math.ceil(r.height / step) + 2;

      // Pre-compute loss values
      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);
        }
      }

      // Find min/max
      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];
      }

      // Draw contours at quantized levels
      const levels = 22;
      ctx.lineWidth = 0.6;
      for (let l = 0; l < levels; l++) {
        const v = mn + ((mx - mn) * l) / (levels - 1);
        const isMajor = l % 5 === 0;
        ctx.strokeStyle = isMajor
          ? "rgba(194,65,12,0.55)"
          : "rgba(0,0,0,0.14)";
        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];
            // marching squares index
            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 t = (v - va) / (vb - va);
              return [p1[0] + (p2[0] - p1[0]) * t, p1[1] + (p2[1] - p1[1]) * t];
            };
            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 = "rgba(194,65,12,0.7)";
      ctx.lineWidth = 1.2;
      const proj = (wx, wy) => [(wx - c.x) * c.z + cx, (wy - c.y) * c.z + cy];
      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]);

  // Mini-map draw
  React.useEffect(() => {
    const cv = minimapRef.current;
    if (!cv) return;
    const ctx = cv.getContext("2d");
    const dpr = window.devicePixelRatio || 1;
    const r = cv.getBoundingClientRect();
    cv.width = r.width * dpr;
    cv.height = r.height * dpr;
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    ctx.clearRect(0, 0, r.width, r.height);

    // World extent for minimap: -700..700 maps to 0..r.width
    const wExt = 1400;
    const w2m = (wx, wy) => [
      (wx + wExt / 2) / wExt * r.width,
      (wy + wExt / 2) / wExt * r.height,
    ];

    // bg
    ctx.fillStyle = "rgba(255,255,255,0.6)";
    ctx.fillRect(0, 0, r.width, r.height);
    // faint contours
    ctx.strokeStyle = "rgba(0,0,0,0.18)";
    ctx.lineWidth = 0.5;
    for (let i = 0; i < 7; i++) {
      ctx.beginPath();
      ctx.arc(r.width/2, r.height/2, 8 + i * 9, 0, Math.PI * 2);
      ctx.stroke();
    }
    // pins
    pins.forEach(pn => {
      const [mx, my] = w2m(pn.wx, pn.wy);
      ctx.beginPath();
      ctx.arc(mx, my, 2.2, 0, Math.PI * 2);
      ctx.fillStyle = V4A_ACCENT;
      ctx.fill();
    });
    // viewport rect
    const W = 1280, H = 820;
    const c = cam;
    const vx0 = c.x - (W / 2) / c.z;
    const vy0 = c.y - (H / 2) / c.z;
    const vx1 = c.x + (W / 2) / c.z;
    const vy1 = c.y + (H / 2) / c.z;
    const [m0x, m0y] = w2m(vx0, vy0);
    const [m1x, m1y] = w2m(vx1, vy1);
    ctx.strokeStyle = V4A_ACCENT;
    ctx.lineWidth = 1.2;
    ctx.strokeRect(m0x, m0y, m1x - m0x, m1y - m0y);
  }, [cam, pins]);

  // Mouse → world coords + drag pan
  const handleMouse = (e) => {
    const r = wrapRef.current.getBoundingClientRect();
    const sx = e.clientX - r.left;
    const sy = e.clientY - r.top;
    const cx = r.width / 2, cy = r.height / 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 = e.clientX - dragRef.current.lastX;
      const dy = e.clientY - dragRef.current.lastY;
      dragRef.current.lastX = e.clientX;
      dragRef.current.lastY = e.clientY;
      dragRef.current.moved += Math.abs(dx) + Math.abs(dy);
      setTarget(null); // cancel any in-flight tween
      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;
    dragRef.current = { active: true, lastX: e.clientX, lastY: e.clientY, moved: 0 };
    setIsDragging(true);
  };
  const handleMouseUp = () => {
    dragRef.current.active = false;
    setIsDragging(false);
  };
  const handleWheel = (e) => {
    e.preventDefault();
    const r = wrapRef.current.getBoundingClientRect();
    const sx = e.clientX - r.left;
    const sy = e.clientY - r.top;
    const cx = r.width / 2, cy = r.height / 2;
    const c = camRef.current;
    // World point under cursor before zoom
    const wxBefore = (sx - cx) / c.z + c.x;
    const wyBefore = (sy - cy) / c.z + c.y;
    // Zoom step
    const factor = Math.exp(-e.deltaY * 0.0015);
    const newZ = Math.max(0.5, Math.min(3.5, c.z * factor));
    // Re-anchor: keep cursor world point fixed
    const newX = wxBefore - (sx - cx) / newZ;
    const newY = wyBefore - (sy - cy) / newZ;
    setTarget(null);
    setCam({ x: newX, y: newY, z: newZ });
  };
  // Wheel listener — must be non-passive to call preventDefault
  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);
    setTarget({ x: pin.wx, y: pin.wy, z: 1.7 });
  };
  const resetCam = () => {
    setActivePin(null);
    setTarget({ x: 0, y: 0, z: 1 });
  };

  // Project pin world coords -> screen %
  const projPin = (pin) => {
    const r = { width: 1280, height: 820 };
    const cx = r.width / 2, cy = r.height / 2;
    const sx = (pin.wx - cam.x) * cam.z + cx;
    const sy = (pin.wy - cam.y) * cam.z + cy;
    return { left: `${(sx / r.width) * 100}%`, top: `${(sy / r.height) * 100}%` };
  };

  const bearing = (() => {
    const ang = Math.atan2(cam.x, -cam.y) * 180 / Math.PI;
    return ((ang + 360) % 360).toFixed(1);
  })();

  return (
    <div
      ref={wrapRef}
      style={{...v4a.root, cursor: isDragging ? 'grabbing' : 'grab'}}
      onMouseMove={handleMouse}
      onMouseDown={handleMouseDown}
    >
      <div style={v4a.paperTex}></div>
      <canvas ref={canvasRef} style={v4a.canvas} />

      {/* Identity */}
      <div style={v4a.title}>
        <h1 style={v4a.h1}>{data.name}</h1>
        <div style={v4a.subtitle}>
          {data.edu}<br/>
          <span style={{color: V4A_ACCENT}}>●</span> {data.status} · {data.location}
        </div>
      </div>

      {/* Reset button — small, top-right */}
      <button style={v4a.resetBtn} onClick={resetCam}>↺ reset view</button>

      {/* Pins */}
      {pins.map((pin) => {
        const isActive = activePin === pin.p.id;
        const isHover = hovered === pin.p.id;
        return (
          <div
            key={pin.p.id}
            style={{...v4a.pin, ...projPin(pin)}}
            onMouseEnter={() => setHovered(pin.p.id)}
            onMouseLeave={() => setHovered(null)}
            onClick={(e) => {
              if (dragRef.current.moved > 4) return; // suppress click after drag
              e.stopPropagation();
              flyTo(pin);
            }}
          >
            <div style={{...v4a.pinDot, ...(isActive?v4a.pinDotActive:{})}}>
            </div>
            <div style={{
              ...v4a.pinCard,
              ...((isHover || isActive) ? v4a.pinCardOpen : {}),
              opacity: (isHover || isActive) ? 1 : 0.85,
            }}>
              <div style={v4a.pinHead}>
                <span style={v4a.pinCoord}>·</span>
                <span>{pin.p.year}</span>
              </div>
              <div style={v4a.pinName}>{pin.p.name}</div>
              <div style={v4a.pinTag}>— {pin.p.tag}</div>
              {(isHover || isActive) && (
                <>
                  <div style={v4a.pinSum}>{pin.p.summary}</div>
                  <div style={v4a.pinKw}>
                    {pin.p.keywords.slice(0,5).map(k => <span key={k} style={v4a.pinKwItem}>{k}</span>)}
                  </div>
                  <div style={v4a.pinStat}>{pin.p.stat} · {pin.p.stat2}</div>
                </>
              )}
            </div>
          </div>
        );
      })}

      {/* Now / experience — anchored to viewport not the world */}
      <div style={v4a.bottomLeft}>
        <div style={v4a.blHead}>/ NOW</div>
        {data.now.map((n,i) => <div key={i} style={v4a.blLine}>— {n}</div>)}
        <div style={{...v4a.blHead, marginTop: 14}}>/ EXP · EDU</div>
        {data.experience.map((e,i) => <div key={`e${i}`} style={v4a.blLine}>{e.role}, {e.org} · {e.when}</div>)}
        {data.education.map((e,i) => <div key={`d${i}`} style={v4a.blLine}>{e.degree}, {e.school} · {e.when}</div>)}
      </div>

      {/* Live news ticker */}
      <V4ANewsTicker />
    </div>
  );
}

// Live news ticker — pulls top stories from Hacker News API (no key, CORS-friendly)
function V4ANewsTicker() {
  const [items, setItems] = React.useState([]);
  React.useEffect(() => {
    let cancelled = false;
    (async () => {
      try {
        const ids = await fetch('https://hacker-news.firebaseio.com/v0/topstories.json').then(r => r.json());
        const top = ids.slice(0, 18);
        const stories = await Promise.all(top.map(id =>
          fetch(`https://hacker-news.firebaseio.com/v0/item/${id}.json`).then(r => r.json())
        ));
        if (!cancelled) {
          setItems(stories.filter(s => s && s.title).map(s => ({
            title: s.title,
            score: s.score || 0,
            comments: s.descendants || 0,
            url: s.url || `https://news.ycombinator.com/item?id=${s.id}`,
            site: (() => { try { return new URL(s.url).hostname.replace(/^www\./,''); } catch { return 'news.ycombinator.com'; } })(),
          })));
        }
      } catch (e) {
        // Fallback if offline
        if (!cancelled) setItems([
          { title: 'Loading top stories from Hacker News...', score: 0, comments: 0, url: '#', site: 'news.ycombinator.com' },
        ]);
      }
    })();
    return () => { cancelled = true; };
  }, []);
  if (items.length === 0) {
    return (
      <div style={v4a.ticker}>
        <div style={v4a.tickerLabel}>HN&nbsp;·&nbsp;TOP&nbsp;<span style={{color:V4A_ACCENT}}>●</span></div>
        <div style={{...v4a.tickerScroll, padding:'0 14px', color:'#888', display:'flex', alignItems:'center'}}>fetching feed…</div>
      </div>
    );
  }
  return (
    <div style={v4a.ticker}>
      <div style={v4a.tickerLabel}>HN&nbsp;·&nbsp;TOP&nbsp;<span style={{color:V4A_ACCENT}}>●</span></div>
      <div style={v4a.tickerScroll}>
        <div style={v4a.tickerTrack}>
          {[...items, ...items].map((it, i) => (
            <a key={i} href={it.url} target="_blank" rel="noopener noreferrer" style={v4a.tickerItem}>
              <span style={{color:V4A_ACCENT, fontWeight:700}}>▲ {it.score}</span>&nbsp;
              <span style={{color:'#fafaf7'}}>{it.title}</span>&nbsp;
              <span style={{color:'#888'}}>({it.site})</span>&nbsp;
              <span style={{color:'#666'}}>· {it.comments} comments</span>
              <span style={v4a.tickerSep}>&nbsp;//&nbsp;</span>
            </a>
          ))}
        </div>
      </div>
    </div>
  );
}

const v4a = {
  root: {
    width: "100%", height: "100%",
    background: "#fafaf7",
    color: "#111",
    position: "relative",
    overflow: "hidden",
    fontFamily: "'Inter', sans-serif",
    cursor: "crosshair",
  },
  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: "#111",
  },
  subtitle: { fontSize: 13, color: "#222", lineHeight: 1.55, marginTop: 6 },

  resetBtn: {
    position: "absolute", top: 40, right: 40, zIndex: 10,
    fontFamily: "'JetBrains Mono', monospace", fontSize: 11,
    padding: "6px 12px",
    background: "rgba(250,250,247,0.92)",
    border: "1px solid #111",
    color: "#111", cursor: "pointer",
    letterSpacing: "0.04em",
  },

  pin: { position: "absolute", zIndex: 8, transform: "translate(-50%, -50%)", cursor: "pointer" },
  pinDot: { width: 10, height: 10, borderRadius: "50%", background: V4A_ACCENT, border: "2px solid #fafaf7", boxShadow: `0 0 0 1px ${V4A_ACCENT}, 0 0 18px ${V4A_ACCENT}55` },
  pinDotActive: { width: 14, height: 14, boxShadow: `0 0 0 2px ${V4A_ACCENT}, 0 0 32px ${V4A_ACCENT}88` },
  pinCard: {
    position: "absolute", left: 18, top: 8,
    minWidth: 220, maxWidth: 320,
    padding: "10px 14px",
    background: "rgba(250,250,247,0.97)",
    border: "1px solid #111",
    boxShadow: "0 4px 14px rgba(0,0,0,0.08)",
    transition: "all 200ms ease",
  },
  pinCardOpen: { boxShadow: "0 14px 36px rgba(0,0,0,0.18)", borderColor: V4A_ACCENT },
  pinHead: { display: "flex", justifyContent: "space-between", fontFamily: "'JetBrains Mono', monospace", fontSize: 9, color: "#888", marginBottom: 4 },
  pinCoord: { color: V4A_ACCENT },
  pinName: { fontFamily: "'Newsreader', serif", fontStyle: "italic", fontSize: 24, lineHeight: 1.05, letterSpacing: "-0.02em" },
  pinTag: { fontFamily: "'JetBrains Mono', monospace", fontSize: 10, color: "#666", marginTop: 2 },
  pinSum: { fontSize: 12, color: "#222", lineHeight: 1.5, marginTop: 8, maxWidth: 280 },
  pinKw: { display: "flex", flexWrap: "wrap", gap: 4, marginTop: 8 },
  pinKwItem: { fontFamily: "'JetBrains Mono', monospace", fontSize: 9, padding: "1px 5px", border: "1px solid #999", color: "#444" },
  pinStat: { fontFamily: "'JetBrains Mono', monospace", fontSize: 10, color: V4A_ACCENT, marginTop: 8, fontWeight: 600 },

  bottomLeft: {
    position: "absolute", bottom: 70, left: 64,
    maxWidth: 360, zIndex: 10,
    padding: "12px 14px",
    background: "rgba(250,250,247,0.9)",
    border: "1px solid #111",
  },
  blHead: { fontFamily: "'JetBrains Mono', monospace", fontSize: 9, letterSpacing: "0.18em", fontWeight: 700, color: V4A_ACCENT, marginBottom: 6 },
  blLine: { fontSize: 12, color: "#222", lineHeight: 1.5, marginTop: 2 },

  ticker: {
    position: "absolute", bottom: 0, left: 0, right: 0,
    height: 36, zIndex: 11,
    background: "#111", color: "#fafaf7",
    display: "flex", alignItems: "center",
    fontFamily: "'JetBrains Mono', monospace", fontSize: 11,
    overflow: "hidden",
    borderTop: `1px solid ${V4A_ACCENT}`,
  },
  tickerLabel: {
    flexShrink: 0,
    padding: "0 14px",
    fontWeight: 700, letterSpacing: "0.08em",
    color: "#fafaf7",
    borderRight: "1px solid #333",
    height: "100%",
    display: "flex", alignItems: "center",
    fontSize: 10,
  },
  tickerScroll: { flex: 1, overflow: "hidden", height: "100%" },
  tickerTrack: {
    display: "inline-flex",
    whiteSpace: "nowrap",
    animation: "v4aTickerScroll 60s linear infinite",
    height: "100%",
    alignItems: "center",
  },
  tickerItem: { padding: "0 8px", color: "#bbb" },
  tickerSep: { color: "#444", padding: "0 6px" },

  legend: {
    position: "absolute",
    top: 200, right: 40,
    padding: "10px 14px",
    background: "rgba(250,250,247,0.92)",
    border: "1px solid #111",
    fontSize: 11, zIndex: 10, maxWidth: 240,
  },
  legendH: { fontFamily: "'JetBrains Mono', monospace", fontSize: 9, letterSpacing: "0.18em", fontWeight: 700, color: V4A_ACCENT, marginBottom: 8 },
  lItem: { display: "flex", alignItems: "center", gap: 8, marginTop: 4, color: "#444", fontSize: 11 },
  lDot: { width: 8, height: 8, borderRadius: "50%", display: "inline-block" },
  lLine: { width: 18, height: 2, background: V4A_ACCENT, display: "inline-block" },
  lContour: { width: 18, height: 6, borderTop: "1px solid #000", borderBottom: "1px solid #000", display: "inline-block" },
};

// Inject keyframe for ticker
(function() {
  if (document.getElementById("v4a-keyframes")) return;
  const s = document.createElement("style");
  s.id = "v4a-keyframes";
  s.textContent = `@keyframes v4aTickerScroll { from { transform: translateX(0); } to { transform: translateX(-50%); } }`;
  document.head.appendChild(s);
})();

window.V4ASignalTerrain = V4ASignalTerrain;
