import React, {
  useCallback,
  useEffect,
  useMemo,
  useRef,
  useState,
  type FormEvent,
  type ReactNode,
} from "react";
import { createRoot } from "react-dom/client";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import type {
  BglandsLauncherApi,
  CommunityPlayerRanking,
  LauncherModuleId,
  LauncherPhase,
  LauncherSettings,
  LauncherSnapshot,
} from "../shared/contracts.ts";

type PageId =
  | "home"
  | "play"
  | "news"
  | "servers"
  | "clans"
  | "rankings"
  | "shop"
  | "modules"
  | "rules"
  | "settings";

type IconName =
  | "home" | "play" | "news" | "server" | "clan" | "rank" | "shop"
  | "modules" | "rules" | "settings" | "user" | "copy" | "shield"
  | "close" | "check" | "repair" | "log" | "arrow" | "wallet"
  | "clock" | "swords" | "map" | "memory" | "ping" | "globe"
  | "more" | "logout" | "external";

const ALL_PAGES: Array<{ id: PageId; label: string; icon: IconName }> = [
  { id: "home", label: "Accueil", icon: "home" },
  { id: "play", label: "Jouer", icon: "play" },
  { id: "news", label: "Actualités", icon: "news" },
  { id: "servers", label: "Serveurs", icon: "server" },
  { id: "clans", label: "Clans", icon: "clan" },
  { id: "rankings", label: "Classements", icon: "rank" },
  { id: "shop", label: "Boutique", icon: "shop" },
  { id: "modules", label: "Modules", icon: "modules" },
  { id: "rules", label: "Règlement", icon: "rules" },
  { id: "settings", label: "Paramètres", icon: "settings" },
];

const PRIMARY_NAV = ALL_PAGES.filter(({ id }) =>
  ["home", "play", "servers", "clans", "modules"].includes(id),
);

const ICONS: Record<IconName, ReactNode> = {
  home: <><path d="m3 11 9-8 9 8"/><path d="M5 10v10h14V10M9 20v-6h6v6"/></>,
  play: <path d="m8 5 11 7-11 7V5Z"/>,
  news: <><path d="M5 4h14v16H5z"/><path d="M8 8h8M8 12h8M8 16h5"/></>,
  server: <><rect x="4" y="4" width="16" height="6" rx="2"/><rect x="4" y="14" width="16" height="6" rx="2"/><path d="M8 7h.01M8 17h.01M12 7h5M12 17h5"/></>,
  clan: <><path d="M12 3 4 7v5c0 5 3.4 8 8 10 4.6-2 8-5 8-10V7l-8-4Z"/><path d="m8 13 2.5 2.5L16 10"/></>,
  rank: <><path d="M8 21h8M12 17v4M7 4h10v4a5 5 0 0 1-10 0V4Z"/><path d="M7 6H4v2a4 4 0 0 0 4 4M17 6h3v2a4 4 0 0 1-4 4"/></>,
  shop: <><path d="M4 9h16l-1 11H5L4 9Z"/><path d="M8 9V7a4 4 0 0 1 8 0v2"/></>,
  modules: <><rect x="3" y="3" width="7" height="7" rx="2"/><rect x="14" y="3" width="7" height="7" rx="2"/><rect x="3" y="14" width="7" height="7" rx="2"/><path d="M17.5 14v7M14 17.5h7"/></>,
  rules: <><path d="M6 3h12v18H6z"/><path d="M9 7h6M9 11h6M9 15h4"/></>,
  settings: <><circle cx="12" cy="12" r="3"/><path d="M20 13.5v-3l-2-.7-.7-1.7.9-1.9-2.1-2.1-1.9.9-1.7-.7-.7-2h-3l-.7 2-1.7.7-1.9-.9-2.1 2.1.9 1.9-.7 1.7-2 .7v3l2 .7.7 1.7-.9 1.9 2.1 2.1 1.9-.9 1.7.7.7 2h3l.7-2 1.7-.7 1.9.9 2.1-2.1-.9-1.9.7-1.7 2-.7Z"/></>,
  user: <><circle cx="12" cy="8" r="4"/><path d="M4 21a8 8 0 0 1 16 0"/></>,
  copy: <><rect x="8" y="8" width="11" height="11" rx="2"/><path d="M16 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2"/></>,
  shield: <><path d="M12 3 4 6v6c0 4.8 3 8 8 10 5-2 8-5.2 8-10V6l-8-3Z"/><path d="m8 12 2.5 2.5L16 9"/></>,
  close: <path d="m6 6 12 12M18 6 6 18"/>,
  check: <path d="m5 12 4 4L19 6"/>,
  repair: <><path d="M20 7v5h-5M4 17v-5h5"/><path d="M7 7a7 7 0 0 1 12 3l1 2M4 12l1 2a7 7 0 0 0 12 3"/></>,
  log: <><path d="M6 3h12v18H6z"/><path d="M9 8h6M9 12h6M9 16h4"/></>,
  arrow: <path d="m9 18 6-6-6-6"/>,
  wallet: <><path d="M4 6h14a2 2 0 0 1 2 2v10H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h12"/><path d="M15 11h5v4h-5a2 2 0 0 1 0-4Z"/></>,
  clock: <><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></>,
  swords: <><path d="m4 20 5-5M3 4l7 7-3 3-7-7 3-3ZM20 4l-7 7 3 3 7-7-3-3ZM20 20l-5-5"/></>,
  map: <><path d="m3 6 6-3 6 3 6-3v15l-6 3-6-3-6 3V6Z"/><path d="M9 3v15M15 6v15"/></>,
  memory: <><rect x="5" y="5" width="14" height="14" rx="2"/><path d="M9 9h6v6H9zM9 2v3M15 2v3M9 19v3M15 19v3M2 9h3M19 9h3M2 15h3M19 15h3"/></>,
  ping: <><path d="M5 15a10 10 0 0 1 14 0M8 18a6 6 0 0 1 8 0"/><circle cx="12" cy="21" r="1"/></>,
  globe: <><circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3a15 15 0 0 1 0 18M12 3a15 15 0 0 0 0 18"/></>,
  more: <><circle cx="5" cy="12" r="1"/><circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/></>,
  logout: <><path d="M10 5H5v14h5M14 8l4 4-4 4M18 12H9"/></>,
  external: <><path d="M14 4h6v6M20 4l-9 9"/><path d="M18 13v6a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h6"/></>,
};

function Icon({ name, size = 20 }: { name: IconName; size?: number }) {
  return <svg className="icon" width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">{ICONS[name]}</svg>;
}

