⑤ Neopets — QuickRef Pet Card

Full revamp of /quickref.phtml into a Pokemon-card style layout, themed with Neopets h5 assets. Standalone (no dependency on the other scripts in this collection). Embedded theme list, default theme stored locally, each pet card can be assigned its own theme via the bottom carousel arrows. Right-click an arrow to reset that pet to the default theme. Also tracks per-pet stat history and shows a small evolution graph.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey, το Greasemonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

You will need to install an extension such as Tampermonkey to install this script.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Userscripts για να εγκαταστήσετε αυτόν τον κώδικα.

You will need to install an extension such as Tampermonkey to install this script.

Θα χρειαστεί να εγκαταστήσετε μια επέκταση διαχείρισης κώδικα χρήστη για να εγκαταστήσετε αυτόν τον κώδικα.

(Έχω ήδη έναν διαχειριστή κώδικα χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Έχω ήδη έναν διαχειριστή στυλ χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

// ==UserScript==
// @name         ⑤ Neopets — QuickRef Pet Card
// @namespace    neopets-qol
// @version      1.0.1
// @author       marius@clraik
// @license      MIT
// @description  Full revamp of /quickref.phtml into a Pokemon-card style layout, themed with Neopets h5 assets. Standalone (no dependency on the other scripts in this collection). Embedded theme list, default theme stored locally, each pet card can be assigned its own theme via the bottom carousel arrows. Right-click an arrow to reset that pet to the default theme. Also tracks per-pet stat history and shows a small evolution graph.
// @match        *://www.neopets.com/quickref.phtml*
// @run-at       document-start
// @grant        none
// ==/UserScript==

(function () {
  'use strict';
  if (window.top !== window) return;

  /* =========================
     CONFIG
  ========================= */
  const CFG = {
    HEAL_GIF:  'https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExMmdwczVnemRzM2I0MjU5ZDZreW9rbzB3N3g5ZXRqZjlkam1rOGh1MiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9cw/WQTfxKgnEycoi8eRLX/giphy.gif',
    HEAL_LINK: 'https://www.neopets.com/safetydeposit.phtml?obj_name=Cooling+Ointment',
    POSE_SWAP: true,
  };

  const STATS_LS = 'jb_np_petstats_v1';
  const HSD_CAP  = 850; // cap officiel Str/Def pour le score HSD

  /* Palette pastel par stat (lisible sur fond clair) */
  const STAT_COLOR = {
    level: '#d889b3',  // rose pastel
    hpMax: '#b59ad6',  // lila pastel
    str:   '#d88989',  // rouge pastel (corail)
    def:   '#5e7eb0',  // bleu foncé pastel (dusty)
    move:  '#7fc7be',  // turquoise pastel
    hsd:   '#a07cd8',  // lila moyen (accent HSD)
  };
  /* Couleurs UI (DA Neopets sobre) */
  const UI_TEXT_DIM = '#888';
  const UI_GRID     = '#e5e5e5';

  /* =========================
     THÈMES par couleur de pet (carte d'identité)
     p = primary (bandeau, bordure)
     s = secondary (gradient, accents)
     a = accent (chips, hover)
     light = bg tinté très pâle
  ========================= */
  const COLOUR_THEMES = {
    // Holidays / Special
    christmas: { p:'#c1121f', s:'#2d6a4f', a:'#ffd700', light:'#fff5f0' },
    halloween: { p:'#e85d04', s:'#3d2645', a:'#ffd700', light:'#fff5e6' },
    valentine: { p:'#ec4899', s:'#c41e6e', a:'#fff5f9', light:'#fff0f5' },
    // Magical
    faerie:    { p:'#ec80c8', s:'#a07cd8', a:'#ffd700', light:'#fdf2fa' },
    royal:     { p:'#7a3fbb', s:'#d4af37', a:'#fff',    light:'#f4ecff' },
    royalgirl: { p:'#7a3fbb', s:'#d4af37', a:'#fff',    light:'#f4ecff' },
    royalboy:  { p:'#5e7eb0', s:'#d4af37', a:'#fff',    light:'#f0f5fc' },
    glowing:   { p:'#ffd700', s:'#ff9500', a:'#3d2645', light:'#fffbe0' },
    // Cute / Pastel
    baby:      { p:'#f9b8c5', s:'#a8d8e0', a:'#fff',    light:'#fff5f7' },
    plushie:   { p:'#f5b5d4', s:'#a8c5e0', a:'#fff',    light:'#fff0f8' },
    transparent:{ p:'#a8d5f5', s:'#dcb8ff', a:'#fff',   light:'#fafcff' },
    invisible: { p:'#cccccc', s:'#a8d5f5', a:'#dcb8ff', light:'#fafafa' },
    // Dark
    shadow:    { p:'#3d2645', s:'#5e4565', a:'#a07cd8', light:'#f0eef5' },
    darigan:   { p:'#3d2645', s:'#a07cd8', a:'#c1121f', light:'#f0eef5' },
    zombie:    { p:'#5d8a17', s:'#3d2645', a:'#a8df3c', light:'#f0f5e8' },
    wraith:    { p:'#3d2645', s:'#8b6fbb', a:'#fff',    light:'#f4ecff' },
    grave:     { p:'#3d2645', s:'#666',    a:'#a07cd8', light:'#f0eef5' },
    skunk:     { p:'#2d2d2d', s:'#fff',    a:'#a07cd8', light:'#f5f5f5' },
    // Nature / Ghost
    ghost:     { p:'#a8d5f5', s:'#d8d8e8', a:'#fff',    light:'#f5fbff' },
    cloud:     { p:'#7eb8d4', s:'#fff',    a:'#dcb8ff', light:'#f5fcff' },
    snow:      { p:'#bcd8e8', s:'#a8d5f5', a:'#fff',    light:'#f5fafe' },
    ice:       { p:'#5e7eb0', s:'#a8d5f5', a:'#fff',    light:'#f5fbff' },
    // Fire / Heat
    fire:      { p:'#e85d04', s:'#c1121f', a:'#ffd700', light:'#fff5e6' },
    magma:     { p:'#e85d04', s:'#3d2645', a:'#ffd700', light:'#fff5e6' },
    // Water
    water:     { p:'#5e7eb0', s:'#a8d5f5', a:'#fff',    light:'#f0f5fc' },
    maraquan:  { p:'#5e7eb0', s:'#7fc7be', a:'#a8d5f5', light:'#f0fcfa' },
    // Mutant / Alien
    mutant:    { p:'#5d8a17', s:'#a8df3c', a:'#3d2645', light:'#f5fbe5' },
    alien:     { p:'#a07cd8', s:'#a8df3c', a:'#fff',    light:'#f4ecff' },
    // Tribal / Ancient
    tyrannian: { p:'#a0826d', s:'#5d3a1f', a:'#c1121f', light:'#fcf8f2' },
    pirate:    { p:'#a0826d', s:'#c1121f', a:'#d4af37', light:'#fcf8f2' },
    desert:    { p:'#d4a574', s:'#a0826d', a:'#fff',    light:'#fcf8f2' },
    island:    { p:'#7fc7be', s:'#a0826d', a:'#ffd700', light:'#f0fcfa' },
    // Mechanical / Steampunk
    robot:     { p:'#5e7eb0', s:'#888',    a:'#a8d5f5', light:'#f0f2f5' },
    steampunk: { p:'#a0826d', s:'#d4af37', a:'#5e7eb0', light:'#fcf8f2' },
    // Solid colours
    blue:      { p:'#5e7eb0', s:'#7fb3d4', a:'#fff',    light:'#f0f5fc' },
    red:       { p:'#c1121f', s:'#d88989', a:'#fff',    light:'#fff5f5' },
    green:     { p:'#5ca817', s:'#7fc7be', a:'#fff',    light:'#f5fcf0' },
    yellow:    { p:'#e6c000', s:'#f5a623', a:'#5d3a1f', light:'#fffce0' },
    orange:    { p:'#e85d04', s:'#f5a623', a:'#fff',    light:'#fff5e6' },
    pink:      { p:'#ec80c8', s:'#f5b5d4', a:'#fff',    light:'#fdf2fa' },
    purple:    { p:'#7a3fbb', s:'#a07cd8', a:'#fff',    light:'#f4ecff' },
    white:     { p:'#bbb',    s:'#a8d5f5', a:'#666',    light:'#fafafa' },
    black:     { p:'#2d2d2d', s:'#666',    a:'#a07cd8', light:'#f5f5f5' },
    brown:     { p:'#8d6e4d', s:'#5d3a1f', a:'#fff',    light:'#fcf8f2' },
    grey:      { p:'#666',    s:'#a8a8a8', a:'#fff',    light:'#f0f0f0' },
    gray:      { p:'#666',    s:'#a8a8a8', a:'#fff',    light:'#f0f0f0' },
    silver:    { p:'#9ba0a8', s:'#5e7eb0', a:'#fff',    light:'#f0f2f5' },
    gold:      { p:'#d4af37', s:'#ffd700', a:'#5d3a1f', light:'#fffce0' },
    // Patterns
    speckled:  { p:'#d4a574', s:'#5d3a1f', a:'#fff',    light:'#fcf8f2' },
    spotted:   { p:'#e6c000', s:'#5d3a1f', a:'#fff',    light:'#fffce0' },
    striped:   { p:'#5e7eb0', s:'#e85d04', a:'#fff',    light:'#f0f5fc' },
    checkered: { p:'#2d2d2d', s:'#fff',    a:'#a07cd8', light:'#f5f5f5' },
    camouflage:{ p:'#5d8a17', s:'#5d3a1f', a:'#a8df3c', light:'#f5fcf0' },
    disco:     { p:'#ec80c8', s:'#a07cd8', a:'#7fc7be', light:'#fdf2fa' },
    jelly:     { p:'#f5b5d4', s:'#a8d5f5', a:'#a8df3c', light:'#fff5fa' },
    sponge:    { p:'#f5a623', s:'#ffd700', a:'#fff',    light:'#fffce0' },
    // Special
    eventide:  { p:'#7a3fbb', s:'#7fc7be', a:'#ffd700', light:'#f4ecff' },
    sketch:    { p:'#5e5e5e', s:'#fff',    a:'#a07cd8', light:'#fafafa' },
    elderly:   { p:'#a8a8a8', s:'#888',    a:'#a07cd8', light:'#f5f5f5' },
    nostalgic: { p:'#a07cd8', s:'#f5b5d4', a:'#ffd700', light:'#f4ecff' },
  };
  const DEFAULT_THEME = { p:'#555', s:'#383838', a:'#a07cd8', light:'#f5f5f5' };

  /* =========================
     THÈMES NEOPETS — AUTONOME (pas de dépendance à ① CORE ni ③ BG).
     Chaque entrée : id (folder name H5), label (display name), ext (extension
     des icônes : .svg ou .png varie par thème), iconPrefix (sous-chemin optionnel
     dans /images/, ex: "v3/" pour basic). Liste maintenue manuellement — si
     un thème H5 sort, on l'ajoute ici. Le thème par défaut (utilisé pour les
     cards SANS override per-pet) est stocké en LS et modifiable via le bouton
     carousel : la 1ère card sur laquelle on clique définit aussi le défaut.
  ========================= */
  const LOCAL_THEMES = [
    { id:'winterholiday',     label:'Winter Holiday',     ext:'png' },
    { id:'basic',             label:'Basic',              ext:'svg', iconPrefix:'v3/' },
    { id:'premium',           label:'Premium',            ext:'svg' },
    { id:'altadorcup',        label:'Altador Cup',        ext:'png' },
    { id:'constellations',    label:'Constellations',     ext:'svg' },
    { id:'birthday',          label:'Birthday',           ext:'png' },
    { id:'destroyedfestival', label:'Faerie Festival',    ext:'svg' },
    { id:'neggs',             label:'Festival of Neggs',  ext:'svg' },
    { id:'grey',              label:'Grey Day',           ext:'png' },
    { id:'newyears',          label:'New Year',           ext:'png' },
    { id:'hauntedwoods',      label:'Haunted Woods',      ext:'svg' },
    { id:'meridell',          label:'Meridell',           ext:'svg' },
    { id:'mysteryisland',     label:'Mystery Island',     ext:'png' },
    { id:'neopiantimes',      label:'Neopian Times',      ext:'svg' },
    { id:'neggsneovia',       label:'Neovian Neggs',      ext:'svg' },
    { id:'tistheseason',      label:'Tis the Season',     ext:'png' },
    { id:'tyrannia',          label:'Tyrannia',           ext:'svg' },
    { id:'valentines',        label:'Valentines',         ext:'svg' },
  ];
  const FALLBACK_THEME = 'altadorcup';
  const QR_DEFAULT_THEME_LS = 'jb_np_quickref_defaultTheme_v1';
  function getDefaultTheme(){
    try {
      const v = localStorage.getItem(QR_DEFAULT_THEME_LS);
      if (v && LOCAL_THEMES.some(t => t.id === v)) return v;
    } catch {}
    return FALLBACK_THEME;
  }
  function setDefaultTheme(theme){
    try { localStorage.setItem(QR_DEFAULT_THEME_LS, theme); } catch {}
  }
  /* Résout l'URL d'une icône (ex: 'mypets', 'petcentral') pour un thème donné.
     Utilise la liste embarquée LOCAL_THEMES pour gérer les variations
     d'extension/sous-chemin par thème. */
  function localIconUrl(iconName, themeId){
    const t = LOCAL_THEMES.find(x => x.id === themeId)
           || LOCAL_THEMES.find(x => x.id === FALLBACK_THEME);
    const ext    = (t && t.ext) || 'svg';
    const prefix = (t && t.iconPrefix) || '';
    return `https://images.neopets.com/themes/h5/${themeId}/images/${prefix}${iconName}-icon.${ext}`;
  }
  // Alias historique — utilisé par effectivePetTheme comme fallback "thème courant"
  function getCurrentNeoTheme(){ return getDefaultTheme(); }
  /* ============================================================
     ASSET PROBING — les thèmes H5 utilisent des noms variables
     (ex: pattern-header.png OU header-pattern.png OU .svg).
     On teste plusieurs candidats par asset et on garde le 1er
     qui répond OK. Cache LS par (theme, asset).
  ============================================================ */
  const ASSET_CACHE_LS = 'jb_np_assetcache_v4';
  const ASSET_CANDIDATES = {
    patternHeader: ['pattern-header.png', 'pattern-header.svg', 'header-pattern.png', 'pattern-header.gif'],
    bgPattern:     ['bg-pattern.png', 'bg-pattern.svg', 'pattern.png', 'background-pattern.png', 'background.png'],
    navBottom:     ['nav-pattern-bottom.svg', 'nav-bottom-pattern.svg', 'nav-pattern-bottom.png', 'nav-bottom.svg'],
    footerPattern: ['footer-pattern.png', 'pattern-footer.png', 'footer-pattern.svg', 'pattern-footer.svg', 'nav-pattern-top.svg'],
    hpNameplate:   ['hp-nameplate.svg', 'hp-nameplate.png', 'nameplate.svg', 'nameplate.png'],
    hpBgTop:       ['hp-bg-top.png', 'hp-bg-top.svg', 'hp-bg.png'],
    hpBgBottom:    ['hp-bg-bottom.png', 'hp-bg-bottom.svg', 'hp-bg-bot.png'],
    carouselArrow: ['carouselarrow-right.svg', 'carouselarrow-right.png', 'carousel-arrow-right.svg', 'arrow-right.svg'],
  };
  function loadAssetCache(){
    try { return JSON.parse(localStorage.getItem(ASSET_CACHE_LS) || '{}') || {}; }
    catch { return {}; }
  }
  function saveAssetCache(c){
    try { localStorage.setItem(ASSET_CACHE_LS, JSON.stringify(c)); } catch {}
  }
  function imageLoads(url){
    return new Promise(resolve => {
      let done = false;
      const img = new Image();
      const finish = (ok) => { if (done) return; done = true; resolve(ok); };
      img.onload  = () => finish(true);
      img.onerror = () => finish(false);
      img.src = url;
      setTimeout(() => finish(false), 4500);
    });
  }
  async function resolveAsset(theme, key){
    const cache = loadAssetCache();
    const ck = `${theme}/${key}`;
    if (ck in cache) return cache[ck] || null;
    const base = `https://images.neopets.com/themes/h5/${theme}/images`;
    for (const name of ASSET_CANDIDATES[key]){
      const url = `${base}/${name}`;
      if (await imageLoads(url)){
        cache[ck] = url;
        saveAssetCache(cache);
        return url;
      }
    }
    cache[ck] = '';
    saveAssetCache(cache);
    return null;
  }
  function setCssUrl(varName, url){
    const v = url ? `url('${url}')` : 'none';
    document.documentElement.style.setProperty(varName, v);
  }

  function applyGlobalNeoTheme(){
    const theme = getCurrentNeoTheme();
    const base  = `https://images.neopets.com/themes/h5/${theme}/images`;
    const root  = document.documentElement.style;
    root.setProperty('--neo-theme', theme);

    // === Pose immédiate du candidat #1 (rendu rapide), puis probe async pour upgrade ===
    setCssUrl('--neo-nameplate',      `${base}/${ASSET_CANDIDATES.hpNameplate[0]}`);
    setCssUrl('--neo-hp-bg-top',      `${base}/${ASSET_CANDIDATES.hpBgTop[0]}`);
    setCssUrl('--neo-hp-bg-bot',      `${base}/${ASSET_CANDIDATES.hpBgBottom[0]}`);
    setCssUrl('--neo-pattern-header', `${base}/${ASSET_CANDIDATES.patternHeader[0]}`);
    setCssUrl('--neo-bg-pattern',     `${base}/${ASSET_CANDIDATES.bgPattern[0]}`);
    setCssUrl('--neo-nav-bottom',     `${base}/${ASSET_CANDIDATES.navBottom[0]}`);
    setCssUrl('--neo-footer-pattern', `${base}/${ASSET_CANDIDATES.footerPattern[0]}`);
    setCssUrl('--neo-carousel-arrow', `${base}/${ASSET_CANDIDATES.carouselArrow[0]}`);

    // === Probe async chaque asset, met à jour le var quand le bon candidat est trouvé ===
    resolveAsset(theme, 'hpNameplate')  .then(u => setCssUrl('--neo-nameplate', u));
    resolveAsset(theme, 'hpBgTop')      .then(u => setCssUrl('--neo-hp-bg-top',      u));
    resolveAsset(theme, 'hpBgBottom')   .then(u => setCssUrl('--neo-hp-bg-bot',      u));
    resolveAsset(theme, 'patternHeader').then(u => setCssUrl('--neo-pattern-header', u));
    resolveAsset(theme, 'bgPattern')    .then(u => setCssUrl('--neo-bg-pattern',     u));
    resolveAsset(theme, 'navBottom')    .then(u => setCssUrl('--neo-nav-bottom',     u));
    resolveAsset(theme, 'footerPattern').then(u => setCssUrl('--neo-footer-pattern', u));
    resolveAsset(theme, 'carouselArrow').then(u => setCssUrl('--neo-carousel-arrow', u));

    // === Icônes : résolution locale (autonome) via LOCAL_THEMES ===
    setCssUrl('--neo-mypets-icon', localIconUrl('mypets',     theme));
    setCssUrl('--neo-corner-icon', localIconUrl('petcentral', theme));

    document.documentElement.dataset.jbNeoTheme = theme;
  }
  // ⑧ QUICKREF est autonome : pas d'écoute des events de ① CORE / ③ BG.
  // Le thème par défaut est lu depuis LS (QR_DEFAULT_THEME_LS). Boot direct.
  applyGlobalNeoTheme();

  /* ============================================================
     THÈME PER-PET — chaque pet card peut avoir son propre thème
     stocké en LS (jb_np_petTheme_<petName>). Override les CSS
     vars sur le petDiv → cascade vers les descendants seulement.
  ============================================================ */
  const PET_THEME_LS_PREFIX = 'jb_np_petTheme_';
  function getPetThemeOverride(petName){
    try { return localStorage.getItem(PET_THEME_LS_PREFIX + petName); }
    catch { return null; }
  }
  function setPetThemeOverride(petName, theme){
    try {
      if (theme) localStorage.setItem(PET_THEME_LS_PREFIX + petName, theme);
      else localStorage.removeItem(PET_THEME_LS_PREFIX + petName);
    } catch {}
  }
  function effectivePetTheme(petName){
    return getPetThemeOverride(petName) || getCurrentNeoTheme();
  }
  /* Applique les CSS vars du thème courant (per-pet OU global) sur le petDiv.
     Cascade : les enfants utilisent ces vars en priorité sur celles du :root. */
  function applyPetCardTheme(petDiv, petName){
    const theme = effectivePetTheme(petName);
    const base  = `https://images.neopets.com/themes/h5/${theme}/images`;
    const setVar = (n, url) => petDiv.style.setProperty(n, url ? `url('${url}')` : 'none');
    // Pose immédiate des candidats #1
    setVar('--neo-nameplate',      `${base}/${ASSET_CANDIDATES.hpNameplate[0]}`);
    setVar('--neo-hp-bg-top',      `${base}/${ASSET_CANDIDATES.hpBgTop[0]}`);
    setVar('--neo-hp-bg-bot',      `${base}/${ASSET_CANDIDATES.hpBgBottom[0]}`);
    setVar('--neo-pattern-header', `${base}/${ASSET_CANDIDATES.patternHeader[0]}`);
    setVar('--neo-bg-pattern',     `${base}/${ASSET_CANDIDATES.bgPattern[0]}`);
    setVar('--neo-nav-bottom',     `${base}/${ASSET_CANDIDATES.navBottom[0]}`);
    setVar('--neo-footer-pattern', `${base}/${ASSET_CANDIDATES.footerPattern[0]}`);
    setVar('--neo-carousel-arrow', `${base}/${ASSET_CANDIDATES.carouselArrow[0]}`);
    // Icône mypets : résolution locale (autonome)
    setVar('--neo-mypets-icon', localIconUrl('mypets', theme));
    // Probe async → upgrade au vrai bon candidat
    resolveAsset(theme, 'hpNameplate')  .then(u => setVar('--neo-nameplate',      u));
    resolveAsset(theme, 'hpBgTop')      .then(u => setVar('--neo-hp-bg-top',      u));
    resolveAsset(theme, 'hpBgBottom')   .then(u => setVar('--neo-hp-bg-bot',      u));
    resolveAsset(theme, 'patternHeader').then(u => setVar('--neo-pattern-header', u));
    resolveAsset(theme, 'bgPattern')    .then(u => setVar('--neo-bg-pattern',     u));
    resolveAsset(theme, 'navBottom')    .then(u => setVar('--neo-nav-bottom',     u));
    resolveAsset(theme, 'footerPattern').then(u => setVar('--neo-footer-pattern', u));
    resolveAsset(theme, 'carouselArrow').then(u => setVar('--neo-carousel-arrow', u));
    // Attribut pour les CSS overrides dark-theme per-card
    petDiv.dataset.jbNeoTheme = theme;
  }
  /* Cycle thèmes pour cette carte (via la liste embarquée LOCAL_THEMES — autonome).
     dir = +1 (suivant) ou -1 (précédent). On en profite pour adopter ce thème
     comme nouveau défaut quickref (pratique : la dernière flèche cliquée pour
     une card devient le thème par défaut des futures cards sans override). */
  function cyclePetTheme(petDiv, petName, dir){
    const themes = LOCAL_THEMES.map(t => t.id);
    if (!themes.length) return;
    const current = effectivePetTheme(petName);
    const idx = themes.indexOf(current);
    const n = themes.length;
    const next = themes[((idx + dir) % n + n) % n];
    setPetThemeOverride(petName, next);
    setDefaultTheme(next);
    applyPetCardTheme(petDiv, petName);
  }
  /* Boutons "carousel arrow" left + right en bas de chaque pet card */
  function addThemeCarouselButton(petDiv, petName){
    if (petDiv.querySelector('.jbThemeCarousel')) return;
    const mkBtn = (cls, dir, title) => {
      const b = document.createElement('button');
      b.className = 'jbThemeCarousel ' + cls;
      b.type = 'button';
      b.title = title;
      b.addEventListener('click', (ev) => {
        ev.preventDefault();
        ev.stopPropagation();
        cyclePetTheme(petDiv, petName, dir);
      });
      b.addEventListener('contextmenu', (ev) => {
        ev.preventDefault();
        setPetThemeOverride(petName, null);
        applyPetCardTheme(petDiv, petName);
      });
      return b;
    };
    petDiv.appendChild(mkBtn('jbThemeCarouselLeft',  -1, 'Thème précédent (right-click = reset)'));
    petDiv.appendChild(mkBtn('jbThemeCarouselRight', +1, 'Thème suivant (right-click = reset)'));
  }

  function getColourTheme(colour){
    if (!colour) return DEFAULT_THEME;
    const key = colour.trim().toLowerCase().replace(/[\s_\-]+/g, '');
    return COLOUR_THEMES[key] || DEFAULT_THEME;
  }
  /* Calcule la luminance d'un hex pour choisir texte clair/foncé en surimpression */
  function isLightColor(hex){
    const r = parseInt(hex.slice(1,3), 16);
    const g = parseInt(hex.slice(3,5), 16);
    const b = parseInt(hex.slice(5,7), 16);
    return (0.299 * r + 0.587 * g + 0.114 * b) / 255 > 0.62;
  }
  function setThemeVars(petDiv, theme){
    const textOnPrimary = isLightColor(theme.p) ? '#2d2d2d' : '#fff';
    petDiv.style.setProperty('--pet-primary',   theme.p);
    petDiv.style.setProperty('--pet-secondary', theme.s);
    petDiv.style.setProperty('--pet-accent',    theme.a);
    petDiv.style.setProperty('--pet-light',     theme.light);
    petDiv.style.setProperty('--pet-text',      textOnPrimary);
  }
  /* Convert helpers */
  function rgbToHex({r,g,b}){
    return '#' + [r,g,b].map(n => Math.max(0,Math.min(255,Math.round(n))).toString(16).padStart(2,'0')).join('');
  }
  function lighten(rgb, amt){ return { r: rgb.r + (255-rgb.r)*amt, g: rgb.g + (255-rgb.g)*amt, b: rgb.b + (255-rgb.b)*amt }; }
  function darken(rgb, amt){  return { r: rgb.r * (1-amt), g: rgb.g * (1-amt), b: rgb.b * (1-amt) }; }
  /* Construit un thème (p/s/a/light) à partir d'un RGB dominant */
  function themeFromRgb(rgb){
    if (!rgb) return null;
    return {
      p:     rgbToHex(rgb),
      s:     rgbToHex(darken(rgb, 0.25)),
      a:     rgbToHex(lighten(rgb, 0.60)),
      light: rgbToHex(lighten(rgb, 0.75)),  // moins blanc → fond plus visible
    };
  }
  /* Extrait la couleur dominante d'une image (avec cache localStorage par sci).
     Tente d'éviter blanc/noir/grayscale qui pollueraient la moyenne. */
  /* Extracteur amélioré : center-weight (le pet est au centre, le background
     occupe les bords) + filtre saturation plus strict + score sat dominant. */
  function pickDominantColor(data, w, h){
    const cx = w / 2, cy = h / 2;
    const maxDist = Math.sqrt(cx*cx + cy*cy);
    const buckets = new Map();
    for (let y = 0; y < h; y++){
      for (let x = 0; x < w; x++){
        const i = (y * w + x) * 4;
        const a = data[i+3];
        if (a < 200) continue;
        const r = data[i], g = data[i+1], b = data[i+2];
        const max = Math.max(r,g,b), min = Math.min(r,g,b);
        const sat = max - min;
        if (max > 230 && sat < 30) continue;  // near-white / pale background
        if (max < 40) continue;               // near-black
        if (sat < 35) continue;               // grayscale = ombres / background
        // Center weight : pet = centre, ignore les bords (background)
        const dist = Math.sqrt((x-cx)*(x-cx) + (y-cy)*(y-cy));
        const cw = Math.max(0.15, 1 - (dist / maxDist) * 0.9);
        // Bucket plus fin (16 par canal au lieu de 8)
        const key = (Math.floor(r/16) << 12) | (Math.floor(g/16) << 6) | Math.floor(b/16);
        const cur = buckets.get(key) || { r:0, g:0, b:0, n:0 };
        cur.r += r * cw; cur.g += g * cw; cur.b += b * cw; cur.n += cw;
        buckets.set(key, cur);
      }
    }
    if (!buckets.size) return null;
    const entries = [...buckets.values()].sort((a,b) => b.n - a.n).slice(0, 8);
    let best = null, bestScore = 0;
    entries.forEach(b => {
      const r = b.r/b.n, g = b.g/b.n, bb = b.b/b.n;
      const sat = Math.max(r,g,bb) - Math.min(r,g,bb);
      // Score : count × saturation² (favorise très fort la saturation)
      const score = b.n * (sat * sat / 100 + 10);
      if (score > bestScore){
        bestScore = score;
        best = { r: Math.round(r), g: Math.round(g), b: Math.round(bb) };
      }
    });
    return best;
  }
  function extractPetImageColor(sci){
    return new Promise(resolve => {
      if (!sci) return resolve(null);
      const cacheKey = 'jb_np_petcolor_' + sci;
      try {
        const cached = JSON.parse(localStorage.getItem(cacheKey) || 'null');
        if (cached) return resolve(cached);
      } catch {}
      const img = new Image();
      img.crossOrigin = 'anonymous';
      let done = false;
      const finish = (rgb) => {
        if (done) return;
        done = true;
        if (rgb){
          try { localStorage.setItem(cacheKey, JSON.stringify(rgb)); } catch {}
        }
        resolve(rgb);
      };
      img.onload = () => {
        try {
          const SIZE = 100;
          const c = document.createElement('canvas');
          c.width = SIZE; c.height = SIZE;
          const ctx = c.getContext('2d');
          ctx.drawImage(img, 0, 0, SIZE, SIZE);
          const data = ctx.getImageData(0, 0, SIZE, SIZE).data;
          finish(pickDominantColor(data, SIZE, SIZE));
        } catch { finish(null); /* canvas CORS-tainted */ }
      };
      img.onerror = () => finish(null);
      setTimeout(() => finish(null), 5000);
      img.src = `https://pets.neopets.com/cp/${sci}/1/1.png`;
    });
  }
  /* Applique d'abord un thème de secours basé sur la Colour name, puis
     upgrade async au thème basé sur la couleur dominante de l'image. */
  function applyPetTheme(petDiv, colour, sci){
    if (petDiv.dataset.jbThemed === '1' && !sci) return;
    // Les assets Neopets (nameplate / bg-top / bg-bottom) sont posés en CSS
    // vars sur :root par applyGlobalNeoTheme() — global, pas per-pet.
    // 1) Fallback synchrone : couleur primary/secondary du pet (par Colour name)
    setThemeVars(petDiv, getColourTheme(colour));
    petDiv.dataset.jbThemed = '1';
    // 2) Upgrade async via image dominante (si sci dispo et pas déjà fait)
    if (sci && petDiv.dataset.jbImgThemed !== '1'){
      petDiv.dataset.jbImgThemed = 'pending';
      extractPetImageColor(sci).then(rgb => {
        const t = themeFromRgb(rgb);
        if (t){
          setThemeVars(petDiv, t);
          petDiv.dataset.jbImgThemed = '1';
        } else {
          petDiv.dataset.jbImgThemed = 'failed';
        }
      });
    }
  }

  /* =========================
     CSS — revamp page + widget stats
  ========================= */
  const CSS = `
    /* ============== Font Neopets ============== */
    @font-face {
      font-family: 'CafeteriaBlack';
      src: url('https://images.neopets.com/js/fonts/cafeteria-black.otf') format('opentype');
      font-display: swap;
    }

    /* ============== KILL parasites ============== */
    td.content > p:first-of-type,
    td.content > p:nth-of-type(2) { display:none !important; }
    td.content > div[style*="float: right"][style*="width: 310px"] { display:none !important; }
    td.content > br{ display:none !important; }

    /* ============== Sidebar ============== */
    td.sidebar { padding:0 16px 0 0 !important; vertical-align:top !important; }
    .sidebarModule{
      background:#fff !important;
      border:1px solid #d4d4d4 !important;
      border-radius:8px !important;
      overflow:hidden !important;
      box-shadow:0 1px 3px rgba(0,0,0,.08) !important;
      margin-bottom:10px !important;
    }
    .sidebarModule .sidebarTable{ width:100% !important; }
    .sidebarModule .sidebarHeader{
      background:linear-gradient(180deg, #555 0%, #383838 100%) !important;
      color:#fff !important;
      padding:7px 12px !important;
      font:800 11px/1.2 'CafeteriaBlack','Trebuchet MS',Verdana,sans-serif !important;
      text-transform:uppercase; letter-spacing:.06em;
      border-bottom:2px solid #1f1f1f !important;
      text-shadow:1px 1px 0 rgba(0,0,0,.45);
    }
    .sidebarModule .sidebarHeader a,
    .sidebarModule .sidebarHeader b{
      color:#fff !important; text-decoration:none !important;
    }
    .sidebarModule td{
      background:transparent !important;
      color:#2d2d2d !important;
      padding:6px 12px !important;
      border:none !important;
    }
    .sidebarModule .activePet{ padding:6px !important; text-align:center !important; }
    .sidebarModule .activePet img{
      border-radius:6px !important;
      border:2px solid #e5e5e5 !important;
      max-width:100% !important; height:auto !important;
      background:#fafafa !important;
    }
    .sidebarModule .activePet a{ color:#2d2d2d !important; }
    .sidebarModule .activePetInfo table{ margin:0 auto !important; }
    .sidebarModule .activePetInfo td{
      font-size:11px !important;
      padding:2px 4px !important;
    }
    .sidebarModule .activePetInfo td:first-child{
      color:#888 !important;
      text-align:right !important;
    }
    .sidebarModule .activePetInfo td:nth-child(2){
      color:#2d2d2d !important;
      font-weight:700 !important;
    }
    .sidebarModule font[color="yellow"] b{ color:#d4a900 !important; }
    .sidebarModule font[color="green"]  b{ color:#5ca817 !important; }
    .sidebarModule font[color="red"]    b{ color:#c1121f !important; }
    .sidebarModule .neofriend{
      padding:10px 12px !important;
      color:#666 !important;
      font-size:11px !important;
      text-align:center !important;
    }
    .sidebarModule input[type="text"]{
      background:#fff !important;
      border:1px solid #c8c8c8 !important;
      color:#2d2d2d !important;
      border-radius:4px !important;
      padding:5px 8px !important;
      font-size:12px !important;
    }
    .sidebarModule input[type="submit"]{
      background:linear-gradient(180deg, #6b6b6b 0%, #4a4a4a 100%) !important;
      color:#fff !important;
      border:1px solid #2d2d2d !important;
      border-radius:14px !important;
      padding:5px 14px !important;
      font:800 12px/1 'CafeteriaBlack','Trebuchet MS',sans-serif !important;
      text-shadow:1px 1px 0 rgba(0,0,0,.4) !important;
      cursor:pointer !important;
      margin-top:4px !important;
      box-shadow:inset 0 1px 0 rgba(255,255,255,.2), 0 1px 2px rgba(0,0,0,.18) !important;
    }
    .sidebarModule input[type="submit"]:hover{
      background:linear-gradient(180deg, #b8a0d8 0%, #8b6fbb 100%) !important;
      border-color:#5d3a8f !important;
    }
    .sidebarModule input[type="submit"]:hover{ filter:brightness(1.05); }

    /* ============== Pet nav (top thumbnails) — neutre, sans BG ============== */
    #nav{
      margin:14px auto !important;
      float:none !important;
      display:flex !important;
      gap:10px !important;
      justify-content:center !important;
      flex-wrap:wrap !important;
      padding:10px 14px !important;
      border:none !important;
      box-shadow:none !important;
      background:transparent !important;
      position:relative !important;
      z-index:2 !important;
    }
    #nav tbody, #nav tr, #nav td{
      all:revert;
      display:block !important;
      background:transparent !important;
    }
    #nav tr{ display:flex !important; gap:10px !important; flex-wrap:wrap !important; justify-content:center !important; }
    #nav td{ padding:0 !important; position:relative !important; }
    #nav td .pet_toggler img{
      width:56px !important; height:56px !important;
      border-radius:8px !important;
      border:2px solid #d4d4d4 !important;
      transition:transform .14s ease, border-color .14s ease, box-shadow .14s ease !important;
      cursor:pointer !important;
      background-size:cover !important;
      background-position:center !important;
      background-color:#fafafa !important;
    }
    #nav td.active_pet .pet_toggler img{
      border-color:#a07cd8 !important;
      box-shadow:0 0 0 2px rgba(160,124,216,.35) !important;
    }
    #nav td .pet_toggler:hover img{
      transform:translateY(-2px) !important;
      border-color:#666 !important;
    }
    #nav td .pet_menu_launcher{
      display:block !important;
      width:14px !important; height:14px !important;
      position:absolute !important;
      right:-3px; bottom:-3px;
      background:#fff !important;
      border:1px solid #a07cd8 !important;
      border-radius:50% !important;
      cursor:pointer !important;
      transition:transform .14s !important;
    }
    #nav td .pet_menu_launcher::before{
      content:'⋯';
      display:flex; align-items:center; justify-content:center;
      width:100%; height:100%;
      color:#a07cd8; font-size:11px; font-weight:900; line-height:1;
    }
    #nav td .pet_menu_launcher:hover{ background:#a07cd8 !important; }
    #nav td .pet_menu_launcher:hover::before{ color:#fff; }
    #nav td ul.pet_menu_hide{ display:none !important; }
    #nav td ul[id$="_menu"]{
      position:absolute !important;
      top:62px !important; left:50% !important;
      transform:translateX(-50%) !important;
      background:#fff !important;
      border:1px solid #d4d4d4 !important;
      border-radius:6px !important;
      padding:4px !important;
      list-style:none !important;
      margin:0 !important;
      min-width:170px !important;
      z-index:1000 !important;
      box-shadow:0 4px 12px rgba(0,0,0,.16) !important;
    }
    #nav td ul[id$="_menu"] li{
      list-style:none !important;
      padding:5px 9px !important;
      border-radius:4px !important;
      font:600 12px/1.3 'Trebuchet MS',Verdana,sans-serif !important;
      color:#444 !important;
      cursor:pointer !important;
    }
    #nav td ul[id$="_menu"] li:hover{
      background:#FFD800 !important;
      color:#3a2c00 !important;
    }
    #nav td ul[id$="_menu"] li a{
      color:inherit !important;
      text-decoration:none !important;
      display:block !important;
    }
    #nav td ul[id$="_menu"] li span.pointer{ display:none !important; }

    /* ============== Pet card (contentModule) — fond neutre, zones internes stylées ============== */
    div[id$="_details"].contentModule{
      background-color:#fafafa !important;
      border:3px solid #000 !important;
      border-radius:12px !important;
      box-shadow:0 6px 20px rgba(0,0,0,.18),
                 inset 0 0 0 1px rgba(255,255,255,.5) !important;
      overflow:hidden !important;
      margin:28px 0 !important; /* + d'espace entre les cartes de pets */
      position:relative !important;
    }
    .contentModuleTable{
      width:100% !important;
      border-collapse:separate !important;
      border-spacing:0 !important;
    }
    /* ============== Bannière (header) ==============
       2 BG layered :
         - nav-pattern-bottom collé en haut (strip horizontal, repeat-x)
         - pattern-header en image unique, NON répété verticalement (1× rempli en height)
       Tout le texte en noir. Bord noir bas pour cohérence carte. */
    .contentModule th.contentModuleHeader,
    .contentModule th.contentModuleHeaderAlt{
      background-color:#fff !important;
      background-image:var(--neo-pattern-header, none) !important;
      background-repeat:repeat-x !important;
      background-position:center center !important;
      background-size:auto 100% !important;
      color:#000 !important;
      padding:18px 380px 18px 22px !important; /* HP@right:150 (210 wide) + switcher@right:8 (~100 wide) + buffer */
      font:800 28px/1.1 'CafeteriaBlack','Trebuchet MS',Verdana,sans-serif !important;
      text-align:left !important;
      border:none !important;
      border-bottom:3px solid #000 !important;
      display:flex !important;
      flex-direction:row !important;
      flex-wrap:nowrap !important;
      align-items:center !important;
      gap:14px !important;
      letter-spacing:.03em;
      text-shadow:none !important;
      position:relative !important;
      min-height:0;
      white-space:nowrap !important;
      overflow:hidden !important;
    }
    /* Nom du pet en grand */
    .contentModule th a{
      font-size:30px !important;
      letter-spacing:.03em;
      flex-shrink:0;
    }
    .contentModule th a{
      color:#000 !important;
      text-decoration:none !important;
    }
    .contentModule th a:hover{ color:#000 !important; text-decoration:underline !important; }
    /* === Override texte BLANC pour les thèmes au pattern-header sombre ===
       Marche pour le thème global ([data-jb-neo-theme] sur <html>) ET pour
       l'override per-pet (data-jb-neo-theme sur .contentModule). */
    .contentModule[data-jb-neo-theme="hauntedwoods"] th,
    .contentModule[data-jb-neo-theme="hauntedwoods"] th a,
    .contentModule[data-jb-neo-theme="hauntedwoods"] .jbHeaderHp .hpLabel,
    .contentModule[data-jb-neo-theme="constellations"] th,
    .contentModule[data-jb-neo-theme="constellations"] th a,
    .contentModule[data-jb-neo-theme="constellations"] .jbHeaderHp .hpLabel,
    .contentModule[data-jb-neo-theme="destroyedfestival"] th,
    .contentModule[data-jb-neo-theme="destroyedfestival"] th a,
    .contentModule[data-jb-neo-theme="destroyedfestival"] .jbHeaderHp .hpLabel,
    .contentModule[data-jb-neo-theme="grey"] th,
    .contentModule[data-jb-neo-theme="grey"] th a,
    .contentModule[data-jb-neo-theme="grey"] .jbHeaderHp .hpLabel,
    .contentModule[data-jb-neo-theme="neggsneovia"] th,
    .contentModule[data-jb-neo-theme="neggsneovia"] th a,
    .contentModule[data-jb-neo-theme="neggsneovia"] .jbHeaderHp .hpLabel,
    .contentModule[data-jb-neo-theme="tyrannia"] th,
    .contentModule[data-jb-neo-theme="tyrannia"] th a,
    .contentModule[data-jb-neo-theme="tyrannia"] .jbHeaderHp .hpLabel,
    .contentModule[data-jb-neo-theme="winterholiday"] th,
    .contentModule[data-jb-neo-theme="winterholiday"] th a,
    .contentModule[data-jb-neo-theme="winterholiday"] .jbHeaderHp .hpLabel,
    .contentModule[data-jb-neo-theme="newyears"] th,
    .contentModule[data-jb-neo-theme="newyears"] th a,
    .contentModule[data-jb-neo-theme="newyears"] .jbHeaderHp .hpLabel,
    .contentModule[data-jb-neo-theme="tistheseason"] th,
    .contentModule[data-jb-neo-theme="tistheseason"] th a,
    .contentModule[data-jb-neo-theme="tistheseason"] .jbHeaderHp .hpLabel{
      color:#fff !important;
      text-shadow:1px 1px 0 rgba(0,0,0,.55) !important;
    }
    .contentModule[data-jb-neo-theme="hauntedwoods"] .jbHeaderAvatar,
    .contentModule[data-jb-neo-theme="constellations"] .jbHeaderAvatar,
    .contentModule[data-jb-neo-theme="destroyedfestival"] .jbHeaderAvatar,
    .contentModule[data-jb-neo-theme="grey"] .jbHeaderAvatar,
    .contentModule[data-jb-neo-theme="neggsneovia"] .jbHeaderAvatar,
    .contentModule[data-jb-neo-theme="tyrannia"] .jbHeaderAvatar,
    .contentModule[data-jb-neo-theme="winterholiday"] .jbHeaderAvatar,
    .contentModule[data-jb-neo-theme="newyears"] .jbHeaderAvatar,
    .contentModule[data-jb-neo-theme="tistheseason"] .jbHeaderAvatar{
      filter:drop-shadow(0 1px 1px rgba(0,0,0,.6));
    }
    /* Icône mypets du thème — flex item normal, centré vertical avec le nom */
    .jbHeaderAvatar{
      position:static !important;
      display:inline-block !important;
      width:52px; height:52px;
      background-image:var(--neo-mypets-icon, url('https://images.neopets.com/themes/h5/basic/images/v3/mypets-icon.svg'));
      background-size:contain;
      background-repeat:no-repeat;
      background-position:center;
      flex-shrink:0;
      flex-grow:0;
      pointer-events:none;
      vertical-align:middle;
      margin:0;
      transform:none !important;
      left:auto !important;
      top:auto !important;
    }
    /* HP bar Pokemon-style — positionné en absolute en haut à droite (sous le strip nav)
       pour libérer le flex flow horizontal (nom / étiquette / petpets en colonnes).
       Décalé à gauche pour respirer entre le contenu et le switcher. */
    .jbHeaderHp{
      position:absolute;
      right:150px;
      top:50%;
      transform:translateY(-50%);
      display:flex; flex-direction:column;
      align-items:flex-end;
      gap:3px;
      width:210px;
    }
    .jbHeaderHp .hpLabel{
      font:800 13px/1 'CafeteriaBlack','Trebuchet MS',Verdana,sans-serif;
      color:#000;
      text-shadow:none;
      letter-spacing:.04em;
    }
    /* Le MAX (total HP) est mis en avant : plus gros et bold.
       Le CUR (HP restants) et le séparateur sont fadés → l'attention va
       sur la valeur totale. */
    .jbHeaderHp .hpLabel .hpMax{
      font-size:22px;
      vertical-align:baseline;
      font-weight:800;
    }
    .jbHeaderHp .hpLabel .hpCur{
      font-size:14px;
      opacity:.55;
      vertical-align:baseline;
      font-weight:700;
    }
    .jbHeaderHp .hpLabel .hpSep{
      opacity:.45;
      vertical-align:baseline;
    }
    .jbHeaderHp .hpBar{
      width:100%;
      height:9px;
      background:rgba(0,0,0,.4);
      border-radius:5px;
      overflow:hidden;
      border:1px solid rgba(0,0,0,.5);
      box-shadow:inset 0 1px 2px rgba(0,0,0,.4);
    }
    .jbHeaderHp .hpFill{
      height:100%;
      background:linear-gradient(180deg, #8de24a 0%, #5ca817 100%);
      transition:width .3s ease;
    }
    .jbHeaderHp.warn .hpFill{ background:linear-gradient(180deg, #ffd84a 0%, #f5a623 100%); }
    .jbHeaderHp.danger .hpFill{ background:linear-gradient(180deg, #ff6b6b 0%, #c1121f 100%); }
    /* Étiquette à côté du nom (même ligne) : cadre semi-transparent simple */
    .jbHeaderChips{
      display:inline-flex !important;
      align-items:center;
      justify-content:flex-start;
      gap:10px;
      padding:5px 14px !important;
      margin:0;
      min-height:0;
      width:auto;
      flex-shrink:0;
      background:rgba(255,255,255,.55) !important;
      border:1.5px solid #000 !important;
      border-radius:14px !important;
      box-shadow:0 1px 2px rgba(0,0,0,.18) !important;
    }
    .jbChip{
      display:inline-block;
      padding:0;
      background:transparent;
      border:none;
      border-radius:0;
      font:700 11px/1.4 'Trebuchet MS',Verdana,sans-serif;
      color:#000;
      text-transform:uppercase;
      letter-spacing:.06em;
      vertical-align:middle;
      text-shadow:none;
      box-shadow:none;
    }
    .jbChip + .jbChip{
      padding-left:10px;
      border-left:1px solid rgba(0,0,0,.45);
    }
    .jbHeaderSub{
      align-self:flex-start;
      font:400 11px/1.3 'Trebuchet MS',Verdana,sans-serif;
      color:#000;
      letter-spacing:.02em;
      margin-top:2px;
      text-shadow:none;
    }
    /* Petpet / Petpetpet en sous-ligne dans la bannière (texte noir) */
    .jbHeaderPetpets{
      align-self:flex-start;
      display:flex;
      flex-wrap:wrap;
      gap:8px;
      margin-top:2px;
      font:700 11px/1.2 'Trebuchet MS',Verdana,sans-serif;
      color:#000;
      text-shadow:none;
    }
    .jbHeaderPetpets .pp{
      display:inline-flex;
      align-items:center;
      gap:6px;
      padding:2px 10px 2px 4px;
      background:rgba(255,255,255,.78);
      border:1px solid #000;
      border-radius:14px;
    }
    .jbHeaderPetpets .pp .ppLbl{
      font:700 8px/1 'Trebuchet MS',Verdana,sans-serif;
      text-transform:uppercase;
      letter-spacing:.08em;
      opacity:.65;
      color:#000;
    }
    .jbHeaderPetpets .pp img{
      width:22px; height:22px;
      border-radius:50%;
      background:#fff;
      padding:1px;
      border:1px solid #000;
      flex-shrink:0;
    }
    /* === Zone info générale : 3 colonnes égales avec BG Background Top du thème === */
    .contentModuleTable > tbody > tr > td{
      padding:18px !important;
      vertical-align:top !important;
      border:none !important;
      border-bottom:3px solid #000 !important;
      display:grid !important;
      gap:18px !important; /* aligné sur le padding du td = espacements harmonisés (padding extérieur = gap inter-cols) */
      grid-template-columns:repeat(3, minmax(0, 1fr)) !important;
      grid-template-areas:"image info notices" !important;
      grid-auto-rows:1fr !important;
      align-items:stretch !important;
      font-family: 'Trebuchet MS', Verdana, sans-serif !important;
      color:#000 !important;
      background-color:#f5efe1 !important;
      background-image:var(--neo-hp-bg-top, none) !important;
      background-repeat:repeat-x !important;
      background-position:top center !important;
      background-size:auto 100% !important;
    }
    .pet_info{
      grid-area:info;
      float:none !important;
      width:auto !important;
      height:100% !important;
      margin:0 !important;
      align-self:stretch !important;
      min-width:0;
      display:flex !important;
      flex-direction:column !important;
      gap:0 !important;
    }
    /* Reset au cas où le margin-bottom legacy traîne encore */
    .pet_info > *{ margin-bottom:0 !important; }
    /* Col 1 : image du pet — cadre noir + bords arrondis, cover pour remplir
       sans marges blanches L/R (la scène du pet remplit toute la case) */
    .pet_image{
      grid-area:image;
      float:none !important;
      width:100% !important;
      max-width:100% !important;
      min-height:0 !important;
      aspect-ratio:1 / 1 !important; /* carrée dans les 2 modes (profil + log) */
      background-size:cover !important;
      background-repeat:no-repeat !important;
      background-position:center !important;
      height:auto !important;
      margin:0 !important;
      position:relative !important;
      overflow:hidden !important;
      border-radius:8px !important;
      background-color:transparent !important;
      border:2px solid #000 !important;
      box-shadow:none !important;
      align-self:start !important; /* override le stretch hérité — sinon aspect-ratio ignoré */
      box-sizing:border-box !important;
    }
    /* Col 3 : notices — contour noir, fond blanc semi-transparent.
       Flex column pour que le bloc Total gain (dernier child) soit ancré en
       bas via margin-top:auto → même position visuelle qu'en mode log (où
       le gain est aussi en bas de la col 3, sous la carte stats). */
    .pet_more{
      grid-area:notices;
      align-self:stretch !important;
      display:flex !important;
      flex-direction:column !important;
      width:100% !important;
      max-width:100% !important;
      box-sizing:border-box !important;
      height:auto !important;
      min-height:300px !important;
      margin:0 !important;
      padding:14px 14px !important;
      background:rgba(255,255,255,.85) !important;
      border:2px solid #000 !important;
      border-radius:8px !important;
      box-shadow:none !important;
      color:#000 !important;
      float:none !important;
      clear:none !important;
      overflow:hidden !important;
      position:relative !important;
    }
    /* Bloc gain en bas de pet_more grâce à margin-top:auto */
    .pet_more > .jbStatsGain.jbGainInNotices{
      margin-top:auto !important;
    }
    /* Bloc gain : même typo / même structure visuelle dans les 2 modes
       (parent = .pet_more en profile, ou .jbStatsSide en log). Séparateur
       dotted au-dessus pour cohérence avec les notices. */
    .jbStatsGain.jbGainInNotices{
      position:static !important;
      display:flex !important;
      flex-direction:column !important;
      align-items:center !important;
      margin:0 !important;
      padding:10px 0 6px !important;
      border-top:1px dotted rgba(0,0,0,.4) !important;
    }
    .jbStatsGain.jbGainInNotices .gainBreak{
      order:1 !important;  /* texte AVANT le bouton */
      width:100% !important;
      text-align:left !important;
      font:400 12px/1.45 'Trebuchet MS',Verdana,sans-serif !important;
      color:#000 !important;
      opacity:1 !important;
      margin:0 0 8px 0 !important;
      padding:0 !important;
    }
    .jbStatsGain.jbGainInNotices .gainBreak b{
      font-weight:700 !important;
    }
    .jbStatsGain.jbGainInNotices .gainBig{
      order:2 !important;
      margin:2px 0 0 0 !important;
    }
    /* Si pas de notices, on laisse la colonne en placeholder pour garder
       l'équilibre visuel 3 colonnes (pas de display:none) */
    .pet_more:has(.pet_notices:empty)::before{
      content:'Aucune notice';
      display:block;
      text-align:center;
      color:#aaa;
      font-style:italic;
      font-size:11px;
      padding:18px 0;
    }
    .pet_more:has(.pet_notices:empty) h3{ display:none !important; }
    /* Garde une taille mini correcte pour images dans les notices */
    .pet_notices .sf img{ max-width:100% !important; height:auto !important; }
    .pet_more h3{
      color:#000 !important; opacity:.7 !important;
      font:800 10px/1.2 'CafeteriaBlack','Trebuchet MS',Verdana,sans-serif !important;
      text-transform:uppercase; letter-spacing:.08em;
      margin:0 !important;
      padding:0 0 2px !important;
      background:transparent !important;
      display:block !important;
      border:none !important;
      text-align:left !important;
    }
    .pet_notices{
      display:block !important;
      width:auto !important;
      height:auto !important;
      min-height:0 !important;
      color:#000 !important;
      font-size:12px !important;
      line-height:1.4 !important;
      background:transparent !important;
    }
    /* Chaque .sf à l'intérieur = une notice. On vire toute image de fond
       pour ne garder que le texte (harmonisation). Séparateur dotted top sur
       chaque item, y compris le premier (= ligne juste sous le titre Notices). */
    .pet_notices .sf{
      display:block !important;
      width:auto !important;
      min-height:0 !important;
      background:none !important;
      background-image:none !important;
      background-color:transparent !important;
      padding:10px 0 !important;
      margin:0 !important;
      color:#000 !important;
      font:400 12px/1.45 'Trebuchet MS',Verdana,sans-serif !important;
      border-top:1px dotted rgba(0,0,0,.4) !important;
      box-sizing:border-box !important;
    }
    /* Le séparateur du premier .sf est conservé (= ligne juste sous le titre Notices) */
    /* Au cas où une img inline traîne dans une notice : neutralisée */
    .pet_notices .sf img{ display:none !important; }
    .pet_notices .sf b{ color:#000 !important; }
    .pet_notices .sf a{ color:#000 !important; font-weight:700 !important; text-decoration:underline !important; }
    .pet_notices .sf a:hover{ color:#000 !important; }
    .pet_notices .sf font[color="green"]{ color:#000 !important; }
    .pet_notices .sf font[color="red"]{ color:#000 !important; }
    /* Le spacer entre 2 notices (généré par Neopets) on le neutralise */
    .pet_notice_spacer{ display:none !important; }
    /* Le tableau original est complètement caché — on reconstruit en divs */
    table.pet_stats{ display:none !important; }

    /* ============== Stats grid (col 2) : liste verticale qui remplit toute la
       hauteur de la colonne (= hauteur pet image / notices). ============== */
    .jbStatsGrid{
      display:flex !important;
      flex-direction:column !important;
      gap:8px !important;
      width:100% !important;
      height:100% !important;
      flex:1 1 auto !important;
      box-sizing:border-box !important;
      margin:0 !important;
    }
    /* Stat card : flex:1 sur chaque pour distribuer l'espace vertical équitablement */
    .jbStatCard{
      display:flex;
      flex-direction:row;
      flex-wrap:wrap;
      align-items:center;
      gap:6px;
      background:rgba(255,255,255,.85);
      border:2px solid #000;
      border-radius:6px;
      padding:8px 14px;
      min-width:0;
      flex:1 1 0;
      box-sizing:border-box;
      box-shadow:none;
      color:#000;
      line-height:1.3;
    }
    .jbStatCard .jbStatLabel{
      color:#000;
      font:800 11px/1.2 'CafeteriaBlack','Trebuchet MS',Verdana,sans-serif;
      text-transform:uppercase; letter-spacing:.05em;
      opacity:.8;
      flex-shrink:0;
    }
    .jbStatCard .jbStatLabel::after{ content:' :'; }
    .jbStatCard .jbStatValue{
      color:#000;
      font:700 12px/1.3 'Trebuchet MS',Verdana,sans-serif;
      word-break:break-word;
      overflow-wrap:anywhere;
      flex:1 1 auto;
      min-width:0;
    }
    /* Images inline (petpet etc.) à taille raisonnable */
    .jbStatCard .jbStatValue img{
      vertical-align:middle;
      max-height:24px;
      width:auto;
      margin-right:3px;
    }
    /* Pas de saut de ligne intempestif sur les <br> Neopets dans la valeur */
    .jbStatCard .jbStatValue br{ display:none !important; }
    .jbStatCard .jbStatValue a{ color:#000; text-decoration:underline; }
    .jbStatCard .jbStatValue a:hover{ color:#000; }
    .jbStatCard font[color="yellow"] b,
    .jbStatCard font[color="green"]  b,
    .jbStatCard font[color="red"]    b{ color:#000; }
    /* Ancien wrapper (visit petpage) — gardé via .jbVisitOnly. Pas d'affichage par défaut */
    .jbPetpetCard{ display:none !important; }
    /* === Carte dédiée Petpet / Petpetpet : GROSSE case sous Fishing === */
    .jbPetpetBigCard{
      display:flex !important;
      flex-direction:column !important;
      gap:14px !important;
      width:100% !important;
      box-sizing:border-box !important;
      background:rgba(255,255,255,.92) !important;
      border:2px solid #000 !important;
      border-radius:10px !important;
      padding:16px 18px !important;
      margin:0 !important;
    }
    .jbPetpetBigCard .jbPetpetRow{
      display:flex !important;
      align-items:center !important;
      gap:14px !important;
      width:100% !important;
      min-height:0 !important;
      overflow:hidden !important;
    }
    .jbPetpetBigCard .jbPetpetRow + .jbPetpetRow{
      border-top:1px dashed rgba(0,0,0,.35) !important;
      padding-top:14px !important;
    }
    /* Image petpet : DIV (pas img) avec background-image → taille 100%
       contrôlée par le CSS, aucune chance de déborder car la div n'a pas
       de dimension intrinsèque comme un img. */
    .jbPetpetBigCard .jbPetpetImg{
      width:100px !important;
      height:100px !important;
      min-width:100px !important;
      min-height:100px !important;
      max-width:100px !important;
      max-height:100px !important;
      flex-shrink:0 !important;
      flex-grow:0 !important;
      background-color:#fff !important;
      background-repeat:no-repeat !important;
      background-position:center !important;
      background-size:contain !important;
      border:2px solid #000 !important;
      border-radius:10px !important;
      padding:6px !important;
      overflow:hidden !important;
      box-sizing:border-box !important;
      box-shadow:0 1px 3px rgba(0,0,0,.15) !important;
      /* Petit "inset" simulé : background-origin content-box pour que l'image
         respecte le padding (sinon elle passe sous le padding). */
      background-origin:content-box !important;
      background-clip:content-box !important;
    }
    .jbPetpetBigCard .jbPetpetNoImg{
      display:flex !important;
      align-items:center !important;
      justify-content:center !important;
      background-image:none !important;
      background-color:#f0f0f0 !important;
      color:#000;
      opacity:.5;
      font:800 40px/1 'CafeteriaBlack','Trebuchet MS',sans-serif;
    }
    .jbPetpetBigCard .jbPetpetMeta{
      display:flex;
      flex-direction:column;
      gap:4px;
      min-width:0;
      flex:1 1 auto;
    }
    .jbPetpetBigCard .jbPetpetLbl{
      font:800 12px/1.2 'CafeteriaBlack','Trebuchet MS',Verdana,sans-serif;
      text-transform:uppercase;
      letter-spacing:.08em;
      color:#000;
      opacity:.7;
    }
    .jbPetpetBigCard .jbPetpetName{
      font:800 19px/1.3 'CafeteriaBlack','Trebuchet MS',Verdana,sans-serif;
      color:#000;
      word-break:break-word;
      letter-spacing:.02em;
    }
    .jbPetpetCard.jbVisitOnly{
      display:flex !important;
      flex-wrap:wrap !important;
      align-items:center !important;
      gap:10px !important;
      width:100% !important;
      max-width:100% !important;
      box-sizing:border-box !important;
      background:rgba(255,255,255,.85) !important;
      border:2px solid #000 !important;
      border-radius:6px !important;
      padding:10px 14px !important;
      margin:0 !important;
      text-align:left !important;
      color:#000 !important;
      font:700 13px/1.4 'Trebuchet MS',Verdana,sans-serif !important;
      flex:1 1 0 !important;
    }
    .jbPetpetCard.jbVisitOnly b{ font-weight:800 !important; }
    .jbPetpetCard.jbVisitOnly div,
    .jbPetpetCard.jbVisitOnly b{
      display:inline !important;
      margin:0 !important;
    }
    .jbPetpetCard.jbVisitOnly br{ display:none !important; }
    .jbPetpetCard.jbVisitOnly img{
      vertical-align:middle !important;
      width:auto !important;
      height:auto !important;
      max-width:none !important;
      max-height:none !important;
      margin:0 4px 0 0 !important;
      display:inline-block !important;
      padding:0 !important;
      border:none !important;
      background:transparent !important;
    }
    .jbPetpetCard a{ color:#000 !important; text-decoration:underline !important; }
    .jbPetpetCard a:hover{ color:#000 !important; }
    /* Neutralise <div style="width:240px; overflow:hidden"> et <b> legacy */
    .jbPetpetCard b,
    .jbPetpetCard div{
      display:block !important;
      width:auto !important;
      max-width:100% !important;
      overflow:visible !important;
      margin:0 auto !important;
      padding:0 !important;
      font-weight:normal !important;
      text-align:center !important;
      float:none !important;
      position:static !important;
      background:transparent !important;
    }
    .jbPetpetCard img{
      width:54px !important; height:54px !important;
      margin:2px !important;
      border-radius:4px !important;
      background:#fff !important;
      padding:3px !important;
      border:1px solid #e5e5e5 !important;
      vertical-align:middle !important;
      display:inline-block !important;
    }
    /* Date de naissance (inline avec parenthèses, dans la valeur Age) */
    .jbBirthday{
      display:inline;
      font:400 11px/1.3 'Trebuchet MS',Verdana,sans-serif;
      color:#000; opacity:.7;
      margin:0 0 0 4px;
      font-style:italic;
    }

    /* ============== HP bar ============== */
    .jbHpBar{
      margin:0 0 8px !important;
      padding:7px 10px !important;
      background:rgba(255,255,255,.78) !important;
      border-radius:5px !important;
      border:1px solid rgba(0,0,0,.08) !important;
      border-left:3px solid #b59ad6 !important;
    }
    .jbHpBar .jbHpHead{
      display:flex; justify-content:space-between; align-items:baseline;
      font:700 9px/1.2 'Trebuchet MS',Verdana,sans-serif; color:#888;
      text-transform:uppercase; letter-spacing:.06em;
      margin-bottom:5px;
    }
    .jbHpBar .jbHpHead b{ color:#2d2d2d; font-size:13px; }
    .jbHpBar .jbHpTrack{
      height:10px; border-radius:5px;
      background:#e0e0e0;
      overflow:hidden;
      border:1px solid #d0d0d0;
    }
    .jbHpBar .jbHpFill{
      height:100%;
      background:linear-gradient(180deg, #8de24a 0%, #5ca817 100%);
      transition:width .3s ease;
    }
    .jbHpBar.warn .jbHpFill{ background:linear-gradient(180deg, #ffd84a 0%, #f5a623 100%); }
    .jbHpBar.danger .jbHpFill{ background:linear-gradient(180deg, #ff6b6b 0%, #c1121f 100%); }

    /* ============== Actions : footer centré avec BG footer-pattern (pas de gradient coloré) ============== */
    div[id$="_details"].contentModule > .jbPetActions{
      display:flex !important;
      flex-wrap:wrap !important;
      justify-content:center !important;
      align-items:center !important;
      gap:8px !important;
      margin:0 !important;
      padding:10px 20px !important; /* slim */
      border:none !important; /* le td au-dessus a déjà border-bottom:3px, pas besoin de double */
      background-color:#2d2d2d !important;
      background-image:var(--neo-footer-pattern, none) !important;
      background-repeat:repeat !important;
      background-position:center !important;
      background-size:auto !important;
      grid-area:auto !important;
    }
    .jbPetActions a{
      display:inline-flex; align-items:center; gap:6px;
      padding:7px 16px;
      background:#fff;
      border:2px solid #000;
      border-bottom-width:3px;
      border-radius:16px;
      color:#000 !important;
      text-decoration:none !important;
      font:800 11px/1.2 'CafeteriaBlack','Trebuchet MS',Verdana,sans-serif;
      letter-spacing:.02em;
      transition:all .12s ease;
      box-shadow:0 2px 0 rgba(0,0,0,.30), inset 0 1px 0 rgba(255,255,255,.6);
    }
    .jbPetActions a:hover{
      background:#000;
      color:#fff !important;
      text-shadow:none;
      transform:translateY(-1px);
      border-bottom-width:2px;
      box-shadow:0 3px 6px rgba(0,0,0,.35), inset 0 1px 0 rgba(255,255,255,.2);
    }
    .jbPetActions a:active{
      transform:translateY(1px);
      border-bottom-width:1px;
      box-shadow:0 1px 2px rgba(0,0,0,.15);
    }
    .jbPetActions a.disabled{
      opacity:.5; pointer-events:none;
      background:linear-gradient(180deg, #b8b8b8 0%, #999 100%);
      color:#fff !important;
      filter:saturate(0);
    }

    /* ============== Boutons "carousel arrow" left/right : cycle thème per-pet
                       — en bas gauche / bas droit de la carte, par-dessus le
                       footer actions. Flèches agrandies mais bounding box mini
                       pour ne pas casser le layout des actions. ============== */
    .jbThemeCarousel{
      position:absolute !important;
      bottom:6px !important;
      width:56px !important;
      height:56px !important;
      padding:0 !important;
      border:none !important;
      background-color:transparent !important;
      background-image:var(--neo-carousel-arrow, none) !important;
      background-repeat:no-repeat !important;
      background-position:center !important;
      background-size:contain !important;
      cursor:pointer !important;
      z-index:5 !important;
      filter:drop-shadow(0 2px 3px rgba(0,0,0,.6));
      transition:transform .12s ease, filter .12s ease;
      appearance:none !important;
    }
    .jbThemeCarouselRight{ right:8px !important; }
    /* Flèche left = même SVG flippé horizontalement */
    .jbThemeCarouselLeft{
      left:8px !important;
      transform:scaleX(-1);
    }
    .jbThemeCarouselRight:hover{
      transform:translateX(3px) scale(1.08);
    }
    .jbThemeCarouselLeft:hover{
      transform:scaleX(-1) translateX(3px) scale(1.08);
    }
    .jbThemeCarousel:hover{ filter:drop-shadow(0 3px 5px rgba(0,0,0,.75)); }
    .jbThemeCarouselRight:active{ transform:translateX(3px) scale(.96); }
    .jbThemeCarouselLeft:active{  transform:scaleX(-1) translateX(3px) scale(.96); }
    .jbThemeCarousel:focus{ outline:none !important; }

    /* ============== Heal overlay ============== */
    #jb-heal-overlay{
      position:absolute !important;
      inset:0 !important;
      z-index:9999 !important;
      display:block !important;
      cursor:pointer !important;
      border-radius:6px;
      overflow:hidden;
    }
    #jb-heal-overlay::after{
      content:'Need Cooling Ointment →';
      position:absolute;
      bottom:8px; left:50%;
      transform:translateX(-50%);
      background:#c1121f;
      color:#fff;
      padding:4px 12px;
      border-radius:11px;
      font:800 11px/1 'CafeteriaBlack','Trebuchet MS',sans-serif;
      text-transform:uppercase; letter-spacing:.05em;
      white-space:nowrap;
      box-shadow:0 2px 6px rgba(0,0,0,.3);
    }
    #jb-heal-overlay img{
      width:100% !important;
      height:100% !important;
      display:block !important;
      object-fit:cover !important;
      object-position:50% 0% !important;
    }

    /* ============== Widget BD stats ==============
       Le widget est maintenant un grid item du td parent (span sur toutes
       les colonnes en mode profile, OR display:contents en mode log pour
       que .jbStatsSide / .jbStatsMain deviennent eux-mêmes grid items du td).
       BG = Background Bottom (body-bg-bottom) du thème. */
    div[id$="_details"].contentModule .jbStatsWidget{
      grid-column:1 / -1;
      margin:0 !important;
      background-color:#f0eadb !important;
      background-image:var(--neo-hp-bg-bot, none) !important;
      background-repeat:repeat-x !important;
      background-position:bottom center !important;
      background-size:auto 100% !important;
      border:none !important;
      border-radius:0 !important;
      box-shadow:none !important;
      color:#000;
      font:13px/1.3 'Trebuchet MS',Verdana,sans-serif;
      overflow:visible;
      position:relative;
    }
    /* Override grid-auto-rows:1fr du td pour permettre la row 2 (widget)
       en hauteur naturelle. */
    .contentModuleTable > tbody > tr > td{
      grid-auto-rows:auto !important;
    }
    /* ============== View toggle (profile vs log icon) ==============
       Position absolute totalement à droite de la bannière (right:8px), la
       HP bar est décalée à right:150px → respire entre contenu et switcher.
       Hauteur calquée sur la bannière. Actif = fond blanc. */
    .jbViewToggle{
      position:absolute !important;
      right:8px;
      top:50%;
      transform:translateY(-50%);
      display:flex !important;
      flex-direction:row;
      gap:2px;
      align-items:stretch;
      padding:4px;
      background:rgba(255,255,255,.55) !important;
      border:1.5px solid #000 !important;
      border-radius:16px !important;
      box-shadow:0 1px 2px rgba(0,0,0,.18) !important;
      box-sizing:border-box;
      height:60px;
    }
    .jbViewToggle button{
      display:inline-flex;
      align-items:center;
      justify-content:center;
      width:44px;
      height:100%;
      padding:0;
      background:transparent;
      border:0;
      border-radius:12px;
      cursor:pointer;
      opacity:.45;
      transition:all .12s ease;
    }
    .jbViewToggle button:hover{ opacity:.9; }
    .jbViewToggle button.active{
      background:#fff;
      opacity:1;
      box-shadow:0 1px 2px rgba(0,0,0,.25);
    }
    .jbViewToggle button img{
      width:28px; height:28px;
      display:block;
      pointer-events:none;
    }
    /* ============== Mode PROFILE : cache complètement le widget BD stats ==============
       En profile mode on ne voit QUE image | info | notices.
       Le bloc Total gain (.jbStatsGain) a été déplacé dans .pet_more avant ce
       masquage → il reste visible dans la col notices. */
    .jb-view-profile .jbStatsWidget{ display:none !important; }

    /* ============== Mode LOG : image | chart | stats sur une row ==============
       3 colonnes STRICTEMENT identiques au mode profile : repeat(3, 1fr).
       Même gap (18px), mêmes marges → la carte totale (td) a la MÊME taille
       et les MÊMES marges dans les 2 modes.

       Le widget, le body ET la side passent en display:contents → c'est la
       SUMMARY (.jbStatsSummary) qui devient directement le grid-item "stats"
       et le main qui devient grid-item "chart". Le bloc gain est appendé
       DANS la summary (cf syncGainLocation) → il hérite naturellement de la
       card stats (border, fond .85, padding) comme il le ferait dans
       .pet_more en mode profile. */
    .jb-view-log .contentModuleTable > tbody > tr > td{
      grid-template-columns:repeat(3, minmax(0, 1fr)) !important;
      grid-template-areas:"image chart stats" !important;
      grid-template-rows:auto !important;
    }
    .jb-view-log .pet_info,
    .jb-view-log .pet_more{ display:none !important; }
    .jb-view-log .jbStatsWidget{ display:contents !important; }
    .jb-view-log .jbStatsBody{ display:contents !important; }
    .jb-view-log .jbStatsSide{ display:contents !important; }
    /* La summary devient elle-même le grid-item "stats" (la side passe en
       display:contents). align-self:start + min-height:0 + overflow:hidden
       laissent la box prendre exactement la hauteur set par JS (= image
       height). Le JS dans applyViewMode mesure .pet_image et applique
       inline height en px sur .jbStatsSummary et .jbStatsMain. */
    /* box-sizing:border-box → le height en px que le JS pose ici inclut
       padding + border. Sans ça, summary.padding (10+10) et main.padding
       (38+10) + leurs borders s'ajoutent par-dessus, et les cards dépassent
       l'image de 24px (summary) ou 52px (main). */
    .jb-view-log .jbStatsSummary{
      grid-area:stats !important;
      align-self:start !important;
      min-height:0 !important;
      overflow:hidden !important;
      box-sizing:border-box !important;
    }
    .jb-view-log .jbStatsMain{
      grid-area:chart !important;
      min-width:0 !important;
      align-self:start !important;
      min-height:0 !important;
      box-sizing:border-box !important;
    }
    /* Condense les rows de stats en log mode pour que les 5 lignes + la
       nameplate logent dans la hauteur de la card (= hauteur image).
       Profile mode garde sa typo intacte (overrides préfixés .jb-view-log). */
    .jb-view-log .jbStatsSummary .stat{
      padding:3px 4px !important;
      min-height:20px !important;
    }
    .jb-view-log .jbStatsSummary .stat .lbl{
      font-size:9px !important;
    }
    .jb-view-log .jbStatsSummary .stat .val{
      font-size:12px !important;
    }
    /* En log mode on cache le texte "Today, X gained Lv ±0 · HP +2 · ..."
       (.gainBreak) pour libérer de la place — on garde uniquement la nameplate
       +N à taille standard (240×84). PAS de margin-top:auto → la nameplate
       est juste sous les stats (pas de gros espace vide qui pousse la card
       au-delà de l'image). L'espace résiduel se retrouve sous la nameplate. */
    .jb-view-log .jbStatsSummary > .jbStatsGain.jbGainInNotices > .gainBreak{
      display:none !important;
    }
    .jb-view-log .jbStatsSummary > .jbStatsGain.jbGainInNotices{
      margin-top:0 !important;
      padding:8px 0 4px !important;
    }
    /* Plus de .jbStatsHead — les tabs sont maintenant dans .jbStatsMain top-right.
       (Les sélecteurs .jbStatsHead / .jbStatsTitle restent ignorés s'ils existent) */
    .jbStatsHead, .jbStatsTitle{ display:none !important; }
    /* Tabs : toggle pill positionné en absolute top-right de la zone chart */
    .jbStatsMain > .jbStatsTabs{
      position:absolute;
      top:8px;
      right:8px;
      z-index:2;
    }
    .jbStatsTabs{
      display:inline-flex;
      gap:0;
      background:rgba(255,255,255,.85);
      border:1.5px solid #000;
      border-radius:14px;
      padding:2px;
    }
    .jbStatsTabs button{
      padding:3px 12px;
      font:700 10px/1.2 'Trebuchet MS',Verdana,sans-serif;
      letter-spacing:.06em; text-transform:uppercase;
      background:transparent;
      color:#000;
      opacity:.55;
      border:0;
      border-radius:12px;
      cursor:pointer;
      transition:all .12s ease;
    }
    .jbStatsTabs button:hover{ opacity:1; }
    .jbStatsTabs button.active{
      background:#000;
      color:#fff;
      opacity:1;
      box-shadow:0 1px 2px rgba(0,0,0,.25);
      text-shadow:none;
    }
    /* Body en grille calquée sur le td parent (image | info | notices) :
       3 colonnes égales, gap 14px. Le widget est maintenant un grid item du
       td → padding 0 (le td a déjà son padding 18px). Côté gauche = stats
       (col 1, largeur image), côté droit = chart (cols 2+3, largeur info+notices). */
    .jbStatsBody{
      display:grid;
      grid-template-columns:repeat(3, minmax(0, 1fr));
      gap:14px;
      padding:0;
      align-items:stretch;
    }
    .jbStatsSide{ grid-column:1; display:flex; flex-direction:column; gap:10px; min-width:0; }
    /* (Anciennement on cachait .jbStatsGain.jbGainInNotices quand il était
       dans .jbStatsSide pour éviter une trace vide, mais maintenant en log
       mode on le RÉAFFICHE sous la carte stats → règle retirée.) */
    .jbStatsMain{
      grid-column:2 / 4;
      display:flex; flex-direction:column; gap:6px; min-width:0; min-height:0;
      position:relative;
      background:rgba(255,255,255,.85);
      border:2px solid #000;
      border-radius:8px;
      padding:38px 10px 10px;  /* top:38px pour laisser place aux tabs en absolute */
      overflow:hidden;
    }
    /* Total gain : pas de fond / pas de bordure → on laisse vivre le nameplate */
    .jbStatsGain{
      display:flex;
      flex-direction:column;
      align-items:center;
      gap:4px;
      padding:4px 0;
      background:transparent !important;
      border:none !important;
      box-shadow:none !important;
      color:#000;
    }
    .jbStatsGain .gainLabel{
      font:700 9px/1.2 'Trebuchet MS',Verdana,sans-serif;
      text-transform:uppercase; letter-spacing:.06em;
      color:#000; opacity:.7;
      text-align:center;
    }
    /* Le chiffre est posé SUR le nameplate du thème actif. background-size:auto
       → le SVG reste à sa taille native (pas de déformation). Le conteneur
       accueille tout le SVG (assez de hauteur pour pas crop). */
    .jbStatsGain .gainBig{
      display:inline-flex;
      align-items:center;
      justify-content:center;
      min-width:240px;
      min-height:84px;
      padding:14px 40px;
      box-sizing:content-box;
      background-image:var(--neo-nameplate, none);
      background-repeat:no-repeat;
      background-position:center;
      background-size:auto;
      font:800 28px/1 'CafeteriaBlack','Trebuchet MS',sans-serif;
      font-variant-numeric:tabular-nums;
      color:#000;
      letter-spacing:.02em;
      filter:drop-shadow(0 1px 2px rgba(0,0,0,.20));
    }
    .jbStatsGain .gainBig.flat,
    .jbStatsGain .gainBig.down{ color:#000; }
    .jbStatsGain .gainBreak{
      margin-top:4px;
      font-size:10px; color:#000; opacity:.8;
      font-variant-numeric:tabular-nums;
      line-height:1.4;
      text-align:center;
    }
    .jbStatsGain .gainBreak b{ color:#000; font-weight:700; opacity:1; }
    /* Chart : flex:1 dans jbStatsMain (col flex). min-height:0 + svg en
       position:absolute → le svg ne dicte plus la hauteur naturelle, donc la
       grille .jbStatsBody est sizée par la sidebar stats (et le svg s'adapte
       via preserveAspectRatio="xMidYMid meet"). */
    .jbStatsChart{
      display:flex; justify-content:center; align-items:center;
      background:rgba(255,255,255,.85);
      border:1px solid #e5e5e5;
      border-radius:6px;
      flex:1 1 0;
      min-height:0;
      position:relative;
      overflow:hidden;
    }
    .jbStatsChart svg{
      display:block;
      position:absolute;
      inset:8px;
      width:calc(100% - 16px);
      height:calc(100% - 16px);
      max-width:calc(100% - 16px);
      max-height:calc(100% - 16px);
    }
    /* Quand renderChart affiche le placeholder "Pas encore d'historique",
       le .jbStatsEmpty doit rester en flux normal (pas dans le SVG absolu). */
    .jbStatsChart .jbStatsEmpty{ position:relative; }
    .jbStatsLegend{
      display:flex; justify-content:center; gap:14px; flex-wrap:wrap;
      font-size:10px; font-weight:700;
    }
    /* Summary BD : liste de stats sur fond body-bg-bottom (hérité du widget parent).
       Contour noir cohérent, séparateurs sombres entre lignes, texte noir. */
    .jbStatsSummary{
      display:flex; flex-direction:column; gap:0;
      padding:10px 14px;
      background:rgba(255,255,255,.85); /* aligné sur l'opacité des cards habituelles (.pet_more, etc.) */
      border:2px solid #000;
      border-radius:8px;
    }
    .jbStatsSummary .stat{
      display:flex; align-items:center; justify-content:space-between;
      gap:8px;
      background:transparent;
      border:none;
      border-bottom:1px solid rgba(0,0,0,.25);
      border-radius:0;
      padding:7px 4px;
      min-height:28px;
      position:relative;
      color:#000;
    }
    .jbStatsSummary .stat:last-child{ border-bottom:none; }
    .jbStatsSummary .stat .lbl{
      color:#000; opacity:.7; font:700 9px/1.2 'Trebuchet MS',Verdana,sans-serif;
      text-transform:uppercase; letter-spacing:.06em;
      display:flex; align-items:center; gap:6px;
    }
    /* Plus de pastille colorée (cohérence noir) */
    .jbStatsSummary .stat .lbl::before{ display:none; }
    /* Plus de bordure latérale colorée */
    /* Fishing : carte info standard (contour noir) */
    .jbStatCard[data-stat="fishing"]{
      flex-direction:row !important;
      justify-content:space-between !important;
      align-items:center !important;
    }
    .jbStatCard[data-stat="fishing"] .jbStatLabel{ font-size:10px !important; }
    .jbStatCard[data-stat="fishing"] .jbStatValue{ font-size:14px !important; }
    .jbStatsSummary .stat .val{
      color:#000; font-weight:800; font-size:13px;
      font-variant-numeric:tabular-nums;
      display:flex; align-items:baseline; gap:6px;
    }
    .jbStatsSummary .stat .delta{ font-size:10px; font-weight:700; color:#000; opacity:.65; }
    .jbStatsSummary .stat .delta.up,
    .jbStatsSummary .stat .delta.down,
    .jbStatsSummary .stat .delta.flat{ color:#000; }
    .jbStatsEmpty{
      padding:18px 8px; text-align:center;
      color:#000; opacity:.65; font-size:12px; font-style:italic;
    }
    .jbStatsFoot{
      display:flex; justify-content:space-between; align-items:center;
      padding:6px 14px;
      background:rgba(255,255,255,.65);
      border-top:1px solid #000;
      font-size:10px; color:#000; opacity:.75;
    }
    .jbStatsFoot button{
      background:transparent; border:0; padding:0; cursor:pointer;
      color:#000;
      font:inherit; text-decoration:underline;
    }
    .jbStatsFoot button:hover{ color:#000; }

    /* ============== Liens internes par défaut — tout en noir ============== */
    .contentModule a{ color:#000; text-decoration:underline; }
    .contentModule a:hover{ color:#000; }
    .contentModule .pet_info, .contentModule .pet_more { color:#000; }

    /* ============== Responsive ============== */
    @media (max-width: 1100px){
      .contentModuleTable > tbody > tr > td{
        grid-template-columns:minmax(220px, 1fr) minmax(0, 1fr) !important;
        grid-template-areas:
          "image info"
          "notices notices" !important;
      }
      .pet_image, .pet_more{ min-height:240px !important; }
      .jbStatsBody{ grid-template-columns:1fr 1fr !important; }
      .jbStatsMain{ grid-column:2 / 3 !important; }
      .jb-view-log .contentModuleTable > tbody > tr > td{
        grid-template-areas:
          "image stats"
          "chart chart" !important;
      }
    }
    @media (max-width: 760px){
      .contentModule th.contentModuleHeader,
      .contentModule th.contentModuleHeaderAlt{
        padding:14px 110px 14px 16px !important; /* moins de place réservée à droite sur mobile */
      }
      .jbHeaderHp{ right:106px !important; width:160px !important; }
      .jbViewToggle{ height:44px !important; padding:3px !important; }
      .jbViewToggle button{ width:36px !important; }
      .jbViewToggle button img{ width:22px !important; height:22px !important; }
      .contentModuleTable > tbody > tr > td{
        grid-template-columns:1fr !important;
        grid-template-areas:
          "image"
          "info"
          "notices" !important;
      }
      .pet_image{ max-width:240px !important; margin:0 auto !important; min-height:220px !important; }
      .pet_more{ min-height:0 !important; }
      .jbStatsBody{ grid-template-columns:1fr !important; }
      .jbStatsSide{ grid-column:1 !important; }
      .jbStatsMain{ grid-column:1 !important; }
      .jb-view-log .contentModuleTable > tbody > tr > td{
        grid-template-columns:1fr !important;
        grid-template-areas:
          "image"
          "stats"
          "chart" !important;
      }
    }
  `;
  function injectStyle(){
    if (document.getElementById('jb-quickref-css')) return;
    const st = document.createElement('style');
    st.id = 'jb-quickref-css';
    st.textContent = CSS;
    (document.head || document.documentElement).appendChild(st);
  }
  injectStyle();

  /* =========================
     POSE SWAP
  ========================= */
  function swapPose(root = document){
    if (!CFG.POSE_SWAP) return;
    root.querySelectorAll("img[src*='/1/4.png']").forEach(img => {
      img.src = img.src.replace('/1/4.png', '/1/5.png');
    });
    root.querySelectorAll("div[style*='/1/4.png']").forEach(div => {
      if (div.style?.backgroundImage){
        div.style.backgroundImage = div.style.backgroundImage.replace('/1/4.png', '/1/5.png');
      }
    });
  }

  /* =========================
     STORAGE STATS
  ========================= */
  function loadStats(){
    try { return JSON.parse(localStorage.getItem(STATS_LS) || '{}') || {}; }
    catch { return {}; }
  }
  function saveStats(d){
    try { localStorage.setItem(STATS_LS, JSON.stringify(d || {})); } catch {}
  }
  function dayKeyNST(d = new Date()){
    return new Intl.DateTimeFormat('en-CA', {
      timeZone: 'America/Los_Angeles',
      year:'numeric', month:'2-digit', day:'2-digit',
    }).format(d);
  }

  /* =========================
     SCRAPING — extraction des stats depuis chaque pet
  ========================= */
  function intOr(text, fallbackRegex){
    if (text == null) return null;
    let m = text.match(/\(([\d,]+)\)/);
    if (!m && fallbackRegex) m = text.match(fallbackRegex);
    if (!m) m = text.match(/[\d,]+/);
    if (!m) return null;
    const n = parseInt(String(m[1] || m[0]).replace(/,/g, ''), 10);
    return isFinite(n) ? n : null;
  }
  function parseStatsTable(tbl){
    const stats = {};
    [...tbl.querySelectorAll('tr')].forEach(tr => {
      const th = tr.querySelector('th');
      const td = tr.querySelector('td');
      if (!th || !td) return;
      const key = th.textContent.trim().replace(/:$/, '').toLowerCase();
      stats[key] = td.textContent.trim();
    });
    return stats;
  }
  function scrapeAllPets(){
    const pets = {};
    document.querySelectorAll('div[id$="_details"].contentModule').forEach(div => {
      const name = div.id.replace(/_details$/, '');
      const tbl = div.querySelector('.pet_stats');
      if (!tbl) return;
      const s = parseStatsTable(tbl);
      let hpMax = null;
      if (s.health){
        const m = s.health.match(/(\d+)\s*\/\s*(\d+)/);
        if (m) hpMax = parseInt(m[2], 10);
      }
      pets[name] = {
        level: intOr(s.level),
        hpMax,
        str:   intOr(s.strength),
        def:   intOr(s.defence),
        move:  intOr(s.move),
      };
    });
    return pets;
  }
  function recordSnapshot(){
    const today = dayKeyNST();
    const pets  = scrapeAllPets();
    const all   = loadStats();
    for (const [name, s] of Object.entries(pets)){
      if (!Object.values(s).some(v => v !== null)) continue;
      if (!all[name]) all[name] = [];
      const list = all[name];
      const last = list[list.length - 1];
      if (last && last.date === today){
        Object.assign(last, s);
      } else {
        list.push({ date: today, ...s });
      }
      if (list.length > 365) list.splice(0, list.length - 365);
    }
    saveStats(all);
  }

  /* =========================
     UTILS NUMÉRIQUES + HSD
  ========================= */
  function fmtN(n){ return (n == null ? '—' : n.toLocaleString()); }
  function fmtDelta(n){
    if (n == null) return '—';
    if (n === 0) return '±0';
    return (n > 0 ? '+' : '') + n.toLocaleString();
  }
  // HSD = HP + min(Str, 850) + min(Def, 850)
  function hsdSum(snap){
    if (!snap) return null;
    const a = snap.hpMax, b = snap.str, c = snap.def;
    if (a == null || b == null || c == null) return null;
    return a + Math.min(b, HSD_CAP) + Math.min(c, HSD_CAP);
  }
  function deltaCls(d){
    if (d == null) return 'flat';
    if (d > 0) return 'up';
    if (d < 0) return 'down';
    return 'flat';
  }

  /* =========================
     CHART SVG
  ========================= */
  function renderChart(container, history, mode){
    if (!history.length){
      container.innerHTML = `<div class="jbStatsEmpty">Pas encore d'historique. Reviens visiter ta quickref chaque jour pour voir la courbe se construire.</div>`;
      return;
    }
    let series;
    let isDeltaMode = false;
    if (mode === 'all'){
      // Mode "All stats" : on plotte la VARIATION depuis le 1er snapshot
      // (= delta), sinon les courbes apparaissent plates parce que les ordres de
      // grandeur écrasent les variations (+2 Level invisible face à 5000 HP).
      isDeltaMode = true;
      const base = history[0];
      const deltaOf = (key) => history.map((h, i) => ({
        x: i,
        y: (h[key] != null && base[key] != null) ? (h[key] - base[key]) : null,
      }));
      series = [
        { key:'level', label:'Level', color:STAT_COLOR.level, data: deltaOf('level') },
        { key:'hpMax', label:'HP',    color:STAT_COLOR.hpMax, data: deltaOf('hpMax') },
        { key:'str',   label:'Str',   color:STAT_COLOR.str,   data: deltaOf('str') },
        { key:'def',   label:'Def',   color:STAT_COLOR.def,   data: deltaOf('def') },
        { key:'move',  label:'Move',  color:STAT_COLOR.move,  data: deltaOf('move') },
      ];
    } else {
      series = [{
        key:'hsd', label:'HSD', color:STAT_COLOR.hsd,
        data: history.map((h,i) => ({ x:i, y: hsdSum(h) })),
      }];
    }
    /* viewBox dynamique = dimensions pixel réelles de l'aire SVG visible
       (= container - border 1px chaque côté - inset 8px chaque côté = -18px
       par axe), pour que le SVG remplisse toute la card sans distorsion et
       que le texte garde une taille screen-stable (1 unité viewBox = 1px
       écran). Fallback 520×200 si le container n'a pas encore de taille
       (widget initialement caché en mode profile). */
    const _rect = container.getBoundingClientRect();
    const _cw = Math.round(_rect.width)  - 18;
    const _ch = Math.round(_rect.height) - 18;
    const W = (_cw > 100 ? _cw : 520);
    const H = (_ch > 100 ? _ch : 200);
    const padL = 54, padR = 14, padT = 12, padB = 24;
    const chartW = W - padL - padR;
    const chartH = H - padT - padB;
    let yMin = Infinity, yMax = -Infinity;
    for (const s of series) for (const p of s.data) {
      if (p.y == null) continue;
      if (p.y < yMin) yMin = p.y;
      if (p.y > yMax) yMax = p.y;
    }
    if (!isFinite(yMin) || !isFinite(yMax)){
      container.innerHTML = `<div class="jbStatsEmpty">Pas de données numériques disponibles.</div>`;
      return;
    }
    // En mode delta on ne floor pas à 0 (peut être négatif), et on étend
    // un peu la range pour rendre les courbes plates en bas du rectangle visibles
    if (yMin === yMax){
      if (isDeltaMode){ yMin = yMin - 1; yMax = yMax + 1; }
      else            { yMin = Math.max(0, yMin - 1); yMax = yMax + 1; }
    }
    const padRng = Math.max((yMax - yMin) * 0.15, 1);
    const yLo = isDeltaMode ? Math.floor(yMin - padRng) : Math.max(0, Math.floor(yMin - padRng));
    const yHi = Math.ceil(yMax + padRng);
    const yRange = yHi - yLo;
    const n = history.length;
    const sx = i => padL + (n > 1 ? (i / (n - 1)) * chartW : chartW / 2);
    const sy = v => padT + (1 - (v - yLo) / yRange) * chartH;
    const fmtY = (v) => {
      const rounded = Math.round(v);
      if (isDeltaMode){
        if (rounded === 0) return '0';
        return (rounded > 0 ? '+' : '') + rounded.toLocaleString();
      }
      return rounded.toLocaleString();
    };
    let svg = `<svg viewBox="0 0 ${W} ${H}" width="${W}" height="${H}" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid meet">`;
    const GRID_LINES = 4;
    for (let i = 0; i <= GRID_LINES; i++){
      const y = padT + (i / GRID_LINES) * chartH;
      const v = yHi - (i / GRID_LINES) * yRange;
      // Ligne du zéro plus visible en mode delta (référence baseline)
      const isZero = isDeltaMode && Math.round(v) === 0;
      const stroke = isZero ? '#888' : UI_GRID;
      const sw = isZero ? '1.2' : '1';
      svg += `<line x1="${padL}" y1="${y.toFixed(1)}" x2="${padL + chartW}" y2="${y.toFixed(1)}" stroke="${stroke}" stroke-width="${sw}" stroke-dasharray="${isZero ? '3,2' : '0'}"/>`;
      svg += `<text x="${padL - 6}" y="${(y + 3).toFixed(1)}" font-size="9" fill="${UI_TEXT_DIM}" text-anchor="end" font-family="Trebuchet MS, Verdana, sans-serif">${fmtY(v)}</text>`;
    }
    svg += `<text x="${padL}" y="${H - 6}" font-size="9" fill="${UI_TEXT_DIM}" font-family="Trebuchet MS, Verdana, sans-serif">${history[0].date}</text>`;
    if (n > 1){
      svg += `<text x="${padL + chartW}" y="${H - 6}" font-size="9" fill="${UI_TEXT_DIM}" text-anchor="end" font-family="Trebuchet MS, Verdana, sans-serif">${history[n-1].date}</text>`;
    }
    for (const s of series){
      const pts = s.data
        .filter(p => p.y != null)
        .map(p => `${sx(p.x).toFixed(1)},${sy(p.y).toFixed(1)}`)
        .join(' ');
      if (!pts) continue;
      svg += `<polyline fill="none" stroke="${s.color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" points="${pts}"/>`;
      for (const p of s.data){
        if (p.y == null) continue;
        svg += `<circle cx="${sx(p.x).toFixed(1)}" cy="${sy(p.y).toFixed(1)}" r="2.5" fill="${s.color}"/>`;
      }
    }
    svg += `</svg>`;
    container.innerHTML = svg;
  }
  function renderLegend(container, mode){
    if (mode !== 'all'){ container.innerHTML = ''; return; }
    container.innerHTML = `
      <span style="color:#000"><span style="color:${STAT_COLOR.level}">●</span> Level</span>
      <span style="color:#000"><span style="color:${STAT_COLOR.hpMax}">●</span> HP</span>
      <span style="color:#000"><span style="color:${STAT_COLOR.str}">●</span> Str</span>
      <span style="color:#000"><span style="color:${STAT_COLOR.def}">●</span> Def</span>
      <span style="color:#000"><span style="color:${STAT_COLOR.move}">●</span> Move</span>
      <span style="color:#000; opacity:.6; font-weight:400; font-style:italic; margin-left:8px;">Δ depuis 1er snapshot</span>`;
  }
  function renderGain(container, history, mode, petName){
    const name = petName || 'this pet';
    if (history.length < 2){
      container.innerHTML = `
        <div class="gainBig flat">—</div>
        <div class="gainBreak">Pas encore assez d'historique pour ${name}.<br>Reviens demain.</div>`;
      return;
    }
    const last = history[history.length - 1];
    const prev = history[history.length - 2];
    if (mode === 'hsd'){
      const d = (hsdSum(last) ?? 0) - (hsdSum(prev) ?? 0);
      const cls = deltaCls(d);
      const sign = d > 0 ? '+' : (d < 0 ? '' : '±');
      container.innerHTML = `
        <div class="gainBig ${cls}">${sign}${Math.abs(d).toLocaleString()}</div>
        <div class="gainBreak">
          Today, ${name} gained
          HP <b>${fmtDelta((last.hpMax ?? 0) - (prev.hpMax ?? 0))}</b>
          · Str <b>${fmtDelta((last.str ?? 0) - (prev.str ?? 0))}</b>
          · Def <b>${fmtDelta((last.def ?? 0) - (prev.def ?? 0))}</b>
        </div>`;
    } else {
      const totalNow  = (last.level ?? 0) + (last.hpMax ?? 0) + (last.str ?? 0) + (last.def ?? 0) + (last.move ?? 0);
      const totalPrev = (prev.level ?? 0) + (prev.hpMax ?? 0) + (prev.str ?? 0) + (prev.def ?? 0) + (prev.move ?? 0);
      const d = totalNow - totalPrev;
      const cls = deltaCls(d);
      const sign = d > 0 ? '+' : (d < 0 ? '' : '±');
      container.innerHTML = `
        <div class="gainBig ${cls}">${sign}${Math.abs(d).toLocaleString()}</div>
        <div class="gainBreak">
          Today, ${name} gained
          Lv <b>${fmtDelta((last.level ?? 0) - (prev.level ?? 0))}</b>
          · HP <b>${fmtDelta((last.hpMax ?? 0) - (prev.hpMax ?? 0))}</b>
          · Str <b>${fmtDelta((last.str ?? 0) - (prev.str ?? 0))}</b>
          · Def <b>${fmtDelta((last.def ?? 0) - (prev.def ?? 0))}</b>
          · Mv <b>${fmtDelta((last.move ?? 0) - (prev.move ?? 0))}</b>
        </div>`;
    }
  }
  function renderSummary(container, history, mode){
    /* Non-destructif : on retire uniquement les anciens .stat (et le
       placeholder éventuel) — on PRÉSERVE les autres enfants comme le
       bloc .jbStatsGain qu'on appendChild'e ici en mode log (cf
       syncGainLocation). Sinon le rerender sur toggle écrase le gain. */
    container.querySelectorAll(':scope > .stat, :scope > .jbStatsEmpty').forEach(el => el.remove());
    if (!history.length) return;
    const last = history[history.length - 1];
    const prev = history.length > 1 ? history[history.length - 2] : null;
    function card(key, label, now, before){
      const d = (now != null && before != null) ? now - before : null;
      const cls = deltaCls(d);
      const deltaTxt = (d == null) ? '' : `<span class="delta ${cls}">${fmtDelta(d)}</span>`;
      return `
        <div class="stat" data-stat="${key}">
          <div class="lbl">${label}</div>
          <div class="val">${fmtN(now)} ${deltaTxt}</div>
        </div>`;
    }
    const html = (mode === 'hsd')
      ? card('hsd',   'HSD Total', hsdSum(last), hsdSum(prev)) +
        card('hpMax', 'HP Max',    last.hpMax,   prev?.hpMax) +
        card('str',   'Strength',  last.str,     prev?.str) +
        card('def',   'Defence',   last.def,     prev?.def)
      : card('level', 'Level',    last.level, prev?.level) +
        card('hpMax', 'HP Max',   last.hpMax, prev?.hpMax) +
        card('str',   'Strength', last.str,   prev?.str) +
        card('def',   'Defence',  last.def,   prev?.def) +
        card('move',  'Move',     last.move,  prev?.move);
    /* Insert au DÉBUT de la summary → les .stat restent au-dessus, le
       bloc gain (s'il a été appendé ici par syncGainLocation) reste en
       bas et garde son margin-top:auto. */
    container.insertAdjacentHTML('afterbegin', html);
  }

  /* =========================
     STATS WIDGET
  ========================= */
  function buildStatsWidget(petName){
    const wrap = document.createElement('div');
    wrap.className = 'jbStatsWidget';
    wrap.innerHTML = `
      <div class="jbStatsBody">
        <div class="jbStatsSide">
          <div class="jbStatsGain"></div>
          <div class="jbStatsSummary"></div>
        </div>
        <div class="jbStatsMain">
          <div class="jbStatsTabs">
            <button data-mode="all" class="active">All stats</button>
            <button data-mode="hsd">HSD</button>
          </div>
          <div class="jbStatsChart"></div>
          <div class="jbStatsLegend"></div>
        </div>
      </div>
    `;
    const gainEl    = wrap.querySelector('.jbStatsGain');
    const chartEl   = wrap.querySelector('.jbStatsChart');
    const legendEl  = wrap.querySelector('.jbStatsLegend');
    const summaryEl = wrap.querySelector('.jbStatsSummary');
    function getHistory(){
      const all = loadStats();
      return (all[petName] || []).slice();
    }
    function render(mode){
      const history = getHistory();
      renderGain(gainEl, history, mode, petName);
      renderChart(chartEl, history, mode);
      renderLegend(legendEl, mode);
      renderSummary(summaryEl, history, mode);
    }
    let currentMode = 'all';
    render(currentMode);
    wrap.querySelectorAll('.jbStatsTabs button').forEach(btn => {
      btn.addEventListener('click', (ev) => {
        ev.preventDefault();
        currentMode = btn.dataset.mode;
        wrap.querySelectorAll('.jbStatsTabs button').forEach(b => b.classList.toggle('active', b === btn));
        render(currentMode);
      });
    });
    /* Re-render auto quand la taille du chart container change (toggle de
       view mode, resize window). viewBox dynamique → on doit recalculer
       pour utiliser toute la hauteur de la card sans écraser le graphique. */
    let _lastW = 0, _lastH = 0;
    try {
      const ro = new ResizeObserver(entries => {
        const r = entries[0]?.contentRect;
        if (!r) return;
        const cw = Math.round(r.width), ch = Math.round(r.height);
        // Anti-bruit : ignore les variations < 4px
        if (Math.abs(cw - _lastW) < 4 && Math.abs(ch - _lastH) < 4) return;
        _lastW = cw; _lastH = ch;
        renderChart(chartEl, getHistory(), currentMode);
      });
      ro.observe(chartEl);
    } catch {}
    /* Hook externe pour rerender après toggle de view mode (setViewMode
       déclenche un requestAnimationFrame puis appelle __jbRerender). */
    wrap.__jbRerender = () => render(currentMode);
    return wrap;
  }

  /* =========================
     PIECES DU REVAMP PAR PET
  ========================= */
  function buildActions(petDiv){
    const name = petDiv.id.replace(/_details$/, '');
    if (petDiv.querySelector('.jbPetActions')) return;
    const menu = document.getElementById(`${name}_menu`);
    if (!menu) return;
    const wrap = document.createElement('div');
    wrap.className = 'jbPetActions';
    [...menu.querySelectorAll('li')].forEach(li => {
      const a = li.querySelector('a');
      const txt = (li.textContent || '').replace(/^\s*»\s*/, '').trim();
      if (!a){
        const span = document.createElement('a');
        span.className = 'disabled';
        span.textContent = txt;
        span.href = 'javascript:void(0)';
        wrap.appendChild(span);
        return;
      }
      const link = document.createElement('a');
      link.href = a.getAttribute('href');
      link.textContent = txt;
      wrap.appendChild(link);
    });
    // Les actions vont en BAS de la pet card (sous le widget Progression)
    // → append direct au contentModule (petDiv), pas dans le td du grid
    petDiv.appendChild(wrap);
  }
  /* HP est désormais dans le bandeau header (decorateHeader). Cette fonction
     se contente de masquer la row Health du tableau original (sinon doublon
     dans la stats grid reconstruite). */
  function hideHealthRow(petDiv){
    const tbl = petDiv.querySelector('.pet_stats');
    if (!tbl) return;
    [...tbl.querySelectorAll('tr')].forEach(tr => {
      const th = tr.querySelector('th');
      if (!th) return;
      if (/Health/i.test(th.textContent || '')){
        tr.classList.add('jbHideRow');
      }
    });
  }
  function petNeedsCure(petDiv){
    return /Find the cure at the NeoHospital!/i.test(petDiv.textContent || '');
  }
  function addHealOverlay(petDiv){
    const petBox = petDiv.querySelector('.pet_image');
    if (!petBox) return;
    const has = petDiv.querySelector('#jb-heal-overlay');
    if (!petNeedsCure(petDiv)){
      if (has) has.remove();
      return;
    }
    if (has) return;
    const a = document.createElement('a');
    a.id = 'jb-heal-overlay';
    a.href = CFG.HEAL_LINK;
    a.target = '_blank';
    a.rel = 'noopener noreferrer';
    a.title = 'Find Cooling Ointment';
    const img = document.createElement('img');
    img.src = CFG.HEAL_GIF;
    img.alt = '';
    img.loading = 'lazy';
    a.appendChild(img);
    petBox.appendChild(a);
  }
  function injectStatsWidget(petDiv){
    if (petDiv.querySelector('.jbStatsWidget')) return;
    const name = petDiv.id.replace(/_details$/, '');
    const td   = petDiv.querySelector('table.contentModuleTable tbody > tr > td');
    if (td){
      // Widget = grid item du td (row 2 en mode profile, OR display:contents
      // en mode log pour que side/main deviennent eux-mêmes grid items).
      td.appendChild(buildStatsWidget(name));
    } else {
      petDiv.appendChild(buildStatsWidget(name));
    }
  }
  /* Place le bloc Total gain dans la col 3 :
     - profile : dernier child de .pet_more (col 3 = notices)
     - log     : dernier child de .jbStatsSummary (col 3 = carte stats)
     Dans les 2 cas, le parent est un flex column avec margin-top:auto sur
     le gain → bouton ancré en bas de la card. Même col, même position,
     même look (style hérité de `.jbStatsGain.jbGainInNotices`). */
  function syncGainLocation(petDiv){
    const mode = loadViewMode(petNameOf(petDiv));
    const gain = petDiv.querySelector('.jbStatsGain');
    if (!gain) return;
    gain.classList.add('jbGainInNotices');
    const more = petDiv.querySelector('.pet_more');
    const summary = petDiv.querySelector('.jbStatsSummary');
    if (mode === 'log'){
      // appendChild force le déplacement en DERNIÈRE position dans .jbStatsSummary
      // (à l'init le gain est dans .jbStatsSide → on le déplace dans la card stats).
      if (summary && (gain.parentElement !== summary || gain.nextElementSibling !== null)){
        summary.appendChild(gain);
      }
      if (more) more.classList.remove('jbHasGain');
    } else {
      if (more && (gain.parentElement !== more || gain.nextElementSibling !== null)){
        more.appendChild(gain);
      }
      if (more) more.classList.add('jbHasGain');
    }
  }
  /* ===== View mode (profile vs log) — PER-PET (chaque carte a son propre
     état persisté, indépendamment des autres cartes du compte). Stocké en
     localStorage sous la clé `jb_view_mode_<petName>`. ===== */
  const VIEW_MODE_LS_PREFIX = 'jb_view_mode_';
  function petNameOf(petDiv){
    return petDiv && petDiv.id ? petDiv.id.replace(/_details$/, '') : '';
  }
  function loadViewMode(petName){
    if (!petName) return 'profile';
    try { return localStorage.getItem(VIEW_MODE_LS_PREFIX + petName) === 'log' ? 'log' : 'profile'; }
    catch { return 'profile'; }
  }
  function saveViewMode(petName, mode){
    if (!petName) return;
    try { localStorage.setItem(VIEW_MODE_LS_PREFIX + petName, mode === 'log' ? 'log' : 'profile'); } catch {}
  }
  /* En log mode, force la hauteur des cards summary et main d'UN pet =
     hauteur de l'image (mesurée après layout). Retry jusqu'à 5 fois si
     l'image n'a pas encore de hauteur (layout pas stable). */
  function syncLogCardHeightsFor(petDiv, retries){
    if (retries == null) retries = 5;
    if (!petDiv) return;
    if (petDiv.classList.contains('jb-view-log')){
      const img = petDiv.querySelector('.pet_image');
      if (!img) return;
      const h = Math.round(img.getBoundingClientRect().height);
      if (h <= 0){
        if (retries > 0) requestAnimationFrame(() => syncLogCardHeightsFor(petDiv, retries - 1));
        return;
      }
      petDiv.querySelectorAll('.jbStatsSummary, .jbStatsMain').forEach(el => {
        el.style.height = h + 'px';
        el.style.maxHeight = h + 'px';
      });
    } else {
      // Profile mode : retire les hauteurs forcées (au cas où on revient du log)
      petDiv.querySelectorAll('.jbStatsSummary, .jbStatsMain').forEach(el => {
        el.style.height = '';
        el.style.maxHeight = '';
      });
    }
  }
  function syncLogCardHeightsAll(){
    document.querySelectorAll('div[id$="_details"].contentModule').forEach(pd => {
      syncLogCardHeightsFor(pd);
    });
  }
  /* Applique le mode (profile/log) à UN seul pet — pas tous. Le mode est
     loadé per-pet via petName. */
  function applyViewModeFor(petDiv){
    if (!petDiv) return;
    const mode = loadViewMode(petNameOf(petDiv));
    petDiv.classList.toggle('jb-view-log',     mode === 'log');
    petDiv.classList.toggle('jb-view-profile', mode !== 'log');
    syncGainLocation(petDiv);
    // Le toggle de CE pet seulement reflète son mode
    petDiv.querySelectorAll('.jbViewToggle button').forEach(b => {
      b.classList.toggle('active', b.dataset.view === mode);
    });
    // Layout stable puis sync des hauteurs
    requestAnimationFrame(() => requestAnimationFrame(() => syncLogCardHeightsFor(petDiv)));
  }
  function applyViewModeAll(){
    document.querySelectorAll('div[id$="_details"].contentModule').forEach(applyViewModeFor);
  }
  function setViewModeFor(petDiv, mode){
    if (!petDiv) return;
    saveViewMode(petNameOf(petDiv), mode);
    applyViewModeFor(petDiv);
    // Re-render le chart de CE pet pour qu'il s'adapte à la nouvelle taille
    requestAnimationFrame(() => {
      const w = petDiv.querySelector('.jbStatsWidget');
      if (w && typeof w.__jbRerender === 'function') w.__jbRerender();
    });
  }
  function addViewToggle(petDiv){
    const th = petDiv.querySelector('th.contentModuleHeader, th.contentModuleHeaderAlt');
    if (!th || th.querySelector('.jbViewToggle')) return;
    const toggle = document.createElement('div');
    toggle.className = 'jbViewToggle';
    toggle.innerHTML = `
      <button data-view="profile" type="button" aria-label="Vue profil" title="Vue profil">
        <img src="https://images.neopets.com/themes/h5/altadorcup/images/profile-icon.png" alt="">
      </button>
      <button data-view="log" type="button" aria-label="Vue BD stats" title="Vue BD stats">
        <img src="https://images.neopets.com/themes/h5/altadorcup/images/transferlog-icon.png" alt="">
      </button>
    `;
    toggle.querySelectorAll('button').forEach(b => {
      b.addEventListener('click', (ev) => {
        ev.preventDefault();
        // Toggle affecte SEULEMENT cette carte (recherche du .contentModule ancêtre)
        const targetPet = b.closest('div[id$="_details"].contentModule');
        setViewModeFor(targetPet, b.dataset.view);
      });
    });
    th.appendChild(toggle);
  }

  /* =========================
     HEADER : ajoute chips Species + Colour
  ========================= */
  function getStatsByLabel(tbl){
    const map = {};
    if (!tbl) return map;
    [...tbl.querySelectorAll('tr')].forEach(tr => {
      const th = tr.querySelector('th');
      const td = tr.querySelector('td');
      if (!th || !td) return;
      const lbl = (th.textContent || '').trim().replace(/:$/, '');
      map[lbl] = { row: tr, value: td.textContent.trim() };
    });
    return map;
  }
  function decorateHeader(petDiv){
    const th = petDiv.querySelector('th.contentModuleHeader, th.contentModuleHeaderAlt');
    if (!th) return;
    const tbl  = petDiv.querySelector('.pet_stats');
    const stats = getStatsByLabel(tbl);
    const species = stats.Species?.value || '';
    const colour  = stats.Colour?.value  || '';
    // Récupère le sci (id de design du pet) pour extraire couleur dominante de l'image
    const sciNow  = petDiv.querySelector('.pet_image')?.dataset?.sci;
    // Applique le thème : fallback synchrone par Colour name, upgrade async via image
    applyPetTheme(petDiv, colour, sciNow);

    // === HP bar Pokemon-style dans le header (idempotent) ===
    if (!th.querySelector('.jbHeaderHp')){
      const hpRaw = stats.Health?.value || '';
      const m = hpRaw.match(/(\d+)\s*\/\s*(\d+)/);
      if (m){
        const cur = parseInt(m[1], 10);
        const max = parseInt(m[2], 10);
        const pct = max > 0 ? (cur / max) * 100 : 0;
        const cls = pct < 25 ? 'danger' : (pct < 50 ? 'warn' : '');
        const hpBlock = document.createElement('div');
        hpBlock.className = 'jbHeaderHp ' + cls;
        hpBlock.innerHTML = `
          <div class="hpLabel">HP <span class="hpCur">${cur.toLocaleString()}</span><span class="hpSep"> / </span><b class="hpMax">${max.toLocaleString()}</b></div>
          <div class="hpBar"><div class="hpFill" style="width:${pct.toFixed(1)}%"></div></div>
        `;
        th.appendChild(hpBlock);
      }
    }

    if (th.dataset.jbDecorated === '1') return;
    const a = th.querySelector('a');
    if (!a) return;
    // Strip les text-nodes directs du th (legacy "with X the Y...", etc.)
    [...th.childNodes].forEach(n => {
      if (n.nodeType === Node.TEXT_NODE && n.textContent.trim()){
        n.remove();
      }
    });
    // Strip aussi les <sub>/<br> directs (parfois utilisés par Neopets)
    [...th.children].forEach(c => {
      if (c === a) return;
      if (/^(SUB|BR|FONT|SMALL)$/i.test(c.tagName) && !c.querySelector('a')){
        c.remove();
      }
    });
    // Icône mypets du thème (avatar décoratif) — sera un flex item normal
    if (!th.querySelector('.jbHeaderAvatar')){
      const av = document.createElement('span');
      av.className = 'jbHeaderAvatar';
      th.insertBefore(av, th.firstChild);
    }
    // Étiquette sous le nom : cadre simple semi-transparent (CSS) + chips Couleur/Espèce
    if ((species || colour) && !th.querySelector('.jbHeaderChips')){
      const chips = document.createElement('span');
      chips.className = 'jbHeaderChips';
      if (colour)  chips.insertAdjacentHTML('beforeend', `<span class="jbChip jbColour">${colour}</span>`);
      if (species) chips.insertAdjacentHTML('beforeend', `<span class="jbChip jbSpecies">${species}</span>`);
      a.insertAdjacentElement('afterend', chips);
    }
    th.dataset.jbDecorated = '1';
  }
  /* Extrait petpet / petpetpet (nom + image) du tableau pet_stats.
     1) Cherche les <th>Petpet</th> standards (cas idéal)
     2) Fallback : scan les <td colspan> pour les <img> /items/ (cas quickref
        où petpet/petpetpet sont dans un bloc unique avec "Click here to visit X!")
        → extrait le nom depuis alt / title / texte adjacent. */
  function extractPetpets(petDiv){
    const out = [];
    const tbl = petDiv.querySelector('.pet_stats');
    if (!tbl) return out;
    // 1) Rows standards <th>Petpet</th> / <th>Petpetpet</th>
    [...tbl.querySelectorAll('tr')].forEach(tr => {
      const th = tr.querySelector('th');
      const td = tr.querySelector('td');
      if (!th || !td) return;
      const lbl = (th.textContent || '').trim().replace(/:$/, '');
      if (!/^Petpet(pet)?$/i.test(lbl)) return;
      const img = td.querySelector('img');
      const name = (td.textContent || '').trim();
      if (!name) return;
      out.push({ lbl, name, img: img ? img.src : null });
    });
    if (out.length) return out;
    // 2) Fallback : <td colspan> contenant des imgs /items/
    [...tbl.querySelectorAll('tr')].forEach(tr => {
      const td = tr.querySelector('td[colspan]');
      if (!td) return;
      const imgs = td.querySelectorAll('img[src*="/items/"], img[src*="/petpets/"]');
      if (!imgs.length) return;
      imgs.forEach((img, i) => {
        // Nom depuis alt / title sinon texte text-node après l'img
        let name = (img.alt || img.title || '').trim();
        if (!name){
          let next = img.nextSibling;
          while (next && next.nodeType !== Node.TEXT_NODE) next = next.nextSibling;
          if (next && next.textContent){
            name = next.textContent.trim().split(/[\n\r]|Click here/i)[0].trim();
          }
        }
        // Fallback ultime : parse depuis le src de l'image (slug)
        if (!name){
          const m = img.src.match(/\/items\/([^.\/]+)\./i);
          if (m) name = m[1].replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
        }
        const lbl = (i === 0) ? 'Petpet' : 'Petpetpet';
        if (name) out.push({ lbl, name, img: img.src });
      });
    });
    return out;
  }
  /* S'assure que pet_more est un enfant direct du <td> (sibling de pet_info,
     pet_image) pour que grid-area:notices fonctionne. */
  function ensureNoticesAsSibling(petDiv){
    const more = petDiv.querySelector('.pet_more');
    const info = petDiv.querySelector('.pet_info');
    if (!more || !info) return;
    const td = info.parentElement;
    if (!td) return;
    if (more.parentElement !== td){
      td.appendChild(more);
    }
  }
  /* Masque les lignes du tableau original (utilisé avant rebuild pour
     définir quelles stats vont dans le widget Progression vs la card) */
  function hideRedundantRows(petDiv){
    const tbl = petDiv.querySelector('.pet_stats');
    if (!tbl) return;
    [...tbl.querySelectorAll('tr')].forEach(tr => {
      const th = tr.querySelector('th');
      if (!th) return;
      const lbl = (th.textContent || '').trim().replace(/:$/, '');
      if (/^(Species|Colour|Gender|Level|Strength|Defence|Move|Petpet|Petpetpet)$/i.test(lbl)){
        tr.classList.add('jbHideRow');
      }
    });
  }

  /* Calcule la date de naissance depuis Age (en jours) */
  function birthdayFromAge(td){
    const m = (td.textContent || '').match(/([\d,]+)/);
    if (!m) return null;
    const days = parseInt(m[1].replace(/,/g, ''), 10);
    if (!isFinite(days) || days <= 0) return null;
    return new Date(Date.now() - days * 86400000);
  }
  function fmtBirthday(date){
    return new Intl.DateTimeFormat('fr-FR', { day:'2-digit', month:'long', year:'numeric' }).format(date);
  }

  /* =========================
     FISHING SKILL (et autres extras du lookup) — fetch + cache journalier
  ========================= */
  function loadPetExtra(petName){
    try { return JSON.parse(localStorage.getItem('jb_np_petextra_' + petName) || 'null'); }
    catch { return null; }
  }
  function savePetExtra(petName, payload){
    try { localStorage.setItem('jb_np_petextra_' + petName, JSON.stringify(payload)); } catch {}
  }
  async function fetchPetExtra(petName){
    if (!petName) return null;
    const cached = loadPetExtra(petName);
    if (cached && cached.date === dayKeyNST()) return cached.data;
    try {
      const res = await fetch(`/petlookup.phtml?pet=${encodeURIComponent(petName)}`, { credentials: 'include' });
      if (!res.ok) return cached?.data || null;
      const html = await res.text();
      const m = html.match(/Fishing\s+Skill:?\s*<\/b>\s*([\d,]+)/i);
      const fishing = m ? parseInt(m[1].replace(/,/g, ''), 10) : null;
      const data = { fishing };
      savePetExtra(petName, { date: dayKeyNST(), data });
      return data;
    } catch {
      return cached?.data || null;
    }
  }
  /* Injecte la card Fishing dans la stats grid après fetch (async, non bloquant) */
  async function addFishingCard(petDiv){
    const name = petDiv.id.replace(/_details$/, '');
    if (!name) return;
    const grid = petDiv.querySelector('.jbStatsGrid');
    if (!grid) return;
    if (grid.querySelector('[data-stat="fishing"]')) return;
    const extra = await fetchPetExtra(name);
    if (!extra || extra.fishing == null) return;
    if (grid.querySelector('[data-stat="fishing"]')) return; // re-check après await
    const card = document.createElement('div');
    card.className = 'jbStatCard';
    card.dataset.stat = 'fishing';
    card.innerHTML = `<span class="jbStatLabel">Fishing</span><span class="jbStatValue">${extra.fishing.toLocaleString()}</span>`;
    // Insère Fishing AVANT la card Petpet dédiée si elle existe (pour que
    // l'ordre final soit : Age / Mood / Hunger / Intel / Visit / Fishing / Petpet).
    const petpetBigCard = grid.querySelector('.jbPetpetBigCard');
    if (petpetBigCard){
      grid.insertBefore(card, petpetBigCard);
    } else {
      grid.appendChild(card);
    }
  }

  /* Reconstruit complètement les stats en divs natifs (.jbStatsGrid) à la
     place du <table class='pet_stats'> qui est caché. Plus aucune dépendance
     au hack display:grid/display:contents sur table/tbody. */
  function rebuildStatsGrid(petDiv){
    const tbl = petDiv.querySelector('table.pet_stats');
    if (!tbl) return;
    if (tbl.dataset.jbRebuilt === '1') return;

    const grid = document.createElement('div');
    grid.className = 'jbStatsGrid';

    let petpetHtml = null;
    [...tbl.querySelectorAll('tr')].forEach(tr => {
      if (tr.classList.contains('jbHideRow')) return;
      const colspanTd = tr.querySelector('td[colspan]');
      if (colspanTd){
        petpetHtml = colspanTd.innerHTML;
        return;
      }
      const th = tr.querySelector('th');
      const td = tr.querySelector('td');
      if (!th || !td) return;

      const label = th.textContent.trim().replace(/:$/, '');

      const card = document.createElement('div');
      card.className = 'jbStatCard';

      const lbl = document.createElement('span');
      lbl.className = 'jbStatLabel';
      lbl.textContent = label;

      const val = document.createElement('span');
      val.className = 'jbStatValue';
      val.innerHTML = td.innerHTML;

      // Birthday calculé pour la card Age (inline avec parenthèses)
      if (/^Age/i.test(label)){
        const birth = birthdayFromAge(td);
        if (birth){
          const sub = document.createElement('span');
          sub.className = 'jbBirthday';
          sub.textContent = `(Né·e le ${fmtBirthday(birth)})`;
          val.appendChild(sub);
        }
      }

      card.appendChild(lbl);
      card.appendChild(val);
      grid.appendChild(card);
    });

    // Card colspan "[icones petpet/petpetpet] Click here to visit X!" — on garde
    // celle-ci telle quelle (elle marche bien et reste cliquable).
    if (petpetHtml){
      const card = document.createElement('div');
      card.className = 'jbPetpetCard jbVisitOnly';
      card.innerHTML = petpetHtml;
      grid.appendChild(card);
    }

    // Insère la grid avant le <table>, qui sera caché par CSS
    tbl.parentNode.insertBefore(grid, tbl);
    tbl.dataset.jbRebuilt = '1';
  }
  /* Construit la carte dédiée Petpet/Petpetpet.
     Technique : on n'utilise PAS de <img> mais une <div> avec background-image.
     Une div n'a aucune taille intrinsèque → elle prend exactement la taille
     qu'on lui donne en CSS. Plus aucune surprise possible. */
  function buildPetpetCard(petpets){
    const card = document.createElement('div');
    card.className = 'jbPetpetBigCard';
    petpets.forEach(p => {
      const row = document.createElement('div');
      row.className = 'jbPetpetRow';
      // Échappement minimal pour url() (les guillemets simples dans le src)
      const safeUrl = p.img ? p.img.replace(/'/g, "\\'") : '';
      const imgInner = p.img
        ? `<div class="jbPetpetImg" style="background-image:url('${safeUrl}')"></div>`
        : '<div class="jbPetpetImg jbPetpetNoImg">?</div>';
      row.innerHTML = `
        ${imgInner}
        <div class="jbPetpetMeta">
          <span class="jbPetpetLbl">${p.lbl}</span>
          <span class="jbPetpetName">${p.name}</span>
        </div>
      `;
      card.appendChild(row);
    });
    return card;
  }

  /* =========================
     APPLY ALL
  ========================= */
  function revampPet(petDiv){
    const name = petDiv.id.replace(/_details$/, '');
    applyPetCardTheme(petDiv, name);     // thème per-pet (ou global si pas d'override)
    decorateHeader(petDiv);      // header + HP block Pokemon-style + thème
    hideRedundantRows(petDiv);
    hideHealthRow(petDiv);       // Health est dans le header maintenant
    rebuildStatsGrid(petDiv);    // grid de divs + petpet card en bas
    addFishingCard(petDiv);      // async fetch lookup
    ensureNoticesAsSibling(petDiv);
    addHealOverlay(petDiv);
    injectStatsWidget(petDiv);   // widget BD stats DANS le td (grid item)
    addViewToggle(petDiv);       // bouton switch profil/log dans le header
    buildActions(petDiv);        // actions APRES le widget (= dernier child)
    addThemeCarouselButton(petDiv, name); // bouton cycle thème bas-droit
  }
  function applyAll(){
    swapPose();
    document.querySelectorAll('div[id$="_details"].contentModule').forEach(revampPet);
    // Applique le view mode PER-PET (chaque carte a son mode propre persisté)
    // → déplace le bloc Total gain, synchronise les boutons toggle, set les
    // hauteurs des cards en log.
    applyViewModeAll();
  }

  /* =========================
     BOOT
  ========================= */
  let scheduled = false;
  function scheduleApply(){
    if (scheduled) return;
    scheduled = true;
    requestAnimationFrame(() => { scheduled = false; applyAll(); });
  }
  function boot(){
    // Enregistre la snapshot du jour AVANT le rendering (au cas où les stats
    // sont scrapées juste après que le DOM est prêt)
    recordSnapshot();
    scheduleApply();
  }
  if (document.readyState === 'loading'){
    document.addEventListener('DOMContentLoaded', boot, { once: true });
  } else {
    boot();
  }
  const root = document.querySelector('td.content') || document.body || document.documentElement;
  new MutationObserver(scheduleApply).observe(root, { subtree:true, childList:true });
  // Resize : le col_w change → image height change → faut re-sync les
  // hauteurs des cards summary et main en log mode.
  let _resizeTO = null;
  window.addEventListener('resize', () => {
    if (_resizeTO) cancelAnimationFrame(_resizeTO);
    _resizeTO = requestAnimationFrame(syncLogCardHeightsAll);
  });
})();