/* ============================================================
   app.jsx — root component, theming, live state, routing
   ============================================================ */

const PAGE_LABELS = {
  today:'Today', employees:'Employees', pto:'Requests', monthly:'Summary',
  reports:'Reports', tasks:'Tasks', support:'Support', oncall:'On-call', messages:'Messages', audit:'Audit log', ml:'Neural',
  teams:'Teams', rooms:'Rooms', bots:'Bots', 'bot-behavior':'Bot Behavior', providers:'Integrations', server:'Server',
  captures:'Captures', collections:'Collections',
  help:'Help / API',
};
const CRUMBS = {
  today:'Activity', employees:'Activity', pto:'Activity', monthly:'Activity', reports:'Activity', tasks:'Activity', support:'Activity', oncall:'Activity',
  messages:'Insight', audit:'Insight', ml:'Insight',
  teams:'Configure', rooms:'Configure', bots:'Configure', 'bot-behavior':'Configure', providers:'Configure', server:'Configure', collections:'Configure',
  help:'Configure',
};

// Canonical page ids (the renderPage switch is the source of truth — includes
// `oncall`/`captures`, which are real pages). Used to bind the SPA page to the
// browser URL so navigation, refresh, deep-links and back/forward all work.
const ALL_PAGE_IDS = [
  'today','employees','pto','monthly','reports','tasks','support','oncall',
  'messages','audit','ml','teams','rooms','bots','bot-behavior','providers','server','help','captures','collections',
];
const pageFromPath = () => {
  try {
    const seg = (window.location.pathname || '/').replace(/^\/+/, '').split('/')[0];
    return ALL_PAGE_IDS.includes(seg) ? seg : 'today';
  } catch (_) { return 'today'; }
};
const pathForPage = (id) => (id === 'today' ? '/' : '/' + id);

// shell.jsx defines these and exports them via `window.X = X`. Babel-in-browser
// transpiles each <script type="text/babel"> independently so we re-bind here.
const LoginPage    = (typeof window !== 'undefined' && window.LoginPage)    || (() => null);
const BottomTabBar = (typeof window !== 'undefined' && window.BottomTabBar) || (() => null);

// primitives.jsx new exports — re-bind so pages can reference by name
const W = window;
const _stub = () => null;
const UserPie          = W.UserPie          || _stub;
const Refresh          = W.Refresh          || _stub;
const SearchInput      = W.SearchInput      || _stub;
const Heatmap          = W.Heatmap          || _stub;
const StatusPill       = W.StatusPill       || _stub;
const useTheme         = W.useTheme         || (() => 'dark');
const resolveAvatarUrl = W.resolveAvatarUrl || (() => null);
// VoiceWidget — always-on wake-word listener + overlay panel (page-ask-ai.jsx, admin-only)
const VoiceWidget          = W.VoiceWidget          || _stub;
// VoiceSettingsSection — gear popover Voice section (page-ask-ai.jsx, admin-only)
const VoiceSettingsSection = W.VoiceSettingsSection || _stub;
// TasksPage — page-tasks.jsx
const TasksPage = W.TasksPage || _stub;
// CollectionsPage — page-collections.jsx
const CollectionsPage = W.CollectionsPage || _stub;
const OnCallPage = W.OnCallPage || _stub;
// SupportPage — page-support.jsx
const SupportPage = W.SupportPage || _stub;
// BotBehaviorPage — page-bot-behavior.jsx
const BotBehaviorPage = W.BotBehaviorPage || _stub;
// parseDetail is consumed directly in page-* files (not used in app.jsx itself,
// but window.parseDetail must be defined before page scripts parse it at load time).
// primitives.jsx loads first and sets window.parseDetail.
// AnimatedHeading + useFadeInOnScroll — primitives.jsx motion exports
const AnimatedHeading = W.AnimatedHeading || (({ text, tag: Tag = 'h2', className = '', style = {} }) => <Tag className={className} style={style}>{text}</Tag>);
const useFadeInOnScroll = W.useFadeInOnScroll || (() => {});
const VPSieServerSelect = W.VPSieServerSelect || _stub;

