Toggle menu
Toggle preferences menu
Toggle personal menu
Not logged in
Your IP address will be publicly visible if you make any edits.

Join the Playtest on Steam Now: SpiritVale

MediaWiki:Common.js: Difference between revisions

MediaWiki interface page
No edit summary
Tags: Reverted Mobile edit Mobile web edit
No edit summary
Tags: Mobile edit Mobile web edit
 
(11 intermediate revisions by the same user not shown)
Line 1: Line 1:
/* Common.js — Spirit Vale Wiki
/* Common.js — Spirit Vale Wiki
   Phase 4.1 rebuild (incremental)
   Phase 4.1 — Core client scripts


   Included (standalone modules):
   Included (standalone modules):
   1) Definitions (v1)
   1) Level Selector / Descriptions (data-series)
  2) Level Selector / Descriptions (data-series)
   2) Universal Popups (tips, discloses, definitions)
   3) Popups (Phase 4.1 span toggles + legacy <details> support)
   3) Tabs
   4) Tabs (Phase 4.1 key-based; uses .sv-hidden + [hidden])
*/
*/


(function () {
(function () {
   /* ================================================================== */
   /* ================================================================== */
   /* MODULE: Definitions (v1)                                           */
   /* MODULE: Level Selector / Descriptions (data-series)                 */
   /* ================================================================== */
   /* ================================================================== */


   var SV = (window.SV = window.SV || {});
   var SV = (window.SV = window.SV || {});
   var COMMON = (SV.common = SV.common || {});
   var COMMON = (SV.common = SV.common || {});
  if (COMMON.definitionsInit) return;
  COMMON.definitionsInit = 1;
  var DEF_SEL = ".sv-def";
  var TIP_ATTR = "data-sv-def-tip";
  var LINK_ATTR = "data-sv-def-link";
  var HOVER_CAPABLE = false;
  try {
    HOVER_CAPABLE =
      !!window.matchMedia &&
      window.matchMedia("(hover:hover) and (pointer:fine)").matches;
  } catch (e) {
    HOVER_CAPABLE = false;
  }
  var tipEl = null;
  var state = {
    visible: false,
    pinned: false,
    anchor: null,
    lastX: 0,
    lastY: 0,
    suppressClickUntil: 0,
  };
  function closest(el, sel) {
    if (!el || el.nodeType !== 1) return null;
    if (el.closest) return el.closest(sel);
    while (el && el.nodeType === 1) {
      if (el.matches && el.matches(sel)) return el;
      el = el.parentElement;
    }
    return null;
  }


   function getTipText(defEl) {
   // Versioned init guard (allows safe updates without stale blocks)
    if (!defEl || !defEl.getAttribute) return "";
  var LEVELS_VERSION = 6; // neutral hooks + bubble styling owned by CSS + no tick/check row
    var t = defEl.getAttribute(TIP_ATTR);
    if (t == null) return "";
    t = String(t);
    if (!t.replace(/\s+/g, "")) return "";
    return t;
  }


  function getLinkTitle(defEl) {
    if (!defEl || !defEl.getAttribute) return "";
    var t = defEl.getAttribute(LINK_ATTR);
    if (t == null) return "";
    return String(t).trim();
  }
  function ensureTipEl() {
    if (tipEl) return tipEl;
    var el = document.createElement("div");
    el.className = "sv-def-tip sv-hidden";
    el.setAttribute("role", "tooltip");
    el.setAttribute("aria-hidden", "true");
    el.style.position = "fixed";
    el.style.zIndex = "99999";
    el.style.maxWidth = "360px";
    el.style.boxSizing = "border-box";
    el.style.padding = "8px 10px";
    el.style.borderRadius = "10px";
    el.style.background = "rgba(10, 14, 22, 0.92)";
    el.style.color = "#fff";
    el.style.fontSize = "14px";
    el.style.lineHeight = "1.25";
    el.style.whiteSpace = "pre-line";
    el.style.pointerEvents = "none";
    el.style.display = "none";
    document.body.appendChild(el);
    tipEl = el;
    return tipEl;
  }
  function setHidden(el, hidden) {
    if (!el) return;
    if (hidden) {
      el.classList.add("sv-hidden");
      el.setAttribute("aria-hidden", "true");
      el.style.display = "none";
    } else {
      el.classList.remove("sv-hidden");
      el.setAttribute("aria-hidden", "false");
      el.style.display = "block";
    }
  }
  function clamp(n, min, max) {
    if (n < min) return min;
    if (n > max) return max;
    return n;
  }
  function positionTipAt(clientX, clientY) {
    var el = ensureTipEl();
    if (!el) return;
    var vw = window.innerWidth || document.documentElement.clientWidth || 0;
    var vh = window.innerHeight || document.documentElement.clientHeight || 0;
    var gapY = state.pinned ? 10 : 16;
    var gapX = 12;
    el.style.left = "0px";
    el.style.top = "0px";
    requestAnimationFrame(function () {
      if (!state.visible) return;
      var r = el.getBoundingClientRect();
      var w = r.width || 240;
      var h = r.height || 60;
      var left = clientX - w / 2;
      var top = clientY - h - gapY;
      left = clamp(left, gapX, Math.max(gapX, vw - w - gapX));
      if (top < gapY) top = clientY + gapY;
      top = clamp(top, gapY, Math.max(gapY, vh - h - gapY));
      el.style.left = Math.round(left) + "px";
      el.style.top = Math.round(top) + "px";
    });
  }
  function showTip(defEl, text, clientX, clientY, pinned) {
    var el = ensureTipEl();
    if (!el) return;
    state.visible = true;
    state.pinned = !!pinned;
    state.anchor = defEl || null;
    state.lastX = clientX || 0;
    state.lastY = clientY || 0;
    el.textContent = text;
    el.style.pointerEvents = state.pinned ? "auto" : "none";
    el.classList.toggle("sv-def-tip--pinned", state.pinned);
    setHidden(el, false);
    positionTipAt(state.lastX, state.lastY);
  }
  function hideTip() {
    if (!tipEl) return;
    state.visible = false;
    state.pinned = false;
    state.anchor = null;
    setHidden(tipEl, true);
  }
  function navigateToTitle(title) {
    if (!title) return;
    if (window.mw && mw.util && typeof mw.util.getUrl === "function") {
      window.location.href = mw.util.getUrl(title);
      return;
    }
    window.location.href = String(title);
  }
  function sameAnchor(el) {
    return !!(el && state.anchor && el === state.anchor);
  }
  function shouldSuppressClick() {
    return Date.now() < state.suppressClickUntil;
  }
  if (HOVER_CAPABLE) {
    document.addEventListener(
      "mouseover",
      function (e) {
        if (state.pinned) return;
        var defEl = closest(e.target, DEF_SEL);
        if (!defEl) {
          if (state.visible && !state.pinned) hideTip();
          return;
        }
        var rel = e.relatedTarget;
        if (rel && defEl.contains(rel)) return;
        var text = getTipText(defEl);
        if (!text) {
          if (state.visible && !state.pinned) hideTip();
          return;
        }
        showTip(defEl, text, e.clientX, e.clientY, false);
      },
      true
    );
    document.addEventListener(
      "mousemove",
      function (e) {
        if (!state.visible || state.pinned) return;
        var defEl = closest(e.target, DEF_SEL);
        if (!defEl || !sameAnchor(defEl)) return;
        state.lastX = e.clientX;
        state.lastY = e.clientY;
        positionTipAt(state.lastX, state.lastY);
      },
      true
    );
    document.addEventListener(
      "mouseout",
      function (e) {
        if (!state.visible || state.pinned) return;
        var defEl = closest(e.target, DEF_SEL);
        if (!defEl || !sameAnchor(defEl)) return;
        var rel = e.relatedTarget;
        if (rel && defEl.contains(rel)) return;
        hideTip();
      },
      true
    );
  }
  document.addEventListener(
    "pointerdown",
    function (e) {
      var defEl = closest(e.target, DEF_SEL);
      if (!defEl) return;
      var text = getTipText(defEl);
      if (!text) {
        if (state.visible) hideTip();
        return;
      }
      var isTouchLike = e.pointerType && e.pointerType !== "mouse";
      if (!isTouchLike) return;
      state.suppressClickUntil = Date.now() + 450;
      var link = getLinkTitle(defEl);
      if (state.pinned && sameAnchor(defEl) && link) {
        e.preventDefault();
        navigateToTitle(link);
        return;
      }
      e.preventDefault();
      showTip(defEl, text, e.clientX, e.clientY, true);
    },
    true
  );
  document.addEventListener(
    "click",
    function (e) {
      if (shouldSuppressClick()) return;
      var defEl = closest(e.target, DEF_SEL);
      if (!defEl) return;
      var text = getTipText(defEl);
      if (!text) {
        if (state.visible) hideTip();
        return;
      }
      var link = getLinkTitle(defEl);
      if (link) {
        e.preventDefault();
        navigateToTitle(link);
        return;
      }
      e.preventDefault();
      if (state.pinned && sameAnchor(defEl)) hideTip();
      else showTip(defEl, text, e.clientX, e.clientY, true);
    },
    true
  );
  document.addEventListener(
    "click",
    function (e) {
      if (!state.visible || !state.pinned) return;
      if (
        tipEl &&
        (e.target === tipEl || (tipEl.contains && tipEl.contains(e.target)))
      )
        return;
      if (closest(e.target, DEF_SEL)) return;
      hideTip();
    },
    true
  );
  document.addEventListener(
    "keydown",
    function (e) {
      if (e.key !== "Escape") return;
      if (state.visible) hideTip();
    },
    true
  );
  window.addEventListener(
    "scroll",
    function () {
      if (!state.visible) return;
      positionTipAt(state.lastX, state.lastY);
    },
    true
  );
  window.addEventListener(
    "resize",
    function () {
      if (!state.visible) return;
      positionTipAt(state.lastX, state.lastY);
    },
    true
  );
  function initAllDefs(root) {
    var container = root || document;
    var defs = container.querySelectorAll(DEF_SEL + "[title]");
    for (var i = 0; i < defs.length; i++) {
      defs[i].removeAttribute("title");
    }
  }
  if (window.mw && mw.hook) {
    mw.hook("wikipage.content").add(function ($content) {
      initAllDefs($content && $content[0]);
    });
  }
  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", function () {
      initAllDefs(document);
    });
  } else {
    initAllDefs(document);
  }
})();
(function () {
  /* ================================================================== */
  /* MODULE: Level Selector / Descriptions (data-series)                */
  /* ================================================================== */
  var SV = (window.SV = window.SV || {});
  var COMMON = (SV.common = SV.common || {});
  // --------- VERSIONED INIT (fixes "old script blocks new script") -----
  var LEVELS_VERSION = 2;
  // If an older Common.js set levelsInit=1, we still want THIS one to run.
   if (typeof COMMON.levelsInit === "number" && COMMON.levelsInit >= LEVELS_VERSION) return;
   if (typeof COMMON.levelsInit === "number" && COMMON.levelsInit >= LEVELS_VERSION) return;
   COMMON.levelsInit = LEVELS_VERSION;
   COMMON.levelsInit = LEVELS_VERSION;


   var CARD_SEL = ".sv-gi-card, .sv-skill-card, .sv-passive-card";
  // Prefer neutral shared hooks; keep legacy class fallbacks for compatibility.
   var CARD_SEL =
    "[data-sv-card='1'], .sv-gi-card, .sv-skill-card, .sv-passive-card";


   // Be a bit forgiving: accept the custom slider even if the data attr changes.
   // Accept the native range input and the custom slider variants.
   var LEVEL_RANGE_SEL =
   var LEVEL_RANGE_SEL =
     "input.sv-level-range[type='range'], .sv-level-range--custom, .sv-level-range[data-sv-slider='1']";
     "input.sv-level-range[type='range'], .sv-level-range--custom, .sv-level-range[data-sv-slider='1']";


   var LEVEL_BOUNDARY_SEL = ".sv-skill-level, .sv-gi-level";
   var LEVEL_BOUNDARY_SEL =
   var LEVEL_SCOPE_SEL = ".sv-gi-bottom, .sv-skill-bottom";
    "[data-sv-level-boundary='1'], .sv-skill-level, .sv-gi-level";
 
   var LEVEL_SCOPE_SEL =
    "[data-sv-level-scope='1'], .sv-gi-bottom, .sv-skill-bottom";
 
   var SERIES_SEL = "[data-series]";
   var SERIES_SEL = "[data-series]";
   var LEVEL_INIT_ATTR = "data-gi-level-init";
   var LEVEL_INIT_ATTR = "data-sv-level-init";


   var _seriesCache = typeof WeakMap !== "undefined" ? new WeakMap() : null;
   var _seriesCache = typeof WeakMap !== "undefined" ? new WeakMap() : null;
Line 461: Line 101:
     if (!el || !el.getAttribute) return false;
     if (!el || !el.getAttribute) return false;
     if (el.getAttribute("data-sv-slider") === "1") return true;
     if (el.getAttribute("data-sv-slider") === "1") return true;
    // fallback: class-based detection
     return !!(el.classList && el.classList.contains("sv-level-range--custom"));
     return !!(el.classList && el.classList.contains("sv-level-range--custom"));
   }
   }


   function parseMaxFromUi(card) {
   function parseMaxFromUi(card) {
    // fallback if attributes drift: parse "... / 10" out of the UI text
     var ui = card && card.querySelector ? card.querySelector(".sv-level-ui") : null;
     var ui = card && card.querySelector ? card.querySelector(".sv-level-ui") : null;
     if (!ui) return null;
     if (!ui) return null;
Line 484: Line 122:
   }
   }


   function getCardLevel(card, maxLevel) {
  function getCardMinLevel(card, slider, maxLevel) {
    // Prefer explicit neutral hooks, then legacy attrs, then slider attrs.
    var raw =
      card.getAttribute("data-min-level") ||
      card.getAttribute("data-minLevel") ||
      card.getAttribute("data-level-min");
 
    if (raw == null || raw === "") {
      if (slider) {
        if (isRangeInput(slider)) raw = slider.getAttribute("min");
        else if (isCustomSlider(slider))
          raw = slider.getAttribute("aria-valuemin") || slider.getAttribute("data-min");
      }
    }
 
    var min = clampInt(raw, 1, maxLevel, 1);
    if (min > maxLevel) min = maxLevel;
    return min;
  }
 
   function getCardLevel(card, minLevel, maxLevel) {
     var raw =
     var raw =
       card.getAttribute("data-level") ||
       card.getAttribute("data-level") ||
Line 492: Line 150:
         : null);
         : null);


     return clampInt(raw, 1, maxLevel, 1);
     return clampInt(raw, minLevel, maxLevel, minLevel);
  }
 
  function getStep(slider) {
    // Default = 1. Support future: data-step, or <input step="">
    var raw = null;
    if (slider) raw = slider.getAttribute("data-step") || slider.getAttribute("step");
    var s = parseInt(raw, 10);
    if (!s || isNaN(s) || s < 1) s = 1;
    return s;
   }
   }


