// living.jsx — Lasso shared "living world" layer
// Everything that makes the app feel alive: hero scene, rankings movement,
// activity signals, XP surge, Trail Journal, level-up ceremony, Trail Pass.
// Motion doctrine: settle, surge, glow. No bounces, no particles.
// All motion gated on [data-lv-anim] + prefers-reduced-motion.

// ─── Shared keyframes + gated motion styles ─────────────────────────
function LivingStyles() {
  return (
    <style>{`
      @media (prefers-reduced-motion: no-preference) {
        [data-lv-anim] .lv-dust { animation: lv-dust 9s linear infinite; }
        [data-lv-anim] .lv-cloud { animation: lv-cloud 16s ease-in-out infinite; }
        [data-lv-anim] .lv-breathe { animation: lv-breathe 4.6s ease-in-out infinite; }
        [data-lv-anim] .lv-horn-shimmer { animation: lv-shimmer 7s ease-in-out infinite; }
        [data-lv-anim] .lv-heat { animation: lv-heat 3.2s ease-in-out infinite; }
        [data-lv-anim] .lv-sweep-once { animation: lv-sweep 1100ms 350ms var(--ease-out) forwards; }
        [data-lv-anim] .lv-wallet-glow { animation: lv-wallet 3.4s ease-in-out infinite; }
        [data-lv-anim] .lv-stamp-in { animation: lv-stamp 480ms var(--ease-out) backwards; }
        [data-lv-anim] .lv-settle { animation: lv-settle 700ms var(--ease-out) backwards; }
        [data-lv-anim] .lv-fade { animation: lv-fade 420ms var(--ease-out) backwards; }
        [data-lv-anim] .lv-pulse-dot { animation: lv-pulse-dot 2.2s ease-in-out infinite; }
      }
      @keyframes lv-dust {
        0%   { transform: translate(0, 0); opacity: 0; }
        12%  { opacity: var(--lv-dust-o, 0.35); }
        88%  { opacity: var(--lv-dust-o, 0.35); }
        100% { transform: translate(26px, -34px); opacity: 0; }
      }
      @keyframes lv-cloud {
        0%   { transform: translateX(-55%); }
        100% { transform: translateX(130%); }
      }
      @keyframes lv-breathe {
        0%, 100% { transform: scale(1); }
        50%      { transform: scale(1.014); }
      }
      @keyframes lv-shimmer {
        0%, 72%  { background-position: -160% 0; }
        88%      { background-position: 160% 0; }
        100%     { background-position: 160% 0; }
      }
      @keyframes lv-heat {
        0%, 100% { transform: scaleY(1); opacity: 0.16; }
        50%      { transform: scaleY(1.5); opacity: 0.28; }
      }
      @keyframes lv-sweep {
        0%   { background-position: -140% 0; }
        100% { background-position: 240% 0; }
      }
      @keyframes lv-wallet {
        0%, 100% { box-shadow: 0 0 0 0 rgba(242,140,27,0); }
        50%      { box-shadow: 0 0 18px 1px rgba(242,140,27,0.28); }
      }
      @keyframes lv-stamp {
        0%   { transform: scale(0.55); opacity: 0; }
        100% { transform: scale(1); opacity: 1; }
      }
      @keyframes lv-settle {
        0%   { transform: scale(1.07) translateY(6px); opacity: 0; }
        100% { transform: scale(1) translateY(0); opacity: 1; }
      }
      @keyframes lv-fade {
        0%   { opacity: 0; }
        100% { opacity: 1; }
      }
      @keyframes lv-pulse-dot {
        0%, 100% { opacity: 1; }
        50%      { opacity: 0.45; }
      }
      /* picked selector lift — a state, transitions handle the motion */
      .lv-picked { transform: translateY(-1px); box-shadow: 0 4px 14px rgba(216,90,20,0.22); }
    `}</style>
  );
}

