// app.jsx — Second World main application

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

// ─── Auth helpers ───────────────────────────────────────────────────────────
function decodeJwtPayload(token) {
  const part = token.split('.')[1];
  if (!part) return null;
  const b64 = part.replace(/-/g, '+').replace(/_/g, '/');
  const json = decodeURIComponent(
    atob(b64).split('').map(c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)).join('')
  );
  return JSON.parse(json);
}
function findMemberByEmail(members, email) {
  if (!email) return null;
  const e = String(email).toLowerCase();
  return members.find(m => (m.email || '').toLowerCase() === e) || null;
}

// Resize an image File to a JPEG data URL (keeps localStorage small)
function fileToCompressedDataUrl(file, maxSize = 512, quality = 0.85) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = () => {
      const img = new Image();
      img.onload = () => {
        let { width, height } = img;
        const ratio = Math.min(maxSize / width, maxSize / height, 1);
        width  = Math.max(1, Math.round(width  * ratio));
        height = Math.max(1, Math.round(height * ratio));
        const canvas = document.createElement('canvas');
        canvas.width = width;
        canvas.height = height;
        canvas.getContext('2d').drawImage(img, 0, 0, width, height);
        try { resolve(canvas.toDataURL('image/jpeg', quality)); }
        catch (e) { reject(e); }
      };
      img.onerror = reject;
      img.src = reader.result;
    };
    reader.onerror = reject;
    reader.readAsDataURL(file);
  });
}

const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "theme": "dark",
  "language": "en",
  "accentHue": 268,
  "heroVariant": "planet"
}/*EDITMODE-END*/;

// ─── Helpers ────────────────────────────────────────────────────────────────
function useScroll() {
  const [y, setY] = useState(0);
  useEffect(() => {
    const onScroll = () => setY(window.scrollY);
    window.addEventListener('scroll', onScroll, { passive: true });
    return () => window.removeEventListener('scroll', onScroll);
  }, []);
  return y;
}

function useReveal(deps = []) {
  useEffect(() => {
    const io = new IntersectionObserver((entries) => {
      entries.forEach(e => { if (e.isIntersecting) e.target.classList.add('in'); });
    }, { threshold: 0.08, rootMargin: '0px 0px -10% 0px' });
    const attach = () => {
      document.querySelectorAll('.reveal:not(.in)').forEach(el => io.observe(el));
    };
    attach();
    // Also re-attach a tick later in case React just rendered new .reveal nodes
    const t = setTimeout(attach, 50);
    return () => { clearTimeout(t); io.disconnect(); };
  }, deps);
}

// ─── Nav ────────────────────────────────────────────────────────────────────
function UserMenu({ currentUser, lang, setView, onSignOut, toast }) {
  const [open, setOpen] = useState(false);
  const ref = useRef(null);

  useEffect(() => {
    if (!open) return;
    const onDocDown = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    const onEsc = (e) => { if (e.key === 'Escape') setOpen(false); };
    document.addEventListener('mousedown', onDocDown);
    document.addEventListener('keydown', onEsc);
    return () => {
      document.removeEventListener('mousedown', onDocDown);
      document.removeEventListener('keydown', onEsc);
    };
  }, [open]);

  const goSettings = () => {
    setOpen(false);
    if (currentUser?.role === 'admin') setView('admin');
    else toast(lang === 'en' ? 'Settings coming soon' : '설정 준비 중');
  };
  const goSignOut = () => { setOpen(false); onSignOut(); };

  return (
    <div ref={ref} className="user-menu-wrap">
      <button className="user-menu-btn"
              onClick={() => setOpen(o => !o)}
              aria-haspopup="menu" aria-expanded={open}
              title={lang === 'en' ? 'Menu' : '메뉴'}>
        <span></span><span></span><span></span>
      </button>
      {open && (
        <div className="user-menu-pop" role="menu">
          <button className="user-menu-item" onClick={goSettings} role="menuitem">
            {lang === 'en' ? 'Settings' : '설정'}
          </button>
          <div className="user-menu-divider"></div>
          <button className="user-menu-item" onClick={goSignOut} role="menuitem">
            {lang === 'en' ? 'Sign out' : '로그아웃'}
          </button>
        </div>
      )}
    </div>
  );
}

function Nav({ t, view, setView, currentUser, profile, onSignIn, onSignOut, theme, setTheme, lang, setLang, heroVariant, setHeroVariant, toast }) {
  // Admin sees admin-uploaded profile image first; others see their own Google photo
  const navAvatarSrc = currentUser
    ? ((currentUser.role === 'admin' && profile?.image) ? profile.image : currentUser.picture)
    : null;
  const scrollY = useScroll();
  const scrolled = scrollY > 40;

  const variants = [
    { value: 'planet', label: 'Earth' },
    { value: 'orb', label: 'Sun' },
    { value: 'wireframe', label: 'Grid' },
  ];

  return (
    <nav className={`nav ${scrolled ? 'scrolled' : ''}`}>
      <div className="nav-brand" onClick={() => setView('home')}>
        <PortalLogo size={30} />
        <span className="nav-brand-text" style={{ textTransform: 'lowercase' }}>second world</span>
      </div>
      <div className="nav-status">
        <span className="nav-status-dot" />
        <span>{t.hero_eyebrow}</span>
      </div>
      <div className="nav-links">
        <div className={`nav-link ${view === 'home' ? 'active' : ''}`} onClick={() => setView('home')}>{t.nav_apps}</div>
        <div className={`nav-link ${view === 'about' ? 'active' : ''}`} onClick={() => setView('about')}>{t.nav_about}</div>
        {currentUser?.role === 'admin' && (
          <div className={`nav-link ${view === 'admin' ? 'active' : ''}`} onClick={() => setView('admin')}>{t.nav_admin}</div>
        )}
      </div>
      <div className="nav-controls">
        {/* Theme toggle — only visible after sign-in */}
        {currentUser && (
          <button
            className="nav-pill-btn"
            onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
            title={theme === 'dark' ? 'Light mode' : 'Dark mode'}
          >
            {theme === 'dark' ? '☾' : '☀'}
          </button>
        )}
        {/* Hero scene variant — segmented control */}
        <div className="nav-seg" role="tablist" aria-label="Hero scene">
          {variants.map(v => (
            <button
              key={v.value}
              className={heroVariant === v.value ? 'on' : ''}
              onClick={() => setHeroVariant(v.value)}
              title={v.label}
            >
              {v.label}
            </button>
          ))}
        </div>
        {/* Language toggle */}
        <div className="nav-seg">
          <button className={lang === 'en' ? 'on' : ''} onClick={() => setLang('en')}>EN</button>
          <button className={lang === 'ko' ? 'on' : ''} onClick={() => setLang('ko')}>한</button>
        </div>
        {/* Sign-in / sign-out */}
        {currentUser ? (
          <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
            <div className="mini-avatar" style={{ width: 28, height: 28, fontSize: 11 }}>
              {navAvatarSrc
                ? <img src={navAvatarSrc} alt={currentUser.name} referrerPolicy="no-referrer" />
                : currentUser.initials}
            </div>
            <UserMenu currentUser={currentUser} lang={lang} setView={setView}
                      onSignOut={onSignOut} toast={toast} />
          </div>
        ) : (
          <button className="nav-signin" onClick={onSignIn}>
            {t.nav_signin}
          </button>
        )}
      </div>
    </nav>
  );
}

