mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-16 11:49:43 +02:00
Installer un modèle demandait d'aller sur huggingface.co, de naviguer dans l'arborescence d'un dépôt et de coller un lien à la main. Rien ne disait si le fichier tiendrait en mémoire, et surtout rien ne reliait un modèle à SON projecteur vision : c'est ainsi qu'un mmproj-Qwen3VL-8B s'est retrouvé configuré pour un Qwen3.8-27B — deux modèles sans rapport, moteur qui démarre et ne voit rien. La recherche Hugging Face arrive dans l'éditeur de preset. Elle ne remonte que les dépôts GGUF ; déplier un dépôt montre ses quantifications avec leur taille et un verdict mémoire, et propose le projecteur vision DU MÊME DÉPÔT — le seul qui corresponde. Quand le dépôt n'en publie pas, Loki le dit au lieu d'aller en chercher un ailleurs. Le nouveau code ne télécharge rien : il produit des URL que le chemin existant consomme tel quel (normalizeHFURL, shardURLSet, sonde d'espace disque, reprise et annulation). Une file d'attente enchaîne modèle puis projecteur — le serveur ne mène qu'un transfert à la fois, et le champ Vision ne se remplit que si le modèle est déjà sélectionné. Trois familles de .gguf cohabitent dans un dépôt et ne veulent pas dire la même chose : mmproj-* (projecteur), mtp-* (poids de décodage spéculatif) et le modèle. Les deux premières ressemblent à un modèle ; les proposer en vrac, ce serait offrir de lancer llama-server sur un encodeur d'images. Les tranches d'une famille sont repliées en une entrée de taille TOTALE : annoncer 15 Go pour un modèle qui en occupe 45 promet une place qui n'existe pas. Le verdict mémoire s'appuie enfin sur le GPU. detectHardware() ne renvoyait que la RAM système alors que detectGPUs() existait déjà : sur un serveur à carte NVIDIA, le verdict se prononçait sur la mauvaise grandeur. Le coût du cache KV reste une estimation assumée — l'exact demanderait de parser l'en-tête GGUF — et l'interface l'annonce comme telle plutôt que d'afficher un chiffre faussement sûr. Le catalogue ajean.link disparaît. Sa route n'avait aucun consommateur (l'écran d'accueil qu'elle attendait n'a jamais existé), son repli embarqué proposait du Qwen2.5 de 2024, et un fork qui laisse le serveur de l'amont décider de ce qu'il propose n'est pas vraiment un fork. Une piste écartée en cours de route : marquer les dépôts « vision » d'après les tags Hugging Face. Ils mentent — des deux dépôts GGUF de Qwen3.8-27B qui publient tous deux un mmproj, seul ggml-org est taggé image-text-to-text. Une pastille sur l'un et pas sur l'autre aurait été pire que rien. La vision est donc déduite de la seule source qui ne se trompe pas : la présence d'un mmproj-*.gguf dans l'arborescence. Vérifié : 8 tests unitaires sur les arborescences réelles des deux dépôts, puis au navigateur contre le vrai Hugging Face — 25 dépôts trouvés, 3 quants listés sans aucun mtp ni mmproj, projecteur Q8_0 proposé et coché, verdicts affichés avec leur explication, modale dans l'écran. L'ordre de la file (modèle puis projecteur) est testé en interceptant les appels, sans transfert. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VueWA9xcYadaYq65tBisix
1397 lines
102 KiB
CSS
1397 lines
102 KiB
CSS
/* ===== Polices =====================================================
|
||
Bricolage Grotesque (titres et texte) + IBM Plex 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.
|
||
swap : le texte s'affiche tout de suite dans la police de repli, puis bascule
|
||
— jamais de blanc au chargement. */
|
||
@font-face{font-family:'Bricolage Grotesque';src:url('/fonts/bricolage.woff2') format('woff2');font-weight:400 700;font-display:swap;font-style:normal}
|
||
@font-face{font-family:'IBM Plex Mono';src:url('/fonts/plexmono-400.woff2') format('woff2');font-weight:400;font-display:swap;font-style:normal}
|
||
@font-face{font-family:'IBM Plex Mono';src:url('/fonts/plexmono-500.woff2') format('woff2');font-weight:500;font-display:swap;font-style:normal}
|
||
|
||
/* ===== Palette « Terre » ===========================================
|
||
Bruns chauds, vert sauge et argile. 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.
|
||
|
||
Rôles : --accent = sauge (sélection, marche, liens) · --warn = argile
|
||
(chiffres, mesures, avertissements) · --err = terre cuite (arrêt, danger).
|
||
--row-bg / --code-bg sont les deux fonds encastrés d'un panneau. */
|
||
:root{color-scheme:light;
|
||
--font:'Bricolage Grotesque',-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif;
|
||
--mono:'IBM Plex Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
|
||
--bg:#f3efe8;--panel:#ffffff;--bubble:#efe9df;--border:rgba(44,38,34,.12);
|
||
--text:#2c2622;--dim:#7a7069;
|
||
--accent:#5f7f52;--accent-bg:rgba(95,127,82,.12);
|
||
--row-bg:#f7f4ee;--row-bg-on:#f4f1ea;
|
||
--ok:#5f7f52;--warn:#a9662f;--err:#c05a3f;--mag:#a9662f;
|
||
--code-bg:#f7f4ee;--danger-bg:#f8e9e4;--warn-bg:#f6efe4;
|
||
--add:#5f7f52;--add-bg:rgba(95,127,82,.12);--del:#c05a3f;--del-bg:rgba(192,90,63,.12);
|
||
--shadow:0 1px 2px rgba(44,38,34,.06)}
|
||
[data-theme="dark"]{color-scheme:dark;
|
||
--bg:#211d1a;--panel:#2b2622;--bubble:#38312a;--border:rgba(236,229,221,.12);
|
||
--text:#ece5dd;--dim:#a99d92;
|
||
--accent:#9ab68c;--accent-bg:rgba(154,182,140,.15);
|
||
--row-bg:#332d28;--row-bg-on:#38312a;
|
||
--ok:#9ab68c;--warn:#d2a07a;--err:#c9705c;--mag:#d2a07a;
|
||
--code-bg:#211d1a;--danger-bg:#3a2621;--warn-bg:#332b23;
|
||
--add:#9ab68c;--add-bg:rgba(154,182,140,.15);--del:#c9705c;--del-bg:rgba(201,112,92,.15);
|
||
--shadow:none}
|
||
/* Safari iOS peint un rectangle gris/noir sur TOUT élément touché (bouton, lien,
|
||
résumé de section, bulle repliable…). C'est un retour tactile par défaut, hors
|
||
de notre charte et impossible à styler : on le coupe. Nos états :active/:focus
|
||
restent, eux, gérés élément par élément. */
|
||
*{box-sizing:border-box;-webkit-tap-highlight-color:transparent}
|
||
/* ⚠️ overflow:hidden sur html/body est INTERDIT EN PWA (tenté puis retiré le
|
||
2026-07-28) : en PWA iOS, si le document ne peut plus défiler, WebKit décale
|
||
TOUTE la mise en page à l'ouverture du clavier pour montrer le champ actif —
|
||
la carte de saisie se retrouve plaquée en haut de l'écran. Le rebond
|
||
élastique du document est le prix à payer ; il est cosmétique, le clavier
|
||
ne l'est pas.
|
||
Laisser le document défiler se paie cher, et EN PWA AUSSI : la coque y fait
|
||
956 pt (obligatoire pour que la carte atteigne le bas de l'écran) alors que la
|
||
fenêtre de mise en page n'en fait que 894 → il reste 62 pt de document
|
||
défilable en permanence, même avec un fil VIDE, et la carte de saisie — posée
|
||
en bottom:0 dans le body — part avec au lieu de rester collée en bas.
|
||
On coupe donc le défilement du document PARTOUT (2026-07-29). Le fil, lui,
|
||
défile toujours dans #chat, qui borne son rebond (overscroll:contain).
|
||
⚠️ SI LE CLAVIER iOS SE REMET À TOUT DÉCALER EN PWA, c'est ce overflow:hidden
|
||
qu'il faut rouvrir en premier — c'était le compromis d'origine. */
|
||
/* Hauteur de la coque : elle DIFFÈRE entre PWA et navigateur, il faut les deux.
|
||
- PWA (écran d'accueil) : 100vh, et SURTOUT ni 100dvh ni 100%. Mesuré sur
|
||
iPhone (2026-07-28) : la vue web couvre tout l'écran (956 pt — le contenu
|
||
passe bien sous l'horloge), mais la fenêtre de mise en page ne fait que
|
||
894 pt et reste calée en haut. Or 100dvh ET 100% valent tous deux 894 → il
|
||
restait une bande morte de 62 pt (= la zone sûre du haut) en bas de l'écran,
|
||
sous la carte de saisie. 100vh se réfère au GRAND viewport (956) : la seule
|
||
unité juste dans ce cas.
|
||
- Navigateur (barre d'URL + barre d'outils visibles) : 100vh vaut justement ce
|
||
GRAND viewport, donc la coque dépasse la zone réellement visible → le
|
||
document défile alors que le fil est vide, et le bas de la carte de saisie
|
||
(modèle / contexte) est coupé. Ici c'est 100dvh qu'il faut : la hauteur
|
||
visible du moment.
|
||
D'où : 100dvh par défaut, et 100vh UNIQUEMENT en PWA.
|
||
⚠️ Le tri se fait sur l'attribut html[data-pwa] (pose par le script du <head>),
|
||
PAS sur le media `display-mode:standalone` : WebKit adosse ce media au `display`
|
||
du MANIFESTE, or cette app n'en a pas (elle vit en apple-mobile-web-app-capable).
|
||
En PWA iOS il repond donc `browser`, la branche navigateur s'appliquait, et la
|
||
bande morte de 62 pt sous la carte de saisie etait de retour (constate le
|
||
2026-07-29).
|
||
⚠️ Ne pas y ajouter overflow:hidden : voir plus haut, ça casse le clavier iOS. */
|
||
body{margin:0;font-family:var(--font);background:var(--bg);color:var(--text);height:100vh;height:100dvh;overscroll-behavior:none;display:flex;font-size:14px}
|
||
html[data-pwa="1"] body{height:100vh}
|
||
/* Seul #chat défile ; le document, lui, est figé (cf. l'avertissement ci-dessus).
|
||
Exception PWA : on lui rend son défilement, sinon le clavier iOS casse tout. */
|
||
html,body{overflow:hidden}
|
||
/* PWA plein écran (viewport-fit=cover) : on décale le contenu des bords pour ne
|
||
pas passer sous l'encoche / la barre système / l'indicateur d'accueil. Les
|
||
env(safe-area-inset-*) valent 0 hors PWA, donc aucun impact ailleurs.
|
||
⚠️ PAS de padding-bas ici : le composer est posé en bottom:0 DANS le body, donc
|
||
une marge basse le décollerait du bord de l'écran et laisserait une bande de
|
||
fond visible sous la carte (visible seulement en PWA, où l'inset vaut ~34 px).
|
||
L'indicateur d'accueil est esquivé bloc par bloc, en padding INTERNE. */
|
||
body{padding:env(safe-area-inset-top) env(safe-area-inset-right) 0 env(safe-area-inset-left)}
|
||
/* Listes déroulantes : la LISTE ouverte n'hérite pas du fond de son <select>.
|
||
Plusieurs de nos select sont en background:transparent + color clair (chevron
|
||
discret) : en thème sombre, le navigateur peignait donc du texte clair sur le
|
||
fond blanc par défaut de la liste — illisible (signalé sur « Cache KV », mais
|
||
ça touchait TOUTES les listes déroulantes en sombre). On habille donc les
|
||
<option> eux-mêmes ; color-scheme (posé sur <html>) ne suffit pas dès que le
|
||
select porte des couleurs explicites. */
|
||
select{color-scheme:inherit}
|
||
option,optgroup{background:var(--panel);color:var(--text)}
|
||
.side{width:320px;border-right:1px solid var(--border);padding:16px;padding-bottom:calc(16px + env(safe-area-inset-bottom));overflow-y:auto;flex-shrink:0}
|
||
.main{flex:1;display:flex;flex-direction:column;min-width:0}
|
||
#menubtn{display:none;position:fixed;top:calc(10px + env(safe-area-inset-top));left:calc(10px + env(safe-area-inset-left));z-index:20;background:var(--panel);border:1px solid var(--border);color:var(--text);width:40px;height:40px;border-radius:6px;font-size:18px;cursor:pointer}
|
||
#backdrop{display:none;position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:10}
|
||
/* Pastille « installation en cours » : suit exactement les règles d'affichage
|
||
du bouton ☰ (elle ne sert que quand le panneau est en tiroir, donc caché). */
|
||
#lc-chip{display:none;position:fixed;top:calc(10px + env(safe-area-inset-top));left:calc(58px + env(safe-area-inset-left));z-index:20;max-width:60vw;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;background:var(--panel);border:1px solid var(--accent);color:var(--accent);height:40px;padding:0 12px;border-radius:6px;font-size:12px;cursor:pointer}
|
||
#lc-chip.failed{border-color:var(--err);color:var(--err)}
|
||
@media (max-width:720px){
|
||
.side{position:fixed;inset:0 auto 0 0;width:80%;max-width:320px;z-index:15;transform:translateX(-100%);transition:transform .2s;background:var(--bg);padding-top:calc(16px + env(safe-area-inset-top));padding-left:calc(16px + env(safe-area-inset-left))}
|
||
.side.open{transform:translateX(0)}
|
||
#menubtn,#backdrop.open{display:block}
|
||
#lc-chip:not([hidden]){display:block}
|
||
body.drawer-open #menubtn,body.drawer-open #lc-chip{display:none}
|
||
.main{width:100%}
|
||
#chat{padding:60px 8px 12px}
|
||
.msg{max-width:92%}
|
||
#chat .msg.assistant,#chat .msg.reasoning{max-width:100%}
|
||
#inputbar{padding:8px;gap:4px}
|
||
#send,#stop{padding:0 12px}
|
||
/* Petit écran (menu replié en tiroir) : on masque l'aide clavier — inutile et
|
||
ça prend de la place. Elle reste visible sur desktop. */
|
||
#sendhint{display:none}
|
||
/* …sauf quand elle annonce que le modèle charge : là c'est une information
|
||
d'état, pas un rappel de raccourci. */
|
||
#sendhint.waiting{display:block}
|
||
}
|
||
/* Desktop : menu plus large et bulles (badges) un peu plus grosses. */
|
||
@media (min-width:721px){
|
||
.side{width:380px}
|
||
.preset-meta .qtag,.preset-meta .rtag,.preset-meta .btag{font-size:11px;padding:2px 9px;border-radius:9px}
|
||
.preset-meta .qtag svg,.preset-meta .rtag svg,.preset-meta .btag svg{width:11px;height:11px}
|
||
}
|
||
h1{margin:0;font-size:18px;color:var(--accent)}
|
||
/* Logo : « Loki » dessiné en pixels (SVG 4x5 par lettre, trait d'un module). Le « A »
|
||
reste un bloc à part : il apparaît en glissant quand le mode agent est actif. */
|
||
#brand{display:inline-flex;align-items:center}
|
||
/* 17px : le logo pixel-art était minuscule à 10px, illisible sur un écran dense.
|
||
Multiple impair volontaire — la grille du SVG (5 px de haut) reste nette. */
|
||
#brand svg{height:17px;width:auto;display:block;shape-rendering:crispEdges;fill:currentColor}
|
||
#brand-word{display:inline-flex;color:var(--accent)}
|
||
#ver{font-size:10px;font-weight:400;color:var(--dim);margin-left:7px;letter-spacing:0;align-self:flex-end;line-height:1;position:relative;top:1px}
|
||
.sidehead{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:6px}
|
||
/* Attribution : Loki est un fork d'AJEAN (MIT). Discrète mais permanente, et le
|
||
nom du projet d'origine renvoie à son dépôt. */
|
||
.brand-fork{font-size:10px;color:var(--dim);margin:0 0 14px;line-height:1.3}
|
||
.brand-fork a{color:var(--dim);text-decoration:underline;text-underline-offset:2px}
|
||
.brand-fork a:hover{color:var(--accent)}
|
||
/* Discussions : mêmes lignes que les presets, plus deux actions au survol.
|
||
Elles restent visibles au clavier (:focus-within) pour rester atteignables
|
||
sans souris. */
|
||
.conv-acts{display:flex;gap:2px;opacity:0;transition:opacity .12s}
|
||
.preset:hover .conv-acts,.preset:focus-within .conv-acts{opacity:1}
|
||
.conv-acts .iconbtn{font-size:11px;line-height:1;padding:4px 6px;background:transparent;border:1px solid transparent;border-radius:6px;color:var(--dim);cursor:pointer}
|
||
.conv-acts .iconbtn:hover{border-color:var(--border);color:var(--text)}
|
||
/* Séparateur de groupe dans la barre latérale : sépare les réglages d'IA des
|
||
réglages d'application, sans imbriquer un second niveau de repli (illisible
|
||
dans une colonne aussi étroite). */
|
||
.side-sep{font-size:10px;text-transform:uppercase;letter-spacing:.12em;color:var(--dim);
|
||
margin:18px 0 8px;padding-top:12px;border-top:1px solid var(--border)}
|
||
/* Choix d'avatar : une grille de pastilles emoji, celle retenue encadrée. */
|
||
.avpick{display:flex;flex-wrap:wrap;gap:4px;margin-top:6px}
|
||
.avpick button{font-size:15px;line-height:1;padding:5px 6px;background:var(--panel);
|
||
border:1px solid var(--border);border-radius:8px;cursor:pointer}
|
||
.avpick button:hover{border-color:var(--accent)}
|
||
.avpick button.sel{border-color:var(--accent);background:color-mix(in srgb,var(--accent) 15%,transparent)}
|
||
/* Avatar + nom en tête de bulle : l'emoji garde sa taille propre, le nom hérite
|
||
du style de .label (petites capitales) mais sans forcer la casse du prénom. */
|
||
.msg .label .av{font-style:normal;font-size:13px;margin-right:5px;vertical-align:-1px}
|
||
.msg .label .who{text-transform:none;letter-spacing:.04em;font-size:11px}
|
||
/* Captures d'écran et images de l'agent : bornées à la largeur de la bulle, et
|
||
cliquables pour ouvrir en grand dans un onglet. */
|
||
.msg .body img{max-width:100%;height:auto;border-radius:8px;border:1px solid var(--border);display:block;margin:8px 0}
|
||
/* pastille d'état service, en haut à droite de la barre latérale */
|
||
.statuspill{display:inline-flex;align-items:center;gap:6px;font-size:11px;padding:3px 10px;border-radius:999px;border:1px solid var(--border);background:var(--panel);color:var(--dim);font-weight:600;white-space:nowrap;cursor:pointer}
|
||
.statuspill.ok{color:var(--ok);border-color:color-mix(in srgb,var(--ok) 45%,transparent);background:color-mix(in srgb,var(--ok) 12%,transparent)}
|
||
.statuspill.err{color:var(--err);border-color:color-mix(in srgb,var(--err) 45%,transparent);background:color-mix(in srgb,var(--err) 12%,transparent)}
|
||
.statuspill.loading{color:var(--warn);border-color:color-mix(in srgb,var(--warn) 45%,transparent);background:color-mix(in srgb,var(--warn) 12%,transparent)}
|
||
.statuspill.loading .dot{animation:pulse 1s ease-in-out infinite}
|
||
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.25}}
|
||
.statuspill .dot{width:7px;height:7px;border-radius:50%;background:currentColor}
|
||
/* en-tête + bouton d'ajout de skill (toujours visible) */
|
||
.skillshead{display:flex;align-items:center;justify-content:space-between;margin:12px 0 5px}
|
||
.skillshead>span{font-size:11.5px;color:var(--dim);font-weight:600}
|
||
.addbtn{font-size:11px;padding:4px 11px;background:var(--panel);color:var(--dim);border:1px solid var(--border);border-radius:999px;cursor:pointer;line-height:1.3;letter-spacing:.02em}
|
||
.addbtn:hover{border-color:var(--dim);color:var(--text);background:var(--accent-bg)}
|
||
.mem-count{font-size:10px;color:var(--dim);font-weight:400;letter-spacing:0;text-transform:none;font-family:var(--font);margin-left:4px}
|
||
/* Titre de sous-section (Mémoire / Accès internet) : simple libellé, PAS de trait
|
||
(le trait est réservé aux sections principales — cf. `details`). La séparation
|
||
visuelle vient de la carte de lignes qui suit (.slist). */
|
||
/* Barre « Voir la mémoire » : libellé à gauche, bouton « + ajouter » à droite.
|
||
Pas de chevron : la liste s'ouvre sous la ligne, ça se voit. */
|
||
.pages-bar{display:flex;align-items:center;gap:8px;cursor:pointer;user-select:none;padding:7px 0;margin-top:4px}
|
||
.pages-bar .pages-title{font-size:11.5px;color:var(--dim);font-weight:600}
|
||
.pages-bar .addbtn{margin-left:auto}
|
||
.mem-search{width:100%;box-sizing:border-box;background:var(--panel);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:5px 9px;font:inherit;font-size:12px;margin:0 0 6px}
|
||
.mem-search:focus{outline:none;border-color:var(--accent)}
|
||
.mem-list{max-height:240px;overflow-y:auto;overscroll-behavior:contain}
|
||
.mem-more{font-size:11px;color:var(--dim);text-align:center;padding:6px 0 2px}
|
||
.ask-overlay{position:fixed;inset:0;background:rgba(0,0,0,.45);z-index:60;display:flex;align-items:center;justify-content:center;padding:20px}
|
||
.ask-box{background:var(--bg);border:1px solid var(--border);border-radius:10px;width:100%;max-width:420px;padding:20px;box-shadow:0 12px 40px rgba(0,0,0,.18)}
|
||
.ask-title{font-size:15px;font-weight:600;color:var(--text);margin-bottom:8px}
|
||
.ask-msg{font-size:13px;color:var(--dim);line-height:1.5;white-space:pre-wrap;word-break:break-word}
|
||
.ask-input{width:100%;box-sizing:border-box;margin-top:12px;background:var(--panel);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:9px 11px;font:inherit;font-size:13px}
|
||
.ask-input:focus{outline:none;border-color:var(--accent)}
|
||
.ask-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:18px}
|
||
.ask-actions button{margin:0;padding:7px 16px}
|
||
.ask-ok{border-color:var(--accent);color:var(--accent)}
|
||
.ask-ok.danger{border-color:var(--err);color:var(--err)}
|
||
#brand-a{display:inline-flex;line-height:0;color:var(--text);margin-right:2px}
|
||
/* Mode agent : le A passe en couleur d'accent (il était masqué hors agent,
|
||
la marque se lisant alors « Loki »). L'état agent reste signalé par sa
|
||
pastille dans les réglages. */
|
||
#brand.agent #brand-a{color:var(--accent)}
|
||
/* interrupteur (switch) */
|
||
/* Sous-groupe du mode agent (Mémoire, Accès internet, Serveurs MCP, Paramètres).
|
||
MÊME grammaire que « Machine » ou « Apparence » : étiquette de groupe NUE
|
||
au-dessus, puis UNE carte .slist qui porte toutes les lignes. Auparavant
|
||
chaque sous-groupe était une carte qui embarquait son propre titre et du
|
||
contenu libre aux marges variables — quatre îlots décollés au lieu d'une
|
||
colonne régulière. Le grisé « agent désactivé » se pose sur le GROUPE
|
||
(étiquette comprise), plus sur la carte seule. */
|
||
.gated-group{margin:0 0 10px;transition:opacity .15s}
|
||
.gated-group.gated{opacity:.4;pointer-events:none;filter:grayscale(.4)}
|
||
.switch{position:relative;display:inline-block;width:36px;height:20px;flex:none}
|
||
.switch input{position:absolute;opacity:0;width:0;height:0}
|
||
/* Piste décochée : un gris franc (pas --border), sinon le pavé blanc ne se
|
||
détache pas du rail en mode clair. Même teinte que .pe-sl. */
|
||
.switch .slider{position:absolute;inset:0;background:color-mix(in srgb,var(--dim) 55%,transparent);border-radius:999px;transition:background .25s ease}
|
||
.switch .slider::before{content:"";position:absolute;width:14px;height:14px;left:3px;top:3px;background:#fff;border-radius:50%;transition:transform .25s ease;box-shadow:0 1px 2px rgba(0,0,0,.25)}
|
||
.switch input:checked + .slider{background:var(--accent)}
|
||
.switch input:checked + .slider::before{transform:translateX(16px);background:var(--bg)}
|
||
.switch input:focus-visible + .slider{box-shadow:0 0 0 2px var(--bg),0 0 0 4px var(--accent)}
|
||
h2{margin:20px 0 8px;font-size:11px;color:var(--dim);text-transform:uppercase;letter-spacing:.1em;font-weight:600}
|
||
details>summary::-webkit-details-marker{display:none}
|
||
.preset-add{display:none;font-size:14px;padding:0 8px;margin:0 8px 0 auto;line-height:1.4;background:var(--panel);color:var(--text);border:1px solid var(--border);border-radius:6px;cursor:pointer}
|
||
.preset-add:hover{border-color:var(--accent);color:var(--accent)}
|
||
details[open]>summary>.preset-add{display:inline-block}
|
||
.tag{display:inline-block;padding:2px 8px;border-radius:10px;font-size:11px;background:var(--panel);border:1px solid var(--border)}
|
||
.tag.ok{color:var(--ok);border-color:var(--ok)}
|
||
.tag.err{color:var(--err);border-color:var(--err)}
|
||
button{background:var(--panel);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:6px 10px;font:inherit;cursor:pointer;margin:2px}
|
||
button:hover{border-color:var(--accent);color:var(--accent)}
|
||
button:disabled{opacity:.5;cursor:not-allowed}
|
||
.row{display:flex;gap:4px;flex-wrap:wrap}
|
||
.preset{display:flex;align-items:center;justify-content:space-between;gap:6px;padding:6px 10px;border:1px solid var(--border);border-radius:6px;margin:4px 0;cursor:pointer;background:var(--panel)}
|
||
.preset:hover{border-color:var(--accent)}
|
||
.preset.active{border-color:var(--accent);color:var(--accent);border-width:1.5px;padding:5.5px 9.5px}
|
||
.preset>span{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||
.preset>button{flex-shrink:0}
|
||
/* Liste de lignes cliquables (presets, pages mémoire) posée à fleur de carte :
|
||
une pile de petites cartes bordées faisait « trop de carrés » là où le reste
|
||
du menu aligne des lignes séparées par un filet. L'actif se signale par un
|
||
voile et un liseré à gauche, plus par un cadre propre. */
|
||
.rowlist>.preset{border:0;border-radius:0;margin:0;padding:6px 10px;background:transparent}
|
||
.rowlist>.preset:not(:last-child){border-bottom:1px solid var(--border)}
|
||
.rowlist>.preset:hover{background:var(--row-bg)}
|
||
.rowlist>.preset.active{position:relative;background:var(--row-bg-on);color:var(--text);font-weight:600}
|
||
/* La carte (.slist) laisse volontairement déborder son contenu (menus), elle ne
|
||
rogne donc pas ses coins : c'est la LIGNE de bord qui porte l'arrondi, sinon
|
||
le voile dépasse l'angle de la carte. */
|
||
.rowlist:first-child>.preset:first-child{border-radius:8px 8px 0 0}
|
||
.rowlist:last-child>.preset:last-child{border-radius:0 0 8px 8px}
|
||
.rowlist:first-child:last-child>.preset:only-child{border-radius:8px}
|
||
/* Liseré de gauche : un trait DROIT (pas un arc) qui court sur TOUTE la hauteur
|
||
de la ligne. Donc pas de box-shadow interne (l'arrondi le courberait) mais un
|
||
élément à part, aux bords francs, que l'arrondi de la ligne de bord se
|
||
contente de ROGNER — il se perd dans l'angle au lieu de le suivre. */
|
||
.rowlist>.preset{overflow:hidden}
|
||
.rowlist>.preset.active::before,.rowlist>.preset.pending::before{content:"";position:absolute;
|
||
left:0;top:0;bottom:0;width:3px;background:var(--text)}
|
||
/* La liste est REDESSINÉE à chaque rafraîchissement : une transition ne jouerait
|
||
pas (l'élément vient de naître), d'où des @keyframes d'entrée. Le liseré entre
|
||
en glissant depuis la gauche, le voile et la puce arrivent en fondu.
|
||
⚠️ Elles ne sont posées QUE via .sel-anim, classe mise par loadPresets le seul
|
||
redessin où la sélection a bougé : sans ça elles repartaient à chaque
|
||
rafraîchissement et la puce clignotait en continu. */
|
||
@keyframes bar-in{from{transform:translateX(-100%)}to{transform:translateX(0)}}
|
||
@keyframes row-in{from{background:transparent}}
|
||
@keyframes dot-in{from{opacity:0;transform:scale(.3)}to{opacity:1;transform:scale(1)}}
|
||
.sel-anim.active::before,.sel-anim.pending::before{animation:bar-in .26s cubic-bezier(.2,.7,.3,1) both}
|
||
.rowlist>.preset.sel-anim.active,.rowlist>.preset.sel-anim.pending{animation:row-in .26s ease-out both}
|
||
.sel-anim .preset-dot{animation:dot-in .26s cubic-bezier(.2,1.5,.5,1) both}
|
||
/* Bascule en cours : la ligne visée s'allume DÉJÀ (le clic doit se voir tout de
|
||
suite), mais sa barre reste EN RETRAIT — même blanc, opacité plus basse, et
|
||
elle respire. Le service met plusieurs secondes à redémarrer : la barre pleine
|
||
(plus la puce) est réservée au preset vraiment actif. */
|
||
.rowlist>.preset.pending{position:relative;background:var(--row-bg);color:var(--text);font-weight:600}
|
||
.rowlist>.preset.pending::before{animation:bar-breathe 1.5s ease-in-out infinite}
|
||
.rowlist>.preset.sel-anim.pending::before{
|
||
animation:bar-in .26s cubic-bezier(.2,.7,.3,1) both,bar-breathe 1.5s ease-in-out .26s infinite}
|
||
@keyframes bar-breathe{0%,100%{opacity:.5}50%{opacity:.2}}
|
||
/* Ligne à deux étages (titre + étiquettes) : l'interligne interne suffit, on
|
||
resserre donc le haut et le bas. */
|
||
.rowlist>.preset .preset-meta{margin-top:4px}
|
||
/* Messages et « voir plus » de la liste : alignés sur le retrait des lignes. */
|
||
.rowlist>.muted,.rowlist>.mem-more{padding:9px 10px}
|
||
.preset-info{flex:1;min-width:0}
|
||
.preset-name{min-width:0;overflow-wrap:anywhere;word-break:break-word}
|
||
/* Puce du preset actif : disque dessiné en CSS (le caractère « ● » dépend de la
|
||
police du système — plus petit et plus bas sous Windows que sous macOS). */
|
||
.preset-dot{display:inline-block;width:7px;height:7px;border-radius:50%;
|
||
background:currentColor;margin:0 7px 0 0;flex-shrink:0;
|
||
/* ⚠️ PAS `vertical-align:middle` : ce mot-clé se cale sur la HAUTEUR D'X de la
|
||
police, qui n'est pas la même d'un système à l'autre (Segoe UI sous Windows
|
||
vs SF sous macOS) — la puce tombait trop bas sur PC. Un décalage EN LONGUEUR
|
||
part de la ligne de base et ne dépend que de la taille de texte : même
|
||
rendu partout. .34em ≈ milieu des capitales, moins la moitié du disque. */
|
||
vertical-align:calc(.34em - 3.5px)}
|
||
/* Icône d'engrenage : centrée verticalement sur la ligne (sa silhouette ronde se
|
||
lit bien au milieu de son coin, là où le crayon penché tirait de travers). */
|
||
.preset-edit{flex-shrink:0;display:inline-flex;align-items:center;justify-content:center;
|
||
margin:0 -2px 0 0;padding:4px;line-height:0;background:transparent;border:none;opacity:.55}
|
||
.preset-edit:hover{opacity:1;color:var(--accent);background:transparent}
|
||
.preset-meta{display:flex;align-items:center;gap:6px;margin-top:3px;flex-wrap:wrap}
|
||
.qtag{flex-shrink:0;font-size:9px;padding:1px 6px;border:1px solid var(--border);color:var(--dim);border-radius:8px;letter-spacing:.03em;text-transform:uppercase}
|
||
.rtag{flex-shrink:0;display:inline-flex;align-items:center;gap:3px;font-size:9px;padding:1px 6px;border:1px solid var(--border);color:var(--dim);border-radius:8px;letter-spacing:.03em}
|
||
.btag{flex-shrink:0;display:inline-flex;align-items:center;gap:3px;font-size:9px;padding:1px 6px;border:1px solid var(--border);color:var(--dim);border-radius:8px;letter-spacing:.03em}
|
||
/* Petit « ? » d'aide : bulle au survol/clic au lieu d'un pavé de texte permanent. */
|
||
.help{display:inline-flex;align-items:center;justify-content:center;width:15px;height:15px;margin-left:6px;flex-shrink:0;font-size:10px;font-weight:600;line-height:1;color:var(--dim);border:1px solid var(--border);border-radius:50%;cursor:help;user-select:none;vertical-align:middle}
|
||
.help:hover{color:var(--accent);border-color:var(--accent)}
|
||
/* La bulle est ancrée à la ligne (.switchrow position:relative), pas au « ? »,
|
||
pour rester alignée au bord gauche du panneau et ne pas déborder. */
|
||
.help>.tip{display:none;position:absolute;z-index:40;left:0;right:0;top:calc(100% + 2px);padding:8px 10px;background:var(--bg);border:1px solid var(--border);border-radius:6px;font-size:11px;font-weight:400;line-height:1.4;color:var(--text);box-shadow:0 4px 16px rgba(0,0,0,.15);white-space:normal;text-align:left;text-transform:none;letter-spacing:normal;font-family:var(--font);cursor:auto}
|
||
.help:hover>.tip,.help:focus>.tip,.help.open>.tip{display:block}
|
||
.kv{display:flex;justify-content:space-between;gap:10px;padding:9px 12px;border-bottom:1px solid var(--border);font-size:13px;min-height:38px;align-items:center}
|
||
.kv span:first-child{color:var(--text);flex-shrink:0}
|
||
.kv:last-child{border-bottom:0}
|
||
/* direction:rtl tronque par la GAUCHE (on garde la fin d'un chemin long), mais
|
||
inversait l'ordre des groupes : « 18,2 / 24 Go » s'affichait « Go 24 / 18,2 ».
|
||
unicode-bidi:plaintext rend chaque valeur dans son sens naturel tout en
|
||
gardant l'ancrage à droite. */
|
||
.kv span:last-child{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;direction:rtl;unicode-bidi:plaintext;text-align:right;min-width:0;color:var(--dim)}
|
||
.bar{height:5px;background:var(--border);border-radius:3px;overflow:hidden;margin:0}
|
||
.bar>div{height:100%;background:var(--accent)}
|
||
#chat{flex:1;min-height:0;overflow-y:auto;overscroll-behavior:contain;padding:20px;display:flex;flex-direction:column;gap:14px}
|
||
:root{--col:max(20px,calc(50% - 450px))}
|
||
@media (max-width:720px){#chat{padding:60px 8px 12px}}
|
||
.msg{max-width:80%;padding:10px 14px;border-radius:10px;white-space:pre-wrap;word-wrap:break-word;line-height:1.45}
|
||
.msg.assistant,.msg.reasoning{white-space:normal}
|
||
.msg .body{overflow-wrap:anywhere;word-break:break-word}
|
||
.msg.user{align-self:flex-end;background:var(--bubble);color:var(--text)}
|
||
.msg.user .label{color:var(--dim)}
|
||
.msg.assistant{align-self:flex-start;background:var(--panel);border:1px solid var(--border)}
|
||
.msg.assistant.errmsg{border-color:rgba(248,81,73,.55);background:rgba(248,81,73,.08);color:var(--err)}
|
||
.msg .body>*{margin:1em 0}
|
||
.msg .body>*:first-child{margin-top:0}
|
||
.msg .body>*:last-child{margin-bottom:0}
|
||
.msg .body h1,.msg .body h2,.msg .body h3,.msg .body h4{font-weight:600;line-height:1.25;margin:1.2em 0 .5em}
|
||
.msg .body h1{font-size:1.3em} .msg .body h2{font-size:1.15em} .msg .body h3{font-size:1em} .msg .body h4{font-size:.95em}
|
||
.msg .body ul,.msg .body ol{padding-left:1.4em}
|
||
.msg .body li{margin:.15em 0}
|
||
.msg .body li>p{margin:0}
|
||
.msg.assistant .body code{background:var(--code-bg);padding:1px 5px;border-radius:3px;font-size:.9em;overflow-wrap:anywhere;word-break:break-word}
|
||
.msg.assistant .body pre,.msg.reasoning .body pre{position:relative;background:var(--code-bg);border:1px solid var(--border);border-radius:6px;padding:10px;overflow-x:auto;max-width:100%;margin:.5em 0}
|
||
.copybtn{position:absolute;bottom:6px;right:6px;font-size:11px;padding:3px 8px;margin:0;background:var(--panel);border:1px solid var(--border);color:var(--dim);border-radius:5px;opacity:.5;transition:opacity .15s,color .15s}
|
||
.msg.assistant .body pre:hover .copybtn,.msg.reasoning .body pre:hover .copybtn{opacity:1}
|
||
.copybtn:hover{border-color:var(--accent);color:var(--accent)}
|
||
.copybtn.done{color:var(--ok);border-color:var(--ok);opacity:1}
|
||
.msg.assistant .body pre code,.msg.reasoning .body pre code{background:transparent;padding:0;font-size:.85em;line-height:1.4}
|
||
.msg.assistant .body blockquote{border-left:3px solid var(--border);padding:0 0 0 10px;margin:.4em 0;color:var(--dim)}
|
||
.msg.assistant .body a{color:var(--accent);text-decoration:none}
|
||
.msg.assistant .body a:hover{text-decoration:underline}
|
||
.msg.assistant .body table{border-collapse:collapse;margin:.5em 0}
|
||
.msg.assistant .body th,.msg.assistant .body td{border:1px solid var(--border);padding:4px 8px}
|
||
.msg.assistant .body hr{border:none;border-top:1px solid var(--border);margin:.6em 0}
|
||
#scrollbtn{position:absolute;bottom:80px;right:20px;background:var(--panel);border:1px solid var(--border);color:var(--accent);width:36px;height:36px;border-radius:50%;cursor:pointer;display:none;font-size:16px;z-index:5}
|
||
#scrollbtn.show{display:block}
|
||
.msg.reasoning{align-self:flex-start;background:transparent;border:1px dashed var(--border);color:var(--mag);font-size:12px;max-width:90%;overflow-wrap:anywhere;word-break:break-word}
|
||
.msg.tool{align-self:flex-start;background:transparent;border:1px dashed var(--border);max-width:90%;white-space:normal;padding:8px 12px;font-size:12px;overflow-wrap:anywhere;word-break:break-word}
|
||
.msg.tool .tool-head{font-size:12px;color:var(--accent);font-weight:600;margin-bottom:6px}
|
||
/* ===== Diff des écritures (outil edit / mémoire) ==================
|
||
Lignes ajoutées en vert, retirées en rouge, contexte neutre. Les couleurs
|
||
viennent de variables : chaque thème peut les ajuster (clair vs sombre). */
|
||
.msg.tool pre.diff{display:block;padding:0;overflow-x:auto;background:var(--code-bg)}
|
||
.msg.tool pre.diff .dl{display:block;padding:1px 10px;white-space:pre-wrap;word-break:break-word;color:var(--dim)}
|
||
.msg.tool pre.diff .dl.add{color:var(--add);background:var(--add-bg)}
|
||
.msg.tool pre.diff .dl.del{color:var(--del);background:var(--del-bg)}
|
||
/* Écriture en direct : le bloc se remplit ligne à ligne pendant que le modèle
|
||
tape. Seule la dernière ligne est en fondu (.fresh) — animer tout le bloc à
|
||
chaque nouvelle ligne le ferait clignoter. Hauteur bornée + scroll auto en
|
||
bas (voir 08-chat-render.js) pour qu'un long script ne pousse pas le fil. */
|
||
.msg.tool pre.diff.live .dl.fresh{animation:difffresh .18s ease-out}
|
||
@keyframes difffresh{from{opacity:0;transform:translateY(2px)}to{opacity:1;transform:none}}
|
||
@media (prefers-reduced-motion:reduce){.msg.tool pre.diff.live .dl.fresh{animation:none}}
|
||
.diff-count{margin-left:8px;letter-spacing:0;text-transform:none}
|
||
.diff-count .a{color:var(--add)}
|
||
.diff-count .d{color:var(--del)}
|
||
.msg.tool .tool-sub{font-size:10px;color:var(--dim);text-transform:uppercase;letter-spacing:.1em;margin:8px 0 4px}
|
||
.msg.tool pre{position:relative;background:var(--code-bg);border:1px solid var(--border);border-radius:6px;padding:8px 10px;overflow:auto;margin:0;max-height:280px}
|
||
.msg.tool pre code{background:transparent;padding:0;font-size:.82em;line-height:1.4;white-space:pre}
|
||
.msg.tool .tool-caret{color:var(--accent);animation:toolblink 1s step-end infinite}
|
||
.msg.tool .tool-wait{font-size:12px;color:var(--dim);margin-top:8px;animation:toolblink 1.2s ease-in-out infinite}
|
||
@keyframes toolblink{0%,100%{opacity:.45}50%{opacity:1}}
|
||
/* Indicateur « le modèle réfléchit » : 3 points animés avant le 1er token. */
|
||
.msg.typing{display:inline-flex;gap:5px;align-items:center;padding:14px 16px}
|
||
.msg.typing span{width:7px;height:7px;border-radius:50%;background:var(--dim);animation:typingdot 1.2s infinite ease-in-out}
|
||
.msg.typing span:nth-child(2){animation-delay:.16s}
|
||
.msg.typing span:nth-child(3){animation-delay:.32s}
|
||
@keyframes typingdot{0%,70%,100%{opacity:.25;transform:translateY(0)}35%{opacity:1;transform:translateY(-4px)}}
|
||
/* Compactage du contexte : le MÊME indicateur, juste étiqueté. Avant, une
|
||
mini-bannière au-dessus de la saisie doublonnait les 3 points animés — deux
|
||
éléments pour un seul état d'attente. Ici les points passent en accent et un
|
||
libellé apparaît à côté : un seul objet à l'écran, au bon endroit. */
|
||
.msg.typing.compacting span:not(.tlabel){background:var(--accent)}
|
||
.msg.typing .tlabel{width:auto;height:auto;border-radius:0;background:none;animation:tlabelfade 1.6s ease-in-out infinite;font-size:12px;color:var(--accent);margin-left:3px;white-space:nowrap}
|
||
@keyframes tlabelfade{0%,100%{opacity:.6}50%{opacity:1}}
|
||
/* Trace laissée DANS le fil quand le contexte a été compacté : un séparateur
|
||
discret, à sa place chronologique — un toast qui disparaît ne dit pas OÙ la
|
||
coupure a eu lieu. */
|
||
.compact-mark{display:flex;align-items:center;gap:10px;margin:14px 2px;font-size:11px;color:var(--dim);letter-spacing:.04em;animation:cmarkin .35s ease}
|
||
.compact-mark::before,.compact-mark::after{content:"";flex:1;height:1px;background:var(--border)}
|
||
@keyframes cmarkin{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:none}}
|
||
/* --- Message en attente d'accusé serveur ---------------------------------
|
||
Affiché DÈS l'appui sur envoyer, en gris et légèrement effacé : le message
|
||
ne disparaît plus le temps de l'aller-retour. La classe est retirée quand le
|
||
flux renvoie l'événement `user`, et la bulle reprend sa couleur pleine —
|
||
signal net que c'est bien parti. */
|
||
.msg.user.pending{opacity:.45;transition:opacity .2s ease}
|
||
.msg.user.pending .label{font-style:italic}
|
||
|
||
/* --- Voile de chargement du fil ------------------------------------------
|
||
Couvre la zone du chat tant que le replay n'est pas fini. Sans lui, une
|
||
connexion lente donne un chat vide indiscernable d'une conversation vide. */
|
||
#chat-loading{position:absolute;inset:0;z-index:6;display:none;flex-direction:column;
|
||
align-items:center;justify-content:center;gap:12px;background:var(--bg);
|
||
color:var(--dim);font-size:12px;letter-spacing:.04em;pointer-events:none}
|
||
#chat-loading.show{display:flex}
|
||
/* Accueil du fil vide. Posé en surimpression du chat (et non DANS le fil) pour ne
|
||
pas perturber le défilement ni le rendu du replay ; pointer-events:none pour
|
||
qu'il ne vole jamais un clic à la zone de saisie. Il vit sous le voile de
|
||
chargement (z-index plus bas) : jamais les deux visibles en même temps. */
|
||
#chat-empty{position:absolute;inset:0 0 150px;z-index:3;display:none;flex-direction:column;
|
||
align-items:center;justify-content:center;gap:10px;pointer-events:none;
|
||
opacity:0;transition:opacity .35s ease}
|
||
#chat-empty.show{display:flex;opacity:1}
|
||
/* Le clone perd les règles d'id de #brand (les id sont retirés), dont l'espace
|
||
entre le « A » et le mot : on le repose ici, à l'échelle du logo agrandi
|
||
(2 px pour 10 px de haut → 4 px pour 20 px), sinon le A s'encastre dans le J. */
|
||
#ce-logo{display:inline-flex;align-items:center;gap:4px;color:var(--text)}
|
||
#ce-logo svg{height:20px;width:auto;display:block;shape-rendering:crispEdges;fill:currentColor}
|
||
#ce-logo .ce-word{display:inline-flex;color:var(--accent)}
|
||
#ce-title{font-size:14px;color:var(--dim);letter-spacing:.03em}
|
||
.cl-dots{display:inline-flex;gap:5px;align-items:center}
|
||
.cl-dots span{width:7px;height:7px;border-radius:50%;background:var(--dim);animation:typingdot 1.2s infinite ease-in-out}
|
||
.cl-dots span:nth-child(2){animation-delay:.16s}
|
||
.cl-dots span:nth-child(3){animation-delay:.32s}
|
||
.msg .label{font-size:10px;color:var(--dim);text-transform:uppercase;letter-spacing:.1em;margin-bottom:4px;display:block}
|
||
/* Bulles repliables (reasoning / tool) : une fois la réponse arrivée, on les
|
||
replie en douceur pour désencombrer le fil, tout en gardant le label (et son
|
||
compteur de tokens) cliquable pour redéplier. */
|
||
.msg.collapsible .label{cursor:pointer;user-select:none;margin-bottom:0}
|
||
/* ===== Barre latérale ==============================================
|
||
Colonne de réglages dense : 12 sections repliées doivent tenir sans faire
|
||
défiler. Un seul gabarit, repris des listes de la modale de preset.
|
||
Hauteurs : en-tête de section 34 px, ligne de réglage 34 px. */
|
||
.side{padding:10px 12px}
|
||
/* ⚠️ Le raccourci `padding` ci-dessus écrase les esquives d'encoche posées plus
|
||
haut (règles tiroir). Quand la barre est un TIROIR (téléphone, ou mode
|
||
« barre en tiroir » sur ordinateur), elle est en position:fixed : le padding
|
||
du <body> ne la protège pas, il faut donc reposer les env() ici — sinon
|
||
« Loki » et la pastille d'état passent sous l'encoche en PWA iOS. */
|
||
@media (max-width:720px){
|
||
.side{padding:calc(10px + env(safe-area-inset-top)) 12px calc(10px + env(safe-area-inset-bottom)) calc(12px + env(safe-area-inset-left))}
|
||
}
|
||
[data-hide-side="1"] .side{padding:calc(10px + env(safe-area-inset-top)) 12px calc(10px + env(safe-area-inset-bottom)) calc(12px + env(safe-area-inset-left))}
|
||
.sidehead{margin-bottom:10px}
|
||
|
||
/* --- En-tête de section --- */
|
||
details{margin:0;border-top:1px solid var(--border)}
|
||
details:first-of-type{border-top:0}
|
||
details[open]{padding-bottom:10px}
|
||
details>summary{display:flex;align-items:center;gap:8px;height:34px;font-size:13px;font-weight:500;
|
||
color:var(--text);cursor:pointer;list-style:none;user-select:none}
|
||
.stitle{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||
/* Tous les titres du menu (sections ET étiquettes de groupe) s'écrivent de la
|
||
MÊME façon : monospace, capitales, même interlettrage. La casse vient du CSS,
|
||
pas du HTML, pour qu'aucun libellé ne puisse diverger. Seule la taille et la
|
||
couleur distinguent les deux niveaux. */
|
||
.stitle,.subhead,.io-cap,.pages-bar .pages-title,.skillshead>span{
|
||
font-family:var(--mono);text-transform:uppercase;letter-spacing:.08em}
|
||
/* Pastille d'état d'une section (mode agent, accès internet, MCP, accès distant).
|
||
Un point + un mot, dans une pilule — l'ancien couple « on / off » en étiquette
|
||
carrée se lisait mal et jurait avec le reste du menu. Actif = pilule pleine
|
||
(inversée), inactif = contour discret. */
|
||
.sbadge{flex:none;display:inline-flex;align-items:center;gap:5px;
|
||
font-family:var(--font);font-size:9.5px;font-weight:600;text-transform:uppercase;letter-spacing:.06em;
|
||
padding:2px 8px;border-radius:999px;border:1px solid var(--border);color:var(--dim);background:transparent;white-space:nowrap}
|
||
.sbadge::before{content:"";width:5px;height:5px;border-radius:50%;background:currentColor;opacity:.55}
|
||
.sbadge.on{background:var(--text);border-color:var(--text);color:var(--bg)}
|
||
.sbadge.on::before{opacity:1}
|
||
.sbadge.warn{color:var(--warn);border-color:color-mix(in srgb,var(--warn) 45%,transparent)}
|
||
.sbadge.err{color:var(--err);border-color:color-mix(in srgb,var(--err) 45%,transparent)}
|
||
.sbadge:empty{display:none}
|
||
/* Dans une étiquette de groupe, la pastille se plaque à DROITE (comme dans les
|
||
titres de section, où .stitle occupe la place) au lieu de coller au libellé.
|
||
L'ordre visuel reste : libellé · « ? » · … · pastille. */
|
||
.subhead .sbadge{margin-left:auto;order:2}
|
||
.subhead .help{order:1}
|
||
details>summary::-webkit-details-marker{display:none}
|
||
details>summary:hover{color:var(--dim)}
|
||
/* Chevron : un carré dont on n'affiche que deux côtés. Il pivote AUTOUR DE SON
|
||
CENTRE (aucune translation), sinon il se décale en tournant. */
|
||
details>summary::after{content:"";flex:none;width:6px;height:6px;
|
||
border-right:1.5px solid var(--dim);border-bottom:1.5px solid var(--dim);
|
||
transform:rotate(-45deg);transition:transform .18s ease}
|
||
details[open]>summary::after{transform:rotate(45deg)}
|
||
|
||
/* --- Étiquette de groupe : une seule règle pour toutes les variantes --- */
|
||
.subhead,.io-cap,.pages-bar .pages-title,.skillshead>span{position:relative;display:flex;align-items:center;
|
||
font-size:11px;font-weight:600;color:var(--dim);margin:10px 2px 5px}
|
||
.io-cap{margin:0 2px 5px}
|
||
/* ⚠️ APRÈS la règle commune ci-dessus, qui pose une marge haute/basse : dans une
|
||
LIGNE de carte, elle décalait le libellé vers le bas au lieu de le centrer. */
|
||
.pages-bar .pages-title{margin:0}
|
||
/* Étiquette de groupe en tête d'un sous-groupe du mode agent : elle suit une
|
||
carte, elle a donc besoin de son air normal — mais pas d'un cumul de marges. */
|
||
.gated-group>.subhead:first-child{margin-top:12px}
|
||
/* Une étiquette de groupe qui suit immédiatement le titre de section n'a pas
|
||
besoin de reprendre de l'air : le titre en a déjà donné. */
|
||
details>summary+.subhead{margin-top:4px}
|
||
.io-group{margin-top:8px}
|
||
|
||
/* --- Carte de liste --- */
|
||
/* overflow VISIBLE : les infobulles (« ? ») sont positionnées en absolu par
|
||
rapport à la ligne, un débordement caché les TRONQUERAIT — elles doivent
|
||
pouvoir déborder de la carte. Les lignes n'ont pas de fond propre, l'arrondi
|
||
n'a donc rien à rogner. */
|
||
.slist{background:var(--panel);border:1px solid var(--border);border-radius:9px;
|
||
overflow:visible;margin:0 0 8px}
|
||
.slist:last-child{margin-bottom:0}
|
||
/* Contenu libre (champ + boutons) : le rembourrage passe sur la carte. */
|
||
.slist.pad{padding:8px 10px}
|
||
.slist.pad>:first-child{margin-top:0}
|
||
.slist.pad>:last-child{margin-bottom:0}
|
||
/* Contenu libre posé DANS une carte de lignes (champs Crawl4AI, liste MCP,
|
||
pages mémoire) : même rembourrage et même filet de séparation qu'une ligne,
|
||
pour qu'il ne flotte pas au milieu de la carte. */
|
||
.slist>.rowbody{padding:9px 10px}
|
||
.slist>.rowbody:not(:first-child),.slist>.pages-bar:not(:first-child){border-top:1px solid var(--border)}
|
||
/* …sauf juste après une ligne de réglage : elle porte DÉJÀ son filet bas, les
|
||
deux se cumuleraient en un trait double. */
|
||
.slist>.switchrow+.rowbody,.slist>.switchrow+.pages-bar{border-top:0}
|
||
.slist>.rowbody>:first-child{margin-top:0}
|
||
.slist>.rowbody>:last-child{margin-bottom:0}
|
||
.slist>.pages-bar{min-height:34px;padding:4px 10px;margin-top:0}
|
||
/* Contrôle posé à droite d'une ligne (liste « mode » de la mémoire) : même place
|
||
que l'interrupteur, largeur ajustée au contenu. */
|
||
.switchrow>.sctl{margin-left:auto;order:2;width:auto;max-width:62%}
|
||
|
||
/* --- Ligne de réglage : hauteur constante, contrôle à droite --- */
|
||
.switchrow{position:relative;display:flex;align-items:center;gap:10px;min-height:34px;padding:6px 0;
|
||
font-size:12.5px;cursor:pointer;user-select:none}
|
||
.slist>.switchrow{padding:6px 10px}
|
||
.slist>.switchrow:not(:last-child){border-bottom:1px solid var(--border)}
|
||
.switchrow .switch{margin-left:auto;order:2;flex:none}
|
||
.switchrow>span:first-child{min-width:0}
|
||
|
||
/* --- Bloc de statistique (GPU, RAM) --- */
|
||
.stat{padding:9px 10px;display:flex;flex-direction:column;gap:6px}
|
||
.stat:not(:last-child){border-bottom:1px solid var(--border)}
|
||
/* La RAM vit dans son propre conteneur (masqué quand l'info manque) : le filet
|
||
de séparation se pose donc sur le conteneur, pas entre deux .stat frères. */
|
||
#ram-details{border-top:1px solid var(--border)}
|
||
.stat-h{display:flex;align-items:baseline;justify-content:space-between;gap:10px}
|
||
.stat-n{font-size:12.5px;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||
.stat-v{flex:none;font-size:12.5px;color:var(--text);font-variant-numeric:tabular-nums}
|
||
.stat-s{font-size:11px;color:var(--dim)}
|
||
|
||
/* --- Lignes clé / valeur (config active) --- */
|
||
.kv{display:flex;align-items:center;justify-content:space-between;gap:10px;min-height:34px;
|
||
padding:6px 10px;border-bottom:1px solid var(--border);font-size:12.5px}
|
||
.kv:last-child{border-bottom:0}
|
||
.kv span:first-child{color:var(--text);flex-shrink:0}
|
||
|
||
/* --- Champs et boutons --- */
|
||
.sctl,.io-input{width:100%;background:var(--bg);color:var(--text);border:1px solid var(--border);
|
||
border-radius:7px;padding:7px 9px;font:inherit;font-size:12.5px}
|
||
.sctl:focus,.io-input:focus{outline:none;border-color:var(--dim)}
|
||
textarea.sctl{min-height:76px;resize:vertical;line-height:1.5}
|
||
.side button:not(.preset-edit):not(.addbtn):not(.preset-add):not(.copybtn):not(.iconbtn){font-size:12px;padding:6px 10px;border-radius:7px}
|
||
.side .row{display:flex;gap:6px;align-items:center;margin:6px 0}
|
||
.side .row .sctl,.side .row .io-input{flex:1;min-width:0}
|
||
/* Bouton d'action à côté d'un champ (copier, ouvrir) : un CARRÉ à la hauteur du
|
||
champ, icône au trait centrée. Le glyphe seul (⧉, ↗) dans un bouton texte se
|
||
posait de travers et changeait de largeur d'une police à l'autre. */
|
||
.iconbtn{flex:none;display:inline-flex;align-items:center;justify-content:center;
|
||
width:34px;height:34px;padding:0;margin:0;color:var(--dim);background:var(--panel);
|
||
border:1px solid var(--border);border-radius:9px;cursor:pointer}
|
||
.iconbtn:hover{color:var(--text);border-color:var(--dim);background:var(--accent-bg)}
|
||
.iconbtn svg{width:15px;height:15px;display:block}
|
||
.side .btn-danger-o{border-color:var(--err);color:var(--err)}
|
||
.side .btn-danger-o:hover{background:var(--danger-bg);border-color:var(--err);color:var(--err)}
|
||
.side .muted{font-size:11.5px;line-height:1.5}
|
||
|
||
/* ===== Réglages d'affichage ======================================
|
||
Chaque option est un interrupteur indépendant (Apparence), porté par un
|
||
attribut sur <html> et persisté dans localStorage + /api/prefs :
|
||
data-hide-reasoning : masque les bulles de raisonnement
|
||
data-hide-tools : masque les bulles d'appel d'outil
|
||
data-fold-tools : les garde repliées (dépliables au clic)
|
||
data-hide-side : barre latérale en tiroir, même sur ordinateur
|
||
Quand raisonnement et outils sont masqués, l'activité reste signalée par
|
||
l'indicateur de frappe (.msg.typing) maintenu pendant tout le tour. */
|
||
[data-hide-reasoning="1"] #chat .msg.reasoning{display:none}
|
||
[data-hide-tools="1"] #chat .msg.tool{display:none}
|
||
/* Barre latérale en tiroir sur TOUTES les tailles d'écran : mêmes règles que
|
||
la version mobile, activées par l'attribut au lieu de la largeur. */
|
||
[data-hide-side="1"] .side{position:fixed;inset:0 auto 0 0;width:80%;max-width:320px;z-index:15;transform:translateX(-100%);transition:transform .2s;background:var(--bg);padding-top:calc(16px + env(safe-area-inset-top));padding-left:calc(16px + env(safe-area-inset-left));border-right:1px solid var(--border)}
|
||
@media (min-width:721px){[data-hide-side="1"] .side{width:380px;max-width:380px}}
|
||
/* Le bouton ☰ est posé au-dessus du fil : on dégage le haut de la
|
||
conversation pour qu'il ne recouvre aucun message. */
|
||
@media (min-width:721px){html[data-hide-side="1"] #chat{padding-top:60px}}
|
||
[data-hide-side="1"] .side.open{transform:translateX(0)}
|
||
[data-hide-side="1"] #menubtn{display:block}
|
||
[data-hide-side="1"] #lc-chip:not([hidden]){display:block}
|
||
[data-hide-side="1"] #backdrop.open{display:block}
|
||
[data-hide-side="1"] body.drawer-open #menubtn,[data-hide-side="1"] body.drawer-open #lc-chip{display:none}
|
||
[data-hide-side="1"] .main{width:100%}
|
||
/* Hauteur animée en JS (mesure scrollHeight → 0) plutôt que grid 1fr→0fr qui
|
||
peut « dépasser ». overflow:hidden clippe le contenu pendant tout le repli.
|
||
width revient à auto SANS délai à l'ouverture (pas de saut), et passe à 0
|
||
APRÈS le fold (width 0s .3s) à la fermeture pour rétrécir la bulle au label. */
|
||
/* height ET width sont animées (en px, pilotées en JS via scrollHeight/scrollWidth)
|
||
→ repli/dépli fluide en diagonale. overflow:hidden clippe pendant l'animation. */
|
||
.msg.collapsible .bodywrap{overflow:hidden;transition:height .42s cubic-bezier(.4,0,.2,1),width .42s cubic-bezier(.4,0,.2,1),opacity .3s ease,margin-top .42s cubic-bezier(.4,0,.2,1);opacity:1;margin-top:4px}
|
||
.msg.collapsible .bodywrap>.body{min-width:0}
|
||
.msg.collapsible.collapsed .bodywrap{opacity:0;margin-top:0}
|
||
/* Composer = un seul bloc cohérent : barre de contexte intégrée en tête (fine
|
||
ligne de progression pleine largeur affleurant le bord) + zone de saisie. */
|
||
#composer{border-top:1px solid var(--border);background:var(--panel)}
|
||
/* La jauge est un ENFANT de #ctxmeta (et non plus un frère au-dessus) : ainsi
|
||
elle peut être clippée par la forme arrondie du bloc — indispensable au
|
||
thème ChatGPT où elle sert de bord bas de carte. Position/rendu inchangés
|
||
pour les autres thèmes : filet collé en haut de la ligne de contexte. */
|
||
#ctxtrack{position:absolute;top:0;left:0;right:0;height:3px;background:var(--panel);overflow:hidden}
|
||
#ctx-fill{height:100%;width:0%;background:var(--ok,#3a7);transition:width .3s,background .3s}
|
||
#ctxmeta{position:relative;display:flex;align-items:center;justify-content:flex-end;gap:10px;padding:8px 12px 0}
|
||
#ctx-compact{display:none;margin:0;padding:2px 10px;font-size:10px;background:var(--warn-bg);border-color:var(--warn,#c93)}
|
||
#inputbar{padding:8px 12px;display:flex;gap:8px}
|
||
/* Astuce clavier sous la saisie (style repris du HTML : un thème doit pouvoir
|
||
changer ses marges, ce qu'un style en ligne empêchait). */
|
||
#sendhint{padding:0 12px 6px;font-size:10px;text-align:center;opacity:.7}
|
||
/* Moteur pas encore prêt : l'aide clavier cède la place à l'état du chargement. */
|
||
#sendhint.waiting{opacity:1;color:var(--dim)}
|
||
#input{flex:1;background:var(--panel);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:10px;font:inherit;resize:none;min-height:56px;max-height:200px;overflow-y:hidden}
|
||
#input:focus{outline:none;border-color:var(--accent)}
|
||
#send{padding:0 20px}
|
||
.muted{color:var(--dim);font-size:13px}
|
||
#toast{position:fixed;bottom:20px;left:50%;transform:translateX(-50%);z-index:50;background:var(--panel);border:1px solid var(--border);padding:8px 16px;border-radius:8px;opacity:0;transition:opacity .2s;pointer-events:none;box-shadow:0 8px 30px rgba(0,0,0,.18)}
|
||
#toast.show{opacity:1}
|
||
@keyframes spin{from{transform:rotate(0)}to{transform:rotate(360deg)}}
|
||
/* iOS Safari auto-zoome au focus sur tout champ < 16px. On force 16px sur les
|
||
appareils tactiles uniquement (le desktop garde son rendu, le pinch-zoom
|
||
manuel reste possible). */
|
||
@media (hover:none) and (pointer:coarse){
|
||
#input,#sysprompt,#modal input,#modal textarea,#modal select{font-size:16px}
|
||
}
|
||
/* Appareil TACTILE en PAYSAGE : on réduit nettement tout pour gagner de la place
|
||
(interface « un peu grosse » en horizontal, utile pour enregistrer des démos).
|
||
Détection par (hover:none)+(pointer:coarse)+landscape : infaillible sur tout
|
||
téléphone couché, sans dépendre d'un seuil de largeur/hauteur en pixels (les
|
||
iPhone Pro Max font 932px de large et ~430px de haut, ce qui piégeait les
|
||
seuils précédents). Le desktop (souris) n'est jamais touché.
|
||
NB : la règle iOS 16px anti-zoom plus haut est surchargée ici volontairement
|
||
pour #input — on garde une saisie compacte (le pinch-zoom manuel reste dispo). */
|
||
@media (hover:none) and (pointer:coarse) and (orientation:landscape){
|
||
body{font-size:11.5px}
|
||
/* Tiroir latéral : on décale le TIROIR ENTIER après l'encoche (left = inset)
|
||
au lieu de padder à l'intérieur — sinon grosse bordure vide à gauche qui
|
||
rétrécit le contenu. Padding interne normal → largeur utile pleine. */
|
||
.side{width:300px;max-width:300px;left:env(safe-area-inset-left);padding:calc(14px + env(safe-area-inset-top)) 14px calc(14px + env(safe-area-inset-bottom)) 14px}
|
||
#chat{padding:48px 10px 6px;gap:7px}
|
||
.msg{padding:6px 10px;border-radius:8px;max-width:78%}
|
||
#chat .msg.assistant,#chat .msg.reasoning{max-width:88%}
|
||
#inputbar{padding:5px 8px;gap:6px}
|
||
/* PAS de font-size < 16px ici : iOS Safari zoome à la sélection du champ. On
|
||
garde 16px (hérité de la règle anti-zoom) et on compacte via hauteur/padding. */
|
||
#input{min-height:34px;max-height:120px;padding:6px 9px}
|
||
#send,#stop{padding:0 14px}
|
||
#composer .muted{display:none}
|
||
#ctxmeta{padding:3px 10px 0}
|
||
h1{font-size:15px}
|
||
h2,details>summary{font-size:13px}
|
||
#menubtn{width:32px;height:32px;font-size:15px;top:calc(6px + env(safe-area-inset-top))}
|
||
#lc-chip{height:32px;font-size:11px;left:calc(46px + env(safe-area-inset-left));top:calc(6px + env(safe-area-inset-top))}
|
||
}
|
||
/* --- Backend llama.cpp (2 modes : rapide / optimisée) --------------------- */
|
||
.lc-head{font-size:12px;line-height:1.5}
|
||
/* Les trois modes sont des LIGNES de la carte d'installation (elles étaient des
|
||
cartes dans une carte) : pas de cadre propre, un filet entre elles, comme
|
||
n'importe quelle ligne de réglage du menu. */
|
||
.lc-modes{display:flex;flex-direction:column}
|
||
.lc-modes.busy{opacity:.5;pointer-events:none}
|
||
.slist>.lc-modes:not(:first-child){border-top:1px solid var(--border)}
|
||
.lc-mode{padding:10px;cursor:pointer;transition:background .12s}
|
||
.lc-mode:not(:last-child){border-bottom:1px solid var(--border)}
|
||
.lc-mode:hover{background:var(--accent-bg)}
|
||
.lc-mode-t{font-size:13px;font-weight:600;color:var(--text);display:flex;align-items:center;gap:6px}
|
||
/* « conseillé » : pilule PLEINE (texte inversé). En contour, elle héritait du
|
||
gris atténué du thème monochrome et devenait illisible en mode sombre. */
|
||
.lc-reco{font-size:9px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;background:var(--text);color:var(--bg);border:1px solid var(--text);border-radius:8px;padding:1px 6px}
|
||
.lc-mode-d{font-size:11px;color:var(--dim);margin-top:3px}
|
||
/* Hauteur réservée : la ligne d'état arrive après l'appel réseau, sans elle
|
||
les trois cartes grandissaient d'un cran une fois l'état connu. */
|
||
.lc-mode-state{font-size:11px;min-height:16px;margin-top:7px;display:flex;align-items:center;gap:12px;flex-wrap:wrap}
|
||
.lc-mode-go{color:var(--accent)}
|
||
.lc-mode-active-tag{color:var(--accent);font-weight:600}
|
||
.lc-update-link{color:var(--dim);text-decoration:none;cursor:pointer}
|
||
.lc-update-link:hover{color:var(--accent)}
|
||
/* Choix du moteur dans l'éditeur de modèle (3 options : rapide/optimisé/perso) */
|
||
.be-opts{display:flex;flex-direction:column;gap:7px;margin:6px 0 8px}
|
||
.be-opt{display:flex;align-items:center;gap:8px;font-size:13px;color:var(--text);cursor:pointer}
|
||
.be-opt input{margin:0;flex-shrink:0}
|
||
.be-note{font-size:11px;color:var(--dim);margin-left:auto}
|
||
|
||
/* ===== Éditeur de preset — listes groupées façon Réglages iOS ======== */
|
||
#modal .modal-card{max-width:520px;overflow:hidden}
|
||
.pe-body{padding:18px 16px 24px;display:flex;flex-direction:column;gap:26px;overflow-y:auto;overflow-x:hidden;flex:1;background:var(--bg)}
|
||
/* Nombres : pas de flèches natives (spinner), champ épuré. */
|
||
.pe-val[type=number]{-moz-appearance:textfield}
|
||
.pe-val::-webkit-outer-spin-button,.pe-val::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}
|
||
/* Un groupe = un petit titre + une liste encadrée aux coins arrondis. */
|
||
.pe-group{display:flex;flex-direction:column;gap:8px}
|
||
/* `display:flex` sur la classe l'emporte sur la règle par défaut du navigateur
|
||
pour [hidden] : sans cette ligne, masquer un groupe via l'attribut hidden
|
||
n'avait AUCUN effet visible (section « Cartes graphiques » qui restait
|
||
affichée avec une seule carte). */
|
||
.pe-group[hidden]{display:none}
|
||
.pe-gh{font-size:12px;font-weight:600;color:var(--dim);padding:0 6px}
|
||
.pe-list{background:var(--panel);border:1px solid var(--border);border-radius:12px;overflow:hidden}
|
||
/* Ligne : libellé à gauche, contrôle à droite, séparateur fin entre lignes. */
|
||
.pe-row{display:flex;align-items:center;gap:14px;min-height:48px;padding:9px 15px}
|
||
/* Séparateur porté par la ligne SUIVANTE (border-top), pas par la précédente
|
||
(border-bottom + :not(:last-child)) : `:last-child` compte aussi les lignes
|
||
masquées, donc une ligne repliée gardait sa bordure basse, qui doublait
|
||
celle de la carte. Avec border-top, la bordure est portée par l'élément
|
||
caché lui-même — invisible tant qu'il l'est. */
|
||
.pe-row + .pe-row{border-top:1px solid var(--border)}
|
||
.pe-row-l{font-size:14px;color:var(--text);flex:1;min-width:0;line-height:1.3}
|
||
.pe-row-l .pe-sub{display:block;font-size:11px;color:var(--dim);margin-top:2px}
|
||
.pe-row-c{flex:none;display:flex;align-items:center;gap:6px;max-width:62%;justify-content:flex-end}
|
||
/* Valeur éditable sans bordure, alignée à droite (nombre / texte). */
|
||
.pe-val{border:none;background:transparent;color:var(--accent);font:inherit;text-align:right;padding:4px 0;width:110px;max-width:44vw}
|
||
.pe-val::placeholder{color:var(--dim)}
|
||
.pe-val:focus{outline:none}
|
||
.pe-unit{font-size:12px;color:var(--dim)}
|
||
/* Select sans bordure, avec chevron discret. */
|
||
.pe-selc{position:relative;display:flex;align-items:center;max-width:100%}
|
||
.pe-selc select{appearance:none;-webkit-appearance:none;border:none;background:transparent;color:var(--accent);font:inherit;text-align:right;text-align-last:right;padding:4px 16px 4px 0;cursor:pointer;max-width:100%;text-overflow:ellipsis}
|
||
.pe-selc select:focus{outline:none}
|
||
.pe-selc::after{content:"›";position:absolute;right:0;transform:rotate(90deg);color:var(--dim);font-size:14px;pointer-events:none}
|
||
/* Variante pleine largeur : la liste native s'ouvre alignée à gauche de la
|
||
ligne (et non collée au bord droit) et le nom de fichier a la place de
|
||
s'afficher en entier. */
|
||
.pe-selc.wide{width:100%}
|
||
.pe-selc.wide select{flex:1;text-align:left;text-align-last:left;padding-right:18px}
|
||
.pe-selc.wide::after{right:2px}
|
||
/* Ligne « champ pleine largeur » (Nom, chemin, config brute…). */
|
||
.pe-row.stack{flex-direction:column;align-items:stretch;gap:8px}
|
||
/* Répartition entre cartes graphiques : UNE barre, sur laquelle on glisse
|
||
directement. Le <input type=range> est posé par-dessus, piste transparente :
|
||
sinon on voyait deux barres superposées (la barre de proportion + la piste
|
||
blanche native du navigateur). Deux teintes du MÊME accent, pas deux
|
||
couleurs — la barre exprime une proportion, pas deux catégories. */
|
||
.gpu-split{position:relative;display:flex;align-items:center;height:22px}
|
||
.gpu-bar{display:flex;width:100%;height:10px;border-radius:999px;overflow:hidden;background:color-mix(in srgb,var(--dim) 20%,transparent)}
|
||
.gpu-bar>span{background:var(--accent);flex:none;min-width:0;transition:width .12s}
|
||
.gpu-bar>span.alt{background:color-mix(in srgb,var(--accent) 42%,transparent)}
|
||
.gpu-range{position:absolute;inset:0;width:100%;height:22px;margin:0;padding:0;background:transparent;-webkit-appearance:none;appearance:none;cursor:ew-resize}
|
||
.gpu-range:focus{outline:none}
|
||
.gpu-range::-webkit-slider-runnable-track{height:22px;background:transparent;border:none}
|
||
.gpu-range::-moz-range-track{height:22px;background:transparent;border:none}
|
||
.gpu-range::-moz-range-progress{background:transparent}
|
||
.gpu-range::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:16px;height:16px;margin-top:3px;border-radius:50%;background:var(--bg);border:2px solid var(--accent);box-shadow:0 1px 3px rgba(0,0,0,.3);cursor:ew-resize}
|
||
.gpu-range::-moz-range-thumb{width:16px;height:16px;border-radius:50%;background:var(--bg);border:2px solid var(--accent);box-shadow:0 1px 3px rgba(0,0,0,.3);cursor:ew-resize}
|
||
.gpu-range:focus-visible::-webkit-slider-thumb{box-shadow:0 0 0 3px var(--accent-bg)}
|
||
.gpu-range:focus-visible::-moz-range-thumb{box-shadow:0 0 0 3px var(--accent-bg)}
|
||
.gpu-legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:11.5px;color:var(--dim)}
|
||
.gpu-legend i{display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:5px}
|
||
.gpu-legend i.alt{background:color-mix(in srgb,var(--accent) 42%,transparent)}
|
||
.gpu-shares{display:flex;flex-wrap:wrap;gap:10px}
|
||
.gpu-share{display:flex;align-items:center;gap:5px;font-size:12px;color:var(--dim)}
|
||
.gpu-share input{width:62px;background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:7px;padding:5px 7px;font:inherit;text-align:right}
|
||
.gpu-share input:focus{outline:none;border-color:var(--accent)}
|
||
.pe-inp{width:100%;box-sizing:border-box;background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:9px 11px;font:inherit;transition:border-color .12s,box-shadow .12s}
|
||
.pe-inp:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-bg)}
|
||
.pe-row.stack.plain .pe-inp{border:none;background:transparent;padding:6px 0;font-size:15px}
|
||
.pe-row.stack.plain .pe-inp:focus{box-shadow:none}
|
||
/* Champ + bouton accolé (téléchargement) — même trait que les autres champs,
|
||
sans pastille colorée qui ressort. */
|
||
.pe-inpbtn{display:flex;align-items:stretch}
|
||
.pe-inpbtn .pe-inp{border-top-right-radius:0;border-bottom-right-radius:0;border-right:0}
|
||
/* Le bouton reprend EXACTEMENT le cadre du champ (mêmes coins, même bordure) :
|
||
sans ça, un thème qui arrondit tous les boutons (ChatGPT) le détachait du
|
||
champ, avec un coin rond au milieu de la paire. */
|
||
.pe-inpbtn button{flex:none;margin:0;border:1px solid var(--border);border-radius:0 8px 8px 0;background:var(--panel);color:var(--dim);padding:0 15px;cursor:pointer;font-size:15px;transition:color .12s,border-color .12s,background .12s}
|
||
.pe-inpbtn button:hover:not(:disabled){color:var(--bg);background:var(--accent);border-color:var(--accent)}
|
||
.pe-inpbtn:focus-within .pe-inp,.pe-inpbtn:focus-within button{border-color:var(--accent)}
|
||
.pe-inpbtn button:disabled{opacity:.5;cursor:default}
|
||
/* Contrôle segmenté (moteur) façon iOS. */
|
||
.pe-seg{display:flex;gap:3px;background:color-mix(in srgb,var(--dim) 16%,transparent);border-radius:9px;padding:3px;width:100%}
|
||
.pe-seg>label{flex:1;text-align:center;font-size:13px;font-weight:500;padding:7px 4px;border-radius:7px;cursor:pointer;color:var(--dim);transition:background .12s,color .12s}
|
||
.pe-seg>label:has(input:checked){background:var(--panel);color:var(--text);box-shadow:0 1px 2px rgba(0,0,0,.22)}
|
||
.pe-seg input{display:none}
|
||
.pe-seg .be-note{display:none}
|
||
.pe-seg-note{font-size:11px;color:var(--dim);padding:0 2px;min-height:14px}
|
||
.pe-note{font-size:11px;color:var(--dim);line-height:1.45}
|
||
/* Barre de progression (téléchargement de modèle). Sans total connu, elle
|
||
défile en boucle via .indet. */
|
||
.pe-bar{height:4px;border-radius:999px;background:color-mix(in srgb,var(--dim) 26%,transparent);overflow:hidden}
|
||
.pe-bar>i{display:block;height:100%;width:0;border-radius:999px;background:var(--accent);transition:width .35s linear}
|
||
.pe-bar.indet>i{width:35%;animation:pe-bar-slide 1.2s ease-in-out infinite;transition:none}
|
||
.pe-bar.done>i{width:100%}
|
||
@keyframes pe-bar-slide{0%{margin-left:-35%}100%{margin-left:100%}}
|
||
/* Action discrète en texte (annuler un téléchargement…). */
|
||
.pe-link{align-self:flex-start;border:none;background:none;padding:0;font:inherit;font-size:11px;color:var(--dim);text-decoration:underline;cursor:pointer}
|
||
.pe-link:hover{color:var(--text)}
|
||
.pe-link:disabled{opacity:.5;cursor:default;text-decoration:none}
|
||
/* Interrupteur à glissière. */
|
||
.pe-switch{position:relative;flex:none;width:40px;height:24px}
|
||
.pe-switch input{position:absolute;opacity:0;width:0;height:0}
|
||
.pe-sl{position:absolute;inset:0;background:color-mix(in srgb,var(--dim) 55%,transparent);border-radius:999px;transition:background .18s}
|
||
.pe-sl::before{content:"";position:absolute;width:18px;height:18px;left:3px;top:3px;background:#fff;border-radius:50%;transition:transform .18s;box-shadow:0 1px 3px rgba(0,0,0,.25)}
|
||
.pe-switch input:checked+.pe-sl{background:var(--accent)}
|
||
.pe-switch input:checked+.pe-sl::before{transform:translateX(16px);background:var(--bg)}
|
||
.pe-switch input:focus-visible+.pe-sl{box-shadow:0 0 0 3px var(--accent-bg)}
|
||
/* Repli « avancé » / config brute : une ligne cliquable dans un groupe. */
|
||
.pe-adv{background:var(--panel);border:1px solid var(--border);border-radius:12px;overflow:hidden}
|
||
.pe-adv>summary{cursor:pointer;font-size:14px;color:var(--text);padding:13px 15px;list-style:none;display:flex;align-items:center;user-select:none}
|
||
.pe-adv>summary::-webkit-details-marker{display:none}
|
||
.pe-adv>summary::after{content:"";margin-left:auto;width:7px;height:7px;border-right:1.5px solid var(--dim);border-bottom:1.5px solid var(--dim);transform:translateY(-2px) rotate(45deg);transition:transform .15s}
|
||
.pe-adv[open]>summary::after{transform:translateY(1px) rotate(-135deg)}
|
||
.pe-adv[open]>summary{border-bottom:1px solid var(--border)}
|
||
.pe-adv .pe-adv-body{padding:14px 15px;display:flex;flex-direction:column;gap:10px}
|
||
.pe-adv textarea.pe-inp{min-height:210px;resize:vertical;line-height:1.5;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;white-space:pre-wrap;overflow-wrap:anywhere;overflow-x:hidden}
|
||
/* Ligne « éditer le .env » : chevron de disclosure + textarea de config brute. */
|
||
.pe-caret{display:inline-block;width:7px;height:7px;border-right:1.5px solid var(--dim);border-bottom:1.5px solid var(--dim);transform:rotate(-45deg);transition:transform .15s}
|
||
.pe-caret.open{transform:rotate(45deg)}
|
||
#m-content{min-height:220px;resize:vertical;line-height:1.5;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;white-space:pre-wrap;overflow-wrap:anywhere;overflow-x:hidden}
|
||
/* Nombres avancés : chaque paire label/valeur sur sa propre ligne (réutilise pe-row). */
|
||
.pe-adv .pe-list{margin:0}
|
||
/* Variante « champ en clair » : page mémoire → le contenu est le champ principal. */
|
||
.pe-adv.pe-raw-plain{border:none;background:transparent}
|
||
.pe-adv.pe-raw-plain>summary{display:none}
|
||
.pe-adv.pe-raw-plain .pe-adv-body{padding:0}
|
||
/* Pied de modale. */
|
||
.pe-foot{padding:9px 12px;border-top:1px solid var(--border);display:flex;justify-content:space-between;
|
||
align-items:center;gap:8px;flex-wrap:nowrap}
|
||
.pe-foot .foot-l{display:flex;align-items:center;gap:8px;min-width:0}
|
||
.pe-foot .foot-r{display:flex;gap:8px;flex:none}
|
||
.pe-btn{margin:0;padding:7px 15px;font-size:12.5px;border-radius:8px;font-weight:600;cursor:pointer;
|
||
border:1px solid var(--border);background:var(--panel);color:var(--text);white-space:nowrap}
|
||
.pe-btn:hover{border-color:var(--dim)}
|
||
.pe-btn.primary{border-color:var(--accent);background:var(--accent);color:var(--bg)}
|
||
.pe-btn.primary:hover{filter:brightness(1.08)}
|
||
/* Suppression : icône seule, en retrait. Un bouton plein « supprimer » pesait
|
||
autant que « enregistrer » alors que c'est l'action rare — et à trois boutons
|
||
larges, le pied passait à deux lignes sur téléphone. */
|
||
.pe-del{flex:none;display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;
|
||
margin:0;padding:0;border:1px solid transparent;border-radius:8px;background:transparent;color:var(--err);cursor:pointer}
|
||
.pe-del svg{width:15px;height:15px;display:block}
|
||
.pe-del:hover{background:var(--danger-bg);border-color:color-mix(in srgb,var(--err) 40%,transparent)}
|
||
/* Option d'une confirmation : une PASTILLE à cocher, pas une case système suivie
|
||
d'un bout de texte. Éteinte : contour discret. Allumée : elle passe au rouge,
|
||
comme l'action destructrice qu'elle étend. Vit dans la boîte de confirmation
|
||
(#ask-check), plus dans le pied de l'éditeur. */
|
||
.pe-delmodel{align-self:flex-start;margin-top:14px;display:inline-flex;align-items:center;gap:6px;font-size:11px;font-weight:600;
|
||
letter-spacing:.02em;color:var(--dim);cursor:pointer;white-space:nowrap;
|
||
padding:5px 11px;border:1px solid var(--border);border-radius:999px;background:var(--panel);
|
||
transition:color .12s,border-color .12s,background .12s}
|
||
.pe-delmodel input{position:absolute;opacity:0;width:0;height:0}
|
||
/* Le point tient lieu de case à cocher : creux au repos, plein une fois coché. */
|
||
.pe-delmodel::before{content:"";width:6px;height:6px;border-radius:50%;flex:none;
|
||
border:1px solid currentColor;background:transparent}
|
||
.pe-delmodel:hover{color:var(--text);border-color:var(--dim)}
|
||
.pe-delmodel:has(input:checked){color:var(--err);border-color:color-mix(in srgb,var(--err) 45%,transparent);background:var(--danger-bg)}
|
||
.pe-delmodel:has(input:checked)::before{background:currentColor}
|
||
/* Éditeur de preset/mémoire — mise en forme propre du modale */
|
||
.modal-ov{position:fixed;inset:0;background:rgba(0,0,0,.45);z-index:30;align-items:center;justify-content:center;padding:20px}
|
||
/* Ouverture/fermeture animées. L'état par défaut est l'état FERMÉ ; la classe
|
||
.show (posée par showModal/hideModal) fait jouer la transition dans les deux
|
||
sens — une @keyframes n'aurait animé que l'ouverture. */
|
||
.modal-ov,.ask-overlay{opacity:0;transition:opacity .16s ease}
|
||
.modal-ov.show,.ask-overlay.show{opacity:1}
|
||
.modal-card,.ask-box{transform:translateY(10px) scale(.985);opacity:0;
|
||
transition:transform .18s cubic-bezier(.2,.8,.3,1),opacity .18s ease}
|
||
.modal-ov.show .modal-card,.ask-overlay.show .ask-box{transform:none;opacity:1}
|
||
@media (prefers-reduced-motion:reduce){
|
||
.modal-ov,.ask-overlay,.modal-card,.ask-box{transition:none}
|
||
}
|
||
.modal-card{position:relative;background:var(--bg);border:1px solid var(--border);border-radius:12px;width:100%;max-width:600px;max-height:90vh;display:flex;flex-direction:column;box-shadow:0 16px 50px rgba(0,0,0,.18)}
|
||
.modal-head{padding:14px 16px;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:center}
|
||
.modal-head strong{font-size:14px}
|
||
/* Croix de fermeture : un CARRÉ, pas un rectangle. Avec un padding asymétrique
|
||
(3px vertical / 10px horizontal) le glyphe paraissait écrasé et la cible de
|
||
clic était basse. 32px, glyphe centré, taille de cible correcte au doigt. */
|
||
.modal-x{margin:0;padding:0;width:32px;height:32px;flex:none;display:inline-flex;align-items:center;justify-content:center;font-size:19px;line-height:1;color:var(--dim)}
|
||
.modal-body{padding:16px;overflow:auto;flex:1}
|
||
/* Modale ouverte mais pas encore remplie : on affiche le voile de chargement À LA
|
||
PLACE du formulaire et du pied. Estomper le formulaire ne suffisait pas — on
|
||
voyait quand même les champs s'écrire un par un derrière le voile. */
|
||
/* Voile OPAQUE, posé sous l'en-tête (le titre reste lisible) : `visibility:hidden`
|
||
sur le formulaire suffirait en théorie, mais un fond plein garantit qu'aucun
|
||
reste du preset précédent (moteur coché, réglages) ne puisse apparaître une
|
||
fraction de seconde. --mh = hauteur mesurée de l'en-tête, posée à l'ouverture. */
|
||
/* ⚠️ Aucune transition tant que le formulaire est caché, NI pendant la frame où
|
||
il réapparaît : WebKit diffère les transitions des éléments non rendus et les
|
||
joue à leur retour à l'écran. Le sélecteur de moteur glissait donc de l'ancien
|
||
preset au nouveau SOUS LES YEUX, une fois le chargement terminé. */
|
||
.modal-ov.loading .pe-body *,.modal-ov.no-anim .pe-body *{transition:none!important}
|
||
.modal-load{display:none;position:absolute;inset:var(--mh,52px) 0 0 0;z-index:2;
|
||
align-items:center;justify-content:center;background:var(--bg);border-radius:0 0 12px 12px}
|
||
.modal-ov.loading .modal-load{display:flex}
|
||
/* visibility (et non display) : le formulaire garde sa place, donc la carte a
|
||
déjà sa taille finale — la révéler ne provoque plus d'à-coup de hauteur. */
|
||
.modal-ov.loading .modal-body,
|
||
.modal-ov.loading .pe-body,
|
||
.modal-ov.loading .pe-foot,
|
||
.modal-ov.loading .modal-foot{visibility:hidden}
|
||
/* Rond qui tourne — les trois points sont la signature du CHAT, pas des modales. */
|
||
.spinner{width:22px;height:22px;border-radius:50%;border:2px solid var(--border);
|
||
border-top-color:var(--dim);animation:spin .7s linear infinite}
|
||
@keyframes spin{to{transform:rotate(360deg)}}
|
||
@media (prefers-reduced-motion:reduce){.spinner{animation-duration:2s}}
|
||
.modal-foot{padding:12px 16px;border-top:1px solid var(--border);display:flex;justify-content:space-between;align-items:center;gap:8px;flex-wrap:wrap}
|
||
.foot-l{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
|
||
.foot-r{display:flex;gap:8px}
|
||
.mform{display:flex;flex-direction:column;gap:14px}
|
||
.fld{display:flex;flex-direction:column;gap:5px}
|
||
.fld-l{font-size:11px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;color:var(--dim)}
|
||
.fld-l .opt{text-transform:none;letter-spacing:0;font-weight:400;opacity:.65}
|
||
.fld-hint{font-size:11px;color:var(--dim);margin-top:2px}
|
||
.ctl{width:100%;box-sizing:border-box;background:var(--panel);color:var(--text);border:1px solid var(--border);border-radius:7px;padding:9px 11px;font:inherit}
|
||
.ctl:focus{outline:none;border-color:var(--accent)}
|
||
.ta{min-height:220px;resize:vertical;line-height:1.5}
|
||
.card{border:1px solid var(--border);border-radius:10px;padding:13px;display:flex;flex-direction:column;gap:13px}
|
||
.card-h{font-size:11px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--accent)}
|
||
.row{display:flex;gap:6px}
|
||
.row .ctl{flex:1;min-width:0}
|
||
.btn-dl{flex-shrink:0;margin:0}
|
||
.input-btn{display:flex}
|
||
.input-btn .ctl{flex:1;min-width:0;border-top-right-radius:0;border-bottom-right-radius:0}
|
||
.input-btn .btn-in{flex-shrink:0;margin:0;padding:9px 13px;border-left:0;border-top-left-radius:0;border-bottom-left-radius:0}
|
||
.input-btn .btn-in:hover{border-left:0}
|
||
.seg{display:flex;gap:6px}
|
||
.seg>label{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;text-align:center;min-height:46px;padding:6px 4px;border:1px solid var(--border);border-radius:8px;cursor:pointer;font-size:12px;background:var(--panel);transition:border-color .12s,background .12s,color .12s}
|
||
.seg>label:hover{border-color:var(--accent)}
|
||
.seg>label:has(input:checked){border-color:var(--accent);background:var(--accent-bg);color:var(--accent)}
|
||
.seg input{display:none}
|
||
.seg .be-note{font-size:10px;color:var(--dim);margin:0}
|
||
.btn-danger{background:var(--danger-bg);border-color:var(--err);color:var(--err);margin:0}
|
||
.btn-save{border-color:var(--accent);color:var(--accent);margin:0}
|
||
.del-model{align-items:center;gap:5px;font-size:11px;color:var(--dim)}
|
||
.del-model input{margin:0}
|
||
.lc-logbar{display:flex;align-items:center;gap:6px;cursor:pointer;user-select:none;font-size:12px;color:var(--dim);font-weight:600;padding:6px 0;margin-top:2px}
|
||
.lc-logbar #lc-log-caret{font-size:9px;transition:transform .12s;display:inline-block}
|
||
.lc-logbar.open #lc-log-caret{transform:rotate(90deg)}
|
||
#lc-log{background:var(--panel);border:1px solid var(--border);border-radius:6px;padding:8px;font-size:10px;line-height:1.45;max-height:220px;overflow:auto;white-space:pre-wrap;word-break:break-all;margin:0}
|
||
.lc-spin{display:inline-block;animation:spin 1s linear infinite}
|
||
/* Serveurs MCP */
|
||
.mcp-list{display:flex;flex-direction:column;gap:4px}
|
||
|
||
.mcp-row{display:flex;align-items:center;gap:8px;padding:6px 10px;border:1px solid var(--border);border-radius:6px;background:var(--panel);cursor:pointer}
|
||
.mcp-row:hover{border-color:var(--accent)}
|
||
.mcp-row.off{opacity:.6}
|
||
.mcp-dot{flex:none;width:8px;height:8px;border-radius:50%}
|
||
.mcp-dot-ok{background:var(--accent)}
|
||
.mcp-dot-err{background:var(--err)}
|
||
.mcp-dot-off{background:var(--dim)}
|
||
.mcp-info{flex:1;min-width:0}
|
||
.mcp-name{font-size:12px;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||
.mcp-meta{display:flex;align-items:center;gap:6px;margin-top:2px}
|
||
.mcp-tag{font-size:10px;padding:1px 7px;border-radius:9px;border:1px solid var(--border);color:var(--dim);text-transform:uppercase;letter-spacing:.04em}
|
||
.mcp-tools{font-size:10px;color:var(--accent)}
|
||
.mcp-err{font-size:10px;color:var(--err)}
|
||
.mcp-switch{flex:none}
|
||
.mcp-edit{flex:none;padding:2px 5px;font-size:13px;line-height:1;background:transparent;border:none;opacity:.5}
|
||
.mcp-edit:hover{opacity:1;color:var(--accent)}
|
||
/* Liste des outils MCP dans la modale */
|
||
.mcp-tools-list{display:flex;flex-direction:column;gap:2px;max-height:240px;overflow:auto;border:1px solid var(--border);border-radius:6px;padding:6px}
|
||
.mcp-tool-row{display:flex;align-items:center;gap:10px;padding:4px 6px;border-radius:5px;cursor:pointer}
|
||
.mcp-tool-row:hover{background:var(--panel)}
|
||
.mcp-tool-switch{flex:none;transform:scale(.85)}
|
||
.mcp-tool-name{font-size:12px;color:var(--text);word-break:break-all}
|
||
|
||
/* Accès OpenAI / Accès distant : champs lecture seule (URL, clé) + libellés
|
||
compacts, pour une présentation homogène et sans surcharge visuelle. */
|
||
.io-input{flex:1;min-width:0;background:var(--panel);color:var(--text);border:1px solid var(--border);border-radius:9px;padding:8px 10px;font:inherit;font-size:13px}
|
||
|
||
/* Modale MCP (ajout/édition) : les champs des groupes stdio/http n'avaient aucun
|
||
espacement vertical (le conteneur n'est pas un flex à gap) → ils se touchaient.
|
||
On aligne leur rythme sur celui du reste du formulaire (.mform). */
|
||
#mcp-stdio-fields,#mcp-http-fields{display:flex;flex-direction:column;gap:14px}
|
||
|
||
/* ===== Mise en page de la conversation =============================
|
||
Colonne de lecture centrée, réponses SANS bulle (texte nu sur le fond),
|
||
question de l'utilisateur en pastille arrondie à droite, composer en
|
||
« pilule », pas d'étiquettes VOUS/Loki. Ces règles surchargent la mise en
|
||
page générique plus haut : elles doivent donc rester APRÈS elle. */
|
||
body{font-size:15px}
|
||
h1{color:var(--text);font-weight:600;letter-spacing:-.01em}
|
||
/* Barre latérale : plus sombre/atténuée que la zone de conversation. */
|
||
.side{background:var(--panel);border-right-color:transparent}
|
||
@media (max-width:720px){.side{background:var(--panel)}}
|
||
/* Colonne de lecture centrée (comme le fil ChatGPT), pleine largeur sur mobile.
|
||
⚠️ JAMAIS le raccourci `padding` ici : le rembourrage BAS réserve la hauteur
|
||
de la carte de saisie (voir #chat plus bas) et il est réglé ailleurs. Une
|
||
règle en raccourci le remet silencieusement à sa valeur du raccourci — c'est
|
||
exactement ce qui, jusqu'à ce correctif, laissait la fin du fil coincée sous
|
||
la carte au-delà de 721 px de large. Longhands uniquement. */
|
||
#chat{padding-top:28px;padding-inline:max(16px,calc(50% - 380px));gap:22px;scrollbar-gutter:stable both-edges}
|
||
/* Réponses de l'assistant : pas de cadre, pas de fond — juste du texte. */
|
||
#chat .msg.assistant{background:transparent;border:none;padding:0;max-width:100%;line-height:1.7}
|
||
/* Question : pastille arrondie à droite, texte lisible sur fond clair OU sombre. */
|
||
#chat .msg.user{background:var(--bubble);color:var(--text);border-radius:20px;padding:11px 18px;max-width:75%;line-height:1.6}
|
||
#chat .msg.user .label{color:var(--dim)}
|
||
/* Libellés : l'amont les masquait tous ici, les mots « user » / « assistant »
|
||
n'ajoutant rien à l'alignement. Ils portent désormais un avatar et un prénom
|
||
(voir 17-identity.js), qui eux identifient vraiment l'interlocuteur — on les
|
||
réaffiche donc, en ligne et discrets. Restent masqués les libellés SANS
|
||
avatar, c'est-à-dire les bulles techniques non repliables. */
|
||
#chat .msg .label{display:none}
|
||
#chat .msg.collapsible .label{display:block}
|
||
#chat .msg.user .label,#chat .msg.assistant .label{display:flex;align-items:center;gap:0;opacity:.85}
|
||
/* Côté question, le libellé s'aligne à droite comme la bulle. */
|
||
#chat .msg.user .label{justify-content:flex-end}
|
||
/* Mesures de génération sous la réponse (prefill / decode). Assez discrètes pour
|
||
ne pas concurrencer le texte, assez lisibles pour être utiles. Escamotables
|
||
depuis Apparence → Affichage. */
|
||
#chat .msg .statline{margin-top:10px;font-family:var(--mono);font-size:10.5px;letter-spacing:.02em;
|
||
color:var(--dim);opacity:.75}
|
||
#chat .msg .statline:empty{display:none}
|
||
html[data-hide-stats="1"] #chat .msg .statline{display:none}
|
||
/* Arrivée en fondu montant plutôt qu'un surgissement sec. L'opacité finale (.75)
|
||
est celle de la règle ci-dessus : l'animation doit y ATTERRIR, sinon la ligne
|
||
clignote en repassant à sa valeur de repos. */
|
||
#chat .msg .statline-in{animation:statin .28s cubic-bezier(.2,.7,.3,1) both}
|
||
@keyframes statin{from{opacity:0;transform:translateY(3px)}to{opacity:.75;transform:none}}
|
||
@media (prefers-reduced-motion:reduce){#chat .msg .statline-in{animation:none}}
|
||
/* Raisonnement / outils : encart gris discret plutôt qu'un cadre pointillé. */
|
||
#chat .msg.reasoning,#chat .msg.tool{background:var(--panel);border:1px solid var(--border);border-radius:14px;color:var(--dim);padding:12px 16px}
|
||
#chat .msg.reasoning{color:var(--dim)}
|
||
#chat .msg.tool .tool-head{color:var(--text)}
|
||
#chat .msg.assistant .body pre,#chat .msg.reasoning .body pre,#chat .msg.tool pre{border-radius:12px;padding:12px 14px}
|
||
#chat .msg.assistant .body a{text-decoration:underline;text-underline-offset:2px}
|
||
/* Composer = UNE seule carte posée PAR-DESSUS le fil : la conversation
|
||
continue de défiler DERRIÈRE elle et s'efface en fondu (dégradé vers le fond)
|
||
au lieu de s'arrêter net au-dessus. Dans la carte, de haut en bas : la saisie
|
||
et ses boutons, un pied discret (préréglage · contexte · compacter), puis la
|
||
jauge de contexte qui FORME le bord bas — elle épouse les coins arrondis
|
||
(overflow:hidden sur le rayon). Les trois blocs sont des frères dans le HTML :
|
||
on les recolle en dessinant les bordures par morceaux et l'ordre visuel vient
|
||
de l'ordre flex — aucun changement de markup. */
|
||
/* Le padding BAS dégage l'indicateur d'accueil de l'iPhone en PWA : la carte
|
||
flotte au-dessus de lui, son arrondi bas reste donc bien visible. Hors PWA
|
||
l'inset vaut 0 → strictement les 8 px d'origine. */
|
||
/* --fade = la bande transparente au-dessus de la carte, celle où le fil doit
|
||
s'effacer. Le dégradé s'exprime dans la MÊME unité (et non en % de la hauteur
|
||
totale du composeur, qui varie) : l'opacité est donc pleine exactement au bord
|
||
haut de la carte. Avec un %, le fondu finissait SOUS ce bord et le texte
|
||
restait lisible au ras de la carte — l'impression de « passer derrière ». */
|
||
#composer{position:absolute;left:0;right:0;bottom:0;z-index:4;border-top:none;display:flex;flex-direction:column;--fade:34px;padding:var(--fade) 0 calc(8px + env(safe-area-inset-bottom));--col:calc(max(16px,calc(50% - 380px)) + var(--sbw,0px));background:linear-gradient(to bottom,transparent 0,var(--bg) var(--fade))}
|
||
/* Réserve sous le fil = hauteur RÉELLE du composeur, mesurée et publiée par
|
||
syncComposer() (08-chat-render.js). Une constante redevient fausse dès que la
|
||
saisie grandit ou qu'une pièce jointe s'affiche, et la fin du fil repasse hors
|
||
d'atteinte. 170px n'est que le repli du tout premier rendu, avant mesure.
|
||
Pas de env(safe-area-inset-bottom) ici : il est déjà DANS le padding bas du
|
||
composeur, donc dans sa hauteur mesurée — le compter deux fois creusait un
|
||
trou en PWA iOS. */
|
||
#chat{padding-bottom:calc(var(--composer-h,170px) + 12px)}
|
||
#scrollbtn{bottom:calc(var(--composer-h,170px) + 10px);border-radius:999px}
|
||
#inputbar{order:1;margin:0 var(--col);padding:8px 8px 8px 16px;gap:8px;align-items:flex-end;background:var(--panel);border:1px solid var(--border);border-bottom:none;border-radius:26px 26px 0 0}
|
||
#input{background:transparent;border:none;border-radius:0;padding:9px 0;min-height:36px;line-height:18px;max-height:160px}
|
||
/* Pas de cadre qui s'assombrit au focus : la carte garde exactement la même
|
||
bordure, seule la saisie change (curseur). */
|
||
#input:focus{border-color:transparent}
|
||
#send,#stop{border-radius:999px;height:36px;min-height:36px;padding:0 18px;margin:0}
|
||
#send{background:var(--text);color:var(--bg);border-color:var(--text);font-weight:600}
|
||
#send:hover:not(:disabled){opacity:.85;color:var(--bg);border-color:var(--text)}
|
||
/* Pied de carte : c'est LUI qui porte l'arrondi bas (26 px, comme en haut) et
|
||
qui clippe la jauge. Un bandeau de 7 px ne peut pas dessiner un rayon de 26 px
|
||
(le navigateur l'écrase, d'où les coins bizarres) : on laisse donc la carte
|
||
découper la jauge dans sa propre forme — la barre est bien épaisse ET rognée
|
||
exactement sur la courbe, symétrique du haut. */
|
||
#ctxmeta{order:2;margin:0 var(--col);padding:6px 16px 15px;gap:10px;background:var(--panel);border:1px solid var(--border);border-top:none;border-radius:0 0 26px 26px;overflow:hidden}
|
||
/* ⚠️ NE PAS piloter la position du composer en JS quand le clavier s'ouvre
|
||
(tenté puis retiré le 2026-07-28, deux fois) : ni asservir la hauteur de la
|
||
coque à visualViewport.height, ni décaler le composer de la hauteur du
|
||
clavier. Dans les deux cas la carte part en haut de l'écran en PWA iOS.
|
||
iOS gère déjà correctement le clavier tout seul — on le laisse faire. */
|
||
#ctx-text,#status-preset{color:var(--dim);font-size:10px;letter-spacing:.02em;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}
|
||
/* Bouton « Postes distants » du composeur : même gabarit que le trombone. L'icône
|
||
dépend de la cible active — écran (local) par défaut, ondes wifi quand un poste
|
||
distant est sélectionné, alors teinté en accent pour signaler « l'IA agit
|
||
ailleurs ». Les deux SVG coexistent ; on n'en montre qu'un via la classe. */
|
||
#node-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)}
|
||
#node-btn:hover{background:var(--row-bg-on);border-color:transparent;color:var(--text)}
|
||
#node-btn:active{transform:scale(.9)}
|
||
#node-btn svg{display:block}
|
||
#node-btn .ic-remote{display:none}
|
||
#node-btn.remote{color:var(--accent)}
|
||
#node-btn.remote .ic-here{display:none}
|
||
#node-btn.remote .ic-remote{display:block}
|
||
/* Fichiers en attente : de petites bulles POSÉES AU-DESSUS de la carte.
|
||
Le composeur réserve déjà 34 px transparents au-dessus de lui (la bande de
|
||
dégradé qui fond vers le fil) : les pastilles s'y logent, et on retire d'autant
|
||
le rembourrage. La carte ne bouge donc pas — c'est la bande vide qui se
|
||
remplit, au lieu du composeur qui grandit vers le haut.
|
||
Elles ne vont PAS dans le pied : cette ligne appartient au nom du modèle. */
|
||
#attach-list{order:0;display:none;gap:6px;margin:0 var(--col);padding:0 2px 8px;
|
||
overflow-x:auto;scrollbar-width:none}
|
||
#attach-list::-webkit-scrollbar{display:none}
|
||
#attach-list.show{display:flex}
|
||
#composer:has(#attach-list.show){--fade:6px}
|
||
/* Posées sur le fil (et non sur la carte) : il leur faut un fond opaque et un
|
||
cadre, sinon le texte de la conversation transparaît derrière. */
|
||
#attach-list .chip-file{flex:0 0 auto;background:var(--panel);border-color:var(--border);
|
||
box-shadow:0 2px 10px rgba(0,0,0,.10)}
|
||
/* Pastille de fichier. Le × n'apparaît qu'au survol : au repos la pastille doit
|
||
rester lisible d'un coup d'œil (nom + poids), pas encombrée d'un bouton. */
|
||
.chip-file{display:flex;align-items:center;gap:8px;max-width:230px;min-width:0;
|
||
padding:4px 12px;border-radius:999px;background:var(--row-bg-on);
|
||
border:1px solid transparent;font-size:12px;line-height:1.3}
|
||
/* Avec un × à droite, la pastille se ferme SUR lui : le rembourrage de droite
|
||
revient à l'épaisseur du rond, sinon le bouton flotte loin du bord et la
|
||
pastille paraît de travers. */
|
||
.chip-file:has(button){padding-right:4px}
|
||
.chip-file .cf-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||
.chip-file .cf-size{flex:none;color:var(--dim);font-size:11px}
|
||
/* Le × est TOUJOURS visible : le révéler au survol le rendait inatteignable sur
|
||
mobile, où il n'y a pas de survol — donc impossible de retirer un fichier. */
|
||
.chip-file button{flex:none;margin:0;padding:0;width:20px;height:20px;font-size:14px;
|
||
display:flex;align-items:center;justify-content:center;line-height:1;
|
||
border-radius:50%;border:none;background:transparent;color:var(--dim)}
|
||
.chip-file button:hover{background:var(--bubble);color:var(--text)}
|
||
/* Transfert en cours : un liseré qui tourne, plutôt qu'une pastille grisée dont
|
||
on ne sait pas si elle travaille ou si elle a échoué. */
|
||
.chip-file.up .cf-size::after{content:"";display:inline-block;vertical-align:-2px;
|
||
margin-left:7px;width:10px;height:10px;border-radius:50%;
|
||
border:1.5px solid var(--border);border-top-color:var(--text);animation:spin .7s linear infinite}
|
||
.chip-file.err{background:var(--danger-bg);color:var(--err)}
|
||
.chip-file.err .cf-size{color:var(--err)}
|
||
@media (prefers-reduced-motion:reduce){.chip-file.up .cf-size::after{animation:none}}
|
||
/* Fichiers d'un message envoyé : leur propre rangée AU-DESSUS de la bulle,
|
||
alignée à droite comme elle. Rangée et bulle restent deux objets distincts,
|
||
séparés par l'espacement normal du fil — les rapprocher les faisait lire comme
|
||
une seule bulle difforme qui déborde vers le haut. */
|
||
#chat .msg-files{align-self:flex-end;display:flex;flex-wrap:wrap;justify-content:flex-end;
|
||
gap:6px;max-width:75%}
|
||
#chat .msg-files .chip-file{background:var(--bubble);max-width:100%}
|
||
/* Lien de téléchargement dans une réponse : un lien normal, mais la flèche dit
|
||
qu'il ramène un fichier plutôt qu'il n'ouvre une page. */
|
||
#chat .msg .filelink{cursor:pointer}
|
||
#chat .msg .filelink::after{content:"↓";margin-left:5px;font-size:.9em;opacity:.7;text-decoration:none;display:inline-block}
|
||
#chat .msg .filelink.busy{opacity:.55;pointer-events:none}
|
||
/* Voile de dépôt. Il couvre la zone de conversation, curseur inclus : c'est la
|
||
réponse à « est-ce que je peux lâcher ici ? », elle doit être sans ambiguïté. */
|
||
#dropveil{position:absolute;inset:0;z-index:20;display:none;align-items:center;
|
||
justify-content:center;padding:24px;background:rgba(0,0,0,.28);pointer-events:none}
|
||
#dropveil.show{display:flex}
|
||
#dropveil>div{padding:22px 30px;border:2px dashed var(--border);border-radius:20px;
|
||
background:var(--panel);color:var(--text);font-size:14px;box-shadow:0 8px 30px rgba(0,0,0,.18)}
|
||
/* Reste de l'UI : coins arrondis et contrastes doux, dans le même esprit. */
|
||
button,.mem-search{border-radius:10px}
|
||
/* Champ + bouton accolés : on ne garde l'arrondi que sur les coins extérieurs. */
|
||
.pe-inpbtn .pe-inp{border-radius:10px 0 0 10px}
|
||
.pe-inpbtn button{border-radius:0 10px 10px 0}
|
||
.input-btn .ctl{border-radius:10px 0 0 10px}
|
||
.input-btn .btn-in{border-radius:0 10px 10px 0}
|
||
.preset{border-radius:12px}
|
||
/* Les presets restent des cartes (fond + bordure) : à plat, sans fond ni
|
||
cadre, la liste ne se lisait plus comme des lignes distinctes. L'actif se
|
||
signale par sa bordure — un simple voile d'accent (7 % de blanc sur fond
|
||
noir) était invisible en mode sombre. */
|
||
.preset{background:var(--panel);border-color:var(--border)}
|
||
.preset:hover{background:var(--accent-bg);border-color:var(--dim)}
|
||
.preset.active{background:var(--panel);border-color:var(--text);color:var(--text)}
|
||
.subhead,.lc-mode-go,.lc-mode-active-tag{color:var(--dim)}
|
||
button:hover{border-color:var(--dim);color:var(--text)}
|
||
|
||
/* Encart de garde-fou : c'est le serveur qui parle (limite d'appels d'outils,
|
||
boucle détectée), pas le modèle. Pastille discrète plutôt qu'une erreur —
|
||
la génération s'est arrêtée proprement, rien n'a échoué. */
|
||
.stopnote{display:inline-flex;align-items:center;gap:7px;margin-top:4px;padding:4px 12px;
|
||
border:1px solid var(--border);border-radius:999px;background:var(--panel);
|
||
color:var(--dim);font-size:12px;line-height:1.5}
|
||
.stopnote::before{content:"";flex:none;width:6px;height:6px;border-radius:50%;background:var(--dim)}
|
||
|
||
/* ===== Fenêtres et sélecteurs =====================================
|
||
Coins arrondis et ombre douce plutôt qu'un cadre marqué. */
|
||
.modal-card,.ask-box,.modal-box,.pe-list,.pe-adv{border-radius:12px}
|
||
.modal-card,.ask-box{box-shadow:0 8px 30px rgba(0,0,0,.12)}
|
||
/* Sélecteurs segmentés : noir plein quand c'est coché, pas de couleur. */
|
||
.pe-seg>label:has(input:checked),.seg>label:has(input:checked){background:var(--text);border-color:var(--text);color:var(--bg)}
|
||
|
||
|
||
/* Curseur de portée de l'export. Même vocabulaire visuel que la répartition GPU
|
||
(piste arrondie + pastille cerclée), mais une seule valeur : la position tout
|
||
à droite vaut « toute la conversation ». Le libellé sous la piste donne le
|
||
compte RÉEL d'échanges et le poids du fichier, la question qu'on se pose
|
||
vraiment avant d'exporter. */
|
||
.xr{position:relative;display:flex;align-items:center;height:22px}
|
||
.xr-track{width:100%;height:10px;border-radius:999px;overflow:hidden;background:color-mix(in srgb,var(--dim) 20%,transparent)}
|
||
.xr-fill{height:100%;background:var(--accent);border-radius:999px;transition:width .12s}
|
||
.xr-range{position:absolute;inset:0;width:100%;height:22px;margin:0;padding:0;background:transparent;-webkit-appearance:none;appearance:none;cursor:ew-resize}
|
||
.xr-range:focus{outline:none}
|
||
.xr-range::-webkit-slider-runnable-track{height:22px;background:transparent;border:none}
|
||
.xr-range::-moz-range-track{height:22px;background:transparent;border:none}
|
||
.xr-range::-moz-range-progress{background:transparent}
|
||
.xr-range::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:16px;height:16px;margin-top:3px;border-radius:50%;background:var(--bg);border:2px solid var(--accent);box-shadow:0 1px 3px rgba(0,0,0,.3);cursor:ew-resize}
|
||
.xr-range::-moz-range-thumb{width:16px;height:16px;border-radius:50%;background:var(--bg);border:2px solid var(--accent);box-shadow:0 1px 3px rgba(0,0,0,.3);cursor:ew-resize}
|
||
.xr-range:focus-visible::-webkit-slider-thumb{box-shadow:0 0 0 3px var(--accent-bg)}
|
||
.xr-range:focus-visible::-moz-range-thumb{box-shadow:0 0 0 3px var(--accent-bg)}
|
||
.xr-range:disabled{cursor:default;opacity:.5}
|
||
/* Conversation vide : un seul message centré à la place des réglages. */
|
||
.xempty{display:flex;flex-direction:column;align-items:center;gap:6px;padding:44px 20px;text-align:center;color:var(--text);font-size:14px}
|
||
.xempty-i{font-size:30px;line-height:1;color:var(--dim);opacity:.7}
|
||
|
||
/* Postes distants : cases de capacités, code d'appairage, commande. */
|
||
.node-caps{display:flex;flex-wrap:wrap;gap:6px}
|
||
.node-cap{display:inline-flex;align-items:center;gap:5px;padding:5px 9px;border:1px solid var(--border);border-radius:7px;font-size:12px;cursor:pointer;user-select:none}
|
||
.node-cap input{margin:0}
|
||
.node-code{font-family:var(--mono,monospace);font-size:22px;font-weight:700;letter-spacing:3px;color:var(--accent);padding:8px 0}
|
||
.node-cmd{white-space:pre-wrap;word-break:break-all;background:var(--code-bg);border:1px solid var(--border);border-radius:7px;padding:8px;margin:6px 0;font-size:12px}
|
||
|
||
/* Poste distant : sélecteur de cible d'exécution. */
|
||
.node-target{display:flex;flex-wrap:wrap;align-items:center;gap:6px;margin-bottom:8px}
|
||
.node-target-l{font-size:12px;color:var(--dim);margin-right:2px;width:100%}
|
||
.node-target-opt{display:inline-flex;align-items:center;gap:5px;padding:5px 9px;border:1px solid var(--border);border-radius:7px;font-size:12px;cursor:pointer}
|
||
.node-target-opt.on{border-color:var(--accent);color:var(--accent)}
|
||
.node-target-opt.off{opacity:.6}
|
||
.node-target-opt input{margin:0}
|
||
|
||
/* ===== Direction « Terre » : surfaces en cartes =======================
|
||
La charte pose des CARTES arrondies séparées par une gouttière, au lieu
|
||
d'une colonne à filets. Bloc en fin de feuille pour prendre le pas sur les
|
||
règles de structure d'origine sans les réécrire.
|
||
Bureau UNIQUEMENT : sur téléphone la barre latérale est un tiroir plein
|
||
écran et le fil occupe toute la largeur — y poser des cartes ne ferait que
|
||
grignoter des pixels précieux. */
|
||
@media (min-width:721px){
|
||
/* Coque : le fond « terre » se voit dans la gouttière entre les deux cartes. */
|
||
body{padding:12px;gap:12px}
|
||
html[data-pwa="1"] body{padding-top:calc(12px + env(safe-area-inset-top))}
|
||
|
||
/* Barre latérale : pile de cartes, sans filet ni bordure de colonne. */
|
||
.side{width:312px;border-right:0;padding:0;background:transparent;
|
||
display:flex;flex-direction:column;gap:12px;overflow-y:auto;overscroll-behavior:contain}
|
||
.sidecard,.side>details{background:var(--panel);border:0;border-top:0;border-radius:20px;
|
||
box-shadow:var(--shadow);margin:0;padding:2px 16px}
|
||
.sidecard{padding:15px 18px 14px;display:flex;flex-direction:column;gap:2px}
|
||
.side>details[open]{padding-bottom:14px}
|
||
.side>details>summary{height:42px}
|
||
/* L'étiquette « Paramètres » n'est plus un filet mais un intertitre entre
|
||
deux cartes : le fond de la coque fait déjà la séparation. */
|
||
.side-sep{border-top:0;margin:10px 0 0;padding:0 6px}
|
||
/* Blocs encastrés DANS une carte : un ton plus chaud, sans bordure — deux
|
||
cadres imbriqués alourdiraient inutilement. */
|
||
.side .slist{background:var(--row-bg);border-color:transparent;border-radius:14px}
|
||
|
||
/* Colonne principale : une seule grande carte qui contient fil et saisie. */
|
||
.main{background:var(--panel);border-radius:20px;box-shadow:var(--shadow);overflow:hidden}
|
||
#chat{padding-top:26px;padding-inline:max(20px,calc(50% - 360px))}
|
||
/* Le dégradé qui fait disparaître le fil sous la carte de saisie doit fondre
|
||
vers le PANNEAU, pas vers le fond de la coque. */
|
||
#composer{background:linear-gradient(to bottom,transparent 0,var(--panel) var(--fade))}
|
||
/* Carte de saisie : posée SUR le panneau, donc un ton au-dessus, et sans
|
||
bordure — c'est l'élévation qui la détache. */
|
||
#inputbar,#ctxmeta{background:var(--row-bg-on);border-color:transparent}
|
||
#input{background:transparent}
|
||
}
|
||
|
||
/* --- Terre : densité et accents ------------------------------------- */
|
||
@media (min-width:721px){
|
||
/* Gouttière resserrée : la charte empile beaucoup de cartes, 12 px entre
|
||
chacune repoussait « Actions » loin sous la ligne de flottaison. */
|
||
.side{gap:8px}
|
||
.side>details>summary{height:38px}
|
||
|
||
/* Discussion active : un cadre sauge COMPLET, mais fin (1 px) — le liseré de
|
||
3 px sur le seul bord gauche se lisait mal et déséquilibrait la ligne. La
|
||
bordure transparente est portée par tous les états : la sélection ne décale
|
||
donc pas le contenu d'un pixel. */
|
||
.side .preset{border:1px solid transparent;border-radius:14px;background:transparent}
|
||
.side .preset.active{background:var(--row-bg-on);border-color:var(--accent)}
|
||
.side .preset:hover{background:var(--row-bg)}
|
||
|
||
/* Bulle de question : coin bas-droit rentré, comme une bulle qui pointe vers
|
||
son auteur. */
|
||
#chat .msg.user{border-radius:20px 20px 6px 20px}
|
||
}
|
||
/* Action principale en sauge : c'est la couleur d'action de la charte, et elle
|
||
se distingue enfin du texte (le bouton reprenait --text, donc un brun sombre
|
||
indistinct du reste). L'arrêt, lui, reste en terre cuite. */
|
||
#send{background:var(--accent);border-color:var(--accent);color:#fff}
|
||
#send:hover:not(:disabled){background:var(--accent);border-color:var(--accent);color:#fff;opacity:.88}
|
||
#stop{background:var(--err);border-color:var(--err);color:#fff}
|
||
#stop:hover{background:var(--err);border-color:var(--err);color:#fff;opacity:.88}
|
||
|
||
/* --- Terre : largeur de lecture --------------------------------------
|
||
La mesure était plafonnée à 760 px et centrée sur la fenêtre : sur un écran
|
||
large, le fil se réduisait à une colonne étroite entourée de vide, alors que
|
||
la charte fait courir le texte sur toute la largeur du panneau.
|
||
Une seule variable pilote désormais le fil ET la carte de saisie — ils
|
||
étaient réglés séparément (450 / 380 / 360 px), donc condamnés à diverger.
|
||
La borne haute reste : au-delà, une ligne devient pénible à lire (l'œil perd
|
||
le début de la suivante). 1080 px ≈ 110 caractères, large mais tenable. */
|
||
@media (min-width:721px){
|
||
:root{--measure:min(1080px,100%)}
|
||
#chat{padding-top:26px;padding-inline:max(20px,calc(50% - var(--measure)/2))}
|
||
#composer{--col:calc(max(20px,calc(50% - var(--measure)/2)) + var(--sbw,0px))}
|
||
}
|
||
|
||
/* --- Terre : rangée de bulles techniques ------------------------------
|
||
Raisonnement, outils et mémoire ne portent qu'une étiquette tant qu'ils sont
|
||
repliés : les empiler gaspillait une ligne entière par appel et repoussait la
|
||
réponse hors de l'écran. Ils défilent donc en rangée, et reprennent la pleine
|
||
largeur dès qu'on en déplie un. */
|
||
.toolrow{display:flex;flex-wrap:wrap;gap:8px;align-items:flex-start}
|
||
.toolrow>.msg.collapsible{margin:0;max-width:100%}
|
||
.toolrow>.msg.collapsible.collapsed{width:auto}
|
||
/* Dépliée : la bulle passe seule sur sa ligne, sinon son contenu serait
|
||
comprimé dans une colonne étroite à côté d'une pastille. */
|
||
.toolrow>.msg.collapsible:not(.collapsed){flex:1 0 100%}
|
||
|
||
/* --- Terre : sections groupées en cartes ------------------------------
|
||
Une carte par section empilait onze arrondis et autant de gouttières : la
|
||
barre latérale débordait de l'écran alors que la plupart de ces lignes sont
|
||
des titres repliés d'une seule ligne. Les sections d'IA d'un côté, celles de
|
||
l'application de l'autre, tiennent maintenant chacune dans UNE carte, avec un
|
||
filet discret entre les lignes. */
|
||
@media (min-width:721px){
|
||
.sidegroup{background:var(--panel);border-radius:20px;box-shadow:var(--shadow);
|
||
padding:2px 16px;display:flex;flex-direction:column}
|
||
/* Dans un groupe, les sections ne sont plus des cartes : elles redeviennent
|
||
des lignes, séparées par un filet — sauf la première. */
|
||
.sidegroup>details{background:transparent;box-shadow:none;border-radius:0;padding:0;
|
||
border-top:1px solid var(--border)}
|
||
.sidegroup>details:first-child{border-top:0}
|
||
.sidegroup>details[open]{padding-bottom:12px}
|
||
}
|
||
|
||
/* --- Recherche Hugging Face -------------------------------------------
|
||
Deux listes successives dans la même zone (dépôts, puis fichiers du dépôt
|
||
choisi) : une seule mise en forme sert aux deux, sinon elles divergent. La
|
||
liste est bornée en hauteur et défile — un dépôt publie facilement vingt
|
||
quantifications, et la modale ne doit pas s'étirer jusqu'à sortir de
|
||
l'écran. */
|
||
.hf-list{display:flex;flex-direction:column;max-height:220px;overflow-y:auto;
|
||
border:1px solid var(--border);border-radius:12px;margin-top:6px}
|
||
.hf-row{display:flex;align-items:center;gap:10px;padding:7px 12px;cursor:pointer;
|
||
border-top:1px solid var(--border);font-size:12px}
|
||
.hf-row:first-child{border-top:0}
|
||
.hf-row:hover{background:var(--row-bg-on)}
|
||
/* Le nom prend la place restante et se tronque : un identifiant de dépôt long
|
||
ne doit pas pousser la taille et le verdict hors de la ligne. */
|
||
.hf-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text)}
|
||
.hf-meta{flex:none;color:var(--dim);font-family:var(--mono);font-size:10.5px;white-space:nowrap}
|
||
/* Verdict mémoire. La couleur seule ne suffit pas — le mot est écrit dedans,
|
||
et l'explication complète est dans l'infobulle. */
|
||
.hf-fit{flex:none;padding:1px 8px;border-radius:999px;font-size:10px;letter-spacing:.03em;
|
||
text-transform:uppercase;border:1px solid transparent}
|
||
.hf-fit.ok{color:var(--ok);border-color:var(--ok)}
|
||
.hf-fit.juste{color:var(--warn);border-color:var(--warn)}
|
||
.hf-fit.trop{color:var(--err);border-color:var(--err)}
|
||
.hf-head{display:flex;align-items:center;gap:10px;margin-top:6px;font-size:12px}
|
||
.hf-mm{display:flex;align-items:center;gap:6px;margin-top:8px;font-size:11.5px;
|
||
color:var(--dim);cursor:pointer}
|
||
.hf-mm.muted{cursor:default}
|