// 타운카 제휴업체 — 메인 앱 (TDS Mobile 재설계)
// 화면: home (메인) → category (카테고리별 리스트)

const { useState, useMemo, useEffect, useRef } = React;

// ────────────────────────────────────────────────────────────
// Tweaks 기본값
//   accent     : 선택/활성 강조 스타일 (그린 솔리드 / 그린 소프트 / 뉴트럴)
//   catLayout  : 카테고리 표현 (그리드 / 리스트)
//   banner     : 상단 배너 (카드 / 스포트라이트)
// ────────────────────────────────────────────────────────────
const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "accent": "neutral",
  "catLayout": "list",
  "banner": "card"
} /*EDITMODE-END*/;

// 강조 스타일 → CSS 변수 세트
const ACCENT_SETS = {
  'green-solid': {
    '--sel-bg': '#0BB53B', '--sel-fg': '#FFFFFF', '--sel-border': '#0BB53B',
    '--tab-active-fg': '#171719', '--tab-underline': '#0BB53B', '--tab-count-fg': '#0BB53B'
  },
  'green-soft': {
    '--sel-bg': '#F0FCF3', '--sel-fg': '#06842B', '--sel-border': 'rgba(11,181,59,0.32)',
    '--tab-active-fg': '#171719', '--tab-underline': '#0BB53B', '--tab-count-fg': '#0BB53B'
  },
  'neutral': {
    '--sel-bg': '#171719', '--sel-fg': '#FFFFFF', '--sel-border': '#171719',
    '--tab-active-fg': '#171719', '--tab-underline': '#171719', '--tab-count-fg': '#515157'
  }
};

// ────────────────────────────────────────────────────────────
// 카테고리 안내 카피
// ────────────────────────────────────────────────────────────
const INTROS = {
  'new-domestic': { title: '국산차', desc: '타운카 공식 인증 신차 전시장입니다. 타운카 제휴 전시장이 아닌 타 전시장에서 출고 시, 세제 혜택 제한 및 출고 지연 등 불편함이 발생할 수 있어요. 공식 할인 외 추가 할인은 제공되지 않으며, 가까운 전시장에 직접 내방해 개별 상담을 받아보시길 권장해요.', modalDesc: '타운카 공식 인증 신차 전시장입니다. 타운카 제휴 전시장이 아닌 타 전시장에서 출고 시, 세제 혜택 제한 및 출고 지연 등 불편함이 발생할 수 있어요.' },
  'new-imported': { title: '수입차', desc: '타운카 공식 인증 신차 전시장입니다. 타운카 제휴 전시장이 아닌 타 전시장에서 출고 시, 세제 혜택 제한 및 출고 지연 등 불편함이 발생할 수 있어요. 공식 할인 외 추가 할인은 제공되지 않으며, 가까운 전시장에 직접 내방해 개별 상담을 받아보시길 권장해요.', modalDesc: '타운카 공식 인증 신차 전시장입니다. 타운카 제휴 전시장이 아닌 타 전시장에서 출고 시, 세제 혜택 제한 및 출고 지연 등 불편함이 발생할 수 있어요.' },
  financing: { title: '할부 금융사', desc: '타운카 개인 영업용 차량에 대한 할부 상품을 취급하는 제휴 금융사예요. 타운카와 제휴되지 않은 타 금융사에서 할부 이용 시, 차량 출고 이후 할부금 전액 상환 요청 등의 불이익이 발생될 수 있어요. (타 금융사 이용 시, 타운카 개인 영업용 차량에 대한 할부 진행 가능 여부 사전 확인 필수)' },
  used: { title: '중고차', desc: '타운카 공식 인증 중고차 전시장입니다. 타운카 제휴 전시장이 아닌 타 전시장에서 출고 시, 세제 혜택 제한 및 출고 지연 등 불편함이 발생할 수 있어요. 해당 전시장에서 보유하고 있지 않은 타 상사 보유 매물도 무상으로 알선 가능해요.' },
  accident: { title: '사고 정비', desc: '사고 수리·정비를 믿고 맡길 수 있는 제휴 정비 업체예요. 사고 접수 및 견적은 업체 담당자에게 직접 문의해주세요. 타운카 제휴 정비소에서는 과잉 정비를 하지 않아요.' },
  service: { title: '일반 정비', desc: '소모품 교체, 고장 수리, 차량 점검 등 일반 정비를 믿고 맡길 수 있는 제휴 정비 업체예요. 타운카 제휴 정비소에서는 과잉 정비를 하지 않아요.' },
  inspection: { title: '자동차 검사', desc: '자동차 정기검사·차령연장 검사가 모두 가능한 TS 한국교통안전공단 지정 검사소예요. 한국교통안전공단 사이버검사소 홈페이지에서도 예약이 가능해요. 사전 예약 필수이며, 당일 예약은 불가할 수 있어요.' },
  detailing: { title: '세차·디테일링', desc: '출장·디테일링 세차, 흡연·악취 클리닝 등 클리닝 전문 업체예요. 담배 냄새 제거는 일반 세차 및 디테일링 세차로는 완벽하게 잡기 어려워, 흡연 클리닝 전문 업체 이용을 권장해요. 사전 예약 필수이며, 당일 예약은 불가할 수 있어요.' },
  custom: { title: '틴팅·복원·튜닝', desc: '틴팅·PPF·블랙박스 시공부터 실내·가죽 복원, 캠핑카 개조까지, 특수 시공을 책임지는 제휴 업체예요. 사전 예약 필수이며, 당일 예약은 불가할 수 있어요.' },
  gps: { title: 'GPS 설치', desc: 'GPS 트래킹이 가능한 단말기를 설치하는 제휴 업체예요. 사전 예약 필수이며, 당일 설치는 불가할 수 있어요.' },
  photo: { title: '차량 사진', desc: '내 차를 멋지게 담아 예약률을 높혀줄 사진 촬영 제휴 업체예요. 사전 예약 필수이며, 당일 예약은 불가할 수 있어요.' }
};

// 바텀시트 전용 — 서브분류별 안내 문구 (있으면 카테고리 공용 문구 대신 표시)
// 업체마다 취급 범위가 달라서 "~전문 업체" 같은 단정은 빼고 실용 안내만 넣는다
const SUB_INTROS = {
  wash: '사전 예약 필수이며, 당일 예약은 불가할 수 있어요.',
  detail: '사전 예약 필수이며, 당일 예약은 불가할 수 있어요.',
  cleaning: '사전 예약 필수이며, 당일 예약은 불가할 수 있어요.',
  tint: '사전 예약 필수이며, 당일 예약은 불가할 수 있어요.',
  interior: '사전 예약 필수이며, 당일 예약은 불가할 수 있어요.',
  camping: '사전 예약 필수이며, 당일 예약은 불가할 수 있어요.',
};

// 혜택 분류: 구매(차 살 때) vs 정비(차 쓸 때)
const PURCHASE_CATS = ['new-domestic', 'new-imported', 'used', 'financing'];
// 홈 카테고리 그룹 (출고 관련 / 정비 관련)
const CAT_GROUPS = [
{ id: 'purchase', title: '출고 제휴업체', cats: ['new-domestic', 'new-imported', 'used', 'financing'] },
{ id: 'service', title: '정비 제휴업체', cats: ['accident', 'service', 'inspection', 'detailing', 'custom', 'gps', 'photo'] }];

function benefitPartnersOf(seg) {
  return PARTNERS.filter((p) => (p.official || p.discount) && (
  seg === 'purchase' ? PURCHASE_CATS.includes(p.category) : !PURCHASE_CATS.includes(p.category)));
}

// ────────────────────────────────────────────────────────────
// 외부 페이지 풀스크린 시트
// 배너 클릭으로 partner 페이지 위에 promo 페이지를 iframe으로 띄움. X로 닫으면 즉시 복귀.
// webview navigate가 아니라 in-page overlay라 native 백 버튼 동작과 무관하게 안전함.
// ────────────────────────────────────────────────────────────
function ExternalSheet({ url, onClose }) {
  // 데스크탑에서 partner 페이지 480px 컬럼과 픽셀 정확히 겹치도록 정렬.
  // 핵심: position:fixed는 viewport(=window.innerWidth, 스크롤바 포함)를 referenceframe으로 쓰는데,
  // partner 컬럼은 body(=clientWidth, 스크롤바 제외) 안에서 centering됨. 차이 = 스크롤바 폭.
  // → 스크롤바 폭을 동적으로 측정해 wrapper의 width를 viewport - scrollbar로 맞추고 left:0 고정.
  // 또한 시트 활성 동안엔 body overflow:hidden + paddingRight로 layout shift 방지.
  const [active, setActive] = React.useState(null);
  const [closing, setClosing] = React.useState(false);
  const [sbw, setSbw] = React.useState(0);

  React.useEffect(() => {
    if (url && url !== active) {
      setActive(url);
      setClosing(false);
    } else if (!url && active && !closing) {
      setClosing(true);
      const t = setTimeout(() => {
        setActive(null);
        setClosing(false);
      }, 280);
      return () => clearTimeout(t);
    }
  }, [url, active, closing]);

  // 스크롤바 폭 측정 (마운트 전 + 리사이즈 시)
  React.useEffect(() => {
    const compute = () => {
      const w = window.innerWidth - document.documentElement.clientWidth;
      setSbw(Math.max(0, w));
    };
    compute();
    window.addEventListener('resize', compute);
    return () => window.removeEventListener('resize', compute);
  }, []);

  // body 스크롤 락은 의도적으로 두지 않음.
  // (이전엔 락을 걸었다 풀었는데, iOS Safari에서 position:fixed 해제 직후 본문 터치 스크롤이
  //  살아나지 않는 버그가 생겨서 원래 동작으로 복원. 시트가 viewport를 덮으므로 사용자가
  //  본문을 터치할 일이 없음 → 락 불필요.)

  if (!active) return null;
  const animation = closing
    ? 'tc-sheet-out-down 280ms cubic-bezier(0.32,0.72,0,1) both'
    : 'tc-sheet-in-up 320ms cubic-bezier(0.32,0.72,0,1) both';
  return (
    <div style={{
      // wrapper — viewport에서 스크롤바 폭만큼 뺀 너비로 잡아 body와 동일한 좌표계를 만듦.
      // 이렇게 하면 안쪽 margin:0 auto가 partner 페이지 컬럼과 같은 픽셀에 정렬됨.
      position: 'fixed', top: 0, bottom: 0,
      left: 0,
      width: `calc(100vw - ${sbw}px)`,
      zIndex: 1000,
      overflow: 'hidden',
      pointerEvents: 'none',
    }}>
      <div key={active} style={{
        width: '100%', maxWidth: 480, height: '100%',
        margin: '0 auto',
        background: '#fff',
        display: 'flex', flexDirection: 'column',
        pointerEvents: 'auto',
        animation,
        paddingTop: 'var(--safe-top, env(safe-area-inset-top, 0px))',
        boxSizing: 'border-box',
      }}>
        {/* 헤더 — X 닫기 + 타이틀 */}
        <div style={{
          height: 52, flexShrink: 0,
          display: 'flex', alignItems: 'center', padding: '0 8px',
          borderBottom: '0.5px solid ' + TC.line, background: '#fff',
        }}>
          <button onClick={onClose} aria-label="닫기" style={{
            all: 'unset', cursor: 'pointer',
            width: 44, height: 44,
            display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0,
          }}>
            <Icon name="x" size={24} color={TC.textStrong} stroke={2.2} />
          </button>
          <div style={{ flex: 1, textAlign: 'center', fontSize: 15, fontWeight: 700, color: TC.textStrong, letterSpacing: '-0.01em' }}>
            혜택 안내
          </div>
          <div style={{ width: 44 }} aria-hidden="true" />
        </div>
        {/* iframe — 시트 폭 그대로 사용 (이전 +17px 트릭은 모바일에서 우측 패딩이 잠식되는 부작용
             있어서 제거). 데스크탑 스크롤바는 자연스럽게 노출되며, 모바일은 iOS가 자동으로 숨김. */}
        <iframe src={active} title="혜택 안내 페이지" style={{
          flex: 1, width: '100%', border: 0, background: '#fff',
        }} />
      </div>
    </div>
  );
}

