/* eslint-disable */
// RScan Client Portal — App v3
// React + Babel-standalone, single-file mobile UI. Implements the design
// handoff (rc-client-portal) wired to the real Workers API. The chrome
// (login + change-pw) is rebuilt here so the whole experience lives inside
// one React tree.

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

// ───────────────────────── Design tokens ─────────────────────────
const TOKENS = {
  brand: '#0B66D6',
  brandSoft: '#E8F0FB',
  brandInk: '#063975',
  ink: '#0E1726',
  body: '#1F2A3A',
  muted: '#5B6878',
  mutedSoft: '#8C97A6',
  hairline: '#E5E8EE',
  hairlineSoft: '#EFF1F5',
  surface: '#FFFFFF',
  bg: '#F5F6F8',
  bgDeep: '#EEF0F4',
  success: '#1B9C6B',
  successSoft: '#E1F4EB',
  warn: '#C77B17',
  warnSoft: '#FBEFD9',
  danger: '#D14343',
  dangerSoft: '#FBE4E4',
  fontMono: '"SF Mono", "JetBrains Mono", ui-monospace, Menlo, monospace',
};

// ───────────────────────── Doc-type catalog ─────────────────────────
// Design has 4 types. Backend now accepts all 4 (plus legacy bank_statement
// kept for old records — normalized to bankbook on display).
const DOC_TYPES = {
  receipt:  { key: 'receipt',  labels: { ja: '領収書', zh: '收据',       en: 'Receipt' },        accent: '#0B66D6', bg: '#E8F0FB' },
  bankbook: { key: 'bankbook', labels: { ja: '通帳',   zh: '存折',       en: 'Bankbook' },       accent: '#1B9C6B', bg: '#E1F4EB' },
  invoice:  { key: 'invoice',  labels: { ja: '請求書', zh: '发票',       en: 'Invoice' },        accent: '#7A4ED6', bg: '#EEE7FB' },
  card:     { key: 'card',     labels: { ja: 'カード', zh: '信用卡账单', en: 'Card statement' }, accent: '#C77B17', bg: '#FBEFD9' },
};

// Normalize legacy backend values onto the design's 4 keys.
function normalizeCategory(cat) {
  if (cat === 'bank_statement') return 'bankbook';
  if (DOC_TYPES[cat]) return cat;
  return 'receipt';
}

// ───────────────────────── i18n strings ─────────────────────────
const STR = {
  ja: {
    upload: 'アップロード', history: '履歴', settings: '設定', done: '完了',
    camera: '写真またはビデオを撮る', gallery: 'フォトライブラリ', files: 'ファイルを選択',
    selectType: '書類の種類', supports: 'JPEG / PNG / PDF / HEIC',
    synced: '同期済み', uploading: 'アップロード中', failed: '失敗', queued: '待機中', retry: '再試行',
    rejected: '却下済み', approved: '受理済み', rejectReason: '却下理由', rejectedAt: '却下日時',
    resubmit: '再アップロード', resubmitHint: '修正した書類を選択してください。',
    today: '今日', yesterday: '昨日', thisMonth: '今月', files_n: '件',
    recent: '最近', total: '合計', account: 'アカウント',
    changePassword: 'パスワード変更', logout: 'ログアウト',
    language: '言語', search: '検索',
    cancel: 'キャンセル', uploadTo: 'アップロード先',
    login: 'ログイン', email: 'ユーザーID', password: 'パスワード',
    loginErr: 'ユーザーIDまたはパスワードが正しくありません',
    pwCurrent: '現在のパスワード', pwNew: '新しいパスワード', pwConfirm: '確認',
    pwHint: '8文字以上、英数字混在を推奨',
    pwMismatch: 'パスワードが一致しません', pwShort: '8文字以上で入力してください',
    update: '更新', loggedOut: 'ログアウトしました',
    noDocs: '該当する書類がありません',
    cpaNote: '書類は会計士から自動で配信されます。',
    annual: '年次決算書', yearend: '年末調整',
    annualSub: '担当会計士から送付', yearendSub: '源泉徴収票・申告書類',
    inProgress: '進行中', readyState: '受領可能', closedState: '確定済み',
    select: '選択', delete: '削除', uploadHistory: 'アップロード履歴',
    histAllDocs: '過去のすべての書類', annualHint: '会計年度ごとの決算書',
    yearendBadge: '受領済',
    emptyTitle: 'まだアップロードはありません',
    emptyHint: '右の領域をタップして、書類を選んでください。',
    receivedYou: '保存しました',
    cameraOnPhone: 'モバイルで開いてください',
  },
  zh: {
    upload: '上传', history: '历史', settings: '设置', done: '完成',
    camera: '拍照', gallery: '照片图库', files: '选取文件',
    selectType: '文档类型', supports: '支持 JPEG / PNG / PDF / HEIC',
    synced: '已同步', uploading: '上传中', failed: '失败', queued: '等待中', retry: '重试',
    rejected: '已拒绝', approved: '已受理', rejectReason: '拒绝原因', rejectedAt: '拒绝时间',
    resubmit: '重新上传', resubmitHint: '请选择修正后的文件。',
    today: '今天', yesterday: '昨天', thisMonth: '本月', files_n: '件',
    recent: '最近', total: '合计', account: '账号',
    changePassword: '修改密码', logout: '退出登录',
    language: '语言', search: '搜索',
    cancel: '取消', uploadTo: '上传到',
    login: '登录', email: '用户名', password: '密码',
    loginErr: '用户名或密码错误',
    pwCurrent: '当前密码', pwNew: '新密码', pwConfirm: '确认',
    pwHint: '建议至少 8 位，含字母和数字',
    pwMismatch: '密码不一致', pwShort: '密码至少 8 个字符',
    update: '更新', loggedOut: '已退出登录',
    noDocs: '没有匹配的文件',
    cpaNote: '文档由会计师自动推送。',
    annual: '年报', yearend: '年末调整',
    annualSub: '由会计师交付', yearendSub: '扣缴凭单与申报材料',
    inProgress: '进行中', readyState: '可领取', closedState: '已确定',
    select: '选择', delete: '删除', uploadHistory: '上传记录',
    histAllDocs: '查看所有已上传文件', annualHint: '按年度查看决算',
    yearendBadge: '已收到',
    emptyTitle: '暂无上传',
    emptyHint: '点按右侧卡片选择文件开始上传。',
    receivedYou: '已保存',
    cameraOnPhone: '请在手机上打开',
  },
  en: {
    upload: 'Upload', history: 'History', settings: 'Settings', done: 'Done',
    camera: 'Take Photo', gallery: 'Photo Library', files: 'Choose Files',
    selectType: 'Document type', supports: 'JPEG / PNG / PDF / HEIC',
    synced: 'Synced', uploading: 'Uploading', failed: 'Failed', queued: 'Queued', retry: 'Retry',
    rejected: 'Rejected', approved: 'Accepted', rejectReason: 'Reject reason', rejectedAt: 'Rejected at',
    resubmit: 'Resubmit', resubmitHint: 'Choose the corrected document.',
    today: 'Today', yesterday: 'Yesterday', thisMonth: 'This month', files_n: 'files',
    recent: 'Recent', total: 'Total', account: 'Account',
    changePassword: 'Change password', logout: 'Sign out',
    language: 'Language', search: 'Search',
    cancel: 'Cancel', uploadTo: 'Upload to',
    login: 'Sign In', email: 'User ID', password: 'Password',
    loginErr: 'Invalid user ID or password',
    pwCurrent: 'Current', pwNew: 'New', pwConfirm: 'Confirm',
    pwHint: 'At least 8 chars, mix letters & numbers',
    pwMismatch: 'Passwords do not match', pwShort: 'Must be at least 8 characters',
    update: 'Update', loggedOut: 'Signed out',
    noDocs: 'No documents match',
    cpaNote: 'Documents are pushed by your CPA.',
    annual: 'Annual report', yearend: 'Year-end adjustment',
    annualSub: 'Delivered by your CPA', yearendSub: 'Withholding & filing docs',
    inProgress: 'In progress', readyState: 'Ready', closedState: 'Closed',
    select: 'Select', delete: 'Delete', uploadHistory: 'Upload history',
    histAllDocs: 'All uploaded documents', annualHint: 'Statements by fiscal year',
    yearendBadge: 'Ready',
    emptyTitle: 'Nothing uploaded yet',
    emptyHint: 'Tap a card on the right to add a document.',
    receivedYou: 'Saved',
    cameraOnPhone: 'Open this on your phone',
  },
};