// ─── Hero ───────────────────────────────────────────────────────────────────
function Hero({ t, scrollY, onCta, onSignIn, variant }) {
  const progress = Math.min(scrollY / 600, 1);
  const titleY = scrollY * 0.25;
  const titleOpacity = 1 - Math.min(scrollY / 500, 1);

  return (
    <section className="hero">
      {variant === 'planet' && <Planet scrollProgress={progress} />}
      {variant === 'orb' && <SunHero3D progress={progress} />}
      {variant === 'wireframe' && <WireframeHero progress={progress} />}

      <div className={`hero-content${variant === 'orb' ? ' hero-content-sun' : ''}`} style={{
        transform: `translateY(${titleY}px)`,
        opacity: titleOpacity
      }}>
        <h1 className="hero-title" style={{ textTransform: 'lowercase' }}>
          <em>{t.hero_title_b}</em>
        </h1>
        <p className="hero-sub">{t.hero_sub}</p>
        <div className="hero-cta">
          <button className="btn btn-primary" onClick={onCta}>{t.hero_cta} →</button>
        </div>
      </div>

      {/* Scroll indicator */}
      <div style={{
        position: 'absolute', bottom: 40, left: '50%',
        transform: `translateX(-50%)`,
        opacity: 1 - Math.min(scrollY / 200, 1),
        fontFamily: 'var(--font-mono)', fontSize: 11,
        letterSpacing: '0.16em', color: 'var(--text-mute)',
        textTransform: 'uppercase', textAlign: 'center'
      }}>
        Scroll<br />
        <span style={{ display: 'inline-block', marginTop: 8, animation: 'bobble 2s ease-in-out infinite' }}>↓</span>
      </div>
    </section>
  );
}

// ─── Sun (3D spherical) ────────────────────────────────────────────────────
// The hero `orb` variant is now rendered by <SunHero3D> from sun3d.jsx — a
// real Three.js sphere with the equirectangular sun map and a tilted spin
// axis. The previous SVG twin-tile slide implementation lived here; it
// was removed once the WebGL version landed because slide-only pseudo-3D
// didn't sell rotation without continental landmarks like the Earth has.

function WireframeHero({ progress }) {
  const scale = 1 - progress * 0.4;
  const opacity = 1 - Math.min(progress * 1.4, 0.85);
  const rot = progress * 30;

  const tunnelRings = Array.from({ length: 27 }).map((_, i) => {
    const t = i / 26;
    const k = Math.pow(1 - t, 1.4);
    return { i, t, rx: 38 + k * 240, ry: 12 + k * 82,
             op: 0.15 + Math.pow(1 - t, 1.6) * 0.55, angle: (i * 180) / 27 };
  });

  const depthCircles = Array.from({ length: 14 }).map((_, i) => {
    const t = i / 13;
    return { i, t, r: 18 + Math.pow(t, 1.55) * 272,
             op: 0.16 + Math.pow(t, 0.6) * 0.5 };
  });

  const spokes = Array.from({ length: 32 }).map((_, i) => {
    const a = (i * 360) / 32;
    const rad = (a * Math.PI) / 180;
    return { i, x1: Math.cos(rad) * 26, y1: Math.sin(rad) * 26,
                x2: Math.cos(rad) * 292, y2: Math.sin(rad) * 292 };
  });

  const ellPath = (rx, ry) => `M ${rx},0 A ${rx},${ry} 0 1 1 ${-rx},0 A ${rx},${ry} 0 1 1 ${rx},0`;
  const cirPath = (r)      => `M ${r},0 A ${r},${r} 0 1 1 ${-r},0 A ${r},${r} 0 1 1 ${r},0`;

  // Particles host every Nth ring/circle (keep total ~30)
  const ringHasDots   = i => i % 5 === 1;
  const circleHasDots = i => i % 3 === 1;

  return (
    <div className="planet-stage" style={{ transform: `scale(${scale}) rotate(${rot}deg)`, opacity }}>
      <div style={{ animation: 'wormholeFloat 14s ease-in-out infinite' }}>
        <svg viewBox="0 0 600 600" width="min(640px, 85vw)" style={{ aspectRatio: 1, filter: 'drop-shadow(0 0 60px oklch(0.7 0.18 268 / 0.55))' }}>
          <defs>
            <linearGradient id="wf-stroke" x1="0" y1="0" x2="1" y2="1">
              <stop offset="0%" stopColor="oklch(0.85 0.16 268)" />
              <stop offset="100%" stopColor="oklch(0.78 0.14 195)" />
            </linearGradient>
            <radialGradient id="wf-core" cx="50%" cy="50%" r="50%">
              <stop offset="0%"   stopColor="oklch(0.95 0.20 268)" stopOpacity="0.85" />
              <stop offset="55%"  stopColor="oklch(0.72 0.18 230)" stopOpacity="0.35" />
              <stop offset="100%" stopColor="oklch(0.30 0.10 220)" stopOpacity="0" />
            </radialGradient>
          </defs>
          <g transform="translate(300 300)">
            {/* Tunnel rings + particles flowing along each */}
            <g style={{ animation: 'spin 80s linear infinite' }}>
              {tunnelRings.map(r => {
                const dur = 7 + r.t * 7;          // outer ring → slower, inner → faster (sucked-in feel)
                const hasDots = ringHasDots(r.i);
                return (
                  <g key={r.i} transform={`rotate(${r.angle})`}>
                    <ellipse cx="0" cy="0" rx={r.rx} ry={r.ry}
                             stroke="url(#wf-stroke)" strokeWidth="0.55" fill="none"
                             opacity={r.op} />
                    {hasDots && [0, 0.33, 0.66].map(d => (
                      <circle key={d} r="1.7" fill="oklch(0.95 0.20 268)" opacity="0.9"
                              style={{ filter: 'drop-shadow(0 0 4px oklch(0.85 0.22 268 / 0.85))' }}>
                        <animateMotion dur={`${dur}s`} repeatCount="indefinite"
                                       begin={`-${(d * dur).toFixed(2)}s`}
                                       path={ellPath(r.rx, r.ry)} />
                      </circle>
                    ))}
                  </g>
                );
              })}
            </g>
            {/* Concentric circles + orbiting particles, counter-rotation */}
            <g style={{ animation: 'portalSpin2 110s linear infinite' }}>
              {depthCircles.map(c => {
                const orbitDur = 6 + c.t * 12;     // small (inner) circle → fast orbit, large → slow
                const hasDots = circleHasDots(c.i);
                return (
                  <g key={c.i}>
                    <circle cx="0" cy="0" r={c.r}
                            stroke="url(#wf-stroke)" strokeWidth="0.45" fill="none"
                            opacity={c.op} />
                    {hasDots && [0, 0.4, 0.7].map(d => (
                      <circle key={d} r="1.5" fill="oklch(0.92 0.18 195)" opacity="0.85"
                              style={{ filter: 'drop-shadow(0 0 3.5px oklch(0.82 0.20 195 / 0.85))' }}>
                        <animateMotion dur={`${orbitDur}s`} repeatCount="indefinite"
                                       begin={`-${(d * orbitDur).toFixed(2)}s`}
                                       path={cirPath(c.r)} />
                      </circle>
                    ))}
                  </g>
                );
              })}
            </g>
            {/* Radial spokes — depth lines */}
            <g opacity="0.2">
              {spokes.map(s => (
                <line key={s.i} x1={s.x1} y1={s.y1} x2={s.x2} y2={s.y2}
                      stroke="url(#wf-stroke)" strokeWidth="0.35" />
              ))}
            </g>
            {/* Core glow */}
            <circle cx="0" cy="0" r="34" fill="url(#wf-core)" />
          </g>
        </svg>
      </div>
    </div>
  );
}