function App() {
  const [authed, setAuthed]   = useState(null); // null = checking; false = login screen; true = dashboard
  // Page state is bound to the URL: initialize from the path (deep-link), and
  // wrap the setter so every navigation pushes /<page> without touching any
  // call site (Sidebar, Topbar, CommandPalette, BottomTabBar, status-nav all
  // receive this same setActive).
  const [active, _setActive]  = useState(pageFromPath);
  const setActive = React.useCallback((next) => {
    _setActive(prev => {
      const val = typeof next === 'function' ? next(prev) : next;
      try {
        const path = pathForPage(val);
        if (window.location.pathname !== path) {
          window.history.pushState({ page: val }, '', path);
        }
      } catch (_) {}
      return val;
    });
  }, []);
  // Persist theme in localStorage so the picked light/dark survives
  // reloads. Aesthetic is LOCKED to 'editorial' — the other 9 directions
  // (pro/terminal/soft/ivory/paper/coal/gold/emerald/copper/violet) were
  // removed at user request. The picker UI is gone; setAest is a no-op
  // kept only so existing call sites don't crash.
  const [theme, setTheme]     = useState(() => {
    try { return localStorage.getItem('theme') || 'dark'; } catch { return 'dark'; }
  });
  const aesthetic = 'editorial';
  const setAest = () => {}; // no-op — aesthetic is locked
  const [density, setDensity] = useState(() => {
    try { return localStorage.getItem('density') || 'balanced'; } catch { return 'balanced'; }
  });
  const [collapsed, setCollapsed] = useState(false);
  const [cmdOpen, setCmdOpen]     = useState(false);
  const [tweaksOpen, setTweaksOpen] = useState(false);

  // Live state — sourced from window.STATUS_DATA, refreshed when real data arrives
  const D = window.STATUS_DATA;
  const [employees, setEmployees] = useState(D.EMPLOYEES);
  const [activity, setActivity]   = useState(D.ACTIVITY);
  const [ptoState, setPtoState]   = useState(D.PTO);
  const [aiText, setAiText]       = useState(D.ROLLUP_TEXT);
  const [aiLoading, setAiLoading] = useState(false);
  const [, forceTick]             = useState(0);
  // Blur loading overlay for EXPLICIT in-app reloads (Refresh button, range
  // changes) — a faded snapshot of the current tab with the Pulse logo. The
  // silent 45s auto-refresh does not trigger it.
  const [refreshing, setRefreshing] = useState(false);
  const refreshStartRef = React.useRef(0);  // when the current refresh began (min-display timing)

  // Refresh state when async fetches in data.jsx complete
  useEffect(() => {
    const onReady = () => {
      const D2 = window.STATUS_DATA;
      setEmployees(D2.EMPLOYEES);
      setActivity(D2.ACTIVITY);
      setPtoState(D2.PTO);
      setAiText(D2.ROLLUP_TEXT);
      forceTick(t => t + 1);
      // Keep the pulse overlay up for a minimum beat so it reads as a
      // deliberate refresh (matches the boot loading screen) instead of a flash.
      const MIN_MS = 850;
      const started = refreshStartRef.current || 0;
      const elapsed = started ? (Date.now() - started) : MIN_MS;
      if (started && elapsed < MIN_MS) {
        setTimeout(() => setRefreshing(false), MIN_MS - elapsed);
      } else {
        setRefreshing(false);
      }
      refreshStartRef.current = 0;
    };
    window.addEventListener('status-data-ready', onReady);
    if ((window.STATUS_DATA.EMPLOYEES || []).length > 0) onReady();
    return () => window.removeEventListener('status-data-ready', onReady);
  }, []);

  // Show the blur overlay on explicit refresh requests (skip silent auto).
  useEffect(() => {
    let safety;
    const onRefresh = (e) => {
      if (e && e.detail && e.detail.silent) return;
      refreshStartRef.current = Date.now();
      setRefreshing(true);
      clearTimeout(safety);
      safety = setTimeout(() => setRefreshing(false), 5000);  // never get stuck
    };
    window.addEventListener('status-data-refresh-requested', onRefresh);
    return () => { window.removeEventListener('status-data-refresh-requested', onRefresh); clearTimeout(safety); };
  }, []);

  // Auto-refresh EVERYTHING on a gentle interval — a silent full re-bootstrap
  // (all datasets: employees, requests, teams, rooms, reports, audit, server…)
  // so every tab stays current with no manual refresh. onReady only updates
  // data + re-renders; it never changes the active tab or closes open modals
  // (those keep their own local state). The Refresh button stays available for
  // an instant on-demand reload.
  useEffect(() => {
    const id = setInterval(() => {
      try {
        if (document.hidden) return;  // don't churn a backgrounded tab
        // silent:true → the blur loading overlay does NOT show for the gentle
        // background auto-refresh (only explicit user refreshes show it).
        window.dispatchEvent(new CustomEvent('status-data-refresh-requested', { detail: { silent: true } }));
      } catch (e) {}
    }, 45000);
    return () => clearInterval(id);
  }, []);

  // Listen for in-app navigation (Today tab Infrastructure cards dispatch this).
  useEffect(() => {
    const onNav = (e) => {
      const tab = e?.detail?.tab || e?.detail?.page;
      if (tab && PAGE_LABELS[tab]) setActive(tab);
    };
    window.addEventListener('status-nav', onNav);
    window.addEventListener('nav-to', onNav);  // A14 crumb click
    // Expose for direct callers
    window.__navigate = (tab) => { if (PAGE_LABELS[tab]) setActive(tab); };
    return () => {
      window.removeEventListener('status-nav', onNav);
      window.removeEventListener('nav-to', onNav);
    };
  }, []);

  // URL <-> page binding: seed the initial history entry once, and restore the
  // page on browser Back/Forward (popstate). Uses the raw setter so Back does
  // not push a redundant entry.
  useEffect(() => {
    try { window.history.replaceState({ page: active }, '', pathForPage(active)); } catch (_) {}
    const onPop = () => _setActive(pageFromPath());
    window.addEventListener('popstate', onPop);
    return () => window.removeEventListener('popstate', onPop);
  }, []);

  // #35: clicking any PersonCell anywhere in the app fires this event.
  // We jump to Employees and stash the requested user id on window so
  // page-employees.jsx auto-opens its drawer on mount/re-render.
  useEffect(() => {
    const onOpenUser = (e) => {
      try {
        const d = e?.detail || {};
        const uid = d.user_id;
        const mxid = d.matrix_id;
        // Stash a hint that EmployeesPage will read on next render.
        window.__focusUser = { user_id: uid, matrix_id: mxid, ts: Date.now() };
        setActive('employees');
        // Re-dispatch after the page mounts so the new instance can react.
        setTimeout(() => {
          try { window.dispatchEvent(new CustomEvent('focus-user-now', { detail: window.__focusUser })); } catch(_) {}
        }, 50);
      } catch (_) {}
    };
    window.addEventListener('request-open-user', onOpenUser);
    return () => window.removeEventListener('request-open-user', onOpenUser);
  }, []);

  // Fade out the inline #m-boot splash once auth has resolved (either
  // way). Splash is in index.html and renders before React/babel finish —
  // here we tell it to fade out.
  useEffect(() => {
    if (authed === null) return;
    const boot = document.getElementById('m-boot');
    if (!boot) return;
    boot.classList.add('m-boot-gone');
    const t = setTimeout(() => { try { boot.remove(); } catch (_) {} }, 400);
    return () => clearTimeout(t);
  }, [authed]);

  // Auth check on load — if /api/auth/me is 200, show dashboard; else show login
  useEffect(() => {
    fetch('/api/auth/me', {credentials:'include'})
      .then(r => setAuthed(r.ok))
      .catch(() => setAuthed(false));
  }, []);

  // Global session-expiry handler — listens to 'auth-expired' (dispatched
  // by parseDetail when any API call returns 401). Shows a one-time toast
  // then reloads so the login screen renders.
  useEffect(() => {
    const onExpired = () => {
      try {
        const t = document.createElement('div');
        t.style.cssText = 'position:fixed;top:20px;left:50%;transform:translateX(-50%);background:var(--bg-elev-2);border:1px solid var(--warn);color:var(--text-1);padding:12px 18px;border-radius:8px;font-size:14px;font-weight:500;z-index:99999;box-shadow:0 8px 24px -4px rgba(0,0,0,.4);';
        t.textContent = '🔒 Session expired — reloading to log in again…';
        document.body.appendChild(t);
      } catch(_) {}
      setTimeout(() => { try { window.location.reload(); } catch(_) {} }, 1800);
    };
    window.addEventListener('auth-expired', onExpired);
    return () => window.removeEventListener('auth-expired', onExpired);
  }, []);

  // SSE — live attendance + activity events
  useEffect(() => {
    if (authed !== true) return;
    let es;
    try {
      es = new EventSource('/api/sse');
      es.addEventListener('attendance', async () => {
        try {
          const attn = await fetch('/api/attendance', {credentials:'include'}).then(r => r.ok ? r.json() : []);
          const byUid = {}; (attn || []).forEach(a => byUid[a.user_id] = a);
          // Use the shared 12-hour + per-user-tz formatter from data.jsx so
          // live refreshes match initial render and a Cairo person's time
          // stays in their morning hour for an admin viewing from NY.
          const _fmtT = (iso, tz) => (window.fmtTime ? window.fmtTime(iso, tz) : null);
          // Build an employee-shaped object from an attendance row — used
          // for BRAND-NEW users (just invited / just signed in for the
          // first time) who weren't in the initial roster. Without this,
          // a new hire's sign-in would never appear until a full reload.
          const _rowToEmp = (a) => ({
            id: a.user_id,
            user_id: a.user_id,
            matrix_id: a.matrix_id,
            display_name: a.display_name,
            team: a.team || null,
            org_role: a.org_role || 'employee',
            is_superadmin: !!a.is_superadmin,
            signed_in: !!a.signed_in,
            signed_off: !!a.signed_off,
            was_late: !!a.was_late,
            on_time_streak: +(a.on_time_streak || 0),
            absence_confirmed: !!a.absence_confirmed,
            hours_worked: typeof a.hours_worked === 'number' ? +a.hours_worked.toFixed(2) : 0,
            blocker_text: a.blocker_text || null,
            had_blocker: !!a.had_blocker,
            sign_in_at: _fmtT(a.sign_in_at, a.timezone || null),
            sign_off_at: _fmtT(a.sign_off_at, a.timezone || null),
            is_test_account: false,
          });
          setEmployees(es2 => {
            const seen = new Set();
            // 1) Patch every existing employee in place — carry ALL the
            //    live fields the Today KPIs read (signed_in/off, was_late,
            //    absence_confirmed for the confirmed-absent count, team +
            //    is_superadmin for grouping/exclusion).
            const patched = es2.map(e => {
              const a = byUid[e.id]; seen.add(e.id);
              if (!a) return e;
              const tz = e.tz || a.timezone || null;
              return { ...e,
                signed_in: !!a.signed_in, signed_off: !!a.signed_off, was_late: !!a.was_late,
                on_time_streak: +(a.on_time_streak || 0),
                absence_confirmed: !!a.absence_confirmed,
                team: a.team != null ? a.team : e.team,
                is_superadmin: a.is_superadmin != null ? !!a.is_superadmin : e.is_superadmin,
                hours_worked: typeof a.hours_worked === 'number' ? +a.hours_worked.toFixed(2) : e.hours_worked,
                blocker_text: a.blocker_text || null, had_blocker: !!a.had_blocker,
                sign_in_at: _fmtT(a.sign_in_at, tz) || e.sign_in_at,
                sign_off_at: _fmtT(a.sign_off_at, tz) || e.sign_off_at };
            });
            // 2) Append any attendance rows for users NOT already in the
            //    roster — new hires, newly-activated users, new-team members.
            const additions = (attn || [])
              .filter(a => !seen.has(a.user_id))
              .map(_rowToEmp);
            return additions.length ? [...patched, ...additions] : patched;
          });
          // Notify page-today.jsx so it re-fetches /api/today/progress
          // (Yes/No badges + Made-progress lists update live).
          window.dispatchEvent(new CustomEvent('attendance-updated'));
        } catch(_) {}
      });
      // Live PTO/Requests updates — fires when a request is filed (room or
      // web app) or approved/declined. Refetch + update the Requests tab with
      // no manual refresh.
      es.addEventListener('pto', async () => {
        try {
          const reqs = await fetch('/api/pto/requests', {credentials:'include'}).then(r => r.ok ? r.json() : null);
          if (Array.isArray(reqs)) setPtoState(reqs.map(p => ({
            id: p.id, display_name: p.display_name || p.matrix_id, matrix_id: p.matrix_id,
            pto_type: p.pto_type || 'Vacation', start_date: p.start_date, end_date: p.end_date,
            days: p.days || 1, status: p.status || 'pending', approved: p.status === 'approved',
          })));
        } catch(_) {}
      });
      es.addEventListener('audit', async () => {
        try {
          const audit = await fetch('/api/audit?limit=20', {credentials:'include'}).then(r => r.ok ? r.json() : []);
          setActivity((audit || []).slice(0, 20).map(a => {
            // Classify
            const trig = (a.trigger || '').toLowerCase();
            const act  = (a.action_type || '').toLowerCase();
            const type =
                trig.includes('sign-in') || act.includes('signin')   ? 'signin'   :
                trig.includes('sign-off') || act.includes('signoff') ? 'signoff'  :
                trig.includes('blocker')                              ? 'blocker' :
                trig.includes('pto')                                  ? 'pto'     :
                act === 'thread_reply' || act.startsWith('bot_')      ? 'bot'     :
                act.includes('ticket') || act.includes('support')     ? 'support' :
                act.includes('strike') || act.includes('violation')   ? 'strike'  :
                act.includes('dm_sent')                               ? 'dm'      : 'admin';
            // Issue #6: show WHICH bot acted (api supplies bot_source for
            // bot-initiated actions) AND the employee it concerns — instead of
            // a generic "Bot". For human/admin actions bot_source is null, so
            // we fall back to the person.
            const botSrc = a.bot_source || null;
            const person = a.user_display_name || a.matrix_id || null;
            const who = botSrc || person || 'System';
            // Meta: humanized label · employee (when a bot acted about someone) · context tail
            const label = a.action_label || a.action_type || 'event';
            const subj  = (botSrc && person) ? ` · ${person}` : '';
            const ctx   = a.body ? ` — ${String(a.body).slice(0, 80)}` : (a.target ? ` → ${a.target}` : '');
            return {
              type, who,
              at: a.occurred_at ? (window.fmtTime ? window.fmtTime(a.occurred_at) : new Date(a.occurred_at).toLocaleTimeString([], {hour:'2-digit', minute:'2-digit'})) : '',
              meta: `${label}${subj}${ctx}`,
            };
          }));
        } catch(_) {}
      });
    } catch(_) {}
    return () => { try { es && es.close(); } catch(_){} };
  }, [authed]);

  // Apply theme + aesthetic + density to <html> and persist to localStorage.
  useEffect(() => {
    document.documentElement.dataset.theme     = theme;
    document.documentElement.dataset.aesthetic = aesthetic;
    document.documentElement.dataset.density   = density;
    try {
      localStorage.setItem('theme', theme);
      localStorage.setItem('aesthetic', aesthetic);
      localStorage.setItem('density', density);
    } catch {}
  }, [theme, aesthetic, density]);

  // Cmd+K
  useEffect(() => {
    const onKey = (e) => {
      if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') {
        e.preventDefault(); setCmdOpen(o => !o);
      }
    };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, []);

  // Real polling: refetch /api/attendance every 30s to keep hours/status fresh
  useEffect(() => {
    const id = setInterval(async () => {
      if (document.hidden) return;  // don't churn a backgrounded tab
      try {
        const attn = await fetch('/api/attendance', {credentials:'include'}).then(r => r.ok ? r.json() : []);
        const byUid = {}; (attn || []).forEach(a => byUid[a.user_id] = a);
        setEmployees(es => es.map(e => {
          const a = byUid[e.id]; if (!a) return e;
          return { ...e, signed_in: !!a.signed_in, signed_off: !!a.signed_off, was_late: !!a.was_late,
                   hours_worked: typeof a.hours_worked === 'number' ? +a.hours_worked.toFixed(2) : e.hours_worked,
                   blocker_text: a.blocker_text || null, had_blocker: !!a.had_blocker };
        }));
        // Keep the Requests tab fresh too (covers web-app submissions that
        // don't emit an SSE event) — no manual refresh needed.
        const reqs = await fetch('/api/pto/requests', {credentials:'include'}).then(r => r.ok ? r.json() : null);
        if (Array.isArray(reqs)) setPtoState(reqs.map(p => ({
          id: p.id, display_name: p.display_name || p.matrix_id, matrix_id: p.matrix_id,
          pto_type: p.pto_type || 'Vacation', start_date: p.start_date, end_date: p.end_date,
          days: p.days || 1, status: p.status || 'pending', approved: p.status === 'approved',
        })));
      } catch(e) { /* ignore */ }
    }, 30000);
    return () => clearInterval(id);
  }, []);

  // Tweaks panel protocol
  useEffect(() => {
    const onMsg = (e) => {
      // Same-origin only — ignore messages from cross-origin frames.
      if (!e || (e.origin && e.origin !== window.location.origin)) return;
      if (e.data?.type === '__activate_edit_mode')   setTweaksOpen(true);
      if (e.data?.type === '__deactivate_edit_mode') setTweaksOpen(false);
    };
    window.addEventListener('message', onMsg);
    try {
      if (window.parent && window.parent !== window && window.parent.location.origin === window.location.origin) {
        window.parent.postMessage({ type: '__edit_mode_available' }, window.location.origin);
      }
    } catch (_e) { /* cross-origin parent: ignore */ }
    return () => window.removeEventListener('message', onMsg);
  }, []);

  const live = {
    employees,
    activity,
    // "Live" = currently working: signed in today AND not signed off yet.
    // (signed_in alone counts everyone who came in today — 21/24 while only
    // a handful are actually still on the clock.)
    liveCount: employees.filter(e => e.signed_in && !e.sign_off_at).length,
    totalCount: employees.length,
  };
  const ai = { text: aiText, loading: aiLoading };

  const counts = {
    pto: ptoState.filter(p => p.status === 'pending').length || null,
    // (Audit log had a hardcoded "12" placeholder badge — removed; it was not
    // a real unread count and never cleared. The Audit log is a browsable
    // trail, not a notifications inbox, so it carries no badge.)
  };

  const renderPage = () => {
    let content;
    switch (active) {
      case 'today':     content = <TodayPage live={live} ai={ai} />; break;
      case 'employees': content = <EmployeesPage live={live} />; break;
      case 'pto':       content = <PtoPage ptoState={ptoState} setPtoState={setPtoState} />; break;
      case 'monthly':   content = <MonthlyPage live={live} />; break;
      case 'reports':   content = <ReportsPage />; break;
      case 'tasks':     content = <TasksPage />; break;
      case 'collections': content = <CollectionsPage />; break;
      case 'support':   content = <SupportPage />; break;
      case 'oncall':    content = <OnCallPage />; break;
      case 'messages':  content = <MessagesPage />; break;
      case 'audit':     content = <AuditPage />; break;
      case 'teams':     content = <TeamsPage />; break;
      case 'rooms':     content = <RoomsPage />; break;
      case 'bots':         content = <BotsPage />; break;
      case 'bot-behavior': content = <BotBehaviorPage />; break;
      case 'providers':    content = <ProvidersPage />; break;
      case 'server':    content = <ServerPage />; break;
      case 'help':      content = <HelpPage />; break;
      case 'captures':  content = (window.CapturesAdminPage ? <window.CapturesAdminPage /> : <div className="muted">Loading…</div>); break;
      case 'ml':        content = (window.MLPage ? <window.MLPage /> : <div className="muted">Loading…</div>); break;
      default:          content = <TodayPage live={live} ai={ai} />; break;
    }
    return <div key={active} className="page-transition-enter">{content}</div>;
  };

  // Keep the inline #m-boot splash visible while authed === null (initial
  // /api/auth/me check). It's already on the page from index.html — we
  // just don't render anything until we know. Once authed flips, the
  // useEffect below fades the splash out.
  if (authed === null) return null;

  const isMobile = typeof window.M_isMobileSurface === 'function' && window.M_isMobileSurface();

  if (authed === false) {
    // Editorial mobile login on Capacitor / touch phones — desktop sees the
    // existing LoginPage with the marketing left rail.
    if (isMobile && window.MobileLoginPage) {
      return (
        <ToastProvider>
          <window.MobileLoginPage onLogin={() => { try { localStorage.removeItem('m_signed_out'); } catch(_){} setAuthed(true); window.location.reload(); }} />
        </ToastProvider>
      );
    }
    return (
      <ToastProvider>
        <LoginPage onLogin={() => { try { localStorage.removeItem('m_signed_out'); } catch(_){} setAuthed(true); window.location.reload(); }} theme={theme} setTheme={setTheme} />
      </ToastProvider>
    );
  }

  // v3.0.0 — on iOS Capacitor (or a true touch phone), render the
  // mobile-first surface instead of the desktop shell. Desktop browsers
  // and tablets continue to see the dense admin dashboard.
  if (isMobile) {
    const MobileApp = window.MobileApp;
    if (MobileApp) {
      return (
        <ToastProvider>
          <MobileApp me={D.ME} />
        </ToastProvider>
      );
    }
  }

  return (
    <ToastProvider>
      <div className="app-shell">
        <Sidebar
          active={active} setActive={setActive}
          collapsed={collapsed} setCollapsed={setCollapsed}
          me={D.ME} counts={counts}
          onSignOut={async () => { try { await fetch('/api/auth/logout', {method:'POST', credentials:'include'}); } catch(_){} try { localStorage.setItem('m_signed_out', '1'); } catch(_){} setAuthed(false); window.location.reload(); }}
        />
        <Topbar
          pageTitle={PAGE_LABELS[active]} pageCrumb={CRUMBS[active]}
          liveCount={live.liveCount} totalCount={live.totalCount}
          onCmdOpen={() => setCmdOpen(true)}
          theme={theme} setTheme={setTheme}
          aesthetic={aesthetic} setAesthetic={setAest}
          density={density} setDensity={setDensity}
          isAdmin={!!(window.isAdminOrAbove ? window.isAdminOrAbove(D.ME) : (D.ME && (D.ME.is_admin || D.ME.org_role === 'admin' || (D.ME.role || '').toLowerCase() === 'admin')))}
          isManager={!!(D.ME && (D.ME.org_role === 'manager' || (D.ME.role || '').toLowerCase() === 'manager'))}
        />
        <main className="main">
          {renderPage()}
        </main>
        <BottomTabBar active={active} setActive={setActive} />
        {refreshing && (
          <div aria-hidden="true" style={{
            position:'fixed', inset:0, zIndex:8500,
            display:'flex', alignItems:'center', justifyContent:'center',
            background: theme === 'light' ? 'rgba(247,242,232,0.28)' : 'rgba(10,10,13,0.30)',
            backdropFilter:'blur(7px) saturate(115%)', WebkitBackdropFilter:'blur(7px) saturate(115%)',
            pointerEvents:'none', animation:'m-boot-fade-in .2s ease both',
          }}>
            {/* Same breathing pulse mark as the boot loading screen (#m-boot). */}
            <div style={{ width:84, height:84, borderRadius:20, overflow:'hidden',
              boxShadow:'0 18px 50px -14px rgba(0,0,0,.45)',
              animation:'m-boot-breathe 1.8s ease-in-out infinite both' }}>
              <img src="/api/branding/favicon" alt="" width={84} height={84}
                style={{ width:'100%', height:'100%', objectFit:'cover', display:'block' }}
                onError={(ev) => {
                  // Never leave an empty box: fall back to an inline pulse glyph.
                  ev.currentTarget.onerror = null;
                  ev.currentTarget.src = "data:image/svg+xml;utf8," + encodeURIComponent(
                    '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" rx="16" fill="#16a34a"/><path d="M8 34 H22 L27 20 L34 46 L39 30 L43 34 H56" fill="none" stroke="#fff" stroke-width="3.6" stroke-linecap="round" stroke-linejoin="round"/></svg>'
                  );
                }} />
            </div>
          </div>
        )}
        <CommandPalette
          open={cmdOpen} onClose={() => setCmdOpen(false)}
          setActive={setActive} employees={employees}
          theme={theme} setTheme={setTheme} setAesthetic={setAest}
        />
        <AskAIWidget />
        <VoiceWidget />
        {(typeof window.HUD === 'function') && React.createElement(window.HUD)}
        {tweaksOpen && (
          <StatusTweaksPanel
            theme={theme} setTheme={setTheme}
            aesthetic={aesthetic} setAesthetic={setAest}
            density={density} setDensity={setDensity}
            onClose={() => {
              setTweaksOpen(false);
              try { if (window.parent && window.parent !== window && window.parent.location && window.parent.location.origin === window.location.origin) { window.parent.postMessage({ type:'__edit_mode_dismissed' }, window.location.origin); } } catch (_e) { /* cross-origin parent */ }
            }}
          />
        )}
      </div>
    </ToastProvider>
  );
}

