/* ============================================================
   GSE — Application (version VPS, parle à l'API du serveur)
   Les données sont stockées sur le serveur (gse.db).
   ============================================================ */
const { useState, useEffect, useCallback, useRef } = React;

const TYPES = ["Ticket blacklist", "Modération", "Support vocal", "Ticket Owner"];
const SLOT_START = 10;
const SLOT_END = 23;
const SLOTS = Array.from({ length: SLOT_END - SLOT_START + 1 }, (_, i) => String(SLOT_START + i).padStart(2, "0") + ":00");

const DOCS = [
  { code: "BL", title: "Doc BL", type: "Ticket blacklist", url: "https://canva.link/c6iphiagx32md2t", grad: "linear-gradient(135deg,#F2555F,#9C2B58)" },
  { code: "MS", title: "Doc Modération stats", type: "Modération", url: "https://canva.link/iym9cw2of9rfnuo", grad: "linear-gradient(135deg,#6C8CFF,#3D55C8)" },
  { code: "SV", title: "Doc SV", type: "Support vocal", url: "https://canva.link/qecu0kfehikinuu", grad: "linear-gradient(135deg,#41D69A,#1D8A66)" },
  { code: "TO", title: "Doc T.Owner", type: "Ticket Owner", url: "https://canva.link/lvdsyzndwdcewls", grad: "linear-gradient(135deg,#F5B23D,#C77818)" },
];

const T = {
  fr: {
    app_title: "Gestion des formations", app_sub: "Corps d'encadrement",
    tab_planning: "Planning", tab_done: "Formations réalisées", tab_docs: "Documentation", tab_admin: "Admin",
    refresh: "Actualiser", refreshed: "Données actualisées",
    menu_account: "Mon compte", menu_docs: "Documentation", menu_lang: "Langue", logout: "Déconnexion",
    role_formateur: "Formateur", role_gerant: "Gérant", role_admin: "Admin",
    st_pending: "En attente", st_validated: "Validée", st_refused: "Refusée",
    login_title: "Connexion staff", pseudo: "Pseudo", password: "Mot de passe", login: "Se connecter",
    login_hint: "Les comptes sont gérés par l'administrateur depuis le panel Admin.",
    login_err: "Pseudo ou mot de passe incorrect.", pseudo_ph: "Ton pseudo",
    login_blocked: "Trop de tentatives. Réessaie dans {min} minutes.", log_login_fail: "Échec de connexion",
    plan_new: "Planifier une formation", type: "Type de formation", day: "Jour", slot: "Créneau",
    trainer: "Pseudo du formateur", add_plan: "Ajouter au planning",
    plan_tip: "Astuce : clique sur une case vide du planning pour pré-sélectionner le jour et le créneau. Les semaines passées sont supprimées automatiquement.",
    week_prev: "‹ Semaine préc.", week_next: "Semaine suiv. ›", week_of: "Semaine du",
    validate: "Valider", refuse: "Refuser", repend: "Remettre en attente", del: "Supprimer",
    pick_day: "Choisis un jour.", pick_slot: "Choisis un créneau.", pick_trainer: "Indique le pseudo du formateur.",
    added_slot: "Formation ajoutée au créneau", removed: "Entrée supprimée", cell_picked: "Créneau sélectionné — complète puis Ajoute",
    done_new: "Enregistrer une formation réalisée", date: "Date", start: "Heure de début", end: "Heure de fin",
    vocal: "Vocal utilisée", vocal_ph: "Nom du salon vocal", comment: "Commentaire",
    comment_ph: "Déroulé, points abordés, remarques…", trained: "GSE Formés", trained_ph: "Pseudos des membres formés",
    signoff: "Formule de fin", save_done: "Enregistrer la formation", saved_done: "Formation enregistrée",
    deleted_done: "Formation supprimée", need_date: "Indique la date.",
    history: "Historique", copy_report: "Copier le rapport", copied: "Rapport copié — colle-le dans Discord",
    copy_manual_title: "Copie manuelle",
    copy_manual_hint: "La copie automatique est bloquée ici. Sélectionne le texte ci-dessous et copie-le (Ctrl/Cmd + C).",
    copy_retry: "Réessayer la copie", close: "Fermer", copied_short: "Rapport copié", copy_manual_do: "Sélectionne et copie manuellement",
    empty_done: "Aucune formation enregistrée pour le moment.",
    docs_title: "Documentation", docs_sub: "Les supports officiels des formations GSE. Ouvre le document correspondant à ta formation.",
    open_doc: "Ouvrir le document",
    acc_title: "Mon compte", acc_avatar: "Avatar", acc_upload: "Choisir une image", acc_remove: "Retirer",
    acc_avatar_hint: "Optionnel. L'image est réduite automatiquement.",
    acc_pwd: "Changer mon mot de passe", acc_current: "Mot de passe actuel", acc_new: "Nouveau mot de passe",
    acc_confirm: "Confirmer le nouveau", acc_save: "Mettre à jour", pwd_changed: "Mot de passe mis à jour",
    pwd_bad_current: "Mot de passe actuel incorrect.", pwd_mismatch: "Les deux saisies ne correspondent pas.",
    pwd_short: "Mot de passe trop court (6 caractères min).",
    avatar_set: "Avatar mis à jour", avatar_removed: "Avatar retiré", avatar_err: "Image illisible.",
    stats: "Statistiques", stat_total: "Formations réalisées au total", stat_week: "cette semaine",
    recent: "Dernières formations", activity: "Historique d'actions", nothing: "Rien ici pour le moment.",
    log_plan_add: "Formation planifiée", log_validate: "Formation validée", log_refuse: "Formation refusée",
    log_pending: "Remise en attente", log_plan_del: "Planification supprimée",
    log_done_add: "Formation réalisée enregistrée", log_done_del: "Formation réalisée supprimée",
    log_pwd: "Mot de passe modifié", log_avatar: "Avatar modifié", log_login: "Connexion",
    log_acc_create: "Compte créé", log_acc_update: "Compte modifié", log_acc_delete: "Compte supprimé",
    log_info: "Informations de la semaine modifiées",
    adm_panel: "Panel administrateur",
    adm_warn: "Crée, modifie ou supprime les comptes du staff. Les mots de passe sont chiffrés : ils ne s'affichent qu'une seule fois, à la création ou à la réinitialisation.",
    adm_create: "Créer un compte", adm_role: "Rôle", adm_gen: "Générer", adm_do_create: "Créer le compte",
    adm_accounts: "Comptes", adm_edit: "Modifier", adm_save: "Enregistrer", adm_cancel: "Annuler",
    adm_you: "(toi)", adm_pwd_optional: "Laisser vide pour ne pas le changer",
    adm_pwd_once_title: "Mot de passe à transmettre",
    adm_pwd_once: "Note-le et transmets-le maintenant : il ne sera plus jamais affiché.",
    adm_need_pseudo: "Indique un pseudo.", adm_pseudo_taken: "Ce pseudo existe déjà.",
    adm_pwd_short: "Mot de passe trop court (6 caractères min).",
    adm_last_admin: "Impossible : il doit rester au moins un admin.",
    adm_self_del: "Tu ne peux pas supprimer ton propre compte.",
    adm_created: "Compte créé", adm_updated: "Compte mis à jour", adm_deleted: "Compte supprimé",
    copy_fail: "Copie impossible", adm_copy: "Copier",
    nav_home: "Accueil", nav_users: "Utilisateurs", nav_hier: "Hiérarchie", menu: "Menu",
    home_hello: "Bonjour", home_upcoming: "Prochaines formations", home_none: "Aucune formation à venir.",
    home_pending: "formation(s) en attente de validation", home_quick: "Accès rapide", home_go_plan: "Voir le planning",
    users_title: "Utilisateurs", hier_title: "Hiérarchie",
    nav_logs: "Logs", logs_title: "Logs du site",
    logs_filter: "Filtrer (pseudo, action, détail…)", logs_empty: "Aucune activité enregistrée.",
    home_info: "Informations de la semaine", info_edit: "Modifier", info_save: "Enregistrer",
    info_cancel: "Annuler", info_empty: "Aucune information pour le moment.",
    info_updated: "Informations mises à jour", info_ph: "Objectifs, annonces, consignes de la semaine…",
    updated_on: "Mis à jour le",
    server_err: "Erreur de connexion au serveur.",
    role_superviseur: "Superviseur Formation", role_responsable: "Responsable Formation",
    role_formateur_test: "Formateur en test",
    log_role: "Rôle modifié", role_changed: "Rôle mis à jour",
    discord_title: "Serveur Discord officiel", discord_join: "Rejoindre",
    discord_online: "en ligne", discord_members: "membres",
    sec_formation: "Formation", sec_entretien: "Entretien", sec_rapports: "Rapports",
    nav_rapports: "Rapports",
    rpt_title: "Rapports", rpt_sub: "Centre de classement des rapports de la GSE. Choisis une catégorie.",
    cat_all: "Tout", cat_entretien: "Entretien", cat_formation: "Formation", cat_recrutement: "Recrutement",
    rpt_new: "Nouveau rapport", rpt_name: "Nom du rapport", rpt_date: "Date de création",
    rpt_notify: "Envoyer une notification", rpt_template: "Sélectionner une template",
    rpt_template_none: "Aucune template", rpt_category: "Sélectionner une catégorie",
    rpt_body: "Rapport", rpt_body_ph: "Rédige ton rapport ici…",
    rpt_proof: "Preuves", rpt_proof_add: "Ajouter des images",
    rpt_proof_hint: "Images uniquement (captures d'écran, photos…). 6 maximum.",
    rpt_create: "Créer le rapport", rpt_created: "Rapport créé",
    rpt_deleted: "Rapport supprimé", rpt_empty: "Aucun rapport dans cette catégorie.",
    rpt_by: "Agent", rpt_back: "‹ Retour", rpt_reports: "rapport(s)",
    rpt_need_name: "Indique le nom du rapport.", rpt_need_cat: "Choisis une catégorie.",
    tpl_save_too: "Enregistrer aussi comme template", tpl_saved: "Template enregistrée",
    log_report_add: "Rapport créé", log_report_del: "Rapport supprimé", log_template_add: "Template créée",
    notif_title: "Derniers rapports notifiés",
    role_supreme: "Admin Suprême",
    ent_title: "Entretiens", ent_sub: "Registre des entretiens de recrutement de la GSE.",
    ent_new: "Nouvel entretien", ent_candidat: "Candidat (pseudo Discord)", ent_temps: "Temps de l'entretien",
    ent_profil: "Commentaire sur le profil", ent_oral: "Commentaire sur l'expression orale",
    ent_grab: "Qui l'a grab ? (optionnel)", ent_decision: "Décision",
    dec_accepte: "Accepté", dec_attente: "En attente", dec_refuse: "Refusé",
    ent_create: "Enregistrer l'entretien", ent_created: "Entretien enregistré", ent_deleted: "Entretien supprimé",
    ent_recruteur: "Recruteur", ent_search_ph: "Rechercher (candidat, recruteur, ID…)",
    ent_total: "Total", ent_empty: "Aucun entretien.", ent_need: "Remplis candidat, temps et commentaires.",
    log_ent_add: "Entretien enregistré", log_ent_del: "Entretien supprimé", log_ent_dec: "Décision d'entretien modifiée",
    rpt_open_s: "Ouverts", rpt_archived_s: "Archivés", rpt_archive: "Archiver", rpt_unarchive: "Rouvrir",
    rpt_st_open: "Ouvert", rpt_st_archived: "Archivé", rpt_search_ph: "Rechercher (nom, agent, contenu, n°…)",
    rpt_archived_ok: "Rapport archivé", rpt_reopened: "Rapport rouvert",
    search_title: "Recherche", search_ph: "Rechercher sur le site…", search_none: "Aucun résultat.",
    search_hint: "Tape au moins 2 caractères.",
    sx_reports: "Rapports", sx_ents: "Entretiens", sx_done: "Formations réalisées", sx_plan: "Planning",
    sx_users: "Utilisateurs", sx_docs: "Documentation",
    notif_bell: "Notifications", notif_none: "Rien de nouveau.", notif_update: "Mise à jour du site",
    notif_report: "Nouveau rapport",
    logs_clear: "Vider les logs", logs_cleared: "Logs vidés", logs_confirm: "Confirmer la suppression définitive",
    log_purge: "Logs purgés",
    discord_invited: "Tu as été invité à rejoindre un serveur",
    trainer_ph: "Pseudo du formateur…",
    sx_pages: "Pages du site",
    req_title: "Requêtes", req_home_title: "Une idée ? Un retour ?",
    req_home_sub: "Propose une amélioration ou signale un souci : ton message part directement à la gestion.",
    req_ph: "Décris ta demande ou ton retour pour les mises à jour du site…",
    req_send: "Envoyer", req_sent: "Merci, retour envoyé à la gestion !", req_need: "Écris ton retour avant d'envoyer.",
    req_empty: "Aucune requête pour l'instant.", req_deleted: "Requête supprimée", req_from: "De",
    req_admin_sub: "Retours envoyés par les GSE depuis l'accueil. Visible uniquement par les admins.",
    log_req_add: "Requête envoyée", log_req_del: "Requête supprimée",
    br_formation: "Formation", br_entretien: "Entretien", br_recruteur: "Recruteur",
    hier_sub: "Les trois branches de la GSE. Un membre peut cumuler plusieurs grades.",
    hier_none: "Personne pour l'instant.",
    hier_hint: "Les grades s'attribuent et se retirent depuis la page Utilisateurs (gestion uniquement).",
    log_grade: "Grades modifiés",
    rpt_edit: "Modifier", rpt_edit_title: "Modifier le rapport", rpt_save: "Enregistrer les modifications",
    rpt_edited: "Rapport modifié", rpt_editleft: "Modifiable encore", rpt_derog_active: "Dérogation de modification active",
    rpt_derog_on: "Accorder une dérogation", rpt_derog_off: "Retirer la dérogation",
    rpt_derog_set: "Dérogation accordée", rpt_derog_unset: "Dérogation retirée",
    log_report_edit: "Rapport modifié", log_report_derog: "Dérogation rapport",
    sec_recrutement: "Recrutement", sec_corps: "Corps d'encadrement",
    nav_rapports_all: "Tous les rapports",
    nav_r_formation: "Rapports formation", nav_r_entretien: "Rapports entretien", nav_r_recrutement: "Rapports recrutement",
    team_formation: "Équipe Formation", team_entretien: "Équipe Entretien", team_recruteur: "Équipe Recrutement",
    team_count: "membre(s) dans cette branche",
    hier_pyr_sub: "La pyramide des rôles du corps d'encadrement de la GSE.",
    role_superviseur_entretien: "Superviseur Entretien", role_superviseur_recrutement: "Superviseur Recrutement",
    role_responsable_entretien: "Responsable Entretien", role_responsable_recrutement: "Responsable Recrutement",
    adm_roles: "Rôles", adm_add_role: "Ajouter un rôle", adm_need_role: "Au moins un rôle est requis.",
    by: "par", loading: "Chargement…",
  },
  en: {
    app_title: "Training management", app_sub: "Management corps",
    tab_planning: "Schedule", tab_done: "Completed trainings", tab_docs: "Documentation", tab_admin: "Admin",
    refresh: "Refresh", refreshed: "Data refreshed",
    menu_account: "My account", menu_docs: "Documentation", menu_lang: "Language", logout: "Log out",
    role_formateur: "Trainer", role_gerant: "Manager", role_admin: "Admin",
    st_pending: "Pending", st_validated: "Approved", st_refused: "Refused",
    login_title: "Staff login", pseudo: "Username", password: "Password", login: "Log in",
    login_hint: "Accounts are managed by the administrator from the Admin panel.",
    login_err: "Wrong username or password.", pseudo_ph: "Your username",
    login_blocked: "Too many attempts. Try again in {min} minutes.", log_login_fail: "Failed login",
    plan_new: "Schedule a training", type: "Training type", day: "Day", slot: "Time slot",
    trainer: "Trainer username", add_plan: "Add to schedule",
    plan_tip: "Tip: click an empty cell in the grid to pre-select the day and slot. Past weeks are removed automatically.",
    week_prev: "‹ Prev. week", week_next: "Next week ›", week_of: "Week of",
    validate: "Approve", refuse: "Refuse", repend: "Set back to pending", del: "Delete",
    pick_day: "Pick a day.", pick_slot: "Pick a time slot.", pick_trainer: "Enter the trainer username.",
    added_slot: "Training added at", removed: "Entry deleted", cell_picked: "Slot selected — fill in then Add",
    done_new: "Record a completed training", date: "Date", start: "Start time", end: "End time",
    vocal: "Voice channel", vocal_ph: "Voice channel name", comment: "Comment",
    comment_ph: "What was covered, notes…", trained: "Trained GSE", trained_ph: "Usernames of trained members",
    signoff: "Sign-off", save_done: "Save training", saved_done: "Training saved",
    deleted_done: "Training deleted", need_date: "Enter the date.",
    history: "History", copy_report: "Copy report", copied: "Report copied — paste it in Discord",
    copy_manual_title: "Manual copy",
    copy_manual_hint: "Automatic copy is blocked here. Select the text below and copy it (Ctrl/Cmd + C).",
    copy_retry: "Retry copy", close: "Close", copied_short: "Report copied", copy_manual_do: "Select and copy manually",
    empty_done: "No training recorded yet.",
    docs_title: "Documentation", docs_sub: "Official GSE training materials. Open the document matching your training.",
    open_doc: "Open document",
    acc_title: "My account", acc_avatar: "Avatar", acc_upload: "Choose an image", acc_remove: "Remove",
    acc_avatar_hint: "Optional. The image is resized automatically.",
    acc_pwd: "Change my password", acc_current: "Current password", acc_new: "New password",
    acc_confirm: "Confirm new password", acc_save: "Update", pwd_changed: "Password updated",
    pwd_bad_current: "Current password is incorrect.", pwd_mismatch: "The two entries don't match.",
    pwd_short: "Password too short (min 6 characters).",
    avatar_set: "Avatar updated", avatar_removed: "Avatar removed", avatar_err: "Unreadable image.",
    stats: "Statistics", stat_total: "Total trainings given", stat_week: "this week",
    recent: "Latest trainings", activity: "Action history", nothing: "Nothing here yet.",
    log_plan_add: "Training scheduled", log_validate: "Training approved", log_refuse: "Training refused",
    log_pending: "Set back to pending", log_plan_del: "Schedule entry deleted",
    log_done_add: "Completed training recorded", log_done_del: "Completed training deleted",
    log_pwd: "Password changed", log_avatar: "Avatar changed", log_login: "Logged in",
    log_acc_create: "Account created", log_acc_update: "Account updated", log_acc_delete: "Account deleted",
    log_info: "Week information updated",
    adm_panel: "Administrator panel",
    adm_warn: "Create, edit or delete staff accounts. Passwords are encrypted: they are shown only once, on creation or reset.",
    adm_create: "Create an account", adm_role: "Role", adm_gen: "Generate", adm_do_create: "Create account",
    adm_accounts: "Accounts", adm_edit: "Edit", adm_save: "Save", adm_cancel: "Cancel",
    adm_you: "(you)", adm_pwd_optional: "Leave empty to keep current",
    adm_pwd_once_title: "Password to share",
    adm_pwd_once: "Save and share it now: it will never be shown again.",
    adm_need_pseudo: "Enter a username.", adm_pseudo_taken: "This username already exists.",
    adm_pwd_short: "Password too short (min 6 characters).",
    adm_last_admin: "Not allowed: at least one admin must remain.",
    adm_self_del: "You can't delete your own account.",
    adm_created: "Account created", adm_updated: "Account updated", adm_deleted: "Account deleted",
    copy_fail: "Copy failed", adm_copy: "Copy",
    nav_home: "Home", nav_users: "Users", nav_hier: "Hierarchy", menu: "Menu",
    home_hello: "Hello", home_upcoming: "Upcoming trainings", home_none: "No upcoming training.",
    home_pending: "training(s) awaiting approval", home_quick: "Quick access", home_go_plan: "Open schedule",
    users_title: "Users", hier_title: "Hierarchy",
    nav_logs: "Logs", logs_title: "Site logs",
    logs_filter: "Filter (username, action, detail…)", logs_empty: "No activity recorded.",
    home_info: "This week's information", info_edit: "Edit", info_save: "Save",
    info_cancel: "Cancel", info_empty: "No information yet.",
    info_updated: "Information updated", info_ph: "Goals, announcements, instructions for the week…",
    updated_on: "Updated on",
    server_err: "Could not reach the server.",
    role_superviseur: "Training Supervisor", role_responsable: "Training Manager",
    role_formateur_test: "Trainer (trial)",
    log_role: "Role changed", role_changed: "Role updated",
    discord_title: "Official Discord server", discord_join: "Join",
    discord_online: "online", discord_members: "members",
    sec_formation: "Training", sec_entretien: "Interviews", sec_rapports: "Reports",
    nav_rapports: "Reports",
    rpt_title: "Reports", rpt_sub: "GSE reports filing center. Pick a category.",
    cat_all: "All", cat_entretien: "Interview", cat_formation: "Training", cat_recrutement: "Recruitment",
    rpt_new: "New report", rpt_name: "Report name", rpt_date: "Creation date",
    rpt_notify: "Send a notification", rpt_template: "Select a template",
    rpt_template_none: "No template", rpt_category: "Select a category",
    rpt_body: "Report", rpt_body_ph: "Write your report here…",
    rpt_proof: "Evidence", rpt_proof_add: "Add images",
    rpt_proof_hint: "Images only (screenshots, photos…). 6 max.",
    rpt_create: "Create report", rpt_created: "Report created",
    rpt_deleted: "Report deleted", rpt_empty: "No report in this category.",
    rpt_by: "Agent", rpt_back: "‹ Back", rpt_reports: "report(s)",
    rpt_need_name: "Enter the report name.", rpt_need_cat: "Pick a category.",
    tpl_save_too: "Also save as template", tpl_saved: "Template saved",
    log_report_add: "Report created", log_report_del: "Report deleted", log_template_add: "Template created",
    notif_title: "Latest notified reports",
    role_supreme: "Supreme Admin",
    ent_title: "Interviews", ent_sub: "GSE recruitment interview register.",
    ent_new: "New interview", ent_candidat: "Candidate (Discord username)", ent_temps: "Interview duration",
    ent_profil: "Profile comment", ent_oral: "Speaking comment",
    ent_grab: "Who grabbed them? (optional)", ent_decision: "Decision",
    dec_accepte: "Accepted", dec_attente: "Pending", dec_refuse: "Refused",
    ent_create: "Save interview", ent_created: "Interview saved", ent_deleted: "Interview deleted",
    ent_recruteur: "Recruiter", ent_search_ph: "Search (candidate, recruiter, ID…)",
    ent_total: "Total", ent_empty: "No interview.", ent_need: "Fill candidate, duration and comments.",
    log_ent_add: "Interview saved", log_ent_del: "Interview deleted", log_ent_dec: "Interview decision changed",
    rpt_open_s: "Open", rpt_archived_s: "Archived", rpt_archive: "Archive", rpt_unarchive: "Reopen",
    rpt_st_open: "Open", rpt_st_archived: "Archived", rpt_search_ph: "Search (name, agent, content, #…)",
    rpt_archived_ok: "Report archived", rpt_reopened: "Report reopened",
    search_title: "Search", search_ph: "Search the site…", search_none: "No result.",
    search_hint: "Type at least 2 characters.",
    sx_reports: "Reports", sx_ents: "Interviews", sx_done: "Completed trainings", sx_plan: "Schedule",
    sx_users: "Users", sx_docs: "Documentation",
    notif_bell: "Notifications", notif_none: "Nothing new.", notif_update: "Site update",
    notif_report: "New report",
    logs_clear: "Clear logs", logs_cleared: "Logs cleared", logs_confirm: "Confirm permanent deletion",
    log_purge: "Logs purged",
    discord_invited: "You've been invited to join a server",
    trainer_ph: "Trainer's username…",
    sx_pages: "Site pages",
    req_title: "Requests", req_home_title: "An idea? Feedback?",
    req_home_sub: "Suggest an improvement or report an issue: your message goes straight to management.",
    req_ph: "Describe your request or feedback for site updates…",
    req_send: "Send", req_sent: "Thanks, feedback sent to management!", req_need: "Write your feedback before sending.",
    req_empty: "No request yet.", req_deleted: "Request deleted", req_from: "From",
    req_admin_sub: "Feedback sent by GSE members from the home page. Visible to admins only.",
    log_req_add: "Request sent", log_req_del: "Request deleted",
    br_formation: "Training", br_entretien: "Interview", br_recruteur: "Recruiter",
    hier_sub: "The three GSE branches. A member can hold several grades.",
    hier_none: "Nobody yet.",
    hier_hint: "Grades are assigned and removed from the Users page (management only).",
    log_grade: "Grades changed",
    rpt_edit: "Edit", rpt_edit_title: "Edit report", rpt_save: "Save changes",
    rpt_edited: "Report edited", rpt_editleft: "Editable for another", rpt_derog_active: "Edit waiver active",
    rpt_derog_on: "Grant edit waiver", rpt_derog_off: "Remove waiver",
    rpt_derog_set: "Waiver granted", rpt_derog_unset: "Waiver removed",
    log_report_edit: "Report edited", log_report_derog: "Report waiver",
    sec_recrutement: "Recruitment", sec_corps: "Management corps",
    nav_rapports_all: "All reports",
    nav_r_formation: "Training reports", nav_r_entretien: "Interview reports", nav_r_recrutement: "Recruitment reports",
    team_formation: "Training team", team_entretien: "Interview team", team_recruteur: "Recruitment team",
    team_count: "member(s) in this branch",
    hier_pyr_sub: "The GSE management corps role pyramid.",
    role_superviseur_entretien: "Interview Supervisor", role_superviseur_recrutement: "Recruitment Supervisor",
    role_responsable_entretien: "Interview Manager", role_responsable_recrutement: "Recruitment Manager",
    adm_roles: "Roles", adm_add_role: "Add a role", adm_need_role: "At least one role is required.",
    by: "by", loading: "Loading…",
  },
};

