// services-bento.jsx — 8 services, 3-column bento, uniform heights
const { useState: useStateSB, useRef: useRefSB, useEffect: useEffectSB } = React;

// Map globe nodes → which service card number they highlight. Each
// node points at the card whose capability it most represents.
const NODE_TO_CARD = {
  crm:   '01',
  data:  '03',
  erp:   '04',
  field: '04',
  phone: '02',
  docs:  '05',
  email: '06',
  slack: '07',
};

// Shared hover bus — the globe writes the hovered node label here,
// every ServiceCard reads it to decide whether to light up. Plain
// pub/sub keeps this simple without React context.
const hoverBus = {
  node: null,
  listeners: new Set(),
  set(next) {
    if (this.node === next) return;
    this.node = next;
    this.listeners.forEach(fn => fn(next));
  },
  sub(fn) { this.listeners.add(fn); return () => this.listeners.delete(fn); },
};

function useHoveredNode() {
  const [node, setNode] = useStateSB(hoverBus.node);
  useEffectSB(() => hoverBus.sub(setNode), []);
  return node;
}

function ServiceCard({ title, eyebrow, accent = 'teal', children, cardId }) {
  const ref = useRefSB(null);
  const [pos, setPos] = useStateSB({ x: -999, y: -999, on: false });
  const hoveredNode = useHoveredNode();
  const isGlobeHot = !!(cardId && hoveredNode && NODE_TO_CARD[hoveredNode] === cardId);
  const onMove = (e) => {
    const r = ref.current.getBoundingClientRect();
    setPos({ x: e.clientX - r.left, y: e.clientY - r.top, on: true });
  };
  const onLeave = () => setPos({ x: -999, y: -999, on: false });

  const color = accent === 'teal' ? '139, 233, 253' : accent === 'violet' ? '188, 162, 255' : '255, 200, 140';
  const colorVar = accent === 'teal' ? 'var(--accent)' : accent === 'violet' ? 'var(--accent-2)' : 'var(--warn)';

  return (
    <div
      ref={ref}
      onMouseMove={onMove}
      onMouseLeave={onLeave}
      style={{
        background: 'var(--bg-panel)',
        border: '1px solid',
        borderColor: isGlobeHot ? `rgba(${color}, 0.55)` : 'var(--bg-line)',
        borderRadius: 16, padding: 28,
        position: 'relative', overflow: 'hidden',
        display: 'flex', flexDirection: 'column',
        height: '100%', width: '100%',
        transition: 'border-color 0.3s, transform 0.3s cubic-bezier(.2,.8,.2,1), box-shadow 0.3s',
        boxShadow: isGlobeHot ? `0 0 0 1px rgba(${color}, 0.25), 0 18px 60px rgba(${color}, 0.14)` : 'none',
        transform: isGlobeHot ? 'translateY(-4px)' : 'translateY(0)',
        cursor: 'pointer',
      }}
      onMouseEnter={(e) => { if (!isGlobeHot) { e.currentTarget.style.borderColor = `rgba(${color}, 0.3)`; e.currentTarget.style.transform = 'translateY(-4px)'; } }}
      onMouseOut={(e) => { if (!isGlobeHot) { e.currentTarget.style.borderColor = 'var(--bg-line)'; e.currentTarget.style.transform = 'translateY(0)'; } }}
    >
      <div style={{
        position: 'absolute', inset: 0, pointerEvents: 'none',
        background: `radial-gradient(360px circle at ${pos.x}px ${pos.y}px, rgba(${color}, 0.10), transparent 60%)`,
        opacity: pos.on ? 1 : 0, transition: 'opacity 0.3s',
      }}/>
      {/* Ambient glow when the globe is pointing at this card */}
      <div style={{
        position: 'absolute', inset: 0, pointerEvents: 'none',
        background: `radial-gradient(circle at 50% 0%, rgba(${color}, 0.12), transparent 70%)`,
        opacity: isGlobeHot ? 1 : 0, transition: 'opacity 0.3s',
      }}/>
      <div style={{ position: 'relative', zIndex: 2, display: 'flex', flexDirection: 'column', height: '100%' }}>
        <div className="mono" style={{ color: colorVar, marginBottom: 16 }}>{eyebrow}</div>
        <h3 style={{
          fontSize: 24, fontWeight: 500,
          letterSpacing: '-0.02em', lineHeight: 1.15, margin: 0, marginBottom: 14,
          textWrap: 'balance',
        }}>{title}</h3>
        <div style={{ display: 'flex', flexDirection: 'column', flex: 1, minHeight: 0 }}>
          {children}
        </div>
      </div>
    </div>
  );
}

function ChecklistBlock({ items, accent, label = 'AUDIT OUTPUTS' }) {
  return (
    <div style={{ marginTop: 'auto', paddingTop: 18, padding: 14, borderRadius: 10, background: 'var(--bg)', border: '1px solid var(--bg-line)' }}>
      <div className="mono" style={{ fontSize: 10, marginBottom: 10, color: accent }}>{label}</div>
      {items.map(x => (
        <div key={x} style={{ fontSize: 12, color: 'var(--ink-dim)', display: 'flex', alignItems: 'center', gap: 8, padding: '4px 0' }}>
          <svg width="10" height="10" viewBox="0 0 10 10"><path d="M2 5 L4 7 L8 3" stroke={accent} strokeWidth="1.5" fill="none" strokeLinecap="round" strokeLinejoin="round"/></svg>
          {x}
        </div>
      ))}
    </div>
  );
}

