Files
Loki/internal/loki/ui/src/styles.css
T
Claude 82136d6e0d Palettes officielles Stitch, géométrie et moniteur relevés
Le projet Stitch porte deux planches que je n'avais pas vues — « Official
Palette for Loki AI v1 » et « Official Dark Mode Palette for Loki AI ». Leurs
couleurs NOMMÉES font désormais foi, et elles corrigent le relevé précédent, que
la compression JPEG des captures avait décalé :

  clair   Main BG #F5F6F8 · Sidebar BG #ECECEC · Primary Accent #95A899
          Typography #333333
  sombre  Deep Navy #0F172A · Slate Surface #1E293B · Sage Accent #84A98C

Le thème sombre retrouve donc exactement les deux teintes du cahier des charges,
que mon relevé sur capture avait fait dériver vers #1E2834/#253141. Les surfaces
non nommées (carte de réponse, carte de saisie, piste de jauge, bulle sombre)
restent relevées sur les écrans de la maquette, en pleine résolution cette fois.

Géométrie, elle aussi mesurée sur la maquette :

- Bulles et carte de réponse : rayon 10 px (contre 16/18 et un coin rentré), la
  carte sans cadre — c'est l'ombre qui la détache.
- Carte de saisie : rayon 14 px et CADRE SAUGE, le seul contour coloré de
  l'écran. Il retombait au gris commun, une règle de coque plus bas dans la
  feuille reprenant la main sur la couleur.
- Liste des discussions : lignes pleine largeur séparées d'un filet, bande plate
  pour l'active — la maquette n'a ni pastille arrondie ni gouttière.
- Moniteur machine : titre en casse normale (le monospace capitales de la charte
  précédente y criait), libellé + pourcentage à la même échelle, jauge de 7 px
  entièrement arrondie. La sous-ligne chiffrée passe en infobulle et le bouton
  de repli disparaît : la maquette n'en a pas.
- Une jauge de VRAM par carte, en plus de la charge : sur une machine à une
  seule carte on retombe sur les trois jauges de la maquette (GPU, VRAM, RAM),
  et pour du LLM c'est la VRAM qui décide si un modèle tient.
- Échelle typographique des planches : corps 16 px dans le fil et la saisie,
  secondaire 14 px medium dans la barre latérale.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LPyFxVHNAN9u5pVzSYMwjd
2026-08-16 19:03:08 +00:00