function StatusTweaksPanel({ theme, setTheme, aesthetic, setAesthetic, density, setDensity, onClose }) {
  const T = window.appT || ((s) => s);
  const lang = (window.appLang || (() => 'en'))();
  return (
    <div style={{ position:'fixed', bottom: 24, right: 24, zIndex: 180, width: 280, background:'var(--bg-elev-1)', border:'1px solid var(--line-hi)', borderRadius:'var(--r-lg)', boxShadow:'0 20px 60px -20px #000000a0' }}>
      <div style={{ display:'flex', alignItems:'center', justifyContent:'space-between', padding:'12px 14px', borderBottom:'1px solid var(--line)' }}>
        <div style={{ fontWeight: 600, fontSize: 13 }}>{T('Tweaks')}</div>
        <button className="btn btn-ghost btn-icon" onClick={onClose}><Icon name="x" size={13}/></button>
      </div>
      <div style={{ padding: 14, display:'flex', flexDirection:'column', gap: 14 }}>
        <Tweak label={T('Theme')}>
          <Segmented value={theme} onChange={setTheme}
            options={[
              {value:'light',label:T('Light')},
              {value:'dark',label:T('Dark')},
            ]} />
        </Tweak>
        {/* Language — mirrors the mobile app's English / العربية switch so an
            admin can change the interface language from the desktop too. Shares
            the same 'm_lang' preference as the phone; switching reloads so the
            new strings + RTL direction apply cleanly everywhere. */}
        <Tweak label={T('Language')}>
          <Segmented value={lang} onChange={(v) => window.setAppLang(v)}
            options={[
              {value:'en',label:'English'},
              {value:'ar',label:'العربية'},
            ]} />
        </Tweak>
        {/* Aesthetic picker removed — Editorial Calm is the only direction. */}
        {/* Density display options removed per request — fixed density. */}
        <div style={{ fontSize: 11.5, color:'var(--text-3)', borderTop:'1px solid var(--line)', paddingTop: 12 }}>
          {T('Editorial Calm · light + dark. Status colors stay constant.')}
        </div>
      </div>
    </div>
  );
}

