// shared.jsx — small reusable bits for the yiteai screens

// ── Shared CDN thumbnail helper ──────────────────────────────────────────────
// Delegates to api.js's getCdnDomain() — single fetch, shared across all components.
let _sharedCdnDomain = null;
const _sharedFetchCdn = () => {
  if (_sharedCdnDomain !== null) return Promise.resolve(_sharedCdnDomain);
  const p = window.YiteAPI?.getCdnDomain?.() || Promise.resolve('');
  return p.then(d => { _sharedCdnDomain = d; return d; });
};
// Returns an OSS-processed thumbnail URL for CDN assets; falls back to original.
const _sharedThumbUrl = (url, tag, cdnDomain, w = 600) => {
  if (!url || !cdnDomain || !url.startsWith(cdnDomain) || url.includes('?')) return url;
  if (tag === 'video') return url + `?x-oss-process=video/snapshot,t_1000,f_jpg,w_${w},m_fast`;
  if (tag === 'image') return url + `?x-oss-process=image/resize,w_${w}/format,webp`;
  return url;
};

const Logo = ({ size = 16 }) => (
  <a href="/" className="yt-logo" style={{ fontSize: size, textDecoration: 'none' }}>
    <img src="/frontend/icon/favicon-32x32.png" alt="" style={{ width: size * 1.4, height: size * 1.4, borderRadius: size * 0.3 }} />
    <span>yiteai<span style={{ color: "var(--accent)" }}>.com</span></span>
  </a>
);

const Icon = ({ name, size = 16, color = 'currentColor', strokeWidth = 1.6 }) => {
  const paths = {
    search: <><circle cx="11" cy="11" r="7"/><path d="M20 20l-3.5-3.5"/></>,
    sparkle: <path d="M12 3l1.8 5.4L19 10l-5.2 1.6L12 17l-1.8-5.4L5 10l5.2-1.6L12 3zM19 3l.8 2.2L22 6l-2.2.8L19 9l-.8-2.2L16 6l2.2-.8L19 3z"/>,
    image: <><rect x="3" y="3" width="18" height="18" rx="2.5"/><circle cx="8.5" cy="8.5" r="1.5"/><path d="M21 15l-5-5L5 21"/></>,
    pen: <><path d="M14 4l6 6L8 22H2v-6L14 4z"/><path d="M14 4l6 6"/></>,
    video: <><rect x="2" y="6" width="14" height="12" rx="2"/><path d="M16 10l6-3v10l-6-3z"/></>,
    voice: <><rect x="9" y="3" width="6" height="12" rx="3"/><path d="M5 11a7 7 0 0 0 14 0M12 18v3"/></>,
    code: <><path d="M9 8l-5 4 5 4M15 8l5 4-5 4"/></>,
    bot: <><rect x="3" y="8" width="18" height="13" rx="3"/><path d="M12 3v5M8 14h.01M16 14h.01"/></>,
    translate: <><path d="M3 6h13M9 4v2m-2 0c0 4-2 7-4 8m4-8c0 3 2.5 6 6 7M14 21l5-12 5 12m-8.5-3h7"/></>,
    upscale: <><path d="M4 9V4h5M20 15v5h-5M4 15v5h5M20 9V4h-5"/></>,
    eraser: <><path d="M16 3l5 5L9 20H4v-5L16 3zM12 7l5 5"/></>,
    star: <path d="M12 3l2.6 6.4L21 10l-5 4.5L17.5 21 12 17.5 6.5 21 8 14.5 3 10l6.4-.6L12 3z"/>,
    arrow: <path d="M5 12h14M13 6l6 6-6 6"/>,
    chevron: <path d="M9 6l6 6-6 6"/>,
    chevronDown: <path d="M6 9l6 6 6-6"/>,
    check: <path d="M5 12l5 5L20 7"/>,
    plus: <path d="M12 5v14M5 12h14"/>,
    download: <><path d="M12 4v12m-5-5l5 5 5-5"/><path d="M4 20h16"/></>,
    upload: <><path d="M12 20V8m-5 5l5-5 5 5"/><path d="M4 4h16"/></>,
    user: <><circle cx="12" cy="8" r="4"/><path d="M4 21c0-4.4 3.6-8 8-8s8 3.6 8 8"/></>,
    bell: <><path d="M6 8a6 6 0 0 1 12 0c0 7 3 8 3 8H3s3-1 3-8z"/><path d="M10 21a2 2 0 0 0 4 0"/></>,
    settings: <><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></>,
    coin: <><circle cx="12" cy="12" r="9"/><path d="M9 9.5h4a2 2 0 0 1 0 4h-3l4 4M9 12.5h6"/></>,
    lock: <><rect x="4" y="11" width="16" height="10" rx="2"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/></>,
    mail: <><rect x="3" y="5" width="18" height="14" rx="2"/><path d="M3 7l9 6 9-6"/></>,
    google: <path d="M21.35 11.1H12v3.8h5.35c-.5 2.4-2.5 3.7-5.35 3.7-3.2 0-5.8-2.6-5.8-5.8s2.6-5.8 5.8-5.8c1.4 0 2.7.5 3.7 1.4l2.7-2.7C16.85 4 14.6 3 12 3 7 3 3 7 3 12s4 9 9 9c5.2 0 8.7-3.65 8.7-8.8 0-.4-.05-.8-.1-1.1z"/>,
    apple: <path d="M16.5 12.5c0-2.7 2.2-4 2.3-4.05-1.25-1.85-3.2-2.1-3.9-2.13-1.65-.17-3.25 1-4.1 1-.85 0-2.15-1-3.55-.95-1.8.03-3.5 1.05-4.45 2.7-1.9 3.3-.5 8.2 1.35 10.9.9 1.3 2 2.8 3.4 2.75 1.4-.05 1.9-.9 3.55-.9s2.1.9 3.55.85c1.45-.05 2.4-1.35 3.3-2.65 1.05-1.55 1.45-3 1.5-3.1-.05-.05-2.95-1.15-2.95-4.4zM13.8 4.5c.7-.85 1.2-2.05 1.05-3.25-1 .05-2.25.7-3 1.55-.65.75-1.25 1.95-1.1 3.15 1.15.1 2.3-.6 3.05-1.45z"/>,
    refresh: <><path d="M3 12a9 9 0 0 1 15-6.7L21 8M21 3v5h-5M21 12a9 9 0 0 1-15 6.7L3 16M3 21v-5h5"/></>,
    grid: <><rect x="3" y="3" width="7" height="7" rx="1.5"/><rect x="14" y="3" width="7" height="7" rx="1.5"/><rect x="3" y="14" width="7" height="7" rx="1.5"/><rect x="14" y="14" width="7" height="7" rx="1.5"/></>,
    list: <><path d="M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01"/></>,
    fire: <path d="M12 2s4 4 4 8a4 4 0 0 1-8 0c0-1 .5-2 .5-2S7 11 7 14a5 5 0 0 0 10 0c0-5-5-12-5-12z"/>,
    folder: <path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7z"/>,
    history: <><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></>,
    layers: <><path d="M12 3l9 5-9 5-9-5 9-5z"/><path d="M3 13l9 5 9-5M3 18l9 5 9-5"/></>,
    aspect: <><rect x="3" y="6" width="18" height="12" rx="2"/></>,
    shuffle: <><path d="M16 3h5v5M4 20l17-17M21 16v5h-5M15 15l6 6M4 4l5 5"/></>,
    close: <path d="M6 6l12 12M18 6L6 18"/>,
    menu: <path d="M3 6h18M3 12h18M3 18h18"/>,
    eye: <><path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7z"/><circle cx="12" cy="12" r="3"/></>,
    flash: <path d="M13 2L4 14h7l-1 8 9-12h-7l1-8z"/>,
    link: <><path d="M10 13a5 5 0 007.54.54l3-3a5 5 0 00-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 00-7.54-.54l-3 3a5 5 0 007.07 7.07l1.71-1.71"/></>,
    file: <><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/></>,
    compass: <><circle cx="12" cy="12" r="9"/><path d="M16.24 7.76l-2.12 6.36-6.36 2.12 2.12-6.36 6.36-2.12z"/></>,
  };
  return (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none"
      stroke={color} strokeWidth={strokeWidth} strokeLinecap="round" strokeLinejoin="round"
      style={{ flexShrink: 0 }}>
      {paths[name] || <circle cx="12" cy="12" r="6"/>}
    </svg>
  );
};

/* ── 计算用户头像 fallback 缩写：多词取首末字母，单词取首字母，邮箱取 @ 前首字母 ──────── */
const computeInitials = (raw) => {
  if (!raw) return '?';
  const s = String(raw).trim();
  if (!s) return '?';
  // 邮箱：取 @ 前的部分
  const before = s.includes('@') ? s.split('@')[0] : s;
  // 按空格 / · / - / _ 分词
  const parts = before.split(/[\s·\-_]+/).filter(Boolean);
  if (parts.length === 0) return s.charAt(0).toUpperCase();
  if (parts.length === 1) return parts[0].charAt(0).toUpperCase();
  return (parts[0].charAt(0) + parts[parts.length - 1].charAt(0)).toUpperCase();
};

