{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "pixel-openspace",
  "title": "Pixel Openspace",
  "description": "A pixel-art open space where your scheduled jobs and AI agents come to work: they sit and type while running, wander to the coffee machine when idle, and their PC smokes when they fail.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "button",
    "card",
    "badge",
    "button-group",
    "dialog",
    "kbd",
    "switch",
    "label",
    "separator",
    "input"
  ],
  "files": [
    {
      "path": "registry/pixel-openspace/pixel-openspace.tsx",
      "content": "'use client'\n\nimport { useEffect, useMemo, useRef, useState, type CSSProperties } from 'react'\nimport { Button } from '@/components/ui/button'\nimport { ButtonGroup } from '@/components/ui/button-group'\nimport { JeuArcade, lireRecord, type Jeu } from './arcade'\nimport { BUREAU, CHAT_ID, CHEF_ID, LARGEUR, avancer, chat, estInvite, invite, marcheur, plan, type Invite, type Statut } from './engine'\nimport { TEXTS, type Texts } from './i18n'\nimport { ACCENT, DefsCommunes, TextesContexte, animerLeDecor, type Lumiere } from './primitives'\nimport { THEME_GEEK } from './themes/geek'\nimport { Frise } from './timeline'\nimport { changerPreferences } from './preferences'\nimport { useDansLeNavigateur, usePreferences } from './navigateur'\nimport { Reglages, useMeteoDuLieu } from './settings'\nimport { fete } from './seasons'\nimport type { Agent, OpenSpaceProps, SceneObject, WallTile } from './types'\nimport { type Equipe, type Pose, type Vue, bientot, heureCourte, obscurite, oublie, poses, posesAuBureau, versVue } from './vue'\nimport { THEMES, ThemeContexte } from './contexte'\nimport { Carte } from './card'\nimport { Chat, ColisDeposes, Confettis, EcranMural, Fenetre, Horloge, Nuit, Records, TableauBlanc } from './decor'\nimport { Fetes } from './festivities'\nimport { SONS, jouerNotes } from './sound'\nimport { Annonce, Parole, Personnage, Poste, cadrage } from './people'\n\n/*\n * pixel-openspace — an open space in pixel art where your scheduled jobs and\n * AI agents come to work. The code speaks French inside (it was born in a\n * French workshop); everything it shows follows the `language` prop.\n */\n\n/** Le temps qu'une parole reste affichée, celui qu'on met à s'étirer, et celui qu'un bureau met à se monter. */\nconst PAROLE_MS = 9_000\nconst ETIREMENT_MS = 2_500\nconst MONTAGE_MS = 8_000\n/** Vingt secondes sans que rien ne bouge, et la scène se met au repos. */\nconst CALME_MS = 20_000\n/** Une image toutes les 33 ms : 30 images par seconde. */\nconst IMAGE_MS = 1000 / 30 - 1\n\n/** Les états qui valent une notification. */\nconst SOUCIS: Array<Statut> = ['en-echec', 'en-retard', 'absent']\n\n/* Ce que ce navigateur retient : les alertes voulues, le son, et les agents qu'il a déjà vus. */\nconst CLE_ALERTES = 'pixel-openspace:alerts'\nconst CLE_SON = 'pixel-openspace:sound'\nconst CLE_CONNUS = 'pixel-openspace:known-agents'\n\nfunction lire(cle: string): string | null {\n  try {\n    return localStorage.getItem(cle)\n  } catch {\n    return null\n  }\n}\n\nfunction ecrire(cle: string, valeur: string) {\n  try {\n    localStorage.setItem(cle, valeur)\n  } catch {\n    /* Navigation privée ou stockage bloqué : on s'en passe. */\n  }\n}\n\nconst CHEF_DECOR = (t: Texts): Agent => ({ id: CHEF_ID, name: t.leadName, emoji: '🧑‍💼', role: t.leadRole, status: 'ok' })\n\n/** L'écran mural quand on ne lui donne rien : combien de tâches, combien au travail, en échec, en retard. */\nfunction ecranParDefaut(agents: Array<Agent>, t: Texts): Array<WallTile> {\n  const compte = (statut: Agent['status']) => agents.filter((a) => a.status === statut).length\n  const echecs = compte('failed')\n  const retards = compte('late')\n  return [\n    { label: t.wall.jobs, value: agents.length, tone: 'info' },\n    { label: t.wall.running, value: compte('working'), tone: 'ok' },\n    { label: t.wall.failed, value: echecs, tone: echecs ? 'alert' : 'neutral' },\n    { label: t.wall.late, value: retards, tone: retards ? 'warn' : 'neutral' },\n  ]\n}\n\n/**\n * The open space. Pass your agents (and optionally a lead, wall tiles,\n * announcements and the weather); update the props as their state changes and\n * the room reacts: whoever starts working sits down and types, whoever\n * finishes stretches and says its last word, a failed agent's PC smokes and\n * the lead comes over. Click an agent to see its card.\n */\nexport function PixelOpenspace({\n  agents,\n  lead,\n  title = 'PIXEL OPENSPACE',\n  wall,\n  announcements = [],\n  weather = null,\n  visitors = 0,\n  deliveries = 0,\n  celebrate = false,\n  timeline: friseProp = true,\n  language: langueProp = 'en',\n  theme: themeProp = 'geek',\n  columns: colonnesProp = 5,\n  seasonal: saisonsProp = true,\n  nightHours = [22, 6],\n  toolbar = true,\n  objectLinks = {},\n  onObjectClick,\n  onRun,\n  now,\n  className,\n  style,\n}: OpenSpaceProps) {\n  /* Ce que le visiteur a réglé derrière la roue dentée passe avant les props. */\n  const preferences = usePreferences()\n  const dansLeNavigateur = useDansLeNavigateur()\n  const language = preferences.language ?? langueProp\n  const enseigne = preferences.title?.trim() || title\n  // Un lieu choisi dans les réglages : sa vraie météo remplace celle des props, une fois trouvée.\n  const etatMeteo = useMeteoDuLieu(preferences.weatherPlace, language)\n  const meteo = etatMeteo.etat === 'trouve' ? etatMeteo.meteo : weather\n  const nomTheme = preferences.theme ?? themeProp\n  const colonnes = preferences.columns ?? colonnesProp\n  const seasonal = preferences.seasonal ?? saisonsProp\n  const assombrir = preferences.night ?? true\n  const mouvement = preferences.motion ?? true\n  const timeline = preferences.timeline ?? friseProp\n\n  // Une langue inconnue (une faute de frappe, un projet en JavaScript) retombe sur l'anglais.\n  const t = TEXTS[language] ?? TEXTS.en\n  const theme = THEMES[nomTheme] ?? THEME_GEEK\n  const equipe: Equipe = {\n    chef: versVue(lead ?? CHEF_DECOR(t), CHEF_ID),\n    agents: agents.map((a) => versVue(a)),\n  }\n\n  const [ouverture] = useState(() => now ?? Date.now())\n  /*\n   * L'horloge, les comptes à rebours et la nuit dépendent de l'heure, qui n'est\n   * pas la même au rendu serveur et dans le navigateur. Sans `now`, la scène ne\n   * se dessine qu'une fois arrivée dans le navigateur ; un cadre vide aux bonnes\n   * proportions tient sa place d'ici là.\n   */\n  const pret = now !== undefined || dansLeNavigateur\n  const [maintenant, setMaintenant] = useState(ouverture)\n  const [choisi, setChoisi] = useState<string | null>(null)\n  const [paroles, setParoles] = useState<Record<string, string>>({})\n  const [etires, setEtires] = useState<Record<string, true>>({})\n  const [nouveaux, setNouveaux] = useState<Record<string, true>>({})\n  const [jeu, setJeu] = useState<Jeu | null>(null)\n  const [deposes, setDeposes] = useState(0)\n  /* Le son et les alertes : le choix fait dans cette visite, sinon celui gardé par le navigateur. */\n  const [sonChoisi, setSonChoisi] = useState<boolean | null>(null)\n  const [alertesChoisies, setAlertesChoisies] = useState<boolean | null>(null)\n  const son = sonChoisi ?? (dansLeNavigateur && lire(CLE_SON) === '1')\n  const permission = dansLeNavigateur && typeof Notification !== 'undefined' ? Notification.permission : 'default'\n  const alertes = alertesChoisies ?? (dansLeNavigateur && lire(CLE_ALERTES) === '1' && permission === 'granted')\n  const alertesBloquees = dansLeNavigateur && (typeof Notification === 'undefined' || permission === 'denied')\n  const audio = useRef<AudioContext | null>(null)\n  const alertesActives = useRef(false)\n  const scenePleinEcran = useRef<HTMLDivElement>(null)\n  /* Les invités annoncés par les props, que la boucle d'animation fait entrer. */\n  const arrivees = useRef<Array<Invite>>([])\n  const minuteries = useRef<Array<ReturnType<typeof setTimeout>>>([])\n  const plusTard = (ms: number, faire: () => void) => minuteries.current.push(setTimeout(faire, ms))\n\n  const jouerSon = (quoi: keyof typeof SONS) => {\n    if (audio.current) jouerNotes(audio.current, SONS[quoi])\n  }\n\n  /* L'heure avance toutes les cinq secondes : horloge, comptes à rebours, nuit. */\n  useEffect(() => {\n    const minuterie = setInterval(() => setMaintenant(Date.now()), 5_000)\n    const liste = minuteries.current\n    return () => {\n      clearInterval(minuterie)\n      liste.forEach(clearTimeout)\n    }\n  }, [])\n\n  /* Le son se rallume d'une visite à l'autre, mais le navigateur ne l'autorise qu'après un geste : le premier clic. */\n  useEffect(() => {\n    if (!son || audio.current) return\n    const reveiller = () => {\n      audio.current ??= new AudioContext()\n      void audio.current.resume()\n    }\n    window.addEventListener('pointerdown', reveiller, { once: true })\n    return () => window.removeEventListener('pointerdown', reveiller)\n  }, [son])\n\n  const basculerSon = () => {\n    if (son) {\n      void audio.current?.close()\n      audio.current = null\n      setSonChoisi(false)\n      ecrire(CLE_SON, '0')\n      return\n    }\n    audio.current = new AudioContext()\n    jouerNotes(audio.current, SONS.travail)\n    setSonChoisi(true)\n    ecrire(CLE_SON, '1')\n  }\n\n  /* Les alertes restent branchées d'une visite à l'autre, tant que le navigateur les autorise ; la boucle les lit ici. */\n  useEffect(() => {\n    alertesActives.current = alertes\n  })\n\n  const basculerAlertes = async () => {\n    if (alertes) {\n      setAlertesChoisies(false)\n      ecrire(CLE_ALERTES, '0')\n      return\n    }\n    if (typeof Notification === 'undefined') return\n    const reponse = Notification.permission === 'granted' ? 'granted' : await Notification.requestPermission()\n    const ok = reponse === 'granted'\n    setAlertesChoisies(ok)\n    ecrire(CLE_ALERTES, ok ? '1' : '0')\n  }\n\n  const basculerPleinEcran = () => {\n    // Le navigateur peut refuser (pas de geste de l'utilisateur, iframe) : on reste alors dans la page.\n    const demande = document.fullscreenElement ? document.exitFullscreen() : scenePleinEcran.current?.requestFullscreen()\n    demande?.catch(() => {})\n  }\n\n  /* Les records des bornes, relus à chaque fermeture de borne. */\n  const records = useMemo(\n    () => (dansLeNavigateur && !jeu ? { snake: lireRecord('snake'), casse: lireRecord('casse-briques') } : { snake: 0, casse: 0 }),\n    [dansLeNavigateur, jeu],\n  )\n\n  /* Un agent cliqué dit ce qu'il fait, le temps de le lire. */\n  const parler = (vue: Vue) => {\n    const id = vue.fiche.id\n    if (!vue.fiche.role) return\n    setParoles((p) => ({ ...p, [id]: vue.fiche.role }))\n    plusTard(5_000, () => setParoles((p) => sans(p, id)))\n  }\n\n  /* Un agent que ce navigateur n'a jamais vu voit son bureau se monter sous ses yeux. */\n  const cle = equipe.agents.map((a) => a.fiche.id).join(',')\n  useEffect(() => {\n    const ids = cle ? cle.split(',') : []\n    const connus = lire(CLE_CONNUS)\n    ecrire(CLE_CONNUS, JSON.stringify(ids))\n    // La toute première visite ne monte rien : tout le monde était déjà là.\n    if (!connus) return\n    let deja: Array<string> = []\n    try {\n      deja = JSON.parse(connus) as Array<string>\n    } catch {\n      return\n    }\n    const arrives = ids.filter((id) => !deja.includes(id))\n    if (!arrives.length) return\n    // eslint-disable-next-line react-hooks/set-state-in-effect -- ce que le navigateur a déjà vu n'est connu qu'après l'hydratation\n    setNouveaux(Object.fromEntries(arrives.map((id) => [id, true as const])))\n    const minuterie = setTimeout(() => setNouveaux({}), MONTAGE_MS)\n    return () => clearTimeout(minuterie)\n  }, [cle])\n\n  /*\n   * Ce qui change d'une fois sur l'autre : qui finit (il s'étire et parle),\n   * qui tombe (notification, alarme), qui se met au travail (bip). Les\n   * compteurs de visiteurs et de livraisons font entrer du monde.\n   */\n  const precedente = useRef<{ equipe: Equipe; visitors: number; deliveries: number } | null>(null)\n  const empreinteEquipe = [equipe.chef, ...equipe.agents].map((v) => `${v.fiche.id}:${v.statut}:${v.brut.dernierMessage ?? ''}`).join('|')\n  useEffect(() => {\n    const avant = precedente.current\n    precedente.current = { equipe, visitors, deliveries }\n    if (!avant) return\n    for (const vue of [equipe.chef, ...equipe.agents]) {\n      const ancienne = [avant.equipe.chef, ...avant.equipe.agents].find((a) => a.fiche.id === vue.fiche.id)\n      if (!ancienne || ancienne.statut === vue.statut) continue\n      const id = vue.fiche.id\n      if (ancienne.statut === 'au-travail') {\n        const mot = vue.brut.dernierMessage\n        if (mot) {\n          // eslint-disable-next-line react-hooks/set-state-in-effect -- la scène réagit à un changement venu des props : une bulle, le temps de la lire\n          setParoles((p) => ({ ...p, [id]: mot }))\n          plusTard(PAROLE_MS, () => setParoles((p) => sans(p, id)))\n        }\n        setEtires((e) => ({ ...e, [id]: true }))\n        plusTard(ETIREMENT_MS, () => setEtires((e) => sans(e, id)))\n      }\n      if (vue.statut === 'au-travail') jouerSon('travail')\n      if (vue.statut === 'en-echec') jouerSon('echec')\n      if (alertesActives.current && SOUCIS.includes(vue.statut) && typeof Notification !== 'undefined') {\n        new Notification(`${vue.fiche.emoji} ${vue.fiche.nom}: ${t.statuses[vue.statut]}`, {\n          body: vue.brut.dernierMessage ?? vue.fiche.role,\n          tag: `pixel-openspace-${id}`,\n        })\n      }\n    }\n    if (visitors > avant.visitors) {\n      for (let i = 0; i < Math.min(2, visitors - avant.visitors); i++) arrivees.current.push('visiteur')\n      jouerSon('sonnette')\n    }\n    for (let i = 0; i < Math.min(2, deliveries - avant.deliveries); i++) arrivees.current.push('livreur')\n    // eslint-disable-next-line react-hooks/exhaustive-deps -- on ne réagit qu'à ce qui se voit : états, messages, compteurs\n  }, [empreinteEquipe, visitors, deliveries])\n\n  const lePlan = plan(cle ? cle.split(',') : [], t.phrases, theme.coins, colonnes)\n  const [sceneAnimee, setScene] = useState<Record<string, Pose>>(() => posesAuBureau(lePlan))\n  // Les déplacements coupés, chacun reste assis à son bureau.\n  const scene = mouvement ? sceneAnimee : posesAuBureau(lePlan)\n\n  /* Les états, les annonces du chef et les heures de nuit, lus par la boucle d'animation sans la relancer. */\n  const statuts = useRef(new Map<string, Statut>())\n  const annonces = useRef<Array<string>>([])\n  const heuresNuit = useRef(nightHours)\n  useEffect(() => {\n    statuts.current = new Map([[CHEF_ID, equipe.chef.statut], ...equipe.agents.map((a) => [a.fiche.id, a.statut] as const)])\n    annonces.current = announcements\n    heuresNuit.current = nightHours\n  })\n\n  useEffect(() => {\n    if (!mouvement || window.matchMedia('(prefers-reduced-motion: reduce)').matches) return\n    const p = plan(cle ? cle.split(',') : [], t.phrases, theme.coins, colonnes)\n    // Chacun se lève à son heure, pas tous ensemble.\n    let marcheurs = [...[p.chef, ...p.bureaux].map((b) => marcheur(b.id, b.siege, 2_000 + Math.random() * 60_000)), chat(p)]\n    let numero = 0\n    let avant: number | null = null\n    let image = 0\n    let attente: ReturnType<typeof setTimeout> | undefined\n    let derniereScene = ''\n    let calmeDepuis: number | null = null\n    let enPause = false\n\n    /* Ce qui se voit d'un marcheur : tant que rien n'en change, inutile de redessiner. */\n    const empreinte = () =>\n      marcheurs\n        .map(\n          (m) =>\n            `${m.id}:${Math.round(m.pos.x)},${Math.round(m.pos.y)},${Math.round(m.angle / 90)},` +\n            `${m.chemin.length ? Math.floor(m.pas / 70) : '-'},${m.lieu.nom},${m.parole ?? ''},${m.porte ? 1 : 0}`,\n        )\n        .join('|')\n\n    const boucle = (instant: number) => {\n      // Trente tours par seconde, pas plus : le pixel art n'en demande pas davantage.\n      if (avant !== null && !enPause && instant - avant < IMAGE_MS) {\n        image = requestAnimationFrame(boucle)\n        return\n      }\n      // Les invités annoncés entrent ; ceux qui sont ressortis s'effacent.\n      for (const genre of arrivees.current.splice(0)) marcheurs.push(invite(p, genre, ++numero))\n      const charges = new Set(marcheurs.filter((m) => m.porte).map((m) => m.id))\n      const [debutNuit, finNuit] = heuresNuit.current\n      const heure = new Date().getHours()\n      const nuit = debutNuit > finNuit ? heure >= debutNuit || heure < finNuit : heure >= debutNuit && heure < finNuit\n      // Un onglet revenu de l'arrière-plan ne téléporte personne ; au calme, les pas sont plus espacés.\n      avancer(\n        p,\n        marcheurs,\n        statuts.current,\n        Math.min(instant - (avant ?? instant), enPause ? 250 : 100),\n        Math.random,\n        annonces.current,\n        nuit,\n      )\n      const livres = marcheurs.filter((m) => charges.has(m.id) && !m.porte).length\n      if (livres) setDeposes((d) => Math.min(3, d + livres))\n      marcheurs = marcheurs.filter((m) => !m.parti)\n      avant = instant\n\n      /*\n       * Le calme : personne ne marche, personne ne travaille ni n'a planté, depuis vingt\n       * secondes. Les animations du décor se figent et la boucle ralentit à quatre tours\n       * par seconde ; le premier qui se lève relance tout.\n       */\n      const bouge = marcheurs.some((m) => m.chemin.length)\n      const occupe = [...statuts.current.values()].some((s) => s === 'au-travail' || s === 'en-echec')\n      calmeDepuis = bouge || occupe ? null : (calmeDepuis ?? instant)\n      const calme = calmeDepuis !== null && instant - calmeDepuis > CALME_MS\n      enPause = calme\n      // Le décor s'anime au rythme de la boucle, et se fige avec elle au calme.\n      if (!calme) animerLeDecor(instant)\n\n      // Et on ne redessine que si quelque chose a changé à l'écran.\n      const vue = empreinte()\n      if (vue !== derniereScene) {\n        derniereScene = vue\n        const suivante = poses(p, marcheurs)\n        // Qui n'a pas bougé garde sa pose d'avant : React ne redessine que ceux qui ont changé.\n        setScene((precedentes) =>\n          Object.fromEntries(\n            Object.entries(suivante).map(([id, pose]) => {\n              const ancienne = precedentes[id]\n              const pareil =\n                ancienne && (Object.keys(pose) as Array<keyof Pose>).every((k) => (k === 'pas' ? !pose.marche : ancienne[k] === pose[k]))\n              return [id, pareil ? ancienne : pose]\n            }),\n          ),\n        )\n      }\n\n      if (enPause) attente = setTimeout(() => (image = requestAnimationFrame(boucle)), 250)\n      else image = requestAnimationFrame(boucle)\n    }\n    image = requestAnimationFrame(boucle)\n    return () => {\n      cancelAnimationFrame(image)\n      clearTimeout(attente)\n    }\n    // eslint-disable-next-line react-hooks/exhaustive-deps -- la boucle ne repart que si les bureaux, leur disposition, la langue, le thème ou les déplacements changent\n  }, [cle, colonnes, language, nomTheme, mouvement])\n\n  const tous = [equipe.chef, ...equipe.agents]\n  const postes = [{ vue: equipe.chef, place: lePlan.chef }, ...equipe.agents.map((vue, i) => ({ vue, place: lePlan.bureaux[i] }))]\n  const presents = postes\n    .filter(({ vue }) => vue.statut !== 'absent')\n    .map(({ vue, place }) => ({\n      vue,\n      place,\n      pose:\n        scene[vue.fiche.id] ??\n        ({\n          ...place.siege.pos,\n          angle: place.siege.face,\n          marche: false,\n          pas: 0,\n          assise: 'bureau',\n          lieu: place.siege.nom,\n          parole: null,\n          porte: false,\n        } satisfies Pose),\n    }))\n    // Le plus bas passe devant : c'est ce qui donne la profondeur.\n    .sort((a, b) => a.pose.y - b.pose.y)\n  const selection = tous.find((a) => a.fiche.id === choisi) ?? null\n  const choisir = (id: string) => setChoisi((c) => (c === id ? null : id))\n  const cliquerAgent = (id: string) => {\n    const vue = tous.find((a) => a.fiche.id === id)\n    if (vue) parler(vue)\n    choisir(id)\n  }\n  const cafe = Boolean(theme.coinCafe) && presents.some(({ pose }) => pose.lieu === theme.coinCafe && !pose.marche)\n  const invites = Object.entries(scene).filter(([id]) => estInvite(id))\n  const poseChat = scene[CHAT_ID]\n  // Les cinq prochains passages, pour le tableau blanc du chef.\n  const prochains = equipe.agents\n    .filter((a) => a.prochain && a.statut !== 'absent')\n    .sort((a, b) => a.prochain!.localeCompare(b.prochain!))\n    .slice(0, 5)\n    .map((a) => ({ heure: heureCourte(a.prochain!, maintenant, t.locale), nom: a.fiche.nom }))\n  const nuit = assombrir ? obscurite(maintenant) : 0\n  const aller = (objet: SceneObject) => {\n    const href = objectLinks[objet]\n    if (!href) return\n    if (onObjectClick) onObjectClick(objet, href)\n    else window.location.href = href\n  }\n\n  // Les lumières qui percent la nuit : les écrans allumés, la lampe du chef, et celles du thème.\n  const lumieres: Array<Lumiere> = [\n    ...postes\n      .filter(({ vue }) => vue.statut !== 'absent')\n      .flatMap(({ vue, place }) =>\n        place.id === CHEF_ID\n          ? [{ x: place.cx, y: place.dy + 10, r: 80 }]\n          : [\n              {\n                x: place.cx - 4,\n                y: place.dy,\n                r: vue.statut === 'au-travail' ? 95 : 60,\n                couleur: vue.statut === 'au-travail' ? ACCENT : undefined,\n              },\n              ...(theme.lumierePoste?.(place.cx, place.dy, theme.tenue(vue.fiche.id).c) ?? []),\n            ],\n      ),\n    ...theme.lumieres,\n  ]\n\n  // La carte s'accroche au-dessus de l'agent choisi, ou à son bureau s'il est absent.\n  const ancre = (() => {\n    if (!selection) return null\n    const present = presents.find(({ vue }) => vue.fiche.id === selection.fiche.id)\n    const place = postes.find(({ vue }) => vue.fiche.id === selection.fiche.id)?.place\n    // Assis à son bureau, la carte passe au-dessus de l'écran pour ne pas le cacher.\n    if (present && present.pose.assise === 'bureau' && place && place.id !== CHEF_ID) {\n      return { x: present.pose.x, haut: place.dy - 16, bas: cadrage(present.pose).bas }\n    }\n    if (present) return { x: present.pose.x, ...cadrage(present.pose) }\n    return place ? { x: place.cx, haut: place.dy - 14, bas: place.dy + BUREAU.hauteur } : null\n  })()\n\n  return (\n    <TextesContexte.Provider value={t}>\n      <ThemeContexte.Provider value={theme}>\n        <div\n          className={['flex flex-col gap-3', className].filter(Boolean).join(' ')}\n          style={{ '--po-accent': '#e8b923', ...style } as CSSProperties}\n        >\n          {toolbar ? (\n            <ButtonGroup className=\"self-end\">\n              <Button variant=\"outline\" size=\"sm\" onClick={basculerPleinEcran}>\n                {t.fullscreen}\n              </Button>\n              <Reglages\n                titre={preferences.title ?? ''}\n                titreOrigine={title}\n                lieu={preferences.weatherPlace ?? ''}\n                lieuOrigine={weather?.place}\n                etatMeteo={etatMeteo}\n                theme={nomTheme}\n                colonnes={colonnes}\n                langue={language}\n                saisons={seasonal}\n                nuit={assombrir}\n                mouvement={mouvement}\n                frise={timeline}\n                son={son}\n                alertes={alertes}\n                alertesBloquees={alertesBloquees}\n                changer={changerPreferences}\n                basculerSon={basculerSon}\n                basculerAlertes={() => void basculerAlertes()}\n                reinitialiser={() => changerPreferences(null)}\n              />\n            </ButtonGroup>\n          ) : null}\n\n          <div\n            ref={scenePleinEcran}\n            style={{ background: theme.fond }}\n            className=\"overflow-x-auto rounded-xl border [&:fullscreen]:flex [&:fullscreen]:items-center [&:fullscreen]:justify-center [&:fullscreen]:rounded-none [&:fullscreen]:border-0\"\n          >\n            <div\n              className=\"relative min-w-[720px] [:fullscreen>&]:w-[min(100vw,calc(100vh*var(--ratio)))]\"\n              style={{ '--ratio': LARGEUR / lePlan.hauteur } as CSSProperties}\n            >\n              {!pret ? <div style={{ aspectRatio: `${LARGEUR} / ${lePlan.hauteur}` }} /> : null}\n              {pret ? (\n                <svg\n                  viewBox={`0 0 ${LARGEUR} ${lePlan.hauteur}`}\n                  className=\"block w-full select-none\"\n                  style={{ fontFamily: \"var(--font-pixel, 'Pixelify Sans'), ui-monospace, monospace\" }}\n                  shapeRendering=\"crispEdges\"\n                  role=\"img\"\n                  aria-label={t.sceneLabel}\n                >\n                  <DefsCommunes />\n                  <theme.Decor hauteur={lePlan.hauteur} titre={enseigne} liens={objectLinks} aller={aller} jouer={setJeu} />\n                  <EcranMural tuiles={wall ?? ecranParDefaut(agents, t)} />\n                  <TableauBlanc prochains={prochains} />\n                  <Records snake={records.snake} casse={records.casse} />\n                  <ColisDeposes nombre={deposes} bas={lePlan.hauteur - 20} />\n                  {seasonal ? <Fetes quoi={fete(maintenant)} bas={lePlan.hauteur - 20} /> : null}\n                  <Fenetre meteo={meteo} />\n                  <Horloge maintenant={maintenant} ouverture={ouverture} />\n                  {cafe && theme.CafeQuiCoule ? <theme.CafeQuiCoule /> : null}\n\n                  {postes.map(({ vue, place }) => (\n                    <Poste\n                      key={vue.fiche.id}\n                      vue={vue}\n                      place={place}\n                      occupe={vue.statut !== 'absent' && (scene[vue.fiche.id]?.assise ?? 'bureau') === 'bureau'}\n                      bientot={bientot(vue, maintenant)}\n                      monte={Boolean(nouveaux[vue.fiche.id])}\n                      oublie={oublie(vue, maintenant)}\n                      choisi={choisi === vue.fiche.id}\n                      choisir={() => choisir(vue.fiche.id)}\n                    />\n                  ))}\n\n                  {presents.map(({ vue, pose }) => (\n                    <Personnage\n                      key={vue.fiche.id}\n                      id={vue.fiche.id}\n                      pose={pose}\n                      etire={Boolean(etires[vue.fiche.id])}\n                      choisir={cliquerAgent}\n                    />\n                  ))}\n                  {invites.map(([id, pose]) => (\n                    <Personnage key={id} id={id} pose={pose} etire={false} />\n                  ))}\n                  {poseChat ? <Chat pose={poseChat} /> : null}\n\n                  {/* Le dossier des chaises occupées, devant le dos de l'agent. */}\n                  {presents\n                    .filter(({ vue, pose }) => pose.assise === 'bureau' && vue.fiche.id !== CHEF_ID)\n                    .map(({ vue, place }) => (\n                      <theme.Dossier\n                        key={vue.fiche.id}\n                        x={place.siege.pos.x}\n                        y={place.siege.pos.y + 4}\n                        couleur={theme.tenue(vue.fiche.id).c}\n                      />\n                    ))}\n\n                  <Nuit niveau={nuit} lumieres={lumieres} hauteur={lePlan.hauteur} voile={theme.nuit} />\n                  {celebrate ? <Confettis hauteur={lePlan.hauteur} /> : null}\n\n                  {/* Les étiquettes et les bulles, au-dessus de tout, même la nuit. */}\n                  {presents.map(({ vue, pose }) => (\n                    <Annonce key={vue.fiche.id} vue={vue} pose={pose} parole={paroles[vue.fiche.id] ?? pose.parole} />\n                  ))}\n                  {invites.map(([id, pose]) =>\n                    pose.parole ? <Parole key={id} x={Math.round(pose.x)} haut={cadrage(pose).haut} texte={pose.parole} /> : null,\n                  )}\n                </svg>\n              ) : null}\n\n              {selection && ancre ? (\n                <Carte\n                  key={selection.fiche.id}\n                  agent={selection}\n                  ancre={ancre}\n                  hauteur={lePlan.hauteur}\n                  maintenant={maintenant}\n                  lancer={onRun}\n                  fermer={() => setChoisi(null)}\n                />\n              ) : null}\n            </div>\n          </div>\n          {/* Comme la scène, la frise attend le navigateur : ses points dépendent de l'heure. */}\n          {pret && timeline && agents.some((a) => a.runs?.length) ? (\n            <Frise agents={lead ? [lead, ...agents] : agents} maintenant={maintenant} choisir={setChoisi} />\n          ) : null}\n          {jeu ? <JeuArcade key={jeu} jeu={jeu} textes={t} fermer={() => setJeu(null)} /> : null}\n        </div>\n      </ThemeContexte.Provider>\n    </TextesContexte.Provider>\n  )\n}\n\nconst sans = <T,>(objet: Record<string, T>, cle: string) => Object.fromEntries(Object.entries(objet).filter(([k]) => k !== cle))\n",
      "type": "registry:component",
      "target": "components/pixel-openspace/pixel-openspace.tsx"
    },
    {
      "path": "registry/pixel-openspace/vue.ts",
      "content": "/*\n * Ce que la scène sait d'un agent, et les calculs purs qu'elle en tire : son état, depuis quand il est passé, quand il repasse, s'il est oublié, et l'obscurité de la salle.\n */\n\nimport { CHEF_ID, marcheur, type Marcheur, type Plan, type Statut } from './engine'\nimport { type Texts } from './i18n'\nimport type { Agent, AgentStatus } from './types'\n\n/* ——— Ce que la scène sait d'un agent ——— */\n\n/** Un agent tel que la scène le manipule : sa fiche, ce qu'il a dit, son état. */\nexport type Vue = {\n  fiche: { id: string; nom: string; emoji: string; role: string; rythme: string | null; vieillitApresH: number }\n  brut: { dernierMessage: string | null; dernierPassage: string | null }\n  statut: Statut\n  prochain: string | null\n  source: Agent\n}\n\nexport const STATUT: Record<AgentStatus, Statut> = {\n  working: 'au-travail',\n  ok: 'a-jour',\n  late: 'en-retard',\n  asleep: 'endormi',\n  failed: 'en-echec',\n  off: 'absent',\n  never: 'jamais',\n  'on-demand': 'a-la-demande',\n}\n\nexport const signales = new Set<string>()\n\n/** Le statut de la scène ; un statut inconnu compte comme « ok », signalé une fois en développement. */\nexport function statutDe(a: Agent): Statut {\n  const statut = STATUT[a.status]\n  if (statut) return statut\n  const cle = `${a.id}:${a.status}`\n  if (typeof process !== 'undefined' && process.env?.NODE_ENV !== 'production' && !signales.has(cle)) {\n    signales.add(cle)\n    console.warn(`pixel-openspace: unknown status \"${a.status}\" for agent \"${a.id}\", shown as \"ok\".`)\n  }\n  return 'a-jour'\n}\n\nexport function versVue(a: Agent, id = a.id): Vue {\n  return {\n    fiche: {\n      id,\n      nom: a.name,\n      emoji: a.emoji ?? '🤖',\n      role: a.role ?? '',\n      rythme: a.schedule ?? null,\n      vieillitApresH: a.staleAfterHours ?? (a.status === 'on-demand' ? 24 * 7 : 72),\n    },\n    brut: { dernierMessage: a.lastMessage ?? null, dernierPassage: a.lastRun ?? null },\n    statut: statutDe(a),\n    prochain: a.nextRun ?? null,\n    source: a,\n  }\n}\n\nexport type Equipe = { chef: Vue; agents: Array<Vue> }\n\nexport function depuis(iso: string | null, maintenant: number, t: Texts): string {\n  if (!iso) return t.never\n  const minutes = Math.round((maintenant - Date.parse(iso)) / 60_000)\n  if (minutes < 1) return t.justNow\n  if (minutes < 60) return t.minutesAgo(minutes)\n  const heures = Math.round(minutes / 60)\n  if (heures < 24) return t.hoursAgo(heures)\n  return t.daysAgo(Math.round(heures / 24))\n}\n\n/** « 09:28 » aujourd'hui, « Mon 09:15 » un autre jour : de quoi tenir sur le tableau blanc. */\nexport function heureCourte(iso: string, maintenant: number, locale: string): string {\n  const d = new Date(iso)\n  const h = d.toLocaleTimeString(locale, { hour: '2-digit', minute: '2-digit' })\n  return d.toDateString() === new Date(maintenant).toDateString() ? h : `${d.toLocaleDateString(locale, { weekday: 'short' })} ${h}`\n}\n\n/** Assis à son bureau, assis au canapé, ou debout. */\nexport type Assise = 'bureau' | 'canape' | null\n\n/** Ce que la page dessine d'un marcheur, à l'image près. */\nexport type Pose = {\n  x: number\n  y: number\n  angle: number\n  marche: boolean\n  pas: number\n  assise: Assise\n  /** Le lieu où il se tient, ou vers lequel il marche. */\n  lieu: string\n  parole: string | null\n  /** Le livreur, son colis dans les bras. */\n  porte: boolean\n}\n\nexport function poses(p: Plan, marcheurs: Array<Marcheur>): Record<string, Pose> {\n  const scene: Record<string, Pose> = Object.fromEntries(\n    marcheurs.map((m) => {\n      const siege = m.id === CHEF_ID ? p.chef.siege : p.bureaux.find((b) => b.id === m.id)?.siege\n      const arrive = !m.chemin.length\n      const assise: Assise = arrive && m.lieu.assis ? (m.lieu === siege ? 'bureau' : 'canape') : null\n      return [\n        m.id,\n        {\n          x: m.pos.x,\n          y: m.pos.y,\n          angle: m.angle,\n          marche: !arrive,\n          pas: m.pas,\n          assise,\n          lieu: m.lieu.nom,\n          parole: arrive ? m.parole : null,\n          porte: m.porte,\n        },\n      ]\n    }),\n  )\n  // Deux au canapé, et la conversation s'engage.\n  const auCanape = Object.values(scene).filter((s) => s.assise === 'canape')\n  if (auCanape.length > 1) for (const s of auCanape) s.parole ??= '…'\n  return scene\n}\n\nexport const posesAuBureau = (p: Plan) =>\n  poses(\n    p,\n    [p.chef, ...p.bureaux].map((b) => marcheur(b.id, b.siege, 0)),\n  )\n\n/** Les minutes avant le prochain passage, quand il approche (dix minutes au plus). */\nexport function bientot(vue: Vue, maintenant: number): number | null {\n  if (!vue.prochain || vue.statut === 'au-travail' || vue.statut === 'absent') return null\n  const reste = Date.parse(vue.prochain) - maintenant\n  // À moins de 30 s, c'est un passage dû maintenant, souvent celui d'un ordonnanceur en retard qui annonce\n  // « tout de suite » à chaque relecture : sans ce seuil, l'écran resterait figé sur « dans 1 min ».\n  if (reste < 30_000) return null\n  const minutes = Math.ceil(reste / 60_000)\n  return minutes <= 10 ? minutes : null\n}\n\n/** L'obscurité de la salle selon l'heure : nuit noire de 21 h à 6 h, le soir et le matin en pente douce. */\nexport function obscurite(maintenant: number): number {\n  const d = new Date(maintenant)\n  const h = d.getHours() + d.getMinutes() / 60\n  const nuit = 0.62\n  if (h >= 8 && h < 18) return 0\n  if (h >= 21 || h < 6) return nuit\n  return h >= 18 ? ((h - 18) / 3) * nuit : ((8 - h) / 2) * nuit\n}\n\n/** Un agent oublié : plus de passage depuis plus longtemps qu'on ne l'admet (staleAfterHours). */\nexport function oublie(vue: Vue, maintenant: number): boolean {\n  if (vue.statut === 'absent' || vue.fiche.id === CHEF_ID) return false\n  if (!vue.brut.dernierPassage) return vue.statut !== 'jamais' && vue.statut !== 'a-la-demande'\n  const heures = (maintenant - Date.parse(vue.brut.dernierPassage)) / 3_600_000\n  return heures > vue.fiche.vieillitApresH\n}\n",
      "type": "registry:lib",
      "target": "components/pixel-openspace/vue.ts"
    },
    {
      "path": "registry/pixel-openspace/contexte.ts",
      "content": "/*\n * Les thèmes, et le contexte qui donne le thème de la salle à tout ce qui la dessine.\n */\n\nimport { createContext, useContext } from 'react'\nimport type { Theme } from './theme'\nimport { THEME_80 } from './themes/eighties'\nimport { THEME_GEEK } from './themes/geek'\nimport { THEME_GYM } from './themes/gym'\nimport { THEME_CUISINE } from './themes/kitchen'\nimport { THEME_MODERNE } from './themes/modern'\nimport { THEME_NEON } from './themes/neon'\nimport type { ThemeName } from './types'\n\n/* Les thèmes disponibles, par leur nom public. */\nexport const THEMES: Record<ThemeName, Theme> = {\n  geek: THEME_GEEK,\n  eighties: THEME_80,\n  gym: THEME_GYM,\n  modern: THEME_MODERNE,\n  kitchen: THEME_CUISINE,\n  neon: THEME_NEON,\n}\n\n/* Le thème de la scène, à portée des postes et des bonshommes. */\nexport const ThemeContexte = createContext<Theme>(THEME_GEEK)\nexport const useTheme = () => useContext(ThemeContexte)\n",
      "type": "registry:lib",
      "target": "components/pixel-openspace/contexte.ts"
    },
    {
      "path": "registry/pixel-openspace/card.tsx",
      "content": "'use client'\n\n/*\n * La fiche d'un agent, accrochée au-dessus de son bureau quand on clique dessus.\n */\n\nimport { useState } from 'react'\nimport { Button } from '@/components/ui/button'\nimport { Badge } from '@/components/ui/badge'\nimport { Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'\nimport { LARGEUR } from './engine'\nimport { LAMPE_PLAN, useTextes } from './primitives'\nimport type { Agent } from './types'\nimport { type Vue, depuis, heureCourte } from './vue'\n\n/** La carte d'un agent, posée sur la scène au-dessus de lui (ou en dessous, près du haut). */\nexport function Carte({\n  agent,\n  ancre,\n  hauteur,\n  maintenant,\n  lancer,\n  fermer,\n}: {\n  agent: Vue\n  ancre: { x: number; haut: number; bas: number }\n  hauteur: number\n  maintenant: number\n  lancer?: (agent: Agent) => void | Promise<void>\n  fermer: () => void\n}) {\n  const t = useTextes()\n  const { fiche, statut, brut, prochain } = agent\n  const [enCours, setEnCours] = useState(false)\n  const dessous = ancre.haut < 240\n  const horizontal = ancre.x < 160 ? '0%' : ancre.x > LARGEUR - 160 ? '-100%' : '-50%'\n  const travaille = statut === 'au-travail'\n  return (\n    <Card\n      // L'attribut plutôt que la prop : la carte compacte du style Base UI, sans gêner le style Radix qui ne la connaît pas.\n      data-size=\"sm\"\n      className=\"absolute z-10 w-72 shadow-xl\"\n      style={{\n        left: `${(ancre.x / LARGEUR) * 100}%`,\n        top: `${((dessous ? ancre.bas : ancre.haut) / hauteur) * 100}%`,\n        transform: dessous ? `translate(${horizontal}, 10px)` : `translate(${horizontal}, calc(-100% - 10px))`,\n      }}\n    >\n      <CardHeader>\n        <CardTitle>\n          <span aria-hidden>{fiche.emoji}</span> {fiche.nom}\n        </CardTitle>\n        {fiche.role ? <CardDescription>{fiche.role}</CardDescription> : null}\n        <CardAction>\n          <Button variant=\"ghost\" size=\"icon-xs\" onClick={fermer} aria-label={t.close}>\n            ✕\n          </Button>\n        </CardAction>\n      </CardHeader>\n      <CardContent className=\"flex flex-col gap-2 text-xs\">\n        <div className=\"flex flex-wrap items-center gap-1.5\">\n          <Badge variant={statut === 'en-echec' ? 'destructive' : 'secondary'}>\n            <span className=\"size-1.5 rounded-full\" style={{ background: LAMPE_PLAN[statut] }} />\n            {t.statuses[statut]}\n          </Badge>\n          {fiche.rythme ? <Badge variant=\"outline\">{fiche.rythme}</Badge> : null}\n        </div>\n        <p className=\"text-muted-foreground\">\n          {t.lastRun} {depuis(brut.dernierPassage, maintenant, t)}\n          {prochain ? ` · ${t.nextRun} ${heureCourte(prochain, maintenant, t.locale)}` : ''}\n        </p>\n        {brut.dernierMessage && statut !== 'absent' ? (\n          <p className=\"line-clamp-2 rounded-md bg-muted px-2 py-1 font-mono text-[0.65rem]\">{brut.dernierMessage}</p>\n        ) : null}\n      </CardContent>\n      {lancer ? (\n        <CardFooter>\n          <Button\n            size=\"sm\"\n            disabled={enCours || travaille}\n            onClick={async () => {\n              setEnCours(true)\n              try {\n                await lancer(agent.source)\n              } finally {\n                setEnCours(false)\n              }\n            }}\n          >\n            {enCours ? t.launching : travaille ? t.running : t.runNow}\n          </Button>\n        </CardFooter>\n      ) : null}\n    </Card>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/pixel-openspace/card.tsx"
    },
    {
      "path": "registry/pixel-openspace/decor.tsx",
      "content": "'use client'\n\n/*\n * Ce qui meuble la salle autour des agents : la nuit, l'écran mural, le tableau blanc, les records, les colis, l'horloge, la fenêtre, le chat, les plantes, la poussière, les confettis et la lampe.\n */\n\nimport { LARGEUR, type Statut } from './engine'\nimport { ACCENT, Anim, LAMPE_PLAN, Pixels, etapes, useTextes, type Lumiere } from './primitives'\nimport type { WallTile, Weather } from './types'\nimport { type Pose } from './vue'\n\n/**\n * La nuit tombe sur la salle, percée par les lumières : un voile bleu nuit,\n * avec un trou doux autour de chaque écran allumé. Les LED, les néons et les\n * écrans au travail jettent en plus une lueur de leur couleur.\n */\nexport function Nuit({\n  niveau,\n  lumieres,\n  hauteur,\n  voile = { couleur: '#0a1033', opacite: 0.62 },\n}: {\n  niveau: number\n  lumieres: Array<Lumiere>\n  hauteur: number\n  voile?: { couleur: string; opacite: number }\n}) {\n  if (!niveau) return null\n  // L'obscurité va jusqu'à 0,62 ; le thème choisit jusqu'où son voile descend.\n  const opacite = (niveau / 0.62) * voile.opacite\n  return (\n    <g pointerEvents=\"none\" shapeRendering=\"auto\">\n      <defs>\n        <radialGradient id=\"trou\">\n          <stop offset=\"0\" stopColor=\"black\" />\n          <stop offset=\"0.45\" stopColor=\"black\" stopOpacity=\"0.85\" />\n          <stop offset=\"1\" stopColor=\"black\" stopOpacity=\"0\" />\n        </radialGradient>\n        {lumieres.map((l, i) =>\n          l.couleur ? (\n            <radialGradient key={i} id={`lueur-${i}`}>\n              <stop offset=\"0\" stopColor={l.couleur} stopOpacity=\"0.4\" />\n              <stop offset=\"1\" stopColor={l.couleur} stopOpacity=\"0\" />\n            </radialGradient>\n          ) : null,\n        )}\n        <mask id=\"nuit-masque\">\n          <rect width={LARGEUR} height={hauteur} fill=\"white\" />\n          {lumieres.map((l, i) => (\n            <circle key={i} cx={l.x} cy={l.y} r={l.r} fill=\"url(#trou)\" />\n          ))}\n        </mask>\n      </defs>\n      <rect width={LARGEUR} height={hauteur} fill={voile.couleur} opacity={opacite} mask=\"url(#nuit-masque)\" />\n      {lumieres.map((l, i) =>\n        l.couleur ? (\n          <ellipse key={i} cx={l.x} cy={l.y + 16} rx={l.r * 0.85} ry={l.r * 0.65} fill={`url(#lueur-${i})`} opacity={niveau / 0.62} />\n        ) : null,\n      )}\n    </g>\n  )\n}\n\n/* La couleur d'une tuile selon son ton. */\nexport const TONS: Record<NonNullable<WallTile['tone']>, string> = {\n  neutral: '#e5e7eb',\n  ok: '#4ade80',\n  info: '#38bdf8',\n  warn: '#fb923c',\n  alert: '#ef4444',\n}\n\n/**\n * Le grand écran accroché au mur de la grande salle : jusqu'à quatre tuiles,\n * leur valeur, et une jauge pour celles qui en ont une.\n */\nexport function EcranMural({ tuiles }: { tuiles: Array<WallTile> }) {\n  const affichees = tuiles.slice(0, 4)\n  if (!affichees.length) return null\n  const largeur = 196 / affichees.length\n  return (\n    <g pointerEvents=\"none\">\n      <rect x=\"30\" y=\"206\" width=\"200\" height=\"34\" fill=\"#0b0d10\" />\n      <rect x=\"32\" y=\"208\" width=\"196\" height=\"30\" fill=\"#0b1220\" />\n      {affichees.map((tuile, i) => {\n        const x = 34 + i * largeur\n        const l = largeur - 2.5\n        const couleur = tuile.tone ? TONS[tuile.tone] : ACCENT\n        return (\n          <g key={`${i}-${tuile.label}`}>\n            <rect x={x} y=\"210\" width={l} height=\"26\" fill=\"#111827\" />\n            <text x={x + l / 2} y=\"217\" fontSize=\"5.5\" textAnchor=\"middle\" fill=\"#94a3b8\">\n              {tuile.label.toUpperCase()}\n            </text>\n            <text x={x + l / 2} y=\"230\" fontSize=\"12\" textAnchor=\"middle\" fill={couleur}>\n              {tuile.value}\n            </text>\n            {tuile.progress !== undefined ? (\n              <>\n                <rect x={x + 4} y=\"232\" width={l - 8} height=\"2\" fill=\"#1f2937\" />\n                <rect x={x + 4} y=\"232\" width={Math.max(0, Math.min(1, tuile.progress)) * (l - 8)} height=\"2\" fill={couleur} />\n              </>\n            ) : null}\n          </g>\n        )\n      })}\n    </g>\n  )\n}\n\n/* ——— Le tableau blanc, les records, la porte, les colis, les fêtes ——— */\n\n/** Le tableau blanc du bureau du chef : les prochains passages, au feutre. */\nexport function TableauBlanc({ prochains }: { prochains: Array<{ heure: string; nom: string }> }) {\n  const t = useTextes()\n  return (\n    <g pointerEvents=\"none\">\n      <rect x=\"32\" y=\"176\" width=\"4\" height=\"12\" fill=\"#6b7280\" />\n      <rect x=\"124\" y=\"176\" width=\"4\" height=\"12\" fill=\"#6b7280\" />\n      <rect x=\"26\" y=\"108\" width=\"108\" height=\"70\" fill=\"#9ca3af\" />\n      <rect x=\"29\" y=\"111\" width=\"102\" height=\"64\" fill=\"#f8fafc\" />\n      <rect x=\"29\" y=\"171\" width=\"102\" height=\"4\" fill=\"#d1d5db\" />\n      <rect x=\"100\" y=\"172\" width=\"10\" height=\"2\" fill=\"#2563eb\" />\n      <rect x=\"112\" y=\"172\" width=\"10\" height=\"2\" fill=\"#dc2626\" />\n      <text x=\"34\" y=\"120\" fontSize=\"7\" fill=\"#1d4ed8\">\n        {t.nextRuns}\n      </text>\n      {prochains.length ? (\n        prochains.map((p, i) => (\n          <text key={p.nom} x=\"34\" y={130 + i * 8.5} fontSize=\"6.5\" fill={i === 0 ? '#dc2626' : '#1f2937'}>\n            {p.heure} · {p.nom}\n          </text>\n        ))\n      ) : (\n        <text x=\"34\" y=\"132\" fontSize=\"6.5\" fill=\"#6b7280\">\n          {t.nothingPlanned}\n        </text>\n      )}\n    </g>\n  )\n}\n\n/** Le panneau des records, accroché au mur à côté du coin gaming. */\nexport function Records({ snake, casse }: { snake: number; casse: number }) {\n  const t = useTextes()\n  const pad = (n: number) => String(n).padStart(3, '0')\n  return (\n    <g pointerEvents=\"none\">\n      <rect x=\"286\" y=\"207\" width=\"70\" height=\"33\" fill=\"#0b0d10\" />\n      <rect x=\"288\" y=\"209\" width=\"66\" height=\"29\" fill=\"#1e1b4b\" />\n      <text x=\"321\" y=\"217\" fontSize=\"6.5\" textAnchor=\"middle\" fill=\"var(--po-accent)\">\n        {t.hiScores}\n        <Anim attributeName=\"opacity\" values=\"1;0.5;1\" dur=\"1.5s\" repeatCount=\"indefinite\" />\n      </text>\n      <text x=\"292\" y=\"226\" fontSize=\"6\" fill=\"#f472b6\">\n        SNAKE\n      </text>\n      <text x=\"350\" y=\"226\" fontSize=\"6\" textAnchor=\"end\" fill=\"#f472b6\">\n        {pad(snake)}\n      </text>\n      <text x=\"292\" y=\"234\" fontSize=\"6\" fill=\"#67e8f9\">\n        CASSE\n      </text>\n      <text x=\"350\" y=\"234\" fontSize=\"6\" textAnchor=\"end\" fill=\"#67e8f9\">\n        {pad(casse)}\n      </text>\n    </g>\n  )\n}\n\n/** Les colis déposés par le livreur depuis l'ouverture de la page. */\nexport function ColisDeposes({ nombre, bas }: { nombre: number; bas: number }) {\n  return (\n    <g pointerEvents=\"none\">\n      {Array.from({ length: nombre }, (_, i) => (\n        <g key={i}>\n          <rect x={128 + i * 6} y={bas - 22 - i * 12} width=\"22\" height=\"14\" fill=\"#b45309\" />\n          <rect x={128 + i * 6} y={bas - 22 - i * 12} width=\"22\" height=\"3\" fill=\"#d97706\" />\n          <rect x={137 + i * 6} y={bas - 22 - i * 12} width=\"4\" height=\"14\" fill=\"#fde68a\" />\n        </g>\n      ))}\n    </g>\n  )\n}\n\n/**\n * L'horloge du bureau du chef, à la vraie heure. Les aiguilles des heures et\n * des minutes suivent la page, rafraîchie toutes les cinq secondes ; la\n * trotteuse tourne seule, calée sur la seconde d'ouverture de la page.\n */\nexport function Horloge({ maintenant, ouverture }: { maintenant: number; ouverture: number }) {\n  const t = useTextes()\n  const cx = 302\n  const cy = 36\n  const d = new Date(maintenant)\n  const minutes = d.getMinutes() + d.getSeconds() / 60\n  const heures = (d.getHours() % 12) + minutes / 60\n  const aiguille = (angle: number, longueur: number, epaisseur: number, couleur: string) => (\n    <rect\n      x={cx - epaisseur / 2}\n      y={cy - longueur}\n      width={epaisseur}\n      height={longueur}\n      fill={couleur}\n      transform={`rotate(${angle} ${cx} ${cy})`}\n    />\n  )\n  return (\n    <g pointerEvents=\"none\">\n      <title>{d.toLocaleTimeString(t.locale, { hour: '2-digit', minute: '2-digit' })}</title>\n      <circle cx={cx} cy={cy} r=\"14\" fill=\"#111827\" shapeRendering=\"auto\" />\n      <circle cx={cx} cy={cy} r=\"12\" fill=\"#f8fafc\" shapeRendering=\"auto\" />\n      {Array.from({ length: 12 }, (_, i) => (\n        <rect\n          key={i}\n          x={cx - 0.75}\n          y={cy - 11}\n          width=\"1.5\"\n          height={i % 3 ? 2 : 3}\n          fill=\"#1f2937\"\n          transform={`rotate(${i * 30} ${cx} ${cy})`}\n        />\n      ))}\n      {aiguille(heures * 30, 6, 2, '#111827')}\n      {aiguille(minutes * 6, 9, 1.5, '#111827')}\n      <rect\n        x={cx - 0.5}\n        y={cy - 10}\n        width=\"1\"\n        height=\"12\"\n        fill=\"#dc2626\"\n        style={{\n          transformOrigin: `${cx}px ${cy}px`,\n          animation: 'trotteuse 60s steps(60) infinite',\n          animationDelay: `-${new Date(ouverture).getSeconds()}s`,\n        }}\n      />\n      <circle cx={cx} cy={cy} r=\"1.5\" fill=\"#dc2626\" shapeRendering=\"auto\" />\n    </g>\n  )\n}\n\n/* ——— La météo, le chat, les plantes, les confettis ——— */\n\nexport const CIEL: Record<Weather['sky'], 'soleil' | 'nuages' | 'brouillard' | 'pluie' | 'neige' | 'orage'> = {\n  clear: 'soleil',\n  clouds: 'nuages',\n  fog: 'brouillard',\n  rain: 'pluie',\n  snow: 'neige',\n  storm: 'orage',\n}\n\n/** La fenêtre du bureau du chef, ouverte sur le ciel qu'on lui donne. */\nexport function Fenetre({ meteo: temps }: { meteo: Weather | null }) {\n  const t = useTextes()\n  const meteo = temps\n    ? { temperature: Math.round(temps.temperature), ciel: CIEL[temps.sky], jour: temps.day, lieu: temps.place ?? '' }\n    : null\n  const x = 94\n  const l = 56\n  const ciel = !meteo\n    ? '#1e293b'\n    : !meteo.jour\n      ? '#0f172a'\n      : { soleil: '#7dd3fc', nuages: '#94a3b8', brouillard: '#cbd5e1', pluie: '#64748b', neige: '#cbd5e1', orage: '#334155' }[meteo.ciel]\n  const nuage = meteo?.ciel === 'orage' ? '#475569' : '#f1f5f9'\n  return (\n    <g pointerEvents=\"none\">\n      <title>{meteo ? `${meteo.lieu ? `${meteo.lieu}: ` : ''}${meteo.temperature} °C` : t.noWeather}</title>\n      <rect x={x - 3} y=\"19\" width={l + 6} height=\"33\" fill=\"#e5e7eb\" />\n      <svg x={x} y=\"22\" width={l} height=\"27\" overflow=\"hidden\">\n        <rect width={l} height=\"27\" fill={ciel} />\n        {meteo?.jour && meteo.ciel === 'soleil' ? (\n          <g>\n            <rect x=\"38\" y=\"5\" width=\"9\" height=\"9\" fill=\"#fde047\" />\n            <rect x=\"36\" y=\"8\" width=\"2\" height=\"3\" fill=\"#facc15\" />\n            <rect x=\"47\" y=\"8\" width=\"2\" height=\"3\" fill=\"#facc15\" />\n            <rect x=\"41\" y=\"3\" width=\"3\" height=\"2\" fill=\"#facc15\" />\n            <rect x=\"41\" y=\"14\" width=\"3\" height=\"2\" fill=\"#facc15\" />\n          </g>\n        ) : null}\n        {meteo && !meteo.jour ? (\n          <g>\n            <rect x=\"40\" y=\"5\" width=\"7\" height=\"7\" fill=\"#f1f5f9\" />\n            <rect x=\"43\" y=\"4\" width=\"6\" height=\"6\" fill={ciel} />\n            {[\n              [8, 6],\n              [20, 3],\n              [28, 9],\n              [14, 12],\n            ].map(([sx, sy]) => (\n              <rect key={`${sx}-${sy}`} x={sx} y={sy} width=\"1\" height=\"1\" fill=\"#f8fafc\">\n                <Anim attributeName=\"opacity\" values=\"1;0.2;1\" dur={`${1.5 + (sx % 3)}s`} repeatCount=\"indefinite\" />\n              </rect>\n            ))}\n          </g>\n        ) : null}\n        {meteo && meteo.ciel !== 'soleil'\n          ? [0, 1].map((i) => (\n              <g key={i}>\n                <rect x={i * 30} y={4 + i * 5} width=\"20\" height=\"6\" fill={nuage} />\n                <rect x={i * 30 + 4} y={1 + i * 5} width=\"10\" height=\"4\" fill={nuage} />\n                <Anim\n                  attributeName=\"transform\"\n                  type=\"translate\"\n                  values={etapes(-20, l, 12)\n                    .split(';')\n                    .map((v) => `${v} 0`)\n                    .join(';')}\n                  dur={`${14 + i * 6}s`}\n                  begin={`${-i * 7}s`}\n                  repeatCount=\"indefinite\"\n                />\n              </g>\n            ))\n          : null}\n        {meteo?.ciel === 'pluie' || meteo?.ciel === 'orage'\n          ? Array.from({ length: 9 }, (_, i) => (\n              <rect key={i} x={3 + i * 6} y=\"0\" width=\"1\" height=\"4\" fill=\"#bfdbfe\">\n                <Anim attributeName=\"y\" values={etapes(8, 27, 5)} dur=\"0.6s\" begin={`${(i % 4) * 0.15}s`} repeatCount=\"indefinite\" />\n              </rect>\n            ))\n          : null}\n        {meteo?.ciel === 'neige'\n          ? Array.from({ length: 8 }, (_, i) => (\n              <rect key={i} x={4 + i * 7} y=\"0\" width=\"2\" height=\"2\" fill=\"#f8fafc\">\n                <Anim\n                  attributeName=\"y\"\n                  values={etapes(6, 27, 7)}\n                  dur={`${2 + (i % 3) * 0.6}s`}\n                  begin={`${(i % 4) * 0.4}s`}\n                  repeatCount=\"indefinite\"\n                />\n              </rect>\n            ))\n          : null}\n        {meteo?.ciel === 'brouillard'\n          ? [8, 15, 21].map((y) => <rect key={y} x=\"0\" y={y} width={l} height=\"3\" fill=\"#f8fafc\" opacity=\"0.6\" />)\n          : null}\n        {meteo?.ciel === 'orage' ? (\n          <rect width={l} height=\"27\" fill=\"#fef9c3\" opacity=\"0\">\n            <Anim attributeName=\"opacity\" values=\"0;0;0.8;0;0.5;0\" keyTimes=\"0;0.8;0.82;0.85;0.87;1\" dur=\"5s\" repeatCount=\"indefinite\" />\n          </rect>\n        ) : null}\n        <rect x=\"0\" y=\"20\" width=\"22\" height=\"7\" fill=\"#0b0d10\" opacity=\"0.65\" />\n        <text x=\"2\" y=\"26\" fontSize=\"6\" fill=\"#f8fafc\">\n          {meteo ? `${meteo.temperature}°` : '—'}\n        </text>\n      </svg>\n      <rect x={x + l / 2 - 1} y=\"22\" width=\"2\" height=\"27\" fill=\"#e5e7eb\" />\n      <rect x={x} y=\"35\" width={l} height=\"2\" fill=\"#e5e7eb\" />\n      <text x={x + l / 2} y=\"58\" fontSize=\"5\" textAnchor=\"middle\" fill=\"#9ca3af\">\n        {meteo?.lieu.toUpperCase()}\n      </text>\n    </g>\n  )\n}\n\n/* Le chat roux tigré : debout de profil, deux temps de marche, et roulé en boule pour la sieste. */\nexport const CHAT_DEBOUT = [\n  ['.......k.k', 'k......kkk', '.K.....kek', '.kKkKkKkk.', '..kkkkkkk.', '..k.k..k.k'],\n  ['.......k.k', 'k......kkk', '.K.....kek', '.kKkKkKkk.', '..kkkkkkk.', '...k.k.k.k'],\n]\nexport const CHAT_ROULE = ['..kkkk..', '.kKkKkk.', 'kkkkkKkk', 'Kkkkkkkk', '.kkkkkkK']\nexport const PELAGE = { k: '#f59e0b', K: '#b45309', e: '#111827' }\n\n/** Le chat d'atelier : il marche de profil, ou dort roulé en boule là où il s'est posé. */\nexport function Chat({ pose }: { pose: Pose }) {\n  const t = useTextes()\n  if (!pose.marche) {\n    return (\n      <g pointerEvents=\"none\">\n        <title>{t.catNap}</title>\n        <g transform={`translate(${Math.round(pose.x) - 12} ${Math.round(pose.y) - 15})`}>\n          <Pixels grille={CHAT_ROULE} couleurs={PELAGE} />\n        </g>\n        {pose.parole ? (\n          <text x={pose.x + 10} y={pose.y - 16} fontSize=\"7\" fill=\"#e0f2fe\">\n            z\n            <Anim attributeName=\"y\" values={etapes(pose.y - 14, pose.y - 24, 4)} dur=\"2s\" repeatCount=\"indefinite\" />\n            <Anim attributeName=\"opacity\" values=\"1;0.75;0.5;0.25\" dur=\"2s\" repeatCount=\"indefinite\" />\n          </text>\n        ) : null}\n      </g>\n    )\n  }\n  const versLaDroite = Math.sin((pose.angle * Math.PI) / 180) >= 0\n  const grille = CHAT_DEBOUT[Math.floor(pose.pas / 140) % 2]\n  return (\n    <g pointerEvents=\"none\">\n      <ellipse cx={pose.x} cy={pose.y} rx=\"12\" ry=\"2.5\" fill=\"black\" opacity=\"0.25\" shapeRendering=\"auto\" />\n      <g transform={`translate(${Math.round(pose.x)} ${Math.round(pose.y) - 18}) scale(${versLaDroite ? 1 : -1} 1) translate(-15 0)`}>\n        <Pixels grille={grille} couleurs={PELAGE} />\n      </g>\n    </g>\n  )\n}\n\n/* La petite plante au pied de chaque bureau ; fanée quand l'agent ne passe plus. */\nexport const PLANTE_VIVE = ['.g..g.', 'gGggGg', '.gGGg.', '..gg..', '.pppp.', '.PPPP.', '..PP..']\nexport const PLANTE_FANEE = ['......', 'b....b', '.b..b.', '..bb..', '.pppp.', '.PPPP.', '..PP..']\n\nexport function PlanteDeBureau({ x, y, fanee }: { x: number; y: number; fanee: boolean }) {\n  return (\n    <g transform={`translate(${x} ${y})`} pointerEvents=\"none\">\n      <Pixels\n        grille={fanee ? PLANTE_FANEE : PLANTE_VIVE}\n        couleurs={{ g: '#4caf50', G: '#2e7d32', b: '#92400e', p: '#d07a3e', P: '#a35a2a' }}\n        u={2}\n      />\n    </g>\n  )\n}\n\n/** La poussière et la toile sur l'écran d'un agent oublié. */\nexport function Poussiere({ cx, dy }: { cx: number; dy: number }) {\n  const g = cx - 4 - 27\n  return (\n    <g pointerEvents=\"none\">\n      {[\n        [4, 2],\n        [12, 8],\n        [22, 4],\n        [31, 12],\n        [40, 6],\n        [48, 14],\n        [8, 15],\n        [26, 17],\n        [44, 2],\n      ].map(([px, py]) => (\n        <rect key={`${px}-${py}`} x={g + px} y={dy - 12 + py} width=\"3\" height=\"2\" fill=\"#d6d3d1\" opacity=\"0.55\" />\n      ))}\n      <path\n        d={`M${g + 54} ${dy - 12}h-10M${g + 54} ${dy - 12}v10M${g + 54} ${dy - 12}l-8 8M${g + 49} ${dy - 12}q-1 4 5 5`}\n        stroke=\"#e5e7eb\"\n        strokeWidth=\"0.8\"\n        fill=\"none\"\n        opacity=\"0.7\"\n        shapeRendering=\"auto\"\n      />\n    </g>\n  )\n}\n\n/** Des confettis qui tombent sur toute la salle : une machine fête son anniversaire. */\nexport function Confettis({ hauteur }: { hauteur: number }) {\n  const couleurs = ['#f472b6', '#facc15', '#22d3ee', '#4ade80', '#a855f7', '#fb923c']\n  return (\n    <g pointerEvents=\"none\">\n      {Array.from({ length: 36 }, (_, i) => {\n        const x = (i * 137) % LARGEUR\n        const duree = 4 + (i % 5)\n        return (\n          <rect key={i} x={x} y=\"-10\" width=\"4\" height=\"6\" fill={couleurs[i % couleurs.length]}>\n            <Anim\n              attributeName=\"y\"\n              values={etapes(-10, hauteur, 24)}\n              dur={`${duree}s`}\n              begin={`${-((i * 0.7) % duree)}s`}\n              repeatCount=\"indefinite\"\n            />\n            <Anim attributeName=\"x\" values={`${x};${x + 12};${x - 8};${x}`} dur=\"3s\" repeatCount=\"indefinite\" />\n          </rect>\n        )\n      })}\n    </g>\n  )\n}\n\nexport function Lampe({ x, y, statut }: { x: number; y: number; statut: Statut }) {\n  return (\n    <rect x={x} y={y} width=\"5\" height=\"5\" fill={LAMPE_PLAN[statut]}>\n      {statut === 'au-travail' || statut === 'en-echec' ? (\n        <Anim attributeName=\"opacity\" values=\"1;0.3;1\" dur=\"1.2s\" repeatCount=\"indefinite\" />\n      ) : null}\n    </rect>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/pixel-openspace/decor.tsx"
    },
    {
      "path": "registry/pixel-openspace/festivities.tsx",
      "content": "'use client'\n\n/*\n * Les décorations de saison : Halloween, Noël et Pâques.\n */\n\nimport { LARGEUR } from './engine'\nimport { Anim, MUR_SALLE, Pixels, etapes } from './primitives'\nimport { type Fete } from './seasons'\n\n/** Les fêtes : Halloween tout le mois d'octobre, Noël tout le mois de décembre. */\n\n/* Pâques : un œuf, le poussin, le lapin de profil. */\nexport const OEUF = ['..ee..', '.eeee.', 'ebbbbe', 'eeeeee', 'ewewew', '.eeee.', '..ee..']\nexport const POUSSIN = ['.yy...', 'yyey..', 'yyyyo.', '.yyy..', '.o.o..']\nexport const LAPIN = ['.....gw.gw', '.....gw.gw', '....wwwwww', 'w..wwwwwep', 'wwwwwwwwww', '.wwwwwwww.', '.ww....ww.']\n/* Où sont cachés les œufs, et de quelles couleurs : x, y (ou « bas », au pied du mur du fond), fond, rayure, points. */\nexport const OEUFS: Array<[number, number | 'bas', string, string, string]> = [\n  [44, 182, '#f9a8d4', '#f472b6', '#fdf2f8'],\n  [318, 96, '#7c2d12', '#facc15', '#92400e'],\n  [470, 182, '#a5f3fc', '#22d3ee', '#ecfeff'],\n  [610, 176, '#7c2d12', '#facc15', '#92400e'],\n  [846, 46, '#bbf7d0', '#4ade80', '#f0fdf4'],\n  [962, 176, '#ddd6fe', '#a78bfa', '#f5f3ff'],\n  [70, 300, '#fde68a', '#f59e0b', '#fffbeb'],\n  [955, 470, '#7c2d12', '#facc15', '#92400e'],\n  [420, 'bas', '#fecdd3', '#fb7185', '#fff1f2'],\n  [690, 'bas', '#a5f3fc', '#38bdf8', '#ecfeff'],\n]\n\nexport const CITROUILLE = ['...g....', '.oooooo.', 'ooyooyoo', 'oooooooo', 'oyyyyyyo', '.oooooo.']\nexport const FANTOME = ['.www.', 'wwwww', 'wewew', 'wwwww', 'wwwww', 'w.w.w']\nexport const SAPIN = [\n  '....y....',\n  '...ggg...',\n  '..ggrgg..',\n  '...ggg...',\n  '..gbgggg.',\n  '.gggggyg.',\n  '..ggggg..',\n  '.grggggbg',\n  'ggggygggg',\n  '....t....',\n  '...ttt...',\n]\n\nexport function Citrouille({ x, y }: { x: number; y: number }) {\n  return (\n    <g transform={`translate(${x} ${y})`}>\n      <Pixels grille={CITROUILLE} couleurs={{ o: '#ea580c', y: '#fde047', g: '#15803d' }} />\n      <Anim attributeName=\"opacity\" values=\"1;0.8;1\" dur=\"2.2s\" repeatCount=\"indefinite\" />\n    </g>\n  )\n}\n\n/** Une toile d'araignée dans un coin de pièce, sous le mur du fond. */\nexport function Toile({ x, y, sens = 1 }: { x: number; y: number; sens?: 1 | -1 }) {\n  const d = (n: number) => x + n * sens\n  return (\n    <path\n      d={`M${x} ${y}H${d(26)}M${x} ${y}V${y + 26}M${x} ${y}L${d(20)} ${y + 20}M${d(10)} ${y}Q${d(9)} ${y + 9} ${x} ${y + 10}M${d(20)} ${y}Q${d(17)} ${y + 17} ${x} ${y + 20}`}\n      stroke=\"#e5e7eb\"\n      strokeWidth=\"1\"\n      fill=\"none\"\n      opacity=\"0.6\"\n      shapeRendering=\"auto\"\n    />\n  )\n}\n\nexport function Fetes({ quoi, bas }: { quoi: Fete; bas: number }) {\n  if (quoi === 'halloween') {\n    return (\n      <g pointerEvents=\"none\">\n        <Citrouille x={446} y={bas - 22} />\n        <Citrouille x={532} y={bas - 22} />\n        <Citrouille x={96} y={bas - 62} />\n        <Citrouille x={250} y={60} />\n        <Citrouille x={656} y={176} />\n        <Citrouille x={830} y={118} />\n        <Toile x={20} y={56} />\n        <Toile x={682} y={56} sens={-1} />\n        <Toile x={690} y={56} />\n        <Toile x={980} y={56} sens={-1} />\n        <Toile x={20} y={246} />\n        <Toile x={980} y={246} sens={-1} />\n        {/* Le fantôme qui flotte dans le coin gaming. */}\n        <g opacity=\"0.85\">\n          <Pixels grille={FANTOME} couleurs={{ w: '#f8fafc', e: '#111827' }} />\n          <Anim\n            attributeName=\"transform\"\n            type=\"translate\"\n            values=\"600 120;620 110;600 100;580 110;600 120\"\n            dur=\"6s\"\n            repeatCount=\"indefinite\"\n          />\n        </g>\n      </g>\n    )\n  }\n  if (quoi === 'paques') {\n    const bas2 = bas - 36\n    return (\n      <g pointerEvents=\"none\">\n        {/* Les œufs cachés : peints en pastel, ou en chocolat dans leur papier doré. */}\n        {OEUFS.map(([x, y, base, rayure, points], i) => (\n          <g key={i} transform={`translate(${x} ${y === 'bas' ? bas2 : y})`}>\n            <Pixels grille={OEUF} couleurs={{ e: base, b: rayure, w: points }} u={3} />\n          </g>\n        ))}\n        {/* Le poussin perché sur le tableau blanc du chef. */}\n        <g transform=\"translate(108 92)\">\n          <Pixels grille={POUSSIN} couleurs={{ y: '#fde047', e: '#111827', o: '#fb923c' }} u={3} />\n        </g>\n        {/* Le lapin qui traverse la grande salle, par petits bonds. */}\n        <g>\n          <Anim\n            attributeName=\"transform\"\n            type=\"translate\"\n            values={etapes(-40, LARGEUR + 10, 48)\n              .split(';')\n              .map((x) => `${x} ${bas - 50}`)\n              .join(';')}\n            dur=\"18s\"\n            repeatCount=\"indefinite\"\n          />\n          <g>\n            <Anim attributeName=\"transform\" type=\"translate\" values=\"0 0;0 -7;0 -4;0 0\" dur=\"0.6s\" repeatCount=\"indefinite\" />\n            <Pixels grille={LAPIN} couleurs={{ w: '#f5f5f4', g: '#d6d3d1', e: '#111827', p: '#f9a8d4' }} />\n          </g>\n        </g>\n      </g>\n    )\n  }\n  if (quoi === 'noel') {\n    return (\n      <g pointerEvents=\"none\">\n        {/* Le sapin, ses boules qui clignotent, les cadeaux au pied. */}\n        <g transform={`translate(876 ${bas - 64})`}>\n          <Pixels grille={SAPIN} couleurs={{ g: '#15803d', y: '#fde047', r: '#ef4444', b: '#3b82f6', t: '#78350f' }} u={5} />\n          {[\n            [12, 22, '#f472b6'],\n            [28, 32, '#facc15'],\n            [16, 42, '#22d3ee'],\n          ].map(([cx, cy, c], i) => (\n            <rect key={i} x={cx as number} y={cy as number} width=\"4\" height=\"4\" fill={c as string}>\n              <Anim attributeName=\"opacity\" values=\"1;0.2;1\" dur={`${0.8 + i * 0.3}s`} repeatCount=\"indefinite\" />\n            </rect>\n          ))}\n        </g>\n        {[\n          [856, '#dc2626'],\n          [928, '#2563eb'],\n        ].map(([x, c]) => (\n          <g key={x}>\n            <rect x={x as number} y={bas - 18} width=\"16\" height=\"14\" fill={c as string} />\n            <rect x={(x as number) + 7} y={bas - 18} width=\"2\" height=\"14\" fill=\"#fde047\" />\n            <rect x={x as number} y={bas - 13} width=\"16\" height=\"2\" fill=\"#fde047\" />\n          </g>\n        ))}\n        {/* La guirlande le long du mur de la grande salle. */}\n        {MUR_SALLE.flatMap(([de, a]) =>\n          Array.from({ length: Math.floor((a - de) / 12) }, (_, i) => (\n            <rect\n              key={`${de}-${i}`}\n              x={de + 4 + i * 12}\n              y=\"243\"\n              width=\"3\"\n              height=\"3\"\n              fill={['#ef4444', '#facc15', '#22c55e', '#3b82f6'][i % 4]}\n            >\n              <Anim attributeName=\"opacity\" values=\"1;0.25;1\" dur=\"1.4s\" begin={`${(i % 4) * 0.35}s`} repeatCount=\"indefinite\" />\n            </rect>\n          )),\n        )}\n        {/* Un bonnet sur la baie serveur. */}\n        <rect x=\"40\" y=\"12\" width=\"38\" height=\"8\" fill=\"#dc2626\" />\n        <rect x=\"40\" y=\"18\" width=\"38\" height=\"3\" fill=\"#f8fafc\" />\n        <rect x=\"76\" y=\"8\" width=\"5\" height=\"5\" fill=\"#f8fafc\" />\n      </g>\n    )\n  }\n  return null\n}\n",
      "type": "registry:component",
      "target": "components/pixel-openspace/festivities.tsx"
    },
    {
      "path": "registry/pixel-openspace/people.tsx",
      "content": "'use client'\n\n/*\n * Les postes et les gens : le bureau de chaque agent, les sprites des bonshommes, leurs bulles et leurs étiquettes.\n */\n\nimport { memo } from 'react'\nimport { BUREAU, CHEF_ID, LARGEUR, type PlaceBureau } from './engine'\nimport { Anim, LAMPE_PLAN, Pixels, useTextes } from './primitives'\nimport { type Assise, type Pose, type Vue } from './vue'\nimport { useTheme } from './contexte'\nimport { Lampe, PlanteDeBureau, Poussiere } from './decor'\n\n/* ——— Les postes ——— */\n\nexport function Poste({\n  vue,\n  place,\n  occupe,\n  bientot,\n  monte,\n  oublie,\n  choisi,\n  choisir,\n}: {\n  vue: Vue\n  place: PlaceBureau\n  occupe: boolean\n  bientot: number | null\n  /** Le bureau d'un agent qui arrive : il tombe du plafond et se pose. */\n  monte: boolean\n  /** Un agent qui ne passe plus depuis des jours : plante fanée, écran poussiéreux. */\n  oublie: boolean\n  choisi: boolean\n  choisir: () => void\n}) {\n  const t = useTextes()\n  const theme = useTheme()\n  const { fiche, statut } = vue\n  const { cx, dy } = place\n  const x = cx - BUREAU.largeur / 2\n  const dessus = BUREAU.hauteur - 12\n  const chaise = { x: place.siege.pos.x, y: place.siege.pos.y + 4 }\n  const chef = place.id === CHEF_ID\n  const accent = chef ? '#e3b95a' : theme.tenue(fiche.id).c\n  const allume = statut !== 'absent'\n  const bureau = { cx, dy, x, dessus, statut, accent, bientot, emoji: fiche.emoji }\n\n  const largeurMontage = Math.max(68, t.newDesk.length * 5 + 16)\n  return (\n    <g\n      role=\"button\"\n      tabIndex={0}\n      aria-label={`${fiche.nom}, ${t.statuses[statut]}`}\n      className=\"cursor-pointer outline-none\"\n      style={monte ? { animation: 'montage-bureau 1.4s cubic-bezier(0.2, 0.9, 0.3, 1.2) both' } : undefined}\n      onClick={choisir}\n      onKeyDown={(e) => {\n        if (e.key === 'Enter' || e.key === ' ') {\n          e.preventDefault()\n          choisir()\n        }\n      }}\n    >\n      <title>{`${fiche.nom} — ${t.statuses[statut]}`}</title>\n      {monte ? (\n        // Le cadre suit la longueur du texte, et le texte tient dans le cadre même dans une police de secours plus large.\n        <g>\n          <rect x={cx - largeurMontage / 2} y={dy - 36} width={largeurMontage} height=\"14\" fill=\"#1b1410\" />\n          <rect x={cx - largeurMontage / 2 + 1} y={dy - 35} width={largeurMontage - 2} height=\"12\" fill=\"var(--po-accent)\" />\n          <text\n            x={cx}\n            y={dy - 26}\n            fontSize=\"8\"\n            textAnchor=\"middle\"\n            fill=\"#1b1410\"\n            textLength={largeurMontage - 10}\n            lengthAdjust=\"spacingAndGlyphs\"\n          >\n            {t.newDesk}\n          </text>\n          <Anim attributeName=\"opacity\" values=\"1;0.4;1\" dur=\"0.8s\" repeatCount=\"indefinite\" />\n        </g>\n      ) : null}\n      {choisi ? (\n        <rect\n          x={x - 8}\n          y={dy - 22}\n          width={BUREAU.largeur + 16}\n          height={BUREAU.hauteur + 66}\n          fill=\"rgb(255 255 255 / 0.06)\"\n          stroke=\"var(--po-accent)\"\n          strokeWidth=\"2\"\n          strokeDasharray=\"6 4\"\n        />\n      ) : null}\n\n      {chef ? (\n        <>\n          <theme.Fauteuil x={cx} y={place.siege.pos.y} />\n          <theme.BureauChef {...bureau} />\n          {/* La plaque du chef, en laiton, tournée vers la pièce. */}\n          <rect x={cx - 42} y={dy + dessus + 1} width=\"84\" height=\"9\" fill=\"#b8892f\" />\n          <rect x={cx - 41} y={dy + dessus + 2} width=\"82\" height=\"7\" fill=\"#e3b95a\" />\n          <Lampe x={cx - 37} y={dy + dessus + 3} statut={statut} />\n          <text x={cx + 3} y={dy + dessus + 8.5} fontSize=\"8\" textAnchor=\"middle\" fill=\"#4a3412\">\n            {fiche.nom}\n          </text>\n        </>\n      ) : (\n        <>\n          <theme.Bureau {...bureau} />\n          {/* La tranche porte le nom, avec la lampe. */}\n          <Lampe x={x + 6} y={dy + dessus + 3} statut={statut} />\n          <text x={cx + 4} y={dy + dessus + 8.5} fontSize=\"9\" textAnchor=\"middle\" fill={theme.plaque} opacity={allume ? 1 : 0.5}>\n            {fiche.nom}\n          </text>\n          {oublie ? <Poussiere cx={cx} dy={dy} /> : null}\n          <PlanteDeBureau x={x - 15} y={dy + 30} fanee={oublie} />\n\n          {/* Le siège : l'assise seule quand l'agent y est (son dossier passe devant lui), repoussé sinon. */}\n          {occupe ? (\n            <theme.Assise x={chaise.x} y={chaise.y} couleur={accent} />\n          ) : (\n            <g transform=\"translate(9 3)\">\n              <theme.Assise x={chaise.x} y={chaise.y} couleur={accent} />\n              <theme.Dossier x={chaise.x} y={chaise.y} couleur={accent} />\n            </g>\n          )}\n        </>\n      )}\n    </g>\n  )\n}\n\nexport type Direction = 'haut' | 'bas' | 'gauche' | 'droite'\n\nexport function direction(angle: number): Direction {\n  const a = ((angle % 360) + 360) % 360\n  if (a < 45 || a >= 315) return 'haut'\n  if (a < 135) return 'droite'\n  if (a < 225) return 'bas'\n  return 'gauche'\n}\n\n/* Un bonhomme fait 8 pixels sur 12 : la tête, le sweat à capuche, les jambes. */\nexport const TETE: Record<'haut' | 'bas' | 'droite', Array<string>> = {\n  bas: ['..hhhh..', '.hhhhhh.', '.hssssh.', '.sesses.', '.cssssc.'],\n  haut: ['..hhhh..', '.hhhhhh.', '.hhhhhh.', '.hhhhhh.', '.cchhcc.'],\n  droite: ['..hhhh..', '.hhhhhh.', '.hhhsss.', '.hhhses.', '.ccsss..'],\n}\n/* La même, casque audio sur les oreilles. */\nexport const TETE_CASQUE: Record<'haut' | 'bas' | 'droite', Array<string>> = {\n  bas: ['..kkkk..', '.khhhhk.', 'khsssshk', 'ksessesk', '.cssssc.'],\n  haut: ['..kkkk..', '.khhhhk.', 'khhhhhhk', 'khhhhhhk', '.cchhcc.'],\n  droite: ['..kkkk..', '.hhhkhh.', '.hhkkss.', '.hhkses.', '.ccsss..'],\n}\n/* Le sweat : la poche ventrale et les cordons de capuche (`t`). */\nexport const BUSTE: Record<'haut' | 'bas' | 'droite', Array<string>> = {\n  bas: ['.ctcctc.', 'cctcctcc', 'sccccccs', '.cCCCCc.'],\n  haut: ['.cccccc.', 'cccccccc', 'sccccccs', '.cccccc.'],\n  droite: ['..cccc..', '..cccc..', '..ccsc..', '..cCCc..'],\n}\n/* Les jambes : debout, puis les deux temps du pas. */\nexport const JAMBES: Record<'face' | 'cote', Array<Array<string>>> = {\n  face: [\n    ['.pp..pp.', '.pp..pp.', '.bb..bb.'],\n    ['.pp..pp.', '.pp..bb.', '.bb.....'],\n    ['.pp..pp.', '.bb..pp.', '.....bb.'],\n  ],\n  cote: [\n    ['..pppp..', '..pp.p..', '..bb.bb.'],\n    ['..pppp..', '.pp...p.', '.bb...bb'],\n    ['..pppp..', '...pp...', '...bbb..'],\n  ],\n}\n\n/* Les bras levés de l'agent qui s'étire, vu de dos. */\nexport const ETIREMENT = ['s.hhhh.s', 'chhhhhhc', 'chhhhhhc', 'chhhhhhc', 'c.hhhh.c', '.cccccc.', 'cccccccc', 'cccccccc']\n\nexport function grilleSprite(sens: Direction, temps: number, assise: Assise, etire: boolean, casque: boolean): Array<string> {\n  if (etire && assise === 'bureau' && sens === 'haut') return ETIREMENT\n  const tete = casque ? TETE_CASQUE : TETE\n  // Au bureau ou sur un pouf, de dos devant l'écran ; le chef, lui, trône face à la pièce.\n  if (assise) return sens === 'bas' ? [...tete.bas, ...BUSTE.bas.slice(0, 3)] : [...tete.haut, ...BUSTE.haut.slice(0, 3)]\n  const miroir = sens === 'gauche'\n  const s = miroir ? 'droite' : sens\n  const g = [...tete[s], ...BUSTE[s], ...JAMBES[s === 'droite' ? 'cote' : 'face'][temps]]\n  return miroir ? g.map((l) => [...l].reverse().join('')) : g\n}\n\nexport const CONTOUR = Object.fromEntries([...'hsectCkpb'].map((c) => [c, '#0b0d10']))\n\n/** Le bonhomme, contour sombre compris. Mémorisé : il ne change qu'au pas suivant. */\nexport const Sprite = memo(function Sprite({\n  id,\n  sens,\n  temps,\n  assise,\n  etire,\n}: {\n  id: string\n  sens: Direction\n  temps: number\n  assise: Assise\n  etire: boolean\n}) {\n  const theme = useTheme()\n  const grille = grilleSprite(sens, temps, assise, etire, theme.casque(id))\n  return (\n    <>\n      {[\n        [-1, 0],\n        [1, 0],\n        [0, -1],\n        [0, 1],\n      ].map(([dx, dy]) => (\n        <g key={`${dx}${dy}`} transform={`translate(${dx} ${dy})`}>\n          <Pixels grille={grille} couleurs={CONTOUR} />\n        </g>\n      ))}\n      <Pixels grille={grille} couleurs={theme.tenue(id)} />\n    </>\n  )\n})\n\n/** La hauteur du sprite au-dessus du point où il se tient, et le haut de sa tête. */\nexport function cadrage(pose: Pose) {\n  const rangs = pose.assise ? 8 : 12\n  // Assis, il s'enfonce dans son fauteuil ou son pouf : au bureau, sa tête passe sous la plaque.\n  const bas = pose.assise ? pose.y + 8 : pose.y\n  return { haut: bas - rangs * 3, bas }\n}\n\nexport function Personnage({ id, pose, etire, choisir }: { id: string; pose: Pose; etire: boolean; choisir?: (id: string) => void }) {\n  const theme = useTheme()\n  const temps = pose.marche ? 1 + (Math.floor(pose.pas / 170) % 2) : 0\n  const { haut, bas } = cadrage(pose)\n  return (\n    <g className={choisir ? 'cursor-pointer' : undefined} onClick={choisir ? () => choisir(id) : undefined}>\n      {pose.assise ? null : <ellipse cx={pose.x} cy={bas} rx=\"10\" ry=\"3\" fill=\"black\" opacity=\"0.3\" shapeRendering=\"auto\" />}\n      <g transform={`translate(${Math.round(pose.x) - 12} ${Math.round(haut)})`}>\n        <Sprite id={id} sens={direction(pose.angle)} temps={temps} assise={pose.assise} etire={etire} />\n        {/* Le colis du livreur, porté à bout de bras. */}\n        {pose.porte ? (\n          <g>\n            <rect x=\"3\" y=\"17\" width=\"18\" height=\"12\" fill=\"#b45309\" />\n            <rect x=\"3\" y=\"17\" width=\"18\" height=\"3\" fill=\"#d97706\" />\n            <rect x=\"10\" y=\"17\" width=\"4\" height=\"12\" fill=\"#fde68a\" />\n          </g>\n        ) : !pose.assise && theme.tasse?.(id) ? (\n          /* La tasse, tenue à la main droite. */\n          <g>\n            <rect x=\"18\" y=\"19\" width=\"6\" height=\"7\" fill=\"#0b0d10\" />\n            <rect x=\"19\" y=\"20\" width=\"4\" height=\"5\" fill=\"#fafaf9\" />\n            <rect x=\"24\" y=\"21\" width=\"2\" height=\"3\" fill=\"#0b0d10\" />\n            <rect x=\"19\" y=\"20\" width=\"4\" height=\"1\" fill=\"#6b3a1e\" />\n          </g>\n        ) : null}\n      </g>\n    </g>\n  )\n}\n\n/**\n * Au-dessus de la tête, une chose à la fois : la bulle de celui qui parle,\n * l'icône de celui qui a un souci, ou l'étiquette de celui qui travaille.\n */\nexport function Annonce({ vue, pose, parole }: { vue: Vue; pose: Pose; parole: string | null }) {\n  const t = useTextes()\n  const { statut, fiche } = vue\n  const { haut } = cadrage(pose)\n  const x = Math.round(pose.x)\n\n  if (parole) return <Parole x={x} haut={haut} texte={parole} />\n\n  if (pose.assise === 'bureau' && (statut === 'en-echec' || statut === 'en-retard')) {\n    return (\n      <g transform={`translate(${x + 10} ${haut - 14})`} pointerEvents=\"none\">\n        <rect width=\"16\" height=\"14\" fill=\"#1b1410\" />\n        <rect x=\"1\" y=\"1\" width=\"14\" height=\"12\" fill={statut === 'en-echec' ? '#ef4444' : '#fb923c'} />\n        <text x=\"8\" y=\"11\" fontSize=\"10\" textAnchor=\"middle\" fill=\"white\">\n          {statut === 'en-echec' ? '!' : 'z'}\n        </text>\n        <Anim\n          attributeName=\"transform\"\n          type=\"translate\"\n          values={`${x + 10} ${haut - 14};${x + 10} ${haut - 17};${x + 10} ${haut - 14}`}\n          dur=\"1.4s\"\n          repeatCount=\"indefinite\"\n        />\n      </g>\n    )\n  }\n\n  /* Endormi : la machine dormait, rien n'est cassé. Pas de pancarte d'alerte,\n     des « z » qui montent doucement, dans un indigo de nuit. */\n  if (pose.assise === 'bureau' && statut === 'endormi') {\n    return (\n      <g pointerEvents=\"none\" fill=\"#a5b4fc\" stroke=\"#1e1b4b\" strokeWidth={2} paintOrder=\"stroke\" fontWeight={700}>\n        {[0, 1, 2].map((i) => (\n          /* Visibles d'emblée : la boucle de la salle se fige au calme, et un « z »\n             parti de zéro y resterait. L'animation ne fait que les faire respirer. */\n          <text key={i} x={x + 12 + i * 6} y={haut + 14 - i * 8} fontSize={9 + i * 3} opacity={1 - i * 0.2}>\n            z\n            <Anim attributeName=\"opacity\" values=\"1;0.35;1\" dur=\"3s\" begin={`${i}s`} repeatCount=\"indefinite\" />\n          </text>\n        ))}\n      </g>\n    )\n  }\n\n  if (statut !== 'au-travail') return null\n  const libelle = t.statuses[statut]\n  const largeur = Math.round(Math.max(fiche.nom.length * 5.4, libelle.length * 4.6 + 12) + 12)\n  return (\n    <g transform={`translate(${x - Math.round(largeur / 2)} ${haut - 30})`} pointerEvents=\"none\">\n      <rect width={largeur} height=\"26\" fill=\"#1b1410\" />\n      <rect x=\"1\" y=\"1\" width={largeur - 2} height=\"24\" fill=\"#2b2420\" />\n      <text x=\"6\" y=\"11\" fontSize=\"9\" fill=\"#fafaf9\">\n        {fiche.nom}\n      </text>\n      <rect x=\"6\" y=\"15\" width=\"5\" height=\"5\" fill={LAMPE_PLAN[statut]} />\n      <text x=\"14\" y=\"21\" fontSize=\"8\" fill={LAMPE_PLAN[statut]}>\n        {libelle}\n      </text>\n    </g>\n  )\n}\n\n/** Une bulle de bande dessinée, en pixels ; « … » fait danser trois points. */\nexport function Parole({ x, haut, texte }: { x: number; haut: number; texte: string }) {\n  const points = texte === '…'\n  const court = texte.length > 46 ? `${texte.slice(0, 45)}…` : texte\n  const largeur = points ? 26 : Math.round(court.length * 4.5 + 12)\n  const gauche = Math.min(Math.max(x - largeur / 2, 8), LARGEUR - largeur - 8)\n  const y = haut - 24\n  return (\n    <g pointerEvents=\"none\">\n      <rect x={gauche - 1} y={y - 1} width={largeur + 2} height=\"18\" fill=\"#1b1410\" />\n      <rect x={gauche} y={y} width={largeur} height=\"16\" fill=\"#fafaf9\" />\n      <rect x={x - 4} y={y + 16} width=\"8\" height=\"3\" fill=\"#fafaf9\" />\n      <rect x={x - 1} y={y + 19} width=\"3\" height=\"3\" fill=\"#fafaf9\" />\n      {points ? (\n        [7, 13, 19].map((dx, i) => (\n          <rect key={dx} x={gauche + dx - 1} y={y + 7} width=\"3\" height=\"3\" fill=\"#44403c\">\n            <Anim attributeName=\"opacity\" values=\"0.2;1;0.2\" dur=\"1s\" begin={`${i * 0.2}s`} repeatCount=\"indefinite\" />\n          </rect>\n        ))\n      ) : (\n        <text x={gauche + 6} y={y + 11} fontSize=\"8\" fill=\"#1c1917\">\n          {court}\n        </text>\n      )}\n    </g>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/pixel-openspace/people.tsx"
    },
    {
      "path": "registry/pixel-openspace/sound.ts",
      "content": "/*\n * Le son 8 bits, joué sans fichier : quelques notes carrées.\n */\n\n/** Un son 8 bits, joué à la volée sans fichier : quelques notes carrées. */\nexport function jouerNotes(audio: AudioContext, notes: Array<[number, number]>) {\n  let t = audio.currentTime\n  for (const [frequence, duree] of notes) {\n    const osc = audio.createOscillator()\n    const volume = audio.createGain()\n    osc.type = 'square'\n    osc.frequency.value = frequence\n    volume.gain.setValueAtTime(0.04, t)\n    volume.gain.exponentialRampToValueAtTime(0.0001, t + duree)\n    osc.connect(volume).connect(audio.destination)\n    osc.start(t)\n    osc.stop(t + duree)\n    t += duree\n  }\n}\n\nexport const SONS = {\n  /** Un agent se met au travail : un petit bip montant. */\n  travail: [\n    [660, 0.07],\n    [880, 0.09],\n  ],\n  /** Un agent tombe en échec : deux notes qui descendent. */\n  echec: [\n    [440, 0.18],\n    [262, 0.3],\n  ],\n  /** Un visiteur pousse la porte : ding-dong. */\n  sonnette: [\n    [988, 0.2],\n    [784, 0.35],\n  ],\n} satisfies Record<string, Array<[number, number]>>\n",
      "type": "registry:lib",
      "target": "components/pixel-openspace/sound.ts"
    },
    {
      "path": "registry/pixel-openspace/engine.ts",
      "content": "/**\n * Le plan de l'open space et la vie qui s'y mène : où sont les bureaux, le\n * coin café, le bureau vitré du chef, et comment chacun s'y déplace. Sans\n * React ni DOM, pour que les trajets se testent seuls ; la page dessine ce\n * que `avancer` calcule.\n *\n * Les coordonnées sont celles du `viewBox` SVG. Un angle de 0 regarde vers le\n * haut, 90 vers la droite.\n */\n\nimport type { Columns } from './types'\nimport { TEXTS, type Phrases } from './i18n'\n\n/** An agent's state, as the engine reads it. */\nexport type Statut = 'au-travail' | 'a-jour' | 'en-retard' | 'endormi' | 'en-echec' | 'absent' | 'jamais' | 'a-la-demande'\n\nexport type Point = { x: number; y: number }\n\n/**\n * Un endroit où se tenir, le chemin qui y mène depuis l'allée, où regarder\n * une fois arrivé, et si l'on s'y assoit.\n */\nexport type Lieu = { nom: string; pos: Point; acces: Array<Point>; face: number; assis?: boolean }\n\n/** Où aller, combien de temps y rester, et ce qu'on y dit en arrivant. */\nexport type Etape = { lieu: Lieu; pause: number; dire?: string }\n\nexport const LARGEUR = 1000\n/** Le plateau d'un bureau. */\nexport const BUREAU = { largeur: 120, hauteur: 50 }\n/**\n * L'écart entre deux bureaux voisins, selon le nombre de colonnes. Les rangées\n * restent centrées dans la salle ; à six, le couloir entre deux bureaux se\n * resserre, mais laisse encore passer quelqu'un.\n */\nconst PAS: Record<Columns, number> = { 4: 200, 5: 175, 6: 152 }\nconst HAUT = 300\nconst RANGEE = 170\n/** L'allée du haut, qui longe le bureau du chef et le coin café. */\nconst ALLEE_HAUT = 262\n/** Des unités du plan par seconde : un pas tranquille. */\nconst VITESSE = 85\n\nexport const CHEF_ID = 'chef'\n\nexport type PlaceBureau = { id: string; cx: number; dy: number; siege: Lieu; visite: Lieu }\n\nexport type Plan = {\n  hauteur: number\n  bureaux: Array<PlaceBureau>\n  chef: PlaceBureau\n  /** Les coins où l'on va se dégourdir les jambes. */\n  pauses: Array<Lieu>\n  /** La porte d'entrée, en bas de la grande salle, par où passent visiteurs et livreurs. */\n  entree: Lieu\n  /** Le pied de la pile de cartons, où le livreur dépose son colis. */\n  cartons: Lieu\n  /** Devant le grand écran mural, où le chef fait son rapport. */\n  ecranMural: Lieu\n  /** Ce que disent les gens de la salle, dans la langue choisie. */\n  phrases: Phrases\n  /** Les passages verticaux entre les colonnes de bureaux. */\n  couloirs: Array<number>\n}\n\nconst chaiseY = (dy: number) => dy + BUREAU.hauteur + 22\n\n/**\n * Un coin de pause d'un thème, dans les pièces du haut : où se tenir, où regarder\n * (0 vers le haut, 180 vers le bas), et si l'on s'y assoit. Le premier compte\n * double, comme le second : c'est le café et son voisin.\n */\nexport type CoinDePause = { nom: string; x: number; y: number; face?: number; assis?: boolean }\n\n/* Les coins du thème geek : la cuisine à droite, le coin gaming au milieu. */\nexport const COINS_GEEK: Array<CoinDePause> = [\n  { nom: 'cafe', x: 768, y: 100 },\n  { nom: 'distributeur', x: 881, y: 104 },\n  { nom: 'frigo', x: 950, y: 108 },\n  { nom: 'arcade', x: 372, y: 112 },\n  { nom: 'arcade-2', x: 415, y: 112 },\n  { nom: 'etabli', x: 636, y: 108 },\n  // Sur les poufs, face à la télé : de dos.\n  { nom: 'pouf-gauche', x: 482, y: 146, assis: true },\n  { nom: 'pouf-droite', x: 538, y: 146, assis: true },\n]\n\nexport function plan(\n  ids: Array<string>,\n  phrases: Phrases = TEXTS.en.phrases,\n  coins: Array<CoinDePause> = COINS_GEEK,\n  colonnes: Columns = 5,\n): Plan {\n  const pas = PAS[colonnes] ?? PAS[5]\n  const parRangee = PAS[colonnes] ? colonnes : 5\n  const x0 = LARGEUR / 2 - ((parRangee - 1) * pas) / 2\n  const rangees = Math.max(1, Math.ceil(ids.length / parRangee))\n  const bureaux = ids.map((id, i) => {\n    const cx = x0 + (i % parRangee) * pas\n    const dy = HAUT + Math.floor(i / parRangee) * RANGEE\n    const allee = dy + 108\n    return {\n      id,\n      cx,\n      dy,\n      siege: { nom: `siege:${id}`, pos: { x: cx, y: chaiseY(dy) - 4 }, acces: [{ x: cx, y: allee }], face: 0, assis: true },\n      visite: { nom: `visite:${id}`, pos: { x: cx + 76, y: chaiseY(dy) }, acces: [{ x: cx + 76, y: allee }], face: -90 },\n    }\n  })\n\n  // Le chef trône derrière son bureau, face à la pièce : il en fait le tour par la droite pour s'asseoir.\n  const chef: PlaceBureau = {\n    id: CHEF_ID,\n    cx: 205,\n    dy: 110,\n    siege: {\n      nom: 'siege:chef',\n      pos: { x: 205, y: 102 },\n      acces: [\n        { x: 260, y: ALLEE_HAUT },\n        { x: 260, y: 182 },\n        { x: 300, y: 182 },\n        { x: 300, y: 96 },\n        { x: 205, y: 96 },\n      ],\n      face: 180,\n      assis: true,\n    },\n    visite: {\n      nom: 'visite:chef',\n      pos: { x: 300, y: 140 },\n      acces: [\n        { x: 260, y: ALLEE_HAUT },\n        { x: 260, y: 182 },\n        { x: 300, y: 182 },\n      ],\n      face: -90,\n    },\n  }\n\n  // Les coins de pause viennent du thème : on y entre par le bas, droit devant soi, depuis l'allée du haut.\n  const pauses: Array<Lieu> = coins.map((c) => ({\n    nom: c.nom,\n    pos: { x: c.x, y: c.y },\n    acces: [{ x: c.x, y: ALLEE_HAUT }],\n    face: c.face ?? 0,\n    assis: c.assis ?? false,\n  }))\n\n  const hauteur = HAUT + rangees * RANGEE + 10\n  const derniereAllee = HAUT + (rangees - 1) * RANGEE + 108\n  return {\n    hauteur,\n    bureaux,\n    chef,\n    pauses,\n    phrases,\n    entree: { nom: 'entree', pos: { x: 500, y: hauteur - 26 }, acces: [{ x: 500, y: derniereAllee }], face: 0 },\n    cartons: { nom: 'cartons', pos: { x: 140, y: hauteur - 46 }, acces: [{ x: 140, y: derniereAllee }], face: -90 },\n    ecranMural: { nom: 'ecran-mural', pos: { x: 130, y: ALLEE_HAUT + 4 }, acces: [{ x: 130, y: ALLEE_HAUT }], face: 0 },\n    couloirs: Array.from({ length: parRangee + 1 }, (_, k) => x0 - pas / 2 + k * pas),\n  }\n}\n\n/**\n * Le chemin d'un lieu à un autre : sortir par son accès jusqu'à l'allée,\n * changer d'allée par le couloir le plus proche, puis entrer par l'accès du\n * lieu visé. On ne traverse jamais un bureau.\n */\nexport function trajet(de: Lieu, vers: Lieu, couloirs: Array<number>): Array<Point> {\n  if (de === vers) return []\n  const points = [...de.acces].reverse()\n  const a = points.at(-1) ?? de.pos\n  const b = vers.acces[0] ?? vers.pos\n  if (Math.abs(a.y - b.y) > 1) {\n    const milieu = (a.x + b.x) / 2\n    const x = couloirs.reduce((p, c) => (Math.abs(c - milieu) < Math.abs(p - milieu) ? c : p))\n    points.push({ x, y: a.y }, { x, y: b.y })\n  }\n  points.push(...vers.acces, vers.pos)\n  return points\n}\n\nexport type Marcheur = {\n  id: string\n  pos: Point\n  angle: number\n  lieu: Lieu\n  chemin: Array<Point>\n  /** Le temps à rester où l'on est, en ms. */\n  attente: number\n  programme: Array<Etape>\n  /** Le temps passé à marcher, qui balance les pieds. */\n  pas: number\n  /** Ce qu'il dira en arrivant, puis ce qu'il dit une fois arrivé. */\n  dire: string | null\n  parole: string | null\n  /** Le livreur porte son colis jusqu'aux cartons. */\n  porte: boolean\n  /** Un visiteur ressorti par la porte : la page peut l'oublier. */\n  parti: boolean\n}\n\nexport function marcheur(id: string, siege: Lieu, attente: number): Marcheur {\n  return {\n    id,\n    pos: { ...siege.pos },\n    angle: siege.face,\n    lieu: siege,\n    chemin: [],\n    attente,\n    programme: [],\n    pas: 0,\n    dire: null,\n    parole: null,\n    porte: false,\n    parti: false,\n  }\n}\n\n/** Ceux qui ne font que passer : le client qui apporte sa demande, le livreur et son colis. */\nexport type Invite = 'visiteur' | 'livreur'\n\nexport const estInvite = (id: string) => id.startsWith('visiteur:') || id.startsWith('livreur:')\n\nexport const CHAT_ID = 'chat'\n\n/** Le chat d'atelier, qui commence sa journée roulé sur un siège. */\nexport function chat(p: Plan): Marcheur {\n  // Le dernier siège du thème (un pouf, un banc…), sinon le premier coin venu.\n  const pouf = [...p.pauses].reverse().find((l) => l.assis) ?? p.pauses[0]\n  return marcheur(CHAT_ID, pouf, 6_000)\n}\n\n/** Le dessus d'un bureau, où le chat saute faire la sieste. */\nconst bureauDuChat = (b: PlaceBureau): Lieu => {\n  const allee = b.siege.acces[0].y\n  return { nom: `bureau-chat:${b.id}`, pos: { x: b.cx + 32, y: b.dy + 38 }, acces: [{ x: b.cx + 32, y: allee }], face: 90, assis: true }\n}\n\n/**\n * Le chat va où il veut : faire la sieste sur un bureau dont l'agent s'est\n * levé (ou est absent), traîner au coin café ou sur les poufs, longtemps.\n */\nfunction programmeChat(p: Plan, marcheurs: Array<Marcheur>, statuts: Map<string, Statut>, hasard: () => number): Array<Etape> {\n  const libres = p.bureaux.filter((b) => {\n    const agent = marcheurs.find((m) => m.id === b.id)\n    return statuts.get(b.id) === 'absent' || (agent && agent.lieu !== b.siege)\n  })\n  if (libres.length && hasard() < 0.6) {\n    const b = libres[Math.floor(hasard() * libres.length)]\n    return [{ lieu: bureauDuChat(b), pause: entre(180_000, 360_000, hasard), dire: 'z' }]\n  }\n  const coin = p.pauses[Math.floor(hasard() * p.pauses.length)]\n  return [{ lieu: coin, pause: entre(60_000, 180_000, hasard) }]\n}\n\n/**\n * Un invité entre par la porte : le visiteur va saluer le chef dans son\n * bureau, le livreur dépose son colis au pied des cartons ; puis chacun\n * ressort par où il est venu.\n */\nexport function invite(p: Plan, genre: Invite, numero: number): Marcheur {\n  const m = marcheur(`${genre}:${numero}`, p.entree, 0)\n  m.programme =\n    genre === 'visiteur'\n      ? [\n          { lieu: p.chef.visite, pause: 5_000, dire: p.phrases.visitor },\n          { lieu: p.entree, pause: 0 },\n        ]\n      : [\n          { lieu: p.cartons, pause: 3_000, dire: p.phrases.courier },\n          { lieu: p.entree, pause: 0 },\n        ]\n  m.porte = genre === 'livreur'\n  return m\n}\n\n/** Ceux que leur état cloue au bureau : le travail, ou le souci qui les y retient. */\nexport const auBureau = (s: Statut | undefined) =>\n  s === 'au-travail' || s === 'en-echec' || s === 'en-retard' || s === 'endormi'\n\nfunction partir(m: Marcheur, etape: Etape, couloirs: Array<number>) {\n  m.chemin = trajet(m.lieu, etape.lieu, couloirs)\n  m.lieu = etape.lieu\n  m.attente = etape.pause\n  m.dire = etape.dire ?? null\n  m.parole = null\n  if (!m.chemin.length) arriver(m)\n}\n\nfunction arriver(m: Marcheur) {\n  m.angle = m.lieu.face\n  m.parole = m.dire\n  if (m.lieu.nom === 'cartons') m.porte = false\n}\n\nfunction marcher(m: Marcheur, dt: number) {\n  let reste = (VITESSE * dt) / 1000\n  while (reste > 0 && m.chemin.length) {\n    const cible = m.chemin[0]\n    const dx = cible.x - m.pos.x\n    const dy = cible.y - m.pos.y\n    const d = Math.hypot(dx, dy)\n    if (d > 0.01) m.angle = (Math.atan2(dx, -dy) * 180) / Math.PI\n    if (d <= reste) {\n      m.pos = { ...cible }\n      m.chemin.shift()\n      reste -= d\n    } else {\n      m.pos = { x: m.pos.x + (dx / d) * reste, y: m.pos.y + (dy / d) * reste }\n      reste = 0\n    }\n  }\n  m.pas += dt\n  if (!m.chemin.length) arriver(m)\n}\n\nconst entre = (min: number, max: number, hasard: () => number) => min + hasard() * (max - min)\n\n/** Un agent désœuvré : un tour au café, à la fontaine ou chez un collègue, puis retour au bureau. */\nfunction programmeAgent(p: Plan, place: PlaceBureau, presents: Array<PlaceBureau>, hasard: () => number): Array<Etape> {\n  const collegues = presents.filter((b) => b.id !== place.id).map((b) => b.visite)\n  const collegue = collegues[Math.floor(hasard() * collegues.length)]\n  // Le café et la fontaine comptent double, et l'on passe parfois voir un collègue.\n  const choix = [...p.pauses, ...p.pauses.slice(0, 2), ...(collegue ? [collegue, collegue] : [])]\n  const lieu = choix[Math.floor(hasard() * choix.length)] ?? p.pauses[0]\n  return [\n    // Chez un collègue, on bavarde. Les sorties sont rares et longues : moins de pas, moins à redessiner.\n    { lieu, pause: entre(30_000, 90_000, hasard), dire: lieu === collegue ? '…' : undefined },\n    { lieu: place.siege, pause: entre(180_000, 480_000, hasard) },\n  ]\n}\n\n/**\n * Le chef en ronde commence par ceux qui ont un souci, qu'il relance, puis\n * passe aux autres bureaux. Hors de sa ronde, il reste au chevet d'un agent\n * en échec jusqu'à ce qu'il soit relancé ; sinon il va voir qui est en\n * retard, ou se sert un café.\n */\nfunction programmeChef(\n  p: Plan,\n  statut: Statut | undefined,\n  presents: Array<PlaceBureau>,\n  soucis: Array<PlaceBureau>,\n  echecs: Set<string>,\n  annonces: Array<string>,\n  hasard: () => number,\n): Array<Etape> {\n  const annoncer = annonces.map((a) => ({ lieu: p.ecranMural, pause: 6_000, dire: a }))\n  if (statut === 'au-travail') {\n    const autres = presents.filter((b) => !soucis.includes(b))\n    const rapport = p.phrases.report(autres.length, soucis.length)\n    return [\n      ...soucis.map((b) => ({ lieu: b.visite, pause: 3_500, dire: p.phrases.relaunch })),\n      ...autres.map((b) => ({ lieu: b.visite, pause: 1_500 })),\n      // La ronde finie, il fait son rapport devant le grand écran.\n      { lieu: p.ecranMural, pause: 6_000, dire: rapport },\n      // Puis ce qui ne doit pas être oublié : les annonces qu'on lui a confiées.\n      ...annoncer,\n      { lieu: p.chef.siege, pause: 3_000 },\n    ]\n  }\n  const enEchec = soucis.find((b) => echecs.has(b.id))\n  if (enEchec) return [{ lieu: enEchec.visite, pause: 60_000, dire: p.phrases.stayingHere }]\n  // Une annonce en attente : de temps en temps, il va la redire devant l'écran.\n  if (annoncer.length && hasard() < 0.5) return [...annoncer, { lieu: p.chef.siege, pause: entre(240_000, 600_000, hasard) }]\n  const souci = soucis.length ? soucis[Math.floor(hasard() * soucis.length)] : null\n  return [\n    souci\n      ? { lieu: souci.visite, pause: entre(3_000, 5_000, hasard), dire: p.phrases.anyTrouble }\n      : { lieu: p.pauses[0], pause: entre(3_000, 5_000, hasard) },\n    { lieu: p.chef.siege, pause: entre(240_000, 600_000, hasard) },\n  ]\n}\n\n/** Fait vivre l'open space pendant `dt` millisecondes. */\nexport function avancer(\n  p: Plan,\n  marcheurs: Array<Marcheur>,\n  statuts: Map<string, Statut>,\n  dt: number,\n  hasard: () => number = Math.random,\n  annonces: Array<string> = [],\n  /** La nuit, les désœuvrés regagnent leur bureau et n'en bougent plus ; le chat dort où il est. */\n  nuit = false,\n) {\n  const presents = p.bureaux.filter((b) => statuts.get(b.id) !== 'absent')\n  const soucis = presents.filter((b) => ['en-echec', 'en-retard'].includes(statuts.get(b.id) ?? ''))\n  const echecs = new Set(soucis.filter((b) => statuts.get(b.id) === 'en-echec').map((b) => b.id))\n\n  for (const m of marcheurs) {\n    if (m.id === CHAT_ID) {\n      if (m.chemin.length) marcher(m, dt)\n      else if (!nuit && (m.attente -= dt) <= 0) partir(m, programmeChat(p, marcheurs, statuts, hasard)[0], p.couloirs)\n      continue\n    }\n    if (estInvite(m.id)) {\n      if (m.chemin.length) marcher(m, dt)\n      else if ((m.attente -= dt) > 0) continue\n      else if (m.programme.length) partir(m, m.programme.shift()!, p.couloirs)\n      else m.parti = true\n      continue\n    }\n    const chef = m.id === CHEF_ID\n    const place = chef ? p.chef : p.bureaux.find((b) => b.id === m.id)\n    if (!place) continue\n    const statut = statuts.get(m.id)\n\n    if (m.chemin.length) {\n      marcher(m, dt)\n      continue\n    }\n    if (statut === 'absent') {\n      Object.assign(m, marcheur(m.id, place.siege, 0))\n      continue\n    }\n    const cloue = chef ? statut === 'en-echec' || statut === 'en-retard' : auBureau(statut)\n    if (cloue) {\n      m.programme = []\n      if (m.lieu !== place.siege) partir(m, { lieu: place.siege, pause: 1_000 }, p.couloirs)\n      continue\n    }\n    // Le chef lancé pendant qu'il est assis part en ronde sans finir son café.\n    if (chef && statut === 'au-travail' && m.lieu === place.siege && !m.programme.length) m.attente = Math.min(m.attente, 1_500)\n    // Au chevet d'un agent qui n'est plus en échec, il s'en va ; et un nouvel échec le fait se lever.\n    if (chef && !m.programme.length) {\n      const veille = m.lieu.nom.startsWith('visite:') ? m.lieu.nom.slice('visite:'.length) : null\n      if (veille && m.dire === p.phrases.stayingHere && !echecs.has(veille)) m.attente = Math.min(m.attente, 1_000)\n      if (m.lieu === place.siege && echecs.size && statut !== 'au-travail') m.attente = Math.min(m.attente, 2_000)\n    }\n\n    // La nuit, hors de la ronde du chef, chacun reste ou rentre à son bureau.\n    if (nuit && !(chef && statut === 'au-travail')) {\n      m.programme = []\n      if (m.lieu !== place.siege) partir(m, { lieu: place.siege, pause: 1_000 }, p.couloirs)\n      continue\n    }\n    m.attente -= dt\n    if (m.attente > 0) continue\n    if (!m.programme.length) {\n      m.programme = chef ? programmeChef(p, statut, presents, soucis, echecs, annonces, hasard) : programmeAgent(p, place, presents, hasard)\n    }\n    partir(m, m.programme.shift()!, p.couloirs)\n  }\n}\n",
      "type": "registry:component",
      "target": "components/pixel-openspace/engine.ts"
    },
    {
      "path": "registry/pixel-openspace/arcade.tsx",
      "content": "'use client'\n\nimport { useEffect, useState } from 'react'\nimport { Badge } from '@/components/ui/badge'\nimport { Button } from '@/components/ui/button'\nimport { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'\nimport { Kbd } from '@/components/ui/kbd'\nimport type { Texts } from './i18n'\n\n/** Les jeux des deux bornes du coin gaming. */\nexport type Jeu = 'snake' | 'casse-briques'\n\n/** La toile, carrée, en pixels. */\nconst COTE = 320\n\ntype Partie = {\n  /** Appelé à chaque point marqué, avec le total. */\n  marquer: (points: number) => void\n  /** Appelé quand la partie est perdue. */\n  perdre: () => void\n}\n\n/** Ce qu'une borne sait de son jeu : son nom, ses couleurs, ses commandes, et comment le lancer. */\nconst BORNES: Record<\n  Jeu,\n  { cadre: string; fond: string; accent: string; lancer: (ctx: CanvasRenderingContext2D, p: Partie) => () => void }\n> = {\n  snake: {\n    cadre: 'border-violet-700',\n    fond: 'bg-violet-950',\n    accent: 'text-pink-400',\n    lancer: lancerSnake,\n  },\n  'casse-briques': {\n    cadre: 'border-cyan-600',\n    fond: 'bg-slate-950',\n    accent: 'text-cyan-300',\n    lancer: lancerCasseBriques,\n  },\n}\n\nconst cleRecord = (jeu: Jeu) => `pixel-openspace:record-${jeu}`\n\n/** Le meilleur score de ce jeu dans ce navigateur. */\nexport function lireRecord(jeu: Jeu): number {\n  try {\n    return Number(localStorage.getItem(cleRecord(jeu)) ?? 0)\n  } catch {\n    return 0\n  }\n}\n\n/** L'écran d'une borne au repos, ou après une partie perdue. */\nfunction ecranTitre(ctx: CanvasRenderingContext2D, jeu: Jeu, perdu: boolean, textes: Texts) {\n  ctx.fillStyle = '#0b0d10'\n  ctx.fillRect(0, 0, COTE, COTE)\n  ctx.textAlign = 'center'\n  if (perdu) {\n    ctx.fillStyle = '#ef4444'\n    ctx.font = '22px monospace'\n    ctx.fillText(textes.arcade.gameOver, COTE / 2, COTE / 2 - 16)\n  } else {\n    ctx.fillStyle = jeu === 'snake' ? '#f472b6' : '#67e8f9'\n    ctx.font = '18px monospace'\n    ctx.fillText(textes.arcade.title[jeu === 'snake' ? 'snake' : 'breakout'], COTE / 2, COTE / 2 - 16)\n  }\n  ctx.fillStyle = '#e5e7eb'\n  ctx.font = '12px monospace'\n  ctx.fillText(perdu ? textes.arcade.replay : textes.arcade.pressPlay, COTE / 2, COTE / 2 + 14)\n}\n\n/**\n * Une borne d'arcade du coin gaming. Elle s'allume sur son écran titre et\n * attend qu'on appuie sur JOUER (ou Entrée, ou Espace) ; Échap la ferme. Le\n * record de chaque jeu reste dans ce navigateur.\n */\nexport function JeuArcade({ jeu, textes, fermer }: { jeu: Jeu; textes: Texts; fermer: () => void }) {\n  const titre = textes.arcade.title[jeu === 'snake' ? 'snake' : 'breakout']\n  const aide = textes.arcade.help[jeu === 'snake' ? 'snake' : 'breakout']\n  const borne = BORNES[jeu]\n  const [toile, setToile] = useState<HTMLCanvasElement | null>(null)\n  const [score, setScore] = useState(0)\n  // La borne ne s'ouvre que dans le navigateur, sur un clic : son record s'y lit dès l'ouverture.\n  const [record, setRecord] = useState(() => lireRecord(jeu))\n  const [phase, setPhase] = useState<'titre' | 'jeu' | 'perdu'>('titre')\n  const [partie, setPartie] = useState(0)\n\n  /* Hors partie, l'écran titre ; en partie, le jeu tourne jusqu'à la défaite. */\n  useEffect(() => {\n    const ctx = toile?.getContext('2d')\n    if (!ctx) return\n    if (phase !== 'jeu') {\n      ecranTitre(ctx, jeu, phase === 'perdu', textes)\n      return\n    }\n    let total = 0\n    return borne.lancer(ctx, {\n      marquer: (points) => {\n        total = points\n        setScore(points)\n      },\n      perdre: () => {\n        if (total > lireRecord(jeu)) {\n          try {\n            localStorage.setItem(cleRecord(jeu), String(total))\n          } catch {\n            /* Pas de stockage : le record ne survivra pas, tant pis. */\n          }\n          setRecord(total)\n        }\n        setPhase('perdu')\n      },\n    })\n  }, [phase, partie, jeu, borne, textes, toile])\n\n  const jouer = () => {\n    setScore(0)\n    setPartie((p) => p + 1)\n    setPhase('jeu')\n  }\n\n  useEffect(() => {\n    const touche = (e: KeyboardEvent) => {\n      if ((e.key === 'Enter' || e.key === ' ') && phase !== 'jeu') {\n        e.preventDefault()\n        jouer()\n      }\n    }\n    window.addEventListener('keydown', touche)\n    return () => window.removeEventListener('keydown', touche)\n  })\n\n  return (\n    <Dialog open onOpenChange={(ouverte) => !ouverte && fermer()}>\n      <DialogContent className={`w-auto max-w-none border-4 sm:max-w-none ${borne.cadre} ${borne.fond}`}>\n        <DialogHeader>\n          <DialogTitle className={`font-mono tracking-widest ${borne.accent}`}>{titre}</DialogTitle>\n        </DialogHeader>\n        <canvas ref={setToile} width={COTE} height={COTE} className=\"block rounded border-2 border-black [image-rendering:pixelated]\" />\n        <DialogFooter className=\"flex-row items-center justify-between gap-4 sm:justify-between\">\n          <Badge variant=\"secondary\" className=\"font-mono\">\n            {textes.arcade.score} {String(score).padStart(3, '0')}\n          </Badge>\n          <Button onClick={jouer} disabled={phase === 'jeu'} className=\"font-bold tracking-widest\">\n            {textes.arcade.play}\n          </Button>\n          <Badge variant=\"outline\" className=\"font-mono\">\n            {textes.arcade.record} {String(record).padStart(3, '0')}\n          </Badge>\n        </DialogFooter>\n        <p className=\"text-center text-xs text-muted-foreground\">\n          {aide} · <Kbd>Enter</Kbd> {textes.arcade.toPlay} · <Kbd>Esc</Kbd> {textes.arcade.toClose}\n        </p>\n      </DialogContent>\n    </Dialog>\n  )\n}\n\n/* ——— Le Snake des composants ——— */\n\nconst CASES = 20\nconst CASE = COTE / CASES\n\ntype Case = { x: number; y: number }\n\n/* Ce que le serpent mange : des composants, chacun sa couleur. */\nconst COMPOSANTS = ['#22c55e', '#e3b95a', '#38bdf8', '#a855f7']\n\nconst DIRECTIONS: Record<string, Case> = {\n  ArrowUp: { x: 0, y: -1 },\n  ArrowDown: { x: 0, y: 1 },\n  ArrowLeft: { x: -1, y: 0 },\n  ArrowRight: { x: 1, y: 0 },\n  z: { x: 0, y: -1 },\n  s: { x: 0, y: 1 },\n  q: { x: -1, y: 0 },\n  d: { x: 1, y: 0 },\n}\n\n/** Un serpent en nappe de câbles RGB, qui avale des composants et grandit d'autant. */\nfunction lancerSnake(ctx: CanvasRenderingContext2D, { marquer, perdre }: Partie) {\n  let serpent: Array<Case> = [\n    { x: 9, y: 10 },\n    { x: 8, y: 10 },\n    { x: 7, y: 10 },\n  ]\n  let direction: Case = { x: 1, y: 0 }\n  let prochaine = direction\n  let points = 0\n  const poser = () => {\n    let c: Case\n    do c = { x: Math.floor(Math.random() * CASES), y: Math.floor(Math.random() * CASES) }\n    while (serpent.some((s) => s.x === c.x && s.y === c.y))\n    return { ...c, couleur: COMPOSANTS[Math.floor(Math.random() * COMPOSANTS.length)] }\n  }\n  let miam = poser()\n\n  const dessiner = () => {\n    ctx.fillStyle = '#0b0d10'\n    ctx.fillRect(0, 0, COTE, COTE)\n    ctx.fillStyle = '#111827'\n    for (let i = 0; i < CASES; i++) for (let j = i % 2; j < CASES; j += 2) ctx.fillRect(i * CASE, j * CASE, CASE, CASE)\n    // Le composant à avaler, avec ses pattes de puce.\n    ctx.fillStyle = miam.couleur\n    ctx.fillRect(miam.x * CASE + 3, miam.y * CASE + 3, CASE - 6, CASE - 6)\n    ctx.fillStyle = '#9ca3af'\n    for (let k = 0; k < 3; k++) {\n      ctx.fillRect(miam.x * CASE + 4 + k * 3, miam.y * CASE + 1, 1, 2)\n      ctx.fillRect(miam.x * CASE + 4 + k * 3, miam.y * CASE + CASE - 3, 1, 2)\n    }\n    serpent.forEach((s, i) => {\n      ctx.fillStyle = i === 0 ? '#f8fafc' : `hsl(${(i * 24 + points * 10) % 360} 85% 60%)`\n      ctx.fillRect(s.x * CASE + 1, s.y * CASE + 1, CASE - 2, CASE - 2)\n    })\n  }\n\n  const avancer = () => {\n    direction = prochaine\n    const tete = { x: serpent[0].x + direction.x, y: serpent[0].y + direction.y }\n    const mur = tete.x < 0 || tete.y < 0 || tete.x >= CASES || tete.y >= CASES\n    if (mur || serpent.some((s) => s.x === tete.x && s.y === tete.y)) {\n      clearInterval(t)\n      perdre()\n      return\n    }\n    serpent = [tete, ...serpent]\n    if (tete.x === miam.x && tete.y === miam.y) {\n      points += 1\n      marquer(points)\n      miam = poser()\n    } else {\n      serpent.pop()\n    }\n    dessiner()\n  }\n\n  const touche = (e: KeyboardEvent) => {\n    const d = DIRECTIONS[e.key]\n    if (!d) return\n    e.preventDefault()\n    // Pas de demi-tour sur soi-même.\n    if (d.x !== -direction.x || d.y !== -direction.y) prochaine = d\n  }\n  window.addEventListener('keydown', touche)\n  dessiner()\n  const t = setInterval(avancer, 120)\n  return () => {\n    clearInterval(t)\n    window.removeEventListener('keydown', touche)\n  }\n}\n\n/* ——— Le Casse-puces ——— */\n\nconst COLONNES = 8\nconst RANGEES = 5\nconst BRIQUE = { largeur: 36, hauteur: 12, marge: 3, haut: 36 }\nconst RAQUETTE = { largeur: 56, hauteur: 6, y: COTE - 22 }\n/* Une rangée par famille de puces : GPU, CPU, RAM, SSD, carte mère. */\nconst RANGS = ['#e3b95a', '#22c55e', '#38bdf8', '#a855f7', '#ef4444']\n\n/**\n * Un casse-briques où les briques sont des puces : la raquette renvoie la\n * bille, chaque puce cassée rapporte dix points, et un tableau vidé en remet\n * un, plus rapide. Trois billes par partie.\n */\nfunction lancerCasseBriques(ctx: CanvasRenderingContext2D, { marquer, perdre }: Partie) {\n  const gauche = (COTE - COLONNES * (BRIQUE.largeur + BRIQUE.marge) + BRIQUE.marge) / 2\n  const nouveauTableau = () =>\n    Array.from({ length: RANGEES * COLONNES }, (_, i) => ({\n      x: gauche + (i % COLONNES) * (BRIQUE.largeur + BRIQUE.marge),\n      y: BRIQUE.haut + Math.floor(i / COLONNES) * (BRIQUE.hauteur + BRIQUE.marge),\n      couleur: RANGS[Math.floor(i / COLONNES)],\n      vivante: true,\n    }))\n  let briques = nouveauTableau()\n  let raquette = (COTE - RAQUETTE.largeur) / 2\n  let vitesse = 3.2\n  let bille = { x: COTE / 2, y: RAQUETTE.y - 10, vx: 2, vy: -vitesse }\n  let billes = 3\n  let points = 0\n  const appuyees = new Set<string>()\n\n  const relancer = () => {\n    bille = { x: raquette + RAQUETTE.largeur / 2, y: RAQUETTE.y - 10, vx: (Math.random() < 0.5 ? -1 : 1) * 2, vy: -vitesse }\n  }\n\n  const dessiner = () => {\n    ctx.fillStyle = '#020617'\n    ctx.fillRect(0, 0, COTE, COTE)\n    for (const b of briques) {\n      if (!b.vivante) continue\n      ctx.fillStyle = b.couleur\n      ctx.fillRect(b.x, b.y, BRIQUE.largeur, BRIQUE.hauteur)\n      // Les pattes dorées de la puce.\n      ctx.fillStyle = 'rgb(0 0 0 / 0.35)'\n      ctx.fillRect(b.x + 4, b.y + 3, BRIQUE.largeur - 8, BRIQUE.hauteur - 6)\n    }\n    ctx.fillStyle = '#e5e7eb'\n    ctx.fillRect(raquette, RAQUETTE.y, RAQUETTE.largeur, RAQUETTE.hauteur)\n    ctx.fillStyle = '#22d3ee'\n    ctx.fillRect(raquette, RAQUETTE.y + RAQUETTE.hauteur - 2, RAQUETTE.largeur, 2)\n    ctx.fillStyle = '#f8fafc'\n    ctx.fillRect(bille.x - 3, bille.y - 3, 6, 6)\n    for (let i = 0; i < billes; i++) {\n      ctx.fillStyle = '#f472b6'\n      ctx.fillRect(8 + i * 10, 10, 6, 6)\n    }\n  }\n\n  let image = 0\n  const tour = () => {\n    if (appuyees.has('ArrowLeft') || appuyees.has('q')) raquette -= 6\n    if (appuyees.has('ArrowRight') || appuyees.has('d')) raquette += 6\n    raquette = Math.max(0, Math.min(COTE - RAQUETTE.largeur, raquette))\n\n    bille.x += bille.vx\n    bille.y += bille.vy\n    if (bille.x < 3 || bille.x > COTE - 3) bille.vx *= -1\n    if (bille.y < 3) bille.vy = Math.abs(bille.vy)\n\n    // La raquette renvoie la bille, d'autant plus en biais qu'elle frappe au bord.\n    if (\n      bille.vy > 0 &&\n      bille.y >= RAQUETTE.y - 3 &&\n      bille.y <= RAQUETTE.y + 4 &&\n      bille.x >= raquette &&\n      bille.x <= raquette + RAQUETTE.largeur\n    ) {\n      const decalage = (bille.x - (raquette + RAQUETTE.largeur / 2)) / (RAQUETTE.largeur / 2)\n      bille.vx = decalage * 4\n      bille.vy = -vitesse\n    }\n\n    for (const b of briques) {\n      if (!b.vivante) continue\n      if (bille.x > b.x && bille.x < b.x + BRIQUE.largeur && bille.y > b.y && bille.y < b.y + BRIQUE.hauteur) {\n        b.vivante = false\n        bille.vy *= -1\n        points += 10\n        marquer(points)\n        break\n      }\n    }\n\n    if (briques.every((b) => !b.vivante)) {\n      vitesse += 0.6\n      briques = nouveauTableau()\n      relancer()\n    }\n\n    if (bille.y > COTE) {\n      billes -= 1\n      if (billes === 0) {\n        dessiner()\n        perdre()\n        return\n      }\n      relancer()\n    }\n\n    dessiner()\n    image = requestAnimationFrame(tour)\n  }\n\n  const bas = (e: KeyboardEvent) => {\n    if (['ArrowLeft', 'ArrowRight', 'q', 'd'].includes(e.key)) {\n      e.preventDefault()\n      appuyees.add(e.key)\n    }\n  }\n  const haut = (e: KeyboardEvent) => appuyees.delete(e.key)\n  const souris = (e: MouseEvent) => {\n    const r = ctx.canvas.getBoundingClientRect()\n    raquette = ((e.clientX - r.left) / r.width) * COTE - RAQUETTE.largeur / 2\n  }\n  window.addEventListener('keydown', bas)\n  window.addEventListener('keyup', haut)\n  ctx.canvas.addEventListener('mousemove', souris)\n  image = requestAnimationFrame(tour)\n  return () => {\n    cancelAnimationFrame(image)\n    window.removeEventListener('keydown', bas)\n    window.removeEventListener('keyup', haut)\n    ctx.canvas.removeEventListener('mousemove', souris)\n  }\n}\n",
      "type": "registry:component",
      "target": "components/pixel-openspace/arcade.tsx"
    },
    {
      "path": "registry/pixel-openspace/preferences.ts",
      "content": "/*\n * Ce que le visiteur a réglé derrière la roue dentée, tel qu'il est gardé dans\n * son navigateur. Relu champ par champ : une valeur abîmée ou d'une ancienne\n * version est ignorée plutôt que de faire planter la salle.\n */\n\nimport type { Columns, Language, ThemeName } from './types'\n\n/** Ce que le visiteur a changé ; ce qu'il n'a pas touché suit les props. */\nexport type Preferences = {\n  title?: string\n  /** Le lieu de la météo à la fenêtre, à la place de celle des props. */\n  weatherPlace?: string\n  theme?: ThemeName\n  language?: Language\n  columns?: Columns\n  seasonal?: boolean\n  night?: boolean\n  motion?: boolean\n  timeline?: boolean\n}\n\nexport const THEMES: Array<ThemeName> = ['geek', 'eighties', 'gym', 'modern', 'kitchen', 'neon']\nexport const LANGUES: Array<Language> = ['en', 'fr']\nexport const COLONNES: Array<Columns> = [4, 5, 6]\nexport const LONGUEUR_ENSEIGNE = 24\n\nconst INTERRUPTEURS = ['seasonal', 'night', 'motion', 'timeline'] as const\n\n/** Ne garde que les réglages valides d'un objet venu du stockage. */\nexport function nettoyerPreferences(brut: unknown): Preferences {\n  if (!brut || typeof brut !== 'object' || Array.isArray(brut)) return {}\n  const b = brut as Record<string, unknown>\n  const p: Preferences = {}\n  if (typeof b.title === 'string' && b.title.trim()) p.title = b.title.slice(0, LONGUEUR_ENSEIGNE)\n  if (typeof b.weatherPlace === 'string' && b.weatherPlace.trim()) p.weatherPlace = b.weatherPlace\n  if (THEMES.includes(b.theme as ThemeName)) p.theme = b.theme as ThemeName\n  if (LANGUES.includes(b.language as Language)) p.language = b.language as Language\n  if (COLONNES.includes(b.columns as Columns)) p.columns = b.columns as Columns\n  for (const cle of INTERRUPTEURS) if (typeof b[cle] === 'boolean') p[cle] = b[cle]\n  return p\n}\n\nconst CLE = 'pixel-openspace:settings'\n\nexport function lirePreferences(): Preferences {\n  try {\n    return nettoyerPreferences(JSON.parse(localStorage.getItem(CLE) ?? '{}'))\n  } catch {\n    return {}\n  }\n}\n\nexport function ecrirePreferences(preferences: Preferences) {\n  try {\n    if (Object.keys(preferences).length) localStorage.setItem(CLE, JSON.stringify(preferences))\n    else localStorage.removeItem(CLE)\n  } catch {\n    /* Navigation privée ou stockage bloqué : le réglage vaut pour cette visite. */\n  }\n}\n\n/*\n * Le magasin des préférences : le stockage n'est lu qu'une fois, et toutes les\n * salles ouvertes sont prévenues d'un changement, même dans un autre onglet.\n * Sans React : `navigateur.ts` le branche sur useSyncExternalStore.\n */\nconst AUCUNE: Preferences = {}\nlet enCache: Preferences | null = null\nconst abonnes = new Set<() => void>()\n\nconst prevenir = () => abonnes.forEach((f) => f())\n\n/** Un autre onglet a changé les réglages : on les relira. */\nfunction changeAilleurs(e: StorageEvent) {\n  if (e.key !== CLE) return\n  enCache = null\n  prevenir()\n}\n\nexport function preferencesActuelles(): Preferences {\n  return (enCache ??= lirePreferences())\n}\n\n/** Au rendu serveur et pendant l'hydratation : rien n'est encore réglé. */\nexport const preferencesServeur = (): Preferences => AUCUNE\n\nexport function abonnerPreferences(f: () => void) {\n  if (!abonnes.size) window.addEventListener('storage', changeAilleurs)\n  abonnes.add(f)\n  return () => {\n    abonnes.delete(f)\n    if (!abonnes.size) window.removeEventListener('storage', changeAilleurs)\n  }\n}\n\n/** Change quelques réglages ; `null` les efface tous, et les props reprennent la main. */\nexport function changerPreferences(modif: Preferences | null) {\n  const apres = modif === null ? {} : nettoyerPreferences({ ...preferencesActuelles(), ...modif })\n  enCache = apres\n  ecrirePreferences(apres)\n  prevenir()\n}\n",
      "type": "registry:lib",
      "target": "components/pixel-openspace/preferences.ts"
    },
    {
      "path": "registry/pixel-openspace/navigateur.ts",
      "content": "'use client'\n\n/*\n * Ce que seul le navigateur connaît, lu sans décalage avec le rendu serveur :\n * useSyncExternalStore donne la valeur du serveur pendant l'hydratation, puis\n * celle du navigateur, sans état recopié dans un effet.\n */\n\nimport { useSyncExternalStore } from 'react'\nimport { abonnerPreferences, preferencesActuelles, preferencesServeur } from './preferences'\n\nconst sansAbonnement = () => () => {}\n\n/** Vrai dans le navigateur ; faux au rendu serveur et pendant l'hydratation. */\nexport function useDansLeNavigateur() {\n  return useSyncExternalStore(\n    sansAbonnement,\n    () => true,\n    () => false,\n  )\n}\n\n/** Les réglages du visiteur, à jour dans toutes les salles ouvertes. */\nexport function usePreferences() {\n  return useSyncExternalStore(abonnerPreferences, preferencesActuelles, preferencesServeur)\n}\n",
      "type": "registry:lib",
      "target": "components/pixel-openspace/navigateur.ts"
    },
    {
      "path": "registry/pixel-openspace/settings.tsx",
      "content": "'use client'\n\n/*\n * La fenêtre des réglages, derrière la roue dentée : le thème, la langue, le\n * son, les alertes, les saisons, la nuit et les déplacements. Ce que le\n * visiteur choisit est gardé dans son navigateur et passe avant les props.\n */\n\nimport { SettingsIcon } from 'lucide-react'\nimport { useEffect, useId, useState, type ReactNode } from 'react'\nimport { Button } from '@/components/ui/button'\nimport { ButtonGroup } from '@/components/ui/button-group'\nimport { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'\nimport { Input } from '@/components/ui/input'\nimport { Label } from '@/components/ui/label'\nimport { Separator } from '@/components/ui/separator'\nimport { Switch } from '@/components/ui/switch'\nimport { COLONNES, LONGUEUR_ENSEIGNE, THEMES, type Preferences } from './preferences'\nimport { useTextes } from './primitives'\nimport type { Columns, Language, ThemeName, Weather } from './types'\nimport { lireMeteo, localiser, type Endroit } from './weather'\n\n/** Où en est la recherche du lieu choisi dans les réglages. */\nexport type EtatMeteo = { etat: 'aucun' } | { etat: 'recherche' } | { etat: 'introuvable' } | { etat: 'trouve'; meteo: Weather }\n\n/**\n * La météo du lieu tapé dans les réglages : cherchée une fois la frappe\n * finie, puis relue toutes les quinze minutes.\n */\nexport function useMeteoDuLieu(lieu: string | undefined, langue: Language): EtatMeteo {\n  const cherche = lieu?.trim()\n  // Le dernier résultat, avec le lieu qu'il concerne : un résultat pour un autre lieu ne compte plus.\n  const [resultat, setResultat] = useState<{ lieu: string; etat: EtatMeteo } | null>(null)\n  useEffect(() => {\n    if (!cherche) return\n    let fini = false\n    let endroit: Endroit | null = null\n    const lire = async () => {\n      try {\n        endroit ??= await localiser(cherche, langue)\n        const meteo = { ...(await lireMeteo(endroit)), place: endroit.nom ?? cherche }\n        if (!fini) setResultat({ lieu: cherche, etat: { etat: 'trouve', meteo } })\n      } catch {\n        if (!fini && !endroit) setResultat({ lieu: cherche, etat: { etat: 'introuvable' } })\n      }\n    }\n    // Une recherche par lieu, pas une par lettre tapée.\n    const attente = setTimeout(lire, 700)\n    const minuterie = setInterval(lire, 15 * 60_000)\n    return () => {\n      fini = true\n      clearTimeout(attente)\n      clearInterval(minuterie)\n    }\n  }, [cherche, langue])\n  if (!cherche) return { etat: 'aucun' }\n  return resultat?.lieu === cherche ? resultat.etat : { etat: 'recherche' }\n}\n\nconst NOMS_LANGUES: Array<[Language, string]> = [\n  ['en', 'English'],\n  ['fr', 'Français'],\n]\n\n/** Un réglage à interrupteur : son nom, une ligne d'explication, et l'interrupteur à droite. */\nfunction Interrupteur({\n  nom,\n  aide,\n  actif,\n  changer,\n  desactive,\n}: {\n  nom: string\n  aide: ReactNode\n  actif: boolean\n  changer: (actif: boolean) => void\n  desactive?: boolean\n}) {\n  const id = useId()\n  return (\n    <div className=\"flex items-start justify-between gap-4\">\n      <div className=\"flex flex-col gap-1\">\n        <Label htmlFor={id}>{nom}</Label>\n        <p className=\"text-sm text-muted-foreground\">{aide}</p>\n      </div>\n      <Switch id={id} checked={actif} onCheckedChange={changer} disabled={desactive} className=\"mt-0.5\" />\n    </div>\n  )\n}\n\n/** Un choix parmi quelques boutons, celui retenu en avant. */\nfunction Choix<T extends string>({\n  nom,\n  options,\n  valeur,\n  changer,\n}: {\n  nom: string\n  options: Array<[T, string]>\n  valeur: T\n  changer: (v: T) => void\n}) {\n  return (\n    <div className=\"flex flex-col gap-2\">\n      <Label>{nom}</Label>\n      <ButtonGroup aria-label={nom} className=\"flex-wrap\">\n        {options.map(([v, libelle]) => (\n          <Button key={v} size=\"sm\" variant={valeur === v ? 'default' : 'outline'} aria-pressed={valeur === v} onClick={() => changer(v)}>\n            {libelle}\n          </Button>\n        ))}\n      </ButtonGroup>\n    </div>\n  )\n}\n\nexport function Reglages({\n  titre,\n  titreOrigine,\n  lieu,\n  lieuOrigine,\n  etatMeteo,\n  theme,\n  colonnes,\n  langue,\n  saisons,\n  nuit,\n  mouvement,\n  frise,\n  son,\n  alertes,\n  alertesBloquees,\n  changer,\n  basculerSon,\n  basculerAlertes,\n  reinitialiser,\n}: {\n  /** L'enseigne choisie par le visiteur, vide s'il garde celle des props. */\n  titre: string\n  titreOrigine: string\n  /** Le lieu tapé par le visiteur, vide s'il garde la météo des props. */\n  lieu: string\n  lieuOrigine?: string\n  etatMeteo: EtatMeteo\n  theme: ThemeName\n  colonnes: Columns\n  langue: Language\n  saisons: boolean\n  nuit: boolean\n  mouvement: boolean\n  frise: boolean\n  son: boolean\n  alertes: boolean\n  alertesBloquees: boolean\n  changer: (preferences: Preferences) => void\n  basculerSon: () => void\n  basculerAlertes: () => void\n  reinitialiser: () => void\n}) {\n  const t = useTextes().settings\n  // Ouverte et fermée à la main, sans `asChild` ni `render` : la même fenêtre marche avec les styles shadcn Radix et Base UI.\n  const [ouverte, setOuverte] = useState(false)\n  const idTitre = useId()\n  const idLieu = useId()\n  const aideMeteo =\n    etatMeteo.etat === 'recherche'\n      ? t.weatherSearching\n      : etatMeteo.etat === 'introuvable'\n        ? t.weatherNotFound\n        : etatMeteo.etat === 'trouve'\n          ? t.weatherFound(etatMeteo.meteo.place ?? lieu, etatMeteo.meteo.temperature)\n          : null\n  return (\n    <Dialog open={ouverte} onOpenChange={setOuverte}>\n      <Button variant=\"outline\" size=\"icon-sm\" aria-label={t.open} title={t.open} onClick={() => setOuverte(true)}>\n        <SettingsIcon />\n      </Button>\n      <DialogContent className=\"max-h-[90svh] overflow-y-auto sm:max-w-md\">\n        <DialogHeader>\n          <DialogTitle>{t.title}</DialogTitle>\n          <DialogDescription>{t.description}</DialogDescription>\n        </DialogHeader>\n        <div className=\"flex flex-col gap-5\">\n          <div className=\"flex flex-col gap-2\">\n            <Label htmlFor={idTitre}>{t.sign}</Label>\n            <Input\n              id={idTitre}\n              value={titre}\n              placeholder={titreOrigine}\n              maxLength={LONGUEUR_ENSEIGNE}\n              onChange={(e) => changer({ title: e.target.value || undefined })}\n            />\n          </div>\n          <div className=\"flex flex-col gap-2\">\n            <Label htmlFor={idLieu}>{t.weather}</Label>\n            <Input\n              id={idLieu}\n              value={lieu}\n              placeholder={lieuOrigine ?? t.weatherPlaceholder}\n              onChange={(e) => changer({ weatherPlace: e.target.value || undefined })}\n            />\n            {aideMeteo ? (\n              <p className={`text-sm ${etatMeteo.etat === 'introuvable' ? 'text-destructive' : 'text-muted-foreground'}`}>{aideMeteo}</p>\n            ) : null}\n          </div>\n          <Choix nom={t.theme} options={THEMES.map((n) => [n, t.themes[n]])} valeur={theme} changer={(v) => changer({ theme: v })} />\n          <Choix\n            nom={t.columns}\n            options={COLONNES.map((n) => [String(n) as `${Columns}`, String(n)])}\n            valeur={String(colonnes) as `${Columns}`}\n            changer={(v) => changer({ columns: Number(v) as Columns })}\n          />\n          <Choix nom={t.language} options={NOMS_LANGUES} valeur={langue} changer={(v) => changer({ language: v })} />\n          <Separator />\n          <Interrupteur nom={t.sound} aide={t.soundHint} actif={son} changer={basculerSon} />\n          <Interrupteur\n            nom={t.alerts}\n            aide={alertesBloquees ? t.alertsBlocked : t.alertsHint}\n            actif={alertes}\n            changer={basculerAlertes}\n            desactive={alertesBloquees}\n          />\n          <Separator />\n          <Interrupteur nom={t.seasonal} aide={t.seasonalHint} actif={saisons} changer={(v) => changer({ seasonal: v })} />\n          <Interrupteur nom={t.night} aide={t.nightHint} actif={nuit} changer={(v) => changer({ night: v })} />\n          <Interrupteur nom={t.motion} aide={t.motionHint} actif={mouvement} changer={(v) => changer({ motion: v })} />\n          <Interrupteur nom={t.timeline} aide={t.timelineHint} actif={frise} changer={(v) => changer({ timeline: v })} />\n        </div>\n        <DialogFooter>\n          <Button variant=\"ghost\" onClick={reinitialiser}>\n            {t.reset}\n          </Button>\n          <Button onClick={() => setOuverte(false)}>{t.done}</Button>\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/pixel-openspace/settings.tsx"
    },
    {
      "path": "registry/pixel-openspace/timeline.tsx",
      "content": "'use client'\n\n/*\n * La journée en frise, sous la salle : une ligne par agent, de minuit à\n * minuit, un point par passage (vert s'il s'est bien passé, rouge sinon), et\n * le trait de l'heure qu'il est. De quoi voir d'un coup d'œil qui a sauté un\n * passage cette nuit.\n */\n\nimport { Card, CardAction, CardContent, CardHeader, CardTitle } from '@/components/ui/card'\nimport { useTextes } from './primitives'\nimport type { Agent } from './types'\n\nconst JOUR_MS = 86_400_000\nconst HEURES = [0, 3, 6, 9, 12, 15, 18, 21, 24]\n\nexport function Frise({ agents, maintenant, choisir }: { agents: Array<Agent>; maintenant: number; choisir: (id: string) => void }) {\n  const t = useTextes()\n  const minuit = new Date(maintenant)\n  minuit.setHours(0, 0, 0, 0)\n  const debut = minuit.getTime()\n  const position = (ms: number) => `${Math.min(100, Math.max(0, ((ms - debut) / JOUR_MS) * 100))}%`\n  // Seuls les passages d'aujourd'hui entrent dans la frise.\n  const lignes = agents.map((a) => ({ agent: a, passages: (a.runs ?? []).filter((r) => Date.parse(r.at) >= debut) }))\n  const total = lignes.reduce((n, l) => n + l.passages.length, 0)\n  const rates = lignes.reduce((n, l) => n + l.passages.filter((r) => r.ok === false).length, 0)\n  const heure = (iso: string) => new Date(iso).toLocaleTimeString(t.locale, { hour: '2-digit', minute: '2-digit' })\n\n  return (\n    <Card>\n      <CardHeader>\n        <CardTitle>{t.timeline.title}</CardTitle>\n        <CardAction className=\"text-xs text-muted-foreground\">\n          {t.timeline.runs(total)}\n          {rates ? <span className=\"text-destructive\"> · {t.timeline.failed(rates)}</span> : null}\n        </CardAction>\n      </CardHeader>\n      <CardContent>\n        <div className=\"grid grid-cols-[minmax(6rem,9rem)_1fr] gap-x-3 gap-y-1.5 text-xs\">\n          {/* Les heures. */}\n          <span />\n          <div className=\"relative h-4 text-[0.65rem] text-muted-foreground\">\n            {HEURES.map((h) => (\n              <span\n                key={h}\n                className={`absolute whitespace-nowrap ${h === 0 ? '' : h === 24 ? '-translate-x-full' : '-translate-x-1/2'}`}\n                style={{ left: `${(h / 24) * 100}%` }}\n              >\n                {t.timeline.hour(h)}\n              </span>\n            ))}\n          </div>\n\n          {lignes.map(({ agent, passages }) => (\n            <div key={agent.id} className=\"contents\">\n              <button type=\"button\" onClick={() => choisir(agent.id)} className=\"truncate text-left hover:text-primary\">\n                {agent.emoji ? <span aria-hidden>{agent.emoji} </span> : null}\n                {agent.name}\n              </button>\n              <div className=\"relative h-5 rounded bg-muted/50\">\n                {[6, 12, 18].map((h) => (\n                  <span key={h} className=\"absolute inset-y-0 w-px bg-border\" style={{ left: `${(h / 24) * 100}%` }} />\n                ))}\n                {passages.map((r, i) => (\n                  <span\n                    key={`${r.at}-${i}`}\n                    title={`${heure(r.at)} · ${r.message || t.timeline.run}`}\n                    className={`absolute top-1/2 size-2.5 -translate-x-1/2 -translate-y-1/2 rounded-full ring-2 ring-background ${\n                      r.ok === false ? 'bg-red-500' : 'bg-emerald-500'\n                    }`}\n                    style={{ left: position(Date.parse(r.at)) }}\n                  />\n                ))}\n                <span className=\"absolute inset-y-[-2px] w-0.5 bg-primary\" style={{ left: position(maintenant) }} />\n              </div>\n            </div>\n          ))}\n        </div>\n      </CardContent>\n    </Card>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/pixel-openspace/timeline.tsx"
    },
    {
      "path": "registry/pixel-openspace/weather.ts",
      "content": "/*\n * La vraie météo d'un lieu, par Open-Meteo : gratuit, sans clé, et ouvert aux\n * navigateurs (CORS). Le lieu est une ville (« Lanton », « Lyon, France ») ou\n * des coordonnées (« 44.70,-1.04 »). Sans React : le serveur local s'en sert aussi.\n */\n\nimport type { Language, Sky, Weather } from './types'\n\n/** Les codes météo de l'OMS, ramenés à ce qu'une fenêtre en pixels sait dessiner. */\nexport function cielDe(code: number): Sky {\n  if (code === 0 || code === 1) return 'clear'\n  if (code === 2 || code === 3) return 'clouds'\n  if (code === 45 || code === 48) return 'fog'\n  if ((code >= 71 && code <= 77) || code === 85 || code === 86) return 'snow'\n  if (code >= 95) return 'storm'\n  return 'rain'\n}\n\n/** « 44.70,-1.04 » : des coordonnées toutes prêtes, sans passer par la recherche de ville. */\nexport function coordonnees(lieu: string): { latitude: number; longitude: number } | null {\n  const m = lieu.trim().match(/^(-?\\d+(?:\\.\\d+)?)\\s*,\\s*(-?\\d+(?:\\.\\d+)?)$/)\n  if (!m) return null\n  const [latitude, longitude] = [Number(m[1]), Number(m[2])]\n  return Math.abs(latitude) <= 90 && Math.abs(longitude) <= 180 ? { latitude, longitude } : null\n}\n\nexport type Endroit = { latitude: number; longitude: number; nom?: string }\n\n/** Trouve une ville ; « Lyon, France » garde le premier mot pour la recherche. */\nexport async function localiser(lieu: string, langue: Language): Promise<Endroit> {\n  const direct = coordonnees(lieu)\n  if (direct) return direct\n  const nom = lieu.split(',')[0].trim()\n  const url = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(nom)}&count=1&language=${langue}&format=json`\n  const reponse = await fetch(url, { signal: AbortSignal.timeout(5000) })\n  const { results } = (await reponse.json()) as { results?: Array<{ latitude: number; longitude: number; name: string }> }\n  if (!results?.length) throw new Error(`place not found: ${lieu}`)\n  return { latitude: results[0].latitude, longitude: results[0].longitude, nom: results[0].name }\n}\n\nexport async function lireMeteo(endroit: Endroit): Promise<Omit<Weather, 'place'>> {\n  const url =\n    `https://api.open-meteo.com/v1/forecast?latitude=${endroit.latitude}&longitude=${endroit.longitude}` +\n    '&current=temperature_2m,weather_code,is_day&timezone=auto'\n  const reponse = await fetch(url, { signal: AbortSignal.timeout(5000) })\n  if (!reponse.ok) throw new Error(`weather: HTTP ${reponse.status}`)\n  const { current } = (await reponse.json()) as { current: { temperature_2m: number; weather_code: number; is_day: number } }\n  return { temperature: Math.round(current.temperature_2m), sky: cielDe(current.weather_code), day: current.is_day === 1 }\n}\n",
      "type": "registry:lib",
      "target": "components/pixel-openspace/weather.ts"
    },
    {
      "path": "registry/pixel-openspace/i18n.ts",
      "content": "import type { Language, ThemeName } from './types'\n\n/** Every word the open space writes or says, in English and in French. */\nexport type Texts = {\n  locale: string\n  statuses: Record<'au-travail' | 'a-jour' | 'en-retard' | 'endormi' | 'en-echec' | 'absent' | 'jamais' | 'a-la-demande', string>\n  leadName: string\n  leadRole: string\n  leadOffice: string\n  nextRuns: string\n  nothingPlanned: string\n  hiScores: string\n  welcome: string\n  newDesk: string\n  inMinutes: (n: number) => string\n  offNote: string\n  justNow: string\n  minutesAgo: (n: number) => string\n  hoursAgo: (n: number) => string\n  daysAgo: (n: number) => string\n  never: string\n  lastRun: string\n  nextRun: string\n  runNow: string\n  running: string\n  launching: string\n  close: string\n  fullscreen: string\n  /** La frise de la journée, sous la salle. */\n  timeline: {\n    title: string\n    run: string\n    runs: (n: number) => string\n    failed: (n: number) => string\n    hour: (h: number) => string\n  }\n  /** La fenêtre des réglages, derrière la roue dentée. */\n  settings: {\n    open: string\n    title: string\n    description: string\n    sign: string\n    weather: string\n    weatherPlaceholder: string\n    weatherSearching: string\n    weatherNotFound: string\n    weatherFound: (place: string, temperature: number) => string\n    theme: string\n    themes: Record<ThemeName, string>\n    columns: string\n    language: string\n    sound: string\n    soundHint: string\n    alerts: string\n    alertsHint: string\n    alertsBlocked: string\n    seasonal: string\n    seasonalHint: string\n    night: string\n    nightHint: string\n    motion: string\n    motionHint: string\n    timeline: string\n    timelineHint: string\n    reset: string\n    done: string\n  }\n  sceneLabel: string\n  /** Les tuiles de l'écran mural quand on ne lui en donne pas. */\n  wall: { jobs: string; running: string; failed: string; late: string }\n  phrases: Phrases\n  objects: Record<\n    'workbench' | 'tv' | 'server-rack' | 'vending-machine' | 'fridge' | 'cpu-box' | 'gpu-box' | 'ram-box' | 'snake' | 'breakout',\n    string\n  >\n  arcade: {\n    play: string\n    score: string\n    record: string\n    gameOver: string\n    pressPlay: string\n    replay: string\n    help: Record<'snake' | 'breakout', string>\n    keys: string\n    toPlay: string\n    toClose: string\n    title: Record<'snake' | 'breakout', string>\n  }\n  catNap: string\n  noWeather: string\n}\n\n/** What the people in the room say. */\nexport type Phrases = {\n  relaunch: string\n  anyTrouble: string\n  stayingHere: string\n  report: (upToDate: number, relaunched: number) => string\n  visitor: string\n  courier: string\n}\n\nconst EN: Texts = {\n  locale: 'en-GB',\n  statuses: {\n    'au-travail': 'working',\n    'a-jour': 'up to date',\n    'en-retard': 'late',\n    endormi: 'asleep',\n    'en-echec': 'failed',\n    absent: 'off',\n    jamais: 'never ran',\n    'a-la-demande': 'on demand',\n  },\n  leadName: 'The lead',\n  leadRole: 'Walks around the desks and relaunches whoever missed their run',\n  leadOffice: 'LEAD OFFICE',\n  nextRuns: 'NEXT RUNS',\n  nothingPlanned: 'nothing planned',\n  hiScores: 'HI-SCORES',\n  welcome: 'WELCOME',\n  newDesk: 'New desk!',\n  inMinutes: (n) => `in ${n} min`,\n  offNote: 'away',\n  justNow: 'just now',\n  minutesAgo: (n) => `${n} min ago`,\n  hoursAgo: (n) => `${n} h ago`,\n  daysAgo: (n) => `${n} d ago`,\n  never: 'never',\n  lastRun: 'last',\n  nextRun: 'next',\n  runNow: 'Run now',\n  running: 'Running',\n  launching: 'Launching…',\n  close: 'Close',\n  fullscreen: '⛶ Fullscreen',\n  timeline: {\n    title: 'Today',\n    run: 'run',\n    runs: (n) => `${n} run${n === 1 ? '' : 's'} since midnight`,\n    failed: (n) => `${n} failed`,\n    hour: (h) => (h === 0 || h === 24 ? '12am' : h === 12 ? '12pm' : h < 12 ? `${h}am` : `${h - 12}pm`),\n  },\n  settings: {\n    open: 'Settings',\n    title: 'Settings',\n    description: 'Saved in this browser only.',\n    sign: 'Sign on the wall',\n    weather: 'Weather at the window',\n    weatherPlaceholder: 'A city, or latitude,longitude',\n    weatherSearching: 'Looking for it…',\n    weatherNotFound: 'Place not found.',\n    weatherFound: (place, temperature) => `${place}: ${temperature} °C right now, from Open-Meteo.`,\n    theme: 'Theme',\n    themes: { geek: 'Geek', eighties: '1980s', gym: 'Gym', modern: 'Modern', kitchen: 'Kitchen', neon: 'Neon city' },\n    columns: 'Desks per row',\n    language: 'Language',\n    sound: '8-bit sound',\n    soundHint: 'A jingle when a job starts, fails, or a visitor comes in.',\n    alerts: 'Browser alerts',\n    alertsHint: 'A notification when a job fails or falls behind.',\n    alertsBlocked: 'Notifications are blocked for this site in your browser settings.',\n    seasonal: 'Seasonal decorations',\n    seasonalHint: 'Halloween in October, Christmas in December, Easter in spring.',\n    night: 'Darken at night',\n    nightHint: 'The room dims in the evening and the screens glow.',\n    motion: 'People walk around',\n    motionHint: 'Off: everybody stays at their desk, which saves CPU.',\n    timeline: 'Day timeline',\n    timelineHint: 'Under the room, one dot per run since midnight.',\n    reset: 'Reset',\n    done: 'Done',\n  },\n  sceneLabel: 'The agents’ open space',\n  wall: { jobs: 'Jobs', running: 'Running', failed: 'Failed', late: 'Late' },\n  phrases: {\n    relaunch: 'Relaunching you!',\n    anyTrouble: 'Any trouble?',\n    stayingHere: 'I’m staying here.',\n    report: (ok, n) => (n ? `Report: ${ok} up to date, ${n} relaunched.` : 'Report: everybody is up to date.'),\n    visitor: 'Hello! I have a request.',\n    courier: 'Parcel delivered!',\n  },\n  objects: {\n    workbench: 'The workbench',\n    tv: 'The TV',\n    'server-rack': 'The server rack',\n    'vending-machine': 'The vending machine',\n    fridge: 'The fridge',\n    'cpu-box': 'The CPU box',\n    'gpu-box': 'The GPU box',\n    'ram-box': 'The RAM box',\n    snake: 'The Snake cabinet: a game?',\n    breakout: 'The Chip Breaker cabinet: a game?',\n  },\n  arcade: {\n    play: 'PLAY',\n    score: 'SCORE',\n    record: 'RECORD',\n    gameOver: 'GAME OVER',\n    pressPlay: 'Press PLAY',\n    replay: 'Press PLAY to replay',\n    help: { snake: 'Arrows or W A S D', breakout: 'Arrows, A D or the mouse' },\n    keys: 'Enter to play · Esc to close',\n    toPlay: 'to play',\n    toClose: 'to close',\n    title: { snake: 'COMPONENT SNAKE', breakout: 'CHIP BREAKER' },\n  },\n  catNap: 'The office cat is napping',\n  noWeather: 'No weather',\n}\n\nconst FR: Texts = {\n  locale: 'fr-FR',\n  statuses: {\n    'au-travail': 'au travail',\n    'a-jour': 'à jour',\n    'en-retard': 'en retard',\n    endormi: 'endormi',\n    'en-echec': 'en échec',\n    absent: 'absent',\n    jamais: 'jamais passé',\n    'a-la-demande': 'à la demande',\n  },\n  leadName: 'Le chef',\n  leadRole: 'Fait le tour des bureaux et relance qui a manqué son passage',\n  leadOffice: 'BUREAU DU CHEF',\n  nextRuns: 'PROCHAINS PASSAGES',\n  nothingPlanned: 'rien de prévu',\n  hiScores: 'HI-SCORES',\n  welcome: 'BIENVENUE',\n  newDesk: 'Nouveau poste !',\n  inMinutes: (n) => `dans ${n} min`,\n  offNote: 'absent',\n  justNow: 'à l’instant',\n  minutesAgo: (n) => `il y a ${n} min`,\n  hoursAgo: (n) => `il y a ${n} h`,\n  daysAgo: (n) => `il y a ${n} j`,\n  never: 'jamais',\n  lastRun: 'passé',\n  nextRun: 'prochain',\n  runNow: 'Lancer maintenant',\n  running: 'Au travail',\n  launching: 'Lancement…',\n  close: 'Fermer',\n  fullscreen: '⛶ Plein écran',\n  timeline: {\n    title: 'La journée',\n    run: 'passage',\n    runs: (n) => `${n} passage${n > 1 ? 's' : ''} depuis minuit`,\n    failed: (n) => `${n} raté${n > 1 ? 's' : ''}`,\n    hour: (h) => `${h} h`,\n  },\n  settings: {\n    open: 'Réglages',\n    title: 'Réglages',\n    description: 'Enregistrés dans ce navigateur seulement.',\n    sign: 'Enseigne au mur',\n    weather: 'Météo à la fenêtre',\n    weatherPlaceholder: 'Une ville, ou latitude,longitude',\n    weatherSearching: 'Recherche…',\n    weatherNotFound: 'Lieu introuvable.',\n    weatherFound: (place, temperature) => `${place} : ${temperature} °C en ce moment, d’après Open-Meteo.`,\n    theme: 'Thème',\n    themes: { geek: 'Geek', eighties: 'Années 80', gym: 'Salle de sport', modern: 'Moderne', kitchen: 'Cuisine', neon: 'Ville néon' },\n    columns: 'Bureaux par rangée',\n    language: 'Langue',\n    sound: 'Son 8 bits',\n    soundHint: 'Une ritournelle quand une tâche démarre, plante, ou qu’un visiteur entre.',\n    alerts: 'Alertes du navigateur',\n    alertsHint: 'Une notification quand une tâche plante ou prend du retard.',\n    alertsBlocked: 'Les notifications sont bloquées pour ce site dans les réglages du navigateur.',\n    seasonal: 'Décorations de saison',\n    seasonalHint: 'Halloween en octobre, Noël en décembre, Pâques au printemps.',\n    night: 'Assombrir la nuit',\n    nightHint: 'La salle s’assombrit le soir et les écrans s’allument.',\n    motion: 'Les bonshommes circulent',\n    motionHint: 'Désactivé : chacun reste à son bureau, et le processeur se repose.',\n    timeline: 'Frise de la journée',\n    timelineHint: 'Sous la salle, un point par passage depuis minuit.',\n    reset: 'Réinitialiser',\n    done: 'Terminé',\n  },\n  sceneLabel: 'L’open space des agents',\n  wall: { jobs: 'Tâches', running: 'En cours', failed: 'En échec', late: 'En retard' },\n  phrases: {\n    relaunch: 'Je te relance !',\n    anyTrouble: 'Un souci ?',\n    stayingHere: 'Je reste là.',\n    report: (ok, n) => (n ? `Rapport : ${ok} à jour, ${n} relancé${n > 1 ? 's' : ''}.` : 'Rapport : tout le monde est à jour.'),\n    visitor: 'Bonjour ! J’ai une demande.',\n    courier: 'Colis livré !',\n  },\n  objects: {\n    workbench: 'L’établi',\n    tv: 'La télé',\n    'server-rack': 'La baie serveur',\n    'vending-machine': 'Le distributeur',\n    fridge: 'Le frigo',\n    'cpu-box': 'Le carton de processeurs',\n    'gpu-box': 'Le carton de cartes graphiques',\n    'ram-box': 'Le carton de mémoire',\n    snake: 'La borne Snake : une partie ?',\n    breakout: 'La borne Casse-puces : une partie ?',\n  },\n  arcade: {\n    play: 'JOUER',\n    score: 'SCORE',\n    record: 'RECORD',\n    gameOver: 'GAME OVER',\n    pressPlay: 'Appuie sur JOUER',\n    replay: 'Appuie sur JOUER pour rejouer',\n    help: { snake: 'Flèches ou Z Q S D', breakout: 'Flèches, Q D ou la souris' },\n    keys: 'Entrée pour jouer · Échap pour fermer',\n    toPlay: 'pour jouer',\n    toClose: 'pour fermer',\n    title: { snake: 'SNAKE DES COMPOSANTS', breakout: 'CASSE-PUCES' },\n  },\n  catNap: 'Le chat de l’atelier fait la sieste',\n  noWeather: 'Pas de météo',\n}\n\nexport const TEXTS: Record<Language, Texts> = { en: EN, fr: FR }\n",
      "type": "registry:component",
      "target": "components/pixel-openspace/i18n.ts"
    },
    {
      "path": "registry/pixel-openspace/types.ts",
      "content": "import type { CSSProperties } from 'react'\n\n/** Where an agent stands right now. */\nexport type AgentStatus =\n  /** Running at this very moment: it sits at its desk, screen on, typing. */\n  | 'working'\n  /** Up to date: it wanders off to the coffee machine between runs. */\n  | 'ok'\n  /** Missed its last run: it dozes at its desk. */\n  | 'late'\n  /**\n   * Missed its last run while the machine was asleep (a laptop with its lid\n   * closed): it sleeps at its desk, and the lead leaves it alone. Nothing is\n   * broken, unlike `late`.\n   */\n  | 'asleep'\n  /** Its last run failed: its PC smokes and the lead comes to see it. */\n  | 'failed'\n  /** Not loaded at all: empty chair, a post-it on a dark screen. */\n  | 'off'\n  /** Never ran yet. */\n  | 'never'\n  /** Runs only when asked. */\n  | 'on-demand'\n\n/** One of your scheduled jobs, workers or AI agents, as the open space shows it. */\nexport type Agent = {\n  /** Stable identifier: it also picks the agent's hair and hoodie colours. */\n  id: string\n  name: string\n  emoji?: string\n  /** One line about what it does; it says it when clicked. */\n  role?: string\n  status: AgentStatus\n  /** What it said last time, shown on its card and in its speech bubble when it finishes. */\n  lastMessage?: string | null\n  /** ISO date of its last run. */\n  lastRun?: string | null\n  /** ISO date of its next run: its screen counts down the last ten minutes. */\n  nextRun?: string | null\n  /** Its rhythm, in words: \"every 30 min\", \"daily at 7:30\". */\n  schedule?: string\n  /** Hours without a run after which its plant wilts and dust covers its screen. Default 72. */\n  staleAfterHours?: number\n  /** Today's runs, oldest first: one dot each on the day timeline under the room. */\n  runs?: Array<AgentRun>\n}\n\n/** One run of an agent, for the day timeline. */\nexport type AgentRun = {\n  /** ISO date of the run. */\n  at: string\n  /** False for a failed run: a red dot. Default true. */\n  ok?: boolean\n  /** Shown when hovering the dot. */\n  message?: string\n}\n\n/** A tile of the big wall screen. */\nexport type WallTile = {\n  label: string\n  value: string | number\n  tone?: 'neutral' | 'ok' | 'info' | 'warn' | 'alert'\n  /** A gauge under the value, from 0 to 1. */\n  progress?: number\n}\n\nexport type Sky = 'clear' | 'clouds' | 'fog' | 'rain' | 'snow' | 'storm'\n\n/** The weather behind the lead's window. */\nexport type Weather = {\n  temperature: number\n  sky: Sky\n  /** False at night: moon and stars instead of the sun. */\n  day: boolean\n  /** Written under the window. */\n  place?: string\n}\n\n/** The props in the room that can be clicked. */\nexport type SceneObject = 'workbench' | 'tv' | 'server-rack' | 'vending-machine' | 'fridge' | 'cpu-box' | 'gpu-box' | 'ram-box'\n\nexport type Language = 'en' | 'fr'\n\n/** The look of the room: a geek open space today, a corporate office around 1986, or a gym where every job rides an exercise bike. */\nexport type ThemeName = 'geek' | 'eighties' | 'gym' | 'modern' | 'kitchen' | 'neon'\n\n/** How many desks per row. */\nexport type Columns = 4 | 5 | 6\n\nexport type OpenSpaceProps = {\n  /** Up to five per row; rows are added as needed. */\n  agents: Array<Agent>\n  /**\n   * The lead, in the glass office: it does its rounds while working, sits\n   * with a failed agent until it recovers, and reports in front of the wall\n   * screen. A decorative lead sits there when none is given.\n   */\n  lead?: Agent\n  /** The neon sign on the main room wall. */\n  title?: string\n  /** Up to four tiles on the big wall screen. Without it, the screen counts your agents: total, running, failed, late. Pass `[]` to leave it dark. */\n  wall?: Array<WallTile>\n  /** What the lead announces in front of the wall screen after its rounds. */\n  announcements?: Array<string>\n  weather?: Weather | null\n  /** A counter: each time it goes up, a visitor walks in to greet the lead. */\n  visitors?: number\n  /** A counter: each time it goes up, a courier drops a parcel by the boxes. */\n  deliveries?: number\n  /** Confetti over the whole room: a birthday, a launch, a record month. */\n  celebrate?: boolean\n  /** The day timeline under the room, when agents have `runs`. Default true. */\n  timeline?: boolean\n  language?: Language\n  /** The look of the room: furniture, outfits, break corners. Default 'geek'. */\n  theme?: ThemeName\n  /** How many desks per row: 4, 5 or 6. Default 5. */\n  columns?: Columns\n  /** Halloween all October, Christmas all December, Easter for the two weeks before Easter Monday. Default true. */\n  seasonal?: boolean\n  /** From what hour to what hour everybody stays at their desk. Default [22, 6]. */\n  nightHours?: [number, number]\n  /** The fullscreen button and the settings gear above the scene. Default true. */\n  toolbar?: boolean\n  /** Where each prop leads when clicked; a prop without a link is not clickable. */\n  objectLinks?: Partial<Record<SceneObject, string>>\n  /** Called instead of following the link, for client-side routers. */\n  onObjectClick?: (object: SceneObject, href: string) => void\n  /** Shows a \"Run now\" button on each agent's card. */\n  onRun?: (agent: Agent) => void | Promise<void>\n  /** The current time, for server rendering: avoids a hydration mismatch on clocks and countdowns. */\n  now?: number\n  className?: string\n  style?: CSSProperties\n}\n",
      "type": "registry:component",
      "target": "components/pixel-openspace/types.ts"
    },
    {
      "path": "registry/pixel-openspace/seasons.ts",
      "content": "/**\n * Les fêtes de la salle : Halloween tout octobre, Noël tout décembre, et\n * Pâques des deux semaines qui la précèdent jusqu'au lundi de Pâques.\n */\nexport type Fete = 'halloween' | 'noel' | 'paques' | null\n\n/** Le dimanche de Pâques d'une année, par l'algorithme grégorien (Meeus, Jones, Butcher). */\nexport function dimancheDePaques(annee: number): Date {\n  const a = annee % 19\n  const b = Math.floor(annee / 100)\n  const c = annee % 100\n  const d = Math.floor(b / 4)\n  const e = b % 4\n  const f = Math.floor((b + 8) / 25)\n  const g = Math.floor((b - f + 1) / 3)\n  const h = (19 * a + b - d - g + 15) % 30\n  const i = Math.floor(c / 4)\n  const k = c % 4\n  const l = (32 + 2 * e + 2 * i - h - k) % 7\n  const m = Math.floor((a + 11 * h + 22 * l) / 451)\n  const mois = Math.floor((h + l - 7 * m + 114) / 31)\n  const jour = ((h + l - 7 * m + 114) % 31) + 1\n  return new Date(annee, mois - 1, jour)\n}\n\nconst JOUR_MS = 86_400_000\n\nexport function fete(maintenant: number): Fete {\n  const d = new Date(maintenant)\n  if (d.getMonth() === 9) return 'halloween'\n  if (d.getMonth() === 11) return 'noel'\n  const paques = dimancheDePaques(d.getFullYear()).getTime()\n  const aujourdhui = new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime()\n  // Deux semaines avant le dimanche de Pâques, jusqu'au lundi de Pâques inclus.\n  if (aujourdhui >= paques - 14 * JOUR_MS && aujourdhui <= paques + JOUR_MS) return 'paques'\n  return null\n}\n",
      "type": "registry:component",
      "target": "components/pixel-openspace/seasons.ts"
    },
    {
      "path": "registry/pixel-openspace/primitives.tsx",
      "content": "'use client'\n\n/*\n * Les briques communes à tous les thèmes : la grille de pixels, les plantes,\n * les néons, les objets cliquables, les animations par sauts, les mots de la\n * scène et la couleur des lampes.\n */\n\nimport { createContext, useContext, useEffect, useRef, type ReactNode } from 'react'\nimport type { Statut } from './engine'\nimport { TEXTS, type Texts } from './i18n'\n\n/* Les mots de la scène, dans la langue choisie, à portée de chaque morceau du décor. */\nexport const TextesContexte = createContext<Texts>(TEXTS.en)\nexport const useTextes = () => useContext(TextesContexte)\n\n/* La couleur d'accent : le laiton de l'atelier, réglable par --po-accent. */\nexport const ACCENT = 'var(--po-accent)'\n\n/* La couleur de la lampe, sur le plan. */\nexport const LAMPE_PLAN: Record<Statut, string> = {\n  'au-travail': ACCENT,\n  'a-jour': '#4ade80',\n  'en-retard': '#fb923c',\n  endormi: '#818cf8',\n  'en-echec': '#ef4444',\n  absent: '#52525b',\n  jamais: '#a1a1aa',\n  'a-la-demande': '#38bdf8',\n}\n\n/*\n * Les animations du décor (LED, néons, télé, ventilateurs, pluie…) ne passent\n * pas par les animations SVG du navigateur : Chrome les recalcule toutes à\n * chaque image, même quand rien ne change, et la page mangeait un tiers de\n * processeur pour des diodes. Chacune s'inscrit ici, et la boucle de la scène\n * les fait avancer par sauts, comme dans un vieux jeu : un élément n'est touché\n * que quand sa valeur change vraiment, et tout se fige au calme.\n *\n * `<Anim>` reprend la syntaxe de `<animate>` (values, dur, begin, keyTimes) :\n * les valeurs se succèdent à intervalles égaux, ou aux instants de keyTimes.\n */\nexport type Piste = {\n  cible: Element\n  attribut: string\n  /** Pour une transformation : translate, rotate… */\n  type?: string\n  valeurs: Array<string>\n  instants: Array<number> | null\n  dureeMs: number\n  debutMs: number\n  derniere?: string\n}\n\nexport const pistes = new Set<Piste>()\n\nexport const enMs = (duree: string | undefined) => (duree ? Number.parseFloat(duree) * (duree.endsWith('ms') ? 1 : 1000) : 0)\n\nexport function Anim({\n  attributeName,\n  type,\n  values,\n  dur,\n  begin,\n  keyTimes,\n}: {\n  attributeName: string\n  type?: string\n  values: string\n  dur: string\n  begin?: string\n  keyTimes?: string\n  repeatCount?: string\n}) {\n  const repere = useRef<SVGDescElement>(null)\n  useEffect(() => {\n    const cible = repere.current?.parentElement\n    if (!cible) return\n    const piste: Piste = {\n      cible,\n      attribut: attributeName,\n      type,\n      valeurs: values.split(';').map((v) => v.trim()),\n      instants: keyTimes ? keyTimes.split(';').map(Number) : null,\n      dureeMs: enMs(dur),\n      debutMs: enMs(begin),\n    }\n    pistes.add(piste)\n    return () => {\n      pistes.delete(piste)\n    }\n  }, [attributeName, type, values, dur, begin, keyTimes])\n  return <desc ref={repere} />\n}\n\n/** Fait avancer toutes les animations du décor à l'instant `t` (ms). */\nexport function animerLeDecor(t: number) {\n  for (const p of pistes) {\n    if (!p.dureeMs) continue\n    const phase = ((((t - p.debutMs) % p.dureeMs) + p.dureeMs) % p.dureeMs) / p.dureeMs\n    let i = Math.min(p.valeurs.length - 1, Math.floor(phase * p.valeurs.length))\n    if (p.instants) {\n      i = 0\n      while (i + 1 < p.instants.length && p.instants[i + 1] <= phase) i++\n    }\n    const v = p.type ? `${p.type}(${p.valeurs[i]})` : p.valeurs[i]\n    if (v !== p.derniere) {\n      p.derniere = v\n      p.cible.setAttribute(p.attribut, v)\n    }\n  }\n}\n\n/** Un trajet découpé en étapes, pour les animations qui déplacent quelque chose. */\nexport function etapes(de: number, a: number, n: number): string {\n  return Array.from({ length: n + 1 }, (_, i) => Math.round((de + ((a - de) * i) / n) * 100) / 100).join(';')\n}\n\nexport const allerRetour = (de: number, a: number, n: number) => `${etapes(de, a, n)};${etapes(a, de, n).split(';').slice(1).join(';')}`\n\n/* ——— Le pixel ——— */\n\n/** Dessine une grille de caractères, un caractère par pixel, en fusionnant les pixels voisins d'une ligne. */\nexport function Pixels({ grille, couleurs, u = 3 }: { grille: Array<string>; couleurs: Record<string, string>; u?: number }) {\n  const rects: Array<ReactNode> = []\n  grille.forEach((ligne, y) => {\n    let x = 0\n    while (x < ligne.length) {\n      const c = ligne[x]\n      let n = 1\n      while (ligne[x + n] === c) n++\n      if (couleurs[c]) rects.push(<rect key={`${x}-${y}`} x={x * u} y={y * u} width={n * u} height={u} fill={couleurs[c]} />)\n      x += n\n    }\n  })\n  return <>{rects}</>\n}\n\nexport const PLANTE = ['..g...g.', '.gGg.gGg', 'gGgggGg.', '.gGgGggg', '..gggGg.', '...ggg..', '.pppppp.', '.PPPPPP.', '..PPPP..']\n\nexport function Plante({ x, y }: { x: number; y: number }) {\n  return (\n    <g transform={`translate(${x} ${y})`}>\n      <Pixels grille={PLANTE} couleurs={{ g: '#4caf50', G: '#2e7d32', p: '#d07a3e', P: '#a35a2a' }} />\n    </g>\n  )\n}\n\n/* ——— Le décor ——— */\n\n/* Les pans du mur de la grande salle, entre ses entrées : le bureau du chef, le coin gaming, la cuisine. */\nexport const MUR_SALLE = [\n  [20, 238],\n  [282, 360],\n  [650, 740],\n  [975, 980],\n]\n\n/** Un néon : le texte, et son halo flou derrière. */\nexport function Neon({ x, y, texte, couleur, taille = 9 }: { x: number; y: number; texte: string; couleur: string; taille?: number }) {\n  return (\n    <g shapeRendering=\"auto\">\n      <text x={x} y={y} fontSize={taille} textAnchor=\"middle\" fill={couleur} filter=\"url(#flou-neon)\" opacity=\"0.9\">\n        {texte}\n      </text>\n      <text x={x} y={y} fontSize={taille} textAnchor=\"middle\" fill=\"#fffbeb\">\n        {texte}\n        <Anim attributeName=\"opacity\" values=\"1;1;0.75;1;1\" dur=\"4s\" repeatCount=\"indefinite\" />\n      </text>\n    </g>\n  )\n}\n\n/** Un objet du décor qu'on peut cliquer : il s'éclaire au survol et mène quelque part. */\nexport function Cliquable({\n  titre,\n  faire,\n  actif = true,\n  children,\n}: {\n  titre: string\n  faire: () => void\n  actif?: boolean\n  children: ReactNode\n}) {\n  if (!actif) return <>{children}</>\n  return (\n    <g\n      role=\"link\"\n      tabIndex={0}\n      aria-label={titre}\n      className=\"cursor-pointer outline-none transition-[filter] hover:brightness-125 focus-visible:brightness-125\"\n      onClick={faire}\n      onKeyDown={(e) => {\n        if (e.key === 'Enter' || e.key === ' ') {\n          e.preventDefault()\n          faire()\n        }\n      }}\n    >\n      <title>{titre}</title>\n      {children}\n    </g>\n  )\n}\n\n/** Une source de lumière dans la nuit ; colorée, elle jette en plus une lueur de sa couleur. */\nexport type Lumiere = { x: number; y: number; r: number; couleur?: string }\n\n/** Ce dont tous les thèmes ont besoin : la trotteuse de l'horloge, la chute d'un bureau qui se monte, le flou des néons. */\nexport function DefsCommunes() {\n  return (\n    <defs>\n      <style>{`@keyframes trotteuse { to { transform: rotate(360deg) } }\n      @keyframes montage-bureau {\n        from { transform: translateY(-160px); opacity: 0 }\n        60% { transform: translateY(6px); opacity: 1 }\n        to { transform: none; opacity: 1 }\n      }`}</style>\n      <filter id=\"flou-neon\" x=\"-20%\" y=\"-50%\" width=\"140%\" height=\"200%\">\n        <feGaussianBlur stdDeviation=\"2.2\" />\n      </filter>\n    </defs>\n  )\n}\n\n/**\n * Un nombre stable tiré d'un identifiant : il choisit les cheveux, la peau et\n * la tenue d'un agent, qui restent les mêmes d'une visite à l'autre.\n */\nexport function hacher(id: string, graine: number, facteur: number) {\n  let h = graine\n  for (const c of id) h = (h * facteur + c.charCodeAt(0)) >>> 0\n  return h\n}\n",
      "type": "registry:component",
      "target": "components/pixel-openspace/primitives.tsx"
    },
    {
      "path": "registry/pixel-openspace/theme.ts",
      "content": "import type { ComponentType } from 'react'\nimport type { CoinDePause, Statut } from './engine'\nimport type { Lumiere } from './primitives'\nimport type { SceneObject } from './types'\n\n/** Ce que le décor reçoit pour se dessiner et se rendre cliquable. */\nexport type PropsDecor = {\n  hauteur: number\n  /** L'enseigne du mur de la grande salle. */\n  titre: string\n  liens: Partial<Record<SceneObject, string>>\n  aller: (objet: SceneObject) => void\n  jouer: (jeu: 'snake' | 'casse-briques') => void\n}\n\n/** Ce qu'un bureau reçoit : sa place sur le plan, l'état de son agent, ce qui s'affiche sur son poste. */\nexport type PropsBureau = {\n  /** Le centre du bureau, le haut du plateau, son bord gauche, et la hauteur du plateau sans sa tranche. */\n  cx: number\n  dy: number\n  x: number\n  dessus: number\n  statut: Statut\n  /** La couleur de l'agent : ses LED, son fauteuil. */\n  accent: string\n  /** Les minutes avant le prochain passage, quand il approche. */\n  bientot: number | null\n  emoji: string\n}\n\nexport type PropsSiege = { x: number; y: number; couleur: string }\n\n/**\n * Un thème d'open space : le décor des pièces, les postes, les tenues, les\n * coins de pause et les lumières de nuit. Le moteur, les bonshommes, les\n * bulles, le tableau blanc, l'horloge, la fenêtre et les saisons sont\n * communs à tous.\n */\nexport type Theme = {\n  /** Les coins des pièces du haut où les agents vont faire une pause. */\n  coins: Array<CoinDePause>\n  /** Le coin où le café coule quand quelqu'un attend devant. */\n  coinCafe?: string\n  /** Le fond autour de la scène. */\n  fond: string\n  /** Les sols, les murs et les pièces du haut, avec leurs objets. */\n  Decor: ComponentType<PropsDecor>\n  /** Le bureau d'un agent : plateau, tranche, et tout ce qui est posé dessus. */\n  Bureau: ComponentType<PropsBureau>\n  /** Le bureau du chef, derrière lequel il trône face à la pièce. */\n  BureauChef: ComponentType<PropsBureau>\n  /** La couleur du nom sur la tranche des bureaux. */\n  plaque: string\n  /** L'assise du siège, son dossier (qui passe devant le dos de l'agent assis), et le fauteuil du chef. */\n  Assise: ComponentType<PropsSiege>\n  Dossier: ComponentType<PropsSiege>\n  Fauteuil: ComponentType<{ x: number; y: number }>\n  /** Les couleurs d'un bonhomme, d'après son identifiant : cheveux, peau, haut, bas… */\n  tenue: (id: string) => Record<string, string>\n  /** Porte-t-il un casque audio ? */\n  casque: (id: string) => boolean\n  /** Le café qui coule, quand quelqu'un attend au coin café. */\n  CafeQuiCoule?: ComponentType\n  /** Les lumières du décor dans la nuit. */\n  lumieres: Array<Lumiere>\n  /** Le voile de la nuit : sa couleur, et son opacité au plus noir (bleu nuit à 0,62 par défaut). */\n  nuit?: { couleur: string; opacite: number }\n  /** Se promène-t-il une tasse à la main ? */\n  tasse?: (id: string) => boolean\n  /** Une lumière de plus par poste occupé (une tour RGB…). */\n  lumierePoste?: (cx: number, dy: number, accent: string) => Array<Lumiere>\n}\n",
      "type": "registry:component",
      "target": "components/pixel-openspace/theme.ts"
    },
    {
      "path": "registry/pixel-openspace/themes/geek.tsx",
      "content": "'use client'\n\n/*\n * Le thème geek : un open space moderne de passionnés de matériel. Bureaux\n * blancs à bande LED, tours vitrées aux ventilateurs RGB, claviers\n * mécaniques, fauteuils gamer ; le chef derrière sa baie serveur, un coin\n * gaming avec deux bornes d'arcade et un établi de montage, une cuisine au\n * frigo plein de canettes. Les bonshommes sont en sweat à capuche.\n */\n\nimport { BUREAU, COINS_GEEK, estInvite, CHEF_ID, type Statut } from '../engine'\nimport {\n  ACCENT,\n  Anim,\n  Cliquable,\n  LAMPE_PLAN,\n  MUR_SALLE,\n  Neon,\n  Plante,\n  allerRetour,\n  etapes,\n  useTextes,\n  type Lumiere,\n  hacher,\n} from '../primitives'\nimport type { PropsBureau, Theme } from '../theme'\nimport type { Jeu } from '../arcade'\nimport type { SceneObject } from '../types'\n\n/** Le café qui coule dans la tasse quand quelqu'un attend devant la machine. */\nexport function CafeQuiCoule() {\n  return (\n    <g pointerEvents=\"none\">\n      <rect x=\"767\" y=\"47\" width=\"2\" height=\"5\" fill=\"#6b3a1e\">\n        <Anim attributeName=\"opacity\" values=\"1;0.4;1\" dur=\"0.5s\" repeatCount=\"indefinite\" />\n      </rect>\n      <rect x=\"764\" y=\"52\" width=\"8\" height=\"3\" fill=\"#6b3a1e\">\n        <Anim attributeName=\"height\" values=\"1;5;5\" dur=\"3s\" repeatCount=\"indefinite\" />\n        <Anim attributeName=\"y\" values=\"56;52;52\" dur=\"3s\" repeatCount=\"indefinite\" />\n      </rect>\n    </g>\n  )\n}\n\n/* ——— Les animations par sauts ——— */\n\nexport const MUR = { arete: '#0f1115', face: '#262a31', plinthe: '#1a1d22' }\n\n/** Un mur vu de biais : son arête sombre, sa face, et la bande LED qui court à son pied. */\nexport function MurHaut({ x, largeur, led }: { x: number; largeur: number; led: string }) {\n  return (\n    <>\n      <rect x={x} y=\"14\" width={largeur} height=\"6\" fill={MUR.arete} />\n      <rect x={x} y=\"20\" width={largeur} height=\"32\" fill={MUR.face} />\n      <rect x={x} y=\"52\" width={largeur} height=\"4\" fill={MUR.plinthe} />\n      <rect x={x} y=\"51\" width={largeur} height=\"2\" fill={led} opacity=\"0.9\" />\n    </>\n  )\n}\n\nexport function Decor({\n  hauteur,\n  titre,\n  liens,\n  aller,\n  jouer,\n}: {\n  hauteur: number\n  titre: string\n  liens: Partial<Record<SceneObject, string>>\n  aller: (objet: SceneObject) => void\n  jouer: (jeu: Jeu) => void\n}) {\n  const t = useTextes()\n  const bas = hauteur - 20\n  const lien = (objet: SceneObject) => ({ titre: t.objects[objet], faire: () => aller(objet), actif: Boolean(liens[objet]) })\n  return (\n    <>\n      <defs>\n        <pattern id=\"beton\" width=\"96\" height=\"96\" patternUnits=\"userSpaceOnUse\">\n          <rect width=\"96\" height=\"96\" fill=\"#565a62\" />\n          <rect width=\"96\" height=\"1\" fill=\"#4c5058\" />\n          <rect width=\"1\" height=\"96\" fill=\"#4c5058\" />\n          <rect x=\"14\" y=\"22\" width=\"2\" height=\"2\" fill=\"#5f636b\" />\n          <rect x=\"60\" y=\"40\" width=\"3\" height=\"2\" fill=\"#4f535b\" />\n          <rect x=\"38\" y=\"74\" width=\"2\" height=\"2\" fill=\"#60646c\" />\n          <rect x=\"80\" y=\"12\" width=\"2\" height=\"3\" fill=\"#51555d\" />\n        </pattern>\n        <pattern id=\"moquette-chef\" width=\"16\" height=\"16\" patternUnits=\"userSpaceOnUse\">\n          <rect width=\"16\" height=\"16\" fill=\"#3a3f4b\" />\n          <rect width=\"8\" height=\"8\" fill=\"#3f4451\" />\n          <rect x=\"8\" y=\"8\" width=\"8\" height=\"8\" fill=\"#3f4451\" />\n        </pattern>\n        <pattern id=\"moquette-jeu\" width=\"12\" height=\"12\" patternUnits=\"userSpaceOnUse\">\n          <rect width=\"12\" height=\"12\" fill=\"#231f36\" />\n          <rect x=\"2\" y=\"2\" width=\"2\" height=\"2\" fill=\"#2c2645\" />\n          <rect x=\"8\" y=\"8\" width=\"2\" height=\"2\" fill=\"#1d1a2e\" />\n        </pattern>\n        <pattern id=\"carreaux\" width=\"32\" height=\"32\" patternUnits=\"userSpaceOnUse\">\n          <rect width=\"32\" height=\"32\" fill=\"#d5d9df\" />\n          <rect width=\"32\" height=\"1\" fill=\"#c2c7ce\" />\n          <rect width=\"1\" height=\"32\" fill=\"#c2c7ce\" />\n        </pattern>\n        <linearGradient id=\"led-jeu\" x1=\"0\" x2=\"1\">\n          <stop offset=\"0\" stopColor=\"#22d3ee\" />\n          <stop offset=\"0.5\" stopColor=\"#a855f7\" />\n          <stop offset=\"1\" stopColor=\"#ec4899\" />\n        </linearGradient>\n      </defs>\n\n      {/* Les sols. */}\n      <rect x=\"20\" y=\"56\" width=\"310\" height=\"142\" fill=\"url(#moquette-chef)\" />\n      <rect x=\"338\" y=\"56\" width=\"344\" height=\"142\" fill=\"url(#moquette-jeu)\" />\n      <rect x=\"690\" y=\"56\" width=\"290\" height=\"142\" fill=\"url(#carreaux)\" />\n      <rect x=\"20\" y=\"198\" width=\"960\" height={bas - 198} fill=\"url(#beton)\" />\n\n      {/* Les murs du fond, chacun avec sa bande LED. */}\n      <MurHaut x={20} largeur={310} led=\"var(--po-accent)\" />\n      <MurHaut x={338} largeur={344} led=\"url(#led-jeu)\" />\n      <MurHaut x={690} largeur={290} led=\"#e0f2fe\" />\n\n      {/* Le bureau du chef : baie serveur, enseigne néon, plantes. */}\n      <Cliquable {...lien('server-rack')}>\n        <BaieServeur x={34} />\n      </Cliquable>\n      <Neon x={222} y={40} texte={t.leadOffice} couleur=\"var(--po-accent)\" taille={11} />\n      <Plante x={300} y={62} />\n\n      {/* Le coin gaming : la borne d'arcade, la grande télé et sa console, les poufs, l'établi de montage. */}\n      <Cliquable titre={t.objects.snake} faire={() => jouer('snake')}>\n        <Arcade x={352} />\n      </Cliquable>\n      <Cliquable titre={t.objects.breakout} faire={() => jouer('casse-briques')}>\n        <BorneCasseBriques x={396} />\n      </Cliquable>\n      <Cliquable {...lien('tv')}>\n        <Tele x={442} />\n      </Cliquable>\n      <rect x=\"456\" y=\"128\" width=\"108\" height=\"46\" fill=\"#3b2f63\" />\n      <rect x=\"456\" y=\"128\" width=\"108\" height=\"2\" fill=\"#a855f7\" />\n      <rect x=\"456\" y=\"172\" width=\"108\" height=\"2\" fill=\"#a855f7\" />\n      <Pouf x={482} y={146} couleur=\"#ef4444\" />\n      <Pouf x={538} y={146} couleur=\"#22d3ee\" />\n      <Cliquable {...lien('workbench')}>\n        <Etabli x={594} />\n      </Cliquable>\n      <Plante x={346} y={160} />\n\n      {/* La cuisine : machine espresso, distributeur, frigo vitré plein de canettes, mange-debout. */}\n      <rect x=\"700\" y=\"24\" width=\"20\" height=\"20\" fill=\"#111827\" />\n      <Neon x={710} y={38} texte=\"☕\" couleur=\"#f472b6\" taille={12} />\n      <rect x=\"730\" y=\"44\" width=\"122\" height=\"24\" fill=\"#f3f4f6\" />\n      <rect x=\"730\" y=\"68\" width=\"122\" height=\"18\" fill=\"#1f2937\" />\n      {[734, 774, 814].map((x) => (\n        <rect key={x} x={x} y=\"72\" width=\"34\" height=\"10\" fill=\"#273244\" />\n      ))}\n      <rect x=\"748\" y=\"22\" width=\"40\" height=\"38\" fill=\"#9ca3af\" />\n      <rect x=\"750\" y=\"24\" width=\"36\" height=\"12\" fill=\"#d1d5db\" />\n      <rect x=\"752\" y=\"38\" width=\"32\" height=\"6\" fill=\"#111827\" />\n      <rect x=\"778\" y=\"27\" width=\"5\" height=\"5\" fill=\"var(--po-accent)\" />\n      <rect x=\"762\" y=\"44\" width=\"12\" height=\"4\" fill=\"#4b5563\" />\n      <rect x=\"763\" y=\"50\" width=\"10\" height=\"8\" fill=\"#fafaf9\" />\n      <path d=\"M765 47v-6M771 47v-6\" stroke=\"#e7e5e4\" strokeWidth=\"2\" fill=\"none\">\n        <Anim attributeName=\"transform\" type=\"translate\" values=\"0 2;0 -4;0 2\" dur=\"2.6s\" repeatCount=\"indefinite\" />\n        <Anim attributeName=\"opacity\" values=\"0;0.7;0\" dur=\"2.6s\" repeatCount=\"indefinite\" />\n      </path>\n      <rect x=\"814\" y=\"50\" width=\"34\" height=\"12\" fill=\"#6b7280\" />\n      <rect x=\"818\" y=\"53\" width=\"26\" height=\"6\" fill=\"#cbd5e1\" />\n      <Cliquable {...lien('vending-machine')}>\n        <Distributeur x={860} />\n      </Cliquable>\n      <Cliquable {...lien('fridge')}>\n        <FrigoVitre x={922} />\n      </Cliquable>\n      <rect x=\"818\" y=\"132\" width=\"8\" height=\"34\" fill=\"#111827\" />\n      <rect x=\"802\" y=\"126\" width=\"40\" height=\"10\" fill=\"#f9fafb\" />\n      <rect x=\"802\" y=\"136\" width=\"40\" height=\"3\" fill=\"#d1d5db\" />\n      {[786, 850].map((x) => (\n        <g key={x}>\n          <rect x={x} y=\"140\" width=\"10\" height=\"10\" fill=\"#111827\" />\n          <rect x={x + 3} y=\"150\" width=\"4\" height=\"12\" fill=\"#374151\" />\n        </g>\n      ))}\n      <Plante x={700} y={160} />\n\n      {/* La grande salle : les cartons de matériel qui attendent, les plantes. */}\n      <Cartons x={56} y={bas - 44} lien={lien} />\n      <Plante x={946} y={bas - 32} />\n\n      {/* Le mur de la grande salle, percé de ses trois entrées, avec sa bande LED ; l'écran y est accroché. */}\n      {MUR_SALLE.map(([de, a]) => (\n        <g key={de}>\n          <rect x={de} y=\"198\" width={a - de} height=\"6\" fill={MUR.arete} />\n          <rect x={de} y=\"204\" width={a - de} height=\"38\" fill={MUR.face} />\n          <rect x={de} y=\"242\" width={a - de} height=\"4\" fill={MUR.plinthe} />\n          <rect x={de} y=\"241\" width={a - de} height=\"2\" fill=\"var(--po-accent)\" opacity=\"0.8\" />\n        </g>\n      ))}\n      <Neon x={695} y={226} texte={titre} couleur=\"#22d3ee\" taille={titre.length > 14 ? 7 : 9} />\n      <Porte bas={bas} />\n      <rect x=\"330\" y=\"14\" width=\"8\" height=\"190\" fill=\"#7dd3fc\" opacity=\"0.18\" />\n      <rect x=\"330\" y=\"14\" width=\"2\" height=\"190\" fill=\"#bae6fd\" opacity=\"0.6\" />\n      <rect x=\"682\" y=\"14\" width=\"8\" height=\"190\" fill={MUR.arete} />\n      <rect x=\"12\" y=\"14\" width=\"8\" height={bas - 6} fill={MUR.arete} />\n      <rect x=\"980\" y=\"14\" width=\"8\" height={bas - 6} fill={MUR.arete} />\n      <rect x=\"12\" y=\"6\" width=\"976\" height=\"8\" fill={MUR.arete} />\n      <rect x=\"12\" y={bas} width=\"976\" height=\"10\" fill={MUR.arete} />\n    </>\n  )\n}\n\n/** La baie serveur : ses tiroirs et leurs diodes qui clignotent chacune à son rythme. */\nexport function BaieServeur({ x }: { x: number }) {\n  return (\n    <g>\n      <rect x={x} y=\"20\" width=\"50\" height=\"84\" fill=\"#0b0d10\" />\n      <rect x={x + 2} y=\"22\" width=\"46\" height=\"80\" fill=\"#16191e\" />\n      {Array.from({ length: 7 }, (_, i) => (\n        <g key={i}>\n          <rect x={x + 4} y={25 + i * 11} width=\"42\" height=\"9\" fill=\"#23272e\" />\n          <rect x={x + 6} y={28 + i * 11} width=\"18\" height=\"2\" fill=\"#3a3f48\" />\n          {[0, 1, 2].map((j) => (\n            <rect key={j} x={x + 30 + j * 5} y={27 + i * 11} width=\"3\" height=\"3\" fill={['#22c55e', '#22c55e', '#38bdf8'][j]}>\n              <Anim attributeName=\"opacity\" values=\"1;0.15;1\" dur={`${0.4 + ((i * 3 + j * 7) % 9) / 6}s`} repeatCount=\"indefinite\" />\n            </rect>\n          ))}\n        </g>\n      ))}\n    </g>\n  )\n}\n\n/** La borne d'arcade : marquee néon, écran qui joue tout seul, boutons. */\nexport function Arcade({ x }: { x: number }) {\n  // Le serpent avance par à-coups vers la puce, comme dans le jeu.\n  const anneaux = ['#f472b6', '#a855f7', '#38bdf8', '#22c55e']\n  return (\n    <g>\n      <rect x={x} y=\"18\" width=\"42\" height=\"84\" fill=\"#4c1d95\" />\n      <rect x={x + 2} y=\"20\" width=\"38\" height=\"10\" fill=\"#ec4899\" />\n      <text x={x + 21} y=\"28\" fontSize=\"7\" textAnchor=\"middle\" fill=\"#fdf2f8\">\n        SNAKE\n      </text>\n      <rect x={x + 5} y=\"33\" width=\"32\" height=\"26\" fill=\"#0b0d10\" />\n      <rect x={x + 8} y=\"36\" width=\"26\" height=\"20\" fill=\"#0f172a\" />\n      <rect x={x + 26} y=\"44\" width=\"4\" height=\"4\" fill=\"#e3b95a\" />\n      <g>\n        <rect x={x + 18} y=\"45\" width=\"3\" height=\"3\" fill=\"#f8fafc\" />\n        {anneaux.map((c, i) => (\n          <rect key={c} x={x + 15 - i * 3} y=\"45\" width=\"3\" height=\"3\" fill={c} />\n        ))}\n        <Anim attributeName=\"transform\" type=\"translate\" values=\"-4 0;-1 0;2 0;5 0;-4 0\" dur=\"2s\" repeatCount=\"indefinite\" />\n      </g>\n      <rect x={x + 3} y=\"62\" width=\"36\" height=\"12\" fill=\"#5b21b6\" />\n      <rect x={x + 9} y=\"65\" width=\"3\" height=\"6\" fill=\"#111827\" />\n      <rect x={x + 8} y=\"63\" width=\"5\" height=\"3\" fill=\"#ef4444\" />\n      {[20, 26, 32].map((dx, i) => (\n        <rect key={dx} x={x + dx} y=\"66\" width=\"4\" height=\"4\" fill={['#facc15', '#22c55e', '#38bdf8'][i]} />\n      ))}\n      <rect x={x + 6} y=\"76\" width=\"30\" height=\"24\" fill=\"#3b0764\" />\n      <rect x={x + 16} y=\"84\" width=\"10\" height=\"4\" fill=\"#facc15\" />\n    </g>\n  )\n}\n\n/** La deuxième borne, cyan : son écran rejoue un casse-briques, la bille rebondit sous les puces. */\nexport function BorneCasseBriques({ x }: { x: number }) {\n  const rangs = ['#e3b95a', '#22c55e', '#38bdf8']\n  return (\n    <g>\n      <rect x={x} y=\"18\" width=\"42\" height=\"84\" fill=\"#0e7490\" />\n      <rect x={x + 2} y=\"20\" width=\"38\" height=\"10\" fill=\"#22d3ee\" />\n      <text x={x + 21} y=\"28\" fontSize=\"6\" textAnchor=\"middle\" fill=\"#083344\">\n        CASSE-PUCES\n      </text>\n      <rect x={x + 5} y=\"33\" width=\"32\" height=\"26\" fill=\"#0b0d10\" />\n      <rect x={x + 8} y=\"36\" width=\"26\" height=\"20\" fill=\"#020617\" />\n      {rangs.map((c, r) =>\n        [0, 1, 2, 3].map((i) => <rect key={`${r}${i}`} x={x + 9 + i * 6} y={37 + r * 3} width=\"5\" height=\"2\" fill={c} />),\n      )}\n      <rect x={x + 18} y=\"46\" width=\"2\" height=\"2\" fill=\"#f8fafc\">\n        <Anim attributeName=\"x\" values={`${x + 10};${x + 31};${x + 18};${x + 10}`} dur=\"2.4s\" repeatCount=\"indefinite\" />\n        <Anim attributeName=\"y\" values=\"53;47;53;47;53\" dur=\"1.2s\" repeatCount=\"indefinite\" />\n      </rect>\n      <rect x={x + 16} y=\"54\" width=\"8\" height=\"1\" fill=\"#e5e7eb\">\n        <Anim attributeName=\"x\" values={`${x + 9};${x + 25};${x + 14};${x + 9}`} dur=\"2.4s\" repeatCount=\"indefinite\" />\n      </rect>\n      <rect x={x + 3} y=\"62\" width=\"36\" height=\"12\" fill=\"#155e75\" />\n      <rect x={x + 8} y=\"66\" width=\"12\" height=\"3\" fill=\"#111827\" />\n      <rect x={x + 12} y=\"65\" width=\"4\" height=\"5\" fill=\"#e5e7eb\" />\n      {[26, 32].map((dx, i) => (\n        <rect key={dx} x={x + dx} y=\"66\" width=\"4\" height=\"4\" fill={['#ef4444', '#facc15'][i]} />\n      ))}\n      <rect x={x + 6} y=\"76\" width=\"30\" height=\"24\" fill=\"#164e63\" />\n      <rect x={x + 16} y=\"84\" width=\"10\" height=\"4\" fill=\"#22d3ee\" />\n    </g>\n  )\n}\n\n/** La grande télé, la console dessous, et un jeu de plateforme qui tourne en boucle. */\nexport function Tele({ x }: { x: number }) {\n  return (\n    <g>\n      <rect x={x} y=\"22\" width=\"148\" height=\"40\" fill=\"#0b0d10\" />\n      <rect x={x + 3} y=\"25\" width=\"142\" height=\"34\" fill=\"#38bdf8\" />\n      <rect x={x + 3} y=\"49\" width=\"142\" height=\"10\" fill=\"#16a34a\" />\n      <rect x={x + 3} y=\"49\" width=\"142\" height=\"2\" fill=\"#4ade80\" />\n      <rect x={x + 30} y=\"40\" width=\"18\" height=\"9\" fill=\"#a16207\" />\n      <rect x={x + 90} y=\"36\" width=\"14\" height=\"13\" fill=\"#15803d\" />\n      <rect x={x + 120} y=\"30\" width=\"10\" height=\"6\" fill=\"#f8fafc\" opacity=\"0.9\" />\n      <rect x={x + 60} y=\"28\" width=\"14\" height=\"5\" fill=\"#f8fafc\" opacity=\"0.8\" />\n      {/* Le héros qui court et saute. */}\n      <rect x={x + 10} y=\"43\" width=\"5\" height=\"6\" fill=\"#ef4444\">\n        <Anim attributeName=\"x\" values={allerRetour(x + 8, x + 134, 18)} dur=\"7s\" repeatCount=\"indefinite\" />\n        <Anim attributeName=\"y\" values=\"43;43;35;43;43\" keyTimes=\"0;0.4;0.47;0.54;1\" dur=\"3.5s\" repeatCount=\"indefinite\" />\n      </rect>\n      <rect x={x + 62} y=\"64\" width=\"24\" height=\"10\" fill=\"#111827\" />\n      <rect x={x + 30} y=\"66\" width=\"88\" height=\"10\" fill=\"#1f2937\" />\n      <rect x={x + 66} y=\"68\" width=\"16\" height=\"5\" fill=\"#f8fafc\" />\n      <rect x={x + 80} y=\"69\" width=\"2\" height=\"2\" fill=\"#38bdf8\" />\n    </g>\n  )\n}\n\nexport function Pouf({ x, y, couleur }: { x: number; y: number; couleur: string }) {\n  return (\n    <g>\n      <rect x={x - 14} y={y - 6} width=\"28\" height=\"20\" fill=\"#0b0d10\" />\n      <rect x={x - 13} y={y - 5} width=\"26\" height=\"18\" fill={couleur} />\n      <rect x={x - 10} y={y - 4} width=\"20\" height=\"4\" fill=\"white\" opacity=\"0.25\" />\n      <rect x={x - 13} y={y + 9} width=\"26\" height=\"4\" fill=\"black\" opacity=\"0.25\" />\n    </g>\n  )\n}\n\n/** L'établi de montage : panneau à outils, tapis antistatique, PC ouvert, barrettes, tournevis. */\nexport function Etabli({ x }: { x: number }) {\n  return (\n    <g>\n      <rect x={x} y=\"20\" width=\"82\" height=\"28\" fill=\"#a3825a\" />\n      {Array.from({ length: 4 }, (_, i) =>\n        Array.from({ length: 10 }, (_, j) => (\n          <rect key={`${i}-${j}`} x={x + 4 + j * 8} y={23 + i * 7} width=\"1\" height=\"1\" fill=\"#6b5236\" />\n        )),\n      )}\n      <rect x={x + 8} y=\"25\" width=\"3\" height=\"16\" fill=\"#ef4444\" />\n      <rect x={x + 16} y=\"24\" width=\"10\" height=\"4\" fill=\"#9ca3af\" />\n      <rect x={x + 20} y=\"28\" width=\"2\" height=\"12\" fill=\"#9ca3af\" />\n      <rect x={x + 32} y=\"26\" width=\"12\" height=\"12\" fill=\"#facc15\" />\n      <rect x={x + 56} y=\"25\" width=\"18\" height=\"6\" fill=\"#374151\" />\n      <rect x={x} y=\"48\" width=\"82\" height=\"22\" fill=\"#9ca3af\" />\n      <rect x={x + 4} y=\"50\" width=\"74\" height=\"18\" fill=\"#0e7490\" />\n      <rect x={x} y=\"70\" width=\"82\" height=\"14\" fill=\"#4b5563\" />\n      <rect x={x + 3} y=\"84\" width=\"5\" height=\"10\" fill=\"#1f2937\" />\n      <rect x={x + 74} y=\"84\" width=\"5\" height=\"10\" fill=\"#1f2937\" />\n      {/* Le boîtier ouvert : carte mère verte, carte graphique, ventilateur RGB. */}\n      <rect x={x + 8} y=\"30\" width=\"30\" height=\"34\" fill=\"#111827\" />\n      <rect x={x + 11} y=\"33\" width=\"24\" height=\"28\" fill=\"#14532d\" />\n      <rect x={x + 13} y=\"36\" width=\"8\" height=\"8\" fill=\"#9ca3af\" />\n      <rect x={x + 23} y=\"36\" width=\"2\" height=\"9\" fill=\"#e5e7eb\" />\n      <rect x={x + 26} y=\"36\" width=\"2\" height=\"9\" fill=\"#e5e7eb\" />\n      <rect x={x + 12} y=\"49\" width=\"22\" height=\"6\" fill=\"#1f2937\" />\n      <rect x={x + 12} y=\"54\" width=\"22\" height=\"1\" fill=\"#22d3ee\">\n        <Anim attributeName=\"fill\" values=\"#22d3ee;#a855f7;#ec4899;#22d3ee\" dur=\"3s\" repeatCount=\"indefinite\" />\n      </rect>\n      {/* Barrettes, tournevis, carton de GPU. */}\n      <rect x={x + 44} y=\"56\" width=\"14\" height=\"3\" fill=\"#166534\" />\n      <rect x={x + 44} y=\"61\" width=\"14\" height=\"3\" fill=\"#166534\" />\n      <rect x={x + 62} y=\"58\" width=\"12\" height=\"2\" fill=\"#f59e0b\" />\n      <rect x={x + 58} y=\"58\" width=\"4\" height=\"2\" fill=\"#9ca3af\" />\n      <rect x={x + 46} y=\"38\" width=\"28\" height=\"14\" fill=\"#16a34a\" />\n      <text x={x + 60} y=\"48\" fontSize=\"6\" textAnchor=\"middle\" fill=\"#f0fdf4\">\n        GPU\n      </text>\n    </g>\n  )\n}\n\n/** Le distributeur : vitrine de snacks et canettes, monnayeur. */\nexport function Distributeur({ x }: { x: number }) {\n  const snacks = ['#f59e0b', '#ef4444', '#22c55e', '#3b82f6', '#a855f7', '#f472b6']\n  return (\n    <g>\n      <rect x={x} y=\"16\" width=\"44\" height=\"80\" fill=\"#b91c1c\" />\n      <rect x={x + 3} y=\"20\" width=\"28\" height=\"64\" fill=\"#0f172a\" />\n      {[0, 1, 2, 3].map((r) =>\n        [0, 1, 2].map((c) => (\n          <rect key={`${r}${c}`} x={x + 6 + c * 8} y={24 + r * 15} width=\"6\" height=\"9\" fill={snacks[(r * 3 + c) % snacks.length]} />\n        )),\n      )}\n      {[0, 1, 2, 3].map((r) => (\n        <rect key={r} x={x + 3} y={34 + r * 15} width=\"28\" height=\"2\" fill=\"#94a3b8\" />\n      ))}\n      <rect x={x + 34} y=\"24\" width=\"7\" height=\"14\" fill=\"#111827\" />\n      <rect x={x + 35} y=\"26\" width=\"5\" height=\"3\" fill=\"#22c55e\" />\n      {[0, 1, 2].map((r) => (\n        <rect key={r} x={x + 35} y={42 + r * 5} width=\"5\" height=\"3\" fill=\"#e5e7eb\" />\n      ))}\n      <rect x={x + 6} y=\"87\" width=\"22\" height=\"6\" fill=\"#111827\" />\n    </g>\n  )\n}\n\n/** Le frigo vitré, éclairé, rempli de canettes de boisson énergisante. */\nexport function FrigoVitre({ x }: { x: number }) {\n  const canettes = ['#22c55e', '#0ea5e9', '#f43f5e', '#facc15']\n  return (\n    <g>\n      <rect x={x} y=\"16\" width=\"54\" height=\"82\" fill=\"#111827\" />\n      <rect x={x + 3} y=\"19\" width=\"48\" height=\"72\" fill=\"#e0f2fe\" />\n      {[0, 1, 2, 3].map((r) => (\n        <g key={r}>\n          {Array.from({ length: 7 }, (_, i) => (\n            <rect key={i} x={x + 6 + i * 6} y={22 + r * 17} width=\"4\" height=\"10\" fill={canettes[(i + r) % canettes.length]} />\n          ))}\n          <rect x={x + 3} y={33 + r * 17} width=\"48\" height=\"2\" fill=\"#94a3b8\" />\n        </g>\n      ))}\n      <rect x={x + 44} y=\"40\" width=\"3\" height=\"20\" fill=\"#9ca3af\" />\n      <rect x={x + 6} y=\"92\" width=\"42\" height=\"4\" fill=\"#0ea5e9\" />\n    </g>\n  )\n}\n\n/** Des cartons de composants empilés, en attente de montage ; chacun ouvre sa table de référence. */\nexport function Cartons({\n  x,\n  y,\n  lien,\n}: {\n  x: number\n  y: number\n  lien: (objet: SceneObject) => { titre: string; faire: () => void; actif: boolean }\n}) {\n  return (\n    <g>\n      <Cliquable {...lien('cpu-box')}>\n        <rect x={x} y={y + 14} width=\"40\" height=\"26\" fill=\"#a16207\" />\n        <rect x={x} y={y + 14} width=\"40\" height=\"4\" fill=\"#ca8a04\" />\n        <text x={x + 20} y={y + 32} fontSize=\"7\" textAnchor=\"middle\" fill=\"#fef3c7\">\n          CPU\n        </text>\n      </Cliquable>\n      <Cliquable {...lien('gpu-box')}>\n        <rect x={x + 6} y={y} width=\"30\" height=\"16\" fill=\"#15803d\" />\n        <rect x={x + 6} y={y} width=\"30\" height=\"3\" fill=\"#16a34a\" />\n        <text x={x + 21} y={y + 12} fontSize=\"6\" textAnchor=\"middle\" fill=\"#f0fdf4\">\n          RTX\n        </text>\n      </Cliquable>\n      <Cliquable {...lien('ram-box')}>\n        <rect x={x + 44} y={y + 22} width=\"22\" height=\"18\" fill=\"#1d4ed8\" />\n        <text x={x + 55} y={y + 34} fontSize=\"6\" textAnchor=\"middle\" fill=\"#eff6ff\">\n          RAM\n        </text>\n      </Cliquable>\n    </g>\n  )\n}\n\n/** La porte vitrée d'entrée, au bas de la grande salle, et son paillasson. */\nexport function Porte({ bas }: { bas: number }) {\n  const t = useTextes()\n  return (\n    <g pointerEvents=\"none\">\n      <rect x=\"472\" y={bas - 14} width=\"56\" height=\"12\" fill=\"#3f3f46\" />\n      <rect x=\"476\" y={bas - 12} width=\"48\" height=\"8\" fill=\"#52525b\" />\n      <text x=\"500\" y={bas - 6} fontSize=\"5.5\" textAnchor=\"middle\" fill=\"#a1a1aa\">\n        {t.welcome}\n      </text>\n      <rect x=\"470\" y={bas} width=\"60\" height=\"10\" fill=\"#7dd3fc\" opacity=\"0.35\" />\n      <rect x=\"499\" y={bas} width=\"2\" height=\"10\" fill=\"#bae6fd\" />\n    </g>\n  )\n}\n\n/** L'écran ultra-large ; `bientot` affiche les minutes avant le prochain passage. */\nexport function Ecran({ cx, dy, statut, bientot }: { cx: number; dy: number; statut: Statut; bientot: number | null }) {\n  const t = useTextes()\n  const travaille = statut === 'au-travail'\n  const l = 54\n  const g = cx - l / 2\n  return (\n    <g>\n      <rect x={cx - 4} y={dy + 10} width=\"8\" height=\"7\" fill=\"#111827\" />\n      <rect x={cx - 12} y={dy + 16} width=\"24\" height=\"3\" fill=\"#111827\" />\n      <rect x={g - 3} y={dy - 15} width={l + 6} height=\"27\" fill=\"#0b0d10\" />\n      {statut === 'endormi' ? (\n        // En veille : l'écran noir, sans le post-it de l'absent.\n        <rect x={g} y={dy - 12} width={l} height=\"21\" fill=\"#020617\" />\n      ) : statut === 'absent' ? (\n        <>\n          <rect x={g} y={dy - 12} width={l} height=\"21\" fill=\"#020617\" />\n          {/* Le post-it laissé en partant. */}\n          <g transform={`rotate(-6 ${cx} ${dy})`}>\n            <rect x={cx - 15} y={dy - 10} width=\"30\" height=\"15\" fill=\"#fde68a\" />\n            <rect x={cx - 15} y={dy - 10} width=\"30\" height=\"3\" fill=\"#fcd34d\" />\n            <text x={cx} y={dy + 2} fontSize=\"7\" textAnchor=\"middle\" fill=\"#78350f\">\n              {t.offNote}\n            </text>\n          </g>\n        </>\n      ) : statut === 'en-echec' ? (\n        <>\n          <rect x={g} y={dy - 12} width={l} height=\"21\" fill=\"#991b1b\">\n            <Anim attributeName=\"opacity\" values=\"1;0.6;1\" dur=\"1s\" repeatCount=\"indefinite\" />\n          </rect>\n          <rect x={cx - 2} y={dy - 9} width=\"4\" height=\"10\" fill=\"#fef2f2\" />\n          <rect x={cx - 2} y={dy + 3} width=\"4\" height=\"4\" fill=\"#fef2f2\" />\n        </>\n      ) : travaille ? (\n        <>\n          <rect x={g} y={dy - 12} width={l} height=\"21\" fill=\"#0b0d10\" />\n          {[0, 1, 2, 3].map((i) => {\n            const w = [30, 18, 36, 14][i]\n            return (\n              <rect key={i} x={g + 3 + (i % 2) * 5} y={dy - 9 + i * 5} width={w} height=\"2\" fill={i % 2 ? '#22d3ee' : 'var(--po-accent)'}>\n                <Anim attributeName=\"width\" values={`${etapes(2, w, 5)};${w}`} dur=\"1.6s\" begin={`${i * 0.4}s`} repeatCount=\"indefinite\" />\n              </rect>\n            )\n          })}\n        </>\n      ) : (\n        <>\n          {/* Le bureau du système : un fond dégradé, deux fenêtres. */}\n          <rect x={g} y={dy - 12} width={l} height=\"21\" fill=\"#312e81\" />\n          <rect x={g} y={dy - 2} width={l} height=\"11\" fill=\"#4338ca\" />\n          <rect x={g + 4} y={dy - 9} width=\"22\" height=\"13\" fill=\"#e0e7ff\" />\n          <rect x={g + 4} y={dy - 9} width=\"22\" height=\"3\" fill=\"#818cf8\" />\n          <rect x={g + 30} y={dy - 7} width=\"20\" height=\"10\" fill=\"#c7d2fe\" />\n          <rect x={g} y={dy + 7} width={l} height=\"2\" fill=\"#1e1b4b\" />\n        </>\n      )}\n      {bientot ? (\n        <g>\n          <rect x={g} y={dy + 1} width={l} height=\"9\" fill=\"#0b1220\" />\n          <text x={cx} y={dy + 8} fontSize=\"7\" textAnchor=\"middle\" fill=\"var(--po-accent)\">\n            {t.inMinutes(bientot)}\n          </text>\n          <Anim attributeName=\"opacity\" values=\"1;0.55;1\" dur=\"2s\" repeatCount=\"indefinite\" />\n        </g>\n      ) : null}\n    </g>\n  )\n}\n\n/**\n * La tour sur le bureau, vitrée, avec deux ventilateurs RGB qui tournent tant\n * que l'agent est là. En échec, elle vire au rouge et fume.\n */\nexport function Tour({\n  x,\n  y,\n  couleur,\n  allumee,\n  fume,\n  tourne,\n}: {\n  x: number\n  y: number\n  couleur: string\n  allumee: boolean\n  fume: boolean\n  /** Les ventilateurs ne tournent que sous la charge : au repos, la tour reste immobile (et ne coûte rien). */\n  tourne: boolean\n}) {\n  return (\n    <g>\n      {fume\n        ? [0, 0.6, 1.2].map((debut, i) => (\n            <rect key={debut} x={x + 6 + i * 3} y={y - 2} width=\"6\" height=\"6\" fill=\"#9ca3af\" opacity=\"0\">\n              <Anim attributeName=\"y\" values={etapes(y - 2, y - 34, 6)} dur=\"1.8s\" begin={`${debut}s`} repeatCount=\"indefinite\" />\n              <Anim attributeName=\"opacity\" values={etapes(0.85, 0, 6)} dur=\"1.8s\" begin={`${debut}s`} repeatCount=\"indefinite\" />\n              <Anim attributeName=\"width\" values={etapes(5, 11, 6)} dur=\"1.8s\" begin={`${debut}s`} repeatCount=\"indefinite\" />\n              <Anim attributeName=\"height\" values={etapes(5, 11, 6)} dur=\"1.8s\" begin={`${debut}s`} repeatCount=\"indefinite\" />\n            </rect>\n          ))\n        : null}\n      <rect x={x} y={y} width=\"22\" height=\"40\" fill=\"#0b0d10\" />\n      <rect x={x + 2} y={y + 2} width=\"18\" height=\"36\" fill=\"#1e293b\" />\n      {[8, 22].map((dy) => (\n        <g key={dy}>\n          <rect x={x + 4} y={y + dy - 5} width=\"14\" height=\"12\" fill={allumee ? couleur : '#334155'} opacity={allumee ? 0.9 : 1} />\n          <rect x={x + 6} y={y + dy - 3} width=\"10\" height=\"8\" fill=\"#0f172a\" />\n          <g transform={`translate(${x + 11} ${y + dy + 1})`}>\n            <rect x=\"-4\" y=\"-1\" width=\"8\" height=\"2\" fill=\"#64748b\">\n              {tourne ? <Anim attributeName=\"transform\" type=\"rotate\" values=\"0;45;90;135\" dur=\"0.4s\" repeatCount=\"indefinite\" /> : null}\n            </rect>\n          </g>\n        </g>\n      ))}\n      {allumee ? (\n        <rect x={x + 2} y={y + 35} width=\"18\" height=\"2\" fill={couleur}>\n          {tourne ? <Anim attributeName=\"opacity\" values=\"1;0.5;1\" dur=\"2.5s\" repeatCount=\"indefinite\" /> : null}\n        </rect>\n      ) : null}\n    </g>\n  )\n}\n\n/** Le clavier mécanique : ses touches arc-en-ciel n'ondulent que quand l'agent tape. */\nexport function Clavier({ x, y, allume, ondule }: { x: number; y: number; allume: boolean; ondule: boolean }) {\n  const arc = ['#ef4444', '#f59e0b', '#22c55e', '#22d3ee', '#a855f7']\n  return (\n    <g>\n      <rect x={x} y={y} width=\"36\" height=\"9\" fill=\"#111827\" />\n      {arc.map((c, i) => (\n        <rect key={c} x={x + 2 + i * 7} y={y + 2} width=\"6\" height=\"5\" fill={allume ? c : '#374151'} opacity=\"0.85\">\n          {ondule ? (\n            <Anim\n              attributeName=\"fill\"\n              values={[...arc.slice(i), ...arc.slice(0, i), arc[i]].join(';')}\n              dur=\"2.5s\"\n              repeatCount=\"indefinite\"\n            />\n          ) : null}\n        </rect>\n      ))}\n    </g>\n  )\n}\n\n/** Le fauteuil gamer du chef, derrière son bureau : noir et laiton, le dossier haut au-dessus de sa tête. */\nexport function Fauteuil({ x, y }: { x: number; y: number }) {\n  return (\n    <g>\n      <rect x={x - 16} y={y - 34} width=\"32\" height=\"42\" fill=\"#0b0d10\" />\n      <rect x={x - 13} y={y - 31} width=\"26\" height=\"36\" fill=\"#1f2937\" />\n      <rect x={x - 11} y={y - 31} width=\"4\" height=\"36\" fill=\"#e3b95a\" />\n      <rect x={x + 7} y={y - 31} width=\"4\" height=\"36\" fill=\"#e3b95a\" />\n      <rect x={x - 6} y={y - 28} width=\"12\" height=\"5\" fill=\"#111827\" />\n      <rect x={x - 20} y={y - 8} width=\"6\" height=\"16\" fill=\"#0b0d10\" />\n      <rect x={x + 14} y={y - 8} width=\"6\" height=\"16\" fill=\"#0b0d10\" />\n    </g>\n  )\n}\n\n/** Le dossier du fauteuil gamer, vu de dos : noir, deux bandes de couleur, l'appui-tête. */\nexport function Dossier({ x, y, couleur }: { x: number; y: number; couleur: string }) {\n  return (\n    <g>\n      <rect x={x - 13} y={y + 2} width=\"26\" height=\"10\" fill=\"#0b0d10\" />\n      <rect x={x - 11} y={y + 4} width=\"22\" height=\"7\" fill=\"#1f2937\" />\n      <rect x={x - 9} y={y + 4} width=\"3\" height=\"7\" fill={couleur} />\n      <rect x={x + 6} y={y + 4} width=\"3\" height=\"7\" fill={couleur} />\n      <rect x={x - 2} y={y + 12} width=\"4\" height=\"3\" fill=\"#111827\" />\n      <rect x={x - 10} y={y + 15} width=\"20\" height=\"3\" fill=\"#0b0d10\" />\n    </g>\n  )\n}\n\n/* ——— Les bonshommes ——— */\n\nexport const CHEMISES = [\n  '#3b82f6',\n  '#10b981',\n  '#e11d48',\n  '#8b5cf6',\n  '#f59e0b',\n  '#0ea5e9',\n  '#14b8a6',\n  '#f97316',\n  '#6366f1',\n  '#84cc16',\n  '#ec4899',\n  '#475569',\n]\nexport const CHEVEUX = ['#2a1d14', '#5a3a22', '#c8a165', '#111111', '#8a4b2a', '#e5e5e5', '#b45309']\nexport const PEAUX = ['#f1c9a5', '#d9a57a', '#a86f48', '#7a4a2c', '#f5d6bf']\nexport const PANTALONS = ['#334155', '#1e3a8a', '#44403c', '#3f3f46', '#365314']\n\n/** Le nombre de cet agent dans ce thème : chaque thème a sa graine, pour des tenues qui changent d'un thème à l'autre. */\nexport const hachage = (id: string) => hacher(id, 7, 31)\n\n/** Un agent sur deux travaille casque sur les oreilles ; le chef, jamais. */\nexport const porteCasque = (id: string) => id !== CHEF_ID && !estInvite(id) && ((hachage(id) >>> 16) & 1) === 1\n\n/** Chacun garde son sweat et sa coupe d'une visite à l'autre. */\nexport function teint(id: string): Record<string, string> {\n  if (id === CHEF_ID) {\n    return { h: '#9ca3af', s: '#f1c9a5', e: '#1c1917', c: '#111827', C: '#1f2937', t: '#e3b95a', k: '#111827', p: '#1f2937', b: '#e5e7eb' }\n  }\n  // Le livreur, en uniforme marron et casquette.\n  if (id.startsWith('livreur:')) {\n    return { h: '#5b3a1a', s: '#d9a57a', e: '#1c1917', c: '#7c4a1e', C: '#5b3a1a', t: '#facc15', k: '#111827', p: '#5b3a1a', b: '#111827' }\n  }\n  const h = hachage(id)\n  const sweat = CHEMISES[h % CHEMISES.length]\n  return {\n    h: CHEVEUX[(h >>> 4) % CHEVEUX.length],\n    s: PEAUX[(h >>> 8) % PEAUX.length],\n    e: '#1c1917',\n    c: sweat,\n    C: `color-mix(in oklab, ${sweat} 70%, black)`,\n    t: '#f8fafc',\n    k: '#111827',\n    p: PANTALONS[(h >>> 12) % PANTALONS.length],\n    b: '#f8fafc',\n  }\n}\n\nexport function Assise({ x, y, couleur }: { x: number; y: number; couleur: string }) {\n  return (\n    <g>\n      <rect x={x - 11} y={y - 12} width=\"22\" height=\"12\" fill=\"#1f2937\" />\n      <rect x={x - 11} y={y - 12} width=\"3\" height=\"12\" fill={couleur} />\n      <rect x={x + 8} y={y - 12} width=\"3\" height=\"12\" fill={couleur} />\n    </g>\n  )\n}\n\n/** Le bureau d'un agent : plateau blanc, bande LED à la couleur de l'état, écran ultra-large, tour, clavier, tasse. */\nfunction BureauGeek({ cx, dy, x, dessus, statut, accent, bientot, emoji }: PropsBureau) {\n  const allume = statut !== 'absent'\n  return (\n    <g>\n      <PlateauGeek x={x} dy={dy} dessus={dessus} statut={statut} />\n      <Ecran cx={cx - 4} dy={dy} statut={statut} bientot={bientot} />\n      <Tour\n        x={x + 94}\n        y={dy - 18}\n        couleur={statut === 'en-echec' ? '#ef4444' : accent}\n        allumee={allume}\n        fume={statut === 'en-echec'}\n        tourne={statut === 'au-travail'}\n      />\n      <Clavier x={cx - 22} y={dy + 22} allume={allume} ondule={statut === 'au-travail'} />\n      <rect x={cx + 18} y={dy + 20} width=\"12\" height=\"13\" fill=\"#111827\" />\n      <rect x={cx + 22} y={dy + 23} width=\"4\" height=\"6\" fill=\"#e5e7eb\" />\n      {/* La tasse et l'emoji de l'agent, en sticker sur le plateau. */}\n      <rect x={x + 8} y={dy + 22} width=\"8\" height=\"9\" fill=\"#111827\" />\n      <rect x={x + 16} y={dy + 24} width=\"3\" height=\"4\" fill=\"#111827\" />\n      <rect x={x + 9} y={dy + 23} width=\"6\" height=\"2\" fill=\"#6b3a1e\" />\n      <text x={x + 12} y={dy + 13} fontSize=\"10\" textAnchor=\"middle\">\n        {emoji}\n      </text>\n    </g>\n  )\n}\n\n/** Le bureau du chef : deux écrans vus de dos sur le côté, le portable, le téléphone. */\nfunction BureauChefGeek({ dy, x, dessus, statut }: PropsBureau) {\n  return (\n    <g>\n      <PlateauGeek x={x} dy={dy} dessus={dessus} statut={statut} />\n      <rect x={x + 6} y={dy - 12} width=\"30\" height=\"22\" fill=\"#1f2937\" />\n      <rect x={x + 8} y={dy - 10} width=\"26\" height=\"18\" fill=\"#374151\" />\n      <rect x={x + 18} y={dy + 10} width=\"6\" height=\"5\" fill=\"#111827\" />\n      <rect x={x + 84} y={dy + 4} width=\"26\" height=\"18\" fill=\"#d1d5db\" />\n      <rect x={x + 86} y={dy + 6} width=\"22\" height=\"13\" fill=\"#9ca3af\" />\n      <rect x={x + 95} y={dy + 11} width=\"4\" height=\"3\" fill=\"#f9fafb\" />\n      <rect x={x + 88} y={dy + 26} width=\"10\" height=\"7\" fill=\"#111827\" />\n    </g>\n  )\n}\n\n/** Le plateau blanc, sa tranche, et la bande LED dessous, à la couleur de l'état. */\nfunction PlateauGeek({ x, dy, dessus, statut }: { x: number; dy: number; dessus: number; statut: Statut }) {\n  return (\n    <>\n      <rect x={x} y={dy} width={BUREAU.largeur} height={dessus} fill=\"#e5e7eb\" />\n      <rect x={x} y={dy} width={BUREAU.largeur} height=\"3\" fill=\"#f9fafb\" />\n      <rect x={x} y={dy + dessus} width={BUREAU.largeur} height=\"12\" fill=\"#c4c8cf\" />\n      <rect x={x} y={dy + dessus + 10} width={BUREAU.largeur} height=\"2\" fill={LAMPE_PLAN[statut]}>\n        {statut === 'au-travail' || statut === 'en-echec' ? (\n          <Anim attributeName=\"opacity\" values=\"1;0.4;1\" dur=\"1.2s\" repeatCount=\"indefinite\" />\n        ) : null}\n      </rect>\n      <rect x={x + 3} y={dy + dessus + 12} width=\"4\" height=\"7\" fill=\"#111827\" />\n      <rect x={x + BUREAU.largeur - 7} y={dy + dessus + 12} width=\"4\" height=\"7\" fill=\"#111827\" />\n    </>\n  )\n}\n\n/* Les lumières du décor dans la nuit : l'enseigne, la baie serveur, les bornes, la télé, la cuisine. */\nconst LUMIERES_GEEK: Array<Lumiere> = [\n  { x: 222, y: 36, r: 60, couleur: ACCENT },\n  { x: 59, y: 60, r: 40, couleur: '#22c55e' },\n  { x: 373, y: 50, r: 50, couleur: '#a855f7' },\n  { x: 417, y: 50, r: 45, couleur: '#22d3ee' },\n  { x: 516, y: 44, r: 80, couleur: '#38bdf8' },\n  { x: 768, y: 40, r: 40 },\n  { x: 882, y: 56, r: 45, couleur: '#f87171' },\n  { x: 949, y: 56, r: 55, couleur: '#e0f2fe' },\n]\n\nexport const THEME_GEEK: Theme = {\n  coins: COINS_GEEK,\n  coinCafe: 'cafe',\n  fond: '#120e0b',\n  Decor,\n  Bureau: BureauGeek,\n  BureauChef: BureauChefGeek,\n  plaque: '#1f2937',\n  Assise,\n  Dossier,\n  Fauteuil,\n  tenue: teint,\n  casque: porteCasque,\n  CafeQuiCoule,\n  lumieres: LUMIERES_GEEK,\n  // La tour RGB de chaque poste éclaire de la couleur de son agent.\n  lumierePoste: (cx, dy, accent) => [{ x: cx + 45, y: dy + 2, r: 34, couleur: accent }],\n}\n",
      "type": "registry:component",
      "target": "components/pixel-openspace/themes/geek.tsx"
    },
    {
      "path": "registry/pixel-openspace/themes/eighties.tsx",
      "content": "'use client'\n\n/*\n * Le thème années 80 : un bureau d'entreprise vers 1986. Boiseries aux\n * murs, moquette orange, écrans cathodiques beiges au texte vert phosphore,\n * téléphones à cadran et bannettes à courrier. Le chef a son ordinateur\n * central à bandes magnétiques et sa lampe de banquier ; la salle de pause a\n * ses bornes d'arcade, sa télé à pieds qui diffuse la mire, son canapé en\n * velours côtelé et sa photocopieuse ; la cuisine, sa cafetière à filtre et\n * son frigo vert avocat. Costumes, cravates, pulls de couleur, et un Walkman\n * sur les oreilles de certains.\n */\n\nimport { BUREAU, CHEF_ID, estInvite, type CoinDePause, type Statut } from '../engine'\nimport { Anim, Cliquable, MUR_SALLE, Neon, Plante, etapes, useTextes, hacher, type Lumiere } from '../primitives'\nimport type { PropsBureau, PropsDecor, PropsSiege, Theme } from '../theme'\nimport { Arcade, BorneCasseBriques } from './geek'\nimport type { SceneObject } from '../types'\n\n/* Les coins de pause : la même géographie que le thème geek, d'autres objets. */\nconst COINS_80: Array<CoinDePause> = [\n  { nom: 'cafe', x: 768, y: 100 },\n  { nom: 'fontaine', x: 881, y: 104 },\n  { nom: 'frigo', x: 950, y: 108 },\n  { nom: 'arcade', x: 372, y: 112 },\n  { nom: 'arcade-2', x: 415, y: 112 },\n  { nom: 'photocopieuse', x: 636, y: 108 },\n  // Sur le canapé, face à la télé : de dos.\n  { nom: 'canape-gauche', x: 482, y: 146, assis: true },\n  { nom: 'canape-droite', x: 538, y: 146, assis: true },\n]\n\nconst BOIS = { arete: '#2b1a0e', face: '#7b4a24', rainure: '#6a3d1c', plinthe: '#3b2412' }\nconst PHOSPHORE = '#4ade80'\n\n/** Un mur lambrissé, vu de biais : son arête, ses lames de bois, sa plinthe. */\nfunction Lambris({ x, largeur, y = 20, hauteur = 32 }: { x: number; largeur: number; y?: number; hauteur?: number }) {\n  return (\n    <>\n      <rect x={x} y={y - 6} width={largeur} height=\"6\" fill={BOIS.arete} />\n      <rect x={x} y={y} width={largeur} height={hauteur} fill={BOIS.face} />\n      {Array.from({ length: Math.floor(largeur / 12) }, (_, i) => (\n        <rect key={i} x={x + 6 + i * 12} y={y} width=\"1\" height={hauteur} fill={BOIS.rainure} />\n      ))}\n      <rect x={x} y={y + hauteur} width={largeur} height=\"4\" fill={BOIS.plinthe} />\n    </>\n  )\n}\n\nfunction Decor({ hauteur, titre, liens, aller, jouer }: PropsDecor) {\n  const t = useTextes()\n  const bas = hauteur - 20\n  const lien = (objet: SceneObject) => ({ titre: t.objects[objet], faire: () => aller(objet), actif: Boolean(liens[objet]) })\n  return (\n    <>\n      <defs>\n        <pattern id=\"moquette-80\" width=\"24\" height=\"24\" patternUnits=\"userSpaceOnUse\">\n          <rect width=\"24\" height=\"24\" fill=\"#6b4a2b\" />\n          <rect x=\"2\" y=\"2\" width=\"8\" height=\"8\" fill=\"#76532f\" />\n          <rect x=\"14\" y=\"14\" width=\"8\" height=\"8\" fill=\"#76532f\" />\n          <rect x=\"11\" width=\"2\" height=\"24\" fill=\"#5e3f23\" />\n          <rect y=\"11\" width=\"24\" height=\"2\" fill=\"#5e3f23\" />\n        </pattern>\n        <pattern id=\"moquette-chef-80\" width=\"10\" height=\"10\" patternUnits=\"userSpaceOnUse\">\n          <rect width=\"10\" height=\"10\" fill=\"#5c2a1e\" />\n          <rect x=\"2\" y=\"2\" width=\"2\" height=\"2\" fill=\"#6b3324\" />\n          <rect x=\"7\" y=\"6\" width=\"2\" height=\"2\" fill=\"#4f2318\" />\n        </pattern>\n        <pattern id=\"shag-80\" width=\"8\" height=\"8\" patternUnits=\"userSpaceOnUse\">\n          <rect width=\"8\" height=\"8\" fill=\"#b45309\" />\n          <rect x=\"1\" y=\"1\" width=\"2\" height=\"2\" fill=\"#c2610f\" />\n          <rect x=\"5\" y=\"4\" width=\"2\" height=\"2\" fill=\"#9a4708\" />\n          <rect x=\"3\" y=\"6\" width=\"1\" height=\"1\" fill=\"#d97706\" />\n        </pattern>\n        <pattern id=\"lino-80\" width=\"24\" height=\"24\" patternUnits=\"userSpaceOnUse\">\n          <rect width=\"24\" height=\"24\" fill=\"#e7d8b0\" />\n          <rect width=\"12\" height=\"12\" fill=\"#c9b48a\" />\n          <rect x=\"12\" y=\"12\" width=\"12\" height=\"12\" fill=\"#c9b48a\" />\n        </pattern>\n        <pattern id=\"faience-80\" width=\"10\" height=\"10\" patternUnits=\"userSpaceOnUse\">\n          <rect width=\"10\" height=\"10\" fill=\"#e9d38a\" />\n          <rect width=\"10\" height=\"1\" fill=\"#d4ba6a\" />\n          <rect width=\"1\" height=\"10\" fill=\"#d4ba6a\" />\n        </pattern>\n      </defs>\n\n      {/* Les sols : moquette bordeaux chez le chef, orange à poils longs en salle de pause, lino en damier en cuisine. */}\n      <rect x=\"20\" y=\"56\" width=\"310\" height=\"142\" fill=\"url(#moquette-chef-80)\" />\n      <rect x=\"338\" y=\"56\" width=\"344\" height=\"142\" fill=\"url(#shag-80)\" />\n      <rect x=\"690\" y=\"56\" width=\"290\" height=\"142\" fill=\"url(#lino-80)\" />\n      <rect x=\"20\" y=\"198\" width=\"960\" height={bas - 198} fill=\"url(#moquette-80)\" />\n\n      {/* Les murs du fond : lambris, et faïence jaune en cuisine. */}\n      <Lambris x={20} largeur={310} />\n      <Lambris x={338} largeur={344} />\n      <rect x=\"690\" y=\"14\" width=\"290\" height=\"6\" fill={BOIS.arete} />\n      <rect x=\"690\" y=\"20\" width=\"290\" height=\"36\" fill=\"url(#faience-80)\" />\n\n      {/* Le bureau du chef : l'ordinateur central, la plaque en laiton, le ficus. */}\n      <Cliquable {...lien('server-rack')}>\n        <OrdinateurCentral x={34} />\n      </Cliquable>\n      <rect x=\"160\" y=\"28\" width=\"124\" height=\"18\" fill=\"#7a5a1e\" />\n      <rect x=\"162\" y=\"30\" width=\"120\" height=\"14\" fill=\"#d4a93b\" />\n      <text x=\"222\" y=\"41\" fontSize=\"9\" textAnchor=\"middle\" fill=\"#3b2a0a\">\n        {t.leadOffice}\n      </text>\n      <Plante x={300} y={62} />\n\n      {/* La salle de pause : les bornes, la télé à pieds et son magnétoscope, le canapé, la photocopieuse. */}\n      <Cliquable titre={t.objects.snake} faire={() => jouer('snake')}>\n        <Arcade x={352} />\n      </Cliquable>\n      <Cliquable titre={t.objects.breakout} faire={() => jouer('casse-briques')}>\n        <BorneCasseBriques x={396} />\n      </Cliquable>\n      <Cliquable {...lien('tv')}>\n        <TeleCathodique x={462} />\n      </Cliquable>\n      <Canape x={456} y={130} />\n      <Cliquable {...lien('workbench')}>\n        <Photocopieuse x={596} />\n      </Cliquable>\n      <Plante x={346} y={160} />\n      <Plante x={650} y={160} />\n\n      {/* La cuisine : plan de travail stratifié, cafetière à filtre, fontaine, frigo vert avocat, téléphone mural, table orange. */}\n      <rect x=\"730\" y=\"44\" width=\"122\" height=\"24\" fill=\"#b08b5b\" />\n      <rect x=\"730\" y=\"44\" width=\"122\" height=\"3\" fill=\"#c49e6c\" />\n      <rect x=\"730\" y=\"68\" width=\"122\" height=\"18\" fill=\"#8b5a2b\" />\n      {[734, 774, 814].map((x) => (\n        <g key={x}>\n          <rect x={x} y=\"71\" width=\"34\" height=\"12\" fill=\"#7a4d24\" />\n          <rect x={x + 14} y=\"76\" width=\"6\" height=\"2\" fill=\"#d4a93b\" />\n        </g>\n      ))}\n      <Cafetiere x={748} />\n      <rect x=\"814\" y=\"50\" width=\"34\" height=\"12\" fill=\"#a8a29e\" />\n      <rect x=\"818\" y=\"53\" width=\"26\" height=\"6\" fill=\"#d6d3d1\" />\n      <rect x=\"700\" y=\"24\" width=\"12\" height=\"22\" fill=\"#facc15\" />\n      <rect x=\"702\" y=\"28\" width=\"8\" height=\"8\" fill=\"#ca8a04\" />\n      <rect x=\"704\" y=\"38\" width=\"4\" height=\"6\" fill=\"#1f2937\" />\n      <Cliquable {...lien('vending-machine')}>\n        <Fontaine x={862} />\n      </Cliquable>\n      <Cliquable {...lien('fridge')}>\n        <FrigoAvocat x={922} />\n      </Cliquable>\n      <rect x=\"818\" y=\"132\" width=\"8\" height=\"34\" fill=\"#78350f\" />\n      <circle cx=\"822\" cy=\"131\" r=\"20\" fill=\"#9a3412\" shapeRendering=\"auto\" />\n      <circle cx=\"822\" cy=\"128\" r=\"20\" fill=\"#ea580c\" shapeRendering=\"auto\" />\n      {[784, 850].map((x) => (\n        <g key={x}>\n          <rect x={x} y=\"140\" width=\"12\" height=\"10\" fill=\"#f97316\" />\n          <rect x={x + 4} y=\"150\" width=\"4\" height=\"12\" fill=\"#57534e\" />\n        </g>\n      ))}\n      <Plante x={700} y={160} />\n\n      {/* La grande salle : les cartons de fournitures, les plantes. */}\n      <Cartons80 x={56} y={bas - 44} lien={lien} />\n      <Plante x={946} y={bas - 32} />\n\n      {/* Le mur de la grande salle, lambrissé, percé de ses trois entrées. */}\n      {MUR_SALLE.map(([de, a]) => (\n        <g key={de}>\n          <Lambris x={de} largeur={a - de} y={204} hauteur={38} />\n        </g>\n      ))}\n      <Neon x={695} y={226} texte={titre} couleur=\"#fb923c\" taille={titre.length > 14 ? 7 : 9} />\n      <Porte80 bas={bas} />\n      <rect x=\"330\" y=\"14\" width=\"8\" height=\"190\" fill={BOIS.arete} />\n      <rect x=\"332\" y=\"14\" width=\"4\" height=\"190\" fill={BOIS.rainure} />\n      <rect x=\"682\" y=\"14\" width=\"8\" height=\"190\" fill={BOIS.arete} />\n      <rect x=\"12\" y=\"14\" width=\"8\" height={bas - 6} fill={BOIS.arete} />\n      <rect x=\"980\" y=\"14\" width=\"8\" height={bas - 6} fill={BOIS.arete} />\n      <rect x=\"12\" y=\"6\" width=\"976\" height=\"8\" fill={BOIS.arete} />\n      <rect x=\"12\" y={bas} width=\"976\" height=\"10\" fill={BOIS.arete} />\n    </>\n  )\n}\n\n/** L'ordinateur central : une armoire beige, deux bobines de bande qui tournent, un pupitre de voyants. */\nfunction OrdinateurCentral({ x }: { x: number }) {\n  return (\n    <g>\n      <rect x={x} y=\"20\" width=\"54\" height=\"86\" fill=\"#a8a092\" />\n      <rect x={x + 2} y=\"22\" width=\"50\" height=\"82\" fill=\"#d6cfbd\" />\n      <rect x={x + 5} y=\"26\" width=\"44\" height=\"40\" fill=\"#3f3a30\" />\n      {[x + 16, x + 38].map((cx) => (\n        <g key={cx}>\n          <circle cx={cx} cy=\"40\" r=\"9\" fill=\"#1c1917\" shapeRendering=\"auto\" />\n          <circle cx={cx} cy=\"40\" r=\"3\" fill=\"#a8a29e\" shapeRendering=\"auto\" />\n          <rect x={cx - 1} y=\"31\" width=\"2\" height=\"18\" fill=\"#57534e\">\n            <Anim\n              attributeName=\"transform\"\n              type=\"rotate\"\n              values={`0 ${cx} 40;60 ${cx} 40;120 ${cx} 40`}\n              dur=\"0.9s\"\n              repeatCount=\"indefinite\"\n            />\n          </rect>\n        </g>\n      ))}\n      <rect x={x + 8} y=\"52\" width=\"38\" height=\"10\" fill=\"#57534e\" />\n      {Array.from({ length: 6 }, (_, i) => (\n        <rect key={i} x={x + 8 + i * 7} y=\"72\" width=\"4\" height=\"4\" fill={['#ef4444', '#facc15', '#22c55e'][i % 3]}>\n          <Anim attributeName=\"opacity\" values=\"1;0.2;1\" dur={`${0.6 + (i % 4) * 0.35}s`} repeatCount=\"indefinite\" />\n        </rect>\n      ))}\n      <rect x={x + 6} y=\"82\" width=\"42\" height=\"18\" fill=\"#c9c1ad\" />\n      <rect x={x + 10} y=\"86\" width=\"34\" height=\"2\" fill=\"#a8a092\" />\n      <rect x={x + 10} y=\"92\" width=\"34\" height=\"2\" fill=\"#a8a092\" />\n    </g>\n  )\n}\n\n/** La télé à pieds en bois : un écran bombé qui diffuse la mire, deux boutons, un magnétoscope dessous. */\nfunction TeleCathodique({ x }: { x: number }) {\n  const mire = ['#e5e7eb', '#facc15', '#22d3ee', '#22c55e', '#d946ef', '#ef4444', '#2563eb']\n  return (\n    <g>\n      <rect x={x} y=\"22\" width=\"96\" height=\"54\" fill=\"#5b3415\" />\n      <rect x={x + 2} y=\"24\" width=\"92\" height=\"50\" fill=\"#7c4a1f\" />\n      <rect x={x + 6} y=\"28\" width=\"64\" height=\"42\" fill=\"#111827\" />\n      {mire.map((c, i) => (\n        <rect key={c} x={x + 9 + i * 8.4} y=\"31\" width=\"8.4\" height=\"28\" fill={c} />\n      ))}\n      <rect x={x + 9} y=\"59\" width=\"58.8\" height=\"8\" fill=\"#1e293b\" />\n      <rect x={x + 9} y=\"31\" width=\"58.8\" height=\"36\" fill=\"white\" opacity=\"0\">\n        <Anim attributeName=\"opacity\" values=\"0;0;0.15;0\" dur=\"3s\" repeatCount=\"indefinite\" />\n      </rect>\n      <circle cx={x + 82} cy=\"38\" r=\"4\" fill=\"#d6d3d1\" shapeRendering=\"auto\" />\n      <circle cx={x + 82} cy=\"52\" r=\"4\" fill=\"#d6d3d1\" shapeRendering=\"auto\" />\n      <rect x={x + 76} y=\"62\" width=\"12\" height=\"2\" fill=\"#a8a29e\" />\n      <rect x={x + 10} y=\"76\" width=\"4\" height=\"12\" fill=\"#3b2412\" />\n      <rect x={x + 82} y=\"76\" width=\"4\" height=\"12\" fill=\"#3b2412\" />\n      <rect x={x + 22} y=\"78\" width=\"52\" height=\"8\" fill=\"#1c1917\" />\n      <rect x={x + 26} y=\"81\" width=\"18\" height=\"2\" fill=\"#57534e\" />\n      <rect x={x + 62} y=\"80\" width=\"6\" height=\"3\" fill=\"#ef4444\">\n        <Anim attributeName=\"opacity\" values=\"1;0.3\" dur=\"1s\" repeatCount=\"indefinite\" />\n      </rect>\n    </g>\n  )\n}\n\n/** Le canapé en velours côtelé, vu de dos depuis la salle. */\nfunction Canape({ x, y }: { x: number; y: number }) {\n  return (\n    <g>\n      <rect x={x} y={y + 10} width=\"108\" height=\"26\" fill=\"#5c3a1f\" />\n      <rect x={x + 6} y={y} width=\"96\" height=\"22\" fill=\"#7c4a2a\" />\n      {Array.from({ length: 12 }, (_, i) => (\n        <rect key={i} x={x + 8 + i * 8} y={y + 2} width=\"2\" height=\"18\" fill=\"#6b3d22\" />\n      ))}\n      <rect x={x} y={y + 4} width=\"10\" height=\"32\" fill=\"#6b3d22\" />\n      <rect x={x + 98} y={y + 4} width=\"10\" height=\"32\" fill=\"#6b3d22\" />\n    </g>\n  )\n}\n\n/** La photocopieuse : une grosse caisse beige, son couvercle, son pupitre et sa pile de feuilles. */\nfunction Photocopieuse({ x }: { x: number }) {\n  return (\n    <g>\n      <rect x={x} y=\"40\" width=\"78\" height=\"52\" fill=\"#a8a092\" />\n      <rect x={x + 2} y=\"42\" width=\"74\" height=\"48\" fill=\"#d6cfbd\" />\n      <rect x={x + 4} y=\"34\" width=\"52\" height=\"10\" fill=\"#57534e\" />\n      <rect x={x + 58} y=\"44\" width=\"16\" height=\"10\" fill=\"#3f3a30\" />\n      <rect x={x + 60} y=\"46\" width=\"5\" height=\"3\" fill=\"#22c55e\">\n        <Anim attributeName=\"opacity\" values=\"1;0.3;1\" dur=\"1.4s\" repeatCount=\"indefinite\" />\n      </rect>\n      <rect x={x + 6} y=\"58\" width=\"66\" height=\"4\" fill=\"#a8a092\" />\n      <rect x={x + 6} y=\"66\" width=\"66\" height=\"4\" fill=\"#a8a092\" />\n      <rect x={x + 78} y=\"60\" width=\"10\" height=\"12\" fill=\"#f5f5f4\" />\n      <rect x={x + 4} y=\"90\" width=\"6\" height=\"6\" fill=\"#3f3a30\" />\n      <rect x={x + 68} y=\"90\" width=\"6\" height=\"6\" fill=\"#3f3a30\" />\n    </g>\n  )\n}\n\n/** La cafetière à filtre : son réservoir, son filtre, sa verseuse où le café attend. */\nfunction Cafetiere({ x }: { x: number }) {\n  return (\n    <g>\n      <rect x={x} y=\"22\" width=\"40\" height=\"38\" fill=\"#1c1917\" />\n      <rect x={x + 2} y=\"24\" width=\"36\" height=\"10\" fill=\"#44403c\" />\n      <rect x={x + 30} y=\"27\" width=\"5\" height=\"4\" fill=\"#ef4444\" />\n      <rect x={x + 10} y=\"40\" width=\"20\" height=\"16\" fill=\"#e0f2fe\" opacity=\"0.6\" />\n      <rect x={x + 11} y=\"48\" width=\"18\" height=\"8\" fill=\"#5b2c0f\" />\n      <rect x={x + 30} y=\"44\" width=\"4\" height=\"8\" fill=\"#1c1917\" />\n    </g>\n  )\n}\n\n/** Le café qui coule dans la verseuse, quand quelqu'un attend devant. */\nfunction CafeQuiCoule80() {\n  return (\n    <g pointerEvents=\"none\">\n      <rect x=\"767\" y=\"36\" width=\"2\" height=\"12\" fill=\"#5b2c0f\">\n        <Anim attributeName=\"opacity\" values=\"1;0.4;1\" dur=\"0.5s\" repeatCount=\"indefinite\" />\n      </rect>\n    </g>\n  )\n}\n\n/** La fontaine à eau, sa grosse bonbonne bleue retournée et ses gobelets. */\nfunction Fontaine({ x }: { x: number }) {\n  return (\n    <g>\n      <rect x={x + 4} y=\"14\" width=\"30\" height=\"30\" fill=\"#60a5fa\" opacity=\"0.85\" />\n      <rect x={x + 8} y=\"18\" width=\"6\" height=\"20\" fill=\"#bfdbfe\" opacity=\"0.8\" />\n      <rect x={x + 12} y=\"10\" width=\"14\" height=\"6\" fill=\"#3b82f6\" />\n      <rect x={x} y=\"44\" width=\"38\" height=\"50\" fill=\"#f5f5f4\" />\n      <rect x={x} y=\"44\" width=\"38\" height=\"4\" fill=\"#d6d3d1\" />\n      <rect x={x + 8} y=\"54\" width=\"6\" height=\"5\" fill=\"#3b82f6\" />\n      <rect x={x + 24} y=\"54\" width=\"6\" height=\"5\" fill=\"#ef4444\" />\n      <rect x={x + 6} y=\"70\" width=\"26\" height=\"4\" fill=\"#a8a29e\" />\n      <rect x={x + 40} y=\"50\" width=\"6\" height=\"22\" fill=\"#e5e7eb\" />\n    </g>\n  )\n}\n\n/** Le frigo vert avocat, poignées chromées. */\nfunction FrigoAvocat({ x }: { x: number }) {\n  return (\n    <g>\n      <rect x={x} y=\"16\" width=\"54\" height=\"82\" fill=\"#4d6b1c\" />\n      <rect x={x + 2} y=\"18\" width=\"50\" height=\"78\" fill=\"#6b8e23\" />\n      <rect x={x + 2} y=\"46\" width=\"50\" height=\"2\" fill=\"#4d6b1c\" />\n      <rect x={x + 42} y=\"26\" width=\"4\" height=\"14\" fill=\"#d6d3d1\" />\n      <rect x={x + 42} y=\"54\" width=\"4\" height=\"26\" fill=\"#d6d3d1\" />\n      <rect x={x + 8} y=\"24\" width=\"12\" height=\"8\" fill=\"#facc15\" />\n    </g>\n  )\n}\n\n/** Les cartons de fournitures : processeurs 8086, cartes EGA, mémoire 64 Ko. */\nfunction Cartons80({\n  x,\n  y,\n  lien,\n}: {\n  x: number\n  y: number\n  lien: (objet: SceneObject) => { titre: string; faire: () => void; actif: boolean }\n}) {\n  return (\n    <g>\n      <Cliquable {...lien('cpu-box')}>\n        <rect x={x} y={y + 14} width=\"40\" height=\"26\" fill=\"#b08b5b\" />\n        <rect x={x} y={y + 14} width=\"40\" height=\"4\" fill=\"#c49e6c\" />\n        <text x={x + 20} y={y + 32} fontSize=\"7\" textAnchor=\"middle\" fill=\"#3b2412\">\n          8086\n        </text>\n      </Cliquable>\n      <Cliquable {...lien('gpu-box')}>\n        <rect x={x + 6} y={y} width=\"30\" height=\"16\" fill=\"#1e3a8a\" />\n        <rect x={x + 6} y={y} width=\"30\" height=\"3\" fill=\"#2563eb\" />\n        <text x={x + 21} y={y + 12} fontSize=\"6\" textAnchor=\"middle\" fill=\"#eff6ff\">\n          EGA\n        </text>\n      </Cliquable>\n      <Cliquable {...lien('ram-box')}>\n        <rect x={x + 44} y={y + 22} width=\"22\" height=\"18\" fill=\"#991b1b\" />\n        <text x={x + 55} y={y + 34} fontSize=\"6\" textAnchor=\"middle\" fill=\"#fef2f2\">\n          64K\n        </text>\n      </Cliquable>\n    </g>\n  )\n}\n\n/** La porte d'entrée en bois, sa poignée en laiton, son paillasson. */\nfunction Porte80({ bas }: { bas: number }) {\n  const t = useTextes()\n  return (\n    <g pointerEvents=\"none\">\n      <rect x=\"472\" y={bas - 14} width=\"56\" height=\"12\" fill=\"#7c2d12\" />\n      <rect x=\"476\" y={bas - 12} width=\"48\" height=\"8\" fill=\"#9a3412\" />\n      <text x=\"500\" y={bas - 6} fontSize=\"5.5\" textAnchor=\"middle\" fill=\"#fed7aa\">\n        {t.welcome}\n      </text>\n      <rect x=\"470\" y={bas} width=\"60\" height=\"10\" fill=\"#7b4a24\" />\n      <rect x=\"499\" y={bas} width=\"2\" height=\"10\" fill={BOIS.arete} />\n      <rect x=\"490\" y={bas + 3} width=\"3\" height=\"3\" fill=\"#d4a93b\" />\n      <rect x=\"507\" y={bas + 3} width=\"3\" height=\"3\" fill=\"#d4a93b\" />\n    </g>\n  )\n}\n\n/* ——— Les postes ——— */\n\n/** Le plateau en bois veiné, sa tranche, ses pieds pleins. */\nfunction PlateauBois({ x, dy, dessus, fonce = false }: { x: number; dy: number; dessus: number; fonce?: boolean }) {\n  const [plateau, veine, tranche] = fonce ? ['#5a2e1a', '#4a2414', '#3d1d10'] : ['#9a6235', '#8a5530', '#74451f']\n  return (\n    <>\n      <rect x={x} y={dy} width={BUREAU.largeur} height={dessus} fill={plateau} />\n      <rect x={x} y={dy} width={BUREAU.largeur} height=\"3\" fill={fonce ? '#6b3a22' : '#ad7442'} />\n      {[10, 22, 31].map((v) => (\n        <rect key={v} x={x + 4} y={dy + v} width={BUREAU.largeur - 8} height=\"1\" fill={veine} />\n      ))}\n      <rect x={x} y={dy + dessus} width={BUREAU.largeur} height=\"12\" fill={tranche} />\n      <rect x={x + 2} y={dy + dessus + 12} width=\"8\" height=\"7\" fill={tranche} />\n      <rect x={x + BUREAU.largeur - 10} y={dy + dessus + 12} width=\"8\" height=\"7\" fill={tranche} />\n    </>\n  )\n}\n\n/**\n * L'écran cathodique beige et son unité centrale. Au repos, l'invite\n * `C:\\>` clignote ; au travail, des lignes vertes défilent ; en échec,\n * ERROR clignote en rouge et l'unité centrale fume ; absent, l'écran est\n * noir et un post-it y est collé.\n */\nfunction Cathodique({ cx, dy, statut, bientot }: { cx: number; dy: number; statut: Statut; bientot: number | null }) {\n  const t = useTextes()\n  const g = cx - 22\n  return (\n    <g>\n      {/* L'unité centrale, centrée sous l'écran : son lecteur de disquettes et son voyant. */}\n      <rect x={cx - 28} y={dy + 6} width=\"56\" height=\"12\" fill=\"#c9c1ad\" />\n      <rect x={cx - 28} y={dy + 6} width=\"56\" height=\"2\" fill=\"#ddd6c3\" />\n      <rect x={cx - 22} y={dy + 11} width=\"18\" height=\"2\" fill=\"#3f3a30\" />\n      <rect x={cx + 18} y={dy + 10} width=\"4\" height=\"3\" fill={statut === 'au-travail' ? '#22c55e' : '#57534e'}>\n        {statut === 'au-travail' ? <Anim attributeName=\"opacity\" values=\"1;0.2;1;1\" dur=\"0.4s\" repeatCount=\"indefinite\" /> : null}\n      </rect>\n      {/* L'écran, bombé dans son boîtier beige. */}\n      <rect x={g - 4} y={dy - 22} width=\"52\" height=\"30\" fill=\"#d8cfb8\" />\n      <rect x={g - 4} y={dy - 22} width=\"52\" height=\"2\" fill=\"#e8e0cc\" />\n      <rect x={g} y={dy - 19} width=\"44\" height=\"23\" fill={statut === 'absent' || statut === 'endormi' ? '#0a0a0a' : '#052e16'} />\n      {statut === 'endormi' ? null : statut === 'absent' ? (\n        <g transform={`rotate(-6 ${cx} ${dy - 8})`}>\n          <rect x={cx - 15} y={dy - 16} width=\"30\" height=\"15\" fill=\"#fde68a\" />\n          <rect x={cx - 15} y={dy - 16} width=\"30\" height=\"3\" fill=\"#fcd34d\" />\n          <text x={cx} y={dy - 4} fontSize=\"7\" textAnchor=\"middle\" fill=\"#78350f\">\n            {t.offNote}\n          </text>\n        </g>\n      ) : statut === 'en-echec' ? (\n        <text x={cx} y={dy - 4} fontSize=\"9\" textAnchor=\"middle\" fill=\"#ef4444\">\n          ERROR\n          <Anim attributeName=\"opacity\" values=\"1;0.2\" dur=\"0.8s\" repeatCount=\"indefinite\" />\n        </text>\n      ) : statut === 'au-travail' ? (\n        <g>\n          {[0, 1, 2, 3].map((i) => {\n            const w = [30, 18, 34, 12][i]\n            return (\n              <rect key={i} x={g + 3 + (i % 2) * 4} y={dy - 17 + i * 5} width={w} height=\"2\" fill={PHOSPHORE}>\n                <Anim attributeName=\"width\" values={`${etapes(2, w, 5)};${w}`} dur=\"1.6s\" begin={`${i * 0.4}s`} repeatCount=\"indefinite\" />\n              </rect>\n            )\n          })}\n        </g>\n      ) : (\n        <g>\n          <text x={g + 3} y={dy - 11} fontSize=\"7\" fill={PHOSPHORE}>\n            {'C:\\\\>'}\n          </text>\n          <rect x={g + 22} y={dy - 16} width=\"4\" height=\"6\" fill={PHOSPHORE}>\n            <Anim attributeName=\"opacity\" values=\"1;0\" dur=\"1s\" repeatCount=\"indefinite\" />\n          </rect>\n        </g>\n      )}\n      {bientot ? (\n        <text x={cx} y={dy + 2} fontSize=\"6.5\" textAnchor=\"middle\" fill={PHOSPHORE}>\n          {t.inMinutes(bientot)}\n          <Anim attributeName=\"opacity\" values=\"1;0.5;1\" dur=\"2s\" repeatCount=\"indefinite\" />\n        </text>\n      ) : null}\n      {/* En échec, l'unité centrale fume. */}\n      {statut === 'en-echec'\n        ? [0, 0.6, 1.2].map((debut, i) => (\n            <rect key={debut} x={cx + 10 + i * 3} y={dy + 4} width=\"6\" height=\"6\" fill=\"#9ca3af\" opacity=\"0\">\n              <Anim attributeName=\"y\" values={etapes(dy + 4, dy - 28, 6)} dur=\"1.8s\" begin={`${debut}s`} repeatCount=\"indefinite\" />\n              <Anim attributeName=\"opacity\" values={etapes(0.85, 0, 6)} dur=\"1.8s\" begin={`${debut}s`} repeatCount=\"indefinite\" />\n            </rect>\n          ))\n        : null}\n    </g>\n  )\n}\n\n/** Le bureau d'un agent : bois veiné, écran cathodique, clavier beige, téléphone à cadran, bannette à courrier. */\nfunction Bureau80({ cx, dy, x, dessus, statut, bientot, emoji }: PropsBureau) {\n  return (\n    <g>\n      <PlateauBois x={x} dy={dy} dessus={dessus} />\n      <Cathodique cx={cx - 4} dy={dy} statut={statut} bientot={bientot} />\n      {/* Le clavier beige, ses rangées de touches. */}\n      <rect x={cx - 24} y={dy + 22} width=\"40\" height=\"10\" fill=\"#d8cfb8\" />\n      <rect x={cx - 22} y={dy + 24} width=\"36\" height=\"2\" fill=\"#a8a092\" />\n      <rect x={cx - 22} y={dy + 28} width=\"36\" height=\"2\" fill=\"#a8a092\" />\n      {/* Le téléphone à cadran. */}\n      <rect x={x + 94} y={dy + 18} width=\"18\" height=\"12\" fill=\"#b91c1c\" />\n      <rect x={x + 92} y={dy + 14} width=\"22\" height=\"5\" fill=\"#7f1d1d\" />\n      <circle cx={x + 103} cy={dy + 24} r=\"4\" fill=\"#fef2f2\" shapeRendering=\"auto\" />\n      <circle cx={x + 103} cy={dy + 24} r=\"1.5\" fill=\"#7f1d1d\" shapeRendering=\"auto\" />\n      {/* La bannette à courrier, et sa pile de feuilles. */}\n      <rect x={x + 90} y={dy + 2} width=\"24\" height=\"10\" fill=\"#57534e\" />\n      <rect x={x + 92} y={dy + 3} width=\"20\" height=\"5\" fill=\"#f5f5f4\" />\n      {/* La tasse et l'emoji de l'agent, en autocollant. */}\n      <rect x={x + 8} y={dy + 22} width=\"8\" height=\"9\" fill=\"#f5f5f4\" />\n      <rect x={x + 16} y={dy + 24} width=\"3\" height=\"4\" fill=\"#f5f5f4\" />\n      <rect x={x + 9} y={dy + 23} width=\"6\" height=\"2\" fill=\"#5b2c0f\" />\n      <text x={x + 12} y={dy + 13} fontSize=\"10\" textAnchor=\"middle\">\n        {emoji}\n      </text>\n    </g>\n  )\n}\n\n/** Le bureau du chef : acajou, lampe de banquier verte, téléphone, sous-main et stylos. */\nfunction BureauChef80({ dy, x, dessus }: PropsBureau) {\n  return (\n    <g>\n      <PlateauBois x={x} dy={dy} dessus={dessus} fonce />\n      <rect x={x + 10} y={dy - 8} width=\"26\" height=\"10\" fill=\"#15803d\" />\n      <rect x={x + 10} y={dy - 8} width=\"26\" height=\"3\" fill=\"#22c55e\" />\n      <rect x={x + 22} y={dy + 2} width=\"2\" height=\"10\" fill=\"#d4a93b\" />\n      <rect x={x + 16} y={dy + 12} width=\"14\" height=\"3\" fill=\"#d4a93b\" />\n      <rect x={x + 40} y={dy + 14} width=\"40\" height=\"18\" fill=\"#14532d\" />\n      <rect x={x + 46} y={dy + 18} width=\"28\" height=\"2\" fill=\"#f5f5f4\" />\n      <rect x={x + 90} y={dy + 16} width=\"18\" height=\"12\" fill=\"#1c1917\" />\n      <rect x={x + 88} y={dy + 12} width=\"22\" height=\"5\" fill=\"#292524\" />\n      <rect x={x + 100} y={dy + 2} width=\"8\" height=\"10\" fill=\"#d4a93b\" />\n      <rect x={x + 102} y={dy - 4} width=\"1\" height=\"8\" fill=\"#1c1917\" />\n      <rect x={x + 105} y={dy - 2} width=\"1\" height=\"6\" fill=\"#b91c1c\" />\n    </g>\n  )\n}\n\n/** Le grand fauteuil de cuir sang-de-bœuf du chef, capitonné. */\nfunction Fauteuil80({ x, y }: { x: number; y: number }) {\n  return (\n    <g>\n      <rect x={x - 17} y={y - 36} width=\"34\" height=\"44\" fill=\"#3f0f0f\" />\n      <rect x={x - 14} y={y - 33} width=\"28\" height=\"38\" fill=\"#6b1f1f\" />\n      {[0, 1, 2].map((r) =>\n        [0, 1, 2].map((c) => <rect key={`${r}${c}`} x={x - 9 + c * 8} y={y - 28 + r * 9} width=\"2\" height=\"2\" fill=\"#3f0f0f\" />),\n      )}\n      <rect x={x - 21} y={y - 8} width=\"7\" height=\"16\" fill=\"#3f0f0f\" />\n      <rect x={x + 14} y={y - 8} width=\"7\" height=\"16\" fill=\"#3f0f0f\" />\n    </g>\n  )\n}\n\n/** La chaise de bureau en tissu orangé ; un liseré de la couleur de l'agent. */\nfunction Assise80({ x, y, couleur }: PropsSiege) {\n  return (\n    <g>\n      <rect x={x - 11} y={y - 12} width=\"22\" height=\"12\" fill=\"#9a4708\" />\n      <rect x={x - 11} y={y - 2} width=\"22\" height=\"2\" fill={couleur} />\n    </g>\n  )\n}\n\nfunction Dossier80({ x, y, couleur }: PropsSiege) {\n  return (\n    <g>\n      <rect x={x - 12} y={y + 2} width=\"24\" height=\"10\" fill=\"#7c3a06\" />\n      <rect x={x - 10} y={y + 4} width=\"20\" height=\"6\" fill=\"#b45309\" />\n      <rect x={x - 10} y={y + 10} width=\"20\" height=\"1\" fill={couleur} />\n      <rect x={x - 2} y={y + 12} width=\"4\" height=\"3\" fill=\"#44403c\" />\n      <rect x={x - 10} y={y + 15} width=\"20\" height=\"3\" fill=\"#292524\" />\n    </g>\n  )\n}\n\n/* ——— Les tenues ——— */\n\nconst VESTES = ['#1e3a8a', '#4b5563', '#7c2d12', '#991b1b', '#065f46', '#b45309', '#6d28d9', '#be185d', '#0e7490', '#57534e']\nconst CRAVATES = ['#dc2626', '#facc15', '#f5f5f4', '#2563eb', '#16a34a', '#f472b6']\nconst CHEVEUX = ['#2a1d14', '#5a3a22', '#c8a165', '#111111', '#8a4b2a', '#fde68a', '#b45309']\nconst PEAUX = ['#f1c9a5', '#d9a57a', '#a86f48', '#7a4a2c', '#f5d6bf']\nconst PANTALONS = ['#44403c', '#78716c', '#1c1917', '#7c2d12', '#d6d3d1']\n\n/** Le nombre de cet agent dans ce thème : chaque thème a sa graine, pour des tenues qui changent d'un thème à l'autre. */\nconst hachage = (id: string) => hacher(id, 11, 33)\n\n/** Costumes, cravates de couleur, pulls vifs ; le chef en costume bleu nuit et cravate rouge. */\nfunction tenue80(id: string): Record<string, string> {\n  if (id === CHEF_ID) {\n    return { h: '#9ca3af', s: '#f1c9a5', e: '#1c1917', c: '#172554', C: '#0f172a', t: '#dc2626', k: '#f97316', p: '#172554', b: '#1c1917' }\n  }\n  // Le livreur, en blouse grise.\n  if (id.startsWith('livreur:')) {\n    return { h: '#5b3a1a', s: '#d9a57a', e: '#1c1917', c: '#78716c', C: '#57534e', t: '#facc15', k: '#111827', p: '#44403c', b: '#111827' }\n  }\n  const h = hachage(id)\n  const veste = VESTES[h % VESTES.length]\n  return {\n    h: CHEVEUX[(h >>> 4) % CHEVEUX.length],\n    s: PEAUX[(h >>> 8) % PEAUX.length],\n    e: '#1c1917',\n    c: veste,\n    C: `color-mix(in oklab, ${veste} 70%, black)`,\n    t: CRAVATES[(h >>> 12) % CRAVATES.length],\n    // Le casque du Walkman, mousse orange.\n    k: '#f97316',\n    p: PANTALONS[(h >>> 16) % PANTALONS.length],\n    b: '#1c1917',\n  }\n}\n\n/** Un agent sur trois a son Walkman sur les oreilles. */\nconst walkman = (id: string) => id !== CHEF_ID && !estInvite(id) && hachage(id) % 3 === 0\n\n/* Les lumières dans la nuit : la lampe de banquier, la télé, les bornes, la fontaine, l'enseigne. */\nconst LUMIERES_80: Array<Lumiere> = [\n  { x: 222, y: 36, r: 50, couleur: '#d4a93b' },\n  { x: 61, y: 60, r: 40, couleur: '#ef4444' },\n  { x: 373, y: 50, r: 50, couleur: '#a855f7' },\n  { x: 417, y: 50, r: 45, couleur: '#22d3ee' },\n  { x: 500, y: 50, r: 70, couleur: '#93c5fd' },\n  { x: 768, y: 40, r: 35 },\n  { x: 881, y: 40, r: 40, couleur: '#60a5fa' },\n  { x: 695, y: 226, r: 60, couleur: '#fb923c' },\n]\n\nexport const THEME_80: Theme = {\n  coins: COINS_80,\n  coinCafe: 'cafe',\n  fond: '#1a120c',\n  Decor,\n  Bureau: Bureau80,\n  BureauChef: BureauChef80,\n  plaque: '#f5deb3',\n  Assise: Assise80,\n  Dossier: Dossier80,\n  Fauteuil: Fauteuil80,\n  tenue: tenue80,\n  casque: walkman,\n  CafeQuiCoule: CafeQuiCoule80,\n  lumieres: LUMIERES_80,\n  // Le vert phosphore de chaque écran cathodique.\n  lumierePoste: (cx, dy) => [{ x: cx - 4, y: dy - 8, r: 42, couleur: '#22c55e' }],\n}\n",
      "type": "registry:component",
      "target": "components/pixel-openspace/themes/eighties.tsx"
    },
    {
      "path": "registry/pixel-openspace/themes/gym.tsx",
      "content": "'use client'\n\n/*\n * Le thème salle de sport : chaque agent a son vélo d'appartement et sa\n * console. Au travail il pédale, la roue d'inertie tourne et la console\n * affiche son effort ; au repos elle attend, READY ; en échec elle passe au\n * rouge et le moteur fume. Le coach a son bureau vitré et sa vitrine à\n * trophées ; la salle de musculation, ses miroirs, sa télé qui passe une\n * séance, ses bancs et son rack à squat ; le bar, son blender, sa fontaine\n * et ses casiers. Débardeurs, shorts, baskets et casques de sport.\n */\n\nimport { BUREAU, CHEF_ID, estInvite, type CoinDePause, type Statut } from '../engine'\nimport { Anim, Cliquable, MUR_SALLE, Neon, Plante, etapes, useTextes, hacher, type Lumiere } from '../primitives'\nimport type { PropsBureau, PropsDecor, PropsSiege, Theme } from '../theme'\nimport { Arcade, BorneCasseBriques, Porte } from './geek'\nimport type { SceneObject } from '../types'\n\n/* Les coins de pause : la même géographie que les autres thèmes, d'autres agrès. */\nconst COINS_GYM: Array<CoinDePause> = [\n  { nom: 'smoothie', x: 768, y: 100 },\n  { nom: 'fontaine', x: 881, y: 104 },\n  { nom: 'casiers', x: 950, y: 108 },\n  { nom: 'arcade', x: 372, y: 112 },\n  { nom: 'arcade-2', x: 415, y: 112 },\n  { nom: 'rack', x: 636, y: 108 },\n  // Sur les bancs de muscu, face aux miroirs : de dos.\n  { nom: 'banc-gauche', x: 482, y: 146, assis: true },\n  { nom: 'banc-droite', x: 538, y: 146, assis: true },\n]\n\nconst MUR = { arete: '#0f172a', face: '#1e293b', bande: '#f97316', plinthe: '#0b1220' }\nconst CYAN = '#22d3ee'\n\n/** Un mur peint, vu de biais, avec sa bande de couleur à hauteur d'épaule. */\nfunction MurPeint({\n  x,\n  largeur,\n  y = 20,\n  hauteur = 32,\n  bande = MUR.bande,\n}: {\n  x: number\n  largeur: number\n  y?: number\n  hauteur?: number\n  bande?: string\n}) {\n  return (\n    <>\n      <rect x={x} y={y - 6} width={largeur} height=\"6\" fill={MUR.arete} />\n      <rect x={x} y={y} width={largeur} height={hauteur} fill={MUR.face} />\n      <rect x={x} y={y + hauteur - 12} width={largeur} height=\"4\" fill={bande} />\n      <rect x={x} y={y + hauteur} width={largeur} height=\"4\" fill={MUR.plinthe} />\n    </>\n  )\n}\n\nfunction Decor({ hauteur, titre, liens, aller, jouer }: PropsDecor) {\n  const t = useTextes()\n  const bas = hauteur - 20\n  const lien = (objet: SceneObject) => ({ titre: t.objects[objet], faire: () => aller(objet), actif: Boolean(liens[objet]) })\n  return (\n    <>\n      <defs>\n        <pattern id=\"sol-gym\" width=\"40\" height=\"40\" patternUnits=\"userSpaceOnUse\">\n          <rect width=\"40\" height=\"40\" fill=\"#4b5059\" />\n          <rect width=\"40\" height=\"1\" fill=\"#41464e\" />\n          <rect width=\"1\" height=\"40\" fill=\"#41464e\" />\n          {[\n            [6, 8],\n            [21, 15],\n            [33, 4],\n            [12, 29],\n            [28, 33],\n            [36, 22],\n          ].map(([x, y]) => (\n            <rect key={`${x}-${y}`} x={x} y={y} width=\"1\" height=\"1\" fill={x % 2 ? '#f97316' : CYAN} opacity=\"0.6\" />\n          ))}\n        </pattern>\n        <pattern id=\"dalles-gym\" width=\"32\" height=\"32\" patternUnits=\"userSpaceOnUse\">\n          <rect width=\"32\" height=\"32\" fill=\"#1f2937\" />\n          <rect width=\"32\" height=\"2\" fill=\"#111827\" />\n          <rect width=\"2\" height=\"32\" fill=\"#111827\" />\n        </pattern>\n        <pattern id=\"parquet-coach\" width=\"36\" height=\"12\" patternUnits=\"userSpaceOnUse\">\n          <rect width=\"36\" height=\"12\" fill=\"#c8a06a\" />\n          <rect y=\"10\" width=\"36\" height=\"2\" fill=\"#a8824f\" />\n          <rect x=\"18\" width=\"2\" height=\"12\" fill=\"#a8824f\" />\n        </pattern>\n        <pattern id=\"carrelage-bar\" width=\"20\" height=\"20\" patternUnits=\"userSpaceOnUse\">\n          <rect width=\"20\" height=\"20\" fill=\"#e5f6fb\" />\n          <rect width=\"20\" height=\"1\" fill=\"#bfe3ee\" />\n          <rect width=\"1\" height=\"20\" fill=\"#bfe3ee\" />\n        </pattern>\n        <linearGradient id=\"miroir-gym\" x1=\"0\" x2=\"1\" y1=\"0\" y2=\"1\">\n          <stop offset=\"0\" stopColor=\"#cbd5e1\" />\n          <stop offset=\"0.5\" stopColor=\"#94a3b8\" />\n          <stop offset=\"1\" stopColor=\"#e2e8f0\" />\n        </linearGradient>\n      </defs>\n\n      {/* Les sols : parquet chez le coach, dalles de caoutchouc en musculation, carrelage au bar, sol sportif moucheté dans la grande salle. */}\n      <rect x=\"20\" y=\"56\" width=\"310\" height=\"142\" fill=\"url(#parquet-coach)\" />\n      <rect x=\"338\" y=\"56\" width=\"344\" height=\"142\" fill=\"url(#dalles-gym)\" />\n      <rect x=\"690\" y=\"56\" width=\"290\" height=\"142\" fill=\"url(#carrelage-bar)\" />\n      <rect x=\"20\" y=\"198\" width=\"960\" height={bas - 198} fill=\"url(#sol-gym)\" />\n\n      {/* Les murs du fond. */}\n      <MurPeint x={20} largeur={310} />\n      <MurPeint x={338} largeur={344} bande={CYAN} />\n      <MurPeint x={690} largeur={290} bande=\"#a3e635\" />\n\n      {/* Le bureau du coach : la vitrine à trophées, l'enseigne, la plante. */}\n      <Cliquable {...lien('server-rack')}>\n        <VitrineTrophees x={34} />\n      </Cliquable>\n      <Neon x={222} y={40} texte={t.leadOffice} couleur=\"#f97316\" taille={11} />\n      <Plante x={300} y={62} />\n\n      {/* La salle de musculation : le mur de miroirs, les bornes, la télé, les bancs, le rack à squat. */}\n      <rect x=\"440\" y=\"22\" width=\"150\" height=\"28\" fill=\"url(#miroir-gym)\" opacity=\"0.55\" />\n      <Cliquable titre={t.objects.snake} faire={() => jouer('snake')}>\n        <Arcade x={352} />\n      </Cliquable>\n      <Cliquable titre={t.objects.breakout} faire={() => jouer('casse-briques')}>\n        <BorneCasseBriques x={396} />\n      </Cliquable>\n      <Cliquable {...lien('tv')}>\n        <TeleSeance x={466} />\n      </Cliquable>\n      <BancMuscu x={482} y={146} />\n      <BancMuscu x={538} y={146} />\n      <Cliquable {...lien('workbench')}>\n        <RackSquat x={596} />\n      </Cliquable>\n      <Plante x={346} y={160} />\n      <Halteres x={640} y={176} />\n\n      {/* Le bar à smoothies : comptoir, blender, corbeille de fruits, fontaine, casiers, mange-debout. */}\n      <rect x=\"730\" y=\"44\" width=\"122\" height=\"24\" fill=\"#f8fafc\" />\n      <rect x=\"730\" y=\"44\" width=\"122\" height=\"3\" fill=\"#ffffff\" />\n      <rect x=\"730\" y=\"68\" width=\"122\" height=\"18\" fill=\"#0e7490\" />\n      <rect x=\"730\" y=\"68\" width=\"122\" height=\"3\" fill=\"#a3e635\" />\n      <Blender x={758} />\n      {[\n        [812, '#f97316'],\n        [820, '#facc15'],\n        [828, '#ef4444'],\n        [816, '#84cc16'],\n        [824, '#fb923c'],\n      ].map(([x, c], i) => (\n        <rect key={i} x={x as number} y={i < 3 ? 48 : 43} width=\"7\" height=\"6\" fill={c as string} />\n      ))}\n      <rect x=\"808\" y=\"52\" width=\"30\" height=\"6\" fill=\"#a16207\" />\n      <Cliquable {...lien('vending-machine')}>\n        <FontaineInox x={864} />\n      </Cliquable>\n      <Cliquable {...lien('fridge')}>\n        <Casiers x={922} />\n      </Cliquable>\n      <rect x=\"818\" y=\"132\" width=\"8\" height=\"34\" fill=\"#334155\" />\n      <rect x=\"802\" y=\"126\" width=\"40\" height=\"10\" fill=\"#a3e635\" />\n      <rect x=\"802\" y=\"136\" width=\"40\" height=\"3\" fill=\"#65a30d\" />\n      {[786, 850].map((x) => (\n        <g key={x}>\n          <rect x={x} y=\"140\" width=\"10\" height=\"10\" fill=\"#0e7490\" />\n          <rect x={x + 3} y=\"150\" width=\"4\" height=\"12\" fill=\"#334155\" />\n        </g>\n      ))}\n      <Plante x={700} y={160} />\n\n      {/* La grande salle : la pile de matériel, les plantes. */}\n      <PileMateriel x={56} y={bas - 44} lien={lien} />\n      <Plante x={946} y={bas - 32} />\n\n      {/* Le mur de la grande salle, percé de ses trois entrées. */}\n      {MUR_SALLE.map(([de, a]) => (\n        <g key={de}>\n          <MurPeint x={de} largeur={a - de} y={204} hauteur={38} />\n        </g>\n      ))}\n      <Neon x={695} y={226} texte={titre} couleur=\"#a3e635\" taille={titre.length > 14 ? 7 : 9} />\n      <Porte bas={bas} />\n      <rect x=\"330\" y=\"14\" width=\"8\" height=\"190\" fill=\"#7dd3fc\" opacity=\"0.18\" />\n      <rect x=\"330\" y=\"14\" width=\"2\" height=\"190\" fill=\"#bae6fd\" opacity=\"0.6\" />\n      <rect x=\"682\" y=\"14\" width=\"8\" height=\"190\" fill={MUR.arete} />\n      <rect x=\"12\" y=\"14\" width=\"8\" height={bas - 6} fill={MUR.arete} />\n      <rect x=\"980\" y=\"14\" width=\"8\" height={bas - 6} fill={MUR.arete} />\n      <rect x=\"12\" y=\"6\" width=\"976\" height=\"8\" fill={MUR.arete} />\n      <rect x=\"12\" y={bas} width=\"976\" height=\"10\" fill={MUR.arete} />\n    </>\n  )\n}\n\n/** La vitrine à trophées : coupes dorées et argentées, médailles. */\nfunction VitrineTrophees({ x }: { x: number }) {\n  return (\n    <g>\n      <rect x={x} y=\"20\" width=\"54\" height=\"86\" fill=\"#334155\" />\n      <rect x={x + 3} y=\"23\" width=\"48\" height=\"80\" fill=\"#bae6fd\" opacity=\"0.25\" />\n      {[44, 70, 96].map((y) => (\n        <rect key={y} x={x + 3} y={y} width=\"48\" height=\"3\" fill=\"#475569\" />\n      ))}\n      {[\n        [8, 30, '#facc15'],\n        [24, 28, '#e5e7eb'],\n        [38, 32, '#d97706'],\n        [10, 56, '#e5e7eb'],\n        [30, 54, '#facc15'],\n      ].map(([dx, y, c], i) => (\n        <g key={i}>\n          <rect x={x + (dx as number)} y={y as number} width=\"8\" height=\"7\" fill={c as string} />\n          <rect x={x + (dx as number) + 3} y={(y as number) + 7} width=\"2\" height=\"4\" fill={c as string} />\n          <rect x={x + (dx as number) + 1} y={(y as number) + 11} width=\"6\" height=\"3\" fill=\"#78350f\" />\n        </g>\n      ))}\n      {[10, 22, 34].map((dx, i) => (\n        <g key={dx}>\n          <rect x={x + dx + 3} y=\"76\" width=\"2\" height=\"8\" fill={['#ef4444', '#2563eb', '#16a34a'][i]} />\n          <rect x={x + dx + 1} y=\"84\" width=\"6\" height=\"6\" fill={['#facc15', '#e5e7eb', '#d97706'][i]} />\n        </g>\n      ))}\n    </g>\n  )\n}\n\n/** La télé murale, qui passe une séance : un bonhomme qui fait des jumping jacks. */\nfunction TeleSeance({ x }: { x: number }) {\n  return (\n    <g>\n      <rect x={x} y=\"24\" width=\"100\" height=\"40\" fill=\"#0b0d10\" />\n      <rect x={x + 3} y=\"27\" width=\"94\" height=\"34\" fill=\"#7c3aed\" />\n      <rect x={x + 3} y=\"53\" width=\"94\" height=\"8\" fill=\"#5b21b6\" />\n      <g>\n        <Anim attributeName=\"transform\" type=\"translate\" values=\"0 0;0 -3\" dur=\"0.8s\" repeatCount=\"indefinite\" />\n        <rect x={x + 47} y=\"32\" width=\"6\" height=\"6\" fill=\"#fde68a\" />\n        <rect x={x + 46} y=\"38\" width=\"8\" height=\"10\" fill=\"#f472b6\" />\n        <rect x={x + 40} y=\"36\" width=\"6\" height=\"3\" fill=\"#fde68a\">\n          <Anim attributeName=\"y\" values=\"36;30\" dur=\"0.8s\" repeatCount=\"indefinite\" />\n        </rect>\n        <rect x={x + 54} y=\"36\" width=\"6\" height=\"3\" fill=\"#fde68a\">\n          <Anim attributeName=\"y\" values=\"36;30\" dur=\"0.8s\" repeatCount=\"indefinite\" />\n        </rect>\n        <rect x={x + 46} y=\"48\" width=\"3\" height=\"6\" fill=\"#1e1b4b\" />\n        <rect x={x + 51} y=\"48\" width=\"3\" height=\"6\" fill=\"#1e1b4b\" />\n      </g>\n      <rect x={x + 8} y=\"30\" width=\"22\" height=\"5\" fill=\"#facc15\" />\n      <rect x={x + 44} y=\"64\" width=\"12\" height=\"4\" fill=\"#0b0d10\" />\n    </g>\n  )\n}\n\n/** Un banc de musculation, vu de la salle. */\nfunction BancMuscu({ x, y }: { x: number; y: number }) {\n  return (\n    <g>\n      <rect x={x - 6} y={y + 8} width=\"4\" height=\"10\" fill=\"#334155\" />\n      <rect x={x + 2} y={y + 8} width=\"4\" height=\"10\" fill=\"#334155\" />\n      <rect x={x - 12} y={y - 6} width=\"24\" height=\"16\" fill=\"#0b0d10\" />\n      <rect x={x - 10} y={y - 4} width=\"20\" height=\"12\" fill=\"#dc2626\" />\n      <rect x={x - 10} y={y - 4} width=\"20\" height=\"3\" fill=\"#ef4444\" />\n    </g>\n  )\n}\n\n/** Le rack à squat : montants d'acier, barre chargée de disques. */\nfunction RackSquat({ x }: { x: number }) {\n  return (\n    <g>\n      <rect x={x + 6} y=\"26\" width=\"5\" height=\"68\" fill=\"#475569\" />\n      <rect x={x + 66} y=\"26\" width=\"5\" height=\"68\" fill=\"#475569\" />\n      <rect x={x + 6} y=\"26\" width=\"65\" height=\"4\" fill=\"#475569\" />\n      <rect x={x} y=\"52\" width=\"78\" height=\"3\" fill=\"#cbd5e1\" />\n      {[x - 2, x + 72].map((dx) => (\n        <g key={dx}>\n          <rect x={dx} y=\"42\" width=\"8\" height=\"22\" fill=\"#111827\" />\n          <rect x={dx + 1} y=\"44\" width=\"6\" height=\"18\" fill=\"#1f2937\" />\n        </g>\n      ))}\n      <rect x={x + 2} y=\"92\" width=\"74\" height=\"4\" fill=\"#334155\" />\n    </g>\n  )\n}\n\nfunction Halteres({ x, y, paires = 2 }: { x: number; y: number; paires?: number }) {\n  return (\n    <g>\n      {[0, 10].slice(0, paires).map((dy) => (\n        <g key={dy}>\n          <rect x={x} y={y + dy} width=\"5\" height=\"7\" fill=\"#111827\" />\n          <rect x={x + 5} y={y + dy + 2} width=\"12\" height=\"3\" fill=\"#9ca3af\" />\n          <rect x={x + 17} y={y + dy} width=\"5\" height=\"7\" fill=\"#111827\" />\n        </g>\n      ))}\n    </g>\n  )\n}\n\n/** Le blender du bar, et son bol de smoothie. */\nfunction Blender({ x }: { x: number }) {\n  return (\n    <g>\n      <rect x={x} y=\"40\" width=\"20\" height=\"8\" fill=\"#111827\" />\n      <rect x={x + 2} y=\"22\" width=\"16\" height=\"18\" fill=\"#e0f2fe\" opacity=\"0.7\" />\n      <rect x={x + 3} y=\"30\" width=\"14\" height=\"10\" fill=\"#f472b6\" />\n      <rect x={x + 4} y=\"18\" width=\"12\" height=\"4\" fill=\"#111827\" />\n    </g>\n  )\n}\n\n/** Le smoothie qui tourne dans le blender, quand quelqu'un attend au bar. */\nfunction BlenderEnMarche() {\n  return (\n    <g pointerEvents=\"none\">\n      <rect x=\"762\" y=\"30\" width=\"12\" height=\"10\" fill=\"#fb7185\">\n        <Anim attributeName=\"fill\" values=\"#f472b6;#fb7185;#f9a8d4\" dur=\"0.3s\" repeatCount=\"indefinite\" />\n      </rect>\n    </g>\n  )\n}\n\n/** La fontaine à eau en inox. */\nfunction FontaineInox({ x }: { x: number }) {\n  return (\n    <g>\n      <rect x={x} y=\"40\" width=\"34\" height=\"54\" fill=\"#94a3b8\" />\n      <rect x={x + 2} y=\"42\" width=\"30\" height=\"50\" fill=\"#cbd5e1\" />\n      <rect x={x + 4} y=\"46\" width=\"26\" height=\"8\" fill=\"#64748b\" />\n      <rect x={x + 15} y=\"44\" width=\"4\" height=\"4\" fill=\"#38bdf8\" />\n      <rect x={x + 10} y=\"64\" width=\"14\" height=\"4\" fill=\"#475569\" />\n    </g>\n  )\n}\n\n/** Les casiers bleus des vestiaires, l'un entrouvert. */\nfunction Casiers({ x }: { x: number }) {\n  return (\n    <g>\n      <rect x={x} y=\"16\" width=\"54\" height=\"82\" fill=\"#1e3a8a\" />\n      {[0, 1, 2].map((c) => (\n        <g key={c}>\n          <rect x={x + 2 + c * 17} y=\"18\" width=\"16\" height=\"78\" fill={c === 1 ? '#1d4ed8' : '#2563eb'} />\n          {[24, 30, 36].map((y) => (\n            <rect key={y} x={x + 6 + c * 17} y={y} width=\"8\" height=\"2\" fill=\"#1e3a8a\" />\n          ))}\n          <rect x={x + 14 + c * 17} y=\"56\" width=\"2\" height=\"6\" fill=\"#e5e7eb\" />\n        </g>\n      ))}\n      <rect x={x + 19} y=\"18\" width=\"3\" height=\"78\" fill=\"#0b1220\" />\n    </g>\n  )\n}\n\n/** La pile de matériel : un bidon de protéines, des tapis roulés, des élastiques. */\nfunction PileMateriel({\n  x,\n  y,\n  lien,\n}: {\n  x: number\n  y: number\n  lien: (objet: SceneObject) => { titre: string; faire: () => void; actif: boolean }\n}) {\n  return (\n    <g>\n      <Cliquable {...lien('cpu-box')}>\n        <rect x={x} y={y + 14} width=\"40\" height=\"26\" fill=\"#111827\" />\n        <rect x={x} y={y + 14} width=\"40\" height=\"4\" fill=\"#a3e635\" />\n        <text x={x + 20} y={y + 32} fontSize=\"7\" textAnchor=\"middle\" fill=\"#a3e635\">\n          WHEY\n        </text>\n      </Cliquable>\n      <Cliquable {...lien('gpu-box')}>\n        <rect x={x + 6} y={y} width=\"30\" height=\"16\" fill=\"#7c3aed\" />\n        <rect x={x + 6} y={y + 4} width=\"30\" height=\"2\" fill=\"#5b21b6\" />\n        <rect x={x + 6} y={y + 10} width=\"30\" height=\"2\" fill=\"#5b21b6\" />\n      </Cliquable>\n      <Cliquable {...lien('ram-box')}>\n        <rect x={x + 44} y={y + 22} width=\"22\" height=\"18\" fill=\"#f97316\" />\n        <rect x={x + 48} y={y + 26} width=\"14\" height=\"10\" fill=\"#fdba74\" />\n      </Cliquable>\n    </g>\n  )\n}\n\n/* ——— Les postes : un vélo d'appartement et sa console ——— */\n\n/** La console du vélo, qui dit l'état de son agent. */\nfunction Console({ cx, dy, statut, bientot }: { cx: number; dy: number; statut: Statut; bientot: number | null }) {\n  const t = useTextes()\n  const g = cx - 22\n  return (\n    <g>\n      <rect x={cx - 2} y={dy + 4} width=\"4\" height=\"16\" fill=\"#9ca3af\" />\n      <rect x={g - 3} y={dy - 18} width=\"50\" height=\"24\" fill=\"#111827\" />\n      {statut === 'endormi' ? (\n        <rect x={g} y={dy - 15} width=\"44\" height=\"18\" fill=\"#020617\" />\n      ) : statut === 'absent' ? (\n        <>\n          <rect x={g} y={dy - 15} width=\"44\" height=\"18\" fill=\"#020617\" />\n          <g transform={`rotate(-6 ${cx} ${dy - 6})`}>\n            <rect x={cx - 15} y={dy - 14} width=\"30\" height=\"15\" fill=\"#fde68a\" />\n            <rect x={cx - 15} y={dy - 14} width=\"30\" height=\"3\" fill=\"#fcd34d\" />\n            <text x={cx} y={dy - 2} fontSize=\"7\" textAnchor=\"middle\" fill=\"#78350f\">\n              {t.offNote}\n            </text>\n          </g>\n        </>\n      ) : statut === 'en-echec' ? (\n        <>\n          <rect x={g} y={dy - 15} width=\"44\" height=\"18\" fill=\"#7f1d1d\" />\n          <text x={cx} y={dy - 2} fontSize=\"9\" textAnchor=\"middle\" fill=\"#fecaca\">\n            ERR\n            <Anim attributeName=\"opacity\" values=\"1;0.2\" dur=\"0.8s\" repeatCount=\"indefinite\" />\n          </text>\n        </>\n      ) : statut === 'au-travail' ? (\n        <>\n          <rect x={g} y={dy - 15} width=\"44\" height=\"18\" fill=\"#082f49\" />\n          {/* L'effort : des barres qui montent et descendent, comme un profil d'intervalle. */}\n          {[0, 1, 2, 3, 4, 5].map((i) => (\n            <rect key={i} x={g + 4 + i * 7} y={dy - 4} width=\"5\" height=\"4\" fill={CYAN}>\n              <Anim\n                attributeName=\"height\"\n                values={['4;10;6;12;4', '8;4;12;6;8', '6;12;4;10;6'][i % 3]}\n                dur=\"1.2s\"\n                repeatCount=\"indefinite\"\n              />\n              <Anim\n                attributeName=\"y\"\n                values={['-4;-10;-6;-12;-4', '-8;-4;-12;-6;-8', '-6;-12;-4;-10;-6'][i % 3]\n                  .split(';')\n                  .map((v) => String(dy + Number(v) + 2))\n                  .join(';')}\n                dur=\"1.2s\"\n                repeatCount=\"indefinite\"\n              />\n            </rect>\n          ))}\n        </>\n      ) : (\n        <>\n          <rect x={g} y={dy - 15} width=\"44\" height=\"18\" fill=\"#0f172a\" />\n          <text x={cx} y={dy - 3} fontSize=\"8\" textAnchor=\"middle\" fill={CYAN}>\n            READY\n          </text>\n        </>\n      )}\n      {bientot ? (\n        <>\n          <rect x={g} y={dy - 2} width=\"44\" height=\"7\" fill=\"#0b1220\" />\n          <text x={cx} y={dy + 4} fontSize=\"6\" textAnchor=\"middle\" fill=\"#a3e635\">\n            {t.inMinutes(bientot)}\n          </text>\n        </>\n      ) : null}\n    </g>\n  )\n}\n\n/** Le tapis de sol, sa tranche où s'inscrit le nom. */\nfunction Tapis({ x, dy, dessus }: { x: number; dy: number; dessus: number }) {\n  return (\n    <>\n      <rect x={x} y={dy} width={BUREAU.largeur} height={dessus} fill=\"#1f2227\" />\n      <rect x={x} y={dy} width={BUREAU.largeur} height=\"2\" fill=\"#2f333a\" />\n      {[8, 16, 24, 32].map((v) => (\n        <rect key={v} x={x + 2} y={dy + v} width={BUREAU.largeur - 4} height=\"1\" fill=\"#26292f\" />\n      ))}\n      <rect x={x} y={dy + dessus} width={BUREAU.largeur} height=\"12\" fill=\"#15171b\" />\n    </>\n  )\n}\n\n/** Le poste : un tapis, la console, le guidon, la roue d'inertie, une gourde, une serviette, des haltères. */\nfunction Velo({ cx, dy, x, dessus, statut, accent, bientot, emoji }: PropsBureau) {\n  const pedale = statut === 'au-travail'\n  const fx = cx + 30\n  const fy = dy + 16\n  return (\n    <g>\n      <Tapis x={x} dy={dy} dessus={dessus} />\n      {/* La roue d'inertie, qui tourne quand l'agent pédale. */}\n      <circle cx={fx} cy={fy} r=\"10\" fill=\"#111827\" shapeRendering=\"auto\" />\n      <circle cx={fx} cy={fy} r=\"8\" fill=\"#475569\" shapeRendering=\"auto\" />\n      <rect x={fx - 1} y={fy - 8} width=\"2\" height=\"16\" fill=\"#cbd5e1\">\n        {pedale ? (\n          <Anim\n            attributeName=\"transform\"\n            type=\"rotate\"\n            values={`0 ${fx} ${fy};45 ${fx} ${fy};90 ${fx} ${fy};135 ${fx} ${fy}`}\n            dur=\"0.3s\"\n            repeatCount=\"indefinite\"\n          />\n        ) : null}\n      </rect>\n      <Console cx={cx - 4} dy={dy} statut={statut} bientot={bientot} />\n      {/* Le guidon, poignées à la couleur de l'agent. */}\n      <rect x={cx - 20} y={dy + 18} width=\"32\" height=\"3\" fill=\"#374151\" />\n      <rect x={cx - 24} y={dy + 16} width=\"6\" height=\"7\" fill={accent} />\n      <rect x={cx + 10} y={dy + 16} width=\"6\" height=\"7\" fill={accent} />\n      {/* La gourde et l'emoji, la serviette, les haltères. */}\n      <rect x={x + 9} y={dy + 18} width=\"7\" height=\"12\" fill=\"#38bdf8\" />\n      <rect x={x + 10} y={dy + 15} width=\"5\" height=\"3\" fill=\"#0f172a\" />\n      <text x={x + 12} y={dy + 12} fontSize=\"10\" textAnchor=\"middle\">\n        {emoji}\n      </text>\n      {/* La serviette pliée devant la gourde, les haltères au bout du tapis, loin de la roue. */}\n      <rect x={x + 20} y={dy + 26} width=\"18\" height=\"8\" fill=\"#f8fafc\" />\n      <rect x={x + 20} y={dy + 31} width=\"18\" height=\"2\" fill={accent} />\n      <Halteres x={x + 96} y={dy + 30} paires={1} />\n      {/* En échec, le moteur de la roue fume. */}\n      {statut === 'en-echec'\n        ? [0, 0.6, 1.2].map((debut, i) => (\n            <rect key={debut} x={fx - 4 + i * 3} y={fy - 8} width=\"6\" height=\"6\" fill=\"#9ca3af\" opacity=\"0\">\n              <Anim attributeName=\"y\" values={etapes(fy - 8, fy - 40, 6)} dur=\"1.8s\" begin={`${debut}s`} repeatCount=\"indefinite\" />\n              <Anim attributeName=\"opacity\" values={etapes(0.85, 0, 6)} dur=\"1.8s\" begin={`${debut}s`} repeatCount=\"indefinite\" />\n            </rect>\n          ))\n        : null}\n    </g>\n  )\n}\n\n/** Le bureau du coach : planchette, chronomètre, shaker, petite coupe. */\nfunction BureauCoach({ dy, x, dessus }: PropsBureau) {\n  return (\n    <g>\n      <rect x={x} y={dy} width={BUREAU.largeur} height={dessus} fill=\"#e5e7eb\" />\n      <rect x={x} y={dy} width={BUREAU.largeur} height=\"3\" fill=\"#f9fafb\" />\n      <rect x={x} y={dy + dessus} width={BUREAU.largeur} height=\"12\" fill=\"#f97316\" />\n      <rect x={x + 3} y={dy + dessus + 12} width=\"4\" height=\"7\" fill=\"#111827\" />\n      <rect x={x + BUREAU.largeur - 7} y={dy + dessus + 12} width=\"4\" height=\"7\" fill=\"#111827\" />\n      <rect x={x + 10} y={dy + 6} width=\"22\" height=\"28\" fill=\"#a16207\" />\n      <rect x={x + 12} y={dy + 10} width=\"18\" height=\"22\" fill=\"#f8fafc\" />\n      {[14, 19, 24].map((v) => (\n        <rect key={v} x={x + 14} y={dy + v} width=\"14\" height=\"2\" fill=\"#94a3b8\" />\n      ))}\n      <rect x={x + 16} y={dy + 4} width=\"10\" height=\"4\" fill=\"#475569\" />\n      <circle cx={x + 98} cy={dy + 22} r=\"7\" fill=\"#e5e7eb\" stroke=\"#111827\" strokeWidth=\"2\" shapeRendering=\"auto\" />\n      <rect x={x + 97} y={dy + 13} width=\"2\" height=\"4\" fill=\"#111827\" />\n      <rect x={x + 97} y={dy + 17} width=\"2\" height=\"5\" fill=\"#ef4444\" />\n      <rect x={x + 78} y={dy + 4} width=\"9\" height=\"18\" fill=\"#a3e635\" />\n      <rect x={x + 78} y={dy + 4} width=\"9\" height=\"4\" fill=\"#111827\" />\n      <rect x={x + 104} y={dy + 2} width=\"8\" height=\"7\" fill=\"#facc15\" />\n      <rect x={x + 107} y={dy + 9} width=\"2\" height=\"3\" fill=\"#facc15\" />\n    </g>\n  )\n}\n\n/** Le fauteuil du coach, rouge et noir. */\nfunction FauteuilCoach({ x, y }: { x: number; y: number }) {\n  return (\n    <g>\n      <rect x={x - 16} y={y - 34} width=\"32\" height=\"42\" fill=\"#0b0d10\" />\n      <rect x={x - 13} y={y - 31} width=\"26\" height=\"36\" fill=\"#b91c1c\" />\n      <rect x={x - 3} y={y - 31} width=\"6\" height=\"36\" fill=\"#0b0d10\" />\n      <rect x={x - 20} y={y - 8} width=\"6\" height=\"16\" fill=\"#0b0d10\" />\n      <rect x={x + 14} y={y - 8} width=\"6\" height=\"16\" fill=\"#0b0d10\" />\n    </g>\n  )\n}\n\n/** La selle du vélo. */\nfunction Selle({ x, y }: PropsSiege) {\n  return (\n    <g>\n      <rect x={x - 9} y={y - 12} width=\"18\" height=\"8\" fill=\"#111827\" />\n      <rect x={x - 7} y={y - 11} width=\"14\" height=\"3\" fill=\"#374151\" />\n      <rect x={x - 2} y={y - 4} width=\"4\" height=\"6\" fill=\"#6b7280\" />\n    </g>\n  )\n}\n\n/** Le cadre arrière du vélo et son pied stabilisateur, qui passent devant le pédaleur. */\nfunction Cadre({ x, y, couleur }: PropsSiege) {\n  return (\n    <g>\n      <rect x={x - 3} y={y} width=\"6\" height=\"12\" fill=\"#374151\" />\n      <rect x={x - 3} y={y + 4} width=\"6\" height=\"2\" fill={couleur} />\n      <rect x={x - 14} y={y + 12} width=\"28\" height=\"4\" fill=\"#1f2937\" />\n      <rect x={x - 16} y={y + 14} width=\"4\" height=\"3\" fill=\"#111827\" />\n      <rect x={x + 12} y={y + 14} width=\"4\" height=\"3\" fill=\"#111827\" />\n    </g>\n  )\n}\n\n/* ——— Les tenues ——— */\n\nconst MAILLOTS = ['#ef4444', '#f97316', '#facc15', '#a3e635', '#22d3ee', '#3b82f6', '#a855f7', '#ec4899', '#f8fafc', '#111827']\nconst CHEVEUX = ['#2a1d14', '#5a3a22', '#c8a165', '#111111', '#8a4b2a', '#e5e5e5', '#b45309']\nconst PEAUX = ['#f1c9a5', '#d9a57a', '#a86f48', '#7a4a2c', '#f5d6bf']\nconst SHORTS = ['#111827', '#1e293b', '#374151', '#1e3a8a', '#7f1d1d']\n\n/** Le nombre de cet agent dans ce thème : chaque thème a sa graine, pour des tenues qui changent d'un thème à l'autre. */\nconst hachage = (id: string) => hacher(id, 13, 37)\n\n/** Débardeurs et t-shirts de couleur vive, shorts, baskets blanches ; le coach en survêtement rouge à bandes. */\nfunction tenueGym(id: string): Record<string, string> {\n  if (id === CHEF_ID) {\n    return { h: '#4b5563', s: '#d9a57a', e: '#1c1917', c: '#dc2626', C: '#991b1b', t: '#f8fafc', k: '#111827', p: '#dc2626', b: '#f8fafc' }\n  }\n  if (id.startsWith('livreur:')) {\n    return { h: '#5b3a1a', s: '#d9a57a', e: '#1c1917', c: '#7c4a1e', C: '#5b3a1a', t: '#facc15', k: '#111827', p: '#5b3a1a', b: '#111827' }\n  }\n  const h = hachage(id)\n  const maillot = MAILLOTS[h % MAILLOTS.length]\n  return {\n    h: CHEVEUX[(h >>> 4) % CHEVEUX.length],\n    s: PEAUX[(h >>> 8) % PEAUX.length],\n    e: '#1c1917',\n    c: maillot,\n    C: `color-mix(in oklab, ${maillot} 75%, black)`,\n    t: maillot,\n    k: '#111827',\n    p: SHORTS[(h >>> 12) % SHORTS.length],\n    b: '#f8fafc',\n  }\n}\n\n/** Un sportif sur deux s'entraîne casque sur les oreilles. */\nconst casqueSport = (id: string) => id !== CHEF_ID && !estInvite(id) && hachage(id) % 2 === 0\n\n/* Les lumières dans la nuit : l'enseigne du coach, la vitrine, les bornes, la télé, le bar, l'enseigne de la salle. */\nconst LUMIERES_GYM: Array<Lumiere> = [\n  { x: 222, y: 36, r: 60, couleur: '#f97316' },\n  { x: 61, y: 60, r: 40, couleur: '#facc15' },\n  { x: 373, y: 50, r: 50, couleur: '#a855f7' },\n  { x: 417, y: 50, r: 45, couleur: '#22d3ee' },\n  { x: 516, y: 44, r: 70, couleur: '#a855f7' },\n  { x: 790, y: 56, r: 60, couleur: '#a3e635' },\n  { x: 695, y: 226, r: 60, couleur: '#a3e635' },\n]\n\nexport const THEME_GYM: Theme = {\n  coins: COINS_GYM,\n  coinCafe: 'smoothie',\n  fond: '#0b1220',\n  Decor,\n  Bureau: Velo,\n  BureauChef: BureauCoach,\n  plaque: '#e5e7eb',\n  Assise: Selle,\n  Dossier: Cadre,\n  Fauteuil: FauteuilCoach,\n  tenue: tenueGym,\n  casque: casqueSport,\n  CafeQuiCoule: BlenderEnMarche,\n  lumieres: LUMIERES_GYM,\n  // La console de chaque vélo éclaire en cyan.\n  lumierePoste: (cx, dy) => [{ x: cx - 4, y: dy - 6, r: 40, couleur: CYAN }],\n}\n",
      "type": "registry:component",
      "target": "components/pixel-openspace/themes/gym.tsx"
    },
    {
      "path": "registry/pixel-openspace/themes/modern.tsx",
      "content": "'use client'\n\n/*\n * Le thème moderne épuré, le seul en clair : parquet de chêne, murs blancs,\n * bureaux assis-debout et portables, beaucoup de plantes. Le chef a sa\n * bibliothèque en chêne et ses lettres noires au mur ; l'espace détente, son\n * écran de présentation, son canapé vert sauge, sa table de ping-pong et ses\n * bornes ; la cuisine ouverte, son îlot, sa machine à espresso, sa fontaine à\n * eau pétillante et son frigo inox. T-shirts pastel, chinos et baskets\n * blanches.\n */\n\nimport { BUREAU, CHEF_ID, estInvite, type CoinDePause, type Statut } from '../engine'\nimport { Anim, Cliquable, MUR_SALLE, Plante, etapes, useTextes, hacher, type Lumiere } from '../primitives'\nimport type { PropsBureau, PropsDecor, PropsSiege, Theme } from '../theme'\nimport { Arcade, BorneCasseBriques, Porte } from './geek'\nimport type { SceneObject } from '../types'\n\n/* Les coins de pause : la même géographie que les autres thèmes. */\nconst COINS_MODERNE: Array<CoinDePause> = [\n  { nom: 'cafe', x: 768, y: 100 },\n  { nom: 'fontaine', x: 881, y: 104 },\n  { nom: 'frigo', x: 950, y: 108 },\n  { nom: 'arcade', x: 372, y: 112 },\n  { nom: 'arcade-2', x: 415, y: 112 },\n  { nom: 'ping-pong', x: 636, y: 108 },\n  // Sur le canapé, face à l'écran : de dos.\n  { nom: 'canape-gauche', x: 482, y: 146, assis: true },\n  { nom: 'canape-droite', x: 538, y: 146, assis: true },\n]\n\nconst MUR = { arete: '#a8a29e', face: '#f5f5f4', plinthe: '#e7e5e4', lame: '#c8a06a' }\nconst ENCRE = '#1c1917'\n\n/** Un mur blanc, vu de biais ; des lames de chêne verticales sur une partie. */\nfunction MurBlanc({\n  x,\n  largeur,\n  y = 20,\n  hauteur = 32,\n  lames = 0,\n}: {\n  x: number\n  largeur: number\n  y?: number\n  hauteur?: number\n  lames?: number\n}) {\n  return (\n    <>\n      <rect x={x} y={y - 6} width={largeur} height=\"6\" fill={MUR.arete} />\n      <rect x={x} y={y} width={largeur} height={hauteur} fill={MUR.face} />\n      {Array.from({ length: lames }, (_, i) => (\n        <rect key={i} x={x + largeur - 6 - i * 6} y={y} width=\"3\" height={hauteur} fill={MUR.lame} />\n      ))}\n      <rect x={x} y={y + hauteur} width={largeur} height=\"4\" fill={MUR.plinthe} />\n    </>\n  )\n}\n\n/** Des lettres noires collées au mur : l'enseigne, sans néon. */\nfunction Lettres({ x, y, texte, taille = 10 }: { x: number; y: number; texte: string; taille?: number }) {\n  return (\n    <text x={x} y={y} fontSize={taille} textAnchor=\"middle\" fill={ENCRE} letterSpacing=\"1\">\n      {texte}\n    </text>\n  )\n}\n\nfunction Decor({ hauteur, titre, liens, aller, jouer }: PropsDecor) {\n  const t = useTextes()\n  const bas = hauteur - 20\n  const lien = (objet: SceneObject) => ({ titre: t.objects[objet], faire: () => aller(objet), actif: Boolean(liens[objet]) })\n  return (\n    <>\n      <defs>\n        <pattern id=\"chene-moderne\" width=\"64\" height=\"16\" patternUnits=\"userSpaceOnUse\">\n          <rect width=\"64\" height=\"16\" fill=\"#e2c597\" />\n          <rect y=\"14\" width=\"64\" height=\"2\" fill=\"#d2b282\" />\n          <rect x=\"40\" width=\"2\" height=\"16\" fill=\"#d2b282\" />\n          <rect x=\"6\" y=\"5\" width=\"18\" height=\"1\" fill=\"#ead2a8\" />\n        </pattern>\n        <pattern id=\"moquette-moderne\" width=\"12\" height=\"12\" patternUnits=\"userSpaceOnUse\">\n          <rect width=\"12\" height=\"12\" fill=\"#d6d3d1\" />\n          <rect x=\"3\" y=\"3\" width=\"2\" height=\"2\" fill=\"#cfcbc8\" />\n        </pattern>\n        <pattern id=\"beton-clair\" width=\"48\" height=\"48\" patternUnits=\"userSpaceOnUse\">\n          <rect width=\"48\" height=\"48\" fill=\"#e7e5e4\" />\n          <rect width=\"48\" height=\"1\" fill=\"#dcd9d6\" />\n          <rect width=\"1\" height=\"48\" fill=\"#dcd9d6\" />\n        </pattern>\n        <pattern id=\"terrazzo\" width=\"20\" height=\"20\" patternUnits=\"userSpaceOnUse\">\n          <rect width=\"20\" height=\"20\" fill=\"#f5f5f4\" />\n          <rect x=\"3\" y=\"4\" width=\"2\" height=\"2\" fill=\"#d6d3d1\" />\n          <rect x=\"12\" y=\"9\" width=\"2\" height=\"1\" fill=\"#a8a29e\" />\n          <rect x=\"7\" y=\"15\" width=\"1\" height=\"2\" fill=\"#fca5a5\" />\n          <rect x=\"16\" y=\"2\" width=\"1\" height=\"1\" fill=\"#86efac\" />\n        </pattern>\n      </defs>\n\n      {/* Les sols : moquette grise chez le chef, béton clair dans l'espace détente, terrazzo en cuisine, chêne dans la grande salle. */}\n      <rect x=\"20\" y=\"56\" width=\"310\" height=\"142\" fill=\"url(#moquette-moderne)\" />\n      <rect x=\"338\" y=\"56\" width=\"344\" height=\"142\" fill=\"url(#beton-clair)\" />\n      <rect x=\"690\" y=\"56\" width=\"290\" height=\"142\" fill=\"url(#terrazzo)\" />\n      <rect x=\"20\" y=\"198\" width=\"960\" height={bas - 198} fill=\"url(#chene-moderne)\" />\n\n      {/* Les murs du fond. */}\n      <MurBlanc x={20} largeur={310} />\n      <MurBlanc x={338} largeur={344} lames={6} />\n      <MurBlanc x={690} largeur={290} />\n\n      {/* Le bureau du chef : la bibliothèque en chêne, les lettres au mur, les plantes. */}\n      <Cliquable {...lien('server-rack')}>\n        <Bibliotheque x={32} />\n      </Cliquable>\n      <Lettres x={222} y={40} texte={t.leadOffice} taille={11} />\n      <Plante x={300} y={62} />\n\n      {/* L'espace détente : les bornes, l'écran de présentation, le canapé vert sauge, la table de ping-pong. */}\n      <Cliquable titre={t.objects.snake} faire={() => jouer('snake')}>\n        <Arcade x={352} />\n      </Cliquable>\n      <Cliquable titre={t.objects.breakout} faire={() => jouer('casse-briques')}>\n        <BorneCasseBriques x={396} />\n      </Cliquable>\n      <Cliquable {...lien('tv')}>\n        <EcranPresentation x={452} />\n      </Cliquable>\n      <rect x=\"452\" y=\"124\" width=\"116\" height=\"52\" fill=\"#d9e4d0\" />\n      <CanapeSauge x={458} y={130} />\n      <Cliquable {...lien('workbench')}>\n        <PingPong x={598} />\n      </Cliquable>\n      <Plante x={346} y={160} />\n      <Plante x={650} y={168} />\n\n      {/* La cuisine ouverte : l'îlot, la machine à espresso, la corbeille de fruits, la fontaine, le frigo inox. */}\n      <rect x=\"730\" y=\"44\" width=\"122\" height=\"24\" fill=\"#fafaf9\" />\n      <rect x=\"730\" y=\"44\" width=\"122\" height=\"3\" fill=\"#ffffff\" />\n      <rect x=\"730\" y=\"68\" width=\"122\" height=\"18\" fill=\"#c8a06a\" />\n      {[734, 774, 814].map((x) => (\n        <rect key={x} x={x} y=\"71\" width=\"34\" height=\"12\" fill=\"#b88f5a\" />\n      ))}\n      <Espresso x={748} />\n      <rect x=\"812\" y=\"52\" width=\"26\" height=\"8\" fill=\"#e7e5e4\" />\n      {[\n        [814, '#ef4444'],\n        [821, '#facc15'],\n        [828, '#84cc16'],\n      ].map(([x, c]) => (\n        <rect key={x} x={x as number} y=\"46\" width=\"6\" height=\"6\" fill={c as string} />\n      ))}\n      <Cliquable {...lien('vending-machine')}>\n        <EauPetillante x={864} />\n      </Cliquable>\n      <Cliquable {...lien('fridge')}>\n        <FrigoInox x={922} />\n      </Cliquable>\n      <rect x=\"818\" y=\"132\" width=\"6\" height=\"34\" fill={ENCRE} />\n      <circle cx=\"821\" cy=\"130\" r=\"18\" fill=\"#d2b282\" shapeRendering=\"auto\" />\n      <circle cx=\"821\" cy=\"128\" r=\"18\" fill=\"#e2c597\" shapeRendering=\"auto\" />\n      {[786, 848].map((x) => (\n        <g key={x}>\n          <rect x={x} y=\"140\" width=\"12\" height=\"10\" fill=\"#f5f5f4\" />\n          <rect x={x + 4} y=\"150\" width=\"4\" height=\"12\" fill={ENCRE} />\n        </g>\n      ))}\n      <Plante x={700} y={160} />\n\n      {/* La grande salle : les cartons, les plantes. */}\n      <CartonsKraft x={56} y={bas - 44} lien={lien} />\n      <Plante x={946} y={bas - 32} />\n\n      {/* Le mur de la grande salle, blanc à lames de chêne, percé de ses trois entrées. */}\n      {MUR_SALLE.map(([de, a]) => (\n        <g key={de}>\n          <MurBlanc x={de} largeur={a - de} y={204} hauteur={38} lames={a - de > 90 ? 4 : 0} />\n        </g>\n      ))}\n      <Lettres x={695} y={228} texte={titre} taille={titre.length > 14 ? 7 : 9} />\n      <Porte bas={bas} />\n      <rect x=\"330\" y=\"14\" width=\"8\" height=\"190\" fill=\"#bae6fd\" opacity=\"0.35\" />\n      <rect x=\"330\" y=\"14\" width=\"2\" height=\"190\" fill=\"#7dd3fc\" opacity=\"0.7\" />\n      <rect x=\"682\" y=\"14\" width=\"8\" height=\"190\" fill={MUR.arete} />\n      <rect x=\"12\" y=\"14\" width=\"8\" height={bas - 6} fill={MUR.arete} />\n      <rect x=\"980\" y=\"14\" width=\"8\" height={bas - 6} fill={MUR.arete} />\n      <rect x=\"12\" y=\"6\" width=\"976\" height=\"8\" fill={MUR.arete} />\n      <rect x=\"12\" y={bas} width=\"976\" height=\"10\" fill={MUR.arete} />\n    </>\n  )\n}\n\n/** La bibliothèque en chêne : livres aux tons doux, plantes retombantes, un vase. */\nfunction Bibliotheque({ x }: { x: number }) {\n  const livres = ['#fda4af', '#93c5fd', '#fde68a', '#a7f3d0', '#c4b5fd', '#fdba74', '#e7e5e4']\n  return (\n    <g>\n      <rect x={x} y=\"20\" width=\"56\" height=\"86\" fill=\"#c8a06a\" />\n      <rect x={x + 3} y=\"23\" width=\"50\" height=\"80\" fill=\"#f5f0e6\" />\n      {[44, 66, 88].map((y) => (\n        <rect key={y} x={x + 3} y={y} width=\"50\" height=\"3\" fill=\"#c8a06a\" />\n      ))}\n      {[24, 46, 68].map((y, rang) => (\n        <g key={y}>\n          {Array.from({ length: 6 }, (_, i) => (\n            <rect\n              key={i}\n              x={x + 6 + i * 7}\n              y={y + 5 + ((i + rang) % 3)}\n              width=\"5\"\n              height={15 - ((i + rang) % 3)}\n              fill={livres[(i * 2 + rang) % livres.length]}\n            />\n          ))}\n        </g>\n      ))}\n      <rect x={x + 38} y=\"74\" width=\"10\" height=\"12\" fill=\"#e7e5e4\" />\n      <rect x={x + 36} y=\"70\" width=\"4\" height=\"6\" fill=\"#4ade80\" />\n      <rect x={x + 44} y=\"68\" width=\"4\" height=\"8\" fill=\"#22c55e\" />\n      <rect x={x + 8} y=\"90\" width=\"2\" height=\"12\" fill=\"#22c55e\" />\n      <rect x={x + 12} y=\"90\" width=\"2\" height=\"9\" fill=\"#4ade80\" />\n    </g>\n  )\n}\n\n/** L'écran de présentation au mur : une courbe qui monte, des barres. */\nfunction EcranPresentation({ x }: { x: number }) {\n  return (\n    <g>\n      <rect x={x} y=\"22\" width=\"116\" height=\"42\" fill={ENCRE} />\n      <rect x={x + 3} y=\"25\" width=\"110\" height=\"36\" fill=\"#f8fafc\" />\n      <rect x={x + 8} y=\"29\" width=\"40\" height=\"4\" fill=\"#cbd5e1\" />\n      {[0, 1, 2, 3, 4].map((i) => (\n        <rect\n          key={i}\n          x={x + 10 + i * 9}\n          y={56 - (i + 1) * 3}\n          width=\"6\"\n          height={(i + 1) * 3}\n          fill={['#93c5fd', '#a7f3d0', '#fde68a', '#fda4af', '#c4b5fd'][i]}\n        />\n      ))}\n      <path\n        d={`M${x + 60} 56 L${x + 72} 50 L${x + 82} 52 L${x + 94} 40 L${x + 106} 34`}\n        stroke=\"#22c55e\"\n        strokeWidth=\"2\"\n        fill=\"none\"\n        shapeRendering=\"auto\"\n      />\n      <rect x={x + 104} y=\"32\" width=\"4\" height=\"4\" fill=\"#22c55e\">\n        <Anim attributeName=\"opacity\" values=\"1;0.3;1\" dur=\"1.6s\" repeatCount=\"indefinite\" />\n      </rect>\n      <rect x={x + 54} y=\"64\" width=\"8\" height=\"6\" fill=\"#a8a29e\" />\n    </g>\n  )\n}\n\n/** Le canapé vert sauge, vu de dos depuis la salle. */\nfunction CanapeSauge({ x, y }: { x: number; y: number }) {\n  return (\n    <g>\n      <rect x={x} y={y + 10} width=\"104\" height=\"24\" fill=\"#6b8f71\" />\n      <rect x={x + 6} y={y} width=\"92\" height=\"20\" fill=\"#86a98b\" />\n      <rect x={x + 50} y={y} width=\"2\" height=\"20\" fill=\"#6b8f71\" />\n      <rect x={x} y={y + 4} width=\"10\" height=\"30\" fill=\"#6b8f71\" />\n      <rect x={x + 94} y={y + 4} width=\"10\" height=\"30\" fill=\"#6b8f71\" />\n      <rect x={x + 4} y={y + 34} width=\"4\" height=\"4\" fill=\"#c8a06a\" />\n      <rect x={x + 96} y={y + 34} width=\"4\" height=\"4\" fill=\"#c8a06a\" />\n    </g>\n  )\n}\n\n/** La table de ping-pong, son filet, ses deux raquettes et la balle qui rebondit. */\nfunction PingPong({ x }: { x: number }) {\n  return (\n    <g>\n      <rect x={x} y=\"38\" width=\"76\" height=\"50\" fill=\"#1e40af\" />\n      <rect x={x + 2} y=\"40\" width=\"72\" height=\"46\" fill=\"#2563eb\" />\n      <rect x={x + 37} y=\"40\" width=\"2\" height=\"46\" fill=\"#f8fafc\" />\n      <rect x={x + 2} y=\"62\" width=\"72\" height=\"1\" fill=\"#f8fafc\" />\n      <rect x={x + 36} y=\"36\" width=\"4\" height=\"54\" fill=\"#cbd5e1\" opacity=\"0.8\" />\n      <rect x={x + 6} y=\"88\" width=\"4\" height=\"8\" fill={ENCRE} />\n      <rect x={x + 66} y=\"88\" width=\"4\" height=\"8\" fill={ENCRE} />\n      <rect x={x + 10} y=\"46\" width=\"7\" height=\"7\" fill=\"#ef4444\" />\n      <rect x={x + 58} y=\"72\" width=\"7\" height=\"7\" fill={ENCRE} />\n      <rect x={x + 20} y=\"58\" width=\"3\" height=\"3\" fill=\"#fafaf9\">\n        <Anim attributeName=\"x\" values={`${x + 20};${x + 54};${x + 20}`} dur=\"1.4s\" repeatCount=\"indefinite\" />\n        <Anim attributeName=\"y\" values=\"56;66;56\" dur=\"0.7s\" repeatCount=\"indefinite\" />\n      </rect>\n    </g>\n  )\n}\n\n/** La machine à espresso, chrome et noir. */\nfunction Espresso({ x }: { x: number }) {\n  return (\n    <g>\n      <rect x={x} y=\"22\" width=\"40\" height=\"38\" fill=\"#a8a29e\" />\n      <rect x={x + 2} y=\"24\" width=\"36\" height=\"12\" fill=\"#e7e5e4\" />\n      <rect x={x + 4} y=\"38\" width=\"32\" height=\"6\" fill={ENCRE} />\n      <rect x={x + 30} y=\"27\" width=\"4\" height=\"4\" fill=\"#22c55e\" />\n      <rect x={x + 14} y=\"44\" width=\"12\" height=\"4\" fill=\"#57534e\" />\n      <rect x={x + 15} y=\"50\" width=\"10\" height=\"8\" fill=\"#fafaf9\" />\n    </g>\n  )\n}\n\n/** Le café qui coule dans la tasse, quand quelqu'un attend devant. */\nfunction CafeModerne() {\n  return (\n    <g pointerEvents=\"none\">\n      <rect x=\"767\" y=\"47\" width=\"2\" height=\"5\" fill=\"#6b3a1e\">\n        <Anim attributeName=\"opacity\" values=\"1;0.4;1\" dur=\"0.5s\" repeatCount=\"indefinite\" />\n      </rect>\n    </g>\n  )\n}\n\n/** La fontaine à eau pétillante, encastrée, avec ses carafes en verre. */\nfunction EauPetillante({ x }: { x: number }) {\n  return (\n    <g>\n      <rect x={x} y=\"40\" width=\"38\" height=\"54\" fill=\"#d6d3d1\" />\n      <rect x={x + 2} y=\"42\" width=\"34\" height=\"50\" fill=\"#fafaf9\" />\n      <rect x={x + 8} y=\"46\" width=\"22\" height=\"10\" fill={ENCRE} />\n      <rect x={x + 11} y=\"48\" width=\"4\" height=\"2\" fill=\"#38bdf8\" />\n      <rect x={x + 23} y=\"48\" width=\"4\" height=\"2\" fill=\"#a3e635\" />\n      <rect x={x + 6} y=\"66\" width=\"8\" height=\"14\" fill=\"#bae6fd\" opacity=\"0.7\" />\n      <rect x={x + 22} y=\"66\" width=\"8\" height=\"14\" fill=\"#bae6fd\" opacity=\"0.7\" />\n    </g>\n  )\n}\n\n/** Le grand frigo inox à deux portes. */\nfunction FrigoInox({ x }: { x: number }) {\n  return (\n    <g>\n      <rect x={x} y=\"16\" width=\"54\" height=\"82\" fill=\"#a8a29e\" />\n      <rect x={x + 2} y=\"18\" width=\"24\" height=\"78\" fill=\"#d6d3d1\" />\n      <rect x={x + 28} y=\"18\" width=\"24\" height=\"78\" fill=\"#e7e5e4\" />\n      <rect x={x + 22} y=\"40\" width=\"2\" height=\"24\" fill=\"#78716c\" />\n      <rect x={x + 30} y=\"40\" width=\"2\" height=\"24\" fill=\"#78716c\" />\n      <rect x={x + 6} y=\"24\" width=\"10\" height=\"6\" fill={ENCRE} />\n    </g>\n  )\n}\n\n/** Des cartons en kraft, juste livrés. */\nfunction CartonsKraft({\n  x,\n  y,\n  lien,\n}: {\n  x: number\n  y: number\n  lien: (objet: SceneObject) => { titre: string; faire: () => void; actif: boolean }\n}) {\n  return (\n    <g>\n      <Cliquable {...lien('cpu-box')}>\n        <rect x={x} y={y + 14} width=\"40\" height=\"26\" fill=\"#c8a06a\" />\n        <rect x={x + 17} y={y + 14} width=\"6\" height=\"26\" fill=\"#e7d3ad\" />\n      </Cliquable>\n      <Cliquable {...lien('gpu-box')}>\n        <rect x={x + 6} y={y} width=\"30\" height=\"16\" fill=\"#d2b282\" />\n        <rect x={x + 6} y={y + 6} width=\"30\" height=\"3\" fill=\"#e7d3ad\" />\n      </Cliquable>\n      <Cliquable {...lien('ram-box')}>\n        <rect x={x + 44} y={y + 22} width=\"22\" height=\"18\" fill=\"#b88f5a\" />\n        <rect x={x + 53} y={y + 22} width=\"4\" height=\"18\" fill=\"#e7d3ad\" />\n      </Cliquable>\n    </g>\n  )\n}\n\n/* ——— Les postes ——— */\n\n/** Le portable, coque alu, ouvert sur le bureau ; ce que son écran affiche dit l'état de son agent. */\nfunction Portable({ cx, dy, statut, bientot }: { cx: number; dy: number; statut: Statut; bientot: number | null }) {\n  const t = useTextes()\n  const l = 52\n  const g = cx - l / 2\n  return (\n    <g>\n      {/* Le clavier du portable, puis la coque de l'écran. */}\n      <rect x={g - 6} y={dy + 10} width={l + 12} height=\"9\" fill=\"#a8a29e\" />\n      <rect x={g - 5} y={dy + 10} width={l + 10} height=\"7\" fill=\"#d6d3d1\" />\n      {[0, 1].map((rang) => (\n        <rect key={rang} x={g} y={dy + 11 + rang * 3} width={l} height=\"2\" fill=\"#a8a29e\" />\n      ))}\n      <rect x={cx - 7} y={dy + 17} width=\"14\" height=\"2\" fill=\"#c4c0bc\" />\n      <rect x={g - 2} y={dy - 16} width={l + 4} height=\"26\" fill=\"#d6d3d1\" />\n      {statut === 'endormi' ? (\n        <rect x={g} y={dy - 14} width={l} height=\"20\" fill={ENCRE} />\n      ) : statut === 'absent' ? (\n        <>\n          <rect x={g} y={dy - 14} width={l} height=\"20\" fill={ENCRE} />\n          <g transform={`rotate(-6 ${cx} ${dy - 4})`}>\n            <rect x={cx - 15} y={dy - 12} width=\"30\" height=\"15\" fill=\"#fde68a\" />\n            <rect x={cx - 15} y={dy - 12} width=\"30\" height=\"3\" fill=\"#fcd34d\" />\n            <text x={cx} y={dy} fontSize=\"7\" textAnchor=\"middle\" fill=\"#78350f\">\n              {t.offNote}\n            </text>\n          </g>\n        </>\n      ) : statut === 'en-echec' ? (\n        <>\n          <rect x={g} y={dy - 14} width={l} height=\"20\" fill=\"#fecaca\" />\n          <rect x={cx - 2} y={dy - 11} width=\"4\" height=\"9\" fill=\"#dc2626\" />\n          <rect x={cx - 2} y={dy} width=\"4\" height=\"3\" fill=\"#dc2626\" />\n        </>\n      ) : statut === 'au-travail' ? (\n        <>\n          <rect x={g} y={dy - 14} width={l} height=\"20\" fill=\"#f8fafc\" />\n          {[0, 1, 2, 3].map((i) => {\n            const w = [30, 18, 36, 14][i]\n            return (\n              <rect\n                key={i}\n                x={g + 3 + (i % 2) * 5}\n                y={dy - 12 + i * 4.5}\n                width={w}\n                height=\"2\"\n                fill={['#a78bfa', '#34d399', '#60a5fa', '#f472b6'][i]}\n              >\n                <Anim attributeName=\"width\" values={`${etapes(2, w, 5)};${w}`} dur=\"1.6s\" begin={`${i * 0.4}s`} repeatCount=\"indefinite\" />\n              </rect>\n            )\n          })}\n        </>\n      ) : (\n        <>\n          <rect x={g} y={dy - 14} width={l} height=\"20\" fill=\"#bfdbfe\" />\n          <rect x={g} y={dy - 4} width={l} height=\"10\" fill=\"#c7d2fe\" />\n          <rect x={g + 4} y={dy - 11} width=\"20\" height=\"12\" fill=\"#f8fafc\" />\n          <rect x={g + 28} y={dy - 9} width=\"18\" height=\"9\" fill=\"#e0e7ff\" />\n        </>\n      )}\n      {bientot ? (\n        <>\n          <rect x={g} y={dy - 1} width={l} height=\"7\" fill=\"#f8fafc\" />\n          <text x={cx} y={dy + 5} fontSize=\"6\" textAnchor=\"middle\" fill=\"#7c3aed\">\n            {t.inMinutes(bientot)}\n          </text>\n        </>\n      ) : null}\n      {statut === 'en-echec'\n        ? [0, 0.6, 1.2].map((debut, i) => (\n            <rect key={debut} x={cx + 18 + i * 3} y={dy + 6} width=\"6\" height=\"6\" fill=\"#a8a29e\" opacity=\"0\">\n              <Anim attributeName=\"y\" values={etapes(dy + 6, dy - 26, 6)} dur=\"1.8s\" begin={`${debut}s`} repeatCount=\"indefinite\" />\n              <Anim attributeName=\"opacity\" values={etapes(0.8, 0, 6)} dur=\"1.8s\" begin={`${debut}s`} repeatCount=\"indefinite\" />\n            </rect>\n          ))\n        : null}\n    </g>\n  )\n}\n\n/** Le plateau de chêne, sa tranche, ses pieds blancs de bureau assis-debout. */\nfunction PlateauChene({ x, dy, dessus }: { x: number; dy: number; dessus: number }) {\n  return (\n    <>\n      <rect x={x} y={dy} width={BUREAU.largeur} height={dessus} fill=\"#e2c597\" />\n      <rect x={x} y={dy} width={BUREAU.largeur} height=\"3\" fill=\"#ecd5ad\" />\n      <rect x={x} y={dy + dessus} width={BUREAU.largeur} height=\"12\" fill=\"#c8a06a\" />\n      <rect x={x + 6} y={dy + dessus + 12} width=\"5\" height=\"7\" fill=\"#f5f5f4\" />\n      <rect x={x + BUREAU.largeur - 11} y={dy + dessus + 12} width=\"5\" height=\"7\" fill=\"#f5f5f4\" />\n      <rect x={x + 2} y={dy + dessus + 18} width=\"13\" height=\"2\" fill=\"#d6d3d1\" />\n      <rect x={x + BUREAU.largeur - 15} y={dy + dessus + 18} width=\"13\" height=\"2\" fill=\"#d6d3d1\" />\n    </>\n  )\n}\n\n/** Le poste : le portable, une souris, carnet, tasse en céramique, une petite plante grasse. */\nfunction BureauModerne({ cx, dy, x, dessus, statut, bientot, emoji }: PropsBureau) {\n  return (\n    <g>\n      <PlateauChene x={x} dy={dy} dessus={dessus} />\n      <Portable cx={cx - 4} dy={dy} statut={statut} bientot={bientot} />\n      <rect x={cx + 20} y={dy + 23} width=\"5\" height=\"7\" fill=\"#fafaf9\" />\n      <rect x={x + 90} y={dy + 6} width=\"18\" height=\"14\" fill=\"#44403c\" />\n      <rect x={x + 92} y={dy + 6} width=\"2\" height=\"14\" fill=\"#f97316\" />\n      <rect x={x + 96} y={dy + 22} width=\"10\" height=\"10\" fill=\"#e7e5e4\" />\n      <rect x={x + 98} y={dy + 17} width=\"2\" height=\"6\" fill=\"#22c55e\" />\n      <rect x={x + 102} y={dy + 15} width=\"2\" height=\"8\" fill=\"#16a34a\" />\n      <rect x={x + 8} y={dy + 22} width=\"8\" height=\"9\" fill=\"#fef3c7\" />\n      <rect x={x + 16} y={dy + 24} width=\"3\" height=\"4\" fill=\"#fef3c7\" />\n      <rect x={x + 9} y={dy + 23} width=\"6\" height=\"2\" fill=\"#6b3a1e\" />\n      <text x={x + 12} y={dy + 13} fontSize=\"10\" textAnchor=\"middle\">\n        {emoji}\n      </text>\n    </g>\n  )\n}\n\n/** Le bureau du chef : un grand écran fin vu de dos, un portable, un carnet, une plante. */\nfunction BureauChefModerne({ dy, x, dessus }: PropsBureau) {\n  return (\n    <g>\n      <PlateauChene x={x} dy={dy} dessus={dessus} />\n      <rect x={x + 6} y={dy - 12} width=\"34\" height=\"22\" fill=\"#e7e5e4\" />\n      <rect x={x + 8} y={dy - 10} width=\"30\" height=\"18\" fill=\"#d6d3d1\" />\n      <rect x={x + 20} y={dy + 10} width=\"6\" height=\"5\" fill=\"#a8a29e\" />\n      <rect x={x + 84} y={dy + 6} width=\"26\" height=\"18\" fill=\"#d6d3d1\" />\n      <rect x={x + 86} y={dy + 8} width=\"22\" height=\"13\" fill=\"#e7e5e4\" />\n      <rect x={x + 96} y={dy + 13} width=\"3\" height=\"3\" fill=\"#a8a29e\" />\n      <rect x={x + 48} y={dy + 16} width=\"20\" height=\"14\" fill=\"#44403c\" />\n      <rect x={x + 66} y={dy + 16} width=\"2\" height=\"14\" fill=\"#f97316\" />\n    </g>\n  )\n}\n\n/** Le fauteuil blanc du chef, coque moulée. */\nfunction FauteuilModerne({ x, y }: { x: number; y: number }) {\n  return (\n    <g>\n      <rect x={x - 16} y={y - 30} width=\"32\" height=\"38\" fill=\"#d6d3d1\" />\n      <rect x={x - 13} y={y - 27} width=\"26\" height=\"32\" fill=\"#fafaf9\" />\n      <rect x={x - 18} y={y - 6} width=\"5\" height=\"14\" fill=\"#d6d3d1\" />\n      <rect x={x + 13} y={y - 6} width=\"5\" height=\"14\" fill=\"#d6d3d1\" />\n    </g>\n  )\n}\n\n/** La chaise en maille graphite ; un liseré de la couleur de l'agent. */\nfunction AssiseMaille({ x, y, couleur }: PropsSiege) {\n  return (\n    <g>\n      <rect x={x - 11} y={y - 12} width=\"22\" height=\"12\" fill=\"#57534e\" />\n      <rect x={x - 11} y={y - 12} width=\"22\" height=\"2\" fill={couleur} />\n    </g>\n  )\n}\n\nfunction DossierMaille({ x, y, couleur }: PropsSiege) {\n  return (\n    <g>\n      <rect x={x - 12} y={y + 2} width=\"24\" height=\"10\" fill=\"#44403c\" />\n      <rect x={x - 10} y={y + 4} width=\"20\" height=\"6\" fill=\"#57534e\" />\n      <rect x={x - 12} y={y + 2} width=\"24\" height=\"1\" fill={couleur} />\n      <rect x={x - 2} y={y + 12} width=\"4\" height=\"3\" fill=\"#a8a29e\" />\n      <rect x={x - 10} y={y + 15} width=\"20\" height=\"3\" fill=\"#d6d3d1\" />\n    </g>\n  )\n}\n\n/* ——— Les tenues ——— */\n\nconst HAUTS = ['#fda4af', '#93c5fd', '#fde68a', '#a7f3d0', '#c4b5fd', '#fdba74', '#f5f5f4', '#cbd5e1', '#99f6e4', '#1c1917']\nconst CHEVEUX = ['#2a1d14', '#5a3a22', '#c8a165', '#111111', '#8a4b2a', '#e5e5e5', '#b45309']\nconst PEAUX = ['#f1c9a5', '#d9a57a', '#a86f48', '#7a4a2c', '#f5d6bf']\nconst BAS = ['#d6c4a1', '#a8a29e', '#1e3a8a', '#44403c', '#e7e5e4']\n\n/** Le nombre de cet agent dans ce thème : chaque thème a sa graine, pour des tenues qui changent d'un thème à l'autre. */\nconst hachage = (id: string) => hacher(id, 17, 41)\n\n/** T-shirts pastel, chinos, baskets blanches ; le chef en surchemise bleu marine. */\nfunction tenueModerne(id: string): Record<string, string> {\n  if (id === CHEF_ID) {\n    return { h: '#78716c', s: '#f1c9a5', e: '#1c1917', c: '#1e3a8a', C: '#172554', t: '#f5f5f4', k: '#1c1917', p: '#d6c4a1', b: '#fafaf9' }\n  }\n  if (id.startsWith('livreur:')) {\n    return { h: '#5b3a1a', s: '#d9a57a', e: '#1c1917', c: '#7c4a1e', C: '#5b3a1a', t: '#facc15', k: '#111827', p: '#5b3a1a', b: '#111827' }\n  }\n  const h = hachage(id)\n  const haut = HAUTS[h % HAUTS.length]\n  return {\n    h: CHEVEUX[(h >>> 4) % CHEVEUX.length],\n    s: PEAUX[(h >>> 8) % PEAUX.length],\n    e: '#1c1917',\n    c: haut,\n    C: `color-mix(in oklab, ${haut} 80%, black)`,\n    t: haut,\n    k: '#f5f5f4',\n    p: BAS[(h >>> 12) % BAS.length],\n    b: '#fafaf9',\n  }\n}\n\n/** Un sur trois écoute de la musique, casque blanc. */\nconst casqueBlanc = (id: string) => id !== CHEF_ID && !estInvite(id) && hachage(id) % 3 === 0\n\n/* La nuit : de grosses suspensions chaudes au-dessus des pièces, l'écran de présentation, le frigo. */\nconst LUMIERES_MODERNE: Array<Lumiere> = [\n  { x: 175, y: 110, r: 90, couleur: '#fde68a' },\n  { x: 510, y: 120, r: 90, couleur: '#fde68a' },\n  { x: 835, y: 110, r: 90, couleur: '#fde68a' },\n  { x: 510, y: 44, r: 60, couleur: '#e0f2fe' },\n  { x: 373, y: 50, r: 45, couleur: '#a855f7' },\n  { x: 417, y: 50, r: 40, couleur: '#22d3ee' },\n]\n\nexport const THEME_MODERNE: Theme = {\n  coins: COINS_MODERNE,\n  coinCafe: 'cafe',\n  fond: '#e7e5e4',\n  Decor,\n  Bureau: BureauModerne,\n  BureauChef: BureauChefModerne,\n  plaque: '#44403c',\n  Assise: AssiseMaille,\n  Dossier: DossierMaille,\n  Fauteuil: FauteuilModerne,\n  tenue: tenueModerne,\n  casque: casqueBlanc,\n  CafeQuiCoule: CafeModerne,\n  lumieres: LUMIERES_MODERNE,\n  // La nuit claire : un crépuscule bleu ardoise, plus léger que les autres thèmes.\n  nuit: { couleur: '#1e293b', opacite: 0.42 },\n  // Le chef, sans cravate, sa tasse à la main.\n  tasse: (id) => id === CHEF_ID,\n  // Une lampe de bureau chaude au-dessus de chaque poste.\n  lumierePoste: (cx, dy) => [{ x: cx, y: dy + 10, r: 55, couleur: '#fde68a' }],\n}\n",
      "type": "registry:component",
      "target": "components/pixel-openspace/themes/modern.tsx"
    },
    {
      "path": "registry/pixel-openspace/themes/kitchen.tsx",
      "content": "'use client'\n\n/*\n * La cuisine de restaurant : damier gris, carrelage métro, pianos de\n * cuisson en inox. Au travail, la poêle grésille sur la flamme ; en échec,\n * elle brûle et fume noir. Le chef de cuisine a son bureau, sa cave à vins et\n * son ardoise ; la salle a ses bornes, l'ardoise du menu, sa banquette rouge\n * et le passe avec sa sonnette ; l'arrière-cuisine a la machine à café, la\n * plonge, le lave-vaisselle et la chambre froide. Des vestes de brigade de\n * toutes les couleurs, une toque pour la moitié, un bandana pour les autres.\n */\n\nimport { BUREAU, CHEF_ID, type CoinDePause, type Statut } from '../engine'\nimport { Anim, Cliquable, MUR_SALLE, etapes, hacher, useTextes, type Lumiere } from '../primitives'\nimport type { PropsBureau, PropsDecor, PropsSiege, Theme } from '../theme'\nimport { Arcade, BorneCasseBriques, Porte } from './geek'\nimport type { SceneObject } from '../types'\n\n/* Les coins de pause : la même géographie que les autres thèmes. */\nconst COINS_CUISINE: Array<CoinDePause> = [\n  { nom: 'cafe', x: 768, y: 100 },\n  { nom: 'plonge', x: 881, y: 104 },\n  { nom: 'chambre-froide', x: 950, y: 108 },\n  { nom: 'arcade', x: 372, y: 112 },\n  { nom: 'arcade-2', x: 415, y: 112 },\n  { nom: 'passe', x: 636, y: 108 },\n  // Sur la banquette de la salle, face au menu : de dos.\n  { nom: 'banquette-gauche', x: 482, y: 146, assis: true },\n  { nom: 'banquette-droite', x: 538, y: 146, assis: true },\n]\n\nconst INOX = { clair: '#e2e8f0', moyen: '#cbd5e1', sombre: '#94a3b8', ombre: '#64748b' }\nconst CRAIE = '#f1f5f9'\nconst ARDOISE = '#1f2937'\n\n/** Un mur en carrelage métro blanc, ses joints gris, et sa frise noire en haut. */\nfunction MurCarrele({ x, largeur, y = 20, hauteur = 32 }: { x: number; largeur: number; y?: number; hauteur?: number }) {\n  return (\n    <>\n      <rect x={x} y={y - 6} width={largeur} height=\"6\" fill=\"#57534e\" />\n      <rect x={x} y={y} width={largeur} height={hauteur} fill=\"url(#carrelage-metro)\" />\n      <rect x={x} y={y} width={largeur} height=\"3\" fill={ARDOISE} />\n      <rect x={x} y={y + hauteur} width={largeur} height=\"4\" fill=\"#d6d3d1\" />\n    </>\n  )\n}\n\n/** Une ardoise, son cadre en bois et un texte à la craie. */\nfunction Ardoise({\n  x,\n  y,\n  largeur,\n  hauteur,\n  texte,\n  taille = 9,\n}: {\n  x: number\n  y: number\n  largeur: number\n  hauteur: number\n  texte: string\n  taille?: number\n}) {\n  return (\n    <g>\n      <rect x={x} y={y} width={largeur} height={hauteur} fill=\"#78350f\" />\n      <rect x={x + 2} y={y + 2} width={largeur - 4} height={hauteur - 4} fill={ARDOISE} />\n      <text x={x + largeur / 2} y={y + hauteur / 2 + taille / 3} fontSize={taille} textAnchor=\"middle\" fill={CRAIE} letterSpacing=\"1\">\n        {texte}\n      </text>\n    </g>\n  )\n}\n\nfunction Decor({ hauteur, titre, liens, aller, jouer }: PropsDecor) {\n  const t = useTextes()\n  const bas = hauteur - 20\n  const lien = (objet: SceneObject) => ({ titre: t.objects[objet], faire: () => aller(objet), actif: Boolean(liens[objet]) })\n  return (\n    <>\n      <defs>\n        {/* Un damier adouci : en noir et blanc francs, les postes et les vestes blanches s'y perdaient. */}\n        <pattern id=\"damier\" width=\"32\" height=\"32\" patternUnits=\"userSpaceOnUse\">\n          <rect width=\"32\" height=\"32\" fill=\"#a8a29e\" />\n          <rect width=\"16\" height=\"16\" fill=\"#78716c\" />\n          <rect x=\"16\" y=\"16\" width=\"16\" height=\"16\" fill=\"#78716c\" />\n        </pattern>\n        <pattern id=\"carrelage-metro\" width=\"16\" height=\"8\" patternUnits=\"userSpaceOnUse\">\n          <rect width=\"16\" height=\"8\" fill=\"#f8fafc\" />\n          <rect y=\"7\" width=\"16\" height=\"1\" fill=\"#cbd5e1\" />\n          <rect x=\"15\" width=\"1\" height=\"4\" fill=\"#cbd5e1\" />\n          <rect x=\"7\" y=\"4\" width=\"1\" height=\"4\" fill=\"#cbd5e1\" />\n        </pattern>\n        <pattern id=\"parquet-fonce\" width=\"48\" height=\"12\" patternUnits=\"userSpaceOnUse\">\n          <rect width=\"48\" height=\"12\" fill=\"#6b3f22\" />\n          <rect y=\"11\" width=\"48\" height=\"1\" fill=\"#4a2a16\" />\n          <rect x=\"30\" width=\"1\" height=\"12\" fill=\"#4a2a16\" />\n        </pattern>\n        <pattern id=\"tomettes\" width=\"16\" height=\"16\" patternUnits=\"userSpaceOnUse\">\n          <rect width=\"16\" height=\"16\" fill=\"#b45309\" />\n          <rect width=\"16\" height=\"1\" fill=\"#92400e\" />\n          <rect width=\"1\" height=\"16\" fill=\"#92400e\" />\n        </pattern>\n        <pattern id=\"sol-plonge\" width=\"20\" height=\"20\" patternUnits=\"userSpaceOnUse\">\n          <rect width=\"20\" height=\"20\" fill=\"#d6d3d1\" />\n          <rect width=\"20\" height=\"1\" fill=\"#a8a29e\" />\n          <rect width=\"1\" height=\"20\" fill=\"#a8a29e\" />\n        </pattern>\n      </defs>\n\n      {/* Les sols : parquet foncé chez le chef, tomettes en salle, carreaux gris en arrière-cuisine, damier en cuisine. */}\n      <rect x=\"20\" y=\"56\" width=\"310\" height=\"142\" fill=\"url(#parquet-fonce)\" />\n      <rect x=\"338\" y=\"56\" width=\"344\" height=\"142\" fill=\"url(#tomettes)\" />\n      <rect x=\"690\" y=\"56\" width=\"290\" height=\"142\" fill=\"url(#sol-plonge)\" />\n      <rect x=\"20\" y=\"198\" width=\"960\" height={bas - 198} fill=\"url(#damier)\" />\n\n      {/* Les murs du fond. */}\n      <MurCarrele x={20} largeur={310} />\n      <MurCarrele x={338} largeur={344} />\n      <MurCarrele x={690} largeur={290} />\n\n      {/* Le bureau du chef de cuisine : la cave à vins, l'ardoise, le pot d'herbes. */}\n      <Cliquable {...lien('server-rack')}>\n        <CaveAVins x={32} />\n      </Cliquable>\n      <Ardoise x={160} y={22} largeur={124} hauteur={24} texte={t.leadOffice} taille={9} />\n      <PotDHerbes x={300} y={66} />\n\n      {/* La salle : les bornes, l'ardoise du menu, la banquette rouge et sa table, le passe. */}\n      <Cliquable titre={t.objects.snake} faire={() => jouer('snake')}>\n        <Arcade x={352} />\n      </Cliquable>\n      <Cliquable titre={t.objects.breakout} faire={() => jouer('casse-briques')}>\n        <BorneCasseBriques x={396} />\n      </Cliquable>\n      <Cliquable {...lien('tv')}>\n        <ArdoiseMenu x={452} />\n      </Cliquable>\n      <TableNappe x={478} y={98} />\n      <Banquette x={458} y={130} />\n      <Cliquable {...lien('workbench')}>\n        <Passe x={598} />\n      </Cliquable>\n      <PotDHerbes x={346} y={164} />\n\n      {/* L'arrière-cuisine : la machine à café, la plonge, le lave-vaisselle, la chambre froide. */}\n      <rect x=\"720\" y=\"44\" width=\"134\" height=\"40\" fill={INOX.sombre} />\n      <rect x=\"720\" y=\"44\" width=\"134\" height=\"4\" fill={INOX.clair} />\n      <MachineACafe x={748} />\n      <Plonge x={800} />\n      <Cliquable {...lien('vending-machine')}>\n        <LaveVaisselle x={862} />\n      </Cliquable>\n      <Cliquable {...lien('fridge')}>\n        <ChambreFroide x={914} />\n      </Cliquable>\n      <rect x=\"760\" y=\"140\" width=\"80\" height=\"10\" fill={INOX.moyen} />\n      <rect x=\"764\" y=\"150\" width=\"4\" height=\"16\" fill={INOX.ombre} />\n      <rect x=\"832\" y=\"150\" width=\"4\" height=\"16\" fill={INOX.ombre} />\n      <PotDHerbes x={704} y={164} />\n\n      {/* La cuisine : les cagettes de légumes livrées. */}\n      <Cagettes x={56} y={bas - 44} lien={lien} />\n\n      {/* Le mur de la cuisine, carrelé, percé de ses trois entrées ; l'enseigne à la craie. */}\n      {MUR_SALLE.map(([de, a]) => (\n        <g key={de}>\n          <MurCarrele x={de} largeur={a - de} y={204} hauteur={38} />\n        </g>\n      ))}\n      <Ardoise x={655} y={210} largeur={80} hauteur={26} texte={titre} taille={titre.length > 14 ? 6 : 8} />\n      <Porte bas={bas} />\n      <rect x=\"330\" y=\"14\" width=\"8\" height=\"190\" fill=\"#57534e\" />\n      <rect x=\"682\" y=\"14\" width=\"8\" height=\"190\" fill=\"#57534e\" />\n      <rect x=\"12\" y=\"14\" width=\"8\" height={bas - 6} fill=\"#44403c\" />\n      <rect x=\"980\" y=\"14\" width=\"8\" height={bas - 6} fill=\"#44403c\" />\n      <rect x=\"12\" y=\"6\" width=\"976\" height=\"8\" fill=\"#44403c\" />\n      <rect x=\"12\" y={bas} width=\"976\" height=\"10\" fill=\"#44403c\" />\n    </>\n  )\n}\n\n/** La cave à vins du chef : des casiers en bois, des bouteilles couchées. */\nfunction CaveAVins({ x }: { x: number }) {\n  const vins = ['#7f1d1d', '#14532d', '#7f1d1d', '#fde68a', '#7f1d1d', '#14532d']\n  return (\n    <g>\n      <rect x={x} y=\"20\" width=\"60\" height=\"86\" fill=\"#5b3412\" />\n      <rect x={x + 3} y=\"23\" width=\"54\" height=\"80\" fill=\"#2a170a\" />\n      {[0, 1, 2, 3].map((rang) =>\n        [0, 1, 2, 3, 4].map((col) => (\n          <g key={`${rang}-${col}`}>\n            <rect x={x + 5 + col * 10} y={26 + rang * 19} width=\"8\" height=\"16\" fill=\"#5b3412\" />\n            <circle cx={x + 9 + col * 10} cy={34 + rang * 19} r=\"3\" fill={vins[(rang * 5 + col) % vins.length]} shapeRendering=\"auto\" />\n          </g>\n        )),\n      )}\n    </g>\n  )\n}\n\n/** Un pot de terre cuite et ses herbes : basilic, thym, romarin. */\nfunction PotDHerbes({ x, y }: { x: number; y: number }) {\n  return (\n    <g>\n      <rect x={x - 2} y={y - 12} width=\"4\" height=\"10\" fill=\"#16a34a\" />\n      <rect x={x - 8} y={y - 10} width=\"5\" height=\"7\" fill=\"#22c55e\" />\n      <rect x={x + 3} y={y - 11} width=\"5\" height=\"8\" fill=\"#15803d\" />\n      <rect x={x - 9} y={y - 3} width=\"18\" height=\"12\" fill=\"#c2410c\" />\n      <rect x={x - 10} y={y - 4} width=\"20\" height=\"3\" fill=\"#9a3412\" />\n    </g>\n  )\n}\n\n/** L'ardoise du menu, au mur de la salle : le plat du jour, à la craie. */\nfunction ArdoiseMenu({ x }: { x: number }) {\n  return (\n    <g>\n      <rect x={x} y=\"20\" width=\"116\" height=\"46\" fill=\"#78350f\" />\n      <rect x={x + 3} y=\"23\" width=\"110\" height=\"40\" fill={ARDOISE} />\n      <text x={x + 58} y=\"34\" fontSize=\"8\" textAnchor=\"middle\" fill=\"#fde68a\" letterSpacing=\"1\">\n        MENU\n      </text>\n      {[40, 46, 52, 58].map((y, i) => (\n        <g key={y}>\n          <rect x={x + 12} y={y} width={[52, 40, 60, 34][i]} height=\"2\" fill={CRAIE} opacity=\"0.8\" />\n          <rect x={x + 92} y={y} width=\"10\" height=\"2\" fill=\"#fde68a\" opacity=\"0.9\" />\n        </g>\n      ))}\n    </g>\n  )\n}\n\n/** La table de la salle, sa nappe à carreaux rouges, deux assiettes, une bougie. */\nfunction TableNappe({ x, y }: { x: number; y: number }) {\n  return (\n    <g>\n      <rect x={x} y={y} width=\"64\" height=\"22\" fill=\"#fafaf9\" />\n      {Array.from({ length: 8 }, (_, i) => (\n        <rect key={i} x={x + i * 8} y={y} width=\"4\" height=\"22\" fill=\"#dc2626\" opacity=\"0.55\" />\n      ))}\n      {[0, 1].map((i) => (\n        <rect key={i} x={x} y={y + 4 + i * 10} width=\"64\" height=\"4\" fill=\"#dc2626\" opacity=\"0.45\" />\n      ))}\n      <circle cx={x + 16} cy={y + 11} r=\"6\" fill=\"#f8fafc\" shapeRendering=\"auto\" />\n      <circle cx={x + 48} cy={y + 11} r=\"6\" fill=\"#f8fafc\" shapeRendering=\"auto\" />\n      <rect x={x + 31} y={y + 4} width=\"3\" height=\"8\" fill=\"#fef3c7\" />\n      <rect x={x + 31} y={y + 1} width=\"3\" height=\"3\" fill=\"#f59e0b\">\n        <Anim attributeName=\"opacity\" values=\"1;0.6;1\" dur=\"0.9s\" repeatCount=\"indefinite\" />\n      </rect>\n    </g>\n  )\n}\n\n/** La banquette en skaï rouge, capitonnée, vue de dos. */\nfunction Banquette({ x, y }: { x: number; y: number }) {\n  return (\n    <g>\n      <rect x={x} y={y + 10} width=\"104\" height=\"24\" fill=\"#991b1b\" />\n      <rect x={x + 4} y={y} width=\"96\" height=\"20\" fill=\"#b91c1c\" />\n      {[16, 36, 56, 76].map((dx) => (\n        <rect key={dx} x={x + dx} y={y + 8} width=\"2\" height=\"2\" fill=\"#7f1d1d\" />\n      ))}\n      <rect x={x + 4} y={y + 34} width=\"4\" height=\"4\" fill=\"#44403c\" />\n      <rect x={x + 96} y={y + 34} width=\"4\" height=\"4\" fill=\"#44403c\" />\n    </g>\n  )\n}\n\n/** Le passe : le comptoir inox, les lampes chauffantes, les assiettes qui attendent, la sonnette. */\nfunction Passe({ x }: { x: number }) {\n  return (\n    <g>\n      <rect x={x} y=\"20\" width=\"76\" height=\"4\" fill={INOX.ombre} />\n      {[x + 8, x + 34, x + 60].map((lx) => (\n        <g key={lx}>\n          <rect x={lx} y=\"24\" width=\"8\" height=\"8\" fill={INOX.sombre} />\n          <rect x={lx + 1} y=\"32\" width=\"6\" height=\"2\" fill=\"#f97316\">\n            <Anim attributeName=\"opacity\" values=\"1;0.7;1\" dur=\"1.2s\" repeatCount=\"indefinite\" />\n          </rect>\n        </g>\n      ))}\n      <rect x={x} y=\"60\" width=\"76\" height=\"10\" fill={INOX.clair} />\n      <rect x={x} y=\"70\" width=\"76\" height=\"20\" fill={INOX.sombre} />\n      {[x + 8, x + 30, x + 52].map((px) => (\n        <g key={px}>\n          <circle cx={px + 8} cy=\"58\" r=\"8\" fill=\"#f8fafc\" shapeRendering=\"auto\" />\n          <rect x={px + 4} y=\"54\" width=\"8\" height=\"5\" fill=\"#ea580c\" />\n          <rect x={px + 6} y=\"53\" width=\"3\" height=\"2\" fill=\"#16a34a\" />\n        </g>\n      ))}\n      <rect x={x + 66} y=\"48\" width=\"6\" height=\"4\" fill=\"#fbbf24\" />\n      <rect x={x + 68} y=\"46\" width=\"2\" height=\"2\" fill=\"#fbbf24\" />\n    </g>\n  )\n}\n\n/** La machine à café du personnel, inox et noir. */\nfunction MachineACafe({ x }: { x: number }) {\n  return (\n    <g>\n      <rect x={x} y=\"18\" width=\"40\" height=\"30\" fill={INOX.ombre} />\n      <rect x={x + 2} y=\"20\" width=\"36\" height=\"10\" fill={INOX.moyen} />\n      <rect x={x + 30} y=\"23\" width=\"4\" height=\"4\" fill=\"#22c55e\" />\n      <rect x={x + 14} y=\"34\" width=\"12\" height=\"4\" fill=\"#1f2937\" />\n      <rect x={x + 15} y=\"38\" width=\"10\" height=\"8\" fill=\"#fafaf9\" />\n    </g>\n  )\n}\n\n/** Le café qui coule dans la tasse, quand quelqu'un attend devant. */\nfunction CafeCuisine() {\n  return (\n    <g pointerEvents=\"none\">\n      <rect x=\"767\" y=\"36\" width=\"2\" height=\"5\" fill=\"#6b3a1e\">\n        <Anim attributeName=\"opacity\" values=\"1;0.4;1\" dur=\"0.5s\" repeatCount=\"indefinite\" />\n      </rect>\n    </g>\n  )\n}\n\n/** La plonge : le bac inox, le robinet col de cygne, la pile d'assiettes. */\nfunction Plonge({ x }: { x: number }) {\n  return (\n    <g>\n      <rect x={x} y=\"50\" width=\"50\" height=\"10\" fill=\"#475569\" />\n      <rect x={x + 22} y=\"28\" width=\"3\" height=\"22\" fill={INOX.clair} />\n      <rect x={x + 22} y=\"28\" width=\"12\" height=\"3\" fill={INOX.clair} />\n      <rect x={x + 32} y=\"31\" width=\"2\" height=\"5\" fill={INOX.clair} />\n      {[0, 1, 2, 3].map((i) => (\n        <rect key={i} x={x + 4} y={46 - i * 3} width=\"14\" height=\"2\" fill=\"#f8fafc\" />\n      ))}\n    </g>\n  )\n}\n\n/** Le lave-vaisselle à capot, qui fume à chaque cycle. */\nfunction LaveVaisselle({ x }: { x: number }) {\n  return (\n    <g>\n      <rect x={x} y=\"22\" width=\"44\" height=\"72\" fill={INOX.sombre} />\n      <rect x={x + 2} y=\"24\" width=\"40\" height=\"40\" fill={INOX.moyen} />\n      <rect x={x + 6} y=\"70\" width=\"32\" height=\"6\" fill={INOX.ombre} />\n      <rect x={x + 30} y=\"80\" width=\"6\" height=\"4\" fill=\"#22c55e\" />\n      {[0, 0.8].map((debut) => (\n        <rect key={debut} x={x + 18} y=\"18\" width=\"8\" height=\"6\" fill=\"#f8fafc\" opacity=\"0\">\n          <Anim attributeName=\"y\" values={etapes(18, 0, 5)} dur=\"1.6s\" begin={`${debut}s`} repeatCount=\"indefinite\" />\n          <Anim attributeName=\"opacity\" values={etapes(0.7, 0, 5)} dur=\"1.6s\" begin={`${debut}s`} repeatCount=\"indefinite\" />\n        </rect>\n      ))}\n    </g>\n  )\n}\n\n/** La chambre froide : sa porte inox épaisse, sa poignée, son givre. */\nfunction ChambreFroide({ x }: { x: number }) {\n  return (\n    <g>\n      <rect x={x} y=\"14\" width=\"60\" height=\"84\" fill={INOX.ombre} />\n      <rect x={x + 3} y=\"17\" width=\"54\" height=\"78\" fill={INOX.moyen} />\n      <rect x={x + 44} y=\"44\" width=\"8\" height=\"20\" fill=\"#334155\" />\n      <rect x={x + 8} y=\"22\" width=\"22\" height=\"8\" fill=\"#0f172a\" />\n      <text x={x + 19} y=\"28\" fontSize=\"5\" textAnchor=\"middle\" fill=\"#38bdf8\">\n        -2°\n      </text>\n      {[24, 40, 56].map((y) => (\n        <rect key={y} x={x + 3} y={y} width=\"54\" height=\"1\" fill=\"#e0f2fe\" opacity=\"0.6\" />\n      ))}\n    </g>\n  )\n}\n\n/** Les cagettes de légumes livrées : tomates, carottes, salades. */\nfunction Cagettes({\n  x,\n  y,\n  lien,\n}: {\n  x: number\n  y: number\n  lien: (objet: SceneObject) => { titre: string; faire: () => void; actif: boolean }\n}) {\n  const cagette = (cx: number, cy: number, largeur: number, legume: string) => (\n    <>\n      <rect x={cx} y={cy} width={largeur} height=\"16\" fill=\"#d4a373\" />\n      <rect x={cx} y={cy + 6} width={largeur} height=\"2\" fill=\"#a47148\" />\n      {Array.from({ length: Math.floor(largeur / 7) }, (_, i) => (\n        <rect key={i} x={cx + 2 + i * 7} y={cy - 4} width=\"6\" height=\"6\" fill={legume} />\n      ))}\n    </>\n  )\n  return (\n    <g>\n      <Cliquable {...lien('cpu-box')}>{cagette(x, y + 24, 40, '#dc2626')}</Cliquable>\n      <Cliquable {...lien('gpu-box')}>{cagette(x + 6, y + 4, 30, '#f97316')}</Cliquable>\n      <Cliquable {...lien('ram-box')}>{cagette(x + 46, y + 24, 22, '#22c55e')}</Cliquable>\n    </g>\n  )\n}\n\n/* ——— Les postes ——— */\n\n/** La barre à bons au-dessus du piano : le bon en cours, ou le post-it d'un poste fermé. */\nfunction BarreABons({ cx, dy, statut, bientot }: { cx: number; dy: number; statut: Statut; bientot: number | null }) {\n  const t = useTextes()\n  return (\n    <g>\n      <rect x={cx - 30} y={dy - 18} width=\"60\" height=\"3\" fill={INOX.ombre} />\n      {statut === 'absent' ? (\n        <g transform={`rotate(-5 ${cx} ${dy - 8})`}>\n          <rect x={cx - 15} y={dy - 16} width=\"30\" height=\"14\" fill=\"#fde68a\" />\n          <text x={cx} y={dy - 6} fontSize=\"7\" textAnchor=\"middle\" fill=\"#78350f\">\n            {t.offNote}\n          </text>\n        </g>\n      ) : (\n        [-20, -4, 12].map((dx, i) => (\n          <g key={dx}>\n            <rect\n              x={cx + dx}\n              y={dy - 15}\n              width=\"12\"\n              height={i === 1 && statut === 'en-echec' ? 12 : 10}\n              fill={i === 1 && statut === 'en-echec' ? '#fecaca' : '#fafaf9'}\n            />\n            <rect x={cx + dx + 2} y={dy - 12} width=\"8\" height=\"1\" fill=\"#94a3b8\" />\n            <rect x={cx + dx + 2} y={dy - 9} width=\"6\" height=\"1\" fill=\"#94a3b8\" />\n          </g>\n        ))\n      )}\n      {bientot ? (\n        <>\n          <rect x={cx - 24} y={dy - 6} width=\"48\" height=\"8\" fill=\"#fafaf9\" />\n          <text x={cx} y={dy} fontSize=\"6\" textAnchor=\"middle\" fill=\"#b91c1c\">\n            {t.inMinutes(bientot)}\n          </text>\n        </>\n      ) : null}\n    </g>\n  )\n}\n\n/** La poêle sur son feu : ce qui s'y passe dit l'état de l'agent. */\nfunction Feu({ cx, dy, statut }: { cx: number; dy: number; statut: Statut }) {\n  const flamme = statut === 'au-travail' || statut === 'en-echec'\n  const echec = statut === 'en-echec'\n  return (\n    <g>\n      {/* Le brûleur, puis ses flammes. */}\n      <rect x={cx - 14} y={dy + 14} width=\"28\" height=\"4\" fill=\"#1f2937\" />\n      {flamme\n        ? [-10, -4, 2, 8].map((dx, i) => (\n            <rect key={dx} x={cx + dx} y={dy + (echec ? 6 : 10)} width=\"4\" height={echec ? 10 : 5} fill={echec ? '#f97316' : '#3b82f6'}>\n              <Anim attributeName=\"height\" values={echec ? `10;14;8;10` : `5;7;4;5`} dur={`${0.4 + i * 0.1}s`} repeatCount=\"indefinite\" />\n            </rect>\n          ))\n        : null}\n      {statut === 'absent' || statut === 'endormi' ? (\n        // Feux éteints, le couvercle posé sur la marmite.\n        <>\n          <rect x={cx - 12} y={dy + 2} width=\"24\" height=\"12\" fill={INOX.ombre} />\n          <rect x={cx - 13} y={dy} width=\"26\" height=\"3\" fill={INOX.sombre} />\n          <rect x={cx - 2} y={dy - 2} width=\"4\" height=\"2\" fill=\"#1f2937\" />\n        </>\n      ) : statut === 'au-travail' || statut === 'en-echec' ? (\n        // La poêle et son manche ; dedans, ce qui saute, ou ce qui brûle.\n        <>\n          <rect x={cx - 14} y={dy + 4} width=\"28\" height=\"8\" fill=\"#1f2937\" />\n          <rect x={cx + 14} y={dy + 6} width=\"14\" height=\"3\" fill=\"#44403c\" />\n          <rect x={cx - 10} y={dy + 4} width=\"20\" height=\"3\" fill={echec ? '#1c1917' : '#ea580c'} />\n          {echec ? null : (\n            <>\n              <rect x={cx - 6} y={dy} width=\"4\" height=\"3\" fill=\"#16a34a\">\n                <Anim attributeName=\"y\" values={`${dy};${dy - 8};${dy}`} dur=\"0.8s\" repeatCount=\"indefinite\" />\n              </rect>\n              <rect x={cx + 3} y={dy + 1} width=\"3\" height=\"3\" fill=\"#facc15\">\n                <Anim attributeName=\"y\" values={`${dy + 1};${dy - 6};${dy + 1}`} dur=\"0.8s\" begin=\"0.4s\" repeatCount=\"indefinite\" />\n              </rect>\n            </>\n          )}\n        </>\n      ) : (\n        // Au repos, la marmite mijote.\n        <>\n          <rect x={cx - 11} y={dy + 1} width=\"22\" height=\"13\" fill={INOX.sombre} />\n          <rect x={cx - 12} y={dy} width=\"24\" height=\"3\" fill={INOX.clair} />\n          <rect x={cx - 9} y={dy + 1} width=\"18\" height=\"2\" fill=\"#b45309\" />\n        </>\n      )}\n      {/* La vapeur de ce qui cuit, ou la fumée noire de ce qui brûle. */}\n      {statut === 'absent' || statut === 'endormi'\n        ? null\n        : [0, 0.6, 1.2].map((debut, i) => (\n            <rect\n              key={debut}\n              x={cx - 6 + i * 5}\n              y={dy}\n              width={echec ? 8 : 4}\n              height={echec ? 8 : 4}\n              fill={echec ? '#1c1917' : '#f8fafc'}\n              opacity=\"0\"\n            >\n              <Anim\n                attributeName=\"y\"\n                values={etapes(dy, dy - (echec ? 30 : 18), 6)}\n                dur=\"1.8s\"\n                begin={`${debut}s`}\n                repeatCount=\"indefinite\"\n              />\n              <Anim\n                attributeName=\"opacity\"\n                values={etapes(echec ? 0.85 : 0.5, 0, 6)}\n                dur=\"1.8s\"\n                begin={`${debut}s`}\n                repeatCount=\"indefinite\"\n              />\n            </rect>\n          ))}\n    </g>\n  )\n}\n\n/** Le piano de cuisson : le dessus inox, la façade et ses boutons. */\nfunction Piano({ x, dy, dessus }: { x: number; dy: number; dessus: number }) {\n  return (\n    <>\n      <rect x={x} y={dy} width={BUREAU.largeur} height={dessus} fill={INOX.moyen} />\n      <rect x={x} y={dy} width={BUREAU.largeur} height=\"3\" fill={INOX.clair} />\n      <rect x={x} y={dy + dessus} width={BUREAU.largeur} height=\"12\" fill={INOX.sombre} />\n      {[0.18, 0.82].map((k) => (\n        <rect key={k} x={x + BUREAU.largeur * k - 3} y={dy + dessus + 12} width=\"6\" height=\"7\" fill={INOX.ombre} />\n      ))}\n    </>\n  )\n}\n\n/** Le poste : le piano, le feu, la barre à bons, la planche à découper et le sel. */\nfunction PosteCuisson({ cx, dy, x, dessus, statut, bientot, emoji }: PropsBureau) {\n  return (\n    <g>\n      <Piano x={x} dy={dy} dessus={dessus} />\n      <BarreABons cx={cx - 4} dy={dy} statut={statut} bientot={bientot} />\n      {/* Le feu, grossi de moitié : c'est lui qui dit l'état du poste, il doit se lire de loin. */}\n      <g transform={`translate(${cx - 4} ${dy + 14}) scale(1.5) translate(${-(cx - 4)} ${-(dy + 14)})`}>\n        <Feu cx={cx - 4} dy={dy + 4} statut={statut} />\n      </g>\n      {/* La planche à découper et ses légumes, le sel, le torchon. */}\n      <rect x={x + 86} y={dy + 6} width=\"22\" height=\"14\" fill=\"#d4a373\" />\n      <rect x={x + 90} y={dy + 9} width=\"5\" height=\"5\" fill=\"#dc2626\" />\n      <rect x={x + 98} y={dy + 10} width=\"6\" height=\"3\" fill=\"#f97316\" />\n      <rect x={x + 96} y={dy + 24} width=\"6\" height=\"8\" fill=\"#f8fafc\" />\n      <rect x={x + 6} y={dy + 22} width=\"14\" height=\"8\" fill=\"#f8fafc\" />\n      <rect x={x + 6} y={dy + 25} width=\"14\" height=\"1\" fill=\"#3b82f6\" />\n      <text x={x + 12} y={dy + 13} fontSize=\"10\" textAnchor=\"middle\">\n        {emoji}\n      </text>\n    </g>\n  )\n}\n\n/** Le bureau du chef de cuisine : le livre de recettes, le portable, un verre de vin. */\nfunction BureauChefCuisine({ dy, x, dessus }: PropsBureau) {\n  return (\n    <g>\n      <rect x={x} y={dy} width={BUREAU.largeur} height={dessus} fill=\"#8b5a2b\" />\n      <rect x={x} y={dy} width={BUREAU.largeur} height=\"3\" fill=\"#a16207\" />\n      <rect x={x} y={dy + dessus} width={BUREAU.largeur} height=\"12\" fill=\"#5b3412\" />\n      <rect x={x + 8} y={dy + 6} width=\"30\" height=\"20\" fill=\"#7f1d1d\" />\n      <rect x={x + 22} y={dy + 6} width=\"2\" height=\"20\" fill=\"#fde68a\" />\n      <rect x={x + 84} y={dy + 6} width=\"26\" height=\"18\" fill={INOX.sombre} />\n      <rect x={x + 86} y={dy + 8} width=\"22\" height=\"13\" fill={INOX.moyen} />\n      <rect x={x + 56} y={dy + 10} width=\"6\" height=\"8\" fill=\"#7f1d1d\" opacity=\"0.85\" />\n      <rect x={x + 58} y={dy + 18} width=\"2\" height=\"6\" fill=\"#e5e7eb\" />\n    </g>\n  )\n}\n\n/** Le fauteuil en cuir du chef de cuisine. */\nfunction FauteuilCuir({ x, y }: { x: number; y: number }) {\n  return (\n    <g>\n      <rect x={x - 16} y={y - 30} width=\"32\" height=\"38\" fill=\"#5b3412\" />\n      <rect x={x - 13} y={y - 27} width=\"26\" height=\"32\" fill=\"#7c4a1e\" />\n      <rect x={x - 18} y={y - 6} width=\"5\" height=\"14\" fill=\"#5b3412\" />\n      <rect x={x + 13} y={y - 6} width=\"5\" height=\"14\" fill=\"#5b3412\" />\n    </g>\n  )\n}\n\n/** Le tabouret inox des postes, un liseré de la couleur de l'agent. */\nfunction AssiseTabouret({ x, y, couleur }: PropsSiege) {\n  return (\n    <g>\n      <rect x={x - 10} y={y - 11} width=\"20\" height=\"10\" fill={INOX.sombre} />\n      <rect x={x - 10} y={y - 11} width=\"20\" height=\"2\" fill={couleur} />\n    </g>\n  )\n}\n\nfunction PiedsTabouret({ x, y, couleur }: PropsSiege) {\n  return (\n    <g>\n      <rect x={x - 10} y={y + 4} width=\"20\" height=\"3\" fill={INOX.ombre} />\n      <rect x={x - 10} y={y + 4} width=\"20\" height=\"1\" fill={couleur} />\n      <rect x={x - 8} y={y + 7} width=\"2\" height=\"10\" fill={INOX.ombre} />\n      <rect x={x + 6} y={y + 7} width=\"2\" height=\"10\" fill={INOX.ombre} />\n      <rect x={x - 8} y={y + 13} width=\"16\" height=\"1\" fill={INOX.ombre} />\n    </g>\n  )\n}\n\n/* ——— Les tenues ——— */\n\nconst TABLIERS = ['#f8fafc', '#1e3a8a', '#1f2937', '#7f1d1d', '#f8fafc', '#334155']\n/* Des vestes de toutes les brigades : tout en blanc, de dos, les cuisiniers ne se distinguaient plus. */\nconst VESTES = ['#f8fafc', '#1f2937', '#f8fafc', '#334155', '#1e3a8a', '#7f1d1d', '#e2e8f0']\nconst BANDANAS = ['#dc2626', '#2563eb', '#16a34a', '#f59e0b', '#9333ea']\nconst PEAUX = ['#f1c9a5', '#d9a57a', '#a86f48', '#7a4a2c', '#f5d6bf']\nconst PANTALONS = ['#1f2937', '#374151', '#111827']\n\n/** Le nombre de cet agent dans ce thème : chaque thème a sa graine, pour des tenues qui changent d'un thème à l'autre. */\nconst hachage = (id: string) => hacher(id, 19, 43)\n\n/** Vestes de brigade de toutes les couleurs, toque ou bandana ; le chef de cuisine en veste noire et toque. */\nfunction tenueCuisine(id: string): Record<string, string> {\n  if (id === CHEF_ID) {\n    return { h: '#ffffff', s: '#f1c9a5', e: '#1c1917', c: '#111827', C: '#030712', t: '#f8fafc', k: '#e5e7eb', p: '#1f2937', b: '#111827' }\n  }\n  if (id.startsWith('livreur:')) {\n    return { h: '#5b3a1a', s: '#d9a57a', e: '#1c1917', c: '#16a34a', C: '#14532d', t: '#facc15', k: '#111827', p: '#1f2937', b: '#111827' }\n  }\n  const h = hachage(id)\n  const veste = VESTES[(h >>> 15) % VESTES.length]\n  return {\n    // Une toque blanche pour la moitié, un bandana de couleur pour les autres.\n    h: h % 2 === 0 ? BANDANAS[(h >>> 3) % BANDANAS.length] : '#ffffff',\n    s: PEAUX[(h >>> 6) % PEAUX.length],\n    e: '#1c1917',\n    c: veste,\n    C: `color-mix(in oklab, ${veste} 75%, black)`,\n    t: TABLIERS[(h >>> 9) % TABLIERS.length],\n    k: '#e5e7eb',\n    p: PANTALONS[(h >>> 12) % PANTALONS.length],\n    b: '#111827',\n  }\n}\n\n/* La nuit : les hottes au-dessus des pièces, les lampes chauffantes du passe, la bougie de la salle. */\nconst LUMIERES_CUISINE: Array<Lumiere> = [\n  { x: 175, y: 110, r: 85, couleur: '#fbbf24' },\n  { x: 510, y: 120, r: 80, couleur: '#fdba74' },\n  { x: 835, y: 110, r: 85, couleur: '#e2e8f0' },\n  { x: 636, y: 34, r: 50, couleur: '#f97316' },\n  { x: 510, y: 104, r: 30, couleur: '#f59e0b' },\n  { x: 373, y: 50, r: 45, couleur: '#a855f7' },\n  { x: 417, y: 50, r: 40, couleur: '#22d3ee' },\n]\n\nexport const THEME_CUISINE: Theme = {\n  coins: COINS_CUISINE,\n  coinCafe: 'cafe',\n  fond: '#1c1917',\n  Decor,\n  Bureau: PosteCuisson,\n  BureauChef: BureauChefCuisine,\n  plaque: '#0f172a',\n  Assise: AssiseTabouret,\n  Dossier: PiedsTabouret,\n  Fauteuil: FauteuilCuir,\n  tenue: tenueCuisine,\n  // Des toques et des bandanas, pas de casque.\n  casque: () => false,\n  CafeQuiCoule: CafeCuisine,\n  lumieres: LUMIERES_CUISINE,\n  // La hotte de chaque poste, et la flamme quand elle est allumée.\n  lumierePoste: (cx, dy) => [{ x: cx, y: dy + 8, r: 55, couleur: '#fdba74' }],\n}\n",
      "type": "registry:component",
      "target": "components/pixel-openspace/themes/kitchen.tsx"
    },
    {
      "path": "registry/pixel-openspace/themes/neon.tsx",
      "content": "'use client'\n\n/*\n * La ville néon, la nuit : béton sombre, tubes roses et cyan, enseignes en\n * katakana. Le chef est un fixer, derrière sa baie de serveurs ; l'espace\n * détente a ses bornes, un panneau holographique, un banc éclairé par en\n * dessous et l'établi où l'on répare un drone ; le coin repas est un stand de\n * ramen sous ses rideaux rouges, avec un distributeur de canettes qui\n * clignote et un frigo couvert d'autocollants. Les postes sont des écrans\n * holographiques : le code y défile en vert, une panne y crache des glitchs.\n * Sweats sombres, cheveux parfois roses, cyan ou violets, un casque sur deux.\n */\n\nimport { BUREAU, CHEF_ID, estInvite, type CoinDePause, type Statut } from '../engine'\nimport { Anim, Cliquable, MUR_SALLE, Neon, etapes, useTextes, hacher, type Lumiere } from '../primitives'\nimport type { PropsBureau, PropsDecor, PropsSiege, Theme } from '../theme'\nimport { Arcade, BorneCasseBriques, Porte } from './geek'\nimport type { SceneObject } from '../types'\n\n/* Les coins de pause : la même géographie que les autres thèmes. */\nconst COINS_NEON: Array<CoinDePause> = [\n  { nom: 'cafe', x: 768, y: 100 },\n  { nom: 'distributeur', x: 881, y: 104 },\n  { nom: 'frigo', x: 950, y: 108 },\n  { nom: 'arcade', x: 372, y: 112 },\n  { nom: 'arcade-2', x: 415, y: 112 },\n  { nom: 'etabli', x: 636, y: 108 },\n  // Sur le banc, face au panneau holographique : de dos.\n  { nom: 'banc-gauche', x: 482, y: 146, assis: true },\n  { nom: 'banc-droite', x: 538, y: 146, assis: true },\n]\n\nconst ROSE = '#f472b6'\nconst CYAN = '#22d3ee'\nconst VIOLET = '#a855f7'\nconst NUIT = '#0b0714'\nconst BETON = { arete: '#2a2238', face: '#171124', plinthe: '#0f0a1a' }\n\n/** Un mur de béton sombre, son tube néon en haut. */\nfunction MurNeon({\n  x,\n  largeur,\n  y = 20,\n  hauteur = 32,\n  tube,\n}: {\n  x: number\n  largeur: number\n  y?: number\n  hauteur?: number\n  tube?: string\n}) {\n  return (\n    <>\n      <rect x={x} y={y - 6} width={largeur} height=\"6\" fill={BETON.arete} />\n      <rect x={x} y={y} width={largeur} height={hauteur} fill={BETON.face} />\n      {tube ? (\n        <>\n          <rect x={x + 4} y={y + 2} width={largeur - 8} height=\"2\" fill={tube} filter=\"url(#flou-neon)\" opacity=\"0.8\" />\n          <rect x={x + 4} y={y + 2} width={largeur - 8} height=\"1\" fill=\"#fdf4ff\" opacity=\"0.9\" />\n        </>\n      ) : null}\n      <rect x={x} y={y + hauteur} width={largeur} height=\"4\" fill={BETON.plinthe} />\n    </>\n  )\n}\n\n/** Des katakana qui tombent sur un écran, colonne par colonne. */\nfunction Pluie({ x, y, largeur, hauteur, couleur = '#4ade80' }: { x: number; y: number; largeur: number; hauteur: number; couleur?: string }) {\n  const colonnes = Math.floor(largeur / 8)\n  const signes = 'アイウエオカキクケコサシスセソ'\n  return (\n    <g>\n      {Array.from({ length: colonnes }, (_, i) => (\n        <text key={i} x={x + 4 + i * 8} y={y + 8} fontSize=\"7\" textAnchor=\"middle\" fill={couleur} opacity=\"0.85\">\n          {signes[(i * 5) % signes.length]}\n          <Anim attributeName=\"y\" values={etapes(y + 6, y + hauteur - 2, 6)} dur={`${1.6 + (i % 3) * 0.5}s`} begin={`${i * 0.3}s`} repeatCount=\"indefinite\" />\n        </text>\n      ))}\n    </g>\n  )\n}\n\nfunction Decor({ hauteur, titre, liens, aller, jouer }: PropsDecor) {\n  const t = useTextes()\n  const bas = hauteur - 20\n  const lien = (objet: SceneObject) => ({ titre: t.objects[objet], faire: () => aller(objet), actif: Boolean(liens[objet]) })\n  return (\n    <>\n      <defs>\n        <pattern id=\"dalle-neon\" width=\"40\" height=\"40\" patternUnits=\"userSpaceOnUse\">\n          <rect width=\"40\" height=\"40\" fill=\"#120d1c\" />\n          <rect width=\"40\" height=\"1\" fill=\"#1f1830\" />\n          <rect width=\"1\" height=\"40\" fill=\"#1f1830\" />\n          {/* Les reflets mouillés des néons sur le sol. */}\n          <rect x=\"8\" y=\"14\" width=\"10\" height=\"1\" fill={CYAN} opacity=\"0.18\" />\n          <rect x=\"24\" y=\"30\" width=\"8\" height=\"1\" fill={ROSE} opacity=\"0.16\" />\n        </pattern>\n        <pattern id=\"moquette-neon\" width=\"10\" height=\"10\" patternUnits=\"userSpaceOnUse\">\n          <rect width=\"10\" height=\"10\" fill=\"#1d1230\" />\n          <rect x=\"2\" y=\"2\" width=\"2\" height=\"2\" fill=\"#241640\" />\n        </pattern>\n        <pattern id=\"grille-neon\" width=\"24\" height=\"24\" patternUnits=\"userSpaceOnUse\">\n          <rect width=\"24\" height=\"24\" fill=\"#0e0a17\" />\n          <rect width=\"24\" height=\"1\" fill={ROSE} opacity=\"0.22\" />\n          <rect width=\"1\" height=\"24\" fill={ROSE} opacity=\"0.22\" />\n        </pattern>\n        <pattern id=\"planches-neon\" width=\"32\" height=\"10\" patternUnits=\"userSpaceOnUse\">\n          <rect width=\"32\" height=\"10\" fill=\"#2a1a14\" />\n          <rect y=\"9\" width=\"32\" height=\"1\" fill=\"#1c110d\" />\n          <rect x=\"20\" width=\"1\" height=\"10\" fill=\"#1c110d\" />\n        </pattern>\n      </defs>\n\n      {/* Les sols : moquette violette chez le fixer, dalles quadrillées de rose à la détente, planches au stand de ramen, métal mouillé dans la grande salle. */}\n      <rect x=\"20\" y=\"56\" width=\"310\" height=\"142\" fill=\"url(#moquette-neon)\" />\n      <rect x=\"338\" y=\"56\" width=\"344\" height=\"142\" fill=\"url(#grille-neon)\" />\n      <rect x=\"690\" y=\"56\" width=\"290\" height=\"142\" fill=\"url(#planches-neon)\" />\n      <rect x=\"20\" y=\"198\" width=\"960\" height={bas - 198} fill=\"url(#dalle-neon)\" />\n\n      {/* Les murs du fond, chacun son tube. */}\n      <MurNeon x={20} largeur={310} tube={VIOLET} />\n      <MurNeon x={338} largeur={344} tube={ROSE} />\n      <MurNeon x={690} largeur={290} tube={CYAN} />\n\n      {/* Chez le fixer : la baie de serveurs, son enseigne au néon rose. */}\n      <Cliquable {...lien('server-rack')}>\n        <BaieServeurs x={34} />\n      </Cliquable>\n      <Neon x={222} y={42} texte={t.leadOffice} couleur={ROSE} taille={10} />\n\n      {/* L'espace détente : les bornes, le panneau holographique, le banc, l'établi du drone. */}\n      <Cliquable titre={t.objects.snake} faire={() => jouer('snake')}>\n        <Arcade x={352} />\n      </Cliquable>\n      <Cliquable titre={t.objects.breakout} faire={() => jouer('casse-briques')}>\n        <BorneCasseBriques x={396} />\n      </Cliquable>\n      <Cliquable {...lien('tv')}>\n        <Hologramme x={452} />\n      </Cliquable>\n      <Banc x={458} y={130} />\n      <Cliquable {...lien('workbench')}>\n        <EtabliDrone x={598} />\n      </Cliquable>\n\n      {/* Le stand de ramen : le comptoir, les rideaux rouges, le bol qui fume, le distributeur, le frigo. */}\n      <StandRamen />\n      <Cliquable {...lien('vending-machine')}>\n        <DistributeurCanettes x={864} />\n      </Cliquable>\n      <Cliquable {...lien('fridge')}>\n        <FrigoAutocollants x={922} />\n      </Cliquable>\n      {[786, 848].map((x) => (\n        <g key={x}>\n          <rect x={x} y=\"140\" width=\"12\" height=\"4\" fill=\"#3f2a1e\" />\n          <rect x={x + 5} y=\"144\" width=\"2\" height=\"18\" fill=\"#52525b\" />\n          <rect x={x + 1} y=\"160\" width=\"10\" height=\"2\" fill=\"#52525b\" />\n        </g>\n      ))}\n\n      {/* La grande salle : les caisses de matériel. */}\n      <Caisses x={56} y={bas - 44} lien={lien} />\n\n      {/* Le mur de la grande salle, son enseigne au néon cyan. */}\n      {MUR_SALLE.map(([de, a]) => (\n        <MurNeon key={de} x={de} largeur={a - de} y={204} hauteur={38} tube={a - de > 90 ? CYAN : undefined} />\n      ))}\n      <Neon x={695} y={230} texte={titre} couleur={CYAN} taille={titre.length > 14 ? 7 : 9} />\n      <Porte bas={bas} />\n      <rect x=\"330\" y=\"14\" width=\"8\" height=\"190\" fill={VIOLET} opacity=\"0.35\" />\n      <rect x=\"330\" y=\"14\" width=\"2\" height=\"190\" fill={ROSE} opacity=\"0.6\" />\n      <rect x=\"682\" y=\"14\" width=\"8\" height=\"190\" fill={BETON.arete} />\n      <rect x=\"12\" y=\"14\" width=\"8\" height={bas - 6} fill={BETON.arete} />\n      <rect x=\"980\" y=\"14\" width=\"8\" height={bas - 6} fill={BETON.arete} />\n      <rect x=\"12\" y=\"6\" width=\"976\" height=\"8\" fill={BETON.arete} />\n      <rect x=\"12\" y={bas} width=\"976\" height=\"10\" fill={BETON.arete} />\n    </>\n  )\n}\n\n/** La baie de serveurs du fixer : ses tiroirs, ses diodes qui clignotent. */\nfunction BaieServeurs({ x }: { x: number }) {\n  return (\n    <g>\n      <rect x={x} y=\"18\" width=\"52\" height=\"88\" fill=\"#05040a\" />\n      <rect x={x + 3} y=\"21\" width=\"46\" height=\"82\" fill=\"#111020\" />\n      {Array.from({ length: 7 }, (_, i) => (\n        <g key={i}>\n          <rect x={x + 5} y={24 + i * 11} width=\"42\" height=\"8\" fill=\"#1c1a30\" />\n          {[0, 1, 2].map((d) => (\n            <rect key={d} x={x + 9 + d * 5} y={27 + i * 11} width=\"2\" height=\"2\" fill={[CYAN, ROSE, '#4ade80'][(i + d) % 3]}>\n              <Anim attributeName=\"opacity\" values=\"1;0.2;1\" dur={`${0.7 + ((i + d) % 4) * 0.3}s`} repeatCount=\"indefinite\" />\n            </rect>\n          ))}\n          <rect x={x + 30} y={27 + i * 11} width=\"14\" height=\"2\" fill=\"#2e2a48\" />\n        </g>\n      ))}\n    </g>\n  )\n}\n\n/** Le panneau holographique : un visage en lignes cyan, des katakana qui tombent. */\nfunction Hologramme({ x }: { x: number }) {\n  return (\n    <g>\n      <rect x={x} y=\"22\" width=\"116\" height=\"42\" fill=\"#05040a\" />\n      <rect x={x + 3} y=\"25\" width=\"110\" height=\"36\" fill=\"#0b1324\" />\n      <Pluie x={x + 4} y={25} largeur={52} hauteur={36} couleur={CYAN} />\n      <g opacity=\"0.9\">\n        <rect x={x + 70} y=\"30\" width=\"22\" height=\"26\" fill=\"none\" stroke={ROSE} strokeWidth=\"2\" />\n        <rect x={x + 75} y=\"38\" width=\"4\" height=\"3\" fill={ROSE} />\n        <rect x={x + 83} y=\"38\" width=\"4\" height=\"3\" fill={ROSE} />\n        <rect x={x + 77} y=\"47\" width=\"8\" height=\"2\" fill={ROSE}>\n          <Anim attributeName=\"width\" values=\"8;4;8\" dur=\"1.2s\" repeatCount=\"indefinite\" />\n        </rect>\n        <Anim attributeName=\"opacity\" values=\"0.9;0.5;0.9;0.9\" dur=\"2.6s\" repeatCount=\"indefinite\" />\n      </g>\n      <rect x={x + 3} y=\"25\" width=\"110\" height=\"2\" fill={CYAN} opacity=\"0.4\">\n        <Anim attributeName=\"y\" values={etapes(25, 59, 8)} dur=\"2s\" repeatCount=\"indefinite\" />\n      </rect>\n      <rect x={x + 54} y=\"64\" width=\"8\" height=\"6\" fill=\"#2a2238\" />\n    </g>\n  )\n}\n\n/** Le banc, vu de dos depuis la salle, éclairé par un tube rose en dessous. */\nfunction Banc({ x, y }: { x: number; y: number }) {\n  return (\n    <g>\n      <rect x={x} y={y + 10} width=\"104\" height=\"22\" fill=\"#241b33\" />\n      <rect x={x + 4} y={y} width=\"96\" height=\"16\" fill=\"#2f2442\" />\n      <rect x={x + 4} y={y} width=\"96\" height=\"2\" fill={VIOLET} opacity=\"0.7\" />\n      <rect x={x + 2} y={y + 33} width=\"100\" height=\"2\" fill={ROSE} filter=\"url(#flou-neon)\" />\n      <rect x={x + 2} y={y + 33} width=\"100\" height=\"1\" fill=\"#fdf2f8\" opacity=\"0.8\" />\n    </g>\n  )\n}\n\n/** L'établi du drone : le drone ouvert, ses hélices, le fer à souder qui fume. */\nfunction EtabliDrone({ x }: { x: number }) {\n  return (\n    <g>\n      <rect x={x} y=\"44\" width=\"76\" height=\"40\" fill=\"#1f1a2b\" />\n      <rect x={x} y=\"44\" width=\"76\" height=\"3\" fill={CYAN} opacity=\"0.5\" />\n      <rect x={x + 4} y=\"84\" width=\"4\" height=\"12\" fill=\"#3a3150\" />\n      <rect x={x + 68} y=\"84\" width=\"4\" height=\"12\" fill=\"#3a3150\" />\n      {/* Le drone : son corps, ses quatre bras, ses hélices qui tournent à vide. */}\n      <rect x={x + 26} y=\"56\" width=\"20\" height=\"12\" fill=\"#3f3f46\" />\n      <rect x={x + 32} y=\"59\" width=\"8\" height=\"5\" fill={ROSE} />\n      {[\n        [x + 16, 50],\n        [x + 50, 50],\n        [x + 16, 72],\n        [x + 50, 72],\n      ].map(([hx, hy], i) => (\n        <g key={i}>\n          <rect x={hx + 2} y={hy + 1} width=\"6\" height=\"2\" fill=\"#71717a\" />\n          <rect x={hx} y={hy} width=\"10\" height=\"2\" fill=\"#a1a1aa\">\n            <Anim attributeName=\"width\" values=\"10;3;10\" dur=\"0.3s\" begin={`${i * 0.07}s`} repeatCount=\"indefinite\" />\n          </rect>\n        </g>\n      ))}\n      <rect x={x + 60} y=\"58\" width=\"2\" height=\"12\" fill=\"#d4d4d8\" />\n      <rect x={x + 60} y=\"56\" width=\"2\" height=\"2\" fill=\"#f97316\" />\n      <rect x={x + 59} y=\"50\" width=\"3\" height=\"3\" fill=\"#a1a1aa\" opacity=\"0\">\n        <Anim attributeName=\"y\" values={etapes(54, 40, 5)} dur=\"1.6s\" repeatCount=\"indefinite\" />\n        <Anim attributeName=\"opacity\" values={etapes(0.7, 0, 5)} dur=\"1.6s\" repeatCount=\"indefinite\" />\n      </rect>\n    </g>\n  )\n}\n\n/** Le stand de ramen : le comptoir, les rideaux rouges (noren), la lanterne, le bol qui fume. */\nfunction StandRamen() {\n  return (\n    <g>\n      <rect x=\"730\" y=\"24\" width=\"122\" height=\"10\" fill=\"#7f1d1d\" />\n      {[0, 1, 2, 3].map((i) => (\n        <g key={i}>\n          <rect x={732 + i * 30} y=\"34\" width=\"28\" height=\"16\" fill=\"#b91c1c\" />\n          <rect x={732 + i * 30} y=\"34\" width=\"28\" height=\"2\" fill=\"#991b1b\" />\n        </g>\n      ))}\n      <text x=\"791\" y=\"46\" fontSize=\"9\" textAnchor=\"middle\" fill=\"#fef2f2\" letterSpacing=\"6\">\n        ラーメン\n      </text>\n      <rect x=\"730\" y=\"50\" width=\"122\" height=\"20\" fill=\"#3f2a1e\" />\n      <rect x=\"730\" y=\"50\" width=\"122\" height=\"3\" fill=\"#57392a\" />\n      <rect x=\"730\" y=\"70\" width=\"122\" height=\"16\" fill=\"#2a1a14\" />\n      {/* La lanterne rouge. */}\n      <rect x=\"716\" y=\"30\" width=\"10\" height=\"16\" fill=\"#dc2626\" />\n      <rect x=\"716\" y=\"34\" width=\"10\" height=\"1\" fill=\"#7f1d1d\" />\n      <rect x=\"716\" y=\"40\" width=\"10\" height=\"1\" fill=\"#7f1d1d\" />\n      {/* Le bol, ses baguettes. */}\n      <rect x=\"760\" y=\"52\" width=\"16\" height=\"6\" fill=\"#f5f5f4\" />\n      <rect x=\"762\" y=\"51\" width=\"12\" height=\"2\" fill=\"#fbbf24\" />\n      <rect x=\"778\" y=\"46\" width=\"1\" height=\"10\" fill=\"#d6a76c\" />\n      <rect x=\"780\" y=\"46\" width=\"1\" height=\"10\" fill=\"#d6a76c\" />\n      <rect x=\"812\" y=\"52\" width=\"16\" height=\"6\" fill=\"#f5f5f4\" />\n      <rect x=\"814\" y=\"51\" width=\"12\" height=\"2\" fill=\"#fbbf24\" />\n    </g>\n  )\n}\n\n/** La vapeur du bol, quand quelqu'un attend au comptoir. */\nfunction VapeurRamen() {\n  return (\n    <g pointerEvents=\"none\">\n      {[0, 0.5, 1].map((debut, i) => (\n        <rect key={debut} x={764 + i * 4} y=\"48\" width=\"2\" height=\"4\" fill=\"#e5e7eb\" opacity=\"0\">\n          <Anim attributeName=\"y\" values={etapes(48, 36, 4)} dur=\"1.5s\" begin={`${debut}s`} repeatCount=\"indefinite\" />\n          <Anim attributeName=\"opacity\" values={etapes(0.8, 0, 4)} dur=\"1.5s\" begin={`${debut}s`} repeatCount=\"indefinite\" />\n        </rect>\n      ))}\n    </g>\n  )\n}\n\n/** Le distributeur de canettes : sa façade qui clignote, ses rangées de canettes. */\nfunction DistributeurCanettes({ x }: { x: number }) {\n  const canettes = [ROSE, CYAN, '#facc15', VIOLET, '#4ade80']\n  return (\n    <g>\n      <rect x={x} y=\"22\" width=\"40\" height=\"72\" fill=\"#1e1b2e\" />\n      <rect x={x + 3} y=\"25\" width=\"34\" height=\"10\" fill={CYAN}>\n        <Anim attributeName=\"opacity\" values=\"1;0.55;1;1\" dur=\"1.8s\" repeatCount=\"indefinite\" />\n      </rect>\n      <rect x={x + 3} y=\"38\" width=\"24\" height=\"44\" fill=\"#0b0714\" />\n      {[0, 1, 2, 3].map((rang) =>\n        [0, 1, 2].map((col) => (\n          <rect key={`${rang}-${col}`} x={x + 6 + col * 7} y={41 + rang * 10} width=\"5\" height=\"8\" fill={canettes[(rang + col) % canettes.length]} />\n        )),\n      )}\n      <rect x={x + 29} y=\"40\" width=\"6\" height=\"16\" fill=\"#27272a\" />\n      <rect x={x + 30} y=\"42\" width=\"4\" height=\"2\" fill=\"#4ade80\" />\n      <rect x={x + 6} y=\"84\" width=\"22\" height=\"6\" fill=\"#0b0714\" />\n    </g>\n  )\n}\n\n/** Le frigo couvert d'autocollants. */\nfunction FrigoAutocollants({ x }: { x: number }) {\n  return (\n    <g>\n      <rect x={x} y=\"16\" width=\"54\" height=\"82\" fill=\"#27272a\" />\n      <rect x={x + 2} y=\"18\" width=\"50\" height=\"30\" fill=\"#3f3f46\" />\n      <rect x={x + 2} y=\"50\" width=\"50\" height=\"46\" fill=\"#3f3f46\" />\n      <rect x={x + 44} y=\"24\" width=\"2\" height=\"18\" fill=\"#71717a\" />\n      <rect x={x + 44} y=\"56\" width=\"2\" height=\"24\" fill=\"#71717a\" />\n      <rect x={x + 8} y=\"24\" width=\"10\" height=\"8\" fill={ROSE} transform={`rotate(-8 ${x + 13} 28)`} />\n      <rect x={x + 22} y=\"60\" width=\"12\" height=\"10\" fill={CYAN} />\n      <rect x={x + 8} y=\"74\" width=\"8\" height=\"8\" fill=\"#facc15\" />\n      <text x={x + 28} y=\"38\" fontSize=\"7\" textAnchor=\"middle\" fill=\"#fdf4ff\">\n        ⚡\n      </text>\n    </g>\n  )\n}\n\n/** Des caisses de matériel, marquées au pochoir. */\nfunction Caisses({\n  x,\n  y,\n  lien,\n}: {\n  x: number\n  y: number\n  lien: (objet: SceneObject) => { titre: string; faire: () => void; actif: boolean }\n}) {\n  return (\n    <g>\n      <Cliquable {...lien('cpu-box')}>\n        <rect x={x} y={y + 14} width=\"40\" height=\"26\" fill=\"#3f3f46\" />\n        <rect x={x} y={y + 14} width=\"40\" height=\"3\" fill=\"#52525b\" />\n        <rect x={x + 6} y={y + 24} width=\"28\" height=\"6\" fill={ROSE} opacity=\"0.8\" />\n      </Cliquable>\n      <Cliquable {...lien('gpu-box')}>\n        <rect x={x + 6} y={y} width=\"30\" height=\"16\" fill=\"#27272a\" />\n        <rect x={x + 10} y={y + 6} width=\"22\" height=\"3\" fill={CYAN} opacity=\"0.8\" />\n      </Cliquable>\n      <Cliquable {...lien('ram-box')}>\n        <rect x={x + 44} y={y + 22} width=\"22\" height=\"18\" fill=\"#3f3f46\" />\n        <rect x={x + 48} y={y + 28} width=\"14\" height=\"4\" fill=\"#facc15\" opacity=\"0.8\" />\n      </Cliquable>\n    </g>\n  )\n}\n\n/* ——— Les postes ——— */\n\n/** L'écran holographique : ce qu'il affiche dit l'état de son agent. */\nfunction EcranHolo({ cx, dy, statut, bientot }: { cx: number; dy: number; statut: Statut; bientot: number | null }) {\n  const t = useTextes()\n  const l = 52\n  const g = cx - l / 2\n  return (\n    <g>\n      {/* Le projecteur posé sur le plateau, puis le cadre de lumière de l'écran. */}\n      <rect x={cx - 10} y={dy + 12} width=\"20\" height=\"5\" fill=\"#27272a\" />\n      <rect x={cx - 6} y={dy + 10} width=\"12\" height=\"2\" fill={CYAN} opacity=\"0.7\" />\n      <rect x={g - 2} y={dy - 16} width={l + 4} height=\"26\" fill=\"#0b0714\" stroke={CYAN} strokeOpacity=\"0.5\" strokeWidth=\"1\" />\n      {statut === 'endormi' ? (\n        // En veille : l'écran noir, sans le post-it de l'absent.\n        <rect x={g} y={dy - 14} width={l} height=\"22\" fill=\"#05040a\" />\n      ) : statut === 'absent' ? (\n        <>\n          <rect x={g} y={dy - 14} width={l} height=\"22\" fill=\"#05040a\" />\n          <g transform={`rotate(-6 ${cx} ${dy - 4})`}>\n            <rect x={cx - 15} y={dy - 12} width=\"30\" height=\"15\" fill=\"#fde68a\" />\n            <rect x={cx - 15} y={dy - 12} width=\"30\" height=\"3\" fill=\"#fcd34d\" />\n            <text x={cx} y={dy} fontSize=\"7\" textAnchor=\"middle\" fill=\"#78350f\">\n              {t.offNote}\n            </text>\n          </g>\n        </>\n      ) : statut === 'en-echec' ? (\n        // Le glitch : des bandes roses et cyan décalées, qui sautent.\n        <>\n          <rect x={g} y={dy - 14} width={l} height=\"22\" fill=\"#2a0a1a\" />\n          {[0, 1, 2, 3, 4].map((i) => (\n            <rect key={i} x={g + (i % 2 ? 6 : 0)} y={dy - 13 + i * 4} width={l - 10} height=\"2\" fill={i % 2 ? CYAN : ROSE}>\n              <Anim attributeName=\"x\" values={`${g};${g + 8};${g + 2};${g + 6}`} dur=\"0.4s\" begin={`${i * 0.08}s`} repeatCount=\"indefinite\" />\n            </rect>\n          ))}\n          <text x={cx} y={dy + 2} fontSize=\"8\" textAnchor=\"middle\" fill=\"#fdf2f8\">\n            ERR\n          </text>\n        </>\n      ) : statut === 'au-travail' ? (\n        <>\n          <rect x={g} y={dy - 14} width={l} height=\"22\" fill=\"#020a06\" />\n          {[0, 1, 2, 3].map((i) => {\n            const w = [30, 18, 36, 14][i]\n            return (\n              <rect key={i} x={g + 3 + (i % 2) * 5} y={dy - 12 + i * 5} width={w} height=\"2\" fill={i % 2 ? CYAN : '#4ade80'}>\n                <Anim attributeName=\"width\" values={`${etapes(2, w, 5)};${w}`} dur=\"1.6s\" begin={`${i * 0.4}s`} repeatCount=\"indefinite\" />\n              </rect>\n            )\n          })}\n        </>\n      ) : (\n        // Au repos : le bureau néon, une fenêtre rose, une fenêtre cyan.\n        <>\n          <rect x={g} y={dy - 14} width={l} height=\"22\" fill=\"#1a1033\" />\n          <rect x={g + 4} y={dy - 11} width=\"20\" height=\"12\" fill=\"none\" stroke={ROSE} strokeWidth=\"1\" />\n          <rect x={g + 28} y={dy - 9} width=\"18\" height=\"9\" fill=\"none\" stroke={CYAN} strokeWidth=\"1\" />\n        </>\n      )}\n      {bientot ? (\n        <>\n          <rect x={g} y={dy} width={l} height=\"7\" fill=\"#05040a\" />\n          <text x={cx} y={dy + 6} fontSize=\"6\" textAnchor=\"middle\" fill={ROSE}>\n            {t.inMinutes(bientot)}\n          </text>\n        </>\n      ) : null}\n      {statut === 'en-echec'\n        ? [0, 0.6, 1.2].map((debut, i) => (\n            <rect key={debut} x={cx + 18 + i * 3} y={dy + 6} width=\"6\" height=\"6\" fill=\"#52525b\" opacity=\"0\">\n              <Anim attributeName=\"y\" values={etapes(dy + 6, dy - 26, 6)} dur=\"1.8s\" begin={`${debut}s`} repeatCount=\"indefinite\" />\n              <Anim attributeName=\"opacity\" values={etapes(0.8, 0, 6)} dur=\"1.8s\" begin={`${debut}s`} repeatCount=\"indefinite\" />\n            </rect>\n          ))\n        : null}\n    </g>\n  )\n}\n\n/** Le plateau en métal noir, son liseré néon de la couleur de l'agent. */\nfunction PlateauNoir({ x, dy, dessus, accent }: { x: number; dy: number; dessus: number; accent: string }) {\n  return (\n    <>\n      <rect x={x} y={dy} width={BUREAU.largeur} height={dessus} fill=\"#1c1828\" />\n      <rect x={x} y={dy} width={BUREAU.largeur} height=\"2\" fill=\"#2e2742\" />\n      <rect x={x} y={dy + dessus} width={BUREAU.largeur} height=\"12\" fill=\"#0f0c18\" />\n      <rect x={x} y={dy + dessus + 10} width={BUREAU.largeur} height=\"2\" fill={accent} opacity=\"0.85\" />\n      <rect x={x + 6} y={dy + dessus + 12} width=\"5\" height=\"7\" fill=\"#27272a\" />\n      <rect x={x + BUREAU.largeur - 11} y={dy + dessus + 12} width=\"5\" height=\"7\" fill=\"#27272a\" />\n    </>\n  )\n}\n\n/** Le poste : l'écran holographique, un clavier qui luit, une canette, l'emoji de l'agent en autocollant. */\nfunction BureauNeon({ cx, dy, x, dessus, statut, accent, bientot, emoji }: PropsBureau) {\n  const allume = statut !== 'absent' && statut !== 'endormi'\n  return (\n    <g>\n      <PlateauNoir x={x} dy={dy} dessus={dessus} accent={accent} />\n      <EcranHolo cx={cx - 4} dy={dy} statut={statut} bientot={bientot} />\n      <rect x={cx - 24} y={dy + 22} width=\"40\" height=\"7\" fill=\"#0b0714\" />\n      <rect x={cx - 23} y={dy + 23} width=\"38\" height=\"5\" fill={allume ? VIOLET : '#27272a'} opacity=\"0.75\" />\n      <rect x={cx + 20} y={dy + 22} width=\"5\" height=\"7\" fill=\"#3f3f46\" />\n      <rect x={x + 96} y={dy + 18} width=\"7\" height=\"12\" fill={ROSE} />\n      <rect x={x + 96} y={dy + 18} width=\"7\" height=\"2\" fill=\"#d4d4d8\" />\n      <text x={x + 12} y={dy + 13} fontSize=\"10\" textAnchor=\"middle\">\n        {emoji}\n      </text>\n    </g>\n  )\n}\n\n/** Le bureau du fixer : deux écrans vus de dos, une mallette, une canette. */\nfunction BureauChefNeon({ dy, x, dessus }: PropsBureau) {\n  return (\n    <g>\n      <PlateauNoir x={x} dy={dy} dessus={dessus} accent={ROSE} />\n      {[6, 42].map((dx) => (\n        <g key={dx}>\n          <rect x={x + dx} y={dy - 12} width=\"32\" height=\"22\" fill=\"#0b0714\" stroke={CYAN} strokeOpacity=\"0.4\" />\n          <rect x={x + dx + 14} y={dy + 10} width=\"4\" height=\"4\" fill=\"#27272a\" />\n        </g>\n      ))}\n      <rect x={x + 84} y={dy + 10} width=\"26\" height=\"16\" fill=\"#3f2a1e\" />\n      <rect x={x + 94} y={dy + 7} width=\"6\" height=\"3\" fill=\"#57392a\" />\n      <rect x={x + 112} y={dy + 18} width=\"5\" height=\"10\" fill={CYAN} />\n    </g>\n  )\n}\n\n/** Le fauteuil du fixer, cuir noir et liseré rose. */\nfunction FauteuilNeon({ x, y }: { x: number; y: number }) {\n  return (\n    <g>\n      <rect x={x - 16} y={y - 30} width=\"32\" height=\"38\" fill=\"#18181b\" />\n      <rect x={x - 13} y={y - 27} width=\"26\" height=\"32\" fill=\"#27272a\" />\n      <rect x={x - 13} y={y - 27} width=\"26\" height=\"2\" fill={ROSE} />\n      <rect x={x - 18} y={y - 6} width=\"5\" height=\"14\" fill=\"#18181b\" />\n      <rect x={x + 13} y={y - 6} width=\"5\" height=\"14\" fill=\"#18181b\" />\n    </g>\n  )\n}\n\n/** Le siège baquet noir, un liseré de la couleur de l'agent. */\nfunction AssiseBaquet({ x, y, couleur }: PropsSiege) {\n  return (\n    <g>\n      <rect x={x - 11} y={y - 12} width=\"22\" height=\"12\" fill=\"#18181b\" />\n      <rect x={x - 11} y={y - 12} width=\"22\" height=\"2\" fill={couleur} />\n    </g>\n  )\n}\n\nfunction DossierBaquet({ x, y, couleur }: PropsSiege) {\n  return (\n    <g>\n      <rect x={x - 12} y={y + 2} width=\"24\" height=\"10\" fill=\"#18181b\" />\n      <rect x={x - 10} y={y + 4} width=\"20\" height=\"6\" fill=\"#27272a\" />\n      <rect x={x - 2} y={y + 4} width=\"4\" height=\"6\" fill={couleur} />\n      <rect x={x - 2} y={y + 12} width=\"4\" height=\"3\" fill=\"#3f3f46\" />\n      <rect x={x - 10} y={y + 15} width=\"20\" height=\"3\" fill=\"#27272a\" />\n    </g>\n  )\n}\n\n/* ——— Les tenues ——— */\n\nconst SWEATS = ['#18181b', '#1e1b4b', '#3b0764', '#0f172a', '#27272a', '#111827', '#4c0519', '#164e63']\nconst CORDONS = [ROSE, CYAN, VIOLET, '#facc15', '#4ade80']\nconst CHEVEUX = ['#111111', '#2a1d14', ROSE, CYAN, VIOLET, '#e5e5e5', '#5a3a22', '#111111']\nconst PEAUX = ['#f1c9a5', '#d9a57a', '#a86f48', '#7a4a2c', '#f5d6bf']\nconst BAS = ['#18181b', '#27272a', '#1e293b', '#0f172a']\n\n/** Le nombre de cet agent dans ce thème : chaque thème a sa graine, pour des tenues qui changent d'un thème à l'autre. */\nconst hachage = (id: string) => hacher(id, 29, 43)\n\n/** Sweats sombres aux cordons fluo, cheveux parfois teints ; le fixer en manteau long. */\nfunction tenueNeon(id: string): Record<string, string> {\n  if (id === CHEF_ID) {\n    return { h: '#111111', s: '#d9a57a', e: '#f472b6', c: '#09090b', C: '#18181b', t: ROSE, k: CYAN, p: '#18181b', b: '#09090b' }\n  }\n  if (id.startsWith('livreur:')) {\n    return { h: '#111111', s: '#d9a57a', e: '#1c1917', c: '#facc15', C: '#ca8a04', t: '#18181b', k: '#18181b', p: '#18181b', b: '#09090b' }\n  }\n  const h = hachage(id)\n  const sweat = SWEATS[h % SWEATS.length]\n  const cordon = CORDONS[(h >>> 3) % CORDONS.length]\n  return {\n    h: CHEVEUX[(h >>> 6) % CHEVEUX.length],\n    s: PEAUX[(h >>> 9) % PEAUX.length],\n    e: '#0b0714',\n    c: sweat,\n    C: cordon,\n    t: cordon,\n    k: cordon,\n    p: BAS[(h >>> 12) % BAS.length],\n    b: '#09090b',\n  }\n}\n\n/** Un sur deux a son casque sur les oreilles, cerclé de néon. */\nconst casqueNeon = (id: string) => id !== CHEF_ID && !estInvite(id) && hachage(id) % 2 === 0\n\n/* La nuit : les tubes des murs, les enseignes, le panneau holographique, le distributeur, la lanterne. */\nconst LUMIERES_NEON: Array<Lumiere> = [\n  { x: 175, y: 40, r: 80, couleur: VIOLET },\n  { x: 222, y: 42, r: 60, couleur: ROSE },\n  { x: 510, y: 40, r: 90, couleur: ROSE },\n  { x: 510, y: 44, r: 60, couleur: CYAN },\n  { x: 835, y: 40, r: 80, couleur: CYAN },\n  { x: 884, y: 30, r: 45, couleur: CYAN },\n  { x: 721, y: 38, r: 30, couleur: '#ef4444' },\n  { x: 373, y: 50, r: 45, couleur: VIOLET },\n  { x: 417, y: 50, r: 40, couleur: CYAN },\n  { x: 510, y: 165, r: 60, couleur: ROSE },\n  { x: 695, y: 228, r: 70, couleur: CYAN },\n]\n\nexport const THEME_NEON: Theme = {\n  coins: COINS_NEON,\n  coinCafe: 'cafe',\n  fond: NUIT,\n  Decor,\n  Bureau: BureauNeon,\n  BureauChef: BureauChefNeon,\n  plaque: CYAN,\n  Assise: AssiseBaquet,\n  Dossier: DossierBaquet,\n  Fauteuil: FauteuilNeon,\n  tenue: tenueNeon,\n  casque: casqueNeon,\n  CafeQuiCoule: VapeurRamen,\n  lumieres: LUMIERES_NEON,\n  // Il fait déjà nuit dans la salle : un voile violet plus léger, que les néons percent.\n  nuit: { couleur: '#0b0620', opacite: 0.45 },\n  // Le halo de l'écran holographique au-dessus de chaque poste occupé.\n  lumierePoste: (cx, dy, accent) => [{ x: cx, y: dy, r: 45, couleur: accent }],\n}\n",
      "type": "registry:component",
      "target": "components/pixel-openspace/themes/neon.tsx"
    }
  ],
  "type": "registry:block"
}