function ChipsBlock({ items, helper }) {
  return (
    <div style={{ marginTop: 'auto', paddingTop: 18 }}>
      {helper && (
        <div style={{ fontSize: 13, color: 'var(--ink-dim)', lineHeight: 1.5, marginBottom: 12 }}>
          {helper}
        </div>
      )}
      <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
        {items.map(t => <span key={t} className="tag" style={{ fontSize: 11 }}>{t}</span>)}
      </div>
    </div>
  );
}

function IconRow({ items, accent }) {
  return (
    <div style={{ marginTop: 'auto', paddingTop: 18, display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8 }}>
      {items.map(x => (
        <div key={x} style={{ fontSize: 12, color: 'var(--ink-dim)', display: 'flex', alignItems: 'center', gap: 6 }}>
          <svg width="10" height="10" viewBox="0 0 10 10"><path d="M2 5 L4 7 L8 3" stroke={accent} strokeWidth="1.5" fill="none" strokeLinecap="round" strokeLinejoin="round"/></svg>
          {x}
        </div>
      ))}
    </div>
  );
}

function LogRow({ accent, accent2 }) {
  return (
    <div style={{
      marginTop: 'auto', paddingTop: 18, padding: 12, borderRadius: 10,
      background: 'var(--bg)', border: '1px solid var(--bg-line)',
      fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--ink-dim)',
      display: 'grid', gap: 4,
    }}>
      <div><span style={{ color: accent2 }}>⚠ §7.4</span> unlimited liability waiver</div>
      <div><span style={{ color: accent2 }}>⚠ §12.1</span> auto renewal, 90d notice</div>
      <div><span style={{ color: accent }}>✓ 17 other clauses</span> reviewed</div>
    </div>
  );
}

// Continent silhouettes — coarse lat/lng polygons that give the globe
// an Earth-like read without needing a real geoJSON file. Each entry
// is a closed loop of [lat, lng] points; we project and clip to the
// visible hemisphere so it behaves like an actual rotating planet.
const CONTINENTS = [
  // North America
  [[70,-165],[72,-140],[60,-135],[55,-130],[48,-125],[35,-120],[25,-110],[18,-105],[15,-92],[18,-88],[25,-80],[30,-82],[35,-76],[42,-70],[48,-65],[55,-60],[60,-65],[65,-80],[70,-95],[72,-120],[72,-150]],
  // South America
  [[12,-72],[8,-78],[-5,-81],[-18,-74],[-30,-72],[-42,-74],[-54,-70],[-54,-66],[-40,-62],[-25,-48],[-10,-36],[0,-50],[8,-60],[12,-70]],
  // Europe
  [[70,-8],[68,20],[70,32],[60,40],[50,42],[42,40],[38,28],[36,18],[36,-6],[42,-10],[50,-5],[60,-2]],
  // Africa
  [[35,-6],[32,10],[30,30],[15,42],[10,50],[-10,42],[-25,40],[-35,20],[-34,18],[-20,12],[0,8],[12,-16],[20,-18],[30,-10]],
  // Asia
  [[70,40],[72,90],[70,140],[60,170],[50,155],[40,140],[32,135],[25,122],[12,108],[5,100],[10,80],[22,72],[30,62],[40,50],[50,45]],
  // Australia
  [[-12,130],[-15,145],[-22,150],[-34,152],[-38,145],[-35,135],[-32,120],[-22,114],[-15,125]],
  // Greenland
  [[82,-35],[78,-20],[70,-22],[62,-42],[68,-52],[78,-55],[82,-45]],
  // Indonesia / SE Asia archipelago (coarse)
  [[2,96],[6,110],[2,120],[-4,118],[-8,110],[-6,100]],
];

// Central connection hub — visually shows AI reaching out to every
// service tile around it. Bigger nodes than the small version so the
// "everything connects through AI" metaphor reads at a glance.
function ConnectionHub() {
  const isMobile = useIsMobile(640);
  if (isMobile) return <ConnectionHubMobile />;
  return <ConnectionHubInner/>;
}

