-- ===========================================================================
--  SHOOTKAM OS — v1.6
--
--  App CSP autonome : installee par le joueur dans apps/lua/, elle ne depend
--  d'aucun serveur ni d'aucun hebergeur.
--
--  v0.1  la fenetre, les sept onglets, les panneaux de verre, la navigation
--  v0.2  la prise de camera — le morceau le plus risque du produit
--  v0.3  le diagnostic : ce que la voiture contient vraiment
--  v0.4  la peinture — la voiture change de couleur pour de vrai
--  v0.5  on voit ce qu'on modifie : nom lisible, apercu 3D, surlignage
--  v0.6  qualite de peinture, et etalonnage teinte / saturation / luminosite
--  v0.7  les prereglages : ce qu'on a peint se garde et se partage
--  v0.8  le neon — le seul reglage qui continue de tourner menu ferme
--  v0.9  tubes visibles, et une camera qu'on pilote
--  v1.0  bordeaux SHOOTKAM, et des panneaux separes
--  v1.1  TROIS fenetres, pas six — et un neon qui est enfin de la lumiere
--  v1.3  les trois ateliers decoupes en gauche et droite, sans exception
--  v1.4  une bande LED, pas quatre lampes ; et des fenetres qui ne bougent
--        qu-au ruban, donc des curseurs qui glissent enfin
--  v1.5  le dernier onglet vide se remplit : raccourci clavier, fichiers, et
--        la regle des verrous ecrite une fois au lieu de sept
--  v1.6  UNE seule fenetre, en colonne — et pas un ecran a reecrire
--
--  ---------------------------------------------------------------------
--  UNE FENETRE, ET PAS UNE DE PLUS
--
--  La v1.0 en avait six : l'enseigne, trois panneaux, le dock, la barre de
--  vue. La v1.1 est passee a trois. La v1.6 n'en a plus qu'une.
--
--  A chaque fois la meme raison, et l'equipe l'a dite le plus clairement :
--  trois fenetres, c'est trop en jeu. Le decoupage laissait le milieu de
--  l'ecran libre pour la voiture — il y arrivait — mais au prix de trois
--  choses a placer, a deplacer et a fermer avant de regler quoi que ce soit.
--
--  LA COLONNE TIENT LA MEME PROMESSE AUTREMENT : elle est etroite. Posee sur
--  un bord elle laisse le milieu libre, donc la voiture visible, et il n'y a
--  plus qu'un objet a manipuler. Les deux moities d'un atelier deviennent
--  deux onglets NOMMES — « Pieces / Peinture » dit ce qu'on va trouver la ou
--  « gauche / droite » ne disait rien.
--
--  ⚠️ ET PAS UN ECRAN N'A ETE REECRIT. Chacun dessine uniquement par rapport
--  a SA boite ; il a suffi de leur en donner une autre. C'est ce que
--  T.origine() permet depuis la v0.1 : toute la mise en page reste ecrite
--  dans le repere 1920 x 1080, la fenetre annonce le coin qu'elle porte et
--  V() le soustrait. Pas une ligne de dessin n'a change en passant de une
--  fenetre a six, de six a trois, puis de trois a une.
-- ===========================================================================

local T  = require('theme')
local C  = require('composants')
local K  = require('camera')
local EP = require('ecran_peinture')
local EQ = require('ecran_prereglages')
local NE = require('neon')
local EN = require('ecran_neon')
local EJ = require('ecran_jantes')
local FA = require('favoris')
local ECA = require('ecran_carrosserie')
local EX = require('ecran_extras')
local BCA = require('carrosserie')
-- ⚠️ heros, specs et icones ne sont plus requis ICI : la voiture 3D, la fiche
-- technique et les pictogrammes du dock vivaient dans l'accueil large, qui
-- n'existe plus.
--
-- icones sert encore — ecran_ciel dessine ses temps avec. heros et specs, eux,
-- ne sont plus requis par PERSONNE : ce sont deux fichiers orphelins. On les
-- garde plutot que de les supprimer, parce qu'ils marchent et qu'un apercu 3D
-- de la voiture resservira le jour ou un ecran voudra en montrer un — mais
-- qu'on ne s'y trompe pas en lisant le dossier : rien ne les charge.
local EM = require('ecran_carte')
local AV = require('voiture')
local EA = require('ecran_audio')
local IM = require('image')
local EI = require('ecran_image')
local EC = require('ecran_ciel')
local RG = require('reglages')
local ER = require('ecran_reglages')
local BC = require('barre_camera')
local W  = require('fenetre')

local ATELIERS = { 'Peinture', 'Préréglages', 'Jantes', 'Carrosserie', 'Néon' }
-- ⚠️ JANTES REPASSE A NON PRET, ET C'EST DELIBERE. Le remplacement de
-- modele ne monte la jante que sur UNE roue sur quatre : les quatre noeuds sont
-- trouves, les quatre enveloppes creees, mais trois restent vides. Tant que la
-- cause n'est pas connue, l'atelier promet quelque chose qu'il ne tient pas.
--
-- On le MASQUE plutot que de le supprimer : le code reste en place, la sonde
-- continue d'ecrire dans le journal, et le jour ou la methode est trouvee il
-- suffit de remettre true. Un atelier retire serait un atelier a reconstruire.
--
-- La geometrie (echelle, deport, carrossage) marchait, elle. Elle part avec le
-- reste : couper la moitie d'un atelier serait plus deroutant que le fermer.
-- Jantes revient : le montage ne prend qu'une roue sur quatre selon les
-- voitures, mais la sonde du journal ne peut rien apprendre si l'atelier est
-- ferme. On le rouvre pour essayer sur plusieurs modeles.
-- Carrosserie passe a PRET : pieces masquables, vitres teintees, salete et
-- assiette. L assiette est annoncee experimentale dans l ecran lui-meme.
local PRETS    = { true, true, true, true, true }