Line 513: Line 180:
     var span = card.querySelector(".sv-level-num");
     var span = card.querySelector(".sv-level-num");
     if (span) span.textContent = String(level);
     if (span) span.textContent = String(level);
  }
  function setLevelMax(card, maxLevel) {
    var span = card.querySelector(".sv-level-max");
    if (span) span.textContent = String(maxLevel);
   }
   }


Line 518: Line 190:
     if (!scope) return;
     if (!scope) return;


    // JS arrays are 0-based; Lua uses 1-based "level".
     var index = level - 1;
     var index = level - 1;
     var nodes = scope.querySelectorAll(SERIES_SEL);
     var nodes = scope.querySelectorAll(SERIES_SEL);
Line 539: Line 212:
   }
   }


   function getCustomMin(slider) {
  function unhide(el) {
    if (!el) return;
    if (el.classList) el.classList.remove("sv-hidden");
    if (el.removeAttribute) el.removeAttribute("hidden");
    if (el.style && el.style.display === "none") el.style.display = "";
  }
 
  function hide(el) {
    if (!el) return;
    if (el.classList) el.classList.add("sv-hidden");
    if (el.setAttribute) el.setAttribute("hidden", "hidden");
  }
 
  /* ================================================================== */
  /* Custom slider helpers (bounds/percent)                              */
  /* ================================================================== */
 
   function getCustomMin(slider, fallback) {
     return clampInt(
     return clampInt(
       slider.getAttribute("aria-valuemin") || slider.getAttribute("data-min"),
       slider.getAttribute("aria-valuemin") || slider.getAttribute("data-min"),
       1,
       1,
       999,
       999,
       1
       fallback != null ? fallback : 1
     );
     );
   }
   }


   function getCustomMax(slider) {
   function getCustomMax(slider, fallback) {
     return clampInt(
     return clampInt(
       slider.getAttribute("aria-valuemax") || slider.getAttribute("data-max"),
       slider.getAttribute("aria-valuemax") || slider.getAttribute("data-max"),
       1,
       1,
       999,
       999,
       1
       fallback != null ? fallback : 1
     );
     );
   }
   }


   function getCustomValue(slider, fallback) {
  function getCustomBounds(slider, fallbackMin) {
    var min = getCustomMin(slider, fallbackMin != null ? fallbackMin : 1);
    var max = getCustomMax(slider, min);
    if (max < min) max = min;
    return { min: min, max: max };
  }
 
   function getCustomValue(slider, min, max, fallback) {
     return clampInt(
     return clampInt(
       slider.getAttribute("aria-valuenow") || slider.getAttribute("data-value"),
       slider.getAttribute("aria-valuenow") || slider.getAttribute("data-value"),
       1,
       min,
       999,
       max,
       fallback != null ? fallback : 1
       fallback != null ? fallback : min
     );
     );
  }
  function pctFromValue(min, max, value) {
    if (max === min) return 0;
    var pct = (value - min) / (max - min);
    if (pct < 0) pct = 0;
    if (pct > 1) pct = 1;
    return pct;
   }
   }


