// Tempo — thème, icônes, libellés de navigation (NAV_LABELS), menu latéral,
// en-tête de page, coques ordinateur et mobile, comportement commun des
// modales (useModalBehavior).

// ─── Thème (clair/sombre) ─────────────────────────────────────────
// Un seul état partagé par tous les appels de useTheme (en-tête, Réglages
// mobile, graphiques du backtest) : une bascule met tout le monde à jour.
// Sans choix enregistré, on suit la préférence du système, y compris quand
// elle change. On n'écrit dans localStorage qu'au clic sur le bouton de thème.
const THEME_KEY = 'tempo:theme';

function applyTheme(theme) {
  const t = theme === 'dark' ? 'dark' : 'light';
  if (typeof document !== 'undefined' && document.documentElement) {
    if (t === 'dark') document.documentElement.setAttribute('data-theme', 'dark');
    else document.documentElement.removeAttribute('data-theme');
    const meta = document.querySelector('meta[name="theme-color"]');
    if (meta) meta.setAttribute('content', t === 'dark' ? '#0a0a0a' : '#ffffff');
  }
}

function themeStoredChoice() {
  try {
    const s = localStorage.getItem(THEME_KEY);
    return s === 'dark' || s === 'light' ? s : null;
  } catch (e) { return null; }
}

function themeSystemChoice() {
  try {
    return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
  } catch (e) { return 'light'; }
}

let themeCurrent = themeStoredChoice() || themeSystemChoice();
const THEME_LISTENERS = new Set();

// setAppTheme(theme, persist) — applique le thème partout ; persist = true
// seulement pour un choix explicite de l'utilisateur.
function setAppTheme(theme, persist) {
  themeCurrent = theme === 'dark' ? 'dark' : 'light';
  applyTheme(themeCurrent);
  if (persist) {
    try { localStorage.setItem(THEME_KEY, themeCurrent); } catch (e) {}
  }
  THEME_LISTENERS.forEach(fn => fn(themeCurrent));
}

function useTheme() {
  const [theme, setTheme] = React.useState(themeCurrent);
  React.useEffect(() => {
    THEME_LISTENERS.add(setTheme);
    setTheme(themeCurrent);
    return () => { THEME_LISTENERS.delete(setTheme); };
  }, []);
  const toggle = React.useCallback(() => setAppTheme(themeCurrent === 'dark' ? 'light' : 'dark', true), []);
  return { theme, toggle };
}

// Thème appliqué dès l'exécution du script (avant le montage de React).
applyTheme(themeCurrent);
try {
  const mq = window.matchMedia('(prefers-color-scheme: dark)');
  const onSystemChange = () => { if (!themeStoredChoice()) setAppTheme(mq.matches ? 'dark' : 'light', false); };
  if (mq.addEventListener) mq.addEventListener('change', onSystemChange);
  else if (mq.addListener) mq.addListener(onSystemChange);
} catch (e) {}
// Choix fait dans un autre onglet (application ou page de vente).
window.addEventListener('storage', (e) => {
  if (e.key === THEME_KEY || e.key === null) setAppTheme(themeStoredChoice() || themeSystemChoice(), false);
});

// ─── État du CME ──────────────────────────────────────────────────
// Source unique : isCMEOpen() de format-prefs.jsx (heure de Chicago, pause
// quotidienne 16:00–17:00, week-end, 1er janvier, Vendredi saint et Noël),
// juste aussi pendant les semaines où
// l'Europe et les États-Unis ne changent pas d'heure le même jour.
function useMarketOpen() {
  const [open, setOpen] = React.useState(() => isCMEOpen(new Date()));
  React.useEffect(() => {
    const t = setInterval(() => setOpen(isCMEOpen(new Date())), 30 * 1000);
    return () => clearInterval(t);
  }, []);
  return open;
}