/* ── MobileDrawer：移动端侧滑菜单（AppShell + TopNav 共用） ──────── */
const MobileDrawer = ({ open, onClose, signedIn, pageHistory = [], onClearHistory }) => {
  const { t: tt } = useI18n();
  if (!open) return null;
  const items = [
    { icon: 'compass', label: tt('nav.discover') || '发现', href: '/discover' },
    { icon: 'image',   label: tt('nav.image') || '图片',    href: '/create2img?tab=ref' },
    { icon: 'video',   label: tt('nav.video') || '视频',    href: '/create2video?tab=ref' },
    { icon: 'layers',  label: tt('nav.template') || '模板', href: '/template/_' },
    { icon: 'folder',  label: tt('nav.assets') || '我的资产', href: '/assets' },
    { icon: 'star',    label: tt('shared.menu.subscription') || '订阅与套餐', href: '/pricing' },
    ...(signedIn ? [
      { icon: 'coin', label: tt('shared.menu.orders') || '支付记录', href: '/orders' },
      { icon: 'user', label: tt('shared.menu.profile') || '个人中心', href: '/profile' },
    ] : [
      { icon: 'user', label: tt('nav.login') || '登录', href: '/login' },
    ]),
  ];
  return (
    <>
      <div onClick={onClose} style={{
        position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.5)', backdropFilter: 'blur(4px)', zIndex: 200,
      }} />
      <div role="dialog" aria-label="导航菜单" style={{
        position: 'fixed', top: 0, left: 0, bottom: 0, width: '78%', maxWidth: 320,
        background: '#0c1018', borderRight: '1px solid var(--line)',
        zIndex: 201, display: 'flex', flexDirection: 'column',
      }}>
        <div style={{ padding: '14px 18px', borderBottom: '1px solid var(--line)', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
          <span style={{ fontSize: 15, fontWeight: 600, color: 'var(--fg-0)' }}>{tt('shared.menu.title') || '菜单'}</span>
          <button onClick={onClose} aria-label="关闭菜单" style={{
            width: 36, height: 36, borderRadius: 8, border: 'none',
            background: 'rgba(255,255,255,0.04)', color: 'var(--fg-2)', cursor: 'pointer',
            display: 'flex', alignItems: 'center', justifyContent: 'center',
          }}>
            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
          </button>
        </div>
        <div style={{ flex: 1, overflowY: 'auto', padding: '8px 0' }}>
          {/* 主菜单 */}
          <div style={{ padding: '8px 18px 4px', fontSize: 11, fontWeight: 600, color: 'var(--fg-3)', letterSpacing: '0.06em', textTransform: 'uppercase' }}>主菜单</div>
          {items.map(m => (
            <a key={m.href} href={m.href} style={{
              display: 'flex', alignItems: 'center', gap: 12, padding: '10px 18px',
              textDecoration: 'none', color: 'var(--fg-1)', fontSize: 14,
            }}>
              <Icon name={m.icon} size={15} color="var(--fg-3)" />
              {m.label}
            </a>
          ))}

          {/* 访问历史（仅 AppShell 传入时显示） */}
          {pageHistory && pageHistory.length > 1 && (
            <>
              <div style={{ padding: '14px 18px 4px', fontSize: 11, fontWeight: 600, color: 'var(--fg-3)', letterSpacing: '0.06em', textTransform: 'uppercase' }}>访问历史</div>
              {pageHistory.slice(1, 9).map((h, i) => (
                <a key={i} href={h.url} style={{
                  display: 'flex', alignItems: 'center', gap: 12, padding: '8px 18px',
                  textDecoration: 'none', color: 'var(--fg-2)', fontSize: 13,
                }}>
                  <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="var(--fg-3)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{ flexShrink: 0 }}>
                    <circle cx="12" cy="12" r="9"/>
                    <polyline points="12 7 12 12 15 14"/>
                  </svg>
                  <div style={{ minWidth: 0, flex: 1 }}>
                    <div style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{h.title}</div>
                    <div style={{ fontSize: 11, color: 'var(--fg-3)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{h.url}</div>
                  </div>
                </a>
              ))}
              {onClearHistory && (
                <button onClick={onClearHistory} style={{
                  margin: '4px 18px 12px', padding: '6px 10px', fontSize: 11,
                  background: 'transparent', border: '1px solid var(--line-2)', borderRadius: 6,
                  color: 'var(--fg-3)', cursor: 'pointer',
                }}>清空历史</button>
              )}
            </>
          )}
        </div>
      </div>
    </>
  );
};

/* ── i18n React hook ──────────────────────────────────────── */
const useI18n = () => {
  const [lang, setLangState] = React.useState(window.i18n ? window.i18n.getLang() : 'en');
  React.useEffect(() => {
    const handler = (e) => setLangState(e.detail.lang);
    window.addEventListener('i18n:change', handler);
    return () => window.removeEventListener('i18n:change', handler);
  }, []);
  const t = window.t || ((k) => k);
  return { lang, t, setLang: (l) => window.i18n && window.i18n.setLang(l) };
};

/* ── Modal hook：ESC 关闭 + body 滚动锁定 ──────────────────────────── */
// 用法：useModalLock(isOpen, onClose)
//   - isOpen 为 true 时锁定 body 滚动 + 监听 ESC 键
//   - 卸载或 isOpen 变 false 时自动恢复
const useModalLock = (isOpen, onClose) => {
  React.useEffect(() => {
    if (!isOpen) return undefined;
    const onKey = (e) => { if (e.key === 'Escape' && onClose) onClose(); };
    window.addEventListener('keydown', onKey);
    const prev = document.body.style.overflow;
    document.body.style.overflow = 'hidden';
    return () => {
      window.removeEventListener('keydown', onKey);
      document.body.style.overflow = prev;
    };
  }, [isOpen, onClose]);
};

/* ── Responsive breakpoint hook ──────────────────────────── */
const useBreakpoint = () => {
  const [w, setW] = React.useState(typeof window !== 'undefined' ? window.innerWidth : 1200);
  React.useEffect(() => {
    const handler = () => setW(window.innerWidth);
    window.addEventListener('resize', handler);
    return () => window.removeEventListener('resize', handler);
  }, []);
  return { isMobile: w < 768, isTablet: w >= 768 && w < 1024, isDesktop: w >= 1024, w };
};

/* ── Shared relative time formatter ──────────────────────── */
const relativeTime = (iso) => {
  if (!iso) return '';
  const t = window.t || (k => k);
  const diff = (Date.now() - new Date(iso).getTime()) / 1000;
  if (diff < 60) return t('time.justNow');
  if (diff < 3600) return Math.floor(diff / 60) + t('time.minutesAgo');
  if (diff < 86400) return Math.floor(diff / 3600) + t('time.hoursAgo');
  return Math.floor(diff / 86400) + t('time.daysAgo');
};

/* ── Reusable carousel hook + component ──────────────────── */
const useCarousel = (len, interval = 3800) => {
  const [idx, setIdx] = React.useState(0);
  React.useEffect(() => {
    if (len <= 1) return;
    const t = setInterval(() => setIdx(i => (i + 1) % len), interval);
    return () => clearInterval(t);
  }, [len, interval]);
  return [idx, setIdx];
};

/**
 * Carousel — universal horizontal sliding carousel.
 *
 * Props:
 *   items     — array of React nodes or data
 *   renderItem(item, index) — render each card (must return stable JSX)
 *   show      — visible cards at once (default 1)
 *   gap       — gap in px (default 16)
 *   interval  — auto-advance ms (default 3500, 0=disabled)
 *   loop      — infinite loop (default true)
 *   dots      — show nav dots (default true)
 *   dotColor  — dot active color (default 'var(--accent)')
 */
const Carousel = ({ items, renderItem, show = 1, gap = 16, interval = 3500, loop = true, dots = true, dotColor = 'var(--accent)' }) => {
  const total = items.length;
  if (total === 0) return null;
  const cloned = loop ? [...items, ...items.slice(0, show)] : items;
  const [pos, setPos] = React.useState(0);
  const [noTrans, setNoTrans] = React.useState(false);
  const timerRef = React.useRef(null);
  const maxPos = loop ? total : Math.max(0, total - show);

  // Auto-advance
  React.useEffect(() => {
    if (!interval || total <= show) return;
    timerRef.current = setInterval(() => setPos(p => p + 1), interval);
    return () => clearInterval(timerRef.current);
  }, [interval, total, show]);

  // Loop reset
  React.useEffect(() => {
    if (loop && pos >= total) {
      const t = setTimeout(() => {
        setNoTrans(true);
        setPos(0);
        requestAnimationFrame(() => requestAnimationFrame(() => setNoTrans(false)));
      }, 520);
      return () => clearTimeout(t);
    }
    if (!loop && pos > maxPos) setPos(maxPos);
  }, [pos, total, loop, maxPos]);

  const cardFlex = `0 0 calc(${100 / show}% - ${gap * (show - 1) / show}px)`;
  const activeDot = pos % total;

  return (
    <div>
      <div style={{ overflow: 'hidden' }}>
        <div style={{
          display: 'flex', gap,
          transition: noTrans ? 'none' : 'transform .5s ease',
          transform: `translateX(calc(-${pos} * (${100 / show}% + ${gap / show}px)))`,
        }}>
          {cloned.map((item, i) => (
            <div key={i} style={{ flex: cardFlex, flexShrink: 0 }}>
              {renderItem(item, i % total)}
            </div>
          ))}
        </div>
      </div>
      {dots && total > show && (
        <div style={{ display: 'flex', justifyContent: 'center', marginTop: 20, gap: 6 }}>
          {Array.from({ length: total }, (_, i) => (
            <button key={i} onClick={() => { setPos(i); clearInterval(timerRef.current); }} style={{
              width: i === activeDot ? 24 : 8, height: 8, borderRadius: 4, border: 'none', cursor: 'pointer',
              background: i === activeDot ? dotColor : 'rgba(255,255,255,0.15)',
              transition: 'all .25s ease',
            }} />
          ))}
        </div>
      )}
    </div>
  );
};

/* ── Language switcher button ─────────────────────────────── */
const LangSwitcher = ({ style }) => {
  const { lang, setLang } = useI18n();
  const next = lang === 'en' ? 'zh' : 'en';
  const label = lang === 'en' ? '中文' : 'EN';
  return (
    <button onClick={() => setLang(next)} style={{
      height: 30, padding: '0 10px', borderRadius: 7, fontSize: 12, fontWeight: 600,
      background: 'rgba(255,255,255,0.05)', border: '1px solid var(--line-2)',
      color: 'var(--fg-2)', cursor: 'pointer', letterSpacing: '0.04em',
      display: 'flex', alignItems: 'center', gap: 4,
      ...style,
    }}>
      <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" style={{ flexShrink: 0 }}>
        <path d="M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18z"/>
        <path d="M3.6 9h16.8M3.6 15h16.8M12 3c-2.5 3-4 5.8-4 9s1.5 6 4 9M12 3c2.5 3 4 5.8 4 9s-1.5 6-4 9"/>
      </svg>
      {label}
    </button>
  );
};

// Top nav bar used across screens
const TopNav = ({ active = 'home', credits = 248, signedIn = true, compact = false }) => {
  const { t } = useI18n();
  const { isMobile } = useBreakpoint();
  const navUser = window.YiteAPI?.getUser?.();
  const navUserAvatar = navUser?.avatar || (navUser?.avatar_oss_key ? (window.YiteAPI?.cdnDomain || '') + '/' + navUser.avatar_oss_key : '');
  const navInitials = computeInitials(navUser?.nickname || navUser?.email);
  // 移动端 hamburger 抽屉（landing 用）
  const [mobNavOpen, setMobNavOpen] = React.useState(false);
  // 移动端：minimal bar — 左 hamburger（抽屉） + 中 brand + 右 用户信息/登录
  if (isMobile) {
    return (
      <>
      <header style={{
        height: 44, flexShrink: 0, padding: '0 8px',
        display: 'flex', alignItems: 'center', justifyContent: 'space-between',
        borderBottom: '1px solid var(--line)',
        background: 'rgba(10,14,26,0.85)', backdropFilter: 'blur(12px)',
        position: 'sticky', top: 0, zIndex: 50,
      }}>
        <button onClick={() => setMobNavOpen(true)} aria-label="菜单" style={{
          width: 36, height: 36, borderRadius: 8, border: 'none', background: 'transparent', color: 'var(--fg-1)',
          display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer',
        }}>
          <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
        </button>
        <Logo size={14} />
        {signedIn ? (
          <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
            <a href="/pricing" style={{
              display: 'flex', alignItems: 'center', gap: 4,
              padding: '5px 10px', borderRadius: 999,
              background: 'rgba(34,211,238,0.08)', border: '1px solid rgba(34,211,238,0.2)',
              fontSize: 12, color: '#b3eef9', textDecoration: 'none', fontVariantNumeric: 'tabular-nums',
            }}>
              <Icon name="coin" size={12} color="var(--accent)" />
              <span>{credits.toLocaleString()}</span>
            </a>
            <a href="/profile" aria-label="个人中心" style={{
              width: 36, height: 36, borderRadius: 999,
              background: 'linear-gradient(135deg, #a78bfa, #22d3ee)',
              display: 'flex', alignItems: 'center', justifyContent: 'center',
              fontSize: 12, fontWeight: 700, color: '#0a0e1a', textDecoration: 'none',
              overflow: 'hidden',
            }}>{navUserAvatar ? <img src={navUserAvatar} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover' }} /> : navInitials}</a>
          </div>
        ) : (
          <a href="/login" style={{
            height: 36, padding: '0 16px', borderRadius: 8,
            background: 'linear-gradient(135deg, var(--accent), var(--accent-2))',
            color: '#052028', fontSize: 13, fontWeight: 600, textDecoration: 'none',
            display: 'flex', alignItems: 'center',
          }}>{t('nav.login') || 'Login'}</a>
        )}
      </header>

      <MobileDrawer open={mobNavOpen} onClose={() => setMobNavOpen(false)} signedIn={signedIn} />
      </>
    );
  }
  return (
  <header style={{
    height: 60, padding: '0 28px',
    display: 'flex', alignItems: 'center', gap: 32,
    borderBottom: '1px solid var(--line)',
    background: 'rgba(10,14,26,0.7)',
    backdropFilter: 'blur(20px)',
    position: 'sticky', top: 0, zIndex: 50,
  }}>
    {!compact && <Logo size={isMobile ? 14 : 16} />}
    {!compact && !isMobile && (
      <nav style={{ display: 'flex', gap: 4 }}>
        {[
          { id: 'discover', key: 'nav.discover', href: '/discover' },
          { id: 'image',    key: 'nav.image',    href: '/create2img?tab=ref' },
          { id: 'video',    key: 'nav.video',    href: '/create2video?tab=ref' },
          { id: 'template', key: 'nav.template', href: '/template/_' },
          { id: 'pricing',  key: 'nav.pricing',  href: '/pricing' },
        ].map(n => (
          <a key={n.id} href={n.href} style={{
            padding: '6px 12px', borderRadius: 8, fontSize: 13,
            color: active === n.id ? 'var(--fg-0)' : 'var(--fg-2)',
            background: active === n.id ? 'rgba(255,255,255,0.05)' : 'transparent',
            textDecoration: 'none', fontWeight: 500,
          }}>{t(n.key)}</a>
        ))}
      </nav>
    )}
    <div style={{ flex: 1 }} />
    <LangSwitcher />
    {signedIn ? (
      <>
        <a href="/pricing" style={{
          display: 'flex', alignItems: 'center', gap: 6,
          padding: isMobile ? '5px 10px' : '6px 12px 6px 10px', borderRadius: 999,
          background: 'rgba(34,211,238,0.08)', border: '1px solid rgba(34,211,238,0.2)',
          fontSize: isMobile ? 12 : 13, color: '#b3eef9', fontVariantNumeric: 'tabular-nums',
          textDecoration: 'none',
        }}>
          <Icon name="coin" size={isMobile ? 12 : 14} color="var(--accent)" />
          <span>{credits.toLocaleString()}</span>
          {!isMobile && <><span style={{ color: 'var(--fg-3)', margin: '0 2px' }}>·</span><span style={{ color: 'var(--fg-2)', fontSize: 11 }}>{t('nav.credits')}</span></>}
        </a>
        {!isMobile && <button style={{
          width: 34, height: 34, borderRadius: 999, border: 'none',
          background: 'rgba(255,255,255,0.05)', color: 'var(--fg-1)',
          display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer',
        }}><Icon name="bell" size={16} /></button>}
        <a href="/profile" aria-label="个人中心" style={{
          width: 36, height: 36, borderRadius: 999,
          background: 'linear-gradient(135deg, #a78bfa, #22d3ee)',
          display: 'flex', alignItems: 'center', justifyContent: 'center',
          fontSize: 13, fontWeight: 700, color: '#0a0e1a', textDecoration: 'none',
          overflow: 'hidden',
        }}>{navUserAvatar ? <img src={navUserAvatar} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover' }} /> : navInitials}</a>
      </>
    ) : (
      <>
        {!isMobile && <a href="/login" className="yt-btn yt-btn-ghost" style={{ height: 34, textDecoration: 'none' }}>{t('nav.login')}</a>}
        <a href={isMobile ? '/login' : '/signup'} className="yt-btn yt-btn-primary" style={{ height: isMobile ? 32 : 34, padding: isMobile ? '0 14px' : '0 18px', fontSize: isMobile ? 12 : 14, textDecoration: 'none' }}>{isMobile ? t('nav.login') : t('nav.getStarted')}</a>
      </>
    )}
  </header>
  );
};

// Compact left-rail nav used on all NON-landing app screens.
// Inspired by Krea / Higgsfield / Recraft / Sora — narrow icon column,
// app-name at top, primary destinations as icon buttons, settings + avatar at the bottom.
const LeftRail = ({ active = 'home', credits: creditsProp, hideFooter = false }) => {
  const { t } = useI18n();
  const cachedCoins = window.YiteAPI ? (window.YiteAPI.getUser()?.coins ?? null) : null;
  const credits = creditsProp !== undefined ? creditsProp : (cachedCoins !== null ? cachedCoins : 0);

  const items = [
    { id: 'discover',  icon: 'compass',  key: 'nav.discover', href: '/discover' },
    { id: 'image',     icon: 'image',    key: 'nav.image',    href: '/create2img?tab=ref' },
    { id: 'video',     icon: 'video',    key: 'nav.video',    href: '/create2video?tab=ref' },
    { id: 'template',  icon: 'layers',   key: 'nav.template', href: '/template/_' },
    { id: 'assets',    icon: 'folder',   key: 'nav.assets',   href: '/assets' },
  ];
  const RailButton = ({ item }) => {
    const isActive = active === item.id;
    return (
      <button title={item.label} style={{
        position: 'relative',
        width: 44, height: 44, padding: 0,
        border: 'none', borderRadius: 10,
        background: isActive ? 'rgba(255,255,255,0.06)' : 'transparent',
        color: isActive ? 'var(--fg-0)' : 'var(--fg-2)',
        display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center',
        gap: 2, cursor: 'pointer', transition: 'all .12s',
      }}
        onMouseEnter={e => { if (!isActive) e.currentTarget.style.background = 'rgba(255,255,255,0.03)'; }}
        onMouseLeave={e => { if (!isActive) e.currentTarget.style.background = 'transparent'; }}>
        {isActive && <div style={{ position: 'absolute', left: -8, top: 10, width: 2, height: 24, borderRadius: 2, background: 'var(--accent)' }} />}
        <Icon name={item.icon} size={17} />
        <span style={{ fontSize: 11, fontWeight: 500, letterSpacing: 0 }}>{item.label}</span>
      </button>
    );
  };
  return (
    <aside style={{
      width: 76, flexShrink: 0,
      borderRight: '1px solid var(--line)',
      background: 'rgba(8,11,20,0.6)',
      backdropFilter: 'blur(20px)',
      display: 'flex', flexDirection: 'column', alignItems: 'center',
      padding: '16px 0 16px',
      position: 'sticky', top: 0, height: '100vh', alignSelf: 'flex-start',
      zIndex: 40,
    }}>
      {/* Logo mark */}
      <a href="/" style={{
        width: 38, height: 38, borderRadius: 10,
        overflow: 'hidden', display: 'flex', alignItems: 'center', justifyContent: 'center',
        marginBottom: 20,
      }}><img src="/frontend/icon/favicon-32x32.png" alt="yiteai.com" style={{ width: '100%', height: '100%', objectFit: 'cover' }} /></a>

      {/* Main nav */}
      <nav style={{ display: 'flex', flexDirection: 'column', gap: 4, flex: 1 }}>
        {items.map(it => (
          <a key={it.id} href={it.href} title={t(it.key)} style={{
            position: 'relative',
            width: 56, height: 52, padding: 0,
            borderRadius: 10,
            background: active === it.id ? 'rgba(255,255,255,0.06)' : 'transparent',
            color: active === it.id ? 'var(--fg-0)' : 'var(--fg-2)',
            display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center',
            gap: 3, cursor: 'pointer', transition: 'all .12s', textDecoration: 'none',
          }}
            onMouseEnter={e => { if (active !== it.id) e.currentTarget.style.background = 'rgba(255,255,255,0.03)'; }}
            onMouseLeave={e => { if (active !== it.id) e.currentTarget.style.background = 'transparent'; }}>
            {active === it.id && <div style={{ position: 'absolute', left: -10, top: 12, width: 2.5, height: 26, borderRadius: 2, background: 'var(--accent)' }} />}
            <Icon name={it.icon} size={19} />
            <span style={{ fontSize: 11, fontWeight: 500, letterSpacing: 0 }}>{t(it.key)}</span>
          </a>
        ))}
      </nav>

      {/* Footer — credits + settings + avatar */}
      {!hideFooter && <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 12, paddingTop: 12, borderTop: '1px solid var(--line)' }}>
        <div title={`${credits.toLocaleString()} ${t('nav.credits')}`} style={{
          fontSize: 12, fontFamily: 'var(--font-mono)',
          color: 'var(--accent)', fontWeight: 600,
          display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 3,
        }}>
          <Icon name="coin" size={15} color="var(--accent)" />
          {credits >= 1000 ? (credits / 1000).toFixed(1) + 'K' : credits}
        </div>
        <button title={t('nav.settings')} style={{
          width: 34, height: 34, padding: 0, border: 'none', background: 'transparent',
          color: 'var(--fg-3)', borderRadius: 8, cursor: 'pointer',
          display: 'flex', alignItems: 'center', justifyContent: 'center',
        }}><Icon name="settings" size={18} /></button>
        <a href="/profile" title={t('nav.profile')} style={{ textDecoration: 'none' }}>
          <div style={{
            width: 34, height: 34, borderRadius: '50%',
            background: 'linear-gradient(135deg, #a78bfa, #22d3ee)',
            display: 'flex', alignItems: 'center', justifyContent: 'center',
            fontSize: 12, fontWeight: 700, color: '#0a0e1a',
          }}>
            {window.YiteAPI?.getUser()?.nickname?.charAt(0)?.toUpperCase() || 'YT'}
          </div>
        </a>
      </div>}
    </aside>
  );
};

// Compact in-app top bar — sits to the right of LeftRail.
// Holds the screen title + a context-aware action area; the heavy-weight site nav
// (logged-out marketing nav) only lives on the landing page.
const AppHeader = ({ title, subtitle, action, breadcrumbs }) => (
  <header style={{
    height: 56, padding: '0 24px',
    display: 'flex', alignItems: 'center', gap: 16,
    borderBottom: '1px solid var(--line)',
    background: 'rgba(10,14,26,0.5)',
    backdropFilter: 'blur(20px)',
    position: 'sticky', top: 0, zIndex: 30,
  }}>
    <div style={{ flex: 1, minWidth: 0 }}>
      {breadcrumbs && (
        <div style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 12, color: 'var(--fg-3)', marginBottom: 1 }}>
          {breadcrumbs.map((b, i) => (
            <React.Fragment key={i}>
              {i > 0 && <Icon name="chevron" size={9} color="var(--fg-3)" />}
              <span style={{ color: i === breadcrumbs.length - 1 ? 'var(--fg-2)' : 'var(--fg-3)' }}>{b}</span>
            </React.Fragment>
          ))}
        </div>
      )}
      <div style={{ display: 'flex', alignItems: 'baseline', gap: 10 }}>
        <h1 style={{ fontSize: 15, fontWeight: 600, margin: 0, color: 'var(--fg-0)', letterSpacing: '-0.005em' }}>{title}</h1>
        {subtitle && <span style={{ fontSize: 12, color: 'var(--fg-3)' }}>{subtitle}</span>}
      </div>
    </div>
    {action}
  </header>
);

// Unified app shell: LeftRail (fixed) + top bar (user info, right-aligned) + work area.
const AppShell = ({ active, children }) => {
  const { t } = useI18n();
  const { isMobile } = useBreakpoint();
  const [user, setUser] = React.useState(() => window.YiteAPI?.getUser?.() || null);
  const userCoins = user?.coins ?? 0;
  const userInitial = computeInitials(user?.nickname || user?.email);
  const userAvatar = user?.avatar || (user?.avatar_oss_key ? (window.YiteAPI?.cdnDomain || '') + '/' + user.avatar_oss_key : '');
  const [menuOpen, setMenuOpen] = React.useState(false);
  const isLoggedIn = window.YiteAPI?.isLoggedIn?.() || false;

  React.useEffect(() => {
    const syncUser = () => setUser(window.YiteAPI?.getUser?.() || null);
    window.addEventListener('yt:user-updated', syncUser);
    window.addEventListener('storage', syncUser);
    return () => {
      window.removeEventListener('yt:user-updated', syncUser);
      window.removeEventListener('storage', syncUser);
    };
  }, []);

  // Google One Tap for non-logged-in users
  React.useEffect(() => {
    if (!isLoggedIn) initGoogleOneTap();
  }, [isLoggedIn]);

  // Close menu on outside click
  React.useEffect(() => {
    if (!menuOpen) return;
    const h = () => setMenuOpen(false);
    setTimeout(() => window.addEventListener('click', h), 0);
    return () => window.removeEventListener('click', h);
  }, [menuOpen]);

  const handleLogout = () => {
    if (window.YiteAPI) { window.YiteAPI.setToken(''); window.YiteAPI.setUser(null); }
    localStorage.removeItem('token');
    window.location.href = '/login';
  };

  // 移动端顶栏：左侧菜单（drawer with 访问历史） + 右侧"+"快捷创建
  const [historyOpen, setHistoryOpen] = React.useState(false);
  const [pageHistory, setPageHistory] = React.useState([]);
  React.useEffect(() => {
    try {
      const raw = localStorage.getItem('yt_page_history');
      const list = raw ? JSON.parse(raw) : [];
      // 当前页面入栈：去重 + 截断到 10 条
      const cur = { url: window.location.pathname + window.location.search, title: document.title || window.location.pathname, ts: Date.now() };
      const filtered = list.filter(x => x.url !== cur.url).slice(0, 9);
      const next = [cur, ...filtered];
      localStorage.setItem('yt_page_history', JSON.stringify(next));
      setPageHistory(next);
    } catch (e) {}
  }, []);

  return (
    <div className="yt" style={{ display: 'flex', width: '100%', height: '100vh', overflow: 'hidden', background: 'var(--bg-1)' }}>
      {!isMobile && <LeftRail active={active} hideFooter={true} />}
      <div style={{ flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
        {/* 移动端顶栏：左 hamburger + 右"+"创建 */}
        {isMobile && (
          <div style={{ height: 44, flexShrink: 0, borderBottom: '1px solid var(--line)', display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '0 8px', background: 'rgba(8,11,20,0.85)', backdropFilter: 'blur(12px)' }}>
            <button onClick={() => setHistoryOpen(true)} aria-label="历史菜单" style={{
              width: 36, height: 36, borderRadius: 8, border: 'none', background: 'transparent', color: 'var(--fg-1)',
              display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer',
            }}>
              <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
            </button>
            <div style={{ fontSize: 14, fontWeight: 600, color: 'var(--fg-2)', letterSpacing: '0.04em' }}>YITE AI</div>
            <a href="/create2img?tab=ref" aria-label="新建图片" style={{
              width: 36, height: 36, borderRadius: 8, background: 'linear-gradient(135deg, var(--accent), var(--accent-2))',
              display: 'flex', alignItems: 'center', justifyContent: 'center', textDecoration: 'none',
            }}>
              <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#052028" strokeWidth="2.5" strokeLinecap="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
            </a>
          </div>
        )}

        {/* History Drawer (mobile) */}
        {isMobile && (
          <MobileDrawer
            open={historyOpen}
            onClose={() => setHistoryOpen(false)}
            signedIn={isLoggedIn}
            pageHistory={pageHistory}
            onClearHistory={() => { localStorage.removeItem('yt_page_history'); setPageHistory(pageHistory.slice(0, 1)); }}
          />
        )}

        {/* Top bar — 桌面端显示；移动端用户信息已下移到底部"我的" tab */}
        {!isMobile && (
        <div style={{ height: 48, flexShrink: 0, borderBottom: '1px solid var(--line)', display: 'flex', alignItems: 'center', justifyContent: 'flex-end', padding: '0 20px', gap: 10, background: 'rgba(8,11,20,0.6)' }}>
          {/* Coins */}
          <a href="/pricing" style={{ textDecoration: 'none' }}>
            <div style={{ display: 'flex', alignItems: 'center', gap: 5, padding: '5px 12px', borderRadius: 8, background: 'rgba(34,211,238,0.06)', border: '1px solid rgba(34,211,238,0.15)', cursor: 'pointer' }}>
              <Icon name="coin" size={13} color="var(--accent)" />
              <span style={{ fontSize: 13, fontWeight: 600, color: 'var(--accent)', fontFamily: 'var(--font-mono)' }}>{userCoins >= 1000 ? (userCoins / 1000).toFixed(1) + 'K' : userCoins}</span>
              <span style={{ fontSize: 12, color: 'var(--fg-3)' }}>{t('common.credits')}</span>
            </div>
          </a>
          {/* Notification bell */}
          <button aria-label="通知" style={{ width: 36, height: 36, borderRadius: 8, border: 'none', background: 'transparent', color: 'var(--fg-3)', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', position: 'relative' }}
            onMouseEnter={e => e.currentTarget.style.background = 'rgba(255,255,255,0.04)'}
            onMouseLeave={e => e.currentTarget.style.background = 'transparent'}>
            <Icon name="bell" size={17} />
          </button>
          {/* User avatar + dropdown */}
          <div style={{ position: 'relative' }}>
            <button onClick={e => { e.stopPropagation(); setMenuOpen(!menuOpen); }}
              aria-label="用户菜单" aria-expanded={menuOpen} aria-haspopup="menu"
              style={{ width: 36, height: 36, borderRadius: '50%', border: 'none', padding: 0, cursor: 'pointer', overflow: 'hidden', background: 'linear-gradient(135deg,#a78bfa,#22d3ee)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
              {userAvatar ? (
                <img src={userAvatar} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }} />
              ) : (
                <span style={{ fontSize: 13, fontWeight: 700, color: '#0a0e1a' }}>{userInitial}</span>
              )}
            </button>
            {menuOpen && (
              <div style={{ position: 'absolute', top: '100%', right: 0, marginTop: 6, width: 200, background: '#1a1e2e', border: '1px solid var(--line-2)', borderRadius: 12, padding: 6, boxShadow: '0 12px 40px rgba(0,0,0,0.6)', zIndex: 100 }}
                onClick={e => e.stopPropagation()}>
                {/* User info */}
                <div style={{ padding: '10px 12px', display: 'flex', alignItems: 'center', gap: 10, borderBottom: '1px solid var(--line)', marginBottom: 4 }}>
                  <div style={{ width: 36, height: 36, borderRadius: '50%', overflow: 'hidden', background: 'linear-gradient(135deg,#a78bfa,#22d3ee)', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
                    {userAvatar ? <img src={userAvatar} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover' }} /> : <span style={{ fontSize: 13, fontWeight: 700, color: '#0a0e1a' }}>{userInitial}</span>}
                  </div>
                  <div style={{ minWidth: 0 }}>
                    <div style={{ fontSize: 13, fontWeight: 600, color: 'var(--fg-0)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{user?.nickname || 'User'}</div>
                    <div style={{ fontSize: 11, color: 'var(--fg-3)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{user?.email || ''}</div>
                  </div>
                </div>
                {/* Menu items */}
                {[
                  { label: t('shared.menu.profile'), icon: 'user', href: '/profile' },
                  { label: t('shared.menu.subscription'), icon: 'star', href: '/pricing' },
                  { label: t('shared.menu.orders'), icon: 'coin', href: '/orders' },
                  { label: t('shared.menu.settings'), icon: 'settings', href: '/profile#settings' },
                ].map(item => (
                  <a key={item.label} href={item.href} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '9px 12px', borderRadius: 8, textDecoration: 'none', color: 'var(--fg-1)', fontSize: 13, cursor: 'pointer', transition: 'background .1s' }}
                    onMouseEnter={e => e.currentTarget.style.background = 'rgba(255,255,255,0.06)'}
                    onMouseLeave={e => e.currentTarget.style.background = 'transparent'}>
                    <Icon name={item.icon} size={15} color="var(--fg-3)" />
                    {item.label}
                  </a>
                ))}
                <div style={{ height: 1, background: 'var(--line)', margin: '4px 8px' }} />
                <button onClick={handleLogout} style={{ width: '100%', display: 'flex', alignItems: 'center', gap: 10, padding: '9px 12px', borderRadius: 8, border: 'none', background: 'transparent', color: '#f87171', fontSize: 13, cursor: 'pointer', textAlign: 'left' }}
                  onMouseEnter={e => e.currentTarget.style.background = 'rgba(248,113,113,0.08)'}
                  onMouseLeave={e => e.currentTarget.style.background = 'transparent'}>
                  <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M9 21H5a2 2 0 01-2-2V5a2 2 0 012-2h4M16 17l5-5-5-5M21 12H9"/></svg>
                  {t('shared.menu.logout')}
                </button>
              </div>
            )}
          </div>
        </div>
        )}
        {/* Work area */}
        <div style={{ flex: 1, minHeight: 0, overflow: 'hidden', paddingBottom: isMobile ? 76 : 0 }}>
          {children}
        </div>
      </div>
      {/* Mobile bottom nav — 5 items, Discover 居中突出，最右侧"我的"展示用户信息 */}
      {isMobile && (
        <nav style={{
          position: 'fixed', bottom: 0, left: 0, right: 0, height: 64,
          background: 'rgba(8,11,20,0.95)', backdropFilter: 'blur(20px)',
          borderTop: '1px solid var(--line)',
          display: 'flex', alignItems: 'center', justifyContent: 'space-around',
          zIndex: 100, paddingBottom: 'env(safe-area-inset-bottom)',
        }}>
          {[
            { id: 'image',    icon: 'image',   key: 'nav.image',    href: '/create2img?tab=ref' },
            { id: 'video',    icon: 'video',   key: 'nav.video',    href: '/create2video?tab=ref' },
            { id: 'discover', icon: 'compass', key: 'nav.discover', href: '/discover', center: true },
            { id: 'template', icon: 'layers',  key: 'nav.template', href: '/template/_' },
            { id: 'profile',  icon: 'user',    label: isLoggedIn ? (t('shared.menu.profile') || '我的') : (t('nav.login') || '登录'), href: isLoggedIn ? '/profile' : '/login', isAvatar: isLoggedIn },
          ].map(item => {
            const isActive = active === item.id;

            // Center "Discover": 圆形渐变按钮，悬浮在 nav 上
            if (item.center) {
              return (
                <a key={item.id} href={item.href} style={{
                  display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 2,
                  textDecoration: 'none', flexShrink: 0,
                }}>
                  <div style={{
                    width: 48, height: 48, borderRadius: 999,
                    background: isActive
                      ? 'linear-gradient(135deg, var(--accent), var(--accent-2))'
                      : 'rgba(34,211,238,0.12)',
                    border: isActive ? 'none' : '1px solid rgba(34,211,238,0.3)',
                    display: 'flex', alignItems: 'center', justifyContent: 'center',
                    marginTop: -16,
                    boxShadow: isActive ? '0 4px 16px rgba(34,211,238,0.4)' : '0 2px 8px rgba(0,0,0,0.3)',
                    transition: 'all .15s',
                  }}>
                    <Icon name={item.icon} size={22} color={isActive ? '#052028' : 'var(--accent)'} />
                  </div>
                  <span style={{ fontSize: 11, fontWeight: 600, color: isActive ? 'var(--accent)' : 'var(--fg-2)' }}>{t(item.key)}</span>
                </a>
              );
            }

            // Rightmost "我的": 头像 + 标签
            if (item.isAvatar) {
              return (
                <a key={item.id} href={item.href} style={{
                  display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 3,
                  color: isActive ? 'var(--accent)' : 'var(--fg-3)',
                  textDecoration: 'none', fontSize: 11, fontWeight: 500,
                  padding: '6px 0', minWidth: 56,
                }}>
                  <div style={{
                    width: 26, height: 26, borderRadius: '50%', overflow: 'hidden',
                    background: 'linear-gradient(135deg,#a78bfa,#22d3ee)',
                    display: 'flex', alignItems: 'center', justifyContent: 'center',
                    border: isActive ? '2px solid var(--accent)' : '2px solid transparent',
                  }}>
                    {userAvatar ? (
                      <img src={userAvatar} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }} />
                    ) : (
                      <span style={{ fontSize: 11, fontWeight: 700, color: '#0a0e1a' }}>{userInitial}</span>
                    )}
                  </div>
                  <span>{item.label}</span>
                </a>
              );
            }

            // Regular nav items
            return (
              <a key={item.id} href={item.href} style={{
                display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 3,
                color: isActive ? 'var(--accent)' : 'var(--fg-3)',
                textDecoration: 'none', fontSize: 11, fontWeight: 500,
                padding: '6px 0', minWidth: 56,
              }}>
                <Icon name={item.icon} size={22} />
                <span>{t(item.key)}</span>
              </a>
            );
          })}
        </nav>
      )}
      <PWAInstallBanner />
    </div>
  );
};

// ─── Skeleton components ──────────────────────────────────────────────────────
const Skeleton = ({ width, height, style, className = '' }) => (
  <div className={`yt-skeleton ${className}`} style={{ width, height, ...style }} />
);

const SkeletonCard = ({ ratio = '4/3' }) => (
  <div className="yt-skeleton-card">
    <div className="yt-skeleton yt-skeleton-img" style={{ aspectRatio: ratio }} />
    <div style={{ padding: '10px 12px' }}>
      <div className="yt-skeleton yt-skeleton-text" style={{ width: '70%' }} />
      <div className="yt-skeleton yt-skeleton-text" style={{ width: '40%' }} />
    </div>
  </div>
);

const SkeletonGrid = ({ count = 10, cols = 'repeat(auto-fill, minmax(240px, 1fr))', ratio = '4/3' }) => (
  <div style={{ display: 'grid', gridTemplateColumns: cols, gap: 16 }}>
    {Array.from({ length: count }).map((_, i) => <SkeletonCard key={i} ratio={ratio} />)}
  </div>
);

const SkeletonBanner = ({ count = 3 }) => (
  <div style={{ display: 'flex', gap: 16 }}>
    {Array.from({ length: count }).map((_, i) => (
      <div key={i} className="yt-skeleton yt-skeleton-banner" style={{ flex: '0 0 calc(33.333% - 11px)', minWidth: 300 }} />
    ))}
  </div>
);

const SkeletonList = ({ count = 5 }) => (
  <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
    {Array.from({ length: count }).map((_, i) => (
      <div key={i} style={{ display: 'flex', gap: 12, padding: 12 }}>
        <div className="yt-skeleton" style={{ width: 64, height: 64, borderRadius: 8 }} />
        <div style={{ flex: 1 }}>
          <div className="yt-skeleton yt-skeleton-text" style={{ width: '50%' }} />
          <div className="yt-skeleton yt-skeleton-text" style={{ width: '80%' }} />
          <div className="yt-skeleton yt-skeleton-text" style={{ width: '30%' }} />
        </div>
      </div>
    ))}
  </div>
);

// ─── Image/Video with fallback ────────────────────────────────────────────────
const SafeImg = ({ src, alt = '', style, fallbackIcon = 'image', ...props }) => {
  const [err, setErr] = React.useState(false);
  if (err || !src) {
    return (
      <div className="yt-img-fallback" style={{ width: '100%', height: '100%', ...style }}>
        <Icon name={fallbackIcon} size={24} color="var(--fg-4)" />
      </div>
    );
  }
  return <img src={src} alt={alt} style={{ objectFit: 'cover', display: 'block', ...style }} onError={() => setErr(true)} {...props} />;
};

const SafeMedia = ({ src, type, style, ...props }) => {
  const isVideo = type === 'video' || /\.(mp4|mov|webm)(\?|$)/i.test(src || '');
  if (isVideo) {
    return <video src={src} muted playsInline preload="metadata" style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block', ...style }} {...props} />;
  }
  return <SafeImg src={src} style={{ width: '100%', height: '100%', ...style }} {...props} />;
};

// ─── Global dialog (replaces native confirm/alert/prompt) ────────────────────
// Usage: await ytConfirm('确认删除？')  → true/false
//        await ytAlert('操作成功')       → void
//        await ytPrompt('输入标题')      → string/null
const _dlgRoot = document.createElement('div');
document.body.appendChild(_dlgRoot);

const _renderDlg = (el) => { ReactDOM.createRoot(_dlgRoot).render(el); };
const _closeDlg = () => { ReactDOM.createRoot(_dlgRoot).render(null); };

const ytConfirm = (message, opts = {}) => new Promise(resolve => {
  const wt = window.t || (k => k);
  const { confirmText = wt('common.confirm'), cancelText = wt('common.cancel'), danger = false } = opts;
  const modalWidth = Math.min(400, window.innerWidth - 32);
  _renderDlg(
    <div style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.6)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 300, padding: '0 16px' }}>
      <div style={{ width: '100%', maxWidth: modalWidth, background: '#141722', border: '1px solid var(--line-2)', borderRadius: 14, padding: 24, boxShadow: '0 24px 60px rgba(0,0,0,0.6)' }}>
        <div style={{ fontSize: 15, color: 'var(--fg-0)', lineHeight: 1.6, marginBottom: 20 }}>{message}</div>
        <div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
          <button onClick={() => { _closeDlg(); resolve(false); }} style={{ height: 38, padding: '0 18px', borderRadius: 8, border: '1px solid var(--line-2)', background: 'transparent', color: 'var(--fg-1)', fontSize: 14, cursor: 'pointer' }}>{cancelText}</button>
          <button onClick={() => { _closeDlg(); resolve(true); }} style={{ height: 38, padding: '0 18px', borderRadius: 8, border: 'none', background: danger ? '#f87171' : 'var(--accent)', color: danger ? '#fff' : '#052028', fontSize: 14, fontWeight: 600, cursor: 'pointer' }}>{confirmText}</button>
        </div>
      </div>
    </div>
  );
});

const ytAlert = (message) => new Promise(resolve => {
  const wt = window.t || (k => k);
  const modalWidth = Math.min(400, window.innerWidth - 32);
  _renderDlg(
    <div style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.6)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 300, padding: '0 16px' }}>
      <div style={{ width: '100%', maxWidth: modalWidth, background: '#141722', border: '1px solid var(--line-2)', borderRadius: 14, padding: 24, boxShadow: '0 24px 60px rgba(0,0,0,0.6)' }}>
        <div style={{ fontSize: 15, color: 'var(--fg-0)', lineHeight: 1.6, marginBottom: 20 }}>{message}</div>
        <div style={{ display: 'flex', justifyContent: 'flex-end' }}>
          <button onClick={() => { _closeDlg(); resolve(); }} style={{ height: 38, padding: '0 18px', borderRadius: 8, border: 'none', background: 'var(--accent)', color: '#052028', fontSize: 14, fontWeight: 600, cursor: 'pointer' }}>{wt('common.confirm')}</button>
        </div>
      </div>
    </div>
  );
});

const ytPrompt = (message, defaultVal = '') => new Promise(resolve => {
  const wt = window.t || (k => k);
  const modalWidth = Math.min(400, window.innerWidth - 32);
  let val = defaultVal;
  const render = () => _renderDlg(
    <div style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.6)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 300, padding: '0 16px' }}>
      <div style={{ width: '100%', maxWidth: modalWidth, background: '#141722', border: '1px solid var(--line-2)', borderRadius: 14, padding: 24, boxShadow: '0 24px 60px rgba(0,0,0,0.6)' }}>
        <div style={{ fontSize: 15, color: 'var(--fg-0)', lineHeight: 1.6, marginBottom: 14 }}>{message}</div>
        <input autoFocus defaultValue={defaultVal} onChange={e => { val = e.target.value; }}
          onKeyDown={e => { if (e.key === 'Enter') { _closeDlg(); resolve(val.trim() || null); } if (e.key === 'Escape') { _closeDlg(); resolve(null); } }}
          className="yt-input" style={{ width: '100%', height: 42, fontSize: 14, marginBottom: 16 }} />
        <div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
          <button onClick={() => { _closeDlg(); resolve(null); }} style={{ height: 38, padding: '0 18px', borderRadius: 8, border: '1px solid var(--line-2)', background: 'transparent', color: 'var(--fg-1)', fontSize: 14, cursor: 'pointer' }}>{wt('common.cancel')}</button>
          <button onClick={() => { _closeDlg(); resolve(val.trim() || null); }} style={{ height: 38, padding: '0 18px', borderRadius: 8, border: 'none', background: 'var(--accent)', color: '#052028', fontSize: 14, fontWeight: 600, cursor: 'pointer' }}>{wt('common.confirm')}</button>
        </div>
      </div>
    </div>
  );
  render();
});