// Compact mobile fill-in for the connective-layer story. A small
// rotating wireframe globe with labeled tool-nodes orbiting around an
// AI core. Mirrors the desktop globe's visual language (latitude
// rings, animated arc pulses, accent-warn nodes) at a phone-friendly
// scale, so the "AI connects everything" narrative reads richly.
function ConnectionHubMobile() {
  const [t, setT] = useStateSB(0);
  useEffectSB(() => {
    let raf;
    const start = performance.now();
    const loop = (now) => {
      setT((now - start) / 1000);
      raf = requestAnimationFrame(loop);
    };
    raf = requestAnimationFrame(loop);
    return () => cancelAnimationFrame(raf);
  }, []);

  const W = 320, H = 260;
  const cx = W / 2, cy = H / 2 + 6, R = 92;
  const rotY = t * 0.25;

  // Tool nodes at fixed lat/lng. Project onto rotating sphere.
  const nodes = [
    { lat:  40, lng: -100, label: 'CRM'   },
    { lat:  52, lng:   10, label: 'DATA'  },
    { lat:  35, lng:  135, label: 'ERP'   },
    { lat: -25, lng:  135, label: 'FIELD' },
    { lat: -15, lng:  -55, label: 'PHONE' },
    { lat:   1, lng:   30, label: 'EMAIL' },
    { lat:  28, lng:   77, label: 'DOCS'  },
    { lat:  55, lng:  -60, label: 'SLACK' },
  ];
  const project = (lat, lng) => {
    const phi = (lat * Math.PI) / 180;
    const lam = (lng * Math.PI) / 180 + rotY;
    const x3 = Math.cos(phi) * Math.sin(lam);
    const y3 = Math.sin(phi);
    const z3 = Math.cos(phi) * Math.cos(lam);
    return { x: cx + x3 * R, y: cy - y3 * R, z: z3 };
  };
  const lats = [-60, -30, 0, 30, 60];

  return (
    <div style={{
      background: 'var(--bg-panel)',
      border: '1px solid var(--bg-line)',
      borderRadius: 16, padding: 22,
      display: 'flex', flexDirection: 'column', gap: 8,
      position: 'relative', overflow: 'hidden',
    }}>
      <div className="mono" style={{ color: 'var(--accent)' }}>// THE CONNECTIVE LAYER</div>
      <h3 style={{
        fontSize: 22, fontWeight: 500, letterSpacing: '-0.02em',
        margin: 0, lineHeight: 1.15, textWrap: 'balance',
      }}>
        AI connects everything<br/>you already run
      </h3>

      <div style={{ position: 'relative', width: '100%', display: 'flex', justifyContent: 'center' }}>
        <svg viewBox={`0 0 ${W} ${H}`} style={{ width: '100%', maxWidth: 360, height: 'auto', overflow: 'visible' }}>
          <defs>
            <radialGradient id="hubm-core" cx="50%" cy="50%" r="50%">
              <stop offset="0%"  stopColor="var(--accent)" stopOpacity="0.9"/>
              <stop offset="70%" stopColor="var(--accent)" stopOpacity="0.3"/>
              <stop offset="100%" stopColor="var(--accent)" stopOpacity="0"/>
            </radialGradient>
            <radialGradient id="hubm-shade" cx="32%" cy="30%" r="78%">
              <stop offset="0%" stopColor="var(--accent)" stopOpacity="0.10"/>
              <stop offset="55%" stopColor="var(--accent)" stopOpacity="0.03"/>
              <stop offset="100%" stopColor="var(--bg)" stopOpacity="0.55"/>
            </radialGradient>
            <radialGradient id="hubm-atmo" cx="50%" cy="50%" r="52%">
              <stop offset="88%" stopColor="var(--accent)" stopOpacity="0"/>
              <stop offset="96%" stopColor="var(--accent)" stopOpacity="0.18"/>
              <stop offset="100%" stopColor="var(--accent)" stopOpacity="0"/>
            </radialGradient>
            <clipPath id="hubm-clip"><circle cx={cx} cy={cy} r={R}/></clipPath>
          </defs>

          <circle cx={cx} cy={cy} r={R + 6} fill="url(#hubm-atmo)"/>
          <circle cx={cx} cy={cy} r={R} fill="url(#hubm-shade)"
            stroke="var(--accent)" strokeOpacity="0.35" strokeWidth="1"/>

          <g clipPath="url(#hubm-clip)">
            {lats.map(lat => {
              const phi = (lat * Math.PI) / 180;
              const yOff = cy - Math.sin(phi) * R;
              return (
                <ellipse key={lat} cx={cx} cy={yOff} rx={R} ry={Math.abs(Math.cos(phi)) * R * 0.18 + 1}
                  fill="none" stroke="var(--accent)" strokeOpacity="0.22" strokeWidth="0.7"/>
              );
            })}
            {/* Continents — coarse silhouettes, dense-sampled and clipped */}
            {CONTINENTS.map((poly, ci) => {
              const dense = [];
              for (let i = 0; i < poly.length; i++) {
                const a = poly[i], b = poly[(i + 1) % poly.length];
                for (let s = 0; s < 6; s++) {
                  const u = s / 6;
                  dense.push([a[0] + (b[0]-a[0])*u, a[1] + (b[1]-a[1])*u]);
                }
              }
              const front = [], back = [];
              let cf = [], cb = [];
              for (const [lat, lng] of dense) {
                const p = project(lat, lng);
                if (p.z > 0) {
                  if (cb.length) { back.push(cb); cb = []; }
                  cf.push(`${p.x.toFixed(1)},${p.y.toFixed(1)}`);
                } else {
                  if (cf.length) { front.push(cf); cf = []; }
                  cb.push(`${p.x.toFixed(1)},${p.y.toFixed(1)}`);
                }
              }
              if (cf.length) front.push(cf);
              if (cb.length) back.push(cb);
              return (
                <g key={ci}>
                  {back.map((pts, si) => (
                    <polyline key={`b${si}`} points={pts.join(' ')} fill="none"
                      stroke="var(--accent)" strokeOpacity="0.22" strokeWidth="0.6"
                      strokeLinejoin="round" strokeLinecap="round"/>
                  ))}
                  {front.map((pts, si) => (
                    <polyline key={`f${si}`} points={pts.join(' ')} fill="none"
                      stroke="var(--accent)" strokeOpacity="0.7" strokeWidth="1"
                      strokeLinejoin="round" strokeLinecap="round"/>
                  ))}
                </g>
              );
            })}
          </g>

          {/* Arcs from core to each node, with traveling pulses */}
          {nodes.map((n, i) => {
            const p = project(n.lat, n.lng);
            const mx = (cx + p.x) / 2, my = (cy + p.y) / 2;
            const dx = mx - cx, dy = my - cy;
            const d = Math.sqrt(dx*dx + dy*dy) || 1;
            const lift = 36;
            const ctrlX = mx + (dx / d) * lift;
            const ctrlY = my + (dy / d) * lift;
            const isFront = p.z > 0;
            const phase = (t * 0.55 + i / nodes.length) % 1;
            const u = phase, iu = 1 - phase;
            const px = iu*iu*cx + 2*iu*u*ctrlX + u*u*p.x;
            const py = iu*iu*cy + 2*iu*u*ctrlY + u*u*p.y;
            return (
              <g key={i}>
                <path d={`M ${cx} ${cy} Q ${ctrlX.toFixed(1)} ${ctrlY.toFixed(1)} ${p.x.toFixed(1)} ${p.y.toFixed(1)}`}
                  fill="none" stroke="var(--warn)"
                  strokeOpacity={isFront ? 0.6 + p.z * 0.2 : 0.25}
                  strokeWidth="0.9" strokeDasharray={isFront ? 'none' : '2 3'}/>
                {isFront && (
                  <circle cx={px} cy={py} r="2" fill="var(--warn)" opacity={(1 - phase) * 0.95}/>
                )}
              </g>
            );
          })}

          {/* Core */}
          <circle cx={cx} cy={cy} r="30" fill="url(#hubm-core)"/>
          <circle cx={cx} cy={cy} r={14 + Math.sin(t * 1.8) * 2}
            fill="none" stroke="var(--accent)" strokeOpacity="0.7" strokeWidth="1.2"/>
          <text x={cx} y={cy + 5} textAnchor="middle"
            fontFamily="var(--font-sans)" fontSize="16" fontWeight="600" fill="var(--ink)">AI</text>

          {/* Nodes */}
          {nodes.map((n, idx) => {
            const p = project(n.lat, n.lng);
            const isFront = p.z > 0;
            const r = 2.8 + Math.max(p.z, 0) * 2;
            if (isFront) {
              return (
                <g key={n.label} opacity={0.5 + p.z * 0.5}>
                  <circle cx={p.x} cy={p.y} r={r + 3} fill="var(--warn)" opacity="0.22"/>
                  <circle cx={p.x} cy={p.y} r={r} fill="var(--warn)" stroke="var(--bg)" strokeWidth="0.8"/>
                </g>
              );
            }
            return (
              <circle key={n.label} cx={p.x} cy={p.y} r="2.2"
                fill="none" stroke="var(--warn)" strokeOpacity="0.45" strokeWidth="0.9"/>
            );
          })}

          {/* Labels — always on top, smooth direction blend */}
          {nodes.map((n, idx) => {
            const p = project(n.lat, n.lng);
            const isFront = p.z > 0;
            const dx = p.x - cx, dy = p.y - cy;
            const d = Math.hypot(dx, dy);
            const fa = (idx / nodes.length) * Math.PI * 2 - Math.PI / 2;
            const fx = Math.cos(fa), fy = Math.sin(fa);
            const w = Math.min(1, d / (R * 0.5));
            let nx = (dx / (d || 1)) * w + fx * (1 - w);
            let ny = (dy / (d || 1)) * w + fy * (1 - w);
            const nl = Math.hypot(nx, ny) || 1;
            nx /= nl; ny /= nl;
            const lx = p.x + nx * 14;
            const ly = p.y + ny * 14;
            return (
              <text key={n.label} x={lx} y={ly + 3} textAnchor="middle"
                fontFamily="var(--font-mono)" fontSize="8"
                fill="var(--ink)" fillOpacity={isFront ? 1 : 0.5}
                letterSpacing="0.08em" style={{ textTransform: 'uppercase' }}>
                {n.label}
              </text>
            );
          })}
        </svg>
      </div>
    </div>
  );
}

