/* global React */
const { useEffect, useMemo, useRef, useState } = React;

/* ============================================================
   Logo glyph — abstract layered-data tree
   ============================================================ */
window.RevGlyph = function RevGlyph({ size = 36 }) {
  return (
    <img
      src="assets/logo-glyph.png"
      alt="Revolute Systems"
      width={size}
      height={size}
      style={{ display: 'block', width: size, height: size, objectFit: 'contain' }}
    />
  );
};

/* Wordmark with tagline — for footer (light) and hero/centerpiece (dark) */
window.RevWordmark = function RevWordmark({ variant = 'light', height = 48 }) {
  const src = variant === 'dark'
    ? 'assets/logo-dark-stacked.png'
    : 'assets/logo-light.png';
  return (
    <img
      src={src}
      alt="Revolute Systems — Making precision practical"
      style={{ display: 'block', height, width: 'auto', objectFit: 'contain' }}
    />
  );
};

/* ============================================================
   Satellite / soil-scan map — full-bleed hero visual
   Generates a procedural orchard-block map with NDVI-style
   color cells. Animated scan-line passes over.
   ============================================================ */
window.SatelliteMap = function SatelliteMap({ accent = '#83B535', mode = 'ndvi', seed: seedOffset }) {
  // Pick a random offset once per mount unless one is provided
  const offset = useMemo(() => seedOffset ?? Math.floor(Math.random() * 100000), [seedOffset]);
  // Unique ID per instance to avoid duplicate SVG defs IDs
  const uid = useMemo(() => Math.random().toString(36).slice(2, 7), []);

  // Pick a feature block label that varies per load
  const featureBlock = useMemo(() => {
    const blocks = [
      { name: 'B-04 · GALA',         ha: '12.4 ha', ndvi: '0.71', canopy: '84%' },
      { name: 'A-12 · BRAEBURN',     ha: '8.2 ha',  ndvi: '0.66', canopy: '78%' },
      { name: 'C-07 · PACKHAMS',     ha: '15.1 ha', ndvi: '0.74', canopy: '88%' },
      { name: 'D-02 · CRIPPS PINK',  ha: '9.6 ha',  ndvi: '0.69', canopy: '81%' },
      { name: 'E-09 · ROSY GLOW',    ha: '11.0 ha', ndvi: '0.72', canopy: '85%' },
      { name: 'F-15 · FORELLE',      ha: '6.8 ha',  ndvi: '0.64', canopy: '76%' },
      { name: 'B-21 · GOLDEN DEL.',  ha: '13.5 ha', ndvi: '0.70', canopy: '83%' },
      { name: 'A-03 · FUJI',         ha: '10.2 ha', ndvi: '0.73', canopy: '86%' },
    ];
    return blocks[offset % blocks.length];
  }, [offset]);

  // Generate a grid of orchard "blocks" of varying shapes, varied by seed
  const blocks = useMemo(() => {
    const seed = (n) => {
      let x = Math.sin((n + offset) * 9301 + 49297) * 233280;
      return x - Math.floor(x);
    };
    const arr = [];
    let id = 0;
    // Organic block shapes by combining adjacent cells
    const cols = 24, rows = 14;
    const grid = Array.from({length: rows}, () => Array(cols).fill(null));
    let blockId = 0;
    for (let r = 0; r < rows; r++) {
      for (let c = 0; c < cols; c++) {
        if (grid[r][c] !== null) continue;
        const w = Math.floor(seed(id++) * 4) + 2;
        const h = Math.floor(seed(id++) * 3) + 2;
        const ndvi = seed(id++);
        for (let dr = 0; dr < h && r+dr < rows; dr++) {
          for (let dc = 0; dc < w && c+dc < cols; dc++) {
            if (grid[r+dr][c+dc] === null) grid[r+dr][c+dc] = blockId;
          }
        }
        arr.push({ id: blockId, r, c, w, h, ndvi });
        blockId++;
      }
    }
    return arr;
  }, [offset]);

  // NDVI color ramp (low = red/brown, high = bright green)
  const ndviColor = (v) => {
    if (v < 0.25) return '#5A3B1F';
    if (v < 0.40) return '#8C6232';
    if (v < 0.55) return '#A89548';
    if (v < 0.70) return '#7A9C32';
    if (v < 0.85) return '#5C9A2D';
    return '#2D7A1F';
  };

  return (
    <svg viewBox="0 0 1200 700" preserveAspectRatio="xMidYMid slice" role="img" aria-labelledby="satMapTitle" style={{ width: '100%', height: '100%' }}><title id="satMapTitle">Satellite NDVI map of an orchard block</title>
      <defs>
        <pattern id={`grid-${uid}`} width="40" height="40" patternUnits="userSpaceOnUse">
          <path d="M 40 0 L 0 0 0 40" fill="none" stroke="rgba(255,255,255,0.04)" strokeWidth="0.5"/>
        </pattern>
        <linearGradient id={`scanline-${uid}`} x1="0" x2="0" y1="0" y2="1">
          <stop offset="0%" stopColor="rgba(131,181,53,0)" />
          <stop offset="50%" stopColor="rgba(131,181,53,0.6)" />
          <stop offset="100%" stopColor="rgba(131,181,53,0)" />
        </linearGradient>
        <radialGradient id={`vignette-${uid}`} cx="50%" cy="50%" r="65%">
          <stop offset="0%" stopColor="rgba(0,0,0,0)" />
          <stop offset="100%" stopColor="rgba(15,20,16,0.7)" />
        </radialGradient>
      </defs>

      {/* Base earth */}
      <rect width="1200" height="700" fill="#1a1f1a" />

      {/* Render blocks */}
      <g transform="translate(20, 20) scale(48, 48)">
        {blocks.map((b) => (
          <g key={b.id}>
            <rect x={b.c} y={b.r} width={b.w} height={b.h}
                  fill={ndviColor(b.ndvi)} opacity="0.85"
                  stroke="rgba(0,0,0,0.25)" strokeWidth="0.02" />
            {Array.from({length: b.w * 4}, (_, i) => (
              <line key={i} x1={b.c + i*0.25 + 0.1} y1={b.r + 0.1}
                    x2={b.c + i*0.25 + 0.1} y2={b.r + b.h - 0.1}
                    stroke="rgba(0,0,0,0.15)" strokeWidth="0.015" />
            ))}
          </g>
        ))}
      </g>

      <rect width="1200" height="700" fill={`url(#grid-${uid})`} />

      {/* Selected block indicator */}
      <g transform="translate(560, 240)">
        <rect x="0" y="0" width="160" height="120" fill="none"
              stroke={accent} strokeWidth="2" strokeDasharray="6 3" />
        <circle cx="0" cy="0" r="4" fill={accent} />
        <circle cx="160" cy="0" r="4" fill={accent} />
        <circle cx="0" cy="120" r="4" fill={accent} />
        <circle cx="160" cy="120" r="4" fill={accent} />
      </g>
      <g transform="translate(730, 245)" fontFamily="JetBrains Mono, monospace" fill="white" fontSize="11">
        <rect x="0" y="0" width="180" height="64" fill="rgba(0,0,0,0.55)"
              stroke="rgba(255,255,255,0.18)" strokeWidth="1" rx="4" />
        <text x="12" y="20" opacity="0.6">BLOCK {featureBlock.name}</text>
        <text x="12" y="38" fontSize="14" fontWeight="500">{featureBlock.ha} · NDVI {featureBlock.ndvi}</text>
        <text x="12" y="54" opacity="0.6">VIGOROUS · {featureBlock.canopy} canopy</text>
      </g>

      {/* Coordinate readout */}
      <g transform="translate(20, 670)" fontFamily="JetBrains Mono, monospace" fill="white" fontSize="10" opacity="0.5">
        <text>33°55'12"S 18°51'30"E · ZOOM 17 · SAT-2 · 2026-04-29</text>
      </g>

      <rect width="1200" height="700" fill={`url(#vignette-${uid})`} pointerEvents="none" />
    </svg>
  );
};

