/* global React, Icon, LiveDot, SppMark, Button, Eyebrow, Chip, Dashed, Ostrich, Wordmark, Lockup, ScoreDial, Spark, MOCK, sbx, ShareResultCard, plainMargin, useLiveEvent, useNextEventForUser, useNextMajor, useUpcomingEvents, useActiveMatchForUser, useMyPendingInvites, useMyPendingEventInvites, useFriendFeed, useNewFollowers, useNotifications, useMyBookings, useMyTeeAssignments, dismissTeeAssignment, startBookedMatch, useFriendsRegisteredForEvents, formatHandle, acceptInvite, declineInvite, useMyWaitlist, useCourses, usePoolBlocks, useHotPools, sharePool, poolShareUrl, blockLabel, useMyGroups, PendingGroupCard, useTeeingUp, useLiveNow, useMyRivalries, useWeeklyRecap */
// Home screen — next event, live leaderboard, activity

// Most-recent completed match → the shareable card pieces (null=loading, false=none).
function useLastMatchCard(userId) {
  const [state, setState] = React.useState(null);
  React.useEffect(() => {
    if (!userId) { setState(false); return undefined; }
    let on = true;
    (async () => {
      const { data: ms } = await sbx.from('matches').select('*')
        .or(`player_a.eq.${userId},player_a2.eq.${userId},player_b.eq.${userId},player_b2.eq.${userId}`)
        .eq('status', 'completed').order('completed_at', { ascending: false }).limit(1);
      const m = ms && ms[0];
      if (!m) { if (on) setState(false); return; }
      const { data: holes } = await sbx.from('match_holes').select('hole_number, result').eq('match_id', m.id).order('hole_number');
      const ids = [m.player_a, m.player_a2, m.player_b, m.player_b2].filter(Boolean);
      const { data: ps } = await sbx.from('profiles').select('id, first_name, handle, avatar_url').in('id', ids);
      const byId = {}; (ps || []).forEach(p => { byId[p.id] = p; });
      const nm = id => { const p = byId[id]; return p ? (p.first_name || p.handle) : 'Player'; };
      const av = id => { const p = byId[id]; return { name: nm(id), avatar: p && p.avatar_url }; };
      const youAreA = m.player_a === userId || m.player_a2 === userId;
      const is2v2 = m.match_type === '2v2';
      const teamA = [m.player_a, m.player_a2].filter(Boolean).map(nm).join(' + ');
      const teamB = [m.player_b, m.player_b2].filter(Boolean).map(nm).join(' + ');
      const theirLabel = youAreA ? teamB : teamA;
      const yourLabel = youAreA ? teamA : teamB;
      const won = (m.result === 'A' && youAreA) || (m.result === 'B' && !youAreA);
      const halved = m.result === 'H';
      const margin = m.final_margin || '';
      const plain = plainMargin ? plainMargin(margin) : margin;
      const headline = halved ? 'Halved' : won ? `W ${margin}` : `L ${margin}`;
      const summary = 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 teamAids = [m.player_a, m.player_a2].filter(Boolean);
      const teamBids = [m.player_b, m.player_b2].filter(Boolean);
      const mu = {
        yours: (youAreA ? teamAids : teamBids).map(av),
        theirs: (youAreA ? teamBids : teamAids).map(av),
      };
      if (on) setState({ matchId: m.id, headline, summary, subline, cells, totalHoles: m.total_holes, matchup: mu, completedAt: m.completed_at || m.created_at });
    })();
    return () => { on = false; };
  }, [userId]);
  return state;
}
// "Your most recent match" / "Your last match was 3 days ago" — plain-English
// distance from a completed_at timestamp. Same calendar day → "most recent";
// then yesterday → a couple of days → N days → weeks → months (+ days) →
// years (+ months).
function lastMatchAgoLine(dateStr) {
  if (!dateStr) return 'Your last match';
  const then = new Date(dateStr);
  const now = new Date();
  if (isNaN(then.getTime())) return 'Your last match';
  const sameDay = then.getFullYear() === now.getFullYear() && then.getMonth() === now.getMonth() && then.getDate() === now.getDate();
  if (sameDay) return 'Your most recent match';
  const days = Math.max(1, Math.floor((now - then) / 86400000));
  let when;
  if (days === 1) when = 'yesterday';
  else if (days === 2) when = 'a couple of days ago';
  else if (days < 7) when = `${days} days ago`;
  else if (days < 30) { const wk = Math.floor(days / 7); when = wk === 1 ? 'a week ago' : `${wk} weeks ago`; }
  else if (days < 365) {
    const mo = Math.floor(days / 30); const rem = days - mo * 30;
    when = `${mo} month${mo > 1 ? 's' : ''}${rem >= 7 ? `, ${Math.floor(rem / 7)} week${Math.floor(rem / 7) > 1 ? 's' : ''}` : ''} ago`;
  } else {
    const yr = Math.floor(days / 365); const mo = Math.floor((days - yr * 365) / 30);
    when = `${yr} year${yr > 1 ? 's' : ''}${mo > 0 ? `, ${mo} month${mo > 1 ? 's' : ''}` : ''} ago`;
  }
  return `Your last match was ${when}`;
}

// "Wednesday, July 1st 2026" — today's date for the Home header.
function todayLine() {
  const d = new Date();
  const day = d.getDate();
  const suffix = (day % 10 === 1 && day !== 11) ? 'st' : (day % 10 === 2 && day !== 12) ? 'nd' : (day % 10 === 3 && day !== 13) ? 'rd' : 'th';
  const weekday = d.toLocaleDateString('en-US', { weekday: 'long' });
  const month = d.toLocaleDateString('en-US', { month: 'long' });
  return `${weekday}, ${month} ${day}${suffix} ${d.getFullYear()}`;
}

// Real "City, ST" from device location (reverse-geocoded, cached for a day).
// Falls back to Miami, FL when permission is denied or lookup fails.
function useGeoCityState() {
  const [loc, setLoc] = React.useState(() => {
    try {
      const c = JSON.parse(localStorage.getItem('spp_geo_city') || 'null');
      if (c && c.text && Date.now() - c.at < 86400000) return c.text;
    } catch (_) {}
    return null;
  });
  React.useEffect(() => {
    if (loc || !navigator.geolocation) return;
    navigator.geolocation.getCurrentPosition(async (pos) => {
      try {
        const { latitude, longitude } = pos.coords;
        const res = await fetch(`https://api.bigdatacloud.net/data/reverse-geocode-client?latitude=${latitude}&longitude=${longitude}&localityLanguage=en`);
        const j = await res.json();
        const city = j.city || j.locality;
        const st = (j.principalSubdivisionCode || '').split('-')[1] || j.principalSubdivision;
        if (city && st) {
          const text = `${city}, ${st}`;
          setLoc(text);
          try { localStorage.setItem('spp_geo_city', JSON.stringify({ text, at: Date.now() })); } catch (_) {}
        }
      } catch (_) {}
    }, () => {}, { maximumAge: 3600000, timeout: 8000 });
  }, [loc]);
  return loc || 'Miami, FL';
}

// Reads events from Supabase via the hooks in events-data.jsx and the
// signed-in user's active match via live-data.jsx. Tweaks-panel
// `liveMode=true` still falls back to a mock live event for design-
// preview when nothing real is live in the DB.