// ─── Living hero — a cinematic dusk moment, not a banner ────────────
function LivingHero({ app }) {
  return (
    <div data-lasso="living-hero" style={{
      margin: '0 20px 18px', borderRadius: 28, overflow: 'hidden', position: 'relative',
      minHeight: 216, color: 'var(--dust-cream)',
      background: 'linear-gradient(180deg, #241209 0%, #3A1F11 46%, #6B3312 78%, #8A4416 100%)',
    }}>
      {/* horizon glow */}
      <div style={{ position: 'absolute', inset: 0, background: 'radial-gradient(ellipse at 78% 96%, rgba(242,140,27,0.4), transparent 58%)' }}/>

      {/* mesas + cacti — simple dusk silhouettes */}
      <svg aria-hidden="true" viewBox="0 0 350 80" preserveAspectRatio="none" style={{ position: 'absolute', left: 0, right: 0, bottom: 0, width: '100%', height: 84, display: 'block' }}>
        <polygon points="0,80 0,46 34,46 46,62 120,62 120,80" fill="#1B120D" opacity="0.85"></polygon>
        <polygon points="150,80 150,58 196,58 210,40 268,40 282,58 350,58 350,80" fill="#1B120D" opacity="0.7"></polygon>
        <rect x="58" y="48" width="5" height="32" rx="2.5" fill="#140C08"></rect>
        <rect x="50" y="54" width="4" height="9" rx="2" fill="#140C08"></rect>
        <rect x="66" y="52" width="4" height="10" rx="2" fill="#140C08"></rect>
        <rect x="304" y="50" width="5" height="30" rx="2.5" fill="#140C08"></rect>
        <rect x="311" y="56" width="4" height="9" rx="2" fill="#140C08"></rect>
      </svg>

      {/* heat shimmer on the horizon */}
      <div className="lv-heat" aria-hidden="true" style={{
        position: 'absolute', left: 0, right: 0, bottom: 70, height: 14, opacity: 0.16,
        background: 'linear-gradient(90deg, transparent, rgba(242,140,27,0.6), transparent)',
        filter: 'blur(6px)', transformOrigin: 'center bottom', pointerEvents: 'none',
      }}/>

      {/* cloud shadow sweeping across */}
      <div className="lv-cloud" aria-hidden="true" style={{
        position: 'absolute', top: 0, bottom: 0, left: 0, width: '46%', pointerEvents: 'none',
        background: 'radial-gradient(ellipse at 50% 30%, rgba(0,0,0,0.22), transparent 68%)',
      }}/>

      {/* dust motes */}
      {[
        { l: '12%', t: '58%', s: 3, d: '0s',    o: 0.3 },
        { l: '30%', t: '70%', s: 2, d: '-2.8s', o: 0.24 },
        { l: '52%', t: '64%', s: 3, d: '-5.4s', o: 0.34 },
        { l: '68%', t: '76%', s: 2, d: '-1.6s', o: 0.2 },
        { l: '82%', t: '60%', s: 2, d: '-6.8s', o: 0.28 },
        { l: '44%', t: '82%', s: 2, d: '-4s',   o: 0.22 },
      ].map((m, i) => (
        <span key={i} className="lv-dust" aria-hidden="true" style={{
          position: 'absolute', left: m.l, top: m.t, width: m.s, height: m.s, borderRadius: '50%',
          background: 'var(--sun-orange)', opacity: 0, '--lv-dust-o': m.o,
          animationDelay: m.d, pointerEvents: 'none',
        }}/>
      ))}

      {/* the bull — breathing, with a slow horn light-shimmer */}
      <div className="lv-breathe" style={{ position: 'absolute', right: -8, bottom: -14, transformOrigin: '60% 80%' }}>
        <Bull size={128}/>
        <div className="lv-horn-shimmer" aria-hidden="true" style={{
          position: 'absolute', inset: 0,
          background: 'linear-gradient(105deg, transparent 38%, rgba(255,222,180,0.55) 50%, transparent 62%)',
          backgroundSize: '220% 100%', backgroundRepeat: 'no-repeat', backgroundPosition: '-160% 0',
          WebkitMaskImage: 'url("assets/bull-twotone.png")', maskImage: 'url("assets/bull-twotone.png")',
          WebkitMaskSize: 'contain', maskSize: 'contain', WebkitMaskRepeat: 'no-repeat', maskRepeat: 'no-repeat',
          mixBlendMode: 'screen', pointerEvents: 'none',
        }}/>
      </div>

      {/* copy */}
      <div style={{ position: 'relative', padding: '24px 22px 22px', maxWidth: '68%' }}>
        <Eyebrow color="var(--sun-orange)">Fast delivery. No bull.</Eyebrow>
        <div style={{ fontFamily: 'var(--font-display)', fontWeight: 900, fontSize: 30, lineHeight: 0.98, letterSpacing: '-0.01em', marginTop: 8 }}>Catch your craving.</div>
        <div style={{ fontFamily: 'var(--font-body)', fontSize: 12.5, color: '#E4C9A8', marginTop: 6, lineHeight: 1.45 }}>Local kitchens, picked up the moment you order.</div>
        <div style={{ marginTop: 14 }}>
          <Button kind="accent" size="sm" onClick={() => {}}>Order now</Button>
        </div>
      </div>
    </div>
  );
}

