/* ===== Polices ===================================================== Inter (interface) + JetBrains Mono (chiffres, chemins, code). Servies par Loki (/fonts/, embarquées dans le binaire) et NON par Google Fonts : une instance locale ou coupée d'internet doit s'afficher correctement, sans qu'un tiers apprenne qui consulte l'interface. Sous-ensemble LATIN uniquement (~48 + ~31 Ko) : il couvre le français, y compris œ et les guillemets. Les symboles de l'UI (↻ ⌫ ☰ 🗑) n'ont jamais été dessinés par la police d'interface — ils viennent du système, ici comme avant. swap : le texte s'affiche tout de suite dans la police de repli, puis bascule — jamais de blanc au chargement. */ @font-face{font-family:'Inter';src:url('/fonts/inter.woff2') format('woff2');font-weight:100 900;font-display:swap;font-style:normal} @font-face{font-family:'JetBrains Mono';src:url('/fonts/jetbrainsmono.woff2') format('woff2');font-weight:100 800;font-display:swap;font-style:normal} /* ===== Palette « Sober Tech » ====================================== Ardoise et sauge. Deux variantes — claire par défaut, sombre via data-theme="dark" sur . Toutes les couleurs de l'UI passent par ces variables ; rien d'autre ne code une couleur en dur. ⚠️ Les valeurs NOMMÉES viennent des deux planches officielles du projet Stitch « Loki AI Chat Modern Redesign v1 » — « Official Palette for Loki AI v1 » et « Official Dark Mode Palette for Loki AI » — pas d'une approximation à l'œil : clair Main BG #F5F6F8 · Sidebar BG #ECECEC · Primary Accent #95A899 Typography #333333 sombre Deep Navy #0F172A · Slate Surface #1E293B · Sage Accent #84A98C Le reste (carte de réponse, carte de saisie, piste de jauge, bulle sombre) est relevé sur les écrans de la maquette, qui posent une hiérarchie de surfaces que l'intuition inverse volontiers : barre latérale la plus SOMBRE des trois #ECECEC / #0F172A fil la surface de référence #F5F6F8 / #1E293B carte s'ÉCARTE du fil #EAEBEE / #2A3646 saisie la plus claire #FBFBFD / #1B2436 En clair la carte de réponse est donc plus sombre que le fil (et non blanche), en sombre plus claire : dans les deux cas elle se détache en s'éloignant du fond, jamais en allant vers le blanc. La bulle de l'utilisateur ne suit PAS l'accent : sauge en clair (#7EA58A), ardoise en sombre (#334257) — la maquette réserve le vert aux jauges dès que le fond est sombre. Rôles : --accent = sauge (sélection, marche, liens, bulle utilisateur) · --warn = ambre (chiffres, mesures, avertissements) · --err = rouille (arrêt, danger). --row-bg / --code-bg sont les deux fonds encastrés d'un panneau. --code-bg est nettement plus soutenu que --bubble : un bloc de code posé DANS la carte de réponse doit se lire comme un encadré, pas se fondre dedans. --on-accent : ce qui s'écrit SUR un aplat de sauge. Il change avec la variante — blanc sur la sauge foncée du thème clair, ardoise sur la sauge claire du thème sombre — sinon un des deux thèmes écrit du texte à 2:1 de contraste sur la bulle utilisateur et le bouton d'envoi. */ :root{color-scheme:light; --font:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif; --mono:'JetBrains Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; --bg:#f5f6f8;--panel:#fbfbfd;--bubble:#eaebee;--border:rgba(51,51,51,.11); --text:#333333;--dim:#6b7280; --accent:#587062;--accent-bg:rgba(149,168,153,.20);--on-accent:#ffffff;--on-err:#ffffff; --accent-fill:#95a899;--accent-soft:#95a899; --user-bg:#95a899;--on-user:#ffffff; --side:#ececec;--row-bg:#f0f0f1;--row-bg-on:#e0e0e2;--track:#dadbdf; --ok:#587062;--warn:#a86a2a;--err:#c0503f;--mag:#a86a2a; --code-bg:#e2e3e7;--danger-bg:#f5e3e0;--warn-bg:#f2ece1; --add:#587062;--add-bg:rgba(149,168,153,.20);--del:#c0503f;--del-bg:rgba(192,80,63,.12); --shadow:0 1px 2px rgba(15,23,42,.05);--shadow-lift:0 4px 16px rgba(15,23,42,.08); --shadow-card:0 2px 10px rgba(15,23,42,.06),0 1px 3px rgba(15,23,42,.04)} /* « Deep Dark » : fond #0F172A, cartes #1E293B. */ [data-theme="dark"]{color-scheme:dark; --bg:#1e293b;--panel:#1b2436;--bubble:#2a3646;--border:rgba(148,163,184,.16); --text:#e2e8f0;--dim:#94a3b8; --accent:#a3c4ab;--accent-bg:rgba(132,169,140,.22);--on-accent:#0f172a;--on-err:#0f172a; --accent-fill:#84a98c;--accent-soft:#84a98c; --user-bg:#334259;--on-user:#e8eef5; --side:#0f172a;--row-bg:#1a2334;--row-bg-on:#232c3f;--track:#232a3a; --ok:#a3c4ab;--warn:#d9a55f;--err:#e0705c;--mag:#d9a55f; --code-bg:#16202f;--danger-bg:#3a1f1c;--warn-bg:#33291b; --add:#a3c4ab;--add-bg:rgba(132,169,140,.22);--del:#e0705c;--del-bg:rgba(224,112,92,.16); --shadow:none;--shadow-lift:0 4px 16px rgba(0,0,0,.35); --shadow-card:0 2px 12px rgba(0,0,0,.28),0 1px 3px rgba(0,0,0,.20)} /* Safari iOS peint un rectangle gris/noir sur TOUT élément touché (bouton, lien, résumé de section, bulle repliable…). C'est un retour tactile par défaut, hors de notre charte et impossible à styler : on le coupe. Nos états :active/:focus restent, eux, gérés élément par élément. */ *{box-sizing:border-box;-webkit-tap-highlight-color:transparent} /* ⚠️ overflow:hidden sur html/body est INTERDIT EN PWA (tenté puis retiré le 2026-07-28) : en PWA iOS, si le document ne peut plus défiler, WebKit décale TOUTE la mise en page à l'ouverture du clavier pour montrer le champ actif — la carte de saisie se retrouve plaquée en haut de l'écran. Le rebond élastique du document est le prix à payer ; il est cosmétique, le clavier ne l'est pas. Laisser le document défiler se paie cher, et EN PWA AUSSI : la coque y fait 956 pt (obligatoire pour que la carte atteigne le bas de l'écran) alors que la fenêtre de mise en page n'en fait que 894 → il reste 62 pt de document défilable en permanence, même avec un fil VIDE, et la carte de saisie — posée en bottom:0 dans le body — part avec au lieu de rester collée en bas. On coupe donc le défilement du document PARTOUT (2026-07-29). Le fil, lui, défile toujours dans #chat, qui borne son rebond (overscroll:contain). ⚠️ SI LE CLAVIER iOS SE REMET À TOUT DÉCALER EN PWA, c'est ce overflow:hidden qu'il faut rouvrir en premier — c'était le compromis d'origine. */ /* Hauteur de la coque : elle DIFFÈRE entre PWA et navigateur, il faut les deux. - PWA (écran d'accueil) : 100vh, et SURTOUT ni 100dvh ni 100%. Mesuré sur iPhone (2026-07-28) : la vue web couvre tout l'écran (956 pt — le contenu passe bien sous l'horloge), mais la fenêtre de mise en page ne fait que 894 pt et reste calée en haut. Or 100dvh ET 100% valent tous deux 894 → il restait une bande morte de 62 pt (= la zone sûre du haut) en bas de l'écran, sous la carte de saisie. 100vh se réfère au GRAND viewport (956) : la seule unité juste dans ce cas. - Navigateur (barre d'URL + barre d'outils visibles) : 100vh vaut justement ce GRAND viewport, donc la coque dépasse la zone réellement visible → le document défile alors que le fil est vide, et le bas de la carte de saisie (modèle / contexte) est coupé. Ici c'est 100dvh qu'il faut : la hauteur visible du moment. D'où : 100dvh par défaut, et 100vh UNIQUEMENT en PWA. ⚠️ Le tri se fait sur l'attribut html[data-pwa] (pose par le script du ), 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 est posé par-dessus, piste transparente : sinon on voyait deux barres superposées (la barre de proportion + la piste blanche native du navigateur). Deux teintes du MÊME accent, pas deux couleurs — la barre exprime une proportion, pas deux catégories. */ .gpu-split{position:relative;display:flex;align-items:center;height:22px} .gpu-bar{display:flex;width:100%;height:10px;border-radius:999px;overflow:hidden;background:color-mix(in srgb,var(--dim) 20%,transparent)} .gpu-bar>span{background:var(--accent);flex:none;min-width:0;transition:width .12s} .gpu-bar>span.alt{background:color-mix(in srgb,var(--accent) 42%,transparent)} .gpu-range{position:absolute;inset:0;width:100%;height:22px;margin:0;padding:0;background:transparent;-webkit-appearance:none;appearance:none;cursor:ew-resize} .gpu-range:focus{outline:none} .gpu-range::-webkit-slider-runnable-track{height:22px;background:transparent;border:none} .gpu-range::-moz-range-track{height:22px;background:transparent;border:none} .gpu-range::-moz-range-progress{background:transparent} .gpu-range::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:16px;height:16px;margin-top:3px;border-radius:50%;background:var(--bg);border:2px solid var(--accent);box-shadow:0 1px 3px rgba(0,0,0,.3);cursor:ew-resize} .gpu-range::-moz-range-thumb{width:16px;height:16px;border-radius:50%;background:var(--bg);border:2px solid var(--accent);box-shadow:0 1px 3px rgba(0,0,0,.3);cursor:ew-resize} .gpu-range:focus-visible::-webkit-slider-thumb{box-shadow:0 0 0 3px var(--accent-bg)} .gpu-range:focus-visible::-moz-range-thumb{box-shadow:0 0 0 3px var(--accent-bg)} .gpu-legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:11.5px;color:var(--dim)} .gpu-legend i{display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:5px} .gpu-legend i.alt{background:color-mix(in srgb,var(--accent) 42%,transparent)} .gpu-shares{display:flex;flex-wrap:wrap;gap:10px} .gpu-share{display:flex;align-items:center;gap:5px;font-size:12px;color:var(--dim)} .gpu-share input{width:62px;background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:3px;padding:5px 7px;font:inherit;text-align:right} .gpu-share input:focus{outline:none;border-color:var(--accent)} .pe-inp{width:100%;box-sizing:border-box;background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:4px;padding:9px 11px;font:inherit;transition:border-color .12s,box-shadow .12s} .pe-inp:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-bg)} .pe-row.stack.plain .pe-inp{border:none;background:transparent;padding:6px 0;font-size:15px} .pe-row.stack.plain .pe-inp:focus{box-shadow:none} /* Champ + bouton accolé (téléchargement) — même trait que les autres champs, sans pastille colorée qui ressort. */ .pe-inpbtn{display:flex;align-items:stretch} .pe-inpbtn .pe-inp{border-top-right-radius:0;border-bottom-right-radius:0;border-right:0} /* Le bouton reprend EXACTEMENT le cadre du champ (mêmes coins, même bordure) : sans ça, un thème qui arrondit tous les boutons (ChatGPT) le détachait du champ, avec un coin rond au milieu de la paire. */ .pe-inpbtn button{flex:none;margin:0;border:1px solid var(--border);border-radius:0 4px 4px 0;background:var(--panel);color:var(--dim);padding:0 15px;cursor:pointer;font-size:15px;transition:color .12s,border-color .12s,background .12s} .pe-inpbtn button:hover:not(:disabled){color:var(--bg);background:var(--accent);border-color:var(--accent)} .pe-inpbtn:focus-within .pe-inp,.pe-inpbtn:focus-within button{border-color:var(--accent)} .pe-inpbtn button:disabled{opacity:.5;cursor:default} /* Contrôle segmenté (moteur) façon iOS. */ .pe-seg{display:flex;gap:3px;background:color-mix(in srgb,var(--dim) 16%,transparent);border-radius:4px;padding:3px;width:100%} .pe-seg>label{flex:1;text-align:center;font-size:13px;font-weight:500;padding:7px 4px;border-radius:3px;cursor:pointer;color:var(--dim);transition:background .12s,color .12s} .pe-seg>label:has(input:checked){background:var(--panel);color:var(--text);box-shadow:0 1px 2px rgba(0,0,0,.22)} .pe-seg input{display:none} .pe-seg-note{font-size:11px;color:var(--dim);padding:0 2px;min-height:14px} .pe-note{font-size:11px;color:var(--dim);line-height:1.45} /* Barre de progression (téléchargement de modèle). Sans total connu, elle défile en boucle via .indet. */ .pe-bar{height:4px;border-radius:999px;background:color-mix(in srgb,var(--dim) 26%,transparent);overflow:hidden} .pe-bar>i{display:block;height:100%;width:0;border-radius:999px;background:var(--accent);transition:width .35s linear} .pe-bar.indet>i{width:35%;animation:pe-bar-slide 1.2s ease-in-out infinite;transition:none} .pe-bar.done>i{width:100%} @keyframes pe-bar-slide{0%{margin-left:-35%}100%{margin-left:100%}} /* Action discrète en texte (annuler un téléchargement…). */ .pe-link{align-self:flex-start;border:none;background:none;padding:0;font:inherit;font-size:11px;color:var(--dim);text-decoration:underline;cursor:pointer} .pe-link:hover{color:var(--text)} .pe-link:disabled{opacity:.5;cursor:default;text-decoration:none} /* Interrupteur à glissière. */ .pe-switch{position:relative;flex:none;width:40px;height:24px} .pe-switch input{position:absolute;opacity:0;width:0;height:0} .pe-sl{position:absolute;inset:0;background:color-mix(in srgb,var(--dim) 55%,transparent);border-radius:999px;transition:background .18s} .pe-sl::before{content:"";position:absolute;width:18px;height:18px;left:3px;top:3px;background:#fff;border-radius:50%;transition:transform .18s;box-shadow:0 1px 3px rgba(0,0,0,.25)} .pe-switch input:checked+.pe-sl{background:var(--accent)} .pe-switch input:checked+.pe-sl::before{transform:translateX(16px);background:var(--bg)} .pe-switch input:focus-visible+.pe-sl{box-shadow:0 0 0 3px var(--accent-bg)} /* Repli « avancé » / config brute : une ligne cliquable dans un groupe. */ .pe-adv{background:var(--panel);border:1px solid var(--border);border-radius:3px;overflow:hidden} .pe-adv>summary{cursor:pointer;font-size:14px;color:var(--text);padding:13px 15px;list-style:none;display:flex;align-items:center;user-select:none} .pe-adv>summary::-webkit-details-marker{display:none} .pe-adv>summary::after{content:"";margin-left:auto;width:7px;height:7px;border-right:1.5px solid var(--dim);border-bottom:1.5px solid var(--dim);transform:translateY(-2px) rotate(45deg);transition:transform .15s} .pe-adv[open]>summary::after{transform:translateY(1px) rotate(-135deg)} .pe-adv[open]>summary{border-bottom:1px solid var(--border)} .pe-adv .pe-adv-body{padding:14px 15px;display:flex;flex-direction:column;gap:10px} .pe-adv textarea.pe-inp{min-height:210px;resize:vertical;line-height:1.5;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;white-space:pre-wrap;overflow-wrap:anywhere;overflow-x:hidden} /* Ligne « éditer le .env » : chevron de disclosure + textarea de config brute. */ .pe-caret{display:inline-block;width:7px;height:7px;border-right:1.5px solid var(--dim);border-bottom:1.5px solid var(--dim);transform:rotate(-45deg);transition:transform .15s} .pe-caret.open{transform:rotate(45deg)} #m-content{min-height:220px;resize:vertical;line-height:1.5;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;white-space:pre-wrap;overflow-wrap:anywhere;overflow-x:hidden} /* Nombres avancés : chaque paire label/valeur sur sa propre ligne (réutilise pe-row). */ .pe-adv .pe-list{margin:0} /* Variante « champ en clair » : page mémoire → le contenu est le champ principal. */ .pe-adv.pe-raw-plain{border:none;background:transparent} .pe-adv.pe-raw-plain>summary{display:none} .pe-adv.pe-raw-plain .pe-adv-body{padding:0} /* Pied de modale. */ .pe-foot{padding:9px 12px;border-top:1px solid var(--border);display:flex;justify-content:space-between; align-items:center;gap:8px;flex-wrap:nowrap} .pe-foot .foot-l{display:flex;align-items:center;gap:8px;min-width:0} .pe-foot .foot-r{display:flex;gap:8px;flex:none} .pe-btn{margin:0;padding:7px 15px;font-size:12.5px;border-radius:4px;font-weight:600;cursor:pointer; border:1px solid var(--border);background:var(--panel);color:var(--text);white-space:nowrap} .pe-btn:hover{border-color:var(--dim)} .pe-btn.primary{border-color:var(--accent);background:var(--accent);color:var(--bg)} .pe-btn.primary:hover{filter:brightness(1.08)} /* Suppression : icône seule, en retrait. Un bouton plein « supprimer » pesait autant que « enregistrer » alors que c'est l'action rare — et à trois boutons larges, le pied passait à deux lignes sur téléphone. */ .pe-del{flex:none;display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px; margin:0;padding:0;border:1px solid transparent;border-radius:4px;background:transparent;color:var(--err);cursor:pointer} .pe-del svg{width:15px;height:15px;display:block} .pe-del:hover{background:var(--danger-bg);border-color:color-mix(in srgb,var(--err) 40%,transparent)} /* Option d'une confirmation : une PASTILLE à cocher, pas une case système suivie d'un bout de texte. Éteinte : contour discret. Allumée : elle passe au rouge, comme l'action destructrice qu'elle étend. Vit dans la boîte de confirmation (#ask-check), plus dans le pied de l'éditeur. */ .pe-delmodel{align-self:flex-start;margin-top:14px;display:inline-flex;align-items:center;gap:6px;font-size:11px;font-weight:600; letter-spacing:.02em;color:var(--dim);cursor:pointer;white-space:nowrap; padding:5px 11px;border:1px solid var(--border);border-radius:999px;background:var(--panel); transition:color .12s,border-color .12s,background .12s} .pe-delmodel input{position:absolute;opacity:0;width:0;height:0} /* Le point tient lieu de case à cocher : creux au repos, plein une fois coché. */ .pe-delmodel::before{content:"";width:6px;height:6px;border-radius:50%;flex:none; border:1px solid currentColor;background:transparent} .pe-delmodel:hover{color:var(--text);border-color:var(--dim)} .pe-delmodel:has(input:checked){color:var(--err);border-color:color-mix(in srgb,var(--err) 45%,transparent);background:var(--danger-bg)} .pe-delmodel:has(input:checked)::before{background:currentColor} /* Éditeur de preset/mémoire — mise en forme propre du modale */ .modal-ov{position:fixed;inset:0;background:rgba(0,0,0,.45);z-index:30;align-items:center;justify-content:center;padding:20px} /* Ouverture/fermeture animées. L'état par défaut est l'état FERMÉ ; la classe .show (posée par showModal/hideModal) fait jouer la transition dans les deux sens — une @keyframes n'aurait animé que l'ouverture. */ .modal-ov,.ask-overlay{opacity:0;transition:opacity .16s ease} .modal-ov.show,.ask-overlay.show{opacity:1} .modal-card,.ask-box{transform:translateY(10px) scale(.985);opacity:0; transition:transform .18s cubic-bezier(.2,.8,.3,1),opacity .18s ease} .modal-ov.show .modal-card,.ask-overlay.show .ask-box{transform:none;opacity:1} @media (prefers-reduced-motion:reduce){ .modal-ov,.ask-overlay,.modal-card,.ask-box{transition:none} } .modal-card{position:relative;background:var(--bg);border:1px solid var(--border);border-radius:3px;width:100%;max-width:600px;max-height:90vh;display:flex;flex-direction:column;box-shadow:0 16px 50px rgba(0,0,0,.18)} .modal-head{padding:14px 16px;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:center} .modal-head strong{font-size:14px} /* Croix de fermeture : un CARRÉ, pas un rectangle. Avec un padding asymétrique (3px vertical / 10px horizontal) le glyphe paraissait écrasé et la cible de clic était basse. 32px, glyphe centré, taille de cible correcte au doigt. */ .modal-x{margin:0;padding:0;width:32px;height:32px;flex:none;display:inline-flex;align-items:center;justify-content:center;font-size:19px;line-height:1;color:var(--dim)} .modal-body{padding:16px;overflow:auto;flex:1} /* Modale ouverte mais pas encore remplie : on affiche le voile de chargement À LA PLACE du formulaire et du pied. Estomper le formulaire ne suffisait pas — on voyait quand même les champs s'écrire un par un derrière le voile. */ /* Voile OPAQUE, posé sous l'en-tête (le titre reste lisible) : `visibility:hidden` sur le formulaire suffirait en théorie, mais un fond plein garantit qu'aucun reste du preset précédent (moteur coché, réglages) ne puisse apparaître une fraction de seconde. --mh = hauteur mesurée de l'en-tête, posée à l'ouverture. */ /* ⚠️ Aucune transition tant que le formulaire est caché, NI pendant la frame où il réapparaît : WebKit diffère les transitions des éléments non rendus et les joue à leur retour à l'écran. Le sélecteur de moteur glissait donc de l'ancien preset au nouveau SOUS LES YEUX, une fois le chargement terminé. */ .modal-ov.loading .pe-body *,.modal-ov.no-anim .pe-body *{transition:none!important} .modal-load{display:none;position:absolute;inset:var(--mh,52px) 0 0 0;z-index:2; align-items:center;justify-content:center;background:var(--bg);border-radius:0 0 5px 5px} .modal-ov.loading .modal-load{display:flex} /* visibility (et non display) : le formulaire garde sa place, donc la carte a déjà sa taille finale — la révéler ne provoque plus d'à-coup de hauteur. */ .modal-ov.loading .modal-body, .modal-ov.loading .pe-body, .modal-ov.loading .pe-foot, .modal-ov.loading .modal-foot{visibility:hidden} /* Rond qui tourne — les trois points sont la signature du CHAT, pas des modales. */ .spinner{width:22px;height:22px;border-radius:50%;border:2px solid var(--border); border-top-color:var(--dim);animation:spin .7s linear infinite} @keyframes spin{to{transform:rotate(360deg)}} @media (prefers-reduced-motion:reduce){.spinner{animation-duration:2s}} .modal-foot{padding:12px 16px;border-top:1px solid var(--border);display:flex;justify-content:space-between;align-items:center;gap:8px;flex-wrap:wrap} .foot-l{display:flex;align-items:center;gap:10px;flex-wrap:wrap} .foot-r{display:flex;gap:8px} .mform{display:flex;flex-direction:column;gap:14px} .fld{display:flex;flex-direction:column;gap:5px} .fld-l{font-size:11px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;color:var(--dim)} .fld-l .opt{text-transform:none;letter-spacing:0;font-weight:400;opacity:.65} .fld-hint{font-size:11px;color:var(--dim);margin-top:2px} .ctl{width:100%;box-sizing:border-box;background:var(--panel);color:var(--text);border:1px solid var(--border);border-radius:3px;padding:9px 11px;font:inherit} .ctl:focus{outline:none;border-color:var(--accent)} .ta{min-height:220px;resize:vertical;line-height:1.5} .card{border:1px solid var(--border);border-radius:4px;padding:13px;display:flex;flex-direction:column;gap:13px} .card-h{font-size:11px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--accent)} .row{display:flex;gap:6px} .row .ctl{flex:1;min-width:0} .btn-dl{flex-shrink:0;margin:0} .input-btn{display:flex} .input-btn .ctl{flex:1;min-width:0;border-top-right-radius:0;border-bottom-right-radius:0} .input-btn .btn-in{flex-shrink:0;margin:0;padding:9px 13px;border-left:0;border-top-left-radius:0;border-bottom-left-radius:0} .input-btn .btn-in:hover{border-left:0} .seg{display:flex;gap:6px} .seg>label{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;text-align:center;min-height:46px;padding:6px 4px;border:1px solid var(--border);border-radius:4px;cursor:pointer;font-size:12px;background:var(--panel);transition:border-color .12s,background .12s,color .12s} .seg>label:hover{border-color:var(--accent)} .seg>label:has(input:checked){border-color:var(--accent);background:var(--accent-bg);color:var(--accent)} .seg input{display:none} .seg .be-note{font-size:10px;color:var(--dim);margin:0} .btn-danger{background:var(--danger-bg);border-color:var(--err);color:var(--err);margin:0} .btn-save{border-color:var(--accent);color:var(--accent);margin:0} .del-model{align-items:center;gap:5px;font-size:11px;color:var(--dim)} .del-model input{margin:0} .lc-logbar{display:flex;align-items:center;gap:6px;cursor:pointer;user-select:none;font-size:12px;color:var(--dim);font-weight:600;padding:6px 0;margin-top:2px} .lc-logbar #lc-log-caret{font-size:9px;transition:transform .12s;display:inline-block} .lc-logbar.open #lc-log-caret{transform:rotate(90deg)} #lc-log{background:var(--panel);border:1px solid var(--border);border-radius:3px;padding:8px;font-size:10px;line-height:1.45;max-height:220px;overflow:auto;white-space:pre-wrap;word-break:break-all;margin:0} .lc-spin{display:inline-block;animation:spin 1s linear infinite} /* Serveurs MCP */ .mcp-list{display:flex;flex-direction:column;gap:4px} .mcp-row{display:flex;align-items:center;gap:8px;padding:6px 10px;border:1px solid var(--border);border-radius:3px;background:var(--panel);cursor:pointer} .mcp-row:hover{border-color:var(--accent)} .mcp-row.off{opacity:.6} .mcp-dot{flex:none;width:8px;height:8px;border-radius:50%} .mcp-dot-ok{background:var(--accent)} .mcp-dot-err{background:var(--err)} .mcp-dot-off{background:var(--dim)} .mcp-info{flex:1;min-width:0} .mcp-name{font-size:12px;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap} .mcp-meta{display:flex;align-items:center;gap:6px;margin-top:2px} .mcp-tag{font-size:10px;padding:1px 7px;border-radius:4px;border:1px solid var(--border);color:var(--dim);text-transform:uppercase;letter-spacing:.04em} /* Version du moteur : même pastille, mais SANS majuscules forcées — un numéro de build et un hash de commit se recopient et se comparent à l'œil, et « AAAA111 » n'est pas le commit « aaaa111 ». Le monospace vient de là aussi. */ .eng-ver{font-family:var(--mono);text-transform:none;letter-spacing:0;color:var(--text)} .mcp-tools{font-size:10px;color:var(--accent)} .mcp-err{font-size:10px;color:var(--err)} .mcp-switch{flex:none} .mcp-edit{flex:none;padding:2px 5px;font-size:13px;line-height:1;background:transparent;border:none;opacity:.5} .mcp-edit:hover{opacity:1;color:var(--accent)} /* Liste des outils MCP dans la modale */ /* Sélecteur d'intensité du raisonnement, dans la barre de saisie. Discret par défaut (même poids visuel que les icônes voisines), il ne s'affirme qu'au survol ou quand une intensité est imposée. */ #think-effort{flex:none;align-self:center;height:22px;padding:0 4px;font-size:11px; color:var(--dim);background:transparent;border:1px solid transparent;border-radius:4px;cursor:pointer} #think-effort:hover:not(:disabled){color:var(--text);border-color:var(--border)} #think-effort:disabled{opacity:.4;cursor:not-allowed} .mcp-actions{display:flex;gap:6px;margin-top:6px} .mcp-cat-tabs{display:flex;flex-wrap:wrap;gap:4px;margin-bottom:8px} .mcp-cat{font-size:11px;padding:2px 9px;border-radius:4px;border:1px solid var(--border);background:transparent;color:var(--dim);cursor:pointer} .mcp-cat:hover{border-color:var(--accent);color:var(--text)} .mcp-cat.on{border-color:var(--accent);color:var(--accent)} .mcp-cat-list{display:flex;flex-direction:column;gap:4px;max-height:52vh;overflow:auto} .mcp-cat-row{display:flex;align-items:center;gap:8px;padding:7px 10px;border:1px solid var(--border);border-radius:3px;background:var(--panel)} .mcp-cat-row:hover{border-color:var(--accent)} .mcp-cat-desc{font-size:11px;color:var(--dim);margin-top:2px;line-height:1.35} .mcp-cat-cmd{font-size:10px;color:var(--dim);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:340px} .mcp-cat-use{flex:none} .mcp-tools-list{display:flex;flex-direction:column;gap:2px;max-height:240px;overflow:auto;border:1px solid var(--border);border-radius:3px;padding:6px} .mcp-tool-row{display:flex;align-items:center;gap:10px;padding:4px 6px;border-radius:3px;cursor:pointer} .mcp-tool-row:hover{background:var(--panel)} .mcp-tool-switch{flex:none;transform:scale(.85)} .mcp-tool-name{font-size:12px;color:var(--text);word-break:break-all} /* Accès OpenAI / Accès distant : champs lecture seule (URL, clé) + libellés compacts, pour une présentation homogène et sans surcharge visuelle. */ .io-input{flex:1;min-width:0;background:var(--panel);color:var(--text);border:1px solid var(--border);border-radius:4px;padding:8px 10px;font:inherit;font-size:13px} /* Modale MCP (ajout/édition) : les champs des groupes stdio/http n'avaient aucun espacement vertical (le conteneur n'est pas un flex à gap) → ils se touchaient. On aligne leur rythme sur celui du reste du formulaire (.mform). */ #mcp-stdio-fields,#mcp-http-fields{display:flex;flex-direction:column;gap:14px} /* ===== Mise en page de la conversation ============================= Colonne de lecture centrée, réponses SANS bulle (texte nu sur le fond), question de l'utilisateur en pastille arrondie à droite, composer en « pilule », pas d'étiquettes VOUS/Loki. Ces règles surchargent la mise en page générique plus haut : elles doivent donc rester APRÈS elle. */ body{font-size:15px} h1{color:var(--text);font-weight:600;letter-spacing:-.01em} /* Barre latérale : plus sombre/atténuée que la zone de conversation. */ .side{background:var(--panel);border-right-color:transparent} @media (max-width:720px){.side{background:var(--panel)}} /* Colonne de lecture centrée (comme le fil ChatGPT), pleine largeur sur mobile. ⚠️ JAMAIS le raccourci `padding` ici : le rembourrage BAS réserve la hauteur de la carte de saisie (voir #chat plus bas) et il est réglé ailleurs. Une règle en raccourci le remet silencieusement à sa valeur du raccourci — c'est exactement ce qui, jusqu'à ce correctif, laissait la fin du fil coincée sous la carte au-delà de 721 px de large. Longhands uniquement. */ #chat{padding-top:28px;padding-inline:max(16px,calc(50% - 380px));gap:22px;scrollbar-gutter:stable both-edges} /* Réponses de l'assistant : pas de cadre, pas de fond — juste du texte. */ #chat .msg.assistant{background:transparent;border:none;padding:0;max-width:100%;line-height:1.7} /* Question : pastille arrondie à droite, texte lisible sur fond clair OU sombre. */ #chat .msg.user{background:var(--bubble);color:var(--text);border-radius:3px;padding:11px 18px;max-width:75%;line-height:1.6} #chat .msg.user .label{color:var(--dim)} /* Libellés : l'amont les masquait tous ici, les mots « user » / « assistant » n'ajoutant rien à l'alignement. Ils portent désormais un avatar et un prénom (voir 17-identity.js), qui eux identifient vraiment l'interlocuteur — on les réaffiche donc, en ligne et discrets. Restent masqués les libellés SANS avatar, c'est-à-dire les bulles techniques non repliables. */ #chat .msg .label{display:none} #chat .msg.collapsible .label{display:block} #chat .msg.user .label,#chat .msg.assistant .label{display:flex;align-items:center;gap:0;opacity:.85} /* Côté question, le libellé s'aligne à droite comme la bulle. */ #chat .msg.user .label{justify-content:flex-end} /* Mesures de génération sous la réponse (prefill / decode). Assez discrètes pour ne pas concurrencer le texte, assez lisibles pour être utiles. Escamotables depuis Apparence → Affichage. */ #chat .msg .statline{margin-top:10px;font-family:var(--mono);font-size:10.5px;letter-spacing:.02em; color:var(--dim);opacity:.75} #chat .msg .statline:empty{display:none} /* « Masquer la vitesse de génération » ne masque QUE la vitesse : le temps de travail du tour (.worktime) n'est pas une mesure de moteur, c'est ce que la réponse a coûté. Une ligne qui ne portait que de la vitesse disparaît en entier — sans .has-work il ne resterait qu'un blanc. */ html[data-hide-stats="1"] #chat .msg .statline>.spd{display:none} html[data-hide-stats="1"] #chat .msg .statline:not(.has-work){display:none} /* Arrivée en fondu montant plutôt qu'un surgissement sec. L'opacité finale (.75) est celle de la règle ci-dessus : l'animation doit y ATTERRIR, sinon la ligne clignote en repassant à sa valeur de repos. */ #chat .msg .statline-in{animation:statin .28s cubic-bezier(.2,.7,.3,1) both} @keyframes statin{from{opacity:0;transform:translateY(3px)}to{opacity:.75;transform:none}} @media (prefers-reduced-motion:reduce){#chat .msg .statline-in{animation:none}} /* Raisonnement / outils : encart gris discret plutôt qu'un cadre pointillé. */ #chat .msg.reasoning,#chat .msg.tool{background:var(--panel);border:1px solid var(--border);border-radius:3px;color:var(--dim);padding:12px 16px} #chat .msg.reasoning{color:var(--dim)} #chat .msg.tool .tool-head{color:var(--text)} #chat .msg.assistant .body pre,#chat .msg.reasoning .body pre,#chat .msg.tool pre{border-radius:3px;padding:12px 14px} #chat .msg.assistant .body a{text-decoration:underline;text-underline-offset:2px} /* Composer = UNE seule carte posée PAR-DESSUS le fil : la conversation continue de défiler DERRIÈRE elle et s'efface en fondu (dégradé vers le fond) au lieu de s'arrêter net au-dessus. Dans la carte, de haut en bas : la saisie et ses boutons, un pied discret (préréglage · contexte · compacter), puis la jauge de contexte qui FORME le bord bas — elle épouse les coins arrondis (overflow:hidden sur le rayon). Les trois blocs sont des frères dans le HTML : on les recolle en dessinant les bordures par morceaux et l'ordre visuel vient de l'ordre flex — aucun changement de markup. */ /* Le padding BAS dégage l'indicateur d'accueil de l'iPhone en PWA : la carte flotte au-dessus de lui, son arrondi bas reste donc bien visible. Hors PWA l'inset vaut 0 → strictement les 8 px d'origine. */ /* --fade = la bande transparente au-dessus de la carte, celle où le fil doit s'effacer. Le dégradé s'exprime dans la MÊME unité (et non en % de la hauteur totale du composeur, qui varie) : l'opacité est donc pleine exactement au bord haut de la carte. Avec un %, le fondu finissait SOUS ce bord et le texte restait lisible au ras de la carte — l'impression de « passer derrière ». */ #composer{position:absolute;left:0;right:0;bottom:0;z-index:4;border-top:none;display:flex;flex-direction:column;--fade:34px;padding:var(--fade) 0 calc(8px + env(safe-area-inset-bottom));--col:calc(max(16px,calc(50% - 380px)) + var(--sbw,0px));background:linear-gradient(to bottom,transparent 0,var(--bg) var(--fade))} /* Réserve sous le fil = hauteur RÉELLE du composeur, mesurée et publiée par syncComposer() (08-chat-render.js). Une constante redevient fausse dès que la saisie grandit ou qu'une pièce jointe s'affiche, et la fin du fil repasse hors d'atteinte. 170px n'est que le repli du tout premier rendu, avant mesure. Pas de env(safe-area-inset-bottom) ici : il est déjà DANS le padding bas du composeur, donc dans sa hauteur mesurée — le compter deux fois creusait un trou en PWA iOS. */ #chat{padding-bottom:calc(var(--composer-h,170px) + 12px)} #scrollbtn{bottom:calc(var(--composer-h,170px) + 10px);border-radius:999px} #inputbar{order:1;margin:0 var(--col);padding:8px 8px 8px 16px;gap:8px;align-items:flex-end;background:var(--panel);border:1px solid var(--border);border-bottom:none;border-radius:4px 4px 0 0} #input{background:transparent;border:none;border-radius:0;padding:9px 0;min-height:36px;line-height:18px;max-height:160px} /* Pas de cadre qui s'assombrit au focus : la carte garde exactement la même bordure, seule la saisie change (curseur). */ #input:focus{border-color:transparent} /* Pied de carte : c'est LUI qui porte l'arrondi bas (26 px, comme en haut) et qui clippe la jauge. Un bandeau de 7 px ne peut pas dessiner un rayon de 26 px (le navigateur l'écrase, d'où les coins bizarres) : on laisse donc la carte découper la jauge dans sa propre forme — la barre est bien épaisse ET rognée exactement sur la courbe, symétrique du haut. */ #ctxmeta{order:2;margin:0 var(--col);padding:6px 16px 15px;gap:10px;background:var(--panel);border:1px solid var(--border);border-top:none;border-radius:0 0 8px 8px;overflow:hidden} /* ⚠️ NE PAS piloter la position du composer en JS quand le clavier s'ouvre (tenté puis retiré le 2026-07-28, deux fois) : ni asservir la hauteur de la coque à visualViewport.height, ni décaler le composer de la hauteur du clavier. Dans les deux cas la carte part en haut de l'écran en PWA iOS. iOS gère déjà correctement le clavier tout seul — on le laisse faire. */ #ctx-text,#status-preset{color:var(--dim);font-size:10px;letter-spacing:.02em;white-space:nowrap} #ctx-compact{border-radius:999px;padding:2px 10px;font-size:10px;background:transparent;border-color:var(--border);color:var(--dim)} #ctx-compact:hover{border-color:var(--dim);color:var(--text)} /* Jauge = bord bas de la carte : épaisse, plaquée au ras du bord, rognée par l'arrondi du pied de carte. */ #ctxtrack{top:auto;bottom:0;height:7px;background:var(--border)} #ctx-fill{background:var(--text);opacity:.75} #sendhint{order:4;padding:12px var(--col) 2px} /* ===== Pièces jointes ============================================== Un fichier joint n'est jamais lu par la page : il est déposé dans le dossier de travail de l'IA et seul son nom revient. L'affichage se limite donc à une pastille — pas d'aperçu, pas de vignette, rien qui promette plus que ça. Le trombone vit dans le PIED de la carte, à droite avec le décompte de contexte et « compacter ». Il est à LEUR échelle (une ligne d'outils de 10 px), pas à celle de « send » : joindre un fichier est un geste occasionnel. */ #attach{width:18px;height:18px;min-height:18px;flex:none;margin:0;padding:0; display:flex;align-items:center;justify-content:center;border-radius:999px; background:transparent;border:1px solid transparent;color:var(--dim)} #attach:hover{background:var(--row-bg-on);border-color:transparent;color:var(--text)} #attach:active{transform:scale(.9)} #attach svg{display:block} /* Fichiers en attente : de petites bulles POSÉES AU-DESSUS de la carte. Le composeur réserve déjà 34 px transparents au-dessus de lui (la bande de dégradé qui fond vers le fil) : les pastilles s'y logent, et on retire d'autant le rembourrage. La carte ne bouge donc pas — c'est la bande vide qui se remplit, au lieu du composeur qui grandit vers le haut. Elles ne vont PAS dans le pied : cette ligne appartient au nom du modèle. */ #attach-list{order:0;display:none;gap:6px;margin:0 var(--col);padding:0 2px 8px; overflow-x:auto;scrollbar-width:none} #attach-list::-webkit-scrollbar{display:none} #attach-list.show{display:flex} #composer:has(#attach-list.show){--fade:6px} /* File d'attente : messages envoyés pendant une réponse, pas encore pris en compte. Même bande que les pièces jointes, en gris. */ #queue-list{order:0;display:none;flex-direction:column;align-items:flex-end;gap:4px;margin:0 var(--col);padding:0 2px 8px} #queue-list.show{display:flex} #composer:has(#queue-list.show){--fade:6px} #queue-list .queued-msg{max-width:80%;padding:5px 12px;border-radius:12px;background:var(--panel); border:1px dashed var(--border);color:var(--dim);font-size:12px;line-height:1.35; white-space:pre-wrap;overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical; box-shadow:0 2px 10px rgba(0,0,0,.10)} #queue-list .queued-msg::before{content:"en attente · ";font-style:italic;opacity:.8} /* Pendant une réponse, envoyer reste possible dès qu'il y a du texte : le message part en file (voir send()). Stop reste à côté. */ html[data-busy="1"][data-hastext="1"] #send{display:inline-flex} /* Posées sur le fil (et non sur la carte) : il leur faut un fond opaque et un cadre, sinon le texte de la conversation transparaît derrière. */ #attach-list .chip-file{flex:0 0 auto;background:var(--panel);border-color:var(--border); box-shadow:0 2px 10px rgba(0,0,0,.10)} /* Pastille de fichier. Le × n'apparaît qu'au survol : au repos la pastille doit rester lisible d'un coup d'œil (nom + poids), pas encombrée d'un bouton. */ .chip-file{display:flex;align-items:center;gap:8px;max-width:230px;min-width:0; padding:4px 12px;border-radius:999px;background:var(--row-bg-on); border:1px solid transparent;font-size:12px;line-height:1.3} /* Avec un × à droite, la pastille se ferme SUR lui : le rembourrage de droite revient à l'épaisseur du rond, sinon le bouton flotte loin du bord et la pastille paraît de travers. */ .chip-file:has(button){padding-right:4px} .chip-file .cf-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap} .chip-file .cf-size{flex:none;color:var(--dim);font-size:11px} /* Le × est TOUJOURS visible : le révéler au survol le rendait inatteignable sur mobile, où il n'y a pas de survol — donc impossible de retirer un fichier. */ .chip-file button{flex:none;margin:0;padding:0;width:20px;height:20px;font-size:14px; display:flex;align-items:center;justify-content:center;line-height:1; border-radius:50%;border:none;background:transparent;color:var(--dim)} .chip-file button:hover{background:var(--bubble);color:var(--text)} /* Transfert en cours : un liseré qui tourne, plutôt qu'une pastille grisée dont on ne sait pas si elle travaille ou si elle a échoué. */ .chip-file.up .cf-size::after{content:"";display:inline-block;vertical-align:-2px; margin-left:7px;width:10px;height:10px;border-radius:50%; border:1.5px solid var(--border);border-top-color:var(--text);animation:spin .7s linear infinite} .chip-file.err{background:var(--danger-bg);color:var(--err)} .chip-file.err .cf-size{color:var(--err)} @media (prefers-reduced-motion:reduce){.chip-file.up .cf-size::after{animation:none}} /* Fichiers d'un message envoyé : leur propre rangée AU-DESSUS de la bulle, alignée à droite comme elle. Rangée et bulle restent deux objets distincts, séparés par l'espacement normal du fil — les rapprocher les faisait lire comme une seule bulle difforme qui déborde vers le haut. */ /* align-items:flex-start : depuis que la rangée peut contenir une VIGNETTE, une pastille texte voisine s'étirait à sa hauteur (étirement par défaut du flex) — et, arrondie à 999px, se retrouvait en gros ovale. Chaque objet garde sa hauteur propre. */ #chat .msg-files{align-self:flex-end;display:flex;flex-wrap:wrap;justify-content:flex-end; align-items:flex-start;gap:6px;max-width:75%} #chat .msg-files .chip-file{background:var(--bubble);max-width:100%} /* Vignette dans une pastille (composeur) : assez grande pour reconnaître la capture qu'on vient de coller, assez petite pour que la rangée reste une rangée de pastilles et pas une planche-contact. */ .chip-file .cf-thumb{flex:none;width:26px;height:26px;object-fit:cover;border-radius:3px; background:var(--panel)} .chip-file.has-thumb{padding-left:4px} /* Pièce jointe IMAGE d'un message envoyé : la vignette remplace la pastille. Plafonnée pour ne pas déséquilibrer le fil ; cliquable pour la voir en grand. */ #chat .msg-files .msg-img{display:flex;max-width:100%;border-radius:4px;overflow:hidden; border:1px solid var(--border);background:var(--panel)} /* margin/border remis à zéro : .chatimg (posée par hydrateImages) s'applique aussi ici, or le cadre est déjà porté par la tuile. */ #chat .msg-files .msg-img img{display:block;max-width:200px;max-height:160px; width:auto;height:auto;object-fit:contain;cursor:zoom-in;margin:0;border:none;border-radius:0} /* Images posées par l'IA dans sa réponse (captures, /api/chat/image). Sans ce plafond elles sortaient à leur taille NATIVE : une capture d'écran remplissait la colonne entière et poussait le texte hors de vue. */ #chat .chatimg{display:block;max-width:100%;max-height:280px;width:auto;height:auto; margin:6px 0;border:1px solid var(--border);border-radius:4px;cursor:zoom-in} /* Loupe : l'image reprend toute la fenêtre, moins une marge. Le fond ferme au clic (curseur zoom-out), l'image non — on veut pouvoir la regarder de près. */ #lightbox{position:fixed;inset:0;z-index:80;display:none;align-items:center; justify-content:center;padding:28px;background:rgba(0,0,0,.72);cursor:zoom-out; opacity:0;transition:opacity .16s ease} #lightbox.show{opacity:1} #lightbox img{max-width:100%;max-height:100%;width:auto;height:auto;object-fit:contain; border-radius:4px;box-shadow:0 16px 50px rgba(0,0,0,.45);cursor:default} #lightbox .lb-x{position:absolute;top:10px;right:14px;margin:0;padding:0; width:36px;height:36px;display:flex;align-items:center;justify-content:center; font-size:22px;line-height:1;border:none;border-radius:50%; background:rgba(0,0,0,.35);color:#fff} #lightbox .lb-x:hover{background:rgba(0,0,0,.6)} #lightbox .lb-name{position:absolute;left:0;right:0;bottom:10px;text-align:center; color:#fff;opacity:.72;font-size:12px;pointer-events:none} @media (prefers-reduced-motion:reduce){#lightbox{transition:none}} /* Lien de téléchargement dans une réponse : un lien normal, mais la flèche dit qu'il ramène un fichier plutôt qu'il n'ouvre une page. */ #chat .msg .filelink{cursor:pointer} #chat .msg .filelink::after{content:"↓";margin-left:5px;font-size:.9em;opacity:.7;text-decoration:none;display:inline-block} #chat .msg .filelink.busy{opacity:.55;pointer-events:none} /* Voile de dépôt. Il couvre la zone de conversation, curseur inclus : c'est la réponse à « est-ce que je peux lâcher ici ? », elle doit être sans ambiguïté. */ #dropveil{position:absolute;inset:0;z-index:20;display:none;align-items:center; justify-content:center;padding:24px;background:rgba(0,0,0,.28);pointer-events:none} #dropveil.show{display:flex} #dropveil>div{padding:22px 30px;border:2px dashed var(--border);border-radius:3px; background:var(--panel);color:var(--text);font-size:14px;box-shadow:0 8px 30px rgba(0,0,0,.18)} /* Reste de l'UI : coins arrondis et contrastes doux, dans le même esprit. */ button,.mem-search{border-radius:4px} /* Champ + bouton accolés : on ne garde l'arrondi que sur les coins extérieurs. */ .pe-inpbtn .pe-inp{border-radius:4px 0 0 4px} .pe-inpbtn button{border-radius:0 4px 4px 0} .input-btn .ctl{border-radius:4px 0 0 4px} .input-btn .btn-in{border-radius:0 4px 4px 0} .preset{border-radius:3px} /* Les presets restent des cartes (fond + bordure) : à plat, sans fond ni cadre, la liste ne se lisait plus comme des lignes distinctes. L'actif se signale par sa bordure — un simple voile d'accent (7 % de blanc sur fond noir) était invisible en mode sombre. */ .preset{background:var(--panel);border-color:var(--border)} .preset:hover{background:var(--accent-bg);border-color:var(--dim)} .preset.active{background:var(--panel);border-color:var(--text);color:var(--text)} .subhead,.lc-mode-go,.lc-mode-active-tag{color:var(--dim)} button:hover{border-color:var(--dim);color:var(--text)} /* Encart de garde-fou : c'est le serveur qui parle (limite d'appels d'outils, boucle détectée), pas le modèle. Pastille discrète plutôt qu'une erreur — la génération s'est arrêtée proprement, rien n'a échoué. */ .stopnote{display:inline-flex;align-items:center;gap:7px;margin-top:4px;padding:4px 12px; border:1px solid var(--border);border-radius:999px;background:var(--panel); color:var(--dim);font-size:12px;line-height:1.5} .stopnote::before{content:"";flex:none;width:6px;height:6px;border-radius:50%;background:var(--dim)} /* ===== Fenêtres et sélecteurs ===================================== Coins arrondis et ombre douce plutôt qu'un cadre marqué. */ .modal-card,.ask-box,.modal-box,.pe-list,.pe-adv{border-radius:3px} .modal-card,.ask-box{box-shadow:0 8px 30px rgba(0,0,0,.12)} /* Sélecteurs segmentés : noir plein quand c'est coché, pas de couleur. */ .pe-seg>label:has(input:checked),.seg>label:has(input:checked){background:var(--text);border-color:var(--text);color:var(--bg)} /* Curseur de portée de l'export. Même vocabulaire visuel que la répartition GPU (piste arrondie + pastille cerclée), mais une seule valeur : la position tout à droite vaut « toute la conversation ». Le libellé sous la piste donne le compte RÉEL d'échanges et le poids du fichier, la question qu'on se pose vraiment avant d'exporter. */ .xr{position:relative;display:flex;align-items:center;height:22px} .xr-track{width:100%;height:10px;border-radius:999px;overflow:hidden;background:color-mix(in srgb,var(--dim) 20%,transparent)} .xr-fill{height:100%;background:var(--accent);border-radius:999px;transition:width .12s} .xr-range{position:absolute;inset:0;width:100%;height:22px;margin:0;padding:0;background:transparent;-webkit-appearance:none;appearance:none;cursor:ew-resize} .xr-range:focus{outline:none} .xr-range::-webkit-slider-runnable-track{height:22px;background:transparent;border:none} .xr-range::-moz-range-track{height:22px;background:transparent;border:none} .xr-range::-moz-range-progress{background:transparent} .xr-range::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:16px;height:16px;margin-top:3px;border-radius:50%;background:var(--bg);border:2px solid var(--accent);box-shadow:0 1px 3px rgba(0,0,0,.3);cursor:ew-resize} .xr-range::-moz-range-thumb{width:16px;height:16px;border-radius:50%;background:var(--bg);border:2px solid var(--accent);box-shadow:0 1px 3px rgba(0,0,0,.3);cursor:ew-resize} .xr-range:focus-visible::-webkit-slider-thumb{box-shadow:0 0 0 3px var(--accent-bg)} .xr-range:focus-visible::-moz-range-thumb{box-shadow:0 0 0 3px var(--accent-bg)} .xr-range:disabled{cursor:default;opacity:.5} /* Conversation vide : un seul message centré à la place des réglages. */ .xempty{display:flex;flex-direction:column;align-items:center;gap:6px;padding:44px 20px;text-align:center;color:var(--text);font-size:14px} .xempty-i{font-size:30px;line-height:1;color:var(--dim);opacity:.7} /* ===== Direction « Terre » : surfaces en cartes ======================= La charte pose des CARTES arrondies séparées par une gouttière, au lieu d'une colonne à filets. Bloc en fin de feuille pour prendre le pas sur les règles de structure d'origine sans les réécrire. Bureau UNIQUEMENT : sur téléphone la barre latérale est un tiroir plein écran et le fil occupe toute la largeur — y poser des cartes ne ferait que grignoter des pixels précieux. */ @media (min-width:721px){ /* Coque : le fond « terre » se voit dans la gouttière entre les deux cartes. */ body{padding:12px;gap:12px} html[data-pwa="1"] body{padding-top:calc(12px + env(safe-area-inset-top))} /* Barre latérale : pile de cartes, sans filet ni bordure de colonne. */ .side{width:312px;border-right:0;padding:0;background:transparent; display:flex;flex-direction:column;gap:12px;overflow-y:auto;overscroll-behavior:contain} .sidecard,.side>details{background:var(--panel);border:0;border-top:0;border-radius:3px; box-shadow:var(--shadow);margin:0;padding:2px 16px} .sidecard{padding:15px 18px 14px;display:flex;flex-direction:column;gap:2px} .side>details[open]{padding-bottom:14px} .side>details>summary{height:42px} /* L'étiquette « Paramètres » n'est plus un filet mais un intertitre entre deux cartes : le fond de la coque fait déjà la séparation. */ .side-sep{border-top:0;margin:10px 0 0;padding:0 6px} /* Blocs encastrés DANS une carte : un ton plus chaud, sans bordure — deux cadres imbriqués alourdiraient inutilement. */ .side .slist{background:var(--row-bg);border-color:transparent;border-radius:3px} /* Colonne principale : une seule grande carte qui contient fil et saisie. */ .main{background:var(--panel);border-radius:3px;box-shadow:var(--shadow);overflow:hidden} #chat{padding-top:26px;padding-inline:max(20px,calc(50% - 360px))} /* Le dégradé qui fait disparaître le fil sous la carte de saisie doit fondre vers le PANNEAU, pas vers le fond de la coque. */ #composer{background:linear-gradient(to bottom,transparent 0,var(--panel) var(--fade))} /* Carte de saisie : posée SUR le panneau, donc un ton au-dessus, et sans bordure — c'est l'élévation qui la détache. */ #inputbar,#ctxmeta{background:var(--row-bg-on);border-color:transparent} #input{background:transparent} } /* --- Terre : densité et accents ------------------------------------- */ @media (min-width:721px){ /* Gouttière resserrée : la charte empile beaucoup de cartes, 12 px entre chacune repoussait « Actions » loin sous la ligne de flottaison. */ .side{gap:8px} .side>details>summary{height:38px} /* Discussion active : un cadre sauge COMPLET, mais fin (1 px) — le liseré de 3 px sur le seul bord gauche se lisait mal et déséquilibrait la ligne. La bordure transparente est portée par tous les états : la sélection ne décale donc pas le contenu d'un pixel. */ .side .preset{border:1px solid transparent;border-radius:3px;background:transparent} .side .preset.active{background:var(--row-bg-on);border-color:var(--accent)} .side .preset:hover{background:var(--row-bg)} /* Bulle de question : coin bas-droit rentré, comme une bulle qui pointe vers son auteur. */ #chat .msg.user{border-radius:4px} } /* Action principale en sauge : c'est la couleur d'action de la charte, et elle se distingue enfin du texte (le bouton reprenait --text, donc un brun sombre indistinct du reste). L'arrêt, lui, reste en terre cuite. */ /* --- Terre : largeur de lecture -------------------------------------- La mesure était plafonnée à 760 px et centrée sur la fenêtre : sur un écran large, le fil se réduisait à une colonne étroite entourée de vide, alors que la charte fait courir le texte sur toute la largeur du panneau. Une seule variable pilote désormais le fil ET la carte de saisie — ils étaient réglés séparément (450 / 380 / 360 px), donc condamnés à diverger. La borne haute reste : au-delà, une ligne devient pénible à lire (l'œil perd le début de la suivante). 1080 px ≈ 110 caractères, large mais tenable. */ @media (min-width:721px){ :root{--measure:min(1080px,100%)} #chat{padding-top:26px;padding-inline:max(20px,calc(50% - var(--measure)/2))} #composer{--col:calc(max(20px,calc(50% - var(--measure)/2)) + var(--sbw,0px))} } /* --- Terre : rangée de bulles techniques ------------------------------ Raisonnement, outils et mémoire ne portent qu'une étiquette tant qu'ils sont repliés : les empiler gaspillait une ligne entière par appel et repoussait la réponse hors de l'écran. Ils défilent donc en rangée, et reprennent la pleine largeur dès qu'on en déplie un. */ .toolrow{display:flex;flex-wrap:wrap;gap:8px;align-items:flex-start} .toolrow>.msg.collapsible{margin:0;max-width:100%} .toolrow>.msg.collapsible.collapsed{width:auto} /* Dépliée : la bulle passe seule sur sa ligne, sinon son contenu serait comprimé dans une colonne étroite à côté d'une pastille. */ .toolrow>.msg.collapsible:not(.collapsed){flex:1 0 100%} /* --- Terre : sections groupées en cartes ------------------------------ Une carte par section empilait onze arrondis et autant de gouttières : la barre latérale débordait de l'écran alors que la plupart de ces lignes sont des titres repliés d'une seule ligne. Les sections d'IA d'un côté, celles de l'application de l'autre, tiennent maintenant chacune dans UNE carte, avec un filet discret entre les lignes. */ @media (min-width:721px){ .sidegroup{background:var(--panel);border-radius:3px;box-shadow:var(--shadow); padding:2px 16px;display:flex;flex-direction:column} /* Dans un groupe, les sections ne sont plus des cartes : elles redeviennent des lignes, séparées par un filet — sauf la première. */ .sidegroup>details{background:transparent;box-shadow:none;border-radius:0;padding:0; border-top:1px solid var(--border)} .sidegroup>details:first-child{border-top:0} .sidegroup>details[open]{padding-bottom:12px} } /* --- Recherche Hugging Face ------------------------------------------- Deux listes successives dans la même zone (dépôts, puis fichiers du dépôt choisi) : une seule mise en forme sert aux deux, sinon elles divergent. La liste est bornée en hauteur et défile — un dépôt publie facilement vingt quantifications, et la modale ne doit pas s'étirer jusqu'à sortir de l'écran. */ .hf-list{display:flex;flex-direction:column;max-height:220px;overflow-y:auto; border:1px solid var(--border);border-radius:3px;margin-top:6px} .hf-row{display:flex;align-items:center;gap:10px;padding:7px 12px;cursor:pointer; border-top:1px solid var(--border);font-size:12px} .hf-row:first-child{border-top:0} .hf-row:hover{background:var(--row-bg-on)} /* Le nom prend la place restante et se tronque : un identifiant de dépôt long ne doit pas pousser la taille et le verdict hors de la ligne. */ .hf-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text)} .hf-meta{flex:none;color:var(--dim);font-family:var(--mono);font-size:10.5px;white-space:nowrap} /* Verdict mémoire. La couleur seule ne suffit pas — le mot est écrit dedans, et l'explication complète est dans l'infobulle. */ .hf-fit{flex:none;padding:1px 8px;border-radius:999px;font-size:10px;letter-spacing:.03em; text-transform:uppercase;border:1px solid transparent} .hf-fit.ok{color:var(--ok);border-color:var(--ok)} .hf-fit.juste{color:var(--warn);border-color:var(--warn)} .hf-fit.trop{color:var(--err);border-color:var(--err)} /* Verrou d'accès : ce n'est pas un verdict mémoire, mais ça se lit au même endroit et ça décide autant de la suite — un dépôt gated ne se télécharge pas sans jeton. */ .hf-fit.gated{color:var(--warn);border-color:var(--warn);text-transform:none} /* Déjà sur le disque : ce n'est pas un verdict mémoire non plus, mais c'est l'information qui décide du clic — cliquer sélectionne au lieu de télécharger. */ .hf-fit.here{color:var(--accent);border-color:var(--accent);text-transform:none} .hf-head{display:flex;align-items:center;gap:10px;margin-top:6px;font-size:12px} /* --- Modèles installés (liste + suppression) -------------------------------- Même allure que la liste Hugging Face juste au-dessus, mais la LIGNE n'est pas cliquable : ici c'est le bouton « supprimer » qui agit, et un curseur main sur toute la ligne laisserait croire le contraire. */ .mdl-list{display:flex;flex-direction:column;max-height:240px;overflow-y:auto; border:1px solid var(--border);border-radius:3px;margin-top:6px} .mdl-row{display:flex;align-items:center;gap:10px;padding:7px 12px; border-top:1px solid var(--border);font-size:12px} .mdl-row:first-child{border-top:0} .mdl-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text)} .mdl-meta{flex:none;color:var(--dim);font-family:var(--mono);font-size:10.5px;white-space:nowrap} .mdl-tag{flex:none;padding:1px 8px;border-radius:999px;font-size:10px;letter-spacing:.03em; border:1px solid var(--border);color:var(--dim)} .mdl-tag.on{color:var(--accent);border-color:var(--accent)} .mdl-tag.warn{color:var(--warn);border-color:var(--warn)} .mdl-row .pe-link{flex:none} .mdl-row .pe-link:disabled{opacity:.4;cursor:not-allowed;text-decoration:none} .hf-mm{display:flex;align-items:center;gap:6px;margin-top:8px;font-size:11.5px; color:var(--dim);cursor:pointer} .hf-mm.muted{cursor:default} /* --- Panneau « Fichiers » (droite) ------------------------------------ Symétrique des réglages à gauche : une colonne fixe qui pousse la conversation au lieu de la recouvrir. C'est possible ici parce que la largeur de lecture est pilotée par --measure et que #chat se recentre tout seul dans la place qui reste — le fil se resserre, il ne passe pas dessous. Le panneau est retiré du flux quand il est fermé (et non simplement décalé) : sinon il conserverait sa gouttière et le fil resterait décentré. */ /* Le panneau ne défile PAS en entier : seule la liste le fait. Avec un overflow sur le panneau, l'en-tête, le fil d'Ariane et le pied (occupation disque, bouton .zip) s'en allaient vers le haut dès qu'il y avait quelques fichiers — on perdait les repères et les actions en même temps. */ #files{display:none;width:320px;flex-shrink:0;border-left:1px solid var(--border); background:var(--panel);padding:16px;padding-bottom:calc(16px + env(safe-area-inset-bottom)); overflow:hidden;flex-direction:column;gap:10px} html[data-files="1"] #files{display:flex} #files-backdrop{display:none;position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:14} .files-head{display:flex;align-items:center;gap:8px;flex:none} .files-head .stitle{flex:1} #files-refresh,#files-close{margin:0;padding:2px 8px;min-height:0;font-size:12px; background:transparent;border:1px solid transparent;color:var(--dim);border-radius:4px} #files-refresh:hover,#files-close:hover{border-color:var(--border);color:var(--text)} /* Fil d'Ariane : il doit pouvoir revenir à la ligne — un identifiant de discussion dans le chemin dépasse largement 320 px. */ .files-crumb{display:flex;flex-wrap:wrap;align-items:center;gap:2px;font-size:11px;flex:none} .files-crumb-b{margin:0;padding:2px 6px;min-height:0;font-size:11px;border:0;border-radius:3px; background:transparent;color:var(--dim);max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} .files-crumb-b:hover:not(:disabled){background:var(--row-bg-on);color:var(--text)} .files-crumb-b.on{color:var(--text);cursor:default} .files-crumb-s{color:var(--dim);opacity:.5} .files-list{display:flex;flex-direction:column;flex:1;min-height:0;overflow-y:auto;overflow-x:hidden; /* La barre de défilement ne doit pas décoller les lignes du bord : on lui rend la gouttière plutôt que d'ajouter une marge que le panneau sans défilement paierait aussi. */ scrollbar-gutter:stable} /* État vide : centré dans la hauteur disponible plutôt que collé en haut d'un grand vide — c'est le cas le plus fréquent à l'ouverture d'une discussion. */ .files-empty{font-size:12px;line-height:1.5;padding:24px 2px;text-align:center;margin:auto 0} /* Une ligne = pastille, nom + méta, actions. Les actions n'apparaissent qu'au survol : au repos on veut lire des noms de fichiers, pas une grille de boutons. Sur écran tactile (pas de survol) elles restent visibles. */ .files-row{display:flex;align-items:center;gap:8px;padding:6px 8px;border-radius:3px;font-size:12px} .files-row:hover{background:var(--row-bg-on)} .files-ic{flex:none;width:12px;text-align:center;color:var(--dim)} .files-row.isdir .files-ic{color:var(--accent)} .files-mid{flex:1;min-width:0;display:flex;flex-direction:column;gap:1px} .files-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text)} .files-meta{font-family:var(--mono);font-size:10px;color:var(--dim);white-space:nowrap} .files-acts{flex:none;display:flex;gap:2px;opacity:0;transition:opacity .12s} .files-row:hover .files-acts,.files-row:focus-within .files-acts{opacity:1} @media (hover:none){.files-acts{opacity:1}} .files-acts .iconbtn{font-size:11px;line-height:1;padding:4px 6px;background:transparent; border:1px solid transparent;border-radius:3px;color:var(--dim);cursor:pointer;margin:0;min-height:0} .files-acts .iconbtn:hover{border-color:var(--border);color:var(--text)} .files-foot{font-family:var(--mono);font-size:10px;padding-top:6px;border-top:1px solid var(--border); display:flex;align-items:center;gap:6px;flex-wrap:wrap;flex:none;margin-top:auto} /* Détour « hors discussion » : discret, en pied de panneau — c'est une sortie de secours pour les fichiers d'avant le rangement par discussion, pas une vue qu'on propose au même rang que la discussion ouverte. */ .files-scope{margin:0;padding:2px 6px;min-height:0;font-family:var(--mono);font-size:10px; background:transparent;border:1px solid var(--border);border-radius:3px;color:var(--dim)} .files-scope:hover{color:var(--text);background:var(--row-bg-on)} /* Téléchargement en cours : downloadWorkspaceFile pose .busy sur la ligne. */ .files-row.busy{opacity:.55;pointer-events:none} /* Bouton dossier du composeur : même gabarit que le trombone, et teinté quand le panneau est ouvert — l'état se lit sans regarder à droite. */ #files-btn{width:18px;height:18px;min-height:18px;flex:none;margin:0;padding:0; display:flex;align-items:center;justify-content:center;border-radius:999px; background:transparent;border:1px solid transparent;color:var(--dim)} #files-btn:hover{background:var(--row-bg-on);border-color:transparent;color:var(--text)} #files-btn:active{transform:scale(.9)} #files-btn svg{display:block} html[data-files="1"] #files-btn{color:var(--accent)} /* Petit écran : tiroir par la droite, comme les réglages par la gauche. Pousser une colonne de 320 px sur un téléphone ne laisserait rien à la conversation. */ @media (max-width:720px){ #files{position:fixed;inset:0 0 0 auto;width:85%;max-width:340px;z-index:15; background:var(--bg);padding-top:calc(16px + env(safe-area-inset-top)); padding-right:calc(16px + env(safe-area-inset-right))} html[data-files="1"] #files-backdrop{display:block} } @media (min-width:721px){ /* Dans la charte « Terre », le panneau est une carte comme les autres. */ #files{background:var(--panel);border-left-color:transparent;border-radius:3px; box-shadow:var(--shadow);margin:0} } /* ===================================================================== ===== Refonte « Sober Tech » ======================================== ===================================================================== Dernière section du fichier, donc dernier mot de la cascade : elle porte la coque de la refonte (barre d'en-tête, zones de la barre latérale, moniteur machine) et redresse les quelques règles historiques qu'elle contredit. Trois principes, tirés de la maquette : 1. Le fil est la seule chose qui compte : tout le reste est atténué, sans cadre superflu, et la barre latérale s'escamote d'un clic. 2. Les données machine sont TOUJOURS visibles mais jamais au premier plan — un pied de barre latérale, trois jauges, pas un tableau. 3. Aucun contour permanent sur les boutons : l'affordance vient du survol (fond qui apparaît, léger enfoncement au clic). ===================================================================== */ /* --- Coque ---------------------------------------------------------- */ .side{background:var(--side);display:flex;flex-direction:column;gap:0;padding:0;overflow:hidden} /* Les trois zones de la barre : en-tête fixe, contenu qui défile, moniteur posé en pied. Sans ça, la liste des discussions poussait les jauges hors de l'écran dès la dixième discussion. */ .sidehead{flex:none;display:flex;align-items:center;gap:8px;margin:0; padding:14px 14px 10px;padding-top:calc(14px + env(safe-area-inset-top))} #side-scroll{flex:1;min-height:0;overflow-y:auto;padding:0 0 12px; scrollbar-gutter:stable;display:flex;flex-direction:column;gap:0} #side-perf{flex:none;border-top:1px solid var(--border);padding:12px 14px; padding-bottom:calc(12px + env(safe-area-inset-bottom))} @media (max-width:720px){ .side{padding:0} #side-scroll{padding-left:calc(12px + env(safe-area-inset-left))} .sidehead{padding-left:calc(14px + env(safe-area-inset-left))} } /* Escamotage sur grand écran : la barre disparaît, le fil reprend la place. L'état est persisté (localStorage) et posé sur avant le rendu, comme le thème — sinon la barre apparaît puis se referme sous les yeux. */ @media (min-width:721px){ html[data-side="0"] .side{display:none} html[data-side="0"] #sidetoggle svg{transform:none} } #sidetoggle svg{transform:scaleX(-1)} /* --- Barre d'en-tête du fil ----------------------------------------- */ /* Elle porte ce qui décrit la conversation en cours : de quoi on parle, et avec QUEL modèle. Le choix du modèle vivait au fond de la barre latérale, dans une liste de presets — trois clics et un défilement pour une action qu'on fait plusieurs fois par session. */ #topbar{flex:none;display:flex;align-items:center;gap:10px;padding:10px 16px; padding-top:calc(10px + env(safe-area-inset-top)); border-bottom:1px solid var(--border);background:var(--bg);z-index:5} #topbar-title{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap; font-size:13.5px;font-weight:500;color:var(--text)} #topbar .iconbtn{width:30px;height:30px;flex:none;display:inline-flex;align-items:center; justify-content:center;border:0;border-radius:4px;background:transparent;color:var(--dim); cursor:pointer;margin:0;padding:0;transition:background .15s,color .15s,transform .1s} #topbar .iconbtn:hover{background:var(--row-bg-on);color:var(--text)} #topbar .iconbtn:active{transform:scale(.92)} #topbar .iconbtn svg{display:block} /* Sélecteur de modèle : le libellé n'est PAS une étiquette flottante mais le nom du preset actif, lisible d'un coup d'œil. */ #model-switch{max-width:230px;font:inherit;font-size:12.5px;color:var(--text); background:var(--panel);border:1px solid var(--border);border-radius:999px; padding:6px 30px 6px 12px;cursor:pointer;appearance:none; background-image:linear-gradient(45deg,transparent 50%,var(--dim) 50%),linear-gradient(135deg,var(--dim) 50%,transparent 50%); background-position:calc(100% - 15px) 52%,calc(100% - 11px) 52%; background-size:4px 4px,4px 4px;background-repeat:no-repeat} #model-switch:hover{border-color:var(--accent)} #model-switch:focus-visible{outline:2px solid var(--accent);outline-offset:1px} @media (max-width:720px){ #topbar{padding-left:56px} /* la place du bouton ☰, en position fixe */ #model-switch{max-width:130px} } /* --- Discussions ----------------------------------------------------- */ /* La liste EST la barre latérale : titre à gauche, heure à droite, une ligne. Le reste (réglages) descend sous un unique repli. */ /* Ligne du projet actif : posée au-dessus des discussions, qu'elle contient. Le sélecteur prend toute la largeur restante — un nom de projet doit être lisible en entier, c'est le repère qui dit dans quel chantier on écrit. */ .projrow{display:flex;align-items:center;gap:6px;padding:10px 12px 8px} .projrow>select{flex:1;min-width:0;font:inherit;font-size:12.5px;color:var(--text); background:var(--panel);border:1px solid var(--border);border-radius:4px;padding:6px 9px} .projrow>select:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-bg)} /* Le « + » des sections est masqué par défaut et révélé par details[open] : hors d'un
, il faut le rendre visible explicitement. */ .projrow>.preset-add{display:inline-block;margin:0} #conv-search{width:calc(100% - 24px);font:inherit;font-size:12.5px;color:var(--text); background:var(--panel);border:1px solid var(--border);border-radius:4px; padding:7px 11px;margin:0 12px 8px} #conv-search::placeholder{color:var(--dim)} #conv-search:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-bg)} .side .preset{padding:11px 14px;margin:0;gap:10px;border-radius:0; border:0;border-bottom:1px solid var(--border)} .side .preset .preset-name{font-size:13px;font-weight:450;white-space:nowrap;overflow:hidden; text-overflow:ellipsis;word-break:normal;overflow-wrap:normal} .side .preset.active .preset-name{font-weight:550} /* Heure de la dernière activité : à DROITE du titre, sur la même ligne, en monospace pour que les colonnes s'alignent d'une ligne à l'autre. ⚠️ Sélecteur en TROIS classes : `.preset>span` (plus haut) pose flex:1 sur tout span enfant d'une ligne — l'heure prenait donc la MOITIÉ de la ligne et le titre était tronqué au premier mot. */ .side .preset .conv-when{flex:none;font-family:var(--mono);font-size:10.5px; color:var(--dim);letter-spacing:-.01em;overflow:visible} .side .preset:hover .conv-when{opacity:0} .conv-empty{font-size:12px;color:var(--dim);padding:8px 11px} .conv-more{display:block;width:100%;font:inherit;font-size:12px;color:var(--dim);background:none; border:0;padding:8px 11px;text-align:left;cursor:pointer} .conv-more:hover{color:var(--text)} /* Discussion au travail : un anneau qui tourne, à gauche du titre. C'est le seul élément animé de la liste — il désigne sans ambiguïté le fil sur lequel un agent est en train de travailler, y compris depuis un autre appareil. */ /* ⚠️ Sélecteur en DEUX classes : `.preset>span` (plus haut) pose flex:1 sur tout span enfant d'une ligne — l'anneau prenait sinon la moitié de la largeur et écrasait le titre, exactement comme l'heure avant lui. */ .preset>.conv-spin,#topbar>.conv-spin{flex:none;display:inline-flex;align-items:center; justify-content:center;line-height:0;color:var(--accent)} /* L'animation porte sur le CONTENEUR (élément HTML), pas sur le : sur un élément SVG, l'origine de la rotation dépend de transform-box, dont le défaut a changé au fil des versions — l'anneau restait figé chez certains. Un span inline-flex tourne partout, sans ambiguïté. Les deux propriétés restent posées sur le svg pour le cas où une règle le viserait encore. */ .conv-spin{animation:conv-spin 900ms linear infinite;transform-origin:center} .conv-spin svg{transform-box:fill-box;transform-origin:center} @keyframes conv-spin{to{transform:rotate(360deg)}} /* Mouvement réduit : l'anneau ne tourne plus mais respire — l'information reste, le mouvement de rotation disparaît. */ @media (prefers-reduced-motion:reduce){ .conv-spin{animation:conv-breathe 1.6s ease-in-out infinite} @keyframes conv-breathe{0%,100%{opacity:1}50%{opacity:.25}} } /* Dans l'en-tête, l'anneau précède le titre : même gabarit que les boutons voisins pour ne pas décaler la ligne quand il apparaît et disparaît. */ #topbar>.conv-spin{width:18px} /* Repli des réglages : tout ce qui n'est pas la conversation tient sous cette seule ligne, pour que la barre latérale de la maquette (discussions + machine) reste la vue par défaut. */ #side-settings{margin-top:8px;border-top:1px solid var(--border)} #side-settings>summary{height:38px} #side-settings>.sidegroup{margin-top:2px} /* --- Moniteur machine (pied de barre latérale) ----------------------- */ .perf-head{display:flex;align-items:center;gap:8px;margin-bottom:11px; font-size:14px;font-weight:600;color:var(--text);letter-spacing:-.01em} /* Bouton du moniteur (« Libérer la VRAM » / « Recharger le modèle ») : poussé à droite du titre, discret tant qu'on ne le cherche pas. En mode rechargement il prend la couleur d'accent — le moteur est arrêté, c'est l'action qui compte à ce moment-là. */ .perf-act{margin-left:auto;flex:none;padding:3px 8px;font-size:11px;line-height:1.4; color:var(--dim);border:1px solid var(--border);border-radius:4px;cursor:pointer; white-space:nowrap} .perf-act:hover:not(:disabled){color:var(--text)} .perf-act:disabled{opacity:.55;cursor:progress} .perf-act.reload{color:var(--accent);border-color:color-mix(in srgb,var(--accent) 45%,transparent)} .perf-row{margin:0 0 14px} .perf-l{display:flex;align-items:baseline;gap:8px;font-size:12.5px;color:var(--text);margin-bottom:6px} .perf-l b{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap; font-weight:400;color:var(--text)} .perf-l .perf-pct{font-size:12.5px;color:var(--text);font-variant-numeric:tabular-nums} /* La jauge : 4 px, coins ronds, remplissage sauge. Elle passe à l'ambre puis au rouille quand la machine sature — c'est là que l'information devient utile. */ .perf-track{height:7px;border-radius:999px;background:var(--track);overflow:hidden} .perf-fill{height:100%;width:0;border-radius:999px;background:var(--accent-fill); transition:width .4s ease,background .4s ease} .perf-fill.hot{background:var(--warn)} .perf-fill.max{background:var(--err)} /* --- Boutons : un cadre, et une micro-interaction au survol ----------- La refonte les avait voulus « sans contour » : contour transparent, et un aplat pris sur --row-bg. Or c'est exactement le fond des blocs qui les contiennent (.slist, .rowbody) : un bouton posé dessus n'avait plus ni trait ni écart de valeur, et « démarrer / redémarrer / arrêter » ou « mettre à jour le moteur » se lisaient comme du texte, pas comme des commandes. On revient donc au cadre — celui d'avant la refonte : trait de 1 px et aplat un ton AU-DESSUS du bloc, pour que le bouton se détache dans les deux cas. La micro-interaction, elle, reste : elle est bonne, elle manquait juste de quelque chose à animer. ⚠️ Ne concerne pas les boutons-icônes de la saisie et de l'en-tête (#send, #stop, #attach, #menubtn…) : ils posent `border:0` PLUS BAS dans la feuille et gardent leur allure nue, qui est la bonne pour un pictogramme seul. */ button{background:var(--panel);border-color:var(--border);border-radius:4px; transition:background .15s ease,color .15s ease,border-color .15s ease,transform .08s ease} button:hover{background:var(--row-bg);border-color:var(--dim);color:var(--text)} button:active:not(:disabled){transform:translateY(1px)} .side .preset,.slist,#conv-search{transition:background .15s ease,border-color .15s ease} /* --- Fil : la réponse redevient une carte --------------------------- */ /* La maquette encadre la réponse de l'IA : sur un fond de fil coloré, du texte nu flotte sans ancrage, et la frontière entre deux réponses se perd. */ #chat{padding-top:22px} #chat .msg.assistant{background:var(--bubble);border:0; border-radius:4px;padding:15px 17px;max-width:100%;box-shadow:var(--shadow-card)} #chat .msg.assistant.errmsg{background:var(--danger-bg);border-color:color-mix(in srgb,var(--err) 45%,transparent)} /* Question : aplat de sauge, coin bas-droit rentré. --on-accent garantit le contraste dans les DEUX variantes. */ #chat .msg.user{background:var(--user-bg);color:var(--on-user);border-radius:4px; padding:13px 16px;max-width:62%;box-shadow:var(--shadow-card)} #chat .msg.user .label{color:var(--on-user);opacity:.75} #chat .msg.user .body a{color:var(--on-user);text-decoration:underline} #chat .msg.user code{background:rgba(255,255,255,.18);color:inherit} [data-theme="dark"] #chat .msg.user code{background:rgba(15,23,42,.22)} /* Les pastilles de fichier d'une question sont posées SUR l'aplat de sauge : sans fond propre, elles s'y fondaient. */ #chat .msg.user .msg-files .chip-file{background:var(--panel);color:var(--text);border-color:transparent} /* --- Saisie : la pilule de la maquette ------------------------------ */ /* Le bouton d'envoi devient une pastille ronde à flèche : c'est la forme de la maquette, et elle laisse toute la largeur au texte. Le mot « Envoyer » reste porté par aria-label et par l'infobulle — l'action n'est pas moins nommée pour un lecteur d'écran, elle l'est moins à l'écran. */ /* Les QUATRE boutons de la barre de saisie — joindre, fichiers, envoyer, arrêter — sont un seul et même objet : 34x34, coins doux (jamais un rond : la maquette n'en a pas), icône de 18 px centrée, fond seulement au survol. Seule la couleur les distingue : atténué pour les secondaires, sauge pour l'envoi, rouille pour l'arrêt. ⚠️ Une seule règle pour les quatre. Il y en avait trois générations empilées (pilule large « Envoyer », aplat noir, aplat sauge) : chaque état tirait sa taille, sa forme et son alignement d'une couche différente. */ #inputbar #attach,#inputbar #files-btn,#inputbar #mic,#send,#stop{ width:34px;height:34px;min-height:34px;flex:none;margin:0;padding:0; display:inline-flex;align-items:center;justify-content:center;align-self:center; border:0;border-radius:4px;background:transparent;color:var(--dim); transition:background .15s ease,color .15s ease,transform .08s ease} /* Envoyer / arrêter : un seul des deux à la fois, décidé par data-busy (posé par syncSendBtn). C'est la feuille qui garde la main sur `display` — un style inline y substituait `inline-block`, et le centrage flex tombait avec lui. */ #stop{display:none} html[data-busy="1"] #send{display:none} html[data-busy="1"] #stop{display:inline-flex} #inputbar #attach svg,#inputbar #files-btn svg,#inputbar #mic svg,#send svg,#stop svg{display:block;width:18px;height:18px} #inputbar #attach:hover,#inputbar #files-btn:hover,#inputbar #mic:hover{background:var(--row-bg-on);color:var(--text)} html[data-files="1"] #files-btn{color:var(--accent)} #send{color:var(--accent)} #send:hover:not(:disabled){background:var(--accent-bg);color:var(--accent);opacity:1;transform:none} #send:disabled{background:transparent;color:var(--dim);opacity:.45} #stop{color:var(--err)} #stop:hover{background:var(--danger-bg);color:var(--err);opacity:1} #inputbar #attach:active,#inputbar #files-btn:active,#send:active:not(:disabled),#stop:active{transform:scale(.92)} /* Les outils de la saisie (joindre, fichiers, envoyer) montent DANS la pilule, à droite du texte : c'est là qu'on les cherche, et le pied de carte redevient ce qu'il doit être — une jauge de contexte, rien d'autre. */ #inputbar{align-items:flex-end;padding:8px 8px 8px 18px;gap:4px;box-shadow:var(--shadow-card); border-radius:3px 3px 0 0} #ctxmeta{border-radius:0 0 5px 5px} #ctxmeta{padding-bottom:12px} #ctxtrack{border-radius:999px} /* --- Coque à plat --------------------------------------------------- La direction précédente posait deux CARTES flottantes séparées par une gouttière ; la maquette est bord à bord : barre latérale collée au bord, simple filet de séparation, fil qui occupe tout le reste. On neutralise donc ici les règles de coque de la section « Terre » plutôt que de les réécrire plus haut — la cascade fait le tri, et l'historique reste lisible. */ @media (min-width:721px){ body{padding:0;gap:0} html[data-pwa="1"] body{padding-top:env(safe-area-inset-top)} .side{width:300px;border-right:1px solid var(--border);border-radius:0; box-shadow:none;background:var(--side);overflow:hidden} /* Dans la barre, plus de cartes : des lignes posées sur le fond. Deux niveaux de cadre imbriqués (carte de section + carte de liste) pour une liste de titres, c'était deux de trop. */ #side-scroll>details,#side-scroll>.sidegroup{background:transparent;box-shadow:none; border-radius:0;padding:0;margin:0} #side-scroll>details>summary{height:34px} .side .slist{background:transparent;border-color:transparent;border-radius:3px} .side #side-settings .slist{background:var(--row-bg);border-radius:3px} /* Le fil n'est plus une carte : il EST la page. */ .main{background:transparent;border-radius:0;box-shadow:none;overflow:visible} #composer{background:linear-gradient(to bottom,transparent 0,var(--bg) var(--fade))} #inputbar,#ctxmeta{background:var(--panel);border-color:var(--border)} #input{background:transparent} } /* Ligne de discussion : les actions (renommer, supprimer) sont posées EN SURIMPRESSION à droite plutôt qu'en flux. En flux, elles réservaient leur largeur en permanence — invisibles mais présentes — et le titre était tronqué au tiers de la place disponible. */ .side .preset{position:relative} .side .preset .conv-acts{position:absolute;right:6px;top:50%;transform:translateY(-50%); margin:0;background:var(--row-bg-on);border-radius:4px;padding:1px} .side .preset.active .conv-acts{background:var(--row-bg)} .side .preset .preset-info{overflow:hidden} /* La sélection est un aplat, pas un cadre : un liseré autour d'une ligne de liste alourdit là où un fond suffit à dire « c'est celle-ci ». */ .side .preset.active{background:var(--row-bg-on);border-color:transparent} .side .preset.active .preset-name{color:var(--text)} /* --- Téléphone : un seul bouton de barre latérale -------------------- */ /* Le ☰ flottant ouvre déjà le tiroir ; garder EN PLUS l'icône d'escamotage de l'en-tête donnait deux boutons voisins pour la même action, et grignotait la place du titre sur un écran qui n'en a pas. */ @media (max-width:720px){ /* Sélecteur en DEUX id : `#topbar .iconbtn` (plus haut) pose déjà un display, et il l'emporterait sur un simple `#sidetoggle`. */ #topbar #sidetoggle{display:none} #topbar{padding-left:52px;padding-right:10px;gap:6px} #topbar-title{font-size:13px} } /* Le ☰ suit la charte des boutons : pas de contour, un fond qui apparaît au survol. Il flotte au-dessus de l'en-tête, il doit donc en reprendre le fond pour ne pas laisser voir le fil défiler dessous. */ #menubtn{background:var(--bg);border:0;color:var(--dim);border-radius:4px; width:34px;height:34px;font-size:16px;top:calc(11px + env(safe-area-inset-top));left:9px} #menubtn:hover{background:var(--row-bg-on);color:var(--text)} #lc-chip{border-radius:4px;height:34px;top:calc(11px + env(safe-area-inset-top));left:calc(50px + env(safe-area-inset-left))} /* Jauges : la piste doit se VOIR, sinon une barre presque vide ressemble à un trait souligné sous l'intitulé. Valeurs relevées sur la maquette. */ .perf-track{background:var(--track)} /* Une liste déroulante posée directement dans une ligne de modale n'a pas de garde-fou de largeur (contrairement à .pe-row-c) : une valeur très longue — un chemin de destination, par exemple — débordait sous l'intitulé. */ .pe-row>.pe-selc{max-width:62%} /* Marque et attribution du fork empilées, pastille d'état à leur droite. */ .sidebrand{flex:1;min-width:0;display:flex;flex-direction:column;gap:1px} .sidebrand .brand-fork{margin:0;font-size:10px;line-height:1.3} /* Blocs de code du fil : encadré franc dans la carte de réponse — fond plus soutenu, filet net et coins alignés sur ceux de la carte. Le bouton « copier » reprend le fond du bloc pour ne pas flotter sur le texte. */ #chat .msg.assistant .body pre,#chat .msg.reasoning .body pre,#chat .msg.tool pre{ background:var(--code-bg);border:1px solid color-mix(in srgb,var(--text) 10%,transparent)} #chat .msg.assistant .body code{background:var(--code-bg); border:1px solid color-mix(in srgb,var(--text) 7%,transparent);border-radius:3px} #chat .msg.assistant .body pre code{border:0;background:transparent} /* --- Titres de la barre latérale ------------------------------------ La maquette n'a qu'un style de titre : casse normale, demi-gras, taille du texte courant (« Performance »). Le monospace en capitales de la charte précédente y jurait, et faisait crier des libellés qui n'ont rien à dire. */ /* Le panneau Fichiers porte les mêmes titres que la barre latérale : un seul style de titre dans toute l'application, celui de la maquette. */ .stitle{font-family:var(--font);text-transform:none;letter-spacing:-.01em; font-size:13.5px;font-weight:600;color:var(--text)} .side-sep{font-family:var(--font);text-transform:none;letter-spacing:0; font-size:12px;font-weight:600;color:var(--dim)} #side-scroll>details>summary,#side-settings>summary{padding:0 14px} #side-settings{margin-top:0;border-top:1px solid var(--border)} #side-settings>.sidegroup{padding:0 12px} /* La liste elle-même n'a pas de gouttière : ses lignes touchent les deux bords, comme dans la maquette. Le filet du bas de la dernière ligne fait la séparation avec ce qui suit. */ #conv-list{margin:0} .side .preset:hover{background:var(--row-bg)} .side .preset .conv-acts{right:8px} /* Message d'absence de matériel : même famille que les jauges, en plus discret. (La sous-ligne chiffrée, elle, a quitté l'affichage — le détail est en infobulle, comme dans la maquette.) */ .perf-sub{font-size:11.5px;color:var(--dim)} /* --- Carte de saisie : le trait sauge de la maquette ----------------- C'est le seul contour coloré de l'écran, et il désigne l'endroit où l'on écrit. ⚠️ Posé EN FIN DE FEUILLE : la règle de coque plus haut (`#inputbar,#ctxmeta{border-color:var(--border)}`) reprenait sinon la main et le cadre retombait au gris commun. La flèche d'envoi prend la MÊME sauge que la maquette — c'est un aplat plein de 18 px, pas du texte. */ @media (min-width:721px){ #inputbar,#ctxmeta{border-color:var(--accent-fill)} #composer:focus-within #inputbar,#composer:focus-within #ctxmeta{border-color:var(--accent)} } #inputbar,#ctxmeta{border-color:var(--accent-fill)} #send{color:var(--accent-fill)} #send:hover:not(:disabled){color:var(--accent)} /* --- Échelle typographique des planches ------------------------------ « Typography: Inter » — Bold 24px pour les titres, Regular 16px pour le corps, Medium 14px pour le texte secondaire. On l'applique là où elle a un sens (le fil, la barre latérale) plutôt qu'en gonflant toute l'interface : les panneaux de réglages sont des formulaires denses, pas du texte de lecture. */ #chat .msg .body{font-size:16px} #chat .msg.user .body,#chat .msg.assistant .body{line-height:1.6} #input{font-size:16px} .side .preset .preset-name{font-size:14px} .side .preset .conv-when{font-size:13px} .perf-head{font-size:16px;font-weight:700} .perf-l,.perf-l .perf-pct{font-size:14px;font-weight:500} .stitle{font-size:14px;font-weight:600} /* Marque : « Loki v0.9.4 » en tête de colonne, à l'échelle des titres. */ #brand svg{height:20px} #ver{font-size:13px;font-weight:500;margin-left:8px} /* --- Boutons-icônes : une seule géométrie -------------------------- Toutes ces commandes portaient un GLYPHE DE TEXTE, donc une taille et une assiette décidées par la police du système. En SVG, elles se centrent normalement — encore faut-il que leur bouton soit une boîte flex et non une ligne de texte. */ /* ⚠️ On NE touche PAS à `display` ici : plusieurs de ces boutons s'affichent et se masquent par ce biais (#menubtn et .preset-add sont en display:none au repos, #scrollbtn aussi). Une règle de centrage qui pose display:inline-flex les rendrait visibles en permanence — le ☰ s'est ainsi retrouvé par-dessus l'en-tête sur grand écran. Le centrage passe donc par le rembourrage, que la taille connue de chaque bouton rend exact. */ .modal-x,.mcp-edit,#files-refresh,#files-close, .files-acts .iconbtn,.conv-acts .iconbtn{ display:inline-flex;align-items:center;justify-content:center;line-height:1} #menubtn{padding:8px;line-height:0} #scrollbtn{padding:9px;line-height:0} .preset-add{padding:5px;line-height:0} #menubtn svg,#scrollbtn svg,.preset-add svg{display:block;margin:0 auto} .modal-x svg,.mcp-edit svg,#files-refresh svg,#files-close svg,.iconbtn svg{ display:block;width:auto;height:auto} /* La pastille du fil d'Ariane des fichiers n'est plus un caractère : elle a la largeur de son dessin. */ .files-ic{width:16px;height:16px;display:inline-flex;align-items:center;justify-content:center} .files-ic svg{display:block} .files-row.isdir .files-ic{color:var(--accent)} /* « Vider la discussion » : icône + libellé, alignés comme un seul bloc. */ #chat-actions-clear,.row>button>svg{vertical-align:-2px} .row>button:has(svg){display:inline-flex;align-items:center;gap:7px} /* Le « + » des sections : même carré que les autres commandes de la barre. */ .preset-add{width:26px;height:26px;border-radius:4px} /* Étiquettes de groupe : même traitement que les titres de section — la maquette n'a qu'un style de titre, et le monospace en capitales de la charte précédente criait au milieu des formulaires. */ .subhead,.io-cap,.pages-bar .pages-title,.skillshead>span{ font-family:var(--font);text-transform:none;letter-spacing:0; font-size:12.5px;font-weight:600;color:var(--dim)} /* --- Discussions et moniteur : le gabarit de carte des réglages ------ La maquette posait ces deux blocs à plat, pleine largeur. Dans l'application réelle ils voisinent avec les sections de réglages, qui sont des CARTES : deux traitements pour deux listes côte à côte, et la barre latérale se lisait comme deux morceaux collés. On aligne donc sur la carte, qui est la forme dominante de la colonne. */ #side-scroll{padding:0 12px 12px} #side-scroll>details>summary,#side-settings>summary{padding:0 2px} /* ⚠️ La gouttière INTÉRIEURE de la carte n'est pas décorative. En la mettant à zéro (le retrait extérieur venant désormais de #side-scroll), les lignes allaient d'un bord à l'autre : le chevron de chaque section tombait dans le congé de 20 px, donc il en débordait visuellement sur la première et la dernière ligne, et les filets de séparation venaient buter dans la courbe. Le rayon paraissait « trop fort » alors que c'est le contenu qui n'avait plus de quoi s'en écarter. On reprend le retrait d'avant la refonte (16 px), qui laisse au congé la place de tourner à vide. */ #side-settings>.sidegroup{padding:2px 16px} @media (max-width:720px){ #side-scroll{padding-left:calc(12px + env(safe-area-inset-left))} } #conv-search{width:100%;margin:0 0 8px} /* La liste EST la carte, au MÊME gabarit que .sidegroup (les sections de réglages) : fond de panneau, rayon de 20, ombre discrète. Les lignes sont rognées par overflow au lieu d'arrondir la première et la dernière à la main. */ #conv-list{background:var(--panel);border-radius:3px;box-shadow:var(--shadow); overflow:hidden;margin:0} .side .preset{border-radius:0;margin:0;padding:10px 12px} .side .preset:not(:last-child){border-bottom:1px solid var(--border)} .side .preset:last-child{border-bottom:0} /* Sélection : bande pleine largeur DANS la carte — un aplat suffit, un cadre ferait un second contour à l'intérieur du premier. */ .side .preset.active{background:var(--row-bg-on)} .side .preset:hover:not(.active){background:color-mix(in srgb,var(--text) 4%,transparent)} .side .preset .conv-acts{background:var(--row-bg-on)} .side .preset.active .conv-acts{background:var(--row-bg-on)} .conv-empty{padding:12px} /* Moniteur machine : titre en étiquette de groupe, jauges dans une carte — même couple que « Paramètres » + sa liste dans les réglages. */ #side-perf{padding:12px 12px 12px;padding-bottom:calc(12px + env(safe-area-inset-bottom))} .perf-head{margin:0 2px 7px;font-size:12.5px;font-weight:600;color:var(--dim)} .perf-body{background:var(--panel);border-radius:3px;box-shadow:var(--shadow); padding:14px 15px 3px} /* ⚠️ Pas de remise à zéro sur :last-child — les jauges vivent dans DEUX conteneurs (les cartes graphiques, puis la mémoire vive). La dernière jauge de chaque conteneur s'y voyait retirer sa marge, et la mémoire vive venait se coller à la VRAM. C'est le rembourrage bas de la carte qui absorbe la dernière marge. */ .perf-row{margin:0 0 12px} .perf-sub{padding:0 0 10px} /* --- Mode Chat/Code (20-mode.js) ------------------------------------------ Sélecteur segmenté dans le pied de la carte du composeur, puce de suggestion, panneau des critères, badge de rôle, carte de question. */ #mode-seg{display:inline-flex;border:1px solid var(--border);border-radius:3px;overflow:hidden;flex:none} #mode-seg button{border:0;background:transparent;color:var(--dim);font-size:10px;letter-spacing:.04em;text-transform:uppercase;padding:2px 8px;cursor:pointer;font-family:inherit} #mode-seg button.on{background:var(--accent-bg);color:var(--accent)} #mode-seg button:not(.on):hover{color:var(--text)} /* Bandeaux du composeur (chrono du tour, puce mode code, critères). Ils vivent DANS #composer — qui est en position:absolute — et suivent donc sa colonne (--col). Posés avant lui dans le DOM, ils restaient dans le flux de la page et disparaissaient SOUS la carte de saisie. */ .composer-banner{margin:0 var(--col) 6px;padding:6px 12px;border:1px solid var(--border);border-radius:4px;background:var(--panel);color:var(--dim);font-size:12px;font-family:var(--mono)} #turn-clock{display:flex;align-items:center;gap:6px;letter-spacing:.03em} #turn-clock::before{content:'';width:6px;height:6px;border-radius:50%;background:var(--accent);animation:tcpulse 1.4s ease-in-out infinite} #turn-clock.done{color:var(--accent)} #turn-clock.done::before{animation:none;opacity:.6} @keyframes tcpulse{0%,100%{opacity:.25}50%{opacity:1}} #code-hint{display:flex;align-items:center;gap:8px;font-size:12px} #code-hint button{border:1px solid var(--accent);background:transparent;color:var(--accent);border-radius:3px;padding:2px 8px;font-size:12px;cursor:pointer;font-family:inherit} #code-hint button:hover{background:var(--accent-bg)} #code-hint #code-hint-no{border-color:var(--border);color:var(--dim);margin-left:auto} #criteria-box{padding:0;overflow:hidden;max-height:38vh;overflow-y:auto} #criteria-box.allpass{border-color:color-mix(in srgb,var(--accent) 45%,var(--border))} #criteria-head{padding:6px 12px;color:var(--dim);cursor:pointer;user-select:none;letter-spacing:.03em;position:sticky;top:0;background:var(--panel)} #criteria-head::after{content:' ▾';opacity:.5} #criteria-box.folded #criteria-head::after{content:' ▸'} #criteria-box.allpass #criteria-head{color:var(--accent)} #criteria-box.folded #criteria-list{display:none} #criteria-list{list-style:none;margin:0;padding:0 12px 8px} #criteria-list li{padding:2px 0;color:var(--text)} #criteria-list li.crit-passed{color:var(--accent)} #criteria-list li.crit-failed{color:var(--err,#b3564d)} #criteria-list li.crit-pending{color:var(--dim)} .rolebadge{display:inline-block;margin-left:6px;padding:0 6px;border:1px solid var(--accent);border-radius:3px;color:var(--accent);font-size:9px;letter-spacing:.05em;text-transform:uppercase;vertical-align:middle} /* Dictée : anneau rouge pulsé pendant l'enregistrement, estompé pendant la transcription. Le gabarit (34 px, fond au survol) vient de la règle commune des boutons de saisie — #mic y est listé avec #attach et #files-btn. */ #mic.rec{color:var(--err);animation:micpulse 1.2s ease-in-out infinite} @keyframes micpulse{50%{box-shadow:0 0 0 5px color-mix(in srgb,var(--err) 25%,transparent)}} #mic.busy{opacity:.55} /* ── Réglages en modale ───────────────────────────────────────────────────── Deux volets : nav des sections à gauche, LE panneau choisi à droite. Les panneaux réutilisent tels quels les gabarits de la barre latérale (.slist, .switchrow, .subhead…), seuls la coque et le routage sont neufs. */ #side-settings-btn{display:flex;align-items:center;gap:8px;width:100%;margin:0;padding:10px 14px;flex:none; background:none;border:0;border-top:1px solid var(--border);border-radius:0;box-shadow:none; font-size:13px;color:var(--text);cursor:pointer;text-align:left} #side-settings-btn:hover{background:var(--bubble)} #side-settings-btn svg{flex:none;opacity:.7} .set-card{max-width:860px;height:min(660px,92vh)} .set-shell{display:flex;flex:1;min-height:0} .set-nav{flex:0 0 200px;border-right:1px solid var(--border);overflow-y:auto; padding:10px 8px;display:flex;flex-direction:column;gap:2px} .set-nav-h{font-size:10px;text-transform:uppercase;letter-spacing:.12em;color:var(--dim);margin:12px 8px 4px} .set-nav-h:first-child{margin-top:2px} .set-nav button{display:flex;align-items:center;gap:8px;width:100%;margin:0;padding:7px 10px; background:none;border:0;border-radius:3px;box-shadow:none; font-size:12.5px;color:var(--text);cursor:pointer;text-align:left} .set-nav button:hover{background:var(--bubble)} .set-nav button.active{background:var(--bubble);font-weight:600} .set-nav button .sbadge{margin-left:auto} .set-body{flex:1;min-width:0;overflow-y:auto;padding:16px} .set-pane{display:none} .set-pane.active{display:block} .set-pane-h{display:flex;align-items:center;gap:8px;font-size:14px;font-weight:600;margin:0 0 12px} .set-pane-h>span:first-child{flex:1;min-width:0} /* Téléphone : la nav passe en rangée défilante au-dessus du panneau, et la modale prend tout l'écran — un panneau de réglages ne se lit pas en timbre. */ @media(max-width:720px){ .set-ov{padding:0} .set-card{max-width:none;height:100vh;height:100dvh;max-height:none;border-radius:0;border:0} .set-shell{flex-direction:column} .set-nav{flex:none;flex-direction:row;align-items:center;overflow-x:auto;overflow-y:hidden; border-right:0;border-bottom:1px solid var(--border);padding:8px;white-space:nowrap} .set-nav-h{display:none} .set-nav button{width:auto;flex:none} } /* Nom du modèle qui a produit la réponse, à côté de « Loki ». Discret (gris, pas de capitales forcées) et borné : un nom de .gguf peut être très long, il s'ellipse au lieu de pousser l'étiquette hors de la bulle. */ .msg .label .modelbadge{display:inline-block;margin-left:6px;padding:0 5px;border:1px solid var(--border);border-radius:3px;color:var(--dim);font-size:9px;letter-spacing:.03em;text-transform:none;max-width:240px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;vertical-align:middle} .msg[data-agent-role="verifier"]{border-left:2px solid var(--accent)} .askcard .body{display:flex;flex-direction:column;gap:8px} .askopts{display:flex;flex-wrap:wrap;gap:6px} .askopts button{border:1px solid var(--accent);background:transparent;color:var(--accent);border-radius:3px;padding:4px 10px;font-size:13px;cursor:pointer;font-family:inherit} .askopts button:hover:not(:disabled){background:var(--accent-bg)} .askopts button:disabled{opacity:.5;cursor:default} /* Capture d'écran rendue dans la bulle de l'outil (web_screenshot) : l'image ne dépend plus du modèle qui pense — ou non — à la recopier dans sa réponse. */ .tool-shot{margin-top:8px} .tool-shot img{max-width:100%;max-height:420px;border:1px solid var(--border);border-radius:4px;display:block;cursor:zoom-in} .mcp-tag.mcp-tag-preset{flex:0 1 auto;min-width:2.5em} .task-consigne{min-height:260px;resize:vertical;line-height:1.6;padding:12px} /* Tâches planifiées (repris de l'amont AJEAN) : ligne en cours d'exécution + modale d'édition. */ .mcp-row.task-running{border-color:var(--accent);background:var(--accent-bg);cursor:default} .task-dot-run{animation:taskpulse 1.1s ease-in-out infinite} @keyframes taskpulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.35;transform:scale(.7)}} .task-run-badge{font-size:10px;font-weight:600;color:var(--accent);letter-spacing:.02em} .task-stop-btn{flex:none;margin:0;padding:3px 10px;font-size:11px;border-radius:6px;background:var(--danger-bg);border:1px solid var(--err);color:var(--err)} .task-stop-btn:hover{background:var(--danger-bg);border-color:var(--err);color:var(--err);opacity:.85} /* La modale des tâches emprunte tout le design de l'éditeur de preset (.pe-*) : même largeur, même mise en carte. */ #task-modal .modal-card{max-width:520px;overflow:hidden} #task-modal-status:empty{display:none} /* Panneau « Dernier résultat » : le compte-rendu complet de l'IA, lisible et défilable — pas une mini-ligne tronquée. Posé dans une carte .pe-list-like. */ .task-report{font-size:13px;line-height:1.55;color:var(--text);background:var(--panel);border:1px solid var(--border);border-radius:12px;padding:11px 14px;max-height:240px;overflow:auto;white-space:pre-wrap;word-break:break-word} .task-report.report-err{border-color:var(--err);color:var(--err)} .task-report.report-run{color:var(--dim);font-style:italic} .task-report.report-empty{color:var(--dim)} /* Compte-rendu rendu en markdown : c'est du HTML de blocs, donc plus de pre-wrap (sinon les sauts de ligne du HTML généré ajoutent des lignes vides). Styles alignés sur ceux des bulles de chat (.msg .body). */ .task-report.md-report{white-space:normal} .md-report>*{margin:.6em 0} .md-report>*:first-child{margin-top:0} .md-report>*:last-child{margin-bottom:0} .md-report h1,.md-report h2,.md-report h3,.md-report h4{font-weight:600;line-height:1.25;margin:.9em 0 .4em} .md-report h1{font-size:1.2em}.md-report h2{font-size:1.1em}.md-report h3{font-size:1em}.md-report h4{font-size:.95em} .md-report ul,.md-report ol{padding-left:1.4em;margin:.5em 0} .md-report li{margin:.15em 0} .md-report li>p{margin:0} .md-report code{background:var(--code-bg);padding:1px 5px;border-radius:3px;font-size:.9em;word-break:break-word} .md-report pre{background:var(--code-bg);border:1px solid var(--border);border-radius:6px;padding:10px;overflow-x:auto;max-width:100%;margin:.5em 0} .md-report pre code{background:transparent;padding:0;font-size:.85em;line-height:1.4} .md-report blockquote{border-left:3px solid var(--border);padding:0 0 0 10px;margin:.4em 0;color:var(--dim)} .md-report a{color:var(--accent);text-decoration:none} .md-report a:hover{text-decoration:underline} .md-report table{border-collapse:collapse;margin:.5em 0} .md-report th,.md-report td{border:1px solid var(--border);padding:4px 8px} .md-report hr{border:none;border-top:1px solid var(--border);margin:.6em 0} /* Historique paginé : bouton « échanges précédents » en tête du fil. */ #chat .history-more{display:flex;justify-content:center;padding:6px 0 14px} #chat .history-more button{font:inherit;font-size:12px;color:var(--dim);background:var(--row-bg); border:1px solid var(--border);border-radius:999px;padding:6px 14px;cursor:pointer; transition:background .15s,color .15s,border-color .15s} #chat .history-more button:hover{color:var(--text);border-color:var(--dim);background:var(--row-bg-on)} #chat .history-more button:disabled{opacity:.6;cursor:default}