function HomeScreen({ go, tier, brandLoud, liveMode, mascot, profile }) {
  const isMember = tier === 'league' || tier === 'plus';
  const greetingName = (profile && profile.first_name) || MOCK.USER.name.split(' ')[0];

  // Real data
  const [liveEvent]            = useLiveEvent();
  const [myNextEvent, nextLoading, myNextIsRegistered] = useNextEventForUser(profile && profile.id);
  const [major]                = useNextMajor();
  const [upcoming, upcomingLoading] = useUpcomingEvents(4);
  const [activeMatch]          = useActiveMatchForUser(profile && profile.id);
  const [pendingInvites]       = useMyPendingInvites(profile && profile.id);
  const [pendingEventInvites]  = useMyPendingEventInvites(profile && profile.id);
  const [feed, feedLoading, followCount] = useFriendFeed(profile && profile.id, 12);
  const [newFollowers]         = useNewFollowers(profile && profile.id);
  const [notifs]               = useNotifications(profile && profile.id);
  const [upcomingBookings]     = useMyBookings(profile && profile.id);
  const nextBooking            = (upcomingBookings && upcomingBookings[0]) || null;
  const [assignments, refreshAssignments] = useMyTeeAssignments(profile && profile.id);
  const [myWaitlist]           = useMyWaitlist(profile && profile.id);
  const [myGroups, reloadGroups] = useMyGroups(profile && profile.id);
  const [allCourses]           = useCourses();
  const [hotPools]             = useHotPools(allCourses);
  const [teeingUp]             = useTeeingUp(profile && profile.id);
  const liveNow                = useLiveNow(profile && profile.id);
  const rivals                 = useMyRivalries(profile && profile.id);
  const recap                  = useWeeklyRecap(profile && profile.id);
  const geoCity                = useGeoCityState();
  // Only count items the user hasn't seen yet (seen IDs written by NotificationsScreen)
  const seenNotifIds = React.useMemo(() => {
    try { return new Set(JSON.parse(localStorage.getItem('spp_seen_notifs') || '[]')); }
    catch { return new Set(); }
  }, []);
  const notificationCount = [
    ...(pendingInvites      || []).map(i => `inv-${i.id}`),
    ...(newFollowers        || []).map(f => `fol-${f.follower_id}-${f.created_at}`),
    ...(pendingEventInvites || []).map(e => `ei-${e.id}`),
    ...(notifs              || []).map(n => `gn-${n.id}`),
  ].filter(id => !seenNotifIds.has(id)).length;

  // Friends-here pills on the Up Next teaser cards.
  const upcomingIds = React.useMemo(
    () => (upcoming || []).slice(0, 4).map(e => e.id),
    [upcoming]
  );
  const [friendsByEvent] = useFriendsRegisteredForEvents(profile && profile.id, upcomingIds);

  // Up Next card priority:
  //   1. User has an active match → show the live preview with real
  //      match data. We synthesize an "event"-shaped object with
  //      status='live' so the existing NextUpCard live branch fires.
  //   2. There's a live event in the DB → use that.
  //   3. User's next registered/open event.
  //   4. Tweaks design-preview fallback when liveMode is on.
  // The active match is already surfaced by its own tee-time card (UpcomingTeeCard
  // in its in-progress state), so don't also render the separate live "Next up"
  // card for it — that's the duplicate. Challenge matches (no tee assignment)
  // still fall through to the live card below.
  const coveredByAssignment = !!activeMatch && (assignments || []).some(
    a => a.matchId === activeMatch.id && a.status !== 'cut' && !a.matchDone
  );
  const liveActiveMatch = activeMatch && !coveredByAssignment ? activeMatch : null;

  // NextRoundStatus ("Scout the matchup") duplicates the UpcomingTeeCard when a
  // tee assignment already covers the same match/slot — suppress it then.
  const nextBookingCovered = !!nextBooking && (assignments || []).some(a =>
    a.status !== 'cut' && !a.matchDone &&
    ((nextBooking.match_id && a.matchId === nextBooking.match_id) || a.slotId === nextBooking.slot_id)
  );

  let nextEvent = null;
  if (liveActiveMatch) {
    nextEvent = {
      id: liveActiveMatch.id,
      status: 'live',
      courseShort: liveActiveMatch.courseName || 'Live Match',
      tagline: 'Live now',
      img: '',
      // The card's "Live" branch reads only courseShort + status; the
      // real match content is rendered inside LiveInlinePreview which
      // we now feed via the activeMatch prop.
    };
  } else {
    nextEvent = liveEvent || myNextEvent;
    if (!nextEvent && liveMode) {
      nextEvent = MOCK.EVENTS.find(e => e.status === 'live') || null;
    }
  }
  // True only when the displayed event is one the user already signed up for.
  const nextEventIsRegistered = !liveActiveMatch && !liveEvent && myNextIsRegistered;

  // Upcoming-teaser strip: skip whatever's already shown as Up Next.
  const upcomingForTeaser = upcoming
    .filter(e => !nextEvent || e.id !== nextEvent.id)
    .slice(0, 3);

  return (
    <div style={{ background: 'var(--canvas)', minHeight: '100%', paddingBottom: 120 }}>
      {/* Top brand bar — white/editorial */}
      <div style={{
        padding: '58px 20px 20px',
        display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between',
        background: 'var(--canvas)',
        color: 'var(--ink)',
        position: 'relative',
      }}>
        <div style={{ position: 'relative' }}>
          <div style={{ fontSize: 11, fontFamily: 'var(--font-mono)', color: 'var(--forest)', opacity: 0.55, letterSpacing: '0.08em', textTransform: 'uppercase' }}>
            {geoCity} · {todayLine()}
          </div>
          <div style={{ fontFamily: 'var(--font-display)', fontSize: 40, lineHeight: 0.92, marginTop: 8, letterSpacing: '-0.02em', color: 'var(--forest)' }}>
            Hey, {greetingName}.
          </div>
          <div className="caption-serif" style={{ fontSize: 16, opacity: 0.65, marginTop: 4, color: 'var(--forest)' }}>
            The birds are restless.
          </div>
        </div>
        <button
          onClick={() => go({ screen: 'notifications' })}
          aria-label={`Notifications${notificationCount ? ` (${notificationCount} unread)` : ''}`}
          style={{
            width: 42, height: 42, borderRadius: 999,
            background: 'var(--paper)',
            border: 'var(--hairline)',
            boxShadow: 'var(--shadow-sm)',
            display: 'flex', alignItems: 'center', justifyContent: 'center',
            position: 'relative',
            color: 'var(--forest)',
            flexShrink: 0, marginTop: 4,
          }}
        >
          <svg width="16" height="18" viewBox="0 0 24 24" fill="none">
            <path d="M12 2a6 6 0 0 0-6 6v4l-2 3h16l-2-3V8a6 6 0 0 0-6-6z" stroke="currentColor" strokeWidth="2" strokeLinejoin="round"/>
            <path d="M10 19a2 2 0 0 0 4 0" stroke="currentColor" strokeWidth="2"/>
          </svg>
          {notificationCount > 0 && (
            <span style={{ position: 'absolute', top: 9, right: 10, width: 8, height: 8, borderRadius: 999, background: 'var(--forest)', border: '1.5px solid var(--paper)' }}/>
          )}
        </button>
      </div>

      {/* Waitlist → booked: your upcoming tee time (and cut notices) */}
      {(assignments || []).filter(a => a.status !== 'cut' && !a.matchDone).map(a => (
        <div key={a.id} style={{ padding: '16px 16px 0' }}>
          <UpcomingTeeCard a={a} go={go} activeMatch={activeMatch && activeMatch.id === a.matchId ? activeMatch : null}/>
        </div>
      ))}
      {(assignments || []).filter(a => a.status === 'cut').map(a => (
        <div key={a.id} style={{ padding: '16px 16px 0' }}><CutCard a={a} onDismiss={refreshAssignments}/></div>
      ))}

      {/* Pending group rounds — invites to answer + groups you're forming */}
      {(myGroups || []).map(g => (
        <div key={g.id} style={{ padding: '16px 16px 0' }}>
          <PendingGroupCard group={g} profile={profile} go={go} onChanged={reloadGroups}/>
        </div>
      ))}

      {/* Your pool — you're on a window waitlist; make the wait legible + shareable */}
      {myWaitlist && myWaitlist.length > 0 && (
        <div style={{ padding: '16px 16px 0' }}>
          <YourPoolCard w={myWaitlist[0]} profile={profile} go={go}/>
        </div>
      )}

      {/* Priority — an active match or an upcoming booking sits at the very top */}
      {nextBooking && !nextBookingCovered && <NextRoundStatus booking={nextBooking} go={go}/>}
      {liveActiveMatch && (
        <div style={{ padding: '16px 16px 0' }}>
          <NextUpCard event={nextEvent} go={go} isMember={isMember} liveMode={liveMode} brandLoud={brandLoud} mascot={mascot} activeMatch={liveActiveMatch} isRegistered={false}/>
        </div>
      )}

      {/* Match invite banners — pending invites the signed-in user got */}
      {pendingInvites && pendingInvites.length > 0 && (
        <div style={{ padding: '16px 16px 0', display: 'flex', flexDirection: 'column', gap: 10 }}>
          {pendingInvites.map(inv => (
            <InviteBanner key={inv.id} invite={inv} profile={profile} go={go}/>
          ))}
        </div>
      )}

      {/* Book your next match → Play */}
      <div style={{ padding: '18px 16px 0' }}>
        <button onClick={() => go({ screen: 'events' })} style={{
          width: '100%', textAlign: 'left', border: 'none',
          borderRadius: 'var(--radius-card-lg)', overflow: 'hidden',
          background: 'linear-gradient(135deg, var(--forest-dark) 0%, var(--forest) 55%, var(--moss) 100%)',
          color: 'var(--cream)', padding: 22, position: 'relative',
          boxShadow: 'var(--shadow-md)', display: 'block',
        }}>
          <div className="grain" style={{ position: 'absolute', inset: 0, pointerEvents: 'none' }}/>
          {/* Flamingo mascot accent — cropped from the wing up (no legs),
              vertically centred in the space to the right of the text */}
          <div style={{
            position: 'absolute', right: 14, top: 0, bottom: 0, width: 120,
            display: 'flex', alignItems: 'center', pointerEvents: 'none',
          }}>
            <div style={{ width: 120, height: 122, overflow: 'hidden', position: 'relative' }}>
              <img src="assets/mascot-full-cream.svg" alt="" style={{
                position: 'absolute', top: 0, left: 0, width: 120, opacity: 0.18,
              }}/>
            </div>
          </div>
          <div style={{ position: 'relative' }}>
            <div style={{ fontFamily: 'var(--font-display)', fontSize: 22, lineHeight: 1, letterSpacing: '-0.02em', whiteSpace: 'nowrap' }}>Book your next match</div>
            <div style={{ fontSize: 13, opacity: 0.85, marginTop: 8, maxWidth: '68%' }}>Challenge a friend or get matched — nine holes.</div>
          </div>
        </button>
      </div>

      {/* Match pools near you — live demand, hot pools first */}
      {hotPools && hotPools.length > 0 && (
        <div style={{ padding: '28px 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' }}>
            Match pools near you
          </div>
          <div style={{ display: 'flex', gap: 10, overflowX: 'auto', padding: '0 16px 2px' }} className="scroll-hide">
            {hotPools.map(pl => (
              <HotPoolCard key={`${pl.course.id}-${pl.dateStr}-${pl.blockHour}`} pool={pl} go={go}/>
            ))}
          </div>
        </div>
      )}

      {/* Teeing up — mutuals' pools, open seats, and locked times.
          Identity exposure is enforced server-side (teeing_up RPC). */}
      {teeingUp && teeingUp.length > 0 && (
        <div style={{ padding: '28px 16px 0' }}>
          <div style={{ fontSize: 11, fontFamily: 'var(--font-mono)', color: 'var(--forest)', opacity: 0.55, letterSpacing: '0.12em', textTransform: 'uppercase', marginBottom: 12, padding: '0 4px' }}>
            Teeing up
          </div>
          <div className="card" style={{ padding: '6px 4px' }}>
            {teeingUp.map((t, i) => (
              <TeeingUpRow key={`${t.kind}-${t.userId}-${t.dateStr}-${i}`} t={t} last={i === teeingUp.length - 1} go={go}/>
            ))}
          </div>
        </div>
      )}

      {/* Live now — counts for everyone, named cards for mutuals (20s poll) */}
      {liveNow && (liveNow.aggregates.length > 0 || liveNow.cards.length > 0) && (
        <div style={{ padding: '28px 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' }}>
            Live now
          </div>
          {liveNow.aggregates.length > 0 && (
            <div style={{ display: 'flex', gap: 8, overflowX: 'auto', padding: '0 16px 10px' }} className="scroll-hide">
              {liveNow.aggregates.map(a => (
                <span key={a.course} style={{
                  flexShrink: 0, fontSize: 11, fontWeight: 700, color: 'var(--forest)',
                  padding: '6px 12px', borderRadius: 999, background: 'rgba(28,73,42,0.07)', border: '1px solid rgba(28,73,42,0.16)',
                  display: 'inline-flex', alignItems: 'center', gap: 6,
                }}>
                  <LiveDot/> {a.n} live at {a.course}
                </span>
              ))}
            </div>
          )}
          {liveNow.cards.length > 0 && (
            <div style={{ display: 'flex', gap: 10, overflowX: 'auto', padding: '0 16px 2px' }} className="scroll-hide">
              {liveNow.cards.map(c => <LiveNowCard key={c.matchId + c.handle} c={c}/>)}
            </div>
          )}
        </div>
      )}

      {/* Major banner — only shown when there's an upcoming Major in the DB */}
      {major && (
        <div style={{ padding: '16px 16px 0' }}>
          <button onClick={() => go({ screen: 'eventDetail', eventId: major.id })} style={{
            width: '100%', textAlign: 'left',
            borderRadius: 'var(--radius-card-lg)', overflow: 'hidden',
            background: `linear-gradient(135deg, var(--forest-dark) 0%, var(--forest) 45%, var(--moss) 100%)`,
            color: 'var(--cream)',
            padding: 22,
            position: 'relative',
            border: 'none',
            display: 'block',
            boxShadow: 'var(--shadow-md)',
          }}>
            <div style={{ position: 'absolute', right: -24, top: -24, opacity: 0.14 }}>
              <img src="assets/mascot-full-cream.svg" alt="" style={{ width: 180, transform: 'rotate(14deg)' }}/>
            </div>
            <div className="grain" style={{ position: 'absolute', inset: 0, pointerEvents: 'none' }}/>
            <div style={{ position: 'relative' }}>
              <div style={{
                display: 'inline-block',
                padding: '5px 10px', borderRadius: 999,
                background: 'rgba(234,226,206,0.12)', backdropFilter: 'blur(10px)',
                border: '1px solid rgba(234,226,206,0.22)',
                fontSize: 10, fontWeight: 800, letterSpacing: '0.16em', textTransform: 'uppercase',
              }}>⛳ Major</div>
              <div style={{ fontFamily: 'var(--font-display)', fontSize: 34, lineHeight: 0.9, marginTop: 12, letterSpacing: '-0.02em' }}>
                {(major.tagline || major.courseShort || '').toUpperCase()}
              </div>
              <div style={{ fontSize: 11, fontFamily: 'var(--font-mono)', opacity: 0.7, marginTop: 8, letterSpacing: '0.06em' }}>
                {(major.date || '').toUpperCase()} · {(major.time || '').toUpperCase()} · {major.field} PLAYERS
              </div>
              <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginTop: 18, gap: 12 }}>
                <div style={{ flex: 1 }}>
                  <div style={{
                    height: 4, borderRadius: 999, background: 'rgba(234,226,206,0.14)', position: 'relative', overflow: 'hidden',
                  }}>
                    <div style={{ width: `${Math.min(100, (major.filled / Math.max(1, major.field)) * 100)}%`, height: '100%', background: 'var(--cream)', borderRadius: 999 }}/>
                  </div>
                  <div style={{ fontSize: 10, fontFamily: 'var(--font-mono)', opacity: 0.7, marginTop: 6, letterSpacing: '0.06em' }}>
                    {major.filled}/{major.field} REGISTERED
                  </div>
                </div>
                <div style={{
                  padding: '9px 14px', borderRadius: 999,
                  background: 'var(--cream)', color: 'var(--forest)',
                  fontSize: 12, fontWeight: 800, letterSpacing: '0.04em',
                  display: 'inline-flex', alignItems: 'center', gap: 6,
                  boxShadow: '0 6px 14px rgba(14,28,19,0.25)',
                }}>
                  Register <Icon.ArrowRight size={14}/>
                </div>
              </div>
            </div>
          </button>
        </div>
      )}

      {/* Rivalry — match play runs on grudges */}
      {rivals && rivals.length > 0 && (
        <div style={{ padding: '28px 16px 0' }}>
          <RivalryCard r={rivals[0]} go={go}/>
        </div>
      )}

      {/* Your week in the Sandbox */}
      {recap && (
        <div style={{ padding: '16px 16px 0' }}>
          <WeeklyRecapCard recap={recap} go={go}/>
        </div>
      )}

      {/* Friend feed — real activity from people you follow (+ yourself) */}
      <div style={{ padding: '28px 16px 0' }}>
        <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 12, padding: '0 4px' }}>
          <div style={{ fontSize: 11, fontFamily: 'var(--font-mono)', color: 'var(--forest)', opacity: 0.55, letterSpacing: '0.12em', textTransform: 'uppercase' }}>The Feed</div>
          <button onClick={() => go({ screen: 'social' })} style={{ fontSize: 11, fontWeight: 700, color: 'var(--forest)', opacity: 0.7, textTransform: 'uppercase', letterSpacing: '0.12em', fontFamily: 'var(--font-mono)' }}>Find people →</button>
        </div>
        {feedLoading ? (
          <div className="card"><SppLoader/></div>
        ) : feed.length === 0 ? (
          <div className="card" style={{ padding: 24, textAlign: 'center' }}>
            <div style={{ fontFamily: 'var(--font-display)', fontSize: 20, color: 'var(--forest)', lineHeight: 1.1 }}>
              {followCount === 0 ? 'Quiet feed.' : 'No recent activity.'}
            </div>
            <div className="caption-serif" style={{ fontSize: 14, color: 'var(--ink)', opacity: 0.7, marginTop: 6 }}>
              {followCount === 0
                ? 'Follow players to see their pools, tee times, and results here.'
                : 'When friends join pools, lock tee times, or finish matches, it shows up here.'}
            </div>
            {followCount === 0 && (
              <>
                <Button variant="forest" size="sm" onClick={() => go({ screen: 'social' })} style={{ marginTop: 14 }}>
                  Find players
                </Button>
                <InviteFriendsLine profile={profile}/>
              </>
            )}
          </div>
        ) : (
          <div className="card" style={{ padding: '6px 4px' }}>
            {feed.map((a, i) => (
              <FeedRow
                key={a.id}
                item={a}
                last={i === feed.length - 1}
                onOpen={() => go({ screen: 'profile', viewingHandle: a.actor.handle })}
              />
            ))}
          </div>
        )}
      </div>

      {/* Brand foot — full lockup (SANDBOX + Pitch & Putt as one unit) */}
      <div style={{ textAlign: 'center', padding: '56px 16px 24px', opacity: 0.22 }}>
        <Lockup variant="forest" size={180} style={{ margin: '0 auto' }}/>
      </div>
    </div>
  );
}

