/* ============================================================
   shell.jsx — sidebar, topbar, command palette, login
   ============================================================ */

/* ── Branding bootstrap ─────────────────────────────────────
   Fetch /api/branding once, store in window.__branding, apply
   document.title and --color-brand CSS var immediately.
   ─────────────────────────────────────────────────────────── */
window.__branding = window.__branding || {
  company_name: 'k9.ms',
  product_name: 'attendance',
  logo_url: '',
  favicon_url: '',
  brand_color: '#6366f1',
  tagline: '',
  login_slogan: 'Sign in to your team.',
  email_footer: '',
};

function _applyBranding(b) {
  window.__branding = b;
  if (b.product_name) document.title = b.product_name;
  if (b.brand_color) document.documentElement.style.setProperty('--color-brand', b.brand_color);
  if (b.favicon_url) {
    let link = document.querySelector("link[rel~='icon']");
    if (!link) { link = document.createElement('link'); link.rel = 'icon'; document.head.appendChild(link); }
    link.href = b.favicon_url;
  }
}

(function _loadBranding() {
  // Use /api/branding/all (public) and map to the shell's legacy shape.
  fetch('/api/branding/all', { credentials: 'include' })
    .then(r => r.ok ? r.json() : null)
    .then(b => {
      if (!b) return;
      _applyBranding({
        product_name: b.brand_name || 'attendance',
        company_name: '',  // single brand: header shows brand_name once + slogan
        logo_url:     b.logo_dark_url || b.logo_light_url || '',
        favicon_url:  b.favicon_url || '',
        brand_color:  b.primary_color || '#6366f1',
        tagline:      b.slogan || '',
        login_slogan: b.slogan || 'Sign in to your team.',
        email_footer: b.support_email || '',
        domain:       b.domain || '',
      });
    })
    .catch(() => {});
})();

const NAV_GROUPS = [
  { label: 'Activity', items: [
    { id: 'today',     label: 'Today',     icon: 'today',    tip: 'Live attendance & team status' },
    { id: 'employees', label: 'Employees', icon: 'team',     tip: 'Employee profiles & compliance' },
    { id: 'pto',       label: 'Requests',  icon: 'pto',      tip: 'PTO & leave request approvals' },
    { id: 'monthly',   label: 'Summary',   icon: 'chart',    tip: 'Monthly attendance summary' },
    { id: 'reports',   label: 'Reports',   icon: 'report',   tip: 'Charts, trends & export' },
    { id: 'tasks',     label: 'Tasks',     icon: 'audit',    tip: 'Track completed, blocked & stalled tasks' },
    // Captures nav entry removed — automatic capture cadence runs in the
    // background; on-demand capture is surfaced inside Bot Behavior now.
    { id: 'support',   label: 'Support',   icon: 'alert',    tip: 'Tickets & support stats' },
    { id: 'oncall',    label: 'On-call',   icon: 'alert',    tip: 'On-call rota & escalation' },
  ]},
  { label: 'Insight', items: [
    { id: 'messages',  label: 'Messages',  icon: 'msg',      tip: 'All standup messages & DM history' },
    { id: 'audit',     label: 'Audit log', icon: 'audit',    tip: 'Admin actions & system audit trail' },
  ]},
  { label: 'Configure', items: [
    { id: 'teams',        label: 'Teams',        icon: 'building', tip: 'Manage teams & default settings' },
    { id: 'rooms',        label: 'Rooms',        icon: 'door',     tip: 'Matrix room assignments' },
    { id: 'bots',         label: 'Bots',         icon: 'ai',       tip: 'Bot configuration & personas' },
    { id: 'bot-behavior', label: 'Bot Behavior', icon: 'sliders', tip: 'Thresholds, keywords, templates & AI prompts' },
    { id: 'providers',    label: 'Integrations', icon: 'plug',     tip: 'Slack, Microsoft Teams, WhatsApp and other third-party integrations' },
    { id: 'server',       label: 'Server',       icon: 'server',   tip: 'Server health & access logs' },
    { id: 'help',         label: 'Help / API',   icon: 'info',     tip: 'API reference & documentation' },
  ]},
];

function Sidebar({ active, setActive, collapsed, setCollapsed, me, counts, onSignOut }) {
  const [profileOpen, setProfileOpen] = useState(false);
  // Re-render sidebar avatar when theme changes (auto preference)
  const _useTheme = window.useTheme || (() => 'dark');
  const _resolveAvatarUrl = window.resolveAvatarUrl || (() => null);
  _useTheme(); // subscribe to theme changes
  const sidebarAvatarUrl = _resolveAvatarUrl(me);
  return (
    <aside className={`side ${collapsed ? 'collapsed' : ''} hide-on-mobile`}>
      <div className="brand" style={{cursor:'pointer'}} onClick={() => window.location.reload()} title="Click to reload">
        <div className="brand-glyph" style={{overflow:'hidden', display:'flex', alignItems:'center', justifyContent:'center'}}>
          <img src={`/api/branding/favicon?ts=${Math.floor(Date.now()/3600000)}`} alt="logo" width={28} height={28}
               style={{borderRadius:6, objectFit:'cover'}}
               onError={(e) => { e.currentTarget.style.display='none'; e.currentTarget.parentElement.innerHTML = ''; const g = document.createElement('div'); g.innerHTML = '<svg width=\"28\" height=\"28\" viewBox=\"0 0 32 32\"><rect x=\"0\" y=\"0\" width=\"32\" height=\"32\" rx=\"7\" fill=\"var(--brand)\"/><text x=\"16\" y=\"22\" text-anchor=\"middle\" font-size=\"18\" font-weight=\"700\" fill=\"#fff\" font-family=\"Inter,sans-serif\">S</text></svg>'; e.currentTarget.parentElement.appendChild(g); }} />
        </div>
        {!collapsed && (
          <div className="brand-mark">
            <span className="name">{(window.__branding && window.__branding.product_name) || 'status'}</span>
            <span className="sub">{(() => { const b = window.__branding || {}; if (b.tagline) return b.tagline; if (b.company_name && b.product_name && b.company_name !== b.product_name) return b.company_name + " / " + b.product_name; return ""; })()}</span>
          </div>
        )}
      </div>

      <NavSections collapsed={collapsed} active={active} setActive={setActive} counts={counts} />

      <div className="side-bottom">
        <div className="me-block" onClick={() => setProfileOpen(true)} style={{cursor:'pointer'}} title="Open profile">
          {sidebarAvatarUrl
            ? <img className="me-avatar" src={sidebarAvatarUrl} alt="" style={{objectFit:'cover'}} />
            : <div className="me-avatar">{me.initials}</div>}
          {!collapsed && (
            <div className="meta">
              <div className="n">{me.display_name}</div>
              <div className="r">● {me.org_role}</div>
            </div>
          )}
        </div>
        {!collapsed && (
          <div style={{display:'flex', gap: 6, marginTop: 4}}>
            <button className="btn btn-ghost" style={{ flex:1, justifyContent:'center' }} onClick={() => setCollapsed(true)}>
              <Icon name="shrink" size={14} /> Collapse
            </button>
            <button className="btn btn-ghost" style={{ justifyContent:'center', color:'var(--err)' }} onClick={onSignOut} title="Sign out">
              <Icon name="x" size={14} />
            </button>
          </div>
        )}
        {collapsed && (
          <button className="btn btn-ghost btn-icon" style={{ width:'100%' }} onClick={() => setCollapsed(false)} title="Expand sidebar">
            <Icon name="expand" size={14} />
          </button>
        )}
      </div>
      {profileOpen && ReactDOM.createPortal(
        <ProfileModal me={me} onClose={() => setProfileOpen(false)} />,
        document.body
      )}
    </aside>
  );
}

// Topbar mic enable/disable. Reads/writes localStorage.voice_enabled and
// dispatches 'voice-pref-change' so the Ask-AI widget can show/hide its
// inline mic button. This is the SINGLE entry point for turning the mic on
// site-wide — replaces the old floating "Voice off" pill.
function MicToggle() {
  const [on, setOn] = useState(() => localStorage.getItem('voice_enabled') === '1');
  useEffect(() => {
    const h = () => setOn(localStorage.getItem('voice_enabled') === '1');
    window.addEventListener('voice-pref-change', h);
    return () => window.removeEventListener('voice-pref-change', h);
  }, []);
  const toggle = () => {
    const next = !on;
    if (next) localStorage.setItem('voice_enabled', '1');
    else localStorage.removeItem('voice_enabled');
    setOn(next);
    window.dispatchEvent(new CustomEvent('voice-pref-change', { detail: { enabled: next } }));
  };
  return (
    <button
      className={`btn btn-icon hide-on-mobile${on ? ' active' : ''}`}
      onClick={toggle}
      title={on ? 'Voice ON — mic available in Ask AI. Click to disable.' : 'Enable voice (Ask AI mic)'}
      style={on ? { background:'var(--brand,#4af)', color:'#fff' } : {}}
    >
      <Icon name="mic" size={15} />
    </button>
  );
}

function Clock() {
  const [t, setT] = useState(new Date());
  const [tz, setTz] = useState(null);
  useEffect(() => {
    const id = setInterval(() => setT(new Date()), 1000);
    return () => clearInterval(id);
  }, []);
  useEffect(() => {
    let alive = true;
    fetch('/api/auth/me', { credentials: 'same-origin' })
      .then(r => r.ok ? r.json() : null)
      .then(j => { if (alive && j && j.timezone) setTz(j.timezone); })
      .catch(() => {});
    return () => { alive = false; };
  }, []);
  const tOpts  = { hour:'2-digit', minute:'2-digit', second:'2-digit', hour12:true, ...(tz ? { timeZone: tz } : {}) };
  const dOpts  = { weekday:'short', month:'short', day:'numeric', ...(tz ? { timeZone: tz } : {}) };
  let offset = '';
  if (tz) {
    try {
      const parts = new Intl.DateTimeFormat('en-US', { timeZone: tz, timeZoneName: 'shortOffset' }).formatToParts(t);
      const o = parts.find(p => p.type === 'timeZoneName');
      offset = (o && o.value) ? o.value : tz;
    } catch (_) { offset = tz; }
  }
  return (
    <div className="clock">
      <div className="t">{t.toLocaleTimeString([], tOpts)}</div>
      <div className="d">{t.toLocaleDateString([], dOpts)}{offset ? ' · ' + offset : ''}</div>
    </div>
  );
}