/* ============================================================
   Tree-level dot-density visual (alt hero)
   ============================================================ */
window.TreeDots = function TreeDots({ accent = '#83B535' }) {
  const dots = useMemo(() => {
    const arr = [];
    const cols = 60, rows = 32;
    let id = 0;
    const seed = (n) => { let x = Math.sin(n * 9301 + 49297) * 233280; return x - Math.floor(x); };
    for (let r = 0; r < rows; r++) {
      for (let c = 0; c < cols; c++) {
        const v = seed(id++);
        // some "rows" follow real orchard rows
        const xj = (seed(id++) - 0.5) * 4;
        const yj = (seed(id++) - 0.5) * 4;
        arr.push({ x: c * 20 + 10 + xj, y: r * 20 + 10 + yj, v });
      }
    }
    return arr;
  }, []);
  const color = (v) => {
    if (v < 0.25) return '#5A3B1F';
    if (v < 0.45) return '#8C6232';
    if (v < 0.6) return '#A89548';
    if (v < 0.75) return '#7A9C32';
    if (v < 0.9) return '#5C9A2D';
    return '#2D7A1F';
  };
  return (
    <svg viewBox="0 0 1200 640" preserveAspectRatio="xMidYMid slice" role="img" aria-labelledby="treeDotsTitle" style={{width: '100%', height: '100%'}}><title id="treeDotsTitle">Tree-by-tree fruit count visualization</title>
      <rect width="1200" height="640" fill="#1a1f1a" />
      {dots.map((d, i) => (
        <circle key={i} cx={d.x} cy={d.y} r={d.v * 4 + 2} fill={color(d.v)} opacity="0.85" />
      ))}
    </svg>
  );
};