const MODULES: Array<{ id: LauncherModuleId; title: string; description: string; icon: IconName; tag: string }> = [
  { id: "keystrokes", title: "Keystrokes", description: "ZQSD, saut et touches pressées.", icon: "modules", tag: "HUD" },
  { id: "cps", title: "Compteur CPS", description: "Clics gauche et droit en direct.", icon: "swords", tag: "PVP" },
  { id: "ping", title: "Latence", description: "Ping réel vers BG LANDS.", icon: "ping", tag: "RÉSEAU" },
  { id: "serverAddress", title: "Adresse serveur", description: "Serveur actuel toujours visible.", icon: "server", tag: "RÉSEAU" },
  { id: "resources", title: "Performances", description: "FPS et mémoire du client.", icon: "memory", tag: "CLIENT" },
  { id: "coordinates", title: "Coordonnées", description: "Position sans ouvrir le debug.", icon: "map", tag: "MONDE" },
  { id: "minimap", title: "Mini-carte tactique", description: "Grille locale, sans joueurs ni minerais.", icon: "map", tag: "BÊTA" },
];

const RULES = [
  { id: "essential", number: "01", title: "Principe essentiel", lead: "Une zone claim est protégée ; une zone non claim expose volontairement ses ressources au PvP et au pillage.", allowed: ["Le PvP est autorisé en zone non claim.", "Le vol dans les coffres et conteneurs accessibles est autorisé.", "Le stuff tombé lors d’un combat loyal peut être récupéré.", "Protéger une base et ses coffres relève de la responsabilité du joueur ou du clan : utilisez les claims."], forbidden: ["Le grief gratuit ou massif et la destruction des infrastructures publiques.", "Le TP-kill, le spawn-kill répété, les pièges sur un spawn public et le combat log.", "Toute triche, duplication, exploitation de bug ou technique explicitement interdite."] },
  { id: "claims", number: "02", title: "Zones claim — protection totale", lead: "Dans une zone claim, la protection du territoire prime sur le combat et le pillage.", forbidden: ["Attaquer, tuer ou provoquer un joueur : tout PvP est interdit.", "Prendre, déplacer ou conserver le stuff d’un autre joueur.", "Ouvrir, vider ou voler un coffre, une shulker, un tonneau ou tout autre conteneur.", "Casser, brûler, noyer, piéger ou modifier une construction sans autorisation.", "Contourner les protections par téléportation, piston, explosion, bug, entité ou membre complice."], note: "Ajouter un joueur à son claim ou à son clan lui accorde un accès réel. Choisissez vos membres avec prudence." },
  { id: "pvp", number: "03", title: "PvP — techniques et limites", lead: "Le combat loyal est autorisé uniquement dans les zones prévues pour le PvP.", allowed: ["Le combat loyal en zone non claim et dans les modes prévus.", "Les embuscades, poursuites et stratégies normales, sans harcèlement ciblé.", "Le Rod PvP avec les mécaniques normales du jeu."], forbidden: ["Le Crystal PvP est interdit.", "Les techniques explosives visant un kill instantané ou le contournement d’une protection.", "Macros, auto-clic, scripts, clients modifiés, désynchronisation volontaire ou lag artificiel."], note: "Une technique autorisée ne doit pas être détournée pour bloquer, harceler ou exploiter une faille." },
  { id: "cheat", number: "04", title: "Triche, duplication et bugs", lead: "Tout avantage obtenu par un outil tiers, une automatisation ou une faille doit être stoppé et signalé.", forbidden: ["Killaura, reach, aim assist, fly, speed, no-clip, anti-knockback, auto-totem, scaffold, ESP, radar, X-Ray, freecam et équivalents.", "Auto-clic, macros, scripts, bots, maintien mécanique d’une touche et farm automatique en étant absent.", "Toute duplication d’item, bloc, monnaie, coffre, shulker, entité ou récompense.", "Utiliser, cacher, vendre, donner ou blanchir un bien dupliqué.", "Provoquer un crash ou une surcharge volontaire."], note: "Une duplication accidentelle ou un bug doit être arrêté immédiatement, gardé confidentiel et signalé par ticket." },
  { id: "fairplay", number: "05", title: "Jeu loyal, signalements et sanctions", lead: "Le staff protège la qualité de jeu, les performances et l’intégrité de la communauté.", forbidden: ["Machines à lag, horloges redstone incontrôlées et amas excessifs d’entités.", "Pseudos, skins, constructions ou noms de clans haineux, sexuels, insultants ou usurpant le staff.", "Se faire justice soi-même après un vol, un cheat ou un exploit."], note: "Selon la gravité : confiscation, rollback, remboursement, exclusion, bannissement temporaire ou définitif. Les décisions sont contestables uniquement par ticket." },
];

const EMPTY_SNAPSHOT: LauncherSnapshot = {
  phase: "booting", launcherVersion: "3.0.0", gameVersion: "—", statusMessage: "Initialisation du launcher…",
  account: null, deviceCode: null, progress: null, error: null,
  server: { host: "game.bglands.fr", port: 25565, online: null, playersOnline: null, playersMaximum: null },
  client: { state: "missing", version: null, installedAt: null, components: 0 },
  microsoft: { configured: false, clientId: "", source: "missing" },
  community: { state: "loading", updatedAt: null, playersOnline: null, servers: [], fortunes: [], playtime: [], clans: [], pvpAvailable: false },
  settings: { minimumMemoryMb: 2048, maximumMemoryMb: 6144, width: 1280, height: 720, autoConnect: true, closeLauncherOnGameStart: false, modules: { keystrokes: true, cps: true, ping: true, serverAddress: true, resources: true, coordinates: true, minimap: false } },
};

const BUSY = new Set<LauncherPhase>(["booting", "verifying", "downloading", "installing", "launching"]);