// ────────────────────────────────────────────────────────────
// 루트 앱
// ────────────────────────────────────────────────────────────
function App() {
  const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
  const [screen, setScreen] = useState('home');
  const [searchFrom, setSearchFrom] = useState('home');
  const [navDir, setNavDir] = useState('');
  const [benefitSeg, setBenefitSeg] = useState('purchase');
  const [activeGroup, setActiveGroup] = useState('purchase');
  const [catBenefit, setCatBenefit] = useState(false);
  const [activeCategory, setActiveCategory] = useState(null);
  const [activeSub, setActiveSub] = useState(null);
  const [detailId, setDetailId] = useState(null);
  const [externalUrl, setExternalUrl] = useState(null);
  const [toast, setToast] = useState({ msg: '', visible: false });

  // 컨텍스트 감지 — 앱 webview에서 열렸는지 여부.
  // ReactNativeWebView 주입 또는 URL ?app_header=hidden 파라미터(앱이 호출 시 붙임)로 판단.
  // 외부 웹(SEO용 towncar.co.kr/towncar-partners 또는 .dev 직접 접근) → 백 버튼 숨김.
  const isInApp = React.useMemo(() => {
    if (typeof window === 'undefined') return false;
    if (window.ReactNativeWebView) return true;
    try {
      const params = new URLSearchParams(window.location.search);
      if (params.get('app_header') === 'hidden') return true;
    } catch (e) {/* noop */}
    return false;
  }, []);

  // 강조 스타일 → CSS 변수 적용
  useEffect(() => {
    const root = document.documentElement;
    const set = ACCENT_SETS[t.accent] || ACCENT_SETS['green-solid'];
    Object.entries(set).forEach(([k, v]) => root.style.setProperty(k, v));
  }, [t.accent]);

  React.useLayoutEffect(() => {
    window.scrollTo(0, 0);
    requestAnimationFrame(() => window.scrollTo(0, 0));
    const t1 = setTimeout(() => window.scrollTo(0, 0), 80);
    const t2 = setTimeout(() => window.scrollTo(0, 0), 280);
    return () => {clearTimeout(t1);clearTimeout(t2);};
  }, [screen]);

  const showToast = (msg) => {
    setToast({ msg, visible: true });
    setTimeout(() => setToast((s) => ({ ...s, visible: false })), 1800);
  };

  const handleCardClick = (p) => setDetailId(p.id);
  const handleCopy = (text) => {
    if (navigator.clipboard) navigator.clipboard.writeText(text).catch(() => {});
    showToast('주소가 복사됐어요');
  };

  const detailPartner = PARTNERS.find((p) => p.id === detailId);

  const setNav = (dir) => {
    document.documentElement.dataset.nav = dir;
    const clear = () => {delete document.documentElement.dataset.nav;};
    return clear;
  };

  const openCategory = (cid, sub) => {
    setNavDir('forward');
    setActiveGroup((CAT_GROUPS.find((g) => g.cats.includes(cid)) || CAT_GROUPS[0]).id);
    setActiveCategory(cid);
    setActiveSub(sub || null);
    setCatBenefit(false);
    setScreen('category');
  };

  const goHome = () => {
    setNavDir('back');
    setScreen('home');
  };

  const openSearch = () => {
    setSearchFrom(screen);
    setNavDir('forward');
    setScreen('search');
  };

  const backFromSearch = () => {
    setNavDir('back');
    setScreen(searchFrom);
  };

  const openBenefits = (seg) => {
    const g = CAT_GROUPS.find((x) => x.id === seg) || CAT_GROUPS[0];
    setNavDir('forward');
    setActiveGroup(g.id);
    setActiveCategory(g.cats[0]);
    setActiveSub(null);
    setCatBenefit(true);
    setScreen('category');
  };

  // ───── 타운카 앱 웹뷰 브릿지 ─────
  // ReactNativeWebView에 메시지 전송.
  // 우리 페이지가 towncar.co.kr 내부 iframe으로 임베드된 경우(현재 운영 환경),
  // ReactNativeWebView는 최상위 window(towncar.co.kr)에만 있고 iframe은 cross-origin이라
  // 직접 접근 불가 → window.parent.postMessage로 한 단계 위로 보냄.
  // 부모 페이지(towncar.co.kr)에 forwarder 스크립트가 메시지를 받아 ReactNativeWebView로 재전송.
  const postToApp = React.useCallback((payload) => {
    if (typeof window === 'undefined') return false;
    const msg = JSON.stringify(payload);
    // 1) 직접 주입된 경우 (페이지가 iframe 아니라 웹뷰에서 직접 로드)
    if (window.ReactNativeWebView) {
      window.ReactNativeWebView.postMessage(msg);
      return true;
    }
    // 2) 동일 origin iframe — parent의 ReactNativeWebView 직접 호출
    try {
      if (window.parent && window.parent !== window && window.parent.ReactNativeWebView) {
        window.parent.ReactNativeWebView.postMessage(msg);
        return true;
      }
    } catch (e) { /* cross-origin */ }
    // 3) cross-origin iframe — parent에 postMessage로 전달
    // 부모(towncar.co.kr) 페이지가 'tc-partners' source를 받아서 ReactNativeWebView로 forward 해야 함
    try {
      if (window.parent && window.parent !== window) {
        window.parent.postMessage({ source: 'tc-partners', ...payload }, '*');
        return true;
      }
    } catch (e) { /* fallthrough */ }
    return false;
  }, []);

  // 웹뷰 닫기 — 홈에서의 X 버튼 또는 더 닫을 게 없는 안드로이드 백
  const closeWebView = React.useCallback(() => {
    if (!postToApp({ type: 'BACK' })) {
      // 일반 브라우저 fallback
      if (history.length > 1) history.back();
      else showToast('웹뷰를 닫습니다 (앱 전용)');
    }
  }, [postToApp]);

  // window.__onAppBack 등록 — 앱이 백 버튼을 누르면 이 함수를 먼저 호출.
  // useRef로 핸들러를 한 번만 등록하고 ref가 최신 state를 가리키게 함. (state 변경마다
  // 재등록하면 cleanup→re-mount 사이의 짧은 순간에 백을 누르면 __onAppBack이 undefined로
  // 보여 앱이 그냥 종료해버리는 race condition 발생함.)
  const backHandlerRef = React.useRef(() => {});
  React.useEffect(() => {
    backHandlerRef.current = () => {
      // 우선순위: 외부 시트 → 상세 모달 → 검색/카테고리 → 홈에서 웹뷰 닫기
      if (externalUrl) { setExternalUrl(null); return; }
      if (detailId) { setDetailId(null); return; }
      if (screen === 'search') { backFromSearch(); return; }
      if (screen === 'category') { goHome(); return; }
      postToApp({ type: 'BACK' });
    };
  }, [externalUrl, detailId, screen, postToApp]);

  // 한 번만 등록 (마운트~언마운트 동안 절대 undefined가 되지 않음)
  React.useEffect(() => {
    const onBack = () => backHandlerRef.current && backHandlerRef.current();
    window.__onAppBack = onBack;
    const onMessage = (e) => {
      const d = e && e.data;
      if (d && d.source === 'tc-app' && d.type === 'BACK') onBack();
    };
    window.addEventListener('message', onMessage);
    return () => {
      window.removeEventListener('message', onMessage);
      if (window.__onAppBack === onBack) delete window.__onAppBack;
    };
  }, []);

  return (
    <div className="tc-app-outer" style={{ minHeight: '100vh', background: '#fff', fontFamily: FF, WebkitFontSmoothing: 'antialiased', overflowX: 'clip', width: '100%', maxWidth: '100%' }}>
      <div style={{
        width: '100%', maxWidth: 480, margin: '0 auto', minHeight: '100vh', background: '#fff',
        position: 'relative', overflowX: 'clip'
      }}>
        <div key={screen} style={{
          animation: navDir ? (navDir === 'forward' ? 'tc-screen-in-right' : 'tc-screen-in-left') + ' 300ms cubic-bezier(0.32,0.72,0,1)' : 'none'
        }}>
          {screen === 'home' ?
          <HomeScreen
            tweaks={t}
            onOpenCategory={openCategory}
            onOpenBenefits={openBenefits}
            onOpenDetail={handleCardClick}
            onSearch={openSearch}
            onClose={isInApp ? closeWebView : null}
            onOpenExternal={setExternalUrl}
            logoSrc={isInApp ? null : 'assets/logomark.svg'} /> :
          screen === 'search' ?
          <SearchScreen
            onBack={backFromSearch}
            onOpenDetail={handleCardClick} /> :
          <CategoryScreen
            group={activeGroup}
            initialCategory={activeCategory}
            initialSub={activeSub}
            initialBenefit={catBenefit}
            onBack={goHome}
            onOpenDetail={handleCardClick}
            onSearch={openSearch}
            onSync={(s) => {setActiveCategory(s.category);setActiveSub(s.sub);setCatBenefit(s.benefitOnly);}} />
          }
        </div>

        <DetailModal
          partner={detailId ? detailPartner : null}
          onClose={() => setDetailId(null)}
          onCall={() => {}}
          onCopy={handleCopy}
          onDirections={() => {}} />

        <ExternalSheet url={externalUrl} onClose={() => setExternalUrl(null)} />

        <Toast message={toast.msg} visible={toast.visible} />
      </div>

      {/* Tweaks */}
      <TweaksPanel title="Tweaks">
        <TweakSection label="강조 스타일" />
        <TweakRadio
          label="선택·활성"
          value={t.accent}
          options={[
          { value: 'green-solid', label: '그린' },
          { value: 'green-soft', label: '소프트' },
          { value: 'neutral', label: '뉴트럴' }]
          }
          onChange={(v) => setTweak('accent', v)} />

        <TweakSection label="레이아웃" />
        <TweakRadio
          label="카테고리"
          value={t.catLayout}
          options={[
          { value: 'grid', label: '그리드' },
          { value: 'list', label: '리스트' }]
          }
          onChange={(v) => setTweak('catLayout', v)} />
        <TweakRadio
          label="상단 배너"
          value={t.banner}
          options={[
          { value: 'card', label: '카드' },
          { value: 'spotlight', label: '스포트라이트' }]
          }
          onChange={(v) => setTweak('banner', v)} />
      </TweaksPanel>
    </div>);

}

