// itinerary-map.jsx — itinerary and map screens

const { useState: useState_IM, useEffect: useEffect_IM, useRef: useRef_IM, useMemo: useMemo_IM, useLayoutEffect: useLayoutEffect_IM } = React;

// ============================================================
// NEARBY IDEAS — suggestions around a day's route, + to add
// ============================================================
function DaySuggestions({ day, places, onAdd }) {
  const [sugs, setSugs] = useState_IM(null); // null = loading
  const dayKey = day ? `${day.day}:${day.stops.map(s => s.ref).join(',')}` : '';

  useEffect_IM(() => {
    let alive = true;
    setSugs(null);
    const geo = day.stops.filter(s => s.lat != null && s.lng != null);
    if (!geo.length || !window.nearbySuggestions) { setSugs([]); return; }
    const center = {
      lat: geo.reduce((s, x) => s + x.lat, 0) / geo.length,
      lng: geo.reduce((s, x) => s + x.lng, 0) / geo.length,
    };
    // breakfast-hint is a placeholder, not a real café — the day still WANTS coffee ideas
    const kinds = day.stops.filter(s => s.kind !== 'stay' && !s.hint).map(s => s.kind);
    window.nearbySuggestions(center, {
      exclude: [...places.map(p => p.id), ...places.map(p => p.name)],
      wantFood: !kinds.includes('meal'),
      wantCoffee: !kinds.includes('coffee'),
    }).then(r => { if (alive) setSugs(r); });
    return () => { alive = false; };
  }, [dayKey]);

  if (sugs && sugs.length === 0) return null;

  return (
    <div className="side-card" style={{ background: 'var(--butter)' }}>
      <h3 style={{ marginBottom: 8 }}>💡 nearby ideas</h3>
      <p style={{ margin: '0 0 10px', fontSize: 13, color: 'var(--ink-soft)' }}>
        {sugs === null ? 'scouting the area…' : 'close to today’s route. + to add.'}
      </p>
      {sugs && (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
          {sugs.map((s) => (
            <div key={s.id} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 8, padding: '6px 0', borderTop: '1px dashed rgba(23,18,17,0.25)' }}>
              <div style={{ minWidth: 0 }}>
                <div style={{ fontFamily: 'var(--font-display)', fontWeight: 600, fontSize: 14, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{s.name}</div>
                <div className="mono" style={{ fontSize: 11, color: 'var(--ink-soft)' }}>
                  {s.kind}{s.rating ? ` · ★ ${s.rating}` : ''}
                </div>
              </div>
              <button className="icon-btn" title={`add ${s.name} to this day`} style={{ width: 26, height: 26, fontSize: 14, flexShrink: 0 }} onClick={() => onAdd(s)}>+</button>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

// ============================================================
// DAY TIMELINE — the whole day as one horizontal strip
// ============================================================
const _TL_COLOR = { meal: '#F5D871', coffee: '#F7A59E', bar: '#FF6FA0', museum: '#B1DDC6', view: '#8AD0C0', fixed: '#1A1310' };
const _dd = (n) => String(n).padStart(2, '0'); // "day 01" … "day 12" (not "day 012")

// Copy with a fallback for browsers/contexts without the async clipboard (plain http, old Safari)
async function copyText(text) {
  try {
    if (navigator.clipboard && navigator.clipboard.writeText) { await navigator.clipboard.writeText(text); return true; }
  } catch (e) { /* fall through */ }
  try {
    const ta = document.createElement('textarea');
    ta.value = text; ta.setAttribute('readonly', ''); ta.style.cssText = 'position:fixed;top:0;left:0;opacity:0';
    document.body.appendChild(ta); ta.select();
    const ok = document.execCommand('copy');
    ta.remove();
    if (ok) return true;
  } catch (e) { /* fall through */ }
  window.prompt('copy this:', text);
  return false;
}

function DayTimeline({ day }) {
  const durOf = (s) => {
    const d = String(s.duration || '');
    const h = d.match(/([\d.]+)\s*h/); if (h) return Math.round(parseFloat(h[1]) * 60);
    const m = d.match(/(\d+)\s*m/); if (m) return parseInt(m[1], 10);
    return 60;
  };
  const stops = (day.stops || []).filter(s => s._min != null);
  const acts = stops.filter(s => s.kind !== 'stay');
  if (acts.length < 2) return null;

  const startMin = Math.floor(Math.min(...stops.map(s => s._min)) / 60) * 60;
  const endMin = Math.ceil(Math.max(...stops.map(s => s._min + (s.kind === 'stay' ? 0 : durOf(s)))) / 60) * 60;
  const span = Math.max(endMin - startMin, 60);
  const pct = (m) => ((m - startMin) / span) * 100;

  // Same numbering as the stop list: every real (non-stay, non-hint) stop counts
  let n = 0;
  const blocks = stops.map(s => ({ ...s, _pin: s.kind !== 'stay' && !s.hint ? ++n : null }));

  const hoursSpan = span / 60;
  const labelEvery = hoursSpan > 10 ? 120 : 60; // hour numbers, every 2h on marathon days
  const ticks = [];
  for (let t = startMin; t <= endMin; t += 60) ticks.push(t);

  // Wide enough that a 1h block fits its name-ish; edge padding so nothing clips
  const trackMinWidth = Math.max(440, Math.round(hoursSpan * 52)); // a 14h day fits a normal laptop column without scrolling
  const slide = 'left 0.4s cubic-bezier(.2,.8,.25,1), width 0.4s cubic-bezier(.2,.8,.25,1)';

  // geometry: one lane of blocks, one axis line underneath, labels below the line
  const LANE_TOP = 4, LANE_H = 36, AXIS_Y = 50;

  return (
    <div style={{ margin: '18px 0 10px' }}>
      <div className="mono" style={{ fontSize: 11, color: 'var(--ink-soft)', letterSpacing: '0.06em', marginBottom: 8 }}>THE DAY, AT A GLANCE</div>
      <div className="tl-wrap" ref={el => { if (el) { const sc = el.firstElementChild; el.classList.toggle('can-scroll', !!sc && sc.scrollWidth > sc.clientWidth + 2); } }}>
      <div className="tl-scroll" style={{ overflowX: 'auto', paddingBottom: 2 }}>
        <div style={{ minWidth: trackMinWidth + 56, padding: '0 28px', boxSizing: 'border-box' }}>
          <div style={{ position: 'relative', height: 74 }}>
            {/* axis: one line, small hour ticks, hour numbers below */}
            <div style={{ position: 'absolute', left: 0, right: 0, top: AXIS_Y, height: 1.5, background: 'var(--ink-line)', opacity: 0.5, borderRadius: 1 }} />
            {ticks.map(t => (
              <React.Fragment key={t}>
                <div style={{ position: 'absolute', left: `${pct(t)}%`, top: AXIS_Y - 3, height: 7.5, width: 1.5, background: 'var(--ink-line)', opacity: 0.5 }} />
                {(t - startMin) % labelEvery === 0 && (
                  <div className="mono" style={{ position: 'absolute', left: `${pct(t)}%`, top: AXIS_Y + 10, transform: 'translateX(-50%)', fontSize: 10, color: 'var(--ink-soft)' }}>{Math.floor(t / 60) % 24}</div>
                )}
              </React.Fragment>
            ))}
            {/* travel gaps: dashed connectors between blocks */}
            {acts.slice(0, -1).map((s, i) => {
              const next = acts[i + 1];
              const from = s._min + durOf(s);
              if (next._min <= from) return null;
              return <div key={`c${i}`} style={{ position: 'absolute', left: `${pct(from)}%`, width: `${pct(next._min) - pct(from)}%`, top: LANE_TOP + LANE_H / 2 - 1, borderTop: '2px dashed var(--ink-line)', opacity: 0.4, transition: slide }} />;
            })}
            {/* leaving / returning to the hotel: little dots ON the axis */}
            {blocks.filter(s => s.kind === 'stay').map((s, i) => (
              <div key={`h${i}`} title={`${s.time} · ${s.label}`} style={{ position: 'absolute', left: `${pct(s._min)}%`, top: AXIS_Y - 7, transform: 'translateX(-50%)', width: 16, height: 16, borderRadius: '50%', background: 'var(--bg)', border: '1.5px solid var(--ink-line)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 9, lineHeight: 1, zIndex: 4, transition: slide }}>⌂</div>
            ))}
            {/* the stops themselves */}
            {blocks.filter(s => s.kind !== 'stay').map((s, i) => {
              const w = Math.max((durOf(s) / span) * 100, 3);
              const wide = (w / 100) * trackMinWidth > 84; // number-only when the name would be "P…" 
              return (
                <div key={i} className="tl-block" title={`${s.time} · ${s.label}`}
                  style={{
                    position: 'absolute', left: `${pct(s._min)}%`, width: `${w}%`, top: LANE_TOP, height: LANE_H,
                    background: s.hint ? 'var(--bg)' : (_TL_COLOR[s.kind] || '#8AD0C0'),
                    color: s.kind === 'fixed' ? 'var(--bg)' : 'var(--ink)',
                    border: s.hint ? '1.5px dashed var(--ink-line)' : '1.5px solid var(--ink-line)',
                    borderRadius: 10, zIndex: 3,
                    display: 'flex', alignItems: 'center', gap: 6, padding: '0 7px',
                    overflow: 'hidden', whiteSpace: 'nowrap', boxSizing: 'border-box',
                    transition: `${slide}, transform 0.15s ease, box-shadow 0.15s ease`,
                  }}>
                  <span style={{
                    width: 17, height: 17, borderRadius: '50%', flexShrink: 0,
                    background: s.fixed ? 'var(--bg)' : 'rgba(250,244,236,0.85)',
                    border: '1.2px solid var(--ink-line)', color: 'var(--ink)',
                    display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
                    fontFamily: 'var(--font-display)', fontWeight: 700, fontSize: 10, lineHeight: 1,
                  }}>{s.fixed ? '🔒' : (s.hint ? '☕' : s._pin)}</span>
                  {wide && (
                    <span style={{ minWidth: 0, overflow: 'hidden' }}>
                      <span style={{ display: 'block', fontFamily: 'var(--font-display)', fontWeight: 600, fontSize: 11, lineHeight: 1.15, overflow: 'hidden', textOverflow: 'ellipsis' }}>{s.label}</span>
                      <span className="mono" style={{ display: 'block', fontSize: 8.5, opacity: 0.65, lineHeight: 1 }}>{s.time}</span>
                    </span>
                  )}
                </div>
              );
            })}
          </div>
        </div>
      </div>
      </div>
    </div>
  );
}

// ============================================================
// ITINERARY
// ============================================================
function ItineraryScreen({ onMap, trip, places, itinerary, onReroute, onBack, onAddPlace, onRemovePlace }) {
  const [showAllNotToday, setShowAllNotToday] = useState_IM(false);
  // add a place straight into this day (name or maps link)
  const [addingPlace, setAddingPlace] = useState_IM(false);
  const [addText, setAddText] = useState_IM('');
  const [addSugs, setAddSugs] = useState_IM([]);
  const [addOpen, setAddOpen] = useState_IM(false);
  const [addBusy, setAddBusy] = useState_IM(false);
  const [addErr, setAddErr] = useState_IM('');
  const addDebounceRef = useRef_IM(null);
  const [dayIdx, setDayIdx] = useState_IM(0);
  const [addingAnchor, setAddingAnchor] = useState_IM(false);
  const [newEvent, setNewEvent] = useState_IM({ name: '', time: '19:00' });
  const [copied, setCopied] = useState_IM(false);
  const [exported, setExported] = useState_IM(false);
  // anchor-name autocomplete: your own saves + google maps
  const [anchorSugs, setAnchorSugs] = useState_IM({ mine: [], maps: [] });
  const [anchorOpen, setAnchorOpen] = useState_IM(false);
  const anchorDebounceRef = useRef_IM(null);
  // drag-and-drop reorder
  const [dragRef, setDragRef] = useState_IM(null);
  const [overRef, setOverRef] = useState_IM(null);

  // FLIP animation: capture row positions right before a reorder, then slide every row
  // from its old spot to its new one on the next render
  const stopsListRef = useRef_IM(null);
  const flipRectsRef = useRef_IM(null);
  const captureRects = () => {
    const el = stopsListRef.current;
    if (!el) return;
    const m = {};
    el.querySelectorAll('[data-flip]').forEach(n => { m[n.getAttribute('data-flip')] = n.getBoundingClientRect().top; });
    flipRectsRef.current = m;
  };
  useLayoutEffect_IM(() => {
    const prev = flipRectsRef.current;
    flipRectsRef.current = null;
    const el = stopsListRef.current;
    if (!prev || !el) return;
    el.querySelectorAll('[data-flip]').forEach(n => {
      const oldTop = prev[n.getAttribute('data-flip')];
      if (oldTop == null) return;
      const dy = oldTop - n.getBoundingClientRect().top;
      if (Math.abs(dy) < 2) return;
      n.style.transition = 'none';
      n.style.transform = `translateY(${dy}px)`;
      void n.offsetHeight; // flush so the jump isn't animated
      n.style.transition = 'transform 0.4s cubic-bezier(.2, .8, .25, 1)';
      n.style.transform = 'translateY(0px)';
    });
  }, [itinerary]);

  const handleShare = () => {
    try {
      const encoded = window.encodeTrip(trip, places, itinerary);
      const url = `${window.location.origin}${window.location.pathname}#t=${encoded}`;
      copyText(url).then(ok => { if (ok) { setCopied(true); setTimeout(() => setCopied(false), 2500); } });
    } catch (e) { console.warn('Share failed:', e); }
  };

  if (!itinerary || itinerary.length === 0) {
    const resolved = (places || []).filter(p => p.lat && p.lng);
    const unresolved = (places || []).filter(p => !p.lat || !p.lng);
    return (
      <div className="itin-screen" data-screen-label="04 Itinerary">
        <div className="page" style={{ maxWidth: 680, paddingTop: 60, textAlign: 'center' }}>
          <h2>couldn't build a route</h2>
          <p className="q-hint" style={{ marginTop: 12 }}>
            none of your saves could be placed on the map{trip && trip.where ? ` near ${trip.where.split(',')[0]}` : ''}.
          </p>
          {unresolved.length > 0 && (
            <div style={{ margin: '16px auto 0', maxWidth: 520, textAlign: 'left', padding: '14px 16px', border: '1.5px dashed var(--ink-line)', borderRadius: 12, background: 'var(--bg-2)' }}>
              <div className="mono" style={{ fontSize: 11, marginBottom: 8 }}>couldn't find</div>
              {unresolved.map(p => (
                <div key={p.id} style={{ fontFamily: 'var(--font-display)', fontWeight: 600, fontSize: 15, padding: '3px 0', overflowWrap: 'anywhere' }}>
                  {p.name}{p.raw && p.raw !== p.name ? <span className="mono" style={{ fontSize: 10, color: 'var(--ink-soft)', textTransform: 'none', letterSpacing: 0 }}> · {p.raw.slice(0, 60)}</span> : null}
                  {p.failReason && window.FAIL_REASON_TEXT[p.failReason] ? <div style={{ fontFamily: 'var(--font-body)', fontWeight: 400, fontSize: 12.5, color: 'var(--coral)' }}>{window.FAIL_REASON_TEXT[p.failReason]}</div> : null}
                </div>
              ))}
              {unresolved.some(p => p.failReason === 'quota') && (
                <p style={{ margin: '10px 0 0', fontSize: 13, color: 'var(--coral)', lineHeight: 1.45 }}>
                  google only allows this app a set number of place lookups a day, and today's are gone. try again tomorrow — or copy the links from google maps on a computer, those carry their own pin and still work.
                </p>
              )}
              <p style={{ margin: '10px 0 0', fontSize: 13, color: 'var(--ink-soft)', lineHeight: 1.45 }}>
                usual culprits: a link that no longer opens in google maps, a typo, or a place that's not actually in this city.
                go back, open the link on your phone to check it still works, or just type the place name instead.
              </p>
            </div>
          )}
          <div style={{ marginTop: 28, display: 'flex', gap: 12, justifyContent: 'center', flexWrap: 'wrap' }}>
            {onBack && <button className="btn primary" onClick={onBack}>← go back and fix it</button>}
          </div>
        </div>
      </div>
    );
  }

  const clampedIdx = Math.min(dayIdx, itinerary.length - 1);
  const day = itinerary[clampedIdx];

  // Adds a locked event to the day you're currently looking at.
  // If she picked (or typed the name of) one of her saved places, the event pins THAT place:
  // it leaves wherever it was routed. Only the two days involved are rebuilt.
  const addAnchor = () => {
    if (!newEvent.name || !newEvent.time) return;
    let ev = {
      name: newEvent.name.trim(), time: newEvent.time, day: String(clampedIdx + 1),
      ...(newEvent.placeId ? { placeId: newEvent.placeId } : {}),
      ...(newEvent.lat != null && newEvent.lng != null ? { lat: newEvent.lat, lng: newEvent.lng } : {}),
    };
    if (!ev.placeId) {
      const hit = places.find(p => p.lat && p.lng && (p.name || '').trim().toLowerCase() === ev.name.toLowerCase());
      if (hit) ev = { ...ev, placeId: hit.id, lat: hit.lat, lng: hit.lng };
    }
    const newTrip = { ...trip, events: [...(trip.events || []), ev] };
    captureRects();
    let newItin = itinerary;
    if (ev.placeId) {
      const src = itinerary.findIndex(d => d.stops.some(s => s.ref === ev.placeId));
      if (src >= 0 && src !== clampedIdx) newItin = window.rebuildOneDay(newItin, src, places, newTrip);
    }
    newItin = window.rebuildOneDay(newItin, clampedIdx, places, newTrip);
    if (onReroute) onReroute(newTrip, newItin);
    setNewEvent({ name: '', time: '19:00' });
    setAnchorSugs({ mine: [], maps: [] });
    setAnchorOpen(false);
    setAddingAnchor(false);
  };

  // Typing in the anchor name field: suggest from her own saves instantly,
  // and from Google Maps (biased near this day's route) after a short debounce
  const handleAnchorInput = (val) => {
    setNewEvent(prev => ({ ...prev, name: val, placeId: undefined, lat: undefined, lng: undefined }));
    const q = val.trim().toLowerCase();
    const mine = q.length >= 1
      ? places.filter(p => p.lat && p.lng && (p.name || '').toLowerCase().includes(q)).slice(0, 4)
      : [];
    setAnchorSugs(prev => ({ ...prev, mine, ...(q.length < 3 ? { maps: [] } : {}) }));
    setAnchorOpen(mine.length > 0);
    clearTimeout(anchorDebounceRef.current);
    if (q.length < 3) return;
    anchorDebounceRef.current = setTimeout(async () => {
      try {
        const geo = day.stops.filter(s => s.lat != null && s.lng != null);
        const opts = { input: val };
        if (geo.length) {
          opts.locationBias = {
            center: { lat: geo.reduce((a, s) => a + s.lat, 0) / geo.length, lng: geo.reduce((a, s) => a + s.lng, 0) / geo.length },
            radius: 30000,
          };
        }
        const { suggestions } = await google.maps.places.AutocompleteSuggestion.fetchAutocompleteSuggestions(opts);
        const maps = suggestions.map(s => s.placePrediction).filter(Boolean).slice(0, 4);
        setAnchorSugs(prev => ({ ...prev, maps }));
        setAnchorOpen(true);
      } catch (e) { /* maps autocomplete unavailable — free text still works */ }
    }, 300);
  };

  const pickAnchorMine = (p) => {
    setNewEvent(prev => ({ ...prev, name: p.name, placeId: p.id, lat: p.lat, lng: p.lng }));
    setAnchorSugs({ mine: [], maps: [] });
    setAnchorOpen(false);
  };

  const pickAnchorMaps = async (pred) => {
    let name = pred.text ? pred.text.text : '';
    let lat, lng;
    try {
      const place = pred.toPlace();
      await place.fetchFields({ fields: ['displayName', 'location'] });
      name = place.displayName || name;
      if (place.location) { lat = place.location.lat(); lng = place.location.lng(); }
    } catch (e) { /* keep prediction text */ }
    setNewEvent(prev => ({ ...prev, name, lat, lng }));
    setAnchorSugs({ mine: [], maps: [] });
    setAnchorOpen(false);
  };

  // Drag-and-drop: flexible stops can be re-ordered within the day; times re-flow in the new order
  const isDraggableStop = (s) => s.kind !== 'stay' && !s.fixed && !s.hint && !String(s.ref || '').startsWith('anc-');
  // True when the dragged row sits above the target — the drop lands AFTER the target row
  const dragBefore = (src, target) => {
    const order = day.stops.filter(isDraggableStop).map(s => s.ref);
    return order.indexOf(src) < order.indexOf(target);
  };
  const handleStopDrop = (targetRef) => {
    const src = dragRef;
    setDragRef(null); setOverRef(null);
    if (!src || src === targetRef) return;
    const order = day.stops.filter(isDraggableStop).map(s => s.ref);
    const from = order.indexOf(src), to = order.indexOf(targetRef);
    if (from < 0 || to < 0) return;
    order.splice(to, 0, ...order.splice(from, 1));
    captureRects();
    const newItin = window.reorderOneDay(itinerary, clampedIdx, order, places, trip);
    if (onReroute) onReroute(trip, newItin);
  };

  // Unlock straight from the stop row — matched by name + time. Only this day is rebuilt;
  // a pinned saved place goes back into today's flexible pool.
  const removeAnchorByStop = (s) => {
    const idx = (trip.events || []).findIndex(e => e.name === s.label && e.time === s.time);
    if (idx < 0) return;
    const ev = trip.events[idx];
    const newTrip = { ...trip, events: trip.events.filter((_, j) => j !== idx) };
    const freed = ev.placeId ? places.find(p => p.id === ev.placeId) : null;
    captureRects();
    const newItin = window.rebuildOneDay(itinerary, clampedIdx, places, newTrip, freed || undefined);
    if (onReroute) onReroute(newTrip, newItin);
  };

  // ── add a place to THIS day ──────────────────────────────────────────────
  const dayBias = () => {
    const geo = day.stops.filter(s => s.lat != null && s.lng != null);
    if (geo.length) return { center: { lat: geo.reduce((a, s) => a + s.lat, 0) / geo.length, lng: geo.reduce((a, s) => a + s.lng, 0) / geo.length }, radius: 30000 };
    if (trip.stayPlace) return { center: { lat: trip.stayPlace.lat, lng: trip.stayPlace.lng }, radius: 30000 };
    return null;
  };
  const handleAddInput = (val) => {
    setAddText(val); setAddErr('');
    clearTimeout(addDebounceRef.current);
    const q = val.trim();
    if (q.length < 3 || q.startsWith('http')) { setAddSugs([]); setAddOpen(false); return; }
    addDebounceRef.current = setTimeout(async () => {
      try {
        const opts = { input: q };
        const b = dayBias(); if (b) opts.locationBias = b;
        const { suggestions } = await google.maps.places.AutocompleteSuggestion.fetchAutocompleteSuggestions(opts);
        setAddSugs(suggestions.map(s => s.placePrediction).filter(Boolean).slice(0, 5));
        setAddOpen(true);
      } catch (e) { /* no autocomplete — Enter still resolves the text */ }
    }, 300);
  };
  // Puts a resolved place into today's plan (as a normal, flexible stop) — only this day is re-timed
  const placeIntoToday = (pl) => {
    const existing = places.find(p => p.id === pl.id);
    if (existing) { handlePullIn(existing); return; }
    captureRects();
    const newItin = window.rebuildOneDay(itinerary, clampedIdx, places, trip, pl);
    if (onAddPlace) onAddPlace(pl, newItin);
  };
  const resetAdd = () => { setAddingPlace(false); setAddText(''); setAddSugs([]); setAddOpen(false); setAddErr(''); setAddBusy(false); };
  const pickAddSug = async (pred) => {
    setAddOpen(false); setAddBusy(true); setAddErr('');
    try {
      const pl = await window.placeFromPrediction(pred);
      if (!pl) throw new Error('no location');
      placeIntoToday(pl);
      resetAdd();
    } catch (e) { setAddBusy(false); setAddErr("couldn't load that one — try typing the name"); }
  };
  const submitAdd = async () => {
    const q = addText.trim();
    if (!q || addBusy) return;
    setAddOpen(false); setAddBusy(true); setAddErr('');
    try {
      const { place: pl, reason } = await window.resolveOnePlaceDetailed(q, dayBias());
      const b = dayBias();
      if (!pl) throw new Error(reason || 'notfound');
      if (b && window.haversineDist && window.haversineDist(pl.lat, pl.lng, b.center.lat, b.center.lng) > 200) throw new Error('far');
      placeIntoToday(pl);
      resetAdd();
    } catch (e) {
      setAddBusy(false);
      setAddErr(e.message === 'far' ? 'found something, but nowhere near this trip — try a more specific name'
        : e.message === 'dead' ? "that link doesn't open anymore — type the place name instead"
        : e.message === 'notplace' ? "that link opens google maps, but not on one place — type the name instead"
        : e.message === 'notmaps' ? "that's not a google maps link — type the place name instead"
        : e.message === 'quota' ? "google's daily lookup limit is used up — this one has to wait until tomorrow"
        : "couldn't find that — check the spelling, or paste its google maps link");
    }
  };

  // ↑/↓ reorder — for touch screens and keyboards, where drag-and-drop doesn't exist
  const moveStop = (ref, dir) => {
    const order = day.stops.filter(isDraggableStop).map(s => s.ref);
    const i = order.indexOf(ref), j = i + dir;
    if (i < 0 || j < 0 || j >= order.length) return;
    [order[i], order[j]] = [order[j], order[i]];
    captureRects();
    const newItin = window.reorderOneDay(itinerary, clampedIdx, order, places, trip);
    if (onReroute) onReroute(trip, newItin);
  };

  // Drop a place from the trip entirely — just this day is re-timed
  const handleRemoveStop = (s) => {
    captureRects();
    const rest = places.filter(p => p.id !== s.ref);
    const newItin = window.rebuildOneDay(itinerary, clampedIdx, rest, trip);
    if (onRemovePlace) onRemovePlace(s.ref, newItin);
  };

  const dateRange = window.fmtDateRange(trip.start, trip.end);

  // Copies the whole itinerary as plain text — for notes apps and the group chat
  const handleExport = () => {
    const lines = [`r we lost? · ${trip.where || 'your trip'}${dateRange ? ` · ${dateRange}` : ''}`, ''];
    itinerary.forEach(d => {
      lines.push(`day ${_dd(d.day)} · ${d.label} — ${d.title}`);
      d.stops.filter(s => !s.hint).forEach(s => {
        lines.push(`  ${s.time.padStart(5, ' ')}  ${s.label}${s.kind && s.kind !== 'stay' ? ` (${s.kind})` : ''}${s.note ? ` — ${s.note}` : ''}`);
      });
      lines.push(`  ~${d.stats.km} km on foot & transit`, '');
    });
    const text = lines.join('\n');
    copyText(text).then(ok => { if (ok) { setExported(true); setTimeout(() => setExported(false), 2500); } });
  };

  // Adds a suggestion to the day you're looking at — only that day is rebuilt
  const handleAddSuggestion = (sug) => {
    captureRects();
    const newItin = window.rebuildOneDay(itinerary, clampedIdx, places, trip, sug);
    if (onAddPlace) onAddPlace(sug, newItin);
  };

  // Pulls a place from another day (or from the unrouted pile) into today.
  // Only the two affected days are rebuilt — everything else stays put.
  const handlePullIn = (p) => {
    captureRects();
    let newItin = itinerary;
    const srcIdx = itinerary.findIndex(d => d.stops.some(s => s.ref === p.id));
    if (srcIdx >= 0 && srcIdx !== clampedIdx) {
      newItin = window.rebuildOneDay(newItin, srcIdx, places.filter(x => x.id !== p.id), trip);
    }
    newItin = window.rebuildOneDay(newItin, clampedIdx, places, trip, p);
    if (onReroute) onReroute(trip, newItin);
  };

  // Re-runs the router — clustering is randomized, so this genuinely re-deals the days
  const handleReshuffle = () => {
    const geoPlaces = places.filter(p => p.lat && p.lng);
    const rebuilt = window.buildItinerary(geoPlaces, trip);
    if (rebuilt && rebuilt.length > 0 && onReroute) onReroute(trip, rebuilt);
  };

  // Pin numbers skip stay stops and the breakfast placeholder
  const dayStops = (() => {
    let n = 0;
    return day.stops.map(s => ({ ...s, _pin: s.kind !== 'stay' && !s.hint ? ++n : null }));
  })();

  // Places not scheduled today (on other days or unrouted).
  // Anchored places are pinned to their day already — pulling them in would duplicate them.
  const todayRefs = new Set(day.stops.map(s => s.ref));
  const anchoredIds = window.anchoredPlaceIds(trip, places);
  const notUsedAll = places.filter(p => p.lat && p.lng && !todayRefs.has(p.id) && !anchoredIds.has(p.id));
  const notUsedToday = showAllNotToday ? notUsedAll : notUsedAll.slice(0, 5);
  const realStopsToday = dayStops.filter(s => s.kind !== 'stay' && !s.hint);
  const stayMissing = !!(trip.stay && trip.stay.trim() && !trip.stayPlace);
  // Saves that never made it onto the map — say so instead of dropping them silently
  const unplaced = (places || []).filter(p => !p.lat || !p.lng);

  // Night owl tip — look for late stops
  const lateStop = day.stops.find(s => {
    if (!s.time || s.kind === 'stay') return false;
    return parseInt(s.time.split(':')[0]) >= 21;
  });

  return (
    <div className="itin-screen" data-screen-label="04 Itinerary">
      <div className="page">
        {/* HEADER */}
        <div className="itin-h">
          <div className="itin-title-block">
            <span className="mono">your trip · {trip.where || "your destination"}</span>
            <h1>route'd.</h1>
            <div className="itin-meta">
              <span className="sticker mint">{itinerary.length} days</span>
              <span className="sticker peach">{places.length - unplaced.length} saves</span>
              <span className="sticker butter">{(trip.events || []).length} anchors</span>
              {dateRange && <span className="sticker">{dateRange}</span>}
            </div>
            {unplaced.length > 0 && (
              <details className="unplaced-note" style={{ marginTop: 10, fontSize: 13, lineHeight: 1.45 }}>
                <summary className="mono" style={{ fontSize: 11, color: 'var(--coral)', textTransform: 'none', letterSpacing: '0.04em', cursor: 'pointer' }}>
                  ⚠ {unplaced.length} of your saves {unplaced.length === 1 ? "isn't" : "aren't"} in this plan — see which
                </summary>
                <div style={{ marginTop: 6, padding: '10px 12px', border: '1.5px dashed var(--ink-line)', borderRadius: 10, background: 'var(--bg-2)', maxWidth: 560 }}>
                  {unplaced.map(p => (
                    <div key={p.id} style={{ padding: '2px 0', overflowWrap: 'anywhere' }}>
                      <b>{p.name}</b>
                      <span style={{ color: 'var(--ink-soft)' }}> — {(p.failReason && window.FAIL_REASON_TEXT[p.failReason]) || "couldn't be found"}</span>
                    </div>
                  ))}
                  {unplaced.some(p => p.failReason === 'quota') && (
                    <div style={{ marginTop: 6, color: 'var(--coral)' }}>google only allows this app a set number of place lookups a day, and today's are gone. links copied from google maps on a computer still work (they carry their own pin).</div>
                  )}
                  <div style={{ marginTop: 6, color: 'var(--ink-soft)' }}>add them by name with "+ add a place" on the day you want, or go back to your saves.</div>
                </div>
              </details>
            )}
            {stayMissing && (
              <p className="mono" style={{ marginTop: 10, fontSize: 11, color: 'var(--coral)', textTransform: 'none', letterSpacing: '0.04em' }}>
                ⚠ couldn't find your stay "{trip.stay}" — days start at the first stop. edit saves to fix it.
              </p>
            )}
          </div>
          <div style={{ display: 'flex', gap: 10, flexWrap: 'wrap' }}>
            {onBack && <button className="btn sm ghost" onClick={onBack} title="back to your saves">← saves</button>}
            <button className="btn sm" onClick={handleExport} style={{ minWidth: 90 }}>{exported ? '✓ copied!' : '↓ export'}</button>
            <button className="btn sm" onClick={handleShare} style={{ minWidth: 90, transition: 'background 0.2s' }}>{copied ? '✓ copied!' : '↗ share'}</button>
            <button className="btn sm primary" onClick={onMap}>see on map →</button>
          </div>
        </div>

        {/* Day tabs */}
        <div className="day-tabs">
          {itinerary.map((d, i) => (
            <button key={i} className={`day-tab ${i === clampedIdx ? 'active' : ''}`} onClick={() => setDayIdx(i)}>
              <span className="d-num">day {_dd(d.day)}</span>
              <span style={{ fontSize: 16 }}>{d.title}</span>
              <span className="d-num">{d.label}</span>
            </button>
          ))}
        </div>

        <div className="itin-grid">
          {/* Main day card */}
          <div className="day-card">
            <div className="day-card-h">
              <div>
                <span className="mono">day {_dd(day.day)} · {day.label}</span>
                <h2>{day.title}</h2>
                <div className="subtitle">{day.vibe}</div>
              </div>
              <div style={{ display: 'flex', gap: 6, flexShrink: 0 }}>
                <span className="sticker dark">{day.stats.km}km total</span>
              </div>
            </div>

            <DayTimeline day={day} />

            {realStopsToday.length === 0 && (
              <div style={{ padding: '22px 20px', border: '1.5px dashed var(--ink-line)', borderRadius: 12, background: 'var(--bg-2)', marginBottom: 6 }}>
                <div style={{ fontFamily: 'var(--font-display)', fontWeight: 600, fontSize: 20, letterSpacing: '-0.02em' }}>a free day. on purpose?</div>
                <p style={{ margin: '6px 0 0', fontSize: 14, color: 'var(--ink-soft)', lineHeight: 1.45 }}>
                  nothing routed here yet — you saved fewer places than you have days. pull something in from "not today",
                  grab a nearby idea, or lock in a plan below. or just sleep in.
                </p>
              </div>
            )}

            <div className="stops" ref={stopsListRef}>
              {dayStops.map((s, i) => (
                <React.Fragment key={i}>
                  {s.kind === 'stay' ? (
                    <div className="stop stay" data-flip={s.ref}>
                      <div className="stop-time">
                        <div className="stop-pin" style={{ background: 'var(--ink-soft)', fontSize: 14 }}>⌂</div>
                        <span className="time">{s.time}</span>
                      </div>
                      <div className="stop-body">
                        <h3 className="where" style={{ fontSize: 16 }}>{s.label}</h3>
                        {s.note && <p className="note">{s.note}</p>}
                      </div>
                    </div>
                  ) : (
                    <div
                      className={`stop ${s.kind || ''} ${s.fixed ? 'fixed' : ''}`}
                      data-flip={s.ref}
                      {...(isDraggableStop(s) ? {
                        draggable: true,
                        onDragStart: (e) => { setDragRef(s.ref); e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', String(s.ref)); },
                        onDragOver: (e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; if (overRef !== s.ref) setOverRef(s.ref); },
                        onDragLeave: (e) => {
                          // ignore "leaves" into the row's own children — only clear when truly out
                          if (e.currentTarget.contains(e.relatedTarget)) return;
                          if (overRef === s.ref) setOverRef(null);
                        },
                        onDrop: (e) => { e.preventDefault(); handleStopDrop(s.ref); },
                        onDragEnd: () => { setDragRef(null); setOverRef(null); },
                        title: 'drag to reorder this day',
                      } : {})}
                      style={{
                        transition: 'transform 0.18s ease, box-shadow 0.18s ease, opacity 0.18s ease',
                        borderRadius: 10,
                        ...(isDraggableStop(s) ? { cursor: 'grab' } : {}),
                        ...(dragRef === s.ref ? { opacity: 0.3, transform: 'scale(0.985) rotate(-0.4deg)', cursor: 'grabbing' } : {}),
                        ...(overRef === s.ref && dragRef && dragRef !== s.ref
                          ? (dragBefore(dragRef, s.ref)
                              ? { boxShadow: 'inset 0 -3px 0 var(--accent)', transform: 'translateY(-4px)' }   // drops after this row
                              : { boxShadow: 'inset 0 3px 0 var(--accent)', transform: 'translateY(4px)' })    // drops before this row
                          : {}),
                        ...(s.hint ? { opacity: 0.75 } : {}),
                      }}
                    >
                      <div className="stop-time">
                        <div className="stop-pin" style={s.hint ? { background: 'var(--bg)', borderStyle: 'dashed' } : undefined}>{s.hint ? '☕' : s._pin}</div>
                        <span className="time">{s.time}</span>
                        <span>{s.duration}</span>
                        {isDraggableStop(s) && <span className="stop-grip" style={{ color: 'var(--ink-soft)', fontSize: 13, opacity: 0.55, letterSpacing: 1 }}>⠿</span>}
                        {isDraggableStop(s) && (
                          <span className="stop-move">
                            <button type="button" aria-label="move earlier" title="move earlier" onClick={() => moveStop(s.ref, -1)}>↑</button>
                            <button type="button" aria-label="move later" title="move later" onClick={() => moveStop(s.ref, 1)}>↓</button>
                          </span>
                        )}
                      </div>
                      <div className="stop-body">
                        <h3 className="where">
                          {s.label}
                          {s.fixed && <span className="tag" style={{ marginLeft: 10, background: 'var(--ink-line)', color: 'var(--bg)', borderColor: 'var(--ink-line)' }}>🔒 locked</span>}
                          {String(s.ref || '').startsWith('anc-') && (
                            <button
                              onClick={() => removeAnchorByStop(s)}
                              title="unlock this"
                              style={{ marginLeft: 6, background: 'none', border: 'none', color: 'var(--ink-soft)', cursor: 'pointer', fontSize: 15, lineHeight: 1, padding: 2, opacity: 0.6 }}
                            >×</button>
                          )}
                          {isDraggableStop(s) && (
                            <button className="stop-skip" type="button" onClick={() => handleRemoveStop(s)} title="drop this place from the trip" aria-label={`drop ${s.label} from the trip`}>× skip</button>
                          )}
                        </h3>
                        {s.note && <p className="note">{s.note}</p>}
                        <div className="stop-tags">
                          <span className="tag">{s.kind}</span>
                          {(s.tags || []).filter(t => String(t).toLowerCase() !== String(s.kind || '').toLowerCase()).map((t, j) => <span key={j} className="tag">{t}</span>)}
                          {s.closedWarning && <span className="tag" style={{ background: 'var(--coral)', borderColor: 'var(--coral)', color: '#fff', fontWeight: 700 }}>may be closed</span>}
                          {!s.closedWarning && s.hoursUnknown && <span className="tag" style={{ opacity: 0.6 }}>check hours</span>}
                        </div>
                      </div>
                    </div>
                  )}
                  {i < dayStops.length - 1 && s.walkToNext > 0 && (
                    <div className="transit" data-flip={`leg-${s.ref}`}>
                      <span></span>
                      <div className="walk-bar">
                        <span className="line"></span>
                        <span>
                          {s.transitToNext ? '↳ transit' : '↳ walk'} · {s.walkToNext != null ? `${s.walkToNext} min` : '—'}
                        </span>
                        <span className="line" style={{ flex: 1 }}></span>
                      </div>
                    </div>
                  )}
                </React.Fragment>
              ))}
            </div>

            {/* Add a place to THIS day — name or maps link, lands as a normal stop */}
            <div style={{ marginTop: 18 }}>
              {addingPlace ? (
                <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', alignItems: 'center', padding: '12px 16px', background: 'var(--bg-2)', borderRadius: 12, border: '1.5px solid var(--ink-line)' }}>
                  <div className="city-ac-wrap" style={{ flex: '1 1 220px', position: 'relative' }}>
                    <input className="field-input"
                      placeholder="place name, or paste a google maps link"
                      value={addText}
                      onChange={e => handleAddInput(e.target.value)}
                      onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); submitAdd(); } if (e.key === 'Escape') resetAdd(); }}
                      onBlur={() => setTimeout(() => setAddOpen(false), 150)}
                      onFocus={() => addSugs.length > 0 && setAddOpen(true)}
                      style={{ width: '100%', fontSize: 13 }}
                      autoComplete="off"
                      autoFocus
                      disabled={addBusy}
                    />
                    {addOpen && addSugs.length > 0 && (
                      <div className="city-dropdown" style={{ zIndex: 50 }}>
                        {addSugs.map((pred, i) => (
                          <div key={`a${i}`} className="city-dropdown-item" onMouseDown={() => pickAddSug(pred)}>
                            {pred.text ? pred.text.text : ''}
                          </div>
                        ))}
                      </div>
                    )}
                  </div>
                  <button className="btn sm primary" onClick={submitAdd} disabled={addBusy || !addText.trim()} style={{ opacity: addBusy || !addText.trim() ? 0.6 : 1 }}>{addBusy ? 'finding…' : `+ add to day ${_dd(day.day)}`}</button>
                  <button className="btn ghost sm" onClick={resetAdd}>cancel</button>
                  <span className="mono" style={{ fontSize: 11, color: addErr ? 'var(--coral)' : 'var(--ink-soft)', width: '100%', textTransform: 'none', letterSpacing: '0.04em' }}>
                    {addErr || 'it joins today as a regular stop — we re-time this day around it, other days stay put'}
                  </span>
                </div>
              ) : (
                <button className="btn ghost sm" onClick={() => { setAddingAnchor(false); setAddingPlace(true); }}>+ add a place to day {_dd(day.day)}</button>
              )}
            </div>

            {/* Lock something into THIS day — no day picker needed */}
            <div style={{ marginTop: 10 }}>
              {addingAnchor ? (
                <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', alignItems: 'center', padding: '12px 16px', background: 'var(--bg-2)', borderRadius: 12, border: '1.5px solid var(--ink-line)' }}>
                  <div className="city-ac-wrap" style={{ flex: '1 1 180px', position: 'relative' }}>
                    <input className="field-input"
                      placeholder="a place from your trip, a venue, a restaurant…"
                      value={newEvent.name}
                      onChange={e => handleAnchorInput(e.target.value)}
                      onKeyDown={e => { if (e.key === 'Enter') { setAnchorOpen(false); addAnchor(); } }}
                      onBlur={() => setTimeout(() => setAnchorOpen(false), 150)}
                      onFocus={() => (anchorSugs.mine.length > 0 || anchorSugs.maps.length > 0) && setAnchorOpen(true)}
                      style={{ width: '100%', fontSize: 13 }}
                      autoComplete="off"
                      autoFocus
                    />
                    {anchorOpen && (anchorSugs.mine.length > 0 || anchorSugs.maps.length > 0) && (
                      <div className="city-dropdown" style={{ zIndex: 50 }}>
                        {anchorSugs.mine.length > 0 && (
                          <div className="mono" style={{ padding: '6px 12px 2px', fontSize: 10, letterSpacing: '0.08em', color: 'var(--ink-soft)' }}>FROM YOUR TRIP</div>
                        )}
                        {anchorSugs.mine.map((p) => (
                          <div key={p.id} className="city-dropdown-item" onMouseDown={() => pickAnchorMine(p)}>
                            📍 {p.name}<span className="mono" style={{ fontSize: 10, color: 'var(--ink-soft)' }}> · {p.kind}</span>
                          </div>
                        ))}
                        {anchorSugs.maps.length > 0 && (
                          <div className="mono" style={{ padding: '6px 12px 2px', fontSize: 10, letterSpacing: '0.08em', color: 'var(--ink-soft)' }}>FROM MAPS</div>
                        )}
                        {anchorSugs.maps.map((pred, i) => (
                          <div key={`m${i}`} className="city-dropdown-item" onMouseDown={() => pickAnchorMaps(pred)}>
                            {pred.text ? pred.text.text : ''}
                          </div>
                        ))}
                      </div>
                    )}
                  </div>
                  <input className="field-input" type="time"
                    value={newEvent.time}
                    onChange={e => setNewEvent({ ...newEvent, time: e.target.value })}
                    style={{ width: 110, fontSize: 13 }}
                  />
                  <button className="btn sm primary" onClick={addAnchor}>🔒 lock it in</button>
                  <button className="btn ghost sm" onClick={() => { setAddingAnchor(false); setAnchorOpen(false); setAnchorSugs({ mine: [], maps: [] }); }}>cancel</button>
                  <span className="mono" style={{ fontSize: 11, color: 'var(--ink-soft)', width: '100%' }}>
                    {newEvent.placeId
                      ? `📍 pinning ${newEvent.name} here — it moves out of wherever it was routed`
                      : 'we keep the hour before free so you get there on time'}
                  </span>
                </div>
              ) : (
                <button className="btn ghost sm" onClick={() => { resetAdd(); setAddingAnchor(true); }}>🔒 lock something in on day {_dd(day.day)}</button>
              )}
            </div>

            {lateStop && (
              <div style={{ marginTop: 24, padding: 16, border: '1.5px dashed var(--ink-line)', borderRadius: 12, display: 'flex', gap: 14, alignItems: 'center', background: 'var(--bg-2)' }}>
                <span style={{ fontSize: 28 }}>🦉</span>
                <div>
                  <div className="mono" style={{ marginBottom: 2 }}>night owl tip</div>
                  <div style={{ fontSize: 14, lineHeight: 1.45 }}>
                    {lateStop.label} is your late stop tonight. pace yourself getting there — everything else is just setup.
                  </div>
                </div>
              </div>
            )}
          </div>

          {/* Sidebar */}
          <div className="sidebar">
            <div className="vibe-card">
              <div className="label">today's vibe</div>
              <div className="vibe">{day.vibe}</div>
            </div>

            <div className="side-card">
              <h3>day stats</h3>
              <div className="stat-row"><span>places</span><span className="v">{day.stats.places}</span></div>
              <div className="stat-row"><span>walked</span><span className="v">{day.stats.km} km</span></div>
              <div className="stat-row"><span>meals</span><span className="v">{day.stats.food}</span></div>
              {dayStops.length > 0 && (
                <div className="stat-row"><span>start → end</span><span className="v" style={{ fontSize: 14 }}>{dayStops[0].time} → {dayStops[dayStops.length-1].time}</span></div>
              )}
            </div>

            <DaySuggestions day={day} places={places} onAdd={handleAddSuggestion} />

            {notUsedToday.length > 0 && (
              <div className="side-card" style={{ background: 'var(--mint)' }}>
                <h3 style={{ marginBottom: 8 }}>not today</h3>
                <p style={{ margin: '0 0 10px', fontSize: 13, color: 'var(--ink-soft)' }}>on other days. + pulls it into today.</p>
                <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
                  {notUsedToday.map((p, i) => (
                    <div key={i} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 8, padding: '6px 0', borderTop: '1px dashed rgba(23,18,17,0.25)' }}>
                      <span style={{ fontFamily: 'var(--font-display)', fontWeight: 600, fontSize: 14, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{p.name}</span>
                      <button className="icon-btn" title={`move ${p.name} to this day`} style={{ width: 26, height: 26, fontSize: 12, flexShrink: 0 }} onClick={() => handlePullIn(p)}>+</button>
                    </div>
                  ))}
                  {notUsedAll.length > 5 && (
                    <button className="btn ghost sm" style={{ alignSelf: 'flex-start', padding: '4px 10px', fontSize: 13 }} onClick={() => setShowAllNotToday(v => !v)}>
                      {showAllNotToday ? 'show fewer' : `+ ${notUsedAll.length - 5} more`}
                    </button>
                  )}
                </div>
              </div>
            )}

            <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
              <button className="btn ghost sm" onClick={handleReshuffle}>⇄ re-shuffle the route</button>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

// ============================================================
// GOOGLE MAP
// ============================================================
function GoogleMap({ stops, hoveredIdx, onHover, onLeave }) {
  const containerRef = useRef_IM(null);
  const mapRef = useRef_IM(null);
  const overlaysRef = useRef_IM([]);
  const polylineRef = useRef_IM(null);
  const [mapsReady, setMapsReady] = useState_IM(!!window.__mapsReady);

  useEffect_IM(() => {
    if (window.__mapsReady) return;
    const h = () => setMapsReady(true);
    window.addEventListener('mapsready', h);
    return () => window.removeEventListener('mapsready', h);
  }, []);

  // Build/rebuild map when stops or mapsReady changes
  useEffect_IM(() => {
    if (!mapsReady || !containerRef.current) return;

    const getCoords = (s) => {
      if (s.lat && s.lng) return { lat: s.lat, lng: s.lng };
      const p = (window.SAMPLE_PLACES || []).find(p => p.id === s.ref);
      return p ? { lat: p.lat, lng: p.lng } : null;
    };

    const stopsWithCoords = stops.map((s, i) => ({ ...s, idx: i, coords: getCoords(s) })).filter(s => s.coords);
    if (stopsWithCoords.length === 0) return;

    // Create map once
    if (!mapRef.current) {
      mapRef.current = new google.maps.Map(containerRef.current, {
        mapTypeId: 'roadmap',
        streetViewControl: false,
        mapTypeControl: false,
        fullscreenControl: false,
        zoomControl: true,
      });
    }
    const map = mapRef.current;

    // Fit bounds
    const bounds = new google.maps.LatLngBounds();
    stopsWithCoords.forEach(s => bounds.extend(s.coords));
    map.fitBounds(bounds, 80);

    // Clear old overlays + polyline
    overlaysRef.current.forEach(o => o.setMap(null));
    overlaysRef.current = [];
    if (polylineRef.current) { polylineRef.current.setMap(null); polylineRef.current = null; }

    // Route line
    polylineRef.current = new google.maps.Polyline({
      path: stopsWithCoords.map(s => s.coords),
      strokeColor: '#FF6FA0',
      strokeWeight: 3,
      strokeOpacity: 0.9,
      map,
    });

    // OverlayView class for HTML pins — defined inside effect so google is guaranteed to exist
    class PinOverlay extends google.maps.OverlayView {
      constructor(pos, el) { super(); this._pos = new google.maps.LatLng(pos.lat, pos.lng); this._el = el; }
      onAdd() { this.getPanes().overlayMouseTarget.appendChild(this._el); }
      draw() {
        const pt = this.getProjection().fromLatLngToDivPixel(this._pos);
        if (pt) { this._el.style.left = `${pt.x}px`; this._el.style.top = `${pt.y}px`; }
      }
      onRemove() { this._el.parentNode && this._el.parentNode.removeChild(this._el); }
    }

    const kindColor = { meal: '#F5D871', coffee: '#F7A59E', bar: '#FF6FA0', museum: '#B1DDC6', view: '#8AD0C0' };

    const pinBg = (s) => s.kind === 'stay' ? '#F3EADB' : (s.fixed ? '#1A1310' : (kindColor[s.kind] || '#8AD0C0'));
    stopsWithCoords.forEach(s => {
      const bg = pinBg(s);
      const fg = s.fixed && s.kind !== 'stay' ? '#FAF4EC' : '#1A1310';
      const el = document.createElement('div');
      el.className = 'rwl-pin' + (s.kind === 'stay' ? ' rwl-pin-stay' : '');
      el.style.cssText = `background:${bg};color:${fg};position:absolute;transform:translate(-50%,-100%);cursor:pointer;`;
      el.innerHTML = `${s.kind === 'stay' ? '⌂' : (s._pin != null ? s._pin : s.idx + 1)}<div class="rwl-pin-tail"></div>`;
      el.addEventListener('mouseenter', () => onHover(s.idx));
      el.addEventListener('mouseleave', () => onLeave());
      const overlay = new PinOverlay(s.coords, el);
      overlay._idx = s.idx;
      overlay._bg = bg;
      overlay.setMap(map);
      overlaysRef.current.push(overlay);
    });
  }, [mapsReady, stops]);

  // Update pin highlight on hover — overlays only exist for stops with coordinates,
  // so match by the stop index stored on the overlay, never by array position
  useEffect_IM(() => {
    overlaysRef.current.forEach((o) => {
      if (!o._el) return;
      const on = o._idx === hoveredIdx;
      o._el.style.background = on ? '#FF6FA0' : o._bg;
      o._el.style.transform = on ? 'translate(-50%,-100%) scale(1.18)' : 'translate(-50%,-100%) scale(1)';
      o._el.style.zIndex = on ? '1000' : '';
    });
  }, [hoveredIdx, stops]);

  if (!mapsReady) return (
    <div style={{ position:'absolute', inset:0, display:'flex', alignItems:'center', justifyContent:'center', background:'var(--bg-2)' }}>
      <span className="mono">loading map...</span>
    </div>
  );

  return <div ref={containerRef} style={{ position:'absolute', inset:0 }} />;
}

// ============================================================
// MAP SCREEN
// ============================================================
function MapScreen({ onBack, itinerary }) {
  const [activeDay, setActiveDay] = useState_IM(0);
  const [hoveredPin, setHoveredPin] = useState_IM(null);

  if (!itinerary || itinerary.length === 0) {
    return (
      <div className="map-screen" data-screen-label="05 Map">
        <div style={{ gridColumn: '1 / -1', display: 'flex', alignItems: 'center', justifyContent: 'center', flexDirection: 'column', gap: 16 }}>
          <p className="mono">no route yet — build an itinerary first.</p>
          <button className="btn ghost sm" onClick={onBack}>← back</button>
        </div>
      </div>
    );
  }

  const day = itinerary[Math.min(activeDay, itinerary.length - 1)];
  // Same numbering as the plan: hotel rows are ⌂, the breakfast placeholder isn't a real stop
  const mapStops = (() => { let n = 0; return day.stops.filter(s => !s.hint).map(s => ({ ...s, _pin: s.kind !== 'stay' ? ++n : null })); })();
  const totalStops = itinerary.reduce((a, d) => a + d.stops.filter(s => s.kind !== 'stay' && !s.hint).length, 0);
  const lockedStops = day.stops.filter(s => s.fixed && s.kind !== 'stay');
  const routingNote = lockedStops.length > 0
    ? `locked: ${lockedStops.map(s => `${s.label} at ${s.time}`).join(', ')}. everything else can shift.`
    : 'no locked stops today. remix as you go.';

  return (
    <div className="map-screen" data-screen-label="05 Map">
      {/* Sidebar */}
      <div className="map-side">
        <span className="mono">your route · {totalStops} stops</span>
        <h2>where to next?</h2>
        <Squiggle color="var(--accent)" />

        <div className="map-day-pick">
          {itinerary.map((d, i) => (
            <button
              key={i}
              className={i === activeDay ? 'on' : ''}
              onClick={() => { setActiveDay(i); setHoveredPin(null); }}
            >day {_dd(d.day)}</button>
          ))}
        </div>

        <div style={{ margin: '4px 0 10px' }}>
          <div style={{ fontFamily: 'var(--font-display)', fontWeight: 600, fontSize: 18, letterSpacing: '-0.015em' }}>{day.title}</div>
          <div className="mono" style={{ marginTop: 2 }}>{day.label} · {day.stats.km}km total</div>
        </div>

        <div className="map-stop-list">
          {mapStops.map((s, i) => (
            <div
              key={i}
              className={`map-stop ${i === hoveredPin ? 'active' : ''}`}
              onMouseEnter={() => setHoveredPin(i)}
              onMouseLeave={() => setHoveredPin(null)}
            >
              <div className="pin" style={s.kind === 'stay'
                ? { background: 'var(--bg-2)', color: 'var(--ink-soft)' }
                : { background: s.fixed ? 'var(--ink-line)' : `var(--${s.kind === 'meal' ? 'butter' : s.kind === 'coffee' ? 'peach' : s.kind === 'bar' ? 'pink' : s.kind === 'museum' ? 'mint' : 'teal'})`, color: s.fixed ? 'var(--bg)' : 'var(--ink-line)' }}>
                {s.kind === 'stay' ? '⌂' : s._pin}
              </div>
              <div className="info">
                <span className="name">{s.label}</span>
                <span className="neigh">{s.kind === 'stay' ? (s.note || 'your stay') : (s.neigh || (s.kind === 'fixed' ? 'locked' : s.kind))}</span>
              </div>
              <span className="when">{s.time}</span>
            </div>
          ))}
        </div>

        <div style={{ marginTop: 18, padding: 14, background: 'var(--bg-2)', borderRadius: 12, border: '1.5px solid var(--ink-line)' }}>
          <div className="mono" style={{ marginBottom: 6 }}>routing notes</div>
          <div style={{ fontSize: 13, lineHeight: 1.45 }}>{routingNote}</div>
        </div>

        <button className="btn ghost sm" onClick={onBack} style={{ marginTop: 18 }}>← back to itinerary</button>
      </div>

      {/* Map canvas */}
      <div className="map-canvas">
        <GoogleMap
          stops={mapStops}
          hoveredIdx={hoveredPin}
          onHover={setHoveredPin}
          onLeave={() => setHoveredPin(null)}
        />

        {/* Compass */}
        <div className="map-compass" style={{ zIndex: 1000 }}>
          <div style={{ textAlign: 'center', lineHeight: 1, fontSize: 11, fontFamily: 'var(--font-mono)' }}>
            <div style={{ color: 'var(--accent)' }}>N</div>
            <div style={{ fontSize: 18, marginTop: 2 }}>↑</div>
          </div>
        </div>

        {/* Legend */}
        <div className="map-legend" style={{ zIndex: 1000 }}>
          <div style={{ fontFamily: 'var(--font-display)', fontWeight: 600, fontSize: 13, marginBottom: 6 }}>legend</div>
          <div className="row"><span className="swatch" style={{ background: 'var(--butter)' }}></span><span>meal</span></div>
          <div className="row"><span className="swatch" style={{ background: 'var(--peach)' }}></span><span>coffee</span></div>
          <div className="row"><span className="swatch" style={{ background: 'var(--mint)' }}></span><span>museum</span></div>
          <div className="row"><span className="swatch" style={{ background: 'var(--teal)' }}></span><span>view</span></div>
          <div className="row"><span className="swatch" style={{ background: 'var(--pink)' }}></span><span>bar</span></div>
          <div className="row"><span className="swatch" style={{ background: 'var(--ink-line)' }}></span><span>locked</span></div>
          {mapStops.some(s => s.kind === 'stay') && <div className="row"><span className="swatch" style={{ background: 'var(--bg-2)' }}></span><span>⌂ your stay</span></div>}
        </div>
      </div>
    </div>
  );
}

Object.assign(window, { ItineraryScreen, MapScreen });