function currentPage(): PageId {
  const value = window.location.hash.replace(/^#/, "") as PageId;
  return ALL_PAGES.some(({ id }) => id === value) ? value : "home";
}

function formatNumber(value: number): string { return new Intl.NumberFormat("fr-FR").format(value); }
function formatPlaytime(seconds: number): string { const hours = Math.floor(seconds / 3600); const minutes = Math.floor((seconds % 3600) / 60); return `${hours} h ${minutes.toString().padStart(2, "0")}`; }
function initials(name: string): string { return name.replace(/[^a-z0-9]/giu, "").slice(0, 2).toUpperCase() || "BG"; }

function StatusPill({ snapshot, compact = false }: { snapshot: LauncherSnapshot; compact?: boolean }) {
  const players = snapshot.community.playersOnline ?? snapshot.server.playersOnline;
  const text = snapshot.server.online === true ? `${players ?? 0}${compact ? "" : " en ligne"}` : snapshot.server.online === false ? "Hors ligne" : "Connexion";
  return <span className={`status-pill ${snapshot.server.online === true ? "is-online" : snapshot.server.online === false ? "is-offline" : ""}`} role="status" aria-atomic="true"><i/>{text}</span>;
}

const Button = React.forwardRef<HTMLButtonElement, React.ButtonHTMLAttributes<HTMLButtonElement> & { variant?: "primary" | "secondary" | "ghost" | "danger"; icon?: IconName }>(function Button({ children, variant = "primary", icon, ...props }, ref) {
  return <button ref={ref} className={`button button--${variant}`} type="button" {...props}>{icon && <Icon name={icon} size={18}/>}<span>{children}</span></button>;
});

function PageTitle({ kicker, title, copy, action }: { kicker: string; title: string; copy: string; action?: ReactNode }) {
  return <header className="page-title"><div><span className="kicker">{kicker}</span><h1 tabIndex={-1}>{title}</h1><p>{copy}</p></div>{action}</header>;
}

function EmptyState({ icon, title, children }: { icon: IconName; title: string; children: ReactNode }) {
  return <div className="empty-state"><span><Icon name={icon}/></span><h2>{title}</h2><p>{children}</p></div>;
}

function HomePage({ snapshot, navigate, onPrimary }: { snapshot: LauncherSnapshot; navigate: (page: PageId) => void; onPrimary: () => void }) {
  const primaryLabel = snapshot.account ? snapshot.phase === "ready" ? "Jouer maintenant" : snapshot.phase === "running" ? "Minecraft ouvert" : "Voir le lancement" : "Se connecter pour jouer";
  const topClan = snapshot.community.clans[0];
  return <div className="scene scene--home page"><div className="scene__backdrop"/><section className="home-hero"><div className="hero-badges"><span>Survie</span><span>Clans</span><span>Cross-play</span></div><span className="kicker kicker--gold">Royaumes, alliances et conquêtes</span><h1 tabIndex={-1}>Bâtis ton royaume.<br/><em>Marque l’histoire.</em></h1><p>La survie française où tes terres, ton clan et ton économie comptent vraiment.</p><div className="hero-actions"><Button icon="play" onClick={onPrimary} disabled={snapshot.phase === "running"}>{primaryLabel}</Button><button className="round-action" type="button" aria-label="Afficher les serveurs" onClick={() => navigate("servers")}><Icon name="server"/></button><StatusPill snapshot={snapshot}/></div><div className="download-state"><div><span>{snapshot.statusMessage}</span><strong>{snapshot.client.state === "verified" ? "Client à jour" : snapshot.client.state === "installed" ? "Client installé" : "Installation automatique"}</strong></div><div className="progress-track"><i style={{ transform: `scaleX(${snapshot.progress?.percent ? snapshot.progress.percent / 100 : snapshot.client.state === "verified" ? 1 : 0.08})` }}/></div></div></section><section className="home-lower" aria-label="Actualités et communauté"><div className="section-heading"><div><span className="kicker">Actualités</span><h2>En ce moment sur BG LANDS</h2></div><button type="button" onClick={() => navigate("news")}>Tout voir <Icon name="arrow" size={15}/></button></div><div className="news-cards"><button className="story-card story-card--launch" type="button" onClick={() => navigate("play")}><span>LAUNCHER 3.0</span><strong>Une nouvelle porte vers le royaume</strong><small>Client officiel · aujourd’hui</small></button><button className="story-card story-card--clan" type="button" onClick={() => navigate("clans")}><span>CLANS</span><strong>{topClan ? `${topClan.name} mène la conquête` : "Crée ton clan, protège tes terres"}</strong><small>{topClan ? `${topClan.members} membres · ${topClan.claims} claims` : "Survie publique"}</small></button><button className="story-card story-card--crossplay" type="button" onClick={() => navigate("servers")}><span>JAVA + BEDROCK</span><strong>Un seul monde, toutes les plateformes</strong><small>Geyser · Floodgate · Velocity</small></button></div></section></div>;
}

function AuthGateway({ snapshot, onConnect, onOpenLogs }: { snapshot: LauncherSnapshot; onConnect: () => void; onOpenLogs: () => void }) {
  return <section className="auth-gateway"><div className="auth-gateway__form"><img src="../resources/assets/logo.png" alt="BG LANDS"/><div><span className="kicker kicker--gold">Compte joueur</span><h1 tabIndex={-1}>Bienvenue sur<br/>BG LANDS</h1><p>Connecte le compte Microsoft qui possède Minecraft Java Edition. L’identification reste sur le site officiel Microsoft.</p></div><div className="identity-field" aria-label="Méthode de connexion Microsoft"><span><Icon name="shield"/></span><div><small>Connexion sécurisée</small><strong>Microsoft + Minecraft</strong></div><Icon name="arrow" size={17}/></div>{snapshot.error && <div className="auth-error" role="alert"><strong>{snapshot.error.title}</strong><p>{snapshot.error.message}</p><button type="button" onClick={onOpenLogs}>Ouvrir le diagnostic</button></div>}<Button icon="external" onClick={onConnect} disabled={!snapshot.microsoft.configured || BUSY.has(snapshot.phase)}>Continuer avec Microsoft</Button>{!snapshot.microsoft.configured && <p className="auth-warning">L’application Microsoft BG LANDS doit d’abord être configurée par l’administrateur.</p>}<div className="auth-trust"><span><Icon name="check"/> Aucun mot de passe transmis</span><span><Icon name="check"/> Jetons chiffrés localement</span></div><small className="legal-copy">En continuant, tu confirmes posséder une licence Minecraft valide. BG LANDS n’est ni associé ni approuvé par Mojang ou Microsoft.</small></div><div className="auth-gateway__art" aria-hidden="true"><div><span>LE ROYAUME T’ATTEND</span><strong>Une identité.<br/>Toute ton aventure.</strong></div></div></section>;
}

function LaunchCenter({ snapshot, onPlay, onRepair, onDisconnect, navigate }: { snapshot: LauncherSnapshot; onPlay: () => void; onRepair: () => void; onDisconnect: () => void; navigate: (page: PageId) => void }) {
  const locked = BUSY.has(snapshot.phase) || snapshot.phase === "running";
  return <div className="scene scene--launch page"><div className="scene__backdrop"/><section className="launch-copy"><span className="kicker kicker--gold">Profil officiel BG LANDS</span><h1 tabIndex={-1}>Salut, {snapshot.account?.displayName}</h1><p>Ton client Fabric, les mods obligatoires et les réglages HUD sont synchronisés automatiquement.</p><div className="launch-profile"><span className="avatar avatar--large">{initials(snapshot.account?.displayName ?? "BG")}</span><div><small>Compte Minecraft</small><strong>{snapshot.account?.displayName}</strong><span><Icon name="shield" size={14}/> Vérifié au lancement</span></div></div><div className="hero-actions"><Button icon="play" onClick={onPlay} disabled={locked || snapshot.server.online === false}>{snapshot.phase === "running" ? "En jeu" : "Lancer BG LANDS"}</Button><StatusPill snapshot={snapshot}/></div><div className="download-state"><div><span>État du client</span><strong>{snapshot.statusMessage}</strong></div><div className="progress-track"><i style={{ transform: `scaleX(${snapshot.progress?.percent ? snapshot.progress.percent / 100 : snapshot.client.state === "verified" ? 1 : 0.12})` }}/></div></div></section><aside className="launch-tools"><button type="button" onClick={onRepair}><Icon name="repair"/><span><strong>Réparer</strong><small>Vérifier les fichiers</small></span></button><button type="button" onClick={() => navigate("modules")}><Icon name="modules"/><span><strong>Modules</strong><small>Personnaliser le HUD</small></span></button><button type="button" onClick={() => navigate("settings")}><Icon name="settings"/><span><strong>Paramètres</strong><small>RAM et résolution</small></span></button><button type="button" onClick={onDisconnect}><Icon name="logout"/><span><strong>Déconnecter</strong><small>Retirer ce compte</small></span></button></aside></div>;
}

function PlayPage({ snapshot, onConnect, onDisconnect, onPlay, onRepair, onLogs, navigate }: { snapshot: LauncherSnapshot; onConnect: () => void; onDisconnect: () => void; onPlay: () => void; onRepair: () => void; onLogs: () => void; navigate: (page: PageId) => void }) {
  return snapshot.account ? <LaunchCenter snapshot={snapshot} onPlay={onPlay} onRepair={onRepair} onDisconnect={onDisconnect} navigate={navigate}/> : <div className="page page--auth"><AuthGateway snapshot={snapshot} onConnect={onConnect} onOpenLogs={onLogs}/></div>;
}

function NewsPage() {
  const stories = [["LAUNCHER 3.0", "Une expérience entièrement reconstruite", "Connexion intégrée, scènes plus immersives et navigation recentrée autour du jeu."], ["SURVIE", "Les royaumes entrent en scène", "Clans, claims, économie commune et conquêtes donnent du poids à chaque alliance."], ["CROSS-PLAY", "Java et Bedrock réunis", "Velocity, Paper, Geyser et Floodgate relient la communauté sur un seul réseau."], ["SÉCURITÉ", "Une distribution contrôlée", "Chaque composant client est vérifié avant son installation dans une instance isolée."]];
  return <div className="page content-page"><PageTitle kicker="Journal du royaume" title="Actualités" copy="Les mises à jour utiles du réseau et du client officiel."/><div className="editorial-grid">{stories.map(([tag, title, copy], index) => <article className={index === 0 ? "editorial-card editorial-card--lead" : "editorial-card"} key={title}><div className={`editorial-card__art art-${index + 1}`}/><div><span>{tag}</span><h2>{title}</h2><p>{copy}</p><small>30 août 2026</small></div></article>)}</div></div>;
}

function ServersPage({ snapshot, onPlay }: { snapshot: LauncherSnapshot; onPlay: () => void }) {
  const networkPlayers = snapshot.community.playersOnline ?? snapshot.server.playersOnline ?? 0;
  const communitySurvival = snapshot.community.servers.find(({ id }) => id.includes("survival"));
  const cards = [
    { id: "java-survival", label: "Survie Java", tag: "CLIENT OFFICIEL", players: communitySurvival?.players ?? networkPlayers, copy: "Claims, économie, métiers, quêtes et clans.", address: snapshot.server.host, tone: "gold" },
    { id: "bedrock-gateway", label: "Survie Bedrock", tag: "SANS MODS", players: networkPlayers, copy: "Le même monde depuis mobile, console ou PC.", address: `${snapshot.server.host}:19157`, tone: "green" },
    { id: "network-hub", label: "Hub du réseau", tag: "COMMUNAUTÉ", players: networkPlayers, copy: "Retrouve les joueurs et choisis ton aventure.", address: snapshot.server.host, tone: "blue" },
  ];
  return <div className="page server-scene"><div className="server-scene__veil"/><PageTitle kicker="Réseau cross-play" title="Choisis ton accès" copy="Trois portes d’entrée, une seule progression BG LANDS." action={<StatusPill snapshot={snapshot}/>}/><div className="server-choices">{cards.map((server) => <article className={`server-choice server-choice--${server.tone}`} key={server.id}><div className="server-choice__visual"><span><i/>{server.players} en ligne</span></div><div><span>{server.tag}</span><h2>{server.label}</h2><p>{server.copy}</p><code>{server.address}</code><Button variant="secondary" icon={server.id === "java-survival" ? "play" : "copy"} onClick={server.id === "java-survival" ? onPlay : () => void navigator.clipboard.writeText(server.address)}>{server.id === "java-survival" ? "Jouer" : "Copier l’adresse"}</Button></div></article>)}</div></div>;
}

function ClansPage({ snapshot, navigate }: { snapshot: LauncherSnapshot; navigate: (page: PageId) => void }) {
  return <div className="page content-page"><PageTitle kicker="Territoires et alliances" title="Clans" copy="Les royaumes les plus puissants de la survie publique." action={<button className="inline-action" type="button" onClick={() => navigate("rankings")}><Icon name="rank"/> Classements joueurs</button>}/>{snapshot.community.clans.length ? <div className="clan-board">{snapshot.community.clans.map((clan, index) => <article className={index === 0 ? "clan-row clan-row--first" : "clan-row"} key={clan.tag}><span className="clan-position">{String(index + 1).padStart(2, "0")}</span><span className="clan-emblem">{clan.tag.slice(0, 2)}</span><div><small>[{clan.tag}] · niveau {clan.level}</small><h2>{clan.name}</h2><p>{clan.members} membres · {clan.claims} claims</p></div><dl><div><dt>Puissance</dt><dd>{formatNumber(clan.power)}</dd></div><div><dt>Banque</dt><dd>{formatNumber(clan.bankBalance)}</dd></div></dl></article>)}</div> : <EmptyState icon="clan" title="Synchronisation des clans">Les données réelles apparaîtront automatiquement dès que le flux communautaire répondra.</EmptyState>}</div>;
}

function RankingRows({ rows, kind }: { rows: CommunityPlayerRanking[]; kind: "fortune" | "playtime" }) {
  return <div className="ranking-board">{rows.map((entry, index) => <div className="ranking-row" key={`${entry.platform}-${entry.name}`}><span>{String(index + 1).padStart(2, "0")}</span><i className="avatar">{initials(entry.name)}</i><div><strong>{entry.name}</strong><small>{entry.platform}</small></div><b>{kind === "fortune" ? `${formatNumber(entry.value)} Ém.` : formatPlaytime(entry.value)}</b></div>)}</div>;
}

function RankingsPage({ snapshot }: { snapshot: LauncherSnapshot }) {
  const [tab, setTab] = useState<"fortune" | "playtime" | "pvp">("fortune");
  const rows = tab === "fortune" ? snapshot.community.fortunes : snapshot.community.playtime;
  return <div className="page content-page"><PageTitle kicker="Honneur et compétition" title="Classements" copy="Les performances publiques de la survie, sans données inventées."/><div className="segmented" role="tablist" aria-label="Classement"><button role="tab" aria-selected={tab === "fortune"} onClick={() => setTab("fortune")}><Icon name="wallet"/>Fortunes</button><button role="tab" aria-selected={tab === "playtime"} onClick={() => setTab("playtime")}><Icon name="clock"/>Temps de jeu</button><button role="tab" aria-selected={tab === "pvp"} onClick={() => setTab("pvp")}><Icon name="swords"/>PvP</button></div>{tab === "pvp" ? <EmptyState icon="swords" title="Le classement PvP arrive">Le serveur ne publie pas encore les éliminations. Aucun faux score n’est affiché.</EmptyState> : rows.length ? <RankingRows rows={rows} kind={tab}/> : <EmptyState icon="rank" title="Classement indisponible">Le flux communautaire est temporairement inaccessible.</EmptyState>}</div>;
}

const COSMETICS = [["Cape Frontière", "Cape", "Motif topographique or et mousse"], ["Ailes du Royaume", "Dos", "Effet visuel royal sans avantage"], ["Trace Émeraude", "Traînée", "Particules vertes lors des déplacements"], ["Halo des Conquérants", "Aura", "Cercle lumineux discret autour du joueur"]];

function ShopPage() {
  return <div className="page content-page"><PageTitle kicker="Cosmétiques uniquement" title="Boutique" copy="Deux grades pour soutenir BG LANDS, sans avantage de gameplay." action={<span className="preview-label">APERÇU · PAIEMENT FERMÉ</span>}/><div className="shop-layout"><section className="grade-card"><span>GRADE I</span><h2>Aventurier</h2><p>Préfixe, couleur de chat, emotes et cape au choix.</p><Button disabled variant="secondary">Bientôt disponible</Button></section><section className="grade-card grade-card--premium"><span>GRADE II</span><h2>Souverain</h2><p>La collection signature, les auras et les cosmétiques saisonniers.</p><Button disabled>Bientôt disponible</Button></section><section className="cosmetic-list"><h2>Collection du royaume</h2>{COSMETICS.map(([name, type, copy], index) => <article key={name}><span><Icon name={index % 2 ? "clan" : "shield"}/></span><div><small>{type}</small><strong>{name}</strong><p>{copy}</p></div><button type="button" disabled>À venir</button></article>)}</section></div></div>;
}

function ModulesPage({ settings, onSave, busy }: { settings: LauncherSettings; onSave: (settings: LauncherSettings) => void; busy: boolean }) {
  const [modules, setModules] = useState(settings.modules);
  const [tab, setTab] = useState<"modules" | "shaders" | "packs">("modules");
  useEffect(() => setModules(settings.modules), [settings.modules]);
  const enabled = Object.values(modules).filter(Boolean).length;
  return <div className="page modules-page"><aside className="modules-tabs"><span className="kicker">Bibliothèque</span><h2>Personnalisation</h2><button type="button" aria-pressed={tab === "modules"} onClick={() => setTab("modules")}>Modules <b>{enabled}</b></button><button type="button" aria-pressed={tab === "shaders"} onClick={() => setTab("shaders")}>Shaders <small>Bientôt</small></button><button type="button" aria-pressed={tab === "packs"} onClick={() => setTab("packs")}>Resource packs <small>Bientôt</small></button><div><Icon name="shield"/><p>Les options restent conformes au règlement : jamais les joueurs ni les minerais.</p></div></aside><section className="modules-content"><PageTitle kicker="Client Fabric officiel" title={tab === "modules" ? "Modules" : tab === "shaders" ? "Shaders" : "Resource packs"} copy={tab === "modules" ? "Active les éléments utiles de ton HUD au prochain lancement." : "Cette bibliothèque sera activée après validation des packs officiels."}/>{tab === "modules" ? <><div className="module-grid">{MODULES.map((module) => <label className="module-item" key={module.id}><span className="module-icon"><Icon name={module.icon}/></span><span><small>{module.tag}</small><strong>{module.title}</strong><em>{module.description}</em></span><input type="checkbox" checked={modules[module.id]} onChange={(event) => setModules((current) => ({ ...current, [module.id]: event.target.checked }))}/><i aria-hidden="true"/></label>)}</div><div className="module-save"><span>{enabled} modules actifs</span><Button disabled={busy} onClick={() => onSave({ ...settings, modules })}>Appliquer</Button></div></> : <EmptyState icon="modules" title="Bibliothèque en préparation">Seuls les fichiers contrôlés et compatibles avec la survie seront proposés ici.</EmptyState>}</section></div>;
}

function RulesPage({ onSupport }: { onSupport: () => void }) {
  const [query, setQuery] = useState("");
  const visible = RULES.filter((rule) => `${rule.title} ${rule.lead} ${(rule.allowed ?? []).join(" ")} ${rule.forbidden.join(" ")} ${rule.note ?? ""}`.toLocaleLowerCase("fr").includes(query.toLocaleLowerCase("fr")));
  return <div className="page content-page rules-page"><PageTitle kicker="Version en vigueur · v6.0" title="Règlement en jeu" copy="Les règles essentielles avant de rejoindre la survie." action={<Button variant="secondary" onClick={onSupport}>Contacter le staff</Button>}/><label className="rules-search" htmlFor="rules-search"><Icon name="rules"/><span>Rechercher</span><input id="rules-search" type="search" value={query} onChange={(event) => setQuery(event.target.value)} placeholder="Claim, PvP, duplication…"/></label><div className="rules-document">{visible.map((rule) => <section id={`rule-${rule.id}`} key={rule.id}><header><span>{rule.number}</span><div><small>SECTION {rule.number} / 05</small><h2>{rule.title}</h2></div></header><p>{rule.lead}</p><div className="rule-columns">{rule.allowed && <div className="rule-box rule-box--allowed"><h3><Icon name="check"/> Autorisé</h3><ul>{rule.allowed.map((item) => <li key={item}>{item}</li>)}</ul></div>}<div className="rule-box rule-box--forbidden"><h3><Icon name="close"/> Interdit</h3><ul>{rule.forbidden.map((item) => <li key={item}>{item}</li>)}</ul></div></div>{rule.note && <aside><Icon name="shield"/><p>{rule.note}</p></aside>}</section>)}{visible.length === 0 && <EmptyState icon="rules" title="Aucun résultat">Essaie un mot plus court.</EmptyState>}<footer><strong>BG LANDS · Règlement en jeu v6.0</strong><span>Version en vigueur</span></footer></div></div>;
}

function Toggle({ title, description, checked, onChange }: { title: string; description: string; checked: boolean; onChange: (checked: boolean) => void }) {
  return <label className="toggle"><span><strong>{title}</strong><small>{description}</small></span><input type="checkbox" checked={checked} onChange={(event) => onChange(event.target.checked)}/><i aria-hidden="true"/></label>;
}

function SettingsPage({ snapshot, onClose, onSave, onConfigureMicrosoft, onMicrosoftSetup, onRepair, onLogs, busy }: { snapshot: LauncherSnapshot; onClose: () => void; onSave: (settings: LauncherSettings) => void; onConfigureMicrosoft: (id: string) => void; onMicrosoftSetup: () => void; onRepair: () => void; onLogs: () => void; busy: boolean }) {
  const [settings, setSettings] = useState(snapshot.settings);
  const [clientId, setClientId] = useState(snapshot.microsoft.clientId);
  const [error, setError] = useState("");
  useEffect(() => setSettings(snapshot.settings), [snapshot.settings]);
  useEffect(() => setClientId(snapshot.microsoft.clientId), [snapshot.microsoft.clientId]);
  const submitMicrosoft = (event: FormEvent) => { event.preventDefault(); const clean = clientId.trim(); if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu.test(clean)) { setError("L’AppID doit être un UUID Microsoft Entra valide au format 8-4-4-4-12."); return; } setError(""); onConfigureMicrosoft(clean); };
  return <div className="page settings-scene"><div className="settings-scene__bg"/><div className="settings-scrim"/><motion.section className="settings-sheet" role="region" aria-labelledby="settings-title" initial={{ x: 36, opacity: 0 }} animate={{ x: 0, opacity: 1 }} transition={{ duration: 0.24 }}><header><span className="settings-mark"><Icon name="settings"/></span><div><span className="kicker">Launcher et client</span><h1 id="settings-title" tabIndex={-1}>Paramètres</h1></div><button className="icon-button" type="button" onClick={onClose} aria-label="Fermer les paramètres"><Icon name="close"/></button></header><div className="settings-scroll"><section className="setting-section"><h2>Mémoire du jeu <output>{(settings.maximumMemoryMb / 1024).toFixed(1)} Go</output></h2><p>Nous recommandons 4 à 6 Go pour le profil officiel.</p><input aria-label="Mémoire maximale" type="range" min="2048" max="16384" step="512" value={settings.maximumMemoryMb} onChange={(event) => setSettings({ ...settings, maximumMemoryMb: Number(event.target.value) })}/><div className="range-legend"><span>2 Go</span><span>16 Go</span></div></section><section className="setting-section"><h2>Affichage de Minecraft</h2><label className="field-label" htmlFor="resolution">Résolution</label><select id="resolution" value={`${settings.width}x${settings.height}`} onChange={(event) => { const [width, height] = event.target.value.split("x").map(Number); setSettings({ ...settings, width: width!, height: height! }); }}><option value="1280x720">1280 × 720</option><option value="1600x900">1600 × 900</option><option value="1920x1080">1920 × 1080</option><option value="2560x1440">2560 × 1440</option></select></section><section className="setting-section setting-section--toggles"><h2>Comportement</h2><Toggle title="Connexion automatique" description="Rejoindre le réseau au lancement." checked={settings.autoConnect} onChange={(checked) => setSettings({ ...settings, autoConnect: checked })}/><Toggle title="Fermer après lancement" description="Libérer les ressources du launcher." checked={settings.closeLauncherOnGameStart} onChange={(checked) => setSettings({ ...settings, closeLauncherOnGameStart: checked })}/></section><section className="setting-section"><h2>Outils du client</h2><div className="settings-tools"><button type="button" onClick={onRepair}><Icon name="repair"/>Réparer le client</button><button type="button" onClick={onLogs}><Icon name="log"/>Ouvrir les logs</button></div></section><details className="advanced-auth"><summary>Configuration Microsoft avancée</summary><form onSubmit={submitMicrosoft}><p>L’Application (client) ID est un identifiant public. Aucun secret ni mot de passe ne doit être collé ici.</p><label className="field-label" htmlFor="microsoft-client-id">Application (client) ID</label><input id="microsoft-client-id" autoComplete="off" spellCheck={false} value={clientId} onChange={(event) => setClientId(event.target.value)} aria-invalid={Boolean(error)} aria-describedby={error ? "appid-error" : undefined}/>{error && <p id="appid-error" className="field-error" role="alert">{error}</p>}<div><button type="button" onClick={onMicrosoftSetup}>Ouvrir Microsoft Entra</button><Button type="submit" disabled={busy}>Enregistrer l’AppID</Button></div></form></details></div><footer><Button variant="secondary" onClick={onClose}>Annuler</Button><Button disabled={busy || settings.minimumMemoryMb > settings.maximumMemoryMb} onClick={() => onSave(settings)}>Appliquer</Button></footer></motion.section></div>;
}