function ConnectionHubInner() {
  const ref = useRefSB(null);
  const [t, setT] = useStateSB(0);
  const hoveredNode = useHoveredNode();
  React.useEffect(() => {
    let raf;
    let start = performance.now();
    const loop = (now) => {
      setT((now - start) / 1000);
      raf = requestAnimationFrame(loop);
    };
    raf = requestAnimationFrame(loop);
    return () => cancelAnimationFrame(raf);
  }, []);

  // Globe setup: orthographic projection of lat/lng points. The whole
  // thing rotates around the Y axis, each labeled node lives at a real
  // latitude/longitude, and lines from the AI core at the center reach
  // out to each one — cities connected through AI.
  // Globe size — keep it tighter so cards around it have breathing room
  // and the continent labels/pulses stay visible past the neighbors.
  const cx = 200, cy = 230, R = 150;
  const rotY = t * 0.22; // radians/sec spin — slower, smoother

  const nodes = [
    { lat:  40,  lng: -100, label: 'crm'   }, // N America
    { lat:  52,  lng:   10, label: 'data'  }, // Europe
    { lat:  35,  lng:  135, label: 'erp'   }, // Japan
    { lat: -25,  lng:  135, label: 'field' }, // Australia
    { lat: -15,  lng:  -55, label: 'phone' }, // S America
    { lat:   1,  lng:   30, label: 'email' }, // Africa
    { lat:  28,  lng:   77, label: 'docs'  }, // India
    { lat:  55,  lng:  -60, label: 'slack' }, // NE Canada
  ];

  // Project a lat/lng onto a rotating 3D sphere, return { x, y, z, visible }
  const project = (lat, lng) => {
    const phi = (lat * Math.PI) / 180;
    const lam = (lng * Math.PI) / 180 + rotY;
    const x3 = Math.cos(phi) * Math.sin(lam);
    const y3 = Math.sin(phi);
    const z3 = Math.cos(phi) * Math.cos(lam);
    return { x: cx + x3 * R, y: cy - y3 * R, z: z3, visible: z3 > -0.1 };
  };

  // Latitude rings (meridians + parallels) for the wireframe
  const lats = [-60, -30, 0, 30, 60];
  const lngs = [-150, -120, -90, -60, -30, 0, 30, 60, 90, 120, 150, 180];

  return (
    <div
      ref={ref}
      className="bento-hub"
      style={{
        gridColumn: '2 / 3', gridRow: '2 / 4',
        background: 'transparent',
        border: 'none',
        borderRadius: 16, padding: '24px 20px',
        position: 'relative', overflow: 'visible',
        display: 'flex', flexDirection: 'column',
        alignItems: 'center', justifyContent: 'flex-start',
        minHeight: 0, height: '100%', width: '100%',
      }}
    >
      <div className="mono" style={{ color: 'var(--accent)', marginBottom: 10, textAlign: 'center' }}>
        // THE CONNECTIVE LAYER
      </div>
      <h3 style={{
        fontSize: 20, fontWeight: 500, letterSpacing: '-0.02em',
        margin: 0, marginBottom: 10, textAlign: 'center', textWrap: 'balance',
        lineHeight: 1.15,
      }}>
        AI connects everything<br/>you already run
      </h3>

      <div style={{ position: 'relative', width: '100%', flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: 0, marginTop: -60 }}>
        <svg viewBox="0 0 400 400" preserveAspectRatio="xMidYMid meet"
          style={{ width: '115%', height: '115%', maxHeight: '115%', display: 'block', overflow: 'visible' }}>
          <defs>
            <radialGradient id="hub-core" cx="50%" cy="50%" r="50%">
              <stop offset="0%"  stopColor="var(--accent)" stopOpacity="0.9"/>
              <stop offset="70%" stopColor="var(--accent)" stopOpacity="0.3"/>
              <stop offset="100%" stopColor="var(--accent)" stopOpacity="0"/>
            </radialGradient>
            <radialGradient id="globe-shade" cx="32%" cy="30%" r="78%">
              <stop offset="0%" stopColor="var(--accent)" stopOpacity="0.10"/>
              <stop offset="55%" stopColor="var(--accent)" stopOpacity="0.03"/>
              <stop offset="100%" stopColor="var(--bg)" stopOpacity="0.55"/>
            </radialGradient>
            <radialGradient id="globe-atmo" cx="50%" cy="50%" r="52%">
              <stop offset="88%" stopColor="var(--accent)" stopOpacity="0"/>
              <stop offset="96%" stopColor="var(--accent)" stopOpacity="0.18"/>
              <stop offset="100%" stopColor="var(--accent)" stopOpacity="0"/>
            </radialGradient>
            <clipPath id="globe-clip">
              <circle cx={cx} cy={cy} r={R}/>
            </clipPath>
          </defs>

          {/* atmospheric halo */}
          <circle cx={cx} cy={cy} r={R + 8} fill="url(#globe-atmo)"/>

          {/* globe outer sphere */}
          <circle cx={cx} cy={cy} r={R} fill="url(#globe-shade)"
            stroke="var(--accent)" strokeOpacity="0.35" strokeWidth="1"/>

          <g clipPath="url(#globe-clip)">
            {/* parallels (latitude rings) — ellipses flattened by cos(lat) */}
            {lats.map(lat => {
              const phi = (lat * Math.PI) / 180;
              const ry = Math.abs(Math.cos(phi)) * R;
              const yOff = cy - Math.sin(phi) * R;
              return (
                <ellipse key={`p${lat}`} cx={cx} cy={yOff} rx={R} ry={ry * 0.18 + 1}
                  fill="none" stroke="var(--accent)" strokeOpacity="0.18" strokeWidth="0.75"/>
              );
            })}

            {/* meridians — removed per request; only latitude rings remain */}

            {/*
              Continents. We dense-sample each polygon edge (great-circle
              interpolation) so the shape stays stable while the globe
              spins — without resampling, the silhouette morphs at the
              horizon. Back-facing portions are drawn as subtle dashed
              lines so you can see the whole outline through the sphere
              rather than popping in and out.
            */}
            {CONTINENTS.map((poly, ci) => {
              // Dense-sample: insert interpolated lat/lng points along
              // every edge so both front and back halves have enough
              // detail to look smooth.
              const dense = [];
              for (let i = 0; i < poly.length; i++) {
                const a = poly[i];
                const b = poly[(i + 1) % poly.length];
                const steps = 8;
                for (let s = 0; s < steps; s++) {
                  const u = s / steps;
                  dense.push([a[0] + (b[0]-a[0])*u, a[1] + (b[1]-a[1])*u]);
                }
              }
              // Project all points; split into front-facing and
              // back-facing runs based on z>0.
              const frontRuns = [];
              const backRuns = [];
              let curFront = [], curBack = [];
              for (const [lat, lng] of dense) {
                const p = project(lat, lng);
                if (p.z > 0) {
                  if (curBack.length) { backRuns.push(curBack); curBack = []; }
                  curFront.push(`${p.x.toFixed(1)},${p.y.toFixed(1)}`);
                } else {
                  if (curFront.length) { frontRuns.push(curFront); curFront = []; }
                  curBack.push(`${p.x.toFixed(1)},${p.y.toFixed(1)}`);
                }
              }
              if (curFront.length) frontRuns.push(curFront);
              if (curBack.length) backRuns.push(curBack);

              return (
                <g key={`c${ci}`}>
                  {/* back half — drawn first, under everything.
                      Solid stroke + low opacity so it reads as "fading
                      behind the globe" rather than dashed. */}
                  {backRuns.map((pts, si) => (
                    <polyline key={`cb${ci}-${si}`} points={pts.join(' ')}
                      fill="none"
                      stroke="var(--accent)" strokeOpacity="0.22"
                      strokeWidth="0.7"
                      strokeLinejoin="round" strokeLinecap="round"/>
                  ))}
                  {/* front half — solid silhouette */}
                  {frontRuns.map((pts, si) => (
                    <polyline key={`cf${ci}-${si}`} points={pts.join(' ')}
                      fill="none"
                      stroke="var(--accent)" strokeOpacity="0.7"
                      strokeWidth="1.1"
                      strokeLinejoin="round" strokeLinecap="round"/>
                  ))}
                </g>
              );
            })}
          </g>

          {/*
            Connection arcs from AI core → every node. Rendered as
            quadratic Bézier curves that bulge outward from the globe,
            so they read as "going around" the sphere instead of
            piercing through it. Back-facing nodes use a dashed, dimmer
            stroke so the connection never disappears.
          */}
          {nodes.map((n, i) => {
            const p = project(n.lat, n.lng);
            // Control point: midpoint pushed outward along the radial
            // direction by a fixed lift — gives every arc the same
            // "rainbow over the planet" feel.
            const mx = (cx + p.x) / 2;
            const my = (cy + p.y) / 2;
            const dx = mx - cx, dy = my - cy;
            const d = Math.sqrt(dx*dx + dy*dy) || 1;
            const lift = 60; // px outward bulge
            const ctrlX = mx + (dx / d) * lift;
            const ctrlY = my + (dy / d) * lift;
            const path = `M ${cx} ${cy} Q ${ctrlX.toFixed(1)} ${ctrlY.toFixed(1)} ${p.x.toFixed(1)} ${p.y.toFixed(1)}`;

            const isFront = p.z > 0;
            // Pulse traveling along the curve
            const phase = (t * 0.55 + i / nodes.length) % 1;
            // Approximate point on Bézier at parameter `phase`
            const u = phase, iu = 1 - phase;
            const pulseX = iu*iu*cx + 2*iu*u*ctrlX + u*u*p.x;
            const pulseY = iu*iu*cy + 2*iu*u*ctrlY + u*u*p.y;

            return (
              <g key={`l${n.label}`}>
                <path d={path}
                  fill="none"
                  stroke="var(--warn)"
                  strokeOpacity={isFront ? 0.65 + p.z * 0.2 : 0.28}
                  strokeWidth="1"
                  strokeDasharray={isFront ? 'none' : '3 4'}/>
                {isFront && (
                  <circle cx={pulseX} cy={pulseY} r="2.5"
                    fill="var(--warn)" opacity={(1 - phase) * 0.95}/>
                )}
              </g>
            );
          })}

          {/* core glow */}
          <circle cx={cx} cy={cy} r="44" fill="url(#hub-core)"/>
          <circle cx={cx} cy={cy} r={22 + Math.sin(t * 1.8) * 3}
            fill="none" stroke="var(--accent)" strokeOpacity="0.7" strokeWidth="1.5"/>

          {/*
            Node dots: front-facing get a solid filled node, back-facing
            get a dim hollow ring so you can see where they are through
            the globe. Labels for every node are rendered in a separate
            always-on-top pass below.

            Each node is wrapped with an invisible large hit-circle so
            the user can hover near it easily; hovering fires hoverBus
            events that highlight the corresponding service card.
          */}
          {nodes.map((n) => {
            const p = project(n.lat, n.lng);
            const isFront = p.z > 0;
            const r = 3.5 + Math.max(p.z, 0) * 2.5;
            const isHot = hoveredNode === n.label;
            const onEnter = () => hoverBus.set(n.label);
            const onLeave = () => hoverBus.set(null);
            const hitR = 16; // generous hit radius around the node dot

            if (isFront) {
              return (
                <g key={`nf${n.label}`}
                  onMouseEnter={onEnter} onMouseLeave={onLeave}
                  style={{ cursor: 'pointer' }}
                  opacity={0.4 + p.z * 0.6}>
                  {/* invisible hit target */}
                  <circle cx={p.x} cy={p.y} r={hitR} fill="transparent"/>
                  <circle cx={p.x} cy={p.y} r={(r + 5) * (isHot ? 1.6 : 1)}
                    fill="var(--warn)" opacity={isHot ? 0.45 : 0.22}
                    style={{ transition: 'r 0.18s, opacity 0.18s' }}/>
                  <circle cx={p.x} cy={p.y} r={r * (isHot ? 1.4 : 1)}
                    fill="var(--warn)" stroke="var(--bg)" strokeWidth="1"
                    style={{ transition: 'r 0.18s' }}/>
                </g>
              );
            }
            return (
              <g key={`nb${n.label}`}
                onMouseEnter={onEnter} onMouseLeave={onLeave}
                style={{ cursor: 'pointer' }}>
                <circle cx={p.x} cy={p.y} r={hitR} fill="transparent"/>
                <circle cx={p.x} cy={p.y} r={isHot ? 4.5 : 3}
                  fill={isHot ? 'var(--warn)' : 'none'}
                  stroke="var(--warn)" strokeOpacity={isHot ? 0.9 : 0.45} strokeWidth="1"
                  style={{ transition: 'r 0.18s' }}/>
              </g>
            );
          })}

          {/* AI core label */}
          <text x={cx} y={cy + 6} textAnchor="middle"
            fontFamily="var(--font-sans)" fontSize="22" fontWeight="600"
            fill="var(--ink)">AI</text>

          {/*
            Labels: always drawn last so they stay on top of the globe
            and never get clipped or hidden, even when their node is on
            the far side of the sphere.
          */}
          {nodes.map((n, idx) => {
            // Labels track nodes smoothly. Two sources of past jitter:
            //  1. textAnchor flipping between start/middle/end at
            //     thresholds — caused visible horizontal hops.
            //  2. The "outward" offset direction collapsing when a
            //     node passed close to the globe center (d → 0),
            //     making the label jump to a random direction.
            //
            // Fix: always use textAnchor="middle" (no threshold
            // switching ever), and blend the live outward direction
            // with a stable per-node fallback angle so the offset
            // never collapses. The label glides smoothly along the
            // globe rim as the sphere spins.
            const p = project(n.lat, n.lng);
            const isFront = p.z > 0;

            const dx = p.x - cx;
            const dy = p.y - cy;
            const d = Math.hypot(dx, dy);

            // Per-node fallback angle: evenly distributed around the
            // globe in node-index order, used when the node is near
            // the center so the direction never becomes undefined.
            const fallbackA = (idx / nodes.length) * Math.PI * 2 - Math.PI / 2;
            const fx = Math.cos(fallbackA);
            const fy = Math.sin(fallbackA);

            // Blend live direction with fallback: at d >= R the label
            // follows the node perfectly; as d shrinks toward 0 the
            // fallback takes over, preventing any snap.
            const w = Math.min(1, d / (R * 0.5));
            let nx = (dx / (d || 1)) * w + fx * (1 - w);
            let ny = (dy / (d || 1)) * w + fy * (1 - w);
            const nlen = Math.hypot(nx, ny) || 1;
            nx /= nlen; ny /= nlen;

            const lx = p.x + nx * 22;
            const ly = p.y + ny * 22;

            return (
              <text key={`lb${n.label}`} x={lx} y={ly + 3}
                textAnchor="middle"
                fontFamily="var(--font-mono)" fontSize="10"
                fill={hoveredNode === n.label ? 'var(--warn)' : 'var(--ink)'}
                fillOpacity={isFront ? 1 : 0.55}
                letterSpacing="0.08em"
                onMouseEnter={() => hoverBus.set(n.label)}
                onMouseLeave={() => hoverBus.set(null)}
                style={{ textTransform: 'uppercase', cursor: 'pointer', transition: 'fill 0.18s' }}>
                {n.label}
              </text>
            );
          })}
        </svg>
      </div>
    </div>
  );
}

