mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +02:00
Quatre retouches d'interface : - Le sélecteur de l'en-tête liste maintenant les MODÈLES du disque (groupe « Modèles (.gguf) ») en plus des presets : en choisir un le charge — POST /api/models/use écrit MODEL seul (contexte, NGL et échantillonnage conservés) et redémarre le service en arrière-plan. Sans preset créé, le sélecteur n'offrait aucun choix. - Pourcentage de chargement : sur un redémarrage à chaud, le modèle est déjà dans le cache disque — llama-server ne lit rien (read_bytes reste à 0) et la pastille passait de « 0 % » à « prêt » sans jamais monter. On prend le plus avancé de read_bytes et de la mémoire résidente (VmRSS), qui grandit cache ou pas. - Discussions triées par date de CRÉATION (récentes en tête) : une discussion garde sa place, écrire dans un vieux fil ne le fait plus remonter. - Bouton Réglages ancré en pied de barre latérale, juste au-dessus du moniteur Performance — toujours au même endroit, quel que soit le nombre de discussions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2132 lines
150 KiB
CSS
2132 lines
150 KiB
CSS
/* ===== 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:3px;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:3px;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:4px}
|
||
.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:3px;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:4px;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:4px;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:3px;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:4px;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:3px;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:3px;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:4px;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:3px;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:3px;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:4px 4px 0 0}
|
||
.rowlist:last-child>.preset:last-child{border-radius:0 0 4px 4px}
|
||
.rowlist:first-child:last-child>.preset:only-child{border-radius:4px}
|
||
/* 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:4px;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:4px;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:4px;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:3px;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:4px;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:3px;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:3px;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:3px;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):not(.wclock){background:var(--accent)}
|
||
/* Compteur de travail sur l'indicateur « … » : il annule la mise en forme de
|
||
pastille que `.msg.typing span` applique à TOUT span de la bulle. */
|
||
.msg.typing .wclock{width:auto;height:auto;border-radius:0;background:none;animation:none;
|
||
font-family:var(--mono);font-size:11px;letter-spacing:.02em;color:var(--dim);
|
||
margin-left:7px;white-space:nowrap}
|
||
.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:4px;
|
||
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:3px;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:3px}
|
||
.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:4px;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épliables 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}
|
||
/* Raisonnement / outils : hauteur BORNÉE avec défilement interne. Un long
|
||
raisonnement (des milliers de tokens) faisait grandir la page de plusieurs
|
||
écrans ; la carte reste maintenant à taille fixe et suit la génération en
|
||
défilant toute seule (voir flushRender, 09-stream.js). L'animation de repli
|
||
garde son overflow:hidden via la hauteur inline — les deux cohabitent. */
|
||
.msg.reasoning .bodywrap,.msg.tool .bodywrap{max-height:280px;overflow-y:auto;overscroll-behavior:contain;scrollbar-width:thin}
|
||
.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:3px;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:4px;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:4px;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:4px;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:3px;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:3px;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:4px;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 4px 4px 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:4px;padding:3px;width:100%}
|
||
.pe-seg>label{flex:1;text-align:center;font-size:13px;font-weight:500;padding:7px 4px;border-radius:3px;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:3px;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:4px;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:4px;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:3px;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 5px 5px}
|
||
.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:3px;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:4px;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:4px;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:3px;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:3px;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:4px;border:1px solid var(--border);color:var(--dim);text-transform:uppercase;letter-spacing:.04em}
|
||
/* Version du moteur : même pastille, mais SANS majuscules forcées — un numéro
|
||
de build et un hash de commit se recopient et se comparent à l'œil, et
|
||
« AAAA111 » n'est pas le commit « aaaa111 ». Le monospace vient de là aussi. */
|
||
.eng-ver{font-family:var(--mono);text-transform:none;letter-spacing:0;color:var(--text)}
|
||
.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 */
|
||
/* Sélecteur d'intensité du raisonnement, dans la barre de saisie. Discret par
|
||
défaut (même poids visuel que les icônes voisines), il ne s'affirme qu'au
|
||
survol ou quand une intensité est imposée. */
|
||
#think-effort{flex:none;align-self:center;height:22px;padding:0 4px;font-size:11px;
|
||
color:var(--dim);background:transparent;border:1px solid transparent;border-radius:4px;cursor:pointer}
|
||
#think-effort:hover:not(:disabled){color:var(--text);border-color:var(--border)}
|
||
#think-effort:disabled{opacity:.4;cursor:not-allowed}
|
||
|
||
.mcp-actions{display:flex;gap:6px;margin-top:6px}
|
||
.mcp-cat-tabs{display:flex;flex-wrap:wrap;gap:4px;margin-bottom:8px}
|
||
.mcp-cat{font-size:11px;padding:2px 9px;border-radius:4px;border:1px solid var(--border);background:transparent;color:var(--dim);cursor:pointer}
|
||
.mcp-cat:hover{border-color:var(--accent);color:var(--text)}
|
||
.mcp-cat.on{border-color:var(--accent);color:var(--accent)}
|
||
.mcp-cat-list{display:flex;flex-direction:column;gap:4px;max-height:52vh;overflow:auto}
|
||
.mcp-cat-row{display:flex;align-items:center;gap:8px;padding:7px 10px;border:1px solid var(--border);border-radius:3px;background:var(--panel)}
|
||
.mcp-cat-row:hover{border-color:var(--accent)}
|
||
.mcp-cat-desc{font-size:11px;color:var(--dim);margin-top:2px;line-height:1.35}
|
||
.mcp-cat-cmd{font-size:10px;color:var(--dim);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:340px}
|
||
.mcp-cat-use{flex:none}
|
||
.mcp-tools-list{display:flex;flex-direction:column;gap:2px;max-height:240px;overflow:auto;border:1px solid var(--border);border-radius:3px;padding:6px}
|
||
.mcp-tool-row{display:flex;align-items:center;gap:10px;padding:4px 6px;border-radius:3px;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:4px;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:3px;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}
|
||
/* « Masquer la vitesse de génération » ne masque QUE la vitesse : le temps de
|
||
travail du tour (.worktime) n'est pas une mesure de moteur, c'est ce que la
|
||
réponse a coûté. Une ligne qui ne portait que de la vitesse disparaît en
|
||
entier — sans .has-work il ne resterait qu'un blanc. */
|
||
html[data-hide-stats="1"] #chat .msg .statline>.spd{display:none}
|
||
html[data-hide-stats="1"] #chat .msg .statline:not(.has-work){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:3px;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:3px;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:4px 4px 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 8px 8px;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. */
|
||
/* align-items:flex-start : depuis que la rangée peut contenir une VIGNETTE, une
|
||
pastille texte voisine s'étirait à sa hauteur (étirement par défaut du flex) —
|
||
et, arrondie à 999px, se retrouvait en gros ovale. Chaque objet garde sa
|
||
hauteur propre. */
|
||
#chat .msg-files{align-self:flex-end;display:flex;flex-wrap:wrap;justify-content:flex-end;
|
||
align-items:flex-start;gap:6px;max-width:75%}
|
||
#chat .msg-files .chip-file{background:var(--bubble);max-width:100%}
|
||
/* Vignette dans une pastille (composeur) : assez grande pour reconnaître la
|
||
capture qu'on vient de coller, assez petite pour que la rangée reste une
|
||
rangée de pastilles et pas une planche-contact. */
|
||
.chip-file .cf-thumb{flex:none;width:26px;height:26px;object-fit:cover;border-radius:3px;
|
||
background:var(--panel)}
|
||
.chip-file.has-thumb{padding-left:4px}
|
||
/* Pièce jointe IMAGE d'un message envoyé : la vignette remplace la pastille.
|
||
Plafonnée pour ne pas déséquilibrer le fil ; cliquable pour la voir en grand. */
|
||
#chat .msg-files .msg-img{display:flex;max-width:100%;border-radius:4px;overflow:hidden;
|
||
border:1px solid var(--border);background:var(--panel)}
|
||
/* margin/border remis à zéro : .chatimg (posée par hydrateImages) s'applique
|
||
aussi ici, or le cadre est déjà porté par la tuile. */
|
||
#chat .msg-files .msg-img img{display:block;max-width:200px;max-height:160px;
|
||
width:auto;height:auto;object-fit:contain;cursor:zoom-in;margin:0;border:none;border-radius:0}
|
||
/* Images posées par l'IA dans sa réponse (captures, /api/chat/image). Sans ce
|
||
plafond elles sortaient à leur taille NATIVE : une capture d'écran remplissait
|
||
la colonne entière et poussait le texte hors de vue. */
|
||
#chat .chatimg{display:block;max-width:100%;max-height:280px;width:auto;height:auto;
|
||
margin:6px 0;border:1px solid var(--border);border-radius:4px;cursor:zoom-in}
|
||
/* Loupe : l'image reprend toute la fenêtre, moins une marge. Le fond ferme au
|
||
clic (curseur zoom-out), l'image non — on veut pouvoir la regarder de près. */
|
||
#lightbox{position:fixed;inset:0;z-index:80;display:none;align-items:center;
|
||
justify-content:center;padding:28px;background:rgba(0,0,0,.72);cursor:zoom-out;
|
||
opacity:0;transition:opacity .16s ease}
|
||
#lightbox.show{opacity:1}
|
||
#lightbox img{max-width:100%;max-height:100%;width:auto;height:auto;object-fit:contain;
|
||
border-radius:4px;box-shadow:0 16px 50px rgba(0,0,0,.45);cursor:default}
|
||
#lightbox .lb-x{position:absolute;top:10px;right:14px;margin:0;padding:0;
|
||
width:36px;height:36px;display:flex;align-items:center;justify-content:center;
|
||
font-size:22px;line-height:1;border:none;border-radius:50%;
|
||
background:rgba(0,0,0,.35);color:#fff}
|
||
#lightbox .lb-x:hover{background:rgba(0,0,0,.6)}
|
||
#lightbox .lb-name{position:absolute;left:0;right:0;bottom:10px;text-align:center;
|
||
color:#fff;opacity:.72;font-size:12px;pointer-events:none}
|
||
@media (prefers-reduced-motion:reduce){#lightbox{transition:none}}
|
||
/* 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:3px;
|
||
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:4px}
|
||
/* Champ + bouton accolés : on ne garde l'arrondi que sur les coins extérieurs. */
|
||
.pe-inpbtn .pe-inp{border-radius:4px 0 0 4px}
|
||
.pe-inpbtn button{border-radius:0 4px 4px 0}
|
||
.input-btn .ctl{border-radius:4px 0 0 4px}
|
||
.input-btn .btn-in{border-radius:0 4px 4px 0}
|
||
.preset{border-radius:3px}
|
||
/* 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:3px}
|
||
.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:3px;
|
||
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:3px}
|
||
|
||
/* Colonne principale : une seule grande carte qui contient fil et saisie. */
|
||
.main{background:var(--panel);border-radius:3px;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:3px;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:4px}
|
||
}
|
||
/* 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:3px;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:3px;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)}
|
||
/* Verrou d'accès : ce n'est pas un verdict mémoire, mais ça se lit au même
|
||
endroit et ça décide autant de la suite — un dépôt gated ne se télécharge
|
||
pas sans jeton. */
|
||
.hf-fit.gated{color:var(--warn);border-color:var(--warn);text-transform:none}
|
||
.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é. */
|
||
/* Le panneau ne défile PAS en entier : seule la liste le fait. Avec un
|
||
overflow sur le panneau, l'en-tête, le fil d'Ariane et le pied (occupation
|
||
disque, bouton .zip) s'en allaient vers le haut dès qu'il y avait quelques
|
||
fichiers — on perdait les repères et les actions en même temps. */
|
||
#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:hidden;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;flex:none}
|
||
.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:4px}
|
||
#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;flex:none}
|
||
.files-crumb-b{margin:0;padding:2px 6px;min-height:0;font-size:11px;border:0;border-radius:3px;
|
||
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;overflow-y:auto;overflow-x:hidden;
|
||
/* La barre de défilement ne doit pas décoller les lignes du bord : on lui
|
||
rend la gouttière plutôt que d'ajouter une marge que le panneau sans
|
||
défilement paierait aussi. */
|
||
scrollbar-gutter:stable}
|
||
/* État vide : centré dans la hauteur disponible plutôt que collé en haut d'un
|
||
grand vide — c'est le cas le plus fréquent à l'ouverture d'une discussion. */
|
||
.files-empty{font-size:12px;line-height:1.5;padding:24px 2px;text-align:center;margin:auto 0}
|
||
/* 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:3px;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:3px;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:6px;border-top:1px solid var(--border);
|
||
display:flex;align-items:center;gap:6px;flex-wrap:wrap;flex:none;margin-top:auto}
|
||
/* 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:3px;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:3px;
|
||
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:4px;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:4px;
|
||
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}
|
||
/* Discussion au travail : un anneau qui tourne, à gauche du titre. C'est le seul
|
||
élément animé de la liste — il désigne sans ambiguïté le fil sur lequel un
|
||
agent est en train de travailler, y compris depuis un autre appareil. */
|
||
/* ⚠️ Sélecteur en DEUX classes : `.preset>span` (plus haut) pose flex:1 sur tout
|
||
span enfant d'une ligne — l'anneau prenait sinon la moitié de la largeur et
|
||
écrasait le titre, exactement comme l'heure avant lui. */
|
||
.preset>.conv-spin,#topbar>.conv-spin{flex:none;display:inline-flex;align-items:center;
|
||
justify-content:center;line-height:0;color:var(--accent)}
|
||
/* L'animation porte sur le CONTENEUR (élément HTML), pas sur le <svg> : sur un
|
||
élément SVG, l'origine de la rotation dépend de transform-box, dont le défaut
|
||
a changé au fil des versions — l'anneau restait figé chez certains. Un span
|
||
inline-flex tourne partout, sans ambiguïté. Les deux propriétés restent
|
||
posées sur le svg pour le cas où une règle le viserait encore. */
|
||
.conv-spin{animation:conv-spin 900ms linear infinite;transform-origin:center}
|
||
.conv-spin svg{transform-box:fill-box;transform-origin:center}
|
||
@keyframes conv-spin{to{transform:rotate(360deg)}}
|
||
/* Mouvement réduit : l'anneau ne tourne plus mais respire — l'information reste,
|
||
le mouvement de rotation disparaît. */
|
||
@media (prefers-reduced-motion:reduce){
|
||
.conv-spin{animation:conv-breathe 1.6s ease-in-out infinite}
|
||
@keyframes conv-breathe{0%,100%{opacity:1}50%{opacity:.25}}
|
||
}
|
||
/* Dans l'en-tête, l'anneau précède le titre : même gabarit que les boutons
|
||
voisins pour ne pas décaler la ligne quand il apparaît et disparaît. */
|
||
#topbar>.conv-spin{width:18px}
|
||
/* 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-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 : un cadre, et une micro-interaction au survol -----------
|
||
La refonte les avait voulus « sans contour » : contour transparent, et un
|
||
aplat pris sur --row-bg. Or c'est exactement le fond des blocs qui les
|
||
contiennent (.slist, .rowbody) : un bouton posé dessus n'avait plus ni trait
|
||
ni écart de valeur, et « démarrer / redémarrer / arrêter » ou « mettre à jour
|
||
le moteur » se lisaient comme du texte, pas comme des commandes.
|
||
On revient donc au cadre — celui d'avant la refonte : trait de 1 px et aplat
|
||
un ton AU-DESSUS du bloc, pour que le bouton se détache dans les deux cas.
|
||
La micro-interaction, elle, reste : elle est bonne, elle manquait juste de
|
||
quelque chose à animer.
|
||
⚠️ Ne concerne pas les boutons-icônes de la saisie et de l'en-tête (#send,
|
||
#stop, #attach, #menubtn…) : ils posent `border:0` PLUS BAS dans la feuille
|
||
et gardent leur allure nue, qui est la bonne pour un pictogramme seul. */
|
||
button{background:var(--panel);border-color:var(--border);border-radius:4px;
|
||
transition:background .15s ease,color .15s ease,border-color .15s ease,transform .08s ease}
|
||
button:hover{background:var(--row-bg);border-color:var(--dim);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:4px;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:4px;
|
||
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:4px;background:transparent;color:var(--dim);
|
||
transition:background .15s ease,color .15s ease,transform .08s ease}
|
||
/* Envoyer / arrêter : un seul des deux à la fois, décidé par data-busy (posé par
|
||
syncSendBtn). C'est la feuille qui garde la main sur `display` — un style
|
||
inline y substituait `inline-block`, et le centrage flex tombait avec lui. */
|
||
#stop{display:none}
|
||
html[data-busy="1"] #send{display:none}
|
||
html[data-busy="1"] #stop{display:inline-flex}
|
||
#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:3px 3px 0 0}
|
||
#ctxmeta{border-radius:0 0 5px 5px}
|
||
#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:3px}
|
||
.side #side-settings .slist{background:var(--row-bg);border-radius:3px}
|
||
/* 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:4px;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:4px;
|
||
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:4px;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:3px}
|
||
#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. */
|
||
/* Le panneau Fichiers porte les mêmes titres que la barre latérale : un seul
|
||
style de titre dans toute l'application, celui de la maquette. */
|
||
.stitle{font-family:var(--font);text-transform:none;letter-spacing:-.01em;
|
||
font-size:13.5px;font-weight:600;color:var(--text)}
|
||
.side-sep{font-family:var(--font);text-transform:none;letter-spacing:0;
|
||
font-size:12px;font-weight:600;color:var(--dim)}
|
||
#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}
|
||
.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}
|
||
|
||
/* --- Boutons-icônes : une seule géométrie --------------------------
|
||
Toutes ces commandes portaient un GLYPHE DE TEXTE, donc une taille et une
|
||
assiette décidées par la police du système. En SVG, elles se centrent
|
||
normalement — encore faut-il que leur bouton soit une boîte flex et non une
|
||
ligne de texte. */
|
||
/* ⚠️ On NE touche PAS à `display` ici : plusieurs de ces boutons s'affichent et
|
||
se masquent par ce biais (#menubtn et .preset-add sont en display:none au
|
||
repos, #scrollbtn aussi). Une règle de centrage qui pose display:inline-flex
|
||
les rendrait visibles en permanence — le ☰ s'est ainsi retrouvé par-dessus
|
||
l'en-tête sur grand écran. Le centrage passe donc par le rembourrage, que la
|
||
taille connue de chaque bouton rend exact. */
|
||
.modal-x,.mcp-edit,#files-refresh,#files-close,
|
||
.files-acts .iconbtn,.conv-acts .iconbtn{
|
||
display:inline-flex;align-items:center;justify-content:center;line-height:1}
|
||
#menubtn{padding:8px;line-height:0}
|
||
#scrollbtn{padding:9px;line-height:0}
|
||
.preset-add{padding:5px;line-height:0}
|
||
#menubtn svg,#scrollbtn svg,.preset-add svg{display:block;margin:0 auto}
|
||
.modal-x svg,.mcp-edit svg,#files-refresh svg,#files-close svg,.iconbtn svg{
|
||
display:block;width:auto;height:auto}
|
||
/* La pastille du fil d'Ariane des fichiers n'est plus un caractère : elle a la
|
||
largeur de son dessin. */
|
||
.files-ic{width:16px;height:16px;display:inline-flex;align-items:center;justify-content:center}
|
||
.files-ic svg{display:block}
|
||
.files-row.isdir .files-ic{color:var(--accent)}
|
||
/* « Vider la discussion » : icône + libellé, alignés comme un seul bloc. */
|
||
#chat-actions-clear,.row>button>svg{vertical-align:-2px}
|
||
.row>button:has(svg){display:inline-flex;align-items:center;gap:7px}
|
||
/* Le « + » des sections : même carré que les autres commandes de la barre. */
|
||
.preset-add{width:26px;height:26px;border-radius:4px}
|
||
/* Étiquettes de groupe : même traitement que les titres de section — la
|
||
maquette n'a qu'un style de titre, et le monospace en capitales de la charte
|
||
précédente criait au milieu des formulaires. */
|
||
.subhead,.io-cap,.pages-bar .pages-title,.skillshead>span{
|
||
font-family:var(--font);text-transform:none;letter-spacing:0;
|
||
font-size:12.5px;font-weight:600;color:var(--dim)}
|
||
|
||
/* --- Discussions et moniteur : le gabarit de carte des réglages ------
|
||
La maquette posait ces deux blocs à plat, pleine largeur. Dans l'application
|
||
réelle ils voisinent avec les sections de réglages, qui sont des CARTES : deux
|
||
traitements pour deux listes côte à côte, et la barre latérale se lisait comme
|
||
deux morceaux collés. On aligne donc sur la carte, qui est la forme dominante
|
||
de la colonne. */
|
||
#side-scroll{padding:0 12px 12px}
|
||
#side-scroll>details>summary,#side-settings>summary{padding:0 2px}
|
||
/* ⚠️ La gouttière INTÉRIEURE de la carte n'est pas décorative. En la mettant à
|
||
zéro (le retrait extérieur venant désormais de #side-scroll), les lignes
|
||
allaient d'un bord à l'autre : le chevron de chaque section tombait dans le
|
||
congé de 20 px, donc il en débordait visuellement sur la première et la
|
||
dernière ligne, et les filets de séparation venaient buter dans la courbe.
|
||
Le rayon paraissait « trop fort » alors que c'est le contenu qui n'avait plus
|
||
de quoi s'en écarter. On reprend le retrait d'avant la refonte (16 px), qui
|
||
laisse au congé la place de tourner à vide. */
|
||
#side-settings>.sidegroup{padding:2px 16px}
|
||
@media (max-width:720px){
|
||
#side-scroll{padding-left:calc(12px + env(safe-area-inset-left))}
|
||
}
|
||
#conv-search{width:100%;margin:0 0 8px}
|
||
/* La liste EST la carte, au MÊME gabarit que .sidegroup (les sections de
|
||
réglages) : fond de panneau, rayon de 20, ombre discrète. Les lignes sont
|
||
rognées par overflow au lieu d'arrondir la première et la dernière à la main. */
|
||
#conv-list{background:var(--panel);border-radius:3px;box-shadow:var(--shadow);
|
||
overflow:hidden;margin:0}
|
||
.side .preset{border-radius:0;margin:0;padding:10px 12px}
|
||
.side .preset:not(:last-child){border-bottom:1px solid var(--border)}
|
||
.side .preset:last-child{border-bottom:0}
|
||
/* Sélection : bande pleine largeur DANS la carte — un aplat suffit, un cadre
|
||
ferait un second contour à l'intérieur du premier. */
|
||
.side .preset.active{background:var(--row-bg-on)}
|
||
.side .preset:hover:not(.active){background:color-mix(in srgb,var(--text) 4%,transparent)}
|
||
.side .preset .conv-acts{background:var(--row-bg-on)}
|
||
.side .preset.active .conv-acts{background:var(--row-bg-on)}
|
||
.conv-empty{padding:12px}
|
||
/* Moniteur machine : titre en étiquette de groupe, jauges dans une carte — même
|
||
couple que « Paramètres » + sa liste dans les réglages. */
|
||
#side-perf{padding:12px 12px 12px;padding-bottom:calc(12px + env(safe-area-inset-bottom))}
|
||
.perf-head{margin:0 2px 7px;font-size:12.5px;font-weight:600;color:var(--dim)}
|
||
.perf-body{background:var(--panel);border-radius:3px;box-shadow:var(--shadow);
|
||
padding:14px 15px 3px}
|
||
/* ⚠️ Pas de remise à zéro sur :last-child — les jauges vivent dans DEUX
|
||
conteneurs (les cartes graphiques, puis la mémoire vive). La dernière jauge
|
||
de chaque conteneur s'y voyait retirer sa marge, et la mémoire vive venait
|
||
se coller à la VRAM. C'est le rembourrage bas de la carte qui absorbe la
|
||
dernière marge. */
|
||
.perf-row{margin:0 0 12px}
|
||
.perf-sub{padding:0 0 10px}
|
||
|
||
/* --- Mode Chat/Code (20-mode.js) ------------------------------------------
|
||
Sélecteur segmenté dans le pied de la carte du composeur, puce de
|
||
suggestion, panneau des critères, badge de rôle, carte de question. */
|
||
#mode-seg{display:inline-flex;border:1px solid var(--border);border-radius:3px;overflow:hidden;flex:none}
|
||
#mode-seg button{border:0;background:transparent;color:var(--dim);font-size:10px;letter-spacing:.04em;text-transform:uppercase;padding:2px 8px;cursor:pointer;font-family:inherit}
|
||
#mode-seg button.on{background:var(--accent-bg);color:var(--accent)}
|
||
#mode-seg button:not(.on):hover{color:var(--text)}
|
||
|
||
/* Bandeaux du composeur (chrono du tour, puce mode code, critères). Ils vivent
|
||
DANS #composer — qui est en position:absolute — et suivent donc sa colonne
|
||
(--col). Posés avant lui dans le DOM, ils restaient dans le flux de la page
|
||
et disparaissaient SOUS la carte de saisie. */
|
||
.composer-banner{margin:0 var(--col) 6px;padding:6px 12px;border:1px solid var(--border);border-radius:4px;background:var(--panel);color:var(--dim);font-size:12px;font-family:var(--mono)}
|
||
#turn-clock{display:flex;align-items:center;gap:6px;letter-spacing:.03em}
|
||
#turn-clock::before{content:'';width:6px;height:6px;border-radius:50%;background:var(--accent);animation:tcpulse 1.4s ease-in-out infinite}
|
||
#turn-clock.done{color:var(--accent)}
|
||
#turn-clock.done::before{animation:none;opacity:.6}
|
||
@keyframes tcpulse{0%,100%{opacity:.25}50%{opacity:1}}
|
||
|
||
#code-hint{display:flex;align-items:center;gap:8px;font-size:12px}
|
||
#code-hint button{border:1px solid var(--accent);background:transparent;color:var(--accent);border-radius:3px;padding:2px 8px;font-size:12px;cursor:pointer;font-family:inherit}
|
||
#code-hint button:hover{background:var(--accent-bg)}
|
||
#code-hint #code-hint-no{border-color:var(--border);color:var(--dim);margin-left:auto}
|
||
|
||
#criteria-box{padding:0;overflow:hidden;max-height:38vh;overflow-y:auto}
|
||
#criteria-box.allpass{border-color:color-mix(in srgb,var(--accent) 45%,var(--border))}
|
||
#criteria-head{padding:6px 12px;color:var(--dim);cursor:pointer;user-select:none;letter-spacing:.03em;position:sticky;top:0;background:var(--panel)}
|
||
#criteria-head::after{content:' ▾';opacity:.5}
|
||
#criteria-box.folded #criteria-head::after{content:' ▸'}
|
||
#criteria-box.allpass #criteria-head{color:var(--accent)}
|
||
#criteria-box.folded #criteria-list{display:none}
|
||
#criteria-list{list-style:none;margin:0;padding:0 12px 8px}
|
||
#criteria-list li{padding:2px 0;color:var(--text)}
|
||
#criteria-list li.crit-passed{color:var(--accent)}
|
||
#criteria-list li.crit-failed{color:var(--err,#b3564d)}
|
||
#criteria-list li.crit-pending{color:var(--dim)}
|
||
|
||
.rolebadge{display:inline-block;margin-left:6px;padding:0 6px;border:1px solid var(--accent);border-radius:3px;color:var(--accent);font-size:9px;letter-spacing:.05em;text-transform:uppercase;vertical-align:middle}
|
||
|
||
/* Dictée : anneau rouge pulsé pendant l'enregistrement, estompé pendant la
|
||
transcription. Le bouton garde le gabarit de ses voisins (joindre, fichiers). */
|
||
#mic.rec{color:var(--err);border-color:var(--err);animation:micpulse 1.2s ease-in-out infinite}
|
||
@keyframes micpulse{50%{box-shadow:0 0 0 5px color-mix(in srgb,var(--err) 25%,transparent)}}
|
||
#mic.busy{opacity:.55}
|
||
|
||
/* ── Réglages en modale ─────────────────────────────────────────────────────
|
||
Deux volets : nav des sections à gauche, LE panneau choisi à droite. Les
|
||
panneaux réutilisent tels quels les gabarits de la barre latérale (.slist,
|
||
.switchrow, .subhead…), seuls la coque et le routage sont neufs. */
|
||
#side-settings-btn{display:flex;align-items:center;gap:8px;width:100%;margin:0;padding:10px 14px;flex:none;
|
||
background:none;border:0;border-top:1px solid var(--border);border-radius:0;box-shadow:none;
|
||
font-size:13px;color:var(--text);cursor:pointer;text-align:left}
|
||
#side-settings-btn:hover{background:var(--bubble)}
|
||
#side-settings-btn svg{flex:none;opacity:.7}
|
||
.set-card{max-width:860px;height:min(660px,92vh)}
|
||
.set-shell{display:flex;flex:1;min-height:0}
|
||
.set-nav{flex:0 0 200px;border-right:1px solid var(--border);overflow-y:auto;
|
||
padding:10px 8px;display:flex;flex-direction:column;gap:2px}
|
||
.set-nav-h{font-size:10px;text-transform:uppercase;letter-spacing:.12em;color:var(--dim);margin:12px 8px 4px}
|
||
.set-nav-h:first-child{margin-top:2px}
|
||
.set-nav button{display:flex;align-items:center;gap:8px;width:100%;margin:0;padding:7px 10px;
|
||
background:none;border:0;border-radius:3px;box-shadow:none;
|
||
font-size:12.5px;color:var(--text);cursor:pointer;text-align:left}
|
||
.set-nav button:hover{background:var(--bubble)}
|
||
.set-nav button.active{background:var(--bubble);font-weight:600}
|
||
.set-nav button .sbadge{margin-left:auto}
|
||
.set-body{flex:1;min-width:0;overflow-y:auto;padding:16px}
|
||
.set-pane{display:none}
|
||
.set-pane.active{display:block}
|
||
.set-pane-h{display:flex;align-items:center;gap:8px;font-size:14px;font-weight:600;margin:0 0 12px}
|
||
.set-pane-h>span:first-child{flex:1;min-width:0}
|
||
/* Téléphone : la nav passe en rangée défilante au-dessus du panneau, et la
|
||
modale prend tout l'écran — un panneau de réglages ne se lit pas en timbre. */
|
||
@media(max-width:720px){
|
||
.set-ov{padding:0}
|
||
.set-card{max-width:none;height:100vh;height:100dvh;max-height:none;border-radius:0;border:0}
|
||
.set-shell{flex-direction:column}
|
||
.set-nav{flex:none;flex-direction:row;align-items:center;overflow-x:auto;overflow-y:hidden;
|
||
border-right:0;border-bottom:1px solid var(--border);padding:8px;white-space:nowrap}
|
||
.set-nav-h{display:none}
|
||
.set-nav button{width:auto;flex:none}
|
||
}
|
||
/* Nom du modèle qui a produit la réponse, à côté de « Loki ». Discret (gris,
|
||
pas de capitales forcées) et borné : un nom de .gguf peut être très long,
|
||
il s'ellipse au lieu de pousser l'étiquette hors de la bulle. */
|
||
.msg .label .modelbadge{display:inline-block;margin-left:6px;padding:0 5px;border:1px solid var(--border);border-radius:3px;color:var(--dim);font-size:9px;letter-spacing:.03em;text-transform:none;max-width:240px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;vertical-align:middle}
|
||
.msg[data-agent-role="verifier"]{border-left:2px solid var(--accent)}
|
||
|
||
.askcard .body{display:flex;flex-direction:column;gap:8px}
|
||
.askopts{display:flex;flex-wrap:wrap;gap:6px}
|
||
.askopts button{border:1px solid var(--accent);background:transparent;color:var(--accent);border-radius:3px;padding:4px 10px;font-size:13px;cursor:pointer;font-family:inherit}
|
||
.askopts button:hover:not(:disabled){background:var(--accent-bg)}
|
||
.askopts button:disabled{opacity:.5;cursor:default}
|
||
|
||
/* Capture d'écran rendue dans la bulle de l'outil (web_screenshot) : l'image
|
||
ne dépend plus du modèle qui pense — ou non — à la recopier dans sa réponse. */
|
||
.tool-shot{margin-top:8px}
|
||
.tool-shot img{max-width:100%;max-height:420px;border:1px solid var(--border);border-radius:4px;display:block;cursor:zoom-in}
|
||
|
||
.mcp-tag.mcp-tag-preset{flex:0 1 auto;min-width:2.5em}
|
||
.task-consigne{min-height:260px;resize:vertical;line-height:1.6;padding:12px}
|
||
|
||
/* Tâches planifiées (repris de l'amont AJEAN) : ligne en cours d'exécution + modale d'édition. */
|
||
.mcp-row.task-running{border-color:var(--accent);background:var(--accent-bg);cursor:default}
|
||
.task-dot-run{animation:taskpulse 1.1s ease-in-out infinite}
|
||
@keyframes taskpulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.35;transform:scale(.7)}}
|
||
.task-run-badge{font-size:10px;font-weight:600;color:var(--accent);letter-spacing:.02em}
|
||
.task-stop-btn{flex:none;margin:0;padding:3px 10px;font-size:11px;border-radius:6px;background:var(--danger-bg);border:1px solid var(--err);color:var(--err)}
|
||
.task-stop-btn:hover{background:var(--danger-bg);border-color:var(--err);color:var(--err);opacity:.85}
|
||
/* La modale des tâches emprunte tout le design de l'éditeur de preset (.pe-*) :
|
||
même largeur, même mise en carte. */
|
||
#task-modal .modal-card{max-width:520px;overflow:hidden}
|
||
#task-modal-status:empty{display:none}
|
||
/* Panneau « Dernier résultat » : le compte-rendu complet de l'IA, lisible et
|
||
défilable — pas une mini-ligne tronquée. Posé dans une carte .pe-list-like. */
|
||
.task-report{font-size:13px;line-height:1.55;color:var(--text);background:var(--panel);border:1px solid var(--border);border-radius:12px;padding:11px 14px;max-height:240px;overflow:auto;white-space:pre-wrap;word-break:break-word}
|
||
.task-report.report-err{border-color:var(--err);color:var(--err)}
|
||
.task-report.report-run{color:var(--dim);font-style:italic}
|
||
.task-report.report-empty{color:var(--dim)}
|
||
/* Compte-rendu rendu en markdown : c'est du HTML de blocs, donc plus de pre-wrap
|
||
(sinon les sauts de ligne du HTML généré ajoutent des lignes vides). Styles
|
||
alignés sur ceux des bulles de chat (.msg .body). */
|
||
.task-report.md-report{white-space:normal}
|
||
.md-report>*{margin:.6em 0}
|
||
.md-report>*:first-child{margin-top:0}
|
||
.md-report>*:last-child{margin-bottom:0}
|
||
.md-report h1,.md-report h2,.md-report h3,.md-report h4{font-weight:600;line-height:1.25;margin:.9em 0 .4em}
|
||
.md-report h1{font-size:1.2em}.md-report h2{font-size:1.1em}.md-report h3{font-size:1em}.md-report h4{font-size:.95em}
|
||
.md-report ul,.md-report ol{padding-left:1.4em;margin:.5em 0}
|
||
.md-report li{margin:.15em 0}
|
||
.md-report li>p{margin:0}
|
||
.md-report code{background:var(--code-bg);padding:1px 5px;border-radius:3px;font-size:.9em;word-break:break-word}
|
||
.md-report pre{background:var(--code-bg);border:1px solid var(--border);border-radius:6px;padding:10px;overflow-x:auto;max-width:100%;margin:.5em 0}
|
||
.md-report pre code{background:transparent;padding:0;font-size:.85em;line-height:1.4}
|
||
.md-report blockquote{border-left:3px solid var(--border);padding:0 0 0 10px;margin:.4em 0;color:var(--dim)}
|
||
.md-report a{color:var(--accent);text-decoration:none}
|
||
.md-report a:hover{text-decoration:underline}
|
||
.md-report table{border-collapse:collapse;margin:.5em 0}
|
||
.md-report th,.md-report td{border:1px solid var(--border);padding:4px 8px}
|
||
.md-report hr{border:none;border-top:1px solid var(--border);margin:.6em 0}
|