function DeviceCodeDialog({ snapshot, onOpen, onCancel, onCopy }: { snapshot: LauncherSnapshot; onOpen: () => void; onCancel: () => void; onCopy: () => void }) {
  const primaryRef = useRef<HTMLButtonElement>(null);
  useEffect(() => { primaryRef.current?.focus(); const close = (event: KeyboardEvent) => { if (event.key === "Escape") onCancel(); }; window.addEventListener("keydown", close); return () => window.removeEventListener("keydown", close); }, [onCancel]);
  if (!snapshot.deviceCode) return null;
  return <motion.div className="modal-backdrop" role="presentation" initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}><motion.section className="device-dialog" role="dialog" aria-modal="true" aria-labelledby="device-title" initial={{ scale: 0.97, opacity: 0, y: 12 }} animate={{ scale: 1, opacity: 1, y: 0 }} exit={{ scale: 0.98, opacity: 0 }}><button className="icon-button device-dialog__close" type="button" onClick={onCancel} aria-label="Annuler la connexion Microsoft"><Icon name="close"/></button><span className="device-dialog__mark"><Icon name="shield"/></span><span className="kicker kicker--gold">Étape finale · Microsoft officiel</span><h2 id="device-title">Autorise BG LANDS</h2><p>Ouvre la page Microsoft, saisis ce code puis reviens ici. La validation sera détectée automatiquement.</p><button className="device-code" type="button" onClick={onCopy}><small>CODE À SAISIR</small><strong>{snapshot.deviceCode.userCode}</strong><span><Icon name="copy"/> Copier</span></button><Button ref={primaryRef} icon="external" onClick={onOpen}>Ouvrir Microsoft</Button><button className="text-button" type="button" onClick={onCancel}>Annuler</button><small className="dialog-security"><Icon name="shield" size={13}/> Le launcher ne voit jamais ton mot de passe.</small></motion.section></motion.div>;
}

