karlexmarin Claude Opus 4.7 (1M context) commited on
Commit
fbec820
·
1 Parent(s): 81fc8a0

v0.7.0: SWA Unmasker (anti-bullshit #1) + foldable main panels + preset auto-fill

Browse files

Ships the first feature of the v0.7 anti-bullshit pack inspired by HF community pain points: detect when a model card claims a max_position_embeddings far larger than its effective context (Mistral-7B-v0.1: declared 32k, attends ~8k via SWA).

NEW
- 🪟 Unmask mode: paste an HF model id (or raw config.json) → 1-second verdict (HONEST / INFLATED / SEVERELY INFLATED / YARN-EXTENDED). Pure browser arithmetic on config.json: SWA window + RoPE-scaling + GQA/d_head heuristics. No GPU, no inference.
- js/swa_unmasker.js: pure logic module, no human strings. Returns warning codes + params; main.js renders via i18n with {placeholder} substitution so EN/ES/FR/ZH all work.
- tFmt() i18n helper: t(key) with {placeholder} replacement.

UX
- All <main> sections now wrapped at runtime in <details open> with foldable header (idempotent wrapMainSectionsAsFoldable). Big ▼ arrow rotates to ▶ when collapsed. Triple-browser marker hide (list-style + ::-webkit-details-marker + ::marker).
- Inventory modal: 4 inv-cards now <details open> with arrow per card.
- Architectures-supported defaults to open; tooltip text removed "(click to expand)".
- Profile + Recipe preset dropdowns now also auto-fill the HF id input (presets are HF model ids; clarifies dual source of truth — preset = cached, 📥 Fetch = live HF Hub).

i18n cleanup (the big one)
- swa_unmasker hardcoded English strings: GONE. All warnings/recommendations/verdict labels live in i18n with {placeholder} substitution × EN/ES/FR/ZH.
- 8 mode-desc strings: refactored from inline string switch to t(`mode_desc.${mode}`) lookup. Section show/hide reduced to a sectionMap object.
- modes.tip updated: "7 modes" → "8 modes" (added Unmask).
- 423 keys × 4 langs, 0 missing / 0 extra (parity verified).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Files changed (5) hide show
  1. index.html +42 -13
  2. js/i18n.js +232 -8
  3. js/main.js +209 -35
  4. js/swa_unmasker.js +107 -0
  5. style.css +146 -4
index.html CHANGED
@@ -249,8 +249,8 @@
249
  <button class="help-close" id="inventory-close" aria-label="Close inventory">×</button>
250
  <h2 id="inv-modal-title" data-i18n="inv.title">🧰 What this tool gives you</h2>
251
  <div class="inventory-grid">
252
- <div class="inv-card">
253
- <h3 data-i18n="inv.recipes.title">🎯 8 recipes &mdash; does this model fit your use case?</h3>
254
  <ul>
255
  <li><strong data-i18n="inv.recipes.x1.title">Custom train vs API</strong>: <span data-i18n="inv.recipes.x1.body">which is cheaper for your traffic?</span></li>
256
  <li><strong data-i18n="inv.recipes.x2.title">Long context</strong>: <span data-i18n="inv.recipes.x2.body">will it handle 32k / 128k tokens reliably?</span></li>
@@ -261,35 +261,35 @@
261
  <li><strong data-i18n="inv.recipes.x22.title">Compute-context</strong>: <span data-i18n="inv.recipes.x22.body">does the model fit the empirical band?</span></li>
262
  <li><strong data-i18n="inv.recipes.x23.title">IH-phase</strong>: <span data-i18n="inv.recipes.x23.body">pre- or post-induction-head?</span></li>
263
  </ul>
264
- </div>
265
- <div class="inv-card">
266
- <h3 data-i18n="inv.diag.title">🔬 Diagnostics</h3>
267
  <ul>
268
  <li data-i18n="inv.diag.gamma"><strong>γ predicted vs observed</strong> &mdash; auto-classifies the model into 5 regimes (normal · fraud / inflated context · compressed · over-Padé · sliding-window)</li>
269
  <li data-i18n="inv.diag.cardy"><strong>Cardy ΔH</strong> &mdash; entropy shift between observed and nominal context</li>
270
  <li data-i18n="inv.diag.fals"><strong>Falsification dashboard</strong> &mdash; checks 23 specific predictions (F1–F23)</li>
271
  <li data-i18n="inv.diag.alg"><strong>Algebraic consistency</strong> &mdash; 8 mathematical identities the model must satisfy</li>
272
  </ul>
273
- </div>
274
- <div class="inv-card">
275
- <h3 data-i18n="inv.verify.title">✓ Formally verified math</h3>
276
  <ul>
277
  <li data-i18n="inv.verify.count"><strong>37 theorems</strong> machine-proven in Lean 4 + Mathlib4</li>
278
  <li data-i18n="inv.verify.click">Click any badge → opens the source line on GitHub</li>
279
  <li data-i18n="inv.verify.reverify">Verify yourself: <code>lake build</code> (≈5 s after cache fetch)</li>
280
  </ul>
281
- </div>
282
- <div class="inv-card">
283
- <h3 data-i18n="inv.export.title">📤 Export &amp; share</h3>
284
  <ul>
285
  <li data-i18n="inv.export.formats"><strong>JSON · Markdown · LaTeX</strong> (paper-ready)</li>
286
  <li data-i18n="inv.export.share">Reproducible share link (state encoded in URL)</li>
287
  <li data-i18n="inv.export.registry">Submit to community registry on GitHub</li>
288
  </ul>
289
- </div>
290
  </div>
291
 
292
- <details class="arch-supported">
293
  <summary data-i18n="arch.summary">Architectures supported (click to expand)</summary>
294
  <div class="arch-badges">
295
  <span class="badge">✓ RoPE-MHA <span class="info"><span class="tooltip" data-i18n="tooltip.mha">Multi-Head Attention: each token position attends through several parallel heads at once.</span></span></span>
@@ -334,6 +334,7 @@
334
  <button class="mode-btn" data-mode="recipe" role="tab" aria-selected="false" data-i18n="modes.recipe">📋 Pick recipe</button>
335
  <button class="mode-btn" data-mode="diagnose" role="tab" aria-selected="false" data-i18n="modes.diagnose">🩺 Diagnose CLI</button>
336
  <button class="mode-btn" data-mode="phase" role="tab" aria-selected="false" data-i18n="modes.phase">📊 Phase diagram</button>
 
337
  </div>
338
  <p id="mode-desc" class="recipe-desc" data-i18n="modes.desc">
339
  <strong>Quickest start</strong>: paste any HuggingFace model id (e.g. <code>meta-llama/Meta-Llama-3-8B</code>),
@@ -623,6 +624,34 @@
623
  <div id="phase-info" class="recipe-desc" style="margin-top:0.6em;"></div>
624
  </section>
625
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
626
  <!-- Recipe selector (mode=recipe) -->
627
  <section id="recipe-section" style="display:none;">
628
  <h2 data-i18n="recipe.title">📋 Recipe</h2>
 
249
  <button class="help-close" id="inventory-close" aria-label="Close inventory">×</button>
250
  <h2 id="inv-modal-title" data-i18n="inv.title">🧰 What this tool gives you</h2>
251
  <div class="inventory-grid">
252
+ <details class="inv-card" open>
253
+ <summary class="inv-card-title" data-i18n="inv.recipes.title">🎯 8 recipes &mdash; does this model fit your use case?</summary>
254
  <ul>
255
  <li><strong data-i18n="inv.recipes.x1.title">Custom train vs API</strong>: <span data-i18n="inv.recipes.x1.body">which is cheaper for your traffic?</span></li>
256
  <li><strong data-i18n="inv.recipes.x2.title">Long context</strong>: <span data-i18n="inv.recipes.x2.body">will it handle 32k / 128k tokens reliably?</span></li>
 
261
  <li><strong data-i18n="inv.recipes.x22.title">Compute-context</strong>: <span data-i18n="inv.recipes.x22.body">does the model fit the empirical band?</span></li>
262
  <li><strong data-i18n="inv.recipes.x23.title">IH-phase</strong>: <span data-i18n="inv.recipes.x23.body">pre- or post-induction-head?</span></li>
263
  </ul>
264
+ </details>
265
+ <details class="inv-card" open>
266
+ <summary class="inv-card-title" data-i18n="inv.diag.title">🔬 Diagnostics</summary>
267
  <ul>
268
  <li data-i18n="inv.diag.gamma"><strong>γ predicted vs observed</strong> &mdash; auto-classifies the model into 5 regimes (normal · fraud / inflated context · compressed · over-Padé · sliding-window)</li>
269
  <li data-i18n="inv.diag.cardy"><strong>Cardy ΔH</strong> &mdash; entropy shift between observed and nominal context</li>
270
  <li data-i18n="inv.diag.fals"><strong>Falsification dashboard</strong> &mdash; checks 23 specific predictions (F1–F23)</li>
271
  <li data-i18n="inv.diag.alg"><strong>Algebraic consistency</strong> &mdash; 8 mathematical identities the model must satisfy</li>
272
  </ul>
273
+ </details>
274
+ <details class="inv-card" open>
275
+ <summary class="inv-card-title" data-i18n="inv.verify.title">✓ Formally verified math</summary>
276
  <ul>
277
  <li data-i18n="inv.verify.count"><strong>37 theorems</strong> machine-proven in Lean 4 + Mathlib4</li>
278
  <li data-i18n="inv.verify.click">Click any badge → opens the source line on GitHub</li>
279
  <li data-i18n="inv.verify.reverify">Verify yourself: <code>lake build</code> (≈5 s after cache fetch)</li>
280
  </ul>
281
+ </details>
282
+ <details class="inv-card" open>
283
+ <summary class="inv-card-title" data-i18n="inv.export.title">📤 Export &amp; share</summary>
284
  <ul>
285
  <li data-i18n="inv.export.formats"><strong>JSON · Markdown · LaTeX</strong> (paper-ready)</li>
286
  <li data-i18n="inv.export.share">Reproducible share link (state encoded in URL)</li>
287
  <li data-i18n="inv.export.registry">Submit to community registry on GitHub</li>
288
  </ul>
289
+ </details>
290
  </div>
291
 
292
+ <details class="arch-supported" open>
293
  <summary data-i18n="arch.summary">Architectures supported (click to expand)</summary>
294
  <div class="arch-badges">
295
  <span class="badge">✓ RoPE-MHA <span class="info"><span class="tooltip" data-i18n="tooltip.mha">Multi-Head Attention: each token position attends through several parallel heads at once.</span></span></span>
 
334
  <button class="mode-btn" data-mode="recipe" role="tab" aria-selected="false" data-i18n="modes.recipe">📋 Pick recipe</button>
335
  <button class="mode-btn" data-mode="diagnose" role="tab" aria-selected="false" data-i18n="modes.diagnose">🩺 Diagnose CLI</button>
336
  <button class="mode-btn" data-mode="phase" role="tab" aria-selected="false" data-i18n="modes.phase">📊 Phase diagram</button>
337
+ <button class="mode-btn" data-mode="unmask" role="tab" aria-selected="false" data-i18n="modes.unmask">🪟 Unmask</button>
338
  </div>
339
  <p id="mode-desc" class="recipe-desc" data-i18n="modes.desc">
340
  <strong>Quickest start</strong>: paste any HuggingFace model id (e.g. <code>meta-llama/Meta-Llama-3-8B</code>),
 
624
  <div id="phase-info" class="recipe-desc" style="margin-top:0.6em;"></div>
625
  </section>
626
 
627
+ <!-- Unmask mode: detect misleading max_position_embeddings via SWA / RoPE-scaling -->
628
+ <section id="unmask-section" style="display:none;">
629
+ <h2><span data-i18n="unmask.title">🪟 Context Unmasker</span>
630
+ <span class="info"><span class="tooltip" data-i18n="unmask.tip">
631
+ Paste a HuggingFace model id (or raw config.json). The tool checks for
632
+ sliding-window attention, RoPE scaling (YaRN/linear/dynamic NTK), and
633
+ GQA — anything that makes <code>max_position_embeddings</code> larger
634
+ than the practical effective context. Mistral-7B-v0.1 is the canonical
635
+ example: declared 32k, attends within ~4-8k.
636
+ </span></span>
637
+ </h2>
638
+ <p class="recipe-desc" data-i18n="unmask.desc">
639
+ <strong>Are you about to spend money on a model that won't actually attend that far?</strong> Paste an id and find out in 1 second. No GPU, no inference — just config.json arithmetic.
640
+ </p>
641
+ <div class="form-row">
642
+ <label for="unmask-id" data-i18n="unmask.id_label">HF model id:</label>
643
+ <input type="text" id="unmask-id" placeholder="e.g. mistralai/Mistral-7B-v0.1" />
644
+ <button type="button" id="unmask-fetch-btn" data-i18n="unmask.fetch_btn">🔍 Unmask</button>
645
+ </div>
646
+ <p id="unmask-status" class="recipe-desc" style="font-size:0.92em;"></p>
647
+ <details style="margin: 0.6em 0;">
648
+ <summary style="cursor:pointer; font-size:0.92em;" data-i18n="unmask.paste_summary">Or paste raw config.json (private / in-dev models)</summary>
649
+ <textarea id="unmask-paste" rows="6" style="width:100%; font-family:monospace; font-size:0.85em; margin-top:0.4em;" placeholder='{"max_position_embeddings": 32768, "sliding_window": 4096, ...}'></textarea>
650
+ <button type="button" id="unmask-paste-btn" data-i18n="unmask.paste_btn" style="margin-top:0.4em;">🔍 Unmask pasted config</button>
651
+ </details>
652
+ <div id="unmask-output" style="margin-top: 1em;"></div>
653
+ </section>
654
+
655
  <!-- Recipe selector (mode=recipe) -->
656
  <section id="recipe-section" style="display:none;">
657
  <h2 data-i18n="recipe.title">📋 Recipe</h2>
js/i18n.js CHANGED
@@ -125,7 +125,7 @@ export const TRANSLATIONS = {
125
  "inv.export.formats": "<strong>JSON · Markdown · LaTeX</strong> (paper-ready)",
126
  "inv.export.share": "Reproducible share link (state encoded in URL)",
127
  "inv.export.registry": "Submit to community registry on GitHub",
128
- "arch.summary": "Architectures supported (click to expand)",
129
  "arch.anyhf": "✓ Any HuggingFace public model",
130
  "tooltip.mha": "Multi-Head Attention: each token position attends through several parallel heads at once.",
131
  "tooltip.gqa": "Grouped Query Attention: queries share fewer keys/values than heads (saves memory but pushes γ toward Hagedorn).",
@@ -133,6 +133,62 @@ export const TRANSLATIONS = {
133
  "tooltip.abspe": "Absolute Position Embeddings: each position has a fixed learned vector added to the token embedding.",
134
  "tooltip.swa": "Sliding Window Attention: each token only attends within a fixed local window (Mistral, gemma-2 use this).",
135
  "tooltip.ssm": "State Space Model: a sequence layer that maintains internal state instead of attention (Mamba, Jamba use this).",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
  "share.import_desc": "Got a JSON file from someone else's TAF analysis? Load it here to see the verdict + chain locally. Same view as if you'd run it yourself.",
137
  "share.import_btn": "📂 Load shared JSON",
138
  "synthesis.system": "You are a precise transformer LLM diagnostic assistant. Given pre-computed TAF formula results, write a clear plain-English summary in 4-6 sentences. Cite the section number (§X.Y) for each number you mention. Always give a concrete recommendation. Do NOT invent numbers.",
@@ -225,7 +281,7 @@ export const TRANSLATIONS = {
225
  "common.no": "No",
226
 
227
  // Mode tooltips
228
- "modes.tip": "<strong>Seven ways to use the tool</strong>.<br><strong>📇 Profile</strong>: paste a model id → 5-recipe TAF Card.<br><strong>🆚 Compare</strong>: 2-3 models side-by-side on one recipe.<br><strong>🔍 Inspect config</strong>: paste raw config.json → full Profile.<br><strong>💬 Ask</strong>: free-form question, browser LLM picks the recipe.<br><strong>📋 Recipe</strong>: manual selection with full form control.<br><strong>🩺 Diagnose CLI</strong>: generate Python command for local γ measurement.<br><strong>📊 Phase diagram</strong>: 23-model panel on (log θ, γ) plane.",
229
  "profile.tip": "<strong>One-click full diagnosis</strong>. Paste any HF model id (or pick preset). Tool runs all 5 recipes (long-context, KV-compression, custom-vs-API, budget, hardware) and produces a single <strong>TAF Card</strong> with verdict per dimension + key numbers + architecture classification.<br><br><strong>Use case</strong>: \"I'm evaluating Qwen2.5-32B for production — what's its full viability profile?\" → paste id → Profile → done.",
230
  "compare.tip": "<strong>Same recipe, multiple models</strong>. Pick 2-3 candidate models and one recipe. See verdicts in a single comparison table.<br><br><strong>Use case</strong>: \"I need long-context retrieval at 16K — which is best: Llama-3-8B, Mistral-7B, or Qwen-7B?\" → pick 3 + X-2 + 16K → see winner.",
231
 
@@ -682,7 +738,7 @@ export const TRANSLATIONS = {
682
  "inv.export.formats": "<strong>JSON · Markdown · LaTeX</strong> (listo para paper)",
683
  "inv.export.share": "Link reproducible (estado codificado en URL)",
684
  "inv.export.registry": "Envía al registro comunitario en GitHub",
685
- "arch.summary": "Arquitecturas soportadas (click para expandir)",
686
  "arch.anyhf": "✓ Cualquier modelo público de HuggingFace",
687
  "tooltip.mha": "Multi-Head Attention: cada posición atiende mediante varios heads paralelos a la vez.",
688
  "tooltip.gqa": "Grouped Query Attention: las queries comparten menos keys/values que heads (ahorra memoria pero empuja γ hacia Hagedorn).",
@@ -690,6 +746,62 @@ export const TRANSLATIONS = {
690
  "tooltip.abspe": "Absolute Position Embeddings: cada posición tiene un vector fijo aprendido sumado al embedding del token.",
691
  "tooltip.swa": "Sliding Window Attention: cada token solo atiende dentro de una ventana local fija (Mistral, gemma-2 lo usan).",
692
  "tooltip.ssm": "State Space Model: capa de secuencia que mantiene estado interno en lugar de atención (Mamba, Jamba lo usan).",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
693
  "share.import_desc": "¿Tienes un fichero JSON del análisis TAF de alguien? Cárgalo aquí para ver el veredicto + cadena localmente. La misma vista que si lo hubieras ejecutado tú.",
694
  "share.import_btn": "📂 Cargar JSON compartido",
695
  "synthesis.system": "Eres un asistente de diagnóstico preciso para LLMs transformer. Dados resultados de fórmulas TAF pre-calculados, escribe un resumen claro en español de 4-6 frases. Cita el número de sección (§X.Y) para cada número que menciones. Da siempre una recomendación concreta. NO inventes números.",
@@ -782,7 +894,7 @@ export const TRANSLATIONS = {
782
  "common.no": "No",
783
 
784
  // Tooltips de modos
785
- "modes.tip": "<strong>Siete formas de usar la herramienta</strong>.<br><strong>📇 Perfil</strong>: pega un id → TAF Card de 5 recetas.<br><strong>🆚 Comparar</strong>: 2-3 modelos lado a lado en una receta.<br><strong>🔍 Inspeccionar config</strong>: pega config.json crudo → Perfil completo.<br><strong>💬 Pregunta</strong>: pregunta libre, el LLM del navegador elige la receta.<br><strong>📋 Receta</strong>: selección manual con control total del formulario.<br><strong>🩺 Diagnóstico CLI</strong>: genera comando Python para medir γ localmente.<br><strong>📊 Diagrama de fase</strong>: panel de 23 modelos en plano (log θ, γ).",
786
  "profile.tip": "<strong>Diagnóstico completo en un click</strong>. Pega cualquier id de modelo HF (o elige preset). La herramienta ejecuta las 5 recetas (contexto largo, compresión KV, custom vs API, presupuesto, hardware) y produce una única <strong>TAF Card</strong> con veredicto por dimensión + números clave + clasificación arquitectónica.<br><br><strong>Caso de uso</strong>: \"Estoy evaluando Qwen2.5-32B para producción — ¿cuál es su perfil completo de viabilidad?\" → pega id → Perfilar → listo.",
787
  "compare.tip": "<strong>Misma receta, múltiples modelos</strong>. Elige 2-3 modelos candidatos y una receta. Ve los veredictos en una única tabla comparativa.<br><br><strong>Caso de uso</strong>: \"Necesito recuperación de contexto largo a 16K — ¿cuál es mejor: Llama-3-8B, Mistral-7B o Qwen-7B?\" → elige 3 + X-2 + 16K → ve el ganador.",
788
 
@@ -1103,7 +1215,7 @@ export const TRANSLATIONS = {
1103
  "inv.export.formats": "<strong>JSON · Markdown · LaTeX</strong> (prêt pour papier)",
1104
  "inv.export.share": "Lien reproductible (état encodé dans l'URL)",
1105
  "inv.export.registry": "Soumettre au registre communautaire sur GitHub",
1106
- "arch.summary": "Architectures prises en charge (cliquez pour déplier)",
1107
  "arch.anyhf": "✓ Tout modèle public HuggingFace",
1108
  "tooltip.mha": "Multi-Head Attention : chaque position attend via plusieurs têtes parallèles à la fois.",
1109
  "tooltip.gqa": "Grouped Query Attention : les queries partagent moins de keys/values que de heads (économise mémoire mais pousse γ vers Hagedorn).",
@@ -1111,6 +1223,62 @@ export const TRANSLATIONS = {
1111
  "tooltip.abspe": "Absolute Position Embeddings : chaque position a un vecteur fixe appris ajouté au token.",
1112
  "tooltip.swa": "Sliding Window Attention : chaque token n'attend que dans une fenêtre locale fixe (Mistral, gemma-2 l'utilisent).",
1113
  "tooltip.ssm": "State Space Model : couche de séquence qui maintient un état interne au lieu d'attention (Mamba, Jamba l'utilisent).",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1114
  "share.import_desc": "Vous avez un fichier JSON de l'analyse TAF de quelqu'un ? Chargez-le ici pour voir le verdict + la chaîne localement. La même vue que si vous l'aviez exécuté vous-même.",
1115
  "share.import_btn": "📂 Charger JSON partagé",
1116
  "synthesis.system": "Vous êtes un assistant de diagnostic précis pour LLMs transformer. Étant donné des résultats de formules TAF pré-calculés, écrivez un résumé clair en français de 4-6 phrases. Citez le numéro de section (§X.Y) pour chaque nombre mentionné. Donnez toujours une recommandation concrète. N'INVENTEZ PAS de nombres.",
@@ -1203,7 +1371,7 @@ export const TRANSLATIONS = {
1203
  "common.no": "Non",
1204
 
1205
  // Tooltips des modes
1206
- "modes.tip": "<strong>Sept façons d'utiliser l'outil</strong>.<br><strong>📇 Profil</strong>: collez un id → TAF Card avec 5 recettes.<br><strong>🆚 Comparer</strong>: 2-3 modèles côte à côte sur une recette.<br><strong>🔍 Inspecter config</strong>: collez config.json brut → Profil complet.<br><strong>💬 Question</strong>: question libre, le LLM du navigateur choisit la recette.<br><strong>📋 Recette</strong>: sélection manuelle avec contrôle total du formulaire.<br><strong>🩺 Diagnostic CLI</strong>: génère commande Python pour mesurer γ localement.<br><strong>📊 Diagramme de phase</strong>: panel de 23 modèles dans le plan (log θ, γ).",
1207
  "profile.tip": "<strong>Diagnostic complet en un clic</strong>. Collez n'importe quel id de modèle HF (ou choisissez préréglage). L'outil exécute les 5 recettes (contexte long, compression KV, custom vs API, budget, hardware) et produit une <strong>TAF Card</strong> unique avec verdict par dimension + nombres clés + classification architecturale.<br><br><strong>Cas d'usage</strong>: « J'évalue Qwen2.5-32B pour la production — quel est son profil complet de viabilité ? » → collez id → Profiler → fait.",
1208
  "compare.tip": "<strong>Même recette, plusieurs modèles</strong>. Choisissez 2-3 modèles candidats et une recette. Voyez les verdicts dans un seul tableau comparatif.<br><br><strong>Cas d'usage</strong>: « J'ai besoin de récupération longue contexte à 16K — quel est le meilleur : Llama-3-8B, Mistral-7B ou Qwen-7B ? » → choisissez 3 + X-2 + 16K → voyez le gagnant.",
1209
 
@@ -1524,7 +1692,7 @@ export const TRANSLATIONS = {
1524
  "inv.export.formats": "<strong>JSON · Markdown · LaTeX</strong>(论文级)",
1525
  "inv.export.share": "可复现的分享链接(状态编入 URL)",
1526
  "inv.export.registry": "提交到 GitHub 上的社区登记",
1527
- "arch.summary": "支持的架构(点击展开)",
1528
  "arch.anyhf": "✓ 任意 HuggingFace 公开模型",
1529
  "tooltip.mha": "Multi-Head Attention:每个 token 位置同时通过多个并行 head 进行注意力计算。",
1530
  "tooltip.gqa": "Grouped Query Attention:queries 共享比 heads 更少的 keys/values(节省内存但把 γ 推向 Hagedorn)。",
@@ -1532,6 +1700,62 @@ export const TRANSLATIONS = {
1532
  "tooltip.abspe": "Absolute Position Embeddings:每个位置有一个固定的学习向量加到 token embedding。",
1533
  "tooltip.swa": "Sliding Window Attention:每个 token 仅在固定局部窗口内做注意力(Mistral、gemma-2 使用此机制)。",
1534
  "tooltip.ssm": "State Space Model:维护内部状态的序列层(取代注意力,Mamba、Jamba 使用此机制)。",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1535
  "share.import_desc": "有他人 TAF 分析的 JSON 文件? 在这里加载以本地查看判定 + 链。与您自己运行的视图相同。",
1536
  "share.import_btn": "📂 加载共享的 JSON",
1537
  "synthesis.system": "您是 transformer LLM 的精确诊断助手。给定预先计算的 TAF 公式结果,用 4-6 句中文写出清晰的摘要。为每个提到的数字引用章节号 (§X.Y)。始终给出具体建议。不要编造数字。",
@@ -1624,7 +1848,7 @@ export const TRANSLATIONS = {
1624
  "common.no": "否",
1625
 
1626
  // 模式提示
1627
- "modes.tip": "<strong>种使用方式</strong>。<br><strong>📇 画像</strong>: 粘贴模型 id → 5 个配方的 TAF 卡。<br><strong>🆚 比较</strong>: 2-3 个模型在一个配方上并排比较。<br><strong>🔍 检查 config</strong>: 粘贴原始 config.json → 完整画像。<br><strong>💬 提问</strong>: 自由形式问题,浏览器 LLM 选择配方。<br><strong>📋 配方</strong>: 手动选择,完全控制表单。<br><strong>🩺 CLI 诊断</strong>: 生成 Python 命令在本地测量 γ。<br><strong>📊 相图</strong>: 23 个面板模型在 (log θ, γ) 平面上。",
1628
  "profile.tip": "<strong>一键完整诊断</strong>。粘贴任意 HF 模型 id (或选择预设)。工具运行所有 5 个配方 (长上下文、KV 压缩、自定义 vs API、预算、硬件),生成单个 <strong>TAF 卡</strong>,显示每个维度的判定 + 关键数字 + 架构分类。<br><br><strong>用例</strong>: \"我正在为生产评估 Qwen2.5-32B — 它的完整可行性概况是什么?\" → 粘贴 id → 画像 → 完成。",
1629
  "compare.tip": "<strong>同一配方,多个模型</strong>。选择 2-3 个候选模型和一个配方。在单个比较表中查看判定。<br><br><strong>用例</strong>: \"我需要在 16K 进行长上下文检索 — 哪个最好: Llama-3-8B、Mistral-7B 或 Qwen-7B?\" → 选择 3 个 + X-2 + 16K → 看赢家。",
1630
 
 
125
  "inv.export.formats": "<strong>JSON · Markdown · LaTeX</strong> (paper-ready)",
126
  "inv.export.share": "Reproducible share link (state encoded in URL)",
127
  "inv.export.registry": "Submit to community registry on GitHub",
128
+ "arch.summary": "Architectures supported",
129
  "arch.anyhf": "✓ Any HuggingFace public model",
130
  "tooltip.mha": "Multi-Head Attention: each token position attends through several parallel heads at once.",
131
  "tooltip.gqa": "Grouped Query Attention: queries share fewer keys/values than heads (saves memory but pushes γ toward Hagedorn).",
 
133
  "tooltip.abspe": "Absolute Position Embeddings: each position has a fixed learned vector added to the token embedding.",
134
  "tooltip.swa": "Sliding Window Attention: each token only attends within a fixed local window (Mistral, gemma-2 use this).",
135
  "tooltip.ssm": "State Space Model: a sequence layer that maintains internal state instead of attention (Mamba, Jamba use this).",
136
+
137
+ // v0.7.0 — anti-bullshit pack #1: SWA / RoPE-scaling unmasker
138
+ "modes.unmask": "🪟 Unmask",
139
+ "unmask.title": "🪟 Context Unmasker",
140
+ "unmask.tip": "Paste a HuggingFace model id (or raw config.json). The tool checks for sliding-window attention, RoPE scaling (YaRN/linear/dynamic NTK), and GQA — anything that makes <code>max_position_embeddings</code> larger than the practical effective context. Mistral-7B-v0.1 is the canonical example: declared 32k, attends within ~4-8k.",
141
+ "unmask.desc": "<strong>Are you about to spend money on a model that won't actually attend that far?</strong> Paste an id and find out in 1 second. No GPU, no inference — just config.json arithmetic.",
142
+ "unmask.id_label": "HF model id:",
143
+ "unmask.fetch_btn": "🔍 Unmask",
144
+ "unmask.paste_summary": "Or paste raw config.json (private / in-dev models)",
145
+ "unmask.paste_btn": "🔍 Unmask pasted config",
146
+ "unmask.label.declared": "Declared context",
147
+ "unmask.label.effective": "Effective (estimate)",
148
+ "unmask.label.ratio": "Ratio",
149
+ "unmask.section.flags": "Architecture flags",
150
+ "unmask.section.warnings": "Warnings",
151
+ "unmask.section.reco": "Recommendation",
152
+ "unmask.flag.swa": "SWA",
153
+ "unmask.flag.rope": "RoPE scaling",
154
+ "unmask.flag.gqa": "GQA",
155
+ "unmask.flag.layers": "Layers",
156
+ "unmask.flag.dhead": "d_head",
157
+ "unmask.flag.theta": "RoPE θ",
158
+ "unmask.flag.yes": "yes",
159
+ "unmask.flag.no": "no",
160
+ "unmask.flag.full_mha": "no (full MHA, {n} heads)",
161
+ "unmask.verdict.honest": "✅ HONEST",
162
+ "unmask.verdict.inflated": "⚠ INFLATED",
163
+ "unmask.verdict.severely_inflated": "❌ SEVERELY INFLATED",
164
+ "unmask.verdict.yarn_extended": "⚠ YARN-EXTENDED",
165
+ "unmask.verdict.unknown": "❓ UNKNOWN",
166
+ "unmask.warn.swa_window": "SWA window: {window} tokens — each layer only attends within this window.",
167
+ "unmask.warn.multihop": "Multi-hop estimate: ~{multiHop} tokens (conservative: window × {factor}).",
168
+ "unmask.warn.yarn": "RoPE scaling ({type}) extends context {factor}× from ~{original} to {declared} tokens.",
169
+ "unmask.warn.yarn_advice": "RoPE-extended context — verify γ behavior at the full claimed length with the γ_check diagnostic.",
170
+ "unmask.warn.gqa_small_dhead": "Small head dim ({d_head}) + GQA: KV cache compression at long context is likely (γ pushed toward Hagedorn).",
171
+ "unmask.reco.honest": "Standard full-attention model. Effective context matches declared ({declared} tokens).",
172
+ "unmask.reco.inflated": "Effective ~{effective} tokens via SWA. Use γ_check to verify behavior at your target evaluation length.",
173
+ "unmask.reco.severely_inflated": "Treat as a ~{effective}-token context model in practice. The {declared}-token claim only applies via cross-layer attention chains, which empirically degrade past ~2× the SWA window.",
174
+ "unmask.reco.yarn_extended": "RoPE-extended context. Run a long-context benchmark (NIAH at 8k / 16k / 32k / full) to confirm the extension holds. Use γ_check with T_eval = {declared}.",
175
+ "unmask.reco.unknown": "Could not parse config. Verify the URL is a valid HF model with public config.json.",
176
+ "unmask.status.empty_id": "⚠ Enter a model id (e.g. mistralai/Mistral-7B-v0.1).",
177
+ "unmask.status.fetching": "⏳ Fetching config.json for {modelId}...",
178
+ "unmask.status.success": "✅ Analyzed {modelId} (verdict: {verdict})",
179
+ "unmask.status.empty_paste": "⚠ Paste a config.json first.",
180
+ "unmask.status.invalid_json": "❌ Not valid JSON: {error}",
181
+ "unmask.status.success_paste": "✅ Analyzed pasted config (verdict: {verdict})",
182
+ "unmask.pasted_label": "(pasted config)",
183
+ "mode_desc.ask": "Type a free-form question. The in-browser LLM picks the right recipe and runs it.",
184
+ "mode_desc.recipe": "Pick a recipe directly and fill the form. Full manual control.",
185
+ "mode_desc.profile": "Quickest start: paste any HuggingFace model id, click Profile. See all 5 recipes scored in seconds.",
186
+ "mode_desc.compare": "Pick 2-3 candidate models + one recipe. See verdicts side-by-side in a comparison table.",
187
+ "mode_desc.inspector": "Paste a config.json directly. Useful for private/in-development models not on HF Hub.",
188
+ "mode_desc.diagnose": "Build the diagnose_model.py CLI command to MEASURE γ_obs on real GPU. Browser predicts; CLI measures.",
189
+ "mode_desc.phase": "γ × θ scatter of the paper's empirical panel. Hover a dot for details, click to load into Diagnose / Recipe forms.",
190
+ "mode_desc.unmask": "Detects whether max_position_embeddings is misleading (SWA / YaRN / RoPE-scaling). Paste a model id, get a 1-line verdict.",
191
+ "profile.preset_loaded": "✅ Loaded preset for <strong>{id}</strong>. Form pre-filled. (Click 📥 Fetch to override with the latest config from HF Hub.)",
192
  "share.import_desc": "Got a JSON file from someone else's TAF analysis? Load it here to see the verdict + chain locally. Same view as if you'd run it yourself.",
193
  "share.import_btn": "📂 Load shared JSON",
194
  "synthesis.system": "You are a precise transformer LLM diagnostic assistant. Given pre-computed TAF formula results, write a clear plain-English summary in 4-6 sentences. Cite the section number (§X.Y) for each number you mention. Always give a concrete recommendation. Do NOT invent numbers.",
 
281
  "common.no": "No",
282
 
283
  // Mode tooltips
284
+ "modes.tip": "<strong>Eight ways to use the tool</strong>.<br><strong>📇 Profile</strong>: paste a model id → 5-recipe TAF Card.<br><strong>🆚 Compare</strong>: 2-3 models side-by-side on one recipe.<br><strong>🔍 Inspect config</strong>: paste raw config.json → full Profile.<br><strong>💬 Ask</strong>: free-form question, browser LLM picks the recipe.<br><strong>📋 Recipe</strong>: manual selection with full form control.<br><strong>🩺 Diagnose CLI</strong>: generate Python command for local γ measurement.<br><strong>📊 Phase diagram</strong>: 23-model panel on (log θ, γ) plane.<br><strong>🪟 Unmask</strong>: detect misleading max_position_embeddings (SWA / YaRN / RoPE-scaling).",
285
  "profile.tip": "<strong>One-click full diagnosis</strong>. Paste any HF model id (or pick preset). Tool runs all 5 recipes (long-context, KV-compression, custom-vs-API, budget, hardware) and produces a single <strong>TAF Card</strong> with verdict per dimension + key numbers + architecture classification.<br><br><strong>Use case</strong>: \"I'm evaluating Qwen2.5-32B for production — what's its full viability profile?\" → paste id → Profile → done.",
286
  "compare.tip": "<strong>Same recipe, multiple models</strong>. Pick 2-3 candidate models and one recipe. See verdicts in a single comparison table.<br><br><strong>Use case</strong>: \"I need long-context retrieval at 16K — which is best: Llama-3-8B, Mistral-7B, or Qwen-7B?\" → pick 3 + X-2 + 16K → see winner.",
287
 
 
738
  "inv.export.formats": "<strong>JSON · Markdown · LaTeX</strong> (listo para paper)",
739
  "inv.export.share": "Link reproducible (estado codificado en URL)",
740
  "inv.export.registry": "Envía al registro comunitario en GitHub",
741
+ "arch.summary": "Arquitecturas soportadas",
742
  "arch.anyhf": "✓ Cualquier modelo público de HuggingFace",
743
  "tooltip.mha": "Multi-Head Attention: cada posición atiende mediante varios heads paralelos a la vez.",
744
  "tooltip.gqa": "Grouped Query Attention: las queries comparten menos keys/values que heads (ahorra memoria pero empuja γ hacia Hagedorn).",
 
746
  "tooltip.abspe": "Absolute Position Embeddings: cada posición tiene un vector fijo aprendido sumado al embedding del token.",
747
  "tooltip.swa": "Sliding Window Attention: cada token solo atiende dentro de una ventana local fija (Mistral, gemma-2 lo usan).",
748
  "tooltip.ssm": "State Space Model: capa de secuencia que mantiene estado interno en lugar de atención (Mamba, Jamba lo usan).",
749
+
750
+ // v0.7.0 — anti-bullshit pack #1: SWA / RoPE-scaling unmasker
751
+ "modes.unmask": "🪟 Desenmascarar",
752
+ "unmask.title": "🪟 Desenmascarador de contexto",
753
+ "unmask.tip": "Pega un id de modelo HuggingFace (o config.json crudo). La herramienta detecta sliding-window attention, RoPE scaling (YaRN/linear/dynamic NTK), y GQA — todo lo que hace que <code>max_position_embeddings</code> sea mayor que el contexto efectivo real. Mistral-7B-v0.1 es el ejemplo canónico: declara 32k, atiende dentro de ~4-8k.",
754
+ "unmask.desc": "<strong>¿Estás a punto de gastar dinero en un modelo que en realidad no atiende tan lejos?</strong> Pega un id y descúbrelo en 1 segundo. Sin GPU, sin inferencia — solo aritmética sobre config.json.",
755
+ "unmask.id_label": "ID modelo HF:",
756
+ "unmask.fetch_btn": "🔍 Desenmascarar",
757
+ "unmask.paste_summary": "O pega config.json crudo (modelos privados / en desarrollo)",
758
+ "unmask.paste_btn": "🔍 Desenmascarar config pegado",
759
+ "unmask.label.declared": "Contexto declarado",
760
+ "unmask.label.effective": "Efectivo (estimado)",
761
+ "unmask.label.ratio": "Ratio",
762
+ "unmask.section.flags": "Banderas de arquitectura",
763
+ "unmask.section.warnings": "Avisos",
764
+ "unmask.section.reco": "Recomendación",
765
+ "unmask.flag.swa": "SWA",
766
+ "unmask.flag.rope": "RoPE scaling",
767
+ "unmask.flag.gqa": "GQA",
768
+ "unmask.flag.layers": "Capas",
769
+ "unmask.flag.dhead": "d_head",
770
+ "unmask.flag.theta": "RoPE θ",
771
+ "unmask.flag.yes": "sí",
772
+ "unmask.flag.no": "no",
773
+ "unmask.flag.full_mha": "no (MHA completo, {n} heads)",
774
+ "unmask.verdict.honest": "✅ HONESTO",
775
+ "unmask.verdict.inflated": "⚠ INFLADO",
776
+ "unmask.verdict.severely_inflated": "❌ GRAVEMENTE INFLADO",
777
+ "unmask.verdict.yarn_extended": "⚠ YARN-EXTENDIDO",
778
+ "unmask.verdict.unknown": "❓ DESCONOCIDO",
779
+ "unmask.warn.swa_window": "Ventana SWA: {window} tokens — cada capa solo atiende dentro de esta ventana.",
780
+ "unmask.warn.multihop": "Estimación multi-hop: ~{multiHop} tokens (conservador: ventana × {factor}).",
781
+ "unmask.warn.yarn": "RoPE scaling ({type}) extiende contexto {factor}× desde ~{original} hasta {declared} tokens.",
782
+ "unmask.warn.yarn_advice": "Contexto RoPE-extendido — verifica el comportamiento de γ a la longitud declarada con el diagnóstico γ_check.",
783
+ "unmask.warn.gqa_small_dhead": "head dim pequeño ({d_head}) + GQA: probable compresión de KV cache a contexto largo (γ empujado hacia Hagedorn).",
784
+ "unmask.reco.honest": "Modelo de atención completa estándar. Contexto efectivo coincide con declarado ({declared} tokens).",
785
+ "unmask.reco.inflated": "Efectivo ~{effective} tokens vía SWA. Usa γ_check para verificar el comportamiento a tu longitud objetivo.",
786
+ "unmask.reco.severely_inflated": "Trátalo como un modelo de ~{effective} tokens en la práctica. El claim de {declared} tokens solo aplica vía cadenas de atención cross-layer, que empíricamente degradan más allá de ~2× la ventana SWA.",
787
+ "unmask.reco.yarn_extended": "Contexto RoPE-extendido. Corre un benchmark long-context (NIAH a 8k / 16k / 32k / full) para confirmar que la extensión se sostiene. Usa γ_check con T_eval = {declared}.",
788
+ "unmask.reco.unknown": "No se pudo parsear el config. Verifica que la URL sea un modelo HF válido con config.json público.",
789
+ "unmask.status.empty_id": "⚠ Introduce un model id (ej. mistralai/Mistral-7B-v0.1).",
790
+ "unmask.status.fetching": "⏳ Obteniendo config.json para {modelId}...",
791
+ "unmask.status.success": "✅ Analizado {modelId} (veredicto: {verdict})",
792
+ "unmask.status.empty_paste": "⚠ Pega un config.json primero.",
793
+ "unmask.status.invalid_json": "❌ JSON inválido: {error}",
794
+ "unmask.status.success_paste": "✅ Config pegado analizado (veredicto: {verdict})",
795
+ "unmask.pasted_label": "(config pegado)",
796
+ "mode_desc.ask": "Escribe una pregunta libre. El LLM en el navegador elige la receta correcta y la ejecuta.",
797
+ "mode_desc.recipe": "Selecciona una receta directamente y rellena el formulario. Control manual completo.",
798
+ "mode_desc.profile": "Inicio más rápido: pega cualquier model id de HuggingFace, click Profile. Mira las 5 recetas en segundos.",
799
+ "mode_desc.compare": "Elige 2-3 modelos candidatos + una receta. Ve veredictos lado a lado en tabla.",
800
+ "mode_desc.inspector": "Pega un config.json directamente. Útil para modelos privados / en desarrollo no en HF Hub.",
801
+ "mode_desc.diagnose": "Construye el comando CLI diagnose_model.py para MEDIR γ_obs en GPU real. El navegador predice; el CLI mide.",
802
+ "mode_desc.phase": "Scatter γ × θ del panel empírico del paper. Hover sobre puntos para detalles, click para cargar en Diagnose / Recipe.",
803
+ "mode_desc.unmask": "Detecta si max_position_embeddings es engañoso (SWA / YaRN / RoPE-scaling). Pega un model id, obtén un veredicto en 1 línea.",
804
+ "profile.preset_loaded": "✅ Preset cargado para <strong>{id}</strong>. Formulario pre-rellenado. (Click 📥 Fetch para sobreescribir con el último config de HF Hub.)",
805
  "share.import_desc": "¿Tienes un fichero JSON del análisis TAF de alguien? Cárgalo aquí para ver el veredicto + cadena localmente. La misma vista que si lo hubieras ejecutado tú.",
806
  "share.import_btn": "📂 Cargar JSON compartido",
807
  "synthesis.system": "Eres un asistente de diagnóstico preciso para LLMs transformer. Dados resultados de fórmulas TAF pre-calculados, escribe un resumen claro en español de 4-6 frases. Cita el número de sección (§X.Y) para cada número que menciones. Da siempre una recomendación concreta. NO inventes números.",
 
894
  "common.no": "No",
895
 
896
  // Tooltips de modos
897
+ "modes.tip": "<strong>Ocho formas de usar la herramienta</strong>.<br><strong>📇 Perfil</strong>: pega un id → TAF Card de 5 recetas.<br><strong>🆚 Comparar</strong>: 2-3 modelos lado a lado en una receta.<br><strong>🔍 Inspeccionar config</strong>: pega config.json crudo → Perfil completo.<br><strong>💬 Pregunta</strong>: pregunta libre, el LLM del navegador elige la receta.<br><strong>📋 Receta</strong>: selección manual con control total del formulario.<br><strong>🩺 Diagnóstico CLI</strong>: genera comando Python para medir γ localmente.<br><strong>📊 Diagrama de fase</strong>: panel de 23 modelos en plano (log θ, γ).<br><strong>🪟 Desenmascarar</strong>: detecta max_position_embeddings engañoso (SWA / YaRN / RoPE-scaling).",
898
  "profile.tip": "<strong>Diagnóstico completo en un click</strong>. Pega cualquier id de modelo HF (o elige preset). La herramienta ejecuta las 5 recetas (contexto largo, compresión KV, custom vs API, presupuesto, hardware) y produce una única <strong>TAF Card</strong> con veredicto por dimensión + números clave + clasificación arquitectónica.<br><br><strong>Caso de uso</strong>: \"Estoy evaluando Qwen2.5-32B para producción — ¿cuál es su perfil completo de viabilidad?\" → pega id → Perfilar → listo.",
899
  "compare.tip": "<strong>Misma receta, múltiples modelos</strong>. Elige 2-3 modelos candidatos y una receta. Ve los veredictos en una única tabla comparativa.<br><br><strong>Caso de uso</strong>: \"Necesito recuperación de contexto largo a 16K — ¿cuál es mejor: Llama-3-8B, Mistral-7B o Qwen-7B?\" → elige 3 + X-2 + 16K → ve el ganador.",
900
 
 
1215
  "inv.export.formats": "<strong>JSON · Markdown · LaTeX</strong> (prêt pour papier)",
1216
  "inv.export.share": "Lien reproductible (état encodé dans l'URL)",
1217
  "inv.export.registry": "Soumettre au registre communautaire sur GitHub",
1218
+ "arch.summary": "Architectures prises en charge",
1219
  "arch.anyhf": "✓ Tout modèle public HuggingFace",
1220
  "tooltip.mha": "Multi-Head Attention : chaque position attend via plusieurs têtes parallèles à la fois.",
1221
  "tooltip.gqa": "Grouped Query Attention : les queries partagent moins de keys/values que de heads (économise mémoire mais pousse γ vers Hagedorn).",
 
1223
  "tooltip.abspe": "Absolute Position Embeddings : chaque position a un vecteur fixe appris ajouté au token.",
1224
  "tooltip.swa": "Sliding Window Attention : chaque token n'attend que dans une fenêtre locale fixe (Mistral, gemma-2 l'utilisent).",
1225
  "tooltip.ssm": "State Space Model : couche de séquence qui maintient un état interne au lieu d'attention (Mamba, Jamba l'utilisent).",
1226
+
1227
+ // v0.7.0 — anti-bullshit pack #1: SWA / RoPE-scaling unmasker
1228
+ "modes.unmask": "🪟 Démasquer",
1229
+ "unmask.title": "🪟 Démasqueur de contexte",
1230
+ "unmask.tip": "Collez un id de modèle HuggingFace (ou config.json brut). L'outil détecte sliding-window attention, RoPE scaling (YaRN/linear/dynamic NTK), et GQA — tout ce qui rend <code>max_position_embeddings</code> plus grand que le contexte effectif réel. Mistral-7B-v0.1 est l'exemple canonique : déclare 32k, attend dans ~4-8k.",
1231
+ "unmask.desc": "<strong>Êtes-vous sur le point de dépenser de l'argent sur un modèle qui n'attend pas vraiment aussi loin ?</strong> Collez un id et découvrez-le en 1 seconde. Sans GPU, sans inférence — juste de l'arithmétique sur config.json.",
1232
+ "unmask.id_label": "ID modèle HF :",
1233
+ "unmask.fetch_btn": "🔍 Démasquer",
1234
+ "unmask.paste_summary": "Ou collez config.json brut (modèles privés / en dev)",
1235
+ "unmask.paste_btn": "🔍 Démasquer config collé",
1236
+ "unmask.label.declared": "Contexte déclaré",
1237
+ "unmask.label.effective": "Effectif (estimé)",
1238
+ "unmask.label.ratio": "Ratio",
1239
+ "unmask.section.flags": "Drapeaux d'architecture",
1240
+ "unmask.section.warnings": "Avertissements",
1241
+ "unmask.section.reco": "Recommandation",
1242
+ "unmask.flag.swa": "SWA",
1243
+ "unmask.flag.rope": "RoPE scaling",
1244
+ "unmask.flag.gqa": "GQA",
1245
+ "unmask.flag.layers": "Couches",
1246
+ "unmask.flag.dhead": "d_head",
1247
+ "unmask.flag.theta": "RoPE θ",
1248
+ "unmask.flag.yes": "oui",
1249
+ "unmask.flag.no": "non",
1250
+ "unmask.flag.full_mha": "non (MHA complet, {n} heads)",
1251
+ "unmask.verdict.honest": "✅ HONNÊTE",
1252
+ "unmask.verdict.inflated": "⚠ GONFLÉ",
1253
+ "unmask.verdict.severely_inflated": "❌ GRAVEMENT GONFLÉ",
1254
+ "unmask.verdict.yarn_extended": "⚠ YARN-ÉTENDU",
1255
+ "unmask.verdict.unknown": "❓ INCONNU",
1256
+ "unmask.warn.swa_window": "Fenêtre SWA : {window} tokens — chaque couche n'attend que dans cette fenêtre.",
1257
+ "unmask.warn.multihop": "Estimation multi-hop : ~{multiHop} tokens (conservateur : fenêtre × {factor}).",
1258
+ "unmask.warn.yarn": "RoPE scaling ({type}) étend le contexte {factor}× de ~{original} à {declared} tokens.",
1259
+ "unmask.warn.yarn_advice": "Contexte RoPE-étendu — vérifiez le comportement de γ à la longueur déclarée avec le diagnostic γ_check.",
1260
+ "unmask.warn.gqa_small_dhead": "Petite head dim ({d_head}) + GQA : compression de KV cache probable en contexte long (γ poussé vers Hagedorn).",
1261
+ "unmask.reco.honest": "Modèle d'attention complète standard. Contexte effectif correspond au déclaré ({declared} tokens).",
1262
+ "unmask.reco.inflated": "Effectif ~{effective} tokens via SWA. Utilisez γ_check pour vérifier le comportement à votre longueur cible.",
1263
+ "unmask.reco.severely_inflated": "Traitez-le comme un modèle de ~{effective} tokens en pratique. Le claim de {declared} tokens ne s'applique que via des chaînes d'attention cross-layer, qui dégradent empiriquement au-delà de ~2× la fenêtre SWA.",
1264
+ "unmask.reco.yarn_extended": "Contexte RoPE-étendu. Lancez un benchmark long-context (NIAH à 8k / 16k / 32k / full) pour confirmer que l'extension tient. Utilisez γ_check avec T_eval = {declared}.",
1265
+ "unmask.reco.unknown": "Impossible de parser le config. Vérifiez que l'URL est un modèle HF valide avec config.json public.",
1266
+ "unmask.status.empty_id": "⚠ Saisissez un model id (ex. mistralai/Mistral-7B-v0.1).",
1267
+ "unmask.status.fetching": "⏳ Récupération config.json pour {modelId}...",
1268
+ "unmask.status.success": "✅ {modelId} analysé (verdict : {verdict})",
1269
+ "unmask.status.empty_paste": "⚠ Collez d'abord un config.json.",
1270
+ "unmask.status.invalid_json": "❌ JSON invalide : {error}",
1271
+ "unmask.status.success_paste": "✅ Config collé analysé (verdict : {verdict})",
1272
+ "unmask.pasted_label": "(config collé)",
1273
+ "mode_desc.ask": "Tapez une question libre. Le LLM dans le navigateur choisit la recette et l'exécute.",
1274
+ "mode_desc.recipe": "Sélectionnez une recette directement et remplissez le formulaire. Contrôle manuel complet.",
1275
+ "mode_desc.profile": "Démarrage le plus rapide : collez n'importe quel model id HuggingFace, cliquez Profile. Voyez les 5 recettes en quelques secondes.",
1276
+ "mode_desc.compare": "Choisissez 2-3 modèles candidats + une recette. Verdicts côte à côte dans un tableau.",
1277
+ "mode_desc.inspector": "Collez un config.json directement. Utile pour modèles privés / en dev non publiés sur HF Hub.",
1278
+ "mode_desc.diagnose": "Construit la commande CLI diagnose_model.py pour MESURER γ_obs sur GPU réel. Le navigateur prédit ; le CLI mesure.",
1279
+ "mode_desc.phase": "Scatter γ × θ du panel empirique du papier. Survolez les points pour détails, cliquez pour charger dans Diagnose / Recipe.",
1280
+ "mode_desc.unmask": "Détecte si max_position_embeddings est trompeur (SWA / YaRN / RoPE-scaling). Collez un model id, obtenez un verdict en 1 ligne.",
1281
+ "profile.preset_loaded": "✅ Préréglage chargé pour <strong>{id}</strong>. Formulaire pré-rempli. (Cliquez 📥 Fetch pour écraser avec le dernier config depuis HF Hub.)",
1282
  "share.import_desc": "Vous avez un fichier JSON de l'analyse TAF de quelqu'un ? Chargez-le ici pour voir le verdict + la chaîne localement. La même vue que si vous l'aviez exécuté vous-même.",
1283
  "share.import_btn": "📂 Charger JSON partagé",
1284
  "synthesis.system": "Vous êtes un assistant de diagnostic précis pour LLMs transformer. Étant donné des résultats de formules TAF pré-calculés, écrivez un résumé clair en français de 4-6 phrases. Citez le numéro de section (§X.Y) pour chaque nombre mentionné. Donnez toujours une recommandation concrète. N'INVENTEZ PAS de nombres.",
 
1371
  "common.no": "Non",
1372
 
1373
  // Tooltips des modes
1374
+ "modes.tip": "<strong>Huit façons d'utiliser l'outil</strong>.<br><strong>📇 Profil</strong>: collez un id → TAF Card avec 5 recettes.<br><strong>🆚 Comparer</strong>: 2-3 modèles côte à côte sur une recette.<br><strong>🔍 Inspecter config</strong>: collez config.json brut → Profil complet.<br><strong>💬 Question</strong>: question libre, le LLM du navigateur choisit la recette.<br><strong>📋 Recette</strong>: sélection manuelle avec contrôle total du formulaire.<br><strong>🩺 Diagnostic CLI</strong>: génère commande Python pour mesurer γ localement.<br><strong>📊 Diagramme de phase</strong>: panel de 23 modèles dans le plan (log θ, γ).<br><strong>🪟 Démasquer</strong>: détecte un max_position_embeddings trompeur (SWA / YaRN / RoPE-scaling).",
1375
  "profile.tip": "<strong>Diagnostic complet en un clic</strong>. Collez n'importe quel id de modèle HF (ou choisissez préréglage). L'outil exécute les 5 recettes (contexte long, compression KV, custom vs API, budget, hardware) et produit une <strong>TAF Card</strong> unique avec verdict par dimension + nombres clés + classification architecturale.<br><br><strong>Cas d'usage</strong>: « J'évalue Qwen2.5-32B pour la production — quel est son profil complet de viabilité ? » → collez id → Profiler → fait.",
1376
  "compare.tip": "<strong>Même recette, plusieurs modèles</strong>. Choisissez 2-3 modèles candidats et une recette. Voyez les verdicts dans un seul tableau comparatif.<br><br><strong>Cas d'usage</strong>: « J'ai besoin de récupération longue contexte à 16K — quel est le meilleur : Llama-3-8B, Mistral-7B ou Qwen-7B ? » → choisissez 3 + X-2 + 16K → voyez le gagnant.",
1377
 
 
1692
  "inv.export.formats": "<strong>JSON · Markdown · LaTeX</strong>(论文级)",
1693
  "inv.export.share": "可复现的分享链接(状态编入 URL)",
1694
  "inv.export.registry": "提交到 GitHub 上的社区登记",
1695
+ "arch.summary": "支持的架构",
1696
  "arch.anyhf": "✓ 任意 HuggingFace 公开模型",
1697
  "tooltip.mha": "Multi-Head Attention:每个 token 位置同时通过多个并行 head 进行注意力计算。",
1698
  "tooltip.gqa": "Grouped Query Attention:queries 共享比 heads 更少的 keys/values(节省内存但把 γ 推向 Hagedorn)。",
 
1700
  "tooltip.abspe": "Absolute Position Embeddings:每个位置有一个固定的学习向量加到 token embedding。",
1701
  "tooltip.swa": "Sliding Window Attention:每个 token 仅在固定局部窗口内做注意力(Mistral、gemma-2 使用此机制)。",
1702
  "tooltip.ssm": "State Space Model:维护内部状态的序列层(取代注意力,Mamba、Jamba 使用此机制)。",
1703
+
1704
+ // v0.7.0 — anti-bullshit pack #1: SWA / RoPE-scaling 揭示器
1705
+ "modes.unmask": "🪟 揭示",
1706
+ "unmask.title": "🪟 上下文揭示器",
1707
+ "unmask.tip": "粘贴 HuggingFace 模型 id(或原始 config.json)。工具检测 sliding-window attention、RoPE 缩放(YaRN/linear/dynamic NTK)和 GQA — 所有使 <code>max_position_embeddings</code> 大于实际有效上下文的因素。Mistral-7B-v0.1 是经典例子:声称 32k,实际只在 ~4-8k 范围内做注意力。",
1708
+ "unmask.desc": "<strong>你即将为一个实际上注意力不到那么远的模型花钱吗?</strong> 粘贴 id,1 秒内得知。无需 GPU,无需推理 — 只是对 config.json 做算术。",
1709
+ "unmask.id_label": "HF 模型 id:",
1710
+ "unmask.fetch_btn": "🔍 揭示",
1711
+ "unmask.paste_summary": "或粘贴原始 config.json(私有 / 在研模型)",
1712
+ "unmask.paste_btn": "🔍 揭示已粘贴的 config",
1713
+ "unmask.label.declared": "声明上下文",
1714
+ "unmask.label.effective": "有效(估计)",
1715
+ "unmask.label.ratio": "比率",
1716
+ "unmask.section.flags": "架构标志",
1717
+ "unmask.section.warnings": "警告",
1718
+ "unmask.section.reco": "建议",
1719
+ "unmask.flag.swa": "SWA",
1720
+ "unmask.flag.rope": "RoPE 缩放",
1721
+ "unmask.flag.gqa": "GQA",
1722
+ "unmask.flag.layers": "层数",
1723
+ "unmask.flag.dhead": "d_head",
1724
+ "unmask.flag.theta": "RoPE θ",
1725
+ "unmask.flag.yes": "是",
1726
+ "unmask.flag.no": "否",
1727
+ "unmask.flag.full_mha": "否(完整 MHA,{n} heads)",
1728
+ "unmask.verdict.honest": "✅ 诚实",
1729
+ "unmask.verdict.inflated": "⚠ 夸大",
1730
+ "unmask.verdict.severely_inflated": "❌ 严重夸大",
1731
+ "unmask.verdict.yarn_extended": "⚠ YARN 扩展",
1732
+ "unmask.verdict.unknown": "❓ 未知",
1733
+ "unmask.warn.swa_window": "SWA 窗口:{window} tokens — 每层仅在此窗口内做注意力。",
1734
+ "unmask.warn.multihop": "多跳估计:~{multiHop} tokens(保守:窗口 × {factor})。",
1735
+ "unmask.warn.yarn": "RoPE 缩放({type})将上下文从 ~{original} 扩展 {factor}× 到 {declared} tokens。",
1736
+ "unmask.warn.yarn_advice": "RoPE 扩展的上下文 — 用 γ_check 诊断在声称的全长度验证 γ 行为。",
1737
+ "unmask.warn.gqa_small_dhead": "小 head dim({d_head})+ GQA:长上下文下 KV 缓存压缩很可能(γ 推向 Hagedorn)。",
1738
+ "unmask.reco.honest": "标准全注意力模型。有效上下文与声明一致({declared} tokens)。",
1739
+ "unmask.reco.inflated": "通过 SWA 有效 ~{effective} tokens。用 γ_check 验证你目标长度的行为。",
1740
+ "unmask.reco.severely_inflated": "实际把它当作 ~{effective} tokens 上下文模型。{declared} tokens 的声明仅通过跨层注意力链生效,经验上超过 ~2× SWA 窗口后会退化。",
1741
+ "unmask.reco.yarn_extended": "RoPE 扩展上下文。运行长上下文 benchmark(NIAH 在 8k / 16k / 32k / 全长度)以确认扩展是否成立。用 γ_check 设 T_eval = {declared}。",
1742
+ "unmask.reco.unknown": "无法解析 config。验证 URL 是带公开 config.json 的有效 HF 模型。",
1743
+ "unmask.status.empty_id": "⚠ 输入一个 model id(例如 mistralai/Mistral-7B-v0.1)。",
1744
+ "unmask.status.fetching": "⏳ 正在获取 {modelId} 的 config.json...",
1745
+ "unmask.status.success": "✅ 已分析 {modelId}(判定:{verdict})",
1746
+ "unmask.status.empty_paste": "⚠ 请先粘贴 config.json。",
1747
+ "unmask.status.invalid_json": "❌ JSON 无效:{error}",
1748
+ "unmask.status.success_paste": "✅ 已分析粘贴的 config(判定:{verdict})",
1749
+ "unmask.pasted_label": "(已粘贴 config)",
1750
+ "mode_desc.ask": "输入自由问题。浏览器内的 LLM 选择正确的 recipe 并运行。",
1751
+ "mode_desc.recipe": "直接选择一个 recipe 并填表。完整手动控制。",
1752
+ "mode_desc.profile": "最快开始:粘贴任意 HuggingFace model id,点击 Profile。几秒内看到 5 个 recipe。",
1753
+ "mode_desc.compare": "选择 2-3 个候选模型 + 一个 recipe。在表格中并排查看判定。",
1754
+ "mode_desc.inspector": "直接粘贴 config.json。适用于未发布 HF Hub 的私有 / 在研模型。",
1755
+ "mode_desc.diagnose": "构建 diagnose_model.py 的 CLI 命令,在真实 GPU 上测量 γ_obs。浏览器预测;CLI 测量。",
1756
+ "mode_desc.phase": "论文经验面板的 γ × θ 散点图。悬停点查看详情,点击加载到 Diagnose / Recipe 表单。",
1757
+ "mode_desc.unmask": "检测 max_position_embeddings 是否误导(SWA / YaRN / RoPE 缩放)。粘贴 model id,1 行判定。",
1758
+ "profile.preset_loaded": "✅ 已为 <strong>{id}</strong> 加载预设。表单已预填。(点击 📥 Fetch 用 HF Hub 最新 config 覆盖。)",
1759
  "share.import_desc": "有他人 TAF 分析的 JSON 文件? 在这里加载以本地查看判定 + 链。与您自己运行的视图相同。",
1760
  "share.import_btn": "📂 加载共享的 JSON",
1761
  "synthesis.system": "您是 transformer LLM 的精确诊断助手。给定预先计算的 TAF 公式结果,用 4-6 句中文写出清晰的摘要。为每个提到的数字引用章节号 (§X.Y)。始终给出具体建议。不要编造数字。",
 
1848
  "common.no": "否",
1849
 
1850
  // 模式提示
1851
+ "modes.tip": "<strong>种使用方式</strong>。<br><strong>📇 画像</strong>: 粘贴模型 id → 5 个配方的 TAF 卡。<br><strong>🆚 比较</strong>: 2-3 个模型在一个配方上并排比较。<br><strong>🔍 检查 config</strong>: 粘贴原始 config.json → 完整画像。<br><strong>💬 提问</strong>: 自由形式问题,浏览器 LLM 选择配方。<br><strong>📋 配方</strong>: 手动选择,完全控制表单。<br><strong>🩺 CLI 诊断</strong>: 生成 Python 命令在本地测量 γ。<br><strong>📊 相图</strong>: 23 个面板模型在 (log θ, γ) 平面上。<br><strong>🪟 揭示</strong>: 检测误导的 max_position_embeddings(SWA / YaRN / RoPE 缩放)。",
1852
  "profile.tip": "<strong>一键完整诊断</strong>。粘贴任意 HF 模型 id (或选择预设)。工具运行所有 5 个配方 (长上下文、KV 压缩、自定义 vs API、预算、硬件),生成单个 <strong>TAF 卡</strong>,显示每个维度的判定 + 关键数字 + 架构分类。<br><br><strong>用例</strong>: \"我正在为生产评估 Qwen2.5-32B — 它的完整可行性概况是什么?\" → 粘贴 id → 画像 → 完成。",
1853
  "compare.tip": "<strong>同一配方,多个模型</strong>。选择 2-3 个候选模型和一个配方。在单个比较表中查看判定。<br><br><strong>用例</strong>: \"我需要在 16K 进行长上下文检索 — 哪个最好: Llama-3-8B、Mistral-7B 或 Qwen-7B?\" → 选择 3 个 + X-2 + 16K → 看赢家。",
1854
 
js/main.js CHANGED
@@ -11,6 +11,7 @@ import { initI18n, setLang, t } from "./i18n.js";
11
  import { initPhaseDiagram } from "./phase_diagram.js";
12
  import { gammaCheckAll, REGIME_META } from "./gamma_check.js";
13
  import { loadLeanManifest, badgeHtml, badgesForUiBinding, renderTheoremTable, getManifest } from "./lean_badges.js";
 
14
 
15
  const TAF_BROWSER_URL = "python/taf_browser.py";
16
  const ENABLE_WEBLLM = true;
@@ -137,6 +138,38 @@ function enableUI() {
137
 
138
  function setStatus(msg) { $("status").textContent = msg; }
139
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
  // ════════════════════════════════════════════════════════════════════
141
  // Mode toggle
142
  // ════════════════════════════════════════════════════════════════════
@@ -153,41 +186,20 @@ document.querySelectorAll(".mode-btn").forEach(btn => {
153
  // Hide all mode sections
154
  ["ask-section", "recipe-section", "form-section",
155
  "profile-section", "compare-section", "inspector-section",
156
- "diagnose-section", "phase-section"].forEach(id => {
157
  const el = $(id);
158
  if (el) el.style.display = "none";
159
  });
160
  // Show selected
161
- if (mode === "ask") {
162
- $("ask-section").style.display = "";
163
- $("mode-desc").textContent =
164
- "Type a free-form question. The in-browser LLM picks the right recipe and runs it.";
165
- } else if (mode === "recipe") {
166
- $("recipe-section").style.display = "";
167
- $("mode-desc").textContent =
168
- "Pick a recipe directly and fill the form. Full manual control.";
169
- } else if (mode === "profile") {
170
- $("profile-section").style.display = "";
171
- $("mode-desc").textContent =
172
- "Quickest start: paste any HuggingFace model id, click Profile. See all 5 recipes scored in seconds.";
173
- } else if (mode === "compare") {
174
- $("compare-section").style.display = "";
175
- $("mode-desc").textContent =
176
- "Pick 2-3 candidate models + one recipe. See verdicts side-by-side in a comparison table.";
177
- } else if (mode === "inspector") {
178
- $("inspector-section").style.display = "";
179
- $("mode-desc").textContent =
180
- "Paste a config.json directly. Useful for private/in-development models not on HF Hub.";
181
- } else if (mode === "diagnose") {
182
- $("diagnose-section").style.display = "";
183
- $("mode-desc").textContent =
184
- "Build the diagnose_model.py CLI command to MEASURE γ_obs on real GPU. Browser predicts; CLI measures.";
185
- } else if (mode === "phase") {
186
- $("phase-section").style.display = "";
187
- $("mode-desc").textContent =
188
- "γ × θ scatter of the paper's empirical panel. Hover a dot for details, click to load into Diagnose / Recipe forms.";
189
- initPhaseDiagram();
190
- }
191
  });
192
  });
193
 
@@ -353,8 +365,14 @@ function getRecipeDefaults(recipeId) {
353
  // ════════════════════════════════════════════════════════════════════
354
  $("preset").addEventListener("change", (e) => {
355
  if (!e.target.value) return;
356
- state.lastModelId = e.target.value; // remember for filename/hash
357
- const proxy = state.pyodide.runPython(`get_preset(${JSON.stringify(e.target.value)})`);
 
 
 
 
 
 
358
  const preset = proxy.toJs ? proxy.toJs({ dict_converter: Object.fromEntries }) : proxy;
359
  if (!preset || Object.keys(preset).length === 0) return;
360
  fillRecipeForm(preset);
@@ -417,6 +435,152 @@ $("hf-fetch-btn").addEventListener("click", async () => {
417
  }
418
  });
419
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
420
  function configToPreset(cfg, modelId) {
421
  const n_attn = cfg.num_attention_heads || cfg.n_head || 0;
422
  const n_kv = cfg.num_key_value_heads || cfg.num_attention_heads || cfg.n_head || 0;
@@ -988,8 +1152,18 @@ function relativeTime(d) {
988
  // ════════════════════════════════════════════════════════════════════
989
  $("profile-preset").addEventListener("change", (e) => {
990
  if (!e.target.value) return;
991
- state.lastModelId = e.target.value; // remember for filename/hash
992
- const proxy = state.pyodide.runPython(`get_preset(${JSON.stringify(e.target.value)})`);
 
 
 
 
 
 
 
 
 
 
993
  const p = proxy.toJs ? proxy.toJs({ dict_converter: Object.fromEntries }) : proxy;
994
  if (!p || Object.keys(p).length === 0) return;
995
  $("profile-theta").value = p.theta;
 
11
  import { initPhaseDiagram } from "./phase_diagram.js";
12
  import { gammaCheckAll, REGIME_META } from "./gamma_check.js";
13
  import { loadLeanManifest, badgeHtml, badgesForUiBinding, renderTheoremTable, getManifest } from "./lean_badges.js";
14
+ import { unmaskConfig } from "./swa_unmasker.js";
15
 
16
  const TAF_BROWSER_URL = "python/taf_browser.py";
17
  const ENABLE_WEBLLM = true;
 
138
 
139
  function setStatus(msg) { $("status").textContent = msg; }
140
 
141
+ // ════════════════════════════════════════════════════════════════════
142
+ // Main-panel wrap: every <main> section gets a foldable details/summary
143
+ // shell at runtime so users can collapse any panel they don't need open.
144
+ // h2 is moved INTO summary so its data-i18n binding survives. Idempotent.
145
+ // ════════════════════════════════════════════════════════════════════
146
+ function wrapMainSectionsAsFoldable() {
147
+ document.querySelectorAll("main > section").forEach(section => {
148
+ if (section.id === "status-bar") return; // skip loading bar
149
+ if (section.querySelector(":scope > details.main-panel")) return; // already wrapped
150
+ const h2 = section.querySelector(":scope > h2");
151
+ if (!h2) return;
152
+
153
+ const details = document.createElement("details");
154
+ details.className = "main-panel";
155
+ details.open = true;
156
+
157
+ const summary = document.createElement("summary");
158
+ summary.className = "main-panel-title";
159
+ summary.appendChild(h2); // preserve h2 + its data-i18n + all children
160
+
161
+ details.appendChild(summary);
162
+ while (section.firstChild) details.appendChild(section.firstChild);
163
+ section.appendChild(details);
164
+ });
165
+
166
+ // Stop ⓘ tooltip clicks inside summaries from toggling the panel.
167
+ document.querySelectorAll(".main-panel > .main-panel-title .info").forEach(el => {
168
+ el.addEventListener("click", (e) => e.stopPropagation());
169
+ });
170
+ }
171
+ wrapMainSectionsAsFoldable();
172
+
173
  // ════════════════════════════════════════════════════════════════════
174
  // Mode toggle
175
  // ════════════════════════════════════════════════════════════════════
 
186
  // Hide all mode sections
187
  ["ask-section", "recipe-section", "form-section",
188
  "profile-section", "compare-section", "inspector-section",
189
+ "diagnose-section", "phase-section", "unmask-section"].forEach(id => {
190
  const el = $(id);
191
  if (el) el.style.display = "none";
192
  });
193
  // Show selected
194
+ const sectionMap = {
195
+ ask: "ask-section", recipe: "recipe-section", profile: "profile-section",
196
+ compare: "compare-section", inspector: "inspector-section",
197
+ diagnose: "diagnose-section", phase: "phase-section", unmask: "unmask-section",
198
+ };
199
+ const sectionId = sectionMap[mode];
200
+ if (sectionId) $(sectionId).style.display = "";
201
+ $("mode-desc").textContent = t(`mode_desc.${mode}`) || "";
202
+ if (mode === "phase") initPhaseDiagram();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
  });
204
  });
205
 
 
365
  // ════════════════════════════════════════════════════════════════════
366
  $("preset").addEventListener("change", (e) => {
367
  if (!e.target.value) return;
368
+ const modelId = e.target.value;
369
+ state.lastModelId = modelId; // remember for filename/hash
370
+ // Mirror behavior with profile-preset: also fill HF id input if present.
371
+ if ($("hf-id")) {
372
+ $("hf-id").value = modelId;
373
+ if ($("hf-status")) $("hf-status").textContent = tFmt("profile.preset_loaded", { id: modelId });
374
+ }
375
+ const proxy = state.pyodide.runPython(`get_preset(${JSON.stringify(modelId)})`);
376
  const preset = proxy.toJs ? proxy.toJs({ dict_converter: Object.fromEntries }) : proxy;
377
  if (!preset || Object.keys(preset).length === 0) return;
378
  fillRecipeForm(preset);
 
435
  }
436
  });
437
 
438
+ // ════════════════════════════════════════════════════════════════════
439
+ // 🪟 Unmask mode (v0.7.0 anti-bullshit pack #1)
440
+ // ════════════════════════════════════════════════════════════════════
441
+
442
+ // Tiny string-template helper: t(key) with {placeholder} substitution.
443
+ // Falls back to the raw key when the i18n entry is missing so dev sees the gap.
444
+ function tFmt(key, params = {}) {
445
+ let s = t(key) || key;
446
+ for (const [k, v] of Object.entries(params)) {
447
+ const fmtVal = v === null || v === undefined ? "—"
448
+ : (typeof v === "number" ? v.toLocaleString() : String(v));
449
+ s = s.replace(new RegExp(`\\{${k}\\}`, "g"), fmtVal);
450
+ }
451
+ return s;
452
+ }
453
+
454
+ const VERDICT_COLOR = {
455
+ honest: "#3fb950",
456
+ inflated: "#f1c40f",
457
+ severely_inflated: "#f85149",
458
+ yarn_extended: "#f1c40f",
459
+ unknown: "#8b949e",
460
+ };
461
+
462
+ function renderUnmaskCard(result, modelId = "") {
463
+ const color = VERDICT_COLOR[result.verdict] || VERDICT_COLOR.unknown;
464
+ const ratioPct = (result.ratio * 100).toFixed(1);
465
+ const f = result.flags;
466
+ const fmtN = (x) => x === null || x === undefined ? "—" : Number(x).toLocaleString();
467
+ const escapeHtml = (s) => String(s).replace(/[&<>"']/g, c =>
468
+ ({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"}[c]));
469
+
470
+ const verdictLabel = t(`unmask.verdict.${result.verdict}`) || result.verdict;
471
+ const labelDeclared = t("unmask.label.declared") || "Declared context";
472
+ const labelEffective = t("unmask.label.effective") || "Effective (estimate)";
473
+ const labelRatio = t("unmask.label.ratio") || "Ratio";
474
+ const sectionFlags = t("unmask.section.flags") || "Architecture flags";
475
+ const sectionWarn = t("unmask.section.warnings")|| "Warnings";
476
+ const sectionReco = t("unmask.section.reco") || "Recommendation";
477
+
478
+ // Architecture flags row labels
479
+ const flagSwa = t("unmask.flag.swa") || "SWA";
480
+ const flagRope = t("unmask.flag.rope") || "RoPE scaling";
481
+ const flagGqa = t("unmask.flag.gqa") || "GQA";
482
+ const flagLayers = t("unmask.flag.layers") || "Layers";
483
+ const flagDhead = t("unmask.flag.dhead") || "d_head";
484
+ const flagTheta = t("unmask.flag.theta") || "RoPE θ";
485
+ const flagYes = t("unmask.flag.yes") || "yes";
486
+ const flagNo = t("unmask.flag.no") || "no";
487
+
488
+ const swaText = f.hasSWA
489
+ ? `${flagYes} (window = ${fmtN(f.swaWindow)})`
490
+ : flagNo;
491
+ const ropeText = f.hasYaRN
492
+ ? `${f.ropeScalingType} (factor = ${f.yarnFactor}, original = ${fmtN(f.yarnOriginal)})`
493
+ : flagNo;
494
+ const gqaText = f.hasGQA
495
+ ? `${flagYes} (${f.n_kv_heads} kv / ${f.n_attn_heads} attn heads)`
496
+ : (t("unmask.flag.full_mha") || "no (full MHA, {n} heads)").replace("{n}", f.n_attn_heads ?? "?");
497
+
498
+ const warningsHtml = result.warnings.length
499
+ ? `<details class="unmask-panel" open><summary class="unmask-panel-title">${sectionWarn}</summary><ul>${result.warnings.map(w =>
500
+ `<li>${tFmt("unmask.warn." + w.code, w.params)}</li>`).join("")}</ul></details>`
501
+ : "";
502
+
503
+ const recoHtml = result.recoCode
504
+ ? `<details class="unmask-panel" open><summary class="unmask-panel-title">${sectionReco}</summary><p class="unmask-reco">${tFmt("unmask.reco." + result.recoCode, result.recoParams)}</p></details>`
505
+ : "";
506
+
507
+ return `
508
+ <div class="unmask-result">
509
+ <div class="unmask-hero" style="border-color: ${color};">
510
+ <div class="unmask-verdict" style="color: ${color};">${verdictLabel}</div>
511
+ ${modelId ? `<div class="unmask-model"><code>${escapeHtml(modelId)}</code></div>` : ""}
512
+ <div class="unmask-numbers">
513
+ <div><span class="unmask-num-label">${labelDeclared}</span><span class="unmask-num-val">${fmtN(result.declaredContext)}</span></div>
514
+ <div><span class="unmask-num-label">${labelEffective}</span><span class="unmask-num-val">${fmtN(result.effectiveContext)}</span></div>
515
+ <div><span class="unmask-num-label">${labelRatio}</span><span class="unmask-num-val">${ratioPct}%</span></div>
516
+ </div>
517
+ </div>
518
+
519
+ <div class="unmask-details">
520
+ <details class="unmask-panel" open>
521
+ <summary class="unmask-panel-title">${sectionFlags}</summary>
522
+ <ul>
523
+ <li><strong>${flagSwa}:</strong> ${swaText}</li>
524
+ <li><strong>${flagRope}:</strong> ${ropeText}</li>
525
+ <li><strong>${flagGqa}:</strong> ${gqaText}</li>
526
+ <li><strong>${flagLayers}:</strong> ${fmtN(f.n_layers)} · <strong>${flagDhead}:</strong> ${fmtN(f.d_head)} · <strong>${flagTheta}:</strong> ${fmtN(f.rope_theta)}</li>
527
+ </ul>
528
+ </details>
529
+ ${warningsHtml}
530
+ ${recoHtml}
531
+ </div>
532
+ </div>
533
+ `;
534
+ }
535
+
536
+ async function runUnmaskFromId() {
537
+ const modelId = ($("unmask-id").value || "").trim();
538
+ if (!modelId) {
539
+ $("unmask-status").textContent = t("unmask.status.empty_id") || "⚠ Enter a model id.";
540
+ return;
541
+ }
542
+ $("unmask-status").textContent = tFmt("unmask.status.fetching", { modelId });
543
+ $("unmask-fetch-btn").disabled = true;
544
+ try {
545
+ const cfg = await fetchHfConfig(modelId);
546
+ const result = unmaskConfig(cfg);
547
+ $("unmask-output").innerHTML = renderUnmaskCard(result, modelId);
548
+ const verdictLocalized = t(`unmask.verdict.${result.verdict}`) || result.verdict;
549
+ $("unmask-status").textContent = tFmt("unmask.status.success", { modelId, verdict: verdictLocalized });
550
+ } catch (err) {
551
+ $("unmask-status").textContent = `❌ ${err.message}`;
552
+ $("unmask-output").innerHTML = "";
553
+ } finally {
554
+ $("unmask-fetch-btn").disabled = false;
555
+ }
556
+ }
557
+
558
+ function runUnmaskFromPaste() {
559
+ const raw = ($("unmask-paste").value || "").trim();
560
+ if (!raw) {
561
+ $("unmask-status").textContent = t("unmask.status.empty_paste") || "⚠ Paste a config.json first.";
562
+ return;
563
+ }
564
+ let cfg;
565
+ try {
566
+ cfg = JSON.parse(raw);
567
+ } catch (e) {
568
+ $("unmask-status").textContent = tFmt("unmask.status.invalid_json", { error: e.message });
569
+ return;
570
+ }
571
+ const result = unmaskConfig(cfg);
572
+ const pastedLabel = t("unmask.pasted_label") || "(pasted config)";
573
+ $("unmask-output").innerHTML = renderUnmaskCard(result, pastedLabel);
574
+ const verdictLocalized = t(`unmask.verdict.${result.verdict}`) || result.verdict;
575
+ $("unmask-status").textContent = tFmt("unmask.status.success_paste", { verdict: verdictLocalized });
576
+ }
577
+
578
+ $("unmask-fetch-btn")?.addEventListener("click", runUnmaskFromId);
579
+ $("unmask-paste-btn")?.addEventListener("click", runUnmaskFromPaste);
580
+ $("unmask-id")?.addEventListener("keydown", (e) => {
581
+ if (e.key === "Enter") { e.preventDefault(); runUnmaskFromId(); }
582
+ });
583
+
584
  function configToPreset(cfg, modelId) {
585
  const n_attn = cfg.num_attention_heads || cfg.n_head || 0;
586
  const n_kv = cfg.num_key_value_heads || cfg.num_attention_heads || cfg.n_head || 0;
 
1152
  // ════════════════════════════════════════════════════════════════════
1153
  $("profile-preset").addEventListener("change", (e) => {
1154
  if (!e.target.value) return;
1155
+ const modelId = e.target.value;
1156
+ state.lastModelId = modelId; // remember for filename/hash
1157
+ // Preset keys ARE valid HF model ids (e.g. "meta-llama/Llama-3.2-1B"). Auto-fill
1158
+ // the HF id input so the user can also click 📥 Fetch to refresh from HF Hub
1159
+ // without retyping. Status hint clarifies the dual source of truth.
1160
+ if ($("profile-hf-id")) {
1161
+ $("profile-hf-id").value = modelId;
1162
+ if ($("profile-hf-status")) {
1163
+ $("profile-hf-status").textContent = tFmt("profile.preset_loaded", { id: modelId });
1164
+ }
1165
+ }
1166
+ const proxy = state.pyodide.runPython(`get_preset(${JSON.stringify(modelId)})`);
1167
  const p = proxy.toJs ? proxy.toJs({ dict_converter: Object.fromEntries }) : proxy;
1168
  if (!p || Object.keys(p).length === 0) return;
1169
  $("profile-theta").value = p.theta;
js/swa_unmasker.js ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // SWA Unmasker (v0.7.0 anti-bullshit pack #1)
2
+ // Pure logic — no human-readable strings. Returns structured warnings/reco
3
+ // codes + params; main.js does the i18n lookup so EN/ES/FR/ZH all work.
4
+
5
+ // Conservative multi-hop bound for SWA models. Empirically the effective
6
+ // "reasoning" context is roughly 2× the window, NOT window × n_layers
7
+ // (which is the theoretical upper bound but breaks down past a few hops).
8
+ const SWA_MULTIHOP_FACTOR = 2;
9
+
10
+ export function unmaskConfig(config) {
11
+ const out = {
12
+ declaredContext: config.max_position_embeddings ?? null,
13
+ effectiveContext: null,
14
+ verdict: "honest",
15
+ ratio: 1.0,
16
+ flags: {
17
+ hasSWA: false,
18
+ swaWindow: null,
19
+ hasYaRN: false,
20
+ yarnFactor: null,
21
+ yarnOriginal: null,
22
+ ropeScalingType: null,
23
+ hasGQA: false,
24
+ n_kv_heads: config.num_key_value_heads ?? config.num_attention_heads ?? null,
25
+ n_attn_heads: config.num_attention_heads ?? null,
26
+ n_layers: config.num_hidden_layers ?? null,
27
+ rope_theta: config.rope_theta ?? null,
28
+ d_head: null,
29
+ },
30
+ warnings: [], // each: { code, params }
31
+ recoCode: null,
32
+ recoParams: {},
33
+ };
34
+
35
+ if (out.flags.n_attn_heads && out.flags.n_kv_heads) {
36
+ out.flags.hasGQA = out.flags.n_kv_heads < out.flags.n_attn_heads;
37
+ }
38
+ if (config.hidden_size && out.flags.n_attn_heads) {
39
+ out.flags.d_head = config.hidden_size / out.flags.n_attn_heads;
40
+ }
41
+
42
+ // SWA: explicit sliding_window field (Mistral, Gemma-2). Some configs set
43
+ // it to null or to max_pe — treat as "no SWA" in those cases.
44
+ const sw = config.sliding_window;
45
+ if (typeof sw === "number" && sw > 0
46
+ && (!out.declaredContext || sw < out.declaredContext)) {
47
+ out.flags.hasSWA = true;
48
+ out.flags.swaWindow = sw;
49
+ }
50
+
51
+ // RoPE scaling (YaRN / linear / dynamic NTK). Only flag if factor > 1.
52
+ const rs = config.rope_scaling;
53
+ if (rs && typeof rs === "object") {
54
+ out.flags.ropeScalingType = rs.type ?? rs.rope_type ?? null;
55
+ out.flags.yarnFactor = rs.factor ?? null;
56
+ out.flags.yarnOriginal = rs.original_max_position_embeddings ?? null;
57
+ if (out.flags.ropeScalingType && out.flags.yarnFactor && out.flags.yarnFactor > 1) {
58
+ out.flags.hasYaRN = true;
59
+ }
60
+ }
61
+
62
+ // Compute verdict
63
+ if (out.flags.hasSWA) {
64
+ const multiHop = out.flags.swaWindow * SWA_MULTIHOP_FACTOR;
65
+ out.effectiveContext = Math.min(multiHop, out.declaredContext ?? multiHop);
66
+ out.ratio = out.declaredContext ? out.effectiveContext / out.declaredContext : 1.0;
67
+ // <= 0.25 catches the canonical Mistral case (window=4096, declared=32768, ratio=0.25 exact)
68
+ out.verdict = out.ratio <= 0.25 ? "severely_inflated" : "inflated";
69
+ out.warnings.push(
70
+ { code: "swa_window", params: { window: out.flags.swaWindow } },
71
+ { code: "multihop", params: { multiHop, factor: SWA_MULTIHOP_FACTOR } },
72
+ );
73
+ out.recoCode = out.verdict;
74
+ out.recoParams = {
75
+ effective: out.effectiveContext,
76
+ declared: out.declaredContext,
77
+ };
78
+ } else if (out.flags.hasYaRN) {
79
+ out.verdict = "yarn_extended";
80
+ const orig = out.flags.yarnOriginal
81
+ ?? (out.declaredContext ? out.declaredContext / out.flags.yarnFactor : null);
82
+ out.effectiveContext = out.declaredContext;
83
+ out.ratio = 1.0;
84
+ out.warnings.push(
85
+ { code: "yarn", params: { type: out.flags.ropeScalingType, factor: out.flags.yarnFactor, original: orig ? Math.round(orig) : null, declared: out.declaredContext } },
86
+ { code: "yarn_advice", params: {} },
87
+ );
88
+ out.recoCode = "yarn_extended";
89
+ out.recoParams = { declared: out.declaredContext };
90
+ } else if (out.declaredContext) {
91
+ out.effectiveContext = out.declaredContext;
92
+ out.verdict = "honest";
93
+ out.recoCode = "honest";
94
+ out.recoParams = { declared: out.declaredContext };
95
+ } else {
96
+ out.verdict = "unknown";
97
+ out.recoCode = "unknown";
98
+ out.recoParams = {};
99
+ }
100
+
101
+ // KV-cache compression hint for small d_head + GQA — independent of verdict
102
+ if (out.flags.hasGQA && out.flags.d_head && out.flags.d_head < 64) {
103
+ out.warnings.push({ code: "gqa_small_dhead", params: { d_head: out.flags.d_head } });
104
+ }
105
+
106
+ return out;
107
+ }
style.css CHANGED
@@ -1,5 +1,130 @@
1
  /* TAF Agent — minimal clean styling */
2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  /* v0.6.2 — landing rework: quick-start strip + inventory grid + arch-supported */
4
  #quickstart-strip {
5
  margin: 1.5em auto 1em;
@@ -72,18 +197,35 @@
72
  gap: 0.8em;
73
  }
74
  .inv-card {
75
- padding: 0.9em 1em;
76
  background: #12181f;
77
  border: 1px solid rgba(255, 255, 255, 0.08);
78
  border-radius: 8px;
79
  }
80
- .inv-card h3 {
81
- margin: 0 0 0.5em;
82
  font-size: 1em;
 
83
  color: #58a6ff;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  }
 
85
  .inv-card ul {
86
- margin: 0;
87
  padding-left: 1em;
88
  font-size: 0.92em;
89
  line-height: 1.5;
 
1
  /* TAF Agent — minimal clean styling */
2
 
3
+ /* v0.7.0 — main panels foldable (every section under <main>) */
4
+ .main-panel { margin: 0; }
5
+ .main-panel > .main-panel-title {
6
+ cursor: pointer;
7
+ list-style: none;
8
+ user-select: none;
9
+ padding: 0 0 0.5em;
10
+ margin-bottom: 0.6em;
11
+ border-bottom: 1px solid rgba(255, 255, 255, 0.06);
12
+ display: flex;
13
+ align-items: baseline;
14
+ gap: 0.5em;
15
+ }
16
+ .main-panel > .main-panel-title::-webkit-details-marker { display: none; }
17
+ .main-panel > .main-panel-title::marker { content: ""; }
18
+ .main-panel > .main-panel-title::before {
19
+ content: "▼";
20
+ display: inline-block;
21
+ font-size: 0.65em;
22
+ color: #58a6ff;
23
+ margin-right: 0.3em;
24
+ transition: transform 0.15s ease;
25
+ flex-shrink: 0;
26
+ }
27
+ .main-panel:not([open]) > .main-panel-title::before { transform: rotate(-90deg); }
28
+ .main-panel > .main-panel-title:hover { background: rgba(255, 255, 255, 0.02); }
29
+ .main-panel > .main-panel-title h2 {
30
+ display: inline;
31
+ margin: 0;
32
+ vertical-align: baseline;
33
+ flex: 1;
34
+ }
35
+
36
+ /* v0.7.0 — Unmask mode (SWA + RoPE-scaling detector) */
37
+ .unmask-result {
38
+ margin-top: 0.8em;
39
+ }
40
+ .unmask-hero {
41
+ padding: 1em 1.2em;
42
+ border: 2px solid #58a6ff;
43
+ border-radius: 10px;
44
+ background: #12181f;
45
+ margin-bottom: 0.8em;
46
+ }
47
+ .unmask-verdict {
48
+ font-size: 1.6em;
49
+ font-weight: 700;
50
+ margin-bottom: 0.2em;
51
+ }
52
+ .unmask-model {
53
+ font-size: 0.92em;
54
+ opacity: 0.85;
55
+ margin-bottom: 0.6em;
56
+ }
57
+ .unmask-numbers {
58
+ display: grid;
59
+ grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
60
+ gap: 0.6em;
61
+ margin-top: 0.5em;
62
+ }
63
+ .unmask-numbers > div {
64
+ display: flex;
65
+ flex-direction: column;
66
+ padding: 0.5em 0.7em;
67
+ background: rgba(0, 0, 0, 0.25);
68
+ border-radius: 6px;
69
+ }
70
+ .unmask-num-label {
71
+ font-size: 0.78em;
72
+ opacity: 0.75;
73
+ text-transform: uppercase;
74
+ letter-spacing: 0.04em;
75
+ }
76
+ .unmask-num-val {
77
+ font-size: 1.3em;
78
+ font-weight: 600;
79
+ font-family: monospace;
80
+ margin-top: 0.15em;
81
+ }
82
+ .unmask-details {
83
+ padding: 0.8em 1em;
84
+ background: #12181f;
85
+ border: 1px solid rgba(255, 255, 255, 0.08);
86
+ border-radius: 8px;
87
+ }
88
+ .unmask-details h4,
89
+ .unmask-panel-title {
90
+ margin: 0.4em 0 0.3em;
91
+ color: #58a6ff;
92
+ font-size: 0.95em;
93
+ cursor: pointer;
94
+ list-style: none;
95
+ user-select: none;
96
+ font-weight: 600;
97
+ }
98
+ .unmask-panel-title::-webkit-details-marker { display: none; }
99
+ .unmask-panel-title::marker { content: ""; }
100
+ .unmask-panel-title::before {
101
+ content: "▼";
102
+ display: inline-block;
103
+ font-size: 0.75em;
104
+ margin-right: 0.4em;
105
+ color: #58a6ff;
106
+ transition: transform 0.15s ease;
107
+ width: 0.9em;
108
+ text-align: center;
109
+ }
110
+ .unmask-panel:not([open]) > .unmask-panel-title::before { transform: rotate(-90deg); }
111
+ .unmask-panel { margin: 0.5em 0; }
112
+ .unmask-details ul {
113
+ margin: 0.2em 0 0.6em;
114
+ padding-left: 1.2em;
115
+ font-size: 0.92em;
116
+ line-height: 1.5;
117
+ }
118
+ .unmask-reco {
119
+ margin: 0.2em 0 0.4em;
120
+ padding: 0.6em 0.8em;
121
+ background: rgba(88, 166, 255, 0.08);
122
+ border-left: 3px solid #58a6ff;
123
+ border-radius: 0 6px 6px 0;
124
+ font-size: 0.92em;
125
+ line-height: 1.5;
126
+ }
127
+
128
  /* v0.6.2 — landing rework: quick-start strip + inventory grid + arch-supported */
129
  #quickstart-strip {
130
  margin: 1.5em auto 1em;
 
197
  gap: 0.8em;
198
  }
199
  .inv-card {
200
+ padding: 0.7em 1em;
201
  background: #12181f;
202
  border: 1px solid rgba(255, 255, 255, 0.08);
203
  border-radius: 8px;
204
  }
205
+ .inv-card-title {
206
+ cursor: pointer;
207
  font-size: 1em;
208
+ font-weight: 600;
209
  color: #58a6ff;
210
+ padding: 0.2em 0;
211
+ list-style: none; /* hide native marker (Chrome, Safari) */
212
+ user-select: none;
213
+ }
214
+ .inv-card-title::-webkit-details-marker { display: none; } /* Safari */
215
+ .inv-card-title::marker { content: ""; } /* Firefox */
216
+ .inv-card-title::before {
217
+ content: "▼";
218
+ display: inline-block;
219
+ font-size: 0.75em;
220
+ margin-right: 0.4em;
221
+ color: #58a6ff;
222
+ transition: transform 0.15s ease;
223
+ width: 0.9em;
224
+ text-align: center;
225
  }
226
+ .inv-card:not([open]) > .inv-card-title::before { transform: rotate(-90deg); }
227
  .inv-card ul {
228
+ margin: 0.4em 0 0;
229
  padding-left: 1em;
230
  font-size: 0.92em;
231
  line-height: 1.5;