/* global React, Icon, Button, Chip, MOCK, formatHandle, useGeolocation, haversineMiles, geocodeCourse, useAvailability, useCourse, useCourseSlots, useFriendsOnSlots, useMyBookings, useMatchup, useMatchDetail, createBooking, cancelBooking, startBookedMatch, checkInBooking, confirmMatchResult, useTeeWaitlist, saveTeeWaitlist, deleteTeeWaitlist, useMyWaitlist, useUserSearch, invitePartner, ShareResultCard, shareResult, plainMargin, usePaymentMethod, PaymentGateSheet, usePoolBlocks, poolShareUrl, sharePool, blockLabel, useMyCoupons, GroupBookingSheet */
// Golfer booking flow (Phase B):
//   BookScreen        — date-first availability: near-you courses + open slots
//   CourseDetailScreen— hero + Sandbox 9 + the day's bookable slots
//   BookingSheet      — pick format (1v1/2v2) + partner, reserve a slot
//   MyRoundsScreen    — upcoming + past bookings, with cancel
//
// Reserve-only for now (no payment). Reads the courses-data.jsx hooks.

// ─── Shared: 7-day date strip ─────────────────────────────────────────
function nextDays(n) {
  const out = [];
  const base = new Date(); base.setHours(0, 0, 0, 0);
  for (let i = 0; i < n; i++) {
    const d = new Date(base); d.setDate(base.getDate() + i);
    out.push(d);
  }
  return out;
}

function DateStrip({ selected, onSelect, horizon = 7 }) {
  // Booking horizon (decided): public books 5 days out, Sandbox+ 7 —
  // a rolling head start ("first pick of the pools"), not a restriction.
  const days = nextDays(horizon);
  const sameDay = (a, b) => a.toDateString() === b.toDateString();
  return (
    <div style={{ display: 'flex', gap: 8, overflowX: 'auto', padding: '0 16px 2px' }} className="scroll-hide">
      {days.map((d, i) => {
        const active = sameDay(d, selected);
        const label = i === 0 ? 'Today' : i === 1 ? 'Tmrw' : d.toLocaleDateString('en-US', { weekday: 'short' });
        return (
          <button key={d.toISOString()} onClick={() => onSelect(d)} style={{
            flex: '0 0 auto', minWidth: 56, padding: '10px 8px', borderRadius: 14,
            background: active ? 'var(--forest)' : 'var(--paper)',
            color: active ? 'var(--cream)' : 'var(--forest)',
            border: active ? 'none' : 'var(--hairline)',
            boxShadow: active ? 'var(--shadow-sm)' : 'none',
            display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 2,
          }}>
            <span style={{ fontSize: 9, fontFamily: 'var(--font-mono)', letterSpacing: '0.08em', textTransform: 'uppercase', opacity: 0.75 }}>{label}</span>
            <span style={{ fontFamily: 'var(--font-display)', fontSize: 18, lineHeight: 1 }}>{d.getDate()}</span>
          </button>
        );
      })}
    </div>
  );
}

function fmtTime(iso) {
  return new Date(iso).toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' });
}

// ─── Slot chip ────────────────────────────────────────────────────────
function SlotChip({ slot, onClick, friends }) {
  return (
    <button onClick={onClick} style={{
      flex: '0 0 auto', padding: '8px 12px', borderRadius: 999,
      background: 'var(--paper)', border: 'var(--hairline)',
      color: 'var(--forest)', display: 'inline-flex', alignItems: 'center', gap: 6,
      fontWeight: 700, fontSize: 13,
    }}>
      {fmtTime(slot.starts_at)}
      {friends && friends.length > 0 && (
        <span style={{ fontSize: 10, opacity: 0.6, fontWeight: 700 }}>· {friends.length}🟢</span>
      )}
    </button>
  );
}

// ─── Who's played each course (avatar strip on the rail cards) ────────
// One query for all visible courses → unique players per course → profiles.
function useCoursePlayers(courseIds) {
  const key = (courseIds || []).slice().sort().join(',');
  const [map, setMap] = React.useState(null); // null = loading, {} = loaded
  React.useEffect(() => {
    if (!key) { setMap({}); return undefined; }
    let on = true;
    (async () => {
      const cIds = key.split(',');
      // matches.course_id is unset by the window matcher, so resolve this course's
      // completed matches through the booking → slot link (same as live board fix).
      const { data: slots } = await sbx.from('tee_slots').select('id, course_id').in('course_id', cIds);
      const slotIds = (slots || []).map(s => s.id);
      let byCourse = {}, courseMap = {};
      (slots || []).forEach(s => { courseMap[s.id] = s.course_id; });
      if (slotIds.length) {
        const { data: bks } = await sbx.from('bookings').select('match_id, slot_id').in('slot_id', slotIds);
        const matchIds = [...new Set((bks || []).map(b => b.match_id).filter(Boolean))];
        if (matchIds.length) {
          const { data: ms } = await sbx.from('matches')
            .select('id, player_a, player_a2, player_b, player_b2, status')
            .in('id', matchIds).eq('status', 'completed').limit(400);
          (ms || []).forEach(m => {
            const cid = courseMap[bks.find(b => b.match_id === m.id)?.slot_id];
            if (cid) {
              const arr = byCourse[cid] || (byCourse[cid] = []);
              [m.player_a, m.player_a2, m.player_b, m.player_b2].forEach(p => { if (p && !arr.includes(p)) arr.push(p); });
            }
          });
        }
      }
      const allIds = [...new Set(Object.keys(byCourse).flatMap(c => byCourse[c].slice(0, 4)))];
      let pById = {};
      if (allIds.length) {
        const { data: profs } = await sbx.from('profiles').select('id, first_name, handle, avatar_url').in('id', allIds);
        (profs || []).forEach(p => { pById[p.id] = p; });
      }
      const out = {};
      for (const cid in byCourse) {
        out[cid] = { count: byCourse[cid].length, players: byCourse[cid].slice(0, 4).map(id => pById[id]).filter(Boolean) };
      }
      if (on) setMap(out);
    })();
    return () => { on = false; };
  }, [key]);
  return map;
}

// ─── Focus rail: one course in focus, neighbours peek in blurred ──────
// Loops: the list is rendered several times over and the scroll position
// silently recenters onto the middle copy after each swipe, so the end of
// the list always rolls into the beginning (even with only 2 courses).
function CourseRail({ items, onOpenCourse, playersByCourse, initialId, focusRef }) {
  const n = items.length;
  const loop = n > 1;
  const reps = loop ? Math.max(3, Math.ceil(7 / n)) : 1;
  const midStart = loop ? n * Math.floor(reps / 2) : 0;
  // Returning from a course page → open centered on that course so the
  // close animation shrinks onto the right card. Keyed on `items` (not just
  // its length): availability re-sorts once geolocation resolves, and a
  // stale index would center a DIFFERENT course after the reorder.
  const initialChild = React.useMemo(() => {
    if (!initialId) return midStart;
    const k = items.findIndex(it => it.course.id === initialId);
    return k >= 0 ? midStart + k : midStart;
  }, [items]);
  const [activeChild, setActiveChild] = React.useState(initialChild);
  const railRef = React.useRef(null);
  const settle = React.useRef(null);

  const centeredIndex = (el) => {
    const mid = el.scrollLeft + el.clientWidth / 2;
    let best = 0, bestD = Infinity;
    Array.from(el.children).forEach((c, i) => {
      const d = Math.abs((c.offsetLeft + c.offsetWidth / 2) - mid);
      if (d < bestD) { bestD = d; best = i; }
    });
    return best;
  };

  // Open centered on the middle copy so there's runway in both directions.
  // Re-centers if the list itself changes (e.g. geo re-sort) so the focused
  // card stays the SAME course, not the same index.
  React.useLayoutEffect(() => {
    const el = railRef.current; if (!el || !el.children.length) return;
    const c = el.children[initialChild] || el.children[midStart];
    el.scrollLeft = c.offsetLeft - (el.clientWidth - c.offsetWidth) / 2;
    setActiveChild(initialChild);
  }, [items]);

  const onScroll = () => {
    const el = railRef.current; if (!el) return;
    const c = centeredIndex(el);
    if (c !== activeChild) setActiveChild(c);
    if (!loop) return;
    // After the swipe settles, teleport back onto the middle copy (identical
    // content either side, so the jump is invisible).
    if (settle.current) window.clearTimeout(settle.current);
    settle.current = window.setTimeout(() => {
      const cc = centeredIndex(el);
      const desired = (cc % n) + midStart;
      if (cc !== desired && el.children[desired]) {
        el.scrollLeft += el.children[desired].offsetLeft - el.children[cc].offsetLeft;
        setActiveChild(desired);
      }
    }, 160);
  };

  return (
    <div ref={railRef} onScroll={onScroll} className="scroll-hide" style={{
      display: 'flex', gap: 12, overflowX: 'auto', overflowY: 'hidden', scrollSnapType: 'x mandatory',
      padding: '4px 34px 8px', height: '100%', boxSizing: 'border-box', alignItems: 'stretch',
    }}>
      {Array.from({ length: n * reps }, (_, idx) => {
        const { course, distanceMi } = items[idx % n];
        const focused = idx === activeChild;
        const pc = playersByCourse ? (playersByCourse[course.id] || { count: 0, players: [] }) : null;
        return (
          <button key={`${course.id}-${idx}`} ref={el => { if (focusRef && focused && el) focusRef.current = el; }} onClick={(e) => {
            if (focused) { onOpenCourse(course, e.currentTarget); return; }
            e.currentTarget.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' });
          }} style={{
            flex: '0 0 calc(100% - 68px)', scrollSnapAlign: 'center',
            height: '100%', borderRadius: 24, overflow: 'hidden', position: 'relative',
            border: 'none', padding: 0, textAlign: 'left', color: '#FFFFFF', cursor: 'pointer',
            background: course.heroImg
              ? `linear-gradient(180deg, rgba(14,28,19,0.05) 25%, rgba(14,28,19,0.82) 100%), url('${course.heroImg}')`
              : 'linear-gradient(160deg, var(--forest-dark) 0%, var(--forest) 55%, var(--moss) 100%)',
            backgroundSize: 'cover', backgroundPosition: 'center',
            boxShadow: focused ? 'var(--shadow-md)' : 'none',
            filter: focused ? 'none' : 'blur(2.5px) brightness(0.85)',
            transform: focused ? 'scale(1)' : 'scale(0.93)',
            opacity: focused ? 1 : 0.75,
            transition: 'filter 0.35s ease, transform 0.35s ease, opacity 0.35s ease, box-shadow 0.35s ease',
          }}>
            <div className="grain" style={{ position: 'absolute', inset: 0, pointerEvents: 'none' }}/>
            {!course.heroImg && (
              <img src="assets/clay-course-hole.png" alt="" style={{
                position: 'absolute', right: -14, top: 26, height: 140, opacity: 0.9,
                pointerEvents: 'none', filter: 'drop-shadow(0 8px 16px rgba(0,0,0,0.3))',
              }}/>
            )}
            <div style={{ position: 'absolute', left: 18, right: 18, bottom: 16 }}>
              <div style={{ fontFamily: 'var(--font-display)', fontSize: 22, lineHeight: 1, letterSpacing: '-0.01em', color: '#FFFFFF', textShadow: '0 2px 12px rgba(14,28,19,0.4)' }}>{course.shortName}</div>
              {/* Who's played here recently — 4 avatars max; overflow gets a
                  "+ more" line; nobody yet gets a placeholder. Blank while
                  the player map is still loading (no flicker). */}
              <div style={{ marginTop: 10, minHeight: 26 }}>
                {pc && pc.players.length > 0 && (
                  <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
                    <div style={{ display: 'flex' }}>
                      {pc.players.slice(0, 4).map((p, j) => (
                        <div key={p.id} style={{
                          width: 26, height: 26, borderRadius: 999, marginLeft: j ? -8 : 0, overflow: 'hidden',
                          background: 'var(--cream)', color: 'var(--forest)', display: 'flex', alignItems: 'center', justifyContent: 'center',
                          fontFamily: 'var(--font-display)', fontSize: 12, boxShadow: '0 0 0 2px rgba(14,28,19,0.55)',
                        }}>
                          {p.avatar_url
                            ? <img src={p.avatar_url} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover' }}/>
                            : ((p.first_name || p.handle || '?').replace(/^@/, '')[0] || '?').toUpperCase()}
                        </div>
                      ))}
                    </div>
                    {pc.count > 4 && (
                      <span style={{ fontSize: 10, fontFamily: 'var(--font-mono)', letterSpacing: '0.05em', opacity: 0.85, textShadow: '0 1px 8px rgba(14,28,19,0.5)' }}>
                        + more have played here
                      </span>
                    )}
                  </div>
                )}
                {pc && pc.players.length === 0 && (
                  <span style={{ fontSize: 11, fontFamily: 'var(--font-mono)', letterSpacing: '0.05em', opacity: 0.75, textShadow: '0 1px 8px rgba(14,28,19,0.5)' }}>
                    No rounds here yet — be the first.
                  </span>
                )}
              </div>
            </div>
          </button>
        );
      })}
    </div>
  );
}