function InstallOverlay({ snapshot }: { snapshot: LauncherSnapshot }) {
  if (!BUSY.has(snapshot.phase) || snapshot.phase === "booting") return null;
  const percent = snapshot.progress?.percent ?? (snapshot.phase === "launching" ? 100 : 0);
  return <div className="install-overlay" role="status" aria-live="polite"><div className="install-card"><img src="../resources/assets/logo.png" alt="BG LANDS"/><span className="install-spinner" aria-hidden="true"/><span className="kicker kicker--gold">Distribution officielle</span><h2>{snapshot.phase === "launching" ? "Ouverture de Minecraft" : snapshot.progress?.label ?? "Préparation du client"}</h2><p>{snapshot.statusMessage}</p><div className="install-progress"><i><b style={{ transform: `scaleX(${percent / 100})` }}/></i><span>{percent > 0 ? `${percent}%` : "Vérification…"}</span></div><small>Ne ferme pas le launcher pendant cette opération.</small></div></div>;
}

function ProfileMenu({ snapshot, onNavigate, onDisconnect, onSupport }: { snapshot: LauncherSnapshot; onNavigate: (page: PageId) => void; onDisconnect: () => void; onSupport: () => void }) {
  return <motion.div className="profile-menu" role="menu" initial={{ opacity: 0, y: -8, scale: 0.98 }} animate={{ opacity: 1, y: 0, scale: 1 }} exit={{ opacity: 0, y: -5, scale: 0.99 }}><div className="profile-menu__head"><span className="avatar avatar--large">{snapshot.account ? initials(snapshot.account.displayName) : <Icon name="user"/>}</span><div><small>{snapshot.account ? "Compte connecté" : "Compte joueur"}</small><strong>{snapshot.account?.displayName ?? "Non connecté"}</strong></div></div>{snapshot.account ? <button role="menuitem" type="button" onClick={onDisconnect}><Icon name="logout"/>Déconnecter</button> : <button role="menuitem" type="button" onClick={() => onNavigate("play")}><Icon name="user"/>Se connecter</button>}<button role="menuitem" type="button" onClick={() => onNavigate("rankings")}><Icon name="rank"/>Classements</button><button role="menuitem" type="button" onClick={() => onNavigate("rules")}><Icon name="rules"/>Règlement</button><button role="menuitem" type="button" onClick={onSupport}><Icon name="external"/>Support Discord</button></motion.div>;
}

