// itrymo prototype — shared UI primitives & filter/sort engine
// Loaded after data.js. Exports tokens, icons, chips, cards, hooks.

// ─────────────────────────────────────────────────────────────
// Tokens
// ─────────────────────────────────────────────────────────────
const T = {
  bg:        '#FAF7F2',
  panel:     '#FFFFFF',
  ink:       '#1A1814',
  faintInk:  '#3A3530',
  muted:     '#8A857C',
  border:    '#EAE3D6',
  borderSoft:'#F0EAE0',
  selBg:     '#1A1814',
  selFg:     '#FFFFFF',
  accent:    '#B36A00',
  accentBg:  '#FBEEDA',
  accentBd:  '#E7C896',
  fontUI:    "'DM Sans', system-ui, sans-serif",
  fontDisp:  "'DM Sans', system-ui, sans-serif",
};

// ─────────────────────────────────────────────────────────────
// Icon set — keep tiny and stroke-based
// ─────────────────────────────────────────────────────────────
function Icon({ name, size = 14, color = 'currentColor', fill = 'none', style }) {
  const p = {
    sparkle: <><path d="M12 3v4M12 17v4M3 12h4M17 12h4M6 6l3 3M15 15l3 3M18 6l-3 3M9 15l-3 3"/></>,
    bolt:    <><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></>,
    map:     <><polygon points="3 6 9 3 15 6 21 3 21 18 15 21 9 18 3 21 3 6"/><line x1="9" y1="3" x2="9" y2="18"/><line x1="15" y1="6" x2="15" y2="21"/></>,
    compass: <><circle cx="12" cy="12" r="9"/><polygon points="16 8 13 14 8 16 11 10 16 8"/></>,
    bed:     <><path d="M3 18V8M3 12h14a4 4 0 0 1 4 4v2M3 18h18"/><circle cx="8" cy="11" r="2"/></>,
    filter:  <><line x1="4" y1="6" x2="20" y2="6"/><line x1="7" y1="12" x2="17" y2="12"/><line x1="10" y1="18" x2="14" y2="18"/></>,
    sort:    <><path d="M7 4v16M3 17l4 4 4-4M17 20V4M13 7l4-4 4 4"/></>,
    close:   <><line x1="6" y1="6" x2="18" y2="18"/><line x1="18" y1="6" x2="6" y2="18"/></>,
    plus:    <><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></>,
    minus:   <><line x1="5" y1="12" x2="19" y2="12"/></>,
    chev:    <><polyline points="6 9 12 15 18 9"/></>,
    chevR:   <><polyline points="9 6 15 12 9 18"/></>,
    search:  <><circle cx="11" cy="11" r="7"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></>,
    check:   <><polyline points="20 6 9 17 4 12"/></>,
    star:    <><polygon points="12 2 15 9 22 9 17 14 19 21 12 17 5 21 7 14 2 9 9 9 12 2"/></>,
    pin:     <><path d="M12 22s7-7 7-13a7 7 0 1 0-14 0c0 6 7 13 7 13z"/><circle cx="12" cy="9" r="2.5"/></>,
    heart:   <><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 1 0-7.78 7.78L12 21.17l8.84-8.84a5.5 5.5 0 0 0 0-7.78z"/></>,
    flame:   <><path d="M12 22c4 0 7-2.5 7-6 0-3-2-5-3-7-1 2-2 3-4 3 0-3-1-5-3-7 .5 5-4 6-4 11 0 3.5 3 6 7 6z"/></>,
    bookmark:<><path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/></>,
    globe:   <><circle cx="12" cy="12" r="9"/><line x1="3" y1="12" x2="21" y2="12"/><path d="M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18"/></>,
    luggage: <><rect x="6" y="7" width="12" height="14" rx="2"/><path d="M10 7V4h4v3"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/></>,
    calendar:<><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="16" y1="2" x2="16" y2="6"/></>,
    plane:   <><path d="M21 16v-2l-8-5V3.5a1.5 1.5 0 0 0-3 0V9l-8 5v2l8-2.5V19l-2 1.5V22l3.5-1 3.5 1v-1.5L11 19v-5.5z"/></>,
    car:     <><path d="M5 13l1.5-4.5A2 2 0 0 1 8.4 7h7.2a2 2 0 0 1 1.9 1.5L19 13M5 13h14v4a1 1 0 0 1-1 1h-1a1 1 0 0 1-1-1M5 13v4a1 1 0 0 0 1 1h1a1 1 0 0 0 1-1"/><circle cx="7.5" cy="14.5" r="0.5"/><circle cx="16.5" cy="14.5" r="0.5"/></>,
    coffee:  <><path d="M4 8h13v5a4 4 0 0 1-4 4H8a4 4 0 0 1-4-4V8z"/><path d="M17 9h2a2 2 0 0 1 0 4h-2"/><line x1="7" y1="2" x2="7" y2="5"/><line x1="11" y1="2" x2="11" y2="5"/></>,
    utensils:<><path d="M7 2v9M4 2v6a3 3 0 0 0 3 3M7 11v11M17 2c-2 0-3 2-3 5s1 4 3 4m0 0v11"/></>,
    moon:    <><path d="M21 12.8A8 8 0 1 1 11.2 3a6 6 0 0 0 9.8 9.8z"/></>,
    grip:    <><circle cx="9" cy="6" r="1"/><circle cx="9" cy="12" r="1"/><circle cx="9" cy="18" r="1"/><circle cx="15" cy="6" r="1"/><circle cx="15" cy="12" r="1"/><circle cx="15" cy="18" r="1"/></>,
    pencil:  <><path d="M12 20h9"/><path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4z"/></>,
    trash:   <><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6M14 11v6"/></>,
    stamp:   <><rect x="5" y="13" width="14" height="3" rx="1"/><rect x="8" y="3" width="8" height="10" rx="1"/><path d="M5 19h14"/><polyline points="9 8 11 10 15 6"/></>,
    postage: <><rect x="3" y="3" width="18" height="18" rx="1" strokeDasharray="2 2"/><rect x="6" y="6" width="12" height="12" rx="1"/></>,
    seal:    <><path d="M12 2l2.4 3.2 3.8-1.2-1 3.9 3.5 2-2.8 2.8 1.5 3.7-4-.4-1.4 3.8-3-2.6-3 2.6-1.4-3.8-4 .4 1.5-3.7L2 10l3.5-2-1-3.9 3.8 1.2z"/><circle cx="12" cy="12" r="4"/></>,
    triedoc: <><rect x="3" y="2" width="13" height="18" rx="2"/><path d="M7 8 Q9 6.5 11 8 Q13 9.5 14 8" fill="none"/><path d="M7 12 Q9 10.5 11 12 Q13 13.5 14 12" fill="none"/><path d="M7 16 Q9 14.5 11 16 Q13 17.5 14 16" fill="none"/><circle cx="19" cy="19" r="4" strokeWidth="0" fill="currentColor"/><polyline points="17 19 18.5 20.5 21.5 17" stroke="white" strokeWidth="1.8" fill="none" strokeLinecap="round" strokeLinejoin="round"/></>,
    tried: <>
      {/* Outer dashed circle */}
      <circle cx="12" cy="12" r="10" strokeDasharray="2.5 1.5" strokeWidth="1.5"/>
      {/* Inner circle */}
      <circle cx="12" cy="12" r="7" strokeWidth="1"/>
      {/* Checkmark */}
      <polyline points="8.5 12 11 14.5 15.5 9.5" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
      {/* Stars */}
      <path d="M12 2.5l.3.6h.7l-.6.4.3.7-.7-.5-.7.5.3-.7-.6-.4h.7z" strokeWidth="0.5"/>
      <path d="M12 21.5l.3.6h.7l-.6.4.3.7-.7-.5-.7.5.3-.7-.6-.4h.7z" strokeWidth="0.5"/>
    </>,
    gear:    <><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></>,
  }[name];
  return (
    <svg width={size} height={size} viewBox="0 0 24 24" fill={fill}
         stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"
         style={{ flexShrink: 0, ...style }}>
      {p}
    </svg>
  );
}