/* ---------- API ---------- */
async function api(path, method = "GET", body) {
  try {
    const res = await fetch(path, {
      method,
      headers: body ? { "Content-Type": "application/json" } : undefined,
      body: body ? JSON.stringify(body) : undefined,
      credentials: "same-origin",
    });
    let data = null;
    try { data = await res.json(); } catch {}
    return { ok: res.ok, status: res.status, data };
  } catch {
    return { ok: false, status: 0, data: null };
  }
}

/* ---------- Utils ---------- */
function frDate(iso) {
  if (!iso) return "—";
  const [y, m, d] = String(iso).split("-");
  if (!y || !m || !d) return iso;
  return `${d}/${m}/${y}`;
}
const DAY_NAMES = {
  fr: ["Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi", "Dimanche"],
  en: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"],
};
const MONTHS = {
  fr: ["janvier", "février", "mars", "avril", "mai", "juin", "juillet", "août", "septembre", "octobre", "novembre", "décembre"],
  en: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"],
};
function toISO(d) {
  return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
}
function startOfWeek(d) {
  const x = new Date(d);
  x.setDate(x.getDate() - ((x.getDay() + 6) % 7));
  x.setHours(0, 0, 0, 0);
  return x;
}
function addDays(d, n) { const x = new Date(d); x.setDate(x.getDate() + n); return x; }
function weekLabel(monday, lang) {
  const sunday = addDays(monday, 6);
  const M = MONTHS[lang] || MONTHS.fr;
  if (monday.getMonth() === sunday.getMonth()) return `${monday.getDate()} – ${sunday.getDate()} ${M[sunday.getMonth()]}`;
  return `${monday.getDate()} ${M[monday.getMonth()]} – ${sunday.getDate()} ${M[sunday.getMonth()]}`;
}
function fmtTs(ts, lang) {
  try {
    return new Date(ts).toLocaleString(lang === "en" ? "en-GB" : "fr-FR", { dateStyle: "short", timeStyle: "short" });
  } catch { return ""; }
}
function genPassword(len = 22) {
  const alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789!@#$%*-_=?";
  const arr = new Uint32Array(len);
  crypto.getRandomValues(arr);
  return Array.from(arr, (n) => alphabet[n % alphabet.length]).join("");
}
async function copyText(text) {
  try {
    if (navigator.clipboard && window.isSecureContext) {
      await navigator.clipboard.writeText(text);
      return true;
    }
  } catch {}
  try {
    const ta = document.createElement("textarea");
    ta.value = text;
    ta.setAttribute("readonly", "");
    ta.style.position = "fixed";
    ta.style.top = "-1000px";
    ta.style.opacity = "0";
    document.body.appendChild(ta);
    ta.focus(); ta.select();
    const ok = document.execCommand("copy");
    document.body.removeChild(ta);
    if (ok) return true;
  } catch {}
  return false;
}
function resizeImage(file, size = 96) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onerror = () => reject(new Error("read"));
    reader.onload = () => {
      const img = new Image();
      img.onerror = () => reject(new Error("img"));
      img.onload = () => {
        const c = document.createElement("canvas");
        c.width = size; c.height = size;
        const ctx = c.getContext("2d");
        const min = Math.min(img.width, img.height);
        const sx = (img.width - min) / 2, sy = (img.height - min) / 2;
        ctx.drawImage(img, sx, sy, min, min, 0, 0, size, size);
        resolve(c.toDataURL("image/jpeg", 0.82));
      };
      img.src = reader.result;
    };
    reader.readAsDataURL(file);
  });
}

const STATUS_META = {
  pending: { tkey: "st_pending", color: "var(--warn)" },
  validated: { tkey: "st_validated", color: "var(--ok)" },
  refused: { tkey: "st_refused", color: "var(--bad)" },
};
const ROLES_ORDER = ["supreme", "admin", "superviseur", "superviseur_entretien", "superviseur_recrutement", "responsable", "responsable_entretien", "responsable_recrutement", "formateur", "formateur_test"];
const RANK = { supreme: 0, admin: 1, superviseur: 2, superviseur_entretien: 2, superviseur_recrutement: 2, responsable: 3, responsable_entretien: 3, responsable_recrutement: 3, formateur: 4, formateur_test: 5 };
const MANAGER_ROLES = ["supreme", "admin", "superviseur", "superviseur_entretien", "superviseur_recrutement", "responsable", "responsable_entretien", "responsable_recrutement"];
function normalizeRole(r) {
  if (r === "gerant") return "responsable";
  return RANK[r] !== undefined ? r : "formateur";
}
function primaryRole(roles) {
  const v = (roles || []).filter((r) => RANK[r] !== undefined);
  v.sort((a, b) => RANK[a] - RANK[b]);
  return v[0] || "formateur";
}
function rolesOf(a) {
  return Array.isArray(a.roles) && a.roles.length ? a.roles : [a.role];
}
const ROLE_META = {
  supreme: { tkey: "role_supreme", color: "#FF5C7A" },
  admin: { tkey: "role_admin", color: "#C792FF" },
  superviseur: { tkey: "role_superviseur", color: "var(--warn)" },
  superviseur_entretien: { tkey: "role_superviseur_entretien", color: "#FF9F43" },
  superviseur_recrutement: { tkey: "role_superviseur_recrutement", color: "#E8843A" },
  responsable: { tkey: "role_responsable", color: "var(--accent)" },
  responsable_entretien: { tkey: "role_responsable_entretien", color: "#8E7CFF" },
  responsable_recrutement: { tkey: "role_responsable_recrutement", color: "#5CC8FF" },
  formateur: { tkey: "role_formateur", color: "var(--ok)" },
  formateur_test: { tkey: "role_formateur_test", color: "var(--muted)" },
  gerant: { tkey: "role_responsable", color: "var(--accent)" },
};
const ADMIN_ROLES = ["supreme", "admin"];
const SITE_UPDATES = [
  { id: "u3", ts: 1781140000000, fr: "Nouveau : module Entretiens, recherche globale et notifications", en: "New: Interviews module, global search and notifications" },
  { id: "u2", ts: 1781050000000, fr: "Rapports : numérotation, statuts Ouvert/Archivé et recherche", en: "Reports: numbering, Open/Archived statuses and search" },
  { id: "u1", ts: 1780900000000, fr: "Lancement du site GSE 🎉", en: "GSE site launch 🎉" },
];
function entIdGen() {
  const a = new Uint8Array(3);
  crypto.getRandomValues(a);
  return "ENT-" + Array.from(a).map((b) => b.toString(16).padStart(2, "0")).join("").toUpperCase();
}
const SUPERVISOR_PLUS = ["supreme", "admin", "superviseur", "superviseur_entretien", "superviseur_recrutement"];
const BRANCHES = {
  formation: { tkey: "br_formation", icon: "🎓", color: "var(--accent)" },
  entretien: { tkey: "br_entretien", icon: "🤝", color: "#F5B23D" },
  recruteur: { tkey: "br_recruteur", icon: "🎯", color: "var(--ok)" },
};
const DEFAULT_TEMPLATES = [
  {
    id: "dt_formation", name: "📋 Template — Rapport de formation", category: "formation",
    content: `📋 RAPPORT DE FORMATION

Pseudo du formateur :
Pseudo du/des GSE formé(s) :
Date : JJ/MM/AAAA
Heure de début :
Heure de fin :
Type de formation : (Ticket blacklist / Modération / Support vocal / Ticket Owner)
Vocal utilisé :

────── Déroulé de la formation ──────
Points abordés :
-
-

Mises en situation réalisées :
-

────── Évaluation ──────
Points forts du GSE :
Points à améliorer :
Note globale :  /10

Décision : (Validé / À revoir / Refusé)
Commentaire final :`,
  },
  {
    id: "dt_entretien", name: "🤝 Template — Rapport d'entretien", category: "entretien",
    content: `🤝 RAPPORT D'ENTRETIEN

Pseudo du recruteur :
Pseudo du candidat :
Date : JJ/MM/AAAA
Heure :
Durée de l'entretien :

────── Profil ──────
Présentation du candidat (motivation, disponibilités) :

Mentalité / maturité :

────── Expression orale ──────
Éloquence, clarté, raisonnement :

────── Conclusion ──────
Points positifs :
Points négatifs :
Décision : (Accepté / En attente / Refusé)
Commentaire final :`,
  },
  {
    id: "dt_recrutement", name: "🎯 Template — Rapport de recrutement", category: "recrutement",
    content: `🎯 RAPPORT DE RECRUTEMENT

Pseudo du recruteur :
Pseudo de la recrue :
Date : JJ/MM/AAAA
Heure :
Qui l'a grab :

────── Intégration ──────
Rôles attribués sur le Discord :
Présentation faite à l'équipe : (Oui / Non)
Documentation transmise : (Oui / Non)

────── Suivi prévu ──────
Formateur référent :
Première formation planifiée le :

Commentaire :`,
  },
];


const RCATS = {
  entretien: { tkey: "cat_entretien", color: "#F5B23D", code: "EN" },
  formation: { tkey: "cat_formation", color: "#6C8CFF", code: "FO" },
  recrutement: { tkey: "cat_recrutement", color: "#41D69A", code: "RE" },
};
function resizeProof(file, maxDim = 900) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onerror = () => reject(new Error("read"));
    reader.onload = () => {
      const img = new Image();
      img.onerror = () => reject(new Error("img"));
      img.onload = () => {
        const scale = Math.min(1, maxDim / Math.max(img.width, img.height));
        const c = document.createElement("canvas");
        c.width = Math.round(img.width * scale);
        c.height = Math.round(img.height * scale);
        c.getContext("2d").drawImage(img, 0, 0, c.width, c.height);
        resolve(c.toDataURL("image/jpeg", 0.8));
      };
      img.src = reader.result;
    };
    reader.readAsDataURL(file);
  });
}