// ─── Icons ────────────────────────────────────────────────────────
const Ico = {
  home: <svg className="ico" width="18" height="18" viewBox="0 0 20 20"><path d="M3 9l7-6 7 6v8a1 1 0 01-1 1h-4v-6H8v6H4a1 1 0 01-1-1z"/></svg>,
  connections: <svg className="ico" width="18" height="18" viewBox="0 0 20 20"><circle cx="5" cy="10" r="2.4"/><circle cx="15" cy="5" r="2.4"/><circle cx="15" cy="15" r="2.4"/><path d="M7 9l6-3M7 11l6 3"/></svg>,
  cockpit: <svg className="ico" width="18" height="18" viewBox="0 0 20 20"><circle cx="10" cy="10" r="7"/><path d="M10 5v5l3 2M5 10h1M14 10h1M10 4v1M10 15v1"/></svg>,
  groups: <svg className="ico" width="18" height="18" viewBox="0 0 20 20"><circle cx="7" cy="8" r="2.6"/><circle cx="14" cy="9" r="2.2"/><path d="M3 16c.6-2.4 2.4-3.5 4-3.5s3.4 1.1 4 3.5M11 15c.5-1.6 1.8-2.5 3-2.5s2.5.9 3 2.5"/></svg>,
  risk: <svg className="ico" width="18" height="18" viewBox="0 0 20 20"><path d="M10 2l7 3v5c0 4-3 7-7 8-4-1-7-4-7-8V5z"/><path d="M7.5 10.5l1.7 1.7L13 8"/></svg>,
  econ: <svg className="ico" width="18" height="18" viewBox="0 0 20 20"><rect x="3" y="4" width="14" height="13" rx="1.5"/><path d="M3 8h14M7 3v3M13 3v3"/></svg>,
  journaling: <svg className="ico" width="18" height="18" viewBox="0 0 20 20"><path d="M5 3h9l3 3v11a1 1 0 01-1 1H5a1 1 0 01-1-1V4a1 1 0 011-1z"/><path d="M14 3v3h3M7 10h7M7 13h5M7 16h3"/></svg>,
  dashboard: <svg className="ico" width="18" height="18" viewBox="0 0 20 20"><rect x="3" y="3" width="7" height="9" rx="1.4"/><rect x="3" y="14" width="7" height="3" rx="1"/><rect x="12" y="3" width="5" height="6" rx="1.4"/><rect x="12" y="11" width="5" height="6" rx="1.4"/></svg>,
  trades: <svg className="ico" width="18" height="18" viewBox="0 0 20 20"><path d="M3 15l4-4 3 2 7-8"/><path d="M13 5h4v4"/></svg>,
  daily: <svg className="ico" width="18" height="18" viewBox="0 0 20 20"><rect x="3" y="4" width="14" height="13" rx="1.5"/><path d="M3 8h14M10 11v3M7 11v3M13 11v3"/></svg>,
  weekly: <svg className="ico" width="18" height="18" viewBox="0 0 20 20"><rect x="3" y="4" width="14" height="13" rx="1.5"/><path d="M3 8h14M6 12h8"/></svg>,
  stats: <svg className="ico" width="18" height="18" viewBox="0 0 20 20"><path d="M3 3v13a1 1 0 001 1h13"/><path d="M7 13l3-4 2.5 2.5L17 6"/></svg>,
  strategy: <svg className="ico" width="18" height="18" viewBox="0 0 20 20"><path d="M5 3h7l3 3v11a1 1 0 01-1 1H5a1 1 0 01-1-1V4a1 1 0 011-1z"/><path d="M12 3v3h3"/><path d="M7 10h2M7 13h6"/><circle cx="12.5" cy="11" r="1"/></svg>,
  data: <svg className="ico" width="18" height="18" viewBox="0 0 20 20"><ellipse cx="10" cy="5" rx="6.5" ry="2.4"/><path d="M3.5 5v5c0 1.3 2.9 2.4 6.5 2.4s6.5-1.1 6.5-2.4V5"/><path d="M3.5 10v5c0 1.3 2.9 2.4 6.5 2.4s6.5-1.1 6.5-2.4v-5"/></svg>,
  prop: <svg className="ico" width="18" height="18" viewBox="0 0 20 20"><rect x="3" y="6" width="14" height="11" rx="1.5"/><path d="M7 6V4a1.5 1.5 0 011.5-1.5h3A1.5 1.5 0 0113 4v2M3 10h14"/></svg>,
  affiliate: <svg className="ico" width="18" height="18" viewBox="0 0 20 20"><path d="M10 4v12M6 8a2 2 0 012-2h2v4H8a2 2 0 01-2-2zM14 12a2 2 0 01-2 2h-2v-4h2a2 2 0 012 2z"/></svg>,
  settings: <svg className="ico" width="18" height="18" viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>,
  plus: <svg className="ico" width="16" height="16" viewBox="0 0 16 16"><path d="M8 2v12M2 8h12"/></svg>,
  search: <svg className="ico" width="16" height="16" viewBox="0 0 16 16"><circle cx="7" cy="7" r="5"/><path d="M11 11l4 4"/></svg>,
  chev: <svg className="ico" width="14" height="14" viewBox="0 0 14 14"><path d="M5 2l5 5-5 5"/></svg>,
  chevDown: <svg className="ico" width="12" height="12" viewBox="0 0 12 12"><path d="M2 4.5l4 4 4-4"/></svg>,
  arrL: <svg className="ico" width="14" height="14" viewBox="0 0 14 14"><path d="M9 2L4 7l5 5"/></svg>,
  arrR: <svg className="ico" width="14" height="14" viewBox="0 0 14 14"><path d="M5 2l5 5-5 5"/></svg>,
  check: <svg className="ico" width="13" height="13" viewBox="0 0 13 13"><path d="M2 6.5l3 3 6-7"/></svg>,
  x: <svg className="ico" width="12" height="12" viewBox="0 0 12 12"><path d="M2 2l8 8M10 2l-8 8"/></svg>,
  upload: <svg className="ico" width="16" height="16" viewBox="0 0 18 18"><path d="M9 12V2M5 6l4-4 4 4"/><path d="M2 12v3a1 1 0 001 1h12a1 1 0 001-1v-3"/></svg>,
  filter: <svg className="ico" width="14" height="14" viewBox="0 0 14 14"><path d="M1 3h12M3 7h8M5 11h4"/></svg>,
  bell: <svg className="ico" width="16" height="16" viewBox="0 0 16 16"><path d="M3 12V7a5 5 0 0110 0v5M1 12h14M6.5 14a1.5 1.5 0 003 0"/></svg>,
  help: <svg className="ico" width="16" height="16" viewBox="0 0 16 16"><circle cx="8" cy="8" r="6"/><path d="M6.2 6.2a2 2 0 113.6 1.3c-.4.5-1.3.6-1.3 1.7"/><circle cx="8" cy="11.5" r=".7" fill="currentColor"/></svg>,
  sun: <svg className="ico" width="16" height="16" viewBox="0 0 16 16"><circle cx="8" cy="8" r="3"/><path d="M8 1v2M8 13v2M1 8h2M13 8h2M2.8 2.8l1.4 1.4M11.8 11.8l1.4 1.4M2.8 13.2l1.4-1.4M11.8 4.2l1.4-1.4"/></svg>,
  moon: <svg className="ico" width="16" height="16" viewBox="0 0 16 16"><path d="M14 9.5A6 6 0 1 1 6.5 2a4.5 4.5 0 0 0 7.5 7.5z"/></svg>,
  cam: <svg className="ico" width="14" height="14" viewBox="0 0 16 16"><rect x="1.5" y="4" width="13" height="9" rx="1.5"/><circle cx="8" cy="8.5" r="2.5"/><path d="M5 4l1-1.5h4L11 4"/></svg>,
  book: <svg className="ico" width="14" height="14" viewBox="0 0 16 16"><path d="M2 3h5a2 2 0 012 2v9a1 1 0 00-1-1H2zM14 3H9a2 2 0 00-2 2v9a1 1 0 011-1h5z"/></svg>,
  sync: <svg className="ico" width="14" height="14" viewBox="0 0 16 16"><path d="M2 8a6 6 0 0110-4.5M14 8a6 6 0 01-10 4.5"/><path d="M11 2v3.5h-3M5 14v-3.5h3"/></svg>,
  refresh: <svg className="ico" width="14" height="14" viewBox="0 0 16 16"><path d="M14 3v4h-4M2 13v-4h4"/><path d="M3.5 7a5 5 0 018.5-2L14 7M12.5 9a5 5 0 01-8.5 2L2 9"/></svg>,
  backtest: <svg className="ico" width="18" height="18" viewBox="0 0 20 20"><path d="M3 3v13a1 1 0 001 1h13"/><path d="M7 13l2.5-3 2 1.5L16 6"/><path d="M8.5 15.5l3 1.8v-3.6z" fill="currentColor" stroke="none"/></svg>,
  fondamental: <svg className="ico" width="18" height="18" viewBox="0 0 20 20"><circle cx="10" cy="10" r="7"/><path d="M3 10h14"/><ellipse cx="10" cy="10" rx="3.2" ry="7"/></svg>,
  more: <svg className="ico" width="18" height="18" viewBox="0 0 20 20"><circle cx="4.5" cy="10" r="1.7" fill="currentColor" stroke="none"/><circle cx="10" cy="10" r="1.7" fill="currentColor" stroke="none"/><circle cx="15.5" cy="10" r="1.7" fill="currentColor" stroke="none"/></svg>,
  sidebar: <svg className="ico" width="18" height="18" viewBox="0 0 20 20"><rect x="3" y="4" width="14" height="12" rx="1.5"/><path d="M7 4v12"/></svg>,
};