function Topbar({ pageTitle, pageCrumb, liveCount, totalCount, onCmdOpen, theme, setTheme, aesthetic, setAesthetic, density, setDensity, isAdmin, isManager }) {
  // Phase 1 — admin/manager Edit Mode toggle. Wraps the global useEditMode
  // hook so this top-bar button stays in sync with any other component that
  // flips the flag (e.g. keyboard shortcut in a future phase).
  const editMode = (typeof useEditMode === 'function') ? useEditMode() : false;
  const canSeeEditToggle = !!(isAdmin || isManager);
  const [qrOpen, setQrOpen] = useState(false);
  const [settingsOpen, setSettingsOpen] = useState(false);
  const [brandingOpen, setBrandingOpen] = useState(false);
  const [sseFlash, setSseFlash] = useState(false);
  const [notifEnabled, setNotifEnabled] = useState(() => {
    try { return localStorage.getItem('notif_enabled') === 'true'; } catch { return false; }
  });
  const [notifCount, setNotifCount] = useState(0);

  // PWA install prompt — capture beforeinstallprompt at topbar level (survives modal lifecycle)
  const [installPrompt, setInstallPrompt] = useState(null);
  const [isInstalled, setIsInstalled] = useState(() =>
    window.matchMedia && window.matchMedia('(display-mode: standalone)').matches
  );

  useEffect(() => {
    if (isInstalled) return;
    const handler = (e) => { e.preventDefault(); setInstallPrompt(e); };
    const _appInstalledHandler = () => { setInstallPrompt(null); setIsInstalled(true); };
    window.addEventListener('beforeinstallprompt', handler);
    window.addEventListener('appinstalled', _appInstalledHandler);
    return () => {
      window.removeEventListener('beforeinstallprompt', handler);
      window.removeEventListener('appinstalled', _appInstalledHandler);
    };
  }, [isInstalled]);

  const doTopbarInstall = async () => {
    if (!installPrompt) return;
    installPrompt.prompt();
    const { outcome } = await installPrompt.userChoice;
    if (outcome === 'accepted') { setInstallPrompt(null); setIsInstalled(true); }
  };

  // Visit count toast — show on 3rd visit, once only
  const toast = useToast();
  const visitToastFiredRef = useRef(false);
  useEffect(() => {
    if (visitToastFiredRef.current) return;
    if (localStorage.getItem('status_install_toast_dismissed') === 'true') return;
    visitToastFiredRef.current = true;
    let count = 0;
    try { count = parseInt(localStorage.getItem('status_visit_count') || '0', 10) || 0; } catch {}
    count += 1;
    try { localStorage.setItem('status_visit_count', String(count)); } catch {}
    if (count === 3) {
      setTimeout(() => {
        toast.push({
          msg: 'Install Status as an app (beta) — press Cmd+Shift+A or use Settings > Install / QR code',
          kind: 'info',
          icon: '↓',
          duration: 10000,
        });
      }, 2000);
    }
  }, []);

  // Keyboard shortcut Cmd+Shift+A → open QR
  useEffect(() => {
    const handler = (e) => {
      if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key === 'A') {
        e.preventDefault();
        setQrOpen(true);
      }
    };
    window.addEventListener('keydown', handler);
    return () => window.removeEventListener('keydown', handler);
  }, []);

  // Theme color overrides — load on mount, apply to CSS vars
  const [themeColors, setThemeColors] = useState({});
  const [themeColorsLoading, setThemeColorsLoading] = useState(false);
  const themeColorKeys = ['on_time', 'late', 'absent', 'pto', 'ai'];
  const themeColorCssVar = { on_time: '--c-on-time', late: '--c-late', absent: '--c-absent', pto: '--c-pto', ai: '--c-ai' };
  const themeColorLabel = { on_time: 'On time', late: 'Late', absent: 'Absent', pto: 'PTO', ai: 'AI accent' };
  const themeColorDefault = { on_time: '#22c55e', late: '#f59e0b', absent: '#ef4444', pto: '#6366f1', ai: '#a855f7' };

  useEffect(() => {
    fetch('/api/branding/theme', { credentials: 'include' })
      .then(r => r.ok ? r.json() : {})
      .then(data => {
        setThemeColors(data || {});
        Object.entries(data || {}).forEach(([k, v]) => {
          if (themeColorCssVar[k]) document.documentElement.style.setProperty(themeColorCssVar[k], v);
        });
      })
      .catch(() => {});
  }, []);

  const saveThemeColor = async (key, value) => {
    setThemeColors(prev => ({ ...prev, [key]: value }));
    if (themeColorCssVar[key]) document.documentElement.style.setProperty(themeColorCssVar[key], value);
    setThemeColorsLoading(true);
    try {
      await fetch('/api/branding/theme', {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        credentials: 'include',
        body: JSON.stringify({ [key]: value }),
      });
    } catch {}
    setThemeColorsLoading(false);
  };

  const clearThemeColor = async (key) => {
    setThemeColors(prev => { const n = {...prev}; delete n[key]; return n; });
    if (themeColorCssVar[key]) document.documentElement.style.removeProperty(themeColorCssVar[key]);
    try {
      await fetch('/api/branding/theme', {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        credentials: 'include',
        body: JSON.stringify({ [key]: null }),
      });
    } catch {}
  };

  const toggleNotif = async () => {
    if (!('Notification' in window)) {
      alert('This browser does not support desktop notifications.');
      return;
    }
    if (!notifEnabled) {
      const perm = await Notification.requestPermission();
      if (perm === 'granted') {
        setNotifEnabled(true);
        try { localStorage.setItem('notif_enabled', 'true'); } catch {}
        setNotifCount(0);
        (typeof Notification !== 'undefined' && Notification.permission === 'granted') && new Notification('Attendance Bot', { body: 'Attendance alerts enabled.', icon: '/static/v2/logo.png' });
      }
    } else {
      setNotifEnabled(false);
      try { localStorage.setItem('notif_enabled', 'false'); } catch {}
    }
  };

  // SSE flash — briefly highlight the live chip when any SSE event arrives
  useEffect(() => {
    const handler = () => {
      setSseFlash(true);
      setTimeout(() => setSseFlash(false), 200);
    };
    window.addEventListener('standup-bot-event', handler);
    // Also flash on attendance/audit SSE events dispatched by app.jsx
    window.addEventListener('attendance-updated', handler);
    return () => {
      window.removeEventListener('standup-bot-event', handler);
      window.removeEventListener('attendance-updated', handler);
    };
  }, []);

  // Close settings popover on outside click
  useEffect(() => {
    if (!settingsOpen) return;
    const handler = (e) => {
      if (!e.target.closest('.settings-popover-wrap')) setSettingsOpen(false);
    };
    document.addEventListener('mousedown', handler);
    return () => document.removeEventListener('mousedown', handler);
  }, [settingsOpen]);

  // Listen for SSE attendance events and fire browser notifications when enabled
  useEffect(() => {
    const handler = (e) => {
      if (!notifEnabled) return;
      try {
        const detail = e.detail || {};
        const name = detail.who || detail.display_name || 'Someone';
        const kind = detail.kind || 'signed in';
        setNotifCount(n => n + 1);
        (typeof Notification !== 'undefined' && Notification.permission === 'granted') && new Notification(`${name} ${kind}`, { body: new Date().toLocaleTimeString(), icon: '/static/v2/logo.png', silent: true });
      } catch {}
    };
    window.addEventListener('standup-bot-event', handler);
    return () => window.removeEventListener('standup-bot-event', handler);
  }, [notifEnabled]);

  return (
    <>
    <header className="top">
      <h1>
        {/* A14: crumb is clickable — jumps to the first page in that section */}
        <span className="crumb"
          style={{ cursor: pageCrumb ? 'pointer' : 'default', textDecoration: pageCrumb ? 'underline dotted transparent' : 'none' }}
          onMouseEnter={e => { if (pageCrumb) e.currentTarget.style.textDecorationColor = 'var(--text-3)'; }}
          onMouseLeave={e => { e.currentTarget.style.textDecorationColor = 'transparent'; }}
          onClick={() => {
            const map = { Activity: 'today', Insight: 'messages', Configure: 'teams' };
            const target = map[pageCrumb];
            if (target) {
              try { window.dispatchEvent(new CustomEvent('nav-to', { detail: { page: target } })); } catch(_){}
              if (typeof window.__navigate === 'function') window.__navigate(target);
              else location.hash = '#' + target;
            }
          }}
          title={pageCrumb ? `Jump to ${pageCrumb}` : ''}>
          {pageCrumb}
        </span>
        <span className="crumb-sep">/</span>
        <span>{pageTitle}</span>
      </h1>

      <div className="right">
        <button className="cmd-pill" onClick={onCmdOpen} title="Open command palette">
          <Icon name="search" size={14} />
          <span>Search or jump to…</span>
          <span className="kbd">⌘ K</span>
        </button>

        {liveCount != null && (
          <div className={`live-pill${sseFlash ? ' sse-flash' : ''}`} title="Live presence (SSE)">
            <span className="pulse" />
            <span>{liveCount}/{totalCount} live</span>
          </div>
        )}

        <Clock />

        {canSeeEditToggle && (
          <button
            className={`btn btn-icon hide-on-mobile${editMode ? ' active' : ''}`}
            onClick={() => { if (typeof setEditMode === 'function') setEditMode(!editMode); }}
            title={editMode
              ? 'Edit mode is ON — click any sign-in/sign-off cell to edit it. Click this button to turn off.'
              : 'Edit mode (admin) — turn on to inline-edit attendance, hours, strikes, and team settings.'}
            aria-label="Toggle admin Edit mode"
            style={editMode ? { background:'var(--brand,#4af)', color:'#fff' } : {}}
          >
            <Icon name="edit" size={15} />
            <span style={{ fontSize: 11, marginLeft: 4, fontWeight: 600 }}>
              {editMode ? 'EDIT ON' : 'Edit'}
            </span>
          </button>
        )}

        <button
          className="btn btn-icon hide-on-mobile"
          onClick={() => {
            try {
              if (typeof window.useHUD === 'function' && window.HUD_DEFAULT_WIDGETS) {
                // Dispatch through the shared state directly; no hook needed here.
                window.__hudState__ = { open: true, widgets: window.HUD_DEFAULT_WIDGETS, title: 'Tactical Overview' };
                window.dispatchEvent(new CustomEvent('hud-state-change', { detail: window.__hudState__ }));
              }
            } catch (_) {}
          }}
          title="HUD overlay (⌘H)"
        >
          <Icon name="grid" size={15} />
        </button>

        {/* MicToggle removed — voice persona deprecated, Ask AI is chat-only now. */}

        <div className="settings-popover-wrap" style={{ position: 'relative' }}>
          <button
            className={`btn btn-icon${settingsOpen ? ' active' : ''}`}
            onClick={() => setSettingsOpen(o => !o)}
            title="Display preferences — density, aesthetic, notifications, theme colors (admin), and install / QR."
            aria-label="Display settings"
          >
            <Icon name="cog" size={15} />
            <span style={{ fontSize: 11, marginLeft: 4, fontWeight: 600 }}>{(window.appT||(s=>s))('Settings')}</span>
          </button>
          {settingsOpen && (
            <div className="settings-popover" onClick={e => e.stopPropagation()}>
              <div className="settings-popover-title">{(window.appT||(s=>s))('Display')}</div>

              {/* Interface language — mirrors the mobile app's English / العربية
                  switch so an admin (or any user) can change the UI language here.
                  Shares the same 'm_lang' preference as the phone; switching
                  reloads so the new strings + RTL direction apply everywhere. */}
              <div className="settings-popover-section" style={{ paddingBottom: 10 }}>
                <div className="settings-popover-label" style={{ marginBottom: 8 }}>{(window.appT||(s=>s))('Language')}</div>
                <div style={{ display:'flex', gap:6 }}>
                  {[{v:'en',l:'English'},{v:'ar',l:'العربية'}].map(o => {
                    const cur = (window.appLang||(()=>'en'))();
                    const on = cur === o.v;
                    return (
                      <button
                        key={o.v}
                        className={`btn ${on ? 'btn-primary' : 'btn-ghost'}`}
                        style={{ flex:1, justifyContent:'center', fontSize:12.5, fontWeight: on?600:500 }}
                        onClick={() => { if (!on) (window.setAppLang||(()=>{}))(o.v); }}
                        aria-pressed={on}
                      >{o.l}</button>
                    );
                  })}
                </div>
              </div>

              {/* Density display options removed per request — the app uses a
                  single fixed density. */}

              {/* Aesthetic picker removed — Editorial Calm (A+D hybrid)
                  is the only direction. Theme toggle (light/dark) lives
                  in its own section above. */}

              <div className="settings-popover-section" style={{ borderTop: '1px solid var(--line)', paddingTop: 10 }}>
                <button
                  className="btn btn-ghost"
                  style={{ width: '100%', justifyContent: 'flex-start', fontSize: 12.5 }}
                  onClick={() => { toggleNotif(); setSettingsOpen(false); }}
                >
                  <Icon name="bell" size={13} />
                  {(window.appT||(s=>s))(notifEnabled ? 'Notifications on' : 'Enable notifications')}
                </button>
                <button
                  className="btn btn-ghost hide-on-mobile"
                  style={{ width: '100%', justifyContent: 'flex-start', fontSize: 12.5, marginTop: 2 }}
                  onClick={() => { setQrOpen(true); setSettingsOpen(false); }}
                >
                  <Icon name="qr" size={13} /> {(window.appT||(s=>s))('Install / QR code')}
                </button>
              </div>

              {/* Theme color overrides — admin only */}
              {isAdmin && (
                <div className="settings-popover-section" style={{ borderTop: '1px solid var(--line)', paddingTop: 10 }}>
                  <div className="settings-popover-label" style={{ marginBottom: 8 }}>{(window.appT||(s=>s))('Status colors')} {themeColorsLoading && <span style={{ color:'var(--text-3)', fontSize:10 }}>(saving…)</span>}</div>
                  {themeColorKeys.map(key => (
                    <div key={key} style={{ display:'flex', alignItems:'center', gap:8, marginBottom:6 }}>
                      <input
                        type="color"
                        value={themeColors[key] || themeColorDefault[key]}
                        onChange={e => saveThemeColor(key, e.target.value)}
                        style={{ width:28, height:22, border:'none', background:'none', cursor:'pointer', padding:0 }}
                        title={themeColorLabel[key]}
                      />
                      <span style={{ fontSize:12, flex:1 }}>{themeColorLabel[key]}</span>
                      {themeColors[key] && (
                        <button
                          className="btn btn-ghost"
                          style={{ fontSize:10, padding:'1px 6px', color:'var(--text-3)' }}
                          onClick={() => clearThemeColor(key)}
                          title="Reset to default"
                        >reset</button>
                      )}
                    </div>
                  ))}
                </div>
              )}

              {/* Voice assistant settings — admin only (V1-V10) */}
              {isAdmin && (
                <div className="settings-popover-section">
                  <VoiceSettingsSection />
                </div>
              )}

              {/* Branding — admin only */}
              {isAdmin && (
                <div className="settings-popover-section" style={{ borderTop: '1px solid var(--line)', paddingTop: 10 }}>
                  <button
                    className="btn btn-ghost"
                    style={{ width: '100%', justifyContent: 'flex-start', fontSize: 12.5 }}
                    onClick={() => { setBrandingOpen(true); setSettingsOpen(false); }}
                  >
                    <Icon name="cog" size={13} /> Branding
                  </button>
                </div>
              )}
            </div>
          )}
        </div>

        {brandingOpen && <BrandingModal onClose={() => setBrandingOpen(false)} onSaved={b => { _applyBranding(b); }} />}

        {/* Persistent PWA install button — only when browser offers install */}
        {installPrompt && !isInstalled && (
          <button
            className="btn btn-ghost"
            style={{ fontSize:12, gap:4 }}
            onClick={doTopbarInstall}
            title="Install Status as an app"
          >
            <Icon name="arrowR" size={13} /> Install app <span className="beta-pill">BETA</span>
          </button>
        )}

        <button className="btn btn-icon" onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')} title="Toggle theme">
          <Icon name={theme === 'dark' ? 'sun' : 'moon'} size={15} />
        </button>
      </div>
    </header>
    {qrOpen && <QrInstallModal onClose={() => setQrOpen(false)} />}
    </>
  );
}

