CODE SOURCE
Calculateur de pourcentage : les sources
Une page indépendante, une logique séparée et les composants communs du site.
- src/pages/pourcentage.mjs
- src/assets/pourcentage.mjs
- src/assets/percentage-calcul.mjs
- src/shared/layout.mjs
- src/shared/support.mjs
- src/assets/style.css
- src/assets/calculator.css
- src/assets/conversion.css
- src/assets/percentage.css
- src/assets/image-compress.css
- src/assets/ad-layout.css
- src/assets/ads.mjs
- src/assets/webmcp.mjs
- site.config.mjs
src/pages/pourcentage.mjs
export default {route:'/outils/calculateur-pourcentage',active:'pourcentage',script:'pourcentage.mjs',title:'Calculateur de pourcentage gratuit en ligne',description:'Calculez une part en pourcentage, une remise, une hausse, une diminution ou une évolution entre deux valeurs. Formule expliquée, sans inscription.',body:`<div class="page-heading"><a class="back" href="/">Tous les outils</a><div class="eyebrow">CALCUL RAPIDE</div><h1>Calculateur de pourcentage</h1><p class="intro">Calculez facilement un pourcentage, une remise, une hausse ou une évolution, gratuitement et sans inscription.</p><p class="assurance">Calcul instantané <span>·</span> Résultat expliqué <span>·</span> Traitement local</p></div><div class="percentage-layout"><section class="workspace percentage-tool"><div class="mode-picker percent-modes" role="tablist" aria-label="Type de calcul"><label><input type="radio" name="percent-mode" value="value" checked><span>Pourcentage d’une valeur<small>Ex. : 15 % de 80 = 12</small></span></label><label><input type="radio" name="percent-mode" value="part"><span>Part en pourcentage<small>Ex. : 25 sur 200 = 12,5 %</small></span></label><label><input type="radio" name="percent-mode" value="change"><span>Augmentation ou diminution<small>Ex. : 100 augmenté de 20 % = 120</small></span></label><label><input type="radio" name="percent-mode" value="evolution"><span>Évolution entre deux valeurs<small>Ex. : de 80 à 100 = +25 %</small></span></label></div><form id="percent-form" novalidate><div id="percent-fields"></div><div class="rounding"><label for="rounding-select">Décimales affichées</label><select id="rounding-select"><option value="auto">Automatique</option><option value="0">0</option><option value="1">1</option><option value="2" selected>2</option><option value="4">4</option></select><p class="muted small">L’arrondi concerne uniquement l’affichage, jamais la valeur mathématique.</p></div><p id="percent-error" role="alert" hidden></p><div class="actions"><button class="primary" type="submit">Calculer</button><button class="secondary" type="reset">Effacer</button></div></form></section><section class="percent-result" aria-live="polite"><div class="eyebrow">RÉSULTAT</div><h2 id="percent-result-title">Votre résultat</h2><p id="percent-empty" class="muted">Saisissez vos valeurs pour voir le calcul.</p><div id="percent-values" hidden><div class="percent-main"><span id="result-label">Résultat</span><strong id="result-number"></strong></div><dl class="formula"><div><dt>Formule</dt><dd id="formula-text"></dd></div><div><dt>Application</dt><dd id="application-text"></dd></div></dl><p id="change-difference" class="notice" hidden></p><div class="actions"><button id="copy-result" class="secondary" type="button">Copier le résultat</button><button id="copy-detail" class="secondary" type="button">Copier le détail</button></div><p id="copy-status" role="status" class="small"></p></div><p class="local-note">Vos valeurs sont calculées dans votre navigateur et ne sont pas envoyées sur nos serveurs.</p></section></div><aside class="ad-space" data-ad-tool="percentage" hidden aria-label="Publicité"><span>Publicité</span></aside><section class="guide percentage-guide"><div class="eyebrow">MODE D’EMPLOI</div><h2>Un calcul expliqué en trois étapes</h2><ol class="steps"><li><h3>Choisissez le cas</h3><p>Pourcentage d’une valeur, part en pourcentage, variation ou évolution.</p></li><li><h3>Saisissez les nombres</h3><p>Les virgules, points et espaces de milliers sont acceptés.</p></li><li><h3>Lisez et copiez</h3><p>La formule, son application et le résultat arrondi sont affichés ensemble.</p></li></ol><h2 class="explanation-title">Les quatre calculs courants</h2><p>Pour calculer une part, multipliez une valeur de référence par le taux puis divisez par 100. Pour exprimer une part dans un total, divisez la part par le total et multipliez par 100. Une augmentation ajoute le taux à la valeur initiale ; une diminution le soustrait. L’évolution compare l’écart entre une valeur nouvelle et une valeur ancienne à cette ancienne valeur. Le signe indique le sens de l’évolution.</p><p>L’outil conserve les calculs intermédiaires avec la précision JavaScript disponible et n’arrondit que l’affichage. Une diminution supérieure à 100 % peut donc produire une valeur négative : elle est expliquée au lieu d’être masquée. Les valeurs négatives sont permises lorsqu’elles ont un sens mathématique, tandis que les formats ambigus et les nombres non finis sont refusés.</p></section><section class="faq"><h2>Questions fréquentes</h2><details><summary>Comment calculer 20 % d’une somme ?</summary><p>Multipliez la somme par 20 puis divisez par 100. Par exemple, 20 % de 150 = 150 × 20 ÷ 100 = 30.</p></details><details><summary>Quelle différence entre augmentation et évolution ?</summary><p>L’augmentation applique un taux à une valeur initiale pour obtenir une nouvelle valeur. L’évolution compare deux valeurs connues : (nouvelle − ancienne) ÷ ancienne × 100.</p></details><details><summary>Comment calculer une remise ?</summary><p>Utilisez le mode Pourcentage d’une valeur pour calculer le montant de la remise, ou le mode Diminution pour obtenir directement le prix après remise.</p></details><details><summary>Pourquoi un total nul est-il impossible ?</summary><p>Une part ne peut pas être rapportée à zéro : la division n’est pas définie. L’outil refuse ce cas et conserve vos autres valeurs.</p></details><details><summary>Les calculs sont-ils envoyés sur un serveur ?</summary><p>Non. Les valeurs, formules et résultats restent dans votre navigateur. Aucun historique automatique n’est conservé et les annonces en ligne sont gérées par Google selon vos choix de confidentialité.</p></details></section><div class="source-link"><a href="/outils/calculateur-pourcentage/source">Consulter le code source du calculateur</a></div>`};
src/assets/pourcentage.mjs
import {parseNumber,calculate,formatNumber} from './percentage-calcul.mjs';
import {registerSelectionTool} from './webmcp.mjs';
const $=id=>document.getElementById(id);let mode='value',last=null,previous=null;
const defs={value:[['rate','Pourcentage','%','Ex. : 15'],['value','Valeur de référence','', 'Ex. : 80']],part:[['part','Part','', 'Ex. : 25'],['total','Total','', 'Ex. : 200']],change:[['initial','Valeur initiale','', 'Ex. : 100'],['rate','Taux','%', 'Ex. : 20']],evolution:[['old','Valeur ancienne','', 'Ex. : 80'],['current','Valeur nouvelle','', 'Ex. : 100']]};
function field(id,label,unit,placeholder){return `<div class="calc-field"><label for="percent-${id}">${label}</label><div class="unit-input"><input id="percent-${id}" type="text" inputmode="decimal" placeholder="${placeholder}" aria-describedby="percent-${id}-error"><span>${unit}</span></div><p class="field-error" id="percent-${id}-error" hidden></p></div>`}
function renderFields(){let html=defs[mode].map(x=>field(...x)).join('');if(mode==='change')html+=`<fieldset class="direction"><legend>Action</legend><label><input type="radio" name="direction" value="increase" checked> Augmenter</label><label><input type="radio" name="direction" value="decrease"> Diminuer</label></fieldset>`;$('percent-fields').innerHTML=html;$('percent-error').hidden=true}
function getValues(){const values={},errors={};for(const [id]of defs[mode]){const r=parseNumber($('percent-'+id).value);if(r.error)errors[id]=r.error;else values[id]=r.value}if(mode==='change')values.direction=document.querySelector('input[name=direction]:checked').value;return {values,errors}}
function show(){const {values,errors}=getValues();for(const [id,message]of Object.entries(errors)){$('percent-'+id).setAttribute('aria-invalid','true');$('percent-'+id+'-error').textContent=message;$('percent-'+id+'-error').hidden=false}$('percent-error').hidden=!Object.keys(errors).length;if(Object.keys(errors).length){$('percent-error').textContent='Corrigez les champs indiqués.';return}try{const result=calculate(mode,values);previous=last;last={mode,values,result};const dec=$('rounding-select').value;$('percent-empty').hidden=true;$('percent-values').hidden=false;$('result-number').textContent=`${result.result>=0&&mode==='evolution'?'+':''}${formatNumber(result.result,dec)}${result.unit==='%'?' %':' €'}`;$('result-label').textContent=mode==='part'||mode==='evolution'?'Pourcentage calculé':mode==='change'?'Valeur finale':'Résultat';$('formula-text').textContent=result.formula;$('application-text').textContent=`${result.application} = ${formatNumber(result.result,dec)}${result.unit==='%'?' %':' €'}`;$('change-difference').hidden=mode!=='change';if(mode==='change'){$('change-difference').textContent=`Écart par rapport à la valeur initiale : ${formatNumber(result.result-values.initial,dec)} €.`;if(result.result<0)$('change-difference').textContent+=' Le taux dépasse 100 % : le résultat devient négatif.'}$('copy-result').disabled=false;$('copy-detail').disabled=false}catch(e){$('percent-error').hidden=false;$('percent-error').textContent=e.message}}
function copy(text){if(!navigator.clipboard)throw new Error('Le presse-papiers n’est pas disponible. Sélectionnez le texte affiché pour le copier.');return navigator.clipboard.writeText(text)}
document.querySelectorAll('input[name=percent-mode]').forEach(r=>r.onchange=()=>{mode=r.value;renderFields()});$('percent-form').onsubmit=e=>{e.preventDefault();show()};$('percent-form').onreset=()=>{last=null;setTimeout(()=>{$('percent-values').hidden=true;$('percent-empty').hidden=false;$('percent-error').hidden=true},0)};$('rounding-select').onchange=()=>{if(last)show()};$('percent-form').oninput=()=>{if(last){$('percent-values').classList.add('stale')}};$('copy-result').onclick=async()=>{if(!last)return;try{await copy(`${formatNumber(last.result.result,$('rounding-select').value)}${last.result.unit==='%'?' %':' €'}`);$('copy-status').textContent='Résultat copié.'}catch(e){$('copy-status').textContent=e.message}};$('copy-detail').onclick=async()=>{if(!last)return;try{await copy(`${last.result.formula}\n${last.result.application} = ${formatNumber(last.result.result,$('rounding-select').value)}${last.result.unit==='%'?' %':' €'}`);$('copy-status').textContent='Détail copié.'}catch(e){$('copy-status').textContent=e.message}};renderFields();
registerSelectionTool({name:'calculate_percentage',title:'Calculer un pourcentage',description:'Calcule avec les valeurs présentes dans le calculateur, sans les transmettre.',inputSchema:{type:'object',additionalProperties:false},execute(){show();if(!last)throw new Error('Les valeurs sont invalides.');return {result:last.result.result,unit:last.result.unit}}});
src/assets/percentage-calcul.mjs
export function parseNumber(value,{required=true}={}){const s=String(value??'').trim().replace(/[\s\u00a0\u202f]/g,'');if(!s)return required?{error:'Saisissez un nombre.'}:{value:0};if(!/^-?(?:\d+(?:[.,]\d+)?|[.,]\d+)$/.test(s))return {error:'Saisissez un nombre avec une virgule ou un point.'};const n=Number(s.replace(',','.'));return Number.isFinite(n)&&Math.abs(n)<=1e15?{value:n}:{error:'Cette valeur est trop grande ou invalide.'}}
export function calculate(mode,v){
const out=(result,formula,application,unit='%')=>({result,formula,application,unit});
if(mode==='part'){if(v.total===0)throw new Error('Le total ne peut pas être nul.');return out(v.part/v.total*100,'part ÷ total × 100',`${v.part} ÷ ${v.total} × 100`)}
if(mode==='value')return out(v.value*v.rate/100,'valeur de référence × taux ÷ 100',`${v.value} × ${v.rate} ÷ 100`, '€');
if(mode==='change'){const result=v.initial*(1+(v.direction==='decrease'?-1:1)*v.rate/100);return out(result,`${v.direction==='decrease'?'valeur initiale × (1 − taux ÷ 100)':'valeur initiale × (1 + taux ÷ 100)'}`,`${v.initial} × (1 ${v.direction==='decrease'?'−':'+'} ${v.rate} ÷ 100)`, '€')}
if(mode==='evolution'){if(v.old===0)throw new Error('Une ancienne valeur égale à zéro ne permet pas de calculer une évolution en pourcentage.');return out((v.current-v.old)/v.old*100,'(valeur nouvelle − valeur ancienne) ÷ valeur ancienne × 100',`(${v.current} − ${v.old}) ÷ ${v.old} × 100`)}
throw new Error('Mode de calcul inconnu.');
}
export const formatNumber=(n,decimals)=>new Intl.NumberFormat('fr-FR',{maximumFractionDigits:decimals==='auto'?6:Number(decimals),minimumFractionDigits:decimals==='auto'?0:Number(decimals),useGrouping:true}).format(n);
src/shared/layout.mjs
import { support } from './support.mjs';
import config from '../../site.config.mjs';
export const escape = s => String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
export function layout({ title, description, route, body, script, active }) {
const navigation=[['fusion-pdf','fusion','Fusionner'],['separation-pdf','separation','Séparer'],['pdf-vers','pdf-vers','PDF vers'],['compresser-image','image-compress','Images'],['calculateur-immobilier','immobilier','Immobilier'],['calculateur-pourcentage','pourcentage','% Pourcentage']].map(([slug,key,label])=>`<a href="/outils/${slug}" ${active===key?'aria-current="page"':''}>${label}</a>`).join('');
return `<!doctype html><html lang="fr"><head><meta charset="utf-8"><meta name="google-adsense-account" content="ca-pub-5015303387352167"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${escape(title)} | ${escape(config.name)}</title><meta name="description" content="${escape(description)}"><link rel="canonical" href="${config.origin}${route}"><link rel="icon" href="/assets/favicon.svg" type="image/svg+xml"><link rel="stylesheet" href="/assets/style.css">
<!-- Chargé en production par ads.mjs, avec la CMP Google publiée dans AdSense. -->
<link rel="stylesheet" href="/assets/calculator.css">
<link rel="stylesheet" href="/assets/conversion.css">
<link rel="stylesheet" href="/assets/percentage.css">
<link rel="stylesheet" href="/assets/image-compress.css">
<link rel="stylesheet" href="/assets/ad-layout.css">
<template id="adsense-script"><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-5015303387352167" crossorigin="anonymous"></script></template>
<script type="module" src="/assets/ads.mjs"></script></head><body><a class="skip" href="#contenu">Aller au contenu</a><header><nav aria-label="Navigation principale"><a class="brand" href="/"><span class="brand-mark" aria-hidden="true">✳</span>${escape(config.name)}</a><div class="nav-links">${navigation}</div><a class="header-kofi" href="https://ko-fi.com/S0D7273NVT" target="_blank" rel="noopener noreferrer"><span aria-hidden="true">☕</span> Soutenez moi</a></nav></header><div class="site-layout"><aside class="ad-rail ad-rail-left" data-ad-rail="left" hidden aria-label="Publicité"><span>Publicité</span></aside><main id="contenu">${body}</main></div>${support}<footer><span>${escape(config.name)} · Des outils à portée de main</span><div><a href="/privacy">Confidentialité</a><a href="/cookies">Cookies</a><a href="/mentions-legales">Mentions légales</a></div></footer>${script ? `<script type="module" src="/assets/${script}"></script>` : ''}</body></html>`;
}
export const heading = (id, title, intro) => `<div class="page-heading"><a class="back" href="/">Tous les outils</a><div class="eyebrow">OUTILS PDF</div><h1>${title}</h1><p class="intro">${intro}</p><p class="assurance">Gratuit <span>·</span> Sans inscription <span>·</span> Traitement dans votre navigateur</p></div>`;
export const upload = multiple => `<div class="dropzone" id="dropzone"><span class="upload-symbol" aria-hidden="true">↑</span><h2>Déposez ${multiple ? 'vos PDF' : 'votre PDF'} ici</h2><p>ou sélectionnez ${multiple ? 'des fichiers' : 'un fichier'} sur votre appareil</p><label class="button primary file-picker">${multiple ? 'Choisir mes PDF' : 'Choisir mon PDF'}<input id="file-input" type="file" accept="application/pdf,.pdf" ${multiple ? 'multiple' : ''}></label><p class="muted small">PDF uniquement · Vos documents restent sur votre appareil</p></div>`;
export const feedback = `<div id="import-errors" aria-live="polite"></div><p id="status" role="status" aria-live="polite"></p><div id="error" role="alert" hidden></div>`;
export const result = `<section id="result" class="result" aria-labelledby="result-title" hidden><div class="eyebrow">TERMINÉ</div><h2 id="result-title">Vos documents sont prêts</h2><div id="downloads"></div><div class="actions"><button id="back-settings" class="secondary">Revenir aux réglages</button><button id="restart" class="text-button">Recommencer</button></div><p id="zip-error" role="alert" hidden></p></section>`;
export function guide(kind, steps, faqs) { return `<aside class="ad-space" data-ad-tool="${kind}" hidden aria-label="Publicité"><span>Publicité</span></aside><section class="guide"><div class="section-heading"><span class="eyebrow">MODE D’EMPLOI</span><h2>Trois étapes, tout simplement</h2></div><ol class="steps">${steps.map(([title, text]) => `<li><h3>${title}</h3><p>${text}</p></li>`).join('')}</ol></section><section class="faq"><h2>Vos questions, nos réponses</h2>${faqs.map(([q,a])=>`<details><summary>${q}</summary><p>${a}</p></details>`).join('')}</section><div class="source-link"><a href="/outils/${kind === 'fusion' ? 'fusion' : 'separation'}-pdf/source">Consulter le code source de cet outil</a></div>`; }
export const commonFaqs = [
['Mes documents sont-ils envoyés quelque part ?', 'La lecture, les aperçus, les PDF générés et les ZIP sont traités dans votre navigateur. Aucun document ni nom de fichier n’est envoyé à un serveur. Les documents ne sont pas enregistrés dans un stockage persistant. Les ressources du site et, si elles sont activées à l’avenir, les annonces peuvent effectuer des requêtes réseau.'],
['Puis-je utiliser un PDF protégé ?', 'Les PDF chiffrés ou protégés ne sont pas pris en charge. Utilisez une copie non protégée que vous êtes autorisé à modifier. Cet outil ne retire pas les protections.'],
['La mise en page est-elle conservée ?', 'Les pages sont copiées sans être converties en images, en conservant leurs dimensions et leur orientation. Les signatures numériques, formulaires interactifs, signets et fonctions avancées peuvent être perdus ou ne plus fonctionner. Vérifiez le résultat avant de l’utiliser.'],
['Et pour les documents volumineux ?', 'La capacité dépend de la mémoire de votre appareil. Cette version limite la sélection à 150 Mo, 5 000 pages et 500 fichiers de sortie par opération. En cas de difficulté, fermez les autres onglets ou traitez moins de pages à la fois. Un fichier très complexe peut atteindre les limites de mémoire plus tôt.']
];
src/shared/support.mjs
export const support = `<aside class="support" aria-label="Soutenir le site"><div><h2>Ces outils vous sont utiles ?</h2><p>Un don aide à les maintenir gratuits et à en créer de nouveaux.</p></div><div class="support-button"><a class="kofi-button" href="https://ko-fi.com/S0D7273NVT" target="_blank" rel="noopener noreferrer"><span aria-hidden="true">☕</span> Soutenez moi</a></div></aside>`;
src/assets/style.css
:root{font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;color:#17243b;background:#f5f7fb;font-size:16px;line-height:1.55;font-synthesis:none;--blue:#2457db;--line:#dce3ee;--muted:#59677d}*{box-sizing:border-box}body{margin:0}button,input,a,label{ -webkit-tap-highlight-color:transparent}a{color:var(--blue);text-underline-offset:4px}button,input{font:inherit}button,.button{display:inline-flex;align-items:center;justify-content:center;gap:.5rem;min-height:46px;padding:.65rem 1.05rem;border-radius:8px;border:1px solid transparent;font-size:.9375rem;font-weight:600;cursor:pointer;text-decoration:none}button:disabled{cursor:not-allowed;opacity:.45}button:hover:not(:disabled),.button:hover{filter:brightness(.95)}:focus-visible{outline:3px solid #e39612;outline-offset:4px}.primary{color:#fff;background:var(--blue)}.secondary{background:#fff;border-color:var(--line);color:#263953}.text-button{background:transparent;color:var(--blue);padding-inline:.5rem}.skip{position:absolute;top:-70px;left:16px;background:#fff;padding:12px;z-index:10}.skip:focus{top:10px}header{background:#fff;border-bottom:1px solid var(--line)}nav{max-width:1160px;min-height:80px;padding:0 32px;margin:auto;display:flex;align-items:center;gap:42px}.brand{display:flex;gap:10px;align-items:center;color:#182844;text-decoration:none;font-weight:750;font-size:1.2rem;white-space:nowrap}.brand-mark{background:var(--blue);color:white;border-radius:8px;width:32px;height:32px;text-align:center;font-size:1.4rem;line-height:32px}.nav-links{display:flex;gap:28px}.nav-links a{text-decoration:none;color:var(--muted);font-size:.9375rem;padding:26px 0;border-bottom:3px solid transparent}.nav-links a[aria-current]{border-color:var(--blue);color:var(--blue);font-weight:650}.local-label{margin-left:auto;color:var(--muted);font-size:.8125rem;border:1px solid var(--line);padding:4px 9px;border-radius:5px}main{max-width:1000px;margin:auto;padding:32px 32px 56px}.back{color:var(--muted);font-size:.875rem;text-decoration:none;display:inline-block;margin-bottom:28px}.back:before{content:'←';margin-right:9px}.eyebrow{font-size:.75rem;letter-spacing:.12em;font-weight:750;color:var(--blue);margin-bottom:10px}h1{font-size:clamp(2rem,5vw,2.8rem);letter-spacing:-.04em;line-height:1.15;margin:0 0 16px;font-weight:750}h2{font-size:1.35rem;letter-spacing:-.02em;line-height:1.3;margin:0 0 12px}h3{font-size:1rem;margin:0 0 7px}.intro{font-size:1.0625rem;color:var(--muted);margin:0 0 18px}.assurance{font-size:.875rem;color:#475770;display:flex;gap:10px;flex-wrap:wrap;margin:0 0 28px}.workspace{background:#fff;border:1px solid var(--line);border-radius:14px;padding:24px;box-shadow:0 4px 16px #17243b04}fieldset{min-width:0;margin:0;padding:0;border:0}.dropzone{background:#f8faff;border:1.5px dashed #b9c9eb;border-radius:9px;padding:28px 18px;text-align:center}.dropzone.dragover{border-color:var(--blue);background:#edf2ff}.dropzone h2{font-size:1.2rem;margin:8px 0}.dropzone p{margin:6px 0 16px;color:var(--muted);font-size:.9375rem}.dropzone .small{margin:14px 0 0;font-size:.8125rem}.upload-symbol{display:block;font-size:1.8rem;line-height:1;margin:0 auto 14px;color:var(--blue);font-weight:500}.file-picker{position:relative}.file-picker:focus-within{outline:3px solid #e39612;outline-offset:4px}.file-picker input{position:absolute;width:100%;height:100%;opacity:0;inset:0;cursor:pointer}.muted{color:var(--muted)}.small{font-size:.875rem}.action-bar{display:flex;align-items:center;justify-content:space-between;gap:20px;margin-top:24px;padding-top:20px;border-top:1px solid var(--line)}.action-bar p{font-size:.875rem;color:var(--muted);margin:0}.action-bar button{white-space:nowrap}.list-heading{display:flex;align-items:center;justify-content:space-between;margin-top:24px}.list-heading h2{margin:0}.file-list{list-style:none;padding:0;display:grid;gap:10px}.file-row{display:flex;align-items:center;gap:14px;border:1px solid var(--line);border-radius:8px;padding:12px}.file-row.dragging{opacity:.5}.position{color:var(--muted);font-size:.875rem;width:22px;flex:none;text-align:center}.pdf-icon{color:var(--blue);background:#edf2ff;padding:8px 6px;border-radius:5px;font-size:.75rem;font-weight:750}.file-details{min-width:0;flex:1}.file-name{display:block;overflow-wrap:anywhere;font-weight:600;font-size:.9375rem}.file-meta{font-size:.8125rem;color:var(--muted)}.file-actions{display:flex;flex-wrap:wrap;gap:4px}.file-actions button{min-height:40px;padding:5px 9px;font-size:.875rem}.totals{font-weight:600;font-size:.9375rem}.warning{color:#795012;font-size:.875rem;background:#fff8e8;padding:10px;border-radius:6px}.field-error,#error,.import-error,#zip-error{color:#9b263a;font-size:.9375rem}#error,.import-error{background:#fff2f3;border:1px solid #f2ccd1;padding:12px;border-radius:8px;margin-top:16px}.import-error{display:flex;justify-content:space-between;align-items:center;gap:12px}.import-error span{overflow-wrap:anywhere}#status:not(:empty){color:#3058aa;margin:16px 0;font-size:.9375rem}#status:empty{margin:0}#status[data-busy=true]:before{content:'';display:inline-block;width:12px;height:12px;margin-right:9px;border:2px solid #aec1ed;border-top-color:var(--blue);border-radius:50%;animation:spin .8s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}@media(prefers-reduced-motion:reduce){#status[data-busy=true]:before{animation:none}}.result{margin-top:24px;padding:24px;border-radius:9px;background:#f1f7ff;border:1px solid #c9dbf9}.actions{display:flex;gap:12px;flex-wrap:wrap;margin-top:20px}.download-row{display:flex;justify-content:space-between;align-items:center;gap:14px;padding:12px 0;border-bottom:1px solid #d7e3f6}.download-row span{overflow-wrap:anywhere}.download-row a{flex:none}input[type=text],input[type=number]{background:white;border:1px solid #aab9cf;border-radius:7px;padding:10px 12px;min-height:46px;max-width:100%;color:#17243b}input[type=text]{width:100%;margin-top:7px}label{font-size:.9375rem;font-weight:550}input[aria-invalid=true]{border-color:#a0263b}.document-info{margin:20px 0;display:flex;align-items:center;gap:12px;padding-bottom:20px;border-bottom:1px solid var(--line)}.document-info .file-details{flex:1}.mode-picker{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin:24px 0}.mode-picker legend{font-weight:650;margin-bottom:14px}.mode-picker label{display:flex;align-items:flex-start;gap:8px;border:1px solid var(--line);border-radius:8px;padding:14px 10px;cursor:pointer}.mode-picker label:has(:checked){border-color:var(--blue);background:#f2f6ff}.mode-picker input{margin-top:5px;accent-color:var(--blue);flex:none}.mode-picker small{display:block;font-size:.8125rem;font-weight:400;color:var(--muted);margin-top:4px}.selection-actions{display:flex;flex-wrap:wrap;align-items:center;gap:10px;margin:16px 0}.selection-actions span{margin-left:auto;font-size:.875rem}.thumbnails{display:grid;grid-template-columns:repeat(auto-fill,minmax(115px,1fr));gap:12px;max-height:530px;overflow:auto;padding:6px}.thumbnail{display:flex;flex-direction:column;background:#f6f8fb;border:2px solid var(--line);padding:9px;position:relative;min-height:173px;border-radius:7px}.thumbnail[aria-pressed=true]{border-color:var(--blue);background:#edf3ff}.thumbnail img{width:100%;height:132px;object-fit:contain;background:#fff;pointer-events:none}.thumbnail .placeholder{height:132px;display:flex;align-items:center;justify-content:center;color:var(--muted);font-size:.75rem;font-weight:400}.thumbnail .page-label{font-size:.8125rem;margin-top:8px}.thumbnail[aria-pressed=true] .page-label:before{content:'✓ ';color:var(--blue)}.range-row{display:flex;gap:12px;align-items:end;margin:12px 0;flex-wrap:wrap}.range-row label{flex:1;min-width:115px}.range-row input{display:block;width:100%;margin-top:6px}.range-row span{align-self:center;min-width:80px;font-size:.875rem;color:var(--muted)}.ad-space{margin:48px 0;min-height:120px;text-align:center;font-size:.75rem;color:var(--muted)}.guide{margin-top:56px}.steps{display:grid;grid-template-columns:repeat(3,1fr);gap:32px;list-style:none;padding:0;counter-reset:step}.steps li:before{counter-increment:step;content:'0' counter(step);display:block;color:var(--blue);font-size:.875rem;font-weight:700;margin-bottom:13px}.steps p{font-size:.9375rem;color:var(--muted);margin:0}.section-heading{margin-bottom:28px}.faq{margin-top:48px;max-width:800px}.faq h2{margin-bottom:24px}details{border-top:1px solid var(--line);padding:18px 0}details:last-child{border-bottom:1px solid var(--line)}summary{cursor:pointer;font-weight:600;font-size:.9375rem}details p{font-size:.9375rem;color:var(--muted);margin:14px 0 0}.source-link{margin-top:32px;font-size:.875rem}footer{border-top:1px solid var(--line);max-width:1096px;margin:auto;padding:28px 32px 36px;display:flex;justify-content:space-between;gap:20px;color:var(--muted);font-size:.8125rem}footer div{display:flex;gap:20px;flex-wrap:wrap}footer a{color:var(--muted)}.home{padding-top:28px}.home .intro{max-width:580px}.tool-grid{display:grid;grid-template-columns:1fr 1fr;gap:22px;margin-top:36px}.tool-card{display:block;padding:30px;background:#fff;border:1px solid var(--line);border-radius:12px;text-decoration:none;color:inherit}.tool-card:hover{border-color:#9bb3ee}.tool-card h2{margin-top:20px;font-size:1.5rem}.tool-card p{color:var(--muted);font-size:.9375rem;margin-bottom:24px}.tool-card .go{font-size:.9375rem;color:var(--blue);font-weight:650}.tool-number{color:var(--blue);font-size:.875rem}.prose{max-width:780px}.prose p{color:var(--muted)}pre{padding:20px;background:#142239;color:#e5ecf7;border-radius:8px;overflow:auto;font-size:.8125rem;line-height:1.65;tab-size:2}code{font-family:ui-monospace,Consolas,monospace}.source-section{margin-top:28px}.source-section h2{overflow-wrap:anywhere}[hidden]{display:none!important}@media(max-width:720px){nav{padding:0 20px;gap:22px;min-height:70px;flex-wrap:wrap}.local-label{display:none}.nav-links{gap:18px;margin-left:auto}.nav-links a{padding:22px 0}main{padding:24px 18px 40px}.back{margin-bottom:24px}.workspace{padding:16px}.dropzone{padding:24px 10px}.action-bar{align-items:stretch;flex-direction:column;gap:14px}.action-bar button{width:100%}.steps,.tool-grid,.mode-picker{grid-template-columns:1fr}.steps{gap:24px}.guide{margin-top:38px}.file-row{gap:10px;flex-wrap:wrap}.file-details{flex-basis:65%}.file-actions{margin-left:auto}.mode-picker{gap:8px}.mode-picker label{padding:12px}.mode-picker small{margin:0}.download-row{align-items:stretch;flex-direction:column}.selection-actions span{width:100%;margin:0}.result{padding:16px}.document-info{flex-wrap:wrap}footer{padding:24px 18px;flex-direction:column}.tool-card{padding:24px}.thumbnails{grid-template-columns:repeat(auto-fill,minmax(100px,1fr))}}@media(min-width:721px){.page-heading{padding-top:4px}}
src/assets/calculator.css
/* Le calculateur reprend les couleurs et commandes du site. */
.calculator{display:grid;grid-template-columns:minmax(0,1.05fr) minmax(0,1fr);gap:24px;align-items:start}.calculator .workspace{padding:24px}.calculator fieldset+fieldset{margin-top:28px;padding-top:24px;border-top:1px solid var(--line)}.calculator legend{font-size:1.15rem;font-weight:700;margin-bottom:16px}.calculator legend span{font-size:.8rem;color:var(--blue);margin-right:8px}.calc-field{margin:16px 0}.calc-field p{margin:6px 0}.unit-input{position:relative}.unit-input input{width:100%;padding-right:70px;font-variant-numeric:tabular-nums}.unit-input span{position:absolute;right:12px;top:18px;color:var(--muted);font-size:.8125rem;pointer-events:none}.field-pair{display:grid;grid-template-columns:1fr 1fr;gap:14px}.required{color:var(--blue)}select{font:inherit;font-size:.9375rem;color:#17243b;border:1px solid #aab9cf;border-radius:7px;width:100%;padding:11px 30px 11px 10px;background:white;margin:8px 0;min-height:46px}.calc-result{background:#fff;border:1px solid var(--line);border-radius:14px;padding:26px;position:sticky;top:20px;min-width:0}.main-metric{background:#eef3ff;border-radius:9px;padding:20px;margin:20px 0}.main-metric span{display:block;font-size:.9375rem}.main-metric strong{display:block;font-size:clamp(1.75rem,3vw,2.3rem);letter-spacing:-.03em;color:var(--blue);margin:4px 0;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.main-metric small{font-size:.8125rem;color:var(--muted)}.key-results{margin:0 0 26px}.key-results>div{padding:13px 0;border-bottom:1px solid var(--line)}.key-results dt{font-size:.875rem;color:var(--muted)}.key-results dt small{display:block;font-size:.75rem}.key-results dd{margin:3px 0 0;font-weight:700;font-size:1.15rem}.result-details{margin:16px 0;font-size:.875rem}.result-details>div{display:flex;justify-content:space-between;gap:18px;margin:10px 0;break-inside:avoid}.result-details dt{color:var(--muted)}.result-details dd{margin:0;text-align:right;font-variant-numeric:tabular-nums;font-weight:550;white-space:nowrap}.simulation-limit{font-size:.8125rem;color:var(--muted);border-top:1px solid var(--line);padding-top:16px;margin:24px 0 0}.notice{background:#edf3ff;border-left:3px solid var(--blue);padding:12px;font-size:.875rem}.calc-guide p{color:var(--muted);font-size:.9375rem}.finance-details{margin-top:20px}.stale{opacity:.48}#calc-stale{opacity:1}figure{margin:24px 0}figcaption{font-size:.9375rem;font-weight:650;margin-bottom:12px}.repayment-bars{height:18px;display:flex;overflow:hidden;border-radius:4px}.repayment-bars span{height:100%}#repayment-legend{list-style:none;padding:0;font-size:.8125rem}#repayment-legend li{margin:7px 0}#repayment-legend li:before{content:'';display:inline-block;width:10px;height:10px;background:var(--legend-color);margin-right:8px;border-radius:2px}.schedule{margin-top:28px}.table-scroll{max-width:100%;overflow:auto}table{border-collapse:collapse;min-width:780px;width:100%;font-size:.8125rem}th,td{text-align:right;padding:12px 10px;border-bottom:1px solid var(--line);white-space:nowrap}thead th{background:#f2f5fc;font-weight:650}tbody tr:nth-child(even){background:#fafbfe}.pagination{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-top:20px}.pagination span{font-size:.875rem}.print-only{display:none}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}#calc-errors{color:#9b263a;background:#fff2f3;padding:12px;border-radius:7px}.tool-grid{grid-template-columns:repeat(3,minmax(0,1fr))}.tool-card{padding:24px}.tool-card h2{font-size:1.3rem}.nav-links{gap:22px}@media(max-width:850px){.calculator{grid-template-columns:1fr}.calc-result{position:static}.tool-grid{grid-template-columns:1fr}.nav-links{gap:14px}.nav-links a{font-size:.875rem}}@media(max-width:450px){.brand{font-size:1rem;gap:7px}.brand-mark{width:26px;height:26px;font-size:1.15rem;line-height:26px}nav{gap:10px;padding:0 14px}.nav-links{gap:12px}.nav-links a{font-size:.8125rem}.calculator .workspace,.calc-result{padding:18px}.field-pair{gap:12px}.result-details>div{gap:10px;flex-wrap:wrap}.result-details dd{margin-left:auto}.pagination{flex-wrap:wrap}.pagination span{order:-1;flex-basis:100%;text-align:center}}
@media print{@page{size:A4;margin:15mm}body{background:#fff;color:#000;font-size:11pt}header,footer,.back,.assurance,.eyebrow,.calculator form,.print-hide,.source-link,.ad-space,.calc-guide,.faq,.schedule,.skip,#calc-stale{display:none!important}main{max-width:none;padding:0}.calculator{display:block}.calc-result{position:static;border:0;padding:0}.page-heading h1{font-size:24pt}.calc-result h2{font-size:16pt}.main-metric{background:none;padding:0}.main-metric strong{font-size:22pt;color:black}.key-results{display:grid;grid-template-columns:1fr 1fr 1fr;gap:18px}.print-only{display:block}.print-only h2{margin-top:25px}.main-metric,.key-results,figure{break-inside:avoid}.repayment-bars{-webkit-print-color-adjust:exact;print-color-adjust:exact}.simulation-limit:after{content:' Le taux nominal n’est pas le TAEG. Modèle à taux fixe sur périodes mensuelles régulières, hors prêts variables, différés, remboursements anticipés et montages multiprêts. Les frais estimés peuvent différer des frais réels. Ce calcul ne vaut pas accord de financement.'}.print-invalid #calc-values,.print-invalid #print-parameters{display:none!important}.print-invalid .calc-result:before{content:'Aucune simulation à jour. Recalculez les résultats avant de les imprimer.';display:block;font-weight:bold}.stale{opacity:1}}
src/assets/conversion.css
.conversion-steps{list-style:none;display:flex;gap:10px;flex-wrap:wrap;padding:0;margin:0 0 24px;counter-reset:steps}.conversion-steps li{font-size:.8125rem;padding:7px 12px;border:1px solid var(--line);background:#fff;border-radius:6px;color:var(--muted)}.conversion-heading{margin:30px 0 14px;font-size:1.15rem}.conversion-heading:first-of-type{margin-top:0}.converter .dropzone{padding:22px 15px}.converter .dropzone h2{margin-top:0}.converter .upload-symbol{display:none}.conversion-formats{margin:16px 0}.page-range-choice{margin-top:22px;padding-top:20px;border-top:1px solid var(--line)}.page-range-choice>select{max-width:340px;display:block}.conversion-file-status{margin:5px 0;font-size:.875rem;font-weight:600}.conversion-output-group{margin:22px 0;padding-bottom:12px;border-bottom:1px solid var(--line)}.conversion-output-group h3{overflow-wrap:anywhere}.text-preview{white-space:pre-wrap;max-height:320px;overflow:auto;overflow-wrap:anywhere}.conversion-help{max-width:800px}.conversion-help p{font-size:.9375rem;color:var(--muted)}.related-tools{margin-top:32px;display:flex;gap:14px;flex-wrap:wrap;font-size:.875rem}.related-tools h2{width:100%;font-size:1.1rem}.tool-grid{grid-template-columns:repeat(2,minmax(0,1fr))}#conversion-files .file-details{flex-basis:60%}.source-index{overflow-wrap:anywhere}.nav-links{flex-wrap:wrap}html:has(.converter) .field-pair{align-items:start}@media(max-width:720px){.conversion-steps{gap:6px}.conversion-steps li{font-size:.75rem;padding:5px 8px}.tool-grid{grid-template-columns:1fr}nav{padding-top:12px;gap:6px}.nav-links{width:100%;margin-left:0;justify-content:space-between;gap:12px}.nav-links a{padding:10px 0}.brand{margin-bottom:2px}.conversion-formats label{padding:12px}.converter .field-pair{grid-template-columns:1fr;gap:4px}}@media print{html{background:#fff}}
src/assets/percentage.css
.percentage-layout{display:grid;grid-template-columns:minmax(0,1fr) minmax(320px,.8fr);gap:24px;align-items:start}.percentage-tool{padding:24px}.percent-modes{grid-template-columns:1fr 1fr;margin:0 0 24px}.percent-modes label{min-height:86px}.direction{display:flex;gap:22px;align-items:center;margin:10px 0 20px}.direction legend{font-weight:600;margin-right:8px}.percent-result{background:#fff;border:1px solid var(--line);border-radius:14px;padding:24px;position:sticky;top:20px}.percent-main{background:#eef3ff;border-radius:9px;padding:20px;margin:20px 0}.percent-main span{display:block;font-size:.9rem;color:var(--muted)}.percent-main strong{display:block;color:var(--blue);font-size:2.4rem;letter-spacing:-.04em;margin-top:5px;overflow-wrap:anywhere}.formula{font-size:.9rem}.formula>div{padding:13px 0;border-bottom:1px solid var(--line)}.formula dt{color:var(--muted)}.formula dd{margin:5px 0 0;font-weight:600;overflow-wrap:anywhere}.local-note{margin-top:24px;padding-top:16px;border-top:1px solid var(--line);color:var(--muted);font-size:.8125rem}.explanation-title{margin-top:42px!important}.percentage-guide>p{max-width:820px;color:var(--muted)}.percent-result .stale{opacity:.5}.percent-result .notice{font-size:.875rem}.copy-status{color:#216b4b;min-height:1.4em}.percentage-tool .calc-field{margin:18px 0}.percentage-tool .unit-input input{padding-right:55px}.percentage-tool .rounding{border-top:1px solid var(--line);padding-top:18px;margin-top:8px}.rounding label{font-weight:600}.rounding select{width:auto;min-width:145px;margin-left:10px}.tool-grid{grid-template-columns:repeat(3,minmax(0,1fr))}@media(max-width:850px){.percentage-layout{grid-template-columns:1fr}.percent-result{position:static}.percent-modes{grid-template-columns:1fr 1fr}}@media(max-width:520px){.percent-modes{grid-template-columns:1fr}.rounding label{display:block}.rounding select{margin:8px 0 0;width:100%}.direction{flex-wrap:wrap;gap:12px}.tool-grid{grid-template-columns:1fr}}
src/assets/image-compress.css
.image-compressor{display:grid;grid-template-columns:minmax(0,1fr) minmax(320px,.8fr);gap:24px;align-items:start}.image-compressor>.workspace{padding:24px}.image-compressor h2{margin-top:28px}.image-compressor h2:first-child{margin-top:0}.compression-levels{grid-template-columns:repeat(3,1fr);margin:14px 0 24px}.compression-levels label{min-height:76px}.advanced-image{margin-top:22px}.advanced-image .field-pair{margin-top:18px}.advanced-image input[type=number]{width:100%;min-height:46px}.advanced-image label{display:block;margin:14px 0}.advanced-image .check{display:flex;gap:9px;align-items:center}.target-input{display:grid;grid-template-columns:1fr 120px;gap:10px;align-items:end;margin-top:14px}.target-input input{width:100%;min-height:46px;padding:10px 12px;border:1px solid #aab9cf;border-radius:7px}.target-input select{margin:0}.image-thumb{object-fit:contain;border-radius:6px;background:#f4f6fa;border:1px solid var(--line);flex:none}.image-compressor .file-row{align-items:center}.image-compressor .file-details{flex:1}.image-compressor .download-row{align-items:flex-start}.image-compressor .download-row span{line-height:1.5}.image-guide{max-width:820px}.image-guide p{color:var(--muted);font-size:.9375rem}.image-compressor .result{position:sticky;top:20px}.image-compressor .result>button{margin-top:16px}.related-tools{margin-top:32px;display:flex;gap:14px;flex-wrap:wrap;font-size:.875rem}.related-tools h2{width:100%;font-size:1.1rem;margin:0}.related-tools a{color:var(--blue)}@media(max-width:850px){.image-compressor{grid-template-columns:1fr}.image-compressor .result{position:static}}@media(max-width:560px){.compression-levels{grid-template-columns:1fr}.target-input{grid-template-columns:1fr}.image-compressor .file-row{flex-wrap:wrap}}
src/assets/ad-layout.css
.site-layout{max-width:1000px;margin:auto;display:grid;grid-template-columns:minmax(0,1fr);align-items:start}
.site-layout main{grid-column:1;min-width:0;width:100%;max-width:none;margin:0}
.ad-rail{display:none;min-height:600px;margin-top:80px;text-align:center;font-size:.75rem;color:var(--muted)}
.support{max-width:936px;margin:0 auto 32px;padding:24px;background:#fff;border:1px solid var(--line);border-radius:12px;display:flex;align-items:center;justify-content:space-between;gap:24px}
.support p{margin:0;color:var(--muted);font-size:.9375rem}.support h2{font-size:1.1rem;margin-bottom:6px}.support-button{flex:none}.support .kofi-button{background:#72a4f2;color:#142239;border-radius:8px;padding:12px 20px;font-weight:700;text-decoration:none;display:inline-flex;gap:8px;align-items:center}
nav{flex-wrap:wrap;gap:16px 28px}.nav-links{flex-wrap:wrap;gap:8px 22px}.nav-links a{white-space:nowrap}.local-label{white-space:nowrap}
@media(min-width:1100px){.site-layout:has(.ad-rail:not([hidden])){max-width:1260px;grid-template-columns:180px minmax(0,1fr);gap:24px}.site-layout:has(.ad-rail:not([hidden])) main{grid-column:2}.ad-rail:not([hidden]){display:block;grid-column:1;grid-row:1}}
@media(max-width:720px){.site-layout main{padding:24px 18px 40px}.support{margin:0 18px 24px;flex-direction:column;align-items:flex-start;gap:16px}.nav-links{margin-left:0;width:100%}.nav-links a{padding:8px 0}nav{padding-top:14px;padding-bottom:10px}}
@media print{.support,.ad-rail{display:none!important}}
.header-kofi{margin-left:auto;flex:none;display:inline-flex;align-items:center;gap:8px;padding:9px 14px;border-radius:8px;background:#72a4f2;color:#142239;text-decoration:none;font-size:.875rem;font-weight:700;white-space:nowrap}.privacy h2{margin-top:32px}.privacy{max-width:850px}.privacy p{line-height:1.75}
@media(max-width:1000px){nav .brand{order:0}.header-kofi{order:1}.nav-links{order:2;width:100%;margin-left:0}.nav-links a{padding:8px 0}nav{padding-block:12px}}
@media print{.header-kofi{display:none!important}}
.privacy-settings{font:inherit;color:var(--muted);background:transparent;padding:0;min-height:32px;text-decoration:underline;text-underline-offset:4px;text-align:left}footer{flex-wrap:wrap}
src/assets/ads.mjs
import config from './config.mjs';
// The AdSense tag deploys the Google CMP published in Privacy & messaging.
// Google's native TCF integration handles consent and ad eligibility.
// Do not wait for a custom consent event before loading the tag: it also loads the CMP.
export function startGoogleAds() {
if (!config.ads.enabled || location.hostname !== 'lupus-duj.pages.dev') return false;
if (document.head.querySelector('script[data-adsense-loaded]')) return false;
const template = document.getElementById('adsense-script');
if (!template) return false;
window.googlefc = window.googlefc || {};
window.googlefc.callbackQueue = window.googlefc.callbackQueue || [];
const settings = document.createElement('button');
settings.type = 'button';
settings.className = 'privacy-settings';
settings.textContent = 'Paramètres de confidentialité et de cookies';
settings.hidden = true;
document.querySelector('footer')?.append(settings);
window.googlefc.callbackQueue.push({ CONSENT_API_READY: () => {
if (typeof window.googlefc.showRevocationMessage === 'function') settings.hidden = false;
}});
settings.addEventListener('click', () => {
window.googlefc.callbackQueue.push(() => window.googlefc.showRevocationMessage());
});
const positions = [...document.querySelectorAll('[data-ad-tool],[data-ad-rail]')];
const desktop = window.matchMedia('(min-width:1100px)');
// No ad units on policy or source pages, but keep the CMP available for revocation.
const contentPage = /^\/outils\/?$/.test(location.pathname) || document.querySelector('[data-ad-tool]');
function placeAds() {
if (!contentPage) return;
for (const position of positions) {
if (position.querySelector('ins') || (position.dataset.adRail && !desktop.matches)) continue;
const slot = config.ads.slots[position.dataset.adTool || position.dataset.adRail];
if (!/^\d+$/.test(slot || '')) continue;
position.hidden = false;
const ad = document.createElement('ins');
ad.className = 'adsbygoogle';
ad.style.display = 'block';
ad.dataset.adClient = config.ads.client;
ad.dataset.adSlot = slot;
ad.dataset.adFormat = 'auto';
ad.dataset.fullWidthResponsive = 'true';
position.append(ad);
const observer = new MutationObserver(() => {
position.hidden = ad.dataset.adStatus === 'unfilled';
});
observer.observe(ad, { attributes: true, attributeFilter: ['data-ad-status'] });
(window.adsbygoogle = window.adsbygoogle || []).push({});
}
}
const script = template.content.querySelector('script').cloneNode(true);
script.dataset.adsenseLoaded = 'true';
script.referrerPolicy = 'strict-origin-when-cross-origin';
script.addEventListener('load', () => {
placeAds();
desktop.addEventListener('change', placeAds);
});
script.addEventListener('error', () => {
for (const position of positions) position.hidden = true;
});
document.head.append(script);
return true;
}
startGoogleAds();
src/assets/webmcp.mjs
// Extension facultative du navigateur ; ne reçoit ni contenu ni nom de document.
export function registerSelectionTool(tool) {
if(!document.modelContext?.registerTool)return;
const lifecycle=new AbortController();
try{Promise.resolve(document.modelContext.registerTool(tool,{signal:lifecycle.signal})).catch(()=>{})}catch{}
window.addEventListener('pagehide',()=>lifecycle.abort(),{once:true});
}
site.config.mjs
export default {
name: 'Les outils',
origin: 'https://lupus-duj.pages.dev',
ads: { enabled: true, client: 'ca-pub-5015303387352167', slots: { fusion: '4967098598', separation: '4967098598', immobilier: '4967098598', conversion: '4967098598', percentage: '4967098598', image: '4967098598', left: '6872786213' } }
};