// ─── Book screen ──────────────────────────────────────────────────────
function BookScreen({ go, profile, embedded, closeSeed, openMap }) {
  const [coords, geoStatus] = useGeolocation();
  // No date picker here anymore — dates/tee times live on the course page.
  const [date] = React.useState(() => { const d = new Date(); d.setHours(0, 0, 0, 0); return d; });
  const [q, setQ] = React.useState('');
  const [list, loading] = useAvailability(date, coords);
  const [zoom, setZoom] = React.useState(null); // { rect, course } → opening animation
  // Returning from a course page: a fullscreen copy of the card shrinks
  // back down onto its spot in the carousel.
  const closeCourseRef = React.useRef(closeSeed || null);
  const [closeAnim, setCloseAnim] = React.useState(closeCourseRef.current);
  const focusRef = React.useRef(null); // the rail's focused card element
  // Fullscreen pins map — opened from the Map button (gooey-grows from it)
  // or re-opened instantly when returning from a course you tapped on it.
  const [mapView, setMapView] = React.useState(() => (openMap ? { rect: null } : null));
  const mapBtnRef = React.useRef(null);

  const filtered = list.filter(({ course }) => {
    if (!q.trim()) return true;
    const s = q.trim().toLowerCase();
    return course.shortName.toLowerCase().includes(s) || course.city.toLowerCase().includes(s);
  });

  const playersByCourse = useCoursePlayers(filtered.map(x => x.course.id));

  // Tap the focused card → the card melts out to fill the screen, then the
  // course page takes over (its hero sits where the card grew to).
  function openCourse(course, el) {
    if (zoom) return;
    const r = el.getBoundingClientRect();
    // The phone stage scales via transform on desktop — convert the card's
    // viewport rect into the transformed ancestor's local space so the
    // overlay lines up exactly with the card.
    let anc = el.parentElement;
    while (anc && getComputedStyle(anc).transform === 'none') anc = anc.parentElement;
    let rect;
    if (anc) {
      const ar = anc.getBoundingClientRect();
      const scale = ar.width / anc.offsetWidth || 1;
      rect = { top: (r.top - ar.top) / scale, left: (r.left - ar.left) / scale, width: r.width / scale, height: r.height / scale };
    } else {
      rect = { top: r.top, left: r.left, width: r.width, height: r.height };
    }
    setZoom({ rect, course });
    // Slide the tab bar down WITH the card's growth (it stays away for the
    // whole course page and springs back with the closing shrink).
    go({ tabsAway: true });
    // Hand the course object over so the page renders instantly (no loader
    // interrupting the zoom) while the full record loads underneath.
    // courseEnter → the course page opens under a fullscreen copy of the
    // card which then slides right to reveal it.
    window.setTimeout(() => go({ screen: 'courseDetail', courseId: course.id, courseSeed: course, courseEnter: true, waitlistDate: undefined, fromMap: undefined, tabsAway: undefined }), 480);
  }

  // Picking a suggestion opens the course page directly — the fullscreen
  // card cover slides right to reveal it (no carousel rect to grow from).
  function openCourseFromSearch(course) {
    go({ screen: 'courseDetail', courseId: course.id, courseSeed: course, courseEnter: true, waitlistDate: undefined, fromMap: undefined, tabsAway: undefined });
  }

  function openMapView() {
    const el = mapBtnRef.current;
    let rect = null;
    if (el) {
      const r = el.getBoundingClientRect();
      let anc = el.parentElement;
      while (anc && getComputedStyle(anc).transform === 'none') anc = anc.parentElement;
      if (anc) {
        const ar = anc.getBoundingClientRect();
        const scale = ar.width / anc.offsetWidth || 1;
        rect = { top: (r.top - ar.top) / scale, left: (r.left - ar.left) / scale, width: r.width / scale, height: r.height / scale };
      } else {
        rect = { top: r.top, left: r.left, width: r.width, height: r.height };
      }
    }
    setMapView({ rect });
  }
  function closeMapView() {
    setMapView(null);
    if (openMap) go({ courseMap: undefined });
  }
  // Tapping a pin → course page; its Return comes back to THIS map view.
  function openCourseFromMap(course) {
    go({ screen: 'courseDetail', courseId: course.id, courseSeed: course, courseEnter: undefined, fromMap: true, waitlistDate: undefined, courseMap: undefined, tabsAway: undefined });
  }

  const locLabel = geoStatus === 'ok' ? 'Near you'
    : geoStatus === 'pending' ? 'Locating…'
    : 'Miami, FL';

  return (
    <div style={embedded
      ? { background: 'var(--canvas)', flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' }
      : { background: 'var(--canvas)', minHeight: '100%', paddingBottom: 120 }}>
      {/* Standalone route keeps its title + My rounds; the embedded Play tab
          goes straight to the search bar (pills above set the hierarchy). */}
      {!embedded && (
        <div style={{ padding: '58px 20px 12px', color: 'var(--forest)' }}>
          <button onClick={() => go({ screen: 'home' })} style={{
            width: 40, height: 40, borderRadius: 999, marginBottom: 12,
            background: 'var(--paper)', border: 'var(--hairline)', color: 'var(--forest)',
            display: 'flex', alignItems: 'center', justifyContent: 'center',
          }}>
            <Icon.ArrowLeft size={16}/>
          </button>
          <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
            <div>
              <div style={{ fontSize: 11, fontFamily: 'var(--font-mono)', opacity: 0.55, letterSpacing: '0.1em', textTransform: 'uppercase' }}>Twilight tee times</div>
              <div style={{ fontFamily: 'var(--font-display)', fontSize: 40, lineHeight: 0.92, marginTop: 8, letterSpacing: '-0.02em' }}>Book a round.</div>
            </div>
            <button onClick={() => go({ screen: 'myRounds' })} style={{
              marginTop: 6, padding: '8px 12px', borderRadius: 999,
              background: 'var(--paper)', border: 'var(--hairline)', color: 'var(--forest)',
              fontSize: 12, fontWeight: 700, display: 'inline-flex', alignItems: 'center', gap: 5, flexShrink: 0,
            }}>
              <Icon.Calendar size={13}/> My rounds
            </button>
          </div>
          <div style={{ fontSize: 12, marginTop: 8, opacity: 0.6, display: 'inline-flex', alignItems: 'center', gap: 5 }}>
            <Icon.Pin size={12}/> {locLabel}
          </div>
        </div>
      )}

      {/* Search — dark like the players search, slimmer than the pills above;
          My rounds sits beside it as a compact matching circle. Typing
          auto-suggests matching courses; picking one opens its page. */}
      <div style={{ padding: embedded ? '6px 14px 6px' : '14px 14px 6px', display: 'flex', gap: 6, flexShrink: 0 }}>
        <div style={{ flex: 1, minWidth: 0, position: 'relative' }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 8, background: 'var(--forest)', borderRadius: 13, padding: '10px 12px', boxShadow: 'var(--shadow-sm)' }}>
            <Icon.Search size={15} color="var(--cream)"/>
            <input value={q} onChange={(e) => setQ(e.target.value)} placeholder="Search courses…" className="explore-search-input"
              style={{ flex: 1, background: 'transparent', border: 'none', outline: 'none', fontSize: 13, color: 'var(--cream)', fontWeight: 600 }}/>
            {q && <button onClick={() => setQ('')} style={{ background: 'transparent', border: 'none', color: 'var(--cream)', fontSize: 12, opacity: 0.7 }}>Clear</button>}
          </div>
          {q.trim() && !loading && (
            <div style={{
              position: 'absolute', top: '100%', left: 0, right: 0, marginTop: 6, zIndex: 60,
              background: 'var(--paper)', borderRadius: 14, boxShadow: 'var(--shadow-md)',
              border: 'var(--hairline)', overflow: 'hidden',
            }}>
              {filtered.length === 0 ? (
                <div style={{ padding: '13px 16px', fontSize: 13, opacity: 0.55 }}>No courses match "{q.trim()}".</div>
              ) : filtered.slice(0, 6).map(({ course }, i, arr) => (
                <button
                  key={course.id}
                  onClick={() => { setQ(''); openCourseFromSearch(course); }}
                  style={{
                    display: 'flex', alignItems: 'center', gap: 10, width: '100%', textAlign: 'left',
                    padding: '12px 16px', background: 'transparent', border: 'none', cursor: 'pointer',
                    borderBottom: i < arr.length - 1 ? '1px solid rgba(14,28,19,0.06)' : 'none',
                  }}>
                  <Icon.Pin size={13} color="var(--forest)"/>
                  <span style={{ flex: 1, minWidth: 0 }}>
                    <span style={{ display: 'block', fontSize: 14, fontWeight: 700, color: 'var(--forest)' }}>{course.shortName}</span>
                    <span style={{ display: 'block', fontSize: 11, opacity: 0.55, marginTop: 2 }}>{course.city}{course.state ? `, ${course.state}` : ''}</span>
                  </span>
                  <Icon.ArrowRight size={13} color="var(--forest)"/>
                </button>
              ))}
            </div>
          )}
        </div>
        <button ref={mapBtnRef} onClick={openMapView} title="See every course on a map" style={{
          alignSelf: 'stretch', borderRadius: 13, flexShrink: 0, padding: '0 9px',
          background: 'var(--forest)', border: 'none', color: 'var(--cream)',
          display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 4, boxShadow: 'var(--shadow-sm)',
          fontSize: 11, fontWeight: 800, whiteSpace: 'nowrap',
        }}>
          <Icon.Pin size={13}/> Map
        </button>
        <button onClick={() => go({ screen: 'myRounds' })} title="My rounds" style={{
          alignSelf: 'stretch', borderRadius: 13, flexShrink: 0, padding: '0 9px',
          background: 'var(--forest)', border: 'none', color: 'var(--cream)',
          display: 'flex', alignItems: 'center', justifyContent: 'center', boxShadow: 'var(--shadow-sm)',
          fontSize: 11, fontWeight: 800, whiteSpace: 'nowrap',
        }}>
          My Rounds
        </button>
      </div>

      {loading ? (
        <div style={{ flex: embedded ? 1 : 'none', display: 'flex', alignItems: 'center', justifyContent: 'center' }}><SppLoader/></div>
      ) : filtered.length === 0 ? (
        <div style={{ padding: '8px 16px 0' }}>
          <div className="card" style={{ padding: 24, textAlign: 'center' }}>
            <div style={{ fontFamily: 'var(--font-display)', fontSize: 20, color: 'var(--forest)' }}>No courses found.</div>
            <div className="caption-serif" style={{ fontSize: 14, opacity: 0.7, marginTop: 6 }}>More courses are joining the Sandbox network soon.</div>
          </div>
        </div>
      ) : (
        /* Course rail — the focused card is crisp, neighbours peek in blurred.
           Embedded: fills the space down to the dock. Standalone: fixed tall. */
        <div style={embedded
          ? { flex: 1, minHeight: 0, paddingBottom: 96, display: 'flex', flexDirection: 'column' }
          : { height: 520, marginTop: 8 }}>
          <CourseRail
            items={filtered}
            playersByCourse={playersByCourse}
            onOpenCourse={openCourse}
            initialId={closeCourseRef.current && closeCourseRef.current.id}
            focusRef={focusRef}
          />
        </div>
      )}

      {zoom && <CourseZoomOverlay zoom={zoom}/>}
      {mapView && (
        <CoursesMapOverlay
          items={filtered}
          viewer={coords}
          originRect={mapView.rect}
          onOpenCourse={openCourseFromMap}
          onClose={closeMapView}
        />
      )}
      {closeAnim && (
        <CourseCloseOverlay
          course={closeAnim}
          focusRef={focusRef}
          onDone={() => { setCloseAnim(null); go({ courseClose: undefined }); }}
        />
      )}
    </div>
  );
}

// ─── Course page → carousel closing animation ─────────────────────────
// Mounts fullscreen (seamless handoff from the course page sliding away),
// waits for the rail to center on the course, then shrinks down onto the
// focused card's exact spot and unmounts — the real card is underneath.
function CourseCloseOverlay({ course, focusRef, onDone }) {
  const [rect, setRect] = React.useState(null);
  React.useEffect(() => {
    let t, tries = 0;
    const attempt = () => {
      const el = focusRef.current;
      if (!el) {
        if (++tries < 50) { t = window.setTimeout(attempt, 60); return; } // rail may still be loading
        onDone(); return;
      }
      const r = el.getBoundingClientRect();
      let anc = el.parentElement;
      while (anc && getComputedStyle(anc).transform === 'none') anc = anc.parentElement;
      let rr;
      if (anc) {
        const ar = anc.getBoundingClientRect();
        const scale = ar.width / anc.offsetWidth || 1;
        rr = { top: (r.top - ar.top) / scale, left: (r.left - ar.left) / scale, width: r.width / scale, height: r.height / scale };
      } else {
        rr = { top: r.top, left: r.left, width: r.width, height: r.height };
      }
      setRect(rr);
      window.setTimeout(onDone, 540);
    };
    t = window.setTimeout(attempt, 120);
    return () => window.clearTimeout(t);
  }, []);
  const pos = rect
    ? { top: rect.top, left: rect.left, width: rect.width, height: rect.height, borderRadius: 24 }
    : { top: 0, left: 0, width: '100%', height: '100%', borderRadius: 0 };
  return (
    <div style={{
      position: 'fixed', zIndex: 600, overflow: 'hidden', ...pos,
      background: course.heroImg
        ? `linear-gradient(180deg, rgba(14,28,19,0.05) 25%, rgba(14,28,19,0.82) 100%), url('${course.heroImg}')`
        : 'linear-gradient(160deg, var(--forest-dark) 0%, var(--forest) 55%, var(--moss) 100%)',
      backgroundSize: 'cover', backgroundPosition: 'center', color: 'var(--cream)',
      transition: 'top 0.5s cubic-bezier(0.32, 1.12, 0.35, 1), left 0.5s cubic-bezier(0.32, 1.12, 0.35, 1), width 0.5s cubic-bezier(0.32, 1.12, 0.35, 1), height 0.5s cubic-bezier(0.32, 1.12, 0.35, 1), border-radius 0.45s ease',
    }}>
      <div className="grain" style={{ position: 'absolute', inset: 0, pointerEvents: 'none' }}/>
      <div style={{ position: 'absolute', left: 20, bottom: 18, right: 20 }}>
        <div style={{ fontFamily: 'var(--font-display)', fontSize: rect ? 22 : 34, lineHeight: 1, letterSpacing: '-0.01em', color: '#FFFFFF', textShadow: '0 2px 12px rgba(14,28,19,0.4)', transition: 'font-size 0.5s cubic-bezier(0.32, 1.12, 0.35, 1)' }}>
          {course.shortName}
        </div>
      </div>
    </div>
  );
}

// ─── Card → course page opening animation ─────────────────────────────
// The tapped card "melts" out from its spot to fill the screen (springy
// gooey ease, corners rounding away), then the course page takes over.
function CourseZoomOverlay({ zoom }) {
  const [grown, setGrown] = React.useState(false);
  React.useEffect(() => {
    const id = requestAnimationFrame(() => requestAnimationFrame(() => setGrown(true)));
    return () => cancelAnimationFrame(id);
  }, []);
  const { rect, course } = zoom;
  const pos = grown
    ? { top: 0, left: 0, width: '100%', height: '100%', borderRadius: 0 }
    : { top: rect.top, left: rect.left, width: rect.width, height: rect.height, borderRadius: 24 };
  return (
    <div style={{
      position: 'fixed', zIndex: 600, overflow: 'hidden', ...pos,
      background: course.heroImg
        ? `linear-gradient(180deg, rgba(14,28,19,0.05) 25%, rgba(14,28,19,0.82) 100%), url('${course.heroImg}')`
        : 'linear-gradient(160deg, var(--forest-dark) 0%, var(--forest) 55%, var(--moss) 100%)',
      backgroundSize: 'cover', backgroundPosition: 'center', color: 'var(--cream)',
      transition: 'top 0.5s cubic-bezier(0.32, 1.12, 0.35, 1), left 0.5s cubic-bezier(0.32, 1.12, 0.35, 1), width 0.5s cubic-bezier(0.32, 1.12, 0.35, 1), height 0.5s cubic-bezier(0.32, 1.12, 0.35, 1), border-radius 0.45s ease',
    }}>
      <div className="grain" style={{ position: 'absolute', inset: 0, pointerEvents: 'none' }}/>
      <div style={{ position: 'absolute', left: 20, bottom: 18, right: 20 }}>
        <div style={{ fontFamily: 'var(--font-display)', fontSize: grown ? 34 : 22, lineHeight: 1, letterSpacing: '-0.01em', color: '#FFFFFF', textShadow: '0 2px 12px rgba(14,28,19,0.4)', transition: 'font-size 0.5s cubic-bezier(0.32, 1.12, 0.35, 1)' }}>
          {course.shortName}
        </div>
      </div>
    </div>
  );
}

// ─── Tee-off window: dual-range hour slider + waitlist ────────────────
// Golfers don't pick a fixed slot — they give the window they're free to
// play in and go on the waitlist for that date. Wider window = better odds.
function DualRangeSlider({ min, max, step, value, onChange }) {
  const trackRef = React.useRef(null);
  const valueRef = React.useRef(value); valueRef.current = value;
  const [drag, setDrag] = React.useState(null); // 0 = left thumb, 1 = right

  const pct = (v) => ((v - min) / (max - min)) * 100;
  const valFromX = (clientX) => {
    const r = trackRef.current.getBoundingClientRect();
    const f = Math.min(1, Math.max(0, (clientX - r.left) / r.width));
    return Math.min(max, Math.max(min, Math.round((min + f * (max - min)) / step) * step));
  };
  const xOf = (e) => (e.touches && e.touches[0] ? e.touches[0] : e).clientX;

  // Tapping the track grabs whichever thumb is closer.
  function onTrackDown(e) {
    const v = valFromX(xOf(e));
    const [a, b] = valueRef.current;
    const i = Math.abs(v - a) <= Math.abs(v - b) ? 0 : 1;
    if (i === 0) onChange([Math.min(v, b - step), b]); else onChange([a, Math.max(v, a + step)]);
    setDrag(i);
  }

  React.useEffect(() => {
    if (drag == null) return undefined;
    const move = (e) => {
      if (e.cancelable) e.preventDefault();
      const v = valFromX(xOf(e));
      const [a, b] = valueRef.current;
      if (drag === 0) onChange([Math.min(v, b - step), b]);
      else onChange([a, Math.max(v, a + step)]);
    };
    const end = () => setDrag(null);
    window.addEventListener('mousemove', move);
    window.addEventListener('mouseup', end);
    window.addEventListener('touchmove', move, { passive: false });
    window.addEventListener('touchend', end);
    return () => {
      window.removeEventListener('mousemove', move);
      window.removeEventListener('mouseup', end);
      window.removeEventListener('touchmove', move);
      window.removeEventListener('touchend', end);
    };
  }, [drag]);

  const thumb = (i) => (
    <div
      onMouseDown={(e) => { e.stopPropagation(); setDrag(i); }}
      onTouchStart={(e) => { e.stopPropagation(); setDrag(i); }}
      style={{
        position: 'absolute', top: '50%', left: `${pct(value[i])}%`,
        transform: 'translate(-50%, -50%)',
        width: 26, height: 26, borderRadius: 999, background: 'var(--paper)',
        border: '2.5px solid var(--forest)', boxShadow: 'var(--shadow-sm)',
        cursor: 'grab', touchAction: 'none', zIndex: 2,
      }}
    />
  );

  return (
    <div
      ref={trackRef}
      onMouseDown={onTrackDown}
      onTouchStart={onTrackDown}
      style={{ position: 'relative', height: 32, cursor: 'pointer', touchAction: 'none' }}
    >
      <div style={{ position: 'absolute', top: '50%', left: 0, right: 0, height: 5, borderRadius: 99, background: 'rgba(28,73,42,0.14)', transform: 'translateY(-50%)' }}/>
      <div style={{
        position: 'absolute', top: '50%', height: 5, borderRadius: 99, background: 'var(--forest)', transform: 'translateY(-50%)',
        left: `${pct(value[0])}%`, width: `${pct(value[1]) - pct(value[0])}%`,
      }}/>
      {thumb(0)}
      {thumb(1)}
    </div>
  );
}