// ─── Libellés de navigation (charte §5) ───────────────────────────
// SEULE source des noms d'écrans : menu latéral, titre de l'en-tête, barre du
// bas mobile, menu Plus, tuiles et raccourcis de l'Accueil.
//   menu   libellé du menu latéral
//   titre  titre de la page (en-tête)
//   court  libellé des cases étroites (barre du bas, raccourcis)
//   desc   phrase d'aide des tuiles et du menu Plus
//   ico    icône de navigation (18 px pour toutes les entrées)
const NAV_LABELS = {
  home:        { menu: 'Accueil',          titre: 'Accueil',                court: 'Accueil',          ico: Ico.home },
  econ:        { menu: 'Calendrier éco.',  titre: 'Calendrier économique',  court: 'Calendrier éco.',  desc: 'Deux semaines d’événements, impact et valeur publiée', ico: Ico.econ },
  routines:    { menu: 'Routines',         titre: 'Routines',               court: 'Routines',         desc: 'Habitudes, vérifications avant trade et agenda', ico: React.cloneElement(Ico.book, { width: 18, height: 18 }) },
  backtest:    { menu: 'Backtest',         titre: 'Backtest',               court: 'Backtest',         ico: Ico.backtest },
  fondamental: { menu: 'Fondamental',      titre: 'Analyse fondamentale',   court: 'Fondamental',      desc: 'Contexte macro : biais, catalyseurs et actualités', ico: Ico.fondamental },
  journal:     { menu: 'Journal',          ico: Ico.journaling },
  dashboard:   { menu: 'Tableau de bord',  titre: 'Tableau de bord',        court: 'Tableau de bord',  desc: 'Indicateurs clés, jauges, P&L cumulé, calendrier', ico: Ico.dashboard },
  trades:      { menu: 'Trades',           titre: 'Trades',                 court: 'Trades',           ico: Ico.trades },
  daily:       { menu: 'Journal quotidien', titre: 'Journal quotidien',     court: 'Journal quotidien', desc: 'Tes journaux quotidiens', ico: Ico.daily },
  stats:       { menu: 'Statistiques',     titre: 'Statistiques',           court: 'Statistiques',     desc: 'Analyse de tes trades et de tes habitudes', ico: Ico.stats },
  weekly:      { menu: 'Bilan hebdo',      titre: 'Bilan hebdomadaire',     court: 'Bilan hebdo',      desc: 'Bilan de tes semaines de trading', ico: Ico.weekly },
  strategy:    { menu: 'Stratégies',       titre: 'Stratégies',             court: 'Stratégies',       desc: 'Tes stratégies et leurs performances', ico: Ico.strategy },
  data:        { menu: 'Données',          titre: 'Gestion des données',    court: 'Données',          desc: 'Import, synchronisation, export et nettoyage', ico: Ico.data },
  settings:    { menu: 'Réglages',         titre: 'Réglages',               court: 'Plus',             ico: Ico.settings },
  profile:     { menu: 'Mon profil' },
};
// Ordre des entrées : menu principal, groupe Journal, barre du bas mobile.
const NAV_MAIN = ['home', 'econ', 'routines', 'backtest', 'fondamental'];
const NAV_JOURNAL = ['dashboard', 'trades', 'daily', 'stats', 'weekly', 'strategy', 'data'];
const NAV_BOTTOM = ['home', 'stats', 'trades', 'backtest', 'settings'];

