Mudanças entre as edições de "Widget:Conquistas"

De Wiki Gla
Ir para navegação Ir para pesquisar
m
m
 
(8 revisões intermediárias pelo mesmo usuário não estão sendo mostradas)
Linha 6: Linha 6:
           {{#widget:Item}}
           {{#widget:Item}}
           {{#widget:Conquistas}}
           {{#widget:Conquistas}}
           {{#invoke:Conquistas|renderAll}}
           {{#invoke:Conquistas|renderTab|tab=geral}}


       Widget:Item é quem traz a CSS dos ícones (.reward-wrapper,
       Widget:Item é quem traz a CSS dos ícones (.reward-wrapper,
Linha 73: Linha 73:


         <!--
         <!--
           Painéis vazios. O Módulo:Conquistas (#invoke renderAll) emite os
           Painéis vazios. O Módulo:Conquistas (#invoke renderTab) emite os
           cards FORA do widget (Smarty não reparsa wikitext, então não dá pra
           cards FORA do widget (Smarty não reparsa wikitext, então não dá pra
           passar como parâmetro). O JS abaixo move cada card pro painel certo
           passar como parâmetro). O JS abaixo move cada card pro painel certo
Linha 80: Linha 80:
           Uso na página da wiki:
           Uso na página da wiki:
               {{#widget:Conquistas}}
               {{#widget:Conquistas}}
               {{#invoke:Conquistas|renderAll}}
               {{#invoke:Conquistas|renderTab|tab=geral}}
         -->
         -->
         <div class="gla-conquistas-panel is-active" data-tab-content="geral">
         <div class="gla-conquistas-panel is-active" data-tab-content="geral">
Linha 351: Linha 351:
             flex-direction: column;
             flex-direction: column;
             gap: 10px;
             gap: 10px;
            position: relative;
            min-height: 0;
        }
        .gla-list.gla-tab-loading {
            min-height: 88px;
            opacity: 0.55;
            pointer-events: none;
        }
        .gla-list.gla-tab-loading::after {
            content: "Carregando...";
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            font-size: 13px;
            font-weight: 600;
            color: var(--gla-ink-2);
            letter-spacing: 0.04em;
        }
        .gla-list.gla-tab-error::after {
            content: "Não foi possível carregar esta aba. Recarregue a página.";
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            width: min(320px, 90%);
            text-align: center;
            font-size: 13px;
            color: #b45309;
            line-height: 1.4;
         }
         }


Linha 363: Linha 396:
             box-shadow: var(--gla-shadow);
             box-shadow: var(--gla-shadow);
             transition: border-color 0.15s, box-shadow 0.15s;
             transition: border-color 0.15s, box-shadow 0.15s;
            content-visibility: auto;
            contain-intrinsic-size: auto 88px;
         }
         }


Linha 449: Linha 484:
         .gla-item-reward-label {
         .gla-item-reward-label {
             font-family: 'Space Grotesk', system-ui, -apple-system, sans-serif;
             font-family: 'Space Grotesk', system-ui, -apple-system, sans-serif;
             font-size: 11px;
             font-size: 14px;
             font-weight: 700;
             font-weight: 700;
             letter-spacing: 0.16em;
             letter-spacing: 0.14em;
             text-transform: uppercase;
             text-transform: uppercase;
             color: var(--gla-accent);
             color: var(--gla-accent);
Linha 458: Linha 493:


         /* Chip de itens — agora é o .reward-wrapper (emitido pelo Lua/Widget:Item).
         /* Chip de itens — agora é o .reward-wrapper (emitido pelo Lua/Widget:Item).
           Encolhe com a quantidade (min-width pra 1 item, max-width pra caber
           padding-bottom maior porque cada .item-wrapper do Widget:Item tem
           5 + chip "+M" sem wrap). */
           um .item-count posicionado em bottom:-10px (badge "x999" pendurado
          abaixo do ícone). Sem espaço extra, o badge encosta na borda. */
         .gla-item-reward .reward-wrapper {
         .gla-item-reward .reward-wrapper {
             display: block;
             display: block;
Linha 465: Linha 501:
             border: 1px solid var(--gla-rule);
             border: 1px solid var(--gla-rule);
             border-radius: 10px;
             border-radius: 10px;
             padding: 7px 12px 9px;
             padding: 9px 8px 18px;
             min-width: 72px;
             min-width: 64px;
             /* 285 = espaço pra 4 ícones (4×48 + 3×4) + chip "+M" (48) + gaps + padding. */
             /* (32 wrapper + 12 margin) + chip 32 = 220 + folga */
             max-width: 285px;
             max-width: 260px;
             width: fit-content;
             width: fit-content;
             box-sizing: border-box;
             box-sizing: border-box;
         }
         }


         /* ─── Botão spoiler (abinha no bottom-center) ────────────────────────── */
         /* Wrapper do texto — title + desc + (eventual) toggle de spoiler.
        /* Posicionado absolutamente no bottom-center do .gla-item-main.
          min-width:0 pra que o flex do .gla-item-left consiga truncar texto
           Pequeno e horizontalmente centralizado — fica no "corredor" central
           longo sem empurrar o botão pra direita. */
          livre entre o título/desc à esquerda e a recompensa à direita,
         .gla-item-text {
          sem aumentar a altura do card. */
            min-width: 0;
         .gla-item-spoiler-toggle {
            display: flex;
             position: absolute;
             flex-direction: column;
             /* main padding-left (18) + icon (52) + gap (14) = 84 → alinhado com o começo do título.
             align-items: flex-start;
              A versão anterior usava left:50% (centrado), mas o chip da recompensa
        }
              encavalava em telas mais apertadas. Ancorar à esquerda elimina a colisão. */
 
            left: 84px;
        /* ─── Botão spoiler ──────────────────────────────────────────────────── */
            bottom: 6px;
        /* Fluir abaixo da descrição, alinhado com o início do texto (não
          com o ícone). Antes era absolute em left:84px fixo e o ícone real
          da wiki (que pode ser ~100px) cobria o botão. */
        .gla-item-spoiler-toggle {
             display: inline-flex;
             display: inline-flex;
             align-items: center;
             align-items: center;
             gap: 6px;
             gap: 6px;
             margin: 0;
             margin: 8px 0 0;
             border: 1px solid var(--gla-rule);
             border: 1px solid var(--gla-rule);
             background: var(--gla-paper);
             background: var(--gla-paper);
Linha 496: Linha 535:
             color: var(--gla-ink-2);
             color: var(--gla-ink-2);
             cursor: pointer;
             cursor: pointer;
            z-index: 1;
             transition: background 0.15s, border-color 0.15s, color 0.15s;
             transition: background 0.15s, border-color 0.15s, color 0.15s;
            user-select: none;
            align-self: flex-start;
         }
         }


Linha 692: Linha 732:
         }
         }


         .gla-item[data-hidden="true"] .gla-item-main,
         /* Censura ABSOLUTA: enquanto não revelado, todo o conteúdo do card
         .gla-item[data-hidden="true"] .gla-item-title,
          fica click-through (pointer-events:none na .gla-item-main) E é
         .gla-item[data-hidden="true"] .gla-item-desc {
          coberto por um overlay transparente (::after no .gla-item) que
            transition: filter 0.25s ease, background-color 0.25s ease, color 0.25s ease;
          absorve hover/click e tira do alcance os tooltips nativos do
          browser (title/alt) — sem o overlay, alguns navegadores ainda
          exibem o tooltip de elementos com pointer-events:none.
          O ::after fica logo acima do conteúdo (z-index 6) e abaixo do
          ribbon/stamp (z-index 2). Como não tem title/alt nem children,
          hover sobre ele não gera tooltip nenhum. Click bubbla pro
          .gla-item e o listener global revela. */
        .gla-conquistas-root[data-reveal-mode="blur"] .gla-item[data-hidden="true"]:not(.is-revealed) .gla-item-main,
         .gla-conquistas-root[data-reveal-mode="redacted"] .gla-item[data-hidden="true"]:not(.is-revealed) .gla-item-main,
         .gla-conquistas-root[data-reveal-mode="placeholder"] .gla-item[data-hidden="true"]:not(.is-revealed) .gla-item-main,
        .gla-conquistas-root[data-reveal-mode="veil"] .gla-item[data-hidden="true"]:not(.is-revealed) .gla-item-main {
            pointer-events: none;
         }
         }


         /* — BLUR
        .gla-conquistas-root[data-reveal-mode="blur"] .gla-item[data-hidden="true"]:not(.is-revealed),
           Aplicado no .gla-item-main (pai que envolve icon + title + desc +
        .gla-conquistas-root[data-reveal-mode="redacted"] .gla-item[data-hidden="true"]:not(.is-revealed),
           reward + spoiler-toggle) — borra TUDO de uma vez. Antes só borrava
        .gla-conquistas-root[data-reveal-mode="placeholder"] .gla-item[data-hidden="true"]:not(.is-revealed),
           título e descrição, mas o ícone da conquista também entregava a
        .gla-conquistas-root[data-reveal-mode="veil"] .gla-item[data-hidden="true"]:not(.is-revealed) {
           secreta. O ribbon "SECRETA" tá no ::before do .gla-item (fora do
            pointer-events: auto;
           main), então continua nítido. */
            position: relative;
        }
 
        /* Overlay transparente que captura hover/click — apenas em blur,
          redacted e placeholder. (Veil tem o seu próprio ::after estilizado
          logo abaixo, e o seletor mais específico de baixo sobrescreve.) */
        .gla-conquistas-root[data-reveal-mode="blur"] .gla-item[data-hidden="true"]:not(.is-revealed)::after,
        .gla-conquistas-root[data-reveal-mode="redacted"] .gla-item[data-hidden="true"]:not(.is-revealed)::after,
        .gla-conquistas-root[data-reveal-mode="placeholder"] .gla-item[data-hidden="true"]:not(.is-revealed)::after {
            content: "";
            position: absolute;
            inset: 0;
            background: transparent;
            pointer-events: auto;
            z-index: 6;
        }
 
        /* Anula QUALQUER interação dentro do card censurado: tooltip dos
          rewards (JS do Widget:Item), tooltip nativo de alt/title em imagens,
          seleção de texto, hover de links na descrição. pointer-events:none
          torna os filhos "transparentes" pra eventos do mouse — o click sobe
          pro .gla-item pai, que dispara o reveal. */
        .gla-conquistas-root[data-reveal-mode="blur"] .gla-item[data-hidden="true"]:not(.is-revealed) .gla-item-main,
        .gla-conquistas-root[data-reveal-mode="redacted"] .gla-item[data-hidden="true"]:not(.is-revealed) .gla-item-main,
        .gla-conquistas-root[data-reveal-mode="placeholder"] .gla-item[data-hidden="true"]:not(.is-revealed) .gla-item-main,
        .gla-conquistas-root[data-reveal-mode="veil"] .gla-item[data-hidden="true"]:not(.is-revealed) .gla-item-main {
            pointer-events: none;
            user-select: none;
            -webkit-user-select: none;
        }
 
        .gla-item[data-hidden="true"] .gla-item-main,
        .gla-item[data-hidden="true"] .gla-item-title,
        .gla-item[data-hidden="true"] .gla-item-desc {
            transition: filter 0.25s ease, background-color 0.25s ease, color 0.25s ease;
        }
 
         /* — BLUR
           Aplicado no .gla-item-main (pai que envolve icon + title + desc +
           reward + spoiler-toggle) — borra TUDO de uma vez. Antes só borrava
           título e descrição, mas o ícone da conquista também entregava a
           secreta. O ribbon "SECRETA" tá no ::before do .gla-item (fora do
           main), então continua nítido. */
         .gla-conquistas-root[data-reveal-mode="blur"] .gla-item[data-hidden="true"]:not(.is-revealed) .gla-item-main {
         .gla-conquistas-root[data-reveal-mode="blur"] .gla-item[data-hidden="true"]:not(.is-revealed) .gla-item-main {
             filter: blur(5px);
             filter: blur(5px);
Linha 772: Linha 865:
             backdrop-filter: blur(4px);
             backdrop-filter: blur(4px);
             -webkit-backdrop-filter: blur(4px);
             -webkit-backdrop-filter: blur(4px);
             z-index: 4;
             z-index: 6;
             pointer-events: none;
            /* auto — overlay captura hover/click. Sem isso, mesmo coberto
              visualmente, os items embaixo ainda recebiam hover e o browser
              mostrava tooltip nativo (title/alt). */
             pointer-events: auto;
             transition: opacity 0.25s ease;
             transition: opacity 0.25s ease;
         }
         }
Linha 917: Linha 1 013:
         }
         }


        .gla-conquistas-root[data-hidden-style="ribbon"] .gla-item[data-hidden="true"] .gla-item-main {
         /* ─── Variante D — STAMP ─────────────────────────────────────────────── */
            padding-left: 38px;
        }
 
        @media (max-width: 720px) {
            .gla-conquistas-root[data-hidden-style="ribbon"] .gla-item[data-hidden="true"] .gla-item-main {
                padding-left: 38px;
            }
        }
 
         /* ─── Variante D — STAMP ─────────────────────────────────────────────── */
         .gla-conquistas-root[data-hidden-style="stamp"] .gla-item[data-hidden="true"]::before {
         .gla-conquistas-root[data-hidden-style="stamp"] .gla-item[data-hidden="true"]::before {
             content: "OCULTA";
             content: "OCULTA";
Linha 999: Linha 1 085:
             display: flex !important;
             display: flex !important;
             /* nowrap = trava a altura. O JS abaixo move itens excedentes pra
             /* nowrap = trava a altura. O JS abaixo move itens excedentes pra
               dentro do chip "+M" depois do . */
               dentro do chip "+M" depois do MAX_VISIBLE-ésimo. */
             flex-wrap: nowrap !important;
             flex-wrap: nowrap !important;
             justify-content: center;
             justify-content: flex-start;
             gap: 4px;
            align-items: flex-start;
             gap: 0;
             width: 100%;
             width: 100%;
         }
         }


         /* ─── Chip "+M" (overflow) ─────────────────────────────────────────── */
         /* ─── Chip "+M" (overflow) ─────────────────────────────────────────── */
         /* Mesmo tamanho/forma do .item-wrapper pra alinhar visualmente com os
         /* Botão de overflow com reticências (⋯). Círculo 32×32 sutil — mesma
           ícones, mas com texto "+N" no centro. Click abre o popover com os
          altura dos sprites pra encaixar na linha, mas borda/fundo accent
          itens escondidos. */
          pra ler como CTA, não como outro slot de item. margin-top: 5px
          alinha com o centro visual do conjunto sprite+badge dos rewards
          (o badge .item-count fica em bottom:-10px do .item-wrapper, então
           o "centro real" do item fica ~5px abaixo do centro do sprite). */
         .gla-item-reward .reward-more-chip {
         .gla-item-reward .reward-more-chip {
             display: inline-flex;
             display: inline-flex;
             align-items: center;
             align-items: center;
             justify-content: center;
             justify-content: center;
             width: 48px;
             width: 32px;
             height: 48px;
             height: 32px;
             padding: 0;
            margin: 5px 6px 0;
             border: 1px solid var(--gla-rule);
             padding: 0 0 4px;
             border-radius: 8px;
             border: 1px solid var(--gla-accent-soft);
             background: var(--gla-paper);
             border-radius: 999px;
             background: var(--gla-accent-bg);
             color: var(--gla-accent);
             color: var(--gla-accent);
             font-family: 'Space Grotesk', system-ui, -apple-system, sans-serif;
             font-family: 'Space Grotesk', system-ui, -apple-system, sans-serif;
             font-size: 18px;
             font-size: 22px;
             font-weight: 700;
             font-weight: 700;
             line-height: 1;
             line-height: 1;
             letter-spacing: -0.02em;
             letter-spacing: 0;
             cursor: pointer;
             cursor: pointer;
             flex-shrink: 0;
             flex-shrink: 0;
            box-sizing: border-box;
             transition: background 0.15s, border-color 0.15s, color 0.15s;
             transition: background 0.15s, border-color 0.15s, color 0.15s;
         }
         }
Linha 1 033: Linha 1 125:
         .gla-item-reward .reward-more-chip:hover,
         .gla-item-reward .reward-more-chip:hover,
         .gla-item-reward .reward-more-chip[aria-expanded="true"] {
         .gla-item-reward .reward-more-chip[aria-expanded="true"] {
             background: var(--gla-accent-bg);
             background: var(--gla-accent);
             border-color: var(--gla-accent-soft);
             border-color: var(--gla-accent);
            color: #ffffff;
         }
         }


         /* Popover ancorado no chip — drop-down dos itens escondidos. */
         /* Popover ancorado no chip via JS vive em document.body com
         .gla-item-reward .reward-overflow-popover {
          position:fixed pra escapar o overflow:hidden de cards com ribbon
             position: absolute;
          (e qualquer outro contexto de clipping). Tooltips dos itens
            top: calc(100% + 6px);
          dentro saem livremente. */
            right: 0;
         .reward-overflow-popover {
             position: fixed;
             display: flex;
             display: flex;
             flex-wrap: wrap;
             flex-wrap: wrap;
             justify-content: flex-end;
             justify-content: flex-start;
             gap: 4px;
            align-items: flex-start;
             padding: 8px;
             gap: 0;
             background: var(--gla-paper);
            /* padding-bottom extra pelo mesmo motivo do .reward-wrapper:
             border: 1px solid var(--gla-rule);
              .item-count fica em bottom:-10px e precisa de espaço. */
             padding: 10px 6px 18px;
             background: var(--gla-paper, #ffffff);
             border: 1px solid var(--gla-rule, #e2e8f0);
             border-radius: 10px;
             border-radius: 10px;
             box-shadow: 0 6px 18px -4px rgba(15, 23, 42, 0.15),
             box-shadow: 0 6px 18px -4px rgba(15, 23, 42, 0.18),
                 0 2px 6px -2px rgba(15, 23, 42, 0.08);
                 0 2px 6px -2px rgba(15, 23, 42, 0.08);
             z-index: 10;
             z-index: 9999;
             max-width: 268px;
             max-width: 260px;
             animation: gla-popover-in 0.18s ease;
             animation: gla-popover-in 0.18s ease;
         }
         }


         .gla-item-reward .reward-overflow-popover[hidden] {
         .reward-overflow-popover[hidden] {
             display: none;
             display: none;
         }
         }
Linha 1 099: Linha 1 196:
             }
             }


             /* No mobile o stack vertical empurra o "bottom" do .gla-item-main
             /* No mobile o spoiler já flui dentro do .gla-item-text, só
               pra cima da recompensa — o spoiler absoluto colidiria com o chip.
               reduz o padding pra ficar levemente mais compacto. */
              Saindo do absoluto e deixando ele fluir como 3º item do flex-wrap. */
             .gla-item-spoiler-toggle {
             .gla-item-spoiler-toggle {
                position: static;
                transform: none;
                align-self: flex-start;
                margin: 4px 0 0;
                 padding: 2px 11px 3px;
                 padding: 2px 11px 3px;
             }
             }
Linha 1 140: Linha 1 232:


             var validTabs = {};
             var validTabs = {};
            var panelByTab = {};
            var tabByName = {};
             panels.forEach(function (p) {
             panels.forEach(function (p) {
                 var name = p.getAttribute("data-tab-content");
                 var name = p.getAttribute("data-tab-content");
                 if (name) validTabs[name] = p.querySelector(".gla-list");
                 if (name) {
                    validTabs[name] = p.querySelector(".gla-list");
                    panelByTab[name] = p;
                }
             });
             });
            tabs.forEach(function (t) {
                var name = t.getAttribute("data-tab");
                if (name) tabByName[name] = t;
            });
            function normalizeTabId(tab) {
                return (tab || "").toLowerCase();
            }


             // Move cards do invoke source para os painéis corretos
            function getCardTab(card) {
                var tag = normalizeTabId(card.getAttribute("data-tab"));
                return validTabs[tag] ? tag : "geral";
            }
 
            function mountCardToPanel(card) {
                if (!card) return;
                var tag = getCardTab(card);
                var list = validTabs[tag];
                if (list) list.appendChild(card);
            }
 
            function redistributeCardsToPanels() {
                root.querySelectorAll(".gla-item[data-tab]").forEach(function (card) {
                    var list = validTabs[getCardTab(card)];
                    if (list && card.parentNode !== list) list.appendChild(card);
                });
            }
 
             // Move cards do invoke source para os painéis corretos (sempre por data-tab)
             document.querySelectorAll(".gla-item[data-tab]").forEach(function (card) {
             document.querySelectorAll(".gla-item[data-tab]").forEach(function (card) {
                 if (root.contains(card)) return;
                 if (root.contains(card)) return;
                 var tag = (card.getAttribute("data-tab") || "").toLowerCase();
                 mountCardToPanel(card);
                var list = validTabs[tag] || validTabs["geral"];
                if (list) list.appendChild(card);
             });
             });


Linha 1 157: Linha 1 279:
             });
             });


             // Estado global
             // Cache por aba pra evitar querySelectorAll em toda busca/filtro.
             var currentSearch = "";
             var cardsByTab = {};
             var currentFilter = "all";
             var hiddenCountByTab = {};
            var currentTab = "geral";
            var searchTimeout = null;


             var filterDefault = root.querySelector("#gla-filter-default");
             function buildTabCaches() {
            var filterColiseu = root.querySelector("#gla-filter-coliseu");
                redistributeCardsToPanels();
            var searchInput = root.querySelector(".gla-conquistas-search");
                cardsByTab = {};
 
                hiddenCountByTab = {};
            function countHiddenInTab(tabName) {
                Object.keys(validTabs).forEach(function (tabName) {
                var panel = root.querySelector('.gla-conquistas-panel[data-tab-content="' + tabName + '"]');
                    var list = validTabs[tabName];
                if (!panel) return 0;
                    if (!list) {
                var n = 0;
                        cardsByTab[tabName] = [];
                panel.querySelectorAll(".gla-item").forEach(function (card) {
                        hiddenCountByTab[tabName] = 0;
                    if (card.getAttribute("data-hidden") === "true") n++;
                        return;
                    }
                    var cards = [];
                    var hiddenCount = 0;
                    for (var i = 0; i < list.children.length; i++) {
                        var el = list.children[i];
                        if (!(el.classList && el.classList.contains("gla-item"))) continue;
                        if (getCardTab(el) !== tabName) continue;
                        cards.push(el);
                        if (el.getAttribute("data-hidden") === "true") hiddenCount++;
                    }
                    cardsByTab[tabName] = cards;
                    hiddenCountByTab[tabName] = hiddenCount;
                 });
                 });
                return n;
             }
             }
            buildTabCaches();


             // Mostra a barra de filtros só quando faz sentido: na aba Coliseu mostra
             var REVEAL_STORAGE_KEY = "glaConquistasRevealed";
            // o subset (One Man Army / Corrida). Nas outras, só mostra o filtro
            // normal/hidden se a aba tiver pelo menos uma conquista oculta —
            // se não tiver, esconde pra não poluir.
            function syncFilterBarForTab(tabName) {
                var hiddenCount = countHiddenInTab(tabName);


                if (filterColiseu) {
            function loadRevealed() {
                     filterColiseu.style.display = tabName === "coliseu" ? "" : "none";
                try {
                }
                     var raw = window.localStorage.getItem(REVEAL_STORAGE_KEY);
                if (filterDefault) {
                    if (!raw) return {};
                     if (tabName === "coliseu") {
                    var arr = JSON.parse(raw);
                        filterDefault.style.display = "none";
                     if (!Array.isArray(arr)) return {};
                     } else {
                    var set = {};
                         filterDefault.style.display = hiddenCount > 0 ? "" : "none";
                     for (var i = 0; i < arr.length; i++) {
                         if (arr[i] != null) set[String(arr[i])] = true;
                     }
                     }
                    return set;
                } catch (e) {
                    return {};
                 }
                 }
            }
            var revealedSet = loadRevealed();


                if (tabName !== "coliseu" && hiddenCount === 0) {
            // Abas já com cards no HTML (ex.: renderAll legado) ficam marcadas.
                    if (currentFilter === "hidden" || currentFilter === "normal") {
            // Com renderTab|tab=geral só "geral" vem na primeira carga.
                        currentFilter = "all";
            var loadedTabs = {};
                    }
            var loadingTabs = {};
                    if (filterDefault) {
            Object.keys(validTabs).forEach(function (tabName) {
                        filterDefault.querySelectorAll(".gla-conquistas-filter").forEach(function (p) {
                var list = validTabs[tabName];
                            p.classList.toggle("is-active", p.getAttribute("data-filter") === "all");
                if (!list) return;
                         });
                for (var i = 0; i < list.children.length; i++) {
                    var el = list.children[i];
                    if (el.classList && el.classList.contains("gla-item") && getCardTab(el) === tabName) {
                        loadedTabs[tabName] = true;
                         break;
                     }
                     }
                 }
                 }
            });
            function getApiUrl() {
                if (window.mw && mw.util && mw.util.wikiScript) {
                    return mw.util.wikiScript("api");
                }
                var path = window.location.pathname || "";
                if (path.indexOf("/index.php") !== -1) {
                    return path.split("/index.php")[0] + "/api.php";
                }
                return "/api.php";
            }
            function fetchParse(wikitext) {
                if (window.mw && mw.Api) {
                    return new mw.Api().post({
                        action: "parse",
                        text: wikitext,
                        contentmodel: "wikitext",
                        disablelimitreport: true
                    }).then(function (data) {
                        return data.parse.text["*"];
                    });
                }
                var body = new URLSearchParams();
                body.set("action", "parse");
                body.set("format", "json");
                body.set("text", wikitext);
                body.set("contentmodel", "wikitext");
                body.set("disablelimitreport", "1");
                return fetch(getApiUrl(), {
                    method: "POST",
                    credentials: "same-origin",
                    headers: { "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8" },
                    body: body.toString()
                })
                    .then(function (res) { return res.json(); })
                    .then(function (data) {
                        if (data.error) throw new Error(data.error.info || "API parse failed");
                        return data.parse.text["*"];
                    });
             }
             }


             function applyVisibility() {
             function fetchTabHtml(tabName) {
                 var panel = root.querySelector('.gla-conquistas-panel[data-tab-content="' + currentTab + '"]');
                 return fetchParse("{{#invoke:Conquistas|renderTab|tab=" + tabName + "}}");
                if (!panel) return;
            }
                var search = currentSearch.toLowerCase();
 
                panel.querySelectorAll(".gla-item").forEach(function (card) {
            var prefetchRestPromise = null;
                    var titleEl = card.querySelector(".gla-item-title");
            var prefetchRestStarted = false;
                    var descEl = card.querySelector(".gla-item-desc");
                    var titleText = titleEl ? titleEl.textContent : "";
                    var descText = descEl ? descEl.textContent : "";
                    var matchSearch = !search ||
                        titleText.toLowerCase().indexOf(search) >= 0 ||
                        descText.toLowerCase().indexOf(search) >= 0;


                     var hidden = card.getAttribute("data-hidden") === "true";
            function runRewardsDeferred(rootEl) {
                     var subtype = (card.getAttribute("data-subtype") || "").toLowerCase();
                if (!window.glaConquistasProcessRewards) return;
                    var matchFilter = true;
                var run = function () { window.glaConquistasProcessRewards(rootEl); };
                     if (currentFilter === "normal") matchFilter = !hidden;
                if (window.requestIdleCallback) {
                    else if (currentFilter === "hidden") matchFilter = hidden;
                    requestIdleCallback(run, { timeout: 1000 });
                    else if (currentFilter === "onemany") matchFilter = subtype === "onemany";
                } else {
                    else if (currentFilter === "corrida") matchFilter = subtype === "corrida";
                    setTimeout(run, 0);
                }
            }
 
            function applyRevealedToCards(cards) {
                if (!cards || !cards.length) return;
                for (var i = 0; i < cards.length; i++) {
                     var card = cards[i];
                    if (card.getAttribute("data-hidden") !== "true") continue;
                     var id = card.getAttribute("data-id");
                     if (id && revealedSet[id]) card.classList.add("is-revealed");
                }
            }
 
            function mountCardsFromHtml(html, tabName) {
                var list = validTabs[tabName];
                if (!list || !html) return;
 
                var tmp = document.createElement("div");
                tmp.innerHTML = html;


                     card.style.display = (matchSearch && matchFilter) ? "" : "none";
                var moved = [];
                tmp.querySelectorAll(".gla-item").forEach(function (card) {
                     if (getCardTab(card) !== tabName) {
                        mountCardToPanel(card);
                        return;
                    }
                    list.appendChild(card);
                    moved.push(card);
                 });
                 });
                applyRevealedToCards(moved);
                loadedTabs[tabName] = true;
                buildTabCaches();
                runRewardsDeferred(list);
             }
             }


             // Busca com debounce
             function mountBundleFromHtml(html) {
            if (searchInput) {
                if (!html) return;
                 searchInput.addEventListener("input", function () {
                var tmp = document.createElement("div");
                     clearTimeout(searchTimeout);
                tmp.innerHTML = html;
                     searchTimeout = setTimeout(function () {
                 tmp.querySelectorAll(".gla-conquistas-source[data-gla-tab]").forEach(function (src) {
                         currentSearch = searchInput.value.trim();
                     var tabName = src.getAttribute("data-gla-tab");
                         applyVisibility();
                     if (!tabName || !validTabs[tabName]) return;
                     }, 240);
                    var list = validTabs[tabName];
                    var moved = [];
                    src.querySelectorAll(".gla-item").forEach(function (card) {
                         if (getCardTab(card) !== tabName) {
                            mountCardToPanel(card);
                            return;
                        }
                        list.appendChild(card);
                         moved.push(card);
                     });
                    applyRevealedToCards(moved);
                    loadedTabs[tabName] = true;
                 });
                 });
                buildTabCaches();
                runRewardsDeferred(root);
             }
             }


             // Filtros (normal / oculta / subtype)
             function startPrefetchRest() {
             root.querySelectorAll(".gla-conquistas-filter").forEach(function (pill) {
                if (prefetchRestStarted) return prefetchRestPromise;
                pill.addEventListener("click", function () {
                prefetchRestStarted = true;
                    pill.parentNode.querySelectorAll(".gla-conquistas-filter").forEach(function (p) {
                prefetchRestPromise = fetchParse("{{#invoke:Conquistas|renderRest|skip=geral}}")
                         p.classList.remove("is-active");
                    .then(function (html) {
                        mountBundleFromHtml(html);
                    })
                    .catch(function () { /* falha silenciosa — aba carrega sob demanda */ });
                return prefetchRestPromise;
            }
 
            function schedulePrefetchRest() {
                var run = function () { startPrefetchRest(); };
                if (window.requestIdleCallback) {
                    requestIdleCallback(run, { timeout: 2000 });
                } else {
                    setTimeout(run, 350);
                }
             }
 
            function loadTabNow(tabName) {
                var list = validTabs[tabName];
                if (list) {
                    list.classList.remove("gla-tab-error");
                    list.classList.add("gla-tab-loading");
                }
                return fetchTabHtml(tabName)
                    .then(function (html) {
                        mountCardsFromHtml(html, tabName);
                        if (list) list.classList.remove("gla-tab-loading");
                    })
                    .catch(function () {
                         if (list) {
                            list.classList.remove("gla-tab-loading");
                            list.classList.add("gla-tab-error");
                        }
                     });
                     });
                    pill.classList.add("is-active");
            }
                    currentFilter = pill.getAttribute("data-filter");
 
                    applyVisibility();
            function ensureTabLoaded(tabName) {
                 });
                if (loadedTabs[tabName]) return Promise.resolve();
            });
                 if (loadingTabs[tabName]) return loadingTabs[tabName];


            // Spoiler — abre/fecha só no botão "Spoiler"
                if (prefetchRestPromise) {
            root.addEventListener("click", function (e) {
                    loadingTabs[tabName] = prefetchRestPromise.then(function () {
                var toggle = e.target.closest(".gla-item-spoiler-toggle");
                        if (loadedTabs[tabName]) return;
                if (!toggle) return;
                        return loadTabNow(tabName);
                if (e.target.closest(".item-wrapper")) return;
                    });
                var card = toggle.closest(".gla-item.has-spoiler");
                    return loadingTabs[tabName];
                if (!card) return;
                 }
                 e.preventDefault();


                 var isOpen = card.classList.contains("is-open");
                 loadingTabs[tabName] = loadTabNow(tabName).then(function () {
                root.querySelectorAll(".gla-item.is-open").forEach(function (c) {
                     delete loadingTabs[tabName];
                     c.classList.remove("is-open");
                 });
                 });
                 root.querySelectorAll(".gla-item-spoiler-toggle").forEach(function (btn) {
                 return loadingTabs[tabName];
                    btn.setAttribute("aria-expanded", "false");
            }
                });
 
                if (!isOpen) {
            // Estado global
                    card.classList.add("is-open");
            var currentSearch = "";
                    toggle.setAttribute("aria-expanded", "true");
            var currentFilter = "all";
                }
            var currentTab = "geral";
             });
             var searchTimeout = null;


             // ─── Reveal + persistência ────────────────────────────────────────
             var filterDefault = root.querySelector("#gla-filter-default");
            // Click no card hidden remove a censura (anti-spoiler) e o estado
             var filterColiseu = root.querySelector("#gla-filter-coliseu");
             // é salvo no localStorage. Próxima visita à página, conquistas que
             var searchInput = root.querySelector(".gla-conquistas-search");
            // o jogador já revelou voltam reveladas — não precisa clicar de novo.
            //
            // Chave: "glaConquistasRevealed" → JSON com array de data-id.
            // Robusto a localStorage indisponível (modo privado, cookies
            // bloqueados) — só falha silenciosamente.
             var REVEAL_STORAGE_KEY = "glaConquistasRevealed";


             function loadRevealed() {
             function countHiddenInTab(tabName) {
                 try {
                 return hiddenCountByTab[tabName] || 0;
                    var raw = window.localStorage.getItem(REVEAL_STORAGE_KEY);
            }
                    if (!raw) return {};
 
                     var arr = JSON.parse(raw);
            // Mostra a barra de filtros só quando faz sentido: na aba Coliseu mostra
                     if (!Array.isArray(arr)) return {};
            // o subset (One Man Army / Corrida). Nas outras, só mostra o filtro
                    var set = {};
            // normal/hidden se a aba tiver pelo menos uma conquista oculta —
                     for (var i = 0; i < arr.length; i++) {
            // se não tiver, esconde pra não poluir.
                         if (arr[i] != null) set[String(arr[i])] = true;
            function syncFilterBarForTab(tabName) {
                var hiddenCount = countHiddenInTab(tabName);
 
                if (filterColiseu) {
                     filterColiseu.style.display = tabName === "coliseu" ? "" : "none";
                }
                if (filterDefault) {
                     if (tabName === "coliseu") {
                        filterDefault.style.display = "none";
                     } else {
                         filterDefault.style.display = hiddenCount > 0 ? "" : "none";
                     }
                     }
                    return set;
                } catch (e) {
                    return {};
                 }
                 }
            }


            function saveRevealed(set) {
                if (tabName !== "coliseu" && hiddenCount === 0) {
                try {
                    if (currentFilter === "hidden" || currentFilter === "normal") {
                     var arr = Object.keys(set);
                        currentFilter = "all";
                    window.localStorage.setItem(REVEAL_STORAGE_KEY, JSON.stringify(arr));
                    }
                 } catch (e) { /* localStorage indisponível — ok */ }
                     if (filterDefault) {
                        filterDefault.querySelectorAll(".gla-conquistas-filter").forEach(function (p) {
                            p.classList.toggle("is-active", p.getAttribute("data-filter") === "all");
                        });
                    }
                 }
            }
 
            // Normalização pra busca: lowercase + remove acentos (NFD divide
            // base+diacrítico, regex remove os diacríticos). Assim "voce"
            // acha "você", "missao" acha "missão", etc.
            function normalize(s) {
                return (s || "")
                    .toLowerCase()
                    .normalize("NFD")
                    .replace(/[̀-ͯ]/g, "");
             }
             }


             var revealedSet = loadRevealed();
             // Cacheia o "haystack" (title + desc normalizados) no próprio card
            // pra não recalcular a cada keystroke. Recomputa só se o conteúdo
            // do card mudar (não acontece na vida do widget — é estático).
            function getHaystack(card) {
                var cached = card.__glaHaystack;
                if (cached) return cached;
                var titleEl = card.querySelector(".gla-item-title");
                var descEl = card.querySelector(".gla-item-desc");
                var titleText = titleEl ? titleEl.textContent : "";
                var descText = descEl ? descEl.textContent : "";
                cached = normalize(titleText + " " + descText);
                card.__glaHaystack = cached;
                return cached;
            }


             // Re-aplica .is-revealed nos cards que já tavam revelados em
             function applyVisibility() {
            // sessões anteriores. Roda agora (depois do JS já ter movido os
                var cards = cardsByTab[currentTab] || [];
            // cards do source pros painéis).
                 if (!cards.length) return;
            root.querySelectorAll('.gla-item[data-hidden="true"][data-id]').forEach(function (card) {
                 if (revealedSet[card.getAttribute("data-id")]) {
                    card.classList.add("is-revealed");
                }
            });


            // Click — revela e persiste. Só ativa se data-reveal-mode no root
                // Token-based search: divide a query em palavras e exige que
            // estiver definido como blur/redacted/placeholder/veil. Ignora
                // TODAS apareçam no haystack (title + desc), em qualquer
            // clicks em interativos dentro do card (botão de spoiler, ícones
                // ordem. "voce grand" → tokens ["voce", "grand"] → casa com
            // de reward com tooltip, links na descrição, chip "+N" de overflow).
                 // "Você entrou na Grand Line pela primeira vez". Acentos e
            root.addEventListener("click", function (e) {
                 // case são ignorados via normalize().
                var mode = root.getAttribute("data-reveal-mode") || "none";
                 var tokens = normalize(currentSearch).split(/\s+/).filter(Boolean);
                 if (mode === "none") return;
                var card = e.target.closest('.gla-item[data-hidden="true"]');
                 if (!card) return;
                 if (card.classList.contains("is-revealed")) return;
                if (e.target.closest(".gla-item-spoiler-toggle, .item-wrapper, a, .reward-more-chip, .reward-overflow-popover")) return;
                card.classList.add("is-revealed");


                 var id = card.getAttribute("data-id");
                 cards.forEach(function (card) {
                if (id) {
                    if (getCardTab(card) !== currentTab) {
                    revealedSet[id] = true;
                        card.style.display = "none";
                    saveRevealed(revealedSet);
                        return;
                }
                    }
            });


            function abrirAba(nome) {
                    var matchSearch = true;
                currentTab = nome;
                    if (tokens.length > 0) {
                currentFilter = "all";
                        var hay = getHaystack(card);
                        for (var i = 0; i < tokens.length; i++) {
                            if (hay.indexOf(tokens[i]) === -1) { matchSearch = false; break; }
                        }
                    }


                tabs.forEach(function (t) { t.classList.remove("is-active"); });
                    var hidden = card.getAttribute("data-hidden") === "true";
                panels.forEach(function (p) { p.classList.remove("is-active"); });
                    var subtype = (card.getAttribute("data-subtype") || "").toLowerCase();
 
                    var matchFilter = true;
                var tab = root.querySelector('.gla-conquistas-tab[data-tab="' + nome + '"]');
                    if (currentFilter === "normal") matchFilter = !hidden;
                var panel = root.querySelector('.gla-conquistas-panel[data-tab-content="' + nome + '"]');
                    else if (currentFilter === "hidden") matchFilter = hidden;
                if (tab) tab.classList.add("is-active");
                    else if (currentFilter === "onemany") matchFilter = subtype === "onemany";
                if (panel) panel.classList.add("is-active");
                    else if (currentFilter === "corrida") matchFilter = subtype === "corrida";


                syncFilterBarForTab(nome);
                     card.style.display = (matchSearch && matchFilter) ? "" : "none";
 
                root.querySelectorAll(".gla-conquistas-filter").forEach(function (p) {
                     if (p.offsetParent !== null && p.style.display !== "none") {
                        p.classList.toggle("is-active", p.getAttribute("data-filter") === "all");
                    }
                 });
                 });
                applyVisibility();
             }
             }


             tabs.forEach(function (tab) {
             // Busca com debounce
                 tab.addEventListener("click", function () {
            if (searchInput) {
                     abrirAba(tab.getAttribute("data-tab"));
                 searchInput.addEventListener("input", function () {
                     clearTimeout(searchTimeout);
                    searchTimeout = setTimeout(function () {
                        currentSearch = searchInput.value.trim();
                        applyVisibility();
                    }, 240);
                 });
                 });
             });
             }


             abrirAba("geral");
             // Filtros (normal / oculta / subtype)
        });
            root.querySelectorAll(".gla-conquistas-filter").forEach(function (pill) {
    </script>
                pill.addEventListener("click", function () {
                    pill.parentNode.querySelectorAll(".gla-conquistas-filter").forEach(function (p) {
                        p.classList.remove("is-active");
                    });
                    pill.classList.add("is-active");
                    currentFilter = pill.getAttribute("data-filter");
                    applyVisibility();
                });
            });


    <!-- ─── Overflow das recompensas: "+M" chip + popover ─────────────────────
            // Spoiler — abre/fecha só no botão "Spoiler".
        Para cada .reward-items com mais de MAX itens, esconde do (MAX+1)º em
            // O Lua emite <span role="button"> (MediaWiki não aceita <button>
        diante, joga os escondidos num popover, e planta um chip "+M" no fim
            // em wikitext), por isso aceitamos click + Enter/Space pra
        da linha. Clique no chip abre o popover ancorado abaixo do chip.
            // preservar semântica de botão acessível.
        Click fora fecha. Esc fecha.
            var openSpoilerCard = null;
        É responsabilidade do widget — o Lua não precisa mudar, já que
             var openSpoilerToggle = null;
        .reward-items vem do Widget:Item igual à Predefinição:Reward.
    ─────────────────────────────────────────────────────────────────────── -->
    <script>
        (function () {
             var MAX_VISIBLE = 4;


             function processOne(reward) {
             function closeOpenSpoiler() {
                 if (reward.dataset.glaOverflow === "done") return;
                 if (openSpoilerCard) {
                 var line = reward.querySelector(".reward-items");
                    openSpoilerCard.classList.remove("is-open");
                 if (!line) return;
                    openSpoilerCard = null;
                 }
                if (openSpoilerToggle) {
                    openSpoilerToggle.setAttribute("aria-expanded", "false");
                    openSpoilerToggle = null;
                 }
            }


                var items = [];
            function toggleSpoiler(toggle) {
                for (var i = 0; i < line.children.length; i++) {
                var card = toggle.closest(".gla-item.has-spoiler");
                    var c = line.children[i];
                if (!card) return;
                    if (c.classList && c.classList.contains("item-wrapper")) items.push(c);
                var isOpen = (openSpoilerCard === card);
                 }
                 closeOpenSpoiler();
                 if (items.length <= MAX_VISIBLE) {
                 if (!isOpen) {
                     reward.dataset.glaOverflow = "done";
                     card.classList.add("is-open");
                     return;
                    toggle.setAttribute("aria-expanded", "true");
                    openSpoilerCard = card;
                     openSpoilerToggle = toggle;
                 }
                 }
            }


                var hidden = items.slice(MAX_VISIBLE);
            root.addEventListener("click", function (e) {
 
                 var toggle = e.target.closest(".gla-item-spoiler-toggle");
                 var popover = document.createElement("div");
                 if (!toggle) return;
                 popover.className = "reward-overflow-popover";
                if (e.target.closest(".item-wrapper")) return;
                 popover.hidden = true;
                 e.preventDefault();
                 hidden.forEach(function (el) { popover.appendChild(el); });
                 toggleSpoiler(toggle);
            });


                var chip = document.createElement("button");
            root.addEventListener("keydown", function (e) {
                 chip.type = "button";
                 if (e.key !== "Enter" && e.key !== " ") return;
                chip.className = "reward-more-chip";
                 var toggle = e.target.closest(".gla-item-spoiler-toggle");
                 chip.setAttribute("aria-expanded", "false");
                 if (!toggle) return;
                 chip.setAttribute("aria-label", "Ver mais " + hidden.length + " recompensa(s)");
                 e.preventDefault();
                 chip.textContent = "+" + hidden.length;
                 toggleSpoiler(toggle);
                 line.appendChild(chip);
            });


                 reward.appendChild(popover);
            // ─── Reveal + persistência ────────────────────────────────────────
            function saveRevealed(set) {
                 try {
                    var arr = Object.keys(set);
                    window.localStorage.setItem(REVEAL_STORAGE_KEY, JSON.stringify(arr));
                } catch (e) { /* localStorage indisponível — ok */ }
            }


                 chip.addEventListener("click", function (e) {
            // Re-aplica .is-revealed nos cards que já tavam revelados em
                     e.stopPropagation();
            // sessões anteriores. Roda agora (depois do JS já ter movido os
                     var open = chip.getAttribute("aria-expanded") === "true";
            // cards do source pros painéis).
            root.querySelectorAll('.gla-item[data-hidden="true"][data-id]').forEach(function (card) {
                if (revealedSet[card.getAttribute("data-id")]) {
                    card.classList.add("is-revealed");
                }
            });
 
            // Click — revela e persiste. Só ativa se data-reveal-mode no root
            // estiver definido como blur/redacted/placeholder/veil.
            //
            // O CSS aplica `pointer-events: none` no .gla-item-main enquanto
            // a censura está ativa — então click em qualquer ponto interno
            // (icon/title/desc/items/spoiler/chip+N) cai direto no .gla-item.
            // Aqui não precisamos mais filtrar interativos: enquanto censurado,
            // tudo é "click pra revelar"; depois que revela, os filtros internos
            // voltam ao normal porque o seletor :not(.is-revealed) deixa de bater.
            root.addEventListener("click", function (e) {
                var mode = root.getAttribute("data-reveal-mode") || "none";
                if (mode === "none") return;
                var card = e.target.closest('.gla-item[data-hidden="true"]');
                if (!card) return;
                if (card.classList.contains("is-revealed")) return;
                card.classList.add("is-revealed");
 
                var id = card.getAttribute("data-id");
                if (id) {
                    revealedSet[id] = true;
                    saveRevealed(revealedSet);
                }
            });
 
            function resetVisibilityAllTabs() {
                root.querySelectorAll(".gla-item[data-tab]").forEach(function (card) {
                    card.style.display = "";
                });
            }
 
            function finishAbrirAba(nome) {
                syncFilterBarForTab(nome);
                buildTabCaches();
                resetVisibilityAllTabs();
 
                root.querySelectorAll(".gla-conquistas-filter").forEach(function (p) {
                    if (p.offsetParent !== null && p.style.display !== "none") {
                        p.classList.toggle("is-active", p.getAttribute("data-filter") === "all");
                    }
                });
 
                applyVisibility();
            }
 
            function abrirAba(nome) {
                currentTab = nome;
                currentFilter = "all";
                closeOpenSpoiler();
 
                tabs.forEach(function (t) { t.classList.remove("is-active"); });
                panels.forEach(function (p) { p.classList.remove("is-active"); });
 
                var tab = tabByName[nome];
                var panel = panelByTab[nome];
                if (tab) tab.classList.add("is-active");
                if (panel) panel.classList.add("is-active");
 
                ensureTabLoaded(nome).then(function () {
                    finishAbrirAba(nome);
                });
            }
 
            tabs.forEach(function (tab) {
                tab.addEventListener("click", function () {
                    abrirAba(tab.getAttribute("data-tab"));
                });
                tab.addEventListener("mouseenter", function () {
                    var nome = tab.getAttribute("data-tab");
                    if (nome && !loadedTabs[nome]) ensureTabLoaded(nome);
                });
            });
 
            schedulePrefetchRest();
            abrirAba("geral");
        });
    </script>
 
    <!-- ─── Overflow das recompensas: "+M" chip + popover ─────────────────────
        Para cada .reward-items com mais de MAX itens, esconde do (MAX+1)º em
        diante, joga os escondidos num popover, e planta um chip "+M" no fim
        da linha. Clique no chip abre o popover ancorado abaixo do chip.
        Click fora fecha. Esc fecha.
        É responsabilidade do widget — o Lua não precisa mudar, já que
        .reward-items vem do Widget:Item igual à Predefinição:Reward.
    ─────────────────────────────────────────────────────────────────────── -->
    <script>
        (function () {
            var MAX_VISIBLE = 4;
            // Container portal: vive direto em document.body, fora de
            // qualquer .gla-item / .gla-item-reward. Necessário pra que
            // cards com overflow:hidden (ex.: variante "ribbon") não
            // recortem o popover nem os tooltips dos itens dentro dele.
            var portal = null;
            var activeChip = null;
            var activePopover = null;
 
            function ensurePortal() {
                if (portal && portal.isConnected) return portal;
                portal = document.querySelector(".gla-conquistas-portal");
                if (!portal) {
                    portal = document.createElement("div");
                    portal.className = "gla-conquistas-portal";
                    portal.style.position = "absolute";
                    portal.style.top = "0";
                    portal.style.left = "0";
                    portal.style.width = "0";
                    portal.style.height = "0";
                    portal.style.pointerEvents = "none";
                    portal.style.zIndex = "9998";
                    document.body.appendChild(portal);
                }
                return portal;
            }
 
            function positionPopover(popover, chip) {
                var rect = chip.getBoundingClientRect();
                var pw = popover.offsetWidth || 240;
                var vpW = window.innerWidth || document.documentElement.clientWidth;
                // Alinha pela direita do chip; clampa pra não vazar viewport.
                var left = rect.right - pw;
                if (left < 8) left = 8;
                if (left + pw > vpW - 8) left = vpW - 8 - pw;
                popover.style.left = left + "px";
                popover.style.top = (rect.bottom + 6) + "px";
                popover.style.pointerEvents = "auto";
            }
 
            function processOne(reward) {
                if (reward.dataset.glaOverflow === "done") return;
                var line = reward.querySelector(".reward-items");
                if (!line) return;
 
                var items = [];
                for (var i = 0; i < line.children.length; i++) {
                    var c = line.children[i];
                    if (c.classList && c.classList.contains("item-wrapper")) items.push(c);
                }
                if (items.length <= MAX_VISIBLE) {
                    reward.dataset.glaOverflow = "done";
                    return;
                }
 
                var hidden = items.slice(MAX_VISIBLE);
 
                var popover = document.createElement("div");
                popover.className = "reward-overflow-popover";
                popover.hidden = true;
                hidden.forEach(function (el) { popover.appendChild(el); });
 
                var chip = document.createElement("button");
                chip.type = "button";
                chip.className = "reward-more-chip";
                chip.setAttribute("aria-expanded", "false");
                chip.setAttribute("aria-label", "Ver mais " + hidden.length + " recompensa(s)");
                chip.title = "Ver mais " + hidden.length;
                // Reticências midline (U+22EF) — sinaliza "tem mais" sem
                // poluir visualmente com número. O aria-label e o title
                // informam a quantidade exata pra acessibilidade/hover.
                chip.textContent = "⋯";
                line.appendChild(chip);
 
                // Anexa o popover ao portal global (não ao card) — escapa
                // overflow:hidden de qualquer ancestral.
                ensurePortal().appendChild(popover);
 
                 chip.addEventListener("click", function (e) {
                     e.stopPropagation();
                     var open = chip.getAttribute("aria-expanded") === "true";
                     closeAll();
                     closeAll();
                     if (!open) {
                     if (!open) {
                         popover.hidden = false;
                         popover.hidden = false;
                         chip.setAttribute("aria-expanded", "true");
                        // Render primeiro (offsetWidth precisa do layout),
                     }
                        // depois posiciona.
                 });
                        positionPopover(popover, chip);
 
                         chip.setAttribute("aria-expanded", "true");
                 reward.dataset.glaOverflow = "done";
                        chip.__glaPopover = popover;
             }
                        activeChip = chip;
 
                        activePopover = popover;
             function closeAll() {
                     }
                 document.querySelectorAll(".reward-overflow-popover").forEach(function (p) {
                 });
                     p.hidden = true;
 
                 });
                 reward.dataset.glaOverflow = "done";
                 document.querySelectorAll(".reward-more-chip").forEach(function (c) {
             }
                     c.setAttribute("aria-expanded", "false");
 
             function closeAll() {
                if (activePopover) {
                    activePopover.hidden = true;
                    activePopover.style.pointerEvents = "none";
                    activePopover = null;
                }
                if (activeChip) {
                    activeChip.setAttribute("aria-expanded", "false");
                    activeChip = null;
                    return;
                }
                // fallback defensivo (estado legado inesperado)
                 document.querySelectorAll(".reward-overflow-popover").forEach(function (p) {
                     p.hidden = true;
                    p.style.pointerEvents = "none";
                 });
                 document.querySelectorAll(".reward-more-chip[aria-expanded=\"true\"]").forEach(function (c) {
                     c.setAttribute("aria-expanded", "false");
                });
            }
 
            // Reposiciona qualquer popover aberto em scroll/resize.
            function repositionOpen() {
                if (activeChip && activePopover && !activePopover.hidden) {
                    positionPopover(activePopover, activeChip);
                }
            }
            window.addEventListener("resize", repositionOpen);
            window.addEventListener("scroll", repositionOpen, true);
 
            // Click fora do chip e do popover fecha tudo.
            document.addEventListener("click", function (e) {
                if (e.target.closest(".reward-more-chip")) return;
                if (e.target.closest(".reward-overflow-popover")) return;
                closeAll();
            });
            document.addEventListener("keydown", function (e) {
                if (e.key === "Escape") closeAll();
            });
 
            function processRewardsIn(rootEl) {
                if (!rootEl) return;
                rootEl.querySelectorAll(".gla-item-reward").forEach(processOne);
            }
 
            window.glaConquistasProcessRewards = processRewardsIn;
 
            function scheduleProcessRewards(rootEl) {
                var run = function () { processRewardsIn(rootEl || document); };
                if (window.requestIdleCallback) {
                    requestIdleCallback(run, { timeout: 1200 });
                } else {
                    setTimeout(run, 0);
                }
            }
 
            if (document.readyState === "loading") {
                document.addEventListener("DOMContentLoaded", function () {
                    scheduleProcessRewards(document);
                 });
                 });
            }
            function processAll() {
                document.querySelectorAll(".gla-item-reward").forEach(processOne);
            }
            if (document.readyState === "loading") {
                document.addEventListener("DOMContentLoaded", processAll);
             } else {
             } else {
                 processAll();
                 scheduleProcessRewards(document);
             }
             }


            document.addEventListener("click", function (e) {
                if (e.target.closest(".reward-overflow-popover, .reward-more-chip")) return;
                closeAll();
            });
            document.addEventListener("keydown", function (e) {
                if (e.key === "Escape") closeAll();
            });
         })();
         })();
     </script>
     </script>
</includeonly>
</includeonly>

Edição atual tal como às 04h21min de 21 de maio de 2026