// ─────────────────────────────────────────────────────────────
// Pill — single source of truth for chips
// ─────────────────────────────────────────────────────────────
function Pill({ active, accent, small, onClick, onRemove, leading, children, title }) {
  const h = small ? 28 : 32;
  const bg = active ? T.selBg : accent ? T.accentBg : '#fff';
  const fg = active ? T.selFg : accent ? T.accent : T.faintInk;
  const bd = active ? T.selBg : accent ? T.accentBd : T.border;
  return (
    <button title={title} onClick={onClick} style={{
      all: 'unset', cursor: onClick ? 'pointer' : 'default',
      height: h, padding: small ? '0 10px' : '0 12px',
      borderRadius: 999, background: bg, color: fg,
      border: `1px solid ${bd}`,
      display: 'inline-flex', alignItems: 'center', gap: 6,
      fontSize: small ? 12 : 13, fontWeight: 500,
      whiteSpace: 'nowrap', flexShrink: 0,
      transition: 'background .15s ease, color .15s ease, border-color .15s ease',
    }}>
      {leading}
      <span>{children}</span>
      {onRemove && (
        <span onClick={(e) => { e.stopPropagation(); onRemove(); }} style={{
          display: 'inline-flex', alignItems: 'center', marginLeft: 2, opacity: 0.75,
        }}>
          <Icon name="close" size={11} />
        </span>
      )}
    </button>
  );
}

