// Money — root app: router + persistence + login gate + notification scheduler.

const TWEAKS_DEFAULTS = {
  dark: true,
  accent: 'mint',
};

const ACCENT_CHOICES = {
  emerald:  { light: '#0F8A5A', dark: '#3DBA85', soft_l: '#D9EBE0', soft_d: 'rgba(61,186,133,0.16)' },
  indigo:   { light: '#4F46E5', dark: '#8073FF', soft_l: '#E5E3F9', soft_d: 'rgba(128,115,255,0.16)' },
  copper:   { light: '#B5683B', dark: '#E89968', soft_l: '#F5DECF', soft_d: 'rgba(232,153,104,0.16)' },
  graphite: { light: '#1A1814', dark: '#F5F1EA', soft_l: '#E0DCD2', soft_d: 'rgba(245,241,234,0.12)' },
  mint:     { light: '#1F8F87', dark: '#5BD4C2', soft_l: '#D2EDE9', soft_d: 'rgba(91,212,194,0.16)' },
};

const DEFAULT_SETTINGS = {
  displayName: 'Саша',
  homeAccountId: 'tin',
  aiEnabled: false,
  aiKey: '',
  aiBaseUrl: 'https://api.apiyi.com/v1',
  aiModel: 'claude-haiku-4-5',                 // быстрая дешёвая — текст трат, голос
  aiModelMultimodal: 'claude-sonnet-4-5',      // умеет картинки/PDF — для договоров кредитов
  notifFlags: { dayBeforeSub: true, threeBefCr: true, dayOf: true, afterCharge: false },
  notifFeed: [],
  // VAPID public key — paired with dist/server/vapid-keys.json.
  // The matching private key must be stored as VAPID_JWK secret in your Cloudflare Worker.
  vapidPublic: 'BLKsq_6sGAcKI_TBc28HyUIr-hhg8crDPH9gO4zbHFlvSTPwVYfLCmz9lxc8PpyHFo8MBi0GM-VFKFIhdWzS9c4',
  // URL of your deployed Cloudflare Worker (e.g. https://money-push.YOUR-NAME.workers.dev).
  // Until set, push subscriptions are only stored locally — server-side fan-out won't work.
  pushServerUrl: '',
};