Line 573: Line 278:
   }
   }


   function updateCustomVisual(slider, value) {
  /* ================================================================== */
     var min = getCustomMin(slider);
  /* Custom slider value bubble                                          */
     var max = getCustomMax(slider);
  /* - JS owns structure + text/position only                            */
     if (max < min) max = min;
  /* - CSS owns visuals                                                  */
  /* ================================================================== */
 
   function ensureValueLabel(slider) {
    if (!slider || !slider.querySelector || !isCustomSlider(slider)) return null;
 
    var bubble = slider.querySelector(".sv-level-bubble");
    if (bubble) return bubble;
 
    bubble = document.createElement("span");
    bubble.className = "sv-level-bubble sv-hidden";
    bubble.setAttribute("hidden", "hidden");
    bubble.setAttribute("aria-hidden", "true");
 
    slider.appendChild(bubble);
    return bubble;
  }
 
  function setValueLabel(slider, value) {
     var bubble = ensureValueLabel(slider);
    if (!bubble) return;
 
     var b = getCustomBounds(slider, 1);
    bubble.textContent = String(value);
    bubble.style.left = pctFromValue(b.min, b.max, value) * 100 + "%";
  }
 
  function showValueLabel(slider) {
    if (!slider) return;
    var bubble = ensureValueLabel(slider);
     if (!bubble) return;
 
    if (slider._svBubbleTimer) {
      clearTimeout(slider._svBubbleTimer);
      slider._svBubbleTimer = null;
    }
 
    unhide(bubble);
  }
 
  function hideValueLabelSoon(slider, delayMs) {
    if (!slider) return;
    var bubble = slider.querySelector ? slider.querySelector(".sv-level-bubble") : null;
    if (!bubble) return;


     var pct = max === min ? 0 : (value - min) / (max - min);
     if (slider._svBubbleTimer) clearTimeout(slider._svBubbleTimer);
     if (pct < 0) pct = 0;
     slider._svBubbleTimer = setTimeout(function () {
     if (pct > 1) pct = 1;
      hide(bubble);
      slider._svBubbleTimer = null;
     }, delayMs != null ? delayMs : 450);
  }


     var left = pct * 100;
  function updateCustomVisual(slider, value) {
    var b = getCustomBounds(slider, 1);
     var left = pctFromValue(b.min, b.max, value) * 100;


     var thumb = slider.querySelector(".sv-level-thumb");
     var thumb = slider.querySelector(".sv-level-thumb");
Line 589: Line 342:
     if (thumb && thumb.style) thumb.style.left = left + "%";
     if (thumb && thumb.style) thumb.style.left = left + "%";
     if (fill && fill.style) fill.style.width = left + "%";
     if (fill && fill.style) fill.style.width = left + "%";
    setValueLabel(slider, value);
   }
   }


   function setCustomValue(slider, value) {
   function setCustomValue(slider, value, min, max) {
     slider.setAttribute("aria-valuenow", String(value));
     slider.setAttribute("aria-valuenow", String(value));
     slider.setAttribute("data-value", String(value));
     slider.setAttribute("data-value", String(value));
    // Helpful SR text (doesn't affect visuals)
    slider.setAttribute("aria-valuetext", "Level " + String(value) + " of " + String(max));
     updateCustomVisual(slider, value);
     updateCustomVisual(slider, value);
  }
  function snapToStep(raw, min, max, step) {
    if (step < 1) step = 1;
    var x = raw;
    if (x < min) x = min;
    if (x > max) x = max;
    var snapped = min + Math.round((x - min) / step) * step;
    if (snapped < min) snapped = min;
    if (snapped > max) snapped = max;
    return snapped;
   }
   }


   function valueFromClientX(slider, clientX) {
   function valueFromClientX(slider, clientX) {
     var rect = slider.getBoundingClientRect();
     var rect = slider.getBoundingClientRect();
     var min = getCustomMin(slider);
     var b = getCustomBounds(slider, 1);
    var max = getCustomMax(slider);
    if (max < min) max = min;


     var w = rect.width || 1;
     var w = rect.width || 1;
Line 608: Line 377:
     if (x > 1) x = 1;
     if (x > 1) x = 1;


     var raw = min + x * (max - min);
     var raw = b.min + x * (b.max - b.min);
     var rounded = Math.round(raw);
    return snapToStep(raw, b.min, b.max, getStep(slider));
     return clampInt(rounded, min, max, min);
  }
 
  /* ================================================================== */
  /* End-of-bar value label (right-side value)                          */
  /* ================================================================== */
 
  function ensureEndValueLabel(card, slider) {
     var wrap = null;
    if (slider) wrap = closest(slider, ".sv-level-slider");
    if (!wrap && card && card.querySelector) wrap = card.querySelector(".sv-level-slider");
    if (!wrap) return null;
 
    var out = wrap.querySelector(".sv-level-endvalue");
    if (out) return out;
 
    out = document.createElement("span");
    out.className = "sv-level-endvalue";
    out.setAttribute("aria-hidden", "true"); // slider already exposes aria-valuetext
    wrap.appendChild(out);
     return out;
  }
 
  function setEndValueLabel(card, slider, value) {
    var out = ensureEndValueLabel(card, slider);
    if (out) out.textContent = String(value);
   }
   }


   function unhide(el) {
  /* ================================================================== */
     if (!el) return;
  /* Core apply / init                                                    */
     if (el.classList) el.classList.remove("sv-hidden");
  /* ================================================================== */
     if (el.removeAttribute) el.removeAttribute("hidden");
 
    if (el.style && el.style.display === "none") el.style.display = "";
   function scheduleApply(card, rawLevel) {
     if (!card) return;
 
    card._svLevelNext = rawLevel;
     if (card._svLevelRaf) return;
 
     if (window.requestAnimationFrame) {
      card._svLevelRaf = requestAnimationFrame(function () {
        card._svLevelRaf = null;
        var v = card._svLevelNext;
        card._svLevelNext = null;
        applyLevelToCard(card, v);
      });
      return;
    }
 
    card._svLevelRaf = setTimeout(function () {
      card._svLevelRaf = null;
      var v = card._svLevelNext;
      card._svLevelNext = null;
      applyLevelToCard(card, v);
    }, 0);
   }
   }


Line 625: Line 439:
     var maxLevel = getCardMaxLevel(card);
     var maxLevel = getCardMaxLevel(card);
     var slider = getLevelSlider(card);
     var slider = getLevelSlider(card);
     var level = clampInt(rawLevel, 1, maxLevel, getCardLevel(card, maxLevel));
    var minLevel = getCardMinLevel(card, slider, maxLevel);
 
     var level = clampInt(rawLevel, minLevel, maxLevel, getCardLevel(card, minLevel, maxLevel));


     card.setAttribute("data-level", String(level));
     card.setAttribute("data-level", String(level));
     setLevelNumber(card, level);
     setLevelNumber(card, level);
    setLevelMax(card, maxLevel);


     if (slider) {
     if (slider) {
Line 636: Line 453:
     }
     }


     if (slider && maxLevel > 1) {
     if (slider) {
       if (isRangeInput(slider)) {
       if (isRangeInput(slider)) {
         slider.setAttribute("min", "1");
         slider.setAttribute("min", String(minLevel));
         slider.setAttribute("max", String(maxLevel));
         slider.setAttribute("max", String(maxLevel));
        slider.setAttribute("step", String(getStep(slider)));
        slider.setAttribute("aria-valuetext", "Level " + String(level) + " of " + String(maxLevel));
         if (String(slider.value) !== String(level)) slider.value = String(level);
         if (String(slider.value) !== String(level)) slider.value = String(level);
       } else if (isCustomSlider(slider)) {
       } else if (isCustomSlider(slider)) {
         setCustomBounds(slider, 1, maxLevel);
         setCustomBounds(slider, minLevel, maxLevel);
         setCustomValue(slider, level);
         setCustomValue(slider, level, minLevel, maxLevel);
       }
       }
    } else if (slider && isCustomSlider(slider)) {
      // Even if max==1, keep the visual consistent
      setCustomBounds(slider, 1, maxLevel);
      setCustomValue(slider, level);
     }
     }
    setEndValueLabel(card, slider, level);


     var boundary = getLevelBoundary(slider);
     var boundary = getLevelBoundary(slider);
Line 663: Line 480:
       var card = cards[i];
       var card = cards[i];


      // Versioned per-card init (fixes "old init stamp blocks new logic")
       var stamped = clampInt(card.getAttribute(LEVEL_INIT_ATTR), 0, 999, 0);
       var stamped = clampInt(card.getAttribute(LEVEL_INIT_ATTR), 0, 999, 0);
       if (stamped >= LEVELS_VERSION) continue;
       if (stamped >= LEVELS_VERSION) continue;
Line 669: Line 485:
       var maxLevel = getCardMaxLevel(card);
       var maxLevel = getCardMaxLevel(card);
       var slider = getLevelSlider(card);
       var slider = getLevelSlider(card);
      var minLevel = getCardMinLevel(card, slider, maxLevel);
      var start = getCardLevel(card, minLevel, maxLevel);


      var start = getCardLevel(card, maxLevel);
       if (slider && maxLevel > minLevel) {
       if (slider && maxLevel > 1) {
         if (isRangeInput(slider)) start = clampInt(slider.value, minLevel, maxLevel, start);
         if (isRangeInput(slider)) start = slider.value;
         else if (isCustomSlider(slider)) start = getCustomValue(slider, minLevel, maxLevel, start);
         else if (isCustomSlider(slider)) start = getCustomValue(slider, start);
       }
       }


Line 681: Line 499:
   }
   }


  /* ================================================================== */
  /* Events                                                              */
  /* ================================================================== */
  // Native range inputs
   document.addEventListener(
   document.addEventListener(
     "input",
     "input",
Line 691: Line 514:
       if (!card) return;
       if (!card) return;


       applyLevelToCard(card, t.value);
       scheduleApply(card, t.value);
     },
     },
     true
     true
   );
   );


  // Custom slider pointer interaction (click anywhere + drag)
   document.addEventListener(
   document.addEventListener(
     "pointerdown",
     "pointerdown",
     function (e) {
     function (e) {
      if (e.button != null && e.button !== 0) return; // primary only
       var slider =
       var slider =
         e.target && e.target.closest
         e.target && e.target.closest
Line 719: Line 545:
       }
       }


       applyLevelToCard(card, valueFromClientX(slider, e.clientX));
       showValueLabel(slider);
      scheduleApply(card, valueFromClientX(slider, e.clientX));
     },
     },
     true
     true
Line 730: Line 557:
       if (e.pointerId !== _drag.pointerId) return;
       if (e.pointerId !== _drag.pointerId) return;
       e.preventDefault();
       e.preventDefault();
       applyLevelToCard(_drag.card, valueFromClientX(_drag.slider, e.clientX));
 
       showValueLabel(_drag.slider);
      scheduleApply(_drag.card, valueFromClientX(_drag.slider, e.clientX));
     },
     },
     true
     true
Line 744: Line 573:
       } catch (err) {}
       } catch (err) {}
     }
     }
    hideValueLabelSoon(_drag.slider, 650);
     _drag = null;
     _drag = null;
   }
   }