const WL_MIN = 6 * 60;   // 6:00 AM
const WL_MAX = 21 * 60;  // 9:00 PM
const WL_STEP = 30;
const wlTime = (m) => new Date(2000, 0, 1, Math.floor(m / 60), m % 60)
  .toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' });

function TeeWindowPicker({ course, date, profile, tier, presetBlock }) {
  const p = (n) => String(n).padStart(2, '0');
  const dateStr = `${date.getFullYear()}-${p(date.getMonth() + 1)}-${p(date.getDate())}`;
  // For TODAY the window can't start in the past — the slider floor rises to
  // the current local time (rounded up to the step). Future dates keep the
  // regular 6 AM opening. Past the last tee-off, today is closed.
  const nowDate = new Date();
  const isToday = date.toDateString() === nowDate.toDateString();
  const nowSlot = Math.ceil((nowDate.getHours() * 60 + nowDate.getMinutes()) / WL_STEP) * WL_STEP;
  const wlMin = isToday ? Math.min(Math.max(WL_MIN, nowSlot), WL_MAX) : WL_MIN;
  const todayClosed = isToday && wlMin >= WL_MAX;
  const clampWin = ([a, b]) => {
    const lo = Math.min(Math.max(a, wlMin), WL_MAX - WL_STEP);
    const hi = Math.min(Math.max(b, lo + WL_STEP), WL_MAX);
    return [lo, hi];
  };
  const userId = profile && profile.id;
  const [saved, refresh] = useTeeWaitlist(course.id, dateStr, userId);
  const [editing, setEditing] = React.useState(false);
  const [win, setWin] = React.useState([16 * 60, 19 * 60]); // default 4–7pm twilight
  const [busy, setBusy] = React.useState(false);
  const isPlus = tier === 'plus';
  // Card-on-file gate: joining any waitlist requires a payment method.
  // The charge itself only ever fires when a match is confirmed.
  const [method, refreshMethod] = usePaymentMethod(userId);
  const [gateOpen, setGateOpen] = React.useState(false);
  // Live pool blocks for this course/date (aggregates only).
  const [pools] = usePoolBlocks(course.id, dateStr);
  const [shared, setShared] = React.useState('');
  // Active promo coupons (e.g. the referral $10) — display-only pre-Stripe.
  const coupons = useMyCoupons(userId);
  const coupon = coupons && coupons[0];
  const couponLine = coupon ? (
    <div style={{
      display: 'flex', alignItems: 'center', gap: 8, marginTop: 12,
      padding: '9px 12px', borderRadius: 12,
      background: 'rgba(28,73,42,0.07)', border: '1px dashed var(--forest)',
      fontSize: 12, fontWeight: 700, color: 'var(--forest)',
    }}>
      🎟 ${Math.round(coupon.amountCents / 100)} off applied at your first charge
      {coupon.referrerHandle ? <span style={{ fontWeight: 400, opacity: 0.75 }}>&nbsp;— sent by {String(coupon.referrerHandle).startsWith('@') ? coupon.referrerHandle : `@${coupon.referrerHandle}`}</span> : null}
    </div>
  ) : null;

  // Load the saved window into the slider whenever the date's record lands.
  // A deep-linked pool block (share link) presets the window to that hour.
  const presetFor = React.useRef(null);
  React.useEffect(() => {
    if (saved) setWin([saved.startMin, saved.endMin]);
    else if (presetBlock != null && presetFor.current !== dateStr) {
      presetFor.current = dateStr;
      setWin([presetBlock * 60, (presetBlock + 1) * 60]);
    } else setWin([16 * 60, 19 * 60]);
    setEditing(false);
  }, [saved && saved.startMin, saved && saved.endMin, dateStr]);

  // Tap a pool chip → aim your window at that block.
  function aimAtBlock(h) {
    setWin([h * 60, (h + 1) * 60]);
    if (saved && !editing) setEditing(true);
  }

  async function nudgeFriends() {
    const blockH = Math.floor((saved ? saved.startMin : clampWin(win)[0]) / 60);
    const inBlock = pools.find(b => b.blockHour === blockH);
    const res = await sharePool({
      courseName: course.shortName, dateStr, blockHour: blockH,
      available: (inBlock && inBlock.available) || 1,
      url: poolShareUrl({ courseId: course.id, dateStr, blockHour: blockH, viaHandle: profile && profile.handle }),
    });
    if (res) { setShared(res === 'copied' ? 'Link copied!' : 'Shared!'); window.setTimeout(() => setShared(''), 2500); }
  }

  // The pool strip — every rendered block has ≥1 golfer available.
  const poolStrip = pools.length > 0 && (
    <div style={{ marginTop: 14 }}>
      <div style={{ fontSize: 10, fontFamily: 'var(--font-mono)', color: 'var(--forest)', opacity: 0.55, letterSpacing: '0.1em', textTransform: 'uppercase' }}>
        Pools forming · tap to aim your window
      </div>
      <div style={{ display: 'flex', gap: 8, overflowX: 'auto', marginTop: 8, paddingBottom: 2 }} className="scroll-hide">
        {pools.map(b => (
          <button key={b.blockHour} onClick={() => aimAtBlock(b.blockHour)} style={{
            flexShrink: 0, textAlign: 'left', cursor: 'pointer', borderRadius: 13, padding: '9px 12px',
            background: b.wouldComplete ? 'var(--forest)' : 'rgba(28,73,42,0.07)',
            border: b.wouldComplete ? '1px solid var(--forest)' : '1px solid rgba(28,73,42,0.16)',
            color: b.wouldComplete ? 'var(--cream)' : 'var(--forest)',
          }}>
            <span style={{ display: 'block', fontSize: 12.5, fontWeight: 800, fontFamily: 'var(--font-mono)' }}>{blockLabel(b.blockHour)}</span>
            <span style={{ display: 'block', fontSize: 10.5, marginTop: 3, opacity: 0.85 }}>
              {b.available} waiting{b.inBand > 0 && b.inBand < b.available ? ` · ${b.inBand} near your level` : ''}
            </span>
            {b.wouldComplete && (
              <span style={{ display: 'block', fontSize: 10, fontWeight: 800, marginTop: 3 }}>⚡ 1 more locks a match</span>
            )}
          </button>
        ))}
      </div>
    </div>
  );

  // Per-round privacy override — quiet rounds never appear in friends'
  // Teeing Up (still counted anonymously in pool aggregates).
  const [quietRound, setQuietRound] = React.useState(false);
  React.useEffect(() => { setQuietRound(!!(saved && saved.quiet)); }, [saved && saved.quiet, dateStr]);

  async function doSave() {
    setBusy(true);
    const w = clampWin(win);
    await saveTeeWaitlist({ userId, courseId: course.id, dateStr, startMin: w[0], endMin: w[1], quiet: quietRound });
    setBusy(false);
    setEditing(false);
    refresh();
  }

  async function confirm() {
    if (!userId || busy) return;
    if (!method) { setGateOpen(true); return; } // no card on file → gate first
    await doSave();
  }

  if (saved === undefined) return <SppLoader size={32} pad={12}/>;

  // Already on the waitlist for this date → confirmation card.
  if (saved && !editing) {
    const myBlock = pools.find(b => b.blockHour === Math.floor(saved.startMin / 60));
    const others = myBlock ? Math.max(0, myBlock.available - 1) : 0;
    return (
      <div className="card" style={{ padding: 20 }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
          <span style={{ width: 9, height: 9, borderRadius: 999, background: 'var(--forest)', flexShrink: 0 }}/>
          <div style={{ fontFamily: 'var(--font-display)', fontSize: 19, color: 'var(--forest)', lineHeight: 1.1 }}>
            You're on the {isPlus ? 'priority ' : ''}waitlist for {wlTime(saved.startMin)} – {wlTime(saved.endMin)}
          </div>
        </div>
        <div className="caption-serif" style={{ fontSize: 13, opacity: 0.7, marginTop: 8, lineHeight: 1.5 }}>
          {others > 0
            ? `You + ${others} other${others === 1 ? '' : 's'} in this pool — we'll lock your tee time as the foursome forms.`
            : "We'll lock in your tee time as spots open in your window and let you know."}
        </div>
        {couponLine}
        <button onClick={nudgeFriends} style={{
          marginTop: 14, width: '100%', padding: '12px 14px', borderRadius: 13,
          background: 'var(--forest)', border: 'none', color: 'var(--cream)',
          fontSize: 13, fontWeight: 800, cursor: 'pointer',
        }}>
          {shared || 'Nudge friends into this pool'}
        </button>
        <button onClick={() => setEditing(true)} style={{
          marginTop: 8, width: '100%', padding: '12px 14px', borderRadius: 13,
          background: 'transparent', border: '1px solid var(--forest)', color: 'var(--forest)',
          fontSize: 13, fontWeight: 800, cursor: 'pointer',
        }}>
          Edit time window
        </button>
        <div style={{ fontSize: 10, fontFamily: 'var(--font-mono)', opacity: 0.5, marginTop: 10, letterSpacing: '0.03em', lineHeight: 1.5 }}>
          {method ? `💳 ${method.label} on file · You're only charged once your match is confirmed.` : ''}
          {method ? <br/> : null}
          Tee times aren't guaranteed.{isPlus ? '' : ' Sandbox+ members get priority on the waitlist.'}
        </div>
      </div>
    );
  }

  // Past the last tee-off for today — no window left to request.
  if (todayClosed) {
    return (
      <div className="card" style={{ padding: 20 }}>
        <div style={{ fontFamily: 'var(--font-display)', fontSize: 20, color: 'var(--forest)', lineHeight: 1 }}>
          That's a wrap for today
        </div>
        <div className="caption-serif" style={{ fontSize: 13, opacity: 0.7, marginTop: 6, lineHeight: 1.5 }}>
          Tee-time requests for today are closed. Pick another day to set your window.
        </div>
      </div>
    );
  }

  // Pick / edit the window. Clamp the displayed/submitted range to the floor
  // so a stale default (or a morning pool block) can't start in the past.
  const vwin = clampWin(win);
  return (
    <div className="card" style={{ padding: 20 }}>
      <div style={{ fontFamily: 'var(--font-display)', fontSize: 20, color: 'var(--forest)', lineHeight: 1 }}>
        What time do you want to tee off?
      </div>
      <div className="caption-serif" style={{ fontSize: 13, opacity: 0.7, marginTop: 6, lineHeight: 1.5 }}>
        Give us the window you can play in — a wider window gives you a better chance of locking in a time.
      </div>

      {poolStrip}
      {couponLine}

      {/* 1fr | auto | 1fr grid keeps "to" pinned dead-center while the time
          labels change width as the thumbs drag. */}
      <div style={{ display: 'grid', gridTemplateColumns: '1fr auto 1fr', alignItems: 'baseline', marginTop: 18 }}>
        <span style={{ fontFamily: 'var(--font-display)', fontSize: 24, color: 'var(--forest)', justifySelf: 'start' }}>{wlTime(vwin[0])}</span>
        <span style={{ fontSize: 11, fontFamily: 'var(--font-mono)', opacity: 0.5 }}>to</span>
        <span style={{ fontFamily: 'var(--font-display)', fontSize: 24, color: 'var(--forest)', justifySelf: 'end' }}>{wlTime(vwin[1])}</span>
      </div>
      <div style={{ marginTop: 6 }}>
        <DualRangeSlider min={wlMin} max={WL_MAX} step={WL_STEP} value={vwin} onChange={setWin}/>
      </div>
      <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 10, fontFamily: 'var(--font-mono)', opacity: 0.45, marginTop: 2 }}>
        <span>{wlTime(wlMin)}</span><span>9 PM</span>
      </div>

      <label style={{ display: 'flex', alignItems: 'center', gap: 8, marginTop: 14, cursor: 'pointer', fontSize: 12, color: 'var(--forest)', fontWeight: 600 }}>
        <input type="checkbox" checked={quietRound} onChange={e => setQuietRound(e.target.checked)} style={{ accentColor: 'var(--forest)' }}/>
        🤫 Quiet round — friends won't see this by name
      </label>

      <button onClick={confirm} disabled={busy || !userId} style={{
        marginTop: 12, width: '100%', padding: '14px 14px', borderRadius: 13,
        background: 'var(--forest)', border: 'none', color: 'var(--cream)',
        fontSize: 14, fontWeight: 800, cursor: 'pointer', opacity: busy ? 0.6 : 1,
      }}>
        {busy ? 'Saving…' : saved ? 'Update my window' : 'Join the waitlist'}
      </button>
      {saved && editing && (
        <button onClick={() => { setWin([saved.startMin, saved.endMin]); setEditing(false); }} style={{
          marginTop: 8, width: '100%', padding: '10px 0', background: 'transparent', border: 'none',
          color: 'var(--forest)', fontSize: 12, fontWeight: 700, cursor: 'pointer', opacity: 0.6,
        }}>Cancel</button>
      )}
      <div style={{ fontSize: 10, fontFamily: 'var(--font-mono)', opacity: 0.5, marginTop: 10, letterSpacing: '0.03em', lineHeight: 1.5 }}>
        {method
          ? `💳 ${method.label} on file · You're only charged once your match is confirmed.`
          : "You'll never be charged until your match is confirmed."}
        <br/>
        Tee times aren't guaranteed. {isPlus ? "As a Sandbox+ member you're on the priority waitlist." : 'Sandbox+ members get priority on the waitlist.'}
      </div>

      {/* Card-on-file gate — saving the test card completes the join. */}
      <PaymentGateSheet
        open={gateOpen}
        userId={userId}
        onClose={() => setGateOpen(false)}
        context="A card on file is required to join the waitlist — it's how we lock your tee time the instant a match forms."
        onAdded={() => { setGateOpen(false); refreshMethod(); doSave(); }}
      />
    </div>
  );
}