1766 lines
125 KiB
CSS
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ===== Polices =====================================================
Inter (interface) + JetBrains Mono (chiffres, chemins, code). Servies par Loki
(/fonts/, embarquées dans le binaire) et NON par Google Fonts : une instance
locale ou coupée d'internet doit s'afficher correctement, sans qu'un tiers
apprenne qui consulte l'interface.
Sous-ensemble LATIN uniquement (~48 + ~31 Ko) : il couvre le français, y
compris œ et les guillemets. Les symboles de l'UI (↻ ⌫ ☰ 🗑) n'ont jamais été
dessinés par la police d'interface — ils viennent du système, ici comme avant.
swap : le texte s'affiche tout de suite dans la police de repli, puis bascule
— jamais de blanc au chargement. */
@font-face{font-family:'Inter';src:url('/fonts/inter.woff2') format('woff2');font-weight:100 900;font-display:swap;font-style:normal}
@font-face{font-family:'JetBrains Mono';src:url('/fonts/jetbrainsmono.woff2') format('woff2');font-weight:100 800;font-display:swap;font-style:normal}
/* ===== Palette « Sober Tech » ======================================
Ardoise et sauge. Deux variantes — claire par défaut, sombre via
data-theme="dark" sur <html>. Toutes les couleurs de l'UI passent par ces
variables ; rien d'autre ne code une couleur en dur.
⚠️ Les valeurs NOMMÉES viennent des deux planches officielles du projet Stitch
« Loki AI Chat Modern Redesign v1 » — « Official Palette for Loki AI v1 » et
« Official Dark Mode Palette for Loki AI » — pas d'une approximation à l'œil :
clair Main BG #F5F6F8 · Sidebar BG #ECECEC · Primary Accent #95A899
Typography #333333
sombre Deep Navy #0F172A · Slate Surface #1E293B · Sage Accent #84A98C
Le reste (carte de réponse, carte de saisie, piste de jauge, bulle sombre) est
relevé sur les écrans de la maquette, qui posent une hiérarchie de surfaces
que l'intuition inverse volontiers :
barre latérale la plus SOMBRE des trois #ECECEC / #0F172A
fil la surface de référence #F5F6F8 / #1E293B
carte s'ÉCARTE du fil #EAEBEE / #2A3646
saisie la plus claire #FBFBFD / #1B2436
En clair la carte de réponse est donc plus sombre que le fil (et non blanche),
en sombre plus claire : dans les deux cas elle se détache en s'éloignant du
fond, jamais en allant vers le blanc.
La bulle de l'utilisateur ne suit PAS l'accent : sauge en clair (#7EA58A),
ardoise en sombre (#334257) — la maquette réserve le vert aux jauges dès que
le fond est sombre.
Rôles : --accent = sauge (sélection, marche, liens, bulle utilisateur) ·
--warn = ambre (chiffres, mesures, avertissements) · --err = rouille (arrêt,
danger). --row-bg / --code-bg sont les deux fonds encastrés d'un panneau.
--code-bg est nettement plus soutenu que --bubble : un bloc de code posé DANS
la carte de réponse doit se lire comme un encadré, pas se fondre dedans.
--on-accent : ce qui s'écrit SUR un aplat de sauge. Il change avec la
variante — blanc sur la sauge foncée du thème clair, ardoise sur la sauge
claire du thème sombre — sinon un des deux thèmes écrit du texte à 2:1 de
contraste sur la bulle utilisateur et le bouton d'envoi. */
:root{color-scheme:light;
--font:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif;
--mono:'JetBrains Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
--bg:#f5f6f8;--panel:#fbfbfd;--bubble:#eaebee;--border:rgba(51,51,51,.11);
--text:#333333;--dim:#6b7280;
--accent:#587062;--accent-bg:rgba(149,168,153,.20);--on-accent:#ffffff;--on-err:#ffffff;
--accent-fill:#95a899;--accent-soft:#95a899;
--user-bg:#95a899;--on-user:#ffffff;
--side:#ececec;--row-bg:#f0f0f1;--row-bg-on:#e0e0e2;--track:#dadbdf;
--ok:#587062;--warn:#a86a2a;--err:#c0503f;--mag:#a86a2a;
--code-bg:#e2e3e7;--danger-bg:#f5e3e0;--warn-bg:#f2ece1;
--add:#587062;--add-bg:rgba(149,168,153,.20);--del:#c0503f;--del-bg:rgba(192,80,63,.12);
--shadow:0 1px 2px rgba(15,23,42,.05);--shadow-lift:0 4px 16px rgba(15,23,42,.08);
--shadow-card:0 2px 10px rgba(15,23,42,.06),0 1px 3px rgba(15,23,42,.04)}
/* « Deep Dark » : fond #0F172A, cartes #1E293B. */
[data-theme="dark"]{color-scheme:dark;
--bg:#1e293b;--panel:#1b2436;--bubble:#2a3646;--border:rgba(148,163,184,.16);
--text:#e2e8f0;--dim:#94a3b8;
--accent:#a3c4ab;--accent-bg:rgba(132,169,140,.22);--on-accent:#0f172a;--on-err:#0f172a;
--accent-fill:#84a98c;--accent-soft:#84a98c;
--user-bg:#334259;--on-user:#e8eef5;
--side:#0f172a;--row-bg:#1a2334;--row-bg-on:#232c3f;--track:#232a3a;
--ok:#a3c4ab;--warn:#d9a55f;--err:#e0705c;--mag:#d9a55f;
--code-bg:#16202f;--danger-bg:#3a1f1c;--warn-bg:#33291b;
--add:#a3c4ab;--add-bg:rgba(132,169,140,.22);--del:#e0705c;--del-bg:rgba(224,112,92,.16);
--shadow:none;--shadow-lift:0 4px 16px rgba(0,0,0,.35);
--shadow-card:0 2px 12px rgba(0,0,0,.28),0 1px 3px rgba(0,0,0,.20)}
/* Safari iOS peint un rectangle gris/noir sur TOUT élément touché (bouton, lien,
résumé de section, bulle repliable…). C'est un retour tactile par défaut, hors
de notre charte et impossible à styler : on le coupe. Nos états :active/:focus
restent, eux, gérés élément par élément. */
*{box-sizing:border-box;-webkit-tap-highlight-color:transparent}
/* ⚠️ overflow:hidden sur html/body est INTERDIT EN PWA (tenté puis retiré le
2026-07-28) : en PWA iOS, si le document ne peut plus défiler, WebKit décale
TOUTE la mise en page à l'ouverture du clavier pour montrer le champ actif —
la carte de saisie se retrouve plaquée en haut de l'écran. Le rebond
élastique du document est le prix à payer ; il est cosmétique, le clavier
ne l'est pas.
Laisser le document défiler se paie cher, et EN PWA AUSSI : la coque y fait
956 pt (obligatoire pour que la carte atteigne le bas de l'écran) alors que la
fenêtre de mise en page n'en fait que 894 → il reste 62 pt de document
défilable en permanence, même avec un fil VIDE, et la carte de saisie — posée
en bottom:0 dans le body — part avec au lieu de rester collée en bas.
On coupe donc le défilement du document PARTOUT (2026-07-29). Le fil, lui,
défile toujours dans #chat, qui borne son rebond (overscroll:contain).
⚠️ SI LE CLAVIER iOS SE REMET À TOUT DÉCALER EN PWA, c'est ce overflow:hidden
qu'il faut rouvrir en premier — c'était le compromis d'origine. */
/* Hauteur de la coque : elle DIFFÈRE entre PWA et navigateur, il faut les deux.
- PWA (écran d'accueil) : 100vh, et SURTOUT ni 100dvh ni 100%. Mesuré sur
iPhone (2026-07-28) : la vue web couvre tout l'écran (956 pt — le contenu
passe bien sous l'horloge), mais la fenêtre de mise en page ne fait que
894 pt et reste calée en haut. Or 100dvh ET 100% valent tous deux 894 → il
restait une bande morte de 62 pt (= la zone sûre du haut) en bas de l'écran,
sous la carte de saisie. 100vh se réfère au GRAND viewport (956) : la seule
unité juste dans ce cas.
- Navigateur (barre d'URL + barre d'outils visibles) : 100vh vaut justement ce
GRAND viewport, donc la coque dépasse la zone réellement visible → le
document défile alors que le fil est vide, et le bas de la carte de saisie
(modèle / contexte) est coupé. Ici c'est 100dvh qu'il faut : la hauteur
visible du moment.
D'où : 100dvh par défaut, et 100vh UNIQUEMENT en PWA.
⚠️ Le tri se fait sur l'attribut html[data-pwa] (pose par le script du <head>),
PAS sur le media `display-mode:standalone` : WebKit adosse ce media au `display`
du MANIFESTE, or cette app n'en a pas (elle vit en apple-mobile-web-app-capable).
En PWA iOS il repond donc `browser`, la branche navigateur s'appliquait, et la
bande morte de 62 pt sous la carte de saisie etait de retour (constate le
2026-07-29).
⚠️ Ne pas y ajouter overflow:hidden : voir plus haut, ça casse le clavier iOS. */
body{margin:0;font-family:var(--font);background:var(--bg);color:var(--text);height:100vh;height:100dvh;overscroll-behavior:none;display:flex;font-size:14px}
html[data-pwa="1"] body{height:100vh}
/* Seul #chat défile ; le document, lui, est figé (cf. l'avertissement ci-dessus).
Exception PWA : on lui rend son défilement, sinon le clavier iOS casse tout. */
html,body{overflow:hidden}
/* PWA plein écran (viewport-fit=cover) : on décale le contenu des bords pour ne
pas passer sous l'encoche / la barre système / l'indicateur d'accueil. Les
env(safe-area-inset-*) valent 0 hors PWA, donc aucun impact ailleurs.
⚠️ PAS de padding-bas ici : le composer est posé en bottom:0 DANS le body, donc
une marge basse le décollerait du bord de l'écran et laisserait une bande de
fond visible sous la carte (visible seulement en PWA, où l'inset vaut ~34 px).
L'indicateur d'accueil est esquivé bloc par bloc, en padding INTERNE. */
body{padding:env(safe-area-inset-top) env(safe-area-inset-right) 0 env(safe-area-inset-left)}
/* Listes déroulantes : la LISTE ouverte n'hérite pas du fond de son <select>.
Plusieurs de nos select sont en background:transparent + color clair (chevron
discret) : en thème sombre, le navigateur peignait donc du texte clair sur le
fond blanc par défaut de la liste — illisible (signalé sur « Cache KV », mais
ça touchait TOUTES les listes déroulantes en sombre). On habille donc les
<option> eux-mêmes ; color-scheme (posé sur <html>) ne suffit pas dès que le
select porte des couleurs explicites. */
select{color-scheme:inherit}
option,optgroup{background:var(--panel);color:var(--text)}
.side{width:320px;border-right:1px solid var(--border);padding:16px;padding-bottom:calc(16px + env(safe-area-inset-bottom));overflow-y:auto;flex-shrink:0}
.main{flex:1;display:flex;flex-direction:column;min-width:0}
#menubtn{display:none;position:fixed;top:calc(10px + env(safe-area-inset-top));left:calc(10px + env(safe-area-inset-left));z-index:20;background:var(--panel);border:1px solid var(--border);color:var(--text);width:40px;height:40px;border-radius:6px;font-size:18px;cursor:pointer}
#backdrop{display:none;position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:10}
/* Pastille « installation en cours » : suit exactement les règles d'affichage
du bouton ☰ (elle ne sert que quand le panneau est en tiroir, donc caché). */
#lc-chip{display:none;position:fixed;top:calc(10px + env(safe-area-inset-top));left:calc(58px + env(safe-area-inset-left));z-index:20;max-width:60vw;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;background:var(--panel);border:1px solid var(--accent);color:var(--accent);height:40px;padding:0 12px;border-radius:6px;font-size:12px;cursor:pointer}
#lc-chip.failed{border-color:var(--err);color:var(--err)}
@media (max-width:720px){
.side{position:fixed;inset:0 auto 0 0;width:80%;max-width:320px;z-index:15;transform:translateX(-100%);transition:transform .2s;background:var(--bg);padding-top:calc(16px + env(safe-area-inset-top));padding-left:calc(16px + env(safe-area-inset-left))}
.side.open{transform:translateX(0)}
#menubtn,#backdrop.open{display:block}
#lc-chip:not([hidden]){display:block}
body.drawer-open #menubtn,body.drawer-open #lc-chip{display:none}
.main{width:100%}
#chat{padding:60px 8px 12px}
.msg{max-width:92%}
#chat .msg.assistant,#chat .msg.reasoning{max-width:100%}
#inputbar{padding:8px;gap:4px}
/* Petit écran (menu replié en tiroir) : on masque l'aide clavier — inutile et
ça prend de la place. Elle reste visible sur desktop. */
#sendhint{display:none}
/* …sauf quand elle annonce que le modèle charge : là c'est une information
d'état, pas un rappel de raccourci. */
#sendhint.waiting{display:block}
}
/* Desktop : menu plus large et bulles (badges) un peu plus grosses. */
@media (min-width:721px){
.side{width:380px}
.preset-meta .qtag,.preset-meta .rtag,.preset-meta .btag{font-size:11px;padding:2px 9px;border-radius:9px}
.preset-meta .qtag svg,.preset-meta .rtag svg,.preset-meta .btag svg{width:11px;height:11px}
}
h1{margin:0;font-size:18px;color:var(--accent)}
/* Logo : « Loki » dessiné en pixels (SVG 4x5 par lettre, trait d'un module). Le « A »
reste un bloc à part : il apparaît en glissant quand le mode agent est actif. */
#brand{display:inline-flex;align-items:center}
/* 17px : le logo pixel-art était minuscule à 10px, illisible sur un écran dense.
Multiple impair volontaire — la grille du SVG (5 px de haut) reste nette. */
#brand svg{height:17px;width:auto;display:block;shape-rendering:crispEdges;fill:currentColor}
#brand-word{display:inline-flex;color:var(--accent)}
#ver{font-size:10px;font-weight:400;color:var(--dim);margin-left:7px;letter-spacing:0;align-self:flex-end;line-height:1;position:relative;top:1px}
.sidehead{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:6px}
/* Attribution : Loki est un fork d'AJEAN (MIT). Discrète mais permanente, et le
nom du projet d'origine renvoie à son dépôt. */
.brand-fork{font-size:10px;color:var(--dim);margin:0 0 14px;line-height:1.3}
.brand-fork a{color:var(--dim);text-decoration:underline;text-underline-offset:2px}
.brand-fork a:hover{color:var(--accent)}
/* Discussions : mêmes lignes que les presets, plus deux actions au survol.
Elles restent visibles au clavier (:focus-within) pour rester atteignables
sans souris. */
.conv-acts{display:flex;gap:2px;opacity:0;transition:opacity .12s}
.preset:hover .conv-acts,.preset:focus-within .conv-acts{opacity:1}
.conv-acts .iconbtn{font-size:11px;line-height:1;padding:4px 6px;background:transparent;border:1px solid transparent;border-radius:6px;color:var(--dim);cursor:pointer}
.conv-acts .iconbtn:hover{border-color:var(--border);color:var(--text)}
/* Séparateur de groupe dans la barre latérale : sépare les réglages d'IA des
réglages d'application, sans imbriquer un second niveau de repli (illisible
dans une colonne aussi étroite). */
.side-sep{font-size:10px;text-transform:uppercase;letter-spacing:.12em;color:var(--dim);
margin:18px 0 8px;padding-top:12px;border-top:1px solid var(--border)}
/* Choix d'avatar : une grille de pastilles emoji, celle retenue encadrée. */
.avpick{display:flex;flex-wrap:wrap;gap:4px;margin-top:6px}
.avpick button{font-size:15px;line-height:1;padding:5px 6px;background:var(--panel);
border:1px solid var(--border);border-radius:8px;cursor:pointer}
.avpick button:hover{border-color:var(--accent)}
.avpick button.sel{border-color:var(--accent);background:color-mix(in srgb,var(--accent) 15%,transparent)}
/* Avatar + nom en tête de bulle : l'emoji garde sa taille propre, le nom hérite
du style de .label (petites capitales) mais sans forcer la casse du prénom. */
.msg .label .av{font-style:normal;font-size:13px;margin-right:5px;vertical-align:-1px}
.msg .label .who{text-transform:none;letter-spacing:.04em;font-size:11px}
/* Captures d'écran et images de l'agent : bornées à la largeur de la bulle, et
cliquables pour ouvrir en grand dans un onglet. */
.msg .body img{max-width:100%;height:auto;border-radius:8px;border:1px solid var(--border);display:block;margin:8px 0}
/* pastille d'état service, en haut à droite de la barre latérale */
.statuspill{display:inline-flex;align-items:center;gap:6px;font-size:11px;padding:3px 10px;border-radius:999px;border:1px solid var(--border);background:var(--panel);color:var(--dim);font-weight:600;white-space:nowrap;cursor:pointer}
.statuspill.ok{color:var(--ok);border-color:color-mix(in srgb,var(--ok) 45%,transparent);background:color-mix(in srgb,var(--ok) 12%,transparent)}
.statuspill.err{color:var(--err);border-color:color-mix(in srgb,var(--err) 45%,transparent);background:color-mix(in srgb,var(--err) 12%,transparent)}
.statuspill.loading{color:var(--warn);border-color:color-mix(in srgb,var(--warn) 45%,transparent);background:color-mix(in srgb,var(--warn) 12%,transparent)}
.statuspill.loading .dot{animation:pulse 1s ease-in-out infinite}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.25}}
.statuspill .dot{width:7px;height:7px;border-radius:50%;background:currentColor}
/* en-tête + bouton d'ajout de skill (toujours visible) */
.skillshead{display:flex;align-items:center;justify-content:space-between;margin:12px 0 5px}
.skillshead>span{font-size:11.5px;color:var(--dim);font-weight:600}
.addbtn{font-size:11px;padding:4px 11px;background:var(--panel);color:var(--dim);border:1px solid var(--border);border-radius:999px;cursor:pointer;line-height:1.3;letter-spacing:.02em}
.addbtn:hover{border-color:var(--dim);color:var(--text);background:var(--accent-bg)}
.mem-count{font-size:10px;color:var(--dim);font-weight:400;letter-spacing:0;text-transform:none;font-family:var(--font);margin-left:4px}
/* Titre de sous-section (Mémoire / Accès internet) : simple libellé, PAS de trait
(le trait est réservé aux sections principales — cf. `details`). La séparation
visuelle vient de la carte de lignes qui suit (.slist). */
/* Barre « Voir la mémoire » : libellé à gauche, bouton « + ajouter » à droite.
Pas de chevron : la liste s'ouvre sous la ligne, ça se voit. */
.pages-bar{display:flex;align-items:center;gap:8px;cursor:pointer;user-select:none;padding:7px 0;margin-top:4px}
.pages-bar .pages-title{font-size:11.5px;color:var(--dim);font-weight:600}
.pages-bar .addbtn{margin-left:auto}
.mem-search{width:100%;box-sizing:border-box;background:var(--panel);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:5px 9px;font:inherit;font-size:12px;margin:0 0 6px}
.mem-search:focus{outline:none;border-color:var(--accent)}
.mem-list{max-height:240px;overflow-y:auto;overscroll-behavior:contain}
.mem-more{font-size:11px;color:var(--dim);text-align:center;padding:6px 0 2px}
.ask-overlay{position:fixed;inset:0;background:rgba(0,0,0,.45);z-index:60;display:flex;align-items:center;justify-content:center;padding:20px}
.ask-box{background:var(--bg);border:1px solid var(--border);border-radius:10px;width:100%;max-width:420px;padding:20px;box-shadow:0 12px 40px rgba(0,0,0,.18)}
.ask-title{font-size:15px;font-weight:600;color:var(--text);margin-bottom:8px}
.ask-msg{font-size:13px;color:var(--dim);line-height:1.5;white-space:pre-wrap;word-break:break-word}
.ask-input{width:100%;box-sizing:border-box;margin-top:12px;background:var(--panel);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:9px 11px;font:inherit;font-size:13px}
.ask-input:focus{outline:none;border-color:var(--accent)}
.ask-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:18px}
.ask-actions button{margin:0;padding:7px 16px}
.ask-ok{border-color:var(--accent);color:var(--accent)}
.ask-ok.danger{border-color:var(--err);color:var(--err)}
#brand-a{display:inline-flex;line-height:0;color:var(--text);margin-right:2px}
/* Mode agent : le A passe en couleur d'accent (il était masqué hors agent,
la marque se lisant alors « Loki »). L'état agent reste signalé par sa
pastille dans les réglages. */
#brand.agent #brand-a{color:var(--accent)}
/* interrupteur (switch) */
/* Sous-groupe du mode agent (Mémoire, Accès internet, Serveurs MCP, Paramètres).
MÊME grammaire que « Machine » ou « Apparence » : étiquette de groupe NUE
au-dessus, puis UNE carte .slist qui porte toutes les lignes. Auparavant
chaque sous-groupe était une carte qui embarquait son propre titre et du
contenu libre aux marges variables — quatre îlots décollés au lieu d'une
colonne régulière. Le grisé « agent désactivé » se pose sur le GROUPE
(étiquette comprise), plus sur la carte seule. */
.gated-group{margin:0 0 10px;transition:opacity .15s}
.gated-group.gated{opacity:.4;pointer-events:none;filter:grayscale(.4)}
.switch{position:relative;display:inline-block;width:36px;height:20px;flex:none}
.switch input{position:absolute;opacity:0;width:0;height:0}
/* Piste décochée : un gris franc (pas --border), sinon le pavé blanc ne se
détache pas du rail en mode clair. Même teinte que .pe-sl. */
.switch .slider{position:absolute;inset:0;background:color-mix(in srgb,var(--dim) 55%,transparent);border-radius:999px;transition:background .25s ease}
.switch .slider::before{content:"";position:absolute;width:14px;height:14px;left:3px;top:3px;background:#fff;border-radius:50%;transition:transform .25s ease;box-shadow:0 1px 2px rgba(0,0,0,.25)}
.switch input:checked + .slider{background:var(--accent)}
.switch input:checked + .slider::before{transform:translateX(16px);background:var(--bg)}
.switch input:focus-visible + .slider{box-shadow:0 0 0 2px var(--bg),0 0 0 4px var(--accent)}
h2{margin:20px 0 8px;font-size:11px;color:var(--dim);text-transform:uppercase;letter-spacing:.1em;font-weight:600}
details>summary::-webkit-details-marker{display:none}
.preset-add{display:none;font-size:14px;padding:0 8px;margin:0 8px 0 auto;line-height:1.4;background:var(--panel);color:var(--text);border:1px solid var(--border);border-radius:6px;cursor:pointer}
.preset-add:hover{border-color:var(--accent);color:var(--accent)}
details[open]>summary>.preset-add{display:inline-block}
.tag{display:inline-block;padding:2px 8px;border-radius:10px;font-size:11px;background:var(--panel);border:1px solid var(--border)}
.tag.ok{color:var(--ok);border-color:var(--ok)}
.tag.err{color:var(--err);border-color:var(--err)}
button{background:var(--panel);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:6px 10px;font:inherit;cursor:pointer;margin:2px}
button:hover{border-color:var(--accent);color:var(--accent)}
button:disabled{opacity:.5;cursor:not-allowed}
.row{display:flex;gap:4px;flex-wrap:wrap}
.preset{display:flex;align-items:center;justify-content:space-between;gap:6px;padding:6px 10px;border:1px solid var(--border);border-radius:6px;margin:4px 0;cursor:pointer;background:var(--panel)}
.preset:hover{border-color:var(--accent)}
.preset.active{border-color:var(--accent);color:var(--accent);border-width:1.5px;padding:5.5px 9.5px}
.preset>span{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.preset>button{flex-shrink:0}
/* Liste de lignes cliquables (presets, pages mémoire) posée à fleur de carte :
une pile de petites cartes bordées faisait « trop de carrés » là où le reste
du menu aligne des lignes séparées par un filet. L'actif se signale par un
voile et un liseré à gauche, plus par un cadre propre. */
.rowlist>.preset{border:0;border-radius:0;margin:0;padding:6px 10px;background:transparent}
.rowlist>.preset:not(:last-child){border-bottom:1px solid var(--border)}
.rowlist>.preset:hover{background:var(--row-bg)}
.rowlist>.preset.active{position:relative;background:var(--row-bg-on);color:var(--text);font-weight:600}
/* La carte (.slist) laisse volontairement déborder son contenu (menus), elle ne
rogne donc pas ses coins : c'est la LIGNE de bord qui porte l'arrondi, sinon
le voile dépasse l'angle de la carte. */
.rowlist:first-child>.preset:first-child{border-radius:8px 8px 0 0}
.rowlist:last-child>.preset:last-child{border-radius:0 0 8px 8px}
.rowlist:first-child:last-child>.preset:only-child{border-radius:8px}
/* Liseré de gauche : un trait DROIT (pas un arc) qui court sur TOUTE la hauteur
de la ligne. Donc pas de box-shadow interne (l'arrondi le courberait) mais un
élément à part, aux bords francs, que l'arrondi de la ligne de bord se
contente de ROGNER — il se perd dans l'angle au lieu de le suivre. */
.rowlist>.preset{overflow:hidden}
.rowlist>.preset.active::before,.rowlist>.preset.pending::before{content:"";position:absolute;
left:0;top:0;bottom:0;width:3px;background:var(--text)}
/* La liste est REDESSINÉE à chaque rafraîchissement : une transition ne jouerait
pas (l'élément vient de naître), d'où des @keyframes d'entrée. Le liseré entre
en glissant depuis la gauche, le voile et la puce arrivent en fondu.
⚠️ Elles ne sont posées QUE via .sel-anim, classe mise par loadPresets le seul
redessin où la sélection a bougé : sans ça elles repartaient à chaque
rafraîchissement et la puce clignotait en continu. */
@keyframes bar-in{from{transform:translateX(-100%)}to{transform:translateX(0)}}
@keyframes row-in{from{background:transparent}}
@keyframes dot-in{from{opacity:0;transform:scale(.3)}to{opacity:1;transform:scale(1)}}
.sel-anim.active::before,.sel-anim.pending::before{animation:bar-in .26s cubic-bezier(.2,.7,.3,1) both}
.rowlist>.preset.sel-anim.active,.rowlist>.preset.sel-anim.pending{animation:row-in .26s ease-out both}
.sel-anim .preset-dot{animation:dot-in .26s cubic-bezier(.2,1.5,.5,1) both}
/* Bascule en cours : la ligne visée s'allume DÉJÀ (le clic doit se voir tout de
suite), mais sa barre reste EN RETRAIT — même blanc, opacité plus basse, et
elle respire. Le service met plusieurs secondes à redémarrer : la barre pleine
(plus la puce) est réservée au preset vraiment actif. */
.rowlist>.preset.pending{position:relative;background:var(--row-bg);color:var(--text);font-weight:600}
.rowlist>.preset.pending::before{animation:bar-breathe 1.5s ease-in-out infinite}
.rowlist>.preset.sel-anim.pending::before{
animation:bar-in .26s cubic-bezier(.2,.7,.3,1) both,bar-breathe 1.5s ease-in-out .26s infinite}
@keyframes bar-breathe{0%,100%{opacity:.5}50%{opacity:.2}}
/* Ligne à deux étages (titre + étiquettes) : l'interligne interne suffit, on
resserre donc le haut et le bas. */
.rowlist>.preset .preset-meta{margin-top:4px}
/* Messages et « voir plus » de la liste : alignés sur le retrait des lignes. */
.rowlist>.muted,.rowlist>.mem-more{padding:9px 10px}
.preset-info{flex:1;min-width:0}
.preset-name{min-width:0;overflow-wrap:anywhere;word-break:break-word}
/* Puce du preset actif : disque dessiné en CSS (le caractère « ● » dépend de la
police du système — plus petit et plus bas sous Windows que sous macOS). */
.preset-dot{display:inline-block;width:7px;height:7px;border-radius:50%;
background:currentColor;margin:0 7px 0 0;flex-shrink:0;
/* ⚠️ PAS `vertical-align:middle` : ce mot-clé se cale sur la HAUTEUR D'X de la
police, qui n'est pas la même d'un système à l'autre (Segoe UI sous Windows
vs SF sous macOS) — la puce tombait trop bas sur PC. Un décalage EN LONGUEUR
part de la ligne de base et ne dépend que de la taille de texte : même
rendu partout. .34em ≈ milieu des capitales, moins la moitié du disque. */
vertical-align:calc(.34em - 3.5px)}
/* Icône d'engrenage : centrée verticalement sur la ligne (sa silhouette ronde se
lit bien au milieu de son coin, là où le crayon penché tirait de travers). */
.preset-edit{flex-shrink:0;display:inline-flex;align-items:center;justify-content:center;
margin:0 -2px 0 0;padding:4px;line-height:0;background:transparent;border:none;opacity:.55}
.preset-edit:hover{opacity:1;color:var(--accent);background:transparent}
.preset-meta{display:flex;align-items:center;gap:6px;margin-top:3px;flex-wrap:wrap}
.qtag{flex-shrink:0;font-size:9px;padding:1px 6px;border:1px solid var(--border);color:var(--dim);border-radius:8px;letter-spacing:.03em;text-transform:uppercase}
.rtag{flex-shrink:0;display:inline-flex;align-items:center;gap:3px;font-size:9px;padding:1px 6px;border:1px solid var(--border);color:var(--dim);border-radius:8px;letter-spacing:.03em}
.btag{flex-shrink:0;display:inline-flex;align-items:center;gap:3px;font-size:9px;padding:1px 6px;border:1px solid var(--border);color:var(--dim);border-radius:8px;letter-spacing:.03em}
/* Petit « ? » d'aide : bulle au survol/clic au lieu d'un pavé de texte permanent. */
.help{display:inline-flex;align-items:center;justify-content:center;width:15px;height:15px;margin-left:6px;flex-shrink:0;font-size:10px;font-weight:600;line-height:1;color:var(--dim);border:1px solid var(--border);border-radius:50%;cursor:help;user-select:none;vertical-align:middle}
.help:hover{color:var(--accent);border-color:var(--accent)}
/* La bulle est ancrée à la ligne (.switchrow position:relative), pas au « ? »,
pour rester alignée au bord gauche du panneau et ne pas déborder. */
.help>.tip{display:none;position:absolute;z-index:40;left:0;right:0;top:calc(100% + 2px);padding:8px 10px;background:var(--bg);border:1px solid var(--border);border-radius:6px;font-size:11px;font-weight:400;line-height:1.4;color:var(--text);box-shadow:0 4px 16px rgba(0,0,0,.15);white-space:normal;text-align:left;text-transform:none;letter-spacing:normal;font-family:var(--font);cursor:auto}
.help:hover>.tip,.help:focus>.tip,.help.open>.tip{display:block}
.kv{display:flex;justify-content:space-between;gap:10px;padding:9px 12px;border-bottom:1px solid var(--border);font-size:13px;min-height:38px;align-items:center}
.kv span:first-child{color:var(--text);flex-shrink:0}
.kv:last-child{border-bottom:0}
/* direction:rtl tronque par la GAUCHE (on garde la fin d'un chemin long), mais
inversait l'ordre des groupes : « 18,2 / 24 Go » s'affichait « Go 24 / 18,2 ».
unicode-bidi:plaintext rend chaque valeur dans son sens naturel tout en
gardant l'ancrage à droite. */
.kv span:last-child{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;direction:rtl;unicode-bidi:plaintext;text-align:right;min-width:0;color:var(--dim)}
.bar{height:5px;background:var(--border);border-radius:3px;overflow:hidden;margin:0}
.bar>div{height:100%;background:var(--accent)}
#chat{flex:1;min-height:0;overflow-y:auto;overscroll-behavior:contain;padding:20px;display:flex;flex-direction:column;gap:14px}
:root{--col:max(20px,calc(50% - 450px))}
@media (max-width:720px){#chat{padding:60px 8px 12px}}
.msg{max-width:80%;padding:10px 14px;border-radius:10px;white-space:pre-wrap;word-wrap:break-word;line-height:1.45}
.msg.assistant,.msg.reasoning{white-space:normal}
.msg .body{overflow-wrap:anywhere;word-break:break-word}
.msg.user{align-self:flex-end;background:var(--bubble);color:var(--text)}
.msg.user .label{color:var(--dim)}
.msg.assistant{align-self:flex-start;background:var(--panel);border:1px solid var(--border)}
.msg.assistant.errmsg{border-color:rgba(248,81,73,.55);background:rgba(248,81,73,.08);color:var(--err)}
.msg .body>*{margin:1em 0}
.msg .body>*:first-child{margin-top:0}
.msg .body>*:last-child{margin-bottom:0}
.msg .body h1,.msg .body h2,.msg .body h3,.msg .body h4{font-weight:600;line-height:1.25;margin:1.2em 0 .5em}
.msg .body h1{font-size:1.3em} .msg .body h2{font-size:1.15em} .msg .body h3{font-size:1em} .msg .body h4{font-size:.95em}
.msg .body ul,.msg .body ol{padding-left:1.4em}
.msg .body li{margin:.15em 0}
.msg .body li>p{margin:0}
.msg.assistant .body code{background:var(--code-bg);padding:1px 5px;border-radius:3px;font-size:.9em;overflow-wrap:anywhere;word-break:break-word}
.msg.assistant .body pre,.msg.reasoning .body pre{position:relative;background:var(--code-bg);border:1px solid var(--border);border-radius:6px;padding:10px;overflow-x:auto;max-width:100%;margin:.5em 0}
.copybtn{position:absolute;bottom:6px;right:6px;font-size:11px;padding:3px 8px;margin:0;background:var(--panel);border:1px solid var(--border);color:var(--dim);border-radius:5px;opacity:.5;transition:opacity .15s,color .15s}
.msg.assistant .body pre:hover .copybtn,.msg.reasoning .body pre:hover .copybtn{opacity:1}
.copybtn:hover{border-color:var(--accent);color:var(--accent)}
.copybtn.done{color:var(--ok);border-color:var(--ok);opacity:1}
.msg.assistant .body pre code,.msg.reasoning .body pre code{background:transparent;padding:0;font-size:.85em;line-height:1.4}
.msg.assistant .body blockquote{border-left:3px solid var(--border);padding:0 0 0 10px;margin:.4em 0;color:var(--dim)}
.msg.assistant .body a{color:var(--accent);text-decoration:none}
.msg.assistant .body a:hover{text-decoration:underline}
.msg.assistant .body table{border-collapse:collapse;margin:.5em 0}
.msg.assistant .body th,.msg.assistant .body td{border:1px solid var(--border);padding:4px 8px}
.msg.assistant .body hr{border:none;border-top:1px solid var(--border);margin:.6em 0}
#scrollbtn{position:absolute;bottom:80px;right:20px;background:var(--panel);border:1px solid var(--border);color:var(--accent);width:36px;height:36px;border-radius:50%;cursor:pointer;display:none;font-size:16px;z-index:5}
#scrollbtn.show{display:block}
.msg.reasoning{align-self:flex-start;background:transparent;border:1px dashed var(--border);color:var(--mag);font-size:12px;max-width:90%;overflow-wrap:anywhere;word-break:break-word}
.msg.tool{align-self:flex-start;background:transparent;border:1px dashed var(--border);max-width:90%;white-space:normal;padding:8px 12px;font-size:12px;overflow-wrap:anywhere;word-break:break-word}
.msg.tool .tool-head{font-size:12px;color:var(--accent);font-weight:600;margin-bottom:6px}
/* ===== Diff des écritures (outil edit / mémoire) ==================
Lignes ajoutées en vert, retirées en rouge, contexte neutre. Les couleurs
viennent de variables : chaque thème peut les ajuster (clair vs sombre). */
.msg.tool pre.diff{display:block;padding:0;overflow-x:auto;background:var(--code-bg)}
.msg.tool pre.diff .dl{display:block;padding:1px 10px;white-space:pre-wrap;word-break:break-word;color:var(--dim)}
.msg.tool pre.diff .dl.add{color:var(--add);background:var(--add-bg)}
.msg.tool pre.diff .dl.del{color:var(--del);background:var(--del-bg)}
/* Écriture en direct : le bloc se remplit ligne à ligne pendant que le modèle
tape. Seule la dernière ligne est en fondu (.fresh) — animer tout le bloc à
chaque nouvelle ligne le ferait clignoter. Hauteur bornée + scroll auto en
bas (voir 08-chat-render.js) pour qu'un long script ne pousse pas le fil. */
.msg.tool pre.diff.live .dl.fresh{animation:difffresh .18s ease-out}
@keyframes difffresh{from{opacity:0;transform:translateY(2px)}to{opacity:1;transform:none}}
@media (prefers-reduced-motion:reduce){.msg.tool pre.diff.live .dl.fresh{animation:none}}
.diff-count{margin-left:8px;letter-spacing:0;text-transform:none}
.diff-count .a{color:var(--add)}
.diff-count .d{color:var(--del)}
.msg.tool .tool-sub{font-size:10px;color:var(--dim);text-transform:uppercase;letter-spacing:.1em;margin:8px 0 4px}
.msg.tool pre{position:relative;background:var(--code-bg);border:1px solid var(--border);border-radius:6px;padding:8px 10px;overflow:auto;margin:0;max-height:280px}
.msg.tool pre code{background:transparent;padding:0;font-size:.82em;line-height:1.4;white-space:pre}
.msg.tool .tool-caret{color:var(--accent);animation:toolblink 1s step-end infinite}
.msg.tool .tool-wait{font-size:12px;color:var(--dim);margin-top:8px;animation:toolblink 1.2s ease-in-out infinite}
@keyframes toolblink{0%,100%{opacity:.45}50%{opacity:1}}
/* Indicateur « le modèle réfléchit » : 3 points animés avant le 1er token. */
.msg.typing{display:inline-flex;gap:5px;align-items:center;padding:14px 16px}
.msg.typing span{width:7px;height:7px;border-radius:50%;background:var(--dim);animation:typingdot 1.2s infinite ease-in-out}
.msg.typing span:nth-child(2){animation-delay:.16s}
.msg.typing span:nth-child(3){animation-delay:.32s}
@keyframes typingdot{0%,70%,100%{opacity:.25;transform:translateY(0)}35%{opacity:1;transform:translateY(-4px)}}
/* Compactage du contexte : le MÊME indicateur, juste étiqueté. Avant, une
mini-bannière au-dessus de la saisie doublonnait les 3 points animés — deux
éléments pour un seul état d'attente. Ici les points passent en accent et un
libellé apparaît à côté : un seul objet à l'écran, au bon endroit. */
.msg.typing.compacting span:not(.tlabel){background:var(--accent)}
.msg.typing .tlabel{width:auto;height:auto;border-radius:0;background:none;animation:tlabelfade 1.6s ease-in-out infinite;font-size:12px;color:var(--accent);margin-left:3px;white-space:nowrap}
@keyframes tlabelfade{0%,100%{opacity:.6}50%{opacity:1}}
/* Trace laissée DANS le fil quand le contexte a été compacté : un séparateur
discret, à sa place chronologique — un toast qui disparaît ne dit pas OÙ la
coupure a eu lieu. */
.compact-mark{display:flex;align-items:center;gap:10px;margin:14px 2px;font-size:11px;color:var(--dim);letter-spacing:.04em;animation:cmarkin .35s ease}
.compact-mark::before,.compact-mark::after{content:"";flex:1;height:1px;background:var(--border)}
@keyframes cmarkin{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:none}}
/* --- Message en attente d'accusé serveur ---------------------------------
Affiché DÈS l'appui sur envoyer, en gris et légèrement effacé : le message
ne disparaît plus le temps de l'aller-retour. La classe est retirée quand le
flux renvoie l'événement `user`, et la bulle reprend sa couleur pleine —
signal net que c'est bien parti. */
.msg.user.pending{opacity:.45;transition:opacity .2s ease}
.msg.user.pending .label{font-style:italic}
/* --- Voile de chargement du fil ------------------------------------------
Couvre la zone du chat tant que le replay n'est pas fini. Sans lui, une
connexion lente donne un chat vide indiscernable d'une conversation vide. */
#chat-loading{position:absolute;inset:0;z-index:6;display:none;flex-direction:column;
align-items:center;justify-content:center;gap:12px;background:var(--bg);
color:var(--dim);font-size:12px;letter-spacing:.04em;pointer-events:none}
#chat-loading.show{display:flex}
/* Accueil du fil vide. Posé en surimpression du chat (et non DANS le fil) pour ne
pas perturber le défilement ni le rendu du replay ; pointer-events:none pour
qu'il ne vole jamais un clic à la zone de saisie. Il vit sous le voile de
chargement (z-index plus bas) : jamais les deux visibles en même temps. */
#chat-empty{position:absolute;inset:0 0 150px;z-index:3;display:none;flex-direction:column;
align-items:center;justify-content:center;gap:10px;pointer-events:none;
opacity:0;transition:opacity .35s ease}
#chat-empty.show{display:flex;opacity:1}
/* Le clone perd les règles d'id de #brand (les id sont retirés), dont l'espace
entre le « A » et le mot : on le repose ici, à l'échelle du logo agrandi
(2 px pour 10 px de haut → 4 px pour 20 px), sinon le A s'encastre dans le J. */
#ce-logo{display:inline-flex;align-items:center;gap:4px;color:var(--text)}
#ce-logo svg{height:20px;width:auto;display:block;shape-rendering:crispEdges;fill:currentColor}
#ce-logo .ce-word{display:inline-flex;color:var(--accent)}
#ce-title{font-size:14px;color:var(--dim);letter-spacing:.03em}
.cl-dots{display:inline-flex;gap:5px;align-items:center}
.cl-dots span{width:7px;height:7px;border-radius:50%;background:var(--dim);animation:typingdot 1.2s infinite ease-in-out}
.cl-dots span:nth-child(2){animation-delay:.16s}
.cl-dots span:nth-child(3){animation-delay:.32s}
.msg .label{font-size:10px;color:var(--dim);text-transform:uppercase;letter-spacing:.1em;margin-bottom:4px;display:block}
/* Bulles repliables (reasoning / tool) : une fois la réponse arrivée, on les
replie en douceur pour désencombrer le fil, tout en gardant le label (et son
compteur de tokens) cliquable pour redéplier. */
.msg.collapsible .label{cursor:pointer;user-select:none;margin-bottom:0}
/* ===== Barre latérale ==============================================
Colonne de réglages dense : 12 sections repliées doivent tenir sans faire
défiler. Un seul gabarit, repris des listes de la modale de preset.
Hauteurs : en-tête de section 34 px, ligne de réglage 34 px. */
.side{padding:10px 12px}
/* ⚠️ Le raccourci `padding` ci-dessus écrase les esquives d'encoche posées plus
haut (règles tiroir). Quand la barre est un TIROIR (téléphone, ou mode
« barre en tiroir » sur ordinateur), elle est en position:fixed : le padding
du <body> ne la protège pas, il faut donc reposer les env() ici — sinon
« Loki » et la pastille d'état passent sous l'encoche en PWA iOS. */
@media (max-width:720px){
.side{padding:calc(10px + env(safe-area-inset-top)) 12px calc(10px + env(safe-area-inset-bottom)) calc(12px + env(safe-area-inset-left))}
}
.sidehead{margin-bottom:10px}
/* --- En-tête de section --- */
details{margin:0;border-top:1px solid var(--border)}
details:first-of-type{border-top:0}
details[open]{padding-bottom:10px}
details>summary{display:flex;align-items:center;gap:8px;height:34px;font-size:13px;font-weight:500;
color:var(--text);cursor:pointer;list-style:none;user-select:none}
.stitle{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* Tous les titres du menu (sections ET étiquettes de groupe) s'écrivent de la
MÊME façon : monospace, capitales, même interlettrage. La casse vient du CSS,
pas du HTML, pour qu'aucun libellé ne puisse diverger. Seule la taille et la
couleur distinguent les deux niveaux. */
.stitle,.subhead,.io-cap,.pages-bar .pages-title,.skillshead>span{
font-family:var(--mono);text-transform:uppercase;letter-spacing:.08em}
/* Pastille d'état d'une section (mode agent, accès internet, MCP, accès distant).
Un point + un mot, dans une pilule — l'ancien couple « on / off » en étiquette
carrée se lisait mal et jurait avec le reste du menu. Actif = pilule pleine
(inversée), inactif = contour discret. */
.sbadge{flex:none;display:inline-flex;align-items:center;gap:5px;
font-family:var(--font);font-size:9.5px;font-weight:600;text-transform:uppercase;letter-spacing:.06em;
padding:2px 8px;border-radius:999px;border:1px solid var(--border);color:var(--dim);background:transparent;white-space:nowrap}
.sbadge::before{content:"";width:5px;height:5px;border-radius:50%;background:currentColor;opacity:.55}
.sbadge.on{background:var(--text);border-color:var(--text);color:var(--bg)}
.sbadge.on::before{opacity:1}
.sbadge.warn{color:var(--warn);border-color:color-mix(in srgb,var(--warn) 45%,transparent)}
.sbadge.err{color:var(--err);border-color:color-mix(in srgb,var(--err) 45%,transparent)}
.sbadge:empty{display:none}
/* Dans une étiquette de groupe, la pastille se plaque à DROITE (comme dans les
titres de section, où .stitle occupe la place) au lieu de coller au libellé.
L'ordre visuel reste : libellé · « ? » · … · pastille. */
.subhead .sbadge{margin-left:auto;order:2}
.subhead .help{order:1}
details>summary::-webkit-details-marker{display:none}
details>summary:hover{color:var(--dim)}
/* Chevron : un carré dont on n'affiche que deux côtés. Il pivote AUTOUR DE SON
CENTRE (aucune translation), sinon il se décale en tournant. */
details>summary::after{content:"";flex:none;width:6px;height:6px;
border-right:1.5px solid var(--dim);border-bottom:1.5px solid var(--dim);
transform:rotate(-45deg);transition:transform .18s ease}
details[open]>summary::after{transform:rotate(45deg)}
/* --- Étiquette de groupe : une seule règle pour toutes les variantes --- */
.subhead,.io-cap,.pages-bar .pages-title,.skillshead>span{position:relative;display:flex;align-items:center;
font-size:11px;font-weight:600;color:var(--dim);margin:10px 2px 5px}
.io-cap{margin:0 2px 5px}
/* ⚠️ APRÈS la règle commune ci-dessus, qui pose une marge haute/basse : dans une
LIGNE de carte, elle décalait le libellé vers le bas au lieu de le centrer. */
.pages-bar .pages-title{margin:0}
/* Étiquette de groupe en tête d'un sous-groupe du mode agent : elle suit une
carte, elle a donc besoin de son air normal — mais pas d'un cumul de marges. */
.gated-group>.subhead:first-child{margin-top:12px}
/* Une étiquette de groupe qui suit immédiatement le titre de section n'a pas
besoin de reprendre de l'air : le titre en a déjà donné. */
details>summary+.subhead{margin-top:4px}
.io-group{margin-top:8px}
/* --- Carte de liste --- */
/* overflow VISIBLE : les infobulles (« ? ») sont positionnées en absolu par
rapport à la ligne, un débordement caché les TRONQUERAIT — elles doivent
pouvoir déborder de la carte. Les lignes n'ont pas de fond propre, l'arrondi
n'a donc rien à rogner. */
.slist{background:var(--panel);border:1px solid var(--border);border-radius:9px;
overflow:visible;margin:0 0 8px}
.slist:last-child{margin-bottom:0}
/* Contenu libre (champ + boutons) : le rembourrage passe sur la carte. */
.slist.pad{padding:8px 10px}
.slist.pad>:first-child{margin-top:0}
.slist.pad>:last-child{margin-bottom:0}
/* Contenu libre posé DANS une carte de lignes (champs Crawl4AI, liste MCP,
pages mémoire) : même rembourrage et même filet de séparation qu'une ligne,
pour qu'il ne flotte pas au milieu de la carte. */
.slist>.rowbody{padding:9px 10px}
.slist>.rowbody:not(:first-child),.slist>.pages-bar:not(:first-child){border-top:1px solid var(--border)}
/* …sauf juste après une ligne de réglage : elle porte DÉJÀ son filet bas, les
deux se cumuleraient en un trait double. */
.slist>.switchrow+.rowbody,.slist>.switchrow+.pages-bar{border-top:0}
.slist>.rowbody>:first-child{margin-top:0}
.slist>.rowbody>:last-child{margin-bottom:0}
.slist>.pages-bar{min-height:34px;padding:4px 10px;margin-top:0}
/* Contrôle posé à droite d'une ligne (liste « mode » de la mémoire) : même place
que l'interrupteur, largeur ajustée au contenu. */
.switchrow>.sctl{margin-left:auto;order:2;width:auto;max-width:62%}
/* --- Ligne de réglage : hauteur constante, contrôle à droite --- */
.switchrow{position:relative;display:flex;align-items:center;gap:10px;min-height:34px;padding:6px 0;
font-size:12.5px;cursor:pointer;user-select:none}
.slist>.switchrow{padding:6px 10px}
.slist>.switchrow:not(:last-child){border-bottom:1px solid var(--border)}
.switchrow .switch{margin-left:auto;order:2;flex:none}
.switchrow>span:first-child{min-width:0}
/* --- Bloc de statistique (GPU, RAM) --- */
.stat{padding:9px 10px;display:flex;flex-direction:column;gap:6px}
.stat:not(:last-child){border-bottom:1px solid var(--border)}
/* La RAM vit dans son propre conteneur (masqué quand l'info manque) : le filet
de séparation se pose donc sur le conteneur, pas entre deux .stat frères. */
#ram-details{border-top:1px solid var(--border)}
.stat-h{display:flex;align-items:baseline;justify-content:space-between;gap:10px}
.stat-n{font-size:12.5px;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.stat-v{flex:none;font-size:12.5px;color:var(--text);font-variant-numeric:tabular-nums}
.stat-s{font-size:11px;color:var(--dim)}
/* --- Lignes clé / valeur (config active) --- */
.kv{display:flex;align-items:center;justify-content:space-between;gap:10px;min-height:34px;
padding:6px 10px;border-bottom:1px solid var(--border);font-size:12.5px}
.kv:last-child{border-bottom:0}
.kv span:first-child{color:var(--text);flex-shrink:0}
/* --- Champs et boutons --- */
.sctl,.io-input{width:100%;background:var(--bg);color:var(--text);border:1px solid var(--border);
border-radius:7px;padding:7px 9px;font:inherit;font-size:12.5px}
.sctl:focus,.io-input:focus{outline:none;border-color:var(--dim)}
textarea.sctl{min-height:76px;resize:vertical;line-height:1.5}
.side button:not(.preset-edit):not(.addbtn):not(.preset-add):not(.copybtn):not(.iconbtn){font-size:12px;padding:6px 10px;border-radius:7px}
.side .row{display:flex;gap:6px;align-items:center;margin:6px 0}
.side .row .sctl,.side .row .io-input{flex:1;min-width:0}
/* Bouton d'action à côté d'un champ (copier, ouvrir) : un CARRÉ à la hauteur du
champ, icône au trait centrée. Le glyphe seul (⧉, ↗) dans un bouton texte se
posait de travers et changeait de largeur d'une police à l'autre. */
.iconbtn{flex:none;display:inline-flex;align-items:center;justify-content:center;
width:34px;height:34px;padding:0;margin:0;color:var(--dim);background:var(--panel);
border:1px solid var(--border);border-radius:9px;cursor:pointer}
.iconbtn:hover{color:var(--text);border-color:var(--dim);background:var(--accent-bg)}
.iconbtn svg{width:15px;height:15px;display:block}
.side .btn-danger-o{border-color:var(--err);color:var(--err)}
.side .btn-danger-o:hover{background:var(--danger-bg);border-color:var(--err);color:var(--err)}
.side .muted{font-size:11.5px;line-height:1.5}
/* ===== Réglages d'affichage ======================================
Chaque option est un interrupteur indépendant (Apparence), porté par un
attribut sur <html> et persisté dans localStorage + /api/prefs :
data-hide-reasoning : masque les bulles de raisonnement
data-hide-tools : masque les bulles d'appel d'outil
data-fold-tools : les garde repliées (déplia­bles au clic)
Quand raisonnement et outils sont masqués, l'activité reste signalée par
l'indicateur de frappe (.msg.typing) maintenu pendant tout le tour. */
[data-hide-reasoning="1"] #chat .msg.reasoning{display:none}
[data-hide-tools="1"] #chat .msg.tool{display:none}
/* Hauteur animée en JS (mesure scrollHeight → 0) plutôt que grid 1fr→0fr qui
peut « dépasser ». overflow:hidden clippe le contenu pendant tout le repli.
width revient à auto SANS délai à l'ouverture (pas de saut), et passe à 0
APRÈS le fold (width 0s .3s) à la fermeture pour rétrécir la bulle au label. */
/* height ET width sont animées (en px, pilotées en JS via scrollHeight/scrollWidth)
→ repli/dépli fluide en diagonale. overflow:hidden clippe pendant l'animation. */
.msg.collapsible .bodywrap{overflow:hidden;transition:height .42s cubic-bezier(.4,0,.2,1),width .42s cubic-bezier(.4,0,.2,1),opacity .3s ease,margin-top .42s cubic-bezier(.4,0,.2,1);opacity:1;margin-top:4px}
.msg.collapsible .bodywrap>.body{min-width:0}
.msg.collapsible.collapsed .bodywrap{opacity:0;margin-top:0}
/* Composer = un seul bloc cohérent : barre de contexte intégrée en tête (fine
ligne de progression pleine largeur affleurant le bord) + zone de saisie. */
#composer{border-top:1px solid var(--border);background:var(--panel)}
/* La jauge est un ENFANT de #ctxmeta (et non plus un frère au-dessus) : ainsi
elle peut être clippée par la forme arrondie du bloc — indispensable au
thème ChatGPT où elle sert de bord bas de carte. Position/rendu inchangés
pour les autres thèmes : filet collé en haut de la ligne de contexte. */
#ctxtrack{position:absolute;top:0;left:0;right:0;height:3px;background:var(--panel);overflow:hidden}
#ctx-fill{height:100%;width:0%;background:var(--ok,#3a7);transition:width .3s,background .3s}
#ctxmeta{position:relative;display:flex;align-items:center;justify-content:flex-end;gap:10px;padding:8px 12px 0}
#ctx-compact{display:none;margin:0;padding:2px 10px;font-size:10px;background:var(--warn-bg);border-color:var(--warn,#c93)}
#inputbar{padding:8px 12px;display:flex;gap:8px}
/* Astuce clavier sous la saisie (style repris du HTML : un thème doit pouvoir
changer ses marges, ce qu'un style en ligne empêchait). */
#sendhint{padding:0 12px 6px;font-size:10px;text-align:center;opacity:.7}
/* Moteur pas encore prêt : l'aide clavier cède la place à l'état du chargement. */
#sendhint.waiting{opacity:1;color:var(--dim)}
#input{flex:1;background:var(--panel);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:10px;font:inherit;resize:none;min-height:56px;max-height:200px;overflow-y:hidden}
#input:focus{outline:none;border-color:var(--accent)}
.muted{color:var(--dim);font-size:13px}
#toast{position:fixed;bottom:20px;left:50%;transform:translateX(-50%);z-index:50;background:var(--panel);border:1px solid var(--border);padding:8px 16px;border-radius:8px;opacity:0;transition:opacity .2s;pointer-events:none;box-shadow:0 8px 30px rgba(0,0,0,.18)}
#toast.show{opacity:1}
@keyframes spin{from{transform:rotate(0)}to{transform:rotate(360deg)}}
/* iOS Safari auto-zoome au focus sur tout champ < 16px. On force 16px sur les
appareils tactiles uniquement (le desktop garde son rendu, le pinch-zoom
manuel reste possible). */
@media (hover:none) and (pointer:coarse){
#input,#sysprompt,#modal input,#modal textarea,#modal select{font-size:16px}
}
/* Appareil TACTILE en PAYSAGE : on réduit nettement tout pour gagner de la place
(interface « un peu grosse » en horizontal, utile pour enregistrer des démos).
Détection par (hover:none)+(pointer:coarse)+landscape : infaillible sur tout
téléphone couché, sans dépendre d'un seuil de largeur/hauteur en pixels (les
iPhone Pro Max font 932px de large et ~430px de haut, ce qui piégeait les
seuils précédents). Le desktop (souris) n'est jamais touché.
NB : la règle iOS 16px anti-zoom plus haut est surchargée ici volontairement
pour #input — on garde une saisie compacte (le pinch-zoom manuel reste dispo). */
@media (hover:none) and (pointer:coarse) and (orientation:landscape){
body{font-size:11.5px}
/* Tiroir latéral : on décale le TIROIR ENTIER après l'encoche (left = inset)
au lieu de padder à l'intérieur — sinon grosse bordure vide à gauche qui
rétrécit le contenu. Padding interne normal → largeur utile pleine. */
.side{width:300px;max-width:300px;left:env(safe-area-inset-left);padding:calc(14px + env(safe-area-inset-top)) 14px calc(14px + env(safe-area-inset-bottom)) 14px}
#chat{padding:48px 10px 6px;gap:7px}
.msg{padding:6px 10px;border-radius:8px;max-width:78%}
#chat .msg.assistant,#chat .msg.reasoning{max-width:88%}
#inputbar{padding:5px 8px;gap:6px}
/* PAS de font-size < 16px ici : iOS Safari zoome à la sélection du champ. On
garde 16px (hérité de la règle anti-zoom) et on compacte via hauteur/padding. */
#input{min-height:34px;max-height:120px;padding:6px 9px}
#composer .muted{display:none}
#ctxmeta{padding:3px 10px 0}
h1{font-size:15px}
h2,details>summary{font-size:13px}
#menubtn{width:32px;height:32px;font-size:15px;top:calc(6px + env(safe-area-inset-top))}
#lc-chip{height:32px;font-size:11px;left:calc(46px + env(safe-area-inset-left));top:calc(6px + env(safe-area-inset-top))}
}
/* --- Backend llama.cpp (2 modes : rapide / optimisée) --------------------- */
.lc-head{font-size:12px;line-height:1.5}
/* Les trois modes sont des LIGNES de la carte d'installation (elles étaient des
cartes dans une carte) : pas de cadre propre, un filet entre elles, comme
n'importe quelle ligne de réglage du menu. */
.lc-modes{display:flex;flex-direction:column}
.lc-modes.busy{opacity:.5;pointer-events:none}
.slist>.lc-modes:not(:first-child){border-top:1px solid var(--border)}
.lc-mode{padding:10px;cursor:pointer;transition:background .12s}
.lc-mode:not(:last-child){border-bottom:1px solid var(--border)}
.lc-mode:hover{background:var(--accent-bg)}
.lc-mode-t{font-size:13px;font-weight:600;color:var(--text);display:flex;align-items:center;gap:6px}
/* « conseillé » : pilule PLEINE (texte inversé). En contour, elle héritait du
gris atténué du thème monochrome et devenait illisible en mode sombre. */
.lc-reco{font-size:9px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;background:var(--text);color:var(--bg);border:1px solid var(--text);border-radius:8px;padding:1px 6px}
.lc-mode-d{font-size:11px;color:var(--dim);margin-top:3px}
/* Hauteur réservée : la ligne d'état arrive après l'appel réseau, sans elle
les trois cartes grandissaient d'un cran une fois l'état connu. */
.lc-mode-state{font-size:11px;min-height:16px;margin-top:7px;display:flex;align-items:center;gap:12px;flex-wrap:wrap}
.lc-mode-go{color:var(--accent)}
.lc-mode-active-tag{color:var(--accent);font-weight:600}
.lc-update-link{color:var(--dim);text-decoration:none;cursor:pointer}
.lc-update-link:hover{color:var(--accent)}
/* Choix du moteur dans l'éditeur de modèle (3 options : rapide/optimisé/perso) */
.be-opts{display:flex;flex-direction:column;gap:7px;margin:6px 0 8px}
.be-opt{display:flex;align-items:center;gap:8px;font-size:13px;color:var(--text);cursor:pointer}
.be-opt input{margin:0;flex-shrink:0}
.be-note{font-size:11px;color:var(--dim);margin-left:auto}
/* ===== Éditeur de preset — listes groupées façon Réglages iOS ======== */
#modal .modal-card{max-width:520px;overflow:hidden}
.pe-body{padding:18px 16px 24px;display:flex;flex-direction:column;gap:26px;overflow-y:auto;overflow-x:hidden;flex:1;background:var(--bg)}
/* Nombres : pas de flèches natives (spinner), champ épuré. */
.pe-val[type=number]{-moz-appearance:textfield}
.pe-val::-webkit-outer-spin-button,.pe-val::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}
/* Un groupe = un petit titre + une liste encadrée aux coins arrondis. */
.pe-group{display:flex;flex-direction:column;gap:8px}
/* `display:flex` sur la classe l'emporte sur la règle par défaut du navigateur
pour [hidden] : sans cette ligne, masquer un groupe via l'attribut hidden
n'avait AUCUN effet visible (section « Cartes graphiques » qui restait
affichée avec une seule carte). */
.pe-group[hidden]{display:none}
.pe-gh{font-size:12px;font-weight:600;color:var(--dim);padding:0 6px}
.pe-list{background:var(--panel);border:1px solid var(--border);border-radius:12px;overflow:hidden}
/* Ligne : libellé à gauche, contrôle à droite, séparateur fin entre lignes. */
.pe-row{display:flex;align-items:center;gap:14px;min-height:48px;padding:9px 15px}
/* Séparateur porté par la ligne SUIVANTE (border-top), pas par la précédente
(border-bottom + :not(:last-child)) : `:last-child` compte aussi les lignes
masquées, donc une ligne repliée gardait sa bordure basse, qui doublait
celle de la carte. Avec border-top, la bordure est portée par l'élément
caché lui-même — invisible tant qu'il l'est. */
.pe-row + .pe-row{border-top:1px solid var(--border)}
.pe-row-l{font-size:14px;color:var(--text);flex:1;min-width:0;line-height:1.3}
.pe-row-l .pe-sub{display:block;font-size:11px;color:var(--dim);margin-top:2px}
.pe-row-c{flex:none;display:flex;align-items:center;gap:6px;max-width:62%;justify-content:flex-end}
/* Valeur éditable sans bordure, alignée à droite (nombre / texte). */
.pe-val{border:none;background:transparent;color:var(--accent);font:inherit;text-align:right;padding:4px 0;width:110px;max-width:44vw}
.pe-val::placeholder{color:var(--dim)}
.pe-val:focus{outline:none}
.pe-unit{font-size:12px;color:var(--dim)}
/* Select sans bordure, avec chevron discret. */
.pe-selc{position:relative;display:flex;align-items:center;max-width:100%}
.pe-selc select{appearance:none;-webkit-appearance:none;border:none;background:transparent;color:var(--accent);font:inherit;text-align:right;text-align-last:right;padding:4px 16px 4px 0;cursor:pointer;max-width:100%;text-overflow:ellipsis}
.pe-selc select:focus{outline:none}
.pe-selc::after{content:"›";position:absolute;right:0;transform:rotate(90deg);color:var(--dim);font-size:14px;pointer-events:none}
/* Variante pleine largeur : la liste native s'ouvre alignée à gauche de la
ligne (et non collée au bord droit) et le nom de fichier a la place de
s'afficher en entier. */
.pe-selc.wide{width:100%}
.pe-selc.wide select{flex:1;text-align:left;text-align-last:left;padding-right:18px}
.pe-selc.wide::after{right:2px}
/* Ligne « champ pleine largeur » (Nom, chemin, config brute…). */
.pe-row.stack{flex-direction:column;align-items:stretch;gap:8px}
/* Répartition entre cartes graphiques : UNE barre, sur laquelle on glisse
directement. Le <input type=range> est posé par-dessus, piste transparente :
sinon on voyait deux barres superposées (la barre de proportion + la piste
blanche native du navigateur). Deux teintes du MÊME accent, pas deux
couleurs — la barre exprime une proportion, pas deux catégories. */
.gpu-split{position:relative;display:flex;align-items:center;height:22px}
.gpu-bar{display:flex;width:100%;height:10px;border-radius:999px;overflow:hidden;background:color-mix(in srgb,var(--dim) 20%,transparent)}
.gpu-bar>span{background:var(--accent);flex:none;min-width:0;transition:width .12s}
.gpu-bar>span.alt{background:color-mix(in srgb,var(--accent) 42%,transparent)}
.gpu-range{position:absolute;inset:0;width:100%;height:22px;margin:0;padding:0;background:transparent;-webkit-appearance:none;appearance:none;cursor:ew-resize}
.gpu-range:focus{outline:none}
.gpu-range::-webkit-slider-runnable-track{height:22px;background:transparent;border:none}
.gpu-range::-moz-range-track{height:22px;background:transparent;border:none}
.gpu-range::-moz-range-progress{background:transparent}
.gpu-range::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:16px;height:16px;margin-top:3px;border-radius:50%;background:var(--bg);border:2px solid var(--accent);box-shadow:0 1px 3px rgba(0,0,0,.3);cursor:ew-resize}
.gpu-range::-moz-range-thumb{width:16px;height:16px;border-radius:50%;background:var(--bg);border:2px solid var(--accent);box-shadow:0 1px 3px rgba(0,0,0,.3);cursor:ew-resize}
.gpu-range:focus-visible::-webkit-slider-thumb{box-shadow:0 0 0 3px var(--accent-bg)}
.gpu-range:focus-visible::-moz-range-thumb{box-shadow:0 0 0 3px var(--accent-bg)}
.gpu-legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:11.5px;color:var(--dim)}
.gpu-legend i{display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:5px}
.gpu-legend i.alt{background:color-mix(in srgb,var(--accent) 42%,transparent)}
.gpu-shares{display:flex;flex-wrap:wrap;gap:10px}
.gpu-share{display:flex;align-items:center;gap:5px;font-size:12px;color:var(--dim)}
.gpu-share input{width:62px;background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:7px;padding:5px 7px;font:inherit;text-align:right}
.gpu-share input:focus{outline:none;border-color:var(--accent)}
.pe-inp{width:100%;box-sizing:border-box;background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:9px 11px;font:inherit;transition:border-color .12s,box-shadow .12s}
.pe-inp:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-bg)}
.pe-row.stack.plain .pe-inp{border:none;background:transparent;padding:6px 0;font-size:15px}
.pe-row.stack.plain .pe-inp:focus{box-shadow:none}
/* Champ + bouton accolé (téléchargement) — même trait que les autres champs,
sans pastille colorée qui ressort. */
.pe-inpbtn{display:flex;align-items:stretch}
.pe-inpbtn .pe-inp{border-top-right-radius:0;border-bottom-right-radius:0;border-right:0}
/* Le bouton reprend EXACTEMENT le cadre du champ (mêmes coins, même bordure) :
sans ça, un thème qui arrondit tous les boutons (ChatGPT) le détachait du
champ, avec un coin rond au milieu de la paire. */
.pe-inpbtn button{flex:none;margin:0;border:1px solid var(--border);border-radius:0 8px 8px 0;background:var(--panel);color:var(--dim);padding:0 15px;cursor:pointer;font-size:15px;transition:color .12s,border-color .12s,background .12s}
.pe-inpbtn button:hover:not(:disabled){color:var(--bg);background:var(--accent);border-color:var(--accent)}
.pe-inpbtn:focus-within .pe-inp,.pe-inpbtn:focus-within button{border-color:var(--accent)}
.pe-inpbtn button:disabled{opacity:.5;cursor:default}
/* Contrôle segmenté (moteur) façon iOS. */
.pe-seg{display:flex;gap:3px;background:color-mix(in srgb,var(--dim) 16%,transparent);border-radius:9px;padding:3px;width:100%}
.pe-seg>label{flex:1;text-align:center;font-size:13px;font-weight:500;padding:7px 4px;border-radius:7px;cursor:pointer;color:var(--dim);transition:background .12s,color .12s}
.pe-seg>label:has(input:checked){background:var(--panel);color:var(--text);box-shadow:0 1px 2px rgba(0,0,0,.22)}
.pe-seg input{display:none}
.pe-seg .be-note{display:none}
.pe-seg-note{font-size:11px;color:var(--dim);padding:0 2px;min-height:14px}
.pe-note{font-size:11px;color:var(--dim);line-height:1.45}
/* Barre de progression (téléchargement de modèle). Sans total connu, elle
défile en boucle via .indet. */
.pe-bar{height:4px;border-radius:999px;background:color-mix(in srgb,var(--dim) 26%,transparent);overflow:hidden}
.pe-bar>i{display:block;height:100%;width:0;border-radius:999px;background:var(--accent);transition:width .35s linear}
.pe-bar.indet>i{width:35%;animation:pe-bar-slide 1.2s ease-in-out infinite;transition:none}
.pe-bar.done>i{width:100%}
@keyframes pe-bar-slide{0%{margin-left:-35%}100%{margin-left:100%}}
/* Action discrète en texte (annuler un téléchargement…). */
.pe-link{align-self:flex-start;border:none;background:none;padding:0;font:inherit;font-size:11px;color:var(--dim);text-decoration:underline;cursor:pointer}
.pe-link:hover{color:var(--text)}
.pe-link:disabled{opacity:.5;cursor:default;text-decoration:none}
/* Interrupteur à glissière. */
.pe-switch{position:relative;flex:none;width:40px;height:24px}
.pe-switch input{position:absolute;opacity:0;width:0;height:0}
.pe-sl{position:absolute;inset:0;background:color-mix(in srgb,var(--dim) 55%,transparent);border-radius:999px;transition:background .18s}
.pe-sl::before{content:"";position:absolute;width:18px;height:18px;left:3px;top:3px;background:#fff;border-radius:50%;transition:transform .18s;box-shadow:0 1px 3px rgba(0,0,0,.25)}
.pe-switch input:checked+.pe-sl{background:var(--accent)}
.pe-switch input:checked+.pe-sl::before{transform:translateX(16px);background:var(--bg)}
.pe-switch input:focus-visible+.pe-sl{box-shadow:0 0 0 3px var(--accent-bg)}
/* Repli « avancé » / config brute : une ligne cliquable dans un groupe. */
.pe-adv{background:var(--panel);border:1px solid var(--border);border-radius:12px;overflow:hidden}
.pe-adv>summary{cursor:pointer;font-size:14px;color:var(--text);padding:13px 15px;list-style:none;display:flex;align-items:center;user-select:none}
.pe-adv>summary::-webkit-details-marker{display:none}
.pe-adv>summary::after{content:"";margin-left:auto;width:7px;height:7px;border-right:1.5px solid var(--dim);border-bottom:1.5px solid var(--dim);transform:translateY(-2px) rotate(45deg);transition:transform .15s}
.pe-adv[open]>summary::after{transform:translateY(1px) rotate(-135deg)}
.pe-adv[open]>summary{border-bottom:1px solid var(--border)}
.pe-adv .pe-adv-body{padding:14px 15px;display:flex;flex-direction:column;gap:10px}
.pe-adv textarea.pe-inp{min-height:210px;resize:vertical;line-height:1.5;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;white-space:pre-wrap;overflow-wrap:anywhere;overflow-x:hidden}
/* Ligne « éditer le .env » : chevron de disclosure + textarea de config brute. */
.pe-caret{display:inline-block;width:7px;height:7px;border-right:1.5px solid var(--dim);border-bottom:1.5px solid var(--dim);transform:rotate(-45deg);transition:transform .15s}
.pe-caret.open{transform:rotate(45deg)}
#m-content{min-height:220px;resize:vertical;line-height:1.5;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;white-space:pre-wrap;overflow-wrap:anywhere;overflow-x:hidden}
/* Nombres avancés : chaque paire label/valeur sur sa propre ligne (réutilise pe-row). */
.pe-adv .pe-list{margin:0}
/* Variante « champ en clair » : page mémoire → le contenu est le champ principal. */
.pe-adv.pe-raw-plain{border:none;background:transparent}
.pe-adv.pe-raw-plain>summary{display:none}
.pe-adv.pe-raw-plain .pe-adv-body{padding:0}
/* Pied de modale. */
.pe-foot{padding:9px 12px;border-top:1px solid var(--border);display:flex;justify-content:space-between;
align-items:center;gap:8px;flex-wrap:nowrap}
.pe-foot .foot-l{display:flex;align-items:center;gap:8px;min-width:0}
.pe-foot .foot-r{display:flex;gap:8px;flex:none}
.pe-btn{margin:0;padding:7px 15px;font-size:12.5px;border-radius:8px;font-weight:600;cursor:pointer;
border:1px solid var(--border);background:var(--panel);color:var(--text);white-space:nowrap}
.pe-btn:hover{border-color:var(--dim)}
.pe-btn.primary{border-color:var(--accent);background:var(--accent);color:var(--bg)}
.pe-btn.primary:hover{filter:brightness(1.08)}
/* Suppression : icône seule, en retrait. Un bouton plein « supprimer » pesait
autant que « enregistrer » alors que c'est l'action rare — et à trois boutons
larges, le pied passait à deux lignes sur téléphone. */
.pe-del{flex:none;display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;
margin:0;padding:0;border:1px solid transparent;border-radius:8px;background:transparent;color:var(--err);cursor:pointer}
.pe-del svg{width:15px;height:15px;display:block}
.pe-del:hover{background:var(--danger-bg);border-color:color-mix(in srgb,var(--err) 40%,transparent)}
/* Option d'une confirmation : une PASTILLE à cocher, pas une case système suivie
d'un bout de texte. Éteinte : contour discret. Allumée : elle passe au rouge,
comme l'action destructrice qu'elle étend. Vit dans la boîte de confirmation
(#ask-check), plus dans le pied de l'éditeur. */
.pe-delmodel{align-self:flex-start;margin-top:14px;display:inline-flex;align-items:center;gap:6px;font-size:11px;font-weight:600;
letter-spacing:.02em;color:var(--dim);cursor:pointer;white-space:nowrap;
padding:5px 11px;border:1px solid var(--border);border-radius:999px;background:var(--panel);
transition:color .12s,border-color .12s,background .12s}
.pe-delmodel input{position:absolute;opacity:0;width:0;height:0}
/* Le point tient lieu de case à cocher : creux au repos, plein une fois coché. */
.pe-delmodel::before{content:"";width:6px;height:6px;border-radius:50%;flex:none;
border:1px solid currentColor;background:transparent}
.pe-delmodel:hover{color:var(--text);border-color:var(--dim)}
.pe-delmodel:has(input:checked){color:var(--err);border-color:color-mix(in srgb,var(--err) 45%,transparent);background:var(--danger-bg)}
.pe-delmodel:has(input:checked)::before{background:currentColor}
/* Éditeur de preset/mémoire — mise en forme propre du modale */
.modal-ov{position:fixed;inset:0;background:rgba(0,0,0,.45);z-index:30;align-items:center;justify-content:center;padding:20px}
/* Ouverture/fermeture animées. L'état par défaut est l'état FERMÉ ; la classe
.show (posée par showModal/hideModal) fait jouer la transition dans les deux
sens — une @keyframes n'aurait animé que l'ouverture. */
.modal-ov,.ask-overlay{opacity:0;transition:opacity .16s ease}
.modal-ov.show,.ask-overlay.show{opacity:1}
.modal-card,.ask-box{transform:translateY(10px) scale(.985);opacity:0;
transition:transform .18s cubic-bezier(.2,.8,.3,1),opacity .18s ease}
.modal-ov.show .modal-card,.ask-overlay.show .ask-box{transform:none;opacity:1}
@media (prefers-reduced-motion:reduce){
.modal-ov,.ask-overlay,.modal-card,.ask-box{transition:none}
}
.modal-card{position:relative;background:var(--bg);border:1px solid var(--border);border-radius:12px;width:100%;max-width:600px;max-height:90vh;display:flex;flex-direction:column;box-shadow:0 16px 50px rgba(0,0,0,.18)}
.modal-head{padding:14px 16px;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:center}
.modal-head strong{font-size:14px}
/* Croix de fermeture : un CARRÉ, pas un rectangle. Avec un padding asymétrique
(3px vertical / 10px horizontal) le glyphe paraissait écrasé et la cible de
clic était basse. 32px, glyphe centré, taille de cible correcte au doigt. */
.modal-x{margin:0;padding:0;width:32px;height:32px;flex:none;display:inline-flex;align-items:center;justify-content:center;font-size:19px;line-height:1;color:var(--dim)}
.modal-body{padding:16px;overflow:auto;flex:1}
/* Modale ouverte mais pas encore remplie : on affiche le voile de chargement À LA
PLACE du formulaire et du pied. Estomper le formulaire ne suffisait pas — on
voyait quand même les champs s'écrire un par un derrière le voile. */
/* Voile OPAQUE, posé sous l'en-tête (le titre reste lisible) : `visibility:hidden`
sur le formulaire suffirait en théorie, mais un fond plein garantit qu'aucun
reste du preset précédent (moteur coché, réglages) ne puisse apparaître une
fraction de seconde. --mh = hauteur mesurée de l'en-tête, posée à l'ouverture. */
/* ⚠️ Aucune transition tant que le formulaire est caché, NI pendant la frame où
il réapparaît : WebKit diffère les transitions des éléments non rendus et les
joue à leur retour à l'écran. Le sélecteur de moteur glissait donc de l'ancien
preset au nouveau SOUS LES YEUX, une fois le chargement terminé. */
.modal-ov.loading .pe-body *,.modal-ov.no-anim .pe-body *{transition:none!important}
.modal-load{display:none;position:absolute;inset:var(--mh,52px) 0 0 0;z-index:2;
align-items:center;justify-content:center;background:var(--bg);border-radius:0 0 12px 12px}
.modal-ov.loading .modal-load{display:flex}
/* visibility (et non display) : le formulaire garde sa place, donc la carte a
déjà sa taille finale — la révéler ne provoque plus d'à-coup de hauteur. */
.modal-ov.loading .modal-body,
.modal-ov.loading .pe-body,
.modal-ov.loading .pe-foot,
.modal-ov.loading .modal-foot{visibility:hidden}
/* Rond qui tourne — les trois points sont la signature du CHAT, pas des modales. */
.spinner{width:22px;height:22px;border-radius:50%;border:2px solid var(--border);
border-top-color:var(--dim);animation:spin .7s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
@media (prefers-reduced-motion:reduce){.spinner{animation-duration:2s}}
.modal-foot{padding:12px 16px;border-top:1px solid var(--border);display:flex;justify-content:space-between;align-items:center;gap:8px;flex-wrap:wrap}
.foot-l{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
.foot-r{display:flex;gap:8px}
.mform{display:flex;flex-direction:column;gap:14px}
.fld{display:flex;flex-direction:column;gap:5px}
.fld-l{font-size:11px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;color:var(--dim)}
.fld-l .opt{text-transform:none;letter-spacing:0;font-weight:400;opacity:.65}
.fld-hint{font-size:11px;color:var(--dim);margin-top:2px}
.ctl{width:100%;box-sizing:border-box;background:var(--panel);color:var(--text);border:1px solid var(--border);border-radius:7px;padding:9px 11px;font:inherit}
.ctl:focus{outline:none;border-color:var(--accent)}
.ta{min-height:220px;resize:vertical;line-height:1.5}
.card{border:1px solid var(--border);border-radius:10px;padding:13px;display:flex;flex-direction:column;gap:13px}
.card-h{font-size:11px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--accent)}
.row{display:flex;gap:6px}
.row .ctl{flex:1;min-width:0}
.btn-dl{flex-shrink:0;margin:0}
.input-btn{display:flex}
.input-btn .ctl{flex:1;min-width:0;border-top-right-radius:0;border-bottom-right-radius:0}
.input-btn .btn-in{flex-shrink:0;margin:0;padding:9px 13px;border-left:0;border-top-left-radius:0;border-bottom-left-radius:0}
.input-btn .btn-in:hover{border-left:0}
.seg{display:flex;gap:6px}
.seg>label{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;text-align:center;min-height:46px;padding:6px 4px;border:1px solid var(--border);border-radius:8px;cursor:pointer;font-size:12px;background:var(--panel);transition:border-color .12s,background .12s,color .12s}
.seg>label:hover{border-color:var(--accent)}
.seg>label:has(input:checked){border-color:var(--accent);background:var(--accent-bg);color:var(--accent)}
.seg input{display:none}
.seg .be-note{font-size:10px;color:var(--dim);margin:0}
.btn-danger{background:var(--danger-bg);border-color:var(--err);color:var(--err);margin:0}
.btn-save{border-color:var(--accent);color:var(--accent);margin:0}
.del-model{align-items:center;gap:5px;font-size:11px;color:var(--dim)}
.del-model input{margin:0}
.lc-logbar{display:flex;align-items:center;gap:6px;cursor:pointer;user-select:none;font-size:12px;color:var(--dim);font-weight:600;padding:6px 0;margin-top:2px}
.lc-logbar #lc-log-caret{font-size:9px;transition:transform .12s;display:inline-block}
.lc-logbar.open #lc-log-caret{transform:rotate(90deg)}
#lc-log{background:var(--panel);border:1px solid var(--border);border-radius:6px;padding:8px;font-size:10px;line-height:1.45;max-height:220px;overflow:auto;white-space:pre-wrap;word-break:break-all;margin:0}
.lc-spin{display:inline-block;animation:spin 1s linear infinite}
/* Serveurs MCP */
.mcp-list{display:flex;flex-direction:column;gap:4px}
.mcp-row{display:flex;align-items:center;gap:8px;padding:6px 10px;border:1px solid var(--border);border-radius:6px;background:var(--panel);cursor:pointer}
.mcp-row:hover{border-color:var(--accent)}
.mcp-row.off{opacity:.6}
.mcp-dot{flex:none;width:8px;height:8px;border-radius:50%}
.mcp-dot-ok{background:var(--accent)}
.mcp-dot-err{background:var(--err)}
.mcp-dot-off{background:var(--dim)}
.mcp-info{flex:1;min-width:0}
.mcp-name{font-size:12px;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.mcp-meta{display:flex;align-items:center;gap:6px;margin-top:2px}
.mcp-tag{font-size:10px;padding:1px 7px;border-radius:9px;border:1px solid var(--border);color:var(--dim);text-transform:uppercase;letter-spacing:.04em}
.mcp-tools{font-size:10px;color:var(--accent)}
.mcp-err{font-size:10px;color:var(--err)}
.mcp-switch{flex:none}
.mcp-edit{flex:none;padding:2px 5px;font-size:13px;line-height:1;background:transparent;border:none;opacity:.5}
.mcp-edit:hover{opacity:1;color:var(--accent)}
/* Liste des outils MCP dans la modale */
.mcp-tools-list{display:flex;flex-direction:column;gap:2px;max-height:240px;overflow:auto;border:1px solid var(--border);border-radius:6px;padding:6px}
.mcp-tool-row{display:flex;align-items:center;gap:10px;padding:4px 6px;border-radius:5px;cursor:pointer}
.mcp-tool-row:hover{background:var(--panel)}
.mcp-tool-switch{flex:none;transform:scale(.85)}
.mcp-tool-name{font-size:12px;color:var(--text);word-break:break-all}
/* Accès OpenAI / Accès distant : champs lecture seule (URL, clé) + libellés
compacts, pour une présentation homogène et sans surcharge visuelle. */
.io-input{flex:1;min-width:0;background:var(--panel);color:var(--text);border:1px solid var(--border);border-radius:9px;padding:8px 10px;font:inherit;font-size:13px}
/* Modale MCP (ajout/édition) : les champs des groupes stdio/http n'avaient aucun
espacement vertical (le conteneur n'est pas un flex à gap) → ils se touchaient.
On aligne leur rythme sur celui du reste du formulaire (.mform). */
#mcp-stdio-fields,#mcp-http-fields{display:flex;flex-direction:column;gap:14px}
/* ===== Mise en page de la conversation =============================
Colonne de lecture centrée, réponses SANS bulle (texte nu sur le fond),
question de l'utilisateur en pastille arrondie à droite, composer en
« pilule », pas d'étiquettes VOUS/Loki. Ces règles surchargent la mise en
page générique plus haut : elles doivent donc rester APRÈS elle. */
body{font-size:15px}
h1{color:var(--text);font-weight:600;letter-spacing:-.01em}
/* Barre latérale : plus sombre/atténuée que la zone de conversation. */
.side{background:var(--panel);border-right-color:transparent}
@media (max-width:720px){.side{background:var(--panel)}}
/* Colonne de lecture centrée (comme le fil ChatGPT), pleine largeur sur mobile.
⚠️ JAMAIS le raccourci `padding` ici : le rembourrage BAS réserve la hauteur
de la carte de saisie (voir #chat plus bas) et il est réglé ailleurs. Une
règle en raccourci le remet silencieusement à sa valeur du raccourci — c'est
exactement ce qui, jusqu'à ce correctif, laissait la fin du fil coincée sous
la carte au-delà de 721 px de large. Longhands uniquement. */
#chat{padding-top:28px;padding-inline:max(16px,calc(50% - 380px));gap:22px;scrollbar-gutter:stable both-edges}
/* Réponses de l'assistant : pas de cadre, pas de fond — juste du texte. */
#chat .msg.assistant{background:transparent;border:none;padding:0;max-width:100%;line-height:1.7}
/* Question : pastille arrondie à droite, texte lisible sur fond clair OU sombre. */
#chat .msg.user{background:var(--bubble);color:var(--text);border-radius:20px;padding:11px 18px;max-width:75%;line-height:1.6}
#chat .msg.user .label{color:var(--dim)}
/* Libellés : l'amont les masquait tous ici, les mots « user » / « assistant »
n'ajoutant rien à l'alignement. Ils portent désormais un avatar et un prénom
(voir 17-identity.js), qui eux identifient vraiment l'interlocuteur — on les
réaffiche donc, en ligne et discrets. Restent masqués les libellés SANS
avatar, c'est-à-dire les bulles techniques non repliables. */
#chat .msg .label{display:none}
#chat .msg.collapsible .label{display:block}
#chat .msg.user .label,#chat .msg.assistant .label{display:flex;align-items:center;gap:0;opacity:.85}
/* Côté question, le libellé s'aligne à droite comme la bulle. */
#chat .msg.user .label{justify-content:flex-end}
/* Mesures de génération sous la réponse (prefill / decode). Assez discrètes pour
ne pas concurrencer le texte, assez lisibles pour être utiles. Escamotables
depuis Apparence → Affichage. */
#chat .msg .statline{margin-top:10px;font-family:var(--mono);font-size:10.5px;letter-spacing:.02em;
color:var(--dim);opacity:.75}
#chat .msg .statline:empty{display:none}
html[data-hide-stats="1"] #chat .msg .statline{display:none}
/* Arrivée en fondu montant plutôt qu'un surgissement sec. L'opacité finale (.75)
est celle de la règle ci-dessus : l'animation doit y ATTERRIR, sinon la ligne
clignote en repassant à sa valeur de repos. */
#chat .msg .statline-in{animation:statin .28s cubic-bezier(.2,.7,.3,1) both}
@keyframes statin{from{opacity:0;transform:translateY(3px)}to{opacity:.75;transform:none}}
@media (prefers-reduced-motion:reduce){#chat .msg .statline-in{animation:none}}
/* Raisonnement / outils : encart gris discret plutôt qu'un cadre pointillé. */
#chat .msg.reasoning,#chat .msg.tool{background:var(--panel);border:1px solid var(--border);border-radius:14px;color:var(--dim);padding:12px 16px}
#chat .msg.reasoning{color:var(--dim)}
#chat .msg.tool .tool-head{color:var(--text)}
#chat .msg.assistant .body pre,#chat .msg.reasoning .body pre,#chat .msg.tool pre{border-radius:12px;padding:12px 14px}
#chat .msg.assistant .body a{text-decoration:underline;text-underline-offset:2px}
/* Composer = UNE seule carte posée PAR-DESSUS le fil : la conversation
continue de défiler DERRIÈRE elle et s'efface en fondu (dégradé vers le fond)
au lieu de s'arrêter net au-dessus. Dans la carte, de haut en bas : la saisie
et ses boutons, un pied discret (préréglage · contexte · compacter), puis la
jauge de contexte qui FORME le bord bas — elle épouse les coins arrondis
(overflow:hidden sur le rayon). Les trois blocs sont des frères dans le HTML :
on les recolle en dessinant les bordures par morceaux et l'ordre visuel vient
de l'ordre flex — aucun changement de markup. */
/* Le padding BAS dégage l'indicateur d'accueil de l'iPhone en PWA : la carte
flotte au-dessus de lui, son arrondi bas reste donc bien visible. Hors PWA
l'inset vaut 0 → strictement les 8 px d'origine. */
/* --fade = la bande transparente au-dessus de la carte, celle où le fil doit
s'effacer. Le dégradé s'exprime dans la MÊME unité (et non en % de la hauteur
totale du composeur, qui varie) : l'opacité est donc pleine exactement au bord
haut de la carte. Avec un %, le fondu finissait SOUS ce bord et le texte
restait lisible au ras de la carte — l'impression de « passer derrière ». */
#composer{position:absolute;left:0;right:0;bottom:0;z-index:4;border-top:none;display:flex;flex-direction:column;--fade:34px;padding:var(--fade) 0 calc(8px + env(safe-area-inset-bottom));--col:calc(max(16px,calc(50% - 380px)) + var(--sbw,0px));background:linear-gradient(to bottom,transparent 0,var(--bg) var(--fade))}
/* Réserve sous le fil = hauteur RÉELLE du composeur, mesurée et publiée par
syncComposer() (08-chat-render.js). Une constante redevient fausse dès que la
saisie grandit ou qu'une pièce jointe s'affiche, et la fin du fil repasse hors
d'atteinte. 170px n'est que le repli du tout premier rendu, avant mesure.
Pas de env(safe-area-inset-bottom) ici : il est déjà DANS le padding bas du
composeur, donc dans sa hauteur mesurée — le compter deux fois creusait un
trou en PWA iOS. */
#chat{padding-bottom:calc(var(--composer-h,170px) + 12px)}
#scrollbtn{bottom:calc(var(--composer-h,170px) + 10px);border-radius:999px}
#inputbar{order:1;margin:0 var(--col);padding:8px 8px 8px 16px;gap:8px;align-items:flex-end;background:var(--panel);border:1px solid var(--border);border-bottom:none;border-radius:26px 26px 0 0}
#input{background:transparent;border:none;border-radius:0;padding:9px 0;min-height:36px;line-height:18px;max-height:160px}
/* Pas de cadre qui s'assombrit au focus : la carte garde exactement la même
bordure, seule la saisie change (curseur). */
#input:focus{border-color:transparent}
/* Pied de carte : c'est LUI qui porte l'arrondi bas (26 px, comme en haut) et
qui clippe la jauge. Un bandeau de 7 px ne peut pas dessiner un rayon de 26 px
(le navigateur l'écrase, d'où les coins bizarres) : on laisse donc la carte
découper la jauge dans sa propre forme — la barre est bien épaisse ET rognée
exactement sur la courbe, symétrique du haut. */
#ctxmeta{order:2;margin:0 var(--col);padding:6px 16px 15px;gap:10px;background:var(--panel);border:1px solid var(--border);border-top:none;border-radius:0 0 26px 26px;overflow:hidden}
/* ⚠️ NE PAS piloter la position du composer en JS quand le clavier s'ouvre
(tenté puis retiré le 2026-07-28, deux fois) : ni asservir la hauteur de la
coque à visualViewport.height, ni décaler le composer de la hauteur du
clavier. Dans les deux cas la carte part en haut de l'écran en PWA iOS.
iOS gère déjà correctement le clavier tout seul — on le laisse faire. */
#ctx-text,#status-preset{color:var(--dim);font-size:10px;letter-spacing:.02em;white-space:nowrap}
#ctx-compact{border-radius:999px;padding:2px 10px;font-size:10px;background:transparent;border-color:var(--border);color:var(--dim)}
#ctx-compact:hover{border-color:var(--dim);color:var(--text)}
/* Jauge = bord bas de la carte : épaisse, plaquée au ras du bord, rognée par
l'arrondi du pied de carte. */
#ctxtrack{top:auto;bottom:0;height:7px;background:var(--border)}
#ctx-fill{background:var(--text);opacity:.75}
#sendhint{order:4;padding:12px var(--col) 2px}
/* ===== Pièces jointes ==============================================
Un fichier joint n'est jamais lu par la page : il est déposé dans le dossier
de travail de l'IA et seul son nom revient. L'affichage se limite donc à une
pastille — pas d'aperçu, pas de vignette, rien qui promette plus que ça.
Le trombone vit dans le PIED de la carte, à droite avec le décompte de contexte
et « compacter ». Il est à LEUR échelle (une ligne d'outils de 10 px), pas à
celle de « send » : joindre un fichier est un geste occasionnel. */
#attach{width:18px;height:18px;min-height:18px;flex:none;margin:0;padding:0;
display:flex;align-items:center;justify-content:center;border-radius:999px;
background:transparent;border:1px solid transparent;color:var(--dim)}
#attach:hover{background:var(--row-bg-on);border-color:transparent;color:var(--text)}
#attach:active{transform:scale(.9)}
#attach svg{display:block}
/* Fichiers en attente : de petites bulles POSÉES AU-DESSUS de la carte.
Le composeur réserve déjà 34 px transparents au-dessus de lui (la bande de
dégradé qui fond vers le fil) : les pastilles s'y logent, et on retire d'autant
le rembourrage. La carte ne bouge donc pas — c'est la bande vide qui se
remplit, au lieu du composeur qui grandit vers le haut.
Elles ne vont PAS dans le pied : cette ligne appartient au nom du modèle. */
#attach-list{order:0;display:none;gap:6px;margin:0 var(--col);padding:0 2px 8px;
overflow-x:auto;scrollbar-width:none}
#attach-list::-webkit-scrollbar{display:none}
#attach-list.show{display:flex}
#composer:has(#attach-list.show){--fade:6px}
/* Posées sur le fil (et non sur la carte) : il leur faut un fond opaque et un
cadre, sinon le texte de la conversation transparaît derrière. */
#attach-list .chip-file{flex:0 0 auto;background:var(--panel);border-color:var(--border);
box-shadow:0 2px 10px rgba(0,0,0,.10)}
/* Pastille de fichier. Le × n'apparaît qu'au survol : au repos la pastille doit
rester lisible d'un coup d'œil (nom + poids), pas encombrée d'un bouton. */
.chip-file{display:flex;align-items:center;gap:8px;max-width:230px;min-width:0;
padding:4px 12px;border-radius:999px;background:var(--row-bg-on);
border:1px solid transparent;font-size:12px;line-height:1.3}
/* Avec un × à droite, la pastille se ferme SUR lui : le rembourrage de droite
revient à l'épaisseur du rond, sinon le bouton flotte loin du bord et la
pastille paraît de travers. */
.chip-file:has(button){padding-right:4px}
.chip-file .cf-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.chip-file .cf-size{flex:none;color:var(--dim);font-size:11px}
/* Le × est TOUJOURS visible : le révéler au survol le rendait inatteignable sur
mobile, où il n'y a pas de survol — donc impossible de retirer un fichier. */
.chip-file button{flex:none;margin:0;padding:0;width:20px;height:20px;font-size:14px;
display:flex;align-items:center;justify-content:center;line-height:1;
border-radius:50%;border:none;background:transparent;color:var(--dim)}
.chip-file button:hover{background:var(--bubble);color:var(--text)}
/* Transfert en cours : un liseré qui tourne, plutôt qu'une pastille grisée dont
on ne sait pas si elle travaille ou si elle a échoué. */
.chip-file.up .cf-size::after{content:"";display:inline-block;vertical-align:-2px;
margin-left:7px;width:10px;height:10px;border-radius:50%;
border:1.5px solid var(--border);border-top-color:var(--text);animation:spin .7s linear infinite}
.chip-file.err{background:var(--danger-bg);color:var(--err)}
.chip-file.err .cf-size{color:var(--err)}
@media (prefers-reduced-motion:reduce){.chip-file.up .cf-size::after{animation:none}}
/* Fichiers d'un message envoyé : leur propre rangée AU-DESSUS de la bulle,
alignée à droite comme elle. Rangée et bulle restent deux objets distincts,
séparés par l'espacement normal du fil — les rapprocher les faisait lire comme
une seule bulle difforme qui déborde vers le haut. */
#chat .msg-files{align-self:flex-end;display:flex;flex-wrap:wrap;justify-content:flex-end;
gap:6px;max-width:75%}
#chat .msg-files .chip-file{background:var(--bubble);max-width:100%}
/* Lien de téléchargement dans une réponse : un lien normal, mais la flèche dit
qu'il ramène un fichier plutôt qu'il n'ouvre une page. */
#chat .msg .filelink{cursor:pointer}
#chat .msg .filelink::after{content:"↓";margin-left:5px;font-size:.9em;opacity:.7;text-decoration:none;display:inline-block}
#chat .msg .filelink.busy{opacity:.55;pointer-events:none}
/* Voile de dépôt. Il couvre la zone de conversation, curseur inclus : c'est la
réponse à « est-ce que je peux lâcher ici ? », elle doit être sans ambiguïté. */
#dropveil{position:absolute;inset:0;z-index:20;display:none;align-items:center;
justify-content:center;padding:24px;background:rgba(0,0,0,.28);pointer-events:none}
#dropveil.show{display:flex}
#dropveil>div{padding:22px 30px;border:2px dashed var(--border);border-radius:20px;
background:var(--panel);color:var(--text);font-size:14px;box-shadow:0 8px 30px rgba(0,0,0,.18)}
/* Reste de l'UI : coins arrondis et contrastes doux, dans le même esprit. */
button,.mem-search{border-radius:10px}
/* Champ + bouton accolés : on ne garde l'arrondi que sur les coins extérieurs. */
.pe-inpbtn .pe-inp{border-radius:10px 0 0 10px}
.pe-inpbtn button{border-radius:0 10px 10px 0}
.input-btn .ctl{border-radius:10px 0 0 10px}
.input-btn .btn-in{border-radius:0 10px 10px 0}
.preset{border-radius:12px}
/* Les presets restent des cartes (fond + bordure) : à plat, sans fond ni
cadre, la liste ne se lisait plus comme des lignes distinctes. L'actif se
signale par sa bordure — un simple voile d'accent (7 % de blanc sur fond
noir) était invisible en mode sombre. */
.preset{background:var(--panel);border-color:var(--border)}
.preset:hover{background:var(--accent-bg);border-color:var(--dim)}
.preset.active{background:var(--panel);border-color:var(--text);color:var(--text)}
.subhead,.lc-mode-go,.lc-mode-active-tag{color:var(--dim)}
button:hover{border-color:var(--dim);color:var(--text)}
/* Encart de garde-fou : c'est le serveur qui parle (limite d'appels d'outils,
boucle détectée), pas le modèle. Pastille discrète plutôt qu'une erreur —
la génération s'est arrêtée proprement, rien n'a échoué. */
.stopnote{display:inline-flex;align-items:center;gap:7px;margin-top:4px;padding:4px 12px;
border:1px solid var(--border);border-radius:999px;background:var(--panel);
color:var(--dim);font-size:12px;line-height:1.5}
.stopnote::before{content:"";flex:none;width:6px;height:6px;border-radius:50%;background:var(--dim)}
/* ===== Fenêtres et sélecteurs =====================================
Coins arrondis et ombre douce plutôt qu'un cadre marqué. */
.modal-card,.ask-box,.modal-box,.pe-list,.pe-adv{border-radius:12px}
.modal-card,.ask-box{box-shadow:0 8px 30px rgba(0,0,0,.12)}
/* Sélecteurs segmentés : noir plein quand c'est coché, pas de couleur. */
.pe-seg>label:has(input:checked),.seg>label:has(input:checked){background:var(--text);border-color:var(--text);color:var(--bg)}
/* Curseur de portée de l'export. Même vocabulaire visuel que la répartition GPU
(piste arrondie + pastille cerclée), mais une seule valeur : la position tout
à droite vaut « toute la conversation ». Le libellé sous la piste donne le
compte RÉEL d'échanges et le poids du fichier, la question qu'on se pose
vraiment avant d'exporter. */
.xr{position:relative;display:flex;align-items:center;height:22px}
.xr-track{width:100%;height:10px;border-radius:999px;overflow:hidden;background:color-mix(in srgb,var(--dim) 20%,transparent)}
.xr-fill{height:100%;background:var(--accent);border-radius:999px;transition:width .12s}
.xr-range{position:absolute;inset:0;width:100%;height:22px;margin:0;padding:0;background:transparent;-webkit-appearance:none;appearance:none;cursor:ew-resize}
.xr-range:focus{outline:none}
.xr-range::-webkit-slider-runnable-track{height:22px;background:transparent;border:none}
.xr-range::-moz-range-track{height:22px;background:transparent;border:none}
.xr-range::-moz-range-progress{background:transparent}
.xr-range::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:16px;height:16px;margin-top:3px;border-radius:50%;background:var(--bg);border:2px solid var(--accent);box-shadow:0 1px 3px rgba(0,0,0,.3);cursor:ew-resize}
.xr-range::-moz-range-thumb{width:16px;height:16px;border-radius:50%;background:var(--bg);border:2px solid var(--accent);box-shadow:0 1px 3px rgba(0,0,0,.3);cursor:ew-resize}
.xr-range:focus-visible::-webkit-slider-thumb{box-shadow:0 0 0 3px var(--accent-bg)}
.xr-range:focus-visible::-moz-range-thumb{box-shadow:0 0 0 3px var(--accent-bg)}
.xr-range:disabled{cursor:default;opacity:.5}
/* Conversation vide : un seul message centré à la place des réglages. */
.xempty{display:flex;flex-direction:column;align-items:center;gap:6px;padding:44px 20px;text-align:center;color:var(--text);font-size:14px}
.xempty-i{font-size:30px;line-height:1;color:var(--dim);opacity:.7}
/* ===== Direction « Terre » : surfaces en cartes =======================
La charte pose des CARTES arrondies séparées par une gouttière, au lieu
d'une colonne à filets. Bloc en fin de feuille pour prendre le pas sur les
règles de structure d'origine sans les réécrire.
Bureau UNIQUEMENT : sur téléphone la barre latérale est un tiroir plein
écran et le fil occupe toute la largeur — y poser des cartes ne ferait que
grignoter des pixels précieux. */
@media (min-width:721px){
/* Coque : le fond « terre » se voit dans la gouttière entre les deux cartes. */
body{padding:12px;gap:12px}
html[data-pwa="1"] body{padding-top:calc(12px + env(safe-area-inset-top))}
/* Barre latérale : pile de cartes, sans filet ni bordure de colonne. */
.side{width:312px;border-right:0;padding:0;background:transparent;
display:flex;flex-direction:column;gap:12px;overflow-y:auto;overscroll-behavior:contain}
.sidecard,.side>details{background:var(--panel);border:0;border-top:0;border-radius:20px;
box-shadow:var(--shadow);margin:0;padding:2px 16px}
.sidecard{padding:15px 18px 14px;display:flex;flex-direction:column;gap:2px}
.side>details[open]{padding-bottom:14px}
.side>details>summary{height:42px}
/* L'étiquette « Paramètres » n'est plus un filet mais un intertitre entre
deux cartes : le fond de la coque fait déjà la séparation. */
.side-sep{border-top:0;margin:10px 0 0;padding:0 6px}
/* Blocs encastrés DANS une carte : un ton plus chaud, sans bordure — deux
cadres imbriqués alourdiraient inutilement. */
.side .slist{background:var(--row-bg);border-color:transparent;border-radius:14px}
/* Colonne principale : une seule grande carte qui contient fil et saisie. */
.main{background:var(--panel);border-radius:20px;box-shadow:var(--shadow);overflow:hidden}
#chat{padding-top:26px;padding-inline:max(20px,calc(50% - 360px))}
/* Le dégradé qui fait disparaître le fil sous la carte de saisie doit fondre
vers le PANNEAU, pas vers le fond de la coque. */
#composer{background:linear-gradient(to bottom,transparent 0,var(--panel) var(--fade))}
/* Carte de saisie : posée SUR le panneau, donc un ton au-dessus, et sans
bordure — c'est l'élévation qui la détache. */
#inputbar,#ctxmeta{background:var(--row-bg-on);border-color:transparent}
#input{background:transparent}
}
/* --- Terre : densité et accents ------------------------------------- */
@media (min-width:721px){
/* Gouttière resserrée : la charte empile beaucoup de cartes, 12 px entre
chacune repoussait « Actions » loin sous la ligne de flottaison. */
.side{gap:8px}
.side>details>summary{height:38px}
/* Discussion active : un cadre sauge COMPLET, mais fin (1 px) — le liseré de
3 px sur le seul bord gauche se lisait mal et déséquilibrait la ligne. La
bordure transparente est portée par tous les états : la sélection ne décale
donc pas le contenu d'un pixel. */
.side .preset{border:1px solid transparent;border-radius:14px;background:transparent}
.side .preset.active{background:var(--row-bg-on);border-color:var(--accent)}
.side .preset:hover{background:var(--row-bg)}
/* Bulle de question : coin bas-droit rentré, comme une bulle qui pointe vers
son auteur. */
#chat .msg.user{border-radius:20px 20px 6px 20px}
}
/* Action principale en sauge : c'est la couleur d'action de la charte, et elle
se distingue enfin du texte (le bouton reprenait --text, donc un brun sombre
indistinct du reste). L'arrêt, lui, reste en terre cuite. */
/* --- Terre : largeur de lecture --------------------------------------
La mesure était plafonnée à 760 px et centrée sur la fenêtre : sur un écran
large, le fil se réduisait à une colonne étroite entourée de vide, alors que
la charte fait courir le texte sur toute la largeur du panneau.
Une seule variable pilote désormais le fil ET la carte de saisie — ils
étaient réglés séparément (450 / 380 / 360 px), donc condamnés à diverger.
La borne haute reste : au-delà, une ligne devient pénible à lire (l'œil perd
le début de la suivante). 1080 px ≈ 110 caractères, large mais tenable. */
@media (min-width:721px){
:root{--measure:min(1080px,100%)}
#chat{padding-top:26px;padding-inline:max(20px,calc(50% - var(--measure)/2))}
#composer{--col:calc(max(20px,calc(50% - var(--measure)/2)) + var(--sbw,0px))}
}
/* --- Terre : rangée de bulles techniques ------------------------------
Raisonnement, outils et mémoire ne portent qu'une étiquette tant qu'ils sont
repliés : les empiler gaspillait une ligne entière par appel et repoussait la
réponse hors de l'écran. Ils défilent donc en rangée, et reprennent la pleine
largeur dès qu'on en déplie un. */
.toolrow{display:flex;flex-wrap:wrap;gap:8px;align-items:flex-start}
.toolrow>.msg.collapsible{margin:0;max-width:100%}
.toolrow>.msg.collapsible.collapsed{width:auto}
/* Dépliée : la bulle passe seule sur sa ligne, sinon son contenu serait
comprimé dans une colonne étroite à côté d'une pastille. */
.toolrow>.msg.collapsible:not(.collapsed){flex:1 0 100%}
/* --- Terre : sections groupées en cartes ------------------------------
Une carte par section empilait onze arrondis et autant de gouttières : la
barre latérale débordait de l'écran alors que la plupart de ces lignes sont
des titres repliés d'une seule ligne. Les sections d'IA d'un côté, celles de
l'application de l'autre, tiennent maintenant chacune dans UNE carte, avec un
filet discret entre les lignes. */
@media (min-width:721px){
.sidegroup{background:var(--panel);border-radius:20px;box-shadow:var(--shadow);
padding:2px 16px;display:flex;flex-direction:column}
/* Dans un groupe, les sections ne sont plus des cartes : elles redeviennent
des lignes, séparées par un filet — sauf la première. */
.sidegroup>details{background:transparent;box-shadow:none;border-radius:0;padding:0;
border-top:1px solid var(--border)}
.sidegroup>details:first-child{border-top:0}
.sidegroup>details[open]{padding-bottom:12px}
}
/* --- Recherche Hugging Face -------------------------------------------
Deux listes successives dans la même zone (dépôts, puis fichiers du dépôt
choisi) : une seule mise en forme sert aux deux, sinon elles divergent. La
liste est bornée en hauteur et défile — un dépôt publie facilement vingt
quantifications, et la modale ne doit pas s'étirer jusqu'à sortir de
l'écran. */
.hf-list{display:flex;flex-direction:column;max-height:220px;overflow-y:auto;
border:1px solid var(--border);border-radius:12px;margin-top:6px}
.hf-row{display:flex;align-items:center;gap:10px;padding:7px 12px;cursor:pointer;
border-top:1px solid var(--border);font-size:12px}
.hf-row:first-child{border-top:0}
.hf-row:hover{background:var(--row-bg-on)}
/* Le nom prend la place restante et se tronque : un identifiant de dépôt long
ne doit pas pousser la taille et le verdict hors de la ligne. */
.hf-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text)}
.hf-meta{flex:none;color:var(--dim);font-family:var(--mono);font-size:10.5px;white-space:nowrap}
/* Verdict mémoire. La couleur seule ne suffit pas — le mot est écrit dedans,
et l'explication complète est dans l'infobulle. */
.hf-fit{flex:none;padding:1px 8px;border-radius:999px;font-size:10px;letter-spacing:.03em;
text-transform:uppercase;border:1px solid transparent}
.hf-fit.ok{color:var(--ok);border-color:var(--ok)}
.hf-fit.juste{color:var(--warn);border-color:var(--warn)}
.hf-fit.trop{color:var(--err);border-color:var(--err)}
.hf-head{display:flex;align-items:center;gap:10px;margin-top:6px;font-size:12px}
.hf-mm{display:flex;align-items:center;gap:6px;margin-top:8px;font-size:11.5px;
color:var(--dim);cursor:pointer}
.hf-mm.muted{cursor:default}
/* --- Panneau « Fichiers » (droite) ------------------------------------
Symétrique des réglages à gauche : une colonne fixe qui pousse la
conversation au lieu de la recouvrir. C'est possible ici parce que la largeur
de lecture est pilotée par --measure et que #chat se recentre tout seul dans
la place qui reste — le fil se resserre, il ne passe pas dessous.
Le panneau est retiré du flux quand il est fermé (et non simplement décalé) :
sinon il conserverait sa gouttière et le fil resterait décentré. */
#files{display:none;width:320px;flex-shrink:0;border-left:1px solid var(--border);
background:var(--panel);padding:16px;padding-bottom:calc(16px + env(safe-area-inset-bottom));
overflow-y:auto;flex-direction:column;gap:10px}
html[data-files="1"] #files{display:flex}
#files-backdrop{display:none;position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:14}
.files-head{display:flex;align-items:center;gap:8px}
.files-head .stitle{flex:1}
#files-refresh,#files-close{margin:0;padding:2px 8px;min-height:0;font-size:12px;
background:transparent;border:1px solid transparent;color:var(--dim);border-radius:8px}
#files-refresh:hover,#files-close:hover{border-color:var(--border);color:var(--text)}
/* Fil d'Ariane : il doit pouvoir revenir à la ligne — un identifiant de
discussion dans le chemin dépasse largement 320 px. */
.files-crumb{display:flex;flex-wrap:wrap;align-items:center;gap:2px;font-size:11px}
.files-crumb-b{margin:0;padding:2px 6px;min-height:0;font-size:11px;border:0;border-radius:7px;
background:transparent;color:var(--dim);max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.files-crumb-b:hover:not(:disabled){background:var(--row-bg-on);color:var(--text)}
.files-crumb-b.on{color:var(--text);cursor:default}
.files-crumb-s{color:var(--dim);opacity:.5}
.files-list{display:flex;flex-direction:column;flex:1;min-height:0}
.files-empty{font-size:12px;line-height:1.5;padding:10px 2px}
/* Une ligne = pastille, nom + méta, actions. Les actions n'apparaissent qu'au
survol : au repos on veut lire des noms de fichiers, pas une grille de
boutons. Sur écran tactile (pas de survol) elles restent visibles. */
.files-row{display:flex;align-items:center;gap:8px;padding:6px 8px;border-radius:12px;font-size:12px}
.files-row:hover{background:var(--row-bg-on)}
.files-ic{flex:none;width:12px;text-align:center;color:var(--dim)}
.files-row.isdir .files-ic{color:var(--accent)}
.files-mid{flex:1;min-width:0;display:flex;flex-direction:column;gap:1px}
.files-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text)}
.files-meta{font-family:var(--mono);font-size:10px;color:var(--dim);white-space:nowrap}
.files-acts{flex:none;display:flex;gap:2px;opacity:0;transition:opacity .12s}
.files-row:hover .files-acts,.files-row:focus-within .files-acts{opacity:1}
@media (hover:none){.files-acts{opacity:1}}
.files-acts .iconbtn{font-size:11px;line-height:1;padding:4px 6px;background:transparent;
border:1px solid transparent;border-radius:6px;color:var(--dim);cursor:pointer;margin:0;min-height:0}
.files-acts .iconbtn:hover{border-color:var(--border);color:var(--text)}
.files-foot{font-family:var(--mono);font-size:10px;padding-top:2px;border-top:1px solid var(--border);
display:flex;align-items:center;gap:6px;flex-wrap:wrap}
/* Détour « hors discussion » : discret, en pied de panneau — c'est une sortie de
secours pour les fichiers d'avant le rangement par discussion, pas une vue
qu'on propose au même rang que la discussion ouverte. */
.files-scope{margin:0;padding:2px 6px;min-height:0;font-family:var(--mono);font-size:10px;
background:transparent;border:1px solid var(--border);border-radius:7px;color:var(--dim)}
.files-scope:hover{color:var(--text);background:var(--row-bg-on)}
/* Téléchargement en cours : downloadWorkspaceFile pose .busy sur la ligne. */
.files-row.busy{opacity:.55;pointer-events:none}
/* Bouton dossier du composeur : même gabarit que le trombone, et teinté quand
le panneau est ouvert — l'état se lit sans regarder à droite. */
#files-btn{width:18px;height:18px;min-height:18px;flex:none;margin:0;padding:0;
display:flex;align-items:center;justify-content:center;border-radius:999px;
background:transparent;border:1px solid transparent;color:var(--dim)}
#files-btn:hover{background:var(--row-bg-on);border-color:transparent;color:var(--text)}
#files-btn:active{transform:scale(.9)}
#files-btn svg{display:block}
html[data-files="1"] #files-btn{color:var(--accent)}
/* Petit écran : tiroir par la droite, comme les réglages par la gauche. Pousser
une colonne de 320 px sur un téléphone ne laisserait rien à la conversation. */
@media (max-width:720px){
#files{position:fixed;inset:0 0 0 auto;width:85%;max-width:340px;z-index:15;
background:var(--bg);padding-top:calc(16px + env(safe-area-inset-top));
padding-right:calc(16px + env(safe-area-inset-right))}
html[data-files="1"] #files-backdrop{display:block}
}
@media (min-width:721px){
/* Dans la charte « Terre », le panneau est une carte comme les autres. */
#files{background:var(--panel);border-left-color:transparent;border-radius:20px;
box-shadow:var(--shadow);margin:0}
}
/* =====================================================================
===== Refonte « Sober Tech » ========================================
=====================================================================
Dernière section du fichier, donc dernier mot de la cascade : elle porte la
coque de la refonte (barre d'en-tête, zones de la barre latérale, moniteur
machine) et redresse les quelques règles historiques qu'elle contredit.
Trois principes, tirés de la maquette :
1. Le fil est la seule chose qui compte : tout le reste est atténué, sans
cadre superflu, et la barre latérale s'escamote d'un clic.
2. Les données machine sont TOUJOURS visibles mais jamais au premier plan —
un pied de barre latérale, trois jauges, pas un tableau.
3. Aucun contour permanent sur les boutons : l'affordance vient du survol
(fond qui apparaît, léger enfoncement au clic).
===================================================================== */
/* --- Coque ---------------------------------------------------------- */
.side{background:var(--side);display:flex;flex-direction:column;gap:0;padding:0;overflow:hidden}
/* Les trois zones de la barre : en-tête fixe, contenu qui défile, moniteur
posé en pied. Sans ça, la liste des discussions poussait les jauges hors de
l'écran dès la dixième discussion. */
.sidehead{flex:none;display:flex;align-items:center;gap:8px;margin:0;
padding:14px 14px 10px;padding-top:calc(14px + env(safe-area-inset-top))}
#side-scroll{flex:1;min-height:0;overflow-y:auto;padding:0 0 12px;
scrollbar-gutter:stable;display:flex;flex-direction:column;gap:0}
#side-perf{flex:none;border-top:1px solid var(--border);padding:12px 14px;
padding-bottom:calc(12px + env(safe-area-inset-bottom))}
@media (max-width:720px){
.side{padding:0}
#side-scroll{padding-left:calc(12px + env(safe-area-inset-left))}
.sidehead{padding-left:calc(14px + env(safe-area-inset-left))}
}
/* Escamotage sur grand écran : la barre disparaît, le fil reprend la place.
L'état est persisté (localStorage) et posé sur <html> avant le rendu, comme
le thème — sinon la barre apparaît puis se referme sous les yeux. */
@media (min-width:721px){
html[data-side="0"] .side{display:none}
html[data-side="0"] #sidetoggle svg{transform:none}
}
#sidetoggle svg{transform:scaleX(-1)}
/* --- Barre d'en-tête du fil ----------------------------------------- */
/* Elle porte ce qui décrit la conversation en cours : de quoi on parle, et avec
QUEL modèle. Le choix du modèle vivait au fond de la barre latérale, dans une
liste de presets — trois clics et un défilement pour une action qu'on fait
plusieurs fois par session. */
#topbar{flex:none;display:flex;align-items:center;gap:10px;padding:10px 16px;
padding-top:calc(10px + env(safe-area-inset-top));
border-bottom:1px solid var(--border);background:var(--bg);z-index:5}
#topbar-title{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
font-size:13.5px;font-weight:500;color:var(--text)}
#topbar .iconbtn{width:30px;height:30px;flex:none;display:inline-flex;align-items:center;
justify-content:center;border:0;border-radius:9px;background:transparent;color:var(--dim);
cursor:pointer;margin:0;padding:0;transition:background .15s,color .15s,transform .1s}
#topbar .iconbtn:hover{background:var(--row-bg-on);color:var(--text)}
#topbar .iconbtn:active{transform:scale(.92)}
#topbar .iconbtn svg{display:block}
/* Sélecteur de modèle : le libellé n'est PAS une étiquette flottante mais le nom
du preset actif, lisible d'un coup d'œil. */
#model-switch{max-width:230px;font:inherit;font-size:12.5px;color:var(--text);
background:var(--panel);border:1px solid var(--border);border-radius:999px;
padding:6px 30px 6px 12px;cursor:pointer;appearance:none;
background-image:linear-gradient(45deg,transparent 50%,var(--dim) 50%),linear-gradient(135deg,var(--dim) 50%,transparent 50%);
background-position:calc(100% - 15px) 52%,calc(100% - 11px) 52%;
background-size:4px 4px,4px 4px;background-repeat:no-repeat}
#model-switch:hover{border-color:var(--accent)}
#model-switch:focus-visible{outline:2px solid var(--accent);outline-offset:1px}
@media (max-width:720px){
#topbar{padding-left:56px} /* la place du bouton ☰, en position fixe */
#model-switch{max-width:130px}
}
/* --- Discussions ----------------------------------------------------- */
/* La liste EST la barre latérale : titre à gauche, heure à droite, une ligne.
Le reste (réglages) descend sous un unique repli. */
#conv-search{width:calc(100% - 24px);font:inherit;font-size:12.5px;color:var(--text);
background:var(--panel);border:1px solid var(--border);border-radius:9px;
padding:7px 11px;margin:0 12px 8px}
#conv-search::placeholder{color:var(--dim)}
#conv-search:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-bg)}
.side .preset{padding:11px 14px;margin:0;gap:10px;border-radius:0;
border:0;border-bottom:1px solid var(--border)}
.side .preset .preset-name{font-size:13px;font-weight:450;white-space:nowrap;overflow:hidden;
text-overflow:ellipsis;word-break:normal;overflow-wrap:normal}
.side .preset.active .preset-name{font-weight:550}
/* Heure de la dernière activité : à DROITE du titre, sur la même ligne, en
monospace pour que les colonnes s'alignent d'une ligne à l'autre.
⚠️ Sélecteur en TROIS classes : `.preset>span` (plus haut) pose flex:1 sur
tout span enfant d'une ligne — l'heure prenait donc la MOITIÉ de la ligne et
le titre était tronqué au premier mot. */
.side .preset .conv-when{flex:none;font-family:var(--mono);font-size:10.5px;
color:var(--dim);letter-spacing:-.01em;overflow:visible}
.side .preset:hover .conv-when{opacity:0}
.conv-empty{font-size:12px;color:var(--dim);padding:8px 11px}
/* Repli des réglages : tout ce qui n'est pas la conversation tient sous cette
seule ligne, pour que la barre latérale de la maquette (discussions +
machine) reste la vue par défaut. */
#side-settings{margin-top:8px;border-top:1px solid var(--border)}
#side-settings>summary{height:38px}
#side-settings>.sidegroup{margin-top:2px}
/* --- Moniteur machine (pied de barre latérale) ----------------------- */
.perf-head{display:flex;align-items:center;gap:8px;margin-bottom:11px;
font-size:14px;font-weight:600;color:var(--text);letter-spacing:-.01em}
.perf-row{margin:0 0 14px}
.perf-row:last-child{margin-bottom:0}
.perf-l{display:flex;align-items:baseline;gap:8px;font-size:12.5px;color:var(--text);margin-bottom:6px}
.perf-l b{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
font-weight:400;color:var(--text)}
.perf-l .perf-pct{font-size:12.5px;color:var(--text);font-variant-numeric:tabular-nums}
/* La jauge : 4 px, coins ronds, remplissage sauge. Elle passe à l'ambre puis au
rouille quand la machine sature — c'est là que l'information devient utile. */
.perf-track{height:7px;border-radius:999px;background:var(--track);overflow:hidden}
.perf-fill{height:100%;width:0;border-radius:999px;background:var(--accent-fill);
transition:width .4s ease,background .4s ease}
.perf-fill.hot{background:var(--warn)}
.perf-fill.max{background:var(--err)}
/* --- Boutons : sans contour, micro-interaction au survol ------------- */
button{border-color:transparent;background:var(--row-bg);border-radius:9px;
transition:background .15s ease,color .15s ease,transform .08s ease}
button:hover{background:var(--row-bg-on);border-color:transparent;color:var(--text)}
button:active:not(:disabled){transform:translateY(1px)}
.side .preset,.slist,#conv-search{transition:background .15s ease,border-color .15s ease}
/* --- Fil : la réponse redevient une carte --------------------------- */
/* La maquette encadre la réponse de l'IA : sur un fond de fil coloré, du texte
nu flotte sans ancrage, et la frontière entre deux réponses se perd. */
#chat{padding-top:22px}
#chat .msg.assistant{background:var(--bubble);border:0;
border-radius:10px;padding:15px 17px;max-width:100%;box-shadow:var(--shadow-card)}
#chat .msg.assistant.errmsg{background:var(--danger-bg);border-color:color-mix(in srgb,var(--err) 45%,transparent)}
/* Question : aplat de sauge, coin bas-droit rentré. --on-accent garantit le
contraste dans les DEUX variantes. */
#chat .msg.user{background:var(--user-bg);color:var(--on-user);border-radius:10px;
padding:13px 16px;max-width:62%;box-shadow:var(--shadow-card)}
#chat .msg.user .label{color:var(--on-user);opacity:.75}
#chat .msg.user .body a{color:var(--on-user);text-decoration:underline}
#chat .msg.user code{background:rgba(255,255,255,.18);color:inherit}
[data-theme="dark"] #chat .msg.user code{background:rgba(15,23,42,.22)}
/* Les pastilles de fichier d'une question sont posées SUR l'aplat de sauge :
sans fond propre, elles s'y fondaient. */
#chat .msg.user .msg-files .chip-file{background:var(--panel);color:var(--text);border-color:transparent}
/* --- Saisie : la pilule de la maquette ------------------------------ */
/* Le bouton d'envoi devient une pastille ronde à flèche : c'est la forme de la
maquette, et elle laisse toute la largeur au texte. Le mot « Envoyer » reste
porté par aria-label et par l'infobulle — l'action n'est pas moins nommée
pour un lecteur d'écran, elle l'est moins à l'écran. */
/* Les QUATRE boutons de la barre de saisie — joindre, fichiers, envoyer,
arrêter — sont un seul et même objet : 34x34, coins doux (jamais un rond :
la maquette n'en a pas), icône de 18 px centrée, fond seulement au survol.
Seule la couleur les distingue : atténué pour les secondaires, sauge pour
l'envoi, rouille pour l'arrêt.
⚠️ Une seule règle pour les quatre. Il y en avait trois générations empilées
(pilule large « Envoyer », aplat noir, aplat sauge) : chaque état tirait sa
taille, sa forme et son alignement d'une couche différente. */
#inputbar #attach,#inputbar #files-btn,#send,#stop{
width:34px;height:34px;min-height:34px;flex:none;margin:0;padding:0;
display:inline-flex;align-items:center;justify-content:center;align-self:center;
border:0;border-radius:10px;background:transparent;color:var(--dim);
transition:background .15s ease,color .15s ease,transform .08s ease}
#inputbar #attach svg,#inputbar #files-btn svg,#send svg,#stop svg{display:block;width:18px;height:18px}
#inputbar #attach:hover,#inputbar #files-btn:hover{background:var(--row-bg-on);color:var(--text)}
html[data-files="1"] #files-btn{color:var(--accent)}
#send{color:var(--accent)}
#send:hover:not(:disabled){background:var(--accent-bg);color:var(--accent);opacity:1;transform:none}
#send:disabled{background:transparent;color:var(--dim);opacity:.45}
#stop{color:var(--err)}
#stop:hover{background:var(--danger-bg);color:var(--err);opacity:1}
#inputbar #attach:active,#inputbar #files-btn:active,#send:active:not(:disabled),#stop:active{transform:scale(.92)}
/* Les outils de la saisie (joindre, fichiers, envoyer) montent DANS la pilule,
à droite du texte : c'est là qu'on les cherche, et le pied de carte redevient
ce qu'il doit être — une jauge de contexte, rien d'autre. */
#inputbar{align-items:flex-end;padding:8px 8px 8px 18px;gap:4px;box-shadow:var(--shadow-card);
border-radius:14px 14px 0 0}
#ctxmeta{border-radius:0 0 14px 14px}
#ctxmeta{padding-bottom:12px}
#ctxtrack{border-radius:999px}
/* --- Coque à plat ---------------------------------------------------
La direction précédente posait deux CARTES flottantes séparées par une
gouttière ; la maquette est bord à bord : barre latérale collée au bord,
simple filet de séparation, fil qui occupe tout le reste. On neutralise donc
ici les règles de coque de la section « Terre » plutôt que de les réécrire
plus haut — la cascade fait le tri, et l'historique reste lisible. */
@media (min-width:721px){
body{padding:0;gap:0}
html[data-pwa="1"] body{padding-top:env(safe-area-inset-top)}
.side{width:300px;border-right:1px solid var(--border);border-radius:0;
box-shadow:none;background:var(--side);overflow:hidden}
/* Dans la barre, plus de cartes : des lignes posées sur le fond. Deux niveaux
de cadre imbriqués (carte de section + carte de liste) pour une liste de
titres, c'était deux de trop. */
#side-scroll>details,#side-scroll>.sidegroup{background:transparent;box-shadow:none;
border-radius:0;padding:0;margin:0}
#side-scroll>details>summary{height:34px}
.side .slist{background:transparent;border-color:transparent;border-radius:12px}
.side #side-settings .slist{background:var(--row-bg);border-radius:12px}
/* Le fil n'est plus une carte : il EST la page. */
.main{background:transparent;border-radius:0;box-shadow:none;overflow:visible}
#composer{background:linear-gradient(to bottom,transparent 0,var(--bg) var(--fade))}
#inputbar,#ctxmeta{background:var(--panel);border-color:var(--border)}
#input{background:transparent}
}
/* Ligne de discussion : les actions (renommer, supprimer) sont posées EN
SURIMPRESSION à droite plutôt qu'en flux. En flux, elles réservaient leur
largeur en permanence — invisibles mais présentes — et le titre était tronqué
au tiers de la place disponible. */
.side .preset{position:relative}
.side .preset .conv-acts{position:absolute;right:6px;top:50%;transform:translateY(-50%);
margin:0;background:var(--row-bg-on);border-radius:9px;padding:1px}
.side .preset.active .conv-acts{background:var(--row-bg)}
.side .preset .preset-info{overflow:hidden}
/* La sélection est un aplat, pas un cadre : un liseré autour d'une ligne de
liste alourdit là où un fond suffit à dire « c'est celle-ci ». */
.side .preset.active{background:var(--row-bg-on);border-color:transparent}
.side .preset.active .preset-name{color:var(--text)}
/* --- Téléphone : un seul bouton de barre latérale -------------------- */
/* Le ☰ flottant ouvre déjà le tiroir ; garder EN PLUS l'icône d'escamotage de
l'en-tête donnait deux boutons voisins pour la même action, et grignotait la
place du titre sur un écran qui n'en a pas. */
@media (max-width:720px){
/* Sélecteur en DEUX id : `#topbar .iconbtn` (plus haut) pose déjà un display,
et il l'emporterait sur un simple `#sidetoggle`. */
#topbar #sidetoggle{display:none}
#topbar{padding-left:52px;padding-right:10px;gap:6px}
#topbar-title{font-size:13px}
}
/* Le ☰ suit la charte des boutons : pas de contour, un fond qui apparaît au
survol. Il flotte au-dessus de l'en-tête, il doit donc en reprendre le fond
pour ne pas laisser voir le fil défiler dessous. */
#menubtn{background:var(--bg);border:0;color:var(--dim);border-radius:10px;
width:34px;height:34px;font-size:16px;top:calc(11px + env(safe-area-inset-top));left:9px}
#menubtn:hover{background:var(--row-bg-on);color:var(--text)}
#lc-chip{border-radius:10px;height:34px;top:calc(11px + env(safe-area-inset-top));left:calc(50px + env(safe-area-inset-left))}
/* Jauges : la piste doit se VOIR, sinon une barre presque vide ressemble à un
trait souligné sous l'intitulé. Valeurs relevées sur la maquette. */
.perf-track{background:var(--track)}
/* Une liste déroulante posée directement dans une ligne de modale n'a pas de
garde-fou de largeur (contrairement à .pe-row-c) : une valeur très longue —
un chemin de destination, par exemple — débordait sous l'intitulé. */
.pe-row>.pe-selc{max-width:62%}
/* Marque et attribution du fork empilées, pastille d'état à leur droite. */
.sidebrand{flex:1;min-width:0;display:flex;flex-direction:column;gap:1px}
.sidebrand .brand-fork{margin:0;font-size:10px;line-height:1.3}
/* Blocs de code du fil : encadré franc dans la carte de réponse — fond plus
soutenu, filet net et coins alignés sur ceux de la carte. Le bouton « copier »
reprend le fond du bloc pour ne pas flotter sur le texte. */
#chat .msg.assistant .body pre,#chat .msg.reasoning .body pre,#chat .msg.tool pre{
background:var(--code-bg);border:1px solid color-mix(in srgb,var(--text) 10%,transparent)}
#chat .msg.assistant .body code{background:var(--code-bg);
border:1px solid color-mix(in srgb,var(--text) 7%,transparent);border-radius:5px}
#chat .msg.assistant .body pre code{border:0;background:transparent}
/* --- Titres de la barre latérale ------------------------------------
La maquette n'a qu'un style de titre : casse normale, demi-gras, taille du
texte courant (« Performance »). Le monospace en capitales de la charte
précédente y jurait, et faisait crier des libellés qui n'ont rien à dire. */
.side .stitle{font-family:var(--font);text-transform:none;letter-spacing:-.01em;
font-size:13.5px;font-weight:600;color:var(--text)}
#side-scroll>details>summary,#side-settings>summary{padding:0 14px}
#side-settings{margin-top:0;border-top:1px solid var(--border)}
#side-settings>.sidegroup{padding:0 12px}
/* La liste elle-même n'a pas de gouttière : ses lignes touchent les deux bords,
comme dans la maquette. Le filet du bas de la dernière ligne fait la
séparation avec ce qui suit. */
#conv-list{margin:0}
.side .preset:hover{background:var(--row-bg)}
.side .preset .conv-acts{right:8px}
/* Message d'absence de matériel : même famille que les jauges, en plus discret.
(La sous-ligne chiffrée, elle, a quitté l'affichage — le détail est en
infobulle, comme dans la maquette.) */
.perf-sub{font-size:11.5px;color:var(--dim)}
/* --- Carte de saisie : le trait sauge de la maquette -----------------
C'est le seul contour coloré de l'écran, et il désigne l'endroit où l'on
écrit. ⚠️ Posé EN FIN DE FEUILLE : la règle de coque plus haut
(`#inputbar,#ctxmeta{border-color:var(--border)}`) reprenait sinon la main et
le cadre retombait au gris commun.
La flèche d'envoi prend la MÊME sauge que la maquette — c'est un aplat plein
de 18 px, pas du texte. */
@media (min-width:721px){
#inputbar,#ctxmeta{border-color:var(--accent-fill)}
#composer:focus-within #inputbar,#composer:focus-within #ctxmeta{border-color:var(--accent)}
}
#inputbar,#ctxmeta{border-color:var(--accent-fill)}
#send{color:var(--accent-fill)}
#send:hover:not(:disabled){color:var(--accent)}
/* --- Échelle typographique des planches ------------------------------
« Typography: Inter » — Bold 24px pour les titres, Regular 16px pour le corps,
Medium 14px pour le texte secondaire. On l'applique là où elle a un sens
(le fil, la barre latérale) plutôt qu'en gonflant toute l'interface : les
panneaux de réglages sont des formulaires denses, pas du texte de lecture. */
#chat .msg .body{font-size:16px}
#chat .msg.user .body,#chat .msg.assistant .body{line-height:1.6}
#input{font-size:16px}
.side .preset .preset-name{font-size:14px}
.side .preset .conv-when{font-size:13px}
.perf-head{font-size:16px;font-weight:700}
.perf-l,.perf-l .perf-pct{font-size:14px;font-weight:500}
.side .stitle{font-size:14px;font-weight:600}
/* Marque : « Loki v0.9.4 » en tête de colonne, à l'échelle des titres. */
#brand svg{height:20px}
#ver{font-size:13px;font-weight:500;margin-left:8px}