MediaWiki:Common.js: Difference between revisions

From HADES
Jump to navigationJump to search
Created page with "/** * ╔═══════════════════════════════════════════════════════════════╗ * ║ HADES WIKI — MediaWiki Theme JavaScript ║ * ║ The Free Encyclopedia of the Underworld ║ * ║ Based on: Hades (2026) by Melanie Martinez ║ * ║ Install in: MediaWiki:Common.js..."
 
No edit summary
 
Line 1: Line 1:
/**
/* ================================================================
  * ╔═══════════════════════════════════════════════════════════════╗
  H A D E S —  MediaWiki:Common.js
* ║        HADES WIKI MediaWiki Theme JavaScript              ║
  Ambient visual layer: candles, gothic arches, rose petals,
* ║    The Free Encyclopedia of the Underworld                  ║
  sacred hearts, damask motifs, wax drips, dust motes
* ║    Based on: Hades (2026) by Melanie Martinez                ║
  ================================================================ */
  * ║    Install in: MediaWiki:Common.js                           ║
* ╚═══════════════════════════════════════════════════════════════╝
*/


(function () {
( function () {
   'use strict';
   'use strict';


   /* ─────────────────────────────────────────
   /* ── FONT PRELOAD ─────────────────────────────────────────── */
    CONFIGURATION
   function preloadFonts() {
  ───────────────────────────────────────── */
     if ( document.getElementById( 'hades-fonts' ) ) { return; }
   const HADES_CONFIG = {
     var link = document.createElement( 'link' );
     candleCount: 4,
     link.id  = 'hades-fonts';
     particleCount: 15,
     link.rel  = 'stylesheet';
     particleColors: ['#c8922a', '#ff8800', '#ffcc44', '#ff6600', '#ffd700'],
     link.href = 'https://fonts.googleapis.com/css2?family=Cinzel+Decorative:wght@400;700;900&family=Cinzel:wght@400;600;700&family=IM+Fell+English:ital@0;1&family=Cormorant+Garamond:ital,wght@0,300;0,400;0,600;1,400&family=UnifrakturMaguntia&family=Playfair+Display+SC:wght@400;700&family=Petit+Formal+Script&display=swap';
     goldColor: '#c8922a',
     document.head.appendChild( link );
     pageFadeInDelay: 100,
    navHighlightDelay: 200,
    siteSubtitle: 'The Free Encyclopedia of the Underworld',
  };
 
  /* ─────────────────────────────────────────
    UTILITY: Wait for DOM element
  ───────────────────────────────────────── */
  function waitFor(selector, callback, timeout = 5000) {
    const el = document.querySelector(selector);
    if (el) return callback(el);
    const start = Date.now();
     const interval = setInterval(() => {
      const found = document.querySelector(selector);
      if (found || Date.now() - start > timeout) {
        clearInterval(interval);
        if (found) callback(found);
      }
    }, 100);
   }
   }


   /* ─────────────────────────────────────────
   /* ── SVG LIBRARY ──────────────────────────────────────────── */
    1. ANIMATED CANDLES IN HEADER
   var SVG = {
  ───────────────────────────────────────── */
   function injectCandles() {
    const header = document.querySelector('#mw-head, .mw-header, .vector-header');
    if (!header) return;


     // Remove existing candle container if present
     /* Tall pillar candle with dripping wax */
     const existing = document.getElementById('hades-candles');
     candleTall: [
     if (existing) existing.remove();
      '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 200" width="44" height="146">',
        /* flame */
        '<g style="animation:h-flame-dance 0.8s ease-in-out infinite;transform-origin:30px 22px">',
          '<ellipse cx="30" cy="28" rx="7" ry="14" fill="#ff7010" opacity="0.9"/>',
          '<ellipse cx="30" cy="30" rx="4" ry="10" fill="#f8c840" opacity="0.95"/>',
          '<ellipse cx="30" cy="32" rx="2" ry="5"  fill="#ffffff" opacity="0.7"/>',
          /* glow halo */
          '<ellipse cx="30" cy="28" rx="14" ry="18" fill="#f8c840" opacity="0.08"/>',
        '</g>',
        /* wick */
        '<line x1="30" y1="40" x2="30" y2="50" stroke="#3a2010" stroke-width="1.5"/>',
        /* wax body */
        '<rect x="18" y="48" width="24" height="130" rx="3" fill="#f2ede0"/>',
        '<rect x="20" y="50" width="4" height="128" rx="1" fill="#ffffff" opacity="0.15"/>',
        /* drips */
        '<path d="M22,50 Q20,60 21,75 Q22,82 22,90" stroke="#e8e0cc" stroke-width="3" fill="none" stroke-linecap="round"/>',
        '<path d="M36,50 Q38,65 37,78" stroke="#e8e0cc" stroke-width="2.5" fill="none" stroke-linecap="round"/>',
        '<path d="M28,48 Q27,58 28,68" stroke="#f0e8d0" stroke-width="2" fill="none" stroke-linecap="round"/>',
        '<ellipse cx="21" cy="90" rx="2.5" ry="3.5" fill="#d8d0c0"/>',
        '<ellipse cx="37" cy="78" rx="2" ry="3" fill="#d8d0c0"/>',
        /* base puddle */
        '<ellipse cx="30" cy="180" rx="18" ry="6" fill="#d8d0c0" opacity="0.6"/>',
        '<rect x="16" y="174" width="28" height="8" rx="3" fill="#c8c0b0"/>',
      '</svg>'
     ].join( '' ),


     const container = document.createElement('div');
     /* Short stubby candle, more wax pooled */
    container.id = 'hades-candles';
    candleShort: [
    container.style.cssText = `
      '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 120" width="36" height="86">',
      display: flex;
        '<g style="animation:h-flame-dance 1.1s ease-in-out infinite;transform-origin:25px 18px">',
      align-items: flex-end;
          '<ellipse cx="25" cy="22" rx="5" ry="10" fill="#ff7010" opacity="0.85"/>',
      gap: 6px;
          '<ellipse cx="25" cy="24" rx="3" ry="7"  fill="#f8c840" opacity="0.95"/>',
      padding: 8px 16px;
          '<ellipse cx="25" cy="25" rx="1.5" ry="4" fill="#ffffff" opacity="0.6"/>',
      position: absolute;
          '<ellipse cx="25" cy="22" rx="10" ry="14" fill="#f8c840" opacity="0.07"/>',
      right: 80px;
        '</g>',
       top: 50%;
        '<line x1="25" y1="32" x2="25" y2="40" stroke="#3a2010" stroke-width="1.5"/>',
      transform: translateY(-50%);
        '<rect x="14" y="38" width="22" height="65" rx="4" fill="#f0ece0"/>',
      z-index: 50;
        '<rect x="16" y="40" width="3" height="62" rx="1" fill="#ffffff" opacity="0.12"/>',
      pointer-events: none;
        '<path d="M18,40 Q16,55 17,70 Q17,78 16,85" stroke="#e0d8c4" stroke-width="3" fill="none" stroke-linecap="round"/>',
    `;
        '<ellipse cx="16" cy="85" rx="2.5" ry="3" fill="#ccc4b0"/>',
        /* large wax pool */
        '<ellipse cx="25" cy="106" rx="22" ry="7" fill="#ddd8c8" opacity="0.7"/>',
        '<rect x="10" y="100" width="30" height="10" rx="4" fill="#c8c0b0"/>',
       '</svg>'
    ].join( '' ),


     const candleHeights = [50, 75, 60, 42];
     /* Cluster of 3 candles on a holder */
    const candleDelays = [0.3, 0, 0.6, 1.0];
    candleCluster: [
      '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 200" width="100" height="167">',
        /* left candle */
        '<g transform="translate(10,20)">',
          '<g style="animation:h-flame-dance 0.9s 0.2s ease-in-out infinite;transform-origin:20px 16px">',
            '<ellipse cx="20" cy="18" rx="5" ry="11" fill="#ff8020" opacity="0.9"/>',
            '<ellipse cx="20" cy="20" rx="3" ry="7"  fill="#f8c840"/>',
            '<ellipse cx="20" cy="22" rx="1.5" ry="4" fill="#fff" opacity="0.6"/>',
            '<ellipse cx="20" cy="18" rx="10" ry="14" fill="#f8c840" opacity="0.07"/>',
          '</g>',
          '<line x1="20" y1="28" x2="20" y2="35" stroke="#3a2010" stroke-width="1"/>',
          '<rect x="12" y="33" width="16" height="100" rx="3" fill="#f2ede0"/>',
          '<path d="M14,35 Q12,50 13,70" stroke="#e0d8c4" stroke-width="2.5" fill="none" stroke-linecap="round"/>',
        '</g>',
        /* centre candle (taller) */
        '<g transform="translate(40,0)">',
          '<g style="animation:h-flame-dance 0.7s ease-in-out infinite;transform-origin:20px 18px">',
            '<ellipse cx="20" cy="20" rx="6" ry="14" fill="#ff6010" opacity="0.95"/>',
            '<ellipse cx="20" cy="23" rx="4" ry="9"  fill="#f8c840"/>',
            '<ellipse cx="20" cy="25" rx="2" ry="5" fill="#fff" opacity="0.7"/>',
            '<ellipse cx="20" cy="20" rx="14" ry="18" fill="#f8c840" opacity="0.08"/>',
          '</g>',
          '<line x1="20" y1="32" x2="20" y2="40" stroke="#3a2010" stroke-width="1.5"/>',
          '<rect x="11" y="38" width="18" height="130" rx="3" fill="#f0ece0"/>',
          '<path d="M13,40 Q11,60 12,85" stroke="#e0d8c4" stroke-width="3" fill="none" stroke-linecap="round"/>',
          '<ellipse cx="12" cy="85" rx="2.5" ry="3.5" fill="#ccc4b0"/>',
        '</g>',
        /* right candle */
        '<g transform="translate(76,28)">',
          '<g style="animation:h-flame-dance 1.0s 0.5s ease-in-out infinite;transform-origin:18px 15px">',
            '<ellipse cx="18" cy="16" rx="4" ry="9" fill="#ff7018" opacity="0.85"/>',
            '<ellipse cx="18" cy="18" rx="3" ry="6" fill="#f8c840"/>',
            '<ellipse cx="18" cy="20" rx="1.5" ry="3.5" fill="#fff" opacity="0.55"/>',
            '<ellipse cx="18" cy="16" rx="9" ry="13" fill="#f8c840" opacity="0.06"/>',
          '</g>',
          '<line x1="18" y1="25" x2="18" y2="32" stroke="#3a2010" stroke-width="1"/>',
          '<rect x="10" y="30" width="15" height="90" rx="3" fill="#f2ede0"/>',
          '<path d="M22,32 Q24,48 23,62" stroke="#e0d8c4" stroke-width="2.5" fill="none" stroke-linecap="round"/>',
        '</g>',
        /* shared base plate */
        '<rect x="4" y="170" width="112" height="14" rx="4" fill="#8a6030"/>',
        '<rect x="2" y="183" width="116" height="8" rx="3" fill="#6a4820"/>',
        '<ellipse cx="60" cy="191" rx="58" ry="6" fill="#4a3010" opacity="0.5"/>',
      '</svg>'
    ].join( '' ),


     candleHeights.forEach((height, i) => {
     /* Gothic pointed arch frame */
      const candleEl = createCandleElement(height, candleDelays[i]);
    gothicArch: [
       container.appendChild(candleEl);
      '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 150" width="90" height="135">',
    });
        '<path d="M10,150 L10,70 Q10,5 50,5 Q90,5 90,70 L90,150" fill="none" stroke="#c8860a" stroke-width="1.5" opacity="0.7"/>',
        /* inner arch */
        '<path d="M18,150 L18,74 Q18,20 50,20 Q82,20 82,74 L82,150" fill="none" stroke="#e8b030" stroke-width="0.8" opacity="0.4"/>',
        /* keystone ornament */
        '<ellipse cx="50" cy="8" rx="6" ry="7" fill="none" stroke="#c8860a" stroke-width="1.2" opacity="0.7"/>',
        '<circle cx="50" cy="8" r="2.5" fill="#c8860a" opacity="0.6"/>',
        /* column capitals */
        '<rect x="4" y="140" width="14" height="10" rx="1" fill="#8a5a18" opacity="0.5"/>',
        '<rect x="82" y="140" width="14" height="10" rx="1" fill="#8a5a18" opacity="0.5"/>',
        /* tracery at top */
        '<path d="M35,35 Q50,18 65,35" fill="none" stroke="#c8860a" stroke-width="0.8" opacity="0.35"/>',
        '<circle cx="50" cy="30" r="4" fill="none" stroke="#e8b030" stroke-width="0.8" opacity="0.4"/>',
       '</svg>'
    ].join( '' ),


     header.style.position = 'relative';
     /* Sacred heart */
     header.appendChild(container);
    sacredHeart: [
  }
      '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 90" width="70" height="79">',
        /* heart body */
        '<path d="M40,70 C20,55 5,45 5,28 C5,16 14,8 25,8 C31,8 37,11 40,16 C43,11 49,8 55,8 C66,8 75,16 75,28 C75,45 60,55 40,70Z" fill="#a84858" opacity="0.8"/>',
        /* light overlay */
        '<path d="M25,14 Q30,10 35,16" stroke="#f0b0c0" stroke-width="2" fill="none" opacity="0.5"/>',
        /* flames on top */
        '<g transform="translate(33,-2)">',
          '<path d="M7,10 Q5,4 7,0 Q9,4 7,10Z" fill="#f8c840" opacity="0.9"/>',
          '<path d="M14,12 Q12,5 14,1 Q16,5 14,12Z" fill="#ff7010" opacity="0.85"/>',
          '<path d="M21,10 Q19,4 21,0 Q23,4 21,10Z" fill="#f8c840" opacity="0.9"/>',
        '</g>',
        /* crown */
        '<g transform="translate(22,-5)">',
          '<path d="M0,12 L0,0 L9,5 L18,0 L27,5 L36,0 L36,12Z" fill="none" stroke="#e8b030" stroke-width="1" opacity="0.7"/>',
          '<circle cx="9"  cy="5" r="2" fill="#e8b030" opacity="0.7"/>',
          '<circle cx="18" cy="0" r="2.5" fill="#f8d060" opacity="0.8"/>',
          '<circle cx="27" cy="5" r="2" fill="#e8b030" opacity="0.7"/>',
        '</g>',
        /* thorns / ring */
        '<circle cx="40" cy="35" r="18" fill="none" stroke="#3a1e0c" stroke-width="1.5" stroke-dasharray="3,4" opacity="0.6"/>',
      '</svg>'
     ].join( '' ),


  function createCandleElement(height, delay) {
    /* Baroque damask ornament */
    const wrapper = document.createElement('div');
    damask: [
     wrapper.style.cssText = 'display:flex;flex-direction:column;align-items:center;position:relative;';
      '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="90" height="90">',
        '<g fill="none" stroke="#c8860a" stroke-width="0.6" opacity="0.8">',
          '<ellipse cx="50" cy="50" rx="8" ry="8"/>',
          '<ellipse cx="50" cy="22" rx="5" ry="16"/>',
          '<ellipse cx="50" cy="78" rx="5" ry="16"/>',
          '<ellipse cx="22" cy="50" rx="16" ry="5"/>',
          '<ellipse cx="78" cy="50" rx="16" ry="5"/>',
          '<path d="M50,34 Q60,42 50,50 Q40,42 50,34Z"/>',
          '<path d="M50,66 Q60,58 50,50 Q40,58 50,66Z"/>',
          '<path d="M34,50 Q42,60 50,50 Q42,40 34,50Z"/>',
          '<path d="M66,50 Q58,60 50,50 Q58,40 66,50Z"/>',
          '<circle cx="50" cy="10"  r="3"/>',
          '<circle cx="50" cy="90"  r="3"/>',
          '<circle cx="10" cy="50"  r="3"/>',
          '<circle cx="90" cy="50"  r="3"/>',
          '<path d="M38,38 Q50,30 62,38 Q70,50 62,62 Q50,70 38,62 Q30,50 38,38Z"/>',
        '</g>',
      '</svg>'
     ].join( '' ),


     // Smoke
     /* Rose petal */
     const smoke = document.createElement('div');
     petal: [
    smoke.style.cssText = `
      '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 36" width="18" height="27">',
      position:absolute;
        '<path d="M12,2 Q20,8 20,20 Q20,32 12,34 Q4,32 4,20 Q4,8 12,2Z" fill="#c06070" opacity="0.7"/>',
      bottom:${height + 36}px;
        '<path d="M12,6 Q16,12 15,22" stroke="#e090a0" stroke-width="0.8" fill="none" opacity="0.5"/>',
      left:50%;
      '</svg>'
      width:4px;
     ].join( '' ),
      height:10px;
      background:rgba(200,180,150,0.25);
      border-radius:50%;
      transform:translateX(-50%);
      animation:hadesSmoke ${2 + delay * 0.5}s ${delay * 0.3}s ease-out infinite;
    `;
     wrapper.appendChild(smoke);


     // Outer flame
     /* Ornate candelabra */
     const outerFlame = document.createElement('div');
     candelabra: [
    outerFlame.style.cssText = `
      '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 160 260" width="130" height="211">',
      width:14px;
        /* left arm candle */
      height:22px;
        '<g transform="translate(4,30)">',
      background:radial-gradient(ellipse at 50% 80%,#ff5500 0%,#ff9900 40%,#ffcc00 70%,rgba(255,220,100,0) 100%);
          '<g style="animation:h-flame-dance 0.85s 0.1s ease-in-out infinite;transform-origin:18px 16px">',
      border-radius:50% 50% 30% 30%/60% 60% 40% 40%;
            '<ellipse cx="18" cy="18" rx="5" ry="12" fill="#ff7010" opacity="0.9"/>',
      animation:hadesCandle ${1.4 + delay * 0.3}s ${delay * 0.2}s ease-in-out infinite;
            '<ellipse cx="18" cy="20" rx="3" ry="8"  fill="#f8c840"/>',
      transform-origin:bottom center;
            '<ellipse cx="18" cy="22" rx="2" ry="4"  fill="#fff" opacity="0.6"/>',
      filter:blur(0.5px);
            '<ellipse cx="18" cy="18" rx="12" ry="16" fill="#f8c840" opacity="0.06"/>',
      margin-bottom:0;
          '</g>',
    `;
          '<line x1="18" y1="30" x2="18" y2="36" stroke="#3a2010" stroke-width="1.2"/>',
     wrapper.appendChild(outerFlame);
          '<rect x="11" y="34" width="14" height="80" rx="3" fill="#f0ece0"/>',
          '<path d="M12,36 Q10,55 11,72" stroke="#e0d8c4" stroke-width="2.5" fill="none" stroke-linecap="round"/>',
        '</g>',
        /* right arm candle */
        '<g transform="translate(134,30)">',
          '<g style="animation:h-flame-dance 1.05s 0.3s ease-in-out infinite;transform-origin:18px 16px">',
            '<ellipse cx="18" cy="18" rx="5" ry="12" fill="#ff7010" opacity="0.9"/>',
            '<ellipse cx="18" cy="20" rx="3" ry="8"  fill="#f8c840"/>',
            '<ellipse cx="18" cy="22" rx="2" ry="4"  fill="#fff" opacity="0.6"/>',
            '<ellipse cx="18" cy="18" rx="12" ry="16" fill="#f8c840" opacity="0.06"/>',
          '</g>',
          '<line x1="18" y1="30" x2="18" y2="36" stroke="#3a2010" stroke-width="1.2"/>',
          '<rect x="11" y="34" width="14" height="80" rx="3" fill="#f0ece0"/>',
          '<path d="M24,36 Q26,55 25,72" stroke="#e0d8c4" stroke-width="2.5" fill="none" stroke-linecap="round"/>',
        '</g>',
        /* centre tall candle */
        '<g transform="translate(62,0)">',
          '<g style="animation:h-flame-dance 0.7s ease-in-out infinite;transform-origin:18px 20px">',
            '<ellipse cx="18" cy="20" rx="7" ry="16" fill="#ff6010" opacity="0.95"/>',
            '<ellipse cx="18" cy="23" rx="4" ry="10" fill="#f8c840"/>',
            '<ellipse cx="18" cy="26" rx="2" ry="6"  fill="#fff" opacity="0.7"/>',
            '<ellipse cx="18" cy="20" rx="16" ry="22" fill="#f8c840" opacity="0.08"/>',
          '</g>',
          '<line x1="18" y1="36" x2="18" y2="44" stroke="#3a2010" stroke-width="1.5"/>',
          '<rect x="10" y="42" width="16" height="120" rx="3" fill="#f2ede0"/>',
          '<path d="M11,44 Q9,68 10,95" stroke="#e0d8c4" stroke-width="3" fill="none" stroke-linecap="round"/>',
          '<ellipse cx="10" cy="95" rx="2.5" ry="3.5" fill="#ccc4b0"/>',
        '</g>',
        /* arms */
        '<path d="M80,90 Q30,80 22,60" fill="none" stroke="#8a6030" stroke-width="3" stroke-linecap="round"/>',
        '<path d="M80,90 Q130,80 152,60" fill="none" stroke="#8a6030" stroke-width="3" stroke-linecap="round"/>',
        /* stem */
        '<rect x="74" y="160" width="12" height="70" rx="4" fill="#7a5020"/>',
        '<ellipse cx="80" cy="162" rx="12" ry="6" fill="#8a6030"/>',
        /* base */
        '<ellipse cx="80" cy="236" rx="45" ry="12" fill="#6a4020" opacity="0.7"/>',
        '<rect x="50" y="228" width="60" height="10" rx="4" fill="#8a5820"/>',
      '</svg>'
     ].join( '' ),


     // Inner flame
     /* Mirror silhouette (album cover gothic arch mirror) */
     const innerFlame = document.createElement('div');
     mirrorSilhouette: [
    innerFlame.style.cssText = `
      '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 160" width="80" height="128">',
      width:5px;
        /* outer frame */
      height:13px;
        '<path d="M8,160 L8,80 Q8,5 50,5 Q92,5 92,80 L92,160" fill="none" stroke="#8a5a18" stroke-width="3" opacity="0.6"/>',
      background:radial-gradient(ellipse at 50% 80%,#fff 0%,#ffe066 50%,rgba(255,200,0,0) 100%);
        /* inner mirror */
      border-radius:50% 50% 30% 30%/60% 60% 40% 40%;
        '<path d="M16,158 L16,82 Q16,18 50,18 Q84,18 84,82 L84,158" fill="rgba(10,5,2,0.5)" stroke="#c8860a" stroke-width="1" opacity="0.5"/>',
      animation:hadesCandle ${1.1 + delay * 0.2}s ${delay * 0.15}s ease-in-out infinite;
        /* glow inside mirror */
      transform-origin:bottom center;
        '<path d="M16,158 L16,82 Q16,18 50,18 Q84,18 84,82 L84,158 Z" fill="url(#mirrorGlow)" opacity="0.3"/>',
      margin-top:-13px;
        '<defs><radialGradient id="mirrorGlow" cx="50%" cy="60%"><stop offset="0%" stop-color="#f8c840" stop-opacity="0.15"/><stop offset="100%" stop-color="#f8c840" stop-opacity="0"/></radialGradient></defs>',
       align-self:center;
        /* spire ornaments */
    `;
        '<path d="M8,80 L8,50 L14,30 L20,50 L20,80" fill="none" stroke="#8a5a18" stroke-width="1.5" opacity="0.5"/>',
     wrapper.appendChild(innerFlame);
        '<path d="M80,80 L80,50 L86,30 L92,50 L92,80" fill="none" stroke="#8a5a18" stroke-width="1.5" opacity="0.5"/>',
        /* keystone */
        '<ellipse cx="50" cy="8" rx="7" ry="8" fill="none" stroke="#c8860a" stroke-width="1.5" opacity="0.6"/>',
        '<circle cx="50" cy="8" r="3" fill="#c8860a" opacity="0.5"/>',
        /* base shelf */
        '<rect x="2" y="152" width="96" height="10" rx="2" fill="#6a4820" opacity="0.5"/>',
        /* sacred heart base motif */
        '<path d="M50,148 C44,143 38,140 38,134 C38,130 41,127 45,127 C47,127 49,128 50,130 C51,128 53,127 55,127 C59,127 62,130 62,134 C62,140 56,143 50,148Z" fill="#a84858" opacity="0.4"/>',
       '</svg>'
     ].join( '' ),


     // Glow orb
     /* Wax drip trail */
     const glow = document.createElement('div');
     waxDrip: [
    glow.style.cssText = `
      '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 120" width="24" height="96">',
      width:22px;
        '<path d="M15,0 Q18,20 17,35 Q16,55 18,70 Q20,85 15,100 Q10,85 12,70 Q14,55 13,35 Q12,20 15,0Z" fill="#f2ede0" opacity="0.7"/>',
      height:22px;
        '<ellipse cx="15" cy="100" rx="8" ry="6" fill="#e8e0cc" opacity="0.6"/>',
      border-radius:50%;
       '</svg>'
      background:rgba(255,160,50,0.1);
     ].join( '' ),
      animation:hadesGlowOrb ${2.2 + delay * 0.4}s ${delay * 0.1}s ease-in-out infinite;
       position:absolute;
      top:0;
      left:50%;
      transform:translateX(-50%);
     `;
    wrapper.appendChild(glow);


     // Wick
     /* Ornate key */
     const wick = document.createElement('div');
     key: [
    wick.style.cssText = 'width:2px;height:5px;background:#1a0800;margin-bottom:-1px;position:relative;z-index:2;';
      '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 120" width="30" height="90">',
     wrapper.appendChild(wick);
        '<circle cx="20" cy="20" r="14" fill="none" stroke="#c8860a" stroke-width="2" opacity="0.8"/>',
        '<circle cx="20" cy="20" r="7"  fill="none" stroke="#e8b030" stroke-width="1" opacity="0.6"/>',
        '<circle cx="20" cy="20" r="3"  fill="#c8860a" opacity="0.7"/>',
        '<rect x="18" y="34" width="4" height="80" rx="1" fill="#c8860a" opacity="0.7"/>',
        '<rect x="22" y="80" width="12" height="4" rx="1" fill="#c8860a" opacity="0.7"/>',
        '<rect x="22" y="94" width="9"  height="4" rx="1" fill="#c8860a" opacity="0.7"/>',
      '</svg>'
     ].join( '' ),


     // Wax body
     /* Crown */
     const waxColors = ['#d4c4a0', '#e8d8b0', '#ddd0a8', '#c8b890'];
     crown: [
    const wax = document.createElement('div');
      '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 80" width="100" height="67">',
    wax.style.cssText = `
        '<path d="M5,75 L5,35 L25,10 L40,35 L60,5 L80,35 L95,10 L115,35 L115,75 Z" fill="none" stroke="#c8860a" stroke-width="2" stroke-linejoin="round" opacity="0.7"/>',
      width:16px;
        '<path d="M5,60 L115,60" stroke="#e8b030" stroke-width="1" opacity="0.5"/>',
      height:${height}px;
        '<circle cx="25"  cy="13" r="4"  fill="#e8b030" opacity="0.7"/>',
      background:linear-gradient(to right,${waxColors[Math.floor(Math.random() * waxColors.length)]}bb 0%,${waxColors[0]} 35%,${waxColors[0]} 70%,${waxColors[0]}88 100%);
        '<circle cx="60"  cy="7"  r="5"  fill="#f8d060" opacity="0.8"/>',
       border-radius:2px 2px 3px 3px;
        '<circle cx="95"  cy="13" r="4"  fill="#e8b030" opacity="0.7"/>',
      position:relative;
        '<circle cx="40"  cy="36" r="3"  fill="#d08090" opacity="0.6"/>',
      box-shadow:inset -3px 0 6px rgba(0,0,0,0.2),inset 3px 0 3px rgba(255,255,255,0.08);
        '<circle cx="80"  cy="36" r="3"  fill="#d08090" opacity="0.6"/>',
    `;
        '<rect x="5" y="68" width="110" height="8" rx="2" fill="#8a5a18" opacity="0.4"/>',
       '</svg>'
    ].join( '' ),


     // Wax drip 1
     /* Floating rose */
     const drip1 = document.createElement('div');
     rose: [
    drip1.style.cssText = `
      '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60" width="48" height="48">',
      position:absolute;top:0;left:30%;width:4px;
        '<circle cx="30" cy="30" r="10" fill="#a84858" opacity="0.8"/>',
      background:inherit;border-radius:0 0 3px 3px;
        '<path d="M30,20 Q38,24 36,32 Q30,38 22,32 Q20,24 30,20Z" fill="#c06070" opacity="0.7"/>',
      animation:hadesWaxDrip ${4 + delay}s ${delay + 0.5}s ease-in-out infinite alternate;
        '<path d="M30,20 Q22,24 24,32 Q30,38 38,32 Q40,24 30,20Z" fill="#b05060" opacity="0.7"/>',
       height:10px;opacity:0.6;
        '<path d="M30,14 Q40,18 40,28 Q40,38 30,42 Q20,38 20,28 Q20,18 30,14Z" fill="none" stroke="#d08090" stroke-width="0.8" opacity="0.5"/>',
    `;
        '<path d="M30,8  Q44,14 44,30 Q44,46 30,50 Q16,46 16,30 Q16,14 30,8Z"  fill="none" stroke="#c06070" stroke-width="0.6" opacity="0.4"/>',
     wax.appendChild(drip1);
        '<path d="M30,50 L30,60" stroke="#3a1e0c" stroke-width="1.5"/>',
        '<path d="M24,56 Q30,52 36,56" stroke="#2a1808" stroke-width="1" fill="none"/>',
       '</svg>'
     ].join( '' )
  };


    // Wax drip 2
  /* ── HELPERS ──────────────────────────────────────────────── */
    const drip2 = document.createElement('div');
  function rand( a, b ) { return Math.random() * ( b - a ) + a; }
    drip2.style.cssText = `
  function randInt( a, b ) { return Math.floor( rand( a, b + 1 ) ); }
      position:absolute;top:0;right:25%;width:3px;
      background:inherit;border-radius:0 0 3px 3px;
      animation:hadesWaxDrip ${5 + delay}s ${delay + 1.5}s ease-in-out infinite alternate;
      height:7px;opacity:0.5;
    `;
    wax.appendChild(drip2);


     wrapper.appendChild(wax);
  /* ── BUILD LAYER ──────────────────────────────────────────── */
     return wrapper;
  function createLayer() {
     var div = document.createElement( 'div' );
    div.className = 'h-ambient';
    div.id = 'hades-ambient';
    document.body.insertBefore( div, document.body.firstChild );
     return div;
   }
   }


   /* ─────────────────────────────────────────
   /* ── SPAWN AMBIENT ELEMENT ───────────────────────────────── */
    2. INJECT EXTRA CSS KEYFRAMES (JS version)
   function spawn( layer, svgKey, cssClass, opts ) {
  ───────────────────────────────────────── */
     opts = opts || {};
   function injectExtraStyles() {
    var el = document.createElement( 'div' );
     const style = document.createElement('style');
     el.className = cssClass;
     style.id = 'hades-js-styles';
     el.innerHTML = SVG[ svgKey ];
     style.textContent = `
      @keyframes hadesSmoke {
        0%  { opacity: 0.4; transform: translateX(-50%) translateY(0) scaleX(1); }
        50%  { opacity: 0.2; transform: translateX(-50%) translateY(-16px) scaleX(1.5); }
        100% { opacity: 0;  transform: translateX(-50%) translateY(-32px) scaleX(2); }
      }
      @keyframes hadesGlowOrb {
        0%,100% { box-shadow:0 0 8px 3px rgba(255,160,50,0.4),0 0 20px 8px rgba(200,100,20,0.2); opacity:0.8; }
        50%    { box-shadow:0 0 14px 6px rgba(255,190,70,0.7),0 0 35px 14px rgba(210,130,35,0.4); opacity:1; }
      }
      @keyframes hadesWaxDrip {
        0%  { height:0px; opacity:0.8; }
        100% { height:18px; opacity:0.3; }
      }
      @keyframes hadesParticleAnim {
        0%  { transform:translateY(0) translateX(0) rotate(0deg) scale(1); opacity:0.9; }
        50%  { transform:translateY(-50vh) translateX(30px) rotate(180deg) scale(0.5); opacity:0.4; }
        100% { transform:translateY(-100vh) translateX(-20px) rotate(360deg) scale(0.1); opacity:0; }
      }
      @keyframes hadesPageReveal {
        from { opacity:0; transform:translateY(16px); }
        to  { opacity:1; transform:translateY(0); }
      }
      @keyframes hadesNavLine {
        from { width:0; left:50%; }
        to  { width:100%; left:0; }
      }
      @keyframes hadesTitlePulse {
        0%,100% { filter:brightness(1); }
        50%    { filter:brightness(1.15) drop-shadow(0 0 8px rgba(200,146,42,0.5)); }
      }
      @keyframes hadesLinkShimmer {
        0%  { background-position:-200% center; }
        100% { background-position:200% center; }
      }
      @keyframes hadesScrollIndicator {
        0%,100% { opacity:0.4; transform:translateY(0); }
        50%    { opacity:1; transform:translateY(4px); }
      }
      @keyframes hadesCrownFloat {
        0%,100% { transform:translateY(0) rotate(-2deg); filter:drop-shadow(0 0 6px rgba(200,146,42,0.5)); }
        50%    { transform:translateY(-5px) rotate(2deg); filter:drop-shadow(0 0 12px rgba(200,146,42,0.8)); }
      }
      @keyframes hadesSubtitleReveal {
        from { opacity:0; letter-spacing:8px; }
        to  { opacity:1; letter-spacing:5px; }
      }


      /* Hover glow for wiki links */
    var scale = rand( opts.minScale || 0.5, opts.maxScale || 1.4 );
      .hades-link-glow {
    el.style.transformOrigin = 'center center';
        position:relative;
    el.style.transform = 'scale(' + scale + ')';
        transition:all 0.3s ease !important;
      }
      .hades-link-glow:hover {
        text-shadow:0 0 12px rgba(200,146,42,0.6) !important;
      }


      /* Section reveal animation */
    /* Position */
      .hades-section-reveal {
    var edged = ( opts.edged !== false ) && Math.random() < 0.35;
        animation:hadesPageReveal 0.6s ease forwards;
    var top, left;
        opacity:0;
    if ( edged ) {
       }
      var side = randInt( 0, 3 );
      if ( side === 0 )      { top = rand( 0, 8 );  left = rand( 5, 90 ); }
      else if ( side === 1 ) { top = rand( 85, 97 ); left = rand( 5, 90 ); }
       else if ( side === 2 ) { top = rand( 5, 90 );  left = rand( 0, 6 );  }
      else                  { top = rand( 5, 90 );  left = rand( 88, 97 );}
    } else {
      top  = rand( opts.topMin  || 5, opts.topMax  || 90 );
      left = rand( opts.leftMin || 5, opts.leftMax || 90 );
    }
    el.style.top  = top  + 'vh';
    el.style.left = left + 'vw';


      /* Candle hover effect */
    /* Animation stagger */
      #hades-candles:hover {
    var delay = rand( 0, 18 );
        filter:brightness(1.2);
    var dur  = rand( opts.durMin || 5, opts.durMax || 14 );
      }
    el.style.animationDelay    = delay + 's';
    el.style.animationDuration = dur + 's';
    if ( opts.opacity ) { el.style.opacity = opts.opacity; }


      /* Ornament spin */
    layer.appendChild( el );
      .hades-spinning-ornament {
    return el;
        display:inline-block;
  }
        animation:hadesOrnamentSpin 8s linear infinite;
      }


      /* Progress bar shimmer */
  /* ── ROSE PETAL FALL ─────────────────────────────────────── */
      .hades-progress {
  function spawnPetal( layer ) {
        height:2px;
    var el = document.createElement( 'div' );
        background:linear-gradient(to right, transparent, #c8922a, transparent);
    el.className = 'h-petal';
        position:fixed;
    el.innerHTML = SVG.petal;
        top:0;
    el.style.left  = rand( 5, 95 ) + 'vw';
        left:0;
    el.style.top  = '-30px';
        z-index:9999;
    el.style.setProperty( '--drift', rand( -40, 60 ) + 'px' );
        transition:width 0.3s ease;
    el.style.setProperty( '--rot',  rand( -180, 360 ) + 'deg' );
      }
    var dur = rand( 8, 18 );
     `;
    el.style.animationDuration = dur + 's';
     document.head.appendChild(style);
    el.style.animationDelay    = rand( 0, 20 ) + 's';
     el.style.opacity = rand( 0.25, 0.55 );
     layer.appendChild( el );
   }
   }


   /* ─────────────────────────────────────────
   /* ── DUST MOTES ──────────────────────────────────────────── */
    3. FLOATING EMBER PARTICLES
   function spawnMote( layer ) {
  ───────────────────────────────────────── */
     var el = document.createElement( 'div' );
   function createParticles() {
     el.className = 'h-mote';
     const container = document.createElement('div');
    el.style.left = rand( 10, 90 ) + 'vw';
     container.id = 'hades-particles';
     el.style.top  = rand( 20, 90 ) + 'vh';
     container.style.cssText = `
    el.style.setProperty( '--mx', rand( -30, 30 ) + 'px' );
      position:fixed;
    el.style.setProperty( '--my', rand( -80, -120 ) + 'px' );
      inset:0;
    var dur = rand( 5, 12 );
      pointer-events:none;
    el.style.animationDuration = dur + 's';
      z-index:1;
    el.style.animationDelay    = rand( 0, 15 ) + 's';
      overflow:hidden;
    /* Vary colour: gold, rose, cream */
     `;
    var colours = [ '#f8d060', '#e8b030', '#d08090', '#f0e8d0' ];
    el.style.background = colours[ randInt( 0, colours.length - 1 ) ];
    el.style.boxShadow  = '0 0 ' + rand( 2, 6 ) + 'px currentColor';
     layer.appendChild( el );
  }


    for (let i = 0; i < HADES_CONFIG.particleCount; i++) {
  /* ── WAX DRIP TRAILS ON CONTENT EDGE ────────────────────── */
      const p = document.createElement('div');
  function injectWaxDrips() {
      const size = 1.5 + Math.random() * 2.5;
    var content = document.getElementById( 'content' )
      const color = HADES_CONFIG.particleColors[Math.floor(Math.random() * HADES_CONFIG.particleColors.length)];
                || document.getElementById( 'mw-content-text' )
      const left = Math.random() * 100;
                || document.querySelector( '.mw-body' );
      const delay = Math.random() * 8;
    if ( !content ) { return; }
      const duration = 6 + Math.random() * 8;


      p.style.cssText = `
    /* Top drip row — candle wax flowing down */
        position:absolute;
    var topBar = document.createElement( 'div' );
        bottom:0;
    topBar.style.cssText = [
        left:${left}%;
      'position:absolute',
        width:${size}px;
      'top:-1px',
        height:${size}px;
      'left:0',
        border-radius:50%;
      'width:100%',
        background:radial-gradient(circle, ${color} 0%, rgba(200,100,20,0.2) 70%, transparent 100%);
      'height:40px',
        animation:hadesParticleAnim ${duration}s ${delay}s ease-out infinite;
      'pointer-events:none',
        opacity:0;
      'overflow:hidden',
      `;
      'z-index:10'
      container.appendChild(p);
    ].join( ';' );
    }


     document.body.prepend(container);
     for ( var i = 0; i < 12; i++ ) {
  }
      var drip = document.createElement( 'div' );
 
       drip.innerHTML = SVG.waxDrip;
  /* ─────────────────────────────────────────
       drip.style.cssText = [
    4. SITE TITLE ENHANCEMENT
         'position:absolute',
  ───────────────────────────────────────── */
         'top:0',
  function enhanceSiteTitle() {
         'left:' + ( i * 8.5 + rand( 0, 4 ) ) + '%',
    // Add animated crown to logo area
         'opacity:' + rand( 0.2, 0.45 ),
    const logo = document.querySelector('#p-logo, .mw-logo, #mw-head-base');
         'transform:scaleY(0)',
    if (logo) {
         'transform-origin:top center',
       const crown = document.createElement('div');
         'animation:h-wax-drip ' + rand( 2, 5 ) + 's ' + rand( 0, 8 ) + 's ease-in forwards'
       crown.style.cssText = `
       ].join( ';' );
         font-size:28px;
       topBar.appendChild( drip );
         text-align:center;
         animation:hadesCrownFloat 3s ease-in-out infinite;
         filter:drop-shadow(0 0 8px rgba(200,146,42,0.6));
         margin-bottom:4px;
         display:block;
         color:#c8922a;
       `;
      crown.textContent = '';
       logo.prepend(crown);
     }
     }


     // Animate site subtitle
     /* Make content position:relative so absolute children work */
     const subtitle = document.querySelector('#p-logo a[title], .mw-wiki-title');
     if ( getComputedStyle( content ).position === 'static' ) {
    if (subtitle) {
       content.style.position = 'relative';
      const sub = document.createElement('div');
       sub.textContent = HADES_CONFIG.siteSubtitle;
      sub.style.cssText = `
        font-family:'Cinzel',serif;
        font-size:8px;
        letter-spacing:5px;
        color:#8b4a20;
        text-transform:uppercase;
        text-align:center;
        animation:hadesSubtitleReveal 2s ease forwards;
        opacity:0;
        margin-top:2px;
      `;
      subtitle.parentNode.insertBefore(sub, subtitle.nextSibling);
     }
     }
    content.appendChild( topBar );
   }
   }


   /* ─────────────────────────────────────────
   /* ── MOUSE PARALLAX ──────────────────────────────────────── */
    5. PAGE CONTENT ANIMATIONS
   function initParallax( layer ) {
  ───────────────────────────────────────── */
     var cx = window.innerWidth / 2, cy = window.innerHeight / 2;
   function animatePageContent() {
     var tx = 0, ty = 0, cx2 = 0, cy2 = 0;
     const content = document.querySelector('#mw-content-text, .mw-body, #content');
     document.addEventListener( 'mousemove', function ( e ) {
     if (content) {
       tx = ( e.clientX - cx ) / cx * 12;
      content.style.animation = 'hadesPageReveal 0.8s 0.1s ease forwards';
       ty = ( e.clientY - cy ) / cy * 8;
      content.style.opacity = '0';
     } );
     }
     ( function tick() {
 
       cx2 += ( tx - cx2 ) * 0.03;
    // Animate each heading with stagger
      cy2 += ( ty - cy2 ) * 0.03;
    const headings = document.querySelectorAll('h2, h3');
       layer.style.transform = 'translate(' + cx2 + 'px,' + cy2 + 'px)';
    headings.forEach((h, i) => {
      requestAnimationFrame( tick );
       h.style.animation = `hadesPageReveal 0.6s ${0.1 + i * 0.08}s ease forwards`;
     }() );
       h.style.opacity = '0';
     });
 
    // Animate infobox
     const infobox = document.querySelector('.infobox');
    if (infobox) {
       infobox.style.animation = 'hadesPageReveal 0.8s 0.3s ease forwards';
       infobox.style.opacity = '0';
     }
   }
   }


   /* ─────────────────────────────────────────
   /* ── CANDLE CLICK SPARK ──────────────────────────────────── */
    6. ENHANCED LINK INTERACTIONS
   function initClickSpark() {
  ───────────────────────────────────────── */
     document.addEventListener( 'click', function ( e ) {
   function enhanceLinks() {
      var sparks = 6;
    // Add ripple effect to all article links
       for ( var s = 0; s < sparks; s++ ) {
     document.querySelectorAll('#mw-content-text a, .mw-body a').forEach(link => {
        ( function ( i ) {
       link.classList.add('hades-link-glow');
          var sp = document.createElement( 'div' );
 
          var angle = ( i / sparks ) * Math.PI * 2 + rand( -0.3, 0.3 );
      link.addEventListener('mouseenter', function () {
          var dist  = rand( 20, 55 );
        this.style.transition = 'all 0.2s ease';
          sp.style.cssText = [
      });
            'position:fixed',
 
            'pointer-events:none',
      link.addEventListener('click', function (e) {
            'z-index:9994',
        const ripple = document.createElement('span');
            'width:' + rand( 3, 6 ) + 'px',
        ripple.style.cssText = `
            'height:' + rand( 3, 6 ) + 'px',
          position:absolute;
            'border-radius:50%',
          width:12px;
            'left:' + e.clientX + 'px',
          height:12px;
            'top:'  + e.clientY + 'px',
          background:rgba(200,146,42,0.6);
            'background:' + ( Math.random() < 0.5 ? '#f8c840' : '#ff7010' ),
          border-radius:50%;
            'box-shadow:0 0 6px #f8c840',
          pointer-events:none;
            'transition:transform 0.5s ease-out,opacity 0.5s ease-out',
          animation:hadesRipple 0.5s ease forwards;
            'opacity:0.9'
          transform:translate(-50%,-50%);
           ].join( ';' );
           top:${e.offsetY}px;
           document.body.appendChild( sp );
           left:${e.offsetX}px;
           requestAnimationFrame( function () {
           z-index:9;
            requestAnimationFrame( function () {
        `;
              sp.style.transform = 'translate(' + Math.cos( angle ) * dist + 'px,' + Math.sin( angle ) * dist + 'px)';
        this.style.position = 'relative';
              sp.style.opacity  = '0';
        this.style.overflow = 'hidden';
              setTimeout( function () { if ( sp.parentNode ) { sp.parentNode.removeChild( sp ); } }, 550 );
        this.appendChild(ripple);
            } );
        setTimeout(() => ripple.remove(), 500);
          } );
      });
        }( s ) );
     });
      }
     } );
   }
   }


   /* ─────────────────────────────────────────
   /* ── GOLD CURSOR TRAIL ────────────────────────────────────── */
    7. NAVIGATION ACTIVE INDICATOR
   function initCursorTrail() {
  ───────────────────────────────────────── */
     var LEN = 8;
   function enhanceNavigation() {
    var dots = [], pos = [];
     const navLinks = document.querySelectorAll(
     var mx = 0, my = 0;
      '#p-namespaces ul li a, #p-views ul li a, .vector-menu-tabs ul li a'
     for ( var i = 0; i < LEN; i++ ) {
     );
       var d = document.createElement( 'div' );
 
      var sz = ( 6 - i * 0.5 );
     navLinks.forEach(link => {
      d.style.cssText = [
       link.addEventListener('mouseenter', function () {
        'position:fixed',
        const indicator = document.createElement('div');
        'pointer-events:none',
        indicator.style.cssText = `
        'z-index:9996',
          position:absolute;
        'width:'  + sz + 'px',
          bottom:0;
        'height:' + sz + 'px',
          left:0;
        'border-radius:50%',
          height:2px;
        'opacity:' + ( ( LEN - i ) / LEN * 0.7 ),
          background:linear-gradient(to right, transparent, #c8922a, transparent);
        'background:' + ( i % 3 === 0 ? '#f8c840' : i % 3 === 1 ? '#c8860a' : '#d08090' ),
          animation:hadesNavLine 0.25s ease forwards;
        'box-shadow:0 0 ' + sz + 'px ' + ( i % 3 === 0 ? '#f8c840' : '#c8860a' ),
          pointer-events:none;
        'mix-blend-mode:screen'
        `;
      ].join( ';' );
        this.style.position = 'relative';
      document.body.appendChild( d );
        this.appendChild(indicator);
      dots.push( d );
       });
       pos.push( { x: 0, y: 0 } );
 
    }
      link.addEventListener('mouseleave', function () {
    document.addEventListener( 'mousemove', function ( e ) { mx = e.clientX; my = e.clientY; } );
        const indicator = this.querySelector('div[style*="hadesNavLine"]');
    ( function tick() {
         if (indicator) indicator.remove();
      pos.unshift( { x: mx, y: my } );
       });
      pos.length = LEN;
     });
      for ( var k = 0; k < LEN; k++ ) {
        dots[ k ].style.left = ( pos[ k ].x - 3 ) + 'px';
         dots[ k ].style.top  = ( pos[ k ].y - 3 ) + 'px';
       }
      requestAnimationFrame( tick );
     }() );
   }
   }


   /* ─────────────────────────────────────────
   /* ── CANDLE GLOW ON HEADERS ──────────────────────────────── */
    8. SCROLL PROGRESS BAR
   function animateHeaders() {
  ───────────────────────────────────────── */
     var heads = document.querySelectorAll( '.mw-headline, h2, h3' );
   function createScrollProgressBar() {
     heads.forEach( function ( h, i ) {
     const bar = document.createElement('div');
       h.style.animationDelay = ( i * 0.4 ) + 's';
    bar.className = 'hades-progress';
     } );
     bar.style.width = '0%';
    document.body.appendChild(bar);
 
    window.addEventListener('scroll', () => {
       const scrollTop = window.scrollY;
      const docHeight = document.documentElement.scrollHeight - window.innerHeight;
      const progress = docHeight > 0 ? (scrollTop / docHeight) * 100 : 0;
      bar.style.width = progress + '%';
     }, { passive: true });
   }
   }


   /* ─────────────────────────────────────────
   /* ── AMBIENT HAZE ────────────────────────────────────────── */
    9. TABLE ENHANCEMENTS
   function createHaze() {
  ───────────────────────────────────────── */
     var hz = document.createElement( 'div' );
   function enhanceTables() {
    hz.id = 'hades-haze';
     document.querySelectorAll('.wikitable tr').forEach((row, i) => {
    hz.style.cssText = [
       row.addEventListener('mouseenter', function () {
      'position:fixed',
         this.style.background = 'rgba(200,146,42,0.08)';
      'inset:0',
         this.style.transition = 'background 0.2s ease';
      'pointer-events:none',
         this.querySelectorAll('td').forEach(td => {
       'z-index:1',
          td.style.color = '#d4c4a0';
      'background:' +
          td.style.transition = 'color 0.2s ease';
         'radial-gradient(ellipse 70% 50% at 50% 10%,rgba(200,134,10,0.06) 0%,transparent 70%),' +
        });
         'radial-gradient(ellipse 50% 40% at 50% 95%,rgba(168,72,88,0.04) 0%,transparent 65%),' +
      });
         'radial-gradient(ellipse 30% 30% at 80% 30%,rgba(248,200,64,0.03) 0%,transparent 60%)',
      row.addEventListener('mouseleave', function () {
      'animation:h-breathe 10s ease-in-out infinite alternate'
        this.style.background = '';
    ].join( ';' );
        this.querySelectorAll('td').forEach(td => { td.style.color = ''; });
    document.body.insertBefore( hz, document.body.firstChild );
      });
    });
   }
   }


   /* ─────────────────────────────────────────
   /* ── MAIN ─────────────────────────────────────────────────── */
    10. SEARCH BAR ENHANCEMENTS
   function init() {
  ───────────────────────────────────────── */
     if ( typeof mw !== 'undefined' ) {
   function enhanceSearch() {
       var action = mw.config.get( 'wgAction' );
    const searchInput = document.querySelector('#searchInput, .vector-search-box input[type="search"]');
       if ( action === 'edit' || action === 'submit' ) { return; }
     if (!searchInput) return;
 
    searchInput.placeholder = 'Search HADES Wiki...';
 
    searchInput.addEventListener('focus', function () {
       this.parentElement.style.boxShadow = '0 0 0 3px rgba(200,146,42,0.2), 0 4px 20px rgba(0,0,0,0.5)';
      this.style.borderColor = '#c8922a';
    });
 
    searchInput.addEventListener('blur', function () {
      this.parentElement.style.boxShadow = '';
       this.style.borderColor = '';
    });
 
    // Add search icon label
    const label = document.createElement('span');
    label.textContent = '🔍';
    label.style.cssText = `
      position:absolute;
      left:12px;
      top:50%;
      transform:translateY(-50%);
      pointer-events:none;
      font-size:14px;
      z-index:5;
    `;
    const searchWrapper = searchInput.parentElement;
    if (searchWrapper) {
      searchWrapper.style.position = 'relative';
      searchWrapper.prepend(label);
     }
     }
  }


  /* ─────────────────────────────────────────
     preloadFonts();
    11. FIRST HEADING SUBTITLE
     createHaze();
  ───────────────────────────────────────── */
  function enhanceFirstHeading() {
     const h1 = document.querySelector('#firstHeading, h1.firstHeading');
     if (!h1) return;


     // Add decorative ornaments around h1
     var layer = createLayer();
    h1.innerHTML = h1.innerHTML;


     const sub = document.createElement('div');
     /* ── Candles ── */
     sub.style.cssText = `
    for ( var i = 0; i < 5; i++ )  { spawn( layer, 'candleTall',    'h-candle-el', { minScale: 0.4, maxScale: 0.9, edged: true  } ); }
      font-family:'IM Fell English',serif;
     for ( var j = 0; j < 5; j++ )  { spawn( layer, 'candleShort',   'h-candle-el', { minScale: 0.5, maxScale: 1.0, edged: true  } ); }
      font-style:italic;
    for ( var k = 0; k < 3; k++ )  { spawn( layer, 'candleCluster', 'h-candle-el', { minScale: 0.4, maxScale: 0.7, edged: false } ); }
      font-size:13px;
    for ( var ci = 0; ci < 2; ci++ ) { spawn( layer, 'candelabra',  'h-candle-el', { minScale: 0.35, maxScale: 0.6, edged: true, opacity: '0.12' } ); }
      color:#8b7a50;
      margin-top:4px;
      letter-spacing:0.5px;
    `;


     // Add page metadata
     /* ── Gothic arches ── */
     const pageName = h1.textContent.trim();
     for ( var a = 0; a < 5; a++ ) { spawn( layer, 'gothicArch',        'h-arch-el', { minScale: 0.5, maxScale: 1.2, opacity: '0.1' } ); }
    const ns = mw.config.get('wgNamespaceNumber');
    if (ns === 0 && pageName) {
      // Main article namespace
      sub.textContent = `Article · Hades Wiki`;
      h1.parentNode.insertBefore(sub, h1.nextSibling);
    }
  }


  /* ─────────────────────────────────────────
    /* ── Mirror silhouettes ── */
    12. FOOTER ENHANCEMENT
    for ( var m = 0; m < 3; m++ ) { spawn( layer, 'mirrorSilhouette', 'h-arch-el', { minScale: 0.4, maxScale: 0.9, opacity: '0.08' } ); }
  ───────────────────────────────────────── */
  function enhanceFooter() {
    const footer = document.querySelector('#footer, .mw-footer');
    if (!footer) return;


     const logo = document.createElement('div');
     /* ── Sacred hearts ── */
     logo.style.cssText = `
    for ( var sh = 0; sh < 4; sh++ ) { spawn( layer, 'sacredHeart',     'h-arch-el', { minScale: 0.4, maxScale: 0.9, opacity: '0.1' } ); }
      font-family:'Cinzel Decorative',serif;
      font-size:16px;
      color:#8b6914;
      letter-spacing:4px;
      margin-bottom:12px;
      text-align:center;
      animation:hadesTitlePulse 4s ease-in-out infinite;
    `;
    logo.textContent = '⚜ HADES WIKI ⚜';
    footer.prepend(logo);


     const divider = document.createElement('div');
     /* ── Crowns ── */
     divider.style.cssText = `
     for ( var cr = 0; cr < 3; cr++ ) { spawn( layer, 'crown',          'h-candle-el', { minScale: 0.5, maxScale: 0.9, opacity: '0.09' } ); }
      height:1px;
      background:linear-gradient(to right, transparent, #c8922a, transparent);
      margin:12px 0;
      opacity:0.4;
    `;
    footer.prepend(divider);
  }


  /* ─────────────────────────────────────────
    /* ── Damask ornaments ── */
    13. IMAGE LIGHTBOX (simple)
    for ( var d = 0; d < 6; d++ ) { spawn( layer, 'damask',   'h-damask-el', { minScale: 0.6, maxScale: 1.4, opacity: '0.07' } ); }
  ───────────────────────────────────────── */
  function initLightbox() {
    document.querySelectorAll('.thumbimage img, .infobox img').forEach(img => {
      img.style.cursor = 'zoom-in';
      img.addEventListener('click', function (e) {
        e.preventDefault();


        const overlay = document.createElement('div');
    /* ── Roses ── */
        overlay.style.cssText = `
    for ( var r = 0; r < 5; r++ )  { spawn( layer, 'rose',     'h-candle-el', { minScale: 0.5, maxScale: 1.0, opacity: '0.1' } ); }
          position:fixed;
          inset:0;
          background:rgba(0,0,0,0.92);
          z-index:99999;
          display:flex;
          align-items:center;
          justify-content:center;
          cursor:zoom-out;
          animation:hadesPageReveal 0.3s ease forwards;
        `;


        const clone = this.cloneNode();
    /* ── Keys ── */
        clone.style.cssText = `
    for ( var kk = 0; kk < 4; kk++ ) { spawn( layer, 'key',   'h-arch-el', { minScale: 0.4, maxScale: 0.9, opacity: '0.09' } ); }
          max-width:90vw;
          max-height:90vh;
          object-fit:contain;
          border:1px solid rgba(200,146,42,0.4);
          box-shadow:0 0 60px rgba(200,146,42,0.2),0 0 120px rgba(0,0,0,0.8);
        `;


        const closeBtn = document.createElement('div');
    /* ── Wax drips (scattered) ── */
        closeBtn.textContent = '';
    for ( var w = 0; w < 8; w++ )  { spawn( layer, 'waxDrip',  'h-candle-el', { minScale: 0.3, maxScale: 0.8, opacity: '0.12' } ); }
        closeBtn.style.cssText = `
          position:absolute;
          top:20px;
          right:24px;
          color:#c8922a;
          font-size:24px;
          cursor:pointer;
          font-family:Cinzel,serif;
          letter-spacing:2px;
          opacity:0.8;
          transition:opacity 0.2s;
        `;
        closeBtn.addEventListener('mouseenter', () => closeBtn.style.opacity = '1');


        const close = () => {
    /* ── Rose petals falling ── */
          overlay.style.opacity = '0';
    for ( var p = 0; p < 14; p++ ) { spawnPetal( layer ); }
          overlay.style.transition = 'opacity 0.3s ease';
          setTimeout(() => overlay.remove(), 300);
        };


        overlay.addEventListener('click', close);
    /* ── Dust motes ── */
        closeBtn.addEventListener('click', close);
    for ( var mo = 0; mo < 20; mo++ ) { spawnMote( layer ); }
        overlay.appendChild(clone);
        overlay.appendChild(closeBtn);
        document.body.appendChild(overlay);
      });
    });
  }


  /* ─────────────────────────────────────────
    /* ── Effects ── */
    14. MAIN EXECUTION (mw.loader.using)
     initParallax( layer );
  ───────────────────────────────────────── */
     initClickSpark();
  function init() {
     initCursorTrail();
    injectExtraStyles();
     animateHeaders();
    createParticles();
     injectWaxDrips();
    injectCandles();
    createScrollProgressBar();
    animatePageContent();
    enhanceLinks();
     enhanceNavigation();
     enhanceTables();
     enhanceSearch();
     enhanceFooter();
     initLightbox();


     // Slight delay for heading enhancement (needs mw object)
     /* Slowly add new petals over time */
     setTimeout(() => {
     setInterval( function () {
       try { enhanceFirstHeading(); } catch (e) { /* not critical */ }
       if ( layer.querySelectorAll( '.h-petal' ).length < 25 ) { spawnPetal( layer ); }
     }, 500);
     }, 8000 );


     console.log('%c⚜ HADES WIKI THEME LOADED ⚜', 'color:#c8922a;font-family:Cinzel,serif;font-size:14px;letter-spacing:3px;');
     /* Replenish motes */
    setInterval( function () {
      if ( layer.querySelectorAll( '.h-mote' ).length < 30 ) { spawnMote( layer ); }
    }, 4000 );
   }
   }


   // Use MediaWiki's jQuery.ready or plain DOMContentLoaded
   /* ── BOOT ─────────────────────────────────────────────────── */
   if (typeof jQuery !== 'undefined' && typeof mw !== 'undefined') {
   if ( typeof mw !== 'undefined' ) {
     mw.loader.using(['mediawiki.util'], function () {
     mw.loader.using( 'mediawiki.util' ).then( function () {
       jQuery(document).ready(function () {
       $( document ).ready( init );
        init();
     } );
      });
     });
   } else {
   } else {
     document.addEventListener('DOMContentLoaded', init);
     document.addEventListener( 'DOMContentLoaded', init );
   }
   }


})();
}() );