local etat = { onglet = 1, atelier = 1, horloge = 0, moitie = 1,
               choixFavoris = false, defileFav = 0 }

-- Ce que la derniere visee a donne, et quand. Le message s'efface tout
-- seul : un retour qui reste a l'ecran devient du decor au bout de dix
-- secondes, et on ne le lit plus.
local viseMessage, viseHorloge = nil, -99

-- ⚠️ LE CLIC REPETE DESCEND D'UNE COUCHE, COMME DANS LE SHOWROOM. On retient
-- donc la pile traversee par le dernier rayon, ou l'on en est, et OU l'on a
-- clique : si le pointeur a bouge, ce n'est plus le meme endroit et la pile
-- doit etre refaite.
local vise = { pile = {}, index = 0, x = -999, y = -999, cam = nil }

-- ---------------------------------------------------------------------------
--  UNE SEULE FENETRE, EN COLONNE.
--
--  ⚠️ TROIS FENETRES, C'ETAIT TROP EN JEU. Retour de l'equipe, et il est juste :
--  un accueil plus deux panneaux, ca fait trois choses a placer, trois a
--  deplacer et trois a fermer, chaque fois qu'on veut regler quoi que ce soit.
--  Le decoupage servait a laisser le milieu de l'ecran libre pour la voiture ;
--  il y arrivait, mais au prix d'un menu eparpille sur toute la largeur.
--
--  La colonne tient la meme promesse autrement : ELLE EST ETROITE. Posee sur un
--  bord, elle laisse le milieu libre — donc la voiture visible — et il n'y a
--  plus qu'un objet a manipuler. Les deux moities d'un atelier deviennent deux
--  onglets NOMMES : « Pieces / Peinture » dit ce qu'on va trouver la ou
--  « gauche / droite » ne disait rien.
--
--  ⚠️ ET LA MISE EN PAGE DES ECRANS N'A PAS BOUGE D'UNE LIGNE. Chacun dessine
--  uniquement par rapport a SA boite (b.x, b.y, b.w, b.h) : il a suffi de leur
--  donner une autre boite. C'est ce que T.origine permet depuis la v0.1, et
--  c'est ce qui avait deja fait passer l'app de six fenetres a trois.
--
--  ⚠️ LA HAUTEUR N'EST PAS ARBITRAIRE. Cinq ecrans posent des elements a plus
--  de 760 px du haut de leur panneau — carrosserie et jantes vont a 796,
--  peinture a 812. Une colonne plus courte les couperait par le bas, en
--  silence. On reserve donc 900, la hauteur du plus grand panneau declare.
-- ---------------------------------------------------------------------------
local FEN = { 'skos_g', 'skos_d' }        -- gardees pour pouvoir les FERMER
local COLONNE = { x = 56, y = 40, w = 668, h = 1242 }
local MARGE_C = 24                        -- le retrait du contenu dans la colonne
local BARRE_VUE = 40

-- ---------------------------------------------------------------------------
--  Fiches des onglets encore en construction. Chacune annonce ce qu'elle
--  contiendra ET ce que la verification du SDK a deja tranche a son sujet :
--  un ecran vide qui ment est pire qu'un ecran vide qui dit ce qu'il attend.
-- ---------------------------------------------------------------------------
local ECRANS = {
    { titre = 'GARAGE', sous = 'Peinture · Jantes · Carrosserie · Néon · Préréglages',
      lignes = { 'Cinq ateliers, aucun ne dépend de physics.allowed().',
                 'C\'est la première brique livrable : elle fonctionne',
                 'sur les sept serveurs SHOOTKAM.' } },

    { titre = 'CIEL', sous = 'Conditions · Heure',
      lignes = { 'Les conditions se lisent partout : température, vent,',
                 'humidité, et le temps qui arrive.',
                 'L\'heure ne se règle qu\'en solo.' },
      note = 'ac.ConditionsSet, setTrackCondition, WindParams : tout existe (V7)' },

    { titre = 'IMAGE', sous = 'Étalonnage · Caméra',
      lignes = { 'ac.setColorCorrection porte tout l\'étalonnage :',
                 'saturation, luminosité, contraste, teinte, température.',
                 'Plus l\'exposition et la profondeur de champ.' },
      note = 'setColorCorrection, setCameraDOF, setExposureMultiplier (V7)' },

    { titre = 'AUDIO', sous = '13 canaux',
      lignes = { 'Volume général, six canaux de la voiture,',
                 'sept de l\'environnement.',
                 'Appliqué en direct, sans redémarrage.' } },

    { titre = 'CARTE', sous = 'Plan · Destinations · Pilotes',
      lignes = { 'Le plan du circuit, votre voiture, les autres pilotes',
                 'et le trafic — isAIControlled les sépare.',
                 'Les dimensions se lisent sur le PNG, jamais sur map.ini.' },
      -- ⚠️ CETTE NOTE DISAIT LE CONTRAIRE DE CE QUI MARCHE. Elle annoncait la
      -- teleportation « refusee en ligne », ce qui est vrai de la
      -- teleportation LOCALE — celle qui passe par la physique, fermee en
      -- ligne. Mais un serveur peut declarer ses propres destinations, et CSP
      -- fournit ac.canTeleportToServerPoint / ac.teleportToServerPoint pour
      -- s'y rendre. Ce sont deux mecanismes differents ; seul le premier est
      -- interdit. La laisser aurait fait renoncer a ce qui fonctionne.
      note = 'Destinations du serveur : ac.teleportToServerPoint fonctionne en ligne' },

    -- ⚠️ CE CHEMIN ANNONÇAIT UN ECRAN QUI N'EXISTE PLUS. « Voiture ·
    -- Interrupteurs · Fumée » decrivait les commandes de la voiture, retirees
    -- sur demande : phares, clignotants et essuie-glaces sont deja sous les
    -- doigts du joueur, sur son volant. Le sous-titre, lui, etait reste — et
    -- comme il est le seul texte visible sous la rangee d'onglets, il promettait
    -- trois choses introuvables a qui cliquait dessus.
    --
    -- Il dit maintenant les deux panneaux reels, comme le fait CARTE : les
    -- pastilles du chemin et les rubans de l'ecran portent les memes mots.
    { titre = 'EXTRAS', sous = 'Apps · Catalogue',
      lignes = { 'Les fenêtres des autres apps SHOOTKAM, montrées ou',
                 'cachées d\'ici, et le catalogue pour en installer une',
                 'sans quitter la partie.' } },

    -- ⚠️ CET ONGLET A SON ECRAN DEPUIS LA v1.5 : la fiche ne sert plus, seuls
    -- le titre et le sous-titre restent lus (le nom de l'onglet, et le chemin
    -- affiche sous la rangee). Le sous-titre annoncait « Apparence · Langue ·
    -- Raccourcis » : il n'y a ni echelle a regler (les fenetres se
    -- redimensionnent depuis la v1.4) ni langue a choisir (l'app est en
    -- francais). Il dit maintenant ce qu'on y trouve vraiment.
    { titre = 'RÉGLAGES', sous = 'Raccourci · Favoris · Fichiers · Diagnostic',
      lignes = { 'Le raccourci clavier, les favoris, les fichiers de l\'app,',
                 'et ce que la partie autorise.' } },
}