// ─── All-courses map view ─────────────────────────────────────────────
// The Map button gooey-grows into a fullscreen map of every SPP course,
// centered on YOU at a 20-mile radius (falls back to framing the pins if
// location is off). Tapping a pin opens that course's page — and its
// Return button brings you back to this map, not the carousel.
function CoursesMapOverlay({ items, viewer, originRect, onOpenCourse, onClose }) {
  const mapRef = React.useRef(null);
  const mapObj = React.useRef(null);
  const pinsRef = React.useRef(null);   // layer group so pins can refresh
  const frameRef = React.useRef(null);  // the intended bounds (re-applied after sizing)
  const youRef = React.useRef(false);
  const [grown, setGrown] = React.useState(!originRect);
  const [closing, setClosing] = React.useState(false);

  React.useEffect(() => {
    if (!originRect) return undefined;
    const id = requestAnimationFrame(() => requestAnimationFrame(() => setGrown(true)));
    return () => cancelAnimationFrame(id);
  }, []);

  // Build the map once. Frame on YOU at a 15-mile radius; the frame is
  // re-applied after the gooey grow settles (fitBounds computed against the
  // tiny pre-grow container picks a uselessly far zoom otherwise).
  React.useEffect(() => {
    if (!mapRef.current || !window.L) return undefined;
    const m = window.L.map(mapRef.current, { zoomControl: false });
    mapObj.current = m;
    window.L.tileLayer(MAP_TILES, { maxZoom: 19, attribution: MAP_ATTR }).addTo(m);
    m.setView([25.7617, -80.1918], 11); // placeholder until framed

    const frameOnYou = (you) => {
      if (youRef.current) return;
      youRef.current = true;
      window.L.circleMarker(you, { radius: 8, color: '#FFFFFF', weight: 3, fillColor: '#2F6FED', fillOpacity: 1 })
        .addTo(m).bindPopup('You are here');
      frameRef.current = window.L.latLng(you[0], you[1]).toBounds(15 * 1609.34 * 2);
      m.fitBounds(frameRef.current);
    };
    if (viewer && viewer.lat != null) {
      frameOnYou([viewer.lat, viewer.lng]);
    } else if (navigator.geolocation) {
      navigator.geolocation.getCurrentPosition(
        pos => frameOnYou([pos.coords.latitude, pos.coords.longitude]),
        () => {}, { maximumAge: 60000, timeout: 8000 }
      );
    }

    const t = window.setTimeout(() => {
      m.invalidateSize();
      if (frameRef.current) m.fitBounds(frameRef.current);
    }, originRect ? 560 : 80);
    return () => { window.clearTimeout(t); mapObj.current = null; m.remove(); };
  }, []);

  // Pins track the course list — it loads (and geocodes) asynchronously, so
  // returning from a course starts empty and fills in as data lands.
  React.useEffect(() => {
    const m = mapObj.current;
    if (!m || !window.L) return;
    if (pinsRef.current) { pinsRef.current.remove(); pinsRef.current = null; }
    const layer = window.L.layerGroup();
    const pts = [];
    for (const it of (items || [])) {
      const c = it.course || it;
      const lat = Number(c.lat), lng = Number(c.lng);
      if (!isFinite(lat) || !isFinite(lng) || (Math.abs(lat) < 0.5 && Math.abs(lng) < 0.5)) continue;
      pts.push([lat, lng]);
      window.L.marker([lat, lng], { icon: sppPinIcon() })
        .on('click', () => onOpenCourse(c))
        .addTo(layer);
    }
    layer.addTo(m);
    pinsRef.current = layer;
    // No location yet → at least frame the pins so the map isn't blank.
    if (!youRef.current && !frameRef.current && pts.length) {
      m.fitBounds(window.L.latLngBounds(pts).pad(0.3));
    }
  }, [items]);

  function close() {
    if (closing) return;
    if (!originRect) { onClose(); return; }
    setClosing(true);
    setGrown(false);
    window.setTimeout(onClose, 500);
  }

  const pos = grown
    ? { top: 0, left: 0, width: '100%', height: '100%', borderRadius: 0 }
    : { top: originRect.top, left: originRect.left, width: originRect.width, height: originRect.height, borderRadius: 13 };

  return (
    <div style={{
      position: 'fixed', zIndex: 800, overflow: 'hidden', background: '#DFEBD9',
      boxShadow: '0 24px 60px rgba(14,28,19,0.4)', ...pos, transition: MAP_SPRING,
    }}>
      <div ref={mapRef} style={{ position: 'absolute', inset: 0 }}/>
      <button onClick={close} aria-label="Close map" style={{
        position: 'absolute', top: 56, right: 16, zIndex: 1100,
        width: 42, height: 42, borderRadius: 999, cursor: 'pointer',
        background: 'rgba(14,28,19,0.72)', backdropFilter: 'blur(10px)', WebkitBackdropFilter: 'blur(10px)',
        color: 'var(--cream)', border: '1px solid rgba(234,226,206,0.25)',
        display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 16, fontWeight: 700,
        opacity: grown && !closing ? 1 : 0, transition: 'opacity 0.3s ease 0.15s',
      }}>✕</button>
    </div>
  );
}

// ─── Course location map (Leaflet via CDN) ──────────────────────
// A small map card under the course header: the course pinned with the
// SPP mark. Tapping it melts out to a fullscreen, pannable map (same
// springy gooey grow as the course cards) showing the course relative to
// your live location, with an ✕ to melt back to the page.
const MAP_TILES = 'https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png';
const MAP_ATTR = '&copy; OpenStreetMap contributors &copy; CARTO';
const MAP_SPRING = 'top 0.5s cubic-bezier(0.32, 1.12, 0.35, 1), left 0.5s cubic-bezier(0.32, 1.12, 0.35, 1), width 0.5s cubic-bezier(0.32, 1.12, 0.35, 1), height 0.5s cubic-bezier(0.32, 1.12, 0.35, 1), border-radius 0.45s ease';

// Course coordinates: trust stored lat/lng only when they're real (the
// seeds ship 0/0 — "null island" off Africa). Otherwise geocode the
// address + city + state the admin portal has on file (Nominatim/OSM),
// cached per course on-device so it's one lookup ever.
function useCourseCoords(course) {
  const rawLat = Number(course.lat), rawLng = Number(course.lng);
  const validRaw = isFinite(rawLat) && isFinite(rawLng) && (Math.abs(rawLat) > 0.5 || Math.abs(rawLng) > 0.5);
  const [coords, setCoords] = React.useState(validRaw ? { lat: rawLat, lng: rawLng } : null);
  React.useEffect(() => {
    let on = true;
    geocodeCourse(course).then(c => { if (on) setCoords(c); });
    return () => { on = false; };
  }, [course.id, course.address, course.lat, course.lng]);
  return coords;
}

function sppPinIcon() {
  return window.L.divIcon({
    className: '',
    iconSize: [38, 46], iconAnchor: [19, 44], popupAnchor: [0, -44],
    html: [
      '<div style="width:38px;height:46px;position:relative;filter:drop-shadow(0 4px 8px rgba(14,28,19,0.4));">',
      '<div style="width:38px;height:38px;border-radius:999px;background:#1C492A;border:2.5px solid #EAE2CE;box-sizing:border-box;display:flex;align-items:center;justify-content:center;">',
      '<img src="assets/monogram-cream.svg" style="height:19px;display:block;" alt=""/>',
      '</div>',
      '<div style="position:absolute;left:50%;top:34px;transform:translateX(-50%);width:0;height:0;border-left:7px solid transparent;border-right:7px solid transparent;border-top:10px solid #1C492A;"></div>',
      '</div>',
    ].join(''),
  });
}

function CourseMapCard({ course }) {
  const coords = useCourseCoords(course);
  const [viewer] = useGeolocation();
  const lat = coords && coords.lat, lng = coords && coords.lng;
  const milesAway = (coords && viewer) ? haversineMiles(viewer.lat, viewer.lng, coords.lat, coords.lng) : null;
  const ok = !!coords && !!window.L;
  const previewRef = React.useRef(null);
  const cardRef = React.useRef(null);
  const [expanded, setExpanded] = React.useState(null); // card rect | null

  // Static preview: interactions off, just the pin on the neighbourhood.
  React.useEffect(() => {
    if (!ok || !previewRef.current) return undefined;
    const m = window.L.map(previewRef.current, {
      zoomControl: false, dragging: false, scrollWheelZoom: false, doubleClickZoom: false,
      boxZoom: false, keyboard: false, touchZoom: false, attributionControl: false,
    }).setView([lat, lng], 14);
    window.L.tileLayer(MAP_TILES, { maxZoom: 19 }).addTo(m);
    window.L.marker([lat, lng], { icon: sppPinIcon(), interactive: false }).addTo(m);
    return () => m.remove();
  }, [ok, lat, lng]);

  if (!ok) return null;

  function expand() {
    const el = cardRef.current; if (!el) return;
    const r = el.getBoundingClientRect();
    // Convert into the phone stage's transformed space (same trick as the
    // course-card zoom) so the grow starts exactly on the widget.
    let anc = el.parentElement;
    while (anc && getComputedStyle(anc).transform === 'none') anc = anc.parentElement;
    let rect;
    if (anc) {
      const ar = anc.getBoundingClientRect();
      const scale = ar.width / anc.offsetWidth || 1;
      rect = { top: (r.top - ar.top) / scale, left: (r.left - ar.left) / scale, width: r.width / scale, height: r.height / scale };
    } else {
      rect = { top: r.top, left: r.left, width: r.width, height: r.height };
    }
    setExpanded(rect);
  }

  return (
    <>
      <div style={{ marginTop: 4 }}>
        <div style={{ fontSize: 11, fontFamily: 'var(--font-mono)', color: 'var(--forest)', opacity: 0.55, letterSpacing: '0.12em', textTransform: 'uppercase', marginBottom: 8 }}>
          {milesAway != null ? `${milesAway.toFixed(1)} Miles Away From You` : 'Explore On A Map'}
        </div>
        <button ref={cardRef} onClick={expand} style={{
          display: 'block', width: '100%', height: 150, borderRadius: 18, overflow: 'hidden',
          border: 'var(--hairline)', boxShadow: 'var(--shadow-sm)', padding: 0, cursor: 'pointer',
          position: 'relative', zIndex: 0, background: '#DFEBD9',
        }}>
          <div ref={previewRef} style={{ position: 'absolute', inset: 0, pointerEvents: 'none' }}/>
          <span style={{
            position: 'absolute', right: 10, bottom: 10, zIndex: 500,
            fontSize: 9, fontFamily: 'var(--font-mono)', fontWeight: 700, letterSpacing: '0.08em', textTransform: 'uppercase',
            background: 'rgba(28,73,42,0.85)', color: 'var(--cream)', padding: '5px 10px', borderRadius: 999,
          }}>Tap To Explore</span>
        </button>
      </div>
      {expanded && <CourseMapOverlay course={course} coords={coords} rect={expanded} onClose={() => setExpanded(null)}/>}
    </>
  );
}

function CourseMapOverlay({ course, coords, rect, onClose }) {
  const lat = coords.lat, lng = coords.lng;
  const mapRef = React.useRef(null);
  const [grown, setGrown] = React.useState(false);
  const [closing, setClosing] = React.useState(false);

  React.useEffect(() => {
    const id = requestAnimationFrame(() => requestAnimationFrame(() => setGrown(true)));
    return () => cancelAnimationFrame(id);
  }, []);

  // Full interactive map: the course pin + your live location; size refresh
  // once the gooey grow settles so tiles fill the whole screen.
  React.useEffect(() => {
    if (!mapRef.current || !window.L) return undefined;
    // Open with the course pin dead-center showing a ~3-mile radius.
    const homeBounds = window.L.latLng(lat, lng).toBounds(3 * 1609.34 * 2);
    const m = window.L.map(mapRef.current, { zoomControl: false });
    m.fitBounds(homeBounds);
    window.L.tileLayer(MAP_TILES, { maxZoom: 19, attribution: MAP_ATTR }).addTo(m);
    window.L.marker([lat, lng], { icon: sppPinIcon() }).addTo(m).bindPopup('<b>' + course.shortName + '</b>');
    // Your live location joins as a blue dot — without stealing the center.
    if (navigator.geolocation) {
      navigator.geolocation.getCurrentPosition(pos => {
        window.L.circleMarker([pos.coords.latitude, pos.coords.longitude], { radius: 8, color: '#FFFFFF', weight: 3, fillColor: '#2F6FED', fillOpacity: 1 })
          .addTo(m).bindPopup('You are here');
      }, () => {}, { maximumAge: 60000, timeout: 8000 });
    }
    // Re-fit once the gooey grow settles so the radius is true fullscreen.
    const t = window.setTimeout(() => { m.invalidateSize(); m.fitBounds(homeBounds); }, 560);
    return () => { window.clearTimeout(t); m.remove(); };
  }, []);

  function close() {
    if (closing) return;
    setClosing(true);
    setGrown(false);
    window.setTimeout(onClose, 500);
  }

  const pos = grown
    ? { top: 0, left: 0, width: '100%', height: '100%', borderRadius: 0 }
    : { top: rect.top, left: rect.left, width: rect.width, height: rect.height, borderRadius: 18 };

  return (
    <div style={{
      position: 'fixed', zIndex: 800, overflow: 'hidden', background: '#DFEBD9',
      boxShadow: '0 24px 60px rgba(14,28,19,0.4)', ...pos, transition: MAP_SPRING,
    }}>
      <div ref={mapRef} style={{ position: 'absolute', inset: 0 }}/>
      <button onClick={close} aria-label="Close map" style={{
        position: 'absolute', top: 56, right: 16, zIndex: 1100,
        width: 42, height: 42, borderRadius: 999, cursor: 'pointer',
        background: 'rgba(14,28,19,0.72)', backdropFilter: 'blur(10px)', WebkitBackdropFilter: 'blur(10px)',
        color: 'var(--cream)', border: '1px solid rgba(234,226,206,0.25)',
        display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 16, fontWeight: 700,
        opacity: grown && !closing ? 1 : 0, transition: 'opacity 0.3s ease 0.15s',
      }}>✕</button>
    </div>
  );
}

