// 웹 버전 전용 스타일
const WEBCSS = `
html{-webkit-text-size-adjust:100%;text-size-adjust:100%}
html,body,#root{width:100%;max-width:100%;overflow-x:hidden}
img,svg{max-width:100%}
.wapp{font-family:var(--f-body);color:var(--c-text);background:var(--c-bg-alt);min-height:100%;display:flex;flex-direction:column;max-width:100%}
.demo-banner-txt-full{display:inline}
.demo-banner-txt-short{display:none}
@media (max-width:680px){.demo-banner-txt-full{display:none}.demo-banner-txt-short{display:inline}}
.ob-hero{position:relative;width:100%;min-height:92vh;overflow:hidden;display:flex;flex-direction:column;box-sizing:border-box;background:linear-gradient(100deg,#7FB6FF 0%,#5585F8 42%,#3D5BD9 72%,#2C3E9E 100%)}
.ob-sky{position:absolute;inset:0;pointer-events:none;transform:translate(calc(var(--mx,0)*10px),calc(var(--my,0)*10px));transition:transform .7s cubic-bezier(.22,1,.36,1)}
.ob-sun{position:absolute;left:8%;top:14%;width:62px;height:62px;border-radius:50%;background:#FFD166;box-shadow:0 0 0 14px rgba(255,209,102,.18),0 0 56px 18px rgba(255,209,102,.35)}
.ob-cloud{position:absolute;height:20px;border-radius:999px;background:rgba(255,255,255,.9)}
.ob-cloud::before{content:'';position:absolute;left:18%;top:-11px;width:26px;height:26px;border-radius:50%;background:inherit}
.ob-cloud::after{content:'';position:absolute;left:48%;top:-7px;width:18px;height:18px;border-radius:50%;background:inherit}
.obc1{left:16%;top:30%;width:88px;animation:obDriftX 46s linear infinite}
.obc2{left:4%;top:44%;width:64px;opacity:.75;animation:obDriftX 62s linear -20s infinite}
@keyframes obDriftX{from{transform:translateX(0)}50%{transform:translateX(46px)}to{transform:translateX(0)}}
.ob-moon{position:absolute;right:9%;top:13%;width:50px;height:50px;border-radius:50%;background:#F6F3FF;box-shadow:inset -11px -7px 0 0 #CDC5F4,0 0 26px rgba(246,243,255,.45)}
.ob-star{position:absolute;width:6px;height:6px;border-radius:50%;background:#fff;animation:obTwinkle 3s ease-in-out var(--delay,0s) infinite}
.ob-star-sm{width:4px;height:4px}
@keyframes obTwinkle{0%,100%{opacity:.25;transform:scale(.8)}50%{opacity:.95;transform:scale(1.15)}}
.ob-earth{position:absolute;inset:0;pointer-events:none}
.ob-globe{position:absolute;left:50%;top:calc(100% - 250px);transform:translateX(-50%);width:1400px;height:1400px;border-radius:50%;overflow:hidden;background:linear-gradient(100deg,#62C4FF 0%,#41A8F2 42%,#2E63C4 66%,#233B93 100%);box-shadow:inset 0 26px 60px rgba(255,255,255,.28),inset 0 -40px 80px rgba(10,20,60,.3),0 -14px 70px rgba(25,45,130,.4)}
.ob-globe-track{position:absolute;top:0;left:0;width:200%;height:100%;animation:obSpin 90s linear infinite}
@keyframes obSpin{to{transform:translateX(-50%)}}
.ob-land{position:absolute;background:#93DFA0}
.oll0{width:300px;height:110px;border-radius:58% 42% 55% 45%/60% 55% 45% 40%}
.oll1{width:210px;height:84px;border-radius:45% 55% 60% 40%/50% 60% 40% 50%;background:#A8E6B2}
.oll2{width:150px;height:64px;border-radius:50% 50% 45% 55%/55% 45% 55% 45%;background:#F1DFA0}
.oll3{width:260px;height:96px;border-radius:52% 48% 44% 56%/58% 46% 54% 42%}
.ob-globe-night{position:absolute;inset:0;background:linear-gradient(100deg,transparent 42%,rgba(18,28,78,0) 50%,rgba(18,28,78,.5) 64%,rgba(13,20,60,.72) 100%)}
.ob-globe-shine{position:absolute;inset:0;border-radius:50%;background:radial-gradient(circle at 38% 8%,rgba(255,255,255,.32),rgba(255,255,255,0) 42%)}
.ob-face{position:absolute;left:50%;transform:translateX(-50%);bottom:158px;width:76px;text-align:center}
.ob-eye{display:inline-block;width:21px;height:21px;background:#fff;border-radius:50%;margin:0 4px;box-shadow:0 2px 5px rgba(20,40,90,.2);text-align:center;line-height:21px}
.ob-eye i{display:inline-block;width:9px;height:9px;background:#22314F;border-radius:50%;vertical-align:middle;animation:obBlink 5.5s ease-in-out infinite}
@keyframes obBlink{0%,91%,100%{transform:scaleY(1)}94%{transform:scaleY(.12)}}
.ob-smile{display:block;width:18px;height:9px;border-bottom:3.5px solid #22314F;border-radius:0 0 14px 14px;margin:3px auto 0}
.ob-city{position:absolute;width:5px;height:5px;border-radius:50%;background:#FFDF8E;box-shadow:0 0 9px 3px rgba(255,223,142,.7);animation:obTwinkle 2.8s ease-in-out var(--delay,0s) infinite}
.ob-gpin{position:absolute;filter:drop-shadow(0 4px 10px rgba(20,40,90,.3));animation:obBob var(--dur,5s) ease-in-out var(--delay,0s) infinite alternate}
@keyframes obBob{from{transform:translateY(0)}to{transform:translateY(-7px)}}
.obg1{left:40%;bottom:216px;--dur:4.6s}
.obg2{left:31%;bottom:190px;--dur:5.4s;--delay:-2s}
.obg3{left:62%;bottom:200px;--dur:5s;--delay:-1s}
.ob-wave{position:absolute;left:0;right:0;bottom:-1px;width:100%;height:64px;pointer-events:none}
.ob-wave path{fill:var(--c-bg-alt)}
@media (prefers-reduced-motion: reduce){.ob-globe-track,.ob-cloud,.ob-star,.ob-city,.ob-gpin,.ob-eye i{animation:none}.ob-sky{transform:none;transition:none}}
.ob-example-card{border:1.5px dashed var(--c-line);border-radius:20px;overflow:hidden;background:var(--c-bg)}
.ob-example-badge{position:absolute;top:8px;left:8px;font-size:11px;font-weight:800;padding:4px 9px;border-radius:20px;background:rgba(23,23,27,.55);color:#fff;line-height:1.2}
.obsec{max-width:1120px;width:100%;margin:0 auto;padding:60px 32px 0;box-sizing:border-box}
.obsec-title{font-size:23px;font-weight:800;margin:0 0 8px;letter-spacing:-.01em;word-break:keep-all}
.obsec-sub{font-size:14px;color:var(--c-sub);margin:0 0 22px}
.ob-cats{display:grid;grid-template-columns:repeat(4,1fr);gap:14px}
.ob-cat{display:flex;flex-direction:column;align-items:flex-start;gap:10px;padding:18px;border:none;border-radius:18px;cursor:pointer;font-family:inherit;text-align:left;transition:transform .18s ease,box-shadow .18s ease}
.ob-cat:hover{transform:translateY(-3px);box-shadow:0 10px 24px rgba(30,50,140,.12)}
.ob-cat:active{transform:translateY(-1px) scale(.98)}
.ob-cat b{font-size:14.5px}
.ob-steps{display:grid;grid-template-columns:repeat(3,1fr);gap:16px}
.ob-step{padding:20px;border:1px solid var(--c-line);border-radius:18px;background:var(--c-bg);height:100%;box-sizing:border-box}
.ob-stepnum{display:flex;width:34px;height:34px;border-radius:50%;background:var(--c-primary);color:#fff;font-weight:800;font-size:15px;align-items:center;justify-content:center;margin-bottom:12px}
.ob-personas{display:flex;flex-direction:column;gap:12px;max-width:760px}
.ob-persona{display:flex;gap:16px;align-items:center;padding:16px 18px;border-radius:18px;background:var(--c-bg);border:1px solid var(--c-line)}
.ob-persona .pic{width:52px;height:52px;border-radius:16px;display:flex;align-items:center;justify-content:center;flex-shrink:0}
.ob-safety{display:grid;grid-template-columns:1fr 1.25fr;gap:30px;align-items:start}
.ob-safety-item{display:flex;gap:12px;align-items:flex-start;padding:14px 16px;border-radius:14px;background:var(--c-bg);border:1px solid var(--c-line);margin-bottom:10px}
.ob-faq{border:1px solid var(--c-line);border-radius:14px;background:var(--c-bg);margin-bottom:10px}
.ob-faq summary{list-style:none;cursor:pointer;padding:15px 18px;font-weight:700;font-size:14.5px;display:flex;justify-content:space-between;align-items:center;gap:10px}
.ob-faq summary::-webkit-details-marker{display:none}
.ob-faq[open] summary{color:var(--c-primary)}
.ob-faq p{margin:0;padding:0 18px 15px;font-size:13.5px;color:var(--c-sub);line-height:1.65}
.ob-cta-band{position:relative;overflow:hidden;max-width:1120px;margin:64px auto 0;border-radius:28px;padding:56px 32px;text-align:center;background:linear-gradient(120deg,#6FA6FF,#4E7DF7 55%,#3D5BD9);color:#fff;box-sizing:border-box}
.ob-cta-pin{position:absolute;right:4%;bottom:14%;animation:obBob 5s ease-in-out infinite alternate}
@media (max-width:860px){.ob-cats{grid-template-columns:repeat(2,1fr)}.ob-steps{grid-template-columns:1fr}.ob-safety{grid-template-columns:1fr;gap:16px}}
/* 스크롤 등장 애니메이션(Reveal 컴포넌트) — .in 클래스가 붙으면 등장. fx-* 변형은 .in 규칙보다 앞에 있어야 함 */
.revcard{opacity:0;transform:translateY(26px);transition:opacity .65s cubic-bezier(.22,.61,.36,1),transform .65s cubic-bezier(.22,.61,.36,1)}
.revcard.fx-pop{transform:translateY(14px) scale(.94)}
.revcard.fx-left{transform:translateX(-24px)}
.revcard.fx-right{transform:translateX(24px)}
.revcard.in{opacity:1;transform:none}
.revcard>.ob-cat{width:100%;height:100%;box-sizing:border-box}
.wgrid>.revcard{display:flex;flex-direction:column;min-width:0}
.wgrid>.revcard>*{flex:1}
@media (max-width:560px){.revcard.fx-left,.revcard.fx-right{transform:translateY(20px)}.revcard.in{transform:none}}
@media (prefers-reduced-motion:reduce){.revcard{opacity:1;transform:none;transition:none}}
/* 팝업 등장 애니메이션(2026-07-30 사용자 원칙: 상호작용은 부드럽고 명확하게) */
@keyframes mmOvIn{from{opacity:0}to{opacity:1}}
@keyframes mmShIn{from{opacity:0;transform:translateY(14px) scale(.98)}to{opacity:1;transform:none}}
.overlay{animation:mmOvIn .16s ease-out}
.overlay .sheet{animation:mmShIn .22s cubic-bezier(.22,.61,.36,1)}
@media (prefers-reduced-motion:reduce){.overlay,.overlay .sheet{animation:none}}
.home-hero{position:relative;overflow:hidden;border-radius:24px;padding:26px 28px;margin-bottom:14px;background:linear-gradient(115deg,#E9F3FF 0%,#EDF0FF 55%,#FFF3E6 100%)}
.home-hero-deco{position:absolute;inset:0;pointer-events:none}
.hh-sun{position:absolute;right:26px;top:22px;width:38px;height:38px;border-radius:50%;background:#FFD166;box-shadow:0 0 0 8px rgba(255,209,102,.22)}
.hh-cloud{position:absolute;right:88px;top:46px;width:52px;height:13px;border-radius:999px;background:#fff}
.hh-cloud::before{content:'';position:absolute;left:22%;top:-8px;width:17px;height:17px;border-radius:50%;background:#fff}
.hh-pin{position:absolute;filter:drop-shadow(0 3px 6px rgba(30,50,120,.18));animation:obBob var(--dur,5s) ease-in-out var(--delay,0s) infinite alternate}
.hhp1{right:168px;top:26px;--dur:4.8s}
.hhp2{right:140px;top:56px;--dur:5.6s;--delay:-2s}
.home-cats{display:flex;gap:10px;overflow-x:auto;padding:2px 2px 8px;margin-bottom:24px;scrollbar-width:none}
.home-cats::-webkit-scrollbar{display:none}
.home-cat{display:flex;align-items:center;gap:7px;padding:10px 15px;border:none;border-radius:999px;cursor:pointer;font-family:inherit;font-size:13px;font-weight:700;white-space:nowrap;flex-shrink:0;transition:transform .15s ease,box-shadow .15s ease}
.home-cat:hover{transform:translateY(-2px);box-shadow:0 6px 14px rgba(30,50,120,.12)}
.home-cat:active{transform:scale(.97)}
@media (prefers-reduced-motion: reduce){.hh-pin{animation:none}}
.auth-page{min-height:100vh;background:linear-gradient(160deg,#E9F3FF 0%,#F2F1FF 48%,#FFF3E8 100%)}
.auth-decor{position:fixed;inset:0;pointer-events:none;z-index:0}
.ad-sun{position:absolute;left:6%;top:10%;width:54px;height:54px;border-radius:50%;background:#FFD166;box-shadow:0 0 0 12px rgba(255,209,102,.16)}
.ad-cloud{position:absolute;left:11%;top:24%;width:62px;height:15px;border-radius:999px;background:#fff;opacity:.95}
.ad-cloud::before{content:'';position:absolute;left:22%;top:-9px;width:20px;height:20px;border-radius:50%;background:#fff}
.ad-pin{position:absolute;filter:drop-shadow(0 3px 8px rgba(30,50,120,.2));animation:obBob var(--dur,5s) ease-in-out var(--delay,0s) infinite alternate}
.adp1{right:8%;top:16%;--dur:4.8s}
.adp2{right:13%;top:34%;--dur:5.6s;--delay:-2s}
.auth-page .wmain{position:relative;z-index:1;background:#fff;border-radius:26px;box-shadow:0 14px 44px rgba(40,60,130,.10);padding:40px 36px 44px;margin:52px auto 68px;flex:none}
/* 인증 화면 폼 톤 — 입력창·버튼 스킨만(레이아웃·기능 불변). 필드가 인라인 스타일이라 !important 필요.
   주의: 오류 빨간 테두리(aria-invalid)와 비활성 버튼 색은 그대로 살아있어야 함 — 아래 순서 바꾸지 말 것 */
.auth-page input:not([type=checkbox]):not([type=radio]),.auth-page select,.auth-page textarea{border-radius:12px!important;background:#F8FBFF;transition:border-color .15s ease,box-shadow .15s ease,background .15s ease}
.auth-page input:not([type=checkbox]):not([type=radio]):focus,.auth-page select:focus,.auth-page textarea:focus{border-color:var(--c-primary)!important;background:#fff;box-shadow:0 0 0 3px rgba(0,102,255,.13)}
.auth-page input[aria-invalid=true]{border-color:var(--c-red)!important;background:#FFF9F9}
.auth-page input[aria-invalid=true]:focus{box-shadow:0 0 0 3px rgba(220,60,60,.12)}
.auth-page .dsbtn>div{border-radius:14px!important;padding-top:12px!important;padding-bottom:12px!important;transition:filter .15s ease,transform .12s ease}
.auth-page .dsbtn:not(.fit)>div{width:100%!important}
.auth-page .dsbtn:not(:disabled):hover>div{filter:brightness(1.05)}
.auth-page .dsbtn:not(:disabled):active>div{transform:scale(.985)}
@media (prefers-reduced-motion: reduce){.ad-pin{animation:none}}
@media (max-width:560px){
  .auth-page .wmain{margin:18px 14px 40px;padding:28px 20px 32px;border-radius:20px;width:auto}
  .ad-sun{width:36px;height:36px;left:4%;top:4%;box-shadow:0 0 0 8px rgba(255,209,102,.16)}
  .ad-cloud{display:none}
  .adp2{display:none}
  .adp1{right:6%;top:6%}
}
@media (max-width:560px){
  .home-hero{padding:20px 18px;border-radius:18px}
  .hh-sun{width:28px;height:28px;right:16px;top:16px;box-shadow:0 0 0 6px rgba(255,209,102,.22)}
  .hh-cloud{display:none}
  .hhp1{right:58px;top:20px}
  .hhp2{display:none}
}
.ob-nav{position:relative;z-index:30;display:flex;align-items:center;padding:22px 40px;min-width:0}
.ob-logo{display:flex;align-items:center;color:#fff;font-size:20px;font-weight:800;letter-spacing:-.02em;min-width:0;white-space:nowrap}
.ob-body{position:relative;z-index:10;flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;padding:24px 24px 190px;box-sizing:border-box;width:100%;min-width:0}
.ob-title{font-weight:800;color:#fff;font-size:44px;line-height:1.18;letter-spacing:-.02em;margin:0;max-width:780px;width:100%;text-shadow:0 2px 24px rgba(0,0,0,.3);word-break:keep-all;overflow-wrap:break-word}
.ob-desc{margin:20px 0 0;color:rgba(255,255,255,.86);font-size:16px;line-height:1.6;max-width:440px;width:100%;word-break:keep-all}
.ob-cta{display:flex;gap:10px;flex-wrap:wrap;align-items:center;justify-content:center;margin-top:28px;max-width:100%;width:100%;box-sizing:border-box}
.ob-cta button{white-space:nowrap;flex:0 0 auto;box-sizing:border-box;max-width:100%}
.ob-feature-row{max-width:1120px;margin:8px auto 56px;padding:0 32px;display:flex;gap:28px;flex-wrap:wrap;box-sizing:border-box;width:100%}
.ob-preview{max-width:1120px;margin:0 auto;padding:48px 32px 20px;width:100%;box-sizing:border-box;min-width:0}
.ob-feature{display:flex;gap:8px;min-width:0;flex:1 1 220px}
.ob-feature span{min-width:0;word-break:keep-all;font-size:13.5px;color:var(--c-sub)}
@media (max-width:900px){
  .ob-title{font-size:34px}
}
@media (max-width:560px){
  .ob-hero{min-height:auto;padding-bottom:150px}
  .ob-sun{width:44px;height:44px;left:-16px;top:13%;box-shadow:0 0 0 9px rgba(255,209,102,.16),0 0 32px 10px rgba(255,209,102,.3)}
  .ob-moon{width:38px;height:38px;right:-13px;top:12%;box-shadow:inset -8px -5px 0 0 #CDC5F4,0 0 16px rgba(246,243,255,.4)}
  .obc2{display:none}
  .obc1{display:none}
  .ob-globe{width:680px;top:calc(100% - 128px)}
  .oll0{width:150px;height:56px}
  .oll1{width:105px;height:42px}
  .oll2{width:76px;height:32px}
  .oll3{width:130px;height:48px}
  .ob-face{bottom:66px;left:50%;transform:translateX(-50%) scale(.84)}
  .ob-city{display:none}
  .ob-gpin{transform:scale(.85)}
  .obg1{left:63%;bottom:98px}
  .obg2{left:25%;bottom:84px}
  .obg3{display:none}
  .ob-wave{height:36px}
  .obsec{padding:44px 16px 0}
  .ob-cta-band{margin:44px 14px 0;padding:40px 20px;border-radius:22px;max-width:none}
  .ob-cta-pin{display:none}
  .ob-nav{padding:16px 16px}
  .ob-logo{font-size:15px}
  .ob-logo img{width:20px!important;height:20px!important}
  .ob-body{padding:20px 20px 32px}
  .ob-title{font-size:26px!important;line-height:1.28!important}
  .ob-desc{font-size:14.5px!important}
  .ob-cta{padding:0 4px;flex-direction:column;align-items:stretch}
  .ob-cta button{font-size:13px!important;padding:12px 16px!important;white-space:normal!important;text-align:center;width:100%!important;min-width:0!important;height:auto!important;min-height:48px!important;aspect-ratio:auto!important;flex:1 1 auto!important;border-radius:14px!important;display:flex!important;justify-content:center!important}
  .ob-feature-row{padding:0 20px;gap:16px;margin-bottom:36px}
  .ob-preview{padding:32px 16px 12px}
  .ob-preview .wgrid{grid-template-columns:1fr!important;gap:14px}
  .ob-feature{flex:1 1 100%}
}
.wnav{display:flex;align-items:center;gap:28px;padding:0 32px;height:64px;background:var(--c-bg);border-bottom:1px solid var(--c-line);position:sticky;top:0;z-index:10;flex-shrink:0}
.wbrand{font-family:var(--f-disp);font-weight:800;font-size:19px}
.wbrand b{color:var(--c-primary)}
.wlinks{display:flex;gap:22px;flex:1}
.wlink{background:none;border:none;font-family:inherit;font-size:14px;font-weight:600;color:var(--c-faint);cursor:pointer;padding:6px 0;border-bottom:2px solid transparent;transition:color .18s ease,border-color .18s ease}
.wlink:hover{color:var(--c-primary)}
.wlink.on{color:var(--c-text);border-color:var(--c-primary)}
.wmain{max-width:1120px;width:100%;margin:0 auto;padding:36px 32px 64px;flex:1}
.wgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:18px}
.itab{display:inline-flex;align-items:center;gap:0;padding:8px;border-radius:12px;border:none;background:none;cursor:pointer;color:var(--c-text);font-family:inherit;transition:gap .3s cubic-bezier(.4,0,.2,1),padding .3s cubic-bezier(.4,0,.2,1),background .2s ease}
.itab:hover{gap:6px;padding:8px 14px;background:var(--c-fill)}
.itab-label{max-width:0;opacity:0;overflow:hidden;white-space:nowrap;font-size:13px;font-weight:600;transition:max-width .3s cubic-bezier(.4,0,.2,1),opacity .2s ease .05s}
.itab:hover .itab-label{max-width:80px;opacity:1}
.mcard-media{position:relative;aspect-ratio:4/3;background:var(--c-fill);overflow:hidden}
.mcard-media img{width:100%;height:100%;object-fit:cover;display:block}
.mcard-save{position:absolute;top:8px;right:8px;width:44px;height:44px;border-radius:50%;background:rgba(23,23,27,.42);border:none;display:flex;align-items:center;justify-content:center;cursor:pointer;padding:0}
.mcard-save:hover{background:rgba(23,23,27,.6)}
.mcard-badge{position:absolute;top:8px;left:8px;font-size:11px;font-weight:800;padding:4px 9px;border-radius:20px;color:#fff;line-height:1.2}
.mcard-badge.primary{background:var(--c-primary)}
.mcard-body{padding:10px 12px 12px}
.mcard-title{font-weight:700;font-size:14.5px;margin:6px 0 3px;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;line-height:1.32;min-height:2.64em}
.mcard{border:1px solid var(--c-line);border-radius:20px;overflow:hidden;background:var(--c-bg);cursor:pointer;text-align:left;padding:0;font-family:inherit;width:100%;transition:box-shadow .15s,transform .15s}
.mcard:hover{box-shadow:0 8px 24px rgba(0,0,0,.09);transform:translateY(-2px)}
.wcard{border:1px solid var(--c-line);border-radius:16px;overflow:hidden;background:var(--c-bg);cursor:pointer;text-align:left;padding:0;font-family:inherit;transition:box-shadow .15s,transform .15s}
.wcard:hover{box-shadow:0 8px 24px rgba(0,0,0,.09);transform:translateY(-2px)}
.wsect{display:flex;align-items:baseline;justify-content:space-between;margin:36px 0 16px}
.wsect:first-child{margin-top:0}
.wsect h2{font-size:19px;font-weight:800;margin:0;display:flex;align-items:center;gap:8px}
.wsect .more{font-size:13px;color:var(--c-faint);background:none;border:none;cursor:pointer;font-family:inherit}
.wtwo{display:grid;grid-template-columns:1.4fr 1fr;gap:32px;align-items:start}
.wside{position:sticky;top:88px;display:flex;flex-direction:column;gap:14px}
.wpanel{border:1px solid #E3EAF6;border-radius:18px;background:linear-gradient(180deg,#FAFCFF,#FFFFFF 70%);padding:20px;box-shadow:0 6px 18px rgba(40,60,130,.05)}
/* 상세 페이지 카테고리 배너 — 장식 전용(상세에 이미지가 전무해서 무드를 부여). 사진(hostPhoto) 있으면 사진 우선 */
.detail-hero{position:relative;overflow:hidden;border-radius:20px;height:150px;display:flex;align-items:center;justify-content:center;margin-bottom:18px}
.detail-hero img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
.detail-hero .dh-puck{position:relative;width:74px;height:74px;border-radius:50%;background:#fff;display:flex;align-items:center;justify-content:center;box-shadow:0 8px 22px rgba(40,60,130,.14)}
.dh-deco{position:absolute;border-radius:999px;background:rgba(255,255,255,.45)}
.dh-c1{width:120px;height:120px;left:-34px;top:-48px}
.dh-c2{width:72px;height:24px;right:13%;top:24px;transform:rotate(-16deg)}
.dh-c3{width:150px;height:150px;right:-44px;bottom:-74px;background:rgba(255,255,255,.3)}
.dh-c4{width:36px;height:13px;left:17%;bottom:28px;transform:rotate(12deg)}
.dh-pin{position:absolute;right:26%;bottom:16%;filter:drop-shadow(0 3px 6px rgba(30,50,120,.18))}
@media (max-width:560px){.detail-hero{height:112px;border-radius:16px}.detail-hero .dh-puck{width:60px;height:60px}}
.wback{background:none;border:none;color:var(--c-faint);font-family:inherit;font-size:13px;cursor:pointer;display:flex;align-items:center;gap:4px;padding:0 0 14px}
.wchat{display:grid;grid-template-columns:1fr 320px;gap:0;border:1px solid var(--c-line);border-radius:16px;overflow:hidden;background:var(--c-bg);height:600px}
.wchatmain{display:flex;flex-direction:column;min-width:0;min-height:0}
.wchatlist{padding:20px;overflow-y:auto;flex:1}
.wchatinput{border-top:1px solid var(--c-line);padding:14px 20px;display:flex;gap:10px;align-items:center}
.wchatside{border-left:1px solid var(--c-line);padding:18px;overflow-y:auto;background:var(--c-bg-alt)}
.wtabs{display:flex;gap:24px;border-bottom:1px solid var(--c-line);margin-bottom:20px}
.wexplorewrap{display:grid;grid-template-columns:320px 1fr;gap:24px}
.hscroll{display:flex;align-items:center;gap:12px;overflow-x:auto;scrollbar-width:none}
.hscroll::-webkit-scrollbar{display:none}
.megamenu{position:absolute;top:100%;left:0;background:var(--c-bg);border:1px solid var(--c-line);border-radius:16px;box-shadow:0 16px 32px rgba(0,0,0,.10);padding:8px;min-width:220px;transform-origin:top left;animation:megain .22s cubic-bezier(.2,.8,.2,1)}
@keyframes megain{from{opacity:0;transform:translateY(-8px) scaleY(.96)}to{opacity:1;transform:none}}
.megalink{background:none;border:none;font-family:inherit;cursor:pointer;text-align:left;padding:11px 14px;border-radius:10px;font-size:13.5px;font-weight:600;color:var(--c-text);display:flex;align-items:center;gap:8px;width:100%;transition:background .15s ease,padding-left .15s ease}
.megalink:hover{background:var(--c-fill);padding-left:18px}
.wlink:focus-visible,.megalink:focus-visible,.itab:focus-visible,.mcard:focus-visible,.mcard-save:focus-visible{outline:2px solid var(--c-primary);outline-offset:2px}
.mobiletabbar{display:none}
.profile-stats{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:20px}
.profile-photo-form{grid-template-columns:200px 1fr}
.profile-menu-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:12px;margin-bottom:20px}
.profile-menu-item{display:flex;align-items:center;gap:12px;cursor:pointer;font-family:inherit;text-align:left;min-height:44px}
.profile-menu-item b{word-break:keep-all}
.profile-menu-text{flex:1;min-width:0}
.profile-save-wrap{margin-bottom:8px}
@media (max-width:860px){
  .wgrid{grid-template-columns:1fr !important; gap:14px}
}
@media (max-width:680px){
  .wnav{padding:0 14px; gap:10px}
  .wlinks{display:none}
  .wmain{padding:16px 14px 84px; max-width:100%}
  .wtwo{grid-template-columns:1fr}
  .wside{position:static}
  .wchat{grid-template-columns:1fr; height:auto; max-height:none}
  .wchatmain{height:70vh; min-height:360px}
  .wchatside{border-left:none; border-top:1px solid var(--c-line); max-height:220px}
  .card,.mcard,.wcard{max-width:100%}
  .qa-gear{opacity:.6;transform:scale(.85)}
  .qa-gear:hover,.qa-gear:focus{opacity:1;transform:none}
  .profile-stats{grid-template-columns:repeat(2,1fr)}
  .profile-menu-grid{grid-template-columns:1fr}
  .profile-save-wrap{margin-bottom:calc(64px + env(safe-area-inset-bottom,0px))}
  .mobiletabbar{display:flex; position:fixed; left:0; right:0; bottom:0; z-index:40; background:var(--c-bg); border-top:1px solid var(--c-line); padding:4px 2px env(safe-area-inset-bottom,4px)}
  .mtab{flex:1; display:flex; flex-direction:column; align-items:center; gap:2px; background:none; border:none; font-family:inherit; font-size:10.5px; font-weight:600; color:var(--c-faint); padding:6px 2px; min-height:48px; cursor:pointer}
  .mtab.on{color:var(--c-primary)}
}
`;
function injectWebCss(){ if(!document.getElementById('__wcss')){ const s=document.createElement('style'); s.id='__wcss'; s.textContent=WEBCSS; document.head.appendChild(s);} }
// 데모 세션 저장 — 비밀번호류는 절대 저장하지 않음. 새로고침해도 가입·참가 상태가 남도록만 함(진짜 백엔드 대체 아님).
// 계정별 상태 분리: 로그인 이메일(정규화)을 키로 세션을 별도 저장해 계정 전환 시 이전 계정 상태가 새 계정에 노출되지 않게 함.
const ACTIVE_ACCOUNT_KEY = nsKey('mm_active_account_v1');
function normalizeAccountId(email) { const e=(email||'').trim().toLowerCase(); return e || 'guest'; }
function accountSessionKey(id) { return nsKey('mm_session_v2:'+normalizeAccountId(id)); }
function getActiveAccountId() { try { return localStorage.getItem(ACTIVE_ACCOUNT_KEY) || 'guest'; } catch(e) { return 'guest'; } }
function setActiveAccountId(id) { try { localStorage.setItem(ACTIVE_ACCOUNT_KEY, normalizeAccountId(id)); } catch(e) {} }
const SESSION_EPHEMERAL = ['toast','rulesOpen','legalOpen','reportOpen','hostCompleteIdx','reviewPopup','reconfirmOpen','locPromptOpen','composeOpen','acctEdit','regionEdit','forceActivityPicker','cancelRegisterConfirm','cancelConfirmId','avoidUserConfirm','isAdmin','adminPasswordInput','withdrawConfirmOpen','myReportsOpen','authChoiceOpen'];
// 브라우저 URL·기록 연동 — 인증 흐름 화면(온보딩~가입완료 전) 판별용(가입 흐름 중 already-authenticated 리다이렉트에 사용)
const AUTH_FLOW_SCREENS = new Set(['onboarding','signup','register','emailverify','resetpassword','consent','setup','schoolemail','schooldocs','admin']);
// 뒤로가기·새로고침 시에도 진짜 회원 전용으로 막아야 하는 화면만 나열(그 외엔 비회원도 자유롭게 둘러볼 수 있어야 함 — "비회원으로 둘러보기" 기능과 충돌하지 않도록).
// 화면 내부에서 이미 "로그인이 필요해요" 등으로 우아하게 처리하는 화면(profile, chat, savedmeetups 등)은 여기 넣지 않는다.
const PRIVATE_SCREENS = new Set(['mymeet','mymeetupcoming','mymeetpast','mymeethosted','createmeetup','attendees']);
function isAuthRoutable(screen, S) { return S.authStatus==='authenticated' || !PRIVATE_SCREENS.has(screen); }
// 해시 쿼리 파싱/직렬화 — 필터·탭 URL 반영에 공용으로 사용
function parseHashQuery(hash) {
  const qIdx = (hash||'').indexOf('?');
  try { return new URLSearchParams(qIdx===-1 ? '' : hash.slice(qIdx+1)); } catch(e) { return new URLSearchParams(); }
}
function hashPathOnly(hash) { const qIdx=(hash||'').indexOf('?'); return qIdx===-1?(hash||''):hash.slice(0,qIdx); }
// screen 하나에 딸린 쿼리 상태를 동기화 — 기본값 생략, 변경 시 push(중복 방지). skipRef로 popstate發 갱신 시 재-push 방지.
function syncQuery(basePath, paramsObj, skipRef) {
  if (skipRef && skipRef.current) { skipRef.current = false; return; }
  const usp = new URLSearchParams();
  Object.entries(paramsObj).forEach(([k,v])=>{ if (v!==undefined && v!==null && v!=='' && v!==false) usp.set(k, v); });
  const qs = usp.toString();
  const next = basePath + (qs?('?'+qs):'');
  if (next !== location.hash) { try { history.pushState({screen:hashPathOnly(next).replace(/^#\//,'')}, '', next); } catch(e) {} }
}
function screenPath(screen, S) {
  if (screen==='detail') return '#/meetup/'+encodeURIComponent(S.detailId||'');
  if (screen==='chat') return '#/meetup/'+encodeURIComponent(S.detailId||'')+'/chat';
  if (screen==='attendees') return '#/meetup/'+encodeURIComponent(S.detailId||'')+'/attendees';
  if (screen==='eventdetail') return '#/events/'+encodeURIComponent(S.detailId||'');
  if (screen==='eventchat') return '#/events/'+encodeURIComponent(S.detailId||'')+'/chat';
  if (screen==='postdetail') return '#/qna/'+encodeURIComponent((S.qnaDetailPost&&typeof S.qnaDetailPost==='object'&&S.qnaDetailPost.id)||''); // 게시물 객체가 아니라 id만 주소에 싣는다(흰 화면 버그 수정)
  if (screen==='onboarding') return '#/';
  return '#/'+screen;
}
// 해시 라우트를 {screen, patch}로 역변환. 알 수 없는 경로는 null.
function pathToScreen(hash) {
  const raw = (hash||'').replace(/^#/, '').split('?')[0];
  const parts = raw.split('/').filter(Boolean);
  if (parts.length===0) return {screen:'onboarding'};
  if (parts[0]==='meetup' && parts[1]) {
    if (parts[2]==='chat') return {screen:'chat', patch:{detailId:decodeURIComponent(parts[1])}};
    if (parts[2]==='attendees') return {screen:'attendees', patch:{detailId:decodeURIComponent(parts[1])}};
    return {screen:'detail', patch:{detailId:decodeURIComponent(parts[1])}};
  }
  if (parts[0]==='events' && parts[1] && parts[2]==='chat') return {screen:'eventchat', patch:{detailId:decodeURIComponent(parts[1])}};
  if (parts[0]==='events' && parts[1]) return {screen:'eventdetail', patch:{detailId:decodeURIComponent(parts[1])}};
  if (parts[0]==='qna' && parts[1]) return {screen:'postdetail', patch:{qnaDetailPostId:decodeURIComponent(parts[1])}}; // id만 넘기고 게시물 본문은 화면에서 서버 조회
  return SCREENS_ROUTABLE.has(parts[0]) ? {screen:parts[0]} : null;
}
// SCREENS 맵(web-main.jsx)이 정의된 뒤에 채워짐 — 순환 의존 없이 안전하게 지연 초기화
const SCREENS_ROUTABLE = new Set(['onboarding','signup','register','emailverify','resetpassword','consent','setup','schoolemail','schooldocs','home','homecur','homeabroad','homesnapshot','hometimeline','explore','region','empty','browseregions','interestregion','events','eventslist','eventshighlights','mymeet','mymeetupcoming','mymeetpast','mymeethosted','qna','seoulall','profile','createmeetup','privacyavoid','savedmeetups','admin']);
const SESSION_SECRET = ['signupPassword','pwCurrent','pwNew','pwConfirm','schoolCode','emailCode'];
function loadSession(BASE) {
  try {
    const id = getActiveAccountId();
    let raw = localStorage.getItem(accountSessionKey(id));
    if (!raw && id==='guest') {
      // 구버전(계정 분리 이전) 전역 세션 키 1회 마이그레이션
      const legacy = localStorage.getItem(nsKey('modumoyeo_demo_session_v1'));
      if (legacy) { raw = legacy; try { localStorage.setItem(accountSessionKey('guest'), legacy); } catch(e) {} }
    }
    if (!raw) return null;
    const saved = JSON.parse(raw);
    return {...BASE, ...saved};
  } catch(e) { return null; }
}
function saveSession(S) {
  try {
    const id = getActiveAccountId(); // 활성 계정 포인터 기준으로만 저장 — 로그인 폼 입력 중 signupEmail 변경으로 다른 계정 데이터가 덮어써지는 것을 방지
    const copy = {...S};
    SESSION_EPHEMERAL.forEach(k=>delete copy[k]);
    SESSION_SECRET.forEach(k=>delete copy[k]);
    localStorage.setItem(accountSessionKey(id), JSON.stringify(copy));
  } catch(e) {}
}
// 인증 관련 모달 공용 접근성 훅 — 포커스 이동·Tab 트랩·Esc·배경 스크롤 잠금·닫힘 시 포커스 복귀
function useModalA11y(open, onEscape) {
  const {useRef, useEffect} = React;
  const dialogRef = useRef(null);
  const previouslyFocused = useRef(null);
  useEffect(()=>{
    if (!open) return;
    previouslyFocused.current = document.activeElement;
    const dlg = dialogRef.current;
    const focusables = () => dlg ? [...dlg.querySelectorAll('button,[href],input,select,textarea,[tabindex]:not([tabindex="-1"])')].filter(el=>!el.disabled && el.offsetParent!==null) : [];
    const first = focusables()[0];
    (first||dlg) && (first||dlg).focus();
    const onKey = e => {
      if (e.key==='Escape') { if (onEscape) { e.preventDefault(); onEscape(); } return; }
      if (e.key==='Tab') {
        const f = focusables();
        if (!f.length) return;
        const idx = f.indexOf(document.activeElement);
        if (e.shiftKey && idx<=0) { e.preventDefault(); f[f.length-1].focus(); }
        else if (!e.shiftKey && idx===f.length-1) { e.preventDefault(); f[0].focus(); }
      }
    };
    document.addEventListener('keydown', onKey);
    const prevOverflow = document.body.style.overflow;
    document.body.style.overflow = 'hidden';
    return ()=>{ document.removeEventListener('keydown', onKey); document.body.style.overflow = prevOverflow; previouslyFocused.current && previouslyFocused.current.focus && previouslyFocused.current.focus(); };
  }, [open]);
  return dialogRef;
}
function switchAccount(BASE, currentS, email, extraPatch) {
  saveSession(currentS);
  const id = normalizeAccountId(email);
  setActiveAccountId(id);
  try { document.dispatchEvent(new CustomEvent('mm:accountchanged')); } catch(e) {} // 계정 전환 직후 찜 등 계정별 UI가 즉시 재조회하도록
  let loaded = null;
  try { const raw = localStorage.getItem(accountSessionKey(id)); if (raw) loaded = {...BASE, ...JSON.parse(raw)}; } catch(e) {}
  return {...(loaded || {...BASE}), ...extraPatch, signupEmail: email};
}
// logoutAccount — 현재 세션만 종료(게스트로 전환), 계정 저장 데이터는 삭제하지 않음
function logoutAccount(BASE) {
  setActiveAccountId('guest');
  try { document.dispatchEvent(new CustomEvent('mm:accountchanged')); } catch(e) {}
  return {...BASE};
}
// 새 모임 개설 시작 — 로그인 확인 후, 이미 진행 중인(완료되지 않은) 내 모임이 있으면 HostLimitModal로 안내하고 새로 만들지 못하게 막음.
// 항상 빈 draft로 시작해서 이전에 쓰다 만 초안이 새 개설 화면에 남아있지 않게 함.
function startCreateMeetup(S, set, go, patch) {
  guardAuth(S, set, go, () => {
    const hasActive = (S.myHostedMeetups||[]).some(hm=>!hm.done);
    if (hasActive && !isAdminUser(S)) { set({hostLimitNoticeOpen:true}); return; } // 운영진은 동시 개설 무제한(2026-07-30 사용자 지시)
    go('createmeetup', {editingMeetupIdx:null, newMeetup:{title:'', when:'', date:'', time:'', cap:'6', place:'', purpose:'', purposes:[], mood:'', desc:''}, ...(patch||{})});
  });
}
// 회원가입·로그인 계열 화면 공용 배경 장식 — 랜딩과 같은 톤(해·구름·핀), 클릭 방해 없음
function AuthDecor(){
  return <div className="auth-decor" aria-hidden="true">
    <span className="ad-sun"></span>
    <span className="ad-cloud"></span>
    <svg className="ad-pin adp1" 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="#FF8A5B"/><circle cx="12" cy="12" r="5" fill="#fff"/></svg>
    <svg className="ad-pin adp2" 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="#8ED9C6"/><circle cx="12" cy="12" r="5" fill="#fff"/></svg>
  </div>;
}
function scrollToAnchor(id){
  requestAnimationFrame(()=>requestAnimationFrame(()=>{
    const el=document.getElementById(id);
    const wrap=el&&el.closest('.wapp');
    if(el&&wrap) wrap.scrollTop = el.getBoundingClientRect().top - wrap.getBoundingClientRect().top + wrap.scrollTop - 84;
  }));
}
function MobileTabBar({S,go}){
  const items=[['home','홈','IconNormalHomeNameHomeFillFalse'],['browseregions','모임찾기','IconNormalSearchNameSearchThickFalse'],['explore','커뮤니티','IconNormalBubbleNameBubbleFillFalse'],['mymeet','내모임','IconNormalPersonsNamePersonsFillFalse'],['profile','내정보','IconNavigationMyPageNameNavigationMypage']];
  return <div className="mobiletabbar">
    {items.map(it=><button key={it[0]} className={'mtab'+(S.screen===it[0]?' on':'')} onClick={()=>go(it[0])}><Ic n={it[2]} s={20}/>{it[1]}</button>)}
  </div>;
}
// 정지 계정 안내 배너(23단계) — 서버가 실제 차단, 화면은 사유·기간·문의 경로를 상시 노출
function SuspensionBanner({S, set}) {
  const sn = S.sanctionActive;
  if (!sn) return null;
  const until = sn.expires_at ? new Date(sn.expires_at).toLocaleString('ko-KR') : null;
  return <div role="alert" style={{background:'#FDECEC', borderBottom:'1px solid var(--c-red)', color:'var(--c-red)', padding:'10px 16px', fontSize:13, lineHeight:1.6, textAlign:'center'}}>
    <b>이용이 제한된 계정이에요.</b> {until?`${until}까지`:'영구적으로'} 모임 개설·참가·글·채팅이 제한돼요. 사유: {sn.reason||'커뮤니티 이용수칙 위반'}. 잘못된 조치라면 <a href="mailto:support@modumoyeo.com" style={{color:'var(--c-red)', textDecoration:'underline'}}>support@modumoyeo.com</a> 으로 문의해주세요.
  </div>;
}
function WNav({S,go,set}){
  const items=[['home','홈'],['browseregions','모임 찾기'],['explore','커뮤니티'],['mymeet','내 모임'],['events','이벤트'], ...(isAdminUser(S)?[['admin','관리자']]:[])];
  const {useState,useEffect,useRef}=React;
  const [hidden,setHidden]=useState(false);
  const [hoverTab,setHoverTab]=useState(null);
  const [pinned,setPinned]=useState(false);
  const closeTimer=useRef(null);
  const ref=useRef(null);
  useEffect(()=>{
    const wrap=ref.current&&ref.current.closest('.wapp');
    if(!wrap) return;
    let last=wrap.scrollTop;
    const onScroll=()=>{
      const y=wrap.scrollTop;
      if(y<40){ setHidden(false); } else if(y>last+4){ setHidden(true); } else if(y<last-4){ setHidden(false); }
      last=y;
    };
    wrap.addEventListener('scroll',onScroll,{passive:true});
    return ()=>wrap.removeEventListener('scroll',onScroll);
  },[]);
  useEffect(()=>{
    if(!hoverTab) return;
    const onKey=e=>{ if(e.key==='Escape'){ setHoverTab(null); setPinned(false); } };
    const onDocClick=e=>{ if(ref.current && !ref.current.contains(e.target)){ setHoverTab(null); setPinned(false); } };
    const onOtherMenu=()=>{ setHoverTab(null); setPinned(false); };
    document.addEventListener('keydown',onKey);
    document.addEventListener('mousedown',onDocClick);
    document.addEventListener('mm:menuopen',onOtherMenu);
    return ()=>{ document.removeEventListener('keydown',onKey); document.removeEventListener('mousedown',onDocClick); document.removeEventListener('mm:menuopen',onOtherMenu); };
  },[hoverTab]);
  const openMenu = k => { if(closeTimer.current) clearTimeout(closeTimer.current); document.dispatchEvent(new CustomEvent('mm:menuopen')); setHoverTab(k); };
  const scheduleClose = () => { if(pinned) return; closeTimer.current = setTimeout(()=>setHoverTab(null), 150); };
  const MEGA = {
    home:[['이번 주 모임', ()=>{go('homecur');setHoverTab(null);setPinned(false);}],['타지 모임', ()=>{go('homeabroad');setHoverTab(null);setPinned(false);}],['타임라인', ()=>{go('hometimeline');setHoverTab(null);setPinned(false);}]],
    browseregions:[['전체 지역 모임', ()=>{go('browseregions',{browseregionsType:'all'});setHoverTab(null);setPinned(false);}],['공식 이벤트', ()=>{go('browseregions',{browseregionsType:'official'});setHoverTab(null);setPinned(false);}],['모임 개설하기', ()=>{ setHoverTab(null);setPinned(false); startCreateMeetup(S,set,go); }]],
    mymeet:[['참가 예정', ()=>{go('mymeetupcoming');setHoverTab(null);setPinned(false);}],['참석 완료', ()=>{go('mymeetpast');setHoverTab(null);setPinned(false);}],['내가 개설한 모임', ()=>{go('mymeethosted');setHoverTab(null);setPinned(false);}],['모임 개설하기', ()=>{ setHoverTab(null);setPinned(false); startCreateMeetup(S,set,go); }]],
    qna:[['전체', ()=>{go('qna',{qtab:0});setHoverTab(null);setPinned(false);}],['해외거주 Q&A', ()=>{go('qna',{qtab:1});setHoverTab(null);setPinned(false);}],['친구만들기', ()=>{go('qna',{qtab:2});setHoverTab(null);setPinned(false);}],['귀국 적응', ()=>{go('qna',{qtab:3});setHoverTab(null);setPinned(false);}]],
  };
  return <>
  <SuspensionBanner S={S} set={set}/>
  <div ref={ref} className="wnav" style={{transform:hidden?'translateY(-100%)':'translateY(0)',transition:'transform .25s ease'}} onMouseLeave={scheduleClose}>
    <Brand go={go} size={16}/>
    <div className="wlinks" style={{justifyContent:'space-evenly'}}>{items.map(it=><div key={it[0]} className="row" style={{position:'relative',gap:0}} onMouseEnter={()=>MEGA[it[0]]&&openMenu(it[0])}>
      <button className={'wlink'+(S.screen===it[0]?' on':'')} style={{fontWeight:600}} aria-haspopup={!!MEGA[it[0]]} aria-expanded={hoverTab===it[0]} onClick={()=>go(it[0])} onKeyDown={e=>{ if(MEGA[it[0]] && e.key==='ArrowDown'){ e.preventDefault(); openMenu(it[0]); setPinned(true); const first=e.currentTarget.parentElement.querySelector('.megalink'); first&&first.focus(); } }}>{it[1]}</button>
      {MEGA[it[0]]&&<button aria-label={it[1]+' 하위 메뉴 열기'} aria-expanded={hoverTab===it[0]} style={{background:'none',border:'none',cursor:'pointer',padding:'6px 4px',minWidth:28,minHeight:44,display:'flex',alignItems:'center',justifyContent:'center',color:'var(--c-faint)'}} onClick={()=>{ if(hoverTab===it[0]&&pinned){ setHoverTab(null); setPinned(false); } else { document.dispatchEvent(new CustomEvent('mm:menuopen')); setHoverTab(it[0]); setPinned(true); } }}><Ic n="IconNormalChevronDownNameChevronDownThickFalseSmall" s={12}/></button>}
      {hoverTab===it[0]&&MEGA[it[0]]&&<div className="megamenu" role="menu" onMouseEnter={()=>openMenu(it[0])} onKeyDown={e=>{ if(e.key==='Escape'){ setHoverTab(null); setPinned(false); } else if(e.key==='ArrowDown'||e.key==='ArrowUp'){ e.preventDefault(); const links=[...e.currentTarget.querySelectorAll('.megalink')]; const idx=links.indexOf(document.activeElement); const next=e.key==='ArrowDown'?(idx+1)%links.length:(idx-1+links.length)%links.length; links[next]&&links[next].focus(); } }}>
        {MEGA[it[0]].map(([label,action])=><button key={label} className="megalink" role="menuitem" onClick={action}><Ic n="IconNormalChevronRightNameChevronRightTightFalseThick" s={11} c="var(--c-faint)"/>{label}</button>)}
      </div>}
    </div>)}</div>
    <Chip icon="IconNormalLocation2NameLocationFillFillTrue" label={countryOf(S.regionVals.cur).city} tone="primary" onClick={()=>set({locPromptOpen:true})}/>
    <div className="itab"><NotifBell go={go} set={set} S={S}/><span className="itab-label">알림</span></div>
    <button className="itab" onClick={()=>go('profile')} style={{borderRadius:20}}><Av name={S.authStatus==='authenticated'?'나':'?'} size={34}/><span className="itab-label">{S.authStatus==='authenticated'?'내정보':'로그인'}</span></button>
  </div>
  <MobileTabBar S={S} go={go}/>
  </>;
}
function DevPanel({S,set}){
  const {useState} = React;
  const [open, setOpen] = useState(false);
  if (!DEMO_MODE) return null;
  const PREAUTH = ['onboarding','signup','register','emailverify','consent','setup','schoolemail','schooldocs'];
  if (PREAUTH.includes(S.screen)) return null;
  const pendingMeetups = Object.keys(S.joined||{}).filter(id=>S.joined[id]==='pending'||S.joined[id]==='waitlisted');
  const pendingEvents = Object.keys(S.eventApplied||{}).filter(id=>S.eventApplied[id]==='pending'||S.eventApplied[id]==='waitlisted');
  if (!open) return <button className="qa-gear" onClick={()=>setOpen(true)} aria-label="프로토타입 제어판" style={{position:'fixed', right:16, bottom:16, zIndex:100, width:40, height:40, borderRadius:'50%', background:'var(--c-text)', color:'#fff', border:'none', cursor:'pointer', display:'flex', alignItems:'center', justifyContent:'center', opacity:.55}}><Ic n="IconNormalSettingNameSetting" s={18}/></button>;
  return <div style={{position:'fixed', right:16, bottom:16, zIndex:100, width:280, background:'var(--c-text)', color:'#fff', borderRadius:14, padding:'14px 16px', boxShadow:'0 12px 32px rgba(0,0,0,.3)'}}>
    <div className="row" style={{justifyContent:'space-between', marginBottom:10}}>
      <b style={{fontSize:12.5}}>프로토타입 데모 제어판</b>
      <button onClick={()=>setOpen(false)} style={{background:'none', border:'none', color:'#fff', cursor:'pointer', opacity:.7}}><Ic n="IconNormalCloseNameCloseThickFalse" s={14}/></button>
    </div>
    <p style={{fontSize:11, opacity:.6, margin:'0 0 10px', lineHeight:1.5}}>실제 서비스 화면에는 보이지 않는 시연용 상태 조작 도구예요.</p>
    {pendingMeetups.length===0&&pendingEvents.length===0?<p style={{fontSize:11.5, opacity:.6, margin:0}}>대기 중인 신청이 없어요.</p>:<div style={{display:'flex', flexDirection:'column', gap:6}}>
      {pendingMeetups.map(id=>{ const m=MEETUPS[id]; if(!m) return null; return <div key={id} className="row" style={{justifyContent:'space-between', gap:8}}>
        <span style={{fontSize:11.5, opacity:.85}}>{m.title}</span>
        <button onClick={()=>set({joined:{...S.joined,[id]:'confirmed'}, toast:'데모: 상태를 참가 확정으로 바꿨어요'})} style={{background:'rgba(255,255,255,.15)', border:'none', color:'#fff', borderRadius:6, padding:'4px 8px', fontSize:10.5, cursor:'pointer'}}>확정 처리</button>
      </div>; })}
      {pendingEvents.map(id=>{ const e=EVENTS.find(x=>x.id===id); if(!e) return null; return <div key={id} className="row" style={{justifyContent:'space-between', gap:8}}>
        <span style={{fontSize:11.5, opacity:.85}}>{e.title}</span>
        <button onClick={()=>set({eventApplied:{...S.eventApplied,[id]:'confirmed'}, toast:'데모: 상태를 참가 확정으로 바꿨어요'})} style={{background:'rgba(255,255,255,.15)', border:'none', color:'#fff', borderRadius:6, padding:'4px 8px', fontSize:10.5, cursor:'pointer'}}>확정 처리</button>
      </div>; })}
    </div>}
  </div>;
}
function Footer({go, set}) {
  const linkStyle = {background:'none', border:'none', color:'var(--c-faint)', fontFamily:'inherit', fontSize:12.5, cursor:'pointer', padding:0};
  return <footer style={{borderTop:'1px solid var(--c-line)', marginTop:48, padding:'32px 32px 28px', color:'var(--c-faint)', fontSize:12.5, lineHeight:1.7}}>
    <div style={{maxWidth:1120, margin:'0 auto'}}>
      <div className="row" style={{gap:8, marginBottom:10}}>
        <img src={asset('assets/modumoyeo-icon.png')} alt="" style={{width:24, height:24, borderRadius:6, flexShrink:0, objectFit:'cover'}} onError={e=>{e.target.style.display='none'; e.target.nextSibling.style.display='flex';}}/>
        <span style={{display:'none', width:24, height:24, borderRadius:6, flexShrink:0, background:'var(--c-primary)', color:'#fff', alignItems:'center', justifyContent:'center', fontFamily:'var(--f-disp)', fontWeight:800, fontSize:12}}>M</span>
        <span className="wbrand" style={{fontSize:15, color:'var(--c-text)'}}>modu<b>moyeo</b></span>
      </div>
      <div></div>
      <div style={{marginBottom:16}}>문의: <a href="mailto:support@modumoyeo.com" style={{color:'inherit'}}>support@modumoyeo.com</a></div>
      <div style={{display:'flex', gap:14, flexWrap:'wrap', marginBottom:14, alignItems:'center'}}>
        <button style={linkStyle} onClick={()=>set&&set({legalOpen:'terms'})}>이용약관</button>
        <button style={linkStyle} onClick={()=>set&&set({legalOpen:'privacy'})}>개인정보처리방침</button>
        <button style={linkStyle} onClick={()=>set&&set({legalOpen:'guide'})}>커뮤니티 운영정책</button>
        <button style={linkStyle} onClick={()=>set&&set({legalOpen:'safety'})}>안전수칙</button>
        <button style={linkStyle} onClick={()=>set&&set({reportOpen:true})}>신고하기</button>
        <button style={linkStyle} onClick={()=>set&&set({legalOpen:'faq'})}>자주 묻는 질문</button>
        <a href="mailto:support@modumoyeo.com" style={linkStyle}>문의하기</a>
        <a href="https://www.instagram.com/modu.moyeo/" target="_blank" rel="noopener noreferrer" style={{display:'inline-flex', alignItems:'center', gap:5, color:'var(--c-faint)', textDecoration:'none'}}><Ic n="IconNormalLogoInstagramNameLogoInstagram" s={14}/>Instagram</a>
      </div>
      <div style={{marginBottom:10}}>서비스명: 모두모여 · 운영자: 김정원 (개인) · 개인정보 보호책임자: 김정원 · 문의: support@modumoyeo.com</div>
      <div>© 2026 Modumoyeo. All rights reserved.</div>
    </div>
  </footer>;
}
// 웹 전용 이용수칙 모달 — 모바일 전용 파일(screens-b.jsx)에만 있던 RulesModal이 웹 번들에는
// 존재하지 않아 "전체 수칙 다시보기"·"참가 신청하기"를 누르면 컴포넌트를 찾지 못해 흰 화면으로
// 깨졌던 문제를 고치기 위해 추가한 웹 버전. 내용은 모바일판과 동일하게 유지.
function WRulesModal({S, set, go}) {
  const [applyErr, setApplyErr] = React.useState(''); // 쿨다운·실패 안내 — 토스트는 3초 만에 사라져 '무반응'으로 오해됨(실사례)
  const m = MEETUPS[S.detailId] || S.tempDetail || MEETUPS.m1;
  const rules = ['서로 존중해주세요. 나이·학교·국가와 상관없이 예의를 지켜주세요.','과도한 스킨십은 금지돼 있어요.','식사·음료 등 개인 비용은 각자 결제하는 게 기본이에요. (스폰서 모임 예외)','노쇼 시 다음 모임 신청이 제한될 수 있어요.','불편한 상황에는 신고하기 — 성희롱·폭언·영업·다단계·사기·스토킹·사진 무단 게시 등 유형별로 접수돼요.','모임 사진은 참가자 동의 없이 SNS에 올릴 수 없어요.'];
  return <div className="overlay" onClick={()=>set({rulesOpen:false})}>
    <div className="sheet" onClick={e=>e.stopPropagation()}>
      <h2 style={{fontSize:18, margin:'0 0 4px'}}>함께 즐거운 모임을 위해</h2>
      <div className="meta" style={{marginBottom:16}}>{m?.title} · {m?.when}</div>
      {rules.map((r,i)=><div key={i} className="rule"><span className="rulenum">{i+1}</span><span>{r}</span></div>)}
      <button style={{background:'none', border:'none', color:'var(--c-primary)', fontFamily:'inherit', fontSize:12.5, padding:'2px 0 14px', cursor:'pointer'}} onClick={()=>set({policyOpen:!S.policyOpen})}>{S.policyOpen?'안전정책 접기':'전체 안전정책 자세히 보기'}</button>
      {S.policyOpen&&<div style={{background:'var(--c-fill-a)', borderRadius:10, padding:'12px 14px', margin:'-4px 0 14px', fontSize:12.5, color:'var(--c-sub)', lineHeight:1.75}}>
        <b style={{display:'block', marginBottom:6, color:'var(--c-text)'}}>신뢰·안전 정책 (PRD 8장)</b>
        신고 유형 — 성희롱/폭언/영업·다단계/사기/스토킹/사진 무단 게시/기타로 분류되어 접수되고, 긴급 신고는 일반 불편 신고보다 우선 처리돼요.<br/>
        차단 시 상대에게 내 프로필과 참가 예정 모임이 보이지 않도록 처리돼요.<br/>
        신고 접수 후에는 임시 신청 제한이 검토되고, 채팅 기록은 신고 처리를 위한 최소 기간만 보관돼요.
      </div>}
      {!(m&&S.joined[m.id])&&<label className="row" style={{gap:8, marginBottom:10, cursor:'pointer', fontSize:13.5}}>
        <input type="checkbox" checked={S.ruleCheck||false} onChange={e=>set({ruleCheck:e.target.checked})} style={{width:18, height:18, accentColor:'var(--c-primary)'}}/>
        위 이용수칙에 동의하고 참가할게요
      </label>}
      {applyErr&&<div className="notice orange" role="alert" style={{marginBottom:12}}><Ic n="IconNormalCircleInfo2NameCircleInfoFillFalse" s={15}/><span>{applyErr}</span></div>}
      {(S.detailId+'').startsWith('my')||!m||S.joined[m.id]
        /* 이미 참가 중(확정·대기·승인대기)이면 읽기 전용 — 수칙 다시보기가 신청 버튼으로 둔갑해
           확정자를 대기로 강등시키던 사고(2026-07-29 실제 발생) 방지 */
        ?<DSBtn label={m&&S.joined[m.id]?'확인했어요':'닫기'} size="md" onClick={()=>set({rulesOpen:false})}/>
        :(()=>{ const full = effectiveJoined(m) >= m.capMax; return <DSBtn label={full?'대기 명단 확정하기':(m.approvalMode==='approval'?'참가 신청하기 (승인 대기)':'참가 확정하기')} size="md" disable={!S.ruleCheck} onClick={()=>{
            if (S.joined[m.id]) { set({rulesOpen:false, ruleCheck:false}); return; } // 이중 안전장치
            const status = full?'waitlisted':(m.approvalMode==='approval'?'pending':'confirmed');
            const applyLocal = () => {
              const patch = {rulesOpen:false, ruleCheck:false, joined:{...S.joined,[m.id]:status}};
              // 확정 신청이면 인원 숫자·게이지가 즉시 올라가게 로컬 카운트도 바로 반영(서버 재조회 전까지의 표시용)
              if (status==='confirmed') {
                m.capJoined = (m.capJoined||0)+1;
                if (S.tempDetail && S.tempDetail.id===m.id) patch.tempDetail = {...S.tempDetail, capJoined:m.capJoined};
              }
              if (full) {
                patch.waitlists = joinWaitlist(m.id, S);
                const pos = myWaitlistPosition(m.id, {waitlists:patch.waitlists}) || (patch.waitlists[m.id]||[]).length;
                patch.toast = `대기 ${pos}번째로 등록됐어요`;
              } else {
                patch.toast = status==='pending' ? '신청이 접수됐어요 — 모임장 승인을 기다려주세요' : '참가가 확정됐어요 — 채팅방이 열렸어요';
              }
              set(patch);
            };
            // 취소 페널티 사전 안내 — 서버 트리거가 최종 강제하지만, 이 기기에서 취소한 기록이 있으면 즉시 알려줌
            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));
              setApplyErr(`최근 참가를 취소해서 지금은 신청할 수 없어요 — 약 ${min}분 뒤에 다시 신청할 수 있어요 (취소 후 1시간 제한)`);
              return;
            }
            // 서버에 저장된 모임이면 서버 신청이 성공한 뒤에만 화면을 바꾼다(12단계 서버화)
            const email = S.acctEmail || S.signupEmail;
            if (isServerMeetupId(m.id) && email) {
              applyToMeetupServer(m.id, email, S.profile.nickname||'나', status).then(res=>{
                if (res && res.error) {
                  const msg = String((res.error&&res.error.message)||'');
                  if (msg.includes('MM_APPLY_COOLDOWN_LONG')) setApplyErr('최근 취소가 잦아 24시간 동안 새 모임 신청이 제한돼요.');
                  else if (msg.includes('MM_APPLY_COOLDOWN')) setApplyErr('참가를 취소한 지 1시간이 지나지 않아 아직 신청할 수 없어요.');
                  else if (msg.includes('MM_SUSPENDED')) setApplyErr('이용이 제한된 계정이에요 — 제한 기간에는 모임에 참가할 수 없어요. 문의: support@modumoyeo.com');
                  else if (msg.includes('MM_NOSHOW_BANNED')) setApplyErr('최근 30일 안에 노쇼가 2회 기록되어 2주간 새 모임 신청이 제한됐어요. 어떤 모임의 기록인지는 알림함에서 확인할 수 있고, 잘못 기록됐다면 운영진 문의로 알려주세요.');
                  else setApplyErr('신청에 실패했어요 — 잠시 후 다시 시도해주세요.');
                  return;
                }
                // 서버가 정원 초과로 자동 대기 전환한 경우 — 화면도 대기 상태로 반영(14단계)
                if (res && res.status === 'waitlisted' && status !== 'waitlisted') {
                  set({rulesOpen:false, ruleCheck:false, joined:{...S.joined,[m.id]:'waitlisted'}, waitlists:joinWaitlist(m.id, S), toast:'그 사이 정원이 가득 찼어요 — 대기 명단에 올려드렸어요. 자리가 나면 자동으로 확정돼요'});
                  return;
                }
                applyLocal();
              });
            } else applyLocal();
          }}/>; })()}
    </div>
  </div>;
}
// 모달 드래그 보호(앱 전체 공통) — 입력창에서 글자를 드래그하다 팝업 밖에서 마우스를 떼면 팝업이 닫히며
// 작성 중이던 내용이 날아가던 버그 방지. 브라우저의 click은 누른 지점과 뗀 지점의 공통 조상에서 발생하므로,
// "누르기 시작한 곳"이 오버레이 자신이 아니면 오버레이의 닫기 클릭을 여기서 차단한다.
(function(){
  if (window.__mmOverlayGuard) return; window.__mmOverlayGuard = true;
  let downTarget = null;
  document.addEventListener('mousedown', function(e){ downTarget = e.target; }, true);
  document.addEventListener('click', function(e){
    if (e.target && e.target.classList && e.target.classList.contains('overlay') && downTarget && downTarget !== e.target) e.stopPropagation();
  }, true);
})();
Object.assign(window, {injectWebCss, WNav, Footer, loadSession, saveSession, DevPanel, WRulesModal, startCreateMeetup, AuthDecor});