-- ---------------------------------------------------------------------------
--  Le panneau de camera : l'instrument de mesure de la v0.2. Il montre en
--  direct ce que le module decide, et pourquoi.
-- ---------------------------------------------------------------------------
local function panneauCamera(x, y, w)
    T.panneau(x, y, w, 236)
    T.ruban(x, y, 300, 'CAMÉRA', 'ac.grabCamera')

    local v   = ac.getCar(0)
    local kmh = v and math.max(0, math.floor((v.speedKmh or 0) + 0.5)) or 0

    T.txt(tostring(kmh), 44, x + 28, y + 74, T.TX, T.F_NOIR)
    T.txt('KM/H', 12, x + 28 + T.mesure(tostring(kmh), 44, T.F_NOIR) + 10, y + 104,
          T.ACCENT, T.F_DEMI, 1.6)

    local ton, mot
    if K.tenue() then       ton, mot = T.GREEN,  'Caméra tenue'
    elseif K.roule() then   ton, mot = T.ORANGE, 'Vous roulez — vue de conduite conservée'
    elseif K.motif() then   ton, mot = T.RED,    'Prise refusée'
    else                    ton, mot = T.TX3,    'Caméra libre' end
    T.cercleP(x + 34, y + 142, 5, ton)
    T.txt(mot, 15, x + 48, y + 132, T.TX, T.F_DEMI)
    if K.motif() then T.txt(K.motif(), 12, x + 48, y + 154, T.TX2, T.F_REG) end

    local bx, bw = x + 28, w - 56
    T.txt('MÉLANGE', 11, bx, y + 180, T.TX2, T.F_REG, 1.4)
    T.txtD(string.format('%.2f', K.melange()), 13, bx + bw, y + 178, T.TX, T.F_DEMI)
    T.rectP(bx, y + 202, bw, 5, rgbm(1, 1, 1, 0.12), 3)
    T.rectP(bx, y + 202, bw * K.melange(), 5, T.ACCENT, 3)

    -- FILET N° 3 : le bouton de secours. Il rend la camera immediatement,
    -- meme si les deux autres filets ont echoue.
    if C.bouton(bx + bw - 200, y + 130, 200, 44, 'Rendre la caméra', false) then
        K.rendre(true)
    end
end

-- ---------------------------------------------------------------------------
--  Adaptateurs.
--
--  Un ECRAN, ici, c'est deux choses : une liste de BOITES et une fonction
--  panneau(i). Les trois ateliers construits les fournissent eux-memes,
--  decoupes en gauche et droite. Il ne reste d'adaptateur que pour les six
--  onglets encore en construction, qui n'ont qu'une fiche d'attente : ils
--  n'ouvrent que le panneau de gauche, et celui de droite se ferme.
-- ---------------------------------------------------------------------------
local ADAPT_FICHE = {
    BOITES = { { x = 56, y = 150, w = 1200, h = 560 } },
    panneau = function(i)
        if i ~= 1 then return end
        local e = ECRANS[etat.onglet]
        local x, y, w = 56, 150, 1200
        T.panneau(x, y, w, 292)
        T.ruban(x, y, 340, e.titre, e.sous)
        T.txt('En construction', 34, x + 28, y + 78, T.TX, T.F_NOIR)
        for j, l in ipairs(e.lignes) do
            T.txt(l, 15, x + 28, y + 142 + (j - 1) * 26, T.TX2, T.F_REG)
        end
        if e.note then C.verrou(x + 28, y + 232, w - 56, e.note) end
        panneauCamera(x, y + 324, w)
    end,
}