// ─── App Grid ───────────────────────────────────────────────────────────────
function AppCard({ app, t, lang, currentUser, onLaunch, onLockedClick }) {
  const accessible = useMemo(() => {
    if (app.visibility === 'public') return true;
    if (!currentUser) return false;
    if (currentUser.role === 'admin') return true;
    if (app.visibility === 'operator' && currentUser.role === 'operator') {
      return currentUser.access?.includes(app.id);
    }
    return false;
  }, [app, currentUser]);

  const locked = !accessible;
  const visClass = app.visibility;

  return (
    <article className={`app-card ${locked ? 'locked' : ''}`}
             style={{ '--vh': app.hue }}
             onClick={() => locked ? onLockedClick(app) : onLaunch(app)}>
      <div className="app-card-visual" style={{
        background: `linear-gradient(135deg, oklch(0.40 0.18 ${app.hue}), oklch(0.22 0.12 ${app.hue}))`
      }}>
        <div className="app-card-glyph">{app.glyph}</div>
        {locked && (
          <div className="locked-overlay">
            <div style={{ fontSize: 11, fontFamily: 'var(--font-mono)', letterSpacing: '0.14em',
                          textTransform: 'uppercase', color: 'var(--text)', opacity: 0.85 }}>
              {currentUser ? t.request_access : t.sign_in} →
            </div>
          </div>
        )}
      </div>
      <div className="app-card-body">
        <div className="app-card-meta">
          <span className={`lock-pill ${visClass}`}>
            {visClass === 'public' && '◯'}
            {visClass === 'operator' && '◐'}
            {visClass === 'admin' && '●'}
            <span style={{ marginLeft: 4 }}>
              {visClass === 'public' && (lang === 'en' ? 'Public' : '공개')}
              {visClass === 'operator' && (lang === 'en' ? 'Operator' : '운영자')}
              {visClass === 'admin' && (lang === 'en' ? 'Admin' : '관리자')}
            </span>
          </span>
          <span style={{ marginLeft: 'auto' }}>{app.cat}</span>
        </div>
        <h3 className="app-card-title">{app.name}</h3>
        <p className="app-card-desc">{app.desc[lang]}</p>
        <div className="app-card-foot">
          <span>:{app.port}</span>
          <div className="app-card-arrow">→</div>
        </div>
      </div>
    </article>
  );
}

function AppsSection({ t, lang, currentUser, onLaunch, onLockedClick }) {
  const apps = window.SW_DATA.apps;
  const publicApps = apps.filter(a => a.visibility === 'public');
  const privateApps = apps.filter(a => a.visibility !== 'public');

  return (
    <>
      <section className="section reveal" id="apps-private">
        <div className="section-head">
          <div className="section-eyebrow" style={{ color: 'oklch(0.85 0.16 268)' }}>◐ {t.private_eyebrow}</div>
          <h2 className="section-title">{t.private_title}</h2>
          <p className="section-desc">{t.private_desc}</p>
        </div>
        <div className="app-grid featured">
          {privateApps.map(app => (
            <AppCard key={app.id} app={app} t={t} lang={lang}
                     currentUser={currentUser}
                     onLaunch={onLaunch} onLockedClick={onLockedClick} />
          ))}
        </div>
      </section>

      <section className="section reveal" id="apps-public" style={{ paddingTop: 60 }}>
        <div className="section-head">
          <div className="section-eyebrow">◯ {t.public_eyebrow}</div>
          <h2 className="section-title">{t.public_title}</h2>
          <p className="section-desc">{t.public_desc}</p>
        </div>
        <div className="app-grid featured">
          {publicApps.map(app => (
            <AppCard key={app.id} app={app} t={t} lang={lang}
                     currentUser={currentUser}
                     onLaunch={onLaunch} onLockedClick={onLockedClick} />
          ))}
        </div>
      </section>
    </>
  );
}

