Files
Loki/internal/ajean/ui/src/styles.css
T
nathaninline 110458c133 Renommage AJEAN (1/3) : module, chemins de donnees et auto-MAJ
Socle du renommage jean -> AJEAN, sans aucun impact visible pour le parc
installe. Trois briques :

- module github.com/nathaninline/ajean, cmd/ajean, internal/ajean,
  package ajean. Purement interne.

- AjeanHome() remplace JeanHome(). Les defauts deviennent
  %ProgramData%\ajean et /etc/ajean, avec migration du dossier existant
  au premier lancement. La migration est un os.Rename dans le dossier
  parent de l'ancien chemin : intra-volume, donc atomique et instantane
  meme avec des .gguf de plusieurs dizaines de Go. Si le rename echoue
  (handle ouvert sous Windows, droits, volume en lecture seule) on
  CONTINUE sur l'ancien chemin et on retentera : jamais de copie, jamais
  de suppression, le pire cas est "rien n'a bouge".
  $AJEAN_HOME et $JEAN_HOME restent tous deux honores, ainsi que
  /etc/default/ajean et /etc/default/jean : un chemin impose a la main
  est un ordre, on ne le migre pas.

- l'auto-MAJ accepte desormais ajean-<os>-<arch> ET jean-<os>-<arch>,
  dans cet ordre. Indispensable dans les deux sens : les binaires deja
  installes ne connaissent que jean-*, et ce binaire-ci doit pouvoir
  s'installer depuis une release anterieure au renommage.

Couvert par des tests, dont le repli quand un handle ouvert bloque le
rename sous Windows.
2026-08-04 13:59:25 +02:00