--- Ce qu'il fait dehors, pour les quatre tuiles. Tout est lu sous pcall et
--- retombe sur une valeur neutre : une donnee absente ne doit pas emporter le
--- dessin de l'accueil entier.
local NOMS_TEMPS = {
    [15] = 'Dégagé', [16] = 'Quelques nuages', [17] = 'Nuages épars',
    [18] = 'Couvert', [19] = 'Bouché', [21] = 'Brume', [20] = 'Brouillard',
    [6] = 'Pluie légère', [7] = 'Pluie', [8] = 'Forte pluie',
    [1] = 'Orage', [10] = 'Neige',
}

local function etatCiel()
    local e = { h = 12, m = 0, tAir = 0, vent = 0, temps = '—' }
    pcall(function()
        local s2 = ac.getSim()
        e.h, e.m = s2.timeHours or 12, s2.timeMinutes or 0
        e.tAir = math.floor(s2.ambientTemperature or 0)
        e.vent = math.floor(s2.windSpeedKmh or 0)
        local w2 = s2.weatherConditions
        if w2 ~= nil then
            e.temps = NOMS_TEMPS[w2.currentType] or '—'
        end
    end)
    return e
end

local function ecranCourant()
    -- Les onglets construits ont leur ecran ; les autres montrent leur fiche.
    if etat.onglet == 2 then return EC end
    if etat.onglet == 3 then return EI end
    if etat.onglet == 4 then return EA end
    if etat.onglet == 5 then return EM end
    if etat.onglet == 6 then return EX end
    if etat.onglet == 7 then return ER end
    if etat.onglet ~= 1 then return ADAPT_FICHE end
    if etat.atelier == 5 then return EN end
    if etat.atelier == 4 then return ECA end
    if etat.atelier == 3 then return EJ end
    if etat.atelier == 2 then return EQ end
    return EP
end

-- ---------------------------------------------------------------------------
--  Ouvrir, fermer et redimensionner les deux panneaux.
--
--  ⚠️ UNIQUEMENT AU CHANGEMENT D'ECRAN. Appele a chaque image, setWindowOpen
--  rouvrirait de force un panneau que le joueur vient de fermer a la main :
--  la fenetre se battrait contre lui.
--
--  ⚠️ min ET max identiques : c'est ainsi que le SDK decrit le
--  redimensionnement en direct — « can be used to update window size live ».
-- ---------------------------------------------------------------------------
local dernier = nil

local function synchroniser()
    -- ⚠️ LA TAILLE SE POSE A CHAQUE IMAGE, ELLE NE SE SOUHAITE PAS.
    --
    -- SIZE dans le manifeste n'est qu'une taille de DEPART : AC retient
    -- ensuite celle que la fenetre avait la derniere fois. L'identifiant
    -- shootkam_os traine depuis la v0.1, ou il valait 1280 x 720, puis
    -- 900 x 112 en v1.0 — le jeu a garde 112, et les ateliers comme les sept
    -- onglets tombaient hors du cadre sans rien afficher de leur absence.
    -- Poser la contrainte ne coute que deux vec2 ; la poser une seule fois
    -- ne suffisait pas.
    -- ⚠️ LE MINIMUM VALAIT LE MAXIMUM : LES FENETRES ETAIENT VERROUILLEES.
    -- La contrainte servait a empecher une taille heritee d'une vieille version
    -- de couper les onglets ; en fixant les deux bornes a la meme valeur, elle
    -- interdisait aussi de les redimensionner. Une plage repond aux deux
    -- besoins : plus de taille absurde, et le joueur reste maitre.
    --
    -- Le contenu suit par ZOOM, pas par reorganisation (voir T.ajuste) : la
    -- composition ne bouge pas, elle grandit.
    local MINI, MAXI = 0.55, 1.70
    pcall(function()
        ac.setWindowSizeConstraints('shootkam_os',
            vec2(COLONNE.w * MINI, COLONNE.h * MINI),
            vec2(COLONNE.w * MAXI, COLONNE.h * MAXI))
    end)

    -- ⚠️ LES DEUX ANCIENNES FENETRES SONT FERMEES, PAS OUBLIEES. Le manifeste
    -- ne les declare plus, mais AC retient les fenetres qu'il a deja vues : chez
    -- un joueur qui avait la version a trois fenetres, skos_g et skos_d
    -- reapparaitraient vides au premier lancement. On les ferme une fois, au
    -- demarrage, et on n'y revient plus.
    if dernier == nil then
        dernier = 'fait'
        for _, id in ipairs(FEN) do
            pcall(function() ac.setWindowOpen(id, false) end)
        end
    end
end

local function ouvert(id)
    local ok, res = pcall(function() return ac.isWindowOpen(id) end)
    return ok and res or false
end

-- ---------------------------------------------------------------------------
--  LE RACCOURCI CLAVIER.
--
--  Depuis la v1.6 il n'y a plus qu'une fenetre a ouvrir et a fermer — c'est
--  tout l'interet de la colonne, et ca simplifie ce raccourci d'autant.
-- ---------------------------------------------------------------------------
RG.surBascule = function()
    pcall(function()
        ac.setWindowOpen('shootkam_os', not ouvert('shootkam_os'))
    end)
end

-- ---------------------------------------------------------------------------
--  Rappels de fenetre. FILET N° 1 : la fermeture rend la camera.
-- ---------------------------------------------------------------------------
function onShow() end