/* ── Branding Modal (admin) ── */
function BrandingModal({ onClose, onSaved }) {
  const defaults = window.__branding || {};
  const [form, setForm] = useState({
    company_name:  defaults.company_name  || '',
    product_name:  defaults.product_name  || '',
    logo_url:      defaults.logo_url      || '',
    favicon_url:   defaults.favicon_url   || '',
    brand_color:   defaults.brand_color   || '#6366f1',
    tagline:       defaults.tagline       || '',
    login_slogan:  defaults.login_slogan  || '',
    email_footer:  defaults.email_footer  || '',
  });
  const [busy, setBusy] = useState(false);
  const [msg, setMsg] = useState('');
  const [logoUploading, setLogoUploading] = useState(false);
  const [faviconUploading, setFaviconUploading] = useState(false);

  const set = (k, v) => setForm(f => ({ ...f, [k]: v }));

  async function save() {
    setBusy(true); setMsg('');
    try {
      const r = await fetch('/api/branding', {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        credentials: 'include',
        body: JSON.stringify(form),
      });
      if (!r.ok) { setMsg('Save failed: ' + (await r.text())); setBusy(false); return; }
      const updated = await r.json();
      if (onSaved) onSaved(updated);
      setMsg('Saved!');
      setTimeout(onClose, 800);
    } catch (e) { setMsg('Error: ' + e.message); }
    setBusy(false);
  }

  async function uploadFile(field, url, file) {
    const setter = field === 'logo_url' ? setLogoUploading : setFaviconUploading;
    setter(true);
    try {
      const fd = new FormData();
      fd.append('file', file);
      const endpoint = field === 'logo_url'
        ? '/api/admin/branding/logo?mode=light'
        : '/api/admin/branding/favicon';
      const r = await fetch(endpoint, { method: 'POST', credentials: 'include', body: fd });
      if (!r.ok) { setMsg('Upload failed'); setter(false); return; }
      const json = await r.json();
      set(field, json.url || url);
      setMsg('Uploaded!');
      // #37: browsers cache favicon + logo aggressively, so the new
      // file isn't visible until the user hard-refreshes. Bust the
      // cached <link rel="icon"> and any <img src="/api/branding/...">
      // by re-pointing them to the same URL with a timestamp query.
      try {
        const bust = '?t=' + Date.now();
        document.querySelectorAll('link[rel~="icon"], link[rel="apple-touch-icon"]').forEach(el => {
          const base = (el.getAttribute('href') || '').split('?')[0];
          if (base.startsWith('/api/branding/')) el.href = base + bust;
        });
        document.querySelectorAll('img').forEach(el => {
          const base = (el.getAttribute('src') || '').split('?')[0];
          if (base.startsWith('/api/branding/')) el.src = base + bust;
        });
        // Tell other components on the page (Topbar, page-misc preview)
        // that branding changed, so they can re-fetch.
        window.dispatchEvent(new CustomEvent('branding-updated', { detail: { ts: Date.now() } }));
      } catch (_) { /* non-fatal */ }
    } catch (e) { setMsg('Upload error: ' + e.message); }
    setter(false);
  }

  const inp = (label, key, opts = {}) => (
    <div style={{ marginBottom: 12 }}>
      <label style={{ fontSize: 12, color: 'var(--text-2)', display: 'block', marginBottom: 4 }}>{label}</label>
      <input
        className="inp"
        type={opts.type || 'text'}
        value={form[key]}
        placeholder={opts.placeholder || ''}
        onChange={e => set(key, e.target.value)}
        style={{ fontSize: 13, ...(opts.style || {}) }}
      />
    </div>
  );

  return (
    <div className="modal-overlay" onClick={e => e.target === e.currentTarget && onClose()}>
      <div className="modal-card" style={{ width: 480, maxHeight: '85vh', overflowY: 'auto' }}>
        <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 20 }}>
          <div style={{ fontWeight: 600, fontSize: 16 }}>Branding</div>
          <button className="btn btn-icon" onClick={onClose}><Icon name="x" size={14} /></button>
        </div>

        {inp('Company name', 'company_name', { placeholder: 'k9.ms' })}
        {inp('Product name', 'product_name', { placeholder: 'attendance' })}
        {inp('Tagline', 'tagline', { placeholder: 'e.g. Team standup made simple' })}
        {inp('Login slogan', 'login_slogan', { placeholder: 'Sign in to your team.' })}
        {inp('Email footer', 'email_footer', { placeholder: 'Powered by k9.ms Attendance' })}

        <div style={{ marginBottom: 12 }}>
          <label style={{ fontSize: 12, color: 'var(--text-2)', display: 'block', marginBottom: 4 }}>Brand color</label>
          <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
            <input type="color" value={form.brand_color} onChange={e => set('brand_color', e.target.value)}
              style={{ width: 36, height: 30, border: 'none', background: 'none', cursor: 'pointer', padding: 0 }} />
            <input className="inp" value={form.brand_color} onChange={e => set('brand_color', e.target.value)}
              placeholder="#6366f1" style={{ fontSize: 13, width: 110 }} />
          </div>
        </div>

        <div style={{ marginBottom: 12 }}>
          <label style={{ fontSize: 12, color: 'var(--text-2)', display: 'block', marginBottom: 4 }}>Logo (PNG/JPG/SVG, light mode)</label>
          <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
            <input className="inp" value={form.logo_url} onChange={e => set('logo_url', e.target.value)}
              placeholder="/api/branding/logo?mode=light" style={{ fontSize: 13, flex: 1 }} />
            <label className="btn btn-ghost" style={{ cursor: 'pointer', fontSize: 12, whiteSpace: 'nowrap' }}>
              {logoUploading ? 'Uploading…' : 'Upload'}
              <input type="file" accept="image/png,image/jpeg,image/svg+xml" style={{ display: 'none' }}
                onChange={e => e.target.files[0] && uploadFile('logo_url', form.logo_url, e.target.files[0])} />
            </label>
          </div>
        </div>

        <div style={{ marginBottom: 20 }}>
          <label style={{ fontSize: 12, color: 'var(--text-2)', display: 'block', marginBottom: 4 }}>Favicon (PNG/ICO/SVG)</label>
          <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
            <input className="inp" value={form.favicon_url} onChange={e => set('favicon_url', e.target.value)}
              placeholder="/api/branding/favicon" style={{ fontSize: 13, flex: 1 }} />
            <label className="btn btn-ghost" style={{ cursor: 'pointer', fontSize: 12, whiteSpace: 'nowrap' }}>
              {faviconUploading ? 'Uploading…' : 'Upload'}
              <input type="file" accept="image/png,image/x-icon,image/svg+xml" style={{ display: 'none' }}
                onChange={e => e.target.files[0] && uploadFile('favicon_url', form.favicon_url, e.target.files[0])} />
            </label>
          </div>
        </div>

        {msg && <div style={{ marginBottom: 12, fontSize: 12.5, color: msg.startsWith('Save') || msg.startsWith('Error') || msg.startsWith('Upload failed') ? 'var(--err)' : 'var(--ok)' }}>{msg}</div>}

        <div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
          <button className="btn btn-ghost" onClick={onClose}>Cancel</button>
          <button className="btn btn-pri" onClick={save} disabled={busy}>{busy ? 'Saving…' : 'Save branding'}</button>
        </div>
      </div>
    </div>
  );
}