// ───────────────────────── Icons (inline SVG) ─────────────────────────
const I = {
  camera: (s=22)=> <svg width={s} height={s} viewBox="0 0 24 24" fill="none"><path d="M4 8.5A2.5 2.5 0 016.5 6h1.2L9 4h6l1.3 2h1.2A2.5 2.5 0 0120 8.5v9A2.5 2.5 0 0117.5 20h-11A2.5 2.5 0 014 17.5v-9z" stroke="currentColor" strokeWidth="1.6"/><circle cx="12" cy="13" r="3.5" stroke="currentColor" strokeWidth="1.6"/></svg>,
  gallery:(s=22)=> <svg width={s} height={s} viewBox="0 0 24 24" fill="none"><rect x="3.5" y="5.5" width="17" height="13" rx="2" stroke="currentColor" strokeWidth="1.6"/><circle cx="8.5" cy="10" r="1.5" fill="currentColor"/><path d="M3.5 16.5l4.5-4 4 3.5 3-2.5 5 4" stroke="currentColor" strokeWidth="1.6" strokeLinejoin="round"/></svg>,
  files:  (s=22)=> <svg width={s} height={s} viewBox="0 0 24 24" fill="none"><path d="M6 4h7l5 5v10a2 2 0 01-2 2H6a2 2 0 01-2-2V6a2 2 0 012-2z" stroke="currentColor" strokeWidth="1.6" strokeLinejoin="round"/><path d="M13 4v5h5" stroke="currentColor" strokeWidth="1.6" strokeLinejoin="round"/></svg>,
  check:  (s=14)=> <svg width={s} height={s} viewBox="0 0 14 14" fill="none"><path d="M2.5 7.5l3 3 6-7" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/></svg>,
  chevron:(s=14)=> <svg width={s} height={s} viewBox="0 0 14 14" fill="none"><path d="M5 2l5 5-5 5" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"/></svg>,
  search: (s=18)=> <svg width={s} height={s} viewBox="0 0 18 18" fill="none"><circle cx="8" cy="8" r="5" stroke="currentColor" strokeWidth="1.6"/><path d="M12 12l3.5 3.5" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round"/></svg>,
  close:  (s=18)=> <svg width={s} height={s} viewBox="0 0 18 18" fill="none"><path d="M4 4l10 10M14 4L4 14" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round"/></svg>,
  plus:   (s=18)=> <svg width={s} height={s} viewBox="0 0 18 18" fill="none"><path d="M9 3v12M3 9h12" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round"/></svg>,
  cloud:  (s=16)=> <svg width={s} height={s} viewBox="0 0 16 16" fill="none"><path d="M5 12h7a3 3 0 00.4-5.97A4.5 4.5 0 004 7.2 3 3 0 005 12z" stroke="currentColor" strokeWidth="1.4" strokeLinejoin="round"/><path d="M7 8.5l1.5 1.5L11 7" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round"/></svg>,
  spinner:(s=16)=> <svg width={s} height={s} viewBox="0 0 16 16" fill="none" style={{animation:'rcSpin 1s linear infinite'}}><circle cx="8" cy="8" r="6" stroke="currentColor" strokeWidth="2" strokeOpacity="0.2"/><path d="M14 8a6 6 0 00-6-6" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/></svg>,
  warn:   (s=16)=> <svg width={s} height={s} viewBox="0 0 16 16" fill="none"><path d="M8 1.5l7 12.5H1L8 1.5z" stroke="currentColor" strokeWidth="1.5" strokeLinejoin="round"/><path d="M8 6v3.5M8 11.5v.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/></svg>,
  back:   (s=22)=> <svg width={s} height={s} viewBox="0 0 24 24" fill="none"><path d="M14 5l-7 7 7 7" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"/></svg>,
  doc:    (s=22)=> <svg width={s} height={s} viewBox="0 0 24 24" fill="none"><path d="M7 3h7l4 4v12a2 2 0 01-2 2H7a2 2 0 01-2-2V5a2 2 0 012-2z" stroke="currentColor" strokeWidth="1.5" strokeLinejoin="round"/><path d="M14 3v4h4" stroke="currentColor" strokeWidth="1.5" strokeLinejoin="round"/><path d="M8 11h8M8 14h8M8 17h5" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round"/></svg>,
  bankbook:(s=22)=> <svg width={s} height={s} viewBox="0 0 24 24" fill="none"><rect x="3.5" y="5" width="17" height="14" rx="1.5" stroke="currentColor" strokeWidth="1.5"/><path d="M12 5v14" stroke="currentColor" strokeWidth="1.2"/><path d="M6 9h3M6 12h3M15 9h3M15 12h3" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round"/></svg>,
  cardIcon:(s=22)=> <svg width={s} height={s} viewBox="0 0 24 24" fill="none"><rect x="3" y="6" width="18" height="13" rx="2" stroke="currentColor" strokeWidth="1.5"/><path d="M3 10h18" stroke="currentColor" strokeWidth="1.5"/><path d="M6 15h4" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round"/></svg>,
  logo:   (s=28)=> <svg width={s} height={s} viewBox="0 0 24 24" fill="none"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" stroke="currentColor" strokeWidth="1.7" strokeLinejoin="round"/><path d="M14 2v6h6" stroke="currentColor" strokeWidth="1.7" strokeLinejoin="round"/><path d="M8 13h8M8 17h6" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/></svg>,
};

function docTypeIcon(type, size=22) {
  if (type === 'bankbook') return I.bankbook(size);
  if (type === 'card')     return I.cardIcon(size);
  return I.doc(size);
}

// ───────────────────────── Helpers ─────────────────────────
function formatDateHeader(iso, lang) {
  const d = new Date(iso);
  const y = d.getFullYear(), m = d.getMonth()+1, day = d.getDate();
  if (lang === 'en') {
    const months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
    return `${months[m-1]} ${day}, ${y}`;
  }
  return `${y}年${m}月${day}日`;
}

function groupByDate(items) {
  const map = new Map();
  for (const it of items) {
    if (!map.has(it.date)) map.set(it.date, []);
    map.get(it.date).push(it);
  }
  return Array.from(map.entries()).sort((a, b) => b[0].localeCompare(a[0]));
}

function statusStyle(status) {
  switch (status) {
    case 'synced':    return { bg: TOKENS.successSoft, fg: TOKENS.success };
    case 'approved':  return { bg: TOKENS.successSoft, fg: TOKENS.success };
    case 'rejected':  return { bg: TOKENS.dangerSoft, fg: TOKENS.danger };
    case 'uploading': return { bg: TOKENS.brandSoft, fg: TOKENS.brand };
    case 'queued':    return { bg: TOKENS.hairlineSoft, fg: TOKENS.muted };
    case 'failed':    return { bg: TOKENS.dangerSoft, fg: TOKENS.danger };
    default:          return { bg: TOKENS.hairlineSoft, fg: TOKENS.muted };
  }
}

function formatFileSize(bytes) {
  if (!bytes) return '—';
  if (bytes < 1024) return bytes + ' B';
  if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(0) + ' KB';
  return (bytes / 1024 / 1024).toFixed(1) + ' MB';
}

function effectiveStatus(row) {
  if (row.review_status === 'rejected') return 'rejected';
  if (row.review_status === 'approved') return 'approved';
  return row.status || 'queued';
}

function statusLabel(status, t) {
  if (status === 'rejected') return t.rejected;
  if (status === 'approved') return t.approved;
  if (status === 'synced') return t.synced;
  if (status === 'failed') return t.failed;
  if (status === 'uploading') return t.uploading;
  return t.queued;
}

// Coerce an API history row into the design's item shape.
function adaptHistoryItem(row) {
  const uploadedAt = new Date(row.uploaded_at);
  const status = effectiveStatus(row);
  return {
    id: row.id,
    name: row.file_name,
    type: normalizeCategory(row.category),
    size: formatFileSize(row.file_size),
    date: row.uploaded_at.slice(0, 10),
    time: uploadedAt.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
    status,
    reviewStatus: row.review_status || null,
    rejectReason: row.reject_reason || null,
    rejectedAt: row.rejected_at || null,
    replacesInboxItemId: row.replaces_inbox_item_id || null,
    raw: row,
  };
}

// Pick the camera/gallery/files <input> accept attribute for a source.
function inputAttrsForSource(src) {
  if (src === 'camera') return { accept: 'image/*', capture: 'environment' };
  if (src === 'gallery') return { accept: 'image/*', multiple: true };
  return { accept: 'image/*,application/pdf,.pdf,.heic,.heif', multiple: true };
}

// ───────────────────────── Toast ─────────────────────────
function Toast({ msg, type='success' }) {
  if (!msg) return null;
  const colors = type === 'success'
    ? { bg: TOKENS.ink, fg: '#fff', icon: TOKENS.success }
    : { bg: TOKENS.danger, fg: '#fff', icon: '#fff' };
  return (
    <div style={{
      position:'fixed', top:24, left:'50%', transform:'translateX(-50%)', zIndex:200,
      background:colors.bg, color:colors.fg, padding:'10px 14px', borderRadius:999,
      fontSize:13, fontWeight:600, display:'flex', alignItems:'center', gap:8,
      boxShadow:'0 6px 20px rgba(0,0,0,0.25)', animation:'rcToast 2.4s ease both',
      whiteSpace:'nowrap', maxWidth:'90%',
    }}>
      <span style={{color:colors.icon, display:'flex'}}>{type === 'success' ? I.check(14) : I.warn(14)}</span>
      {msg}
    </div>
  );
}