/* ============================================================
   Mini map widget — for cards
   ============================================================ */
window.MiniMap = function MiniMap({ accent = '#83B535', variant = 'ndvi' }) {
  // Real soil/yield scan imagery
  if (variant === 'soil') {
    return (
      <div role="img" aria-label="Soil EC scan" data-img-file="assets/soil-scan.png"
           style={{ width: '100%', height: '100%', backgroundImage: "url('assets/soil-scan.png')", backgroundSize: 'cover', backgroundPosition: 'center', display: 'block' }} />
    );
  }
  if (variant === 'yield') {
    return (
      <div role="img" aria-label="Yield map" data-img-file="assets/yield-scan.png"
           style={{ width: '100%', height: '100%', backgroundImage: "url('assets/yield-scan.png')", backgroundSize: 'cover', backgroundPosition: 'center', display: 'block' }} />
    );
  }
  const seed = (n) => { let x = Math.sin(n * 9301 + 49297) * 233280; return x - Math.floor(x); };
  const cells = useMemo(() => {
    const arr = [];
    let id = 0;
    for (let r = 0; r < 14; r++) {
      for (let c = 0; c < 22; c++) {
        arr.push({ r, c, v: seed(id++ + (variant === 'soil' ? 99 : variant === 'yield' ? 200 : 0)) });
      }
    }
    return arr;
  }, [variant]);
  const ramp = (v) => {
    if (variant === 'soil') {
      // soil EC ramp (blue -> tan)
      if (v < 0.3) return '#7A9CB8';
      if (v < 0.55) return '#A8A878';
      if (v < 0.75) return '#C4AC74';
      return '#5A4A2A';
    }
    if (variant === 'yield') {
      if (v < 0.25) return '#7A4438';
      if (v < 0.5) return '#C4A038';
      if (v < 0.75) return '#7A9C32';
      return '#2D7A1F';
    }
    if (v < 0.3) return '#5A3B1F';
    if (v < 0.5) return '#8C6232';
    if (v < 0.7) return '#7A9C32';
    return '#2D7A1F';
  };
  return (
    <svg viewBox="0 0 220 140" role="img" aria-labelledby="miniMapTitle" style={{width: '100%', height: '100%', display: 'block'}}><title id="miniMapTitle">Block-level data map</title><rect width="220" height="140" fill="#1a1f1a" />
      {cells.map((c, i) => (
        <rect key={i} x={c.c * 10} y={c.r * 10} width="10" height="10" fill={ramp(c.v)} opacity="0.9" />
      ))}
      {/* row lines */}
      {Array.from({length: 22}, (_, i) => (
        <line key={i} x1={i*10 + 0.5} y1="0" x2={i*10 + 0.5} y2="140" stroke="rgba(0,0,0,0.3)" strokeWidth="0.5" />
      ))}
    </svg>
  );
};