const LOG_LABEL = {
  plan_add: "log_plan_add", validate: "log_validate", refuse: "log_refuse",
  pending: "log_pending", plan_del: "log_plan_del",
  done_add: "log_done_add", done_del: "log_done_del",
  pwd: "log_pwd", avatar: "log_avatar", login: "log_login",
  acc_create: "log_acc_create", acc_update: "log_acc_update", acc_delete: "log_acc_delete",
  info: "log_info",
  role_change: "log_role",
  report_add: "log_report_add", report_del: "log_report_del", template_add: "log_template_add",
  ent_add: "log_ent_add", ent_del: "log_ent_del", ent_dec: "log_ent_dec", purge: "log_purge",
  req_add: "log_req_add", req_del: "log_req_del", grade_change: "log_grade",
  report_edit: "log_report_edit", report_derog: "log_report_derog",
};
const LOG_COLOR = {
  validate: "var(--ok)", refuse: "var(--bad)", plan_del: "var(--bad)",
  done_del: "var(--bad)", acc_delete: "var(--bad)",
  acc_create: "var(--ok)", login: "var(--accent)",
};
const CSS = `
  .gse * { box-sizing: border-box; }
  .gse {
    --bg:#0E1119; --surface:#161A24; --surface2:#1E2330; --border:#2A3142;
    --text:#E6E9F2; --muted:#8C93A6; --accent:#6C8CFF; --accent-dim:rgba(108,140,255,.14);
    --ok:#41D69A; --warn:#F5B23D; --bad:#F2555F;
    --mono: ui-monospace,"SF Mono",Menlo,Consolas,monospace;
    --sans: ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;
    font-family: var(--sans); color: var(--text);
    background:
      radial-gradient(900px 600px at 88% -8%, rgba(108,140,255,.13), transparent 60%),
      radial-gradient(750px 550px at -8% 108%, rgba(124,92,224,.10), transparent 60%),
      #0B0E16;
    background-attachment: fixed;
    min-height: 100%; line-height: 1.5; -webkit-font-smoothing: antialiased;
  }
  .gse main > * { animation: viewfade .22s ease; }
  @keyframes viewfade { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
  .gse { scrollbar-color: #2A3142 transparent; }
  .gse ::-webkit-scrollbar { height: 10px; width: 10px; }
  .gse ::-webkit-scrollbar-thumb { background: #2A3142; border-radius: 99px; }
  .gse ::-webkit-scrollbar-track { background: transparent; }
  .gse .eyebrow { font-family: var(--mono); font-size: 11px; letter-spacing: .18em; text-transform: uppercase; color: var(--muted); }
  .gse input, .gse select, .gse textarea {
    width: 100%; background: var(--bg); color: var(--text);
    border: 1px solid var(--border); border-radius: 8px;
    padding: 10px 12px; font-size: 14px; font-family: var(--sans);
    outline: none; transition: border-color .15s, box-shadow .15s;
  }
  .gse textarea { resize: vertical; min-height: 64px; }
  .gse input:focus, .gse select:focus, .gse textarea:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-dim); }
  .gse label { display:block; font-size:12px; color:var(--muted); margin-bottom:6px; }
  .gse .btn {
    border: 1px solid var(--border); background: var(--surface2); color: var(--text);
    border-radius: 8px; padding: 10px 16px; font-size: 14px; font-weight: 600;
    cursor: pointer; transition: background .15s, border-color .15s, transform .05s; font-family: var(--sans);
  }
  .gse .btn:hover { background:#262C3D; }
  .gse .btn:active { transform: translateY(1px); }
  .gse .btn:focus-visible { outline:none; box-shadow:0 0 0 3px var(--accent-dim); }
  .gse .btn-primary { background: var(--accent); border-color: var(--accent); color:#fff; }
  .gse .btn-primary:hover { background:#5A7BF5; }
  .gse .btn-ok { background: transparent; border-color: var(--ok); color: var(--ok); }
  .gse .btn-ok:hover { background: rgba(65,214,154,.12); }
  .gse .btn-bad { background: transparent; border-color: var(--bad); color: var(--bad); }
  .gse .btn-bad:hover { background: rgba(242,85,95,.12); }
  .gse .btn-ghost { background: transparent; border-color: transparent; color: var(--muted); padding:6px 10px; }
  .gse .btn-ghost:hover { background: var(--surface2); color: var(--text); }
  .gse .btn-sm { padding:6px 12px; font-size:13px; }
  .gse .card {
    background: linear-gradient(180deg, rgba(255,255,255,.018), transparent 40%), var(--surface);
    border: 1px solid var(--border); border-radius: 16px; padding: 20px;
    transition: border-color .2s;
  }
  .gse .tab {
    background: transparent; border:none; color: var(--muted);
    font-family: var(--mono); font-size: 12px; letter-spacing:.1em; text-transform:uppercase;
    padding: 14px 4px; cursor:pointer; border-bottom: 2px solid transparent; font-weight:600;
  }
  .gse .tab.active { color: var(--text); border-bottom-color: var(--accent); }
  .gse .tab:hover { color: var(--text); }
  .gse .badge {
    font-family: var(--mono); font-size: 11px; letter-spacing:.08em; text-transform:uppercase;
    padding: 4px 9px; border-radius: 999px; font-weight:600; border: 1px solid currentColor; display:inline-block;
  }
  .gse .toast {
    position: fixed; bottom: 22px; left: 50%; transform: translateX(-50%);
    background: var(--surface2); border:1px solid var(--border); color: var(--text);
    padding: 12px 18px; border-radius: 10px; font-size:14px; z-index:70;
    box-shadow: 0 12px 40px rgba(0,0,0,.5); animation: rise .2s ease; max-width: 90vw;
  }
  @keyframes rise { from { opacity:0; transform: translate(-50%,8px);} to {opacity:1;} }
  .gse .row { display:flex; gap:12px; flex-wrap:wrap; }
  .gse .grid2 { display:grid; grid-template-columns:1fr 1fr; gap:14px; }
  @media (max-width:560px){ .gse .grid2 { grid-template-columns:1fr; } }
  .gse .logo {
    width:42px; height:42px; border-radius:10px; display:grid; place-items:center;
    background: linear-gradient(135deg, var(--accent), #9C6CFF);
    font-family: var(--mono); font-weight:700; color:#fff; letter-spacing:.04em; font-size:15px; flex-shrink:0;
  }
  .gse code.pw {
    font-family: var(--mono); font-size: 13px; background: var(--bg);
    border:1px solid var(--border); border-radius:6px; padding:3px 8px; word-break:break-all;
  }

  /* ----- Insigne GSE animé ----- */
  .gse .glogo { flex-shrink:0; filter: drop-shadow(0 0 7px rgba(108,140,255,.4)); }
  .gse .gring1 { transform-origin: 24px 24px; animation: gspin 16s linear infinite; }
  .gse .gring2 { transform-origin: 24px 24px; animation: gspin 26s linear infinite reverse; }
  .gse .gpulse { animation: gpulse 3.2s ease-in-out infinite; }
  @keyframes gspin { to { transform: rotate(360deg); } }
  @keyframes gpulse { 0%,100% { opacity:.3; } 50% { opacity:.85; } }

  /* ----- Profil / menu ----- */
  .gse .profile {
    display:flex; align-items:center; gap:10px; cursor:pointer; border:1px solid transparent;
    border-radius:10px; padding:6px 10px; transition: background .15s, border-color .15s; background:transparent;
    font-family:var(--sans); color:var(--text);
  }
  .gse .profile:hover { background:var(--surface2); border-color:var(--border); }
  .gse .avatar {
    width:34px; height:34px; border-radius:50%; overflow:hidden; flex-shrink:0;
    display:grid; place-items:center; font-weight:700; font-size:14px; color:#fff;
    background: linear-gradient(135deg, var(--accent), #9C6CFF);
  }
  .gse .avatar img { width:100%; height:100%; object-fit:cover; display:block; }
  .gse .avatar.lg { width:84px; height:84px; font-size:32px; }
  .gse .dropdown {
    position:absolute; right:0; top:calc(100% + 8px); z-index:40; width:230px;
    background:var(--surface); border:1px solid var(--border); border-radius:12px; padding:8px;
    box-shadow:0 18px 50px rgba(0,0,0,.5); animation: rise .15s ease;
  }
  .gse .ditem {
    display:flex; align-items:center; gap:10px; width:100%; text-align:left;
    background:transparent; border:none; color:var(--text); font-size:14px; font-weight:500;
    padding:10px 10px; border-radius:8px; cursor:pointer; font-family:var(--sans);
  }
  .gse .ditem:hover { background:var(--surface2); }
  .gse .dsep { height:1px; background:var(--border); margin:6px 4px; }
  .gse .langrow { display:flex; gap:6px; padding:4px 10px 8px; }
  .gse .langbtn {
    flex:1; border:1px solid var(--border); background:transparent; color:var(--muted);
    border-radius:7px; padding:6px 0; font-size:12.5px; font-weight:700; cursor:pointer; font-family:var(--mono);
  }
  .gse .langbtn.on { color:#fff; background:var(--accent); border-color:var(--accent); }

  /* ----- Grille horaire ----- */
  .gse .tgrid-wrap { overflow-x:auto; border:1px solid var(--border); border-radius:14px; background:var(--surface); }
  .gse .tgrid { display:grid; grid-template-columns:58px repeat(7, minmax(116px,1fr)); min-width:760px; }
  .gse .tcorner { background:var(--surface2); border-bottom:1px solid var(--border); position:sticky; left:0; z-index:2; }
  .gse .thead { background:var(--surface2); border-bottom:1px solid var(--border); border-left:1px solid var(--border); padding:9px 4px; text-align:center; }
  .gse .thead .hn { font-family:var(--mono); font-size:10.5px; letter-spacing:.1em; text-transform:uppercase; color:var(--muted); }
  .gse .thead .hd { font-weight:700; font-size:15px; }
  .gse .thead.today .hn, .gse .thead.today .hd { color:var(--accent); }
  .gse .tslot {
    font-family:var(--mono); font-size:11px; color:var(--muted); padding:6px 6px 0; text-align:right;
    border-top:1px solid var(--border); background:var(--surface); position:sticky; left:0; z-index:1;
  }
  .gse .tcell {
    border-top:1px solid var(--border); border-left:1px solid var(--border);
    min-height:52px; padding:4px; cursor:pointer; transition:background .12s; display:flex; flex-direction:column; gap:4px;
  }
  .gse .tcell:hover { background:var(--surface2); }
  .gse .tcell.today { background:rgba(108,140,255,.045); }
  .gse .tev { background:var(--surface2); border:1px solid var(--border); border-left:3px solid var(--st,var(--border)); border-radius:8px; padding:6px 7px; }
  .gse .tev .ttype { font-size:12px; font-weight:600; line-height:1.25; }
  .gse .tev .twho { font-size:10.5px; color:var(--muted); margin-top:1px; }
  .gse .tev .tact { display:flex; gap:4px; flex-wrap:wrap; margin-top:5px; }
  .gse .icon-btn {
    border:1px solid var(--border); background:transparent; border-radius:6px;
    width:24px; height:24px; display:grid; place-items:center; cursor:pointer;
    font-size:12px; color:var(--muted); transition: background .12s, color .12s, border-color .12s;
  }
  .gse .icon-btn:hover { background: var(--surface); color: var(--text); }
  .gse .icon-ok:hover { color: var(--ok); border-color: var(--ok); }
  .gse .icon-bad:hover { color: var(--bad); border-color: var(--bad); }

  /* ----- Fiches formations réalisées ----- */
  .gse .fcard { background: var(--surface); border:1px solid var(--border); border-radius:16px; overflow:hidden; }
  .gse .fcard .fhead {
    padding:18px 20px; position:relative;
    background: radial-gradient(120% 140% at 0% 0%, rgba(108,140,255,.16), transparent 60%), var(--surface2);
    border-bottom:1px solid var(--border);
  }
  .gse .fcard .ftype { font-size:18px; font-weight:700; letter-spacing:-.01em; }
  .gse .fcard .fmeta { font-family:var(--mono); font-size:12px; color:var(--muted); margin-top:6px; display:flex; gap:14px; flex-wrap:wrap; }
  .gse .fcard .fmeta b { color: var(--text); font-weight:600; }
  .gse .fcard .fbody { padding:18px 20px; display:grid; gap:16px; }
  .gse .frow .flabel { font-family:var(--mono); font-size:10.5px; letter-spacing:.16em; text-transform:uppercase; color:var(--muted); margin-bottom:5px; }
  .gse .quote { border-left:2px solid var(--accent); padding:2px 0 2px 14px; font-size:14px; line-height:1.6; }
  .gse .chips { display:flex; gap:7px; flex-wrap:wrap; }
  .gse .chip { background: var(--accent-dim); border:1px solid rgba(108,140,255,.3); color:#B9C7FF; border-radius:999px; padding:4px 11px; font-size:12.5px; font-weight:600; }
  .gse .fcard .ffoot { padding:14px 20px; border-top:1px solid var(--border); display:flex; align-items:center; gap:12px; flex-wrap:wrap; background: rgba(0,0,0,.12); }
  .gse .fcard .fsign { font-style:italic; color:var(--muted); font-size:13.5px; flex:1; min-width:140px; }

  /* ----- Documentation ----- */
  .gse .docgrid { display:grid; grid-template-columns:1fr 1fr; gap:16px; }
  @media (max-width:640px){ .gse .docgrid { grid-template-columns:1fr; } }
  .gse .doccard {
    background:var(--surface); border:1px solid var(--border); border-radius:16px; padding:18px;
    display:flex; flex-direction:column; gap:14px; transition: border-color .15s, transform .15s;
  }
  .gse .doccard:hover { border-color:var(--accent); transform: translateY(-2px); }
  .gse .docmono {
    width:52px; height:52px; border-radius:13px; display:grid; place-items:center;
    font-family:var(--mono); font-weight:700; font-size:17px; color:#fff; letter-spacing:.02em;
  }
  .gse .doccard .dt { font-size:16px; font-weight:700; }
  .gse .doccard .dd { font-size:12.5px; color:var(--muted); margin-top:2px; }
  .gse .doccard a.open {
    margin-top:auto; text-decoration:none; text-align:center;
    border:1px solid var(--border); background:var(--surface2); color:var(--text);
    border-radius:8px; padding:9px 14px; font-size:13.5px; font-weight:600; transition: background .15s;
  }
  .gse .doccard a.open:hover { background:#262C3D; }

  /* ----- Mon compte ----- */
  .gse .statbig { font-size:42px; font-weight:800; letter-spacing:-.02em; line-height:1; }
  .gse .statsub { font-size:13px; color:var(--muted); margin-top:6px; }
  .gse .statweek { color:var(--accent); font-weight:700; }
  .gse .lline { display:flex; gap:12px; padding:10px 2px; border-top:1px solid var(--border); align-items:baseline; }
  .gse .lline:first-of-type { border-top:none; }
  .gse .lts { font-family:var(--mono); font-size:11px; color:var(--muted); white-space:nowrap; }
  .gse .lact { font-size:13.5px; }
  .gse .ldet { font-size:12.5px; color:var(--muted); }

  /* ----- Modale ----- */
  .gse .overlay { position:fixed; inset:0; background:rgba(8,10,16,.72); display:grid; place-items:center; z-index:60; padding:18px; }
  .gse .modal { background:var(--surface); border:1px solid var(--border); border-radius:14px; padding:20px; width:100%; max-width:460px; }

  /* ----- Hamburger / layout / sidebar ----- */
  .gse .hamb {
    width:40px; height:40px; border-radius:10px; border:1px solid var(--border); background:var(--surface2);
    display:flex; flex-direction:column; justify-content:center; align-items:center; gap:4px; cursor:pointer; flex-shrink:0;
    transition: background .15s;
  }
  .gse .hamb:hover { background:#262C3D; }
  .gse .hamb span { display:block; width:17px; height:2px; border-radius:2px; background:var(--text); }
  .gse .layout { display:flex; gap:24px; align-items:flex-start; }
  .gse .sidebar {
    width:232px; flex-shrink:0; display:flex; flex-direction:column; gap:16px;
    position:sticky; top:16px; align-self:flex-start; max-height:calc(100vh - 32px); overflow:auto;
    background:var(--surface); border:1px solid var(--border); border-radius:16px; padding:14px;
  }
  .gse .sidebar .cubes { justify-content:center; }
  .gse .cubes { display:flex; gap:10px; }
  .gse .cube {
    width:44px; height:44px; border-radius:12px; border:1px solid var(--border); background:var(--surface2);
    display:grid; place-items:center; color:var(--muted); cursor:pointer;
    transition: background .15s, color .15s, border-color .15s;
  }
  .gse .cube:hover { color:var(--text); border-color:var(--accent); }
  .gse .cube.on { color:#fff; background:var(--accent); border-color:var(--accent); }
  .gse .snav { display:flex; flex-direction:column; gap:3px; }
  .gse .sitem {
    display:flex; align-items:center; gap:11px;
    text-align:left; padding:8px 10px; border-radius:10px; border:none; background:transparent;
    color:var(--muted); font-family:var(--sans); font-size:13.5px; font-weight:600; cursor:pointer;
    transition: background .12s, color .12s;
  }
  .gse .sitem:hover { background:var(--surface2); color:var(--text); }
  .gse .sitem.active { background:var(--accent-dim); color:var(--text); box-shadow: inset 3px 0 0 var(--accent); }
  .gse .sitem::after { content:"›"; margin-left:auto; color:transparent; transition:color .15s; }
  .gse .sitem:hover::after, .gse .sitem.active::after { color:var(--muted); }
  .gse .sicon {
    width:30px; height:30px; border-radius:8px; flex-shrink:0;
    background:rgba(108,140,255,.13); color:#9DB1FF;
    display:grid; place-items:center; transition: background .12s, color .12s;
  }
  .gse .sitem:hover .sicon { color:#C3CFFF; }
  .gse .sitem.active .sicon { background:var(--accent); color:#fff; }
  .gse .ssep { height:1px; background:var(--border); margin:8px 6px; }
  .gse .scrim { position:fixed; inset:0; background:rgba(8,10,16,.6); z-index:54; display:none; }
  @media (max-width:820px){
    .gse .scrim { display:block; }
    .gse .sidebar {
      position:fixed; left:0; top:0; bottom:0; width:242px; z-index:56;
      background:var(--surface); border-right:1px solid var(--border); padding:18px; overflow:auto;
    }
  }

  /* ----- Accueil ----- */
  .gse .hero {
    border:1px solid var(--border); border-radius:18px; padding:26px 24px;
    background: radial-gradient(130% 160% at 0% 0%, rgba(108,140,255,.2), transparent 55%),
                radial-gradient(120% 150% at 100% 100%, rgba(156,108,255,.12), transparent 55%), var(--surface);
    display:flex; gap:18px; align-items:center; flex-wrap:wrap;
  }
  .gse .hero .ht { font-size:24px; font-weight:800; letter-spacing:-.015em; }
  .gse .hero .hd2 { color:var(--muted); font-size:13.5px; margin-top:4px; text-transform:capitalize; }
  .gse .pendban {
    border:1px solid rgba(245,178,61,.4); background:rgba(245,178,61,.08); border-radius:13px;
    padding:14px 18px; display:flex; align-items:center; gap:12px; flex-wrap:wrap;
  }
  .gse .qtiles { display:grid; grid-template-columns:repeat(auto-fit, minmax(155px, 1fr)); gap:12px; }
  @media (max-width:640px){ .gse .qtiles { grid-template-columns:1fr; } }
  .gse .qtile {
    border:1px solid var(--border); background:var(--surface); border-radius:13px; padding:16px;
    text-align:left; cursor:pointer; color:var(--text); font-family:var(--sans);
    transition: border-color .15s, transform .15s;
  }
  .gse .qtile:hover { border-color:var(--accent); transform:translateY(-2px); }
  .gse .qtile .qi { font-size:20px; }
  .gse .qtile .qn { font-weight:700; font-size:14px; margin-top:8px; }

  /* ----- Infos de la semaine ----- */
  .gse .winfo {
    border:1px solid rgba(108,140,255,.35); border-radius:16px; padding:18px 20px;
    background: linear-gradient(180deg, rgba(108,140,255,.09), rgba(108,140,255,.02)), var(--surface);
  }
  .gse .winfo .wtitle { display:flex; align-items:center; gap:10px; margin-bottom:12px; }
  .gse .winfo .wtxt { font-size:14.5px; line-height:1.75; white-space:pre-wrap; }
  .gse .winfo .wmeta { font-family:var(--mono); font-size:11px; color:var(--muted); margin-top:12px; }

  /* ----- Rapports ----- */
  .gse .shead {
    font-family:var(--mono); font-size:10px; letter-spacing:.16em; text-transform:uppercase;
    color:var(--muted); padding:12px 10px 4px;
  }
  .gse .rcats { display:grid; grid-template-columns:repeat(auto-fit, minmax(210px, 1fr)); gap:16px; }
  .gse .rcat {
    background:var(--surface); border:1px solid var(--border); border-radius:16px; padding:18px;
    cursor:pointer; text-align:left; font-family:var(--sans); color:var(--text);
    transition:border-color .15s, transform .15s; position:relative; overflow:hidden;
  }
  .gse .rcat:hover { border-color:var(--rc, var(--accent)); transform:translateY(-2px); }
  .gse .rcat::after {
    content:""; position:absolute; left:0; right:0; bottom:0; height:3px;
    background:var(--rc, var(--accent)); opacity:.85;
  }
  .gse .rcat .rcode {
    width:40px; height:40px; border-radius:10px; display:grid; place-items:center;
    font-family:var(--mono); font-weight:800; font-size:13px; color:#fff; background:var(--rc, var(--accent));
  }
  .gse .rcat .rlabel { font-family:var(--mono); font-size:11px; letter-spacing:.14em; text-transform:uppercase; color:var(--muted); margin-top:14px; }
  .gse .rcat .rcount { font-size:34px; font-weight:800; letter-spacing:-.02em; margin-top:4px; }
  .gse .rrow {
    display:flex; align-items:center; gap:14px; padding:13px 16px;
    border-top:1px solid var(--border); cursor:pointer; transition:background .12s;
  }
  .gse .rrow:hover { background:var(--surface2); }
  .gse .rrow:first-of-type { border-top:none; }
  .gse .rnum { font-family:var(--mono); font-size:11px; color:var(--muted); flex-shrink:0; }
  .gse .proofgrid { display:flex; gap:10px; flex-wrap:wrap; }
  .gse .proofthumb { position:relative; width:96px; height:72px; border-radius:8px; overflow:hidden; border:1px solid var(--border); }
  .gse .proofthumb img { width:100%; height:100%; object-fit:cover; display:block; }
  .gse .proofthumb button {
    position:absolute; top:3px; right:3px; width:20px; height:20px; border-radius:5px;
    background:rgba(0,0,0,.65); border:none; color:#fff; cursor:pointer; font-size:11px; line-height:1;
  }
  .gse .rimgs { display:grid; grid-template-columns:repeat(auto-fill, minmax(220px,1fr)); gap:12px; }
  .gse .rimgs img { width:100%; border-radius:10px; border:1px solid var(--border); display:block; }

  /* ----- Lifting visuel ----- */
  .gse .hbtn {
    width:38px; height:38px; border-radius:10px; border:1px solid var(--border); background:var(--surface2);
    display:grid; place-items:center; color:var(--muted); cursor:pointer; position:relative;
    transition: color .15s, border-color .15s, background .15s; flex-shrink:0;
  }
  .gse .hbtn:hover { color:var(--text); border-color:var(--accent); }
  .gse .hdot {
    position:absolute; top:-4px; right:-4px; min-width:16px; height:16px; border-radius:999px;
    background:var(--bad); color:#fff; font-size:10px; font-weight:800; display:grid; place-items:center; padding:0 4px;
    border:2px solid var(--surface); box-sizing:content-box;
  }
  .gse .ph-pill {
    display:inline-flex; align-items:center; gap:7px; font-family:var(--mono); font-size:10px;
    letter-spacing:.18em; text-transform:uppercase; color:var(--muted);
    background:var(--surface2); border:1px solid var(--border); border-radius:999px; padding:5px 13px;
  }
  .gse .ph-pill::before { content:""; width:6px; height:6px; border-radius:50%; background:var(--accent); }
  .gse .ph-title {
    font-size:clamp(22px, 3vw, 30px); font-weight:800; letter-spacing:.12em; text-transform:uppercase;
    color:#9FB4FF; margin-top:10px;
  }
  .gse .ph-sub { color:var(--muted); font-size:13.5px; margin-top:6px; max-width:620px; line-height:1.7; }
  .gse .stchip {
    font-size:11px; font-weight:800; letter-spacing:.06em; text-transform:uppercase;
    padding:5px 13px; border-radius:999px; color:#fff; flex-shrink:0;
  }
  .gse .stattile {
    background:var(--surface); border:1px solid var(--border); border-radius:13px; padding:12px 16px; min-width:120px;
    border-bottom:2px solid var(--sc, var(--border));
  }
  .gse .stattile .sv { font-size:22px; font-weight:800; font-family:var(--mono); }
  .gse .stattile .sl { font-family:var(--mono); font-size:10px; letter-spacing:.14em; text-transform:uppercase; color:var(--muted); margin-top:2px; }
  .gse .tabchip {
    display:flex; align-items:center; gap:8px; padding:9px 16px; border-radius:10px;
    border:1px solid var(--border); background:var(--surface); color:var(--muted);
    font-weight:700; font-size:13px; cursor:pointer; font-family:var(--sans);
  }
  .gse .tabchip.on { color:var(--text); border-color:var(--accent); background:var(--accent-dim); }
  .gse .entid {
    font-family:var(--mono); font-size:11px; font-weight:700; letter-spacing:.08em;
    background:rgba(108,140,255,.12); color:#9DB1FF; border:1px solid rgba(108,140,255,.3);
    border-radius:7px; padding:3px 9px; flex-shrink:0;
  }
  .gse .sx-section { font-family:var(--mono); font-size:10px; letter-spacing:.16em; text-transform:uppercase; color:var(--muted); padding:12px 4px 6px; }
  .gse .sx-row {
    display:flex; align-items:center; gap:10px; padding:9px 12px; border-radius:9px;
    cursor:pointer; font-size:13.5px;
  }
  .gse .sx-row:hover { background:var(--surface2); }
  .gse .ndrop {
    position:absolute; right:0; top:calc(100% + 8px); z-index:45; width:320px; max-height:380px; overflow:auto;
    background:var(--surface); border:1px solid var(--border); border-radius:12px; padding:8px;
    box-shadow:0 18px 50px rgba(0,0,0,.5); animation: rise .15s ease;
  }
  .gse .nrow { display:flex; gap:10px; padding:10px; border-radius:8px; cursor:pointer; align-items:flex-start; }
  .gse .nrow:hover { background:var(--surface2); }
  .gse .dcard2 {
    background:#1A1C24; border:1px solid var(--border); border-radius:14px; padding:16px 18px;
  }
  .gse .dcard2 .djoin2 {
    background:#23A559; color:#fff; border:none; border-radius:8px; padding:11px 22px;
    font-size:14px; font-weight:700; text-decoration:none; display:inline-block; transition:background .15s;
  }
  .gse .dcard2 .djoin2:hover { background:#1E9150; }

  .gse .gchip {
    width:26px; height:26px; border-radius:7px; border:1px solid var(--border); background:var(--surface2);
    display:grid; place-items:center; font-size:13px; cursor:pointer; opacity:.4; transition:all .15s; padding:0;
  }
  .gse .gchip.on { opacity:1; background:var(--surface); }
  .gse .gchip:hover { opacity:1; }
  /* ----- Carte Discord ----- */
  .gse .dcard {
    display:flex; align-items:center; gap:16px; flex-wrap:wrap;
    border:1px solid rgba(88,101,242,.45); border-radius:16px; padding:16px 20px;
    background: linear-gradient(135deg, rgba(88,101,242,.16), rgba(88,101,242,.03)), var(--surface);
  }
  .gse .dicon {
    width:54px; height:54px; border-radius:16px; overflow:hidden; flex-shrink:0;
    background:#5865F2; display:grid; place-items:center; color:#fff;
  }
  .gse .dicon img { width:100%; height:100%; object-fit:cover; display:block; }
  .gse a.djoin {
    background:#5865F2; border:1px solid #5865F2; color:#fff; text-decoration:none;
    border-radius:8px; padding:10px 18px; font-size:14px; font-weight:700; transition: background .15s;
  }
  .gse a.djoin:hover { background:#4752C4; }
  .gse .ddot { display:inline-block; width:8px; height:8px; border-radius:50%; margin-right:5px; }
  .gse select.roleselect { width:auto; padding:6px 10px; font-size:12.5px; }

  /* ----- Hiérarchie ----- */
  .gse .tier {
    margin:0 auto; border:1px solid var(--border); border-top:3px solid var(--tc,var(--border));
    background:var(--surface); border-radius:14px; padding:16px 18px; width:100%;
  }
  .gse .tier .tt { display:flex; align-items:baseline; gap:10px; margin-bottom:12px; }
  .gse .tier .members { display:flex; gap:10px; flex-wrap:wrap; justify-content:center; }
  .gse .member {
    display:flex; align-items:center; gap:8px; background:var(--surface2);
    border:1px solid var(--border); border-radius:999px; padding:5px 13px 5px 6px; font-size:13.5px; font-weight:600;
  }
  .gse .member .avatar { width:26px; height:26px; font-size:11px; }

  /* ----- Utilisateurs ----- */
  .gse .urow {
    display:flex; align-items:center; justify-content:space-between; gap:12px;
    padding:13px 16px; border-top:1px solid var(--border);
  }
  .gse .urow:first-of-type { border-top:none; }
`;

function Field({ label, children }) {
  return (<div><label>{label}</label>{children}</div>);
}