/* ── QR Install Modal (D3/D6) ── */
function QrInstallModal({ onClose }) {
  const url = window.location.origin + '/';
  const qrRef = useRef(null);

  // Render QR code using local QRCode.js lib (no external API call needed)
  useEffect(() => {
    if (!qrRef.current) return;
    if (typeof QRCode === 'undefined') return;
    qrRef.current.innerHTML = '';
    new QRCode(qrRef.current, {
      text: url,
      width: 180,
      height: 180,
      colorDark: '#111318',
      colorLight: '#ffffff',
      correctLevel: QRCode.CorrectLevel.M,
    });
  }, [url]);

  // Listen for browser beforeinstallprompt
  const [installPrompt, setInstallPrompt] = useState(null);
  useEffect(() => {
    const handler = (e) => { e.preventDefault(); setInstallPrompt(e); };
    window.addEventListener('beforeinstallprompt', handler);
    return () => window.removeEventListener('beforeinstallprompt', handler);
  }, []);

  const doInstall = async () => {
    if (!installPrompt) return;
    installPrompt.prompt();
    const { outcome } = await installPrompt.userChoice;
    if (outcome === 'accepted') { setInstallPrompt(null); onClose(); }
  };

  // Close on Escape
  useEffect(() => {
    const onKey = (e) => { if (e.key === 'Escape') onClose(); };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [onClose]);

  return (
    <div className="drawer-overlay" onClick={onClose} style={{ zIndex: 300 }}>
      <div
        onClick={e => e.stopPropagation()}
        style={{
          background: 'var(--bg-elev-1)',
          border: '1px solid var(--line-hi)',
          borderRadius: 'var(--r-lg)',
          width: 320, padding: 24,
          position: 'fixed',
          top: '50%', left: '50%',
          transform: 'translate(-50%,-50%)',
          boxShadow: '0 24px 80px -20px #000000c0',
          textAlign: 'center',
        }}
      >
        <div style={{ display:'flex', alignItems:'center', justifyContent:'space-between', marginBottom: 16 }}>
          <div style={{ fontWeight: 600, fontSize: 15 }}>Install / Open on mobile <span className="beta-pill">BETA</span></div>
          <button className="btn btn-ghost btn-icon" onClick={onClose}><Icon name="x" size={14}/></button>
        </div>

        <div style={{ background:'#ffffff', borderRadius: 'var(--r)', padding: 12, display:'inline-block', marginBottom: 16 }}>
          <div ref={qrRef} style={{ width: 180, height: 180 }} />
        </div>

        <div className="muted" style={{ fontSize: 12, marginBottom: 16 }}>
          Scan with your phone to open the dashboard. Add to home screen for a native-app experience.
        </div>

        <div style={{ fontSize: 11, fontFamily:'var(--font-mono)', color:'var(--text-3)', wordBreak:'break-all', marginBottom: 16, padding:'6px 8px', background:'var(--bg-elev-2)', borderRadius:'var(--r)' }}>
          {url}
        </div>

        {installPrompt && (
          <button className="btn btn-pri" style={{ width:'100%' }} onClick={doInstall}>
            Install as app
          </button>
        )}
        {!installPrompt && (
          <div className="muted" style={{ fontSize: 12 }}>
            To install: open in Chrome/Edge on Android, or Safari on iOS, then "Add to Home Screen."
          </div>
        )}
      </div>
    </div>
  );
}

/* ============================================================
   COMMAND PALETTE
   ============================================================ */

function CommandPalette({ open, onClose, setActive, employees, setTheme, theme, setAesthetic }) {
  const [q, setQ] = useState('');
  const [idx, setIdx] = useState(0);
  const [helpResults, setHelpResults] = useState([]);
  const helpDebRef = useRef(null);

  // Detect "Help: <query>" prefix
  const helpPrefix = 'help:';
  const isHelpMode = q.trim().toLowerCase().startsWith(helpPrefix);
  const helpQ = isHelpMode ? q.trim().slice(helpPrefix.length).trim() : '';

  // Fetch help results when in help mode
  useEffect(() => {
    if (!isHelpMode || !helpQ) { setHelpResults([]); return; }
    clearTimeout(helpDebRef.current);
    helpDebRef.current = setTimeout(async () => {
      try {
        const r = await fetch(`/api/help/search?q=${encodeURIComponent(helpQ)}`, { credentials: 'include' });
        if (!r.ok) return;
        const data = await r.json();
        setHelpResults(Array.isArray(data) ? data.slice(0, 8) : []);
      } catch { setHelpResults([]); }
    }, 200);
    return () => clearTimeout(helpDebRef.current);
  }, [helpQ, isHelpMode]);

  const items = useMemo(() => {
    const pages = NAV_GROUPS.flatMap(g => g.items.map(i => ({ ...i, kind:'page', section: g.label })));
    const people = employees.slice(0, 30).map(e => ({ id: `emp-${e.id}`, kind:'person', label: e.display_name, hint: e.matrix_id, employee: e, icon: 'user' }));
    const actions = [
      { id:'theme-dark',  kind:'action', label:'Switch to dark theme',  icon:'moon', do: () => setTheme('dark') },
      { id:'theme-light', kind:'action', label:'Switch to light theme', icon:'sun',  do: () => setTheme('light') },
      // Aesthetic picker removed — Editorial Calm is the only direction.
      // Theme toggle (light/dark) stays accessible via the entries above.
    ];
    return [...pages, ...people, ...actions];
  }, [employees]);

  const filtered = useMemo(() => {
    if (isHelpMode) return [];
    if (!q.trim()) return items;
    const lc = q.toLowerCase();
    return items.filter(i => (i.label || '').toLowerCase().includes(lc) || (i.hint || '').toLowerCase().includes(lc));
  }, [q, items, isHelpMode]);

  const allVisible = isHelpMode
    ? helpResults.map(r => ({ id: `help-${r.slug}`, kind:'help', label: r.title, hint: r.category, slug: r.slug }))
    : filtered;

  useEffect(() => { setIdx(0); }, [q]);

  useEffect(() => {
    if (!open) return;
    const onKey = (e) => {
      if (e.key === 'Escape') onClose();
      if (e.key === 'ArrowDown') { e.preventDefault(); setIdx(i => Math.min(i + 1, allVisible.length - 1)); }
      if (e.key === 'ArrowUp')   { e.preventDefault(); setIdx(i => Math.max(i - 1, 0)); }
      if (e.key === 'Enter') {
        const sel = allVisible[idx];
        if (!sel) return;
        if (sel.kind === 'page') setActive(sel.id);
        else if (sel.kind === 'action') sel.do();
        else if (sel.kind === 'person') setActive('employees');
        else if (sel.kind === 'help') { setActive('help'); }
        onClose();
      }
    };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [open, allVisible, idx, onClose, setActive]);

  if (!open) return null;

  // group non-help items
  const grouped = filtered.reduce((acc, item) => {
    const k = item.kind === 'page' ? 'Pages' : item.kind === 'person' ? 'People' : 'Actions';
    (acc[k] ||= []).push(item);
    return acc;
  }, {});

  let runningIdx = -1;

  const renderItem = (item) => {
    runningIdx++;
    const isOn = runningIdx === idx;
    return (
      <div key={item.id} className={`cmd-row ${isOn ? 'on' : ''}`}
        onClick={() => {
          if (item.kind === 'page') setActive(item.id);
          else if (item.kind === 'action') item.do();
          else if (item.kind === 'person') setActive('employees');
          else if (item.kind === 'help') setActive('help');
          onClose();
        }}>
        <Icon name={item.icon || (item.kind === 'help' ? 'info' : 'arrowR')} size={14} />
        <span className="label">{item.label}</span>
        {item.hint && <span className="hint">{item.hint}</span>}
      </div>
    );
  };

  return (
    <div className="cmd-overlay" onClick={onClose}>
      <div className="cmd-box" onClick={e => e.stopPropagation()}>
        <div className="cmd-input-wrap">
          <Icon name="search" size={16} />
          <input
            autoFocus value={q} onChange={e => setQ(e.target.value)}
            placeholder={isHelpMode ? 'Search help articles…' : 'Search pages, people, or run a command…'}
          />
          <span className="kbd" style={{ fontFamily:'var(--font-mono)', fontSize:11, color:'var(--text-3)', padding:'2px 6px', background:'var(--bg)', border:'1px solid var(--line)', borderRadius:3 }}>esc</span>
        </div>
        <div className="cmd-list">
          {isHelpMode ? (
            <>
              <div className="cmd-section-label">Help articles</div>
              {helpResults.length === 0 && helpQ && (
                <div className="empty"><div className="t">No articles found</div><div className="s">Try a different query.</div></div>
              )}
              {helpResults.length === 0 && !helpQ && (
                <div className="empty"><div className="t">Type to search help</div><div className="s">e.g. Help: sign in</div></div>
              )}
              {helpResults.map(r => ({id:`help-${r.slug}`, kind:'help', label:r.title, hint:r.category, slug:r.slug})).map(renderItem)}
            </>
          ) : (
            <>
              {Object.entries(grouped).map(([section, list]) => (
                <div key={section}>
                  <div className="cmd-section-label">{section}</div>
                  {list.map(renderItem)}
                </div>
              ))}
              {filtered.length === 0 && (
                <div className="empty"><div className="t">No results</div><div className="s">Try "Help: your question" to search articles.</div></div>
              )}
              {/* Hint about Help: mode */}
              {q.trim() && (
                <div style={{ padding:'6px 12px', fontSize:11, color:'var(--text-3)', borderTop:'1px solid var(--line)' }}>
                  Tip: type <span style={{ fontFamily:'var(--font-mono)' }}>Help: {q.trim()}</span> to search help articles
                </div>
              )}
            </>
          )}
        </div>
      </div>
    </div>
  );
}

/* ============================================================
   LOGIN
   ============================================================ */

const LOGIN_LAST_ID_KEY = 'status_last_matrix_id';

function normalizeLoginMatrixId(value) {
  let s = (value || '').trim().toLowerCase();
  if (!s || /\s/.test(s)) return '';
  if (s.startsWith('@')) s = s.slice(1);
  if (!s.includes(':')) s = `${s}:k9.ms`;
  return `@${s}`;
}

function loginDeviceKey(matrixId) {
  const canonical = normalizeLoginMatrixId(matrixId);
  return canonical ? `status_device_${canonical}` : '';
}

function apiErrorText(detail, fallback = 'Request failed') {
  const norm = (v) => {
    if (v == null) return '';
    if (typeof v === 'string') return v;
    if (Array.isArray(v)) return v.map(norm).filter(Boolean).join('; ');
    if (typeof v === 'object') {
      const nested = v.msg || v.message || v.error || v.detail;
      if (nested && nested !== v) {
        const n = norm(nested);
        if (n) return n;
      }
      try { return JSON.stringify(v); } catch(_) { return String(v); }
    }
    return String(v);
  };
  if (!detail) return fallback;
  const out = norm(detail);
  return out || fallback;
}

function LoginPage({ onLogin, theme, setTheme }) {
  const [step, setStep] = useState('id');
  const [chatId, setChatId] = useState(() => localStorage.getItem(LOGIN_LAST_ID_KEY) || '');
  const [code, setCode] = useState('');
  const [pw, setPw] = useState('');
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState('');
  const [loginStats, setLoginStats] = useState(null);
  // Mobile + auto-trust detection. On Capacitor / true touch phones, we hide
  // the "Sign in with password" button and silently attempt the trusted-
  // device login on mount so returning users skip the OTP screen entirely.
  const isMobileSurface = (typeof window.M_isMobileSurface === 'function')
    ? !!window.M_isMobileSurface()
    : false;
  // Auth headers: the native Capacitor app announces itself so the backend
  // exempts it from the employee web-login block (employees use the app, not
  // the web dashboard). No header on mobile/desktop web → web stays gated.
  const authHeaders = () => {
    const h = { 'Content-Type': 'application/json' };
    try {
      const cap = window.Capacitor;
      const native = cap && (
        (typeof cap.isNativePlatform === 'function' && cap.isNativePlatform()) ||
        (typeof cap.getPlatform === 'function' && cap.getPlatform() !== 'web')
      );
      if (native) h['X-K9-Client'] = 'ios-app';
    } catch (_) {}
    return h;
  };
  const [autoTrying, setAutoTrying] = useState(() => {
    const last = localStorage.getItem(LOGIN_LAST_ID_KEY) || '';
    return !!last; // attempt iff we know who you are
  });

  useEffect(() => {
    let alive = true;
    fetch('/api/login/stats')
      .then(r => r.ok ? r.json() : null)
      .then(data => { if (alive) setLoginStats(data); })
      .catch(() => {});
    return () => { alive = false; };
  }, []);

  const rememberDevice = (matrixId, token) => {
    const id = normalizeLoginMatrixId(matrixId);
    const key = loginDeviceKey(id);
    if (!id || !key || !token) return;
    const expiry = new Date(Date.now() + 90 * 24 * 60 * 60 * 1000).toISOString();
    localStorage.setItem(key, token);
    localStorage.setItem(`${key}_expiry`, expiry);
    localStorage.setItem(LOGIN_LAST_ID_KEY, id);
  };

  const rememberedDevice = (matrixId) => {
    const id = normalizeLoginMatrixId(matrixId);
    const canonicalKey = loginDeviceKey(id);
    if (!id || !canonicalKey) return null;
    const keys = [canonicalKey];
    const legacyKey = `standup_device_${id}`;
    if (legacyKey !== canonicalKey) keys.push(legacyKey);
    for (const key of keys) {
      const token = localStorage.getItem(key);
      const expiry = localStorage.getItem(`${key}_expiry`);
      if (!token || !expiry) continue;
      if (new Date(expiry) <= new Date()) {
        localStorage.removeItem(key);
        localStorage.removeItem(`${key}_expiry`);
        continue;
      }
      if (key !== canonicalKey) {
        localStorage.setItem(canonicalKey, token);
        localStorage.setItem(`${canonicalKey}_expiry`, expiry);
        localStorage.removeItem(key);
        localStorage.removeItem(`${key}_expiry`);
      }
      return { id, token, key: canonicalKey };
    }
    return null;
  };

  const loginWithDevice = async (matrixId) => {
    const remembered = rememberedDevice(matrixId);
    if (!remembered) return false;
    let r;
    try {
      r = await fetch('/api/auth/device-login', {
        method:'POST', credentials:'include',
        headers: authHeaders(),
        body: JSON.stringify({ matrix_id: remembered.id, device_token: remembered.token }),
      });
    } catch (_) {
      // Network blip — keep the trust token so the next attempt can still
      // skip OTP. Never punish a transient failure with a re-login.
      return false;
    }
    if (!r.ok) {
      // Only forget the device when the server explicitly rejects the token
      // (401/403 = revoked/expired). For 5xx or anything else, keep trust so
      // the user isn't forced back to OTP by a temporary backend hiccup.
      if (r.status === 401 || r.status === 403) {
        localStorage.removeItem(remembered.key);
        localStorage.removeItem(`${remembered.key}_expiry`);
      }
      return false;
    }
    const data = await r.json();
    // Refresh the stored 90-day expiry in lockstep with the server's sliding
    // window so an actively-used device never lets its token lapse → no re-OTP.
    if (data.device_token) rememberDevice(data.matrix_id || remembered.id, data.device_token);
    localStorage.setItem(LOGIN_LAST_ID_KEY, data.matrix_id || remembered.id);
    onLogin(data);
    return true;
  };

  // Auto-try the trusted-device login on every cold start when we remember
  // who this device belongs to. Returning users never see the OTP screen —
  // they just land in the app. Only falls through to UI if the stored token
  // has expired or was server-side revoked.
  useEffect(() => {
    let cancelled = false;
    const tryAuto = async () => {
      const last = localStorage.getItem(LOGIN_LAST_ID_KEY) || '';
      if (!last) { setAutoTrying(false); return; }
      try {
        const ok = await loginWithDevice(last);
        if (!cancelled && !ok) setAutoTrying(false);
      } catch (_) { if (!cancelled) setAutoTrying(false); }
    };
    if (autoTrying) tryAuto();
    return () => { cancelled = true; };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  const send = async () => {
    const id = normalizeLoginMatrixId(chatId);
    if (!id || busy) return;
    setBusy(true); setErr('');
    try {
      if (await loginWithDevice(id)) return;
      const r = await fetch('/api/auth/request-otp', {
        method:'POST', credentials:'include',
        headers:{'Content-Type':'application/json'},
        body: JSON.stringify({ matrix_id: id }),
      });
      if (!r.ok) {
        const j = await r.json().catch(() => ({}));
        throw new Error(apiErrorText(j.detail ?? j.message, `Could not send code (${r.status})`));
      }
      localStorage.setItem(LOGIN_LAST_ID_KEY, id);
      setStep('code');
    } catch (e) { setErr(apiErrorText(e.message, 'Failed')); } finally { setBusy(false); }
  };
  const verify = async () => {
    if (code.length < 6 || busy) return;
    setBusy(true); setErr('');
    try {
      const r = await fetch('/api/auth/verify-otp', {
        method:'POST', credentials:'include',
        headers: authHeaders(),
        body: JSON.stringify({ matrix_id: normalizeLoginMatrixId(chatId), otp: code }),
      });
      if (!r.ok) {
        const j = await r.json().catch(() => ({}));
        throw new Error(apiErrorText(j.detail ?? j.message, 'Invalid code'));
      }
      const data = await r.json();
      if (data.device_token) rememberDevice(data.matrix_id || chatId, data.device_token);
      onLogin(data);
    } catch (e) { setErr(apiErrorText(e.message, 'Failed')); } finally { setBusy(false); }
  };
  const loginPassword = async () => {
    const id = normalizeLoginMatrixId(chatId);
    if (!id || !pw || busy) return;
    setBusy(true); setErr('');
    try {
      const r = await fetch('/api/auth/login-password', {
        method:'POST', credentials:'include',
        headers:{'Content-Type':'application/json'},
        body: JSON.stringify({ matrix_id: id, password: pw }),
      });
      if (!r.ok) {
        const j = await r.json().catch(() => ({}));
        throw new Error(apiErrorText(j.detail ?? j.message, 'Invalid credentials'));
      }
      const data = await r.json();
      localStorage.setItem(LOGIN_LAST_ID_KEY, data.matrix_id || id);
      onLogin(data);
    } catch (e) { setErr(apiErrorText(e.message, 'Failed')); } finally { setBusy(false); }
  };

  // demo mosaic seed
  const tiles = useMemo(() => {
    const out = [];
    for (let i = 0; i < 64; i++) {
      const r = Math.random();
      out.push(r > 0.85 ? 'err' : r > 0.7 ? 'warn' : r > 0.25 ? 'ok' : '');
    }
    return out;
  }, []);

  const signedIn = loginStats?.signed_in_today ?? '—';
  const totalUsers = loginStats?.total_users ?? '—';
  const lateToday = loginStats?.late_today ?? '—';
  const absentToday = loginStats?.absent_today ?? '—';
  const blockersToday = loginStats?.active_blockers ?? '—';

  // Quiet "welcome back" frame while the trusted-device login is in flight.
  // No form, no fields — just the brand. Either we get logged in silently
  // or we drop through to the normal OTP screen.
  if (autoTrying) {
    return (
      <div className="login-wrap" style={isMobileSurface ? { background: '#000', color: '#f3f1ec' } : undefined}>
        <div className="panel-left">
          <div className="login-card" style={{ display:'flex', flexDirection:'column', alignItems:'center', gap: 16, padding: '48px 24px' }}>
            <div style={{ width: 48, height: 48, borderRadius: 12, overflow: 'hidden', display:'flex', alignItems:'center', justifyContent:'center' }}>
              <img src="/api/branding/favicon" alt="" width={48} height={48} style={{borderRadius:12, objectFit:'cover'}} />
            </div>
            <div style={{ fontFamily:"'Fraunces',serif", fontStyle:'italic', fontWeight:300, fontSize: 24, letterSpacing:'-0.015em' }}>
              {(window.appT||(s=>s))('welcome back…')}
            </div>
            <div style={{ fontFamily:"'JetBrains Mono',monospace", fontSize: 10.5, letterSpacing:'.12em', textTransform:'uppercase', opacity:.5 }}>
              {(window.appT||(s=>s))('signing you in')}
            </div>
          </div>
        </div>
      </div>
    );
  }

  return (
    <div className="login-wrap">
      <div className="panel-left">
        <div className="login-card">
          <div style={{ display:'flex', alignItems:'center', gap: 12, marginBottom: 32 }}>
            <div className="brand-glyph" style={{ width: 36, height: 36, overflow:'hidden', display:'flex', alignItems:'center', justifyContent:'center' }}><img src="/api/branding/favicon" alt="logo" width={36} height={36} style={{borderRadius:8, objectFit:'cover'}} onError={(e)=>{e.currentTarget.replaceWith(Object.assign(document.createElement('div'),{innerHTML:'<svg width=36 height=36 viewBox=\"0 0 32 32\"><rect width=32 height=32 rx=7 fill=\"var(--brand)\"/><text x=16 y=22 text-anchor=middle font-size=18 font-weight=700 fill=#fff>S</text></svg>'}));}} /></div>
            <div>
              <div style={{ fontWeight: 600, fontSize: 18, letterSpacing:'-0.02em' }}>{(window.__branding && window.__branding.product_name) || 'status'}</div>
              <div className="eyebrow">{(() => { const b = window.__branding || {}; if (b.tagline) return b.tagline; if (b.company_name && b.product_name && b.company_name !== b.product_name) return b.company_name + " / " + b.product_name; return b.product_name || ""; })()}</div>
            </div>
          </div>

          <div className="serif" style={{ fontSize: 32, fontWeight: 400, letterSpacing:'-0.01em', lineHeight: 1.1, marginBottom: 8 }}>
            {(window.__branding && window.__branding.login_slogan) || (window.appT||(s=>s))('Sign in to your team.')}
          </div>
          <div style={{ color:'var(--text-2)', marginBottom: 28, fontSize: 13.5 }}>
            {(window.appT||(s=>s))("We'll DM a 6-digit code to your K9 Chat ID. No passwords, no setup.")}
          </div>

          {err && (
            <div className="badge badge-err" style={{ marginBottom: 12, padding:'7px 12px' }}>
              <Icon name="alert" size={13} /> {err}
            </div>
          )}

          {step === 'id' ? (
            <div className="login-form">
              <div>
                <label className="login-label">{(window.appT||(s=>s))('K9 Chat ID')}</label>
                <input className="inp" value={chatId} onChange={e => setChatId(e.target.value)}
                  onKeyDown={e => e.key === 'Enter' && send()}
                  style={{ fontSize: 14, padding: '10px 12px' }} />
              </div>
              <button className="btn btn-pri" onClick={send} disabled={!chatId.trim() || busy}
                style={{ width:'100%', justifyContent:'center', padding:'10px' }}>
                {busy ? (window.appT||(s=>s))('Sending code…') : (window.appT||(s=>s))('Send code')} <Icon name="arrowR" size={14} />
              </button>
              <div style={{ fontSize: 12, color:'var(--text-3)', textAlign:'center', marginTop: 8 }}>
                <Icon name="check" size={12} /> {(window.appT||(s=>s))('Trusted devices stay signed in for 90 days')}
              </div>
              {/* Password login is a desktop-only fallback (App Review demo
                  account). On Capacitor / mobile phones we hide it — OTP +
                  trusted-device is the only auth path. */}
              {!isMobileSurface && (
                <button className="btn btn-ghost" onClick={() => { setErr(''); setStep('pw'); }}
                  style={{ width:'100%', justifyContent:'center', fontSize: 12.5 }}>
                  {(window.appT||(s=>s))('Sign in with password')}
                </button>
              )}
            </div>
          ) : step === 'pw' ? (
            <div className="login-form">
              <div>
                <label className="login-label">{(window.appT||(s=>s))('K9 Chat ID')}</label>
                <input className="inp" value={chatId} onChange={e => setChatId(e.target.value)}
                  onKeyDown={e => e.key === 'Enter' && loginPassword()} autoFocus
                  style={{ fontSize: 14, padding: '10px 12px' }} />
              </div>
              <div>
                <label className="login-label">{(window.appT||(s=>s))('Password')}</label>
                <input className="inp" type="password" value={pw} autoComplete="current-password"
                  onChange={e => setPw(e.target.value)}
                  onKeyDown={e => e.key === 'Enter' && loginPassword()}
                  style={{ fontSize: 14, padding: '10px 12px' }} />
              </div>
              <button className="btn btn-pri" onClick={loginPassword} disabled={!chatId.trim() || !pw || busy}
                style={{ width:'100%', justifyContent:'center', padding:'10px' }}>
                {busy ? (window.appT||(s=>s))('Signing in…') : (window.appT||(s=>s))('Sign in')} <Icon name="arrowR" size={14} />
              </button>
              <button className="btn btn-ghost" onClick={() => { setStep('id'); setPw(''); setErr(''); }}
                style={{ width:'100%', justifyContent:'center' }}>
                <Icon name="arrowL" size={13} /> {(window.appT||(s=>s))('Use a one-time code instead')}
              </button>
            </div>
          ) : (
            <div className="login-form">
              <div className="badge badge-info" style={{ padding:'7px 12px' }}>
                <Icon name="check" size={13} /> {(window.appT||(s=>s))('Code sent to')} <strong>{chatId}</strong> — {(window.appT||(s=>s))('expires in 5 min')}
              </div>
              <div className="login-otp">
                <label className="login-label">{(window.appT||(s=>s))('Verification code')}</label>
                <input className="inp" maxLength={6} value={code}
                  onChange={e => setCode(e.target.value.replace(/\D/g, ''))}
                  onKeyDown={e => e.key === 'Enter' && verify()}
                  placeholder="······" autoFocus />
              </div>
              <button className="btn btn-pri" onClick={verify} disabled={code.length < 6 || busy}
                style={{ width:'100%', justifyContent:'center', padding:'10px' }}>
                {busy ? (window.appT||(s=>s))('Verifying…') : (window.appT||(s=>s))('Sign in')} <Icon name="check" size={14} />
              </button>
              <button className="btn btn-ghost" onClick={() => { setStep('id'); setCode(''); setErr(''); }}
                style={{ width:'100%', justifyContent:'center' }}>
                <Icon name="arrowL" size={13} /> {(window.appT||(s=>s))('Back')}
              </button>
              <div style={{ fontSize: 12, color:'var(--text-3)', textAlign:'center' }}>
                {(window.appT||(s=>s))('Tip: enter')} <code className="mono" style={{padding:'1px 5px',background:'var(--bg-elev-2)',borderRadius:3}}>123456</code> {(window.appT||(s=>s))('to demo')}
              </div>
            </div>
          )}

          <div style={{ marginTop: 36, display:'flex', alignItems:'center', justifyContent:'space-between', fontSize: 11.5, color:'var(--text-3)' }}>
            <span className="mono">v1.4.2 · {new Date().toISOString().slice(0,10)}</span>
            <button className="btn btn-ghost btn-icon" onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
              <Icon name={theme === 'dark' ? 'sun' : 'moon'} size={14} />
            </button>
          </div>
        </div>
      </div>

      <div className="panel-right">
        <div style={{ position:'relative', zIndex: 1, maxWidth: 480, width:'100%' }}>
          <div style={{ display:'flex', alignItems:'center', gap: 10, marginBottom: 20 }}>
            <span className="pulse" style={{ width: 8, height: 8 }} />
            <span className="eyebrow">{(window.appLang && window.appLang()==='ar' ? 'مباشر · اليوم، ' : 'live · today, ')}{new Date().toLocaleDateString([],{ month:'short', day:'numeric' })}</span>
          </div>
          <div className="serif" style={{ fontSize: 38, lineHeight: 1.1, letterSpacing:'-0.01em', marginBottom: 18 }}>
            {(window.appLang && window.appLang()==='ar')
              ? <>تم تسجيل دخول {signedIn} من {totalUsers}.<br /><span style={{ color:'var(--text-3)' }}>{lateToday} متأخر · {absentToday} غائب · {blockersToday} عوائق.</span></>
              : <>{signedIn} of {totalUsers} signed in.<br /><span style={{ color:'var(--text-3)' }}>{lateToday} late · {absentToday} absent · {blockersToday} blockers.</span></>}
          </div>
          <div className="demo-mosaic" style={{ marginBottom: 22 }}>
            {tiles.map((t, i) => <div key={i} className={`demo-tile ${t}`} />)}
          </div>
          <div style={{ fontSize: 12.5, color:'var(--text-2)', lineHeight: 1.6 }}>
            {(window.appT||(s=>s))("One K9 room. One dashboard. Adaptive late-detection, AI roll-ups, payroll-ready exports — and a bot that's nicer than your old time-clock.")}
          </div>
        </div>
      </div>
    </div>
  );
}

/* AvatarVariantRow — upload / remove one variant (dark or light) */
function AvatarVariantRow({ label, variant, currentUrl, onUploaded, onRemoved }) {
  const [preview, setPreview] = useState(currentUrl);
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState('');
  const fileRef = React.useRef();

  const onPick = (file) => {
    if (!file) return;
    setErr('');
    const reader = new FileReader();
    reader.onload = async (ev) => {
      setPreview(ev.target.result);
      setBusy(true);
      try {
        const fd = new FormData();
        fd.append('file', file);
        const r = await fetch(`/api/users/me/avatar?variant=${variant}`, {
          method: 'POST', credentials: 'include', body: fd,
        });
        if (!r.ok) { const j = await r.json().catch(()=>({})); throw new Error(j.detail || `${r.status}`); }
        const j = await r.json();
        setPreview(j.url + '?t=' + Date.now());
        onUploaded(variant, j.url);
      } catch(e) { setErr(e.message || String(e)); setPreview(currentUrl); }
      finally { setBusy(false); }
    };
    reader.readAsDataURL(file);
  };

  const onRemove = async () => {
    setBusy(true); setErr('');
    try {
      const r = await fetch(`/api/users/me/avatar?variant=${variant}`, { method: 'DELETE', credentials: 'include' });
      if (!r.ok) throw new Error(`${r.status}`);
      setPreview(null);
      onRemoved(variant);
    } catch(e) { setErr(e.message || String(e)); }
    finally { setBusy(false); }
  };

  return (
    <div style={{display:'flex', gap:10, alignItems:'center', marginBottom:10}}>
      {preview
        ? <img src={preview} alt="" style={{width:44,height:44,borderRadius:8,objectFit:'cover',border:'1px solid var(--line)',flexShrink:0}}
            onError={() => setPreview(null)}/>
        : <div style={{width:44,height:44,borderRadius:8,background:'var(--bg-elev-2)',border:'1px dashed var(--line)',display:'flex',alignItems:'center',justifyContent:'center',fontSize:9,color:'var(--text-3)',flexShrink:0,textAlign:'center'}}>none</div>}
      <div style={{flex:1, minWidth:0}}>
        <div style={{fontSize:11.5, fontWeight:500, marginBottom:4}}>{label}</div>
        <div style={{display:'flex', gap:6}}>
          <input ref={fileRef} type="file" accept="image/png,image/jpeg,image/webp" hidden onChange={e => onPick(e.target.files[0])} />
          <button className="btn btn-ghost" style={{fontSize:11, padding:'2px 8px'}} onClick={() => fileRef.current?.click()} disabled={busy}>
            {busy ? 'Uploading…' : preview ? 'Replace' : 'Upload'}
          </button>
          {preview && <button className="btn btn-ghost" style={{fontSize:11, padding:'2px 8px', color:'var(--err)'}} onClick={onRemove} disabled={busy}>Remove</button>}
        </div>
        {err && <div style={{fontSize:10, color:'var(--err)', marginTop:2}}>{err}</div>}
      </div>
    </div>
  );
}

/* ─── Linked accounts section (CP6) ──────────────────────────────── */
function LinkedAccountsSection({ me, onMsg }) {
  const [accounts, setAccounts] = React.useState(null); // null = loading
  const [busy, setBusy] = React.useState(false);
  const toast = useToast ? useToast() : { push: () => {} };

  const load = async () => {
    try {
      const r = await fetch('/api/users/me/external-accounts', { credentials: 'same-origin' });
      if (r.ok) setAccounts(await r.json());
      else setAccounts([]);
    } catch { setAccounts([]); }
  };

  React.useEffect(() => { load(); }, []);

  const unlink = async (protocol) => {
    if (!confirm(`Unlink ${protocol} account?`)) return;
    setBusy(true);
    try {
      const r = await fetch(`/api/users/me/external/${protocol}`, {
        method: 'DELETE',
        credentials: 'same-origin',
      });
      if (!r.ok) throw new Error(`${r.status}`);
      onMsg && onMsg(`${protocol} account unlinked`);
      load();
    } catch(e) {
      onMsg && onMsg(`Unlink failed: ${e.message}`);
    } finally { setBusy(false); }
  };

  const PROTO_LABELS = { matrix: 'K9 Chat', slack: 'Slack', teams: 'Microsoft Teams' };

  return (
    <div style={{borderTop:'1px solid var(--line)', paddingTop:14, marginTop:14}}>
      <div style={{fontSize:13, fontWeight:600, marginBottom:8}}>Linked accounts</div>
      <div style={{fontSize:11.5, color:'var(--text-2)', marginBottom:10}}>
        Accounts linked to your profile for bot messaging. K9 Chat is always linked.
      </div>

      {accounts === null && <div className="muted" style={{fontSize:12}}>Loading...</div>}

      {accounts && accounts.length === 0 && (
        <div className="muted" style={{fontSize:12}}>No external accounts linked yet.</div>
      )}

      {accounts && accounts.map(acct => (
        <div key={acct.protocol + acct.external_id} style={{
          background:'var(--bg-2)', borderRadius:6, padding:'7px 10px',
          marginBottom:6, display:'flex', justifyContent:'space-between', alignItems:'center',
        }}>
          <div>
            <div style={{fontSize:12.5, fontWeight:500}}>{PROTO_LABELS[acct.protocol] || acct.protocol}</div>
            <div className="muted mono" style={{fontSize:11}}>{acct.display_name || acct.external_id}</div>
            {acct.last_seen_at && (
              <div className="muted" style={{fontSize:10.5}}>
                Last seen {new Date(acct.last_seen_at).toLocaleDateString()}
              </div>
            )}
            {!acct.verified && (
              <div style={{fontSize:10, color:'var(--warn)'}}>Email-matched (not OAuth-verified)</div>
            )}
          </div>
          {acct.protocol !== 'matrix' && (
            <button
              className="btn btn-ghost"
              style={{color:'var(--danger)', borderColor:'var(--danger)', fontSize:11}}
              disabled={busy}
              onClick={() => unlink(acct.protocol)}
            >
              Unlink
            </button>
          )}
        </div>
      ))}

      {/* If no Slack account, show Link Slack button */}
      {accounts && !accounts.find(a => a.protocol === 'slack') && (
        <button
          className="btn btn-ghost"
          style={{fontSize:12, marginTop:4}}
          onClick={() => window.open('/slack/install', 'slack_install', 'width=600,height=700')}
        >
          Link Slack workspace
        </button>
      )}
    </div>
  );
}

function ProfileModal({ me, onClose }) {
  const [name, setName] = useState(me.display_name || '');
  const [busy, setBusy] = useState(false);
  const [pwOld, setPwOld] = useState('');
  const [pwNew, setPwNew] = useState('');
  const [pwBusy, setPwBusy] = useState(false);
  const [msg, setMsg] = useState('');
  // Avatar variant state — seeded from me (which now includes avatar_dark_url etc.)
  const [darkUrl, setDarkUrl]   = useState(me.avatar_dark_url  || null);
  const [lightUrl, setLightUrl] = useState(me.avatar_light_url || null);
  const [pref, setPref]         = useState(me.avatar_preference || 'auto');
  const [prefBusy, setPrefBusy] = useState(false);
  // Team selection
  const [team, setTeam]         = useState(me.team || '');
  const [teams, setTeams]       = useState([]);
  const [teamBusy, setTeamBusy] = useState(false);
  // Work location
  const [workLocation, setWorkLocation] = useState(me.work_location || 'office');
  const [remoteDays, setRemoteDays]     = useState((me.remote_days || '').split(',').filter(Boolean));
  const [locBusy, setLocBusy]           = useState(false);
  // Contact fields
  const [cellPhone, setCellPhone]       = useState(me.cell_phone || '');
  const [altPhone, setAltPhone]         = useState(me.alt_phone || '');
  const [privateEmail, setPrivateEmail] = useState(me.private_email || '');
  const [contactBusy, setContactBusy]   = useState(false);
  // CP5 smart-routing: preferred DM channel
  const [dmProto, setDmProto]   = useState(me.preferred_protocol || 'auto');
  const [dmProtoBusy, setDmProtoBusy] = useState(false);

  useEffect(() => {
    fetch('/api/teams', { credentials: 'include' })
      .then(r => r.ok ? r.json() : [])
      .then(data => setTeams(data))
      .catch(() => {});
  }, []);

  const saveTeam = async (newTeam) => {
    setTeamBusy(true); setMsg('');
    try {
      const userId = me.user_id || me.id;
      const r = await fetch(`/api/employees/${userId}`, {
        method: 'PATCH',
        credentials: 'include',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ team: newTeam || null }),
      });
      if (!r.ok) throw new Error(await r.text());
      setTeam(newTeam);
      setMsg('✓ Team saved');
      if (window.STATUS_DATA?.ME) window.STATUS_DATA.ME.team = newTeam || null;
    } catch(e) { setMsg(`✗ ${e.message || e}`); }
    finally { setTeamBusy(false); }
  };

  const saveWorkLocation = async () => {
    setLocBusy(true); setMsg('');
    try {
      const userId = me.user_id || me.id;
      const r = await fetch(`/api/users/${userId}/work-location`, {
        method: 'PUT',
        credentials: 'include',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          location: workLocation,
          remote_days: workLocation === 'hybrid' ? remoteDays.join(',') : null,
        }),
      });
      if (!r.ok) throw new Error(await r.text());
      setMsg('Work location saved');
      if (window.STATUS_DATA?.ME) {
        window.STATUS_DATA.ME.work_location = workLocation;
        window.STATUS_DATA.ME.remote_days = workLocation === 'hybrid' ? remoteDays.join(',') : null;
      }
    } catch(e) { setMsg(`Error: ${e.message || e}`); }
    finally { setLocBusy(false); }
  };

  const saveName = async () => {
    setBusy(true); setMsg('');
    try {
      const r = await fetch('/api/auth/me/profile', {
        method:'PUT', credentials:'include',
        headers:{'Content-Type':'application/json'},
        body: JSON.stringify({display_name: name}),
      });
      if (!r.ok) throw new Error(await r.text());
      setMsg('✓ Name saved');
      if (window.STATUS_DATA?.ME) window.STATUS_DATA.ME.display_name = name;
    } catch(e){ setMsg(`✗ ${e.message || e}`); }
    finally { setBusy(false); }
  };

  const savePref = async (newPref) => {
    setPref(newPref);
    setPrefBusy(true);
    try {
      await fetch('/api/users/me/avatar-preference', {
        method:'PUT', credentials:'include',
        headers:{'Content-Type':'application/json'},
        body: JSON.stringify({preference: newPref}),
      });
      if (window.STATUS_DATA?.ME) window.STATUS_DATA.ME.avatar_preference = newPref;
    } catch(_) {}
    finally { setPrefBusy(false); }
  };

  const onUploaded = (variant, url) => {
    if (variant === 'dark')  { setDarkUrl(url);  if (window.STATUS_DATA?.ME) window.STATUS_DATA.ME.avatar_dark_url  = url; }
    if (variant === 'light') { setLightUrl(url); if (window.STATUS_DATA?.ME) window.STATUS_DATA.ME.avatar_light_url = url; }
    setMsg('✓ Avatar uploaded');
  };
  const onRemoved = (variant) => {
    if (variant === 'dark')  { setDarkUrl(null);  if (window.STATUS_DATA?.ME) window.STATUS_DATA.ME.avatar_dark_url  = null; }
    if (variant === 'light') { setLightUrl(null); if (window.STATUS_DATA?.ME) window.STATUS_DATA.ME.avatar_light_url = null; }
    setMsg('✓ Avatar removed');
  };

  const saveContact = async () => {
    setContactBusy(true); setMsg('');
    try {
      const userId = me.user_id || me.id;
      const r = await fetch(`/api/employees/${userId}/profile`, {
        method: 'PUT',
        credentials: 'include',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          cell_phone: cellPhone || null,
          alt_phone: altPhone || null,
          private_email: privateEmail || null,
        }),
      });
      if (!r.ok) { const j = await r.json().catch(()=>({})); throw new Error(j.detail || `${r.status}`); }
      setMsg('Contact saved');
      if (window.STATUS_DATA?.ME) {
        window.STATUS_DATA.ME.cell_phone = cellPhone || null;
        window.STATUS_DATA.ME.alt_phone = altPhone || null;
        window.STATUS_DATA.ME.private_email = privateEmail || null;
      }
    } catch(e) { setMsg(`Error: ${e.message || e}`); }
    finally { setContactBusy(false); }
  };

  const saveDmProto = async (newProto) => {
    setDmProtoBusy(true); setMsg('');
    try {
      const userId = me.user_id || me.id;
      const r = await fetch(`/api/employees/${userId}/profile`, {
        method: 'PUT',
        credentials: 'include',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ preferred_protocol: newProto === 'auto' ? null : newProto }),
      });
      if (!r.ok) { const j = await r.json().catch(()=>({})); throw new Error(j.detail || `${r.status}`); }
      setDmProto(newProto);
      setMsg('DM channel preference saved');
      if (window.STATUS_DATA?.ME) window.STATUS_DATA.ME.preferred_protocol = newProto === 'auto' ? null : newProto;
    } catch(e) { setMsg(`Error: ${e.message || e}`); }
    finally { setDmProtoBusy(false); }
  };

  const changePw = async () => {
    if (!pwNew || pwNew.length < 6) { setMsg('✗ New password must be at least 6 chars'); return; }
    setPwBusy(true); setMsg('');
    try {
      const r = await fetch('/api/auth/change-password', {
        method:'POST', credentials:'include',
        headers:{'Content-Type':'application/json'},
        body: JSON.stringify({old_password: pwOld, new_password: pwNew}),
      });
      if (!r.ok) { const j = await r.json().catch(()=>({})); throw new Error(j.detail || `${r.status}`); }
      setMsg('✓ Password changed'); setPwOld(''); setPwNew('');
    } catch(e){ setMsg(`✗ ${e.message || e}`); }
    finally { setPwBusy(false); }
  };

  // Esc to close + body scroll lock
  useEffect(() => {
    const prev = document.body.style.overflow;
    document.body.style.overflow = 'hidden';
    const onKey = (e) => { if (e.key === 'Escape') onClose(); };
    document.addEventListener('keydown', onKey);
    return () => {
      document.body.style.overflow = prev;
      document.removeEventListener('keydown', onKey);
    };
  }, []);

  return (
    <div onClick={onClose} style={{position:'fixed', inset:0, background:'rgba(0,0,0,0.75)', backdropFilter:'blur(4px)', zIndex: 1000, display:'flex', alignItems:'center', justifyContent:'center', padding: 24}}>
      <div onClick={e => e.stopPropagation()} style={{background:'var(--bg-elev-1)', border:'1px solid var(--line-hi)', borderRadius: 12, width:'100%', maxWidth: 520, maxHeight: '90vh', overflowY:'auto', padding: 24, boxShadow:'0 20px 60px -10px #000a'}}>
        <div style={{display:'flex', alignItems:'center', justifyContent:'space-between', marginBottom: 20}}>
          <div style={{fontSize:16, fontWeight:600}}>Your profile</div>
          <button className="btn btn-ghost btn-icon" onClick={onClose}><Icon name="x" size={14}/></button>
        </div>

        {/* Avatar variants */}
        <div style={{borderBottom:'1px solid var(--line)', paddingBottom:14, marginBottom:14}}>
          <div style={{fontSize:13, fontWeight:600, marginBottom:10}}>Profile picture</div>
          <AvatarVariantRow label="Dark version (for dark backgrounds)" variant="dark"  currentUrl={darkUrl}  onUploaded={onUploaded} onRemoved={onRemoved} />
          <AvatarVariantRow label="Light version (for light backgrounds)" variant="light" currentUrl={lightUrl} onUploaded={onUploaded} onRemoved={onRemoved} />
          <div style={{fontSize:11.5, fontWeight:500, marginTop:6, marginBottom:6}}>Show variant</div>
          <div style={{display:'flex', gap:6}}>
            {['auto','dark','light'].map(v => (
              <button key={v}
                className={`btn ${pref === v ? 'btn-pri' : 'btn-ghost'}`}
                style={{fontSize:11, padding:'3px 10px', textTransform:'capitalize'}}
                onClick={() => savePref(v)}
                disabled={prefBusy}
              >{v === 'auto' ? 'Auto (follows theme)' : v === 'dark' ? 'Always dark' : 'Always light'}</button>
            ))}
          </div>
          <div style={{fontSize:10.5, color:'var(--text-3)', marginTop:6}}>
            Auto: uses dark variant in dark theme, light variant in light theme. Max 2 MB, PNG/JPG/WEBP.
          </div>
          <div style={{fontSize:10.5, color:'var(--text-3)', marginTop:4}}>
            Leave blank to use your K9 Chat profile picture automatically.
          </div>
        </div>

        {/* Display name */}
        <div style={{marginBottom: 14}}>
          <label className="login-label">Display name</label>
          <div style={{display:'flex', gap: 8}}>
            <input className="inp" value={name} onChange={e => setName(e.target.value)} style={{flex:1}}/>
            <button className="btn btn-pri" onClick={saveName} disabled={busy || name === me.display_name}>Save</button>
          </div>
        </div>

        {/* Team */}
        <div style={{marginBottom: 14}}>
          <label className="login-label">Team</label>
          <div style={{display:'flex', gap: 8, alignItems:'center'}}>
            <select className="inp" value={team} onChange={e => setTeam(e.target.value)} style={{flex:1}} disabled={teamBusy}>
              <option value="">(no team)</option>
              {teams.map(t => (
                <option key={t.id || t.team_name} value={t.team_name}>{t.team_name}</option>
              ))}
            </select>
            <button className="btn btn-pri" onClick={() => saveTeam(team)} disabled={teamBusy || team === (me.team || '')}>Save</button>
          </div>
        </div>

        {/* Work location */}
        <div style={{marginBottom: 14}}>
          <label className="login-label">Work location</label>
          <div style={{display:'flex', gap: 8, alignItems:'center'}}>
            <select className="inp" value={workLocation} onChange={e => setWorkLocation(e.target.value)} style={{flex:1}} disabled={locBusy}>
              <option value="office">Office</option>
              <option value="remote">Remote</option>
              <option value="hybrid">Hybrid</option>
            </select>
            <button className="btn btn-pri" onClick={saveWorkLocation} disabled={locBusy}>Save</button>
          </div>
          {workLocation === 'hybrid' && (
            <div style={{marginTop: 8}}>
              <div style={{fontSize:11, color:'var(--fg-muted)', marginBottom:4}}>Remote days (tick days you work remotely)</div>
              <div style={{display:'flex', gap: 6, flexWrap:'wrap'}}>
                {['Mon','Tue','Wed','Thu','Fri','Sat','Sun'].map((label, idx) => {
                  const checked = remoteDays.includes(String(idx));
                  return (
                    <label key={idx} style={{display:'flex', alignItems:'center', gap: 3, fontSize: 12, cursor:'pointer'}}>
                      <input type="checkbox" checked={checked} onChange={e => {
                        setRemoteDays(prev => e.target.checked ? [...prev, String(idx)] : prev.filter(d => d !== String(idx)));
                      }}/>
                      {label}
                    </label>
                  );
                })}
              </div>
            </div>
          )}
        </div>

        {/* Contact */}
        <div style={{borderTop:'1px solid var(--line)', paddingTop:14, marginTop:14}}>
          <div style={{fontSize:13, fontWeight:600, marginBottom:10}}>Contact</div>
          <div style={{display:'flex', flexDirection:'column', gap:8}}>
            <div>
              <label className="login-label">Cell phone</label>
              <input className="inp" type="tel" placeholder="e.g. +1 555 123 4567" value={cellPhone} onChange={e => setCellPhone(e.target.value)}/>
            </div>
            <div>
              <label className="login-label">Alternate phone</label>
              <input className="inp" type="tel" placeholder="e.g. +1 555 987 6543" value={altPhone} onChange={e => setAltPhone(e.target.value)}/>
            </div>
            <div>
              <label className="login-label">Private email</label>
              <input className="inp" type="email" placeholder="e.g. you@personal.com" value={privateEmail} onChange={e => setPrivateEmail(e.target.value)}/>
            </div>
            <button className="btn btn-pri" onClick={saveContact} disabled={contactBusy} style={{alignSelf:'flex-start'}}>{contactBusy ? 'Saving...' : 'Save contact'}</button>
          </div>
        </div>

        {/* CP5 — Preferred DM channel */}
        <div style={{borderTop:'1px solid var(--line)', paddingTop: 14, marginTop: 14}}>
          <div style={{fontSize:13, fontWeight:600, marginBottom: 8}}>Preferred DM channel</div>
          <div style={{fontSize:12, color:'var(--text-2)', marginBottom: 10}}>
            Where the bot sends you direct messages. Auto uses your most recently active account.
          </div>
          <div style={{display:'flex', gap:8, alignItems:'center'}}>
            <select
              className="inp"
              style={{flex:1}}
              value={dmProto}
              disabled={dmProtoBusy}
              onChange={e => saveDmProto(e.target.value)}
            >
              <option value="auto">Auto (most recent activity)</option>
              <option value="matrix">K9 Chat</option>
              <option value="slack">Slack</option>
              <option value="teams">Microsoft Teams</option>
            </select>
            {dmProtoBusy && <span style={{fontSize:12, color:'var(--text-2)'}}>Saving...</span>}
          </div>
        </div>

        {/* Linked accounts */}
        <LinkedAccountsSection me={me} onMsg={setMsg} />

        {/* Change password */}
        <div style={{borderTop:'1px solid var(--line)', paddingTop: 14, marginTop: 14}}>
          <div style={{fontSize:13, fontWeight:600, marginBottom: 10}}>Change password</div>
          <div style={{display:'flex', flexDirection:'column', gap:8}}>
            <input className="inp" type="password" placeholder="Current password (leave blank if none)" value={pwOld} onChange={e=>setPwOld(e.target.value)}/>
            <input className="inp" type="password" placeholder="New password (min 6 chars)" value={pwNew} onChange={e=>setPwNew(e.target.value)}/>
            <button className="btn btn-pri" onClick={changePw} disabled={pwBusy || !pwNew}>{pwBusy?'Saving…':'Change password'}</button>
          </div>
        </div>
        {msg && <div style={{marginTop:14, fontSize:12.5, color: msg.startsWith('✓') ? 'var(--ok)' : 'var(--err)'}}>{msg}</div>}
      </div>
    </div>
  );
}