// ─── Course detail ────────────────────────────────────────────────────
// courseSeed: the course object handed over by the rail card, so the page
// renders instantly mid-zoom-animation (no loader flash) while the full
// record + holes load underneath.
function CourseDetailScreen({ go, courseId, profile, courseSeed, enterSlide, tier, waitlistDate, fromMap, presetBlock }) {
  const [loaded, holes, loading] = useCourse(courseId);
  const [date, setDate] = React.useState(() => {
    // Arriving from a waitlist card → open on that entry's date.
    if (waitlistDate) { const d = new Date(`${waitlistDate}T00:00:00`); if (!isNaN(d)) return d; }
    const d = new Date(); d.setHours(0, 0, 0, 0); return d;
  });
  const [slots, slotsLoading] = useCourseSlots(courseId, date);
  const [booking, setBooking] = React.useState(null);
  const [groupOpen, setGroupOpen] = React.useState(false); // Book-as-a-group sheet
  // Enter: the page mounts under a fullscreen copy of the tapped card
  // (identical to the zoom overlay's grown state) which slides right to
  // reveal it. 'hold' → one frame fullscreen → 'exit' slides away.
  const [cover, setCover] = React.useState(enterSlide ? 'hold' : null);
  // Close: the exact reverse — the fullscreen card slides back in from the
  // right over the page, then routes back so it can shrink onto the
  // carousel. 'hold' → mounted off-screen right → 'slide' covers the page.
  const [closing, setClosing] = React.useState(null);

  React.useEffect(() => {
    if (!enterSlide) return undefined;
    const t1 = window.setTimeout(() => setCover('exit'), 60);
    const t2 = window.setTimeout(() => { setCover(null); go({ courseEnter: undefined }); }, 560);
    return () => { window.clearTimeout(t1); window.clearTimeout(t2); };
  }, []);

  const course = loaded || courseSeed || null;
  if (loading && !course) return <SppLoader fill/>;
  if (!course) return <div style={{ background: 'var(--canvas)', minHeight: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--forest)' }}>Course not found.</div>;

  function goBack() {
    if (closing) return;
    if (fromMap) {
      // Opened from the pins map → straight back onto the map, no card
      // cover sliding over the page (the screen transition handles it).
      go({ screen: 'events', playTab: 'sbx', courseMap: true, fromMap: undefined, courseClose: undefined });
      return;
    }
    setClosing('hold');
    requestAnimationFrame(() => requestAnimationFrame(() => setClosing('slide')));
    window.setTimeout(() => go({
      screen: 'events', playTab: 'sbx',
      courseClose: { id: course.id, shortName: course.shortName, heroImg: course.heroImg },
    }), 540);
  }

  // Fullscreen card face — the same look the zoom overlay grows into.
  const cardFace = (z, style) => (
    <div style={{
      position: 'fixed', inset: 0, zIndex: z, overflow: 'hidden', color: 'var(--cream)',
      background: course.heroImg
        ? `linear-gradient(180deg, rgba(14,28,19,0.05) 25%, rgba(14,28,19,0.82) 100%), url('${course.heroImg}')`
        : 'linear-gradient(160deg, var(--forest-dark) 0%, var(--forest) 55%, var(--moss) 100%)',
      backgroundSize: 'cover', backgroundPosition: 'center', ...style,
    }}>
      <div className="grain" style={{ position: 'absolute', inset: 0, pointerEvents: 'none' }}/>
      <div style={{ position: 'absolute', left: 20, bottom: 18, right: 20 }}>
        <div style={{ fontFamily: 'var(--font-display)', fontSize: 34, lineHeight: 1, letterSpacing: '-0.01em', color: '#FFFFFF', textShadow: '0 2px 12px rgba(14,28,19,0.4)' }}>
          {course.shortName}
        </div>
      </div>
    </div>
  );

  const totalYards = holes.reduce((s, h) => s + (h.sandbox_yards || 0), 0);

  return (
    <>
      {/* On top of the page while entering, sliding right to reveal it */}
      {cover && cardFace(700, {
        transform: cover === 'exit' ? 'translateX(103%)' : 'none',
        transition: 'transform 0.48s cubic-bezier(0.55, 0.06, 0.35, 1)',
        boxShadow: '-24px 0 60px rgba(14,28,19,0.45)',
      })}
      {/* Reverse on close: the card slides back in from the right over the
          page, then shrinks onto the carousel after the route flips */}
      {closing && cardFace(700, {
        transform: closing === 'slide' ? 'translateX(0)' : 'translateX(103%)',
        transition: 'transform 0.48s cubic-bezier(0.55, 0.06, 0.35, 1)',
        boxShadow: '-24px 0 60px rgba(14,28,19,0.45)',
      })}
    <div style={{ background: 'var(--canvas)', minHeight: '100%', paddingBottom: 130 }}>
      {/* Hero */}
      <div style={{
        height: 220, position: 'relative', color: 'var(--cream)',
        background: (course.renderImg || course.heroImg)
          ? `linear-gradient(180deg, rgba(14,28,19,0.1) 0%, rgba(14,28,19,0.7) 100%), url('${course.renderImg || course.heroImg}')`
          : 'linear-gradient(135deg, var(--forest-dark) 0%, var(--forest) 55%, var(--moss) 100%)',
        backgroundSize: 'cover', backgroundPosition: 'center', overflow: 'hidden',
      }}>
        <div className="grain" style={{ position: 'absolute', inset: 0, pointerEvents: 'none' }}/>
        {/* Clay course-hole diorama (when no custom render set) */}
        {!(course.renderImg || course.heroImg) && (
          <img src="assets/clay-course-hole.png" alt="" style={{
            position: 'absolute', right: -10, bottom: -6, height: 150, opacity: 0.92,
            pointerEvents: 'none', filter: 'drop-shadow(0 8px 16px rgba(0,0,0,0.3))',
          }}/>
        )}
        <button onClick={goBack} style={{
          position: 'absolute', top: 56, left: 16, width: 40, height: 40, borderRadius: 999,
          background: 'rgba(14,28,19,0.55)', backdropFilter: 'blur(12px)', WebkitBackdropFilter: 'blur(12px)',
          color: 'var(--cream)', display: 'flex', alignItems: 'center', justifyContent: 'center',
          border: '1px solid rgba(234,226,206,0.22)',
        }}><Icon.ArrowLeft size={16}/></button>
        <div style={{ position: 'absolute', left: 20, bottom: 16, right: 20 }}>
          <div style={{ fontFamily: 'var(--font-display)', fontSize: 34, lineHeight: 0.95, letterSpacing: '-0.02em', color: '#FFFFFF', textShadow: '0 2px 12px rgba(14,28,19,0.4)' }}>{course.shortName}</div>
          <div style={{ fontSize: 12, fontFamily: 'var(--font-mono)', color: '#FFFFFF', opacity: 0.85, marginTop: 6, letterSpacing: '0.04em', textShadow: '0 1px 8px rgba(14,28,19,0.5)' }}>
            {course.city.toUpperCase()}{course.state ? `, ${course.state}` : ''}
          </div>
        </div>
      </div>

      {/* Sandbox 9 summary — the scorecard below carries the numbers */}
      <div style={{ padding: '16px 16px 0' }}>
        {/* Where the course is — tap to explore relative to your location */}
        <CourseMapCard course={course}/>
        {course.description && (
          <div className="caption-serif" style={{ fontSize: 15, color: 'var(--ink)', opacity: 0.8, marginTop: 14, lineHeight: 1.5 }}>
            {course.description}
          </div>
        )}
        {course.realPar && (
          <div style={{ fontSize: 11, fontFamily: 'var(--font-mono)', color: 'var(--forest)', opacity: 0.5, marginTop: 10, letterSpacing: '0.03em' }}>
            PLAYS OVER {course.name.toUpperCase()} · REAL COURSE PAR {course.realPar} · {course.realYardage?.toLocaleString()} YDS
          </div>
        )}

        {holes.length > 0 && <Scorecard holes={holes}/>}
      </div>

      {/* Booking — availability window + waitlist instead of fixed slots */}
      <div style={{ padding: '22px 0 0' }}>
        <div style={{ fontSize: 11, fontFamily: 'var(--font-mono)', color: 'var(--forest)', opacity: 0.55, letterSpacing: '0.12em', textTransform: 'uppercase', padding: '0 20px 12px' }}>When do you want to play</div>
        <DateStrip selected={date} onSelect={setDate} horizon={tier === 'plus' ? 7 : 5}/>
        <div style={{ padding: '16px 16px 0' }}>
          <TeeWindowPicker course={course} date={date} profile={profile} tier={tier} presetBlock={presetBlock}/>
          {/* Group booking: bring your own foursome — window in, exact time
              at lock, nothing held during the 3h accept clock. */}
          <button onClick={() => setGroupOpen(true)} style={{
            marginTop: 10, width: '100%', padding: '13px 14px', borderRadius: 13,
            background: 'transparent', border: '1.5px dashed var(--forest)', color: 'var(--forest)',
            fontSize: 13, fontWeight: 800, cursor: 'pointer',
          }}>
            👥 Book as a group — pick your partner & opponents
          </button>
          {groupOpen && (
            <GroupBookingSheet course={course} date={date} profile={profile} onClose={() => setGroupOpen(false)} onCreated={() => {}}/>
          )}
        </div>
      </div>

      {booking && (
        <BookingSheet
          slot={booking}
          course={course}
          profile={profile}
          onClose={() => setBooking(null)}
          onBooked={() => { setBooking(null); go({ screen: 'myRounds' }); }}
        />
      )}
    </div>
    </>
  );
}

// Per-hole Sandbox scorecard (yardages set by course / Sandbox admins).
// Fits the full 9 on screen with no sideways scrolling: fixed table layout
// squeezes the hole columns evenly into whatever width the card has.
function Scorecard({ holes }) {
  const totYards = holes.reduce((s, h) => s + (h.sandbox_yards || 0), 0);
  const totPar   = holes.reduce((s, h) => s + (h.par || 0), 0);
  const cell = { padding: '8px 1px', textAlign: 'center', fontSize: 11, overflow: 'hidden' };
  const rowLabel = { ...cell, textAlign: 'left', paddingLeft: 10, fontFamily: 'var(--font-mono)', fontSize: 8, letterSpacing: '0.08em', textTransform: 'uppercase' };
  return (
    <div style={{ marginTop: 16 }}>
      <div style={{ fontSize: 11, fontFamily: 'var(--font-mono)', color: 'var(--forest)', opacity: 0.55, letterSpacing: '0.12em', textTransform: 'uppercase', marginBottom: 8 }}>
        Sandbox 9 · scorecard
      </div>
      <div className="card" style={{ padding: 0, overflow: 'hidden' }}>
        <table style={{ borderCollapse: 'collapse', width: '100%', tableLayout: 'fixed' }}>
          <colgroup>
            <col style={{ width: 42 }}/>
            {holes.map(h => <col key={h.hole_number}/>)}
            <col style={{ width: 34 }}/>
          </colgroup>
          <tbody>
            <tr style={{ background: 'var(--forest)', color: 'var(--cream)' }}>
              <td style={{ ...rowLabel, opacity: 0.85 }}>Hole</td>
              {holes.map(h => <td key={h.hole_number} style={{ ...cell, fontFamily: 'var(--font-display)', fontSize: 13 }}>{h.hole_number}</td>)}
              <td style={{ ...cell, fontFamily: 'var(--font-mono)', fontSize: 8, letterSpacing: '0.04em', opacity: 0.85 }}>TOT</td>
            </tr>
            <tr style={{ borderBottom: '1px solid rgba(14,28,19,0.06)' }}>
              <td style={{ ...rowLabel, color: 'var(--forest)', opacity: 0.6 }}>Yards</td>
              {holes.map(h => <td key={h.hole_number} style={{ ...cell, fontWeight: 700, color: 'var(--ink)' }}>{h.sandbox_yards ?? '—'}</td>)}
              <td style={{ ...cell, fontWeight: 800, color: 'var(--forest)' }}>{totYards || '—'}</td>
            </tr>
            <tr>
              <td style={{ ...rowLabel, color: 'var(--forest)', opacity: 0.6 }}>Par</td>
              {holes.map(h => <td key={h.hole_number} style={{ ...cell, color: 'var(--ink)', opacity: 0.75 }}>{h.par}</td>)}
              <td style={{ ...cell, fontWeight: 800, color: 'var(--forest)' }}>{totPar}</td>
            </tr>
          </tbody>
        </table>
      </div>
    </div>
  );
}

function Stat({ label, value }) {
  return (
    <div style={{ flex: 1, textAlign: 'center' }}>
      <div style={{ fontFamily: 'var(--font-display)', fontSize: 22, color: 'var(--forest)', lineHeight: 1 }}>{value}</div>
      <div style={{ fontSize: 9, fontFamily: 'var(--font-mono)', color: 'var(--forest)', opacity: 0.55, letterSpacing: '0.1em', textTransform: 'uppercase', marginTop: 4 }}>{label}</div>
    </div>
  );
}
function Divider() { return <div style={{ width: 1, background: 'rgba(14,28,19,0.08)' }}/>; }

// ─── Booking sheet ────────────────────────────────────────────────────
function BookingSheet({ slot, course, profile, onClose, onBooked }) {
  const [matchType, setMatchType] = React.useState('1v1');
  const [hasPartner, setHasPartner] = React.useState(null); // null | true | false (2v2 only)
  const [partner, setPartner] = React.useState(null); // profile row
  const [q, setQ] = React.useState('');
  const [results] = useUserSearch(q, 6);
  const [busy, setBusy] = React.useState(false);
  const [err, setErr] = React.useState('');

  function pickFormat(k) {
    setMatchType(k);
    setHasPartner(null); setPartner(null); setQ(''); setErr('');
  }

  React.useEffect(() => {
    const prev = document.body.style.overflow;
    document.body.style.overflow = 'hidden';
    return () => { document.body.style.overflow = prev; };
  }, []);

  async function reserve() {
    if (matchType === '2v2') {
      if (hasPartner === null) { setErr('Do you have a partner? Pick Yes or No.'); return; }
      if (hasPartner === true && !partner) { setErr('Search and pick your partner, or choose No.'); return; }
    }
    setErr(''); setBusy(true);
    try {
      if (matchType === '2v2' && hasPartner && partner) {
        // Invite a chosen partner — they must accept (consent) to be booked.
        await invitePartner({ slotId: slot.id, partnerId: partner.id, price: slot.price });
      } else {
        await createBooking({
          slotId: slot.id, userId: profile.id, partnerId: null,
          needsPartner: matchType === '2v2' && hasPartner === false,
          matchType, price: slot.price,
        });
      }
      onBooked();
    } catch (e) { setErr(e.message || 'Could not reserve.'); setBusy(false); }
  }

  const when = new Date(slot.starts_at);
  const dateLabel = when.toLocaleDateString('en-US', { weekday: 'long', month: 'short', day: 'numeric' });

  return (
    <div onClick={(e) => { if (e.target === e.currentTarget && !busy) onClose(); }} style={{
      position: 'fixed', inset: 0, background: 'rgba(14,28,19,0.6)',
      backdropFilter: 'blur(6px)', WebkitBackdropFilter: 'blur(6px)',
      display: 'flex', alignItems: 'flex-end', justifyContent: 'center', zIndex: 1000,
    }}>
      <div style={{ width: '100%', maxWidth: 440, background: '#FFFFFF', borderTopLeftRadius: 24, borderTopRightRadius: 24, maxHeight: '92vh', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
        <div style={{ width: 38, height: 4, borderRadius: 999, background: 'rgba(14,28,19,0.16)', margin: '10px auto 0' }}/>
        <div style={{ padding: '14px 20px 18px', overflowY: 'auto' }}>
          <div style={{ fontFamily: 'var(--font-display)', fontSize: 24, color: 'var(--forest)' }}>Reserve your round</div>
          <div style={{ fontSize: 13, opacity: 0.7, marginTop: 4 }}>
            {course.shortName} · {dateLabel} · {fmtTime(slot.starts_at)}
          </div>

          {/* Format toggle */}
          <div style={{ display: 'flex', gap: 8, marginTop: 18, background: 'rgba(14,28,19,0.05)', borderRadius: 12, padding: 4 }}>
            {[['1v1', 'Head-to-head'], ['2v2', '2v2 Scramble']].map(([k, l]) => (
              <button key={k} onClick={() => pickFormat(k)} style={{
                flex: 1, padding: '10px', borderRadius: 9,
                background: matchType === k ? 'var(--forest)' : 'transparent',
                color: matchType === k ? 'var(--cream)' : 'var(--forest)',
                fontWeight: 700, fontSize: 13,
              }}>{l}</button>
            ))}
          </div>

          {/* 2v2: do you have a partner? */}
          {matchType === '2v2' && (
            <div style={{ marginTop: 16 }}>
              <div style={{ fontSize: 11, fontFamily: 'var(--font-mono)', color: 'var(--forest)', opacity: 0.6, letterSpacing: '0.1em', textTransform: 'uppercase', marginBottom: 8 }}>Do you have a partner?</div>
              <div style={{ display: 'flex', gap: 8, marginBottom: hasPartner !== null ? 14 : 0 }}>
                {[['yes', 'Yes — invite them', true], ['no', "No — pair me up", false]].map(([k, l, v]) => (
                  <button key={k} onClick={() => { setHasPartner(v); setPartner(null); setQ(''); setErr(''); }} style={{
                    flex: 1, padding: '12px 10px', borderRadius: 12,
                    background: hasPartner === v ? 'var(--forest)' : 'var(--paper)',
                    color: hasPartner === v ? 'var(--cream)' : 'var(--forest)',
                    border: hasPartner === v ? 'none' : 'var(--hairline)',
                    fontWeight: 700, fontSize: 13,
                  }}>{l}</button>
                ))}
              </div>

              {hasPartner === false && (
                <div style={{ background: 'rgba(28,73,42,0.06)', borderRadius: 12, padding: '12px 14px', fontSize: 13, color: 'var(--ink)', opacity: 0.85, lineHeight: 1.4 }}>
                  We'll pair you with another solo at this tee time. Your match locks only once a full foursome is set — you'll never be left in a 2-on-1.
                </div>
              )}
            </div>
          )}

          {/* Partner picker — only when "Yes" */}
          {matchType === '2v2' && hasPartner === true && (
            <div style={{ marginTop: 14 }}>
              <div style={{ fontSize: 11, fontFamily: 'var(--font-mono)', color: 'var(--forest)', opacity: 0.6, letterSpacing: '0.1em', textTransform: 'uppercase', marginBottom: 8 }}>Your partner</div>
              {partner ? (
                <div style={{ display: 'flex', alignItems: 'center', gap: 10, background: 'var(--paper)', borderRadius: 12, padding: '10px 12px', border: 'var(--hairline)' }}>
                  <div style={{ width: 32, height: 32, borderRadius: 999, background: '#5A7B4A', color: 'var(--cream)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontFamily: 'var(--font-display)', fontSize: 15, overflow: 'hidden' }}>
                    {partner.avatar_url ? <img src={partner.avatar_url} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover' }}/> : (partner.first_name || partner.handle || '?').replace(/^@/, '').charAt(0).toUpperCase()}
                  </div>
                  <div style={{ flex: 1 }}>
                    <div style={{ fontSize: 13, fontWeight: 700 }}>{[partner.first_name, partner.last_name].filter(Boolean).join(' ') || partner.handle}</div>
                    <div style={{ fontSize: 11, opacity: 0.6 }}>{formatHandle(partner.handle)}</div>
                  </div>
                  <button onClick={() => { setPartner(null); setQ(''); }} style={{ background: 'transparent', border: 'none', color: 'var(--forest)', fontSize: 12, fontWeight: 700, opacity: 0.7 }}>Change</button>
                </div>
              ) : (
                <>
                  <div style={{ display: 'flex', alignItems: 'center', gap: 10, background: 'var(--paper)', borderRadius: 12, padding: '10px 12px', border: 'var(--hairline)' }}>
                    <Icon.Search size={15} color="var(--forest)"/>
                    <input value={q} onChange={(e) => setQ(e.target.value)} placeholder="Find a partner by @handle…" style={{ flex: 1, background: 'transparent', border: 'none', outline: 'none', fontSize: 14, fontWeight: 600, color: 'var(--ink)' }}/>
                  </div>
                  {q.trim() && results.length > 0 && (
                    <div className="card" style={{ marginTop: 8, overflow: 'hidden' }}>
                      {results.filter(r => r.id !== profile.id).map((r, i, arr) => (
                        <button key={r.id} onClick={() => { setPartner(r); }} style={{
                          width: '100%', textAlign: 'left', background: 'transparent', border: 'none',
                          padding: '10px 12px', display: 'flex', alignItems: 'center', gap: 10,
                          borderBottom: i < arr.length - 1 ? '1px solid rgba(14,28,19,0.05)' : 'none', cursor: 'pointer',
                        }}>
                          <div style={{ width: 28, height: 28, borderRadius: 999, background: '#5A7B4A', color: 'var(--cream)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontFamily: 'var(--font-display)', fontSize: 13, overflow: 'hidden' }}>
                            {r.avatar_url ? <img src={r.avatar_url} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover' }}/> : (r.first_name || r.handle || '?').replace(/^@/, '').charAt(0).toUpperCase()}
                          </div>
                          <div style={{ flex: 1 }}>
                            <div style={{ fontSize: 13, fontWeight: 700 }}>{[r.first_name, r.last_name].filter(Boolean).join(' ') || r.handle}</div>
                            <div style={{ fontSize: 11, opacity: 0.55 }}>{formatHandle(r.handle)}</div>
                          </div>
                        </button>
                      ))}
                    </div>
                  )}
                </>
              )}
            </div>
          )}

          {/* Price + reserve */}
          <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginTop: 20, padding: '14px 0 4px' }}>
            <div>
              <div style={{ fontSize: 11, fontFamily: 'var(--font-mono)', opacity: 0.55, letterSpacing: '0.08em', textTransform: 'uppercase' }}>Price (set by course)</div>
              <div style={{ fontFamily: 'var(--font-display)', fontSize: 28, color: 'var(--forest)', marginTop: 2 }}>${slot.price}</div>
            </div>
            <div style={{ fontSize: 11, opacity: 0.5, textAlign: 'right', maxWidth: 150 }}>Reserve now, pay at the course. No charge yet.</div>
          </div>

          {err && <div style={{ background: 'rgba(196,69,54,0.08)', color: '#9C2E22', borderRadius: 10, padding: '10px 12px', fontSize: 13, fontWeight: 600, marginTop: 8 }}>{err}</div>}

          <Button variant="forest" full size="md" onClick={reserve} disabled={busy} style={{ marginTop: 16 }}>
            {busy ? 'Reserving…' : 'Reserve tee time'}
          </Button>
          <Button variant="outline" full size="md" onClick={onClose} disabled={busy} style={{ marginTop: 10 }}>Cancel</Button>
        </div>
      </div>
    </div>
  );
}

// ─── My Rounds ────────────────────────────────────────────────────────
function MyRoundsScreen({ go, profile }) {
  const userId = profile && profile.id;
  const [upcoming, past, loading] = useMyBookings(userId);
  const [waitlist, refreshWaitlist] = useMyWaitlist(userId);
  const [busyId, setBusyId] = React.useState(null);

  async function cancel(b) {
    setBusyId(b.id);
    try { await cancelBooking({ bookingId: b.id }); } catch (_) {}
    setBusyId(null);
  }
  async function leaveWaitlist(w) {
    const k = `${w.courseId}|${w.dateStr}`;
    setBusyId(k);
    try { await deleteTeeWaitlist({ userId, courseId: w.courseId, dateStr: w.dateStr }); } catch (_) {}
    setBusyId(null);
    refreshWaitlist();
  }

  const anyLoading = loading || waitlist === null;
  const empty = !anyLoading && upcoming.length === 0 && (waitlist || []).length === 0;

  return (
    <div style={{ background: 'var(--canvas)', minHeight: '100%', paddingBottom: 120 }}>
      <div style={{ padding: '58px 22px 16px', display: 'flex', alignItems: 'center', gap: 12 }}>
        <button onClick={() => go({ screen: 'events', playTab: 'sbx' })} style={{ width: 40, height: 40, borderRadius: 999, background: 'var(--paper)', border: 'var(--hairline)', color: 'var(--forest)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
          <Icon.ArrowLeft size={16}/>
        </button>
        <div style={{ fontFamily: 'var(--font-display)', fontSize: 30, color: 'var(--forest)', letterSpacing: '-0.02em' }}>My Rounds</div>
      </div>

      {/* Waitlist windows + confirmed tee times — past rounds live in match history. */}
      <div style={{ padding: '4px 16px 0' }}>
        {anyLoading ? (
          <SppLoader/>
        ) : empty ? (
          <div className="card" style={{ padding: 22, textAlign: 'center', marginBottom: 18 }}>
            <img src="assets/clay-golfer.png" alt="" style={{ height: 120, margin: '0 auto 6px', display: 'block' }}/>
            <div style={{ fontFamily: 'var(--font-display)', fontSize: 18, color: 'var(--forest)' }}>Nothing booked yet.</div>
            <div className="caption-serif" style={{ fontSize: 14, opacity: 0.7, marginTop: 4, marginBottom: 14 }}>You have no upcoming tee times — find a twilight round near you.</div>
            <Button variant="forest" size="sm" onClick={() => go({ screen: 'events', playTab: 'sbx' })}>Book a round</Button>
          </div>
        ) : (
          <>
            {(waitlist || []).length > 0 && (
              <>
                <Section label="On the waitlist"/>
                <div style={{ display: 'flex', flexDirection: 'column', gap: 12, marginBottom: 18 }}>
                  {waitlist.map(w => (
                    <WaitlistCard
                      key={`${w.courseId}|${w.dateStr}`}
                      w={w}
                      busy={busyId === `${w.courseId}|${w.dateStr}`}
                      onLeave={() => leaveWaitlist(w)}
                      go={go}
                    />
                  ))}
                </div>
              </>
            )}
            {upcoming.length > 0 && (
              <>
                <Section label="Confirmed tee times"/>
                <div style={{ display: 'flex', flexDirection: 'column', gap: 12, marginBottom: 18 }}>
                  {upcoming.map(b => <RoundCard key={b.id} b={b} onCancel={() => cancel(b)} busy={busyId === b.id} go={go}/>)}
                </div>
              </>
            )}
          </>
        )}
      </div>
    </div>
  );
}

// One waitlist window: course + date + the hour range, pending until a
// tee time inside the window gets confirmed (then it shows as a booking).
function WaitlistCard({ w, busy, onLeave, go }) {
  const when = new Date(`${w.dateStr}T12:00:00`);
  return (
    <div className="card" style={{ padding: 14 }}>
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
        <button onClick={() => go({ screen: 'courseDetail', courseId: w.courseId, waitlistDate: w.dateStr })} style={{ background: 'transparent', border: 'none', padding: 0, textAlign: 'left' }}>
          <div style={{ fontFamily: 'var(--font-display)', fontSize: 19, color: 'var(--forest)', lineHeight: 1 }}>{w.courseName || 'Course'}</div>
          <div style={{ fontSize: 12, opacity: 0.65, marginTop: 5 }}>
            {when.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' })} · {wlTime(w.startMin)} – {wlTime(w.endMin)} window
          </div>
        </button>
        <span style={{
          fontSize: 9, fontFamily: 'var(--font-mono)', fontWeight: 800, letterSpacing: '0.08em', textTransform: 'uppercase',
          padding: '5px 10px', borderRadius: 999, flexShrink: 0,
          background: 'rgba(28,73,42,0.08)', color: 'var(--forest)', border: '1px dashed rgba(28,73,42,0.35)',
        }}>Waitlist</span>
      </div>
      <div style={{ fontSize: 11, fontFamily: 'var(--font-mono)', color: 'var(--forest)', opacity: 0.5, marginTop: 9, letterSpacing: '0.03em' }}>
        We'll confirm a tee time inside your window as spots open.
      </div>
      <div style={{ display: 'flex', gap: 16, marginTop: 10 }}>
        <button onClick={() => go({ screen: 'courseDetail', courseId: w.courseId, waitlistDate: w.dateStr })} style={{
          background: 'transparent', border: 'none', color: 'var(--forest)', fontSize: 12, fontWeight: 800,
          display: 'inline-flex', alignItems: 'center', gap: 4, padding: 0, cursor: 'pointer',
        }}>
          Edit window <Icon.ArrowRight size={13}/>
        </button>
        <button onClick={onLeave} disabled={busy} style={{
          background: 'transparent', border: 'none', color: 'var(--loss, #C44536)',
          fontSize: 12, fontWeight: 700, opacity: busy ? 0.5 : 0.85, padding: 0, cursor: 'pointer',
        }}>
          {busy ? 'Leaving…' : 'Leave waitlist'}
        </button>
      </div>
    </div>
  );
}

function Section({ label }) {
  return <div style={{ fontSize: 11, fontFamily: 'var(--font-mono)', color: 'var(--forest)', opacity: 0.55, letterSpacing: '0.12em', textTransform: 'uppercase', margin: '6px 4px 10px' }}>{label}</div>;
}

function RoundCard({ b, onCancel, busy, past, go }) {
  const slot = b.slot || {};
  const course = slot.course || {};
  const when = slot.starts_at ? new Date(slot.starts_at) : null;
  const within24h = when && (when.getTime() - Date.now() < 24 * 3600 * 1000);
  return (
    <div className="card" style={{ padding: 14, opacity: past ? 0.7 : 1 }}>
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
        <button onClick={() => course.id && go({ screen: 'courseDetail', courseId: course.id, waitlistDate: undefined })} style={{ background: 'transparent', border: 'none', padding: 0, textAlign: 'left' }}>
          <div style={{ fontFamily: 'var(--font-display)', fontSize: 19, color: 'var(--forest)', lineHeight: 1 }}>{course.short_name || 'Course'}</div>
          <div style={{ fontSize: 12, opacity: 0.65, marginTop: 5 }}>
            {when ? when.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' }) : ''} · {when ? fmtTime(slot.starts_at) : ''}
          </div>
        </button>
        <Chip variant={b.match_type === '2v2' ? 'forest' : 'default'} style={{ fontSize: 10 }}>{b.match_type === '2v2' ? '2v2' : '1v1'}</Chip>
      </div>
      {b.partner && (
        <div style={{ fontSize: 12, opacity: 0.7, marginTop: 8 }}>Partner: {formatHandle(b.partner.handle)}</div>
      )}
      {b.match_id && (
        <button onClick={() => go({ screen: past ? 'matchDetail' : 'matchup', matchId: b.match_id, from: 'myRounds' })} style={{
          marginTop: 10, background: 'transparent', border: 'none', color: 'var(--forest)',
          fontSize: 12, fontWeight: 800, display: 'inline-flex', alignItems: 'center', gap: 4, padding: 0,
        }}>
          {past ? 'View result' : 'View matchup'} <Icon.ArrowRight size={13}/>
        </button>
      )}
      {!past && !within24h && (
        <button onClick={onCancel} disabled={busy} style={{ marginTop: 12, background: 'transparent', border: 'none', color: 'var(--loss, #C44536)', fontSize: 12, fontWeight: 700, opacity: busy ? 0.5 : 0.85 }}>
          {busy ? 'Cancelling…' : 'Cancel reservation'}
        </button>
      )}
      {!past && within24h && (
        <div style={{ marginTop: 12, fontSize: 11, fontFamily: 'var(--font-mono)', color: 'var(--forest)', opacity: 0.55, letterSpacing: '0.04em' }}>
          Locked in — within 24h of tee time.
        </div>
      )}
    </div>
  );
}

// ─── Matchup reveal (scout your foursome / opponent) ──────────────────
function fmtCountdown(ms) {
  if (ms <= 0) return 'now';
  const s = Math.floor(ms / 1000), h = Math.floor(s / 3600), m = Math.floor((s % 3600) / 60), ss = s % 60;
  if (h > 0) return `${h}h ${m}m`;
  return `${m}:${String(ss).padStart(2, '0')}`;
}

function MatchupScreen({ go, matchId, profile }) {
  const [data, loading, reload] = useMatchup(matchId);
  const [busy, setBusy] = React.useState(false);
  const [err, setErr] = React.useState('');
  const [nowMs, setNowMs] = React.useState(Date.now());
  React.useEffect(() => {
    const iv = setInterval(() => setNowMs(Date.now()), 1000);
    return () => clearInterval(iv);
  }, []);
  const meId = profile && profile.id;

  async function doCheckIn(mid) {
    setBusy(true); setErr('');
    try { await checkInBooking(mid); await reload(); }
    catch (e) { setErr(e.message || 'Could not check you in.'); }
    setBusy(false);
  }
  async function doStart(mid) {
    setBusy(true); setErr('');
    try { await startBookedMatch(mid); go({ screen: 'match', matchId: mid }); }
    catch (e) { setErr(e.message || 'Could not start the match.'); setBusy(false); }
  }

  if (loading) return <SppLoader fill/>;
  if (!data) return <div style={{ background: 'var(--canvas)', minHeight: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--forest)' }}>Matchup not found.</div>;

  const { match, teamA, teamB, checkins = {}, teeISO } = data;
  const is2v2 = (teamA.length + teamB.length) > 2;
  const mySide = teamA.some(p => p.id === meId) ? 'A' : teamB.some(p => p.id === meId) ? 'B' : null;

  const players = [...teamA, ...teamB];
  const isReady = (id) => ['checked_in', 'playing'].includes(checkins[id]);
  const teeMs = teeISO ? new Date(teeISO).getTime() : null;
  const within10 = teeMs != null && nowMs >= teeMs - 10 * 60000;
  const teePassed = teeMs != null && nowMs >= teeMs;
  const total = players.length;
  const readyCount = players.filter(p => isReady(p.id)).length;
  const iAmReady = isReady(meId);
  const allReady = total > 0 && readyCount === total;
  // All in -> start early (within 10 min). Tee passed -> any checked-in player starts.
  const canStart = iAmReady && ((allReady && within10) || teePassed);

  return (
    <div style={{ background: 'var(--canvas)', minHeight: '100%', paddingBottom: 120 }}>
      <div style={{
        padding: '56px 20px 24px', color: 'var(--cream)', position: 'relative',
        background: 'linear-gradient(135deg, var(--forest-dark) 0%, var(--forest) 55%, var(--moss) 100%)',
      }}>
        <div className="grain" style={{ position: 'absolute', inset: 0, pointerEvents: 'none' }}/>
        <button onClick={() => go({ screen: 'myRounds' })} style={{
          position: 'absolute', top: 56, left: 16, width: 40, height: 40, borderRadius: 999,
          background: 'rgba(14,28,19,0.45)', backdropFilter: 'blur(10px)', WebkitBackdropFilter: 'blur(10px)',
          color: 'var(--cream)', display: 'flex', alignItems: 'center', justifyContent: 'center',
          border: '1px solid rgba(234,226,206,0.22)',
        }}><Icon.ArrowLeft size={16}/></button>
        <div style={{ position: 'relative', textAlign: 'center', marginTop: 8 }}>
          <div style={{ fontSize: 10, fontFamily: 'var(--font-mono)', opacity: 0.7, letterSpacing: '0.16em', textTransform: 'uppercase' }}>{is2v2 ? '2v2 Scramble' : 'Head-to-head'} · {match.course_name}</div>
          <div style={{ fontFamily: 'var(--font-display)', fontSize: 32, marginTop: 8, letterSpacing: '-0.01em' }}>The Matchup</div>
          {match.status !== 'completed' && teeMs != null && (
            <div style={{ marginTop: 10, display: 'inline-flex', alignItems: 'center', gap: 8, background: 'rgba(14,28,19,0.35)', borderRadius: 999, padding: '6px 14px' }}>
              <span style={{ fontSize: 11, fontFamily: 'var(--font-mono)', letterSpacing: '0.08em', opacity: 0.85 }}>
                {match.status === 'active' ? 'LIVE NOW'
                  : teePassed ? 'TEE TIME IS HERE'
                    : `TEES OFF IN ${fmtCountdown(teeMs - nowMs).toUpperCase()}`}
              </span>
            </div>
          )}
        </div>
      </div>

      <div style={{ padding: '20px 16px 0' }}>
        <TeamBlock label={mySide === 'A' ? 'Your team' : 'Team A'} players={teamA} meId={meId} go={go} checkins={checkins} matchId={match.id}/>
        <div style={{ textAlign: 'center', fontFamily: 'var(--font-display)', fontSize: 22, color: 'var(--forest)', opacity: 0.5, margin: '10px 0' }}>VS</div>
        <TeamBlock label={mySide === 'B' ? 'Your team' : (is2v2 ? 'Team B' : 'Opponent')} players={teamB} meId={meId} go={go} checkins={checkins} matchId={match.id}/>
      </div>

      <div style={{ padding: '20px 16px 0' }}>
        {err && <div style={{ marginBottom: 12, fontSize: 13, color: 'var(--loss)', background: 'rgba(155,58,46,0.08)', padding: '10px 14px', borderRadius: 10 }}>{err}</div>}

        {match.status === 'completed' ? (
          <Button variant="forest" full size="md" onClick={() => go({ screen: 'matchDetail', matchId: match.id, from: 'myRounds' })}>
            View result
          </Button>
        ) : match.status === 'active' ? (
          <Button variant="forest" full size="md" onClick={() => go({ screen: 'match', matchId: match.id })}>
            ⛳ Join match — in progress
          </Button>
        ) : canStart ? (
          <Button variant="forest" full size="md" onClick={() => doStart(match.id)} disabled={busy}>
            {busy ? 'Starting…' : '⛳ Start match'}
          </Button>
        ) : !within10 ? (
          <div style={{ textAlign: 'center', fontSize: 13, color: 'var(--ink)', opacity: 0.7, padding: '12px 8px', lineHeight: 1.5 }}>
            ⏱ Check-in opens 10 minutes before tee time. {readyCount > 0 ? `${readyCount}/${total} already in.` : 'Scout your opponents below in the meantime.'}
          </div>
        ) : !iAmReady ? (
          <Button variant="forest" full size="md" onClick={() => doCheckIn(match.id)} disabled={busy}>
            {busy ? 'Checking in…' : `✓ Check in — ${readyCount}/${total} ready`}
          </Button>
        ) : (
          <div style={{ textAlign: 'center', fontSize: 13, color: 'var(--forest)', fontWeight: 700, padding: '12px 8px', lineHeight: 1.5 }}>
            Checked in ✓ · {readyCount}/{total} ready<br/>
            <span style={{ fontWeight: 400, opacity: 0.7, color: 'var(--ink)' }}>
              {teePassed ? 'You can start now — no-shows will be skipped.' : `Waiting on ${total - readyCount} more, or start once the tee time hits.`}
            </span>
          </div>
        )}

        <Button variant="outline" full size="md" onClick={() => go({ screen: 'chat', matchId: match.id, title: 'Group chat' })} style={{ marginTop: 10 }}>
          💬 Group chat
        </Button>
        <div className="caption-serif" style={{ fontSize: 14, color: 'var(--ink)', opacity: 0.65, textAlign: 'center', lineHeight: 1.5, marginTop: 14 }}>
          Scout your opponents — know your SBX gap before the first tee.
        </div>
      </div>
    </div>
  );
}

function TeamBlock({ label, players, meId, go, checkins, matchId }) {
  const isReady = (id) => ['checked_in', 'playing'].includes((checkins || {})[id]);
  return (
    <div style={{ marginBottom: 4 }}>
      <div style={{ fontSize: 10, fontFamily: 'var(--font-mono)', color: 'var(--forest)', opacity: 0.55, letterSpacing: '0.12em', textTransform: 'uppercase', marginBottom: 8, paddingLeft: 4 }}>{label}</div>
      <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
        {players.map(p => <ScoutCard key={p.id} p={p} me={p.id === meId} go={go} ready={isReady(p.id)} matchId={matchId}/>)}
      </div>
    </div>
  );
}

function ScoutCard({ p, me, go, ready, matchId }) {
  const name = [p.first_name, p.last_name].filter(Boolean).join(' ') || p.handle;
  const initial = (name || '?').replace(/^@/, '').charAt(0).toUpperCase();
  return (
    <button onClick={() => go({ screen: 'profile', viewingHandle: p.handle, backTo: matchId ? { screen: 'matchup', matchId } : undefined })} className="card" style={{
      width: '100%', textAlign: 'left', display: 'flex', alignItems: 'center', gap: 14, padding: 14,
      border: me ? '1.5px solid var(--forest)' : 'var(--hairline)',
    }}>
      <div style={{ position: 'relative', flexShrink: 0 }}>
        <div style={{ width: 48, height: 48, borderRadius: 999, background: '#5A7B4A', color: 'var(--cream)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontFamily: 'var(--font-display)', fontSize: 20, overflow: 'hidden' }}>
          {p.avatar_url ? <img src={p.avatar_url} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover' }}/> : initial}
        </div>
        {ready && (
          <div style={{ position: 'absolute', bottom: -2, right: -2, width: 18, height: 18, borderRadius: 999, background: 'var(--forest)', color: 'var(--cream)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 10, fontWeight: 900, border: '2px solid #fff' }}>✓</div>
        )}
      </div>
      <div style={{ flex: 1, minWidth: 0 }}>
        <div style={{ fontSize: 15, fontWeight: 800, color: 'var(--ink)' }}>{name}{me && <span style={{ opacity: 0.5, fontWeight: 600 }}> · you</span>}{ready && <span style={{ fontSize: 10, fontFamily: 'var(--font-mono)', color: 'var(--forest)', marginLeft: 6, letterSpacing: '0.05em' }}>CHECKED IN</span>}</div>
        <div style={{ fontSize: 12, opacity: 0.6, marginTop: 2 }}>{formatHandle(p.handle)}</div>
      </div>
      <div style={{ textAlign: 'right' }}>
        <div style={{ fontFamily: 'var(--font-display)', fontSize: 22, color: 'var(--forest)', lineHeight: 1 }}>{(Number(p.sbx) || 4).toFixed(3)}</div>
        <div style={{ fontSize: 9, fontFamily: 'var(--font-mono)', color: 'var(--forest)', opacity: 0.55, letterSpacing: '0.1em', marginTop: 3 }}>SBX</div>
      </div>
    </button>
  );
}

// ─── Match detail / summary (+ confirm result) ────────────────────────
function MatchDetailScreen({ go, matchId, profile, from }) {
  const [data, loading] = useMatchDetail(matchId);
  // Where "Return" sends you — back to wherever you opened this from.
  const backTo = from === 'profile' ? { screen: 'profile', scrollTo: 'history' }
    : from === 'myRounds' ? { screen: 'myRounds' }
    : { screen: 'stats' };
  const [busy, setBusy] = React.useState(false);
  const [err, setErr] = React.useState('');
  const [showHoles, setShowHoles] = React.useState(false);
  const cardRef = React.useRef(null);
  const topRef = React.useRef(null);
  const innerRef = React.useRef(null);
  const exitRef = React.useRef(null);
  const meId = profile && profile.id;

  // Scroll so the Exit button sits above the bottom dock after opening.
  function scrollHolesUp() {
    const el = exitRef.current || innerRef.current;
    if (!el) return;
    const sc = el.closest && el.closest('.screen-enter');
    if (!sc) { el.scrollIntoView({ behavior: 'smooth', block: 'end' }); return; }
    const er = el.getBoundingClientRect(), sr = sc.getBoundingClientRect();
    const DOCK = 120; // room for the tab bar + a little breathing space
    const delta = er.bottom - (sr.bottom - DOCK);
    if (delta > 0) sc.scrollBy({ top: delta, behavior: 'smooth' });
  }

  function toggleHoles() {
    setShowHoles(prev => {
      const next = !prev;
      window.setTimeout(() => {
        if (next) scrollHolesUp();
        else if (topRef.current) topRef.current.scrollIntoView({ behavior: 'smooth', block: 'start' });
      }, next ? 320 : 60);
      return next;
    });
  }

  if (loading) return <SppLoader fill/>;
  if (!data) return <div style={{ background: 'var(--canvas)', minHeight: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--forest)' }}>Match not found.</div>;

  const { match: m, holes, teamA, teamB } = data;
  const is2v2 = m.match_type === '2v2';
  const youAreA = m.player_a === meId || m.player_a2 === meId;
  const isParticipant = youAreA || m.player_b === meId || m.player_b2 === meId;
  const decided = m.status === 'completed' || m.result != null;

  // Result from your POV
  let verdict = 'In progress';
  if (m.result === 'H') verdict = 'Halved';
  else if (m.result === 'A') verdict = youAreA ? 'Won' : 'Lost';
  else if (m.result === 'B') verdict = youAreA ? 'Lost' : 'Won';
  const won = verdict === 'Won';

  const holesWon = holes.filter(h => (h.result === 'A' && youAreA) || (h.result === 'B' && !youAreA)).length;
  const holesLost = holes.filter(h => (h.result === 'B' && youAreA) || (h.result === 'A' && !youAreA)).length;
  const holesHalved = holes.filter(h => h.result === 'H').length;

  const mySide = youAreA ? 'a' : 'b';
  const iConfirmed = mySide === 'a' ? m.confirmed_a : m.confirmed_b;
  const theyConfirmed = mySide === 'a' ? m.confirmed_b : m.confirmed_a;
  const bothConfirmed = m.confirmed_a && m.confirmed_b;

  async function confirm() {
    setBusy(true); setErr('');
    try {
      await confirmMatchResult(matchId);
      // Rating + points just recomputed server-side — refresh our profile.
      try { if (typeof window.reloadProfile === 'function') window.reloadProfile(); } catch (_) {}
    } catch (e) { setErr(e.message || 'Could not confirm.'); }
    setBusy(false);
  }

  const teamLabel = (team) => team.map(p => p.first_name || p.handle).join(' + ') || '—';
  const theirLabel = youAreA ? teamLabel(teamB) : teamLabel(teamA);
  const yourLabel = youAreA ? teamLabel(teamA) : teamLabel(teamB);
  const halved = verdict === 'Halved';
  const margin = m.final_margin || '';
  const plain = plainMargin ? plainMargin(margin) : margin;
  const headline = !decided ? 'In progress' : halved ? 'Halved' : won ? `W ${margin}` : `L ${margin}`;
  const summary = !decided ? 'Not finished yet.'
    : halved ? 'All square — matched hole for hole.'
    : won ? `Beat ${theirLabel} · ${plain}` : `${theirLabel} took it · ${plain}`;
  const subline = `${is2v2 ? `${yourLabel} vs ${theirLabel}` : `You vs ${theirLabel}`} · ${m.course_name || 'Sandbox'}`;
  const cells = holes.map(h => {
    const w = (h.result === 'A' && youAreA) || (h.result === 'B' && !youAreA);
    const l = (h.result === 'B' && youAreA) || (h.result === 'A' && !youAreA);
    return { n: h.hole_number, lab: h.result == null ? '' : w ? 'W' : l ? 'L' : 'H' };
  });
  const toAv = (p) => ({ name: p.first_name || p.handle, avatar: p.avatar_url });
  const matchup = {
    yours: (youAreA ? teamA : teamB).map(toAv),
    theirs: (youAreA ? teamB : teamA).map(toAv),
  };

  return (
    <div style={{ background: 'var(--canvas)', minHeight: '100%', display: 'flex', flexDirection: 'column' }}>
      {/* Header */}
      <div ref={topRef} style={{ padding: '50px 16px 6px', display: 'flex', alignItems: 'center' }}>
        <button onClick={() => go(backTo)} style={{
          width: 38, height: 38, borderRadius: 999, background: 'var(--paper)', border: 'var(--hairline)',
          color: 'var(--forest)', display: 'flex', alignItems: 'center', justifyContent: 'center',
        }}><Icon.ArrowLeft size={16}/></button>
      </div>

      {/* 3D result card, centred a little lower */}
      <div style={{ flex: showHoles ? '0 0 auto' : 1, display: 'flex', flexDirection: 'column', justifyContent: 'center', padding: '2% 16px 0' }}>
        <div style={{ maxWidth: 420, width: '100%', margin: '0 auto' }}>
          <ShareResultCard ref={cardRef} headline={headline} summary={summary} subline={subline} cells={cells} totalHoles={holes.length} matchup={matchup}/>

          {/* Still in progress → jump back in */}
          {!decided && isParticipant && (
            <Button variant="forest" full size="lg" onClick={() => go({ screen: 'match', matchId })} style={{ marginTop: 16 }}>
              Continue match <Icon.ArrowRight size={16}/>
            </Button>
          )}

          {/* Confirmation — only while it still needs confirming; once both
              sides agree, the hole-by-hole folder takes this spot. */}
          {isParticipant && decided && !bothConfirmed && (
            <div className="card" style={{ padding: 16, textAlign: 'center', marginTop: 16 }}>
              {iConfirmed ? (
                <>
                  <div style={{ fontSize: 11, fontFamily: 'var(--font-mono)', color: 'var(--forest)', opacity: 0.6, letterSpacing: '0.1em', textTransform: 'uppercase' }}>Confirmed on your side</div>
                  <div className="caption-serif" style={{ fontSize: 14, opacity: 0.75, marginTop: 6 }}>Waiting for the other side to confirm…</div>
                </>
              ) : (
                <>
                  <div style={{ fontFamily: 'var(--font-display)', fontSize: 18, color: 'var(--forest)' }}>Confirm the result</div>
                  <div className="caption-serif" style={{ fontSize: 13, opacity: 0.7, marginTop: 4, marginBottom: 12, lineHeight: 1.5 }}>
                    Both {is2v2 ? 'teams' : 'players'} confirm before it counts toward SBX.{theyConfirmed ? ' The other side already confirmed.' : ''}
                  </div>
                  {err && <div style={{ fontSize: 12, color: 'var(--loss, #C44536)', marginBottom: 8 }}>{err}</div>}
                  <Button variant="forest" full onClick={confirm} disabled={busy}>{busy ? 'Confirming…' : 'Confirm result'}</Button>
                </>
              )}
            </div>
          )}

          {/* Actions */}
          <div style={{ display: 'flex', flexDirection: 'column', gap: 10, marginTop: 16 }}>
            {decided && bothConfirmed && (
              <button onClick={() => shareCardImage(cardRef.current, { youWon: won, halved, margin, theirLabel })}
                style={{ width: '100%', padding: 15, borderRadius: 14, border: 'none', cursor: 'pointer', background: 'var(--forest)', color: 'var(--cream)', fontWeight: 800, fontSize: 14 }}>
                Share scorecard
              </button>
            )}
            <button onClick={toggleHoles}
              style={{ width: '100%', padding: 15, borderRadius: 14, cursor: 'pointer', background: 'transparent', border: '1px solid var(--forest)', color: 'var(--forest)', fontWeight: 800, fontSize: 14, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6 }}>
              {showHoles ? 'Hide hole details' : 'Hole by hole details'}
              <span style={{ transition: 'transform 0.3s ease', transform: showHoles ? 'rotate(180deg)' : 'none', display: 'inline-flex' }}><Icon.Chevron dir="down" size={14} color="var(--forest)"/></span>
            </button>
            {bothConfirmed && (
              <div style={{ fontSize: 10, fontFamily: 'var(--font-mono)', color: 'var(--forest)', opacity: 0.55, textAlign: 'center', letterSpacing: '0.02em', whiteSpace: 'nowrap' }}>
                ✓ Confirmed by both sides · counts toward SBX
              </div>
            )}

            {/* Gooey folder: one tab per hole, full stroke-by-stroke detail */}
            {showHoles && (
              <div ref={innerRef} className="step-reveal" style={{ paddingTop: 4 }}>
                <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 10, marginBottom: 14 }}>
                  <Stat label="Holes won" value={holesWon}/>
                  <Stat label="Lost" value={holesLost}/>
                  <Stat label="Halved" value={holesHalved}/>
                </div>
                <HoleFolder holes={holes} youAreA={youAreA} is2v2={is2v2} byId={data.byId}
                  playerStatsByHole={data.playerStatsByHole} meId={meId} isParticipant={isParticipant}
                  teamA={teamA} teamB={teamB}/>
              </div>
            )}

            {/* Exit sits below whatever's expanded */}
            <button ref={exitRef} onClick={() => go(backTo)}
              style={{ width: '100%', padding: 13, borderRadius: 14, cursor: 'pointer', background: 'transparent', border: 'none', color: 'var(--forest)', opacity: 0.6, fontWeight: 800, fontSize: 13 }}>
              Return
            </button>
          </div>
        </div>
      </div>
    </div>
  );
}

// ─── Gooey hole-by-hole folder ────────────────────────────────────────
// Manila-folder UI: one tab per hole, the active tab "melts" into the panel
// via an SVG gooey filter. Panel shows the full stroke-by-stroke shot log
// (every player's outcome per stroke, whose ball the team took, caddie
// suggestion, putt rounds) when recorded; older matches fall back to the
// per-hole summary. 2v2 gets Only-your-shots / Team-shots pills.
function HoleFolder({ holes, youAreA, is2v2, byId, playerStatsByHole, meId, isParticipant, teamA, teamB, focusHole }) {
  const [active, setActive] = React.useState(focusHole || 0);
  const [scope, setScope] = React.useState('mine'); // mine | team
  // 18-hole rounds page the tab strip by nine — a Front 9/Back 9 toggle
  // switches pages, and opening lands on whichever nine holds the hole.
  const many = holes.length > 9;
  const [nine, setNine] = React.useState(() => (many && (focusHole || 0) >= 9 ? 'back' : 'front'));
  // The caller can steer which hole is open (e.g. tapping H12 on the wallet
  // card opens the folder straight to hole 12, on the back nine).
  React.useEffect(() => {
    if (focusHole != null && focusHole >= 0 && focusHole < (holes.length || 1)) {
      setActive(focusHole);
      if (many) setNine(focusHole >= 9 ? 'back' : 'front');
    }
  }, [focusHole]);
  function switchNine(k) {
    if (k === nine) return;
    setNine(k);
    // Keep the same relative hole on the other nine (H3 ↔ H12).
    setActive(o => Math.min((o % 9) + (k === 'back' ? 9 : 0), holes.length - 1));
  }
  const TAB_H = 38;
  const n = holes.length || 1;
  const nineOffset = many && nine === 'back' ? 9 : 0;
  const visibleHoles = many ? holes.slice(nineOffset, nineOffset + 9) : holes;
  const vn = visibleHoles.length || 1;
  const activeLocal = Math.min(Math.max(active - nineOffset, 0), vn - 1);
  const h = holes[Math.min(active, n - 1)] || {};
  const showPills = is2v2 && isParticipant;
  const teamScope = !showPills || scope === 'team';

  const name = (id) => { const p = byId[id]; return p ? (p.first_name || formatHandle(p.handle)) : 'Player'; };
  const isMe = (id) => id === meId;
  const FW = { hit: 'fairway hit', left: 'missed the fairway left', right: 'missed the fairway right', long: 'long of the fairway', short: 'short of the fairway' };

  const w = (h.result === 'A' && youAreA) || (h.result === 'B' && !youAreA);
  const l = (h.result === 'B' && youAreA) || (h.result === 'A' && !youAreA);
  const verdict = h.result == null ? 'Not played' : w ? 'Won' : l ? 'Lost' : 'Halved';
  const yourScore = youAreA ? h.player_a_score : h.player_b_score;
  const oppScore  = youAreA ? h.player_b_score : h.player_a_score;
  const log = (youAreA ? h.shot_log_a : h.shot_log_b) || null;
  const pStats = (playerStatsByHole || {})[h.hole_number] || [];

  // Which players' lines to show inside an event, honouring the pill scope.
  const visible = (ids) => teamScope ? ids : ids.filter(isMe);

  const cream = 'var(--cream)';
  const dim = { fontSize: 11, fontFamily: 'var(--font-mono)', letterSpacing: '0.1em', textTransform: 'uppercase', opacity: 0.6 };
  const line = { fontSize: 13, lineHeight: 1.45 };

  function ShotEvent({ ev }) {
    const ids = visible(Object.keys(ev.outcomes || {}));
    const pickedName = ev.picked ? name(ev.picked) : null;
    return (
      <div style={{ padding: '10px 0', borderBottom: '1px solid rgba(234,226,206,0.14)' }}>
        <div style={dim}>Shot {ev.shot}</div>
        {ids.map(pid => {
          const o = ev.outcomes[pid] || {};
          const bits = [];
          if (o.fairway) bits.push(FW[o.fairway] || o.fairway);
          if (o.reached === 'holed') bits.push('holed out!');
          else if (o.reached === true) bits.push(`on the green${o.zone ? ` · ${o.zone}` : ''}`);
          else if (o.reached === false) bits.push('missed the green');
          if (o.ob) bits.push('OB / penalty');
          return (
            <div key={pid} style={{ ...line, marginTop: 5 }}>
              <b>{name(pid)}{isMe(pid) ? ' (you)' : ''}</b> — {bits.length ? bits.join(' · ') : 'logged'}
            </div>
          );
        })}
        {pickedName && (
          <div style={{ ...line, marginTop: 6, opacity: 0.85 }}>
            → Team took <b>{pickedName}{isMe(ev.picked) ? ' (you)' : ''}</b>'s ball
            {ev.suggested ? (ev.suggested === ev.picked ? ' · caddie agreed' : ` · caddie liked ${name(ev.suggested)}'s`) : ''}
          </div>
        )}
      </div>
    );
  }

  function PuttEvent({ ev }) {
    const all = Object.keys(ev.results || {});
    const ids = visible(all);
    // In "only your shots", still surface a partner's make — that's the story of the hole.
    const partnerMade = !teamScope ? all.find(pid => !isMe(pid) && ev.results[pid] === 'made') : null;
    return (
      <div style={{ padding: '10px 0', borderBottom: '1px solid rgba(234,226,206,0.14)' }}>
        <div style={dim}>Putt · shot {ev.shot}{ev.round > 1 ? ` · round ${ev.round}` : ''}</div>
        {ids.map(pid => (
          <div key={pid} style={{ ...line, marginTop: 5 }}>
            <b>{name(pid)}{isMe(pid) ? ' (you)' : ''}</b> — {ev.results[pid] === 'made' ? 'made the putt' : 'missed'}
          </div>
        ))}
        {partnerMade && (
          <div style={{ ...line, marginTop: 5, opacity: 0.85 }}>
            <b>{name(partnerMade)}</b> (partner) made the putt
          </div>
        )}
      </div>
    );
  }

  // Older matches (no stroke log) — the per-hole summary we do have.
  // Reads YOUR side's columns; the legacy shared columns (pre-migration) are
  // trusted only when the recorded player is actually on your team, since the
  // other side's write could have overwritten yours.
  function SummaryFallback() {
    const myTeamIds = ((youAreA ? teamA : teamB) || []).map(p => p.id);
    const onMyTeam = (id) => id != null && myTeamIds.includes(id);
    const ball  = (youAreA ? h.ball_player_a : h.ball_player_b) || (onMyTeam(h.ball_player) ? h.ball_player : null);
    const holed = (youAreA ? h.holed_by_a : h.holed_by_b) || (onMyTeam(h.holed_by) ? h.holed_by : null);
    const zone  = (youAreA ? h.zone_a : h.zone_b) || (ball ? h.zone : null);
    const rows = [];
    if (is2v2) {
      if (ball)  rows.push(['Ball played', `${name(ball)}${isMe(ball) ? ' (you)' : ''}`]);
      if (holed) rows.push(['Holed by', `${name(holed)}${isMe(holed) ? ' (you)' : ''}`]);
      if (zone)  rows.push(['Ball position', zone]);
    } else {
      const fair = youAreA ? h.player_a_fairway : h.player_b_fairway;
      const gir  = youAreA ? h.player_a_gir : h.player_b_gir;
      const putts = youAreA ? h.player_a_putts : h.player_b_putts;
      if (fair) rows.push(['Fairway', FW[fair] || fair]);
      if (gir != null) rows.push(['Green in reg', gir ? 'Yes' : 'No']);
      if (putts != null) rows.push(['Putts', putts]);
    }
    const stats = visible(pStats.map(s => s.player_id)).map(pid => pStats.find(s => s.player_id === pid)).filter(Boolean);
    return (
      <div>
        {rows.map(([k, v]) => (
          <div key={k} style={{ display: 'flex', justifyContent: 'space-between', padding: '9px 0', borderBottom: '1px solid rgba(234,226,206,0.14)' }}>
            <span style={dim}>{k}</span><span style={{ fontSize: 13, fontWeight: 700 }}>{v}</span>
          </div>
        ))}
        {is2v2 && stats.map(s => {
          const bits = [];
          if (s.fairway) bits.push(FW[s.fairway] || s.fairway);
          if (s.on_green === true || s.gir === true) bits.push('reached the green');
          else if (s.on_green === false) bits.push('missed the green');
          if (s.ob) bits.push('OB');
          if (s.zone) bits.push(s.zone);
          return (
            <div key={s.player_id} style={{ ...line, padding: '9px 0', borderBottom: '1px solid rgba(234,226,206,0.14)' }}>
              <b>{name(s.player_id)}{isMe(s.player_id) ? ' (you)' : ''}</b> — {bits.length ? bits.join(' · ') : 'no detail logged'}
            </div>
          );
        })}
        {rows.length === 0 && stats.length === 0 && (
          <div style={{ ...line, opacity: 0.7, padding: '9px 0' }}>No shot detail was recorded for this hole.</div>
        )}
      </div>
    );
  }

  const pill = (on) => ({
    flex: 1, padding: '8px 10px', borderRadius: 999, fontSize: 12, fontWeight: 800, cursor: 'pointer',
    background: on ? cream : 'rgba(234,226,206,0.1)', color: on ? 'var(--forest)' : cream,
    border: on ? 'none' : '1px solid rgba(234,226,206,0.24)',
  });

  return (
    <div style={{ position: 'relative', marginTop: 4 }}>
      {/* Gooey filter (blur + contrast melts the tab into the panel) */}
      <svg width="0" height="0" style={{ position: 'absolute' }} aria-hidden="true">
        <defs>
          <filter id="spp-goo">
            <feGaussianBlur in="SourceGraphic" stdDeviation="7" result="blur"/>
            <feColorMatrix in="blur" mode="matrix" values="1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 19 -9" result="goo"/>
            <feComposite in="SourceGraphic" in2="goo" operator="atop"/>
          </filter>
        </defs>
      </svg>

      {/* Filtered background: sliding tab bump + folder body, merged gooey */}
      <div style={{ position: 'absolute', inset: 0, filter: 'url(#spp-goo)', pointerEvents: 'none' }}>
        <div style={{
          position: 'absolute', top: 0, height: TAB_H + 12,
          left: `${activeLocal * (100 / vn)}%`, width: `${100 / vn}%`,
          background: 'var(--forest)', borderRadius: '12px 12px 0 0',
          transition: 'left 0.4s cubic-bezier(0.3, 0.9, 0.3, 1)',
        }}/>
        <div style={{ position: 'absolute', top: TAB_H, left: 0, right: 0, bottom: 0, background: 'var(--forest)', borderRadius: 20 }}/>
      </div>

      {/* Crisp layer: tab hit-targets + panel content */}
      <div style={{ position: 'relative' }}>
        <div style={{ display: 'flex' }}>
          {visibleHoles.map((hh, i) => {
            const gi = nineOffset + i;
            return (
              <button key={hh.hole_number} onClick={() => setActive(gi)} style={{
                flex: 1, height: TAB_H, background: 'transparent', border: 'none', cursor: 'pointer',
                fontFamily: 'var(--font-mono)', fontSize: 12, fontWeight: 800,
                color: gi === active ? cream : 'var(--forest)', opacity: gi === active ? 1 : 0.6,
                transition: 'color 0.25s',
              }}>{hh.hole_number}</button>
            );
          })}
        </div>

        <div key={`${active}-${scope}`} className="step-reveal" style={{ padding: '16px 18px 18px', color: cream }}>
          {many && (
            <div style={{ display: 'flex', gap: 8, marginBottom: 12 }}>
              <button onClick={() => switchNine('front')} style={pill(nine === 'front')}>Front 9</button>
              <button onClick={() => switchNine('back')} style={pill(nine === 'back')}>Back 9</button>
            </div>
          )}
          <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between' }}>
            <div style={{ fontFamily: 'var(--font-display)', fontSize: 24 }}>Hole {h.hole_number}</div>
            <div style={{ fontFamily: 'var(--font-display)', fontSize: 16, opacity: 0.9 }}>{verdict}</div>
          </div>
          <div style={{ ...dim, marginTop: 4 }}>
            Par {h.par || 3}{h.distance_yards != null ? ` · ${h.distance_yards}y` : ''} · You {yourScore != null ? yourScore : '—'} — Them {oppScore != null ? oppScore : '—'}
          </div>

          {showPills && (
            <div style={{ display: 'flex', gap: 8, marginTop: 14 }}>
              <button onClick={() => setScope('mine')} style={pill(scope === 'mine')}>Only your shots</button>
              <button onClick={() => setScope('team')} style={pill(scope === 'team')}>Team shots</button>
            </div>
          )}

          <div style={{ marginTop: 8 }}>
            {log && log.length
              ? log.map((ev, i) => ev.phase === 'putt' ? <PuttEvent key={i} ev={ev}/> : <ShotEvent key={i} ev={ev}/>)
              : <SummaryFallback/>}
          </div>
        </div>
      </div>
    </div>
  );
}

Object.assign(window, { BookScreen, CourseDetailScreen, MyRoundsScreen, MatchupScreen, MatchDetailScreen, HoleFolder, DualRangeSlider });