/* ============================================================
   Fruit growth curve — sigmoidal (logistic) with predicted tail
   ============================================================ */
window.TimeSeries = function TimeSeries({ accent = '#83B535', height = 160, instanceId = '' }) {
  // Unique ID per instance to avoid duplicate SVG gradient IDs
  const uid = React.useMemo(() => instanceId || Math.random().toString(36).slice(2, 7), [instanceId]);
  const gradId = `tsGrad-${uid}`;
  // Logistic / sigmoid: y = L / (1 + e^(-k(t - t0)))
  // Domain: weeks 0..28 from full bloom; current = week ~18 (about 65%)
  const W = 480;
  const top = 16;
  const bottom = height - 22;
  const usableH = bottom - top;
  const N = 60;
  const currentIdx = Math.round(N * (18 / 28)); // ~week 18 of 28

  const { actual, predicted, dots } = useMemo(() => {
    const k = 0.42;     // steepness
    const t0 = 13;      // inflection week
    const L = 1;        // asymptote (normalised)
    const sigmoid = (t) => L / (1 + Math.exp(-k * (t - t0)));

    const all = [];
    for (let i = 0; i <= N; i++) {
      const week = (i / N) * 28;
      const base = sigmoid(week);
      // tiny field noise only on observed segment, none on prediction
      const noise = i <= currentIdx ? (Math.sin(i * 1.9) * 0.012 + Math.sin(i * 0.7) * 0.008) : 0;
      const v = Math.max(0, Math.min(1, base + noise));
      const x = (i / N) * W;
      const y = bottom - v * usableH;
      all.push({ x, y, week });
    }
    const actualPts = all.slice(0, currentIdx + 1);
    const predictedPts = all.slice(currentIdx);
    // Weekly observation dots only on the actual segment
    const dotPts = [];
    for (let w = 1; w <= 18; w++) {
      const idx = Math.round((w / 28) * N);
      if (all[idx]) dotPts.push(all[idx]);
    }
    return { actual: actualPts, predicted: predictedPts, dots: dotPts };
  }, [height]);

  const toPath = (pts) => pts.map((p, i) => `${i === 0 ? 'M' : 'L'}${p.x.toFixed(1)},${p.y.toFixed(1)}`).join(' ');
  const actualPath = toPath(actual);
  const predictedPath = toPath(predicted);
  const lastActual = actual[actual.length - 1];
  const fillPath = `M0,${bottom} ${actualPath.replace(/^M/, 'L')} L${lastActual.x.toFixed(1)},${bottom} Z`;

  // Target asymptote line
  const targetY = top + 4;

  return (
    <svg viewBox={`0 0 ${W} ${height}`} role="img" aria-labelledby="tsTitle"
         style={{width: '100%', height: '100%', display: 'block'}}>
      <title id="tsTitle">Fruit growth curve — sigmoidal, with predicted final size</title>
      <defs>
        <linearGradient id={gradId} x1="0" y1="0" x2="0" y2="1">
          <stop offset="0%" stopColor={accent} stopOpacity="0.32" />
          <stop offset="100%" stopColor={accent} stopOpacity="0" />
        </linearGradient>
      </defs>

      {/* gridlines */}
      {[0, 0.25, 0.5, 0.75, 1].map((t, i) => (
        <line key={i} x1="0" y1={top + t * usableH} x2={W} y2={top + t * usableH}
              stroke="var(--line)" strokeWidth="0.5" />
      ))}

      {/* target band (predicted final size) */}
      <line x1="0" y1={targetY} x2={W} y2={targetY}
            stroke={accent} strokeWidth="1" strokeDasharray="2 3" opacity="0.5" />
      <text x={W - 4} y={targetY - 3} fontFamily="JetBrains Mono, monospace" fontSize="8"
            fill="var(--muted)" textAnchor="end">TARGET 75 mm</text>

      {/* fill under observed curve */}
      <path d={fillPath} fill={`url(#${gradId})`} />

      {/* observed (solid) */}
      <path d={actualPath} fill="none" stroke={accent} strokeWidth="2" strokeLinecap="round" />

      {/* predicted (dashed continuation) */}
      <path d={predictedPath} fill="none" stroke={accent} strokeWidth="1.6"
            strokeDasharray="4 4" strokeLinecap="round" opacity="0.75" />

      {/* "today" marker */}
      <line x1={lastActual.x} y1={top} x2={lastActual.x} y2={bottom}
            stroke="var(--muted)" strokeWidth="0.5" strokeDasharray="2 3" opacity="0.5" />
      <text x={lastActual.x + 4} y={top + 9} fontFamily="JetBrains Mono, monospace" fontSize="8"
            fill="var(--muted)">WEEK 18 · TODAY</text>

      {/* weekly observation dots */}
      {dots.map((p, i) => (
        <circle key={i} cx={p.x} cy={p.y} r="2.4" fill={accent} stroke="var(--paper)" strokeWidth="1" />
      ))}

      {/* x-axis labels */}
      <text x="6" y={height - 6} fontFamily="JetBrains Mono, monospace" fontSize="9" fill="var(--muted)">BLOOM</text>
      <text x={W / 2} y={height - 6} fontFamily="JetBrains Mono, monospace" fontSize="9"
            fill="var(--muted)" textAnchor="middle">WEEK 14</text>
      <text x={W - 4} y={height - 6} fontFamily="JetBrains Mono, monospace" fontSize="9"
            fill="var(--muted)" textAnchor="end">HARVEST</text>
    </svg>
  );
};