function NextUpCard({ event, go, isMember, liveMode, brandLoud, mascot, activeMatch, isRegistered }) {
  if (!event) return null;
  const live = event.status === 'live';
  // When this card is in "live" mode and we have a real active match,
  // route to the match screen with its id; otherwise route to live (the
  // legacy live-scorecard route) for design preview, or eventDetail.
  const targetScreen = live ? (activeMatch ? 'match' : 'live') : 'eventDetail';
  const targetParams = activeMatch
    ? { matchId: activeMatch.id }
    : { eventId: event.id };
  return (
    <button onClick={() => go({ screen: targetScreen, ...targetParams })} className="card-hero" style={{
      width: '100%', textAlign: 'left',
      background: 'var(--forest)',
      color: 'var(--cream)',
      padding: 0, border: 'none',
      position: 'relative', display: 'block',
    }}>
      {/* Immersive hero */}
      <div style={{
        height: 200,
        backgroundImage: `linear-gradient(180deg, rgba(14,28,19,0.05) 0%, rgba(14,28,19,0.3) 50%, rgba(14,28,19,0.9) 100%), url('${event.img}')`,
        backgroundSize: 'cover', backgroundPosition: 'center',
        position: 'relative',
      }}>
        <div style={{ position: 'absolute', top: 16, left: 16, display: 'flex', gap: 6 }}>
          {live ? (
            <Chip variant="clay" icon={<LiveDot/>}>
              LIVE · HOLE {activeMatch ? Math.min((activeMatch.thru || 0) + 1, activeMatch.totalHoles || 9) : MOCK.LIVE.currentHole}
            </Chip>
          ) : (
            <div style={{
              padding: '6px 10px', borderRadius: 999,
              background: 'rgba(255,255,255,0.14)', backdropFilter: 'blur(10px)', WebkitBackdropFilter: 'blur(10px)',
              border: '1px solid rgba(255,255,255,0.2)',
              fontSize: 10, fontWeight: 700, letterSpacing: '0.12em', textTransform: 'uppercase', color: 'var(--cream)',
            }}>{event.tagline}</div>
          )}
        </div>
        {!live && (
          <div style={{ position: 'absolute', top: 16, right: 16, textAlign: 'right' }}>
            <div style={{ fontSize: 10, fontFamily: 'var(--font-mono)', opacity: 0.7, letterSpacing: '0.08em' }}>TEE OFF</div>
            <div style={{ fontFamily: 'var(--font-display)', fontSize: 22, lineHeight: 1, marginTop: 2, letterSpacing: '-0.02em' }}>{event.time}</div>
          </div>
        )}
        {mascot === 'full' && (
          <img src="assets/mascot-full-cream.svg" alt="" style={{
            position: 'absolute', right: -14, bottom: -24, width: 110, opacity: 0.22,
            transform: 'rotate(-6deg)',
          }}/>
        )}
        {/* Title floating at bottom of hero */}
        <div style={{ position: 'absolute', bottom: 18, left: 20, right: 20 }}>
          <div style={{ fontFamily: 'var(--font-display)', fontSize: 34, lineHeight: 0.92, letterSpacing: '-0.02em' }}>
            {live ? 'Rejoin your game' : event.courseShort}
          </div>
          <div style={{ fontSize: 12, fontFamily: 'var(--font-mono)', opacity: 0.8, marginTop: 6, letterSpacing: '0.04em' }}>
            {live
              ? (activeMatch && activeMatch.courseName
                  ? activeMatch.courseName.toUpperCase()
                  : 'LIVE NOW')
              : (event.dateFull || '').toUpperCase()}
          </div>
        </div>
      </div>

      {/* Body */}
      <div style={{ padding: '18px 20px 20px' }}>
        {live ? (
          <LiveInlinePreview match={activeMatch}/>
        ) : (
          <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12 }}>
            <div>
              <div style={{ fontSize: 11, fontFamily: 'var(--font-mono)', opacity: 0.6, letterSpacing: '0.06em' }}>
                {event.filled}/{event.field} · FIELD
              </div>
              <div style={{ fontSize: 13, opacity: 0.85, marginTop: 4 }}>
                {isMember ? (
                  <><span style={{ fontWeight: 700 }}>Included</span> <span style={{ opacity: 0.55, textDecoration: 'line-through', marginLeft: 4 }}>${event.priceWalkup}</span></>
                ) : (
                  <><span style={{ opacity: 0.6 }}>${event.priceWalkup} walk-up</span> · <span style={{ fontWeight: 700 }}>${event.priceMember}</span> <span style={{ opacity: 0.6 }}>member</span></>
                )}
              </div>
            </div>
            {isRegistered ? (
              <div style={{
                padding: '8px 14px', borderRadius: 999,
                background: 'var(--forest)', color: 'var(--cream)',
                fontSize: 11, fontWeight: 800, letterSpacing: '0.08em', textTransform: 'uppercase',
              }}>Coming Up ✓</div>
            ) : (
              <Button variant="primary" size="sm" onClick={(e) => { e.stopPropagation(); go({ screen: 'eventDetail', eventId: event.id }); }}>
                {isMember ? 'Grab spot' : 'Register'}
                <Icon.ArrowRight size={14}/>
              </Button>
            )}
          </div>
        )}
      </div>
    </button>
  );
}

