
/* === timeline === */
// Animated timeline grid with now-line sweep + starring
const { useState, useEffect, useRef } = React;

const STAGES = [
  { name: 'Coachella', color: '#E8453C' },
  { name: 'Outdoor', color: '#3E7DE8' },
  { name: 'Sonora', color: '#2FAE6E' },
  { name: 'Mojave', color: '#F07A2A' },
  { name: 'Sahara', color: '#8A4FD6' },
  { name: 'Quasar', color: '#E8B93A' },
  { name: 'Do LaB', color: '#14A699' },
];

// sets: stageIndex, start (minutes from 0), length (min), name, starred
const SETS = [
  { stage: 0, start: 60, len: 90, name: 'Addison Rae', star: false },
  { stage: 0, start: 180, len: 75, name: 'Sabrina C.', star: true },
  { stage: 1, start: 0, len: 60, name: 'Turnstile', star: true },
  { stage: 1, start: 90, len: 90, name: 'Laufey', star: true },
  { stage: 2, start: 30, len: 40, name: 'Cachirulo', star: false },
  { stage: 2, start: 90, len: 60, name: 'French Police', star: true },
  { stage: 3, start: 20, len: 80, name: 'Moby', star: true },
  { stage: 3, start: 130, len: 75, name: 'FKA twigs', star: true },
  { stage: 4, start: 0, len: 55, name: 'KATSEYE', star: true },
  { stage: 4, start: 90, len: 60, name: 'Subtronics', star: false },
  { stage: 5, start: 60, len: 90, name: 'PAWSA', star: false },
  { stage: 6, start: 40, len: 55, name: 'Surprise', star: false },
  { stage: 6, start: 110, len: 75, name: 'Omnom', star: true },
];

// each crew member's schedule through the 4-hour window: list of {start, end, stage}.
// Only one stage at a time. Between slots they're "in transit" (not shown).
const CREW_SCHEDULE = [
  { k: 'K', c: '#D63A7A', slots: [
    { start: 0,   end: 50,  stage: 6 }, // Do LaB surprise
    { start: 55,  end: 110, stage: 3 }, // Moby at Mojave
    { start: 125, end: 200, stage: 0 }, // Sabrina C. at Coachella
  ]},
  { k: 'D', c: '#5A6EE8', slots: [
    { start: 0,   end: 55,  stage: 4 }, // KATSEYE
    { start: 65,  end: 140, stage: 5 }, // PAWSA at Quasar
    { start: 150, end: 225, stage: 3 }, // FKA twigs
  ]},
  { k: 'L', c: '#2FAE6E', slots: [
    { start: 0,   end: 60,  stage: 1 }, // Turnstile at Outdoor
    { start: 70,  end: 130, stage: 2 }, // French Police at Sonora
    { start: 140, end: 200, stage: 4 }, // Subtronics
  ]},
  { k: 'S', c: '#F07A2A', slots: [
    { start: 10,  end: 110, stage: 3 }, // Moby
    { start: 125, end: 180, stage: 0 }, // Sabrina C.
    { start: 185, end: 240, stage: 6 }, // Omnom
  ]},
];

// stage bearing: angle in degrees (0 = up/N, clockwise). Used for the little arrows.
const STAGE_BEARING = [45, 90, 0, 315, 135, 180, 225]; // one per STAGES index

function crewAtStage(stageIdx, nowMin) {
  const out = [];
  for (const crew of CREW_SCHEDULE) {
    for (const slot of crew.slots) {
      if (slot.stage === stageIdx && nowMin >= slot.start && nowMin < slot.end) {
        out.push({ k: crew.k, c: crew.c });
        break;
      }
    }
  }
  return out;
}

function DirArrow({ deg, size = 9, color = 'rgba(255,255,255,0.55)' }) {
  return (
    <svg width={size} height={size} viewBox="0 0 24 24" style={{ transform: `rotate(${deg}deg)`, transition: 'transform .4s', flexShrink: 0 }}>
      <path d="M12 3l6 8h-4v10h-4V11H6z" fill={color}/>
    </svg>
  );
}