// ─────────────────────────────────────────────────────────────
// Place card — used by both mobile (full width) and web (grid)
// variants: 'wide' (mobile, image up top) | 'grid' (web)
// ─────────────────────────────────────────────────────────────
function placePrice(place) {
  let h = 0;
  for (let i = 0; i < place.id.length; i++) h = (h * 31 + place.id.charCodeAt(i)) >>> 0;
  const idx = h % 4;
  const bands = {
    eat:  [[800, 1500], [1500, 3000], [3000, 6000], [6000, 12000]],
    do:   [[0, 1000], [1000, 2500], [2500, 5000], [5000, 12000]],
    stay: [[5000, 9000], [9000, 18000], [18000, 38000], [38000, 80000]],
  };
  const rates = { jpy: 1, usd: 0.0067, eur: 0.0062, gbp: 0.0053 };
  const syms = { jpy: '¥', usd: '$', eur: '€', gbp: '£' };
  const [curId, setCurId] = React.useState(() => { try { return localStorage.getItem('itrymo_currency') || 'jpy'; } catch { return 'jpy'; } });
  React.useEffect(() => {
    const handler = () => { try { setCurId(localStorage.getItem('itrymo_currency') || 'jpy'); } catch {} };
    window.addEventListener('itrymo_pref_change', handler);
    return () => window.removeEventListener('itrymo_pref_change', handler);
  }, []);
  const rate = rates[curId] || 1;
  const sym = syms[curId] || '¥';
  const isFree = place.bucket === 'do' && place.budget === 'free';
  const unit = isFree ? '' : ({ eat: '/ person', do: '/ ticket', stay: '/ night' }[place.bucket] || '');
  const fmt = (v) => curId === 'jpy' ? v.toLocaleString() : (v * rate).toFixed(0);

  // For Eat places use the actual eatBudget field
  if (place.bucket === 'eat') {
    if (place.eatBudget === 'budget')  return { label: `Under ${sym}${fmt(1500)}`, unit };
    if (place.eatBudget === 'mid')     return { label: `${sym}${fmt(1500)}–${sym}${fmt(3000)}`, unit };
    if (place.eatBudget === 'splurge') return { label: `${sym}${fmt(3000)}+`, unit };
  }

  // For Do places use the actual budget field
  if (place.bucket === 'do') {
    if (place.budget === 'free')    return { label: 'Free', unit };
    if (place.budget === 'mid')     return { label: `${sym}${fmt(1000)}–${sym}${fmt(3000)}`, unit };
    if (place.budget === 'high')    return { label: `${sym}${fmt(3000)}–${sym}${fmt(8000)}`, unit };
    if (place.budget === 'splurge') return { label: `${sym}${fmt(8000)}+`, unit };
  }

  // For Stay places use the tier field
  if (place.bucket === 'stay') {
    if (place.tier === 'savings') return { label: `${sym}${fmt(3000)}–${sym}${fmt(8000)}`, unit };
    if (place.tier === 'safe')    return { label: `${sym}${fmt(8000)}–${sym}${fmt(20000)}`, unit };
    if (place.tier === 'premium') return { label: `${sym}${fmt(20000)}+`, unit };
  }

  const [lo, hi] = (bands[place.bucket] || bands.eat)[idx];
  const label = `${lo === 0 ? 'Free' : sym + fmt(lo)}–${sym}${fmt(hi)}`;
  return { label, unit };
}