function LiveInlinePreview({ match }) {
  // Real active match takes priority; fall back to MOCK.LIVE.yourMatch
  // for design preview when no real one exists (Tweaks `liveMode`).
  const m = match || MOCK.LIVE.yourMatch;
  if (!m) return null;
  const state = m.state; // + = you up, - = down, 0 = AS
  const label = state > 0 ? `${state} UP` : state < 0 ? `${-state} DN` : 'AS';
  const accent = state > 0 ? 'var(--clay)' : state < 0 ? '#E7B8A7' : 'var(--cream)';
  const oppName = (m.teamB && m.teamB.name) || '—';
  return (
    <div>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
        <div>
          <Eyebrow color="var(--cream)" style={{ opacity: 0.5 }}>Current score</Eyebrow>
          <div style={{ display: 'flex', alignItems: 'baseline', gap: 8, marginTop: 2 }}>
            <span style={{ fontFamily: 'var(--font-display)', fontSize: 26, color: accent, letterSpacing: '-0.01em' }}>{label}</span>
            <span style={{ fontSize: 11, opacity: 0.65 }}>thru {m.thru}</span>
          </div>
        </div>
        <div style={{ textAlign: 'right' }}>
          <Eyebrow color="var(--cream)" style={{ opacity: 0.5 }}>vs</Eyebrow>
          <div style={{ fontSize: 13, fontWeight: 700, marginTop: 2 }}>{oppName}</div>
        </div>
      </div>
      <Button variant="clay" size="sm" full style={{ marginTop: 12 }}>
        Open live scorecard <Icon.ArrowRight size={14}/>
      </Button>
    </div>
  );
}

function QuickStat({ label, value, sub, trend, icon, locked, featured }) {
  const bg = featured ? 'var(--forest)' : 'var(--paper)';
  const fg = featured ? 'var(--cream)' : 'var(--forest)';
  const trendColor = featured ? '#C9D8BE' : 'var(--moss-light)';
  return (
    <div style={{
      background: bg,
      color: fg,
      border: featured ? 'none' : 'var(--hairline)',
      borderRadius: 18,
      padding: '14px 16px 16px',
      minWidth: 132,
      position: 'relative',
      opacity: locked ? 0.65 : 1,
      boxShadow: featured ? 'var(--shadow-md)' : 'var(--shadow-sm)',
    }}>
      <div style={{
        fontSize: 10, fontFamily: 'var(--font-mono)',
        display: 'flex', alignItems: 'center', gap: 4,
        opacity: featured ? 0.7 : 0.55,
        letterSpacing: '0.08em', textTransform: 'uppercase',
      }}>
        {label} {icon}
        {locked && <Icon.Lock size={11} color="currentColor"/>}
      </div>
      <div style={{ display: 'flex', alignItems: 'baseline', gap: 6, marginTop: 10 }}>
        <span className="display-num" style={{ fontSize: 28 }}>{locked ? '—.———' : value}</span>
        {trend && !locked && (
          <span style={{ fontSize: 10, color: trendColor, fontWeight: 800, fontFamily: 'var(--font-mono)' }}>{trend}</span>
        )}
      </div>
      {sub && (
        <div style={{
          fontSize: 10, opacity: 0.55, marginTop: 4,
          fontFamily: 'var(--font-mono)', letterSpacing: '0.08em', textTransform: 'uppercase',
        }}>{sub}</div>
      )}
    </div>
  );
}

// ─── Friend feed row ─────────────────────────────────────────────────
function FeedRow({ item, last, onOpen }) {
  const a = item.actor;
  const initial = ((a.first_name || a.handle || '?').replace(/^@/, '') || '?').charAt(0).toUpperCase();

  let primary, accent;
  if (item.type === 'event_signup') {
    const ev = item.payload.event;
    primary = (
      <>
        <span style={{ fontWeight: 700, color: 'var(--forest)' }}>{formatHandle(a.handle)}</span>
        <span style={{ opacity: 0.75 }}> signed up for </span>
        <span style={{ fontWeight: 700, color: 'var(--forest)' }}>{ev.course_short || ev.course_name}</span>
      </>
    );
    accent = '⛳';
  } else if (item.type === 'match_result') {
    const p   = item.payload;
    const opp = p.opponent;
    const oppLabel = opp ? (opp.handle ? formatHandle(opp.handle) : (opp.first_name || 'opponent')) : 'opponent';
    if (p.result === 'H') {
      primary = (
        <>
          <span style={{ fontWeight: 700, color: 'var(--forest)' }}>{formatHandle(a.handle)}</span>
          <span style={{ opacity: 0.75 }}> halved with </span>
          <span style={{ fontWeight: 700 }}>{oppLabel}</span>
        </>
      );
      accent = '½';
    } else if (p.won) {
      primary = (
        <>
          <span style={{ fontWeight: 700, color: 'var(--forest)' }}>{formatHandle(a.handle)}</span>
          <span style={{ opacity: 0.75 }}> beat </span>
          <span style={{ fontWeight: 700 }}>{oppLabel}</span>
          {p.margin && <span style={{ opacity: 0.6 }}> · {p.margin}</span>}
        </>
      );
      accent = '🏆';
    } else {
      primary = (
        <>
          <span style={{ fontWeight: 700, color: 'var(--forest)' }}>{formatHandle(a.handle)}</span>
          <span style={{ opacity: 0.75 }}> lost to </span>
          <span style={{ fontWeight: 700 }}>{oppLabel}</span>
          {p.margin && <span style={{ opacity: 0.6 }}> · {p.margin}</span>}
        </>
      );
    }
  }

  return (
    <button
      onClick={onOpen}
      style={{
        width: '100%', textAlign: 'left',
        padding: '14px 14px',
        display: 'flex', alignItems: 'center', gap: 12,
        background: 'transparent', border: 'none',
        borderBottom: last ? 'none' : '1px solid rgba(14,28,19,0.05)',
        cursor: 'pointer',
      }}
    >
      <div style={{
        width: 36, height: 36, borderRadius: 999,
        background: '#5A7B4A', color: 'var(--cream)',
        display: 'flex', alignItems: 'center', justifyContent: 'center',
        fontFamily: 'var(--font-display)', fontSize: 16,
        overflow: 'hidden', flexShrink: 0,
        border: '2px solid var(--cream)',
      }}>
        {a.avatar_url
          ? <img src={a.avatar_url} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover' }}/>
          : initial}
      </div>
      <div style={{ flex: 1, fontSize: 13, lineHeight: 1.4, color: 'var(--ink)' }}>{primary}</div>
      {accent && <span style={{ fontSize: 16 }}>{accent}</span>}
      <span style={{ fontSize: 10, opacity: 0.5, fontWeight: 600, letterSpacing: '0.06em', fontFamily: 'var(--font-mono)' }}>
        {timeAgoShort(item.ts)}
      </span>
    </button>
  );
}