// ── 숫자 강조 — 배너 텍스트처럼 주기적으로 한 번씩 회전
function NumberSpin({ children, interval = 4200 }) {
  const [k, setK] = React.useState(0);
  React.useEffect(() => {
    const reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
    if (reduce) return;
    const id = setInterval(() => {if (!document.hidden) setK((v) => v + 1);}, interval);
    return () => clearInterval(id);
  }, [interval]);
  return (
    <span style={{ display: 'inline-block' }}>
      <span key={k} style={{ display: 'inline-block', color: TC.primary, animation: 'tc-banner-rise 560ms cubic-bezier(0.22,0.61,0.36,1)' }}>{children}</span>
    </span>);

}

// ────────────────────────────────────────────────────────────
// HOME 스크린
// ────────────────────────────────────────────────────────────
function HomeScreen({ tweaks, onOpenCategory, onOpenBenefits, onOpenDetail, onSearch, onClose, onOpenExternal, logoSrc }) {
  const counts = useMemo(() => {
    const out = {};
    CATEGORIES.forEach((c) => {out[c.id] = PARTNERS.filter((p) => p.category === c.id).length;});
    return out;
  }, []);
  const total = PARTNERS.length;
  const purchaseCount = useMemo(() => benefitPartnersOf('purchase').length, []);
  const serviceCount = useMemo(() => benefitPartnersOf('service').length, []);
  const [scrolled, setScrolled] = React.useState(false);
  const sentinelRef = React.useRef(null);
  const rootRef = React.useRef(null);
  const promoRef = React.useRef(null);
  const benefitRef = React.useRef(null);
  const introRef = React.useRef(null);
  const [mid, setMid] = React.useState(220);
  React.useLayoutEffect(() => {
    const measure = () => {
      const root = rootRef.current,sec = introRef.current;
      if (!root || !sec) return;
      const r = root.getBoundingClientRect();
      const b = sec.getBoundingClientRect();
      const m = Math.round(b.bottom - r.top);
      if (m > 0) setMid((prev) => prev === m ? prev : m);
    };
    measure();
    const t1 = setTimeout(measure, 120);
    const t2 = setTimeout(measure, 400);
    window.addEventListener('resize', measure);
    if (document.fonts && document.fonts.ready) document.fonts.ready.then(measure);
    return () => {clearTimeout(t1);clearTimeout(t2);window.removeEventListener('resize', measure);};
  }, []);
  React.useEffect(() => {
    const update = () => {
      const el = sentinelRef.current;
      const top = el ? el.getBoundingClientRect().top : -window.scrollY;
      setScrolled(top < 0);
    };
    let io;
    if (sentinelRef.current && window.IntersectionObserver) {
      io = new IntersectionObserver(([e]) => setScrolled(!e.isIntersecting), { threshold: 0 });
      io.observe(sentinelRef.current);
    }
    window.addEventListener('scroll', update, { passive: true });
    update();
    return () => {
      if (io) io.disconnect();
      window.removeEventListener('scroll', update);
    };
  }, []);

  return (
    <div ref={rootRef} style={{ display: 'flex', flexDirection: 'column', minHeight: '100vh', background: `linear-gradient(to bottom, #fff 0px, #fff ${Math.max(0, mid - 40)}px, #F2F2F4 ${mid + 30}px, #F2F2F4 100%)`, position: 'relative', paddingTop: 'var(--safe-top, env(safe-area-inset-top, 0px))' }}>
      <div ref={sentinelRef} style={{ height: 1 }} aria-hidden="true" />
      <PageHeader
        title=""
        align="between"
        leadingIcon={onClose ? "chevron-left" : null}
        onLeading={onClose || undefined}
        trailingIcon="search"
        onTrailing={onSearch}
        floating
        scrolled={scrolled}
        logoSrc={logoSrc} />

      {/* 인트로 — 큰 타이틀 카피 (여유있게) */}
      <div ref={introRef} style={{ position: 'relative', zIndex: 1, padding: "24px 24px 0" }}>
        <h1 style={{
          margin: 0, color: TC.textStrong,
          lineHeight: '38px', wordBreak: 'keep-all', fontSize: "28px", letterSpacing: "0px", fontWeight: "700",
          animation: 'tc-banner-rise 560ms cubic-bezier(0.22,0.61,0.36,1) both'
        }}>타운카에서<br />검증한 <NumberSpin>{total}개</NumberSpin>의 업체를<br />한 곳에 모았어요</h1>
      </div>

      {/* 차주 전용 특별 혜택관 — 좌우 2분할 카드 (흰 영역) */}
      <section ref={benefitRef} style={{ margin: '18px 16px 0', position: 'relative', zIndex: 1 }}>
        <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 }}>
          <BenefitBigCard
            title={'차량 구매\n혜택'}
            desc={purchaseCount + '곳'}
            img="https://cdn.prod.website-files.com/62e24a964a47bcd52ce89452/6a2259224340f9c5993d10b9_coinbag.avif"
            imgSize={70}
            onClick={() => onOpenBenefits('purchase')} />
          <BenefitBigCard
            title={'차량 정비\n혜택'}
            desc={serviceCount + '곳'}
            img="https://cdn.prod.website-files.com/62e24a964a47bcd52ce89452/6a22592134511041fc1fc5f4_fix.avif"
            imgSize={68}
            onClick={() => onOpenBenefits('service')} />
        </div>
      </section>

      {/* 진행 중인 프로모션 — 배너 캐러셀 */}
      <div style={{ paddingTop: 18, position: 'relative', zIndex: 1 }}>
        <div ref={promoRef}>
          <AdCarousel ads={ADS} variant={tweaks.banner} onAdClick={onOpenExternal} />
        </div>
      </div>

      <div style={{ flex: 1, width: '100%', position: 'relative', zIndex: 1 }}>
        {/* 카테고리 — 출고/정비 두 그룹 */}
        {CAT_GROUPS.map((g) => {
          const cats = g.cats.map((id) => CATEGORIES.find((c) => c.id === id)).filter(Boolean);
          const eyebrow = g.id === 'service' ? '안심하고 맡기세요' : '차량구매 예정이신가요?';
          return (
            <section key={g.id} style={{ margin: '18px 16px 0', position: 'relative' }}>
              <div style={{ background: '#fff', borderRadius: 22, overflow: 'hidden', position: 'relative', padding: tweaks.catLayout === 'grid' ? '18px 12px 14px' : '18px 0 0' }}>
                <span style={{ position: 'absolute', top: 0, left: 0, zIndex: 2, display: 'inline-flex', alignItems: 'center', height: 26, padding: '0 13px', background: TC.primary, color: '#fff', letterSpacing: '-0.01em', fontSize: "12px", fontWeight: "600", borderRadius: '22px 0 14px 0' }}>{eyebrow}</span>
                <div style={{ padding: tweaks.catLayout === 'grid' ? '16px 4px 8px' : '16px 16px 8px' }}>
                  <SectionTitle action={{ label: '전체 보기', onClick: () => onOpenCategory(cats[0].id) }}>{g.title}</SectionTitle>
                </div>
                {tweaks.catLayout === 'grid' ?
                <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 4, marginTop: 0 }}>
                    {cats.map((c) =>
                  <CategoryCard key={c.id} category={c} count={counts[c.id]} onClick={() => onOpenCategory(c.id)} />
                  )}
                  </div> :

                <div style={{ marginTop: 0 }}>
                    {cats.map((c, i) =>
                  <CategoryRow key={c.id} category={c} count={counts[c.id]} last={i === cats.length - 1} onClick={() => onOpenCategory(c.id)} />
                  )}
                  </div>
                }
              </div>
            </section>);

        })}

        <ListFooter />
        <TowncarFooter />
      </div>
    </div>);

}