// ─── Stats Strip ────────────────────────────────────────────────────────────
function StatsStrip({ lang }) {
  const stats = [
    { num: '10', lbl: lang === 'en' ? 'Apps online' : '온라인 앱' },
    { num: '96 GB', lbl: lang === 'en' ? 'GPU + CUDA' : 'GPU + CUDA' },
    { num: '99.4%', lbl: lang === 'en' ? '30-day uptime' : '30일 가동률' },
    { num: '1,247', lbl: lang === 'en' ? 'Inferences today' : '오늘 추론' }
  ];
  return (
    <section className="section reveal" style={{ paddingTop: 0 }}>
      <div className="stats">
        {stats.map(s => (
          <div className="stat" key={s.lbl}>
            <div className="stat-num">{s.num}</div>
            <div className="stat-lbl">{s.lbl}</div>
          </div>
        ))}
      </div>
    </section>
  );
}

// ─── About / Profile ────────────────────────────────────────────────────────
function AboutPage({ t, lang, profile, currentUser }) {
  const avatarSrc = profile.image || currentUser?.picture || 'assets/jl-logo.jpeg';
  const isPhoto   = Boolean(profile.image || currentUser?.picture);
  return (
    <div className="section" style={{ paddingTop: 140 }}>
      <div className="section-eyebrow">◇ {t.profile_eyebrow}</div>

      <div className="profile-hero">
        <div className="avatar">
          <img src={avatarSrc}
               className={isPhoto ? 'avatar-photo' : ''}
               referrerPolicy="no-referrer"
               alt={profile.name} />
        </div>
        <div>
          <h1 className="profile-name">{profile.name}</h1>
          <div className="profile-handle">{profile.handle} · {profile.title[lang]}</div>
          <p className="profile-bio">{profile.bio[lang]}</p>
          <div className="profile-meta">
            <span>{profile.location}</span>
            <span>{profile.server}</span>
            <span>{lang === 'en' ? 'Open to interesting collaborations' : '재미있는 협업 환영'}</span>
          </div>
          <div className="social-row">
            {profile.socials.map(s => (
              <a key={s.kind} className="social-link" href={s.href}>
                <SocialIcon kind={s.kind} /> {s.label}
              </a>
            ))}
          </div>
        </div>
      </div>

      <div className="profile-section">
        <h3>{lang === 'en' ? 'Now' : '지금'}</h3>
        <div className="profile-grid">
          {profile.nowItems.map((n, i) => (
            <div className="now-card" key={i}>
              <h4>{n.title[lang]}</h4>
              <p>{n.body[lang]}</p>
            </div>
          ))}
        </div>
      </div>

      <div className="profile-section">
        <h3>{lang === 'en' ? 'Path so far' : '지나온 길'}</h3>
        <ul className="role-list">
          {profile.roles.map((r, i) => (
            <li key={i}>
              <div className="role-co">
                {r.co} {r.current && <span style={{ marginLeft: 8, fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--accent)', verticalAlign: 'middle' }}>● NOW</span>}
              </div>
              <div className="role-title">{r.title[lang]}</div>
            </li>
          ))}
        </ul>
      </div>

      <div className="profile-section">
        <h3>{lang === 'en' ? 'Skills & interests' : '스킬 & 관심사'}</h3>
        <div className="skill-chips">
          {profile.skills.map(s => (
            <span key={s.tag} className={`chip ${s.accent ? 'accent' : ''}`}>{s.tag}</span>
          ))}
        </div>
      </div>

      <div className="profile-section">
        <h3>{lang === 'en' ? 'Recent activity' : '최근 활동'}</h3>
        <ul style={{ listStyle: 'none', padding: 0, margin: 0 }}>
          {profile.activity.map((a, i) => (
            <li key={i} style={{
              display: 'grid',
              gridTemplateColumns: '120px 1fr',
              gap: 24, padding: '14px 0',
              borderBottom: '0.5px solid var(--border)',
              fontSize: 14
            }}>
              <span style={{ fontFamily: 'var(--font-mono)', fontSize: 12, color: 'var(--text-mute)' }}>{a.date}</span>
              <span style={{ color: 'var(--text-dim)' }}>{a.text[lang]}</span>
            </li>
          ))}
        </ul>
      </div>
    </div>
  );
}

function SocialIcon({ kind }) {
  const icons = {
    github: '◉', x: '✕', linkedin: 'in', mail: '✉'
  };
  return <span style={{ width: 14, textAlign: 'center', fontSize: 12 }}>{icons[kind] || '◇'}</span>;
}

// ─── Login Modal ────────────────────────────────────────────────────────────
function LoginModal({ t, lang, onClose, onCredential }) {
  const buttonRef = useRef(null);
  const [gisReady, setGisReady] = useState(Boolean(window.google?.accounts?.id));
  const cfg = window.SW_AUTH || {};

  // Wait for GIS script to load (it has async/defer)
  useEffect(() => {
    if (gisReady) return;
    const t0 = Date.now();
    const id = setInterval(() => {
      if (window.google?.accounts?.id) {
        clearInterval(id);
        setGisReady(true);
      } else if (Date.now() - t0 > 8000) {
        clearInterval(id);
      }
    }, 120);
    return () => clearInterval(id);
  }, [gisReady]);

  // Initialize and render the Google button
  useEffect(() => {
    if (!gisReady || !buttonRef.current || !cfg.google_client_id) return;
    window.google.accounts.id.initialize({
      client_id: cfg.google_client_id,
      callback: onCredential,
      auto_select: false,
      cancel_on_tap_outside: true,
      ux_mode: 'popup'
    });
    buttonRef.current.innerHTML = '';
    window.google.accounts.id.renderButton(buttonRef.current, {
      type: 'standard',
      theme: 'filled_black',
      size: 'large',
      shape: 'rectangular',
      text: 'continue_with',
      logo_alignment: 'left',
      width: 320
    });
  }, [gisReady, cfg.google_client_id, onCredential]);

  return (
    <div className="modal-backdrop" onClick={onClose}>
      <div className="modal" onClick={e => e.stopPropagation()}>
        <PortalLogo size={36} />
        <h2 className="modal-title">{t.sign_in}</h2>
        <p className="modal-sub">{t.sign_in_sub}</p>

        <div ref={buttonRef} style={{ display: 'flex', justifyContent: 'center', minHeight: 44, margin: '14px 0 8px' }} />
        {!gisReady && (
          <p style={{ fontSize: 11, color: 'var(--text-mute)', textAlign: 'center', fontFamily: 'var(--font-mono)' }}>
            {lang === 'en' ? 'Loading Google Sign-In…' : 'Google 로그인 불러오는 중…'}
          </p>
        )}

        <p style={{ fontSize: 11, color: 'var(--text-mute)', textAlign: 'center', marginTop: 14, lineHeight: 1.5 }}>
          {lang === 'en'
            ? 'Only allowlisted Google accounts can enter.'
            : '허용된 Google 계정만 입장할 수 있습니다.'}
        </p>

        <div className="modal-actions" style={{ marginTop: 18 }}>
          <button className="btn btn-ghost" style={{ width: '100%', justifyContent: 'center' }} onClick={onClose}>
            {t.cancel}
          </button>
        </div>
      </div>
    </div>
  );
}

// ─── Admin Dashboard ────────────────────────────────────────────────────────
function AdminDashboard({ lang, profile, members, apps, currentUser, onUpdateProfile, onUpdateMember, toast }) {
  const [tab, setTab] = useState('profile');

  return (
    <div style={{ maxWidth: 1240, margin: '0 auto', padding: '0 28px' }}>
      <div className="admin-shell">
        <aside className="admin-side">
          <h4>{lang === 'en' ? 'Admin' : '관리'}</h4>
          {[
            { id: 'profile', en: 'Profile', ko: '프로필', count: '' },
            { id: 'members', en: 'Members', ko: '구성원', count: members.length },
            { id: 'apps', en: 'Apps', ko: '앱', count: apps.length },
            { id: 'system', en: 'System', ko: '시스템', count: '' }
          ].map(item => (
            <div key={item.id} className={`admin-nav-item ${tab === item.id ? 'active' : ''}`}
                 onClick={() => setTab(item.id)}>
              <span>{lang === 'en' ? item.en : item.ko}</span>
              {item.count !== '' && <span className="admin-nav-count">{item.count}</span>}
            </div>
          ))}
        </aside>

        <main className="admin-main">
          {tab === 'profile' && <AdminProfile lang={lang} profile={profile} currentUser={currentUser} onUpdate={onUpdateProfile} toast={toast} />}
          {tab === 'members' && <AdminMembers lang={lang} members={members} apps={apps} onUpdate={onUpdateMember} toast={toast} />}
          {tab === 'apps' && <AdminApps lang={lang} apps={apps} />}
          {tab === 'system' && <AdminSystem lang={lang} />}
        </main>
      </div>
    </div>
  );
}

function AdminProfile({ lang, profile, currentUser, onUpdate, toast }) {
  const [draft, setDraft] = useState({
    name: profile.name,
    handle: profile.handle,
    titleEn: profile.title.en,
    titleKo: profile.title.ko,
    bioEn: profile.bio.en,
    bioKo: profile.bio.ko,
    location: profile.location,
    server: profile.server,
    image: profile.image || ''
  });
  const fileRef = useRef(null);

  const save = () => {
    onUpdate({
      ...profile,
      name: draft.name,
      handle: draft.handle,
      title: { en: draft.titleEn, ko: draft.titleKo },
      bio: { en: draft.bioEn, ko: draft.bioKo },
      location: draft.location,
      server: draft.server,
      image: draft.image
    });
    toast(lang === 'en' ? 'Profile saved' : '프로필 저장됨');
  };

  const onPickImage = async (e) => {
    const file = e.target.files?.[0];
    if (!file) return;
    if (!/^image\//.test(file.type)) {
      toast(lang === 'en' ? 'Please pick an image file' : '이미지 파일을 선택하세요');
      return;
    }
    try {
      const dataUrl = await fileToCompressedDataUrl(file, 512, 0.85);
      setDraft({ ...draft, image: dataUrl });
      onUpdate({ ...profile, image: dataUrl });   // commit immediately so landing page reflects it
      toast(lang === 'en' ? 'Image updated' : '이미지 저장됨');
    } catch (_) {
      toast(lang === 'en' ? 'Could not read image' : '이미지를 읽지 못했습니다');
    } finally {
      e.target.value = '';
    }
  };

  const onRemoveImage = () => {
    setDraft({ ...draft, image: '' });
    onUpdate({ ...profile, image: '' });
    toast(lang === 'en' ? 'Image removed' : '이미지 제거됨');
  };

  const onUseGooglePhoto = () => {
    if (!currentUser?.picture) return;
    const url = currentUser.picture;
    setDraft({ ...draft, image: url });
    onUpdate({ ...profile, image: url });
    toast(lang === 'en' ? 'Google photo set as profile' : 'Google 사진을 프로필로 설정');
  };

  // Same fallback chain as landing About so admin can preview-compare
  const previewSrc    = draft.image || currentUser?.picture || 'assets/jl-logo.jpeg';
  const isPreviewPhoto = Boolean(draft.image || currentUser?.picture);
  const sourceLabel   = draft.image
    ? (lang === 'en' ? 'Active: uploaded image (overrides everything)' : '활성: 업로드 이미지 (모든 방문자에게 표시)')
    : currentUser?.picture
    ? (lang === 'en' ? 'Active: Google photo (only when signed in — anonymous visitors see default)'
                     : '활성: Google 사진 (로그인 상태에서만, 비로그인 방문자는 기본 이미지)')
    : (lang === 'en' ? 'Active: default image' : '활성: 기본 이미지');

  return (
    <>
      <div className="panel">
        <h2>{lang === 'en' ? 'Profile' : '프로필'}</h2>
        <p className="panel-sub">
          {lang === 'en' ? 'Edit what visitors see on the About page.' : 'About 페이지에 보일 내용을 수정하세요.'}
        </p>

        {/* Profile image */}
        <div className="field-row" style={{ alignItems: 'center' }}>
          <label>image</label>
          <div style={{ display: 'flex', alignItems: 'center', gap: 14, width: '100%' }}>
            <div style={{
              width: 84, height: 84, borderRadius: '50%',
              overflow: 'hidden',
              border: '0.5px solid var(--border-hi)',
              background: 'var(--surface)',
              flex: '0 0 auto',
              display: 'flex', alignItems: 'center', justifyContent: 'center',
            }}>
              <img src={previewSrc} alt="" referrerPolicy="no-referrer"
                   style={{
                     width: isPreviewPhoto ? '100%' : '78%',
                     height: isPreviewPhoto ? '100%' : '78%',
                     objectFit: isPreviewPhoto ? 'cover' : 'contain',
                     display: 'block'
                   }} />
            </div>
            <div style={{ display: 'flex', flexDirection: 'column', gap: 6, flex: 1 }}>
              <input ref={fileRef} type="file" accept="image/*" onChange={onPickImage} style={{ display: 'none' }} />
              <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
                <button className="btn" onClick={() => fileRef.current?.click()}>
                  {lang === 'en' ? 'Upload image' : '이미지 업로드'}
                </button>
                {currentUser?.picture && draft.image !== currentUser.picture && (
                  <button className="btn" onClick={onUseGooglePhoto}>
                    {lang === 'en' ? 'Use Google photo' : 'Google 사진 사용'}
                  </button>
                )}
                {draft.image && (
                  <button className="btn btn-ghost" onClick={onRemoveImage}>
                    {lang === 'en' ? 'Remove' : '제거'}
                  </button>
                )}
              </div>
              <span style={{ fontSize: 11, color: 'var(--text-mute)', fontFamily: 'var(--font-mono)' }}>
                {lang === 'en' ? 'JPG/PNG · auto-resized to 512px when uploading' : 'JPG/PNG · 업로드 시 512px 로 자동 리사이즈'}
              </span>
              <span style={{ fontSize: 11, color: 'var(--text)', opacity: 0.85, fontFamily: 'var(--font-mono)' }}>
                {sourceLabel}
              </span>
            </div>
          </div>
        </div>

        <div className="field-row">
          <label>name</label>
          <input className="input" value={draft.name} onChange={e => setDraft({ ...draft, name: e.target.value })} />
        </div>
        <div className="field-row">
          <label>handle</label>
          <input className="input" value={draft.handle} onChange={e => setDraft({ ...draft, handle: e.target.value })} />
        </div>
        <div className="field-row">
          <label>title · en</label>
          <input className="input" value={draft.titleEn} onChange={e => setDraft({ ...draft, titleEn: e.target.value })} />
        </div>
        <div className="field-row">
          <label>title · ko</label>
          <input className="input" value={draft.titleKo} onChange={e => setDraft({ ...draft, titleKo: e.target.value })} />
        </div>
        <div className="field-row">
          <label>bio · en</label>
          <textarea className="textarea" value={draft.bioEn} onChange={e => setDraft({ ...draft, bioEn: e.target.value })} />
        </div>
        <div className="field-row">
          <label>bio · ko</label>
          <textarea className="textarea" value={draft.bioKo} onChange={e => setDraft({ ...draft, bioKo: e.target.value })} />
        </div>
        <div className="field-row">
          <label>location</label>
          <input className="input" value={draft.location} onChange={e => setDraft({ ...draft, location: e.target.value })} />
        </div>
        <div className="field-row">
          <label>server</label>
          <input className="input" value={draft.server} onChange={e => setDraft({ ...draft, server: e.target.value })} />
        </div>

        <div style={{ display: 'flex', gap: 10, marginTop: 20 }}>
          <button className="btn btn-primary" onClick={save}>
            {lang === 'en' ? 'Save changes' : '변경 저장'}
          </button>
          <button className="btn btn-ghost" onClick={() => toast(lang === 'en' ? 'Reverted' : '되돌림')}>
            {lang === 'en' ? 'Discard' : '취소'}
          </button>
        </div>
      </div>
    </>
  );
}

function AdminMembers({ lang, members, apps, onUpdate, toast }) {
  const operatorMembers = members.filter(m => m.role !== 'admin');

  return (
    <div className="panel">
      <h2>{lang === 'en' ? 'Members & access' : '구성원 & 접근 권한'}</h2>
      <p className="panel-sub">
        {lang === 'en'
          ? 'Toggle which operator-only apps each member can step into. Public apps are visible to everyone.'
          : '각 운영자가 들어갈 수 있는 앱을 토글하세요. 공개 앱은 모두에게 보입니다.'}
      </p>

      {/* Admin row */}
      <div className="member-row">
        <div className="member-id">
          <div className="mini-avatar" style={{ background: 'linear-gradient(135deg, oklch(0.78 0.16 75), oklch(0.7 0.18 25))' }}>JL</div>
          <div>
            <div>Jaewon Lim</div>
            <div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--text-mute)' }}>@idaydream</div>
          </div>
        </div>
        <div style={{ fontSize: 13, color: 'var(--text-dim)' }}>
          {lang === 'en' ? 'Full access to all apps' : '모든 앱 접근 가능'}
        </div>
        <span className="role-tag" style={{ color: 'oklch(0.85 0.14 75)' }}>● admin</span>
      </div>

      {operatorMembers.map(m => {
        const operatorApps = apps.filter(a => a.visibility === 'operator');
        const adminApps = apps.filter(a => a.visibility === 'admin');
        return (
          <div key={m.id} className="member-row" style={{ alignItems: 'flex-start' }}>
            <div className="member-id">
              <div className="mini-avatar">{m.initials}</div>
              <div>
                <div>{m.name}</div>
                <div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--text-mute)' }}>{m.handle}</div>
              </div>
            </div>
            <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
              <div style={{ fontSize: 10.5, fontFamily: 'var(--font-mono)', color: 'var(--text-mute)', letterSpacing: '0.12em', textTransform: 'uppercase' }}>
                {lang === 'en' ? 'Operator apps' : '운영자 앱'}
              </div>
              <div className="access-tags">
                {operatorApps.map(a => {
                  const on = m.access?.includes(a.id);
                  return (
                    <span key={a.id} className={`access-tag ${on ? 'on' : ''}`}
                          onClick={() => {
                            const next = on
                              ? (m.access || []).filter(x => x !== a.id)
                              : [...(m.access || []), a.id];
                            onUpdate({ ...m, access: next });
                          }}>
                      {on ? '●' : '○'} {a.name}
                    </span>
                  );
                })}
              </div>
              <div style={{ fontSize: 10.5, fontFamily: 'var(--font-mono)', color: 'var(--text-mute)', letterSpacing: '0.12em', textTransform: 'uppercase', marginTop: 6 }}>
                {lang === 'en' ? 'Admin-only (locked)' : '관리자 전용 (잠김)'}
              </div>
              <div className="access-tags">
                {adminApps.map(a => (
                  <span key={a.id} className="access-tag" style={{ opacity: 0.4, cursor: 'not-allowed' }}>
                    ⊠ {a.name}
                  </span>
                ))}
              </div>
            </div>
            <span className="role-tag">○ operator</span>
          </div>
        );
      })}

      <button className="btn" style={{ marginTop: 24 }}
              onClick={() => toast(lang === 'en' ? 'Invite link copied' : '초대 링크 복사됨')}>
        + {lang === 'en' ? 'Invite member' : '구성원 초대'}
      </button>
    </div>
  );
}