// ─── Empty state ──────────────────────────────────────────────────────────────
const EmptyState = ({ icon = 'image', title, subtitle, action }) => {
  const { t } = useI18n();
  const displayTitle = title !== undefined ? title : t('shared.empty.noContent');
  return (
    <div className="yt-fadein" style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: '60px 0', gap: 14 }}>
      <div style={{ width: 64, height: 64, borderRadius: 14, background: 'var(--surface)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
        <Icon name={icon} size={28} color="var(--fg-3)" />
      </div>
      <div style={{ fontSize: 15, fontWeight: 500, color: 'var(--fg-2)' }}>{displayTitle}</div>
      {subtitle && <div style={{ fontSize: 12, color: 'var(--fg-3)' }}>{subtitle}</div>}
      {action}
    </div>
  );
};

// ─── Publish modal (title + type selection before publishing) ─────────────────
const PublishModal = ({ onConfirm, onCancel }) => {
  const { t } = useI18n();
  const [title, setTitle] = React.useState('');
  const [pubType, setPubType] = React.useState('work');
  const [coverFile, setCoverFile] = React.useState(null);
  const [coverPreview, setCoverPreview] = React.useState(null);
  const coverRef = React.useRef(null);

  const handleCoverSelect = e => {
    const f = e.target.files?.[0]; e.target.value = '';
    if (!f) return;
    setCoverFile(f);
    const url = URL.createObjectURL(f);
    setCoverPreview(url);
  };
  const clearCover = () => { setCoverFile(null); setCoverPreview(null); };
  const isAnimated = coverFile && /\.(gif|webp|apng)$/i.test(coverFile.name);

  const TypeBtn = ({ value, label, desc, icon }) => {
    const active = pubType === value;
    return (
      <div onClick={() => setPubType(value)} style={{
        flex: 1, padding: '14px 16px', borderRadius: 10, cursor: 'pointer',
        border: active ? '2px solid var(--accent)' : '1px solid var(--line-2)',
        background: active ? 'rgba(34,211,238,0.06)' : 'rgba(255,255,255,0.02)',
        transition: 'all .12s',
      }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 6 }}>
          {icon}
          <span style={{ fontSize: 15, fontWeight: 600, color: active ? 'var(--fg-0)' : 'var(--fg-1)' }}>{label}</span>
        </div>
        <div style={{ fontSize: 12, color: 'var(--fg-3)', lineHeight: 1.5 }}>{desc}</div>
      </div>
    );
  };
  const modalWidth = Math.min(480, window.innerWidth - 32);
  return ReactDOM.createPortal(
    <div style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.6)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 300, padding: '0 16px' }}
      onClick={e => { if (e.target === e.currentTarget) onCancel(); }}>
      <div style={{ width: '100%', maxWidth: modalWidth, background: '#141722', border: '1px solid var(--line-2)', borderRadius: 14, padding: 24, boxShadow: '0 24px 60px rgba(0,0,0,0.6)' }}>
        <div style={{ fontSize: 17, fontWeight: 700, color: 'var(--fg-0)', marginBottom: 18 }}>{t('shared.publish.title')}</div>

        {/* Type selector */}
        <div style={{ display: 'flex', gap: 10, marginBottom: 18 }}>
          <TypeBtn value="work" label={t('shared.publish.work')}
            desc={t('shared.publish.workDesc')}
            icon={<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke={pubType === 'work' ? 'var(--accent)' : 'var(--fg-3)'} strokeWidth="2" strokeLinecap="round"><rect x="3" y="3" width="18" height="18" rx="2.5"/><circle cx="8.5" cy="8.5" r="1.5"/><path d="M21 15l-5-5L5 21"/></svg>} />
          <TypeBtn value="template" label={t('shared.publish.template')}
            desc={t('shared.publish.templateDesc')}
            icon={<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke={pubType === 'template' ? 'var(--accent)' : 'var(--fg-3)'} strokeWidth="2" strokeLinecap="round"><path d="M12 3l9 5-9 5-9-5 9-5z"/><path d="M3 13l9 5 9-5M3 18l9 5 9-5"/></svg>} />
        </div>

        {/* Title */}
        <input autoFocus value={title} onChange={e => setTitle(e.target.value)}
          onKeyDown={e => { if (e.key === 'Enter' && title.trim()) onConfirm(title.trim(), pubType === 'template', coverFile); if (e.key === 'Escape') onCancel(); }}
          placeholder={pubType === 'template' ? t('shared.publish.titlePlaceholder.template') : t('shared.publish.titlePlaceholder.work')}
          className="yt-input"
          style={{ width: '100%', marginBottom: 14, height: 44, fontSize: 15 }} />

        {/* Cover image (optional) */}
        <div style={{ marginBottom: 18 }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
            <span style={{ fontSize: 13, color: 'var(--fg-2)' }}>{t('shared.publish.customCover')}</span>
            <span style={{ fontSize: 11, color: 'var(--fg-3)' }}>（{t('shared.publish.coverHint')}）</span>
          </div>
          {coverPreview ? (
            <div style={{ position: 'relative', display: 'inline-block' }}>
              <img src={coverPreview} alt="" style={{ height: 80, borderRadius: 8, objectFit: 'cover', display: 'block' }} />
              {isAnimated && <span style={{ position: 'absolute', bottom: 4, left: 4, fontSize: 9, padding: '1px 5px', borderRadius: 3, background: 'rgba(0,0,0,0.7)', color: '#fff', fontWeight: 600 }}>{t('shared.publish.animated')}</span>}
              <button onClick={clearCover} style={{ position: 'absolute', top: -6, right: -6, width: 18, height: 18, borderRadius: '50%', background: 'rgba(0,0,0,0.8)', border: 'none', color: '#fff', fontSize: 10, cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>✕</button>
            </div>
          ) : (
            <button onClick={() => coverRef.current?.click()} style={{ height: 40, padding: '0 16px', borderRadius: 8, border: '1px dashed var(--line-2)', background: 'rgba(255,255,255,0.02)', color: 'var(--fg-3)', fontSize: 13, cursor: 'pointer', display: 'flex', alignItems: 'center', gap: 6 }}
              onMouseEnter={e => e.currentTarget.style.borderColor = 'var(--accent)'}
              onMouseLeave={e => e.currentTarget.style.borderColor = 'var(--line-2)'}>
              <Icon name="image" size={14} /> {t('shared.publish.selectCover')}
            </button>
          )}
          <input ref={coverRef} type="file" accept="image/jpeg,image/png,image/webp,image/gif" style={{ display: 'none' }} onChange={handleCoverSelect} />
        </div>

        <div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
          <button onClick={onCancel} style={{ height: 40, padding: '0 20px', borderRadius: 8, border: '1px solid var(--line-2)', background: 'transparent', color: 'var(--fg-1)', fontSize: 14, cursor: 'pointer' }}>{t('common.cancel')}</button>
          <button onClick={() => title.trim() && onConfirm(title.trim(), pubType === 'template', coverFile)} style={{ height: 40, padding: '0 20px', borderRadius: 8, border: 'none', background: 'linear-gradient(135deg, var(--accent), var(--accent-2))', color: '#052028', fontSize: 14, fontWeight: 600, cursor: 'pointer', opacity: title.trim() ? 1 : 0.5 }}>
            {pubType === 'template' ? t('shared.publish.publishTemplate') : t('shared.publish.publishWork')}
          </button>
        </div>
      </div>
    </div>,
    document.body
  );
};

// ─── Shared masonry card (used by discover + user-profile) ───────────────────
const InspirationCard = ({ tpl, isNew, isHot }) => {
  const { t } = useI18n();
  const isVideo = tpl.media_type === 'video';
  const [hover, setHover] = React.useState(false);
  const [imgLoaded, setImgLoaded] = React.useState(false);
  const [liked, setLiked] = React.useState(!!tpl.is_liked);
  const [likes, setLikes] = React.useState(tpl.like_count || 0);
  const [cdnDomain, setCdnDomain] = React.useState(() => _sharedCdnDomain || window.YiteAPI?.cdnDomain || '');
  const videoRef = React.useRef(null);
  const src = tpl.cover_url || tpl.result_url;
  const thumbSrc = _sharedThumbUrl(src, tpl.media_type, cdnDomain, 800);

  React.useEffect(() => {
    if (_sharedCdnDomain) return;
    _sharedFetchCdn().then(d => setCdnDomain(d));
  }, []);

  React.useEffect(() => {
    if (!isVideo || !videoRef.current) return;
    if (hover) { videoRef.current.play().catch(() => {}); }
    else { videoRef.current.pause(); videoRef.current.currentTime = 0; }
  }, [hover, isVideo]);

  const handleLike = (e) => {
    e.stopPropagation(); e.preventDefault();
    if (!window.YiteAPI?.isLoggedIn?.()) { showLoginModal(); return; }
    if (liked) {
      setLiked(false); setLikes(c => Math.max(0, c - 1));
      window.YiteAPI.templates.unlike(tpl.id).catch(() => { setLiked(true); setLikes(c => c + 1); });
    } else {
      setLiked(true); setLikes(c => c + 1);
      window.YiteAPI.templates.like(tpl.id).catch(() => { setLiked(false); setLikes(c => Math.max(0, c - 1)); });
    }
  };

  const userInitial = (tpl.user_nickname || tpl.nickname || '?')[0].toUpperCase();
  const showUser = tpl.user_id || tpl.user_nickname;
  const useCount = tpl.use_count || 0;
  const likeCount = tpl.like_count || 0;

  // 整张卡可点击：作品 → 创作页（带模板 ID）；模板 → 模板详情页
  const handleCardClick = () => {
    if (tpl.is_template) {
      window.location.href = '/template/' + tpl.id;
    } else {
      const base = tpl.media_type === 'video' ? '/create2video' : '/create2img';
      const tt = tpl.task_type || '';
      let tab = 'ref';
      if (tt.includes('文生')) tab = 'text';
      else if (tt.includes('开始') || tt.includes('结束')) tab = 'frames';
      window.location.href = base + '?tab=' + tab + '&from_template=' + tpl.id;
    }
  };
  const handleCardKey = (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handleCardClick(); } };

  return (
    <div
      role="button"
      tabIndex={0}
      onClick={handleCardClick}
      onKeyDown={handleCardKey}
      aria-label={tpl.title || tpl.prompt || (tpl.is_template ? 'Template' : 'Work')}
      className="yt-clickcard"
      style={{
        breakInside: 'avoid', marginBottom: 4,
        position: 'relative', overflow: 'hidden', cursor: 'pointer',
        borderRadius: 8, background: '#111',
      }}
      onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}>

      {/* Media — video: OSS JPEG (fast) or <video preload=metadata> as fallback (first frame) */}
      {isVideo ? (
        <div style={{ position: 'relative', width: '100%', background: '#111' }}>
          {!cdnDomain ? (
            // CDN not loaded yet: shimmer skeleton
            <div className="yt-skeleton" style={{ width: '100%', aspectRatio: '16/9' }} />
          ) : thumbSrc && thumbSrc !== src ? (
            // OSS snapshot JPEG available (URL was rewritten with x-oss-process)
            <>
              <img src={thumbSrc} alt="" loading="lazy" decoding="async"
                onLoad={() => setImgLoaded(true)}
                onError={e => { e.target.style.opacity = '0'; }}
                style={{ width: '100%', display: 'block', transition: 'transform .3s', transform: hover ? 'scale(1.03)' : 'scale(1)' }} />
              {!imgLoaded && <div className="yt-skeleton" style={{ width: '100%', aspectRatio: '16/9', position: 'absolute', inset: 0 }} />}
              {/* Video overlay — only rendered/loaded when hovering OSS-thumbnailed videos */}
              {hover && (
                <video src={src} muted loop playsInline preload="auto" autoPlay
                  style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', display: 'block', transition: 'transform .3s', transform: 'scale(1.03)' }} />
              )}
            </>
          ) : (
            // OSS processing not applicable (external CDN, or already had query params): <video preload=metadata>
            <video ref={videoRef} src={src} muted loop playsInline preload="metadata"
              onLoadedMetadata={() => setImgLoaded(true)}
              onError={e => { e.target.style.opacity = '0'; }}
              style={{ width: '100%', display: 'block', transition: 'transform .3s', transform: hover ? 'scale(1.03)' : 'scale(1)' }} />
          )}
        </div>
      ) : (
        <img src={thumbSrc || src} alt="" loading="lazy" decoding="async"
          onLoad={() => setImgLoaded(true)}
          onError={e => { e.target.style.display = 'none'; }}
          style={{ width: '100%', display: 'block', transition: 'transform .3s', transform: hover ? 'scale(1.03)' : 'scale(1)' }} />
      )}
      {!isVideo && !imgLoaded && <div className="yt-skeleton" style={{ width: '100%', aspectRatio: '4/3' }} />}

      {/* Video badge — prominent */}
      {isVideo && (
        <div style={{ position: 'absolute', top: 8, left: 8, display: 'flex', alignItems: 'center', gap: 4, padding: '3px 10px', borderRadius: 6, background: 'rgba(0,0,0,0.7)', backdropFilter: 'blur(4px)', fontSize: 11, color: '#fff', fontWeight: 600, pointerEvents: 'none', zIndex: 2 }}>
          <svg width="10" height="10" viewBox="0 0 12 12" fill="#fff"><path d="M2 1l9 5-9 5z"/></svg>
          {t('shared.badge.video')}
        </div>
      )}

      {/* NEW / HOT badge — top right */}
      {(isNew || isHot) && (
        <div style={{ position: 'absolute', top: 8, right: 8, padding: '3px 8px', borderRadius: 6, fontSize: 11, fontWeight: 700, letterSpacing: '0.03em', pointerEvents: 'none', zIndex: 2,
          background: isHot ? 'linear-gradient(135deg, #f97316, #ef4444)' : 'linear-gradient(135deg, #22d3ee, #6366f1)',
          color: '#fff', boxShadow: '0 2px 8px rgba(0,0,0,0.3)',
        }}>
          {isHot ? t('shared.badge.hot') : t('shared.badge.new')}
        </div>
      )}

      {/* Bottom gradient */}
      <div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: '50%', background: 'linear-gradient(transparent, rgba(0,0,0,0.75))', pointerEvents: 'none' }} />

      {/* Bottom info — user + title (left), stats (right) */}
      <div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, padding: '10px 12px', zIndex: 1 }}>
        <div style={{ display: 'flex', alignItems: 'flex-end', justifyContent: 'space-between', gap: 8 }}>
          {/* Left: user + title */}
          <div style={{ flex: 1, minWidth: 0 }}>
            {showUser && (
              <a href={'/u/' + tpl.user_id} onClick={e => e.stopPropagation()}
                style={{ display: 'inline-flex', alignItems: 'center', gap: 7, textDecoration: 'none', marginBottom: 4 }}>
                {tpl.user_avatar ? (
                  <img src={tpl.user_avatar} alt="" style={{ width: 26, height: 26, borderRadius: '50%', objectFit: 'cover', flexShrink: 0, border: '1.5px solid rgba(255,255,255,0.2)' }} />
                ) : (
                  <div style={{ width: 26, height: 26, borderRadius: '50%', background: 'linear-gradient(135deg, var(--accent), var(--accent-2))', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 11, fontWeight: 700, color: '#06080f', flexShrink: 0 }}>
                    {userInitial}
                  </div>
                )}
                <span style={{ fontSize: 13, color: '#fff', fontWeight: 600, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', textShadow: '0 1px 3px rgba(0,0,0,0.5)' }}>{tpl.user_nickname || 'User'}</span>
              </a>
            )}
            {(tpl.title || tpl.prompt) && (
              <div style={{ fontSize: 13, fontWeight: 500, color: 'rgba(255,255,255,0.85)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', textShadow: '0 1px 2px rgba(0,0,0,0.5)' }}>
                {tpl.title || tpl.prompt?.slice(0, 40) || ''}
              </div>
            )}
          </div>
          {/* Right: like + use count */}
          <div style={{ display: 'flex', alignItems: 'center', gap: 10, flexShrink: 0 }}>
            <button onClick={handleLike} style={{
              display: 'flex', alignItems: 'center', gap: 3, fontSize: 12, fontWeight: 500,
              color: liked ? '#f87171' : 'rgba(255,255,255,0.65)',
              background: 'none', border: 'none', cursor: 'pointer', padding: 0,
              transition: 'color .15s, transform .15s',
            }}
              onMouseEnter={e => e.currentTarget.style.transform = 'scale(1.1)'}
              onMouseLeave={e => e.currentTarget.style.transform = 'scale(1)'}>
              <svg width="14" height="14" viewBox="0 0 24 24" fill={liked ? 'currentColor' : 'none'} stroke="currentColor" strokeWidth={liked ? '0' : '2'}><path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/></svg>
              {likes >= 1000 ? (likes / 1000).toFixed(1) + 'k' : likes}
            </button>
            <span style={{ display: 'flex', alignItems: 'center', gap: 3, fontSize: 12, color: 'rgba(255,255,255,0.65)', fontWeight: 500 }}>
              <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M7 17l9.2-9.2M17 17V7H7"/></svg>
              {useCount >= 1000 ? (useCount / 1000).toFixed(1) + 'k' : useCount}
            </span>
          </div>
        </div>
      </div>

      {/* Hover overlay — 视觉提示（card 整体点击触发动作） */}
      {hover && (
        <div style={{
          position: 'absolute', inset: 0,
          background: 'rgba(0,0,0,0.45)',
          display: 'flex', alignItems: 'center', justifyContent: 'center',
          pointerEvents: 'none',
        }}>
          <div style={{
            height: 36, padding: '0 24px', borderRadius: 999,
            background: 'rgba(255,255,255,0.95)', color: '#000',
            fontSize: 14, fontWeight: 700,
            display: 'flex', alignItems: 'center',
            boxShadow: '0 4px 12px rgba(0,0,0,0.3)',
          }}>
            {t('shared.action.makeSame')}
          </div>
        </div>
      )}

      {/* Template modal */}
    </div>
  );
};