function timeAgoShort(iso) {
  if (!iso) return '';
  const diff = Math.max(0, (Date.now() - new Date(iso).getTime()) / 1000);
  if (diff < 60)        return 'now';
  if (diff < 3600)      return `${Math.floor(diff / 60)}m`;
  if (diff < 86400)     return `${Math.floor(diff / 3600)}h`;
  if (diff < 86400 * 7) return `${Math.floor(diff / 86400)}d`;
  return new Date(iso).toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
}

// ─── Friends-here pill (avatar stack on event cards) ─────────────────
function FriendsHere({ friends, size = 18, max = 4, light = false }) {
  if (!friends || friends.length === 0) return null;
  const shown = friends.slice(0, max);
  const extra = friends.length - shown.length;
  const fg = light ? 'var(--cream)' : 'var(--forest)';
  return (
    <div style={{
      display: 'inline-flex', alignItems: 'center', gap: 6,
      padding: '4px 8px 4px 4px', borderRadius: 999,
      background: light ? 'rgba(14,28,19,0.45)' : 'rgba(14,28,19,0.06)',
      backdropFilter: light ? 'blur(8px)' : 'none',
      WebkitBackdropFilter: light ? 'blur(8px)' : 'none',
      border: light ? '1px solid rgba(234,226,206,0.18)' : '1px solid rgba(14,28,19,0.04)',
    }}>
      <div style={{ display: 'inline-flex' }}>
        {shown.map((f, i) => {
          const initial = ((f.first_name || f.handle || '?').replace(/^@/, '') || '?').charAt(0).toUpperCase();
          return (
            <div key={f.id} style={{
              width: size, height: size, borderRadius: 999,
              marginLeft: i === 0 ? 0 : -6,
              background: '#5A7B4A', color: 'var(--cream)',
              display: 'flex', alignItems: 'center', justifyContent: 'center',
              fontSize: Math.round(size * 0.5), fontWeight: 700, fontFamily: 'var(--font-display)',
              overflow: 'hidden',
              border: `1.5px solid ${light ? '#0E1C13' : 'var(--paper)'}`,
              boxSizing: 'content-box',
            }}>
              {f.avatar_url
                ? <img src={f.avatar_url} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover' }}/>
                : initial}
            </div>
          );
        })}
      </div>
      <span style={{
        fontSize: 11, fontWeight: 700, color: fg,
        opacity: light ? 1 : 0.85,
      }}>
        {friends.length} {friends.length === 1 ? 'friend' : 'friends'}
        {extra > 0 ? '' : ' here'}
      </span>
    </div>
  );
}

// ─── Waitlist → booked: your upcoming tee time ────────────────────────
// The course seated you from the waitlist. Shows the tee time + your
// foursome; tap to expand the group and open teammates' profiles (ghost
// demo players aren't tappable). Rescheduled bookings carry a badge.
// How early before tee time the "Check in & start round" button appears.
const TEE_START_LEAD_MS = 15 * 60 * 1000;

function UpcomingTeeCard({ a, go, activeMatch }) {
  const time = new Date(a.teeISO).toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' });
  const day = new Date(a.teeISO).toLocaleDateString('en-US', { weekday: 'long', month: 'short', day: 'numeric' });
  const group = (a.group || []).slice(0, 4);

  // Coarse ticker (30s) for the "tees off in…" line — the real countdown +
  // check-in/start all live on the Matchup screen this card opens.
  const [nowMs, setNowMs] = React.useState(Date.now());
  React.useEffect(() => {
    const iv = setInterval(() => setNowMs(Date.now()), 30000);
    return () => clearInterval(iv);
  }, []);
  const teeMs = new Date(a.teeISO).getTime();
  const inProgress = !!a.matchActive && !a.matchDone;
  const hasMatch = !!a.matchId && !a.matchDone;

  const countdown = () => {
    const diff = teeMs - nowMs;
    if (diff <= 0) return 'Tee time is here';
    const mins = Math.round(diff / 60000);
    if (mins < 60) return `Tees off in ${mins} min`;
    const hrs = Math.floor(mins / 60);
    return `Tees off in ${hrs}h ${mins % 60}m`;
  };

  const onTap = () => {
    if (inProgress) { go({ screen: 'match', matchId: a.matchId }); return; }
    if (hasMatch) { go({ screen: 'matchup', matchId: a.matchId }); }
    // Projection-only (no match yet): nothing to open — copy explains why.
  };

  const seat = (m, size) => (
    <div style={{
      width: size, height: size, borderRadius: 999, overflow: 'hidden', flexShrink: 0,
      background: 'var(--cream)', color: 'var(--forest)', display: 'flex', alignItems: 'center', justifyContent: 'center',
      fontFamily: 'var(--font-display)', fontSize: size * 0.42, boxShadow: '0 0 0 2px rgba(14,28,19,0.45)',
    }}>
      {m.avatar_url
        ? <img src={m.avatar_url} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover' }}/>
        : (m.initials || '?')}
    </div>
  );

  // Live score line, pulled from the active match (same data the old separate
  // "Rejoin your game" card showed) so this one card carries everything.
  const st = activeMatch ? activeMatch.state : null;
  const scoreLabel = st == null ? '' : st > 0 ? `${st} UP` : st < 0 ? `${-st} DN` : 'AS';
  const scoreAccent = st > 0 ? 'var(--clay)' : st < 0 ? '#E7B8A7' : 'var(--cream)';
  const oppName = (activeMatch && activeMatch.teamB && activeMatch.teamB.name) || null;
  const liveHole = activeMatch ? Math.min((activeMatch.thru || 0) + 1, activeMatch.totalHoles || 9) : null;

  const eyebrow = inProgress ? (liveHole ? `⛳ Live · Hole ${liveHole}` : '⛳ Match in progress')
    : a.status === 'rescheduled' ? "You're in! · Rescheduled"
      : "You're in! · Scout opponents";

  return (
    <button onClick={onTap} style={{
      width: '100%', textAlign: 'left', border: 'none', display: 'block',
      borderRadius: 'var(--radius-card-lg)', overflow: 'hidden', cursor: 'pointer',
      background: 'linear-gradient(135deg, var(--forest-dark) 0%, var(--forest) 55%, var(--moss) 100%)',
      color: 'var(--cream)', padding: 20, position: 'relative', boxShadow: 'var(--shadow-md)',
    }}>
      <div className="grain" style={{ position: 'absolute', inset: 0, pointerEvents: 'none' }}/>
      <div style={{ position: 'relative' }}>
        <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
          <span style={{ fontSize: 10, fontFamily: 'var(--font-mono)', letterSpacing: '0.14em', textTransform: 'uppercase', opacity: 0.8, fontWeight: 700 }}>
            {eyebrow}
          </span>
          <span style={{ fontSize: 12, opacity: 0.7 }}>▶</span>
        </div>
        <div style={{ fontFamily: 'var(--font-display)', fontSize: 34, lineHeight: 0.95, marginTop: 10, letterSpacing: '-0.02em' }}>
          {time} · {a.courseName}
        </div>
        <div style={{ fontSize: 12, fontFamily: 'var(--font-mono)', opacity: 0.75, marginTop: 6, letterSpacing: '0.04em' }}>
          {day.toUpperCase()}{a.courseCity ? ` · ${a.courseCity.toUpperCase()}` : ''}
        </div>

        <div style={{ display: 'flex', alignItems: 'center', marginTop: 14 }}>
          {group.map((m, i) => (
            <div key={i} style={{ marginLeft: i ? -9 : 0 }}>{seat(m, 30)}</div>
          ))}
          <span style={{ fontSize: 11, opacity: 0.8, marginLeft: 10 }}>
            {inProgress ? 'Your group is playing' : group.length ? 'Your foursome' : ''}
          </span>
        </div>

        {inProgress ? (
          <div style={{ marginTop: 14 }}>
            <div style={{ fontFamily: 'var(--font-display)', fontSize: 24, letterSpacing: '-0.01em', lineHeight: 1 }}>Rejoin your game</div>
            {activeMatch && (
              <div style={{ marginTop: 12, display: 'flex', justifyContent: 'space-between', alignItems: 'center', background: 'rgba(234,226,206,0.1)', borderRadius: 12, padding: '10px 13px' }}>
                <div>
                  <div style={{ fontSize: 9, fontFamily: 'var(--font-mono)', letterSpacing: '0.1em', textTransform: 'uppercase', opacity: 0.55 }}>Current score</div>
                  <div style={{ display: 'flex', alignItems: 'baseline', gap: 8, marginTop: 3 }}>
                    <span style={{ fontFamily: 'var(--font-display)', fontSize: 24, color: scoreAccent }}>{scoreLabel || '—'}</span>
                    <span style={{ fontSize: 11, opacity: 0.65 }}>thru {activeMatch.thru || 0}</span>
                  </div>
                </div>
                {oppName && (
                  <div style={{ textAlign: 'right' }}>
                    <div style={{ fontSize: 9, fontFamily: 'var(--font-mono)', letterSpacing: '0.1em', textTransform: 'uppercase', opacity: 0.55 }}>vs</div>
                    <div style={{ fontSize: 13, fontWeight: 700, marginTop: 3 }}>{oppName}</div>
                  </div>
                )}
              </div>
            )}
            <div style={{
              marginTop: 12, textAlign: 'center', padding: '13px 14px', borderRadius: 13,
              background: 'var(--cream)', color: 'var(--forest)', fontWeight: 800, fontSize: 14, boxShadow: 'var(--shadow-sm)',
            }}>
              Re-join game in progress →
            </div>
          </div>
        ) : hasMatch ? (
          <div style={{ marginTop: 13, display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8, background: 'rgba(234,226,206,0.12)', borderRadius: 12, padding: '10px 13px' }}>
            <span style={{ fontSize: 12, fontFamily: 'var(--font-mono)', letterSpacing: '0.04em', opacity: 0.9 }}>⏱ {countdown().toUpperCase()}</span>
            <span style={{ fontSize: 12, fontWeight: 800 }}>Scout opponents ›</span>
          </div>
        ) : (
          <div style={{ fontSize: 11, opacity: 0.7, marginTop: 12, lineHeight: 1.45 }}>
            ⏱ We'll open your scorecard here once your group is fully matched. Arrive 5–10 minutes before your tee time.
          </div>
        )}
      </div>
    </button>
  );
}