Line 749: Line 580:
   document.addEventListener("pointerup", endDrag, true);
   document.addEventListener("pointerup", endDrag, true);
   document.addEventListener("pointercancel", endDrag, true);
   document.addEventListener("pointercancel", endDrag, true);
  // Mouse-only slider interaction:
  // - Remove keyboard-driven adjustments on custom sliders
  // - Block native <input type="range"> from changing via keyboard (if present)
  var _SV_BLOCK_KEYS = {
    ArrowLeft: 1,
    ArrowRight: 1,
    ArrowUp: 1,
    ArrowDown: 1,
    Left: 1,
    Right: 1,
    Up: 1,
    Down: 1,
    Home: 1,
    End: 1,
    PageUp: 1,
    PageDown: 1
  };


   document.addEventListener(
   document.addEventListener(
     "keydown",
     "keydown",
     function (e) {
     function (e) {
      var key = e && e.key;
      if (!_SV_BLOCK_KEYS[key]) return;
      var el = document.activeElement;
      if (!el) return;
      // Native range inputs (keep mouse drag, disable keyboard nudges)
      if (isRangeInput(el) && el.classList && el.classList.contains("sv-level-range")) {
        e.preventDefault();
        return;
      }
      // Custom sliders (we keep focus behaviors, but no keyboard changes)
      var slider = closest(el, ".sv-level-range--custom, .sv-level-range[data-sv-slider='1']");
      if (slider) {
        e.preventDefault();
        return;
      }
    },
    true
  );
  // Show bubble on focus (discoverability), hide on blur
  document.addEventListener(
    "focusin",
    function () {
       var el = document.activeElement;
       var el = document.activeElement;
       if (!el || !el.closest) return;
       if (!el || !el.closest) return;
       var slider = el.closest(".sv-level-range--custom, .sv-level-range[data-sv-slider='1']");
       var slider = el.closest(".sv-level-range--custom, .sv-level-range[data-sv-slider='1']");
       if (!slider) return;
       if (!slider) return;


       var card = closest(slider, CARD_SEL);
       var b = getCustomBounds(slider, 1);
       if (!card) return;
       var v = getCustomValue(slider, b.min, b.max, b.min);


       var min = getCustomMin(slider);
       setValueLabel(slider, v);
       var max = getCustomMax(slider);
       showValueLabel(slider);
      if (max < min) max = min;
    },
    true
  );


       var cur = getCustomValue(slider, getCardLevel(card, max));
  document.addEventListener(
       var next = cur;
    "focusout",
    function (e) {
       var t = e.target;
      if (!t || !t.closest) return;
      var slider = t.closest(".sv-level-range--custom, .sv-level-range[data-sv-slider='1']");
       if (!slider) return;


       if (e.key === "ArrowLeft" || e.key === "Left" || e.key === "ArrowDown") {
       hideValueLabelSoon(slider, 300);
        next = cur - 1;
      } else if (
        e.key === "ArrowRight" ||
        e.key === "Right" ||
        e.key === "ArrowUp"
      ) {
        next = cur + 1;
      } else if (e.key === "Home") {
        next = min;
      } else if (e.key === "End") {
        next = max;
      } else if (e.key === "PageDown") {
        next = cur - 5;
      } else if (e.key === "PageUp") {
        next = cur + 5;
      } else {
        return;
      }
 
      e.preventDefault();
      next = clampInt(next, min, max, cur);
      applyLevelToCard(card, next);
     },
     },
     true
     true
Line 800: Line 659:
   }
   }


  // Expose for debugging / manual re-init if needed
   COMMON.initAllLevels = initAllLevels;
   COMMON.initAllLevels = initAllLevels;