// ─── Google One Tap login ────────────────────────────────────────────────────
// Loads Google GIS script, fetches client_id from /api/v1/config,
// and shows One Tap popup for non-logged-in users.
const _googleOneTapInitialized = { current: false };

const initGoogleOneTap = () => {
  // Skip if already logged in or already initialized
  if (window.YiteAPI?.isLoggedIn?.()) return;
  if (_googleOneTapInitialized.current) return;
  _googleOneTapInitialized.current = true;

  // Define global callback before script loads
  window.__yiteGoogleOneTapCallback = async (response) => {
    if (!response || !response.credential) return;
    try {
      const res = await fetch('/api/v1/auth/google/onetap', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ credential: response.credential }),
      });
      const data = await res.json();
      if (!res.ok) throw new Error(data.error || 'Login failed');
      if (data.token && window.YiteAPI) {
        window.YiteAPI.setToken(data.token);
        if (data.user) window.YiteAPI.setUser(data.user);
        window.location.reload();
      }
    } catch (e) {
      console.error('[GoogleOneTap] callback error:', e);
    }
  };

  // Fetch config to get client_id, then load script
  fetch('/api/v1/config')
    .then(r => r.json())
    .then(cfg => {
      const clientId = cfg.google_client_id;
      if (!clientId) return; // no client_id configured, skip

      // Load Google GIS script
      const script = document.createElement('script');
      script.src = 'https://accounts.google.com/gsi/client';
      script.async = true;
      script.defer = true;
      script.onload = () => {
        try {
          window.google.accounts.id.initialize({
            client_id: clientId,
            callback: window.__yiteGoogleOneTapCallback,
            auto_select: true,
          });
          window.google.accounts.id.prompt();
        } catch (e) {
          console.error('[GoogleOneTap] initialize error:', e);
        }
      };
      script.onerror = () => {
        console.warn('[GoogleOneTap] failed to load GIS script');
      };
      document.head.appendChild(script);
    })
    .catch(e => {
      console.warn('[GoogleOneTap] failed to fetch config:', e);
    });
};