function TimelineAnim() {
  const [starOnly, setStarOnly] = useState(false);
  const [now, setNow] = useState(110);
  const rafRef = useRef(0);
  const startedAt = useRef(0);

  useEffect(() => {
    const loop = (t) => {
      if (!startedAt.current) startedAt.current = t;
      const elapsed = (t - startedAt.current) / 1000;
      // sweep from 30 to 210 over 20s, loop
      const pos = 30 + ((elapsed * 9) % 180);
      setNow(pos);
      rafRef.current = requestAnimationFrame(loop);
    };
    rafRef.current = requestAnimationFrame(loop);
    return () => cancelAnimationFrame(rafRef.current);
  }, []);

  // auto toggle star-only every 6 seconds
  useEffect(() => {
    const i = setInterval(() => setStarOnly(v => !v), 5000);
    return () => clearInterval(i);
  }, []);

  const visible = starOnly ? SETS.filter(s => s.star) : SETS;

  // grid: 4 hours (240 min), x starts at 80 (stage label col)
  const W = 560, H = 520, labelW = 90, headerH = 32, rowH = (H - headerH) / STAGES.length;
  const minToX = (m) => labelW + (m / 240) * (W - labelW);

  return (
    <div style={{ position: 'relative' }}>
      <div style={{ position: 'absolute', inset: -40, background: 'radial-gradient(circle, rgba(232,69,60,0.10), transparent 60%)', filter: 'blur(30px)', pointerEvents: 'none' }}></div>
      <div style={{
        position: 'relative',
        width: W, maxWidth: '100%',
        background: '#0A0A0D',
        border: '1px solid rgba(255,255,255,0.08)',
        borderRadius: 20,
        overflow: 'hidden',
        fontFamily: 'Geist, sans-serif',
        boxShadow: '0 30px 60px -20px rgba(0,0,0,0.6)',
      }}>
        {/* Controls bar */}
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '14px 18px', borderBottom: '1px solid rgba(255,255,255,0.06)' }}>
          <div style={{ display: 'flex', gap: 4, background: 'rgba(255,255,255,0.06)', borderRadius: 999, padding: 3 }}>
            {['Fri', 'Sat', 'Sun'].map((d, i) => (
              <div key={d} style={{
                padding: '6px 14px', borderRadius: 999, fontSize: 13, fontWeight: 500,
                background: i === 0 ? 'rgba(255,255,255,0.14)' : 'transparent',
                color: i === 0 ? '#fff' : 'rgba(255,255,255,0.5)',
              }}>{d}</div>
            ))}
          </div>
          <button
            onClick={() => setStarOnly(v => !v)}
            style={{
              display: 'flex', alignItems: 'center', gap: 6,
              padding: '7px 12px', borderRadius: 999,
              background: starOnly ? '#E8B93A' : 'rgba(255,255,255,0.06)',
              color: starOnly ? '#000' : '#fff',
              border: 'none', cursor: 'pointer', fontSize: 12, fontWeight: 600,
              fontFamily: 'inherit',
              transition: 'all .3s',
            }}
          >
            <svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l2.9 6.9 7.1.6-5.4 4.7 1.6 7-6.2-3.8-6.2 3.8 1.6-7L2 9.5l7.1-.6z"/></svg>
            My stars
          </button>
        </div>

        {/* Grid */}
        <div style={{ position: 'relative', height: H }}>
          {/* Hour markers */}
          <div style={{ position: 'absolute', left: labelW, right: 0, top: 0, height: headerH, display: 'flex', borderBottom: '1px solid rgba(255,255,255,0.05)' }}>
            {['8PM', '9PM', '10PM', '11PM'].map((h, i) => (
              <div key={h} style={{ flex: 1, fontSize: 10, color: 'rgba(255,255,255,0.4)', fontFamily: 'Geist Mono, monospace', padding: '10px 0 0 6px', borderLeft: i > 0 ? '1px solid rgba(255,255,255,0.04)' : 'none' }}>{h}</div>
            ))}
          </div>

          {/* Stage rows */}
          {STAGES.map((s, i) => (
            <div key={s.name} style={{
              position: 'absolute', left: 0, right: 0,
              top: headerH + i * rowH, height: rowH,
              borderBottom: '1px solid rgba(255,255,255,0.04)',
              display: 'flex', alignItems: 'center',
            }}>
              <div style={{
                width: labelW, padding: '0 10px', fontSize: 11, fontWeight: 500,
                color: '#fff', height: '100%',
                display: 'flex', flexDirection: 'column', justifyContent: 'center', gap: 3,
                background: s.color + '18',
                borderLeft: `3px solid ${s.color}`,
                position: 'relative',
              }}>
                <div style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{s.name}</div>
                <div style={{ display: 'flex', alignItems: 'center', gap: 3, fontSize: 9, color: 'rgba(255,255,255,0.45)', fontFamily: 'Geist Mono, monospace' }}>
                  <DirArrow deg={STAGE_BEARING[i]} />
                  <span>{['269m', '276m', '208m', 'HERE', '350m', '110m', '250m'][i]}</span>
                </div>
                {(() => {
                  const here = crewAtStage(i, now);
                  if (here.length === 0) return null;
                  return (
                    <div style={{ display: 'flex', marginTop: 1 }}>
                      {here.map((c, ci) => (
                        <div key={c.k} style={{
                          width: 13, height: 13, borderRadius: '50%',
                          background: c.c,
                          color: '#fff',
                          fontSize: 7.5, fontWeight: 700, letterSpacing: 0,
                          display: 'flex', alignItems: 'center', justifyContent: 'center',
                          border: '1.5px solid #0A0A0D',
                          marginLeft: ci === 0 ? 0 : -3,
                          fontFamily: 'Geist, sans-serif',
                          transition: 'opacity .3s',
                        }}>{c.k}</div>
                      ))}
                    </div>
                  );
                })()}
              </div>
            </div>
          ))}

          {/* Vertical gridlines */}
          {[0, 1, 2, 3].map(i => (
            <div key={i} style={{
              position: 'absolute', top: headerH, bottom: 0,
              left: minToX(i * 60), width: 1, background: 'rgba(255,255,255,0.04)',
            }}/>
          ))}

          {/* Sets */}
          {SETS.map((set, idx) => {
            const show = !starOnly || set.star;
            const x = minToX(set.start);
            const w = (set.len / 240) * (W - labelW);
            const y = headerH + set.stage * rowH + 5;
            const h = rowH - 10;
            const s = STAGES[set.stage];
            return (
              <div key={idx} style={{
                position: 'absolute', left: x, top: y, width: w, height: h,
                background: set.star ? s.color : s.color + '30',
                border: `1px solid ${set.star ? s.color : s.color + '60'}`,
                borderRadius: 6,
                padding: '4px 6px',
                fontSize: 10, fontWeight: 600, color: set.star ? '#fff' : s.color,
                opacity: show ? 1 : 0.12,
                transform: show ? 'scale(1)' : 'scale(0.95)',
                transition: 'opacity .5s, transform .5s',
                overflow: 'hidden',
                display: 'flex', flexDirection: 'column', justifyContent: 'space-between',
              }}>
                <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'start', gap: 4 }}>
                  <span style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{set.name}</span>
                  {set.star && <svg width="9" height="9" viewBox="0 0 24 24" fill="#fff"><path d="M12 2l2.9 6.9 7.1.6-5.4 4.7 1.6 7-6.2-3.8-6.2 3.8 1.6-7L2 9.5l7.1-.6z"/></svg>}
                </div>
              </div>
            );
          })}

          {/* Now line */}
          <div style={{
            position: 'absolute', top: 0, bottom: 0,
            left: minToX(now), width: 2,
            background: '#E8453C',
            boxShadow: '0 0 12px rgba(232,69,60,0.8)',
            zIndex: 20,
            pointerEvents: 'none',
          }}>
            <div style={{
              position: 'absolute', top: 4, left: '50%', transform: 'translateX(-50%)',
              background: '#E8453C', color: '#fff', fontSize: 9, padding: '2px 6px',
              borderRadius: 4, fontFamily: 'Geist Mono, monospace', whiteSpace: 'nowrap',
            }}>
              {(() => {
                const h = Math.floor(8 + now / 60);
                const m = Math.floor(now % 60);
                return `${h > 12 ? h - 12 : h}:${String(m).padStart(2, '0')}PM`;
              })()}
            </div>
          </div>
        </div>

        {/* Caption */}
        <div style={{
          padding: '12px 18px',
          fontSize: 11, fontFamily: 'Geist Mono, monospace',
          color: 'rgba(255,255,255,0.5)',
          borderTop: '1px solid rgba(255,255,255,0.06)',
          display: 'flex', justifyContent: 'space-between',
        }}>
          <span style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
            <span style={{ width: 6, height: 6, borderRadius: 50, background: '#E8453C', boxShadow: '0 0 6px #E8453C' }}></span>
            NOW
          </span>
          <span>{visible.length} sets shown · {starOnly ? 'stars only' : 'everything'}</span>
        </div>
      </div>
    </div>
  );
}