// ── 추천 배너 캐러셀 (TDS 라이트 카드)
function AdCarousel({ ads, variant, inset = false, onAdClick }) {
  const [index, setIndex] = React.useState(0);
  const drag = useDragScroll();
  const trackRef = drag.ref;
  const rafRef = React.useRef(null);

  // 표준 native CSS scroll-snap에 맡김. JS 스냅 로직 제거 — iOS Safari·Chrome 모두
  // 자체 momentum + snap을 가장 자연스럽게 처리. handleScroll은 페이지네이션 인디케이터
  // 용으로만 현재 index를 추적.
  const handleScroll = () => {
    if (rafRef.current) return;
    rafRef.current = requestAnimationFrame(() => {
      rafRef.current = null;
      const el = trackRef.current;
      if (!el) return;
      const first = el.children[0];
      const w = first ? first.offsetWidth + 12 : el.clientWidth;
      const i = Math.round(el.scrollLeft / w);
      setIndex((prev) => prev === i ? prev : i);
    });
  };
  const goTo = (i) => {
    pauseRef.current = Date.now() + 6000;
    const el = trackRef.current;
    if (!el) return;
    const child = el.children[i];
    const pad = parseFloat(getComputedStyle(el).paddingLeft) || 0;
    if (child) el.scrollTo({ left: child.offsetLeft - pad, behavior: 'smooth' });else
    el.scrollTo({ left: el.clientWidth * i, behavior: 'smooth' });
  };

  // 6초마다 자동 이동 (사용자 조작 직후엔 8초간 멈춤 → 충분히 읽고 누를 수 있는 여유)
  const indexRef = React.useRef(0);
  indexRef.current = index;
  const pauseRef = React.useRef(0);
  React.useEffect(() => {
    if (ads.length < 2) return;
    const id = setInterval(() => {
      if (Date.now() < pauseRef.current) return;
      const el = trackRef.current;
      if (!el || document.hidden) return;
      const next = (indexRef.current + 1) % ads.length;
      const child = el.children[next];
      const pad = parseFloat(getComputedStyle(el).paddingLeft) || 0;
      if (child) el.scrollTo({ left: child.offsetLeft - pad, behavior: 'smooth' });
    }, 6000);
    return () => clearInterval(id);
  }, [ads.length]);

  return (
    <div style={{ paddingTop: inset ? 16 : 14, padding: "0px" }}>
      <div style={{ position: 'relative', margin: inset ? '0 -18px' : 0 }}>
        <div ref={trackRef} onScroll={handleScroll}
          onPointerDown={() => { pauseRef.current = Date.now() + 8000; }}
          {...drag.handlers} style={{ ...{
            // 표준 native scroll-snap mandatory — 일반적인 터치 carousel과 동일.
            // iOS Safari·Chrome 모두 자체 momentum + snap을 자연스럽게 처리.
            display: 'flex', overflowX: 'auto', scrollSnapType: 'x mandatory',
            WebkitOverflowScrolling: 'touch', scrollbarWidth: 'none',
            padding: inset ? '0 18px' : '0 16px', gap: 12,
            scrollPaddingLeft: inset ? 18 : 16, cursor: 'grab',
            touchAction: 'pan-x',
          }, padding: "0px 16px" }} className="hide-scroll">
          {ads.map((ad, i) =>
          <AdSlide key={ad.id} ad={ad} variant={variant} inset={inset} active={i === index} badge={ads.length > 1 ? i + 1 + ' / ' + ads.length : null} onClick={() => {
            if (ad.url) {
              if (window.haptic) window.haptic.light();
              // webview navigate 대신 부모(App)에게 외부 URL을 알려서 풀스크린 시트로 띄움.
              // 이러면 partner 페이지 자체는 그대로 있어서 X 버튼으로 즉시 복귀 가능.
              if (onAdClick) onAdClick(ad.url);
            }
          }} />
          )}
        </div>
      </div>
    </div>);

}

// ── 추천 배너 한 장 (배경 이미지)
function AdSlide({ ad, variant, inset = false, active = false, badge = null, onClick }) {
  const spotlight = variant !== 'card';
  const hasImg = !!ad.image;
  const handleClick = (e) => {onClick && onClick();};
  const titleColor = hasImg ? '#fff' : TC.textStrong;
  const subColor = hasImg ? 'rgba(255,255,255,0.9)' : TC.textNeutral;
  return (
    <button onClick={handleClick} style={{
      all: 'unset', cursor: 'pointer', flexShrink: 0,
      width: inset ? 'calc(100% - 36px)' : 'calc(100% - 8px)', scrollSnapAlign: 'start', scrollSnapStop: 'always', boxSizing: 'border-box',
      borderRadius: 18, overflow: 'hidden', position: 'relative',
      background: hasImg ? '#222' : spotlight ? '#F0FCF3' : TC.bgAlt,
      minHeight: 160, display: 'flex', flexDirection: 'column', justifyContent: 'space-between'
    }}>
      {hasImg &&
      <div aria-hidden="true" style={{
        position: 'absolute', inset: 0,
        backgroundImage: `url("${ad.image}")`, backgroundSize: 'cover', backgroundPosition: 'center'
      }} />
      }
      {hasImg &&
      <div aria-hidden="true" style={{
        position: 'absolute', inset: 0,
        background: 'linear-gradient(90deg, rgba(0,0,0,0.62) 0%, rgba(0,0,0,0.34) 52%, rgba(0,0,0,0.06) 100%)'
      }} />
      }

      <div key={active ? 'on' : 'off'} style={{ position: 'relative', zIndex: 1, padding: '16px 16px 0' }}>
        <div style={{
          fontSize: 12.5, fontWeight: 600, color: subColor, letterSpacing: '-0.005em',
          animation: active ? 'tc-banner-rise 460ms cubic-bezier(0.22,0.61,0.36,1) both' : 'none'
        }}>{ad.eyebrow}</div>
        <div style={{
          marginTop: 3, fontSize: 19, fontWeight: 700, lineHeight: '25px',
          letterSpacing: '-0.025em', color: titleColor, wordBreak: 'keep-all',
          textShadow: hasImg ? '0 1px 8px rgba(0,0,0,0.28)' : 'none',
          animation: active ? 'tc-banner-rise 460ms 90ms cubic-bezier(0.22,0.61,0.36,1) both' : 'none'
        }}>{ad.headline}</div>
      </div>

      <div style={{ position: 'relative', zIndex: 1, padding: '0 16px 16px', display: 'flex', alignItems: 'center', justifyContent: 'flex-start', gap: 12 }}>
        {ad.body &&
        <div style={{
          fontSize: 12.5, color: subColor, letterSpacing: '-0.005em', lineHeight: '17px',
          wordBreak: 'keep-all', flex: 1, display: 'flex', alignItems: 'center', gap: 4,
          animation: active ? 'tc-banner-rise 460ms 160ms cubic-bezier(0.22,0.61,0.36,1) both' : 'none'
        }}>
            <Icon name="map-pin" size={12} color={hasImg ? 'rgba(255,255,255,0.85)' : TC.textAssist} stroke={2} style={{ flexShrink: 0 }} />
            {ad.body}
          </div>
        }
      </div>
      {badge &&
      <div style={{
        position: 'absolute', right: 12, bottom: 12, zIndex: 2,
        height: 22, padding: '0 9px', borderRadius: 999,
        background: 'rgba(0,0,0,0.42)', backdropFilter: 'blur(4px)', WebkitBackdropFilter: 'blur(4px)',
        display: 'inline-flex', alignItems: 'center',
        fontSize: 11, fontWeight: 600, color: '#fff', letterSpacing: '0.01em', fontVariantNumeric: 'tabular-nums'
      }}>{badge}</div>
      }
    </button>);

}

// ── 카테고리 카드 (그리드 · TDS Asset 타일)
function CategoryCard({ category, count, onClick }) {
  const [pressed, setPressed] = React.useState(false);
  return (
    <button onClick={onClick} style={{
      all: 'unset', cursor: 'pointer', position: 'relative', boxSizing: 'border-box',
      borderRadius: 16, padding: '14px 10px', background: pressed ? TC.fill : 'transparent',
      minHeight: 108, display: 'flex', flexDirection: 'column', justifyContent: 'space-between', gap: 16,
      transition: 'transform 120ms cubic-bezier(0.22,0.61,0.36,1), background 120ms',
      transform: pressed ? 'scale(0.975)' : 'scale(1)'
    }}
    onPointerDown={() => setPressed(true)}
    onPointerUp={() => setPressed(false)}
    onPointerLeave={() => setPressed(false)}
    onPointerCancel={() => setPressed(false)}>
      <AssetTile category={category.id} size={44} />
      <div>
        <div style={{ fontSize: 15, fontWeight: 700, color: TC.textStrong, letterSpacing: '-0.02em', lineHeight: '19px', marginBottom: 3 }}>{category.label}</div>
        <div style={{ fontSize: 12.5, color: TC.textAlt, fontWeight: 500, letterSpacing: '-0.005em' }}>{count}개 {category.unit}</div>
      </div>
    </button>);

}

// ── 카테고리 로우 (리스트 · TDS ListRow)
function CategoryRow({ category, count, onClick, last }) {
  const [pressed, setPressed] = React.useState(false);
  return (
    <button onClick={onClick} style={{
      all: 'unset', cursor: 'pointer', boxSizing: 'border-box', width: '100%', position: 'relative',
      display: 'flex', alignItems: 'center', gap: 13, padding: last ? '13px 16px 19px' : '13px 16px',
      background: pressed ? TC.fill : 'transparent',
      transition: 'background 100ms'
    }}
    onPointerDown={() => setPressed(true)}
    onPointerUp={() => setPressed(false)}
    onPointerLeave={() => setPressed(false)}
    onPointerCancel={() => setPressed(false)}>
      <AssetTile category={category.id} size={42} />
      <div style={{ flex: 1, minWidth: 0 }}>
        <div style={{ fontSize: 16, fontWeight: 600, color: TC.textStrong, letterSpacing: '-0.02em' }}>{category.label}</div>
        <div style={{ marginTop: 2, fontSize: 13, fontWeight: 500, color: TC.textAlt, letterSpacing: '-0.005em' }}>{count}개 {category.unit}</div>
      </div>
      <span style={{
        flexShrink: 0, display: 'inline-flex', alignItems: 'center',
        height: 30, padding: '0 13px', borderRadius: 999,
        background: TC.fill, color: TC.textNeutral,
        fontSize: 13, fontWeight: 600, letterSpacing: '-0.005em', pointerEvents: 'none'
      }}>보기</span>
      {!last &&
      <span style={{ position: 'absolute', left: 16, right: 16, bottom: 0, height: 1, background: TC.lineSoft }} />
      }
    </button>);

}

