Refonte : modèle chargé, attribution, ombres, flèche, favicon

Retours sur la refonte, cinq corrections.

Modèle affiché
  Le sélecteur de l'en-tête ne montrait que le PRESET actif — or un preset n'est
  « actif » que si son empreinte correspond exactement à la configuration. Une
  configuration écrite à la main, héritée de l'image ou modifiée d'un cran depuis
  le dernier preset n'en désigne aucun, et l'en-tête restait muet alors qu'un
  modèle tournait. Il retombe désormais sur le nom du fichier de la
  configuration active (loadCfg le lui donne), et l'infobulle porte le chemin
  complet.

Attribution
  « fork de AJEAN » revient sous le nom de l'app, dans l'en-tête de la barre
  latérale, au lieu du pied de la zone défilante.

Ombres
  Les bulles du fil et la carte de saisie portent une vraie ombre douce
  (--shadow-card), comme la maquette — le filet de 1 px ne les décollait pas du
  fond.

Bouton d'envoi
  Chevron sauge sur fond transparent, à la place de la pastille verte pleine :
  c'est le dessin de la maquette, et il suit alors la même charte que ses
  voisins (dépôt, fichiers).

Favicon
  Carré sauge (#5E7F5A) au lieu du carré noir, aux quatre endroits qui le
  dessinent : favicon SVG, icône d'accueil iOS, icône du .exe (icon.ico) et PNG
  macOS — tous rendus depuis sys_brand_icon.go, seule source du dessin. La
  variante « template » de macOS reste noire : le système ne lit que son alpha.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LPyFxVHNAN9u5pVzSYMwjd
This commit is contained in:
Claude committed 2026-08-16 18:15:24 +00:00
1 parent f99e5b59b8
commit 276a8287cb
8 files changed
+152 -63

No files matched your search

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

+8 -3
View File
@@ -2,12 +2,15 @@ package loki
// sys_brand_icon.go — l'icône de la marque Loki, rendue à la volée. // sys_brand_icon.go — l'icône de la marque Loki, rendue à la volée.
// //
// RÉPLIQUE EXACTE du favicon de l'UI web : carré à coins arrondis NOIR + « j » // RÉPLIQUE EXACTE du favicon de l'UI web : carré à coins arrondis SAUGE + « j »
// blanc (rects (6,3) (6,5) (4,7) sur une grille 12x12). Une seule source pour // blanc (rects (6,3) (6,5) (4,7) sur une grille 12x12). Une seule source pour
// tous les usages — zone de notification Windows, barre de menus macOS, icône du // tous les usages — zone de notification Windows, barre de menus macOS, icône du
// .exe — pour qu'ils ne puissent plus diverger comme quand le favicon est passé // .exe — pour qu'ils ne puissent plus diverger comme quand le favicon est passé
// au noir en laissant les icônes système en bleu. // au noir en laissant les icônes système en bleu.
// //
// La sauge est --accent de l'interface (#5E7F5A) : l'icône de l'onglet et celle
// du bureau portent la couleur de l'app, pas un carré noir anonyme.
//
// Aucun asset binaire à committer : tout est dessiné ici, et l'icône du .exe est // Aucun asset binaire à committer : tout est dessiné ici, et l'icône du .exe est
// produite par `go generate ./cmd/loki` (voir tools/gen-icon). // produite par `go generate ./cmd/loki` (voir tools/gen-icon).
@@ -22,6 +25,8 @@ import (
const trayIconSize = 32 const trayIconSize = 32
var ( var (
// brandSage = --accent du thème clair. Le blanc du « j » s'y lit à 4,9:1.
brandSage = color.RGBA{0x5e, 0x7f, 0x5a, 0xff}
brandBlack = color.RGBA{0x00, 0x00, 0x00, 0xff} brandBlack = color.RGBA{0x00, 0x00, 0x00, 0xff}
brandWhite = color.RGBA{0xff, 0xff, 0xff, 0xff} brandWhite = color.RGBA{0xff, 0xff, 0xff, 0xff}
brandClear = color.RGBA{0, 0, 0, 0} brandClear = color.RGBA{0, 0, 0, 0}
@@ -80,9 +85,9 @@ func encodePNG(img *image.RGBA) []byte {
return buf.Bytes() return buf.Bytes()
} }
// BrandIconPNG rend l'icône de marque (noir + « j » blanc) en PNG de n pixels. // BrandIconPNG rend l'icône de marque (sauge + « j » blanc) en PNG de n pixels.
// Exporté pour le générateur d'icône du .exe (tools/gen-icon). // Exporté pour le générateur d'icône du .exe (tools/gen-icon).
func BrandIconPNG(n int) []byte { return encodePNG(brandIconImage(n, brandBlack, brandWhite)) } func BrandIconPNG(n int) []byte { return encodePNG(brandIconImage(n, brandSage, brandWhite)) }
// brandTemplatePNG rend la variante « template » attendue par macOS : seule la // brandTemplatePNG rend la variante « template » attendue par macOS : seule la
// couche alpha compte, le système colore la forme selon le thème de la barre de // couche alpha compte, le système colore la forme selon le thème de la barre de
+72 -30
View File
@@ -12,8 +12,8 @@
<!-- Favicon (même marque « loki » que la landing/portail) + icône PWA (écran d'accueil <!-- Favicon (même marque « loki » que la landing/portail) + icône PWA (écran d'accueil
iOS). SVG inline pour le favicon, PNG 180×180 en data-URI pour apple-touch-icon iOS). SVG inline pour le favicon, PNG 180×180 en data-URI pour apple-touch-icon
(iOS n'accepte pas le SVG pour l'icône d'accueil). Aucun fichier externe requis. --> (iOS n'accepte pas le SVG pour l'icône d'accueil). Aucun fichier externe requis. -->
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'><rect width='12' height='12' rx='2' ry='2' fill='%23000'/><rect x='6' y='3' width='2' height='2' fill='%23fff'/><rect x='6' y='5' width='2' height='2' fill='%23fff'/><rect x='4' y='7' width='2' height='2' fill='%23fff'/></svg>"> <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'><rect width='12' height='12' rx='2' ry='2' fill='%235E7F5A'/><rect x='6' y='3' width='2' height='2' fill='%23fff'/><rect x='6' y='5' width='2' height='2' fill='%23fff'/><rect x='4' y='7' width='2' height='2' fill='%23fff'/></svg>">
<link rel="apple-touch-icon" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALQAAAC0CAIAAACyr5FlAAABBUlEQVR42u3SAQkAAAwCQfuX3lI4GNxF0E8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIArU2NbcYhDHOIQhzgQB+JAHIgDcSAOxIE4xCEOcYhDHIgDcSAOxIE4EAfiQBziEIc4xCEOcYgDcSAOxIE4EAfiQBziEIc4xCEOcYgDcSAOxIE4EAfiQByIQxziEIc4xIE4EAfiQByIA3EgDsQhDnGIQxziEIc4EAfiQByIAxciDsSBOBAH4kAc4hCHOMQhDnGIA3EgDsSBOBAH4kAc4hCHOMQhDnGIA3EgDsSBOBAH4kAciEMc4hCHOMQBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADfLBHMhi4TnG6EAAAAAElFTkSuQmCC"> <link rel="apple-touch-icon" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALQAAAC0CAYAAAA9zQYyAAADD0lEQVR4nOzdW27aWgBGYag6ylZqh1I6lFZKppmjEylRLlyMsbG9+n3vsP2w8mfDC193d/Dj17ene5zDuv39/bif+4xZDhAwQ8wR+GRvKGJuMVXcN7+JkJnSrWGPfrGQmdPYsL+MeZGYmdvYxq76KxAyS7hmrQcvtJhZyjXtDQpazCxtaIMXgxYzazGkxbNBi5m1udTkyaDFzFqda3PU13awVkeDts6s3alGPwUtZrbiWKuuHKS8C9o6szUfm7XQpLwGbZ3ZqrftWmhSBE3Kc9CuG2zdS8MWmhRBkyJoUvbuz5RYaFIETYqgSRE0KYImRdCkCJoUQZMiaFIETYqgSRE0KYImRdCkCJoUQZMiaFIETYqgSbnLj9ez2/05PCxy7s/D90XOXYqFJkXQpAiaFEGTImhSBE2KoEkRNCmCJkXQpAiaFEGTImhSBE2KoEkRNCmCJkXQpAiaFEGTImhSBE2KoEkRNCmCJkXQpAiaFEGTImhSBE2KoEkRNCmCJkXQpAiaFEGTImhSBE2KoEkRNCmCJkXQpAiaFEGTImhSBE2KoEkRNCmCJkXQpAiaFEGTImhSBE2KoEkRNCmCJkXQpAialK9LP8C9/Tk8LHLuz8P3Rc7911hoUgRNiqBJETQpgiZF0KQImhRBkyJoUgRNiqBJETQpgiZF0KQImhRBkyJoUgRNiqBJETQpgiZF0KQImhRBkyJoUgRNiqBJETQpgiZl/+PXt6elHwKmYqFJETQpgiZF0KQImhRBkyJoUgRNiqBJETQpgiZF0KQImhRBkyJoUgRNiqBJETQpgibly9/fj/ulHwKm8H/LFpoUQZMiaFKeg3aPZuteGrbQpAialNegXTvYqrftWmhS3gVtpdmaj81aaFI+BW2l2YpjrR5daFGzdqcadeUg5WTQVpq1Otfm2YUWNWtzqcmLVw5RsxZDWhx0hxY1Sxva4OAPhaJmKde0NypSPwXHPYwZ0VFf21lr5ja2sZvDtNZM6daxnGxphc0tpvqvP8vVQdwMMcfV9S53YYGzu9Nnr/8CAAD//xZRk2OJYiOqAAAAAElFTkSuQmCC">
<!-- Marque le mode PWA AVANT le rendu : la hauteur de coque et le défilement du <!-- Marque le mode PWA AVANT le rendu : la hauteur de coque et le défilement du
document en dépendent (voir styles.css). ⚠️ On NE PEUT PAS se fier au média document en dépendent (voir styles.css). ⚠️ On NE PEUT PAS se fier au média
`display-mode:standalone` : WebKit l'adosse au `display` du MANIFESTE, or cette `display-mode:standalone` : WebKit l'adosse au `display` du MANIFESTE, or cette
@@ -66,7 +66,8 @@ document.documentElement.setAttribute('data-perf',localStorage.getItem('loki-per
--ok:#5e7f5a;--warn:#a86a2a;--err:#c0503f;--mag:#a86a2a; --ok:#5e7f5a;--warn:#a86a2a;--err:#c0503f;--mag:#a86a2a;
--code-bg:#f4f6f5;--danger-bg:#faeae7;--warn-bg:#f7f0e5; --code-bg:#f4f6f5;--danger-bg:#faeae7;--warn-bg:#f7f0e5;
--add:#5e7f5a;--add-bg:rgba(94,127,90,.11);--del:#c0503f;--del-bg:rgba(192,80,63,.12); --add:#5e7f5a;--add-bg:rgba(94,127,90,.11);--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: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,.07),0 1px 3px rgba(15,23,42,.05)}
/* « Deep Dark » : fond #0F172A, cartes #1E293B. */ /* « Deep Dark » : fond #0F172A, cartes #1E293B. */
[data-theme="dark"]{color-scheme:dark; [data-theme="dark"]{color-scheme:dark;
--bg:#0f172a;--panel:#1e293b;--bubble:#1e293b;--border:rgba(148,163,184,.17); --bg:#0f172a;--panel:#1e293b;--bubble:#1e293b;--border:rgba(148,163,184,.17);
@@ -77,7 +78,8 @@ document.documentElement.setAttribute('data-perf',localStorage.getItem('loki-per
--ok:#8fb08c;--warn:#d9a55f;--err:#e0705c;--mag:#d9a55f; --ok:#8fb08c;--warn:#d9a55f;--err:#e0705c;--mag:#d9a55f;
--code-bg:#131c2f;--danger-bg:#3a1f1c;--warn-bg:#33291b; --code-bg:#131c2f;--danger-bg:#3a1f1c;--warn-bg:#33291b;
--add:#8fb08c;--add-bg:rgba(143,176,140,.16);--del:#e0705c;--del-bg:rgba(224,112,92,.16); --add:#8fb08c;--add-bg:rgba(143,176,140,.16);--del:#e0705c;--del-bg:rgba(224,112,92,.16);
--shadow:none;--shadow-lift:0 4px 16px rgba(0,0,0,.35)} --shadow:none;--shadow-lift:0 4px 16px rgba(0,0,0,.35);
--shadow-card:0 2px 12px rgba(0,0,0,.30),0 1px 3px rgba(0,0,0,.22)}
/* Safari iOS peint un rectangle gris/noir sur TOUT élément touché (bouton, lien, /* 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 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 de notre charte et impossible à styler : on le coupe. Nos états :active/:focus
@@ -1601,12 +1603,12 @@ button:active:not(:disabled){transform:translateY(1px)}
nu flotte sans ancrage, et la frontière entre deux réponses se perd. */ nu flotte sans ancrage, et la frontière entre deux réponses se perd. */
#chat{padding-top:22px} #chat{padding-top:22px}
#chat .msg.assistant{background:var(--bubble);border:1px solid var(--border); #chat .msg.assistant{background:var(--bubble);border:1px solid var(--border);
border-radius:16px;padding:14px 18px;max-width:100%;box-shadow:var(--shadow)} border-radius:16px;padding:14px 18px;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)} #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 /* Question : aplat de sauge, coin bas-droit rentré. --on-accent garantit le
contraste dans les DEUX variantes. */ contraste dans les DEUX variantes. */
#chat .msg.user{background:var(--accent);color:var(--on-accent);border-radius:18px 18px 6px 18px; #chat .msg.user{background:var(--accent);color:var(--on-accent);border-radius:18px 18px 6px 18px;
padding:11px 16px;max-width:75%} padding:11px 16px;max-width:75%;box-shadow:var(--shadow-card)}
#chat .msg.user .label{color:var(--on-accent);opacity:.75} #chat .msg.user .label{color:var(--on-accent);opacity:.75}
#chat .msg.user .body a{color:var(--on-accent);text-decoration:underline} #chat .msg.user .body a{color:var(--on-accent);text-decoration:underline}
#chat .msg.user code{background:rgba(255,255,255,.18);color:inherit} #chat .msg.user code{background:rgba(255,255,255,.18);color:inherit}
@@ -1623,13 +1625,16 @@ button:active:not(:disabled){transform:translateY(1px)}
#send,#stop{width:34px;height:34px;min-height:34px;padding:0;border-radius:999px; #send,#stop{width:34px;height:34px;min-height:34px;padding:0;border-radius:999px;
display:inline-flex;align-items:center;justify-content:center;flex:none} display:inline-flex;align-items:center;justify-content:center;flex:none}
#send svg,#stop svg{display:block} #send svg,#stop svg{display:block}
#send:hover:not(:disabled){opacity:1;transform:none;background:color-mix(in srgb,var(--accent) 85%,var(--text))} #send{background:transparent;border-color:transparent;color:var(--accent)}
#send:hover:not(:disabled){opacity:1;transform:none;background:var(--accent-bg);color:var(--accent)}
#send:active:not(:disabled){transform:scale(.92)} #send:active:not(:disabled){transform:scale(.92)}
#stop:hover{opacity:1;background:color-mix(in srgb,var(--err) 85%,var(--text))} #send:disabled{background:transparent;color:var(--dim)}
#stop{background:transparent;border-color:transparent;color:var(--err)}
#stop:hover{opacity:1;background:var(--danger-bg);color:var(--err)}
/* Les outils de la saisie (joindre, fichiers, envoyer) montent DANS la pilule, /* 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 à 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. */ 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)} #inputbar{align-items:flex-end;padding:8px 8px 8px 18px;gap:4px;box-shadow:var(--shadow-card)}
#composer:focus-within #inputbar,#composer:focus-within #ctxmeta{border-color:var(--accent-soft)} #composer:focus-within #inputbar,#composer:focus-within #ctxmeta{border-color:var(--accent-soft)}
#inputbar #attach,#inputbar #files-btn{width:34px;height:34px;min-height:34px;border-radius:999px; #inputbar #attach,#inputbar #files-btn{width:34px;height:34px;min-height:34px;border-radius:999px;
align-self:center} align-self:center}
@@ -1701,6 +1706,9 @@ button:active:not(:disabled){transform:translateY(1px)}
garde-fou de largeur (contrairement à .pe-row-c) : une valeur très longue — garde-fou de largeur (contrairement à .pe-row-c) : une valeur très longue —
un chemin de destination, par exemple — débordait sous l'intitulé. */ un chemin de destination, par exemple — débordait sous l'intitulé. */
.pe-row>.pe-selc{max-width:62%} .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}
</style></head><body> </style></head><body>
<button id="menubtn" onclick="toggleSide()">☰</button> <button id="menubtn" onclick="toggleSide()">☰</button>
<!-- Installation d'un moteur en cours : le détail vit dans le panneau, qui est <!-- Installation d'un moteur en cours : le détail vit dans le panneau, qui est
@@ -1713,7 +1721,12 @@ button:active:not(:disabled){transform:translateY(1px)}
C'est ce découpage qui garde les jauges visibles quel que soit le nombre C'est ce découpage qui garde les jauges visibles quel que soit le nombre
de discussions — elles les poussaient auparavant hors de l'écran. --> de discussions — elles les poussaient auparavant hors de l'écran. -->
<div class="sidehead"> <div class="sidehead">
<div class="sidebrand">
<h1 id="brand" aria-label="loki" title="Loki — fork d'AJEAN (github.com/nathaninline/ajean, MIT)"><span id="brand-a"><svg viewBox="0 0 4 5" shape-rendering="crispEdges" fill="currentColor"><rect x="0" y="0" width="1" height="5"/><rect x="1" y="4" width="3" height="1"/></svg></span><span id="brand-word"><svg viewBox="0 0 11 5" shape-rendering="crispEdges" fill="currentColor"><rect x="0" y="0" width="3" height="1"/><rect x="0" y="1" width="1" height="3"/><rect x="2" y="1" width="1" height="3"/><rect x="0" y="4" width="3" height="1"/><rect x="4" y="0" width="1" height="5"/><rect x="6" y="0" width="1" height="2"/><rect x="5" y="2" width="1" height="1"/><rect x="6" y="3" width="1" height="2"/><rect x="8" y="0" width="3" height="1"/><rect x="9" y="1" width="1" height="3"/><rect x="8" y="4" width="3" height="1"/></svg></span><span id="ver"></span></h1> <h1 id="brand" aria-label="loki" title="Loki — fork d'AJEAN (github.com/nathaninline/ajean, MIT)"><span id="brand-a"><svg viewBox="0 0 4 5" shape-rendering="crispEdges" fill="currentColor"><rect x="0" y="0" width="1" height="5"/><rect x="1" y="4" width="3" height="1"/></svg></span><span id="brand-word"><svg viewBox="0 0 11 5" shape-rendering="crispEdges" fill="currentColor"><rect x="0" y="0" width="3" height="1"/><rect x="0" y="1" width="1" height="3"/><rect x="2" y="1" width="1" height="3"/><rect x="0" y="4" width="3" height="1"/><rect x="4" y="0" width="1" height="5"/><rect x="6" y="0" width="1" height="2"/><rect x="5" y="2" width="1" height="1"/><rect x="6" y="3" width="1" height="2"/><rect x="8" y="0" width="3" height="1"/><rect x="9" y="1" width="1" height="3"/><rect x="8" y="4" width="3" height="1"/></svg></span><span id="ver"></span></h1>
<!-- Attribution du fork (licence MIT de l'amont) : sous le nom de l'app,
là où elle a toujours vécu. -->
<div class="brand-fork">fork de <a href="https://github.com/nathaninline/ajean" target="_blank" rel="noopener noreferrer" title="AJEAN par nathaninline — licence MIT">AJEAN</a></div>
</div><!-- /.sidebrand -->
<span id="status-svc" class="statuspill" onclick="toggleSvcLog()" title="voir le journal du moteur">…</span> <span id="status-svc" class="statuspill" onclick="toggleSvcLog()" title="voir le journal du moteur">…</span>
</div> </div>
<div id="side-scroll"> <div id="side-scroll">
@@ -2009,10 +2022,6 @@ button:active:not(:disabled){transform:translateY(1px)}
</details> </details>
</div><!-- /sidegroup --> </div><!-- /sidegroup -->
</details><!-- /#side-settings --> </details><!-- /#side-settings -->
<!-- Attribution du fork (licence MIT de l'amont) : en pied de la zone
défilante, sous les réglages. Elle a quitté l'en-tête, que la refonte
réduit à la marque et à l'état du moteur, mais elle reste affichée. -->
<div class="brand-fork">fork de <a href="https://github.com/nathaninline/ajean" target="_blank" rel="noopener noreferrer" title="AJEAN par nathaninline — licence MIT">AJEAN</a></div>
</div><!-- /#side-scroll --> </div><!-- /#side-scroll -->
<!-- Moniteur machine : charge GPU, VRAM et mémoire vive. Posé en pied, en <!-- Moniteur machine : charge GPU, VRAM et mémoire vive. Posé en pied, en
jauges fines — visible en permanence sans jamais disputer la place au jauges fines — visible en permanence sans jamais disputer la place au
@@ -2089,7 +2098,7 @@ button:active:not(:disabled){transform:translateY(1px)}
reste porté par aria-label et l'infobulle — l'action garde son nom reste porté par aria-label et l'infobulle — l'action garde son nom
pour un lecteur d'écran, elle ne l'affiche simplement plus. --> pour un lecteur d'écran, elle ne l'affiche simplement plus. -->
<button id="send" onclick="send()" title="Envoyer" aria-label="Envoyer"> <button id="send" onclick="send()" title="Envoyer" aria-label="Envoyer">
<svg viewBox="0 0 24 24" width="17" height="17" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 12h15"/><path d="m13 6 6 6-6 6"/></svg> <svg viewBox="0 0 24 24" width="19" height="19" fill="currentColor" stroke="none" aria-hidden="true"><path d="M3.2 20.6 21.5 12 3.2 3.4l.02 6.69L16 12 3.22 13.9z"/></svg>
</button> </button>
<button id="stop" onclick="stopGen()" style="display:none" title="Arrêter la génération" aria-label="Arrêter la génération"> <button id="stop" onclick="stopGen()" style="display:none" title="Arrêter la génération" aria-label="Arrêter la génération">
<svg viewBox="0 0 24 24" width="15" height="15" fill="currentColor" stroke="none" aria-hidden="true"><rect x="6" y="6" width="12" height="12" rx="2"/></svg> <svg viewBox="0 0 24 24" width="15" height="15" fill="currentColor" stroke="none" aria-hidden="true"><rect x="6" y="6" width="12" height="12" rx="2"/></svg>
@@ -3030,6 +3039,9 @@ async function loadCfg(){
else v='llama.cpp personnalisé : '+c.BIN; else v='llama.cpp personnalisé : '+c.BIN;
rows.push(row('MOTEUR', v, c.BIN)); rows.push(row('MOTEUR', v, c.BIN));
} }
// Le modèle chargé alimente aussi le sélecteur de l'en-tête : c'est la seule
// source qui le connaisse quand aucun preset ne correspond à la configuration.
if(typeof setLoadedModel === 'function') setLoadedModel(c.MODEL || '');
['MODEL','CTX','BATCH','UBATCH','NGL'].filter(k=>c[k]).forEach(k=>{ ['MODEL','CTX','BATCH','UBATCH','NGL'].filter(k=>c[k]).forEach(k=>{
let v=c[k]; if(k==='MODEL') v=v.split('/').pop(); let v=c[k]; if(k==='MODEL') v=v.split('/').pop();
rows.push(row(k, v)); rows.push(row(k, v));
@@ -6774,33 +6786,63 @@ function onModelSwitch(sel){
switchTo(n, name); switchTo(n, name);
} }
// Remplit le sélecteur à partir de la liste de presets déjà chargée par // Le sélecteur affiche LE MODÈLE CHARGÉ, pas seulement le preset actif.
// loadPresets() — aucun appel réseau supplémentaire. //
// Les deux ne coïncident pas toujours : un preset n'est « actif » que si son
// empreinte correspond exactement à la configuration courante. Une configuration
// écrite à la main, héritée d'une image, ou modifiée d'un cran depuis le dernier
// preset ne correspond à AUCUN d'entre eux — et l'en-tête n'affichait alors rien
// alors qu'un modèle était bel et bien chargé. On retombe donc sur le nom du
// fichier de la configuration active.
//
// Deux sources, deux moments : loadPresets() donne la liste, loadCfg() donne le
// modèle. Chacune dépose ce qu'elle sait et redemande le dessin — elles partent
// en parallèle (loadAll), aucun ordre n'est garanti.
let MS_PRESETS = [], MS_ACTIVE = null, MS_MODEL = '';
function renderModelSwitch(presets, active){ function renderModelSwitch(presets, active){
MS_PRESETS = presets || []; MS_ACTIVE = active || null;
paintModelSwitch();
}
// Appelé par loadCfg avec la valeur MODEL de la configuration active.
function setLoadedModel(name){
MS_MODEL = (name || '').trim();
paintModelSwitch();
}
// Nom lisible d'un fichier de modèle : sans dossier ni extension. Un .gguf
// s'appelle « Qwen3-14B-Instruct-Q4_K_M.gguf » — le chemin et le suffixe
// n'apprennent rien et mangent toute la place.
function modelLabel(path){
const base = path.split(/[\\/]/).pop();
return base.replace(/\.gguf$/i, '');
}
function paintModelSwitch(){
const sel = document.getElementById('model-switch'); const sel = document.getElementById('model-switch');
if(!sel) return; if(!sel) return;
const prev = sel.value;
sel.textContent = ''; sel.textContent = '';
if(!presets || !presets.length){ // Ligne de tête : ce qui est RÉELLEMENT chargé, quand aucun preset ne le
// revendique. Value vide → onModelSwitch l'ignore, elle ne bascule rien.
if(!MS_ACTIVE){
const o = document.createElement('option'); const o = document.createElement('option');
o.textContent = 'aucun preset'; o.value = ''; o.value = '';
sel.appendChild(o); sel.disabled = true; o.textContent = MS_MODEL ? modelLabel(MS_MODEL) : (MS_PRESETS.length ? 'aucun modèle actif' : 'aucun preset');
return; o.selected = true;
sel.appendChild(o);
} }
sel.disabled = false; MS_PRESETS.forEach((p, i) => {
presets.forEach((p, i) => {
const o = document.createElement('option'); const o = document.createElement('option');
o.value = String(i + 1); o.value = String(i + 1);
o.textContent = p.name; o.textContent = p.name;
if(active && p.id === active.id) o.selected = true; if(MS_ACTIVE && p.id === MS_ACTIVE.id) o.selected = true;
sel.appendChild(o); sel.appendChild(o);
}); });
// Aucun preset actif (configuration modifiée à la main) : on le dit, plutôt sel.disabled = !MS_PRESETS.length;
// que de laisser le premier de la liste passer pour le modèle chargé. sel.title = MS_MODEL
if(!active){ ? 'modèle chargé : ' + MS_MODEL + ' — changer de preset relance le moteur'
const o = document.createElement('option'); : 'aucun modèle configuré';
o.value = ''; o.textContent = 'configuration hors preset'; o.selected = true; // Une bascule en cours ne doit pas voir sa sélection sauter sous la souris.
sel.insertBefore(o, sel.firstChild); if(prev && [...sel.options].some(o => o.value === prev)) sel.value = prev;
}
} }
// État initial des libellés (le CSS, lui, est déjà appliqué par le <head>). // État initial des libellés (le CSS, lui, est déjà appliqué par le <head>).
+8 -7
View File
@@ -12,8 +12,8 @@
<!-- Favicon (même marque « loki » que la landing/portail) + icône PWA (écran d'accueil <!-- Favicon (même marque « loki » que la landing/portail) + icône PWA (écran d'accueil
iOS). SVG inline pour le favicon, PNG 180×180 en data-URI pour apple-touch-icon iOS). SVG inline pour le favicon, PNG 180×180 en data-URI pour apple-touch-icon
(iOS n'accepte pas le SVG pour l'icône d'accueil). Aucun fichier externe requis. --> (iOS n'accepte pas le SVG pour l'icône d'accueil). Aucun fichier externe requis. -->
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'><rect width='12' height='12' rx='2' ry='2' fill='%23000'/><rect x='6' y='3' width='2' height='2' fill='%23fff'/><rect x='6' y='5' width='2' height='2' fill='%23fff'/><rect x='4' y='7' width='2' height='2' fill='%23fff'/></svg>"> <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'><rect width='12' height='12' rx='2' ry='2' fill='%235E7F5A'/><rect x='6' y='3' width='2' height='2' fill='%23fff'/><rect x='6' y='5' width='2' height='2' fill='%23fff'/><rect x='4' y='7' width='2' height='2' fill='%23fff'/></svg>">
<link rel="apple-touch-icon" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALQAAAC0CAIAAACyr5FlAAABBUlEQVR42u3SAQkAAAwCQfuX3lI4GNxF0E8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIArU2NbcYhDHOIQhzgQB+JAHIgDcSAOxIE4xCEOcYhDHIgDcSAOxIE4EAfiQBziEIc4xCEOcYgDcSAOxIE4EAfiQBziEIc4xCEOcYgDcSAOxIE4EAfiQByIQxziEIc4xIE4EAfiQByIA3EgDsQhDnGIQxziEIc4EAfiQByIAxciDsSBOBAH4kAc4hCHOMQhDnGIA3EgDsSBOBAH4kAc4hCHOMQhDnGIA3EgDsSBOBAH4kAciEMc4hCHOMQBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADfLBHMhi4TnG6EAAAAAElFTkSuQmCC"> <link rel="apple-touch-icon" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALQAAAC0CAYAAAA9zQYyAAADD0lEQVR4nOzdW27aWgBGYag6ylZqh1I6lFZKppmjEylRLlyMsbG9+n3vsP2w8mfDC193d/Dj17ene5zDuv39/bif+4xZDhAwQ8wR+GRvKGJuMVXcN7+JkJnSrWGPfrGQmdPYsL+MeZGYmdvYxq76KxAyS7hmrQcvtJhZyjXtDQpazCxtaIMXgxYzazGkxbNBi5m1udTkyaDFzFqda3PU13awVkeDts6s3alGPwUtZrbiWKuuHKS8C9o6szUfm7XQpLwGbZ3ZqrftWmhSBE3Kc9CuG2zdS8MWmhRBkyJoUvbuz5RYaFIETYqgSRE0KYImRdCkCJoUQZMiaFIETYqgSRE0KYImRdCkCJoUQZMiaFIETYqgSbnLj9ez2/05PCxy7s/D90XOXYqFJkXQpAiaFEGTImhSBE2KoEkRNCmCJkXQpAiaFEGTImhSBE2KoEkRNCmCJkXQpAiaFEGTImhSBE2KoEkRNCmCJkXQpAiaFEGTImhSBE2KoEkRNCmCJkXQpAiaFEGTImhSBE2KoEkRNCmCJkXQpAiaFEGTImhSBE2KoEkRNCmCJkXQpAiaFEGTImhSBE2KoEkRNCmCJkXQpAialK9LP8C9/Tk8LHLuz8P3Rc7911hoUgRNiqBJETQpgiZF0KQImhRBkyJoUgRNiqBJETQpgiZF0KQImhRBkyJoUgRNiqBJETQpgiZF0KQImhRBkyJoUgRNiqBJETQpgiZl/+PXt6elHwKmYqFJETQpgiZF0KQImhRBkyJoUgRNiqBJETQpgiZF0KQImhRBkyJoUgRNiqBJETQpgibly9/fj/ulHwKm8H/LFpoUQZMiaFKeg3aPZuteGrbQpAialNegXTvYqrftWmhS3gVtpdmaj81aaFI+BW2l2YpjrR5daFGzdqcadeUg5WTQVpq1Otfm2YUWNWtzqcmLVw5RsxZDWhx0hxY1Sxva4OAPhaJmKde0NypSPwXHPYwZ0VFf21lr5ja2sZvDtNZM6daxnGxphc0tpvqvP8vVQdwMMcfV9S53YYGzu9Nnr/8CAAD//xZRk2OJYiOqAAAAAElFTkSuQmCC">
<!-- Marque le mode PWA AVANT le rendu : la hauteur de coque et le défilement du <!-- Marque le mode PWA AVANT le rendu : la hauteur de coque et le défilement du
document en dépendent (voir styles.css). ⚠️ On NE PEUT PAS se fier au média document en dépendent (voir styles.css). ⚠️ On NE PEUT PAS se fier au média
`display-mode:standalone` : WebKit l'adosse au `display` du MANIFESTE, or cette `display-mode:standalone` : WebKit l'adosse au `display` du MANIFESTE, or cette
@@ -42,7 +42,12 @@ document.documentElement.setAttribute('data-perf',localStorage.getItem('loki-per
C'est ce découpage qui garde les jauges visibles quel que soit le nombre C'est ce découpage qui garde les jauges visibles quel que soit le nombre
de discussions — elles les poussaient auparavant hors de l'écran. --> de discussions — elles les poussaient auparavant hors de l'écran. -->
<div class="sidehead"> <div class="sidehead">
<div class="sidebrand">
<h1 id="brand" aria-label="loki" title="Loki — fork d'AJEAN (github.com/nathaninline/ajean, MIT)"><span id="brand-a"><svg viewBox="0 0 4 5" shape-rendering="crispEdges" fill="currentColor"><rect x="0" y="0" width="1" height="5"/><rect x="1" y="4" width="3" height="1"/></svg></span><span id="brand-word"><svg viewBox="0 0 11 5" shape-rendering="crispEdges" fill="currentColor"><rect x="0" y="0" width="3" height="1"/><rect x="0" y="1" width="1" height="3"/><rect x="2" y="1" width="1" height="3"/><rect x="0" y="4" width="3" height="1"/><rect x="4" y="0" width="1" height="5"/><rect x="6" y="0" width="1" height="2"/><rect x="5" y="2" width="1" height="1"/><rect x="6" y="3" width="1" height="2"/><rect x="8" y="0" width="3" height="1"/><rect x="9" y="1" width="1" height="3"/><rect x="8" y="4" width="3" height="1"/></svg></span><span id="ver"></span></h1> <h1 id="brand" aria-label="loki" title="Loki — fork d'AJEAN (github.com/nathaninline/ajean, MIT)"><span id="brand-a"><svg viewBox="0 0 4 5" shape-rendering="crispEdges" fill="currentColor"><rect x="0" y="0" width="1" height="5"/><rect x="1" y="4" width="3" height="1"/></svg></span><span id="brand-word"><svg viewBox="0 0 11 5" shape-rendering="crispEdges" fill="currentColor"><rect x="0" y="0" width="3" height="1"/><rect x="0" y="1" width="1" height="3"/><rect x="2" y="1" width="1" height="3"/><rect x="0" y="4" width="3" height="1"/><rect x="4" y="0" width="1" height="5"/><rect x="6" y="0" width="1" height="2"/><rect x="5" y="2" width="1" height="1"/><rect x="6" y="3" width="1" height="2"/><rect x="8" y="0" width="3" height="1"/><rect x="9" y="1" width="1" height="3"/><rect x="8" y="4" width="3" height="1"/></svg></span><span id="ver"></span></h1>
<!-- Attribution du fork (licence MIT de l'amont) : sous le nom de l'app,
là où elle a toujours vécu. -->
<div class="brand-fork">fork de <a href="https://github.com/nathaninline/ajean" target="_blank" rel="noopener noreferrer" title="AJEAN par nathaninline — licence MIT">AJEAN</a></div>
</div><!-- /.sidebrand -->
<span id="status-svc" class="statuspill" onclick="toggleSvcLog()" title="voir le journal du moteur">…</span> <span id="status-svc" class="statuspill" onclick="toggleSvcLog()" title="voir le journal du moteur">…</span>
</div> </div>
<div id="side-scroll"> <div id="side-scroll">
@@ -338,10 +343,6 @@ document.documentElement.setAttribute('data-perf',localStorage.getItem('loki-per
</details> </details>
</div><!-- /sidegroup --> </div><!-- /sidegroup -->
</details><!-- /#side-settings --> </details><!-- /#side-settings -->
<!-- Attribution du fork (licence MIT de l'amont) : en pied de la zone
défilante, sous les réglages. Elle a quitté l'en-tête, que la refonte
réduit à la marque et à l'état du moteur, mais elle reste affichée. -->
<div class="brand-fork">fork de <a href="https://github.com/nathaninline/ajean" target="_blank" rel="noopener noreferrer" title="AJEAN par nathaninline — licence MIT">AJEAN</a></div>
</div><!-- /#side-scroll --> </div><!-- /#side-scroll -->
<!-- Moniteur machine : charge GPU, VRAM et mémoire vive. Posé en pied, en <!-- Moniteur machine : charge GPU, VRAM et mémoire vive. Posé en pied, en
jauges fines — visible en permanence sans jamais disputer la place au jauges fines — visible en permanence sans jamais disputer la place au
@@ -418,7 +419,7 @@ document.documentElement.setAttribute('data-perf',localStorage.getItem('loki-per
reste porté par aria-label et l'infobulle — l'action garde son nom reste porté par aria-label et l'infobulle — l'action garde son nom
pour un lecteur d'écran, elle ne l'affiche simplement plus. --> pour un lecteur d'écran, elle ne l'affiche simplement plus. -->
<button id="send" onclick="send()" title="Envoyer" aria-label="Envoyer"> <button id="send" onclick="send()" title="Envoyer" aria-label="Envoyer">
<svg viewBox="0 0 24 24" width="17" height="17" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 12h15"/><path d="m13 6 6 6-6 6"/></svg> <svg viewBox="0 0 24 24" width="19" height="19" fill="currentColor" stroke="none" aria-hidden="true"><path d="M3.2 20.6 21.5 12 3.2 3.4l.02 6.69L16 12 3.22 13.9z"/></svg>
</button> </button>
<button id="stop" onclick="stopGen()" style="display:none" title="Arrêter la génération" aria-label="Arrêter la génération"> <button id="stop" onclick="stopGen()" style="display:none" title="Arrêter la génération" aria-label="Arrêter la génération">
<svg viewBox="0 0 24 24" width="15" height="15" fill="currentColor" stroke="none" aria-hidden="true"><rect x="6" y="6" width="12" height="12" rx="2"/></svg> <svg viewBox="0 0 24 24" width="15" height="15" fill="currentColor" stroke="none" aria-hidden="true"><rect x="6" y="6" width="12" height="12" rx="2"/></svg>
+3
View File
@@ -205,6 +205,9 @@ async function loadCfg(){
else v='llama.cpp personnalisé : '+c.BIN; else v='llama.cpp personnalisé : '+c.BIN;
rows.push(row('MOTEUR', v, c.BIN)); rows.push(row('MOTEUR', v, c.BIN));
} }
// Le modèle chargé alimente aussi le sélecteur de l'en-tête : c'est la seule
// source qui le connaisse quand aucun preset ne correspond à la configuration.
if(typeof setLoadedModel === 'function') setLoadedModel(c.MODEL || '');
['MODEL','CTX','BATCH','UBATCH','NGL'].filter(k=>c[k]).forEach(k=>{ ['MODEL','CTX','BATCH','UBATCH','NGL'].filter(k=>c[k]).forEach(k=>{
let v=c[k]; if(k==='MODEL') v=v.split('/').pop(); let v=c[k]; if(k==='MODEL') v=v.split('/').pop();
rows.push(row(k, v)); rows.push(row(k, v));
+46 -16
View File
@@ -67,33 +67,63 @@ function onModelSwitch(sel){
switchTo(n, name); switchTo(n, name);
} }
// Remplit le sélecteur à partir de la liste de presets déjà chargée par // Le sélecteur affiche LE MODÈLE CHARGÉ, pas seulement le preset actif.
// loadPresets() — aucun appel réseau supplémentaire. //
// Les deux ne coïncident pas toujours : un preset n'est « actif » que si son
// empreinte correspond exactement à la configuration courante. Une configuration
// écrite à la main, héritée d'une image, ou modifiée d'un cran depuis le dernier
// preset ne correspond à AUCUN d'entre eux — et l'en-tête n'affichait alors rien
// alors qu'un modèle était bel et bien chargé. On retombe donc sur le nom du
// fichier de la configuration active.
//
// Deux sources, deux moments : loadPresets() donne la liste, loadCfg() donne le
// modèle. Chacune dépose ce qu'elle sait et redemande le dessin — elles partent
// en parallèle (loadAll), aucun ordre n'est garanti.
let MS_PRESETS = [], MS_ACTIVE = null, MS_MODEL = '';
function renderModelSwitch(presets, active){ function renderModelSwitch(presets, active){
MS_PRESETS = presets || []; MS_ACTIVE = active || null;
paintModelSwitch();
}
// Appelé par loadCfg avec la valeur MODEL de la configuration active.
function setLoadedModel(name){
MS_MODEL = (name || '').trim();
paintModelSwitch();
}
// Nom lisible d'un fichier de modèle : sans dossier ni extension. Un .gguf
// s'appelle « Qwen3-14B-Instruct-Q4_K_M.gguf » — le chemin et le suffixe
// n'apprennent rien et mangent toute la place.
function modelLabel(path){
const base = path.split(/[\\/]/).pop();
return base.replace(/\.gguf$/i, '');
}
function paintModelSwitch(){
const sel = document.getElementById('model-switch'); const sel = document.getElementById('model-switch');
if(!sel) return; if(!sel) return;
const prev = sel.value;
sel.textContent = ''; sel.textContent = '';
if(!presets || !presets.length){ // Ligne de tête : ce qui est RÉELLEMENT chargé, quand aucun preset ne le
// revendique. Value vide → onModelSwitch l'ignore, elle ne bascule rien.
if(!MS_ACTIVE){
const o = document.createElement('option'); const o = document.createElement('option');
o.textContent = 'aucun preset'; o.value = ''; o.value = '';
sel.appendChild(o); sel.disabled = true; o.textContent = MS_MODEL ? modelLabel(MS_MODEL) : (MS_PRESETS.length ? 'aucun modèle actif' : 'aucun preset');
return; o.selected = true;
sel.appendChild(o);
} }
sel.disabled = false; MS_PRESETS.forEach((p, i) => {
presets.forEach((p, i) => {
const o = document.createElement('option'); const o = document.createElement('option');
o.value = String(i + 1); o.value = String(i + 1);
o.textContent = p.name; o.textContent = p.name;
if(active && p.id === active.id) o.selected = true; if(MS_ACTIVE && p.id === MS_ACTIVE.id) o.selected = true;
sel.appendChild(o); sel.appendChild(o);
}); });
// Aucun preset actif (configuration modifiée à la main) : on le dit, plutôt sel.disabled = !MS_PRESETS.length;
// que de laisser le premier de la liste passer pour le modèle chargé. sel.title = MS_MODEL
if(!active){ ? 'modèle chargé : ' + MS_MODEL + ' — changer de preset relance le moteur'
const o = document.createElement('option'); : 'aucun modèle configuré';
o.value = ''; o.textContent = 'configuration hors preset'; o.selected = true; // Une bascule en cours ne doit pas voir sa sélection sauter sous la souris.
sel.insertBefore(o, sel.firstChild); if(prev && [...sel.options].some(o => o.value === prev)) sel.value = prev;
}
} }
// État initial des libellés (le CSS, lui, est déjà appliqué par le <head>). // État initial des libellés (le CSS, lui, est déjà appliqué par le <head>).
+15 -7
View File
@@ -35,7 +35,8 @@
--ok:#5e7f5a;--warn:#a86a2a;--err:#c0503f;--mag:#a86a2a; --ok:#5e7f5a;--warn:#a86a2a;--err:#c0503f;--mag:#a86a2a;
--code-bg:#f4f6f5;--danger-bg:#faeae7;--warn-bg:#f7f0e5; --code-bg:#f4f6f5;--danger-bg:#faeae7;--warn-bg:#f7f0e5;
--add:#5e7f5a;--add-bg:rgba(94,127,90,.11);--del:#c0503f;--del-bg:rgba(192,80,63,.12); --add:#5e7f5a;--add-bg:rgba(94,127,90,.11);--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: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,.07),0 1px 3px rgba(15,23,42,.05)}
/* « Deep Dark » : fond #0F172A, cartes #1E293B. */ /* « Deep Dark » : fond #0F172A, cartes #1E293B. */
[data-theme="dark"]{color-scheme:dark; [data-theme="dark"]{color-scheme:dark;
--bg:#0f172a;--panel:#1e293b;--bubble:#1e293b;--border:rgba(148,163,184,.17); --bg:#0f172a;--panel:#1e293b;--bubble:#1e293b;--border:rgba(148,163,184,.17);
@@ -46,7 +47,8 @@
--ok:#8fb08c;--warn:#d9a55f;--err:#e0705c;--mag:#d9a55f; --ok:#8fb08c;--warn:#d9a55f;--err:#e0705c;--mag:#d9a55f;
--code-bg:#131c2f;--danger-bg:#3a1f1c;--warn-bg:#33291b; --code-bg:#131c2f;--danger-bg:#3a1f1c;--warn-bg:#33291b;
--add:#8fb08c;--add-bg:rgba(143,176,140,.16);--del:#e0705c;--del-bg:rgba(224,112,92,.16); --add:#8fb08c;--add-bg:rgba(143,176,140,.16);--del:#e0705c;--del-bg:rgba(224,112,92,.16);
--shadow:none;--shadow-lift:0 4px 16px rgba(0,0,0,.35)} --shadow:none;--shadow-lift:0 4px 16px rgba(0,0,0,.35);
--shadow-card:0 2px 12px rgba(0,0,0,.30),0 1px 3px rgba(0,0,0,.22)}
/* Safari iOS peint un rectangle gris/noir sur TOUT élément touché (bouton, lien, /* 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 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 de notre charte et impossible à styler : on le coupe. Nos états :active/:focus
@@ -1570,12 +1572,12 @@ button:active:not(:disabled){transform:translateY(1px)}
nu flotte sans ancrage, et la frontière entre deux réponses se perd. */ nu flotte sans ancrage, et la frontière entre deux réponses se perd. */
#chat{padding-top:22px} #chat{padding-top:22px}
#chat .msg.assistant{background:var(--bubble);border:1px solid var(--border); #chat .msg.assistant{background:var(--bubble);border:1px solid var(--border);
border-radius:16px;padding:14px 18px;max-width:100%;box-shadow:var(--shadow)} border-radius:16px;padding:14px 18px;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)} #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 /* Question : aplat de sauge, coin bas-droit rentré. --on-accent garantit le
contraste dans les DEUX variantes. */ contraste dans les DEUX variantes. */
#chat .msg.user{background:var(--accent);color:var(--on-accent);border-radius:18px 18px 6px 18px; #chat .msg.user{background:var(--accent);color:var(--on-accent);border-radius:18px 18px 6px 18px;
padding:11px 16px;max-width:75%} padding:11px 16px;max-width:75%;box-shadow:var(--shadow-card)}
#chat .msg.user .label{color:var(--on-accent);opacity:.75} #chat .msg.user .label{color:var(--on-accent);opacity:.75}
#chat .msg.user .body a{color:var(--on-accent);text-decoration:underline} #chat .msg.user .body a{color:var(--on-accent);text-decoration:underline}
#chat .msg.user code{background:rgba(255,255,255,.18);color:inherit} #chat .msg.user code{background:rgba(255,255,255,.18);color:inherit}
@@ -1592,13 +1594,16 @@ button:active:not(:disabled){transform:translateY(1px)}
#send,#stop{width:34px;height:34px;min-height:34px;padding:0;border-radius:999px; #send,#stop{width:34px;height:34px;min-height:34px;padding:0;border-radius:999px;
display:inline-flex;align-items:center;justify-content:center;flex:none} display:inline-flex;align-items:center;justify-content:center;flex:none}
#send svg,#stop svg{display:block} #send svg,#stop svg{display:block}
#send:hover:not(:disabled){opacity:1;transform:none;background:color-mix(in srgb,var(--accent) 85%,var(--text))} #send{background:transparent;border-color:transparent;color:var(--accent)}
#send:hover:not(:disabled){opacity:1;transform:none;background:var(--accent-bg);color:var(--accent)}
#send:active:not(:disabled){transform:scale(.92)} #send:active:not(:disabled){transform:scale(.92)}
#stop:hover{opacity:1;background:color-mix(in srgb,var(--err) 85%,var(--text))} #send:disabled{background:transparent;color:var(--dim)}
#stop{background:transparent;border-color:transparent;color:var(--err)}
#stop:hover{opacity:1;background:var(--danger-bg);color:var(--err)}
/* Les outils de la saisie (joindre, fichiers, envoyer) montent DANS la pilule, /* 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 à 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. */ 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)} #inputbar{align-items:flex-end;padding:8px 8px 8px 18px;gap:4px;box-shadow:var(--shadow-card)}
#composer:focus-within #inputbar,#composer:focus-within #ctxmeta{border-color:var(--accent-soft)} #composer:focus-within #inputbar,#composer:focus-within #ctxmeta{border-color:var(--accent-soft)}
#inputbar #attach,#inputbar #files-btn{width:34px;height:34px;min-height:34px;border-radius:999px; #inputbar #attach,#inputbar #files-btn{width:34px;height:34px;min-height:34px;border-radius:999px;
align-self:center} align-self:center}
@@ -1670,3 +1675,6 @@ button:active:not(:disabled){transform:translateY(1px)}
garde-fou de largeur (contrairement à .pe-row-c) : une valeur très longue — garde-fou de largeur (contrairement à .pe-row-c) : une valeur très longue —
un chemin de destination, par exemple — débordait sous l'intitulé. */ un chemin de destination, par exemple — débordait sous l'intitulé. */
.pe-row>.pe-selc{max-width:62%} .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}