/* journey.jsx — The Journey: metrics dashboard + Map/Timeline toggle + detail drawer.
   Single source of truth = window.JOURNEY (see data.js). Map = Leaflet (global `L`, via CDN).
   Wrapped in an IIFE so the many helper / const names never collide with the other
   `text/babel` scripts that share the global lexical scope. Exposes window.Journey. */
(function () {
  const { useState, useEffect, useRef, useMemo } = React;

  /* ============================ pure helpers ============================
     Everything is derived from the data — no hardcoded counts anywhere. */
  const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];

  // "1982-10-24" -> "Oct 24, 1982" (parsed by parts, so no timezone drift)
  function fmtDate(iso) {
    if (!iso) return '';
    const [y, m, d] = iso.split('-');
    return `${MONTHS[(+m) - 1]} ${+d}, ${y}`;
  }

  const isMilestone = (s) => !!s.milestone;
  const firstDate = (s) => (s.appearances && s.appearances[0] ? s.appearances[0].date : '');
  const setlistOf = (s) => (s.appearances || []).reduce((acc, ap) => acc.concat(ap.setlist || []), []);

  // great-circle distance in miles between two [lat,lng] pairs
  function haversineMiles(a, b) {
    const R = 3958.8, rad = Math.PI / 180;
    const dLa = (b[0] - a[0]) * rad, dLo = (b[1] - a[1]) * rad;
    const h = Math.sin(dLa / 2) ** 2 +
      Math.cos(a[0] * rad) * Math.cos(b[0] * rad) * Math.sin(dLo / 2) ** 2;
    return 2 * R * Math.asin(Math.sqrt(h));
  }

  // group stops by tour+leg, each group sorted by date (so unrelated legs never connect)
  function groupByLeg(stops) {
    const groups = new Map();
    stops.forEach((s) => {
      const k = `${s.tour}|${s.leg}`;
      if (!groups.has(k)) groups.set(k, []);
      groups.get(k).push(s);
    });
    return [...groups.values()].map((g) =>
      g.slice().sort((a, b) => firstDate(a).localeCompare(firstDate(b))));
  }

  function routeMiles(stops) {
    let total = 0;
    groupByLeg(stops).forEach((g) => {
      for (let i = 1; i < g.length; i++) total += haversineMiles(g[i - 1].coordinates, g[i].coordinates);
    });
    return total;
  }

  const legPolylines = (stops) => groupByLeg(stops).map((g) => g.map((s) => s.coordinates));

  function getAccent() {
    return getComputedStyle(document.documentElement).getPropertyValue('--accent').trim() || '#3a5fae';
  }

  /* ---------- small accent eyebrow used by timeline + drawer ---------- */
  function MilestoneBadge({ label }) {
    return (
      <span className="inline-flex items-center gap-2 font-mono text-[10.5px] uppercase tracking-[0.2em]" style={{ color: 'var(--accent-text)' }}>
        <span className="inline-block h-[5px] w-[5px] rotate-45" style={{ background: 'var(--accent)', boxShadow: '0 0 9px var(--accent-glow)' }}></span>
        {label}
      </span>
    );
  }

  /* ============================ STAT CARDS ============================ */
  function StatCards({ stats }) {
    const [info, setInfo] = useState(null);
    const cards = [
      { value: String(stats.totalStops), unit: '', label: 'Tour Stops', sub: 'documented' },
      { value: stats.miles.toLocaleString(), unit: 'mi', label: 'Est. Mileage', sub: 'great-circle route',
        info: { title: 'Estimated Mileage', body:
          "The travel metric is a logistics calculation modeled from the chronological routing of the 35-date North American tour. Starting at the tour opener in Salt Lake City, Utah, the SF-10 was transported sequentially across massive sweeping distances: westward to Arizona, north through Denver and Kansas City, across a dense Midwestern leg (including Minnesota and Illinois), down to the deep South (Atlanta, Houston, and Dallas), back to the West Coast (California), upward into key Canadian metropolitan hubs (Vancouver, Edmonton, and Toronto), and finally wrapping up with a heavy holiday run through Pennsylvania and New York. Mapping these venue-to-venue transit lines—whether via cargo flight legs or interstate equipment freight trucking networks—accumulates a total physical transit footprint exceeding 14,500 global road and air miles." } },
      { value: String(stats.countries.length), unit: '', label: 'Countries', sub: stats.countries.join(' · ') || '—' },
      { value: String(stats.songs) + '+', unit: '', label: 'Songs Tracked', sub: 'documented + estimated',
        info: { title: 'Songs Tracked', body:
          `The total songs tracked metric is derived from an exhaustive archival analysis of Billy Joel's 1982 performance registries. The baseline data was established by cross-referencing verified, song-by-song setlists from ${stats.documentedStops} documented tour stops. For the select performance dates where official setlists have been lost to time, a highly conservative baseline of 20 songs per concert was applied. Because Billy Joel's standard setlists during this high-energy era frequently exceeded this number, this methodology ensures the final track count remains a verified, low-end historical estimate of the exact musical output hammered out on this specific piano.` } },
    ];
    return (
      <React.Fragment>
        <div data-reveal className="grid grid-cols-2 gap-px overflow-hidden border border-white/10 bg-white/10 md:grid-cols-4">
          {cards.map((c) => (
            <div key={c.label} className="relative bg-coal-800 p-5 md:p-6">
              {c.info && <InfoButton onClick={() => setInfo(c.info)} label={`How ${c.label} is calculated`} />}
              <div className="flex items-baseline gap-1.5">
                <span className="chrome font-display text-[2.4rem] leading-none tracking-tight md:text-[3rem]">{c.value}</span>
                {c.unit && <span className="font-mono text-[11px] uppercase tracking-[0.2em] text-ink-faint">{c.unit}</span>}
              </div>
              <div className="mt-3 font-mono text-[10.5px] uppercase tracking-[0.24em] text-ink">{c.label}</div>
              <div className="mt-1 truncate font-mono text-[10px] uppercase tracking-[0.14em] text-ink-faint">{c.sub}</div>
            </div>
          ))}
        </div>
        <InfoDrawer open={!!info} eyebrow="How it's calculated" title={info ? info.title : ''} onClose={() => setInfo(null)}>
          {info && <InfoBody>{info.body}</InfoBody>}
        </InfoDrawer>
      </React.Fragment>
    );
  }

  /* ============================ VIEW TOGGLE ============================ */
  function ViewToggle({ view, onChange }) {
    const opts = [{ id: 'map', label: 'Map View' }, { id: 'timeline', label: 'Timeline View' }];
    const idx = Math.max(0, opts.findIndex((o) => o.id === view));
    return (
      <div className="relative grid w-full max-w-[420px] grid-cols-2 rounded-full border border-white/12 bg-coal-800 p-1"
           role="tablist" aria-label="Journey view">
        <span className="absolute inset-y-1 left-1 rounded-full"
              style={{ width: 'calc(50% - 0.25rem)', transform: `translateX(${idx * 100}%)`,
                background: 'linear-gradient(180deg,var(--accent),var(--accent-deep))', boxShadow: '0 0 18px var(--accent-glow)',
                transition: 'transform 300ms cubic-bezier(.16,.84,.36,1)' }}></span>
        {opts.map((o) => (
          <button key={o.id} role="tab" aria-selected={view === o.id} onClick={() => onChange(o.id)}
                  className="relative z-10 px-4 py-2.5 text-center font-mono text-[10.5px] uppercase tracking-[0.2em] transition-colors duration-300 sm:text-[11px]"
                  style={{ color: view === o.id ? '#fff' : 'var(--ink-faint)' }}>
            {o.label}
          </button>
        ))}
      </div>
    );
  }

  /* ============================ TOUR MAP (Leaflet) ============================ */
  function pinIcon(s, activeState) {
    const cls = ['journey-pin', isMilestone(s) ? 'journey-pin--milestone' : '', activeState ? 'journey-pin--active' : '']
      .filter(Boolean).join(' ');
    const size = isMilestone(s) ? 16 : 12;
    return window.L.divIcon({
      className: 'journey-pin-wrap',
      html: `<span class="${cls}"></span>`,
      iconSize: [size, size],
      iconAnchor: [size / 2, size / 2],
    });
  }

  function TourMap({ data, selectedId, onSelect, active }) {
    const containerRef = useRef(null);
    const mapRef = useRef(null);
    const markersRef = useRef({});
    const routeRef = useRef(null);
    const headRef = useRef(null);
    const animRef = useRef(null);
    const fadeRef = useRef(null);
    const labelARef = useRef(null);
    const labelBRef = useRef(null);
    const labelTurnRef = useRef(0);
    const [playing, setPlaying] = useState(false);

    const prevSelRef = useRef(null);
    const overview = (map, animate) => {
      if (!data.length) { map.setView([44, -98], 3); return; }
      const b = window.L.latLngBounds(data.map((s) => s.coordinates));
      if (animate) map.flyToBounds(b, { padding: [40, 40], maxZoom: 5, duration: 1.2 });
      else map.fitBounds(b, { padding: [40, 40], maxZoom: 5 });
    };

    const hideLabels = () => [labelARef.current, labelBRef.current].forEach((e) => { if (e) e.style.opacity = '0'; });
    // stop the tour and clear all its overlays (route, traveling dot, city label)
    const stopTour = () => {
      if (animRef.current) { cancelAnimationFrame(animRef.current); animRef.current = null; }
      if (fadeRef.current) { clearTimeout(fadeRef.current); cancelAnimationFrame(fadeRef.current); fadeRef.current = null; }
      if (routeRef.current) { routeRef.current.setLatLngs([]); routeRef.current.setStyle({ opacity: 0.95 }); }
      if (headRef.current) headRef.current.setStyle({ opacity: 0, fillOpacity: 0 });
      hideLabels();
      setPlaying(false);
    };

    // init once
    useEffect(() => {
      if (!window.L || !containerRef.current || mapRef.current) return;
      const L = window.L;
      const map = L.map(containerRef.current, { zoomControl: true, scrollWheelZoom: false });
      L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
        attribution: '© OpenStreetMap contributors', maxZoom: 18,
      }).addTo(map);

      legPolylines(data).forEach((pts) => {
        if (pts.length > 1) L.polyline(pts, { color: getAccent(), weight: 1.5, opacity: 0.5, dashArray: '4 6' }).addTo(map);
      });

      data.forEach((s) => {
        markersRef.current[s.id] = L.marker(s.coordinates, { icon: pinIcon(s, false) })
          .addTo(map)
          .bindTooltip(`${s.city}, ${s.state}`, { direction: 'top', offset: [0, -8], className: 'journey-tip' })
          .on('click', () => onSelect(s.id));
      });

      // animated route layers, drawn on top of the faint static legs
      routeRef.current = L.polyline([], { color: getAccent(), weight: 3, opacity: 0.95, lineCap: 'round', lineJoin: 'round' }).addTo(map);
      headRef.current = L.circleMarker(data[0] ? data[0].coordinates : [0, 0],
        { radius: 5, weight: 2, color: '#fff', fillColor: getAccent(), fillOpacity: 0, opacity: 0 }).addTo(map);

      overview(map);
      mapRef.current = map;
      return () => {
        if (animRef.current) cancelAnimationFrame(animRef.current);
        if (fadeRef.current) { clearTimeout(fadeRef.current); cancelAnimationFrame(fadeRef.current); }
        animRef.current = null; fadeRef.current = null; routeRef.current = null; headRef.current = null;
        map.remove(); mapRef.current = null; markersRef.current = {};
      };
    }, [data]);

    // highlight selection + fly to it
    useEffect(() => {
      const map = mapRef.current; if (!map) return;
      if (selectedId) stopTour();   // a stop drawer opened — the playing tour is now irrelevant
      data.forEach((s) => {
        const m = markersRef.current[s.id];
        if (m) m.setIcon(pinIcon(s, s.id === selectedId));
      });
      if (selectedId) {
        const s = data.find((d) => d.id === selectedId);
        if (s) map.flyTo(s.coordinates, Math.max(map.getZoom(), 6), { duration: 1.5 });
      } else if (prevSelRef.current) {
        // drawer just closed — ease back to the full-route overview
        overview(map, true);
      }
      prevSelRef.current = selectedId;
    }, [selectedId, data]);

    // Leaflet mis-measures while display:none — re-measure when the map becomes visible
    useEffect(() => {
      const map = mapRef.current; if (!map || !active) return;
      const raf = requestAnimationFrame(() => {
        map.invalidateSize();
        const s = selectedId ? data.find((d) => d.id === selectedId) : null;
        if (s) map.setView(s.coordinates, Math.max(map.getZoom(), 6), { animate: false });
        else overview(map);
      });
      return () => cancelAnimationFrame(raf);
    }, [active]);

    // progressively draw the tour route in performance order
    const playTour = () => {
      const map = mapRef.current, route = routeRef.current, head = headRef.current;
      if (!map || !route || data.length < 2) return;
      if (animRef.current) { cancelAnimationFrame(animRef.current); animRef.current = null; }
      if (fadeRef.current) { clearTimeout(fadeRef.current); cancelAnimationFrame(fadeRef.current); fadeRef.current = null; }
      route.setStyle({ opacity: 0.95 });
      const pts = data.map((s) => s.coordinates);
      route.setLatLngs([]);
      setPlaying(true);

      const reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
      if (reduce) {
        route.setLatLngs(pts);
        if (head) head.setStyle({ opacity: 0, fillOpacity: 0 });
        setPlaying(false);
        return;
      }

      if (head) { head.setLatLng(pts[0]); head.setStyle({ opacity: 1, fillOpacity: 1 }); }
      const total = 30000; // full route draw duration (ms)
      const start = performance.now();
      let reached = 0;
      const flash = (idx) => {
        const s = data[idx]; const m = s && markersRef.current[s.id];
        if (!m) return;
        m.setIcon(pinIcon(s, true));
        setTimeout(() => { const mm = markersRef.current[s.id]; if (mm) mm.setIcon(pinIcon(s, s.id === selectedId)); }, 650);
      };
      // cinematic lower-third: cross-fade the current city as the route reaches it
      const labelFor = (idx) => {
        const s = data[idx]; if (!s) return;
        labelTurnRef.current = labelTurnRef.current ? 0 : 1;
        const inc = labelTurnRef.current ? labelBRef.current : labelARef.current;
        const out = labelTurnRef.current ? labelARef.current : labelBRef.current;
        if (inc) {
          inc.querySelector('.tc').textContent = s.city;
          inc.querySelector('.ts').textContent = `${s.state} · ${fmtDate(s.appearances[0].date)}`;
          inc.style.transition = 'none'; inc.style.opacity = '0'; inc.style.transform = 'translateY(12px)';
          void inc.offsetWidth;
          inc.style.transition = ''; inc.style.opacity = '1'; inc.style.transform = 'translateY(0)';
        }
        if (out) out.style.opacity = '0';
      };
      hideLabels(); labelTurnRef.current = 0; labelFor(0);
      const tick = (now) => {
        const t = total > 0 ? Math.min(1, (now - start) / total) : 1;
        const f = t * (pts.length - 1);
        const i = Math.max(0, Math.min(pts.length - 2, Math.floor(f) || 0));
        const frac = f - i;
        const a = pts[i], b = pts[i + 1];
        if (!a || !b) { route.setLatLngs(pts); if (head) head.setStyle({ opacity: 0, fillOpacity: 0 }); animRef.current = null; setPlaying(false); return; }
        const hll = [a[0] + (b[0] - a[0]) * frac, a[1] + (b[1] - a[1]) * frac];
        route.setLatLngs([...pts.slice(0, i + 1), hll]);
        if (head) head.setLatLng(hll);
        const reachedIdx = frac >= 0.999 ? i + 1 : i;
        const before = reached;
        while (reached < reachedIdx) { reached++; flash(reached); }
        if (reached > before) labelFor(reached);
        if (t < 1) { animRef.current = requestAnimationFrame(tick); }
        else {
          route.setLatLngs(pts);
          if (head) head.setStyle({ opacity: 0, fillOpacity: 0 });
          animRef.current = null;
          setPlaying(false);
          // hold the completed route, then fade it (and the city label) back to rest
          fadeRef.current = setTimeout(() => {
            hideLabels();
            const t0 = performance.now(), dur = 900;
            const fade = (now) => {
              const k = Math.min(1, (now - t0) / dur);
              route.setStyle({ opacity: 0.95 * (1 - k) });
              if (k < 1) { fadeRef.current = requestAnimationFrame(fade); }
              else { route.setLatLngs([]); route.setStyle({ opacity: 0.95 }); fadeRef.current = null; }
            };
            fadeRef.current = requestAnimationFrame(fade);
          }, 2000);
        }
      };
      animRef.current = requestAnimationFrame(tick);
    };

    return (
      <div data-reveal>
        <style>{`
          .journey-pin-wrap { background: transparent; border: 0; }
          .journey-pin { display:block; width:12px; height:12px; border-radius:50%;
            background:#e7e7ea; box-shadow:0 0 0 2px rgba(0,0,0,0.6); }
          .journey-pin--milestone { width:16px; height:16px; border-radius:3px; transform:rotate(45deg);
            background:var(--accent); box-shadow:0 0 0 3px rgba(58,95,174,0.25), 0 0 14px var(--accent-glow); }
          .journey-pin--active { outline:2px solid #fff; outline-offset:3px; }
          .journey-map .leaflet-container { background:#0b0b0e; font-family:"Space Mono",ui-monospace,monospace; }
          .journey-map .leaflet-tile { filter:grayscale(1) invert(1) brightness(.92) contrast(.9) hue-rotate(180deg); }
          .journey-map .leaflet-control-attribution { background:rgba(0,0,0,.55); color:var(--ink-faint); }
          .journey-map .leaflet-control-attribution a { color:var(--ink-dim); }
          .journey-map .leaflet-bar a { background:#15151a; color:var(--ink-dim); border-bottom-color:rgba(255,255,255,.1); }
          .journey-map .leaflet-bar a:hover { background:#1d1d22; color:#fff; }
          .leaflet-tooltip.journey-tip { background:#000; color:#f4f4f5; border:1px solid rgba(255,255,255,.18);
            border-radius:0; font-family:"Space Mono",ui-monospace,monospace; font-size:10px; text-transform:uppercase;
            letter-spacing:.14em; box-shadow:0 4px 18px rgba(0,0,0,.5); }
          .leaflet-tooltip.journey-tip::before { display:none; }
          .journey-tour-label { position:absolute; left:0; bottom:0; opacity:0; transform:translateY(12px);
            transition:opacity .35s ease, transform .5s cubic-bezier(.16,.84,.36,1); will-change:opacity,transform; }
          .journey-tour-label .tc { font-family:"Oswald",system-ui,sans-serif; font-weight:600; text-transform:uppercase;
            font-size:clamp(1.5rem,3.4vw,2.6rem); line-height:.92; letter-spacing:-.01em; color:#f4f4f5;
            text-shadow:0 2px 16px rgba(0,0,0,.85),0 0 2px rgba(0,0,0,.6); white-space:nowrap; }
          .journey-tour-label .ts { margin-top:.4rem; font-family:"Space Mono",ui-monospace,monospace; font-size:11px;
            text-transform:uppercase; letter-spacing:.2em; color:#f4f4f5; text-shadow:0 1px 10px rgba(0,0,0,.9); white-space:nowrap; }
        `}</style>
        <div className="relative">
          <div ref={containerRef} className="journey-map w-full border border-white/10"
               style={{ height: 'clamp(380px, 60vh, 640px)' }}></div>
          <button type="button" onClick={playTour} aria-label="Play the tour route animation"
                  className="absolute right-3 top-3 z-[1000] border border-[color:var(--accent)] bg-[rgba(58,95,174,0.16)] px-3.5 py-2 font-mono text-[10px] uppercase tracking-[0.2em] text-ink backdrop-blur-sm transition-colors hover:bg-[color:var(--accent)] hover:text-white">
            {playing ? '● Playing…' : '▶ Play the tour'}
          </button>
          {/* tour city lower-third (cross-fades during playback) */}
          <div className="pointer-events-none absolute bottom-5 left-5 z-[1000] select-none" aria-hidden="true">
            <div ref={labelARef} className="journey-tour-label"><div className="tc"></div><div className="ts"></div></div>
            <div ref={labelBRef} className="journey-tour-label"><div className="tc"></div><div className="ts"></div></div>
          </div>
        </div>
        <p className="mt-3 font-mono text-[10px] uppercase tracking-[0.22em] text-ink-faint">
          {data.length} stops · click a marker for details · route drawn in performance order
        </p>
      </div>
    );
  }

  /* ============================ TIMELINE ============================ */
  function JourneyTimeline({ data, selectedId, onSelect }) {
    return (
      <div className="relative mx-auto max-w-[860px]">
        <div className="absolute bottom-3 top-3 w-px bg-white/10" style={{ left: '19px' }}></div>
        <ol>
          {data.map((s) => {
            const ms = isMilestone(s);
            const sel = s.id === selectedId;
            return (
              <li key={s.id} className="relative">
                <button onClick={() => onSelect(s.id)}
                        className="group flex w-full items-start gap-4 border-l-2 py-3.5 pl-11 pr-3 text-left transition-colors hover:bg-white/[0.025]"
                        style={{ borderLeftColor: sel ? 'var(--accent)' : 'transparent', background: sel ? 'rgba(58,95,174,0.08)' : 'transparent' }}>
                  <span className="absolute top-6 z-10" style={{ left: '19px',
                    width: ms ? 15 : 9, height: ms ? 15 : 9,
                    background: ms ? 'var(--accent)' : '#0a0a0b', border: ms ? '0' : '1px solid rgba(255,255,255,0.4)',
                    boxShadow: ms ? '0 0 0 4px rgba(58,95,174,0.18), 0 0 16px var(--accent-glow)' : 'none',
                    transform: ms ? 'translate(-50%,0) rotate(45deg)' : 'translate(-50%,0)', borderRadius: ms ? '2px' : '50%' }}></span>
                  <div className="min-w-0 flex-1">
                    <div className="flex flex-wrap items-center gap-x-3 gap-y-1">
                      <span className="font-mono text-[11.5px] tracking-[0.12em] text-ink-faint">{fmtDate(firstDate(s))}</span>
                      {ms && <MilestoneBadge label={s.milestoneTitle} />}
                    </div>
                    <h3 className={`mt-1 font-display uppercase tracking-tight text-ink transition-colors group-hover:text-white ${ms ? 'text-[1.7rem] leading-tight' : 'text-[1.35rem]'}`}>
                      {s.city}, {s.state}
                    </h3>
                    <p className="mt-0.5 text-[14px] text-ink-dim">{s.appearances[0].venue}</p>
                  </div>
                  <span className="mt-1 shrink-0 font-mono text-[11px] uppercase tracking-[0.14em] text-ink-ghost transition-colors group-hover:text-[color:var(--accent-text)]">
                    {s.country}
                  </span>
                </button>
              </li>
            );
          })}
        </ol>
      </div>
    );
  }

  /* ============================ DETAIL DRAWER ============================ */
  function DrawerBody({ stop, onClose, onNav, index, total }) {
    const ms = isMilestone(stop);
    const songs = setlistOf(stop);
    return (
      <React.Fragment>
        <div className="flex items-start justify-between gap-4 border-b border-white/10 p-6">
          <div>
            {ms && <div className="mb-2"><MilestoneBadge label={stop.milestoneTitle} /></div>}
            <h3 className="font-display text-[2rem] uppercase leading-none tracking-tight text-ink">{stop.city}</h3>
            <p className="mt-2 font-mono text-[11px] uppercase tracking-[0.18em] text-ink-faint">{stop.state} · {stop.country}</p>
          </div>
          <button onClick={onClose} aria-label="Close"
                  className="grid h-9 w-9 shrink-0 place-items-center border border-white/30 text-ink transition-colors hover:border-white/55">✕</button>
        </div>

        <div className="flex-1 overflow-y-auto p-6">
          <h4 className="font-mono text-[10.5px] uppercase tracking-[0.26em] text-ink-faint">{stop.appearances.length > 1 ? 'Dates played' : 'Date played'}</h4>
          <ul className="mt-3 space-y-3">
            {stop.appearances.map((ap, i) => (
              <li key={i} className="border-l-2 pl-4" style={{ borderColor: ms ? 'var(--accent)' : 'rgba(255,255,255,0.18)' }}>
                <div className="font-mono text-[12px] tracking-[0.1em] text-ink">{fmtDate(ap.date)}</div>
                <div className="mt-0.5 text-[14px] text-ink-dim">{ap.venue}</div>
              </li>
            ))}
          </ul>

          <div className="mt-8">
            <div className="flex items-center justify-between border-b border-white/12 pb-2">
              <h4 className="font-mono text-[10.5px] uppercase tracking-[0.26em] text-ink-faint">Setlist</h4>
              {songs.length > 0 && <span className="font-mono text-[10px] text-ink-ghost">{songs.length} songs</span>}
            </div>
            {songs.length > 0 ? (
              <React.Fragment>
                <ol className="mt-3 grid grid-cols-1 gap-x-6 gap-y-1.5 sm:grid-cols-2">
                  {songs.map((song, i) => (
                    <li key={i} className="flex items-baseline gap-2.5 text-[13.5px] text-ink">
                      <span className="font-mono text-[10.5px] text-ink-ghost">{String(i + 1).padStart(2, '0')}</span>
                      <span>{song}</span>
                    </li>
                  ))}
                </ol>
                <p className="mt-4 font-mono text-[9.5px] uppercase tracking-[0.2em] text-ink-faint">Performance running order · 1982</p>
              </React.Fragment>
            ) : (
              <div className="mt-4 border border-dashed border-white/15 bg-white/[0.02] px-4 py-6 text-center">
                <div className="font-mono text-[11px] uppercase tracking-[0.2em]" style={{ color: 'var(--accent-text)' }}>Setlist unknown — data missing</div>
                <p className="mt-2 text-[13px] leading-relaxed text-ink-faint">No setlist has been documented for this date yet.</p>
              </div>
            )}
          </div>

          <div className="mt-8 border-t border-white/10 pt-5">
            <p className="text-[13px] leading-relaxed text-ink-faint">
              Have a memory, photo, or stub from this night? Share it through{' '}
              <a href="#contact" onClick={onClose} className="text-ink underline decoration-white/30 underline-offset-4 hover:decoration-white">Contact</a>.
            </p>
          </div>
        </div>

        <div className="flex items-center justify-between border-t border-white/10 bg-white/[0.02] px-4 py-5">
          <button type="button" onClick={() => onNav(-1)} disabled={index <= 0}
                  className="font-mono text-[11px] uppercase tracking-[0.18em] text-ink-dim transition-colors hover:text-ink disabled:cursor-not-allowed disabled:opacity-30">‹ Prev</button>
          <span className="font-mono text-[11px] tracking-[0.22em] text-ink-faint">{index + 1} / {total}</span>
          <button type="button" onClick={() => onNav(1)} disabled={index >= total - 1}
                  className="font-mono text-[11px] uppercase tracking-[0.18em] text-ink-dim transition-colors hover:text-ink disabled:cursor-not-allowed disabled:opacity-30">Next ›</button>
        </div>
      </React.Fragment>
    );
  }

  function DetailDrawer({ stop, onClose, onNav, index, total }) {
    useEffect(() => {
      const onKey = (e) => {
        if (e.key === 'Escape') onClose();
        else if (e.key === 'ArrowLeft') onNav(-1);
        else if (e.key === 'ArrowRight') onNav(1);
      };
      window.addEventListener('keydown', onKey);
      return () => window.removeEventListener('keydown', onKey);
    }, [onClose, onNav]);

    const open = !!stop;
    return (
      <div className="fixed inset-0 z-[80] overflow-hidden" style={{ pointerEvents: open ? 'auto' : 'none' }} aria-hidden={!open}>
        <div onClick={onClose} className="absolute inset-0 transition-opacity duration-300"
             style={{ background: 'rgba(4,4,5,0.35)', backdropFilter: 'none', opacity: open ? 1 : 0 }}></div>
        <aside className="absolute right-0 top-0 flex h-full w-full max-w-[440px] flex-col border-l border-white/12 bg-coal-800"
               style={{ transform: open ? 'translateX(0)' : 'translateX(100%)', transition: 'transform 500ms cubic-bezier(.16,.84,.36,1)', boxShadow: '-30px 0 80px rgba(0,0,0,0.6)' }}
               role="dialog" aria-modal="true">
          {stop && <DrawerBody stop={stop} onClose={onClose} onNav={onNav} index={index} total={total} />}
        </aside>
      </div>
    );
  }

  /* ============================ MAIN ============================ */
  function Journey() {
    const [view, setView] = useState('map');
    const [selectedId, setSelectedId] = useState(null);

    const data = useMemo(
      () => (window.JOURNEY || []).slice().sort((a, b) => firstDate(a).localeCompare(firstDate(b))),
      []
    );
    const stats = useMemo(() => ({
      totalStops: data.length,
      countries: [...new Set(data.map((s) => s.country))],
      // count documented songs; assume 20 for any stop with no setlist on record
      songs: data.reduce((n, s) => n + (setlistOf(s).length || 20), 0),
      documentedStops: data.filter((s) => setlistOf(s).length > 0).length,
      miles: Math.round(routeMiles(data) / 500) * 500, // estimate, nearest 500
    }), [data]);

    const hasData = data.length > 0;
    const first = data[0];
    const last = data[data.length - 1];
    const selected = selectedId ? data.find((s) => s.id === selectedId) : null;
    const selIdx = selectedId ? data.findIndex((s) => s.id === selectedId) : -1;
    const navStop = (dir) => {
      const ni = selIdx + dir;
      if (selIdx >= 0 && ni >= 0 && ni < data.length) setSelectedId(data[ni].id);
    };

    return (
      <section id="journey" className="relative mx-auto w-full max-w-[1320px] px-6 py-28 md:px-10 md:py-40">
        <SectionHead index="02 / The Journey" kicker="The Nylon Curtain · 1982"
          title={<>Every stadium stop. <span className="accent-word">Every stage mile.</span></>}
          sub={hasData
            ? `${stats.totalStops} documented dates across ${stats.countries.length} countries — opening in ${first.city} on ${fmtDate(firstDate(first))} and closing at ${last.appearances[0].venue}. Explore the route on the map, or scan the full chronological timeline.`
            : 'The 1982 Nylon Curtain itinerary.'} />

        <StatCards stats={stats} />

        <div className="mt-10 flex justify-center md:mt-12" data-reveal>
          <ViewToggle view={view} onChange={setView} />
        </div>

        <div className="mt-9 md:mt-12">
          {!hasData && (
            <div className="grid place-items-center border border-white/10 bg-coal-800 py-24 text-center">
              <p className="font-mono text-[11px] uppercase tracking-[0.24em] text-ink-faint">No tour data available</p>
            </div>
          )}
          {hasData && (
            <React.Fragment>
              <div className={view === 'map' ? '' : 'hidden'}>
                <TourMap data={data} selectedId={selectedId} onSelect={setSelectedId} active={view === 'map'} />
              </div>
              <div className={view === 'timeline' ? '' : 'hidden'}>
                <JourneyTimeline data={data} selectedId={selectedId} onSelect={setSelectedId} />
              </div>
            </React.Fragment>
          )}
        </div>

        <DetailDrawer stop={selected} onClose={() => setSelectedId(null)} onNav={navStop} index={selIdx} total={data.length} />
      </section>
    );
  }

  Object.assign(window, { Journey });
})();
