// 웹 스크린 A — 온보딩 · 홈 · 탐색 · 지역 · 빈지역
function WScrOnboarding({set, go}){
  const {useState, useEffect, useRef} = React;
  const [remoteMeetups, setRemoteMeetups] = useState([]);
  useEffect(()=>{ if (DEMO_MODE) return; let alive=true; fetchRemoteMeetups().then(list=>{ if(alive) setRemoteMeetups(list); }); return ()=>{alive=false;}; }, []);
  const previewMeetups = DEMO_MODE ? [MEETUPS.m1, MEETUPS.m2, MEETUPS.m6].filter(Boolean) : remoteMeetups.slice(0,3);
  // 하늘 장식(해·달·별·구름)이 마우스를 아주 은은하게 따라오는 패럴랙스 — React 상태 없이 CSS 변수만 갱신해서 리렌더 비용이 없음
  const decoRef = useRef(null);
  const onHeroMove = e => {
    const el = decoRef.current; if (!el) return;
    const r = e.currentTarget.getBoundingClientRect();
    el.style.setProperty('--mx', ((e.clientX-r.left)/r.width-.5).toFixed(3));
    el.style.setProperty('--my', ((e.clientY-r.top)/r.height-.5).toFixed(3));
  };
  return <div className="wapp">
    <section className="ob-hero" onMouseMove={onHeroMove}>
      <div className="ob-sky" ref={decoRef} aria-hidden="true">
        <span className="ob-sun"></span>
        <span className="ob-cloud obc1"></span>
        <span className="ob-cloud obc2"></span>
        <span className="ob-moon"></span>
        <span className="ob-star" style={{right:'14%',top:'22%','--delay':'0s'}}></span>
        <span className="ob-star" style={{right:'22%',top:'12%','--delay':'-1.2s'}}></span>
        <span className="ob-star" style={{right:'6%',top:'34%','--delay':'-2.1s'}}></span>
        <span className="ob-star ob-star-sm" style={{right:'28%',top:'28%','--delay':'-.6s'}}></span>
        <span className="ob-star ob-star-sm" style={{right:'10%',top:'8%','--delay':'-1.8s'}}></span>
      </div>
      <div className="ob-earth" aria-hidden="true">
        <div className="ob-globe">
          <div className="ob-globe-track">
            {[[3,'4%',0],[14,'12%',1],[26,'3%',2],[37,'11%',3],[53,'4%',0],[64,'12%',1],[76,'3%',2],[87,'11%',3]].map(([x,y,k],i)=><span key={i} className={'ob-land oll'+k} style={{left:x+'%',top:y}}></span>)}
          </div>
          <div className="ob-globe-night"></div>
          <div className="ob-globe-shine"></div>
        </div>
        <div className="ob-face">
          <span className="ob-eye"><i></i></span><span className="ob-eye"><i></i></span>
          <span className="ob-smile"></span>
        </div>
        <span className="ob-city" style={{right:'30%',bottom:'150px','--delay':'0s'}}></span>
        <span className="ob-city" style={{right:'26%',bottom:'118px','--delay':'-1s'}}></span>
        <span className="ob-city" style={{right:'36%',bottom:'98px','--delay':'-1.7s'}}></span>
        <span className="ob-city" style={{right:'22%',bottom:'168px','--delay':'-.4s'}}></span>
        <svg className="ob-gpin obg1" width="26" height="33" viewBox="0 0 24 30"><path d="M12 0C5.4 0 0 5.4 0 12c0 9 12 18 12 18s12-9 12-18c0-6.6-5.4-12-12-12z" fill="#FF8A5B"/><circle cx="12" cy="12" r="5" fill="#fff"/></svg>
        <svg className="ob-gpin obg2" width="22" height="28" viewBox="0 0 24 30"><path d="M12 0C5.4 0 0 5.4 0 12c0 9 12 18 12 18s12-9 12-18c0-6.6-5.4-12-12-12z" fill="#8ED9C6"/><circle cx="12" cy="12" r="5" fill="#fff"/></svg>
        <svg className="ob-gpin obg3" width="22" height="28" viewBox="0 0 24 30"><path d="M12 0C5.4 0 0 5.4 0 12c0 9 12 18 12 18s12-9 12-18c0-6.6-5.4-12-12-12z" fill="#FFD166"/><circle cx="12" cy="12" r="5" fill="#fff"/></svg>
      </div>
      <nav className="ob-nav">
        <div className="ob-logo"><img src={asset('assets/modumoyeo-icon.png')} alt="" style={{width:24,height:24,borderRadius:6,objectFit:'cover',marginRight:8,flexShrink:0}} onError={e=>{e.target.style.display='none';}}/>modu<span style={{color:'#a9d4ff'}}>moyeo</span></div>
      </nav>
      <div className="ob-body">
        <h1 className="ob-title"><span style={{color:'#ffd166'}}>모두</span> 어디에 있든,<br/>모여요</h1>
        <p className="ob-desc">해외에서 만나는 친구부터 귀국 후 다시 이어지는 인연까지 —<br/><span style={{whiteSpace:'nowrap'}}>지역 기반으로 연결돼요.</span></p>
        <div className="ob-cta">
          <button onClick={()=>{ if(!DEMO_MODE){ kakaoSignIn().then(r=>{ if(r&&(r.error||r.skipped)) set({toast:'카카오 연결에 실패했어요 — 잠시 후 다시 시도해주세요'}); }); return; } go('register',{authMethod:'kakao', pendingRegistration:true}); }} style={{display:'flex',alignItems:'center',gap:8,background:'#FEE500',color:'#191919',border:'none',borderRadius:999,fontSize:14,fontWeight:700,padding:'13px 22px',cursor:'pointer',fontFamily:'inherit'}}><Ic n="IconNormalLogoKakaoNameLogoKakao" s={17}/>카카오로 회원가입</button>
          <button onClick={()=>go('register',{authMethod:'email', pendingRegistration:true})} style={{background:'rgba(255,255,255,.14)',backdropFilter:'blur(10px)',color:'#fff',border:'1px solid rgba(255,255,255,.32)',borderRadius:999,fontSize:14,fontWeight:700,padding:'13px 22px',cursor:'pointer',fontFamily:'inherit'}}>이메일로 회원가입</button>
          <button onClick={()=>go('signup')} style={{background:'none',border:'1px solid rgba(255,255,255,.5)',borderRadius:999,color:'#fff',fontSize:13.5,fontWeight:700,padding:'12px 20px',cursor:'pointer',fontFamily:'inherit'}}>이미 계정이 있어요 · 로그인</button>
        </div>
        {DEMO_MODE&&<KakaoDemoPanel go={go}/>}
        <button onClick={()=>go('home',{toast:'비회원으로 둘러보는 중이에요 — 실제 참가·채팅·글쓰기는 회원가입 후 이용할 수 있어요'})} style={{marginTop:16,background:'none',border:'none',color:'rgba(255,255,255,.75)',fontSize:12.5,textDecoration:'underline',textUnderlineOffset:'2px',cursor:'pointer',fontFamily:'inherit',padding:0,maxWidth:'100%'}}>비회원으로 둘러보기</button>
      </div>
      <svg className="ob-wave" viewBox="0 0 1440 84" preserveAspectRatio="none" aria-hidden="true"><path d="M0,46 C220,84 470,10 740,32 C1010,54 1230,16 1440,42 L1440,84 L0,84 Z"/></svg>
    </section>
    <div className="ob-preview">
      {previewMeetups.length===0?<>
        <Reveal><h2 style={{fontSize:19, fontWeight:800, margin:'0 0 6px'}}>모임을 열면 이렇게 소개돼요</h2>
        <p style={{fontSize:13.5, color:'var(--c-sub)', margin:'0 0 16px'}}>아직 열린 모임이 없어요. 아래 카드는 이해를 돕기 위한 <b>예시 화면</b>이에요 — 실제 모임이 아니에요.</p></Reveal>
        <div className="wgrid">
          {[
            {title:'한강 러닝 크루', tag:'러닝·운동', when:'토요일 오전 10시 · 6명 정원', icon:'IconNormalTrophyNameTrophyFillFalse', bg:'var(--c-primary-bg)', fg:'var(--c-primary)'},
            {title:'홍대 보드게임 나이트', tag:'보드게임', when:'금요일 오후 7시 · 4명 정원', icon:'IconNormalDiamondNameDiamondFillFalse', bg:'var(--c-violet-bg)', fg:'var(--c-violet)'},
            {title:'영어·한국어 언어 교환', tag:'언어 교환', when:'일요일 오후 2시 · 8명 정원', icon:'IconNormalBubbleNameBubbleFillFalse', bg:'var(--c-mint-bg)', fg:'var(--c-mint)'},
          ].map((ex,i)=><Reveal key={ex.title} fx="pop" delay={i*110}><div className="ob-example-card">
            <div style={{position:'relative', aspectRatio:'4/3', background:ex.bg, display:'flex', alignItems:'center', justifyContent:'center'}}>
              <ActIcon label={ex.tag} size={40} color={ex.fg} fallbackIcon={ex.icon}/>
              <span className="ob-example-badge">예시 화면</span>
            </div>
            <div style={{padding:'10px 12px 12px'}}>
              <div style={{display:'flex', gap:4, flexWrap:'wrap', marginBottom:6}}>
                <span className="chip line">예시</span>
                <Chip label={ex.tag} tone="primary"/>
              </div>
              <div style={{fontWeight:700, fontSize:14.5, margin:'6px 0 3px'}}>{ex.title}</div>
              <div className="meta">{ex.when}</div>
              <div className="meta" style={{marginTop:7, color:'var(--c-primary)', fontWeight:600}}>가입하면 이런 모임을 직접 열 수 있어요</div>
            </div>
          </div></Reveal>)}
        </div>
        <p style={{fontSize:12.5, color:'var(--c-faint)', margin:'14px 0 0'}}>첫 모임의 주인이 되어보세요 — 가입 후 1분이면 모임을 열 수 있어요.</p>
      </>:<>
        <Reveal><h2 style={{fontSize:19, fontWeight:800, margin:'0 0 16px'}}>이번 주에 실제로 열리는 모임들이에요</h2></Reveal>
        <div className="wgrid">{previewMeetups.map((m,i)=><Reveal key={m.id} delay={(i%3)*90}><MeetCard m={m} onClick={()=>go('register',{authMethod:'email', toast:'가입하면 이 모임에 바로 신청할 수 있어요'})}/></Reveal>)}</div>
      </>}
    </div>
    <div className="obsec">
      <Reveal><h2 className="obsec-title">오늘은 뭐 하고 싶으세요?</h2>
      <p className="obsec-sub">활동을 고르면 그 활동이 열리는 모임을 바로 보여드려요.</p></Reveal>
      <div className="ob-cats">
        {[
          ['러닝·운동','IconNormalTrophyNameTrophyFillFalse','var(--c-primary-bg)','var(--c-primary)'],
          ['식사·맛집','IconNormalStarNameStarFillFalse','var(--c-coral-bg)','var(--c-coral)'],
          ['카페','IconNormalCoffeeNameCoffeeFillFalse','var(--c-orange-bg)','var(--c-orange)'],
          ['보드게임','IconNormalDiamondNameDiamondFillFalse','var(--c-violet-bg)','var(--c-violet)'],
          ['전시·공연','IconNormalTicketNameTicketFillFalse','var(--c-mint-bg)','var(--c-mint)'],
          ['여행','IconNormalCompassNameCompassFillFalse','var(--c-green-bg)','var(--c-green)'],
          ['언어 교환','IconNormalBubbleNameBubbleFillFalse','var(--c-yellow-bg)','var(--c-yellow)'],
          ['스터디','IconNormalBookNameBookFillFalse','var(--c-primary-bg)','var(--c-primary)'],
        ].map(([label,icon,bg,fg],i)=><Reveal key={label} fx="pop" delay={i*55}><button className="ob-cat" style={{background:bg}} onClick={()=>go('browseregions',{browseregionsType:'member', pendingActivityFilter:label})}>
          <ActIcon label={label} size={24} color={fg} fallbackIcon={icon}/>
          <b style={{color:fg}}>{label}</b>
        </button></Reveal>)}
      </div>
    </div>
    <div className="obsec">
      <Reveal><h2 className="obsec-title">시작은 이렇게 간단해요</h2></Reveal>
      <div className="ob-steps">
        {[
          ['가입하고 닉네임 정하기','이메일이면 충분해요. 실명·학교 인증은 나중에 원할 때만.'],
          ['모임 찾기, 없으면 직접 열기','활동·지역으로 찾아보고, 마음에 드는 게 없으면 1분 만에 직접 열 수 있어요.'],
          ['채팅으로 인사하고 만나기','참가가 확정되면 채팅방이 열려요. 미리 인사 나누고 편하게 만나요.'],
        ].map(([t,d],i)=><Reveal key={t} delay={i*90}><div className="ob-step">
          <span className="ob-stepnum">{i+1}</span>
          <b style={{fontSize:15, display:'block', marginBottom:6}}>{t}</b>
          <p style={{margin:0, fontSize:13.5, color:'var(--c-sub)', lineHeight:1.6}}>{d}</p>
        </div></Reveal>)}
      </div>
    </div>
    <div className="obsec">
      <Reveal><h2 className="obsec-title">이런 순간에 모두모여를 열어보세요</h2></Reveal>
      <div className="ob-personas">
        {[
          ['IconNormalGraduationNameGraduationFillFalse','var(--c-primary-bg)','var(--c-primary)','유학 와서 아는 사람이 하나도 없을 때','같은 도시의 한국인 유학생·교환학생을 활동으로 만나요.'],
          ['IconNormalPassportNamePassportFillFalse','var(--c-coral-bg)','var(--c-coral)','워홀로 새 도시에 막 도착했을 때','현지 정보도 나누고, 주말에 같이 놀 사람도 찾고.'],
          ['IconNormalHomeNameHomeFillFalse','var(--c-mint-bg)','var(--c-mint)','귀국했는데 유학 시절 얘기할 친구가 그리울 때','해외 경험이 통하는 사람들과 다시 이어져요.'],
        ].map(([icon,bg,fg,t,d],i)=><Reveal key={t} delay={i*90}><div className="ob-persona">
          <span className="pic" style={{background:bg}}><Ic n={icon} s={24} c={fg}/></span>
          <span style={{flex:1}}><b style={{fontSize:15}}>{t}</b><br/><span style={{fontSize:13.5, color:'var(--c-sub)'}}>{d}</span></span>
        </div></Reveal>)}
      </div>
    </div>
    <div className="obsec">
      <div className="ob-safety">
        <div>
          <Reveal fx="left"><h2 className="obsec-title">안심하고 만날 수 있도록</h2>
          <p style={{fontSize:14, color:'var(--c-sub)', lineHeight:1.7, margin:0}}>처음 보는 사람들과의 모임이 불안하지 않게, 기본적인 안전장치를 서비스 안에 넣어뒀어요.</p></Reveal>
        </div>
        <div>
          {[
            ['IconNormalFlagNameFlagFillFalse','모든 화면에서 신고 접수','모임·게시글·채팅 어디서든 신고할 수 있고, 운영진이 직접 확인해요.'],
            ['IconNormalCircleCheck2NameCircleCheckFillFillTrue','참가 전 이용수칙 동의','존중·비용·노쇼 기준에 동의해야 참가할 수 있어요. 기대치를 미리 맞춰요.'],
            ['IconNormalMailNameMail','문의 채널 상시 운영','불편한 일이 있으면 support@modumoyeo.com 으로 언제든 알려주세요.'],
          ].map(([icon,t,d],i)=><Reveal key={t} fx="right" delay={i*90}><div className="ob-safety-item">
            <Ic n={icon} s={20} c="var(--c-primary)" style={{marginTop:2}}/>
            <span style={{flex:1}}><b style={{fontSize:14}}>{t}</b><br/><span style={{fontSize:13, color:'var(--c-sub)', lineHeight:1.6}}>{d}</span></span>
          </div></Reveal>)}
        </div>
      </div>
    </div>
    <div className="obsec" style={{maxWidth:780}}>
      <Reveal><h2 className="obsec-title">자주 묻는 질문</h2></Reveal>
      {LEGAL_DOCS.faq.body.map(([q,a],i)=><Reveal key={q} delay={i*50}><details className="ob-faq">
        <summary>{q}<Ic n="IconNormalChevronDownNameChevronDownThickFalseSmall" s={14} c="var(--c-faint)"/></summary>
        <p>{a}</p>
      </details></Reveal>)}
    </div>
    <Reveal fx="pop"><div className="ob-cta-band">
      <span className="ob-cta-pin" aria-hidden="true"><svg width="30" height="38" viewBox="0 0 24 30"><path d="M12 0C5.4 0 0 5.4 0 12c0 9 12 18 12 18s12-9 12-18c0-6.6-5.4-12-12-12z" fill="#FFD166"/><circle cx="12" cy="12" r="5" fill="#fff"/></svg></span>
      <h2 style={{fontSize:24, fontWeight:800, margin:'0 0 10px', letterSpacing:'-.01em', wordBreak:'keep-all'}}>이번 주말, 새로운 얼굴들과 마주 앉아볼까요?</h2>
      <p style={{fontSize:14, margin:'0 0 22px', opacity:.9}}>가입은 1분이면 끝나요. 모임은 천천히 골라도 괜찮아요.</p>
      <div style={{display:'flex', gap:10, justifyContent:'center', flexWrap:'wrap'}}>
        <button onClick={()=>go('register',{authMethod:'email', pendingRegistration:true})} style={{background:'#fff',color:'#3D5BD9',border:'none',borderRadius:999,fontSize:14,fontWeight:800,padding:'13px 26px',cursor:'pointer',fontFamily:'inherit'}}>이메일로 회원가입</button>
        <button onClick={()=>go('home',{toast:'비회원으로 둘러보는 중이에요 — 실제 참가·채팅·글쓰기는 회원가입 후 이용할 수 있어요'})} style={{background:'none',border:'1px solid rgba(255,255,255,.55)',borderRadius:999,color:'#fff',fontSize:13.5,fontWeight:700,padding:'12px 22px',cursor:'pointer',fontFamily:'inherit'}}>비회원으로 둘러보기</button>
      </div>
    </div></Reveal>
    <Footer go={go} set={set}/>
  </div>;
}
// 데모 전용 · 카카오 인증 결과 시뮬레이션 패널 — 공개 모드 DOM에는 절대 렌더링되지 않음(WScrOnboarding에서 DEMO_MODE로 게이팅)
function KakaoDemoPanel({go}) {
  const {useState} = React;
  const [result, setResult] = useState(null);
  const [busy, setBusy] = useState(false);
  const btnStyle = {background:'rgba(255,255,255,.14)', border:'1px solid rgba(255,255,255,.3)', color:'#fff', borderRadius:8, padding:'8px 14px', minHeight:40, fontFamily:'inherit', fontSize:12, fontWeight:600, cursor:'pointer', whiteSpace:'nowrap'};
  const messages = {
    cancel: '카카오 인증이 취소됐어요. 다시 시도하거나 다른 방법으로 가입할 수 있어요.',
    fail: '카카오 인증을 완료하지 못했어요. 잠시 후 다시 시도해주세요.',
    network: '인터넷 연결을 확인한 뒤 다시 시도해 주세요.',
    servererr: '일시적인 오류가 발생했어요. 잠시 후 다시 시도해 주세요.',
  };
  const run = (r) => { if (busy) return; setBusy(true); setResult(null); setTimeout(()=>{ setBusy(false); setResult(r); }, 500); };
  const succeed = () => { if (busy) return; setBusy(true); setTimeout(()=>{ setBusy(false); go('register',{authMethod:'kakao', pendingRegistration:true}); }, 500); };
  return <div style={{marginTop:14, background:'rgba(255,255,255,.08)', border:'1px solid rgba(255,255,255,.25)', borderRadius:12, padding:'12px 14px', maxWidth:360}}>
    <div style={{fontSize:11, color:'rgba(255,255,255,.6)', fontWeight:700, marginBottom:8}}>데모 전용 · 카카오 인증 결과 테스트</div>
    <div style={{display:'flex', flexWrap:'wrap', gap:8}}>
      <button disabled={busy} onClick={succeed} style={btnStyle}>성공</button>
      <button disabled={busy} onClick={()=>run('cancel')} style={btnStyle}>취소</button>
      <button disabled={busy} onClick={()=>run('fail')} style={btnStyle}>실패</button>
      <button disabled={busy} onClick={()=>run('network')} style={btnStyle}>네트워크 오류</button>
      <button disabled={busy} onClick={()=>run('servererr')} style={btnStyle}>서버 오류</button>
    </div>
    {busy&&<div className="row" style={{gap:8, marginTop:10}}><span className="spinner"></span><span style={{fontSize:12, color:'rgba(255,255,255,.75)'}}>카카오 인증 확인 중…</span></div>}
    {result&&<div role="alert" aria-live="assertive" style={{fontSize:12.5, color:'#fff', lineHeight:1.6, marginTop:10}}>
      {messages[result]}
      <div style={{display:'flex', gap:8, marginTop:8, flexWrap:'wrap'}}>
        <button onClick={()=>run(result)} style={{...btnStyle, fontWeight:700}}>재시도</button>
        <button onClick={()=>go('register',{authMethod:'email', pendingRegistration:true})} style={btnStyle}>이메일로 가입</button>
        <button onClick={()=>go('signup')} style={btnStyle}>로그인</button>
      </div>
    </div>}
  </div>;
}
function WScrSignup({S, set, go}) {
  const {useEffect, useState} = React;
  const [remember, setRemember] = useState(false);
  const [errs, setErrs] = useState({});
  const [submitting, setSubmitting] = useState(false);
  const [attempts, setAttempts] = useState(0);
  const [lockedUntil, setLockedUntil] = useState(0);
  const [, tick] = useState(0);
  const [resetOpen, setResetOpen] = useState(false);
  const [resetSending, setResetSending] = useState(false);
  const [resetSent, setResetSent] = useState(false);
  useEffect(()=>{ const saved = localStorage.getItem(nsKey('mm_remember_email')); if (saved && !S.signupEmail) { set({signupEmail:saved}); setRemember(true); } }, []);
  useEffect(()=>{ if (!lockedUntil) return; const t = setInterval(()=>tick(n=>n+1), 1000); return ()=>clearInterval(t); }, [lockedUntil]);
  const lockSecondsLeft = lockedUntil ? Math.max(0, Math.ceil((lockedUntil-Date.now())/1000)) : 0;
  const focusFirstError = (e) => { const id = e.email?'w-login-email':e.password?'w-login-pw':null; if (id) setTimeout(()=>{ const el=document.getElementById(id); el&&el.focus(); }, 30); };
  const login = () => {
    if (submitting) return;
    if (lockedUntil && lockSecondsLeft>0) { setErrs({general:`로그인 시도가 너무 많아요. ${lockSecondsLeft}초 후 다시 시도해 주세요.`}); return; }
    if (lockedUntil && lockSecondsLeft<=0) { setLockedUntil(0); setAttempts(0); }
    const e = {};
    if (!S.signupEmail.trim()) e.email = '이메일을 입력해 주세요.';
    else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(S.signupEmail)) e.email = '올바른 이메일 형식을 입력해 주세요.';
    if (!S.signupPassword) e.password = '비밀번호를 입력해 주세요.';
    else if (S.signupPassword.length<8) e.password = '비밀번호는 8자 이상이어야 해요.';
    if (Object.keys(e).length) { setErrs(e); focusFirstError(e); return; }
    setErrs({}); setSubmitting(true);
    const fail = (msg) => {
      setSubmitting(false);
      const next = attempts+1;
      setAttempts(next);
      if (next>=5) { setLockedUntil(Date.now()+20000); setErrs({general:'로그인 시도가 너무 많아요. 20초 후 다시 시도해 주세요.'}); }
      else setErrs({general:msg});
    };
    authSignIn(S.signupEmail.trim(), S.signupPassword).then(res=>{
      if (res.skipped) { setSubmitting(false); setErrs({general:'서버 연동이 안 돼 있어서 지금은 로그인할 수 없어요.'}); return; }
      if (res.error) { fail(res.error); return; }
      setSubmitting(false);
      setAttempts(0); setLockedUntil(0);
      if (remember) localStorage.setItem(nsKey('mm_remember_email'), S.signupEmail); else localStorage.removeItem(nsKey('mm_remember_email'));
      set(switchAccount(BASE, S, S.signupEmail, {screen:'home', pendingRegistration:false, toast:'다시 오셨네요! 반가워요 :)', authStatus:'authenticated', onboardingCompleted:true, authMethod:null, regName:'', cancelRegisterConfirm:null, acctEmail:S.signupEmail.trim()}));
    });
  };
  return <div className="wapp auth-page">
    <AuthDecor/>
    <div className="wmain" style={{maxWidth:460}} onKeyDown={e=>{ if(e.key==='Enter'){ e.preventDefault(); login(); } }}>
      <div className="wbrand" style={{fontSize:20,marginBottom:24}}><img src={asset('assets/modumoyeo-icon.png')} alt="" style={{width:22,height:22,borderRadius:6,verticalAlign:'-5px',marginRight:8,objectFit:'cover'}} onError={e=>{e.target.style.display='none';}}/>modu<b>moyeo</b></div>
      <h2 style={{fontSize:22,fontWeight:800,margin:'0 0 16px'}}>이메일로 로그인</h2>
      {!DEMO_MODE&&<button onClick={()=>kakaoSignIn().then(r=>{ if(r&&(r.error||r.skipped)) set({toast:'카카오 연결에 실패했어요 — 잠시 후 다시 시도해주세요'}); })} style={{display:'flex',alignItems:'center',justifyContent:'center',gap:8,width:'100%',background:'#FEE500',color:'#191919',border:'none',borderRadius:10,fontSize:14,fontWeight:700,padding:'12px',cursor:'pointer',fontFamily:'inherit',marginBottom:14}}><Ic n="IconNormalLogoKakaoNameLogoKakao" s={16}/>카카오로 로그인</button>}
      {DEMO_MODE&&<div style={{background:'var(--c-yellow-bg)', color:'var(--c-text)', fontSize:12.5, fontWeight:700, borderRadius:8, padding:'8px 12px', marginBottom:16}}>실제 가입된 계정으로 로그인돼요(Supabase 연동)</div>}
      {errs.general&&<div role="alert" aria-live="assertive" style={{background:'var(--c-red-bg,#fdeaea)', color:'var(--c-red)', border:'1px solid var(--c-red)', borderRadius:8, padding:'10px 12px', marginBottom:16, fontSize:13, fontWeight:600}}>{errs.general}</div>}
      <div style={{marginBottom:16}}>
        <div className="meta" style={{marginBottom:6}}>이메일</div>
        <EmailField id="w-login-email" value={S.signupEmail} onChange={v=>{set({signupEmail:v}); if(errs.email) setErrs(x=>({...x,email:null}));}} externalError={errs.email}/>
      </div>
      <div style={{marginBottom:10}}>
        <div className="meta" style={{marginBottom:6}}>비밀번호</div>
        <PasswordField id="w-login-pw" value={S.signupPassword} onChange={v=>{set({signupPassword:v}); if(errs.password) setErrs(x=>({...x,password:null}));}} externalError={errs.password}/>
      </div>
      <label className="row" style={{gap:7, margin:'2px 0 6px', cursor:'pointer', fontSize:13.5}}>
        <input type="checkbox" checked={remember} onChange={e=>setRemember(e.target.checked)} style={{width:17, height:17, accentColor:'var(--c-primary)'}}/>이메일 기억하기
      </label>
      <div className="meta" style={{margin:'0 0 6px', lineHeight:1.6}}>공용 기기에서는 끄는 걸 권장해요.</div>
      {!resetOpen?<button type="button" onClick={()=>setResetOpen(true)} style={{background:'none', border:'none', color:'var(--c-primary)', fontFamily:'inherit', fontSize:12.5, fontWeight:600, cursor:'pointer', padding:0, minHeight:44, display:'inline-flex', alignItems:'center'}}>비밀번호를 잊으셨나요?</button>
      :<div style={{margin:'4px 0 6px', padding:'12px 14px', border:'1px solid var(--c-line)', borderRadius:10}}>
        {!resetSent?<>
          <div className="meta" style={{marginBottom:8}}>가입한 이메일로 재설정 링크를 보내드려요.</div>
          <div style={{display:'flex', gap:8}}>
            <input type="email" value={S.signupEmail} onChange={e=>set({signupEmail:e.target.value})} placeholder="you@example.com" style={{flex:1, minWidth:0, border:'1px solid var(--c-line)', borderRadius:8, padding:'9px 12px', fontFamily:'inherit', fontSize:14, outline:'none'}}/>
            <button type="button" disabled={!S.signupEmail.includes('@')||resetSending} onClick={()=>{
              setResetSending(true);
              authRequestPasswordReset(S.signupEmail.trim()).then(res=>{
                setResetSending(false);
                if (res.skipped) { set({toast:'서버 연동이 안 돼 있어서 지금은 보낼 수 없어요'}); return; }
                if (res.error) { set({toast:res.error}); return; }
                setResetSent(true);
              });
            }} style={{background:'var(--c-primary)', color:'#fff', border:'none', borderRadius:8, padding:'0 14px', minHeight:38, fontFamily:'inherit', fontSize:13, fontWeight:700, cursor:'pointer'}}>{resetSending?'보내는 중…':'보내기'}</button>
          </div>
        </>:<div className="row" style={{gap:6}}><Ic n="IconNormalCircleCheck2NameCircleCheckFillFillTrue" s={15} c="var(--c-primary)"/><span className="meta" style={{color:'var(--c-primary)', fontWeight:600}}>재설정 메일을 보냈어요 — 메일함(스팸함 포함)을 확인해주세요.</span></div>}
      </div>}
      <p className="meta" style={{margin:'6px 2px 22px', lineHeight:1.6}}>이미 가입된 계정이면 약관 동의 없이 바로 로그인돼요. 비밀번호는 복호화할 수 없는 방식으로 저장되고, 이 기기에는 절대 남지 않아요.</p>
      <DSBtn label={submitting?'로그인 중…':(lockedUntil&&lockSecondsLeft>0)?`잠시 후 다시 시도 (${lockSecondsLeft}초)`:'로그인'} disable={submitting||(lockedUntil&&lockSecondsLeft>0)} onClick={login}/>
    </div>
  </div>;
}
function WScrConsent({S, set, go}) {
  const c = S.consents;
  const required = [
    {k:'terms', label:'이용약관에 동의합니다.'},
    {k:'privacy', label:'개인정보 처리에 관한 안내를 확인했습니다.'},
    {k:'collect', label:'회원관리용 개인정보 수집·이용에 동의합니다.'},
    {k:'age', label:'만 19세 이상입니다. (필수)'},
    {k:'safety', label:'커뮤니티 운영원칙과 안전수칙을 준수합니다.'},
  ];
  const optional = [
    {k:'marketing', label:'이벤트·혜택 및 광고성 정보 수신에 동의합니다.', help:'동의하면 가입 시 입력한 이메일로 이벤트·혜택 안내가 발송돼요.'},
    {k:'location', label:'현재 위치를 사용해 가까운 모임을 찾습니다.', help:'브라우저·앱의 위치 권한을 별도로 요청해요. 이메일과는 무관해요.'},
  ];
  const items = [...required, ...optional];
  const allRequired = required.every(i=>c[i.k]);
  const allEverything = items.every(i=>c[i.k]);
  const toggleRequired = () => { const next={...c}; required.forEach(i=>next[i.k]=!allRequired); set({consents:next}); };
  const toggleAll = () => { const next={...c}; items.forEach(i=>next[i.k]=!allEverything); set({consents:next}); };
  return <div className="wapp auth-page">
    <AuthDecor/>
    <div className="wmain" style={{maxWidth:560}}>
      <div className="wbrand" style={{fontSize:20,marginBottom:20}}><img src={asset('assets/modumoyeo-icon.png')} alt="" style={{width:22,height:22,borderRadius:6,verticalAlign:'-5px',marginRight:8,objectFit:'cover'}} onError={e=>{e.target.style.display='none';}}/>modu<b>moyeo</b></div>
      <h2 style={{fontSize:22,fontWeight:800,margin:'0 0 10px'}}>약관 동의</h2>
      <p style={{margin:'0 0 20px', fontSize:14, color:'var(--c-sub)', lineHeight:1.6}}>서비스 제공에 꼭 필요한 항목만 필수예요. 선택 항목은 동의하지 않아도 가입할 수 있어요.</p>

      <div className="card" style={{marginBottom:16, padding:'16px 18px'}}>
        <div className="row" style={{justifyContent:'space-between', marginBottom:12}}>
          <b style={{fontSize:14}}>필수 동의</b>
          <button onClick={toggleRequired} style={{background:'none', border:'1px solid var(--c-line)', borderRadius:8, padding:'5px 12px', cursor:'pointer', fontFamily:'inherit', fontSize:12.5, fontWeight:700}}>필수 항목 모두 동의</button>
        </div>
        {required.map(i=><label key={i.k} className="row" style={{gap:12, padding:'10px 0', borderTop:'1px solid var(--c-line)', cursor:'pointer', fontSize:13.5, alignItems:'flex-start'}}>
          <input type="checkbox" checked={c[i.k]||false} onChange={e=>set({consents:{...c,[i.k]:e.target.checked}})} style={{width:18, height:18, accentColor:'var(--c-primary)', flexShrink:0, marginTop:1}}/>
          <span style={{flex:1}}>{i.label}</span>
        </label>)}
      </div>

      <div className="card" style={{marginBottom:16, padding:'16px 18px', background:'var(--c-fill-a)'}}>
        <b style={{fontSize:14, display:'block', marginBottom:12}}>선택 동의</b>
        {optional.map(i=><label key={i.k} className="row" style={{gap:12, padding:'10px 0', borderTop:'1px solid var(--c-line)', cursor:'pointer', fontSize:13.5, alignItems:'flex-start'}}>
          <input type="checkbox" checked={c[i.k]||false} onChange={e=>set({consents:{...c,[i.k]:e.target.checked}})} style={{width:18, height:18, accentColor:'var(--c-primary)', flexShrink:0, marginTop:1}}/>
          <span style={{flex:1}}>{i.label}<span className="meta" style={{display:'block', marginTop:2}}>{i.help} 동의하지 않아도 가입엔 영향 없고, 설정에서 언제든 철회할 수 있어요.</span></span>
        </label>)}
      </div>
      <button onClick={toggleAll} style={{width:'100%', textAlign:'left', background:'none', border:'1px solid var(--c-line)', borderRadius:10, padding:'10px 14px', marginBottom:14, cursor:'pointer', fontFamily:'inherit', fontSize:13.5, fontWeight:700, color:'var(--c-primary)'}}>선택 항목 포함 전체 동의</button>

      <p className="meta" style={{margin:'0 0 80px', lineHeight:1.6}}>학교 인증은 가입 필수 조건이 아니에요 — 원하는 분만 프로필에서 선택 신청해요. 뱃지는 신원이나 안전을 보증하지 않아요.</p>
    </div>
    <div style={{position:'sticky', bottom:0, background:'var(--c-bg)', borderTop:'1px solid var(--c-line)', padding:'14px 32px', display:'flex', justifyContent:'center'}}>
      <div style={{width:'100%', maxWidth:560}}><DSBtn label="동의하고 계속하기" disable={!allRequired} onClick={()=>go('setup')}/></div>
    </div>
  </div>;
}
function WScrSetup({S, set, go}) {
  const [nickErr, setNickErr] = React.useState('');
  const interests = S.profile.interests||[];
  const customInterests = S.profile.customInterests||[];
  const toggleInterest = t => { const has = interests.includes(t); const next = has?interests.filter(x=>x!==t):(interests.length<3?[...interests,t]:interests); set({profile:{...S.profile, interests:next}}); };
  const addCustom = t => set({profile:{...S.profile, customInterests:[...customInterests, t]}});
  const removeCustom = t => set({profile:{...S.profile, customInterests:customInterests.filter(x=>x!==t)}});
  const finish = () => {
    // 19단계: 프로필을 서버에 저장 — 닉네임이 이미 있으면 가입 완료를 막고 인라인 안내
    setNickErr('');
    saveMyProfile(S.signupEmail, S.profile).then(res=>{
      if (res && !res.error && !res.dup && !res.reserved) saveMarketingConsent(S.signupEmail, !!(S.consents&&S.consents.marketing));
      if (res && res.dup) { setNickErr('이미 사용 중인 닉네임이에요 — 다른 닉네임을 골라주세요.'); return; }
      if (res && res.reserved) { setNickErr('운영진 사칭이 의심되는 닉네임은 사용할 수 없어요 — 다른 닉네임을 골라주세요.'); return; }
      if (res && res.error) {
        const raw = String(res.error.message||'');
        setNickErr(/row-level security|violates|permission/i.test(raw) ? '지금은 닉네임 확인을 할 수 없어요 — 가입은 계속되며, 로그인 후 내 정보에서 프로필을 저장하면 확인돼요.' : (raw||'프로필 저장에 실패했어요 — 잠시 후 다시 시도해주세요.'));
        return;
      }
      finishAfterSave();
    });
  };
  const finishAfterSave = () => {
    const toast = '가입 완료 — 모두모여에 오신 걸 환영해요';
    markWelcomePending(S);
    setActiveAccountId(S.signupEmail); // 이 시점부터 이 계정 이메일 기준으로 세션이 저장·분리됨
    const authPatch = {authStatus:'authenticated', onboardingCompleted:true, pendingRegistration:false, authMethod:null, confirmedAuthMethod:S.authMethod, regName:'', cancelRegisterConfirm:null, acctEmail:S.signupEmail, profile:{...S.profile}};
    if (S.schoolVerifyChoice==='email') return go('schoolemail',{toast, ...authPatch});
    if (S.schoolVerifyChoice==='docs') return go('schooldocs',{toast, ...authPatch});
    return go('home',{toast, ...authPatch});
  };
  return <div className="wapp auth-page">
    <AuthDecor/>
    <div className="wmain" style={{maxWidth:520}}>
      <div className="wbrand" style={{fontSize:20,marginBottom:24}}><img src={asset('assets/modumoyeo-icon.png')} alt="" style={{width:22,height:22,borderRadius:6,verticalAlign:'-5px',marginRight:8,objectFit:'cover'}} onError={e=>{e.target.style.display='none';}}/>modu<b>moyeo</b></div>
      <h2 style={{fontSize:22,fontWeight:800,margin:'0 0 18px'}}>기본 프로필</h2>
      <div style={{marginBottom:16}}>
        <div className="meta" style={{marginBottom:6}}>닉네임</div>
        <input value={S.profile.nickname} onChange={e=>set({profile:{...S.profile, nickname:e.target.value}})} style={{width:'100%', border:'1px solid var(--c-line)', borderRadius:8, padding:'10px 14px', fontFamily:'inherit', fontSize:14, outline:'none'}}/>
        {nickErr&&<div role="alert" className="meta" style={{color:'var(--c-red)', marginTop:6, fontWeight:600}}>{nickErr}</div>}
      </div>
      <div style={{marginBottom:16}}>
        <div className="meta" style={{marginBottom:6}}>지금 계신 지역</div>
        <CountrySelect value={S.regionVals.cur} onChange={v=>set({regionVals:{...S.regionVals, cur:v}})}/>
      </div>
      <div style={{marginBottom:20}}>
        <div className="meta" style={{marginBottom:6}}>관심 지역 <span style={{color:'var(--c-faint)'}}>(선택 · 귀국·이동 예정지 등)</span></div>
        <CountrySelect value={S.regionVals.interest} onChange={v=>set({regionVals:{...S.regionVals, interest:v}})}/>
      </div>
      <div style={{marginBottom:20}}>
        <div className="meta" style={{marginBottom:8}}>관심사 <span style={{color:'var(--c-faint)'}}>· 홈 추천에 활용돼요 ({interests.length}/3)</span></div>
        <div style={{display:'flex', gap:8, flexWrap:'wrap'}}>{INTERESTS_POOL.map(t=><Chip key={t} label={t} onClick={()=>toggleInterest(t)} on={interests.includes(t)}/>)}</div>
      </div>
      <div className="card" style={{marginBottom:20, padding:'16px 18px'}}>
        <CustomActivityInput items={customInterests} onAdd={addCustom} onRemove={removeCustom} existingPool={INTERESTS_POOL}/>
      </div>
      <p className="meta" style={{margin:'16px 2px', lineHeight:1.6}}>닉네임은 다른 사람에게 보이는 이름이에요. 실명은 회원가입 정보에만 저장되고 공개되지 않아요.</p>
      <div style={{display:'flex', gap:10}}>
        <DSBtn label="맞춤 모임 보기" disable={!S.profile.nickname.trim()} onClick={finish} style={{flex:1}}/>
      </div>
      <button style={{background:'none', border:'none', color:'var(--c-faint)', fontFamily:'inherit', fontSize:13, cursor:'pointer', padding:'14px 0 0'}} onClick={finish}>나중에 채울게요 · 건너뛰기</button>
    </div>
  </div>;
}
function WScrHome({S,set,go}){
  const {useState, useEffect} = React;
  const [q, setQ] = useState('');
  const [remoteMeetups, setRemoteMeetups] = useState([]);
  useEffect(()=>{ let alive=true; fetchRemoteMeetups().then(list=>{ if(alive) setRemoteMeetups(list); }); return ()=>{alive=false;}; }, []);
  const {seoul, curCityTitle, abroad, abroadTitle, myCode, myCity, interestCode, interestCity} = computeHomeData(S);
  const savedMeetups = useSavedMeetups(S);
  const honorific = honorificName(S);
  const interests = [...(S.profile.interests||[]), ...(S.profile.customInterests||[])];
  const recommended = interests.length ? seoul.filter(m=>m.cardTags&&m.cardTags.some(t=>interests.some(i=>t[0].includes(i)||i.includes(t[0])))) : [];
  const needsProfile = S.authStatus==='authenticated' && (!S.emailVerified || S.schoolBadges.length===0);
  const search = q.trim() ? [...seoul, ...abroad].filter(m=>m.title.includes(q.trim())&&meetupVisibleTo(m, S.viewAsUser)) : null;
  return <div className="wapp"><WNav S={S} go={go} set={set}/>
    <div className="wmain">
      <div className="home-hero">
        <div className="home-hero-deco" aria-hidden="true">
          <span className="hh-sun"></span>
          <span className="hh-cloud"></span>
          <svg className="hh-pin hhp1" width="18" height="23" viewBox="0 0 24 30"><path d="M12 0C5.4 0 0 5.4 0 12c0 9 12 18 12 18s12-9 12-18c0-6.6-5.4-12-12-12z" fill="#FF8A5B"/><circle cx="12" cy="12" r="5" fill="#fff"/></svg>
          <svg className="hh-pin hhp2" width="15" height="19" viewBox="0 0 24 30"><path d="M12 0C5.4 0 0 5.4 0 12c0 9 12 18 12 18s12-9 12-18c0-6.6-5.4-12-12-12z" fill="#8ED9C6"/><circle cx="12" cy="12" r="5" fill="#fff"/></svg>
        </div>
        <h1 style={{fontSize:24, fontWeight:800, margin:'0 0 4px', letterSpacing:'-.01em', wordBreak:'keep-all', overflowWrap:'break-word', position:'relative'}}>{honorific?`${honorific}님, `:''}{myCity}에서 이번 주 뭐 하고 싶으세요?</h1>
        <p className="meta" style={{margin:'0 0 16px', position:'relative'}}>지금 탐색 중인 지역이에요 · 언제든 바꿀 수 있어요</p>
        <div className="row" style={{gap:8, flexWrap:'wrap', marginBottom:16, position:'relative'}}>
          <Chip icon="IconNormalLocation2NameLocationFillFillTrue" label={`현재 ${myCity}`} tone="primary"/>
          <Chip icon="IconNormalChangeNameChange" label={`이동 예정 ${S.regionVals.move}`} onClick={()=>go('region',{regionEdit:'move'})}/>
          <Chip icon="IconNormalGlobe2NameGlobeFillFalse" label={`관심 ${interestCity}`} onClick={()=>go('interestregion')}/>
        </div>
        <div style={{position:'relative'}}>
          <input value={q} onChange={e=>setQ(e.target.value)} placeholder="모임 이름으로 검색해보세요" aria-label="모임 검색" style={{width:'100%', border:'none', background:'#fff', borderRadius:'var(--r-md)', padding:'13px 16px 13px 40px', fontFamily:'inherit', fontSize:14.5, outline:'none', boxShadow:'0 3px 14px rgba(30,50,120,.09)'}}/>
          <Ic n="IconNormalSearchNameSearchThickFalse" s={17} c="var(--c-faint)" style={{position:'absolute', left:14, top:'50%', transform:'translateY(-50%)'}}/>
        </div>
      </div>
      <div className="home-cats" aria-label="활동으로 바로 찾기">
        {[
          ['러닝·운동','IconNormalTrophyNameTrophyFillFalse','var(--c-primary-bg)','var(--c-primary)'],
          ['식사·맛집','IconNormalStarNameStarFillFalse','var(--c-coral-bg)','var(--c-coral)'],
          ['카페','IconNormalCoffeeNameCoffeeFillFalse','var(--c-orange-bg)','var(--c-orange)'],
          ['보드게임','IconNormalDiamondNameDiamondFillFalse','var(--c-violet-bg)','var(--c-violet)'],
          ['전시·공연','IconNormalTicketNameTicketFillFalse','var(--c-mint-bg)','var(--c-mint)'],
          ['여행','IconNormalCompassNameCompassFillFalse','var(--c-green-bg)','var(--c-green)'],
          ['언어 교환','IconNormalBubbleNameBubbleFillFalse','var(--c-yellow-bg)','var(--c-yellow)'],
          ['스터디','IconNormalBookNameBookFillFalse','var(--c-primary-bg)','var(--c-primary)'],
        ].map(([label,icon,bg,fg])=><button key={label} className="home-cat" style={{background:bg, color:fg}} onClick={()=>go('browseregions',{browseregionsType:'member', pendingActivityFilter:label})}>
          <ActIcon label={label} size={16} color={fg} fallbackIcon={icon}/>{label}
        </button>)}
      </div>

      {needsProfile&&<button className="card" style={{width:'100%',display:'flex',gap:12,alignItems:'center',cursor:'pointer',fontFamily:'inherit',textAlign:'left',marginBottom:24,background:'var(--c-yellow-bg)',border:'none'}} onClick={()=>go('profile',{profileTab:2})}>
        <Ic n="IconNormalCircleInfo2NameCircleInfoFillFalse" s={20} c="var(--c-yellow)"/>
        <span style={{flex:1,fontSize:13.5}}><b>{!S.emailVerified?'이메일 인증을 완료해주세요':'학교 인증으로 신뢰도를 높여보세요'}</b><br/><span className="meta">완료하면 프로필에 인증 배지가 표시돼요</span></span>
      </button>}

      {search?<>
        <div className="wsect"><h2>&ldquo;{q}&rdquo; 검색 결과</h2></div>
        {search.length===0?<p className="meta" style={{margin:'0 0 24px'}}>일치하는 모임이 없어요.</p>:<div className="wgrid" style={{marginBottom:24}}>{search.map(m=><MeetCard key={m.id} m={m} onClick={()=>m.mine?go('mymeethosted'):MEETUPS[m.id]?go('detail',{detailId:m.id}):go('detail',{detailId:m.id, tempDetail:synthDetailFromCard(m)})}/>)}</div>}
      </>:<>
        {savedMeetups.length>0&&<>
          <div className="wsect"><h2><Ic n="IconNormalHeartNameHeartFillFillTrue" s={17} c="var(--c-coral)"/>내가 찜한 모임</h2>{savedMeetups.length>4&&<button className="more" onClick={()=>go('savedmeetups')}>전체 보기</button>}</div>
          <div className="wgrid" style={{marginBottom:28}}>{savedMeetups.slice(0,4).map(m=><MeetCard key={'saved-'+m.id} m={m} onClick={()=>go('detail',{detailId:m.id})}/>)}</div>
        </>}

        {remoteMeetups.length>0&&<>
          <div className="wsect"><h2><Ic n="IconNormalGlobe2NameGlobeFillFalse" s={17} c="var(--c-primary)"/>실제 등록된 모임</h2></div>
          <div className="wgrid" style={{marginBottom:28}}>{remoteMeetups.map(m=><MeetCard key={m.id} m={m} onClick={()=>go('detail',{detailId:m.id, tempDetail:m})}/>)}</div>
        </>}

        <div className="wsect"><h2><Ic n="IconNormalFireNameFireFillFalse" s={18} c="var(--c-orange)"/>이번 주 {myCity} 추천 모임</h2><button className="more" onClick={()=>go('homecur')}>전체 보기</button></div>
        {seoul.length===0?<div className="card" style={{textAlign:'center', padding:'28px 20px', marginBottom:28}}>
          <p style={{fontSize:14.5, fontWeight:700, margin:'0 0 6px'}}>아직 추천할 모임이 없어요.</p>
          <p className="meta" style={{margin:'0 0 14px'}}>관심 활동과 지역에 맞는 모임이 등록되면 여기에서 확인할 수 있어요.</p>
          <div className="row" style={{gap:8, justifyContent:'center'}}><DSBtn label="첫 모임 만들기" size="md" onClick={()=>startCreateMeetup(S,set,go)}/><DSBtn label="관심 활동 설정하기" size="md" variant="outlined" onClick={()=>go('profile',{profileTab:2})}/></div>
        </div>:<div className="wgrid" style={{marginBottom:28}}>{seoul.slice(0,4).map(m=><MeetCard key={m.id} m={m} onClick={()=>m.mine?go('mymeethosted',{toast:'내가 개설한 모임 관리로 이동했어요'}):MEETUPS[m.id]?go('detail',{detailId:m.id}):go('detail',{detailId:m.id, tempDetail:synthDetailFromCard(m)})}/>)}</div>}

        {recommended.length>0&&<>
          <div className="wsect"><h2><Ic n="IconNormalHeartNameHeartFillFalse" s={17} c="var(--c-coral)"/>관심사 기반 추천</h2></div>
          <div className="wgrid" style={{marginBottom:28}}>{recommended.slice(0,4).map(m=><MeetCard key={'rec-'+m.id} m={m} onClick={()=>MEETUPS[m.id]?go('detail',{detailId:m.id}):go('detail',{detailId:m.id, tempDetail:synthDetailFromCard(m)})}/>)}</div>
        </>}

        <div className="wsect"><h2>{abroadTitle}</h2><button className="more" onClick={()=>go('homeabroad')}>전체 보기</button></div>
        {abroad.length===0?<p className="meta" style={{margin:'0 0 28px'}}>아직 등록된 모임이 없어요.</p>:<div className="wgrid" style={{marginBottom:28}}>{abroad.slice(0,4).map(m=><MeetCard key={m.id} m={m} onClick={()=>MEETUPS[m.id]?go('detail',{detailId:m.id}):go('detail',{detailId:m.id, tempDetail:synthDetailFromCard(m)})}/>)}</div>}

        <div className="wsect"><h2>최근 커뮤니티 활동</h2><button className="more" onClick={()=>go('hometimeline')}>더보기</button></div>
        <div style={{marginBottom:12}}>{HOME_TIMELINE.slice(0,3).map((t,i)=><button key={i} className="row" style={{gap:12,padding:'12px 0',width:'100%',background:'none',border:'none',borderTop:'1px solid var(--c-line)',cursor:'pointer',fontFamily:'inherit',textAlign:'left'}} onClick={()=>{ const a=t.action; if(a.screen==='postdetail'){ set({qnaDetailPost:a.post, screen:'postdetail'}); } else { go(a.screen, a.detailId?{detailId:a.detailId}:{}); } }}>
          <Av name={t.who} size={30}/><div style={{flex:1}}><div style={{fontSize:13.5}}><b>{t.who}</b>{'님이 '}{t.text}</div><div className="meta">{t.meta}</div></div>
        </button>)}</div>
      </>}

      <button className="card" style={{width:'100%',display:'flex',gap:14,alignItems:'center',cursor:'pointer',fontFamily:'inherit',textAlign:'left',background:'var(--c-mint-bg)',border:'none',margin:'12px 0'}} onClick={()=>go('explore')}>
        <Ic n="IconNormalSparkleNameSparkle" s={24} c="var(--c-mint)"/>
        <span><b style={{fontSize:15}}>오늘의 동문과 함께할 활동을 찾아보세요</b><br/><span className="meta">사람을 고르는 게 아니라, 같이 할 활동을 골라요</span></span>
      </button>
    </div>
    <Footer go={go} set={set}/>
  </div>;
}
function WScrHomeCur({S,set,go}){
  const {seoul, curCityTitle} = computeHomeData(S);
  return <div className="wapp"><WNav S={S} go={go} set={set}/>
    <div className="wmain">
      <button className="wback" onClick={()=>go('home')}><Ic n="IconNormalChevronLeftNameChevronLeftTightFalseThick" s={14}/>홈으로</button>
      <div className="wsect"><h2><Ic n="IconNormalFireNameFireFillFalse" s={18} c="var(--c-orange)"/>{curCityTitle}</h2></div>
      <div className="wgrid">{seoul.map(m=><MeetCard key={m.id} m={m} onClick={()=>m.mine?go('mymeethosted',{toast:'내가 개설한 모임 관리로 이동했어요'}):MEETUPS[m.id]?go('detail',{detailId:m.id}):go('detail',{detailId:m.id, tempDetail:synthDetailFromCard(m)})}/>)}</div>
    </div>
    <Footer go={go} set={set}/>
  </div>;
}
function WScrHomeAbroad({S,set,go}){
  const {abroad, abroadTitle} = computeHomeData(S);
  return <div className="wapp"><WNav S={S} go={go} set={set}/>
    <div className="wmain">
      <button className="wback" onClick={()=>go('home')}><Ic n="IconNormalChevronLeftNameChevronLeftTightFalseThick" s={14}/>홈으로</button>
      <div className="wsect"><h2>{abroadTitle}</h2><button className="more" onClick={()=>go('browseregions')}>다른 지역 둘러보기</button></div>
      <div className="wgrid">{abroad.map(m=><MeetCard key={m.id} m={m} onClick={()=>MEETUPS[m.id]?go('detail',{detailId:m.id}):go('detail',{detailId:m.id, tempDetail:synthDetailFromCard(m)})}/>)}</div>
    </div>
    <Footer go={go} set={set}/>
  </div>;
}
function WScrHomeSnapshot({S,set,go}){
  const stats = [
    ['전체 회원','2,481'],['이번 달 모임','132'],['활동 국가','18'],['첫 참가 만족도','96%'],
    ['누적 모임 수','1,204'],['평균 참가 인원','5.4명'],['재참가율','61%'],['이번 주 신규 가입','87명'],
  ];
  return <div className="wapp"><WNav S={S} go={go} set={set}/>
    <div className="wmain">
      <button className="wback" onClick={()=>go('home')}><Ic n="IconNormalChevronLeftNameChevronLeftTightFalseThick" s={14}/>홈으로</button>
      <div className="wsect"><h2>커뮤니티 스냅샷</h2></div>
      <div style={{display:'grid', gridTemplateColumns:'repeat(4,1fr)', gap:14, marginBottom:12}}>
        {stats.map((s,i)=><Reveal key={s[0]} delay={i*60}><div className="card" style={{textAlign:'center', padding:'20px 10px'}}><b style={{fontSize:24, display:'block', color:'var(--c-primary)'}}>{s[1]}</b><span className="meta">{s[0]}</span></div></Reveal>)}
      </div>
    </div>
    <Footer go={go} set={set}/>
  </div>;
}
function WScrHomeTimeline({S,set,go}){
  const page = S.timelinePage||1;
  const perPage = 5;
  const totalPages = Math.ceil(HOME_TIMELINE.length/perPage);
  const items = HOME_TIMELINE.slice((page-1)*perPage, page*perPage);
  const openItem = t => { const a=t.action; if(a.screen==='postdetail'){ set({qnaDetailPost:a.post, screen:'postdetail'}); } else { go(a.screen, a.detailId?{detailId:a.detailId}:{}); } };
  return <div className="wapp"><WNav S={S} go={go} set={set}/>
    <div className="wmain">
      <button className="wback" onClick={()=>go('home')}><Ic n="IconNormalChevronLeftNameChevronLeftTightFalseThick" s={14}/>홈으로</button>
      <div className="wsect"><h2>타임라인</h2></div>
      {items.map((t,i)=><button key={i} className="row" style={{gap:12,padding:'14px 0',borderTop:'1px solid var(--c-line)',width:'100%',background:'none',border:'none',borderTop:'1px solid var(--c-line)',cursor:'pointer',fontFamily:'inherit',textAlign:'left'}} onClick={()=>openItem(t)}>
        <Av name={t.who}/><div style={{flex:1}}><div style={{fontSize:14}}><b>{t.who}</b>{'님이 '}{t.text}</div><div className="meta">{t.meta}</div></div>
        <Ic n="IconNormalChevronRightNameChevronRightTightFalseThick" s={14} c="var(--c-faint)"/>
      </button>)}
      <Pagination page={page} total={totalPages} onChange={n=>set({timelinePage:n})}/>
      <div className="wsect" style={{marginTop:32}}><h2>최근 후기</h2></div>
      <div style={{display:'grid', gridTemplateColumns:'repeat(3,1fr)', gap:14, marginBottom:12}}>
        {[['닉네임F','강남 밤모임','혼자 갔는데도 자연스럽게 어울릴 수 있었어요. 다음에도 또 갈 것 같아요.'],['닉네임G','런던 펍모임','같은 처지의 사람들과 얘기하니 위로가 됐어요. 시간 가는 줄 몰랐네요.'],['닉네임H','홍대 스터디 모임','정산이 깔끔하고 다들 매너가 좋아서 부담 없이 참여했어요.']].map((r,i)=><Reveal key={r[0]} delay={i*80}><div className="card" style={{padding:'16px 18px'}}>
          <div className="row" style={{gap:8, marginBottom:8}}><Av name={r[0]} size={28}/><b style={{fontSize:13.5}}>{r[0]}</b></div>
          <div className="meta" style={{marginBottom:6}}>{r[1]}</div>
          <p style={{fontSize:13, color:'var(--c-sub)', margin:0, lineHeight:1.6}}>{r[2]}</p>
        </div></Reveal>)}
      </div>
    </div>
    <Footer go={go} set={set}/>
  </div>;
}
const COMMUNITY_POSTS = [
  {id:'cp1', name:'지은', city:'런던', time:'2시간 전', topic:'같이 해요', body:'이번 주말에 하이드파크에서 가볍게 러닝하고 커피 마실 분 있나요?', tags:['러닝','커피','첫참가자환영'], likes:4, comments:3, relatedId:'c-gb1'},
  {id:'cp2', name:'민수', city:'서울', time:'5시간 전', topic:'현지 생활', body:'강남역 근처에 24시간 하는 스터디 카페 찾았는데 콘센트도 넉넉하고 조용해요.', tags:['현지생활','스터디'], likes:9, comments:2, relatedId:'m2'},
  {id:'cp3', name:'하나', city:'토론토', time:'어제', topic:'새로 왔어요', body:'토론토 온 지 일주일 됐어요. 같은 학교 출신 아니어도 좋으니 가볍게 인사 나눌 모임 있으면 알려주세요!', tags:['새로온사람','네트워킹'], likes:12, comments:6, relatedId:'c-ca1'},
  {id:'cp4', name:'태호', city:'서울', time:'2일 전', topic:'학교·워홀', body:'호주 워홀 마치고 서울 온 지 1년 됐는데, 같은 경험 있는 분들과 얘기하고 싶어서 등산 모임 열었어요.', tags:['등산','귀국적응'], likes:15, comments:5, relatedId:'m1'},
  {id:'cp5', name:'유진', city:'런던', time:'3일 전', topic:'같이 해요', body:'런던 소호 근처 브런치 맛집 다녀왔어요. 다음 모임 때 같이 가실 분들 구해요.', tags:['브런치','현지생활'], likes:7, comments:4, relatedId:'c-gb1'},
];
const LOCAL_INFO = [
  {title:'영국 학생비자(Student route) 신청 절차', body:'대학에서 CAS(입학허가확인서)를 받은 뒤, GOV.UK 공식 사이트에서 온라인으로 신청해요. 여권, CAS, 재정증명, 영어능력증명을 준비하고 생체정보(지문·사진) 등록 예약을 잡으면 됩니다. 영국 밖에서 신청하면 보통 3주, 영국 안에서 신청하면 최대 8주 정도 걸려요. 승인되면 실물 비자 카드 없이 온라인 계정(eVisa)으로 발급되는 방식이라, 계정 등록까지 꼭 마쳐야 해요.', verified:true, source:'GOV.UK 공식 안내 기준', author:'운영진', postedAt:'2026.07.28'},
  {title:'런던 오이스터카드 학생 할인, 오해하기 쉬운 부분', body:'18세 이상 런던 소재 학교 재학생은 TfL 공식 사이트에서 18+ Student Oyster 포토카드를 신청할 수 있어요. 다만 이 할인은 트래블카드·버스&트램 정기권에만 30% 적용되고, 그냥 건별 요금(Pay As You Go)에는 학생 할인이 붙지 않아요 — 많이들 헷갈리는 부분이라 미리 알아두면 좋아요. 신청엔 재학증명과 증명사진이 필요합니다.', verified:true, source:'TfL 공식 안내 기준', author:'운영진', postedAt:'2026.07.28'},
  {title:'영국 은행 계좌 개설, 뭐부터 준비하면 되나', body:'대부분 은행은 여권과 함께 학교에서 발급하는 재학확인서(이름·과정·기간·영국 내 주소 명시)를 요구해요. 일부 은행(HSBC 등)은 학기 시작 전부터 온라인으로 미리 개설을 시작할 수 있어서, 도착 전에 진행해두면 초반에 현금을 많이 들고 다닐 필요가 줄어들어요. 구체적인 은행별 조건은 계속 바뀌니, 최종 확인은 학교의 International Student Office나 UKCISA(영국 공식 유학생 지원 기관) 안내를 참고하는 걸 추천해요.', verified:true, source:'UKCISA 공식 안내 기준', author:'운영진', postedAt:'2026.07.28'},
  {title:'미국 유학 — SEVIS(I-901) 비용, 언제 얼마나 내나', body:'학교에서 I-20를 받은 뒤, 비자 인터뷰 예약(DS-160 작성) 전에 fmjfee.com에서 SEVIS I-901 수수료를 미리 내야 해요. F-1 학생 기준 350달러이고, 비자 수수료와는 완전히 별개의 요금이에요. 이 결제를 안 하면 인터뷰 자체가 안 잡히니, I-20 받자마자 제일 먼저 처리하는 걸 추천해요. 결제 영수증은 입국 심사 때도 요구될 수 있어서 꼭 보관해두세요.', verified:true, source:'미국 국토안보부(DHS)·ICE 공식 안내 기준', author:'운영진', postedAt:'2026.07.28'},
  {title:'항공권, 진짜 "화요일에 사면 싸다"가 맞을까?', body:'요즘 항공업계 데이터를 보면 "특정 요일에 예매하면 싸다"는 말은 사실 근거가 약해졌어요(과거 항공사들이 월요일 밤에 요금을 갱신하던 시절 생긴 낭설이라, 지금은 그 구조 자체가 없어졌어요). 요일보다 훨씬 중요한 건 "출발일까지 얼마나 남았을 때 사느냐"예요 — 국내선은 보통 출발 1~3개월 전, 국제선은 2~5개월 전이 대체로 저렴한 편이라고 해요. 다만 항공권 가격은 노선·시즌마다 편차가 커서 이건 "무조건 지켜야 할 법칙"이라기보다 참고용 경향으로 봐주세요. 가격 알림 기능을 켜두고 여유 있게 지켜보는 게 특정 요일을 노리는 것보다 실속 있어요.', author:'운영진', postedAt:'2026.07.28'},
  {title:'런던 맛집, 어디서 찾는 게 좋을까', body:'특정 가게를 콕 집어 추천드리기보다, 검증된 방법을 안내할게요. 보로우 마켓(Borough Market)처럼 오래된 대형 시장은 여러 노점을 한 번에 둘러볼 수 있어 실패 확률이 낮은 편이에요. 그 외 개별 맛집은 유행이 빠르고 가게가 자주 바뀌기 때문에, 저희가 이 자리에 딱 하나로 못 박아 두는 것보다 — 이야기 게시판에 "런던 맛집 추천"으로 검색하거나 직접 글을 올려서 최신 후기를 주고받는 걸 추천해요. 실제로 다녀온 유학생들의 최신 후기가 몇 달 전 정보보다 훨씬 정확해요.', author:'운영진', postedAt:'2026.07.28'},
  {title:'런던 대중교통 오이스터 카드 팁', body:'학생 할인 등록하면 통학·통근비를 크게 줄일 수 있어요. 등록은 TfL 사이트에서 여권과 재학증명만 있으면 돼요.', author:'운영진'},
  {title:'서울 귀국 후 통신사 재개통 순서', body:'유심 개통 전 본인 명의 확인이 필요해요. 알뜰폰이 학생에게는 대체로 더 저렴해요.', author:'운영진'},
  {title:'토론토 워홀 초기 정착 체크리스트', body:'SIN 번호, 은행 계좌, 핸드폰 순서로 처리하면 대기 시간을 줄일 수 있어요.', author:'운영진'},
];
function WScrExplore({S,set,go}){
  const {useState, useEffect, useRef} = React;
  const tab = S.communityTab||0;
  const TABS = ['이야기','Q&A','모임 후기','공지사항']; // 지역 정보 탭 제거(2026-07-30 사용자 확정 — 복잡도 축소)
  const TAB_SLUGS = ['stories','qna','reviews','notice'];
  const QNA_CATS = ['all','abroad','friends','return','local'];
  const initHQ = React.useMemo(()=>parseHashQuery(location.hash), []);
  const skipQuerySync = useRef(true);
  const [q, setQ] = useState(()=>{ try { return initHQ.get('q')||''; } catch(e) { return ''; } });
  const qCommittedRef = useRef(q); // 마지막으로 history에 확정 반영된 검색어(중복 push 방지)
  const qDebounceRef = useRef(null);
  useEffect(()=>{
    const hq = parseHashQuery(location.hash);
    const idx = TAB_SLUGS.indexOf(hq.get('tab'));
    if (idx!==-1 && idx!==tab) set({communityTab:idx});
    if (idx===1) { const catIdx = QNA_CATS.indexOf(hq.get('category')); if (catIdx>0) set({qtab:catIdx}); }
  }, []);
  useEffect(()=>{
    syncQuery('#/explore', {tab: tab===0?null:TAB_SLUGS[tab], category: (tab===1 && S.qtab) ? QNA_CATS[S.qtab] : null, q: (tab===0 && q) ? q : null}, skipQuerySync);
  }, [tab, S.qtab]);
  // 이야기 탭 검색어: 입력 중에는 replaceState로만 주소를 갱신하고(기록 과다 생성 방지), 잠시 멈추면 한 번만 push해 뒤로가기 한 단계로 되돌릴 수 있게 함
  useEffect(()=>{
    if (tab!==0) return;
    const usp = parseHashQuery(location.hash);
    if (q) usp.set('q', q); else usp.delete('q');
    const qs = usp.toString();
    const next = '#/explore'+(qs?('?'+qs):'');
    if (next !== location.hash) { try { history.replaceState({screen:'explore'}, '', next); } catch(e) {} }
    clearTimeout(qDebounceRef.current);
    qDebounceRef.current = setTimeout(()=>{
      if (q !== qCommittedRef.current) { try { history.pushState({screen:'explore'}, '', location.hash); } catch(e) {} qCommittedRef.current = q; }
    }, 600);
    return ()=>clearTimeout(qDebounceRef.current);
  }, [q, tab]);
  useEffect(()=>{
    const onPop = () => {
      if (!location.hash.startsWith('#/explore')) return;
      const hq = parseHashQuery(location.hash);
      skipQuerySync.current = true;
      const idx = TAB_SLUGS.indexOf(hq.get('tab'));
      const catIdx = QNA_CATS.indexOf(hq.get('category'));
      let restoredQ = ''; try { restoredQ = hq.get('q')||''; } catch(e) {}
      qCommittedRef.current = restoredQ;
      setQ(restoredQ);
      set({communityTab: idx!==-1?idx:0, qtab: (idx===1 && catIdx>0) ? catIdx : 0});
    };
    window.addEventListener('popstate', onPop);
    return ()=>window.removeEventListener('popstate', onPop);
  }, []);
  const topics = ['새로 왔어요','같이 해요','현지 생활','학교·워홀','귀국 후 적응','자유게시판'];
  const [topicFilter, setTopicFilter] = useState(null);
  const isAdmin = isAdminUser(S);
  const myCity = countryOf(S.regionVals.cur).city;

  // ── 서버(Supabase) 게시물 로딩 — 이야기/모임후기/공지사항 공용 ─────────────
  const [storyPosts, setStoryPosts] = useState([]);
  const [reviewPosts, setReviewPosts] = useState([]);
  const [noticePosts, setNoticePosts] = useState([]);
  const loadStory = () => fetchPosts('story').then(list=>setStoryPosts((list||[]).filter(x=>!isBlockedEmail(x.author_email))));
  React.useEffect(()=>{ const f=()=>loadStory(); document.addEventListener('mm:contentremoved', f); return ()=>document.removeEventListener('mm:contentremoved', f); }, []);
  const loadReview = () => fetchPosts('review').then(list=>setReviewPosts((list||[]).filter(x=>!isBlockedEmail(x.author_email))));
  React.useEffect(()=>{ const f=()=>loadReview(); document.addEventListener('mm:contentremoved', f); return ()=>document.removeEventListener('mm:contentremoved', f); }, []);
  const loadNotice = () => fetchPosts('notice').then(setNoticePosts);
  useEffect(()=>{ if(tab===0) loadStory(); }, [tab]);
  useEffect(()=>{ if(tab===2) loadReview(); }, [tab]);
  useEffect(()=>{ if(tab===3) loadNotice(); }, [tab]);

  const submitStory = () => {
    const body = S.storyDraftBody.trim();
    if (!body) return;
    createPost({category:'story', topic:S.storyDraftTopic, author_email:S.acctEmail, author_name:S.profile.nickname||'나', body, related_meetup_id:S.storyDraftRelated||null, is_admin_post:isAdmin}).then(res=>{
      if (res&&res.error) { set({toast:'글 등록에 실패했어요'}); return; }
      set({storyComposeOpen:false, storyDraftBody:'', storyDraftRelated:'', storyPage:1, toast:'글이 올라갔어요'});
      loadStory();
    });
  };
  const submitNotice = () => {
    const body = S.noticeDraftBody.trim();
    if (!body || !isAdmin) return;
    createPost({category:'notice', topic:'공지', author_email:S.acctEmail, author_name:'운영진', body, is_admin_post:true, font_size:S.noticeDraftSize||'normal'}).then(res=>{
      if (res&&res.error) { set({toast:'공지 등록에 실패했어요'}); return; }
      set({noticeComposeOpen:false, noticeDraftBody:'', noticeDraftSize:'normal', toast:'공지사항이 등록됐어요'});
      loadNotice();
    });
  };
  const noticeTextareaRef = useRef(null);
  const insertBold = () => {
    const ta = noticeTextareaRef.current; if (!ta) return;
    const start = ta.selectionStart, end = ta.selectionEnd;
    const val = S.noticeDraftBody;
    const selected = val.slice(start, end) || '강조할 문구';
    const next = val.slice(0, start) + '**' + selected + '**' + val.slice(end);
    set({noticeDraftBody: next});
    setTimeout(()=>{ ta.focus(); ta.setSelectionRange(start+2, start+2+selected.length); }, 0);
  };
  const removePost = (id, reload) => { if(!isAdmin) return; deletePost(id).then(res=>{ if(res&&res.error){ set({toast:'삭제 실패'}); return; } set({toast:'삭제했어요'}); reload(); }); };

  let filteredStory = storyPosts;
  if (topicFilter) filteredStory = filteredStory.filter(p=>p.topic===topicFilter);
  if (q.trim()) filteredStory = filteredStory.filter(p=>(p.body||'').includes(q.trim()));
  const storyPerPage = 7;
  const storyTotalPages = Math.max(1, Math.ceil(filteredStory.length/storyPerPage));
  const storyPage = Math.min(S.storyPage||1, storyTotalPages);
  const pagedStoryPosts = filteredStory.slice((storyPage-1)*storyPerPage, storyPage*storyPerPage);

  return <div className="wapp"><WNav S={S} go={go} set={set}/>
    <div className="wmain" style={{maxWidth:680}}>
      <div className="wsect"><h2>커뮤니티</h2></div>
      <div className="row" style={{gap:6, marginBottom:18, overflowX:'auto'}}>
        {TABS.map((t,i)=><button key={t} className={'qtab'+(tab===i?' on':'')} style={{whiteSpace:'nowrap'}} onClick={()=>set({communityTab:i})}>{t}</button>)}
      </div>

      {tab===0&&<>
        <p className="meta" style={{marginBottom:10}}>현재 지역: {myCity}</p>
        <div className="row" style={{gap:8, marginBottom:16}}>
          <div className="row" style={{gap:8, background:'var(--c-fill)', borderRadius:10, padding:'10px 14px', flex:1}}>
            <Ic n="IconNormalSearchNameSearchThickFalse" s={17} c="var(--c-faint)"/>
            <input value={q} onChange={e=>setQ(e.target.value)} placeholder="이야기 검색" style={{flex:1, border:'none', background:'none', fontFamily:'inherit', fontSize:14, outline:'none'}}/>
          </div>
          <DSBtn label="글쓰기" size="md" onClick={()=>guardAuth(S,set,go,()=>set({storyComposeOpen:!S.storyComposeOpen}))}/>
        </div>
        {S.storyComposeOpen&&<div className="card" style={{marginBottom:16}}>
          <b style={{fontSize:15}}>새 이야기 쓰기</b>
          <div style={{display:'flex',gap:8,margin:'12px 0', flexWrap:'wrap'}}>{topics.map(t=><Chip key={t} label={t} onClick={()=>set({storyDraftTopic:t})} on={S.storyDraftTopic===t}/>)}</div>
          <textarea value={S.storyDraftBody} onChange={e=>set({storyDraftBody:e.target.value})} placeholder="어떤 이야기를 나누고 싶으세요?" rows={3} style={{width:'100%',border:'1px solid var(--c-line)',borderRadius:8,padding:'10px 14px',fontFamily:'inherit',fontSize:14,marginBottom:10,outline:'none',resize:'none'}}/>
          {S.myHostedMeetups.length>0&&<div style={{marginBottom:12}}>
            <div className="meta" style={{marginBottom:6}}>관련 모임 연결 (선택)</div>
            <select value={S.storyDraftRelated} onChange={e=>set({storyDraftRelated:e.target.value})} style={{width:'100%', border:'1px solid var(--c-line)', borderRadius:8, padding:'10px 14px', fontFamily:'inherit', fontSize:14, outline:'none'}}>
              <option value="">연결 안 함</option>
              {S.myHostedMeetups.map((hm,i)=><option key={i} value={'my'+i}>{hm.title||'(제목 없음)'}</option>)}
            </select>
          </div>}
          <DSBtn label="올리기" size="md" disable={!S.storyDraftBody.trim()} onClick={submitStory}/>
        </div>}
        <div className="row" style={{gap:8, flexWrap:'wrap', marginBottom:20}}>
          <Chip label="전체" onClick={()=>setTopicFilter(null)} on={!topicFilter}/>
          {topics.map(t=><Chip key={t} label={t} onClick={()=>setTopicFilter(t)} on={topicFilter===t}/>)}
        </div>
        {pagedStoryPosts.length===0?<div className="card" style={{textAlign:'center', padding:'32px 20px'}}>
          <p style={{fontSize:14.5, fontWeight:700, margin:'0 0 6px'}}>{topicFilter||q.trim()?'해당하는 이야기가 없어요.':'아직 등록된 이야기가 없어요. 서버 연동 전이면 여기가 비어있는 게 정상이에요.'}</p>
          <p className="meta" style={{margin:0}}>모임 경험이나 유학 생활 이야기를 처음으로 나눠보세요.</p>
        </div>:pagedStoryPosts.map((p,i)=><Reveal key={p.id} delay={i*70}><div className="card" style={{marginBottom:12, padding:'16px 18px', cursor:'pointer'}} onClick={()=>set({qnaDetailPost:{...p, postSource:'story'}, screen:'postdetail'})}>
          <div className="row" style={{gap:8, marginBottom:8, justifyContent:'space-between'}}>
            <div className="row" style={{gap:8}}><Av name={p.author_name} size={30}/><span style={p.is_admin_post?{fontSize:13.5, fontWeight:900, color:'var(--c-primary)'}:{fontSize:13.5, fontWeight:700}}>{p.author_name}{p.is_admin_post&&<span className="chip green" style={{fontSize:10,padding:'1px 6px',marginLeft:5}}>운영진</span>}</span><span className="meta">· {timeAgo(new Date(p.created_at).getTime())}</span>{p.topic&&<span className="chip" style={{fontSize:11}}>{p.topic}</span>}</div>
            {(isAdmin||p.author_email===S.acctEmail)&&<button style={{background:'none',border:'none',color:'var(--c-red)',fontSize:12,cursor:'pointer'}} onClick={e=>{e.stopPropagation(); if(isAdminUser(S)&&p.author_email&&p.author_email!==S.acctEmail){ set({adminRemove:{kind:'post', id:p.id, ownerEmail:p.author_email, title:(p.topic||p.body||'게시글').slice(0,30)}}); return;} removePost(p.id, loadStory);}}>삭제</button>}
          </div>
          <p style={{fontSize:14, color:'var(--c-text)', margin:'0 0 8px', lineHeight:1.6}}>{p.body}</p>
          {p.related_meetup_id&&<button style={{background:'none', border:'none', color:'var(--c-primary)', fontFamily:'inherit', fontSize:12.5, fontWeight:700, cursor:'pointer', padding:0}} onClick={e=>{e.stopPropagation(); MEETUPS[p.related_meetup_id]?go('detail',{detailId:p.related_meetup_id}):set({toast:'연결된 모임을 찾을 수 없어요'});}}>관련 모임 보기</button>}
        </div></Reveal>)}
        <Pagination page={storyPage} total={storyTotalPages} onChange={n=>set({storyPage:n})}/>
      </>}

      {tab===1&&<WScrQnAInline S={S} set={set} go={go}/>}

      {tab===2&&<>
        {(()=>{ const eligible = S.completed.map(id=>MEETUPS[id]).filter(Boolean);
          const perPage=7, totalPages=Math.max(1,Math.ceil(reviewPosts.length/perPage)), page=Math.min(S.reviewPage||1,totalPages), paged=reviewPosts.slice((page-1)*perPage, page*perPage);
          return <>
          <div className="row" style={{gap:8, marginBottom:16}}>
            <DSBtn label="후기 쓰기" size="md" onClick={()=>guardAuth(S,set,go,()=>{ if(eligible.length===0){ set({toast:'실제로 참석 완료한 모임이 있어야 후기를 쓸 수 있어요'}); return; } set({reviewComposeOpen:!S.reviewComposeOpen, reviewDraftMeetupId:eligible[0]?eligible[0].id:''}); })}/>
            <span className="meta" style={{alignSelf:'center'}}>{eligible.length===0?'참석 완료한 모임이 있어야 후기를 쓸 수 있어요':`참석 완료 ${eligible.length}건 — 후기를 남길 수 있어요`}</span>
          </div>
          {S.reviewComposeOpen&&eligible.length>0&&<div className="card" style={{marginBottom:16}}>
            <b style={{fontSize:15}}>후기 쓰기</b>
            <div className="meta" style={{margin:'10px 0 6px'}}>어떤 모임이었나요?</div>
            <select value={S.reviewDraftMeetupId} onChange={e=>set({reviewDraftMeetupId:e.target.value})} style={{width:'100%', border:'1px solid var(--c-line)', borderRadius:8, padding:'10px 14px', fontFamily:'inherit', fontSize:14, outline:'none', marginBottom:10}}>
              {eligible.map(m=><option key={m.id} value={m.id}>{m.title}</option>)}
            </select>
            <textarea value={S.reviewDraftBody} onChange={e=>set({reviewDraftBody:e.target.value})} placeholder="어떤 모임이었는지 자유롭게 남겨주세요" rows={3} style={{width:'100%',border:'1px solid var(--c-line)',borderRadius:8,padding:'10px 14px',fontFamily:'inherit',fontSize:14,marginBottom:12,outline:'none',resize:'none'}}/>
            <DSBtn label="올리기" size="md" disable={!S.reviewDraftBody.trim()} onClick={()=>{
              const m = MEETUPS[S.reviewDraftMeetupId];
              createPost({category:'review', topic:m.title, author_email:S.acctEmail, author_name:S.profile.nickname||'나', body:S.reviewDraftBody.trim(), related_meetup_id:S.reviewDraftMeetupId, meetup_title:m.title, meetup_when:m.when, meetup_place:m.place, meetup_size:effectiveJoined(m)+'명 참여', is_admin_post:isAdmin}).then(res=>{
                if (res&&res.error) { set({toast:'후기 등록에 실패했어요'}); return; }
                set({reviewComposeOpen:false, reviewDraftBody:'', reviewPage:1, toast:'후기가 올라갔어요'});
                loadReview();
              });
            }}/>
          </div>}
          {paged.length===0?<div className="card" style={{textAlign:'center', padding:'32px 20px'}}>
            <p style={{fontSize:14.5, fontWeight:700, margin:'0 0 6px'}}>아직 작성된 후기가 없어요.</p>
            <p className="meta" style={{margin:0}}>모임이 끝나면 참가자의 첫 후기를 확인할 수 있어요.</p>
          </div>:<>{paged.map((r,i)=><Reveal key={r.id} delay={i*80}><div className="card" style={{marginBottom:12, padding:'16px 18px'}}>
            <div className="row" style={{gap:8, marginBottom:8, justifyContent:'space-between'}}>
              <div className="row" style={{gap:8}}><Av name={r.author_name} size={28}/><b style={{fontSize:13.5}}>{r.author_name}</b><span className="meta">· {r.meetup_title} · {timeAgo(new Date(r.created_at).getTime())}</span></div>
              {(isAdmin||r.author_email===S.acctEmail)&&<button style={{background:'none',border:'none',color:'var(--c-red)',fontSize:12,cursor:'pointer'}} onClick={()=>{ if(isAdminUser(S)&&r.author_email&&r.author_email!==S.acctEmail){ set({adminRemove:{kind:'post', id:r.id, ownerEmail:r.author_email, title:(r.meetup_title||r.body||'후기').slice(0,30)}}); return;} removePost(r.id, loadReview); }}>삭제</button>}
            </div>
            {(r.meetup_when||r.meetup_place||r.meetup_size)&&<div className="meta" style={{marginBottom:8, fontSize:12}}>{[r.meetup_when,r.meetup_place,r.meetup_size].filter(Boolean).join(' · ')}</div>}
            <p style={{fontSize:13.5, color:'var(--c-sub)', margin:0, lineHeight:1.6}}>{r.body}</p>
          </div></Reveal>)}
          <Pagination page={page} total={totalPages} onChange={n=>set({reviewPage:n})}/></>}
        </>; })()}
      </>}


      {tab===3&&<>
        {isAdmin&&<div className="row" style={{gap:8, marginBottom:16}}>
          <DSBtn label={S.noticeComposeOpen?'취소':'공지 작성'} size="md" onClick={()=>set({noticeComposeOpen:!S.noticeComposeOpen})}/>
          <span className="meta" style={{alignSelf:'center'}}>운영진 계정으로 로그인 중이라 공지를 작성할 수 있어요</span>
        </div>}
        {isAdmin&&S.noticeComposeOpen&&<div className="card" style={{marginBottom:16}}>
          <b style={{fontSize:15}}>공지사항 작성</b>
          <div className="row" style={{gap:8, margin:'10px 0'}}>
            <button type="button" style={{background:'var(--c-fill)', border:'none', borderRadius:6, padding:'6px 12px', fontFamily:'inherit', fontSize:13, fontWeight:800, cursor:'pointer'}} onClick={insertBold}>B 굵게</button>
            <select value={S.noticeDraftSize||'normal'} onChange={e=>set({noticeDraftSize:e.target.value})} style={{border:'1px solid var(--c-line)', borderRadius:6, padding:'6px 10px', fontFamily:'inherit', fontSize:13, outline:'none'}}>
              <option value="normal">보통 글자 크기</option>
              <option value="large">크게</option>
              <option value="xlarge">아주 크게</option>
            </select>
            <span className="meta" style={{alignSelf:'center'}}>강조하고 싶은 문구를 선택하고 "B 굵게"를 누르세요</span>
          </div>
          <textarea ref={noticeTextareaRef} value={S.noticeDraftBody} onChange={e=>set({noticeDraftBody:e.target.value})} placeholder="공지 내용을 입력하세요" rows={5} style={{width:'100%',border:'1px solid var(--c-line)',borderRadius:8,padding:'10px 14px',fontFamily:'inherit',fontSize:14,marginBottom:8,outline:'none',resize:'none'}}/>
          <div style={{marginBottom:12}}>
            <div className="meta" style={{marginBottom:4}}>미리보기</div>
            <div className="card" style={{background:'var(--c-fill-a)', padding:'10px 14px'}} dangerouslySetInnerHTML={{__html: renderNoticeBody(S.noticeDraftBody)||'<span style="color:var(--c-faint)">내용을 입력하면 여기 미리보기가 나와요</span>'}}/>
          </div>
          <DSBtn label="게시하기" size="md" disable={!S.noticeDraftBody.trim()} onClick={submitNotice}/>
        </div>}
        {noticePosts.length===0?<div className="card" style={{textAlign:'center', padding:'32px 20px'}}>
          <p style={{fontSize:14.5, fontWeight:700, margin:0}}>등록된 공지사항이 없어요.</p>
        </div>:noticePosts.map((p,i)=><Reveal key={p.id} delay={i*70}><div className="card" style={{marginBottom:12, padding:'16px 18px'}}>
          <div className="row" style={{gap:8, marginBottom:8, justifyContent:'space-between'}}>
            <div className="row" style={{gap:8}}><span className="chip green" style={{fontSize:11}}>운영진</span><b style={{fontSize:13.5}}>공지사항</b><span className="meta">· {timeAgo(new Date(p.created_at).getTime())}</span></div>
            {isAdmin&&<button style={{background:'none',border:'none',color:'var(--c-red)',fontSize:12,cursor:'pointer'}} onClick={()=>removePost(p.id, loadNotice)}>삭제</button>}
          </div>
          <div dangerouslySetInnerHTML={{__html: renderNoticeBody(p.body)}} style={{color:'var(--c-text)', lineHeight:1.6, fontSize:p.font_size==='xlarge'?19:p.font_size==='large'?16:14}}/>
        </div></Reveal>)}
      </>}
    </div>
    <Footer go={go} set={set}/>
  </div>;
}
function WScrQnAInline({S,set,go}){
  const {useState, useEffect} = React;
  const tabs=['전체','해외거주 Q&A','친구만들기','귀국 적응'];
  const t=S.qtab||0;
  const [serverPosts, setServerPosts] = useState([]);
  useEffect(()=>{ fetchPosts('qna').then(setServerPosts); }, []);
  const base=DEMO_MODE?[{cat:1,tag:'해외거주 Q&A',title:'지금 영국에서 워홀 중인데, 저 같은 사람 여기 있나요?',body:'런던 온 지 3개월 됐는데 여기서도 한인 유학생·워홀러 분들과 연결돼서 지내고 싶어요.',meta:'지은 · 런던 · 공감 5'},{cat:2,tag:'친구만들기',title:'서울 사는 미국 고등학교 출신 분 계신가요?',body:'귀국한 지 1년 됐는데 아직도 유학 시절 얘기할 친구가 없어요.',meta:'태호 · 서울 · 공감 3'},{cat:1,tag:'해외거주 Q&A',title:'토론토 워홀 비자 팁 나눠요',body:'워홀 비자 준비하며 헷갈렸던 부분 정리해봤어요.',meta:'하나 · 토론토 · 공감 6'},{cat:3,tag:'귀국 적응',title:'귀국하고 다시 적응하는 데 얼마나 걸리셨나요',body:'생각보다 역문화충격이 크네요. 다들 어떻게 극복하셨어요?',meta:'유진 · 서울 · 공감 4'}]:[];
  const tagOf = i => ['전체','해외거주 Q&A','친구만들기','귀국 적응'][i];
  const catOf = topic => tagOf(1)===topic?1:tagOf(2)===topic?2:tagOf(3)===topic?3:0;
  const fromServer = serverPosts.map(p=>{ const sp=qnaSplit(p.body); return {...p, ...sp, cat:catOf(p.topic), tag:p.topic, postSource:'qna'}; });
  const posts=[...fromServer, ...base].filter(p=>t===0||p.cat===t);
  return <div>
    <div className="row" style={{gap:6, marginBottom:16, overflowX:'auto'}}>{tabs.map((tb,i)=><Chip key={tb} label={tb} onClick={()=>set({qtab:i})} on={t===i}/>)}</div>
    {posts.length===0&&<div className="card" style={{textAlign:'center', padding:'32px 20px', marginBottom:12}}>
      <p style={{fontSize:14.5, fontWeight:700, margin:'0 0 6px'}}>아직 등록된 질문이 없어요.</p>
      <p className="meta" style={{margin:'0 0 14px'}}>유학 생활, 귀국 후 생활, 모임에 관해 궁금한 점을 처음으로 남겨보세요.</p>
      <DSBtn label="첫 질문 남기기" size="md" onClick={()=>guardAuth(S,set,go,()=>go('qna',{composeOpen:true, draftTag:1, draftTitle:'', draftBody:''}))}/>
    </div>}
    {posts.map((p,i)=><Reveal key={p.id||p.title+i} delay={Math.min(i,6)*70}><div className="card" style={{marginBottom:12, cursor:'pointer', padding:'16px 18px'}} onClick={()=>set({qnaDetailPost:p, screen:'postdetail'})}>
      <Chip label={p.tag} tone="primary"/>
      <div style={{fontSize:15.5,fontWeight:700,margin:'10px 0 6px'}}>{p.title}</div>
      <p style={{fontSize:13.5,color:'var(--c-sub)',margin:'0 0 10px',lineHeight:1.6}}>{p.body}</p>
      <span className="meta">{p.meta || (p.postSource==='qna' ? `${p.author_name||'익명'} · ${timeAgo(new Date(p.created_at).getTime())}` : '')}</span>
    </div></Reveal>)}
    <button className="card" style={{width:'100%', textAlign:'left', cursor:'pointer', fontFamily:'inherit', background:'var(--c-primary-bg)', border:'none', padding:'14px 16px'}} onClick={()=>go('qna')}>Q&A 전체 보기 · 검색 · 글쓰기</button>
  </div>;
}
function WScrRegion({S,set,go}){
  const rows=[['cur','현재 거주','IconNormalLocation2NameLocationFillFillTrue'],['move','이동(귀국) 예정','IconNormalChangeNameChange'],['interest','관심 지역','IconNormalGlobe2NameGlobeFillFalse']];
  return <div className="wapp"><WNav S={S} go={go} set={set}/>
    <div className="wmain" style={{maxWidth:640}}>
      <div className="wsect"><h2>지역 설정</h2></div>
      <p style={{margin:'0 0 18px',fontSize:14,color:'var(--c-sub)',lineHeight:1.6}}>거주·귀국·관심 지역을 나눠 두면 지금 있는 곳과 돌아갈 곳의 모임을 함께 추천해드려요.</p>
      {rows.map(r=>{const editing=S.regionEdit===r[0]; const isCountry=r[0]==='cur'||r[0]==='interest'; const isMove=r[0]==='move'; const locked=r[0]==='cur'&&S.locGranted; return <div key={r[0]} className="card" style={{display:'flex',alignItems:editing&&(isCountry||isMove)?'flex-start':'center',gap:14,marginBottom:10}}>
        <Ic n={r[2]} s={22} c="var(--c-primary)" style={{marginTop:editing&&(isCountry||isMove)?2:0}}/>
        {locked?<>
          <span style={{flex:1}}><span className="meta">{r[1]}</span><br/><b style={{fontSize:16}}>{S.regionVals[r[0]]}</b></span>
          <Chip icon="IconNormalLocation2NameLocationFillFillTrue" label="위치 자동 설정" tone="violet"/>
        </>:editing?(isMove?<div style={{flex:1}}>
          <div className="meta" style={{marginBottom:6}}>귀국 예정일</div>
          <input type="date" value={S.moveDate} onChange={e=>set({moveDate:e.target.value})} style={{width:'100%',border:'1px solid var(--c-line)',borderRadius:8,padding:'8px 12px',fontFamily:'inherit',fontSize:14,outline:'none',marginBottom:10}}/>
          <div className="meta" style={{marginBottom:6}}>귀국 국가</div>
          <CountrySelect value={S.moveCountry} onChange={v=>set({moveCountry:v})}/>
          <button style={{marginTop:8,background:'var(--c-primary)',color:'#fff',border:'none',borderRadius:8,padding:'8px 14px',fontFamily:'inherit',fontSize:13,fontWeight:700,cursor:'pointer'}} onClick={()=>{
            const d = S.moveDate ? new Date(S.moveDate) : null;
            const label = d ? `${d.getMonth()+1}월 ${d.getDate()}일` : S.regionVals.move.split(' · ')[0];
            set({regionVals:{...S.regionVals, move:`${label} · ${S.moveCountry}`}, regionEdit:null});
          }}>저장</button>
          <button style={{marginTop:8,marginLeft:8,background:'none',color:'var(--c-sub)',border:'1px solid var(--c-line)',borderRadius:8,padding:'8px 14px',fontFamily:'inherit',fontSize:13,fontWeight:700,cursor:'pointer'}} onClick={()=>set({regionVals:{...S.regionVals, move:'설정 안 함'}, regionEdit:null, moveDate:''})}>비워두기</button>
        </div>:isCountry?<div style={{flex:1}}>
          <div className="meta" style={{marginBottom:6}}>{r[1]} — 국가 선택</div>
          <CountrySelect value={S.regionDraft} onChange={v=>set({regionDraft:v})}/>
          <button style={{marginTop:8,background:'var(--c-primary)',color:'#fff',border:'none',borderRadius:8,padding:'8px 14px',fontFamily:'inherit',fontSize:13,fontWeight:700,cursor:'pointer'}} onClick={()=>set({regionVals:{...S.regionVals,[r[0]]:S.regionDraft||S.regionVals[r[0]]}, regionEdit:null})}>저장</button>
        </div>:<>
          <input autoFocus value={S.regionDraft} onChange={e=>set({regionDraft:e.target.value})} onKeyDown={e=>{if(e.key==='Enter') set({regionVals:{...S.regionVals,[r[0]]:S.regionDraft||S.regionVals[r[0]]}, regionEdit:null});}} style={{flex:1,border:'1px solid var(--c-line)',borderRadius:8,padding:'8px 12px',fontFamily:'inherit',fontSize:15,outline:'none'}}/>
          <button style={{background:'var(--c-primary)',color:'#fff',border:'none',borderRadius:8,padding:'8px 14px',fontFamily:'inherit',fontSize:13,fontWeight:700,cursor:'pointer'}} onClick={()=>set({regionVals:{...S.regionVals,[r[0]]:S.regionDraft||S.regionVals[r[0]]}, regionEdit:null})}>저장</button>
        </>):<>
          <button style={{flex:1,background:'none',border:'none',textAlign:'left',cursor:'pointer',fontFamily:'inherit',padding:0}} onClick={()=>set({regionEdit:r[0], regionDraft:S.regionVals[r[0]]})}><span className="meta">{r[1]}</span><br/><b style={{fontSize:16}}>{S.regionVals[r[0]]}</b></button>
          <Chip label={r[0]==='move'?'선택':'필수'} tone={r[0]==='move'?'line':'primary'}/>{/* 2026-07-30 확정: 이동예정=선택, 관심지역=필수 */}
          <button className="iconbtn" aria-label="수정" onClick={()=>set({regionEdit:r[0], regionDraft:S.regionVals[r[0]]})}><Ic n="IconNormalPencil2NamePencilFillFalse" s={17} c="var(--c-faint)"/></button>
        </>}
      </div>;})}
      {S.locGranted&&<p className="meta" style={{margin:'-2px 2px 14px', lineHeight:1.6}}>현재 거주 지역은 위치 정보로 자동 설정돼 직접 바꿀 수 없어요. 변경하려면 브라우저의 위치 권한을 다시 설정해주세요.</p>}
      <div className="card" style={{marginBottom:10}}>
        <span className="meta">체류 기간 유형</span>
        <div style={{display:'flex',gap:8,marginTop:10,flexWrap:'wrap',justifyContent:'space-evenly'}}>{['단기 여행','교환학기','유학생','워홀','장기거주'].map((t,i)=><Chip key={t} label={t} onClick={()=>set({stayType:t})} on={(S.stayType||'교환학기')===t}/>)}</div>
      </div>
      <DSBtn label="저장하기" onClick={()=>go('home',{toast:'지역 설정이 저장됐어요 — 홈 추천에 바로 반영돼요'})}/>
    </div>
  </div>;
}
function WScrBrowseRegions({S,set,go}){
  const {useState, useEffect, useRef} = React;
  const events = useEvents();
  const initQ = React.useMemo(()=>parseHashQuery(location.hash), []);
  const validKind = ['all','member','official'];
  const type = S.browseregionsType||'all';
  const setType = t => { set({browseregionsType:t}); };
  const skipQuerySync = useRef(true); // 마운트 시 1회는 URL이 이미 맞는 상태이므로 재-push 방지
  const CITY={KR:'서울',GB:'런던',US:'뉴욕',CA:'토론토',BE:'브뤼셀'};
  const curCode = countryOf(S.regionVals.cur).code;
  const ALL=DEMO_MODE?[
    {code:'KR',id:'m2',title:'강남 밤모임',when:'오늘(목) 오후 8시',size:'4~6명',cardTags:[['친구 만들기','primary'],['조용한 대화','']],soloLine:'혼자 참여 환영',capMax:6,capJoined:4,host:{name:'지은',reviews:24},dateBucket:'today'},
    {code:'GB',id:'c-gb1',title:'런던 한인 브런치 모임',when:'토 오전 11시',size:'4~6명',cardTags:[['브런치','primary'],['조용한 대화','']],soloLine:'첫 참가자 환영 · 대기 1',capMax:6,capJoined:2,dateBucket:'week'},
    {code:'US',id:'c-us1',title:'뉴욕 커리어 커피챗',when:'오늘(수) 오후 6시',size:'4~6명',cardTags:[['커리어','primary'],['조용한 대화','']],soloLine:'첫 참가자 환영 · 대기 1',capMax:6,capJoined:3,dateBucket:'today'},
    {code:'CA',id:'c-ca1',title:'토론토 유학생 네트워킹',when:'금 오후 7시',size:'4~6명',cardTags:[['네트워킹','primary'],['조용한 대화','']],soloLine:'첫 참가자 환영 · 대기 1',capMax:6,capJoined:2,dateBucket:'week'},
    {code:'BE',id:'c-be1',title:'브뤼셀 유학생 브런치',when:'토 오전 11시',size:'4~6명',cardTags:[['브런치','primary'],['조용한 대화','']],soloLine:'첫 참가자 환영 · 대기 1',capMax:6,capJoined:1,dateBucket:'week'},
    {code:'KR',id:'m1',title:'유학생 귀국 환영 웰컴파티',when:'8월 2일(토) 오후 6시',size:'정원 40명',cardTags:[['동문교류','primary'],['활발한 네트워킹','']],soloLine:'혼자 참여 환영',capMax:40,capJoined:37,host:{name:'윤영진',reviews:34},dateBucket:'month'},
    {code:'GB',id:'c-gb2',title:'런던 스터디 카페 모임',when:'화 오후 7시',size:'4~6명',cardTags:[['스터디','primary'],['첫 참가자 환영','green']],soloLine:'방금 등록됨',capMax:6,capJoined:5,dateBucket:'week'},
    {code:'US',id:'c-us2',title:'뉴욕 러닝 크루',when:'토 오전 9시',size:'4~6명',cardTags:[['운동','primary'],['첫 참가자 환영','green']],soloLine:'방금 등록됨',capMax:6,capJoined:5,dateBucket:'week'},
    {code:'CA',id:'c-ca2',title:'토론토 스터디 모임',when:'일 오후 2시',size:'4~6명',cardTags:[['스터디','primary'],['첫 참가자 환영','green']],soloLine:'방금 등록됨',capMax:6,capJoined:5,dateBucket:'week'},
    {code:'BE',id:'c-be2',title:'브뤼셀 스터디 모임',when:'화 오후 7시',size:'4~6명',cardTags:[['스터디','primary'],['첫 참가자 환영','green']],soloLine:'방금 등록됨',capMax:6,capJoined:5,dateBucket:'week'},
    {code:'KR',id:'x1',title:'귀국 취준 커피챗',when:'수 오후 8시',size:'4~6명',cardTags:[['커리어','primary'],['조용한 대화','']],soloLine:'첫 참가자 환영 · 대기 1',capMax:6,capJoined:6,dateBucket:'week'},
    {code:'GB',id:'x2',title:'한인 유학생 펍모임',when:'금 오후 8시',size:'4~6명',cardTags:[['펍모임','primary']],soloLine:'혼자 참여 환영',capMax:6,capJoined:6,dateBucket:'week'},
    {code:'US',id:'x6',title:'뉴욕 유학생 네트워킹 브런치',when:'일 오전 11시',size:'4~6명',cardTags:[['네트워킹','primary']],soloLine:'혼자 참여 환영',capMax:6,capJoined:6,dateBucket:'week'},
    {code:'CA',id:'x3',title:'워홀러 브런치 모임',when:'토 오전 11시',size:'4~6명',cardTags:[['CA 토론토','line']],soloLine:'첫 참가자 비율 70%',capMax:6,capJoined:6,dateBucket:'week'},
    {code:'BE',id:'x7',title:'브뤼셀 유학생 모임',when:'금 오후 7시',size:'4~6명',cardTags:[['BE 브뤼셀','line']],soloLine:'혼자 참여 환영',capMax:6,capJoined:6,dateBucket:'week'},
    {code:'ON',id:'on1',title:'온라인 영어 회화 스터디',when:'매주 화 오후 9시 · 화상',size:'4~6명',cardTags:[['스터디','primary'],['첫 참가자 환영','green']],soloLine:'방금 등록됨',capMax:6,capJoined:3,dateBucket:'week',online:true},
    {code:'ON',id:'on2',title:'온라인 커리어 코칭 톡',when:'목 오후 8시 · 화상',size:'4~6명',cardTags:[['커리어','primary']],soloLine:'첫 참가자 환영 · 대기 1',capMax:6,capJoined:4,dateBucket:'week',online:true},
  ]:[];
  const OFFICIAL = events.map(e=>({code:'KR', id:e.id, title:e.title, when:e.when, size:e.mode==='firstcome'?`정원 ${e.cap}명`:'추첨제', cardTags:[[e.tag, e.tone]], soloLine:e.cost, capMax:e.mode==='firstcome'?e.cap:null, capJoined:e.mode==='firstcome'?e.applied:null, official:true, isEvent:true, dateBucket:'month'}));
  const base = (type==='official'?OFFICIAL:type==='member'?ALL:[...ALL, ...OFFICIAL]);
  const ACTIVITIES = [...new Set(base.map(m=>m.cardTags&&m.cardTags[0]&&m.cardTags[0][0]).filter(t=>t&&!/^[A-Z]{2} /.test(t)))];
  const popupCat = POPUP_ACTIVITIES.find(([label])=>label===S.pendingActivityFilter);
  const matchedActivity = popupCat ? ACTIVITIES.find(a=>popupCat[2].test(a)) : null;
  const [filters,setFilters] = useState(()=>{
    const region = initQ.get('region');
    const date = initQ.get('date');
    const activity = initQ.get('activity');
    return {
      region: region && (CITY[region]||region==='all') ? region : ((S.pendingActivityFilter||S.pendingCustomActivity)?'all':(CITY[curCode]?curCode:'all')),
      date: ['today','week','all'].includes(date) ? date : 'all',
      activity: activity || matchedActivity || 'all', // 목록 없는 값이면 아래 effect에서 안전하게 정리
      online: initQ.get('online')==='true',
      beginner: initQ.get('beginner')==='true',
    };
  });
  useEffect(()=>{ if(S.pendingActivityFilter||S.pendingCustomActivity) set({pendingActivityFilter:null, pendingCustomActivity:null}); }, []);
  // 마운트 시 kind 쿼리 유효성 검사 후 1회 반영
  useEffect(()=>{ const kind=initQ.get('kind'); if(validKind.includes(kind) && kind!==type) setType(kind); }, []);
  // activity는 목록(ACTIVITIES) 계산 이후에만 검증 가능 — 잘못된 값이면 조용히 'all'로 정리
  useEffect(()=>{ if(filters.activity!=='all' && !ACTIVITIES.includes(filters.activity)) setFilters(f=>({...f, activity:'all'})); }, [ACTIVITIES.join('|')]);
  const patch = p => setFilters(f=>({...f,...p}));
  const toggle = (key,val) => patch({[key]: filters[key]===val?'all':val});
  // 필터·구분(type) 변경을 쿼리로 반영 — 기본값 생략, 실제 변경 시에만 push(중복 방지)
  useEffect(()=>{
    syncQuery('#/browseregions', {kind: type==='all'?null:type, region: filters.region==='all'?null:filters.region, date: filters.date==='all'?null:filters.date, activity: filters.activity==='all'?null:filters.activity, online: filters.online?'true':null, beginner: filters.beginner?'true':null}, skipQuerySync);
  }, [type, filters.region, filters.date, filters.activity, filters.online, filters.beginner]);
  // 브라우저 뒤로가기·앞으로가기로 이 화면에 쿼리가 다시 적용될 때 상태를 반영(재-push 방지)
  useEffect(()=>{
    const onPop = () => {
      if (!location.hash.startsWith('#/browseregions')) return;
      const q = parseHashQuery(location.hash);
      skipQuerySync.current = true;
      const kind = q.get('kind'); if (validKind.includes(kind)) setType(kind); else if (!q.has('kind')) setType('all');
      setFilters({
        region: q.get('region') && (CITY[q.get('region')]||q.get('region')==='all') ? q.get('region') : 'all',
        date: ['today','week'].includes(q.get('date')) ? q.get('date') : 'all',
        activity: q.get('activity') || 'all',
        online: q.get('online')==='true',
        beginner: q.get('beginner')==='true',
      });
    };
    window.addEventListener('popstate', onPop);
    return ()=>window.removeEventListener('popstate', onPop);
  }, []);
  const list = base.filter(m=>{
    if(!meetupVisibleTo(m, S.viewAsUser)) return false;
    if(filters.region!=='all' && m.code!==filters.region) return false;
    if(filters.date!=='all' && (m.dateBucket||'week')!==filters.date) return false;
    if(filters.activity!=='all' && !(m.cardTags&&m.cardTags[0]&&m.cardTags[0][0]===filters.activity)) return false;
    if(filters.online && !m.online) return false;
    if(filters.beginner && !(m.soloLine&&(m.soloLine.includes('첫 참가자')||m.soloLine.includes('방금 등록')))) return false;
    return true;
  });
  const anyFilterOn = filters.region!=='all'||filters.date!=='all'||filters.activity!=='all'||filters.online||filters.beginner;
  return <div className="wapp"><WNav S={S} go={go} set={set}/>
    <div className="wmain" style={{maxWidth:1080}}>
      <div className="wsect"><h2>모임 찾기</h2></div>
      {type==='official'&&setType('all')}
      <div className="row" style={{gap:8, marginBottom:14, flexWrap:'wrap'}}>
        <Chip label="전체" onClick={()=>setType('all')} on={type==='all'}/>
        <Chip label="회원 모임" onClick={()=>setType('member')} on={type==='member'}/>
      </div>
      {/* 공식 이벤트는 이벤트 탭으로 통일(2026-07-30 사용자 확정) — 여기선 안내 링크만 */}
      <button className="card row" style={{width:'100%', gap:10, marginBottom:14, cursor:'pointer', fontFamily:'inherit', textAlign:'left', padding:'12px 16px'}} onClick={()=>go('events')}>
        <Ic n="IconNormalMegaphoneNameMegaphoneFillFalse" s={16} c="var(--c-primary)"/>
        <span style={{flex:1, fontSize:13.5}}><b>운영진이 여는 공식 이벤트</b>는 이벤트 탭에서 확인하세요</span>
        <Ic n="IconNormalChevronRightNameChevronRightTightFalseThick" s={13} c="var(--c-faint)"/>
      </button>
      <div className="row" style={{gap:8, marginBottom:12, flexWrap:'wrap'}}>
        <Chip icon="IconNormalCalendarNameCalendar" label="오늘" onClick={()=>toggle('date','today')} on={filters.date==='today'}/>
        <Chip label="이번 주" onClick={()=>toggle('date','week')} on={filters.date==='week'}/>
        <Chip icon="IconNormalVideoNameVideo" label="온라인" onClick={()=>patch({online:!filters.online})} on={filters.online}/>
        <Chip label="첫 참가자 환영" onClick={()=>patch({beginner:!filters.beginner})} on={filters.beginner}/>
        {anyFilterOn&&<Chip label="필터 초기화" onClick={()=>setFilters({region:'all',date:'all',activity:'all',online:false,beginner:false})}/>}
      </div>
      <div className="card" style={{padding:'14px 16px', marginBottom:20, display:'flex', flexDirection:'column', gap:10}}>
        <div>
          <div className="meta" style={{marginBottom:8}}>지역</div>
          <div className="row" style={{gap:6, flexWrap:'wrap'}}>
            <Chip label="전체" onClick={()=>patch({region:'all'})} on={filters.region==='all'}/>
            {Object.entries(CITY).map(([code,city])=><Chip key={code} label={city+(code===curCode?' · 현재 위치':'')} onClick={()=>patch({region:code})} on={filters.region===code}/>)}
          </div>
        </div>
        <div>
          <div className="meta" style={{marginBottom:8}}>활동</div>
          <div className="row" style={{gap:6, flexWrap:'wrap'}}>
            <Chip label="전체" onClick={()=>patch({activity:'all'})} on={filters.activity==='all'}/>
            {ACTIVITIES.map(a=><Chip key={a} label={a} onClick={()=>toggle('activity',a)} on={filters.activity===a}/>)}
          </div>
        </div>
      </div>
      <p style={{margin:'0 0 18px',fontSize:14,color:'var(--c-sub)'}}>{anyFilterOn?`조건에 맞는 모임 ${list.length}개예요.`:'국가 상관없이 최근 활발한 순으로 보여드려요. 모두모여 공식 모임은 운영진이 직접 진행해요.'}</p>
      {list.length===0?
        <div className="card" style={{textAlign:'center', padding:'32px 20px'}}>
          <Ic n="IconNormalTelescopeNameTelescope" s={28} c="var(--c-faint)"/>
          {anyFilterOn?<>
            <p style={{margin:'12px 0 4px', fontSize:14.5, fontWeight:700}}>현재 조건에 맞는 모임이 없어요.</p>
            <div className="row" style={{gap:8, justifyContent:'center', marginTop:12}}><DSBtn label="필터 초기화" variant="outlined" size="md" onClick={()=>setFilters({region:'all',date:'all',activity:'all',online:false,beginner:false})}/><DSBtn label="다른 활동 둘러보기" size="md" onClick={()=>go('explore')}/></div>
          </>:type==='official'?<>
            <p style={{margin:'12px 0 4px', fontSize:14.5, fontWeight:700}}>아직 예정된 공식 이벤트가 없어요.</p>
            <p className="meta" style={{margin:0}}>모두모여 운영팀이 직접 기획하거나 확인한 이벤트가 등록되면 여기에서 확인할 수 있어요.</p>
          </>:<>
            <p style={{margin:'12px 0 4px', fontSize:14.5, fontWeight:700}}>아직 등록된 모임이 없어요.</p>
            <p className="meta" style={{margin:'0 0 12px'}}>새로운 사람들과 하고 싶은 활동이 있다면 첫 모임을 만들어보세요.</p>
            <div className="row" style={{gap:8, justifyContent:'center'}}><DSBtn label="첫 모임 만들기" size="md" onClick={()=>startCreateMeetup(S,set,go)}/><DSBtn label="다른 지역 보기" variant="outlined" size="md" onClick={()=>patch({region:'all'})}/></div>
          </>}
        </div>:
        <div className="wgrid">{list.map(m=><MeetCard key={(m.isEvent?'ev-':'')+m.id} m={m} onClick={()=>m.isEvent?go('eventdetail',{eventId:m.id}):(MEETUPS[m.id]?go('detail',{detailId:m.id}):go('detail',{detailId:m.id, tempDetail:synthDetailFromCard(m)}))}/>)}</div>}
    </div>
    <Footer go={go} set={set}/>
  </div>;
}
function WScrInterestRegion({S,set,go}){
  const meta=countryOf(S.regionVals.interest);
  const code=meta.code, city=meta.city;
  const DATA={
    GB:[{id:'c-gb1',title:'런던 한인 브런치 모임',when:'토 오전 11시',size:'4~6명',cardTags:[['브런치','primary'],['조용한 대화','']],soloLine:'첫 참가자 환영 · 대기 1'},{id:'x2',title:'한인 유학생 펍모임',when:'금 오후 8시',size:'4~6명',cardTags:[['GB 런던','line']],soloLine:'혼자 참여 환영'},{id:'c-gb2',title:'런던 스터디 카페 모임',when:'화 오후 7시',size:'4~6명',cardTags:[['스터디','primary']],soloLine:'방금 등록됨'},{id:'c-gb1p',title:'런던 유학생 펍모임',when:'지난 목 오후 8시',size:'4~6명',cardTags:[['펍모임','primary']],soloLine:'22명 참여 완료',expired:true}],
    US:[{id:'c-us1',title:'뉴욕 커리어 커피챗',when:'수 오후 6시',size:'4~6명',cardTags:[['커리어','primary'],['조용한 대화','']],soloLine:'첫 참가자 환영 · 대기 1'},{id:'x6',title:'뉴욕 유학생 네트워킹 브런치',when:'일 오전 11시',size:'4~6명',cardTags:[['US 뉴욕','line']],soloLine:'혼자 참여 환영'},{id:'c-us2',title:'뉴욕 러닝 크루',when:'토 오전 9시',size:'4~6명',cardTags:[['운동','primary']],soloLine:'방금 등록됨'},{id:'c-us1p',title:'뉴욕 유학생 네트워킹',when:'지난 일 오전 11시',size:'4~6명',cardTags:[['네트워킹','primary']],soloLine:'31명 참여 완료',expired:true}],
    CA:[{id:'c-ca1',title:'토론토 유학생 네트워킹',when:'금 오후 7시',size:'4~6명',cardTags:[['네트워킹','primary'],['조용한 대화','']],soloLine:'첫 참가자 환영 · 대기 1'},{id:'x3',title:'워홀러 브런치 모임',when:'토 오전 11시',size:'4~6명',cardTags:[['CA 토론토','line']],soloLine:'첫 참가자 비율 70%'},{id:'c-ca2',title:'토론토 스터디 모임',when:'일 오후 2시',size:'4~6명',cardTags:[['스터디','primary']],soloLine:'방금 등록됨'},{id:'c-ca1p',title:'토론토 워홀러 브런치',when:'지난 토 오전 11시',size:'4~6명',cardTags:[['브런치','primary']],soloLine:'19명 참여 완료',expired:true}],
    BE:[{id:'c-be1',title:'브뤼셀 유학생 브런치',when:'토 오전 11시',size:'4~6명',cardTags:[['브런치','primary'],['조용한 대화','']],soloLine:'첫 참가자 환영 · 대기 1'},{id:'x7',title:'브뤼셀 유학생 모임',when:'금 오후 7시',size:'4~6명',cardTags:[['BE 브뤼셀','line']],soloLine:'혼자 참여 환영'},{id:'c-be2',title:'브뤼셀 스터디 모임',when:'화 오후 7시',size:'4~6명',cardTags:[['스터디','primary']],soloLine:'방금 등록됨'},{id:'c-be1p',title:'브뤼셀 유학생 모임',when:'지난 금 오후 7시',size:'4~6명',cardTags:[['모임','primary']],soloLine:'15명 참여 완료',expired:true}],
    KR:[{id:'x5',title:'서울 귀국생 네트워킹',when:'토 오후 3시',size:'10명 내외',cardTags:[['KR 서울','line']],soloLine:'혼자 참여 환영'}],
  };
  const list=DATA[code]||genCityMeetups(city);
  return <div className="wapp"><WNav S={S} go={go} set={set}/>
    <div className="wmain" style={{maxWidth:900}}>
      <div className="wsect"><h2>{city} 모임</h2></div>
      <p style={{margin:'0 0 18px',fontSize:14,color:'var(--c-sub)'}}>관심 지역으로 설정한 {city}의 모임이에요.</p>
      <div className="wgrid">{list.map(m=><MeetCard key={m.id} m={m} onClick={()=>MEETUPS[m.id]?go('detail',{detailId:m.id}):go('detail',{detailId:m.id, tempDetail:synthDetailFromCard(m)})}/>)}</div>
    </div>
  </div>;
}
function WScrEmpty({S,set,go}){
  const n=S.interest+(S.interested?1:0);
  return <div className="wapp"><WNav S={S} go={go} set={set}/>
    <div className="wmain" style={{maxWidth:640}}>
      <button className="wback" onClick={()=>go('home')}><Ic n="IconNormalChevronLeftNameChevronLeftTightFalseThick" s={14}/>홈으로</button>
      <div style={{textAlign:'center',padding:'20px 0 24px'}}>
        <Ph h={140} r={18} label="빈 지역" icon="IconNormalTelescopeNameTelescope" style={{margin:'0 auto',maxWidth:280}}/>
        <h2 style={{fontSize:20,margin:'20px 0 8px'}}>아직 맨체스터 모임이 없어요</h2>
        <p style={{margin:0,fontSize:14,color:'var(--c-sub)'}}>대신, 열리면 가장 먼저 알려드릴게요.</p>
      </div>
      <div className="card" style={{marginBottom:14}}>
        <b style={{fontSize:15}}>맨체스터에서 모임이 열리면 알려주세요</b>
        <div className="row" style={{gap:8,margin:'10px 0 14px'}}><Ic n="IconNormalBellPlusNameBellPlus" s={18} c="var(--c-primary)"/><span style={{fontSize:14}}>지금까지 <b style={{color:'var(--c-primary)'}}>{n}명</b>이 기다리고 있어요</span></div>
        <DSBtn label={S.interested?'관심 등록 완료':'관심 등록하기'} variant={S.interested?'outlined':'solid'} color={S.interested?'assistive':'primary'} size="md" onClick={()=>guardAuth(S,set,go,()=>set({interested:!S.interested,toast:S.interested?null:'등록 완료 — 모임이 열리면 알려드려요'}))}/>
      </div>
      <div className="card" style={{marginBottom:14}}>
        <b style={{fontSize:15}}>5명 이상 관심 등록 시 운영진이 검토해요</b>
        <div className="meta" style={{margin:'10px 0 8px'}}>어떤 모임을 원하세요? (투표)</div>
        {[['펍모임 · 현지생활 적응',3],['브런치 · 친구 만들기',2],['취준 스터디 · 커리어',1]].map(v=>{const c=v[1]+(S.votes[v[0]]?1:0);
          return <button key={v[0]} className="row" style={{width:'100%',gap:8,padding:'10px 12px',marginBottom:8,border:'1px solid '+(S.votes[v[0]]?'var(--c-primary)':'var(--c-line)'),background:S.votes[v[0]]?'var(--c-primary-bg)':'var(--c-bg)',borderRadius:10,cursor:'pointer',fontFamily:'inherit',fontSize:14}} onClick={()=>guardAuth(S,set,go,()=>set({votes:{...S.votes,[v[0]]:!S.votes[v[0]]}}))}><span style={{flex:1,textAlign:'left'}}>{v[0]}</span><b style={{color:'var(--c-primary)'}}>{c}/5</b></button>;})}
      </div>
      <button className="card" style={{width:'100%',display:'flex',gap:12,alignItems:'center',cursor:'pointer',fontFamily:'inherit',textAlign:'left',marginBottom:14}} onClick={()=>startCreateMeetup(S,set,go,{toast:'모모가 첫 모임 개설을 도와드릴게요!'})}>
        <Ic n="IconNormalSparkleNameSparkle" s={22} c="var(--c-violet)"/><span style={{flex:1,fontSize:14}}><b>제가 첫 호스트가 될게요</b><br/><span className="meta">템플릿으로 쉽게 시작해요 · 첫 호스트 뱃지 제공</span></span>
      </button>
      <button className="card" style={{width:'100%',display:'flex',gap:12,alignItems:'center',cursor:'pointer',fontFamily:'inherit',textAlign:'left'}} onClick={()=>go('home',{region:'gb',toast:'가까운 도시 — 런던 모임을 보여드려요'})}>
        <Ic n="IconNormalCompassNameCompassFillFalse" s={22} c="var(--c-primary)"/><span style={{flex:1,fontSize:14}}><b>가까운 런던 모임 둘러보기</b><br/><span className="meta">기차 2시간 · 이번 주 3개 모임</span></span>
      </button>
    </div>
  </div>;
}
function WScrPrivacyAvoid({S, set, go}) {
  return <div className="wapp"><WNav S={S} go={go} set={set}/>
    <div className="wmain" style={{maxWidth:640}}>
      <button className="wback" onClick={()=>go('profile')}><Ic n="IconNormalChevronLeftNameChevronLeftTightFalseThick" s={14}/>내 정보로</button>
      <div className="wsect"><h2>지인 마주침 방지</h2></div>
      {DEMO_MODE?<PrivacyAvoidBody S={S} set={set}/>:<div className="card" style={{textAlign:'center', padding:'32px 20px'}}>
        <Ic n="IconNormalClockNameClockFillFalse" s={32} c="var(--c-faint)"/>
        <p style={{fontSize:14.5, fontWeight:700, margin:'14px 0 6px'}}>준비 중이에요</p>
        <p className="meta" style={{margin:0, lineHeight:1.6}}>아는 사람과 같은 모임에서 마주치지 않도록 설정하는 기능을 만들고 있어요.</p>
      </div>}
    </div>
  </div>;
}
// 이메일 미인증 상태일 때 보여주는 안내 — 실제 Supabase 인증 메일 재발송으로 연결(가짜 인증코드 입력칸 없음)
function EmailVerifyResendRow({S, set}){
  const {useState} = React;
  const [sent, setSent] = useState(false);
  const resend = () => {
    authResendConfirmation(S.acctEmail).then(res=>{
      if (res.skipped) { set({toast:'서버 연동이 안 돼 있어서 지금은 보낼 수 없어요'}); return; }
      if (res.error) { set({toast:res.error}); return; }
      setSent(true);
      set({toast:'인증 메일을 다시 보냈어요 — 메일함(스팸함 포함)을 확인해주세요'});
    });
  };
  return <div className="row" style={{gap:8, flexWrap:'wrap', alignItems:'center'}}>
    <Chip icon="IconNormalCircleInfo2NameCircleInfoFillFalse" label="아직 인증 전이에요" tone="orange"/>
    <button style={{background:'none', border:'1px solid var(--c-line)', borderRadius:8, padding:'7px 14px', fontFamily:'inherit', fontSize:13, cursor:'pointer'}} onClick={resend} disabled={sent}>{sent?'다시 보냈어요':'인증 메일 다시 보내기'}</button>
  </div>;
}
// 내가 쓴 Q&A 글 — posts 테이블에서 내 이메일로 작성한 것만 걸러서 보여줌(10단계: Q&A 서버화 이후 실데이터)
function MyQnaPostsList({S, set, go}){
  const {useState, useEffect} = React;
  const [posts, setPosts] = useState(null);
  const load = () => fetchPosts('qna').then(all=>setPosts(all.filter(p=>p.author_email===S.acctEmail)));
  useEffect(()=>{ load(); }, []);
  if (posts===null) return <p className="meta">불러오는 중...</p>;
  if (posts.length===0) return <p className="meta">아직 쓴 글이 없어요.</p>;
  return <>{posts.map(p=>{ const sp=qnaSplit(p.body); return <div key={p.id} className="card" style={{width:'100%', marginBottom:10, position:'relative'}}>
    <button style={{width:'100%', cursor:'pointer', fontFamily:'inherit', textAlign:'left', background:'none', border:'none', padding:0}} onClick={()=>set({qnaDetailPost:{...p, ...sp, postSource:'qna'}, screen:'postdetail'})}>
      <Chip label={p.topic} tone="primary"/>
      <div style={{fontSize:14, fontWeight:700, margin:'8px 0 4px'}}>{sp.title}</div>
      <div className="meta">{timeAgo(new Date(p.created_at).getTime())}</div>
    </button>
    <button style={{position:'absolute', top:10, right:10, background:'none', border:'none', color:'var(--c-red)', fontSize:12, cursor:'pointer'}} onClick={()=>deletePost(p.id).then(load)}>삭제</button>
  </div>; })}</>;
}
function WScrProfile({S,set,go}){
  const [profErr, setProfErr] = React.useState('');
  const [profSaving, setProfSaving] = React.useState(false);
  const [profSavedAt, setProfSavedAt] = React.useState(0);
  const tab = S.profileTab||0;
  if (S.authStatus !== 'authenticated') {
    return <div className="wapp"><WNav S={S} go={go} set={set}/>
      <div className="wmain" style={{maxWidth:480}}>
        <div className="wsect"><h2>내 정보</h2></div>
        <div className="card" style={{textAlign:'center', padding:'40px 20px'}}>
          <p style={{fontSize:15, fontWeight:700, margin:'0 0 8px'}}>로그인이 필요해요</p>
          <p className="meta" style={{margin:'0 0 20px'}}>회원가입하면 내 프로필, 참여 기록, 계정 설정을 볼 수 있어요.</p>
          <div style={{display:'flex', flexDirection:'column', gap:8}}>
            <DSBtn label="회원가입" size="md" onClick={()=>go('signup')}/>
            <DSBtn label="이미 계정이 있어요 · 로그인" variant="outlined" size="md" onClick={()=>go('signup')}/>
          </div>
        </div>
      </div>
    </div>;
  }
  return <div className="wapp"><WNav S={S} go={go} set={set}/>
    <div className="wmain" style={{maxWidth:680}}>
      <div className="wsect"><h2>내 정보</h2></div>
      <div className="wtabs">{['프로필','내 활동','회원정보'].map((t,i)=><button key={t} className={'qtab'+(tab===i?' on':'')} onClick={()=>set({profileTab:i})}>{t}</button>)}</div>
      {tab===0&&<>
      <div className="profile-stats">
        <div className="card" style={{textAlign:'center', padding:'16px 8px'}}><b style={{fontSize:22, display:'block'}}>{S.myHostedMeetups.length}</b><span className="meta">주최한 모임</span></div>
        <div className="card" style={{textAlign:'center', padding:'16px 8px'}}><b style={{fontSize:22, display:'block'}}>{S.completed.length}</b><span className="meta">참석한 모임</span></div>
        <div className="card" style={{textAlign:'center', padding:'16px 8px'}}><b style={{fontSize:22, display:'block', color:'var(--c-green)'}}>0</b><span className="meta">노쇼 이력</span></div>
        <div className="card" style={{textAlign:'center', padding:'16px 8px'}}><b style={{fontSize:22, display:'block'}}>{[S.locGranted,S.emailVerified].filter(Boolean).length+S.schoolBadges.length}</b><span className="meta">인증 배지</span></div>
      </div>
      <div className="wtwo profile-photo-form">
        <div style={{display:'flex', flexDirection:'column', alignItems:'center', gap:10}}>
          <PhotoUpload value={S.profile.photoUrl} onChange={url=>set({profile:{...S.profile, photoUrl:url}})} folder="profile" shape="circle" size={160} placeholder="사진 올리기"/>
        </div>
        <div className="card" style={{display:'flex', flexDirection:'column', gap:12}}>
          <div>
            <div className="meta" style={{marginBottom:6}}>닉네임
          </div>
            <input value={S.profile.nickname} onChange={e=>set({profile:{...S.profile, nickname:e.target.value}})} style={{width:'100%', border:'1px solid var(--c-line)', borderRadius:8, padding:'10px 14px', fontFamily:'inherit', fontSize:14, outline:'none'}}/>
            {S.profileNickConflict&&<div role="alert" className="notice orange" style={{marginTop:8}}><Ic n="IconNormalCircleInfo2NameCircleInfoFillFalse" s={15}/><span><b>이 닉네임은 이미 다른 회원이 사용 중이에요.</b> 다른 닉네임으로 바꾸고 아래 [프로필 저장]을 눌러야 확정돼요.</span></div>}
          </div>
          <div>
            <div className="meta" style={{marginBottom:6}}>학교 (선택)</div>
            <SchoolSelect value={S.profile.school} onChange={v=>set({profile:{...S.profile, school:v}})}/>
          </div>
          <div>
            <div className="meta" style={{marginBottom:6}}>한줄 소개</div>
            <textarea value={S.profile.bio} onChange={e=>set({profile:{...S.profile, bio:e.target.value}})} rows={2} style={{width:'100%', border:'1px solid var(--c-line)', borderRadius:8, padding:'10px 14px', fontFamily:'inherit', fontSize:14, outline:'none', resize:'none'}}/>
          </div>
          <div>
            <div className="meta" style={{marginBottom:8}}>관심사 (최대 3개)</div>
            <div style={{display:'flex', gap:8, flexWrap:'wrap'}}>{INTERESTS_POOL.map(t=>{const arr=S.profile.interests||[]; const has=arr.includes(t); return <Chip key={t} label={t} onClick={()=>set({profile:{...S.profile, interests: has?arr.filter(x=>x!==t):(arr.length<3?[...arr,t]:arr)}})} on={has}/>;})}</div>
            <button style={{background:'none', border:'none', color:'var(--c-primary)', fontFamily:'inherit', fontSize:12.5, fontWeight:600, cursor:'pointer', padding:'8px 0 0'}} onClick={()=>set({screen:'home', forceActivityPicker:true})}>관심 활동 다시 선택하기</button>
          </div>
          <label className="row" style={{gap:8, cursor:'pointer', fontSize:13.5, marginTop:2}}>
            <input type="checkbox" checked={!!S.profile.photoPublic} onChange={e=>set({profile:{...S.profile, photoPublic:e.target.checked}})} style={{width:17, height:17, accentColor:'var(--c-primary)'}}/>
            모임에 내 사진 공개
          </label>
          <p className="meta" style={{margin:'-4px 0 0', lineHeight:1.5}}>끄면 내가 연 모임에서 사진 대신 첫 글자 아바타로 표시돼요. 언제든 바꿀 수 있어요.</p>
          <label className="row" style={{gap:8, cursor:'pointer', fontSize:13.5, marginTop:2}}>
            <input type="checkbox" checked={!!(S.consents&&S.consents.marketing)} onChange={e=>{ const on=e.target.checked; set({consents:{...S.consents, marketing:on}}); saveMarketingConsent(S.acctEmail||S.signupEmail, on).then(r=>{ set({toast: r&&r.error ? '수신 동의 저장에 실패했어요 — 잠시 후 다시 시도해주세요' : (on?'이벤트·혜택 이메일 수신에 동의했어요':'이벤트·혜택 이메일 수신 동의를 철회했어요')}); }); }} style={{width:17, height:17, accentColor:'var(--c-primary)'}}/>
            이벤트·혜택 이메일 수신 동의
          </label>
          <p className="meta" style={{margin:'-4px 0 0', lineHeight:1.5}}>가입 때 선택한 값이에요. 체크를 바꾸면 바로 저장되고, 언제든 철회할 수 있어요.</p>
          {profErr&&<div role="alert" className="notice orange" style={{margin:0}}><Ic n="IconNormalCircleInfo2NameCircleInfoFillFalse" s={15}/><span>{profErr}</span></div>}
          {profSavedAt>0&&!profErr&&<div className="notice blue" style={{margin:0}}><Ic n="IconNormalCircleCheck2NameCircleCheckFillFillTrue" s={15}/><span>프로필이 저장됐어요 — 다른 회원에게도 이 정보로 보여요.</span></div>}
          <DSBtn label={profSaving?'저장 중…':'프로필 저장'} size="md" disable={profSaving} onClick={()=>{
            setProfErr(''); setProfSaving(true);
            saveMyProfile(S.acctEmail||S.signupEmail, S.profile).then(res=>{
              setProfSaving(false);
              if (res && res.dup) { setProfErr('이미 사용 중인 닉네임이에요 — 다른 닉네임을 골라주세요.'); return; }
              if (res && res.reserved) { setProfErr('운영진 사칭이 의심되는 닉네임은 사용할 수 없어요 — 다른 닉네임을 골라주세요.'); return; }
              if (res && res.error) {
                const raw = String(res.error.message||'');
                setProfErr(/row-level security|violates|permission/i.test(raw) ? '저장 권한을 확인하지 못했어요 — 로그아웃 후 다시 로그인해서 저장해주세요.' : (raw||'저장에 실패했어요 — 잠시 후 다시 시도해주세요.'));
                return;
              }
              if (res && res.skipped) { setProfErr('서버 연동이 안 돼 있어 저장할 수 없어요.'); return; }
              setProfSavedAt(Date.now()); set({profileNickConflict:false});
            });
          }}/>
        </div>
      </div>
      <div className="row" style={{gap:6, flexWrap:'wrap', marginTop:16}}><TrustBadges items={[...(S.locGranted?[`${S.regionVals.cur} 거주 인증`]:[]), ...(S.emailVerified?['이메일 인증']:[]), ...S.schoolBadges]}/><MeetupBadge count={S.completed.length}/></div>
      <div style={{marginTop:12}}><TrustSignals S={S} size="lg"/></div>
      <p className="meta" style={{margin:'10px 0 20px', lineHeight:1.6}}>인증은 서열이 아니라 사실 확인이에요 — 완료한 것만 표시되고, 뱃지는 신원이나 안전을 보증하지 않아요.</p>
      <div className="profile-menu-grid">
        <button className="card profile-menu-item" onClick={()=>go('region')}>
          <Ic n="IconNormalLocation2NameLocationFillFalse" s={20} c="var(--c-primary)"/>
          <span className="profile-menu-text"><b style={{fontSize:14.5}}>지역 설정</b><br/><span className="meta">거주·귀국·관심 지역 관리</span></span>
          <Ic n="IconNormalChevronRightNameChevronRightTightFalseThick" s={14} c="var(--c-faint)"/>
        </button>
        <button className="card profile-menu-item" onClick={()=>go('schoolemail')}>
          <Ic n="IconNormalVerifiedCheck2NameVerifiedCheckFillFalse" s={20} c="var(--c-primary)"/>
          <span className="profile-menu-text"><b style={{fontSize:14.5}}>학교 인증</b><br/><span className="meta">선택 사항 — 이메일 또는 학생증</span></span>
          <Ic n="IconNormalChevronRightNameChevronRightTightFalseThick" s={14} c="var(--c-faint)"/>
        </button>
        <button className="card profile-menu-item" onClick={()=>go('privacyavoid')}>
          <Ic n="IconNormalEyeSlashNameEyeSlashFillFalse" s={20} c="var(--c-primary)"/>
          <span className="profile-menu-text"><b style={{fontSize:14.5}}>지인 마주침 방지</b><br/><span className="meta">아는 사람과 마주칠 가능성 줄이기</span></span>
          <Ic n="IconNormalChevronRightNameChevronRightTightFalseThick" s={14} c="var(--c-faint)"/>
        </button>
        <button className="card profile-menu-item" onClick={()=>startCreateMeetup(S,set,go)}>
          <Ic n="IconNormalCalendarNameCalendar" s={20} c="var(--c-primary)"/>
          <span className="profile-menu-text"><b style={{fontSize:14.5}}>모임 개설하기</b><br/><span className="meta">내가 호스트가 되어 모임을 열어요</span></span>
          <Ic n="IconNormalChevronRightNameChevronRightTightFalseThick" s={14} c="var(--c-faint)"/>
        </button>
      </div>
      <div className="profile-save-wrap"><DSBtn label="저장하기" onClick={()=>set({toast:'프로필이 저장됐어요'})}/></div>
      </>}
      {tab===1&&<div className="wtwo">
        <div>
          <b style={{fontSize:15}}>내가 쓴 글</b>
          <div style={{margin:'12px 0 24px'}}>
            <MyQnaPostsList S={S} set={set} go={go}/>
          </div>
          <b style={{fontSize:15}}>내가 주최한 모임 <span className="meta" style={{fontWeight:400}}>({S.myHostedMeetups.length}개)</span></b>
          <div style={{margin:'12px 0'}}>
            {S.myHostedMeetups.length===0?<p className="meta">아직 개설한 모임이 없어요.</p>:S.myHostedMeetups.map((m,i)=><button key={i} className="card" style={{width:'100%', marginBottom:10, cursor:'pointer', fontFamily:'inherit', textAlign:'left'}} onClick={()=>go('mymeethosted')}>
              <div style={{fontSize:14, fontWeight:700, marginBottom:4}}>{m.title||'(제목 없음)'}</div>
              <div className="meta">{m.when||'일정 미정'} · {m.place||'장소 미정'} · <Chip label="모임개설예정" tone="violet"/></div>
            </button>)}
          </div>
        </div>
        <div>
          <b style={{fontSize:15}}>내가 쓴 댓글</b>
          <div style={{margin:'12px 0'}}>
            <p className="meta">댓글 모아보기는 준비 중이에요.</p>
          </div>
        </div>
      </div>}
      {tab===2&&<div style={{maxWidth:480}}>
        <div className="card" style={{marginBottom:14}}>
          <div className="row" style={{justifyContent:'space-between', marginBottom:S.acctEdit==='email'?10:0}}>
            <div><div className="meta">이메일</div><b style={{fontSize:15}}>{S.confirmedAuthMethod==='kakao'?'카카오 계정으로 로그인 중':S.acctEmail}</b></div>
            {S.confirmedAuthMethod!=='kakao'&&S.acctEdit!=='email'&&<button style={{background:'none', border:'1px solid var(--c-line)', borderRadius:8, padding:'7px 14px', fontFamily:'inherit', fontSize:13, cursor:'pointer'}} onClick={()=>set({acctEdit:'email'})}>변경</button>}
          </div>
          {S.confirmedAuthMethod!=='kakao'&&S.acctEdit==='email'&&<div style={{display:'flex', gap:8}}>
            <input type="email" value={S.acctEmail} onChange={e=>set({acctEmail:e.target.value})} style={{flex:1, border:'1px solid var(--c-line)', borderRadius:8, padding:'10px 14px', fontFamily:'inherit', fontSize:14, outline:'none'}}/>
            <button style={{background:'var(--c-primary)', color:'#fff', border:'none', borderRadius:8, padding:'0 16px', fontFamily:'inherit', fontSize:13, fontWeight:700, cursor:'pointer'}} onClick={()=>{ authChangeEmail(S.acctEmail).then(res=>{ if(res.skipped){ set({toast:'서버 연동이 안 돼 있어서 지금은 변경할 수 없어요'}); return; } if(res.error){ set({toast:res.error}); return; } set({acctEdit:null, emailVerified:false, phoneVerified:false, phoneCodeSent:false, phoneCode:'', toast:'변경 확인 메일을 새 이메일로 보냈어요 — 링크를 눌러야 최종 반영돼요'}); }); }}>저장</button>
          </div>}
        </div>
        <div className="card" style={{marginBottom:14}}>
          <div className="row" style={{justifyContent:'space-between', marginBottom:S.acctEdit==='pw'?12:0}}>
            <div className="meta">비밀번호</div>
            {S.confirmedAuthMethod!=='kakao'&&S.acctEdit!=='pw'&&<button style={{background:'none', border:'1px solid var(--c-line)', borderRadius:8, padding:'7px 14px', fontFamily:'inherit', fontSize:13, cursor:'pointer'}} onClick={()=>set({acctEdit:'pw'})}>변경</button>}
          </div>
          {S.confirmedAuthMethod==='kakao'?<p className="meta" style={{margin:0, lineHeight:1.6}}>카카오 계정으로 로그인해요 · 비밀번호는 카카오에서 관리돼요</p>:S.acctEdit==='pw'&&<div style={{display:'flex', flexDirection:'column', gap:10}}>
            <PasswordField value={S.pwCurrent} onChange={v=>set({pwCurrent:v})} placeholder="현재 비밀번호"/>
            <PasswordField value={S.pwNew} onChange={v=>set({pwNew:v})} placeholder="새 비밀번호 (8자 이상)" showRequirements/>
            <PasswordField value={S.pwConfirm} onChange={v=>set({pwConfirm:v})} placeholder="새 비밀번호 확인"/>
            <DSBtn label="비밀번호 변경" size="md" disable={S.pwNew.length<8||S.pwNew!==S.pwConfirm||!S.pwCurrent} onClick={()=>{ authChangePassword(S.pwNew).then(res=>{ if(res.skipped){ set({toast:'서버 연동이 안 돼 있어서 지금은 변경할 수 없어요'}); return; } if(res.error){ set({toast:res.error}); return; } set({acctEdit:null, pwCurrent:'', pwNew:'', pwConfirm:'', toast:'비밀번호가 변경됐어요'}); }); }}/>
          </div>}
        </div>
        <div className="card" style={{marginBottom:14}}>
          <div className="meta" style={{marginBottom:8}}>이메일 인증</div>
          {S.emailVerified?<Chip icon="IconNormalVerifiedCheck2NameVerifiedCheckFillFillTrue" label={S.acctEmail+' · 인증 완료'} tone="green"/>:<EmailVerifyResendRow S={S} set={set}/>}
        </div>
        <div className="card" style={{marginBottom:12, padding:0}}>
          <div style={{width:'100%', display:'flex', justifyContent:'space-between', alignItems:'center', padding:'14px 18px', fontSize:14}}>
            <span>로그인 방식</span>
            <span className="meta">{S.confirmedAuthMethod==='kakao'?'카카오 연결됨':'이메일'}</span>
          </div>
          <div style={{width:'100%', display:'flex', justifyContent:'space-between', alignItems:'center', borderTop:'1px solid var(--c-line)', padding:'14px 18px', fontSize:14, color:'var(--c-faint)'}}>
            <span>차단한 사용자 관리</span>
            <span className="meta">준비 중</span>
          </div>
          <button style={{width:'100%', display:'flex', justifyContent:'space-between', alignItems:'center', background:'none', border:'none', borderTop:'1px solid var(--c-line)', padding:'14px 18px', fontFamily:'inherit', fontSize:14, cursor:'pointer', textAlign:'left'}} onClick={()=>set({myReportsOpen:!S.myReportsOpen})}>
            <span>신고 내역</span>
            <Ic n="IconNormalChevronRightNameChevronRightTightFalseThick" s={13} c="var(--c-faint)"/>
          </button>
          {S.myReportsOpen&&<MyReportsList S={S}/>}
          <button style={{width:'100%', display:'flex', justifyContent:'space-between', alignItems:'center', background:'none', border:'none', borderTop:'1px solid var(--c-line)', padding:'14px 18px', fontFamily:'inherit', fontSize:14, cursor:'pointer', textAlign:'left'}} onClick={()=>set({legalOpen:'terms'})}>
            <span>이용약관 보기</span>
            <Ic n="IconNormalChevronRightNameChevronRightTightFalseThick" s={13} c="var(--c-faint)"/>
          </button>
        </div>
        <button style={{width:'100%', background:'none', border:'1px solid var(--c-line)', borderRadius:10, padding:'13px', fontFamily:'inherit', fontSize:14, fontWeight:600, cursor:'pointer', marginBottom:10}} onClick={async()=>{ try { await authSignOut(); } catch(e) {} set(logoutAccount(BASE)); }}>로그아웃</button>
        <button style={{width:'100%', background:'none', border:'1px solid var(--c-line)', borderRadius:10, padding:'13px', fontFamily:'inherit', fontSize:14, fontWeight:600, cursor:'pointer', color:'var(--c-red)'}} onClick={()=>set({withdrawConfirmOpen:true})}>회원 탈퇴</button>
      </div>}
    </div>
  </div>;
}
function MyReportsList({S}) {
  const {useState, useEffect} = React;
  const [reports, setReports] = useState(null);
  useEffect(()=>{ fetchMyReports(S.acctEmail).then(setReports); }, []);
  if (reports===null) return <p className="meta" style={{padding:'0 18px 14px'}}>불러오는 중...</p>;
  if (reports.length===0) return <p className="meta" style={{padding:'0 18px 14px'}}>제출한 신고 내역이 없어요.</p>;
  return <div style={{padding:'0 18px 14px'}}>{reports.map(r=><div key={r.id} style={{borderTop:'1px solid var(--c-line)', padding:'12px 0'}}>
    <div className="row" style={{justifyContent:'space-between', marginBottom:4}}><b style={{fontSize:13.5}}>{r.report_type} · {r.target||'(대상 미기재)'}</b><Chip label={r.status} tone={r.status==='처리완료'?'green':'orange'}/></div>
    <div className="meta" style={{fontSize:12}}>{r.reason}</div>
  </div>)}</div>;
}
function WScrCreateMeetup({S,set,go}){
  const {useState} = React;
  const [step, setStep] = useState(0);
  const d=S.newMeetup;
  const upd = p => set({newMeetup:{...d, ...p}});
  const purposes=['러닝·운동','식사·맛집','카페','보드게임','전시·공연','여행','언어 교환','스터디','펍·바','네트워킹'];
  const moods=['조용한 대화','활발한 네트워킹','술 없는 모임'];
  const STEPS = ['기본 정보','날짜와 장소','정원·승인','참가비','안전·취소 정책','미리보기'];
  const canNext = [
    !!d.title.trim(),
    !!(d.date&&d.time&&(d.regionType==='abroad'?d.country:true)&&d.place),
    !!(d.cap||'').trim(),
    d.feeType!=='paid'||!!(d.cost||'').trim(),
    !!d.safetyAck,
    true,
  ];
  const next = () => setStep(s=>Math.min(STEPS.length-1, s+1));
  const back = () => setStep(s=>Math.max(0, s-1));
  const editing = S.editingMeetupIdx!=null;
  // 선택된 활동들을 화면에 표시할 라벨 배열로 변환 ('기타·직접 입력'은 실제 입력한 텍스트로 치환)
  const purposeLabels = (d.purposes||[]).map(p=>p==='기타·직접 입력'?(d.customActivity||'기타'):p).filter(Boolean);
  const resetDraft = {title:'', when:'', date:'', time:'', cap:'6', place:'', purposes:[], purpose:'', customActivity:'', mood:'', desc:''};
  const submit = () => {
    if (editing) {
      const idx = S.editingMeetupIdx;
      const prev = S.myHostedMeetups[idx] || {};
      // 참가자에게 알릴 변경 내용 요약 — 실제로 바뀐 항목만 문장으로 구성
      const changes = [];
      if ((prev.purposes||[]).join(',')!==purposeLabels.join(',')) changes.push(`활동이 ${(prev.purposes||[]).join('·')||'미정'} → ${purposeLabels.join('·')||'미정'}으로`);
      if ((prev.cap||'')!==(d.cap||'')) changes.push(`정원이 ${prev.cap||'?'}명 → ${d.cap||'?'}명으로`);
      if ((prev.when||'')!==(d.when||'')) changes.push(`일정이 ${prev.when||'미정'} → ${d.when||'미정'}으로`);
      if ((prev.place||'')!==(d.place||'')) changes.push(`장소가 ${prev.place||'미정'} → ${d.place||'미정'}으로`);
      const summary = changes.length ? changes.join(', ')+' 변경됐어요.' : '모임 정보가 수정됐어요.';
      const prevApplicants = prev.applicants||[];
      const confirmedCount = prevApplicants.filter(a=>a.status==='confirmed').length;
      const updatedApplicants = prevApplicants.map(a=>a.status==='confirmed'?{...a, needsAck:true, ackNote:summary}:a);
      const updatedEntry = {...prev, ...d, purpose:purposeLabels[0]||'', purposes:purposeLabels, applicants:updatedApplicants};
      const nextHosted = S.myHostedMeetups.map((hm,hi)=>hi===idx?updatedEntry:hm);
      const applyEditLocal = () => go('mymeethosted', {myHostedMeetups:nextHosted, editingMeetupIdx:null, newMeetup:resetDraft, toast: confirmedCount>0 ? `모임 정보를 수정했어요 — 확정 참가자 ${confirmedCount}명에게 재확인을 요청했어요` : '모임 정보를 수정했어요'});
      // 서버에 저장된 모임이면 서버 수정이 성공한 뒤에만 화면 반영(15단계) — 실패를 숨기지 않기 원칙
      if (prev.serverId && isServerMeetupId(prev.serverId)) {
        const capNum2 = parseInt(d.cap)||6;
        updateRemoteMeetup(prev.serverId, {
          title: d.title, purposes: purposeLabels, country: d.country||'대한민국', city: d.placeVisibility==='confirmed'?'':(d.place||''), place_visibility: d.placeVisibility||'public',
          lat: ((d.regionType||'kr')==='kr'&&d.placeLat)||null, lng: ((d.regionType||'kr')==='kr'&&d.placeLng)||null,
          place: d.placeVisibility==='confirmed'?'':(d.place||''), when_label: d.when||'', event_date: d.date||null, cap: capNum2, approval_mode: d.approvalMode||'instant', waitlist_ok: d.waitlistOk!==false,
          fee_text: ({free:'참가비 없음', split:'현장에서 각자 결제', dutch:'주최자 선결제 · 현장에서 N분의 1 정산', paid:'예상 비용 '+(d.cost||'')+'원'+((d.costIncludes||'').trim()?' ('+d.costIncludes.trim()+')':'')}[d.feeType||'free']), description: d.desc||'', solo_welcome: !!d.soloWelcome, photo_url: d.photoUrl||null,
        }, summary).then(res=>{
          if (res && res.error) {
            const gone = String(res.error.message||'').includes('MM_MEETUP_GONE');
            set({toast: gone ? '이 모임은 서버에서 이미 삭제된 상태라 수정할 수 없어요 — 목록에서 삭제 후 새로 개설해주세요.' : '수정이 서버에 반영되지 못했어요 — 다른 회원에게는 아직 옛 정보가 보여요. 잠시 후 다시 시도해주세요.'});
            return;
          }
          saveMeetupPlaceDetail(prev.serverId, d.placeVisibility==='confirmed'?(d.place||''):null).then(r=>{ if(r&&r.error) console.error('place detail 동기화 실패', r.error); });
          applyEditLocal();
        });
      } else applyEditLocal();
      return;
    }
    // 취소 페널티 중에는 모임 개설도 차단(사용자 정책 2026-07-30) — 서버 트리거(trg_meetups_create_guard)가 최종 강제
    const penaltyUntil = Number((()=>{ try { return localStorage.getItem(nsKey('mm_cancel_penalty:'+((S.acctEmail||S.signupEmail||'').toLowerCase()))); } catch(e) { return 0; } })()||0);
    if (Date.now() < penaltyUntil) {
      const min = Math.max(1, Math.ceil((penaltyUntil-Date.now())/60000));
      set({toast:`최근 참가를 취소해서 지금은 모임을 열 수 없어요 — 약 ${min}분 뒤에 가능해요 (취소 후 1시간 제한)`});
      return;
    }
    const idx=S.myHostedMeetups.length;
    const capNum = parseInt(d.cap)||6;
    // 모임장 승인제인 경우, 데모 신청자를 정원보다 넉넉히 시딩해 승인대기 충돌(정원 초과) 상황을 재현할 수 있게 함
    const applicants = (d.approvalMode==='approval' && DEMO_MODE)
      ? ATTENDEES.slice(0, Math.min(capNum+2, ATTENDEES.length)).map(n=>({name:n, status:'pending'}))
      : [];
    const newEntry = {...d, purpose:purposeLabels[0]||'', purposes:purposeLabels, applicants, createdAt: editing?(d.createdAt||Date.now()):Date.now()};
    const nextHosted = [...S.myHostedMeetups, newEntry];
    const toast = '모임이 개설됐어요 — 채팅방이 열렸어요';
    go('chat',{detailId:'my'+idx, toast, myHostedMeetups:nextHosted, newMeetup:resetDraft});
    // 실제 서버(Supabase)가 연동돼 있으면 다른 계정에서도 보이도록 서버에도 등록. 연동 안 됐으면 skipped:true만 반환되고 아무 일도 안 일어남.
    createRemoteMeetup({
      host_email: S.acctEmail||S.signupEmail||'guest', host_name: S.profile.nickname||'호스트',
      title: d.title, purposes: purposeLabels, region_type: d.regionType||'local', country: d.country||'대한민국', city: d.placeVisibility==='confirmed'?'':(d.place||''),
      place: d.placeVisibility==='confirmed'?'':(d.place||''), place_visibility: d.placeVisibility||'public', when_label: d.when||'', event_date: d.date||null, cap: capNum, approval_mode: d.approvalMode||'instant', waitlist_ok: d.waitlistOk!==false,
      lat: ((d.regionType||'kr')==='kr'&&d.placeLat)||null, lng: ((d.regionType||'kr')==='kr'&&d.placeLng)||null,
      fee_text: ({free:'참가비 없음', split:'현장에서 각자 결제', dutch:'주최자 선결제 · 현장에서 N분의 1 정산', paid:'예상 비용 '+(d.cost||'')+'원'+((d.costIncludes||'').trim()?' ('+d.costIncludes.trim()+')':'')}[d.feeType||'free']), description: d.desc||'', solo_welcome: !!d.soloWelcome, photo_url: d.photoUrl||null,
    }).then(res=>{
      // 저장 실패를 더 이상 숨기지 않는다 — 서버에 없으면 다른 사람에게 안 보이므로 반드시 알림
      if (res && res.error) {
        console.error('createRemoteMeetup 실패', res.error);
        const msg = String((res.error&&res.error.message)||'');
        if (msg.includes('MM_SUSPENDED')) set({toast:'이용이 제한된 계정이에요 — 제한 기간에는 모임을 열 수 없어요. 문의: support@modumoyeo.com'});
        else if (msg.includes('MM_APPLY_COOLDOWN')) set({toast:'최근 참가 취소로 지금은 모임을 열 수 없어요 — 방금 만든 모임은 서버에 저장되지 않았으니 삭제 후 나중에 다시 만들어주세요.'});
        else set({toast:'모임이 서버에 저장되지 못했어요 — 이 모임은 다른 회원에게 보이지 않아요. 모임을 삭제 후 다시 만들어주세요.'});
        return;
      }
      if (res && res.data && res.data.id) {
        // 서버가 발급한 진짜 ID를 저장해둬야 나중에 그 모임을 서버에서 찾아 삭제할 수 있음
        set({myHostedMeetups: nextHosted.map((mm,ix)=>ix===idx?{...mm, serverId:res.data.id}:mm)});
        if (d.placeVisibility==='confirmed' && d.place) saveMeetupPlaceDetail(res.data.id, d.place).then(r=>{ if(r&&r.error) console.error('place detail 저장 실패', r.error); });
      }
    });
  };
  return <div className="wapp"><WNav S={S} go={go} set={set}/>
    <div className="wmain" style={{maxWidth:640}}>
      <button className="wback" onClick={()=>step===0?go(editing?'mymeethosted':'profile'):back()}><Ic n="IconNormalChevronLeftNameChevronLeftTightFalseThick" s={14}/>{step===0?(editing?'내가 개설한 모임으로':'내 정보로'):'이전 단계'}</button>
      <div className="wsect"><h2>{editing?'모임 정보 수정':'모임 개설'}</h2></div>
      <div style={{display:'flex', gap:6, marginBottom:24}}>
        {STEPS.map((t,i)=><div key={t} style={{flex:1, textAlign:'center'}}>
          <div style={{width:24, height:24, borderRadius:'50%', margin:'0 auto 6px', display:'flex', alignItems:'center', justifyContent:'center', fontSize:11, fontWeight:700, background:step>=i?'var(--c-primary)':'var(--c-fill)', color:step>=i?'#fff':'var(--c-faint)'}}>{i+1}</div>
          <div style={{fontSize:10, color:step>=i?'var(--c-text)':'var(--c-faint)', lineHeight:1.3}}>{t}</div>
        </div>)}
      </div>

      <div style={{display:'flex', flexDirection:'column', gap:16}}>
        {step===0&&<>
          <PhotoUpload value={d.photoUrl} onChange={url=>upd({photoUrl:url})} folder="meetup" shape="rounded" size="100%" placeholder="모임 대표 사진 올리기"/>
          <div>
            <div className="meta" style={{marginBottom:6}}>모임 이름</div>
            <input value={d.title} onChange={e=>upd({title:e.target.value})} placeholder="예: 강남 밤모임" style={{width:'100%', border:'1px solid var(--c-line)', borderRadius:8, padding:'10px 14px', fontFamily:'inherit', fontSize:14, outline:'none'}}/>
          </div>
          <div>
            <div className="meta" style={{marginBottom:8}}>어떤 활동인가요? <span style={{fontWeight:400}}>(여러 개 선택 가능)</span></div>
            <div style={{display:'flex', gap:8, flexWrap:'wrap'}}>{purposes.map(p=><Chip key={p} label={p} onClick={()=>upd({purposes:(d.purposes||[]).includes(p)?(d.purposes||[]).filter(x=>x!==p):[...(d.purposes||[]),p]})} on={(d.purposes||[]).includes(p)}/>)}<Chip label="기타·직접 입력" onClick={()=>upd({purposes:(d.purposes||[]).includes('기타·직접 입력')?(d.purposes||[]).filter(x=>x!=='기타·직접 입력'):[...(d.purposes||[]),'기타·직접 입력']})} on={(d.purposes||[]).includes('기타·직접 입력')}/></div>
            {(d.purposes||[]).includes('기타·직접 입력')&&<div style={{marginTop:10}}>
              <div className="meta" style={{marginBottom:6}}>어떤 활동을 하는 모임인가요?</div>
              <input value={d.customActivity||''} onChange={e=>upd({customActivity:e.target.value})} placeholder="예: 보드게임을 하면서 같이 저녁을 먹는 모임" style={{width:'100%', border:'1px solid var(--c-line)', borderRadius:8, padding:'10px 14px', fontFamily:'inherit', fontSize:14, outline:'none'}}/>
              {(d.customActivity||'').trim()
                ?<p className="meta" style={{margin:'6px 2px 0', color:'var(--c-green)', fontWeight:700}}>"{(d.customActivity||'').trim()}" 활동으로 모임 카드에 표시돼요 — 이대로 진행하면 반영됩니다.</p>
                :<p className="meta" style={{margin:'6px 2px 0'}}>목록에 없는 활동도 자유롭게 입력할 수 있어요 — 입력만 하면 자동으로 반영돼요.</p>}
            </div>}
            {(d.purposes||[]).length>1&&<div className="notice" style={{marginTop:10}}><Ic n="IconNormalCircleInfo2NameCircleInfoFillFalse" s={15}/><span>활동을 여러 개 고르면 모임 안에 여러 활동이 함께 진행돼요. 참가자가 일부 활동만 참여하고 싶다면 신청 후 채팅방에서 직접 조율해요.</span></div>}
          </div>
          <div>
            <div className="meta" style={{marginBottom:8}}>분위기</div>
            <div style={{display:'flex', gap:8, flexWrap:'wrap'}}>{moods.map(m=><Chip key={m} label={m} onClick={()=>upd({mood:m})} on={d.mood===m}/>)}</div>
          </div>
          <label className="row" style={{gap:8, cursor:'pointer', fontSize:13.5, minHeight:36}}>
            <input type="checkbox" checked={!!d.soloWelcome} onChange={e=>upd({soloWelcome:e.target.checked})} style={{width:18, height:18, accentColor:'var(--c-primary)'}}/>
            혼자 참여 환영 — 혼자 오는 참가자도 자연스럽게 어울릴 수 있도록 안내를 추가할게요
          </label>
          <div>
            <div className="meta" style={{marginBottom:6}}>소개</div>
            <textarea value={d.desc} onChange={e=>upd({desc:e.target.value})} rows={3} placeholder="어떤 모임인지 소개해주세요" style={{width:'100%', border:'1px solid var(--c-line)', borderRadius:8, padding:'10px 14px', fontFamily:'inherit', fontSize:14, outline:'none', resize:'none'}}/>
          </div>
        </>}

        {step===1&&<>
          <div style={{display:'flex', gap:12}}>
            <div style={{flex:1}}>
              <div className="meta" style={{marginBottom:6}}>날짜</div>
              <DatePicker value={d.date} onChange={dt=>upd({date:dt, when:formatWhen(dt, d.time)})}/>
            </div>
            <div style={{flex:1}}>
              <div className="meta" style={{marginBottom:6}}>시간</div>
              <TimePicker value={d.time} onChange={t=>upd({time:t, when:formatWhen(d.date, t)})}/>
            </div>
          </div>
          <div>
            <div className="meta" style={{marginBottom:8}}>모임 지역</div>
            <div style={{display:'flex', gap:8, marginBottom:10}}>
              <Chip label="국내 (카카오맵)" onClick={()=>upd({regionType:'kr'})} on={(d.regionType||'kr')==='kr'}/>
              <Chip label="해외" onClick={()=>upd({regionType:'abroad'})} on={d.regionType==='abroad'}/>
            </div>
            {(d.regionType||'kr')==='kr'?<>
              <div className="meta" style={{marginBottom:6}}>장소 (이름 또는 주소)</div>
              <div className="row" style={{gap:8}}>
                <input value={d.place} onChange={e=>upd({place:e.target.value, placeLat:null, placeLng:null})} placeholder="예: 강남역 카페 또는 서울 강남구 테헤란로 123" style={{flex:1, minWidth:0, border:'1px solid var(--c-line)', borderRadius:8, padding:'10px 14px', fontFamily:'inherit', fontSize:14, outline:'none'}}/>
                {getKakaoMapKey()&&<DSBtn label="지도 검색" size="md" fit variant="outlined" onClick={()=>upd({placeSearchOpen:true})}/>}
              </div>
              {d.placeLat&&<p className="meta" style={{margin:'6px 2px 0', color:'var(--c-green)', fontWeight:700}}>카카오맵 장소가 연결됐어요 — 상세 화면에 정확한 핀이 표시돼요.</p>}
              <div style={{marginTop:10}}>
                <MapEmbed {...(d.placeLat?{lat:d.placeLat, lng:d.placeLng}:geocodeKR(d.place))} height={160} label="입력한 위치 미리보기"/>
                <p className="meta" style={{margin:'6px 2px 0'}}>{d.placeLat?'선택한 장소 위치예요. 장소명을 수정하면 연결이 풀려요.':'지도 검색으로 장소를 선택하면 정확한 위치가 저장돼요.'}</p>
              </div>
              {d.placeSearchOpen&&<PlaceSearchModal initial={d.place} onClose={()=>upd({placeSearchOpen:false})} onPick={pk=>upd({place:pk.name, placeLat:pk.lat, placeLng:pk.lng, placeSearchOpen:false})}/>}
            </>:<>
              <div className="meta" style={{marginBottom:6}}>국가</div>
              <CountrySelect value={d.country||''} onChange={v=>upd({country:v})} exclude={['대한민국']}/>
              <div className="meta" style={{margin:'12px 0 6px'}}>가게명 (선택)</div>
              <input value={d.placeName||''} onChange={e=>upd({placeName:e.target.value})} placeholder="예: Blue Bottle Coffee Manhattan" style={{width:'100%', border:'1px solid var(--c-line)', borderRadius:8, padding:'10px 14px', fontFamily:'inherit', fontSize:14, outline:'none', marginBottom:10}}/>
              <div className="meta" style={{marginBottom:6}}>주소</div>
              <input value={d.place} onChange={e=>upd({place:e.target.value})} placeholder="예: 123 5th Ave, New York, NY" style={{width:'100%', border:'1px solid var(--c-line)', borderRadius:8, padding:'10px 14px', fontFamily:'inherit', fontSize:14, outline:'none'}}/>
              <p className="meta" style={{margin:'6px 2px 0'}}>해외 지역은 아직 지도 없이 국가·주소 텍스트로만 안내돼요.</p>
            </>}
          </div>
        </>}

        {step===2&&<>
          <div>
            <div className="meta" style={{marginBottom:6}}>정원</div>
            <div className="row" style={{gap:8}}>
              <button style={{width:38, height:38, borderRadius:8, border:'1px solid var(--c-line)', background:'var(--c-bg)', fontSize:17, cursor:'pointer'}} onClick={()=>upd({cap:String(Math.max(2,(parseInt(d.cap)||6)-1))})}>−</button>
              <input value={d.cap} onChange={e=>upd({cap:e.target.value.replace(/[^0-9]/g,'')})} type="number" min="2" max="50" style={{flex:1, textAlign:'center', border:'1px solid var(--c-line)', borderRadius:8, padding:'10px 14px', fontFamily:'inherit', fontSize:14, outline:'none'}}/>
              <button style={{width:38, height:38, borderRadius:8, border:'1px solid var(--c-line)', background:'var(--c-bg)', fontSize:17, cursor:'pointer'}} onClick={()=>upd({cap:String(Math.min(50,(parseInt(d.cap)||6)+1))})}>+</button>
            </div>
          </div>
          <div>
            <div className="meta" style={{marginBottom:8}}>정원 마감 시 대기 명단</div>
            <div style={{display:'flex', gap:8}}>
              <Chip label="대기 명단 받기" onClick={()=>upd({waitlistOk:true})} on={d.waitlistOk!==false}/>
              <Chip label="받지 않기" onClick={()=>upd({waitlistOk:false})} on={d.waitlistOk===false}/>
            </div>
          </div>
          <div>
            <div className="meta" style={{marginBottom:8}}>정원과 승인 방식</div>
            <div style={{display:'flex', gap:8, marginBottom:8}}>
              <Chip label="즉시 확정" onClick={()=>upd({approvalMode:'instant'})} on={(d.approvalMode||'instant')==='instant'}/>
              <Chip label="모임장 승인제" onClick={()=>upd({approvalMode:'approval'})} on={d.approvalMode==='approval'}/>
            </div>
            <p className="meta" style={{margin:0}}>{d.approvalMode==='approval'?'신청이 오면 먼저 승인 대기 상태가 되고, 직접 승인해야 참가가 확정돼요.':'정원이 남아있으면 신청 즉시 참가가 확정돼요.'}</p>
          </div>
          <div>
            <div className="meta" style={{marginBottom:8}}>정확한 장소 공개</div>
            <div style={{display:'flex', gap:8, marginBottom:8}}>
              <Chip label="누구나 볼 수 있게 공개" onClick={()=>upd({placeVisibility:'public'})} on={(d.placeVisibility||'public')==='public'}/>
              <Chip label="참가 확정자에게만 공개" onClick={()=>upd({placeVisibility:'confirmed'})} on={d.placeVisibility==='confirmed'}/>
            </div>
            <p className="meta" style={{margin:0}}>{d.placeVisibility==='confirmed'?'목록·상세에는 동네 단위까지만 보이고, 정확한 주소는 참가가 확정된 회원에게만 공개돼요.':'모임 상세에서 정확한 장소를 누구나 바로 볼 수 있어요.'}</p>
          </div>
        </>}

        {step===3&&<>
          <div>
            <div className="meta" style={{marginBottom:8}}>참가비</div>
            <div style={{display:'flex', gap:8, marginBottom:10, flexWrap:'wrap'}}>
              <Chip label="참가비 없음" onClick={()=>upd({feeType:'free'})} on={(d.feeType||'free')==='free'}/>
              <Chip label="현장에서 각자 결제" onClick={()=>upd({feeType:'split'})} on={d.feeType==='split'}/>
              <Chip label="주최자 선결제 · N분의 1" onClick={()=>upd({feeType:'dutch'})} on={d.feeType==='dutch'}/>
              <Chip label="참가비 있음 (금액 입력)" onClick={()=>upd({feeType:'paid'})} on={d.feeType==='paid'}/>
            </div>
            {d.feeType==='split'&&<p className="meta" style={{margin:'0 2px 10px', lineHeight:1.6}}>각자 먹거나 쓴 만큼 현장에서 직접 결제하는 방식이에요.</p>}
            {d.feeType==='dutch'&&<p className="meta" style={{margin:'0 2px 10px', lineHeight:1.6}}>주최자가 먼저 계산하고, 현장에서 인원수대로 나눠(N분의 1) 정산하는 방식이에요.</p>}
            {d.feeType==='paid'&&<>
              <div className="meta" style={{marginBottom:6}}>참가비 (원)</div>
              <input value={d.cost||''} onChange={e=>upd({cost:e.target.value.replace(/[^0-9]/g,'')})} placeholder="예: 20000" style={{width:'100%', border:'1px solid var(--c-line)', borderRadius:8, padding:'10px 14px', fontFamily:'inherit', fontSize:14, outline:'none', marginBottom:10}}/>
              <div className="meta" style={{marginBottom:6}}>포함 내역</div>
              <input value={d.costIncludes||''} onChange={e=>upd({costIncludes:e.target.value})} placeholder="예: 식사·음료 포함" style={{width:'100%', border:'1px solid var(--c-line)', borderRadius:8, padding:'10px 14px', fontFamily:'inherit', fontSize:14, outline:'none'}}/>
              <p className="meta" style={{margin:'10px 2px 0', lineHeight:1.6}}>참가비는 앱에서 결제되지 않고, 모임 당일 현장에서 정산해요.</p>
            </>}
          </div>
        </>}

        {step===4&&<>
          <div className="card" style={{padding:'16px 18px'}}>
            <b style={{fontSize:14.5, display:'block', marginBottom:10}}>취소 및 참석 안내 (기본값)</b>
            <div style={{display:'flex', flexDirection:'column', gap:6}}>
              <span style={{fontSize:13.5, color:'var(--c-sub)'}}>모임 24시간 전까지 — 참가 취소 가능해요</span>
              <span style={{fontSize:13.5, color:'var(--c-sub)'}}>24시간 이내 취소·노쇼 — 다음 신청에 제한이 있을 수 있어요</span>
              <span style={{fontSize:13.5, color:'var(--c-sub)'}}>모임장 사정으로 취소된 경우 — 바로 알려드려요</span>
            </div>
          </div>
          <label className="row" style={{gap:8, cursor:'pointer', fontSize:13.5}}>
            <input type="checkbox" checked={!!d.safetyAck} onChange={e=>upd({safetyAck:e.target.checked})} style={{width:18, height:18, accentColor:'var(--c-primary)'}}/>
            안전수칙과 취소 및 참석 안내를 참가자에게 안내하고, 커뮤니티 가이드를 준수할게요
          </label>
          <div className="card" style={{padding:'14px 16px', marginTop:4}}>
            <label className="row" style={{gap:8, cursor:'pointer', fontSize:13.5, alignItems:'flex-start'}}>
              <input type="checkbox" checked={!!d.photoPromoConsent} onChange={e=>upd({photoPromoConsent:e.target.checked})} style={{width:18, height:18, accentColor:'var(--c-primary)', marginTop:2}}/>
              <span>이 모임에서 찍힌 사진을 운영진 SNS 홍보에 활용해도 괜찮아요</span>
            </label>
            <p className="meta" style={{margin:'8px 0 0 26px', lineHeight:1.6}}>체크하면 신청 화면에 미리 안내돼서, 참가자가 이걸 보고 신청 여부를 정할 수 있어요. 실제로는 참가자가 모임 후기에 직접 올린 사진만 홍보에 쓰여요 — 후기에 안 올리면 쓰이지 않아요.</p>
          </div>
        </>}

        {step===5&&<>
          <p className="meta" style={{margin:'0 0 4px'}}>참가자에게 이렇게 보여요</p>
          <div className="wgrid" style={{maxWidth:280}}>
            <MeetCard m={{id:'preview', title:d.title||'(제목 없음)', when:d.when||'일정 미정', size:(d.cap||'6')+'명 정원', cardTags:[...purposeLabels.map(p=>[p,'primary']), [d.mood||'','']].filter(t=>t[0]), soloLine:'내가 개설한 모임', soloWelcome:!!d.soloWelcome, capMax:parseInt(d.cap)||undefined, capJoined:0, host:{name:S.profile.nickname||'나', reviews:0}}} onClick={()=>{}}/>
          </div>
          {!editing&&<div className="notice"><Ic n="IconNormalCircleInfo2NameCircleInfoFillFalse" s={16}/><span>등록하면 바로 공개돼요.</span></div>}
        </>}

        <div className="row" style={{gap:10, marginTop:8}}>
          {step<STEPS.length-1?<DSBtn label="다음" disable={!canNext[step]} onClick={next} style={{flex:1}}/>:<DSBtn label={editing?'수정 사항 저장하기':'모임 개설하기'} disable={!d.title.trim()} onClick={submit} style={{flex:1}}/>}
        </div>
      </div>
    </div>
  </div>;
}
function WScrEvents({S,set,go}){
  const likes=S.eventLikes||{};
  const events = useEvents();
  return <div className="wapp"><WNav S={S} go={go} set={set}/>
    <div className="wmain" style={{maxWidth:760}}>
      <div className="wsect"><h2>이벤트</h2></div>
      <div className="notice violet" style={{marginBottom:20}}><Ic n="IconNormalStarNameStarFillFillTrue" s={16}/><span>운영진이 직접 여는 특별 이벤트 게시판이에요 — 일반 회원은 글을 올릴 수 없고, 공감으로 관심을 표현할 수 있어요.</span></div>
      <div style={{display:'grid', gridTemplateColumns:'repeat(2,1fr)', gap:16}}>
        <button className="card" style={{textAlign:'left', cursor:'pointer', fontFamily:'inherit', padding:'20px 22px'}} onClick={()=>go('eventslist')}>
          <Ic n="IconNormalStarNameStarFillFillTrue" s={22} c="var(--c-primary)"/>
          <div style={{fontSize:16, fontWeight:800, margin:'10px 0 4px'}}>이벤트 전체보기</div>
          <div className="meta">{events.length}개 진행 중 · 자세히 보기</div>
        </button>
        <button className="card" style={{textAlign:'left', cursor:'pointer', fontFamily:'inherit', padding:'20px 22px'}} onClick={()=>go('eventshighlights')}>
          <Ic n="IconNormalCameraNameCameraFillFalse" s={22} c="var(--c-violet)"/>
          <div style={{fontSize:16, fontWeight:800, margin:'10px 0 4px'}}>지난 이벤트 하이라이트</div>
          <div className="meta">사진과 후기 모아보기</div>
        </button>
      </div>
    </div>
    <Footer go={go} set={set}/>
  </div>;
}
// 운영진 이벤트 작성(2026-07-30 사용자 지시) — events 테이블에 직접 등록(RLS: 운영진만 쓰기 가능)
function AdminEventCompose({S, set}) {
  const [open, setOpen] = React.useState(false);
  const [d, setD] = React.useState({title:'', tag:'', when_label:'', city:'', cost_text:'무료', cap:'10', mode:'firstcome', description:''});
  const [busy, setBusy] = React.useState(false);
  const [err, setErr] = React.useState('');
  const upd = patch => setD(prev=>({...prev, ...patch}));
  const inputSt = {width:'100%', border:'1px solid var(--c-line)', borderRadius:8, padding:'9px 12px', fontFamily:'inherit', fontSize:14, outline:'none'};
  const submit = () => {
    if (busy) return;
    if (!d.title.trim()) { setErr('제목을 입력해주세요.'); return; }
    setErr(''); setBusy(true);
    adminCreateEvent({title:d.title.trim(), tag:d.tag.trim()||'이벤트', tone:'primary', when_label:d.when_label.trim(), city:d.city.trim(), cost_text:d.cost_text.trim()||'무료', description:d.description.trim(), mode:d.mode, cap:parseInt(d.cap)||10, host_label:'모두모여 운영진', created_by:S.acctEmail||''}).then(res=>{
      setBusy(false);
      if (res && res.error) { setErr('등록에 실패했어요 — 운영진 계정인지 확인해주세요.'); return; }
      setOpen(false); setD({title:'', tag:'', when_label:'', city:'', cost_text:'무료', cap:'10', mode:'firstcome', description:''});
      set({toast:'이벤트가 등록됐어요 — 목록에 바로 표시돼요'});
    });
  };
  if (!open) return <div className="card" style={{marginBottom:16, display:'flex', alignItems:'center', gap:10}}>
    <Ic n="IconNormalMegaphoneNameMegaphoneFillFalse" s={16} c="var(--c-primary)"/>
    <span style={{flex:1, fontSize:13.5, fontWeight:700}}>운영진 전용 — 공식 이벤트 등록</span>
    <DSBtn label="이벤트 만들기" size="md" fit onClick={()=>setOpen(true)}/>
  </div>;
  return <div className="card" style={{marginBottom:16, display:'flex', flexDirection:'column', gap:10}}>
    <b style={{fontSize:14.5}}>공식 이벤트 등록</b>
    <input placeholder="제목 (예: 서울 유학생 소개팅 파티)" value={d.title} onChange={e=>upd({title:e.target.value})} style={inputSt}/>
    <div style={{display:'flex', gap:8}}>
      <input placeholder="태그 (예: 소개팅)" value={d.tag} onChange={e=>upd({tag:e.target.value})} style={{...inputSt, flex:1}}/>
      <input placeholder="도시 (예: 서울)" value={d.city} onChange={e=>upd({city:e.target.value})} style={{...inputSt, flex:1}}/>
    </div>
    <input placeholder="일정 표시 (예: 8월 16일(토) 오후 6시 · 강남)" value={d.when_label} onChange={e=>upd({when_label:e.target.value})} style={inputSt}/>
    <div style={{display:'flex', gap:8}}>
      <input placeholder="비용 표시 (예: 4만원 (식사 포함))" value={d.cost_text} onChange={e=>upd({cost_text:e.target.value})} style={{...inputSt, flex:1}}/>
      <input placeholder="정원" type="number" min="1" value={d.cap} onChange={e=>upd({cap:e.target.value})} style={{...inputSt, width:90, flex:'none'}}/>
    </div>
    <div className="row" style={{gap:8}}>
      <Chip label="선착순 확정" tone={d.mode==='firstcome'?'primary':'line'} onClick={()=>upd({mode:'firstcome'})}/>
      <Chip label="사연 추첨" tone={d.mode==='lottery'?'primary':'line'} onClick={()=>upd({mode:'lottery'})}/>
    </div>
    <textarea placeholder="소개 문구" rows={3} value={d.description} onChange={e=>upd({description:e.target.value})} style={{...inputSt, resize:'none'}}/>
    {err&&<div role="alert" className="meta" style={{color:'var(--c-red)', fontWeight:600}}>{err}</div>}
    <div style={{display:'flex', gap:8}}>
      <div style={{flex:1}}><DSBtn label="취소" variant="outlined" color="assistive" size="md" onClick={()=>setOpen(false)}/></div>
      <div style={{flex:1}}><DSBtn label={busy?'등록 중…':'등록하기'} size="md" disable={busy} onClick={submit}/></div>
    </div>
  </div>;
}
function WScrEventsList({S,set,go}){
  const likes=S.eventLikes||{};
  const toggleLike=(id,ev)=>{ ev.stopPropagation(); set({eventLikes:{...likes,[id]:!likes[id]}}); };
  const events = useEvents();
  const page = S.eventsPage||1;
  const perPage = 2;
  const totalPages = Math.max(1, Math.ceil(events.length/perPage));
  const items = events.slice((page-1)*perPage, page*perPage);
  return <div className="wapp"><WNav S={S} go={go} set={set}/>
    <div className="wmain" style={{maxWidth:760}}>
      <button className="wback" onClick={()=>go('events')}><Ic n="IconNormalChevronLeftNameChevronLeftTightFalseThick" s={14}/>이벤트로</button>
      <div className="wsect"><h2>이벤트 전체보기</h2></div>
      {isAdminUser(S)&&<AdminEventCompose S={S} set={set}/>}
      {events.length===0&&<div className="card" style={{textAlign:'center', padding:'40px 20px'}}>
        <p style={{fontSize:14.5, fontWeight:700, margin:0}}>지금 진행 중인 이벤트가 없어요.</p>
      </div>}
      {items.map((e,i)=><Reveal key={e.id} delay={i*80}><div className="card" style={{marginBottom:14, padding:'20px 22px', cursor:'pointer'}} onClick={()=>go('eventdetail',{eventId:e.id})}>
        <Chip label={e.tag} tone={e.tone}/>
        <div style={{fontSize:17, fontWeight:800, margin:'10px 0 5px'}}>{e.title}</div>
        <div className="meta" style={{marginBottom:4}}>{e.when} · 주최 {e.host}</div>
        <div className="meta" style={{marginBottom:10}}>{e.city} · {e.cost}{e.sponsor?` · 협찬 ${e.sponsor} (${e.sponsorItem})`:''}</div>
        <p style={{fontSize:14, color:'var(--c-sub)', margin:'0 0 12px', lineHeight:1.6}}>{e.desc}</p>
        <div style={{marginBottom:12}}><EventStats e={e}/></div>
        <button onClick={ev=>toggleLike(e.id,ev)} style={{display:'flex', alignItems:'center', gap:6, background:'none', border:'1px solid var(--c-line)', borderRadius:20, padding:'8px 16px', cursor:'pointer', fontFamily:'inherit', fontSize:13, fontWeight:700, color:likes[e.id]?'var(--c-red)':'var(--c-sub)'}}>
          <Ic n={likes[e.id]?'IconNormalHeartNameHeartFillFillTrue':'IconNormalHeartNameHeartFillFalse'} s={16}/>공감{likes[e.id]?' (관심 등록됨)':''}
        </button>
      </div></Reveal>)}
      <Pagination page={page} total={totalPages} onChange={n=>set({eventsPage:n})}/>
    </div>
    <Footer go={go} set={set}/>
  </div>;
}
function WScrEventsHighlights({S,set,go}){
  const highlights = [['7월 벙개 · 을지로 골목 투어','참가 22명 · 공감 41'],['6월 소개팅 파티 · 홍대','참가 18명 · 공감 63'],['5월 워홀러 밋업 · 토론토','참가 14명 · 공감 29'],['4월 스터디 모임 데이','참가 11명 · 공감 18'],['3월 봄맞이 피크닉','참가 26명 · 공감 52'],['2월 신년 소개팅','참가 20명 · 공감 47']];
  const page = S.highlightsPage||1;
  const perPage = 3;
  const totalPages = Math.ceil(highlights.length/perPage);
  const items = highlights.slice((page-1)*perPage, page*perPage);
  return <div className="wapp"><WNav S={S} go={go} set={set}/>
    <div className="wmain" style={{maxWidth:760}}>
      <button className="wback" onClick={()=>go('events')}><Ic n="IconNormalChevronLeftNameChevronLeftTightFalseThick" s={14}/>이벤트로</button>
      <div className="wsect"><h2>지난 이벤트 하이라이트</h2></div>
      <div style={{display:'grid', gridTemplateColumns:'repeat(3,1fr)', gap:14, marginBottom:12}}>
        {items.map((h,i)=><Reveal key={h[0]} delay={i*80}><button className="card" style={{width:'100%', cursor:'pointer', fontFamily:'inherit', textAlign:'left', padding:0, overflow:'hidden'}} onClick={()=>set({toast:'다음 이벤트에서 만나요 — 사진첩은 준비 중이에요'})}>
          <Ph h={120} label="이벤트 사진"/>
          <div style={{padding:'12px 14px'}}><b style={{fontSize:13.5}}>{h[0]}</b><div className="meta" style={{marginTop:4}}>{h[1]}</div></div>
        </button></Reveal>)}
      </div>
      <Pagination page={page} total={totalPages} onChange={n=>set({highlightsPage:n})}/>
    </div>
    <Footer go={go} set={set}/>
  </div>;
}
function WScrEventChat({S,set,go}){
  const {useState, useEffect, useRef} = React;
  const eid = String(S.detailId||'');
  const meta = S.eventChatMeta||{};
  const [msgs, setMsgs] = useState(null);
  const [input, setInput] = useState('');
  const [err, setErr] = useState('');
  const listRef = useRef(null);
  useEffect(()=>{
    let alive=true;
    const load=()=>fetchEventChat(eid).then(rows=>{ if(alive&&rows) setMsgs(rows); else if(alive&&rows===null&&msgs===null) setErr('채팅방에 입장할 수 없어요 — 선정자만 입장할 수 있는 공간이에요.'); });
    load();
    const iv=setInterval(load, 5000);
    return ()=>{ alive=false; clearInterval(iv); };
  }, [eid]);
  useEffect(()=>{ const el=listRef.current; if(el) requestAnimationFrame(()=>{ el.scrollTop=el.scrollHeight; }); }, [msgs&&msgs.length]);
  const send=()=>{
    const v=input.trim(); if(!v) return;
    setInput(''); setErr('');
    sendEventChat(eid, v).then(r=>{
      if(r&&r.error){ const m=String(r.error.message||''); setErr(m.includes('MM_RATE_LIMIT')?'메시지를 너무 빠르게 보내고 있어요 — 잠시 후 다시 보내주세요.':m.includes('MM_NOT_ALLOWED')?'선정자만 보낼 수 있어요.':'전송에 실패했어요 — 잠시 후 다시 시도해주세요.'); setInput(v); return; }
      fetchEventChat(eid).then(rows=>{ if(rows) setMsgs(rows); });
    });
  };
  return <div className="wapp"><WNav S={S} go={go} set={set}/>
    <div className="wmain" style={{maxWidth:640}}>
      <button className="wback" onClick={()=>go(isAdminUser(S)?'admin':'eventdetail')}><Ic n="IconNormalChevronLeftNameChevronLeftTightFalseThick" s={14}/>{isAdminUser(S)?'운영진 페이지로':'이벤트로'}</button>
      <h2 style={{fontSize:19, margin:'8px 0 6px'}}>{meta.title||'이벤트'} · 선정자 채팅방</h2>
      {meta.anonymous&&<div className="notice blue" style={{marginBottom:10}}><Ic n="IconNormalCircleInfo2NameCircleInfoFillFalse" s={15}/><span>익명 채팅방이에요 — 서로 가명으로만 표시돼요. 신고가 접수되면 운영진은 계정을 확인할 수 있어요.</span></div>}
      {err&&<div role="alert" className="notice orange" style={{marginBottom:10}}><Ic n="IconNormalCircleExclamation2NameCircleExclamationFillFalse" s={15}/><span>{err}</span></div>}
      <div ref={listRef} style={{height:380, overflowY:'auto', background:'var(--c-fill-a)', borderRadius:14, padding:'14px'}}>
        {msgs==null&&!err&&<p className="meta" style={{textAlign:'center', padding:'20px 0'}}>채팅을 불러오는 중…</p>}
        {msgs&&msgs.length===0&&<p className="meta" style={{textAlign:'center', padding:'20px 0'}}>아직 대화가 없어요 — 첫 인사를 남겨보세요!</p>}
        {(msgs||[]).map(m=><div key={m.id} style={{display:'flex', flexDirection:'column', alignItems:m.mine?'flex-end':'flex-start', marginBottom:10}}>
          {!m.mine&&<span className="row" style={{gap:5, fontSize:11.5, color:'var(--c-sub)', marginBottom:3}}><Av name={m.display_name} size={18}/>{m.display_name}{m.is_admin&&<AdminBadge small/>}</span>}
          <div style={{maxWidth:'78%', background:m.mine?'var(--c-primary)':(m.is_admin?'var(--c-primary-bg)':'#fff'), color:m.mine?'#fff':'var(--c-text)', borderRadius:12, padding:'8px 12px', fontSize:13.5, lineHeight:1.55, whiteSpace:'pre-wrap', wordBreak:'break-word', border:m.mine?'none':'1px solid var(--c-line)'}}>{m.body}</div>
        </div>)}
      </div>
      <div className="row" style={{gap:8, marginTop:10}}>
        <input value={input} onChange={e2=>setInput(e2.target.value)} onKeyDown={e2=>{ if(e2.key==='Enter') send(); }} placeholder="메시지 입력" style={{flex:1, minWidth:0, background:'var(--c-fill)', border:'none', borderRadius:999, padding:'11px 14px', fontSize:13.5, fontFamily:'inherit', outline:'none'}}/>
        <button className="iconbtn" aria-label="보내기" onClick={send}><Ic n="IconNormalSendNameSendFillFillTrue" s={19} c="var(--c-primary)"/></button>
      </div>
      <p className="meta" style={{margin:'8px 2px 0'}}>사진·링크 전송은 안전을 위해 제한될 수 있어요. 문제가 있는 메시지는 신고하기로 알려주세요.</p>
    </div>
  </div>;
}
function WScrEventDetail({S,set,go}){
  // 31.5: 내 신청 상태(서버)·채팅방 존재 조회 — 선정 배지·입장 버튼용
  const [myApp, setMyApp] = React.useState(undefined);
  const [evRoom2, setEvRoom2] = React.useState(null);
  const isServerEvent = /^[0-9a-f-]{36}$/.test(String((S.tempEventDetail&&S.tempEventDetail.id)||S.detailId||''));
  React.useEffect(()=>{
    const eid = String((S.tempEventDetail&&S.tempEventDetail.id)||S.detailId||'');
    if (!isServerEvent || S.authStatus!=='authenticated' || !S.acctEmail) { setMyApp(null); return; }
    let al=true;
    fetchMyEventApplication(eid, S.acctEmail).then(r=>{ if(al) setMyApp(r); });
    fetchEventChatRoom(eid).then(r=>{ if(al) setEvRoom2(r); });
    return ()=>{ al=false; };
  }, [S.detailId, S.authStatus, S.acctEmail]);

  const events = useEvents();
  const e = events.find(x=>x.id===S.eventId);
  const likes = S.eventLikes||{};
  const applied = e && S.eventApplied&&S.eventApplied[e.id];
  const ack = S.eventTermsAck;
  const applyEvent = () => guardAuth(S,set,go,()=>{
    const status = e.mode==='firstcome'?(e.applied>=e.cap?'waitlisted':'confirmed'):'pending';
    applyToEvent(e.id, {applicant_email:S.acctEmail, applicant_name:S.profile.nickname||'나', status, reason:S.eventReason||null, group_choice:(S.eventGroup&&S.eventGroup[e.id])||null}).then(res=>{
      if (res&&res.error) { set({toast:'신청에 실패했어요 — 다시 시도해주세요'}); return; }
      set({eventApplied:{...S.eventApplied,[e.id]:status}, eventReason:'', toast:e.mode==='firstcome'?(status==='waitlisted'?'대기 신청 완료 — 자리가 나면 알려드려요':'참가가 확정됐어요'):'신청 완료 — 정원 초과 시 사연을 보고 추첨해요'});
    });
  });
  const cancelEvent = (toast) => {
    cancelEventApplication(e.id, S.acctEmail).then(res=>{
      if (res&&res.error) { set({toast:'취소에 실패했어요 — 다시 시도해주세요'}); return; }
      const next = {...S.eventApplied}; delete next[e.id];
      set({eventApplied:next, eventCondAck:false, toast});
    });
  };
  if (!e) return <div className="wapp"><WNav S={S} go={go} set={set}/><div className="wmain" style={{maxWidth:680}}>
    <button className="wback" onClick={()=>go('events')}><Ic n="IconNormalChevronLeftNameChevronLeftTightFalseThick" s={14}/>이벤트로</button>
    <p className="meta" style={{padding:'24px 0'}}>이벤트를 찾을 수 없어요 — 종료됐거나 존재하지 않는 이벤트일 수 있어요.</p>
  </div></div>;
  return <div className="wapp"><WNav S={S} go={go} set={set}/>
    <div className="wmain" style={{maxWidth:680}}>
      <button className="wback" onClick={()=>go('events')}><Ic n="IconNormalChevronLeftNameChevronLeftTightFalseThick" s={14}/>이벤트로</button>
      <div className="detail-hero" style={{background:TONE_BG[e.tone]||'var(--c-primary-bg)', height:120}} aria-hidden="true">
        <span className="dh-deco dh-c1"/><span className="dh-deco dh-c2"/><span className="dh-deco dh-c3"/><span className="dh-deco dh-c4"/>
        <span className="dh-puck" style={{width:64,height:64}}><ActIcon label={e.tag} size={30} color={TONE_FG[e.tone]||'var(--c-primary)'} fallbackIcon={{'소개팅':'IconNormalHeartNameHeartFillFalse','네트워킹':'IconNormalPersonsNamePersonsFillFalse','언어 교환':'IconNormalBubbleNameBubbleFillFalse'}[e.tag]||'IconNormalSparkleNameSparkle'}/></span>
        <svg className="dh-pin" width="20" height="25" viewBox="0 0 24 30"><path d="M12 0C5.4 0 0 5.4 0 12c0 9 12 18 12 18s12-9 12-18c0-6.6-5.4-12-12-12z" fill="#FF8A5B"/><circle cx="12" cy="12" r="5" fill="#fff"/></svg>
      </div>
      <Chip label={e.tag} tone={e.tone}/>
      {myApp&&(()=>{ const st = myApp.status; const shown = (st==='rejected'&&!myApp.rejected_notified_at)?'pending':st; return <div className="card" style={{marginTop:12, padding:'14px 16px'}}>
        {shown==='selected'?<>
          <div className="row" style={{gap:8}}><Ic n="IconNormalCircleCheck2NameCircleCheckFillFillTrue" s={18} c="var(--c-green)"/><b style={{fontSize:14.5}}>선정됐어요! 🎉</b></div>
          <p className="meta" style={{margin:'6px 0 0', lineHeight:1.6}}>자세한 안내는 선정자 채팅방에서 운영진 공지로 알려드려요.{evRoom2?'':' 채팅방이 열리면 알림과 함께 여기에 입장 버튼이 생겨요.'}</p>
          {evRoom2&&<div style={{marginTop:10}}><DSBtn label="선정자 채팅방 입장" size="md" onClick={()=>{ set({eventChatMeta:{title:e.title, anonymous:!!evRoom2.anonymous}}); go('eventchat'); }}/></div>}
        </>:shown==='rejected'?<>
          <div className="row" style={{gap:8}}><Ic n="IconNormalCircleInfo2NameCircleInfoFillFalse" s={17} c="var(--c-sub)"/><b style={{fontSize:14}}>이번엔 함께하지 못하게 됐어요</b></div>
          <p className="meta" style={{margin:'6px 0 0', lineHeight:1.6}}>신청해주셔서 감사해요 — 다음 이벤트에서 꼭 만나요.</p>
        </>:<>
          <div className="row" style={{gap:8}}><span className="spinner" style={{width:14,height:14}}></span><b style={{fontSize:14}}>심사 중이에요</b></div>
          <p className="meta" style={{margin:'6px 0 0', lineHeight:1.6}}>결과가 정해지면 알림으로 알려드려요.</p>
        </>}
      </div>; })()}
      {isAdminUser(S)&&<button style={{background:'none',border:'none',color:'var(--c-red)',fontFamily:'inherit',fontSize:12.5,fontWeight:800,cursor:'pointer',padding:'6px 0 0',display:'block'}} onClick={()=>{ if(!window.confirm('이 이벤트를 삭제할까요? 신청 내역도 함께 삭제돼요.')) return; adminDeleteEvent(e.id).then(r=>{ if(r&&r.error){ set({toast:'삭제에 실패했어요'}); return;} go('eventslist',{toast:'이벤트를 삭제했어요'}); }); }}>운영진: 이벤트 삭제</button>}
      <div style={{fontSize:22, fontWeight:800, margin:'12px 0 6px'}}>{e.title}</div>
      <div className="meta" style={{marginBottom:4}}>{e.when} · 주최 {e.host}</div>
      <div className="meta" style={{marginBottom:14}}>{e.city} · {e.cost}{e.sponsor?` · 협찬 ${e.sponsor} (${e.sponsorItem})`:''}</div>
      <p style={{fontSize:14.5, color:'var(--c-sub)', margin:'0 0 16px', lineHeight:1.7}}>{e.desc}</p>
      <div className="card" style={{marginBottom:16, padding:'18px 20px'}}><EventStats e={e}/></div>
      <button onClick={()=>set({eventLikes:{...likes, [e.id]:!likes[e.id]}})} style={{display:'flex', alignItems:'center', gap:6, background:'none', border:'1px solid var(--c-line)', borderRadius:20, padding:'8px 16px', marginBottom:20, cursor:'pointer', fontFamily:'inherit', fontSize:13.5, fontWeight:700, color:likes[e.id]?'var(--c-red)':'var(--c-sub)'}}>
        <Ic n={likes[e.id]?'IconNormalHeartNameHeartFillFillTrue':'IconNormalHeartNameHeartFillFalse'} s={17}/>공감{likes[e.id]?' (관심 등록됨)':''}
      </button>
      {!applied?<div className="card" style={{marginBottom:20, padding:'18px 20px'}}>
        <b style={{fontSize:15}}>{e.mode==='firstcome'&&e.applied>=e.cap?'대기 신청하기 (정원 마감)':'참가 신청하기'+(e.mode==='firstcome'?' (선착순 확정)':' (운영진 추첨)')}</b>
        <p className="meta" style={{margin:'8px 0 12px'}}>{e.mode==='firstcome'?(e.applied>=e.cap?'정원이 찼어요. 대기 신청하면 자리가 생겼을 때 알려드려요.':'신청하면 선착순으로 자리가 즉시 확정돼요.'):'정원이 넘으면 사연을 보고 운영진이 랜덤으로 선정해요 — 결과는 추첨 후 안내돼요.'}</p>
        {e.mode!=='firstcome'&&<textarea value={S.eventReason||''} onChange={ev=>set({eventReason:ev.target.value})} rows={3} placeholder="참가하고 싶은 이유를 적어주세요" style={{width:'100%', border:'1px solid var(--c-line)', borderRadius:8, padding:'10px 14px', fontFamily:'inherit', fontSize:14, outline:'none', resize:'none', marginBottom:12}}/>}
        {e.mode==='firstcome'&&e.tag==='소개팅'&&<div style={{marginBottom:12}}>
          <p className="meta" style={{margin:'0 0 8px', lineHeight:1.6}}>이 모임은 참가자 구성의 균형을 위해 그룹별 정원을 운영해요. 그룹 정보는 다른 참가자에게 공개되지 않고 정원 계산에만 쓰여요.</p>
          <div style={{display:'flex', gap:8}}>{['그룹 A 신청','그룹 B 신청'].map(g=><Chip key={g} label={g} onClick={()=>set({eventGroup:{...S.eventGroup, [e.id]:g}})} on={(S.eventGroup&&S.eventGroup[e.id])===g}/>)}</div>
        </div>}
        {!ack?<div style={{background:'var(--c-fill-a)', borderRadius:10, padding:'14px 16px', marginBottom:12}}>
          <b style={{fontSize:13.5, display:'block', marginBottom:6}}>1. 참가 조건 확인</b>
          <p className="meta" style={{margin:'0 0 10px', lineHeight:1.6}}>비용 {e.cost} · 현장에서 각자 결제해요 · 취소는 행사 24시간 전까지 가능해요.</p>
          <label className="row" style={{gap:8, cursor:'pointer', fontSize:13}}>
            <input type="checkbox" checked={!!S.eventCondAck} onChange={ev=>set({eventCondAck:ev.target.checked})} style={{width:16, height:16, accentColor:'var(--c-primary)'}}/>비용과 취소 정책을 확인했어요
          </label>
        </div>:null}
        {S.eventCondAck?<DSBtn label={e.mode==='firstcome'?(e.applied>=e.cap?'대기 신청하기':'신청 확정하기'):'신청하기'} size="md" disable={(e.mode!=='firstcome'&&!(S.eventReason||'').trim())||(e.mode==='firstcome'&&e.tag==='소개팅'&&!(S.eventGroup&&S.eventGroup[e.id]))} onClick={applyEvent}/>
        :<DSBtn label="다음" size="md" disable style={{opacity:.5}}/>}
      </div>:<div className="card" style={{marginBottom:20, padding:'18px 20px'}}>
        {applied==='waitlisted'&&<>
          <div className="row" style={{gap:8, marginBottom:8}}><Ic n="IconNormalCircleInfo2NameCircleInfoFillFalse" s={16} c="var(--c-orange)"/><b style={{fontSize:14.5}}>대기 신청 완료</b></div>
          <p className="meta" style={{margin:'0 0 14px'}}>자리가 생기면 알림을 보내드리고, 참가가 확정돼요.</p>
          <button style={{background:'none', border:'1px solid var(--c-line)', borderRadius:10, padding:'9px 16px', fontFamily:'inherit', fontSize:13, fontWeight:600, cursor:'pointer', color:'var(--c-red)'}} onClick={()=>cancelEvent('대기 신청을 취소했어요')}>대기 취소</button>
        </>}
        {applied==='pending'&&<>
          <div className="row" style={{gap:8, marginBottom:8}}><Ic n="IconNormalCircleInfo2NameCircleInfoFillFalse" s={16} c="var(--c-orange)"/><b style={{fontSize:14.5}}>신청이 접수됐어요 · 승인 대기</b></div>
          <p className="meta" style={{margin:'0 0 14px'}}>결과 안내 — {e.when.split(' · ')[0]} 시작 전까지 알려드려요.</p>
          <button style={{background:'none', border:'1px solid var(--c-line)', borderRadius:10, padding:'9px 16px', fontFamily:'inherit', fontSize:13, fontWeight:600, cursor:'pointer', color:'var(--c-red)'}} onClick={()=>cancelEvent('신청을 취소했어요')}>신청 취소</button>
        </>}
        {applied==='confirmed'&&<>
          <div className="row" style={{gap:8, marginBottom:8}}><Ic n="IconNormalCircleCheck2NameCircleCheckFillFillTrue" s={16} c="var(--c-green)"/><b style={{fontSize:14.5}}>참가 확정</b></div>
          <p className="meta" style={{margin:'0 0 4px'}}>비용 {e.cost} · 현장에서 각자 결제해요</p>
          <p className="meta" style={{margin:'0 0 14px'}}>취소는 모임 시작 24시간 전까지 가능해요.</p>
          <button style={{background:'none', border:'1px solid var(--c-line)', borderRadius:10, padding:'9px 16px', fontFamily:'inherit', fontSize:13, fontWeight:600, cursor:'pointer', color:'var(--c-red)'}} onClick={()=>cancelEvent('신청을 취소했어요')}>신청 취소</button>
        </>}
      </div>}
    </div>
    <Footer go={go} set={set}/>
  </div>;
}
function WScrSeoulAll({S,set,go}){
  const all=[MEETUPS.m2, MEETUPS.m1, MEETUPS.m6, ...(DEMO_MODE?[{id:'x1', title:'귀국 취준 커피챗', when:'수 오후 8시', size:'4~6명', cardTags:[['커리어','primary'],['조용한 대화','']], soloLine:'첫 참가자 환영 · 대기 1'}, {id:'x4', title:'홍대 브런치 모임', when:'토 오전 11시', size:'4~6명', cardTags:[['친구 만들기','primary'],['첫 참가자 환영','green']], soloLine:'혼자 참여 환영'}]:[])].filter(Boolean);
  return <div className="wapp"><WNav S={S} go={go} set={set}/>
    <div className="wmain">
      <button className="wback" onClick={()=>go('home')}><Ic n="IconNormalChevronLeftNameChevronLeftTightFalseThick" s={14}/>홈으로</button>
      <div className="wsect"><h2>서울 · 전체 모임</h2></div>
      {all.length===0?<div className="card" style={{textAlign:'center', padding:'32px 20px'}}>
        <p style={{fontSize:14.5, fontWeight:700, margin:'0 0 6px'}}>아직 등록된 모임이 없어요.</p>
        <p className="meta" style={{margin:'0 0 14px'}}>새로운 사람들과 하고 싶은 활동이 있다면 첫 모임을 만들어보세요.</p>
        <DSBtn label="첫 모임 만들기" size="md" onClick={()=>startCreateMeetup(S,set,go)}/>
      </div>:<div className="wgrid">{all.map(m=><MeetCard key={m.id} m={m} onClick={()=>MEETUPS[m.id]?go('detail',{detailId:m.id}):go('detail',{detailId:m.id, tempDetail:synthDetailFromCard(m)})}/>)}</div>}
    </div>
  </div>;
}
function WScrSavedMeetups({S,set,go}){
  const savedMeetups = useSavedMeetups(S);
  return <div className="wapp"><WNav S={S} go={go} set={set}/>
    <div className="wmain">
      <button className="wback" onClick={()=>go('home')}><Ic n="IconNormalChevronLeftNameChevronLeftTightFalseThick" s={14}/>홈으로</button>
      <div className="wsect"><h2><Ic n="IconNormalHeartNameHeartFillFillTrue" s={17} c="var(--c-coral)"/>내가 찜한 모임</h2></div>
      {savedMeetups.length===0?<div className="card" style={{textAlign:'center', padding:'32px 20px'}}>
        <p style={{fontSize:14.5, fontWeight:700, margin:'0 0 6px'}}>아직 찜한 모임이 없어요.</p>
        <p className="meta" style={{margin:'0 0 14px'}}>모임 카드의 하트를 누르면 여기에 모아서 볼 수 있어요.</p>
        <DSBtn label="모임 둘러보러 가기" size="md" onClick={()=>go('home')}/>
      </div>:<div className="wgrid">{savedMeetups.map(m=><MeetCard key={m.id} m={m} onClick={()=>go('detail',{detailId:m.id})}/>)}</div>}
    </div>
  </div>;
}
Object.assign(window, {WScrOnboarding, WScrSignup, WScrConsent, WScrSetup, WScrHome, WScrHomeCur, WScrHomeAbroad, WScrHomeSnapshot, WScrHomeTimeline, WScrExplore, WScrRegion, WScrEmpty, WScrSeoulAll, WScrSavedMeetups, WScrProfile, WScrCreateMeetup});