// ─── Wrangler Rankings — earned titles + weekly movement ────────────
const RANK_TITLES = {
  1: { label: 'Most Wanted',       bg: 'linear-gradient(100deg, #B47818 0%, #E8B54A 48%, #B47818 100%)', fg: '#241505' },
  2: { label: "Sheriff's Pick",    bg: 'var(--dark-chocolate)', fg: 'var(--dust-cream)' },
  3: { label: 'Legendary Kitchen', bg: 'rgba(245,239,231,0.92)', fg: 'var(--dark-chocolate)' },
};

// Seeded weekly movement — derived from order counts (reviews) + week number,
// so re-ranks stay consistent all week and feel earned, not random.
function lvWeekOfYear() {
  const now = new Date();
  const start = new Date(now.getFullYear(), 0, 1);
  return Math.floor((now - start) / (7 * 24 * 3600 * 1000));
}
function lvHash(str) {
  let h = 0;
  for (let i = 0; i < str.length; i++) h = ((h << 5) - h + str.charCodeAt(i)) | 0;
  return Math.abs(h);
}
function rankMoveFor(r, rank) {
  const seed = lvHash(r.id) + lvWeekOfYear() + Math.round((r.reviews || 100) / 40);
  const delta = (seed % 7) - 3; // -3..+3, stable for the week
  if (rank === 1 && delta <= 0) return 0; // the champ held
  return delta;
}
// Kitchens that climbed this week (for the "your kitchen moved up" note)
function rankClimbers(items) {
  return items.filter((r, i) => rankMoveFor(r, i + 1) > 0).map(r => r.id);
}

function RankTitleChip({ rank }) {
  const t = RANK_TITLES[rank];
  if (!t) return null;
  return (
    <span data-lasso="rank-title" style={{
      display: 'inline-flex', alignItems: 'center', gap: 5, padding: '4px 10px', borderRadius: 999,
      background: t.bg, color: t.fg, fontFamily: 'var(--font-display)', fontWeight: 900,
      fontSize: 9.5, letterSpacing: '0.09em', textTransform: 'uppercase',
      boxShadow: rank === 1 ? '0 2px 10px rgba(232,181,74,0.4)' : '0 2px 8px rgba(0,0,0,0.3)',
    }}>
      <Ico.star c={t.fg} s={10}/>{t.label}
    </span>
  );
}

function RankMoveChip({ move }) {
  const up = move > 0, down = move < 0;
  const fg = up ? '#7FD695' : down ? '#E88B7A' : 'rgba(245,239,231,0.75)';
  return (
    <span data-lasso="rank-move" style={{
      display: 'inline-flex', alignItems: 'center', gap: 4,
      fontFamily: 'var(--font-display)', fontWeight: 800, fontSize: 10.5, color: fg,
    }}>
      {up ? `\u25B2${move}` : down ? `\u25BC${Math.abs(move)}` : '\u2014'}
      <span style={{ fontFamily: 'var(--font-body)', fontWeight: 400, fontSize: 10, color: 'rgba(245,239,231,0.6)' }}>
        {up || down ? 'this week' : 'held'}
      </span>
    </span>
  );
}