/* ============================================================
   Fruit size distribution histogram
   ============================================================ */
window.SizeDistribution = function SizeDistribution({ accent = '#83B535' }) {
  const bars = [
    { label: '60mm', val: 8, target: false },
    { label: '65mm', val: 18, target: false },
    { label: '70mm', val: 28, target: true },
    { label: '75mm', val: 30, target: true },
    { label: '80mm', val: 12, target: true },
    { label: '85mm', val: 4, target: false },
  ];
  const max = 32;
  return (
    <svg viewBox="0 0 480 220" role="img" aria-labelledby="sdTitle" style={{width: '100%', height: '100%', display: 'block'}}><title id="sdTitle">Fruit size distribution histogram</title>{bars.map((b, i) => {
        const h = (b.val / max) * 160;
        const x = i * 76 + 30;
        return (
          <g key={i}>
            <rect x={x} y={180 - h} width="56" height={h}
                  fill={b.target ? accent : 'var(--soil)'} opacity={b.target ? 0.95 : 0.55}
                  rx="1" />
            <text x={x + 28} y={200} fontFamily="JetBrains Mono, monospace" fontSize="10"
                  fill="var(--muted)" textAnchor="middle">{b.label}</text>
            <text x={x + 28} y={175 - h} fontFamily="Inter" fontSize="11" fontWeight="500"
                  fill="var(--charcoal)" textAnchor="middle">{b.val}%</text>
          </g>
        );
      })}
      {/* target band */}
      <rect x="20" y="20" width="440" height="2" fill={accent} opacity="0.3" />
      <text x="20" y="16" fontFamily="JetBrains Mono, monospace" fontSize="9" fill="var(--muted)">PACKHOUSE TARGET BAND</text>
    </svg>
  );
};

/* ============================================================
   Soil EC profile cross-section
   ============================================================ */