// You were on the waitlist but a foursome never formed for you.
function CutCard({ a, onDismiss }) {
  const [hidden, setHidden] = React.useState(false);
  const day = new Date(a.teeISO).toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' });
  if (hidden) return null;
  return (
    <div className="card" style={{ padding: '14px 16px', display: 'flex', alignItems: 'flex-start', gap: 12 }}>
      <span style={{ fontSize: 20, lineHeight: 1 }}>😔</span>
      <div style={{ flex: 1, minWidth: 0 }}>
        <div style={{ fontFamily: 'var(--font-display)', fontSize: 16, color: 'var(--forest)', lineHeight: 1.1 }}>
          Sorry — you didn't make the cut this time.
        </div>
        <div style={{ fontSize: 12, opacity: 0.65, marginTop: 4, lineHeight: 1.45 }}>
          We couldn't seat you at {a.courseName} on {day}. Your window stays on the waitlist — we'll keep trying as times open.
        </div>
      </div>
      <button
        onClick={() => { setHidden(true); dismissTeeAssignment(a.id); onDismiss && onDismiss(); }}
        aria-label="Dismiss"
        style={{ background: 'transparent', border: 'none', color: 'var(--forest)', opacity: 0.5, fontSize: 14, cursor: 'pointer', padding: 2 }}>
        ✕
      </button>
    </div>
  );
}

// Home card: status of your next reserved round.
function NextRoundStatus({ booking, go }) {
  const slot = booking.slot || {};
  const course = slot.course || {};
  const when = slot.starts_at ? new Date(slot.starts_at) : null;
  const whenLabel = when ? `${when.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' })} · ${when.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' })}` : '';

  let eyebrow, title, sub, onClick, icon;
  if (booking.match_id) {
    eyebrow = 'Your match is set';
    title = 'Scout the matchup';
    sub = `${course.short_name || 'Course'} · ${whenLabel}`;
    icon = '🏌️';
    onClick = () => go({ screen: 'matchup', matchId: booking.match_id });
  } else if (booking.partner_id && booking.partner) {
    const pname = [booking.partner.first_name, booking.partner.last_name].filter(Boolean).join(' ') || booking.partner.handle;
    eyebrow = 'Paired up';
    title = `With ${pname}`;
    sub = `${formatHandle(booking.partner.handle)} · tap to message`;
    icon = '🤝';
    onClick = () => go({ screen: 'chat', dmWith: booking.partner.id, title: pname });
  } else if (booking.needs_partner) {
    eyebrow = 'Finding your partner';
    title = 'On the waitlist';
    sub = `${course.short_name || 'Course'} · ${whenLabel}`;
    icon = '⏳';
    onClick = () => go({ screen: 'myRounds' });
  } else {
    eyebrow = booking.match_type === '2v2' ? 'Team booked' : 'Booked';
    title = 'Finding your match';
    sub = `${course.short_name || 'Course'} · ${whenLabel}`;
    icon = '⛳';
    onClick = () => go({ screen: 'myRounds' });
  }

  return (
    <div style={{ padding: '16px 16px 0' }}>
      <button onClick={onClick} className="card" style={{
        width: '100%', textAlign: 'left', padding: 16, borderRadius: 'var(--radius-card-lg)',
        display: 'flex', alignItems: 'center', gap: 14, border: '1.5px solid var(--forest)',
      }}>
        <span style={{ fontSize: 26 }}>{icon}</span>
        <div style={{ flex: 1, minWidth: 0 }}>
          <div style={{ fontSize: 10, fontFamily: 'var(--font-mono)', color: 'var(--forest)', opacity: 0.6, letterSpacing: '0.14em', textTransform: 'uppercase' }}>{eyebrow}</div>
          <div style={{ fontFamily: 'var(--font-display)', fontSize: 20, color: 'var(--forest)', lineHeight: 1.05, marginTop: 4, letterSpacing: '-0.01em' }}>{title}</div>
          <div style={{ fontSize: 12, opacity: 0.65, marginTop: 4 }}>{sub}</div>
        </div>
        <Icon.ArrowRight size={16} color="var(--forest)"/>
      </button>
    </div>
  );
}

function MiniEventCard({ event, go, friendsHere }) {
  return (
    <button onClick={() => go({ screen: 'eventDetail', eventId: event.id })} className="card" style={{
      minWidth: 190, maxWidth: 190,
      overflow: 'hidden',
      textAlign: 'left',
      padding: 0,
      flexShrink: 0,
      display: 'block',
      borderRadius: 20,
    }}>
      <div style={{
        height: 104,
        backgroundImage: `linear-gradient(180deg, rgba(14,28,19,0) 40%, rgba(14,28,19,0.55) 100%), url('${event.img}')`,
        backgroundSize: 'cover', backgroundPosition: 'center',
        position: 'relative',
      }}>
        {event.isMajor && (
          <div style={{
            position: 'absolute', top: 10, left: 10,
            padding: '4px 8px', borderRadius: 999,
            background: 'rgba(255,255,255,0.16)', backdropFilter: 'blur(10px)',
            border: '1px solid rgba(255,255,255,0.22)',
            fontSize: 9, fontWeight: 800, letterSpacing: '0.16em', textTransform: 'uppercase', color: 'var(--cream)',
          }}>⛳ Major</div>
        )}
        {event.status === 'member-only' && (
          <div style={{
            position: 'absolute', top: 10, left: 10,
            padding: '4px 8px', borderRadius: 999,
            background: 'rgba(14,28,19,0.65)', backdropFilter: 'blur(10px)',
            fontSize: 9, fontWeight: 800, letterSpacing: '0.12em', textTransform: 'uppercase',
            color: 'var(--cream)', display: 'inline-flex', alignItems: 'center', gap: 4,
          }}><Icon.Lock size={9} color="currentColor"/> Member</div>
        )}
        <div style={{ position: 'absolute', bottom: 8, left: 12, right: 12, color: 'var(--cream)' }}>
          <div style={{ fontFamily: 'var(--font-display)', fontSize: 17, lineHeight: 1, letterSpacing: '-0.01em' }}>{event.courseShort}</div>
        </div>
      </div>
      <div style={{ padding: '10px 12px 14px' }}>
        <div style={{ fontSize: 11, fontFamily: 'var(--font-mono)', color: 'var(--forest)', opacity: 0.7, letterSpacing: '0.04em' }}>
          {event.date.toUpperCase()} · {event.time}
        </div>
        {friendsHere && friendsHere.length > 0 && (
          <div style={{ marginTop: 8 }}>
            <FriendsHere friends={friendsHere} size={18} max={3}/>
          </div>
        )}
      </div>
    </button>
  );
}

function AvatarBy({ handle, url: urlProp, name: nameProp, size = 36, zoomable = false }) {
  const [zoomed, setZoomed] = React.useState(false);
  const f    = MOCK.FRIENDS.find(x => x.handle === handle);
  const url  = urlProp  || (f && f.avatar)  || null;
  const name = nameProp || (f && f.name)     || (handle ? String(handle).replace(/^@/, '') : '?');
  const initial = String(name).charAt(0).toUpperCase();

  const imgStyle = { width: '100%', height: '100%', objectFit: 'cover' };
  const initials = (sz) => (
    <div style={{
      width: '100%', height: '100%',
      background: '#5A7B4A', color: 'var(--cream)',
      display: 'flex', alignItems: 'center', justifyContent: 'center',
      fontFamily: 'var(--font-display)', fontSize: sz * 0.42, lineHeight: 1,
    }}>{initial}</div>
  );

  return (
    <>
      <div
        onClick={zoomable ? (e) => { e.stopPropagation(); setZoomed(true); } : undefined}
        style={{
          width: size, height: size, borderRadius: 999, overflow: 'hidden',
          border: '2px solid var(--cream)', flexShrink: 0,
          cursor: zoomable ? 'pointer' : 'default',
        }}
      >
        {url ? <img src={url} alt={name} style={imgStyle}/> : initials(size)}
      </div>

      {zoomed && (
        <div
          onClick={() => setZoomed(false)}
          style={{
            position: 'fixed', inset: 0, zIndex: 9999,
            background: 'rgba(14,28,19,0.88)', backdropFilter: 'blur(14px)',
            display: 'flex', flexDirection: 'column',
            alignItems: 'center', justifyContent: 'center', gap: 20,
          }}
        >
          <div style={{
            width: 220, height: 220, borderRadius: 999, overflow: 'hidden',
            border: '3px solid var(--cream)', boxShadow: '0 24px 64px rgba(0,0,0,0.5)',
          }}>
            {url ? <img src={url} alt={name} style={imgStyle}/> : initials(220)}
          </div>
          <div style={{
            color: 'var(--cream)', fontFamily: 'var(--font-display)',
            fontSize: 22, letterSpacing: '-0.01em',
          }}>{name}</div>
          <div style={{
            fontSize: 11, color: 'rgba(234,226,206,0.55)',
            fontFamily: 'var(--font-mono)', letterSpacing: '0.1em',
          }}>TAP TO CLOSE</div>
        </div>
      )}
    </>
  );
}