/* ===================== APP ===================== */
function App() {
  const [lang, setLang] = useState(() => {
    try { return localStorage.getItem("gse:lang") === "en" ? "en" : "fr"; } catch { return "fr"; }
  });
  const [state, setState] = useState(null); // {me, accounts, planning, formations, weekinfo, myLogs, logs?}
  const [booting, setBooting] = useState(true);
  const [view, setView] = useState("home");
  const [toast, setToast] = useState("");
  const [menuOpen, setMenuOpen] = useState(false);
  const [sidebarOpen, setSidebarOpen] = useState(true);
  const [searchOpen, setSearchOpen] = useState(false);
  const [pendingReport, setPendingReport] = useState(null);
  const [pendingRcat, setPendingRcat] = useState(null);
  const menuRef = useRef(null);

  const t = useCallback((k) => (T[lang] && T[lang][k]) || T.fr[k] || k, [lang]);

  const flash = useCallback((m) => {
    setToast(m);
    setTimeout(() => setToast(""), 2400);
  }, []);

  const loadState = useCallback(async () => {
    const r = await api("/api/state");
    if (r.ok) setState(r.data);
    else if (r.status === 401) setState(null);
    else flash(t("server_err"));
    return r;
  }, [flash, t]);

  useEffect(() => {
    (async () => {
      await loadState();
      setBooting(false);
    })();
  }, []);

  useEffect(() => {
    function onDoc(e) {
      if (menuRef.current && !menuRef.current.contains(e.target)) setMenuOpen(false);
    }
    document.addEventListener("mousedown", onDoc);
    return () => document.removeEventListener("mousedown", onDoc);
  }, []);

  const refresh = useCallback(async () => {
    const r = await loadState();
    if (r.ok) flash(t("refreshed"));
  }, [loadState, flash, t]);

  function changeLang(l) {
    setLang(l);
    try { localStorage.setItem("gse:lang", l); } catch {}
  }

  if (booting) {
    return (
      <div className="gse" style={{ display: "grid", placeItems: "center", minHeight: "60vh" }}>
        <style>{CSS}</style>
        <span className="eyebrow">{t("loading")}</span>
      </div>
    );
  }

  if (!state) {
    return <Login t={t} onSuccess={loadState} />;
  }

  const me = state.me;
  const isAdmin = ADMIN_ROLES.includes(me.role);
  const isSupreme = me.role === "supreme";
  const canManage = MANAGER_ROLES.includes(me.role);

  if ((view === "admin" || view === "logs") && !isAdmin) setView("home");

  async function logout() {
    await api("/api/logout", "POST");
    setState(null);
    setView("home");
    setMenuOpen(false);
  }

  function go(v) {
    setView(v);
    try { if (window.innerWidth <= 820) setSidebarOpen(false); } catch {}
  }



  return (
    <div className="gse" style={{ minHeight: "100vh" }}>
      <style>{CSS}</style>
      <div style={{ width: "100%", boxSizing: "border-box", padding: "20px clamp(16px, 2.5vw, 40px) 80px" }}>
        <header style={{ display: "flex", alignItems: "center", gap: 14, marginBottom: 22 }}>
          <button className="hamb" onClick={() => setSidebarOpen((o) => !o)} title={t("menu")} aria-label={t("menu")}>
            <span /><span /><span />
          </button>
          <GseLogo />
          <div style={{ flex: 1, minWidth: 0 }}>
            <div style={{ fontSize: 18, fontWeight: 700 }}>{t("app_title")}</div>
            <div className="eyebrow">{t("app_sub")}</div>
          </div>

          <button className="hbtn" title={t("search_title")} onClick={() => setSearchOpen(true)}><SearchIcon /></button>
          <NotifBell reports={state.reports || []} t={t} lang={lang}
            loadSeen={async () => Number(localStorage.getItem("gse:seen") || 0)}
            saveSeen={async (v) => { try { localStorage.setItem("gse:seen", String(v)); } catch {} }}
            onPickReport={(id) => { setPendingReport(id); go("rapports"); }} />
          <div style={{ position: "relative" }} ref={menuRef}>
            <button className="profile" onClick={() => setMenuOpen((o) => !o)}>
              <span className="avatar">
                {me.avatar ? <img src={me.avatar} alt="" /> : me.pseudo.slice(0, 1).toUpperCase()}
              </span>
              <span style={{ textAlign: "left" }}>
                <span style={{ display: "block", fontSize: 14, fontWeight: 600, lineHeight: 1.15 }}>{me.pseudo}</span>
                <span style={{ display: "block", fontSize: 11, color: ROLE_META[me.role].color, fontFamily: "var(--mono)", textTransform: "uppercase", letterSpacing: ".08em" }}>
                  {t(ROLE_META[me.role].tkey)}
                </span>
              </span>
              <span style={{ color: "var(--muted)", fontSize: 11 }}>▾</span>
            </button>

            {menuOpen && (
              <div className="dropdown">
                <button className="ditem" onClick={() => { go("compte"); setMenuOpen(false); }}>
                  <span>👤</span> {t("menu_account")}
                </button>
                <button className="ditem" onClick={() => { go("docs"); setMenuOpen(false); }}>
                  <span>📚</span> {t("menu_docs")}
                </button>
                <div className="dsep" />
                <div style={{ padding: "6px 10px 2px" }} className="eyebrow">{t("menu_lang")}</div>
                <div className="langrow">
                  <button className={`langbtn ${lang === "fr" ? "on" : ""}`} onClick={() => changeLang("fr")}>FR</button>
                  <button className={`langbtn ${lang === "en" ? "on" : ""}`} onClick={() => changeLang("en")}>EN</button>
                </div>
                <div className="dsep" />
                <button className="ditem" onClick={logout} style={{ color: "var(--bad)" }}>
                  <span>⏻</span> {t("logout")}
                </button>
              </div>
            )}
          </div>
        </header>

        <div className="layout">
          {sidebarOpen && (
            <>
              <div className="scrim" onClick={() => setSidebarOpen(false)} />
              <aside className="sidebar">
                <div className="cubes">
                  <button className={`cube ${view === "home" ? "on" : ""}`} onClick={() => go("home")} title={t("nav_home")}>
                    <HouseIcon />
                  </button>
                  <button className={`cube ${view === "users" ? "on" : ""}`} onClick={() => go("users")} title={t("nav_users")}>
                    <UsersIcon />
                  </button>
                </div>
                <nav className="snav">
                  <button className={`sitem ${view === "home" ? "active" : ""}`} onClick={() => go("home")}>
                    <span className="sicon"><HouseIcon /></span>
                    {t("nav_home")}
                  </button>
                  <div className="shead">{t("sec_formation")}</div>
                  <button className={`sitem ${view === "planning" ? "active" : ""}`} onClick={() => go("planning")}>
                    <span className="sicon"><CalendarIcon /></span>
                    {t("tab_planning")}
                  </button>
                  <button className={`sitem ${view === "realisees" ? "active" : ""}`} onClick={() => go("realisees")}>
                    <span className="sicon"><ClipboardIcon /></span>
                    {t("tab_done")}
                  </button>
                  <button className="sitem" onClick={() => { setPendingRcat("formation"); go("rapports"); }}>
                    <span className="sicon"><FileTextIcon /></span>
                    {t("nav_r_formation")}
                  </button>
                  <button className={`sitem ${view === "team_formation" ? "active" : ""}`} onClick={() => go("team_formation")}>
                    <span className="sicon">🎓</span>
                    {t("team_formation")}
                  </button>
                  <div className="shead">{t("sec_entretien")}</div>
                  <button className={`sitem ${view === "entretiens" ? "active" : ""}`} onClick={() => go("entretiens")}>
                    <span className="sicon"><ChatIcon /></span>
                    {t("ent_title")}
                  </button>
                  <button className="sitem" onClick={() => { setPendingRcat("entretien"); go("rapports"); }}>
                    <span className="sicon"><FileTextIcon /></span>
                    {t("nav_r_entretien")}
                  </button>
                  <button className={`sitem ${view === "team_entretien" ? "active" : ""}`} onClick={() => go("team_entretien")}>
                    <span className="sicon">🤝</span>
                    {t("team_entretien")}
                  </button>
                  <div className="shead">{t("sec_recrutement")}</div>
                  <button className="sitem" onClick={() => { setPendingRcat("recrutement"); go("rapports"); }}>
                    <span className="sicon"><FileTextIcon /></span>
                    {t("nav_r_recrutement")}
                  </button>
                  <button className={`sitem ${view === "team_recruteur" ? "active" : ""}`} onClick={() => go("team_recruteur")}>
                    <span className="sicon">🎯</span>
                    {t("team_recruteur")}
                  </button>
                  <div className="shead">{t("sec_rapports")}</div>
                  <button className={`sitem ${view === "rapports" ? "active" : ""}`} onClick={() => go("rapports")}>
                    <span className="sicon"><FileTextIcon /></span>
                    {t("nav_rapports_all")}
                  </button>
                  <div className="shead">{t("sec_corps")}</div>
                  <button className={`sitem ${view === "users" ? "active" : ""}`} onClick={() => go("users")}>
                    <span className="sicon"><UsersIcon /></span>
                    {t("nav_users")}
                  </button>
                  <button className={`sitem ${view === "docs" ? "active" : ""}`} onClick={() => go("docs")}>
                    <span className="sicon"><BookIcon /></span>
                    {t("tab_docs")}
                  </button>
                  {isAdmin && (
                    <>
                      <div className="ssep" />
                      <button className={`sitem ${view === "admin" ? "active" : ""}`} onClick={() => go("admin")} style={{ color: view === "admin" ? "#C792FF" : undefined }}>
                        <span className="sicon" style={view === "admin" ? { background: "#C792FF", color: "#fff" } : { color: "#C792FF", background: "rgba(199,146,255,.13)" }}><ShieldIcon /></span>
                        {t("tab_admin")}
                      </button>
                      <button className={`sitem ${view === "requetes" ? "active" : ""}`} onClick={() => go("requetes")} style={{ color: view === "requetes" ? "#C792FF" : undefined }}>
                        <span className="sicon" style={view === "requetes" ? { background: "#C792FF", color: "#fff" } : { color: "#C792FF", background: "rgba(199,146,255,.13)" }}><InboxIcon /></span>
                        {t("req_title")}
                      </button>
                      <button className={`sitem ${view === "logs" ? "active" : ""}`} onClick={() => go("logs")} style={{ color: view === "logs" ? "#C792FF" : undefined }}>
                        <span className="sicon" style={view === "logs" ? { background: "#C792FF", color: "#fff" } : { color: "#C792FF", background: "rgba(199,146,255,.13)" }}><ScrollIcon /></span>
                        {t("nav_logs")}
                      </button>
                    </>
                  )}
                </nav>
                <button className="btn btn-ghost" onClick={refresh} style={{ textAlign: "left" }}>↻ {t("refresh")}</button>
              </aside>
            </>
          )}

          <main style={{ flex: 1, minWidth: 0 }}>
            {view === "home" && (
              <Home me={me} state={state} canManage={canManage} isAdmin={isAdmin}
                t={t} lang={lang} go={go} reload={loadState} flash={flash} />
            )}
            {view === "planning" && (
              <Planning me={me} canManage={canManage} planning={state.planning}
                reload={loadState} flash={flash} t={t} lang={lang} />
            )}
            {view === "realisees" && (
              <Realisees me={me} canManage={canManage} formations={state.formations}
                reload={loadState} flash={flash} t={t} />
            )}
            {view === "docs" && <Docs t={t} />}
            {view === "rapports" && (
              <Rapports me={me} canManage={canManage} reports={state.reports || []} templates={state.templates || []}
                reload={loadState} flash={flash} t={t} lang={lang}
                pendingReport={pendingReport} onConsumed={() => setPendingReport(null)}
                pendingCat={pendingRcat} onCatConsumed={() => setPendingRcat(null)} />
            )}
            {view === "entretiens" && (
              <Entretiens me={me} canManage={canManage} entretiens={state.entretiens || []}
                reload={loadState} flash={flash} t={t} lang={lang} />
            )}
            {view === "requetes" && isAdmin && (
              <Requests requests={state.requests || []} reload={loadState} t={t} lang={lang} flash={flash} />
            )}
            {view === "team_formation" && <BranchTeam branch="formation" accounts={state.accounts} t={t} />}
            {view === "team_entretien" && <BranchTeam branch="entretien" accounts={state.accounts} t={t} />}
            {view === "team_recruteur" && <BranchTeam branch="recruteur" accounts={state.accounts} t={t} />}
            {view === "users" && <Users accounts={state.accounts} me={me} reload={loadState} flash={flash} t={t} />}
            {view === "compte" && (
              <Compte me={me} formations={state.formations} myLogs={state.myLogs || []}
                reload={loadState} flash={flash} t={t} lang={lang} />
            )}
            {view === "admin" && isAdmin && (
              <Admin me={me} accounts={state.accounts} reload={loadState} flash={flash} t={t} />
            )}
            {view === "logs" && isAdmin && (
              <LogsView logs={state.logs || []} t={t} lang={lang} isSupreme={isSupreme}
                onPurge={async () => {
                  const r = await api("/api/logs", "DELETE");
                  if (!r.ok) return flash(t("server_err"));
                  await loadState();
                  flash(t("logs_cleared"));
                }} />
            )}
          </main>
        </div>
      </div>

      {searchOpen && (
        <GlobalSearch t={t} onClose={() => setSearchOpen(false)}
          reports={state.reports || []} entretiens={state.entretiens || []}
          formations={state.formations} planning={state.planning} accounts={state.accounts}
          go={(v) => { setSearchOpen(false); go(v); }}

          pages={[
            { id: "home", k: "nav_home", icon: "🏠" },
            { id: "planning", k: "tab_planning", icon: "🗓️" },
            { id: "realisees", k: "tab_done", icon: "✅" },
            { id: "rapports", k: "nav_rapports", icon: "📄" },
            { id: "entretiens", k: "ent_title", icon: "🤝" },
            { id: "team_formation", k: "team_formation", icon: "🎓" },
            { id: "team_entretien", k: "team_entretien", icon: "🤝" },
            { id: "team_recruteur", k: "team_recruteur", icon: "🎯" },
            { id: "docs", k: "tab_docs", icon: "📚" },
            { id: "users", k: "nav_users", icon: "👥" },
            { id: "compte", k: "menu_account", icon: "👤" },
            ...(isAdmin ? [
              { id: "admin", k: "tab_admin", icon: "🛠️" },
              { id: "requetes", k: "req_title", icon: "💬" },
              { id: "logs", k: "nav_logs", icon: "🗒️" },
            ] : []),
          ]}
          onPickReport={(id) => { setSearchOpen(false); setPendingReport(id); go("rapports"); }} />
      )}
      {toast && <div className="toast">{toast}</div>}
    </div>
  );
}

/* ===================== LOGIN ===================== */
function Login({ t, onSuccess }) {
  const [pseudo, setPseudo] = useState("");
  const [pwd, setPwd] = useState("");
  const [err, setErr] = useState("");
  const [busy, setBusy] = useState(false);

  async function submit() {
    if (busy) return;
    setBusy(true);
    const r = await api("/api/login", "POST", { pseudo: pseudo.trim(), password: pwd });
    setBusy(false);
    if (!r.ok) {
      if (r.status === 429) {
        const m = (r.data && r.data.minutes) || 15;
        setErr(t("login_blocked").replace("{min}", m));
      } else {
        setErr(r.status === 401 ? t("login_err") : t("server_err"));
      }
      return;
    }
    await onSuccess();
  }

  return (
    <div className="gse" style={{ minHeight: "100vh", display: "grid", placeItems: "center", padding: 18 }}>
      <style>{CSS}</style>
      <div className="card" style={{ width: "100%", maxWidth: 380 }}>
        <div style={{ display: "flex", alignItems: "center", gap: 12, marginBottom: 22 }}>
          <GseLogo />
          <div>
            <div style={{ fontWeight: 700, fontSize: 16 }}>{t("app_title")}</div>
            <div className="eyebrow">{t("login_title")}</div>
          </div>
        </div>
        <div style={{ display: "grid", gap: 14 }}>
          <Field label={t("pseudo")}>
            <input value={pseudo} onChange={(e) => { setPseudo(e.target.value); setErr(""); }}
              placeholder={t("pseudo_ph")} onKeyDown={(e) => e.key === "Enter" && submit()} />
          </Field>
          <Field label={t("password")}>
            <input type="password" value={pwd} onChange={(e) => { setPwd(e.target.value); setErr(""); }}
              placeholder="••••••••" onKeyDown={(e) => e.key === "Enter" && submit()} />
          </Field>
          {err && <div style={{ color: "var(--bad)", fontSize: 13 }}>{err}</div>}
          <button className="btn btn-primary" onClick={submit} disabled={busy}>{t("login")}</button>
          <p style={{ fontSize: 12, color: "var(--muted)", margin: 0, lineHeight: 1.6 }}>{t("login_hint")}</p>
        </div>
      </div>
    </div>
  );
}

/* ===================== PLANNING ===================== */
function Planning({ me, canManage, planning, reload, flash, t, lang }) {
  const today = new Date();
  const thisMonday = startOfWeek(today);
  const [weekStart, setWeekStart] = useState(thisMonday);

  const [type, setType] = useState(TYPES[0]);
  const [date, setDate] = useState("");
  const [time, setTime] = useState(SLOTS[0]);
  const [formateur, setFormateur] = useState(me.pseudo);

  async function add() {
    if (!date) return flash(t("pick_day"));
    if (!time) return flash(t("pick_slot"));
    if (!formateur.trim()) return flash(t("pick_trainer"));
    const r = await api("/api/planning", "POST", { type, date, time, formateur: formateur.trim() });
    if (!r.ok) return flash(t("server_err"));
    await reload();
    flash(`${t("added_slot")} ${time}`);
  }
  async function setStatus(p, status) {
    const r = await api(`/api/planning/${p.id}/status`, "POST", { status });
    if (!r.ok) return flash(t("server_err"));
    await reload();
  }
  async function remove(p) {
    const r = await api(`/api/planning/${p.id}`, "DELETE");
    if (!r.ok) return flash(t("server_err"));
    await reload();
    flash(t("removed"));
  }

  const days = Array.from({ length: 7 }, (_, i) => addDays(weekStart, i));
  const todayISO = toISO(today);
  const atCurrentWeek = toISO(weekStart) <= toISO(thisMonday);
  const DN = DAY_NAMES[lang] || DAY_NAMES.fr;

  function eventsFor(iso, slot) {
    const h = slot.slice(0, 2);
    return planning.filter((p) => p.date === iso && (p.time || "").slice(0, 2) === h);
  }
  function pickCell(iso, slot) { setDate(iso); setTime(slot); flash(t("cell_picked")); }

  const cells = [<div key="corner" className="tcorner" />];
  days.forEach((d) => {
    const iso = toISO(d);
    cells.push(
      <div key={"h" + iso} className={`thead ${iso === todayISO ? "today" : ""}`}>
        <div className="hn">{DN[(d.getDay() + 6) % 7].slice(0, 3)}</div>
        <div className="hd">{d.getDate()}</div>
      </div>
    );
  });
  SLOTS.forEach((slot) => {
    cells.push(<div key={"s" + slot} className="tslot">{slot}</div>);
    days.forEach((d) => {
      const iso = toISO(d);
      const evs = eventsFor(iso, slot);
      cells.push(
        <div key={iso + slot} className={`tcell ${iso === todayISO ? "today" : ""}`}
          onClick={(e) => { if (e.target === e.currentTarget) pickCell(iso, slot); }}>
          {evs.map((p) => {
            const st = STATUS_META[p.status];
            const canDelete = canManage || p.by === me.pseudo;
            return (
              <div key={p.id} className="tev" style={{ "--st": st.color }}>
                <div className="ttype">{p.type}</div>
                <div className="twho">{p.formateur}</div>
                <div style={{ marginTop: 4 }}>
                  <span className="badge" style={{ color: st.color, fontSize: 9.5, padding: "2px 6px" }}>{t(st.tkey)}</span>
                </div>
                {(canManage || canDelete) && (
                  <div className="tact">
                    {canManage && p.status !== "validated" && (
                      <button className="icon-btn icon-ok" title={t("validate")} onClick={() => setStatus(p, "validated")}>✓</button>
                    )}
                    {canManage && p.status !== "refused" && (
                      <button className="icon-btn icon-bad" title={t("refuse")} onClick={() => setStatus(p, "refused")}>✕</button>
                    )}
                    {canManage && p.status !== "pending" && (
                      <button className="icon-btn" title={t("repend")} onClick={() => setStatus(p, "pending")}>↺</button>
                    )}
                    {canDelete && (
                      <button className="icon-btn icon-bad" title={t("del")} onClick={() => remove(p)}>🗑</button>
                    )}
                  </div>
                )}
              </div>
            );
          })}
        </div>
      );
    });
  });

  return (
    <div style={{ display: "grid", gap: 24 }}>
      <div className="card">
        <div className="eyebrow" style={{ marginBottom: 14 }}>{t("plan_new")}</div>
        <div className="grid2">
          <Field label={t("type")}>
            <select value={type} onChange={(e) => setType(e.target.value)}>
              {TYPES.map((x) => <option key={x}>{x}</option>)}
            </select>
          </Field>
          <Field label={t("day")}>
            <input type="date" value={date} min={todayISO} onChange={(e) => setDate(e.target.value)} />
          </Field>
        </div>
        <div className="grid2" style={{ marginTop: 14 }}>
          <Field label={t("slot")}>
            <select value={time} onChange={(e) => setTime(e.target.value)}>
              {SLOTS.map((s) => <option key={s}>{s}</option>)}
            </select>
          </Field>
          <Field label={t("trainer")}>
            <input value={formateur} onChange={(e) => setFormateur(e.target.value)} />
          </Field>
        </div>
        <div style={{ marginTop: 16 }}>
          <button className="btn btn-primary" onClick={add}>{t("add_plan")}</button>
        </div>
        <p style={{ fontSize: 12, color: "var(--muted)", margin: "12px 0 0", lineHeight: 1.6 }}>{t("plan_tip")}</p>
      </div>

      <div className="row" style={{ alignItems: "center" }}>
        <button className="btn btn-sm" disabled={atCurrentWeek}
          onClick={() => !atCurrentWeek && setWeekStart(addDays(weekStart, -7))}
          style={{ opacity: atCurrentWeek ? 0.4 : 1, cursor: atCurrentWeek ? "not-allowed" : "pointer" }}>
          {t("week_prev")}
        </button>
        <div style={{ flex: 1, textAlign: "center" }}>
          <div className="eyebrow">{t("week_of")}</div>
          <div style={{ fontWeight: 700, fontSize: 16 }}>{weekLabel(weekStart, lang)}</div>
        </div>
        <button className="btn btn-sm" onClick={() => setWeekStart(addDays(weekStart, 7))}>{t("week_next")}</button>
      </div>

      <div className="tgrid-wrap"><div className="tgrid">{cells}</div></div>
    </div>
  );
}

/* ===================== FORMATIONS RÉALISÉES ===================== */
const emptyForm = (pseudo) => ({
  nom: TYPES[0], date: "", debut: "", fin: "", vocal: "",
  formateur: "", commentaire: "", formes: "", cordialement: "Cordialement,",
});