830 lines
64 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.
/* ===== Palette =====================================================
Un seul thème (« Clean ») : noir et blanc, sobre. 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. */
:root{color-scheme:light;--font:-apple-system,BlinkMacSystemFont,'Segoe UI',Inter,Helvetica,Arial,sans-serif;--mono:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;--bubble:#f0f0f0;--bg:#ffffff;--panel:#fafafa;--border:#e0e0e2;--text:#0a0a0a;--dim:#6b6b70;--accent:#0a0a0a;--accent-bg:rgba(0,0,0,.05);--ok:#0a0a0a;--warn:#6b6b6b;--err:#c0392b;--mag:#6b6b6b;--code-bg:#f5f5f5;--danger-bg:#fbeae8;--warn-bg:#f2f2f2;--add:#1a7f37;--add-bg:rgba(26,127,55,.10);--del:#cf222e;--del-bg:rgba(207,34,46,.10)}
[data-theme="dark"]{color-scheme:dark;--bubble:#232326;--bg:#0b0b0c;--panel:#161618;--border:#33333a;--text:#f2f2f4;--dim:#a0a0a8;--accent:#f5f5f5;--accent-bg:rgba(255,255,255,.07);--ok:#f5f5f5;--warn:#8e8e8e;--err:#f2777a;--mag:#8e8e8e;--code-bg:#000000;--danger-bg:#2e1a19;--warn-bg:#1c1c1c;--add:#4ac26b;--add-bg:rgba(74,194,107,.13);--del:#f85149;--del-bg:rgba(248,81,73,.13)}
*{box-sizing:border-box}
/* ⚠️ 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}
@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}
body.drawer-open #menubtn{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}
#send,#stop{padding:0 12px}
/* 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}
}
/* 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 : « AJEAN » 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}
#brand svg{height:10px;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:18px}
/* 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}
.statuspill .port{opacity:.6;font-weight:400}
/* 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:3px 11px 4px;background:var(--panel);color:var(--text);border:1px solid var(--border);border-radius:6px;cursor:pointer;line-height:1.2}
.addbtn:hover{border-color:var(--accent);color:var(--accent)}
.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 englobante .gated-block. */
/* Barre repliable « Pages » : un seul chevron, bouton + ajouter aligné à droite. */
.pages-bar{display:flex;align-items:center;gap:8px;cursor:pointer;user-select:none;padding:7px 0;margin-top:4px}
.pages-bar .pages-caret{flex-shrink:0;font-size:9px;color:var(--dim);transition:transform .12s}
.pages-bar.open .pages-caret{transform:rotate(90deg)}
.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-list .preset{margin:3px 0;padding:5px 9px}
.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);animation:askpop .12s ease-out}
@keyframes askpop{from{opacity:0;transform:translateY(6px) scale(.98)}to{opacity:1;transform:none}}
.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 « JEAN »). L'état agent reste signalé par sa
pastille dans les réglages. */
#brand.agent #brand-a{color:var(--accent)}
/* interrupteur (switch) */
/* Sous-réglage du mode agent inaccessible quand l'agent est off : grisé + inerte. */
/* Sous-réglages du mode agent présentés en CARTES (bordure + fond) pour ne pas
les confondre avec les traits des sections principales. */
/* Bloc du mode agent = carte de liste : le rembourrage est porté par les
lignes (comme .slist), pas par la carte, sinon les retraits se cumulent. */
/* ⚠️ PAS d'overflow:hidden ici : les bulles d'aide (« ? ») sont positionnées en
absolu par rapport à la ligne, donc un débordement caché les TRONQUE — c'est
ce qui rendait plusieurs infobulles illisibles. */
.gated-block{margin:6px 0;border:1px solid var(--border);border-radius:10px;padding:0 0 10px;background:var(--panel);overflow:visible;transition:opacity .15s}
.gated-block>.switchrow:first-of-type{padding-top:0}
.gated-block.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}
.preset-info{flex:1;min-width:0}
.preset-name{min-width:0;overflow-wrap:anywhere;word-break:break-word}
.preset-edit{flex-shrink:0;align-self:flex-start;margin:-3px -4px 0 0;padding:2px 4px;font-size:13px;line-height:1;background:transparent;border:none;opacity:.6}
.preset-edit:hover{opacity:1;color:var(--accent)}
.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}
.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}
.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}
/* Premier élément d'une carte : sa marge haute s'ajoutait au rembourrage de la
carte (18 px de vide au-dessus de « Mémoire », « Accès internet », etc.). */
.gated-block>:first-child{margin-top:0}
/* 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 : cf. la note sur .gated-block — les infobulles doivent
pouvoir déborder de la carte. Les lignes n'ont pas de fond propre, l'arrondi
n'a donc rien à rogner. */
.slist,.gated-block{background:var(--panel);border:1px solid var(--border);border-radius:9px;
overflow:visible;margin:0 0 8px}
.slist:last-child,.gated-block:last-child{margin-bottom:0}
/* Contenu libre (champ + boutons) : le rembourrage passe sur la carte. */
.slist.pad,.gated-block{padding:8px 10px}
.slist.pad>:first-child{margin-top:0}
.slist.pad>:last-child{margin-bottom:0}
/* --- 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){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}
.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)
data-hide-side : barre latérale en tiroir, même sur ordinateur
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}
/* Barre latérale en tiroir sur TOUTES les tailles d'écran : mêmes règles que
la version mobile, activées par l'attribut au lieu de la largeur. */
[data-hide-side="1"] .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));border-right:1px solid var(--border)}
@media (min-width:721px){[data-hide-side="1"] .side{width:380px;max-width:380px}}
/* Le bouton ☰ est posé au-dessus du fil : on dégage le haut de la
conversation pour qu'il ne recouvre aucun message. */
@media (min-width:721px){html[data-hide-side="1"] #chat{padding-top:60px}}
[data-hide-side="1"] .side.open{transform:translateX(0)}
[data-hide-side="1"] #menubtn{display:block}
[data-hide-side="1"] #backdrop.open{display:block}
[data-hide-side="1"] body.drawer-open #menubtn{display:none}
[data-hide-side="1"] .main{width:100%}
/* 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-text{font-size:10px;letter-spacing:.03em;white-space:nowrap}
#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}
#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)}
#send{padding:0 20px}
.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}
#send,#stop{padding:0 14px}
#composer .muted{display:none}
#ctxmeta{padding:3px 10px 0}
#ctx-text{font-size:9px}
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))}
}
/* --- Backend llama.cpp (2 modes : rapide / optimisée) --------------------- */
.lc-head{font-size:12px;line-height:1.5;margin-bottom:10px}
.lc-modes{display:flex;flex-direction:column;gap:8px}
.lc-modes.busy{opacity:.5;pointer-events:none}
.lc-mode{border:1px solid var(--border);border-radius:8px;padding:10px 12px;cursor:pointer;transition:border-color .12s,background .12s}
.lc-mode:hover{border-color:var(--dim)}
.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}
.lc-mode-state{font-size:11px;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}
.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}
.pe-row:not(:last-child){border-bottom: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}
.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:13px 16px;border-top:1px solid var(--border);display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap}
.pe-foot .foot-r{display:flex;gap:8px}
.pe-btn{margin:0;padding:9px 18px;border-radius:9px;font-weight:600;cursor:pointer;border:1px solid var(--border);background:var(--panel);color:var(--text)}
.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)}
.pe-btn.ghost-danger{border-color:transparent;background:transparent;color:var(--err);padding:9px 10px}
.pe-btn.ghost-danger:hover{background:var(--danger-bg)}
.pe-delmodel{display:inline-flex;align-items:center;gap:6px;font-size:11px;color:var(--dim);cursor:pointer}
.pe-delmodel input{margin:0}
/* É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}
.modal-card{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}
.modal-x{margin:0;padding:3px 10px;font-size:16px;line-height:1;color:var(--dim)}
.modal-body{padding:16px;overflow:auto;flex:1}
.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:4px}
.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}
/* Toggles de la sous-section « Paramètres » du mode agent : chacun dans sa
propre carte bordée et arrondie (comme les serveurs MCP), plus lisible. */
#param-block .switchrow{border:1px solid var(--border);border-radius:8px;padding:9px 11px;background:var(--bg)}
#param-block .switchrow+.switchrow{margin-top:8px}
/* 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/JEAN. 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. */
#chat{padding:28px max(16px,calc(50% - 380px)) 8px;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)}
/* Étiquettes VOUS / JEAN : inutiles ici (l'alignement suffit), sauf pour les
bulles repliables (raisonnement / outils) où le label est le bouton de repli. */
#chat .msg .label{display:none}
#chat .msg.collapsible .label{display:block}
/* 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. */
#composer{position:absolute;left:0;right:0;bottom:0;z-index:4;border-top:none;display:flex;flex-direction:column;padding:34px 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) 34%)}
#chat{padding-bottom:calc(170px + env(safe-area-inset-bottom))}
#scrollbtn{bottom:calc(180px + env(safe-area-inset-bottom));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}
#send,#stop{border-radius:999px;height:36px;min-height:36px;padding:0 18px;margin:0}
#send{background:var(--text);color:var(--bg);border-color:var(--text);font-weight:600}
#send:hover:not(:disabled){opacity:.85;color:var(--bg);border-color:var(--text)}
/* 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}
#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}
/* Reste de l'UI : coins arrondis et contrastes doux, dans le même esprit. */
button,.addbtn,.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,.gated-block,.lc-mode{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)}