// React hook to trigger One Tap in components
const useGoogleOneTap = () => {
  React.useEffect(() => {
    if (!window.YiteAPI?.isLoggedIn?.()) {
      initGoogleOneTap();
    }
  }, []);
};

// ─── Login modal (global, replaces /login page redirect) ─────────────────────
const LoginModal = ({ onClose, onSuccess }) => {
  const { t } = useI18n();
  const [mode, setMode] = React.useState('login'); // 'login' | 'signup'
  const [email, setEmail] = React.useState('');
  const [password, setPassword] = React.useState('');
  const [nickname, setNickname] = React.useState('');
  const [loading, setLoading] = React.useState(false);
  const [error, setError] = React.useState('');

  const handleGoogleLogin = () => {
    // Redirect to Google OAuth (traditional flow as fallback)
    window.location.href = '/api/v1/auth/gmail?redirect=' + encodeURIComponent(window.location.href);
  };

  const handleSubmit = async () => {
    if (!email.trim() || !password.trim()) { setError(t('shared.login.emailRequired')); return; }
    if (mode === 'signup' && !nickname.trim()) { setError(t('shared.login.nicknameRequired') || t('shared.login.emailRequired')); return; }
    setLoading(true); setError('');
    try {
      const url = mode === 'login' ? '/api/v1/auth/login' : '/api/v1/auth/register';
      const body = mode === 'login'
        ? { email: email.trim(), password }
        : { email: email.trim(), password, nickname: nickname.trim() };
      const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }).then(r => r.json());
      if (res.error) { setError(res.error); setLoading(false); return; }
      if (res.token && window.YiteAPI) {
        window.YiteAPI.setToken(res.token);
        if (res.user) window.YiteAPI.setUser(res.user);
      }
      setLoading(false);
      if (onSuccess) onSuccess();
      else window.location.reload();
    } catch (e) { setError(e.message || t('common.error')); setLoading(false); }
  };

  const modalWidth = Math.min(420, window.innerWidth - 32);
  return ReactDOM.createPortal(
    <div style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.7)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 300, backdropFilter: 'blur(4px)', padding: '0 16px' }}
      onClick={e => { if (e.target === e.currentTarget && onClose) onClose(); }}>
      <div style={{ width: '100%', maxWidth: modalWidth, background: '#141722', border: '1px solid var(--line-2)', borderRadius: 18, padding: '36px 32px 28px', boxShadow: '0 24px 80px rgba(0,0,0,0.7)', position: 'relative' }}
        onClick={e => e.stopPropagation()}>
        {onClose && <button onClick={onClose} style={{ position: 'absolute', top: 14, right: 14, background: 'none', border: 'none', color: 'var(--fg-3)', cursor: 'pointer', fontSize: 18 }}>✕</button>}

        {/* Logo + title */}
        <div style={{ textAlign: 'center', marginBottom: 28 }}>
          <img src="/frontend/icon/favicon-32x32.png" alt="yiteai.com" style={{ width: 44, height: 44, borderRadius: 12, marginBottom: 14 }} />
          <div style={{ fontSize: 22, fontWeight: 700, color: 'var(--fg-0)' }}>{mode === 'login' ? t('shared.login.welcome') : t('shared.login.create')}</div>
          <div style={{ fontSize: 13, color: 'var(--fg-3)', marginTop: 6 }}>{mode === 'login' ? t('shared.login.subtitle') : t('shared.login.registerSub')}</div>
        </div>

        {/* Google button */}
        <button onClick={handleGoogleLogin} style={{
          width: '100%', height: 46, borderRadius: 10, border: '1px solid var(--line-2)',
          background: 'rgba(255,255,255,0.04)', color: 'var(--fg-0)', fontSize: 15, fontWeight: 500,
          cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 10,
          marginBottom: 20, transition: 'all .12s',
        }}
          onMouseEnter={e => { e.currentTarget.style.background = 'rgba(255,255,255,0.08)'; e.currentTarget.style.borderColor = 'var(--line-strong)'; }}
          onMouseLeave={e => { e.currentTarget.style.background = 'rgba(255,255,255,0.04)'; e.currentTarget.style.borderColor = 'var(--line-2)'; }}>
          <svg width="18" height="18" viewBox="0 0 24 24"><path d="M21.35 11.1H12v3.8h5.35c-.5 2.4-2.5 3.7-5.35 3.7-3.2 0-5.8-2.6-5.8-5.8s2.6-5.8 5.8-5.8c1.4 0 2.7.5 3.7 1.4l2.7-2.7C16.85 4 14.6 3 12 3 7 3 3 7 3 12s4 9 9 9c5.2 0 8.7-3.65 8.7-8.8 0-.4-.05-.8-.1-1.1z" fill="var(--fg-0)"/></svg>
          {t('shared.login.google')}
        </button>

        {/* Divider */}
        <div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 20 }}>
          <div style={{ flex: 1, height: 1, background: 'var(--line)' }} />
          <span style={{ fontSize: 12, color: 'var(--fg-3)' }}>{t('shared.login.orEmail')}</span>
          <div style={{ flex: 1, height: 1, background: 'var(--line)' }} />
        </div>

        {/* Form */}
        <div style={{ display: 'flex', flexDirection: 'column', gap: 16, marginBottom: 16 }}>
          {mode === 'signup' && (
            <input value={nickname} onChange={e => setNickname(e.target.value)} placeholder={t('shared.login.nickname') || 'Nickname'}
              className="yt-input" style={{ height: 44, fontSize: 14 }} />
          )}
          <input type="email" value={email} onChange={e => setEmail(e.target.value)} placeholder={t('shared.login.email')}
            onKeyDown={e => { if (e.key === 'Enter') handleSubmit(); }}
            className="yt-input" style={{ height: 44, fontSize: 14 }} />
          <input type="password" value={password} onChange={e => setPassword(e.target.value)} placeholder={t('shared.login.password')}
            onKeyDown={e => { if (e.key === 'Enter') handleSubmit(); }}
            className="yt-input" style={{ height: 44, fontSize: 14 }} />
        </div>

        {error && <div style={{ padding: '8px 12px', borderRadius: 8, background: 'rgba(248,113,113,0.1)', border: '1px solid rgba(248,113,113,0.3)', fontSize: 12, color: '#fca5a5', marginBottom: 12 }}>{error}</div>}

        <button onClick={handleSubmit} disabled={loading} style={{
          width: '100%', height: 46, borderRadius: 10, border: 'none',
          background: 'linear-gradient(135deg, var(--accent), var(--accent-2))',
          color: '#052028', fontSize: 15, fontWeight: 700, cursor: loading ? 'not-allowed' : 'pointer',
          opacity: loading ? 0.6 : 1, marginBottom: 16,
          boxShadow: '0 1px 0 rgba(255,255,255,.3) inset, 0 6px 16px rgba(34,211,238,.2)',
        }}>
          {loading ? t('common.processing') : mode === 'login' ? t('shared.login.login') : t('shared.login.register')}
        </button>

        {/* Toggle mode */}
        <div style={{ textAlign: 'center', fontSize: 13, color: 'var(--fg-3)' }}>
          {mode === 'login' ? (
            <>{t('shared.login.noAccount')} <button onClick={() => { setMode('signup'); setError(''); }} style={{ background: 'none', border: 'none', color: 'var(--accent)', cursor: 'pointer', fontSize: 13, fontWeight: 600 }}>{t('shared.login.signUpNow')}</button></>
          ) : (
            <>{t('shared.login.haveAccount')} <button onClick={() => { setMode('login'); setError(''); }} style={{ background: 'none', border: 'none', color: 'var(--accent)', cursor: 'pointer', fontSize: 13, fontWeight: 600 }}>{t('shared.login.loginNow')}</button></>
          )}
        </div>
        <div style={{ textAlign: 'center', fontSize: 11, color: 'var(--fg-4)', marginTop: 12 }}>
          {t('shared.login.agreeTo')} <a href="/terms" target="_blank" style={{ color: 'var(--fg-3)' }}>{t('auth.terms')}</a> {t('shared.login.and')} <a href="/privacy" target="_blank" style={{ color: 'var(--fg-3)' }}>{t('auth.privacy')}</a>
        </div>
      </div>
    </div>,
    document.body
  );
};