// ─── Activity labels — live-world signals on cards ──────────────────
const ACTIVITY_MAP = {
  'rojo-smoke':      { text: 'Hot kitchen',      color: 'var(--brand-orange)', pulse: true },
  'maverick-burger': { text: '4 riders nearby',  color: 'var(--live-green)',   pulse: true },
  'cactus-cream':    { text: 'Fresh batch out',  color: 'var(--sun-orange)',   pulse: false },
  'sundown-tacos':   { text: 'Busy trail',       color: 'var(--horse-brown)',  pulse: false },
  'iron-noodle':     { text: 'Hot kitchen',      color: 'var(--brand-orange)', pulse: true },
  'mesa-greens':     { text: 'Quiet trail',      color: 'var(--warm-stone)',   pulse: false },
  'verde-vegano':    { text: 'Quiet trail',      color: 'var(--warm-stone)',   pulse: false },
  'dune-coffee':     { text: 'Fresh batch out',  color: 'var(--sun-orange)',   pulse: false },
  'big-tex-pizza':   { text: '3 riders nearby',  color: 'var(--live-green)',   pulse: true },
  'canteen-bowls':   { text: 'Quiet trail',      color: 'var(--warm-stone)',   pulse: false },
  'prairie-pies':    { text: 'Fresh batch out',  color: 'var(--sun-orange)',   pulse: false },
  'wok-this-way':    { text: 'Busy trail',       color: 'var(--horse-brown)',  pulse: false },
};

function ActivityLabel({ id, style }) {
  const a = ACTIVITY_MAP[id];
  if (!a) return null;
  return (
    <span data-lasso="activity-label" style={{
      display: 'inline-flex', alignItems: 'center', gap: 5,
      fontFamily: 'var(--font-body)', fontSize: 11, color: 'var(--warm-stone)', ...style,
    }}>
      <span className={a.pulse ? 'lv-pulse-dot' : ''} style={{ width: 6, height: 6, borderRadius: '50%', background: a.color, flexShrink: 0 }}/>
      {a.text}
    </span>
  );
}