function App() {
  // ── Auth gate ──────────────────────────────────
  const [loggedIn, setLoggedIn] = React.useState(() => {
    try { return localStorage.getItem('money:loggedIn') === 'true'; } catch { return false; }
  });

  // ── Persistent UI state ────────────────────────
  const [screen, setScreen] = React.useState(() => {
    try { return localStorage.getItem('money:screen') || 'home'; } catch { return 'home'; }
  });
  const [tweaks, setTweaks] = React.useState(() => {
    try {
      const saved = JSON.parse(localStorage.getItem('money:tweaks:v2') || 'null');
      return saved || TWEAKS_DEFAULTS;
    } catch { return TWEAKS_DEFAULTS; }
  });

  const [settings, setSettings] = React.useState(() => {
    try {
      const s = JSON.parse(localStorage.getItem('money:settings') || 'null');
      return { ...DEFAULT_SETTINGS, ...(s || {}) };
    } catch { return DEFAULT_SETTINGS; }
  });
  React.useEffect(() => { try { localStorage.setItem('money:settings', JSON.stringify(settings)); } catch {} }, [settings]);
  const setSetting = (k, v) => setSettings(s => ({ ...s, [k]: typeof v === 'function' ? v(s[k]) : v }));

  React.useEffect(() => { try { localStorage.setItem('money:screen', screen); } catch {} }, [screen]);
  React.useEffect(() => { try { localStorage.setItem('money:tweaks:v2', JSON.stringify(tweaks)); } catch {} }, [tweaks]);

  // ── Apply accent override to tokens ────────────
  React.useEffect(() => {
    const a = ACCENT_CHOICES[tweaks.accent] || ACCENT_CHOICES.mint;
    TOK.light.accent = a.light;     TOK.light.accentSoft = a.soft_l;
    TOK.light.positive = tweaks.accent === 'graphite' ? '#0F8A5A' : a.light;
    TOK.dark.accent = a.dark;       TOK.dark.accentSoft = a.soft_d;
    TOK.dark.positive = tweaks.accent === 'graphite' ? '#3DBA85' : a.dark;
  }, [tweaks.accent]);

  React.useEffect(() => { document.body.classList.toggle('dark', tweaks.dark); }, [tweaks.dark]);

  // ── Tx store wiring ────────────────────────────
  const [txs, setTxs] = React.useState([]);
  const [storeReady, setStoreReady] = React.useState(false);
  React.useEffect(() => {
    let unsubscribe;
    (async () => {
      await TxStore.init();
      // Initialize current-month totals.
      recomputeMonth();
      setTxs(TxStore.list());
      setStoreReady(true);
      unsubscribe = TxStore.onChange(list => { setTxs(list.slice()); Sync.schedule(); });
    })();
    return () => { if (unsubscribe) unsubscribe(); };
  }, []);

  // ── Cloud sync (Cloudflare Worker) ──────────────
  const [syncStatus, setSyncStatus] = React.useState(() => Sync.getStatus());
  React.useEffect(() => {
    const email = (() => { try { return localStorage.getItem('money:email') || ''; } catch { return ''; } })();
    Sync.configure({
      email,
      serverUrl: settings.pushServerUrl,
      txStoreGetter: () => TxStore.list(),
      onRemoteApplied: () => {
        // Refresh local React state after remote snapshot was applied.
        setTxs(TxStore.list());
        // Force a re-read of settings from localStorage (in case sync overwrote them).
        try {
          const s = JSON.parse(localStorage.getItem('money:settings') || 'null');
          if (s) setSettings(cur => ({ ...DEFAULT_SETTINGS, ...s, aiKey: cur.aiKey }));
        } catch {}
      },
    });
    const off = Sync.onChange(setSyncStatus);
    return () => off();
  }, [settings.pushServerUrl]);

  // Pull from server on first launch (after login + store ready).
  const didInitialPullRef = React.useRef(false);
  React.useEffect(() => {
    if (!loggedIn || !storeReady) return;
    if (didInitialPullRef.current) return;
    if (!settings.pushServerUrl) return;
    didInitialPullRef.current = true;
    Sync.pull().then(applied => {
      if (applied) setTxs(TxStore.list());
    });
  }, [loggedIn, storeReady, settings.pushServerUrl]);

  // Periodic background sync every 60s.
  React.useEffect(() => {
    if (!loggedIn || !settings.pushServerUrl) return;
    const t = setInterval(() => Sync.schedule(0), 60_000);
    return () => clearInterval(t);
  }, [loggedIn, settings.pushServerUrl]);

  // Push on settings changes (debounced inside Sync.schedule).
  React.useEffect(() => { if (storeReady) Sync.schedule(); }, [settings, storeReady]);

  // Manual triggers for Settings UI.
  const onPullNow   = () => Sync.pull().then(() => setTxs(TxStore.list()));
  const onPushNow   = () => Sync.push();
  const onResetSync = () => { if (confirm('Стереть копию с сервера? Локальные данные сохранятся.')) Sync.reset(); };

  const addTx = React.useCallback(async (tx) => {
    const full = await TxStore.add(tx);
    pushNotif({
      icon: tx.type === 'income' ? 'arrowDownRight' : 'arrowUpRight',
      title: (tx.type === 'income' ? '+' : '−') + fmtR(full.amount) + ' ₽ · ' + ((DATA.categories.find(c => c.id === full.categoryId) || {}).title || ''),
      sub: full.note || (DATA.accounts.find(a => a.id === full.accountId) || {}).bank || '',
    });
    return full;
  }, []);
  const updateTx = React.useCallback((id, patch) => TxStore.update(id, patch), []);
  const removeTx = React.useCallback((id) => TxStore.remove(id), []);

  // ── In-app notification feed helpers ───────────
  const pushNotif = React.useCallback((n) => {
    const item = {
      id: 'n_' + Date.now() + '_' + Math.random().toString(36).slice(2, 5),
      icon: n.icon || 'bell',
      title: n.title,
      sub: n.sub || n.body,
      time: new Date().toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' }),
      read: false,
    };
    setSettings(s => ({ ...s, notifFeed: [...((s.notifFeed) || []).slice(-50), item] }));
  }, []);

  // ── Add picker / Add screens ───────────────────
  const [showAddPicker, setShowAddPicker] = React.useState(false);
  const [showAdd, setShowAdd] = React.useState(false);
  const [addMode, setAddMode] = React.useState('expense');
  const [editTxId, setEditTxId] = React.useState(null);

  const dark = tweaks.dark;
  const t = T(dark);

  const handleNav = (tab) => {
    if (tab === 'add') { setShowAddPicker(true); return; }
    setScreen(tab);
  };
  const onNav = (id, payload) => {
    setScreen(id);
    if (id === 'txs' && payload && payload.editId) setEditTxId(payload.editId);
  };
  const onBack = () => setScreen('home');

  // ── Recurring auto-charges ──────────────────────
  // Runs on: app boot (post-store-init), tab visibility, every 5 min.
  // Creates real transactions for subs/credits/incomes when their day is reached.
  React.useEffect(() => {
    if (!storeReady) return;
    let cancelled = false;
    const tick = async () => {
      if (cancelled) return;
      try {
        const created = await runAutoCharges({ pushNotif });
        if (created.length) {
          // After auto-charges Sync schedules itself via TxStore.onChange.
          setTxs(TxStore.list());
        }
      } catch (e) { console.warn('auto-charge tick failed', e); }
    };
    tick();
    const onVis = () => { if (document.visibilityState === 'visible') tick(); };
    document.addEventListener('visibilitychange', onVis);
    const t = setInterval(tick, 5 * 60_000);
    return () => { cancelled = true; document.removeEventListener('visibilitychange', onVis); clearInterval(t); };
  }, [storeReady]);

  // ── Notification scheduler (foreground) ─────────
  // Walks subs/credits/incomes against today and shows local notifications
  // when matching rules trigger. Runs every 60s while app is open.
  React.useEffect(() => {
    if (!storeReady) return;
    let timer = setInterval(checkReminders, 60_000);
    setTimeout(checkReminders, 4000); // initial pass shortly after launch
    return () => clearInterval(timer);
    function checkReminders() {
      const flags = settings.notifFlags || {};
      const today = new Date();
      const todayKey = today.toISOString().slice(0, 10);
      const tomorrowKey = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1).toISOString().slice(0, 10);
      const inThreeKey = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 3).toISOString().slice(0, 10);

      const triggers = [];
      DATA.subs.filter(s => s.active !== false).forEach(s => {
        const subDate = new Date(today.getFullYear(), today.getMonth(), s.day).toISOString().slice(0, 10);
        if (flags.dayBeforeSub && subDate === tomorrowKey)
          triggers.push({ id: 'sub-' + s.id + '-' + tomorrowKey, title: `Завтра спишется ${s.title}`, body: `${fmtR(s.amount)} ₽`, icon: 'music' });
        if (flags.dayOf && subDate === todayKey)
          triggers.push({ id: 'sub-' + s.id + '-' + todayKey + '-d', title: `${s.title} · ${fmtR(s.amount)} ₽`, body: 'Спишется сегодня', icon: 'music' });
      });
      DATA.credits.forEach(c => {
        const y = today.getFullYear(), m = today.getMonth() + 1;
        const day = creditPaymentDayForMonth(c, y, m);
        const amount = creditPaymentForMonth(c, y, m);
        const crDate = new Date(today.getFullYear(), today.getMonth(), day).toISOString().slice(0, 10);
        if (flags.threeBefCr && crDate === inThreeKey)
          triggers.push({ id: 'cr-' + c.id + '-' + inThreeKey, title: `Кредит «${c.title}» через 3 дня`, body: `${fmtR(amount)} ₽`, icon: 'bank' });
        if (flags.dayOf && crDate === todayKey)
          triggers.push({ id: 'cr-' + c.id + '-' + todayKey + '-d', title: `Кредит «${c.title}» сегодня`, body: `${fmtR(amount)} ₽`, icon: 'bank' });
      });

      const seen = new Set((settings.notifFeed || []).map(n => n.id));
      const newItems = triggers.filter(x => !seen.has(x.id));
      if (!newItems.length) return;
      // Push to in-app feed.
      setSettings(s => {
        const feed = [...((s.notifFeed) || [])];
        for (const it of newItems) {
          feed.push({
            id: it.id, icon: it.icon, title: it.title, sub: it.body,
            time: new Date().toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' }),
            read: false,
          });
        }
        return { ...s, notifFeed: feed.slice(-50) };
      });
      // System-level notifications if permission granted.
      if (typeof Notification !== 'undefined' && Notification.permission === 'granted' && navigator.serviceWorker && navigator.serviceWorker.controller) {
        for (const it of newItems) {
          navigator.serviceWorker.controller.postMessage({ type: 'money:notify', title: it.title, body: it.body, tag: it.id, url: './' });
        }
      }
    }
  }, [storeReady, settings.notifFlags]);

  // ── Export / Import ────────────────────────────
  const onExport = async () => {
    const payload = {
      version: 1,
      exportedAt: new Date().toISOString(),
      tweaks,
      settings: { ...settings, aiKey: settings.aiKey ? '***' : '' },
      accounts: DATA.accounts,
      categories: DATA.categories,
      subs: DATA.subs,
      credits: DATA.credits,
      incomeSources: DATA.incomeSources,
      goals: DATA.goals,
      rates: DATA.rates,
      transactions: TxStore.list(),
    };
    const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = `money-backup-${new Date().toISOString().slice(0, 10)}.json`;
    a.click();
    setTimeout(() => URL.revokeObjectURL(url), 5000);
  };

  const onImport = async (file) => {
    if (!confirm('Заменить текущие данные? Это действие нельзя отменить.')) return;
    try {
      const text = await file.text();
      const j = JSON.parse(text);
      if (j.tweaks) setTweaks(j.tweaks);
      if (j.settings) setSettings(s => ({ ...s, ...j.settings, aiKey: j.settings.aiKey === '***' ? s.aiKey : (j.settings.aiKey || '') }));
      if (j.accounts) { DATA.accounts = j.accounts; persistAccounts(); }
      if (j.categories) { DATA.categories = j.categories; persistCategories(); }
      if (j.subs) { DATA.subs = j.subs; persistSubs(); }
      if (j.credits) { DATA.credits = j.credits; persistCredits(); }
      if (j.incomeSources) { DATA.incomeSources = j.incomeSources; persistIncomeSources(); }
      if (j.goals) { DATA.goals = j.goals; persistGoals(); }
      if (j.rates) { DATA.rates = j.rates; saveLS('money:rates', DATA.rates); }
      if (Array.isArray(j.transactions)) await TxStore.importAll(j.transactions);
      alert('Импорт завершён.');
    } catch (e) {
      alert('Ошибка импорта: ' + e.message);
    }
  };

  // ── Push subscription via VAPID (Settings → «Разрешить уведомления») ──
  const onRequestPush = async () => {
    try {
      if (!('serviceWorker' in navigator) || !('PushManager' in window)) {
        pushNotif({ icon: 'bell', title: 'Push не поддерживается', sub: 'Браузер или платформа без Web Push' });
        return;
      }
      const reg = await navigator.serviceWorker.ready;
      const vapidPublic = (settings && settings.vapidPublic) || '';
      if (!vapidPublic) {
        pushNotif({ icon: 'bell', title: 'Нет VAPID-ключа', sub: 'Заполните vapidPublic в настройках' });
        return;
      }
      const sub = await reg.pushManager.subscribe({
        userVisibleOnly: true,
        applicationServerKey: urlBase64ToUint8Array(vapidPublic),
      });
      const subJson = sub.toJSON();
      setSettings(s => ({ ...s, pushSubscription: subJson }));
      // If push server URL is configured, register the subscription there.
      if (settings.pushServerUrl) {
        try {
          await fetch(settings.pushServerUrl.replace(/\/$/, '') + '/subscribe', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ subscription: subJson }),
          });
          pushNotif({ icon: 'check', title: 'Push активирован', sub: 'Подписка отправлена на ваш сервер' });
        } catch (e) {
          pushNotif({ icon: 'bell', title: 'Сервер недоступен', sub: 'Подписка сохранена локально' });
        }
      } else {
        pushNotif({ icon: 'check', title: 'Push разрешён', sub: 'Сервер для рассылки не настроен — пока только локальные' });
      }
    } catch (e) {
      console.warn('push subscribe failed', e);
      pushNotif({ icon: 'bell', title: 'Push не удался', sub: e.message });
    }
  };

  // ── SW message bridge (deep-link navigation from notification click) ──
  React.useEffect(() => {
    if (!('serviceWorker' in navigator)) return;
    const onMessage = (e) => {
      const d = e.data || {};
      if (d.type === 'money:nav' && d.url) {
        const u = new URL(d.url, location.origin);
        const sc = u.searchParams.get('screen');
        if (sc) setScreen(sc);
      }
    };
    navigator.serviceWorker.addEventListener('message', onMessage);
    return () => navigator.serviceWorker.removeEventListener('message', onMessage);
  }, []);

  // ── URL ?screen=… ?action=add-expense ──────────
  React.useEffect(() => {
    const params = new URLSearchParams(location.search);
    const sc = params.get('screen');
    if (sc) setScreen(sc);
    const action = params.get('action');
    if (action === 'add-expense') { setAddMode('expense'); setShowAdd(true); }
    if (action === 'add-income')  { setAddMode('income');  setShowAdd(true); }
    if (sc || action) history.replaceState(null, '', location.pathname);
  }, []);

  // ── Logout ─────────────────────────────────────
  const onLogout = () => {
    try { localStorage.removeItem('money:loggedIn'); } catch {}
    setLoggedIn(false);
  };

  // ── Login gate ─────────────────────────────────
  if (!loggedIn) {
    return (
      <WebShell dark={dark}>
        <div style={{ width: '100%', height: '100%', background: t.bg, color: t.ink, position: 'relative', overflow: 'hidden', fontFamily: TOK.fontText }}>
          <StatusBar dark={dark} />
          <LoginScreen dark={dark} onLogin={() => setLoggedIn(true)} />
          <HomeIndicator dark={dark} />
        </div>
      </WebShell>
    );
  }

  // ── Screen mapping ─────────────────────────────
  const screenEl = (() => {
    switch (screen) {
      case 'home':          return <HomeScreen dark={dark} onNav={onNav} settings={settings} txs={txs} />;
      case 'analytics':     return <AnalyticsScreen dark={dark} onNav={onNav} txs={txs} />;
      case 'goal':          return <GoalScreen dark={dark} onNav={onNav} />;
      case 'me':            return <SettingsScreen dark={dark} setDark={v => setTweaks(t => ({ ...t, dark: v }))}
                                       onNav={onNav} settings={settings} setSetting={setSetting}
                                       onLogout={onLogout} onExport={onExport} onImport={onImport} onRequestPush={onRequestPush}
                                       syncStatus={syncStatus} onPullNow={onPullNow} onPushNow={onPushNow} onResetSync={onResetSync} />;
      case 'accounts':      return <AccountsScreen dark={dark} onBack={onBack} onNav={onNav} />;
      case 'subs':          return <SubscriptionsScreen dark={dark} onBack={onBack} />;
      case 'credits':       return <CreditsScreen dark={dark} onBack={onBack} settings={settings} />;
      case 'calendar':      return <CalendarScreen dark={dark} onBack={onBack} txs={txs}
                                       onPlan={(date) => {
                                         setAddMode('expense'); setShowAdd(true);
                                         // store planned date on the picker; AddExpense handles current time only,
                                         // so on save it'll mark date as the planned ISO.
                                         window.__plannedDate = date.toISOString();
                                       }} />;
      case 'notifications': return <NotificationsScreen dark={dark} onBack={onBack} onNav={onNav} settings={settings} setSetting={setSetting} />;
      case 'txs':           return <AllTransactionsScreen dark={dark} onBack={onBack} txs={txs} onUpdateTx={updateTx} onRemoveTx={removeTx} openEdit={editTxId} />;
      case 'categories':    return <CategoriesScreen dark={dark} onBack={onBack} />;
      default:              return <HomeScreen dark={dark} onNav={onNav} settings={settings} txs={txs} />;
    }
  })();

  const noTabBar = false;
  const activeTab = ['home','analytics','goal','me'].includes(screen) ? screen : 'home';

  // Wrap addTx to honour planned date if set.
  const addTxWithDate = React.useCallback(async (tx) => {
    const d = window.__plannedDate;
    if (d) { tx = { ...tx, date: d, isPlanned: true }; window.__plannedDate = null; }
    return addTx(tx);
  }, [addTx]);

  return (
    <WebShell dark={dark}>
      <div style={{ width: '100%', height: '100%', background: t.bg, color: t.ink, position: 'relative', overflow: 'hidden', fontFamily: TOK.fontText }}>
        <StatusBar dark={dark} />
        {screenEl}
        {!noTabBar && <TabBar active={activeTab} onChange={handleNav} dark={dark} />}
        {!noTabBar && <HomeIndicator dark={dark} />}

        <Sheet open={showAddPicker} onClose={() => setShowAddPicker(false)} dark={dark} title="Что добавить?">
          <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10, marginBottom: 14 }}>
            <button onClick={() => { setShowAddPicker(false); setAddMode('expense'); setShowAdd(true); }} style={{
              padding: 20, borderRadius: 18, border: 'none', cursor: 'pointer',
              background: t.dangerSoft, color: t.danger, textAlign: 'left', fontFamily: 'inherit',
            }}>
              <IconCircle icon="arrowUpRight" color={t.danger} size={44} dark={dark} bg="#fff" fg={t.danger} />
              <div style={{ fontFamily: TOK.fontDisplay, fontSize: 16, fontWeight: 800, marginTop: 14, color: t.ink }}>Расход</div>
              <div style={{ fontSize: 12, color: t.muted, fontWeight: 500, marginTop: 2 }}>Новая трата</div>
            </button>
            <button onClick={() => { setShowAddPicker(false); setAddMode('income'); setShowAdd(true); }} style={{
              padding: 20, borderRadius: 18, border: 'none', cursor: 'pointer',
              background: t.accentSoft, color: t.accent, textAlign: 'left', fontFamily: 'inherit',
            }}>
              <IconCircle icon="arrowDownRight" color={t.accent} size={44} dark={dark} bg="#fff" fg={t.accent} />
              <div style={{ fontFamily: TOK.fontDisplay, fontSize: 16, fontWeight: 800, marginTop: 14, color: t.ink }}>Доход</div>
              <div style={{ fontSize: 12, color: t.muted, fontWeight: 500, marginTop: 2 }}>Поступление</div>
            </button>
          </div>
          <ListRow dark={dark} icon="music" color={t.accent2} title="Подписка" sub="Добавить новую подписку"
            right={<Icon d={ICONS.chevronR} size={14} stroke={t.faint} />}
            onClick={() => { setShowAddPicker(false); onNav('subs'); }} />
          <ListRow dark={dark} icon="bank" color={t.warn} title="Кредит" sub="Загрузить договор"
            right={<Icon d={ICONS.chevronR} size={14} stroke={t.faint} />}
            onClick={() => { setShowAddPicker(false); onNav('credits'); }} />
          <ListRow dark={dark} icon="target" color={t.accent} title="Цель" sub="Накопления на мечту"
            right={<Icon d={ICONS.chevronR} size={14} stroke={t.faint} />}
            onClick={() => { setShowAddPicker(false); onNav('goal'); }} />
          <ListRow dark={dark} icon="card" color={t.accent2} title="Счёт / карту" sub="Привязать новый счёт"
            right={<Icon d={ICONS.chevronR} size={14} stroke={t.faint} />}
            onClick={() => { setShowAddPicker(false); onNav('accounts'); }} last />
        </Sheet>

        {showAdd && (
          <div style={{ position: 'absolute', inset: 0, background: t.bg, zIndex: 90, animation: 'slideUp .25s ease' }}>
            {addMode === 'expense'
              ? <AddExpenseScreen dark={dark} onClose={() => setShowAdd(false)} settings={settings} onAddTx={addTxWithDate} />
              : <AddIncomeScreen dark={dark} onClose={() => setShowAdd(false)} onAddTx={addTxWithDate} />}
          </div>
        )}
      </div>
    </WebShell>
  );
}