window.SoilProfile = function SoilProfile() {
  const layers = [
    { d: '0–25cm', cells: [0.3, 0.5, 0.8, 0.7, 0.4, 0.3, 0.5, 0.6, 0.7, 0.5, 0.4, 0.3] },
    { d: '25–50cm', cells: [0.4, 0.6, 0.9, 0.8, 0.5, 0.4, 0.6, 0.7, 0.8, 0.6, 0.5, 0.4] },
    { d: '50–90cm', cells: [0.5, 0.7, 0.95, 0.9, 0.6, 0.5, 0.7, 0.8, 0.9, 0.7, 0.6, 0.5] },
  ];
  const ramp = (v) => {
    if (v < 0.4) return '#C4AC74';
    if (v < 0.6) return '#A89548';
    if (v < 0.8) return '#7A6B38';
    return '#3D3520';
  };
  return (
    <svg viewBox="0 0 480 220" style={{width: '100%', height: '100%', display: 'block'}}>
      {layers.map((l, li) => (
        <g key={li}>
          <text x="0" y={50 + li * 60} fontFamily="JetBrains Mono, monospace" fontSize="10" fill="var(--muted)">{l.d}</text>
          {l.cells.map((v, ci) => (
            <rect key={ci} x={70 + ci * 32} y={30 + li * 60} width="30" height="50"
                  fill={ramp(v)} stroke="rgba(0,0,0,0.15)" strokeWidth="0.5" />
          ))}
        </g>
      ))}
      <text x="70" y={220 - 4} fontFamily="JetBrains Mono, monospace" fontSize="9" fill="var(--muted)">EC mS/m · low → high</text>
    </svg>
  );
};

/* ============================================================
   Calibration scatter
   ============================================================ */
window.CalibrationScatter = function CalibrationScatter({ accent = '#83B535' }) {
  const points = useMemo(() => {
    const arr = [];
    const seed = (n) => { let x = Math.sin(n * 9301 + 49297) * 233280; return x - Math.floor(x); };
    for (let i = 0; i < 60; i++) {
      const x = seed(i) * 100;
      const y = x * 0.85 + (seed(i + 100) - 0.5) * 18;
      arr.push({ x, y });
    }
    return arr;
  }, []);
  return (
    <svg viewBox="0 0 480 220" style={{width: '100%', height: '100%', display: 'block'}}>
      <g transform="translate(40, 10)">
        <line x1="0" y1="180" x2="0" y2="0" stroke="var(--line-strong)" />
        <line x1="0" y1="180" x2="400" y2="180" stroke="var(--line-strong)" />
        {/* trend line */}
        <line x1="0" y1="180" x2="400" y2="20" stroke={accent} strokeWidth="1.5" strokeDasharray="3 3" opacity="0.6" />
        {points.map((p, i) => (
          <circle key={i} cx={p.x * 4} cy={180 - p.y * 1.8} r="3.5"
                  fill={accent} opacity="0.65" />
        ))}
        <text x="0" y="-2" fontFamily="JetBrains Mono, monospace" fontSize="9" fill="var(--muted)">CAMERA COUNT</text>
        <text x="380" y="200" fontFamily="JetBrains Mono, monospace" fontSize="9" fill="var(--muted)" textAnchor="end">HAND COUNT</text>
        <text x="200" y="20" fontFamily="JetBrains Mono, monospace" fontSize="11" fill={accent} fontWeight="500">R² = 0.94</text>
      </g>
    </svg>
  );
};

/* ============================================================
   Variable rate prescription map preview
   ============================================================ */
window.VRMap = function VRMap({ accent = '#83B535' }) {
  const seed = (n) => { let x = Math.sin(n * 9301 + 49297) * 233280; return x - Math.floor(x); };
  const cells = useMemo(() => {
    const arr = [];
    let id = 5;
    for (let r = 0; r < 12; r++) {
      for (let c = 0; c < 18; c++) {
        const z = Math.floor(seed(id++) * 4);
        arr.push({ r, c, z });
      }
    }
    return arr;
  }, []);
  const zoneColor = ['#3D5A1F', '#5C9A2D', '#A89548', '#C46A38'];
  const zoneLabel = ['LOW', 'STD', 'MED', 'HIGH'];
  return (
    <svg viewBox="0 0 220 160" role="img" aria-labelledby="vrTitle" style={{width: '100%', height: '100%', display: 'block'}}><title id="vrTitle">Variable-rate prescription map: nitrogen application zones</title><rect width="220" height="160" fill="#1a1f1a" />
      {cells.map((c, i) => (
        <rect key={i} x={c.c * 12 + 4} y={c.r * 12 + 4} width="12" height="12"
              fill={zoneColor[c.z]} opacity="0.92" />
      ))}
      {/* legend */}
      <g transform="translate(8, 145)">
        {zoneLabel.map((l, i) => (
          <g key={i} transform={`translate(${i * 52}, 0)`}>
            <rect width="8" height="8" fill={zoneColor[i]} />
            <text x="12" y="7" fontFamily="JetBrains Mono, monospace" fontSize="8" fill="white" opacity="0.8">{l}</text>
          </g>
        ))}
      </g>
    </svg>
  );
};

