Xrem786 / Uniwersalny Licznik Akcji Moderatora dla Hyperreal

// ==UserScript==
// @name         Uniwersalny Licznik Akcji Moderatora dla Hyperreal
// @namespace    http://tampermonkey.net/
// @version      7.1
// @description  Dashboard moderatorski z wykresami Canvas, cache localStorage, heatmapą, timelineą, eksportem CSV i pełną responsywnością mobile.
// @author       Merx
// @copyright 2026, Merx (https://openuserjs.org/users/Xrem786)
// @match        https://hyperreal.info/talk/mcp.php?i=mcp_logs*
// @match        https://hyperreal.info/talk/mcp.php?i=150*
// @grant        none
// @run-at       document-start
// @license      MIT
// ==/UserScript==

(function () {
  'use strict';

  // ═══════════════════════════════════════════════════════════════
  // §1  KONFIGURACJA
  // ═══════════════════════════════════════════════════════════════
  const TOTAL_PAGES = 250;
  const PER_PAGE    = 15;
  const DELAY_MS    = 150;
  const CACHE_TTL   = 6 * 60 * 60 * 1000;          // 6 h
  const CACHE_PFX   = 'mod-counter-';
  const COLLAPSE_KEY = 'mod-counter-collapsed';

  const COLORS = ['#4fc3f7','#81c784','#ffb74d','#e57373','#ba68c8',
                  '#4db6ac','#fff176','#f06292','#90a4ae','#aed581'];
  const C_HI   = '#4fc3f7';
  const C_BAR  = '#546e7a';

  const MONTHS_PL = {
    'stycznia':0,'lutego':1,'marca':2,'kwietnia':3,
    'maja':4,'czerwca':5,'lipca':6,'sierpnia':7,
    'września':8,'października':9,'listopada':10,'grudnia':11
  };
  const MONTH_NAMES = [
    'Styczeń','Luty','Marzec','Kwiecień','Maj','Czerwiec',
    'Lipiec','Sierpień','Wrzesień','Październik','Listopad','Grudzień'
  ];

  const ACTION_GROUPS = [
    { name:'Akceptacja',   re:/^zaakceptowano/i },
    { name:'Usuwanie',     re:/^usunięto/i },
    { name:'Edycja',       re:/^edytowano/i },
    { name:'Blokowanie',   re:/^(za|od)blokowano/i },
    { name:'Przenoszenie', re:/^przeniesiono/i },
    { name:'Zarządzanie',  re:/^(scalono|podzielono|zmieniono|przywrócono)/i },
  ];

  // ═══════════════════════════════════════════════════════════════
  // §2  SVG IKONY (Lucide-style 16×16)
  // ═══════════════════════════════════════════════════════════════
  const I = {
    barChart:  '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><rect x="3" y="12" width="4" height="9" rx="1"/><rect x="10" y="7" width="4" height="14" rx="1"/><rect x="17" y="3" width="4" height="18" rx="1"/></svg>',
    pieChart:  '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M21.21 15.89A10 10 0 1 1 8 2.83"/><path d="M22 12A10 10 0 0 0 12 2v10z"/></svg>',
    list:      '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><circle cx="4" cy="6" r="1" fill="currentColor"/><circle cx="4" cy="12" r="1" fill="currentColor"/><circle cx="4" cy="18" r="1" fill="currentColor"/></svg>',
    trash:     '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/></svg>',
    copy:      '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>',
    download:  '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>',
    refresh:   '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>',
    chevronR:  '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><polyline points="9 18 15 12 9 6"/></svg>',
    chevronD:  '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><polyline points="6 9 12 15 18 9"/></svg>',
    code:      '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>',
    trendUp:   '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><polyline points="23 6 13.5 15.5 8.5 10.5 1 18"/><polyline points="17 6 23 6 23 12"/></svg>',
    trendDn:   '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><polyline points="23 18 13.5 8.5 8.5 13.5 1 6"/><polyline points="17 18 23 18 23 12"/></svg>',
    timeline:  '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>',
    grid:      '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/></svg>',
    back:      '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/></svg>',
    check:     '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><polyline points="20 6 9 17 4 12"/></svg>',
  };

  // ═══════════════════════════════════════════════════════════════
  // §3  STYLE INJECTION (responsywność + animacje + motyw)
  // ═══════════════════════════════════════════════════════════════
  function injectStyles() {
    if (document.getElementById('mc-styles')) return;
    const s = document.createElement('style');
    s.id = 'mc-styles';
    s.textContent = `
      /* --- Karty statystyk --- */
      .mc-cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(110px,1fr));gap:10px;margin-bottom:16px}
      .mc-card{background:var(--mc-card);border-radius:8px;padding:12px 10px;text-align:center;transition:transform .15s,box-shadow .15s}
      .mc-card:hover{transform:translateY(-2px);box-shadow:0 4px 12px rgba(0,0,0,.3)}
      .mc-val{font-size:22px;font-weight:bold;line-height:1.2}
      .mc-lbl{font-size:11px;color:var(--mc-muted);margin-top:4px}
      .mc-trend{font-size:11px;margin-top:2px;font-weight:600}

      /* --- Bottom grid --- */
      .mc-bottom{display:grid;grid-template-columns:280px 1fr;gap:16px;margin-bottom:16px}
      .mc-section-title{font-size:13px;font-weight:bold;margin-bottom:8px;color:var(--mc-text);display:flex;align-items:center;gap:6px}
      .mc-action-row{display:flex;justify-content:space-between;padding:6px 8px;border-radius:4px;margin-bottom:2px;font-size:12px;cursor:default}
      .mc-action-row:nth-child(even){background:var(--mc-card)}

      /* --- Tooltip --- */
      .mc-tip{position:fixed;z-index:10000;background:var(--mc-bg);color:var(--mc-text);border:1px solid var(--mc-border);border-radius:6px;padding:6px 10px;font-size:12px;pointer-events:none;display:none;max-width:260px;box-shadow:0 4px 12px rgba(0,0,0,.4);white-space:nowrap}

      /* --- Skeleton --- */
      .mc-skel{border-radius:4px;background:linear-gradient(90deg,var(--mc-card) 25%,var(--mc-border) 50%,var(--mc-card) 75%);background-size:200% 100%;animation:mc-shimmer 1.5s infinite}
      @keyframes mc-shimmer{0%{background-position:200% 0}100%{background-position:-200% 0}}

      /* --- Confetti overlay --- */
      .mc-confetti{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:100}

      /* --- Heatmap --- */
      .mc-hm-grid{display:grid;grid-template-columns:repeat(auto-fill,16px);gap:3px}
      .mc-hm-cell{width:14px;height:14px;border-radius:3px;cursor:pointer;transition:transform .1s}
      .mc-hm-cell:hover{transform:scale(1.3)}

      /* --- Toggle/collapse chevron --- */
      .mc-chev{display:inline-flex;transition:transform .2s;margin-right:6px}
      .mc-chev.mc-open{transform:rotate(90deg)}

      /* --- Badge --- */
      .mc-badge{display:inline-flex;align-items:center;gap:4px;font-size:11px;padding:2px 8px;border-radius:10px}
      .mc-badge-ok{background:#1b5e20;color:#81c784}
      .mc-badge-warn{background:#4e342e;color:#ffb74d}
      .mc-badge-old{background:#4a1c1c;color:#e57373}

      /* --- Group toggle --- */
      .mc-group-toggle{font-size:11px;cursor:pointer;color:var(--mc-muted);text-decoration:underline;margin-left:8px}
      .mc-group-toggle:hover{color:var(--mc-text)}

      /* --- Detail back button --- */
      .mc-back{display:inline-flex;align-items:center;gap:4px;font-size:12px;cursor:pointer;color:${C_HI};margin-bottom:8px;border:none;background:none;padding:0}
      .mc-back:hover{text-decoration:underline}

      /* --- General --- */
      .mc-clickable{cursor:pointer}
      #mod-counter-container .btn{transition:background .15s,border-color .15s}
      #mod-counter-container .btn:focus-visible{outline:2px solid ${C_HI};outline-offset:2px}
      #mod-counter-container canvas{cursor:pointer}
      .mc-controls{display:flex;align-items:center;gap:10px;flex-wrap:wrap}

      /* --- Dark theme vars (default) --- */
      :root{--mc-bg:#212529;--mc-card:#2b3035;--mc-text:#cfd8dc;--mc-muted:#90a4ae;--mc-border:#37474f}

      /* --- Light override --- */
      body.mc-light{--mc-bg:#f5f5f5;--mc-card:#ffffff;--mc-text:#1a1a2e;--mc-muted:#546e7a;--mc-border:#e0e0e0}

      /* --- Mobile ≤640px --- */
      @media(max-width:640px){
        .mc-cards{grid-template-columns:repeat(2,1fr)!important;gap:8px!important}
        .mc-card{padding:10px 8px!important}
        .mc-val{font-size:18px!important}
        .mc-bottom{grid-template-columns:1fr!important}
        .mc-controls{flex-direction:column!important;align-items:stretch!important;gap:8px!important}
        .mc-controls>*{width:100%!important}
        #mod-counter-container .btn{min-height:44px;font-size:14px!important}
        #mod-counter-container select{min-height:44px;font-size:14px!important}
        .mc-hm-grid{overflow-x:auto}
      }

      /* --- Reduced motion --- */
      @media(prefers-reduced-motion:reduce){
        .mc-card,.mc-hm-cell,.mc-chev{transition:none!important}
        .mc-skel{animation:none!important}
        .mc-confetti{display:none!important}
      }
    `;
    (document.head || document.documentElement).appendChild(s);
  }

  // ═══════════════════════════════════════════════════════════════
  // §4  THEME DETECTION
  // ═══════════════════════════════════════════════════════════════
  function detectTheme() {
    const body = document.body;
    if (!body) return;
    const bg = getComputedStyle(body).backgroundColor;
    if (!bg || bg === 'transparent') return;
    const m = bg.match(/\d+/g);
    if (!m) return;
    const lum = (0.299 * +m[0] + 0.587 * +m[1] + 0.114 * +m[2]) / 255;
    if (lum > 0.5) body.classList.add('mc-light');
  }

  // ═══════════════════════════════════════════════════════════════
  // §5  CACHE (localStorage)
  // ═══════════════════════════════════════════════════════════════
  function cacheKey(mi) {
    const now = new Date();
    const y = mi > now.getMonth() ? now.getFullYear() - 1 : now.getFullYear();
    return `${CACHE_PFX}${y}-${String(mi + 1).padStart(2, '0')}`;
  }
  function loadCache(mi) {
    try { const r = localStorage.getItem(cacheKey(mi)); return r ? JSON.parse(r) : null; }
    catch { return null; }
  }
  function saveCache(mi, stats, daily, monthName) {
    try {
      localStorage.setItem(cacheKey(mi), JSON.stringify({ stats, daily, monthName, timestamp: Date.now() }));
    } catch (e) { console.warn('Cache save failed:', e); }
  }
  function clearAllCache() {
    const keys = [];
    for (let i = 0; i < localStorage.length; i++) {
      const k = localStorage.key(i);
      if (k && k.startsWith(CACHE_PFX)) keys.push(k);
    }
    keys.forEach(k => localStorage.removeItem(k));
  }
  function isStale(c) { return !c || !c.timestamp || Date.now() - c.timestamp > CACHE_TTL; }
  function fmtTs(ts) {
    const d = new Date(ts), p = n => String(n).padStart(2, '0');
    return `${p(d.getDate())}.${p(d.getMonth() + 1)}.${d.getFullYear()}, ${p(d.getHours())}:${p(d.getMinutes())}`;
  }
  function cacheBadge(ts) {
    if (!ts) return '';
    const age = Date.now() - ts;
    if (age < 3600000) return `<span class="mc-badge mc-badge-ok">${I.check} Aktualne</span>`;
    if (age < CACHE_TTL) return `<span class="mc-badge mc-badge-warn">Dane z: ${fmtTs(ts)}</span>`;
    return `<span class="mc-badge mc-badge-old">Nieaktualne — ${fmtTs(ts)}</span>`;
  }

  // ═══════════════════════════════════════════════════════════════
  // §6  UTILS
  // ═══════════════════════════════════════════════════════════════
  function getUser() {
    const el = document.querySelector('.offcanvas-title .username-coloured')
      || document.querySelector('nav .username-coloured');
    return el ? el.textContent.trim() : null;
  }
  function parseDate(s) {
    const t = s.trim().toLowerCase();
    // phpBB relative dates like "40 min temu", "mniej niż minutę temu" etc. are all today
    if (t === 'dzisiaj' || t.includes('temu') || t.includes('minut') || t.includes('godzin')) { 
      const d = new Date(); d.setHours(0,0,0,0); return d; 
    }
    if (t === 'wczoraj') { const d = new Date(); d.setDate(d.getDate()-1); d.setHours(0,0,0,0); return d; }
    const p = s.trim().split(' ');
    if (p.length !== 3) return null;
    const day = parseInt(p[0],10), mo = MONTHS_PL[p[1].toLowerCase()], yr = parseInt(p[2],10);
    if (isNaN(day)||mo===undefined||isNaN(yr)) return null;
    return new Date(yr, mo, day);
  }
  function median(arr) {
    if (!arr.length) return 0;
    const s = [...arr].sort((a,b)=>a-b);
    const m = Math.floor(s.length/2);
    return s.length%2 ? s[m] : (s[m-1]+s[m])/2;
  }
  function trunc(s, n) { return s.length > n ? s.slice(0,n-1)+'…' : s; }
  function easeOut(t) { return 1 - Math.pow(1 - t, 3); }
  function noMotion() { return window.matchMedia('(prefers-reduced-motion:reduce)').matches; }

  function countUp(el, target, duration = 500) {
    if (noMotion() || !target) { el.textContent = target; return; }
    const start = performance.now();
    (function tick(now) {
      const p = Math.min((now - start) / duration, 1);
      el.textContent = Math.round(target * easeOut(p));
      if (p < 1) requestAnimationFrame(tick);
    })(performance.now());
  }

  // ═══════════════════════════════════════════════════════════════
  // §6b  CONFETTI 🎉 (jeśli #1 w rankingu)
  // ═══════════════════════════════════════════════════════════════
  function launchConfetti(container) {
    if (noMotion()) return;
    const wrap = container.closest('.mc-cards') || container;
    wrap.style.position = 'relative';
    const cvs = document.createElement('canvas');
    cvs.className = 'mc-confetti';
    const rect = wrap.getBoundingClientRect();
    cvs.width = rect.width; cvs.height = rect.height;
    wrap.appendChild(cvs);
    const ctx = cvs.getContext('2d');
    const particles = Array.from({length:60}, () => ({
      x: Math.random()*cvs.width, y: -10,
      vx: (Math.random()-.5)*5, vy: Math.random()*3+2,
      c: COLORS[Math.floor(Math.random()*COLORS.length)],
      sz: Math.random()*6+3, r: Math.random()*360, rs: (Math.random()-.5)*10
    }));
    const t0 = performance.now();
    (function draw(now) {
      if (now-t0 > 2000) { cvs.remove(); return; }
      ctx.clearRect(0,0,cvs.width,cvs.height);
      for (const p of particles) {
        p.x+=p.vx; p.y+=p.vy; p.vy+=.12; p.r+=p.rs;
        ctx.save(); ctx.translate(p.x,p.y); ctx.rotate(p.r*Math.PI/180);
        ctx.fillStyle=p.c; ctx.globalAlpha=Math.max(0,1-(now-t0)/2000);
        ctx.fillRect(-p.sz/2,-p.sz/2,p.sz,p.sz); ctx.restore();
      }
      requestAnimationFrame(draw);
    })(t0);
  }

  // ═══════════════════════════════════════════════════════════════
  // §7  ACTION GROUPING
  // ═══════════════════════════════════════════════════════════════
  function groupActions(actions) {
    const grouped = {};
    for (const [action, count] of Object.entries(actions)) {
      const g = ACTION_GROUPS.find(g => g.re.test(action));
      const name = g ? g.name : 'Inne';
      grouped[name] = (grouped[name]||0) + count;
    }
    return grouped;
  }

  // ═══════════════════════════════════════════════════════════════
  // §8  SCANNER (z danymi dziennymi)
  // ═══════════════════════════════════════════════════════════════
  let scanCtrl = null;

  async function scanPages(monthIdx, onProgress) {
    if (scanCtrl) scanCtrl.abort();
    scanCtrl = new AbortController();
    const sig = scanCtrl.signal;
    const stats = {}, daily = {};
    let targetYear = null;

    // Zawsze używaj kanonicznego URL logów — unika problemu z i=150
    const base = 'https://hyperreal.info/talk/mcp.php?i=mcp_logs&mode=front';

    for (let pg = 1; pg <= TOTAL_PAGES; pg++) {
      if (sig.aborted) throw new DOMException('Aborted','AbortError');
      const furl = `${base}&start=${(pg-1)*PER_PAGE}`;
      if (onProgress) onProgress(pg, TOTAL_PAGES);
      const res = await fetch(furl, {signal:sig});
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      const doc = new DOMParser().parseFromString(await res.text(), 'text/html');

      // Wiersze logów: <div class="row m-0 mt-1 p-1 align-items-center bg1">
      // Nie są bezpośrednimi dziećmi .card, więc szukamy bez >
      const rows = doc.querySelectorAll('.card.mb-3.pb-1 .row.bg1');
      if (!rows.length) break;
      let stop = false;
      for (const row of rows) {
        // Struktura: [col-md-6 akcja] [col-md-2 moderator+IP] [col-md-2 czas]
        const cols = row.querySelectorAll(':scope > div[class*="col-md-2"]');
        const dEl = cols.length >= 2 ? cols[cols.length - 1] : null;
        if (!dEl) continue;
        const rawDate = dEl.textContent.replace(/Czas:/i,'').trim();
        const d = parseDate(rawDate);
        if (!d) continue;
        const eM = d.getMonth(), eY = d.getFullYear();
        if (eM === monthIdx) {
          if (targetYear === null) targetYear = eY;
          if (eY === targetYear) {
            const mEl = row.querySelector('a.username-coloured');
            const aEl = row.querySelector('div[class*="col-md-6"] strong');
            if (!mEl||!aEl) continue;
            const mod = mEl.textContent.trim();
            const act = aEl.textContent.trim().split('\u201E')[0].split('„')[0].trim();
            if (!stats[mod]) stats[mod] = {};
            stats[mod][act] = (stats[mod][act]||0) + 1;
            // daily
            const dk = `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`;
            if (!daily[mod]) daily[mod] = {};
            daily[mod][dk] = (daily[mod][dk]||0) + 1;
          } else { stop=true; break; }
        } else if (targetYear!==null && (eY<targetYear||(eY===targetYear&&eM<monthIdx))) {
          stop=true; break;
        }
      }
      if (stop) break;
      await new Promise(r=>setTimeout(r,DELAY_MS));
    }
    scanCtrl = null;
    return { stats, daily };
  }

  // ═══════════════════════════════════════════════════════════════
  // §9  BBCode GENERATOR
  // ═══════════════════════════════════════════════════════════════
  function genBBCode(stats, monthName) {
    const proc = Object.entries(stats).map(([n,a])=>({name:n,actions:a,total:Object.values(a).reduce((s,c)=>s+c,0)})).sort((a,b)=>b.total-a.total);
    let bb = `[size=150][b]Statystyki działalności moderatorów — ${monthName}[/b][/size]\n\n`;
    proc.forEach((m,i)=>{
      if(i>0)bb+='\n[hr]\n\n';
      bb+=`[mention]${m.name}[/mention] — łącznie: [b][size=150][color=red]${m.total}[/color][/size][/b]\n[list]\n`;
      Object.entries(m.actions).sort((a,b)=>b[1]-a[1]).forEach(([a,c])=>{bb+=`[*]${a}: [b]${c}[/b]\n`;});
      bb+='[/list]\n';
    });
    return bb;
  }

  // ═══════════════════════════════════════════════════════════════
  // §10  CSV EXPORT
  // ═══════════════════════════════════════════════════════════════
  function exportCSV(stats, monthName) {
    let csv = '\uFEFFModerator;Akcja;Ilość\n';
    for (const [mod, actions] of Object.entries(stats)) {
      for (const [act, cnt] of Object.entries(actions)) {
        csv += `${mod};${act};${cnt}\n`;
      }
    }
    const blob = new Blob([csv], {type:'text/csv;charset=utf-8;'});
    const a = document.createElement('a');
    a.href = URL.createObjectURL(blob);
    a.download = `moderacja_${monthName.toLowerCase()}.csv`;
    a.click();
    URL.revokeObjectURL(a.href);
  }

  // ═══════════════════════════════════════════════════════════════
  // §11  TOOLTIP SYSTEM
  // ═══════════════════════════════════════════════════════════════
  let tipEl = null;
  function showTip(x, y, html) {
    if (!tipEl) { tipEl = document.createElement('div'); tipEl.className='mc-tip'; document.body.appendChild(tipEl); }
    tipEl.innerHTML = html;
    tipEl.style.display = 'block';
    // Clamp to viewport
    const vw = window.innerWidth, vh = window.innerHeight;
    let left = x + 14, top = y - 10;
    tipEl.style.left = left+'px'; tipEl.style.top = top+'px';
    const r = tipEl.getBoundingClientRect();
    if (r.right > vw) tipEl.style.left = (x - r.width - 8)+'px';
    if (r.bottom > vh) tipEl.style.top = (y - r.height - 8)+'px';
  }
  function hideTip() { if (tipEl) tipEl.style.display = 'none'; }

  // ═══════════════════════════════════════════════════════════════
  // §12  CHARTS — Canvas API
  // ═══════════════════════════════════════════════════════════════

  // --- 12a: Bar chart (ranking) ---
  function drawBarChart(canvas, processed, currentUser, onBarClick) {
    const ctx = canvas.getContext('2d');
    const dpr = window.devicePixelRatio || 1;
    const mobile = (canvas.parentElement.clientWidth || 600) < 500;
    const barH = mobile ? 24 : 28, gap = mobile ? 4 : 6;
    const lblW = mobile ? 90 : 160;
    const pad = { t:10, r:70, b:10, l:lblW+10 };
    const W = canvas.parentElement.clientWidth || 600;
    const H = pad.t+pad.b+processed.length*(barH+gap)-gap;
    canvas.width=W*dpr; canvas.height=H*dpr;
    canvas.style.width=W+'px'; canvas.style.height=H+'px';
    ctx.scale(dpr,dpr);
    const max = processed.length ? processed[0].total : 1;
    const barW_area = W-pad.l-pad.r;
    const rects = [];

    processed.forEach((m,i)=>{
      const y = pad.t+i*(barH+gap);
      const w = Math.max(4, (m.total/max)*barW_area);
      const isMe = currentUser && m.name.toLowerCase()===currentUser.toLowerCase();
      ctx.fillStyle = isMe ? C_HI : C_BAR;
      ctx.beginPath(); ctx.roundRect(pad.l,y,w,barH,4); ctx.fill();
      // Label
      ctx.fillStyle = isMe ? C_HI : '#cfd8dc';
      ctx.font = (isMe?'bold ':'') + (mobile?'10':'12') + 'px system-ui,sans-serif';
      ctx.textAlign='right'; ctx.textBaseline='middle';
      const label = isMe ? `★ ${trunc(m.name,mobile?10:20)}` : trunc(m.name,mobile?10:20);
      ctx.fillText(label, pad.l-8, y+barH/2);
      // Count
      ctx.fillStyle = isMe ? '#fff' : '#90a4ae';
      ctx.font = (isMe?'bold ':'') + (mobile?'11':'13') + 'px system-ui,sans-serif';
      ctx.textAlign='left';
      ctx.fillText(m.total.toString(), pad.l+w+8, y+barH/2);
      rects.push({x:pad.l,y,w,h:barH,mod:m});
    });

    // Hover + tooltip
    let hoverIdx = -1;
    canvas.onmousemove = (e) => {
      const cr = canvas.getBoundingClientRect();
      const mx=e.clientX-cr.left, my=e.clientY-cr.top;
      let found = -1;
      for (let i=0;i<rects.length;i++) {
        const r=rects[i];
        if (mx>=0 && mx<=W && my>=r.y && my<=r.y+r.h) { found=i; break; }
      }
      if (found !== hoverIdx) {
        hoverIdx = found;
        if (found >= 0) {
          const m = rects[found].mod;
          const total = processed.reduce((s,x)=>s+x.total,0);
          showTip(e.clientX, e.clientY, `<b>${m.name}</b>: ${m.total} akcji (${((m.total/total)*100).toFixed(1)}%)`);
        } else { hideTip(); }
      } else if (found >= 0) {
        showTip(e.clientX, e.clientY, tipEl.innerHTML);
      }
    };
    canvas.onmouseleave = () => { hoverIdx=-1; hideTip(); };

    // Click → detail
    canvas.onclick = (e) => {
      const cr = canvas.getBoundingClientRect();
      const mx=e.clientX-cr.left, my=e.clientY-cr.top;
      for (const r of rects) {
        if (mx>=0 && mx<=W && my>=r.y && my<=r.y+r.h) {
          hideTip();
          if (onBarClick) onBarClick(r.mod);
          break;
        }
      }
    };

    // Touch support
    canvas.ontouchstart = (e) => {
      const t = e.touches[0], cr = canvas.getBoundingClientRect();
      const mx=t.clientX-cr.left, my=t.clientY-cr.top;
      for (const r of rects) {
        if (mx>=0 && mx<=W && my>=r.y && my<=r.y+r.h) {
          if (onBarClick) onBarClick(r.mod);
          break;
        }
      }
    };
  }

  // --- 12b: Donut chart ---
  function drawDonutChart(canvas, actions) {
    const ctx = canvas.getContext('2d');
    const dpr = window.devicePixelRatio || 1;
    const sz = Math.min(canvas.parentElement.clientWidth || 260, 260);
    canvas.width=sz*dpr; canvas.height=sz*dpr;
    canvas.style.width=sz+'px'; canvas.style.height=sz+'px';
    ctx.scale(dpr,dpr);
    const sorted = Object.entries(actions).sort((a,b)=>b[1]-a[1]);
    const total = sorted.reduce((s,[,c])=>s+c,0);
    const slices=[]; let other=0;
    sorted.forEach(([n,c],i)=>{
      if(i<5) slices.push({name:n,count:c,pct:((c/total)*100).toFixed(1)});
      else other+=c;
    });
    if(other>0) slices.push({name:'Inne',count:other,pct:((other/total)*100).toFixed(1)});
    const cx=sz/2, cy=sz/2, oR=sz/2-10, iR=oR*.55;
    let ang = -Math.PI/2;
    const sliceAngles = [];
    slices.forEach((s,i)=>{
      const sa = (s.count/total)*2*Math.PI;
      ctx.beginPath(); ctx.arc(cx,cy,oR,ang,ang+sa); ctx.arc(cx,cy,iR,ang+sa,ang,true); ctx.closePath();
      ctx.fillStyle=COLORS[i%COLORS.length]; ctx.fill();
      sliceAngles.push({start:ang, end:ang+sa, slice:s, color:COLORS[i%COLORS.length]});
      ang+=sa;
    });
    ctx.fillStyle='var(--mc-text,#cfd8dc)'; ctx.font='bold 26px system-ui,sans-serif';
    ctx.textAlign='center'; ctx.textBaseline='middle';
    ctx.fillStyle='#cfd8dc'; ctx.fillText(total.toString(),cx,cy-8);
    ctx.font='11px system-ui,sans-serif'; ctx.fillStyle='#90a4ae';
    ctx.fillText('akcji',cx,cy+14);

    // Tooltip
    canvas.onmousemove = (e) => {
      const cr=canvas.getBoundingClientRect();
      const mx=e.clientX-cr.left, my=e.clientY-cr.top;
      const dx=mx-cx, dy=my-cy, dist=Math.sqrt(dx*dx+dy*dy);
      if (dist<iR||dist>oR) { hideTip(); return; }
      let angle = Math.atan2(dy,dx);
      if (angle < -Math.PI/2) angle += 2*Math.PI;
      for (const sa of sliceAngles) {
        let s=sa.start, en=sa.end;
        if (s<-Math.PI/2) s+=2*Math.PI;
        if (en<-Math.PI/2) en+=2*Math.PI;
        if (angle>=sa.start && angle<sa.end) {
          showTip(e.clientX,e.clientY,`<span style="color:${sa.color}">●</span> ${sa.slice.name}: <b>${sa.slice.count}</b> (${sa.slice.pct}%)`);
          return;
        }
      }
      hideTip();
    };
    canvas.onmouseleave = () => hideTip();
    return slices;
  }

  // --- 12c: Line chart (timeline multi-month) ---
  function drawLineChart(canvas, currentUser) {
    const months = [];
    for (let i = 0; i < localStorage.length; i++) {
      const k = localStorage.key(i);
      if (!k || !k.startsWith(CACHE_PFX)) continue;
      try {
        const data = JSON.parse(localStorage.getItem(k));
        if (!data || !data.stats) continue;
        const ym = k.replace(CACHE_PFX,'');
        const [yr,mo] = ym.split('-').map(Number);
        const uStats = currentUser ? data.stats[currentUser] : null;
        const total = uStats ? Object.values(uStats).reduce((s,c)=>s+c,0) : 0;
        months.push({key:ym, yr, mo:mo-1, label:MONTH_NAMES[mo-1]?.slice(0,3)||'?', total});
      } catch {}
    }
    if (months.length < 2) { canvas.style.display='none'; return false; }
    months.sort((a,b)=>a.key.localeCompare(b.key));

    const ctx = canvas.getContext('2d');
    const dpr = window.devicePixelRatio || 1;
    const W = canvas.parentElement.clientWidth || 400, H = 140;
    canvas.width=W*dpr; canvas.height=H*dpr;
    canvas.style.width=W+'px'; canvas.style.height=H+'px';
    ctx.scale(dpr,dpr);
    const pad = {t:20,r:20,b:30,l:40};
    const aW = W-pad.l-pad.r, aH = H-pad.t-pad.b;
    const maxV = Math.max(...months.map(m=>m.total),1);

    // Grid lines
    ctx.strokeStyle='#37474f'; ctx.lineWidth=1;
    for (let i=0;i<=4;i++) {
      const y = pad.t+aH*(1-i/4);
      ctx.beginPath(); ctx.moveTo(pad.l,y); ctx.lineTo(W-pad.r,y); ctx.stroke();
      ctx.fillStyle='#546e7a'; ctx.font='10px system-ui,sans-serif';
      ctx.textAlign='right'; ctx.textBaseline='middle';
      ctx.fillText(Math.round(maxV*i/4).toString(), pad.l-6, y);
    }

    // Points + line
    const points = months.map((m,i)=>{
      const x = pad.l + (months.length===1?aW/2:i/(months.length-1)*aW);
      const y = pad.t + aH*(1-m.total/maxV);
      return {x,y,...m};
    });

    ctx.strokeStyle=C_HI; ctx.lineWidth=2.5; ctx.lineJoin='round';
    ctx.beginPath();
    points.forEach((p,i)=>i===0?ctx.moveTo(p.x,p.y):ctx.lineTo(p.x,p.y));
    ctx.stroke();

    // Fill under
    ctx.globalAlpha=.1; ctx.fillStyle=C_HI;
    ctx.beginPath();
    ctx.moveTo(points[0].x,pad.t+aH);
    points.forEach(p=>ctx.lineTo(p.x,p.y));
    ctx.lineTo(points[points.length-1].x,pad.t+aH);
    ctx.closePath(); ctx.fill(); ctx.globalAlpha=1;

    // Dots + labels
    points.forEach(p=>{
      ctx.fillStyle=C_HI;
      ctx.beginPath(); ctx.arc(p.x,p.y,4,0,Math.PI*2); ctx.fill();
      ctx.fillStyle='#fff';
      ctx.beginPath(); ctx.arc(p.x,p.y,2,0,Math.PI*2); ctx.fill();
      // X label
      ctx.fillStyle='#90a4ae'; ctx.font='10px system-ui,sans-serif';
      ctx.textAlign='center'; ctx.textBaseline='top';
      ctx.fillText(p.label, p.x, H-pad.b+6);
      // Value above dot
      ctx.fillStyle='#cfd8dc'; ctx.font='bold 10px system-ui,sans-serif';
      ctx.textBaseline='bottom';
      ctx.fillText(p.total.toString(), p.x, p.y-8);
    });
    return true;
  }

  // --- 12d: Heatmap (aktywność dzienna — styl GitHub) ---
  function renderHeatmap(container, dailyData, monthIdx) {
    container.innerHTML = '';
    if (!dailyData || !Object.keys(dailyData).length) {
      container.innerHTML='<div style="color:var(--mc-muted);font-size:12px">Brak danych dziennych.</div>';
      return;
    }
    const now = new Date();
    const year = monthIdx > now.getMonth() ? now.getFullYear()-1 : now.getFullYear();
    const daysInMonth = new Date(year, monthIdx+1, 0).getDate();
    const counts = [];
    let maxC = 0;
    for (let d=1;d<=daysInMonth;d++) {
      const key = `${year}-${String(monthIdx+1).padStart(2,'0')}-${String(d).padStart(2,'0')}`;
      const c = dailyData[key]||0;
      counts.push({day:d,count:c,key});
      if(c>maxC) maxC=c;
    }

    const grid = document.createElement('div');
    grid.className='mc-hm-grid';
    grid.style.gridTemplateColumns=`repeat(${Math.min(daysInMonth,Math.ceil(daysInMonth/1))},16px)`;

    counts.forEach(c=>{
      const cell = document.createElement('div');
      cell.className='mc-hm-cell';
      const intensity = maxC ? c.count/maxC : 0;
      if (c.count===0) {
        cell.style.background='var(--mc-card)';
      } else {
        const alpha = 0.2 + intensity * 0.8;
        cell.style.background=`rgba(79,195,247,${alpha})`;
      }
      cell.title=`${c.day}. — ${c.count} akcji`;
      cell.addEventListener('mouseenter',(e)=>showTip(e.clientX,e.clientY,`<b>Dzień ${c.day}</b>: ${c.count} akcji`));
      cell.addEventListener('mouseleave',hideTip);
      grid.appendChild(cell);
    });

    // Day numbers below
    const labels = document.createElement('div');
    labels.style.cssText='display:flex;gap:3px;margin-top:4px;font-size:9px;color:var(--mc-muted)';
    for (let d=1;d<=daysInMonth;d++) {
      const l = document.createElement('div');
      l.style.cssText='width:14px;text-align:center';
      l.textContent = d%5===0||d===1?d:'';
      labels.appendChild(l);
    }

    container.appendChild(grid);
    container.appendChild(labels);
  }

  // ═══════════════════════════════════════════════════════════════
  // §13  SKELETON LOADING
  // ═══════════════════════════════════════════════════════════════
  function renderSkeleton(el) {
    el.innerHTML = `
      <div style="padding:5px 0">
        <div class="mc-cards" style="margin-bottom:16px">
          ${[1,2,3,4].map(()=>'<div class="mc-card"><div class="mc-skel" style="height:50px"></div></div>').join('')}
        </div>
        <div class="mc-skel" style="height:120px;border-radius:6px;margin-bottom:12px"></div>
        <div class="mc-bottom">
          <div class="mc-skel" style="height:200px;border-radius:6px"></div>
          <div class="mc-skel" style="height:200px;border-radius:6px"></div>
        </div>
      </div>`;
  }

  // ═══════════════════════════════════════════════════════════════
  // §14  DASHBOARD RENDERER
  // ═══════════════════════════════════════════════════════════════
  let _dashState = {};   // przechowuje kontekst do re-renderowania detali

  function renderDashboard(stats, daily, monthName, timestamp, monthIdx) {
    const result = document.getElementById('mc-result');
    if (!result) return;
    result.innerHTML = '';

    const user = getUser();
    const proc = Object.entries(stats).map(([n,a])=>({
      name:n, actions:a, total:Object.values(a).reduce((s,c)=>s+c,0)
    })).sort((a,b)=>b.total-a.total);

    if (!proc.length) {
      result.innerHTML='<div style="padding:15px;color:var(--mc-muted)">Brak danych dla wybranego miesiąca.</div>';
      return;
    }

    _dashState = { stats, daily, proc, user, monthName, monthIdx, timestamp };

    const teamTotal = proc.reduce((s,m)=>s+m.total,0);
    const teamAvg = (teamTotal/proc.length).toFixed(1);
    const teamMed = median(proc.map(m=>m.total));
    const my = user ? proc.find(m=>m.name.toLowerCase()===user.toLowerCase()) : null;
    const myTotal = my?my.total:0;
    const myRank = my?proc.indexOf(my)+1:'-';
    const myPct = my?((myTotal/teamTotal)*100).toFixed(1):'0';

    // Trend vs prev month
    let trendHtml = '';
    if (my) {
      const prevMi = (monthIdx-1+12)%12;
      const prevCache = loadCache(prevMi);
      if (prevCache && prevCache.stats && prevCache.stats[my.name]) {
        const prevTotal = Object.values(prevCache.stats[my.name]).reduce((s,c)=>s+c,0);
        if (prevTotal > 0) {
          const pct = ((myTotal-prevTotal)/prevTotal*100).toFixed(0);
          const up = myTotal >= prevTotal;
          trendHtml = `<div class="mc-trend" style="color:${up?'#81c784':'#e57373'}">${up?I.trendUp:I.trendDn} ${up?'+':''}${pct}% vs ${MONTH_NAMES[prevMi].slice(0,3)}</div>`;
        }
      }
    }

    const wrap = document.createElement('div');
    wrap.style.padding='5px 0';

    // --- Header z badge ---
    const hdr = document.createElement('div');
    hdr.style.cssText='display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;flex-wrap:wrap;gap:5px';
    hdr.innerHTML=`<div style="font-size:15px;font-weight:bold;display:flex;align-items:center;gap:6px">${I.barChart} Statystyki — ${monthName}</div><div>${cacheBadge(timestamp)}</div>`;
    wrap.appendChild(hdr);

    // --- Stat cards ---
    if (my) {
      const cards = document.createElement('div');
      cards.className='mc-cards';
      const data = [
        {lbl:'Twoje akcje',val:myTotal,color:C_HI,trend:trendHtml,animate:true},
        {lbl:'Ranking',val:`#${myRank} / ${proc.length}`,color:'#81c784'},
        {lbl:'% wkładu',val:`${myPct}%`,color:'#ffb74d'},
        {lbl:'Śr. drużyny',val:teamAvg,color:'#ba68c8'},
        {lbl:'Mediana',val:teamMed,color:'#4db6ac'},
      ];
      data.forEach(cd=>{
        const c = document.createElement('div');
        c.className='mc-card';
        const valEl = document.createElement('div');
        valEl.className='mc-val';
        valEl.style.color=cd.color;
        if (cd.animate && typeof cd.val==='number') {
          valEl.textContent='0';
          setTimeout(()=>countUp(valEl,cd.val,600),100);
        } else {
          valEl.textContent=cd.val;
        }
        c.appendChild(valEl);
        c.insertAdjacentHTML('beforeend',`<div class="mc-lbl">${cd.lbl}</div>`);
        if(cd.trend) c.insertAdjacentHTML('beforeend',cd.trend);
        cards.appendChild(c);
      });
      wrap.appendChild(cards);

      // Confetti if #1 🎉
      if (myRank === 1) setTimeout(()=>launchConfetti(cards), 300);
    }

    // --- Bar chart ---
    const barSec = document.createElement('div');
    barSec.style.marginBottom='16px';
    barSec.innerHTML=`<div class="mc-section-title">${I.barChart} Ranking moderatorów <span style="font-weight:normal;font-size:11px;color:var(--mc-muted)">(kliknij słupek)</span></div>`;
    const barCvs = document.createElement('canvas');
    barSec.appendChild(barCvs);
    wrap.appendChild(barSec);

    // --- Bottom detail section ---
    const bottomWrap = document.createElement('div');
    bottomWrap.id='mc-bottom-detail';
    wrap.appendChild(bottomWrap);

    // Render bottom for current user (or first mod if no user)
    function renderBottom(modData) {
      bottomWrap.innerHTML='';
      if (!modData) return;
      const isMe = user && modData.name.toLowerCase()===user.toLowerCase();

      // Back button if viewing someone else
      if (!isMe) {
        const back = document.createElement('button');
        back.type='button';
        back.className='mc-back';
        back.innerHTML=`${I.back} Powrót do moich statystyk`;
        back.onclick=()=>renderBottom(my||proc[0]);
        bottomWrap.appendChild(back);
      }

      const viewLabel = isMe ? 'Twoje' : modData.name;

      const row = document.createElement('div');
      row.className='mc-bottom';

      // Donut
      const donutSec = document.createElement('div');
      donutSec.style.textAlign='center';
      // Group toggle
      let showGrouped = false;
      const titleRow = document.createElement('div');
      titleRow.className='mc-section-title';
      titleRow.style.justifyContent='center';
      titleRow.innerHTML=`${I.pieChart} Podział akcji (${viewLabel})`;
      const toggleGrp = document.createElement('span');
      toggleGrp.className='mc-group-toggle';
      toggleGrp.textContent='grupuj';
      titleRow.appendChild(toggleGrp);
      donutSec.appendChild(titleRow);

      const donutCvs = document.createElement('canvas');
      donutSec.appendChild(donutCvs);
      const legendDiv = document.createElement('div');
      legendDiv.style.cssText='margin-top:8px;text-align:left;padding-left:10px';
      donutSec.appendChild(legendDiv);

      function redrawDonut() {
        legendDiv.innerHTML='';
        const acts = showGrouped ? groupActions(modData.actions) : modData.actions;
        const slices = drawDonutChart(donutCvs, acts);
        slices.forEach((s,i)=>{
          const item = document.createElement('div');
          item.style.cssText='display:flex;align-items:center;gap:6px;margin-bottom:3px;font-size:11px';
          item.innerHTML=`<span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:${COLORS[i%COLORS.length]}"></span><span style="color:var(--mc-text)">${s.name}</span><span style="color:var(--mc-muted);margin-left:auto">${s.pct}%</span>`;
          legendDiv.appendChild(item);
        });
        toggleGrp.textContent = showGrouped ? 'szczegóły' : 'grupuj';
      }
      toggleGrp.onclick = () => { showGrouped=!showGrouped; redrawDonut(); };

      row.appendChild(donutSec);

      // Top actions list
      const topSec = document.createElement('div');
      topSec.innerHTML=`<div class="mc-section-title">${I.list} Top akcje (${viewLabel})</div>`;
      const topList = document.createElement('div');
      Object.entries(modData.actions).sort((a,b)=>b[1]-a[1]).forEach(([act,cnt],i)=>{
        const r = document.createElement('div');
        r.className='mc-action-row';
        r.innerHTML=`<span style="color:var(--mc-text)">${act}</span><span style="font-weight:bold;color:${COLORS[i%COLORS.length]}">${cnt}</span>`;
        topList.appendChild(r);
      });
      topSec.appendChild(topList);
      row.appendChild(topSec);
      bottomWrap.appendChild(row);

      // --- Heatmap (daily activity) ---
      const modDaily = (daily && daily[modData.name]) ? daily[modData.name] : null;
      if (modDaily) {
        const hmSec = document.createElement('div');
        hmSec.style.marginBottom='16px';
        hmSec.innerHTML=`<div class="mc-section-title">${I.grid} Aktywność dzienna (${viewLabel})</div>`;
        const hmContainer = document.createElement('div');
        hmSec.appendChild(hmContainer);
        bottomWrap.appendChild(hmSec);
        renderHeatmap(hmContainer, modDaily, monthIdx);
      }

      setTimeout(redrawDonut, 0);
    }

    // --- Line chart (timeline — multi-month) ---
    if (user) {
      const lineSec = document.createElement('div');
      lineSec.style.marginBottom='16px';
      lineSec.innerHTML=`<div class="mc-section-title">${I.timeline} Twoja aktywność w czasie</div>`;
      const lineCvs = document.createElement('canvas');
      lineSec.appendChild(lineCvs);
      wrap.appendChild(lineSec);
      setTimeout(()=>{
        const ok = drawLineChart(lineCvs, user);
        if (!ok) lineSec.style.display='none';
      },0);
    }

    // --- BBCode + CSV (ukryte) ---
    const actionsSec = document.createElement('div');
    actionsSec.style.marginTop='8px';
    const actRow = document.createElement('div');
    actRow.style.cssText='display:flex;gap:8px;align-items:center;flex-wrap:wrap';

    const togBB = mkBtn('btn-outline-secondary', `${I.chevronR} Pokaż BBCode`);
    const cpBB = mkBtn('btn-primary', `${I.copy} Kopiuj BBCode`);
    cpBB.style.display='none';
    const dlCSV = mkBtn('btn-outline-secondary', `${I.download} CSV`);

    const bbTA = document.createElement('textarea');
    bbTA.className='form-control';
    bbTA.style.cssText='width:100%;height:260px;margin-top:8px;font-family:monospace;font-size:11px;display:none;resize:vertical';
    bbTA.readOnly=true;
    bbTA.value=genBBCode(stats,monthName);

    togBB.onclick=()=>{
      const show=bbTA.style.display==='none';
      bbTA.style.display=show?'block':'none';
      cpBB.style.display=show?'inline-flex':'none';
      togBB.innerHTML=show?`${I.chevronD} Ukryj BBCode`:`${I.chevronR} Pokaż BBCode`;
    };
    cpBB.onclick=()=>{
      navigator.clipboard.writeText(bbTA.value).then(()=>{
        cpBB.innerHTML=`${I.check} Skopiowano!`;
        setTimeout(()=>{cpBB.innerHTML=`${I.copy} Kopiuj BBCode`;},2000);
      }).catch(()=>{bbTA.focus();bbTA.select();document.execCommand('copy');
        cpBB.innerHTML=`${I.check} Skopiowano!`;
        setTimeout(()=>{cpBB.innerHTML=`${I.copy} Kopiuj BBCode`;},2000);
      });
    };
    dlCSV.onclick=()=>exportCSV(stats,monthName);

    actRow.append(togBB,cpBB,dlCSV);
    actionsSec.append(actRow,bbTA);
    wrap.appendChild(actionsSec);

    result.appendChild(wrap);

    // Draw charts after DOM attach
    setTimeout(()=>{
      drawBarChart(barCvs, proc, user, (mod)=>renderBottom(mod));
      renderBottom(my || proc[0]);
    },0);
  }

  // ═══════════════════════════════════════════════════════════════
  // §15  SCAN RUNNER
  // ═══════════════════════════════════════════════════════════════
  async function runScan(mi, mName) {
    const prog = document.getElementById('mc-progress');
    const pbar = document.getElementById('mc-pbar');
    const refBtn = document.getElementById('mc-refresh');
    const scanBtn = document.getElementById('mc-scan');
    if(refBtn){refBtn.disabled=true;refBtn.innerHTML=`${I.refresh} Skanowanie...`;}
    if(scanBtn)scanBtn.disabled=true;
    if(prog)prog.style.display='block';
    if(pbar){pbar.style.display='block';pbar.value=0;}

    try {
      const {stats,daily} = await scanPages(mi,(pg,tot)=>{
        if(prog)prog.textContent=`Skanowanie strony ${pg} / ${tot}...`;
        if(pbar){pbar.value=pg;pbar.max=tot;}
      });
      saveCache(mi,stats,daily,mName);
      if(Object.keys(stats).length) {
        renderDashboard(stats,daily,mName,Date.now(),mi);
      } else {
        const r=document.getElementById('mc-result');
        if(r)r.innerHTML='<div style="padding:10px;color:var(--mc-muted)">Brak akcji w wybranym miesiącu.</div>';
      }
    } catch(e) {
      if(e.name==='AbortError')return;
      console.error(e);
      if(prog){prog.textContent=`Błąd: ${e.message}`;prog.style.color='#ff6b6b';prog.style.display='block';}
    } finally {
      if(pbar)pbar.style.display='none';
      if(prog&&prog.style.color!=='rgb(255, 107, 107)')prog.style.display='none';
      if(refBtn){refBtn.disabled=false;refBtn.innerHTML=`${I.refresh} Odśwież`;}
      if(scanBtn)scanBtn.disabled=false;
    }
  }

  // ═══════════════════════════════════════════════════════════════
  // §16  UI BUILDER
  // ═══════════════════════════════════════════════════════════════
  function mkBtn(cls, html) {
    const b = document.createElement('div');
    b.className=`btn btn-sm ${cls}`;
    b.style.cssText='font-size:12px;display:inline-flex;align-items:center;gap:4px;cursor:pointer';
    b.innerHTML=html;
    return b;
  }

  function createUI() {
    if (document.getElementById('mc-container')) return;
    const target = document.querySelector('h4.cp-page-title');
    if (!target) return;

    injectStyles();
    detectTheme();

    const ctr = document.createElement('div');
    ctr.id='mc-container'; ctr.className='card mb-3';

    // --- Header (collapsible) ---
    const hdr = document.createElement('div');
    hdr.className='card-header mc-clickable';
    hdr.style.cssText='display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:8px;user-select:none';

    const collapsed = localStorage.getItem(COLLAPSE_KEY)==='1';

    const titleSpan = document.createElement('span');
    titleSpan.style.cssText='font-weight:bold;display:flex;align-items:center;gap:6px';
    titleSpan.innerHTML=`<span class="mc-chev${collapsed?'':' mc-open'}">${I.chevronR}</span>Licznik Akcji Moderatorów`;

    const clearBtn = mkBtn('btn-outline-danger', `${I.trash} Wyczyść cache`);
    clearBtn.style.fontSize='11px';
    clearBtn.onclick=(e)=>{
      e.stopPropagation();
      clearAllCache();
      const r=document.getElementById('mc-result'); if(r)r.innerHTML='';
      clearBtn.innerHTML=`${I.check} Wyczyszczono`;
      setTimeout(()=>{clearBtn.innerHTML=`${I.trash} Wyczyść cache`;},2000);
    };

    hdr.append(titleSpan, clearBtn);
    ctr.appendChild(hdr);

    // --- Body ---
    const body = document.createElement('div');
    body.className='card-body';
    body.id='mc-body';
    body.style.padding='12px 15px';
    if (collapsed) body.style.display='none';

    // Toggle collapse
    hdr.onclick=()=>{
      const isCollapsed=body.style.display==='none';
      body.style.display=isCollapsed?'':'none';
      titleSpan.querySelector('.mc-chev').classList.toggle('mc-open',isCollapsed);
      localStorage.setItem(COLLAPSE_KEY,isCollapsed?'0':'1');
    };

    // Controls
    const controls = document.createElement('div');
    controls.className='mc-controls';

    const label = document.createElement('label');
    label.textContent='Miesiąc:'; label.style.fontWeight='500';

    const sel = document.createElement('select');
    sel.id='mc-month'; sel.className='form-select form-select-sm'; sel.style.width='auto';
    const curMo = new Date().getMonth();
    MONTH_NAMES.forEach((m,i)=>{
      const o=document.createElement('option'); o.value=i; o.textContent=m;
      if(i===curMo)o.selected=true; sel.appendChild(o);
    });

    const scanBtn = mkBtn('btn-primary','Skanuj');
    scanBtn.id='mc-scan';

    const refBtn = mkBtn('btn-outline-secondary',`${I.refresh} Odśwież`);
    refBtn.id='mc-refresh'; refBtn.style.display='none';

    function doScan(){runScan(parseInt(sel.value,10),MONTH_NAMES[parseInt(sel.value,10)]);}
    scanBtn.onclick=doScan;
    refBtn.onclick=doScan;

    sel.onchange=()=>{
      const mi=parseInt(sel.value,10);
      const c=loadCache(mi);
      const r=document.getElementById('mc-result');
      if(c&&Object.keys(c.stats).length){
        renderDashboard(c.stats,c.daily||{},c.monthName,c.timestamp,mi);
        refBtn.style.display='inline-flex';
        if(isStale(c))runScan(mi,MONTH_NAMES[mi]);
      } else if(r){
        r.innerHTML='<div style="padding:10px;color:var(--mc-muted);font-size:13px">Brak danych w cache. Kliknij "Skanuj".</div>';
        refBtn.style.display='none';
      }
    };

    const prog = document.createElement('div');
    prog.id='mc-progress';
    prog.style.cssText='display:none;margin-top:10px;font-weight:bold;font-size:13px;color:var(--mc-text)';

    const pbar = document.createElement('progress');
    pbar.id='mc-pbar'; pbar.max=TOTAL_PAGES; pbar.value=0;
    pbar.style.cssText='width:100%;height:6px;margin-top:5px;display:none';

    const result = document.createElement('div');
    result.id='mc-result';

    controls.append(label,sel,scanBtn,refBtn);
    body.append(controls,prog,pbar,result);
    ctr.appendChild(body);
    // Wstawiamy przed FORM, żeby uniknąć przechwytywania zdarzeń (np. odświeżanie po kliknięciu buttona)
    const formTarget = target.closest('form');
    if (formTarget) {
      formTarget.parentNode.insertBefore(ctr, formTarget);
    } else {
      target.parentNode.insertBefore(ctr, target.nextSibling);
    }

    // --- Auto-load cache ---
    const mi = parseInt(sel.value,10);
    const cached = loadCache(mi);
    if(cached&&Object.keys(cached.stats).length){
      renderDashboard(cached.stats,cached.daily||{},cached.monthName,cached.timestamp,mi);
      refBtn.style.display='inline-flex';
      if(isStale(cached))runScan(mi,MONTH_NAMES[mi]);
    }
  }

  // ═══════════════════════════════════════════════════════════════
  // §17  KEYBOARD SHORTCUTS
  // ═══════════════════════════════════════════════════════════════
  document.addEventListener('keydown',(e)=>{
    if(e.ctrlKey&&e.shiftKey&&e.key==='S'){
      e.preventDefault();
      const sel=document.getElementById('mc-month');
      if(sel)runScan(parseInt(sel.value,10),MONTH_NAMES[parseInt(sel.value,10)]);
    }
    if(e.ctrlKey&&e.shiftKey&&e.key==='M'){
      e.preventDefault();
      const body=document.getElementById('mc-body');
      if(body){
        const show=body.style.display==='none';
        body.style.display=show?'':'none';
        localStorage.setItem(COLLAPSE_KEY,show?'0':'1');
        const chev=document.querySelector('.mc-chev');
        if(chev)chev.classList.toggle('mc-open',show);
      }
    }
  });

  // ═══════════════════════════════════════════════════════════════
  // §18  INITIALIZATION
  // ═══════════════════════════════════════════════════════════════
  function init() {
    if (document.querySelector('h4.cp-page-title')) { createUI(); return; }
    const t = document.body || document.documentElement;
    if (!t) {
      document.addEventListener('DOMContentLoaded', () => {
        if (document.querySelector('h4.cp-page-title')) createUI();
        else observe();
      });
      return;
    }
    observe();
  }
  function observe() {
    const obs = new MutationObserver((_,o) => {
      if (document.querySelector('h4.cp-page-title')) { o.disconnect(); createUI(); }
    });
    obs.observe(document.body||document.documentElement, {childList:true,subtree:true});
    setTimeout(()=>obs.disconnect(), 30000);
  }

  init();
})();