Latest revision as of 01:20, 15 April 2026

/* ================================================================
   H A D E S  —  MediaWiki:Common.js
   Ambient visual layer: candles, gothic arches, rose petals,
   sacred hearts, damask motifs, wax drips, dust motes
   ================================================================ */

( function () {
  'use strict';

  /* ── FONT PRELOAD ─────────────────────────────────────────── */
  function preloadFonts() {
    if ( document.getElementById( 'hades-fonts' ) ) { return; }
    var link = document.createElement( 'link' );
    link.id   = 'hades-fonts';
    link.rel  = 'stylesheet';
    link.href = 'https://fonts.googleapis.com/css2?family=Cinzel+Decorative:wght@400;700;900&family=Cinzel:wght@400;600;700&family=IM+Fell+English:ital@0;1&family=Cormorant+Garamond:ital,wght@0,300;0,400;0,600;1,400&family=UnifrakturMaguntia&family=Playfair+Display+SC:wght@400;700&family=Petit+Formal+Script&display=swap';
    document.head.appendChild( link );
  }

  /* ── SVG LIBRARY ──────────────────────────────────────────── */
  var SVG = {

    /* Tall pillar candle with dripping wax */
    candleTall: [
      '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 200" width="44" height="146">',
        /* flame */
        '<g style="animation:h-flame-dance 0.8s ease-in-out infinite;transform-origin:30px 22px">',
          '<ellipse cx="30" cy="28" rx="7" ry="14" fill="#ff7010" opacity="0.9"/>',
          '<ellipse cx="30" cy="30" rx="4" ry="10" fill="#f8c840" opacity="0.95"/>',
          '<ellipse cx="30" cy="32" rx="2" ry="5"  fill="#ffffff" opacity="0.7"/>',
          /* glow halo */
          '<ellipse cx="30" cy="28" rx="14" ry="18" fill="#f8c840" opacity="0.08"/>',
        '</g>',
        /* wick */
        '<line x1="30" y1="40" x2="30" y2="50" stroke="#3a2010" stroke-width="1.5"/>',
        /* wax body */
        '<rect x="18" y="48" width="24" height="130" rx="3" fill="#f2ede0"/>',
        '<rect x="20" y="50" width="4" height="128" rx="1" fill="#ffffff" opacity="0.15"/>',
        /* drips */
        '<path d="M22,50 Q20,60 21,75 Q22,82 22,90" stroke="#e8e0cc" stroke-width="3" fill="none" stroke-linecap="round"/>',
        '<path d="M36,50 Q38,65 37,78" stroke="#e8e0cc" stroke-width="2.5" fill="none" stroke-linecap="round"/>',
        '<path d="M28,48 Q27,58 28,68" stroke="#f0e8d0" stroke-width="2" fill="none" stroke-linecap="round"/>',
        '<ellipse cx="21" cy="90" rx="2.5" ry="3.5" fill="#d8d0c0"/>',
        '<ellipse cx="37" cy="78" rx="2" ry="3" fill="#d8d0c0"/>',
        /* base puddle */
        '<ellipse cx="30" cy="180" rx="18" ry="6" fill="#d8d0c0" opacity="0.6"/>',
        '<rect x="16" y="174" width="28" height="8" rx="3" fill="#c8c0b0"/>',
      '</svg>'
    ].join( '' ),

    /* Short stubby candle, more wax pooled */
    candleShort: [
      '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 120" width="36" height="86">',
        '<g style="animation:h-flame-dance 1.1s ease-in-out infinite;transform-origin:25px 18px">',
          '<ellipse cx="25" cy="22" rx="5" ry="10" fill="#ff7010" opacity="0.85"/>',
          '<ellipse cx="25" cy="24" rx="3" ry="7"  fill="#f8c840" opacity="0.95"/>',
          '<ellipse cx="25" cy="25" rx="1.5" ry="4" fill="#ffffff" opacity="0.6"/>',
          '<ellipse cx="25" cy="22" rx="10" ry="14" fill="#f8c840" opacity="0.07"/>',
        '</g>',
        '<line x1="25" y1="32" x2="25" y2="40" stroke="#3a2010" stroke-width="1.5"/>',
        '<rect x="14" y="38" width="22" height="65" rx="4" fill="#f0ece0"/>',
        '<rect x="16" y="40" width="3" height="62" rx="1" fill="#ffffff" opacity="0.12"/>',
        '<path d="M18,40 Q16,55 17,70 Q17,78 16,85" stroke="#e0d8c4" stroke-width="3" fill="none" stroke-linecap="round"/>',
        '<ellipse cx="16" cy="85" rx="2.5" ry="3" fill="#ccc4b0"/>',
        /* large wax pool */
        '<ellipse cx="25" cy="106" rx="22" ry="7" fill="#ddd8c8" opacity="0.7"/>',
        '<rect x="10" y="100" width="30" height="10" rx="4" fill="#c8c0b0"/>',
      '</svg>'
    ].join( '' ),

    /* Cluster of 3 candles on a holder */
    candleCluster: [
      '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 200" width="100" height="167">',
        /* left candle */
        '<g transform="translate(10,20)">',
          '<g style="animation:h-flame-dance 0.9s 0.2s ease-in-out infinite;transform-origin:20px 16px">',
            '<ellipse cx="20" cy="18" rx="5" ry="11" fill="#ff8020" opacity="0.9"/>',
            '<ellipse cx="20" cy="20" rx="3" ry="7"  fill="#f8c840"/>',
            '<ellipse cx="20" cy="22" rx="1.5" ry="4" fill="#fff" opacity="0.6"/>',
            '<ellipse cx="20" cy="18" rx="10" ry="14" fill="#f8c840" opacity="0.07"/>',
          '</g>',
          '<line x1="20" y1="28" x2="20" y2="35" stroke="#3a2010" stroke-width="1"/>',
          '<rect x="12" y="33" width="16" height="100" rx="3" fill="#f2ede0"/>',
          '<path d="M14,35 Q12,50 13,70" stroke="#e0d8c4" stroke-width="2.5" fill="none" stroke-linecap="round"/>',
        '</g>',
        /* centre candle (taller) */
        '<g transform="translate(40,0)">',
          '<g style="animation:h-flame-dance 0.7s ease-in-out infinite;transform-origin:20px 18px">',
            '<ellipse cx="20" cy="20" rx="6" ry="14" fill="#ff6010" opacity="0.95"/>',
            '<ellipse cx="20" cy="23" rx="4" ry="9"  fill="#f8c840"/>',
            '<ellipse cx="20" cy="25" rx="2" ry="5"  fill="#fff" opacity="0.7"/>',
            '<ellipse cx="20" cy="20" rx="14" ry="18" fill="#f8c840" opacity="0.08"/>',
          '</g>',
          '<line x1="20" y1="32" x2="20" y2="40" stroke="#3a2010" stroke-width="1.5"/>',
          '<rect x="11" y="38" width="18" height="130" rx="3" fill="#f0ece0"/>',
          '<path d="M13,40 Q11,60 12,85" stroke="#e0d8c4" stroke-width="3" fill="none" stroke-linecap="round"/>',
          '<ellipse cx="12" cy="85" rx="2.5" ry="3.5" fill="#ccc4b0"/>',
        '</g>',
        /* right candle */
        '<g transform="translate(76,28)">',
          '<g style="animation:h-flame-dance 1.0s 0.5s ease-in-out infinite;transform-origin:18px 15px">',
            '<ellipse cx="18" cy="16" rx="4" ry="9" fill="#ff7018" opacity="0.85"/>',
            '<ellipse cx="18" cy="18" rx="3" ry="6" fill="#f8c840"/>',
            '<ellipse cx="18" cy="20" rx="1.5" ry="3.5" fill="#fff" opacity="0.55"/>',
            '<ellipse cx="18" cy="16" rx="9" ry="13" fill="#f8c840" opacity="0.06"/>',
          '</g>',
          '<line x1="18" y1="25" x2="18" y2="32" stroke="#3a2010" stroke-width="1"/>',
          '<rect x="10" y="30" width="15" height="90" rx="3" fill="#f2ede0"/>',
          '<path d="M22,32 Q24,48 23,62" stroke="#e0d8c4" stroke-width="2.5" fill="none" stroke-linecap="round"/>',
        '</g>',
        /* shared base plate */
        '<rect x="4" y="170" width="112" height="14" rx="4" fill="#8a6030"/>',
        '<rect x="2" y="183" width="116" height="8" rx="3" fill="#6a4820"/>',
        '<ellipse cx="60" cy="191" rx="58" ry="6" fill="#4a3010" opacity="0.5"/>',
      '</svg>'
    ].join( '' ),

    /* Gothic pointed arch frame */
    gothicArch: [
      '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 150" width="90" height="135">',
        '<path d="M10,150 L10,70 Q10,5 50,5 Q90,5 90,70 L90,150" fill="none" stroke="#c8860a" stroke-width="1.5" opacity="0.7"/>',
        /* inner arch */
        '<path d="M18,150 L18,74 Q18,20 50,20 Q82,20 82,74 L82,150" fill="none" stroke="#e8b030" stroke-width="0.8" opacity="0.4"/>',
        /* keystone ornament */
        '<ellipse cx="50" cy="8" rx="6" ry="7" fill="none" stroke="#c8860a" stroke-width="1.2" opacity="0.7"/>',
        '<circle cx="50" cy="8" r="2.5" fill="#c8860a" opacity="0.6"/>',
        /* column capitals */
        '<rect x="4" y="140" width="14" height="10" rx="1" fill="#8a5a18" opacity="0.5"/>',
        '<rect x="82" y="140" width="14" height="10" rx="1" fill="#8a5a18" opacity="0.5"/>',
        /* tracery at top */
        '<path d="M35,35 Q50,18 65,35" fill="none" stroke="#c8860a" stroke-width="0.8" opacity="0.35"/>',
        '<circle cx="50" cy="30" r="4" fill="none" stroke="#e8b030" stroke-width="0.8" opacity="0.4"/>',
      '</svg>'
    ].join( '' ),

    /* Sacred heart */
    sacredHeart: [
      '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 90" width="70" height="79">',
        /* heart body */
        '<path d="M40,70 C20,55 5,45 5,28 C5,16 14,8 25,8 C31,8 37,11 40,16 C43,11 49,8 55,8 C66,8 75,16 75,28 C75,45 60,55 40,70Z" fill="#a84858" opacity="0.8"/>',
        /* light overlay */
        '<path d="M25,14 Q30,10 35,16" stroke="#f0b0c0" stroke-width="2" fill="none" opacity="0.5"/>',
        /* flames on top */
        '<g transform="translate(33,-2)">',
          '<path d="M7,10 Q5,4 7,0 Q9,4 7,10Z" fill="#f8c840" opacity="0.9"/>',
          '<path d="M14,12 Q12,5 14,1 Q16,5 14,12Z" fill="#ff7010" opacity="0.85"/>',
          '<path d="M21,10 Q19,4 21,0 Q23,4 21,10Z" fill="#f8c840" opacity="0.9"/>',
        '</g>',
        /* crown */
        '<g transform="translate(22,-5)">',
          '<path d="M0,12 L0,0 L9,5 L18,0 L27,5 L36,0 L36,12Z" fill="none" stroke="#e8b030" stroke-width="1" opacity="0.7"/>',
          '<circle cx="9"  cy="5" r="2" fill="#e8b030" opacity="0.7"/>',
          '<circle cx="18" cy="0" r="2.5" fill="#f8d060" opacity="0.8"/>',
          '<circle cx="27" cy="5" r="2" fill="#e8b030" opacity="0.7"/>',
        '</g>',
        /* thorns / ring */
        '<circle cx="40" cy="35" r="18" fill="none" stroke="#3a1e0c" stroke-width="1.5" stroke-dasharray="3,4" opacity="0.6"/>',
      '</svg>'
    ].join( '' ),

    /* Baroque damask ornament */
    damask: [
      '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="90" height="90">',
        '<g fill="none" stroke="#c8860a" stroke-width="0.6" opacity="0.8">',
          '<ellipse cx="50" cy="50" rx="8" ry="8"/>',
          '<ellipse cx="50" cy="22" rx="5" ry="16"/>',
          '<ellipse cx="50" cy="78" rx="5" ry="16"/>',
          '<ellipse cx="22" cy="50" rx="16" ry="5"/>',
          '<ellipse cx="78" cy="50" rx="16" ry="5"/>',
          '<path d="M50,34 Q60,42 50,50 Q40,42 50,34Z"/>',
          '<path d="M50,66 Q60,58 50,50 Q40,58 50,66Z"/>',
          '<path d="M34,50 Q42,60 50,50 Q42,40 34,50Z"/>',
          '<path d="M66,50 Q58,60 50,50 Q58,40 66,50Z"/>',
          '<circle cx="50" cy="10"  r="3"/>',
          '<circle cx="50" cy="90"  r="3"/>',
          '<circle cx="10" cy="50"  r="3"/>',
          '<circle cx="90" cy="50"  r="3"/>',
          '<path d="M38,38 Q50,30 62,38 Q70,50 62,62 Q50,70 38,62 Q30,50 38,38Z"/>',
        '</g>',
      '</svg>'
    ].join( '' ),

    /* Rose petal */
    petal: [
      '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 36" width="18" height="27">',
        '<path d="M12,2 Q20,8 20,20 Q20,32 12,34 Q4,32 4,20 Q4,8 12,2Z" fill="#c06070" opacity="0.7"/>',
        '<path d="M12,6 Q16,12 15,22" stroke="#e090a0" stroke-width="0.8" fill="none" opacity="0.5"/>',
      '</svg>'
    ].join( '' ),

    /* Ornate candelabra */
    candelabra: [
      '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 160 260" width="130" height="211">',
        /* left arm candle */
        '<g transform="translate(4,30)">',
          '<g style="animation:h-flame-dance 0.85s 0.1s ease-in-out infinite;transform-origin:18px 16px">',
            '<ellipse cx="18" cy="18" rx="5" ry="12" fill="#ff7010" opacity="0.9"/>',
            '<ellipse cx="18" cy="20" rx="3" ry="8"  fill="#f8c840"/>',
            '<ellipse cx="18" cy="22" rx="2" ry="4"  fill="#fff" opacity="0.6"/>',
            '<ellipse cx="18" cy="18" rx="12" ry="16" fill="#f8c840" opacity="0.06"/>',
          '</g>',
          '<line x1="18" y1="30" x2="18" y2="36" stroke="#3a2010" stroke-width="1.2"/>',
          '<rect x="11" y="34" width="14" height="80" rx="3" fill="#f0ece0"/>',
          '<path d="M12,36 Q10,55 11,72" stroke="#e0d8c4" stroke-width="2.5" fill="none" stroke-linecap="round"/>',
        '</g>',
        /* right arm candle */
        '<g transform="translate(134,30)">',
          '<g style="animation:h-flame-dance 1.05s 0.3s ease-in-out infinite;transform-origin:18px 16px">',
            '<ellipse cx="18" cy="18" rx="5" ry="12" fill="#ff7010" opacity="0.9"/>',
            '<ellipse cx="18" cy="20" rx="3" ry="8"  fill="#f8c840"/>',
            '<ellipse cx="18" cy="22" rx="2" ry="4"  fill="#fff" opacity="0.6"/>',
            '<ellipse cx="18" cy="18" rx="12" ry="16" fill="#f8c840" opacity="0.06"/>',
          '</g>',
          '<line x1="18" y1="30" x2="18" y2="36" stroke="#3a2010" stroke-width="1.2"/>',
          '<rect x="11" y="34" width="14" height="80" rx="3" fill="#f0ece0"/>',
          '<path d="M24,36 Q26,55 25,72" stroke="#e0d8c4" stroke-width="2.5" fill="none" stroke-linecap="round"/>',
        '</g>',
        /* centre tall candle */
        '<g transform="translate(62,0)">',
          '<g style="animation:h-flame-dance 0.7s ease-in-out infinite;transform-origin:18px 20px">',
            '<ellipse cx="18" cy="20" rx="7" ry="16" fill="#ff6010" opacity="0.95"/>',
            '<ellipse cx="18" cy="23" rx="4" ry="10" fill="#f8c840"/>',
            '<ellipse cx="18" cy="26" rx="2" ry="6"  fill="#fff" opacity="0.7"/>',
            '<ellipse cx="18" cy="20" rx="16" ry="22" fill="#f8c840" opacity="0.08"/>',
          '</g>',
          '<line x1="18" y1="36" x2="18" y2="44" stroke="#3a2010" stroke-width="1.5"/>',
          '<rect x="10" y="42" width="16" height="120" rx="3" fill="#f2ede0"/>',
          '<path d="M11,44 Q9,68 10,95" stroke="#e0d8c4" stroke-width="3" fill="none" stroke-linecap="round"/>',
          '<ellipse cx="10" cy="95" rx="2.5" ry="3.5" fill="#ccc4b0"/>',
        '</g>',
        /* arms */
        '<path d="M80,90 Q30,80 22,60" fill="none" stroke="#8a6030" stroke-width="3" stroke-linecap="round"/>',
        '<path d="M80,90 Q130,80 152,60" fill="none" stroke="#8a6030" stroke-width="3" stroke-linecap="round"/>',
        /* stem */
        '<rect x="74" y="160" width="12" height="70" rx="4" fill="#7a5020"/>',
        '<ellipse cx="80" cy="162" rx="12" ry="6" fill="#8a6030"/>',
        /* base */
        '<ellipse cx="80" cy="236" rx="45" ry="12" fill="#6a4020" opacity="0.7"/>',
        '<rect x="50" y="228" width="60" height="10" rx="4" fill="#8a5820"/>',
      '</svg>'
    ].join( '' ),

    /* Mirror silhouette (album cover gothic arch mirror) */
    mirrorSilhouette: [
      '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 160" width="80" height="128">',
        /* outer frame */
        '<path d="M8,160 L8,80 Q8,5 50,5 Q92,5 92,80 L92,160" fill="none" stroke="#8a5a18" stroke-width="3" opacity="0.6"/>',
        /* inner mirror */
        '<path d="M16,158 L16,82 Q16,18 50,18 Q84,18 84,82 L84,158" fill="rgba(10,5,2,0.5)" stroke="#c8860a" stroke-width="1" opacity="0.5"/>',
        /* glow inside mirror */
        '<path d="M16,158 L16,82 Q16,18 50,18 Q84,18 84,82 L84,158 Z" fill="url(#mirrorGlow)" opacity="0.3"/>',
        '<defs><radialGradient id="mirrorGlow" cx="50%" cy="60%"><stop offset="0%" stop-color="#f8c840" stop-opacity="0.15"/><stop offset="100%" stop-color="#f8c840" stop-opacity="0"/></radialGradient></defs>',
        /* spire ornaments */
        '<path d="M8,80 L8,50 L14,30 L20,50 L20,80" fill="none" stroke="#8a5a18" stroke-width="1.5" opacity="0.5"/>',
        '<path d="M80,80 L80,50 L86,30 L92,50 L92,80" fill="none" stroke="#8a5a18" stroke-width="1.5" opacity="0.5"/>',
        /* keystone */
        '<ellipse cx="50" cy="8" rx="7" ry="8" fill="none" stroke="#c8860a" stroke-width="1.5" opacity="0.6"/>',
        '<circle cx="50" cy="8" r="3" fill="#c8860a" opacity="0.5"/>',
        /* base shelf */
        '<rect x="2" y="152" width="96" height="10" rx="2" fill="#6a4820" opacity="0.5"/>',
        /* sacred heart base motif */
        '<path d="M50,148 C44,143 38,140 38,134 C38,130 41,127 45,127 C47,127 49,128 50,130 C51,128 53,127 55,127 C59,127 62,130 62,134 C62,140 56,143 50,148Z" fill="#a84858" opacity="0.4"/>',
      '</svg>'
    ].join( '' ),

    /* Wax drip trail */
    waxDrip: [
      '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 120" width="24" height="96">',
        '<path d="M15,0 Q18,20 17,35 Q16,55 18,70 Q20,85 15,100 Q10,85 12,70 Q14,55 13,35 Q12,20 15,0Z" fill="#f2ede0" opacity="0.7"/>',
        '<ellipse cx="15" cy="100" rx="8" ry="6" fill="#e8e0cc" opacity="0.6"/>',
      '</svg>'
    ].join( '' ),

    /* Ornate key */
    key: [
      '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 120" width="30" height="90">',
        '<circle cx="20" cy="20" r="14" fill="none" stroke="#c8860a" stroke-width="2" opacity="0.8"/>',
        '<circle cx="20" cy="20" r="7"  fill="none" stroke="#e8b030" stroke-width="1" opacity="0.6"/>',
        '<circle cx="20" cy="20" r="3"  fill="#c8860a" opacity="0.7"/>',
        '<rect x="18" y="34" width="4" height="80" rx="1" fill="#c8860a" opacity="0.7"/>',
        '<rect x="22" y="80" width="12" height="4" rx="1" fill="#c8860a" opacity="0.7"/>',
        '<rect x="22" y="94" width="9"  height="4" rx="1" fill="#c8860a" opacity="0.7"/>',
      '</svg>'
    ].join( '' ),

    /* Crown */
    crown: [
      '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 80" width="100" height="67">',
        '<path d="M5,75 L5,35 L25,10 L40,35 L60,5 L80,35 L95,10 L115,35 L115,75 Z" fill="none" stroke="#c8860a" stroke-width="2" stroke-linejoin="round" opacity="0.7"/>',
        '<path d="M5,60 L115,60" stroke="#e8b030" stroke-width="1" opacity="0.5"/>',
        '<circle cx="25"  cy="13" r="4"  fill="#e8b030" opacity="0.7"/>',
        '<circle cx="60"  cy="7"  r="5"  fill="#f8d060" opacity="0.8"/>',
        '<circle cx="95"  cy="13" r="4"  fill="#e8b030" opacity="0.7"/>',
        '<circle cx="40"  cy="36" r="3"  fill="#d08090" opacity="0.6"/>',
        '<circle cx="80"  cy="36" r="3"  fill="#d08090" opacity="0.6"/>',
        '<rect x="5" y="68" width="110" height="8" rx="2" fill="#8a5a18" opacity="0.4"/>',
      '</svg>'
    ].join( '' ),

    /* Floating rose */
    rose: [
      '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60" width="48" height="48">',
        '<circle cx="30" cy="30" r="10" fill="#a84858" opacity="0.8"/>',
        '<path d="M30,20 Q38,24 36,32 Q30,38 22,32 Q20,24 30,20Z" fill="#c06070" opacity="0.7"/>',
        '<path d="M30,20 Q22,24 24,32 Q30,38 38,32 Q40,24 30,20Z" fill="#b05060" opacity="0.7"/>',
        '<path d="M30,14 Q40,18 40,28 Q40,38 30,42 Q20,38 20,28 Q20,18 30,14Z" fill="none" stroke="#d08090" stroke-width="0.8" opacity="0.5"/>',
        '<path d="M30,8  Q44,14 44,30 Q44,46 30,50 Q16,46 16,30 Q16,14 30,8Z"  fill="none" stroke="#c06070" stroke-width="0.6" opacity="0.4"/>',
        '<path d="M30,50 L30,60" stroke="#3a1e0c" stroke-width="1.5"/>',
        '<path d="M24,56 Q30,52 36,56" stroke="#2a1808" stroke-width="1" fill="none"/>',
      '</svg>'
    ].join( '' )
  };

  /* ── HELPERS ──────────────────────────────────────────────── */
  function rand( a, b ) { return Math.random() * ( b - a ) + a; }
  function randInt( a, b ) { return Math.floor( rand( a, b + 1 ) ); }

  /* ── BUILD LAYER ──────────────────────────────────────────── */
  function createLayer() {
    var div = document.createElement( 'div' );
    div.className = 'h-ambient';
    div.id = 'hades-ambient';
    document.body.insertBefore( div, document.body.firstChild );
    return div;
  }

  /* ── SPAWN AMBIENT ELEMENT ───────────────────────────────── */
  function spawn( layer, svgKey, cssClass, opts ) {
    opts = opts || {};
    var el = document.createElement( 'div' );
    el.className = cssClass;
    el.innerHTML = SVG[ svgKey ];

    var scale = rand( opts.minScale || 0.5, opts.maxScale || 1.4 );
    el.style.transformOrigin = 'center center';
    el.style.transform = 'scale(' + scale + ')';

    /* Position */
    var edged = ( opts.edged !== false ) && Math.random() < 0.35;
    var top, left;
    if ( edged ) {
      var side = randInt( 0, 3 );
      if ( side === 0 )      { top = rand( 0, 8 );   left = rand( 5, 90 ); }
      else if ( side === 1 ) { top = rand( 85, 97 ); left = rand( 5, 90 ); }
      else if ( side === 2 ) { top = rand( 5, 90 );  left = rand( 0, 6 );  }
      else                   { top = rand( 5, 90 );  left = rand( 88, 97 );}
    } else {
      top  = rand( opts.topMin  || 5, opts.topMax  || 90 );
      left = rand( opts.leftMin || 5, opts.leftMax || 90 );
    }
    el.style.top  = top  + 'vh';
    el.style.left = left + 'vw';

    /* Animation stagger */
    var delay = rand( 0, 18 );
    var dur   = rand( opts.durMin || 5, opts.durMax || 14 );
    el.style.animationDelay    = delay + 's';
    el.style.animationDuration = dur + 's';
    if ( opts.opacity ) { el.style.opacity = opts.opacity; }

    layer.appendChild( el );
    return el;
  }

  /* ── ROSE PETAL FALL ─────────────────────────────────────── */
  function spawnPetal( layer ) {
    var el = document.createElement( 'div' );
    el.className = 'h-petal';
    el.innerHTML = SVG.petal;
    el.style.left  = rand( 5, 95 ) + 'vw';
    el.style.top   = '-30px';
    el.style.setProperty( '--drift', rand( -40, 60 ) + 'px' );
    el.style.setProperty( '--rot',   rand( -180, 360 ) + 'deg' );
    var dur = rand( 8, 18 );
    el.style.animationDuration = dur + 's';
    el.style.animationDelay    = rand( 0, 20 ) + 's';
    el.style.opacity = rand( 0.25, 0.55 );
    layer.appendChild( el );
  }

  /* ── DUST MOTES ──────────────────────────────────────────── */
  function spawnMote( layer ) {
    var el = document.createElement( 'div' );
    el.className = 'h-mote';
    el.style.left = rand( 10, 90 ) + 'vw';
    el.style.top  = rand( 20, 90 ) + 'vh';
    el.style.setProperty( '--mx', rand( -30, 30 ) + 'px' );
    el.style.setProperty( '--my', rand( -80, -120 ) + 'px' );
    var dur = rand( 5, 12 );
    el.style.animationDuration = dur + 's';
    el.style.animationDelay    = rand( 0, 15 ) + 's';
    /* Vary colour: gold, rose, cream */
    var colours = [ '#f8d060', '#e8b030', '#d08090', '#f0e8d0' ];
    el.style.background = colours[ randInt( 0, colours.length - 1 ) ];
    el.style.boxShadow  = '0 0 ' + rand( 2, 6 ) + 'px currentColor';
    layer.appendChild( el );
  }

  /* ── WAX DRIP TRAILS ON CONTENT EDGE ────────────────────── */
  function injectWaxDrips() {
    var content = document.getElementById( 'content' )
                || document.getElementById( 'mw-content-text' )
                || document.querySelector( '.mw-body' );
    if ( !content ) { return; }

    /* Top drip row — candle wax flowing down */
    var topBar = document.createElement( 'div' );
    topBar.style.cssText = [
      'position:absolute',
      'top:-1px',
      'left:0',
      'width:100%',
      'height:40px',
      'pointer-events:none',
      'overflow:hidden',
      'z-index:10'
    ].join( ';' );

    for ( var i = 0; i < 12; i++ ) {
      var drip = document.createElement( 'div' );
      drip.innerHTML = SVG.waxDrip;
      drip.style.cssText = [
        'position:absolute',
        'top:0',
        'left:' + ( i * 8.5 + rand( 0, 4 ) ) + '%',
        'opacity:' + rand( 0.2, 0.45 ),
        'transform:scaleY(0)',
        'transform-origin:top center',
        'animation:h-wax-drip ' + rand( 2, 5 ) + 's ' + rand( 0, 8 ) + 's ease-in forwards'
      ].join( ';' );
      topBar.appendChild( drip );
    }

    /* Make content position:relative so absolute children work */
    if ( getComputedStyle( content ).position === 'static' ) {
      content.style.position = 'relative';
    }
    content.appendChild( topBar );
  }

  /* ── MOUSE PARALLAX ──────────────────────────────────────── */
  function initParallax( layer ) {
    var cx = window.innerWidth / 2, cy = window.innerHeight / 2;
    var tx = 0, ty = 0, cx2 = 0, cy2 = 0;
    document.addEventListener( 'mousemove', function ( e ) {
      tx = ( e.clientX - cx ) / cx * 12;
      ty = ( e.clientY - cy ) / cy * 8;
    } );
    ( function tick() {
      cx2 += ( tx - cx2 ) * 0.03;
      cy2 += ( ty - cy2 ) * 0.03;
      layer.style.transform = 'translate(' + cx2 + 'px,' + cy2 + 'px)';
      requestAnimationFrame( tick );
    }() );
  }

  /* ── CANDLE CLICK SPARK ──────────────────────────────────── */
  function initClickSpark() {
    document.addEventListener( 'click', function ( e ) {
      var sparks = 6;
      for ( var s = 0; s < sparks; s++ ) {
        ( function ( i ) {
          var sp = document.createElement( 'div' );
          var angle = ( i / sparks ) * Math.PI * 2 + rand( -0.3, 0.3 );
          var dist  = rand( 20, 55 );
          sp.style.cssText = [
            'position:fixed',
            'pointer-events:none',
            'z-index:9994',
            'width:' + rand( 3, 6 ) + 'px',
            'height:' + rand( 3, 6 ) + 'px',
            'border-radius:50%',
            'left:' + e.clientX + 'px',
            'top:'  + e.clientY + 'px',
            'background:' + ( Math.random() < 0.5 ? '#f8c840' : '#ff7010' ),
            'box-shadow:0 0 6px #f8c840',
            'transition:transform 0.5s ease-out,opacity 0.5s ease-out',
            'opacity:0.9'
          ].join( ';' );
          document.body.appendChild( sp );
          requestAnimationFrame( function () {
            requestAnimationFrame( function () {
              sp.style.transform = 'translate(' + Math.cos( angle ) * dist + 'px,' + Math.sin( angle ) * dist + 'px)';
              sp.style.opacity   = '0';
              setTimeout( function () { if ( sp.parentNode ) { sp.parentNode.removeChild( sp ); } }, 550 );
            } );
          } );
        }( s ) );
      }
    } );
  }

  /* ── GOLD CURSOR TRAIL ────────────────────────────────────── */
  function initCursorTrail() {
    var LEN = 8;
    var dots = [], pos = [];
    var mx = 0, my = 0;
    for ( var i = 0; i < LEN; i++ ) {
      var d = document.createElement( 'div' );
      var sz = ( 6 - i * 0.5 );
      d.style.cssText = [
        'position:fixed',
        'pointer-events:none',
        'z-index:9996',
        'width:'  + sz + 'px',
        'height:' + sz + 'px',
        'border-radius:50%',
        'opacity:' + ( ( LEN - i ) / LEN * 0.7 ),
        'background:' + ( i % 3 === 0 ? '#f8c840' : i % 3 === 1 ? '#c8860a' : '#d08090' ),
        'box-shadow:0 0 ' + sz + 'px ' + ( i % 3 === 0 ? '#f8c840' : '#c8860a' ),
        'mix-blend-mode:screen'
      ].join( ';' );
      document.body.appendChild( d );
      dots.push( d );
      pos.push( { x: 0, y: 0 } );
    }
    document.addEventListener( 'mousemove', function ( e ) { mx = e.clientX; my = e.clientY; } );
    ( function tick() {
      pos.unshift( { x: mx, y: my } );
      pos.length = LEN;
      for ( var k = 0; k < LEN; k++ ) {
        dots[ k ].style.left = ( pos[ k ].x - 3 ) + 'px';
        dots[ k ].style.top  = ( pos[ k ].y - 3 ) + 'px';
      }
      requestAnimationFrame( tick );
    }() );
  }

  /* ── CANDLE GLOW ON HEADERS ──────────────────────────────── */
  function animateHeaders() {
    var heads = document.querySelectorAll( '.mw-headline, h2, h3' );
    heads.forEach( function ( h, i ) {
      h.style.animationDelay = ( i * 0.4 ) + 's';
    } );
  }

  /* ── AMBIENT HAZE ────────────────────────────────────────── */
  function createHaze() {
    var hz = document.createElement( 'div' );
    hz.id = 'hades-haze';
    hz.style.cssText = [
      'position:fixed',
      'inset:0',
      'pointer-events:none',
      'z-index:1',
      'background:' +
        'radial-gradient(ellipse 70% 50% at 50% 10%,rgba(200,134,10,0.06) 0%,transparent 70%),' +
        'radial-gradient(ellipse 50% 40% at 50% 95%,rgba(168,72,88,0.04) 0%,transparent 65%),' +
        'radial-gradient(ellipse 30% 30% at 80% 30%,rgba(248,200,64,0.03) 0%,transparent 60%)',
      'animation:h-breathe 10s ease-in-out infinite alternate'
    ].join( ';' );
    document.body.insertBefore( hz, document.body.firstChild );
  }

  /* ── MAIN ─────────────────────────────────────────────────── */
  function init() {
    if ( typeof mw !== 'undefined' ) {
      var action = mw.config.get( 'wgAction' );
      if ( action === 'edit' || action === 'submit' ) { return; }
    }

    preloadFonts();
    createHaze();

    var layer = createLayer();

    /* ── Candles ── */
    for ( var i = 0; i < 5; i++ )  { spawn( layer, 'candleTall',    'h-candle-el', { minScale: 0.4, maxScale: 0.9, edged: true  } ); }
    for ( var j = 0; j < 5; j++ )  { spawn( layer, 'candleShort',   'h-candle-el', { minScale: 0.5, maxScale: 1.0, edged: true  } ); }
    for ( var k = 0; k < 3; k++ )  { spawn( layer, 'candleCluster', 'h-candle-el', { minScale: 0.4, maxScale: 0.7, edged: false } ); }
    for ( var ci = 0; ci < 2; ci++ ) { spawn( layer, 'candelabra',   'h-candle-el', { minScale: 0.35, maxScale: 0.6, edged: true, opacity: '0.12' } ); }

    /* ── Gothic arches ── */
    for ( var a = 0; a < 5; a++ )  { spawn( layer, 'gothicArch',        'h-arch-el', { minScale: 0.5, maxScale: 1.2, opacity: '0.1' } ); }

    /* ── Mirror silhouettes ── */
    for ( var m = 0; m < 3; m++ )  { spawn( layer, 'mirrorSilhouette',  'h-arch-el', { minScale: 0.4, maxScale: 0.9, opacity: '0.08' } ); }

    /* ── Sacred hearts ── */
    for ( var sh = 0; sh < 4; sh++ ) { spawn( layer, 'sacredHeart',     'h-arch-el', { minScale: 0.4, maxScale: 0.9, opacity: '0.1' } ); }

    /* ── Crowns ── */
    for ( var cr = 0; cr < 3; cr++ ) { spawn( layer, 'crown',           'h-candle-el', { minScale: 0.5, maxScale: 0.9, opacity: '0.09' } ); }

    /* ── Damask ornaments ── */
    for ( var d = 0; d < 6; d++ )  { spawn( layer, 'damask',   'h-damask-el', { minScale: 0.6, maxScale: 1.4, opacity: '0.07' } ); }

    /* ── Roses ── */
    for ( var r = 0; r < 5; r++ )  { spawn( layer, 'rose',     'h-candle-el', { minScale: 0.5, maxScale: 1.0, opacity: '0.1' } ); }

    /* ── Keys ── */
    for ( var kk = 0; kk < 4; kk++ ) { spawn( layer, 'key',    'h-arch-el', { minScale: 0.4, maxScale: 0.9, opacity: '0.09' } ); }

    /* ── Wax drips (scattered) ── */
    for ( var w = 0; w < 8; w++ )  { spawn( layer, 'waxDrip',  'h-candle-el', { minScale: 0.3, maxScale: 0.8, opacity: '0.12' } ); }

    /* ── Rose petals falling ── */
    for ( var p = 0; p < 14; p++ ) { spawnPetal( layer ); }

    /* ── Dust motes ── */
    for ( var mo = 0; mo < 20; mo++ ) { spawnMote( layer ); }

    /* ── Effects ── */
    initParallax( layer );
    initClickSpark();
    initCursorTrail();
    animateHeaders();
    injectWaxDrips();

    /* Slowly add new petals over time */
    setInterval( function () {
      if ( layer.querySelectorAll( '.h-petal' ).length < 25 ) { spawnPetal( layer ); }
    }, 8000 );

    /* Replenish motes */
    setInterval( function () {
      if ( layer.querySelectorAll( '.h-mote' ).length < 30 ) { spawnMote( layer ); }
    }, 4000 );
  }

  /* ── BOOT ─────────────────────────────────────────────────── */
  if ( typeof mw !== 'undefined' ) {
    mw.loader.using( 'mediawiki.util' ).then( function () {
      $( document ).ready( init );
    } );
  } else {
    document.addEventListener( 'DOMContentLoaded', init );
  }

}() );