// ─── Match invite banner (shown on Home when user has pending invites) ─
function InviteBanner({ invite, profile, go }) {
  const [busy, setBusy] = React.useState(null); // 'accept' | 'decline' | null
  const [err, setErr]   = React.useState('');

  const senderLabel = invite.invited_by_handle || invite.invited_by_first_name || 'A friend';
  const modeLabel   = invite.match_type === '2v2' ? '2v2' : '1v1';
  const courseHint  = invite.course_name ? ` at ${invite.course_name}` : '';

  async function onAccept() {
    setBusy('accept'); setErr('');
    try {
      const matchId = await acceptInvite({ invite, profile });
      go({ screen: 'match', matchId });
    } catch (e) {
      setErr(e.message || 'Could not accept.');
      setBusy(null);
    }
  }

  async function onDecline() {
    setBusy('decline'); setErr('');
    try {
      await declineInvite({ invite });
    } catch (e) {
      setErr(e.message || 'Could not decline.');
      setBusy(null);
    }
  }

  return (
    <div className="card" style={{
      padding: 14,
      background: `linear-gradient(135deg, var(--forest) 0%, var(--moss) 100%)`,
      color: 'var(--paper)',
      border: 'none',
    }}>
      <div style={{ fontSize: 10, fontFamily: 'var(--font-mono)', opacity: 0.75, letterSpacing: '0.14em', textTransform: 'uppercase' }}>
        Match invite
      </div>
      <div style={{ fontFamily: 'var(--font-display)', fontSize: 18, lineHeight: 1.15, marginTop: 6, letterSpacing: '-0.01em' }}>
        {senderLabel} invited you to a {modeLabel}{courseHint}.
      </div>
      {err && (
        <div style={{ marginTop: 8, fontSize: 12, color: '#E7B8A7' }}>{err}</div>
      )}
      <div style={{ display: 'flex', gap: 8, marginTop: 12 }}>
        <button onClick={onAccept} disabled={!!busy} style={{
          flex: 1, padding: '10px 0', borderRadius: 999,
          background: 'var(--paper)', color: 'var(--forest)',
          border: 'none', fontWeight: 800, fontSize: 13,
          opacity: busy ? 0.6 : 1,
        }}>
          {busy === 'accept' ? 'Joining…' : 'Accept'}
        </button>
        <button onClick={onDecline} disabled={!!busy} style={{
          padding: '10px 16px', borderRadius: 999,
          background: 'transparent', color: 'var(--paper)',
          border: '1px solid rgba(255,255,255,0.4)',
          fontWeight: 700, fontSize: 13,
          opacity: busy ? 0.6 : 1,
        }}>
          {busy === 'decline' ? '…' : 'Decline'}
        </button>
      </div>
    </div>
  );
}