ReactDOM.createRoot(document.getElementById('timeline-anim-root')).render(<TimelineAnim />);

/* === crewmap === */
// Live map with moving crew dots
const { useState: useStateCM, useEffect: useEffectCM, useRef: useRefCM } = React;

const CREW = [
  { name: 'Karl', emoji: '🤠', color: '#D63A7A', path: [[30, 45], [40, 50], [45, 55], [42, 60]] },
  { name: 'Dan',  emoji: '🎸', color: '#5A6EE8', path: [[60, 25], [55, 30], [50, 35], [52, 40]] },
  { name: 'Louise', emoji: '🎨', color: '#2FAE6E', path: [[70, 60], [65, 55], [55, 50], [50, 48]] },
  { name: 'Scott', emoji: '🔥', color: '#F07A2A', path: [[50, 75], [48, 70], [45, 68], [43, 65]] },
];

const STAGE_PINS = [
  { name: 'Laufey', stage: 'Outdoor', x: 62, y: 28, color: '#3E7DE8' },
  { name: 'FKA twigs', stage: 'Mojave', x: 40, y: 72, color: '#F07A2A' },
  { name: 'French Police', stage: 'Sonora', x: 38, y: 50, color: '#2FAE6E' },
  { name: 'Subtronics', stage: 'Sahara', x: 75, y: 75, color: '#8A4FD6' },
];