Line 820: Line 678:
(function () {
(function () {
   /* ================================================================== */
   /* ================================================================== */
   /* MODULE: Popups                                                     */
   /* MODULE: Universal Popups                                           */
  /*  - Emulates Definitions tooltip behavior (hover + pin)            */
  /*  - Content source is the page DOM (notes/users/requirements)      */
  /*  - Legacy <details> remains as no-JS fallback                      */
   /* ================================================================== */
   /* ================================================================== */


   var SV = (window.SV = window.SV || {});
   var SV = (window.SV = window.SV || {});
   var COMMON = (SV.common = SV.common || {});
   var COMMON = (SV.common = SV.common || {});
  if (COMMON.popupsInit) return;
  COMMON.popupsInit = 1;


   var CARD_SEL = ".sv-gi-card, .sv-skill-card, .sv-passive-card";
  // Bumped only because we changed positioning margins (safe cache-bust).
   var UPOP_VERSION = 32; // Universal Popups v3.2
  if (typeof COMMON.universalPopupsInit === "number" && COMMON.universalPopupsInit >= UPOP_VERSION) return;
  COMMON.universalPopupsInit = UPOP_VERSION;
 
  try { document.documentElement.classList.add("sv-uipop-v3"); } catch (e) {}


   var POPUP_BTN_SEL =
   var DEF_SEL = ".sv-def";
    ".sv-tip-btn[data-sv-toggle='1'], .sv-disclose-btn[data-sv-toggle='1']";
  var DEF_TIP_ATTR = "data-sv-def-tip";
  var DEF_LINK_ATTR = "data-sv-def-link";


   var POPUP_DETAILS_SEL = "details.sv-tip, details.sv-disclose";
   var TIP_BTN_SEL = ".sv-tip-btn[data-sv-toggle='1'], details.sv-tip > summary";
   var POPUP_SUMMARY_SEL = "details.sv-tip > summary, details.sv-disclose > summary";
   var DISCLOSE_BTN_SEL = ".sv-disclose-btn[data-sv-toggle='1'], details.sv-disclose > summary";


   var POPUP_WRAP_SEL = ".sv-tip, .sv-disclose";
   var WRAP_SEL = ".sv-tip, .sv-disclose";
   var POPUP_POP_SEL = ".sv-tip-pop, .sv-disclose-pop";
   var POP_SEL = ".sv-tip-pop, .sv-disclose-pop";
  var POPUP_BODY_SEL = ".sv-tip-pop-body, .sv-disclose-pop-body";


   var HOVER_CAPABLE = false;
   var HOVER_CAPABLE = false;
   try {
   try {
     HOVER_CAPABLE =
     HOVER_CAPABLE = !!window.matchMedia && window.matchMedia("(hover:hover) and (pointer:fine)").matches;
      !!window.matchMedia &&
   } catch (e) { HOVER_CAPABLE = false; }
      window.matchMedia("(hover:hover) and (pointer:fine)").matches;
   } catch (e) {
    HOVER_CAPABLE = false;
  }


   var popEl = null;
   var popEl = null;


   var state = {
   var state = {
     visible: false,
     open: false,
     pinned: false,
     pinned: false,
     anchor: null,
     trigger: null,
     lastX: 0,
     hideTimer: 0,
     lastY: 0,
     anchorKind: "trigger", // trigger | finger | center
     suppressClickUntil: 0,
    anchorX: 0,
     anchorY: 0
   };
   };


Line 879: Line 734:
   }
   }


   function shouldSuppressClick() {
   function safeIdSelector(id) {
     return Date.now() < state.suppressClickUntil;
     return "#" + String(id).replace(/([ !"#$%&'()*+,.\/:;<=>?@[\\\]^`{|}~])/g, "\\$1");
   }
   }


   function sameAnchor(el) {
   function isInsidePopup(target) {
     return !!(el && state.anchor && el === state.anchor);
     return !!(popEl && (target === popEl || (popEl.contains && popEl.contains(target))));
   }
   }


   function setExpanded(el, expanded) {
   function isSummaryTrigger(trigger) {
     if (!el || !el.setAttribute) return;
     return !!(trigger && trigger.tagName === "SUMMARY");
    el.setAttribute("aria-expanded", expanded ? "true" : "false");
   }
   }


Line 896: Line 750:


     var el = document.createElement("div");
     var el = document.createElement("div");
     el.className = "sv-pop-tip sv-hidden";
     el.className = "sv-uipop sv-uipop--sm";
     el.setAttribute("role", "dialog");
     el.setAttribute("role", "dialog");
     el.setAttribute("aria-hidden", "true");
     el.setAttribute("aria-hidden", "true");
    document.body.appendChild(el);
    el.addEventListener("mouseenter", function () {
      if (!HOVER_CAPABLE) return;
      if (!state.open || state.pinned) return;
      clearTimeout(state.hideTimer);
      state.hideTimer = 0;
    });


     el.style.position = "fixed";
     el.addEventListener("mouseleave", function () {
    el.style.zIndex = "99998";
      if (!HOVER_CAPABLE) return;
    el.style.maxWidth = "420px";
      if (!state.open || state.pinned) return;
    el.style.boxSizing = "border-box";
      clearTimeout(state.hideTimer);
    el.style.padding = "10px 12px";
      state.hideTimer = setTimeout(function () {
    el.style.borderRadius = "12px";
        if (state.open && !state.pinned) hidePopup();
    el.style.background = "rgba(10, 14, 22, 0.92)";
      }, 80);
     el.style.color = "#fff";
     });
    el.style.fontSize = "14px";
    el.style.lineHeight = "1.25";
    el.style.pointerEvents = "none";
    el.style.display = "none";


    document.body.appendChild(el);
     popEl = el;
     popEl = el;
     return popEl;
     return popEl;
Line 920: Line 777:
   function setHidden(el, hidden) {
   function setHidden(el, hidden) {
     if (!el) return;
     if (!el) return;
     if (hidden) {
    el.setAttribute("aria-hidden", hidden ? "true" : "false");
      el.classList.add("sv-hidden");
  }
      el.setAttribute("aria-hidden", "true");
 
      el.style.display = "none";
  function stripIds(root) {
     } else {
    if (!root || root.nodeType !== 1) return;
      el.classList.remove("sv-hidden");
     if (root.hasAttribute && root.hasAttribute("id")) root.removeAttribute("id");
      el.setAttribute("aria-hidden", "false");
    var nodes = root.querySelectorAll ? root.querySelectorAll("[id]") : [];
      el.style.display = "block";
    for (var i = 0; i < nodes.length; i++) nodes[i].removeAttribute("id");
     }
  }
 
  function unhideTree(root) {
    if (!root || root.nodeType !== 1) return;
 
    if (root.hasAttribute && root.hasAttribute("hidden")) root.removeAttribute("hidden");
    if (root.classList) root.classList.remove("sv-hidden");
    if (root.style && root.style.display === "none") root.style.display = "";
 
     var kids = root.children || [];
    for (var i = 0; i < kids.length; i++) unhideTree(kids[i]);
  }
 
  function isBlankContent(node) {
    if (!node) return true;
    var t = node.textContent;
    if (t == null) return true;
    var compact = String(t).replace(/\s+/g, "").replace(/[—–-]+/g, "").trim();
    return !compact;
  }
 
  function getDefTipText(defEl) {
    if (!defEl || !defEl.getAttribute) return "";
    var t = defEl.getAttribute(DEF_TIP_ATTR);
    if (t == null) return "";
    t = String(t);
    t = t.replace(/\\r\\n/g, "\n").replace(/\\n/g, "\n").replace(/\\r/g, "\n");
    if (!t.replace(/\s+/g, "")) return "";
     return t;
   }
   }


   function safeIdSelector(id) {
   function getDefLinkTitle(defEl) {
     return "#" + String(id).replace(/([ !"#$%&'()*+,.\/:;<=>?@[\\\]^`{|}~])/g, "\\$1");
     if (!defEl || !defEl.getAttribute) return "";
    var t = defEl.getAttribute(DEF_LINK_ATTR);
    if (t == null) return "";
    return String(t).trim();
   }
   }


Line 939: Line 827:


     if (trigger.tagName === "SUMMARY") {
     if (trigger.tagName === "SUMMARY") {
       var det = closest(trigger, POPUP_DETAILS_SEL);
       var det = closest(trigger, "details.sv-tip, details.sv-disclose");
       if (!det) return null;
       if (!det) return null;
 
       var pop0 = det.querySelector(POP_SEL);
       var pop = det.querySelector(POPUP_POP_SEL);
       if (!pop0) return null;
       if (!pop) return null;
      return (
 
        pop0.querySelector(".sv-tip-pop-body") ||
      var body = pop.querySelector(POPUP_BODY_SEL);
        pop0.querySelector(".sv-disclose-pop-body") ||
       return body || pop;
        pop0.querySelector(".sv-disclose-list") ||
        pop0
       );
     }
     }


     var id = trigger.getAttribute ? trigger.getAttribute("aria-controls") : "";
     var id = trigger.getAttribute ? trigger.getAttribute("aria-controls") : "";
     if (id) {
     if (id) {
       var wrap = closest(trigger, POPUP_WRAP_SEL);
       var wrap = closest(trigger, WRAP_SEL);
       if (wrap) {
       if (wrap) {
         try {
         try {
           var local = wrap.querySelector(safeIdSelector(id));
           var local = wrap.querySelector(safeIdSelector(id));
           if (local) {
           if (local) {
             var b0 = local.querySelector ? local.querySelector(POPUP_BODY_SEL) : null;
             return (
            return b0 || local;
              local.querySelector(".sv-tip-pop-body") ||
              local.querySelector(".sv-disclose-pop-body") ||
              local.querySelector(".sv-disclose-list") ||
              local
            );
           }
           }
         } catch (e) {}
         } catch (e) {}
Line 964: Line 858:
       var global = document.getElementById(id);
       var global = document.getElementById(id);
       if (global) {
       if (global) {
         var b1 = global.querySelector ? global.querySelector(POPUP_BODY_SEL) : null;
         return (
        return b1 || global;
          global.querySelector(".sv-tip-pop-body") ||
          global.querySelector(".sv-disclose-pop-body") ||
          global.querySelector(".sv-disclose-list") ||
          global
        );
       }
       }
     }
     }


     var w = closest(trigger, POPUP_WRAP_SEL);
     var w = closest(trigger, WRAP_SEL);
     if (w) {
     if (w) {
       var p = w.querySelector(POPUP_POP_SEL);
       var pop = w.querySelector(POP_SEL);
       if (p) {
       if (pop) {
         var b2 = p.querySelector(POPUP_BODY_SEL);
         return (
        return b2 || p;
          pop.querySelector(".sv-tip-pop-body") ||
          pop.querySelector(".sv-disclose-pop-body") ||
          pop.querySelector(".sv-disclose-list") ||
          pop
        );
       }
       }
     }
     }
Line 981: Line 883:
   }
   }


   function isBlankContent(node) {
   function guessTitle(trigger, sourceNode) {
     if (!node) return true;
     var pop = null;
    if (sourceNode && sourceNode.nodeType === 1) pop = closest(sourceNode, POP_SEL);
    if (pop) {
      var t = pop.querySelector(".sv-tip-pop-title") || pop.querySelector(".sv-disclose-pop-title");
      if (t && t.textContent) return String(t.textContent).trim();
    }


     var t = node.textContent;
     var aria = trigger && trigger.getAttribute ? trigger.getAttribute("aria-label") : "";
     if (t == null) return true;
     if (aria) return String(aria).trim();


     var compact = String(t)
     var txt = trigger && trigger.textContent ? String(trigger.textContent).trim() : "";
      .replace(/\s+/g, "")
    if (txt) return txt.replace(/\s+/g, " ").trim();
      .replace(/[—–-]+/g, "")
      .trim();


     return !compact;
     return "Info";
   }
   }


   function stripIds(root) {
   function buildHeaderLink(linkTitle, displayText) {
     if (!root || root.nodeType !== 1) return;
     if (!linkTitle) return null;
     if (root.hasAttribute && root.hasAttribute("id")) root.removeAttribute("id");
 
     var nodes = root.querySelectorAll ? root.querySelectorAll("[id]") : [];
     var a = document.createElement("a");
     for (var i = 0; i < nodes.length; i++) nodes[i].removeAttribute("id");
     a.className = "sv-uipop-title-link";
  }
     a.textContent = displayText || String(linkTitle);


  function unhideTree(root) {
    if (window.mw && mw.util && typeof mw.util.getUrl === "function") a.href = mw.util.getUrl(linkTitle);
     if (!root || root.nodeType !== 1) return;
     else a.href = String(linkTitle);


     if (root.hasAttribute && root.hasAttribute("hidden")) root.removeAttribute("hidden");
     a.addEventListener("click", function (e) { e.stopPropagation(); });
    if (root.classList) root.classList.remove("sv-hidden");


     if (root.style && root.style.display === "none") root.style.display = "";
     return a;
  }


     var kids = root.children || [];
  function fireContentHook(el) {
     for (var i = 0; i < kids.length; i++) unhideTree(kids[i]);
     if (!el) return;
     if (window.mw && mw.hook && window.jQuery) {
      try { mw.hook("wikipage.content").fire(window.jQuery(el)); } catch (e) {}
    }
   }
   }


   function setPopContentFromNode(sourceNode) {
   function renderPopup(opts) {
     var el = ensurePopEl();
     var el = ensurePopEl();
     if (!el) return;
     if (!el) return;
    el.classList.remove("sv-uipop--sm", "sv-uipop--lg");
    el.classList.add(opts.size === "lg" ? "sv-uipop--lg" : "sv-uipop--sm");


     while (el.firstChild) el.removeChild(el.firstChild);
     while (el.firstChild) el.removeChild(el.firstChild);


     var clone = sourceNode.cloneNode(true);
     var head = document.createElement("div");
     stripIds(clone);
     head.className = "sv-uipop-head" + (opts.pinned ? " sv-uipop-head--clickable" : "");
     unhideTree(clone);
 
    var title = document.createElement("div");
    title.className = "sv-uipop-title";
    if (opts.titleLink) title.appendChild(opts.titleLink);
    else title.textContent = opts.title || "Info";
 
    head.appendChild(title);
     el.appendChild(head);


     var isBody =
     var body = document.createElement("div");
      clone.classList &&
    body.className = "sv-uipop-body";
      (clone.classList.contains("sv-tip-pop-body") ||
    if (opts.bodyPre) body.classList.add("sv-uipop-body--pre");
        clone.classList.contains("sv-disclose-pop-body"));


     if (isBody) {
     if (opts.bodyNode) {
       while (clone.firstChild) el.appendChild(clone.firstChild);
       var clone = opts.bodyNode.cloneNode(true);
     } else {
      stripIds(clone);
       el.appendChild(clone);
      unhideTree(clone);
      body.appendChild(clone);
     } else if (opts.bodyText != null) {
       body.textContent = String(opts.bodyText);
     }
     }
    el.appendChild(body);
    if (opts.pinned) {
      head.addEventListener(
        "click",
        function (e) {
          e.preventDefault();
          hidePopup();
        },
        { once: true }
      );
    }
    fireContentHook(el);
   }
   }


   function positionPopAt(clientX, clientY) {
   function positionPopup(trigger) {
     var el = ensurePopEl();
     var el = ensurePopEl();
     if (!el) return;
     if (!el) return;
    var vw = window.innerWidth || document.documentElement.clientWidth || 0;
    var vh = window.innerHeight || document.documentElement.clientHeight || 0;
    var gapY = state.pinned ? 10 : 16;
    var gapX = 12;


     el.style.left = "0px";
     el.style.left = "0px";
     el.style.top = "0px";
     el.style.top = "0px";
    setHidden(el, false);


     requestAnimationFrame(function () {
     requestAnimationFrame(function () {
       if (!state.visible) return;
       if (!state.open) return;
 
      var vw = window.innerWidth || document.documentElement.clientWidth || 0;
      var vh = window.innerHeight || document.documentElement.clientHeight || 0;
 
      var tight = vw <= 520;
      var margin = tight ? 18 : 10;
      var gap = tight ? 14 : 12;
 
      var pr = el.getBoundingClientRect();
      var w = pr.width || 240;
      var h = pr.height || 180;


       var r = el.getBoundingClientRect();
       var left = margin;
       var w = r.width || 260;
       var top = margin;
      var h = r.height || 80;


       var left = clientX - w / 2;
       if (state.anchorKind === "center") {
       var top = clientY - h - gapY;
        left = (vw - w) / 2;
        top = (vh - h) / 2;
       } else if (state.anchorKind === "finger") {
        left = (vw - w) / 2;
        var y = state.anchorY || vh / 2;
        top = y - h - gap;
        if (top < margin) top = y + gap;
      } else {
        if (!trigger || !trigger.getBoundingClientRect) return;
        var tr = trigger.getBoundingClientRect();
        var isIconish = (tr.width || 0) <= 40;
        left = isIconish ? tr.right - w : tr.left;
        top = tr.bottom + gap;
        left = clamp(left, margin, Math.max(margin, vw - w - margin));
        if (top + h + margin > vh) top = tr.top - h - gap;
      }


       left = clamp(left, gapX, Math.max(gapX, vw - w - gapX));
       left = clamp(left, margin, Math.max(margin, vw - w - margin));
      if (top < gapY) top = clientY + gapY;
       top = clamp(top, margin, Math.max(margin, vh - h - margin));
       top = clamp(top, gapY, Math.max(gapY, vh - h - gapY));


       el.style.left = Math.round(left) + "px";
       el.style.left = Math.round(left) + "px";
Line 1,068: Line 1,022:
   }
   }


   function hidePop() {
   function setExpanded(trigger, expanded) {
     if (!popEl) return;
     if (!trigger || !trigger.setAttribute) return;
    trigger.setAttribute("aria-expanded", expanded ? "true" : "false");
  }


     setExpanded(state.anchor, false);
  function hidePopup() {
    if (!state.open) return;
 
     clearTimeout(state.hideTimer);
    state.hideTimer = 0;


     if (state.anchor && state.anchor.tagName === "SUMMARY") {
     setExpanded(state.trigger, false);
      var det = closest(state.anchor, POPUP_DETAILS_SEL);
      if (det && det.open) det.open = false;
    }


     state.visible = false;
     state.open = false;
     state.pinned = false;
     state.pinned = false;
     state.anchor = null;
     state.trigger = null;
    state.anchorKind = "trigger";


     setHidden(popEl, true);
     setHidden(ensurePopEl(), true);
   }
   }


   function showPop(trigger, sourceNode, clientX, clientY, pinned) {
   function openPopup(trigger, opts) {
    var el = ensurePopEl();
     if (!trigger) return;
     if (!el) return;


     if (state.visible && state.anchor && state.anchor !== trigger) hidePop();
     if (state.open && state.trigger && state.trigger !== trigger) hidePopup();


     state.visible = true;
     state.open = true;
     state.pinned = !!pinned;
     state.pinned = !!opts.pinned;
     state.anchor = trigger || null;
     state.trigger = trigger;
    state.lastX = clientX || 0;
    state.lastY = clientY || 0;


     setPopContentFromNode(sourceNode);
     setExpanded(trigger, true);


     el.style.pointerEvents = state.pinned ? "auto" : "none";
     renderPopup(opts);
     el.classList.toggle("sv-pop-tip--pinned", state.pinned);
     setHidden(popEl, false);
    positionPopup(trigger);
  }


     setExpanded(trigger, true);
  function findTrigger(t) {
     if (!t) return null;
    if (isInsidePopup(t)) return null;


     if (trigger && trigger.tagName === "SUMMARY") {
     var defEl = closest(t, DEF_SEL);
      var det = closest(trigger, POPUP_DETAILS_SEL);
    if (defEl && defEl.getAttribute && defEl.getAttribute(DEF_TIP_ATTR) != null) return defEl;
      if (det && det.open) det.open = false;
    }


     setHidden(el, false);
     var tip = closest(t, TIP_BTN_SEL);
     positionPopAt(state.lastX, state.lastY);
     if (tip) return tip;
  }


  function findTriggerFromEventTarget(t) {
    var dis = closest(t, DISCLOSE_BTN_SEL);
     if (!t) return null;
     if (dis) return dis;
    return closest(t, POPUP_BTN_SEL + ", " + POPUP_SUMMARY_SEL);
  }


  function isTouchLikeEvent(e) {
     return null;
     return !!(e && e.pointerType && e.pointerType !== "mouse");
   }
   }


   if (HOVER_CAPABLE) {
   function resolveOpts(trigger) {
     document.addEventListener(
     var isDef = trigger && trigger.matches && trigger.matches(DEF_SEL);
      "mouseover",
    var isTip = trigger && trigger.matches && trigger.matches(TIP_BTN_SEL);
      function (e) {
    var isDisclose = trigger && trigger.matches && trigger.matches(DISCLOSE_BTN_SEL);
        if (state.pinned) return;


        var trigger = findTriggerFromEventTarget(e.target);
    var mode = "hover";
        if (!trigger) {
    var size = "sm";
          if (state.visible && !state.pinned) hidePop();
          return;
        }


        var rel = e.relatedTarget;
    var attrMode = trigger.getAttribute && trigger.getAttribute("data-sv-pop");
        if (rel && trigger.contains(rel)) return;
    if (attrMode === "hover" || attrMode === "click") mode = attrMode;


        var source = getContentNodeFromTrigger(trigger);
    var attrSize = trigger.getAttribute && trigger.getAttribute("data-sv-pop-size");
        if (isBlankContent(source)) {
    if (attrSize === "sm" || attrSize === "lg") size = attrSize;
          if (state.visible && !state.pinned) hidePop();
          return;
        }


        showPop(trigger, source, e.clientX, e.clientY, false);
    if (!attrMode) {
      },
      if (isDisclose) mode = "click";
      true
      else if (isTip) mode = "hover";
     );
      else if (isDef) mode = getDefLinkTitle(trigger) ? "click" : "hover";
    }
 
     if (!attrSize) size = mode === "click" ? "lg" : "sm";


     document.addEventListener(
     if (!HOVER_CAPABLE && mode === "hover") {
       "mousemove",
       mode = "click";
       function (e) {
       size = "sm";
        if (!state.visible || state.pinned) return;
    }


        var trigger = findTriggerFromEventTarget(e.target);
    if (isDef) {
        if (!trigger || !sameAnchor(trigger)) return;
      var tipText = getDefTipText(trigger);
      if (!tipText) return null;


        state.lastX = e.clientX;
      var linkTitle = getDefLinkTitle(trigger);
        state.lastY = e.clientY;
        positionPopAt(state.lastX, state.lastY);
      },
      true
    );


    document.addEventListener(
      var label = trigger.querySelector ? trigger.querySelector(".sv-def-text") : null;
       "mouseout",
       var titleText = label ? String(label.textContent || "").trim() : String(trigger.textContent || "").trim();
      function (e) {
      if (!titleText) titleText = "Definition";
        if (!state.visible || state.pinned) return;


         var trigger = findTriggerFromEventTarget(e.target);
      return {
         if (!trigger || !sameAnchor(trigger)) return;
         pinned: mode === "click",
        size: size,
        title: titleText,
        titleLink: linkTitle ? buildHeaderLink(linkTitle, titleText) : null,
        bodyText: tipText,
        bodyPre: true,
         bodyNode: null
      };
    }


        var rel = e.relatedTarget;
    var source = getContentNodeFromTrigger(trigger);
        if (rel && trigger.contains(rel)) return;
    if (isBlankContent(source)) return null;


        hidePop();
    return {
       },
      pinned: mode === "click",
       true
      size: size,
     );
      title: guessTitle(trigger, source),
      titleLink: null,
      bodyNode: source,
       bodyPre: false,
       bodyText: null
     };
   }
   }


   document.addEventListener(
   window.addEventListener(
     "pointerdown",
     "pointerdown",
     function (e) {
     function (e) {
       var trigger = findTriggerFromEventTarget(e.target);
      if (!e) return;
 
      if (isInsidePopup(e.target)) return;
 
       var trigger = findTrigger(e.target);
       if (!trigger) return;
       if (!trigger) return;


       var source = getContentNodeFromTrigger(trigger);
       var opts = resolveOpts(trigger);
       if (isBlankContent(source)) {
       if (!opts) return;
        if (state.visible) hidePop();
 
      if (isSummaryTrigger(trigger)) e.preventDefault();
 
      state.anchorKind = !HOVER_CAPABLE || (e.pointerType && e.pointerType !== "mouse") ? "finger" : "trigger";
      state.anchorX = typeof e.clientX === "number" ? e.clientX : 0;
      state.anchorY = typeof e.clientY === "number" ? e.clientY : 0;
 
      if (HOVER_CAPABLE && !opts.pinned && (e.pointerType === "mouse" || !e.pointerType)) {
        e.stopPropagation();
         return;
         return;
       }
       }


       if (!isTouchLikeEvent(e)) return;
       e.preventDefault();
      e.stopPropagation();


       e.preventDefault();
       if (state.open && state.pinned && state.trigger === trigger) {
        hidePopup();
        return;
      }


       state.suppressClickUntil = Date.now() + 450;
       openPopup(trigger, opts);
      showPop(trigger, source, e.clientX, e.clientY, true);
     },
     },
     true
     true
   );
   );


   document.addEventListener(
   window.addEventListener(
     "click",
     "mouseover",
     function (e) {
     function (e) {
       if (shouldSuppressClick()) return;
       if (!HOVER_CAPABLE) return;
      if (isInsidePopup(e.target)) return;


       var trigger = findTriggerFromEventTarget(e.target);
       var trigger = findTrigger(e.target);
       if (!trigger) return;
       if (!trigger) {
 
         if (state.open && !state.pinned) hidePopup();
      var source = getContentNodeFromTrigger(trigger);
      if (isBlankContent(source)) {
         if (state.visible) hidePop();
         return;
         return;
       }
       }


       e.preventDefault();
      var opts = resolveOpts(trigger);
      if (!opts || opts.pinned) return;
 
      state.anchorKind = "trigger";
 
       e.stopPropagation();
      clearTimeout(state.hideTimer);
      state.hideTimer = 0;


       if (state.pinned && sameAnchor(trigger)) hidePop();
       openPopup(trigger, opts);
      else showPop(trigger, source, e.clientX, e.clientY, true);
     },
     },
     true
     true
   );
   );


   document.addEventListener(
   window.addEventListener(
     "keydown",
     "mouseout",
     function (e) {
     function (e) {
       if (!(e.key === "Enter" || e.key === " " || e.key === "Spacebar")) return;
       if (!HOVER_CAPABLE) return;
      if (!state.open || state.pinned) return;


      var active = document.activeElement;
       var trigger = findTrigger(e.target);
       var trigger = findTriggerFromEventTarget(active);
       if (!trigger || trigger !== state.trigger) return;
       if (!trigger) return;


      var source = getContentNodeFromTrigger(trigger);
       e.stopPropagation();
      if (isBlankContent(source)) {
        if (state.visible) hidePop();
        return;
      }
 
       e.preventDefault();


       if (state.pinned && sameAnchor(trigger)) hidePop();
       clearTimeout(state.hideTimer);
       else showPop(trigger, source, state.lastX || 0, state.lastY || 0, true);
      state.hideTimer = setTimeout(function () {
        if (state.open && !state.pinned) hidePopup();
       }, 80);
     },
     },
     true
     true
   );
   );


   document.addEventListener(
   window.addEventListener(
     "click",
     "click",
     function (e) {
     function (e) {
       if (!state.visible || !state.pinned) return;
       if (!state.open) return;
      if (isInsidePopup(e.target)) return;


       if (popEl && (e.target === popEl || (popEl.contains && popEl.contains(e.target))))
       var trigger = findTrigger(e.target);
      if (trigger) {
        e.stopPropagation();
         return;
         return;
      }


       if (findTriggerFromEventTarget(e.target)) return;
       if (state.pinned || !HOVER_CAPABLE) {
 
        hidePopup();
      hidePop();
        e.stopPropagation();
      }
     },
     },
     true
     true
   );
   );


   document.addEventListener(
   window.addEventListener(
     "keydown",
     "keydown",
     function (e) {
     function (e) {
       if (e.key !== "Escape") return;
       if (e.key !== "Escape") return;
       if (state.visible) hidePop();
       if (state.open) {
        hidePopup();
        e.stopPropagation();
      }
     },
     },
     true
     true
Line 1,274: Line 1,254:
     "scroll",
     "scroll",
     function () {
     function () {
       if (!state.visible) return;
       if (!state.open || !state.trigger) return;
       positionPopAt(state.lastX, state.lastY);
       positionPopup(state.trigger);
     },
     },
     true
     true
Line 1,283: Line 1,263:
     "resize",
     "resize",
     function () {
     function () {
       if (!state.visible) return;
       if (!state.open || !state.trigger) return;
       positionPopAt(state.lastX, state.lastY);
       positionPopup(state.trigger);
     },
     },
     true
     true
   );
   );


   function hideTrigger(el) {
   function stripDefTitles(root) {
    if (!el) return;
    if (el.classList) el.classList.add("sv-hidden");
    el.setAttribute("hidden", "hidden");
    el.setAttribute("aria-hidden", "true");
  }
 
  function initAllPopups(root) {
     var container = root || document;
     var container = root || document;
 
     var defs = container.querySelectorAll(DEF_SEL + "[title]");
     var btns = container.querySelectorAll(POPUP_BTN_SEL);
     for (var i = 0; i < defs.length; i++) defs[i].removeAttribute("title");
     for (var i = 0; i < btns.length; i++) {
      var b = btns[i];
      var node = getContentNodeFromTrigger(b);
      if (isBlankContent(node)) hideTrigger(b);
      else setExpanded(b, false);
    }
 
    var ds = container.querySelectorAll(POPUP_DETAILS_SEL);
    for (var j = 0; j < ds.length; j++) {
      var d = ds[j];
      var s = d.querySelector("summary");
      var pop = d.querySelector(POPUP_POP_SEL);
      var body = pop && pop.querySelector ? pop.querySelector(POPUP_BODY_SEL) : null;
      var node2 = body || pop;
 
      if (isBlankContent(node2)) {
        hideTrigger(d);
      } else {
        if (d.open) d.open = false;
        if (s) setExpanded(s, false);
      }
    }
   }
   }


   if (window.mw && mw.hook) {
   if (window.mw && mw.hook) {
     mw.hook("wikipage.content").add(function ($content) {
     mw.hook("wikipage.content").add(function ($content) {
       initAllPopups($content && $content[0]);
       stripDefTitles($content && $content[0]);
     });
     });
   }
   }
Line 1,332: Line 1,283:
   if (document.readyState === "loading") {
   if (document.readyState === "loading") {
     document.addEventListener("DOMContentLoaded", function () {
     document.addEventListener("DOMContentLoaded", function () {
       initAllPopups(document);
       stripDefTitles(document);
     });
     });
   } else {
   } else {
     initAllPopups(document);
     stripDefTitles(document);
   }
   }
})();
})();
Line 1,571: Line 1,522:
   } else {
   } else {
     initAllTabs(document);
     initAllTabs(document);
  }
})();
(function () {
  /* ================================================================== */
  /* MODULE: Universal Popups (v2) — refined                            */
  /*  - Removes redundant hint for click-open (large) popups            */
  /*  - Leaves hover hints optional (but CSS currently hides legacy)    */
  /*  - Keeps links inside popups fully interactive (Popups friendly)  */
  /* ================================================================== */
  var SV = (window.SV = window.SV || {});
  var COMMON = (SV.common = SV.common || {});
  var POPV2_VERSION = 2; // bumped due to header/hint changes
  if (typeof COMMON.popupsV2Init === "number" && COMMON.popupsV2Init >= POPV2_VERSION) return;
  COMMON.popupsV2Init = POPV2_VERSION;
  try { document.documentElement.classList.add("sv-uipop-v2"); } catch (e) {}
  var DEF_SEL = ".sv-def";
  var DEF_TIP_ATTR = "data-sv-def-tip";
  var DEF_LINK_ATTR = "data-sv-def-link";
  var TIP_BTN_SEL = ".sv-tip-btn[data-sv-toggle='1'], details.sv-tip > summary";
  var DISCLOSE_BTN_SEL = ".sv-disclose-btn[data-sv-toggle='1'], details.sv-disclose > summary";
  var POPUP_DETAILS_SEL = "details.sv-tip, details.sv-disclose";
  var POPUP_WRAP_SEL = ".sv-tip, .sv-disclose";
  var POPUP_POP_SEL = ".sv-tip-pop, .sv-disclose-pop";
  var HOVER_CAPABLE = false;
  try {
    HOVER_CAPABLE = !!window.matchMedia && window.matchMedia("(hover:hover) and (pointer:fine)").matches;
  } catch (e) {
    HOVER_CAPABLE = false;
  }
  var popEl = null;
  var state = {
    open: false,
    pinned: false,
    mode: "hover",      // hover | click
    size: "sm",        // sm | lg
    trigger: null,
    lastPointerType: "mouse",
    hideTimer: 0
  };
  function closest(el, sel) {
    if (!el || el.nodeType !== 1) return null;
    if (el.closest) return el.closest(sel);
    while (el && el.nodeType === 1) {
      if (el.matches && el.matches(sel)) return el;
      el = el.parentElement;
    }
    return null;
  }
  function clamp(n, min, max) {
    if (n < min) return min;
    if (n > max) return max;
    return n;
  }
  function safeIdSelector(id) {
    return "#" + String(id).replace(/([ !"#$%&'()*+,.\/:;<=>?@[\\\]^`{|}~])/g, "\\$1");
  }
  function ensurePopEl() {
    if (popEl) return popEl;
    var el = document.createElement("div");
    el.className = "sv-uipop sv-uipop--sm";
    el.setAttribute("role", "dialog");
    el.setAttribute("aria-hidden", "true");
    document.body.appendChild(el);
    popEl = el;
    return popEl;
  }
  function setHidden(el, hidden) {
    if (!el) return;
    el.setAttribute("aria-hidden", hidden ? "true" : "false");
  }
  function stripIds(root) {
    if (!root || root.nodeType !== 1) return;
    if (root.hasAttribute && root.hasAttribute("id")) root.removeAttribute("id");
    var nodes = root.querySelectorAll ? root.querySelectorAll("[id]") : [];
    for (var i = 0; i < nodes.length; i++) nodes[i].removeAttribute("id");
  }
  function unhideTree(root) {
    if (!root || root.nodeType !== 1) return;
    if (root.hasAttribute && root.hasAttribute("hidden")) root.removeAttribute("hidden");
    if (root.classList) root.classList.remove("sv-hidden");
    if (root.style && root.style.display === "none") root.style.display = "";
    var kids = root.children || [];
    for (var i = 0; i < kids.length; i++) unhideTree(kids[i]);
  }
  function isBlankContent(node) {
    if (!node) return true;
    var t = node.textContent;
    if (t == null) return true;
    var compact = String(t).replace(/\s+/g, "").replace(/[—–-]+/g, "").trim();
    return !compact;
  }
  function getDefTipText(defEl) {
    if (!defEl || !defEl.getAttribute) return "";
    var t = defEl.getAttribute(DEF_TIP_ATTR);
    if (t == null) return "";
    t = String(t);
    if (!t.replace(/\s+/g, "")) return "";
    return t;
  }
  function getDefLinkTitle(defEl) {
    if (!defEl || !defEl.getAttribute) return "";
    var t = defEl.getAttribute(DEF_LINK_ATTR);
    if (t == null) return "";
    return String(t).trim();
  }
  function getContentNodeFromTrigger(trigger) {
    if (!trigger) return null;
    if (trigger.tagName === "SUMMARY") {
      var det = closest(trigger, POPUP_DETAILS_SEL);
      if (!det) return null;
      var pop = det.querySelector(POPUP_POP_SEL);
      if (!pop) return null;
      var body =
        pop.querySelector(".sv-tip-pop-body") ||
        pop.querySelector(".sv-disclose-pop-body") ||
        pop.querySelector(".sv-disclose-list");
      return body || pop;
    }
    var id = trigger.getAttribute ? trigger.getAttribute("aria-controls") : "";
    if (id) {
      var wrap = closest(trigger, POPUP_WRAP_SEL);
      if (wrap) {
        try {
          var local = wrap.querySelector(safeIdSelector(id));
          if (local) {
            var b0 =
              local.querySelector(".sv-tip-pop-body") ||
              local.querySelector(".sv-disclose-pop-body") ||
              local.querySelector(".sv-disclose-list");
            return b0 || local;
          }
        } catch (e) {}
      }
      var global = document.getElementById(id);
      if (global) {
        var b1 =
          global.querySelector(".sv-tip-pop-body") ||
          global.querySelector(".sv-disclose-pop-body") ||
          global.querySelector(".sv-disclose-list");
        return b1 || global;
      }
    }
    var w = closest(trigger, POPUP_WRAP_SEL);
    if (w) {
      var p = w.querySelector(POPUP_POP_SEL);
      if (p) {
        var b2 =
          p.querySelector(".sv-tip-pop-body") ||
          p.querySelector(".sv-disclose-pop-body") ||
          p.querySelector(".sv-disclose-list");
        return b2 || p;
      }
    }
    return null;
  }
  function guessTitle(trigger, sourceNode) {
    var pop = null;
    if (sourceNode && sourceNode.nodeType === 1) {
      pop = closest(sourceNode, POPUP_POP_SEL);
    }
    if (pop) {
      var t = pop.querySelector(".sv-tip-pop-title") || pop.querySelector(".sv-disclose-pop-title");
      if (t && t.textContent) return String(t.textContent).trim();
    }
    var aria = trigger && trigger.getAttribute ? trigger.getAttribute("aria-label") : "";
    if (aria) return String(aria).trim();
    var txt = trigger && trigger.textContent ? String(trigger.textContent).trim() : "";
    if (txt) return txt.replace(/\s+/g, " ").trim();
    return "Info";
  }
  function buildActionsForDefinition(linkTitle) {
    if (!linkTitle) return null;
    var wrap = document.createElement("div");
    wrap.className = "sv-uipop-actions";
    var a = document.createElement("a");
    a.className = "sv-uipop-action";
    a.textContent = "Open page";
    if (window.mw && mw.util && typeof mw.util.getUrl === "function") a.href = mw.util.getUrl(linkTitle);
    else a.href = String(linkTitle);
    // IMPORTANT: do not prevent default/capture on this link — Popups extension can hook it.
    wrap.appendChild(a);
    return wrap;
  }
  function renderPopup(opts) {
    var el = ensurePopEl();
    if (!el) return;
    el.classList.remove("sv-uipop--sm", "sv-uipop--lg");
    el.classList.add(opts.size === "lg" ? "sv-uipop--lg" : "sv-uipop--sm");
    while (el.firstChild) el.removeChild(el.firstChild);
    // Header
    var head = document.createElement("div");
    head.className = "sv-uipop-head";
    // Click-open popups: center title + NO hint element
    if (opts.mode === "click") head.className += " sv-uipop-head--clickable sv-uipop-head--center";
    var title = document.createElement("div");
    title.className = "sv-uipop-title";
    title.textContent = opts.title || "Info";
    head.appendChild(title);
    // Hover popups: hint is allowed (but not required)
    if (opts.mode !== "click" && opts.hint) {
      var hint = document.createElement("div");
      hint.className = "sv-uipop-hint";
      hint.textContent = opts.hint;
      head.appendChild(hint);
    }
    el.appendChild(head);
    // Body
    var body = document.createElement("div");
    body.className = "sv-uipop-body";
    if (opts.bodyPre) body.classList.add("sv-uipop-body--pre");
    if (opts.bodyNode) {
      var clone = opts.bodyNode.cloneNode(true);
      stripIds(clone);
      unhideTree(clone);
      body.appendChild(clone);
    } else if (opts.bodyText != null) {
      body.textContent = String(opts.bodyText);
    }
    if (opts.actionsNode) body.appendChild(opts.actionsNode);
    el.appendChild(body);
    // Header click closes for click-open
    if (opts.mode === "click") {
      head.addEventListener("click", function (e) {
        e.preventDefault();
        hidePopup(true);
      }, { once: true });
    }
  }
  function positionPopupNearTrigger(trigger) {
    var el = ensurePopEl();
    if (!el || !trigger || !trigger.getBoundingClientRect) return;
    el.style.left = "0px";
    el.style.top = "0px";
    setHidden(el, false);
    requestAnimationFrame(function () {
      if (!state.open) return;
      var vw = window.innerWidth || document.documentElement.clientWidth || 0;
      var vh = window.innerHeight || document.documentElement.clientHeight || 0;
      var margin = 10;
      var gap = 8;
      var tr = trigger.getBoundingClientRect();
      var pr = el.getBoundingClientRect();
      var w = pr.width || 320;
      var h = pr.height || 180;
      var isIconish = (tr.width || 0) <= 40;
      var left = isIconish ? (tr.right - w) : tr.left;
      var top = tr.bottom + gap;
      left = clamp(left, margin, Math.max(margin, vw - w - margin));
      if (top + h + margin > vh) top = tr.top - h - gap;
      top = clamp(top, margin, Math.max(margin, vh - h - margin));
      el.style.left = Math.round(left) + "px";
      el.style.top = Math.round(top) + "px";
    });
  }
  function setExpanded(trigger, expanded) {
    if (!trigger || !trigger.setAttribute) return;
    trigger.setAttribute("aria-expanded", expanded ? "true" : "false");
  }
  function hidePopup() {
    if (!state.open) return;
    clearTimeout(state.hideTimer);
    state.hideTimer = 0;
    setExpanded(state.trigger, false);
    state.open = false;
    state.pinned = false;
    state.mode = "hover";
    state.size = "sm";
    state.trigger = null;
    var el = ensurePopEl();
    setHidden(el, true);
  }
  function openPopup(trigger, opts) {
    if (!trigger) return;
    if (state.open && state.trigger && state.trigger !== trigger) hidePopup();
    state.open = true;
    state.pinned = !!opts.pinned;
    state.mode = opts.mode;
    state.size = opts.size;
    state.trigger = trigger;
    setExpanded(trigger, true);
    renderPopup(opts);
    setHidden(popEl, false);
    positionPopupNearTrigger(trigger);
  }
  function findTrigger(t) {
    if (!t) return null;
    // Never treat clicks/hover inside the popup as trigger changes
    if (popEl && (t === popEl || (popEl.contains && popEl.contains(t)))) return null;
    var defEl = closest(t, DEF_SEL);
    if (defEl) return defEl;
    var tip = closest(t, TIP_BTN_SEL);
    if (tip) return tip;
    var dis = closest(t, DISCLOSE_BTN_SEL);
    if (dis) return dis;
    return null;
  }
  function resolveOptsForTrigger(trigger) {
    var isDef = trigger && trigger.matches && trigger.matches(DEF_SEL);
    var isTip = trigger && trigger.matches && trigger.matches(TIP_BTN_SEL);
    var isDisclose = trigger && trigger.matches && trigger.matches(DISCLOSE_BTN_SEL);
    var mode = "hover";
    var size = "sm";
    // Manual overrides (optional)
    var attrMode = trigger.getAttribute && trigger.getAttribute("data-sv-pop");
    if (attrMode === "hover" || attrMode === "click") mode = attrMode;
    var attrSize = trigger.getAttribute && trigger.getAttribute("data-sv-pop-size");
    if (attrSize === "sm" || attrSize === "lg") size = attrSize;
    if (!attrMode) {
      if (isDisclose) mode = "click";
      else if (isTip) mode = "hover";
      else if (isDef) {
        var link = getDefLinkTitle(trigger);
        mode = link ? "click" : "hover";
      }
    }
    if (!attrSize) size = (mode === "click") ? "lg" : "sm";
    if (isDef) {
      var tip = getDefTipText(trigger);
      if (!tip) return null;
      var linkTitle = getDefLinkTitle(trigger);
      var title = String(trigger.textContent || "").trim() || "Definition";
      return {
        mode: mode,
        size: size,
        pinned: (mode === "click"),
        title: title,
        hint: (mode === "click") ? "" : (HOVER_CAPABLE ? "Hover to view" : "Tap to view"),
        bodyText: tip,
        bodyPre: true,
        actionsNode: (mode === "click") ? buildActionsForDefinition(linkTitle) : null
      };
    }
    var sourceNode = getContentNodeFromTrigger(trigger);
    if (isBlankContent(sourceNode)) return null;
    return {
      mode: mode,
      size: size,
      pinned: (mode === "click"),
      title: guessTitle(trigger, sourceNode),
      hint: (mode === "click") ? "" : (HOVER_CAPABLE ? "Hover to view" : "Tap to view"),
      bodyNode: sourceNode,
      bodyPre: false,
      actionsNode: null
    };
  }
  function isInsidePopup(target) {
    return !!(popEl && (target === popEl || (popEl.contains && popEl.contains(target))));
  }
  /* ------------------------------------------------------------------ */
  /* Event Intercepts (window capture) — supersedes legacy systems      */
  /* ------------------------------------------------------------------ */
  window.addEventListener("pointerdown", function (e) {
    if (e && e.pointerType) state.lastPointerType = e.pointerType;
    // If user clicks inside popup, allow normal interactions (links, selection)
    if (isInsidePopup(e.target)) return;
    var trigger = findTrigger(e.target);
    if (!trigger) return;
    var opts = resolveOptsForTrigger(trigger);
    if (!opts) return;
    // Hover-mode on mouse: don’t click-open; let hover do it
    if (opts.mode === "hover" && HOVER_CAPABLE && (e.pointerType === "mouse" || !e.pointerType)) {
      e.stopPropagation();
      return;
    }
    e.preventDefault();
    e.stopPropagation();
    if (state.open && state.pinned && state.trigger === trigger) {
      hidePopup(true);
      return;
    }
    openPopup(trigger, opts);
  }, true);
  // Hover open/close (small)
  window.addEventListener("mouseover", function (e) {
    if (!HOVER_CAPABLE) return;
    if (isInsidePopup(e.target)) return; // allow moving into click popups without side effects
    var trigger = findTrigger(e.target);
    if (!trigger) {
      if (state.open && !state.pinned) hidePopup(false);
      return;
    }
    var opts = resolveOptsForTrigger(trigger);
    if (!opts || opts.mode !== "hover") return;
    e.stopPropagation();
    clearTimeout(state.hideTimer);
    state.hideTimer = 0;
    openPopup(trigger, opts);
  }, true);
  window.addEventListener("mouseout", function (e) {
    if (!HOVER_CAPABLE) return;
    if (!state.open || state.pinned) return;
    var trigger = findTrigger(e.target);
    if (!trigger || trigger !== state.trigger) return;
    e.stopPropagation();
    clearTimeout(state.hideTimer);
    state.hideTimer = setTimeout(function () {
      if (state.open && !state.pinned) hidePopup(false);
    }, 60);
  }, true);
  // Outside click closes pinned
  window.addEventListener("click", function (e) {
    if (!state.open) return;
    // If click inside popup, do nothing (links should work; Popups can hook them)
    if (isInsidePopup(e.target)) return;
    // Clicking a trigger is handled by pointerdown
    var trigger = findTrigger(e.target);
    if (trigger) {
      e.stopPropagation();
      return;
    }
    if (state.pinned) {
      hidePopup(true);
      e.stopPropagation();
    }
  }, true);
  window.addEventListener("keydown", function (e) {
    if (e.key !== "Escape") return;
    if (state.open) {
      hidePopup(true);
      e.stopPropagation();
    }
  }, true);
  window.addEventListener("scroll", function () {
    if (!state.open || !state.trigger) return;
    positionPopupNearTrigger(state.trigger);
  }, true);
  window.addEventListener("resize", function () {
    if (!state.open || !state.trigger) return;
    positionPopupNearTrigger(state.trigger);
  }, true);
  // Remove native title tooltips on defs if any remain
  function stripDefTitles(root) {
    var container = root || document;
    var defs = container.querySelectorAll(DEF_SEL + "[title]");
    for (var i = 0; i < defs.length; i++) defs[i].removeAttribute("title");
  }
  if (window.mw && mw.hook) {
    mw.hook("wikipage.content").add(function ($content) {
      stripDefTitles($content && $content[0]);
    });
  }
  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", function () { stripDefTitles(document); });
  } else {
    stripDefTitles(document);
   }
   }
})();
})();