// ─── Menu latéral ─────────────────────────────────────────────────
// Entrée du menu. Composant de niveau module (et non recréé à chaque rendu du
// menu) : le bouton garde le focus clavier après un changement d'écran.
// Menu replié : icône seule, libellé en info-bulle et en nom accessible.
function SidebarItem({ id, sub, on, collapsed, onChange }) {
  const nav = NAV_LABELS[id] || {};
  const label = nav.menu || id;
  return (
    <button
      type="button"
      onClick={() => onChange && onChange(id)}
      className="tap"
      aria-current={on ? 'page' : undefined}
      aria-label={collapsed ? label : undefined}
      title={collapsed ? label : undefined}
      style={{
        width: '100%',
        background: on ? 'var(--blue-soft)' : 'transparent',
        border: 'none',
        padding: collapsed ? '9px 0' : sub ? '8px 12px 8px 36px' : '8px 12px',
        margin: '1px 0',
        cursor: 'pointer',
        display: 'flex', alignItems: 'center', justifyContent: collapsed ? 'center' : 'flex-start', gap: 11,
        color: on ? 'var(--blue-600)' : 'var(--fg-2)',
        textAlign: 'left',
        borderRadius: 8,
        fontFamily: 'inherit',
        position: 'relative',
      }}>
      {on && (collapsed || !sub) && <span aria-hidden="true" style={{ position: 'absolute', left: -8, top: 8, bottom: 8, width: 2.5, borderRadius: 2, background: 'var(--blue)' }}></span>}
      <span style={{ display: 'flex', opacity: on ? 1 : .7 }}>{nav.ico}</span>
      {!collapsed && <span style={{ fontSize: 13, fontWeight: on ? 600 : 500, letterSpacing: '-.01em', flex: 1, minWidth: 0, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{label}</span>}
    </button>
  );
}

function Sidebar({ active, onChange, journalingOpen = true, setJournalingOpen, user, displayName, collapsed = false }) {
  const email = user?.email || '';
  const handle = email ? email.split('@')[0] : '—';
  // Initials prefer the display name, else the email handle.
  const initialsSource = (displayName || handle || '').trim();
  const initials = initialsSource.slice(0, 2).toUpperCase() || '—';
  const profileLabel = (displayName || '').trim() || NAV_LABELS.profile.menu;
  const profileHint = NAV_LABELS.profile.menu + ' — ouvrir les réglages';
  const isInJournal = NAV_JOURNAL.includes(active);
  // Replié, l'en-tête du groupe Journal disparaît : ses entrées restent visibles.
  const showJournal = collapsed || journalingOpen;

  return (
    <aside id="tempo-menu" aria-label="Menu" style={{
      width: collapsed ? 64 : 232, flexShrink: 0, height: '100%',
      borderRight: '1px solid var(--line)',
      display: 'flex', flexDirection: 'column',
      background: 'var(--bg-app)',
      overflow: 'hidden',
      transition: 'width .2s var(--ease)',
    }}>
      {/* Marque */}
      <div style={{ padding: collapsed ? '18px 0' : '18px 16px', display: 'flex', alignItems: 'center', justifyContent: collapsed ? 'center' : 'flex-start', gap: 9 }}>
        <div aria-hidden="true" style={{ width: 28, height: 28, flexShrink: 0, borderRadius: 8, background: 'var(--blue)', display: 'flex', alignItems: 'center', justifyContent: 'center', position: 'relative' }}>
          <div style={{ width: 1.5, height: 14, background: '#fff' }}></div>
          <div style={{ position: 'absolute', width: 10, height: 1.5, background: '#fff', top: 8 }}></div>
        </div>
        {!collapsed && <span style={{ fontSize: 18, fontWeight: 700, letterSpacing: '-.02em', color: 'var(--blue-600)' }}>Tempo</span>}
      </div>

      {/* Profil : ouvre les réglages. Jamais surligné, seule l'entrée
          Réglages porte l'état actif. */}
      <button type="button" className="tap" onClick={() => onChange('settings')} title={profileHint} aria-label={collapsed ? profileHint : undefined} style={{
        margin: '0 12px 14px',
        background: 'transparent',
        border: 'none', borderRadius: 10,
        padding: collapsed ? '4px 0' : '8px 10px', cursor: 'pointer',
        display: 'flex', alignItems: 'center', justifyContent: collapsed ? 'center' : 'flex-start', gap: 10,
        color: 'var(--fg)', fontFamily: 'inherit',
      }}>
        <div aria-hidden="true" style={{ width: 32, height: 32, flexShrink: 0, borderRadius: '50%', background: 'linear-gradient(135deg, #1e293b, #475569)', color: '#fff', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 12, fontWeight: 600 }}>{initials}</div>
        {!collapsed && (
          <div style={{ display: 'flex', flexDirection: 'column', gap: 1, flex: 1, minWidth: 0, textAlign: 'left', overflow: 'hidden' }}>
            <span style={{ fontSize: 13, fontWeight: 600, letterSpacing: '-.01em', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{profileLabel}</span>
            <span style={{ fontSize: 11, color: 'var(--fg-3)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{email || '—'}</span>
          </div>
        )}
      </button>

      <nav aria-label="Navigation principale" style={{ flex: 1, minHeight: 0, overflowY: 'auto', overflowX: 'hidden', padding: '0 12px' }}>
        {NAV_MAIN.map(id => <SidebarItem key={id} id={id} on={active === id} collapsed={collapsed} onChange={onChange}/>)}

        {/* Groupe Journal */}
        <div style={{ marginTop: 14 }}>
          {collapsed ? (
            <div aria-hidden="true" style={{ height: 1, background: 'var(--line)', margin: '0 6px 8px' }}></div>
          ) : (
            <button
              type="button"
              onClick={() => setJournalingOpen && setJournalingOpen(!journalingOpen)}
              aria-expanded={journalingOpen}
              className="tap"
              style={{
                width: '100%', background: isInJournal ? 'var(--blue-soft)' : 'transparent',
                border: 'none', padding: '8px 12px', cursor: 'pointer',
                display: 'flex', alignItems: 'center', gap: 11,
                color: isInJournal ? 'var(--blue-600)' : 'var(--fg-2)',
                textAlign: 'left', borderRadius: 8, position: 'relative', fontFamily: 'inherit',
              }}>
              {isInJournal && <span aria-hidden="true" style={{ position: 'absolute', left: -8, top: 8, bottom: 8, width: 2.5, borderRadius: 2, background: 'var(--blue)' }}></span>}
              <span style={{ display: 'flex', opacity: isInJournal ? 1 : .7 }}>{NAV_LABELS.journal.ico}</span>
              <span style={{ fontSize: 13, fontWeight: isInJournal ? 600 : 500, letterSpacing: '-.01em', flex: 1 }}>{NAV_LABELS.journal.menu}</span>
              <span style={{ display: 'flex', transform: journalingOpen ? 'rotate(180deg)' : 'rotate(0deg)', transition: 'transform .2s var(--ease)', color: 'var(--fg-3)' }}>{Ico.chevDown}</span>
            </button>
          )}
          {showJournal && NAV_JOURNAL.map(id => <SidebarItem key={id} id={id} sub on={active === id} collapsed={collapsed} onChange={onChange}/>)}
        </div>
      </nav>

      <div style={{ padding: '4px 12px 14px', display: 'flex', flexDirection: 'column', gap: 2 }}>
        <SidebarItem id="settings" on={active === 'settings'} collapsed={collapsed} onChange={onChange}/>
      </div>
    </aside>
  );
}

// ─── Coque ordinateur : contexte partagé ──────────────────────────
// DesktopScreen fournit la route active (titre de l'en-tête) et le repli du
// menu (bouton de l'en-tête). Hors de la coque (mobile), le contexte vaut
// null : PageHeader garde sa prop title et n'affiche pas le bouton de repli.
const SIDEBAR_PREF_KEY = 'tempo:sidebar';
const DesktopShellContext = React.createContext(null);

function sidebarReadCollapsed() {
  try { return localStorage.getItem(SIDEBAR_PREF_KEY) === 'collapsed'; }
  catch (e) { return false; }
}

// ─── En-tête de page (collant, commun aux pages ordinateur) ───────
function PageHeader({ title, right, left }) {
  const shell = React.useContext(DesktopShellContext);
  const marketOpen = useMarketOpen();
  const { theme, toggle } = useTheme();
  const route = shell ? NAV_LABELS[shell.active] : null;
  const shownTitle = (route && route.titre) || title;
  const sidebarHint = shell ? (shell.collapsed ? 'Afficher le menu' : 'Masquer le menu') : '';
  const themeHint = theme === 'dark' ? 'Passer en clair' : 'Passer en sombre';
  const contact = window.LEGAL_CONTACT || 'contact@tempo-trading.com';
  const iconBtn = { background: 'transparent', border: '1px solid var(--line)', borderRadius: 8, width: 32, height: 32, flexShrink: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', color: 'var(--fg-2)' };
  // Titre de l'écran : le h1 de la page (un seul par écran). L'Accueil a déjà
  // le sien dans son bandeau (« Bonjour… ») : le titre de la barre y reste un
  // simple texte. PageHeader ne sert qu'aux variantes ordinateur ; les
  // variantes mobiles portent leur propre h1.
  const TitleTag = shell && shell.active === 'home' ? 'span' : 'h1';
  return (
    <div style={{
      // flexShrink 0 : plusieurs pages rangent leur zone .scroll en colonne
      // flex de hauteur fixe ; sans lui, l'en-tête y serait écrasé.
      position: 'sticky', top: 0, zIndex: 10, flexShrink: 0,
      height: 56, padding: shell ? '0 24px' : '0 16px',
      display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12,
      borderBottom: '1px solid var(--line)',
      background: 'var(--header-bg, rgba(255,255,255,0.86))', backdropFilter: 'blur(16px)', WebkitBackdropFilter: 'blur(16px)',
    }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 14, minWidth: 0 }}>
        {shell && (
          <React.Fragment>
            <button type="button" className="tap" onClick={shell.toggleSidebar} aria-controls="tempo-menu" aria-expanded={!shell.collapsed} title={sidebarHint} aria-label={sidebarHint} style={{ background: 'transparent', border: 'none', padding: 6, cursor: 'pointer', color: 'var(--fg-2)', display: 'flex', flexShrink: 0 }}>
              {Ico.sidebar}
            </button>
            <span aria-hidden="true" style={{ height: 16, width: 1, flexShrink: 0, background: 'var(--line)' }}></span>
          </React.Fragment>
        )}
        <TitleTag style={{ margin: 0, fontSize: 16, fontWeight: 600, letterSpacing: '-.015em', whiteSpace: 'nowrap' }}>{shownTitle}</TitleTag>
        {left}
      </div>
      <div style={{ display: 'flex', alignItems: 'center', gap: 10, flexShrink: 0 }}>
        {marketOpen ? (
          <span title="CME Globex ouvert" style={{ display: 'inline-flex', alignItems: 'center', gap: 6, padding: '4px 10px', borderRadius: 999, background: 'rgba(22,163,74,0.10)', color: 'var(--green-text)', fontSize: 12, fontWeight: 500, whiteSpace: 'nowrap' }}>
            <span style={{ width: 6, height: 6, borderRadius: '50%', background: 'var(--green)', animation: 'pulse-dot 2s var(--ease) infinite' }}></span>
            CME ouvert
          </span>
        ) : (
          <span title="CME Globex en pause — maintenance quotidienne, week-end ou jour férié" style={{ display: 'inline-flex', alignItems: 'center', gap: 6, padding: '4px 10px', borderRadius: 999, background: 'var(--bg-elev)', color: 'var(--fg-2)', fontSize: 12, fontWeight: 500, border: '1px solid var(--line)', whiteSpace: 'nowrap' }}>
            <span style={{ width: 6, height: 6, borderRadius: '50%', background: 'var(--fg-4)' }}></span>
            CME fermé
          </span>
        )}
        <button type="button" onClick={toggle} title={themeHint} aria-label={themeHint} className="tap" style={iconBtn}>{theme === 'dark' ? Ico.moon : Ico.sun}</button>
        <a href={'mailto:' + contact} title="Aide et contact" aria-label="Aide et contact" className="tap" style={{ ...iconBtn, textDecoration: 'none' }}>{Ico.help}</a>
        {right}
      </div>
    </div>
  );
}

// ─── Coque ordinateur ─────────────────────────────────────────────
// Hauteur fixe de 100dvh : seule la zone .scroll de chaque page défile, le
// menu latéral et l'en-tête restent visibles. Le repli du menu est une
// préférence de l'appareil (localStorage).
// Lien « Aller au contenu » : le focus va sur la zone de contenu sans changer
// l'adresse (les ancres #cgu, #cgv… sont lues par App).
function skipToContent(e) {
  if (e && e.preventDefault) e.preventDefault();
  const m = document.getElementById('contenu');
  if (m && typeof m.focus === 'function') m.focus();
}

function DesktopScreen({ children, active, onChange, journalingOpen, setJournalingOpen, user, displayName }) {
  const [collapsed, setCollapsed] = React.useState(sidebarReadCollapsed);
  React.useEffect(() => {
    try {
      if (collapsed) localStorage.setItem(SIDEBAR_PREF_KEY, 'collapsed');
      else localStorage.removeItem(SIDEBAR_PREF_KEY);
    } catch (e) {}
  }, [collapsed]);
  const toggleSidebar = React.useCallback(() => setCollapsed(c => !c), []);
  const shell = React.useMemo(() => ({ active, collapsed, toggleSidebar }), [active, collapsed, toggleSidebar]);
  return (
    <DesktopShellContext.Provider value={shell}>
      <div style={{ width: '100%', height: '100dvh', overflow: 'hidden', display: 'flex', background: 'var(--bg-app)' }}>
        {/* Lien d'évitement : premier élément atteint au clavier, visible
            seulement quand il a le focus (.skip-link, index.html). Il saute le
            menu et pose le focus sur la zone de contenu (#contenu). */}
        <a href="#contenu" className="skip-link" onClick={skipToContent}>Aller au contenu</a>
        <Sidebar active={active} onChange={onChange} journalingOpen={journalingOpen} setJournalingOpen={setJournalingOpen} user={user} displayName={displayName} collapsed={collapsed}/>
        <main id="contenu" tabIndex={-1} style={{ flex: 1, minWidth: 0, minHeight: 0, overflow: 'hidden', position: 'relative', background: 'var(--bg-app)', outline: 'none' }}>{children}</main>
      </div>
    </DesktopShellContext.Provider>
  );
}

// ─── Barre du bas mobile ──────────────────────────────────────────
// Barre en flux (bas du shell flex), safe-area iOS gérée via env().
// Libellés courts de NAV_LABELS. Les routes absentes de la barre (dashboard,
// daily, weekly, strategy, econ, routines, fondamental, data) sont joignables
// via le hub « Plus » : on surligne alors l'onglet Plus pour garder un état
// actif cohérent.
function BottomNav({ active, onChange }) {
  const current = NAV_BOTTOM.includes(active) ? active : 'settings';
  return (
    <nav aria-label="Navigation principale" style={{
      flexShrink: 0, zIndex: 40,
      background: 'var(--header-bg, rgba(255,255,255,0.92))',
      backdropFilter: 'blur(20px)', WebkitBackdropFilter: 'blur(20px)',
      borderTop: '1px solid var(--line)',
      paddingTop: 8,
      paddingBottom: 'calc(8px + env(safe-area-inset-bottom))',
      display: 'flex', justifyContent: 'space-around', alignItems: 'flex-start',
    }}>
      {NAV_BOTTOM.map(id => {
        const on = current === id;
        // Onglet Plus : icône « ••• » (hub de navigation), pas l'engrenage.
        const ico = id === 'settings' ? Ico.more : NAV_LABELS[id].ico;
        return (
          <button key={id} type="button" className="tap" onClick={() => onChange && onChange(id)} aria-current={on ? 'page' : undefined} style={{
            flex: '1 1 0', minWidth: 0,
            // Zone tactile : 48 px de haut au moins (l'icône et le libellé
            // restent centrés).
            background: 'transparent', border: 'none', padding: '4px 2px', minHeight: 48,
            display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 4,
            color: on ? 'var(--blue-600)' : 'var(--fg-3)', cursor: 'pointer',
            fontFamily: 'inherit',
          }}>
            {ico}
            <span style={{ fontSize: 9.5, fontWeight: 600, letterSpacing: '.04em', whiteSpace: 'nowrap' }}>{NAV_LABELS[id].court}</span>
          </button>
        );
      })}
    </nav>
  );
}

// Shell mobile : la page occupe le viewport réel (aucune barre de statut
// simulée, aucun cadre d'appareil). Colonne flex plein écran : zone de
// contenu scrollable + BottomNav en flux. Safe-areas iOS via env() —
// viewport-fit=cover est déjà posé dans index.html.
function MobileScreen({ children, active, onChange, showNav = true }) {
  // Le conteneur .scroll est un noeud DOM unique partagé entre toutes les
  // routes : on remet le scroll en haut à chaque changement d'onglet. C'est
  // aussi le repère <main> de l'app sur mobile (comme DesktopScreen).
  const scrollRef = React.useRef(null);
  React.useEffect(() => { if (scrollRef.current) scrollRef.current.scrollTop = 0; }, [active]);
  return (
    <div style={{
      position: 'fixed', top: 0, left: 0, right: 0, bottom: 0,
      display: 'flex', flexDirection: 'column',
      background: 'var(--bg-app)',
      overflow: 'hidden',
    }}>
      <main id="contenu" tabIndex={-1} className="scroll" ref={scrollRef} style={{
        flex: 1, minHeight: 0, overflowY: 'auto',
        WebkitOverflowScrolling: 'touch',
        paddingTop: 'env(safe-area-inset-top)',
        background: 'var(--bg-app)', outline: 'none',
      }}>{children}</main>
      {showNav && <BottomNav active={active} onChange={onChange}/>}
    </div>
  );
}

// ─── Modales : comportement commun (charte §8) ────────────────────
// useModalBehavior({ open, onRequestClose, isDirty, dialogRef, labelledBy })
// sert à TOUTES les modales et feuilles :
//   const modal = useModalBehavior({ open, onRequestClose: onClose, isDirty });
//   if (!open) return null;
//   <div style={voile} {...modal.backdropProps}>
//     <div {...modal.dialogProps} aria-labelledby="mon-titre" style={carte}>
//       <button type="button" aria-label="Fermer" onClick={modal.requestClose}>…</button>
//       …
//       <ModalDiscardConfirm modal={modal}/>
//     </div>
//   </div>
// - Le fond ne ferme que sur mousedown hors de la carte : une sélection de
//   texte qui se termine sur le fond ne ferme plus rien. Pas d'onClick sur le
//   fond ni de stopPropagation sur la carte. Seul le bouton principal compte
//   (comme l'ancien onClick) : un clic droit, un clic molette ou un Ctrl+clic
//   (clic secondaire sur Mac) sur le fond ne ferment rien. Un toucher émet un
//   mousedown de bouton 0 : rien ne change au doigt.
// - Échap ferme la modale du dessus seulement (pile).
// - isDirty : croix, fond, Échap et requestClose affichent d'abord
//   « Abandonner ta saisie ? » dans la modale (ModalDiscardConfirm).
// - Focus : premier champ à l'ouverture (la carte elle-même sur écran tactile,
//   pour ne pas ouvrir le clavier), piégé dans la modale, rendu au
//   déclencheur à la fermeture.
// Après un enregistrement réussi, la modale appelle directement son onClose
// (et non requestClose) : il n'y a plus rien à abandonner.
const MODAL_STACK = [];
const MODAL_FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]):not([type="hidden"]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
const MODAL_FIELDS = 'input:not([disabled]):not([type="hidden"]):not([readonly]), select:not([disabled]), textarea:not([disabled]):not([readonly])';
const MODAL_DISCARD_QUESTION = 'Abandonner ta saisie ?';

function modalIsVisible(el) { return el.getClientRects().length > 0; }

function useModalBehavior({ open, onRequestClose, isDirty = false, dialogRef, labelledBy } = {}) {
  const ownRef = React.useRef(null);
  const ref = dialogRef || ownRef;
  const refHolder = React.useRef(ref);
  refHolder.current = ref;
  const closeRef = React.useRef(onRequestClose);
  closeRef.current = onRequestClose;
  const dirtyRef = React.useRef(isDirty);
  dirtyRef.current = isDirty;
  const [confirmDiscard, setConfirmDiscard] = React.useState(false);
  const confirmRef = React.useRef(false);
  confirmRef.current = confirmDiscard;
  const focusBeforeConfirm = React.useRef(null);

  // Déclencheur mémorisé au rendu d'ouverture, avant qu'un autoFocus de la
  // modale ne déplace le focus.
  const openerRef = React.useRef(null);
  const wasOpen = React.useRef(false);
  if (open && !wasOpen.current) openerRef.current = document.activeElement;
  wasOpen.current = !!open;

  const discard = React.useCallback(() => {
    confirmRef.current = false;
    setConfirmDiscard(false);
    if (closeRef.current) closeRef.current();
  }, []);

  const requestClose = React.useCallback(() => {
    if (confirmRef.current) return; // la question est déjà affichée
    if (dirtyRef.current) {
      focusBeforeConfirm.current = document.activeElement;
      confirmRef.current = true;
      setConfirmDiscard(true);
      return;
    }
    if (closeRef.current) closeRef.current();
  }, []);

  const keepEditing = React.useCallback(() => {
    confirmRef.current = false;
    setConfirmDiscard(false);
    const back = focusBeforeConfirm.current;
    focusBeforeConfirm.current = null;
    requestAnimationFrame(() => {
      const box = refHolder.current && refHolder.current.current;
      if (!box) return;
      if (back && back.isConnected && box.contains(back)) back.focus({ preventScroll: true });
      else box.focus({ preventScroll: true });
    });
  }, []);

  React.useEffect(() => {
    if (!open) {
      confirmRef.current = false;
      setConfirmDiscard(false);
      return undefined;
    }
    const token = {};
    MODAL_STACK.push(token);
    const opener = openerRef.current;
    const box = () => refHolder.current && refHolder.current.current;
    const focusables = () => {
      const n = box();
      return n ? Array.from(n.querySelectorAll(MODAL_FOCUSABLE)).filter(modalIsVisible) : [];
    };
    const raf = requestAnimationFrame(() => {
      const n = box();
      if (!n || n.contains(document.activeElement)) return; // autoFocus de la modale respecté
      let coarse = false;
      try { coarse = window.matchMedia('(pointer: coarse)').matches; } catch (e) {}
      const field = coarse ? null : Array.from(n.querySelectorAll(MODAL_FIELDS)).find(modalIsVisible);
      const target = field || (coarse ? n : focusables()[0]) || n;
      target.focus({ preventScroll: true });
    });
    const onKey = (e) => {
      if (MODAL_STACK[MODAL_STACK.length - 1] !== token) return;
      if (e.key === 'Escape' || e.key === 'Esc') {
        if (e.defaultPrevented || e.isComposing) return;
        e.preventDefault();
        if (confirmRef.current) keepEditing();
        else requestClose();
        return;
      }
      if (e.key !== 'Tab') return;
      const n = box();
      if (!n) return;
      const list = focusables();
      const cur = document.activeElement;
      if (!list.length) { e.preventDefault(); n.focus({ preventScroll: true }); return; }
      const first = list[0];
      const last = list[list.length - 1];
      if (!n.contains(cur)) { e.preventDefault(); (e.shiftKey ? last : first).focus(); return; }
      if (e.shiftKey && (cur === first || cur === n)) { e.preventDefault(); last.focus(); }
      else if (!e.shiftKey && cur === last) { e.preventDefault(); first.focus(); }
    };
    document.addEventListener('keydown', onKey);
    return () => {
      cancelAnimationFrame(raf);
      document.removeEventListener('keydown', onKey);
      const i = MODAL_STACK.indexOf(token);
      if (i >= 0) MODAL_STACK.splice(i, 1);
      // Focus rendu au déclencheur, sauf si l'utilisateur est déjà ailleurs.
      const n = box();
      const cur = document.activeElement;
      const lost = !cur || cur === document.body || !cur.isConnected || (n && n.contains(cur));
      if (lost && opener && opener !== document.body && opener.isConnected
          && typeof opener.focus === 'function' && !(n && n.contains(opener))) {
        opener.focus({ preventScroll: true });
      }
    };
  }, [open, requestClose, keepEditing]);

  return {
    requestClose,
    backdropProps: { onMouseDown: (e) => { if (e.button === 0 && !e.ctrlKey && e.target === e.currentTarget) requestClose(); } },
    dialogProps: { ref, role: 'dialog', 'aria-modal': 'true', tabIndex: -1, 'aria-labelledby': labelledBy },
    dialogRef: ref,
    confirmDiscard,
    keepEditing,
    discard,
  };
}

// Confirmation d'abandon d'une saisie, affichée DANS la modale (jamais
// window.confirm). Ne rend rien tant que la question n'est pas posée.
function ModalDiscardConfirm({ modal, detail = 'Ce que tu as saisi ne sera pas enregistré.', style }) {
  if (!modal || !modal.confirmDiscard) return null;
  return (
    <div role="group" aria-label={MODAL_DISCARD_QUESTION} style={{
      display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 10,
      padding: '12px 14px', borderRadius: 10,
      background: 'var(--red-soft)', border: '1px solid var(--line)',
      ...style,
    }}>
      <div style={{ flex: '1 1 200px', minWidth: 0 }}>
        <div style={{ fontSize: 13.5, fontWeight: 600, color: 'var(--fg)' }}>{MODAL_DISCARD_QUESTION}</div>
        {detail && <div style={{ fontSize: 12, color: 'var(--fg-2)', marginTop: 2 }}>{detail}</div>}
      </div>
      <div style={{ display: 'flex', gap: 8, flexShrink: 0, marginLeft: 'auto' }}>
        <button type="button" autoFocus className="btn tap" onClick={modal.keepEditing} style={{ color: 'var(--fg)' }}>Continuer la saisie</button>
        <button type="button" className="btn tap" onClick={modal.discard} style={{ background: 'var(--sell-bg, #dc2626)', borderColor: 'var(--sell-bg, #dc2626)', color: '#fff' }}>Abandonner</button>
      </div>
    </div>
  );
}

// Conteneur commun des pages de contenu (ordinateur) : même largeur maximale et
// même alignement (centré) partout, pour que les cartes ne changent ni de largeur
// ni de position d'une page à l'autre. Les pages formulaires (Réglages, Données)
// gardent leur colonne de 720 px.
const PAGE_MAX_W = 1400;
const PAGE_BODY = { maxWidth: PAGE_MAX_W, width: '100%', margin: '0 auto', boxSizing: 'border-box' };

// ─── État vide partagé (charte §7) ───────────────────────────────────
// UN seul format pour tous les écrans : icône, titre gras, une phrase, une
// action principale (bleue) et, au besoin, une action secondaire.
//   icon       élément d'icône (Ico.x), agrandi à 28 px
//   primary    { label, onClick, ico } — bouton bleu
//   secondary  { label, onClick, ico } — bouton neutre
//   compact    marges réduites (mobile, intérieur d'une carte déjà titrée)
function EmptyState({ icon, title, text, primary, secondary, compact = false, style }) {
  return (
    <div style={{ padding: compact ? '28px 12px' : '44px 16px', textAlign: 'center', ...style }}>
      {icon && (
        <div aria-hidden="true" style={{ color: 'var(--fg-4)', marginBottom: 12, display: 'flex', justifyContent: 'center' }}>
          {React.cloneElement(icon, { width: 28, height: 28 })}
        </div>
      )}
      {title && <div style={{ fontSize: 15, fontWeight: 600, color: 'var(--fg)', letterSpacing: '-.01em', marginBottom: 6 }}>{title}</div>}
      {text && <div style={{ fontSize: 12.5, color: 'var(--fg-3)', maxWidth: 420, margin: '0 auto', lineHeight: 1.5 }}>{text}</div>}
      {(primary || secondary) && (
        <div style={{ display: 'flex', gap: 10, justifyContent: 'center', flexWrap: 'wrap', marginTop: 18 }}>
          {primary && (
            <button type="button" onClick={primary.onClick} className="btn btn-blue tap" style={{ color: '#fff' }}>
              {primary.ico}{primary.ico ? ' ' : ''}{primary.label}
            </button>
          )}
          {secondary && (
            <button type="button" onClick={secondary.onClick} className="btn tap" style={{ color: 'var(--fg)' }}>
              {secondary.ico}{secondary.ico ? ' ' : ''}{secondary.label}
            </button>
          )}
        </div>
      )}
    </div>
  );
}

// Compte sans aucun trade : même titre, même phrase et mêmes actions sur tous
// les écrans (Accueil, Tableau de bord, Trades, Journal quotidien, Statistiques,
// Bilan hebdo). `openImport(jour, onglet)` : fenêtre « Ajouter des trades ».
const EMPTY_TRADES_TITLE = 'Aucun trade pour l’instant';
const EMPTY_TRADES_TEXT = 'Ajoute ton premier trade ou importe un fichier de ton courtier.';
const EMPTY_DAY_TITLE = 'Aucun trade ce jour';
function EmptyTrades({ openImport, onAdd, onImport, text = EMPTY_TRADES_TEXT, compact = false, style }) {
  const add = onAdd || (openImport ? () => openImport(null, 'manual') : null);
  const imp = onImport || (openImport ? () => openImport(null, 'csv') : null);
  return (
    <EmptyState icon={Ico.trades} title={EMPTY_TRADES_TITLE} text={text} compact={compact} style={style}
      primary={add ? { label: 'Ajouter un trade', onClick: add, ico: Ico.plus } : null}
      secondary={imp ? { label: 'Importer un fichier', onClick: imp, ico: Ico.upload } : null}/>
  );
}

// ─── Commande segmentée partagée (onglets, périodes, choix exclusifs) ──
// UN seul style dans toute l'application : rail gris, option active sur fond
// de carte, texte foncé en gras ; options inactives en --fg-2 (contraste AA).
//   options   [{ id, label, ico, count, title }]
//   kind      'tabs' (role tablist, change de panneau) | 'choice' (role radiogroup)
//   size      'xs' | 'sm' | 'md'
//   idPrefix  onglets reliés à des panneaux : id « <prefix>-tab-<id> »,
//             aria-controls « <prefix>-panel-<id> »
//   stretch   les options se partagent toute la largeur
//   scroll    rail défilant horizontalement (mobile), jamais de retour à la ligne
// Clavier : flèches gauche/droite, Début, Fin (sélection et focus suivent).
// Taille tactile : sous 900 px de large, chaque option fait au moins 44 px de
// côté quelle que soit la taille demandée (classe .seg-opt, index.html) ; le
// rail grandit avec elles.
const SEGMENTED_SIZES = {
  xs: { pad: '5px 8px',  fs: 11.5, radius: 6, rail: 9 },
  sm: { pad: '6px 12px', fs: 12,   radius: 7, rail: 10 },
  md: { pad: '7px 15px', fs: 12.5, radius: 8, rail: 11 },
};
function SegmentedTabs({ options, value, onChange, kind = 'tabs', size = 'md', ariaLabel, idPrefix, stretch = false, scroll = false, disabled = false, title, style }) {
  const sz = SEGMENTED_SIZES[size] || SEGMENTED_SIZES.md;
  const railRef = React.useRef(null);
  const isTabs = kind === 'tabs';
  const hasValue = options.some(o => o.id === value);
  const onKey = (e) => {
    const k = e.key;
    if (k !== 'ArrowRight' && k !== 'ArrowLeft' && k !== 'Home' && k !== 'End') return;
    if (disabled || !options.length) return;
    e.preventDefault();
    const i = Math.max(0, options.findIndex(o => o.id === value));
    const n = options.length;
    const j = k === 'Home' ? 0 : k === 'End' ? n - 1 : (i + (k === 'ArrowRight' ? 1 : n - 1)) % n;
    onChange(options[j].id);
    const btns = railRef.current ? railRef.current.querySelectorAll('button') : [];
    if (btns[j]) btns[j].focus();
  };
  return (
    <div ref={railRef} role={isTabs ? 'tablist' : 'radiogroup'} aria-label={ariaLabel} title={title} onKeyDown={onKey}
      className={scroll ? 'scroll' : undefined}
      style={{
        display: stretch || scroll ? 'flex' : 'inline-flex', gap: 3, padding: 3, boxSizing: 'border-box',
        background: 'var(--bg-elev)', border: '1px solid var(--line)', borderRadius: sz.rail,
        maxWidth: '100%', overflowX: scroll ? 'auto' : undefined, scrollbarWidth: scroll ? 'none' : undefined,
        opacity: disabled ? 0.6 : 1, ...style,
      }}>
      {options.map((o, i) => {
        const on = o.id === value;
        return (
          <button key={o.id} type="button" role={isTabs ? 'tab' : 'radio'}
            aria-selected={isTabs ? on : undefined} aria-checked={isTabs ? undefined : on}
            id={idPrefix ? idPrefix + '-tab-' + o.id : undefined}
            aria-controls={idPrefix && isTabs ? idPrefix + '-panel-' + o.id : undefined}
            tabIndex={on || (!hasValue && i === 0) ? 0 : -1}
            disabled={disabled} title={o.title}
            onClick={() => { if (!disabled) onChange(o.id); }} className="tap seg-opt" style={{
              flex: stretch ? 1 : undefined, flexShrink: scroll ? 0 : undefined,
              padding: sz.pad, borderRadius: sz.radius, border: 'none', cursor: disabled ? 'default' : 'pointer',
              background: on ? 'var(--bg-card)' : 'transparent',
              color: on ? 'var(--fg)' : 'var(--fg-2)',
              fontSize: sz.fs, fontWeight: on ? 600 : 500, letterSpacing: '-.005em', fontFamily: 'inherit',
              boxShadow: on ? '0 1px 3px rgba(0,0,0,0.10)' : 'none',
              display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: 7, whiteSpace: 'nowrap',
            }}>
            {o.ico}{o.label}
            {o.count != null && o.count > 0 && (
              <span className="num" style={{ background: on ? 'var(--bg-elev)' : 'var(--bg-card)', color: 'var(--fg-2)', borderRadius: 999, padding: '1px 7px', fontSize: 10, fontWeight: 600 }}>{o.count}</span>
            )}
          </button>
        );
      })}
    </div>
  );
}

// ─── Panneaux des onglets inactifs ────────────────────────────────
// Chaque onglet de SegmentedTabs (idPrefix) désigne son panneau par
// aria-controls. Quand un écran ne dessine que le panneau de l'onglet choisi,
// <TabPanelStubs idPrefix options value/> pose à côté les panneaux des autres
// onglets, vides et masqués (hidden) : aucune référence ne pointe dans le vide
// (même convention que la fenêtre « Ajouter des trades », où les trois
// panneaux existent et un seul est visible).
function TabPanelStubs({ idPrefix, options, value }) {
  if (!idPrefix || !Array.isArray(options)) return null;
  return options.filter(o => o && o.id !== value).map(o => (
    <div key={o.id} role="tabpanel" id={idPrefix + '-panel-' + o.id} aria-labelledby={idPrefix + '-tab-' + o.id} hidden></div>
  ));
}

// ─── Titre de carte ───────────────────────────────────────────────
// <CardTitle>Agenda</CardTitle> : titre de section d'un écran, toujours un h2
// (le h1 est le titre de l'écran : PageHeader sur ordinateur, titre de page sur
// mobile). Mêmes mesures que les anciens titres en <span> ; `style` les
// surcharge au besoin (taille, couleur, marge), `id` sert aux aria-labelledby.
function CardTitle({ children, style, id, className }) {
  return <h2 id={id} className={className} style={{ margin: 0, fontSize: 15, fontWeight: 600, letterSpacing: '-.01em', ...style }}>{children}</h2>;
}

Object.assign(window, {
  CardTitle, TabPanelStubs,
  EmptyState, EmptyTrades, EMPTY_TRADES_TITLE, EMPTY_TRADES_TEXT, EMPTY_DAY_TITLE, SegmentedTabs,
  PAGE_MAX_W, PAGE_BODY,
  Ico, NAV_LABELS, NAV_MAIN, NAV_JOURNAL, NAV_BOTTOM,
  Sidebar, PageHeader, DesktopScreen, DesktopShellContext, BottomNav, MobileScreen,
  useModalBehavior, ModalDiscardConfirm,
  useTheme, setAppTheme, applyTheme, useMarketOpen,
});
