// Shared UI components for EquityIQ V2.
// Mirrors the reusable pieces in apps/web/src/components/ but rewritten for
// static JSX. No mock data — every consumer passes real props in, and null
// fields render as "—" via the formatters below.

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

// ============ FORMATTERS ============
window.formatPrice = (v) =>
  (v == null || Number.isNaN(v)) ? '—' : '$' + Number(v).toFixed(2);
window.formatPct = (v) => {
  if (v == null || Number.isNaN(v)) return '—';
  const n = Number(v);
  const sign = n > 0 ? '+' : '';
  return sign + n.toFixed(1) + '%';
};
window.formatInt = (v) => (v == null ? '—' : Number(v).toLocaleString());
window.formatDate = (iso) => {
  if (!iso) return '—';
  const d = new Date(iso);
  return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
};
window.formatRelative = (iso) => {
  if (!iso) return '—';
  const sec = Math.max(0, Math.floor((Date.now() - new Date(iso).getTime()) / 1000));
  if (sec < 60) return sec + 's ago';
  if (sec < 3600) return Math.floor(sec / 60) + 'm ago';
  if (sec < 86400) return Math.floor(sec / 3600) + 'h ago';
  return Math.floor(sec / 86400) + 'd ago';
};

// ============ useApi — simple fetch hook ============
window.useApi = function useApi(fn, deps = []) {
  const [state, setState] = useState({ data: null, loading: true, error: null });
  const run = useCallback(() => {
    let alive = true;
    setState(s => ({ ...s, loading: true, error: null }));
    fn()
      .then(data => { if (alive) setState({ data, loading: false, error: null }); })
      .catch(error => { if (alive) setState({ data: null, loading: false, error }); });
    return () => { alive = false; };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, deps);
  useEffect(run, [run]);
  return { ...state, reload: run };
};

// ============ LOGIN SCREEN ============
window.LoginScreen = function LoginScreen({ authError }) {
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const displayError = error || authError;

  const signIn = async () => {
    setLoading(true);
    setError(null);
    try { await window.EQ_AUTH.signIn(); }
    catch (e) { setError(e.message || 'Sign-in failed'); setLoading(false); }
  };

  return (
    <div style={{
      display: 'flex', alignItems: 'center', justifyContent: 'center',
      minHeight: '100vh', width: '100%', background: 'var(--paper)',
      fontFamily: 'var(--font-sans)'
    }}>
      <div style={{
        width: 360, padding: '36px 32px', borderRadius: 12,
        background: '#002060', color: 'white',
        boxShadow: '0 25px 60px -12px rgba(0,0,0,0.5)',
        textAlign: 'center'
      }}>
        <img src="shared/onni-logo.png" alt="Onni" style={{ width: 52, height: 52, objectFit: 'contain', margin: '0 auto 16px', display: 'block' }}/>
        <div style={{ fontSize: 11, color: 'rgba(255,255,255,0.7)', letterSpacing: '0.14em', fontWeight: 500, marginBottom: 4 }}>ONNIQUANT</div>
        <div style={{ fontSize: 22, fontWeight: 600, letterSpacing: '-0.02em', marginBottom: 28 }}>EquityIQ</div>
        {displayError && <div style={{ background: 'rgba(220,38,38,0.15)', color: '#fca5a5', padding: '8px 12px', borderRadius: 8, fontSize: 12.5, marginBottom: 16, textAlign: 'left' }}>{displayError}</div>}
        <button onClick={signIn} disabled={loading} style={{
          width: '100%', padding: '11px 16px', borderRadius: 8,
          background: 'linear-gradient(90deg, #00A5B8, #00D084)',
          color: 'white', fontSize: 14, fontWeight: 500, letterSpacing: '0.01em',
          cursor: loading ? 'default' : 'pointer', opacity: loading ? 0.5 : 1,
          display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 10,
          border: 'none'
        }}>
          <svg width="16" height="16" viewBox="0 0 21 21" fill="currentColor">
            <path d="M0 0h10v10H0z"/><path d="M11 0h10v10H11z"/>
            <path d="M0 11h10v10H0z"/><path d="M11 11h10v10H11z"/>
          </svg>
          {loading ? 'Redirecting…' : 'Sign in with Microsoft'}
        </button>
        <div style={{ marginTop: 14, fontSize: 10.5, color: 'rgba(255,255,255,0.4)' }}>Powered by Onni</div>
      </div>
    </div>
  );
};

// ============ SIDEBAR ============
// `requiresElevated` flags entries that should show a red lock icon (and
// not navigate) when the current user isn't in EQ_AUTHORIZED_EMAILS.
// `accent` overrides the label color (used for the Corporate Development tab).
const NAV_ITEMS = [
  { id: 'dashboard',     label: 'Dashboard',             icon: 'M3 9l9-7 9 7v11a2 2 0 0 1-2 2h-4v-7h-6v7H5a2 2 0 0 1-2-2z' },
  { id: 'analyses',      label: 'Analyses',              icon: 'M4 4h16v4H4zm0 6h10v4H4zm0 6h16v4H4z' },
  { id: 'strategies',    label: 'Strategies',            icon: 'M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5' },
  { id: 'approvals',     label: 'Approvals',             icon: 'M9 12l2 2 4-4m5 2a9 9 0 1 1-18 0 9 9 0 0 1 18 0z',
    requiresElevated: true },
  { id: 'corporate-dev', label: 'Corporate Development', icon: 'M3 21h18M5 21V7l8-4v18M19 21V11l-6-4',
    requiresElevated: true, accent: '#0077b6' },
  { id: 'settings',      label: 'Settings',              icon: 'M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 0 0-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 0 0-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 0 0-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 0 0-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 0 0 1.066-2.573c-.94-1.543.826-3.31 2.37-2.37a1.724 1.724 0 0 0 2.572-1.065zM15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0z' },
];

// SVG path for a lock icon (used in nav item right-side badge)
const LOCK_ICON_PATH = 'M5 11h14a1 1 0 011 1v8a1 1 0 01-1 1H5a1 1 0 01-1-1v-8a1 1 0 011-1zm2 0V7a5 5 0 0110 0v4';

window.Sidebar = function Sidebar({ page, setPage, session, isElevated, onHide, onProfileClick }) {
  const email = session?.user?.email || '';
  const displayName = session?.user?.user_metadata?.name
    || session?.user?.user_metadata?.full_name
    || email.split('@')[0]
    || 'User';
  const initials = displayName.split(/[.\s]+/).map(s => s[0]).filter(Boolean).slice(0, 2).join('').toUpperCase();

  return (
    <aside style={{
      width: 232, flexShrink: 0, background: 'var(--paper)',
      borderRight: '1px solid var(--ink-200)',
      display: 'flex', flexDirection: 'column',
      position: 'sticky', top: 0, height: '100vh'
    }}>
      <div style={{ padding: '0 14px 0 20px', minHeight: 68, display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8, borderBottom: '1px solid var(--ink-150)' }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 10, minWidth: 0 }}>
          <img src="shared/onni-logo.png" alt="Onni" style={{ width: 28, height: 28, objectFit: 'contain', flexShrink: 0 }}/>
          <div style={{ minWidth: 0 }}>
            <div style={{ fontSize: 10, color: 'var(--ink-500)', letterSpacing: '0.12em', fontWeight: 500 }}>ONNIQUANT</div>
            <div style={{ fontSize: 15, fontWeight: 600, color: 'var(--ink-900)', letterSpacing: '-0.01em' }}>EquityIQ</div>
          </div>
        </div>
        {onHide && (
          <button
            onClick={onHide}
            title="Hide sidebar"
            style={{
              width: 26, height: 26, borderRadius: 6, flexShrink: 0,
              background: 'transparent', border: '1px solid transparent',
              color: 'var(--ink-500)', cursor: 'pointer',
              display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
            }}
            onMouseEnter={e => { e.currentTarget.style.background = 'var(--ink-100)'; e.currentTarget.style.borderColor = 'var(--ink-200)'; }}
            onMouseLeave={e => { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.borderColor = 'transparent'; }}
          >
            <svg width="14" height="14" viewBox="0 0 24 24" fill="none">
              <path d="M15 6l-6 6 6 6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
            </svg>
          </button>
        )}
      </div>

      <nav style={{ padding: '12px 8px', flex: 1 }}>
        <div style={{ fontSize: 10, color: 'var(--ink-400)', padding: '8px 10px 4px', letterSpacing: '0.08em', fontWeight: 500, textTransform: 'uppercase' }}>Workflows</div>
        {NAV_ITEMS.map(item => {
          const active = page === item.id || (page === 'analysis' && item.id === 'analyses');
          // Prefer DB-sourced isElevated prop; fall back to client-side
          // email check when prop hasn't loaded yet (e.g. before /auth/me
          // resolves on first render after sign-in).
          const elevated = isElevated === true
            ? true
            : (window.EQ_IS_AUTHORIZED ? window.EQ_IS_AUTHORIZED(session) : false);
          const locked = item.requiresElevated && !elevated;
          const labelColor = item.accent
            ? item.accent
            : (active ? 'var(--ink-900)' : 'var(--ink-600)');
          return (
            <button
              key={item.id}
              onClick={() => { if (!locked) setPage(item.id); }}
              disabled={locked}
              title={locked ? 'Restricted — contact your administrator for access' : undefined}
              style={{
                width: '100%', padding: '7px 10px',
                display: 'flex', alignItems: 'center', gap: 9,
                background: active ? 'var(--ink-100)' : 'transparent',
                color: labelColor,
                border: 'none', borderRadius: 6, fontSize: 13, fontWeight: active ? 500 : 400,
                textAlign: 'left', marginBottom: 1, fontFamily: 'inherit', transition: 'all 0.1s',
                cursor: locked ? 'not-allowed' : 'pointer',
                opacity: locked ? 0.85 : 1,
              }}
              onMouseEnter={e => { if (!active && !locked) e.currentTarget.style.background = 'var(--ink-50)'; }}
              onMouseLeave={e => { if (!active) e.currentTarget.style.background = 'transparent'; }}
            >
              <svg width="14" height="14" viewBox="0 0 24 24" fill="none" style={{ opacity: 0.75, flexShrink: 0 }}>
                <path d={item.icon} stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"/>
              </svg>
              <span style={{ flex: 1 }}>{item.label}</span>
              {locked && (
                <svg width="12" height="12" viewBox="0 0 24 24" fill="none" style={{ color: 'var(--pass)', flexShrink: 0 }}>
                  <path d={LOCK_ICON_PATH} stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
                </svg>
              )}
            </button>
          );
        })}
      </nav>

      <div style={{ padding: 8, borderTop: '1px solid var(--ink-150)' }}>
        <button
          onClick={onProfileClick}
          title="Profile & settings"
          style={{
            width: '100%', padding: 8, borderRadius: 8,
            background: 'transparent', border: '1px solid transparent',
            display: 'flex', alignItems: 'center', gap: 9, cursor: 'pointer',
            fontFamily: 'inherit', textAlign: 'left',
          }}
          onMouseEnter={e => { e.currentTarget.style.background = 'var(--ink-50)'; e.currentTarget.style.borderColor = 'var(--ink-150)'; }}
          onMouseLeave={e => { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.borderColor = 'transparent'; }}
        >
          <div style={{
            width: 28, height: 28, borderRadius: '50%',
            background: 'linear-gradient(135deg, #0070C0, #00C896)',
            color: 'white', fontSize: 11, fontWeight: 600,
            display: 'flex', alignItems: 'center', justifyContent: 'center',
            flexShrink: 0
          }}>{initials}</div>
          <div style={{ flex: 1, minWidth: 0 }}>
            <div style={{ fontSize: 12, color: 'var(--ink-800)', fontWeight: 500, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{displayName}</div>
            <div style={{ fontSize: 10.5, color: 'var(--ink-500)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{email}</div>
          </div>
          <svg width="14" height="14" viewBox="0 0 24 24" fill="none" style={{ color: 'var(--ink-400)', flexShrink: 0 }}>
            <path d="M9 6l6 6-6 6" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"/>
          </svg>
        </button>
      </div>
    </aside>
  );
};

// ============ PROFILE MODAL ============
// Triggered by clicking the profile chip in the sidebar bottom. Shows the
// session user's email (read-only), an editable display-name field, and a
// sign-out button. Display-name persists to Supabase user_metadata.
window.ProfileModal = function ProfileModal({ session, onClose, onSignOut }) {
  const { useState } = React;
  const email = session?.user?.email || '';
  const initialName =
    session?.user?.user_metadata?.name
    || session?.user?.user_metadata?.full_name
    || email.split('@')[0]
    || '';
  const [name, setName] = useState(initialName);
  const [busy, setBusy] = useState(false);
  const [msg, setMsg] = useState(null);
  const initials = (initialName || email).split(/[.\s]+/).map(s => s[0]).filter(Boolean).slice(0, 2).join('').toUpperCase() || '?';

  const save = async () => {
    if (!window.EQ_AUTH?.supabase) {
      setMsg({ ok: false, text: 'Auth client unavailable' });
      return;
    }
    setBusy(true); setMsg(null);
    try {
      const { error } = await window.EQ_AUTH.supabase.auth.updateUser({ data: { name } });
      if (error) throw error;
      setMsg({ ok: true, text: 'Saved' });
      setTimeout(() => setMsg(null), 2000);
    } catch (e) {
      setMsg({ ok: false, text: e.message || 'Save failed' });
    } finally {
      setBusy(false);
    }
  };

  const handleSignOut = async () => {
    onClose();
    if (onSignOut) await onSignOut();
  };

  return (
    <div
      onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}
      style={{
        position: 'fixed', inset: 0, zIndex: 60,
        background: 'rgba(10,20,40,0.45)',
        display: 'flex', alignItems: 'center', justifyContent: 'center',
      }}
    >
      <div style={{
        width: 420, maxWidth: '92vw',
        background: 'var(--paper)', borderRadius: 14, overflow: 'hidden',
        border: '1px solid var(--ink-150)',
        boxShadow: '0 24px 60px rgba(10,22,40,0.22)',
      }}>
        <div style={{ padding: '16px 20px', borderBottom: '1px solid var(--ink-150)', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
          <div style={{ fontSize: 13, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.06em', color: 'var(--ink-900)' }}>Profile</div>
          <button onClick={onClose} title="Close" style={{
            width: 26, height: 26, borderRadius: 6, background: 'transparent',
            border: 'none', color: 'var(--ink-500)', cursor: 'pointer',
          }}>
            <svg width="14" height="14" viewBox="0 0 24 24" fill="none">
              <path d="M6 6l12 12M18 6l-12 12" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/>
            </svg>
          </button>
        </div>

        <div style={{ padding: '20px 22px' }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 14, marginBottom: 22 }}>
            <div style={{
              width: 48, height: 48, borderRadius: '50%',
              background: 'linear-gradient(135deg, #0070C0, #00C896)',
              color: 'white', fontSize: 17, fontWeight: 700,
              display: 'flex', alignItems: 'center', justifyContent: 'center',
              flexShrink: 0,
            }}>{initials}</div>
            <div style={{ minWidth: 0 }}>
              <div style={{ fontSize: 14, fontWeight: 600, color: 'var(--ink-900)' }}>{name || email}</div>
              <div style={{ fontSize: 12, color: 'var(--ink-500)' }}>{email}</div>
            </div>
          </div>

          {/* Display name (editable) */}
          <label style={{ display: 'block', marginBottom: 16 }}>
            <div style={{ fontSize: 11, fontWeight: 600, color: 'var(--ink-500)', textTransform: 'uppercase', letterSpacing: '0.06em', marginBottom: 6 }}>Display name</div>
            <input
              type="text" value={name} onChange={e => setName(e.target.value)}
              placeholder="Your name"
              style={{
                width: '100%', padding: '8px 10px', fontSize: 13,
                border: '1px solid var(--ink-200)', borderRadius: 7,
                background: 'var(--paper)', color: 'var(--ink-900)',
                fontFamily: 'inherit', boxSizing: 'border-box',
              }}
            />
          </label>

          {/* Email (read-only) */}
          <label style={{ display: 'block', marginBottom: 18 }}>
            <div style={{ fontSize: 11, fontWeight: 600, color: 'var(--ink-500)', textTransform: 'uppercase', letterSpacing: '0.06em', marginBottom: 6 }}>Email</div>
            <input
              type="email" value={email} readOnly
              style={{
                width: '100%', padding: '8px 10px', fontSize: 13,
                border: '1px solid var(--ink-150)', borderRadius: 7,
                background: 'var(--ink-50)', color: 'var(--ink-700)',
                fontFamily: 'inherit', boxSizing: 'border-box', cursor: 'default',
              }}
            />
          </label>

          {msg && (
            <div style={{
              padding: '7px 11px', borderRadius: 7, marginBottom: 14, fontSize: 12.5,
              background: msg.ok ? 'var(--buy-soft)' : 'var(--pass-soft)',
              color: msg.ok ? 'var(--buy)' : 'var(--pass)',
              border: '1px solid ' + (msg.ok ? 'var(--buy-soft)' : 'var(--pass-soft)'),
            }}>{msg.ok ? '✓ ' : '✗ '}{msg.text}</div>
          )}

          <div style={{ display: 'flex', gap: 8, justifyContent: 'space-between', alignItems: 'center' }}>
            <button onClick={handleSignOut} style={{
              padding: '8px 14px', fontSize: 12.5, fontWeight: 500,
              background: 'transparent', color: 'var(--pass)',
              border: '1px solid var(--pass)', borderRadius: 7,
              cursor: 'pointer', fontFamily: 'inherit',
            }}>Sign out</button>
            <div style={{ display: 'flex', gap: 8 }}>
              <button onClick={onClose} style={{
                padding: '8px 14px', fontSize: 12.5, fontWeight: 500,
                background: 'var(--ink-100)', color: 'var(--ink-700)',
                border: '1px solid var(--ink-200)', borderRadius: 7,
                cursor: 'pointer', fontFamily: 'inherit',
              }}>Cancel</button>
              <button onClick={save} disabled={busy || name === initialName} style={{
                padding: '8px 14px', fontSize: 12.5, fontWeight: 500,
                background: 'var(--onni-primary)', color: 'white',
                border: 'none', borderRadius: 7, cursor: 'pointer',
                opacity: (busy || name === initialName) ? 0.5 : 1,
                fontFamily: 'inherit',
              }}>{busy ? 'Saving…' : 'Save'}</button>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
};

// ============ DECISION BADGE ============
window.DecisionBadge = function DecisionBadge({ decision, size = 'sm' }) {
  const big = size === 'lg';
  const label = decision ?? '—';
  const isBuy = decision === 'BUY';
  const isRunning = decision === 'RUNNING' || decision === 'running';
  const color = isBuy ? 'var(--buy)' : isRunning ? 'var(--ink-500)' : 'var(--pass)';
  const bg = isBuy ? 'var(--buy-soft)' : isRunning ? 'var(--ink-100)' : 'var(--pass-soft)';

  return (
    <span style={{
      display: 'inline-flex', alignItems: 'center', gap: big ? 7 : 5,
      padding: big ? '5px 12px' : '2px 8px',
      borderRadius: 999, background: bg, color,
      fontFamily: 'var(--font-mono)', fontWeight: 600,
      fontSize: big ? 13 : 10.5, letterSpacing: '0.04em'
    }}>
      <span style={{
        width: big ? 7 : 5, height: big ? 7 : 5, borderRadius: '50%', background: 'currentColor',
        animation: isRunning ? 'pulse 1.4s ease-in-out infinite' : 'none',
        flexShrink: 0
      }}/>
      {label}
    </span>
  );
};

// ============ STATUS BADGE ============
window.StatusBadge = function StatusBadge({ status }) {
  const map = {
    queued:     { bg: 'var(--ink-100)',   fg: 'var(--ink-600)' },
    running:    { bg: 'var(--warn-soft)', fg: 'var(--warn)' },
    completed:  { bg: 'var(--buy-soft)', fg: 'var(--buy)' },
    failed:     { bg: 'var(--pass-soft)', fg: 'var(--pass)' },
    cancelled:  { bg: 'var(--ink-100)',   fg: 'var(--ink-500)' },
  };
  const s = map[status] || { bg: 'var(--ink-100)', fg: 'var(--ink-600)' };
  return (
    <span style={{
      display: 'inline-block', padding: '2px 8px', borderRadius: 999,
      background: s.bg, color: s.fg, fontFamily: 'var(--font-mono)',
      fontSize: 10.5, fontWeight: 600, letterSpacing: '0.04em', textTransform: 'capitalize'
    }}>{status || '—'}</span>
  );
};

// ============ SPARKLINE ============
window.Sparkline = function Sparkline({ data, width = 120, height = 36, fill = true }) {
  if (!data || data.length < 2) {
    return <div style={{ width, height, color: 'var(--ink-400)', fontSize: 11, display: 'flex', alignItems: 'center' }}>—</div>;
  }
  const pad = 2;
  const min = Math.min(...data), max = Math.max(...data);
  const range = max - min || 1;
  const stepX = (width - pad*2) / (data.length - 1);
  const pts = data.map((v, i) => [pad + i*stepX, pad + (height - pad*2) * (1 - (v - min) / range)]);
  const line = pts.map((p, i) => (i ? 'L' : 'M') + p[0].toFixed(1) + ',' + p[1].toFixed(1)).join(' ');
  const area = line + ` L${width-pad},${height-pad} L${pad},${height-pad} Z`;
  const up = data[data.length-1] >= data[0];
  const c = up ? 'var(--buy)' : 'var(--pass)';
  return (
    <svg width={width} height={height} style={{ display: 'block' }}>
      {fill && <path d={area} fill={c} fillOpacity="0.08"/>}
      <path d={line} stroke={c} strokeWidth="1.5" fill="none" strokeLinecap="round" strokeLinejoin="round"/>
      <circle cx={pts[pts.length-1][0]} cy={pts[pts.length-1][1]} r="2.5" fill={c}/>
    </svg>
  );
};

// ============ MOBILE NAV BAR ============
// Shown instead of the sidebar on screens < 768px. Bottom tab bar style.
window.MobileNav = function MobileNav({ page, setPage, session, isElevated }) {
  const MOBILE_TABS = [
    { id: 'dashboard',     label: 'Home',      icon: 'M3 9l9-7 9 7v11a2 2 0 0 1-2 2h-4v-7h-6v7H5a2 2 0 0 1-2-2z' },
    { id: 'analyses',      label: 'Analyses',  icon: 'M4 4h16v4H4zm0 6h10v4H4zm0 6h16v4H4z' },
    { id: 'approvals',     label: 'Approvals', icon: 'M9 12l2 2 4-4m5 2a9 9 0 1 1-18 0 9 9 0 0 1 18 0z',
      requiresElevated: true },
    { id: 'corporate-dev', label: 'Corp Dev',  icon: 'M3 21h18M5 21V7l8-4v18M19 21V11l-6-4',
      requiresElevated: true, accent: '#0077b6' },
    { id: 'settings',      label: 'Settings',  icon: 'M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 0 0-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 0 0-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 0 0-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 0 0-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 0 0 1.066-2.573c-.94-1.543.826-3.31 2.37-2.37a1.724 1.724 0 0 0 2.572-1.065zM15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0z' },
  ];
  const active = p => page === p || (page === 'analysis' && p === 'analyses');
  const elevated = isElevated === true
    ? true
    : (window.EQ_IS_AUTHORIZED ? window.EQ_IS_AUTHORIZED(session) : false);
  return (
    <nav style={{
      display: 'flex', background: 'var(--paper)',
      borderBottom: '1px solid var(--ink-150)',
      padding: '0 4px', gap: 2, flexShrink: 0,
    }}>
      {MOBILE_TABS.map(t => {
        const locked = t.requiresElevated && !elevated;
        const baseColor = t.accent || (active(t.id) ? 'var(--onni-primary)' : 'var(--ink-400)');
        return (
          <button key={t.id}
            onClick={() => { if (!locked) setPage(t.id); }}
            disabled={locked}
            title={locked ? 'Restricted' : undefined}
            style={{
              flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center',
              gap: 3, padding: '10px 4px 8px', position: 'relative',
              background: 'transparent', border: 'none',
              cursor: locked ? 'not-allowed' : 'pointer',
              color: baseColor, fontFamily: 'inherit',
              borderBottom: active(t.id) ? '2px solid var(--onni-primary)' : '2px solid transparent',
              opacity: locked ? 0.85 : 1,
            }}
          >
            <svg width="18" height="18" viewBox="0 0 24 24" fill="none">
              <path d={t.icon} stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"/>
            </svg>
            <span style={{ fontSize: 9.5, fontWeight: active(t.id) ? 600 : 400, letterSpacing: '0.02em' }}>{t.label}</span>
            {locked && (
              <svg width="9" height="9" viewBox="0 0 24 24" fill="none" style={{
                position: 'absolute', top: 7, right: 'calc(50% - 17px)', color: 'var(--pass)',
              }}>
                <path d={LOCK_ICON_PATH} stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"/>
              </svg>
            )}
          </button>
        );
      })}
    </nav>
  );
};

// ============ TOP BAR ============
window.TopBar = function TopBar({ title, subtitle, right }) {
  const px = window.innerWidth < 768 ? 16 : 32;
  return (
    <div style={{
      display: 'flex', justifyContent: 'space-between', alignItems: 'center',
      padding: `0 ${px}px`, minHeight: 68, borderBottom: '1px solid var(--ink-150)',
      background: 'var(--paper)', position: 'sticky', top: 0, zIndex: 10, gap: 16,
      flexShrink: 0,
    }}>
      <div style={{ minWidth: 0 }}>
        <h1 style={{ margin: 0, fontSize: 19, fontWeight: 700, color: 'var(--ink-900)', letterSpacing: '-0.025em' }}>{title}</h1>
        {subtitle && <div style={{ fontSize: 12, color: 'var(--ink-400)', marginTop: 2, letterSpacing: '0.01em' }}>{subtitle}</div>}
      </div>
      <div style={{ display: 'flex', alignItems: 'center', gap: 8, flexShrink: 0 }}>
        {right}
      </div>
    </div>
  );
};

// ============ KEY-VAL pair ============
window.KV = function KV({ label, value, hint, color }) {
  return (
    <div>
      <div style={{ fontSize: 10, color: 'var(--ink-600)', textTransform: 'uppercase', letterSpacing: '0.08em', fontWeight: 700, marginBottom: 5 }}>{label}</div>
      <div style={{ fontSize: 20, fontWeight: 700, color: color || 'var(--ink-900)', letterSpacing: '-0.02em', fontFamily: 'var(--font-mono)' }}>{value ?? '—'}</div>
      {hint && <div style={{ fontSize: 11, color: 'var(--ink-500)', marginTop: 3 }}>{hint}</div>}
    </div>
  );
};

// ============ useCountUp — smooth numeric count-up on mount / value change ============
//
// Returns the incrementing value on each RAF tick for `duration` ms, eased
// out. If `target` is not a finite number (null, skeleton, arbitrary node,
// string) it passes through unchanged — safe to call unconditionally so the
// Rules of Hooks are satisfied.
window.useCountUp = function useCountUp(target, duration = 900) {
  const { useState, useEffect, useRef } = React;
  const prev = useRef(0);
  const [value, setValue] = useState(
    typeof target === 'number' && isFinite(target) ? 0 : target
  );

  useEffect(() => {
    if (typeof target !== 'number' || !isFinite(target)) {
      setValue(target);
      return;
    }
    const from = prev.current;
    const to = target;
    if (from === to) { setValue(to); return; }

    const start = performance.now();
    let rafId;
    const tick = (now) => {
      const t = Math.min((now - start) / duration, 1);
      const eased = 1 - Math.pow(1 - t, 3); // ease-out-cubic
      const current = from + (to - from) * eased;
      setValue(Math.round(current));
      if (t < 1) rafId = requestAnimationFrame(tick);
      else prev.current = to;
    };
    rafId = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(rafId);
  }, [target, duration]);

  return value;
};

// ============ STAT CARD ============
window.StatCard = function StatCard({ label, value, hint, accent, icon, onClick, active }) {
  const displayValue = window.useCountUp(value);
  const isClickable = !!onClick;

  return (
    <div className="card card-hover" onClick={onClick} style={{
      background: active ? (accent ? accent + '0C' : 'var(--ink-50)') : 'var(--paper)',
      borderRadius: 12,
      borderTop: accent ? `3px solid ${accent}` : active ? '3px solid var(--ink-400)' : '1px solid var(--ink-150)',
      borderRight: '1px solid ' + (active ? (accent || 'var(--ink-400)') : 'var(--ink-150)'),
      borderBottom: '1px solid ' + (active ? (accent || 'var(--ink-400)') : 'var(--ink-150)'),
      borderLeft: '1px solid ' + (active ? (accent || 'var(--ink-400)') : 'var(--ink-150)'),
      padding: '16px 18px',
      display: 'flex', flexDirection: 'column', gap: 8,
      cursor: isClickable ? 'pointer' : 'default',
      transition: 'background 0.15s ease, border-color 0.15s ease',
    }}>
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
        <div style={{ fontSize: 10.5, color: 'var(--ink-700)', textTransform: 'uppercase', letterSpacing: '0.08em', fontWeight: 700 }}>{label}</div>
        {icon && (
          <div style={{
            width: 28, height: 28, borderRadius: 7,
            background: accent ? accent + '18' : 'var(--ink-100)',
            display: 'flex', alignItems: 'center', justifyContent: 'center'
          }}>
            <svg width="13" height="13" viewBox="0 0 24 24" fill="none" style={{ color: accent || 'var(--ink-500)' }}>
              <path d={icon} stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"/>
            </svg>
          </div>
        )}
      </div>
      <div className="stat-tnum" style={{ fontSize: 30, fontWeight: 700, color: accent || 'var(--ink-900)', letterSpacing: '-0.03em', fontFamily: 'var(--font-mono)', lineHeight: 1 }}>{displayValue ?? '—'}</div>
      {hint && <div style={{ fontSize: 11.5, color: 'var(--ink-500)' }}>{hint}</div>}
    </div>
  );
};

// ============ EMPTY STATE ============
window.EmptyState = function EmptyState({ title, hint }) {
  return (
    <div style={{
      padding: '48px 20px', textAlign: 'center',
      background: 'var(--paper)', border: '1px dashed var(--ink-200)',
      borderRadius: 12, color: 'var(--ink-500)'
    }}>
      <svg width="32" height="32" viewBox="0 0 24 24" fill="none" style={{ margin: '0 auto 12px', display: 'block', color: 'var(--ink-300)' }}>
        <path d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/>
      </svg>
      <div style={{ fontSize: 13.5, color: 'var(--ink-600)', fontWeight: 600 }}>{title}</div>
      {hint && <div style={{ fontSize: 12, marginTop: 6, color: 'var(--ink-400)' }}>{hint}</div>}
    </div>
  );
};

// ============ ERROR BANNER ============
window.ErrorBanner = function ErrorBanner({ error, onRetry }) {
  if (!error) return null;
  return (
    <div style={{
      padding: '10px 14px', background: 'var(--pass-soft)', color: 'var(--pass)',
      border: '1px solid var(--pass-soft)', borderRadius: 8, fontSize: 12.5,
      display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 12
    }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" style={{ flexShrink: 0 }}><path d="M12 9v4m0 4h.01M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round"/></svg>
        <span>{error.message || String(error)}</span>
      </div>
      {onRetry && <button onClick={onRetry} style={{
        padding: '4px 10px', fontSize: 11, background: 'var(--paper)', color: 'var(--pass)',
        border: '1px solid var(--pass-soft)', borderRadius: 6, cursor: 'pointer', whiteSpace: 'nowrap', fontFamily: 'inherit'
      }}>Retry</button>}
    </div>
  );
};