function ServicesBento({ headerless }) {
  const isMobile = useIsMobile(640);
  return (
    <section className="section">
      {!headerless && (
        <div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '1fr 1fr', gap: isMobile ? 16 : 80, marginBottom: isMobile ? 32 : 56, alignItems: 'end' }}>
          <Reveal>
            <div>
              <div className="mono" style={{ color: 'var(--accent)', marginBottom: 18 }}>// WHAT WE BUILD</div>
              <h2 style={{
                fontSize: 'clamp(32px, 4.5vw, 56px)', fontWeight: 500,
                letterSpacing: '-0.03em', lineHeight: 1.02, margin: 0, textWrap: 'balance',
              }}>
                Every service tied to an outcome.
              </h2>
            </div>
          </Reveal>
          <Reveal delay={120}>
            <p style={{ color: 'var(--ink-dim)', fontSize: 17, lineHeight: 1.5, margin: 0, maxWidth: 460 }}>
              We build on top of the tools you already use. No rip and replace. No new platform
              to learn. Your team keeps working in the systems they know.
            </p>
          </Reveal>
        </div>
      )}

      <div className="bento-grid" style={{
        display: 'grid',
        gridTemplateColumns: 'repeat(3, 1fr)',
        // Cap row heights so the grid doesn't balloon on ultrawide
        // (21:9) displays. Rows grow to fit content, capped at 380px
        // so cards stay in a comfortable reading range.
        gridAutoRows: 'minmax(340px, 380px)',
        gap: 24,
        width: '100%',
        // Row 3 now sized tighter — the globe hub spans rows 2–3 so
        // it still has room, and side cards 4/5 don't have dead
        // vertical space under their content.
        gridTemplateRows: 'minmax(340px, 380px) minmax(260px, 280px) minmax(280px, 300px) minmax(340px, 380px)',
      }}>
        <Reveal style={{ display: 'flex' }}>
          <ServiceCard cardId="01" eyebrow="01 · START HERE" title="AI readiness audit and workflow discovery" accent="teal">
            <p style={{ color: 'var(--ink-dim)', fontSize: 14, lineHeight: 1.55, margin: 0 }}>
              We map where admin time is being lost and where revenue is leaking. You walk away
              with a ranked list of opportunities, effort estimates, and a clear first engagement.
            </p>
            <ChecklistBlock accent="var(--accent)" items={['Workflow map of key departments', 'Time loss findings', 'Revenue leak findings', 'Ranked opportunity list']}/>
          </ServiceCard>
        </Reveal>

        <Reveal delay={60} style={{ display: 'flex' }}>
          <ServiceCard cardId="02" eyebrow="02 · REVENUE CAPTURE" title="AI phone agents" accent="violet">
            <p style={{ color: 'var(--ink-dim)', fontSize: 14, lineHeight: 1.55, margin: 0 }}>
              24/7 inbound call coverage. Never miss another lead because it rang at 7 pm.
              Qualifies, books, and hands off warmly to your team.
            </p>
            <IconRow accent="var(--accent-2)" items={['Answers in under 2s', 'Qualifies intent', 'Books directly to CRM', 'After-hours coverage']}/>
          </ServiceCard>
        </Reveal>

        <Reveal delay={120} style={{ display: 'flex' }}>
          <ServiceCard cardId="03" eyebrow="03 · ADMIN TAX" title="Workflow automation" accent="teal">
            <p style={{ color: 'var(--ink-dim)', fontSize: 14, lineHeight: 1.55, margin: 0 }}>
              Recurring data flows, approvals, and notifications run on a schedule or trigger.
              The work happens without anyone touching it. Skilled hours come back to your team.
            </p>
            <IconRow accent="var(--accent)" items={['Zero copy-paste', 'Human checkpoints', 'Works with your stack', 'Audit trail included']}/>
          </ServiceCard>
        </Reveal>

        <Reveal delay={180} style={{ display: 'flex', gridColumn: '1 / 2', gridRow: '2 / 4' }}>
          <ServiceCard cardId="04" eyebrow="04 · YOUR STACK" title="AI integration foundation" accent="teal">
            <p style={{ color: 'var(--ink-dim)', fontSize: 14, lineHeight: 1.55, margin: 0 }}>
              AI that lives inside the tools your team already uses. No new login. No retraining.
              The assistant shows up where the work happens.
            </p>
            <ChipsBlock helper="AI plugs into platforms like:" items={['ServiceTitan', 'Salesforce', 'HubSpot', 'Microsoft 365', 'NetSuite', 'Jobber', 'Procore', 'QuickBooks']}/>
          </ServiceCard>
        </Reveal>

        {/* Centered hub — grid column 2, rows 2–3 — surrounded by services */}
        <ConnectionHub/>

        <Reveal delay={240} style={{ display: 'flex', gridColumn: '3 / 4', gridRow: '2 / 4' }}>
          <ServiceCard cardId="05" eyebrow="05 · RISK" title="Contract and document review AI" accent="violet">
            <p style={{ color: 'var(--ink-dim)', fontSize: 14, lineHeight: 1.55, margin: 0 }}>
              Catch costly clauses before you sign. Flag indemnity, auto renew, liability caps,
              and payment terms that cost you money.
            </p>
            <ChecklistBlock accent="var(--accent-2)" label="WHAT WE FLAG" items={['Hidden indemnity and liability traps', 'Auto-renewal and termination clauses', 'Payment terms and late-fee triggers', 'Insurance and bonding gaps']}/>
          </ServiceCard>
        </Reveal>

        <Reveal delay={300} style={{ display: 'flex', gridRow: '4 / 5' }}>
          <ServiceCard cardId="06" eyebrow="06 · INBOX ZERO" title="Email and communication automation" accent="warn">
            <p style={{ color: 'var(--ink-dim)', fontSize: 14, lineHeight: 1.55, margin: 0 }}>
              Drafts, triage, summarization, and follow ups across Microsoft 365 and Gmail.
              Inbox becomes a task queue, not a second job.
            </p>
            <IconRow accent="var(--warn)" items={['Auto-draft replies', 'Priority triage', 'Thread summaries', 'Follow up nudges']}/>
          </ServiceCard>
        </Reveal>

        <Reveal delay={360} style={{ display: 'flex' }}>
          <ServiceCard cardId="07" eyebrow="07 · MEMORY" title="Internal knowledge AI" accent="teal">
            <p style={{ color: 'var(--ink-dim)', fontSize: 14, lineHeight: 1.55, margin: 0 }}>
              Your company's policies, procedures, and history, searchable in plain English
              with source citations every time.
            </p>
            <IconRow accent="var(--accent)" items={['Plain-English search', 'Source citations', 'Onboarding ready', 'Stays up to date']}/>
          </ServiceCard>
        </Reveal>

        <Reveal delay={420} style={{ display: 'flex' }}>
          <ServiceCard cardId="08" eyebrow="08 · OPERATE" title="Ongoing managed AI retainer" accent="violet">
            <p style={{ color: 'var(--ink-dim)', fontSize: 14, lineHeight: 1.55, margin: 0 }}>
              After go live, we monitor, tune, and extend. Monthly reviews tied to measured
              outcomes. Month to month, never locked in.
            </p>
            <IconRow accent="var(--accent-2)" items={['Monthly tuning', 'Outcome metrics', 'Kill switch', 'New workflow onboarding']}/>
          </ServiceCard>
        </Reveal>
      </div>
    </section>
  );
}

window.ServicesBento = ServicesBento;
window.ConnectionHub = ConnectionHub;
