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: Reverted Mobile edit Mobile web edit
Line 1,576: Line 1,576:
(function () {
(function () {
   /* ================================================================== */
   /* ================================================================== */
   /* MODULE: Universal Popups (v2) — refined                             */
   /* MODULE: Universal Popups (v2.1) — refined                           */
   /*  - Removes redundant hint for click-open (large) popups            */
   /*  Fixes: mobile close, keyboard open, summary default toggle,        */
   /*   - Leaves hover hints optional (but CSS currently hides legacy)    */
   /*         hover enter/leave timer handling                            */
  /*  - Keeps links inside popups fully interactive (Popups friendly)  */
   /* ================================================================== */
   /* ================================================================== */


Line 1,585: Line 1,584:
   var COMMON = (SV.common = SV.common || {});
   var COMMON = (SV.common = SV.common || {});


   var POPV2_VERSION = 2; // bumped due to header/hint changes
   var POPV2_VERSION = 21; // 2.1
   if (typeof COMMON.popupsV2Init === "number" && COMMON.popupsV2Init >= POPV2_VERSION) return;
   if (typeof COMMON.popupsV2Init === "number" && COMMON.popupsV2Init >= POPV2_VERSION) return;
   COMMON.popupsV2Init = POPV2_VERSION;
   COMMON.popupsV2Init = POPV2_VERSION;
Line 1,614: Line 1,613:
     open: false,
     open: false,
     pinned: false,
     pinned: false,
     mode: "hover",     // hover | click
     mode: "hover", // hover | click
     size: "sm",         // sm | lg
     size: "sm",   // sm | lg
     trigger: null,
     trigger: null,
     lastPointerType: "mouse",
     lastPointerType: "mouse",
Line 1,639: Line 1,638:
   function safeIdSelector(id) {
   function safeIdSelector(id) {
     return "#" + String(id).replace(/([ !"#$%&'()*+,.\/:;<=>?@[\\\]^`{|}~])/g, "\\$1");
     return "#" + String(id).replace(/([ !"#$%&'()*+,.\/:;<=>?@[\\\]^`{|}~])/g, "\\$1");
  }
  function setHidden(el, hidden) {
    if (!el) return;
    el.setAttribute("aria-hidden", hidden ? "true" : "false");
   }
   }


Line 1,648: Line 1,652:
     el.setAttribute("role", "dialog");
     el.setAttribute("role", "dialog");
     el.setAttribute("aria-hidden", "true");
     el.setAttribute("aria-hidden", "true");
    // Keep hover popups from closing while pointer is over the popup itself
    el.addEventListener("mouseenter", function () {
      if (!HOVER_CAPABLE) return;
      if (!state.open || state.pinned) return;
      clearTimeout(state.hideTimer);
      state.hideTimer = 0;
    });
    el.addEventListener("mouseleave", function () {
      if (!HOVER_CAPABLE) return;
      if (!state.open || state.pinned) return;
      clearTimeout(state.hideTimer);
      state.hideTimer = setTimeout(function () {
        if (state.open && !state.pinned) hidePopup();
      }, 80);
    });


     document.body.appendChild(el);
     document.body.appendChild(el);
     popEl = el;
     popEl = el;
     return popEl;
     return popEl;
  }
  function setHidden(el, hidden) {
    if (!el) return;
    el.setAttribute("aria-hidden", hidden ? "true" : "false");
   }
   }


Line 1,700: Line 1,716:
     if (t == null) return "";
     if (t == null) return "";
     return String(t).trim();
     return String(t).trim();
  }
  function isSummaryTrigger(trigger) {
    return !!(trigger && trigger.tagName === "SUMMARY");
   }
   }


Line 1,763: Line 1,783:
   function guessTitle(trigger, sourceNode) {
   function guessTitle(trigger, sourceNode) {
     var pop = null;
     var pop = null;
     if (sourceNode && sourceNode.nodeType === 1) {
     if (sourceNode && sourceNode.nodeType === 1) pop = closest(sourceNode, POPUP_POP_SEL);
      pop = closest(sourceNode, POPUP_POP_SEL);
 
    }
     if (pop) {
     if (pop) {
       var t = pop.querySelector(".sv-tip-pop-title") || pop.querySelector(".sv-disclose-pop-title");
       var t = pop.querySelector(".sv-tip-pop-title") || pop.querySelector(".sv-disclose-pop-title");
Line 1,793: Line 1,812:
     else a.href = String(linkTitle);
     else a.href = String(linkTitle);


     // IMPORTANT: do not prevent default/capture on this link — Popups extension can hook it.
     // Do not prevent default/capture on this link — Popups extension can hook it.
     wrap.appendChild(a);
     wrap.appendChild(a);
     return wrap;
     return wrap;
Line 1,850: Line 1,869:
       head.addEventListener("click", function (e) {
       head.addEventListener("click", function (e) {
         e.preventDefault();
         e.preventDefault();
         hidePopup(true);
         hidePopup();
       }, { once: true });
       }, { once: true });
     }
     }
Line 1,975: Line 1,994:


     if (!attrSize) size = (mode === "click") ? "lg" : "sm";
     if (!attrSize) size = (mode === "click") ? "lg" : "sm";
    // No-hover devices: “hover” becomes tap-to-open (click-mode) so it can close properly.
    if (!HOVER_CAPABLE && mode === "hover") {
      mode = "click";
      size = (attrSize === "lg") ? "lg" : "sm";
    }


     if (isDef) {
     if (isDef) {
Line 2,019: Line 2,044:


   window.addEventListener("pointerdown", function (e) {
   window.addEventListener("pointerdown", function (e) {
     if (e && e.pointerType) state.lastPointerType = e.pointerType;
     if (!e) return;
 
    // ignore right/middle clicks
    if (typeof e.button === "number" && e.button !== 0) return;
 
    if (e.pointerType) state.lastPointerType = e.pointerType;


     // If user clicks inside popup, allow normal interactions (links, selection)
     // If user clicks inside popup, allow normal interactions (links, selection)
Line 2,029: Line 2,059:
     var opts = resolveOptsForTrigger(trigger);
     var opts = resolveOptsForTrigger(trigger);
     if (!opts) return;
     if (!opts) return;
    // Prevent native <details>/<summary> toggling whenever summary is used
    if (isSummaryTrigger(trigger)) e.preventDefault();


     // Hover-mode on mouse: don’t click-open; let hover do it
     // Hover-mode on mouse: don’t click-open; let hover do it
Line 2,040: Line 2,073:


     if (state.open && state.pinned && state.trigger === trigger) {
     if (state.open && state.pinned && state.trigger === trigger) {
       hidePopup(true);
       hidePopup();
       return;
       return;
    }
    openPopup(trigger, opts);
  }, true);
  // Keyboard open (Enter/Space) for accessibility + summary safety
  window.addEventListener("keydown", function (e) {
    if (!e) return;
    if (e.key === "Escape") {
      if (state.open) {
        hidePopup();
        e.stopPropagation();
      }
      return;
    }
    var isActivate = (e.key === "Enter" || e.key === " ");
    if (!isActivate) return;
    if (isInsidePopup(e.target)) return;
    var trigger = findTrigger(e.target);
    if (!trigger) return;
    var opts = resolveOptsForTrigger(trigger);
    if (!opts) return;
    // Keyboard activation should behave like click-open so user can read/interact
    if (opts.mode === "hover") {
      opts.mode = "click";
      opts.pinned = true;
      opts.size = "sm";
      opts.hint = "";
      opts.actionsNode = opts.actionsNode || null;
    }
    e.preventDefault();
    e.stopPropagation();
    // Stop native summary toggling as well
    if (isSummaryTrigger(trigger)) {
      try {
        var det = closest(trigger, POPUP_DETAILS_SEL);
        if (det && det.open) det.open = false;
      } catch (x) {}
     }
     }


Line 2,050: Line 2,129:
   window.addEventListener("mouseover", function (e) {
   window.addEventListener("mouseover", function (e) {
     if (!HOVER_CAPABLE) return;
     if (!HOVER_CAPABLE) return;
     if (isInsidePopup(e.target)) return; // allow moving into click popups without side effects
 
    // If entering the popup itself, keep it open
     if (isInsidePopup(e.target)) {
      if (state.open && !state.pinned) {
        clearTimeout(state.hideTimer);
        state.hideTimer = 0;
      }
      return;
    }


     var trigger = findTrigger(e.target);
     var trigger = findTrigger(e.target);
     if (!trigger) {
     if (!trigger) {
       if (state.open && !state.pinned) hidePopup(false);
       if (state.open && !state.pinned) hidePopup();
       return;
       return;
     }
     }
Line 2,080: Line 2,167:
     clearTimeout(state.hideTimer);
     clearTimeout(state.hideTimer);
     state.hideTimer = setTimeout(function () {
     state.hideTimer = setTimeout(function () {
       if (state.open && !state.pinned) hidePopup(false);
       if (state.open && !state.pinned) hidePopup();
     }, 60);
     }, 80);
   }, true);
   }, true);


   // Outside click closes pinned
   // Outside click closes (pinned always; and also closes “tap-open” popups on no-hover devices)
   window.addEventListener("click", function (e) {
   window.addEventListener("click", function (e) {
     if (!state.open) return;
     if (!state.open) return;


    // If click inside popup, do nothing (links should work; Popups can hook them)
     if (isInsidePopup(e.target)) return;
     if (isInsidePopup(e.target)) return;


    // Clicking a trigger is handled by pointerdown
     var trigger = findTrigger(e.target);
     var trigger = findTrigger(e.target);
     if (trigger) {
     if (trigger) {
Line 2,098: Line 2,183:
     }
     }


     if (state.pinned) {
     if (state.pinned || !HOVER_CAPABLE) {
      hidePopup(true);
       hidePopup();
      e.stopPropagation();
    }
  }, true);
 
  window.addEventListener("keydown", function (e) {
    if (e.key !== "Escape") return;
    if (state.open) {
       hidePopup(true);
       e.stopPropagation();
       e.stopPropagation();
     }
     }

Revision as of 22:17, 26 February 2026

/* Common.js — Spirit Vale Wiki
   Phase 4.1 rebuild (incremental)

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

(function () {
  /* ================================================================== */
  /* MODULE: Definitions (v1)                                           */
  /* ================================================================== */

  var SV = (window.SV = window.SV || {});
  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) {
    if (!defEl || !defEl.getAttribute) return "";
    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;
  COMMON.levelsInit = LEVELS_VERSION;

  var CARD_SEL = ".sv-gi-card, .sv-skill-card, .sv-passive-card";

  // Be a bit forgiving: accept the custom slider even if the data attr changes.
  var LEVEL_RANGE_SEL =
    "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_SCOPE_SEL = ".sv-gi-bottom, .sv-skill-bottom";
  var SERIES_SEL = "[data-series]";
  var LEVEL_INIT_ATTR = "data-gi-level-init";

  var _seriesCache = typeof WeakMap !== "undefined" ? new WeakMap() : null;
  var _drag = null;

  function clampInt(value, min, max, fallback) {
    var n = parseInt(value, 10);
    if (isNaN(n)) n = fallback != null ? fallback : min;
    if (n < min) return min;
    if (n > max) return max;
    return n;
  }

  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 parseSeries(el) {
    if (!el) return null;

    if (_seriesCache) {
      if (_seriesCache.has(el)) return _seriesCache.get(el);
    } else if (el._svSeries !== undefined) {
      return el._svSeries;
    }

    var raw = el.getAttribute("data-series");
    var parsed = null;

    if (raw != null && raw !== "") {
      try {
        parsed = JSON.parse(raw);
        if (!Array.isArray(parsed)) parsed = null;
      } catch (e) {
        parsed = null;
      }
    }

    if (_seriesCache) _seriesCache.set(el, parsed);
    else el._svSeries = parsed;

    return parsed;
  }

  function isAfter(boundaryNode, el) {
    if (!boundaryNode || !el) return true;
    if (boundaryNode === el) return false;
    if (!boundaryNode.compareDocumentPosition) return true;
    return (boundaryNode.compareDocumentPosition(el) & 4) !== 0;
  }

  function isRangeInput(el) {
    return !!(el && el.tagName === "INPUT" && el.type === "range");
  }

  function isCustomSlider(el) {
    if (!el || !el.getAttribute) return false;
    if (el.getAttribute("data-sv-slider") === "1") return true;
    // fallback: class-based detection
    return !!(el.classList && el.classList.contains("sv-level-range--custom"));
  }

  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;
    if (!ui) return null;
    var m = String(ui.textContent || "").match(/\/\s*(\d+)/);
    return m ? m[1] : null;
  }

  function getCardMaxLevel(card) {
    var raw =
      card.getAttribute("data-max-level") ||
      card.getAttribute("data-maxLevel") ||
      card.getAttribute("data-max") ||
      card.getAttribute("data-level-max") ||
      parseMaxFromUi(card);

    return clampInt(raw, 1, 999, 1);
  }

  function getCardLevel(card, maxLevel) {
    var raw =
      card.getAttribute("data-level") ||
      card.getAttribute("data-gi-level") ||
      (card.querySelector && card.querySelector(".sv-level-num")
        ? card.querySelector(".sv-level-num").textContent
        : null);

    return clampInt(raw, 1, maxLevel, 1);
  }

  function getLevelSlider(card) {
    return card.querySelector(LEVEL_RANGE_SEL);
  }

  function getLevelBoundary(slider) {
    if (!slider) return null;
    return closest(slider, LEVEL_BOUNDARY_SEL) || slider;
  }

  function getLevelScopeContainer(card, boundary) {
    var scope = card.querySelector(LEVEL_SCOPE_SEL) || card;
    if (boundary && scope !== card && !scope.contains(boundary)) return card;
    return scope;
  }

  function setLevelNumber(card, level) {
    var span = card.querySelector(".sv-level-num");
    if (span) span.textContent = String(level);
  }

  function applySeriesToScope(scope, boundary, level) {
    if (!scope) return;

    var index = level - 1;
    var nodes = scope.querySelectorAll(SERIES_SEL);

    for (var i = 0; i < nodes.length; i++) {
      var el = nodes[i];
      if (boundary && !isAfter(boundary, el)) continue;

      var series = parseSeries(el);
      if (!series || series.length === 0) continue;

      var safeIndex = index;
      if (safeIndex >= series.length) safeIndex = series.length - 1;
      if (safeIndex < 0) safeIndex = 0;

      var value = series[safeIndex];
      if (value == null) value = "";

      el.textContent = String(value);
    }
  }

  function getCustomMin(slider) {
    return clampInt(
      slider.getAttribute("aria-valuemin") || slider.getAttribute("data-min"),
      1,
      999,
      1
    );
  }

  function getCustomMax(slider) {
    return clampInt(
      slider.getAttribute("aria-valuemax") || slider.getAttribute("data-max"),
      1,
      999,
      1
    );
  }

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

  function setCustomBounds(slider, min, max) {
    slider.setAttribute("aria-valuemin", String(min));
    slider.setAttribute("aria-valuemax", String(max));
    slider.setAttribute("data-min", String(min));
    slider.setAttribute("data-max", String(max));
  }

  function updateCustomVisual(slider, value) {
    var min = getCustomMin(slider);
    var max = getCustomMax(slider);
    if (max < min) max = min;

    var pct = max === min ? 0 : (value - min) / (max - min);
    if (pct < 0) pct = 0;
    if (pct > 1) pct = 1;

    var left = pct * 100;

    var thumb = slider.querySelector(".sv-level-thumb");
    var fill = slider.querySelector(".sv-level-fill");

    if (thumb && thumb.style) thumb.style.left = left + "%";
    if (fill && fill.style) fill.style.width = left + "%";
  }

  function setCustomValue(slider, value) {
    slider.setAttribute("aria-valuenow", String(value));
    slider.setAttribute("data-value", String(value));
    updateCustomVisual(slider, value);
  }

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

    var w = rect.width || 1;
    var x = (clientX - rect.left) / w;
    if (x < 0) x = 0;
    if (x > 1) x = 1;

    var raw = min + x * (max - min);
    var rounded = Math.round(raw);
    return clampInt(rounded, min, max, min);
  }

  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 applyLevelToCard(card, rawLevel) {
    if (!card) return;

    var maxLevel = getCardMaxLevel(card);
    var slider = getLevelSlider(card);
    var level = clampInt(rawLevel, 1, maxLevel, getCardLevel(card, maxLevel));

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

    if (slider) {
      unhide(slider);
      var wrap = closest(slider, ".sv-level-slider");
      if (wrap) unhide(wrap);
    }

    if (slider && maxLevel > 1) {
      if (isRangeInput(slider)) {
        slider.setAttribute("min", "1");
        slider.setAttribute("max", String(maxLevel));
        if (String(slider.value) !== String(level)) slider.value = String(level);
      } else if (isCustomSlider(slider)) {
        setCustomBounds(slider, 1, maxLevel);
        setCustomValue(slider, level);
      }
    } else if (slider && isCustomSlider(slider)) {
      // Even if max==1, keep the visual consistent
      setCustomBounds(slider, 1, maxLevel);
      setCustomValue(slider, level);
    }

    var boundary = getLevelBoundary(slider);
    var scope = getLevelScopeContainer(card, boundary);
    applySeriesToScope(scope, boundary, level);
  }

  function initLevels(root) {
    var container = root || document;
    var cards = container.querySelectorAll(CARD_SEL);

    for (var i = 0; i < cards.length; 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);
      if (stamped >= LEVELS_VERSION) continue;

      var maxLevel = getCardMaxLevel(card);
      var slider = getLevelSlider(card);

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

      applyLevelToCard(card, start);
      card.setAttribute(LEVEL_INIT_ATTR, String(LEVELS_VERSION));
    }
  }

  document.addEventListener(
    "input",
    function (e) {
      var t = e.target;
      if (!t || t.nodeType !== 1) return;
      if (!t.matches || !t.matches("input.sv-level-range[type='range']")) return;

      var card = closest(t, CARD_SEL);
      if (!card) return;

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

  document.addEventListener(
    "pointerdown",
    function (e) {
      var slider =
        e.target && e.target.closest
          ? e.target.closest(".sv-level-range--custom, .sv-level-range[data-sv-slider='1']")
          : null;
      if (!slider) return;

      var card = closest(slider, CARD_SEL);
      if (!card) return;

      e.preventDefault();
      if (slider.focus) slider.focus();

      _drag = { slider: slider, card: card, pointerId: e.pointerId };

      if (slider.setPointerCapture) {
        try {
          slider.setPointerCapture(e.pointerId);
        } catch (err) {}
      }

      applyLevelToCard(card, valueFromClientX(slider, e.clientX));
    },
    true
  );

  document.addEventListener(
    "pointermove",
    function (e) {
      if (!_drag) return;
      if (e.pointerId !== _drag.pointerId) return;
      e.preventDefault();
      applyLevelToCard(_drag.card, valueFromClientX(_drag.slider, e.clientX));
    },
    true
  );

  function endDrag(e) {
    if (!_drag) return;
    if (e && e.pointerId != null && e.pointerId !== _drag.pointerId) return;

    if (_drag.slider && _drag.slider.releasePointerCapture) {
      try {
        _drag.slider.releasePointerCapture(_drag.pointerId);
      } catch (err) {}
    }
    _drag = null;
  }

  document.addEventListener("pointerup", endDrag, true);
  document.addEventListener("pointercancel", endDrag, true);

  document.addEventListener(
    "keydown",
    function (e) {
      var el = document.activeElement;
      if (!el || !el.closest) return;

      var slider = el.closest(".sv-level-range--custom, .sv-level-range[data-sv-slider='1']");
      if (!slider) return;

      var card = closest(slider, CARD_SEL);
      if (!card) return;

      var min = getCustomMin(slider);
      var max = getCustomMax(slider);
      if (max < min) max = min;

      var cur = getCustomValue(slider, getCardLevel(card, max));
      var next = cur;

      if (e.key === "ArrowLeft" || e.key === "Left" || e.key === "ArrowDown") {
        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
  );

  function initAllLevels(root) {
    initLevels(root);
  }

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

  if (window.mw && mw.hook) {
    mw.hook("wikipage.content").add(function ($content) {
      initAllLevels($content && $content[0]);
    });
  }

  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", function () {
      initAllLevels(document);
    });
  } else {
    initAllLevels(document);
  }
})();

(function () {
  /* ================================================================== */
  /* MODULE: 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 COMMON = (SV.common = SV.common || {});
  if (COMMON.popupsInit) return;
  COMMON.popupsInit = 1;

  var CARD_SEL = ".sv-gi-card, .sv-skill-card, .sv-passive-card";

  var POPUP_BTN_SEL =
    ".sv-tip-btn[data-sv-toggle='1'], .sv-disclose-btn[data-sv-toggle='1']";

  var POPUP_DETAILS_SEL = "details.sv-tip, details.sv-disclose";
  var POPUP_SUMMARY_SEL = "details.sv-tip > summary, details.sv-disclose > summary";

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

  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 = {
    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 clamp(n, min, max) {
    if (n < min) return min;
    if (n > max) return max;
    return n;
  }

  function shouldSuppressClick() {
    return Date.now() < state.suppressClickUntil;
  }

  function sameAnchor(el) {
    return !!(el && state.anchor && el === state.anchor);
  }

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

  function ensurePopEl() {
    if (popEl) return popEl;

    var el = document.createElement("div");
    el.className = "sv-pop-tip sv-hidden";
    el.setAttribute("role", "dialog");
    el.setAttribute("aria-hidden", "true");

    el.style.position = "fixed";
    el.style.zIndex = "99998";
    el.style.maxWidth = "420px";
    el.style.boxSizing = "border-box";
    el.style.padding = "10px 12px";
    el.style.borderRadius = "12px";
    el.style.background = "rgba(10, 14, 22, 0.92)";
    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;
    return popEl;
  }

  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 safeIdSelector(id) {
    return "#" + String(id).replace(/([ !"#$%&'()*+,.\/:;<=>?@[\\\]^`{|}~])/g, "\\$1");
  }

  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(POPUP_BODY_SEL);
      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 ? local.querySelector(POPUP_BODY_SEL) : null;
            return b0 || local;
          }
        } catch (e) {}
      }

      var global = document.getElementById(id);
      if (global) {
        var b1 = global.querySelector ? global.querySelector(POPUP_BODY_SEL) : null;
        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(POPUP_BODY_SEL);
        return b2 || p;
      }
    }

    return null;
  }

  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 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 setPopContentFromNode(sourceNode) {
    var el = ensurePopEl();
    if (!el) return;

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

    var clone = sourceNode.cloneNode(true);
    stripIds(clone);
    unhideTree(clone);

    var isBody =
      clone.classList &&
      (clone.classList.contains("sv-tip-pop-body") ||
        clone.classList.contains("sv-disclose-pop-body"));

    if (isBody) {
      while (clone.firstChild) el.appendChild(clone.firstChild);
    } else {
      el.appendChild(clone);
    }
  }

  function positionPopAt(clientX, clientY) {
    var el = ensurePopEl();
    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 || 260;
      var h = r.height || 80;

      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 hidePop() {
    if (!popEl) return;

    setExpanded(state.anchor, false);

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

    state.visible = false;
    state.pinned = false;
    state.anchor = null;

    setHidden(popEl, true);
  }

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

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

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

    setPopContentFromNode(sourceNode);

    el.style.pointerEvents = state.pinned ? "auto" : "none";
    el.classList.toggle("sv-pop-tip--pinned", state.pinned);

    setExpanded(trigger, true);

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

    setHidden(el, false);
    positionPopAt(state.lastX, state.lastY);
  }

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

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

  if (HOVER_CAPABLE) {
    document.addEventListener(
      "mouseover",
      function (e) {
        if (state.pinned) return;

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

        var rel = e.relatedTarget;
        if (rel && trigger.contains(rel)) return;

        var source = getContentNodeFromTrigger(trigger);
        if (isBlankContent(source)) {
          if (state.visible && !state.pinned) hidePop();
          return;
        }

        showPop(trigger, source, e.clientX, e.clientY, false);
      },
      true
    );

    document.addEventListener(
      "mousemove",
      function (e) {
        if (!state.visible || state.pinned) return;

        var trigger = findTriggerFromEventTarget(e.target);
        if (!trigger || !sameAnchor(trigger)) return;

        state.lastX = e.clientX;
        state.lastY = e.clientY;
        positionPopAt(state.lastX, state.lastY);
      },
      true
    );

    document.addEventListener(
      "mouseout",
      function (e) {
        if (!state.visible || state.pinned) return;

        var trigger = findTriggerFromEventTarget(e.target);
        if (!trigger || !sameAnchor(trigger)) return;

        var rel = e.relatedTarget;
        if (rel && trigger.contains(rel)) return;

        hidePop();
      },
      true
    );
  }

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

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

      if (!isTouchLikeEvent(e)) return;

      e.preventDefault();

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

  document.addEventListener(
    "click",
    function (e) {
      if (shouldSuppressClick()) return;

      var trigger = findTriggerFromEventTarget(e.target);
      if (!trigger) return;

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

      e.preventDefault();

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

  document.addEventListener(
    "keydown",
    function (e) {
      if (!(e.key === "Enter" || e.key === " " || e.key === "Spacebar")) return;

      var active = document.activeElement;
      var trigger = findTriggerFromEventTarget(active);
      if (!trigger) return;

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

      e.preventDefault();

      if (state.pinned && sameAnchor(trigger)) hidePop();
      else showPop(trigger, source, state.lastX || 0, state.lastY || 0, true);
    },
    true
  );

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

      if (popEl && (e.target === popEl || (popEl.contains && popEl.contains(e.target))))
        return;

      if (findTriggerFromEventTarget(e.target)) return;

      hidePop();
    },
    true
  );

  document.addEventListener(
    "keydown",
    function (e) {
      if (e.key !== "Escape") return;
      if (state.visible) hidePop();
    },
    true
  );

  window.addEventListener(
    "scroll",
    function () {
      if (!state.visible) return;
      positionPopAt(state.lastX, state.lastY);
    },
    true
  );

  window.addEventListener(
    "resize",
    function () {
      if (!state.visible) return;
      positionPopAt(state.lastX, state.lastY);
    },
    true
  );

  function hideTrigger(el) {
    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 btns = container.querySelectorAll(POPUP_BTN_SEL);
    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) {
    mw.hook("wikipage.content").add(function ($content) {
      initAllPopups($content && $content[0]);
    });
  }

  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", function () {
      initAllPopups(document);
    });
  } else {
    initAllPopups(document);
  }
})();

(function () {
  /* ================================================================== */
  /* MODULE: Tabs                                                       */
  /* ================================================================== */

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

  var TABS_ROOT_SEL = ".sv-tabs[data-tabs='1']";
  var TAB_SEL = ".sv-tab";
  var PANEL_SEL = ".sv-tabpanel";

  var _tabsUidCounter = 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 getTabs(root) {
    return root.querySelectorAll(TAB_SEL);
  }

  function getPanels(root) {
    return root.querySelectorAll(PANEL_SEL);
  }

  function getPanelByKey(root, key) {
    var panels = getPanels(root);
    for (var i = 0; i < panels.length; i++) {
      if (panels[i].getAttribute("data-panel") === key) return panels[i];
    }
    return null;
  }

  function ensureUniqueTabIds(root) {
    var uid = root.getAttribute("data-tabs-uid");
    if (!uid) {
      _tabsUidCounter++;
      uid = (root.getAttribute("data-tabs-root") || "sv-tabs") + "-" + _tabsUidCounter;
      root.setAttribute("data-tabs-uid", uid);
    }

    var tabs = getTabs(root);
    for (var i = 0; i < tabs.length; i++) {
      var tab = tabs[i];
      var key = tab.getAttribute("data-tab") || String(i + 1);
      var panel = getPanelByKey(root, key);

      var tabId = uid + "-tab-" + key;
      tab.id = tabId;

      if (panel) {
        var panelId = uid + "-panel-" + key;
        panel.id = panelId;
        tab.setAttribute("aria-controls", panelId);
        panel.setAttribute("aria-labelledby", tabId);
      }
    }
  }

  function showPanel(panel) {
    if (!panel) return;
    panel.setAttribute("data-active", "1");
    if (panel.classList) panel.classList.remove("sv-hidden");
    panel.removeAttribute("hidden");
  }

  function hidePanel(panel) {
    if (!panel) return;
    panel.setAttribute("data-active", "0");
    if (panel.classList) panel.classList.add("sv-hidden");
    panel.setAttribute("hidden", "hidden");
  }

  function activateTab(root, btn) {
    if (!root || !btn) return;

    var tabs = getTabs(root);
    var key = btn.getAttribute("data-tab") || "";

    for (var i = 0; i < tabs.length; i++) {
      var t = tabs[i];
      var active = t === btn;
      t.setAttribute("aria-selected", active ? "true" : "false");
      t.setAttribute("tabindex", active ? "0" : "-1");
      if (t.classList) t.classList.toggle("sv-tab--active", active);
    }

    var panels = getPanels(root);
    for (var j = 0; j < panels.length; j++) {
      var p = panels[j];
      var isTarget = p.getAttribute("data-panel") === key;
      if (isTarget) showPanel(p);
      else hidePanel(p);
    }

    root.setAttribute("data-active-tab", key);
  }

  function focusTab(tabs, idx) {
    if (idx < 0) idx = 0;
    if (idx >= tabs.length) idx = tabs.length - 1;
    var t = tabs[idx];
    if (t && t.focus) t.focus();
    return t;
  }

  function indexOfTab(tabs, btn) {
    for (var i = 0; i < tabs.length; i++) {
      if (tabs[i] === btn) return i;
    }
    return -1;
  }

  function normalizeTabsRoot(root) {
    if (!root) return;
    if (root.getAttribute("data-gi-tabs-init") === "1") return;

    ensureUniqueTabIds(root);

    var tabs = getTabs(root);
    var panels = getPanels(root);
    if (!tabs.length || !panels.length) {
      root.setAttribute("data-gi-tabs-init", "1");
      return;
    }

    var activeBtn = null;
    for (var i = 0; i < tabs.length; i++) {
      if (tabs[i].getAttribute("aria-selected") === "true") {
        activeBtn = tabs[i];
        break;
      }
    }
    if (!activeBtn) activeBtn = tabs[0];

    activateTab(root, activeBtn);
    root.setAttribute("data-gi-tabs-init", "1");
  }

  function initTabs(root) {
    var container = root || document;
    var roots = container.querySelectorAll(TABS_ROOT_SEL);
    for (var i = 0; i < roots.length; i++) normalizeTabsRoot(roots[i]);
  }

  document.addEventListener(
    "click",
    function (e) {
      var btn = e.target && e.target.closest ? e.target.closest(TAB_SEL) : null;
      if (!btn) return;

      var root = closest(btn, ".sv-tabs");
      if (!root || root.getAttribute("data-tabs") !== "1") return;

      e.preventDefault();
      activateTab(root, btn);
    },
    true
  );

  document.addEventListener(
    "keydown",
    function (e) {
      var btn =
        document.activeElement && document.activeElement.closest
          ? document.activeElement.closest(TAB_SEL)
          : null;
      if (!btn) return;

      var root = closest(btn, ".sv-tabs");
      if (!root || root.getAttribute("data-tabs") !== "1") return;

      var tabs = getTabs(root);
      if (!tabs || !tabs.length) return;

      var idx = indexOfTab(tabs, btn);
      if (idx < 0) return;

      if (e.key === "ArrowLeft" || e.key === "Left") {
        e.preventDefault();
        activateTab(root, focusTab(tabs, idx - 1));
        return;
      }
      if (e.key === "ArrowRight" || e.key === "Right") {
        e.preventDefault();
        activateTab(root, focusTab(tabs, idx + 1));
        return;
      }
      if (e.key === "Home") {
        e.preventDefault();
        activateTab(root, focusTab(tabs, 0));
        return;
      }
      if (e.key === "End") {
        e.preventDefault();
        activateTab(root, focusTab(tabs, tabs.length - 1));
        return;
      }
      if (e.key === "Enter" || e.key === " ") {
        e.preventDefault();
        activateTab(root, btn);
        return;
      }
    },
    true
  );

  function initAllTabs(root) {
    initTabs(root);
  }

  if (window.mw && mw.hook) {
    mw.hook("wikipage.content").add(function ($content) {
      initAllTabs($content && $content[0]);
    });
  }

  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", function () {
      initAllTabs(document);
    });
  } else {
    initAllTabs(document);
  }
})();

(function () {
  /* ================================================================== */
  /* MODULE: Universal Popups (v2.1) — refined                           */
  /*   Fixes: mobile close, keyboard open, summary default toggle,        */
  /*          hover enter/leave timer handling                            */
  /* ================================================================== */

  var SV = (window.SV = window.SV || {});
  var COMMON = (SV.common = SV.common || {});

  var POPV2_VERSION = 21; // 2.1
  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 setHidden(el, hidden) {
    if (!el) return;
    el.setAttribute("aria-hidden", hidden ? "true" : "false");
  }

  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");

    // Keep hover popups from closing while pointer is over the popup itself
    el.addEventListener("mouseenter", function () {
      if (!HOVER_CAPABLE) return;
      if (!state.open || state.pinned) return;
      clearTimeout(state.hideTimer);
      state.hideTimer = 0;
    });

    el.addEventListener("mouseleave", function () {
      if (!HOVER_CAPABLE) return;
      if (!state.open || state.pinned) return;
      clearTimeout(state.hideTimer);
      state.hideTimer = setTimeout(function () {
        if (state.open && !state.pinned) hidePopup();
      }, 80);
    });

    document.body.appendChild(el);
    popEl = el;
    return popEl;
  }

  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 isSummaryTrigger(trigger) {
    return !!(trigger && trigger.tagName === "SUMMARY");
  }

  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);

    // 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();
      }, { 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";

    // No-hover devices: “hover” becomes tap-to-open (click-mode) so it can close properly.
    if (!HOVER_CAPABLE && mode === "hover") {
      mode = "click";
      size = (attrSize === "lg") ? "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) return;

    // ignore right/middle clicks
    if (typeof e.button === "number" && e.button !== 0) return;

    if (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;

    // Prevent native <details>/<summary> toggling whenever summary is used
    if (isSummaryTrigger(trigger)) e.preventDefault();

    // 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();
      return;
    }

    openPopup(trigger, opts);
  }, true);

  // Keyboard open (Enter/Space) for accessibility + summary safety
  window.addEventListener("keydown", function (e) {
    if (!e) return;

    if (e.key === "Escape") {
      if (state.open) {
        hidePopup();
        e.stopPropagation();
      }
      return;
    }

    var isActivate = (e.key === "Enter" || e.key === " ");
    if (!isActivate) return;

    if (isInsidePopup(e.target)) return;

    var trigger = findTrigger(e.target);
    if (!trigger) return;

    var opts = resolveOptsForTrigger(trigger);
    if (!opts) return;

    // Keyboard activation should behave like click-open so user can read/interact
    if (opts.mode === "hover") {
      opts.mode = "click";
      opts.pinned = true;
      opts.size = "sm";
      opts.hint = "";
      opts.actionsNode = opts.actionsNode || null;
    }

    e.preventDefault();
    e.stopPropagation();

    // Stop native summary toggling as well
    if (isSummaryTrigger(trigger)) {
      try {
        var det = closest(trigger, POPUP_DETAILS_SEL);
        if (det && det.open) det.open = false;
      } catch (x) {}
    }

    openPopup(trigger, opts);
  }, true);

  // Hover open/close (small)
  window.addEventListener("mouseover", function (e) {
    if (!HOVER_CAPABLE) return;

    // If entering the popup itself, keep it open
    if (isInsidePopup(e.target)) {
      if (state.open && !state.pinned) {
        clearTimeout(state.hideTimer);
        state.hideTimer = 0;
      }
      return;
    }

    var trigger = findTrigger(e.target);
    if (!trigger) {
      if (state.open && !state.pinned) hidePopup();
      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();
    }, 80);
  }, true);

  // Outside click closes (pinned always; and also closes “tap-open” popups on no-hover devices)
  window.addEventListener("click", function (e) {
    if (!state.open) return;

    if (isInsidePopup(e.target)) return;

    var trigger = findTrigger(e.target);
    if (trigger) {
      e.stopPropagation();
      return;
    }

    if (state.pinned || !HOVER_CAPABLE) {
      hidePopup();
      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);
  }
})();