function Realisees({ me, canManage, formations, reload, flash, t }) {
  const [f, setF] = useState(emptyForm(me.pseudo));
  const [copyFallback, setCopyFallback] = useState(null);

  function set(k, v) { setF((p) => ({ ...p, [k]: v })); }

  async function add() {
    if (!f.date) return flash(t("need_date"));
    const r = await api("/api/formations", "POST", { ...f, formateur: f.formateur.trim() });
    if (!r.ok) return flash(t("server_err"));
    setF(emptyForm(me.pseudo));
    await reload();
    flash(t("saved_done"));
  }
  async function remove(x) {
    const r = await api(`/api/formations/${x.id}`, "DELETE");
    if (!r.ok) return flash(t("server_err"));
    await reload();
    flash(t("deleted_done"));
  }

  function report(x) {
    return [
      `**Nom de la formation :** ${x.nom}`,
      `**Date :** ${frDate(x.date)}`,
      `**Heure de début :** ${x.debut || "—"}`,
      `**Heure de fin :** ${x.fin || "—"}`,
      `**Vocal utilisée :** ${x.vocal || "—"}`,
      `**Formateur :** ${x.formateur || "—"}`,
      `**Commentaire :** ${x.commentaire || "—"}`,
      `**GSE Formés :** ${x.formes || "—"}`,
      ``,
      `${x.cordialement || ""}`.trim(),
    ].join("\n");
  }
  async function copy(x) {
    const text = report(x);
    const ok = await copyText(text);
    if (ok) flash(t("copied"));
    else setCopyFallback(text);
  }

  return (
    <div style={{ display: "grid", gap: 24 }}>
      <div className="card">
        <div className="eyebrow" style={{ marginBottom: 14 }}>{t("done_new")}</div>
        <div style={{ display: "grid", gap: 14 }}>
          <div className="grid2">
            <Field label={t("type")}>
              <select value={f.nom} onChange={(e) => set("nom", e.target.value)}>
                {TYPES.map((x) => <option key={x}>{x}</option>)}
              </select>
            </Field>
            <Field label={t("date")}><input type="date" value={f.date} onChange={(e) => set("date", e.target.value)} /></Field>
          </div>
          <div className="grid2">
            <Field label={t("start")}><input type="time" value={f.debut} onChange={(e) => set("debut", e.target.value)} /></Field>
            <Field label={t("end")}><input type="time" value={f.fin} onChange={(e) => set("fin", e.target.value)} /></Field>
          </div>
          <div className="grid2">
            <Field label={t("vocal")}><input value={f.vocal} onChange={(e) => set("vocal", e.target.value)} placeholder={t("vocal_ph")} /></Field>
            <Field label={t("trainer")}><input value={f.formateur} onChange={(e) => set("formateur", e.target.value)} placeholder={t("trainer_ph")} /></Field>
          </div>
          <Field label={t("comment")}><textarea value={f.commentaire} onChange={(e) => set("commentaire", e.target.value)} placeholder={t("comment_ph")} /></Field>
          <Field label={t("trained")}><textarea value={f.formes} onChange={(e) => set("formes", e.target.value)} placeholder={t("trained_ph")} /></Field>
          <Field label={t("signoff")}><input value={f.cordialement} onChange={(e) => set("cordialement", e.target.value)} /></Field>
          <div><button className="btn btn-primary" onClick={add}>{t("save_done")}</button></div>
        </div>
      </div>

      <div>
        <div className="eyebrow" style={{ marginBottom: 14 }}>{t("history")} · {formations.length}</div>
        {formations.length === 0 ? (
          <div className="card" style={{ textAlign: "center", color: "var(--muted)" }}>{t("empty_done")}</div>
        ) : (
          <div style={{ display: "grid", gap: 16 }}>
            {formations.map((x) => {
              const canDelete = canManage || x.by === me.pseudo;
              const formes = (x.formes || "").split(/[\n,]/).map((s) => s.trim()).filter(Boolean);
              return (
                <div key={x.id} className="fcard">
                  <div className="fhead">
                    <div className="ftype">{x.nom}</div>
                    <div className="fmeta">
                      <span><b>{frDate(x.date)}</b></span>
                      <span>{x.debut || "—"} → {x.fin || "—"}</span>
                      {x.vocal && <span>🔊 {x.vocal}</span>}
                      <span>{t("by")} <b>{x.formateur}</b></span>
                    </div>
                  </div>
                  <div className="fbody">
                    {x.commentaire && (
                      <div className="frow">
                        <div className="flabel">{t("comment")}</div>
                        <div className="quote">{x.commentaire}</div>
                      </div>
                    )}
                    {formes.length > 0 && (
                      <div className="frow">
                        <div className="flabel">{t("trained")}</div>
                        <div className="chips">{formes.map((p, i) => <span key={i} className="chip">{p}</span>)}</div>
                      </div>
                    )}
                  </div>
                  <div className="ffoot">
                    <span className="fsign">{x.cordialement || "Cordialement,"}</span>
                    <button className="btn btn-sm" onClick={() => copy(x)}>{t("copy_report")}</button>
                    {canDelete && (
                      <button className="btn btn-ghost btn-sm" onClick={() => remove(x)} style={{ color: "var(--bad)" }}>{t("del")}</button>
                    )}
                  </div>
                </div>
              );
            })}
          </div>
        )}
      </div>

      {copyFallback !== null && (
        <div className="overlay" onClick={() => setCopyFallback(null)}>
          <div className="modal" onClick={(e) => e.stopPropagation()}>
            <div className="eyebrow" style={{ marginBottom: 10 }}>{t("copy_manual_title")}</div>
            <p style={{ fontSize: 13, color: "var(--muted)", margin: "0 0 12px" }}>{t("copy_manual_hint")}</p>
            <textarea readOnly value={copyFallback} onFocus={(e) => e.target.select()}
              style={{ minHeight: 200, fontFamily: "var(--mono)", fontSize: 12.5 }} />
            <div className="row" style={{ marginTop: 14, justifyContent: "flex-end" }}>
              <button className="btn btn-sm" onClick={async () => {
                const ok = await copyText(copyFallback);
                if (ok) { flash(t("copied_short")); setCopyFallback(null); }
                else flash(t("copy_manual_do"));
              }}>{t("copy_retry")}</button>
              <button className="btn btn-primary btn-sm" onClick={() => setCopyFallback(null)}>{t("close")}</button>
            </div>
          </div>
        </div>
      )}
    </div>
  );
}

/* ===================== DOCUMENTATION ===================== */
function Docs({ t }) {
  return (
    <div style={{ display: "grid", gap: 20 }}>
      <PageHead crumb={t("docs_title")} title={t("docs_title")} sub={t("docs_sub")} />
      <div className="docgrid">
        {DOCS.map((d) => (
          <div key={d.code} className="doccard">
            <div style={{ display: "flex", gap: 14, alignItems: "center" }}>
              <div className="docmono" style={{ background: d.grad }}>{d.code}</div>
              <div>
                <div className="dt">{d.title}</div>
                <div className="dd">{d.type}</div>
              </div>
            </div>
            <a className="open" href={d.url} target="_blank" rel="noopener noreferrer">
              {t("open_doc")} ↗
            </a>
          </div>
        ))}
      </div>
    </div>
  );
}

/* ===================== ACCUEIL ===================== */
function Home({ me, state, canManage, isAdmin, t, lang, go, reload, flash }) {
  const today = new Date();
  const todayISO = toISO(today);
  let dateStr = "";
  try {
    dateStr = today.toLocaleDateString(lang === "en" ? "en-GB" : "fr-FR", { weekday: "long", day: "numeric", month: "long", year: "numeric" });
  } catch {}

  const upcoming = state.planning
    .filter((p) => p.date >= todayISO && p.status !== "refused")
    .sort((a, b) => (a.date + (a.time || "")).localeCompare(b.date + (b.time || "")))
    .slice(0, 4);
  const pending = state.planning.filter((p) => p.status === "pending").length;

  return (
    <div style={{ display: "grid", gap: 20 }}>
      <div className="hero">
        <span className="avatar lg">
          {me.avatar ? <img src={me.avatar} alt="" /> : me.pseudo.slice(0, 1).toUpperCase()}
        </span>
        <div style={{ flex: 1, minWidth: 200 }}>
          <div className="ht">{t("home_hello")}, {me.pseudo} 👋</div>
          <div className="hd2">{dateStr}</div>
        </div>
        <span className="badge" style={{ color: ROLE_META[me.role].color }}>{t(ROLE_META[me.role].tkey)}</span>
      </div>

      <WeekInfo weekinfo={state.weekinfo} isAdmin={isAdmin} reload={reload} flash={flash} t={t} lang={lang} />

      {canManage && pending > 0 && (
        <div className="pendban">
          <span style={{ fontSize: 18 }}>⏳</span>
          <span style={{ flex: 1, fontSize: 14 }}>
            <b style={{ color: "var(--warn)" }}>{pending}</b> {t("home_pending")}
          </span>
          <button className="btn btn-sm" onClick={() => go("planning")}>{t("home_go_plan")}</button>
        </div>
      )}

      <div className="card">
        <div className="eyebrow" style={{ marginBottom: 14 }}>{t("home_upcoming")}</div>
        {upcoming.length === 0 ? (
          <div style={{ color: "var(--muted)", fontSize: 13 }}>{t("home_none")}</div>
        ) : (
          <div>
            {upcoming.map((p) => {
              const st = STATUS_META[p.status];
              return (
                <div key={p.id} className="lline" style={{ alignItems: "center" }}>
                  <span className="lts">{frDate(p.date)} {p.time || ""}</span>
                  <span className="lact" style={{ fontWeight: 600, flex: 1 }}>{p.type}</span>
                  <span className="ldet">{p.formateur}</span>
                  <span className="badge" style={{ color: st.color, fontSize: 9.5, padding: "2px 7px" }}>{t(st.tkey)}</span>
                </div>
              );
            })}
          </div>
        )}
      </div>

      <div>
        <div className="eyebrow" style={{ marginBottom: 12 }}>{t("home_quick")}</div>
        <QuickTiles t={t} go={go} />
      </div>


      {(state.reports || []).filter((r) => r.notify).length > 0 && (
        <div className="card">
          <div className="eyebrow" style={{ marginBottom: 14 }}>📣 {t("notif_title")}</div>
          {(state.reports || []).filter((r) => r.notify).sort((a, b) => b.createdAt - a.createdAt).slice(0, 3).map((r) => {
            const rc = RCATS[r.category] || RCATS.formation;
            return (
              <div key={r.id} className="lline" style={{ alignItems: "center", cursor: "pointer" }} onClick={() => go("rapports")}>
                <span className="lts">{frDate(r.date)}</span>
                <span className="lact" style={{ fontWeight: 600, flex: 1 }}>{r.name}</span>
                <span className="ldet">{r.by}</span>
                <span className="badge" style={{ color: rc.color, fontSize: 9.5, padding: "2px 7px" }}>{t(rc.tkey)}</span>
              </div>
            );
          })}
        </div>
      )}

      <DiscordCard t={t} />

      <RequestBox flash={flash} t={t} reload={reload} />
    </div>
  );
}

/* ===================== INFOS DE LA SEMAINE ===================== */
function WeekInfo({ weekinfo, isAdmin, reload, flash, t, lang }) {
  const [editing, setEditing] = useState(false);
  const [draft, setDraft] = useState("");

  function startEdit() {
    setDraft((weekinfo && weekinfo.text) || "");
    setEditing(true);
  }
  async function save() {
    const r = await api("/api/weekinfo", "PUT", { text: draft.trim() });
    if (!r.ok) return flash(t("server_err"));
    setEditing(false);
    await reload();
    flash(t("info_updated"));
  }

  const hasText = weekinfo && weekinfo.text;

  return (
    <div className="winfo">
      <div className="wtitle">
        <span style={{ fontSize: 17 }}>📌</span>
        <span style={{ fontWeight: 800, fontSize: 16, letterSpacing: "-.01em", flex: 1 }}>{t("home_info")}</span>
        {isAdmin && !editing && (
          <button className="btn btn-ghost btn-sm" onClick={startEdit}>{t("info_edit")}</button>
        )}
      </div>

      {editing ? (
        <div style={{ display: "grid", gap: 12 }}>
          <textarea value={draft} onChange={(e) => setDraft(e.target.value)}
            placeholder={t("info_ph")} style={{ minHeight: 130, fontSize: 14 }} />
          <div className="row" style={{ justifyContent: "flex-end" }}>
            <button className="btn btn-ghost btn-sm" onClick={() => setEditing(false)}>{t("info_cancel")}</button>
            <button className="btn btn-primary btn-sm" onClick={save}>{t("info_save")}</button>
          </div>
        </div>
      ) : hasText ? (
        <>
          <div className="wtxt">{weekinfo.text}</div>
          <div className="wmeta">
            {t("updated_on")} {fmtTs(weekinfo.updatedAt, lang)} · {t("by")} {weekinfo.updatedBy}
          </div>
        </>
      ) : (
        <div style={{ color: "var(--muted)", fontSize: 13.5 }}>{t("info_empty")}</div>
      )}
    </div>
  );
}

/* ===================== UTILISATEURS ===================== */
function Users({ accounts, me, reload, flash, t }) {
  const myRank = RANK[me.role] !== undefined ? RANK[me.role] : 99;
  const isAdminU = me.role === "admin";
  const canEditRoles = MANAGER_ROLES.includes(me.role);
  const list = [...accounts].sort(
    (a, b) => ((RANK[a.role] ?? 9) - (RANK[b.role] ?? 9)) || a.pseudo.localeCompare(b.pseudo)
  );

  function optionsFor(target) {
    if (!canEditRoles) return null;
    if (target.pseudo === me.pseudo) return null;
    const SUPme = me.role === "supreme";
    const ADMme = ADMIN_ROLES.includes(me.role);
    if (SUPme) return ROLES_ORDER;
    if (ADMme) {
      if (target.role === "supreme") return null;
      return ROLES_ORDER.filter((r) => r !== "supreme");
    }
    if ((RANK[target.role] ?? 9) <= myRank) return null;
    return ROLES_ORDER.filter((r) => RANK[r] > myRank);
  }

  function canGrade(target) {
    if (!canEditRoles) return false;
    const SUPme = me.role === "supreme";
    const ADMme = ADMIN_ROLES.includes(me.role);
    if (SUPme) return true;
    if (ADMme) return target.role !== "supreme";
    return (RANK[target.role] ?? 9) > myRank;
  }

  async function toggleGrade(target, g) {
    const cur = Array.isArray(target.grades) ? target.grades : [];
    const grades = cur.includes(g) ? cur.filter((x) => x !== g) : [...cur, g];
    const r = await api(`/api/grades/${target.id}`, "PUT", { grades });
    if (!r.ok) return flash(t("server_err"));
    await reload();
  }

  async function setRolesFor(target, roles) {
    if (!roles.length) return flash(t("adm_need_role"));
    const r = await api(`/api/roles/${target.id}`, "PUT", { roles });
    if (!r.ok) {
      if (r.data && r.data.error === "lastadmin") return flash(t("adm_last_admin"));
      return flash(t("server_err"));
    }
    await reload();
    flash(t("role_changed"));
  }

  return (
    <div style={{ display: "grid", gap: 20 }}>
      <PageHead crumb={t("users_title")} title={t("users_title")} />
      <div className="card" style={{ padding: 0, overflow: "hidden" }}>
        {list.map((a) => {
          const opts = optionsFor(a);
          return (
            <div key={a.id} className="urow">
              <span style={{ display: "flex", alignItems: "center", gap: 12 }}>
                <span className="avatar">
                  {a.avatar ? <img src={a.avatar} alt="" /> : a.pseudo.slice(0, 1).toUpperCase()}
                </span>
                <span style={{ fontWeight: 600, fontSize: 14.5 }}>{a.pseudo}</span>
              </span>
              <span style={{ display: "flex", alignItems: "center", gap: 10, flexWrap: "wrap", justifyContent: "flex-end" }}>
                {opts ? (
                  <RolePicker value={rolesOf(a)} onChange={(roles) => setRolesFor(a, roles)} options={opts} t={t} />
                ) : (
                  rolesOf(a).map((r) => (
                    <span key={r} className="badge" style={{ color: (ROLE_META[r] || {}).color }}>GSE - {t((ROLE_META[r] || ROLE_META.formateur).tkey)}</span>
                  ))
                )}
                {canGrade(a) ? (
                  <span style={{ display: "flex", gap: 5 }}>
                    {Object.keys(BRANCHES).map((g) => {
                      const onb = (a.grades || []).includes(g);
                      return (
                        <button key={g} className={`gchip ${onb ? "on" : ""}`} title={t(BRANCHES[g].tkey)}
                          style={onb ? { borderColor: BRANCHES[g].color } : {}}
                          onClick={() => toggleGrade(a, g)}>{BRANCHES[g].icon}</button>
                      );
                    })}
                  </span>
                ) : (a.grades || []).length > 0 ? (
                  <span style={{ display: "flex", gap: 4 }}>
                    {(a.grades || []).map((g) => BRANCHES[g] ? (
                      <span key={g} title={t(BRANCHES[g].tkey)} style={{ fontSize: 13 }}>{BRANCHES[g].icon}</span>
                    ) : null)}
                  </span>
                ) : null}
              </span>
            </div>
          );
        })}
      </div>
    </div>
  );
}

/* ===================== HIÉRARCHIE ===================== */
/* ===================== MON COMPTE ===================== */
function Compte({ me, formations, myLogs, reload, flash, t, lang }) {
  const fileRef = useRef(null);
  const [cur, setCur] = useState("");
  const [npw, setNpw] = useState("");
  const [cpw, setCpw] = useState("");

  async function onAvatar(e) {
    const file = e.target.files && e.target.files[0];
    e.target.value = "";
    if (!file) return;
    try {
      const data = await resizeImage(file, 96);
      const r = await api("/api/me/avatar", "PUT", { avatar: data });
      if (!r.ok) return flash(t("server_err"));
      await reload();
      flash(t("avatar_set"));
    } catch { flash(t("avatar_err")); }
  }
  async function removeAvatar() {
    const r = await api("/api/me/avatar", "PUT", { avatar: null });
    if (!r.ok) return flash(t("server_err"));
    await reload();
    flash(t("avatar_removed"));
  }

  async function changePwd() {
    if (npw.length < 6) return flash(t("pwd_short"));
    if (npw !== cpw) return flash(t("pwd_mismatch"));
    const r = await api("/api/me/password", "PUT", { current: cur, next: npw });
    if (!r.ok) {
      if (r.data && r.data.error === "current") return flash(t("pwd_bad_current"));
      return flash(t("server_err"));
    }
    setCur(""); setNpw(""); setCpw("");
    flash(t("pwd_changed"));
  }

  const mine = formations.filter((x) => (x.formateur || "").toLowerCase() === me.pseudo.toLowerCase());
  const monday = startOfWeek(new Date());
  const mondayISO = toISO(monday);
  const sundayISO = toISO(addDays(monday, 6));
  const mineWeek = mine.filter((x) => x.date >= mondayISO && x.date <= sundayISO);
  const recent = [...mine].sort((a, b) => (b.date + (b.debut || "")).localeCompare(a.date + (a.debut || ""))).slice(0, 5);

  return (
    <div style={{ display: "grid", gap: 24 }}>
      <div style={{ fontSize: 22, fontWeight: 800, letterSpacing: "-.01em" }}>{t("acc_title")}</div>

      <div className="card">
        <div style={{ display: "flex", gap: 18, alignItems: "center", flexWrap: "wrap" }}>
          <span className="avatar lg">
            {me.avatar ? <img src={me.avatar} alt="" /> : me.pseudo.slice(0, 1).toUpperCase()}
          </span>
          <div style={{ flex: 1, minWidth: 180 }}>
            <div style={{ fontSize: 18, fontWeight: 700 }}>{me.pseudo}</div>
            <span className="badge" style={{ color: ROLE_META[me.role].color, marginTop: 6 }}>{t(ROLE_META[me.role].tkey)}</span>
            <p style={{ fontSize: 12, color: "var(--muted)", margin: "10px 0 0" }}>{t("acc_avatar_hint")}</p>
          </div>
          <div className="row">
            <input ref={fileRef} type="file" accept="image/*" style={{ display: "none" }} onChange={onAvatar} />
            <button className="btn btn-sm" onClick={() => fileRef.current && fileRef.current.click()}>{t("acc_upload")}</button>
            {me.avatar && <button className="btn btn-ghost btn-sm" onClick={removeAvatar} style={{ color: "var(--bad)" }}>{t("acc_remove")}</button>}
          </div>
        </div>
      </div>

      <div className="grid2">
        <div className="card">
          <div className="eyebrow" style={{ marginBottom: 14 }}>{t("stats")}</div>
          <div className="statbig">{mine.length}</div>
          <div className="statsub">{t("stat_total")}</div>
          <div className="statsub" style={{ marginTop: 12 }}>
            <span className="statweek">{mineWeek.length}</span> {t("stat_week")}
          </div>
        </div>
        <div className="card">
          <div className="eyebrow" style={{ marginBottom: 14 }}>{t("recent")}</div>
          {recent.length === 0 ? (
            <div style={{ color: "var(--muted)", fontSize: 13 }}>{t("nothing")}</div>
          ) : (
            <div>
              {recent.map((x) => (
                <div key={x.id} className="lline">
                  <span className="lts">{frDate(x.date)} {x.debut || ""}</span>
                  <span className="lact" style={{ fontWeight: 600 }}>{x.nom}</span>
                </div>
              ))}
            </div>
          )}
        </div>
      </div>

      <div className="card">
        <div className="eyebrow" style={{ marginBottom: 14 }}>{t("acc_pwd")}</div>
        <div className="grid2">
          <Field label={t("acc_current")}>
            <input type="password" value={cur} onChange={(e) => setCur(e.target.value)} />
          </Field>
          <div />
          <Field label={t("acc_new")}>
            <input type="password" value={npw} onChange={(e) => setNpw(e.target.value)} />
          </Field>
          <Field label={t("acc_confirm")}>
            <input type="password" value={cpw} onChange={(e) => setCpw(e.target.value)} />
          </Field>
        </div>
        <div style={{ marginTop: 16 }}>
          <button className="btn btn-primary" onClick={changePwd}>{t("acc_save")}</button>
        </div>
      </div>

      <div className="card">
        <div className="eyebrow" style={{ marginBottom: 14 }}>{t("activity")}</div>
        {myLogs.length === 0 ? (
          <div style={{ color: "var(--muted)", fontSize: 13 }}>{t("nothing")}</div>
        ) : (
          <div>
            {myLogs.map((l) => (
              <div key={l.id} className="lline">
                <span className="lts">{fmtTs(l.ts, lang)}</span>
                <span>
                  <span className="lact" style={{ fontWeight: 600, color: LOG_COLOR[l.action] || "var(--text)" }}>
                    {t(LOG_LABEL[l.action] || l.action)}
                  </span>
                  {l.detail && <span className="ldet"> — {l.detail}</span>}
                </span>
              </div>
            ))}
          </div>
        )}
      </div>
    </div>
  );
}