function App() {
  const api = window.bglands as BglandsLauncherApi | undefined;
  const reducedMotion = useReducedMotion();
  const [snapshot, setSnapshot] = useState(EMPTY_SNAPSHOT);
  const [page, setPage] = useState<PageId>(currentPage());
  const [busy, setBusy] = useState(false);
  const [profileOpen, setProfileOpen] = useState(false);
  const [moreOpen, setMoreOpen] = useState(false);
  const [toast, setToast] = useState<{ text: string; error: boolean } | null>(null);

  useEffect(() => {
    if (!api) { setSnapshot({ ...EMPTY_SNAPSHOT, phase: "signed-out", microsoft: { configured: true, clientId: "aperçu", source: "embedded" }, statusMessage: "Aperçu du launcher" }); return; }
    const unsubscribe = api.onSnapshot(setSnapshot);
    void api.getSnapshot().then(setSnapshot).catch(() => setToast({ text: "Impossible de charger l’état du launcher.", error: true }));
    return unsubscribe;
  }, [api]);

  useEffect(() => { const update = () => setPage(currentPage()); window.addEventListener("hashchange", update); return () => window.removeEventListener("hashchange", update); }, []);
  useEffect(() => { if (!toast) return; const timer = window.setTimeout(() => setToast(null), 4000); return () => window.clearTimeout(timer); }, [toast]);

  const navigate = useCallback((next: PageId) => { setProfileOpen(false); setMoreOpen(false); window.location.hash = next; setPage(next); window.requestAnimationFrame(() => document.querySelector<HTMLElement>(".page h1")?.focus()); }, []);
  const action = useCallback(async (operation: () => Promise<void>, success?: string) => { if (!api || busy) return; setBusy(true); try { await operation(); setSnapshot(await api.getSnapshot()); if (success) setToast({ text: success, error: false }); } catch { setToast({ text: "L’opération n’a pas pu aboutir. Le détail est disponible dans les logs.", error: true }); try { setSnapshot(await api.getSnapshot()); } catch { /* pushed state remains authoritative */ } } finally { setBusy(false); } }, [api, busy]);
  const connect = useCallback(() => { if (!api || busy) return; if (!snapshot.microsoft.configured) { navigate("settings"); setToast({ text: "L’application Microsoft BG LANDS doit être configurée.", error: true }); return; } navigate("play"); setBusy(true); void api.connectMicrosoft().catch(() => setToast({ text: "Connexion Microsoft impossible.", error: true })).finally(() => setBusy(false)); window.setTimeout(() => setBusy(false), 450); }, [api, busy, navigate, snapshot.microsoft.configured]);
  const primaryAction = useCallback(() => { if (!snapshot.account) { navigate("play"); return; } if (snapshot.phase === "ready") { void action(() => api!.play()); return; } if (snapshot.phase === "error") { void action(() => api!.repair()); return; } navigate("play"); }, [action, api, navigate, snapshot.account, snapshot.phase]);

  const content = useMemo(() => {
    const common = { snapshot };
    switch (page) {
      case "home": return <HomePage {...common} navigate={navigate} onPrimary={primaryAction}/>;
      case "play": return <PlayPage {...common} navigate={navigate} onConnect={connect} onDisconnect={() => void action(() => api!.disconnectMicrosoft(), "Compte déconnecté.")} onPlay={() => void action(() => api!.play())} onRepair={() => void action(() => api!.repair(), "Vérification du client lancée.")} onLogs={() => void action(() => api!.openLogs())}/>;
      case "news": return <NewsPage/>;
      case "servers": return <ServersPage {...common} onPlay={primaryAction}/>;
      case "clans": return <ClansPage {...common} navigate={navigate}/>;
      case "rankings": return <RankingsPage {...common}/>;
      case "shop": return <ShopPage/>;
      case "modules": return <ModulesPage settings={snapshot.settings} busy={busy} onSave={(settings) => void action(() => api!.saveSettings(settings), "Modules appliqués au prochain lancement.")}/>;
      case "rules": return <RulesPage onSupport={() => void action(() => api!.openSupport())}/>;
      case "settings": return <SettingsPage {...common} busy={busy} onClose={() => navigate("home")} onSave={(settings) => void action(() => api!.saveSettings(settings), "Paramètres enregistrés.")} onConfigureMicrosoft={(id) => void action(() => api!.configureMicrosoft(id), "Application Microsoft configurée.")} onMicrosoftSetup={() => void action(() => api!.openMicrosoftSetup())} onRepair={() => void action(() => api!.repair())} onLogs={() => void action(() => api!.openLogs())}/>;
    }
  }, [action, api, busy, connect, navigate, page, primaryAction, snapshot]);

  const secondaryPages = ALL_PAGES.filter(({ id }) => ["news", "rankings", "shop", "rules"].includes(id));
  return <div className="launcher-shell" data-phase={snapshot.phase}><header className="titlebar"><button className="brand" type="button" onClick={() => navigate("home")} aria-label="Accueil BG LANDS"><img src="../resources/assets/logo.png" alt=""/><span><strong>LAUNCHER</strong><small>v{snapshot.launcherVersion}</small></span></button><div className="titlebar__meta"><span>JAVA EDITION</span><i/><span>{snapshot.gameVersion === "—" ? "PROFIL OFFICIEL" : `MINECRAFT ${snapshot.gameVersion}`}</span></div><div className="titlebar__actions"><button className="icon-button" type="button" onClick={() => navigate("settings")} aria-label="Ouvrir les paramètres"><Icon name="settings"/></button><button className="language-chip" type="button" aria-label="Langue actuelle : français"><Icon name="globe" size={16}/><span>FR</span></button><StatusPill snapshot={snapshot} compact/><div className="profile-anchor"><button className="profile-chip" type="button" aria-haspopup="menu" aria-expanded={profileOpen} onClick={() => setProfileOpen((open) => !open)}><span className="avatar">{snapshot.account ? initials(snapshot.account.displayName) : <Icon name="user" size={16}/>}</span><span><small>{snapshot.account ? "Joueur" : "Compte"}</small><strong>{snapshot.account?.displayName ?? "Connexion"}</strong></span><Icon name="arrow" size={14}/></button><AnimatePresence>{profileOpen && <ProfileMenu snapshot={snapshot} onNavigate={navigate} onDisconnect={() => void action(() => api!.disconnectMicrosoft(), "Compte déconnecté.")} onSupport={() => void action(() => api!.openSupport())}/>}</AnimatePresence></div></div></header><div className="workspace"><aside className="navigation-rail"><nav aria-label="Navigation principale">{PRIMARY_NAV.map((item) => <button type="button" key={item.id} className={page === item.id ? "is-active" : ""} aria-current={page === item.id ? "page" : undefined} onClick={() => navigate(item.id)}><Icon name={item.icon}/><span>{item.label}</span>{item.id === "modules" && <b>{Object.values(snapshot.settings.modules).filter(Boolean).length}</b>}</button>)}</nav><div className="rail-bottom"><button type="button" className={page === "shop" ? "is-active" : ""} onClick={() => navigate("shop")}><Icon name="shop"/><span>Boutique</span></button><div className="more-anchor"><button type="button" aria-haspopup="menu" aria-expanded={moreOpen} onClick={() => setMoreOpen((open) => !open)}><Icon name="more"/><span>Plus</span></button><AnimatePresence>{moreOpen && <motion.div className="more-menu" role="menu" initial={{ opacity: 0, x: -8 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: -5 }}>{secondaryPages.map((item) => <button role="menuitem" type="button" key={item.id} onClick={() => navigate(item.id)}><Icon name={item.icon}/>{item.label}</button>)}</motion.div>}</AnimatePresence></div></div></aside><main id="main-content" tabIndex={-1}><AnimatePresence mode="wait" initial={false}><motion.div className="route-frame" key={page} initial={reducedMotion ? false : { opacity: 0, x: 8 }} animate={{ opacity: 1, x: 0 }} exit={reducedMotion ? undefined : { opacity: 0, x: -5 }} transition={{ duration: reducedMotion ? 0 : 0.2, ease: [0.22, 1, 0.36, 1] }}>{content}</motion.div></AnimatePresence></main></div>{snapshot.phase === "booting" && <div className="boot-screen"><img src="../resources/assets/logo.png" alt="BG LANDS"/><span className="install-spinner"/><p>Ouverture des portes du royaume…</p></div>}<AnimatePresence>{snapshot.phase === "device-code" && <DeviceCodeDialog snapshot={snapshot} onOpen={() => void action(() => api!.openMicrosoftLogin())} onCancel={() => void action(() => api!.cancelMicrosoftLogin())} onCopy={() => { if (snapshot.deviceCode) void navigator.clipboard.writeText(snapshot.deviceCode.userCode).then(() => setToast({ text: "Code Microsoft copié.", error: false })); }}/>}</AnimatePresence><InstallOverlay snapshot={snapshot}/><AnimatePresence>{toast && <motion.div className={`toast ${toast.error ? "toast--error" : ""}`} role={toast.error ? "alert" : "status"} initial={{ opacity: 0, y: 12 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: 8 }}><i/>{toast.text}</motion.div>}</AnimatePresence></div>;
}

const root = document.getElementById("root");
if (!root) throw new Error("Renderer root is missing");
createRoot(root).render(<App/>);