function useEscClose(onClose) {
  React.useEffect(() => {
    if (!onClose) return;
    const h = (e) => { if (e.key === 'Escape') onClose(); };
    document.addEventListener('keydown', h);
    return () => document.removeEventListener('keydown', h);
  }, [onClose]);
}

function PlaceCard({ place, variant = 'wide', onTag, activeTags, onOpen }) {
  const isGrid = variant === 'grid';
  const isList = variant === 'list';
  const fav = useIsFav(place.id);
  const [hover, setHover] = React.useState(false);
  const price = placePrice(place);
  const distLabel = place.distKm < 10
    ? `${place.distKm.toFixed(1)} km`
    : `${Math.round(place.distKm)} km`;
  const subLabel = ((GROUPS[place.bucket] || []).find((g) => g.id === place.sub) || {}).label || '';

  const cardA11y = onOpen ? {
    role: 'button', tabIndex: 0,
    'aria-label': `${place.name}, rated ${place.rating}, ${place.area}`,
    onKeyDown: (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onOpen(place); } },
  } : {};

  if (isList) {
    return (
      <div onClick={() => onOpen && onOpen(place)} {...cardA11y}
        onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
        style={{
        background: T.panel, borderRadius: 12,
        border: `1px solid ${T.borderSoft}`,
        padding: '12px 14px', flexShrink: 0,
        display: 'flex', alignItems: 'center', gap: 14,
        cursor: onOpen ? 'pointer' : 'default',
        boxShadow: hover && onOpen ? '0 4px 14px rgba(0,0,0,0.07)' : 'none',
        transition: 'box-shadow .15s ease',
      }}>
        <div style={{ flex: 1, minWidth: 0 }}>
          <div style={{ display: 'flex', alignItems: 'baseline', gap: 8 }}>
            <div style={{ fontSize: 15, fontWeight: 600, lineHeight: 1.2, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{place.name}</div>
          </div>
          <div style={{ fontSize: 12, color: T.muted, display: 'flex', alignItems: 'center', gap: 8, marginTop: 4 }}>
            <span><Icon name="star" size={11} color={T.accent} style={{ verticalAlign: -1 }}/> {place.rating.toFixed(1)}</span>
            <span>·</span>
            <span><Icon name="pin" size={11} style={{ verticalAlign: -1 }}/> {place.area}</span>
            <span>·</span>
            <span>{distLabel}</span>
          </div>
        </div>
        <div style={{ textAlign: 'right', flexShrink: 0 }}>
          <div style={{ fontSize: 14, fontWeight: 700, color: T.ink, whiteSpace: 'nowrap' }}>{price.label}</div>
          <div style={{ fontSize: 11, color: T.muted, whiteSpace: 'nowrap' }}>{price.unit}</div>
        </div>
        <button onClick={(e) => { e.stopPropagation(); toggleFav(place.id); }} title={fav ? 'Saved to favorites' : 'Save to favorites'} style={{
          all: 'unset', cursor: 'pointer', flexShrink: 0,
          width: 34, height: 34, borderRadius: 17, background: T.bg,
          display: 'flex', alignItems: 'center', justifyContent: 'center',
        }}>
          <Icon name="heart" size={15} color={fav ? '#E0533D' : T.faintInk} fill={fav ? '#E0533D' : 'none'} />
        </button>
      </div>
    );
  }

  return (
    <div onClick={() => onOpen && onOpen(place)} {...cardA11y}
      onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
      style={{
      background: T.panel, borderRadius: 16,
      border: `1px solid ${T.borderSoft}`,
      overflow: 'hidden', flexShrink: 0,
      display: 'flex', flexDirection: 'column',
      cursor: onOpen ? 'pointer' : 'default',
      transform: hover && onOpen ? 'translateY(-2px)' : 'none',
      boxShadow: hover && onOpen ? '0 10px 24px rgba(0,0,0,0.08)' : 'none',
      transition: 'transform .15s ease, box-shadow .15s ease',
    }}>
      <div style={{
        height: isGrid ? 140 : 120, flexShrink: 0,
        background: `repeating-linear-gradient(135deg, ${place.swatch} 0 14px, ${place.swatch}b3 14px 28px)`,
        position: 'relative',
      }}>
        <div style={{
          position: 'absolute', top: 10, left: 10,
          background: 'rgba(255,255,255,0.92)',
          padding: '4px 9px', borderRadius: 999,
          fontSize: 11, fontWeight: 600,
        }}>
          <span style={{ color: T.ink, fontWeight: 700 }}>{price.label}</span>
          <span style={{ color: T.muted, fontWeight: 500 }}> {price.unit}</span>
        </div>
        <button onClick={(e) => { e.stopPropagation(); toggleFav(place.id); }} title={fav ? 'Saved to favorites' : 'Save to favorites'} style={{
          all: 'unset', cursor: 'pointer',
          position: 'absolute', top: 10, right: 10,
          width: 30, height: 30, borderRadius: 15,
          background: 'rgba(255,255,255,0.92)',
          display: 'flex', alignItems: 'center', justifyContent: 'center',
        }}>
          <Icon name="heart" size={14}
                color={fav ? '#E0533D' : T.faintInk}
                fill={fav ? '#E0533D' : 'none'} />
        </button>
      </div>
      <div style={{ padding: '12px 14px 14px', display: 'flex', flexDirection: 'column', gap: 6 }}>
        <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 8 }}>
          <div style={{ fontSize: 15, fontWeight: 600, lineHeight: 1.2 }}>{place.name}</div>
          <div style={{ fontSize: 12, color: T.muted, whiteSpace: 'nowrap' }}>
            <Icon name="star" size={11} color={T.accent} style={{ verticalAlign: -1 }}/> {place.rating.toFixed(1)}
          </div>
        </div>
        <div style={{ fontSize: 12, color: T.muted, display: 'flex', alignItems: 'center', gap: 8 }}>
          <span><Icon name="pin" size={11} style={{ verticalAlign: -1 }}/> {place.area}</span>
          <span>·</span>
          <span>{distLabel}</span>
          <span>·</span>
          <span><Icon name="heart" size={11} style={{ verticalAlign: -1 }} /> {place.saved.toLocaleString()}</span>
        </div>
        {/* Show tags only when searching/filtering, or on non-grid variants */}
        {(!isGrid || (activeTags && activeTags.length > 0)) && (
          <div style={{ display: 'flex', flexWrap: 'wrap', gap: 4, marginTop: 4 }}>
            {place.tags.slice(0, 3).map((tg) => {
              const on = activeTags && activeTags.includes(tg);
              return (
                <span key={tg} onClick={(e) => { e.stopPropagation(); onTag && onTag(tg); }} style={{
                  cursor: onTag ? 'pointer' : 'default',
                  fontSize: 11, padding: '2px 8px', borderRadius: 999,
                  background: on ? T.selBg : T.bg,
                  color: on ? T.selFg : T.muted,
                  border: `1px solid ${on ? T.selBg : T.borderSoft}`,
                  fontWeight: 500,
                }}>{tg}</span>
              );
            })}
          </div>
        )}
      </div>
    </div>
  );
}