// ─── Your pool status (window waitlist) ───────────────────────────────
// Top-of-home card while you're waiting to be matched: live pool count,
// your current effective search band (the engine widens hourly), and the
// share loop — every waitlister recruits for their own tee time.
function YourPoolCard({ w, profile, go }) {
  const [pools] = usePoolBlocks(w.courseId, w.dateStr);
  const [shared, setShared] = React.useState('');
  const blockH = Math.floor(w.startMin / 60);
  const inBlock = (pools || []).find(b => b.blockHour === blockH);
  const others = inBlock ? Math.max(0, inBlock.available - 1) : 0;
  const sbx = (MOCK.USER && MOCK.USER.sbx) || 4;
  const joined = w.createdAt ? new Date(w.createdAt).getTime() : Date.now();
  const hrs = Math.max(0, Math.floor((Date.now() - joined) / 3600000));
  const lo = Math.max(2, Math.floor(sbx) - hrs);
  const hi = Math.min(7, Math.floor(sbx) + hrs);
  const fullSpectrum = lo <= 2 && hi >= 7;
  const widensIn = 60 - Math.floor(((Date.now() - joined) / 60000) % 60);
  const day = new Date(`${w.dateStr}T00:00:00`).toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' });
  const t = (m) => new Date(2000, 0, 1, Math.floor(m / 60), m % 60).toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' });

  async function nudge() {
    const res = await sharePool({
      courseName: w.courseName, dateStr: w.dateStr, blockHour: blockH,
      available: (inBlock && inBlock.available) || 1,
      url: poolShareUrl({ courseId: w.courseId, dateStr: w.dateStr, blockHour: blockH, viaHandle: profile && profile.handle }),
    });
    if (res) { setShared(res === 'copied' ? 'Link copied!' : 'Shared!'); window.setTimeout(() => setShared(''), 2500); }
  }

  return (
    <div className="card" style={{ padding: 16, borderRadius: 'var(--radius-card-lg)', border: '1.5px solid var(--forest)' }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
        <span style={{ width: 8, height: 8, borderRadius: 999, background: 'var(--forest)' }}/>
        <span style={{ fontSize: 10, fontFamily: 'var(--font-mono)', color: 'var(--forest)', opacity: 0.6, letterSpacing: '0.14em', textTransform: 'uppercase' }}>Your pool</span>
      </div>
      <button onClick={() => go({ screen: 'courseDetail', courseId: w.courseId, waitlistDate: w.dateStr })} style={{
        background: 'transparent', border: 'none', padding: 0, textAlign: 'left', width: '100%', cursor: 'pointer', display: 'block',
      }}>
        <div style={{ fontFamily: 'var(--font-display)', fontSize: 20, color: 'var(--forest)', lineHeight: 1.05, marginTop: 8, letterSpacing: '-0.01em' }}>
          {t(w.startMin)} – {t(w.endMin)} · {w.courseName}
        </div>
        <div style={{ fontSize: 12, color: 'var(--ink)', opacity: 0.65, marginTop: 4 }}>
          {day} · {others > 0 ? `You + ${others} other${others === 1 ? '' : 's'} waiting` : 'First one in — bring the pool to you'}
          {inBlock && inBlock.wouldComplete ? ' · ⚡ 1 more locks a match' : ''}
        </div>
        <div style={{ fontSize: 10.5, fontFamily: 'var(--font-mono)', color: 'var(--forest)', opacity: 0.5, marginTop: 4, letterSpacing: '0.03em' }}>
          {fullSpectrum ? 'SEARCHING THE FULL SBX SPECTRUM' : `SEARCHING SBX ${lo}.000–${hi}.999 · WIDENS IN ${widensIn}M`}
        </div>
      </button>
      <button onClick={nudge} style={{
        marginTop: 12, width: '100%', padding: '11px 0', borderRadius: 999,
        background: 'var(--forest)', border: 'none', color: 'var(--cream)',
        fontSize: 13, fontWeight: 800, cursor: 'pointer',
      }}>
        {shared || 'Nudge friends into this pool'}
      </button>
    </div>
  );
}

// ─── Teeing Up row — a mutual's window / open seat / locked time ──────
function TeeingUpRow({ t, last, go }) {
  const day = t.dateStr ? new Date(`${t.dateStr}T00:00:00`).toLocaleDateString('en-US', { weekday: 'short' }) : '';
  const tm = (m) => new Date(2000, 0, 1, Math.floor(m / 60), m % 60).toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' });
  const who = t.name || (t.handle || '').replace(/^@/, '');
  const initial = (who || '?').charAt(0).toUpperCase();

  let line, cta, onTap;
  if (t.kind === 'seat') {
    line = <>needs a 4th · {day} {tm(t.startMin)}–{tm(t.endMin)} · {t.courseShort}</>;
    cta = 'Take the seat';
    onTap = () => go({ seatInvite: t.token });
  } else if (t.kind === 'window') {
    line = <>is in the {day} {tm(t.startMin)}–{tm(t.endMin)} pool · {t.courseShort}</>;
    cta = 'Join this pool';
    onTap = () => go({ screen: 'courseDetail', courseId: t.courseId, waitlistDate: t.dateStr, poolBlock: Math.floor(t.startMin / 60) });
  } else {
    const at = t.teeISO ? new Date(t.teeISO).toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' }) : '';
    line = <>plays {t.courseShort} {day} · {at}</>;
    cta = 'Play that day';
    onTap = () => go({ screen: 'courseDetail', courseId: t.courseId, waitlistDate: t.dateStr });
  }

  return (
    <div style={{
      display: 'flex', alignItems: 'center', gap: 12, padding: '12px 14px',
      borderBottom: last ? 'none' : '1px solid rgba(14,28,19,0.05)',
    }}>
      <div style={{
        width: 34, height: 34, borderRadius: 999, background: '#5A7B4A', color: 'var(--cream)',
        display: 'flex', alignItems: 'center', justifyContent: 'center',
        fontFamily: 'var(--font-display)', fontSize: 15, overflow: 'hidden', flexShrink: 0,
        border: '2px solid var(--cream)',
      }}>
        {t.avatarUrl ? <img src={t.avatarUrl} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover' }}/> : initial}
      </div>
      <div style={{ flex: 1, minWidth: 0, fontSize: 12.5, lineHeight: 1.4, color: 'var(--ink)' }}>
        <span style={{ fontWeight: 700, color: 'var(--forest)' }}>{formatHandle(t.handle)}</span>{' '}
        <span style={{ opacity: 0.75 }}>{line}</span>
      </div>
      <button onClick={onTap} style={{
        flexShrink: 0, fontSize: 11, fontWeight: 800, padding: '7px 12px', borderRadius: 999, cursor: 'pointer',
        background: t.kind === 'seat' ? 'var(--forest)' : 'transparent',
        color: t.kind === 'seat' ? 'var(--cream)' : 'var(--forest)',
        border: '1px solid var(--forest)',
      }}>{cta}</button>
    </div>
  );
}

// ─── Live-now card — a mutual mid-match, score polling every 20s ──────
function LiveNowCard({ c }) {
  const lead = c.aWins - c.bWins;
  const myLead = c.side === 'A' ? lead : -lead;
  const label = myLead > 0 ? `${myLead} UP` : myLead < 0 ? `${-myLead} DN` : 'AS';
  return (
    <div className="card" style={{ minWidth: 190, maxWidth: 190, flexShrink: 0, padding: 14, borderRadius: 20 }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
        <LiveDot/>
        <span style={{ fontSize: 10, fontFamily: 'var(--font-mono)', color: 'var(--forest)', opacity: 0.6, letterSpacing: '0.1em', textTransform: 'uppercase' }}>Live · {c.course}</span>
      </div>
      <div style={{ fontSize: 13, fontWeight: 700, color: 'var(--forest)', marginTop: 8 }}>
        {formatHandle(c.handle)}
      </div>
      <div style={{ display: 'flex', alignItems: 'baseline', gap: 8, marginTop: 4 }}>
        <span style={{ fontFamily: 'var(--font-display)', fontSize: 22, color: 'var(--forest)', letterSpacing: '-0.01em' }}>{label}</span>
        <span style={{ fontSize: 11, opacity: 0.6 }}>thru {c.thru}</span>
      </div>
    </div>
  );
}

// ─── Rivalry card — repeat opponents want a rubber match ──────────────
function RivalryCard({ r, go }) {
  const record = `${r.wins}–${r.losses}${r.halves ? `–${r.halves}` : ''}`;
  const tied = r.wins === r.losses;
  const leading = r.wins > r.losses;
  const budding = r.meetings < 3;
  const hook = tied ? 'Dead even. Someone has to settle this.'
    : leading ? `You own the edge — don't let ${formatHandle(r.handle)} forget it.`
    : `${formatHandle(r.handle)} has your number. Change that.`;
  return (
    <div style={{
      borderRadius: 'var(--radius-card-lg)', overflow: 'hidden', position: 'relative',
      background: 'linear-gradient(135deg, var(--forest-dark) 0%, var(--forest) 55%, var(--moss) 100%)',
      color: 'var(--cream)', padding: 18, boxShadow: 'var(--shadow-md)',
    }}>
      <div className="grain" style={{ position: 'absolute', inset: 0, pointerEvents: 'none' }}/>
      <div style={{ position: 'relative' }}>
        <div style={{ fontSize: 10, fontFamily: 'var(--font-mono)', letterSpacing: '0.14em', textTransform: 'uppercase', opacity: 0.75, fontWeight: 700 }}>
          {budding ? 'Budding rivalry' : 'Rivalry'} · {r.meetings} meetings
        </div>
        <div style={{ fontFamily: 'var(--font-display)', fontSize: 24, lineHeight: 1, marginTop: 8, letterSpacing: '-0.01em' }}>
          You're {record} vs {formatHandle(r.handle)}
        </div>
        <div className="caption-serif" style={{ fontSize: 14, opacity: 0.8, marginTop: 6 }}>{hook}</div>
        <div style={{ display: 'flex', gap: 8, marginTop: 14 }}>
          <button onClick={() => go({ screen: 'events', playTab: 'sbx' })} style={{
            flex: 1, padding: '11px 0', borderRadius: 999, background: 'var(--cream)', color: 'var(--forest)',
            border: 'none', fontWeight: 800, fontSize: 13, cursor: 'pointer',
          }}>Book the rubber match</button>
          <button onClick={() => go({ screen: 'challenge' })} style={{
            padding: '11px 14px', borderRadius: 999, background: 'transparent', color: 'var(--cream)',
            border: '1px solid rgba(255,255,255,0.4)', fontWeight: 700, fontSize: 13, cursor: 'pointer',
          }}>Casual</button>
        </div>
      </div>
    </div>
  );
}

// ─── Weekly recap — your week in the Sandbox ──────────────────────────
function WeeklyRecapCard({ recap, go }) {
  const sbxVal = (MOCK.USER.sbx || 0).toFixed(3);
  const delta = MOCK.USER.sbxDelta;
  return (
    <button onClick={() => go({ screen: 'stats' })} className="card" style={{
      width: '100%', textAlign: 'left', padding: 16, borderRadius: 'var(--radius-card-lg)', display: 'block', cursor: 'pointer',
    }}>
      <div style={{ fontSize: 10, fontFamily: 'var(--font-mono)', color: 'var(--forest)', opacity: 0.6, letterSpacing: '0.14em', textTransform: 'uppercase' }}>
        Your week in the Sandbox
      </div>
      <div style={{ display: 'flex', alignItems: 'baseline', gap: 14, marginTop: 10, flexWrap: 'wrap' }}>
        <span>
          <span className="display-num" style={{ fontSize: 26, color: 'var(--forest)' }}>{recap.w}–{recap.l}{recap.h ? `–${recap.h}` : ''}</span>
          <span style={{ fontSize: 10, fontFamily: 'var(--font-mono)', opacity: 0.55, marginLeft: 6, letterSpacing: '0.08em' }}>W–L{recap.h ? '–H' : ''}</span>
        </span>
        <span>
          <span className="display-num" style={{ fontSize: 26, color: 'var(--forest)' }}>{sbxVal}</span>
          {delta != null && (
            <span style={{ fontSize: 11, fontWeight: 800, fontFamily: 'var(--font-mono)', marginLeft: 6, color: delta >= 0 ? 'var(--moss-light)' : 'var(--loss, #B23B2B)' }}>
              {delta >= 0 ? '+' : ''}{Number(delta).toFixed(3)}
            </span>
          )}
          <span style={{ fontSize: 10, fontFamily: 'var(--font-mono)', opacity: 0.55, marginLeft: 6, letterSpacing: '0.08em' }}>SBX</span>
        </span>
        <span style={{ fontSize: 11, opacity: 0.6 }}>{recap.played} match{recap.played === 1 ? '' : 'es'} this week</span>
      </div>
    </button>
  );
}

// ─── Invite-friends line (feed empty state → referral) ────────────────
function InviteFriendsLine({ profile }) {
  const [msg, setMsg] = React.useState('');
  async function invite() {
    const code = profile && profile.referral_code;
    const ref = code || ((profile && profile.handle) || '').replace(/^@/, '');
    const url = `https://sbx.golf/?via=${encodeURIComponent(ref)}`;
    const text = code
      ? `Come play rated pitch & putt with me — use my code ${code} for $10 off your first round. Create your account here:`
      : 'Come play rated pitch & putt with me — you get $10 off your first round:';
    try {
      if (navigator.share) { await navigator.share({ title: 'Sandbox Pitch & Putt', text, url }); setMsg('Shared!'); }
      else { await navigator.clipboard.writeText(`${text} ${url}`); setMsg('Link copied!'); }
    } catch (_) { /* dismissed */ }
    window.setTimeout(() => setMsg(''), 2500);
  }
  return (
    <button onClick={invite} style={{
      display: 'block', margin: '10px auto 0', background: 'transparent', border: 'none',
      color: 'var(--forest)', fontSize: 12, fontWeight: 700, cursor: 'pointer',
    }}>
      {msg || 'No friends on SBX yet? Invite them — they get $10 off →'}
    </button>
  );
}

// ─── Hot pool card (Match pools near you) ─────────────────────────────
function HotPoolCard({ pool, go }) {
  const c = pool.course;
  return (
    <button onClick={() => go({ screen: 'courseDetail', courseId: c.id, courseSeed: c, waitlistDate: pool.dateStr, poolBlock: pool.blockHour })} className="card" style={{
      minWidth: 178, maxWidth: 178, flexShrink: 0, textAlign: 'left', padding: 14,
      borderRadius: 20, display: 'block', cursor: 'pointer',
      border: pool.wouldComplete ? '1.5px solid var(--forest)' : undefined,
    }}>
      {pool.wouldComplete && (
        <div style={{
          display: 'inline-block', fontSize: 9, fontWeight: 800, letterSpacing: '0.1em', textTransform: 'uppercase',
          color: 'var(--cream)', background: 'var(--forest)', borderRadius: 999, padding: '3px 8px', marginBottom: 8,
        }}>⚡ 1 more locks it</div>
      )}
      <div style={{ fontFamily: 'var(--font-display)', fontSize: 17, color: 'var(--forest)', lineHeight: 1.05, letterSpacing: '-0.01em' }}>{c.shortName}</div>
      <div style={{ fontSize: 11, fontFamily: 'var(--font-mono)', color: 'var(--forest)', opacity: 0.7, marginTop: 6, letterSpacing: '0.04em' }}>
        {pool.dayLabel.toUpperCase()} · {blockLabel(pool.blockHour).toUpperCase()}
      </div>
      <div style={{ fontSize: 11.5, color: 'var(--ink)', opacity: 0.7, marginTop: 6 }}>
        {pool.available} waiting{pool.inBand > 0 && pool.inBand < pool.available ? ` · ${pool.inBand} near you` : ''}{pool.fromPrice ? ` · from $${pool.fromPrice}` : ''}
      </div>
    </button>
  );
}

Object.assign(window, { HomeScreen, NextUpCard, QuickStat, MiniEventCard, AvatarBy, InviteBanner, FeedRow, FriendsHere, useLastMatchCard, lastMatchAgoLine });
