Basculer le menu
Changer de menu des préférences
Basculer le menu personnel
Non connecté(e)
Votre adresse IP sera visible au public si vous faites des modifications.

« MediaWiki:Common.js » : différence entre les versions

Page de l’interface de MediaWiki
Aucun résumé des modifications
Assistant Microguide 2026-10-28-1368b589 : page sans l'info demandée, refus erronés corrigés, questions-liste, vocabulaire (pen G, sero, pseu, legio...)
 
(96 versions intermédiaires par le même utilisateur non affichées)
Ligne 1 : Ligne 1 :
document.addEventListener('DOMContentLoaded', function () {
// Forcer l'ouverture des sections hidden dans citizen qui les cachent par défaut
     document.querySelectorAll('[hidden="until-found"], [hidden]').forEach(function (el) {
 
function ouvrirSections() {
    document.querySelectorAll('.citizen-section').forEach(function (el) {
        el.removeAttribute('hidden');
    });
}
 
document.addEventListener('DOMContentLoaded', ouvrirSections);
 
window.addEventListener('pageshow', function (e) {
    if (e.persisted) {
        ouvrirSections();
    }
});
 
/* Enlever le téléverser sur hamburger et empêcher les menus défilent */
 
function ouvrirSections() {
     document.querySelectorAll('.citizen-section').forEach(function (el) {
         el.removeAttribute('hidden');
         el.removeAttribute('hidden');
     });
     });
    var upload = document.getElementById('t-upload');
    if (upload) upload.remove();
}
/* Désactive les liens sur toutes les images */
$(document).ready(function() {
    $('.mw-file-description').each(function() {
        $(this).replaceWith($(this).children());
    });
});
// ============================================
// CALCULATEUR IRC — Microguide
// ============================================
if (mw.config.get('wgPageName') === 'Calculateur') {
    var style = document.createElement('style');
    style.textContent = `
        #calc-app * { box-sizing: border-box; font-family: 'Segoe UI', system-ui, sans-serif; }
        #calc-app { background: #f0f4f8; padding: 16px; border-radius: 12px; }
        .calc-header { background: linear-gradient(135deg, #1a5fa8 0%, #2980d4 100%); padding: 18px 22px; border-radius: 12px; color: white; margin-bottom: 16px; }
        .calc-header h1 { margin: 0; font-size: 20px; font-weight: 700; }
        .calc-header p { margin: 4px 0 0; font-size: 12px; opacity: 0.75; }
        .calc-card { background: white; border-radius: 12px; padding: 20px 22px; margin-bottom: 14px; box-shadow: 0 2px 8px rgba(0,0,0,0.06); border: 1px solid #e2e8f0; }
        .calc-card h2 { margin: 0 0 14px; font-size: 15px; font-weight: 700; color: #0f2d5e; }
        .calc-grid { display: grid; grid-template-columns: 1fr 1fr 1fr 1fr; gap: 10px; margin-bottom: 12px; }
        .calc-grid label { font-size: 11px; color: #64748b; font-weight: 600; display: block; margin-bottom: 4px; }
        .calc-grid input { width: 100%; padding: 8px 10px; border-radius: 8px; border: 1.5px solid #e2e8f0; font-size: 14px; color: #0f2d5e; font-weight: 600; outline: none; }
        .calc-sexe-btn { padding: 6px 18px; border-radius: 8px; border: 1.5px solid #e2e8f0; background: white; color: #64748b; font-weight: 600; cursor: pointer; font-size: 13px; margin-right: 8px; }
        .calc-sexe-btn.active { border-color: #0f2d5e; background: #0f2d5e; color: white; }
        .calc-result-box { background: #f8fafc; border-radius: 10px; padding: 14px 18px; display: flex; align-items: center; gap: 20px; flex-wrap: wrap; margin-top: 10px; }
        .calc-clcr-val { font-size: 32px; font-weight: 800; line-height: 1; }
        .calc-clcr-label { font-size: 12px; font-weight: 600; margin-top: 2px; }
        .calc-search { width: 100%; padding: 10px 14px; border-radius: 10px; border: 1.5px solid #e2e8f0; font-size: 14px; margin-bottom: 10px; outline: none; }
        .calc-cats { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 10px; }
        .calc-cat-btn { padding: 6px 14px; border-radius: 20px; border: 1.5px solid #e2e8f0; background: white; color: #475569; font-weight: 600; cursor: pointer; font-size: 12px; }
        .calc-cat-btn.active { border-color: #0f2d5e; background: #0f2d5e; color: white; }
        .calc-drugs { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
        .calc-drug-btn { padding: 6px 12px; border-radius: 8px; border: 1.5px solid #e2e8f0; background: white; color: #475569; font-weight: 500; cursor: pointer; font-size: 12px; }
        .calc-drug-btn.active { border-color: #1a9d8f; background: #e8f6f4; color: #0f7a6e; font-weight: 700; }
        .calc-table { width: 100%; border-collapse: collapse; font-size: 13px; margin-top: 10px; }
        .calc-table th { background: #f1f5f9; padding: 8px 12px; text-align: left; font-weight: 700; color: #0f2d5e; }
        .calc-table td { padding: 10px 12px; border-bottom: 1px solid #f1f5f9; }
        .calc-table tr.active-row { background: #e8f4f0 !important; outline: 2px solid #1a9d8f; }
        .calc-table tr.active-row td { color: #0a5a52; font-weight: 700; }
        .calc-badge { background: #1a9d8f; color: white; font-size: 10px; padding: 2px 6px; border-radius: 4px; margin-left: 8px; font-weight: 700; }
        .calc-note { background: #fefce8; border: 1px solid #fde68a; border-radius: 8px; padding: 10px 14px; font-size: 12px; color: #92400e; margin-bottom: 12px; }
        .calc-warning { background: #fef2f2; border: 1px solid #fecaca; border-radius: 10px; padding: 12px 16px; font-size: 11px; color: #991b1b; margin-top: 14px; }
        .calc-search-results button { display: block; width: 100%; text-align: left; padding: 8px 12px; margin-bottom: 4px; background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px; cursor: pointer; font-size: 13px; color: #0f2d5e; }
        .calc-hint { margin-top: 12px; padding: 10px 14px; background: #f0f9ff; border-radius: 8px; font-size: 12px; color: #0369a1; }
        .calc-imc-info { border-left: 1px solid #e2e8f0; padding-left: 16px; font-size: 11px; color: #d97706; }
        @media (max-width: 600px) { .calc-grid { grid-template-columns: 1fr 1fr; } }
        .vanco-header { background: linear-gradient(135deg, #7c3aed 0%, #9f67f5 100%); padding: 18px 22px; border-radius: 12px; color: white; margin-bottom: 16px; }
        .vanco-header h1 { margin: 0; font-size: 20px; font-weight: 700; }
        .vanco-header p { margin: 4px 0 0; font-size: 12px; opacity: 0.75; }
        .vanco-card { background: white; border-radius: 12px; padding: 20px 22px; margin-bottom: 14px; box-shadow: 0 2px 8px rgba(124,58,237,0.08); border: 1px solid #ede9fe; }
        .vanco-card h2 { margin: 0 0 14px; font-size: 15px; font-weight: 700; color: #5b21b6; }
        .vanco-section-title { font-size: 13px; font-weight: 700; color: #5b21b6; margin: 14px 0 8px; padding-bottom: 4px; border-bottom: 2px solid #ede9fe; }
        .vanco-table { width: 100%; border-collapse: collapse; font-size: 13px; margin-top: 8px; }
        .vanco-table th { background: #ede9fe; padding: 8px 12px; text-align: left; font-weight: 700; color: #5b21b6; }
        .vanco-table td { padding: 9px 12px; border-bottom: 1px solid #f5f3ff; }
        .vanco-table tr.vanco-active { background: #f3e8ff !important; }
        .vanco-table tr.vanco-active td { color: #6d28d9; font-weight: 700; }
        .vanco-badge { background: #7c3aed; color: white; font-size: 10px; padding: 2px 6px; border-radius: 4px; margin-left: 6px; font-weight: 700; }
        .vanco-input-row { display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 14px; }
        .vanco-input-group { flex: 1; min-width: 120px; }
        .vanco-input-group label { font-size: 11px; color: #7c3aed; font-weight: 600; display: block; margin-bottom: 4px; }
        .vanco-input-group input { width: 100%; padding: 8px 10px; border-radius: 8px; border: 1.5px solid #ede9fe; font-size: 14px; color: #4c1d95; font-weight: 600; outline: none; }
        .vanco-result-box { background: #f3e8ff; border: 2px solid #c4b5fd; border-radius: 10px; padding: 14px 18px; margin-top: 12px; display: flex; gap: 24px; flex-wrap: wrap; align-items: flex-start; }
        .vanco-result-val { font-size: 26px; font-weight: 800; color: #6d28d9; line-height: 1; }
        .vanco-warning { background: #fef2f2; border: 1px solid #fecaca; border-radius: 10px; padding: 12px 16px; font-size: 11px; color: #991b1b; margin-top: 14px; }
        .vanco-note { background: #fefce8; border: 1px solid #fde68a; border-radius: 8px; padding: 10px 14px; font-size: 12px; color: #92400e; margin-bottom: 12px; }
        .vanco-tab-btns { display: flex; gap: 8px; margin-bottom: 16px; }
        .vanco-tab-btn { padding: 7px 18px; border-radius: 8px; border: 1.5px solid #ede9fe; background: white; color: #7c3aed; font-weight: 600; cursor: pointer; font-size: 13px; }
        .vanco-tab-btn.active { background: #7c3aed; border-color: #7c3aed; color: white; }
        .vanco-prelevement { background: #f5f3ff; border-radius: 10px; padding: 14px 18px; margin-top: 12px; font-size: 13px; }
        @media (max-width: 600px) { .vanco-input-row { flex-direction: column; } }
    `;
    document.head.appendChild(style);
    var ANTIBIOS = {
        "Céphalosporines": {
            "Céfadroxil (PO)": { notes: "", doses: [
                { min: 30, max: 999, label: "500 mg PO q12h ou 1g PO q12-24h" },
                { min: 20, max: 29, label: "1g PO x1 dose puis 500 mg PO q12h" },
                { min: 10, max: 19, label: "1g PO x1 dose puis 500 mg PO q24h" },
                { min: 0, max: 9, label: "1g PO x1 dose puis 500 mg PO q24-36h" },
            ]},
            "Céfazoline (IV)": { notes: "", doses: [
                { min: 35, max: 999, label: "1-2g IV q8h (fréq. max. q6h)" },
                { min: 11, max: 34, label: "1-2g IV q12h" },
                { min: 0, max: 10, label: "1g IV q24h" },
            ]},
            "Céfépime (IV) — IIA/IU": { notes: "Infections intra-abdominales et urinaires", doses: [
                { min: 30, max: 999, label: "1-2g IV q12h" },
                { min: 11, max: 29, label: "1-2g IV q24h" },
                { min: 6, max: 10, label: "1g IV q24h" },
                { min: 0, max: 5, label: "500 mg IV q24h" },
            ]},
            "Céfépime (IV) — NF": { notes: "Neutropénie fébrile", doses: [
                { min: 30, max: 999, label: "2g IV q8h" },
                { min: 11, max: 29, label: "2g IV q12h" },
                { min: 6, max: 10, label: "2g IV q24h" },
                { min: 0, max: 5, label: "1g IV q24h" },
            ]},
            "Céfixime (PO)": { notes: "", doses: [
                { min: 21, max: 999, label: "400 mg PO q24h" },
                { min: 11, max: 20, label: "300 mg (¾ co.) PO q24h" },
                { min: 0, max: 10, label: "200 mg PO q24h" },
            ]},
            "Céfotaxime (IV)": { notes: "", doses: [
                { min: 21, max: 999, label: "1-2g IV q6-8h (jusqu'à 2g IV q4h si SNC)" },
                { min: 0, max: 20, label: "Diminuer la dose de 50%. Certains: 2g IV q12-24h" },
            ]},
            "Céfoxitine (IV)": { notes: "Dose max: 12g/jour", doses: [
                { min: 30, max: 999, label: "1-2g IV q6-8h" },
                { min: 11, max: 29, label: "1-2g IV q8-12h" },
                { min: 5, max: 10, label: "1-2g IV q12-24h" },
                { min: 1, max: 4, label: "50% de la dose IV q12-24h" },
                { min: 0, max: 0, label: "50% de la dose IV q24-48h" },
            ]},
            "Cefprozil (PO)": { notes: "", doses: [
                { min: 31, max: 999, label: "250-500 mg PO q12h" },
                { min: 0, max: 30, label: "250 mg PO q12h" },
            ]},
            "Ceftazidime (IV)": { notes: "", doses: [
                { min: 31, max: 999, label: "1-2g IV q8h" },
                { min: 16, max: 30, label: "1-2g IV q12h" },
                { min: 6, max: 15, label: "1-2g IV q24h" },
                { min: 1, max: 5, label: "500mg-1g IV q24h" },
                { min: 0, max: 0, label: "500mg-1g IV q48h" },
            ]},
            "Ceftobiprole (IV)": { notes: "Perfusion sur 4h si Clcr > 150 mL/min", doses: [
                { min: 31, max: 999, label: "500 mg IV q8h perf. sur 2h" },
                { min: 16, max: 30, label: "500 mg IV q12h perf. sur 2h" },
                { min: 6, max: 15, label: "250 mg IV q12h perf. sur 2h" },
                { min: 0, max: 5, label: "250 mg IV q24h perf. sur 2h" },
            ]},
            "Ceftriaxone (IV)": { notes: "Aucun ajustement requis sauf IR + IH (dose max: 2g/jour)", doses: [
                { min: 0, max: 999, label: "1-2g IV q24h (max: 2g IV q12h pour SNC)" },
            ]},
            "Céfuroxime axétil (PO)": { notes: "", doses: [
                { min: 11, max: 999, label: "250-500 mg PO q12h" },
                { min: 0, max: 10, label: "250 mg PO q24h" },
            ]},
            "Céfuroxime (IV)": { notes: "", doses: [
                { min: 21, max: 999, label: "750mg-1,5g IV q8h" },
                { min: 11, max: 20, label: "750 mg IV q12h" },
                { min: 0, max: 10, label: "750 mg IV q24h" },
            ]},
            "Céphalexine (PO)": { notes: "", doses: [
                { min: 41, max: 999, label: "250mg-1g PO q6h" },
                { min: 11, max: 40, label: "250-500 mg PO q8-12h" },
                { min: 0, max: 10, label: "250-500 mg PO q12-24h" },
            ]},
        },
        "Carbapénèmes": {
            "Ertapénem (IV)": { notes: "", doses: [
                { min: 31, max: 999, label: "1g IV q24h" },
                { min: 0, max: 30, label: "500 mg IV q24h" },
            ]},
            "Imipénem-Cilastatine (IV)": { notes: "PR ≥ 70 kg: 500 mg IV q6h (max 4g/jour). PR < 70 kg: voir monographie.", doses: [
                { min: 71, max: 999, label: "500 mg IV q6h (max: 4g/jour)" },
                { min: 41, max: 70, label: "500 mg IV q6-8h" },
                { min: 21, max: 40, label: "500 mg IV q8-12h" },
                { min: 6, max: 20, label: "250 mg IV q12h" },
                { min: 0, max: 5, label: "⚠️ Non recommandé" },
            ]},
            "Méropénem (IV) — usuel": { notes: "Posologie usuelle", doses: [
                { min: 26, max: 999, label: "1g IV q8h" },
                { min: 11, max: 25, label: "1g IV q12h" },
                { min: 6, max: 10, label: "500 mg IV q12h" },
                { min: 0, max: 5, label: "500 mg IV q24h" },
            ]},
            "Méropénem (IV) — SNC": { notes: "Infections du SNC", doses: [
                { min: 26, max: 999, label: "2g IV q8h" },
                { min: 11, max: 25, label: "2g IV q12h" },
                { min: 6, max: 10, label: "1g IV q12h" },
                { min: 0, max: 5, label: "1g IV q24h" },
            ]},
        },
        "Macrolides": {
            "Azithromycine (PO)": { notes: "Aucun ajustement requis", doses: [{ min: 0, max: 999, label: "250-500 mg PO q24h" }]},
            "Azithromycine (IV)": { notes: "Aucun ajustement requis", doses: [{ min: 0, max: 999, label: "500 mg IV q24h" }]},
            "Clarithromycine (PO)": { notes: "", doses: [
                { min: 31, max: 999, label: "250-500 mg PO q12h" },
                { min: 0, max: 30, label: "250 mg PO q24h (ou q12h si infection sévère)" },
            ]},
            "Érythromycine (PO/IV)": { notes: "Aucun ajustement requis", doses: [{ min: 0, max: 999, label: "Posologie selon indication et formulation" }]},
        },
        "Pénicillines": {
            "Amoxicilline (PO)": { notes: "", doses: [
                { min: 31, max: 999, label: "250mg-1g PO q8h" },
                { min: 11, max: 30, label: "250-500 mg PO q12h" },
                { min: 0, max: 10, label: "250-500 mg PO q24h" },
            ]},
            "Amoxicilline-Clavulanate (PO)": { notes: "", doses: [
                { min: 31, max: 999, label: "500/125 mg PO q8h ou 875/125 mg PO q12h" },
                { min: 11, max: 30, label: "500/125 mg PO q12h" },
                { min: 0, max: 10, label: "500/125 mg PO q24h" },
            ]},
            "Ampicilline (IV)": { notes: "Posologie usuelle: 1-2g IV q4-6h", doses: [
                { min: 41, max: 999, label: "1-2g IV q6h" },
                { min: 31, max: 40, label: "1-2g IV q6-8h" },
                { min: 16, max: 30, label: "1-2g IV q8-12h" },
                { min: 0, max: 15, label: "1-2g IV q12-24h" },
            ]},
            "Cloxacilline (PO)": { notes: "Aucun ajustement requis", doses: [{ min: 0, max: 999, label: "250-500 mg PO q6h (jusqu'à 4g/jour)" }]},
            "Cloxacilline (IV)": { notes: "Aucun ajustement requis", doses: [{ min: 0, max: 999, label: "1-2g IV q4-6h" }]},
            "Pénicilline G sodique (IV)": { notes: "DC = dose de charge", doses: [
                { min: 21, max: 999, label: "1-4 mU IV q4-6h" },
                { min: 11, max: 20, label: "DC suivie de 50% de la dose IV q4-6h" },
                { min: 0, max: 10, label: "DC suivie de 50% de la dose IV q8-12h" },
            ]},
            "Pipéracilline-Tazobactam (IV) — usuel": { notes: "Posologie usuelle", doses: [
                { min: 41, max: 999, label: "3,375g [3g/0,375g] IV q6h ou 4,5g [4g/0,5g] IV q8h" },
                { min: 21, max: 40, label: "2,25g [2g/0,25g] IV q6h" },
                { min: 0, max: 20, label: "2,25g [2g/0,25g] IV q8h" },
            ]},
            "Pipéracilline-Tazobactam (IV) — PAH/NF": { notes: "Pneumonie hospitalière ou neutropénie fébrile", doses: [
                { min: 41, max: 999, label: "4,5g [4g/0,5g] IV q6h" },
                { min: 21, max: 40, label: "3,375g [3g/0,375g] IV q6h" },
                { min: 0, max: 20, label: "2,25g [2g/0,25g] IV q6h" },
            ]},
        },
        "Fluoroquinolones": {
            "Ciprofloxacine (PO)": { notes: "", doses: [
                { min: 31, max: 999, label: "500-750 mg PO q12h" },
                { min: 16, max: 30, label: "500 mg PO q12h" },
                { min: 0, max: 15, label: "500 mg PO q24h ou 250 mg PO q12h" },
            ]},
            "Ciprofloxacine (IV)": { notes: "", doses: [
                { min: 31, max: 999, label: "400 mg IV q8-12h" },
                { min: 16, max: 30, label: "400 mg IV q12h" },
                { min: 0, max: 15, label: "400 mg IV q24h ou 200 mg IV q12h" },
            ]},
            "Lévofloxacine — légère/prostatite": { notes: "Ex: prostatite chronique", doses: [
                { min: 51, max: 999, label: "500 mg PO/IV q24h" },
                { min: 21, max: 50, label: "500 mg PO/IV x1 dose, puis 250 mg PO/IV q24h" },
                { min: 0, max: 20, label: "500 mg PO/IV x1 dose, puis 250 mg PO/IV q48h" },
            ]},
            "Lévofloxacine — PAH": { notes: "Pneumonie hospitalière", doses: [
                { min: 51, max: 999, label: "750 mg PO/IV q24h" },
                { min: 21, max: 50, label: "750 mg PO/IV q48h" },
                { min: 0, max: 20, label: "750 mg PO/IV x1 dose, puis 500 mg PO/IV q48h" },
            ]},
            "Moxifloxacine (PO/IV)": { notes: "Aucun ajustement requis", doses: [{ min: 0, max: 999, label: "400 mg PO/IV q24h" }]},
            "Norfloxacine (PO) — IU": { notes: "Infection urinaire", doses: [
                { min: 31, max: 999, label: "400 mg PO q12-24h" },
                { min: 0, max: 30, label: "400 mg PO q24h" },
            ]},
        },
        "Autres antibactériens": {
            "Clindamycine (PO)": { notes: "Aucun ajustement requis", doses: [{ min: 0, max: 999, label: "150-450 mg PO q6h" }]},
            "Clindamycine (IV)": { notes: "Aucun ajustement requis", doses: [{ min: 0, max: 999, label: "600-900 mg IV q8h (fréq. max.: q6h)" }]},
            "Daptomycine (IV) — usuelle": { notes: "Pour dose 6 mg/kg: utiliser PD si PR > 140 kg", doses: [
                { min: 31, max: 999, label: "4-6 mg/kg PR IV q24h" },
                { min: 0, max: 30, label: "Même dose IV q48h" },
            ]},
            "Doxycycline (PO)": { notes: "Aucun ajustement requis", doses: [{ min: 0, max: 999, label: "100 mg PO q12h" }]},
            "Linézolide (PO/IV)": { notes: "Aucun ajustement requis", doses: [{ min: 0, max: 999, label: "600 mg PO/IV q12h" }]},
            "Métronidazole (PO)": { notes: "", doses: [
                { min: 11, max: 999, label: "250-500 mg PO q8h ou 500 mg PO q12h" },
                { min: 0, max: 10, label: "500 mg PO q12h" },
            ]},
            "Métronidazole (IV)": { notes: "", doses: [
                { min: 11, max: 999, label: "500 mg IV q8h (fréq. max.: q6h)" },
                { min: 0, max: 10, label: "500 mg IV q12h" },
            ]},
            "Nitrofurantoïne (PO)": { notes: "Cystite non compliquée uniquement", doses: [
                { min: 46, max: 999, label: "50-100 mg PO QID ou 100 mg PO q12h (monohydrate)" },
                { min: 31, max: 45, label: "Données limitées. Possiblement efficace à court terme." },
                { min: 0, max: 30, label: "⚠️ Contre-indiqué — risque de toxicité accru" },
            ]},
            "Tétracycline (PO)": { notes: "", doses: [
                { min: 51, max: 999, label: "250-500 mg PO q6h" },
                { min: 11, max: 50, label: "250-500 mg PO q12h" },
                { min: 0, max: 10, label: "250-500 mg PO q24h" },
            ]},
            "Tigécycline (IV)": { notes: "Aucun ajustement requis", doses: [{ min: 0, max: 999, label: "100 mg IV x1 dose puis 50 mg IV q12h" }]},
            "Triméthoprime (PO)": { notes: "", doses: [
                { min: 16, max: 999, label: "100 mg PO q12h" },
                { min: 6, max: 15, label: "50 mg PO q12h" },
                { min: 0, max: 5, label: "⚠️ Non recommandé" },
            ]},
            "TMP-SMX — usuel": { notes: "80 mg TMP = 1 co SS; 160 mg TMP = 1 co DS", doses: [
                { min: 31, max: 999, label: "160-320 mg TMP PO/IV q12h" },
                { min: 16, max: 30, label: "80 mg TMP PO/IV q12h ou 160 mg TMP PO/IV q24h" },
                { min: 0, max: 15, label: "⚠️ Non recommandé" },
            ]},
            "Vancomycine (IV)": { notes: "⚠️ Amorcer empiriquement selon âge, fonction rénale et poids puis ajuster selon dosages sériques. DC = 25 mg/kg PR. DM: 15-20 mg/kg PD/dose q8-12h (max 2g/dose). Consulter pharmacien.", doses: [
                { min: 71, max: 999, label: "DM IV q12h — surveiller dosages sériques" },
                { min: 41, max: 70, label: "DM IV q24h — surveiller dosages sériques" },
                { min: 21, max: 40, label: "DM IV q36-48h — dosages sériques quotidiens" },
                { min: 11, max: 20, label: "DM IV q72-96h" },
                { min: 0, max: 10, label: "DM IV q5-7 jours — consulter pharmacien" },
            ]},
            "Vancomycine (PO)": { notes: "Aucun ajustement requis", doses: [{ min: 0, max: 999, label: "125 mg PO QID (max 500 mg PO QID si infection compliquée)" }]},
        },
        "Antifongiques": {
            "Anidulafungine (IV)": { notes: "Aucun ajustement requis", doses: [{ min: 0, max: 999, label: "200 mg IV x1 dose puis 100 mg IV q24h" }]},
            "Caspofungine (IV)": { notes: "Aucun ajustement requis", doses: [{ min: 0, max: 999, label: "70 mg IV x1 dose puis 50 mg IV q24h" }]},
            "Fluconazole (PO/IV)": { notes: "DC = 100% de la dose. Écart: 100-800 mg PO/IV q24h", doses: [
                { min: 51, max: 999, label: "DC puis 100% dose PO/IV q24h" },
                { min: 11, max: 50, label: "DC puis 50% de la dose PO/IV q24h" },
                { min: 0, max: 10, label: "DC puis 25-50% de la dose PO/IV q24h" },
            ]},
            "Isavuconazole (PO/IV)": { notes: "Aucun ajustement requis. 372 mg sulfate = 200 mg isavuconazole.", doses: [{ min: 0, max: 999, label: "DC: 200 mg PO/IV q8h x6 doses, puis 200 mg PO/IV q24h" }]},
            "Kétoconazole (PO)": { notes: "Aucun ajustement requis", doses: [{ min: 0, max: 999, label: "200-400 mg PO q24h" }]},
            "Micafungine — prophylaxie": { notes: "Aucun ajustement requis", doses: [{ min: 0, max: 999, label: "50-100 mg IV q24h" }]},
            "Micafungine — traitement": { notes: "Aucun ajustement requis", doses: [{ min: 0, max: 999, label: "100-150 mg IV q24h" }]},
            "Terbinafine (PO)": { notes: "", doses: [
                { min: 51, max: 999, label: "250 mg PO q24h" },
                { min: 0, max: 50, label: "⚠️ Non recommandé — données limitées" },
            ]},
        },
        "Antimycobactériens": {
            "Ethambutol (PO)": { notes: "Si PR > 120% PI, baser dose sur PI.", doses: [
                { min: 31, max: 999, label: "15-25 mg/kg PO q24h" },
                { min: 0, max: 30, label: "15-25 mg/kg/dose PO DIE 3 jours/semaine" },
            ]},
            "Isoniazide (PO)": { notes: "Aucun ajustement requis", doses: [{ min: 0, max: 999, label: "5 mg/kg PO q24h (max: 300 mg)" }]},
            "Pyrazinamide (PO)": { notes: "Si PR > 120% PI, baser dose sur PI.", doses: [
                { min: 31, max: 999, label: "20-25 mg/kg PO q24h (max: 2g)" },
                { min: 0, max: 30, label: "25-35 mg/kg/dose PO DIE 3 jours/semaine" },
            ]},
            "Rifabutine (PO)": { notes: "", doses: [
                { min: 16, max: 999, label: "300 mg PO q24h" },
                { min: 0, max: 15, label: "150 mg PO q24h" },
            ]},
            "Rifampicine (PO)": { notes: "Aucun ajustement requis", doses: [{ min: 0, max: 999, label: "10 mg/kg PO q24h (max: 600 mg)" }]},
        },
        "Antiviraux": {
            "Acyclovir (IV)": { notes: "Posologie usuelle: 5-15 mg/kg/dose IV q8h", doses: [
                { min: 26, max: 999, label: "100% de la dose IV q8h" },
                { min: 11, max: 25, label: "100% de la dose IV q12h" },
                { min: 6, max: 10, label: "100% de la dose IV q24h" },
                { min: 0, max: 5, label: "50% de la dose IV q24h" },
            ]},
            "Acyclovir (PO) — zona": { notes: "", doses: [
                { min: 26, max: 999, label: "800 mg PO q4h (5 fois/jour)" },
                { min: 11, max: 25, label: "800 mg PO q8h" },
                { min: 0, max: 10, label: "800 mg PO q12h" },
            ]},
            "Famciclovir (PO) — zona": { notes: "", doses: [
                { min: 41, max: 999, label: "500 mg PO q8h" },
                { min: 21, max: 40, label: "500 mg PO q12h" },
                { min: 6, max: 20, label: "500 mg PO q24h" },
                { min: 0, max: 5, label: "250 mg PO q24h" },
            ]},
            "Oseltamivir (PO) — traitement": { notes: "Influenza A et B", doses: [
                { min: 31, max: 999, label: "75 mg PO q12h" },
                { min: 11, max: 30, label: "30 mg PO q12h" },
                { min: 6, max: 10, label: "30 mg PO q24h" },
                { min: 0, max: 5, label: "75 mg PO x1 dose" },
            ]},
            "Oseltamivir (PO) — prophylaxie": { notes: "Prophylaxie Influenza A et B", doses: [
                { min: 31, max: 999, label: "75 mg PO q24h" },
                { min: 11, max: 30, label: "30 mg PO q24h" },
                { min: 6, max: 10, label: "30 mg PO q48h" },
                { min: 0, max: 5, label: "Privilégier zanamivir inhalation" },
            ]},
            "Valacyclovir (PO) — zona": { notes: "", doses: [
                { min: 31, max: 999, label: "1g PO q8h" },
                { min: 16, max: 30, label: "1g PO q12h" },
                { min: 6, max: 15, label: "1g PO q24h" },
                { min: 0, max: 5, label: "500 mg PO q24h" },
            ]},
            "Valacyclovir (PO) — herpès génital": { notes: "", doses: [
                { min: 31, max: 999, label: "1g PO q12h" },
                { min: 16, max: 30, label: "1g PO q24h" },
                { min: 0, max: 15, label: "500 mg PO q24h" },
            ]},
        },
    };
    function calcClcr(age, poids, creat, sexe) {
        var k = sexe === 'F' ? 0.85 : 1.0;
        return ((140 - age) * poids / (49 * creat)) * 60 * k;
    }
    function calcPI(taille, sexe) {
        return sexe === 'F' ? 45.5 + 0.91 * (taille - 152) : 50 + 0.91 * (taille - 152);
    }
    function calcIMC(poids, taille) {
        return poids / Math.pow(taille / 100, 2);
    }
    var state = {
        age: '', poids: '', taille: '', creat: '', sexe: 'H',
        selectedCat: '', selectedDrug: '', search: '',
        mode: 'cg', dfgDirect: ''
    };
    function getDrugData(cat, drug) {
        return ANTIBIOS[cat] && ANTIBIOS[cat][drug] ? ANTIBIOS[cat][drug] : null;
    }
    function getActiveDose(doses, clcr) {
        var clcrRounded = Math.round(clcr * 10) / 10;
        for (var i = 0; i < doses.length; i++) {
            if (clcrRounded >= doses[i].min && clcrRounded <= doses[i].max) return i;
        }
        for (var i = doses.length - 1; i >= 0; i--) {
            if (clcrRounded >= doses[i].min) return i;
        }
        return doses.length - 1;
    }
    function calcClcrFromState() {
        if (state.mode === 'direct') {
            var dfg = parseFloat(state.dfgDirect);
            return { clcr: isNaN(dfg) ? null : dfg, imc: null, pi: null, poidsCalc: null };
        }
        var age = parseFloat(state.age);
        var poids = parseFloat(state.poids);
        var taille = parseFloat(state.taille);
        var creat = parseFloat(state.creat);
        var imc = (!isNaN(poids) && !isNaN(taille)) ? calcIMC(poids, taille) : null;
        var pi = (!isNaN(taille)) ? calcPI(taille, state.sexe) : null;
        var poidsCalc = null;
        if (!isNaN(poids) && !isNaN(taille)) {
            poidsCalc = imc < 30 ? poids : pi + 0.4 * (poids - pi);
        }
        var clcr = (!isNaN(age) && poidsCalc !== null && !isNaN(creat)) ? calcClcr(age, poidsCalc, creat, state.sexe) : null;
        return { clcr: clcr, imc: imc, pi: pi, poidsCalc: poidsCalc };
    }
    function renderResult() {
        var res = calcClcrFromState();
        var clcr = res.clcr; var imc = res.imc; var pi = res.pi; var poidsCalc = res.poidsCalc;
        var clcrColor = '#64748b'; var clcrLabel = '';
        if (clcr !== null) {
            if (clcr >= 60) { clcrColor = '#16a34a'; clcrLabel = 'Normale / légèrement réduite'; }
            else if (clcr >= 30) { clcrColor = '#d97706'; clcrLabel = 'IRC modérée'; }
            else if (clcr >= 15) { clcrColor = '#ea580c'; clcrLabel = 'IRC sévère'; }
            else { clcrColor = '#dc2626'; clcrLabel = 'IRC très sévère / terminale'; }
        }
        var box = document.getElementById('calc-result-box');
        if (box) {
            if (clcr !== null) {
                var html = '<div>';
                html += '<div style="font-size:11px;color:#64748b;font-weight:600">' + (state.mode === 'direct' ? 'DFG (LABORATOIRE)' : 'CLAIRANCE CRÉATININE') + '</div>';
                html += '<div class="calc-clcr-val" style="color:' + clcrColor + '">' + clcr.toFixed(1) + ' <span style="font-size:14px;font-weight:500">mL/min</span></div>';
                html += '<div class="calc-clcr-label" style="color:' + clcrColor + '">' + clcrLabel + '</div>';
                html += '</div>';
                if (imc !== null) {
                    html += '<div class="calc-imc-info">';
                    html += '<div style="font-size:11px;color:#64748b;font-weight:600">IMC</div>';
                    html += '<div style="font-size:20px;font-weight:700;color:#0f2d5e">' + imc.toFixed(1) + ' <span style="font-size:11px">kg/m\u00b2</span></div>';
                    if (imc >= 30 && pi) {
                        html += '<div>IMC \u2265 30 \u2014 Poids de dosage : <b>' + poidsCalc.toFixed(1) + ' kg</b><br>Poids id\u00e9al : ' + pi.toFixed(1) + ' kg</div>';
                    }
                    html += '</div>';
                }
                box.innerHTML = html;
                box.style.display = 'flex';
                box.style.border = '2px solid ' + clcrColor + '33';
            } else {
                box.innerHTML = '';
                box.style.display = 'none';
            }
        }
        var btnH = document.getElementById('calc-btn-h');
        var btnF = document.getElementById('calc-btn-f');
        if (btnH) btnH.className = 'calc-sexe-btn' + (state.sexe === 'H' ? ' active' : '');
        if (btnF) btnF.className = 'calc-sexe-btn' + (state.sexe === 'F' ? ' active' : '');
        renderDrugResult(clcr);
    }
    function renderDrugResult(clcr) {
        var drugData = getDrugData(state.selectedCat, state.selectedDrug);
        var section = document.getElementById('calc-drug-result');
        if (!section) return;
        if (!state.selectedDrug || !drugData) { section.innerHTML = ''; return; }
        var html = '<div class="calc-card">';
        html += '<h2>3. Posologie recommand\u00e9e</h2>';
        html += '<div style="font-size:13px;color:#64748b;margin-bottom:14px">' + state.selectedDrug + '</div>';
        if (drugData.notes) html += '<div class="calc-note">\u26a0\ufe0f ' + drugData.notes + '</div>';
        html += '<table class="calc-table"><thead><tr><th>ClCr (mL/min)</th><th>Posologie</th></tr></thead><tbody>';
        var activeIdx = clcr !== null ? getActiveDose(drugData.doses, clcr) : -1;
        drugData.doses.forEach(function(d, i) {
            var isActive = clcr !== null && i === activeIdx;
            var rangeLabel = d.max >= 999 ? '\u2265 ' + d.min : d.min === 0 ? '< ' + (drugData.doses.filter(function(x){return x.min>0;}).map(function(x){return x.min;}).sort(function(a,b){return a-b;})[0]||5) : d.min + ' \u2013 ' + d.max;
            html += '<tr' + (isActive ? ' class="active-row"' : ' style="background:' + (i%2===0?'white':'#f8fafc') + '"') + '>';
            html += '<td style="font-weight:600;white-space:nowrap">' + (isActive ? '\u25b6 ' : '') + rangeLabel + '</td>';
            html += '<td>' + d.label + (isActive ? '<span class="calc-badge">VOTRE PATIENT</span>' : '') + '</td>';
            html += '</tr>';
        });
        html += '</tbody></table>';
        if (clcr === null) html += '<div class="calc-hint">\ud83d\udca1 Entrez les donn\u00e9es du patient (\u00e9tape 1) pour mettre en \u00e9vidence la posologie appropri\u00e9e.</div>';
        html += '</div>';
        section.innerHTML = html;
    }
    function renderDrugSelector() {
        var searchResults = [];
        if (state.search && state.search.length >= 2) {
            var q = state.search.toLowerCase();
            Object.keys(ANTIBIOS).forEach(function(cat) {
                Object.keys(ANTIBIOS[cat]).forEach(function(drug) {
                    if (drug.toLowerCase().indexOf(q) !== -1) searchResults.push({ cat: cat, drug: drug });
                });
            });
        }
        var sr = document.getElementById('calc-search-results');
        if (sr) {
            var html = '';
            searchResults.forEach(function(r) {
                html += '<button onclick="calcSelectDrug(\'' + r.cat.replace(/'/g, "\\'") + '\',\'' + r.drug.replace(/'/g, "\\'") + '\')">';
                html += '<span style="color:#64748b;font-size:11px">' + r.cat + ' / </span>' + r.drug + '</button>';
            });
            sr.innerHTML = html;
        }
        var cats = document.getElementById('calc-cats');
        if (cats) {
            var html = '';
            Object.keys(ANTIBIOS).forEach(function(cat) {
                html += '<button class="calc-cat-btn ' + (state.selectedCat === cat ? 'active' : '') + '" onclick="calcSetCat(\'' + cat.replace(/'/g, "\\'") + '\')">' + cat + '</button>';
            });
            cats.innerHTML = html;
        }
        var drugs = document.getElementById('calc-drugs');
        if (drugs) {
            var html = '';
            if (state.selectedCat && ANTIBIOS[state.selectedCat]) {
                Object.keys(ANTIBIOS[state.selectedCat]).forEach(function(drug) {
                    html += '<button class="calc-drug-btn ' + (state.selectedDrug === drug ? 'active' : '') + '" onclick="calcSetDrug(\'' + drug.replace(/'/g, "\\'") + '\')">' + drug + '</button>';
                });
            }
            drugs.innerHTML = html;
        }
    }
    function renderVanco() {
        var app = document.getElementById('vanco-app');
        if (!app) return;
        var html = '';
        // Header violet
        html += '<div class="vanco-header">';
        html += '<h1>Vancomycine IV \u2014 Tableaux posologiques</h1>';
        html += '<p>Bas\u00e9 sur les lignes directrices APES \u2022 Adultes seulement</p>';
        html += '</div>';
        // Explication
        html += '<div class="vanco-card">';
        html += '<h2>\ud83d\udcd6 Comment utiliser ces tableaux</h2>';
        html += '<ol style="font-size:13px;color:#374151;line-height:1.8;margin:0;padding-left:20px;">';
        html += '<li><b>Dose de charge (optionnelle) :</b> Trouver le poids du patient dans le tableau 1 \u2192 administrer la dose correspondante en IV \u00d7 1 seule fois. Recommand\u00e9e chez les patients gravement malades.</li>';
        html += '<li><b>Dose d\u2019entretien :</b> Trouver le poids dans le tableau 2 \u2192 c\u2019est la dose par administration.</li>';
        html += '<li><b>Fr\u00e9quence :</b> Utiliser le DFGe fourni par le laboratoire (mL/min/1,73m\u00b2) dans le tableau 2 \u2192 d\u00e9termine l\u2019intervalle entre les doses (q8h, q12h, q24h).</li>';
        html += '<li><b>Pr\u00e9l\u00e8vements :</b> Consulter le tableau 3 selon la fr\u00e9quence choisie pour planifier le moment des pr\u00e9l\u00e8vements PIC et creux.</li>';
        html += '<li><b>Ajustement :</b> Toujours ajuster selon les dosages s\u00e9riques obtenus. Consulter un pharmacien pour les cas complexes (IMC \u2265 30, insuffisance r\u00e9nale s\u00e9v\u00e8re, soins intensifs).</li>';
        html += '</ol>';
        html += '<div style="margin-top:12px;padding:10px 14px;background:#f0fdf4;border-radius:8px;font-size:12px;color:#166534;">';
        html += '\ud83d\udca1 <b>Astuce :</b> Pour un calcul plus pr\u00e9cis de la dose d\u2019entretien (particulièrement si IMC \u2265 30), utiliser <b>vancopk.com</b>.';
        html += '</div>';
        html += '</div>';
        // Tableau 1 — Dose de charge
        html += '<div class="vanco-card">';
        html += '<h2>Tableau 1 \u2014 Dose de charge \u00d7 1 dose</h2>';
        html += '<div style="font-size:12px;color:#64748b;margin-bottom:12px;">Calcul\u00e9e \u00e0 25 mg/kg de poids r\u00e9el. Non applicable aux usagers sous th\u00e9rapie de remplacement r\u00e9nal.</div>';
        html += '<div style="overflow-x:auto;">';
        html += '<table class="vanco-table"><thead><tr><th>Poids (kg)</th><th>Dose (mg)</th><th>Poids (kg)</th><th>Dose (mg)</th><th>Poids (kg)</th><th>Dose (mg)</th></tr></thead><tbody>';
        html += '<tr><td>35 \u2013 39,9</td><td><b>750</b></td><td>60 \u2013 69,9</td><td><b>1500</b></td><td>90 \u2013 99,9</td><td><b>2250</b></td></tr>';
        html += '<tr><td>40 \u2013 49,9</td><td><b>1000</b></td><td>70 \u2013 79,9</td><td><b>1750</b></td><td>100 \u2013 129,9</td><td><b>2500</b></td></tr>';
        html += '<tr><td>50 \u2013 59,9</td><td><b>1250</b></td><td>80 \u2013 89,9</td><td><b>2000</b></td><td>130 et plus</td><td><b>3000</b></td></tr>';
        html += '</tbody></table>';
        html += '</div></div>';
        // Tableau 2 — Entretien
        html += '<div class="vanco-card">';
        html += '<h2>Tableau 2 \u2014 Traitement d\u2019entretien</h2>';
        html += '<div style="font-size:12px;color:#64748b;margin-bottom:12px;">Croiser la dose selon le poids <b>ET</b> la fr\u00e9quence selon le DFGe du laboratoire.</div>';
        html += '<div style="display:flex;gap:16px;flex-wrap:wrap;">';
        html += '<div style="flex:1;min-width:180px;">';
        html += '<div class="vanco-section-title">Dose selon le poids</div>';
        html += '<table class="vanco-table"><thead><tr><th>Poids (kg)</th><th>Dose / administration</th></tr></thead><tbody>';
        html += '<tr><td>35 \u2013 39,9</td><td><b>500 mg</b></td></tr>';
        html += '<tr><td>40 \u2013 59,9</td><td><b>750 mg</b></td></tr>';
        html += '<tr><td>60 \u2013 79,9</td><td><b>1000 mg</b></td></tr>';
        html += '<tr><td>80 \u2013 99,9</td><td><b>1250 mg</b></td></tr>';
        html += '<tr><td>100 et plus</td><td><b>1500 mg</b></td></tr>';
        html += '</tbody></table></div>';
        html += '<div style="flex:1;min-width:220px;">';
        html += '<div class="vanco-section-title">Fr\u00e9quence selon le DFGe (mL/min/1,73m\u00b2)</div>';
        html += '<table class="vanco-table"><thead><tr><th>DFGe</th><th>Fr\u00e9quence</th></tr></thead><tbody>';
        html += '<tr><td>Plus de 90</td><td><b>q 8h</b></td></tr>';
        html += '<tr><td>50 \u2013 90</td><td><b>q 12h</b></td></tr>';
        html += '<tr><td>15 \u2013 49</td><td><b>q 24h</b></td></tr>';
        html += '<tr><td>Moins de 15</td><td><b>Dose unique puis selon dosage</b></td></tr>';
        html += '</tbody></table></div>';
        html += '</div></div>';
        // Tableau 3 — Prélèvements
        html += '<div class="vanco-card">';
        html += '<h2>Tableau 3 \u2014 Moment des pr\u00e9l\u00e8vements recommand\u00e9s</h2>';
        html += '<div style="font-size:12px;color:#64748b;margin-bottom:12px;">Incluant la dose de charge s\u2019il y a lieu.</div>';
        html += '<div style="overflow-x:auto;">';
        html += '<table class="vanco-table"><thead><tr><th>Intervalle posologique</th><th>PIC</th><th>CREUX</th></tr></thead><tbody>';
        html += '<tr><td style="font-weight:700;">q8h ou q12h</td><td>60 \u00e0 120 min post fin de perfusion de la <b>4\u1d49 dose</b></td><td>Juste avant la <b>4\u1d49 dose</b></td></tr>';
        html += '<tr><td style="font-weight:700;">q24h</td><td>60 \u00e0 120 min post fin de perfusion de la <b>3\u1d49 dose</b></td><td>Juste avant la <b>3\u1d49 dose</b></td></tr>';
        html += '</tbody></table>';
        html += '</div>';
        html += '<div style="font-size:12px;color:#64748b;margin-top:10px;">Des pr\u00e9l\u00e8vements par <b>creux seulement</b> doivent \u00eatre effectu\u00e9s pour les patients dont l\u2019\u00e9tat d\u2019\u00e9quilibre n\u2019est pas atteint.</div>';
        html += '</div>';
        // Avertissement
        html += '<div class="vanco-warning">\u26a0\ufe0f <b>Avertissement clinique :</b> Ces tableaux sont fournis \u00e0 titre de r\u00e9f\u00e9rence. Toujours ajuster selon les dosages s\u00e9riques. Consulter un pharmacien pour tout ajustement posologique de la vancomycine et se r\u00e9f\u00e9rer au guide APES (bulletin N\u00b0 40).</div>';
        app.innerHTML = html;
    }
    function render() {
        var app = document.getElementById('calc-app');
        if (!app) return;
        var html = '';
        html += '<div class="calc-header">';
        html += '<h1>Calculateur posologique \u2014 Insuffisance r\u00e9nale</h1>';
        html += '<p>Bas\u00e9 sur le guide APES (RPEI) \u2014 Octobre 2019 \u2022 Adultes seulement \u2022 Le jugement clinique doit pr\u00e9valoir</p>';
        html += '</div>';
        html += '<div class="calc-card">';
        html += '<h2>1. Fonction r\u00e9nale</h2>';
        html += '<div style="display:flex;gap:8px;margin-bottom:16px;">';
        html += '<button id="calc-btn-cg" class="calc-sexe-btn ' + (state.mode === 'cg' ? 'active' : '') + '" onclick="calcSetMode(\'cg\')">Calculer (Cockcroft-Gault)</button>';
        html += '<button id="calc-btn-direct" class="calc-sexe-btn ' + (state.mode === 'direct' ? 'active' : '') + '" onclick="calcSetMode(\'direct\')">DFG d\u00e9j\u00e0 connu</button>';
        html += '</div>';
        if (state.mode === 'cg') {
            html += '<div style="margin-bottom:12px;display:flex;align-items:center;gap:8px;">';
            html += '<span style="font-size:13px;color:#64748b;width:50px">Sexe</span>';
            html += '<button id="calc-btn-h" class="calc-sexe-btn ' + (state.sexe === 'H' ? 'active' : '') + '" onclick="calcSetSexe(\'H\')">Homme</button>';
            html += '<button id="calc-btn-f" class="calc-sexe-btn ' + (state.sexe === 'F' ? 'active' : '') + '" onclick="calcSetSexe(\'F\')">Femme</button>';
            html += '</div>';
            html += '<div class="calc-grid">';
            html += '<div><label>\u00c2ge (ans)</label><input id="calc-age" type="number" placeholder="ex: 65" oninput="calcSetField(\'age\',this.value)"></div>';
            html += '<div><label>Poids r\u00e9el (kg)</label><input id="calc-poids" type="number" placeholder="ex: 70" oninput="calcSetField(\'poids\',this.value)"></div>';
            html += '<div><label>Taille (cm)</label><input id="calc-taille" type="number" placeholder="ex: 170" oninput="calcSetField(\'taille\',this.value)"></div>';
            html += '<div><label>Cr\u00e9atinine (\u03bcmol/L)</label><input id="calc-creat" type="number" placeholder="ex: 120" oninput="calcSetField(\'creat\',this.value)"></div>';
            html += '</div>';
        } else {
            html += '<div style="max-width:250px;">';
            html += '<label style="font-size:11px;color:#64748b;font-weight:600;display:block;margin-bottom:4px;">DFG / ClCr (mL/min)</label>';
            html += '<input id="calc-dfg-direct" type="number" placeholder="ex: 45" style="width:100%;padding:10px 14px;border-radius:8px;border:1.5px solid #e2e8f0;font-size:18px;font-weight:700;color:#0f2d5e;outline:none;" oninput="calcSetField(\'dfgDirect\',this.value)">';
            html += '<div style="font-size:11px;color:#64748b;margin-top:6px;">Valeur fournie par le laboratoire</div>';
            html += '</div>';
        }
        html += '<div id="calc-result-box" class="calc-result-box" style="display:none"></div>';
        html += '</div>';
        html += '<div class="calc-card">';
        html += '<h2>2. S\u00e9lection de l\'antimicrobien</h2>';
        html += '<input id="calc-search-input" class="calc-search" type="text" placeholder="\ud83d\udd0d Rechercher un antimicrobien..." oninput="calcSetSearch(this.value)">';
        html += '<div id="calc-search-results" class="calc-search-results"></div>';
        html += '<div id="calc-cats" class="calc-cats"></div>';
        html += '<div id="calc-drugs" class="calc-drugs"></div>';
        html += '</div>';
        html += '<div id="calc-drug-result"></div>';
        html += '<div class="calc-warning">\u26a0\ufe0f <b>Avertissement clinique :</b> Cet outil est bas\u00e9 sur le guide APES (RPEI, octobre 2019). Les ajustements servent uniquement \u00e0 guider la d\u00e9cision clinique \u2014 le jugement clinique doit pr\u00e9valoir. Consulter un pharmacien pour les cas complexes.</div>';
        html += '<div id="vanco-app" style="margin-top:24px;"></div>';
        app.innerHTML = html;
        renderResult();
        renderDrugSelector();
        renderVanco();
    }
    window.calcSetMode = function(mode) { state.mode = mode; render(); };
    window.calcSetField = function(field, val) { state[field] = val; renderResult(); };
    window.calcSetSexe = function(sexe) { state.sexe = sexe; renderResult(); };
    window.calcSetSearch = function(val) { state.search = val; state.selectedCat = ''; state.selectedDrug = ''; renderDrugSelector(); renderDrugResult(calcClcrFromState().clcr); };
    window.calcSetCat = function(cat) { state.selectedCat = cat; state.selectedDrug = ''; renderDrugSelector(); renderDrugResult(calcClcrFromState().clcr); };
    window.calcSetDrug = function(drug) { state.selectedDrug = drug; renderDrugSelector(); renderDrugResult(calcClcrFromState().clcr); };
    window.calcSelectDrug = function(cat, drug) { state.selectedCat = cat; state.selectedDrug = drug; state.search = ''; var si = document.getElementById('calc-search-input'); if(si) si.value=''; renderDrugSelector(); renderDrugResult(calcClcrFromState().clcr); };
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', render);
    } else {
        render();
    }
}
/* ====== DEBUT Loupe images ======
  Un clic sur une image l'ouvre en grand (lisible sur telephone).
  Independant de l'assistant : supprimer ce bloc ne touche a rien d'autre.
  ============ */
/* =====================================================================
  Agrandissement des images — microguide.ca
  =====================================================================
  Un clic (ou une tape) sur n'importe quelle image d'une page l'ouvre en
  grand, par-dessus la page. Sur téléphone, on peut ensuite zoomer et
  déplacer l'image avec deux doigts, puis fermer d'une tape.
  Indépendant de l'assistant : ce bloc peut être retiré sans toucher au
  reste. Aucune donnée ne sort du wiki.
  ===================================================================== */
(function () {
  "use strict";
  if (window.__microguideLoupe) return;
  window.__microguideLoupe = true;
  var CSS = [
    '.mgl-fond{position:fixed;inset:0;z-index:9500;background:rgba(10,14,18,.92);',
    '  display:flex;align-items:center;justify-content:center;',
    '  padding:env(safe-area-inset-top,0px) 0 env(safe-area-inset-bottom,0px);',
    '  opacity:0;transition:opacity .15s ease;touch-action:none}',
    '.mgl-fond.mgl-ouvert{opacity:1}',
    '.mgl-cadre{max-width:100%;max-height:100%;overflow:auto;',
    '  -webkit-overflow-scrolling:touch;touch-action:pinch-zoom;display:flex;',
    '  align-items:center;justify-content:center}',
    '.mgl-cadre img{display:block;max-width:100%;max-height:100vh;',
    '  width:auto;height:auto;object-fit:contain;background:#fff;border-radius:3px}',
    '.mgl-fermer{position:fixed;z-index:9501;',
    '  top:calc(10px + env(safe-area-inset-top,0px));',
    '  right:calc(10px + env(safe-area-inset-right,0px));',
    '  width:44px;height:44px;border-radius:50%;border:none;cursor:pointer;',
    '  background:rgba(255,255,255,.92);color:#1c2228;font-size:22px;line-height:44px;',
    '  text-align:center;padding:0;font-family:inherit}',
    '.mgl-legende{position:fixed;left:0;right:0;z-index:9501;',
    '  bottom:calc(8px + env(safe-area-inset-bottom,0px));',
    '  text-align:center;color:#e8e4dc;font:400 12px/1.5 system-ui,sans-serif;',
    '  padding:0 16px;pointer-events:none;text-shadow:0 1px 3px rgba(0,0,0,.8)}',
    // sur la page elle-même : montrer que l'image est cliquable
    '.mw-parser-output img{cursor:zoom-in}',
    '@media (prefers-reduced-motion:reduce){.mgl-fond{transition:none}}'
  ].join("");
  var style = document.createElement("style");
  style.textContent = CSS;
  document.head.appendChild(style);
  // Les images affichées sont des vignettes. L'originale, bien plus lisible
  // sur téléphone, se déduit de l'adresse : /images/thumb/a/ab/X.jpg/550px-X.jpg
  // devient /images/a/ab/X.jpg
  function pleineTaille(src) {
    try {
      var u = new URL(src, location.href);
      if (u.origin !== location.origin) return src;
      var m = u.pathname.match(/^(.*)\/thumb(\/[0-9a-f]\/[0-9a-f]{2}\/[^\/]+)\/[^\/]+$/);
      if (m) { u.pathname = m[1] + m[2]; return u.href; }
      return src;
    } catch (e) { return src; }
  }
  var fond = null;
  function fermer() {
    if (!fond) return;
    fond.classList.remove("mgl-ouvert");
    var f = fond; fond = null;
    setTimeout(function () { if (f && f.parentNode) f.parentNode.removeChild(f); }, 160);
    document.documentElement.style.overflow = "";
  }
  function ouvrir(img) {
    if (fond) return;
    fond = document.createElement("div");
    fond.className = "mgl-fond";
    var cadre = document.createElement("div");
    cadre.className = "mgl-cadre";
    var grande = document.createElement("img");
    grande.src = pleineTaille(img.currentSrc || img.src);
    grande.alt = img.alt || "";
    cadre.appendChild(grande);
    fond.appendChild(cadre);
    var bouton = document.createElement("button");
    bouton.className = "mgl-fermer";
    bouton.setAttribute("aria-label", "Fermer");
    bouton.textContent = "✕";
    fond.appendChild(bouton);
    var legende = document.createElement("div");
    legende.className = "mgl-legende";
    legende.textContent = "Pincez pour zoomer · touchez le fond pour fermer";
    fond.appendChild(legende);
    document.body.appendChild(fond);
    document.documentElement.style.overflow = "hidden";
    requestAnimationFrame(function () { fond.classList.add("mgl-ouvert"); });
    bouton.addEventListener("click", fermer);
    fond.addEventListener("click", function (e) {
      // fermer seulement si on touche le fond, pas l'image elle-même
      if (e.target === fond || e.target === cadre || e.target === legende) fermer();
    });
  }
  document.addEventListener("click", function (e) {
    if (fond) return;
    var img = e.target;
    if (!img || img.tagName !== "IMG") return;
    // ne pas intercepter l'assistant, ni les icônes de l'interface du wiki
    if (img.closest(".mga-panel, .mga-launch, .mw-header, .citizen-header, .mw-footer")) return;
    if (!img.closest(".mw-parser-output, #mw-content-text")) return;
    if (img.naturalWidth && img.naturalWidth < 80) return;  // puce ou icône
    e.preventDefault();
    e.stopPropagation();
    ouvrir(img);
  }, true);
  document.addEventListener("keydown", function (e) {
    if (e.key === "Escape") fermer();
  });
})();
/* ====== FIN Loupe images ====== */
/* ====== DEBUT Assistant Microguide ====== */
/* =====================================================================
  Assistant Microguide — recherche-réponse pour microguide.ca
  =====================================================================
  Tout se passe dans le navigateur du visiteur : il lit le wiki par son
  API publique, reconstruit son index dès que le wiki change, et répond
  en recopiant mot pour mot le texte, les liens et les tableaux du wiki.
  Aucun serveur, aucune clé d'API, aucun modèle de langue : il ne rédige
  jamais, donc il ne peut rien inventer.
  TROIS COMPORTEMENTS
    1. Sujet traité par une page -> réponse citée, avec le lien.
    2. Sujet seulement MENTIONNÉ en passant -> la phrase exacte est
        montrée, précédée d'un avertissement : ce n'est pas forcément le
        traitement complet ni celui de choix.
    3. Sujet absent -> refus explicite.
  INSTALLATION : coller ce fichier au complet dans MediaWiki:Common.js.
  POUR L'ENLEVER : supprimer ce bloc. Rien d'autre n'est modifié.
  ===================================================================== */
/* ---------- 1/4 vocabulaire ---------- */
// Vocabulaire de l'assistant microguide.
//
// GLOSSAIRE : français québécois, abréviations cliniques (atb, tx = traitement,
// rx = médicaments, dx = diagnostic, pip-tazo, vanco…), et formes latines ou
// françaises des genres (le wiki mélange « Entérocoques » et « Enterococcus »)
// -> mots réellement écrits dans le wiki. Appliqué sans IA, donc sans erreur.
//
// INTENTIONS : quelle SORTE de renseignement la question demande, pour
// privilégier la bonne section (« quoi donner » -> Traitements, et le
// tableau d'antibiogramme qui va avec).
window.MICROGUIDE_GLOSSAIRE = {
  "atb": [
    "antibiotique",
    "antibiotiques"
  ],
  "abx": [
    "antibiotique",
    "antibiotiques"
  ],
  "anti biotique": [
    "antibiotique"
  ],
  "traiter": [
    "traitement",
    "traitements"
  ],
  "traite": [
    "traitement"
  ],
  "soigner": [
    "traitement"
  ],
  "donner": [
    "traitement"
  ],
  "prescrire": [
    "traitement"
  ],
  "couvrir": [
    "couverture",
    "traitement"
  ],
  "couvre": [
    "couverture"
  ],
  "tuer": [
    "traitement",
    "sensibilite"
  ],
  "marche": [
    "efficace",
    "traitement"
  ],
  "fonctionne": [
    "efficace",
    "traitement"
  ],
  "pogner": [
    "infection",
    "acquisition"
  ],
  "attraper": [
    "infection",
    "acquisition"
  ],
  "poumon": [
    "pulmonaire",
    "pneumonie"
  ],
  "poumons": [
    "pulmonaire",
    "pneumonie"
  ],
  "pipi": [
    "urinaire"
  ],
  "urine": [
    "urinaire"
  ],
  "vessie": [
    "cystite",
    "urinaire"
  ],
  "rein": [
    "pyelonephrite",
    "urinaire"
  ],
  "reins": [
    "pyelonephrite",
    "urinaire"
  ],
  "sang": [
    "bacteriemie",
    "sepsis",
    "hemoculture"
  ],
  "coeur": [
    "endocardite"
  ],
  "peau": [
    "cutanee",
    "tissus mous",
    "cellulite"
  ],
  "os": [
    "osteomyelite",
    "osteoarticulaire"
  ],
  "cerveau": [
    "meningite"
  ],
  "ventre": [
    "intra abdominale",
    "digestif"
  ],
  "diarrhee": [
    "digestif",
    "colite"
  ],
  "oreille": [
    "otite"
  ],
  "gorge": [
    "pharyngite",
    "orl"
  ],
  "yeux": [
    "keratite",
    "ophtalmologique"
  ],
  "oeil": [
    "keratite",
    "ophtalmologique"
  ],
  "bibitte": [
    "bacterie"
  ],
  "germe": [
    "bacterie"
  ],
  "microbe": [
    "bacterie"
  ],
  "resistant": [
    "resistance",
    "resistances"
  ],
  "resiste": [
    "resistance",
    "resistances"
  ],
  "dose": [
    "posologie",
    "dose"
  ],
  "poso": [
    "posologie"
  ],
  "grossesse": [
    "grossesse",
    "enceinte"
  ],
  "enceinte": [
    "grossesse"
  ],
  "allergie": [
    "allergie",
    "reaction",
    "indesirable"
  ],
  "penicilline": [
    "penicilline",
    "penicillines"
  ],
  "pseudo": [
    "pseudomonas"
  ],
  "staph": [
    "staphylococcus",
    "staphyloccocus"
  ],
  "strep": [
    "streptococcus",
    "streptocoque"
  ],
  "pneumo": [
    "pneumoniae",
    "pneumocoque"
  ],
  // Thomas a supprimé la page SARM le 21 oct. 2026 : tout est maintenant
  // dans la page du Staphylococcus aureus, section « Spécificité du SARM ».
  "sarm": [
    "sarm",
    "staphylococcus",
    "aureus",
    "meticilline",
    "resistant"
  ],
  "sasm": [
    "staphylococcus",
    "aureus",
    "sasm"
  ],
  "e coli": [
    "escherichia",
    "coli"
  ],
  "ecoli": [
    "escherichia",
    "coli"
  ],
  "c diff": [
    "clostridioides",
    "difficile"
  ],
  "cdiff": [
    "clostridioides",
    "difficile"
  ],
  "klebs": [
    "klebsiella"
  ],
  "entero": [
    "enterocoque",
    "enterococcus",
    "enterobacter"
  ],
  "bgn": [
    "gram negatif"
  ],
  "bgp": [
    "gram positif"
  ],
  "cocci": [
    "cocci",
    "coque"
  ],
  "labo": [
    "laboratoire"
  ],
  "gram": [
    "gram"
  ],
  "vanco": [
    "vancomycine"
  ],
  "cipro": [
    "ciprofloxacine"
  ],
  "levo": [
    "levofloxacine"
  ],
  "moxi": [
    "moxifloxacine"
  ],
  "ceftri": [
    "ceftriaxone"
  ],
  "cefta": [
    "ceftazidime"
  ],
  "pip tazo": [
    "piperacilline",
    "tazobactam"
  ],
  "piptazo": [
    "piperacilline",
    "tazobactam"
  ],
  "tazo": [
    "tazobactam",
    "piperacilline"
  ],
  "mero": [
    "meropeneme",
    "meropenem"
  ],
  "erta": [
    "ertapenem"
  ],
  "clinda": [
    "clindamycine"
  ],
  "flagyl": [
    "metronidazole"
  ],
  "metro": [
    "metronidazole"
  ],
  "doxy": [
    "doxycycline"
  ],
  "azithro": [
    "azithromycine"
  ],
  "clav": [
    "clavulanate",
    "amoxicilline"
  ],
  "amox": [
    "amoxicilline"
  ],
  "cloxa": [
    "cloxacilline"
  ],
  "cefaz": [
    "cefazoline",
    "cefazolin"
  ],
  "keflex": [
    "cephalexine"
  ],
  "genta": [
    "gentamicine"
  ],
  "tobra": [
    "tobramycine"
  ],
  "septra": [
    "triclotrimoxazole",
    "tmp",
    "smx"
  ],
  "tmp smx": [
    "triclotrimoxazole"
  ],
  "fkp": [
    "fibrose kystique",
    "fkp"
  ],
  "mpoc": [
    "mpoc"
  ],
  "si": [
    "soins intensifs"
  ],
  "usi": [
    "soins intensifs"
  ],
  "enterococcus faecalis": [
    "enterocoques",
    "faecalis"
  ],
  "enterococcus faecium": [
    "enterocoques",
    "faecium"
  ],
  "streptocoque": [
    "streptococcus",
    "streptocoque"
  ],
  "streptococcus": [
    "streptococcus",
    "streptocoque"
  ],
  "staphylocoque": [
    "staphylococcus",
    "staphyloccocus"
  ],
  "haemophilus": [
    "hemophilus",
    "influenzae"
  ],
  "hemophilus": [
    "hemophilus",
    "influenzae"
  ],
  "clostridioides": [
    "clostridioides",
    "difficile"
  ],
  "legionella": [
    "legionella",
    "pneumophilia"
  ],
  "stenotrophomonas": [
    "stenotrophomonas",
    "maltophilia"
  ],
  "tx": [
    "traitement",
    "traitements"
  ],
  "rx": [
    "antibiotique",
    "antibiotiques",
    "traitement"
  ],
  "dx": [
    "diagnostic",
    "laboratoire",
    "gram"
  ],
  "ddx": [
    "diagnostic",
    "laboratoire"
  ],
  "bactériémie": [
    "bacteriemie",
    "sepsis"
  ],
  "empirique": [
    "traitement",
    "empirique"
  ],
  "sensibilite": [
    "sensibilite",
    "antibiogramme"
  ],
  "antibiogramme": [
    "sensibilite",
    "taux"
  ],
  "antibios": [
    "antibiotique",
    "antibiotiques"
  ],
  "antibio": [
    "antibiotique",
    "antibiotiques"
  ],
  "quand donner": [
    "indications"
  ],
  "quoi savoir": [
    "perles",
    "cliniques"
  ],
  "prevenir": [
    "prevention"
  ],
  "attrape": [
    "epidemiologie",
    "acquisition"
  ],
  "bon choix": [
    "indications",
    "pharmacocinetique"
  ],
  "penetre": [
    "concentre",
    "diffusion"
  ],
  "bonne option": [
    "indications"
  ],
  "ca marche tu": [
    "indications",
    "efficace"
  ],
  "allergique": [
    "allergie",
    "reaction"
  ],
  "reaction": [
    "reaction",
    "indesirable",
    "effet"
  ],
  "danger": [
    "effet",
    "indesirable"
  ],
  "risque": [
    "effet",
    "indesirable",
    "risque"
  ],
  "ajuster": [
    "posologie",
    "insuffisance"
  ],
  "insuffisance renale": [
    "renale",
    "ajustement"
  ],
  "surveiller": [
    "suivi",
    "surveillance"
  ],
  "combien de temps": [
    "duree",
    "jours"
  ],
  "remplacer": [
    "alternative",
    "spectre"
  ],
  "contagieux": [
    "transmission"
  ],
  "transmet": [
    "transmission",
    "acquisition"
  ],
  "a risque": [
    "risque",
    "facteur",
    "epidemiologie"
  ],
  "compare": [
    "comparaison",
    "spectre"
  ],
  "enterocoque": [
    "enterocoques",
    "faecium",
    "faecalis"
  ],
  "enterocoques": [
    "enterocoques",
    "faecium",
    "faecalis"
  ],
  "combien de": [
    "posologie",
    "dose"
  ],
  "vient d ou": [
    "epidemiologie"
  ],
  "comprime": [
    "relais",
    "per os"
  ],
  "je donne quoi": [
    "traitement",
    "traitements"
  ],
  // ----- NOMS FRANÇAIS ET SURNOMS DES BACTÉRIES -----
  // Le wiki titre ses pages en latin; à l'hôpital on dit « le pneumocoque »,
  // « la gono », « la kleb ». Chaque valeur est un mot RÉELLEMENT écrit dans
  // le titre de la page visée : aucune interprétation, juste une traduction.
  "pneumocoque": ["streptococcus", "pneumoniae"],
  "pneumocoques": ["streptococcus", "pneumoniae"],
  "meningocoque": ["neisseria", "meningitidis"],
  "meningo": ["neisseria", "meningitidis"],
  "gonocoque": ["neisseria", "gonorrhoeae"],
  "gono": ["neisseria", "gonorrhoeae"],
  "gonorrhee": ["neisseria", "gonorrhoeae"],
  "salmonelle": ["salmonella"],
  "salmonelles": ["salmonella"],
  "typhi": ["salmonella", "typhoide"],
  "salmonellose": ["salmonella"],
  "listeriose": ["listeria", "monocytogenes"],
  "listeria": ["listeria", "monocytogenes"],
  "legionelle": ["legionella", "pneumophilia"],
  "legionellose": ["legionella", "pneumophilia"],
  "kleb": ["klebsiella"],
  "h flu": ["hemophilus", "influenzae"],
  "hflu": ["hemophilus", "influenzae"],
  "hfluenzae": ["hemophilus", "influenzae"],
  "sero": ["serologies", "serologie"],
  "seros": ["serologies", "serologie"],
  "pen g": ["penicilline"],
  "pen v": ["penicilline"],
  "peni": ["penicilline"],
  "pen": ["penicilline"],
  "h influenzae": ["hemophilus", "influenzae"],
  "nocardiose": ["nocardia"],
  "actino": ["actinomyces"],
  "colibacille": ["escherichia", "coli"],
  "staph dore": ["staphylococcus", "aureus"],
  "scn": ["staphylococcus", "coagulase", "negative"],
  "coag neg": ["staphylococcus", "coagulase", "negative"],
  "coagulase negatif": ["staphylococcus", "coagulase", "negative"],
  "diphterie": ["corynebacterium", "diphtheriae"],
  "corynebacterie": ["corynebacterium"],
  "charbon": ["bacillus", "anthracis"],
  "pyo": ["pseudomonas", "aeruginosa"],
  "steno": ["stenotrophomonas", "maltophilia"],
  // Abréviations et formes françaises vues dans les questions Kaggle du 28 oct. 2026.
  "stenotro": ["stenotrophomonas", "maltophilia"],
  "pseu": ["pseudomonas"],
  "aeru": ["pseudomonas", "aeruginosa"],
  "p aeru": ["pseudomonas", "aeruginosa"],
  "legio": ["legionella", "pneumophilia"],
  "pneumophila": ["legionella", "pneumophilia"],
  "salmonelle": ["salmonella"],
  "salmonelles": ["salmonella"],
  "typhi": ["salmonella", "typhoide"],
  "clox": ["cloxacilline"],
  "cefa": ["cefazoline"],
  "c diff": ["clostridioides", "difficile"],
  "dacd": ["clostridioides", "difficile"],
  "viridans": ["viridans"],
  // ----- NOMS COMMERCIAUX ET ABRÉVIATIONS DE MÉDICAMENTS -----
  "ancef": ["cefazoline"],
  "rocephin": ["ceftriaxone"],
  "fortaz": ["ceftazidime"],
  "maxipime": ["cefepime"],
  "tazocin": ["piperacilline", "tazobactam"],
  "zosyn": ["piperacilline", "tazobactam"],
  "zithromax": ["azithromycine"],
  "levaquin": ["levofloxacine"],
  "cleocin": ["clindamycine"],
  "dapto": ["daptomycine"],
  "zyvox": ["linezolide"],
  "macrobid": ["nitrofurantoine"],
  "nitro": ["nitrofurantoine"],
  "tige": ["tigecycline"],
  "ampi": ["ampicilline"],
  "cefox": ["cefoxitine"],
  "cefuro": ["cefuroxime"],
  "imi": ["imipenem"],
  "norflo": ["norfloxacine"],
  "fluco": ["fluconazole"],
  "rifampin": ["rifampicine"],
  "inh": ["isoniazide"],
  "pza": ["pyrazinamide"],
  "emb": ["ethambutol"],
  "acyclo": ["acyclovir"],
  "valtrex": ["valacyclovir"],
  "tamiflu": ["oseltamivir"],
  "famvir": ["famciclovir"],
  "pnc": ["penicilline"],
  // ----- CONCEPTS CLINIQUES : MES MOTS -> CEUX DU WIKI -----
  // Le glossaire traduisait les noms (pneumocoque -> Streptococcus
  // pneumoniae). Il ne traduisait pas ce qu'on DEMANDE : « est-ce que ça
  // passe dans le LCR ? » alors que la page écrit « méninges », « à jeun »
  // pour « avec de la nourriture », « en IR » pour « clairance ».
  // « hémoculture positive » : ce que le wiki écrit, c'est « bactériémie ».
  // Pluriels des mots de prélèvement : le résident dit « dans les urines »,
  // « dans les selles », le wiki écrit « urinaire », « digestif ».
  "urines": ["urinaire", "urinaires"],
  "selles": ["digestif", "colite", "diarrhee"],
  "expectorations": ["pulmonaire", "pneumonie", "respiratoire"],
  "plaies": ["cutanee", "tissus mous", "cellulite"],
  "hemoculture": ["bacteriemie", "sepsis", "hemoculture"],
  "hemocultures": ["bacteriemie", "sepsis", "hemoculture"],
  "hemoculture positive": ["bacteriemie", "sepsis"],
  "ddx": ["diagnostic", "laboratoire"],
  "lcr": ["meninges", "meningee", "snc", "meningite"],
  "liquide cephalorachidien": ["meninges", "snc"],
  "a jeun": ["nourriture", "repas", "alimentation", "biodisponibilite"],
  "jeun": ["nourriture", "repas", "alimentation"],
  "avec la bouffe": ["nourriture", "repas"],
  "manger": ["nourriture", "repas"],
  "ir": ["clairance", "renale", "insuffisance renale"],
  "dialyse": ["hemodialyse", "clairance", "posologie"],
  "dfg": ["clairance", "renale"],
  "hemoc": ["hemoculture", "hemocultures"],
  "hemocs": ["hemoculture", "hemocultures"],
  "switch": ["relais", "per os"],
  "step down": ["relais", "per os"],
  "premiere ligne": ["premier choix", "indications", "spectre"],
  "1ere ligne": ["premier choix", "indications", "spectre"],
  "premiere intention": ["premier choix", "indications"],
  "pour les oreilles": ["ototoxicite", "ototoxique"],
  "ototox": ["ototoxicite", "ototoxique"],
  "nephrotox": ["nephrotoxicite", "nephrotoxique"],
  "pour les reins": ["nephrotoxicite", "nephrotoxique", "renale"],
  "pour le foie": ["hepatique", "hepatite", "hepatotoxicite"],
  "foie": ["hepatique", "hepatite"],
  "chez l enfant": ["pediatrique", "enfant", "enfants"],
  "chez le vieux": ["age", "agee", "personne agee"],
  "chez la personne agee": ["age", "agee"],
  // ----- MÉCANISMES DE RÉSISTANCE -----
  // Le wiki leur consacre une page entière, « Les résistances aux
  // antibiotiques ». Son titre ne contient aucun de ces sigles : sans cette
  // table, une question sur une BLSE ou une KPC ne trouve pas sa page.
  "esbl": ["resistances", "antibiotiques"],
  "blse": ["resistances", "antibiotiques"],
  "ampc": ["resistances", "antibiotiques"],
  "kpc": ["resistances", "antibiotiques"],
  "ndm": ["resistances", "antibiotiques"],
  "oxa": ["resistances", "antibiotiques"],
  "carbapenemase": ["resistances", "antibiotiques"],
  "carbapenemases": ["resistances", "antibiotiques"],
  "penicillinase": ["resistances", "antibiotiques"],
  "penicillinases": ["resistances", "antibiotiques"],
  "cephalosporinase": ["resistances", "antibiotiques"],
  "efflux": ["resistances", "antibiotiques"],
  "space": ["resistances", "antibiotiques"],
  "spice": ["resistances", "antibiotiques"],
  "pekk": ["resistances", "antibiotiques"],
  "pnc g": ["penicilline"],
  "peni": ["penicilline"],
  "peni g": ["penicilline"]
};
window.MICROGUIDE_INTENTIONS = {
  "traitement": {
    "declencheurs": [
      "traiter",
      "traite",
      "traitement",
      "atb",
      "abx",
      "antibiotique",
      "antibiotiques",
      "soigner",
      "donner",
      "prescrire",
      "couvrir",
      "couverture",
      "couvre",
      "couvert",
      "efficace",
      "quoi donner",
      "tuer",
      "marche",
      "fonctionne",
      "choix",
      "empirique",
      "per os",
      "po",
      "iv",
      "tx",
      "rx",
      "antibiogramme",
      "sensibilite",
      "taux",
      "antibios",
      "antibio",
      "quand donner",
      "je donne quoi",
      "patient avec",
      "on donne quoi",
      "je mets quoi",
      "je commence quoi"
    ],
    "titres": [
      "traitement",
      "traitements",
      "efficace",
      "efficaces",
      "spectre",
      "couverture",
      "sensibilite",
      "posologie",
      "taux",
      "antibiogramme"
    ]
  },
  "resistance": {
    "declencheurs": [
      "resistance",
      "resistances",
      "resistant",
      "resiste",
      "mecanisme",
      "mecanismes",
      "blse",
      "carbapenemase",
      "sarm",
      "ervr",
      "resiste",
      "resistent",
      "resistance",
      "mecanisme"
    ],
    "titres": [
      "resistance",
      "resistances",
      "mecanisme",
      "mecanismes"
    ]
  },
  "clinique": {
    "declencheurs": [
      "infection",
      "infections",
      "cause",
      "causes",
      "clinique",
      "maladie",
      "symptome",
      "symptomes",
      "manifestation",
      "manifestations",
      "tableau",
      "pneumonie",
      "cellulite",
      "endocardite",
      "meningite",
      "urinaire",
      "osteomyelite",
      "abces",
      "bacteriemie",
      "sepsis",
      "otite",
      "keratite"
    ],
    "titres": [
      "infection",
      "infections",
      "clinique",
      "manifestations",
      "principales"
    ]
  },
  "epidemiologie": {
    "declencheurs": [
      "epidemiologie",
      "reservoir",
      "ou vit",
      "transmission",
      "attraper",
      "pogner",
      "acquisition",
      "colonisation",
      "colonisant",
      "risque",
      "nosocomial",
      "nosocomiale",
      "communautaire",
      "porteur",
      "attrape",
      "attrapent",
      "pogne",
      "ou vit",
      "reservoir",
      "vit",
      "transmet",
      "transmis",
      "transmission",
      "contagieux",
      "contagion",
      "propage",
      "population",
      "populations",
      "terrain",
      "facteur",
      "facteurs",
      "porteur",
      "colonise",
      "vient d ou",
      "vient dou",
      "provient",
      "ou le trouve",
      "habitat",
      "niche"
    ],
    "titres": [
      "epidemiologie",
      "reservoir",
      "transmission",
      "acquisition",
      "specificite",
      "specificite",
      "facteur"
    ]
  },
  "laboratoire": {
    "declencheurs": [
      "laboratoire",
      "labo",
      "identification",
      "identifier",
      "culture",
      "gram",
      "microscope",
      "coloration",
      "catalase",
      "coagulase",
      "oxydase",
      "lactose",
      "milieu",
      "gelose",
      "hemoculture",
      "tsi",
      "dx",
      "ddx"
    ],
    "titres": [
      "laboratoire",
      "laboratoires",
      "microscope",
      "identification",
      "culture",
      "caracteristiques",
      "tests"
    ]
  },
  "indications": {
    "declencheurs": [
      "indication",
      "indications",
      "quand donner",
      "quand utiliser",
      "dans quel cas",
      "quand prescrire"
    ],
    "titres": [
      "indication",
      "indications"
    ]
  },
  "prevention": {
    "declencheurs": [
      "prevenir",
      "prevention",
      "vaccin",
      "prophylaxie",
      "eviter"
    ],
    "titres": [
      "prevention",
      "prophylaxie"
    ]
  },
  "perles": {
    "declencheurs": [
      "perle",
      "perles",
      "quoi savoir",
      "a savoir",
      "piege",
      "truc",
      "particularite"
    ],
    "titres": [
      "perle",
      "perles",
      "particularite"
    ]
  },
  "distribution": {
    "declencheurs": [
      "urinaire",
      "urinaires",
      "urine",
      "vessie",
      "rein",
      "renale",
      "lcr",
      "meninge",
      "meninges",
      "cerebral",
      "snc",
      "os",
      "osseux",
      "osteo",
      "articulaire",
      "prostate",
      "prostatique",
      "poumon",
      "pulmonaire",
      "peau",
      "cutane",
      "tissu",
      "tissus",
      "biliaire",
      "oculaire",
      "oeil",
      "placenta",
      "lait",
      "penetre",
      "penetration",
      "concentre",
      "concentration",
      "diffuse",
      "diffusion",
      "bon choix",
      "bonne option",
      "efficace pour"
    ],
    "titres": [
      "pharmacocinet",
      "biodisponibilit",
      "perle",
      "particularite",
      "indication"
    ]
  },
  "effets": {
    "declencheurs": [
      "effet",
      "effets",
      "secondaire",
      "secondaires",
      "indesirable",
      "indesirables",
      "reaction",
      "reactions",
      "allergie",
      "allergique",
      "allergies",
      "intolerance",
      "toxicite",
      "toxique",
      "eruption",
      "rash",
      "anaphylaxie",
      "nausee",
      "nausees",
      "diarrhee",
      "vomissement",
      "phlebite",
      "irritation",
      "hepatique",
      "renale",
      "nephrotoxicite",
      "ototoxicite",
      "qt",
      // ce qu'on dit à l'hôpital pour désigner une toxicité d'organe :
      // « c'est-tu dur pour le foie ? », « ça brûle-tu les oreilles ? »
      "foie",
      "hepatotoxicite",
      "hepatite",
      "oreille",
      "oreilles",
      "surdite",
      "acouphene",
      "vestibulaire",
      "ototoxique",
      "nephrotoxique",
      "tendinite",
      "risque",
      "danger",
      "complication"
    ],
    "titres": [
      "effet",
      "indesirable",
      "secondaire",
      "suivi",
      "perle"
    ]
  },
  "ajustement": {
    "declencheurs": [
      "ajuster",
      "ajustement",
      "insuffisance",
      "renale",
      "renal",
      "clairance",
      "dialyse",
      "hemodialyse",
      "cockcroft",
      "dfg"
    ],
    "titres": [
      "posologie",
      "perle",
      "pharmacocinet"
    ]
  },
  "surveillance": {
    "declencheurs": [
      "surveiller",
      "surveillance",
      "suivi",
      "suivis",
      "doser",
      "dosage",
      "taux",
      "monitorer",
      "controler",
      "bilan",
      "fsc",
      "creatinine"
    ],
    "titres": [
      "effet",
      "suivi",
      "perle",
      "posologie"
    ]
  },
  "duree": {
    "declencheurs": [
      "duree",
      "combien de temps",
      "combien de jours",
      "jusqu a quand",
      "pendant combien"
    ],
    "titres": [
      "posologie",
      "indication",
      "perle"
    ]
  },
  "interaction": {
    "declencheurs": [
      "interaction",
      "interactions",
      "inr",
      "warfarine",
      "coumadin",
      "associer",
      "incompatib"
    ],
    "titres": [
      "interaction",
      "perle"
    ]
  },
  "alternative": {
    "declencheurs": [
      "remplacer",
      "alternative",
      "alternatives",
      "si allergie",
      "a la place",
      "autre option",
      "substitut"
    ],
    "titres": [
      "spectre",
      "indication",
      "perle",
      "traitement"
    ]
  },
  "comparaison": {
    "declencheurs": [
      "compare",
      "comparaison",
      "par rapport",
      "avantage",
      "difference",
      "mieux que",
      "contrairement",
      "versus",
      "plutot que"
    ],
    "titres": [
      "spectre",
      "perle",
      "indication",
      "particularite"
    ]
  },
  "posologie": {
    "declencheurs": [
      "posologie",
      "poso",
      "dose",
      "doses",
      "combien de",
      "combien je",
      "mg",
      "grammes",
      "par jour",
      "q8h",
      "q12h",
      "die",
      "bid",
      "tid"
    ],
    "titres": [
      "posologie",
      "dose"
    ]
  },
  "relais": {
    "declencheurs": [
      "comprime",
      "comprimes",
      "capsule",
      "capsules",
      "par la bouche",
      "per os",
      "relais",
      "po",
      "orale",
      "oral"
    ],
    "titres": [
      "relais",
      "per os",
      "pharmacocinet",
      "biodisponibilit"
    ]
  }
};
/* ---------- 2/4 construction de l'index ---------- */
// Construit l'index de microguide.ca DANS LE NAVIGATEUR, à partir de l'API
// du wiki. Aucun fichier à maintenir : l'index se refait tout seul, donc il
// est toujours à jour avec le wiki.
//
// Deux appels suffisent : le contenu de toutes les pages, puis l'URL des
// images (~130 Ko). L'index est gardé dans le navigateur et reconstruit
// dès que le wiki change, donc il suit les modifications du site.
(function (root) {
  "use strict";
  var MOTS_VIDES = ("a au aux avec ce ces dans de des du elle en et eux il ils je la le les leur lui ma mais " +
    "me meme mes moi mon ne nos notre nous on ou par pas pour qu que qui sa se ses son sur ta te tes toi ton " +
    "tu un une vos votre vous y est sont etre ete suis es sommes etes ont ai as avons avez avait etait sera " +
    "seront c d j l m n s t quel quelle quels quelles quoi comment pourquoi quand combien cet cette plus " +
    "moins tres bien aussi alors donc car si comme tout tous toute toutes autre autres faire fait fais peut " +
    "peux peuvent doit dois doivent").split(" ");
  function normalise(s) {
    return (s || "").toLowerCase().normalize("NFD")
      .replace(/[̀-ͯ]/g, "")
      .replace(/[-'’/]/g, " ");
  }
  var VIDES = new Set(MOTS_VIDES);
  function mots(s) {
    // « groupe A » / « groupe B » : la lettre seule serait jetée (un seul
    // caractère), et les streptocoques A, B, C deviendraient identiques.
    // « groupe C et G » : la deuxième lettre se perdait, et la page des
    // streptocoques C et G devenait introuvable par « groupe G ».
    var t = normalise(s)
      .replace(/\bgroupe\s+([a-g])\s+et\s+([a-g])\b/g, "groupe$1 groupe$2")
      .replace(/\bsg([abcg])\b/g, "groupe$1")
      .replace(/\bgroupe\s+([a-g])\b/g, "groupe$1")
      .replace(/\b(strep|streptocoque|streptocoques|streptococcus)\s+([abcg])\b/g, "$1 groupe$2");
    return (t.match(/[a-z0-9]+/g) || []).filter(function (w) {
      return w.length > 1 && !VIDES.has(w);
    });
  }
  // ------------------------------------------------ nettoyage du wikitext


     /* Observer pour les changements dynamiques */
  function cellules(bloc) {
     const observer = new MutationObserver(function () {
     var out = [];
        document.querySelectorAll('[hidden="until-found"], [hidden]').forEach(function (el) {
     bloc.split("\n").forEach(function (l) {
            el.removeAttribute('hidden');
      l = l.trim();
      if (l[0] === "!" || l[0] === "|") {
        l = l.replace(/^[!|]+/, "");
        if (l.indexOf("|") >= 0 && l.indexOf("||") < 0) l = l.replace(/^[^|]*\|(?!\|)/, "");
        l.split("||").forEach(function (c) {
          c = c.replace(/^[a-z\-]+\s*=\s*"[^"]*"\s*\|/, "").trim();
          if (c && !/^[-\s|]*$/.test(c)) out.push(c);
         });
         });
      }
    });
    return out.join("\n");
  }
  function nettoyer(t) {
    t = t.replace(/<!--[\s\S]*?-->/g, "");
    t = t.replace(/<ref[^>]*\/>/g, "").replace(/<ref[\s\S]*?<\/ref>/g, "");
    for (var i = 0; i < 5; i++) {
      var t2 = t.replace(/\{\{[^{}]*\}\}/g, "");
      if (t2 === t) break;
      t = t2;
    }
    t = t.replace(/\{\|[\s\S]*?\|\}/g, function (m) { return cellules(m); });
    t = t.replace(/\[\[(?:Fichier|File|Image)\s*:[^\]]*\]\]/gi, "");
    t = t.replace(/\[\[([^\]|]*)\|([^\]]*)\]\]/g, "$2").replace(/\[\[([^\]]*)\]\]/g, "$1");
    // (les liens internes restent du texte : la page citée est déjà donnée)
    // Les liens externes : on GARDE l'adresse. Avant, « [url texte] » ne
    // gardait que « texte » et le lien disparaissait de la réponse.
    t = t.replace(/\[(https?:\/\/\S+)\s+([^\]]*)\]/g, "$2 ($1)")
        .replace(/\[(https?:\/\/\S+)\]/g, "$1");
    t = t.replace(/<br\s*\/?>/gi, "\n");
    t = t.replace(/<\/?(div|span|p|big|small|center|u|b|i|sub|sup|font|blockquote)[^>]*>/gi, "");
    t = t.replace(/<[^>]+>/g, " ");
    t = t.replace(/'''/g, "").replace(/''/g, "");
    t = t.replace(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">");
    t = t.replace(/[ \t]+/g, " ").replace(/\n\s*\n\s*\n+/g, "\n\n");
    return t.trim();
  }
  // ------------------------------------------------ découpage en sections
  function decouper(wikitext) {
    var lignes = wikitext.split("\n");
    var sections = [], cur = { titre: "", lignes: [] };
    lignes.forEach(function (ligne) {
      var m = ligne.match(/^\s*(={2,6})\s*(.+?)\s*\1\s*$/);
      if (m) {
        if (cur.lignes.length) sections.push(cur);
        var brut = m[2];
        // un titre qui n'est qu'une image n'est pas un titre
        if (/^\s*\[\[(?:Fichier|File|Image)\s*:/i.test(brut)) brut = "";
        cur = { titre: nettoyer(brut) || brut.replace(/^['= ]+|['= ]+$/g, ""), lignes: [] };
      } else {
        cur.lignes.push(ligne);
      }
    });
    if (cur.lignes.length) sections.push(cur);
    return sections.map(function (s) {
      var brut = s.lignes.join("\n");
      var imgs = [];
      var re = /\[\[(?:Fichier|File|Image)\s*:\s*([^|\]]+)/gi, m;
      while ((m = re.exec(brut))) imgs.push(m[1].trim().replace(/_/g, " "));
      var texte = nettoyer(brut);
      var puces = [], prose = [];
      texte.split("\n").forEach(function (l) {
        l = l.trim();
        if (!l) return;
        if (/^[\*\#•·–-]\s*/.test(l)) puces.push(l.replace(/^[\*\#•·–-]+\s*/, "").trim());
        else prose.push(l);
      });
      return { titre: s.titre, puces: puces, prose: prose, images: imgs };
    }).filter(function (s) {
      return s.puces.length || s.prose.length || s.images.length;
    });
  }
  // ------------------------------------------------ appels à l'API
  function api(params, base) {
    var u = (base || "") + "/api.php?origin=*&format=json&formatversion=2&" +
      Object.keys(params).map(function (k) {
        return encodeURIComponent(k) + "=" + encodeURIComponent(params[k]);
      }).join("&");
    return fetch(u, { credentials: "omit" }).then(function (r) {
      if (!r.ok) throw new Error("API " + r.status);
      return r.json();
     });
     });
     observer.observe(document.body, { childList: true, subtree: true });
  }
});
 
  function toutesLesPages(base) {
    var pages = [];
    function tour(cont) {
      var p = {
        action: "query", generator: "allpages", gapnamespace: 0, gaplimit: "max",
        prop: "revisions", rvprop: "content|timestamp", rvslots: "main"
      };
      if (cont) for (var k in cont) p[k] = cont[k];
      return api(p, base).then(function (d) {
        ((d.query && d.query.pages) || []).forEach(function (pg) {
          var rev = pg.revisions && pg.revisions[0];
          var txt = rev && rev.slots && rev.slots.main && rev.slots.main.content;
          if (txt) pages.push({ titre: pg.title, wikitext: txt });
        });
        if (d.continue) return tour(d.continue);
        return pages;
      });
    }
    return tour(null);
  }
 
  // Les doses du calculateur de Thomas vivent dans MediaWiki:Common.js,
  // dans un objet ANTIBIOS : classe -> medicament -> paliers selon la
  // clairance renale. On les recupere pour pouvoir repondre aux questions
  // de posologie que le texte des pages ne couvre pas.
  function dosesDuCalculateur(base) {
    return api({ action: "query", titles: "MediaWiki:Common.js",
                prop: "revisions", rvprop: "content", rvslots: "main" }, base)
      .then(function (d) {
        var pg = d.query && d.query.pages && d.query.pages[0];
        var txt = pg && pg.revisions && pg.revisions[0].slots.main.content;
        if (!txt) return {};
        var i = txt.indexOf("var ANTIBIOS");
        if (i < 0) return {};
        var fin = txt.indexOf("/* ====== DEBUT Assistant Microguide");
        var zone = txt.slice(i, fin > i ? fin : txt.length);
        var out = {};
        // « Nom (voie) »: { notes: "...", doses: [ {min,max,label}, ... ] }
        // Le crochet fermant cherché est celui qui ferme la LISTE, pas celui
        // d'un libellé : Thomas écrit « 3,375g [3g/0,375g] IV q6h », et un
        // crochet non gourmand s'arrêtait au milieu du libellé. Résultat :
        // toute la pipéracilline-tazobactam disparaissait du calculateur.
        // On exige donc le « ] } » qui ferme la liste et l'objet.
        var re = /"([^"]{3,60})"\s*:\s*\{\s*notes\s*:\s*"([^"]*)"\s*,\s*doses\s*:\s*\[([\s\S]*?)\]\s*\}/g, m;
        while ((m = re.exec(zone))) {
          var nom = m[1], note = m[2], corps = m[3];
          var paliers = [], rl = /min\s*:\s*(\d+)\s*,\s*max\s*:\s*(\d+)\s*,\s*label\s*:\s*"([^"]+)"/g, r2;
          while ((r2 = rl.exec(corps))) {
            paliers.push({ min: +r2[1], max: +r2[2], label: r2[3] });
          }
          if (paliers.length) out[nom] = { note: note, paliers: paliers };
        }
        return out;
      })["catch"](function () { return {}; });
  }
 
  function toutesLesImages(base) {
    var map = {};
    function tour(cont) {
      var p = { action: "query", list: "allimages", ailimit: "max" };
      if (cont) for (var k in cont) p[k] = cont[k];
      return api(p, base).then(function (d) {
        ((d.query && d.query.allimages) || []).forEach(function (im) {
          map[im.name.replace(/_/g, " ")] = im.url;
        });
        if (d.continue) return tour(d.continue);
        return map;
      });
    }
    return tour(null);
  }
 
  // ------------------------------------------------ assemblage de l'index
 
  function assembler(pagesBrutes, imgMap, base, doses) {
    doses = doses || {};
    var pages = [], sections = [], vus = {};
 
    // Le wiki contient des redirections et des pages-coquilles (titres mal
    // orthographiés, pages vides) qui, sans ça, passent devant les vraies.
    pagesBrutes = pagesBrutes.filter(function (p) {
      if (/^\s*#\s*REDIRECT/i.test(p.wikitext)) return false;
      return nettoyer(p.wikitext).replace(/\s/g, "").length >= 80;
    });
    // à titre égal, on garde la page la plus fournie
    pagesBrutes.sort(function (a, b) { return b.wikitext.length - a.wikitext.length; });
 
    pagesBrutes.forEach(function (p) {
      if (vus[p.titre]) return;            // redirections vers le même titre
      var secs = decouper(p.wikitext);
      if (!secs.length) return;
      vus[p.titre] = 1;
 
      var pid = pages.length;
      var imgsPage = [];
      secs.forEach(function (sec, k) {
        sec.images.forEach(function (n) {
          if (imgMap[n]) {
            imgsPage.push({
              nom: n, url: imgMap[n],
              section: sec.titre || p.titre,
              entete: k === 0 && !sec.titre
            });
          }
        });
      });
 
      pages.push({
        id: pid, titre: p.titre,
        url: base + "/index.php/" + encodeURI(p.titre.replace(/ /g, "_")),
        images: imgsPage
      });
 
      secs.forEach(function (s) {
        var plat = s.prose.concat(s.puces).join(" ");
        var images = s.images.filter(function (n) { return imgMap[n]; })
          .map(function (n) { return { nom: n, url: imgMap[n] }; });
        if (plat.length < 15 && !images.length) return;
        sections.push({
          page: pid, titre: s.titre || p.titre,
          puces: s.puces, prose: s.prose, images: images,
          // Le NOM DE LA PAGE est indexé dans chacune de ses sections :
          // la page « Ceftazidime » a des sections « Spectre », « Posologie »
          // qui ne contiennent jamais le mot « ceftazidime ». Sans ceci, la
          // page est introuvable par son propre nom.
          // Le titre de section compte double, pour qu'une section
          // « Couverture » se trouve même si son contenu est une image.
          mots: mots(p.titre + " " + (s.titre || "") + " " + (s.titre || "") + " " + plat)
        });
      });
    });
 
    // GRAM : la page « Bactéries » du wiki classe tout, en deux grandes
    // sections (gram positive, puis gram négative). C'est la source la plus
    // fiable : le gram de chaque fiche est dans son image d'en-tête, donc
    // illisible pour nous, alors qu'ici il est écrit noir sur blanc.
    var gramParTitre = {}, morphoParTitre = {};
    pagesBrutes.forEach(function (p) {
      if (!/^bact[eé]ries$/i.test(p.titre.trim())) return;
      var coupe = p.wikitext.search(/gram\s*n[ée]gativ/i);
      var parts = coupe < 0 ? [p.wikitext] : [p.wikitext.slice(0, coupe), p.wikitext.slice(coupe)];
      parts.forEach(function (bout, k) {
        var g = (parts.length === 2 && k === 1) ? "neg" : "pos";
        // On parcourt le texte dans l'ordre : chaque titre met à jour la
        // morphologie courante, chaque lien hérite de la dernière vue.
        var morpho = null;
        var re = /(^|\n)\s*=+([^\n=][^\n]*?)=+\s*(?=\n|$)|\[\[([^\]|#]+)/g, m;
        while ((m = re.exec(bout))) {
          if (m[2] !== undefined) {
            var th = m[2].toLowerCase();
            if (/coccobacill/.test(th)) morpho = "coccobacille";
            else if (/cocci|coque/.test(th)) morpho = "cocci";
            else if (/bacill/.test(th)) morpho = "bacille";
          } else if (m[3]) {
            var cible = m[3].trim().replace(/_/g, " ");
            if (/^(fichier|file|image|cat[eé]gorie|category)\s*:/i.test(cible)) continue;
            gramParTitre[cible] = g;
            if (morpho) morphoParTitre[cible] = morpho;
          }
        }
      });
    });
    // CLASSE D'ANTIBIOTIQUE : la page « Antibiotiques » les range par famille
    // (beta-lactames, quinolones, glycopeptides, aminosides, lincosamides,
    // macrolides, tetracyclines). Meme principe que le Gram des bacteries.
    // PAGES DE LABORATOIRE. Elles ne sont ni des bactéries ni des
    // antibiotiques : « quel traitement pour Culture de pus » n'a aucun sens.
    // On les reconnaît comme les antibiotiques, par ce que la page
    // d'aiguillage « Tests de laboratoires » relie.
    var laboParTitre = {};
    pagesBrutes.forEach(function (p) {
      if (!/^tests de laboratoires?$/i.test(p.titre.trim())) return;
      var reL = /\[\[([^\]|#]+)/g, mL;
      while ((mL = reL.exec(p.wikitext))) {
        var cL = mL[1].trim().replace(/_/g, " ");
        if (/^(fichier|file|image|cat[e\u00e9]gorie|category)\s*:/i.test(cL)) continue;
        laboParTitre[cL] = true;
      }
    });
 
    var classeParTitre = {}, antibioParTitre = {};
    pagesBrutes.forEach(function (p) {
      if (!/^antibiotiques$/i.test(p.titre.trim())) return;
      var classe = null;
      var re2 = /(^|\n)\s*=+([^\n=][^\n]*?)=+\s*(?=\n|$)|\[\[([^\]|#]+)/g, m2;
      while ((m2 = re2.exec(p.wikitext))) {
        if (m2[2] !== undefined) {
          var tc = m2[2].toLowerCase().replace(/<[^>]+>/g, "").replace(/['\[\]]/g, "").trim();
          if (/lactam|penicill|cephalo|carbapen/.test(tc)) classe = "beta-lactamine";
          else if (/quinolon/.test(tc)) classe = "quinolone";
          else if (/glycopeptid/.test(tc)) classe = "glycopeptide";
          else if (/aminosid|aminoglycosid/.test(tc)) classe = "aminoside";
          else if (/lincosamid/.test(tc)) classe = "lincosamide";
          else if (/macrolid/.test(tc)) classe = "macrolide";
          else if (/tetracyclin|cyclin/.test(tc)) classe = "tetracycline";
          else if (/imidazol|metro/.test(tc)) classe = "nitroimidazole";
          else classe = null;
        } else if (m2[3]) {
          var c2 = m2[3].trim().replace(/_/g, " ");
          if (/^(fichier|file|image|cat[e\u00e9]gorie|category)\s*:/i.test(c2)) continue;
          // tout ce qui est lie depuis la page Antibiotiques EST un antibiotique
          antibioParTitre[c2] = true;
          if (classe) classeParTitre[c2] = classe;
        }
      }
    });
 
    pages.forEach(function (p) {
      if (gramParTitre[p.titre]) p.gram = gramParTitre[p.titre];
      if (morphoParTitre[p.titre]) p.morpho = morphoParTitre[p.titre];
      if (classeParTitre[p.titre]) p.classe = classeParTitre[p.titre];
      // doses du calculateur : on relie « Cefadroxil (PO) » a la page qui
      // porte ce nom, en comparant les mots significatifs.
      var cles = mots(p.titre);
      for (var dn in doses) {
        var md = mots(dn.replace(/\([^)]*\)/g, "").split(/\s*[\u2014\u2013]\s*/)[0]);
        if (!md.length) continue;
        // correspondance par radical : « meropenem » doit rejoindre
        // « meropeneme », « cefazolin » rejoindre « cefazoline »
        var communs = md.filter(function (w) {
          return cles.some(function (c) {
            if (c === w) return true;
            var court = Math.min(c.length, w.length);
            return court >= 6 && c.slice(0, court) === w.slice(0, court);
          });
        });
        if (communs.length) {
          (p.doses = p.doses || []).push({ nom: dn, note: doses[dn].note, paliers: doses[dn].paliers });
        }
      }
      // Une page de laboratoire ne doit pas être confondue avec les pages
      // qu'elle cite au passage : la page d'aiguillage renvoie aussi vers
      // « Bactéries », « Antibiotiques » ou « Clostridioides difficile ».
      // Une page qui a une section d'infections, d'épidémiologie ou de
      // résistance est une bactérie, pas un test.
      var txtP = p.wikitext || "";
      var estBact = /Infections principales|[Ée]pid[eé]miologie|M[eé]canismes? de r[eé]sistance|Traitements? g[eé]n[eé]ralement/i.test(txtP);
      var estNav = /^(bact[eé]ries|antibiotiques|classification|accueil|infections|calculateur|les r[eé]sistances aux antibiotiques)$/i
                    .test(p.titre.trim());
      if ((laboParTitre[p.titre] || /^tests de laboratoires?$/i.test(p.titre.trim())) &&
          !antibioParTitre[p.titre] && !estBact && !estNav && !p.gram) p.labo = true;
      if (antibioParTitre[p.titre] || /^(les |l')?(p[e\u00e9]nicillines?|c[e\u00e9]phalosporines?|macrolides?|quinolones?|aminosides?|carbap[e\u00e9]n[e\u00e8]mes?|t[e\u00e9]tracyclines?)/i.test(p.titre.trim())) p.antibio = true;
    });
 
    // Le calculateur contient ~70 medicaments, dont plusieurs n'ont PAS de
    // page sur le wiki (ampicilline, rifampicine...). Leur dose existe
    // pourtant : on en fait une entree a part entiere, qui renvoie vers la
    // page Calculateur, plutot que de repondre « pas l'information ».
    var dejaRattachees = {};
    pages.forEach(function (p) {
      (p.doses || []).forEach(function (d) { dejaRattachees[d.nom] = 1; });
    });
    var urlCalc = base + "/index.php/Calculateur";
    Object.keys(doses).forEach(function (nomD) {
      if (dejaRattachees[nomD]) return;
      var propre = nomD.replace(/\s*\([^)]*\)\s*/g, "")
                      .split(/\s*[\u2014\u2013]\s*/)[0]          // qualificatif apres un tiret long
                      .trim();
      if (!propre) return;
      // une seule entree par medicament, meme s'il a plusieurs voies
      var dejaId = -1;
      for (var q = 0; q < pages.length; q++) {
        if (pages[q].calcNom === propre) { dejaId = q; break; }
      }
      if (dejaId >= 0) {
        pages[dejaId].doses.push({ nom: nomD, note: doses[nomD].note, paliers: doses[nomD].paliers });
        return;
      }
      var pid = pages.length;
      pages.push({ id: pid, titre: propre, url: urlCalc, images: [], calcNom: propre,
                  doses: [{ nom: nomD, note: doses[nomD].note, paliers: doses[nomD].paliers }] });
    });
    // une section indexable pour chaque medicament du calculateur
    pages.forEach(function (p) {
      if (!p.calcNom) return;
      var lignes = [];
      p.doses.forEach(function (d) {
        d.paliers.forEach(function (pal) {
          var plage = pal.max >= 999 ? "clairance \u2265 " + pal.min
                    : pal.min <= 0 ? "clairance < " + (pal.max + 1)
                    : "clairance " + pal.min + " \u00e0 " + pal.max;
          lignes.push(d.nom + " \u2014 " + plage + " mL/min : " + pal.label);
        });
        if (d.note) lignes.push(d.note);
      });
      sections.push({ page: p.id, titre: "Posologie (calculateur de doses)",
        prose: [], puces: lignes, images: [],
        mots: mots(p.titre + " " + p.titre + " posologie dose " + lignes.join(" ")) });
    });
 
    var df = {};
    sections.forEach(function (s) {
      new Set(s.mots).forEach(function (w) { df[w] = (df[w] || 0) + 1; });
    });
    sections.forEach(function (s) {
      var tf = {};
      s.mots.forEach(function (w) { tf[w] = (tf[w] || 0) + 1; });
      s.tf = tf; s.len = s.mots.length; delete s.mots;
    });
 
    return {
      pages: pages, sections: sections, df: df, N: sections.length,
      wiki: base, mots_vides: MOTS_VIDES,
      glossaire: root.MICROGUIDE_GLOSSAIRE || {},
      intentions: root.MICROGUIDE_INTENTIONS || {},
      bati: Date.now()
    };
  }
 
  // ------------------------- cache local (validé par la date du wiki)
 
  var CLE = "microguide-index-v1";
 
  function duCache() {
    try {
      var b = localStorage.getItem(CLE);
      if (!b) return null;
      var d = JSON.parse(b);
      if (!d.bati) return null;
      d.glossaire = root.MICROGUIDE_GLOSSAIRE || {};
      d.intentions = root.MICROGUIDE_INTENTIONS || {};
      return d;
    } catch (e) { return null; }
  }
 
  function auCache(d) {
    try { localStorage.setItem(CLE, JSON.stringify(d)); } catch (e) { /* quota : tant pis */ }
  }
 
  // Date de la dernière modification du wiki (articles + fichiers).
  // Une seule petite requête (~1 Ko) : elle dit s'il faut tout relire.
  function derniereModif(base) {
    return api({
      action: "query", list: "recentchanges",
      rcprop: "timestamp", rclimit: 1, rcnamespace: "0|6", rctype: "edit|new|log"
    }, base).then(function (d) {
      var rc = d.query && d.query.recentchanges;
      return (rc && rc.length) ? rc[0].timestamp : null;
    })["catch"](function () { return null; });
  }
 
  // Construit (ou relit) l'index.
  // L'index n'est réutilisé que si le wiki n'a pas bougé depuis : une page
  // modifiée, ajoutée ou retirée est donc prise en compte à la question
  // suivante, sans rien faire de spécial.
  function charger(base) {
    base = base || "";
    var c = duCache();
    return derniereModif(base).then(function (modif) {
      if (c && modif && c.modif === modif) return c;  // wiki inchangé
      if (c && !modif) return c;                      // hors ligne : on garde
      return Promise.all([toutesLesPages(base), toutesLesImages(base), dosesDuCalculateur(base)])
        .then(function (r) {
          var ix = assembler(r[0], r[1], base, r[2]);
          ix.modif = modif;
          auCache(ix);
          return ix;
        });
    });
  }
 
  root.MicroguideIndexeur = {
    charger: charger, assembler: assembler, decouper: decouper,
    nettoyer: nettoyer, mots: mots, viderCache: function () {
      try { localStorage.removeItem(CLE); } catch (e) {}
    }
  };
})(typeof module !== "undefined" ? module.exports : window);
 
/* ---------- 3/4 moteur de réponse ---------- */
// Moteur de réponse de l'assistant microguide — AUCUNE INVENTION POSSIBLE.
//
// La réponse est composée uniquement de phrases et de puces recopiées
// telles quelles depuis les pages du wiki, avec le titre de la section et
// le lien. Il n'y a pas de modèle de langue qui réécrit : le texte affiché
// existe mot pour mot sur microguide.ca.
//
// Principe repris du bibliothécaire de Thomas : les passages sources SONT
// la réponse.
 
(function (root) {
  "use strict";
 
  function normalise(s) {
    return (s || "")
      .toLowerCase()
      .normalize("NFD")
      .replace(/[̀-ͯ]/g, "");
  }
 
  // Mots trop généraux pour désigner une page à eux seuls : sans ça,
  // « quel ATB pour une ostéomyélite » désignerait la page « Les
  // résistances aux antibiotiques » juste par le mot « antibiotique ».
  var GENERIQUES = {
    antibiotique: 1, infection: 1, bacterie: 1, traitement: 1, resistance: 1,
    laboratoire: 1, test: 1, classification: 1, couverture: 1, calculateur: 1,
    accueil: 1, principale: 1, general: 1, spectre: 1, posologie: 1,
    utilise: 1, frequemment: 1, complet: 1, autre: 1,
    // « X est résistant à quoi ? » ne doit pas désigner la page SARM
    // (« Staphylococcus aureus RÉSISTANT à la méticilline ») par ce seul mot.
    resistant: 1, intrinseque: 1, emblee: 1, groupe: 1, espece: 1,
    negative: 1, positive: 1, negatif: 1, positif: 1, gram: 1, bacille: 1,
    complexe: 1, principaux: 1, methicilline: 1,
    meticilline: 1, donner: 1, quoi: 1, pathogene: 1
  };
 
  // « groupe A », mais aussi « strep B », « SGA », « strep du groupe C » :
  // la lettre seule serait jetée (un mot d'une lettre). On la colle au mot
  // « groupe », exactement comme l'indexeur le fait sur les titres du wiki.
  function lettreDeGroupe(t) {
    return t.replace(/\bgroupe\s+([a-g])\s+et\s+([a-g])\b/g, "groupe$1 groupe$2")
            .replace(/\bsg([abcg])\b/g, "groupe$1")
            .replace(/\bgroupe\s+([a-g])\b/g, "groupe$1")
            .replace(/\b(strep|streptocoque|streptocoques|streptococcus)\s+([abcg])\b/g,
                    "$1 groupe$2");
  }
 
  // RACINE COMMUNE ENTRE LE LATIN ET LE FRANÇAIS. Le wiki titre « Salmonella »,
  // l'hôpital dit « salmonelle » ou « salmonellose »; « Nocardia » devient
  // « nocardiose », « Legionella » devient « légionelle ». Plutôt que de lister
  // ces formes une par une, on enlève la terminaison et on compare la racine :
  // ça marche pour toutes les pages, y compris celles que Thomas ajoutera.
  function racineNom(w) {
    var r = w.replace(/(ellose|iose|ella|elle|ose|ia|ie|a|e)$/, "");
    return r.length >= 5 ? r : w;
  }
  function memeNom(a, b) {
    if (a === b) return true;
    var ra = racineNom(a), rb = racineNom(b);
    return ra === rb && ra.length >= 5;
  }
 
  // TOLÉRANCE À UNE FAUTE DE FRAPPE. « péniclline », « ciprofloxcine » : une
  // lettre sautée, doublée ou interchangée ne doit pas faire perdre la page.
  // Une seule faute, et seulement sur les mots longs, pour ne pas confondre
  // deux vrais noms voisins.
  function uneFauteDeFrappe(a, b) {
    if (Math.abs(a.length - b.length) > 1) return false;
    if (a.length < 7 || b.length < 7) return false;
    var i = 0, j = a.length - 1, k = b.length - 1;
    while (i < a.length && i < b.length && a[i] === b[i]) i++;
    if (i === a.length && i === b.length) return false;    // identiques
    while (j >= i && k >= i && a[j] === b[k]) { j--; k--; }
    // ce qui reste au milieu doit tenir en un seul caractère de chaque côté
    return (j - i < 1) && (k - i < 1);
  }
 
  function stem(w) { return w.length > 3 ? w.replace(/s$/, "") : w; }
 
  function Moteur(index) {
    this.ix = index;
    this.vides = new Set(index.mots_vides || []);
    this.gloss = index.glossaire || {};
    // longueur moyenne des sections, pour BM25
    var t = 0;
    for (var i = 0; i < index.sections.length; i++) t += index.sections[i].len;
    this.avgdl = t / Math.max(1, index.sections.length);
  }
 
  Moteur.prototype.mots = function (s) {
    var self = this;
    // « groupe A » / « groupe B » : voir la même règle dans l'indexeur.
    var t = lettreDeGroupe(normalise(s));
    return (t.match(/[a-z0-9]+/g) || []).filter(function (w) {
      return w.length > 1 && !self.vides.has(w);
    });
  };
 
  // Ajoute le vocabulaire québécois / les abréviations cliniques
  Moteur.prototype.etendre = function (q) {
    // « C. diff », « H. flu », « S. aureus » : le point et le trait d'union
    // cassaient la reconnaissance des abréviations du glossaire, qui sont
    // écrites avec une simple espace. On les ramène à une espace.
    var n = normalise(q).replace(/[.\-'\u2019]/g, " ").replace(/\s+/g, " ");
    var base = this.mots(q);
    var ajouts = [];
    for (var cle in this.gloss) {
      // Le « s » du pluriel ne doit pas faire rater l'entrée : « urines »
      // doit trouver la clé « urine », « hémocultures » la clé « hémoculture ».
      // Mais seulement pour les mots de CONCEPT : pour les noms, le pluriel
      // distingue deux pages (« La pénicilline » et « Pénicillines »).
      var re = new RegExp("\\b" + cle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") +
                          (this.nomDePage(cle) ? "" : "s?") + "\\b");
      if (re.test(n)) {
        var vals = this.gloss[cle];
        for (var i = 0; i < vals.length; i++) {
          ajouts = ajouts.concat(this.mots(vals[i]));
        }
      }
    }
    // les mots de la question pèsent plus que les ajouts du glossaire
    var poids = {};
    base.forEach(function (w) { poids[w] = Math.max(poids[w] || 0, 1); });
    ajouts.forEach(function (w) { poids[w] = Math.max(poids[w] || 0, 0.6); });
    return poids;
  };
 
  // La clé du glossaire désigne-t-elle une page (un nom), ou un concept ?
  Moteur.prototype.nomDePage = function (cle) {
    if (!this._titresGloss) {
      var t = {}, self = this;
      this.ix.pages.forEach(function (p) {
        self.mots(p.titre).forEach(function (w) { t[w] = 1; });
      });
      this._titresGloss = t;
    }
    // Un mot qui déclenche une intention (« réaction », « dose », « durée »)
    // n'est pas non plus candidat au pluriel : il pèse sur le choix de la
    // section, et l'élargir déplace la réponse.
    if (!this._declencheursGloss) {
      var dg = {}, defs = this.ix.intentions || {}, moi2 = this;
      for (var n in defs) {
        (defs[n].declencheurs || []).forEach(function (d) {
          moi2.mots(d).forEach(function (w) { dg[w] = 1; });
        });
      }
      this._declencheursGloss = dg;
    }
    var mots = this.mots(cle);
    for (var i = 0; i < mots.length; i++) {
      if (this._titresGloss[mots[i]] || this._declencheursGloss[mots[i]]) return true;
    }
    var vals = this.gloss[cle] || [];
    for (var j = 0; j < vals.length; j++) {
      var mv = this.mots(vals[j]);
      for (var k = 0; k < mv.length; k++) if (this._titresGloss[mv[k]]) return true;
    }
    return false;
  };
 
  // Quelle sorte de renseignement la question demande-t-elle ?
  // « quoi donner pour un pseudomonas » -> traitement, donc on privilégie
  // les sections « Traitements » plutôt qu'une autre qui parle aussi d'ATB.
  Moteur.prototype.intentions = function (question) {
    var n = normalise(question), out = [];
    var defs = this.ix.intentions || {};
    for (var nom in defs) {
      var d = defs[nom], touche = false;
      for (var i = 0; i < d.declencheurs.length; i++) {
        var t = d.declencheurs[i];
        var re = new RegExp("\\b" + t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
        if (re.test(n)) { touche = true; break; }
      }
      if (touche) out.push({ nom: nom, titres: d.titres });
    }
    return out;
  };
 
  // BM25 sur les sections
  Moteur.prototype.chercher = function (question, nb) {
    var ix = this.ix, self = this;
    var poids = this.etendre(question);
    var inten = this.intentions(question);
    var k1 = 1.4, b = 0.72, N = ix.N;
    var res = [];
 
    // Quelle page la question NOMME-t-elle ? « le SARM ça se traite comment »
    // doit répondre avec la page du SARM, pas avec une autre page qui parle
    // aussi de traitement. On mesure la correspondance avec le titre de page,
    // pondérée par la rareté des mots (un mot rare identifie mieux).
    // On retient aussi QUELS mots chaque page a reconnus : « Staphylococcus
    // coagulase négative » ne reconnaît que « staphylococcus » là où
    // « Staphylococcus aureus » reconnaît « staphylococcus » ET « aureus ».
    // C'est ce qui permet plus bas d'écarter l'autre staphylocoque.
    var poidsStem = {};
    for (var pw in poids) {
      var ps = stem(pw);
      if (!(poidsStem[ps] >= poids[pw])) poidsStem[ps] = poids[pw];
    }
 
    // MOTS QUI DÉSIGNENT UNE PAGE SANS ÊTRE DANS SON TITRE.
    // « anthrax » n'apparaît que dans Bacillus anthracis, « diphtérie » que
    // dans Corynebacterium diphtheriae : ces mots désignent clairement une
    // page. À l'inverse « endocardite » est cité dans 21 pages et « pneumonie »
    // dans 27 : ce sont des mentions cliniques, pas des sujets de page.
    // Le critère est la concentration : un mot qui vit à 60 % ou plus dans
    // une seule page la désigne.
    if (!this._designe) {
      var parMot = {}, dansTx = {}, horsTx = {}, enProse = {}, enClinique = {}, sigleWiki = {}, enTitre = {};
      ix.sections.forEach(function (s) {
        var tx = /trait|efficac|spectre|couvertur|indication|sensibilit|resistance/.test(normalise(s.titre));
        // Un terme qui n'apparaît QUE dans une puce est un élément de liste :
        // « chorioamnionite » est une infection parmi d'autres du strep B, pas
        // un autre nom du strep B. « Anthrax », écrit en prose (« Cause
        // l'ANTHRAX »), désigne au contraire Bacillus anthracis. Idée tirée
        // du moteur concurrent : seule une fiche est le sujet d'une réponse.
        s.prose.forEach(function (l) {
          self.mots(l).forEach(function (w) { enProse[w] = 1; });
        });
        // UN NOM DE MALADIE RESTE UN SUJET, MÊME DANS UNE PUCE.
        // « SSSS », « syndrome d'Austrian », « ETEC » sont écrits en puce dans
        // la section des infections : ce sont des noms de tableaux cliniques,
        // et la page qui les porte est bien celle qui les explique. Un terme
        // cité dans une section de laboratoire ou de posologie (« CRP »,
        // « AUC ») n'est, lui, qu'une mention de passage.
        // Les SIGLES du wiki, où qu'ils soient : SDD, CLSI, BLSE, ETEC.
        // Ils sont courts, donc exclus par la règle de longueur, alors qu'ils
        // désignent très précisément ce dont ils parlent.
        s.prose.concat(s.puces).concat([s.titre]).forEach(function (l) {
          (l.match(/\b[A-Z\u00C0-\u00de]{3,6}\b/g) || []).forEach(function (b7) {
            self.mots(b7).forEach(function (w) { sigleWiki[w] = 1; });
          });
        });
        // UN MOT QUI TITRE UNE SECTION EST UN SUJET DE LA PAGE.
        // « Séroconversion » titre une section de la page Sérologies : la page
        // en parle pour de bon, ce n'est pas une mention de passage. Sans ça,
        // sa définition était présentée comme « pas forcément le traitement
        // complet » — une phrase absurde pour un test de laboratoire.
        self.mots(s.titre).forEach(function (w) { enTitre[w] = 1; });
        if (/infection|clinique|perle|manifestation|syndrome|tableau|specificite/.test(normalise(s.titre))) {
          s.puces.forEach(function (l) {
            // seulement les NOMS PROPRES et les SIGLES : « SSSS », « ETEC »,
            // « Austrian », « Waterhouse ». « chorioamnionite », nom commun,
            // reste un élément de liste parmi d'autres, pas un sujet.
            var mots8 = l.match(/[A-Za-z\u00C0-\u00ff][A-Za-z\u00C0-\u00ff'-]*/g) || [];
            for (var mi8 = 0; mi8 < mots8.length; mi8++) {
              var b8 = mots8[mi8];
              var sigle8 = b8.length >= 3 && b8 === b8.toUpperCase();
              var propre8 = mi8 > 0 && /^[A-Z\u00C0-\u00de][a-z\u00e0-\u00ff]{3,}$/.test(b8);
              if (sigle8 || propre8) self.mots(b8).forEach(function (w) { enClinique[w] = 1; });
            }
          });
        }
        for (var w in s.tf) {
          var p = parMot[w] || (parMot[w] = {});
          p[s.page] = (p[s.page] || 0) + s.tf[w];
          if (tx) dansTx[w] = (dansTx[w] || 0) + s.tf[w];
          else horsTx[w] = (horsTx[w] || 0) + s.tf[w];
        }
      });
      // Les pages d'index (« Bactéries », « Antibiotiques », « Classification »)
      // énumèrent des dizaines d'organismes sans les traiter. Sans cette
      // exclusion, « Brucella » semblerait désigner la page « Bactéries » et
      // l'assistant répondrait au lieu de refuser.
      var INDEX = /^(accueil|bacteries|antibiotiques|classification|infections|tests de laboratoires|calculateur|calculateur de dose|couverture des antibiotiques|template|demo|positif|bacilles gram)/;
      var estIndex = ix.pages.map(function (p) { return INDEX.test(normalise(p.titre)); });
 
      // Page de MÉDICAMENT ou page de BACTÉRIE ? Le wiki suit son propre
      // gabarit : un médicament a Spectre / Posologie / Relais per os, une
      // bactérie a Infections principales / Épidémiologie / Traitements.
      // C'est ce qui distingue « anthrax » (qui désigne la bactérie Bacillus
      // anthracis, un vrai sujet du wiki) de « rickettsies » (juste cité dans
      // le spectre de la doxycycline, sans page à elle).
      var titresPage = [];
      ix.sections.forEach(function (s) {
        (titresPage[s.page] = titresPage[s.page] || []).push(normalise(s.titre));
      });
      var estMedicament = ix.pages.map(function (p, i) {
        var t = (titresPage[i] || []).join(" | ");
        var med = /posologie|relais|biodisponibilit|pharmacocinet|indications principales/.test(t);
        var bact = /infections principales|epidemiologie|mecanisme.{0,4} de resistance|taux de sensibilit/.test(t);
        return med && !bact;
      });
 
      var des = {}, desFaible = {};
      // Un verbe conjugué ne désigne pas une page : « causent » n'apparaît
      // qu'une fois, dans Staphylococcus aureus, et se mettait à la désigner.
      var VERBE = /(ent|ons|ez|ais|ait|aient|era|eront|erait|ant)$/;
      // Un mot qui sert a exprimer une INTENTION (transmet, resiste, donne,
      // ajuster, surveiller...) decrit la question, pas un sujet de page :
      // il ne doit jamais designer une page.
      if (!this._motsIntention) {
        var mi2 = {};
        var defs2 = ix.intentions || {};
        for (var ni in defs2) {
          (defs2[ni].declencheurs || []).forEach(function (d) {
            self.mots(d).forEach(function (w) { mi2[stem(w)] = 1; });
          });
        }
        this._motsIntention = mi2;
      }
      var motsIntention = this._motsIntention;
      for (var w2 in parMot) {
        if ((ix.df[w2] || 0) > 12) continue;          // mot trop répandu
        // Un mot court n'est pas un nom : « sert », « parle », « voit »
        // désignaient des pages entières parce qu'ils n'étaient écrits qu'à
        // un seul endroit du wiki. Les vrais désignateurs (anthrax,
        // diphtérie, scarlatine, Waterhouse) sont des mots longs.
        if (w2.length < 6 && !sigleWiki[w2]) continue;
        if (VERBE.test(w2) || motsIntention[w2]) continue;
        var tot = 0, meilleur = -1, meilleurN = 0;
        for (var pg in parMot[w2]) {
          tot += parMot[w2][pg];
          if (parMot[w2][pg] > meilleurN) { meilleurN = parMot[w2][pg]; meilleur = +pg; }
        }
        if (!tot || meilleurN / tot < 0.6 || estIndex[meilleur]) continue;
        // sujet traité par une page de bactérie -> vraie réponse
        // sujet seulement cité dans une page de médicament -> simple mention
        // Un mot qui n'apparaît que dans les sections de TRAITEMENT d'une
        // page est un médicament cité là, pas le sujet de la page :
        // « colistine » vit dans les Traitements de Proteus mirabilis, alors
        // qu'« anthrax » vit dans les Infections de Bacillus anthracis.
        var surtoutTx = (dansTx[w2] || 0) > 0 && (horsTx[w2] || 0) === 0;
        // Une page de CLASSE ou de generalites (« Les aminosides », « Les
        // resistances aux antibiotiques ») enumere : un mot cite dedans n'en
        // fait pas son sujet. Idee du moteur concurrent : seule une fiche
        // peut etre le sujet d'une reponse, le reste est une mention.
        var CLASSE = /^(les |l'|la |le )?(resistance|aminoside|carbapeneme|macrolide|quinolone|cephalosporine|penicilline|tetracycline|glycopeptide|lincosamide)/;
        var estClasse = CLASSE.test(normalise(ix.pages[meilleur].titre));
        if (estMedicament[meilleur] || surtoutTx || estClasse ||
            (!enProse[w2] && !enClinique[w2] && !enTitre[w2])) desFaible[w2] = meilleur;
        else des[w2] = meilleur;
      }
      this._estMedicament = estMedicament;
      this._designe = des;
      this._designeFaible = desFaible;
    }
    var designe = this._designe;
 
    // mots rares de la question (une espèce, pas un mot courant)
    var rares = [];
    for (var rw in poids) {
      if (poids[rw] >= 1 && !GENERIQUES[stem(rw)] && ix.df[rw] && ix.df[rw] <= 15) rares.push(rw);
    }
    // index « quels mots apparaissent dans le texte de chaque page »
    var motsPage = this._motsPage;
    if (!motsPage) {
      motsPage = this._motsPage = [];
      ix.sections.forEach(function (s) {
        var m = motsPage[s.page] || (motsPage[s.page] = {});
        for (var w in s.tf) m[w] = 1;
      });
    }
 
    // Où, dans la phrase, la page est-elle nommée ? (sert au départage)
    var motsBruts = lettreDeGroupe(normalise(question)).match(/[a-z0-9]+/g) || [];
    function position(trouves) {
      var meilleur = Infinity;
      for (var qi = 0; qi < motsBruts.length; qi++) {
        var ws = stem(motsBruts[qi]);
        if (trouves[ws]) { if (qi < meilleur) meilleur = qi; continue; }
        // mot du glossaire : « la gono » nomme Neisseria gonorrhoeae
        var vals = self.gloss[motsBruts[qi]];
        if (vals) {
          for (var vi = 0; vi < vals.length; vi++) {
            if (trouves[stem(vals[vi])] && qi < meilleur) meilleur = qi;
          }
        }
      }
      return meilleur;
    }
 
    // FAUTE DE FRAPPE, EN DERNIER RECOURS. On ne corrige un mot que si aucune
    // page ne le reconnaît tel quel, et seulement si la correction est UNIQUE :
    // deux noms de médicaments se ressemblent souvent à une lettre près, et
    // corriger au hasard serait pire que de ne pas répondre.
    if (!this._motsTousTitres) {
      var tt = [];
      ix.pages.forEach(function (p) {
        self.mots(p.titre).forEach(function (w) { if (tt.indexOf(w) < 0) tt.push(w); });
      });
      this._motsTousTitres = tt;
    }
    var tousTitres = this._motsTousTitres;
    for (var fw2 in poidsStem) {
      // un mot que le wiki écrit vraiment n'est pas une faute de frappe :
      // « pneumonie » ne doit jamais devenir « pneumoniae ».
      if (poidsStem[fw2] < 1 || fw2.length < 7) continue;
      if (ix.df[fw2] !== undefined || this.gloss[fw2]) continue;
      var reconnu = false, corrections = [];
      for (var ti3 = 0; ti3 < tousTitres.length; ti3++) {
        var tw3 = stem(tousTitres[ti3]);
        var cc = Math.min(fw2.length, tw3.length);
        if (fw2 === tw3 || memeNom(fw2, tw3) ||
            (cc >= 5 && fw2.length <= tw3.length + 2 && fw2.slice(0, cc) === tw3.slice(0, cc))) {
          reconnu = true; break;
        }
        if (uneFauteDeFrappe(fw2, tw3) && corrections.indexOf(tw3) < 0) corrections.push(tw3);
      }
      if (!reconnu && corrections.length === 1) poidsStem[corrections[0]] = 1;
    }
 
    var entite = [], entMots = [], pos = [], parTitre = [];
    for (var pi = 0; pi < ix.pages.length; pi++) {
      var mt = self.mots(ix.pages[pi].titre), e = 0, nbTitre = 0;
      var vusMot = {}, trouves = {};
      for (var mi = 0; mi < mt.length; mi++) {
        var mw = stem(mt[mi]);
        if (vusMot[mw]) continue;
        vusMot[mw] = 1;
        // correspondance directe, ou abréviation par troncature :
        // « actino » pour actinomyces, « azithro » pour azithromycine.
        var ok = poidsStem[mw] >= 0.6;
        if (!ok) {
          for (var qw in poidsStem) {
            // « azithro » pour azithromycine, mais aussi « ertapénème » écrit
            // en entier pour la page « Ertapénem » : la troncature peut être
            // d'un côté comme de l'autre.
            // Une abréviation est TOUJOURS plus courte que le nom complet.
            // Sans cette borne, « actinomycétome » et « actinomycetemcomitans »
            // passaient pour la page « Actinomyces sp ».
            var c0 = Math.min(qw.length, mw.length);
            if (c0 >= 5 && qw.length <= mw.length + 2 &&
                qw.slice(0, c0) === mw.slice(0, c0)) { ok = true; break; }
            if (memeNom(qw, mw)) { ok = true; break; }
          }
        }
        if (ok) {
          var dfp = ix.df[mt[mi]] || 1;
          e += Math.log(1 + (N - dfp + 0.5) / (dfp + 0.5));
          trouves[mw] = 1;
          if (!GENERIQUES[mw]) nbTitre++;
        }
      }
      // un mot du texte qui désigne cette page la nomme aussi
      // (« anthrax » -> Bacillus anthracis, « ancef » -> Cefazoline)
      for (var dw in poids) {
        if (poids[dw] >= 1 && designe[dw] === pi && !GENERIQUES[stem(dw)]) {
          var dfd = ix.df[dw] || 1;
          e += Math.log(1 + (N - dfd + 0.5) / (dfd + 0.5));
          trouves[dw] = 1;
        }
      }
      // Un titre long qui contient les mots cherchés PLUS beaucoup d'autres
      // décrit autre chose : « Staphylococcus aureus résistant à la
      // méticilline » n'est pas la réponse à « staph aureus ». On pondère
      // donc par la part du titre que la question couvre réellement.
      var totalMots = Object.keys(vusMot).length || 1;
      var nbTrouves = Object.keys(trouves).length;
      // Une page est « nommée » si un mot propre à elle est reconnu...
      var propre = false;
      for (var tw in trouves) { if (!GENERIQUES[tw]) { propre = true; break; } }
      // ...ou si la question reprend l'essentiel d'un titre fait de mots
      // courants : « c'est quoi les résistances aux antibiotiques » doit
      // trouver sa page, alors que le seul mot « antibiotique » ne suffit pas.
      if (!propre && nbTrouves >= 2 && nbTrouves / totalMots >= 0.6) propre = true;
      entite[pi] = propre ? e * (nbTrouves / totalMots) : 0;
      // « Staphylococcus epidermidis » : deux pages reconnaissent le genre,
      // mais une seule parle d'epidermidis dans son texte. C'est celle-là
      // qu'il faut. On favorise la page qui contient les mots rares de la
      // question que son titre ne porte pas.
      if (entite[pi] > 0 && rares.length) {
        var dansTexte = 0;
        for (var ri = 0; ri < rares.length; ri++) {
          if (!trouves[stem(rares[ri])] && motsPage[pi] && motsPage[pi][rares[ri]]) dansTexte++;
        }
        if (dansTexte) entite[pi] *= 1 + 0.9 * dansTexte;
      }
      entMots[pi] = trouves;
      parTitre[pi] = nbTitre > 0;
      pos[pi] = position(trouves);
    }
    // LE NOM L'EMPORTE SUR L'INDICE. Une page peut être « nommée » de deux
    // façons : par un mot de son titre, ou par un mot de son texte qui ne
    // vit que chez elle (« anthrax » -> Bacillus anthracis). Le second est
    // utile, mais c'est un indice, pas un nom : « parle-moi de la
    // ciprofloxacine » tombait sur la page Sérologies, parce que le mot
    // « parle » n'est écrit que là. Dès qu'une page est nommée par son
    // titre, les pages désignées par un simple indice s'effacent.
    var qqunParTitre = false;
    for (var pt2 = 0; pt2 < entite.length; pt2++) {
      if (entite[pt2] > 0 && parTitre[pt2]) { qqunParTitre = true; break; }
    }
    if (qqunParTitre) {
      for (var pt3 = 0; pt3 < entite.length; pt3++) {
        if (entite[pt3] > 0 && !parTitre[pt3]) entite[pt3] = 0;
      }
    }
    // LE SUJET EST NOMMÉ EN PREMIER. « Est-ce que la ceftriaxone couvre
    // l'entérocoque ? » parle de la ceftriaxone : c'est sa page qu'on veut
    // en tête, l'entérocoque n'est que le critère. Deux pages nommées, c'est
    // celle citée en premier qui est le sujet de la phrase.
    var premier = Infinity;
    for (var pp = 0; pp < entite.length; pp++) {
      if (entite[pp] > 0 && pos[pp] < premier) premier = pos[pp];
    }
    if (premier < Infinity) {
      for (var pq = 0; pq < entite.length; pq++) {
        if (entite[pq] > 0 && pos[pq] === premier) entite[pq] *= 2.6;
      }
    }
    var entiteMax = Math.max.apply(null, entite.concat([0]));
    this._entite = entite;
    this._entMots = entMots;
 
    for (var i = 0; i < ix.sections.length; i++) {
      var s = ix.sections[i], score = 0, touches = 0;
      for (var w in poids) {
        var f = s.tf[w];
        if (!f) continue;
        var df = ix.df[w] || 1;
        var idf = Math.log(1 + (N - df + 0.5) / (df + 0.5));
        score += poids[w] * idf * (f * (k1 + 1)) / (f + k1 * (1 - b + b * s.len / self.avgdl));
        touches++;
      }
      if (!score) continue;
      // bonus quand le titre de la page ou de la section correspond
      var tp = normalise(ix.pages[s.page].titre);
      var ts = normalise(s.titre);
      for (var w2 in poids) {
        if (poids[w2] < 1) continue;
        if (tp.indexOf(w2) >= 0) score += 2.2 * poids[w2];
        if (ts.indexOf(w2) >= 0) score += 1.5 * poids[w2];
      }
      // la section répond-elle à la sorte de question posée ?
      var bonusIntention = 0;
      for (var a = 0; a < inten.length; a++) {
        var titres = inten[a].titres;
        for (var c = 0; c < titres.length; c++) {
          if (ts.indexOf(titres[c]) >= 0) {
            bonusIntention = Math.max(bonusIntention, 1);
            break;
          }
        }
      }
      if (inten.length) {
        // multiplicatif : une section hors sujet garde son score, la bonne
        // section passe devant sans écraser la pertinence des mots.
        score *= bonusIntention ? 1.9 : 0.85;
      }
      // la page nommée par la question passe devant les autres
      if (entiteMax > 0 && entite[s.page] > 0) {
        score *= 1 + 1.6 * (entite[s.page] / entiteMax);
      }
      // et, parmi les pages nommées, celle que la question nomme en premier
      // est le sujet : « est-ce que le céfépime couvre le SARM ? » parle du
      // céfépime, le SARM n'est que le critère.
      if (premier < Infinity && pos[s.page] === premier) score *= 1.5;
      res.push({ i: i, score: score, touches: touches, ent: entite[s.page] || 0 });
    }
    res.sort(function (a, b) { return b.score - a.score; });
    var sortie = res.slice(0, nb || 8);
 
    // LA PAGE NOMMÉE DOIT TOUJOURS ÊTRE CANDIDATE. « Quel traitement pour
    // ciprof » : « ciprof » n'est écrit nulle part dans le wiki, donc la page
    // Ciprofloxacine n'a presque aucun score de mots et tombait hors de la
    // liste — l'assistant refusait alors que la page est clairement nommée.
    // On la réinjecte : c'est le classement qui doit suivre la reconnaissance
    // du nom, pas l'inverse.
    if (entiteMax > 0 && !sortie.some(function (h) { return h.ent > 0; })) {
      var repeche = [];
      for (var ri2 = 0; ri2 < ix.sections.length; ri2++) {
        var s2 = ix.sections[ri2];
        if (entite[s2.page] < entiteMax * 0.999) continue;
        if (!s2.prose.length && !s2.puces.length) continue;
        // on privilégie la section qui répond à la sorte de question posée
        var ts2 = normalise(s2.titre), bon2 = 0;
        for (var a2 = 0; a2 < inten.length; a2++) {
          var ti2 = inten[a2].titres;
          for (var c2 = 0; c2 < ti2.length; c2++) if (ts2.indexOf(ti2[c2]) >= 0) bon2 = 1;
        }
        repeche.push({ i: ri2, score: 1 + bon2, touches: 1, ent: entite[s2.page] });
      }
      repeche.sort(function (a, b) { return b.score - a.score; });
      if (repeche.length) sortie = repeche.slice(0, 3).concat(sortie).slice(0, nb || 8);
    }
    return sortie;
  };
 
  // Compose la réponse : toujours un texte, toujours du verbatim + source.
  Moteur.prototype._repondre = function (question) {
    var ix = this.ix;
    var hits = this.chercher(question, 10);
    var inten = this.intentions(question);
 
    if (!hits.length) {
      return {
        etat: "rien",
        intro: "Cette information n'est pas dans Microguide. Elle sera peut-être ajoutée " +
              "dans une version ultérieure.",
        blocs: [],
        suggestions: ix.pages.slice(0, 0)
      };
    }
 
    // RÈGLE 1 — le wiki n'a que des pages de bactéries et d'antibiotiques.
    // Si la question ne nomme aucune de ces pages (« quel antibiotique pour
    // une infection urinaire », « comment traiter une endocardite »), on ne
    // répond pas : le wiki n'a pas de page de pathologie, et deviner une
    // bactérie au hasard serait trompeur.
    // ESPÈCE NON COUVERTE. « Clostridium tetani » : le genre existe dans le
    // wiki (Clostridium perfringens), mais pas l'espèce. Répondre avec la
    // page du voisin serait une erreur grave. Règle étroite : un mot qui
    // suit immédiatement un genre connu et qui est TOTALEMENT absent du
    // wiki => espèce non couverte. (Étroite exprès : « parle » aussi est
    // absent du wiki, mais il ne suit aucun genre.)
    var moi = this;
    if (!this._motsTitres) {
      var mt2 = {}, listeT2 = [];
      ix.pages.forEach(function (p) {
        moi.mots(p.titre).forEach(function (w) {
          mt2[stem(w)] = 1;
          if (listeT2.indexOf(w) < 0) listeT2.push(w);
        });
      });
      this._motsTitres = mt2;
      this._listeTitres = listeT2;
    }
 
    // Le mot est-il un nom que le wiki porte dans un titre ? On accepte les
    // variantes d'écriture par troncature, comme le fait le classement :
    // « meropenem » pour « méropénème », « azithro » pour « azithromycine ».
    // Sans ça, « méropénem-vaborbactam » passerait pour un nom entièrement
    // inconnu alors que le wiki a bien la page du méropénème.
    function connuDesTitres(w) {
      if (moi._motsTitres[stem(w)]) return true;
      // le glossaire traduit aussi vers un titre : « kleb » -> Klebsiella
      var tr = moi.gloss[w];
      if (tr) {
        for (var ti = 0; ti < tr.length; ti++) {
          if (moi._motsTitres[stem(tr[ti])]) return true;
        }
      }
      var l = moi._listeTitres;
      for (var i2 = 0; i2 < l.length; i2++) {
        var court = Math.min(l[i2].length, w.length);
        if (court >= 6 && w.length <= l[i2].length + 2 &&
            l[i2].slice(0, court) === w.slice(0, court)) return true;
        // Une faute de frappe dans le genre ne doit pas faire dire au wiki
        // qu'il ne couvre pas le germe : « Staphylcoccus aureus » reste du
        // Staphylococcus aureus.
        if (uneFauteDeFrappe(w, l[i2]) || memeNom(w, l[i2])) return true;
      }
      return false;
    }
 
    // Mot qui décrit la QUESTION et non un nom : verbe, mot du glossaire,
    // déclencheur d'intention. « couvre », « ajuster », « donne » finissent
    // comme des noms de médicaments (« ceftolozane ») : seule une liste, pas
    // une terminaison, permet de les distinguer.
    if (!this._motsQuestion) {
      var mq = {};
      var defs3 = ix.intentions || {};
      for (var n3 in defs3) {
        (defs3[n3].declencheurs || []).forEach(function (d) {
          moi.mots(d).forEach(function (w) { mq[stem(w)] = 1; });
        });
      }
      // Seulement les mots d'ACTION du glossaire : « traiter », « couvrir »…
      // Les noms d'organismes qu'il contient (« listeria », « pneumocoque »)
      // sont au contraire des sujets, pas des mots de question.
      for (var cle3 in this.gloss) {
        var vals3 = this.gloss[cle3];
        var nom3 = vals3.some(function (v) { return moi._motsTitres[stem(v)]; });
        if (!nom3) moi.mots(cle3).forEach(function (w) { mq[stem(w)] = 1; });
      }
      this._motsQuestion = mq;
    }
    var VERBE_FR = /(ent|ons|ez|ais|ait|aient|era|eront|erait|ant|er|ir|oir|issent|ement)$/;
    function motDeQuestion(w) {
      // Un nom que le wiki porte dans un titre n'est jamais un mot de
      // question, même s'il finit comme un verbe : « citrobacter » et
      // « enterobacter » se terminent en -er comme « ajuster ».
      if (connuDesTitres(w) && !GENERIQUES[stem(w)]) return false;
      return moi._motsQuestion[stem(w)] || VERBE_FR.test(w) || moi.vides.has(w) ||
            GENERIQUES[stem(w)];
    }
 
    // Suite de mots TELLE QU'ÉCRITE, sans retirer les petits mots : c'est la
    // seule façon de distinguer « Mycoplasma pneumoniae » (deux mots collés,
    // un nom d'espèce) de « traiter un pseudomonas » (un article au milieu).
    var brut = lettreDeGroupe(normalise(question)).match(/[a-z0-9]+/g) || [];
    var suite = this.mots(question);
 
    // NOM COMPOSÉ ABSENT. « ceftazidime-avibactam », « méropénème-vaborbactam »,
    // « ampicilline-sulbactam » : le trait d'union lie deux molécules. Si le
    // wiki n'a que l'une des deux, la page trouvée n'est pas le médicament
    // demandé — et leurs spectres diffèrent justement par l'inhibiteur.
    var compose = normalise(question).match(/[a-z]{5,}-[a-z]{3,}/g) || [];
    for (var ci = 0; ci < compose.length; ci++) {
      var parts = compose[ci].split("-");
      if (motDeQuestion(parts[0]) || motDeQuestion(parts[1])) continue;
      if (parts[1].length < 5 && ix.df[parts[1]] !== undefined) continue;
      var c1 = connuDesTitres(parts[0]), c2 = connuDesTitres(parts[1]);
      if (c1 !== c2) {
        return {
          etat: "hors-sujet",
          intro: "Cette information n'est pas dans Microguide. Le wiki a une page sur l'une " +
                "des deux molécules, mais pas sur cette association-là; elle sera peut-être " +
                "ajoutée dans une version ultérieure.",
          blocs: []
        };
      }
    }
 
    // « LA PIPÉRACILLINE SEULE », « l'amoxicilline sans clavulanate ». Le wiki
    // n'a que la page de l'association; son spectre n'est pas celui de la
    // molécule seule, c'est justement l'inhibiteur qui fait la différence.
    if (/\b(seule?|sans)\b/.test(normalise(question))) {
      for (var si = 0; si < ix.pages.length; si++) {
        var tParts = normalise(ix.pages[si].titre).replace(/\s*\([^)]*\)/g, "").split("-");
        if (tParts.length !== 2) continue;
        var a1 = this.mots(tParts[0])[0], a2 = this.mots(tParts[1])[0];
        if (!a1 || !a2) continue;
        if (brut.indexOf(a1) >= 0 && brut.indexOf(a2) < 0) {
          return {
            etat: "hors-sujet",
            intro: "Cette information n'est pas dans Microguide. Le wiki documente « " +
                  ix.pages[si].titre + " », l'association; il ne donne pas le spectre de la " +
                  "molécule seule, qui est justement différent.",
            blocs: []
          };
        }
      }
    }
 
    // GENRE NON COUVERT. « Mycoplasma pneumoniae » : le wiki n'a pas de
    // Mycoplasma, mais le mot « pneumoniae » correspond au pneumocoque, et
    // l'assistant répondait avec la mauvaise bactérie. Règle : si le premier
    // mot d'un couple précis est inconnu des titres alors que le second y
    // figure, c'est un nom que le wiki ne couvre pas.
    function pageCite(pid, w) {
      return ix.sections.some(function (sx) {
        return sx.page === pid && moi.mots(sx.titre + " " + sx.prose.join(" ") + " " +
                                          sx.puces.join(" ")).indexOf(w) >= 0;
      });
    }
    for (var g = 0; g + 1 < brut.length; g++) {
      var g1 = brut[g], g2 = brut[g + 1];
      // Un verbe ou un mot courant ne designe pas un genre : « ajuster
      // penicilline » ne doit pas passer pour un nom d'espece inconnu.
      // Les DEUX mots doivent avoir l'allure latine d'un binôme (genre en
      // -a, -us, -um, -er, -es; espèce en -ae, -us, -a...). Questions Kaggle du
      // 28 oct. 2026 : « auxquels Serratia », « contre Legionella », « triade
      // pneumonie » passaient pour des genres inconnus, et l'assistant
      // refusait des questions qui nommaient en toutes lettres une page du wiki.
      if (g1.length >= 5 && g2.length >= 5 &&
          /(a|us|um|er|es)$/.test(g1) && /(ae|us|um|ii|is|i|a|ens|ans)$/.test(g2) &&
          !motDeQuestion(g1) && !motDeQuestion(g2) &&
          !this.gloss[g1] && !this.gloss[g1 + " " + g2] &&  // abréviation ou espèce connue
          // Un genre sans page mais que le wiki CITE (« Mycoplasma » : spectre
          // de la doxy et de l'azithro, culture respiratoire, PCR) n'est pas
          // inconnu : la recherche le trouvera en mention. 28 oct. 2026.
          // Seulement si la page trouvée en tête parle bien de ce genre :
          // sinon « Mycoplasma pneumoniae traitement » repartait vers le
          // pneumocoque, l'erreur grave que cette règle empêche.
          !(ix.df[g1] >= 2 && hits.length && pageCite(ix.sections[hits[0].i].page, g1)) &&
          !connuDesTitres(g1) && connuDesTitres(g2)) {
        return {
          etat: "hors-sujet",
          intro: "Cette information n'est pas dans Microguide. Le wiki ne couvre pas ce " +
                "nom-là; il sera peut-être ajouté dans une version ultérieure.",
          blocs: []
        };
      }
    }
 
    // ORGANISME TOTALEMENT ABSENT. « Borrelia burgdorferi », « Burkholderia
    // cepacia », « Haemophilus ducreyi » : deux mots latins d'affilée dont
    // AUCUN n'existe dans le wiki. Sans cette règle, le classement se
    // rabattait sur la page qui leur ressemble le plus — la pire erreur
    // possible, puisqu'elle a l'air d'une vraie réponse.
    for (var h = 0; h + 1 < brut.length; h++) {
      var h1 = brut[h], h2 = brut[h + 1];
      if (h1.length >= 6 && h2.length >= 5 &&
          /(ae|us|um|ii|is|i|a|ens|ans)$/.test(h2) &&
          !motDeQuestion(h1) && !motDeQuestion(h2) &&
          !this.gloss[h1] && !this.gloss[h1 + " " + h2] &&
          ix.df[h1] === undefined && ix.df[h2] === undefined &&
          !connuDesTitres(h1) && !connuDesTitres(h2)) {
        return {
          etat: "hors-sujet",
          intro: "Cette information n'est pas dans Microguide. Le wiki ne contient aucune page " +
                "sur ce germe-là; elle sera peut-être ajoutée dans une version ultérieure.",
          blocs: []
        };
      }
    }
 
    // Quels mots l'utilisateur a-t-il écrits avec une majuscule, ailleurs qu'en
    // début de phrase ? C'est ainsi qu'on écrit un nom propre : « Salmonella
    // Dublin », « Haemophilus Ducreyi ».
    var majusculeDansQuestion = {};
    (function () {
      var mots = (question || "").match(/[A-Za-z\u00C0-\u00ff'-]+/g) || [];
      for (var mi = 1; mi < mots.length; mi++) {
        if (/^[A-Z\u00C0-\u00de]/.test(mots[mi])) {
          moi.mots(mots[mi]).forEach(function (w) { majusculeDansQuestion[w] = 1; });
        }
      }
    })();
 
    var desForts = this._designe || {};
    for (var k = 0; k + 1 < brut.length; k++) {
      // Le second mot est-il juste cité au passage ailleurs ? « bovis » et
      // « sulfaméthoxazole » existent une fois dans le wiki, dans une puce
      // d'une AUTRE page : ça ne fait pas d'eux des sujets couverts.
      var citeAilleurs = ix.df[brut[k + 1]] !== undefined &&
                        (ix.df[brut[k + 1]] > 2 || desForts[brut[k + 1]] !== undefined);
      // Le second mot doit avoir l'allure d'un nom d'espèce latin
      // (burgdorferi, cepacia, suis, parainfluenzae) et non d'un verbe
      // français conjugué : « la listeria POUSSE au frigo », « le cefepime
      // ENTRE dans le SNC » ne nomment aucune espèce. C'est l'erreur que
      // j'ai déjà faite trois fois ici : une terminaison vague attrape les
      // verbes, une terminaison latine ne les attrape pas.
      var LATIN = /(ae|us|um|ii|is|i|a|ens|ans)$/;
      // « Streptococcus suis » : le nom d'espèce est ici un mot de quatre
      // lettres qui est aussi un verbe français courant. On l'accepte quand
      // il SUIT un genre connu et qu'il est absent du wiki — « pourquoi »,
      // trop long, ne passe pas par là.
      var petitNom = brut[k + 1].length === 4 && LATIN.test(brut[k + 1]) &&
                    ix.df[brut[k + 1]] === undefined;
      if (connuDesTitres(brut[k]) &&
          !motDeQuestion(brut[k]) &&
          !motDeQuestion(brut[k + 1]) &&
          // nom d'allure latine, OU mot écrit avec une majuscule au milieu de
          // la phrase et absent du wiki : « Salmonella Dublin » — un sérovar
          // que Thomas n'a pas documenté. La majuscule vient de la question
          // telle que l'utilisateur l'a tapée.
          (LATIN.test(brut[k + 1]) ||
          (majusculeDansQuestion[brut[k + 1]] && ix.df[brut[k + 1]] === undefined)) &&
          brut[k + 1].length >= 5 &&
          !citeAilleurs &&
          !this.gloss[brut[k + 1]] &&
          !connuDesTitres(brut[k + 1])) {
        return {
          etat: "hors-sujet",
          intro: "Cette information n'est pas dans Microguide. Le wiki couvre d'autres espèces " +
                "de ce genre, mais pas celle-là; elle sera peut-être ajoutée dans une version " +
                "ultérieure.",
          blocs: []
        };
      }
    }
 
    // La page citée doit être une page que la question nomme. Sinon on
    // écarte tout : répondre « pneumonie » avec une page d'antibiotique prise
    // au hasard serait trompeur.
    // Une question qui demande une LISTE (« quelles bactéries causent… »,
    // « quels antibiotiques donnent… ») doit donner une liste, même si une
    // page se trouve nommée au passage : sinon on répond avec une seule
    // bactérie là où le wiki en documente dix.
    // « quelles sont les indications de X » nomme une page : ce n'est pas une
    // question-liste. On exige un verbe d'effet, et pas « sont ».
    // On accepte le singulier comme le pluriel : beaucoup écrivent « quels
    // bacilles DONNE des infections urinaires ». Le verbe doit décrire un
    // effet, sinon « quelles sont les indications de X » deviendrait une liste.
    // Le sujet doit être une CATÉGORIE (bactéries, bacilles, antibiotiques,
    // gram…), pas une page précise : « quelles infections fait le staph
    // aureus » parle d'une seule page et doit rester une réponse de page.
    // « DDX D'UNE HÉMOCULTURE POSITIVE À GRAM NÉGATIF »
    // Un résident ne demande pas « quelles bactéries causent… », il demande
    // un diagnostic différentiel. C'est pourtant la même question, et le wiki
    // a la réponse : les pages Gram négatif qui parlent de bactériémie. La
    // forme changeait tout, pas le contenu.
    var veutDdx = /\b(ddx|dxx|diagnostic differentiel|diagnostics differentiels|dx differentiel|differentiel|differentiels)\b/
                    .test(normalise(question).replace(/[.'\u2019-]/g, " "));
    // « quels atb donnent… » ou « les atb qui donnent… » : la même question-liste
    var NOMS_LISTE = "(bacterie|bacteries|bacille|bacilles|germe|germes|microbe|microbes|pathogene|pathogenes|cocci|coque|coques|gram|antibiotique|antibiotiques|atb|atbs|abx|abxs|antibio|antibios|medicament|medicaments|molecule|molecules|lactamine|lactamines|penicilline|penicillines|cephalosporine|cephalosporines|carbapeneme|carbapenemes|quinolone|quinolones|fluoroquinolone|fluoroquinolones|glycopeptide|glycopeptides|aminoside|aminosides|macrolide|macrolides|tetracycline|tetracyclines|cycline|cyclines)", VERBES_LISTE = "(cause|causent|donne|donnent|fait|font|provoque|provoquent|entraine|entrainent|induit|induisent|occasionne|occasionnent|produit|produisent|couvre|couvrent|pousse|poussent|croit|croissent|se cultive|se cultivent|sont|est)";
    var nq = normalise(question);
    var veutListe = veutDdx ||
      new RegExp("\\b(quel|quels|quelle|quelles)\\b[^?]{0,20}\\b" + NOMS_LISTE + "\\b[^?]{0,45}\\b" + VERBES_LISTE + "\\b").test(nq) ||
      new RegExp("\\b" + NOMS_LISTE + "\\s+(qui|qu)\\b[^?]{0,20}\\b" + VERBES_LISTE + "\\b").test(nq);
 
    // Une page PORTAIL (index, classe, généralités) n'est le sujet de rien :
    // elle énumère. Idée reprise du moteur concurrent, qui règle d'un coup
    // « aztréonam → Les résistances » et « ostéomyélite → Antibiotiques ».
    var PORTAIL = /^(accueil|bacteries|antibiotiques|classification|infections|tests de laboratoires|calculateur|couverture des antibiotiques|template|demo|positif|bacilles gram)/;
    var estPortail = ix.pages.map(function (pg) { return PORTAIL.test(normalise(pg.titre)); });
 
    var nommes = hits.filter(function (h) {
      if (h.ent <= 0) return false;
      // un portail ne passe que si la question le nomme vraiment par son titre
      if (estPortail[ix.sections[h.i].page]) {
        var mt9 = moi.mots(ix.pages[ix.sections[h.i].page].titre).map(stem);
        var q9 = moi.mots(question).map(stem);
        // il faut que la question nomme le portail par un mot qui lui est
        // propre (« quinolone »), pas par un mot passe-partout
        // (« antibiotique ») qui désigne la moitié du wiki.
        var communs9 = mt9.filter(function (w) {
          return q9.indexOf(w) >= 0 && !GENERIQUES[w];
        });
        if (!communs9.length) return false;
      }
      return true;
    });
    // « Quels médicaments sont efficaces contre Listeria monocytogenes ? » a la
    // forme d'une question-liste, mais elle nomme UNE bactérie du wiki : c'est
    // la question de traitement de cette page, pas une liste de médicaments.
    // Questions Kaggle du 28 oct. 2026 : Listeria, Klebsiella, Legionella,
    // Nocardia et le SGA recevaient 4 à 8 pages d'antibiotiques à la place.
    // Seulement si la question parle de TRAITER : « quels antibiotiques
    // causent des pneumonies » reste une liste (« pneumonies » évoque le
    // pneumocoque sans le nommer).
    if (veutListe && !veutDdx &&
        /\bantibio|\batbs?\b|\babxs?\b|\bmedicament|\bmolecule/.test(nq) &&
        /\b(efficace|efficaces|trait|traiter|traitement|couvr|couvre|couvrent|sensible|resist|donner|choix)/.test(nq)) {
      var bactNommees = {};
      nommes.forEach(function (h) {
        var pg = ix.pages[ix.sections[h.i].page];
        if (pg.gram || pg.morpho) bactNommees[pg.id] = 1;
      });
      if (Object.keys(bactNommees).length === 1) veutListe = false;
    }
    if (!nommes.length || veutListe) {
      // ============================================================
      // RECHERCHE TRANSVERSALE
      // « Quels antibiotiques donnent des diarrhées ? », « quelles bactéries
      // causent une pneumonie ? », « quels gram négatifs font des
      // pneumonies ? » : aucune page n'a ce sujet pour titre, mais la réponse
      // est écrite dans une douzaine de pages. Refuser serait aussi faux que
      // de citer une page au hasard : on liste ce qui est écrit, page par page.
      // ============================================================
      var poidsQ = this.etendre(question);
      var nq2 = normalise(question);
 
      // ============================================================
      // QUESTION DE CLASSIFICATION
      // « Quelles sont les bactéries Gram négatif ? », « quels sont les cocci
      // Gram positif ? » : la question ne porte sur aucune infection, elle
      // demande la CATÉGORIE elle-même. La réponse est la liste des pages que
      // le wiki range dans cette catégorie, lue sur sa propre page
      // « Bactéries ». Aucun texte inventé : ce sont les titres de ses pages.
      // ============================================================
      var gramQ = /\bgram\s*(-|neg)/.test(nq2) ? "neg"
                : /\bgram\s*(\+|pos)/.test(nq2) ? "pos" : null;
      var morphQ = /\bcoccobacill/.test(nq2) ? "coccobacille"
                : /\bcocci|\bcoque/.test(nq2) ? "cocci"
                : /\bbacill/.test(nq2) ? "bacille" : null;
      // « quels sont les… » sans autre sujet : rien d'autre à chercher que la
      // catégorie. Un mot de contenu en plus (« pneumonie ») veut dire que la
      // question porte sur autre chose : c'est la recherche transversale.
      var restant = [];
      for (var cz in poidsQ) {
        var rz = stem(cz);
        if (poidsQ[cz] >= 1 && !GENERIQUES[rz] && !this.vides.has(cz) &&
            rz.length >= 4 &&
            !/^(gram|positi|negati|cocci|coque|bacill|coccobacill|sont|liste|lister|nomme|nommer|enumer|donne|donner|montre|dire|savoir|connai|existe|couvert|microguide|wiki)/.test(rz)) {
          restant.push(cz);
        }
      }
      if ((gramQ || morphQ) && !restant.length) {
        var membres = ix.pages.filter(function (pg) {
          if (gramQ && pg.gram !== gramQ) return false;
          if (morphQ && pg.morpho !== morphQ) return false;
          return !!pg.gram;        // seules les pages de bactéries sont classées
        });
        if (membres.length >= 2) {
          var pluriel = { cocci: "Cocci", bacille: "Bacilles", coccobacille: "Coccobacilles" };
          var nomCat = ((morphQ ? pluriel[morphQ] : "Bactéries") + " " +
                      (gramQ === "neg" ? "Gram négatif" : gramQ === "pos" ? "Gram positif" : "")).trim();
          var pageCat = null;
          for (var zc = 0; zc < ix.pages.length; zc++) {
            if (/^bacteries$/.test(normalise(ix.pages[zc].titre))) { pageCat = ix.pages[zc]; break; }
          }
          return {
            etat: "classification",
            intro: "Voici les " + membres.length + " pages que le wiki range dans cette " +
                  "catégorie. Le classement est celui de sa propre page « Bactéries » : " +
                  "je n'ai rien déduit moi-même. Chaque nom est le titre exact de sa page.",
            blocs: [{
              page: pageCat ? pageCat.titre : "Bactéries",
              url: pageCat ? pageCat.url : (ix.base || "") + "/index.php/Bact%C3%A9ries",
              images: [],
              sections: [{ titre: nomCat, prose: [],
                          puces: membres.map(function (pg) { return pg.titre; }) }]
            }]
          };
        }
      }
 
      // index par racine : « diarrhées » doit retrouver « diarrhée »
      if (!this._racines) {
        var rac = [], dfRac = {};
        ix.sections.forEach(function (sx, k) {
          var r = rac[k] = {}, vu = {};
          for (var w in sx.tf) {
            var rw = stem(w);
            r[rw] = (r[rw] || 0) + sx.tf[w];
            if (!vu[rw]) { dfRac[rw] = (dfRac[rw] || 0) + 1; vu[rw] = 1; }
          }
        });
        this._racines = rac; this._dfRacines = dfRac;
      }
      var racines = this._racines, dfRacines = this._dfRacines;
 
      // La recherche transversale ne sert QUE pour une question qui demande
      // explicitement une liste. Sinon « quel atb pour une endocardite »
      // listerait toutes les pages qui citent le mot, au lieu de refuser.
      var termesT = veutListe ? [] : null;
      if (termesT) {
      for (var cw in poidsQ) {
        var rc = stem(cw);
        // On accepte aussi les mots APPORTÉS PAR LE GLOSSAIRE (poids 0,6) :
        // « hémoculture positive » devient « bactériémie », qui est le mot
        // que le wiki emploie. Sans ça, la recherche cherchait un mot absent
        // du wiki et ne trouvait rien.
        if (poidsQ[cw] >= 0.6 && !GENERIQUES[rc] && !this.vides.has(cw) &&
            // un mot court mais rare compte aussi : « rash » (4 lettres) était
            // ignoré, et « quels atb donnent un rash » était refusé
            (rc.length >= 5 || (rc.length === 4 && dfRacines[rc] <= 12)) &&
            dfRacines[rc] && dfRacines[rc] <= 40 &&
            termesT.indexOf(rc) < 0) termesT.push(rc);
      }
 
      }
      if (termesT && termesT.length) {
        // La question vise les médicaments ou les bactéries ?
        var cible = /\bantibio|\batbs?\b|\babxs?\b|\bmedicament|\bmolecule|\blactam|\bquinolon|\bglycopeptid|\baminosid|\bmacrolid|\btetracyclin|\bcyclin|\blincosamid|\bpenicillin|\bcephalospor|\bcarbapenem/.test(nq2) ? "med"
                  : /\bbacterie|\bgerme|\bmicrobe|\bpathogene|\bbacille|\bcocci|\bgram/.test(nq2) ? "bact"
                  : null;
        var estMed = this._estMedicament || [];
 
        // « gram négatif » / « gram positif » : on ne garde que les pages dont
        // le wiki dit qu'elles sont de ce gram-là.
        var gram = /\bgram\s*(-|neg)/.test(nq2) ? "neg"
                : /\bgram\s*(\+|pos)/.test(nq2) ? "pos" : null;
        // Le gram vient de la page « Bactéries », qui classe tout le wiki.
        var gramPage = [];
        if (gram) ix.pages.forEach(function (pg) { if (pg.gram) gramPage[pg.id] = pg.gram; });
 
        // Le wiki n'écrit le gram en toutes lettres que sur quelques pages
        // (ailleurs il est dans l'image d'en-tête). Si le filtre ne laisse
        // presque rien, on l'abandonne et on le dit, plutôt que de rendre une
        // liste tronquée qui aurait l'air complète.
        var gramUtilisable = false;
        if (gram) {
          var n5 = 0;
          gramPage.forEach(function (g5) { if (g5 === gram) n5++; });
          gramUtilisable = n5 >= 3;
        }
        var gramIgnore = !!gram && !gramUtilisable;
 
        // Morphologie demandée : cocci, bacille, coccobacille.
        var morph = /\bcoccobacill/.test(nq2) ? "coccobacille"
                  : /\bcocci|\bcoque/.test(nq2) ? "cocci"
                  : /\bbacill/.test(nq2) ? "bacille" : null;
        var morphoPage = [];
        if (morph) ix.pages.forEach(function (pg) { if (pg.morpho) morphoPage[pg.id] = pg.morpho; });
        var nM = 0;
        if (morph) morphoPage.forEach(function (v) { if (v === morph) nM++; });
        var morphUtilisable = nM >= 3;
 
        // Classe d'antibiotique demandee (page « Antibiotiques »).
        var cl = /\blactam|\bpenicillin|\bcephalospor|\bcarbapenem/.test(nq2) ? "beta-lactamine"
              : /\bquinolon/.test(nq2) ? "quinolone"
              : /\bglycopeptid/.test(nq2) ? "glycopeptide"
              : /\baminosid|\baminoglycosid/.test(nq2) ? "aminoside"
              : /\bmacrolid/.test(nq2) ? "macrolide"
              : /\btetracyclin|\bcyclin/.test(nq2) ? "tetracycline"
              : /\blincosamid/.test(nq2) ? "lincosamide" : null;
        var classePage = [];
        if (cl) ix.pages.forEach(function (pg) { if (pg.classe) classePage[pg.id] = pg.classe; });
        var nC = 0;
        if (cl) classePage.forEach(function (v) { if (v === cl) nC++; });
        var clUtilisable = nC >= 2;
 
        var meilleures = {}, meilleuresB = {}, aBonusT = false;
        ix.sections.forEach(function (sx, k) {
          // « antibiotique » = page liee depuis la page Antibiotiques, ou
          // page reconnue structurellement comme un medicament.
          var estATB = estMed[sx.page] || !!ix.pages[sx.page].antibio;
          if (cible === "med" && !estATB) return;
          if (cible === "bact" && estATB) return;
          if (gramUtilisable && gramPage[sx.page] !== gram) return;
          if (morphUtilisable && morphoPage[sx.page] !== morph) return;
          if (clUtilisable && classePage[sx.page] !== cl) return;
          var r4 = racines[k]; if (!r4) return;
          var trouves4 = termesT.filter(function (w) { return r4[w]; });
          if (!trouves4.length) return;
          var ts4 = normalise(sx.titre), bonus4 = 0;
          for (var a4 = 0; a4 < inten.length; a4++) {
            var ti4 = inten[a4].titres;
            for (var c4 = 0; c4 < ti4.length; c4++) if (ts4.indexOf(ti4[c4]) >= 0) bonus4 = 1;
          }
          if (bonus4) aBonusT = true;
          var occ4 = 0;
          trouves4.forEach(function (w) { occ4 += r4[w]; });
          var sc4 = trouves4.length * 3 + occ4 + bonus4 * 3;
          if (!meilleures[sx.page] || meilleures[sx.page].sc < sc4) {
            meilleures[sx.page] = { s: sx, sc: sc4, bonus: bonus4 };
          }
          // On retient à part la meilleure section qui répond vraiment à la
          // sorte de question posée : sinon une page perdait sa place parce
          // qu'une autre de ses sections citait le mot plus souvent.
          if (bonus4 && (!meilleuresB[sx.page] || meilleuresB[sx.page].sc < sc4)) {
            meilleuresB[sx.page] = { s: sx, sc: sc4, bonus: bonus4 };
          }
        });
        if (aBonusT) {
          Object.keys(meilleures).forEach(function (kk) {
            if (meilleuresB[kk]) meilleures[kk] = meilleuresB[kk];
            else delete meilleures[kk];
          });
        }
 
        var listeT = Object.keys(meilleures)
          .sort(function (x, y) { return meilleures[y].sc - meilleures[x].sc; })
          .slice(0, 8)
          .map(function (pid) {
            var page = ix.pages[pid], sx = meilleures[pid].s;
            var lignes = sx.prose.concat(sx.puces).filter(function (l) {
              var mm = moi.mots(l).map(stem);
              return termesT.some(function (w) { return mm.indexOf(w) >= 0; });
            }).slice(0, 3);
            if (!lignes.length) return null;
            return { page: page.titre, url: page.url, images: [],
                    sections: [{ titre: sx.titre, prose: lignes, puces: [] }] };
          }).filter(Boolean);
 
        if (listeT.length >= 2) {
          return {
            etat: "transversal",
            intro: "Aucune page du wiki ne porte sur ce sujet précis, mais il en est question " +
                  "sur " + listeT.length + " pages. Voici chaque passage, recopié tel quel. " +
                  "La liste se limite à ce que le wiki dit : elle n'est pas forcément complète." +
                  (gramIgnore ? " Le Gram n'a pas pu être départagé : la liste n'est pas " +
                    "filtrée par Gram." : ""),
            blocs: listeT
          };
        }
      }
 
      // SIMPLE MENTION. Le sujet n'a pas de page, mais il est cité quelque
      // part — « la doxycycline couvre les rickettsies ». On ne présente
      // surtout pas ça comme LE traitement : on montre la phrase exacte et on
      // dit franchement que le wiki ne traite pas le sujet.
      var faibles = this._designeFaible || {};
      var poidsQ = this.etendre(question);
      var citees = {}, termes = [];
      for (var fw in poidsQ) {
        if (poidsQ[fw] >= 1 && faibles[fw] !== undefined && !GENERIQUES[stem(fw)]) {
          citees[faibles[fw]] = 1;
          if (termes.indexOf(fw) < 0) termes.push(fw);
        }
      }
      var pagesCitees = Object.keys(citees).slice(0, 3).map(function (pid) {
        var page = ix.pages[pid];
        var phrases = [];
        ix.sections.forEach(function (s) {
          if (s.page !== +pid) return;
          s.prose.concat(s.puces).forEach(function (l) {
            var nl = normalise(l);
            for (var k = 0; k < termes.length; k++) {
              if (nl.indexOf(termes[k]) >= 0 && phrases.length < 2) { phrases.push(l); break; }
            }
          });
        });
        return { page: page.titre, url: page.url, images: [],
                sections: phrases.length ? [{ titre: "Mention sur cette page",
                                              prose: phrases, puces: [] }] : [] };
      }).filter(function (b) { return b.sections.length; });
 
      if (pagesCitees.length) {
        // L'avertissement sur « le traitement complet » n'a de sens que pour
        // une bactérie ou un médicament. Sur une page de laboratoire, il est
        // absurde : une définition n'est pas un traitement.
        var toutLabo = pagesCitees.every(function (b) {
          for (var li = 0; li < ix.pages.length; li++) {
            if (ix.pages[li].titre === b.page) return !!ix.pages[li].labo;
          }
          return false;
        });
        return {
          etat: "mention",
          intro: toutLabo
            ? "Microguide n'a pas de page sur ce sujet précis. Il en est question en passant " +
              "sur la ou les pages ci-dessous; ce n'est donc pas un exposé complet."
            : "Microguide n'a pas de page sur ce sujet. Il est seulement mentionné en " +
              "passant sur la ou les pages ci-dessous. Ce n'est donc pas forcément le " +
              "traitement complet ni le traitement de choix : à valider ailleurs.",
          blocs: pagesCitees
        };
      }
 
      if (!nommes.length) {
        return {
          etat: "hors-sujet",
          intro: "Cette information n'est pas dans Microguide. Le wiki contient des pages de " +
                "bactéries et des pages d'antibiotiques; elle sera peut-être ajoutée dans une " +
                "version ultérieure.",
          blocs: []
        };
      }
      // question-liste sans résultat transversal : on répond normalement
    }
    hits = nommes;
 
    // RÈGLE 2 — ne pas répondre avec un organisme voisin. Une page n'est
    // gardée que si elle reconnaît quelque chose que la meilleure page ne
    // reconnaît pas : « Staphylococcus coagulase négative » ne reconnaît que
    // « staphylococcus », déjà reconnu par « Staphylococcus aureus », donc on
    // l'écarte. Mais pour « est-ce que la ceftriaxone couvre le pseudomonas »,
    // chaque page apporte son propre nom et les deux sont gardées.
    var entMots = this._entMots, entite = this._entite;
    var tete = entMots[ix.sections[hits[0].i].page] || {};
    hits = hits.filter(function (h) {
      var pid = ix.sections[h.i].page;
      if (pid === ix.sections[hits[0].i].page) return true;
      if (!entite[pid]) return false;                // page non nommée
      var mots = entMots[pid] || {};
      for (var w in mots) { if (!tete[w]) return true; }  // apporte un nom neuf
      return false;
    });
 
    // on regroupe par page, en gardant l'ordre de pertinence
    var parPage = {}, ordre = [];
    hits.forEach(function (h) {
      var s = ix.sections[h.i];
      if (!parPage[s.page]) { parPage[s.page] = []; ordre.push(s.page); }
      parPage[s.page].push({ s: s, score: h.score });
    });
 
    var meilleur = hits[0].score;
    var blocs = [];
    // page principale : jusqu'à 3 sections ; puis 2 pages d'appoint, 1 section
    ordre.slice(0, 3).forEach(function (pid, rang) {
      var page = ix.pages[pid];
      var vuTitre = {};
      var retenues = parPage[pid]
        .filter(function (x) {
          // Une section sans texte (contenu uniquement en image) occuperait
          // une place à l'écran pour n'afficher qu'une boîte vide, et
          // chasserait une section utile. Son image est déjà montrée à part.
          if (!x.s.prose.length && !x.s.puces.length) return false;
          // jamais deux fois la même section
          if (vuTitre[x.s.titre]) return false;
          vuTitre[x.s.titre] = 1;
          return x.score >= meilleur * (rang === 0 ? 0.28 : 0.45);
        });
      var secs = retenues.slice(0, rang === 0 ? 3 : 1);
      // Si la question demande un traitement, la section de traitement doit
      // être montrée même si d'autres sections ont un meilleur score.
      if (rang === 0 && inten.length) {
        var colle = function (x) {
          var ts = normalise(x.s.titre);
          for (var a2 = 0; a2 < inten.length; a2++) {
            var ti = inten[a2].titres;
            for (var c2 = 0; c2 < ti.length; c2++) if (ts.indexOf(ti[c2]) >= 0) return true;
          }
          return false;
        };
        if (!secs.some(colle)) {
          for (var z = 0; z < retenues.length; z++) {
            if (colle(retenues[z])) { secs = [retenues[z]].concat(secs).slice(0, 3); break; }
          }
        }
        // Une question de traitement doit montrer la section « Traitements »
        // elle-même, pas seulement le tableau d'antibiogramme qui l'accompagne.
        var veutTx = inten.some(function (x) { return x.nom === "traitement"; });
        var estTx = function (x) { return /trait|efficac/.test(normalise(x.s.titre)); };
        if (veutTx && !secs.some(estTx)) {
          for (var z2 = 0; z2 < retenues.length; z2++) {
            if (estTx(retenues[z2])) { secs = [retenues[z2]].concat(secs).slice(0, 3); break; }
          }
        }
      }
      if (!secs.length) return;
      // Images à afficher : celles dont la section correspond à la sorte de
      // question posée (un tableau de couverture pour une question de
      // traitement), plus la fiche d'en-tête de la page quand c'est la page
      // principale. Les images résument souvent mieux que le texte.
      var titresRetenus = {};
      secs.forEach(function (x) { titresRetenus[normalise(x.s.titre)] = 1; });
      var images = (page.images || []).filter(function (im) {
        var ns = normalise(im.section);
        if (titresRetenus[ns]) return true;
        for (var a = 0; a < inten.length; a++) {
          var titres = inten[a].titres;
          for (var c = 0; c < titres.length; c++) {
            if (ns.indexOf(titres[c]) >= 0) return true;
          }
        }
        return rang === 0 && im.entete;
      }).slice(0, 3);
 
      blocs.push({
        page: page.titre,
        url: page.url,
        images: images,
        sections: secs.map(function (x) {
          return { titre: x.s.titre, prose: x.s.prose, puces: x.s.puces };
        })
      });
    });
 
    var faible = meilleur < 6;
    var p0 = blocs.length ? blocs[0].page : null;
    var intro;
    if (faible) {
      intro = "Je ne trouve pas de passage qui réponde clairement à cette question. Voici ce " +
              "qui s'en rapproche le plus, recopié tel quel. À vérifier : ce n'est peut-être " +
              "pas ce que vous cherchez.";
    } else {
      intro = "D'après " + (blocs.length > 1 ? "les pages du wiki" : "la page « " + p0 + " »") +
              ", voici ce qui est écrit sur microguide.ca, recopié mot pour mot :";
    }
 
    // DOSES DU CALCULATEUR. Quand on demande une posologie et que la section
    // Posologie de la page n'en donne pas (ou pas selon la fonction renale),
    // on complete avec les paliers du calculateur de doses du wiki. Ce sont
    // les valeurs ecrites par Thomas lui-meme, recopiees telles quelles.
    var veutDose = inten.some(function (x) { return x.nom === "posologie" || x.nom === "ajustement"; });
    if (veutDose && blocs.length) {
      var pgDose = null;
      for (var pdi = 0; pdi < ix.pages.length; pdi++) {
        if (ix.pages[pdi].titre === blocs[0].page) { pgDose = ix.pages[pdi]; break; }
      }
      if (pgDose && pgDose.doses && pgDose.doses.length) {
        var dejaDose = blocs[0].sections.some(function (s) {
          return /\d\s*(mg|g)\b/i.test(s.prose.concat(s.puces).join(" "));
        });
        var veutRenal = inten.some(function (x) { return x.nom === "ajustement"; });
        if (!dejaDose || veutRenal) {
          // On insère en tête, donc on parcourt à l'envers : la posologie
          // usuelle doit rester la première affichée, pas la variante.
          var listeD = pgDose.doses.slice().sort(function (a, b) {
            var ua = /usuel/i.test(a.nom) ? 0 : 1, ub = /usuel/i.test(b.nom) ? 0 : 1;
            return ua - ub;
          }).reverse();
          listeD.forEach(function (d) {
            var lignes = d.paliers.map(function (pal) {
              var plage = pal.max >= 999 ? "clairance \u2265 " + pal.min
                        : pal.min <= 0 ? "clairance < " + (pal.max + 1)
                        : "clairance " + pal.min + " \u00e0 " + pal.max;
              return plage + " mL/min : " + pal.label;
            });
            if (d.note) lignes.push(d.note);
            blocs[0].sections.unshift({
              titre: "Calculateur de doses \u2014 " + d.nom,
              prose: [], puces: lignes
            });
          });
          blocs[0].sections = blocs[0].sections.slice(0, 4);
        }
      }
    }
 
    // Un medicament present UNIQUEMENT dans le calculateur n'a pas de page :
    // on ne connait que sa dose. Il faut le dire, sinon une question de
    // spectre recevrait un tableau de doses sans explication.
    if (blocs.length) {
      var seulCalc = true;
      for (var sc = 0; sc < blocs.length; sc++) {
        var pgc = null;
        for (var sq = 0; sq < ix.pages.length; sq++) {
          if (ix.pages[sq].titre === blocs[sc].page) { pgc = ix.pages[sq]; break; }
        }
        if (!pgc || !pgc.calcNom) { seulCalc = false; break; }
      }
      if (seulCalc) {
        intro = "Microguide n'a pas de page sur ce m\u00e9dicament : seul son dosage figure " +
                "dans le calculateur de doses du wiki. Le voici, recopi\u00e9 tel quel. Pour le " +
                "spectre ou les indications, l'information n'est pas dans le wiki.";
        // Pour une question de dose, le calculateur EST la r\u00e9ponse. Pour toute
        // autre question (spectre, effets, interactions), le wiki ne r\u00e9pond pas :
        // montrer un tableau de doses laisserait croire le contraire.
        if (!veutDose) {
          return { etat: "mention", intro: intro, blocs: blocs };
        }
      }
    }
 
    // L'INFORMATION EST DANS UNE IMAGE. Si la section qui repond a la question
    // existe mais ne contient qu'un tableau en image, il faut le DIRE : sinon
    // l'utilisateur croit que le wiki ne couvre pas le sujet, alors que
    // l'information est la, sous ses yeux, dans la figure.
    if (blocs.length && inten.length) {
      var pgImg = null;
      for (var zi = 0; zi < ix.pages.length; zi++) {
        if (ix.pages[zi].titre === blocs[0].page) { pgImg = ix.pages[zi]; break; }
      }
      if (pgImg) {
        var titresAffiches = {};
        blocs[0].sections.forEach(function (s) { titresAffiches[normalise(s.titre)] = 1; });
        var muettes = [];
        ix.sections.forEach(function (s) {
          if (s.page !== pgImg.id) return;
          if (s.prose.length || s.puces.length) return;      // elle a du texte
          var ts7 = normalise(s.titre);
          if (titresAffiches[ts7]) return;
          for (var a7 = 0; a7 < inten.length; a7++) {
            var ti7 = inten[a7].titres;
            for (var c7 = 0; c7 < ti7.length; c7++) {
              if (ts7.indexOf(ti7[c7]) >= 0 && muettes.indexOf(s.titre) < 0) muettes.push(s.titre);
            }
          }
        });
        if (muettes.length) {
          blocs[0].sections.push({
            titre: "\u00c0 lire dans la figure",
            prose: ["Sur cette page, « " + muettes.join(" » et « ") + " » n'existe " +
                    "qu'en image : le wiki n'en donne aucun texte. L'information est donc " +
                    "uniquement dans le tableau affich\u00e9 ci-dessus, pas citable mot pour mot."],
            puces: []
          });
        }
      }
    }
 
    // ====================================================================
    // LA PAGE EXISTE, MAIS PAS CETTE INFORMATION (28 oct. 2026).
    // « dose de X » alors que la fiche de X n'a aucune section de posologie :
    // l'assistant recopiait une autre section de la page (spectre, perles),
    // comme si c'était la réponse. On le DIT : la bonne page est trouvée,
    // mais elle n'a pas de section sur ce qui est demandé. Les sections
    // restent affichées, le visiteur juge. Règle volontairement étroite :
    // seulement les intentions qui ont un titre de section propre, et seulement
    // si AUCUNE section de la page (même en image) ne porte ce titre.
    // Mesuré le 28 oct. 2026 : une première version (6 sortes d'information,
    // déclencheurs larges) se trompait souvent — les interactions sont écrites
    // dans les « Perles », « éviter » n'est pas une question de prévention.
    // D'où trois sortes seulement, des mots qui ne trompent pas, et une
    // vérification du TEXTE affiché, pas seulement des titres.
    var SECTION_PROPRE = {
      posologie:  { quoi: "la posologie",
                    demande: /\b(dose|doses|posologie|poso|dosage)\b/,
                    titre: /posologie|dose/,
                    texte: /\d\s*(mg|g|ui|mcg|µg)\b|mg\/kg/ },
      effets:      { quoi: "les effets indésirables",
                    demande: /\beffets? (secondaires?|indesirables?)\b/,
                    titre: /effet|indesirable|secondaire|toxicite/,
                    texte: /effet|indesirable|toxicit/ },
      interaction: { quoi: "les interactions",
                    demande: /\binteractions?\b/,
                    titre: /interaction/,
                    texte: /interaction|inr|warfarin|coumadin|cyp|qt\b/ }
    };
    if (blocs.length && !faible && !veutListe) {
      var pgSp = null;
      for (var qs = 0; qs < ix.pages.length; qs++) {
        if (ix.pages[qs].titre === blocs[0].page) { pgSp = ix.pages[qs]; break; }
      }
      var nqs = normalise(question);
      var affiche = normalise(blocs[0].sections.map(function (s) {
        return s.titre + " " + s.prose.join(" ") + " " + s.puces.join(" ");
      }).join(" "));
      var manque = [];
      if (pgSp) Object.keys(SECTION_PROPRE).forEach(function (k) {
        var sp = SECTION_PROPRE[k];
        if (!sp.demande.test(nqs)) return;
        if (sp.texte.test(affiche)) return;              // la réponse est affichée
        var aSection = ix.sections.some(function (sx) {  // même en image
          return sx.page === pgSp.id && sp.titre.test(normalise(sx.titre));
        });
        if (!aSection) manque.push(sp.quoi);
      });
      if (manque.length) {
        return {
          etat: "mention",
          intro: "La page « " + blocs[0].page + " » existe, mais elle ne donne pas " +
                manque.join(" ni ") + ". Voici ce qu'elle contient de plus proche, recopié " +
                "tel quel : ce n'est peut-être pas la réponse à votre question.",
          blocs: blocs
        };
      }
    }
 
    // ====================================================================
    // HORS DU PÉRIMÈTRE DU WIKI.
    // Microguide documente des bactéries et des antibiotiques. Il ne
    // documente pas les protocoles d'hygiène, les seuils de laboratoire, les
    // scores cliniques ni les cibles de dosage sérique. Une question de ce
    // genre nomme pourtant souvent une vraie page (« dépistage du SARM »,
    // « cible AUC de la vanco »), et l'assistant répondait avec elle : la
    // bonne page, mais pas la réponse — ce qui est pire qu'un refus.
    // Le filtre ne s'applique PAS si la page a justement une section sur ce
    // thème : le wiki parle bien du dépistage du strep B en grossesse.
    var HORS_PERIMETRE = [
      { quoi: "les protocoles d'hygiène et d'isolement",
        re: /\b(protocole|precaution|precautions|isolement|isoler|desinfect|lavage des mains|depistage a l admission|chambre individuelle)\b/ },
      { quoi: "les seuils et valeurs de laboratoire",
        re: /\b(valeur normale|valeurs normales|seuil|seuils|cible|cibles|creux|taux serique|dosage serique|auc|procalcitonine|crp)\b/ },
      { quoi: "les scores et critères cliniques",
        re: /\b(score|scores|criteres de severite|critere de severite|classification de|indication operatoire|chirurgical)\b/ }
    ];
    if (blocs.length && !veutListe) {
      var nqp = normalise(question).replace(/[.'\u2019-]/g, " ");
      for (var hp = 0; hp < HORS_PERIMETRE.length; hp++) {
        var trouve = nqp.match(HORS_PERIMETRE[hp].re);
        if (!trouve) continue;
        // on cherche sur la page LE terme employé, pas toute la famille :
        // la page de la vanco parle de « cible » pour autre chose que l'AUC.
        var termeHp = new RegExp("\\b" + trouve[0].replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "\\b");
        // la page en parle-t-elle quand même ? (titre de section ou texte)
        var pgHp = null;
        for (var qh = 0; qh < ix.pages.length; qh++) {
          if (ix.pages[qh].titre === blocs[0].page) { pgHp = ix.pages[qh]; break; }
        }
        var enParle = false;
        if (pgHp) {
          ix.sections.forEach(function (sx) {
            if (sx.page !== pgHp.id || enParle) return;
            var tout = normalise(sx.titre + " " + sx.prose.join(" ") + " " + sx.puces.join(" "))
                        .replace(/[.'\u2019-]/g, " ");
            if (termeHp.test(tout)) enParle = true;
          });
        }
        if (!enParle) {
          return {
            etat: "mention",
            intro: "Microguide ne couvre pas " + HORS_PERIMETRE[hp].quoi + " : c'est un guide " +
                  "des bactéries et des antibiotiques. La page ci-dessous porte sur le bon " +
                  "sujet, mais elle ne répond pas à cette question-là.",
            blocs: blocs
          };
        }
      }
    }
 
    return { etat: faible ? "faible" : "ok", intro: intro, blocs: blocs };
  };
 
  // ====================================================================
  // HÉSITATION → « Vous parliez plutôt de… ». Principe repris des modèles
  // de décision (Jev, TypeSafe AI) : un choix fermé porte une confiance, et
  // quand elle est basse on DEMANDE au lieu de deviner. Mesuré le 27 oct.
  // 2026 sur les jeux d'atelier et scellé : quand une autre page obtient au
  // moins 40 % du score de la meilleure, les réponses ne sont justes
  // qu'environ 2 fois sur 3, contre 9 sur 10 ailleurs. La réponse ne change
  // pas : on ajoute seulement, dans « autres », jusqu'à 2 pages proposées en
  // boutons. Sur un refus, ce sont les pages les plus proches, à ouvrir
  // « quand même » : le visiteur juge, le refus reste affiché.
  var SEUIL_HESITATION = 0.4;
  Moteur.prototype.repondre = function (question) {
    var r = this._repondre(question);
    r.autres = [];
    if (r.etat === "transversal" || r.etat === "classification") return r;
    var ix = this.ix, bruts = this.chercher(question, 10);
    if (!bruts.length) return r;
    var vus = {};
    r.blocs.forEach(function (b) { vus[b.page] = 1; });
    var s1 = bruts[0].score;
    for (var i = 0; i < bruts.length && r.autres.length < 2; i++) {
      var pg = ix.pages[ix.sections[bruts[i].i].page];
      if (vus[pg.titre]) continue;
      vus[pg.titre] = 1;
      if (bruts[i].score >= SEUIL_HESITATION * s1) r.autres.push(pg.titre);
    }
    return r;
  };
 
  // Le visiteur a choisi la page : on montre ce qu'ELLE dit sur la question,
  // ses sections les mieux classées (au plus 3), toujours recopiées telles
  // quelles. Sans section touchée par les mots de la question, ses premières
  // sections.
  Moteur.prototype.repondreSur = function (question, titre) {
    var ix = this.ix, page = null;
    for (var i = 0; i < ix.pages.length; i++) if (ix.pages[i].titre === titre) { page = ix.pages[i]; break; }
    if (!page) return { etat: "rien", intro: "Cette page n'existe plus dans le wiki.", blocs: [], autres: [] };
    var aTexte = function (s) { return s.prose.length || s.puces.length; };
    var secs = [], vu = {};
    this.chercher(question, ix.sections.length).forEach(function (h) {
      var s = ix.sections[h.i];
      if (s.page === page.id && aTexte(s) && !vu[s.titre] && secs.length < 3) { vu[s.titre] = 1; secs.push(s); }
    });
    if (!secs.length) {
      ix.sections.forEach(function (s) {
        if (s.page === page.id && aTexte(s) && !vu[s.titre] && secs.length < 2) { vu[s.titre] = 1; secs.push(s); }
      });
    }
    return {
      etat: "ok",
      intro: "D'après la page « " + page.titre + " », voici ce qui est écrit sur microguide.ca, recopié mot pour mot :",
      blocs: [{
        page: page.titre, url: page.url,
        images: (page.images || []).filter(function (im) { return im.entete; }).slice(0, 1),
        sections: secs.map(function (s) { return { titre: s.titre, prose: s.prose, puces: s.puces }; })
      }],
      autres: []
    };
  };
 
  // Rendu texte (pour les essais en ligne de commande)
  Moteur.prototype.texte = function (question) {
    var r = this.repondre(question);
    var out = ["Q: " + question, "", r.intro, ""];
    r.blocs.forEach(function (b) {
      out.push("── " + b.page + "  <" + b.url + ">");
      (b.images || []).forEach(function (im) {
        out.push("  [IMAGE] " + im.nom + "  (§ " + im.section + ")");
      });
      b.sections.forEach(function (s) {
        out.push("  § " + s.titre);
        s.prose.forEach(function (p) { out.push("    " + p); });
        s.puces.forEach(function (p) { out.push("    • " + p); });
      });
      out.push("");
    });
    return out.join("\n");
  };
 
  root.MicroguideMoteur = Moteur;
})(typeof module !== "undefined" ? module.exports : window);
 
/* ---------- 4/4 bulle ---------- */
// Bulle de l'assistant microguide : lanceur en bas à droite, visible sur
// toutes les pages du wiki. Tout tourne dans le navigateur du visiteur, à
// partir de l'API du wiki : aucun serveur, aucune clé. Seule exception, un
// signalement de mauvaise réponse est envoyé (si le visiteur clique) au relais
// Google Apps Script du compte Microguide, qui le transmet par courriel.
 
(function () {
  "use strict";
 
  if (window.__microguideAssistant) return;  // déjà chargé
  window.__microguideAssistant = true;
  // Numéro de version, écrit par assembler.sh. Il sert à vérifier CE QUI EST
  // EN LIGNE : deux fois cette semaine, une correction était faite sur le Mac
  // mais pas publiée, et je l'ai annoncée comme réglée. Plus jamais.
  window.MICROGUIDE_VERSION = "2026-10-28-1368b589";
 
  var BASE = (window.MICROGUIDE_BASE !== undefined)
    ? window.MICROGUIDE_BASE
    : (location.origin || "");
 
  var CSS = [
    '.mga-launch{position:fixed;z-index:9000;right:calc(18px + env(safe-area-inset-right,0px));',
    'bottom:calc(18px + env(safe-area-inset-bottom,0px));display:flex;align-items:center;gap:9px;',
    'background:#3f7490;color:#fff;border:none;font:600 14.5px/1 inherit;padding:13px 19px;',
    'border-radius:30px;cursor:pointer;box-shadow:0 6px 22px -6px rgba(20,40,55,.5)}',
    '.mga-launch:hover{filter:brightness(1.08)}',
    '.mga-launch svg{width:18px;height:18px;flex:0 0 auto}',
    '.mga-panel{position:fixed;z-index:9001;right:calc(18px + env(safe-area-inset-right,0px));',
    'bottom:calc(18px + env(safe-area-inset-bottom,0px));',
    'width:min(440px,calc(100vw - 32px));max-height:min(680px,calc(100vh - 36px));',
    // Pas de dvh : sur iOS, les unités liées au viewport dynamique forcent
    // Safari à recalculer la hauteur quand la barre d'adresse se rétracte, ce
    // qui déplaçait la barre d'outils fixe du thème Citizen en plein écran.
    // On reste sur des valeurs fixes, quitte à être un peu plus prudent.
    '.mga-panel{max-height:min(680px,calc(100vh - 36px))}',
    'background:#fff;color:#1c2228;border:1px solid #dcd8d0;border-radius:12px;',
    'box-shadow:0 18px 50px -14px rgba(15,30,42,.45);display:flex;flex-direction:column;',
    'overflow:hidden;font:400 15px/1.6 "IBM Plex Sans",system-ui,sans-serif}',
    '.mga-head{display:flex;align-items:center;gap:10px;padding:13px 15px;border-bottom:1px solid #dcd8d0}',
    '.mga-head .t{font-weight:600;font-size:14.5px;flex:1;min-width:0}',
    '.mga-ico{background:none;border:none;color:#8995a0;cursor:pointer;font-size:19px;line-height:1;padding:4px 6px;border-radius:5px}',
    '.mga-ico:hover{color:#1c2228;background:#f4f1ea}',
    '.mga-log{flex:1;overflow-y:auto;padding:15px;display:flex;flex-direction:column;gap:17px;min-height:0}',
    '.mga-marge{flex:0 0 auto}',
    '.mga-intro{font-size:13.5px;color:#55616b}',
    // Lien de signalement : discret, sous la réponse.
    '.mga-signal{display:block;margin:6px 0 2px;padding:0;border:none;background:none;',
    '  color:#8a949b;font:400 11.5px/1.4 inherit;cursor:pointer;text-decoration:underline;',
    '  text-underline-offset:2px}',
    '.mga-signal:hover{color:#5a646b}',
    '.mga-signal:disabled{color:#8a949b;cursor:default;text-decoration:none}',
    '.mga-signal[hidden],.mga-sigbox[hidden]{display:none}',
    // Bulle de signalement, ouverte par le lien ci-dessus.
    '.mga-sigbox{margin:8px 0 2px;padding:10px 12px;border-left:3px solid #8a949b;',
    '  background:#f4f1ea;border-radius:0 6px 6px 0;font-size:12.5px}',
    '.mga-sigbox p{margin:0 0 6px;color:#3c464e}',
    '.mga-sigbox textarea{display:block;width:100%;box-sizing:border-box;min-height:60px;',
    '  resize:vertical;font:inherit;padding:6px 8px;border:1px solid #c9c3b6;border-radius:4px;',
    '  background:#fff;color:#1c2228;margin:0 0 7px}',
    '.mga-sigrow{display:flex;align-items:center;gap:10px;flex-wrap:wrap}',
    '.mga-sigbox button{font:inherit;cursor:pointer;border-radius:4px;padding:5px 11px}',
    '.mga-sigenv{border:1px solid #3f7490;background:#3f7490;color:#fff}',
    '.mga-sigenv:disabled{opacity:.55;cursor:default}',
    '.mga-sigann{border:1px solid #c9c3b6;background:transparent;color:#3c464e}',
    '.mga-sigetat{color:#5a646b}',
    '.mga-sigetat.err{color:#b3261e}',
    // Encadré d'accueil : plus visible que l'ancien sous-titre gris,
    // il porte les deux phrases qui situent l'outil.
    '.mga-accueil{border-left:4px solid #3f7490;background:#e4eff6;border-radius:0 6px 6px 0;',
    '  padding:11px 14px;margin-bottom:4px;display:flex;flex-direction:column;gap:4px}',
    '.mga-accueil strong{color:#2b5b73;font-size:14px;font-weight:600;line-height:1.35}',
    '.mga-accueil span{color:#44525c;font-size:13.5px;line-height:1.45}',
    '.mga-intro p{margin:0 0 9px}',
    '.mga-samples{display:flex;flex-direction:column;gap:6px;margin-top:11px}',
    '.mga-samples button{text-align:left;background:none;border:1px solid #dcd8d0;color:#1c2228;',
    'font:400 13px/1.5 inherit;padding:8px 11px;border-radius:6px;cursor:pointer}',
    '.mga-samples button:hover{border-color:#3f7490;background:#e4eff6}',
    '.mga-q{align-self:flex-end;max-width:88%;background:#3f7490;color:#fff;padding:9px 13px;',
    'border-radius:12px 12px 3px 12px;font-size:14px}',
    '.mga-a{font-size:14px}',
    '.mga-lead{color:#55616b;margin:0 0 12px}',
    '.mga-lead.weak{background:#faf0dd;border-left:3px solid #8a5a1c;padding:9px 12px;color:#1c2228}',
    '.mga-src{border-top:1px solid #dcd8d0;padding-top:11px;margin-top:13px}',
    '.mga-src:first-of-type{border-top:none;padding-top:0;margin-top:0}',
    '.mga-srch{display:flex;align-items:baseline;gap:7px;flex-wrap:wrap;margin-bottom:8px}',
    '.mga-srch .lbl{font-family:"IBM Plex Mono",monospace;font-size:10px;letter-spacing:.1em;',
    'text-transform:uppercase;color:#8995a0}',
    '.mga-srch a{color:#3f7490;font-weight:600;font-size:14px;text-decoration:none;border-bottom:1px solid transparent}',
    '.mga-srch a:hover{border-bottom-color:#3f7490}',
    '.mga-sec{margin:0 0 11px}',
    // « Vous parliez plutôt de… » : boutons sobres, sous la réponse.
    '.mga-autres{display:flex;flex-wrap:wrap;align-items:baseline;gap:6px 8px;margin:12px 0 4px;',
    '  padding-top:10px;border-top:1px solid #dcd8d0}',
    '.mga-autres-t{flex:1 0 100%;font-size:12.5px;color:#55616b}',
    '.mga-autres button{background:none;border:1px solid #c9c3b6;color:#2b5b73;font:400 13px/1.4 inherit;',
    '  padding:5px 10px;border-radius:4px;cursor:pointer;text-align:left}',
    '.mga-autres button:hover{border-color:#3f7490;background:#e4eff6}',
    '.mga-autres button:disabled{cursor:default;color:#8a949b;background:none;border-color:#dcd8d0}',
    '.mga-autres button.choisi{color:#2b5b73;border-color:#3f7490;background:#e4eff6}',
    // « voir dans la page » : à droite du titre de section, en minuscules.
    '.mga-voir{margin-left:8px;font-family:"IBM Plex Sans",system-ui,sans-serif;font-size:11px;',
    '  letter-spacing:0;text-transform:none;color:#8995a0;text-decoration:underline;text-underline-offset:2px}',
    '.mga-voir:hover{color:#3f7490}',
    '.mga-sect{font-family:"IBM Plex Mono",monospace;font-size:10.5px;letter-spacing:.07em;',
    'text-transform:uppercase;color:#3f7490;margin-bottom:5px}',
    '.mga-quote{font-family:"IBM Plex Serif",Georgia,serif;font-size:14px;background:#f4f1ea;',
    'border-left:2px solid #dcd8d0;padding:9px 12px;color:#1c2228}',
    '.mga-quote p{margin:0 0 6px}.mga-quote p:last-child{margin-bottom:0}',
    // Une adresse longue ne se coupe pas toute seule et élargit la boîte :
    // sur un petit écran, ça faisait déborder toute la réponse.
    '.mga-quote,.mga-quote p,.mga-quote li{overflow-wrap:anywhere;word-break:break-word}',
    '.mga-quote a{overflow-wrap:anywhere}',
    '.mga-a,.mga-src,.mga-sec{min-width:0;max-width:100%}',
    '.mga-quote ul{margin:5px 0 0;padding-left:19px}.mga-quote li{margin:2px 0}',
    '.mga-fig{margin:0 0 11px}',
    '.mga-fig img{display:block;width:100%;height:auto;border:1px solid #dcd8d0;border-radius:4px;background:#fff}',
    '.mga-fig figcaption{font-family:"IBM Plex Mono",monospace;font-size:10.5px;color:#8995a0;margin-top:4px}',
    '.mga-none{background:#faf0dd;border-left:3px solid #8a5a1c;padding:11px 13px;font-size:13.5px}',
    '.mga-ask{border-top:1px solid #dcd8d0;padding:11px;display:flex;gap:8px}',
    '.mga-ask input{flex:1;min-width:0;background:#fbfaf7;color:#1c2228;border:1px solid #dcd8d0;',
    'border-radius:7px;padding:10px 12px;font-family:inherit;font-size:16px;line-height:1.4}',
    '.mga-ask input:focus{outline:2px solid #3f7490;outline-offset:-1px;border-color:#3f7490}',
    '.mga-ask button{background:#3f7490;color:#fff;border:none;border-radius:7px;padding:0 15px;',
    'font:600 14px/1 inherit;cursor:pointer}',
    '.mga-foot{padding:0 13px 11px;font-family:"IBM Plex Mono",monospace;font-size:11px;color:#8995a0}',
    '@media (max-width:460px){',
    // Plein écran utile sur téléphone, et saisie à 16px : en dessous, iOS
    // zoome automatiquement sur le champ et casse la mise en page.
    '  .mga-panel{right:max(8px,env(safe-area-inset-right,0px));',
    '    left:max(8px,env(safe-area-inset-left,0px));width:auto;',
    '    bottom:max(8px,env(safe-area-inset-bottom,0px));max-height:calc(100vh - 160px)}',
    '  .mga-ask input{font-size:16px}',
    // Sur téléphone, le thème Citizen affiche sa propre barre d'outils fixée
    // en bas (logo, loupe, menu). Le lanceur doit flotter AU-DESSUS, sinon il
    // la recouvre. On le réduit aussi à une pastille ronde pour ne pas barrer
    // l'écran : le texte n'est plus nécessaire, l'icône suffit.
    '  .mga-launch{right:max(12px,env(safe-area-inset-right,0px));',
    '    bottom:calc(72px + env(safe-area-inset-bottom,0px));',
    '    width:52px;height:52px;padding:0;border-radius:50%;justify-content:center;',
    '    font-size:0;gap:0}',
    '  .mga-launch svg{width:24px;height:24px}',
    '  .mga-panel{bottom:calc(72px + env(safe-area-inset-bottom,0px));',
    '    max-height:calc(100vh - 160px)}',
    '  .mga-fig img{max-width:100%}',
    '}',
    // Nos règles .mga-panel/.mga-launch ont une spécificité plus forte que la
    // règle [hidden] du navigateur : sans ceci, le panneau reste affiché même
    // quand il est marqué caché.
    '.mga-panel[hidden],.mga-launch[hidden]{display:none!important}'
  ].join("");
 
  var SAMPLES = [
    "Quels antibiotiques pour traiter un Pseudomonas ?",
    "Quelle est la couverture de la ceftriaxone ?",
    "Comment traite-t-on un C. difficile ?",
    "Comment traite-t-on un SARM ?",
    "Quelle est la posologie de la gentamicine ?"
  ];
 
  function el(tag, cls, txt) {
    var n = document.createElement(tag);
    if (cls) n.className = cls;
    if (txt != null) n.textContent = txt;
    return n;
  }
 
  var moteur = null, chargement = null, log, intro, champ, panel, launch;
 
  function construire() {
    var style = el("style");
    style.textContent = CSS;
    document.head.appendChild(style);
 
    launch = el("button", "mga-launch");
    launch.setAttribute("aria-label", "Ouvrir l'assistant");
    launch.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" ' +
      'stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 11.5a8.4 8.4 0 0 1-.9 ' +
      '3.8A8.5 8.5 0 0 1 12.5 20a8.4 8.4 0 0 1-3.8-.9L3 21l1.9-5.7A8.4 8.4 0 0 1 4 11.5a8.5 8.5 0 0 1 ' +
      '4.7-7.6A8.4 8.4 0 0 1 12.5 3h.5a8.5 8.5 0 0 1 8 8z"/></svg>';
    launch.appendChild(document.createTextNode("Demander au guide"));
    document.body.appendChild(launch);
 
    panel = el("section", "mga-panel");
    panel.hidden = true;
    panel.setAttribute("aria-label", "Assistant Microguide");
 
    var head = el("div", "mga-head");
    var t = el("div", "t", "Assistant Microguide");
    head.appendChild(t);
    var bReset = el("button", "mga-ico", "⟲");
    bReset.title = "Effacer";
    var bClose = el("button", "mga-ico", "✕");
    bClose.title = "Fermer";
    head.appendChild(bReset);
    head.appendChild(bClose);
    panel.appendChild(head);
 
    log = el("div", "mga-log");
    intro = el("div", "mga-intro");
    var cadre = el("div", "mga-accueil");
    cadre.appendChild(el("strong", null, "Répond uniquement avec le contenu du wiki"));
    cadre.appendChild(el("span", null, "Posez votre question en français, comme à l'oral."));
    intro.appendChild(cadre);
 
    var samples = el("div", "mga-samples");
    SAMPLES.forEach(function (q) {
      var b = el("button", null, q);
      b.addEventListener("click", function () { demander(q); });
      samples.appendChild(b);
    });
    intro.appendChild(samples);
    log.appendChild(intro);
    panel.appendChild(log);
 
    var form = el("form", "mga-ask");
    champ = el("input");
    champ.type = "text";
    champ.placeholder = "Votre question…";
    champ.autocomplete = "off";
    champ.setAttribute("aria-label", "Votre question");
    var env = el("button", null, "Chercher");
    env.type = "submit";
    form.appendChild(champ);
    form.appendChild(env);
    form.addEventListener("submit", function (e) {
      e.preventDefault();
      demander(champ.value);
    });
    panel.appendChild(form);
 
    var foot = el("div", "mga-foot", "Index reconstruit depuis le wiki");
    panel.appendChild(foot);
    document.body.appendChild(panel);
 
    // Le lanceur se place en bas à droite, mais le wiki y met déjà des choses :
    // la barre d'outils de Citizen sur téléphone, le bouton de connexion sur
    // ordinateur. Plutôt que de deviner une position, on regarde ce qui est
    // réellement à l'écran et on remonte juste assez pour ne rien recouvrir.
     function eviterLesObstacles() {
      launch.style.bottom = "";
      if (launch.hidden) return;
      var r = launch.getBoundingClientRect();
      if (!r.width) return;
      var basMax = 0;
      var tous = document.body.querySelectorAll("*");
      for (var i = 0; i < tous.length; i++) {
        var n = tous[i];
        if (n === launch || n === panel || panel.contains(n) || launch.contains(n)) continue;
        var st = getComputedStyle(n);
        if (st.position !== "fixed" && st.position !== "sticky") continue;
        if (st.visibility === "hidden" || st.display === "none" || st.opacity === "0") continue;
        var b = n.getBoundingClientRect();
        if (!b.width || !b.height || b.height > innerHeight * 0.6) continue;
        // chevauchement avec le lanceur ?
        if (b.right > r.left && b.left < r.right && b.bottom > r.top && b.top < r.bottom) {
          var hauteurDepuisLeBas = innerHeight - b.top;
          if (hauteurDepuisLeBas > basMax) basMax = hauteurDepuisLeBas;
        }
      }
      if (basMax > 0) {
        var marge = basMax + 14;
        launch.style.bottom = "calc(" + marge + "px + env(safe-area-inset-bottom,0px))";
        panel.style.bottom = "calc(" + marge + "px + env(safe-area-inset-bottom,0px))";
      } else {
        panel.style.bottom = "";
      }
    }
    setTimeout(eviterLesObstacles, 400);
    window.addEventListener("resize", eviterLesObstacles);
    window.addEventListener("orientationchange", function () { setTimeout(eviterLesObstacles, 300); });
 
    launch.addEventListener("click", ouvrir);
    bClose.addEventListener("click", fermer);
    bReset.addEventListener("click", function () {
      Array.prototype.slice.call(log.children).forEach(function (n) {
        if (n !== intro) n.remove();
      });
      intro.hidden = false;
      champ.value = "";
      champ.focus();
    });
    document.addEventListener("keydown", function (e) {
      if (e.key === "Escape" && !panel.hidden) fermer();
    });
    panel.__foot = foot;
  }
 
  function ouvrir() {
    panel.hidden = false;
    launch.hidden = true;
    precharger();
    setTimeout(function () { champ.focus(); }, 30);
  }
  function fermer() {
    panel.hidden = true;
    launch.hidden = false;
  }
 
  // L'index se bâtit au premier usage, pas au chargement de la page : la
  // navigation normale du wiki n'est donc jamais ralentie.
  function precharger() {
    if (moteur || chargement) return chargement;
    chargement = window.MicroguideIndexeur.charger(BASE).then(function (ix) {
      ix.glossaire = window.MICROGUIDE_GLOSSAIRE || {};
      ix.intentions = window.MICROGUIDE_INTENTIONS || {};
      moteur = new window.MicroguideMoteur(ix);
      if (panel.__foot) {
        panel.__foot.textContent = ix.pages.length + " pages · " + ix.sections.length + " sections";
      }
      return moteur;
    });
    return chargement;
  }
 
  // Transforme les adresses visibles en vrais liens cliquables, sans jamais
  // passer par innerHTML (le texte du wiki reste du texte, pas du code).
  function avecLiens(parent, texte) {
    var re = /https?:\/\/[^\s)\]]+/g, pos = 0, m;
    while ((m = re.exec(texte))) {
      if (m.index > pos) parent.appendChild(document.createTextNode(texte.slice(pos, m.index)));
      var a = document.createElement("a");
      a.href = m[0];
      a.textContent = m[0].length > 48 ? m[0].slice(0, 45) + "…" : m[0];
      a.target = "_blank";
      a.rel = "noopener";
      a.style.color = "#3f7490";
      parent.appendChild(a);
      pos = m.index + m[0].length;
    }
    if (pos < texte.length) parent.appendChild(document.createTextNode(texte.slice(pos)));
    return parent;
  }
 
  // Ancre MediaWiki d'une section (mode HTML5 : espaces → _, le reste tel
  // quel). Vérifiée contre l'API du wiki par ancres.js.
  function ancreSection(titre) {
    return String(titre).trim().replace(/\s+/g, "_");
  }
 
  // Lien vers la section dans la page, avec la phrase citée surlignée par le
  // navigateur (fragment de texte « #:~:text= », Chrome, Edge, Safari 16.1+,
  // Firefox 131+). Un navigateur qui ne le connaît pas va simplement à la
  // section. On prend les premiers mots de la première phrase : c'est assez
  // pour être unique, et assez court pour ne pas déborder sur un lien ou une
  // mise en forme qui casserait la correspondance.
  function lienSection(url, s, estPage, calc) {
    if (calc) return url;
    var phrase = (s.prose[0] || s.puces[0] || "").replace(/\s+/g, " ").trim();
    var mots = phrase.split(" ").slice(0, 8).join(" ").replace(/[\s,;:.!?»«()]+$/, "");
    var frag = estPage ? "" : encodeURIComponent(ancreSection(s.titre));
    if (mots) frag += ":~:text=" + encodeURIComponent(mots).replace(/-/g, "%2D");
    return url + (frag ? "#" + frag : "");
  }
 
  function rendreSection(s, url, estPage, calc) {
    var wrap = el("div", "mga-sec");
    var t = el("div", "mga-sect", s.titre);
    if (url) {
      var voir = el("a", "mga-voir", "voir dans la page");
      voir.href = lienSection(url, s, estPage, calc);
      t.appendChild(voir);
    }
    wrap.appendChild(t);
    var box = el("div", "mga-quote");
    s.prose.forEach(function (p) { box.appendChild(avecLiens(el("p"), p)); });
    if (s.puces.length) {
      var ul = el("ul");
      s.puces.forEach(function (p) { ul.appendChild(avecLiens(el("li"), p)); });
      box.appendChild(ul);
    }
    wrap.appendChild(box);
    return wrap;
  }
 
  // ----- SIGNALEMENT D'UNE MAUVAISE RÉPONSE -----
  // Les questions de Thomas et de ses collègues sont le seul jeu d'essai qui
  // ne vienne pas de ma tête : ce sont elles qui trouvent les vraies erreurs.
  // Depuis le 23 oct. 2026, un signalement est ENVOYÉ par courriel (question,
  // réponse donnée, commentaire facultatif) au relais Google Apps Script du
  // compte microguide.informations@gmail.com, rangé sous l'étiquette « IA ».
  // Il est aussi gardé dans le navigateur du visiteur, comme avant : taper
  // « signalements » dans la bulle, ou MICROGUIDE_SIGNALEMENTS() en console.
  var CLE_SIGNAL = "mga-signalements";
  // Même relais que les commentaires de pages (bloc « Commentaires Microguide »).
  // Seule adresse externe autorisée par securite.js.
  var RELAIS_SIGNALEMENT = "https://script.google.com/macros/s/AKfycbwWRFhsdOCjo89l83t07pD_2w4FCU0a1t4nXUx3dTcVYzffLumoRKc6uCxNmTrrxsVI/exec";
 
  // La réponse telle que le visiteur l'a vue, en texte brut, pour le courriel.
  function texteReponse(r) {
    var l = [r.intro || ""];
    (r.blocs || []).forEach(function (b) {
      l.push("", "Page : " + b.page + (b.url ? " (" + b.url + ")" : ""));
      (b.sections || []).forEach(function (s) {
        l.push("— " + s.titre);
        s.prose.forEach(function (p) { l.push(p); });
        s.puces.forEach(function (p) { l.push("• " + p); });
      });
    });
    return l.join("\n").slice(0, 6000);
  }
 
  function envoyerSignalement(question, r, commentaire) {
    var corps = JSON.stringify({
      type: "ia",
      question: question,
      reponse: texteReponse(r),
      etat: r.etat,
      pages: (r.blocs || []).map(function (b) { return b.page; }).join(", "),
      page: (window.mw && mw.config && mw.config.get("wgTitle")) || document.title,
      lien: location.href.split("#")[0],
      message: commentaire,
      site_web: ""
    });
    // text/plain évite la requête préalable CORS, que Apps Script ne gère pas.
    return fetch(RELAIS_SIGNALEMENT, { method: "POST", body: corps,
                                      headers: { "Content-Type": "text/plain;charset=utf-8" } })
      .then(function (rep) { return rep.json(); })
      .catch(function () {
        // Réponse illisible après la redirection de Google : renvoi en mode opaque.
        return fetch(RELAIS_SIGNALEMENT, { method: "POST", body: corps, mode: "no-cors" })
          .then(function () { return { ok: true }; });
      });
  }
 
  function signalements() {
    try { return JSON.parse(localStorage.getItem(CLE_SIGNAL) || "[]"); }
    catch (e) { return []; }
  }
 
  function signaler(question, r) {
    try {
      var l = signalements();
      l.push({
        question: question,
        date: new Date().toISOString().slice(0, 16),
        etat: r.etat,
        pages: (r.blocs || []).map(function (b) { return b.page; })
      });
      localStorage.setItem(CLE_SIGNAL, JSON.stringify(l.slice(-200)));
    } catch (e) {}
  }
 
  window.MICROGUIDE_SIGNALEMENTS = function () { return signalements(); };
 
  function rendreReponse(r, question) {
    var box = el("div", "mga-a");
    // une « mention » est un avertissement, pas une réponse : fond ambré
    box.appendChild(el("p", "mga-lead" +
      (r.etat === "faible" || r.etat === "mention" ? " weak" : ""), r.intro));
    if (!r.blocs.length) {
      box.appendChild(el("div", "mga-none",
        "Nommez une bactérie ou un antibiotique du wiki et je vous sortirai ce qui y est écrit."));
      proposer(box, question, r);
      box.appendChild(boutonSignaler(question, r));
      return box;
    }
    r.blocs.forEach(function (b) {
      var src = el("div", "mga-src");
      var h = el("div", "mga-srch");
      h.appendChild(el("span", "lbl", "Page"));
      var a = el("a", null, b.page);
      a.href = b.url;
      h.appendChild(a);
      src.appendChild(h);
      (b.images || []).forEach(function (im) {
        var fig = el("figure", "mga-fig");
        var img = document.createElement("img");
        // Une image ne peut venir que du wiki lui-meme. Si un jour l'API
        // renvoyait une adresse externe (page piegee, miroir, redirection),
        // on ne la charge pas : ca eviterait de faire fuiter la navigation
        // des lecteurs vers un tiers.
        var memeOrigine = false;
        try { memeOrigine = new URL(im.url, location.href).origin === location.origin; } catch (e) {}
        if (!memeOrigine) return;
        img.src = im.url;
        img.alt = im.section + " — " + b.page;
        img.loading = "lazy";
        fig.appendChild(img);
        fig.appendChild(el("figcaption", null, "Tableau du wiki — " + im.section));
        src.appendChild(fig);
      });
      // une section dont le contenu n'est qu'un tableau en image n'a rien à
      // écrire : l'image est déjà affichée plus haut, on saute la boîte vide
      b.sections.forEach(function (s) {
        if (!s.prose.length && !s.puces.length) return;
        // la section du calculateur de doses n'est pas un titre du wiki
        // (indexeur.js la fabrique) : on mène au haut de la page, sans phrase
        var calc = s.titre === "Posologie (calculateur de doses)";
        src.appendChild(rendreSection(s, b.url, s.titre === b.page, calc));
      });
      box.appendChild(src);
    });
    proposer(box, question, r);
    box.appendChild(boutonSignaler(question, r));
    return box;
  }
 
  // HÉSITATION : le moteur propose jusqu'à 2 autres pages (r.autres) quand il
  // n'est pas sûr de son choix. Un clic montre ce que CETTE page dit sur la
  // question, toujours recopié tel quel. Sur un refus, ce sont les pages les
  // plus proches, à ouvrir « quand même ».
  function proposer(box, question, r) {
    if (!r.autres || !r.autres.length) return;
    var refus = !r.blocs.length || r.etat === "hors-sujet";
    var w = el("div", "mga-autres");
    w.appendChild(el("span", "mga-autres-t", refus ? "Pages proches, à ouvrir quand même :"
                                                    : "Vous parliez plutôt de :"));
    r.autres.forEach(function (titre) {
      var b = el("button", null, titre);
      b.type = "button";
      b.addEventListener("click", function () {
        w.querySelectorAll("button").forEach(function (x) { x.disabled = true; });
        b.classList.add("choisi");
        precharger().then(function (m) {
          var rep = m.repondreSur(question, titre);
          var suite = rendreReponse(rep, question + " [page choisie : " + titre + "]");
          box.parentNode.insertBefore(suite, box.nextSibling);
          suite.scrollIntoView({ block: "start", behavior: "smooth" });
        });
      });
      w.appendChild(b);
    });
    box.appendChild(w);
  }
 
  // Un lien discret sous la réponse. Pas de pouce, pas de couleur : il ne doit
  // pas attirer l'œil, juste être là quand la réponse est mauvaise. Il ouvre
  // une petite bulle : commentaire facultatif, puis « Envoyer le signalement ».
  function boutonSignaler(question, r) {
    var wrap = el("div");
    var b = el("button", "mga-signal", "cette réponse est mauvaise");
    b.type = "button";
    wrap.appendChild(b);
 
    var boite = el("div", "mga-sigbox");
    boite.hidden = true;
    boite.appendChild(el("p", null, "Qu'est-ce qui ne va pas avec cette réponse ? (optionnel)"));
    var zone = document.createElement("textarea");
    zone.maxLength = 1500;
    zone.setAttribute("aria-label", "Qu'est-ce qui ne va pas avec cette réponse ? (optionnel)");
    boite.appendChild(zone);
    var ligne = el("div", "mga-sigrow");
    var envoi = el("button", "mga-sigenv", "Envoyer le signalement");
    envoi.type = "button";
    var annuler = el("button", "mga-sigann", "Annuler");
    annuler.type = "button";
    var etat = el("span", "mga-sigetat", "");
    ligne.appendChild(envoi);
    ligne.appendChild(annuler);
    ligne.appendChild(etat);
    boite.appendChild(ligne);
    wrap.appendChild(boite);
 
    b.addEventListener("click", function () {
      b.hidden = true;
      boite.hidden = false;
      zone.focus();
    });
    annuler.addEventListener("click", function () {
      boite.hidden = true;
      b.hidden = false;
    });
    envoi.addEventListener("click", function () {
      signaler(question, r);            // gardé dans ce navigateur, quoi qu'il arrive
      envoi.disabled = true;
      annuler.disabled = true;
      etat.className = "mga-sigetat";
      etat.textContent = "Envoi…";
      envoyerSignalement(question, r, zone.value.trim()).then(function (rep) {
        if (rep && rep.ok) {
          wrap.replaceChild(el("span", "mga-signal", "signalement envoyé, merci"), boite);
        } else {
          etat.className = "mga-sigetat err";
          etat.textContent = (rep && rep.erreur) || "L'envoi n'a pas fonctionné, réessaie plus tard.";
          envoi.disabled = false;
          annuler.disabled = false;
        }
      }).catch(function () {
        etat.className = "mga-sigetat err";
        etat.textContent = "L'envoi n'a pas fonctionné (connexion ?), réessaie plus tard.";
        envoi.disabled = false;
        annuler.disabled = false;
      });
    });
    return wrap;
  }
 
  // On amène le DÉBUT de la réponse en haut de la zone de lecture, pas sa
  // fin : sinon il faut remonter à chaque question pour lire la première
  // ligne. Une marge est ajoutée sous la dernière réponse pour que le début
  // puisse monter jusqu'en haut même quand la réponse est courte.
  function montrerDebut(bulleQuestion) {
    var marge = log.querySelector(".mga-marge");
    if (!marge) { marge = el("div", "mga-marge"); log.appendChild(marge); }
    else log.appendChild(marge);
    var manque = log.clientHeight - (log.scrollHeight - bulleQuestion.offsetTop + log.firstChild.offsetTop);
    marge.style.height = manque > 0 ? manque + "px" : "0px";
    log.scrollTop = bulleQuestion.offsetTop - log.offsetTop;
  }
 
  function demander(q) {
    q = (q || "").trim();
    if (!q) return;
    intro.hidden = true;
 
    // Taper « signalements » affiche les réponses marquées comme mauvaises.
    // Elles ne vivent que dans CE navigateur : sans cette commande, Thomas ne
    // peut pas les relire depuis son téléphone, où il n'a pas de console.
    if (/^signalement/i.test(q)) {
      var l = signalements();
      log.appendChild(el("div", "mga-q", q));
      var boite = el("div", "mga-a");
      boite.appendChild(el("p", "mga-lead",
        l.length ? "Voici les " + l.length + " réponse(s) que vous avez marquées comme mauvaises, " +
                  "dans ce navigateur-ci. Chacune a aussi été envoyée par courriel à l'équipe de Microguide."
                : "Aucun signalement dans ce navigateur. Ils sont enregistrés séparément sur " +
                  "chaque appareil : si vous avez cliqué sur votre téléphone, c'est là qu'ils sont."));
      if (l.length) {
        var ul = el("ul");
        l.slice().reverse().forEach(function (x) {
          ul.appendChild(el("li", null, x.date + " — « " + x.question + " » → " +
            (x.pages && x.pages.length ? x.pages.join(" + ") : "refus")));
        });
        var q2 = el("div", "mga-quote");
        q2.appendChild(ul);
        boite.appendChild(q2);
      }
      log.appendChild(boite);
      champ.value = "";
      return;
    }
 
    var bulle = el("div", "mga-q", q);
    log.appendChild(bulle);
    var attente = el("p", "mga-lead", "Je cherche dans le wiki…");
    log.appendChild(attente);
    montrerDebut(bulle);
    champ.value = "";
 
    precharger().then(function (m) {
      attente.remove();
      var rep = m.repondre(q);
      log.appendChild(rendreReponse(rep, q));
      montrerDebut(bulle);
    })["catch"](function (e) {
      attente.remove();
      log.appendChild(el("div", "mga-none",
        "Je n'ai pas pu lire le wiki (" + (e && e.message ? e.message : "erreur") +
        "). Réessayez dans un moment."));
    });
  }
 
  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", construire);
  } else {
    construire();
  }
})();
 
/* ====== FIN Assistant Microguide (essai) ====== */
 
 
/* ====== DEBUT Commentaires Microguide ======
  Formulaire « Un commentaire ou une question sur cette page ? » : un lien
  sous le titre de chaque page ouvre une petite bulle où écrire directement
  (le formulaire du bas de page a été retiré). Les messages passent par le
  relais Google Apps Script du compte microguide.informations@gmail.com, qui
  les envoie par courriel.
  Indépendant de l'assistant et de la loupe : supprimer ce bloc ne touche à
  rien d'autre. Aucun identifiant ici — l'adresse du relais n'est pas secrète.
  ============ */
(function () {
  "use strict";
  if (window.__microguideCommentaires) return;
  window.__microguideCommentaires = true;
 
  // Adresse « /exec » du déploiement Apps Script (Déployer > Application Web).
  var RELAIS = "https://script.google.com/macros/s/AKfycbwWRFhsdOCjo89l83t07pD_2w4FCU0a1t4nXUx3dTcVYzffLumoRKc6uCxNmTrrxsVI/exec";
  var DELAI_ENTRE_ENVOIS = 60 * 1000;  // un envoi par minute et par visiteur
  var MAX_MESSAGE = 3000;
 
  if (RELAIS.indexOf("https://script.google.com/") !== 0) return;
  if (!window.mw || !mw.config) return;
  // Pages de contenu seulement, en lecture (pas en modification, pas les pages spéciales).
  if (mw.config.get("wgNamespaceNumber") !== 0) return;
  if (mw.config.get("wgAction") !== "view") return;
 
  var CSS = [
    '.mgc{margin:2.5em 0 1em;padding-top:1em;',
    '  border-top:1px solid var(--border-color-base,#c4d6e4);font-size:.95em}',
    '.mgc-titre{margin:0 0 .25em;font-weight:600;',
    '  color:var(--color-progressive,#1f4e68)}',
    '.mgc-aide{margin:0 0 .75em;opacity:.75;font-size:.92em}',
    '.mgc textarea,.mgc input[type=email]{display:block;width:100%;box-sizing:border-box;',
    '  font:inherit;color:inherit;background:transparent;',
    '  border:1px solid var(--border-color-base,#c4d6e4);border-radius:2px;',
    '  padding:8px 10px;margin:0 0 .6em}',
    '.mgc textarea{min-height:90px;resize:vertical}',
    '.mgc textarea:focus,.mgc input[type=email]:focus{outline:2px solid #5b8fa8;outline-offset:0}',
    '.mgc-piege{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}',
    '.mgc-ligne{display:flex;align-items:center;gap:12px;flex-wrap:wrap}',
    '.mgc button[type=submit]{font:inherit;cursor:pointer;padding:7px 16px;border-radius:2px;',
    '  border:1px solid #1f4e68;background:#1f4e68;color:#fff}',
    '.mgc button[type=submit]:disabled{opacity:.55;cursor:default}',
    '.mgc-etat{font-size:.92em}',
    '.mgc-etat.ok{color:#2e7d4f}',
    '.mgc-etat.err{color:#b3261e}',
    // lien sous le titre et bulle qu'il ouvre
    '.mgc-haut{position:relative;margin:.35em 0 0;font-size:.88em}',
    '.mgc-lien{font:inherit;background:none;border:none;padding:0;cursor:pointer;',
    '  color:var(--color-progressive,#1f4e68);text-decoration:underline;',
    '  text-decoration-color:rgba(91,143,168,.5);text-underline-offset:3px}',
    '.mgc-lien:hover{text-decoration-color:currentColor}',
    '.mgc-bulle{position:absolute;left:0;top:calc(100% + 8px);z-index:900;',
    '  width:min(420px,calc(100vw - 32px));box-sizing:border-box;padding:14px 16px 12px;',
    '  background:var(--color-surface-1,#fff);color:var(--color-base,#202122);',
    '  border:1px solid var(--border-color-base,#c4d6e4);border-top:3px solid #5b8fa8;',
    '  box-shadow:0 6px 20px rgba(20,40,60,.14)}',
    '.mgc-bulle[hidden]{display:none}',
    '.mgc-bulle .mgc{margin:0;padding:0;border:none;font-size:1em}',
    '.mgc-fermer{position:absolute;top:6px;right:8px;width:28px;height:28px;padding:0;',
    '  font:400 20px/28px system-ui,sans-serif;background:none;border:none;cursor:pointer;',
    '  color:inherit;opacity:.6}',
    '.mgc-fermer:hover{opacity:1}'
  ].join("");
 
  function lireDernierEnvoi() {
    try { return Number(localStorage.getItem("microguide_commentaire_dernier") || 0); }
    catch (e) { return 0; }
  }
  function noterEnvoi() {
    try { localStorage.setItem("microguide_commentaire_dernier", String(Date.now())); }
    catch (e) { /* navigation privée : tant pis, le relais a son propre plafond */ }
  }
 
  function envoyer(donnees) {
    var corps = JSON.stringify(donnees);
    // text/plain évite la requête préalable CORS, que Apps Script ne gère pas.
    return fetch(RELAIS, { method: "POST", body: corps,
                          headers: { "Content-Type": "text/plain;charset=utf-8" } })
      .then(function (r) { return r.json(); })
      .catch(function () {
        // Certains navigateurs bloquent la lecture de la réponse après la
        // redirection de Google : on renvoie en mode opaque, sans réponse lisible.
        return fetch(RELAIS, { method: "POST", body: corps, mode: "no-cors" })
          .then(function () { return { ok: true }; });
      });
  }
 
  // Le formulaire affiché dans la bulle.
  function creerFormulaire(page, avecTitre) {
    var bloc = document.createElement("form");
    bloc.className = "mgc";
    bloc.setAttribute("novalidate", "");
    bloc.innerHTML =
      (avecTitre ? '<p class="mgc-titre">Un commentaire ou une question sur cette page ?</p>' : '') +
      '<p class="mgc-aide">Une erreur, une précision, une suggestion : écris-nous, ça arrive directement à l\'équipe de Microguide.</p>' +
      '<label class="mgc-piege" aria-hidden="true">Site web <input type="text" name="site_web" tabindex="-1" autocomplete="off"></label>' +
      '<textarea name="message" maxlength="' + MAX_MESSAGE + '" required aria-label="Ton commentaire" placeholder="Ton commentaire ou ta question…"></textarea>' +
      '<input type="email" name="courriel" maxlength="200" aria-label="Ton courriel (facultatif)" placeholder="Ton courriel (facultatif, si tu veux une réponse)">' +
      '<div class="mgc-ligne"><button type="submit">Envoyer</button>' +
      '<span class="mgc-etat" role="status" aria-live="polite"></span></div>';
 
    var etat = bloc.querySelector(".mgc-etat");
    var bouton = bloc.querySelector("button[type=submit]");
    function afficher(texte, classe) { etat.textContent = texte; etat.className = "mgc-etat " + (classe || ""); }
 
    bloc.addEventListener("submit", function (ev) {
      ev.preventDefault();
      var message = bloc.message.value.trim();
      if (!message) { afficher("Écris d'abord ton commentaire.", "err"); return; }
      var attente = DELAI_ENTRE_ENVOIS - (Date.now() - lireDernierEnvoi());
      if (attente > 0) {
        afficher("Merci ! Attends " + Math.ceil(attente / 1000) + " secondes avant d'envoyer un autre commentaire.", "err");
        return;
      }
      bouton.disabled = true;
      afficher("Envoi…");
      envoyer({
        page: page,
        lien: location.href.split("#")[0],
        message: message,
        courriel: bloc.courriel.value.trim(),
        site_web: bloc.site_web.value
      }).then(function (r) {
        if (r && r.ok) {
          noterEnvoi();
          bloc.message.value = "";
          afficher("Merci, ton commentaire a été envoyé.", "ok");
        } else {
          afficher((r && r.erreur) || "L'envoi n'a pas fonctionné. Réessaie plus tard.", "err");
        }
      }).catch(function () {
        afficher("L'envoi n'a pas fonctionné (connexion ?). Réessaie plus tard.", "err");
      }).then(function () { bouton.disabled = false; });
    });
    return bloc;
  }
 
  // Lien sous le titre, qui ouvre la bulle juste en dessous.
  function construireHaut(page) {
    var titre = document.querySelector(".citizen-page-heading") ||
                document.getElementById("firstHeading");
    if (!titre) return;
 
    var haut = document.createElement("div");
    haut.className = "mgc-haut";
    var lien = document.createElement("button");
    lien.type = "button";
    lien.className = "mgc-lien";
    lien.textContent = "Une erreur ou une question sur cette page ? Écris-nous";
    lien.setAttribute("aria-expanded", "false");
 
    var bulle = document.createElement("div");
    bulle.className = "mgc-bulle";
    bulle.hidden = true;
    bulle.setAttribute("role", "dialog");
    bulle.setAttribute("aria-label", "Commentaire sur cette page");
    var fermer = document.createElement("button");
    fermer.type = "button";
    fermer.className = "mgc-fermer";
    fermer.setAttribute("aria-label", "Fermer");
    fermer.textContent = "×";
    bulle.appendChild(fermer);
    bulle.appendChild(creerFormulaire(page, true));
 
    function ouvrir() {
      bulle.hidden = false;
      lien.setAttribute("aria-expanded", "true");
      bulle.querySelector("textarea").focus();
    }
    function refermer() {
      bulle.hidden = true;
      lien.setAttribute("aria-expanded", "false");
    }
    lien.addEventListener("click", function () { bulle.hidden ? ouvrir() : refermer(); });
    fermer.addEventListener("click", function () { refermer(); lien.focus(); });
    document.addEventListener("keydown", function (ev) {
      if (ev.key === "Escape" && !bulle.hidden) { refermer(); lien.focus(); }
    });
    // Un clic ailleurs referme la bulle, sauf si un commentaire est en cours d'écriture.
    document.addEventListener("click", function (ev) {
      if (bulle.hidden || haut.contains(ev.target)) return;
      if (bulle.querySelector("textarea").value.trim()) return;
      refermer();
    });
 
    haut.appendChild(lien);
    haut.appendChild(bulle);
    // Dans le bloc du titre de Citizen (et non à côté : l'en-tête est une rangée
    // flexible, un voisin se retrouverait à droite du titre).
    // Thème Vector (comptes connectés) : sous la barre du titre, elle aussi flexible.
    if (titre.id === "firstHeading") {
      var barre = titre.closest(".mw-body-header") || titre;
      barre.parentNode.insertBefore(haut, barre.nextSibling);
    } else titre.appendChild(haut);
  }
 
  function construire() {
    var contenu = document.getElementById("mw-content-text");
    if (!contenu || document.querySelector(".mgc-haut")) return;
 
    var style = document.createElement("style");
    style.textContent = CSS;
    document.head.appendChild(style);
 
    var page = mw.config.get("wgTitle") || document.title;
    construireHaut(page);
  }
 
  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", construire);
  } else {
    construire();
  }
})();
/* ====== FIN Commentaires Microguide ====== */
 
/* DEBUT Spectre antibiotique */
/* Sous chaque image faite avec les générateurs du Mac (« Spectre - <antibiotique>.jpg » ou
  « Fiche - <bactérie>.jpg »), ajoute un bouton « Télécharger l'image » qui enregistre le fichier en pleine taille.
  Simple lien de téléchargement : fonctionne aussi sur Safari et iPhone. */
(function () {
var MOTIF = /^(.*?)\/(?:thumb\/)?([0-9a-f]\/[0-9a-f]{2})\/((?:Spectre|Fiche)_-_[^\/]+?\.(?:png|jpe?g))(?:\/[^\/]+)?$/i;
 
mw.hook('wikipage.content').add(function ($contenu) {
$contenu.find('img').each(function () {
var img = this;
if (img.getAttribute('data-mg-spectre')) return;
var u;
try { u = new URL(img.getAttribute('src'), location.href); } catch (e) { return; }
if (u.origin !== location.origin) return;
var m = u.pathname.match(MOTIF);
if (!m) return;
img.setAttribute('data-mg-spectre', '1');
 
var fichier = decodeURIComponent(m[3]);
var a = document.createElement('a');
a.href = m[1] + '/' + m[2] + '/' + m[3];
a.download = fichier.replace(/_/g, ' ');
a.textContent = 'Télécharger l’image';
a.style.cssText = 'display:inline-block;margin:8px 0 4px;padding:6px 14px;font-size:14px;background:#34566e;color:#fff;border-radius:3px;text-decoration:none';
var bloc = img.closest('figure, .thumb, span[typeof], .mw-default-size') || img.parentNode;
var p = document.createElement('div');
p.appendChild(a);
bloc.parentNode.insertBefore(p, bloc.nextSibling);
});
});
}());
/* FIN Spectre antibiotique */

Dernière version du 28 octobre 2026 à 16:29

// Forcer l'ouverture des sections hidden dans citizen qui les cachent par défaut

function ouvrirSections() {
    document.querySelectorAll('.citizen-section').forEach(function (el) {
        el.removeAttribute('hidden');
    });
}

document.addEventListener('DOMContentLoaded', ouvrirSections);

window.addEventListener('pageshow', function (e) {
    if (e.persisted) {
        ouvrirSections();
    }
});

/* Enlever le téléverser sur hamburger et empêcher les menus défilent */

function ouvrirSections() {
    document.querySelectorAll('.citizen-section').forEach(function (el) {
        el.removeAttribute('hidden');
    });
    var upload = document.getElementById('t-upload');
    if (upload) upload.remove();
}




/* Désactive les liens sur toutes les images */
$(document).ready(function() {
    $('.mw-file-description').each(function() {
        $(this).replaceWith($(this).children());
    });
});


// ============================================
// CALCULATEUR IRC — Microguide
// ============================================
if (mw.config.get('wgPageName') === 'Calculateur') {

    var style = document.createElement('style');
    style.textContent = `
        #calc-app * { box-sizing: border-box; font-family: 'Segoe UI', system-ui, sans-serif; }
        #calc-app { background: #f0f4f8; padding: 16px; border-radius: 12px; }
        .calc-header { background: linear-gradient(135deg, #1a5fa8 0%, #2980d4 100%); padding: 18px 22px; border-radius: 12px; color: white; margin-bottom: 16px; }
        .calc-header h1 { margin: 0; font-size: 20px; font-weight: 700; }
        .calc-header p { margin: 4px 0 0; font-size: 12px; opacity: 0.75; }
        .calc-card { background: white; border-radius: 12px; padding: 20px 22px; margin-bottom: 14px; box-shadow: 0 2px 8px rgba(0,0,0,0.06); border: 1px solid #e2e8f0; }
        .calc-card h2 { margin: 0 0 14px; font-size: 15px; font-weight: 700; color: #0f2d5e; }
        .calc-grid { display: grid; grid-template-columns: 1fr 1fr 1fr 1fr; gap: 10px; margin-bottom: 12px; }
        .calc-grid label { font-size: 11px; color: #64748b; font-weight: 600; display: block; margin-bottom: 4px; }
        .calc-grid input { width: 100%; padding: 8px 10px; border-radius: 8px; border: 1.5px solid #e2e8f0; font-size: 14px; color: #0f2d5e; font-weight: 600; outline: none; }
        .calc-sexe-btn { padding: 6px 18px; border-radius: 8px; border: 1.5px solid #e2e8f0; background: white; color: #64748b; font-weight: 600; cursor: pointer; font-size: 13px; margin-right: 8px; }
        .calc-sexe-btn.active { border-color: #0f2d5e; background: #0f2d5e; color: white; }
        .calc-result-box { background: #f8fafc; border-radius: 10px; padding: 14px 18px; display: flex; align-items: center; gap: 20px; flex-wrap: wrap; margin-top: 10px; }
        .calc-clcr-val { font-size: 32px; font-weight: 800; line-height: 1; }
        .calc-clcr-label { font-size: 12px; font-weight: 600; margin-top: 2px; }
        .calc-search { width: 100%; padding: 10px 14px; border-radius: 10px; border: 1.5px solid #e2e8f0; font-size: 14px; margin-bottom: 10px; outline: none; }
        .calc-cats { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 10px; }
        .calc-cat-btn { padding: 6px 14px; border-radius: 20px; border: 1.5px solid #e2e8f0; background: white; color: #475569; font-weight: 600; cursor: pointer; font-size: 12px; }
        .calc-cat-btn.active { border-color: #0f2d5e; background: #0f2d5e; color: white; }
        .calc-drugs { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
        .calc-drug-btn { padding: 6px 12px; border-radius: 8px; border: 1.5px solid #e2e8f0; background: white; color: #475569; font-weight: 500; cursor: pointer; font-size: 12px; }
        .calc-drug-btn.active { border-color: #1a9d8f; background: #e8f6f4; color: #0f7a6e; font-weight: 700; }
        .calc-table { width: 100%; border-collapse: collapse; font-size: 13px; margin-top: 10px; }
        .calc-table th { background: #f1f5f9; padding: 8px 12px; text-align: left; font-weight: 700; color: #0f2d5e; }
        .calc-table td { padding: 10px 12px; border-bottom: 1px solid #f1f5f9; }
        .calc-table tr.active-row { background: #e8f4f0 !important; outline: 2px solid #1a9d8f; }
        .calc-table tr.active-row td { color: #0a5a52; font-weight: 700; }
        .calc-badge { background: #1a9d8f; color: white; font-size: 10px; padding: 2px 6px; border-radius: 4px; margin-left: 8px; font-weight: 700; }
        .calc-note { background: #fefce8; border: 1px solid #fde68a; border-radius: 8px; padding: 10px 14px; font-size: 12px; color: #92400e; margin-bottom: 12px; }
        .calc-warning { background: #fef2f2; border: 1px solid #fecaca; border-radius: 10px; padding: 12px 16px; font-size: 11px; color: #991b1b; margin-top: 14px; }
        .calc-search-results button { display: block; width: 100%; text-align: left; padding: 8px 12px; margin-bottom: 4px; background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px; cursor: pointer; font-size: 13px; color: #0f2d5e; }
        .calc-hint { margin-top: 12px; padding: 10px 14px; background: #f0f9ff; border-radius: 8px; font-size: 12px; color: #0369a1; }
        .calc-imc-info { border-left: 1px solid #e2e8f0; padding-left: 16px; font-size: 11px; color: #d97706; }
        @media (max-width: 600px) { .calc-grid { grid-template-columns: 1fr 1fr; } }
        .vanco-header { background: linear-gradient(135deg, #7c3aed 0%, #9f67f5 100%); padding: 18px 22px; border-radius: 12px; color: white; margin-bottom: 16px; }
        .vanco-header h1 { margin: 0; font-size: 20px; font-weight: 700; }
        .vanco-header p { margin: 4px 0 0; font-size: 12px; opacity: 0.75; }
        .vanco-card { background: white; border-radius: 12px; padding: 20px 22px; margin-bottom: 14px; box-shadow: 0 2px 8px rgba(124,58,237,0.08); border: 1px solid #ede9fe; }
        .vanco-card h2 { margin: 0 0 14px; font-size: 15px; font-weight: 700; color: #5b21b6; }
        .vanco-section-title { font-size: 13px; font-weight: 700; color: #5b21b6; margin: 14px 0 8px; padding-bottom: 4px; border-bottom: 2px solid #ede9fe; }
        .vanco-table { width: 100%; border-collapse: collapse; font-size: 13px; margin-top: 8px; }
        .vanco-table th { background: #ede9fe; padding: 8px 12px; text-align: left; font-weight: 700; color: #5b21b6; }
        .vanco-table td { padding: 9px 12px; border-bottom: 1px solid #f5f3ff; }
        .vanco-table tr.vanco-active { background: #f3e8ff !important; }
        .vanco-table tr.vanco-active td { color: #6d28d9; font-weight: 700; }
        .vanco-badge { background: #7c3aed; color: white; font-size: 10px; padding: 2px 6px; border-radius: 4px; margin-left: 6px; font-weight: 700; }
        .vanco-input-row { display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 14px; }
        .vanco-input-group { flex: 1; min-width: 120px; }
        .vanco-input-group label { font-size: 11px; color: #7c3aed; font-weight: 600; display: block; margin-bottom: 4px; }
        .vanco-input-group input { width: 100%; padding: 8px 10px; border-radius: 8px; border: 1.5px solid #ede9fe; font-size: 14px; color: #4c1d95; font-weight: 600; outline: none; }
        .vanco-result-box { background: #f3e8ff; border: 2px solid #c4b5fd; border-radius: 10px; padding: 14px 18px; margin-top: 12px; display: flex; gap: 24px; flex-wrap: wrap; align-items: flex-start; }
        .vanco-result-val { font-size: 26px; font-weight: 800; color: #6d28d9; line-height: 1; }
        .vanco-warning { background: #fef2f2; border: 1px solid #fecaca; border-radius: 10px; padding: 12px 16px; font-size: 11px; color: #991b1b; margin-top: 14px; }
        .vanco-note { background: #fefce8; border: 1px solid #fde68a; border-radius: 8px; padding: 10px 14px; font-size: 12px; color: #92400e; margin-bottom: 12px; }
        .vanco-tab-btns { display: flex; gap: 8px; margin-bottom: 16px; }
        .vanco-tab-btn { padding: 7px 18px; border-radius: 8px; border: 1.5px solid #ede9fe; background: white; color: #7c3aed; font-weight: 600; cursor: pointer; font-size: 13px; }
        .vanco-tab-btn.active { background: #7c3aed; border-color: #7c3aed; color: white; }
        .vanco-prelevement { background: #f5f3ff; border-radius: 10px; padding: 14px 18px; margin-top: 12px; font-size: 13px; }
        @media (max-width: 600px) { .vanco-input-row { flex-direction: column; } }
    `;
    document.head.appendChild(style);

    var ANTIBIOS = {
        "Céphalosporines": {
            "Céfadroxil (PO)": { notes: "", doses: [
                { min: 30, max: 999, label: "500 mg PO q12h ou 1g PO q12-24h" },
                { min: 20, max: 29, label: "1g PO x1 dose puis 500 mg PO q12h" },
                { min: 10, max: 19, label: "1g PO x1 dose puis 500 mg PO q24h" },
                { min: 0, max: 9, label: "1g PO x1 dose puis 500 mg PO q24-36h" },
            ]},
            "Céfazoline (IV)": { notes: "", doses: [
                { min: 35, max: 999, label: "1-2g IV q8h (fréq. max. q6h)" },
                { min: 11, max: 34, label: "1-2g IV q12h" },
                { min: 0, max: 10, label: "1g IV q24h" },
            ]},
            "Céfépime (IV) — IIA/IU": { notes: "Infections intra-abdominales et urinaires", doses: [
                { min: 30, max: 999, label: "1-2g IV q12h" },
                { min: 11, max: 29, label: "1-2g IV q24h" },
                { min: 6, max: 10, label: "1g IV q24h" },
                { min: 0, max: 5, label: "500 mg IV q24h" },
            ]},
            "Céfépime (IV) — NF": { notes: "Neutropénie fébrile", doses: [
                { min: 30, max: 999, label: "2g IV q8h" },
                { min: 11, max: 29, label: "2g IV q12h" },
                { min: 6, max: 10, label: "2g IV q24h" },
                { min: 0, max: 5, label: "1g IV q24h" },
            ]},
            "Céfixime (PO)": { notes: "", doses: [
                { min: 21, max: 999, label: "400 mg PO q24h" },
                { min: 11, max: 20, label: "300 mg (¾ co.) PO q24h" },
                { min: 0, max: 10, label: "200 mg PO q24h" },
            ]},
            "Céfotaxime (IV)": { notes: "", doses: [
                { min: 21, max: 999, label: "1-2g IV q6-8h (jusqu'à 2g IV q4h si SNC)" },
                { min: 0, max: 20, label: "Diminuer la dose de 50%. Certains: 2g IV q12-24h" },
            ]},
            "Céfoxitine (IV)": { notes: "Dose max: 12g/jour", doses: [
                { min: 30, max: 999, label: "1-2g IV q6-8h" },
                { min: 11, max: 29, label: "1-2g IV q8-12h" },
                { min: 5, max: 10, label: "1-2g IV q12-24h" },
                { min: 1, max: 4, label: "50% de la dose IV q12-24h" },
                { min: 0, max: 0, label: "50% de la dose IV q24-48h" },
            ]},
            "Cefprozil (PO)": { notes: "", doses: [
                { min: 31, max: 999, label: "250-500 mg PO q12h" },
                { min: 0, max: 30, label: "250 mg PO q12h" },
            ]},
            "Ceftazidime (IV)": { notes: "", doses: [
                { min: 31, max: 999, label: "1-2g IV q8h" },
                { min: 16, max: 30, label: "1-2g IV q12h" },
                { min: 6, max: 15, label: "1-2g IV q24h" },
                { min: 1, max: 5, label: "500mg-1g IV q24h" },
                { min: 0, max: 0, label: "500mg-1g IV q48h" },
            ]},
            "Ceftobiprole (IV)": { notes: "Perfusion sur 4h si Clcr > 150 mL/min", doses: [
                { min: 31, max: 999, label: "500 mg IV q8h perf. sur 2h" },
                { min: 16, max: 30, label: "500 mg IV q12h perf. sur 2h" },
                { min: 6, max: 15, label: "250 mg IV q12h perf. sur 2h" },
                { min: 0, max: 5, label: "250 mg IV q24h perf. sur 2h" },
            ]},
            "Ceftriaxone (IV)": { notes: "Aucun ajustement requis sauf IR + IH (dose max: 2g/jour)", doses: [
                { min: 0, max: 999, label: "1-2g IV q24h (max: 2g IV q12h pour SNC)" },
            ]},
            "Céfuroxime axétil (PO)": { notes: "", doses: [
                { min: 11, max: 999, label: "250-500 mg PO q12h" },
                { min: 0, max: 10, label: "250 mg PO q24h" },
            ]},
            "Céfuroxime (IV)": { notes: "", doses: [
                { min: 21, max: 999, label: "750mg-1,5g IV q8h" },
                { min: 11, max: 20, label: "750 mg IV q12h" },
                { min: 0, max: 10, label: "750 mg IV q24h" },
            ]},
            "Céphalexine (PO)": { notes: "", doses: [
                { min: 41, max: 999, label: "250mg-1g PO q6h" },
                { min: 11, max: 40, label: "250-500 mg PO q8-12h" },
                { min: 0, max: 10, label: "250-500 mg PO q12-24h" },
            ]},
        },
        "Carbapénèmes": {
            "Ertapénem (IV)": { notes: "", doses: [
                { min: 31, max: 999, label: "1g IV q24h" },
                { min: 0, max: 30, label: "500 mg IV q24h" },
            ]},
            "Imipénem-Cilastatine (IV)": { notes: "PR ≥ 70 kg: 500 mg IV q6h (max 4g/jour). PR < 70 kg: voir monographie.", doses: [
                { min: 71, max: 999, label: "500 mg IV q6h (max: 4g/jour)" },
                { min: 41, max: 70, label: "500 mg IV q6-8h" },
                { min: 21, max: 40, label: "500 mg IV q8-12h" },
                { min: 6, max: 20, label: "250 mg IV q12h" },
                { min: 0, max: 5, label: "⚠️ Non recommandé" },
            ]},
            "Méropénem (IV) — usuel": { notes: "Posologie usuelle", doses: [
                { min: 26, max: 999, label: "1g IV q8h" },
                { min: 11, max: 25, label: "1g IV q12h" },
                { min: 6, max: 10, label: "500 mg IV q12h" },
                { min: 0, max: 5, label: "500 mg IV q24h" },
            ]},
            "Méropénem (IV) — SNC": { notes: "Infections du SNC", doses: [
                { min: 26, max: 999, label: "2g IV q8h" },
                { min: 11, max: 25, label: "2g IV q12h" },
                { min: 6, max: 10, label: "1g IV q12h" },
                { min: 0, max: 5, label: "1g IV q24h" },
            ]},
        },
        "Macrolides": {
            "Azithromycine (PO)": { notes: "Aucun ajustement requis", doses: [{ min: 0, max: 999, label: "250-500 mg PO q24h" }]},
            "Azithromycine (IV)": { notes: "Aucun ajustement requis", doses: [{ min: 0, max: 999, label: "500 mg IV q24h" }]},
            "Clarithromycine (PO)": { notes: "", doses: [
                { min: 31, max: 999, label: "250-500 mg PO q12h" },
                { min: 0, max: 30, label: "250 mg PO q24h (ou q12h si infection sévère)" },
            ]},
            "Érythromycine (PO/IV)": { notes: "Aucun ajustement requis", doses: [{ min: 0, max: 999, label: "Posologie selon indication et formulation" }]},
        },
        "Pénicillines": {
            "Amoxicilline (PO)": { notes: "", doses: [
                { min: 31, max: 999, label: "250mg-1g PO q8h" },
                { min: 11, max: 30, label: "250-500 mg PO q12h" },
                { min: 0, max: 10, label: "250-500 mg PO q24h" },
            ]},
            "Amoxicilline-Clavulanate (PO)": { notes: "", doses: [
                { min: 31, max: 999, label: "500/125 mg PO q8h ou 875/125 mg PO q12h" },
                { min: 11, max: 30, label: "500/125 mg PO q12h" },
                { min: 0, max: 10, label: "500/125 mg PO q24h" },
            ]},
            "Ampicilline (IV)": { notes: "Posologie usuelle: 1-2g IV q4-6h", doses: [
                { min: 41, max: 999, label: "1-2g IV q6h" },
                { min: 31, max: 40, label: "1-2g IV q6-8h" },
                { min: 16, max: 30, label: "1-2g IV q8-12h" },
                { min: 0, max: 15, label: "1-2g IV q12-24h" },
            ]},
            "Cloxacilline (PO)": { notes: "Aucun ajustement requis", doses: [{ min: 0, max: 999, label: "250-500 mg PO q6h (jusqu'à 4g/jour)" }]},
            "Cloxacilline (IV)": { notes: "Aucun ajustement requis", doses: [{ min: 0, max: 999, label: "1-2g IV q4-6h" }]},
            "Pénicilline G sodique (IV)": { notes: "DC = dose de charge", doses: [
                { min: 21, max: 999, label: "1-4 mU IV q4-6h" },
                { min: 11, max: 20, label: "DC suivie de 50% de la dose IV q4-6h" },
                { min: 0, max: 10, label: "DC suivie de 50% de la dose IV q8-12h" },
            ]},
            "Pipéracilline-Tazobactam (IV) — usuel": { notes: "Posologie usuelle", doses: [
                { min: 41, max: 999, label: "3,375g [3g/0,375g] IV q6h ou 4,5g [4g/0,5g] IV q8h" },
                { min: 21, max: 40, label: "2,25g [2g/0,25g] IV q6h" },
                { min: 0, max: 20, label: "2,25g [2g/0,25g] IV q8h" },
            ]},
            "Pipéracilline-Tazobactam (IV) — PAH/NF": { notes: "Pneumonie hospitalière ou neutropénie fébrile", doses: [
                { min: 41, max: 999, label: "4,5g [4g/0,5g] IV q6h" },
                { min: 21, max: 40, label: "3,375g [3g/0,375g] IV q6h" },
                { min: 0, max: 20, label: "2,25g [2g/0,25g] IV q6h" },
            ]},
        },
        "Fluoroquinolones": {
            "Ciprofloxacine (PO)": { notes: "", doses: [
                { min: 31, max: 999, label: "500-750 mg PO q12h" },
                { min: 16, max: 30, label: "500 mg PO q12h" },
                { min: 0, max: 15, label: "500 mg PO q24h ou 250 mg PO q12h" },
            ]},
            "Ciprofloxacine (IV)": { notes: "", doses: [
                { min: 31, max: 999, label: "400 mg IV q8-12h" },
                { min: 16, max: 30, label: "400 mg IV q12h" },
                { min: 0, max: 15, label: "400 mg IV q24h ou 200 mg IV q12h" },
            ]},
            "Lévofloxacine — légère/prostatite": { notes: "Ex: prostatite chronique", doses: [
                { min: 51, max: 999, label: "500 mg PO/IV q24h" },
                { min: 21, max: 50, label: "500 mg PO/IV x1 dose, puis 250 mg PO/IV q24h" },
                { min: 0, max: 20, label: "500 mg PO/IV x1 dose, puis 250 mg PO/IV q48h" },
            ]},
            "Lévofloxacine — PAH": { notes: "Pneumonie hospitalière", doses: [
                { min: 51, max: 999, label: "750 mg PO/IV q24h" },
                { min: 21, max: 50, label: "750 mg PO/IV q48h" },
                { min: 0, max: 20, label: "750 mg PO/IV x1 dose, puis 500 mg PO/IV q48h" },
            ]},
            "Moxifloxacine (PO/IV)": { notes: "Aucun ajustement requis", doses: [{ min: 0, max: 999, label: "400 mg PO/IV q24h" }]},
            "Norfloxacine (PO) — IU": { notes: "Infection urinaire", doses: [
                { min: 31, max: 999, label: "400 mg PO q12-24h" },
                { min: 0, max: 30, label: "400 mg PO q24h" },
            ]},
        },
        "Autres antibactériens": {
            "Clindamycine (PO)": { notes: "Aucun ajustement requis", doses: [{ min: 0, max: 999, label: "150-450 mg PO q6h" }]},
            "Clindamycine (IV)": { notes: "Aucun ajustement requis", doses: [{ min: 0, max: 999, label: "600-900 mg IV q8h (fréq. max.: q6h)" }]},
            "Daptomycine (IV) — usuelle": { notes: "Pour dose 6 mg/kg: utiliser PD si PR > 140 kg", doses: [
                { min: 31, max: 999, label: "4-6 mg/kg PR IV q24h" },
                { min: 0, max: 30, label: "Même dose IV q48h" },
            ]},
            "Doxycycline (PO)": { notes: "Aucun ajustement requis", doses: [{ min: 0, max: 999, label: "100 mg PO q12h" }]},
            "Linézolide (PO/IV)": { notes: "Aucun ajustement requis", doses: [{ min: 0, max: 999, label: "600 mg PO/IV q12h" }]},
            "Métronidazole (PO)": { notes: "", doses: [
                { min: 11, max: 999, label: "250-500 mg PO q8h ou 500 mg PO q12h" },
                { min: 0, max: 10, label: "500 mg PO q12h" },
            ]},
            "Métronidazole (IV)": { notes: "", doses: [
                { min: 11, max: 999, label: "500 mg IV q8h (fréq. max.: q6h)" },
                { min: 0, max: 10, label: "500 mg IV q12h" },
            ]},
            "Nitrofurantoïne (PO)": { notes: "Cystite non compliquée uniquement", doses: [
                { min: 46, max: 999, label: "50-100 mg PO QID ou 100 mg PO q12h (monohydrate)" },
                { min: 31, max: 45, label: "Données limitées. Possiblement efficace à court terme." },
                { min: 0, max: 30, label: "⚠️ Contre-indiqué — risque de toxicité accru" },
            ]},
            "Tétracycline (PO)": { notes: "", doses: [
                { min: 51, max: 999, label: "250-500 mg PO q6h" },
                { min: 11, max: 50, label: "250-500 mg PO q12h" },
                { min: 0, max: 10, label: "250-500 mg PO q24h" },
            ]},
            "Tigécycline (IV)": { notes: "Aucun ajustement requis", doses: [{ min: 0, max: 999, label: "100 mg IV x1 dose puis 50 mg IV q12h" }]},
            "Triméthoprime (PO)": { notes: "", doses: [
                { min: 16, max: 999, label: "100 mg PO q12h" },
                { min: 6, max: 15, label: "50 mg PO q12h" },
                { min: 0, max: 5, label: "⚠️ Non recommandé" },
            ]},
            "TMP-SMX — usuel": { notes: "80 mg TMP = 1 co SS; 160 mg TMP = 1 co DS", doses: [
                { min: 31, max: 999, label: "160-320 mg TMP PO/IV q12h" },
                { min: 16, max: 30, label: "80 mg TMP PO/IV q12h ou 160 mg TMP PO/IV q24h" },
                { min: 0, max: 15, label: "⚠️ Non recommandé" },
            ]},
            "Vancomycine (IV)": { notes: "⚠️ Amorcer empiriquement selon âge, fonction rénale et poids puis ajuster selon dosages sériques. DC = 25 mg/kg PR. DM: 15-20 mg/kg PD/dose q8-12h (max 2g/dose). Consulter pharmacien.", doses: [
                { min: 71, max: 999, label: "DM IV q12h — surveiller dosages sériques" },
                { min: 41, max: 70, label: "DM IV q24h — surveiller dosages sériques" },
                { min: 21, max: 40, label: "DM IV q36-48h — dosages sériques quotidiens" },
                { min: 11, max: 20, label: "DM IV q72-96h" },
                { min: 0, max: 10, label: "DM IV q5-7 jours — consulter pharmacien" },
            ]},
            "Vancomycine (PO)": { notes: "Aucun ajustement requis", doses: [{ min: 0, max: 999, label: "125 mg PO QID (max 500 mg PO QID si infection compliquée)" }]},
        },
        "Antifongiques": {
            "Anidulafungine (IV)": { notes: "Aucun ajustement requis", doses: [{ min: 0, max: 999, label: "200 mg IV x1 dose puis 100 mg IV q24h" }]},
            "Caspofungine (IV)": { notes: "Aucun ajustement requis", doses: [{ min: 0, max: 999, label: "70 mg IV x1 dose puis 50 mg IV q24h" }]},
            "Fluconazole (PO/IV)": { notes: "DC = 100% de la dose. Écart: 100-800 mg PO/IV q24h", doses: [
                { min: 51, max: 999, label: "DC puis 100% dose PO/IV q24h" },
                { min: 11, max: 50, label: "DC puis 50% de la dose PO/IV q24h" },
                { min: 0, max: 10, label: "DC puis 25-50% de la dose PO/IV q24h" },
            ]},
            "Isavuconazole (PO/IV)": { notes: "Aucun ajustement requis. 372 mg sulfate = 200 mg isavuconazole.", doses: [{ min: 0, max: 999, label: "DC: 200 mg PO/IV q8h x6 doses, puis 200 mg PO/IV q24h" }]},
            "Kétoconazole (PO)": { notes: "Aucun ajustement requis", doses: [{ min: 0, max: 999, label: "200-400 mg PO q24h" }]},
            "Micafungine — prophylaxie": { notes: "Aucun ajustement requis", doses: [{ min: 0, max: 999, label: "50-100 mg IV q24h" }]},
            "Micafungine — traitement": { notes: "Aucun ajustement requis", doses: [{ min: 0, max: 999, label: "100-150 mg IV q24h" }]},
            "Terbinafine (PO)": { notes: "", doses: [
                { min: 51, max: 999, label: "250 mg PO q24h" },
                { min: 0, max: 50, label: "⚠️ Non recommandé — données limitées" },
            ]},
        },
        "Antimycobactériens": {
            "Ethambutol (PO)": { notes: "Si PR > 120% PI, baser dose sur PI.", doses: [
                { min: 31, max: 999, label: "15-25 mg/kg PO q24h" },
                { min: 0, max: 30, label: "15-25 mg/kg/dose PO DIE 3 jours/semaine" },
            ]},
            "Isoniazide (PO)": { notes: "Aucun ajustement requis", doses: [{ min: 0, max: 999, label: "5 mg/kg PO q24h (max: 300 mg)" }]},
            "Pyrazinamide (PO)": { notes: "Si PR > 120% PI, baser dose sur PI.", doses: [
                { min: 31, max: 999, label: "20-25 mg/kg PO q24h (max: 2g)" },
                { min: 0, max: 30, label: "25-35 mg/kg/dose PO DIE 3 jours/semaine" },
            ]},
            "Rifabutine (PO)": { notes: "", doses: [
                { min: 16, max: 999, label: "300 mg PO q24h" },
                { min: 0, max: 15, label: "150 mg PO q24h" },
            ]},
            "Rifampicine (PO)": { notes: "Aucun ajustement requis", doses: [{ min: 0, max: 999, label: "10 mg/kg PO q24h (max: 600 mg)" }]},
        },
        "Antiviraux": {
            "Acyclovir (IV)": { notes: "Posologie usuelle: 5-15 mg/kg/dose IV q8h", doses: [
                { min: 26, max: 999, label: "100% de la dose IV q8h" },
                { min: 11, max: 25, label: "100% de la dose IV q12h" },
                { min: 6, max: 10, label: "100% de la dose IV q24h" },
                { min: 0, max: 5, label: "50% de la dose IV q24h" },
            ]},
            "Acyclovir (PO) — zona": { notes: "", doses: [
                { min: 26, max: 999, label: "800 mg PO q4h (5 fois/jour)" },
                { min: 11, max: 25, label: "800 mg PO q8h" },
                { min: 0, max: 10, label: "800 mg PO q12h" },
            ]},
            "Famciclovir (PO) — zona": { notes: "", doses: [
                { min: 41, max: 999, label: "500 mg PO q8h" },
                { min: 21, max: 40, label: "500 mg PO q12h" },
                { min: 6, max: 20, label: "500 mg PO q24h" },
                { min: 0, max: 5, label: "250 mg PO q24h" },
            ]},
            "Oseltamivir (PO) — traitement": { notes: "Influenza A et B", doses: [
                { min: 31, max: 999, label: "75 mg PO q12h" },
                { min: 11, max: 30, label: "30 mg PO q12h" },
                { min: 6, max: 10, label: "30 mg PO q24h" },
                { min: 0, max: 5, label: "75 mg PO x1 dose" },
            ]},
            "Oseltamivir (PO) — prophylaxie": { notes: "Prophylaxie Influenza A et B", doses: [
                { min: 31, max: 999, label: "75 mg PO q24h" },
                { min: 11, max: 30, label: "30 mg PO q24h" },
                { min: 6, max: 10, label: "30 mg PO q48h" },
                { min: 0, max: 5, label: "Privilégier zanamivir inhalation" },
            ]},
            "Valacyclovir (PO) — zona": { notes: "", doses: [
                { min: 31, max: 999, label: "1g PO q8h" },
                { min: 16, max: 30, label: "1g PO q12h" },
                { min: 6, max: 15, label: "1g PO q24h" },
                { min: 0, max: 5, label: "500 mg PO q24h" },
            ]},
            "Valacyclovir (PO) — herpès génital": { notes: "", doses: [
                { min: 31, max: 999, label: "1g PO q12h" },
                { min: 16, max: 30, label: "1g PO q24h" },
                { min: 0, max: 15, label: "500 mg PO q24h" },
            ]},
        },
    };

    function calcClcr(age, poids, creat, sexe) {
        var k = sexe === 'F' ? 0.85 : 1.0;
        return ((140 - age) * poids / (49 * creat)) * 60 * k;
    }
    function calcPI(taille, sexe) {
        return sexe === 'F' ? 45.5 + 0.91 * (taille - 152) : 50 + 0.91 * (taille - 152);
    }
    function calcIMC(poids, taille) {
        return poids / Math.pow(taille / 100, 2);
    }

    var state = {
        age: '', poids: '', taille: '', creat: '', sexe: 'H',
        selectedCat: '', selectedDrug: '', search: '',
        mode: 'cg', dfgDirect: ''
    };

    function getDrugData(cat, drug) {
        return ANTIBIOS[cat] && ANTIBIOS[cat][drug] ? ANTIBIOS[cat][drug] : null;
    }

    function getActiveDose(doses, clcr) {
        var clcrRounded = Math.round(clcr * 10) / 10;
        for (var i = 0; i < doses.length; i++) {
            if (clcrRounded >= doses[i].min && clcrRounded <= doses[i].max) return i;
        }
        for (var i = doses.length - 1; i >= 0; i--) {
            if (clcrRounded >= doses[i].min) return i;
        }
        return doses.length - 1;
    }

    function calcClcrFromState() {
        if (state.mode === 'direct') {
            var dfg = parseFloat(state.dfgDirect);
            return { clcr: isNaN(dfg) ? null : dfg, imc: null, pi: null, poidsCalc: null };
        }
        var age = parseFloat(state.age);
        var poids = parseFloat(state.poids);
        var taille = parseFloat(state.taille);
        var creat = parseFloat(state.creat);
        var imc = (!isNaN(poids) && !isNaN(taille)) ? calcIMC(poids, taille) : null;
        var pi = (!isNaN(taille)) ? calcPI(taille, state.sexe) : null;
        var poidsCalc = null;
        if (!isNaN(poids) && !isNaN(taille)) {
            poidsCalc = imc < 30 ? poids : pi + 0.4 * (poids - pi);
        }
        var clcr = (!isNaN(age) && poidsCalc !== null && !isNaN(creat)) ? calcClcr(age, poidsCalc, creat, state.sexe) : null;
        return { clcr: clcr, imc: imc, pi: pi, poidsCalc: poidsCalc };
    }

    function renderResult() {
        var res = calcClcrFromState();
        var clcr = res.clcr; var imc = res.imc; var pi = res.pi; var poidsCalc = res.poidsCalc;
        var clcrColor = '#64748b'; var clcrLabel = '';
        if (clcr !== null) {
            if (clcr >= 60) { clcrColor = '#16a34a'; clcrLabel = 'Normale / légèrement réduite'; }
            else if (clcr >= 30) { clcrColor = '#d97706'; clcrLabel = 'IRC modérée'; }
            else if (clcr >= 15) { clcrColor = '#ea580c'; clcrLabel = 'IRC sévère'; }
            else { clcrColor = '#dc2626'; clcrLabel = 'IRC très sévère / terminale'; }
        }
        var box = document.getElementById('calc-result-box');
        if (box) {
            if (clcr !== null) {
                var html = '<div>';
                html += '<div style="font-size:11px;color:#64748b;font-weight:600">' + (state.mode === 'direct' ? 'DFG (LABORATOIRE)' : 'CLAIRANCE CRÉATININE') + '</div>';
                html += '<div class="calc-clcr-val" style="color:' + clcrColor + '">' + clcr.toFixed(1) + ' <span style="font-size:14px;font-weight:500">mL/min</span></div>';
                html += '<div class="calc-clcr-label" style="color:' + clcrColor + '">' + clcrLabel + '</div>';
                html += '</div>';
                if (imc !== null) {
                    html += '<div class="calc-imc-info">';
                    html += '<div style="font-size:11px;color:#64748b;font-weight:600">IMC</div>';
                    html += '<div style="font-size:20px;font-weight:700;color:#0f2d5e">' + imc.toFixed(1) + ' <span style="font-size:11px">kg/m\u00b2</span></div>';
                    if (imc >= 30 && pi) {
                        html += '<div>IMC \u2265 30 \u2014 Poids de dosage : <b>' + poidsCalc.toFixed(1) + ' kg</b><br>Poids id\u00e9al : ' + pi.toFixed(1) + ' kg</div>';
                    }
                    html += '</div>';
                }
                box.innerHTML = html;
                box.style.display = 'flex';
                box.style.border = '2px solid ' + clcrColor + '33';
            } else {
                box.innerHTML = '';
                box.style.display = 'none';
            }
        }
        var btnH = document.getElementById('calc-btn-h');
        var btnF = document.getElementById('calc-btn-f');
        if (btnH) btnH.className = 'calc-sexe-btn' + (state.sexe === 'H' ? ' active' : '');
        if (btnF) btnF.className = 'calc-sexe-btn' + (state.sexe === 'F' ? ' active' : '');
        renderDrugResult(clcr);
    }

    function renderDrugResult(clcr) {
        var drugData = getDrugData(state.selectedCat, state.selectedDrug);
        var section = document.getElementById('calc-drug-result');
        if (!section) return;
        if (!state.selectedDrug || !drugData) { section.innerHTML = ''; return; }
        var html = '<div class="calc-card">';
        html += '<h2>3. Posologie recommand\u00e9e</h2>';
        html += '<div style="font-size:13px;color:#64748b;margin-bottom:14px">' + state.selectedDrug + '</div>';
        if (drugData.notes) html += '<div class="calc-note">\u26a0\ufe0f ' + drugData.notes + '</div>';
        html += '<table class="calc-table"><thead><tr><th>ClCr (mL/min)</th><th>Posologie</th></tr></thead><tbody>';
        var activeIdx = clcr !== null ? getActiveDose(drugData.doses, clcr) : -1;
        drugData.doses.forEach(function(d, i) {
            var isActive = clcr !== null && i === activeIdx;
            var rangeLabel = d.max >= 999 ? '\u2265 ' + d.min : d.min === 0 ? '< ' + (drugData.doses.filter(function(x){return x.min>0;}).map(function(x){return x.min;}).sort(function(a,b){return a-b;})[0]||5) : d.min + ' \u2013 ' + d.max;
            html += '<tr' + (isActive ? ' class="active-row"' : ' style="background:' + (i%2===0?'white':'#f8fafc') + '"') + '>';
            html += '<td style="font-weight:600;white-space:nowrap">' + (isActive ? '\u25b6 ' : '') + rangeLabel + '</td>';
            html += '<td>' + d.label + (isActive ? '<span class="calc-badge">VOTRE PATIENT</span>' : '') + '</td>';
            html += '</tr>';
        });
        html += '</tbody></table>';
        if (clcr === null) html += '<div class="calc-hint">\ud83d\udca1 Entrez les donn\u00e9es du patient (\u00e9tape 1) pour mettre en \u00e9vidence la posologie appropri\u00e9e.</div>';
        html += '</div>';
        section.innerHTML = html;
    }

    function renderDrugSelector() {
        var searchResults = [];
        if (state.search && state.search.length >= 2) {
            var q = state.search.toLowerCase();
            Object.keys(ANTIBIOS).forEach(function(cat) {
                Object.keys(ANTIBIOS[cat]).forEach(function(drug) {
                    if (drug.toLowerCase().indexOf(q) !== -1) searchResults.push({ cat: cat, drug: drug });
                });
            });
        }
        var sr = document.getElementById('calc-search-results');
        if (sr) {
            var html = '';
            searchResults.forEach(function(r) {
                html += '<button onclick="calcSelectDrug(\'' + r.cat.replace(/'/g, "\\'") + '\',\'' + r.drug.replace(/'/g, "\\'") + '\')">';
                html += '<span style="color:#64748b;font-size:11px">' + r.cat + ' / </span>' + r.drug + '</button>';
            });
            sr.innerHTML = html;
        }
        var cats = document.getElementById('calc-cats');
        if (cats) {
            var html = '';
            Object.keys(ANTIBIOS).forEach(function(cat) {
                html += '<button class="calc-cat-btn ' + (state.selectedCat === cat ? 'active' : '') + '" onclick="calcSetCat(\'' + cat.replace(/'/g, "\\'") + '\')">' + cat + '</button>';
            });
            cats.innerHTML = html;
        }
        var drugs = document.getElementById('calc-drugs');
        if (drugs) {
            var html = '';
            if (state.selectedCat && ANTIBIOS[state.selectedCat]) {
                Object.keys(ANTIBIOS[state.selectedCat]).forEach(function(drug) {
                    html += '<button class="calc-drug-btn ' + (state.selectedDrug === drug ? 'active' : '') + '" onclick="calcSetDrug(\'' + drug.replace(/'/g, "\\'") + '\')">' + drug + '</button>';
                });
            }
            drugs.innerHTML = html;
        }
    }

    function renderVanco() {
        var app = document.getElementById('vanco-app');
        if (!app) return;
        var html = '';

        // Header violet
        html += '<div class="vanco-header">';
        html += '<h1>Vancomycine IV \u2014 Tableaux posologiques</h1>';
        html += '<p>Bas\u00e9 sur les lignes directrices APES \u2022 Adultes seulement</p>';
        html += '</div>';

        // Explication
        html += '<div class="vanco-card">';
        html += '<h2>\ud83d\udcd6 Comment utiliser ces tableaux</h2>';
        html += '<ol style="font-size:13px;color:#374151;line-height:1.8;margin:0;padding-left:20px;">';
        html += '<li><b>Dose de charge (optionnelle) :</b> Trouver le poids du patient dans le tableau 1 \u2192 administrer la dose correspondante en IV \u00d7 1 seule fois. Recommand\u00e9e chez les patients gravement malades.</li>';
        html += '<li><b>Dose d\u2019entretien :</b> Trouver le poids dans le tableau 2 \u2192 c\u2019est la dose par administration.</li>';
        html += '<li><b>Fr\u00e9quence :</b> Utiliser le DFGe fourni par le laboratoire (mL/min/1,73m\u00b2) dans le tableau 2 \u2192 d\u00e9termine l\u2019intervalle entre les doses (q8h, q12h, q24h).</li>';
        html += '<li><b>Pr\u00e9l\u00e8vements :</b> Consulter le tableau 3 selon la fr\u00e9quence choisie pour planifier le moment des pr\u00e9l\u00e8vements PIC et creux.</li>';
        html += '<li><b>Ajustement :</b> Toujours ajuster selon les dosages s\u00e9riques obtenus. Consulter un pharmacien pour les cas complexes (IMC \u2265 30, insuffisance r\u00e9nale s\u00e9v\u00e8re, soins intensifs).</li>';
        html += '</ol>';
        html += '<div style="margin-top:12px;padding:10px 14px;background:#f0fdf4;border-radius:8px;font-size:12px;color:#166534;">';
        html += '\ud83d\udca1 <b>Astuce :</b> Pour un calcul plus pr\u00e9cis de la dose d\u2019entretien (particulièrement si IMC \u2265 30), utiliser <b>vancopk.com</b>.';
        html += '</div>';
        html += '</div>';

        // Tableau 1 — Dose de charge
        html += '<div class="vanco-card">';
        html += '<h2>Tableau 1 \u2014 Dose de charge \u00d7 1 dose</h2>';
        html += '<div style="font-size:12px;color:#64748b;margin-bottom:12px;">Calcul\u00e9e \u00e0 25 mg/kg de poids r\u00e9el. Non applicable aux usagers sous th\u00e9rapie de remplacement r\u00e9nal.</div>';
        html += '<div style="overflow-x:auto;">';
        html += '<table class="vanco-table"><thead><tr><th>Poids (kg)</th><th>Dose (mg)</th><th>Poids (kg)</th><th>Dose (mg)</th><th>Poids (kg)</th><th>Dose (mg)</th></tr></thead><tbody>';
        html += '<tr><td>35 \u2013 39,9</td><td><b>750</b></td><td>60 \u2013 69,9</td><td><b>1500</b></td><td>90 \u2013 99,9</td><td><b>2250</b></td></tr>';
        html += '<tr><td>40 \u2013 49,9</td><td><b>1000</b></td><td>70 \u2013 79,9</td><td><b>1750</b></td><td>100 \u2013 129,9</td><td><b>2500</b></td></tr>';
        html += '<tr><td>50 \u2013 59,9</td><td><b>1250</b></td><td>80 \u2013 89,9</td><td><b>2000</b></td><td>130 et plus</td><td><b>3000</b></td></tr>';
        html += '</tbody></table>';
        html += '</div></div>';

        // Tableau 2 — Entretien
        html += '<div class="vanco-card">';
        html += '<h2>Tableau 2 \u2014 Traitement d\u2019entretien</h2>';
        html += '<div style="font-size:12px;color:#64748b;margin-bottom:12px;">Croiser la dose selon le poids <b>ET</b> la fr\u00e9quence selon le DFGe du laboratoire.</div>';
        html += '<div style="display:flex;gap:16px;flex-wrap:wrap;">';

        html += '<div style="flex:1;min-width:180px;">';
        html += '<div class="vanco-section-title">Dose selon le poids</div>';
        html += '<table class="vanco-table"><thead><tr><th>Poids (kg)</th><th>Dose / administration</th></tr></thead><tbody>';
        html += '<tr><td>35 \u2013 39,9</td><td><b>500 mg</b></td></tr>';
        html += '<tr><td>40 \u2013 59,9</td><td><b>750 mg</b></td></tr>';
        html += '<tr><td>60 \u2013 79,9</td><td><b>1000 mg</b></td></tr>';
        html += '<tr><td>80 \u2013 99,9</td><td><b>1250 mg</b></td></tr>';
        html += '<tr><td>100 et plus</td><td><b>1500 mg</b></td></tr>';
        html += '</tbody></table></div>';

        html += '<div style="flex:1;min-width:220px;">';
        html += '<div class="vanco-section-title">Fr\u00e9quence selon le DFGe (mL/min/1,73m\u00b2)</div>';
        html += '<table class="vanco-table"><thead><tr><th>DFGe</th><th>Fr\u00e9quence</th></tr></thead><tbody>';
        html += '<tr><td>Plus de 90</td><td><b>q 8h</b></td></tr>';
        html += '<tr><td>50 \u2013 90</td><td><b>q 12h</b></td></tr>';
        html += '<tr><td>15 \u2013 49</td><td><b>q 24h</b></td></tr>';
        html += '<tr><td>Moins de 15</td><td><b>Dose unique puis selon dosage</b></td></tr>';
        html += '</tbody></table></div>';
        html += '</div></div>';

        // Tableau 3 — Prélèvements
        html += '<div class="vanco-card">';
        html += '<h2>Tableau 3 \u2014 Moment des pr\u00e9l\u00e8vements recommand\u00e9s</h2>';
        html += '<div style="font-size:12px;color:#64748b;margin-bottom:12px;">Incluant la dose de charge s\u2019il y a lieu.</div>';
        html += '<div style="overflow-x:auto;">';
        html += '<table class="vanco-table"><thead><tr><th>Intervalle posologique</th><th>PIC</th><th>CREUX</th></tr></thead><tbody>';
        html += '<tr><td style="font-weight:700;">q8h ou q12h</td><td>60 \u00e0 120 min post fin de perfusion de la <b>4\u1d49 dose</b></td><td>Juste avant la <b>4\u1d49 dose</b></td></tr>';
        html += '<tr><td style="font-weight:700;">q24h</td><td>60 \u00e0 120 min post fin de perfusion de la <b>3\u1d49 dose</b></td><td>Juste avant la <b>3\u1d49 dose</b></td></tr>';
        html += '</tbody></table>';
        html += '</div>';
        html += '<div style="font-size:12px;color:#64748b;margin-top:10px;">Des pr\u00e9l\u00e8vements par <b>creux seulement</b> doivent \u00eatre effectu\u00e9s pour les patients dont l\u2019\u00e9tat d\u2019\u00e9quilibre n\u2019est pas atteint.</div>';
        html += '</div>';

        // Avertissement
        html += '<div class="vanco-warning">\u26a0\ufe0f <b>Avertissement clinique :</b> Ces tableaux sont fournis \u00e0 titre de r\u00e9f\u00e9rence. Toujours ajuster selon les dosages s\u00e9riques. Consulter un pharmacien pour tout ajustement posologique de la vancomycine et se r\u00e9f\u00e9rer au guide APES (bulletin N\u00b0 40).</div>';

        app.innerHTML = html;
    }

    function render() {
        var app = document.getElementById('calc-app');
        if (!app) return;
        var html = '';
        html += '<div class="calc-header">';
        html += '<h1>Calculateur posologique \u2014 Insuffisance r\u00e9nale</h1>';
        html += '<p>Bas\u00e9 sur le guide APES (RPEI) \u2014 Octobre 2019 \u2022 Adultes seulement \u2022 Le jugement clinique doit pr\u00e9valoir</p>';
        html += '</div>';

        html += '<div class="calc-card">';
        html += '<h2>1. Fonction r\u00e9nale</h2>';
        html += '<div style="display:flex;gap:8px;margin-bottom:16px;">';
        html += '<button id="calc-btn-cg" class="calc-sexe-btn ' + (state.mode === 'cg' ? 'active' : '') + '" onclick="calcSetMode(\'cg\')">Calculer (Cockcroft-Gault)</button>';
        html += '<button id="calc-btn-direct" class="calc-sexe-btn ' + (state.mode === 'direct' ? 'active' : '') + '" onclick="calcSetMode(\'direct\')">DFG d\u00e9j\u00e0 connu</button>';
        html += '</div>';
        if (state.mode === 'cg') {
            html += '<div style="margin-bottom:12px;display:flex;align-items:center;gap:8px;">';
            html += '<span style="font-size:13px;color:#64748b;width:50px">Sexe</span>';
            html += '<button id="calc-btn-h" class="calc-sexe-btn ' + (state.sexe === 'H' ? 'active' : '') + '" onclick="calcSetSexe(\'H\')">Homme</button>';
            html += '<button id="calc-btn-f" class="calc-sexe-btn ' + (state.sexe === 'F' ? 'active' : '') + '" onclick="calcSetSexe(\'F\')">Femme</button>';
            html += '</div>';
            html += '<div class="calc-grid">';
            html += '<div><label>\u00c2ge (ans)</label><input id="calc-age" type="number" placeholder="ex: 65" oninput="calcSetField(\'age\',this.value)"></div>';
            html += '<div><label>Poids r\u00e9el (kg)</label><input id="calc-poids" type="number" placeholder="ex: 70" oninput="calcSetField(\'poids\',this.value)"></div>';
            html += '<div><label>Taille (cm)</label><input id="calc-taille" type="number" placeholder="ex: 170" oninput="calcSetField(\'taille\',this.value)"></div>';
            html += '<div><label>Cr\u00e9atinine (\u03bcmol/L)</label><input id="calc-creat" type="number" placeholder="ex: 120" oninput="calcSetField(\'creat\',this.value)"></div>';
            html += '</div>';
        } else {
            html += '<div style="max-width:250px;">';
            html += '<label style="font-size:11px;color:#64748b;font-weight:600;display:block;margin-bottom:4px;">DFG / ClCr (mL/min)</label>';
            html += '<input id="calc-dfg-direct" type="number" placeholder="ex: 45" style="width:100%;padding:10px 14px;border-radius:8px;border:1.5px solid #e2e8f0;font-size:18px;font-weight:700;color:#0f2d5e;outline:none;" oninput="calcSetField(\'dfgDirect\',this.value)">';
            html += '<div style="font-size:11px;color:#64748b;margin-top:6px;">Valeur fournie par le laboratoire</div>';
            html += '</div>';
        }
        html += '<div id="calc-result-box" class="calc-result-box" style="display:none"></div>';
        html += '</div>';

        html += '<div class="calc-card">';
        html += '<h2>2. S\u00e9lection de l\'antimicrobien</h2>';
        html += '<input id="calc-search-input" class="calc-search" type="text" placeholder="\ud83d\udd0d Rechercher un antimicrobien..." oninput="calcSetSearch(this.value)">';
        html += '<div id="calc-search-results" class="calc-search-results"></div>';
        html += '<div id="calc-cats" class="calc-cats"></div>';
        html += '<div id="calc-drugs" class="calc-drugs"></div>';
        html += '</div>';

        html += '<div id="calc-drug-result"></div>';
        html += '<div class="calc-warning">\u26a0\ufe0f <b>Avertissement clinique :</b> Cet outil est bas\u00e9 sur le guide APES (RPEI, octobre 2019). Les ajustements servent uniquement \u00e0 guider la d\u00e9cision clinique \u2014 le jugement clinique doit pr\u00e9valoir. Consulter un pharmacien pour les cas complexes.</div>';
        html += '<div id="vanco-app" style="margin-top:24px;"></div>';

        app.innerHTML = html;
        renderResult();
        renderDrugSelector();
        renderVanco();
    }

    window.calcSetMode = function(mode) { state.mode = mode; render(); };
    window.calcSetField = function(field, val) { state[field] = val; renderResult(); };
    window.calcSetSexe = function(sexe) { state.sexe = sexe; renderResult(); };
    window.calcSetSearch = function(val) { state.search = val; state.selectedCat = ''; state.selectedDrug = ''; renderDrugSelector(); renderDrugResult(calcClcrFromState().clcr); };
    window.calcSetCat = function(cat) { state.selectedCat = cat; state.selectedDrug = ''; renderDrugSelector(); renderDrugResult(calcClcrFromState().clcr); };
    window.calcSetDrug = function(drug) { state.selectedDrug = drug; renderDrugSelector(); renderDrugResult(calcClcrFromState().clcr); };
    window.calcSelectDrug = function(cat, drug) { state.selectedCat = cat; state.selectedDrug = drug; state.search = ''; var si = document.getElementById('calc-search-input'); if(si) si.value=''; renderDrugSelector(); renderDrugResult(calcClcrFromState().clcr); };

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', render);
    } else {
        render();
    }
}

/* ====== DEBUT Loupe images ======
   Un clic sur une image l'ouvre en grand (lisible sur telephone).
   Independant de l'assistant : supprimer ce bloc ne touche a rien d'autre.
   ============ */

/* =====================================================================
   Agrandissement des images — microguide.ca
   =====================================================================
   Un clic (ou une tape) sur n'importe quelle image d'une page l'ouvre en
   grand, par-dessus la page. Sur téléphone, on peut ensuite zoomer et
   déplacer l'image avec deux doigts, puis fermer d'une tape.

   Indépendant de l'assistant : ce bloc peut être retiré sans toucher au
   reste. Aucune donnée ne sort du wiki.
   ===================================================================== */
(function () {
  "use strict";
  if (window.__microguideLoupe) return;
  window.__microguideLoupe = true;

  var CSS = [
    '.mgl-fond{position:fixed;inset:0;z-index:9500;background:rgba(10,14,18,.92);',
    '  display:flex;align-items:center;justify-content:center;',
    '  padding:env(safe-area-inset-top,0px) 0 env(safe-area-inset-bottom,0px);',
    '  opacity:0;transition:opacity .15s ease;touch-action:none}',
    '.mgl-fond.mgl-ouvert{opacity:1}',
    '.mgl-cadre{max-width:100%;max-height:100%;overflow:auto;',
    '  -webkit-overflow-scrolling:touch;touch-action:pinch-zoom;display:flex;',
    '  align-items:center;justify-content:center}',
    '.mgl-cadre img{display:block;max-width:100%;max-height:100vh;',
    '  width:auto;height:auto;object-fit:contain;background:#fff;border-radius:3px}',
    '.mgl-fermer{position:fixed;z-index:9501;',
    '  top:calc(10px + env(safe-area-inset-top,0px));',
    '  right:calc(10px + env(safe-area-inset-right,0px));',
    '  width:44px;height:44px;border-radius:50%;border:none;cursor:pointer;',
    '  background:rgba(255,255,255,.92);color:#1c2228;font-size:22px;line-height:44px;',
    '  text-align:center;padding:0;font-family:inherit}',
    '.mgl-legende{position:fixed;left:0;right:0;z-index:9501;',
    '  bottom:calc(8px + env(safe-area-inset-bottom,0px));',
    '  text-align:center;color:#e8e4dc;font:400 12px/1.5 system-ui,sans-serif;',
    '  padding:0 16px;pointer-events:none;text-shadow:0 1px 3px rgba(0,0,0,.8)}',
    // sur la page elle-même : montrer que l'image est cliquable
    '.mw-parser-output img{cursor:zoom-in}',
    '@media (prefers-reduced-motion:reduce){.mgl-fond{transition:none}}'
  ].join("");

  var style = document.createElement("style");
  style.textContent = CSS;
  document.head.appendChild(style);

  // Les images affichées sont des vignettes. L'originale, bien plus lisible
  // sur téléphone, se déduit de l'adresse : /images/thumb/a/ab/X.jpg/550px-X.jpg
  // devient /images/a/ab/X.jpg
  function pleineTaille(src) {
    try {
      var u = new URL(src, location.href);
      if (u.origin !== location.origin) return src;
      var m = u.pathname.match(/^(.*)\/thumb(\/[0-9a-f]\/[0-9a-f]{2}\/[^\/]+)\/[^\/]+$/);
      if (m) { u.pathname = m[1] + m[2]; return u.href; }
      return src;
    } catch (e) { return src; }
  }

  var fond = null;

  function fermer() {
    if (!fond) return;
    fond.classList.remove("mgl-ouvert");
    var f = fond; fond = null;
    setTimeout(function () { if (f && f.parentNode) f.parentNode.removeChild(f); }, 160);
    document.documentElement.style.overflow = "";
  }

  function ouvrir(img) {
    if (fond) return;
    fond = document.createElement("div");
    fond.className = "mgl-fond";

    var cadre = document.createElement("div");
    cadre.className = "mgl-cadre";
    var grande = document.createElement("img");
    grande.src = pleineTaille(img.currentSrc || img.src);
    grande.alt = img.alt || "";
    cadre.appendChild(grande);
    fond.appendChild(cadre);

    var bouton = document.createElement("button");
    bouton.className = "mgl-fermer";
    bouton.setAttribute("aria-label", "Fermer");
    bouton.textContent = "✕";
    fond.appendChild(bouton);

    var legende = document.createElement("div");
    legende.className = "mgl-legende";
    legende.textContent = "Pincez pour zoomer · touchez le fond pour fermer";
    fond.appendChild(legende);

    document.body.appendChild(fond);
    document.documentElement.style.overflow = "hidden";
    requestAnimationFrame(function () { fond.classList.add("mgl-ouvert"); });

    bouton.addEventListener("click", fermer);
    fond.addEventListener("click", function (e) {
      // fermer seulement si on touche le fond, pas l'image elle-même
      if (e.target === fond || e.target === cadre || e.target === legende) fermer();
    });
  }

  document.addEventListener("click", function (e) {
    if (fond) return;
    var img = e.target;
    if (!img || img.tagName !== "IMG") return;
    // ne pas intercepter l'assistant, ni les icônes de l'interface du wiki
    if (img.closest(".mga-panel, .mga-launch, .mw-header, .citizen-header, .mw-footer")) return;
    if (!img.closest(".mw-parser-output, #mw-content-text")) return;
    if (img.naturalWidth && img.naturalWidth < 80) return;   // puce ou icône
    e.preventDefault();
    e.stopPropagation();
    ouvrir(img);
  }, true);

  document.addEventListener("keydown", function (e) {
    if (e.key === "Escape") fermer();
  });
})();

/* ====== FIN Loupe images ====== */

/* ====== DEBUT Assistant Microguide ====== */
/* =====================================================================
   Assistant Microguide — recherche-réponse pour microguide.ca
   =====================================================================
   Tout se passe dans le navigateur du visiteur : il lit le wiki par son
   API publique, reconstruit son index dès que le wiki change, et répond
   en recopiant mot pour mot le texte, les liens et les tableaux du wiki.
   Aucun serveur, aucune clé d'API, aucun modèle de langue : il ne rédige
   jamais, donc il ne peut rien inventer.

   TROIS COMPORTEMENTS
     1. Sujet traité par une page -> réponse citée, avec le lien.
     2. Sujet seulement MENTIONNÉ en passant -> la phrase exacte est
        montrée, précédée d'un avertissement : ce n'est pas forcément le
        traitement complet ni celui de choix.
     3. Sujet absent -> refus explicite.

   INSTALLATION : coller ce fichier au complet dans MediaWiki:Common.js.
   POUR L'ENLEVER : supprimer ce bloc. Rien d'autre n'est modifié.
   ===================================================================== */


/* ---------- 1/4 vocabulaire ---------- */
// Vocabulaire de l'assistant microguide.
//
// GLOSSAIRE : français québécois, abréviations cliniques (atb, tx = traitement,
// rx = médicaments, dx = diagnostic, pip-tazo, vanco…), et formes latines ou
// françaises des genres (le wiki mélange « Entérocoques » et « Enterococcus »)
// -> mots réellement écrits dans le wiki. Appliqué sans IA, donc sans erreur.
//
// INTENTIONS : quelle SORTE de renseignement la question demande, pour
// privilégier la bonne section (« quoi donner » -> Traitements, et le
// tableau d'antibiogramme qui va avec).

window.MICROGUIDE_GLOSSAIRE = {
  "atb": [
    "antibiotique",
    "antibiotiques"
  ],
  "abx": [
    "antibiotique",
    "antibiotiques"
  ],
  "anti biotique": [
    "antibiotique"
  ],
  "traiter": [
    "traitement",
    "traitements"
  ],
  "traite": [
    "traitement"
  ],
  "soigner": [
    "traitement"
  ],
  "donner": [
    "traitement"
  ],
  "prescrire": [
    "traitement"
  ],
  "couvrir": [
    "couverture",
    "traitement"
  ],
  "couvre": [
    "couverture"
  ],
  "tuer": [
    "traitement",
    "sensibilite"
  ],
  "marche": [
    "efficace",
    "traitement"
  ],
  "fonctionne": [
    "efficace",
    "traitement"
  ],
  "pogner": [
    "infection",
    "acquisition"
  ],
  "attraper": [
    "infection",
    "acquisition"
  ],
  "poumon": [
    "pulmonaire",
    "pneumonie"
  ],
  "poumons": [
    "pulmonaire",
    "pneumonie"
  ],
  "pipi": [
    "urinaire"
  ],
  "urine": [
    "urinaire"
  ],
  "vessie": [
    "cystite",
    "urinaire"
  ],
  "rein": [
    "pyelonephrite",
    "urinaire"
  ],
  "reins": [
    "pyelonephrite",
    "urinaire"
  ],
  "sang": [
    "bacteriemie",
    "sepsis",
    "hemoculture"
  ],
  "coeur": [
    "endocardite"
  ],
  "peau": [
    "cutanee",
    "tissus mous",
    "cellulite"
  ],
  "os": [
    "osteomyelite",
    "osteoarticulaire"
  ],
  "cerveau": [
    "meningite"
  ],
  "ventre": [
    "intra abdominale",
    "digestif"
  ],
  "diarrhee": [
    "digestif",
    "colite"
  ],
  "oreille": [
    "otite"
  ],
  "gorge": [
    "pharyngite",
    "orl"
  ],
  "yeux": [
    "keratite",
    "ophtalmologique"
  ],
  "oeil": [
    "keratite",
    "ophtalmologique"
  ],
  "bibitte": [
    "bacterie"
  ],
  "germe": [
    "bacterie"
  ],
  "microbe": [
    "bacterie"
  ],
  "resistant": [
    "resistance",
    "resistances"
  ],
  "resiste": [
    "resistance",
    "resistances"
  ],
  "dose": [
    "posologie",
    "dose"
  ],
  "poso": [
    "posologie"
  ],
  "grossesse": [
    "grossesse",
    "enceinte"
  ],
  "enceinte": [
    "grossesse"
  ],
  "allergie": [
    "allergie",
    "reaction",
    "indesirable"
  ],
  "penicilline": [
    "penicilline",
    "penicillines"
  ],
  "pseudo": [
    "pseudomonas"
  ],
  "staph": [
    "staphylococcus",
    "staphyloccocus"
  ],
  "strep": [
    "streptococcus",
    "streptocoque"
  ],
  "pneumo": [
    "pneumoniae",
    "pneumocoque"
  ],
  // Thomas a supprimé la page SARM le 21 oct. 2026 : tout est maintenant
  // dans la page du Staphylococcus aureus, section « Spécificité du SARM ».
  "sarm": [
    "sarm",
    "staphylococcus",
    "aureus",
    "meticilline",
    "resistant"
  ],
  "sasm": [
    "staphylococcus",
    "aureus",
    "sasm"
  ],
  "e coli": [
    "escherichia",
    "coli"
  ],
  "ecoli": [
    "escherichia",
    "coli"
  ],
  "c diff": [
    "clostridioides",
    "difficile"
  ],
  "cdiff": [
    "clostridioides",
    "difficile"
  ],
  "klebs": [
    "klebsiella"
  ],
  "entero": [
    "enterocoque",
    "enterococcus",
    "enterobacter"
  ],
  "bgn": [
    "gram negatif"
  ],
  "bgp": [
    "gram positif"
  ],
  "cocci": [
    "cocci",
    "coque"
  ],
  "labo": [
    "laboratoire"
  ],
  "gram": [
    "gram"
  ],
  "vanco": [
    "vancomycine"
  ],
  "cipro": [
    "ciprofloxacine"
  ],
  "levo": [
    "levofloxacine"
  ],
  "moxi": [
    "moxifloxacine"
  ],
  "ceftri": [
    "ceftriaxone"
  ],
  "cefta": [
    "ceftazidime"
  ],
  "pip tazo": [
    "piperacilline",
    "tazobactam"
  ],
  "piptazo": [
    "piperacilline",
    "tazobactam"
  ],
  "tazo": [
    "tazobactam",
    "piperacilline"
  ],
  "mero": [
    "meropeneme",
    "meropenem"
  ],
  "erta": [
    "ertapenem"
  ],
  "clinda": [
    "clindamycine"
  ],
  "flagyl": [
    "metronidazole"
  ],
  "metro": [
    "metronidazole"
  ],
  "doxy": [
    "doxycycline"
  ],
  "azithro": [
    "azithromycine"
  ],
  "clav": [
    "clavulanate",
    "amoxicilline"
  ],
  "amox": [
    "amoxicilline"
  ],
  "cloxa": [
    "cloxacilline"
  ],
  "cefaz": [
    "cefazoline",
    "cefazolin"
  ],
  "keflex": [
    "cephalexine"
  ],
  "genta": [
    "gentamicine"
  ],
  "tobra": [
    "tobramycine"
  ],
  "septra": [
    "triclotrimoxazole",
    "tmp",
    "smx"
  ],
  "tmp smx": [
    "triclotrimoxazole"
  ],
  "fkp": [
    "fibrose kystique",
    "fkp"
  ],
  "mpoc": [
    "mpoc"
  ],
  "si": [
    "soins intensifs"
  ],
  "usi": [
    "soins intensifs"
  ],
  "enterococcus faecalis": [
    "enterocoques",
    "faecalis"
  ],
  "enterococcus faecium": [
    "enterocoques",
    "faecium"
  ],
  "streptocoque": [
    "streptococcus",
    "streptocoque"
  ],
  "streptococcus": [
    "streptococcus",
    "streptocoque"
  ],
  "staphylocoque": [
    "staphylococcus",
    "staphyloccocus"
  ],
  "haemophilus": [
    "hemophilus",
    "influenzae"
  ],
  "hemophilus": [
    "hemophilus",
    "influenzae"
  ],
  "clostridioides": [
    "clostridioides",
    "difficile"
  ],
  "legionella": [
    "legionella",
    "pneumophilia"
  ],
  "stenotrophomonas": [
    "stenotrophomonas",
    "maltophilia"
  ],
  "tx": [
    "traitement",
    "traitements"
  ],
  "rx": [
    "antibiotique",
    "antibiotiques",
    "traitement"
  ],
  "dx": [
    "diagnostic",
    "laboratoire",
    "gram"
  ],
  "ddx": [
    "diagnostic",
    "laboratoire"
  ],
  "bactériémie": [
    "bacteriemie",
    "sepsis"
  ],
  "empirique": [
    "traitement",
    "empirique"
  ],
  "sensibilite": [
    "sensibilite",
    "antibiogramme"
  ],
  "antibiogramme": [
    "sensibilite",
    "taux"
  ],
  "antibios": [
    "antibiotique",
    "antibiotiques"
  ],
  "antibio": [
    "antibiotique",
    "antibiotiques"
  ],
  "quand donner": [
    "indications"
  ],
  "quoi savoir": [
    "perles",
    "cliniques"
  ],
  "prevenir": [
    "prevention"
  ],
  "attrape": [
    "epidemiologie",
    "acquisition"
  ],
  "bon choix": [
    "indications",
    "pharmacocinetique"
  ],
  "penetre": [
    "concentre",
    "diffusion"
  ],
  "bonne option": [
    "indications"
  ],
  "ca marche tu": [
    "indications",
    "efficace"
  ],
  "allergique": [
    "allergie",
    "reaction"
  ],
  "reaction": [
    "reaction",
    "indesirable",
    "effet"
  ],
  "danger": [
    "effet",
    "indesirable"
  ],
  "risque": [
    "effet",
    "indesirable",
    "risque"
  ],
  "ajuster": [
    "posologie",
    "insuffisance"
  ],
  "insuffisance renale": [
    "renale",
    "ajustement"
  ],
  "surveiller": [
    "suivi",
    "surveillance"
  ],
  "combien de temps": [
    "duree",
    "jours"
  ],
  "remplacer": [
    "alternative",
    "spectre"
  ],
  "contagieux": [
    "transmission"
  ],
  "transmet": [
    "transmission",
    "acquisition"
  ],
  "a risque": [
    "risque",
    "facteur",
    "epidemiologie"
  ],
  "compare": [
    "comparaison",
    "spectre"
  ],
  "enterocoque": [
    "enterocoques",
    "faecium",
    "faecalis"
  ],
  "enterocoques": [
    "enterocoques",
    "faecium",
    "faecalis"
  ],
  "combien de": [
    "posologie",
    "dose"
  ],
  "vient d ou": [
    "epidemiologie"
  ],
  "comprime": [
    "relais",
    "per os"
  ],
  "je donne quoi": [
    "traitement",
    "traitements"
  ],

  // ----- NOMS FRANÇAIS ET SURNOMS DES BACTÉRIES -----
  // Le wiki titre ses pages en latin; à l'hôpital on dit « le pneumocoque »,
  // « la gono », « la kleb ». Chaque valeur est un mot RÉELLEMENT écrit dans
  // le titre de la page visée : aucune interprétation, juste une traduction.
  "pneumocoque": ["streptococcus", "pneumoniae"],
  "pneumocoques": ["streptococcus", "pneumoniae"],
  "meningocoque": ["neisseria", "meningitidis"],
  "meningo": ["neisseria", "meningitidis"],
  "gonocoque": ["neisseria", "gonorrhoeae"],
  "gono": ["neisseria", "gonorrhoeae"],
  "gonorrhee": ["neisseria", "gonorrhoeae"],
  "salmonelle": ["salmonella"],
  "salmonelles": ["salmonella"],
  "typhi": ["salmonella", "typhoide"],
  "salmonellose": ["salmonella"],
  "listeriose": ["listeria", "monocytogenes"],
  "listeria": ["listeria", "monocytogenes"],
  "legionelle": ["legionella", "pneumophilia"],
  "legionellose": ["legionella", "pneumophilia"],
  "kleb": ["klebsiella"],
  "h flu": ["hemophilus", "influenzae"],
  "hflu": ["hemophilus", "influenzae"],
  "hfluenzae": ["hemophilus", "influenzae"],
  "sero": ["serologies", "serologie"],
  "seros": ["serologies", "serologie"],
  "pen g": ["penicilline"],
  "pen v": ["penicilline"],
  "peni": ["penicilline"],
  "pen": ["penicilline"],
  "h influenzae": ["hemophilus", "influenzae"],
  "nocardiose": ["nocardia"],
  "actino": ["actinomyces"],
  "colibacille": ["escherichia", "coli"],
  "staph dore": ["staphylococcus", "aureus"],
  "scn": ["staphylococcus", "coagulase", "negative"],
  "coag neg": ["staphylococcus", "coagulase", "negative"],
  "coagulase negatif": ["staphylococcus", "coagulase", "negative"],
  "diphterie": ["corynebacterium", "diphtheriae"],
  "corynebacterie": ["corynebacterium"],
  "charbon": ["bacillus", "anthracis"],
  "pyo": ["pseudomonas", "aeruginosa"],
  "steno": ["stenotrophomonas", "maltophilia"],
  // Abréviations et formes françaises vues dans les questions Kaggle du 28 oct. 2026.
  "stenotro": ["stenotrophomonas", "maltophilia"],
  "pseu": ["pseudomonas"],
  "aeru": ["pseudomonas", "aeruginosa"],
  "p aeru": ["pseudomonas", "aeruginosa"],
  "legio": ["legionella", "pneumophilia"],
  "pneumophila": ["legionella", "pneumophilia"],
  "salmonelle": ["salmonella"],
  "salmonelles": ["salmonella"],
  "typhi": ["salmonella", "typhoide"],
  "clox": ["cloxacilline"],
  "cefa": ["cefazoline"],
  "c diff": ["clostridioides", "difficile"],
  "dacd": ["clostridioides", "difficile"],
  "viridans": ["viridans"],

  // ----- NOMS COMMERCIAUX ET ABRÉVIATIONS DE MÉDICAMENTS -----
  "ancef": ["cefazoline"],
  "rocephin": ["ceftriaxone"],
  "fortaz": ["ceftazidime"],
  "maxipime": ["cefepime"],
  "tazocin": ["piperacilline", "tazobactam"],
  "zosyn": ["piperacilline", "tazobactam"],
  "zithromax": ["azithromycine"],
  "levaquin": ["levofloxacine"],
  "cleocin": ["clindamycine"],
  "dapto": ["daptomycine"],
  "zyvox": ["linezolide"],
  "macrobid": ["nitrofurantoine"],
  "nitro": ["nitrofurantoine"],
  "tige": ["tigecycline"],
  "ampi": ["ampicilline"],
  "cefox": ["cefoxitine"],
  "cefuro": ["cefuroxime"],
  "imi": ["imipenem"],
  "norflo": ["norfloxacine"],
  "fluco": ["fluconazole"],
  "rifampin": ["rifampicine"],
  "inh": ["isoniazide"],
  "pza": ["pyrazinamide"],
  "emb": ["ethambutol"],
  "acyclo": ["acyclovir"],
  "valtrex": ["valacyclovir"],
  "tamiflu": ["oseltamivir"],
  "famvir": ["famciclovir"],
  "pnc": ["penicilline"],
  // ----- CONCEPTS CLINIQUES : MES MOTS -> CEUX DU WIKI -----
  // Le glossaire traduisait les noms (pneumocoque -> Streptococcus
  // pneumoniae). Il ne traduisait pas ce qu'on DEMANDE : « est-ce que ça
  // passe dans le LCR ? » alors que la page écrit « méninges », « à jeun »
  // pour « avec de la nourriture », « en IR » pour « clairance ».
  // « hémoculture positive » : ce que le wiki écrit, c'est « bactériémie ».
  // Pluriels des mots de prélèvement : le résident dit « dans les urines »,
  // « dans les selles », le wiki écrit « urinaire », « digestif ».
  "urines": ["urinaire", "urinaires"],
  "selles": ["digestif", "colite", "diarrhee"],
  "expectorations": ["pulmonaire", "pneumonie", "respiratoire"],
  "plaies": ["cutanee", "tissus mous", "cellulite"],

  "hemoculture": ["bacteriemie", "sepsis", "hemoculture"],
  "hemocultures": ["bacteriemie", "sepsis", "hemoculture"],
  "hemoculture positive": ["bacteriemie", "sepsis"],
  "ddx": ["diagnostic", "laboratoire"],

  "lcr": ["meninges", "meningee", "snc", "meningite"],
  "liquide cephalorachidien": ["meninges", "snc"],
  "a jeun": ["nourriture", "repas", "alimentation", "biodisponibilite"],
  "jeun": ["nourriture", "repas", "alimentation"],
  "avec la bouffe": ["nourriture", "repas"],
  "manger": ["nourriture", "repas"],
  "ir": ["clairance", "renale", "insuffisance renale"],
  "dialyse": ["hemodialyse", "clairance", "posologie"],
  "dfg": ["clairance", "renale"],
  "hemoc": ["hemoculture", "hemocultures"],
  "hemocs": ["hemoculture", "hemocultures"],
  "switch": ["relais", "per os"],
  "step down": ["relais", "per os"],
  "premiere ligne": ["premier choix", "indications", "spectre"],
  "1ere ligne": ["premier choix", "indications", "spectre"],
  "premiere intention": ["premier choix", "indications"],
  "pour les oreilles": ["ototoxicite", "ototoxique"],
  "ototox": ["ototoxicite", "ototoxique"],
  "nephrotox": ["nephrotoxicite", "nephrotoxique"],
  "pour les reins": ["nephrotoxicite", "nephrotoxique", "renale"],
  "pour le foie": ["hepatique", "hepatite", "hepatotoxicite"],
  "foie": ["hepatique", "hepatite"],
  "chez l enfant": ["pediatrique", "enfant", "enfants"],
  "chez le vieux": ["age", "agee", "personne agee"],
  "chez la personne agee": ["age", "agee"],

  // ----- MÉCANISMES DE RÉSISTANCE -----
  // Le wiki leur consacre une page entière, « Les résistances aux
  // antibiotiques ». Son titre ne contient aucun de ces sigles : sans cette
  // table, une question sur une BLSE ou une KPC ne trouve pas sa page.
  "esbl": ["resistances", "antibiotiques"],
  "blse": ["resistances", "antibiotiques"],
  "ampc": ["resistances", "antibiotiques"],
  "kpc": ["resistances", "antibiotiques"],
  "ndm": ["resistances", "antibiotiques"],
  "oxa": ["resistances", "antibiotiques"],
  "carbapenemase": ["resistances", "antibiotiques"],
  "carbapenemases": ["resistances", "antibiotiques"],
  "penicillinase": ["resistances", "antibiotiques"],
  "penicillinases": ["resistances", "antibiotiques"],
  "cephalosporinase": ["resistances", "antibiotiques"],
  "efflux": ["resistances", "antibiotiques"],
  "space": ["resistances", "antibiotiques"],
  "spice": ["resistances", "antibiotiques"],
  "pekk": ["resistances", "antibiotiques"],

  "pnc g": ["penicilline"],
  "peni": ["penicilline"],
  "peni g": ["penicilline"]
};

window.MICROGUIDE_INTENTIONS = {
  "traitement": {
    "declencheurs": [
      "traiter",
      "traite",
      "traitement",
      "atb",
      "abx",
      "antibiotique",
      "antibiotiques",
      "soigner",
      "donner",
      "prescrire",
      "couvrir",
      "couverture",
      "couvre",
      "couvert",
      "efficace",
      "quoi donner",
      "tuer",
      "marche",
      "fonctionne",
      "choix",
      "empirique",
      "per os",
      "po",
      "iv",
      "tx",
      "rx",
      "antibiogramme",
      "sensibilite",
      "taux",
      "antibios",
      "antibio",
      "quand donner",
      "je donne quoi",
      "patient avec",
      "on donne quoi",
      "je mets quoi",
      "je commence quoi"
    ],
    "titres": [
      "traitement",
      "traitements",
      "efficace",
      "efficaces",
      "spectre",
      "couverture",
      "sensibilite",
      "posologie",
      "taux",
      "antibiogramme"
    ]
  },
  "resistance": {
    "declencheurs": [
      "resistance",
      "resistances",
      "resistant",
      "resiste",
      "mecanisme",
      "mecanismes",
      "blse",
      "carbapenemase",
      "sarm",
      "ervr",
      "resiste",
      "resistent",
      "resistance",
      "mecanisme"
    ],
    "titres": [
      "resistance",
      "resistances",
      "mecanisme",
      "mecanismes"
    ]
  },
  "clinique": {
    "declencheurs": [
      "infection",
      "infections",
      "cause",
      "causes",
      "clinique",
      "maladie",
      "symptome",
      "symptomes",
      "manifestation",
      "manifestations",
      "tableau",
      "pneumonie",
      "cellulite",
      "endocardite",
      "meningite",
      "urinaire",
      "osteomyelite",
      "abces",
      "bacteriemie",
      "sepsis",
      "otite",
      "keratite"
    ],
    "titres": [
      "infection",
      "infections",
      "clinique",
      "manifestations",
      "principales"
    ]
  },
  "epidemiologie": {
    "declencheurs": [
      "epidemiologie",
      "reservoir",
      "ou vit",
      "transmission",
      "attraper",
      "pogner",
      "acquisition",
      "colonisation",
      "colonisant",
      "risque",
      "nosocomial",
      "nosocomiale",
      "communautaire",
      "porteur",
      "attrape",
      "attrapent",
      "pogne",
      "ou vit",
      "reservoir",
      "vit",
      "transmet",
      "transmis",
      "transmission",
      "contagieux",
      "contagion",
      "propage",
      "population",
      "populations",
      "terrain",
      "facteur",
      "facteurs",
      "porteur",
      "colonise",
      "vient d ou",
      "vient dou",
      "provient",
      "ou le trouve",
      "habitat",
      "niche"
    ],
    "titres": [
      "epidemiologie",
      "reservoir",
      "transmission",
      "acquisition",
      "specificite",
      "specificite",
      "facteur"
    ]
  },
  "laboratoire": {
    "declencheurs": [
      "laboratoire",
      "labo",
      "identification",
      "identifier",
      "culture",
      "gram",
      "microscope",
      "coloration",
      "catalase",
      "coagulase",
      "oxydase",
      "lactose",
      "milieu",
      "gelose",
      "hemoculture",
      "tsi",
      "dx",
      "ddx"
    ],
    "titres": [
      "laboratoire",
      "laboratoires",
      "microscope",
      "identification",
      "culture",
      "caracteristiques",
      "tests"
    ]
  },
  "indications": {
    "declencheurs": [
      "indication",
      "indications",
      "quand donner",
      "quand utiliser",
      "dans quel cas",
      "quand prescrire"
    ],
    "titres": [
      "indication",
      "indications"
    ]
  },
  "prevention": {
    "declencheurs": [
      "prevenir",
      "prevention",
      "vaccin",
      "prophylaxie",
      "eviter"
    ],
    "titres": [
      "prevention",
      "prophylaxie"
    ]
  },
  "perles": {
    "declencheurs": [
      "perle",
      "perles",
      "quoi savoir",
      "a savoir",
      "piege",
      "truc",
      "particularite"
    ],
    "titres": [
      "perle",
      "perles",
      "particularite"
    ]
  },
  "distribution": {
    "declencheurs": [
      "urinaire",
      "urinaires",
      "urine",
      "vessie",
      "rein",
      "renale",
      "lcr",
      "meninge",
      "meninges",
      "cerebral",
      "snc",
      "os",
      "osseux",
      "osteo",
      "articulaire",
      "prostate",
      "prostatique",
      "poumon",
      "pulmonaire",
      "peau",
      "cutane",
      "tissu",
      "tissus",
      "biliaire",
      "oculaire",
      "oeil",
      "placenta",
      "lait",
      "penetre",
      "penetration",
      "concentre",
      "concentration",
      "diffuse",
      "diffusion",
      "bon choix",
      "bonne option",
      "efficace pour"
    ],
    "titres": [
      "pharmacocinet",
      "biodisponibilit",
      "perle",
      "particularite",
      "indication"
    ]
  },
  "effets": {
    "declencheurs": [
      "effet",
      "effets",
      "secondaire",
      "secondaires",
      "indesirable",
      "indesirables",
      "reaction",
      "reactions",
      "allergie",
      "allergique",
      "allergies",
      "intolerance",
      "toxicite",
      "toxique",
      "eruption",
      "rash",
      "anaphylaxie",
      "nausee",
      "nausees",
      "diarrhee",
      "vomissement",
      "phlebite",
      "irritation",
      "hepatique",
      "renale",
      "nephrotoxicite",
      "ototoxicite",
      "qt",
      // ce qu'on dit à l'hôpital pour désigner une toxicité d'organe :
      // « c'est-tu dur pour le foie ? », « ça brûle-tu les oreilles ? »
      "foie",
      "hepatotoxicite",
      "hepatite",
      "oreille",
      "oreilles",
      "surdite",
      "acouphene",
      "vestibulaire",
      "ototoxique",
      "nephrotoxique",
      "tendinite",
      "risque",
      "danger",
      "complication"
    ],
    "titres": [
      "effet",
      "indesirable",
      "secondaire",
      "suivi",
      "perle"
    ]
  },
  "ajustement": {
    "declencheurs": [
      "ajuster",
      "ajustement",
      "insuffisance",
      "renale",
      "renal",
      "clairance",
      "dialyse",
      "hemodialyse",
      "cockcroft",
      "dfg"
    ],
    "titres": [
      "posologie",
      "perle",
      "pharmacocinet"
    ]
  },
  "surveillance": {
    "declencheurs": [
      "surveiller",
      "surveillance",
      "suivi",
      "suivis",
      "doser",
      "dosage",
      "taux",
      "monitorer",
      "controler",
      "bilan",
      "fsc",
      "creatinine"
    ],
    "titres": [
      "effet",
      "suivi",
      "perle",
      "posologie"
    ]
  },
  "duree": {
    "declencheurs": [
      "duree",
      "combien de temps",
      "combien de jours",
      "jusqu a quand",
      "pendant combien"
    ],
    "titres": [
      "posologie",
      "indication",
      "perle"
    ]
  },
  "interaction": {
    "declencheurs": [
      "interaction",
      "interactions",
      "inr",
      "warfarine",
      "coumadin",
      "associer",
      "incompatib"
    ],
    "titres": [
      "interaction",
      "perle"
    ]
  },
  "alternative": {
    "declencheurs": [
      "remplacer",
      "alternative",
      "alternatives",
      "si allergie",
      "a la place",
      "autre option",
      "substitut"
    ],
    "titres": [
      "spectre",
      "indication",
      "perle",
      "traitement"
    ]
  },
  "comparaison": {
    "declencheurs": [
      "compare",
      "comparaison",
      "par rapport",
      "avantage",
      "difference",
      "mieux que",
      "contrairement",
      "versus",
      "plutot que"
    ],
    "titres": [
      "spectre",
      "perle",
      "indication",
      "particularite"
    ]
  },
  "posologie": {
    "declencheurs": [
      "posologie",
      "poso",
      "dose",
      "doses",
      "combien de",
      "combien je",
      "mg",
      "grammes",
      "par jour",
      "q8h",
      "q12h",
      "die",
      "bid",
      "tid"
    ],
    "titres": [
      "posologie",
      "dose"
    ]
  },
  "relais": {
    "declencheurs": [
      "comprime",
      "comprimes",
      "capsule",
      "capsules",
      "par la bouche",
      "per os",
      "relais",
      "po",
      "orale",
      "oral"
    ],
    "titres": [
      "relais",
      "per os",
      "pharmacocinet",
      "biodisponibilit"
    ]
  }
};

/* ---------- 2/4 construction de l'index ---------- */
// Construit l'index de microguide.ca DANS LE NAVIGATEUR, à partir de l'API
// du wiki. Aucun fichier à maintenir : l'index se refait tout seul, donc il
// est toujours à jour avec le wiki.
//
// Deux appels suffisent : le contenu de toutes les pages, puis l'URL des
// images (~130 Ko). L'index est gardé dans le navigateur et reconstruit
// dès que le wiki change, donc il suit les modifications du site.

(function (root) {
  "use strict";

  var MOTS_VIDES = ("a au aux avec ce ces dans de des du elle en et eux il ils je la le les leur lui ma mais " +
    "me meme mes moi mon ne nos notre nous on ou par pas pour qu que qui sa se ses son sur ta te tes toi ton " +
    "tu un une vos votre vous y est sont etre ete suis es sommes etes ont ai as avons avez avait etait sera " +
    "seront c d j l m n s t quel quelle quels quelles quoi comment pourquoi quand combien cet cette plus " +
    "moins tres bien aussi alors donc car si comme tout tous toute toutes autre autres faire fait fais peut " +
    "peux peuvent doit dois doivent").split(" ");

  function normalise(s) {
    return (s || "").toLowerCase().normalize("NFD")
      .replace(/[̀-ͯ]/g, "")
      .replace(/[-'’/]/g, " ");
  }

  var VIDES = new Set(MOTS_VIDES);

  function mots(s) {
    // « groupe A » / « groupe B » : la lettre seule serait jetée (un seul
    // caractère), et les streptocoques A, B, C deviendraient identiques.
    // « groupe C et G » : la deuxième lettre se perdait, et la page des
    // streptocoques C et G devenait introuvable par « groupe G ».
    var t = normalise(s)
      .replace(/\bgroupe\s+([a-g])\s+et\s+([a-g])\b/g, "groupe$1 groupe$2")
      .replace(/\bsg([abcg])\b/g, "groupe$1")
      .replace(/\bgroupe\s+([a-g])\b/g, "groupe$1")
      .replace(/\b(strep|streptocoque|streptocoques|streptococcus)\s+([abcg])\b/g, "$1 groupe$2");
    return (t.match(/[a-z0-9]+/g) || []).filter(function (w) {
      return w.length > 1 && !VIDES.has(w);
    });
  }

  // ------------------------------------------------ nettoyage du wikitext

  function cellules(bloc) {
    var out = [];
    bloc.split("\n").forEach(function (l) {
      l = l.trim();
      if (l[0] === "!" || l[0] === "|") {
        l = l.replace(/^[!|]+/, "");
        if (l.indexOf("|") >= 0 && l.indexOf("||") < 0) l = l.replace(/^[^|]*\|(?!\|)/, "");
        l.split("||").forEach(function (c) {
          c = c.replace(/^[a-z\-]+\s*=\s*"[^"]*"\s*\|/, "").trim();
          if (c && !/^[-\s|]*$/.test(c)) out.push(c);
        });
      }
    });
    return out.join("\n");
  }

  function nettoyer(t) {
    t = t.replace(/<!--[\s\S]*?-->/g, "");
    t = t.replace(/<ref[^>]*\/>/g, "").replace(/<ref[\s\S]*?<\/ref>/g, "");
    for (var i = 0; i < 5; i++) {
      var t2 = t.replace(/\{\{[^{}]*\}\}/g, "");
      if (t2 === t) break;
      t = t2;
    }
    t = t.replace(/\{\|[\s\S]*?\|\}/g, function (m) { return cellules(m); });
    t = t.replace(/\[\[(?:Fichier|File|Image)\s*:[^\]]*\]\]/gi, "");
    t = t.replace(/\[\[([^\]|]*)\|([^\]]*)\]\]/g, "$2").replace(/\[\[([^\]]*)\]\]/g, "$1");
    // (les liens internes restent du texte : la page citée est déjà donnée)
    // Les liens externes : on GARDE l'adresse. Avant, « [url texte] » ne
    // gardait que « texte » et le lien disparaissait de la réponse.
    t = t.replace(/\[(https?:\/\/\S+)\s+([^\]]*)\]/g, "$2 ($1)")
         .replace(/\[(https?:\/\/\S+)\]/g, "$1");
    t = t.replace(/<br\s*\/?>/gi, "\n");
    t = t.replace(/<\/?(div|span|p|big|small|center|u|b|i|sub|sup|font|blockquote)[^>]*>/gi, "");
    t = t.replace(/<[^>]+>/g, " ");
    t = t.replace(/'''/g, "").replace(/''/g, "");
    t = t.replace(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">");
    t = t.replace(/[ \t]+/g, " ").replace(/\n\s*\n\s*\n+/g, "\n\n");
    return t.trim();
  }

  // ------------------------------------------------ découpage en sections

  function decouper(wikitext) {
    var lignes = wikitext.split("\n");
    var sections = [], cur = { titre: "", lignes: [] };
    lignes.forEach(function (ligne) {
      var m = ligne.match(/^\s*(={2,6})\s*(.+?)\s*\1\s*$/);
      if (m) {
        if (cur.lignes.length) sections.push(cur);
        var brut = m[2];
        // un titre qui n'est qu'une image n'est pas un titre
        if (/^\s*\[\[(?:Fichier|File|Image)\s*:/i.test(brut)) brut = "";
        cur = { titre: nettoyer(brut) || brut.replace(/^['= ]+|['= ]+$/g, ""), lignes: [] };
      } else {
        cur.lignes.push(ligne);
      }
    });
    if (cur.lignes.length) sections.push(cur);

    return sections.map(function (s) {
      var brut = s.lignes.join("\n");
      var imgs = [];
      var re = /\[\[(?:Fichier|File|Image)\s*:\s*([^|\]]+)/gi, m;
      while ((m = re.exec(brut))) imgs.push(m[1].trim().replace(/_/g, " "));
      var texte = nettoyer(brut);
      var puces = [], prose = [];
      texte.split("\n").forEach(function (l) {
        l = l.trim();
        if (!l) return;
        if (/^[\*\#•·–-]\s*/.test(l)) puces.push(l.replace(/^[\*\#•·–-]+\s*/, "").trim());
        else prose.push(l);
      });
      return { titre: s.titre, puces: puces, prose: prose, images: imgs };
    }).filter(function (s) {
      return s.puces.length || s.prose.length || s.images.length;
    });
  }

  // ------------------------------------------------ appels à l'API

  function api(params, base) {
    var u = (base || "") + "/api.php?origin=*&format=json&formatversion=2&" +
      Object.keys(params).map(function (k) {
        return encodeURIComponent(k) + "=" + encodeURIComponent(params[k]);
      }).join("&");
    return fetch(u, { credentials: "omit" }).then(function (r) {
      if (!r.ok) throw new Error("API " + r.status);
      return r.json();
    });
  }

  function toutesLesPages(base) {
    var pages = [];
    function tour(cont) {
      var p = {
        action: "query", generator: "allpages", gapnamespace: 0, gaplimit: "max",
        prop: "revisions", rvprop: "content|timestamp", rvslots: "main"
      };
      if (cont) for (var k in cont) p[k] = cont[k];
      return api(p, base).then(function (d) {
        ((d.query && d.query.pages) || []).forEach(function (pg) {
          var rev = pg.revisions && pg.revisions[0];
          var txt = rev && rev.slots && rev.slots.main && rev.slots.main.content;
          if (txt) pages.push({ titre: pg.title, wikitext: txt });
        });
        if (d.continue) return tour(d.continue);
        return pages;
      });
    }
    return tour(null);
  }

  // Les doses du calculateur de Thomas vivent dans MediaWiki:Common.js,
  // dans un objet ANTIBIOS : classe -> medicament -> paliers selon la
  // clairance renale. On les recupere pour pouvoir repondre aux questions
  // de posologie que le texte des pages ne couvre pas.
  function dosesDuCalculateur(base) {
    return api({ action: "query", titles: "MediaWiki:Common.js",
                 prop: "revisions", rvprop: "content", rvslots: "main" }, base)
      .then(function (d) {
        var pg = d.query && d.query.pages && d.query.pages[0];
        var txt = pg && pg.revisions && pg.revisions[0].slots.main.content;
        if (!txt) return {};
        var i = txt.indexOf("var ANTIBIOS");
        if (i < 0) return {};
        var fin = txt.indexOf("/* ====== DEBUT Assistant Microguide");
        var zone = txt.slice(i, fin > i ? fin : txt.length);
        var out = {};
        // « Nom (voie) »: { notes: "...", doses: [ {min,max,label}, ... ] }
        // Le crochet fermant cherché est celui qui ferme la LISTE, pas celui
        // d'un libellé : Thomas écrit « 3,375g [3g/0,375g] IV q6h », et un
        // crochet non gourmand s'arrêtait au milieu du libellé. Résultat :
        // toute la pipéracilline-tazobactam disparaissait du calculateur.
        // On exige donc le « ] } » qui ferme la liste et l'objet.
        var re = /"([^"]{3,60})"\s*:\s*\{\s*notes\s*:\s*"([^"]*)"\s*,\s*doses\s*:\s*\[([\s\S]*?)\]\s*\}/g, m;
        while ((m = re.exec(zone))) {
          var nom = m[1], note = m[2], corps = m[3];
          var paliers = [], rl = /min\s*:\s*(\d+)\s*,\s*max\s*:\s*(\d+)\s*,\s*label\s*:\s*"([^"]+)"/g, r2;
          while ((r2 = rl.exec(corps))) {
            paliers.push({ min: +r2[1], max: +r2[2], label: r2[3] });
          }
          if (paliers.length) out[nom] = { note: note, paliers: paliers };
        }
        return out;
      })["catch"](function () { return {}; });
  }

  function toutesLesImages(base) {
    var map = {};
    function tour(cont) {
      var p = { action: "query", list: "allimages", ailimit: "max" };
      if (cont) for (var k in cont) p[k] = cont[k];
      return api(p, base).then(function (d) {
        ((d.query && d.query.allimages) || []).forEach(function (im) {
          map[im.name.replace(/_/g, " ")] = im.url;
        });
        if (d.continue) return tour(d.continue);
        return map;
      });
    }
    return tour(null);
  }

  // ------------------------------------------------ assemblage de l'index

  function assembler(pagesBrutes, imgMap, base, doses) {
    doses = doses || {};
    var pages = [], sections = [], vus = {};

    // Le wiki contient des redirections et des pages-coquilles (titres mal
    // orthographiés, pages vides) qui, sans ça, passent devant les vraies.
    pagesBrutes = pagesBrutes.filter(function (p) {
      if (/^\s*#\s*REDIRECT/i.test(p.wikitext)) return false;
      return nettoyer(p.wikitext).replace(/\s/g, "").length >= 80;
    });
    // à titre égal, on garde la page la plus fournie
    pagesBrutes.sort(function (a, b) { return b.wikitext.length - a.wikitext.length; });

    pagesBrutes.forEach(function (p) {
      if (vus[p.titre]) return;            // redirections vers le même titre
      var secs = decouper(p.wikitext);
      if (!secs.length) return;
      vus[p.titre] = 1;

      var pid = pages.length;
      var imgsPage = [];
      secs.forEach(function (sec, k) {
        sec.images.forEach(function (n) {
          if (imgMap[n]) {
            imgsPage.push({
              nom: n, url: imgMap[n],
              section: sec.titre || p.titre,
              entete: k === 0 && !sec.titre
            });
          }
        });
      });

      pages.push({
        id: pid, titre: p.titre,
        url: base + "/index.php/" + encodeURI(p.titre.replace(/ /g, "_")),
        images: imgsPage
      });

      secs.forEach(function (s) {
        var plat = s.prose.concat(s.puces).join(" ");
        var images = s.images.filter(function (n) { return imgMap[n]; })
          .map(function (n) { return { nom: n, url: imgMap[n] }; });
        if (plat.length < 15 && !images.length) return;
        sections.push({
          page: pid, titre: s.titre || p.titre,
          puces: s.puces, prose: s.prose, images: images,
          // Le NOM DE LA PAGE est indexé dans chacune de ses sections :
          // la page « Ceftazidime » a des sections « Spectre », « Posologie »
          // qui ne contiennent jamais le mot « ceftazidime ». Sans ceci, la
          // page est introuvable par son propre nom.
          // Le titre de section compte double, pour qu'une section
          // « Couverture » se trouve même si son contenu est une image.
          mots: mots(p.titre + " " + (s.titre || "") + " " + (s.titre || "") + " " + plat)
        });
      });
    });

    // GRAM : la page « Bactéries » du wiki classe tout, en deux grandes
    // sections (gram positive, puis gram négative). C'est la source la plus
    // fiable : le gram de chaque fiche est dans son image d'en-tête, donc
    // illisible pour nous, alors qu'ici il est écrit noir sur blanc.
    var gramParTitre = {}, morphoParTitre = {};
    pagesBrutes.forEach(function (p) {
      if (!/^bact[eé]ries$/i.test(p.titre.trim())) return;
      var coupe = p.wikitext.search(/gram\s*n[ée]gativ/i);
      var parts = coupe < 0 ? [p.wikitext] : [p.wikitext.slice(0, coupe), p.wikitext.slice(coupe)];
      parts.forEach(function (bout, k) {
        var g = (parts.length === 2 && k === 1) ? "neg" : "pos";
        // On parcourt le texte dans l'ordre : chaque titre met à jour la
        // morphologie courante, chaque lien hérite de la dernière vue.
        var morpho = null;
        var re = /(^|\n)\s*=+([^\n=][^\n]*?)=+\s*(?=\n|$)|\[\[([^\]|#]+)/g, m;
        while ((m = re.exec(bout))) {
          if (m[2] !== undefined) {
            var th = m[2].toLowerCase();
            if (/coccobacill/.test(th)) morpho = "coccobacille";
            else if (/cocci|coque/.test(th)) morpho = "cocci";
            else if (/bacill/.test(th)) morpho = "bacille";
          } else if (m[3]) {
            var cible = m[3].trim().replace(/_/g, " ");
            if (/^(fichier|file|image|cat[eé]gorie|category)\s*:/i.test(cible)) continue;
            gramParTitre[cible] = g;
            if (morpho) morphoParTitre[cible] = morpho;
          }
        }
      });
    });
    // CLASSE D'ANTIBIOTIQUE : la page « Antibiotiques » les range par famille
    // (beta-lactames, quinolones, glycopeptides, aminosides, lincosamides,
    // macrolides, tetracyclines). Meme principe que le Gram des bacteries.
    // PAGES DE LABORATOIRE. Elles ne sont ni des bactéries ni des
    // antibiotiques : « quel traitement pour Culture de pus » n'a aucun sens.
    // On les reconnaît comme les antibiotiques, par ce que la page
    // d'aiguillage « Tests de laboratoires » relie.
    var laboParTitre = {};
    pagesBrutes.forEach(function (p) {
      if (!/^tests de laboratoires?$/i.test(p.titre.trim())) return;
      var reL = /\[\[([^\]|#]+)/g, mL;
      while ((mL = reL.exec(p.wikitext))) {
        var cL = mL[1].trim().replace(/_/g, " ");
        if (/^(fichier|file|image|cat[e\u00e9]gorie|category)\s*:/i.test(cL)) continue;
        laboParTitre[cL] = true;
      }
    });

    var classeParTitre = {}, antibioParTitre = {};
    pagesBrutes.forEach(function (p) {
      if (!/^antibiotiques$/i.test(p.titre.trim())) return;
      var classe = null;
      var re2 = /(^|\n)\s*=+([^\n=][^\n]*?)=+\s*(?=\n|$)|\[\[([^\]|#]+)/g, m2;
      while ((m2 = re2.exec(p.wikitext))) {
        if (m2[2] !== undefined) {
          var tc = m2[2].toLowerCase().replace(/<[^>]+>/g, "").replace(/['\[\]]/g, "").trim();
          if (/lactam|penicill|cephalo|carbapen/.test(tc)) classe = "beta-lactamine";
          else if (/quinolon/.test(tc)) classe = "quinolone";
          else if (/glycopeptid/.test(tc)) classe = "glycopeptide";
          else if (/aminosid|aminoglycosid/.test(tc)) classe = "aminoside";
          else if (/lincosamid/.test(tc)) classe = "lincosamide";
          else if (/macrolid/.test(tc)) classe = "macrolide";
          else if (/tetracyclin|cyclin/.test(tc)) classe = "tetracycline";
          else if (/imidazol|metro/.test(tc)) classe = "nitroimidazole";
          else classe = null;
        } else if (m2[3]) {
          var c2 = m2[3].trim().replace(/_/g, " ");
          if (/^(fichier|file|image|cat[e\u00e9]gorie|category)\s*:/i.test(c2)) continue;
          // tout ce qui est lie depuis la page Antibiotiques EST un antibiotique
          antibioParTitre[c2] = true;
          if (classe) classeParTitre[c2] = classe;
        }
      }
    });

    pages.forEach(function (p) {
      if (gramParTitre[p.titre]) p.gram = gramParTitre[p.titre];
      if (morphoParTitre[p.titre]) p.morpho = morphoParTitre[p.titre];
      if (classeParTitre[p.titre]) p.classe = classeParTitre[p.titre];
      // doses du calculateur : on relie « Cefadroxil (PO) » a la page qui
      // porte ce nom, en comparant les mots significatifs.
      var cles = mots(p.titre);
      for (var dn in doses) {
        var md = mots(dn.replace(/\([^)]*\)/g, "").split(/\s*[\u2014\u2013]\s*/)[0]);
        if (!md.length) continue;
        // correspondance par radical : « meropenem » doit rejoindre
        // « meropeneme », « cefazolin » rejoindre « cefazoline »
        var communs = md.filter(function (w) {
          return cles.some(function (c) {
            if (c === w) return true;
            var court = Math.min(c.length, w.length);
            return court >= 6 && c.slice(0, court) === w.slice(0, court);
          });
        });
        if (communs.length) {
          (p.doses = p.doses || []).push({ nom: dn, note: doses[dn].note, paliers: doses[dn].paliers });
        }
      }
      // Une page de laboratoire ne doit pas être confondue avec les pages
      // qu'elle cite au passage : la page d'aiguillage renvoie aussi vers
      // « Bactéries », « Antibiotiques » ou « Clostridioides difficile ».
      // Une page qui a une section d'infections, d'épidémiologie ou de
      // résistance est une bactérie, pas un test.
      var txtP = p.wikitext || "";
      var estBact = /Infections principales|[Ée]pid[eé]miologie|M[eé]canismes? de r[eé]sistance|Traitements? g[eé]n[eé]ralement/i.test(txtP);
      var estNav = /^(bact[eé]ries|antibiotiques|classification|accueil|infections|calculateur|les r[eé]sistances aux antibiotiques)$/i
                     .test(p.titre.trim());
      if ((laboParTitre[p.titre] || /^tests de laboratoires?$/i.test(p.titre.trim())) &&
          !antibioParTitre[p.titre] && !estBact && !estNav && !p.gram) p.labo = true;
      if (antibioParTitre[p.titre] || /^(les |l')?(p[e\u00e9]nicillines?|c[e\u00e9]phalosporines?|macrolides?|quinolones?|aminosides?|carbap[e\u00e9]n[e\u00e8]mes?|t[e\u00e9]tracyclines?)/i.test(p.titre.trim())) p.antibio = true;
    });

    // Le calculateur contient ~70 medicaments, dont plusieurs n'ont PAS de
    // page sur le wiki (ampicilline, rifampicine...). Leur dose existe
    // pourtant : on en fait une entree a part entiere, qui renvoie vers la
    // page Calculateur, plutot que de repondre « pas l'information ».
    var dejaRattachees = {};
    pages.forEach(function (p) {
      (p.doses || []).forEach(function (d) { dejaRattachees[d.nom] = 1; });
    });
    var urlCalc = base + "/index.php/Calculateur";
    Object.keys(doses).forEach(function (nomD) {
      if (dejaRattachees[nomD]) return;
      var propre = nomD.replace(/\s*\([^)]*\)\s*/g, "")
                       .split(/\s*[\u2014\u2013]\s*/)[0]          // qualificatif apres un tiret long
                       .trim();
      if (!propre) return;
      // une seule entree par medicament, meme s'il a plusieurs voies
      var dejaId = -1;
      for (var q = 0; q < pages.length; q++) {
        if (pages[q].calcNom === propre) { dejaId = q; break; }
      }
      if (dejaId >= 0) {
        pages[dejaId].doses.push({ nom: nomD, note: doses[nomD].note, paliers: doses[nomD].paliers });
        return;
      }
      var pid = pages.length;
      pages.push({ id: pid, titre: propre, url: urlCalc, images: [], calcNom: propre,
                   doses: [{ nom: nomD, note: doses[nomD].note, paliers: doses[nomD].paliers }] });
    });
    // une section indexable pour chaque medicament du calculateur
    pages.forEach(function (p) {
      if (!p.calcNom) return;
      var lignes = [];
      p.doses.forEach(function (d) {
        d.paliers.forEach(function (pal) {
          var plage = pal.max >= 999 ? "clairance \u2265 " + pal.min
                    : pal.min <= 0 ? "clairance < " + (pal.max + 1)
                    : "clairance " + pal.min + " \u00e0 " + pal.max;
          lignes.push(d.nom + " \u2014 " + plage + " mL/min : " + pal.label);
        });
        if (d.note) lignes.push(d.note);
      });
      sections.push({ page: p.id, titre: "Posologie (calculateur de doses)",
        prose: [], puces: lignes, images: [],
        mots: mots(p.titre + " " + p.titre + " posologie dose " + lignes.join(" ")) });
    });

    var df = {};
    sections.forEach(function (s) {
      new Set(s.mots).forEach(function (w) { df[w] = (df[w] || 0) + 1; });
    });
    sections.forEach(function (s) {
      var tf = {};
      s.mots.forEach(function (w) { tf[w] = (tf[w] || 0) + 1; });
      s.tf = tf; s.len = s.mots.length; delete s.mots;
    });

    return {
      pages: pages, sections: sections, df: df, N: sections.length,
      wiki: base, mots_vides: MOTS_VIDES,
      glossaire: root.MICROGUIDE_GLOSSAIRE || {},
      intentions: root.MICROGUIDE_INTENTIONS || {},
      bati: Date.now()
    };
  }

  // ------------------------- cache local (validé par la date du wiki)

  var CLE = "microguide-index-v1";

  function duCache() {
    try {
      var b = localStorage.getItem(CLE);
      if (!b) return null;
      var d = JSON.parse(b);
      if (!d.bati) return null;
      d.glossaire = root.MICROGUIDE_GLOSSAIRE || {};
      d.intentions = root.MICROGUIDE_INTENTIONS || {};
      return d;
    } catch (e) { return null; }
  }

  function auCache(d) {
    try { localStorage.setItem(CLE, JSON.stringify(d)); } catch (e) { /* quota : tant pis */ }
  }

  // Date de la dernière modification du wiki (articles + fichiers).
  // Une seule petite requête (~1 Ko) : elle dit s'il faut tout relire.
  function derniereModif(base) {
    return api({
      action: "query", list: "recentchanges",
      rcprop: "timestamp", rclimit: 1, rcnamespace: "0|6", rctype: "edit|new|log"
    }, base).then(function (d) {
      var rc = d.query && d.query.recentchanges;
      return (rc && rc.length) ? rc[0].timestamp : null;
    })["catch"](function () { return null; });
  }

  // Construit (ou relit) l'index.
  // L'index n'est réutilisé que si le wiki n'a pas bougé depuis : une page
  // modifiée, ajoutée ou retirée est donc prise en compte à la question
  // suivante, sans rien faire de spécial.
  function charger(base) {
    base = base || "";
    var c = duCache();
    return derniereModif(base).then(function (modif) {
      if (c && modif && c.modif === modif) return c;   // wiki inchangé
      if (c && !modif) return c;                       // hors ligne : on garde
      return Promise.all([toutesLesPages(base), toutesLesImages(base), dosesDuCalculateur(base)])
        .then(function (r) {
          var ix = assembler(r[0], r[1], base, r[2]);
          ix.modif = modif;
          auCache(ix);
          return ix;
        });
    });
  }

  root.MicroguideIndexeur = {
    charger: charger, assembler: assembler, decouper: decouper,
    nettoyer: nettoyer, mots: mots, viderCache: function () {
      try { localStorage.removeItem(CLE); } catch (e) {}
    }
  };
})(typeof module !== "undefined" ? module.exports : window);

/* ---------- 3/4 moteur de réponse ---------- */
// Moteur de réponse de l'assistant microguide — AUCUNE INVENTION POSSIBLE.
//
// La réponse est composée uniquement de phrases et de puces recopiées
// telles quelles depuis les pages du wiki, avec le titre de la section et
// le lien. Il n'y a pas de modèle de langue qui réécrit : le texte affiché
// existe mot pour mot sur microguide.ca.
//
// Principe repris du bibliothécaire de Thomas : les passages sources SONT
// la réponse.

(function (root) {
  "use strict";

  function normalise(s) {
    return (s || "")
      .toLowerCase()
      .normalize("NFD")
      .replace(/[̀-ͯ]/g, "");
  }

  // Mots trop généraux pour désigner une page à eux seuls : sans ça,
  // « quel ATB pour une ostéomyélite » désignerait la page « Les
  // résistances aux antibiotiques » juste par le mot « antibiotique ».
  var GENERIQUES = {
    antibiotique: 1, infection: 1, bacterie: 1, traitement: 1, resistance: 1,
    laboratoire: 1, test: 1, classification: 1, couverture: 1, calculateur: 1,
    accueil: 1, principale: 1, general: 1, spectre: 1, posologie: 1,
    utilise: 1, frequemment: 1, complet: 1, autre: 1,
    // « X est résistant à quoi ? » ne doit pas désigner la page SARM
    // (« Staphylococcus aureus RÉSISTANT à la méticilline ») par ce seul mot.
    resistant: 1, intrinseque: 1, emblee: 1, groupe: 1, espece: 1,
    negative: 1, positive: 1, negatif: 1, positif: 1, gram: 1, bacille: 1,
    complexe: 1, principaux: 1, methicilline: 1,
    meticilline: 1, donner: 1, quoi: 1, pathogene: 1
  };

  // « groupe A », mais aussi « strep B », « SGA », « strep du groupe C » :
  // la lettre seule serait jetée (un mot d'une lettre). On la colle au mot
  // « groupe », exactement comme l'indexeur le fait sur les titres du wiki.
  function lettreDeGroupe(t) {
    return t.replace(/\bgroupe\s+([a-g])\s+et\s+([a-g])\b/g, "groupe$1 groupe$2")
            .replace(/\bsg([abcg])\b/g, "groupe$1")
            .replace(/\bgroupe\s+([a-g])\b/g, "groupe$1")
            .replace(/\b(strep|streptocoque|streptocoques|streptococcus)\s+([abcg])\b/g,
                     "$1 groupe$2");
  }

  // RACINE COMMUNE ENTRE LE LATIN ET LE FRANÇAIS. Le wiki titre « Salmonella »,
  // l'hôpital dit « salmonelle » ou « salmonellose »; « Nocardia » devient
  // « nocardiose », « Legionella » devient « légionelle ». Plutôt que de lister
  // ces formes une par une, on enlève la terminaison et on compare la racine :
  // ça marche pour toutes les pages, y compris celles que Thomas ajoutera.
  function racineNom(w) {
    var r = w.replace(/(ellose|iose|ella|elle|ose|ia|ie|a|e)$/, "");
    return r.length >= 5 ? r : w;
  }
  function memeNom(a, b) {
    if (a === b) return true;
    var ra = racineNom(a), rb = racineNom(b);
    return ra === rb && ra.length >= 5;
  }

  // TOLÉRANCE À UNE FAUTE DE FRAPPE. « péniclline », « ciprofloxcine » : une
  // lettre sautée, doublée ou interchangée ne doit pas faire perdre la page.
  // Une seule faute, et seulement sur les mots longs, pour ne pas confondre
  // deux vrais noms voisins.
  function uneFauteDeFrappe(a, b) {
    if (Math.abs(a.length - b.length) > 1) return false;
    if (a.length < 7 || b.length < 7) return false;
    var i = 0, j = a.length - 1, k = b.length - 1;
    while (i < a.length && i < b.length && a[i] === b[i]) i++;
    if (i === a.length && i === b.length) return false;     // identiques
    while (j >= i && k >= i && a[j] === b[k]) { j--; k--; }
    // ce qui reste au milieu doit tenir en un seul caractère de chaque côté
    return (j - i < 1) && (k - i < 1);
  }

  function stem(w) { return w.length > 3 ? w.replace(/s$/, "") : w; }

  function Moteur(index) {
    this.ix = index;
    this.vides = new Set(index.mots_vides || []);
    this.gloss = index.glossaire || {};
    // longueur moyenne des sections, pour BM25
    var t = 0;
    for (var i = 0; i < index.sections.length; i++) t += index.sections[i].len;
    this.avgdl = t / Math.max(1, index.sections.length);
  }

  Moteur.prototype.mots = function (s) {
    var self = this;
    // « groupe A » / « groupe B » : voir la même règle dans l'indexeur.
    var t = lettreDeGroupe(normalise(s));
    return (t.match(/[a-z0-9]+/g) || []).filter(function (w) {
      return w.length > 1 && !self.vides.has(w);
    });
  };

  // Ajoute le vocabulaire québécois / les abréviations cliniques
  Moteur.prototype.etendre = function (q) {
    // « C. diff », « H. flu », « S. aureus » : le point et le trait d'union
    // cassaient la reconnaissance des abréviations du glossaire, qui sont
    // écrites avec une simple espace. On les ramène à une espace.
    var n = normalise(q).replace(/[.\-'\u2019]/g, " ").replace(/\s+/g, " ");
    var base = this.mots(q);
    var ajouts = [];
    for (var cle in this.gloss) {
      // Le « s » du pluriel ne doit pas faire rater l'entrée : « urines »
      // doit trouver la clé « urine », « hémocultures » la clé « hémoculture ».
      // Mais seulement pour les mots de CONCEPT : pour les noms, le pluriel
      // distingue deux pages (« La pénicilline » et « Pénicillines »).
      var re = new RegExp("\\b" + cle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") +
                          (this.nomDePage(cle) ? "" : "s?") + "\\b");
      if (re.test(n)) {
        var vals = this.gloss[cle];
        for (var i = 0; i < vals.length; i++) {
          ajouts = ajouts.concat(this.mots(vals[i]));
        }
      }
    }
    // les mots de la question pèsent plus que les ajouts du glossaire
    var poids = {};
    base.forEach(function (w) { poids[w] = Math.max(poids[w] || 0, 1); });
    ajouts.forEach(function (w) { poids[w] = Math.max(poids[w] || 0, 0.6); });
    return poids;
  };

  // La clé du glossaire désigne-t-elle une page (un nom), ou un concept ?
  Moteur.prototype.nomDePage = function (cle) {
    if (!this._titresGloss) {
      var t = {}, self = this;
      this.ix.pages.forEach(function (p) {
        self.mots(p.titre).forEach(function (w) { t[w] = 1; });
      });
      this._titresGloss = t;
    }
    // Un mot qui déclenche une intention (« réaction », « dose », « durée »)
    // n'est pas non plus candidat au pluriel : il pèse sur le choix de la
    // section, et l'élargir déplace la réponse.
    if (!this._declencheursGloss) {
      var dg = {}, defs = this.ix.intentions || {}, moi2 = this;
      for (var n in defs) {
        (defs[n].declencheurs || []).forEach(function (d) {
          moi2.mots(d).forEach(function (w) { dg[w] = 1; });
        });
      }
      this._declencheursGloss = dg;
    }
    var mots = this.mots(cle);
    for (var i = 0; i < mots.length; i++) {
      if (this._titresGloss[mots[i]] || this._declencheursGloss[mots[i]]) return true;
    }
    var vals = this.gloss[cle] || [];
    for (var j = 0; j < vals.length; j++) {
      var mv = this.mots(vals[j]);
      for (var k = 0; k < mv.length; k++) if (this._titresGloss[mv[k]]) return true;
    }
    return false;
  };

  // Quelle sorte de renseignement la question demande-t-elle ?
  // « quoi donner pour un pseudomonas » -> traitement, donc on privilégie
  // les sections « Traitements » plutôt qu'une autre qui parle aussi d'ATB.
  Moteur.prototype.intentions = function (question) {
    var n = normalise(question), out = [];
    var defs = this.ix.intentions || {};
    for (var nom in defs) {
      var d = defs[nom], touche = false;
      for (var i = 0; i < d.declencheurs.length; i++) {
        var t = d.declencheurs[i];
        var re = new RegExp("\\b" + t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
        if (re.test(n)) { touche = true; break; }
      }
      if (touche) out.push({ nom: nom, titres: d.titres });
    }
    return out;
  };

  // BM25 sur les sections
  Moteur.prototype.chercher = function (question, nb) {
    var ix = this.ix, self = this;
    var poids = this.etendre(question);
    var inten = this.intentions(question);
    var k1 = 1.4, b = 0.72, N = ix.N;
    var res = [];

    // Quelle page la question NOMME-t-elle ? « le SARM ça se traite comment »
    // doit répondre avec la page du SARM, pas avec une autre page qui parle
    // aussi de traitement. On mesure la correspondance avec le titre de page,
    // pondérée par la rareté des mots (un mot rare identifie mieux).
    // On retient aussi QUELS mots chaque page a reconnus : « Staphylococcus
    // coagulase négative » ne reconnaît que « staphylococcus » là où
    // « Staphylococcus aureus » reconnaît « staphylococcus » ET « aureus ».
    // C'est ce qui permet plus bas d'écarter l'autre staphylocoque.
    var poidsStem = {};
    for (var pw in poids) {
      var ps = stem(pw);
      if (!(poidsStem[ps] >= poids[pw])) poidsStem[ps] = poids[pw];
    }

    // MOTS QUI DÉSIGNENT UNE PAGE SANS ÊTRE DANS SON TITRE.
    // « anthrax » n'apparaît que dans Bacillus anthracis, « diphtérie » que
    // dans Corynebacterium diphtheriae : ces mots désignent clairement une
    // page. À l'inverse « endocardite » est cité dans 21 pages et « pneumonie »
    // dans 27 : ce sont des mentions cliniques, pas des sujets de page.
    // Le critère est la concentration : un mot qui vit à 60 % ou plus dans
    // une seule page la désigne.
    if (!this._designe) {
      var parMot = {}, dansTx = {}, horsTx = {}, enProse = {}, enClinique = {}, sigleWiki = {}, enTitre = {};
      ix.sections.forEach(function (s) {
        var tx = /trait|efficac|spectre|couvertur|indication|sensibilit|resistance/.test(normalise(s.titre));
        // Un terme qui n'apparaît QUE dans une puce est un élément de liste :
        // « chorioamnionite » est une infection parmi d'autres du strep B, pas
        // un autre nom du strep B. « Anthrax », écrit en prose (« Cause
        // l'ANTHRAX »), désigne au contraire Bacillus anthracis. Idée tirée
        // du moteur concurrent : seule une fiche est le sujet d'une réponse.
        s.prose.forEach(function (l) {
          self.mots(l).forEach(function (w) { enProse[w] = 1; });
        });
        // UN NOM DE MALADIE RESTE UN SUJET, MÊME DANS UNE PUCE.
        // « SSSS », « syndrome d'Austrian », « ETEC » sont écrits en puce dans
        // la section des infections : ce sont des noms de tableaux cliniques,
        // et la page qui les porte est bien celle qui les explique. Un terme
        // cité dans une section de laboratoire ou de posologie (« CRP »,
        // « AUC ») n'est, lui, qu'une mention de passage.
        // Les SIGLES du wiki, où qu'ils soient : SDD, CLSI, BLSE, ETEC.
        // Ils sont courts, donc exclus par la règle de longueur, alors qu'ils
        // désignent très précisément ce dont ils parlent.
        s.prose.concat(s.puces).concat([s.titre]).forEach(function (l) {
          (l.match(/\b[A-Z\u00C0-\u00de]{3,6}\b/g) || []).forEach(function (b7) {
            self.mots(b7).forEach(function (w) { sigleWiki[w] = 1; });
          });
        });
        // UN MOT QUI TITRE UNE SECTION EST UN SUJET DE LA PAGE.
        // « Séroconversion » titre une section de la page Sérologies : la page
        // en parle pour de bon, ce n'est pas une mention de passage. Sans ça,
        // sa définition était présentée comme « pas forcément le traitement
        // complet » — une phrase absurde pour un test de laboratoire.
        self.mots(s.titre).forEach(function (w) { enTitre[w] = 1; });
        if (/infection|clinique|perle|manifestation|syndrome|tableau|specificite/.test(normalise(s.titre))) {
          s.puces.forEach(function (l) {
            // seulement les NOMS PROPRES et les SIGLES : « SSSS », « ETEC »,
            // « Austrian », « Waterhouse ». « chorioamnionite », nom commun,
            // reste un élément de liste parmi d'autres, pas un sujet.
            var mots8 = l.match(/[A-Za-z\u00C0-\u00ff][A-Za-z\u00C0-\u00ff'-]*/g) || [];
            for (var mi8 = 0; mi8 < mots8.length; mi8++) {
              var b8 = mots8[mi8];
              var sigle8 = b8.length >= 3 && b8 === b8.toUpperCase();
              var propre8 = mi8 > 0 && /^[A-Z\u00C0-\u00de][a-z\u00e0-\u00ff]{3,}$/.test(b8);
              if (sigle8 || propre8) self.mots(b8).forEach(function (w) { enClinique[w] = 1; });
            }
          });
        }
        for (var w in s.tf) {
          var p = parMot[w] || (parMot[w] = {});
          p[s.page] = (p[s.page] || 0) + s.tf[w];
          if (tx) dansTx[w] = (dansTx[w] || 0) + s.tf[w];
          else horsTx[w] = (horsTx[w] || 0) + s.tf[w];
        }
      });
      // Les pages d'index (« Bactéries », « Antibiotiques », « Classification »)
      // énumèrent des dizaines d'organismes sans les traiter. Sans cette
      // exclusion, « Brucella » semblerait désigner la page « Bactéries » et
      // l'assistant répondrait au lieu de refuser.
      var INDEX = /^(accueil|bacteries|antibiotiques|classification|infections|tests de laboratoires|calculateur|calculateur de dose|couverture des antibiotiques|template|demo|positif|bacilles gram)/;
      var estIndex = ix.pages.map(function (p) { return INDEX.test(normalise(p.titre)); });

      // Page de MÉDICAMENT ou page de BACTÉRIE ? Le wiki suit son propre
      // gabarit : un médicament a Spectre / Posologie / Relais per os, une
      // bactérie a Infections principales / Épidémiologie / Traitements.
      // C'est ce qui distingue « anthrax » (qui désigne la bactérie Bacillus
      // anthracis, un vrai sujet du wiki) de « rickettsies » (juste cité dans
      // le spectre de la doxycycline, sans page à elle).
      var titresPage = [];
      ix.sections.forEach(function (s) {
        (titresPage[s.page] = titresPage[s.page] || []).push(normalise(s.titre));
      });
      var estMedicament = ix.pages.map(function (p, i) {
        var t = (titresPage[i] || []).join(" | ");
        var med = /posologie|relais|biodisponibilit|pharmacocinet|indications principales/.test(t);
        var bact = /infections principales|epidemiologie|mecanisme.{0,4} de resistance|taux de sensibilit/.test(t);
        return med && !bact;
      });

      var des = {}, desFaible = {};
      // Un verbe conjugué ne désigne pas une page : « causent » n'apparaît
      // qu'une fois, dans Staphylococcus aureus, et se mettait à la désigner.
      var VERBE = /(ent|ons|ez|ais|ait|aient|era|eront|erait|ant)$/;
      // Un mot qui sert a exprimer une INTENTION (transmet, resiste, donne,
      // ajuster, surveiller...) decrit la question, pas un sujet de page :
      // il ne doit jamais designer une page.
      if (!this._motsIntention) {
        var mi2 = {};
        var defs2 = ix.intentions || {};
        for (var ni in defs2) {
          (defs2[ni].declencheurs || []).forEach(function (d) {
            self.mots(d).forEach(function (w) { mi2[stem(w)] = 1; });
          });
        }
        this._motsIntention = mi2;
      }
      var motsIntention = this._motsIntention;
      for (var w2 in parMot) {
        if ((ix.df[w2] || 0) > 12) continue;          // mot trop répandu
        // Un mot court n'est pas un nom : « sert », « parle », « voit »
        // désignaient des pages entières parce qu'ils n'étaient écrits qu'à
        // un seul endroit du wiki. Les vrais désignateurs (anthrax,
        // diphtérie, scarlatine, Waterhouse) sont des mots longs.
        if (w2.length < 6 && !sigleWiki[w2]) continue;
        if (VERBE.test(w2) || motsIntention[w2]) continue;
        var tot = 0, meilleur = -1, meilleurN = 0;
        for (var pg in parMot[w2]) {
          tot += parMot[w2][pg];
          if (parMot[w2][pg] > meilleurN) { meilleurN = parMot[w2][pg]; meilleur = +pg; }
        }
        if (!tot || meilleurN / tot < 0.6 || estIndex[meilleur]) continue;
        // sujet traité par une page de bactérie -> vraie réponse
        // sujet seulement cité dans une page de médicament -> simple mention
        // Un mot qui n'apparaît que dans les sections de TRAITEMENT d'une
        // page est un médicament cité là, pas le sujet de la page :
        // « colistine » vit dans les Traitements de Proteus mirabilis, alors
        // qu'« anthrax » vit dans les Infections de Bacillus anthracis.
        var surtoutTx = (dansTx[w2] || 0) > 0 && (horsTx[w2] || 0) === 0;
        // Une page de CLASSE ou de generalites (« Les aminosides », « Les
        // resistances aux antibiotiques ») enumere : un mot cite dedans n'en
        // fait pas son sujet. Idee du moteur concurrent : seule une fiche
        // peut etre le sujet d'une reponse, le reste est une mention.
        var CLASSE = /^(les |l'|la |le )?(resistance|aminoside|carbapeneme|macrolide|quinolone|cephalosporine|penicilline|tetracycline|glycopeptide|lincosamide)/;
        var estClasse = CLASSE.test(normalise(ix.pages[meilleur].titre));
        if (estMedicament[meilleur] || surtoutTx || estClasse ||
            (!enProse[w2] && !enClinique[w2] && !enTitre[w2])) desFaible[w2] = meilleur;
        else des[w2] = meilleur;
      }
      this._estMedicament = estMedicament;
      this._designe = des;
      this._designeFaible = desFaible;
    }
    var designe = this._designe;

    // mots rares de la question (une espèce, pas un mot courant)
    var rares = [];
    for (var rw in poids) {
      if (poids[rw] >= 1 && !GENERIQUES[stem(rw)] && ix.df[rw] && ix.df[rw] <= 15) rares.push(rw);
    }
    // index « quels mots apparaissent dans le texte de chaque page »
    var motsPage = this._motsPage;
    if (!motsPage) {
      motsPage = this._motsPage = [];
      ix.sections.forEach(function (s) {
        var m = motsPage[s.page] || (motsPage[s.page] = {});
        for (var w in s.tf) m[w] = 1;
      });
    }

    // Où, dans la phrase, la page est-elle nommée ? (sert au départage)
    var motsBruts = lettreDeGroupe(normalise(question)).match(/[a-z0-9]+/g) || [];
    function position(trouves) {
      var meilleur = Infinity;
      for (var qi = 0; qi < motsBruts.length; qi++) {
        var ws = stem(motsBruts[qi]);
        if (trouves[ws]) { if (qi < meilleur) meilleur = qi; continue; }
        // mot du glossaire : « la gono » nomme Neisseria gonorrhoeae
        var vals = self.gloss[motsBruts[qi]];
        if (vals) {
          for (var vi = 0; vi < vals.length; vi++) {
            if (trouves[stem(vals[vi])] && qi < meilleur) meilleur = qi;
          }
        }
      }
      return meilleur;
    }

    // FAUTE DE FRAPPE, EN DERNIER RECOURS. On ne corrige un mot que si aucune
    // page ne le reconnaît tel quel, et seulement si la correction est UNIQUE :
    // deux noms de médicaments se ressemblent souvent à une lettre près, et
    // corriger au hasard serait pire que de ne pas répondre.
    if (!this._motsTousTitres) {
      var tt = [];
      ix.pages.forEach(function (p) {
        self.mots(p.titre).forEach(function (w) { if (tt.indexOf(w) < 0) tt.push(w); });
      });
      this._motsTousTitres = tt;
    }
    var tousTitres = this._motsTousTitres;
    for (var fw2 in poidsStem) {
      // un mot que le wiki écrit vraiment n'est pas une faute de frappe :
      // « pneumonie » ne doit jamais devenir « pneumoniae ».
      if (poidsStem[fw2] < 1 || fw2.length < 7) continue;
      if (ix.df[fw2] !== undefined || this.gloss[fw2]) continue;
      var reconnu = false, corrections = [];
      for (var ti3 = 0; ti3 < tousTitres.length; ti3++) {
        var tw3 = stem(tousTitres[ti3]);
        var cc = Math.min(fw2.length, tw3.length);
        if (fw2 === tw3 || memeNom(fw2, tw3) ||
            (cc >= 5 && fw2.length <= tw3.length + 2 && fw2.slice(0, cc) === tw3.slice(0, cc))) {
          reconnu = true; break;
        }
        if (uneFauteDeFrappe(fw2, tw3) && corrections.indexOf(tw3) < 0) corrections.push(tw3);
      }
      if (!reconnu && corrections.length === 1) poidsStem[corrections[0]] = 1;
    }

    var entite = [], entMots = [], pos = [], parTitre = [];
    for (var pi = 0; pi < ix.pages.length; pi++) {
      var mt = self.mots(ix.pages[pi].titre), e = 0, nbTitre = 0;
      var vusMot = {}, trouves = {};
      for (var mi = 0; mi < mt.length; mi++) {
        var mw = stem(mt[mi]);
        if (vusMot[mw]) continue;
        vusMot[mw] = 1;
        // correspondance directe, ou abréviation par troncature :
        // « actino » pour actinomyces, « azithro » pour azithromycine.
        var ok = poidsStem[mw] >= 0.6;
        if (!ok) {
          for (var qw in poidsStem) {
            // « azithro » pour azithromycine, mais aussi « ertapénème » écrit
            // en entier pour la page « Ertapénem » : la troncature peut être
            // d'un côté comme de l'autre.
            // Une abréviation est TOUJOURS plus courte que le nom complet.
            // Sans cette borne, « actinomycétome » et « actinomycetemcomitans »
            // passaient pour la page « Actinomyces sp ».
            var c0 = Math.min(qw.length, mw.length);
            if (c0 >= 5 && qw.length <= mw.length + 2 &&
                qw.slice(0, c0) === mw.slice(0, c0)) { ok = true; break; }
            if (memeNom(qw, mw)) { ok = true; break; }
          }
        }
        if (ok) {
          var dfp = ix.df[mt[mi]] || 1;
          e += Math.log(1 + (N - dfp + 0.5) / (dfp + 0.5));
          trouves[mw] = 1;
          if (!GENERIQUES[mw]) nbTitre++;
        }
      }
      // un mot du texte qui désigne cette page la nomme aussi
      // (« anthrax » -> Bacillus anthracis, « ancef » -> Cefazoline)
      for (var dw in poids) {
        if (poids[dw] >= 1 && designe[dw] === pi && !GENERIQUES[stem(dw)]) {
          var dfd = ix.df[dw] || 1;
          e += Math.log(1 + (N - dfd + 0.5) / (dfd + 0.5));
          trouves[dw] = 1;
        }
      }
      // Un titre long qui contient les mots cherchés PLUS beaucoup d'autres
      // décrit autre chose : « Staphylococcus aureus résistant à la
      // méticilline » n'est pas la réponse à « staph aureus ». On pondère
      // donc par la part du titre que la question couvre réellement.
      var totalMots = Object.keys(vusMot).length || 1;
      var nbTrouves = Object.keys(trouves).length;
      // Une page est « nommée » si un mot propre à elle est reconnu...
      var propre = false;
      for (var tw in trouves) { if (!GENERIQUES[tw]) { propre = true; break; } }
      // ...ou si la question reprend l'essentiel d'un titre fait de mots
      // courants : « c'est quoi les résistances aux antibiotiques » doit
      // trouver sa page, alors que le seul mot « antibiotique » ne suffit pas.
      if (!propre && nbTrouves >= 2 && nbTrouves / totalMots >= 0.6) propre = true;
      entite[pi] = propre ? e * (nbTrouves / totalMots) : 0;
      // « Staphylococcus epidermidis » : deux pages reconnaissent le genre,
      // mais une seule parle d'epidermidis dans son texte. C'est celle-là
      // qu'il faut. On favorise la page qui contient les mots rares de la
      // question que son titre ne porte pas.
      if (entite[pi] > 0 && rares.length) {
        var dansTexte = 0;
        for (var ri = 0; ri < rares.length; ri++) {
          if (!trouves[stem(rares[ri])] && motsPage[pi] && motsPage[pi][rares[ri]]) dansTexte++;
        }
        if (dansTexte) entite[pi] *= 1 + 0.9 * dansTexte;
      }
      entMots[pi] = trouves;
      parTitre[pi] = nbTitre > 0;
      pos[pi] = position(trouves);
    }
    // LE NOM L'EMPORTE SUR L'INDICE. Une page peut être « nommée » de deux
    // façons : par un mot de son titre, ou par un mot de son texte qui ne
    // vit que chez elle (« anthrax » -> Bacillus anthracis). Le second est
    // utile, mais c'est un indice, pas un nom : « parle-moi de la
    // ciprofloxacine » tombait sur la page Sérologies, parce que le mot
    // « parle » n'est écrit que là. Dès qu'une page est nommée par son
    // titre, les pages désignées par un simple indice s'effacent.
    var qqunParTitre = false;
    for (var pt2 = 0; pt2 < entite.length; pt2++) {
      if (entite[pt2] > 0 && parTitre[pt2]) { qqunParTitre = true; break; }
    }
    if (qqunParTitre) {
      for (var pt3 = 0; pt3 < entite.length; pt3++) {
        if (entite[pt3] > 0 && !parTitre[pt3]) entite[pt3] = 0;
      }
    }
    // LE SUJET EST NOMMÉ EN PREMIER. « Est-ce que la ceftriaxone couvre
    // l'entérocoque ? » parle de la ceftriaxone : c'est sa page qu'on veut
    // en tête, l'entérocoque n'est que le critère. Deux pages nommées, c'est
    // celle citée en premier qui est le sujet de la phrase.
    var premier = Infinity;
    for (var pp = 0; pp < entite.length; pp++) {
      if (entite[pp] > 0 && pos[pp] < premier) premier = pos[pp];
    }
    if (premier < Infinity) {
      for (var pq = 0; pq < entite.length; pq++) {
        if (entite[pq] > 0 && pos[pq] === premier) entite[pq] *= 2.6;
      }
    }
    var entiteMax = Math.max.apply(null, entite.concat([0]));
    this._entite = entite;
    this._entMots = entMots;

    for (var i = 0; i < ix.sections.length; i++) {
      var s = ix.sections[i], score = 0, touches = 0;
      for (var w in poids) {
        var f = s.tf[w];
        if (!f) continue;
        var df = ix.df[w] || 1;
        var idf = Math.log(1 + (N - df + 0.5) / (df + 0.5));
        score += poids[w] * idf * (f * (k1 + 1)) / (f + k1 * (1 - b + b * s.len / self.avgdl));
        touches++;
      }
      if (!score) continue;
      // bonus quand le titre de la page ou de la section correspond
      var tp = normalise(ix.pages[s.page].titre);
      var ts = normalise(s.titre);
      for (var w2 in poids) {
        if (poids[w2] < 1) continue;
        if (tp.indexOf(w2) >= 0) score += 2.2 * poids[w2];
        if (ts.indexOf(w2) >= 0) score += 1.5 * poids[w2];
      }
      // la section répond-elle à la sorte de question posée ?
      var bonusIntention = 0;
      for (var a = 0; a < inten.length; a++) {
        var titres = inten[a].titres;
        for (var c = 0; c < titres.length; c++) {
          if (ts.indexOf(titres[c]) >= 0) {
            bonusIntention = Math.max(bonusIntention, 1);
            break;
          }
        }
      }
      if (inten.length) {
        // multiplicatif : une section hors sujet garde son score, la bonne
        // section passe devant sans écraser la pertinence des mots.
        score *= bonusIntention ? 1.9 : 0.85;
      }
      // la page nommée par la question passe devant les autres
      if (entiteMax > 0 && entite[s.page] > 0) {
        score *= 1 + 1.6 * (entite[s.page] / entiteMax);
      }
      // et, parmi les pages nommées, celle que la question nomme en premier
      // est le sujet : « est-ce que le céfépime couvre le SARM ? » parle du
      // céfépime, le SARM n'est que le critère.
      if (premier < Infinity && pos[s.page] === premier) score *= 1.5;
      res.push({ i: i, score: score, touches: touches, ent: entite[s.page] || 0 });
    }
    res.sort(function (a, b) { return b.score - a.score; });
    var sortie = res.slice(0, nb || 8);

    // LA PAGE NOMMÉE DOIT TOUJOURS ÊTRE CANDIDATE. « Quel traitement pour
    // ciprof » : « ciprof » n'est écrit nulle part dans le wiki, donc la page
    // Ciprofloxacine n'a presque aucun score de mots et tombait hors de la
    // liste — l'assistant refusait alors que la page est clairement nommée.
    // On la réinjecte : c'est le classement qui doit suivre la reconnaissance
    // du nom, pas l'inverse.
    if (entiteMax > 0 && !sortie.some(function (h) { return h.ent > 0; })) {
      var repeche = [];
      for (var ri2 = 0; ri2 < ix.sections.length; ri2++) {
        var s2 = ix.sections[ri2];
        if (entite[s2.page] < entiteMax * 0.999) continue;
        if (!s2.prose.length && !s2.puces.length) continue;
        // on privilégie la section qui répond à la sorte de question posée
        var ts2 = normalise(s2.titre), bon2 = 0;
        for (var a2 = 0; a2 < inten.length; a2++) {
          var ti2 = inten[a2].titres;
          for (var c2 = 0; c2 < ti2.length; c2++) if (ts2.indexOf(ti2[c2]) >= 0) bon2 = 1;
        }
        repeche.push({ i: ri2, score: 1 + bon2, touches: 1, ent: entite[s2.page] });
      }
      repeche.sort(function (a, b) { return b.score - a.score; });
      if (repeche.length) sortie = repeche.slice(0, 3).concat(sortie).slice(0, nb || 8);
    }
    return sortie;
  };

  // Compose la réponse : toujours un texte, toujours du verbatim + source.
  Moteur.prototype._repondre = function (question) {
    var ix = this.ix;
    var hits = this.chercher(question, 10);
    var inten = this.intentions(question);

    if (!hits.length) {
      return {
        etat: "rien",
        intro: "Cette information n'est pas dans Microguide. Elle sera peut-être ajoutée " +
               "dans une version ultérieure.",
        blocs: [],
        suggestions: ix.pages.slice(0, 0)
      };
    }

    // RÈGLE 1 — le wiki n'a que des pages de bactéries et d'antibiotiques.
    // Si la question ne nomme aucune de ces pages (« quel antibiotique pour
    // une infection urinaire », « comment traiter une endocardite »), on ne
    // répond pas : le wiki n'a pas de page de pathologie, et deviner une
    // bactérie au hasard serait trompeur.
    // ESPÈCE NON COUVERTE. « Clostridium tetani » : le genre existe dans le
    // wiki (Clostridium perfringens), mais pas l'espèce. Répondre avec la
    // page du voisin serait une erreur grave. Règle étroite : un mot qui
    // suit immédiatement un genre connu et qui est TOTALEMENT absent du
    // wiki => espèce non couverte. (Étroite exprès : « parle » aussi est
    // absent du wiki, mais il ne suit aucun genre.)
    var moi = this;
    if (!this._motsTitres) {
      var mt2 = {}, listeT2 = [];
      ix.pages.forEach(function (p) {
        moi.mots(p.titre).forEach(function (w) {
          mt2[stem(w)] = 1;
          if (listeT2.indexOf(w) < 0) listeT2.push(w);
        });
      });
      this._motsTitres = mt2;
      this._listeTitres = listeT2;
    }

    // Le mot est-il un nom que le wiki porte dans un titre ? On accepte les
    // variantes d'écriture par troncature, comme le fait le classement :
    // « meropenem » pour « méropénème », « azithro » pour « azithromycine ».
    // Sans ça, « méropénem-vaborbactam » passerait pour un nom entièrement
    // inconnu alors que le wiki a bien la page du méropénème.
    function connuDesTitres(w) {
      if (moi._motsTitres[stem(w)]) return true;
      // le glossaire traduit aussi vers un titre : « kleb » -> Klebsiella
      var tr = moi.gloss[w];
      if (tr) {
        for (var ti = 0; ti < tr.length; ti++) {
          if (moi._motsTitres[stem(tr[ti])]) return true;
        }
      }
      var l = moi._listeTitres;
      for (var i2 = 0; i2 < l.length; i2++) {
        var court = Math.min(l[i2].length, w.length);
        if (court >= 6 && w.length <= l[i2].length + 2 &&
            l[i2].slice(0, court) === w.slice(0, court)) return true;
        // Une faute de frappe dans le genre ne doit pas faire dire au wiki
        // qu'il ne couvre pas le germe : « Staphylcoccus aureus » reste du
        // Staphylococcus aureus.
        if (uneFauteDeFrappe(w, l[i2]) || memeNom(w, l[i2])) return true;
      }
      return false;
    }

    // Mot qui décrit la QUESTION et non un nom : verbe, mot du glossaire,
    // déclencheur d'intention. « couvre », « ajuster », « donne » finissent
    // comme des noms de médicaments (« ceftolozane ») : seule une liste, pas
    // une terminaison, permet de les distinguer.
    if (!this._motsQuestion) {
      var mq = {};
      var defs3 = ix.intentions || {};
      for (var n3 in defs3) {
        (defs3[n3].declencheurs || []).forEach(function (d) {
          moi.mots(d).forEach(function (w) { mq[stem(w)] = 1; });
        });
      }
      // Seulement les mots d'ACTION du glossaire : « traiter », « couvrir »…
      // Les noms d'organismes qu'il contient (« listeria », « pneumocoque »)
      // sont au contraire des sujets, pas des mots de question.
      for (var cle3 in this.gloss) {
        var vals3 = this.gloss[cle3];
        var nom3 = vals3.some(function (v) { return moi._motsTitres[stem(v)]; });
        if (!nom3) moi.mots(cle3).forEach(function (w) { mq[stem(w)] = 1; });
      }
      this._motsQuestion = mq;
    }
    var VERBE_FR = /(ent|ons|ez|ais|ait|aient|era|eront|erait|ant|er|ir|oir|issent|ement)$/;
    function motDeQuestion(w) {
      // Un nom que le wiki porte dans un titre n'est jamais un mot de
      // question, même s'il finit comme un verbe : « citrobacter » et
      // « enterobacter » se terminent en -er comme « ajuster ».
      if (connuDesTitres(w) && !GENERIQUES[stem(w)]) return false;
      return moi._motsQuestion[stem(w)] || VERBE_FR.test(w) || moi.vides.has(w) ||
             GENERIQUES[stem(w)];
    }

    // Suite de mots TELLE QU'ÉCRITE, sans retirer les petits mots : c'est la
    // seule façon de distinguer « Mycoplasma pneumoniae » (deux mots collés,
    // un nom d'espèce) de « traiter un pseudomonas » (un article au milieu).
    var brut = lettreDeGroupe(normalise(question)).match(/[a-z0-9]+/g) || [];
    var suite = this.mots(question);

    // NOM COMPOSÉ ABSENT. « ceftazidime-avibactam », « méropénème-vaborbactam »,
    // « ampicilline-sulbactam » : le trait d'union lie deux molécules. Si le
    // wiki n'a que l'une des deux, la page trouvée n'est pas le médicament
    // demandé — et leurs spectres diffèrent justement par l'inhibiteur.
    var compose = normalise(question).match(/[a-z]{5,}-[a-z]{3,}/g) || [];
    for (var ci = 0; ci < compose.length; ci++) {
      var parts = compose[ci].split("-");
      if (motDeQuestion(parts[0]) || motDeQuestion(parts[1])) continue;
      if (parts[1].length < 5 && ix.df[parts[1]] !== undefined) continue;
      var c1 = connuDesTitres(parts[0]), c2 = connuDesTitres(parts[1]);
      if (c1 !== c2) {
        return {
          etat: "hors-sujet",
          intro: "Cette information n'est pas dans Microguide. Le wiki a une page sur l'une " +
                 "des deux molécules, mais pas sur cette association-là; elle sera peut-être " +
                 "ajoutée dans une version ultérieure.",
          blocs: []
        };
      }
    }

    // « LA PIPÉRACILLINE SEULE », « l'amoxicilline sans clavulanate ». Le wiki
    // n'a que la page de l'association; son spectre n'est pas celui de la
    // molécule seule, c'est justement l'inhibiteur qui fait la différence.
    if (/\b(seule?|sans)\b/.test(normalise(question))) {
      for (var si = 0; si < ix.pages.length; si++) {
        var tParts = normalise(ix.pages[si].titre).replace(/\s*\([^)]*\)/g, "").split("-");
        if (tParts.length !== 2) continue;
        var a1 = this.mots(tParts[0])[0], a2 = this.mots(tParts[1])[0];
        if (!a1 || !a2) continue;
        if (brut.indexOf(a1) >= 0 && brut.indexOf(a2) < 0) {
          return {
            etat: "hors-sujet",
            intro: "Cette information n'est pas dans Microguide. Le wiki documente « " +
                   ix.pages[si].titre + " », l'association; il ne donne pas le spectre de la " +
                   "molécule seule, qui est justement différent.",
            blocs: []
          };
        }
      }
    }

    // GENRE NON COUVERT. « Mycoplasma pneumoniae » : le wiki n'a pas de
    // Mycoplasma, mais le mot « pneumoniae » correspond au pneumocoque, et
    // l'assistant répondait avec la mauvaise bactérie. Règle : si le premier
    // mot d'un couple précis est inconnu des titres alors que le second y
    // figure, c'est un nom que le wiki ne couvre pas.
    function pageCite(pid, w) {
      return ix.sections.some(function (sx) {
        return sx.page === pid && moi.mots(sx.titre + " " + sx.prose.join(" ") + " " +
                                           sx.puces.join(" ")).indexOf(w) >= 0;
      });
    }
    for (var g = 0; g + 1 < brut.length; g++) {
      var g1 = brut[g], g2 = brut[g + 1];
      // Un verbe ou un mot courant ne designe pas un genre : « ajuster
      // penicilline » ne doit pas passer pour un nom d'espece inconnu.
      // Les DEUX mots doivent avoir l'allure latine d'un binôme (genre en
      // -a, -us, -um, -er, -es; espèce en -ae, -us, -a...). Questions Kaggle du
      // 28 oct. 2026 : « auxquels Serratia », « contre Legionella », « triade
      // pneumonie » passaient pour des genres inconnus, et l'assistant
      // refusait des questions qui nommaient en toutes lettres une page du wiki.
      if (g1.length >= 5 && g2.length >= 5 &&
          /(a|us|um|er|es)$/.test(g1) && /(ae|us|um|ii|is|i|a|ens|ans)$/.test(g2) &&
          !motDeQuestion(g1) && !motDeQuestion(g2) &&
          !this.gloss[g1] && !this.gloss[g1 + " " + g2] &&   // abréviation ou espèce connue
          // Un genre sans page mais que le wiki CITE (« Mycoplasma » : spectre
          // de la doxy et de l'azithro, culture respiratoire, PCR) n'est pas
          // inconnu : la recherche le trouvera en mention. 28 oct. 2026.
          // Seulement si la page trouvée en tête parle bien de ce genre :
          // sinon « Mycoplasma pneumoniae traitement » repartait vers le
          // pneumocoque, l'erreur grave que cette règle empêche.
          !(ix.df[g1] >= 2 && hits.length && pageCite(ix.sections[hits[0].i].page, g1)) &&
          !connuDesTitres(g1) && connuDesTitres(g2)) {
        return {
          etat: "hors-sujet",
          intro: "Cette information n'est pas dans Microguide. Le wiki ne couvre pas ce " +
                 "nom-là; il sera peut-être ajouté dans une version ultérieure.",
          blocs: []
        };
      }
    }

    // ORGANISME TOTALEMENT ABSENT. « Borrelia burgdorferi », « Burkholderia
    // cepacia », « Haemophilus ducreyi » : deux mots latins d'affilée dont
    // AUCUN n'existe dans le wiki. Sans cette règle, le classement se
    // rabattait sur la page qui leur ressemble le plus — la pire erreur
    // possible, puisqu'elle a l'air d'une vraie réponse.
    for (var h = 0; h + 1 < brut.length; h++) {
      var h1 = brut[h], h2 = brut[h + 1];
      if (h1.length >= 6 && h2.length >= 5 &&
          /(ae|us|um|ii|is|i|a|ens|ans)$/.test(h2) &&
          !motDeQuestion(h1) && !motDeQuestion(h2) &&
          !this.gloss[h1] && !this.gloss[h1 + " " + h2] &&
          ix.df[h1] === undefined && ix.df[h2] === undefined &&
          !connuDesTitres(h1) && !connuDesTitres(h2)) {
        return {
          etat: "hors-sujet",
          intro: "Cette information n'est pas dans Microguide. Le wiki ne contient aucune page " +
                 "sur ce germe-là; elle sera peut-être ajoutée dans une version ultérieure.",
          blocs: []
        };
      }
    }

    // Quels mots l'utilisateur a-t-il écrits avec une majuscule, ailleurs qu'en
    // début de phrase ? C'est ainsi qu'on écrit un nom propre : « Salmonella
    // Dublin », « Haemophilus Ducreyi ».
    var majusculeDansQuestion = {};
    (function () {
      var mots = (question || "").match(/[A-Za-z\u00C0-\u00ff'-]+/g) || [];
      for (var mi = 1; mi < mots.length; mi++) {
        if (/^[A-Z\u00C0-\u00de]/.test(mots[mi])) {
          moi.mots(mots[mi]).forEach(function (w) { majusculeDansQuestion[w] = 1; });
        }
      }
    })();

    var desForts = this._designe || {};
    for (var k = 0; k + 1 < brut.length; k++) {
      // Le second mot est-il juste cité au passage ailleurs ? « bovis » et
      // « sulfaméthoxazole » existent une fois dans le wiki, dans une puce
      // d'une AUTRE page : ça ne fait pas d'eux des sujets couverts.
      var citeAilleurs = ix.df[brut[k + 1]] !== undefined &&
                         (ix.df[brut[k + 1]] > 2 || desForts[brut[k + 1]] !== undefined);
      // Le second mot doit avoir l'allure d'un nom d'espèce latin
      // (burgdorferi, cepacia, suis, parainfluenzae) et non d'un verbe
      // français conjugué : « la listeria POUSSE au frigo », « le cefepime
      // ENTRE dans le SNC » ne nomment aucune espèce. C'est l'erreur que
      // j'ai déjà faite trois fois ici : une terminaison vague attrape les
      // verbes, une terminaison latine ne les attrape pas.
      var LATIN = /(ae|us|um|ii|is|i|a|ens|ans)$/;
      // « Streptococcus suis » : le nom d'espèce est ici un mot de quatre
      // lettres qui est aussi un verbe français courant. On l'accepte quand
      // il SUIT un genre connu et qu'il est absent du wiki — « pourquoi »,
      // trop long, ne passe pas par là.
      var petitNom = brut[k + 1].length === 4 && LATIN.test(brut[k + 1]) &&
                     ix.df[brut[k + 1]] === undefined;
      if (connuDesTitres(brut[k]) &&
          !motDeQuestion(brut[k]) &&
          !motDeQuestion(brut[k + 1]) &&
          // nom d'allure latine, OU mot écrit avec une majuscule au milieu de
          // la phrase et absent du wiki : « Salmonella Dublin » — un sérovar
          // que Thomas n'a pas documenté. La majuscule vient de la question
          // telle que l'utilisateur l'a tapée.
          (LATIN.test(brut[k + 1]) ||
           (majusculeDansQuestion[brut[k + 1]] && ix.df[brut[k + 1]] === undefined)) &&
          brut[k + 1].length >= 5 &&
          !citeAilleurs &&
          !this.gloss[brut[k + 1]] &&
          !connuDesTitres(brut[k + 1])) {
        return {
          etat: "hors-sujet",
          intro: "Cette information n'est pas dans Microguide. Le wiki couvre d'autres espèces " +
                 "de ce genre, mais pas celle-là; elle sera peut-être ajoutée dans une version " +
                 "ultérieure.",
          blocs: []
        };
      }
    }

    // La page citée doit être une page que la question nomme. Sinon on
    // écarte tout : répondre « pneumonie » avec une page d'antibiotique prise
    // au hasard serait trompeur.
    // Une question qui demande une LISTE (« quelles bactéries causent… »,
    // « quels antibiotiques donnent… ») doit donner une liste, même si une
    // page se trouve nommée au passage : sinon on répond avec une seule
    // bactérie là où le wiki en documente dix.
    // « quelles sont les indications de X » nomme une page : ce n'est pas une
    // question-liste. On exige un verbe d'effet, et pas « sont ».
    // On accepte le singulier comme le pluriel : beaucoup écrivent « quels
    // bacilles DONNE des infections urinaires ». Le verbe doit décrire un
    // effet, sinon « quelles sont les indications de X » deviendrait une liste.
    // Le sujet doit être une CATÉGORIE (bactéries, bacilles, antibiotiques,
    // gram…), pas une page précise : « quelles infections fait le staph
    // aureus » parle d'une seule page et doit rester une réponse de page.
    // « DDX D'UNE HÉMOCULTURE POSITIVE À GRAM NÉGATIF »
    // Un résident ne demande pas « quelles bactéries causent… », il demande
    // un diagnostic différentiel. C'est pourtant la même question, et le wiki
    // a la réponse : les pages Gram négatif qui parlent de bactériémie. La
    // forme changeait tout, pas le contenu.
    var veutDdx = /\b(ddx|dxx|diagnostic differentiel|diagnostics differentiels|dx differentiel|differentiel|differentiels)\b/
                    .test(normalise(question).replace(/[.'\u2019-]/g, " "));
    // « quels atb donnent… » ou « les atb qui donnent… » : la même question-liste
    var NOMS_LISTE = "(bacterie|bacteries|bacille|bacilles|germe|germes|microbe|microbes|pathogene|pathogenes|cocci|coque|coques|gram|antibiotique|antibiotiques|atb|atbs|abx|abxs|antibio|antibios|medicament|medicaments|molecule|molecules|lactamine|lactamines|penicilline|penicillines|cephalosporine|cephalosporines|carbapeneme|carbapenemes|quinolone|quinolones|fluoroquinolone|fluoroquinolones|glycopeptide|glycopeptides|aminoside|aminosides|macrolide|macrolides|tetracycline|tetracyclines|cycline|cyclines)", VERBES_LISTE = "(cause|causent|donne|donnent|fait|font|provoque|provoquent|entraine|entrainent|induit|induisent|occasionne|occasionnent|produit|produisent|couvre|couvrent|pousse|poussent|croit|croissent|se cultive|se cultivent|sont|est)";
    var nq = normalise(question);
    var veutListe = veutDdx ||
      new RegExp("\\b(quel|quels|quelle|quelles)\\b[^?]{0,20}\\b" + NOMS_LISTE + "\\b[^?]{0,45}\\b" + VERBES_LISTE + "\\b").test(nq) ||
      new RegExp("\\b" + NOMS_LISTE + "\\s+(qui|qu)\\b[^?]{0,20}\\b" + VERBES_LISTE + "\\b").test(nq);

    // Une page PORTAIL (index, classe, généralités) n'est le sujet de rien :
    // elle énumère. Idée reprise du moteur concurrent, qui règle d'un coup
    // « aztréonam → Les résistances » et « ostéomyélite → Antibiotiques ».
    var PORTAIL = /^(accueil|bacteries|antibiotiques|classification|infections|tests de laboratoires|calculateur|couverture des antibiotiques|template|demo|positif|bacilles gram)/;
    var estPortail = ix.pages.map(function (pg) { return PORTAIL.test(normalise(pg.titre)); });

    var nommes = hits.filter(function (h) {
      if (h.ent <= 0) return false;
      // un portail ne passe que si la question le nomme vraiment par son titre
      if (estPortail[ix.sections[h.i].page]) {
        var mt9 = moi.mots(ix.pages[ix.sections[h.i].page].titre).map(stem);
        var q9 = moi.mots(question).map(stem);
        // il faut que la question nomme le portail par un mot qui lui est
        // propre (« quinolone »), pas par un mot passe-partout
        // (« antibiotique ») qui désigne la moitié du wiki.
        var communs9 = mt9.filter(function (w) {
          return q9.indexOf(w) >= 0 && !GENERIQUES[w];
        });
        if (!communs9.length) return false;
      }
      return true;
    });
    // « Quels médicaments sont efficaces contre Listeria monocytogenes ? » a la
    // forme d'une question-liste, mais elle nomme UNE bactérie du wiki : c'est
    // la question de traitement de cette page, pas une liste de médicaments.
    // Questions Kaggle du 28 oct. 2026 : Listeria, Klebsiella, Legionella,
    // Nocardia et le SGA recevaient 4 à 8 pages d'antibiotiques à la place.
    // Seulement si la question parle de TRAITER : « quels antibiotiques
    // causent des pneumonies » reste une liste (« pneumonies » évoque le
    // pneumocoque sans le nommer).
    if (veutListe && !veutDdx &&
        /\bantibio|\batbs?\b|\babxs?\b|\bmedicament|\bmolecule/.test(nq) &&
        /\b(efficace|efficaces|trait|traiter|traitement|couvr|couvre|couvrent|sensible|resist|donner|choix)/.test(nq)) {
      var bactNommees = {};
      nommes.forEach(function (h) {
        var pg = ix.pages[ix.sections[h.i].page];
        if (pg.gram || pg.morpho) bactNommees[pg.id] = 1;
      });
      if (Object.keys(bactNommees).length === 1) veutListe = false;
    }
    if (!nommes.length || veutListe) {
      // ============================================================
      // RECHERCHE TRANSVERSALE
      // « Quels antibiotiques donnent des diarrhées ? », « quelles bactéries
      // causent une pneumonie ? », « quels gram négatifs font des
      // pneumonies ? » : aucune page n'a ce sujet pour titre, mais la réponse
      // est écrite dans une douzaine de pages. Refuser serait aussi faux que
      // de citer une page au hasard : on liste ce qui est écrit, page par page.
      // ============================================================
      var poidsQ = this.etendre(question);
      var nq2 = normalise(question);

      // ============================================================
      // QUESTION DE CLASSIFICATION
      // « Quelles sont les bactéries Gram négatif ? », « quels sont les cocci
      // Gram positif ? » : la question ne porte sur aucune infection, elle
      // demande la CATÉGORIE elle-même. La réponse est la liste des pages que
      // le wiki range dans cette catégorie, lue sur sa propre page
      // « Bactéries ». Aucun texte inventé : ce sont les titres de ses pages.
      // ============================================================
      var gramQ = /\bgram\s*(-|neg)/.test(nq2) ? "neg"
                : /\bgram\s*(\+|pos)/.test(nq2) ? "pos" : null;
      var morphQ = /\bcoccobacill/.test(nq2) ? "coccobacille"
                 : /\bcocci|\bcoque/.test(nq2) ? "cocci"
                 : /\bbacill/.test(nq2) ? "bacille" : null;
      // « quels sont les… » sans autre sujet : rien d'autre à chercher que la
      // catégorie. Un mot de contenu en plus (« pneumonie ») veut dire que la
      // question porte sur autre chose : c'est la recherche transversale.
      var restant = [];
      for (var cz in poidsQ) {
        var rz = stem(cz);
        if (poidsQ[cz] >= 1 && !GENERIQUES[rz] && !this.vides.has(cz) &&
            rz.length >= 4 &&
            !/^(gram|positi|negati|cocci|coque|bacill|coccobacill|sont|liste|lister|nomme|nommer|enumer|donne|donner|montre|dire|savoir|connai|existe|couvert|microguide|wiki)/.test(rz)) {
          restant.push(cz);
        }
      }
      if ((gramQ || morphQ) && !restant.length) {
        var membres = ix.pages.filter(function (pg) {
          if (gramQ && pg.gram !== gramQ) return false;
          if (morphQ && pg.morpho !== morphQ) return false;
          return !!pg.gram;         // seules les pages de bactéries sont classées
        });
        if (membres.length >= 2) {
          var pluriel = { cocci: "Cocci", bacille: "Bacilles", coccobacille: "Coccobacilles" };
          var nomCat = ((morphQ ? pluriel[morphQ] : "Bactéries") + " " +
                       (gramQ === "neg" ? "Gram négatif" : gramQ === "pos" ? "Gram positif" : "")).trim();
          var pageCat = null;
          for (var zc = 0; zc < ix.pages.length; zc++) {
            if (/^bacteries$/.test(normalise(ix.pages[zc].titre))) { pageCat = ix.pages[zc]; break; }
          }
          return {
            etat: "classification",
            intro: "Voici les " + membres.length + " pages que le wiki range dans cette " +
                   "catégorie. Le classement est celui de sa propre page « Bactéries » : " +
                   "je n'ai rien déduit moi-même. Chaque nom est le titre exact de sa page.",
            blocs: [{
              page: pageCat ? pageCat.titre : "Bactéries",
              url: pageCat ? pageCat.url : (ix.base || "") + "/index.php/Bact%C3%A9ries",
              images: [],
              sections: [{ titre: nomCat, prose: [],
                           puces: membres.map(function (pg) { return pg.titre; }) }]
            }]
          };
        }
      }

      // index par racine : « diarrhées » doit retrouver « diarrhée »
      if (!this._racines) {
        var rac = [], dfRac = {};
        ix.sections.forEach(function (sx, k) {
          var r = rac[k] = {}, vu = {};
          for (var w in sx.tf) {
            var rw = stem(w);
            r[rw] = (r[rw] || 0) + sx.tf[w];
            if (!vu[rw]) { dfRac[rw] = (dfRac[rw] || 0) + 1; vu[rw] = 1; }
          }
        });
        this._racines = rac; this._dfRacines = dfRac;
      }
      var racines = this._racines, dfRacines = this._dfRacines;

      // La recherche transversale ne sert QUE pour une question qui demande
      // explicitement une liste. Sinon « quel atb pour une endocardite »
      // listerait toutes les pages qui citent le mot, au lieu de refuser.
      var termesT = veutListe ? [] : null;
      if (termesT) {
      for (var cw in poidsQ) {
        var rc = stem(cw);
        // On accepte aussi les mots APPORTÉS PAR LE GLOSSAIRE (poids 0,6) :
        // « hémoculture positive » devient « bactériémie », qui est le mot
        // que le wiki emploie. Sans ça, la recherche cherchait un mot absent
        // du wiki et ne trouvait rien.
        if (poidsQ[cw] >= 0.6 && !GENERIQUES[rc] && !this.vides.has(cw) &&
            // un mot court mais rare compte aussi : « rash » (4 lettres) était
            // ignoré, et « quels atb donnent un rash » était refusé
            (rc.length >= 5 || (rc.length === 4 && dfRacines[rc] <= 12)) &&
            dfRacines[rc] && dfRacines[rc] <= 40 &&
            termesT.indexOf(rc) < 0) termesT.push(rc);
      }

      }
      if (termesT && termesT.length) {
        // La question vise les médicaments ou les bactéries ?
        var cible = /\bantibio|\batbs?\b|\babxs?\b|\bmedicament|\bmolecule|\blactam|\bquinolon|\bglycopeptid|\baminosid|\bmacrolid|\btetracyclin|\bcyclin|\blincosamid|\bpenicillin|\bcephalospor|\bcarbapenem/.test(nq2) ? "med"
                  : /\bbacterie|\bgerme|\bmicrobe|\bpathogene|\bbacille|\bcocci|\bgram/.test(nq2) ? "bact"
                  : null;
        var estMed = this._estMedicament || [];

        // « gram négatif » / « gram positif » : on ne garde que les pages dont
        // le wiki dit qu'elles sont de ce gram-là.
        var gram = /\bgram\s*(-|neg)/.test(nq2) ? "neg"
                 : /\bgram\s*(\+|pos)/.test(nq2) ? "pos" : null;
        // Le gram vient de la page « Bactéries », qui classe tout le wiki.
        var gramPage = [];
        if (gram) ix.pages.forEach(function (pg) { if (pg.gram) gramPage[pg.id] = pg.gram; });

        // Le wiki n'écrit le gram en toutes lettres que sur quelques pages
        // (ailleurs il est dans l'image d'en-tête). Si le filtre ne laisse
        // presque rien, on l'abandonne et on le dit, plutôt que de rendre une
        // liste tronquée qui aurait l'air complète.
        var gramUtilisable = false;
        if (gram) {
          var n5 = 0;
          gramPage.forEach(function (g5) { if (g5 === gram) n5++; });
          gramUtilisable = n5 >= 3;
        }
        var gramIgnore = !!gram && !gramUtilisable;

        // Morphologie demandée : cocci, bacille, coccobacille.
        var morph = /\bcoccobacill/.test(nq2) ? "coccobacille"
                  : /\bcocci|\bcoque/.test(nq2) ? "cocci"
                  : /\bbacill/.test(nq2) ? "bacille" : null;
        var morphoPage = [];
        if (morph) ix.pages.forEach(function (pg) { if (pg.morpho) morphoPage[pg.id] = pg.morpho; });
        var nM = 0;
        if (morph) morphoPage.forEach(function (v) { if (v === morph) nM++; });
        var morphUtilisable = nM >= 3;

        // Classe d'antibiotique demandee (page « Antibiotiques »).
        var cl = /\blactam|\bpenicillin|\bcephalospor|\bcarbapenem/.test(nq2) ? "beta-lactamine"
               : /\bquinolon/.test(nq2) ? "quinolone"
               : /\bglycopeptid/.test(nq2) ? "glycopeptide"
               : /\baminosid|\baminoglycosid/.test(nq2) ? "aminoside"
               : /\bmacrolid/.test(nq2) ? "macrolide"
               : /\btetracyclin|\bcyclin/.test(nq2) ? "tetracycline"
               : /\blincosamid/.test(nq2) ? "lincosamide" : null;
        var classePage = [];
        if (cl) ix.pages.forEach(function (pg) { if (pg.classe) classePage[pg.id] = pg.classe; });
        var nC = 0;
        if (cl) classePage.forEach(function (v) { if (v === cl) nC++; });
        var clUtilisable = nC >= 2;

        var meilleures = {}, meilleuresB = {}, aBonusT = false;
        ix.sections.forEach(function (sx, k) {
          // « antibiotique » = page liee depuis la page Antibiotiques, ou
          // page reconnue structurellement comme un medicament.
          var estATB = estMed[sx.page] || !!ix.pages[sx.page].antibio;
          if (cible === "med" && !estATB) return;
          if (cible === "bact" && estATB) return;
          if (gramUtilisable && gramPage[sx.page] !== gram) return;
          if (morphUtilisable && morphoPage[sx.page] !== morph) return;
          if (clUtilisable && classePage[sx.page] !== cl) return;
          var r4 = racines[k]; if (!r4) return;
          var trouves4 = termesT.filter(function (w) { return r4[w]; });
          if (!trouves4.length) return;
          var ts4 = normalise(sx.titre), bonus4 = 0;
          for (var a4 = 0; a4 < inten.length; a4++) {
            var ti4 = inten[a4].titres;
            for (var c4 = 0; c4 < ti4.length; c4++) if (ts4.indexOf(ti4[c4]) >= 0) bonus4 = 1;
          }
          if (bonus4) aBonusT = true;
          var occ4 = 0;
          trouves4.forEach(function (w) { occ4 += r4[w]; });
          var sc4 = trouves4.length * 3 + occ4 + bonus4 * 3;
          if (!meilleures[sx.page] || meilleures[sx.page].sc < sc4) {
            meilleures[sx.page] = { s: sx, sc: sc4, bonus: bonus4 };
          }
          // On retient à part la meilleure section qui répond vraiment à la
          // sorte de question posée : sinon une page perdait sa place parce
          // qu'une autre de ses sections citait le mot plus souvent.
          if (bonus4 && (!meilleuresB[sx.page] || meilleuresB[sx.page].sc < sc4)) {
            meilleuresB[sx.page] = { s: sx, sc: sc4, bonus: bonus4 };
          }
        });
        if (aBonusT) {
          Object.keys(meilleures).forEach(function (kk) {
            if (meilleuresB[kk]) meilleures[kk] = meilleuresB[kk];
            else delete meilleures[kk];
          });
        }

        var listeT = Object.keys(meilleures)
          .sort(function (x, y) { return meilleures[y].sc - meilleures[x].sc; })
          .slice(0, 8)
          .map(function (pid) {
            var page = ix.pages[pid], sx = meilleures[pid].s;
            var lignes = sx.prose.concat(sx.puces).filter(function (l) {
              var mm = moi.mots(l).map(stem);
              return termesT.some(function (w) { return mm.indexOf(w) >= 0; });
            }).slice(0, 3);
            if (!lignes.length) return null;
            return { page: page.titre, url: page.url, images: [],
                     sections: [{ titre: sx.titre, prose: lignes, puces: [] }] };
          }).filter(Boolean);

        if (listeT.length >= 2) {
          return {
            etat: "transversal",
            intro: "Aucune page du wiki ne porte sur ce sujet précis, mais il en est question " +
                   "sur " + listeT.length + " pages. Voici chaque passage, recopié tel quel. " +
                   "La liste se limite à ce que le wiki dit : elle n'est pas forcément complète." +
                   (gramIgnore ? " Le Gram n'a pas pu être départagé : la liste n'est pas " +
                    "filtrée par Gram." : ""),
            blocs: listeT
          };
        }
      }

      // SIMPLE MENTION. Le sujet n'a pas de page, mais il est cité quelque
      // part — « la doxycycline couvre les rickettsies ». On ne présente
      // surtout pas ça comme LE traitement : on montre la phrase exacte et on
      // dit franchement que le wiki ne traite pas le sujet.
      var faibles = this._designeFaible || {};
      var poidsQ = this.etendre(question);
      var citees = {}, termes = [];
      for (var fw in poidsQ) {
        if (poidsQ[fw] >= 1 && faibles[fw] !== undefined && !GENERIQUES[stem(fw)]) {
          citees[faibles[fw]] = 1;
          if (termes.indexOf(fw) < 0) termes.push(fw);
        }
      }
      var pagesCitees = Object.keys(citees).slice(0, 3).map(function (pid) {
        var page = ix.pages[pid];
        var phrases = [];
        ix.sections.forEach(function (s) {
          if (s.page !== +pid) return;
          s.prose.concat(s.puces).forEach(function (l) {
            var nl = normalise(l);
            for (var k = 0; k < termes.length; k++) {
              if (nl.indexOf(termes[k]) >= 0 && phrases.length < 2) { phrases.push(l); break; }
            }
          });
        });
        return { page: page.titre, url: page.url, images: [],
                 sections: phrases.length ? [{ titre: "Mention sur cette page",
                                               prose: phrases, puces: [] }] : [] };
      }).filter(function (b) { return b.sections.length; });

      if (pagesCitees.length) {
        // L'avertissement sur « le traitement complet » n'a de sens que pour
        // une bactérie ou un médicament. Sur une page de laboratoire, il est
        // absurde : une définition n'est pas un traitement.
        var toutLabo = pagesCitees.every(function (b) {
          for (var li = 0; li < ix.pages.length; li++) {
            if (ix.pages[li].titre === b.page) return !!ix.pages[li].labo;
          }
          return false;
        });
        return {
          etat: "mention",
          intro: toutLabo
            ? "Microguide n'a pas de page sur ce sujet précis. Il en est question en passant " +
              "sur la ou les pages ci-dessous; ce n'est donc pas un exposé complet."
            : "Microguide n'a pas de page sur ce sujet. Il est seulement mentionné en " +
              "passant sur la ou les pages ci-dessous. Ce n'est donc pas forcément le " +
              "traitement complet ni le traitement de choix : à valider ailleurs.",
          blocs: pagesCitees
        };
      }

      if (!nommes.length) {
        return {
          etat: "hors-sujet",
          intro: "Cette information n'est pas dans Microguide. Le wiki contient des pages de " +
                 "bactéries et des pages d'antibiotiques; elle sera peut-être ajoutée dans une " +
                 "version ultérieure.",
          blocs: []
        };
      }
      // question-liste sans résultat transversal : on répond normalement
    }
    hits = nommes;

    // RÈGLE 2 — ne pas répondre avec un organisme voisin. Une page n'est
    // gardée que si elle reconnaît quelque chose que la meilleure page ne
    // reconnaît pas : « Staphylococcus coagulase négative » ne reconnaît que
    // « staphylococcus », déjà reconnu par « Staphylococcus aureus », donc on
    // l'écarte. Mais pour « est-ce que la ceftriaxone couvre le pseudomonas »,
    // chaque page apporte son propre nom et les deux sont gardées.
    var entMots = this._entMots, entite = this._entite;
    var tete = entMots[ix.sections[hits[0].i].page] || {};
    hits = hits.filter(function (h) {
      var pid = ix.sections[h.i].page;
      if (pid === ix.sections[hits[0].i].page) return true;
      if (!entite[pid]) return false;                 // page non nommée
      var mots = entMots[pid] || {};
      for (var w in mots) { if (!tete[w]) return true; }   // apporte un nom neuf
      return false;
    });

    // on regroupe par page, en gardant l'ordre de pertinence
    var parPage = {}, ordre = [];
    hits.forEach(function (h) {
      var s = ix.sections[h.i];
      if (!parPage[s.page]) { parPage[s.page] = []; ordre.push(s.page); }
      parPage[s.page].push({ s: s, score: h.score });
    });

    var meilleur = hits[0].score;
    var blocs = [];
    // page principale : jusqu'à 3 sections ; puis 2 pages d'appoint, 1 section
    ordre.slice(0, 3).forEach(function (pid, rang) {
      var page = ix.pages[pid];
      var vuTitre = {};
      var retenues = parPage[pid]
        .filter(function (x) {
          // Une section sans texte (contenu uniquement en image) occuperait
          // une place à l'écran pour n'afficher qu'une boîte vide, et
          // chasserait une section utile. Son image est déjà montrée à part.
          if (!x.s.prose.length && !x.s.puces.length) return false;
          // jamais deux fois la même section
          if (vuTitre[x.s.titre]) return false;
          vuTitre[x.s.titre] = 1;
          return x.score >= meilleur * (rang === 0 ? 0.28 : 0.45);
        });
      var secs = retenues.slice(0, rang === 0 ? 3 : 1);
      // Si la question demande un traitement, la section de traitement doit
      // être montrée même si d'autres sections ont un meilleur score.
      if (rang === 0 && inten.length) {
        var colle = function (x) {
          var ts = normalise(x.s.titre);
          for (var a2 = 0; a2 < inten.length; a2++) {
            var ti = inten[a2].titres;
            for (var c2 = 0; c2 < ti.length; c2++) if (ts.indexOf(ti[c2]) >= 0) return true;
          }
          return false;
        };
        if (!secs.some(colle)) {
          for (var z = 0; z < retenues.length; z++) {
            if (colle(retenues[z])) { secs = [retenues[z]].concat(secs).slice(0, 3); break; }
          }
        }
        // Une question de traitement doit montrer la section « Traitements »
        // elle-même, pas seulement le tableau d'antibiogramme qui l'accompagne.
        var veutTx = inten.some(function (x) { return x.nom === "traitement"; });
        var estTx = function (x) { return /trait|efficac/.test(normalise(x.s.titre)); };
        if (veutTx && !secs.some(estTx)) {
          for (var z2 = 0; z2 < retenues.length; z2++) {
            if (estTx(retenues[z2])) { secs = [retenues[z2]].concat(secs).slice(0, 3); break; }
          }
        }
      }
      if (!secs.length) return;
      // Images à afficher : celles dont la section correspond à la sorte de
      // question posée (un tableau de couverture pour une question de
      // traitement), plus la fiche d'en-tête de la page quand c'est la page
      // principale. Les images résument souvent mieux que le texte.
      var titresRetenus = {};
      secs.forEach(function (x) { titresRetenus[normalise(x.s.titre)] = 1; });
      var images = (page.images || []).filter(function (im) {
        var ns = normalise(im.section);
        if (titresRetenus[ns]) return true;
        for (var a = 0; a < inten.length; a++) {
          var titres = inten[a].titres;
          for (var c = 0; c < titres.length; c++) {
            if (ns.indexOf(titres[c]) >= 0) return true;
          }
        }
        return rang === 0 && im.entete;
      }).slice(0, 3);

      blocs.push({
        page: page.titre,
        url: page.url,
        images: images,
        sections: secs.map(function (x) {
          return { titre: x.s.titre, prose: x.s.prose, puces: x.s.puces };
        })
      });
    });

    var faible = meilleur < 6;
    var p0 = blocs.length ? blocs[0].page : null;
    var intro;
    if (faible) {
      intro = "Je ne trouve pas de passage qui réponde clairement à cette question. Voici ce " +
              "qui s'en rapproche le plus, recopié tel quel. À vérifier : ce n'est peut-être " +
              "pas ce que vous cherchez.";
    } else {
      intro = "D'après " + (blocs.length > 1 ? "les pages du wiki" : "la page « " + p0 + " »") +
              ", voici ce qui est écrit sur microguide.ca, recopié mot pour mot :";
    }

    // DOSES DU CALCULATEUR. Quand on demande une posologie et que la section
    // Posologie de la page n'en donne pas (ou pas selon la fonction renale),
    // on complete avec les paliers du calculateur de doses du wiki. Ce sont
    // les valeurs ecrites par Thomas lui-meme, recopiees telles quelles.
    var veutDose = inten.some(function (x) { return x.nom === "posologie" || x.nom === "ajustement"; });
    if (veutDose && blocs.length) {
      var pgDose = null;
      for (var pdi = 0; pdi < ix.pages.length; pdi++) {
        if (ix.pages[pdi].titre === blocs[0].page) { pgDose = ix.pages[pdi]; break; }
      }
      if (pgDose && pgDose.doses && pgDose.doses.length) {
        var dejaDose = blocs[0].sections.some(function (s) {
          return /\d\s*(mg|g)\b/i.test(s.prose.concat(s.puces).join(" "));
        });
        var veutRenal = inten.some(function (x) { return x.nom === "ajustement"; });
        if (!dejaDose || veutRenal) {
          // On insère en tête, donc on parcourt à l'envers : la posologie
          // usuelle doit rester la première affichée, pas la variante.
          var listeD = pgDose.doses.slice().sort(function (a, b) {
            var ua = /usuel/i.test(a.nom) ? 0 : 1, ub = /usuel/i.test(b.nom) ? 0 : 1;
            return ua - ub;
          }).reverse();
          listeD.forEach(function (d) {
            var lignes = d.paliers.map(function (pal) {
              var plage = pal.max >= 999 ? "clairance \u2265 " + pal.min
                        : pal.min <= 0 ? "clairance < " + (pal.max + 1)
                        : "clairance " + pal.min + " \u00e0 " + pal.max;
              return plage + " mL/min : " + pal.label;
            });
            if (d.note) lignes.push(d.note);
            blocs[0].sections.unshift({
              titre: "Calculateur de doses \u2014 " + d.nom,
              prose: [], puces: lignes
            });
          });
          blocs[0].sections = blocs[0].sections.slice(0, 4);
        }
      }
    }

    // Un medicament present UNIQUEMENT dans le calculateur n'a pas de page :
    // on ne connait que sa dose. Il faut le dire, sinon une question de
    // spectre recevrait un tableau de doses sans explication.
    if (blocs.length) {
      var seulCalc = true;
      for (var sc = 0; sc < blocs.length; sc++) {
        var pgc = null;
        for (var sq = 0; sq < ix.pages.length; sq++) {
          if (ix.pages[sq].titre === blocs[sc].page) { pgc = ix.pages[sq]; break; }
        }
        if (!pgc || !pgc.calcNom) { seulCalc = false; break; }
      }
      if (seulCalc) {
        intro = "Microguide n'a pas de page sur ce m\u00e9dicament : seul son dosage figure " +
                "dans le calculateur de doses du wiki. Le voici, recopi\u00e9 tel quel. Pour le " +
                "spectre ou les indications, l'information n'est pas dans le wiki.";
        // Pour une question de dose, le calculateur EST la r\u00e9ponse. Pour toute
        // autre question (spectre, effets, interactions), le wiki ne r\u00e9pond pas :
        // montrer un tableau de doses laisserait croire le contraire.
        if (!veutDose) {
          return { etat: "mention", intro: intro, blocs: blocs };
        }
      }
    }

    // L'INFORMATION EST DANS UNE IMAGE. Si la section qui repond a la question
    // existe mais ne contient qu'un tableau en image, il faut le DIRE : sinon
    // l'utilisateur croit que le wiki ne couvre pas le sujet, alors que
    // l'information est la, sous ses yeux, dans la figure.
    if (blocs.length && inten.length) {
      var pgImg = null;
      for (var zi = 0; zi < ix.pages.length; zi++) {
        if (ix.pages[zi].titre === blocs[0].page) { pgImg = ix.pages[zi]; break; }
      }
      if (pgImg) {
        var titresAffiches = {};
        blocs[0].sections.forEach(function (s) { titresAffiches[normalise(s.titre)] = 1; });
        var muettes = [];
        ix.sections.forEach(function (s) {
          if (s.page !== pgImg.id) return;
          if (s.prose.length || s.puces.length) return;      // elle a du texte
          var ts7 = normalise(s.titre);
          if (titresAffiches[ts7]) return;
          for (var a7 = 0; a7 < inten.length; a7++) {
            var ti7 = inten[a7].titres;
            for (var c7 = 0; c7 < ti7.length; c7++) {
              if (ts7.indexOf(ti7[c7]) >= 0 && muettes.indexOf(s.titre) < 0) muettes.push(s.titre);
            }
          }
        });
        if (muettes.length) {
          blocs[0].sections.push({
            titre: "\u00c0 lire dans la figure",
            prose: ["Sur cette page, « " + muettes.join(" » et « ") + " » n'existe " +
                    "qu'en image : le wiki n'en donne aucun texte. L'information est donc " +
                    "uniquement dans le tableau affich\u00e9 ci-dessus, pas citable mot pour mot."],
            puces: []
          });
        }
      }
    }

    // ====================================================================
    // LA PAGE EXISTE, MAIS PAS CETTE INFORMATION (28 oct. 2026).
    // « dose de X » alors que la fiche de X n'a aucune section de posologie :
    // l'assistant recopiait une autre section de la page (spectre, perles),
    // comme si c'était la réponse. On le DIT : la bonne page est trouvée,
    // mais elle n'a pas de section sur ce qui est demandé. Les sections
    // restent affichées, le visiteur juge. Règle volontairement étroite :
    // seulement les intentions qui ont un titre de section propre, et seulement
    // si AUCUNE section de la page (même en image) ne porte ce titre.
    // Mesuré le 28 oct. 2026 : une première version (6 sortes d'information,
    // déclencheurs larges) se trompait souvent — les interactions sont écrites
    // dans les « Perles », « éviter » n'est pas une question de prévention.
    // D'où trois sortes seulement, des mots qui ne trompent pas, et une
    // vérification du TEXTE affiché, pas seulement des titres.
    var SECTION_PROPRE = {
      posologie:   { quoi: "la posologie",
                     demande: /\b(dose|doses|posologie|poso|dosage)\b/,
                     titre: /posologie|dose/,
                     texte: /\d\s*(mg|g|ui|mcg|µg)\b|mg\/kg/ },
      effets:      { quoi: "les effets indésirables",
                     demande: /\beffets? (secondaires?|indesirables?)\b/,
                     titre: /effet|indesirable|secondaire|toxicite/,
                     texte: /effet|indesirable|toxicit/ },
      interaction: { quoi: "les interactions",
                     demande: /\binteractions?\b/,
                     titre: /interaction/,
                     texte: /interaction|inr|warfarin|coumadin|cyp|qt\b/ }
    };
    if (blocs.length && !faible && !veutListe) {
      var pgSp = null;
      for (var qs = 0; qs < ix.pages.length; qs++) {
        if (ix.pages[qs].titre === blocs[0].page) { pgSp = ix.pages[qs]; break; }
      }
      var nqs = normalise(question);
      var affiche = normalise(blocs[0].sections.map(function (s) {
        return s.titre + " " + s.prose.join(" ") + " " + s.puces.join(" ");
      }).join(" "));
      var manque = [];
      if (pgSp) Object.keys(SECTION_PROPRE).forEach(function (k) {
        var sp = SECTION_PROPRE[k];
        if (!sp.demande.test(nqs)) return;
        if (sp.texte.test(affiche)) return;              // la réponse est affichée
        var aSection = ix.sections.some(function (sx) {   // même en image
          return sx.page === pgSp.id && sp.titre.test(normalise(sx.titre));
        });
        if (!aSection) manque.push(sp.quoi);
      });
      if (manque.length) {
        return {
          etat: "mention",
          intro: "La page « " + blocs[0].page + " » existe, mais elle ne donne pas " +
                 manque.join(" ni ") + ". Voici ce qu'elle contient de plus proche, recopié " +
                 "tel quel : ce n'est peut-être pas la réponse à votre question.",
          blocs: blocs
        };
      }
    }

    // ====================================================================
    // HORS DU PÉRIMÈTRE DU WIKI.
    // Microguide documente des bactéries et des antibiotiques. Il ne
    // documente pas les protocoles d'hygiène, les seuils de laboratoire, les
    // scores cliniques ni les cibles de dosage sérique. Une question de ce
    // genre nomme pourtant souvent une vraie page (« dépistage du SARM »,
    // « cible AUC de la vanco »), et l'assistant répondait avec elle : la
    // bonne page, mais pas la réponse — ce qui est pire qu'un refus.
    // Le filtre ne s'applique PAS si la page a justement une section sur ce
    // thème : le wiki parle bien du dépistage du strep B en grossesse.
    var HORS_PERIMETRE = [
      { quoi: "les protocoles d'hygiène et d'isolement",
        re: /\b(protocole|precaution|precautions|isolement|isoler|desinfect|lavage des mains|depistage a l admission|chambre individuelle)\b/ },
      { quoi: "les seuils et valeurs de laboratoire",
        re: /\b(valeur normale|valeurs normales|seuil|seuils|cible|cibles|creux|taux serique|dosage serique|auc|procalcitonine|crp)\b/ },
      { quoi: "les scores et critères cliniques",
        re: /\b(score|scores|criteres de severite|critere de severite|classification de|indication operatoire|chirurgical)\b/ }
    ];
    if (blocs.length && !veutListe) {
      var nqp = normalise(question).replace(/[.'\u2019-]/g, " ");
      for (var hp = 0; hp < HORS_PERIMETRE.length; hp++) {
        var trouve = nqp.match(HORS_PERIMETRE[hp].re);
        if (!trouve) continue;
        // on cherche sur la page LE terme employé, pas toute la famille :
        // la page de la vanco parle de « cible » pour autre chose que l'AUC.
        var termeHp = new RegExp("\\b" + trouve[0].replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "\\b");
        // la page en parle-t-elle quand même ? (titre de section ou texte)
        var pgHp = null;
        for (var qh = 0; qh < ix.pages.length; qh++) {
          if (ix.pages[qh].titre === blocs[0].page) { pgHp = ix.pages[qh]; break; }
        }
        var enParle = false;
        if (pgHp) {
          ix.sections.forEach(function (sx) {
            if (sx.page !== pgHp.id || enParle) return;
            var tout = normalise(sx.titre + " " + sx.prose.join(" ") + " " + sx.puces.join(" "))
                         .replace(/[.'\u2019-]/g, " ");
            if (termeHp.test(tout)) enParle = true;
          });
        }
        if (!enParle) {
          return {
            etat: "mention",
            intro: "Microguide ne couvre pas " + HORS_PERIMETRE[hp].quoi + " : c'est un guide " +
                   "des bactéries et des antibiotiques. La page ci-dessous porte sur le bon " +
                   "sujet, mais elle ne répond pas à cette question-là.",
            blocs: blocs
          };
        }
      }
    }

    return { etat: faible ? "faible" : "ok", intro: intro, blocs: blocs };
  };

  // ====================================================================
  // HÉSITATION → « Vous parliez plutôt de… ». Principe repris des modèles
  // de décision (Jev, TypeSafe AI) : un choix fermé porte une confiance, et
  // quand elle est basse on DEMANDE au lieu de deviner. Mesuré le 27 oct.
  // 2026 sur les jeux d'atelier et scellé : quand une autre page obtient au
  // moins 40 % du score de la meilleure, les réponses ne sont justes
  // qu'environ 2 fois sur 3, contre 9 sur 10 ailleurs. La réponse ne change
  // pas : on ajoute seulement, dans « autres », jusqu'à 2 pages proposées en
  // boutons. Sur un refus, ce sont les pages les plus proches, à ouvrir
  // « quand même » : le visiteur juge, le refus reste affiché.
  var SEUIL_HESITATION = 0.4;
  Moteur.prototype.repondre = function (question) {
    var r = this._repondre(question);
    r.autres = [];
    if (r.etat === "transversal" || r.etat === "classification") return r;
    var ix = this.ix, bruts = this.chercher(question, 10);
    if (!bruts.length) return r;
    var vus = {};
    r.blocs.forEach(function (b) { vus[b.page] = 1; });
    var s1 = bruts[0].score;
    for (var i = 0; i < bruts.length && r.autres.length < 2; i++) {
      var pg = ix.pages[ix.sections[bruts[i].i].page];
      if (vus[pg.titre]) continue;
      vus[pg.titre] = 1;
      if (bruts[i].score >= SEUIL_HESITATION * s1) r.autres.push(pg.titre);
    }
    return r;
  };

  // Le visiteur a choisi la page : on montre ce qu'ELLE dit sur la question,
  // ses sections les mieux classées (au plus 3), toujours recopiées telles
  // quelles. Sans section touchée par les mots de la question, ses premières
  // sections.
  Moteur.prototype.repondreSur = function (question, titre) {
    var ix = this.ix, page = null;
    for (var i = 0; i < ix.pages.length; i++) if (ix.pages[i].titre === titre) { page = ix.pages[i]; break; }
    if (!page) return { etat: "rien", intro: "Cette page n'existe plus dans le wiki.", blocs: [], autres: [] };
    var aTexte = function (s) { return s.prose.length || s.puces.length; };
    var secs = [], vu = {};
    this.chercher(question, ix.sections.length).forEach(function (h) {
      var s = ix.sections[h.i];
      if (s.page === page.id && aTexte(s) && !vu[s.titre] && secs.length < 3) { vu[s.titre] = 1; secs.push(s); }
    });
    if (!secs.length) {
      ix.sections.forEach(function (s) {
        if (s.page === page.id && aTexte(s) && !vu[s.titre] && secs.length < 2) { vu[s.titre] = 1; secs.push(s); }
      });
    }
    return {
      etat: "ok",
      intro: "D'après la page « " + page.titre + " », voici ce qui est écrit sur microguide.ca, recopié mot pour mot :",
      blocs: [{
        page: page.titre, url: page.url,
        images: (page.images || []).filter(function (im) { return im.entete; }).slice(0, 1),
        sections: secs.map(function (s) { return { titre: s.titre, prose: s.prose, puces: s.puces }; })
      }],
      autres: []
    };
  };

  // Rendu texte (pour les essais en ligne de commande)
  Moteur.prototype.texte = function (question) {
    var r = this.repondre(question);
    var out = ["Q: " + question, "", r.intro, ""];
    r.blocs.forEach(function (b) {
      out.push("── " + b.page + "  <" + b.url + ">");
      (b.images || []).forEach(function (im) {
        out.push("   [IMAGE] " + im.nom + "  (§ " + im.section + ")");
      });
      b.sections.forEach(function (s) {
        out.push("   § " + s.titre);
        s.prose.forEach(function (p) { out.push("     " + p); });
        s.puces.forEach(function (p) { out.push("     • " + p); });
      });
      out.push("");
    });
    return out.join("\n");
  };

  root.MicroguideMoteur = Moteur;
})(typeof module !== "undefined" ? module.exports : window);

/* ---------- 4/4 bulle ---------- */
// Bulle de l'assistant microguide : lanceur en bas à droite, visible sur
// toutes les pages du wiki. Tout tourne dans le navigateur du visiteur, à
// partir de l'API du wiki : aucun serveur, aucune clé. Seule exception, un
// signalement de mauvaise réponse est envoyé (si le visiteur clique) au relais
// Google Apps Script du compte Microguide, qui le transmet par courriel.

(function () {
  "use strict";

  if (window.__microguideAssistant) return;   // déjà chargé
  window.__microguideAssistant = true;
  // Numéro de version, écrit par assembler.sh. Il sert à vérifier CE QUI EST
  // EN LIGNE : deux fois cette semaine, une correction était faite sur le Mac
  // mais pas publiée, et je l'ai annoncée comme réglée. Plus jamais.
  window.MICROGUIDE_VERSION = "2026-10-28-1368b589";

  var BASE = (window.MICROGUIDE_BASE !== undefined)
    ? window.MICROGUIDE_BASE
    : (location.origin || "");

  var CSS = [
    '.mga-launch{position:fixed;z-index:9000;right:calc(18px + env(safe-area-inset-right,0px));',
    'bottom:calc(18px + env(safe-area-inset-bottom,0px));display:flex;align-items:center;gap:9px;',
    'background:#3f7490;color:#fff;border:none;font:600 14.5px/1 inherit;padding:13px 19px;',
    'border-radius:30px;cursor:pointer;box-shadow:0 6px 22px -6px rgba(20,40,55,.5)}',
    '.mga-launch:hover{filter:brightness(1.08)}',
    '.mga-launch svg{width:18px;height:18px;flex:0 0 auto}',
    '.mga-panel{position:fixed;z-index:9001;right:calc(18px + env(safe-area-inset-right,0px));',
    'bottom:calc(18px + env(safe-area-inset-bottom,0px));',
    'width:min(440px,calc(100vw - 32px));max-height:min(680px,calc(100vh - 36px));',
    // Pas de dvh : sur iOS, les unités liées au viewport dynamique forcent
    // Safari à recalculer la hauteur quand la barre d'adresse se rétracte, ce
    // qui déplaçait la barre d'outils fixe du thème Citizen en plein écran.
    // On reste sur des valeurs fixes, quitte à être un peu plus prudent.
    '.mga-panel{max-height:min(680px,calc(100vh - 36px))}',
    'background:#fff;color:#1c2228;border:1px solid #dcd8d0;border-radius:12px;',
    'box-shadow:0 18px 50px -14px rgba(15,30,42,.45);display:flex;flex-direction:column;',
    'overflow:hidden;font:400 15px/1.6 "IBM Plex Sans",system-ui,sans-serif}',
    '.mga-head{display:flex;align-items:center;gap:10px;padding:13px 15px;border-bottom:1px solid #dcd8d0}',
    '.mga-head .t{font-weight:600;font-size:14.5px;flex:1;min-width:0}',
    '.mga-ico{background:none;border:none;color:#8995a0;cursor:pointer;font-size:19px;line-height:1;padding:4px 6px;border-radius:5px}',
    '.mga-ico:hover{color:#1c2228;background:#f4f1ea}',
    '.mga-log{flex:1;overflow-y:auto;padding:15px;display:flex;flex-direction:column;gap:17px;min-height:0}',
    '.mga-marge{flex:0 0 auto}',
    '.mga-intro{font-size:13.5px;color:#55616b}',
    // Lien de signalement : discret, sous la réponse.
    '.mga-signal{display:block;margin:6px 0 2px;padding:0;border:none;background:none;',
    '  color:#8a949b;font:400 11.5px/1.4 inherit;cursor:pointer;text-decoration:underline;',
    '  text-underline-offset:2px}',
    '.mga-signal:hover{color:#5a646b}',
    '.mga-signal:disabled{color:#8a949b;cursor:default;text-decoration:none}',
    '.mga-signal[hidden],.mga-sigbox[hidden]{display:none}',
    // Bulle de signalement, ouverte par le lien ci-dessus.
    '.mga-sigbox{margin:8px 0 2px;padding:10px 12px;border-left:3px solid #8a949b;',
    '  background:#f4f1ea;border-radius:0 6px 6px 0;font-size:12.5px}',
    '.mga-sigbox p{margin:0 0 6px;color:#3c464e}',
    '.mga-sigbox textarea{display:block;width:100%;box-sizing:border-box;min-height:60px;',
    '  resize:vertical;font:inherit;padding:6px 8px;border:1px solid #c9c3b6;border-radius:4px;',
    '  background:#fff;color:#1c2228;margin:0 0 7px}',
    '.mga-sigrow{display:flex;align-items:center;gap:10px;flex-wrap:wrap}',
    '.mga-sigbox button{font:inherit;cursor:pointer;border-radius:4px;padding:5px 11px}',
    '.mga-sigenv{border:1px solid #3f7490;background:#3f7490;color:#fff}',
    '.mga-sigenv:disabled{opacity:.55;cursor:default}',
    '.mga-sigann{border:1px solid #c9c3b6;background:transparent;color:#3c464e}',
    '.mga-sigetat{color:#5a646b}',
    '.mga-sigetat.err{color:#b3261e}',
    // Encadré d'accueil : plus visible que l'ancien sous-titre gris,
    // il porte les deux phrases qui situent l'outil.
    '.mga-accueil{border-left:4px solid #3f7490;background:#e4eff6;border-radius:0 6px 6px 0;',
    '  padding:11px 14px;margin-bottom:4px;display:flex;flex-direction:column;gap:4px}',
    '.mga-accueil strong{color:#2b5b73;font-size:14px;font-weight:600;line-height:1.35}',
    '.mga-accueil span{color:#44525c;font-size:13.5px;line-height:1.45}',
    '.mga-intro p{margin:0 0 9px}',
    '.mga-samples{display:flex;flex-direction:column;gap:6px;margin-top:11px}',
    '.mga-samples button{text-align:left;background:none;border:1px solid #dcd8d0;color:#1c2228;',
    'font:400 13px/1.5 inherit;padding:8px 11px;border-radius:6px;cursor:pointer}',
    '.mga-samples button:hover{border-color:#3f7490;background:#e4eff6}',
    '.mga-q{align-self:flex-end;max-width:88%;background:#3f7490;color:#fff;padding:9px 13px;',
    'border-radius:12px 12px 3px 12px;font-size:14px}',
    '.mga-a{font-size:14px}',
    '.mga-lead{color:#55616b;margin:0 0 12px}',
    '.mga-lead.weak{background:#faf0dd;border-left:3px solid #8a5a1c;padding:9px 12px;color:#1c2228}',
    '.mga-src{border-top:1px solid #dcd8d0;padding-top:11px;margin-top:13px}',
    '.mga-src:first-of-type{border-top:none;padding-top:0;margin-top:0}',
    '.mga-srch{display:flex;align-items:baseline;gap:7px;flex-wrap:wrap;margin-bottom:8px}',
    '.mga-srch .lbl{font-family:"IBM Plex Mono",monospace;font-size:10px;letter-spacing:.1em;',
    'text-transform:uppercase;color:#8995a0}',
    '.mga-srch a{color:#3f7490;font-weight:600;font-size:14px;text-decoration:none;border-bottom:1px solid transparent}',
    '.mga-srch a:hover{border-bottom-color:#3f7490}',
    '.mga-sec{margin:0 0 11px}',
    // « Vous parliez plutôt de… » : boutons sobres, sous la réponse.
    '.mga-autres{display:flex;flex-wrap:wrap;align-items:baseline;gap:6px 8px;margin:12px 0 4px;',
    '  padding-top:10px;border-top:1px solid #dcd8d0}',
    '.mga-autres-t{flex:1 0 100%;font-size:12.5px;color:#55616b}',
    '.mga-autres button{background:none;border:1px solid #c9c3b6;color:#2b5b73;font:400 13px/1.4 inherit;',
    '  padding:5px 10px;border-radius:4px;cursor:pointer;text-align:left}',
    '.mga-autres button:hover{border-color:#3f7490;background:#e4eff6}',
    '.mga-autres button:disabled{cursor:default;color:#8a949b;background:none;border-color:#dcd8d0}',
    '.mga-autres button.choisi{color:#2b5b73;border-color:#3f7490;background:#e4eff6}',
    // « voir dans la page » : à droite du titre de section, en minuscules.
    '.mga-voir{margin-left:8px;font-family:"IBM Plex Sans",system-ui,sans-serif;font-size:11px;',
    '  letter-spacing:0;text-transform:none;color:#8995a0;text-decoration:underline;text-underline-offset:2px}',
    '.mga-voir:hover{color:#3f7490}',
    '.mga-sect{font-family:"IBM Plex Mono",monospace;font-size:10.5px;letter-spacing:.07em;',
    'text-transform:uppercase;color:#3f7490;margin-bottom:5px}',
    '.mga-quote{font-family:"IBM Plex Serif",Georgia,serif;font-size:14px;background:#f4f1ea;',
    'border-left:2px solid #dcd8d0;padding:9px 12px;color:#1c2228}',
    '.mga-quote p{margin:0 0 6px}.mga-quote p:last-child{margin-bottom:0}',
    // Une adresse longue ne se coupe pas toute seule et élargit la boîte :
    // sur un petit écran, ça faisait déborder toute la réponse.
    '.mga-quote,.mga-quote p,.mga-quote li{overflow-wrap:anywhere;word-break:break-word}',
    '.mga-quote a{overflow-wrap:anywhere}',
    '.mga-a,.mga-src,.mga-sec{min-width:0;max-width:100%}',
    '.mga-quote ul{margin:5px 0 0;padding-left:19px}.mga-quote li{margin:2px 0}',
    '.mga-fig{margin:0 0 11px}',
    '.mga-fig img{display:block;width:100%;height:auto;border:1px solid #dcd8d0;border-radius:4px;background:#fff}',
    '.mga-fig figcaption{font-family:"IBM Plex Mono",monospace;font-size:10.5px;color:#8995a0;margin-top:4px}',
    '.mga-none{background:#faf0dd;border-left:3px solid #8a5a1c;padding:11px 13px;font-size:13.5px}',
    '.mga-ask{border-top:1px solid #dcd8d0;padding:11px;display:flex;gap:8px}',
    '.mga-ask input{flex:1;min-width:0;background:#fbfaf7;color:#1c2228;border:1px solid #dcd8d0;',
    'border-radius:7px;padding:10px 12px;font-family:inherit;font-size:16px;line-height:1.4}',
    '.mga-ask input:focus{outline:2px solid #3f7490;outline-offset:-1px;border-color:#3f7490}',
    '.mga-ask button{background:#3f7490;color:#fff;border:none;border-radius:7px;padding:0 15px;',
    'font:600 14px/1 inherit;cursor:pointer}',
    '.mga-foot{padding:0 13px 11px;font-family:"IBM Plex Mono",monospace;font-size:11px;color:#8995a0}',
    '@media (max-width:460px){',
    // Plein écran utile sur téléphone, et saisie à 16px : en dessous, iOS
    // zoome automatiquement sur le champ et casse la mise en page.
    '  .mga-panel{right:max(8px,env(safe-area-inset-right,0px));',
    '    left:max(8px,env(safe-area-inset-left,0px));width:auto;',
    '    bottom:max(8px,env(safe-area-inset-bottom,0px));max-height:calc(100vh - 160px)}',
    '  .mga-ask input{font-size:16px}',
    // Sur téléphone, le thème Citizen affiche sa propre barre d'outils fixée
    // en bas (logo, loupe, menu). Le lanceur doit flotter AU-DESSUS, sinon il
    // la recouvre. On le réduit aussi à une pastille ronde pour ne pas barrer
    // l'écran : le texte n'est plus nécessaire, l'icône suffit.
    '  .mga-launch{right:max(12px,env(safe-area-inset-right,0px));',
    '    bottom:calc(72px + env(safe-area-inset-bottom,0px));',
    '    width:52px;height:52px;padding:0;border-radius:50%;justify-content:center;',
    '    font-size:0;gap:0}',
    '  .mga-launch svg{width:24px;height:24px}',
    '  .mga-panel{bottom:calc(72px + env(safe-area-inset-bottom,0px));',
    '    max-height:calc(100vh - 160px)}',
    '  .mga-fig img{max-width:100%}',
    '}',
    // Nos règles .mga-panel/.mga-launch ont une spécificité plus forte que la
    // règle [hidden] du navigateur : sans ceci, le panneau reste affiché même
    // quand il est marqué caché.
    '.mga-panel[hidden],.mga-launch[hidden]{display:none!important}'
  ].join("");

  var SAMPLES = [
    "Quels antibiotiques pour traiter un Pseudomonas ?",
    "Quelle est la couverture de la ceftriaxone ?",
    "Comment traite-t-on un C. difficile ?",
    "Comment traite-t-on un SARM ?",
    "Quelle est la posologie de la gentamicine ?"
  ];

  function el(tag, cls, txt) {
    var n = document.createElement(tag);
    if (cls) n.className = cls;
    if (txt != null) n.textContent = txt;
    return n;
  }

  var moteur = null, chargement = null, log, intro, champ, panel, launch;

  function construire() {
    var style = el("style");
    style.textContent = CSS;
    document.head.appendChild(style);

    launch = el("button", "mga-launch");
    launch.setAttribute("aria-label", "Ouvrir l'assistant");
    launch.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" ' +
      'stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 11.5a8.4 8.4 0 0 1-.9 ' +
      '3.8A8.5 8.5 0 0 1 12.5 20a8.4 8.4 0 0 1-3.8-.9L3 21l1.9-5.7A8.4 8.4 0 0 1 4 11.5a8.5 8.5 0 0 1 ' +
      '4.7-7.6A8.4 8.4 0 0 1 12.5 3h.5a8.5 8.5 0 0 1 8 8z"/></svg>';
    launch.appendChild(document.createTextNode("Demander au guide"));
    document.body.appendChild(launch);

    panel = el("section", "mga-panel");
    panel.hidden = true;
    panel.setAttribute("aria-label", "Assistant Microguide");

    var head = el("div", "mga-head");
    var t = el("div", "t", "Assistant Microguide");
    head.appendChild(t);
    var bReset = el("button", "mga-ico", "⟲");
    bReset.title = "Effacer";
    var bClose = el("button", "mga-ico", "✕");
    bClose.title = "Fermer";
    head.appendChild(bReset);
    head.appendChild(bClose);
    panel.appendChild(head);

    log = el("div", "mga-log");
    intro = el("div", "mga-intro");
    var cadre = el("div", "mga-accueil");
    cadre.appendChild(el("strong", null, "Répond uniquement avec le contenu du wiki"));
    cadre.appendChild(el("span", null, "Posez votre question en français, comme à l'oral."));
    intro.appendChild(cadre);

    var samples = el("div", "mga-samples");
    SAMPLES.forEach(function (q) {
      var b = el("button", null, q);
      b.addEventListener("click", function () { demander(q); });
      samples.appendChild(b);
    });
    intro.appendChild(samples);
    log.appendChild(intro);
    panel.appendChild(log);

    var form = el("form", "mga-ask");
    champ = el("input");
    champ.type = "text";
    champ.placeholder = "Votre question…";
    champ.autocomplete = "off";
    champ.setAttribute("aria-label", "Votre question");
    var env = el("button", null, "Chercher");
    env.type = "submit";
    form.appendChild(champ);
    form.appendChild(env);
    form.addEventListener("submit", function (e) {
      e.preventDefault();
      demander(champ.value);
    });
    panel.appendChild(form);

    var foot = el("div", "mga-foot", "Index reconstruit depuis le wiki");
    panel.appendChild(foot);
    document.body.appendChild(panel);

    // Le lanceur se place en bas à droite, mais le wiki y met déjà des choses :
    // la barre d'outils de Citizen sur téléphone, le bouton de connexion sur
    // ordinateur. Plutôt que de deviner une position, on regarde ce qui est
    // réellement à l'écran et on remonte juste assez pour ne rien recouvrir.
    function eviterLesObstacles() {
      launch.style.bottom = "";
      if (launch.hidden) return;
      var r = launch.getBoundingClientRect();
      if (!r.width) return;
      var basMax = 0;
      var tous = document.body.querySelectorAll("*");
      for (var i = 0; i < tous.length; i++) {
        var n = tous[i];
        if (n === launch || n === panel || panel.contains(n) || launch.contains(n)) continue;
        var st = getComputedStyle(n);
        if (st.position !== "fixed" && st.position !== "sticky") continue;
        if (st.visibility === "hidden" || st.display === "none" || st.opacity === "0") continue;
        var b = n.getBoundingClientRect();
        if (!b.width || !b.height || b.height > innerHeight * 0.6) continue;
        // chevauchement avec le lanceur ?
        if (b.right > r.left && b.left < r.right && b.bottom > r.top && b.top < r.bottom) {
          var hauteurDepuisLeBas = innerHeight - b.top;
          if (hauteurDepuisLeBas > basMax) basMax = hauteurDepuisLeBas;
        }
      }
      if (basMax > 0) {
        var marge = basMax + 14;
        launch.style.bottom = "calc(" + marge + "px + env(safe-area-inset-bottom,0px))";
        panel.style.bottom = "calc(" + marge + "px + env(safe-area-inset-bottom,0px))";
      } else {
        panel.style.bottom = "";
      }
    }
    setTimeout(eviterLesObstacles, 400);
    window.addEventListener("resize", eviterLesObstacles);
    window.addEventListener("orientationchange", function () { setTimeout(eviterLesObstacles, 300); });

    launch.addEventListener("click", ouvrir);
    bClose.addEventListener("click", fermer);
    bReset.addEventListener("click", function () {
      Array.prototype.slice.call(log.children).forEach(function (n) {
        if (n !== intro) n.remove();
      });
      intro.hidden = false;
      champ.value = "";
      champ.focus();
    });
    document.addEventListener("keydown", function (e) {
      if (e.key === "Escape" && !panel.hidden) fermer();
    });
    panel.__foot = foot;
  }

  function ouvrir() {
    panel.hidden = false;
    launch.hidden = true;
    precharger();
    setTimeout(function () { champ.focus(); }, 30);
  }
  function fermer() {
    panel.hidden = true;
    launch.hidden = false;
  }

  // L'index se bâtit au premier usage, pas au chargement de la page : la
  // navigation normale du wiki n'est donc jamais ralentie.
  function precharger() {
    if (moteur || chargement) return chargement;
    chargement = window.MicroguideIndexeur.charger(BASE).then(function (ix) {
      ix.glossaire = window.MICROGUIDE_GLOSSAIRE || {};
      ix.intentions = window.MICROGUIDE_INTENTIONS || {};
      moteur = new window.MicroguideMoteur(ix);
      if (panel.__foot) {
        panel.__foot.textContent = ix.pages.length + " pages · " + ix.sections.length + " sections";
      }
      return moteur;
    });
    return chargement;
  }

  // Transforme les adresses visibles en vrais liens cliquables, sans jamais
  // passer par innerHTML (le texte du wiki reste du texte, pas du code).
  function avecLiens(parent, texte) {
    var re = /https?:\/\/[^\s)\]]+/g, pos = 0, m;
    while ((m = re.exec(texte))) {
      if (m.index > pos) parent.appendChild(document.createTextNode(texte.slice(pos, m.index)));
      var a = document.createElement("a");
      a.href = m[0];
      a.textContent = m[0].length > 48 ? m[0].slice(0, 45) + "…" : m[0];
      a.target = "_blank";
      a.rel = "noopener";
      a.style.color = "#3f7490";
      parent.appendChild(a);
      pos = m.index + m[0].length;
    }
    if (pos < texte.length) parent.appendChild(document.createTextNode(texte.slice(pos)));
    return parent;
  }

  // Ancre MediaWiki d'une section (mode HTML5 : espaces → _, le reste tel
  // quel). Vérifiée contre l'API du wiki par ancres.js.
  function ancreSection(titre) {
    return String(titre).trim().replace(/\s+/g, "_");
  }

  // Lien vers la section dans la page, avec la phrase citée surlignée par le
  // navigateur (fragment de texte « #:~:text= », Chrome, Edge, Safari 16.1+,
  // Firefox 131+). Un navigateur qui ne le connaît pas va simplement à la
  // section. On prend les premiers mots de la première phrase : c'est assez
  // pour être unique, et assez court pour ne pas déborder sur un lien ou une
  // mise en forme qui casserait la correspondance.
  function lienSection(url, s, estPage, calc) {
    if (calc) return url;
    var phrase = (s.prose[0] || s.puces[0] || "").replace(/\s+/g, " ").trim();
    var mots = phrase.split(" ").slice(0, 8).join(" ").replace(/[\s,;:.!?»«()]+$/, "");
    var frag = estPage ? "" : encodeURIComponent(ancreSection(s.titre));
    if (mots) frag += ":~:text=" + encodeURIComponent(mots).replace(/-/g, "%2D");
    return url + (frag ? "#" + frag : "");
  }

  function rendreSection(s, url, estPage, calc) {
    var wrap = el("div", "mga-sec");
    var t = el("div", "mga-sect", s.titre);
    if (url) {
      var voir = el("a", "mga-voir", "voir dans la page");
      voir.href = lienSection(url, s, estPage, calc);
      t.appendChild(voir);
    }
    wrap.appendChild(t);
    var box = el("div", "mga-quote");
    s.prose.forEach(function (p) { box.appendChild(avecLiens(el("p"), p)); });
    if (s.puces.length) {
      var ul = el("ul");
      s.puces.forEach(function (p) { ul.appendChild(avecLiens(el("li"), p)); });
      box.appendChild(ul);
    }
    wrap.appendChild(box);
    return wrap;
  }

  // ----- SIGNALEMENT D'UNE MAUVAISE RÉPONSE -----
  // Les questions de Thomas et de ses collègues sont le seul jeu d'essai qui
  // ne vienne pas de ma tête : ce sont elles qui trouvent les vraies erreurs.
  // Depuis le 23 oct. 2026, un signalement est ENVOYÉ par courriel (question,
  // réponse donnée, commentaire facultatif) au relais Google Apps Script du
  // compte microguide.informations@gmail.com, rangé sous l'étiquette « IA ».
  // Il est aussi gardé dans le navigateur du visiteur, comme avant : taper
  // « signalements » dans la bulle, ou MICROGUIDE_SIGNALEMENTS() en console.
  var CLE_SIGNAL = "mga-signalements";
  // Même relais que les commentaires de pages (bloc « Commentaires Microguide »).
  // Seule adresse externe autorisée par securite.js.
  var RELAIS_SIGNALEMENT = "https://script.google.com/macros/s/AKfycbwWRFhsdOCjo89l83t07pD_2w4FCU0a1t4nXUx3dTcVYzffLumoRKc6uCxNmTrrxsVI/exec";

  // La réponse telle que le visiteur l'a vue, en texte brut, pour le courriel.
  function texteReponse(r) {
    var l = [r.intro || ""];
    (r.blocs || []).forEach(function (b) {
      l.push("", "Page : " + b.page + (b.url ? " (" + b.url + ")" : ""));
      (b.sections || []).forEach(function (s) {
        l.push("— " + s.titre);
        s.prose.forEach(function (p) { l.push(p); });
        s.puces.forEach(function (p) { l.push("• " + p); });
      });
    });
    return l.join("\n").slice(0, 6000);
  }

  function envoyerSignalement(question, r, commentaire) {
    var corps = JSON.stringify({
      type: "ia",
      question: question,
      reponse: texteReponse(r),
      etat: r.etat,
      pages: (r.blocs || []).map(function (b) { return b.page; }).join(", "),
      page: (window.mw && mw.config && mw.config.get("wgTitle")) || document.title,
      lien: location.href.split("#")[0],
      message: commentaire,
      site_web: ""
    });
    // text/plain évite la requête préalable CORS, que Apps Script ne gère pas.
    return fetch(RELAIS_SIGNALEMENT, { method: "POST", body: corps,
                                       headers: { "Content-Type": "text/plain;charset=utf-8" } })
      .then(function (rep) { return rep.json(); })
      .catch(function () {
        // Réponse illisible après la redirection de Google : renvoi en mode opaque.
        return fetch(RELAIS_SIGNALEMENT, { method: "POST", body: corps, mode: "no-cors" })
          .then(function () { return { ok: true }; });
      });
  }

  function signalements() {
    try { return JSON.parse(localStorage.getItem(CLE_SIGNAL) || "[]"); }
    catch (e) { return []; }
  }

  function signaler(question, r) {
    try {
      var l = signalements();
      l.push({
        question: question,
        date: new Date().toISOString().slice(0, 16),
        etat: r.etat,
        pages: (r.blocs || []).map(function (b) { return b.page; })
      });
      localStorage.setItem(CLE_SIGNAL, JSON.stringify(l.slice(-200)));
    } catch (e) {}
  }

  window.MICROGUIDE_SIGNALEMENTS = function () { return signalements(); };

  function rendreReponse(r, question) {
    var box = el("div", "mga-a");
    // une « mention » est un avertissement, pas une réponse : fond ambré
    box.appendChild(el("p", "mga-lead" +
      (r.etat === "faible" || r.etat === "mention" ? " weak" : ""), r.intro));
    if (!r.blocs.length) {
      box.appendChild(el("div", "mga-none",
        "Nommez une bactérie ou un antibiotique du wiki et je vous sortirai ce qui y est écrit."));
      proposer(box, question, r);
      box.appendChild(boutonSignaler(question, r));
      return box;
    }
    r.blocs.forEach(function (b) {
      var src = el("div", "mga-src");
      var h = el("div", "mga-srch");
      h.appendChild(el("span", "lbl", "Page"));
      var a = el("a", null, b.page);
      a.href = b.url;
      h.appendChild(a);
      src.appendChild(h);
      (b.images || []).forEach(function (im) {
        var fig = el("figure", "mga-fig");
        var img = document.createElement("img");
        // Une image ne peut venir que du wiki lui-meme. Si un jour l'API
        // renvoyait une adresse externe (page piegee, miroir, redirection),
        // on ne la charge pas : ca eviterait de faire fuiter la navigation
        // des lecteurs vers un tiers.
        var memeOrigine = false;
        try { memeOrigine = new URL(im.url, location.href).origin === location.origin; } catch (e) {}
        if (!memeOrigine) return;
        img.src = im.url;
        img.alt = im.section + " — " + b.page;
        img.loading = "lazy";
        fig.appendChild(img);
        fig.appendChild(el("figcaption", null, "Tableau du wiki — " + im.section));
        src.appendChild(fig);
      });
      // une section dont le contenu n'est qu'un tableau en image n'a rien à
      // écrire : l'image est déjà affichée plus haut, on saute la boîte vide
      b.sections.forEach(function (s) {
        if (!s.prose.length && !s.puces.length) return;
        // la section du calculateur de doses n'est pas un titre du wiki
        // (indexeur.js la fabrique) : on mène au haut de la page, sans phrase
        var calc = s.titre === "Posologie (calculateur de doses)";
        src.appendChild(rendreSection(s, b.url, s.titre === b.page, calc));
      });
      box.appendChild(src);
    });
    proposer(box, question, r);
    box.appendChild(boutonSignaler(question, r));
    return box;
  }

  // HÉSITATION : le moteur propose jusqu'à 2 autres pages (r.autres) quand il
  // n'est pas sûr de son choix. Un clic montre ce que CETTE page dit sur la
  // question, toujours recopié tel quel. Sur un refus, ce sont les pages les
  // plus proches, à ouvrir « quand même ».
  function proposer(box, question, r) {
    if (!r.autres || !r.autres.length) return;
    var refus = !r.blocs.length || r.etat === "hors-sujet";
    var w = el("div", "mga-autres");
    w.appendChild(el("span", "mga-autres-t", refus ? "Pages proches, à ouvrir quand même :"
                                                    : "Vous parliez plutôt de :"));
    r.autres.forEach(function (titre) {
      var b = el("button", null, titre);
      b.type = "button";
      b.addEventListener("click", function () {
        w.querySelectorAll("button").forEach(function (x) { x.disabled = true; });
        b.classList.add("choisi");
        precharger().then(function (m) {
          var rep = m.repondreSur(question, titre);
          var suite = rendreReponse(rep, question + " [page choisie : " + titre + "]");
          box.parentNode.insertBefore(suite, box.nextSibling);
          suite.scrollIntoView({ block: "start", behavior: "smooth" });
        });
      });
      w.appendChild(b);
    });
    box.appendChild(w);
  }

  // Un lien discret sous la réponse. Pas de pouce, pas de couleur : il ne doit
  // pas attirer l'œil, juste être là quand la réponse est mauvaise. Il ouvre
  // une petite bulle : commentaire facultatif, puis « Envoyer le signalement ».
  function boutonSignaler(question, r) {
    var wrap = el("div");
    var b = el("button", "mga-signal", "cette réponse est mauvaise");
    b.type = "button";
    wrap.appendChild(b);

    var boite = el("div", "mga-sigbox");
    boite.hidden = true;
    boite.appendChild(el("p", null, "Qu'est-ce qui ne va pas avec cette réponse ? (optionnel)"));
    var zone = document.createElement("textarea");
    zone.maxLength = 1500;
    zone.setAttribute("aria-label", "Qu'est-ce qui ne va pas avec cette réponse ? (optionnel)");
    boite.appendChild(zone);
    var ligne = el("div", "mga-sigrow");
    var envoi = el("button", "mga-sigenv", "Envoyer le signalement");
    envoi.type = "button";
    var annuler = el("button", "mga-sigann", "Annuler");
    annuler.type = "button";
    var etat = el("span", "mga-sigetat", "");
    ligne.appendChild(envoi);
    ligne.appendChild(annuler);
    ligne.appendChild(etat);
    boite.appendChild(ligne);
    wrap.appendChild(boite);

    b.addEventListener("click", function () {
      b.hidden = true;
      boite.hidden = false;
      zone.focus();
    });
    annuler.addEventListener("click", function () {
      boite.hidden = true;
      b.hidden = false;
    });
    envoi.addEventListener("click", function () {
      signaler(question, r);             // gardé dans ce navigateur, quoi qu'il arrive
      envoi.disabled = true;
      annuler.disabled = true;
      etat.className = "mga-sigetat";
      etat.textContent = "Envoi…";
      envoyerSignalement(question, r, zone.value.trim()).then(function (rep) {
        if (rep && rep.ok) {
          wrap.replaceChild(el("span", "mga-signal", "signalement envoyé, merci"), boite);
        } else {
          etat.className = "mga-sigetat err";
          etat.textContent = (rep && rep.erreur) || "L'envoi n'a pas fonctionné, réessaie plus tard.";
          envoi.disabled = false;
          annuler.disabled = false;
        }
      }).catch(function () {
        etat.className = "mga-sigetat err";
        etat.textContent = "L'envoi n'a pas fonctionné (connexion ?), réessaie plus tard.";
        envoi.disabled = false;
        annuler.disabled = false;
      });
    });
    return wrap;
  }

  // On amène le DÉBUT de la réponse en haut de la zone de lecture, pas sa
  // fin : sinon il faut remonter à chaque question pour lire la première
  // ligne. Une marge est ajoutée sous la dernière réponse pour que le début
  // puisse monter jusqu'en haut même quand la réponse est courte.
  function montrerDebut(bulleQuestion) {
    var marge = log.querySelector(".mga-marge");
    if (!marge) { marge = el("div", "mga-marge"); log.appendChild(marge); }
    else log.appendChild(marge);
    var manque = log.clientHeight - (log.scrollHeight - bulleQuestion.offsetTop + log.firstChild.offsetTop);
    marge.style.height = manque > 0 ? manque + "px" : "0px";
    log.scrollTop = bulleQuestion.offsetTop - log.offsetTop;
  }

  function demander(q) {
    q = (q || "").trim();
    if (!q) return;
    intro.hidden = true;

    // Taper « signalements » affiche les réponses marquées comme mauvaises.
    // Elles ne vivent que dans CE navigateur : sans cette commande, Thomas ne
    // peut pas les relire depuis son téléphone, où il n'a pas de console.
    if (/^signalement/i.test(q)) {
      var l = signalements();
      log.appendChild(el("div", "mga-q", q));
      var boite = el("div", "mga-a");
      boite.appendChild(el("p", "mga-lead",
        l.length ? "Voici les " + l.length + " réponse(s) que vous avez marquées comme mauvaises, " +
                   "dans ce navigateur-ci. Chacune a aussi été envoyée par courriel à l'équipe de Microguide." 
                 : "Aucun signalement dans ce navigateur. Ils sont enregistrés séparément sur " +
                   "chaque appareil : si vous avez cliqué sur votre téléphone, c'est là qu'ils sont."));
      if (l.length) {
        var ul = el("ul");
        l.slice().reverse().forEach(function (x) {
          ul.appendChild(el("li", null, x.date + " — « " + x.question + " » → " +
            (x.pages && x.pages.length ? x.pages.join(" + ") : "refus")));
        });
        var q2 = el("div", "mga-quote");
        q2.appendChild(ul);
        boite.appendChild(q2);
      }
      log.appendChild(boite);
      champ.value = "";
      return;
    }

    var bulle = el("div", "mga-q", q);
    log.appendChild(bulle);
    var attente = el("p", "mga-lead", "Je cherche dans le wiki…");
    log.appendChild(attente);
    montrerDebut(bulle);
    champ.value = "";

    precharger().then(function (m) {
      attente.remove();
      var rep = m.repondre(q);
      log.appendChild(rendreReponse(rep, q));
      montrerDebut(bulle);
    })["catch"](function (e) {
      attente.remove();
      log.appendChild(el("div", "mga-none",
        "Je n'ai pas pu lire le wiki (" + (e && e.message ? e.message : "erreur") +
        "). Réessayez dans un moment."));
    });
  }

  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", construire);
  } else {
    construire();
  }
})();

/* ====== FIN Assistant Microguide (essai) ====== */


/* ====== DEBUT Commentaires Microguide ======
   Formulaire « Un commentaire ou une question sur cette page ? » : un lien
   sous le titre de chaque page ouvre une petite bulle où écrire directement
   (le formulaire du bas de page a été retiré). Les messages passent par le
   relais Google Apps Script du compte microguide.informations@gmail.com, qui
   les envoie par courriel.
   Indépendant de l'assistant et de la loupe : supprimer ce bloc ne touche à
   rien d'autre. Aucun identifiant ici — l'adresse du relais n'est pas secrète.
   ============ */
(function () {
  "use strict";
  if (window.__microguideCommentaires) return;
  window.__microguideCommentaires = true;

  // Adresse « /exec » du déploiement Apps Script (Déployer > Application Web).
  var RELAIS = "https://script.google.com/macros/s/AKfycbwWRFhsdOCjo89l83t07pD_2w4FCU0a1t4nXUx3dTcVYzffLumoRKc6uCxNmTrrxsVI/exec";
  var DELAI_ENTRE_ENVOIS = 60 * 1000;   // un envoi par minute et par visiteur
  var MAX_MESSAGE = 3000;

  if (RELAIS.indexOf("https://script.google.com/") !== 0) return;
  if (!window.mw || !mw.config) return;
  // Pages de contenu seulement, en lecture (pas en modification, pas les pages spéciales).
  if (mw.config.get("wgNamespaceNumber") !== 0) return;
  if (mw.config.get("wgAction") !== "view") return;

  var CSS = [
    '.mgc{margin:2.5em 0 1em;padding-top:1em;',
    '  border-top:1px solid var(--border-color-base,#c4d6e4);font-size:.95em}',
    '.mgc-titre{margin:0 0 .25em;font-weight:600;',
    '  color:var(--color-progressive,#1f4e68)}',
    '.mgc-aide{margin:0 0 .75em;opacity:.75;font-size:.92em}',
    '.mgc textarea,.mgc input[type=email]{display:block;width:100%;box-sizing:border-box;',
    '  font:inherit;color:inherit;background:transparent;',
    '  border:1px solid var(--border-color-base,#c4d6e4);border-radius:2px;',
    '  padding:8px 10px;margin:0 0 .6em}',
    '.mgc textarea{min-height:90px;resize:vertical}',
    '.mgc textarea:focus,.mgc input[type=email]:focus{outline:2px solid #5b8fa8;outline-offset:0}',
    '.mgc-piege{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}',
    '.mgc-ligne{display:flex;align-items:center;gap:12px;flex-wrap:wrap}',
    '.mgc button[type=submit]{font:inherit;cursor:pointer;padding:7px 16px;border-radius:2px;',
    '  border:1px solid #1f4e68;background:#1f4e68;color:#fff}',
    '.mgc button[type=submit]:disabled{opacity:.55;cursor:default}',
    '.mgc-etat{font-size:.92em}',
    '.mgc-etat.ok{color:#2e7d4f}',
    '.mgc-etat.err{color:#b3261e}',
    // lien sous le titre et bulle qu'il ouvre
    '.mgc-haut{position:relative;margin:.35em 0 0;font-size:.88em}',
    '.mgc-lien{font:inherit;background:none;border:none;padding:0;cursor:pointer;',
    '  color:var(--color-progressive,#1f4e68);text-decoration:underline;',
    '  text-decoration-color:rgba(91,143,168,.5);text-underline-offset:3px}',
    '.mgc-lien:hover{text-decoration-color:currentColor}',
    '.mgc-bulle{position:absolute;left:0;top:calc(100% + 8px);z-index:900;',
    '  width:min(420px,calc(100vw - 32px));box-sizing:border-box;padding:14px 16px 12px;',
    '  background:var(--color-surface-1,#fff);color:var(--color-base,#202122);',
    '  border:1px solid var(--border-color-base,#c4d6e4);border-top:3px solid #5b8fa8;',
    '  box-shadow:0 6px 20px rgba(20,40,60,.14)}',
    '.mgc-bulle[hidden]{display:none}',
    '.mgc-bulle .mgc{margin:0;padding:0;border:none;font-size:1em}',
    '.mgc-fermer{position:absolute;top:6px;right:8px;width:28px;height:28px;padding:0;',
    '  font:400 20px/28px system-ui,sans-serif;background:none;border:none;cursor:pointer;',
    '  color:inherit;opacity:.6}',
    '.mgc-fermer:hover{opacity:1}'
  ].join("");

  function lireDernierEnvoi() {
    try { return Number(localStorage.getItem("microguide_commentaire_dernier") || 0); }
    catch (e) { return 0; }
  }
  function noterEnvoi() {
    try { localStorage.setItem("microguide_commentaire_dernier", String(Date.now())); }
    catch (e) { /* navigation privée : tant pis, le relais a son propre plafond */ }
  }

  function envoyer(donnees) {
    var corps = JSON.stringify(donnees);
    // text/plain évite la requête préalable CORS, que Apps Script ne gère pas.
    return fetch(RELAIS, { method: "POST", body: corps,
                           headers: { "Content-Type": "text/plain;charset=utf-8" } })
      .then(function (r) { return r.json(); })
      .catch(function () {
        // Certains navigateurs bloquent la lecture de la réponse après la
        // redirection de Google : on renvoie en mode opaque, sans réponse lisible.
        return fetch(RELAIS, { method: "POST", body: corps, mode: "no-cors" })
          .then(function () { return { ok: true }; });
      });
  }

  // Le formulaire affiché dans la bulle.
  function creerFormulaire(page, avecTitre) {
    var bloc = document.createElement("form");
    bloc.className = "mgc";
    bloc.setAttribute("novalidate", "");
    bloc.innerHTML =
      (avecTitre ? '<p class="mgc-titre">Un commentaire ou une question sur cette page ?</p>' : '') +
      '<p class="mgc-aide">Une erreur, une précision, une suggestion : écris-nous, ça arrive directement à l\'équipe de Microguide.</p>' +
      '<label class="mgc-piege" aria-hidden="true">Site web <input type="text" name="site_web" tabindex="-1" autocomplete="off"></label>' +
      '<textarea name="message" maxlength="' + MAX_MESSAGE + '" required aria-label="Ton commentaire" placeholder="Ton commentaire ou ta question…"></textarea>' +
      '<input type="email" name="courriel" maxlength="200" aria-label="Ton courriel (facultatif)" placeholder="Ton courriel (facultatif, si tu veux une réponse)">' +
      '<div class="mgc-ligne"><button type="submit">Envoyer</button>' +
      '<span class="mgc-etat" role="status" aria-live="polite"></span></div>';

    var etat = bloc.querySelector(".mgc-etat");
    var bouton = bloc.querySelector("button[type=submit]");
    function afficher(texte, classe) { etat.textContent = texte; etat.className = "mgc-etat " + (classe || ""); }

    bloc.addEventListener("submit", function (ev) {
      ev.preventDefault();
      var message = bloc.message.value.trim();
      if (!message) { afficher("Écris d'abord ton commentaire.", "err"); return; }
      var attente = DELAI_ENTRE_ENVOIS - (Date.now() - lireDernierEnvoi());
      if (attente > 0) {
        afficher("Merci ! Attends " + Math.ceil(attente / 1000) + " secondes avant d'envoyer un autre commentaire.", "err");
        return;
      }
      bouton.disabled = true;
      afficher("Envoi…");
      envoyer({
        page: page,
        lien: location.href.split("#")[0],
        message: message,
        courriel: bloc.courriel.value.trim(),
        site_web: bloc.site_web.value
      }).then(function (r) {
        if (r && r.ok) {
          noterEnvoi();
          bloc.message.value = "";
          afficher("Merci, ton commentaire a été envoyé.", "ok");
        } else {
          afficher((r && r.erreur) || "L'envoi n'a pas fonctionné. Réessaie plus tard.", "err");
        }
      }).catch(function () {
        afficher("L'envoi n'a pas fonctionné (connexion ?). Réessaie plus tard.", "err");
      }).then(function () { bouton.disabled = false; });
    });
    return bloc;
  }

  // Lien sous le titre, qui ouvre la bulle juste en dessous.
  function construireHaut(page) {
    var titre = document.querySelector(".citizen-page-heading") ||
                document.getElementById("firstHeading");
    if (!titre) return;

    var haut = document.createElement("div");
    haut.className = "mgc-haut";
    var lien = document.createElement("button");
    lien.type = "button";
    lien.className = "mgc-lien";
    lien.textContent = "Une erreur ou une question sur cette page ? Écris-nous";
    lien.setAttribute("aria-expanded", "false");

    var bulle = document.createElement("div");
    bulle.className = "mgc-bulle";
    bulle.hidden = true;
    bulle.setAttribute("role", "dialog");
    bulle.setAttribute("aria-label", "Commentaire sur cette page");
    var fermer = document.createElement("button");
    fermer.type = "button";
    fermer.className = "mgc-fermer";
    fermer.setAttribute("aria-label", "Fermer");
    fermer.textContent = "×";
    bulle.appendChild(fermer);
    bulle.appendChild(creerFormulaire(page, true));

    function ouvrir() {
      bulle.hidden = false;
      lien.setAttribute("aria-expanded", "true");
      bulle.querySelector("textarea").focus();
    }
    function refermer() {
      bulle.hidden = true;
      lien.setAttribute("aria-expanded", "false");
    }
    lien.addEventListener("click", function () { bulle.hidden ? ouvrir() : refermer(); });
    fermer.addEventListener("click", function () { refermer(); lien.focus(); });
    document.addEventListener("keydown", function (ev) {
      if (ev.key === "Escape" && !bulle.hidden) { refermer(); lien.focus(); }
    });
    // Un clic ailleurs referme la bulle, sauf si un commentaire est en cours d'écriture.
    document.addEventListener("click", function (ev) {
      if (bulle.hidden || haut.contains(ev.target)) return;
      if (bulle.querySelector("textarea").value.trim()) return;
      refermer();
    });

    haut.appendChild(lien);
    haut.appendChild(bulle);
    // Dans le bloc du titre de Citizen (et non à côté : l'en-tête est une rangée
    // flexible, un voisin se retrouverait à droite du titre).
    // Thème Vector (comptes connectés) : sous la barre du titre, elle aussi flexible.
    if (titre.id === "firstHeading") {
      var barre = titre.closest(".mw-body-header") || titre;
      barre.parentNode.insertBefore(haut, barre.nextSibling);
    } else titre.appendChild(haut);
  }

  function construire() {
    var contenu = document.getElementById("mw-content-text");
    if (!contenu || document.querySelector(".mgc-haut")) return;

    var style = document.createElement("style");
    style.textContent = CSS;
    document.head.appendChild(style);

    var page = mw.config.get("wgTitle") || document.title;
    construireHaut(page);
  }

  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", construire);
  } else {
    construire();
  }
})();
/* ====== FIN Commentaires Microguide ====== */

/* DEBUT Spectre antibiotique */
/* Sous chaque image faite avec les générateurs du Mac (« Spectre - <antibiotique>.jpg » ou
   « Fiche - <bactérie>.jpg »), ajoute un bouton « Télécharger l'image » qui enregistre le fichier en pleine taille.
   Simple lien de téléchargement : fonctionne aussi sur Safari et iPhone. */
(function () {
	var MOTIF = /^(.*?)\/(?:thumb\/)?([0-9a-f]\/[0-9a-f]{2})\/((?:Spectre|Fiche)_-_[^\/]+?\.(?:png|jpe?g))(?:\/[^\/]+)?$/i;

	mw.hook('wikipage.content').add(function ($contenu) {
		$contenu.find('img').each(function () {
			var img = this;
			if (img.getAttribute('data-mg-spectre')) return;
			var u;
			try { u = new URL(img.getAttribute('src'), location.href); } catch (e) { return; }
			if (u.origin !== location.origin) return;
			var m = u.pathname.match(MOTIF);
			if (!m) return;
			img.setAttribute('data-mg-spectre', '1');

			var fichier = decodeURIComponent(m[3]);
			var a = document.createElement('a');
			a.href = m[1] + '/' + m[2] + '/' + m[3];
			a.download = fichier.replace(/_/g, ' ');
			a.textContent = 'Télécharger l’image';
			a.style.cssText = 'display:inline-block;margin:8px 0 4px;padding:6px 14px;font-size:14px;background:#34566e;color:#fff;border-radius:3px;text-decoration:none';
			var bloc = img.closest('figure, .thumb, span[typeof], .mw-default-size') || img.parentNode;
			var p = document.createElement('div');
			p.appendChild(a);
			bloc.parentNode.insertBefore(p, bloc.nextSibling);
		});
	});
}());
/* FIN Spectre antibiotique */