// ─── Web shell (centers phone on desktop, full-screen on mobile) ───
function WebShell({ children, dark }) {
  const [vp, setVp] = React.useState(() => ({
    w: typeof window !== 'undefined' ? window.innerWidth : 1200,
    h: typeof window !== 'undefined' ? window.innerHeight : 900,
  }));
  React.useEffect(() => {
    const r = () => setVp({ w: window.innerWidth, h: window.innerHeight });
    window.addEventListener('resize', r);
    return () => window.removeEventListener('resize', r);
  }, []);

  const isPhoneWidth = vp.w < 500;
  const SCREEN_W = 412, SCREEN_H = 892;

  if (isPhoneWidth) {
    return (
      <div style={{ width: '100vw', height: '100vh', background: dark ? TOK.dark.bg : TOK.light.bg }}>
        <div style={{ width: '100%', height: '100%', position: 'relative' }}>
          {children}
        </div>
      </div>
    );
  }

  const t = T(dark);
  const phoneFull = SCREEN_H + 16;
  const scale = Math.min(1, (vp.h - 60) / phoneFull);

  return (
    <div style={{
      width: '100vw', height: '100vh',
      background: dark ? '#0A0907' : '#E8E1D4',
      display: 'flex', alignItems: 'center', justifyContent: 'center',
      position: 'relative', overflow: 'hidden',
    }}>
      <div style={{ position: 'absolute', top: '-10%', left: '-5%', width: 600, height: 600, borderRadius: '50%',
        background: `radial-gradient(circle, ${t.accent}22, transparent 60%)`, filter: 'blur(40px)', zIndex: 1 }} />
      <div style={{ position: 'absolute', bottom: '-15%', right: '-10%', width: 700, height: 700, borderRadius: '50%',
        background: `radial-gradient(circle, ${t.accent2}1A, transparent 60%)`, filter: 'blur(50px)', zIndex: 1 }} />

      <div style={{ position: 'absolute', left: 60, top: 60, zIndex: 1, color: t.ink, maxWidth: 360 }}>
        <div style={{
          width: 56, height: 56, borderRadius: 18,
          background: `linear-gradient(135deg, ${t.accent}, ${dark ? '#2A8C66' : '#0B6B47'})`,
          display: 'flex', alignItems: 'center', justifyContent: 'center',
          boxShadow: `0 10px 24px ${t.accent}55`, marginBottom: 24,
        }}>
          <div style={{ fontFamily: TOK.fontDisplay, fontSize: 28, fontWeight: 800, color: '#fff', letterSpacing: -1 }}>M</div>
        </div>
        <div style={{ fontFamily: TOK.fontDisplay, fontSize: 44, fontWeight: 800, color: t.ink, letterSpacing: -1.2, lineHeight: 1.05 }}>
          Money.
        </div>
        <div style={{ fontSize: 16, color: t.muted, marginTop: 12, lineHeight: 1.5, fontWeight: 500 }}>
          Бюджет, подписки, кредиты, цели — в одном приложении.<br/>
          Откройте на iPhone и добавьте на главный экран — будет жить офлайн.
        </div>
      </div>

      <div style={{
        position: 'absolute', left: 60, bottom: 50, zIndex: 1, color: t.muted, fontSize: 12, fontWeight: 600,
        display: 'flex', alignItems: 'center', gap: 8,
      }}>
        <div style={{ width: 24, height: 24, borderRadius: 6, background: t.surfaceAlt, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
          <Icon d={ICONS.download} size={12} stroke={t.muted} />
        </div>
        Safari · «Поделиться» → «На экран Домой»
      </div>

      <div style={{
        width: SCREEN_W + 16, height: SCREEN_H + 16,
        borderRadius: 56, background: '#1A1814', padding: 8,
        boxShadow: '0 60px 120px rgba(15,10,5,0.35), 0 0 0 1px rgba(0,0,0,0.15), inset 0 0 0 2px rgba(255,255,255,0.04)',
        position: 'relative', zIndex: 2,
        transform: scale < 1 ? `scale(${scale})` : undefined,
        transformOrigin: 'center center',
      }}>
        <div style={{ width: '100%', height: '100%', borderRadius: 48, overflow: 'hidden', position: 'relative' }}>
          <div style={{
            position: 'absolute', top: 11, left: '50%', transform: 'translateX(-50%)',
            width: 124, height: 36, borderRadius: 22, background: '#000', zIndex: 50,
          }} />
          {children}
        </div>
      </div>
    </div>
  );
}

// ─── Helpers ─────────────────────────────────────
function urlBase64ToUint8Array(b64) {
  const padding = '='.repeat((4 - b64.length % 4) % 4);
  const base64 = (b64 + padding).replace(/-/g, '+').replace(/_/g, '/');
  const raw = atob(base64);
  const arr = new Uint8Array(raw.length);
  for (let i = 0; i < raw.length; i++) arr[i] = raw.charCodeAt(i);
  return arr;
}

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