/* ===================== ADMIN ===================== */
function Admin({ me, accounts, reload, flash, t }) {
  const [nPseudo, setNPseudo] = useState("");
  const [nPwd, setNPwd] = useState(genPassword());
  const [nRoles, setNRoles] = useState(["formateur"]);
  const SUP = me.role === "supreme";
  const PICK_OPTS = ROLES_ORDER.filter((r) => SUP || r !== "supreme");
  const [editing, setEditing] = useState(null); // {id, pseudo, role, password:""}
  const [showOnce, setShowOnce] = useState(null); // {pseudo, password}

  async function addAccount() {
    const p = nPseudo.trim();
    if (!p) return flash(t("adm_need_pseudo"));
    if (nPwd.length < 6) return flash(t("adm_pwd_short"));
    if (!nRoles.length) return flash(t("adm_need_role"));
    const r = await api("/api/admin/accounts", "POST", { pseudo: p, password: nPwd, roles: nRoles });
    if (!r.ok) {
      if (r.data && r.data.error === "taken") return flash(t("adm_pseudo_taken"));
      return flash(t("server_err"));
    }
    setShowOnce({ pseudo: p, password: nPwd });
    setNPseudo(""); setNPwd(genPassword()); setNRoles(["formateur"]);
    await reload();
    flash(`${t("adm_created")} · ${p}`);
  }

  function startEdit(a) { setEditing({ id: a.id, pseudo: a.pseudo, roles: rolesOf(a), password: "" }); }

  async function saveEdit() {
    const p = editing.pseudo.trim();
    if (!p) return flash(t("adm_need_pseudo"));
    if (editing.password && editing.password.length < 6) return flash(t("adm_pwd_short"));
    if (!editing.roles.length) return flash(t("adm_need_role"));
    const body = { pseudo: p, roles: editing.roles };
    if (editing.password) body.password = editing.password;
    const r = await api(`/api/admin/accounts/${editing.id}`, "PUT", body);
    if (!r.ok) {
      if (r.data && r.data.error === "taken") return flash(t("adm_pseudo_taken"));
      if (r.data && r.data.error === "lastadmin") return flash(t("adm_last_admin"));
      return flash(t("server_err"));
    }
    if (editing.password) setShowOnce({ pseudo: p, password: editing.password });
    setEditing(null);
    await reload();
    flash(t("adm_updated"));
  }

  async function deleteAccount(a) {
    const r = await api(`/api/admin/accounts/${a.id}`, "DELETE");
    if (!r.ok) {
      if (r.data && r.data.error === "self") return flash(t("adm_self_del"));
      if (r.data && r.data.error === "lastadmin") return flash(t("adm_last_admin"));
      return flash(t("server_err"));
    }
    await reload();
    flash(`${t("adm_deleted")} · ${a.pseudo}`);
  }

  return (
    <div style={{ display: "grid", gap: 24 }}>
      <div className="card" style={{ borderColor: "rgba(199,146,255,.35)" }}>
        <div className="eyebrow" style={{ marginBottom: 8, color: "#C792FF" }}>{t("adm_panel")}</div>
        <p style={{ fontSize: 13, color: "var(--muted)", margin: 0 }}>{t("adm_warn")}</p>
      </div>

      <div className="card">
        <div className="eyebrow" style={{ marginBottom: 14 }}>{t("adm_create")}</div>
        <div className="grid2">
          <Field label={t("pseudo")}><input value={nPseudo} onChange={(e) => setNPseudo(e.target.value)} /></Field>
          <Field label={t("adm_roles")}>
            <RolePicker value={nRoles} onChange={setNRoles} options={PICK_OPTS} t={t} />
          </Field>
        </div>
        <div style={{ marginTop: 14 }}>
          <Field label={t("password")}>
            <div className="row" style={{ flexWrap: "nowrap" }}>
              <input value={nPwd} onChange={(e) => setNPwd(e.target.value)} />
              <button className="btn btn-sm" onClick={() => setNPwd(genPassword())} style={{ whiteSpace: "nowrap" }}>{t("adm_gen")}</button>
            </div>
          </Field>
        </div>
        <div style={{ marginTop: 16 }}>
          <button className="btn btn-primary" onClick={addAccount}>{t("adm_do_create")}</button>
        </div>
      </div>

      <div>
        <div className="eyebrow" style={{ marginBottom: 14 }}>{t("adm_accounts")} · {accounts.length}</div>
        <div style={{ display: "grid", gap: 12 }}>
          {accounts.map((a) => {
            const isEditing = editing && editing.id === a.id;
            if (isEditing) {
              return (
                <div key={a.id} className="card" style={{ padding: 18, borderColor: "var(--accent)" }}>
                  <div className="grid2">
                    <Field label={t("pseudo")}><input value={editing.pseudo} onChange={(e) => setEditing({ ...editing, pseudo: e.target.value })} /></Field>
                    <Field label={t("adm_roles")}>
                      <RolePicker value={editing.roles} onChange={(roles) => setEditing({ ...editing, roles })} options={PICK_OPTS} t={t} />
                    </Field>
                  </div>
                  <div style={{ marginTop: 14 }}>
                    <Field label={t("password")}>
                      <div className="row" style={{ flexWrap: "nowrap" }}>
                        <input value={editing.password} placeholder={t("adm_pwd_optional")}
                          onChange={(e) => setEditing({ ...editing, password: e.target.value })} />
                        <button className="btn btn-sm" onClick={() => setEditing({ ...editing, password: genPassword() })} style={{ whiteSpace: "nowrap" }}>{t("adm_gen")}</button>
                      </div>
                    </Field>
                  </div>
                  <div className="row" style={{ marginTop: 16 }}>
                    <button className="btn btn-primary btn-sm" onClick={saveEdit}>{t("adm_save")}</button>
                    <button className="btn btn-ghost btn-sm" onClick={() => setEditing(null)}>{t("adm_cancel")}</button>
                  </div>
                </div>
              );
            }
            return (
              <div key={a.id} className="card" style={{ padding: 16 }}>
                <div style={{ display: "flex", alignItems: "center", gap: 12, flexWrap: "wrap" }}>
                  <span className="avatar">
                    {a.avatar ? <img src={a.avatar} alt="" /> : a.pseudo.slice(0, 1).toUpperCase()}
                  </span>
                  <div style={{ flex: 1, minWidth: 160 }}>
                    <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
                      <span style={{ fontWeight: 700 }}>{a.pseudo}</span>
                      {a.pseudo === me.pseudo && <span style={{ fontSize: 11, color: "var(--muted)" }}>{t("adm_you")}</span>}
                    </div>
                    <div style={{ marginTop: 6 }}>
                      {rolesOf(a).map((r) => (
                        <span key={r} className="badge" style={{ color: (ROLE_META[r] || {}).color }}>{t((ROLE_META[r] || ROLE_META.formateur).tkey)}</span>
                      ))}
                    </div>
                  </div>
                  <div className="row">
                    <button className="btn btn-sm" onClick={() => startEdit(a)}>{t("adm_edit")}</button>
                    <button className="btn btn-bad btn-sm" onClick={() => deleteAccount(a)}>{t("del")}</button>
                  </div>
                </div>
              </div>
            );
          })}
        </div>
      </div>

      {showOnce && (
        <div className="overlay" onClick={() => setShowOnce(null)}>
          <div className="modal" onClick={(e) => e.stopPropagation()}>
            <div className="eyebrow" style={{ marginBottom: 10, color: "#C792FF" }}>{t("adm_pwd_once_title")}</div>
            <p style={{ fontSize: 13, color: "var(--muted)", margin: "0 0 12px" }}>{t("adm_pwd_once")}</p>
            <div style={{ display: "grid", gap: 8 }}>
              <div><span className="eyebrow">{t("pseudo")}</span></div>
              <code className="pw">{showOnce.pseudo}</code>
              <div style={{ marginTop: 6 }}><span className="eyebrow">{t("password")}</span></div>
              <code className="pw">{showOnce.password}</code>
            </div>
            <div className="row" style={{ marginTop: 16, justifyContent: "flex-end" }}>
              <button className="btn btn-sm" onClick={async () => {
                const ok = await copyText(`${showOnce.pseudo} / ${showOnce.password}`);
                flash(ok ? t("copied_short") : t("copy_fail"));
              }}>{t("adm_copy")}</button>
              <button className="btn btn-primary btn-sm" onClick={() => setShowOnce(null)}>{t("close")}</button>
            </div>
          </div>
        </div>
      )}
    </div>
  );
}

/* ===================== LOGS ===================== */
function LogsView({ logs, t, lang, isSupreme, onPurge }) {
  const [confirming, setConfirming] = useState(false);
  const [q, setQ] = useState("");
  const needle = q.trim().toLowerCase();
  const list = logs.filter((l) => {
    if (!needle) return true;
    const label = t(LOG_LABEL[l.action] || l.action);
    return (
      (l.who || "").toLowerCase().includes(needle) ||
      label.toLowerCase().includes(needle) ||
      (l.detail || "").toLowerCase().includes(needle)
    );
  }).slice(0, 200);

  return (
    <div style={{ display: "grid", gap: 20 }}>
      <div className="row" style={{ alignItems: "flex-end" }}>
        <div style={{ flex: 1 }}><PageHead crumb="ADMIN" title={t("logs_title")} /></div>
        {isSupreme && !confirming && (
          <button className="btn btn-bad btn-sm" onClick={() => setConfirming(true)}>🗑 {t("logs_clear")}</button>
        )}
        {isSupreme && confirming && (
          <>
            <button className="btn btn-bad btn-sm" onClick={() => { setConfirming(false); onPurge(); }}>{t("logs_confirm")}</button>
            <button className="btn btn-ghost btn-sm" onClick={() => setConfirming(false)}>{t("adm_cancel")}</button>
          </>
        )}
      </div>
      <input value={q} onChange={(e) => setQ(e.target.value)} placeholder={t("logs_filter")} />
      <div className="card" style={{ padding: "6px 18px" }}>
        {list.length === 0 ? (
          <div style={{ color: "var(--muted)", fontSize: 13, padding: "12px 0" }}>{t("logs_empty")}</div>
        ) : (
          list.map((l) => (
            <div key={l.id} className="lline" style={{ alignItems: "center" }}>
              <span className="lts">{fmtTs(l.ts, lang)}</span>
              <span style={{ fontWeight: 700, fontSize: 13, color: (ROLE_META[l.role] || {}).color || "var(--text)", minWidth: 90 }}>
                {l.who}
              </span>
              <span style={{ flex: 1 }}>
                <span className="lact" style={{ fontWeight: 600, color: LOG_COLOR[l.action] || "var(--text)" }}>
                  {t(LOG_LABEL[l.action] || l.action)}
                </span>
                {l.detail && <span className="ldet"> — {l.detail}</span>}
              </span>
            </div>
          ))
        )}
      </div>
    </div>
  );
}

/* ===================== ICÔNES ===================== */
function HouseIcon() {
  return (
    <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
      <path d="M3 10.5L12 3l9 7.5" />
      <path d="M5 9.5V20a1 1 0 0 0 1 1h4v-6h4v6h4a1 1 0 0 0 1-1V9.5" />
    </svg>
  );
}
function PyramidIcon({ small }) {
  const s = small ? 16 : 20;
  return (
    <svg width={s} height={s} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
      <path d="M12 3L22 20H2z" />
      <path d="M8.7 8.8h6.6" />
      <path d="M5.8 14.4h12.4" />
    </svg>
  );
}
function CalendarIcon() {
  return (
    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
      <rect x="3" y="5" width="18" height="16" rx="2" />
      <path d="M8 3v4M16 3v4M3 10h18" />
    </svg>
  );
}
function ClipboardIcon() {
  return (
    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
      <rect x="5" y="4" width="14" height="18" rx="2" />
      <path d="M9 2h6v4H9z" />
      <path d="M9 13l2 2 4-4" />
    </svg>
  );
}
function BookIcon() {
  return (
    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
      <path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20V3H6.5A2.5 2.5 0 0 0 4 5.5z" />
      <path d="M4 19.5A2.5 2.5 0 0 0 6.5 22H20v-5" />
    </svg>
  );
}
function UsersIcon() {
  return (
    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
      <circle cx="9" cy="8" r="3.2" />
      <path d="M3.5 20c0-3 2.5-5 5.5-5s5.5 2 5.5 5" />
      <circle cx="17" cy="9" r="2.4" />
      <path d="M16.5 14.6c2.4.3 4 2 4 4.4" />
    </svg>
  );
}
function ShieldIcon() {
  return (
    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
      <path d="M12 3l8 3v6c0 5-3.5 7.8-8 9-4.5-1.2-8-4-8-9V6z" />
    </svg>
  );
}

function FileTextIcon() {
  return (
    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
      <path d="M6 2h8l5 5v15H6z" />
      <path d="M14 2v5h5" />
      <path d="M9 12h7M9 16h7" />
    </svg>
  );
}

function ScrollIcon() {
  return (
    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
      <path d="M4 5h16M4 12h16M4 19h10" />
    </svg>
  );
}


/* ===================== INSIGNE GSE ===================== */
function GseLogo({ size = 42 }) {
  return (
    <svg className="glogo" width={size} height={size} viewBox="0 0 48 48" aria-label="GSE">
      <defs>
        <linearGradient id="gseShield" x1="0" y1="0" x2="1" y2="1">
          <stop offset="0" stopColor="#6C8CFF" />
          <stop offset="1" stopColor="#7B5CE0" />
        </linearGradient>
      </defs>
      {/* anneaux tactiques rotatifs */}
      <circle className="gring1" cx="24" cy="24" r="22" fill="none" stroke="#6C8CFF" strokeWidth="1.6" strokeDasharray="4 5" opacity="0.85" />
      <circle className="gring2" cx="24" cy="24" r="18.6" fill="none" stroke="#F5B23D" strokeWidth="1" strokeDasharray="1.5 6" opacity="0.9" />
      {/* halo pulsant */}
      <circle className="gpulse" cx="24" cy="24" r="15.6" fill="rgba(108,140,255,.18)" />
      {/* bouclier */}
      <path d="M24 9.5 L33.5 13.2 V23.5 C33.5 30.6 29.4 34.9 24 36.8 C18.6 34.9 14.5 30.6 14.5 23.5 V13.2 Z"
        fill="url(#gseShield)" stroke="#F5B23D" strokeWidth="1.2" strokeLinejoin="round" />
      {/* étoile de grade */}
      <path d="M24 12.9 l1.08 2.2 2.42.35 -1.75 1.7 .41 2.41 -2.16-1.14 -2.16 1.14 .41-2.41 -1.75-1.7 2.42-.35 Z" fill="#FFD978" />
      {/* sigle */}
      <text x="24" y="27.4" textAnchor="middle"
        fontFamily="ui-monospace, 'SF Mono', Menlo, monospace" fontWeight="800" fontSize="7.4"
        fill="#FFFFFF" letterSpacing="1.1">GSE</text>
      {/* chevrons militaires */}
      <path d="M19.4 30.2 L24 27.7 L28.6 30.2" fill="none" stroke="#FFD978" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" />
      <path d="M20.4 32.6 L24 30.6 L27.6 32.6" fill="none" stroke="#FFD978" strokeWidth="1.2" strokeLinecap="round" strokeLinejoin="round" opacity="0.8" />
    </svg>
  );
}


/* ===================== CARTE DISCORD ===================== */
const DISCORD_INVITE = "6dZcWKsyyy";
function DiscordMark() {
  return (
    <svg width="30" height="30" viewBox="0 0 24 24" fill="currentColor">
      <path d="M19.27 5.33A16.7 16.7 0 0 0 15.06 4c-.2.36-.43.84-.59 1.22a15.5 15.5 0 0 0-4.94 0C9.37 4.84 9.13 4.36 8.93 4a16.6 16.6 0 0 0-4.21 1.33C2.1 9.27 1.4 13.1 1.75 16.88a16.9 16.9 0 0 0 5.18 2.66c.42-.58.79-1.2 1.11-1.85-.61-.23-1.2-.52-1.75-.86.15-.11.29-.22.43-.34 3.36 1.58 7.01 1.58 10.33 0 .14.12.28.23.43.34-.55.34-1.14.63-1.76.86.32.65.69 1.27 1.11 1.85a16.8 16.8 0 0 0 5.19-2.66c.42-4.38-.7-8.18-2.75-11.55ZM8.68 14.55c-1.01 0-1.84-.94-1.84-2.1 0-1.15.81-2.1 1.84-2.1 1.04 0 1.86.95 1.84 2.1 0 1.16-.81 2.1-1.84 2.1Zm6.64 0c-1.01 0-1.84-.94-1.84-2.1 0-1.15.81-2.1 1.84-2.1 1.04 0 1.86.95 1.84 2.1 0 1.16-.8 2.1-1.84 2.1Z"/>
    </svg>
  );
}
function DiscordCard({ t }) {
  const [info, setInfo] = useState(null);
  useEffect(() => {
    let on = true;
    (async () => {
      try {
        const r = await fetch(`https://discord.com/api/v10/invites/${DISCORD_INVITE}?with_counts=true`);
        const d = await r.json();
        if (on && d && d.guild) {
          setInfo({
            name: d.guild.name,
            icon: d.guild.icon ? `https://cdn.discordapp.com/icons/${d.guild.id}/${d.guild.icon}.png?size=128` : null,
            online: d.approximate_presence_count,
            total: d.approximate_member_count,
          });
        }
      } catch {}
    })();
    return () => { on = false; };
  }, []);

  return (
    <div className="dcard2">
      <div style={{ fontSize: 12, fontWeight: 700, color: "var(--muted)", letterSpacing: ".02em", textTransform: "uppercase", marginBottom: 12 }}>
        {t("discord_invited")}
      </div>
      <div style={{ display: "flex", alignItems: "center", gap: 14, flexWrap: "wrap" }}>
        <span className="dicon" style={{ width: 50, height: 50, borderRadius: 15 }}>
          {info && info.icon ? <img src={info.icon} alt="" /> : <DiscordMark />}
        </span>
        <div style={{ flex: 1, minWidth: 160 }}>
          <div style={{ fontWeight: 800, fontSize: 16 }}>{(info && info.name) || "GSE"}</div>
          <div style={{ fontSize: 13, color: "var(--muted)", marginTop: 4 }}>
            <span className="ddot" style={{ background: "#23A559" }} />{info ? info.online : "—"} {t("discord_online")}
            <span className="ddot" style={{ background: "var(--muted)", marginLeft: 14 }} />{info ? info.total : "—"} {t("discord_members")}
          </div>
        </div>
        <a className="djoin2" href={`https://discord.gg/${DISCORD_INVITE}`} target="_blank" rel="noopener noreferrer">
          {t("discord_join")}
        </a>
      </div>
    </div>
  );
}