/* ============================================================
   BOTTOM TAB BAR — mobile only (<768px), fixed to viewport bottom
   Shows 5 key tabs: Today / Employees / Requests / Messages / Server
   ============================================================ */
const BOTTOM_TABS = [
  { id: 'today',     label: 'Today',    icon: 'today'  },
  { id: 'employees', label: 'Employees', icon: 'team' },
  { id: 'pto',       label: 'Requests', icon: 'pto'    },
  { id: 'oncall',    label: 'On-call',  icon: 'clock'  },
  { id: 'support',   label: 'Support',  icon: 'info'   },
];

function BottomTabBar({ active, setActive }) {
  return (
    <>
      <div className="mobile-beta-ribbon" aria-hidden="true">Mobile · BETA</div>
      <nav className="bottom-tab-bar show-on-mobile">
        {BOTTOM_TABS.map(tab => (
          <button
            key={tab.id}
            className={`bottom-tab ${active === tab.id ? 'active' : ''}`}
            onClick={() => setActive(tab.id)}
          >
            <Icon name={tab.icon} size={20} />
            <span className="bottom-tab-label">{tab.label}</span>
          </button>
        ))}
      </nav>
    </>
  );
}

// B2: collapsible nav sections, per-group state persisted to localStorage.
function NavSections({ collapsed, active, setActive, counts }) {
  const [openMap, setOpenMap] = useState(() => {
    try {
      const raw = localStorage.getItem('nav_section_open');
      if (raw) return JSON.parse(raw);
    } catch (_) {}
    return Object.fromEntries(NAV_GROUPS.map(g => [g.label, true]));
  });
  const persist = (next) => {
    setOpenMap(next);
    try { localStorage.setItem('nav_section_open', JSON.stringify(next)); } catch (_) {}
  };
  const toggle = (label) => persist({ ...openMap, [label]: !openMap[label] });

  // Gate adminOnly entries by current user's role. Admin tier includes
  // moderator + superadmin so promoted users see admin-only sidebar
  // items (Server, Bots, Bot Behavior, etc.).
  const meRole = String(((window.STATUS_DATA || {}).ME || {}).org_role || '').toLowerCase();
  const isAdmin = meRole === 'admin' || meRole === 'owner'
    || meRole === 'moderator' || meRole === 'superadmin';

  return (
    <nav className="nav">
      {NAV_GROUPS.map(group => {
        const visibleItems = group.items.filter(i => !i.adminOnly || isAdmin);
        if (visibleItems.length === 0) return null;
        const isOpen = collapsed ? true : (openMap[group.label] !== false);
        return (
          <div className="nav-section" key={group.label}>
            {!collapsed && (
              <div
                className="nav-section-label"
                role="button"
                tabIndex={0}
                onClick={() => {
                  // #39: clicking a section header now NAVIGATES to its first
                  // page (and expands the section), instead of only collapse-
                  // toggling — clicking "Insights"/"Audit" used to feel like it
                  // did nothing or hid the menu. The chevron still toggles.
                  if (openMap[group.label] === false) persist({ ...openMap, [group.label]: true });
                  if (visibleItems[0]) setActive(visibleItems[0].id);
                }}
                onKeyDown={e => { if (e.key === 'Enter' || e.key === ' ') { if (visibleItems[0]) setActive(visibleItems[0].id); } }}
                style={{ cursor: 'pointer', display:'flex', alignItems:'center', justifyContent:'space-between', userSelect:'none' }}
                title={`Open ${group.label}`}
              >
                <span>
                  {(window.appT || (s=>s))(group.label)}
                  <span style={{ opacity: 0.55, marginLeft: 4, fontWeight: 400 }}>· {visibleItems.length}</span>
                </span>
                <span
                  onClick={e => { e.stopPropagation(); toggle(group.label); }}
                  title={isOpen ? `Collapse ${group.label}` : `Expand ${group.label}`}
                  style={{ opacity: 0.5, fontSize: 11, padding: '0 4px', cursor: 'pointer', transform: isOpen ? 'rotate(90deg)' : 'none', transition: 'transform .15s' }}>▶</span>
              </div>
            )}
            {isOpen && visibleItems.map(item => (
              <div
                key={item.id}
                className={`nav-item ${active === item.id ? 'active' : ''} ${counts?.[item.id] ? 'has-pill' : ''}`}
                onClick={() => setActive(item.id)}
                title={collapsed ? (window.appT || (s=>s))(item.label) : ''}
              >
                <Icon name={item.icon} size={16} />
                {!collapsed && <span className="label">{(window.appT || (s=>s))(item.label)}</span>}
                {!collapsed && counts?.[item.id] != null && <span className="count">{counts[item.id]}</span>}
                {!collapsed && item.pill && <span className="count" style={{color:'var(--brand)'}}>{item.pill}</span>}
                {item.tip && <span className="nav-tooltip">{item.tip}</span>}
              </div>
            ))}
          </div>
        );
      })}
    </nav>
  );
}

window.BottomTabBar = BottomTabBar;
window.Sidebar = Sidebar;
window.Topbar = Topbar;
window.CommandPalette = CommandPalette;
window.LoginPage = LoginPage;
window.NAV_GROUPS = NAV_GROUPS;