function AdminApps({ lang, apps }) {
  return (
    <div className="panel">
      <h2>{lang === 'en' ? 'Apps' : '앱'}</h2>
      <p className="panel-sub">
        {lang === 'en'
          ? 'All applications discovered on ML003. Drag to reorder, click to edit visibility.'
          : 'ML003에서 발견된 모든 애플리케이션. 드래그로 순서 변경, 클릭해서 공개 범위 편집.'}
      </p>
      <div style={{ display: 'flex', flexDirection: 'column', gap: 1, background: 'var(--border)', border: '0.5px solid var(--border)', borderRadius: 12, overflow: 'hidden' }}>
        {apps.map(a => (
          <div key={a.id} style={{
            display: 'grid', gridTemplateColumns: '40px 1fr 100px 100px 80px',
            gap: 12, padding: '14px 18px', alignItems: 'center',
            background: 'var(--bg-1)', fontSize: 13.5
          }}>
            <div style={{
              width: 32, height: 32, borderRadius: 8,
              background: `linear-gradient(135deg, oklch(0.4 0.18 ${a.hue}), oklch(0.25 0.12 ${a.hue}))`,
              display: 'flex', alignItems: 'center', justifyContent: 'center',
              fontFamily: 'var(--font-display)', fontSize: 18, color: 'rgba(255,255,255,0.9)'
            }}>{a.glyph}</div>
            <div>
              <div style={{ fontWeight: 500 }}>{a.name}</div>
              <div style={{ fontSize: 12, color: 'var(--text-mute)', fontFamily: 'var(--font-mono)' }}>:{a.port} · {a.cat}</div>
            </div>
            <span className={`lock-pill ${a.visibility}`}>
              {a.visibility}
            </span>
            <span style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'oklch(0.85 0.14 155)' }}>● online</span>
            <button className="btn btn-ghost" style={{ height: 28, padding: '0 12px', fontSize: 11 }}>
              {lang === 'en' ? 'Edit' : '편집'}
            </button>
          </div>
        ))}
      </div>
    </div>
  );
}