function CrewMap() {
  const [t, setT] = useStateCM(0);
  useEffectCM(() => {
    let raf; let start = 0;
    const loop = (ts) => {
      if (!start) start = ts;
      setT(((ts - start) / 8000) % 1);
      raf = requestAnimationFrame(loop);
    };
    raf = requestAnimationFrame(loop);
    return () => cancelAnimationFrame(raf);
  }, []);

  // interpolate along path
  const crewPos = (c) => {
    const segs = c.path.length - 1;
    const total = t * segs;
    const i = Math.floor(total) % segs;
    const f = total - Math.floor(total);
    const [ax, ay] = c.path[i];
    const [bx, by] = c.path[i + 1];
    return [ax + (bx - ax) * f, ay + (by - ay) * f];
  };

  return (
    <div style={{ position: 'relative', display: 'flex', justifyContent: 'center' }}>
      <div style={{ position: 'absolute', inset: -40, background: 'radial-gradient(circle, rgba(47,174,110,0.12), transparent 60%)', filter: 'blur(40px)', pointerEvents: 'none' }}></div>

      <div style={{
        position: 'relative',
        width: 440, aspectRatio: '440/520', maxWidth: '100%',
        background: '#0B1714',
        borderRadius: 24,
        overflow: 'hidden',
        border: '1px solid rgba(255,255,255,0.08)',
        boxShadow: '0 30px 60px -20px rgba(0,0,0,0.6)',
        fontFamily: 'Geist, sans-serif',
      }}>
        {/* Map-ish background */}
        <svg width="100%" height="100%" viewBox="0 0 440 520" style={{ position: 'absolute', inset: 0 }}>
          <defs>
            <pattern id="mapgrid" 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="1"/>
            </pattern>
          </defs>
          <rect width="440" height="520" fill="url(#mapgrid)"/>
          {/* Roads */}
          <path d="M 0 260 L 440 260" stroke="rgba(255,255,255,0.06)" strokeWidth="30" />
          <path d="M 220 0 L 220 520" stroke="rgba(255,255,255,0.06)" strokeWidth="30" />
          {/* Buildings */}
          {[[50,50,60,80],[160,80,50,40],[250,60,70,50],[340,30,60,100],[60,300,40,60],[140,340,50,40],[280,300,50,80],[350,380,60,50],[80,430,80,50]].map((b, i) => (
            <rect key={i} x={b[0]} y={b[1]} width={b[2]} height={b[3]} fill="rgba(255,255,255,0.04)" rx="2"/>
          ))}
          <text x="395" y="200" fill="rgba(255,255,255,0.3)" fontSize="10" textAnchor="end" transform="rotate(-90 395 200)" fontFamily="Geist Mono, monospace">MONROE ST</text>
        </svg>

        {/* Header */}
        <div style={{ position: 'absolute', top: 0, left: 0, right: 0, padding: '14px 16px', display: 'flex', justifyContent: 'space-between', alignItems: 'center', zIndex: 5 }}>
          <div style={{ width: 32, height: 32, borderRadius: 50, background: 'rgba(0,0,0,0.5)', border: '1px solid rgba(255,255,255,0.1)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="1.5"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83 0 2 2 0 010-2.83l.06-.06a1.65 1.65 0 00.33-1.82 1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 010-2.83 2 2 0 012.83 0l.06.06a1.65 1.65 0 001.82.33H9a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 0 2 2 0 010 2.83l-.06.06a1.65 1.65 0 00-.33 1.82V9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z"/></svg>
          </div>
          <div style={{ fontSize: 14, fontWeight: 500 }}>Map</div>
          <div style={{ display: 'flex', gap: 4, background: 'rgba(0,0,0,0.5)', padding: 3, borderRadius: 999, border: '1px solid rgba(255,255,255,0.1)' }}>
            <div style={{ padding: '4px 8px', borderRadius: 999 }}>📢</div>
            <div style={{ padding: '4px 8px', borderRadius: 999, background: 'rgba(62,125,232,0.3)' }}>🗺️</div>
          </div>
        </div>

        {/* Stage pins */}
        {STAGE_PINS.map(p => (
          <div key={p.name} style={{
            position: 'absolute',
            left: `${p.x}%`, top: `${p.y}%`, transform: 'translate(-50%, -50%)',
            background: p.color, color: '#fff', fontSize: 10, fontWeight: 600,
            padding: '3px 10px', borderRadius: 999, whiteSpace: 'nowrap',
            boxShadow: '0 2px 8px rgba(0,0,0,0.4)',
          }}>
            {p.name}
            <div style={{ fontSize: 8, color: 'rgba(255,255,255,0.8)', fontWeight: 400, marginTop: 1, textAlign: 'center' }}>{p.stage}</div>
          </div>
        ))}

        {/* Crew dots */}
        {CREW.map(c => {
          const [x, y] = crewPos(c);
          return (
            <div key={c.name} style={{
              position: 'absolute', left: `${x}%`, top: `${y}%`,
              transform: 'translate(-50%, -50%)',
              transition: 'none',
              zIndex: 10,
            }}>
              {/* Pulse */}
              <div style={{
                position: 'absolute', left: '50%', top: '50%', transform: 'translate(-50%, -50%)',
                width: 50, height: 50, borderRadius: '50%',
                background: c.color, opacity: 0.3,
                animation: 'crewPulse 2s ease-out infinite',
              }}/>
              {/* Pill */}
              <div style={{
                background: c.color, color: '#fff', fontSize: 11, fontWeight: 600,
                padding: '4px 10px 4px 4px', borderRadius: 999,
                display: 'flex', alignItems: 'center', gap: 6,
                border: '2px solid #fff',
                boxShadow: '0 4px 12px rgba(0,0,0,0.4)',
                position: 'relative',
              }}>
                <span style={{ width: 20, height: 20, borderRadius: '50%', background: 'rgba(255,255,255,0.3)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 12 }}>{c.emoji}</span>
                <span>{c.name}</span>
              </div>
              <div style={{ width: 1, height: 8, background: '#fff', margin: '0 auto', opacity: 0.5 }}/>
              <div style={{ width: 6, height: 6, borderRadius: '50%', background: '#fff', margin: '0 auto', boxShadow: '0 0 8px #fff' }}/>
            </div>
          );
        })}

        {/* Bottom card for Dan */}
        <div style={{ position: 'absolute', left: 16, right: 16, bottom: 16, background: 'rgba(15,25,22,0.95)', backdropFilter: 'blur(10px)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: 16, padding: 12 }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 10 }}>
            <div style={{ width: 32, height: 32, borderRadius: 50, background: '#5A6EE8', color: '#fff', fontWeight: 600, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 13 }}>D</div>
            <div style={{ flex: 1 }}>
              <div style={{ fontSize: 13, fontWeight: 600 }}>Dan</div>
              <div style={{ fontSize: 10, color: 'rgba(255,255,255,0.5)', fontFamily: 'Geist Mono, monospace', display: 'flex', alignItems: 'center', gap: 4 }}>
                <svg width="8" height="8" viewBox="0 0 24 24" fill="currentColor"><path d="M2 12a10 10 0 0120 0"/></svg>
                10m ago · Outdoor Theatre
              </div>
            </div>
            <div style={{ fontSize: 18, color: 'rgba(255,255,255,0.4)' }}>×</div>
          </div>
          <div style={{ display: 'flex', gap: 8 }}>
            <div style={{ flex: 1, background: '#3E7DE8', color: '#fff', padding: '8px 0', borderRadius: 999, textAlign: 'center', fontSize: 12, fontWeight: 600 }}>↗ Find</div>
            <div style={{ flex: 1, background: 'rgba(255,255,255,0.08)', color: '#fff', padding: '8px 0', borderRadius: 999, textAlign: 'center', fontSize: 12, fontWeight: 500 }}>💬 Message</div>
          </div>
        </div>

        <style>{`
          @keyframes crewPulse {
            0% { transform: translate(-50%, -50%) scale(0.6); opacity: 0.5; }
            100% { transform: translate(-50%, -50%) scale(2.2); opacity: 0; }
          }
        `}</style>
      </div>
    </div>
  );
}

ReactDOM.createRoot(document.getElementById('crew-map-root')).render(<CrewMap />);

/* === precision === */
// Precision Finding: rotating arrow + shrinking distance
const { useState: useStateP, useEffect: useEffectP } = React;

function PrecisionAnim() {
  const [phase, setPhase] = useStateP(0); // 0-1 over 10s cycle
  useEffectP(() => {
    let raf; let start = 0;
    const loop = (t) => {
      if (!start) start = t;
      setPhase((((t - start) / 10000) % 1));
      raf = requestAnimationFrame(loop);
    };
    raf = requestAnimationFrame(loop);
    return () => cancelAnimationFrame(raf);
  }, []);

  // Approach sequence: 75m GPS -> 40m UWB -> 12m -> 2m "here"
  const seq = [
    { d: 75, unit: 'm', kind: 'GPS', rot: 42 },
    { d: 40, unit: 'm', kind: 'UWB', rot: 18 },
    { d: 12, unit: 'm', kind: 'UWB', rot: -8 },
    { d: 2,  unit: 'm', kind: 'UWB', rot: 0 },
  ];
  const stepF = phase * seq.length;
  const idx = Math.min(Math.floor(stepF), seq.length - 1);
  const f = stepF - idx;
  const cur = seq[idx];
  const next = seq[Math.min(idx + 1, seq.length - 1)];
  const d = cur.d + (next.d - cur.d) * f;
  const rot = cur.rot + (next.rot - cur.rot) * f;
  const isUWB = d <= 40;

  return (
    <div style={{ position: 'relative', display: 'flex', justifyContent: 'center', padding: 10 }}>
      <div style={{ position: 'absolute', inset: -40, background: 'radial-gradient(circle, rgba(90,110,232,0.18), transparent 60%)', filter: 'blur(40px)', pointerEvents: 'none' }}></div>

      <div style={{
        position: 'relative',
        width: 320, aspectRatio: '280/580', maxWidth: '100%',
        background: '#000',
        borderRadius: 53,
        boxShadow: '0 0 0 2px #1a1a1a, 0 0 0 10px #0a0a0a, 0 40px 80px -20px rgba(0,0,0,0.7)',
        fontFamily: 'Geist, sans-serif',
        overflow: 'hidden',
      }}>
        <div style={{ position: 'absolute', top: 14, left: '50%', transform: 'translateX(-50%)', width: 100, height: 28, background: '#000', borderRadius: 999, zIndex: 10 }}/>

        <div style={{ position: 'absolute', inset: 6, borderRadius: 47, overflow: 'hidden', background: '#000', color: '#fff' }}>
          {/* Top bar */}
          <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '50px 24px 14px' }}>
            <div style={{ background: 'rgba(255,255,255,0.12)', padding: '6px 14px', borderRadius: 999, fontSize: 13 }}>Done</div>
            <div style={{ fontWeight: 600, fontSize: 15 }}>Finding Karl</div>
            <div style={{ width: 30, height: 30, borderRadius: 50, background: 'rgba(255,255,255,0.12)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
              <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="1.5"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4M12 8h.01"/></svg>
            </div>
          </div>

          {/* Direction ring with confidence arc at top */}
          <div style={{ position: 'relative', margin: '40px auto 0', width: 260, height: 260, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
            {(() => {
              // arc width (degrees) = angular uncertainty of direction
              // Far away: tight directional lock (small arc)
              // Very close: angle becomes noisy (wide arc) — and GPS is always loose
              const arcWidth = isUWB
                ? (d < 5 ? 80 : d < 15 ? 40 : 22)
                : 70;
              const ringColor = isUWB ? '#5A6EE8' : 'rgba(255,255,255,0.18)';
              const arcColor = isUWB ? '#5A6EE8' : 'rgba(255,255,255,0.55)';
              const R = 120;
              const C = 2 * Math.PI * R;
              const arcLen = (arcWidth / 360) * C;
              const gapLen = C - arcLen;
              return (
                <svg width="260" height="260" viewBox="0 0 260 260" style={{ position: 'absolute', transform: `rotate(${rot}deg)`, transition: 'transform .8s ease-out' }}>
                  {/* Base ring */}
                  <circle cx="130" cy="130" r={R} fill="none" stroke={ringColor} strokeWidth={isUWB ? 2 : 1.5} style={{ transition: 'stroke .5s' }}/>
                  {/* Direction arc at top (rotate -90deg so dash starts at top) */}
                  <circle
                    cx="130" cy="130" r={R}
                    fill="none"
                    stroke={arcColor}
                    strokeWidth={isUWB ? 6 : 5}
                    strokeLinecap="round"
                    strokeDasharray={`${arcLen} ${gapLen}`}
                    strokeDashoffset={arcLen / 2}
                    transform="rotate(-90 130 130)"
                    style={{ transition: 'stroke .5s, stroke-dasharray .6s' }}
                  />
                  {isUWB && (
                    <circle cx="130" cy="130" r={R} fill="none" stroke="#5A6EE8" strokeWidth="10" opacity="0.18" strokeLinecap="round" strokeDasharray={`${arcLen} ${gapLen}`} strokeDashoffset={arcLen / 2} transform="rotate(-90 130 130)" />
                  )}
                </svg>
              );
            })()}

            {/* Distance text */}
            <div style={{ textAlign: 'center', zIndex: 2 }}>
              <div style={{ fontSize: 52, fontWeight: 700, letterSpacing: '-0.04em', lineHeight: 1 }}>
                {d < 3 ? 'HERE' : `~${Math.round(d)}${cur.unit}`}
              </div>
              <div style={{ fontSize: 11, color: 'rgba(255,255,255,0.5)', marginTop: 8, fontFamily: 'Geist Mono, monospace', textTransform: 'uppercase', letterSpacing: '0.1em' }}>
                {isUWB ? 'UWB · 3cm accuracy' : 'GPS · last seen now'}
              </div>
            </div>
          </div>

          {/* Status pill */}
          <div style={{ position: 'absolute', bottom: 50, left: 24, right: 24 }}>
            <div style={{
              background: isUWB ? 'rgba(90,110,232,0.2)' : 'rgba(255,255,255,0.06)',
              border: `1px solid ${isUWB ? 'rgba(90,110,232,0.4)' : 'rgba(255,255,255,0.08)'}`,
              borderRadius: 999, padding: '14px 20px',
              display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 10,
              transition: 'all .5s',
            }}>
              <div style={{
                width: 8, height: 8, borderRadius: '50%',
                background: isUWB ? '#5A6EE8' : 'rgba(255,255,255,0.4)',
                boxShadow: isUWB ? '0 0 8px #5A6EE8' : 'none',
              }}/>
              <span style={{ fontSize: 14, fontWeight: 500, color: isUWB ? '#fff' : 'rgba(255,255,255,0.5)' }}>
                {isUWB ? 'Precision Finding enabled' : 'Enable Precision Finding'}
              </span>
            </div>
            <div style={{ textAlign: 'center', fontSize: 11, color: 'rgba(255,255,255,0.4)', marginTop: 10, fontFamily: 'Geist Mono, monospace' }}>
              {d > 40 ? `Move closer (~${Math.round(d)}m away, need < 40m)` : 'Pointing at Karl'}
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

ReactDOM.createRoot(document.getElementById('precision-anim-root')).render(<PrecisionAnim />);

/* === mesh === */
// Mesh hop animation: phone -> radio -> radio -> phone
const { useState: useStateM, useEffect: useEffectM } = React;

function MeshAnim() {
  const [t, setT] = useStateM(0);
  const [isPortrait, setIsPortrait] = useStateM(
    () => typeof window !== 'undefined' && window.innerWidth < 720
  );
  useEffectM(() => {
    let raf; let start = 0;
    const loop = (ts) => {
      if (!start) start = ts;
      setT((ts - start) / 1000); // seconds
      raf = requestAnimationFrame(loop);
    };
    raf = requestAnimationFrame(loop);
    return () => cancelAnimationFrame(raf);
  }, []);
  useEffectM(() => {
    const onResize = () => setIsPortrait(window.innerWidth < 720);
    window.addEventListener('resize', onResize);
    return () => window.removeEventListener('resize', onResize);
  }, []);

  // 4 device pairs (phone + mesh radio) placed around the canvas.
  // On narrow (portrait) screens, we swap to a taller viewBox with
  // devices laid out vertically so the pairs don't shrink to thumbnails.
  const W = isPortrait ? 600 : 1040;
  const H = isPortrait ? 900 : 520;
  const devices = isPortrait
    ? [
        { id: 'K', name: 'Karl',   x: 160, y: 180, color: '#E85A8F' },
        { id: 'D', name: 'Dan',    x: 460, y: 340, color: '#5A6EE8' },
        { id: 'L', name: 'Louise', x: 180, y: 560, color: '#2FAE6E' },
        { id: 'S', name: 'Scott',  x: 440, y: 740, color: '#E8A04A' },
      ]
    : [
        { id: 'K', name: 'Karl',   x: 150, y: 155, color: '#E85A8F' },
        { id: 'D', name: 'Dan',    x: 870, y: 215, color: '#5A6EE8' },
        { id: 'L', name: 'Louise', x: 760, y: 415, color: '#2FAE6E' },
        { id: 'S', name: 'Scott',  x: 260, y: 360, color: '#E8A04A' },
      ];

  // Scheduled transmissions. Each message: sender, type, at (s), text, relay (optional device id)
  // Ring expands from sender, reaches each receiver at a time prop to distance.
  const CYCLE = 28;
  const schedule = [
    { at: 0.5,  from: 'K', type: 'POS', text: 'at Gobi' },
    { at: 4.0,  from: 'D', type: 'POS', text: 'at Outdoor Theatre' },
    { at: 7.5,  from: 'L', type: 'MSG', text: 'where u at?', relay: 'S' },
    { at: 11.5, from: 'S', type: 'POS', text: 'at Mojave' },
    { at: 15.0, from: 'D', type: 'MSG', text: 'by the Mojave', relay: 'L' },
    { at: 18.5, from: 'L', type: 'POS', text: 'at Heineken House' },
    { at: 22.0, from: 'K', type: 'MSG', text: 'meet at Sonora?' },
    { at: 25.5, from: 'S', type: 'POS', text: 'at Mojave' },
  ];

  const now = t % CYCLE;

  // Ring speed: pixels/sec. Slower so the wave reads clearly.
  const SPEED = 380;
  const RING_LIFE = 3.2;
  const MSG_LIFE = 3.8; // how long bubbles on receivers stay visible

  const activeRings = [];
  const blips = []; // { id, age, color }
  const receiveBubbles = []; // bubbles anchored on each RECEIVER

  const dist = (a, b) => Math.hypot(a.x - b.x, a.y - b.y);

  schedule.forEach((m) => {
    const age = now - m.at;
    if (age < 0 || age > MSG_LIFE + 2) return;
    const sender = devices.find(d => d.id === m.from);
    // Ring colored by SENDER
    if (age < RING_LIFE) {
      activeRings.push({ x: sender.x, y: sender.y, age, color: sender.color });
    }
    // For each other device, compute when the wave arrives — show blip + bubble
    devices.forEach(d => {
      if (d.id === m.from) return;
      const arriveT = dist(sender, d) / SPEED;
      const dt = age - arriveT;
      if (dt < 0) return;
      if (dt < 0.7) {
        blips.push({ x: d.x, y: d.y, age: dt, color: sender.color, id: d.id });
      }
      if (dt < MSG_LIFE) {
        receiveBubbles.push({
          receiverId: d.id,
          x: d.x, y: d.y,
          age: dt,
          senderName: sender.name,
          senderColor: sender.color,
          text: m.text,
          type: m.type,
        });
      }
    });
    // Relay pulse
    if (m.relay && age > 0.15 && age < 1.2) {
      const r = devices.find(d => d.id === m.relay);
      if (r) blips.push({ x: r.x, y: r.y, age: (age - 0.15), color: sender.color, id: r.id, relay: true });
    }
  });

  // Per receiver, show only the most recent bubble (prevent stacking)
  const bubbleByReceiver = {};
  receiveBubbles.forEach(b => {
    const cur = bubbleByReceiver[b.receiverId];
    if (!cur || b.age < cur.age) bubbleByReceiver[b.receiverId] = b;
  });
  const bubbles = Object.values(bubbleByReceiver);

  return (
    <div style={{ position: 'relative' }}>
      <div style={{
        background: '#0A0A0D',
        border: '1px solid rgba(255,255,255,0.08)',
        borderRadius: 24,
        padding: isPortrait ? '20px 18px 20px' : '60px 32px 32px',
        position: 'relative',
        overflow: 'hidden',
      }}>
        <div style={isPortrait ? {
          display: 'flex', flexDirection: 'column', gap: 4,
          fontFamily: 'Geist Mono, monospace', fontSize: 10, color: 'rgba(255,255,255,0.4)',
          textTransform: 'uppercase', letterSpacing: '0.12em', marginBottom: 14,
        } : { position: 'absolute', top: 20, left: 24, fontFamily: 'Geist Mono, monospace', fontSize: 11, color: 'rgba(255,255,255,0.4)', textTransform: 'uppercase', letterSpacing: '0.15em' }}>
          Peer-to-peer · 902–928 MHz · ~2 km per hop
        </div>
        <div style={isPortrait ? {
          display: 'flex', alignItems: 'center', gap: 6,
          fontFamily: 'Geist Mono, monospace', fontSize: 10, color: 'rgba(255,255,255,0.4)',
          textTransform: 'uppercase', letterSpacing: '0.12em', marginBottom: 10,
        } : { position: 'absolute', top: 20, right: 24, fontFamily: 'Geist Mono, monospace', fontSize: 11, color: 'rgba(255,255,255,0.4)', textTransform: 'uppercase', letterSpacing: '0.15em', display: 'flex', alignItems: 'center', gap: 8 }}>
          <span style={{ width: 6, height: 6, borderRadius: '50%', background: '#2FAE6E', boxShadow: '0 0 6px #2FAE6E' }}/>
          LIVE · no carrier needed
        </div>

        <svg width="100%" viewBox={`0 0 ${W} ${H}`} style={{ maxWidth: '100%', display: 'block', height: 'auto' }}>
          {/* Grid */}
          <defs>
            <pattern id="grid-sm" width="24" height="24" patternUnits="userSpaceOnUse">
              <path d="M 24 0 L 0 0 0 24" fill="none" stroke="rgba(255,255,255,0.04)" strokeWidth="1"/>
            </pattern>
            <pattern id="grid-lg" width="120" height="120" patternUnits="userSpaceOnUse">
              <path d="M 120 0 L 0 0 0 120" fill="none" stroke="rgba(255,255,255,0.08)" strokeWidth="1"/>
            </pattern>
          </defs>
          <rect width={W} height={H} fill="url(#grid-sm)"/>
          <rect width={W} height={H} fill="url(#grid-lg)"/>

          {/* Expanding rings — with radial gradient so the wave has a soft inner fill */}
          <defs>
            {devices.map(d => (
              <radialGradient key={'rg' + d.id} id={`ringFill-${d.id}`}>
                <stop offset="0%" stopColor={d.color} stopOpacity="0"/>
                <stop offset="70%" stopColor={d.color} stopOpacity="0"/>
                <stop offset="92%" stopColor={d.color} stopOpacity="0.18"/>
                <stop offset="100%" stopColor={d.color} stopOpacity="0.02"/>
              </radialGradient>
            ))}
          </defs>
          {activeRings.map((r, i) => {
            const radius = r.age * SPEED;
            const lifeF = r.age / RING_LIFE;
            const opacity = Math.max(0, 1 - lifeF);
            const senderId = devices.find(d => d.color === r.color)?.id;
            return (
              <g key={'ring' + i}>
                {/* soft filled wave */}
                <circle cx={r.x} cy={r.y} r={radius} fill={`url(#ringFill-${senderId})`} opacity={opacity * 0.9}/>
                {/* crisp leading edge */}
                <circle cx={r.x} cy={r.y} r={radius} fill="none" stroke={r.color} strokeWidth="1.5" opacity={opacity * 0.75}/>
              </g>
            );
          })}

          {/* Device pairs */}
          {devices.map((d) => {
            const blip = blips.filter(b => b.id === d.id).sort((a, b) => a.age - b.age)[0];
            const blipStrength = blip ? Math.max(0, 1 - blip.age / 0.7) : 0;
            const blipColor = blip ? blip.color : d.color;
            return (
              <g key={d.id} transform={`translate(${d.x} ${d.y}) scale(1.25)`}>
                {/* Blip halo */}
                {blip && (
                  <circle cx="0" cy="0" r={28 + (1 - blipStrength) * 18} fill="none" stroke={blipColor} strokeWidth="2" opacity={blipStrength * 0.8}/>
                )}
                {/* Phone (center) */}
                <g transform="translate(0 0)">
                  <rect x="-12" y="-22" width="24" height="44" rx="4" fill="#111" stroke={d.color} strokeWidth="1.5" opacity="0.9"/>
                  <rect x="-9" y="-18" width="18" height="32" rx="2" fill="#000"/>
                  <circle cx="0" cy="18" r="1.5" fill="rgba(255,255,255,0.3)"/>
                </g>
                {/* Mesh radio — clipped onto the lower-left of the phone */}
                <g transform="translate(-10 9) scale(0.68)">
                  <rect x="-16" y="-12" width="32" height="24" rx="4" fill="#1a1a1f" stroke="rgba(255,255,255,0.45)" strokeWidth="1.4"/>
                  <circle cx="-8" cy="0" r="2" fill={blip ? blipColor : '#2FAE6E'} opacity={blip ? 1 : 0.9}>
                    {blip && <animate attributeName="opacity" values="1;0.3;1" dur="0.3s"/>}
                  </circle>
                  <rect x="-2" y="-6" width="10" height="12" rx="1" fill="none" stroke="rgba(255,255,255,0.35)" strokeWidth="1.3"/>
                  {/* Stubby antenna poking out the corner */}
                  <line x1="-14" y1="-10" x2="-22" y2="-18" stroke="rgba(255,255,255,0.55)" strokeWidth="1.4"/>
                  <circle cx="-22" cy="-18" r="2" fill="rgba(255,255,255,0.6)"/>
                </g>
                {/* Avatar dot */}
                <circle cx="0" cy="-44" r="13" fill={d.color}/>
                <text x="0" y="-40" fill="#fff" fontSize="12" fontWeight="700" textAnchor="middle" fontFamily="Geist">{d.id}</text>
                {/* Name */}
                <text x="0" y="48" fill="rgba(255,255,255,0.75)" fontSize="11" textAnchor="middle" fontFamily="Geist" fontWeight="500">{d.name}</text>
              </g>
            );
          })}

          {/* Bubbles — shown ABOVE each RECEIVER, indicating what they just got */}
          {bubbles.map((b, i) => {
            // entry/exit fade
            const opacity = b.age < 0.25 ? b.age / 0.25 : Math.max(0, 1 - (b.age - 2.6) / 1.0);
            if (opacity <= 0) return null;
            const lift = Math.min(b.age * 18, 22);
            const label = `${b.senderName}: ${b.text}`;
            const w = 28 + label.length * 6.4;
            const isPos = b.type === 'POS';
            return (
              <g key={'rb' + i} opacity={opacity} transform={`translate(${b.x} ${b.y - 100 - lift})`}>
                {/* tail */}
                <path d={`M -6 14 L 0 22 L 6 14 Z`} fill="#16171C" stroke={b.senderColor} strokeWidth="1"/>
                <rect x={-w/2} y="-15" width={w} height="30" rx="15" fill="#16171C" stroke={b.senderColor} strokeWidth="1"/>
                {/* icon */}
                <g transform={`translate(${-w/2 + 14} 0)`}>
                  {isPos ? (
                    // pin
                    <g stroke={b.senderColor} fill="none" strokeWidth="1.5">
                      <path d="M 0 -7 C 3.5 -7 6 -4.5 6 -1 C 6 3 0 9 0 9 C 0 9 -6 3 -6 -1 C -6 -4.5 -3.5 -7 0 -7 Z" fill={b.senderColor} fillOpacity="0.25"/>
                      <circle cx="0" cy="-1" r="2" fill={b.senderColor}/>
                    </g>
                  ) : (
                    // chat bubble
                    <g stroke={b.senderColor} fill={b.senderColor} fillOpacity="0.25" strokeWidth="1.5">
                      <path d="M -6 -4 C -6 -6 -4.5 -7 -3 -7 L 5 -7 C 6.5 -7 8 -6 8 -4 L 8 1 C 8 3 6.5 4 5 4 L 1 4 L -2 7 L -2 4 L -3 4 C -4.5 4 -6 3 -6 1 Z"/>
                    </g>
                  )}
                </g>
                <text x={-w/2 + 28} y="4" fill="#fff" fontSize="12" fontFamily="Geist" fontWeight="500" textAnchor="start">
                  <tspan fill={b.senderColor} fontWeight="600">{b.senderName}:</tspan>
                  <tspan dx="4" fill="rgba(255,255,255,0.9)">{b.text}</tspan>
                </text>
              </g>
            );
          })}
        </svg>

        {/* Legend */}
        <div style={{ display: 'flex', justifyContent: 'center', gap: 28, marginTop: 20, fontSize: 12, color: 'rgba(255,255,255,0.55)', fontFamily: 'Geist Mono, monospace', flexWrap: 'wrap' }}>
          <span style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
            <svg width="14" height="14" viewBox="-10 -10 20 20" fill="none" stroke="rgba(255,255,255,0.7)" strokeWidth="1.5">
              <path d="M 0 -7 C 3.5 -7 6 -4.5 6 -1 C 6 3 0 9 0 9 C 0 9 -6 3 -6 -1 C -6 -4.5 -3.5 -7 0 -7 Z"/>
              <circle cx="0" cy="-1" r="2"/>
            </svg>
            Position beacon
          </span>
          <span style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
            <svg width="14" height="14" viewBox="-10 -10 20 20" fill="none" stroke="rgba(255,255,255,0.7)" strokeWidth="1.5">
              <path d="M -6 -4 C -6 -6 -4.5 -7 -3 -7 L 5 -7 C 6.5 -7 8 -6 8 -4 L 8 1 C 8 3 6.5 4 5 4 L 1 4 L -2 7 L -2 4 L -3 4 C -4.5 4 -6 3 -6 1 Z"/>
            </svg>
            Chat message
          </span>
          <span style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0110 0v4"/></svg>
            End-to-end encrypted
          </span>
        </div>
      </div>
    </div>
  );
}

ReactDOM.createRoot(document.getElementById('mesh-anim-root')).render(<MeshAnim />);