// ─────────────────────────────────────────────────────────────
// Filter + sort engine
// ─────────────────────────────────────────────────────────────
function applyFilters(places, { bucket, tags, wards, searchQuery, foodFilters, doFilters, stayFilters }) {
  const q = (searchQuery || '').trim().toLowerCase();
  return places.filter((p) => {
    if (bucket !== 'all' && p.bucket !== bucket) return false;
    // Text search — matches name, area, or any tag
    if (q) {
      const hay = (p.name + ' ' + (p.area || '') + ' ' + (p.tags || []).join(' ')).toLowerCase();
      if (!hay.includes(q)) return false;
    }
    if (tags && tags.length) {
      // OR semantics within tags — any match counts
      const hasAny = tags.some((t) => p.tags.includes(t));
      if (!hasAny) return false;
    }
    // Ward / area filter — OR across selected wards, case-insensitive
    if (wards && wards.length) {
      const areaLower = (p.area || '').toLowerCase();
      const inWard = wards.some((w) => (WARD_NEIGHBORHOODS_LOWER[w] || new Set()).has(areaLower));
      if (!inWard) return false;
    }
    // Shared "Travelling With" lives in foodFilters (companions + withFlags) for every bucket that has the fields
    if (foodFilters && p.companions) {
      const ff = foodFilters;
      if (ff.companions && !p.companions.includes(ff.companions)) return false;
      if (ff.withFlags && ff.withFlags.length && p.withFlags) {
        const ok = ff.withFlags.every((flag) => p.withFlags.includes(flag));
        if (!ok) return false;
      }
    }
    // Eat-only structured filters
    if (foodFilters && p.bucket === 'eat') {
      const ff = foodFilters;
      if (p.occasion) {
        if (ff.occasion === 'with' && p.occasion === 'none') return false;
        if (ff.occasion === 'none' && p.occasion === 'with') return false;
      }
      if (p.noExtra && ff.inclusions) {
        if (ff.inclusions.includes('noExtraCharges')  && !p.noExtra.charges) return false;
        if (ff.inclusions.includes('noDrinkRequired') && !p.noExtra.drink)   return false;
        if (ff.inclusions.includes('noTableCharge')   && !p.noExtra.table)   return false;
      }
      // Eat price filter using real eatBudget field
      if (ff.eatBudget && p.eatBudget && p.eatBudget !== ff.eatBudget) return false;
    }
    // Do-only structured filters
    if (doFilters && p.bucket === 'do') {
      const df = doFilters;
      if (df.budget && p.budget && p.budget !== df.budget) return false;
      if (df.env && p.env && p.env !== df.env) return false;
      if (df.energy && p.energy && p.energy !== df.energy) return false;
      if (df.envTypes && df.envTypes.length && p.envTypes) {
        const ok = df.envTypes.some((e) => p.envTypes.includes(e));
        if (!ok) return false;
      }
    }
    // Stay-only structured filters
    if (stayFilters && p.bucket === 'stay') {
      const sf = stayFilters;
      // Tier / Proximity — multi-select OR
      if (sf.tiers && sf.tiers.length && !sf.tiers.includes(p.tier)) return false;
      if (sf.proximity && sf.proximity.length && !sf.proximity.includes(p.proximity)) return false;
      // Must-haves / Nice-to-haves — require all selected
      if (sf.mustHaves && sf.mustHaves.length && p.mustHaves) {
        const ok = sf.mustHaves.every((m) => p.mustHaves.includes(m));
        if (!ok) return false;
      }
      if (sf.niceToHaves && sf.niceToHaves.length && p.niceToHaves) {
        const ok = sf.niceToHaves.every((m) => p.niceToHaves.includes(m));
        if (!ok) return false;
      }
    }
    return true;
  });
}