function Tweak({ label, children }) {
  return (
    <div>
      <div style={{ fontSize: 11, fontFamily:'var(--font-mono)', color:'var(--text-3)', textTransform:'uppercase', letterSpacing: '0.06em', marginBottom: 6 }}>{label}</div>
      {children}
    </div>
  );
}

/* Top-level React error boundary. Catches ANY render exception anywhere
 * in the tree and replaces the blank screen with a recoverable fallback
 * (with the actual error message + a "Reload" button). Without this,
 * one buggy field in one drawer can take down the entire dashboard. */
class AppErrorBoundary extends React.Component {
  constructor(props) { super(props); this.state = { error: null, info: null }; }
  static getDerivedStateFromError(error) { return { error }; }
  componentDidCatch(error, info) {
    try { console.error('App crashed:', error, info); } catch (_) {}
    this.setState({ info });
  }
  render() {
    if (!this.state.error) return this.props.children;
    const reload = () => { try { location.reload(); } catch (_) {} };
    const msg = String(this.state.error?.message || this.state.error);
    const stack = String(this.state.error?.stack || '').split('\n').slice(0, 6).join('\n');
    return (
      <div style={{
        position:'fixed', inset:0, display:'flex', alignItems:'center', justifyContent:'center',
        background:'var(--bg, #0a0a0d)', color:'var(--text, #ddd)',
        fontFamily:'-apple-system, BlinkMacSystemFont, Segoe UI, sans-serif',
        padding:24, zIndex:99999,
      }}>
        <div style={{
          maxWidth:560, width:'100%',
          background:'var(--bg-elev-1, #18181f)', border:'1px solid var(--line, #2a2a35)',
          borderRadius:12, padding:24, boxShadow:'0 24px 80px -20px #000000c0',
        }}>
          <div style={{ fontSize:18, fontWeight:600, marginBottom:8 }}>
            Something broke
          </div>
          <div style={{ fontSize:13, color:'var(--text-2, #aaa)', marginBottom:18, lineHeight:1.5 }}>
            The dashboard hit an unexpected error. Your data is safe — reload to
            recover. If this keeps happening on the same screen, screenshot the
            details below and report the bug.
          </div>
          <pre style={{
            fontSize:11.5, background:'var(--bg, #0a0a0d)', padding:10, borderRadius:6,
            border:'1px solid var(--line, #2a2a35)', color:'var(--text-3, #888)',
            maxHeight:200, overflow:'auto', marginBottom:16,
            whiteSpace:'pre-wrap', wordBreak:'break-word',
          }}>{msg}{stack ? '\n\n' + stack : ''}</pre>
          <div style={{ display:'flex', gap:8, justifyContent:'flex-end' }}>
            <button onClick={reload} style={{
              background:'#7c3aed', color:'#fff', border:0, padding:'8px 16px',
              borderRadius:8, fontWeight:600, cursor:'pointer', fontSize:13,
            }}>Reload dashboard</button>
          </div>
        </div>
      </div>
    );
  }
}

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