// gp-app.jsx — root: state, direction presets, tweaks, routing
// ───────────────────────────────────────────────────────────────
const {
  useTweaks, TweaksPanel, TweakSection, TweakRadio, TweakToggle, TweakColor,
  Sidebar: AppSidebar, Topbar: AppTopbar,
  Dashboard: AppDashboard, EstimatorModule: AppEstimator,
  MursModule: AppMurs, FenestrationModule: AppFen, Mesure: AppMesure,
  ProjetModule: AppProjet, ConfigModule: AppConfig,
  RecettesModule: AppRecettes, SoumissionModule: AppSoumission,
  OrganisationModule: AppOrg, SystemesModule: AppSystemes, ServicesModule: AppServices, CourrielsModule: AppCourriels, DEFAULT_ORG: aDefaultOrg, DEFAULT_RECIPE_CHOICES: aDefaultChoices,
  WidgetBuilder: AppBuilder, BUILDER_WIDGETS: aBuilderWidgets, seedWidgetsFromLegacy: aSeedWidgets,
  UtilisateursModule: AppUtilisateurs, ProjetFormModule: AppProjetForm,
  PRODUCT_SYSTEMS: aProductSystems,
  CategoryView: AppCategory, ModulePager,
  moduleById: aModById, PLAN_MEASURES, getSpec: aGetSpec, CUSTOM_MODULES: aCustom, ESTIMATION_MODULES: aEstMods,
  validateRegistry: aValidate, loadState: aLoad, saveState: aSave,
  Portfolio: AppPortfolio, PROJECTS: aProjects, projectById: aProjectById,
  LoginScreen: AppLogin, AwaitingScreen: AppAwaiting, ActivityModule: AppActivity, TeamModule: AppTeam,
  ACTIVITY_LOG: aActivityLog, moduleById: aModById2,
  USER_PERMS: aUserPerms0, hasCap: aHasCap, userById: aUserById,
  MODULE_GROUPS: aGroups, KnowledgeBase: AppKnowledgeBase, Icon: MAppIcon,
  GPServices: aServices, defaultModuleConfig: aDefModCfg, emptyProjectData: aEmptyProj,
} = window;

// Projet D1 (API) → forme attendue par l'app (portefeuille, topbar, dashboard).
// Les champs riches (avancement, montant…) seront dérivés des données réelles plus tard.
const STATUT_FROM = { active: 'En cours', submission: 'Soumission', done: 'Complété', archived: 'Archivé' };
function mapProject(p) {
  return {
    id: p.id, no: p.no || '', nom: p.name || 'Projet', adresse: p.address || '', ville: p.ville || '',
    type: p.type || '', statut: STATUT_FROM[p.status] || 'En cours', storeId: p.store_id,
    client: p.client || '', tel: p.phone || '', cible: p.cible || '', notes: p.notes || '',
    date: '', avancement: 0, montant: '—', modulesFaits: 0, estimateur: '',
  };
}

// ── Helpers : initialiser byProject depuis un état sauvegardé ─────
function initByProject(saved) {
  // v4+ : le champ byProject est déjà présent (migration faite par gp-core)
  if (saved && saved.byProject) return saved.byProject;
  // Fallback bootstrapping pour un état neuf ou migration manquée
  const pid = (saved && saved.activeProjectId) || aProjects[0].id;
  const proj = aEmptyProj();
  if (aDefaultChoices) {
    Object.entries(aDefaultChoices).forEach(([mid, ch]) => {
      proj.moduleConfig[mid] = { ...aDefModCfg(mid), recipe: ch.recipe, brand: ch.brand };
    });
  }
  // Projet vierge par défaut : aucune mesure de démo (le relevé se fait sur plan).
  proj.measures = (saved && saved.measures) || [];
  return { [pid]: proj };
}

// Each "direction" is a coherent preset; tweaks fine-tune from there.
const DIR_PRESET = {
  a: { theme: 'light', accent: '#2f6bed', type: 'technique' },
  b: { theme: 'light', accent: '#c2603a', type: 'humaniste' }
};