// ── 공식 리테일러 카드 (가로 스크롤)
function OfficialCard({ partner, onClick }) {
  const handleClick = (e) => {
    if (onClick) onClick();
  };
  return (
    <button onClick={handleClick} style={{
      all: 'unset', cursor: 'pointer', flexShrink: 0, boxSizing: 'border-box',
      width: 220, padding: 16, borderRadius: 18, background: TC.bgAlt,
      display: 'flex', flexDirection: 'column', gap: 12
    }}>
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
        <OfficialBadge />
      </div>
      <div>
        <div style={{ fontSize: 11.5, fontWeight: 600, color: TC.textAlt, letterSpacing: '0.01em', marginBottom: 3 }}>{partner.brand}</div>
        <div style={{ fontSize: 16, fontWeight: 700, color: TC.textStrong, letterSpacing: '-0.02em', lineHeight: '21px' }}>{partner.name}</div>
      </div>
      <div style={{ fontSize: 12.5, color: TC.textNeutral, letterSpacing: '-0.005em', lineHeight: '17px', display: 'flex', alignItems: 'center', gap: 4, minWidth: 0 }}>
        <Icon name="map-pin" size={12} color={TC.textAssist} stroke={2} style={{ flexShrink: 0 }} />
        <span style={{ flex: 1, minWidth: 0, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{partner.address}</span>
      </div>
    </button>);

}

// ── 공식 브랜드 로고 (URL 채우면 자동 표시, 없으면 브랜드명 플레이스홀더)
const BRAND_LOGOS = {






























































































































































  // audi: 'https://.../audi.png',
  // landrover: 'https://.../landrover.png',
}; // ── 공식 협력 브랜드 로우 (브랜드 단위 · 누르면 해당 브랜드 리스트로)
function OfficialBrandRow({ brand, onClick, last }) {const [pressed, setPressed] = React.useState(false);const logo = BRAND_LOGOS[brand.id];return <button onClick={onClick} style={{ all: 'unset', cursor: 'pointer', boxSizing: 'border-box', width: '100%', position: 'relative', display: 'flex', alignItems: 'center', gap: 13, padding: last ? '13px 16px 19px' : '13px 16px', background: pressed ? TC.bgAlt : '#fff', transition: 'background 100ms' }} onPointerDown={() => setPressed(true)} onPointerUp={() => setPressed(false)} onPointerLeave={() => setPressed(false)} onPointerCancel={() => setPressed(false)}>
      <div style={{ flexShrink: 0, width: 42, height: 42, borderRadius: 12, background: '#fff', border: '1px solid ' + TC.line, overflow: 'hidden', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
        {logo ? <img src={logo} alt={brand.label} style={{ width: '100%', height: '100%', objectFit: 'contain' }} /> : <span style={{ fontSize: 11, fontWeight: 800, color: TC.textNeutral, letterSpacing: '-0.02em' }}>{brand.label}</span>}
      </div>
      <div style={{ flex: 1, minWidth: 0 }}>
        <div style={{ fontSize: 16, fontWeight: 700, color: TC.textStrong, letterSpacing: '-0.02em', display: 'flex', alignItems: 'center', gap: 6 }}>
          {brand.label}
          <OfficialBadge />
        </div>
        <div style={{ marginTop: 2, fontSize: 13, fontWeight: 500, color: TC.textAlt, letterSpacing: '-0.005em' }}>{brand.count}개 {brand.unit}</div>
      </div>
      <span style={{ flexShrink: 0, display: 'inline-flex', alignItems: 'center', height: 30, padding: '0 13px', borderRadius: 999, background: TC.bgAlt, color: TC.textNeutral, fontSize: 13, fontWeight: 600, letterSpacing: '-0.005em', pointerEvents: 'none' }}>보기</span>
      {!last && <span style={{ position: 'absolute', left: 16, right: 16, bottom: 0, height: 1, background: TC.lineSoft }} />}
    </button>;} // ── 섹션 타이틀
function SectionTitle({ children, action, ml = 8 }) {return <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', padding: '0 4px' }}>
      <h2 style={{ fontWeight: 700, color: TC.textStrong, letterSpacing: '-0.02em', fontSize: "18px", margin: 0 }}>{children}</h2>
      {action && <button onClick={action.onClick} style={{ all: 'unset', cursor: 'pointer', fontSize: 13, fontWeight: 600, color: TC.textAlt, letterSpacing: '-0.005em', display: 'inline-flex', alignItems: 'center', gap: 2 }}>
          {action.label}
        </button>}
    </div>;} // ── 푸터
// ── 타운카 메인 푸터 (모바일 전용 — 회사 정보 + 약관 4개)
// 배경은 #fff — iOS 시스템 영역(홈 인디케이터)과 색이 연결되도록.
// ListFooter(도와드릴게요)는 그 위에서 body의 회색(#F2F2F4) 그대로.
function TowncarFooter() {
  const termStyle = { fontSize: 12, color: '#4E5968', textDecoration: 'none', letterSpacing: '-0.005em', fontWeight: 600 };
  return (
    <div style={{ background: '#fff', fontFamily: FF, padding: '64px 24px 36px' }}>
      <img
        src="https://cdn.prod.website-files.com/62e24a964a47bcd52ce89452/6363909230032c015e9f2af3_%E1%84%90%E1%85%A1%E1%84%8B%E1%85%AE%E1%86%AB%E1%84%8F%E1%85%A1%20%E1%84%80%E1%85%B5%E1%84%87%E1%85%A9%E1%86%AB%20%E1%84%85%E1%85%A9%E1%84%80%E1%85%A9.png"
        alt="타운카 로고"
        style={{ width: 88, height: 'auto', display: 'block', marginBottom: 18 }}
      />
      <div style={{ fontSize: 12, color: '#6B7684', lineHeight: '19px', letterSpacing: '-0.005em', wordBreak: 'keep-all' }}>
        대표자 : 최윤진<br />
        주소 : 경기도 하남시 검단산로 239, 4층 407호(창우동, 하남시벤처센터) (주)타운즈<br />
        사업자 등록번호 : 875-86-01580<br />
        대표번호 : 1833 - 4155
      </div>
      <div style={{ marginTop: 16, fontSize: 12, color: '#8B95A1', lineHeight: '17px', letterSpacing: '-0.005em', wordBreak: 'keep-all' }}>
        © TOWNZ INC. ALL RIGHTS RESERVED.<br />
        주식회사 타운즈는 통신판매중개업자로서 직접 거래 당사자가 아니며, 판매자 소유 차량의 정보·상태·보험·사고 및 거래에 대한 책임은 판매자와 이용자에게 있습니다.
      </div>
      <div style={{ marginTop: 22, display: 'flex', flexDirection: 'column', gap: 12 }}>
        {[
          ['https://www.towncar.co.kr/terms/doc-0806', '이용약관'],
          ['https://townz.notion.site/206f40c28e6d801196a9f723c5a80acd', '개인정보처리방침'],
          ['https://townz.notion.site/05df5d397c474080b9c92f5d6c66007c', '자동차대여약관'],
          ['https://townz.notion.site/206f40c28e6d80b1978cdb5a890fa6a7?source=copy_link', '위치정보 이용약관'],
        ].map(([url, label]) => (
          <a key={url} href={url} target="_blank" rel="noopener noreferrer"
            onClick={(e) => { e.preventDefault(); window.tcOpenExternal && window.tcOpenExternal(url); }}
            style={termStyle}>{label}</a>
        ))}
      </div>
    </div>
  );
}

function ListFooter() {return <div style={{ padding: '96px 24px 96px', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 1, fontFamily: FF }}>
      <div style={{ fontSize: 11, color: TC.textAlt, letterSpacing: '-0.005em', textAlign: 'center', lineHeight: '16px' }}>
        제휴업체 입점·정보 수정 문의
      </div>
      <div style={{ fontSize: 11, color: TC.textAlt, letterSpacing: '-0.005em', textAlign: 'center', lineHeight: '16px', wordBreak: 'keep-all', maxWidth: 280 }}>
        <a href="https://towncar.channel.io/home" target="_blank" rel="noopener noreferrer"
          onClick={(e) => { e.preventDefault(); window.tcOpenExternal && window.tcOpenExternal('https://towncar.channel.io/home'); }}
          style={{ color: TC.textNeutral, fontWeight: 600, textDecoration: 'underline', textUnderlineOffset: '2px' }}>타운카 고객센터</a>로 알려주시면 빠르게 도와드릴게요
      </div>
    </div>;} // ── 탭 본문 (스와이프 페이저 패널 · 검색/서브필터 지원)
function CatBody({ tabId, group, sub, search, onOpenDetail }) {
  const groupDef = CAT_GROUPS.find((g) => g.id === group) || CAT_GROUPS[0];
  const benefit = tabId === '__benefit';
  const q = (search || '').trim().toLowerCase();
  let list,grouped = null;
  if (benefit) {
    list = PARTNERS.filter((p) => groupDef.cats.includes(p.category) && (p.official || p.discount));
  } else {
    list = PARTNERS.filter((p) => p.category === tabId);
    if (sub) list = list.filter((p) => p.sub === sub);
  }
  if (q) list = list.filter((p) => p.name.toLowerCase().includes(q) || (p.address || '').toLowerCase().includes(q) || (p.brand || '').toLowerCase().includes(q) || (p.area || '').toLowerCase().includes(q));

  if (benefit && !q) {
    const map = new Map();
    list.forEach((p) => {const label = p.brand || (CATEGORIES.find((c) => c.id === p.category) || {}).label || '기타';if (!map.has(label)) map.set(label, { brand: { label }, items: [] });map.get(label).items.push(p);});
    grouped = [...map.values()].filter((g) => g.items.length);
  } else if (!benefit && !sub && !q) {
    const spec = tabId === 'new-domestic' ? DOMESTIC_BRANDS : tabId === 'new-imported' ? IMPORTED_BRANDS : tabId === 'financing' ? FINANCING_SUBS : tabId === 'detailing' ? DETAILING_SUBS : tabId === 'custom' ? CUSTOM_SUBS : null;
    if (spec) {const map = new Map();spec.forEach((b) => map.set(b.id, { brand: b, items: [] }));list.forEach((p) => {const g = map.get(p.sub);if (g) g.items.push(p);});grouped = [...map.values()].filter((g) => g.items.length);}
  }
  const cat = CATEGORIES.find((c) => c.id === tabId);
  const intro = !benefit && (!sub || tabId === 'financing') && !q && INTROS[tabId];
  const subLabel = sub ? ([...NEW_BRANDS, ...ETC_SUBS, ...FINANCING_SUBS].find((x) => x.id === sub) || {}).label : null;
  return (
    <div>
      <div style={{ padding: '10px 16px 12px', background: '#fff', fontSize: 13, color: TC.textNeutral, letterSpacing: '-0.005em', display: 'flex', alignItems: 'center', gap: 4 }}>
        <span>
        {benefit ?
        <>타운카 혜택 <strong style={{ fontWeight: 700, color: TC.textStrong }}>{list.length}곳</strong> · {groupDef.title}</> :
        sub ?
        <><span style={{ color: TC.textAlt }}>{subLabel}</span>{' · '}<strong style={{ fontWeight: 700, color: TC.textStrong }}>{list.length}개 {cat?.unit || ''}</strong></> :
        <>총 <strong style={{ fontWeight: 700, color: TC.textStrong }}>{list.length}개</strong> {cat?.unit || '제휴 업체'}</>}
        </span>
        {intro && <InfoTooltip text={intro.desc} />}
      </div>
      {list.length === 0 ?
      <EmptyState
        icon={q ? 'search-x' : tabId === 'photo' ? 'camera' : 'search-x'}
        title={q ? '검색 결과가 없어요' : tabId === 'photo' ? '곧 만나요' : '등록된 업체가 없어요'}
        subtitle={q ? '다른 키워드로 검색해보세요' : tabId === 'photo' ? '현재 제휴 업체를 모집하고 있어요' : '다른 카테고리를 선택해주세요'} /> :
      grouped ?
      grouped.map((g) =>
      <div key={g.brand.label}>
              <GroupHeader label={g.brand.label} count={g.items.length} />
              {g.items.map((p, i) => <PartnerCard key={p.id} partner={p} brandLabel={null} showAsset showBenefit={benefit} last={i === g.items.length - 1} onClick={() => onOpenDetail(p)} />)}
            </div>) :
      list.map((p, i) =>
      <PartnerCard key={p.id} partner={p} showAsset brandLabel={(tabId === 'new-domestic' || tabId === 'new-imported') && !sub ? p.brand : null} showBenefit={benefit} last={i === list.length - 1} onClick={() => onOpenDetail(p)} />)}
      {(list.length > 0 || tabId === 'photo') &&
      <div style={{ borderTop: '1px solid ' + TC.line, background: '#fff' }}>
          <ListFooter />
        </div>}
    </div>);

}
// ────────────────────────────────────────────────────────────
// 카테고리 스크린
// ────────────────────────────────────────────────────────────
function CategoryScreen({ group, initialCategory, initialSub, initialBenefit, onBack, onOpenDetail, onSearch, onSync }) {const groupDef = CAT_GROUPS.find((g) => g.id === group) || CAT_GROUPS[0];const groupCats = useMemo(() => groupDef.cats.map((id) => CATEGORIES.find((c) => c.id === id)).filter(Boolean), [group]);const benefitCount = useMemo(() => PARTNERS.filter((p) => (p.official || p.discount) && groupDef.cats.includes(p.category)).length, [group]);const [category, setCategory] = useState(initialCategory || groupDef.cats[0]);const [sub, setSub] = useState(initialSub || null);const [search, setSearch] = useState('');const [benefitOnly, setBenefitOnly] = useState(!!initialBenefit);const [catDir, setCatDir] = useState('next');const [dragX, setDragX] = useState(0);const [dragging, setDragging] = useState(false);const pagerRef = useRef(null);const [pagerW, setPagerW] = useState(448);React.useEffect(() => {const measure = () => {if (pagerRef.current) setPagerW(pagerRef.current.clientWidth);};measure();const t = setTimeout(measure, 200);window.addEventListener('resize', measure);return () => {clearTimeout(t);window.removeEventListener('resize', measure);};}, [sub, search, benefitOnly]);const tabsIds = ['__benefit', ...groupCats.map((c) => c.id)];const activeIndex = benefitOnly ? 0 : Math.max(0, tabsIds.indexOf(category));const counts = useMemo(() => {const out = {};CATEGORIES.forEach((c) => {out[c.id] = PARTNERS.filter((p) => p.category === c.id).length;});return out;}, []);const listRef = useRef(null);const resetListScroll = React.useCallback(() => {if (listRef.current) listRef.current.scrollTop = 0;}, []);React.useLayoutEffect(() => {resetListScroll();requestAnimationFrame(() => {resetListScroll();requestAnimationFrame(resetListScroll);});const t1 = setTimeout(resetListScroll, 80);const t2 = setTimeout(resetListScroll, 280);const t3 = setTimeout(resetListScroll, 500);return () => {clearTimeout(t1);clearTimeout(t2);clearTimeout(t3);};}, [category, sub, search, resetListScroll]);const firstCatRun = useRef(true);useEffect(() => {if (firstCatRun.current) {firstCatRun.current = false;return;}setSub(null);}, [category]);const switchCategory = (next) => {if (!next || next === category) {setBenefitOnly(false);return;}const cur = CATEGORIES.findIndex((c) => c.id === category);const nxt = CATEGORIES.findIndex((c) => c.id === next);setCatDir(nxt >= cur ? 'next' : 'prev');if (window.haptic) window.haptic.selection();setBenefitOnly(false);setCategory(next);};const onTab = (id) => {if (id === '__benefit') {if (benefitOnly) return;setCatDir('prev');if (window.haptic) window.haptic.selection();setBenefitOnly(true);} else {if (benefitOnly) setCatDir('next');switchCategory(id);}};const swipeRef = useRef(null);const onSwipeStart = (e) => {if (e.pointerType === 'mouse') return;if (sub || search.trim()) {swipeRef.current = null;return;}swipeRef.current = { x: e.clientX, y: e.clientY, t: performance.now(), locked: null };};
  const onSwipeMove = (e) => {
    const st = swipeRef.current;if (!st) return;
    const dx = e.clientX - st.x,dy = e.clientY - st.y;
    // 잠금: 8px 이상 움직이고, 가로가 세로보다 명확히 클 때만(adx > ady * 1.4) 가로로 잠금.
    // 그 외(거의 모든 일반 스크롤)는 세로로 잠금 → preventDefault 없이 native 세로 스크롤 통과.
    if (st.locked === null) {
      const adx = Math.abs(dx), ady = Math.abs(dy);
      if (adx > 8 || ady > 8) {
        if (adx > 8 && adx > ady * 1.4) st.locked = 'h';
        else st.locked = 'v';
      }
    }
    if (st.locked === 'h') {
      if (!dragging) setDragging(true);
      const ids = ['__benefit', ...groupCats.map((c) => c.id)];
      const ai = benefitOnly ? 0 : Math.max(0, ids.indexOf(category));
      let d = dx;
      // 마지막 탭의 좌측 스와이프(d<0)는 갈 곳이 없어 저항감만 줌.
      if (ai === ids.length - 1 && d < 0) d = d * 0.32;
      setDragX(d);
      if (e.cancelable) e.preventDefault();
    }
  };
  const onSwipeEnd = (e) => {
    const st = swipeRef.current;swipeRef.current = null;
    if (!st || st.locked !== 'h') {setDragging(false);setDragX(0);return;}
    const dx = e.clientX - st.x;const dt = performance.now() - st.t;
    const W = pagerW || 1;
    const ids = ['__benefit', ...groupCats.map((c) => c.id)];
    const ai = benefitOnly ? 0 : Math.max(0, ids.indexOf(category));
    // 임계값 더 관대: 화면폭 8% 또는 700ms 안에 14px+ 플릭이면 전환
    const fast = dt < 700 && Math.abs(dx) > 14;
    const commit = Math.abs(dx) > W * 0.08 || fast;
    setDragging(false);
    // 첫 탭에서 우측 스와이프(dx>0) → 홈 복귀 (iOS 스와이프-뒤로가기 패턴)
    if (commit && dx > 0 && ai === 0 && onBack) {
      if (window.haptic) window.haptic.light();
      setDragX(0);
      onBack();
      return;
    }
    if (commit && dx < 0 && ai < ids.length - 1) {if (window.haptic) window.haptic.selection();onTab(ids[ai + 1]);setDragX(0);} else
    if (commit && dx > 0 && ai > 0) {if (window.haptic) window.haptic.selection();onTab(ids[ai - 1]);setDragX(0);} else
    {setDragX(0);}
  };const prevFilteredCount = useRef(null);React.useEffect(() => {if (onSync) onSync({ category, sub, benefitOnly });}, [category, sub, benefitOnly]);const filtered = useMemo(() => {let list;if (benefitOnly) {// 혜택만: 그룹 전체에서 혜택 제공 업체 (탭 무관)
      list = PARTNERS.filter((p) => groupDef.cats.includes(p.category) && (p.official || p.discount));} else {list = PARTNERS.filter((p) => p.category === category);if (sub) list = list.filter((p) => p.sub === sub);}if (search.trim()) {const q = search.trim().toLowerCase();list = list.filter((p) => p.name.toLowerCase().includes(q) || (p.address || '').toLowerCase().includes(q) || (p.brand || '').toLowerCase().includes(q) || (p.area || '').toLowerCase().includes(q));}return list;}, [category, sub, search, benefitOnly]);useEffect(() => {if (!search.trim()) {prevFilteredCount.current = null;return;}
    const prev = prevFilteredCount.current;
    const next = filtered.length;
    if (prev != null && prev !== next) {
      if (prev === 0 && next > 0 && window.haptic) window.haptic.selection();else
      if (prev > 0 && next === 0 && window.haptic) window.haptic.warning();
    }
    prevFilteredCount.current = next;
  }, [filtered.length, search]);

  const subItems = useMemo(() => {
    if (category === 'new-domestic') return DOMESTIC_BRANDS.map((b) => ({ id: b.id, label: b.label }));
    if (category === 'new-imported') return IMPORTED_BRANDS.map((b) => ({ id: b.id, label: b.label }));
    if (category === 'financing') return FINANCING_SUBS;
    if (category === 'detailing') return DETAILING_SUBS;
    if (category === 'custom') return CUSTOM_SUBS;
    return null;
  }, [category]);

  const grouped = useMemo(() => {
    if (benefitOnly) {
      if (search.trim()) return null;
      const map = new Map();
      filtered.forEach((p) => {
        const label = p.brand || CATEGORIES.find((c) => c.id === p.category)?.label || '기타';
        if (!map.has(label)) map.set(label, { brand: { label }, items: [] });
        map.get(label).items.push(p);
      });
      return Array.from(map.values()).filter((g) => g.items.length > 0);
    }
    const groupSpec =
    category === 'new-domestic' ? DOMESTIC_BRANDS :
    category === 'new-imported' ? IMPORTED_BRANDS :
    category === 'financing' ? FINANCING_SUBS :
    category === 'detailing' ? DETAILING_SUBS :
    category === 'custom' ? CUSTOM_SUBS : null;
    if (!groupSpec || sub || search.trim()) return null;
    const map = new Map();
    groupSpec.forEach((b) => map.set(b.id, { brand: b, items: [] }));
    filtered.forEach((p) => {const g = map.get(p.sub);if (g) g.items.push(p);});
    return Array.from(map.values()).filter((g) => g.items.length > 0);
  }, [category, sub, search, filtered]);

  const intro = INTROS[category];
  const curCat = CATEGORIES.find((c) => c.id === category);

  // CategoryScreen 마운트 동안 body 스크롤 잠금 — iOS Safari에서 상단(헤더/탭/검색바)을
  // 터치해서 위로 끌 때 body가 같이 스크롤되면서 뒤로가기 버튼이 상태바 뒤로 사라지는 현상 방지.
  React.useEffect(() => {
    const prevHtmlOverflow = document.documentElement.style.overflow;
    const prevBodyOverflow = document.body.style.overflow;
    const prevBodyPosition = document.body.style.position;
    const prevBodyWidth = document.body.style.width;
    document.documentElement.style.overflow = 'hidden';
    document.body.style.overflow = 'hidden';
    document.body.style.position = 'fixed';
    document.body.style.width = '100%';
    return () => {
      document.documentElement.style.overflow = prevHtmlOverflow;
      document.body.style.overflow = prevBodyOverflow;
      document.body.style.position = prevBodyPosition;
      document.body.style.width = prevBodyWidth;
    };
  }, []);

  // 데스크탑 마우스 휠 — CategoryScreen은 height:100dvh + overflow:hidden 이라 body 스크롤이
  // 안 먹고, 활성 슬라이드는 pager의 translateX 때문에 마우스 커서 아래에서 발견되지 않을 수
  // 있다(브라우저 native scroll이 listRef를 못 찾음). 따라서 모든 wheel 이벤트를 가로채서
  // 직접 listRef로 스크롤 전달 + preventDefault로 기본 동작 차단.
  React.useEffect(() => {
    const onWheel = (e) => {
      const sc = listRef.current;
      if (!sc) return;
      // 모달, 토스트 등 내부에 별도 스크롤 컨테이너가 있는 경우엔 native 처리에 맡김
      // (data-modal-scroll 마커가 있는 컨테이너는 모달 본문 스크롤이므로 그대로 둠)
      const modalScroll = e.target.closest?.('[data-modal-scroll]');
      if (modalScroll) return;
      // deltaMode 보정: 0=pixel, 1=line(≈16px), 2=page
      const m = e.deltaMode === 1 ? 16 : e.deltaMode === 2 ? sc.clientHeight : 1;
      // listRef로 강제 전달
      if (e.cancelable) e.preventDefault();
      sc.scrollTop += e.deltaY * m;
      sc.scrollLeft += e.deltaX * m;
    };
    // passive: false 여야 preventDefault 가능
    window.addEventListener('wheel', onWheel, { passive: false });
    return () => window.removeEventListener('wheel', onWheel);
  }, []);

  return (
    <div style={{
      display: 'flex', flexDirection: 'column',
      height: '100dvh', overflow: 'hidden',
      background: '#fff', fontFamily: FF,
      // iOS Safari 상태바 영역(시계/노치)만큼 상단 여백 — 헤더가 상태바 아래에 정렬되도록
      // (Android 앱 웹뷰에서는 --safe-top이 0px로 강제됨, interactions.js 참조)
      paddingTop: 'var(--safe-top, env(safe-area-inset-top, 0px))',
    }}>
      <PageHeader title={groupDef.title} onLeading={onBack} leadingIcon="chevron-left" trailingIcon="search" onTrailing={onSearch} />

      <CategoryTabs
        items={[{ id: '__benefit', label: '타운카 전용 혜택' }, ...groupCats]}
        active={benefitOnly ? '__benefit' : category}
        onChange={onTab}
        counts={{ ...counts, __benefit: benefitCount }} />

      {!benefitOnly && subItems && <SubChips key={category} items={subItems} active={sub} onChange={setSub} />}

      <SearchBar value={search} onChange={setSearch} />

      {sub || search.trim() ?
      <div
        ref={listRef}
        onPointerDown={onSwipeStart}
        onPointerMove={onSwipeMove}
        onPointerUp={onSwipeEnd}
        onPointerCancel={() => {swipeRef.current = null;}}
        style={{ flex: 1, overflowY: 'auto', WebkitOverflowScrolling: 'touch', background: '#fff', touchAction: 'pan-y', overscrollBehavior: 'none' }}>
        <CatBody tabId={benefitOnly ? '__benefit' : category} group={group} sub={sub} search={search} onOpenDetail={onOpenDetail} />
      </div> :
      <div
        ref={pagerRef}
        onPointerDown={onSwipeStart}
        onPointerMove={onSwipeMove}
        onPointerUp={onSwipeEnd}
        onPointerCancel={() => {swipeRef.current = null;setDragging(false);setDragX(0);}}
        style={{ flex: 1, position: 'relative', overflow: 'hidden', background: '#fff', touchAction: 'pan-y', overscrollBehavior: 'none' }}>
        <div style={{ display: 'flex', height: '100%', willChange: 'transform', transform: `translateX(${-activeIndex * pagerW + dragX}px)`, transition: dragging ? 'none' : 'transform 300ms cubic-bezier(0.22,0.61,0.36,1)' }}>
          {tabsIds.map((tid, i) =>
          <div key={tid} ref={i === activeIndex ? listRef : null} style={{ width: pagerW, flexShrink: 0, height: '100%', overflowY: i === activeIndex ? 'auto' : 'hidden', WebkitOverflowScrolling: 'touch', overscrollBehavior: 'none', touchAction: 'pan-y' }}>
            {Math.abs(i - activeIndex) <= 1 ? <CatBody tabId={tid} group={group} onOpenDetail={onOpenDetail} /> : null}
          </div>
          )}
        </div>
      </div>
      }
    </div>);

}

// ── 카테고리 안내 (i 아이콘 → 말풍선 툴팁)
function InfoTooltip({ text }) {
  const [open, setOpen] = React.useState(false);
  React.useEffect(() => {
    if (!open) return;
    const onKey = (e) => { if (e.key === 'Escape') setOpen(false); };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [open]);
  return (
    <span style={{ position: 'relative', display: 'inline-flex', verticalAlign: 'middle', flexShrink: 0 }}>
      <button
        onClick={(e) => { e.stopPropagation(); if (window.haptic) window.haptic.selection(); setOpen((o) => !o); }}
        aria-label="안내 보기"
        style={{
          all: 'unset', cursor: 'pointer', display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
          width: 22, height: 22, borderRadius: 999, color: open ? TC.textNeutral : TC.textAlt,
          background: open ? TC.fill : 'transparent', transition: 'background 120ms, color 120ms'
        }}>
        <Icon name="info" size={15} color="currentColor" stroke={2} />
      </button>
      {open &&
      <>
        <span onClick={(e) => { e.stopPropagation(); setOpen(false); }} style={{ position: 'fixed', inset: 0, zIndex: 60 }} />
        <span style={{
          position: 'absolute', top: 'calc(100% + 9px)', left: -6, zIndex: 61,
          width: 'min(264px, calc(100vw - 110px))', padding: '12px 14px', borderRadius: 12,
          background: TC.textStrong, color: '#fff', fontSize: 12.5, fontWeight: 400,
          lineHeight: '18px', letterSpacing: '-0.005em', wordBreak: 'keep-all',
          boxShadow: '0 8px 28px rgba(0,0,0,0.22)',
          animation: 'tc-fade-in 160ms cubic-bezier(0.22,0.61,0.36,1) both'
        }}>
          <span aria-hidden="true" style={{ position: 'absolute', top: -4, left: 12, width: 10, height: 10, background: TC.textStrong, transform: 'rotate(45deg)', borderRadius: 2 }} />
          {text}
        </span>
      </>}
    </span>);

}

// ── 그룹 헤더
function GroupHeader({ label, count }) {
  return (
    <div style={{
      position: 'sticky', top: 0, zIndex: 5, background: TC.bgAlt,
      padding: '11px 16px', display: 'flex', alignItems: 'center', gap: 6,
      fontFamily: FF
    }}>
      <h3 style={{ margin: 0, fontSize: 14, fontWeight: 700, color: TC.textStrong, letterSpacing: '-0.02em', lineHeight: 1 }}>{label}</h3>
      <span style={{ fontSize: 11, fontWeight: 400, color: TC.primary, letterSpacing: '-0.005em', lineHeight: 1 }}>{count}</span>
    </div>);

}

// ── 혜택관 진입 행 (홈 · 카테고리 스타일 리스트)
function BenefitEntryRow({ icon, title, desc, onClick, last }) {
  const [pressed, setPressed] = React.useState(false);
  return (
    <button onClick={onClick} style={{
      all: 'unset', cursor: 'pointer', boxSizing: 'border-box', width: '100%', position: 'relative',
      display: 'flex', alignItems: 'center', gap: 13, padding: last ? '13px 16px 19px' : '13px 16px',
      background: pressed ? TC.fill : 'transparent', transition: 'background 100ms'
    }}
    onPointerDown={() => setPressed(true)}
    onPointerUp={() => setPressed(false)}
    onPointerLeave={() => setPressed(false)}
    onPointerCancel={() => setPressed(false)}>
      <div style={{
        flexShrink: 0, width: 42, height: 42, borderRadius: 13, background: TC.primaryWeak,
        display: 'flex', alignItems: 'center', justifyContent: 'center'
      }}>
        <Icon name={icon} size={21} color={TC.primaryPressed} stroke={2} />
      </div>
      <div style={{ flex: 1, minWidth: 0 }}>
        <div style={{ color: TC.textStrong, letterSpacing: '-0.02em', fontSize: "17px", fontWeight: "700" }}>{title}</div>
        <div style={{ marginTop: 2, fontSize: 13, fontWeight: 500, color: TC.textAlt, letterSpacing: '-0.005em' }}>{desc}</div>
      </div>
      <span style={{
        flexShrink: 0, display: 'inline-flex', alignItems: 'center',
        height: 30, padding: '0 13px', borderRadius: 999,
        background: TC.fill, color: TC.textNeutral,
        fontSize: 13, fontWeight: 600, letterSpacing: '-0.005em', pointerEvents: 'none'
      }}>보기</span>
      {!last && <span style={{ position: 'absolute', left: 16, right: 16, bottom: 0, height: 1, background: TC.lineSoft }} />}
    </button>);

}

// ── 혜택관 진입 카드 (홈)
function BenefitBigCard({ title, desc, img, imgSize = 72, onClick }) {
  const [pressed, setPressed] = React.useState(false);
  return (
    <button onClick={onClick} style={{
      all: 'unset', cursor: 'pointer', boxSizing: 'border-box', position: 'relative',
      borderRadius: 18, background: pressed ? TC.bgAlt : '#fff',
      minHeight: 112, display: 'flex', flexDirection: 'column', alignItems: 'flex-start', justifyContent: 'space-between', gap: 8,
      transition: 'transform 120ms cubic-bezier(0.22,0.61,0.36,1), background 120ms',
      transform: pressed ? 'scale(0.975)' : 'scale(1)', padding: "14px 16px"
    }}
    onPointerDown={() => setPressed(true)}
    onPointerUp={() => setPressed(false)}
    onPointerLeave={() => setPressed(false)}
    onPointerCancel={() => setPressed(false)}>
      <div style={{ fontSize: 18, fontWeight: 700, color: TC.textStrong, letterSpacing: '-0.02em', lineHeight: '25px', whiteSpace: 'pre-line' }}>{title}</div>
      {desc && <div style={{ fontSize: 13, color: TC.textAlt, letterSpacing: '-0.005em', fontWeight: "500" }}>{desc}</div>}
      <img src={img} alt="" aria-hidden="true" style={{ objectFit: 'contain', flexShrink: 0, position: 'absolute', right: 14, bottom: 14, width: imgSize + "px", height: imgSize + "px" }} />
    </button>);

}

// ── 혜택관 진입 카드 (홈)
function BenefitEntryCard({ icon, title, desc, onClick }) {
  const [pressed, setPressed] = React.useState(false);
  return (
    <button onClick={onClick} style={{
      all: 'unset', cursor: 'pointer', boxSizing: 'border-box',
      borderRadius: 18, padding: '16px 14px', background: pressed ? TC.bgAlt : '#fff',
      border: '1px solid ' + TC.line,
      display: 'flex', flexDirection: 'column', gap: 12, minHeight: 112,
      transition: 'transform 120ms cubic-bezier(0.22,0.61,0.36,1), background 120ms',
      transform: pressed ? 'scale(0.975)' : 'scale(1)'
    }}
    onPointerDown={() => setPressed(true)}
    onPointerUp={() => setPressed(false)}
    onPointerLeave={() => setPressed(false)}
    onPointerCancel={() => setPressed(false)}>
      <div style={{
        width: 40, height: 40, borderRadius: 12, background: TC.primaryWeak,
        display: 'flex', alignItems: 'center', justifyContent: 'center'
      }}>
        <Icon name={icon} size={21} color={TC.primaryPressed} stroke={2} />
      </div>
      <div>
        <div style={{ fontSize: 15, fontWeight: 700, color: TC.textStrong, letterSpacing: '-0.02em', marginBottom: 3 }}>{title}</div>
        <div style={{ fontSize: 12.5, color: TC.textAlt, fontWeight: 500, letterSpacing: '-0.005em' }}>{desc}</div>
      </div>
    </button>);

}

// ── 혜택 행 (실제 혜택을 헤드라인으로)
function BenefitRow({ partner, last, onClick }) {
  const p = partner;
  const [pressed, setPressed] = React.useState(false);
  const benefit = p.discount || '타운카 회원 전용 혜택';
  return (
    <div onClick={() => onClick && onClick()} role="button" tabIndex={0}
    onPointerDown={() => setPressed(true)} onPointerUp={() => setPressed(false)}
    onPointerLeave={() => setPressed(false)} onPointerCancel={() => setPressed(false)}
    style={{
      background: pressed ? TC.bgAlt : '#fff', padding: '15px 16px', cursor: 'pointer',
      position: 'relative', fontFamily: FF, transition: 'background 100ms',
      WebkitTapHighlightColor: 'transparent'
    }}>
      <div style={{ display: 'flex', alignItems: 'flex-start', gap: 8 }}>
        <div style={{ flex: 1, minWidth: 0 }}>
          {p.brand && <div style={{ fontSize: 11.5, fontWeight: 600, color: TC.textAlt, letterSpacing: '0.01em', marginBottom: 2 }}>{p.brand}</div>}
          <div style={{ fontSize: 16, fontWeight: 700, color: TC.textStrong, letterSpacing: '-0.02em', lineHeight: '21px' }}>{p.name}</div>
        </div>
        <Icon name="chevron-right" size={18} color={TC.textAssist} style={{ marginTop: 2 }} />
      </div>

      <div style={{
        marginTop: 9, display: 'flex', alignItems: 'center', gap: 7,
        padding: '9px 11px', borderRadius: 10, background: TC.primaryWeak
      }}>
        <Icon name="gift" size={14} color={TC.primaryPressed} stroke={2.2} style={{ flexShrink: 0 }} />
        <span style={{ fontSize: 13, fontWeight: 700, color: TC.primaryPressed, letterSpacing: '-0.005em', lineHeight: '17px', wordBreak: 'keep-all' }}>{benefit}</span>
      </div>

      <div style={{
        marginTop: 8, fontSize: 12.5, color: p.address ? TC.textNeutral : TC.textAssist,
        letterSpacing: '-0.005em', lineHeight: '17px', display: 'flex', alignItems: 'center', gap: 4, minWidth: 0
      }}>
        <Icon name="map-pin" size={12} color={TC.textAssist} stroke={2} style={{ flexShrink: 0 }} />
        <span style={{ flex: 1, minWidth: 0, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{p.address || '지점 내방 문의'}</span>
      </div>

      {!last && <span style={{ position: 'absolute', left: 16, right: 16, bottom: 0, height: 1, background: TC.lineSoft }} />}
    </div>);

}

// ── 차주 특별 혜택관 (출고/정비 그룹별 혜택 업체 전부)
function BenefitsScreen({ scope, onBack, onOpenDetail }) {
  const group = CAT_GROUPS.find((g) => g.id === scope) || CAT_GROUPS[0];
  const list = useMemo(() => PARTNERS.filter((p) => (p.official || p.discount) && group.cats.includes(p.category)), [scope]);
  const title = scope === 'service' ? '차량 정비 혜택' : '차량 구매 혜택';
  const desc = scope === 'service' ? '차를 관리할 때 받는 회원 전용 할인' : '차를 살 때 받는 회원 전용가·할인';
  return (
    <div style={{ display: 'flex', flexDirection: 'column', minHeight: '100dvh', background: TC.bgAlt, fontFamily: FF }}>
      <PageHeader title={title} onBack={onBack} leadingIcon="chevron-left" onLeading={onBack} />

      <div style={{ padding: '14px 16px 0' }}>
        <div style={{ fontSize: 13, color: TC.textNeutral, letterSpacing: '-0.005em', marginBottom: 10, padding: '0 2px' }}>
          {desc} · <strong style={{ fontWeight: 700, color: TC.textStrong }}>{list.length}곳</strong>
        </div>
        {list.length > 0 ?
        <div style={{ background: '#fff', borderRadius: 22, overflow: 'hidden' }}>
            {list.map((p, i) =>
          <PartnerCard key={p.id} partner={p} brandLabel={p.brand} showAsset showBenefit last={i === list.length - 1} onClick={() => onOpenDetail(p)} />
          )}
          </div> :

        <div style={{ background: '#fff', borderRadius: 22, padding: '8px 0' }}>
            <EmptyState icon="gift" title="준비 중이에요" subtitle="회원 전용 혜택 업체를 모으고 있어요" />
          </div>
        }
      </div>
      <ListFooter />
    </div>);

}

// ── 업체 검색 화면
function SearchScreen({ onBack, onOpenDetail }) {
  const [q, setQ] = useState('');
  const inputRef = useRef(null);

  useEffect(() => {
    const t = setTimeout(() => {if (inputRef.current) inputRef.current.focus();}, 220);
    return () => clearTimeout(t);
  }, []);

  const results = useMemo(() => {
    const query = q.trim().toLowerCase();
    if (!query) return [];
    return PARTNERS.filter((p) =>
    p.name.toLowerCase().includes(query) ||
    (p.address || '').toLowerCase().includes(query) ||
    (p.brand || '').toLowerCase().includes(query) ||
    (p.area || '').toLowerCase().includes(query));
  }, [q]);

  return (
    <div style={{ display: 'flex', flexDirection: 'column', minHeight: '100dvh', background: '#fff', fontFamily: FF, paddingTop: 'var(--safe-top, env(safe-area-inset-top, 0px))' }}>
      {/* 헤더: 뒤로 + 검색 입력 */}
      <div style={{
        height: 56, padding: '0 8px 0 4px', display: 'flex', alignItems: 'center', gap: 6,
        background: '#fff', position: 'sticky', top: 0, zIndex: 30
      }}>
        <button onClick={onBack} aria-label="뒤로" style={{
          all: 'unset', cursor: 'pointer', width: 44, height: 44,
          display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0
        }}>
          <Icon name="chevron-left" size={26} color={TC.textStrong} stroke={2} />
        </button>
        <div style={{
          flex: 1, height: 44, padding: '0 12px', borderRadius: 12, background: TC.bgAlt,
          display: 'flex', alignItems: 'center', gap: 8
        }}>
          <Icon name="search" size={18} color={TC.textAlt} stroke={2} />
          <input
            ref={inputRef}
            value={q}
            onChange={(e) => setQ(e.target.value)}
            placeholder="찾으시는 업체가 있으세요?"
            autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck={false}
            style={{
              all: 'unset', flex: 1, fontSize: 16, fontWeight: 500, fontFamily: FF,
              letterSpacing: '-0.01em', color: TC.textStrong, minWidth: 0
            }} />
          {q &&
          <button onClick={() => {setQ('');if (inputRef.current) inputRef.current.focus();}} style={{
            all: 'unset', cursor: 'pointer', display: 'flex',
            width: 20, height: 20, borderRadius: 999, background: TC.lineStrong,
            alignItems: 'center', justifyContent: 'center'
          }}><Icon name="x" size={12} color="#fff" stroke={2.6} /></button>
          }
        </div>
      </div>

      {!q.trim() ?
      <EmptyState icon="search" title="찾으시는 업체가 있으세요?" subtitle="지점명·지역·브랜드로 검색해보세요" /> :
      results.length === 0 ?
      <EmptyState icon="search-x" title="검색 결과가 없어요" subtitle="다른 키워드로 검색해보세요" /> :

      <div style={{ flex: 1, background: '#fff' }}>
          <div style={{ padding: '8px 16px 8px', fontSize: 13, color: TC.textNeutral, letterSpacing: '-0.005em' }}>
            <strong style={{ fontWeight: 700, color: TC.textStrong }}>{results.length}곳</strong> 찾았어요
          </div>
          {results.map((p, i) =>
        <PartnerCard key={p.id} partner={p} brandLabel={p.brand} showAsset last={i === results.length - 1} onClick={() => onOpenDetail(p)} />
        )}
        </div>
      }
    </div>);

}

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);

Object.assign(window, {
  App, HomeScreen, CategoryScreen, AdCarousel, AdSlide, CategoryCard, CategoryRow,
  OfficialCard, GroupHeader, InfoTooltip, SectionTitle, ListFooter, INTROS, SUB_INTROS
});