// Global helper: show login modal anywhere
// Usage: showLoginModal() → opens modal, returns Promise that resolves when logged in
let _loginModalResolver = null;
const _loginModalRoot = document.createElement('div');
document.body.appendChild(_loginModalRoot);

const showLoginModal = () => new Promise(resolve => {
  _loginModalResolver = resolve;
  const render = () => ReactDOM.createRoot(_loginModalRoot).render(
    <LoginModal
      onClose={() => { ReactDOM.createRoot(_loginModalRoot).render(null); resolve(false); }}
      onSuccess={() => { ReactDOM.createRoot(_loginModalRoot).render(null); resolve(true); }}
    />
  );
  render();
});

// ─── Footer (landing + legal pages) ──────────────────────────────────────────
const Footer = () => {
  const { t } = useI18n();
  const { isMobile } = useBreakpoint();
  return (
    <footer style={{ borderTop: '1px solid var(--line)', background: 'rgba(6,8,15,0.9)', padding: isMobile ? '32px 20px 24px' : '48px 28px 32px' }}>
      <div style={{ maxWidth: 1400, margin: '0 auto', display: 'grid', gridTemplateColumns: isMobile ? 'repeat(2, 1fr)' : 'repeat(4, 1fr)', gap: isMobile ? 24 : 40 }}>
        {/* Col 1: Brand */}
        <div>
          <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 14 }}>
            <img src="/frontend/icon/favicon-32x32.png" alt="" style={{ width: 28, height: 28, borderRadius: 6 }} />
            <span style={{ fontSize: 18, fontWeight: 700, color: 'var(--fg-0)' }}>yiteai<span style={{ color: "var(--accent)" }}>.com</span></span>
          </div>
          <p style={{ fontSize: 13, color: 'var(--fg-3)', lineHeight: 1.6, margin: 0 }}>{t('shared.footer.tagline')}</p>
        </div>
        {/* Col 2: Create */}
        <div>
          <div style={{ fontSize: 13, fontWeight: 600, color: 'var(--fg-0)', marginBottom: 14, textTransform: 'uppercase', letterSpacing: '0.05em' }}>{t('shared.footer.create')}</div>
          <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
            <a href="/create2img?tab=ref" style={{ fontSize: 13, color: 'var(--fg-2)', textDecoration: 'none' }}>{t('shared.footer.imgGen')}</a>
            <a href="/create2video?tab=ref" style={{ fontSize: 13, color: 'var(--fg-2)', textDecoration: 'none' }}>{t('shared.footer.vidGen')}</a>
          </div>
        </div>
        {/* Col 3: Discover */}
        <div>
          <div style={{ fontSize: 13, fontWeight: 600, color: 'var(--fg-0)', marginBottom: 14, textTransform: 'uppercase', letterSpacing: '0.05em' }}>{t('shared.footer.discover')}</div>
          <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
            <a href="/discover" style={{ fontSize: 13, color: 'var(--fg-2)', textDecoration: 'none' }}>{t('shared.footer.explore')}</a>
            <a href="/template/_" style={{ fontSize: 13, color: 'var(--fg-2)', textDecoration: 'none' }}>{t('shared.footer.templates')}</a>
          </div>
        </div>
        {/* Col 4: Company */}
        <div>
          <div style={{ fontSize: 13, fontWeight: 600, color: 'var(--fg-0)', marginBottom: 14, textTransform: 'uppercase', letterSpacing: '0.05em' }}>{t('shared.footer.company')}</div>
          <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
            <a href="/about" style={{ fontSize: 13, color: 'var(--fg-2)', textDecoration: 'none' }}>{t('shared.footer.about')}</a>
            <a href="/privacy" style={{ fontSize: 13, color: 'var(--fg-2)', textDecoration: 'none' }}>{t('shared.footer.privacy')}</a>
            <a href="/terms" style={{ fontSize: 13, color: 'var(--fg-2)', textDecoration: 'none' }}>{t('shared.footer.terms')}</a>
            <a href="/refund" style={{ fontSize: 13, color: 'var(--fg-2)', textDecoration: 'none' }}>{t('shared.footer.refund')}</a>
          </div>
        </div>
      </div>
      <div style={{ maxWidth: 1400, margin: '0 auto', paddingTop: 28, marginTop: 28, borderTop: '1px solid var(--line)', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
        <span style={{ fontSize: 12, color: 'var(--fg-4)' }}>© 2026 yiteai.com. All rights reserved.</span>
        <span style={{ fontSize: 12, color: 'var(--fg-4)' }}>support@yiteai.com</span>
      </div>
    </footer>
  );
};