/* ===================== RAPPORTS ===================== */
function Rapports({ me, canManage, reports, templates, reload, flash, t, lang, pendingReport, onConsumed, pendingCat, onCatConsumed }) {
  const ME = me.pseudo;
  const [sub, setSub] = useState("cats");
  const [cat, setCat] = useState("all");
  const [statusTab, setStatusTab] = useState("open");
  const [query, setQuery] = useState("");
  const [current, setCurrent] = useState(null);
  const [busy, setBusy] = useState(false);
  const fileRef = useRef(null);

  const today = toISO(new Date());
  const [fName, setFName] = useState("");
  const [fDate, setFDate] = useState(today);
  const [fNotify, setFNotify] = useState(false);
  const [fTpl, setFTpl] = useState("");
  const [fCat, setFCat] = useState("");
  const [fBody, setFBody] = useState("");
  const [fImgs, setFImgs] = useState([]);
  const [fTplSave, setFTplSave] = useState(false);

  // Ouverture demandée depuis la recherche/notifications
  useEffect(() => {
    if (pendingReport) {
      const m = reports.find((r) => r.id === pendingReport);
      if (m) open(m);
      onConsumed();
    }
  }, [pendingReport]);
  useEffect(() => {
    if (pendingCat) {
      setCat(pendingCat);
      setStatusTab("open");
      setQuery("");
      setSub("list");
      onCatConsumed();
    }
  }, [pendingCat]);

  const allT = [...DEFAULT_TEMPLATES, ...templates];
  const SUPPLUS = SUPERVISOR_PLUS.includes(me.role);

  function canEditNow(r) {
    if (r.by !== ME) return false;
    if (r.derog) return true;
    return Date.now() - r.createdAt < 15 * 60 * 1000;
  }
  function editLeftMin(r) {
    return Math.max(0, Math.ceil((15 * 60 * 1000 - (Date.now() - r.createdAt)) / 60000));
  }
  function startEdit(r) {
    setFName(r.name); setFDate(r.date); setFCat(r.category);
    setFBody(r.body || ""); setFImgs(r.images || []); setFNotify(!!r.notify);
    setSub("edit");
  }
  async function saveEdit() {
    if (!fName.trim()) return flash(t("rpt_need_name"));
    if (!fCat) return flash(t("rpt_need_cat"));
    if (busy) return;
    setBusy(true);
    const ok = await updateReport(current, {
      name: fName.trim(), date: fDate || today, category: fCat,
      body: fBody, images: fImgs, notify: fNotify,
    });
    setBusy(false);
    if (ok === false) return;
    setSub("view");
    flash(t("rpt_edited"));
  }

  const counts = { all: reports.length };
  Object.keys(RCATS).forEach((c) => { counts[c] = reports.filter((r) => r.category === c).length; });

  const inCat = cat === "all" ? reports : reports.filter((r) => r.category === cat);
  const nOpen = inCat.filter((r) => (r.status || "open") === "open").length;
  const nArch = inCat.length - nOpen;
  const nq = query.trim().toLowerCase();
  const list = inCat
    .filter((r) => (r.status || "open") === statusTab)
    .filter((r) => !nq || [r.name, r.by, r.body, "#" + (r.number || ""), frDate(r.date)]
      .some((v) => String(v || "").toLowerCase().includes(nq)))
    .slice().sort((a, b) => b.createdAt - a.createdAt);

  function resetForm() {
    setFName(""); setFDate(today); setFNotify(false); setFTpl(""); setFCat("");
    setFBody(""); setFImgs([]); setFTplSave(false);
  }
  function pickTemplate(id) {
    setFTpl(id);
    const tp = allT.find((x) => x.id === id);
    if (tp) {
      setFBody(tp.content || "");
      if (tp.category && RCATS[tp.category]) setFCat(tp.category);
    }
  }
  async function addImages(e) {
    const files = Array.from(e.target.files || []);
    e.target.value = "";
    const next = [...fImgs];
    for (const file of files) {
      if (next.length >= 6) break;
      try { next.push(await resizeProof(file, 900)); } catch {}
    }
    setFImgs(next);
  }

  async function create() {
    if (!fName.trim()) return flash(t("rpt_need_name"));
    if (!fCat) return flash(t("rpt_need_cat"));
    if (busy) return;
    setBusy(true);
    const r = await api("/api/reports", "POST", {
      name: fName.trim(), category: fCat, date: fDate || today,
      notify: fNotify, body: fBody, images: fImgs,
      saveTemplate: fTplSave && canManage,
    });
    setBusy(false);
    if (!r.ok) return flash(t("server_err"));
    if (fTplSave && canManage) flash(t("tpl_saved"));
    resetForm();
    setCat(fCat);
    setStatusTab("open");
    await reload();
    setSub("list");
    flash(t("rpt_created"));
  }
  async function open(meta) {
    const r = await api(`/api/reports/${meta.id}`);
    setCurrent(r.ok && r.data ? r.data : { ...meta, images: [] });
    setSub("view");
  }
  async function setStatus(r, status) {
    const res = await api(`/api/reports/${r.id}/status`, "PUT", { status });
    if (!res.ok) return flash(t("server_err"));
    if (current && current.id === r.id) setCurrent({ ...current, status });
    await reload();
    flash(status === "archived" ? t("rpt_archived_ok") : t("rpt_reopened"));
  }
  async function removeReport(r) {
    if (!(canManage || r.by === ME)) return;
    const res = await api(`/api/reports/${r.id}`, "DELETE");
    if (!res.ok) return flash(t("server_err"));
    setCurrent(null);
    await reload();
    setSub("list");
    flash(t("rpt_deleted"));
  }
  async function updateReport(r, patch) {
    const res = await api(`/api/reports/${r.id}`, "PUT", patch);
    if (!res.ok) { flash(t("server_err")); return false; }
    setCurrent({ ...current, ...patch, editedAt: Date.now() });
    await reload();
    return true;
  }
  async function setDerog(r, derog) {
    const res = await api(`/api/reports/${r.id}/derog`, "PUT", { derog });
    if (!res.ok) return flash(t("server_err"));
    if (current && current.id === r.id) setCurrent({ ...current, derog });
    await reload();
    flash(derog ? t("rpt_derog_set") : t("rpt_derog_unset"));
  }

  const stChip = (s) => (
    <span className="stchip" style={{ background: (s || "open") === "open" ? "var(--ok)" : "#5A6273" }}>
      {(s || "open") === "open" ? t("rpt_st_open") : t("rpt_st_archived")}
    </span>
  );

  if (sub === "view" && current) {
    const rc = RCATS[current.category] || RCATS.formation;
    const mayEdit = canManage || current.by === ME;
    return (
      <div style={{ display: "grid", gap: 20 }}>
        <div className="row" style={{ alignItems: "center" }}>
          <button className="btn btn-sm" onClick={() => { setCurrent(null); setSub("list"); }}>{t("rpt_back")}</button>
          <div style={{ flex: 1 }} />
          {canEditNow(current) && (
            <button className="btn btn-sm" onClick={() => startEdit(current)}>✏️ {t("rpt_edit")}</button>
          )}
          {SUPPLUS && (current.derog ? (
            <button className="btn btn-sm" onClick={() => setDerog(current, false)}>🔒 {t("rpt_derog_off")}</button>
          ) : (
            <button className="btn btn-sm" onClick={() => setDerog(current, true)}>🔓 {t("rpt_derog_on")}</button>
          ))}
          {mayEdit && ((current.status || "open") === "open" ? (
            <button className="btn btn-sm" onClick={() => setStatus(current, "archived")}>🗄 {t("rpt_archive")}</button>
          ) : (
            <button className="btn btn-sm" onClick={() => setStatus(current, "open")}>↺ {t("rpt_unarchive")}</button>
          ))}
          {mayEdit && <button className="btn btn-bad btn-sm" onClick={() => removeReport(current)}>{t("del")}</button>}
        </div>
        <div className="card">
          <div style={{ display: "flex", alignItems: "center", gap: 12, flexWrap: "wrap" }}>
            <span className="rnum" style={{ fontSize: 13 }}>#{String(current.number || 0).padStart(3, "0")}</span>
            <span className="badge" style={{ color: rc.color }}>{t(rc.tkey)}</span>
            {stChip(current.status)}
            <span style={{ fontSize: 20, fontWeight: 800, flex: 1, minWidth: 200 }}>{current.name}</span>
          </div>
          <div style={{ fontFamily: "var(--mono)", fontSize: 12, color: "var(--muted)", marginTop: 8 }}>
            {t("rpt_by")} : {current.by} · {frDate(current.date)} · {fmtTs(current.createdAt, lang)}
            {current.editedAt ? <span> · ✏️ {fmtTs(current.editedAt, lang)}</span> : null}
          </div>
          {current.derog ? (
            <div style={{ fontSize: 11.5, color: "var(--accent)", marginTop: 6, fontFamily: "var(--mono)" }}>🔓 {t("rpt_derog_active")}</div>
          ) : canEditNow(current) ? (
            <div style={{ fontSize: 11.5, color: "var(--warn)", marginTop: 6, fontFamily: "var(--mono)" }}>✏️ {t("rpt_editleft")} {editLeftMin(current)} min</div>
          ) : null}
          <div style={{ marginTop: 18, fontSize: 14.5, lineHeight: 1.75, whiteSpace: "pre-wrap" }}>
            {current.body || "—"}
          </div>
        </div>
        {current.images && current.images.length > 0 && (
          <div className="card">
            <div className="eyebrow" style={{ marginBottom: 14 }}>{t("rpt_proof")} · {current.images.length}</div>
            <div className="rimgs">{current.images.map((img, i) => <img key={i} src={img} alt="" />)}</div>
          </div>
        )}
      </div>
    );
  }

  if (sub === "edit" && current) {
    return (
      <div style={{ display: "grid", gap: 20 }}>
        <div className="row" style={{ alignItems: "center" }}>
          <button className="btn btn-sm" onClick={() => setSub("view")}>{t("rpt_back")}</button>
          <div style={{ fontSize: 20, fontWeight: 800, flex: 1 }}>
            {t("rpt_edit_title")} <span className="rnum" style={{ fontSize: 14 }}>#{String(current.number || 0).padStart(3, "0")}</span>
          </div>
        </div>
        <div className="card" style={{ display: "grid", gap: 14 }}>
          <div className="grid2">
            <Field label={t("rpt_name")}><input value={fName} onChange={(e) => setFName(e.target.value)} /></Field>
            <Field label={t("rpt_date")}><input type="date" value={fDate} onChange={(e) => setFDate(e.target.value)} /></Field>
          </div>
          <Field label={t("rpt_category")}>
            <select value={fCat} onChange={(e) => setFCat(e.target.value)}>
              {Object.keys(RCATS).map((c) => <option key={c} value={c}>{t(RCATS[c].tkey)}</option>)}
            </select>
          </Field>
          <Field label={t("rpt_body")}>
            <textarea value={fBody} onChange={(e) => setFBody(e.target.value)} style={{ minHeight: 300, fontSize: 14, lineHeight: 1.7 }} />
          </Field>
          <div>
            <label>{t("rpt_proof")}</label>
            <div className="proofgrid">
              {fImgs.map((img, i) => (
                <span key={i} className="proofthumb">
                  <img src={img} alt="" />
                  <button onClick={() => setFImgs(fImgs.filter((_, j) => j !== i))}>✕</button>
                </span>
              ))}
              {fImgs.length < 6 && (
                <button className="btn btn-sm" onClick={() => fileRef.current && fileRef.current.click()}
                  style={{ height: 72, minWidth: 96 }}>+ {t("rpt_proof_add")}</button>
              )}
            </div>
            <input ref={fileRef} type="file" accept="image/*" multiple style={{ display: "none" }} onChange={addImages} />
          </div>
          <div className="row" style={{ alignItems: "center" }}>
            <label style={{ display: "flex", alignItems: "center", gap: 8, margin: 0, fontSize: 13.5, color: "var(--text)", cursor: "pointer" }}>
              <input type="checkbox" checked={fNotify} onChange={(e) => setFNotify(e.target.checked)} style={{ width: "auto" }} />
              {t("rpt_notify")}
            </label>
            <div style={{ flex: 1 }} />
            <button className="btn btn-primary" onClick={saveEdit} disabled={busy}>{t("rpt_save")}</button>
          </div>
        </div>
      </div>
    );
  }

    if (sub === "new") {
    return (
      <div style={{ display: "grid", gap: 20 }}>
        <div className="row" style={{ alignItems: "center" }}>
          <button className="btn btn-sm" onClick={() => setSub("list")}>{t("rpt_back")}</button>
          <div style={{ fontSize: 20, fontWeight: 800, flex: 1 }}>{t("rpt_new")}</div>
        </div>
        <div className="card" style={{ display: "grid", gap: 14 }}>
          <div className="grid2">
            <Field label={t("rpt_name")}><input value={fName} onChange={(e) => setFName(e.target.value)} /></Field>
            <Field label={t("rpt_date")}><input type="date" value={fDate} onChange={(e) => setFDate(e.target.value)} /></Field>
          </div>
          <div className="grid2">
            <Field label={t("rpt_category")}>
              <select value={fCat} onChange={(e) => setFCat(e.target.value)}>
                <option value="">—</option>
                {Object.keys(RCATS).map((c) => <option key={c} value={c}>{t(RCATS[c].tkey)}</option>)}
              </select>
            </Field>
            <Field label={t("rpt_template")}>
              <select value={fTpl} onChange={(e) => pickTemplate(e.target.value)}>
                <option value="">{t("rpt_template_none")}</option>
                {allT.map((tp) => <option key={tp.id} value={tp.id}>{tp.name}</option>)}
              </select>
            </Field>
          </div>
          <Field label={t("rpt_body")}>
            <textarea value={fBody} onChange={(e) => setFBody(e.target.value)}
              placeholder={t("rpt_body_ph")} style={{ minHeight: 300, fontSize: 14, lineHeight: 1.7 }} />
          </Field>
          <div>
            <label>{t("rpt_proof")}</label>
            <div className="proofgrid">
              {fImgs.map((img, i) => (
                <span key={i} className="proofthumb">
                  <img src={img} alt="" />
                  <button onClick={() => setFImgs(fImgs.filter((_, j) => j !== i))}>✕</button>
                </span>
              ))}
              {fImgs.length < 6 && (
                <button className="btn btn-sm" onClick={() => fileRef.current && fileRef.current.click()}
                  style={{ height: 72, minWidth: 96 }}>+ {t("rpt_proof_add")}</button>
              )}
            </div>
            <input ref={fileRef} type="file" accept="image/*" multiple style={{ display: "none" }} onChange={addImages} />
            <p style={{ fontSize: 12, color: "var(--muted)", margin: "8px 0 0" }}>{t("rpt_proof_hint")}</p>
          </div>
          <div className="row" style={{ alignItems: "center" }}>
            <label style={{ display: "flex", alignItems: "center", gap: 8, margin: 0, fontSize: 13.5, color: "var(--text)", cursor: "pointer" }}>
              <input type="checkbox" checked={fNotify} onChange={(e) => setFNotify(e.target.checked)} style={{ width: "auto" }} />
              {t("rpt_notify")}
            </label>
            {canManage && (
              <label style={{ display: "flex", alignItems: "center", gap: 8, margin: 0, fontSize: 13.5, color: "var(--text)", cursor: "pointer" }}>
                <input type="checkbox" checked={fTplSave} onChange={(e) => setFTplSave(e.target.checked)} style={{ width: "auto" }} />
                💾 {t("tpl_save_too")}
              </label>
            )}
            <div style={{ flex: 1 }} />
            <button className="btn btn-primary" onClick={create} disabled={busy}>{t("rpt_create")}</button>
          </div>
        </div>
      </div>
    );
  }

  if (sub === "list") {
    return (
      <div style={{ display: "grid", gap: 18 }}>
        <div className="row" style={{ alignItems: "center" }}>
          <button className="btn btn-sm" onClick={() => { setSub("cats"); setQuery(""); }}>{t("rpt_back")}</button>
          <div style={{ flex: 1 }}>
            <span style={{ fontSize: 20, fontWeight: 800 }}>
              {cat === "all" ? t("cat_all") : t(RCATS[cat].tkey)}
            </span>
          </div>
          <button className="btn btn-primary btn-sm" onClick={() => { resetForm(); if (cat !== "all") setFCat(cat); setSub("new"); }}>
            + {t("rpt_new")}
          </button>
        </div>
        <input value={query} onChange={(e) => setQuery(e.target.value)} placeholder={t("rpt_search_ph")} />
        <div className="row">
          <button className={`tabchip ${statusTab === "open" ? "on" : ""}`} onClick={() => setStatusTab("open")}>
            <span className="ddot" style={{ background: "var(--ok)", marginRight: 0 }} />
            {t("rpt_open_s")} <b style={{ fontFamily: "var(--mono)" }}>{nOpen}</b>
          </button>
          <button className={`tabchip ${statusTab === "archived" ? "on" : ""}`} onClick={() => setStatusTab("archived")}>
            <span className="ddot" style={{ background: "#5A6273", marginRight: 0 }} />
            {t("rpt_archived_s")} <b style={{ fontFamily: "var(--mono)" }}>{nArch}</b>
          </button>
        </div>
        <div className="card" style={{ padding: 0, overflow: "hidden" }}>
          {list.length === 0 ? (
            <div style={{ color: "var(--muted)", fontSize: 13.5, padding: "18px 16px", textAlign: "center" }}>{t("rpt_empty")}</div>
          ) : (
            list.map((r) => {
              const rc = RCATS[r.category] || RCATS.formation;
              return (
                <div key={r.id} className="rrow" onClick={() => open(r)}>
                  <span className="rnum">#{String(r.number || 0).padStart(3, "0")}</span>
                  <span style={{ flex: 1, minWidth: 0 }}>
                    <span style={{ display: "block", fontWeight: 700, fontSize: 14.5, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{r.name}</span>
                    <span style={{ display: "block", fontSize: 12, color: "var(--muted)", marginTop: 2 }}>
                      {t("rpt_by")} : {r.by} · {frDate(r.date)}
                    </span>
                  </span>
                  {r.notify ? <span title={t("rpt_notify")}>📣</span> : null}
                  <span className="badge" style={{ color: rc.color, flexShrink: 0 }}>{t(rc.tkey)}</span>
                  {stChip(r.status)}
                  <span style={{ color: "var(--muted)" }}>›</span>
                </div>
              );
            })
          )}
        </div>
      </div>
    );
  }

  return (
    <div style={{ display: "grid", gap: 22 }}>
      <PageHead crumb={t("rpt_title")} title={t("rpt_title")} sub={t("rpt_sub")} />
      <div className="rcats">
        <button className="rcat" style={{ "--rc": "#F5B23D" }} onClick={() => { setCat("all"); setStatusTab("open"); setSub("list"); }}>
          <div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start" }}>
            <span className="rcode" style={{ background: "linear-gradient(135deg,#F5B23D,#C77818)" }}>ALL</span>
            <span className="rnum">#00</span>
          </div>
          <div className="rlabel">{t("cat_all")}</div>
          <div className="rcount">{counts.all}</div>
        </button>
        {Object.keys(RCATS).map((c, i) => (
          <button key={c} className="rcat" style={{ "--rc": RCATS[c].color }} onClick={() => { setCat(c); setStatusTab("open"); setSub("list"); }}>
            <div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start" }}>
              <span className="rcode" style={{ background: RCATS[c].color }}>{RCATS[c].code}</span>
              <span className="rnum">#{String(i + 1).padStart(2, "0")}</span>
            </div>
            <div className="rlabel">{t(RCATS[c].tkey)}</div>
            <div className="rcount">{counts[c]}</div>
          </button>
        ))}
      </div>
    </div>
  );
}

/* ===================== EN-TÊTE DE PAGE ===================== */
function PageHead({ crumb, title, sub }) {
  return (
    <div>
      <span className="ph-pill">GSE / {crumb}</span>
      <div className="ph-title">{title}</div>
      {sub && <p className="ph-sub">{sub}</p>}
    </div>
  );
}
function ChatIcon() {
  return (
    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
      <path d="M21 12a8 8 0 0 1-8 8H5l-2 2V12a8 8 0 0 1 8-8h2a8 8 0 0 1 8 8z" />
      <path d="M8.5 11.5h.01M12 11.5h.01M15.5 11.5h.01" />
    </svg>
  );
}
function InboxIcon() {
  return (
    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
      <path d="M22 12h-6l-2 3h-4l-2-3H2" />
      <path d="M5 5h14l3 7v7H2v-7z" />
    </svg>
  );
}
function SearchIcon() {
  return (
    <svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
      <circle cx="11" cy="11" r="7" />
      <path d="M21 21l-4.3-4.3" />
    </svg>
  );
}
function BellIcon() {
  return (
    <svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
      <path d="M18 8a6 6 0 1 0-12 0c0 7-3 8-3 8h18s-3-1-3-8" />
      <path d="M10.3 21a2 2 0 0 0 3.4 0" />
    </svg>
  );
}

/* ===================== CLOCHE DE NOTIFICATIONS ===================== */
function NotifBell({ reports, t, lang, loadSeen, saveSeen, onPickReport }) {
  const [open, setOpen] = useState(false);
  const [seen, setSeen] = useState(Number.MAX_SAFE_INTEGER);
  const ref = useRef(null);

  useEffect(() => {
    (async () => {
      try { setSeen(Number(await loadSeen()) || 0); } catch { setSeen(0); }
    })();
  }, []);
  useEffect(() => {
    function onDoc(e) { if (ref.current && !ref.current.contains(e.target)) setOpen(false); }
    document.addEventListener("mousedown", onDoc);
    return () => document.removeEventListener("mousedown", onDoc);
  }, []);

  const items = [
    ...reports.map((r) => ({ id: "r" + r.id, ts: r.createdAt, type: "report", label: r.name, by: r.by, rid: r.id })),
    ...SITE_UPDATES.map((u) => ({ id: u.id, ts: u.ts, type: "update", label: lang === "en" ? u.en : u.fr })),
  ].sort((a, b) => b.ts - a.ts).slice(0, 12);
  const unseen = items.filter((i) => i.ts > seen).length;

  async function toggle() {
    const willOpen = !open;
    setOpen(willOpen);
    if (willOpen) {
      const now = Date.now();
      setSeen(now);
      try { await saveSeen(now); } catch {}
    }
  }

  return (
    <div style={{ position: "relative" }} ref={ref}>
      <button className="hbtn" title={t("notif_bell")} onClick={toggle}>
        <BellIcon />
        {unseen > 0 && <span className="hdot">{unseen > 9 ? "9+" : unseen}</span>}
      </button>
      {open && (
        <div className="ndrop">
          <div className="eyebrow" style={{ padding: "6px 10px 8px" }}>{t("notif_bell")}</div>
          {items.length === 0 ? (
            <div style={{ color: "var(--muted)", fontSize: 13, padding: "8px 10px" }}>{t("notif_none")}</div>
          ) : (
            items.map((i) => (
              <div key={i.id} className="nrow"
                onClick={() => { if (i.type === "report") { setOpen(false); onPickReport(i.rid); } }}>
                <span style={{ fontSize: 15 }}>{i.type === "report" ? "📄" : "🛠️"}</span>
                <span style={{ flex: 1, minWidth: 0 }}>
                  <span style={{ display: "block", fontSize: 13, fontWeight: 600 }}>
                    {i.type === "report" ? t("notif_report") : t("notif_update")}
                  </span>
                  <span style={{ display: "block", fontSize: 12.5, color: "var(--muted)", marginTop: 1 }}>
                    {i.label}{i.by ? ` — ${i.by}` : ""}
                  </span>
                  <span style={{ display: "block", fontSize: 10.5, color: "var(--muted)", fontFamily: "var(--mono)", marginTop: 3 }}>
                    {fmtTs(i.ts, lang)}
                  </span>
                </span>
              </div>
            ))
          )}
        </div>
      )}
    </div>
  );
}

/* ===================== RECHERCHE GLOBALE ===================== */
function GlobalSearch({ t, onClose, reports, entretiens, formations, planning, accounts, pages, go, onPickReport }) {
  const [q, setQ] = useState("");
  const inputRef = useRef(null);
  useEffect(() => { if (inputRef.current) inputRef.current.focus(); }, []);
  useEffect(() => {
    function onKey(e) { if (e.key === "Escape") onClose(); }
    document.addEventListener("keydown", onKey);
    return () => document.removeEventListener("keydown", onKey);
  }, [onClose]);

  const n = q.trim().toLowerCase();
  const has = (...vals) => vals.some((v) => String(v || "").toLowerCase().includes(n));
  const ready = n.length >= 2;

  const rRes = ready ? reports.filter((r) => has(r.name, r.by, r.body, "#" + (r.number || ""))).slice(0, 5) : [];
  const eRes = ready ? entretiens.filter((e) => has(e.candidat, e.recruteur, e.entId, e.profil, e.oral)).slice(0, 5) : [];
  const fRes = ready ? formations.filter((f) => has(f.nom, f.formateur, f.commentaire, f.formes)).slice(0, 5) : [];
  const pRes = ready ? planning.filter((p) => has(p.type, p.formateur)).slice(0, 5) : [];
  const uRes = ready ? accounts.filter((a) => has(a.pseudo)).slice(0, 5) : [];
  const dRes = ready ? DOCS.filter((d) => has(d.title, d.type)).slice(0, 4) : [];
  const gRes = ready ? (pages || []).filter((p) => String(t(p.k)).toLowerCase().includes(n)).slice(0, 6) : [];
  const none = ready && !rRes.length && !eRes.length && !fRes.length && !pRes.length && !uRes.length && !dRes.length && !gRes.length;

  function Section({ label, children }) {
    return (<div><div className="sx-section">{label}</div>{children}</div>);
  }

  return (
    <div className="overlay" onClick={onClose} style={{ alignItems: "flex-start", paddingTop: "10vh" }}>
      <div className="modal" onClick={(e) => e.stopPropagation()} style={{ maxWidth: 560, maxHeight: "72vh", overflow: "auto" }}>
        <div style={{ display: "flex", alignItems: "center", gap: 10 }}>
          <SearchIcon />
          <input ref={inputRef} value={q} onChange={(e) => setQ(e.target.value)} placeholder={t("search_ph")}
            style={{ border: "none", background: "transparent", boxShadow: "none", fontSize: 16, padding: "6px 0" }} />
        </div>
        {!ready && <div style={{ color: "var(--muted)", fontSize: 12.5, marginTop: 10 }}>{t("search_hint")}</div>}
        {none && <div style={{ color: "var(--muted)", fontSize: 13.5, marginTop: 14 }}>{t("search_none")}</div>}
        {gRes.length > 0 && (
          <Section label={t("sx_pages")}>
            {gRes.map((p) => (
              <div key={p.id} className="sx-row" onClick={() => go(p.id)}>
                <span style={{ fontSize: 15 }}>{p.icon}</span>
                <span style={{ flex: 1, fontWeight: 600 }}>{t(p.k)}</span>
                <span style={{ color: "var(--muted)" }}>›</span>
              </div>
            ))}
          </Section>
        )}
                {rRes.length > 0 && (
          <Section label={t("sx_reports")}>
            {rRes.map((r) => (
              <div key={r.id} className="sx-row" onClick={() => onPickReport(r.id)}>
                <span className="rnum">#{String(r.number || 0).padStart(3, "0")}</span>
                <span style={{ flex: 1, fontWeight: 600 }}>{r.name}</span>
                <span style={{ color: "var(--muted)", fontSize: 12 }}>{r.by}</span>
              </div>
            ))}
          </Section>
        )}
        {eRes.length > 0 && (
          <Section label={t("sx_ents")}>
            {eRes.map((e) => (
              <div key={e.id} className="sx-row" onClick={() => go("entretiens")}>
                <span className="entid">{e.entId}</span>
                <span style={{ flex: 1, fontWeight: 600 }}>{e.candidat}</span>
                <span style={{ color: "var(--muted)", fontSize: 12 }}>{e.recruteur}</span>
              </div>
            ))}
          </Section>
        )}
        {fRes.length > 0 && (
          <Section label={t("sx_done")}>
            {fRes.map((f) => (
              <div key={f.id} className="sx-row" onClick={() => go("realisees")}>
                <span style={{ flex: 1, fontWeight: 600 }}>{f.nom}</span>
                <span style={{ color: "var(--muted)", fontSize: 12 }}>{frDate(f.date)} · {f.formateur}</span>
              </div>
            ))}
          </Section>
        )}
        {pRes.length > 0 && (
          <Section label={t("sx_plan")}>
            {pRes.map((p) => (
              <div key={p.id} className="sx-row" onClick={() => go("planning")}>
                <span style={{ flex: 1, fontWeight: 600 }}>{p.type}</span>
                <span style={{ color: "var(--muted)", fontSize: 12 }}>{frDate(p.date)} {p.time} · {p.formateur}</span>
              </div>
            ))}
          </Section>
        )}
        {uRes.length > 0 && (
          <Section label={t("sx_users")}>
            {uRes.map((a) => (
              <div key={a.id} className="sx-row" onClick={() => go("users")}>
                <span className="avatar" style={{ width: 24, height: 24, fontSize: 11 }}>
                  {a.avatar ? <img src={a.avatar} alt="" /> : a.pseudo.slice(0, 1).toUpperCase()}
                </span>
                <span style={{ flex: 1, fontWeight: 600 }}>{a.pseudo}</span>
                <span className="badge" style={{ color: ROLE_META[a.role].color, fontSize: 9.5 }}>{t(ROLE_META[a.role].tkey)}</span>
              </div>
            ))}
          </Section>
        )}
        {dRes.length > 0 && (
          <Section label={t("sx_docs")}>
            {dRes.map((d) => (
              <a key={d.code} className="sx-row" href={d.url} target="_blank" rel="noopener noreferrer"
                style={{ textDecoration: "none", color: "var(--text)" }}>
                <span className="docmono" style={{ background: d.grad, width: 26, height: 26, fontSize: 11, borderRadius: 7 }}>{d.code}</span>
                <span style={{ flex: 1, fontWeight: 600 }}>{d.title}</span>
                <span style={{ color: "var(--muted)" }}>↗</span>
              </a>
            ))}
          </Section>
        )}
      </div>
    </div>
  );
}

/* ===================== ENTRETIENS ===================== */
const DECS = {
  accepte: { tkey: "dec_accepte", color: "var(--ok)", icon: "✓" },
  attente: { tkey: "dec_attente", color: "var(--warn)", icon: "⏳" },
  refuse: { tkey: "dec_refuse", color: "var(--bad)", icon: "✕" },
};
function Entretiens({ me, canManage, entretiens, reload, flash, t, lang }) {
  const ME = me.pseudo;
  const [sub, setSub] = useState("list");
  const [current, setCurrent] = useState(null);
  const [query, setQuery] = useState("");
  const [decFilter, setDecFilter] = useState("all");
  const [busy, setBusy] = useState(false);

  const [fCand, setFCand] = useState("");
  const [fTemps, setFTemps] = useState("");
  const [fProfil, setFProfil] = useState("");
  const [fOral, setFOral] = useState("");
  const [fGrab, setFGrab] = useState("");
  const [fDec, setFDec] = useState("accepte");

  const stats = {
    total: entretiens.length,
    accepte: entretiens.filter((e) => e.decision === "accepte").length,
    attente: entretiens.filter((e) => e.decision === "attente").length,
    refuse: entretiens.filter((e) => e.decision === "refuse").length,
  };
  const nq = query.trim().toLowerCase();
  const list = entretiens
    .filter((e) => decFilter === "all" || e.decision === decFilter)
    .filter((e) => !nq || [e.candidat, e.recruteur, e.entId, e.grab].some((v) => String(v || "").toLowerCase().includes(nq)))
    .slice().sort((a, b) => b.createdAt - a.createdAt);

  function resetForm() { setFCand(""); setFTemps(""); setFProfil(""); setFOral(""); setFGrab(""); setFDec("accepte"); }

  async function create() {
    if (!fCand.trim() || !fTemps.trim() || !fProfil.trim() || !fOral.trim()) return flash(t("ent_need"));
    if (busy) return;
    setBusy(true);
    const r = await api("/api/entretiens", "POST", {
      candidat: fCand.trim(), temps: fTemps.trim(), profil: fProfil.trim(),
      oral: fOral.trim(), grab: fGrab.trim(), decision: fDec,
    });
    setBusy(false);
    if (!r.ok) return flash(t("server_err"));
    resetForm();
    await reload();
    setSub("list");
    flash(t("ent_created"));
  }
  async function setDecision(e, decision) {
    if (!canManage) return;
    const r = await api(`/api/entretiens/${e.id}/decision`, "PUT", { decision });
    if (!r.ok) return flash(t("server_err"));
    if (current && current.id === e.id) setCurrent({ ...current, decision });
    await reload();
  }
  async function removeEnt(e) {
    if (!(canManage || e.recruteur === ME)) return;
    const r = await api(`/api/entretiens/${e.id}`, "DELETE");
    if (!r.ok) return flash(t("server_err"));
    setCurrent(null);
    await reload();
    setSub("list");
    flash(t("ent_deleted"));
  }

  const decBadge = (d) => {
    const m = DECS[d] || DECS.attente;
    return <span className="badge" style={{ color: m.color }}>{m.icon} {t(m.tkey)}</span>;
  };

  if (sub === "view" && current) {
    const mayEdit = canManage || current.recruteur === ME;
    return (
      <div style={{ display: "grid", gap: 20 }}>
        <div className="row" style={{ alignItems: "center" }}>
          <button className="btn btn-sm" onClick={() => { setCurrent(null); setSub("list"); }}>{t("rpt_back")}</button>
          <div style={{ flex: 1 }} />
          {mayEdit && <button className="btn btn-bad btn-sm" onClick={() => removeEnt(current)}>{t("del")}</button>}
        </div>
        <div className="card">
          <div style={{ display: "flex", alignItems: "center", gap: 12, flexWrap: "wrap" }}>
            <span className="entid">{current.entId}</span>
            <span style={{ fontSize: 20, fontWeight: 800, flex: 1, minWidth: 160 }}>{current.candidat}</span>
            {decBadge(current.decision)}
          </div>
          <div style={{ fontFamily: "var(--mono)", fontSize: 12, color: "var(--muted)", marginTop: 8 }}>
            {t("ent_recruteur")} : {current.recruteur} · ⏱ {current.temps} · {fmtTs(current.createdAt, lang)}
            {current.grab ? ` · 🎣 ${current.grab}` : ""}
          </div>
          <div style={{ marginTop: 18, display: "grid", gap: 16 }}>
            <div className="frow">
              <div className="flabel">{t("ent_profil")}</div>
              <div className="quote">{current.profil}</div>
            </div>
            <div className="frow">
              <div className="flabel">{t("ent_oral")}</div>
              <div className="quote">{current.oral}</div>
            </div>
          </div>
          {canManage && (
            <div className="row" style={{ marginTop: 20 }}>
              {Object.keys(DECS).map((d) => (
                <button key={d} className={`tabchip ${current.decision === d ? "on" : ""}`} onClick={() => setDecision(current, d)}>
                  {DECS[d].icon} {t(DECS[d].tkey)}
                </button>
              ))}
            </div>
          )}
        </div>
      </div>
    );
  }

  if (sub === "new") {
    return (
      <div style={{ display: "grid", gap: 20 }}>
        <div className="row" style={{ alignItems: "center" }}>
          <button className="btn btn-sm" onClick={() => setSub("list")}>{t("rpt_back")}</button>
          <div style={{ fontSize: 20, fontWeight: 800, flex: 1 }}>{t("ent_new")}</div>
        </div>
        <div className="card" style={{ display: "grid", gap: 14 }}>
          <div className="grid2">
            <Field label={t("ent_candidat")}><input value={fCand} onChange={(e) => setFCand(e.target.value)} placeholder="@pseudo" /></Field>
            <Field label={t("ent_temps")}><input value={fTemps} onChange={(e) => setFTemps(e.target.value)} placeholder="Ex : 10min" /></Field>
          </div>
          <Field label={t("ent_profil")}>
            <textarea value={fProfil} onChange={(e) => setFProfil(e.target.value)}
              placeholder="Mentalité, maturité, attitude, motivation…" style={{ minHeight: 100 }} />
          </Field>
          <Field label={t("ent_oral")}>
            <textarea value={fOral} onChange={(e) => setFOral(e.target.value)}
              placeholder="Éloquence, clarté, raisonnement…" style={{ minHeight: 100 }} />
          </Field>
          <Field label={t("ent_grab")}><input value={fGrab} onChange={(e) => setFGrab(e.target.value)} /></Field>
          <div>
            <label>{t("ent_decision")}</label>
            <div className="row">
              {Object.keys(DECS).map((d) => (
                <button key={d} className={`tabchip ${fDec === d ? "on" : ""}`}
                  style={fDec === d ? { borderColor: DECS[d].color, color: DECS[d].color, background: "transparent" } : {}}
                  onClick={() => setFDec(d)}>
                  {DECS[d].icon} {t(DECS[d].tkey)}
                </button>
              ))}
            </div>
          </div>
          <div><button className="btn btn-primary" onClick={create} disabled={busy}>{t("ent_create")}</button></div>
        </div>
      </div>
    );
  }

  return (
    <div style={{ display: "grid", gap: 18 }}>
      <div className="row" style={{ alignItems: "flex-end" }}>
        <div style={{ flex: 1 }}>
          <PageHead crumb={t("sec_entretien")} title={t("ent_title")} sub={t("ent_sub")} />
        </div>
        <button className="btn btn-primary btn-sm" onClick={() => { resetForm(); setSub("new"); }}>+ {t("ent_new")}</button>
      </div>
      <div className="row">
        {[["all", "ent_total", "var(--accent)", stats.total],
          ["accepte", "dec_accepte", "var(--ok)", stats.accepte],
          ["attente", "dec_attente", "var(--warn)", stats.attente],
          ["refuse", "dec_refuse", "var(--bad)", stats.refuse]].map(([k, lk, c, v]) => (
          <button key={k} className="stattile" style={{ "--sc": c, cursor: "pointer", textAlign: "left",
            outline: decFilter === k ? `1px solid ${c}` : "none", fontFamily: "var(--sans)", color: "var(--text)" }}
            onClick={() => setDecFilter(k)}>
            <div className="sv" style={{ color: k === "all" ? "var(--text)" : c }}>{v}</div>
            <div className="sl">{t(lk)}</div>
          </button>
        ))}
      </div>
      <input value={query} onChange={(e) => setQuery(e.target.value)} placeholder={t("ent_search_ph")} />
      <div className="card" style={{ padding: 0, overflow: "hidden" }}>
        {list.length === 0 ? (
          <div style={{ color: "var(--muted)", fontSize: 13.5, padding: "18px 16px", textAlign: "center" }}>{t("ent_empty")}</div>
        ) : (
          list.map((e) => (
            <div key={e.id} className="rrow" onClick={() => { setCurrent(e); setSub("view"); }}>
              <span className="entid">{e.entId}</span>
              <span style={{ flex: 1, minWidth: 0 }}>
                <span style={{ display: "block", fontWeight: 700, fontSize: 14.5 }}>{e.candidat}</span>
                <span style={{ display: "block", fontSize: 12, color: "var(--muted)", marginTop: 2 }}>
                  {t("ent_recruteur")} : {e.recruteur} · {fmtTs(e.createdAt, lang)}
                </span>
              </span>
              {decBadge(e.decision)}
              <span style={{ color: "var(--muted)" }}>›</span>
            </div>
          ))
        )}
      </div>
    </div>
  );
}


/* ===================== ACCÈS RAPIDE ALÉATOIRE ===================== */
function QuickTiles({ t, go }) {
  const [tiles] = useState(() => {
    const pool = [
      { id: "planning", icon: "🗓️", k: "tab_planning" },
      { id: "realisees", icon: "✅", k: "tab_done" },
      { id: "rapports", icon: "📄", k: "nav_rapports" },
      { id: "entretiens", icon: "🤝", k: "ent_title" },
      { id: "docs", icon: "📚", k: "tab_docs" },
      { id: "users", icon: "👥", k: "nav_users" },
    ];
    for (let i = pool.length - 1; i > 0; i--) {
      const j = Math.floor(Math.random() * (i + 1));
      [pool[i], pool[j]] = [pool[j], pool[i]];
    }
    return pool.slice(0, 4);
  });
  return (
    <div className="qtiles">
      {tiles.map((x) => (
        <button key={x.id} className="qtile" onClick={() => go(x.id)}>
          <div className="qi">{x.icon}</div>
          <div className="qn">{t(x.k)}</div>
        </button>
      ))}
    </div>
  );
}


/* ===================== REQUÊTES ===================== */
function RequestBox({ flash, t, reload }) {
  const [txt, setTxt] = useState("");
  const [busy, setBusy] = useState(false);
  async function send() {
    if (!txt.trim()) return flash(t("req_need"));
    if (busy) return;
    setBusy(true);
    const r = await api("/api/requests", "POST", { text: txt.trim() });
    setBusy(false);
    if (!r.ok) return flash(t("server_err"));
    setTxt("");
    if (reload) await reload();
    flash(t("req_sent"));
  }
  return (
    <div className="card">
      <div className="eyebrow" style={{ marginBottom: 10 }}>💬 {t("req_home_title")}</div>
      <p style={{ color: "var(--muted)", fontSize: 13, margin: "0 0 12px", lineHeight: 1.6 }}>{t("req_home_sub")}</p>
      <textarea value={txt} onChange={(e) => setTxt(e.target.value)} placeholder={t("req_ph")} style={{ minHeight: 80 }} />
      <div style={{ marginTop: 10, textAlign: "right" }}>
        <button className="btn btn-primary btn-sm" onClick={send} disabled={busy}>{t("req_send")}</button>
      </div>
    </div>
  );
}

function Requests({ requests, reload, t, lang, flash }) {
  async function remove(r) {
    const res = await api(`/api/requests/${r.id}`, "DELETE");
    if (!res.ok) return flash(t("server_err"));
    await reload();
    flash(t("req_deleted"));
  }
  return (
    <div style={{ display: "grid", gap: 20 }}>
      <PageHead crumb="ADMIN" title={t("req_title")} sub={t("req_admin_sub")} />
      <div className="card" style={{ padding: 0, overflow: "hidden" }}>
        {requests.length === 0 ? (
          <div style={{ color: "var(--muted)", fontSize: 13.5, padding: "18px 16px", textAlign: "center" }}>{t("req_empty")}</div>
        ) : (
          requests.map((r) => (
            <div key={r.id} style={{ padding: "14px 16px", borderTop: "1px solid var(--border)" }}>
              <div style={{ display: "flex", alignItems: "center", gap: 10, flexWrap: "wrap" }}>
                <span style={{ fontWeight: 700, fontSize: 14 }}>{r.by}</span>
                <span className="badge" style={{ color: (ROLE_META[r.role] || {}).color || "var(--muted)", fontSize: 9.5 }}>
                  {ROLE_META[r.role] ? t(ROLE_META[r.role].tkey) : r.role}
                </span>
                <span style={{ fontFamily: "var(--mono)", fontSize: 11, color: "var(--muted)", flex: 1 }}>{fmtTs(r.ts, lang)}</span>
                <button className="btn btn-bad btn-sm" onClick={() => remove(r)}>{t("del")}</button>
              </div>
              <div style={{ marginTop: 8, fontSize: 14, lineHeight: 1.7, whiteSpace: "pre-wrap" }}>{r.text}</div>
            </div>
          ))
        )}
      </div>
    </div>
  );
}


/* ===================== ÉQUIPES DE BRANCHE ===================== */
function BranchTeam({ branch, accounts, t }) {
  const meta = BRANCHES[branch];
  const tkey = "team_" + branch;
  const members = accounts
    .filter((a) => (a.grades || []).includes(branch))
    .sort((x, y) => ((RANK[x.role] ?? 9) - (RANK[y.role] ?? 9)) || x.pseudo.localeCompare(y.pseudo));
  return (
    <div style={{ display: "grid", gap: 20 }}>
      <PageHead crumb={t("sec_corps")} title={`${meta.icon} ${t(tkey)}`} sub={t("hier_hint")} />
      <div className="stattile" style={{ "--sc": meta.color, maxWidth: 260 }}>
        <div className="sv" style={{ color: meta.color }}>{members.length}</div>
        <div className="sl">{t("team_count")}</div>
      </div>
      <div className="card" style={{ borderTop: `2px solid ${meta.color}`, padding: 0, overflow: "hidden" }}>
        {members.length === 0 ? (
          <div style={{ color: "var(--muted)", fontSize: 13.5, padding: "18px 16px", textAlign: "center" }}>{t("hier_none")}</div>
        ) : (
          members.map((a) => (
            <div key={a.id} className="urow">
              <span style={{ display: "flex", alignItems: "center", gap: 12 }}>
                <span className="avatar">
                  {a.avatar ? <img src={a.avatar} alt="" /> : a.pseudo.slice(0, 1).toUpperCase()}
                </span>
                <span style={{ fontWeight: 600, fontSize: 14.5 }}>{a.pseudo}</span>
              </span>
              <span style={{ display: "flex", alignItems: "center", gap: 8 }}>
                {(a.grades || []).map((g) => BRANCHES[g] ? (
                  <span key={g} title={t(BRANCHES[g].tkey)} style={{ fontSize: 13 }}>{BRANCHES[g].icon}</span>
                ) : null)}
                <span className="badge" style={{ color: ROLE_META[a.role].color }}>{t(ROLE_META[a.role].tkey)}</span>
              </span>
            </div>
          ))
        )}
      </div>
    </div>
  );
}


/* ===================== SÉLECTEUR DE RÔLES MULTIPLES ===================== */
function RolePicker({ value, onChange, options, t }) {
  const opts = (options || ROLES_ORDER).filter((r) => !value.includes(r));
  return (
    <span style={{ display: "inline-flex", flexWrap: "wrap", gap: 6, alignItems: "center" }}>
      {value.map((r) => (
        <span key={r} className="badge" style={{ color: (ROLE_META[r] || {}).color, display: "inline-flex", alignItems: "center", gap: 6 }}>
          {t((ROLE_META[r] || ROLE_META.formateur).tkey)}
          {value.length > 1 && (
            <button onClick={() => onChange(value.filter((x) => x !== r))}
              style={{ background: "none", border: "none", color: "inherit", cursor: "pointer", padding: 0, fontSize: 11, lineHeight: 1 }}>✕</button>
          )}
        </span>
      ))}
      {opts.length > 0 && (
        <select className="roleselect" value="" onChange={(e) => { if (e.target.value) onChange([...value, e.target.value]); }}>
          <option value="">＋ {t("adm_add_role")}</option>
          {opts.map((r) => <option key={r} value={r}>{t((ROLE_META[r] || ROLE_META.formateur).tkey)}</option>)}
        </select>
      )}
    </span>
  );
}