// ───────────────────────── Login screen ─────────────────────────
function LoginScreen({ lang, onSuccess }) {
  const t = STR[lang];
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [err, setErr] = useState('');
  const [busy, setBusy] = useState(false);

  async function submit(e) {
    e.preventDefault();
    setErr('');
    setBusy(true);
    try {
      const data = await api.login(email.trim(), password);
      api.setToken(data.token);
      localStorage.setItem('token', data.token);
      localStorage.setItem('user', JSON.stringify(data.user));
      onSuccess(data.user);
    } catch (e) {
      setErr(t.loginErr);
    } finally {
      setBusy(false);
    }
  }

  return (
    <div style={{
      minHeight:'100vh', display:'flex', alignItems:'center', justifyContent:'center',
      padding:'24px 20px', background:TOKENS.bg,
    }}>
      <div style={{
        width:'100%', maxWidth:380, padding:'32px 24px 28px', background:TOKENS.surface,
        borderRadius:24, boxShadow:'0 14px 50px rgba(15,30,60,0.08)', border:'1px solid '+TOKENS.hairline,
      }}>
        <div style={{display:'flex', alignItems:'center', gap:12, marginBottom:24}}>
          <div style={{
            width:48, height:48, borderRadius:14,
            background:`linear-gradient(140deg, ${TOKENS.brand}, ${TOKENS.brandInk})`,
            color:'#fff', display:'flex', alignItems:'center', justifyContent:'center',
          }}>{I.logo(26)}</div>
          <div>
            <div style={{fontSize:11, color:TOKENS.muted, fontWeight:600, letterSpacing:0.4, textTransform:'uppercase'}}>RScan Portal</div>
            <div style={{fontSize:18, fontWeight:700, color:TOKENS.ink, marginTop:2}}>{t.login}</div>
          </div>
        </div>
        <form onSubmit={submit} autoComplete="on">
          <div style={{display:'flex', flexDirection:'column', gap:10, marginBottom:14}}>
            <input
              type="email" value={email} onChange={e=>setEmail(e.target.value)}
              placeholder={t.email} autoComplete="email" required
              style={{
                padding:'14px 14px', borderRadius:12, border:'1px solid '+TOKENS.hairline,
                background:TOKENS.bg, fontSize:15, color:TOKENS.ink, outline:'none',
              }}
            />
            <input
              type="password" value={password} onChange={e=>setPassword(e.target.value)}
              placeholder={t.password} autoComplete="current-password" required
              style={{
                padding:'14px 14px', borderRadius:12, border:'1px solid '+TOKENS.hairline,
                background:TOKENS.bg, fontSize:15, color:TOKENS.ink, outline:'none',
              }}
            />
          </div>
          {err && <div className="rc-shake" style={{
            padding:'10px 14px', borderRadius:10, background:TOKENS.dangerSoft, color:TOKENS.danger,
            fontSize:12, fontWeight:600, marginBottom:12, display:'flex', alignItems:'center', gap:8,
          }}>{I.warn(14)} {err}</div>}
          <button type="submit" disabled={busy} className="rc-pressable" style={{
            width:'100%', padding:'14px', borderRadius:12, border:'none', cursor:'pointer',
            background: busy ? TOKENS.mutedSoft : TOKENS.brand, color:'#fff',
            fontSize:15, fontWeight:700, display:'flex', alignItems:'center', justifyContent:'center', gap:8,
          }}>
            {busy && I.spinner(16)}
            {t.login}
          </button>
        </form>
      </div>
    </div>
  );
}

// ───────────────────────── First-login change-pw screen ─────────────────────────
function ChangePwScreen({ lang, onSuccess, onCancel }) {
  const t = STR[lang];
  const [cur, setCur] = useState('');
  const [nw, setNw] = useState('');
  const [cnf, setCnf] = useState('');
  const [err, setErr] = useState('');
  const [busy, setBusy] = useState(false);

  async function submit(e) {
    e.preventDefault();
    setErr('');
    if (nw.length < 8) { setErr(t.pwShort); return; }
    if (nw !== cnf) { setErr(t.pwMismatch); return; }
    setBusy(true);
    try {
      await api.changePassword(cur, nw);
      const user = JSON.parse(localStorage.getItem('user') || '{}');
      user.must_change_pw = false;
      localStorage.setItem('user', JSON.stringify(user));
      onSuccess(user);
    } catch (e) {
      setErr(e.message || 'error');
    } finally {
      setBusy(false);
    }
  }

  return (
    <div style={{
      minHeight:'100vh', display:'flex', alignItems:'center', justifyContent:'center',
      padding:'24px 20px', background:TOKENS.bg,
    }}>
      <div style={{
        width:'100%', maxWidth:380, padding:'24px', background:TOKENS.surface,
        borderRadius:24, boxShadow:'0 14px 50px rgba(15,30,60,0.08)', border:'1px solid '+TOKENS.hairline,
      }}>
        <div style={{fontSize:11, color:TOKENS.muted, fontWeight:600, letterSpacing:0.4, textTransform:'uppercase'}}>RScan</div>
        <div style={{fontSize:20, fontWeight:700, color:TOKENS.ink, marginTop:2, marginBottom:18}}>{t.changePassword}</div>
        <form onSubmit={submit} autoComplete="off">
          <div style={{background:TOKENS.bg, borderRadius:14, border:'1px solid '+TOKENS.hairline, overflow:'hidden'}}>
            {[
              { v: cur, set: setCur, label: t.pwCurrent, ac: 'current-password' },
              { v: nw, set: setNw, label: t.pwNew, ac: 'new-password' },
              { v: cnf, set: setCnf, label: t.pwConfirm, ac: 'new-password' },
            ].map((f, i) => (
              <div key={i} style={{display:'flex', flexDirection:'column', padding:'10px 14px', borderTop: i ? '1px solid '+TOKENS.hairlineSoft : 'none'}}>
                <span style={{fontSize:11, color:TOKENS.muted, fontWeight:600}}>{f.label}</span>
                <input type="password" value={f.v} onChange={e=>f.set(e.target.value)} autoComplete={f.ac} required
                  placeholder="••••••••"
                  style={{marginTop:4, fontSize:15, color:TOKENS.ink, border:'none', outline:'none', background:'transparent', padding:0, fontFamily:TOKENS.fontMono}}
                />
              </div>
            ))}
          </div>
          <div style={{marginTop:10, padding:'10px 14px', borderRadius:10, background:TOKENS.warnSoft, color:TOKENS.warn, fontSize:11, display:'flex', gap:8, alignItems:'flex-start'}}>
            <span style={{display:'flex', flexShrink:0, paddingTop:1}}>{I.warn(14)}</span>
            <span>{t.pwHint}</span>
          </div>
          {err && <div className="rc-shake" style={{marginTop:10, padding:'10px 14px', borderRadius:10, background:TOKENS.dangerSoft, color:TOKENS.danger, fontSize:12, fontWeight:600}}>{err}</div>}
          <button type="submit" disabled={busy} className="rc-pressable" style={{
            width:'100%', marginTop:14, padding:'14px', borderRadius:12, border:'none', cursor:'pointer',
            background: busy ? TOKENS.mutedSoft : TOKENS.brand, color:'#fff', fontSize:15, fontWeight:700,
            display:'flex', alignItems:'center', justifyContent:'center', gap:8,
          }}>
            {busy && I.spinner(16)}
            {t.update}
          </button>
          {onCancel && (
            <button type="button" onClick={onCancel} className="rc-pressable" style={{
              width:'100%', marginTop:8, padding:'12px', borderRadius:12, border:'none', background:'transparent', color:TOKENS.muted, fontSize:13, fontWeight:600, cursor:'pointer',
            }}>{t.cancel}</button>
          )}
        </form>
      </div>
    </div>
  );
}