// ─── PWA install prompt ─────────────────────────────────────────────────────
// Inject manifest link into <head> once
if (!document.querySelector('link[rel="manifest"]')) {
  const link = document.createElement('link');
  link.rel = 'manifest';
  link.href = '/frontend/manifest.json';
  document.head.appendChild(link);
  // Also set theme-color meta
  if (!document.querySelector('meta[name="theme-color"]')) {
    const meta = document.createElement('meta');
    meta.name = 'theme-color';
    meta.content = '#06080f';
    document.head.appendChild(meta);
  }
}

// Auto-claim PWA install reward when running in standalone mode
if (window.matchMedia('(display-mode: standalone)').matches && window.YiteAPI?.isLoggedIn?.()) {
  const claimed = localStorage.getItem('yt_pwa_claimed');
  if (!claimed) {
    fetch('/api/v1/user/pwa-install', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + (window.YiteAPI?.getToken?.() || '') },
    }).then(r => r.json()).then(d => {
      if (d.ok && !d.already_claimed && d.reward) {
        localStorage.setItem('yt_pwa_claimed', '1');
        ytAlert((window.i18n?.getLang?.() === 'zh' ? '恭喜！安装奖励 ' : 'Congratulations! Install reward: +') + d.reward + (window.i18n?.getLang?.() === 'zh' ? ' 额度已到账' : ' credits added'));
      } else {
        localStorage.setItem('yt_pwa_claimed', '1');
      }
    }).catch(() => {});
  }
}