const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "direction": "a",
  "theme": "light",
  "accent": "#2f6bed",
  "type": "technique",
  "density": "regular",
  "nav": "expanded",
  "summary": "lateral"
} /*EDITMODE-END*/;

function App() {
  const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);

  // Session : utilisateur connecté (null → écran de connexion)
  const [user, setUser] = React.useState(() => {
    const saved = aLoad && aLoad();
    return (saved && saved.user) || null;
  });
  const [active, setActive] = React.useState('dashboard');
  const [unit, setUnit] = React.useState('imp');
  const [mesureOpen, setMesureOpen] = React.useState(false);

  // Le serveur fait autorité (prod) : au chargement, GET /me rapatrie rôle/statut/modules
  // à jour et remplace la copie en cache. Un simple refresh reflète donc tout changement
  // d'accès ; une session révoquée/expirée entraîne la déconnexion. No-op en mode démo.
  React.useEffect(() => {
    if (!window.GPTOAuth || !window.GPTOAuth.enabled) return;
    window.GPTOAuth.me().then(({ data }) => {
      if (data && data.user) setUser(data.user);
      else if (user) setUser(null);
    }).catch(() => {});
  }, []);

  // ── Conteneur principal par-projet ────────────────────────────
  // byProject[pid] = { measures: [...], moduleConfig: { [mid]: {...} } }
  const [byProject, setByProject] = React.useState(() => {
    const saved = aLoad && aLoad();
    return initByProject(saved);
  });

  const [savedAt, setSavedAt] = React.useState(null);
  const [activity, setActivity] = React.useState(aActivityLog);
  const [userPerms, setUserPerms] = React.useState(() => {
    const saved = aLoad && aLoad();
    return (saved && saved.userPerms) || aUserPerms0;
  });
  // Permissions effectives : en prod, les overrides du compte connecté viennent du serveur
  // (user.perms) et pilotent son propre menu ; sinon la map locale (démo).
  const effPerms = React.useMemo(
    () => (user && user.perms) ? { ...userPerms, [user.id]: user.perms } : userPerms,
    [user, userPerms]);
  // Gating des modules d'estimation : en prod, un estimateur ne voit que ses modules autorisés.
  const EST_IDS = React.useMemo(() => new Set((aEstMods || []).map((m) => m.id)), []);
  const modAllowed = React.useCallback((mid) => {
    const gate = window.GPTOAuth && window.GPTOAuth.enabled && user && user.role === 'user';
    if (!gate || !EST_IDS.has(mid)) return true;
    return !!(user.modules && user.modules[mid]);
  }, [user, EST_IDS]);
  const [org, setOrg] = React.useState(() => {
    const saved = aLoad && aLoad();
    return (saved && saved.org) || aDefaultOrg;
  });
  const [systems, setSystems] = React.useState(() => {
    const saved = aLoad && aLoad();
    return (saved && saved.systems) || aProductSystems.map((s) => ({ ...s, picks: { ...s.picks } }));
  });
  const [widgets, setWidgets] = React.useState(() => {
    const saved = aLoad && aLoad();
    if (saved && saved.widgets) return saved.widgets;
    // Premier lancement : amorcer depuis les modules + RECIPES existants.
    return aSeedWidgets ? aSeedWidgets() : (aBuilderWidgets ? [...aBuilderWidgets] : []);
  });
  // Mode production : les projets viennent de D1 ; démo : mock local.
  const prodMode = !!(window.GPTOAuth && window.GPTOAuth.enabled);
  const [projects, setProjects] = React.useState(() => prodMode ? [] : aProjects);
  const [projectsLoaded, setProjectsLoaded] = React.useState(!prodMode);
  const [stores, setStores] = React.useState([]);
  const [newProjOpen, setNewProjOpen] = React.useState(false);
  const [projFields, setProjFields] = React.useState(null);   // config du formulaire projet (prod)
  const NP_EMPTY = { storeId: '' };
  const [npForm, setNpForm] = React.useState(NP_EMPTY);
  const [activeProjectId, setActiveProjectId] = React.useState(() => {
    if (prodMode) return null;
    const saved = aLoad && aLoad();
    return (saved && saved.activeProjectId) || aProjects[0].id;
  });

  const project = prodMode ? (projects.find((p) => p.id === activeProjectId) || null) : aProjectById(activeProjectId);
  const [mesureCtx, setMesureCtx] = React.useState(null);

  // Charge la liste des projets depuis D1 (prod). preferId = projet à ouvrir après création.
  const loadProjects = React.useCallback((preferId) => {
    if (!prodMode) return Promise.resolve();
    return window.GPTOAuth.call('/api/projects', null, 'GET').then(({ ok, data }) => {
      if (!ok) { setProjectsLoaded(true); return; }
      const mapped = (data.projects || []).map(mapProject);
      setProjects(mapped);
      setProjectsLoaded(true);
      setActiveProjectId((prev) => {
        if (preferId && mapped.some((p) => p.id === preferId)) return preferId;
        if (mapped.some((p) => p.id === prev)) return prev;
        return mapped[0] ? mapped[0].id : null;
      });
      // Sans aucun projet, on atterrit sur le module « Projet » (et non un dashboard vide).
      if (!mapped.length) setActive('projet');
    }).catch(() => setProjectsLoaded(true));
  }, [prodMode]);

  // Au login (compte actif) : charger les projets + les magasins (pour le super).
  React.useEffect(() => {
    if (!prodMode || !user || (user.status && user.status !== 'active')) return;
    loadProjects();
    window.GPTOAuth.call('/api/auth/form', null, 'GET').then(({ ok, data }) => { if (ok) setProjFields(data.config.fields); });
    if (user.role === 'super_admin') window.GPTOAuth.call('/api/auth/stores', null, 'GET').then(({ ok, data }) => { if (ok) setStores(data.stores || []); });
  }, [user, loadProjects]);

  // Champs intégrés → clés d'API (les autres gardent leur clé).
  const FIELD_API = { nom: 'name', tel: 'phone', adresse: 'address' };
  const npValid = () => {
    const flds = projFields || [];
    if (user.role === 'super_admin' && !npForm.storeId) return false;
    return flds.every((f) => !(f.enabled && f.required) || String(npForm[f.key] || '').trim());
  };
  const createProject = () => {
    if (!npValid()) return;
    const body = { custom: {} };
    (projFields || []).forEach((f) => {
      if (!f.enabled) return;
      const v = String(npForm[f.key] || '').trim();
      if (f.builtin) body[FIELD_API[f.key] || f.key] = v; else body.custom[f.key] = v;
    });
    if (user.role === 'super_admin') body.storeId = npForm.storeId;
    window.GPTOAuth.call('/api/projects/create', body).then(({ ok, data }) => {
      if (ok) { setNewProjOpen(false); setNpForm(NP_EMPTY); loadProjects(data.id); setActive('dashboard'); }
    });
  };

  // ── Sélecteurs dérivés du projet courant ──────────────────────
  // Projet vierge par défaut : aucune mesure de démo (en prod comme en projet neuf).
  const projectData = byProject[activeProjectId] || { measures: prodMode ? [] : PLAN_MEASURES, moduleConfig: {} };
  const measures = projectData.measures;
  const moduleConfig = projectData.moduleConfig;

  // Compatibilité descendante : recipeChoices au sens ancien (recipe + brand + excl)
  const recipeChoices = React.useMemo(() => {
    const out = {};
    Object.entries(moduleConfig).forEach(([mid, cfg]) => {
      out[mid] = { recipe: cfg.recipe, brand: cfg.brand, excl: cfg.excl || {} };
    });
    return out;
  }, [moduleConfig]);

  // ── Injecter measureService avec accès au state ───────────────
  React.useEffect(() => {
    if (aServices && aServices.measures) {
      aServices.measures._bind(
        () => ({ byProject }),
        (updater) => setByProject(updater)
      );
    }
  }, [byProject]);

  // ── Pont take-off : le moteur GPM_ injecte ses mesures ici ────
  // Reçoit [{kind,val,unit,name,apps}] depuis gp-takeoff.js et les écrit
  // dans le projet actif, avec la cible définie à l'ouverture (__GPTO_takeoffTarget).
  React.useEffect(() => {
    window.GPTO_takeoffInject = (items) => {
      if (!items || !items.length) return 0;
      const fallback = window.__GPTO_takeoffTarget || 'toiture';
      const stamp = Date.now();
      setByProject((prev) => {
        const proj = prev[activeProjectId] || aEmptyProj();
        const added = items.map((it, i) => ({
          id: 'me' + stamp + i,
          name: it.name || `Forme ${i + 1}`,
          kind: it.kind, val: it.val, unit: it.unit,
          target: it.target || fallback, // ventilation → module dédié ; sinon module d'ouverture
          page: 1, source: 'plan',
        }));
        return { ...prev, [activeProjectId]: { ...proj, measures: [...proj.measures, ...added] } };
      });
      logAction('mesure', 'a injecté des relevés de plan', `${items.length} forme${items.length > 1 ? 's' : ''}`);
      return items.length;
    };
    return () => { try { delete window.GPTO_takeoffInject; } catch (_) {} };
  }, [activeProjectId]);

  // Validation du registre au démarrage
  React.useEffect(() => { aValidate && aValidate(); }, []);

  // ── Frontière de persistance : sauvegarde à chaque changement ──
  React.useEffect(() => {
    if (!aSave) return;
    const ts = aSave({ byProject, activeProjectId, user, userPerms, org, systems, widgets });
    if (ts) setSavedAt(ts);
  }, [byProject, activeProjectId, user, userPerms, org, systems, widgets]);

  // ── Mutateur bas niveau : patch du sous-arbre byProject[pid] ──
  const patchProject = React.useCallback((pid, patcher) => {
    setByProject(prev => {
      const cur = prev[pid] || aEmptyProj();
      return { ...prev, [pid]: patcher(cur) };
    });
  }, []);

  // ── Mesures du projet courant ─────────────────────────────────
  const setMeasures = React.useCallback((updater) => {
    patchProject(activeProjectId, (proj) => ({
      ...proj,
      measures: typeof updater === 'function' ? updater(proj.measures) : updater,
    }));
  }, [activeProjectId, patchProject]);

  // ── Configuration d'un module pour le projet courant ──────────
  const patchModuleConfig = React.useCallback((moduleId, changes) => {
    patchProject(activeProjectId, (proj) => {
      const cur = proj.moduleConfig[moduleId] || aDefModCfg(moduleId);
      return {
        ...proj,
        moduleConfig: {
          ...proj.moduleConfig,
          [moduleId]: { ...cur, ...changes },
        },
      };
    });
  }, [activeProjectId, patchProject]);

  const chooseRecipe = (moduleId, recipe, brand) => {
    patchProject(activeProjectId, (proj) => {
      const cur = proj.moduleConfig[moduleId] || aDefModCfg(moduleId);
      const excl = cur.recipe === recipe ? cur.excl || {} : {};
      return {
        ...proj,
        moduleConfig: {
          ...proj.moduleConfig,
          [moduleId]: { ...cur, recipe, brand, excl },
        },
      };
    });
  };

  const toggleExclude = (moduleId, sku) => {
    patchProject(activeProjectId, (proj) => {
      const cur = proj.moduleConfig[moduleId] || aDefModCfg(moduleId);
      const excl = { ...(cur.excl || {}), [sku]: !cur.excl?.[sku] };
      return {
        ...proj,
        moduleConfig: { ...proj.moduleConfig, [moduleId]: { ...cur, excl } },
      };
    });
    logAction('recipe', 'a ajusté un composant de recette', sku);
  };

  const applySystemAll = (sys) => {
    patchProject(activeProjectId, (proj) => {
      const nextConfig = { ...proj.moduleConfig };
      Object.entries(sys.picks).forEach(([mid, p]) => {
        const cur = nextConfig[mid] || aDefModCfg(mid);
        nextConfig[mid] = { ...cur, recipe: p.recipe, brand: p.brand, excl: {} };
      });
      return { ...proj, moduleConfig: nextConfig };
    });
    logAction('recipe', `a appliqué le système ${sys.name}`, `${Object.keys(sys.picks).length} modules`);
  };

  const login = (u) => {
    setUser(u);
    setActivity((a) => [{ id: 'a' + Date.now(), userId: u.id, type: 'login', action: 's\'est connecté(e)', cible: '', projet: '', ts: new Date().toISOString().slice(0, 16).replace('T', ' ') }, ...a]);
    setActive('dashboard');
  };
  const logout = () => { setUser(null); setActive('dashboard'); };

  const [toast, setToast] = React.useState(null);
  const toastT = React.useRef(null);
  const logAction = React.useCallback((type, action, cible = '', projet = '') => {
    setUser((u) => {
      if (u) setActivity((a) => [{ id: 'a' + Date.now(), userId: u.id, type, action, cible, projet,
        ts: new Date().toISOString().slice(0, 16).replace('T', ' ') }, ...a]);
      return u;
    });
    setToast(`${action}${cible ? ' — ' + cible : ''}`);
    clearTimeout(toastT.current);
    toastT.current = setTimeout(() => setToast(null), 2800);
  }, []);

  const setUserPermFor = (userId, cap, value) => {
    setUserPerms((prev) => {
      const next = { ...prev, [userId]: { ...(prev[userId] || {}) } };
      if (value) next[userId][cap] = true; else delete next[userId][cap];
      return next;
    });
    const tgt = aUserById && aUserById(userId);
    logAction('user', value ? 'a accordé une permission' : 'a retiré une permission',
      `${cap} — ${tgt ? tgt.prenom + ' ' + tgt.nom : userId}`);
  };

  const mod = aModById(active) || aModById('dashboard');
  const isCat = typeof active === 'string' && active.startsWith('cat:');
  const catName = isCat ? active.slice(4) : null;

  const openMesure = (ctx) => {
    // Cible par défaut des mesures injectées = module d'où l'on ouvre le take-off.
    window.__GPTO_takeoffTarget = ctx || 'toiture';
    if (typeof window.GPM_open === 'function') { window.GPM_open(); return; }
    // Repli (environnement sans moteur GPM_) : ancien overlay React.
    setMesureCtx(ctx || null); setMesureOpen(true);
  };

  const onNav = (id) => {
    if (id === 'mesure') { openMesure(null); return; }
    setActive(id);
    setMesureOpen(false);
    window.scrollTo({ top: 0 });
  };

  const setDirection = (dir) => setTweak({ direction: dir, ...DIR_PRESET[dir] });

  // ── Séquence de navigation à plat (hooks AVANT tout retour anticipé : les
  //    règles des Hooks exigent un nombre d'appels constant entre les renders,
  //    y compris la transition login → connecté). Garde sur user === null.
  const navSeq = React.useMemo(() => {
    if (!user) return [];
    const seq = [];
    aGroups.forEach((g) => g.items.forEach((m) => {
      if (m.tool) return;
      if (m.cap && !aHasCap(user, m.cap, effPerms)) return;
      if (m.superOnly && user.role !== 'super_admin') return;
      if (!modAllowed(m.id)) return;
      if (org.hidden && org.hidden[m.id]) return;
      seq.push({ id: m.id, name: m.name, cat: m.cat, icon: m.icon });
    }));
    return seq;
  }, [user, effPerms, org.hidden, modAllowed]);

  const navPrevNext = React.useMemo(() => {
    const cur = navSeq.findIndex((m) => m.id === active);
    return { cur, prev: cur > 0 ? navSeq[cur - 1] : null, next: cur >= 0 && cur < navSeq.length - 1 ? navSeq[cur + 1] : null };
  }, [navSeq, active]);

  // Garde d'authentification — APRÈS tous les hooks.
  if (!user) {
    return (
      <div className="approot" data-direction={t.direction} data-theme={t.theme}
        data-type={t.type} style={{ '--accent': t.accent, '--accent-ink': '#ffffff' }}>
        <AppLogin onLogin={login} />
      </div>
    );
  }
  // Accès sur autorisation : compte connecté mais pas encore approuvé (ou désactivé).
  // En mode démo, les profils n'ont pas de `status` → accès normal.
  if (user.status && user.status !== 'active') {
    return (
      <div className="approot" data-direction={t.direction} data-theme={t.theme}
        data-type={t.type} style={{ '--accent': t.accent, '--accent-ink': '#ffffff' }}>
        <AppAwaiting user={user} onLogout={logout} />
      </div>
    );
  }

  // ── Modale « Nouveau projet » (prod) — partagée par l'état vide et le portefeuille ──
  // Formulaire « Nouveau projet » — affiché DANS la zone de contenu (pas une modale).
  const F = (k, v) => setNpForm({ ...npForm, [k]: v });
  const newProjectForm = (
    <div className="fade-in">
      <div className="mhead" style={{ paddingBottom: 18 }}>
        <div className="mhead-ic"><MAppIcon n="clipboard" /></div>
        <div className="mhead-tx">
          <div className="mhead-tt">Nouveau projet</div>
          <div className="mhead-sub">Renseignez les informations du projet. Vous pourrez les modifier ensuite.</div>
        </div>
      </div>
      <div className="card" style={{ maxWidth: 760 }}>
        <div className="fgrid">
          {user.role === 'super_admin' &&
            <div className="fcell full"><label className="lbl">Magasin *</label>
              <select className="field" value={npForm.storeId} onChange={(e) => F('storeId', e.target.value)}>
                <option value="">— Choisir un magasin —</option>{stores.map((s) => <option key={s.id} value={s.id}>{s.name}</option>)}
              </select></div>}
          {(projFields || []).filter((f) => f.enabled).map((f) => (
            <div className={'fcell' + (f.full || f.type === 'textarea' ? ' full' : '')} key={f.key}>
              <label className="lbl">{f.label}{f.required ? ' *' : ''}</label>
              {f.type === 'textarea'
                ? <textarea className="field" rows={3} value={npForm[f.key] || ''} onChange={(e) => F(f.key, e.target.value)} />
                : <input className="field" type={f.type === 'number' ? 'number' : 'text'} value={npForm[f.key] || ''} onChange={(e) => F(f.key, e.target.value)} autoFocus={f.key === 'nom'} />}
            </div>
          ))}
        </div>
        <div style={{ display: 'flex', gap: 8, marginTop: 18 }}>
          <button className="btn" onClick={() => { setNewProjOpen(false); setNpForm(NP_EMPTY); }}>Annuler</button>
          <button className="btn accent" disabled={!npValid()} onClick={createProject}>Créer le projet</button>
        </div>
      </div>
    </div>
  );

  // Chargement des projets (prod) — plein écran bref, uniquement le temps du fetch.
  if (prodMode && !projectsLoaded) {
    return <div className="approot" data-theme={t.theme} data-type={t.type} style={{ '--accent': t.accent, '--accent-ink': '#ffffff' }}>
      <div className="empty tickbox" style={{ margin: '80px auto', maxWidth: 360 }}><div className="empty-s">Chargement de vos projets…</div></div>
    </div>;
  }

  // Vues qui n'ont PAS besoin d'un projet actif (réglages, admin, portefeuille).
  const PROJECT_FREE = ['portfolio', 'recettes', 'organisation', 'systemes', 'services', 'historique', 'equipe', 'courriels', 'builder'];
  // Carte « aucun projet » affichée DANS l'app (barre latérale conservée), pas en plein écran.
  const emptyProjectView = (
    <div className="fade-in">
      <div className="empty tickbox" style={{ margin: '48px auto', maxWidth: 460 }}>
        <div className="empty-t">Aucun projet</div>
        <div className="empty-s">Vous n'avez pas encore de projet dans votre magasin. Créez-en un pour commencer un relevé.</div>
        <button className="btn accent" style={{ margin: '16px auto 0' }} onClick={() => setNewProjOpen(true)}>＋ Nouveau projet</button>
      </div>
    </div>
  );

  const switchProject = (id) => {
    // Initialiser les données du projet s'il est nouveau
    setByProject(prev => {
      if (prev[id]) return prev;
      const proj = aEmptyProj();
      if (aDefaultChoices) {
        Object.entries(aDefaultChoices).forEach(([mid, ch]) => {
          proj.moduleConfig[mid] = { ...aDefModCfg(mid), recipe: ch.recipe, brand: ch.brand };
        });
      }
      return { ...prev, [id]: proj };
    });
    setActiveProjectId(id);
    window.scrollTo({ top: 0 });
  };

  const renderModule = () => {
    if (prodMode && newProjOpen) return newProjectForm;   // formulaire inline (pas de modale)
    if (isCat) return <AppCategory cat={catName} onNav={onNav} org={org} />;
    // Pas de projet actif (prod) : les vues qui en dépendent affichent l'invite de création,
    // à l'intérieur de l'app (barre latérale conservée) — pas de plein écran.
    if (prodMode && !project && !PROJECT_FREE.includes(active)) return emptyProjectView;
    if ((mod.cap && !aHasCap(user, mod.cap, effPerms)) || (mod.superOnly && user.role !== 'super_admin') || !modAllowed(active)) {
      return (
        <div className="fade-in access-denied">
          <div className="empty tickbox" style={{ maxWidth: 480, margin: '40px auto' }}>
            <div className="empty-t">Accès restreint</div>
            <div className="empty-s">La section « {mod.name} » est réservée aux administrateurs. Contactez un administrateur pour obtenir les permissions.</div>
            <button className="btn" style={{ margin: '14px auto 0' }} onClick={() => onNav('dashboard')}>Retour au tableau de bord</button>
          </div>
        </div>
      );
    }
    switch (active) {
      case 'portfolio': return <AppPortfolio projects={projects} activeId={activeProjectId} onOpen={(id) => { switchProject(id); onNav('dashboard'); }} onNew={prodMode ? () => setNewProjOpen(true) : undefined} />;
      case 'dashboard': return <AppDashboard onNav={onNav} measures={measures} project={project} org={org} />;
      case 'projet':    return <AppProjet project={project} onLog={logAction} />;
      case 'config':    return <AppConfig onLog={logAction} />;
      case 'recettes':  return <AppRecettes onLog={logAction} systems={systems} />;
      case 'organisation': return <AppOrg org={org} onChange={setOrg} onLog={logAction} />;
      case 'builder': return <AppBuilder widgets={widgets} onChange={setWidgets} onLog={logAction} org={org} />;
      case 'courriels': return <AppCourriels user={user} onLog={logAction} />;
      case 'formprojet': return <AppProjetForm user={user} onLog={logAction} />;
      case 'systemes':  return <AppSystemes systems={systems} onChange={setSystems} onLog={logAction} />;
      case 'services':  return <AppServices onLog={logAction} />;
      case 'historique': return <AppActivity user={user} activity={activity} />;
      case 'equipe':    return <AppUtilisateurs user={user} onLog={logAction} />;
      case 'soumission': return <AppSoumission
        measures={measures}
        project={project}
        onLog={logAction}
        recipeChoices={recipeChoices}
        onChoose={chooseRecipe}
        onToggleExclude={toggleExclude}
        systems={systems} />;
      case 'murs': return <AppMurs unit={unit} onMesure={() => openMesure('murs')} measures={measures} />;
      case 'fenestration': return <AppFen unit={unit} onMesure={() => openMesure('fenestration')} />;
      default: {
        const cfg = moduleConfig[active] || aDefModCfg(active);
        return <AppEstimator
          mod={mod}
          spec={aGetSpec(active)}
          unit={unit}
          onMesure={() => openMesure(mod.id)}
          measures={measures}
          moduleConfig={cfg}
          onPatchConfig={(changes) => patchModuleConfig(active, changes)}
          recipeChoices={recipeChoices}
          onChooseRecipe={chooseRecipe}
          onToggleExclude={toggleExclude}
          onApplySystem={applySystemAll}
          systems={systems} />;
      }
    }
  };

  return (
    <div
      className="approot"
      style={{ height: '100%', '--accent': t.accent, '--accent-ink': '#ffffff' }}
      data-direction={t.direction}
      data-theme={t.theme}
      data-density={t.density}
      data-type={t.type}
      data-nav={t.nav}
      data-summary={t.summary}>

      <div className="app">
        <AppSidebar activeId={mesureOpen ? 'mesure' : active} onNav={onNav} savedAt={savedAt}
          project={project} projects={projects} onSwitchProject={switchProject}
          user={user} onLogout={logout} userPerms={effPerms} hidden={org.hidden || {}} org={org} />
        <main className="ws">
          <AppTopbar
            crumbCat={isCat ? null : mod.cat} crumbName={isCat ? catName : mod.name}
            isHome={active === 'dashboard'} isCat={isCat}
            onHome={() => onNav('dashboard')}
            onCrumbCat={(c) => onNav('cat:' + c)}
            project={project} projects={projects}
            onSwitchProject={(id) => { switchProject(id); onNav('dashboard'); }}
            onPortfolio={() => onNav('portfolio')}
            direction={t.direction} onDirection={setDirection}
            theme={t.theme} onTheme={(v) => setTweak('theme', v)}
            unit={unit} onUnit={setUnit}
            onMesure={() => openMesure(!isCat && active !== 'dashboard' && !mod.system ? active : null)} />

          <div className="ws-inner">
            {!isCat && navPrevNext.cur >= 0 &&
              <ModulePager prev={navPrevNext.prev} cur={navSeq[navPrevNext.cur]} next={navPrevNext.next} onNav={onNav} />}
            {renderModule()}
          </div>
        </main>
      </div>

      {mesureOpen &&
        <AppMesure
          onClose={() => setMesureOpen(false)}
          measures={measures}
          setMeasures={setMeasures}
          ctx={mesureCtx}
          projectId={activeProjectId}
          projectName={project && project.nom}
          onExport={() => aServices && aServices.measures.exportJSON(activeProjectId, project && project.nom)}
          onImport={(file) => aServices && aServices.measures.importJSON(file).then(imported => {
            setMeasures(imported);
            logAction('mesure', 'a importé des relevés de plan', `${imported.length} formes`);
          }).catch(err => alert(err.message))} />
      }

      {AppKnowledgeBase && <AppKnowledgeBase />}

      {toast &&
        <div className="audit-toast" role="status">
          <span className="audit-toast-dot" />
          <span className="audit-toast-tx"><b>Journalisé</b> · {toast}</span>
          <button className="audit-toast-link" onClick={() => { setToast(null); onNav('historique'); }}>Voir</button>
        </div>}

      <TweaksPanel title="Tweaks">
        <TweakSection label="Direction" />
        <TweakRadio label="Préréglage" value={t.direction}
          options={[{ value: 'a', label: 'Atelier' }, { value: 'b', label: 'Chantier' }]}
          onChange={setDirection} />

        <TweakSection label="Apparence" />
        <TweakColor label="Couleur d'accent" value={t.accent}
          options={['#2f6bed', '#c2603a', '#1f8a5b', '#7b59d6']}
          onChange={(v) => setTweak('accent', v)} />
        <TweakToggle label="Thème sombre" value={t.theme === 'dark'}
          onChange={(v) => setTweak('theme', v ? 'dark' : 'light')} />
        <TweakRadio label="Typographie" value={t.type}
          options={[{ value: 'technique', label: 'Technique' }, { value: 'humaniste', label: 'Humaniste' }, { value: 'neutre', label: 'Neutre' }]}
          onChange={(v) => setTweak('type', v)} />
        <TweakRadio label="Densité" value={t.density}
          options={[{ value: 'compact', label: 'Compact' }, { value: 'regular', label: 'Standard' }, { value: 'airy', label: 'Aéré' }]}
          onChange={(v) => setTweak('density', v)} />

        <TweakSection label="Disposition" />
        <TweakRadio label="Navigation" value={t.nav}
          options={[{ value: 'expanded', label: 'Étendue' }, { value: 'rail', label: 'Icônes' }]}
          onChange={(v) => setTweak('nav', v)} />
        <TweakRadio label="Sommaire" value={t.summary}
          options={[{ value: 'lateral', label: 'Latéral' }, { value: 'inline', label: 'En ligne' }]}
          onChange={(v) => setTweak('summary', v)} />
      </TweaksPanel>
    </div>
  );
}

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