function AdminSystem({ lang }) {
  const ITALIAN_RED = 'oklch(0.58 0.22 25)';
  // Snapshot from nvidia-smi + /proc on ML003 (manually captured 2026-04-28)
  // TODO: replace with live fetch from system.mindlab.kr endpoint (see #5)
  const hostStats = [
    { k: 'CPU load', v: '1.47 / 32',       sub: '5% (1-min)' },
    { k: 'Memory',   v: '31 / 251 GiB',    sub: '12% used' },
    { k: 'Network',  v: '1 Gbps',          sub: 'enp210s0f1np1' },
    { k: 'Storage',  v: '983 G / 1.8 T',   sub: 'NVMe · 57% used' },
  ];
  const gpuStats = [
    { k: 'GPU 0', v: '49°C · 0% util', sub: 'RTX 4090 · 16/24 GB' },
    { k: 'GPU 1', v: '46°C · 0% util', sub: 'RTX 4090 · 17/24 GB' },
    { k: 'GPU 2', v: '47°C · 0% util', sub: 'RTX 4090 · 22/24 GB' },
    { k: 'GPU 3', v: '50°C · 0% util', sub: 'RTX 4090 · 11/24 GB' },
  ];
  const labelStyle = { fontFamily: 'var(--font-mono)', fontSize: 10.5, letterSpacing: '0.12em', textTransform: 'uppercase', color: 'var(--text-mute)' };

  return (
    <div className="panel">
      <h2>{lang === 'en' ? 'System' : '시스템'}</h2>
      <p className="panel-sub">
        {lang === 'en' ? 'ML003 GPU server status.' : 'ML003 GPU 서버 상태.'}
      </p>

      {/* Top row: host stats */}
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, minmax(0, 1fr))', gap: 12 }}>
        {hostStats.map(s => (
          <div key={s.k} className="now-card">
            <div style={labelStyle}>{s.k}</div>
            <h4 style={{ marginTop: 6 }}>{s.v}</h4>
            <p>{s.sub}</p>
          </div>
        ))}
      </div>

      {/* GPU row: forced 4-up, italian red border */}
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, minmax(0, 1fr))', gap: 12, marginTop: 12 }}>
        {gpuStats.map(s => (
          <div key={s.k} className="now-card"
               style={{
                 border: `1px solid ${ITALIAN_RED}`,
                 boxShadow: `0 0 0 0.5px ${ITALIAN_RED} inset, 0 0 14px oklch(0.58 0.22 25 / 0.18)`
               }}>
            <div style={{ ...labelStyle, color: ITALIAN_RED }}>{s.k}</div>
            <h4 style={{ marginTop: 6 }}>{s.v}</h4>
            <p>{s.sub}</p>
          </div>
        ))}
      </div>
    </div>
  );
}