/* ============================================================
   Camera/scout illustration (line diagram)
   ============================================================ */
window.ScoutDiagram = function ScoutDiagram({ accent = '#83B535' }) {
  return (
    <svg viewBox="0 0 220 140" style={{width: '100%', height: '100%', display: 'block'}}>
      <rect width="220" height="140" fill="var(--paper-warm)" />
      {/* trees */}
      {[20, 60, 100, 140, 180].map((x, i) => (
        <g key={i}>
          <rect x={x - 1} y="80" width="2" height="20" fill="#5A3B1F" />
          <circle cx={x} cy="75" r="14" fill={accent} opacity="0.7" />
          <circle cx={x} cy="75" r="14" fill="none" stroke={accent} strokeWidth="1" />
        </g>
      ))}
      {/* ground */}
      <line x1="0" y1="100" x2="220" y2="100" stroke="var(--soil)" strokeWidth="2" />
      {/* ATV */}
      <g transform="translate(80, 105)">
        <rect x="-12" y="-10" width="24" height="10" fill="var(--charcoal)" rx="2" />
        <circle cx="-8" cy="2" r="4" fill="var(--charcoal)" />
        <circle cx="8" cy="2" r="4" fill="var(--charcoal)" />
        {/* camera FoV */}
        <path d="M -12 -8 L -50 -30 L -50 0 Z" fill={accent} opacity="0.18" />
        <path d="M 12 -8 L 50 -30 L 50 0 Z" fill={accent} opacity="0.18" />
      </g>
      <text x="110" y="135" fontFamily="JetBrains Mono, monospace" fontSize="9"
            fill="var(--muted)" textAnchor="middle">SCOUT FIELD CAMERA · 5 FRAMES / TREE</text>
    </svg>
  );
};

/* ============================================================
   Generic placeholder image with label
   ============================================================ */
window.ImgPlaceholder = function ImgPlaceholder({ label, tone = 'warm', icon = 'orchard' }) {
  const bg = tone === 'warm' ? 'var(--paper-warm)' : 'var(--charcoal)';
  const fg = tone === 'warm' ? 'var(--muted)' : 'rgba(255,255,255,0.55)';
  return (
    <div style={{
      width: '100%', height: '100%', background: bg, display: 'flex',
      alignItems: 'center', justifyContent: 'center', flexDirection: 'column', gap: 12,
      fontFamily: 'JetBrains Mono, monospace', fontSize: 11, color: fg,
      letterSpacing: '0.08em', textTransform: 'uppercase',
      backgroundImage: `repeating-linear-gradient(45deg, transparent 0 12px, rgba(35,35,35,0.04) 12px 13px)`
    }}>
      <svg width="40" height="40" viewBox="0 0 40 40" fill="none">
        <circle cx="20" cy="22" r="10" fill={fg} opacity="0.2" />
        <path d="M14 22 L20 14 L26 22 Z" fill={fg} opacity="0.4" />
        <rect x="6" y="32" width="28" height="2" fill={fg} opacity="0.3" />
      </svg>
      <span>📷 {label}</span>
    </div>
  );
};

Object.assign(window, {
  RevGlyph: window.RevGlyph,
  RevWordmark: window.RevWordmark,
  SatelliteMap: window.SatelliteMap,
  TreeDots: window.TreeDots,
  MiniMap: window.MiniMap,
  TimeSeries: window.TimeSeries,
  SizeDistribution: window.SizeDistribution,
  SoilProfile: window.SoilProfile,
  CalibrationScatter: window.CalibrationScatter,
  VRMap: window.VRMap,
  ScoutDiagram: window.ScoutDiagram,
  ImgPlaceholder: window.ImgPlaceholder,
});