function sortPlaces(places, sortId) {
  const arr = places.slice();
  switch (sortId) {
    case 'nearest':  arr.sort((a, b) => a.distKm - b.distKm); break;
    case 'trending': arr.sort((a, b) => b.trend - a.trend); break;
    case 'top':      arr.sort((a, b) => b.rating - a.rating); break;
    case 'recommended':
    default:
      // Editorial + community blend — weighted mix of rating, trend, saves
      arr.sort((a, b) => (
        (b.rating * 0.4 + (b.trend / 100) * 0.3 + Math.log10(b.saved + 1) * 0.3) -
        (a.rating * 0.4 + (a.trend / 100) * 0.3 + Math.log10(a.saved + 1) * 0.3)
      ));
  }
  return arr;
}

// Helper — get all tag groups for a bucket (handles "all")
function getGroupsFor(bucket) {
  if (bucket === 'all' || bucket === 'itinerary') {
    // Build a flattened "all" view by concatenating all buckets'
    // groups but keep them grouped so the sheet still scans cleanly.
    const out = [];
    BUCKETS.forEach((b) => {
      if (b.id === 'all') return;
      (GROUPS[b.id] || []).forEach((g) => {
        out.push({ id: `${b.id}-${g.id}`, label: `${b.label} · ${g.label}`, tags: g.tags });
      });
    });
    return out;
  }
  return GROUPS[bucket] || [];
}