// ───────────────────────── Upload home (the primary screen) ─────────────────────────
function UploadHome({ lang, t, user, history, monthCounts, totalMonth, queue, onOpenMenu, onPickType, onRetry, onClearDone, onOpenDetail, onGoHistory }) {
  const types = ['receipt', 'bankbook', 'invoice', 'card'];
  const recent = history.slice(0, 4);
  const stats = types.map(type => ({ type, n: history.filter(it => it.type === type).length }));
  const total = stats.reduce((s, x) => s + x.n, 0);

  const now = new Date();
  const periodLabel = lang === 'ja'
    ? `${now.getFullYear()}年${now.getMonth()+1}月`
    : lang === 'zh'
    ? `${now.getFullYear()}年${now.getMonth()+1}月`
    : `${now.toLocaleString('en', { month: 'short' })} ${now.getFullYear()}`;

  const companyLabel = (user?.company_id || 'rscan').toLowerCase();

  return (
    <div style={{flex:1, overflow:'auto', padding:'0 16px 24px'}} className="rc-no-scrollbar">
      {/* Header */}
      <div style={{display:'flex', alignItems:'center', justifyContent:'space-between', padding:'4px 0 14px'}}>
        <div>
          <div style={{fontSize:11, fontWeight:600, color:TOKENS.muted, letterSpacing:0.4, textTransform:'uppercase'}}>
            {companyLabel} · {periodLabel}
          </div>
          <div style={{fontSize:24, fontWeight:700, color:TOKENS.ink, marginTop:2, letterSpacing:-0.2}}>{t.upload}</div>
        </div>
        <button onClick={onOpenMenu} style={{
          width:40, height:40, borderRadius:12, border:'1px solid '+TOKENS.hairline,
          background:TOKENS.surface, color:TOKENS.muted, display:'flex', alignItems:'center', justifyContent:'center', cursor:'pointer',
        }} className="rc-pressable" aria-label="menu">
          <svg width="20" height="20" viewBox="0 0 20 20" fill="none">
            <circle cx="10" cy="4" r="1.6" fill="currentColor"/><circle cx="10" cy="10" r="1.6" fill="currentColor"/><circle cx="10" cy="16" r="1.6" fill="currentColor"/>
          </svg>
        </button>
      </div>

      {/* Section label */}
      <div style={{display:'flex', justifyContent:'space-between', alignItems:'baseline', padding:'0 2px 8px'}}>
        <span style={{fontSize:11, color:TOKENS.muted, fontWeight:600, letterSpacing:0.4, textTransform:'uppercase'}}>{t.selectType}</span>
        <span style={{fontSize:11, color:TOKENS.mutedSoft}}>{t.supports}</span>
      </div>

      {/* 2×2 doc-type grid */}
      <div style={{display:'grid', gridTemplateColumns:'1fr 1fr', gap:8, marginBottom:14}}>
        {types.map(k => {
          const dt = DOC_TYPES[k];
          return (
            <button key={k} onClick={() => onPickType(k)} className="rc-pressable" style={{
              minHeight:116, borderRadius:16, padding:14, position:'relative', overflow:'hidden',
              border:`1px solid ${dt.accent}33`, background:dt.bg, color:dt.accent, textAlign:'left',
              display:'flex', flexDirection:'column', justifyContent:'space-between', gap:8, cursor:'pointer',
            }}>
              <div style={{position:'absolute', inset:0, opacity:0.4, backgroundImage:`repeating-linear-gradient(135deg, ${dt.accent}14 0 1px, transparent 1px 9px)`}}/>
              <div style={{position:'relative', display:'flex', justifyContent:'space-between', alignItems:'flex-start'}}>
                <div style={{width:38, height:38, borderRadius:10, background:'#fff', display:'flex', alignItems:'center', justifyContent:'center', boxShadow:`0 1px 2px ${dt.accent}26`}}>
                  {docTypeIcon(k, 22)}
                </div>
                {monthCounts[k] > 0 && (
                  <div style={{padding:'3px 8px', borderRadius:999, background:'#fff', fontSize:11, fontWeight:700, color:dt.accent, fontFeatureSettings:'"tnum"', boxShadow:`0 1px 2px ${dt.accent}1a`}}>
                    {monthCounts[k]}
                  </div>
                )}
              </div>
              <div style={{position:'relative'}}>
                <div style={{fontSize:15, fontWeight:700, color:TOKENS.ink, marginBottom:2}}>{dt.labels[lang]}</div>
                <div style={{fontSize:11, color:dt.accent, fontWeight:600, display:'flex', alignItems:'center', gap:4}}>
                  {I.plus(12)}
                  {lang === 'ja' ? 'アップロード' : lang === 'zh' ? '上传' : 'Upload'}
                </div>
              </div>
            </button>
          );
        })}
      </div>

      {/* Upload queue */}
      {queue.length > 0 && <UploadQueue queue={queue} t={t} lang={lang} onRetry={onRetry} onClearDone={onClearDone}/>}

      {/* Month stats */}
      <div style={{padding:14, borderRadius:14, background:TOKENS.surface, border:'1px solid '+TOKENS.hairline, marginBottom:14}}>
        <div style={{display:'flex', justifyContent:'space-between', alignItems:'baseline', marginBottom:10}}>
          <span style={{fontSize:11, color:TOKENS.muted, fontWeight:600, letterSpacing:0.4, textTransform:'uppercase'}}>{t.thisMonth}</span>
          <span style={{fontSize:13, color:TOKENS.ink, fontWeight:700, fontFeatureSettings:'"tnum"'}}>{totalMonth} {t.files_n}</span>
        </div>
        <div style={{display:'flex', height:6, borderRadius:3, overflow:'hidden', marginBottom:10, background:TOKENS.hairlineSoft}}>
          {types.map(k => monthCounts[k] > 0 && (
            <div key={k} style={{flex: monthCounts[k], background: DOC_TYPES[k].accent}}/>
          ))}
        </div>
        <div style={{display:'grid', gridTemplateColumns:'repeat(4, 1fr)', gap:8}}>
          {types.map(k => {
            const dt = DOC_TYPES[k];
            return (
              <div key={k}>
                <div style={{fontSize:18, fontWeight:700, color:TOKENS.ink, fontFeatureSettings:'"tnum"'}}>{monthCounts[k] || 0}</div>
                <div style={{fontSize:10, color:dt.accent, fontWeight:600, display:'flex', alignItems:'center', gap:4}}>
                  <span style={{width:6, height:6, borderRadius:'50%', background:dt.accent}}/>
                  {dt.labels[lang]}
                </div>
              </div>
            );
          })}
        </div>
      </div>

      {/* Recent */}
      <div>
        <div style={{display:'flex', justifyContent:'space-between', alignItems:'baseline', marginBottom:8}}>
          <span style={{fontSize:11, color:TOKENS.muted, fontWeight:600, letterSpacing:0.4, textTransform:'uppercase'}}>{t.recent}</span>
          <button onClick={onGoHistory} style={{fontSize:12, color:TOKENS.brand, fontWeight:600, background:'none', border:'none', cursor:'pointer'}}>{t.history} →</button>
        </div>
        <div style={{background:TOKENS.surface, borderRadius:14, border:'1px solid '+TOKENS.hairline, overflow:'hidden'}}>
          {recent.length === 0 && (
            <div style={{padding:'24px 16px', textAlign:'center', color:TOKENS.mutedSoft, fontSize:13}}>{t.emptyTitle}</div>
          )}
          {recent.map((it, i) => {
            const dt = DOC_TYPES[it.type];
            return (
              <button key={it.id} onClick={()=>onOpenDetail(it)} className="rc-pressable" style={{
                width:'100%', display:'flex', alignItems:'center', gap:10, padding:'10px 12px',
                borderTop: i ? '1px solid '+TOKENS.hairlineSoft : 'none',
                border:'none', background:'transparent', textAlign:'left', cursor:'pointer',
              }}>
                <div style={{width:34, height:40, borderRadius:5, background:dt.bg, color:dt.accent, display:'flex', alignItems:'center', justifyContent:'center', flexShrink:0, backgroundImage:`repeating-linear-gradient(135deg, ${dt.accent}15 0 1px, transparent 1px 6px)`}}>{docTypeIcon(it.type, 18)}</div>
                <div style={{flex:1, minWidth:0}}>
                  <div style={{fontSize:13, color:TOKENS.ink, fontWeight:500, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap'}}>{it.name}</div>
                  <div style={{fontSize:11, color:TOKENS.muted, marginTop:1}}>
                    <span style={{color:dt.accent, fontWeight:600}}>{dt.labels[lang]}</span>
                    <span style={{color:TOKENS.hairline, margin:'0 5px'}}>·</span>
                    {it.size} · {it.time}
                  </div>
                </div>
                <StatusPill item={it} t={t}/>
              </button>
            );
          })}
        </div>
      </div>
    </div>
  );
}

function UploadQueue({ queue, t, lang, onRetry, onClearDone }) {
  const active = queue.filter(q => q.status !== 'synced' && q.status !== 'failed').length;
  const done = queue.filter(q => q.status === 'synced').length;
  return (
    <div style={{marginBottom:14, padding:'10px 12px 6px', borderRadius:14, background:TOKENS.surface, border:'1px solid '+TOKENS.hairline}}>
      <div style={{display:'flex', justifyContent:'space-between', alignItems:'center', padding:'0 2px 8px'}}>
        <div style={{fontSize:11, color:TOKENS.muted, fontWeight:600, letterSpacing:0.4, textTransform:'uppercase', display:'flex', alignItems:'center', gap:6}}>
          <span style={{color: active ? TOKENS.brand : TOKENS.success, display:'flex'}}>{active ? I.spinner(12) : I.cloud(12)}</span>
          {active ? `${t.uploading} · ${active}` : `${t.synced} · ${done}`}
        </div>
        {done > 0 && active === 0 && (
          <button onClick={onClearDone} style={{fontSize:11, color:TOKENS.muted, border:'none', background:'none', fontWeight:600, cursor:'pointer'}}>
            {lang === 'ja' ? 'クリア' : lang === 'zh' ? '清除' : 'Clear'}
          </button>
        )}
      </div>
      {queue.map(q => {
        const dt = DOC_TYPES[q.type];
        const sty = statusStyle(q.status);
        return (
          <div key={q.id} style={{display:'flex', alignItems:'center', gap:10, padding:'8px 2px'}}>
            <div style={{width:28, height:34, borderRadius:4, background:dt.bg, color:dt.accent, display:'flex', alignItems:'center', justifyContent:'center', flexShrink:0}}>{docTypeIcon(q.type, 14)}</div>
            <div style={{flex:1, minWidth:0}}>
              <div style={{fontSize:12, color:TOKENS.ink, fontWeight:500, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap'}}>{q.name}</div>
              {q.status === 'uploading' ? (
                <div style={{marginTop:4, height:3, borderRadius:2, background:TOKENS.hairlineSoft, overflow:'hidden'}}>
                  <div style={{width:`${q.progress}%`, height:'100%', background:dt.accent, transition:'width 0.3s'}}/>
                </div>
              ) : (
                <div style={{fontSize:10, color:sty.fg, fontWeight:600, marginTop:1}}>
                  {q.status === 'synced' ? t.synced : q.status === 'failed' ? (q.error || t.failed) : t.queued}
                </div>
              )}
            </div>
            {q.status === 'failed' ? (
              <button onClick={()=>onRetry(q.id)} className="rc-pressable" style={{fontSize:11, padding:'5px 10px', borderRadius:8, border:'1px solid '+TOKENS.danger, background:TOKENS.dangerSoft, color:TOKENS.danger, fontWeight:700, cursor:'pointer'}}>{t.retry}</button>
            ) : q.status === 'uploading' ? (
              <span style={{fontSize:11, color:TOKENS.muted, fontFeatureSettings:'"tnum"', fontWeight:600}}>{q.progress}%</span>
            ) : q.status === 'synced' ? (
              <span style={{color:TOKENS.success, display:'flex'}}>{I.check(14)}</span>
            ) : (
              <span style={{color:TOKENS.muted, display:'flex'}}>{I.spinner(12)}</span>
            )}
          </div>
        );
      })}
    </div>
  );
}

function StatusPill({ item, t }) {
  const sty = statusStyle(item.status);
  const rejected = item.status === 'rejected';
  return (
    <span style={{
      display:'inline-flex', alignItems:'center', justifyContent:'center', gap:4,
      padding: rejected ? '4px 8px' : 0,
      minWidth: rejected ? 0 : 18,
      borderRadius:999,
      background: rejected ? sty.bg : 'transparent',
      color: sty.fg,
      fontSize:11,
      fontWeight:700,
      whiteSpace:'nowrap',
      flexShrink:0,
    }}>
      {rejected ? t.rejected : I.check(14)}
    </span>
  );
}

// ───────────────────────── Source picker sheet ─────────────────────────
function SourceSheet({ t, lang, type, onClose, onPick }) {
  const dt = type ? DOC_TYPES[type] : null;
  const opts = [
    { k:'camera',  label:t.camera,  icon:I.camera(22) },
    { k:'gallery', label:t.gallery, icon:I.gallery(22) },
    { k:'files',   label:t.files,   icon:I.files(22) },
  ];
  return (
    <div style={{position:'fixed', inset:0, zIndex:100}}>
      <div onClick={onClose} style={{position:'absolute', inset:0, background:'rgba(10,15,25,0.45)', animation:'rcFadeIn 0.18s'}}/>
      <div style={{position:'absolute', bottom:0, left:0, right:0, padding:'8px 10px 12px', animation:'rcSlideUp 0.22s cubic-bezier(.2,.9,.3,1)'}}>
        <div style={{background:'rgba(245,245,247,0.92)', backdropFilter:'blur(30px)', WebkitBackdropFilter:'blur(30px)', borderRadius:14, overflow:'hidden', boxShadow:'0 1px 0 rgba(0,0,0,0.04)'}}>
          {dt && (
            <div style={{padding:'14px 16px 12px', textAlign:'center', borderBottom:'0.5px solid rgba(60,60,67,0.18)'}}>
              <div style={{fontSize:13, color:TOKENS.muted, fontWeight:500}}>{t.uploadTo}</div>
              <div style={{fontSize:15, color:dt.accent, fontWeight:700, marginTop:2, display:'flex', alignItems:'center', justifyContent:'center', gap:6}}>
                <span style={{display:'flex'}}>{docTypeIcon(type, 16)}</span>
                {dt.labels[lang]}
              </div>
            </div>
          )}
          {opts.map((o, i) => (
            <button key={o.k} onClick={()=>onPick(o.k)} className="rc-pressable" style={{
              display:'flex', alignItems:'center', justifyContent:'space-between',
              padding:'16px 18px', width:'100%', border:'none', background:'transparent',
              textAlign:'left', borderTop: i ? '0.5px solid rgba(60,60,67,0.18)' : 'none',
              fontSize:17, color:TOKENS.brand, fontWeight:500, cursor:'pointer',
            }}>
              <span>{o.label}</span>
              <span style={{color:TOKENS.brand, display:'flex'}}>{o.icon}</span>
            </button>
          ))}
        </div>
        <button onClick={onClose} className="rc-pressable" style={{
          width:'100%', marginTop:8, padding:'16px 18px', borderRadius:14,
          background:'rgba(255,255,255,0.95)', backdropFilter:'blur(30px)', WebkitBackdropFilter:'blur(30px)',
          border:'none', color:TOKENS.brand, fontWeight:700, fontSize:17, cursor:'pointer',
        }}>{t.cancel}</button>
      </div>
    </div>
  );
}

// ───────────────────────── Menu sheet (kebab) ─────────────────────────
function MenuSheet({ lang, t, user, onClose, onNavigate, onSetLang }) {
  const items = [
    {
      k:'history',
      icon:<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 4v6l4 2" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round"/><circle cx="10" cy="10" r="7.2" stroke="currentColor" strokeWidth="1.6"/></svg>,
      label:t.uploadHistory, hint:t.histAllDocs,
    },
    {
      k:'annual',
      icon:<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><rect x="3.5" y="2.5" width="13" height="15" rx="1.5" stroke="currentColor" strokeWidth="1.6"/><path d="M7 6h6M7 9h6M7 12h4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/></svg>,
      label:t.annual, hint:t.annualHint,
    },
    {
      k:'yearend',
      icon:<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M3 7h14M5 4v3M15 4v3" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round"/><rect x="3" y="7" width="14" height="10" rx="1.5" stroke="currentColor" strokeWidth="1.6"/><path d="M7 12l2 2 4-4" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"/></svg>,
      label:t.yearend, hint:lang === 'ja' ? '令和7年度' : lang === 'zh' ? '2025 年度' : 'FY 2025',
      badge:t.yearendBadge,
    },
    {
      k:'changepw',
      icon:<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><rect x="4" y="9" width="12" height="8" rx="1.5" stroke="currentColor" strokeWidth="1.6"/><path d="M7 9V6.5a3 3 0 016 0V9" stroke="currentColor" strokeWidth="1.6"/></svg>,
      label:t.changePassword,
    },
    {
      k:'logout', danger:true,
      icon:<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M11 3H4a1 1 0 00-1 1v12a1 1 0 001 1h7" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round"/><path d="M9 10h9m0 0l-3-3m3 3l-3 3" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"/></svg>,
      label:t.logout,
    },
  ];

  const userInitial = (user?.name || user?.email || 'U').charAt(0).toUpperCase();
  const userEmail = user?.email || '';
  const userCompany = user?.company_id || '';

  return (
    <div style={{position:'fixed', inset:0, zIndex:100}}>
      <div onClick={onClose} style={{position:'absolute', inset:0, background:'rgba(10,15,25,0.45)', animation:'rcFadeIn 0.18s'}}/>
      <div style={{
        position:'absolute', left:0, right:0, bottom:0, background:TOKENS.bg,
        borderTopLeftRadius:22, borderTopRightRadius:22, padding:'8px 16px 28px',
        animation:'rcSlideUp 0.22s cubic-bezier(.2,.9,.3,1)', maxHeight:'85%', overflow:'auto',
      }} className="rc-no-scrollbar">
        <div style={{width:36, height:4, borderRadius:2, background:TOKENS.hairline, margin:'6px auto 12px'}}/>

        {/* User card */}
        <div style={{
          padding:14, borderRadius:16, marginBottom:14, display:'flex', alignItems:'center', gap:12,
          background:`linear-gradient(140deg, ${TOKENS.brand} 0%, ${TOKENS.brandInk} 100%)`,
          color:'#fff', position:'relative', overflow:'hidden',
        }}>
          <div style={{position:'absolute', inset:0, opacity:0.16, backgroundImage:'repeating-linear-gradient(135deg, rgba(255,255,255,0.5) 0 1px, transparent 1px 18px)'}}/>
          <div style={{position:'relative', width:48, height:48, borderRadius:14, background:'rgba(255,255,255,0.18)', border:'1px solid rgba(255,255,255,0.3)', display:'flex', alignItems:'center', justifyContent:'center', fontSize:18, fontWeight:700}}>
            {userInitial}
          </div>
          <div style={{position:'relative', flex:1, minWidth:0}}>
            <div style={{fontSize:15, fontWeight:700, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap'}}>{userEmail}</div>
            <div style={{fontSize:11, opacity:0.85, marginTop:2}}>{userCompany}</div>
          </div>
        </div>

        {/* Nav list */}
        <div style={{background:TOKENS.surface, borderRadius:14, overflow:'hidden'}}>
          {items.map((it, i) => (
            <button key={it.k} onClick={()=>onNavigate(it.k)} className="rc-pressable" style={{
              width:'100%', display:'flex', alignItems:'center', gap:12, padding:'14px 16px',
              background:'none', border:'none', textAlign:'left',
              borderTop: i ? '1px solid '+TOKENS.hairlineSoft : 'none',
              color: it.danger ? TOKENS.danger : TOKENS.body, cursor:'pointer',
            }}>
              <div style={{
                width:36, height:36, borderRadius:10,
                background: it.danger ? TOKENS.dangerSoft : TOKENS.brandSoft,
                color: it.danger ? TOKENS.danger : TOKENS.brand,
                display:'flex', alignItems:'center', justifyContent:'center',
              }}>{it.icon}</div>
              <div style={{flex:1, minWidth:0}}>
                <div style={{fontSize:14, color: it.danger ? TOKENS.danger : TOKENS.ink, fontWeight:600}}>{it.label}</div>
                {it.hint && <div style={{fontSize:11, color:TOKENS.muted, marginTop:1, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap'}}>{it.hint}</div>}
              </div>
              {it.badge && <span style={{fontSize:10, fontWeight:700, padding:'2px 8px', borderRadius:999, background:TOKENS.successSoft, color:TOKENS.success}}>{it.badge}</span>}
              {!it.danger && <span style={{color:TOKENS.mutedSoft, display:'flex'}}>{I.chevron(14)}</span>}
            </button>
          ))}
        </div>

        {/* Language picker */}
        <div style={{marginTop:14, padding:'10px 14px', borderRadius:12, background:TOKENS.surface, border:'1px solid '+TOKENS.hairline}}>
          <div style={{display:'flex', justifyContent:'space-between', alignItems:'center'}}>
            <span style={{fontSize:12, color:TOKENS.muted, fontWeight:600}}>{t.language}</span>
            <div style={{display:'flex', gap:4, background:TOKENS.hairlineSoft, padding:2, borderRadius:8}}>
              {[{k:'ja', label:'日本語'},{k:'zh', label:'中文'},{k:'en', label:'EN'}].map(o => {
                const active = lang === o.k;
                return (
                  <button key={o.k} onClick={()=>onSetLang(o.k)} className="rc-pressable" style={{
                    padding:'4px 8px', borderRadius:6, fontSize:11, fontWeight:600, border:'none',
                    background: active ? '#fff' : 'transparent', cursor:'pointer',
                    color: active ? TOKENS.brand : TOKENS.muted,
                    boxShadow: active ? '0 1px 2px rgba(0,0,0,0.06)' : 'none',
                  }}>{o.label}</button>
                );
              })}
            </div>
          </div>
        </div>

        <div style={{textAlign:'center', fontSize:11, color:TOKENS.mutedSoft, marginTop:12}}>v 3.0.0</div>
      </div>
    </div>
  );
}

// ───────────────────────── Change-password bottom sheet ─────────────────────────
function ChangePwSheet({ lang, t, onClose, onDone }) {
  const [cur, setCur] = useState('');
  const [nw, setNw] = useState('');
  const [cnf, setCnf] = useState('');
  const [err, setErr] = useState('');
  const [busy, setBusy] = useState(false);

  async function submit(e) {
    e.preventDefault();
    setErr('');
    if (nw.length < 8) { setErr(t.pwShort); return; }
    if (nw !== cnf) { setErr(t.pwMismatch); return; }
    setBusy(true);
    try {
      await api.changePassword(cur, nw);
      onDone();
    } catch (e) {
      setErr(e.message || 'error');
    } finally {
      setBusy(false);
    }
  }

  return (
    <div style={{position:'fixed', inset:0, zIndex:110}}>
      <div onClick={onClose} style={{position:'absolute', inset:0, background:'rgba(10,15,25,0.5)', animation:'rcFadeIn 0.18s'}}/>
      <div style={{position:'absolute', bottom:0, left:0, right:0, background:TOKENS.bg,
        borderTopLeftRadius:22, borderTopRightRadius:22, padding:'8px 16px 28px',
        animation:'rcSlideUp 0.22s cubic-bezier(.2,.9,.3,1)'}}>
        <div style={{width:36, height:4, borderRadius:2, background:TOKENS.hairline, margin:'6px auto 12px'}}/>
        <div style={{display:'flex', alignItems:'center', justifyContent:'space-between', marginBottom:14}}>
          <span style={{fontSize:18, fontWeight:700, color:TOKENS.ink}}>{t.changePassword}</span>
          <button onClick={onClose} style={{color:TOKENS.muted, border:'none', background:'none', cursor:'pointer'}}>{I.close(18)}</button>
        </div>
        <form onSubmit={submit} autoComplete="off">
          <div style={{background:TOKENS.surface, borderRadius:14, border:'1px solid '+TOKENS.hairline, overflow:'hidden'}}>
            {[
              { v: cur, set: setCur, label: t.pwCurrent, ac:'current-password' },
              { v: nw, set: setNw, label: t.pwNew, ac:'new-password' },
              { v: cnf, set: setCnf, label: t.pwConfirm, ac:'new-password' },
            ].map((f, i) => (
              <div key={i} style={{display:'flex', flexDirection:'column', padding:'10px 14px', borderTop: i ? '1px solid '+TOKENS.hairlineSoft : 'none'}}>
                <span style={{fontSize:11, color:TOKENS.muted, fontWeight:600}}>{f.label}</span>
                <input type="password" value={f.v} onChange={e=>f.set(e.target.value)} autoComplete={f.ac} required
                  placeholder="••••••••"
                  style={{marginTop:4, fontSize:15, color:TOKENS.ink, border:'none', outline:'none', background:'transparent', padding:0, fontFamily:TOKENS.fontMono}}
                />
              </div>
            ))}
          </div>
          <div style={{marginTop:10, padding:'10px 14px', borderRadius:10, background:TOKENS.warnSoft, color:TOKENS.warn, fontSize:11, display:'flex', gap:8, alignItems:'flex-start'}}>
            <span style={{display:'flex', flexShrink:0, paddingTop:1}}>{I.warn(14)}</span>
            <span>{t.pwHint}</span>
          </div>
          {err && <div className="rc-shake" style={{marginTop:10, padding:'10px 14px', borderRadius:10, background:TOKENS.dangerSoft, color:TOKENS.danger, fontSize:12, fontWeight:600}}>{err}</div>}
          <button type="submit" disabled={busy} className="rc-pressable" style={{
            width:'100%', marginTop:14, padding:14, borderRadius:12,
            background: busy ? TOKENS.mutedSoft : TOKENS.brand, color:'#fff',
            border:'none', fontSize:15, fontWeight:700, cursor:'pointer',
            display:'flex', alignItems:'center', justifyContent:'center', gap:8,
          }}>
            {busy && I.spinner(16)}
            {t.update}
          </button>
        </form>
      </div>
    </div>
  );
}

// ───────────────────────── History screen ─────────────────────────
function HistoryScreen({ lang, t, history, onBack, onOpenDetail }) {
  const [filter, setFilter] = useState('all');
  const [query, setQuery] = useState('');

  const filterPills = [
    { key:'all', label: lang === 'ja' ? 'すべて' : lang === 'zh' ? '全部' : 'All' },
    ...Object.values(DOC_TYPES).map(d => ({ key:d.key, label:d.labels[lang] })),
  ];

  const items = history.filter(i => {
    if (filter !== 'all' && i.type !== filter) return false;
    if (query && !i.name.toLowerCase().includes(query.toLowerCase())) return false;
    return true;
  });
  const groups = groupByDate(items);

  return (
    <div style={{position:'fixed', inset:0, background:TOKENS.bg, zIndex:40, display:'flex', flexDirection:'column', animation:'rcSlideLeft 0.22s cubic-bezier(.2,.9,.3,1)'}}>
      <div style={{flex:1, overflow:'auto'}} className="rc-no-scrollbar">
        <div style={{padding:'4px 16px 0', position:'sticky', top:0, background:TOKENS.bg, zIndex:5}}>
          <div style={{display:'flex', alignItems:'center', justifyContent:'space-between', padding:'8px 0 10px', gap:8}}>
            <button onClick={onBack} style={{width:36, height:36, borderRadius:10, border:'1px solid '+TOKENS.hairline, background:TOKENS.surface, color:TOKENS.body, display:'flex', alignItems:'center', justifyContent:'center', flexShrink:0, cursor:'pointer'}} className="rc-pressable">{I.back(20)}</button>
            <div style={{flex:1, minWidth:0}}>
              <div style={{fontSize:11, fontWeight:600, color:TOKENS.muted, letterSpacing:0.4, textTransform:'uppercase'}}>{history.length} {t.files_n}</div>
              <div style={{fontSize:22, fontWeight:700, color:TOKENS.ink, marginTop:2}}>{t.history}</div>
            </div>
          </div>

          <div style={{display:'flex', alignItems:'center', gap:8, padding:'9px 12px', borderRadius:10, background:TOKENS.surface, border:'1px solid '+TOKENS.hairline, marginBottom:10}}>
            <span style={{color:TOKENS.muted, display:'flex'}}>{I.search(16)}</span>
            <input value={query} onChange={e=>setQuery(e.target.value)} placeholder={t.search + '…'}
              style={{flex:1, fontSize:14, color:TOKENS.ink, border:'none', outline:'none', background:'transparent', minWidth:0}}/>
            {query && <button onClick={()=>setQuery('')} style={{color:TOKENS.muted, border:'none', background:'none', display:'flex', cursor:'pointer'}}>{I.close(14)}</button>}
          </div>

          <div style={{display:'flex', gap:6, overflowX:'auto', paddingBottom:10}} className="rc-no-scrollbar">
            {filterPills.map(p => {
              const active = filter === p.key;
              const dt = DOC_TYPES[p.key];
              const accent = dt ? dt.accent : TOKENS.brand;
              const count = p.key === 'all' ? history.length : history.filter(i => i.type === p.key).length;
              return (
                <button key={p.key} onClick={()=>setFilter(p.key)} className="rc-pressable" style={{
                  padding:'6px 11px', borderRadius:999, fontSize:12, fontWeight:600,
                  border:'1px solid '+(active ? accent : TOKENS.hairline),
                  background: active ? (dt ? dt.bg : TOKENS.brandSoft) : TOKENS.surface,
                  color: active ? accent : TOKENS.body, whiteSpace:'nowrap', flexShrink:0,
                  display:'flex', alignItems:'center', gap:5, cursor:'pointer',
                }}>{p.label}<span style={{fontSize:10, opacity:0.85, fontFeatureSettings:'"tnum"'}}>{count}</span></button>
              );
            })}
          </div>
        </div>

        <div style={{padding:'0 16px 24px'}}>
          {groups.length === 0 && (
            <div style={{padding:'40px 16px', textAlign:'center', color:TOKENS.muted, fontSize:13}}>{t.noDocs}</div>
          )}
          {groups.map(([date, list]) => (
            <div key={date} style={{marginBottom:12}}>
              <div style={{display:'flex', justifyContent:'space-between', alignItems:'baseline', padding:'6px 4px'}}>
                <span style={{fontSize:11, color:TOKENS.muted, fontWeight:600, letterSpacing:0.3, textTransform:'uppercase'}}>{formatDateHeader(date, lang)}</span>
                <span style={{fontSize:11, color:TOKENS.mutedSoft, fontFeatureSettings:'"tnum"'}}>{list.length}</span>
              </div>
              <div style={{background:TOKENS.surface, borderRadius:12, border:'1px solid '+TOKENS.hairline, overflow:'hidden'}}>
                {list.map((it, i) => {
                  const dt = DOC_TYPES[it.type];
                  return (
                    <button key={it.id} onClick={()=>onOpenDetail(it)} className="rc-pressable" style={{
                      display:'flex', alignItems:'center', gap:10, padding:'9px 12px',
                      borderTop: i ? '1px solid '+TOKENS.hairlineSoft : 'none',
                      width:'100%', border:'none', background:'transparent', textAlign:'left', cursor:'pointer',
                    }}>
                      <div style={{width:36, height:44, borderRadius:5, background:dt.bg, color:dt.accent, display:'flex', alignItems:'center', justifyContent:'center', flexShrink:0, backgroundImage:`repeating-linear-gradient(135deg, ${dt.accent}15 0 1px, transparent 1px 6px)`}}>{docTypeIcon(it.type, 18)}</div>
                      <div style={{flex:1, minWidth:0}}>
                        <div style={{fontSize:13, color:TOKENS.ink, fontWeight:500, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap'}}>{it.name}</div>
                        <div style={{fontSize:11, color:TOKENS.muted, marginTop:1}}>
                          <span style={{color:dt.accent, fontWeight:600}}>{dt.labels[lang]}</span>
                          <span style={{color:TOKENS.hairline, margin:'0 5px'}}>·</span>
                          {it.size}<span style={{color:TOKENS.hairline, margin:'0 5px'}}>·</span>{it.time}
                        </div>
                      </div>
                      <span style={{color:TOKENS.mutedSoft, display:'flex'}}>{I.chevron(12)}</span>
                    </button>
                  );
                })}
              </div>
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

// ───────────────────────── Detail sheet ─────────────────────────
function DetailSheet({ item, lang, t, onClose, onResubmit }) {
  if (!item) return null;
  const dt = DOC_TYPES[item.type];
  const status = statusLabel(item.status, t);
  const statusColors = statusStyle(item.status);
  const rejectedAt = item.rejectedAt ? new Date(item.rejectedAt) : null;
  return (
    <div style={{position:'fixed', inset:0, zIndex:100}}>
      <div onClick={onClose} style={{position:'absolute', inset:0, background:'rgba(10,15,25,0.55)', animation:'rcFadeIn 0.18s'}}/>
      <div style={{
        position:'absolute', bottom:0, left:0, right:0, top:60, background:TOKENS.bg,
        borderTopLeftRadius:20, borderTopRightRadius:20, padding:'8px 16px 28px',
        animation:'rcSlideUp 0.22s cubic-bezier(.2,.9,.3,1)', overflow:'auto', display:'flex', flexDirection:'column',
      }} className="rc-no-scrollbar">
        <div style={{width:36, height:4, borderRadius:2, background:TOKENS.hairline, margin:'6px auto 12px', flexShrink:0}}/>
        <div style={{display:'flex', alignItems:'center', justifyContent:'space-between', marginBottom:12, flexShrink:0}}>
          <div style={{flex:1, minWidth:0}}>
            <div style={{fontSize:11, color:dt.accent, fontWeight:700, letterSpacing:0.4, textTransform:'uppercase'}}>{dt.labels[lang]}</div>
            <div style={{fontSize:16, fontWeight:600, color:TOKENS.ink, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap'}}>{item.name}</div>
          </div>
          <button onClick={onClose} style={{width:32, height:32, borderRadius:16, border:'none', background:TOKENS.surface, color:TOKENS.muted, display:'flex', alignItems:'center', justifyContent:'center', cursor:'pointer'}}>{I.close(16)}</button>
        </div>

        <div style={{
          flex:1, borderRadius:14, position:'relative', overflow:'hidden', background:'#FAFAF6',
          backgroundImage:`repeating-linear-gradient(180deg, transparent 0 28px, rgba(0,0,0,0.05) 28px 29px), repeating-linear-gradient(135deg, ${dt.accent}11 0 1px, transparent 1px 12px)`,
          border:'1px solid '+TOKENS.hairline, minHeight:200,
          display:'flex', alignItems:'center', justifyContent:'center',
        }}>
          <div style={{position:'absolute', top:12, left:12, padding:'4px 8px', borderRadius:5, background:'#fff', color:dt.accent, fontSize:10, fontWeight:700, boxShadow:'0 1px 2px rgba(0,0,0,0.06)'}}>{dt.labels[lang]}</div>
          <div style={{color:dt.accent+'55'}}>{docTypeIcon(item.type, 60)}</div>
        </div>

        <div style={{marginTop:12, background:TOKENS.surface, borderRadius:12, border:'1px solid '+TOKENS.hairline, padding:'4px 12px', flexShrink:0}}>
          {[
            { k: lang === 'ja' ? '日付' : lang === 'zh' ? '日期' : 'Date', v: formatDateHeader(item.date, lang) + ' ' + item.time },
            { k: lang === 'ja' ? 'サイズ' : lang === 'zh' ? '大小' : 'Size', v: item.size },
            { k: lang === 'ja' ? '状態' : lang === 'zh' ? '状态' : 'Status', v: status, color: statusColors.fg },
            ...(rejectedAt ? [{ k: t.rejectedAt, v: rejectedAt.toLocaleString() }] : []),
          ].map((row, i) => (
            <div key={row.k} style={{display:'flex', justifyContent:'space-between', alignItems:'center', padding:'10px 0', borderTop: i ? '1px solid '+TOKENS.hairlineSoft : 'none'}}>
              <span style={{fontSize:12, color:TOKENS.muted, fontWeight:600}}>{row.k}</span>
              <span style={{fontSize:13, color: row.color || TOKENS.ink, fontWeight:500, fontFeatureSettings:'"tnum"'}}>{row.v}</span>
            </div>
          ))}
        </div>

        {item.status === 'rejected' && (
          <>
            <div style={{
              marginTop:12, padding:12, borderRadius:12,
              background:TOKENS.dangerSoft, color:TOKENS.danger,
              border:'1px solid rgba(209,67,67,0.22)', flexShrink:0,
            }}>
              <div style={{display:'flex', alignItems:'center', gap:6, fontSize:12, fontWeight:700, marginBottom:6}}>
                <span style={{display:'flex'}}>{I.warn(15)}</span>
                {t.rejectReason}
              </div>
              <div style={{fontSize:13, lineHeight:1.55, color:TOKENS.ink}}>
                {item.rejectReason || '—'}
              </div>
            </div>
            <button onClick={() => onResubmit(item)} className="rc-pressable" style={{
              width:'100%', marginTop:12, padding:14, borderRadius:12,
              border:'none', background:TOKENS.brand, color:'#fff',
              fontSize:15, fontWeight:700, cursor:'pointer',
              display:'flex', alignItems:'center', justifyContent:'center', gap:8, flexShrink:0,
            }}>
              {I.plus(16)}
              {t.resubmit}
            </button>
            <div style={{marginTop:7, fontSize:11, color:TOKENS.muted, textAlign:'center', flexShrink:0}}>
              {t.resubmitHint}
            </div>
          </>
        )}
      </div>
    </div>
  );
}

// ───────────────────────── Docs by year (annual / yearend) ─────────────────────────
function DocsByYearScreen({ lang, t, kind, onBack }) {
  // Backend doesn't (yet) push CPA-delivered docs to clients via this portal,
  // so render the screen layout with an empty-state message. When the API is
  // added, swap in a real fetch + the design's grouped list.
  const title = kind === 'annual' ? t.annual : t.yearend;
  const subtitle = kind === 'annual' ? t.annualSub : t.yearendSub;

  return (
    <div style={{position:'fixed', inset:0, background:TOKENS.bg, zIndex:40, display:'flex', flexDirection:'column', animation:'rcSlideLeft 0.22s cubic-bezier(.2,.9,.3,1)'}}>
      <div style={{flex:1, overflow:'auto'}} className="rc-no-scrollbar">
        <div style={{padding:'8px 16px 12px', position:'sticky', top:0, background:TOKENS.bg, zIndex:5, display:'flex', alignItems:'center', gap:10, borderBottom:'1px solid '+TOKENS.hairlineSoft}}>
          <button onClick={onBack} style={{width:36, height:36, borderRadius:10, border:'1px solid '+TOKENS.hairline, background:TOKENS.surface, color:TOKENS.body, display:'flex', alignItems:'center', justifyContent:'center', flexShrink:0, cursor:'pointer'}} className="rc-pressable">{I.back(20)}</button>
          <div style={{flex:1, minWidth:0}}>
            <div style={{fontSize:11, fontWeight:600, color:TOKENS.muted, letterSpacing:0.4, textTransform:'uppercase'}}>{subtitle}</div>
            <div style={{fontSize:20, fontWeight:700, color:TOKENS.ink, marginTop:2, letterSpacing:-0.2}}>{title}</div>
          </div>
        </div>

        <div style={{padding:'14px 16px 28px'}}>
          <div style={{padding:'40px 16px', textAlign:'center', color:TOKENS.muted, fontSize:13, background:TOKENS.surface, borderRadius:12, border:'1px solid '+TOKENS.hairline}}>
            <div style={{display:'flex', justifyContent:'center', marginBottom:12, color:TOKENS.mutedSoft}}>{I.cloud(28)}</div>
            <div style={{color:TOKENS.body, fontWeight:600, marginBottom:4}}>{lang === 'ja' ? '配信予定' : lang === 'zh' ? '待发布' : 'Coming soon'}</div>
            <div>{t.cpaNote}</div>
          </div>
        </div>
      </div>
    </div>
  );
}

// ───────────────────────── Main app shell ─────────────────────────
function MainApp({ user: initialUser, lang, setLang, onLogout }) {
  const [user, setUser] = useState(initialUser);
  const t = STR[lang];

  // Pushed screens (overlays on top of upload root).
  const [screen, setScreen] = useState(null); // null | 'history' | 'annual' | 'yearend'
  // Sheets layered on top of everything.
  const [sheet, setSheet] = useState(null); // null | 'menu' | 'source' | 'changepw'
  const [pendingType, setPendingType] = useState(null);
  const [pendingReplacement, setPendingReplacement] = useState(null);
  const [detail, setDetail] = useState(null);
  const [toast, setToast] = useState(null);
  const [history, setHistory] = useState([]);
  const [queue, setQueue] = useState([]);
  const fileInputRef = useRef(null);
  const fileInputAttrs = useRef({ accept: '', capture: '', multiple: false });

  function showToast(msg, type='success') {
    setToast({ msg, type, key: Date.now() });
    setTimeout(() => setToast(null), 2400);
  }

  const setLangPersist = setLang; // persistence happens in App via effect

  // Load history once on mount; refresh when an upload completes.
  const reloadHistory = useCallback(async () => {
    try {
      const data = await api.history(50, 0, '');
      const adapted = (data.items || []).map(adaptHistoryItem);
      setHistory(adapted);
    } catch (e) {
      // Silent failure on initial load; the user can retry by navigating.
    }
  }, []);

  useEffect(() => {
    reloadHistory();
  }, [reloadHistory]);

  // Per-type counts for the current month, used for the home cards + stats.
  const monthCounts = useMemo(() => {
    const now = new Date();
    const ym = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
    const counts = { receipt: 0, bankbook: 0, invoice: 0, card: 0 };
    for (const it of history) {
      if (it.date.startsWith(ym) && counts[it.type] !== undefined) counts[it.type]++;
    }
    return counts;
  }, [history]);

  const totalMonth = monthCounts.receipt + monthCounts.bankbook + monthCounts.invoice + monthCounts.card;

  // ── Upload flow: source picker triggers a hidden <input>; the chosen
  // files are pushed into the upload queue and POST'd serially.
  const onPickType = (type) => {
    setPendingType(type);
    setPendingReplacement(null);
    setSheet('source');
  };

  const onPickSource = (src) => {
    const type = pendingType;
    const replacement = pendingReplacement;
    setSheet(null);
    if (!type) return;
    // Stage the input attributes then trigger click on a fresh input.
    fileInputAttrs.current = { ...inputAttrsForSource(src), type, replacesInboxItemId: replacement?.id || '' };
    setTimeout(() => fileInputRef.current && fileInputRef.current.click(), 50);
  };

  const onFilesChosen = async (e) => {
    const files = Array.from(e.target.files || []);
    const type = fileInputAttrs.current.type || 'receipt';
    const replacesInboxItemId = fileInputAttrs.current.replacesInboxItemId || '';
    e.target.value = ''; // allow re-pick of the same file later
    setPendingType(null);
    setPendingReplacement(null);
    for (const file of files) {
      await uploadOne(file, type, replacesInboxItemId);
    }
  };

  async function uploadOne(file, type, replacesInboxItemId = '') {
    const id = 'q' + Date.now() + Math.random().toString(36).slice(2, 5);
    const item = { id, name: file.name, type, status: 'uploading', progress: 0, file, replacesInboxItemId };
    setQueue(q => [...q, item]);
    // Fake-progress tick while the real upload happens. The server response
    // either flips the queue item to synced or failed.
    const tickId = setInterval(() => {
      setQueue(q => q.map(x => x.id === id && x.status === 'uploading'
        ? { ...x, progress: Math.min(95, x.progress + Math.round(6 + Math.random() * 9)) }
        : x));
    }, 380);
    try {
      const resp = await api.upload(file, type, replacesInboxItemId);
      setQueue(q => q.map(x => x.id === id ? { ...x, status: 'synced', progress: 100 } : x));
      showToast(t.synced + ' · ' + file.name);
      reloadHistory();
    } catch (err) {
      setQueue(q => q.map(x => x.id === id ? { ...x, status: 'failed', error: err.message } : x));
      showToast(err.message || t.failed, 'error');
    } finally {
      clearInterval(tickId);
    }
  }

  const onRetry = async (id) => {
    const target = queue.find(x => x.id === id);
    if (!target || !target.file) {
      // We didn't keep the File reference; remove and let user re-pick.
      setQueue(q => q.filter(x => x.id !== id));
      showToast(t.failed + ' · retry by re-uploading', 'error');
      return;
    }
    setQueue(q => q.map(x => x.id === id ? { ...x, status: 'uploading', progress: 0 } : x));
    try {
      await api.upload(target.file, target.type, target.replacesInboxItemId || '');
      setQueue(q => q.map(x => x.id === id ? { ...x, status: 'synced', progress: 100 } : x));
      reloadHistory();
    } catch (err) {
      setQueue(q => q.map(x => x.id === id ? { ...x, status: 'failed', error: err.message } : x));
    }
  };

  const onClearDone = () => setQueue(q => q.filter(x => x.status !== 'synced'));

  const onResubmit = (item) => {
    setDetail(null);
    setPendingType(item.type);
    setPendingReplacement(item);
    setSheet('source');
  };

  const onMenuNavigate = (k) => {
    setSheet(null);
    if (k === 'logout') {
      setTimeout(() => {
        localStorage.removeItem('token');
        localStorage.removeItem('user');
        api.setToken(null);
        onLogout();
      }, 100);
      return;
    }
    if (k === 'changepw') {
      setTimeout(() => setSheet('changepw'), 200);
      return;
    }
    setScreen(k);
  };

  return (
    <div style={{position:'fixed', inset:0, display:'flex', flexDirection:'column', overflow:'hidden', background:TOKENS.bg}}>
      {/* Upload root */}
      <UploadHome
        lang={lang} t={t} user={user}
        history={history} monthCounts={monthCounts} totalMonth={totalMonth}
        queue={queue}
        onOpenMenu={() => setSheet('menu')}
        onPickType={onPickType}
        onRetry={onRetry}
        onClearDone={onClearDone}
        onOpenDetail={setDetail}
        onGoHistory={() => setScreen('history')}
      />

      {/* Hidden file input. Re-rendered each pick via key to apply fresh attrs. */}
      <input
        ref={fileInputRef}
        type="file"
        style={{ display:'none' }}
        accept={fileInputAttrs.current.accept || 'image/*,application/pdf,.pdf'}
        capture={fileInputAttrs.current.capture || undefined}
        multiple={!!fileInputAttrs.current.multiple}
        onChange={onFilesChosen}
      />

      {/* Pushed screens */}
      {screen === 'history' && (
        <HistoryScreen lang={lang} t={t} history={history} onBack={() => setScreen(null)} onOpenDetail={setDetail}/>
      )}
      {(screen === 'annual' || screen === 'yearend') && (
        <DocsByYearScreen lang={lang} t={t} kind={screen} onBack={() => setScreen(null)}/>
      )}

      {/* Sheets */}
      {sheet === 'menu' && (
        <MenuSheet lang={lang} t={t} user={user} onClose={() => setSheet(null)} onNavigate={onMenuNavigate} onSetLang={setLangPersist}/>
      )}
      {sheet === 'source' && (
        <SourceSheet t={t} lang={lang} type={pendingType} onClose={() => { setSheet(null); setPendingType(null); }} onPick={onPickSource}/>
      )}
      {sheet === 'changepw' && (
        <ChangePwSheet lang={lang} t={t}
          onClose={() => setSheet(null)}
          onDone={() => { setSheet(null); showToast(t.update + ' ✓'); }}
        />
      )}

      {detail && <DetailSheet item={detail} lang={lang} t={t} onClose={() => setDetail(null)} onResubmit={onResubmit}/>}
      <Toast msg={toast?.msg} type={toast?.type || 'success'}/>
    </div>
  );
}

// ───────────────────────── Root app: auth gate ─────────────────────────
function App() {
  const [lang, setLang] = useState(() => localStorage.getItem('rscan_lang') || (
    (navigator.language || '').toLowerCase().startsWith('zh') ? 'zh' :
    (navigator.language || '').toLowerCase().startsWith('en') ? 'en' : 'ja'
  ));
  const [phase, setPhase] = useState('boot'); // 'boot' | 'login' | 'changepw' | 'app'
  const [user, setUser] = useState(null);

  // Persist any lang change to localStorage so subsequent renders/screens
  // pick it up.
  useEffect(() => {
    localStorage.setItem('rscan_lang', lang);
    document.documentElement.lang = lang;
  }, [lang]);

  useEffect(() => {
    const token = localStorage.getItem('token');
    const userStr = localStorage.getItem('user');
    if (!token || !userStr) {
      setPhase('login');
      return;
    }
    try {
      const u = JSON.parse(userStr);
      api.setToken(token);
      setUser(u);
      setPhase(u.must_change_pw ? 'changepw' : 'app');
    } catch {
      setPhase('login');
    }
  }, []);

  if (phase === 'boot') return <div style={{minHeight:'100vh', display:'flex', alignItems:'center', justifyContent:'center', color:TOKENS.muted}}>{I.spinner(20)}</div>;
  if (phase === 'login') return <LoginScreen lang={lang} onSuccess={(u) => { setUser(u); setPhase(u.must_change_pw ? 'changepw' : 'app'); }}/>;
  if (phase === 'changepw') return <ChangePwScreen lang={lang} onSuccess={(u) => { setUser(u); setPhase('app'); }}/>;
  return <MainApp user={user} lang={lang} setLang={setLang} onLogout={() => { setUser(null); setPhase('login'); }}/>;
}

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