function onHide()
    K.rendre(true)
    EP.quitte()
    for _, id in ipairs(FEN) do
        pcall(function() ac.setWindowOpen(id, false) end)
    end
end

-- ---------------------------------------------------------------------------
--  Rappel du monde : tourne a chaque image, fenetres ouvertes OU FERMEES.
--  C'est ce qui permet au neon de rester anime apres qu'on a ferme le menu.
-- ---------------------------------------------------------------------------
function onWorldUpdate(dt)
    -- ⚠️ LE RACCOURCI S'ARME ICI, PAS DANS L'ECRAN DES REGLAGES. Un bouton
    -- cree au premier dessin du panneau ne servirait a rien : c'est justement
    -- quand l'app est FERMEE qu'on a besoin d'une touche pour la rappeler.
    -- (Avec LAZY = 1, ce rappel ne demarre qu'apres la premiere ouverture de
    -- la session — l'ecran des reglages le dit plutot que de le taire.)
    RG.preparer()
    RG.mesurer()
    NE.avance(dt)
    -- La balance de caisse tourne ici et non dans le dessin du panneau : elle
    -- doit continuer quand le menu est range, sinon on ne peut pas la regarder.
    pcall(function() BCA.avance(dt) end)
    -- ⚠️ L'ETALONNAGE SE REPOSE A CHAQUE IMAGE, ET DEPUIS ICI, PAS DEPUIS LE
    -- DESSIN DU PANNEAU. ac.setColorCorrection remet son etat a zero a chaque
    -- appel : ne l'appeler qu'en dessinant l'ecran ferait disparaitre la
    -- retouche des qu'on ferme le menu — c'est-a-dire au moment precis ou on
    -- veut la voir.
    IM.appliquer()
end

-- ---------------------------------------------------------------------------
--  LA COLONNE — l'unique fenetre. Elle porte la marque, les sept onglets, le
--  chemin, les favoris, les deux moities de l'atelier courant, le panneau
--  lui-meme et la barre de vue. Elle tient aussi la camera.
-- ---------------------------------------------------------------------------
function windowMain(dt)
    dt = dt or 0.016
    etat.horloge = etat.horloge + dt
    -- L'echelle AVANT l'origine : les deux servent au meme repere, et T.V lit
    -- les deux. Lire « a quelle taille, puis a quel endroit » suit le geste.
    T.ajuste(COLONNE.w, COLONNE.h)
    T.origine(COLONNE.x, COLONNE.y)

    K.avance(dt, true)
    synchroniser()

    -- -----------------------------------------------------------------------
    --  VISER UNE PIECE SUR LA VOITURE — Ctrl + clic.
    --
    --  ⚠️ CTRL, ET PAS UN CLIC SEUL. Le clic gauche sert deja partout : les
    --  pastilles, les curseurs, le deplacement de la carte. Un clic nu qui
    --  selectionnerait aussi une piece rendrait chaque geste ambigu. Ctrl est
    --  la convention de tous les outils qui font de la selection dans une vue
    --  3D, et il ne sert a rien d'autre ici.
    --
    --  ⚠️ ET SEULEMENT HORS DES FENETRES. Ctrl-cliquer un bouton de l'app
    --  enverrait un rayon derriere le panneau et selectionnerait ce qu'il y a
    --  dessous — une piece qu'on ne voit meme pas.
    --
    --  Il ne se declenche que dans l'atelier PEINTURE : ailleurs, la selection
    --  d'une piece n'a pas de sens, et le rayon couterait pour rien.
    if etat.onglet == 1 and etat.atelier == 1 then
        local surFenetre = false
        pcall(function()
            surFenetre = ui.windowHovered(ui.HoveredFlags.AnyWindow) == true
        end)
        local ctrl = false
        pcall(function() ctrl = ac.getUI().ctrlDown == true end)
        if ctrl and not surFenetre and ui.mouseClicked(ui.MouseButton.Left) then
            local sx, sy = nil, nil
            pcall(function()
                local m = ui.mousePos()
                sx, sy = m.x, m.y
            end)

            -- ⚠️ LA PILE VAUT POUR UN POINT DE VUE, PAS POUR UN PIXEL. Meme
            -- pointeur immobile, deplacer la camera au clavier change tout ce
            -- que le rayon traverse. On retient donc aussi d'ou l'on regardait :
            -- dix centimetres d'ecart, et la pile est refaite.
            local cam = nil
            pcall(function() cam = ac.getCameraPosition():clone() end)

            -- Meme endroit qu'au dernier clic ? On descend d'une couche.
            -- Sinon on refait la pile. Six pixels de tolerance : la main
            -- bouge toujours un peu entre deux clics, et exiger le pixel
            -- exact rendrait le cycle impossible a declencher.
            local memeEndroit = sx ~= nil
                                and math.abs(sx - vise.x) < 6
                                and math.abs(sy - vise.y) < 6
                                and (cam == nil or vise.cam == nil
                                     or cam:distance(vise.cam) < 0.1)
            if memeEndroit and #vise.pile > 0 then
                vise.index = vise.index % #vise.pile + 1
            else
                vise.pile = AV.viserTous(10)
                vise.index = 1
                vise.x, vise.y = sx or -999, sy or -999
                vise.cam = cam
            end

            -- ⚠️ DEUX MATERIAUX PEUVENT DESIGNER LA MEME PIECE. La liste
            -- regroupe par nom lisible : la tole exterieure et sa doublure
            -- portent toutes deux « Carrosserie ». Descendre de l'une a
            -- l'autre ne changerait rien a l'ecran, et le clic paraitrait
            -- mort. On continue donc a descendre tant que le resultat est
            -- identique — au plus une fois le tour de la pile.
            local avant = EP.selection()
            local mat, nom, elargi = nil, nil, false
            for essai = 1, math.max(1, #vise.pile) do
                mat = vise.pile[vise.index]
                if mat == nil then break end
                nom, elargi = EP.choisirMateriau(mat)
                if nom ~= avant or not memeEndroit or essai == #vise.pile then
                    break
                end
                vise.index = vise.index % #vise.pile + 1
            end

            if mat ~= nil then
                -- ⚠️ ON DIT QUAND LA PIECE EXISTE MAIS EST MASQUEE. Viser un
                -- pneu ou une vitre ne selectionne rien : ils sont ecartes de
                -- la liste par defaut. Sans ce message, le clic parait
                -- simplement ne pas marcher.
                viseMessage = (nom or ('Non peignable : ' .. AV.libelle(mat)))
                if #vise.pile > 1 then
                    viseMessage = string.format('%s   (%d/%d — Ctrl+clic : dessous)',
                                                viseMessage, vise.index, #vise.pile)
                end
                -- La liste vient de changer d'etendue toute seule : le taire
                -- laisserait croire a un bug la prochaine fois qu'on la lit.
                if elargi then
                    viseMessage = viseMessage .. '   • vue complète activée'
                end
                viseHorloge = etat.horloge
            end
        end
    end

    -- L'orbite est lue ICI et nulle part ailleurs : appelee depuis chaque
    -- fenetre, elle multiplierait la rotation par le nombre de panneaux.
    -- dt lui sert : le mode libre se deplace en metres par SECONDE, pas par
    -- image — sans quoi la vitesse dependrait de la machine.
    BC.souris(dt)

    local x, y, w, h = COLONNE.x, COLONNE.y, COLONNE.w, COLONNE.h
    local sx, sw = x + MARGE_C, w - MARGE_C * 2

    -- ⚠️ LA FENETRE NE SE DEPLACE QUE PAR SA MARQUE. Voir fenetre.lua : en
    -- NO_TITLE_BAR, ImGui deplace la fenetre des qu'on presse sur du vide, ce
    -- qui volait le glissement aux curseurs. La bande de la marque, a gauche,
    -- est le seul endroit laisse libre.
    --
    -- ⚠️ ET UNE SEULE POIGNEE POUR TOUTE LA FENETRE. L'ancre de la colonne est
    -- posee en mode normal — elle epargne la bande de la marque — puis on passe
    -- en MONO : les panneaux ancrent alors TOUT, ruban compris. Sans cela, le
    -- ruban de chaque panneau afficherait encore le curseur de deplacement pour
    -- un geste qui ne fait plus rien, la colonne etant deja tenue.
    W.MONO = false
    W.ancrer(x, y, w, h, x, 300, 56)
    W.MONO = true

    -- -----------------------------------------------------------------------
    --  LE FOND.
    --
    --  ⚠️ PAS DE FLOU D'ARRIERE-PLAN DANS CSP. Le verre depoli du croquis
    --  floute ce qu'il y a derriere ; aucune fonction de dessin ne sait le
    --  faire a bon marche ici. On construit donc la profondeur autrement : un
    --  degrade et un halo colore. A l'oeil l'effet tient ; au cout, c'est deux
    --  rectangles.
    -- -----------------------------------------------------------------------
    T.rectP(x, y, w, h, rgbm(0.045, 0.05, 0.075, 0.97), 20)
    T.degrade(x, y, w, h, rgbm(0.10, 0.06, 0.16, 0.85),
              rgbm(0.03, 0.04, 0.07, 0.90), false)
    T.lueur(x + w * 0.5, y + 120, 300, rgbm(0.62, 0.10, 0.34, 1), 10)
    T.rectC(x, y, w, h, rgbm(1, 1, 1, 0.07), 20, 1)
    T.degrade(x + 24, y, w - 48, 3, T.ACCENT, rgbm(0.60, 0.26, 1, 1), true)

    -- -----------------------------------------------------------------------
    --  RANGEE 1 — la marque, et l'etat de la partie.
    -- -----------------------------------------------------------------------
    T.cercleC(x + 40, y + 30, 11, T.ACCENT, 2)
    T.cercleP(x + 40, y + 30, 4, T.ACCENT)
    T.txt('SHOOTKAM', 15, x + 62, y + 19, T.TX, T.F_NOIR, 1.5)

    local pseudo = '—'
    pcall(function() pseudo = ac.getDriverName(0) or '—' end)
    local sim = ac.getSim()
    local ips = sim and math.floor(sim.fps or 0) or 0
    local ton = ips < 30 and T.RED or (ips < 50 and T.ORANGE or T.GREEN)
    T.txtD(T.court(pseudo, 11, T.F_DEMI, 200), 11, x + w - 74, y + 16,
           T.TX, T.F_DEMI)
    T.txtD(string.format('%d img/s  ·  v%s', ips, RG.version()), 9,
           x + w - 74, y + 34, T.TX3, T.F_REG)
    T.lueur(x + w - 48, y + 30, 16, ton, 6)
    T.cercleP(x + w - 48, y + 30, 5, ton)

    -- -----------------------------------------------------------------------
    --  RANGEE 2 — les sept onglets, sur toute la largeur.
    --
    --  ⚠️ ILS SE REPLIENT, ILS NE DEBORDENT PAS. Ils tenaient sur une rangee
    --  quand l'accueil faisait 1200 de large ; la colonne en fait 668, et la
    --  fenetre est redimensionnable par-dessus le marche. Une rangee ecrite en
    --  dur sortirait du cadre sans qu'on puisse le prevoir ici. Chaque onglet
    --  mesure son texte et passe a la ligne quand il n'y a plus la place —
    --  meme procede que la barre d'options de SHOOTKAM MAP.
    -- -----------------------------------------------------------------------
    local oy = y + 56
    local ox = sx
    for i2 = 1, #ECRANS do
        local nom = ECRANS[i2].titre
        local lw = T.mesure(nom, 11, T.F_DEMI, 1.2) + 26
        if ox > sx and ox + lw > sx + sw then
            ox, oy = sx, oy + 38
        end
        local act = (i2 == etat.onglet)
        T.txtC(nom, 11, ox + lw / 2, oy + 10,
               act and T.TX or T.TX3, act and T.F_DEMI or T.F_REG, 1.2)
        if act then
            T.degrade(ox + 8, oy + 30, lw - 16, 3, T.ACCENT,
                      rgbm(0.92, 0.17, 0.68, 1), true)
        elseif C.clique(ox, oy, lw, 34) then
            if etat.onglet == 1 then EP.quitte() end
            etat.onglet = i2
            etat.moitie = 1
            if i2 == 4 then pcall(EA.entre) end
        end
        ox = ox + lw + 6
    end

    -- -----------------------------------------------------------------------
    --  RANGEE 3 — LE SOUS-MENU, SOUS SON ONGLET.
    --
    --  ⚠️ IL VIVAIT EN BAS DE L'ECRAN, ET C'ETAIT UNE ERREUR DE LECTURE. Les
    --  ateliers du Garage sont un niveau DE PLUS que l'onglet Garage : les
    --  poser a l'autre bout du panneau cassait le lien de parente. Rien ne
    --  disait qu'ils dependaient de l'onglet actif, et changer d'onglet
    --  semblait ne rien faire puisque la rangee restait a sa place.
    -- -----------------------------------------------------------------------
    local sy = oy + 44
    local ax = sx
    if etat.onglet == 1 then
        -- ⚠️ L'ATELIER ACTIF NE PEUT PAS ETRE UN ATELIER CACHE. Sans ce
        -- garde-fou, quitter l'app sur Jantes puis y revenir rouvrirait un
        -- ecran dont plus aucun bouton ne permet de sortir.
        if not PRETS[etat.atelier] then etat.atelier = 1 end

        for i2, nom in ipairs(ATELIERS) do
            -- ⚠️ CE QUI N'EST PAS PRET NE SE MONTRE PLUS DU TOUT. Un atelier
            -- pas pret restait visible mais eteint, pour ne pas faire croire
            -- qu'il n'existait pas. A l'usage c'est l'inverse qui se produit :
            -- une case grise au milieu du chemin donne envie de cliquer, et ne
            -- rien obtenir ressemble a une panne.
            if PRETS[i2] then
                local lw = T.mesure(nom, 11, T.F_DEMI) + 30
                if ax > sx and ax + lw > sx + sw then
                    ax, sy = sx, sy + 40
                end
                local act = (i2 == etat.atelier)
                if T.bouton(ax, sy, lw, 34, nom, act,
                            rgbm(0.92, 0.17, 0.68, 1), rgbm(0.60, 0.26, 1, 1), 11)
                   and not act then
                    if etat.atelier == 1 and i2 ~= 1 then EP.quitte() end
                    etat.atelier = i2
                    etat.moitie = 1
                end
                ax = ax + lw + 8
            end
        end
    else
        -- Les autres onglets n'ont pas d'ateliers : on montre ce qu'ils
        -- contiennent, tire de leur propre sous-titre. Inerte, mais a la bonne
        -- place — le chemin reste lisible d'un onglet a l'autre.
        for morceau in string.gmatch(ECRANS[etat.onglet].sous, '[^·]+') do
            local nom = morceau:match('^%s*(.-)%s*$')
            if nom ~= '' then
                local lw = T.mesure(nom, 11, T.F_REG) + 26
                if ax > sx and ax + lw > sx + sw then
                    ax, sy = sx, sy + 40
                end
                T.rectP(ax, sy, lw, 34, rgbm(1, 1, 1, 0.04), 11)
                T.txtC(nom, 11, ax + lw / 2, sy + 10, T.TX3, T.F_REG)
                ax = ax + lw + 8
            end
        end
    end

    -- Le retour de la visee, a droite du chemin, pendant quatre secondes.
    if viseMessage ~= nil and etat.horloge - viseHorloge < 4 then
        T.txtD('▸ ' .. T.court(viseMessage, 11, T.F_DEMI, sw - 40), 11,
               sx + sw, sy + 44, T.ACCENT, T.F_DEMI)
    end

    -- -----------------------------------------------------------------------
    --  RANGEE 4 — LES FAVORIS, EN BANDE.
    --
    --  ⚠️ ILS ETAIENT UN PANNEAU DE 240 DE HAUT DANS L'ANCIEN ACCUEIL. La
    --  colonne n'a pas cette place, et surtout ce n'est pas ce qu'on leur
    --  demande : un favori sert a regler UNE valeur sans entrer dans son
    --  onglet. Une bande de curseurs fait cela en 34 px.
    --
    --  ⚠️ ET ELLE NE PREND PAS DE PLACE QUAND ELLE EST VIDE. Une bande vide
    --  avec un titre « FAVORIS » occuperait la hauteur d'une rangee pour dire
    --  qu'il n'y a rien a montrer.
    -- -----------------------------------------------------------------------
    local fy = sy + 46
    local favs = FA.liste()
    if #favs > 0 then
        T.txt('FAVORIS', 9, sx, fy + 8, rgbm(1, 0.78, 0.20, 1), T.F_DEMI, 1.6)
        local fx = sx + T.mesure('FAVORIS', 9, T.F_DEMI, 1.6) + 14
        local reste = sx + sw - fx
        local n = math.max(1, math.min(#favs, math.floor(reste / 148)))
        local bw = (reste - (n - 1) * 8) / n
        for i2 = 1, n do
            local e2 = favs[i2]
            T.rectP(fx, fy, bw, 34, rgbm(1, 1, 1, 0.05), 10)
            T.txt(T.court(e2.nom, 10, T.F_REG, bw - 20), 10, fx + 10, fy + 3,
                  T.TX2, T.F_REG)
            local part = FA.part(e2)
            local rx, rw2 = fx + 10, bw - 20
            T.rectP(rx, fy + 23, rw2, 4, rgbm(1, 1, 1, 0.14), 2)
            T.rectP(rx, fy + 23, rw2 * part, 4, rgbm(1, 0.78, 0.20, 1), 2)
            -- ⚠️ LA ZONE DE PRISE EST PLUS HAUTE QUE LE RAIL. Viser un trait de
            -- quatre pixels a la souris est penible, et personne ne remarque
            -- qu'on a ete large.
            if C.zonePrise('fav' .. e2.id, rx - 4, fy, rw2 + 8, 34) then
                FA.poser(e2, math.max(0, math.min(1, (T.sourisX() - rx) / rw2)))
            end
            fx = fx + bw + 8
        end
        if #favs > n then
            T.txtD('+' .. (#favs - n), 9, sx + sw, fy + 12, T.TX3, T.F_REG)
        end
        fy = fy + 44
    end

    -- -----------------------------------------------------------------------
    --  RANGEE 5 — LES DEUX MOITIES, EN ONGLETS NOMMES.
    --
    --  ⚠️ « GAUCHE » ET « DROITE » NE VEULENT PLUS RIEN DIRE. Ils decrivaient
    --  une POSITION a l'ecran, et cette position n'existe plus : il n'y a
    --  qu'une colonne. Chaque ecran donne donc le nom de ses deux moities, et
    --  ce sont les memes mots que ses rubans — on sait ce qu'on va trouver
    --  avant de cliquer.
    --
    --  ⚠️ ET UN ECRAN QUI N'A QU'UNE MOITIE N'AFFICHE AUCUN ONGLET. Une rangee
    --  d'un seul element donne a croire qu'il en manque un.
    -- -----------------------------------------------------------------------
    local e = ecranCourant()
    local noms = e.NOMS or {}
    local nMoities = 0
    for i2 = 1, 2 do if e.BOITES[i2] ~= nil then nMoities = i2 end end
    if etat.moitie > nMoities then etat.moitie = 1 end

    local py = fy
    if nMoities > 1 then
        local tx = sx
        for i2 = 1, nMoities do
            local nom = noms[i2] or (i2 == 1 and 'Gauche' or 'Droite')
            local lw = math.max(140, T.mesure(nom, 12, T.F_DEMI) + 44)
            local act = (i2 == etat.moitie)
            T.rectP(tx, py, lw, 34, act and T.PROFOND or rgbm(1, 1, 1, 0.05), 11)
            if act then T.rectC(tx, py, lw, 34, T.ACCENT, 11, 1) end
            T.txtC(nom, 12, tx + lw / 2, py + 9, act and T.TX or T.TX3,
                   act and T.F_DEMI or T.F_REG)
            if not act and C.clique(tx, py, lw, 34) then etat.moitie = i2 end
            tx = tx + lw + 8
        end
        py = py + 44
    end

    -- -----------------------------------------------------------------------
    --  LE PANNEAU.
    --
    --  ⚠️ ON REECRIT SA BOITE, ON NE TOUCHE PAS A SON DESSIN. Chaque ecran
    --  dessine par rapport a b.x / b.y / b.w / b.h ; lui donner une autre boite
    --  suffit a le deplacer. C'est ce qui a permis de refaire toute la fenetre
    --  sans ouvrir un seul ecran.
    --
    --  ⚠️ LA HAUTEUR EST CELLE QUI RESTE, JAMAIS CELLE QUE L'ECRAN DEMANDE.
    --  Elle ne peut que GRANDIR par rapport aux 900 reserves — le sous-menu qui
    --  ne se replie pas rend de la place — et un panneau plus haut que prevu ne
    --  coupe jamais rien.
    -- -----------------------------------------------------------------------
    local b = e.BOITES[etat.moitie]
    if b ~= nil then
        b.x, b.y, b.w = sx, py, sw
        b.h = (y + h - MARGE_C - BARRE_VUE - 12) - py
        e.panneau(etat.moitie, dt)
    end

    -- -----------------------------------------------------------------------
    --  LA BARRE DE VUE, tout en bas.
    -- -----------------------------------------------------------------------
    BC.barre(sx, y + h - MARGE_C - BARRE_VUE, sw)
end

-- ⚠️ windowGauche ET windowDroite ONT DISPARU AVEC LEURS FENETRES (v1.6). Elles
-- ne faisaient qu'annoncer une origine et deleguer a l'ecran courant ; la
-- colonne fait le meme travail en ligne, avec la boite qu'elle reecrit
-- elle-meme. Le manifeste ne declare plus qu'une fenetre : les laisser
-- laisserait deux fonctions que plus aucun manifeste n'appelle.