// Segmented control — used for Sort in both mobile + web
function Segmented({ options, value, onChange, size = 'md' }) {
  const small = size === 'sm';
  return (
    <div style={{
      background: '#F4EFE6', borderRadius: 10, padding: 3, display: 'grid',
      gridTemplateColumns: `repeat(${options.length}, 1fr)`, gap: 2,
      fontSize: small ? 11 : 12, fontWeight: 600,
    }}>
      {options.map((o) => {
        const on = o.id === value;
        return (
          <button key={o.id} onClick={() => onChange(o.id)} style={{
            all: 'unset', cursor: 'pointer',
            textAlign: 'center', padding: small ? '6px 0' : '8px 0', borderRadius: 8,
            background: on ? '#fff' : 'transparent',
            color: on ? T.ink : T.muted,
            boxShadow: on ? '0 1px 2px rgba(0,0,0,0.06)' : 'none',
          }}>{o.label}</button>
        );
      })}
    </div>
  );
}

Object.assign(window, {
  T, Icon, Pill, PlaceCard, applyFilters, sortPlaces, getGroupsFor, Segmented,
  getTagCounts, suggestTags, Highlight, TagSuggestion, useEscClose,
});

// ─────────────────────────────────────────────────────────────
// Autosuggest helpers
// ─────────────────────────────────────────────────────────────
function getTagCounts(places) {
  const m = new Map();
  places.forEach((p) => p.tags.forEach((t) => m.set(t, (m.get(t) || 0) + 1)));
  return m;
}

// Returns ranked flat list of {tag, group, groupId, count, active}.
// Prioritizes prefix matches > substring matches > popular tags.
function suggestTags(query, groups, tagCounts, activeTags, max = 14) {
  if (!query) return [];
  const q = query.trim().toLowerCase();
  if (!q) return [];
  const seen = new Set();
  const out = [];
  groups.forEach((g) => {
    g.tags.forEach((tag) => {
      if (seen.has(tag)) return;
      const lower = tag.toLowerCase();
      const idx = lower.indexOf(q);
      if (idx < 0) return;
      // Also match individual words (e.g. "soup" matches "Noodles & Soups")
      seen.add(tag);
      const wordStart = idx === 0 || /[\s&/.]/.test(lower[idx - 1]);
      const count = tagCounts.get(tag) || 0;
      const score = (idx === 0 ? 1000 : wordStart ? 500 : 100) - idx + Math.log10(count + 1) * 5;
      out.push({
        tag, group: g.label, groupId: g.id,
        count, score,
        active: activeTags.includes(tag),
      });
    });
  });
  return out.sort((a, b) => b.score - a.score).slice(0, max);
}

function Highlight({ text, query }) {
  if (!query) return text;
  const q = query.trim();
  if (!q) return text;
  const idx = text.toLowerCase().indexOf(q.toLowerCase());
  if (idx < 0) return text;
  return (
    <>
      {text.slice(0, idx)}
      <strong style={{ color: T.accent, fontWeight: 700 }}>
        {text.slice(idx, idx + q.length)}
      </strong>
      {text.slice(idx + q.length)}
    </>
  );
}

// One row in the suggestion list
function TagSuggestion({ s, query, onToggle, showGroup = true }) {
  return (
    <button onClick={() => onToggle(s.tag)} style={{
      all: 'unset', cursor: 'pointer', display: 'flex',
      width: '100%', boxSizing: 'border-box',
      padding: '8px 10px', borderRadius: 8, gap: 10, alignItems: 'center',
      background: s.active ? T.bg : 'transparent',
    }}>
      <div style={{
        width: 18, height: 18, borderRadius: 5,
        border: `1.5px solid ${s.active ? T.selBg : T.border}`,
        background: s.active ? T.selBg : '#fff',
        display: 'flex', alignItems: 'center', justifyContent: 'center',
        flexShrink: 0,
      }}>
        {s.active && <Icon name="check" size={11} color={T.selFg} />}
      </div>
      <div style={{ flex: 1, minWidth: 0 }}>
        <div style={{ fontSize: 13, fontWeight: 500, color: T.ink, lineHeight: 1.2,
                      overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
          <Highlight text={s.tag} query={query} />
        </div>
        {showGroup && (
          <div style={{ fontSize: 11, color: T.muted, marginTop: 2 }}>
            in {s.group}
          </div>
        )}
      </div>
      <div style={{ fontSize: 11, color: T.muted, whiteSpace: 'nowrap' }}>
        {s.count} {s.count === 1 ? 'place' : 'places'}
      </div>
    </button>
  );
}