// ─── Footer ─────────────────────────────────────────────────────────────────
function Footer({ t }) {
  return (
    <footer className="footer">
      <div className="footer-mark">
        <PortalLogo size={28} animated={false} />
        <span style={{ fontFamily: 'var(--font-display)', fontSize: 16, textTransform: 'lowercase' }}>second world</span>
      </div>
      <div className="footer-text">{t.footer_text}</div>
      <div className="footer-text" style={{ marginTop: 8 }}>© 2026 · Built on ML003</div>
    </footer>
  );
}

// ─── Root App ───────────────────────────────────────────────────────────────
function App() {
  const [tw, setTweak] = useTweaks(TWEAK_DEFAULTS);
  const [view, setView] = useState('home');
  const [showLogin, setShowLogin] = useState(false);
  const [currentUser, setCurrentUser] = useState(null);
  const [members, setMembers] = useState(window.SW_DATA.members);
  const [profile, setProfile] = useState(() => {
    let p = window.SW_DATA.profile;
    try {
      const img = localStorage.getItem('sw_profile_image_v1');
      if (img) p = { ...p, image: img };
    } catch (_) { /* ignore */ }
    return p;
  });

  // Persist profile image (only) to localStorage
  useEffect(() => {
    try {
      if (profile.image) localStorage.setItem('sw_profile_image_v1', profile.image);
      else localStorage.removeItem('sw_profile_image_v1');
    } catch (_) { /* ignore */ }
  }, [profile.image]);

  // Pick a random hero variant per visit, based on the access second
  useEffect(() => {
    const variants = ['planet', 'orb', 'wireframe'];
    const idx = Math.floor(Date.now() / 1000) % variants.length;
    setTweak('heroVariant', variants[idx]);
    // run once per page load
  }, []);
  const [toastMsg, setToastMsg] = useState('');
  const apps = window.SW_DATA.apps;

  const scrollY = useScroll();
  useReveal([view]);

  const t = window.SW_DATA.i18n[tw.language];

  // Apply theme + accent hue to <html>
  useEffect(() => {
    document.documentElement.dataset.theme = tw.theme;
    document.documentElement.style.setProperty('--accent-h', tw.accentHue);
  }, [tw.theme, tw.accentHue]);

  // Scroll to top on view change
  useEffect(() => { window.scrollTo({ top: 0, behavior: 'instant' }); }, [view]);

  const toast = useCallback((msg) => {
    setToastMsg(msg);
    setTimeout(() => setToastMsg(''), 2200);
  }, []);

  // Restore session on mount (if not expired)
  useEffect(() => {
    try {
      const cfg = window.SW_AUTH || {};
      const raw = localStorage.getItem(cfg.session_key || 'sw_session_v1');
      if (!raw) return;
      const sess = JSON.parse(raw);
      if (sess.expiresAt && sess.expiresAt < Date.now()) {
        localStorage.removeItem(cfg.session_key || 'sw_session_v1');
        return;
      }
      const member = members.find(m => m.id === sess.memberId);
      if (member) setCurrentUser({ ...member, email: sess.email, picture: sess.picture });
    } catch (_) { /* ignore */ }
  }, []); // run once

  const handleGoogleCredential = useCallback((response) => {
    if (!response?.credential) return;
    let payload;
    try { payload = decodeJwtPayload(response.credential); }
    catch (_) { toast(tw.language === 'en' ? 'Invalid credential' : '인증 토큰 오류'); return; }
    if (!payload?.email_verified) {
      toast(tw.language === 'en' ? 'Email not verified by Google' : 'Google 미인증 이메일');
      return;
    }
    const member = findMemberByEmail(members, payload.email);
    if (!member) {
      toast(tw.language === 'en'
        ? `Access denied: ${payload.email}`
        : `접근 거부: ${payload.email}`);
      return;
    }
    const user = { ...member, email: payload.email, picture: payload.picture };
    setCurrentUser(user);
    setShowLogin(false);
    try {
      const cfg = window.SW_AUTH || {};
      localStorage.setItem(cfg.session_key || 'sw_session_v1', JSON.stringify({
        memberId: member.id,
        email: payload.email,
        picture: payload.picture,
        expiresAt: (payload.exp || 0) * 1000
      }));
    } catch (_) { /* localStorage might be unavailable */ }
    toast(`${tw.language === 'en' ? 'Welcome,' : '환영합니다,'} ${member.name}`);
  }, [members, tw.language, toast]);

  const handleSignOut = useCallback(() => {
    setCurrentUser(null);
    setView('home');
    try {
      const cfg = window.SW_AUTH || {};
      localStorage.removeItem(cfg.session_key || 'sw_session_v1');
    } catch (_) { /* ignore */ }
    if (window.google?.accounts?.id) {
      try { window.google.accounts.id.disableAutoSelect(); } catch (_) {}
    }
  }, []);

  const handleLaunch = (app) => {
    toast(`${tw.language === 'en' ? 'Launching' : '실행 중'} ${app.name} → :${app.port}`);
  };

  const handleLockedClick = (app) => {
    if (!currentUser) {
      setShowLogin(true);
    } else {
      toast(tw.language === 'en'
        ? `Access to ${app.name} not granted. Ask Jaewon.`
        : `${app.name} 접근 권한이 없습니다. Jaewon에게 요청하세요.`);
    }
  };

  const updateMember = (m) => {
    setMembers(ms => ms.map(x => x.id === m.id ? m : x));
    if (currentUser?.id === m.id) setCurrentUser(m);
  };

  return (
    <>
      <div className="cosmos"></div>
      <div className="stars"></div>

      <Nav t={t} view={view} setView={setView}
           currentUser={currentUser}
           profile={profile}
           onSignIn={() => setShowLogin(true)}
           onSignOut={handleSignOut}
           theme={tw.theme} setTheme={(v) => setTweak('theme', v)}
           lang={tw.language} setLang={(v) => setTweak('language', v)}
           heroVariant={tw.heroVariant} setHeroVariant={(v) => setTweak('heroVariant', v)}
           toast={toast} />

      {view === 'home' && (
        <>
          <Hero t={t} scrollY={scrollY} variant={tw.heroVariant}
                onCta={() => document.getElementById('apps-public')?.scrollIntoView({ behavior: 'smooth' })}
                onSignIn={() => setShowLogin(true)} />
          <AppsSection t={t} lang={tw.language} currentUser={currentUser}
                       onLaunch={handleLaunch} onLockedClick={handleLockedClick} />
          <StatsStrip lang={tw.language} />
          <Footer t={t} />
        </>
      )}

      {view === 'about' && (
        <>
          <AboutPage t={t} lang={tw.language} profile={profile} currentUser={currentUser} />
          <Footer t={t} />
        </>
      )}

      {view === 'admin' && currentUser?.role === 'admin' && (
        <AdminDashboard lang={tw.language} profile={profile} members={members} apps={apps}
                        currentUser={currentUser}
                        onUpdateProfile={setProfile} onUpdateMember={updateMember}
                        toast={toast} />
      )}

      {showLogin && (
        <LoginModal t={t} lang={tw.language}
                    onClose={() => setShowLogin(false)}
                    onCredential={handleGoogleCredential} />
      )}

      <TweaksPanel>
        <TweakSection label="Display" />
        <TweakRadio label="Language" value={tw.language}
                    options={['en', 'ko']}
                    onChange={(v) => setTweak('language', v)} />
        <TweakSection label="Accent" />
        <TweakSlider label="Hue" value={tw.accentHue} min={0} max={360} step={1}
                     onChange={(v) => setTweak('accentHue', v)} />
      </TweaksPanel>

      {toastMsg && <div className="toast show">{toastMsg}</div>}
    </>
  );
}

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