// Capture the beforeinstallprompt event
let _deferredInstallPrompt = null;
window.addEventListener('beforeinstallprompt', e => {
  e.preventDefault();
  _deferredInstallPrompt = e;
});

const PWAInstallBanner = () => {
  const { t } = useI18n();
  const { isMobile } = useBreakpoint();
  const [show, setShow] = React.useState(false);
  const [isIOS, setIsIOS] = React.useState(false);

  React.useEffect(() => {
    // Don't show if already installed (standalone mode)
    if (window.matchMedia('(display-mode: standalone)').matches) return;
    // Don't show if dismissed recently (24h cooldown)
    const dismissed = localStorage.getItem('yt_pwa_dismissed');
    if (dismissed && Date.now() - Number(dismissed) < 86400000) return;

    const ua = navigator.userAgent || '';
    const ios = /iPad|iPhone|iPod/.test(ua) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
    setIsIOS(ios);

    // Show after 3 seconds on mobile
    if (isMobile) {
      const timer = setTimeout(() => setShow(true), 3000);
      return () => clearTimeout(timer);
    }
  }, [isMobile]);

  const handleInstall = async () => {
    if (_deferredInstallPrompt) {
      _deferredInstallPrompt.prompt();
      const result = await _deferredInstallPrompt.userChoice;
      if (result.outcome === 'accepted') setShow(false);
      _deferredInstallPrompt = null;
    }
  };

  const handleDismiss = () => {
    setShow(false);
    localStorage.setItem('yt_pwa_dismissed', String(Date.now()));
  };

  if (!show) return null;

  return (
    <div style={{
      position: 'fixed', bottom: isMobile ? 68 : 16, left: isMobile ? 12 : 'auto', right: isMobile ? 12 : 16,
      maxWidth: isMobile ? 'none' : 360, zIndex: 200,
      background: '#1a1e2e', border: '1px solid rgba(34,211,238,0.25)',
      borderRadius: 14, padding: '16px 18px', boxShadow: '0 12px 40px rgba(0,0,0,0.5)',
      display: 'flex', alignItems: 'center', gap: 12,
      animation: 'yt-fadein .3s ease',
    }}>
      <img src="/frontend/icon/android-chrome-192x192.png" alt="" style={{ width: 40, height: 40, borderRadius: 10, flexShrink: 0 }} />
      <div style={{ flex: 1, minWidth: 0 }}>
        <div style={{ fontSize: 14, fontWeight: 600, color: 'var(--fg-0)', marginBottom: 2 }}>
          {t('pwa.addToHome')}
        </div>
        <div style={{ fontSize: 12, color: 'var(--fg-3)', lineHeight: 1.4 }}>
          {isIOS ? t('pwa.iosHint') : t('pwa.quickAccess')}
        </div>
      </div>
      {!isIOS && _deferredInstallPrompt ? (
        <button onClick={handleInstall} style={{
          height: 34, padding: '0 14px', borderRadius: 8, border: 'none',
          background: 'var(--accent)', color: '#052028', fontSize: 13, fontWeight: 600,
          cursor: 'pointer', flexShrink: 0,
        }}>{t('pwa.install')}</button>
      ) : null}
      <button onClick={handleDismiss} style={{
        width: 24, height: 24, borderRadius: 6, border: 'none',
        background: 'rgba(255,255,255,0.06)', color: 'var(--fg-3)',
        fontSize: 14, cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center',
        flexShrink: 0,
      }}>✕</button>
    </div>
  );
};

// 带 localStorage 持久化的 useState，页面切走再回来时保留值
const usePersistentState = (key, defaultValue) => {
  const [value, setValue] = React.useState(() => {
    try {
      const raw = localStorage.getItem(key);
      return raw !== null ? JSON.parse(raw) : defaultValue;
    } catch { return defaultValue; }
  });
  React.useEffect(() => {
    try { localStorage.setItem(key, JSON.stringify(value)); } catch {}
  }, [key, value]);
  return [value, setValue];
};

Object.assign(window, {
  Logo, Icon, TopNav, LeftRail, AppHeader, AppShell, Footer,
  Skeleton, SkeletonCard, SkeletonGrid, SkeletonBanner, SkeletonList,
  SafeImg, SafeMedia, EmptyState, InspirationCard, PublishModal,
  ytConfirm, ytAlert, ytPrompt, LoginModal, showLoginModal,
  initGoogleOneTap, useGoogleOneTap,
  useI18n, useBreakpoint, useCarousel, Carousel, relativeTime, PWAInstallBanner,
  usePersistentState,
});