// ─── CountUp — ease-out cubic number surge ──────────────────────────
function CountUp({ to, from = 0, duration = 900, prefix = '', suffix = '', format }) {
  const [val, setVal] = React.useState(from);
  React.useEffect(() => {
    const reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
    if (reduce) { setVal(to); return; }
    let raf; const t0 = performance.now();
    const tick = (t) => {
      const p = Math.min(1, (t - t0) / duration);
      const e = 1 - Math.pow(1 - p, 3); // ease-out cubic
      setVal(Math.round(from + (to - from) * e));
      if (p < 1) raf = requestAnimationFrame(tick);
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, [to, from, duration]);
  return <>{prefix}{format ? format(val) : val.toLocaleString()}{suffix}</>;
}

// ─── Trail Journal — history as a story ─────────────────────────────
const TRAIL_JOURNAL = [
  { id: 'tj7', icon: 'trophy', date: 'May 18', title: 'Biggest ride yet',        sub: 'A £34.50 feast from Rojo & Smoke. Four plates deep.' },
  { id: 'tj6', icon: 'badge',  date: 'May 12', title: 'Made Drover',             sub: 'Rank three on the trail. 1,840 XP and counting.' },
  { id: 'tj5', icon: 'wallet', date: 'Apr 30', title: 'Maya sent you £10',       sub: '"For the brisket emergency." Spent same night.' },
  { id: 'tj4', icon: 'flame',  date: 'Apr 06', title: 'Six weeks riding',        sub: 'Longest streak yet — one order a week, no misses.' },
  { id: 'tj3', icon: 'star',   icon2: true, date: 'Mar 03', title: 'Early Bird, earned', sub: 'Sunrise orders three mornings running.' },
  { id: 'tj2', icon: 'ride',   date: 'Feb 20', title: 'First favourite',         sub: 'Cactus & Cream — the flat white did it.' },
  { id: 'tj1', icon: 'ride',   date: 'Feb 12', title: 'First ride',              sub: 'One flat white, one cinnamon knot. It begins.' },
];

function TrailJournalSheet({ open, onClose, app }) {
  const JIcon = ({ name }) => {
    const c = 'var(--brand-orange)', s = 15;
    if (name === 'trophy') return <Ico.trophy c={c} s={s}/>;
    if (name === 'badge')  return <Ico.badge c={c} s={s}/>;
    if (name === 'wallet') return <Ico.wallet c={c} s={s}/>;
    if (name === 'flame')  return <Ico.flame c={c} s={s}/>;
    if (name === 'star')   return <Ico.star c={c} s={s}/>;
    return <Ico.ride c={c} s={s}/>;
  };
  return (
    <BottomSheet open={open} onClose={onClose} title="Trail Journal" maxHeight="88%">
      <div data-lasso="trail-journal" style={{ padding: '0 20px 28px' }}>
        <div style={{ fontFamily: 'var(--font-body)', fontSize: 12.5, color: 'var(--warm-stone)', lineHeight: 1.5, marginBottom: 18 }}>
          {app.ordersCount != null ? app.ordersCount : PROFILE.total_orders} rides so far. Here's how the story goes.
        </div>
        <div style={{ position: 'relative', paddingLeft: 34 }}>
          {/* the spine */}
          <div style={{ position: 'absolute', left: 14, top: 6, bottom: 6, width: 2, borderRadius: 1, background: 'linear-gradient(180deg, var(--brand-orange), var(--soft-line))' }}/>
          {TRAIL_JOURNAL.map((e, i) => (
            <div key={e.id} className="lv-fade" style={{ position: 'relative', paddingBottom: i === TRAIL_JOURNAL.length - 1 ? 0 : 22, animationDelay: `${i * 60}ms` }}>
              {/* milestone node */}
              <div style={{
                position: 'absolute', left: -34, top: 0, width: 30, height: 30, borderRadius: '50%',
                background: 'var(--bg-warm)', border: '2px solid var(--brand-orange)',
                display: 'flex', alignItems: 'center', justifyContent: 'center',
              }}><JIcon name={e.icon}/></div>
              <div style={{ paddingLeft: 8 }}>
                <div style={{ fontFamily: 'var(--font-body)', fontSize: 10.5, color: 'var(--warm-stone)', textTransform: 'uppercase', letterSpacing: '0.08em' }}>{e.date}</div>
                <div style={{ fontFamily: 'var(--font-display)', fontWeight: 900, fontSize: 15, color: 'var(--fg-primary)', marginTop: 2 }}>{e.title}</div>
                <div style={{ fontFamily: 'var(--font-body)', fontSize: 12, color: 'var(--warm-stone)', marginTop: 2, lineHeight: 1.45 }}>{e.sub}</div>
              </div>
            </div>
          ))}
        </div>
        <div style={{ textAlign: 'center', fontFamily: 'var(--font-display)', fontWeight: 800, fontSize: 12, color: 'var(--warm-stone)', marginTop: 26 }}>
          Every legend starts somewhere.
        </div>
      </div>
    </BottomSheet>
  );
}

// ─── Level-up ceremony — one sweep, no confetti ─────────────────────
const LV_NEXT_RANK = { threshold: 2500, label: 'Trail Boss' };

function LevelUpOverlay({ app }) {
  const [show, setShow] = React.useState(false);
  const [fromXp, setFromXp] = React.useState(0);
  const prev = React.useRef(app.xp);
  React.useEffect(() => {
    if (prev.current < LV_NEXT_RANK.threshold && app.xp >= LV_NEXT_RANK.threshold) {
      setFromXp(prev.current);
      setShow(true);
    }
    prev.current = app.xp;
  }, [app.xp]);
  if (!show) return null;
  return (
    <div data-lasso="levelup-overlay" data-level-up-modal="" data-level-up-state="ceremony" data-screen-label="Level-up ceremony" style={{
      position: 'absolute', inset: 0, zIndex: 400, display: 'flex', alignItems: 'center', justifyContent: 'center',
      background: 'rgba(20,12,8,0.9)', backdropFilter: 'blur(6px)',
    }}>
      <div className="lv-settle" style={{ textAlign: 'center', padding: '0 32px' }}>
        {/* badge reveal */}
        <div className="lv-fade" style={{
          width: 92, height: 92, borderRadius: '50%', margin: '0 auto 18px', animationDelay: '500ms',
          background: 'radial-gradient(circle, rgba(242,140,27,0.18), transparent 70%)',
          border: '2px solid var(--brand-orange)', display: 'flex', alignItems: 'center', justifyContent: 'center',
        }}><Bull size={56}/></div>
        <div style={{ fontFamily: 'var(--font-display)', fontWeight: 800, fontSize: 11, letterSpacing: '0.16em', textTransform: 'uppercase', color: 'var(--sun-orange)' }}>New rank</div>
        <div style={{ fontFamily: 'var(--font-display)', fontWeight: 900, fontSize: 40, lineHeight: 1, color: '#fff', marginTop: 8, letterSpacing: '-0.01em' }}>{LV_NEXT_RANK.label}</div>
        {/* XP chip with a single light sweep */}
        <div className="lv-sweep-once" style={{
          display: 'inline-flex', alignItems: 'center', gap: 7, marginTop: 16, padding: '9px 18px', borderRadius: 999,
          background: 'linear-gradient(105deg, var(--dark-chocolate) 40%, rgba(242,140,27,0.55) 50%, var(--dark-chocolate) 60%)',
          backgroundSize: '260% 100%', backgroundPosition: '-140% 0',
          border: '1px solid rgba(242,140,27,0.4)',
        }}>
          <Ico.badge c="var(--sun-orange)" s={15}/>
          <span style={{ fontFamily: 'var(--font-display)', fontWeight: 900, fontSize: 14, color: 'var(--dust-cream)' }}>
            <CountUp from={fromXp} to={app.xp} duration={1300}/> XP
          </span>
        </div>
        <div style={{ fontFamily: 'var(--font-body)', fontSize: 12.5, color: '#C9B7AA', marginTop: 14, lineHeight: 1.5 }}>
          You run this trail now. Kitchens tip their hat.
        </div>
        <div style={{ marginTop: 22 }}>
          <Button kind="accent" size="lg" onClick={() => setShow(false)}>Ride on</Button>
        </div>
      </div>
    </div>
  );
}

// ─── Trail Pass — per-kitchen passport ──────────────────────────────
// A stamp card PER KITCHEN: visits punch stamps, a reward waits at the end.
const TRAIL_PASS_CONFIG = {
  'rojo-smoke':      { total: 8,  reward: 'free brisket tacos',   seed: 3 },
  'maverick-burger': { total: 6,  reward: 'a free ranch burger',  seed: 4 },
  'cactus-cream':    { total: 8,  reward: 'a free pastry',        seed: 5 },
  'sundown-tacos':   { total: 6,  reward: 'a free taco trio',     seed: 1 },
  'dune-coffee':     { total: 10, reward: 'a free drink',         seed: 3 },
  'big-tex-pizza':   { total: 8,  reward: 'a free 12" pizza',     seed: 2 },
};

function trailPassFor(rid, passStamps) {
  const cfg = TRAIL_PASS_CONFIG[rid];
  if (!cfg) return null;
  const bumps = (passStamps && passStamps[rid]) || 0;
  const stamps = Math.min(cfg.total, cfg.seed + bumps);
  return { rid, total: cfg.total, reward: cfg.reward, stamps, justStamped: bumps > 0 };
}

// The stamp row — horseshoes punch in; the last slot is the reward.
function TrailPassStamps({ pass, size = 26 }) {
  return (
    <div style={{ display: 'flex', gap: 7, flexWrap: 'wrap' }}>
      {Array.from({ length: pass.total }).map((_, i) => {
        const stamped = i < pass.stamps;
        const isNewest = pass.justStamped && i === pass.stamps - 1;
        const isReward = i === pass.total - 1;
        return (
          <div key={i} className={isNewest ? 'lv-stamp-in' : ''} style={{
            width: size, height: size, borderRadius: '50%', flexShrink: 0,
            display: 'flex', alignItems: 'center', justifyContent: 'center',
            background: stamped ? 'var(--brand-orange)' : 'transparent',
            border: '1.8px ' + (stamped ? 'solid var(--brand-orange)' : isReward ? 'dashed var(--brand-orange)' : 'dashed var(--soft-line)'),
          }}>
            {isReward && !stamped
              ? <Ico.star c="var(--brand-orange)" s={size * 0.48}/>
              : <Ico.horseshoe c={stamped ? '#fff' : 'var(--soft-line)'} s={size * 0.52}/>}
          </div>
        );
      })}
    </div>
  );
}

// Full card — restaurant screen + Rewards summary.
function TrailPassCard({ rid, app, compact = false, onOpen }) {
  const pass = trailPassFor(rid, app.passStamps);
  if (!pass) return null;
  const r = RESTAURANT_LOOKUP[rid];
  const left = pass.total - pass.stamps;
  const done = left === 0;
  return (
    <div data-lasso="trail-pass" data-restaurant-id={rid} onClick={onOpen} style={{
      borderRadius: 18, padding: compact ? '13px 14px' : '16px 16px 14px', cursor: onOpen ? 'pointer' : 'default',
      background: 'var(--bg-surface)', border: '1.5px solid var(--soft-line)', boxShadow: 'var(--shadow-md)',
    }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 10 }}>
        <Ico.horseshoe c="var(--brand-orange)" s={15}/>
        <span style={{ fontFamily: 'var(--font-display)', fontWeight: 900, fontSize: 11, letterSpacing: '0.1em', textTransform: 'uppercase', color: 'var(--brand-orange)' }}>Trail Pass</span>
        {compact && r && <span style={{ fontFamily: 'var(--font-display)', fontWeight: 800, fontSize: 12.5, color: 'var(--fg-primary)', marginLeft: 'auto' }}>{r.name}</span>}
      </div>
      <TrailPassStamps pass={pass} size={compact ? 22 : 27}/>
      <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 8, marginTop: 10 }}>
        <div style={{ fontFamily: 'var(--font-body)', fontSize: 11.5, color: 'var(--warm-stone)' }}>
          {done
            ? <span style={{ color: 'var(--live-green)', fontFamily: 'var(--font-display)', fontWeight: 800 }}>Pass complete — reward's yours</span>
            : <>{left} {left === 1 ? 'visit' : 'visits'} to <span style={{ color: 'var(--fg-primary)', fontFamily: 'var(--font-display)', fontWeight: 800 }}>{pass.reward}</span></>}
        </div>
        {!compact && <div style={{ fontFamily: 'var(--font-display)', fontWeight: 800, fontSize: 11, color: 'var(--brand-orange)', flexShrink: 0 }}>{pass.stamps}/{pass.total}</div>}
      </div>
    </div>
  );
}

// ─── Exports ─────────────────────────────────────────────────────────
Object.assign(window, {
  LivingStyles, LivingHero,
  RankTitleChip, RankMoveChip, RANK_TITLES, rankMoveFor, rankClimbers,
  ActivityLabel, ACTIVITY_MAP,
  CountUp,
  TrailJournalSheet, TRAIL_JOURNAL,
  LevelUpOverlay, LV_NEXT_RANK,
  TrailPassCard, TrailPassStamps, trailPassFor, TRAIL_PASS_CONFIG,
});
