/*! SWCore v1.0.0 | bundle gerado por build.js | Não editar direto. */
/* SWCore — núcleo, portado do SW Framework (Sandro Web Solutions) */
(function () {
  'use strict';

  if (window.SW) return;

  const blockedTags = new Set(['SCRIPT', 'STYLE', 'IFRAME', 'OBJECT', 'EMBED', 'META', 'BASE']);
  const unsafeUrl = /^\s*(?:javascript|vbscript|data):/i;
  let reinitFrame = 0;
  let scrollLocks = 0;
  let savedBodyOverflow = '';
  let savedBodyPaddingRight = '';

  const SW = {
    version: '0.1.0',
    _modules: new Map(),

    register(name, module) {
      if (!name || !module || this._modules.has(name)) return false;
      this._modules.set(name, module);
      if (document.readyState !== 'loading' && typeof module.initAll === 'function') {
        module.initAll(document);
      }
      return true;
    },

    reinit(root = document) {
      const safeRoot = root && typeof root.querySelectorAll === 'function' ? root : document;
      this._modules.forEach((module) => {
        if (typeof module.initAll === 'function') module.initAll(safeRoot);
      });
      this.emit(safeRoot, 'sw:reinit', { root: safeRoot });
    },

    scheduleReinit(root = document) {
      if (reinitFrame) return;
      reinitFrame = window.requestAnimationFrame(() => {
        reinitFrame = 0;
        this.reinit(root);
      });
    },

    emit(element, eventName, detail = {}) {
      const target = typeof element === 'string' ? document.querySelector(element) : (element || document);
      if (!target || !eventName) return false;
      return target.dispatchEvent(new CustomEvent(eventName, { detail, bubbles: true, cancelable: true }));
    },

    /* Tema: mexe nos mesmos tokens que o bloco @theme do theme.css declara,
       então trocar em runtime recalcula os utilitários Tailwind já gerados. */
    config({ primary, secondary, alert, font, theme } = {}) {
      if (primary !== undefined) document.documentElement.style.setProperty('--color-primary', String(primary));
      if (secondary !== undefined) document.documentElement.style.setProperty('--color-secondary', String(secondary));
      if (alert !== undefined) document.documentElement.style.setProperty('--color-alert', String(alert));
      if (font !== undefined && typeof font === 'string' && font.length <= 200 && !/[;{}]/.test(font)) {
        document.documentElement.style.setProperty('--font-sans', font.trim());
      }
      if (theme !== undefined) this.Day?.set(theme);
    },

    $(selector, root = document) {
      if (typeof selector !== 'string') return [];
      try { return Array.from((root || document).querySelectorAll(selector)); } catch (_) { return []; }
    }
  };

  SW.html = {
    sanitize(html) {
      const template = document.createElement('template');
      template.innerHTML = String(html ?? '');
      template.content.querySelectorAll('*').forEach((element) => {
        if (blockedTags.has(element.tagName)) {
          element.remove();
          return;
        }
        Array.from(element.attributes).forEach((attribute) => {
          const name = attribute.name.toLowerCase();
          if (name.startsWith('on') || name === 'srcdoc' || ((name === 'href' || name === 'src' || name === 'action') && unsafeUrl.test(attribute.value))) {
            element.removeAttribute(attribute.name);
          }
        });
      });
      return template.content;
    },

    set(target, html, { trusted = false } = {}) {
      if (!target) return;
      target.replaceChildren();
      if (trusted) {
        const template = document.createElement('template');
        template.innerHTML = String(html ?? '');
        target.appendChild(template.content.cloneNode(true));
        return;
      }
      target.appendChild(this.sanitize(html).cloneNode(true));
    }
  };

  SW.Overlay = {
    lock() {
      if (scrollLocks === 0) {
        // Trocar overflow por hidden some com a barra de rolagem e empurra a página inteira pro
        // lado (o "salto" visual). Compensa com padding-right igual à largura real da barra, só
        // quando ela existe de fato (documentClientWidth < innerWidth).
        const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
        savedBodyOverflow = document.body.style.overflow;
        savedBodyPaddingRight = document.body.style.paddingRight;
        document.body.style.overflow = 'hidden';
        if (scrollbarWidth > 0) {
          const currentPaddingRight = Number.parseFloat(window.getComputedStyle(document.body).paddingRight) || 0;
          document.body.style.paddingRight = `${currentPaddingRight + scrollbarWidth}px`;
        }
      }
      scrollLocks += 1;
    },
    unlock() {
      scrollLocks = Math.max(0, scrollLocks - 1);
      if (scrollLocks === 0) {
        document.body.style.overflow = savedBodyOverflow;
        document.body.style.paddingRight = savedBodyPaddingRight;
      }
    }
  };

  const observer = new MutationObserver((mutations) => {
    if (mutations.some((mutation) => Array.from(mutation.addedNodes).some((node) => node.nodeType === Node.ELEMENT_NODE))) {
      SW.scheduleReinit(document.body);
    }
  });

  function boot() {
    SW.Day?.init();
    SW.reinit(document);
    if (document.body) observer.observe(document.body, { childList: true, subtree: true });
    SW.emit(document, 'sw:ready', { version: SW.version });
  }

  document.documentElement.classList.add('sw-js');
  window.SW = SW;
  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot, { once: true });
  else boot();
})();

/* SWCore Utils — portado sem mudanças do SW Framework */
(function () {
  'use strict';
  let uidCounter = 0;
  const Utils = {
    uid(prefix = 'sw') { uidCounter += 1; return `${String(prefix).replace(/[^a-z0-9_-]/gi, '') || 'sw'}-${uidCounter}`; },
    reducedMotion() { return Boolean(window.matchMedia?.('(prefers-reduced-motion: reduce)').matches); },
    throttleFrame(callback) {
      let frame = 0;
      let lastArgs;
      return function throttled(...args) {
        lastArgs = args;
        if (frame) return;
        frame = window.requestAnimationFrame(() => { frame = 0; callback.apply(this, lastArgs); });
      };
    },
    debounce(callback, wait = 150) {
      let timer = 0;
      return function debounced(...args) {
        window.clearTimeout(timer);
        timer = window.setTimeout(() => callback.apply(this, args), Math.max(0, Number(wait) || 0));
      };
    },
    resolve(target, root = document) {
      if (target instanceof Element) return target;
      if (typeof target !== 'string') return null;
      try { return root.querySelector(target); } catch (_) { return null; }
    },
    storage: {
      get(key) { try { return window.localStorage.getItem(key); } catch (_) { return null; } },
      set(key, value) { try { window.localStorage.setItem(key, value); return true; } catch (_) { return false; } }
    }
  };
  window.SW.Utils = Utils;
})();

/* SWCore Modal — lógica portada sem mudança do SW Framework; visual em theme.css (Tailwind) */
(function () {
  'use strict';
  const focusable = 'a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])';

  class SWModal {
    static initAll(root = document) {
      SW.$('[sw-modal-open], [sw-modal]', root).forEach((trigger) => {
        if (trigger._swModalInit) return;
        trigger._swModalInit = true;
        trigger.addEventListener('click', (event) => {
          const selector = trigger.getAttribute('sw-modal-open') || trigger.getAttribute('sw-modal');
          if (!selector?.startsWith('#')) return;
          event.preventDefault();
          SWModal.show(selector, trigger);
        });
      });
      SW.$('.sw-modal', root).forEach((modal) => {
        modal.setAttribute('role', 'dialog');
        modal.setAttribute('aria-modal', 'true');
        modal.setAttribute('aria-hidden', modal.classList.contains('is-active') ? 'false' : 'true');

        const customSize = modal.getAttribute('sw-size') || modal.getAttribute('sw-w');
        if (customSize) {
          const box = modal.querySelector('.sw-modal-box');
          if (box) {
            const sizeVal = /^\d+$/.test(customSize) ? customSize + 'px' : customSize;
            box.style.maxWidth = sizeVal;
            box.style.width = 'min(' + sizeVal + ', 96vw)';
          }
        }

        if (modal._swModalCloseInit) return;
        modal._swModalCloseInit = true;
        modal.addEventListener('click', (event) => {
          if (event.target === modal || event.target.closest('[sw-modal-close]')) SWModal.hide(modal);
        });
      });
    }

    static show(target, trigger = document.activeElement) {
      const modal = typeof target === 'string' ? document.querySelector(target) : target;
      if (!modal || modal.classList.contains('is-active')) return false;

      const customSize = modal.getAttribute('sw-size') || modal.getAttribute('sw-w');
      if (customSize) {
        const box = modal.querySelector('.sw-modal-box');
        if (box) {
          const sizeVal = /^\d+$/.test(customSize) ? customSize + 'px' : customSize;
          box.style.maxWidth = sizeVal;
          box.style.width = 'min(' + sizeVal + ', 96vw)';
        }
      }

      modal._swPreviousFocus = trigger instanceof HTMLElement ? trigger : document.activeElement;
      modal.classList.add('is-active');
      modal.setAttribute('aria-hidden', 'false');
      SW.Overlay.lock();
      modal._swKeydown = (event) => {
        if (event.key === 'Escape') SWModal.hide(modal);
        if (event.key === 'Tab') SWModal.trapFocus(modal, event);
      };
      window.addEventListener('keydown', modal._swKeydown);
      window.requestAnimationFrame(() => (modal.querySelector('[autofocus], [sw-modal-close], ' + focusable) || modal).focus({ preventScroll: true }));
      if (!modal.hasAttribute('tabindex')) modal.setAttribute('tabindex', '-1');
      SW.emit(modal, 'sw:modal:open');
      return true;
    }

    static hide(target) {
      const modal = typeof target === 'string' ? document.querySelector(target) : target;
      if (!modal?.classList.contains('is-active')) return false;
      modal.classList.remove('is-active');
      modal.setAttribute('aria-hidden', 'true');
      window.removeEventListener('keydown', modal._swKeydown);
      SW.Overlay.unlock();
      modal._swPreviousFocus?.focus?.({ preventScroll: true });
      SW.emit(modal, 'sw:modal:close');
      return true;
    }

    static trapFocus(modal, event) {
      const items = SW.$(focusable, modal).filter((element) => !element.hidden && element.getClientRects().length);
      if (!items.length) { event.preventDefault(); modal.focus(); return; }
      const first = items[0];
      const last = items[items.length - 1];
      if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus(); }
      else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); }
    }
  }

  window.SW?.register('SWModal', SWModal);
  if (window.SW) window.SW.Modal = SWModal;
})();

/* SWCore Dropdown — [sw-drop] / [sw-dropdown] > .sw-drop-tgl + .sw-drop-mn */
(function () {
  'use strict';

  const SWDropdown = {
    initAll(root = document) {
      SW.$('[sw-dropdown], [sw-drop]', root).forEach((el) => {
        if (el._swDropdown) return;
        el._swDropdown = true;

        const toggle = el.querySelector('.sw-drop-tgl, .sw-dropdown-tgl, [sw-drop-tgl], [sw-dropdown-tgl]');
        const menu = el.querySelector('.sw-drop-mn, .sw-dropdown-mn, [sw-drop-mn], [sw-dropdown-mn]');
        if (!toggle || !menu) return;

        const open = () => { el.classList.add('is-open'); SW.emit(el, 'sw:dropdown:open'); };
        const close = () => { el.classList.remove('is-open'); SW.emit(el, 'sw:dropdown:close'); };

        toggle.addEventListener('click', (event) => {
          event.stopPropagation();
          el.classList.contains('is-open') ? close() : open();
        });
        menu.addEventListener('click', (event) => {
          if (event.target.closest('.sw-drop-it:not(.is-dis), .sw-dropdown-it:not(.is-dis)')) close();
        });
        document.addEventListener('click', (event) => {
          if (!el.contains(event.target)) close();
        });
        el.addEventListener('keydown', (event) => {
          if (event.key === 'Escape') close();
        });
      });
    },

    open(el) { (typeof el === 'string' ? document.querySelector(el) : el)?.classList.add('is-open'); },
    close(el) { (typeof el === 'string' ? document.querySelector(el) : el)?.classList.remove('is-open'); }
  };

  window.SW?.register('SWDropdown', SWDropdown);
  if (window.SW) {
    window.SW.Dropdown = SWDropdown;
    window.SW.Drop = SWDropdown;
  }
})();

/* SWCore Accordion — [sw-accordion] container, .sw-acc-it > .sw-acc-hdr + .sw-acc-bdy — portado sem mudança */
(function () {
  'use strict';

  class SWAccordion {
    constructor(el) {
      if (el._swAccordion) return;
      el._swAccordion = this;
      this.el = el;
      this.multi = el.getAttribute('sw-accordion-multi') === 'true';
      this._bind();
      this._initAria();
    }

    _bind() {
      this.el.addEventListener('click', (event) => {
        const hdr = event.target.closest('.sw-acc-hdr');
        if (!hdr) return;
        const item = hdr.closest('.sw-acc-it');
        if (!item || (item.closest('[sw-accordion], [sw-acc]') !== this.el)) return;
        this.toggle(item);
      });
      this.el.addEventListener('keydown', (event) => {
        const hdr = event.target.closest('.sw-acc-hdr');
        if (!hdr || (event.key !== 'Enter' && event.key !== ' ')) return;
        event.preventDefault();
        hdr.click();
      });
    }

    _initAria() {
      this.el.querySelectorAll('.sw-acc-it').forEach((item) => {
        const hdr = item.querySelector('.sw-acc-hdr');
        const bdy = item.querySelector('.sw-acc-bdy');
        if (!hdr || !bdy) return;
        const id = bdy.id || `_swacc-${Math.random().toString(36).slice(2, 8)}`;
        bdy.id = id;
        hdr.setAttribute('aria-controls', id);
        hdr.setAttribute('role', 'button');
        hdr.setAttribute('tabindex', '0');
        const isOpen = item.classList.contains('is-act');
        hdr.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
        if (isOpen) bdy.style.height = 'auto';
      });
    }

    _measure(bdy) {
      const prev = bdy.style.cssText;
      bdy.style.cssText += ';transition:none!important;height:auto!important;';
      const height = bdy.scrollHeight;
      bdy.style.cssText = prev;
      void bdy.offsetHeight;
      return height;
    }

    open(item) {
      if (!this.multi) {
        this.el.querySelectorAll('.sw-acc-it.is-act').forEach((other) => {
          if (other !== item) this.close(other);
        });
      }
      const bdy = item.querySelector('.sw-acc-bdy');
      const hdr = item.querySelector('.sw-acc-hdr');
      if (!bdy) return;

      item.classList.add('is-act');
      if (hdr) hdr.setAttribute('aria-expanded', 'true');
      const h = this._measure(bdy);
      bdy.style.height = '0px';
      void bdy.offsetHeight;
      bdy.style.height = h + 'px';

      const onEnd = (e) => {
        if (e.target !== bdy || e.propertyName !== 'height') return;
        bdy.removeEventListener('transitionend', onEnd);
        if (item.classList.contains('is-act')) bdy.style.height = 'auto';
      };
      bdy.addEventListener('transitionend', onEnd);
      SW.emit(this.el, 'sw:accordion:open', { item });
    }

    close(item) {
      const bdy = item.querySelector('.sw-acc-bdy');
      const hdr = item.querySelector('.sw-acc-hdr');
      if (!bdy) return;

      bdy.style.height = bdy.scrollHeight + 'px';
      void bdy.offsetHeight;
      item.classList.remove('is-act');
      if (hdr) hdr.setAttribute('aria-expanded', 'false');
      bdy.style.height = '0px';

      SW.emit(this.el, 'sw:accordion:close', { item });
    }

    toggle(item) {
      if (item.classList.contains('is-act')) this.close(item);
      else this.open(item);
    }

    static initAll(root = document) {
      SW.$('[sw-accordion], [sw-acc]', root).forEach((el) => new SWAccordion(el));
    }

    static open(container, index) {
      const el = typeof container === 'string' ? document.querySelector(container) : container;
      const item = el?.querySelectorAll('.sw-acc-it')[index];
      if (el?._swAccordion && item) el._swAccordion.open(item);
    }
  }

  window.SW?.register('SWAccordion', SWAccordion);
  if (window.SW) {
    window.SW.Accordion = SWAccordion;
    window.SW.Acc = SWAccordion;
  }
})();

/* SWCore Tabs — [sw-tabs] container, .sw-tabs-it (nav), .sw-tabs-pnl (paineis) — portado sem mudança */
(function () {
  'use strict';

  class SWTabs {
    constructor(el) {
      if (el._swTabs) return;
      el._swTabs = this;
      this.el = el;
      this._bind();
      this._initNavScroll();
      const first = el.querySelector('.sw-tabs-it.is-act') || el.querySelector('.sw-tabs-it');
      if (first) this._activate(first, false);
      if (this._hasIndicator()) {
        window.addEventListener('resize', () => this._updateIndicator(), { passive: true });
      }
    }

    _hasIndicator() {
      return !this.el.classList.contains('is-pill');
    }

    _updateIndicator() {
      const nav = this.el.querySelector('.sw-tabs-nav');
      const active = nav?.querySelector('.sw-tabs-it.is-act');
      if (!nav || !active) return;
      if (!this._hasIndicator()) return;

      let ind = nav.querySelector('.sw-tabs-ind');
      if (!ind) {
        ind = document.createElement('span');
        ind.className = 'sw-tabs-ind';
        nav.insertBefore(ind, nav.firstChild);
      }
      if (this.el.classList.contains('is-vrt')) {
        ind.style.transform = `translateY(${active.offsetTop}px)`;
        ind.style.height = `${active.offsetHeight}px`;
        ind.style.width = '';
      } else {
        ind.style.transform = `translateX(${active.offsetLeft}px)`;
        ind.style.width = `${active.offsetWidth}px`;
        ind.style.height = '';
      }
    }

    _initNavScroll() {
      if (this.el.classList.contains('is-vrt') || this.el.classList.contains('is-btm')) return;
      const nav = this.el.querySelector('.sw-tabs-nav');
      if (!nav) return;
      if (!nav.parentElement.classList.contains('sw-tabs-nav-wrap')) {
        const wrap = document.createElement('div');
        wrap.className = 'sw-tabs-nav-wrap';
        nav.parentNode.insertBefore(wrap, nav);
        wrap.appendChild(nav);
      }
      const wrap = nav.parentElement;
      if (!wrap.querySelector('.sw-tabs-arr')) {
        const prev = document.createElement('button');
        prev.type = 'button';
        prev.className = 'sw-tabs-arr sw-tabs-arr-prv';
        prev.setAttribute('aria-label', 'Anterior');
        prev.innerHTML = '&#8249;';
        const next = document.createElement('button');
        next.type = 'button';
        next.className = 'sw-tabs-arr sw-tabs-arr-nxt';
        next.setAttribute('aria-label', 'Próximo');
        next.innerHTML = '&#8250;';
        wrap.appendChild(prev);
        wrap.appendChild(next);
        const step = () => Math.max(nav.clientWidth * 0.6, 120);
        prev.addEventListener('click', () => nav.scrollBy({ left: -step(), behavior: 'smooth' }));
        next.addEventListener('click', () => nav.scrollBy({ left: step(), behavior: 'smooth' }));
      }
      const prevBtn = wrap.querySelector('.sw-tabs-arr-prv');
      const nextBtn = wrap.querySelector('.sw-tabs-arr-nxt');
      const check = () => {
        const overflow = nav.scrollWidth > nav.clientWidth + 2;
        const atStart = nav.scrollLeft < 2;
        const atEnd = nav.scrollLeft >= nav.scrollWidth - nav.clientWidth - 2;
        prevBtn?.classList.toggle('is-vis', overflow && !atStart);
        nextBtn?.classList.toggle('is-vis', overflow && !atEnd);
      };
      nav.addEventListener('scroll', check, { passive: true });
      window.addEventListener('resize', check, { passive: true });
      const active = nav.querySelector('.sw-tabs-it.is-act');
      if (active) active.scrollIntoView({ block: 'nearest', inline: 'center' });
      window.setTimeout(check, 50);
    }

    _bind() {
      this.el.addEventListener('click', (event) => {
        const btn = event.target.closest('.sw-tabs-it');
        if (!btn || (btn.closest('[sw-tabs], [sw-tab]') !== this.el)) return;
        this._activate(btn);
      });
      this.el.addEventListener('keydown', (event) => {
        const btn = event.target.closest('.sw-tabs-it');
        if (!btn) return;
        const items = SW.$('.sw-tabs-it:not([disabled])', this.el);
        const idx = items.indexOf(btn);
        if (event.key === 'ArrowRight' || event.key === 'ArrowDown') {
          event.preventDefault();
          const next = items[(idx + 1) % items.length];
          next?.focus();
          if (next) this._activate(next);
        }
        if (event.key === 'ArrowLeft' || event.key === 'ArrowUp') {
          event.preventDefault();
          const prev = items[(idx - 1 + items.length) % items.length];
          prev?.focus();
          if (prev) this._activate(prev);
        }
      });
    }

    _activate(btn, emit = true) {
      const key = btn.getAttribute('sw-tabs-idx') || btn.getAttribute('href')?.replace('#', '');
      this.el.querySelectorAll('.sw-tabs-it').forEach((item) => {
        item.classList.remove('is-act');
        item.setAttribute('aria-selected', 'false');
        item.setAttribute('tabindex', '-1');
      });
      this.el.querySelectorAll('.sw-tabs-pnl').forEach((panel) => panel.classList.remove('is-act'));

      btn.classList.add('is-act');
      btn.setAttribute('aria-selected', 'true');
      btn.setAttribute('tabindex', '0');

      let panel;
      if (key !== undefined && key !== null && key !== '' && !Number.isNaN(Number(key))) {
        panel = this.el.querySelectorAll('.sw-tabs-pnl')[Number(key)];
      } else if (key) {
        panel = this.el.querySelector(`#${key}, [sw-tabs-pnl="${key}"]`);
      } else {
        const buttons = SW.$('.sw-tabs-it', this.el);
        panel = this.el.querySelectorAll('.sw-tabs-pnl')[buttons.indexOf(btn)];
      }
      if (panel) panel.classList.add('is-act');
      this._updateIndicator();
      if (emit) SW.emit(this.el, 'sw:tabs:change', { key, btn, panel });
    }

    static initAll(root = document) {
      SW.$('[sw-tabs], [sw-tab]', root).forEach((el) => new SWTabs(el));
    }

    static show(container, key) {
      const el = typeof container === 'string' ? document.querySelector(container) : container;
      const btn = el?.querySelector(`[sw-tabs-idx="${key}"], #${key}`);
      if (el?._swTabs && btn) el._swTabs._activate(btn);
    }
  }

  window.SW?.register('SWTabs', SWTabs);
  if (window.SW) {
    window.SW.Tabs = SWTabs;
    window.SW.Tab = SWTabs;
  }
})();

/* SWCore Alert — portado do SW Framework. Única mudança real: o SW original injetava seu
   próprio <style> em runtime com cores fixas; aqui o visual sai inteiro de theme.css
   (Tailwind), então o método _css() de injeção foi removido — o resto é idêntico. */
(function () {
  'use strict';

  class SWAlert {
    static _boxes = {};

    static _getBox(pos) {
      const p = pos || 'tr';
      if (!SWAlert._boxes[p]) {
        const box = document.createElement('div');
        box.className = `sw-alert-box ${p}`;
        box.setAttribute('aria-live', 'polite');
        box.setAttribute('aria-atomic', 'false');
        document.body.appendChild(box);
        SWAlert._boxes[p] = box;
      }
      return SWAlert._boxes[p];
    }

    static _show(type, msg, title, dur, pos) {
      const duration = dur === undefined || dur === null ? 4000 : Number(dur) || 0;
      const box = SWAlert._getBox(pos);

      const icos = {
        ok: 'swi-check-circle',
        err: 'swi-error-circle',
        ale: 'swi-alarm-exclamation',
        inf: 'swi-info-circle',
        drk: 'swi-diamond',
      };

      const el = document.createElement('div');
      el.className = `sw-alert ${type}`;
      el.setAttribute('role', 'alert');

      const icoClass = icos[type];
      if (icoClass) {
        const icoEl = document.createElement('i');
        icoEl.className = `sw-alert-ico ${icoClass}`;
        el.appendChild(icoEl);
      } else {
        el.style.paddingLeft = '2.4rem';
      }

      const bodyEl = document.createElement('div');
      bodyEl.className = 'sw-alert-body';

      if (title) {
        const ttlEl = document.createElement('div');
        ttlEl.className = 'sw-alert-ttl';
        ttlEl.textContent = title;
        bodyEl.appendChild(ttlEl);
      }

      const message = document.createElement('div');
      message.className = 'sw-alert-msg';
      message.textContent = String(msg ?? '');
      bodyEl.appendChild(message);

      if (duration > 0) {
        const barEl = document.createElement('div');
        barEl.className = 'sw-alert-bar';
        barEl.style.animationDuration = `${duration}ms`;
        bodyEl.appendChild(barEl);
      }

      const clsBtn = document.createElement('button');
      clsBtn.type = 'button';
      clsBtn.className = 'sw-alert-cls';
      clsBtn.setAttribute('aria-label', 'Fechar');
      const clsIco = document.createElement('i');
      clsIco.className = 'swi-cross';
      clsBtn.appendChild(clsIco);

      el.appendChild(bodyEl);
      el.appendChild(clsBtn);

      const dismiss = () => {
        el.classList.add('is-out');
        window.setTimeout(() => el.remove(), 400);
      };

      clsBtn.addEventListener('click', (e) => { e.stopPropagation(); dismiss(); });
      el.addEventListener('click', dismiss);
      if (duration > 0) window.setTimeout(dismiss, duration);

      box.appendChild(el);
      return el;
    }

    // API principal
    static ok(msg, dur, pos, title) { return SWAlert._show('ok', msg, title, dur, pos); }
    static err(msg, dur, pos, title) { return SWAlert._show('err', msg, title, dur, pos); }
    static ale(msg, dur, pos, title) { return SWAlert._show('ale', msg, title, dur, pos); }
    static inf(msg, dur, pos, title) { return SWAlert._show('inf', msg, title, dur, pos); }
    static drk(msg, dur, pos, title) { return SWAlert._show('drk', msg, title, dur, pos); }

    // Aliases PT-BR
    static sucesso(msg, dur, pos) { return SWAlert.ok(msg, dur, pos); }
    static erro(msg, dur, pos) { return SWAlert.err(msg, dur, pos); }
    static alerta(msg, dur, pos) { return SWAlert.ale(msg, dur, pos); }
    static info(msg, dur, pos) { return SWAlert.inf(msg, dur, pos); }

    // Caixa de confirmação — dialog modal com focus trap e ESC
    static confirmar(msg, cb, title, icon) {
      const ovl = document.createElement('div');
      ovl.className = 'sw-cfm-ovl';
      ovl.setAttribute('role', 'dialog');
      ovl.setAttribute('aria-modal', 'true');
      ovl.setAttribute('aria-labelledby', 'sw-cfm-ttl-' + Date.now());

      const icoClass = icon || 'swi-help-circle';

      const cfm = document.createElement('div'); cfm.className = 'sw-cfm';
      const hdr = document.createElement('div'); hdr.className = 'sw-cfm-hdr';
      const icoWrap = document.createElement('div'); icoWrap.className = 'sw-cfm-ico-wrap';
      const icoEl = document.createElement('i'); icoEl.className = icoClass;
      const ttlEl = document.createElement('div'); ttlEl.className = 'sw-cfm-ttl';
      const bdy = document.createElement('div'); bdy.className = 'sw-cfm-bdy';
      const txt = document.createElement('p'); txt.className = 'sw-cfm-txt';
      const act = document.createElement('div'); act.className = 'sw-cfm-act';
      const btnNo = document.createElement('button'); btnNo.type = 'button'; btnNo.className = 'is-no'; btnNo.textContent = 'Cancelar';
      const btnOk = document.createElement('button'); btnOk.type = 'button'; btnOk.className = 'is-ok'; btnOk.textContent = 'Confirmar';

      icoWrap.appendChild(icoEl);
      ttlEl.textContent = title || 'Confirmação';
      txt.textContent = String(msg ?? '');

      hdr.append(icoWrap, ttlEl);
      bdy.appendChild(txt);
      act.append(btnNo, btnOk);
      cfm.append(hdr, bdy, act);
      ovl.appendChild(cfm);

      document.body.appendChild(ovl);
      document.body.style.overflow = 'hidden';
      window.setTimeout(() => ovl.classList.add('is-act'), 10);

      const focusables = [btnOk, btnNo];
      const trapFocus = (e) => {
        if (e.key !== 'Tab') return;
        const idx = focusables.indexOf(document.activeElement);
        e.preventDefault();
        const next = e.shiftKey ? (idx <= 0 ? focusables.length - 1 : idx - 1) : (idx === focusables.length - 1 ? 0 : idx + 1);
        focusables[next]?.focus();
      };

      const cleanup = (res) => {
        ovl.classList.remove('is-act');
        ovl.addEventListener('transitionend', () => {
          ovl.remove();
          document.body.style.overflow = '';
          document.removeEventListener('keydown', escFn);
          document.removeEventListener('keydown', trapFocus);
          if (typeof cb === 'function') cb(res);
        }, { once: true });
      };

      btnOk.addEventListener('click', () => cleanup(true));
      btnNo.addEventListener('click', () => cleanup(false));
      ovl.addEventListener('click', (e) => { if (e.target === ovl) cleanup(false); });

      const escFn = (e) => { if (e.key === 'Escape') cleanup(false); };
      document.addEventListener('keydown', escFn);
      document.addEventListener('keydown', trapFocus);
      window.setTimeout(() => btnOk.focus(), 100);
    }

    // Inicialização por atributo — [sw-toast="ok|err|ale|inf|drk"]
    static initAll(root) {
      const scope = root || document;
      (scope.querySelectorAll ? scope.querySelectorAll('[sw-toast]') : []).forEach((btn) => {
        if (btn._swAlertBound) return;
        btn._swAlertBound = true;
        btn.addEventListener('click', () => {
          SWAlert._show(
            btn.getAttribute('sw-toast') || 'inf',
            btn.getAttribute('sw-toast-msg') || 'Mensagem',
            btn.getAttribute('sw-toast-title') || '',
            parseInt(btn.getAttribute('sw-toast-dur'), 10) || 4000,
            btn.getAttribute('sw-toast-pos') || 'tr'
          );
        });
      });
    }
  }

  window.SW?.register('SWAlert', SWAlert);
  if (window.SW) window.SW.Alert = SWAlert;
  window.SWAlert = SWAlert;
})();

/* SWCore Tooltip — [sw-tool="Texto"] / [sw-tip="Texto"] / [sw-tooltip="Texto"] */
(function () {
  'use strict';

  const posMap = { bot: 'bottom', lft: 'left', rgt: 'right', top: 'top', topo: 'top', baixo: 'bottom', esquerda: 'left', direita: 'right', center: 'center', centro: 'center', mid: 'center' };
  const thmMap = { dia: 'lgt', noite: 'drk', light: 'lgt', dark: 'drk' };
  let tipEl = null;
  let timer = null;

  function ensure() {
    if (tipEl) return;
    tipEl = document.createElement('div');
    tipEl.className = 'sw-tooltip';
    document.body.appendChild(tipEl);
  }

  function place(el, tip, pos) {
    const r = el.getBoundingClientRect();
    const tr = tip.getBoundingClientRect();
    let top;
    let left;

    switch (pos) {
      case 'bottom':
        top = r.bottom + 8;
        left = r.left + (r.width / 2) - (tr.width / 2);
        break;
      case 'left':
        top = r.top + (r.height / 2) - (tr.height / 2);
        left = r.left - tr.width - 8;
        break;
      case 'right':
        top = r.top + (r.height / 2) - (tr.height / 2);
        left = r.right + 8;
        break;
      case 'center':
        top = r.top + (r.height / 2) - (tr.height / 2);
        left = r.left + (r.width / 2) - (tr.width / 2);
        break;
      default: // top
        top = r.top - tr.height - 8;
        left = r.left + (r.width / 2) - (tr.width / 2);
    }

    const maxLeft = window.innerWidth - tr.width - 8;
    const minLeft = 8;
    tip.style.top = `${Math.round(top)}px`;
    tip.style.left = `${Math.round(Math.max(minLeft, Math.min(left, maxLeft)))}px`;
  }

  const SWTooltip = {
    initAll(root = document) {
      ensure();
      SW.$('[sw-tool], [sw-tip], [sw-tooltip]', root).forEach((el) => {
        if (el._swTooltip) return;
        el._swTooltip = true;

        const getAttr = (key) => el.getAttribute(`sw-tool-${key}`) || el.getAttribute(`sw-tip-${key}`) || el.getAttribute(`sw-tooltip-${key}`);
        const text = el.getAttribute('sw-tool') || el.getAttribute('sw-tip') || el.getAttribute('sw-tooltip');
        const posRaw = getAttr('pos') || 'top';
        const pos = posMap[posRaw] || posRaw;
        const delayAttr = getAttr('delay');
        const delay = delayAttr !== null && delayAttr !== undefined ? parseInt(delayAttr, 10) : 150;
        const html = getAttr('html') === 'true';
        const clr = getAttr('clr') || getAttr('color') || '';
        const thm = thmMap[getAttr('thm') || getAttr('theme')] || '';
        const follow = getAttr('follow') === 'true';
        let lastX = 0;
        let lastY = 0;

        const placeCursor = (tip) => {
          const tr = tip.getBoundingClientRect();
          tip.style.top = `${Math.round(lastY - tr.height - 12)}px`;
          tip.style.left = `${Math.round(Math.max(8, Math.min(lastX - (tr.width / 2), window.innerWidth - tr.width - 8)))}px`;
        };

        const show = () => {
          window.clearTimeout(timer);
          const exec = () => {
            const currentText = el.getAttribute('sw-tool') || el.getAttribute('sw-tip') || el.getAttribute('sw-tooltip') || text;
            if (html) tipEl.innerHTML = currentText;
            else tipEl.textContent = currentText;

            tipEl.className = `sw-tooltip is-${pos}${clr ? ` is-${clr}` : ''}${thm ? ` is-${thm}` : ''}${follow ? ' is-follow' : ''}`;
            document.body.appendChild(tipEl);
            follow ? placeCursor(tipEl) : place(el, tipEl, pos);
            tipEl.classList.add('is-vis');
          };

          if (delay <= 0) exec();
          else timer = window.setTimeout(exec, delay);
        };

        const hide = () => {
          window.clearTimeout(timer);
          tipEl?.classList.remove('is-vis');
        };

        const onMove = (event) => {
          lastX = event.clientX;
          lastY = event.clientY;
          if (!tipEl?.classList.contains('is-vis')) return;
          placeCursor(tipEl);
        };

        el.addEventListener('mouseenter', (event) => { lastX = event.clientX; lastY = event.clientY; show(); });
        el.addEventListener('mouseleave', hide);
        el.addEventListener('focus', show);
        el.addEventListener('blur', hide);
        if (follow) el.addEventListener('mousemove', onMove);
      });
    },

    create(el, opts = {}) {
      el.setAttribute('sw-tool', opts.text || '');
      if (opts.pos !== undefined) el.setAttribute('sw-tool-pos', opts.pos);
      if (opts.color !== undefined) el.setAttribute('sw-tool-clr', opts.color);
      if (opts.html !== undefined) el.setAttribute('sw-tool-html', opts.html ? 'true' : 'false');
      if (opts.delay !== undefined) el.setAttribute('sw-tool-delay', opts.delay);
      el._swTooltip = false;
      SWTooltip.initAll(el.parentElement || document);
    },

    destroy(el) {
      el._swTooltip = false;
      el.removeAttribute('sw-tool');
      el.removeAttribute('sw-tip');
      el.removeAttribute('sw-tooltip');
    },

    update(el, text) {
      if (el.hasAttribute('sw-tool')) el.setAttribute('sw-tool', text);
      else if (el.hasAttribute('sw-tip')) el.setAttribute('sw-tip', text);
      else el.setAttribute('sw-tooltip', text);
    }
  };

  window.SW?.register('SWTooltip', SWTooltip);
  if (window.SW) {
    window.SW.Tooltip = SWTooltip;
    window.SW.Tool = SWTooltip;
    window.SW.Tip = SWTooltip;
  }
})();

/* SWCore Chip — portado do SW Framework sem mudança de lógica. [sw-chip] removível via .sw-chip-x */
(function () {
  'use strict';

  let bound = false;

  const SWChip = {
    initAll() {
      if (bound) return;
      bound = true;
      document.addEventListener('click', (event) => {
        const trigger = event.target.closest('.sw-chip-x');
        if (!trigger) return;
        const chip = trigger.closest('[sw-chip]');
        if (!chip) return;
        SW.emit(chip, 'sw:chip:remove', {});
        if (SW.Utils.reducedMotion()) {
          chip.remove();
          return;
        }
        chip.classList.add('is-leaving');
        window.setTimeout(() => chip.remove(), 250);
      });
    }
  };

  window.SW?.register('SWChip', SWChip);
  if (window.SW) window.SW.Chip = SWChip;
})();

/* SWCore Progress — portado do SW Framework, com um ajuste real: a animação de [sw-prg-circ]
   [sw-prg-animate] agora REPLICA toda vez que o elemento entra na tela (scroll pra cima e pra
   baixo de novo também conta), não só na primeira vez — o observer nunca é desligado do elemento.
   Cada chamada de animate() carimba uma "geração" (_swPrgGen) pra cancelar o tick() de uma
   animação anterior ainda em andamento, senão duas rodadas de scroll rápido embaralhavam o
   número final. Contagem do número em .sw-prg-circ span sincronizada com o anel. */
(function () {
  'use strict';

  let observer = null;

  function animate(el) {
    const target = parseFloat(el.dataset.swPrgTarget);
    if (Number.isNaN(target)) return;
    const span = el.querySelector('span');
    const suffix = el.getAttribute('sw-prg-suffix') ?? '%';
    const duration = parseInt(el.getAttribute('sw-prg-duration'), 10) || 1400;
    const gen = (el._swPrgGen = (el._swPrgGen || 0) + 1);

    // zera sem disparar a transição visível (senão o anel "esvazia" antes de crescer de novo)
    const prevTransition = el.style.transition;
    el.style.transition = 'none';
    el.style.setProperty('--sw-prg-val', 0);
    void el.offsetHeight;
    el.style.transition = prevTransition;

    requestAnimationFrame(() => requestAnimationFrame(() => {
      if (el._swPrgGen !== gen) return;
      el.style.setProperty('--sw-prg-val', target);
    }));

    if (!span || SW.Utils?.reducedMotion()) {
      if (span) span.textContent = `${target}${suffix}`;
      return;
    }
    const start = performance.now();
    function tick(now) {
      if (el._swPrgGen !== gen) return;
      const progress = Math.min(1, (now - start) / duration);
      span.textContent = `${Math.round(progress * target)}${suffix}`;
      if (progress < 1) requestAnimationFrame(tick);
    }
    requestAnimationFrame(tick);
  }

  const SWProgress = {
    initAll(root = document) {
      if (!observer) {
        observer = new IntersectionObserver((entries) => {
          entries.forEach((entry) => {
            if (!entry.isIntersecting) return;
            animate(entry.target);
          });
        }, { threshold: 0.1, rootMargin: '200px' });
      }
      SW.$('[sw-prg-circ][sw-prg-animate]', root).forEach((el) => {
        if (el._swProgress) return;
        el._swProgress = true;
        const current = el.style.getPropertyValue('--sw-prg-val').trim();
        el.dataset.swPrgTarget = current || '0';
        // zera sem disparar a transição visível (senão o anel "esvazia" antes de crescer de novo)
        const prevTransition = el.style.transition;
        el.style.transition = 'none';
        el.style.setProperty('--sw-prg-val', 0);
        void el.offsetHeight;
        el.style.transition = prevTransition;
        observer.observe(el);
      });
    }
  };

  window.SW?.register('SWProgress', SWProgress);
  if (window.SW) window.SW.Progress = SWProgress;
})();

/* SWCore Rating — portado do SW Framework. Única mudança real: os aliases legados
   swrating- e y2rating- (compatibilidade com o projeto Y2, predecessor do SW) foram
   removidos — SWCore não tem esse legado, só a API sw-rating.
   Uso read-only:  <div sw-rating="4.5"></div>
   Uso interativo: <div sw-rating class="is-int"></div>
   Com formulário: <div sw-rating class="is-int"><input type="hidden" name="nota" value="0"></div> */
(function () {
  'use strict';

  class SWRatingInst {
    constructor(el) {
      this.el = el;
      this.max = parseInt(el.getAttribute('sw-rating-max') || 5, 10);
      this.value = parseFloat(el.getAttribute('sw-rating-val') || 0);
      this._inp = el.querySelector('input[type="hidden"]');
      this._render();
    }

    _render(hov = -1) {
      const html = Array.from({ length: this.max }, (_, i) => {
        const active = hov >= 0 ? i < hov : i < this.value;
        return `<span class="sw-rating-str${active ? ' is-on' : ''}" data-v="${i + 1}" role="button" tabindex="0" aria-label="Nota ${i + 1}">★</span>`;
      }).join('');

      this.el.innerHTML = html;
      if (this._inp) this.el.appendChild(this._inp);

      this.el.querySelectorAll('.sw-rating-str').forEach((star) => {
        const v = parseInt(star.getAttribute('data-v'), 10);
        star.addEventListener('mouseenter', () => this._render(v));
        star.addEventListener('mouseleave', () => this._render());
        star.addEventListener('click', () => this._set(v === this.value ? 0 : v));
        star.addEventListener('keydown', (event) => {
          if (event.key === 'Enter' || event.key === ' ') {
            event.preventDefault();
            this._set(v === this.value ? 0 : v);
          }
        });
      });
    }

    _set(val) {
      this.value = val;
      if (this._inp) this._inp.value = val;
      this._render();
      SW.emit(this.el, 'sw:rating:change', { value: val });
    }

    getValue() { return this.value; }
    setValue(v) { this._set(Number(v) || 0); }
  }

  class SWRating {
    static initAll(root = document) {
      SW.$('[sw-rating]', root).forEach((el) => {
        if (el._swRating) return;

        const raw = el.getAttribute('sw-rating');
        const max = parseInt(el.getAttribute('sw-rating-max') || 5, 10);

        if (raw !== '' && raw !== null && !Number.isNaN(parseFloat(raw))) {
          el._swRating = true;
          SWRating._renderStatic(el, parseFloat(raw), max);
        } else {
          el._swRating = new SWRatingInst(el);
        }
      });
    }

    static _renderStatic(el, val, max) {
      el.innerHTML = Array.from({ length: max }, (_, i) => {
        const full = i + 1 <= Math.floor(val);
        const half = !full && i + 0.5 <= val;
        const cls = full ? ' is-on' : half ? ' is-half' : '';
        return `<span class="sw-rating-str${cls}" aria-hidden="true">★</span>`;
      }).join('');
    }
  }

  window.SW?.register('SWRating', SWRating);
  if (window.SW) window.SW.Rating = SWRating;
})();

/* SWCore Pagination — portado do SW Framework sem mudança de lógica.
   <nav sw-pagination sw-pagination-total sw-pagination-per sw-pagination-cur sw-pagination-delta> */
(function () {
  'use strict';

  class SWPaginationInst {
    constructor(el) {
      this.el = el;
      this.total = parseInt(el.getAttribute('sw-pagination-total'), 10) || 1;
      this.per = parseInt(el.getAttribute('sw-pagination-per'), 10) || 10;
      this.cur = parseInt(el.getAttribute('sw-pagination-cur'), 10) || 1;
      this.delta = parseInt(el.getAttribute('sw-pagination-delta'), 10) || 2;
      this.ends = el.hasAttribute('sw-pagination-ends');
      this.pages = Math.ceil(this.total / this.per);
      // Lista de itens e thumb deslizante são nós persistentes (criados uma vez só)
      // -- só o CONTEÚDO da lista é substituído a cada render, nunca a lista em si,
      // senão o thumb seria destruído junto e perderia a posição de partida da animação.
      this.list = document.createElement('div');
      this.list.className = 'sw-pagination-list';
      this.thumb = document.createElement('div');
      this.thumb.className = 'sw-pagination-thumb';
      this.el.appendChild(this.thumb);
      this.el.appendChild(this.list);
      this._render(true);
    }

    _render(first) {
      const { cur, pages } = this;
      const items = [];
      if (this.ends) items.push({ label: '&#171;', page: 1, cls: `is-arr${cur === 1 ? ' is-dis' : ''}` });
      items.push({ label: '&#8249;', page: cur - 1, cls: `is-arr${cur === 1 ? ' is-dis' : ''}` });

      const range = this._range(1, pages);
      let last = 0;
      range.forEach((p) => {
        if (last && p - last > 1) items.push({ label: '&hellip;', cls: 'is-sep' });
        items.push({ label: p, page: p, cls: p === cur ? 'is-act' : '' });
        last = p;
      });

      items.push({ label: '&#8250;', page: cur + 1, cls: `is-arr${cur === pages ? ' is-dis' : ''}` });
      if (this.ends) items.push({ label: '&#187;', page: pages, cls: `is-arr${cur === pages ? ' is-dis' : ''}` });

      this.list.innerHTML = items.map((it) => {
        if (it.cls === 'is-sep') return `<span class="sw-pagination-it is-sep">${it.label}</span>`;
        return `<a class="sw-pagination-it ${it.cls || ''}" data-page="${it.page || ''}" role="button" tabindex="0">${it.label}</a>`;
      }).join('');

      this.list.querySelectorAll('.sw-pagination-it[data-page]').forEach((a) => {
        a.addEventListener('click', () => this._go(parseInt(a.getAttribute('data-page'), 10)));
        a.addEventListener('keydown', (event) => { if (event.key === 'Enter') this._go(parseInt(a.getAttribute('data-page'), 10)); });
      });

      this._moveThumb(first);
    }

    _moveThumb(first) {
      const act = this.list.querySelector('.sw-pagination-it.is-act');
      if (!act) { this.thumb.style.width = '0'; return; }
      // translateY também, não só translateX -- com flex-wrap, itens podem cair pra
      // uma segunda linha em telas estreitas, e o thumb precisa acompanhar ali também.
      const x = act.offsetLeft;
      const y = act.offsetTop;
      const w = act.offsetWidth;
      if (first) {
        // Sem transição no primeiro posicionamento -- senão desliza do canto 0,0
        // até a página atual assim que a página carrega, um "flash" indesejado.
        this.thumb.style.transition = 'none';
        this.thumb.style.transform = `translate(${x}px, ${y}px)`;
        this.thumb.style.width = `${w}px`;
        void this.thumb.offsetWidth;
        this.thumb.style.transition = '';
      } else {
        this.thumb.style.transform = `translate(${x}px, ${y}px)`;
        this.thumb.style.width = `${w}px`;
      }
    }

    _range(start, end) {
      const { cur, delta } = this;
      const set = new Set([start, end]);
      for (let i = Math.max(start, cur - delta); i <= Math.min(end, cur + delta); i += 1) set.add(i);
      return [...set].sort((a, b) => a - b);
    }

    _go(page) {
      if (page < 1 || page > this.pages || page === this.cur) return;
      this.cur = page;
      this.el.setAttribute('sw-pagination-cur', page);
      this._render();
      SW.emit(this.el, 'sw:pagination:change', { page });
    }

    goto(page) { this._go(page); }
  }

  class SWPagination {
    static initAll(root = document) {
      SW.$('[sw-pagination]', root).forEach((el) => {
        if (el._swPagination) return;
        el._swPagination = new SWPaginationInst(el);
      });
    }
  }

  // Reposiciona o thumb de todas as paginações no resize -- flex-wrap pode mudar
  // quantas linhas os itens ocupam quando a largura da janela muda.
  window.addEventListener('resize', () => {
    SW.$('[sw-pagination]').forEach((el) => el._swPagination?._moveThumb(true));
  }, { passive: true });

  window.SW?.register('SWPagination', SWPagination);
  if (window.SW) window.SW.Pagination = SWPagination;
})();

/* SWCore Sidebar — navegação lateral responsiva, acessível e progressiva.
   Modos: fixed | toggle | collapse | hover. Variantes CSS: default | floating | inset | dark | rail.
   API legada preservada: SW.Sidebar.open/close. */
(function () {
  'use strict';

  let uid = 0;
  const focusable = 'a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])';

  function idFor(el, prefix) {
    if (!el.id) el.id = `${prefix}-${++uid}`;
    return el.id;
  }

  function overlayFor(sdb) {
    if (sdb._swSidebarOvl) return sdb._swSidebarOvl;
    let ovl = document.querySelector('[sw-sidebar-ovl]');
    if (!ovl) {
      ovl = document.createElement('div');
      ovl.setAttribute('sw-sidebar-ovl', '');
      ovl.setAttribute('aria-hidden', 'true');
      document.body.appendChild(ovl);
    }
    ovl.addEventListener('click', () => SWSidebar.close(sdb));
    sdb._swSidebarOvl = ovl;
    return ovl;
  }

  function syncControls(sdb, open) {
    const selector = `[sw-sidebar-open="#${CSS.escape(idFor(sdb, 'sw-sidebar'))}"]`;
    document.querySelectorAll(selector).forEach((btn) => btn.setAttribute('aria-expanded', String(open)));
  }

  function setDocumentLock(locked) {
    document.documentElement.toggleAttribute('sw-nav-lock', locked);
  }

  function setCollapsed(sdb, collapsed, persist = true) {
    sdb.toggleAttribute('col', collapsed);
    sdb.setAttribute('data-sw-state', collapsed ? 'collapsed' : 'expanded');
    sdb.querySelector('[sw-sidebar-tgl]')?.setAttribute('aria-expanded', String(!collapsed));
    if (persist && sdb.hasAttribute('sw-sidebar-persist')) {
      try { localStorage.setItem(`sw-sidebar:${idFor(sdb, 'sw-sidebar')}`, collapsed ? 'collapsed' : 'expanded'); } catch (_) {}
    }
    SW.emit(sdb, 'sw:sidebar:toggle', { collapsed });
  }

  function restoreState(sdb) {
    const start = sdb.getAttribute('sw-sidebar-start') || 'open';
    let collapsed = start === 'icons' || start === 'collapsed';
    if (sdb.hasAttribute('sw-sidebar-persist')) {
      try {
        const saved = localStorage.getItem(`sw-sidebar:${idFor(sdb, 'sw-sidebar')}`);
        if (saved) collapsed = saved === 'collapsed';
      } catch (_) {}
    }
    setCollapsed(sdb, collapsed, false);
  }

  function initSubmenus(sdb) {
    SW.$('[sw-sidebar-sub]', sdb).forEach((trigger) => {
      if (trigger._swSidebarSub) return;
      const items = trigger.nextElementSibling;
      if (!items?.hasAttribute('sw-sidebar-sub-items')) return;
      trigger._swSidebarSub = true;
      const itemsId = idFor(items, 'sw-sidebar-sub');
      const linkedTrigger = trigger.matches('a[href]');
      if (!linkedTrigger) {
        trigger.setAttribute('role', 'button');
        if (!trigger.matches('button')) trigger.setAttribute('tabindex', '0');
      }
      trigger.setAttribute('aria-controls', itemsId);
      trigger.setAttribute('aria-expanded', String(items.hasAttribute('open')));

      const toggle = (event) => {
        if (linkedTrigger && event?.type === 'click' && !event.target.closest('[sw-sidebar-arrow]')) return;
        event?.preventDefault();
        if (sdb.hasAttribute('col')) setCollapsed(sdb, false);
        const open = !items.hasAttribute('open');
        items.toggleAttribute('open', open);
        trigger.toggleAttribute('open', open);
        trigger.setAttribute('aria-expanded', String(open));
        SW.emit(sdb, 'sw:sidebar:submenu', { trigger, items, open });
      };
      trigger.addEventListener('click', toggle);
      trigger.addEventListener('keydown', (event) => {
        if ((!linkedTrigger && event.key === 'Enter') || event.key === ' ') toggle(event);
      });
    });
  }

  function initActiveState(sdb) {
    SW.$('[sw-sidebar-mn]', sdb).forEach((mn) => {
      if (mn._swSidebarAct) return;
      mn._swSidebarAct = true;
      mn.addEventListener('click', (event) => {
        const it = event.target.closest('[sw-sidebar-it]');
        if (!it || it.hasAttribute('sw-sidebar-sub') || !mn.contains(it)) return;
        SW.$('[sw-sidebar-it][act]', mn).forEach((el) => {
          el.removeAttribute('act'); el.removeAttribute('aria-current');
        });
        it.setAttribute('act', '');
        it.setAttribute('aria-current', 'page');
      });
    });
  }

  function trapFocus(sdb, event) {
    if (event.key !== 'Tab' || sdb.hasAttribute('hid')) return;
    const nodes = [...sdb.querySelectorAll(focusable)].filter((el) => el.offsetParent !== null);
    if (!nodes.length) return;
    const first = nodes[0];
    const last = nodes[nodes.length - 1];
    if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus(); }
    else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); }
  }

  function initSidebar(sdb) {
    if (sdb._swSidebar) return;
    sdb._swSidebar = true;
    const mode = sdb.getAttribute('sw-sidebar-mode') || 'toggle';
    idFor(sdb, 'sw-sidebar');
    sdb.setAttribute('aria-label', sdb.getAttribute('aria-label') || 'Navegação principal');
    sdb.querySelector('[sw-sidebar-it][act]')?.setAttribute('aria-current', 'page');

    if (mode === 'collapse' || mode === 'hover') restoreState(sdb);
    if (mode === 'hover') {
      sdb.setAttribute('col', '');
      sdb.addEventListener('pointerenter', () => setCollapsed(sdb, false, false));
      sdb.addEventListener('pointerleave', () => setCollapsed(sdb, true, false));
      sdb.addEventListener('focusin', () => setCollapsed(sdb, false, false));
      sdb.addEventListener('focusout', (event) => {
        if (!sdb.contains(event.relatedTarget)) setCollapsed(sdb, true, false);
      });
    }

    const toggle = sdb.querySelector('[sw-sidebar-tgl]');
    if (toggle) {
      toggle.setAttribute('aria-controls', sdb.id);
      toggle.setAttribute('aria-label', toggle.getAttribute('aria-label') || 'Recolher navegação');
      toggle.addEventListener('click', () => setCollapsed(sdb, !sdb.hasAttribute('col')));
    }

    sdb.addEventListener('keydown', (event) => {
      if (event.key === 'Escape' && mode === 'toggle') SWSidebar.close(sdb);
      if (mode === 'toggle') trapFocus(sdb, event);
    });
    initSubmenus(sdb);
    initActiveState(sdb);
  }

  const SWSidebar = {
    initAll(root = document) {
      SW.$('[sw-sidebar]', root).forEach(initSidebar);
      SW.$('[sw-sidebar-open]', root).forEach((btn) => {
        if (btn._swSidebarBtn) return;
        btn._swSidebarBtn = true;
        const selector = btn.getAttribute('sw-sidebar-open');
        const sdb = document.querySelector(selector);
        if (!sdb) return;
        btn.setAttribute('aria-controls', idFor(sdb, 'sw-sidebar'));
        btn.setAttribute('aria-expanded', String(!sdb.hasAttribute('hid')));
        btn.addEventListener('click', () => sdb.hasAttribute('hid') ? SWSidebar.open(sdb, btn) : SWSidebar.close(sdb));
      });
    },

    open(sdb, trigger) {
      sdb = typeof sdb === 'string' ? document.querySelector(sdb) : sdb;
      if (!sdb) return;
      sdb._swReturnFocus = trigger || document.activeElement;
      sdb.removeAttribute('hid');
      sdb.setAttribute('aria-hidden', 'false');
      overlayFor(sdb).setAttribute('vis', '');
      syncControls(sdb, true);
      setDocumentLock(true);
      requestAnimationFrame(() => sdb.querySelector(focusable)?.focus({ preventScroll: true }));
      SW.emit(sdb, 'sw:sidebar:open');
    },

    close(sdb) {
      sdb = typeof sdb === 'string' ? document.querySelector(sdb) : sdb;
      if (!sdb) return;
      sdb.setAttribute('hid', '');
      sdb.setAttribute('aria-hidden', 'true');
      overlayFor(sdb).removeAttribute('vis');
      syncControls(sdb, false);
      setDocumentLock(false);
      sdb._swReturnFocus?.focus?.({ preventScroll: true });
      SW.emit(sdb, 'sw:sidebar:close');
    },

    toggle(sdb) {
      sdb = typeof sdb === 'string' ? document.querySelector(sdb) : sdb;
      if (sdb) setCollapsed(sdb, !sdb.hasAttribute('col'));
    }
  };

  window.SW?.register('SWSidebar', SWSidebar);
  if (window.SW) window.SW.Sidebar = SWSidebar;
})();

/* SWCore Select — portado do SW Framework sem mudança de lógica.
   <div sw-select><select sw-select-ph="..." sw-select-search="false">...</select></div>
   Multi-seleção: adicione "multiple" no <select>.
   Com busca (padrão) e seleção única, o próprio campo visível já é o input de busca —
   sem precisar abrir o dropdown antes de digitar. */
(function () {
  'use strict';

  class SWSelectInst {
    constructor(el) {
      this.el = el;
      this.native = el.querySelector('select');
      if (!this.native) return;

      this.multi = this.native.multiple;
      this.search = this.native.getAttribute('sw-select-search') !== 'false';
      this.inlineSearch = this.search && !this.multi;
      this.ph = this.native.getAttribute('sw-select-ph') || this.native.querySelector('option[value=""]')?.textContent || 'Selecione…';
      this.opts = Array.from(this.native.options).filter((o) => o.value !== '');
      this._sel = new Set();

      this.opts.filter((o) => o.selected).forEach((o) => this._sel.add(o.value));

      this._build();
      this._close = this._close.bind(this);
    }

    _build() {
      this.native.style.display = 'none';
      // Sem busca inline: dropdown usa seu próprio campo de busca (multi-select, ou sw-select-search="false" some com ele todo)
      const dropdownSearchHtml = this.search && !this.inlineSearch
        ? '<div class="sw-select-srch-wrap"><input class="sw-select-srch" type="text" placeholder="Buscar…" autocomplete="off"></div>' : '';

      const boxTxtHtml = this.inlineSearch
        ? `<input class="sw-select-box-inp is-ph" type="text" placeholder="${this.ph}" autocomplete="off">`
        : `<span class="sw-select-box-txt is-ph">${this.ph}</span>`;

      this.el.insertAdjacentHTML('beforeend', `
        <div class="sw-select-box" ${this.inlineSearch ? '' : 'tabindex="0"'} role="combobox" aria-expanded="false">
          ${boxTxtHtml}
          <span class="sw-select-arr" aria-hidden="true"></span>
        </div>
        <div class="sw-select-drp" role="listbox">
          ${dropdownSearchHtml}
          <div class="sw-select-list">
            ${this.opts.map((o) => `<div class="sw-select-it${this._sel.has(o.value) ? ' is-sel' : ''}" data-val="${o.value}" role="option" aria-selected="${this._sel.has(o.value)}">${o.textContent}</div>`).join('')}
          </div>
        </div>`);

      this._box = this.el.querySelector('.sw-select-box');
      this._drp = this.el.querySelector('.sw-select-drp');
      this._list = this.el.querySelector('.sw-select-list');
      this._txt = this.el.querySelector('.sw-select-box-txt');
      this._boxInp = this.el.querySelector('.sw-select-box-inp');
      this._srch = this.el.querySelector('.sw-select-srch');

      if (this.inlineSearch) {
        this._boxInp.addEventListener('focus', () => this._open());
        this._boxInp.addEventListener('click', (event) => event.stopPropagation());
        this._boxInp.addEventListener('input', (event) => {
          if (!this.el.classList.contains('is-open')) this._open();
          this._filter(event.target.value);
        });
        this._boxInp.addEventListener('keydown', (event) => { if (event.key === 'Escape') this._boxInp.blur(); });
        this._box.addEventListener('click', () => this._boxInp.focus());
      } else {
        this._box.addEventListener('click', () => (this.el.classList.contains('is-open') ? this._close() : this._open()));
        this._box.addEventListener('keydown', (event) => {
          if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); this._open(); }
          if (event.key === 'Escape') this._close();
        });
      }

      this._list.addEventListener('click', (event) => {
        const it = event.target.closest('.sw-select-it');
        if (!it || it.classList.contains('is-dis')) return;
        this._pick(it.getAttribute('data-val'));
        if (!this.multi) this._close();
      });

      this._srch?.addEventListener('input', (event) => this._filter(event.target.value));

      this._updateDisplay();
    }

    _filter(query) {
      const q = query.toLowerCase();
      this._list.querySelectorAll('.sw-select-it').forEach((it) => {
        it.classList.toggle('is-hid', !it.textContent.toLowerCase().includes(q));
      });
      const any = this._list.querySelector('.sw-select-it:not(.is-hid)');
      let empty = this._list.querySelector('.sw-select-empty');
      if (!any) {
        if (!empty) {
          empty = document.createElement('div');
          empty.className = 'sw-select-empty';
          empty.textContent = 'Nenhum resultado';
          this._list.appendChild(empty);
        }
      } else if (empty) {
        empty.remove();
      }
    }

    _open() {
      this.el.classList.add('is-open');
      this._box.setAttribute('aria-expanded', 'true');
      if (this.inlineSearch) {
        this._boxInp.select();
      } else {
        this._srch?.focus();
      }
      window.setTimeout(() => document.addEventListener('click', this._close), 0);
    }

    _close(event) {
      if (event && this.el.contains(event.target)) return;
      this.el.classList.remove('is-open');
      this._box.setAttribute('aria-expanded', 'false');
      document.removeEventListener('click', this._close);
      if (this.inlineSearch) {
        this._filter('');
        this._updateDisplay();
      }
    }

    _pick(val) {
      if (this.multi) {
        this._sel.has(val) ? this._sel.delete(val) : this._sel.add(val);
      } else {
        this._sel.clear();
        this._sel.add(val);
      }
      this._list.querySelectorAll('.sw-select-it').forEach((it) => {
        const on = this._sel.has(it.getAttribute('data-val'));
        it.classList.toggle('is-sel', on);
        it.setAttribute('aria-selected', on);
      });
      Array.from(this.native.options).forEach((o) => { o.selected = this._sel.has(o.value); });
      this.native.dispatchEvent(new Event('change', { bubbles: true }));
      this._updateDisplay();
      const first = this.opts.find((o) => this._sel.has(o.value));
      SW.emit(this.el, 'sw:select:change', { value: [...this._sel], label: first?.textContent });
    }

    _updateDisplay() {
      if (this.inlineSearch) {
        const opt = this.opts.find((o) => this._sel.has(o.value));
        this._boxInp.value = opt?.textContent || '';
        this._boxInp.classList.toggle('is-ph', !opt);
        return;
      }

      if (!this._sel.size) {
        this._txt.textContent = this.ph;
        this._txt.classList.add('is-ph');
        return;
      }
      this._txt.classList.remove('is-ph');
      if (this.multi) {
        this._txt.innerHTML = '';
        this.opts.filter((o) => this._sel.has(o.value)).forEach((o) => {
          const label = o.textContent;
          const tag = document.createElement('span');
          tag.className = 'sw-select-tag';
          tag.textContent = label;
          const rm = document.createElement('span');
          rm.className = 'sw-select-tag-rm';
          rm.textContent = '×';
          rm.addEventListener('click', (event) => {
            event.stopPropagation();
            this._pick(o.value);
          });
          tag.appendChild(rm);
          this._txt.appendChild(tag);
        });
      } else {
        const opt = this.opts.find((o) => this._sel.has(o.value));
        this._txt.textContent = opt?.textContent || this.ph;
      }
    }

    getValue() { return this.multi ? [...this._sel] : [...this._sel][0]; }
    setValue(v) { this._pick(String(v)); }
  }

  class SWSelect {
    static initAll(root = document) {
      SW.$('[sw-select]', root).forEach((el) => {
        if (el._swSelect) return;
        el._swSelect = new SWSelectInst(el);
      });
    }

    static set(target, value, opts = {}) {
      const el = typeof target === 'string' ? document.querySelector(target) : target;
      el?._swSelect?.setValue(value);
      return opts;
    }
  }

  window.SW?.register('SWSelect', SWSelect);
  if (window.SW) window.SW.Select = SWSelect;
})();

/* SWCore Navbar — navegação superior moderna, responsiva e acessível.
   Compatível com a marcação sw-navbar existente e com os painéis legados sw2-navbar. */
(function () {
  'use strict';

  let uid = 0;
  const focusable = 'a[href],button:not([disabled]),input:not([disabled]),[tabindex]:not([tabindex="-1"])';
  const idFor = (el, prefix) => el.id || (el.id = `${prefix}-${++uid}`);

  function getOverlay(nav) {
    if (nav._swNavbarOvl) return nav._swNavbarOvl;
    let ovl = document.querySelector('[sw-navbar-ovl]');
    if (!ovl) {
      ovl = document.createElement('div');
      ovl.setAttribute('sw-navbar-ovl', '');
      ovl.setAttribute('aria-hidden', 'true');
      document.body.appendChild(ovl);
    }
    ovl.addEventListener('click', () => setOpen(nav, false));
    nav._swNavbarOvl = ovl;
    return ovl;
  }

  function getMenu(nav) {
    return nav._swNavbarDrawer || nav.querySelector('[sw-navbar-mn]');
  }

  function getDrawerMenu(nav) {
    if (nav._swNavbarDrawer) return nav._swNavbarDrawer;
    const mn = nav.querySelector('[sw-navbar-mn]');
    const mobilePos = (nav.getAttribute('sw-navbar-mobile') || 'right').toLowerCase();
    if (!mn || mobilePos === 'top') return mn;
    const placeholder = document.createComment('sw-navbar-mn-placeholder');
    mn.parentElement.insertBefore(placeholder, mn);
    mn._swPlaceholder = placeholder;
    document.body.appendChild(mn);
    mn.setAttribute('sw-navbar-drawer', '');
    mn.setAttribute('sw-navbar-mobile', mobilePos);
    mn.setAttribute('data-sw-navbar-owner', idFor(nav, 'sw-navbar'));
    nav._swNavbarDrawer = mn;
    return mn;
  }

  function setOpen(nav, open) {
    const mn = getDrawerMenu(nav);
    nav.toggleAttribute('open', open);
    mn?.toggleAttribute('open', open);
    mn?.setAttribute('aria-hidden', String(!open && matchMedia('(max-width: 992px)').matches));
    nav.querySelector('[sw-navbar-tgl]')?.setAttribute('aria-expanded', String(open));
    getOverlay(nav).toggleAttribute('vis', open);
    document.documentElement.toggleAttribute('sw-nav-lock', open);
    if (open) {
      nav._swReturnFocus = document.activeElement;
      requestAnimationFrame(() => mn?.querySelector(focusable)?.focus({ preventScroll: true }));
    } else {
      closeAll(nav);
      nav._swReturnFocus?.focus?.({ preventScroll: true });
    }
    SW.emit(nav, 'sw:navbar:toggle', { open });
  }

  function closeAll(nav) {
    const roots = [nav, nav._swNavbarDrawer].filter(Boolean);
    roots.forEach((root) => root.querySelectorAll('[sw-navbar-it][open], [has-sub][open]').forEach((it) => {
      it.removeAttribute('open');
      it.setAttribute('aria-expanded', 'false');
    }));
  }

  function setupDisclosure(trigger, panel, nav) {
    if (!panel) return;
    trigger.setAttribute('aria-haspopup', 'true');
    trigger.setAttribute('aria-controls', idFor(panel, 'sw-navbar-menu'));
    trigger.setAttribute('aria-expanded', String(trigger.hasAttribute('open')));
    const toggle = (event) => {
      event.preventDefault();
      event.stopPropagation();
      const open = !trigger.hasAttribute('open');
      closeAll(nav);
      trigger.toggleAttribute('open', open);
      trigger.setAttribute('aria-expanded', String(open));
      SW.emit(trigger, 'sw:navbar:dropdown', { open });
    };
    trigger.addEventListener('click', toggle);
    trigger.addEventListener('keydown', (event) => {
      if (event.key === 'Enter' || event.key === ' ' || event.key === 'ArrowDown') {
        toggle(event);
        if (trigger.hasAttribute('open')) requestAnimationFrame(() => panel.querySelector(focusable)?.focus());
      }
    });
  }

  function initDropdowns(nav) {
    nav.querySelectorAll('[sw-navbar-it][sw-navbar-drop]').forEach((trigger) => {
      if (trigger._swNavbarDrop) return;
      trigger._swNavbarDrop = true;
      setupDisclosure(trigger, trigger.nextElementSibling?.hasAttribute('sw-navbar-drop-mn') ? trigger.nextElementSibling : null, nav);
    });
    nav.querySelectorAll('[has-sub]').forEach((trigger) => {
      if (trigger._swNavbarSub) return;
      trigger._swNavbarSub = true;
      setupDisclosure(trigger, trigger.querySelector(':scope > [sw-navbar-drop-sub]'), nav);
    });
  }

  function initActive(nav) {
    const mn = nav.querySelector('[sw-navbar-mn]');
    if (!mn) return;
    mn.querySelector('[sw-navbar-it][act]')?.setAttribute('aria-current', 'page');
    mn.addEventListener('click', (event) => {
      const it = event.target.closest('[sw-navbar-it]');
      if (!it || it.hasAttribute('sw-navbar-drop')) return;
      mn.querySelectorAll('[sw-navbar-it][act]').forEach((el) => { el.removeAttribute('act'); el.removeAttribute('aria-current'); });
      it.setAttribute('act', '');
      it.setAttribute('aria-current', 'page');
      if (matchMedia('(max-width: 992px)').matches) setOpen(nav, false);
    });
  }

  function initKeyboard(nav) {
    document.addEventListener('click', (event) => {
      if (nav.contains(event.target) || nav._swNavbarDrawer?.contains(event.target)) return;
      closeAll(nav);
    });
    document.addEventListener('keydown', (event) => {
      if (event.key === 'Escape') {
        if (nav.hasAttribute('open')) setOpen(nav, false);
        else closeAll(nav);
      }
      if (event.key !== 'Tab' || !nav.hasAttribute('open')) return;
      const menu = getMenu(nav);
      const nodes = [...menu.querySelectorAll(focusable)].filter((el) => el.offsetParent !== null);
      if (!nodes.length) return;
      if (event.shiftKey && document.activeElement === nodes[0]) { event.preventDefault(); nodes.at(-1).focus(); }
      else if (!event.shiftKey && document.activeElement === nodes.at(-1)) { event.preventDefault(); nodes[0].focus(); }
    });
  }

  function initScroll(nav) {
    if (!['fixed', 'sticky'].includes(nav.getAttribute('sw-navbar-mode'))) return;
    const threshold = Number(nav.getAttribute('sw-navbar-shrink-at')) || 40;
    let lastY = window.scrollY;
    let ticking = false;
    const update = () => {
      const y = window.scrollY;
      nav.toggleAttribute('scrolled', y > threshold);
      if (nav.hasAttribute('sw-navbar-autohide')) {
        nav.toggleAttribute('hidden', y > threshold * 2 && y > lastY + 2);
      }
      nav.toggleAttribute('scroll-up', y < lastY);
      lastY = y;
      ticking = false;
    };
    addEventListener('scroll', () => { if (!ticking) { ticking = true; requestAnimationFrame(update); } }, { passive: true });
    update();
  }

  function initNav(nav) {
    if (nav._swNavbar) return;
    nav._swNavbar = true;
    idFor(nav, 'sw-navbar');
    nav.setAttribute('aria-label', nav.getAttribute('aria-label') || 'Navegação principal');
    const btn = nav.querySelector('[sw-navbar-tgl]');
    const mn = nav.querySelector('[sw-navbar-mn]');
    if (btn && mn) {
      btn.setAttribute('aria-controls', idFor(mn, 'sw-navbar-menu'));
      btn.setAttribute('aria-expanded', 'false');
      btn.addEventListener('click', (event) => { event.stopPropagation(); setOpen(nav, !nav.hasAttribute('open')); });
    }
    initDropdowns(nav);
    initActive(nav);
    initKeyboard(nav);
    initScroll(nav);
  }

  function initBasementPanels(root) {
    SW.$('[sw2-navbar-panel]', root).forEach((panel) => {
      if (panel._swPanelInit) return;
      panel._swPanelInit = true;
      const cards = panel.querySelector('[sw2-navbar-panel-cards]');
      const [prev, next] = panel.querySelectorAll('[sw2-navbar-panel-arr]');
      prev?.addEventListener('click', () => cards?.scrollBy({ left: -320, behavior: 'smooth' }));
      next?.addEventListener('click', () => cards?.scrollBy({ left: 320, behavior: 'smooth' }));
    });
  }

  const SWNavbar = {
    initAll(root = document) {
      SW.$('[sw-navbar]', root).forEach(initNav);
      initBasementPanels(root);
    },
    open(nav) { nav = typeof nav === 'string' ? document.querySelector(nav) : nav; if (nav) setOpen(nav, true); },
    close(nav) { nav = typeof nav === 'string' ? document.querySelector(nav) : nav; if (nav) setOpen(nav, false); },
    toggle(nav) { nav = typeof nav === 'string' ? document.querySelector(nav) : nav; if (nav) setOpen(nav, !nav.hasAttribute('open')); }
  };

  window.SW?.register('SWNavbar', SWNavbar);
  if (window.SW) window.SW.Navbar = SWNavbar;
})();

/* SWCore Slider — portado do SW Framework sem mudança de lógica.
   [sw-slider] > .sw-slider-trk > .sw-slider-it, .sw-slider-prv/-nxt, .sw-slider-dot-grp
   sw-slider-effect="zoom" ativa o efeito Ken Burns (itens empilhados, sem clonagem de loop). */
(function () {
  'use strict';

  class SWSliderInst {
    constructor(el) {
      this.el = el;
      this.trk = el.querySelector('.sw-slider-trk');
      this.items = Array.from(el.querySelectorAll('.sw-slider-it'));
      this.btnPrv = el.querySelector('.sw-slider-prv');
      this.btnNxt = el.querySelector('.sw-slider-nxt');
      this.dotWrap = el.querySelector('.sw-slider-dot-grp');
      this.btnPly = el.querySelector('.sw-slider-ply');

      this.show = Math.max(1, parseInt(el.getAttribute('sw-slider-show'), 10) || 1);
      this.loop = el.getAttribute('sw-slider-loop') !== 'false';
      this.auto = parseInt(el.getAttribute('sw-slider-auto'), 10) || 0;
      this.hoverRun = el.getAttribute('sw-slider-hover') === 'run';
      this.effect = el.getAttribute('sw-slider-effect') || 'slide';
      this.dur = el.getAttribute('sw-slider-dur') || null;
      this.scroll = Math.min(this.show, Math.max(1, parseInt(el.getAttribute('sw-slider-scroll'), 10) || this.show));

      this.total = this.items.length;
      this.pages = this.loop
        ? Math.ceil(this.total / this.scroll)
        : Math.max(1, Math.floor((this.total - this.show) / this.scroll) + 1);
      this.page = 0;
      this._paused = false;
      this._timer = null;
      this._busy = false;
      this._infinite = false;

      if (!this.trk || !this.total) return;

      this._setupInfinite();
      this._buildDots();
      this._bind();
      this._layout(false);
      if (this.auto > 0) this._timerStart();
    }

    _setupInfinite() {
      if (!this.loop || this.effect === 'zoom') return;
      this._infinite = true;
      const appCount = this.show + Math.max(0, this.pages * this.scroll - this.total);
      this.items.slice(-this.scroll).slice().reverse()
        .forEach((it) => this.trk.insertBefore(it.cloneNode(true), this.trk.firstChild));
      this.items.slice(0, appCount)
        .forEach((it) => this.trk.appendChild(it.cloneNode(true)));
      this._extPos = 1;
    }

    _buildDots() {
      if (!this.dotWrap) return;
      this.dotWrap.innerHTML = '';
      this.dots = Array.from({ length: this.pages }, (_, i) => {
        const btn = document.createElement('button');
        btn.type = 'button';
        btn.className = 'sw-slider-dot';
        btn.setAttribute('aria-label', `Página ${i + 1}`);
        btn.addEventListener('click', () => this._goTo(i));
        this.dotWrap.appendChild(btn);
        return btn;
      });
    }

    _timerStart() {
      window.clearInterval(this._timer);
      this._timer = window.setInterval(() => this._step(1), this.auto);
    }

    _timerStop() {
      window.clearInterval(this._timer);
      this._timer = null;
    }

    _bind() {
      this.btnPrv?.addEventListener('click', (event) => { event.preventDefault(); this._step(-1); });
      this.btnNxt?.addEventListener('click', (event) => { event.preventDefault(); this._step(1); });

      this.btnPly?.addEventListener('click', (event) => {
        event.preventDefault();
        this._paused = !this._paused;
        this._paused ? this._timerStop() : this._timerStart();
        this.btnPly.classList.toggle('is-paused', this._paused);
      });

      if (this.auto > 0 && !this.hoverRun) {
        this.el.addEventListener('mouseenter', () => { if (!this._paused) this._timerStop(); });
        this.el.addEventListener('mouseleave', () => { if (!this._paused) this._timerStart(); });
      }

      let startX = 0;
      this.el.addEventListener('touchstart', (event) => { startX = event.touches[0].clientX; }, { passive: true });
      this.el.addEventListener('touchend', (event) => {
        const diff = startX - event.changedTouches[0].clientX;
        if (Math.abs(diff) > 50) this._step(diff > 0 ? 1 : -1);
      });

      this.el.setAttribute('tabindex', '0');
      this.el.addEventListener('keydown', (event) => {
        if (event.key === 'ArrowLeft') { event.preventDefault(); this._step(-1); }
        if (event.key === 'ArrowRight') { event.preventDefault(); this._step(1); }
      });

      if (window.ResizeObserver) {
        this._ro = new ResizeObserver(() => this._layout(false));
        this._ro.observe(this.el);
      } else {
        window.addEventListener('resize', () => this._layout(false), { passive: true });
      }
    }

    _step(dir) {
      if (this._busy) return;
      if (this._infinite) {
        if (this.el.clientWidth === 0) return;
        this._busy = true;
        this._extPos += dir;
        this._layout(true);
        const done = () => {
          this.trk.removeEventListener('transitionend', done);
          if (this._extPos === 0) { this._extPos = this.pages; this._layout(false); }
          else if (this._extPos === this.pages + 1) { this._extPos = 1; this._layout(false); }
          this._busy = false;
        };
        this.trk.addEventListener('transitionend', done);
        const durMs = this.dur ? (this.dur.includes('ms') ? parseFloat(this.dur) : parseFloat(this.dur) * 1000) : 500;
        window.setTimeout(() => { if (this._busy) done(); }, durMs + 100);
      } else {
        let next = this.page + dir;
        if (this.loop) next = ((next % this.pages) + this.pages) % this.pages;
        else next = Math.max(0, Math.min(next, this.pages - 1));
        this._goTo(next);
      }
    }

    _goTo(page) {
      if (this._infinite) this._extPos = page + 1;
      this.page = page;
      this._layout(true);
      SW.emit(this.el, 'sw:slider:change', { page });
    }

    _layout(animate) {
      if (this.effect === 'zoom') { this._layoutZoom(); return; }
      const width = this.el.clientWidth;
      if (width === 0) return;

      const itemW = width / this.show;
      this.trk.querySelectorAll('.sw-slider-it').forEach((it) => { it.style.width = `${itemW}px`; });

      const pos = this._infinite ? this._extPos : this.page;
      const offset = pos * this.scroll * itemW;
      const customTr = this.dur ? `transform ${this.dur} cubic-bezier(.4,0,.2,1)` : '';

      if (!animate) {
        this.trk.style.transition = 'none';
        this.trk.style.transform = `translateX(-${offset}px)`;
        void this.trk.getBoundingClientRect();
        this.trk.style.transition = customTr;
      } else {
        if (customTr) this.trk.style.transition = customTr;
        this.trk.style.transform = `translateX(-${offset}px)`;
      }

      if (this._infinite) this.page = (((this._extPos - 1) % this.pages) + this.pages) % this.pages;
      this.dots?.forEach((dot, i) => dot.classList.toggle('is-act', i === this.page));
    }

    _layoutZoom() {
      const prevIdx = this.items.findIndex((it) => it.classList.contains('is-act'));
      const fadeDur = this.dur || null;
      const incoming = this.items[this.page];
      incoming.classList.remove('is-out');
      if (fadeDur) incoming.style.transition = `opacity ${fadeDur} ease`;
      incoming.style.setProperty('--kb-anim', `sw-slider-kb-${(this.page % 4) + 1}`);
      const child = incoming.firstElementChild;
      if (child) {
        child.style.animation = 'none';
        void incoming.getBoundingClientRect();
        child.style.animation = '';
      }
      incoming.classList.add('is-act');

      if (prevIdx !== -1 && prevIdx !== this.page) {
        const outgoing = this.items[prevIdx];
        if (fadeDur) outgoing.style.transition = `opacity ${fadeDur} ease`;
        outgoing.classList.remove('is-act');
        outgoing.classList.add('is-out');
        outgoing.addEventListener('transitionend', () => outgoing.classList.remove('is-out'), { once: true });
      }
      this.dots?.forEach((dot, i) => dot.classList.toggle('is-act', i === this.page));
    }
  }

  class SWSlider {
    static initAll(root = document) {
      SW.$('[sw-slider]', root).forEach((el) => {
        if (el._swSlider) return;
        el._swSlider = new SWSliderInst(el);
      });
    }

    static next(el) { el._swSlider?._step(1); }
    static prev(el) { el._swSlider?._step(-1); }
    static goTo(el, idx) { el._swSlider?._goTo(idx); }
  }

  window.SW?.register('SWSlider', SWSlider);
  if (window.SW) window.SW.Slider = SWSlider;
})();

/* SWCore Carousel — portado do SW Framework sem mudança de lógica.
   [sw-carousel] > .sw-carousel-trk > itens; troca com efeitos (fade/slide/zoom/bounce/flip) */
(function () {
  'use strict';

  class SWCarouselInst {
    constructor(el) {
      this.el = el;
      this.trk = el.querySelector('.sw-carousel-trk');
      if (!this.trk) return;
      this.items = Array.from(this.trk.children);
      if (!this.items.length) return;
      this.cur = 0;
      this.busy = false;
      this.effect = el.getAttribute('sw-carousel-effect') || (el.classList.contains('is-fde') ? 'fade' : 'slide-left');
      this.dur = parseInt(el.getAttribute('sw-carousel-dur'), 10) || 600;
      this.loop = el.getAttribute('sw-carousel-loop') !== 'false';
      this.auto = parseInt(el.getAttribute('sw-carousel-auto'), 10) || 0;
      this.timer = null;
      this.dts = el.querySelector('.sw-carousel-dts');
      this.prv = el.querySelector('.sw-carousel-prv');
      this.nxt = el.querySelector('.sw-carousel-nxt');
      this._build();
      this._bind();
      if (this.auto) this._resume();
    }

    _build() {
      this.el.style.setProperty('--sw-car-dur', `${this.dur}ms`);
      this.items.forEach((it, i) => {
        it.style.transition = 'none';
        it.style.opacity = i === 0 ? '1' : '0';
        it.style.visibility = i === 0 ? 'visible' : 'hidden';
        it.style.transform = 'none';
        it.classList.toggle('is-act', i === 0);
      });
      void this.trk.offsetHeight;
      this.items.forEach((it) => { it.style.transition = ''; });
      if (this.dts) {
        this.dts.innerHTML = '';
        this.items.forEach((_, i) => {
          const dot = document.createElement('button');
          dot.type = 'button';
          dot.className = `sw-carousel-dot${i === 0 ? ' is-act' : ''}`;
          dot.setAttribute('aria-label', `Slide ${i + 1}`);
          dot.addEventListener('click', () => this._goTo(i));
          this.dts.appendChild(dot);
        });
      }
    }

    _bind() {
      this.prv?.addEventListener('click', () => this._go(-1));
      this.nxt?.addEventListener('click', () => this._go(1));
      let sx = 0;
      this.el.addEventListener('pointerdown', (event) => { sx = event.clientX; });
      this.el.addEventListener('pointerup', (event) => {
        const dx = event.clientX - sx;
        if (Math.abs(dx) > 40) this._go(dx < 0 ? 1 : -1);
      });
      if (this.auto) {
        this.el.addEventListener('mouseenter', () => this._pause());
        this.el.addEventListener('mouseleave', () => this._resume());
      }
    }

    _go(dir) {
      let next = this.cur + dir;
      if (this.loop) next = (next + this.items.length) % this.items.length;
      else if (next < 0 || next >= this.items.length) return;
      this._goTo(next);
    }

    _goTo(idx) {
      if (this.busy || idx === this.cur) return;
      this.busy = true;
      const prev = this.cur;
      const out = this.items[prev];
      const inn = this.items[idx];
      const eff = inn.getAttribute('sw-carousel-effect') || this.effect;

      out.style.position = 'absolute';
      inn.style.position = 'relative';
      inn.style.transition = 'none';
      inn.style.visibility = 'visible';
      inn.style.opacity = '0';
      inn.style.zIndex = '1';
      out.style.zIndex = '0';
      this._setStart(inn, eff);

      void inn.getBoundingClientRect();
      inn.style.transition = '';
      inn.style.opacity = '1';
      inn.style.transform = 'none';

      out.style.opacity = '0';
      this._setOut(out, eff);

      this.cur = idx;
      if (this.dts) Array.from(this.dts.children).forEach((dot, i) => dot.classList.toggle('is-act', i === idx));
      SW.emit(this.el, 'sw:carousel:change', { index: idx });

      window.setTimeout(() => {
        out.style.visibility = 'hidden';
        out.style.transform = 'none';
        out.style.zIndex = '';
        inn.style.zIndex = '';
        out.classList.remove('is-act');
        inn.classList.add('is-act');
        out.style.position = '';
        inn.style.position = '';
        this.busy = false;
      }, this.dur);
    }

    _setStart(item, eff) {
      const map = {
        'slide-left': 'translateX(100%)', 'slide-right': 'translateX(-100%)',
        'slide-up': 'translateY(100%)', 'slide-down': 'translateY(-100%)',
        'zoom-in': 'scale(0.85)', 'zoom-out': 'scale(1.15)',
        bounce: 'scale(0.7) translateY(40px)', flip: 'rotateY(90deg)'
      };
      item.style.transform = map[eff] || 'none';
    }

    _setOut(item, eff) {
      const map = {
        'slide-left': 'translateX(-100%)', 'slide-right': 'translateX(100%)',
        'slide-up': 'translateY(-100%)', 'slide-down': 'translateY(100%)',
        'zoom-in': 'scale(1.15)', 'zoom-out': 'scale(0.85)',
        bounce: 'scale(0.7) translateY(-40px)', flip: 'rotateY(-90deg)'
      };
      item.style.transform = map[eff] || 'none';
    }

    _pause() { window.clearInterval(this.timer); this.timer = null; }
    _resume() {
      if (!this.auto) return;
      this._pause();
      this.timer = window.setInterval(() => this._go(1), this.auto);
    }
  }

  class SWCarousel {
    static initAll(root = document) {
      SW.$('[sw-carousel]', root).forEach((el) => {
        if (el._swCarousel) return;
        el._swCarousel = new SWCarouselInst(el);
      });
    }

    static next(el) { el._swCarousel?._go(1); }
    static prev(el) { el._swCarousel?._go(-1); }
    static goTo(el, idx) { el._swCarousel?._goTo(idx); }
    static pause(el) { el._swCarousel?._pause(); }
    static resume(el) { el._swCarousel?._resume(); }
  }

  window.SW?.register('SWCarousel', SWCarousel);
  if (window.SW) window.SW.Carousel = SWCarousel;
})();

/* SWCore Valid — portado do SW Framework sem mudança de lógica. Validação acessível de
   formulário sobre as constraints nativas do HTML. <form sw-valid sw-valid-live="true|blur|false"> */
(function () {
  'use strict';

  const instances = new WeakMap();
  let fieldCounter = 0;

  class SWValid {
    constructor(form) {
      this.form = form;
      this.form.noValidate = true;
      this.live = form.getAttribute('sw-valid-live') || 'true';
      this.fields().forEach((field) => this.bind(field));
      this.form.addEventListener('submit', (event) => {
        if (!this.check({ focus: true })) {
          event.preventDefault();
          SW.emit(this.form, 'sw:valid-error', { form: this.form });
          return;
        }
        if (!SW.emit(this.form, 'sw:valid-submit', { form: this.form })) event.preventDefault();
      });
      this.form.addEventListener('reset', () => {
        this.fields().forEach((field) => {
          field.classList.remove('is-invalid', 'is-valid');
          field.removeAttribute('aria-invalid');
          delete field.dataset.swValidTouched;
          const error = field.id ? this.form.querySelector(`#${CSS.escape(field.id)}-error`) : null;
          if (error) { error.hidden = true; error.textContent = ''; }
        });
        delete this.form.dataset.swValidState;
      });
    }

    fields() {
      return Array.from(this.form.elements).filter((field) =>
        field instanceof HTMLElement &&
        typeof field.checkValidity === 'function' &&
        !field.disabled &&
        !['button', 'submit', 'reset', 'hidden'].includes(field.type)
      );
    }

    bind(field) {
      if (field._swValidInit) return;
      field._swValidInit = true;
      if (this.live === 'false') return;

      field.addEventListener('blur', () => {
        field.dataset.swValidTouched = 'true';
        this.validateField(field);
        this.updateState();
      });
      if (this.live !== 'blur') {
        field.addEventListener('input', () => {
          if (field.dataset.swValidTouched === 'true' || field.getAttribute('aria-invalid') === 'true') {
            this.validateField(field);
            this.updateState();
          }
        });
      }
      field.addEventListener('change', () => {
        field.dataset.swValidTouched = 'true';
        this.validateField(field);
        this.updateState();
      });
    }

    errorElement(field) {
      if (!field.id) {
        fieldCounter += 1;
        field.id = `sw-field-${fieldCounter}`;
      }
      const errorId = `${field.id}-error`;
      let error = this.form.querySelector(`#${CSS.escape(errorId)}`);
      if (!error) {
        error = document.createElement('p');
        error.id = errorId;
        error.className = 'sw-form-error';
        error.setAttribute('role', 'alert');
        error.hidden = true;
        (field.closest('.sw-form-group') || field.parentElement || this.form).appendChild(error);
      }
      return error;
    }

    validateField(field) {
      const valid = field.checkValidity();
      const error = this.errorElement(field);
      field.classList.toggle('is-invalid', !valid);
      field.classList.toggle('is-valid', valid && field.dataset.swValidTouched === 'true' && String(field.value || '') !== '');

      if (!valid) {
        field.setAttribute('aria-invalid', 'true');
        const describedBy = new Set((field.getAttribute('aria-describedby') || '').split(/\s+/).filter(Boolean));
        describedBy.add(error.id);
        field.setAttribute('aria-describedby', Array.from(describedBy).join(' '));
        error.textContent = field.dataset.swError || field.validationMessage || 'Revise este campo.';
        error.hidden = false;
      } else {
        field.removeAttribute('aria-invalid');
        const describedBy = (field.getAttribute('aria-describedby') || '').split(/\s+/).filter((id) => id && id !== error.id);
        if (describedBy.length) field.setAttribute('aria-describedby', describedBy.join(' '));
        else field.removeAttribute('aria-describedby');
        error.textContent = '';
        error.hidden = true;
      }
      return valid;
    }

    updateState() {
      const valid = this.fields().every((field) => field.checkValidity());
      this.form.dataset.swValidState = valid ? 'valid' : 'invalid';
      return valid;
    }

    check({ focus = true } = {}) {
      const fields = this.fields();
      let firstInvalid = null;
      fields.forEach((field) => {
        field.dataset.swValidTouched = 'true';
        if (!this.validateField(field) && !firstInvalid) firstInvalid = field;
      });
      this.form.dataset.swValidState = firstInvalid ? 'invalid' : 'valid';
      if (firstInvalid && focus) firstInvalid.focus({ preventScroll: false });
      return !firstInvalid;
    }

    static initAll(root = document) {
      SW.$('form[sw-valid]', root).forEach((form) => this.init(form));
    }

    static init(form) {
      if (!(form instanceof HTMLFormElement)) return null;
      if (!instances.has(form)) instances.set(form, new SWValid(form));
      const instance = instances.get(form);
      instance.fields().forEach((field) => instance.bind(field));
      return instance;
    }

    static check(target, options = {}) {
      const form = typeof target === 'string' ? document.querySelector(target) : target;
      return this.init(form)?.check(options) ?? false;
    }
  }

  window.SW?.register('SWValid', SWValid);
  if (window.SW) window.SW.Valid = SWValid;
})();

/* SWCore Mask — portado do SW Framework sem mudança de lógica. Máscaras de input: nomes fixos
   (cpf/cnpj/telefone/cep/data/hora/cartao/dinheiro/valor) e padrões por token via
   sw-mask="custom" sw-mask-mask="##.###.###-#" (# dígito, A letra, * qualquer)
   ou registrados com SWMask.addMask(nome, padrão). */
(function () {
  'use strict';

  const joinParts = (digits, sizes, separators) => {
    let cursor = 0;
    let output = '';
    sizes.forEach((size, index) => {
      const part = digits.slice(cursor, cursor + size);
      if (!part) return;
      if (output && separators[index - 1]) output += separators[index - 1];
      output += part;
      cursor += size;
    });
    return output;
  };

  const moneyFormat = (digits) => {
    if (!digits) return '';
    return digits.replace(/(\d)(\d{2})$/, '$1,$2').replace(/(?=(\d{3})+(?!\d))\B/g, '.');
  };

  const patternFormat = (pattern, raw) => {
    let out = '';
    let ri = 0;
    for (let pi = 0; pi < pattern.length && ri < raw.length; pi += 1) {
      const token = pattern[pi];
      if (token === '#' || token === 'A' || token === '*') {
        while (ri < raw.length) {
          const ch = raw[ri];
          ri += 1;
          const ok = token === '*' || (token === '#' && /[0-9]/.test(ch)) || (token === 'A' && /[a-zA-Z]/.test(ch));
          if (ok) { out += ch; break; }
        }
      } else {
        out += token;
        if (raw[ri] === token) ri += 1;
      }
    }
    return out;
  };

  const limits = { cpf: 11, cnpj: 14, document: 14, phone: 11, telefone: 11, cep: 8, date: 8, data: 8, hora: 4, cartao: 16 };

  const formatters = {};
  formatters.cpf = (digits) => joinParts(digits, [3, 3, 3, 2], ['.', '.', '-']);
  formatters.cnpj = (digits) => joinParts(digits, [2, 3, 3, 4, 2], ['.', '.', '/', '-']);
  formatters.document = (digits) => (digits.length <= 11 ? formatters.cpf(digits) : formatters.cnpj(digits));
  formatters.phone = (digits) => {
    if (!digits) return '';
    const area = digits.slice(0, 2);
    const middleSize = digits.length > 10 ? 5 : 4;
    const middle = digits.slice(2, 2 + middleSize);
    const end = digits.slice(2 + middleSize);
    return `${digits.length > 2 ? `(${area}) ` : area}${middle}${end ? `-${end}` : ''}`;
  };
  formatters.telefone = formatters.phone;
  formatters.cep = (digits) => joinParts(digits, [5, 3], ['-']);
  formatters.date = (digits) => joinParts(digits, [2, 2, 4], ['/', '/']);
  formatters.data = formatters.date;
  formatters.hora = (digits) => joinParts(digits, [2, 2], [':']);
  formatters.cartao = (digits) => joinParts(digits, [4, 4, 4, 4], [' ', ' ', ' ']);
  formatters.valor = (digits) => moneyFormat(digits.slice(0, 15));
  formatters.dinheiro = (digits) => {
    const value = moneyFormat(digits.slice(0, 15));
    return value ? `R$ ${value}` : '';
  };

  const customMasks = {};

  class SWMask {
    static initAll(root = document) {
      SW.$('input[sw-mask]', root).forEach((input) => this.init(input));
    }

    static init(input) {
      if (!(input instanceof HTMLInputElement) || input._swMaskInit) return false;
      const name = input.getAttribute('sw-mask');
      const pattern = name === 'custom' ? input.getAttribute('sw-mask-mask') : customMasks[name];
      if (!formatters[name] && !pattern) return false;
      input._swMaskInit = true;
      input._swMaskPattern = pattern || null;
      if (!input.hasAttribute('inputmode') && !pattern) input.inputMode = 'numeric';
      input.addEventListener('input', () => this.apply(input));
      this.apply(input, false);
      return true;
    }

    static _digits(input) {
      const name = input.getAttribute('sw-mask');
      const pattern = input._swMaskPattern;
      const raw = String(input.value || '');
      if (pattern) return raw.replace(/[^0-9a-zA-Z]/g, '');
      const limit = limits[name];
      const digits = raw.replace(/\D/g, '');
      return typeof limit === 'number' ? digits.slice(0, limit) : digits;
    }

    static apply(input, emit = true) {
      if (!(input instanceof HTMLInputElement)) return '';
      const name = input.getAttribute('sw-mask');
      const pattern = input._swMaskPattern;
      const digits = this._digits(input);
      const formatted = pattern ? patternFormat(pattern, digits) : (formatters[name] ? formatters[name](digits) : '');
      input.value = formatted;
      input.dataset.swMaskValue = digits;
      if (emit) SW.emit(input, 'sw:mask-input', { mask: name, raw: digits, value: input.value });
      return digits;
    }

    static raw(target) {
      const input = typeof target === 'string' ? document.querySelector(target) : target;
      if (!(input instanceof HTMLInputElement)) return '';
      return this.apply(input, false);
    }

    static set(target, value, { emit = true } = {}) {
      const input = typeof target === 'string' ? document.querySelector(target) : target;
      if (!(input instanceof HTMLInputElement)) return false;
      input.value = String(value ?? '');
      this.apply(input, emit);
      return true;
    }

    static addMask(name, pattern) {
      customMasks[name] = pattern;
    }
  }

  window.SW?.register('SWMask', SWMask);
  if (window.SW) window.SW.Mask = SWMask;
  window.SWMask = SWMask;
})();

/* SWCore Material Input — portado do SW Framework sem mudança de lógica.
   <div class="fld-mat"><input sw-matinp sw-matinp-label="Nome" sw-matinp-req ...></div> */
(function () {
  'use strict';

  const masks = {
    telefone: (v) => applyPattern(v.replace(/\D/g, ''), v.replace(/\D/g, '').length <= 10 ? '(##) ####-####' : '(##) #####-####'),
    cpf: (v) => applyPattern(v.replace(/\D/g, ''), '###.###.###-##'),
    cnpj: (v) => applyPattern(v.replace(/\D/g, ''), '##.###.###/####-##'),
    cep: (v) => applyPattern(v.replace(/\D/g, ''), '#####-###'),
    data: (v) => applyPattern(v.replace(/\D/g, ''), '##/##/####'),
    hora: (v) => applyPattern(v.replace(/\D/g, ''), '##:##'),
    cartao: (v) => applyPattern(v.replace(/\D/g, ''), '#### #### #### ####')
  };

  function applyPattern(digits, pattern) {
    let i = 0;
    return pattern.split('').map((c) => (c === '#' ? (digits[i++] ?? '') : (i < digits.length ? c : ''))).join('');
  }

  const rules = {
    req: (v, _p, el) => v.trim() !== '' || (el.getAttribute('sw-matinp-msg-req') || 'Campo obrigatório.'),
    email: (v, _p, el) => !v || /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v) || (el.getAttribute('sw-matinp-msg-email') || 'E-mail inválido.'),
    url: (v, _p, el) => !v || /^https?:\/\/.+/.test(v) || (el.getAttribute('sw-matinp-msg-url') || 'URL inválida.'),
    minlen: (v, p, el) => !v || v.length >= parseInt(p, 10) || (el.getAttribute('sw-matinp-msg-minlen') || `Mínimo ${p} caracteres.`),
    maxlen: (v, p, el) => !v || v.length <= parseInt(p, 10) || (el.getAttribute('sw-matinp-msg-maxlen') || `Máximo ${p} caracteres.`),
    min: (v, p, el) => !v || parseFloat(v) >= parseFloat(p) || (el.getAttribute('sw-matinp-msg-min') || `Valor mínimo: ${p}.`),
    max: (v, p, el) => !v || parseFloat(v) <= parseFloat(p) || (el.getAttribute('sw-matinp-msg-max') || `Valor máximo: ${p}.`),
    match: (v, p, el) => {
      const other = document.querySelector(p);
      return !other || v === other.value || (el.getAttribute('sw-matinp-msg-match') || 'Os valores não coincidem.');
    },
    regex: (v, p, el) => {
      try { return !v || new RegExp(p).test(v) || (el.getAttribute('sw-matinp-msg-regex') || 'Formato inválido.'); }
      catch (_) { return true; }
    }
  };

  class SWMatinpInst {
    constructor(el) {
      this._inp = el;
      this._wrap = el.closest('.fld-mat') || el.parentElement;
      this._mask = el.getAttribute('sw-matinp-mask') || null;
      this._validateOn = el.getAttribute('sw-matinp-validate') || 'blur';
      el._swMatinp = this;
      this._build();
      this._bindEvents();
    }

    _build() {
      const el = this._inp;
      const wrap = this._wrap;
      if (!el.placeholder) el.placeholder = ' ';

      let lbl = wrap.querySelector('.fld-mat-lbl');
      if (!lbl && el.getAttribute('sw-matinp-label')) {
        lbl = document.createElement('label');
        lbl.className = 'fld-mat-lbl';
        lbl.textContent = el.getAttribute('sw-matinp-label');
        if (el.id) lbl.setAttribute('for', el.id);
        el.insertAdjacentElement('afterend', lbl);
      }

      const hintText = el.getAttribute('sw-matinp-hint');
      if (hintText) {
        let hint = wrap.querySelector('.fld-mat-hint');
        if (!hint) {
          hint = document.createElement('span');
          hint.className = 'fld-mat-hint';
          wrap.appendChild(hint);
        }
        hint.textContent = hintText;
        this._hint = hint;
      }

      let msg = wrap.querySelector('.fld-mat-msg');
      if (!msg) {
        msg = document.createElement('span');
        msg.className = 'fld-mat-msg';
        msg.setAttribute('aria-live', 'polite');
        wrap.appendChild(msg);
      }
      this._msg = msg;
    }

    _bindEvents() {
      const el = this._inp;
      if (this._mask && masks[this._mask]) {
        el.addEventListener('input', () => {
          el.value = masks[this._mask](el.value);
          SW.emit(el, 'sw:matinp:change', { value: el.value });
        });
      }

      if (this._validateOn === 'input') {
        el.addEventListener('input', () => { this.validate(); SW.emit(el, 'sw:matinp:change', { value: el.value }); });
      } else if (this._validateOn === 'change') {
        el.addEventListener('change', () => this.validate());
      } else {
        el.addEventListener('blur', () => this.validate());
        el.addEventListener('input', () => {
          if (this._wrap.classList.contains('is-err')) this.validate();
          SW.emit(el, 'sw:matinp:change', { value: el.value });
        });
      }
    }

    validate() {
      const el = this._inp;
      const val = el.value;
      let error = '';
      for (const [rule, fn] of Object.entries(rules)) {
        if (!el.hasAttribute(`sw-matinp-${rule}`)) continue;
        const param = el.getAttribute(`sw-matinp-${rule}`) || '';
        const res = fn(val, param, el);
        if (res !== true) { error = res; break; }
      }
      if (error) {
        this.setError(error);
        SW.emit(el, 'sw:matinp:invalid', { value: val, error });
        return false;
      }
      this.clearError();
      if (val !== '') {
        this._wrap.classList.add('is-ok');
        SW.emit(el, 'sw:matinp:valid', { value: val });
      }
      return true;
    }

    setError(msg) {
      this._wrap.classList.add('is-err');
      this._wrap.classList.remove('is-ok');
      if (this._msg) this._msg.textContent = msg;
      if (this._hint) this._hint.style.display = 'none';
    }

    clearError() {
      this._wrap.classList.remove('is-err');
      if (this._msg) this._msg.textContent = '';
      if (this._hint) this._hint.style.display = '';
    }

    getValue() { return this._inp.value; }
    setValue(v) { this._inp.value = v; this.validate(); }
    reset() { this._inp.value = ''; this.clearError(); this._wrap.classList.remove('is-ok'); }
  }

  class SWMatinp {
    static initAll(root = document) {
      SW.$('[sw-matinp]', root).forEach((el) => {
        if (el._swMatinp) return;
        new SWMatinpInst(el);
      });
    }

    static addRule(name, fn) { rules[name] = fn; }
  }

  window.SW?.register('SWMatinp', SWMatinp);
  if (window.SW) window.SW.Matinp = SWMatinp;
})();

/* SWCore Scroll-to-Top — portado do SW Framework sem mudança de lógica.
   [sw-top] no botão; fica .is-vis após 300px de scroll */
(function () {
  'use strict';
  let bound = false;

  const SWTop = {
    initAll(root = document) {
      SW.$('[sw-top]', root).forEach((btn) => {
        if (btn._swTop) return;
        btn._swTop = true;
        btn.addEventListener('click', () => window.scrollTo({ top: 0, behavior: SW.Utils?.reducedMotion() ? 'auto' : 'smooth' }));
      });

      if (bound) return;
      bound = true;
      const update = () => {
        const visible = window.scrollY > 300;
        document.querySelectorAll('[sw-top]').forEach((btn) => btn.classList.toggle('is-vis', visible));
      };
      window.addEventListener('scroll', update, { passive: true });
      update();
    }
  };

  window.SW?.register('SWTop', SWTop);
  if (window.SW) window.SW.Top = SWTop;
})();

/* SWCore Smooth Scroll — portado do SW Framework, com uma extensão real: o destino pode
   declarar sw-scroll-path (URL bonita, tipo /sobre, via History API), sw-scroll-title (troca
   document.title), sw-scroll-desc (troca <meta name="description">) e sw-scroll-canonical="true"
   (atualiza <link rel="canonical"> pra apontar pro caminho bonito) — dá pra ter uma página única
   que se comporta como se tivesse rotas de verdade, sem hash feio na URL (#ancora) e com SEO
   correto por seção. Sem sw-scroll-path no destino, o link continua se comportando exatamente
   como antes (só rola, não mexe na URL/head). Recarregar a página numa URL bonita, ou
   voltar/avançar pelo histórico do navegador, também funciona (registry + popstate, restaura
   título/descrição/canônica originais ao sair de toda seção registrada). Exige que o servidor
   sirva o mesmo HTML pra qualquer caminho registrado (rewrite/fallback de SPA) — isso é
   configuração de servidor, fica de fora do componente.
   <a sw-scroll href="#secao"> ou sw-scroll-target/-offset · SW.Scroll.to('#sel', offset)
   <section id="secao" sw-scroll-path="/sobre" sw-scroll-title="Sobre nós — Minha Empresa"
            sw-scroll-desc="Conheça a história da empresa." sw-scroll-canonical="true"> */
(function () {
  'use strict';

  const registry = new Map(); // path -> elemento do destino
  let routingBound = false;
  // Estado de antes de qualquer troca — pra restaurar ao voltar pra um caminho não registrado
  let home = null;

  function buildRegistry(root) {
    SW.$('[sw-scroll-path]', root).forEach((el) => {
      const path = el.getAttribute('sw-scroll-path');
      if (path) registry.set(path, el);
    });
  }

  function metaDescEl() {
    let el = document.querySelector('meta[name="description"]');
    if (!el) {
      el = document.createElement('meta');
      el.setAttribute('name', 'description');
      document.head.appendChild(el);
    }
    return el;
  }

  function canonicalEl() {
    let el = document.querySelector('link[rel="canonical"]');
    if (!el) {
      el = document.createElement('link');
      el.setAttribute('rel', 'canonical');
      document.head.appendChild(el);
    }
    return el;
  }

  function captureHome() {
    const descEl = document.querySelector('meta[name="description"]');
    const canonEl = document.querySelector('link[rel="canonical"]');
    home = {
      title: document.title,
      desc: descEl?.getAttribute('content') ?? null,
      canonical: canonEl?.getAttribute('href') ?? null,
    };
  }

  // Restaura pro estado de antes de qualquer seção ser visitada — remove de vez a tag,
  // em vez de só deixá-la parada no último valor, se ela não existia originalmente.
  function restoreHome() {
    if (!home) return;
    document.title = home.title;
    if (home.desc !== null) metaDescEl().setAttribute('content', home.desc);
    else document.querySelector('meta[name="description"]')?.remove();
    if (home.canonical !== null) canonicalEl().setAttribute('href', home.canonical);
    else document.querySelector('link[rel="canonical"]')?.remove();
  }

  function applyRoute(path, { push, smooth }) {
    const el = registry.get(path);
    if (!el) return false;

    const title = el.getAttribute('sw-scroll-title');
    if (title) document.title = title;

    const desc = el.getAttribute('sw-scroll-desc');
    if (desc) metaDescEl().setAttribute('content', desc);

    if (el.hasAttribute('sw-scroll-canonical')) {
      canonicalEl().setAttribute('href', window.location.origin + path);
    }

    if (push) window.history.pushState({ swScrollPath: path }, '', path);
    const offset = parseInt(el.getAttribute('sw-scroll-offset'), 10) || 80;
    const top = el.getBoundingClientRect().top + window.scrollY - offset;
    window.scrollTo({ top, behavior: smooth && !SW.Utils?.reducedMotion() ? 'smooth' : 'auto' });
    SW.emit(el, 'sw:scroll:arrived');
    return true;
  }

  const SWScroll = {
    initAll(root = document) {
      buildRegistry(root);

      SW.$('[sw-scroll]', root).forEach((el) => {
        if (el._swScroll) return;
        el._swScroll = true;
        el.addEventListener('click', (event) => {
          const sel = el.getAttribute('sw-scroll-target') || el.getAttribute('href');
          const offset = parseInt(el.getAttribute('sw-scroll-offset'), 10) || 80;
          if (!sel || !sel.startsWith('#')) return;
          event.preventDefault();
          const target = document.querySelector(sel);
          const path = target?.getAttribute('sw-scroll-path');
          if (path) applyRoute(path, { push: true, smooth: true });
          else SWScroll.to(sel, offset);
        });
      });

      // Roteamento (registry + popstate) é global — só faz sentido montar uma vez, na
      // primeira chamada com root=document (chamadas com root menor são reinit de subárvore).
      if (root !== document || routingBound) return;
      routingBound = true;
      captureHome();

      if (registry.has(window.location.pathname)) {
        // Sem animação no load inicial — a página já "nasce" no lugar certo, não desliza até lá.
        window.requestAnimationFrame(() => applyRoute(window.location.pathname, { push: false, smooth: false }));
      }

      window.addEventListener('popstate', () => {
        // Voltar/avançar pra um caminho que não é nenhuma seção registrada (ex.: a própria
        // página de origem) não teria de quem puxar título/descrição/canônica certos — sem
        // isso, ficava preso no estado da última seção visitada mesmo depois de "sair" dela.
        if (!applyRoute(window.location.pathname, { push: false, smooth: true })) restoreHome();
      });
    },

    to(sel, offset = 80) {
      const target = typeof sel === 'string' ? document.querySelector(sel) : sel;
      if (!target) return;
      const top = target.getBoundingClientRect().top + window.scrollY - offset;
      window.scrollTo({ top, behavior: SW.Utils?.reducedMotion() ? 'auto' : 'smooth' });
      SW.emit(target, 'sw:scroll:arrived');
    },

    // Navega por código pro caminho bonito registrado (equivalente a clicar num link que aponta pra ele).
    goto(path) { return applyRoute(path, { push: true, smooth: true }); }
  };

  window.SW?.register('SWScroll', SWScroll);
  if (window.SW) window.SW.Scroll = SWScroll;
})();

/* SWCore Scrollspy — portado do SW Framework sem mudança de lógica.
   <nav sw-scrollspy sw-scrollspy-offset sw-scrollspy-tgt><a href="#sec">...</a></nav> */
(function () {
  'use strict';

  const SWScrollspy = {
    initAll(root = document) {
      SW.$('[sw-scrollspy]', root).forEach((nav) => {
        if (nav._swScrollspy) return;
        nav._swScrollspy = true;
        const offset = parseInt(nav.getAttribute('sw-scrollspy-offset'), 10) || 80;
        const tgtSel = nav.getAttribute('sw-scrollspy-tgt');
        const container = tgtSel ? document.querySelector(tgtSel) : null;
        const scroller = container || window;
        const links = Array.from(nav.querySelectorAll('a[href^="#"]'));
        const targets = links.map((a) => document.querySelector(a.getAttribute('href'))).filter(Boolean);

        let ticking = false;
        const tick = () => {
          if (ticking) return;
          ticking = true;
          window.requestAnimationFrame(() => {
            const refTop = container ? container.getBoundingClientRect().top : 0;
            let cur = -1;
            targets.forEach((t, i) => { if (t.getBoundingClientRect().top - refTop <= offset) cur = i; });
            const atBottom = container
              ? container.scrollTop + container.clientHeight >= container.scrollHeight - 4
              : window.scrollY + window.innerHeight >= document.documentElement.scrollHeight - 4;
            if (atBottom && targets.length) cur = targets.length - 1;
            links.forEach((a, i) => {
              const active = i === cur;
              a.classList.toggle('is-act', active);
              a.setAttribute('aria-current', active ? 'true' : 'false');
              if (active) SW.emit(nav, 'sw:scrollspy:change', { id: targets[i]?.id, link: a });
            });
            ticking = false;
          });
        };

        links.forEach((a, i) => {
          a.addEventListener('click', (event) => {
            event.preventDefault();
            const target = targets[i];
            if (!target) return;
            if (container) {
              const top = target.getBoundingClientRect().top - container.getBoundingClientRect().top + container.scrollTop;
              container.scrollTo({ top: top - offset + 1, behavior: 'smooth' });
            } else {
              const top = target.getBoundingClientRect().top + window.scrollY;
              window.scrollTo({ top: top - offset + 1, behavior: 'smooth' });
            }
          });
        });

        scroller.addEventListener('scroll', tick, { passive: true });
        tick();
      });
    }
  };

  window.SW?.register('SWScrollspy', SWScrollspy);
  if (window.SW) window.SW.Scrollspy = SWScrollspy;
})();

/* SWCore Textarea auto-resize — portado do SW Framework sem mudança de lógica.
   <textarea sw-textarea sw-textarea-min="60" sw-textarea-max="240"> */
(function () {
  'use strict';

  const SWTextarea = {
    initAll(root = document) {
      SW.$('[sw-textarea]', root).forEach((el) => {
        if (el._swTextarea) return;
        el._swTextarea = true;

        const min = parseInt(el.getAttribute('sw-textarea-min'), 10) || parseInt(el.style.minHeight, 10) || 60;
        const max = parseInt(el.getAttribute('sw-textarea-max'), 10) || 0;
        el.style.overflowY = 'hidden';
        el.style.resize = 'none';
        el.style.minHeight = `${min}px`;

        const resize = () => {
          el.style.height = 'auto';
          const cs = window.getComputedStyle(el);
          const borderBox = cs.boxSizing === 'border-box';
          const borderTop = parseFloat(cs.borderTopWidth) || 0;
          const borderBottom = parseFloat(cs.borderBottomWidth) || 0;
          const borderExtra = borderBox ? (borderTop + borderBottom) : 0;

          let h = el.scrollHeight + borderExtra;
          if (h < min) h = min;
          if (max && h > max) {
            h = max;
            el.style.overflowY = 'auto';
          } else {
            el.style.overflowY = 'hidden';
          }
          el.style.height = `${h}px`;
        };

        el.addEventListener('input', resize);
        el.addEventListener('change', resize);
        window.addEventListener('resize', resize);
        setTimeout(resize, 0);
        resize();
      });
    }
  };

  window.SW?.register('SWTextarea', SWTextarea);
  if (window.SW) window.SW.Textarea = SWTextarea;
})();

/* SWCore Textlimit — portado do SW Framework sem mudança de lógica.
   <textarea sw-textlimit sw-textlimit-max="200" sw-textlimit-txt="{count}/{max}" sw-textlimit-align="center"> */
(function () {
  'use strict';

  const SWTextlimit = {
    initAll(root = document) {
      SW.$('[sw-textlimit]', root).forEach((el) => {
        if (el._swTextlimit) return;
        el._swTextlimit = true;
        const max = parseInt(el.getAttribute('sw-textlimit-max'), 10) || parseInt(el.getAttribute('maxlength'), 10) || 150;
        const tpl = el.getAttribute('sw-textlimit-txt') || '{count}/{max}';
        const align = el.getAttribute('sw-textlimit-align') || (el.hasAttribute('sw-textlimit-center') ? 'center' : '');
        el.setAttribute('maxlength', max);

        if (el.hasAttribute('sw-textlimit-center')) {
          el.style.textAlign = 'center';
        }

        const counter = document.createElement('span');
        counter.className = 'sw-textlimit-cnt';
        if (align) {
          counter.style.textAlign = align;
          counter.classList.add(`is-${align}`);
        }
        el.insertAdjacentElement('afterend', counter);

        const update = () => {
          const n = el.value.length;
          const rem = max - n;
          counter.textContent = tpl.replace('{count}', n).replace('{max}', max).replace('{remaining}', rem);
          counter.classList.toggle('is-ale', rem >= 0 && rem <= Math.ceil(max * 0.2));
          counter.classList.toggle('is-err', rem < 0);
        };
        el.addEventListener('input', update);
        update();
      });
    }
  };

  window.SW?.register('SWTextlimit', SWTextlimit);
  if (window.SW) window.SW.Textlimit = SWTextlimit;
})();

/* SWCore Segmented Control — portado do SW Framework sem mudança de lógica.
   <div class="seg"> com <input type="radio"> + <label> ou <div class="seg-it is-act">
   — insere um .seg-thumb que desliza até o item ativo. */
(function () {
  'use strict';

  function getActiveItem(seg) {
    const checkedInput = seg.querySelector('input[type="radio"]:checked');
    if (checkedInput) return checkedInput.nextElementSibling;
    return seg.querySelector('.seg-it.is-act');
  }

  function moveThumb(seg, thumb) {
    const active = getActiveItem(seg);
    if (!active) { thumb.style.width = '0'; return; }
    const segRect = seg.getBoundingClientRect();
    const itemRect = active.getBoundingClientRect();
    thumb.style.width = `${itemRect.width}px`;
    thumb.style.transform = `translateX(${itemRect.left - segRect.left}px)`;
  }

  function ensureThumb(seg) {
    let thumb = seg.querySelector(':scope > .seg-thumb');
    if (!thumb) {
      thumb = document.createElement('div');
      thumb.className = 'seg-thumb';
      seg.insertBefore(thumb, seg.firstChild);
    }
    return thumb;
  }

  const SWSeg = {
    initAll(root = document) {
      SW.$('.seg', root).forEach((seg) => {
        const thumb = ensureThumb(seg);
        if (!seg._swSeg) {
          seg._swSeg = true;
          seg.addEventListener('change', () => moveThumb(seg, thumb));
          seg.addEventListener('click', (event) => {
            if (event.target.closest('.seg-it')) moveThumb(seg, thumb);
          });
          window.addEventListener('resize', () => moveThumb(seg, thumb), { passive: true });
        }
        thumb.style.transition = 'none';
        moveThumb(seg, thumb);
        requestAnimationFrame(() => { thumb.style.transition = ''; });
      });
    }
  };

  window.SW?.register('SWSeg', SWSeg);
  if (window.SW) window.SW.Seg = SWSeg;
})();

/* SWCore Infinite Scroll — portado do SW Framework sem mudança de lógica.
   <div sw-infinite sw-infinite-url="/api/posts?page={page}" sw-infinite-target="#lista" sw-infinite-per sw-infinite-offset>
   Eventos: sw:infinite:load { page, items } · sw:infinite:end · sw:infinite:error { page } */
(function () {
  'use strict';

  class SWInfiniteInst {
    constructor(el) {
      this.el = el;
      this.url = el.getAttribute('sw-infinite-url');
      this.target = document.querySelector(el.getAttribute('sw-infinite-target') || 'body');
      this.offset = parseInt(el.getAttribute('sw-infinite-offset'), 10) || 300;
      this.page = parseInt(el.getAttribute('sw-infinite-start'), 10) || 1;
      this.loading = false;
      this.done = false;

      window.addEventListener('scroll', () => this._check(), { passive: true });
      this._check();
    }

    _check() {
      if (this.loading || this.done) return;
      const dist = document.documentElement.scrollHeight - window.scrollY - window.innerHeight;
      if (dist <= this.offset) this._load();
    }

    _load() {
      this.loading = true;
      const url = this.url.replace('{page}', this.page);

      const spin = document.createElement('div');
      spin.className = 'sw-infinite-loader';
      spin.textContent = 'Carregando…';
      this.target.appendChild(spin);

      fetch(url)
        .then((r) => r.text())
        .then((html) => {
          spin.remove();
          const tmp = document.createElement('div');
          tmp.innerHTML = html;
          const items = Array.from(tmp.children);

          if (!items.length) {
            this.done = true;
            SW.emit(this.el, 'sw:infinite:end');
            return;
          }

          const frag = document.createDocumentFragment();
          items.forEach((it) => frag.appendChild(it));
          this.target.appendChild(frag);
          SW.reinit(this.target);
          this.page += 1;
          SW.emit(this.el, 'sw:infinite:load', { page: this.page, items });
        })
        .catch(() => {
          spin.remove();
          SW.emit(this.el, 'sw:infinite:error', { page: this.page });
        })
        .finally(() => { this.loading = false; });
    }
  }

  class SWInfinite {
    static initAll(root = document) {
      SW.$('[sw-infinite]', root).forEach((el) => {
        if (el._swInfinite) return;
        el._swInfinite = new SWInfiniteInst(el);
      });
    }
  }

  window.SW?.register('SWInfinite', SWInfinite);
  if (window.SW) window.SW.Infinite = SWInfinite;
})();

/* SWCore Panel / Offcanvas — [sw-panel-open="#id"] / [sw-panel="#id"], [sw-panel-close] */
(function () {
  'use strict';

  function getOverlay() {
    let ovl = document.querySelector('.sw-panel-ovl');
    if (!ovl) {
      ovl = document.createElement('div');
      ovl.className = 'sw-panel-ovl';
      document.body.appendChild(ovl);
      ovl.addEventListener('click', () => {
        const active = document.querySelector('.sw-panel.is-active, .sw-panel.is-act');
        if (active) SWPanel.hide(active);
      });
    }
    if (ovl.parentElement !== document.body) {
      document.body.appendChild(ovl);
    }
    return ovl;
  }

  function mountAllToBody(root = document) {
    SW.$('.sw-panel, [sw-panel]', root).forEach((panel) => {
      if (panel.parentElement && panel.parentElement !== document.body) {
        document.body.appendChild(panel);
      }
    });
  }

  class SWPanel {
    static initAll(root = document) {
      mountAllToBody(root);

      SW.$('[sw-panel-open], [sw-panel-target]', root).forEach((trigger) => {
        if (trigger._swPanelInit) return;
        trigger._swPanelInit = true;
        trigger.addEventListener('click', (event) => {
          const selector = trigger.getAttribute('sw-panel-open') || trigger.getAttribute('sw-panel-target');
          if (!selector?.startsWith('#')) return;
          event.preventDefault();
          SWPanel.show(selector, trigger);
        });
      });

      SW.$('.sw-panel, [sw-panel]', root).forEach((panel) => {
        panel.setAttribute('role', 'dialog');
        panel.setAttribute('aria-modal', 'true');
        const isOpen = panel.classList.contains('is-active') || panel.classList.contains('is-act');
        panel.setAttribute('aria-hidden', isOpen ? 'false' : 'true');

        if (panel._swPanelCloseInit) return;
        panel._swPanelCloseInit = true;
        panel.addEventListener('click', (event) => {
          if (event.target.closest('[sw-panel-close], .pnl-cls')) {
            event.preventDefault();
            SWPanel.hide(panel);
          }
        });
      });
    }

    static show(target, trigger = document.activeElement) {
      const panel = typeof target === 'string' ? document.querySelector(target) : target;
      if (!panel || panel.classList.contains('is-active') || panel.classList.contains('is-act')) return false;

      // Garante que o overlay e o painel fiquem no fim do body para sobrepor qualquer stacking context (menu, sidebar, header)
      const ovl = getOverlay();
      document.body.appendChild(ovl);
      document.body.appendChild(panel);

      panel._swPreviousFocus = trigger instanceof HTMLElement ? trigger : document.activeElement;

      // Força reflow no estado fechado para o browser calcular a posição de partida antes de animar
      void ovl.offsetWidth;
      void panel.offsetWidth;

      panel.classList.add('is-active', 'is-act');
      panel.setAttribute('aria-hidden', 'false');
      if (!panel.hasAttribute('tabindex')) panel.setAttribute('tabindex', '-1');

      panel._swKeydown = (event) => {
        if (event.key === 'Escape') SWPanel.hide(panel);
      };
      window.addEventListener('keydown', panel._swKeydown);

      ovl.classList.add('is-active', 'is-act');
      SW.Overlay?.lock();

      window.requestAnimationFrame(() => {
        (panel.querySelector('[autofocus], [sw-panel-close], .pnl-cls, button, input, select, textarea, a[href]') || panel).focus({ preventScroll: true });
      });

      SW.emit(panel, 'sw:panel:open');
      return true;
    }

    static hide(target) {
      const panel = typeof target === 'string' ? document.querySelector(target) : target;
      if (!panel || (!panel.classList.contains('is-active') && !panel.classList.contains('is-act'))) return false;

      // Remove a classe ativa permitindo que a transição CSS execute a saída direcional (0.35s)
      panel.classList.remove('is-active', 'is-act');
      panel.setAttribute('aria-hidden', 'true');

      if (panel._swKeydown) window.removeEventListener('keydown', panel._swKeydown);

      const activePanels = document.querySelectorAll('.sw-panel.is-active, .sw-panel.is-act');
      if (activePanels.length === 0) {
        const ovl = document.querySelector('.sw-panel-ovl');
        ovl?.classList.remove('is-active', 'is-act');
        SW.Overlay?.unlock();
      }

      panel._swPreviousFocus?.focus?.({ preventScroll: true });
      SW.emit(panel, 'sw:panel:close');
      return true;
    }

    static toggle(target, trigger) {
      const panel = typeof target === 'string' ? document.querySelector(target) : target;
      if (!panel) return false;
      if (panel.classList.contains('is-active') || panel.classList.contains('is-act')) {
        return SWPanel.hide(panel);
      }
      return SWPanel.show(panel, trigger);
    }
  }

  window.SW?.register('SWPanel', SWPanel);
  if (window.SW) window.SW.Panel = SWPanel;
})();

/* SWCore LGPD/Cookies — portado do SW Framework sem mudança de lógica.
   <div sw-lgpd sw-lgpd-msg sw-lgpd-modelo> ou SWLgpd.init({...}) / SW.Lgpd.init({...}) */
(function () {
  'use strict';

  const SWLgpd = {
    initAll(root = document) {
      SW.$('[sw-lgpd]', root).forEach((el) => {
        if (el._swLgpd) return;
        el._swLgpd = true;
        SWLgpd.init({
          msg: el.getAttribute('sw-lgpd-msg') || 'Usamos cookies para melhorar sua experiência.',
          accept: el.getAttribute('sw-lgpd-ok') || 'Aceitar',
          reject: el.getAttribute('sw-lgpd-nao') || null,
          link: el.getAttribute('sw-lgpd-link') || '',
          modelo: parseInt(el.getAttribute('sw-lgpd-modelo'), 10) || 1,
          days: parseInt(el.getAttribute('sw-lgpd-dias'), 10) || 365,
          key: el.getAttribute('sw-lgpd-key') || 'sw_lgpd'
        });
        el.remove();
      });
    },

    init(opts = {}) {
      const cfg = {
        msg: 'Usamos cookies para melhorar sua experiência.',
        accept: 'Aceitar',
        reject: null,
        link: '',
        linkTxt: 'Saiba mais',
        modelo: 1,
        days: 365,
        key: 'sw_lgpd',
        onAccept: null,
        onReject: null,
        ...opts
      };

      if (window.localStorage.getItem(cfg.key)) return;
      document.querySelectorAll('[sw-lgpd], .sw-lgpd').forEach((el) => el.remove());

      const bar = document.createElement('div');
      bar._swLgpd = true;
      bar.setAttribute('sw-lgpd', '');
      bar.className = 'sw-lgpd';
      if (cfg.modelo > 1) bar.classList.add(`is-m${cfg.modelo}`);

      const linkHtml = cfg.link ? ` <a href="${cfg.link}" target="_blank" rel="noopener">${cfg.linkTxt}</a>` : '';
      const icon = '<i class="sw-lgpd-ico swi-check-shield"></i>';
      const rejectBtn = cfg.reject ? `<button type="button" class="sw-lgpd-rej">${cfg.reject}</button>` : '';
      bar.innerHTML = `${icon}<p class="sw-lgpd-txt">${cfg.msg}${linkHtml}</p><div class="sw-lgpd-act">${rejectBtn}<button type="button" class="sw-lgpd-ok">${cfg.accept}</button></div>`;
      document.body.appendChild(bar);
      void bar.offsetHeight;
      window.requestAnimationFrame(() => bar.classList.add('is-vis'));

      const close = (accepted) => {
        bar.classList.remove('is-vis');
        window.setTimeout(() => bar.remove(), 450);
        if (accepted) {
          const exp = new Date();
          exp.setDate(exp.getDate() + cfg.days);
          window.localStorage.setItem(cfg.key, exp.toISOString());
          cfg.onAccept?.();
        } else {
          cfg.onReject?.();
        }
      };
      bar.querySelector('.sw-lgpd-ok')?.addEventListener('click', () => close(true));
      bar.querySelector('.sw-lgpd-rej')?.addEventListener('click', () => close(false));
    },

    clear(key = 'sw_lgpd') { window.localStorage.removeItem(key); }
  };

  window.SW?.register('SWLgpd', SWLgpd);
  if (window.SW) window.SW.Lgpd = SWLgpd;
  window.SWLgpd = SWLgpd;
})();

/* SWCore Lightbox — portado do SW Framework. Único ajuste real: trava de scroll trocada de
   document.body.style.overflow (empurrava a página inteira quando a barra de rolagem sumia)
   pra SW.Overlay.lock()/unlock() — o mesmo mecanismo com referência contada que o Modal usa,
   compensa a largura da barra com padding-right em vez de deixar a página saltar de lugar.
   <a sw-lightbox href="foto.jpg" sw-lightbox-grp="galeria" sw-lightbox-cap="Legenda"><img src="thumb.jpg"></a> */
(function () {
  'use strict';

  class SWLight {
    static _ovl = null;
    static _items = [];
    static _cur = 0;
    static _timer = null;

    static _norm(el) {
      return {
        src: el.getAttribute('href') || el.getAttribute('sw-lightbox-src') || '',
        cap: el.getAttribute('sw-lightbox-cap') || el.title || '',
        zoom: el.hasAttribute('sw-lightbox-zoom'),
        auto: parseInt(el.getAttribute('sw-lightbox-auto'), 10) || 0,
        count: el.getAttribute('sw-lightbox-count') !== 'false'
      };
    }

    static initAll(root = document) {
      SWLight._ensure();
      SW.$('[sw-lightbox]', root).forEach((el) => {
        if (el._swLightInit) return;
        el._swLightInit = true;
        el.addEventListener('click', (event) => {
          event.preventDefault();
          const grp = el.getAttribute('sw-lightbox-grp');
          const els = grp ? SW.$(`[sw-lightbox][sw-lightbox-grp="${grp}"]`) : [el];
          SWLight._items = els.map(SWLight._norm);
          SWLight._cur = Math.max(0, els.indexOf(el));
          SWLight._show(SWLight._cur);
        });
      });
    }

    static _ensure() {
      if (SWLight._ovl) return;
      const ovl = document.createElement('div');
      ovl.className = 'sw-lightbox-ovl';
      ovl.innerHTML = `
        <div class="sw-lightbox-wrap"><img class="sw-lightbox-img" src="" alt=""></div>
        <div class="sw-lightbox-ldr"></div>
        <button type="button" class="sw-lightbox-cls" aria-label="Fechar"><i class="swi-x"></i></button>
        <button type="button" class="sw-lightbox-prv" aria-label="Anterior"><i class="swi-chevron-left"></i></button>
        <button type="button" class="sw-lightbox-nxt" aria-label="Próximo"><i class="swi-chevron-right"></i></button>
        <div class="sw-lightbox-cap"></div>
        <div class="sw-lightbox-cnt"></div>`;
      document.body.appendChild(ovl);
      SWLight._ovl = ovl;

      ovl.querySelector('.sw-lightbox-cls').addEventListener('click', SWLight.close);
      ovl.querySelector('.sw-lightbox-prv').addEventListener('click', () => SWLight._step(-1));
      ovl.querySelector('.sw-lightbox-nxt').addEventListener('click', () => SWLight._step(1));
      ovl.addEventListener('click', (event) => { if (event.target === ovl) SWLight.close(); });

      ovl.querySelector('.sw-lightbox-img').addEventListener('click', () => {
        if (SWLight._items[SWLight._cur]?.zoom) ovl.querySelector('.sw-lightbox-wrap').classList.toggle('is-zoom');
      });

      let startX = 0;
      ovl.addEventListener('touchstart', (event) => { startX = event.touches[0].clientX; }, { passive: true });
      ovl.addEventListener('touchend', (event) => {
        const diff = startX - event.changedTouches[0].clientX;
        if (Math.abs(diff) > 50) SWLight._step(diff > 0 ? 1 : -1);
      });

      document.addEventListener('keydown', (event) => {
        if (!SWLight._ovl?.classList.contains('is-open')) return;
        if (event.key === 'Escape') SWLight.close();
        if (event.key === 'ArrowLeft') SWLight._step(-1);
        if (event.key === 'ArrowRight') SWLight._step(1);
      });
    }

    static _show(idx) {
      const item = SWLight._items[idx];
      if (!item) return;
      const wasOpen = SWLight._ovl.classList.contains('is-open');

      const img = SWLight._ovl.querySelector('.sw-lightbox-img');
      const wrap = SWLight._ovl.querySelector('.sw-lightbox-wrap');
      const loader = SWLight._ovl.querySelector('.sw-lightbox-ldr');
      wrap.classList.remove('is-zoom');
      wrap.classList.toggle('has-zoom', !!item.zoom);
      img.style.opacity = '0';
      loader.classList.add('is-vis');
      const swap = () => {
        img.onload = () => { img.style.opacity = '1'; loader.classList.remove('is-vis'); };
        img.onerror = () => { loader.classList.remove('is-vis'); };
        img.src = item.src;
      };
      // Ao navegar (já aberto), espera o fade-out da imagem anterior terminar antes de trocar
      // o src — senão, com imagem local/rápida, a troca acontece no mesmo instante em que a
      // opacidade zera e o navegador reverte a transição no meio do caminho (nunca some de
      // verdade). Na abertura inicial não tem imagem anterior pra desaparecer, troca na hora.
      if (wasOpen) window.setTimeout(swap, 200); else swap();

      SWLight._ovl.querySelector('.sw-lightbox-cap').textContent = item.cap;

      const showNav = SWLight._items.length > 1;
      SWLight._ovl.querySelector('.sw-lightbox-cnt').textContent = showNav && item.count ? `${idx + 1} / ${SWLight._items.length}` : '';
      SWLight._ovl.querySelector('.sw-lightbox-prv').style.display = showNav ? '' : 'none';
      SWLight._ovl.querySelector('.sw-lightbox-nxt').style.display = showNav ? '' : 'none';
      SWLight._ovl.classList.add('is-open');
      if (!wasOpen) SW.Overlay.lock();

      SW.emit(document, 'sw:lightbox:open', { index: idx, src: item.src });

      SWLight._timerStop();
      if (item.auto > 0 && showNav) SWLight._timer = window.setInterval(() => SWLight._step(1), item.auto);
    }

    static _timerStop() {
      if (SWLight._timer) { window.clearInterval(SWLight._timer); SWLight._timer = null; }
    }

    static _step(dir) {
      SWLight._cur = (SWLight._cur + dir + SWLight._items.length) % SWLight._items.length;
      SWLight._show(SWLight._cur);
      SW.emit(document, 'sw:lightbox:nav', { index: SWLight._cur });
    }

    static close() {
      SWLight._timerStop();
      const wasOpen = SWLight._ovl?.classList.contains('is-open');
      SWLight._ovl?.classList.remove('is-open');
      if (wasOpen) SW.Overlay.unlock();
      SW.emit(document, 'sw:lightbox:close', {});
    }

    static next() { SWLight._step(1); }
    static prev() { SWLight._step(-1); }

    static open(src, opts = {}) {
      const cap = typeof opts === 'string' ? opts : (opts.caption || '');
      SWLight._ensure();
      SWLight._items = [{ src, cap, zoom: opts.zoom || false, auto: 0, count: true }];
      SWLight._cur = 0;
      SWLight._show(0);
    }
  }

  window.SW?.register('SWLight', SWLight);
  if (window.SW) window.SW.Light = SWLight;
})();

/* SWCore AJAX — portado do SW Framework sem mudança de lógica. Injeta fragmentos HTML
   same-origin com segurança (CSRF automático, timeout, sanitização via SW.html).
   <a sw-ajax="/rota" sw-target="#alvo"> · SW.Ajax.load(url, dest) / .post(url, data, dest)
   sw-ajax-effect/-duration/-distance/-delay: aplica uma classe real do catálogo de animação
   (.sw-ani-* / .sw-loop-*, já portado no SWCore) ao conteúdo injetado — sem GSAP nenhum. */
(function () {
  'use strict';

  class SWAjax {
    static initAll(root = document) {
      SW.$('[sw-ajax], [sw-ajax-src]', root).forEach((element) => {
        if (element._swAjaxInit) return;
        element._swAjaxInit = true;
        const trigger = (element.getAttribute('sw-ajax-trigger') || 'click').toLowerCase();
        if (trigger === 'load') {
          SWAjax.execute(element);
        } else if (trigger === 'hover') {
          element.addEventListener('mouseenter', () => SWAjax.execute(element), { once: true });
        } else {
          element.addEventListener('click', (event) => {
            event.preventDefault();
            SWAjax.execute(element);
          });
        }
      });
    }

    static async execute(trigger, overrideBody) {
      const sourceSelector = trigger.getAttribute('sw-ajax-src');
      const targetSelector = trigger.getAttribute('sw-target');
      const targetType = trigger.getAttribute('sw-ajax-target');
      const trusted = trigger.hasAttribute('sw-ajax-trusted');
      const extractSelector = trigger.getAttribute('sw-ajax-extract');
      const push = trigger.hasAttribute('sw-ajax-push');
      const method = (trigger.getAttribute('sw-ajax-method') || 'GET').toUpperCase();
      const showLoader = (trigger.getAttribute('sw-ajax-loader') || '').toLowerCase() !== 'off';
      const effect = trigger.getAttribute('sw-ajax-effect');
      const duration = trigger.getAttribute('sw-ajax-duration');
      const distance = trigger.getAttribute('sw-ajax-distance');
      const delay = trigger.getAttribute('sw-ajax-delay');
      const target = this.resolveTarget(trigger, targetType, targetSelector);
      let content = '';
      let requestUrl = '';

      try {
        if (sourceSelector) {
          const source = document.querySelector(sourceSelector);
          if (!source) throw new Error(`Elemento interno não encontrado: ${sourceSelector}`);
          content = source.innerHTML;
        } else {
          const rawUrl = trigger.getAttribute('sw-ajax');
          if (!rawUrl) throw new Error('Fonte AJAX ausente.');
          const url = new URL(rawUrl, window.location.href);
          if (url.origin !== window.location.origin && !trigger.hasAttribute('sw-ajax-crossorigin')) {
            throw new Error('SWAjax bloqueou uma origem externa não autorizada.');
          }
          requestUrl = url.href;
          const timeoutMs = Math.min(60000, Math.max(1000, parseInt(trigger.getAttribute('sw-ajax-timeout'), 10) || 15000));
          const controller = new AbortController();
          const timeout = window.setTimeout(() => controller.abort(), timeoutMs);
          SW.emit(trigger, 'sw:ajax:start', { url: url.href });
          if (target && showLoader) this._setLoading(target.content, true);
          try {
            const fetchOptions = {
              method,
              credentials: 'same-origin',
              headers: { 'X-Requested-With': 'XMLHttpRequest', Accept: 'text/html' },
              signal: controller.signal
            };
            if (method !== 'GET') {
              const body = overrideBody !== undefined ? overrideBody : (trigger.tagName === 'FORM' ? new FormData(trigger) : undefined);
              if (body !== undefined) {
                fetchOptions.body = body instanceof FormData ? body : JSON.stringify(body);
                if (!(body instanceof FormData)) fetchOptions.headers['Content-Type'] = 'application/json';
              }
              const csrf = document.querySelector('meta[name="csrf-token"]')?.content || window._csrf || '';
              if (csrf) fetchOptions.headers['X-CSRF-Token'] = csrf;
            }
            const response = await fetch(url.href, fetchOptions);
            if (!response.ok) throw new Error(`HTTP ${response.status}`);
            const contentType = response.headers.get('content-type') || '';
            if (!contentType.includes('text/html') && !contentType.includes('text/plain')) {
              throw new Error(`Tipo de conteúdo não suportado: ${contentType || 'desconhecido'}`);
            }
            content = await response.text();
          } finally {
            window.clearTimeout(timeout);
          }
        }

        if (/<html[\s>]/i.test(content) || /<body[\s>]/i.test(content)) {
          const parsed = new DOMParser().parseFromString(content, 'text/html');
          const extracted = extractSelector ? parsed.querySelector(extractSelector) : parsed.body;
          content = extracted ? extracted.innerHTML : content;
        } else if (extractSelector) {
          const scratch = document.createElement('div');
          scratch.innerHTML = content;
          const extracted = scratch.querySelector(extractSelector);
          if (extracted) content = extracted.innerHTML;
        }

        if (!target) throw new Error('Alvo de injeção não encontrado.');
        SW.html.set(target.content, content, { trusted });
        SW.reinit(target.content);
        if (effect || duration || distance || delay) {
          this._applyEffect(target.content, { effect, duration, distance, delay });
        }

        if (targetType === 'panel') SW.Panel?.show(target.overlay);
        if (targetType === 'modal') SW.Modal?.show(target.overlay);
        if (push && requestUrl && window.history?.pushState) window.history.pushState({}, '', requestUrl);
        this._setLoading(target.content, false);
        SW.emit(trigger, 'sw:ajax:done', { sourceSelector });
      } catch (error) {
        console.error('[SW-AJAX]', error);
        SW.emit(trigger, 'sw:ajax:error', { error });
        const message = error.name === 'AbortError' ? 'A requisição demorou demais.' : 'Não foi possível carregar o conteúdo.';
        if (target) {
          this._setLoading(target.content, false);
          this._setError(target.content, message);
        }
        SW.Alert?.err(message);
      }
    }

    // Efeito de entrada declarado direto na tag — sw-ajax-effect/-duration/-distance/-delay.
    // Aceita o nome curto (fade, up, pop...) → vira .sw-ani-<nome>, ou a classe completa pra
    // usar o catálogo .sw-loop-* também. --sw-dist/--sw-dist-lg são dois tokens separados no
    // SWCore (up/down usam --sw-dist, left/right usam --sw-dist-lg) — sw-ajax-distance
    // sobrescreve os dois juntos, já que o autor da tag só quer "a distância", não qual token
    // interno cada direção usa. Atraso não tem token CSS aqui (o SWCore nunca precisou de um
    // até agora) — aplicado direto como animation-delay no elemento.
    static _applyEffect(element, { effect, duration, distance, delay }) {
      const toTime = (value) => (value && /^\d+$/.test(value) ? `${value}ms` : value);
      if (duration) element.style.setProperty('--sw-spd', toTime(duration));
      if (distance) {
        const dist = /^\d+(\.\d+)?$/.test(distance) ? `${distance}rem` : distance;
        element.style.setProperty('--sw-dist', dist);
        element.style.setProperty('--sw-dist-lg', dist);
      }
      if (delay) element.style.animationDelay = toTime(delay);
      if (!effect) return;
      const cls = /^sw-(ani|loop)-/.test(effect) ? effect : `sw-ani-${effect}`;
      element.className = element.className.replace(/\bsw-(ani|loop)-[a-z-]+\b/g, '').trim();
      element.classList.add(cls);
      void element.offsetWidth;
    }

    static _setLoading(element, isLoading) {
      if (!element) return;
      element.classList.toggle('sw-ajax-loading', isLoading);
      if (isLoading) element.setAttribute('aria-busy', 'true');
      else element.removeAttribute('aria-busy');
    }

    static _setError(element, message) {
      if (!element) return;
      element.classList.add('sw-ajax-error');
      element.setAttribute('title', message);
      window.setTimeout(() => element.classList.remove('sw-ajax-error'), 2000);
    }

    static resolveTarget(trigger, type, selector) {
      if (type === 'panel') {
        const requested = trigger.getAttribute('sw-panel');
        const panelSelector = requested?.startsWith('#') ? requested : '#sw-global-panel';
        let panel = document.querySelector(panelSelector);
        if (!panel) {
          panel = document.createElement('aside');
          panel.id = panelSelector.slice(1);
          panel.className = 'sw-panel';
          panel.setAttribute('aria-hidden', 'true');
          document.body.appendChild(panel);
        }
        return { overlay: panel, content: panel };
      }
      if (type === 'modal') {
        const requested = trigger.getAttribute('sw-modal');
        const modalSelector = requested?.startsWith('#') ? requested : '#sw-global-modal';
        let modal = document.querySelector(modalSelector);
        if (!modal) {
          modal = document.createElement('div');
          modal.id = modalSelector.slice(1);
          modal.className = 'sw-modal';
          const body = document.createElement('div');
          body.className = 'sw-modal-box';
          modal.appendChild(body);
          document.body.appendChild(modal);
        }
        return { overlay: modal, content: modal.querySelector('.sw-modal-box') };
      }
      const content = selector ? document.querySelector(selector) : null;
      return content ? { overlay: content, content } : null;
    }

    static load(url, dest, opts = {}) {
      const trigger = document.createElement('span');
      trigger.setAttribute('sw-ajax', url);
      if (dest) trigger.setAttribute('sw-target', dest);
      if (opts.trusted) trigger.setAttribute('sw-ajax-trusted', '');
      if (opts.crossorigin) trigger.setAttribute('sw-ajax-crossorigin', '');
      if (opts.push) trigger.setAttribute('sw-ajax-push', '');
      if (opts.extract) trigger.setAttribute('sw-ajax-extract', opts.extract);
      return SWAjax.execute(trigger);
    }

    static post(url, data, dest, opts = {}) {
      const trigger = document.createElement('span');
      trigger.setAttribute('sw-ajax', url);
      trigger.setAttribute('sw-ajax-method', 'POST');
      if (dest) trigger.setAttribute('sw-target', dest);
      if (opts.trusted) trigger.setAttribute('sw-ajax-trusted', '');
      if (opts.crossorigin) trigger.setAttribute('sw-ajax-crossorigin', '');
      if (opts.push) trigger.setAttribute('sw-ajax-push', '');
      if (opts.extract) trigger.setAttribute('sw-ajax-extract', opts.extract);
      return SWAjax.execute(trigger, data);
    }
  }

  window.SW?.register('SWAjax', SWAjax);
  if (window.SW) window.SW.Ajax = SWAjax;
})();

/* SWCore Anime — Scroll Reveal & Micro-interações
   Modos de Scroll:
   1. Animar UMA VEZ SÓ: class="reveal-*" | [sw-anime-rev] | [sw-anime="once"] | [sw-anime-once] | [sw-scroll-once]
   2. Animar TODA VEZ que o scroll passar: [sw-anime="repeat"] | [sw-anime-repeat] | [sw-anime-mnt] | .is-repeat
*/
(function () {
  'use strict';

  class SWAnime {
    static _revObs = null; // Anima 1x (Once)
    static _repObs = null; // Anima toda vez (Repeat / Bidirecional)

    static initAll(root) {
      const scope = (root && root.querySelectorAll) ? root : document;

      requestAnimationFrame(function () {
        requestAnimationFrame(function () {
          // Elementos que repetem a cada scroll (Repeat / Mount-Unmount)
          scope.querySelectorAll('[sw-anime="repeat"], [sw-anime-repeat], [sw-anime-mnt], [class*="reveal-"].is-repeat').forEach(function (el) {
            if (el._swRep) return;
            el._swRep = true;
            SWAnime._getRepObs().observe(el);
          });

          // Elementos que animam uma única vez (Once)
          scope.querySelectorAll('[class*="reveal-"]:not([sw-anime="repeat"]):not([sw-anime-repeat]):not([sw-anime-mnt]):not(.is-repeat), [sw-anime-rev], [sw-anime="once"], [sw-anime-once], [sw-scroll-once]').forEach(function (el) {
            if (el._swRev || el._swRep) return;
            el._swRev = true;
            SWAnime._getRevObs().observe(el);
          });
        });
      });
    }

    static _getRevObs() {
      if (!SWAnime._revObs) {
        SWAnime._revObs = new IntersectionObserver(function (entries) {
          entries.forEach(function (e) {
            if (!e.isIntersecting) return;
            e.target.classList.add('is-revealed');
            SWAnime._revObs.unobserve(e.target);
          });
        }, { threshold: 0.1, rootMargin: '0px 0px -40px 0px' });
      }
      return SWAnime._revObs;
    }

    static _getRepObs() {
      if (!SWAnime._repObs) {
        SWAnime._repObs = new IntersectionObserver(function (entries) {
          entries.forEach(function (e) {
            if (e.isIntersecting) {
              e.target.classList.add('is-revealed');
            } else {
              e.target.classList.remove('is-revealed');
            }
          });
        }, { threshold: 0.05, rootMargin: '0px 0px -20px 0px' });
      }
      return SWAnime._repObs;
    }
  }

  window.SW?.register('SWAnime', SWAnime);
  if (window.SW) window.SW.Anime = SWAnime;
})();

/* SWCore Lazy Load — portado do SW Framework, com um adicional real: a classe .is-loading
   (nova) fica no <img> do momento em que entra na margem de observação até o probe terminar
   (sucesso vira .is-ldd, falha vira .is-err) — dá pra estilizar um spinner via CSS enquanto
   espera. <img sw-lazy sw-lazy-src="real.jpg" src="placeholder.jpg"> */
(function () {
  'use strict';
  let observer = null;

  function load(img) {
    const src = img.getAttribute('sw-lazy-src');
    if (!src) return;
    img.classList.add('is-loading');
    const probe = new Image();
    probe.onload = () => { img.src = src; img.classList.remove('is-loading'); img.classList.add('is-ldd'); SW.emit(img, 'sw:lazy:loaded'); };
    probe.onerror = () => { img.classList.remove('is-loading'); img.classList.add('is-err'); SW.emit(img, 'sw:lazy:error'); };
    probe.src = src;
  }

  const SWLazy = {
    initAll(root = document) {
      if (!observer) {
        observer = new IntersectionObserver((entries) => {
          entries.forEach((entry) => {
            if (!entry.isIntersecting) return;
            load(entry.target);
            observer.unobserve(entry.target);
          });
        }, { rootMargin: '200px' });
      }
      SW.$('img[sw-lazy]', root).forEach((img) => {
        if (img._swLazy) return;
        img._swLazy = true;
        observer.observe(img);
      });
    }
  };

  window.SW?.register('SWLazy', SWLazy);
  if (window.SW) window.SW.Lazy = SWLazy;
})();

/* SWCore Drag — portado do SW Framework. Única mudança real: os aliases legados
   swdrag- e y2drag- (compatibilidade com o Y2, predecessor do SW) foram removidos — SWCore não
   tem esse legado, só a API sw-drag.
   <div sw-drag>Elemento</div>
   <div sw-drag sw-drag-handle=".handle" sw-drag-axis="x|y" sw-drag-bounds="parent|window"></div> */
(function () {
  'use strict';

  let maxZIndex = 50;

  class SWDragInst {
    constructor(el) {
      this.el = el;
      const handleSelector = el.getAttribute('sw-drag-handle');
      this.handle = handleSelector ? el.querySelector(handleSelector) || el : el;
      this.axis = el.getAttribute('sw-drag-axis') || 'both';
      this.bounds = el.getAttribute('sw-drag-bounds') || null;

      if (!this.handle) return;
      if (el.style.position === '' || el.style.position === 'static') {
        el.style.position = 'absolute';
      }

      this.handle.style.cursor = 'grab';
      this.handle.addEventListener('mousedown', (event) => this._start(event));
      this.handle.addEventListener('touchstart', (event) => this._start(event), { passive: false });
    }

    _start(event) {
      event.preventDefault();
      const pt = event.touches ? event.touches[0] : event;
      this._ox = pt.clientX - this.el.offsetLeft;
      this._oy = pt.clientY - this.el.offsetTop;
      this.handle.style.cursor = 'grabbing';
      this.el.classList.add('is-dragging');
      this.el.style.zIndex = ++maxZIndex;

      SW.emit(this.el, 'sw:drag:start');

      const move = (ev) => {
        const p = ev.touches ? ev.touches[0] : ev;
        let nx = p.clientX - this._ox;
        let ny = p.clientY - this._oy;

        if (this.bounds === 'parent' && this.el.parentElement) {
          const par = this.el.parentElement;
          nx = Math.max(0, Math.min(nx, par.offsetWidth - this.el.offsetWidth));
          ny = Math.max(0, Math.min(ny, par.offsetHeight - this.el.offsetHeight));
        } else if (this.bounds === 'window') {
          nx = Math.max(0, Math.min(nx, window.innerWidth - this.el.offsetWidth));
          ny = Math.max(0, Math.min(ny, window.innerHeight - this.el.offsetHeight));
        }

        if (this.axis !== 'y') this.el.style.left = `${nx}px`;
        if (this.axis !== 'x') this.el.style.top = `${ny}px`;

        SW.emit(this.el, 'sw:drag:move', { x: nx, y: ny });
      };

      const end = () => {
        this.handle.style.cursor = 'grab';
        this.el.classList.remove('is-dragging');
        document.removeEventListener('mousemove', move);
        document.removeEventListener('mouseup', end);
        document.removeEventListener('touchmove', move);
        document.removeEventListener('touchend', end);
        SW.emit(this.el, 'sw:drag:end');
      };

      document.addEventListener('mousemove', move);
      document.addEventListener('mouseup', end);
      document.addEventListener('touchmove', move, { passive: false });
      document.addEventListener('touchend', end);
    }
  }

  class SWDrag {
    static initAll(root = document) {
      SW.$('[sw-drag]', root).forEach((el) => {
        if (el._swDrag) return;
        el._swDrag = new SWDragInst(el);
      });
    }
  }

  window.SW?.register('SWDrag', SWDrag);
  if (window.SW) window.SW.Drag = SWDrag;
})();

/* SWCore Upload — portado do SW Framework sem mudança de lógica.
   <div sw-upload sw-upload-accept sw-upload-max sw-upload-size sw-upload-txt><input type="file"></div>
   Eventos: sw:upload:add { files } · sw:upload:remove { index } · API: SW.Upload.getFiles(el) */
(function () {
  'use strict';

  function fmtSize(bytes) {
    if (bytes >= 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
    return `${(bytes / 1024).toFixed(0)} KB`;
  }

  class SWUploadInst {
    constructor(el) {
      this.el = el;
      this.input = el.querySelector('input[type="file"]');
      this.accept = el.getAttribute('sw-upload-accept') || this.input?.accept || '*';
      this.max = parseInt(el.getAttribute('sw-upload-max'), 10) || 0;
      this.maxSize = parseInt(el.getAttribute('sw-upload-size'), 10) || 10 * 1024 * 1024;
      this.txt = el.getAttribute('sw-upload-txt') || 'Arraste ou <strong>clique aqui</strong>';
      this.files = [];
      el._swUploadInst = this;

      this._build();
      this._bind();
    }

    _build() {
      if (!this.input) {
        this.input = document.createElement('input');
        this.input.type = 'file';
        this.el.appendChild(this.input);
      }
      this.input.style.display = 'none';
      if (this.accept !== '*') this.input.accept = this.accept;
      if (this.max !== 1) this.input.multiple = true;

      this.zone = document.createElement('div');
      this.zone.className = 'sw-upload-zone';
      this.zone.innerHTML = `<i class="swi-image-add sw-upload-ico"></i><p class="sw-upload-txt">${this.txt}</p><p class="sw-upload-sub">Tamanho máx: ${fmtSize(this.maxSize)}</p>`;
      this.el.insertBefore(this.zone, this.input);

      this.prev = document.createElement('div');
      this.prev.className = 'sw-upload-prev';
      this.el.appendChild(this.prev);

      this.errEl = document.createElement('div');
      this.errEl.className = 'sw-upload-err';
      this.el.appendChild(this.errEl);
    }

    _bind() {
      this.zone.addEventListener('click', () => this.input.click());
      this.input.addEventListener('change', () => this._add(Array.from(this.input.files)));
      this.zone.addEventListener('dragover', (event) => { event.preventDefault(); this.zone.classList.add('is-ov'); });
      this.zone.addEventListener('dragleave', () => this.zone.classList.remove('is-ov'));
      this.zone.addEventListener('drop', (event) => {
        event.preventDefault();
        this.zone.classList.remove('is-ov');
        this._add(Array.from(event.dataTransfer.files));
      });
    }

    _add(newFiles) {
      this.errEl.textContent = '';
      const errors = [];
      newFiles.forEach((f) => {
        if (this.max && this.files.length >= this.max) { errors.push(`Máximo de ${this.max} arquivo(s).`); return; }
        if (f.size > this.maxSize) { errors.push(`"${f.name}" excede ${fmtSize(this.maxSize)}.`); return; }
        this.files.push(f);
      });
      if (errors.length) this.errEl.textContent = errors[0];
      this._renderPrev();
      SW.emit(this.el, 'sw:upload:add', { files: this.files });
    }

    _renderPrev() {
      this.prev.innerHTML = '';
      this.files.forEach((f, i) => {
        const item = document.createElement('div');
        item.className = 'sw-upload-item';
        if (f.type.startsWith('image/')) {
          const img = document.createElement('img');
          img.src = URL.createObjectURL(f);
          img.onload = () => URL.revokeObjectURL(img.src);
          item.appendChild(img);
        } else {
          item.innerHTML = `<div class="sw-upload-item-name">${f.name}</div>`;
        }
        const rm = document.createElement('div');
        rm.className = 'sw-upload-item-rm';
        rm.innerHTML = '<i class="swi-x"></i>';
        rm.addEventListener('click', (event) => { event.stopPropagation(); this._remove(i); });
        item.appendChild(rm);
        this.prev.appendChild(item);
      });
    }

    _remove(i) {
      this.files.splice(i, 1);
      this._renderPrev();
      SW.emit(this.el, 'sw:upload:remove', { index: i });
    }

    clear() { this.files = []; this._renderPrev(); }
  }

  class SWUpload {
    static initAll(root = document) {
      SW.$('[sw-upload]', root).forEach((el) => {
        if (el._swUpload) return;
        el._swUpload = true;
        new SWUploadInst(el);
      });
    }

    static getFiles(el) {
      return (typeof el === 'string' ? document.querySelector(el) : el)?._swUploadInst?.files || [];
    }
  }

  window.SW?.register('SWUpload', SWUpload);
  if (window.SW) window.SW.Upload = SWUpload;
})();

/* SWCore Cotação — portado do SW Framework sem mudança de lógica.
   <div sw-cotacao sw-cotacao-pares="USD-BRL,EUR-BRL" sw-cotacao-tipo="c" sw-cotacao-mode="cards|table" sw-cotacao-auto="30000">
   Usa a API pública da AwesomeAPI (awesomeapi.com.br), sem token. */
(function () {
  'use strict';

  class SWCotacaoInst {
    constructor(el) {
      this.el = el;
      const paresAttr = el.getAttribute('sw-cotacao-pares') || 'USD-BRL,EUR-BRL';
      this.pares = paresAttr.split(',').map((p) => p.trim());
      this.tipo = el.getAttribute('sw-cotacao-tipo') || 'c';
      this.mode = el.getAttribute('sw-cotacao-mode') || 'cards';
      this.auto = parseInt(el.getAttribute('sw-cotacao-auto') || '0', 10);
      this._fetch();
      if (this.auto > 0) window.setInterval(() => this._fetch(), this.auto);
    }

    _fetch() {
      const pairs = this.pares.join(',');
      fetch(`https://economia.awesomeapi.com.br/last/${pairs}`)
        .then((r) => r.json())
        .then((data) => this._render(data))
        .catch(() => { this.el.innerHTML = '<p class="sw-cotacao-mut">Cotação indisponível no momento.</p>'; });
    }

    _render(data) {
      const vals = Object.values(data);
      if (!vals.length) {
        this.el.innerHTML = '<p class="sw-cotacao-mut">Nenhuma cotação encontrada.</p>';
        return;
      }
      if (this.mode === 'table') {
        const rows = vals.map((v) => {
          const pct = parseFloat(v.pctChange || 0);
          const cls = pct >= 0 ? 'is-up' : 'is-dwn';
          const sign = pct >= 0 ? '▲' : '▼';
          const bid = parseFloat(v.bid || 0);
          const ask = parseFloat(v.ask || 0);
          const high = parseFloat(v.high || 0);
          const low = parseFloat(v.low || 0);
          const dec = bid < 1 ? 6 : bid < 10 ? 4 : 2;
          return `<tr><td class="sw-cotacao-tcod">${v.code}/${v.codein}</td><td class="sw-cotacao-tval">R$ ${bid.toFixed(dec)}</td><td class="sw-cotacao-tval">R$ ${ask.toFixed(dec)}</td><td class="sw-cotacao-tpct ${cls}">${sign} ${Math.abs(pct).toFixed(2)}%</td><td class="sw-cotacao-tval">R$ ${high.toFixed(dec)}</td><td class="sw-cotacao-tval">R$ ${low.toFixed(dec)}</td></tr>`;
        }).join('');
        this.el.innerHTML = `<table class="sw-cotacao-table"><thead><tr><th>Par</th><th>Compra (Bid)</th><th>Venda (Ask)</th><th>Variação</th><th>Máx 24h</th><th>Mín 24h</th></tr></thead><tbody>${rows}</tbody></table>`;
      } else {
        const items = vals.map((v) => {
          const val = parseFloat(this.tipo === 'c' ? v.bid : v.ask);
          const pct = parseFloat(v.pctChange || 0);
          const cls = pct >= 0 ? 'is-up' : 'is-dwn';
          const sign = pct >= 0 ? '▲' : '▼';
          const label = this.tipo === 'c' ? 'Compra' : 'Venda';
          return `<div class="sw-cotacao-it"><span class="sw-cotacao-par">${v.code} / ${v.codein} (${label})</span><span class="sw-cotacao-val ${cls}">R$ ${val.toFixed(val < 1 ? 4 : 2)}</span><span class="sw-cotacao-sub ${cls}">${sign} ${Math.abs(pct).toFixed(2)}%</span></div>`;
        }).join('');
        this.el.innerHTML = `<div class="sw-cotacao-wrap">${items}</div>`;
      }
    }
  }

  class SWCotacao {
    static initAll(root = document) {
      SW.$('[sw-cotacao]', root).forEach((el) => {
        if (el._swCotacao) return;
        el._swCotacao = new SWCotacaoInst(el);
      });
    }
  }

  window.SW?.register('SWCotacao', SWCotacao);
  if (window.SW) window.SW.Cotacao = SWCotacao;
})();

/* SWCore Cropper — portado do SW Framework sem mudança de lógica. Canvas nativo, sem
   dependências externas. Sem sw-cropper-upload (ou "#"), fica em modo demo: gera o recorte
   local via data URL e mostra em sw-cropper-out, sem tentar enviar pra nenhum servidor.
   <div sw-cropper sw-cropper-ratio="1" sw-cropper-upload="/api/imgs/upload" sw-cropper-preview="#prev" sw-cropper-out="#img">
     <input type="file" accept="image/*" sw-cropper-input>
     <canvas sw-cropper-canvas></canvas>
     <button type="button" sw-cropper-btn>Cortar e Enviar</button>
   </div>
   Eventos: sw:cropper:ready · sw:cropper:done { url, thumb, id } · sw:cropper:error { erro } */
(function () {
  'use strict';

  class SWCropperInst {
    constructor(el) {
      this.el = el;
      this.ratio = parseFloat(el.getAttribute('sw-cropper-ratio') ?? '1') || 1;
      this.url = el.getAttribute('sw-cropper-upload') || '#';
      this.refId = el.getAttribute('sw-cropper-ref') || '';
      this.local = el.getAttribute('sw-cropper-local') || '';
      this.tipo = el.getAttribute('sw-cropper-tipo') || 'avatar';
      this.previewSel = el.getAttribute('sw-cropper-preview') || null;
      this.outSel = el.getAttribute('sw-cropper-out') || null;

      this.input = el.querySelector('[sw-cropper-input]');
      this.btnCrop = el.querySelector('[sw-cropper-btn]');
      this.canvas = el.querySelector('[sw-cropper-canvas]');
      this.img = new Image();
      this.box = null;

      el._swCropperInst = this;
      this._bind();
    }

    _bind() {
      this.input?.addEventListener('change', (event) => this._onFile(event));
      this.btnCrop?.addEventListener('click', () => this._crop());
    }

    _onFile(event) {
      const file = event.target.files[0];
      if (!file) return;
      const reader = new FileReader();
      reader.onload = (ev) => {
        this.img.onload = () => this._setup();
        this.img.src = ev.target.result;
      };
      reader.readAsDataURL(file);
    }

    _setup() {
      if (!this.canvas) return;
      const maxW = Math.min(500, this.canvas.parentElement?.clientWidth || this.img.width || 400);
      const scale = Math.min(1, maxW / this.img.width);
      this.canvas.width = this.img.width * scale;
      this.canvas.height = this.img.height * scale;
      this.canvas.style.display = 'block';
      this._scale = scale;

      const effectiveRatio = this.ratio > 0 ? this.ratio : (this.canvas.width / this.canvas.height);
      const w = Math.min(this.canvas.width, this.canvas.height * effectiveRatio);
      const h = w / effectiveRatio;
      this.box = { x: (this.canvas.width - w) / 2, y: (this.canvas.height - h) / 2, w, h };

      this._draw();
      this._bindDrag();
      SW.emit(this.el, 'sw:cropper:ready');
    }

    _draw() {
      if (!this.canvas || !this.box) return;
      const ctx = this.canvas.getContext('2d');
      ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
      ctx.drawImage(this.img, 0, 0, this.canvas.width, this.canvas.height);
      ctx.save();
      ctx.fillStyle = 'rgba(0,0,0,.55)';
      ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
      ctx.clearRect(this.box.x, this.box.y, this.box.w, this.box.h);
      ctx.drawImage(
        this.img,
        this.box.x / this._scale, this.box.y / this._scale,
        this.box.w / this._scale, this.box.h / this._scale,
        this.box.x, this.box.y, this.box.w, this.box.h
      );
      ctx.strokeStyle = '#6366f1';
      ctx.lineWidth = 2;
      ctx.strokeRect(this.box.x, this.box.y, this.box.w, this.box.h);
      ctx.restore();

      if (this.previewSel) {
        const prevEl = document.querySelector(this.previewSel);
        if (prevEl) {
          const pCanvas = document.createElement('canvas');
          pCanvas.width = this.box.w;
          pCanvas.height = this.box.h;
          const pCtx = pCanvas.getContext('2d');
          pCtx.drawImage(
            this.img,
            this.box.x / this._scale, this.box.y / this._scale,
            this.box.w / this._scale, this.box.h / this._scale,
            0, 0, this.box.w, this.box.h
          );
          prevEl.style.backgroundImage = `url(${pCanvas.toDataURL('image/png')})`;
          prevEl.style.backgroundSize = 'cover';
          prevEl.style.backgroundPosition = 'center';
        }
      }
    }

    _bindDrag() {
      let dragging = false;
      let startX = 0;
      let startY = 0;
      let origX = 0;
      let origY = 0;

      const pos = (event) => {
        const r = this.canvas.getBoundingClientRect();
        const p = event.touches ? event.touches[0] : event;
        return { x: (p.clientX - r.left) * (this.canvas.width / r.width), y: (p.clientY - r.top) * (this.canvas.height / r.height) };
      };

      const down = (event) => {
        const p = pos(event);
        if (p.x < this.box.x || p.x > this.box.x + this.box.w || p.y < this.box.y || p.y > this.box.y + this.box.h) return;
        dragging = true;
        startX = p.x; startY = p.y;
        origX = this.box.x; origY = this.box.y;
      };
      const move = (event) => {
        if (!dragging) return;
        const p = pos(event);
        this.box.x = Math.max(0, Math.min(this.canvas.width - this.box.w, origX + (p.x - startX)));
        this.box.y = Math.max(0, Math.min(this.canvas.height - this.box.h, origY + (p.y - startY)));
        this._draw();
      };
      const up = () => { dragging = false; };

      this.canvas.onmousedown = down;
      this.canvas.onmousemove = move;
      window.addEventListener('mouseup', up);
      this.canvas.ontouchstart = down;
      this.canvas.ontouchmove = move;
      window.addEventListener('touchend', up);
    }

    async _crop() {
      if (!this.box) return;
      const out = document.createElement('canvas');
      const targetW = 800;
      const targetH = Math.round(800 / (this.ratio > 0 ? this.ratio : (this.box.w / this.box.h)));
      out.width = targetW;
      out.height = targetH;
      const ctx = out.getContext('2d');
      ctx.drawImage(
        this.img,
        this.box.x / this._scale, this.box.y / this._scale,
        this.box.w / this._scale, this.box.h / this._scale,
        0, 0, targetW, targetH
      );
      const dataUrl = out.toDataURL('image/webp', 0.9);

      if (!this.url || this.url === '#') {
        if (this.outSel) {
          document.querySelectorAll(this.outSel).forEach((el) => {
            el.src = dataUrl;
            el.style.display = 'block';
          });
        }
        SW.emit(this.el, 'sw:cropper:done', { url: dataUrl, thumb: dataUrl, id: 'demo' });
        SW.Alert?.ok('Recorte concluído (modo demo).');
        return;
      }

      const csrf = document.querySelector('meta[name="csrf-token"]')?.content || window._csrf || '';
      const fd = new FormData();
      fd.append('dataUrl', dataUrl);
      fd.append('ref_id', this.refId);
      fd.append('local', this.local);
      fd.append('tipo', this.tipo);

      try {
        const res = await fetch(this.url, {
          method: 'POST',
          headers: { 'X-CSRF-Token': csrf, 'X-Requested-With': 'XMLHttpRequest' },
          body: fd,
        });
        const json = await res.json();
        if (!json.ok) throw new Error(json.erro || 'Erro no upload');

        if (this.outSel) {
          document.querySelectorAll(this.outSel).forEach((el) => {
            el.src = `${json.thumb || json.img}?t=${Date.now()}`;
            el.style.display = 'block';
          });
        }
        SW.emit(this.el, 'sw:cropper:done', { url: json.img, thumb: json.thumb, id: json.id });
      } catch (err) {
        SW.emit(this.el, 'sw:cropper:error', { erro: err.message });
      }
    }
  }

  class SWCropper {
    static initAll(root = document) {
      SW.$('[sw-cropper]', root).forEach((el) => {
        if (el._swCropper) return;
        el._swCropper = new SWCropperInst(el);
      });
    }

    static get(el) { return el._swCropperInst || el._swCropper; }
  }

  window.SW?.register('SWCropper', SWCropper);
  if (window.SW) window.SW.Cropper = SWCropper;
})();

/* SWCore Code — syntax highlighter zero-dependência, portado do SW Framework. Única mudança
   real: os aliases legados y2code/y2-code (compatibilidade com o Y2) foram removidos. O
   atributo canônico do original, "swcode" (sem hífen), foi mantido junto com "sw-code" —
   os dois funcionam. Os 5 temas (one-dark/palenight/dracula/github-dark/monokai) e o botão de
   trocar tema foram mantidos. Realce de sintaxe (js/css/php/json/bash/html) idêntico ao original.
   <pre swcode="js" swcode-theme="dracula" swcode-lines><code>...</code></pre> */
(function () {
  'use strict';

  class SWCode {
    static initAll(root = document) {
      const selectors = 'pre[swcode], pre[sw-code], pre[data-swcode], pre[data-sw-code]';
      const elements = Array.from((root || document).querySelectorAll(selectors));
      const seen = new Set(elements.filter((pre) => pre && !pre._swDone));

      const languageCodes = Array.from((root || document).querySelectorAll('code[class*="language-"], code[class*="lang-"]'));
      languageCodes.forEach((code) => {
        const pre = code.parentElement;
        if (pre && pre.tagName === 'PRE' && !pre._swDone) seen.add(pre);
      });

      seen.forEach((pre) => SWCode._process(pre));
    }

    static _highlight(code, lang) {
      code = code.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');

      const tokens = [];
      let tokenIndex = 0;
      function ph(content, cls) {
        const k = `__TOKEN_${tokenIndex}__`;
        const tk = cls.replace('tk-', '');
        tokens[tokenIndex] = `<span tk="${tk}">${content}</span>`;
        tokenIndex++;
        return k;
      }

      let res = code;

      if (lang === 'js' || lang === 'javascript' || lang === 'ts' || lang === 'typescript') {
        res = res
          .replace(/(\/\/.*$|\/\*[\s\S]*?\*\/)/gm, m => ph(m, 'tk-com'))
          .replace(/('[^'\\]*(?:\\.[^'\\]*)*'|"[^"\\]*(?:\\.[^"\\]*)*"|`[^`\\]*(?:\\.[^`\\]*)*`)/g, m => ph(m, 'tk-str'))
          .replace(/\b(const|let|var|function|return|if|else|for|while|break|continue|switch|case|default|try|catch|finally|throw|new|class|extends|super|import|from|export|async|await|this|typeof|instanceof)\b/g, m => ph(m, 'tk-kw'))
          .replace(/\b(true|false|null|undefined)\b/g, m => ph(m, 'tk-bln'))
          .replace(/\b(\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)\b/g, m => ph(m, 'tk-num'))
          .replace(/\b([a-zA-Z_$][\w$]*)\s*(?=\()/g, (m, f) => f.startsWith('__TOKEN_') ? m : ph(f, 'tk-fn') + m.slice(f.length));
      } else if (lang === 'css' || lang === 'scss' || lang === 'less') {
        res = res
          .replace(/(\/\*[\s\S]*?\*\/)/g, m => ph(m, 'tk-com'))
          .replace(/(@[\w-]+)/g, m => ph(m, 'tk-at'))
          .replace(/(^|\}|\n)([^\{\n]+?)(\s*\{)/g, (m, p, s, b) => p + ph(s.trim(), 'tk-tag') + b)
          .replace(/(--[\w-]+)/g, m => ph(m, 'tk-var'))
          .replace(/([\w-]+)(\s*:\s*)/g, (m, p, s) => ph(p, 'tk-prp') + s)
          .replace(/(:\s*)([^;\}]+)/g, (m, s, v) => s + ph(v, 'tk-str'))
          .replace(/(\b\d+(?:\.\d+)?(px|em|rem|%|vh|vw|ch|ex|cm|mm|in|pt|pc)?\b)/g, m => ph(m, 'tk-num'));
      } else if (lang === 'php') {
        res = res
          .replace(/(&lt;\?php|&lt;\?|\?&gt;)/gi, m => ph(m, 'tk-kw'))
          .replace(/(\/\/[^\n]*|#[^\n]*|\/\*[\s\S]*?\*\/)/g, m => ph(m, 'tk-com'))
          .replace(/('[^'\\]*(?:\\.[^'\\]*)*'|"[^"\\]*(?:\\.[^"\\]*)*")/g, m => ph(m, 'tk-str'))
          .replace(/\b(abstract|and|array|as|break|callable|case|catch|class|clone|const|continue|declare|default|do|echo|else|elseif|empty|enddeclare|endfor|endforeach|endif|endswitch|endwhile|eval|exit|extends|final|finally|fn|for|foreach|function|global|goto|if|implements|include|include_once|instanceof|insteadof|interface|isset|list|match|namespace|new|or|parent|print|private|protected|public|readonly|require|require_once|return|self|static|switch|throw|trait|try|unset|use|var|while|xor|yield)\b/g, m => ph(m, 'tk-kw'))
          .replace(/(\$[a-zA-Z_][\w]*)/g, m => ph(m, 'tk-var'))
          .replace(/\b(\d+(?:\.\d+)?)\b/g, m => ph(m, 'tk-num'))
          .replace(/\b([a-zA-Z_][\w]*)\s*(?=\()/g, (m, f) => f.startsWith('__TOKEN_') ? m : ph(f, 'tk-fn') + m.slice(f.length));
      } else if (lang === 'json') {
        res = res
          .replace(/("[^"]*")/g, m => ph(m, 'tk-str'))
          .replace(/\b(\d+(?:\.\d+)?)\b/g, m => ph(m, 'tk-num'))
          .replace(/\b(true|false|null)\b/g, m => ph(m, 'tk-bln'));
      } else if (lang === 'bash' || lang === 'sh' || lang === 'shell') {
        res = res
          .replace(/(#.*$)/gm, m => ph(m, 'tk-com'))
          .replace(/('[^']*'|"[^"\\]*(?:\\.[^"\\]*)*")/g, m => ph(m, 'tk-str'))
          .replace(/\b(if|then|else|elif|fi|for|in|do|done|while|until|case|esac|function|return|exit|break|continue|local|export|source|echo|read|shift|set|unset|trap|eval)\b/g, m => ph(m, 'tk-kw'))
          .replace(/(\$\{[^}]+\}|\$[a-zA-Z_][\w]*)/g, m => ph(m, 'tk-var'))
          .replace(/\b(\d+)\b/g, m => ph(m, 'tk-num'));
      } else {
        res = res
          .replace(/(&lt;!--[\s\S]*?--&gt;)/g, m => ph(m, 'tk-com'))
          .replace(/(&lt;\/?[a-zA-Z0-9\-]+)([\s\S]*?)(&gt;)/g, (m, t, a, c) => {
            let attrs = a
              .replace(/([a-zA-Z0-9\-:]+)(=)("[^"]*"|'[^']*')/g, (_, n, e, v) => ph(n, 'tk-atr') + e + ph(v, 'tk-str'))
              .replace(/\s([a-zA-Z][a-zA-Z0-9\-:]*)(?=[\s\/>]|$)/g, (_, n) => ' ' + (n.includes('TOKEN') ? n : ph(n, 'tk-atr')));
            return ph(t, 'tk-tag') + attrs + ph(c, 'tk-tag');
          });
      }

      for (let i = tokens.length - 1; i >= 0; i--) {
        res = res.replace(`__TOKEN_${i}__`, tokens[i]);
      }
      return res;
    }

    static _wrapLines(html) {
      return '<div ln>' +
        html.split(/\r?\n/).map(line => `<div ln-r><span ln-n></span><span ln-c>${line || ' '}</span></div>`).join('') +
        '</div>';
    }

    static _process(pre) {
      if (pre._swDone) return;
      pre._swDone = true;

      let lang = pre.getAttribute('swcode') || pre.getAttribute('sw-code') || pre.getAttribute('data-swcode') || pre.getAttribute('data-sw-code') || '';
      const codeEl = pre.querySelector('code');
      if (!lang && codeEl) {
        const cls = [...codeEl.classList].find(c => c.startsWith('language-') || c.startsWith('lang-'));
        if (cls) lang = cls.replace(/lang(uage)?-/, '');
      }

      const hasLines = pre.hasAttribute('swcode-lines') || pre.hasAttribute('sw-code-lines') || pre.hasAttribute('data-swcode-lines') || pre.hasAttribute('data-sw-code-lines');
      const script = pre.querySelector('script[type="text/plain"]');
      const raw = (script ? script.textContent : (codeEl || pre).textContent)
        .replace(/<\\\/script>/gi, '</script>')
        .replace(/^\s+|\s+$/g, '')
        .replace(/[\uFEFF\u200B\u0000-\u0008\u000B-\u000C\u000D-\u001F]/g, '');

      let hl = SWCode._highlight(raw, lang.toLowerCase() || 'html');
      if (hasLines) hl = SWCode._wrapLines(hl);

      const themes = ['one-dark', 'palenight', 'dracula', 'github-dark', 'monokai'];
      const initTheme = pre.getAttribute('swcode-theme') || pre.getAttribute('sw-code-theme') || pre.getAttribute('data-swcode-theme') || pre.getAttribute('data-sw-code-theme') || 'one-dark';
      let themeIdx = Math.max(0, themes.indexOf(initTheme));

      const wrap = document.createElement('div');
      wrap._swDone = true;
      wrap.setAttribute('sw-code-wrap', '');
      wrap.setAttribute('sw-code-theme', themes[themeIdx]);

      const hdr = document.createElement('div');
      hdr.setAttribute('sw-code-hdr', '');

      const badge = document.createElement('span');
      badge.setAttribute('sw-code-lang', '');
      badge.textContent = lang || 'code';

      const themeBtn = document.createElement('button');
      themeBtn.type = 'button';
      themeBtn.setAttribute('sw-code-thm', '');
      themeBtn.textContent = themes[themeIdx];
      themeBtn.onclick = () => {
        themeIdx = (themeIdx + 1) % themes.length;
        wrap.setAttribute('sw-code-theme', themes[themeIdx]);
        themeBtn.textContent = themes[themeIdx];
      };

      const btn = document.createElement('button');
      btn.type = 'button';
      btn.setAttribute('sw-code-cpy', '');
      btn.textContent = 'Copiar';
      btn.onclick = () => {
        navigator.clipboard.writeText(raw).then(() => {
          btn.textContent = 'Copiado!';
          btn.classList.add('is-act');
          setTimeout(() => { btn.textContent = 'Copiar'; btn.classList.remove('is-act'); }, 1200);
        });
      };

      hdr.appendChild(badge);
      hdr.appendChild(themeBtn);
      hdr.appendChild(btn);

      const newPre = document.createElement('pre');
      newPre._swDone = true;
      newPre.setAttribute('sw-code-pre', '');
      if (hasLines) newPre.setAttribute('sw-code-lines', '');
      newPre.innerHTML = hl;

      if (pre.parentNode) {
        pre.parentNode.insertBefore(wrap, pre);
        wrap.appendChild(hdr);
        wrap.appendChild(newPre);
        pre.parentNode.removeChild(pre);
      }
    }
  }

  window.SW?.register('SWCode', SWCode);
  if (window.SW) window.SW.Code = SWCode;
})();

/* SWCore Content — portado do SW Framework sem mudança de lógica. Sem demo em docs/index.html
   por precisar de um endpoint JSON real pra ser testado de ponta a ponta.
   <div sw-content="/api/posts" sw-content-tmpl="#tmpl" sw-content-empty sw-content-err sw-content-trigger="load|click|visible" sw-content-paginate>
   <template>...{{campo}}...</template></div> — fetch JSON, interpola em <template>. API: SWContent.load(el) */
(function () {
  'use strict';

  class SWContentInst {
    constructor(el) {
      this.el = el;
      this.url = el.getAttribute('sw-content');
      this.tmplSel = el.getAttribute('sw-content-tmpl') || null;
      this.emptyMsg = el.getAttribute('sw-content-empty') || '';
      this.errMsg = el.getAttribute('sw-content-err') || 'Erro ao carregar conteúdo.';
      this.trigger = el.getAttribute('sw-content-trigger') || 'load';
      this.paginate = el.hasAttribute('sw-content-paginate');
      this.page = 1;
      this.loading = false;
      this.tmpl = this.tmplSel ? document.querySelector(this.tmplSel) : el.querySelector('template');
      el._swContentInst = this;
      this._bind();
    }

    _bind() {
      if (this.trigger === 'load') this.load();
      if (this.trigger === 'click') {
        const btn = document.querySelector(this.el.getAttribute('sw-content-btn') || '[sw-content-load]');
        btn?.addEventListener('click', () => this.load());
      }
      if (this.trigger === 'visible') {
        const observer = new IntersectionObserver(([entry]) => {
          if (entry.isIntersecting) { observer.disconnect(); this.load(); }
        });
        observer.observe(this.el);
      }
    }

    async load(page = this.page) {
      if (this.loading) return;
      this.loading = true;
      this.page = page;

      let url = this.url;
      if (this.paginate) url += `${url.includes('?') ? '&' : '?'}page=${page}`;

      try {
        const res = await fetch(url, { headers: { Accept: 'application/json', 'X-Requested-With': 'XMLHttpRequest' } });
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        const json = await res.json();
        this._render(json);
      } catch (_) {
        this._showErr();
      } finally {
        this.loading = false;
      }
    }

    _render(json) {
      const list = Array.isArray(json) ? json : Array.isArray(json.dados) ? json.dados : Array.isArray(json.data) ? json.data : [json];
      const container = document.createElement('div');

      if (!list.length) {
        if (this.emptyMsg) container.innerHTML = `<p class="sw-content-mut">${this.emptyMsg}</p>`;
      } else {
        list.forEach((item) => {
          const node = this._renderItem(item);
          if (node) container.appendChild(node);
        });
      }

      const pagina = json.pagina ?? json.page ?? json.current_page ?? 1;
      const paginas = json.paginas ?? json.pages ?? json.last_page ?? 1;

      if (this.paginate && paginas > 1) container.appendChild(this._buildPager(pagina, paginas));

      const keep = this.el.querySelector('template');
      this.el.innerHTML = '';
      if (keep) this.el.appendChild(keep);
      this.el.appendChild(container);
      SW.reinit(container);

      SW.emit(this.el, 'sw:content:loaded', { dados: json });
    }

    _renderItem(item) {
      if (!this.tmpl) return null;
      const clone = this.tmpl.content.cloneNode(true);
      this._interpolate(clone, item);
      const wrap = document.createElement('div');
      wrap.appendChild(clone);
      return wrap.firstElementChild || wrap;
    }

    _interpolate(node, data) {
      if (node.nodeType === Node.TEXT_NODE) {
        node.textContent = node.textContent.replace(/\{\{(\w+)\}\}/g, (_, k) => data[k] ?? '');
        return;
      }
      if (node.nodeType === Node.ELEMENT_NODE) {
        Array.from(node.attributes).forEach((attr) => {
          attr.value = attr.value.replace(/\{\{(\w+)\}\}/g, (_, k) => data[k] ?? '');
        });
      }
      node.childNodes.forEach((c) => this._interpolate(c, data));
    }

    _buildPager(pagina, paginas) {
      const div = document.createElement('div');
      div.className = 'sw-content-pager';
      if (pagina > 1) {
        const btn = document.createElement('button');
        btn.type = 'button';
        btn.className = 'sw-btn sw-btn-sm';
        btn.textContent = '← Anterior';
        btn.addEventListener('click', () => this.load(pagina - 1));
        div.appendChild(btn);
      }
      const info = document.createElement('span');
      info.className = 'sw-content-mut';
      info.textContent = `Página ${pagina} de ${paginas}`;
      div.appendChild(info);
      if (pagina < paginas) {
        const btn = document.createElement('button');
        btn.type = 'button';
        btn.className = 'sw-btn sw-btn-sm';
        btn.textContent = 'Próxima →';
        btn.addEventListener('click', () => this.load(pagina + 1));
        div.appendChild(btn);
      }
      return div;
    }

    _showErr() {
      this.el.innerHTML = `<p class="sw-content-err">${this.errMsg}</p>`;
    }
  }

  class SWContent {
    static initAll(root = document) {
      SW.$('[sw-content]', root).forEach((el) => {
        if (el._swContent) return;
        el._swContent = new SWContentInst(el);
      });
    }

    static load(el) { (el._swContentInst || el._swContent)?.load(); }
  }

  window.SW?.register('SWContent', SWContent);
  if (window.SW) window.SW.Content = SWContent;
})();

/* SWCore Rich Text Editor — portado do SW Framework sem mudança de lógica (exceto os ícones,
   que usam a classe única "swi-x" do SWCore em vez do par "swi swi-x" do original).
   <textarea sw-editor sw-editor-simple sw-editor-height sw-editor-min sw-editor-max sw-editor-resizable> */
(function () {
  'use strict';

  class SWEditorInst {
    constructor(el) {
      this.el = el;
      this.name = el.getAttribute('name') || el.getAttribute('sw-editor-name') || 'content';
      this.ph = el.getAttribute('sw-editor-placeholder') || el.getAttribute('placeholder') || '';
      this.h = parseInt(el.getAttribute('sw-editor-height'), 10) || 150;
      this.minH = parseInt(el.getAttribute('sw-editor-min'), 10) || 100;
      this.maxH = parseInt(el.getAttribute('sw-editor-max'), 10) || 800;
      this.rzbl = el.getAttribute('sw-editor-resizable') !== 'false';
      this.isSimple = el.hasAttribute('sw-editor-simple') || el.getAttribute('sw-editor-mode') === 'simple';
      this.val = el.value || '';
      this._render();
      this._bind();
    }

    _render() {
      const box = document.createElement('div');
      box.className = 'sw-editor-box' + (this.isSimple ? ' is-simple' : '');

      const tb = document.createElement('div');
      tb.className = 'sw-editor-tb';

      if (!this.isSimple) {
        const fmtGrp = document.createElement('div');
        fmtGrp.className = 'sw-editor-grp';
        const fmt = document.createElement('select');
        fmt.className = 'sw-editor-fmt';
        fmt.title = 'Formato do bloco';
        fmt.innerHTML = ['P', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6'].map((t) => `<option value="${t}">${t}</option>`).join('');
        fmtGrp.appendChild(fmt);
        tb.appendChild(fmtGrp);
        this.fmt = fmt;
      }

      const groups = this.isSimple
        ? [
          [
            ['bold', '<b>B</b>', 'Negrito (Ctrl+B)'],
            ['italic', '<i>I</i>', 'Itálico (Ctrl+I)'],
            ['underline', '<u>U</u>', 'Sublinhado (Ctrl+U)'],
            ['strikeThrough', '<s>S</s>', 'Tachado']
          ]
        ]
        : [
          [
            ['bold', '<i class="swi-bold"></i>', 'Negrito (Ctrl+B)'],
            ['italic', '<i class="swi-italic"></i>', 'Itálico (Ctrl+I)'],
            ['underline', '<i class="swi-underline"></i>', 'Sublinhado (Ctrl+U)'],
            ['strikeThrough', '<i class="swi-strikethrough"></i>', 'Tachado']
          ],
          [
            ['foreColor', '<i class="swi-font-color"></i>', 'Cor da fonte'],
            ['backColor', '<i class="swi-highlight"></i>', 'Cor de fundo'],
            ['clean', '<i class="swi-eraser"></i>', 'Limpar formatação']
          ],
          [
            ['justifyLeft', '<i class="swi-align-left"></i>', 'Alinhar esquerda'],
            ['justifyCenter', '<i class="swi-align-middle"></i>', 'Centralizar'],
            ['justifyRight', '<i class="swi-align-right"></i>', 'Alinhar direita'],
            ['justifyFull', '<i class="swi-align-justify"></i>', 'Justificar']
          ],
          [
            ['insertUnorderedList', '<i class="swi-list-ul"></i>', 'Lista'],
            ['insertOrderedList', '<i class="swi-list-ol"></i>', 'Lista numerada'],
            ['hr', '<i class="swi-minus"></i>', 'Linha horizontal'],
            ['link', '<i class="swi-link"></i>', 'Link (Ctrl+K)'],
            ['image', '<i class="swi-image"></i>', 'Imagem'],
            ['code', '<i class="swi-code"></i>', 'Ver HTML'],
            ['fullscreen', '<i class="swi-expand"></i>', 'Tela cheia']
          ]
        ];

      groups.forEach((grp) => {
        const g = document.createElement('div');
        g.className = 'sw-editor-grp';
        grp.forEach(([cmd, content, title]) => {
          g.innerHTML += `<button type="button" class="sw-editor-btn" data-cmd="${cmd}" title="${title}">${content}</button>`;
        });
        tb.appendChild(g);
      });

      if (!this.isSimple) {
        const fgI = document.createElement('input');
        fgI.type = 'color'; fgI.className = 'sw-editor-fg'; fgI.value = '#e53e3e';
        fgI.style.cssText = 'position:absolute;opacity:0;width:1px;height:1px;pointer-events:none';
        const bgI = document.createElement('input');
        bgI.type = 'color'; bgI.className = 'sw-editor-bg'; bgI.value = '#ffff00';
        bgI.style.cssText = 'position:absolute;opacity:0;width:1px;height:1px;pointer-events:none';
        tb.appendChild(fgI);
        tb.appendChild(bgI);
        this.fgI = fgI;
        this.bgI = bgI;

        window.setTimeout(() => {
          const fgBtn = tb.querySelector('[data-cmd="foreColor"]');
          if (fgBtn) {
            fgBtn.style.position = 'relative';
            fgBtn.appendChild(fgI);
            fgI.style.cssText = 'position:absolute;top:0;left:0;width:100%;height:100%;opacity:0;cursor:pointer';
          }
          const bgBtn = tb.querySelector('[data-cmd="backColor"]');
          if (bgBtn) {
            bgBtn.style.position = 'relative';
            bgBtn.appendChild(bgI);
            bgI.style.cssText = 'position:absolute;top:0;left:0;width:100%;height:100%;opacity:0;cursor:pointer';
          }
        }, 0);
      }

      const wrap = document.createElement('div');
      wrap.className = 'sw-editor-wrap';
      const ed = document.createElement('div');
      ed.className = 'sw-editor-ed';
      ed.contentEditable = 'true';
      ed.style.minHeight = `${this.h}px`;
      if (this.ph) ed.setAttribute('placeholder', this.ph);
      ed.innerHTML = this.val || '<p><br></p>';

      const code = document.createElement('textarea');
      code.className = 'sw-editor-code';
      code.value = ed.innerHTML;
      code.spellcheck = false;

      wrap.appendChild(ed);
      wrap.appendChild(code);

      const hidden = document.createElement('textarea');
      hidden.className = 'sw-editor-hidden';
      hidden.name = this.name;
      hidden.style.display = 'none';
      hidden.value = ed.innerHTML;

      const rz = document.createElement('div');
      rz.className = 'sw-editor-rz';
      rz.innerHTML = '<i class="swi-minus"></i>';
      if (!this.rzbl) rz.style.display = 'none';

      const stat = document.createElement('div');
      stat.className = 'sw-editor-stat';
      stat.innerHTML = '<span class="sw-editor-chars">0 caracteres · 0 palavras</span>';

      box.appendChild(tb);
      box.appendChild(wrap);
      box.appendChild(rz);
      box.appendChild(stat);
      box.appendChild(hidden);

      this.el.parentNode.insertBefore(box, this.el.nextSibling);
      this.el.style.display = 'none';

      this.box = box; this.tb = tb; this.wrap = wrap;
      this.ed = ed; this.code = code; this.hidden = hidden;
      this.rz = rz; this.stat = stat;
      this._updateStatus();
    }

    _bind() {
      this._sel = null;
      const saveSel = () => {
        const s = window.getSelection();
        if (s && s.rangeCount) { try { this._sel = s.getRangeAt(0).cloneRange(); } catch (_) { this._sel = null; } }
      };
      const restSel = () => {
        if (!this._sel) return;
        try { const s = window.getSelection(); s.removeAllRanges(); s.addRange(this._sel); } catch (_) { /* seleção fora do documento */ }
      };

      this.ed.addEventListener('mouseup', saveSel);
      this.ed.addEventListener('keyup', saveSel);
      this.ed.addEventListener('focus', saveSel);
      if (this.fgI) this.fgI.addEventListener('mousedown', saveSel);
      if (this.bgI) this.bgI.addEventListener('mousedown', saveSel);

      this.tb.addEventListener('mousedown', (event) => {
        const btn = event.target.closest('.sw-editor-btn');
        if (!btn) return;
        const cmd = btn.getAttribute('data-cmd');
        saveSel();
        if (cmd === 'foreColor' || cmd === 'backColor') return;
        event.preventDefault();
        this._exec(cmd, restSel);
        window.setTimeout(() => { this.ed.focus(); this._updateBtns(); }, 0);
      });

      if (this.fmt) {
        this.fmt.addEventListener('change', () => {
          document.execCommand('formatBlock', false, this.fmt.value);
          this._sync(); this.ed.focus(); this._updateBtns();
        });
      }

      if (this.fgI) {
        this.fgI.addEventListener('input', () => {
          this.ed.focus(); restSel();
          document.execCommand('foreColor', false, this.fgI.value);
          this._sync();
        });
      }
      if (this.bgI) {
        this.bgI.addEventListener('input', () => {
          this.ed.focus(); restSel();
          document.execCommand('hiliteColor', false, this.bgI.value);
          this._sync();
        });
      }

      this.ed.addEventListener('input', () => { this._sync(); this._updateStatus(); });
      this.ed.addEventListener('keyup', () => this._updateBtns());
      this.ed.addEventListener('mouseup', () => this._updateBtns());
      this.ed.addEventListener('keydown', (event) => this._shortcuts(event, restSel));
      this.code.addEventListener('input', () => { this._sync(); this._updateStatus(); });

      if (this.rzbl) this._bindResize();
    }

    _bindResize() {
      let startY = 0;
      let startH = 0;
      const move = (event) => {
        const h = Math.max(this.minH, Math.min(this.maxH, startH + event.clientY - startY));
        this.wrap.style.height = `${h}px`;
        this.ed.style.minHeight = `${h}px`;
      };
      const up = () => {
        document.removeEventListener('mousemove', move);
        document.removeEventListener('mouseup', up);
        this.box.classList.remove('sw-editor-resizing');
        document.body.style.userSelect = '';
      };
      this.rz.addEventListener('mousedown', (event) => {
        event.preventDefault();
        startY = event.clientY;
        startH = this.wrap.offsetHeight;
        this.box.classList.add('sw-editor-resizing');
        document.body.style.userSelect = 'none';
        document.addEventListener('mousemove', move);
        document.addEventListener('mouseup', up);
      });
    }

    _exec(cmd, restSel) {
      if (cmd === 'code') {
        const show = this.code.style.display !== 'block';
        if (show) {
          this.code.value = this.ed.innerHTML;
          this.code.style.minHeight = `${this.ed.offsetHeight}px`;
          this.ed.style.display = 'none';
          this.code.style.display = 'block';
          this.box.classList.add('sw-editor-codeview');
        } else {
          this.ed.innerHTML = this.code.value;
          this.code.style.display = 'none';
          this.ed.style.display = '';
          this.box.classList.remove('sw-editor-codeview');
        }
        this._sync(); this._updateBtns(); return;
      }
      if (cmd === 'clean') { document.execCommand('removeFormat'); document.execCommand('unlink'); this._sync(); return; }
      if (cmd === 'hr') { document.execCommand('insertHorizontalRule'); this._sync(); return; }
      if (cmd === 'link') { const u = window.prompt('URL do link:'); if (u) { this.ed.focus(); restSel(); document.execCommand('createLink', false, u); } this._sync(); return; }
      if (cmd === 'image') { const u = window.prompt('URL da imagem:'); if (u) { this.ed.focus(); restSel(); document.execCommand('insertImage', false, u); } this._sync(); return; }
      if (cmd === 'fullscreen') {
        const on = !this.box.classList.contains('sw-editor-fullscreen');
        this.box.classList.toggle('sw-editor-fullscreen', on);
        document.body.style.overflow = on ? 'hidden' : '';
        this.wrap.style.height = on ? 'calc(100vh - 10rem)' : '';
        this._updateBtns(); return;
      }
      if (cmd === 'undo' || cmd === 'redo') { document.execCommand(cmd); this._sync(); return; }
      this.ed.focus();
      if (this._sel) restSel();
      document.execCommand(cmd);
      this._sync();
    }

    _sync() {
      if (this.code.style.display === 'block') {
        this.hidden.value = this.code.value;
      } else {
        this.hidden.value = this.ed.innerHTML;
        this.code.value = this.ed.innerHTML;
      }
    }

    _updateStatus() {
      const t = this.ed.innerText || '';
      const w = t.trim() ? t.trim().split(/\s+/).length : 0;
      this.stat.querySelector('.sw-editor-chars').textContent = `${t.length} caracteres · ${w} palavras`;
    }

    _updateBtns() {
      this.tb.querySelectorAll('.sw-editor-btn').forEach((b) => b.classList.remove('is-act'));
      ['bold', 'italic', 'underline', 'strikeThrough', 'insertUnorderedList', 'insertOrderedList', 'justifyLeft', 'justifyCenter', 'justifyRight', 'justifyFull'].forEach((cmd) => {
        try { if (document.queryCommandState(cmd)) this.tb.querySelector(`[data-cmd="${cmd}"]`)?.classList.add('is-act'); } catch (_) { /* comando indisponível neste contexto */ }
      });
      if (this.code.style.display === 'block') this.tb.querySelector('[data-cmd="code"]')?.classList.add('is-act');
      if (this.box.classList.contains('sw-editor-fullscreen')) this.tb.querySelector('[data-cmd="fullscreen"]')?.classList.add('is-act');
      if (this.fmt) {
        try { const fb = document.queryCommandValue('formatBlock'); if (fb) this.fmt.value = fb.toUpperCase(); } catch (_) { /* comando indisponível neste contexto */ }
      }
    }

    _shortcuts(event, restSel) {
      if (!event.ctrlKey && !event.metaKey) return;
      const k = event.key.toLowerCase();
      const map = { b: 'bold', i: 'italic', u: 'underline', k: 'link', z: 'undo', y: 'redo' };
      const fmts = { 0: 'P', 1: 'H1', 2: 'H2', 3: 'H3', 4: 'H4', 5: 'H5', 6: 'H6' };
      if (map[k]) { event.preventDefault(); this._exec(map[k], restSel); this._sync(); }
      else if (fmts[k] && this.fmt) { event.preventDefault(); document.execCommand('formatBlock', false, fmts[k]); this._sync(); }
    }

    getValue() { return this.hidden.value; }
    setValue(html) { this.ed.innerHTML = html || '<p><br></p>'; this._sync(); this._updateStatus(); }
  }

  class SWEditor {
    static initAll(root = document) {
      SW.$('[sw-editor]', root).forEach((el) => {
        if (el._swEditor) return;
        el._swEditor = new SWEditorInst(el);
      });
    }
  }

  window.SW?.register('SWEditor', SWEditor);
  if (window.SW) window.SW.Editor = SWEditor;
})();

/* SWCore Table — portado do SW Framework sem mudança de lógica. Tabela interativa
   (busca, ordenação, paginação, seleção, visibilidade de colunas, export CSV/copiar, fonte
   AJAX ou dados JS puros via SWTable.fromData). <table sw-table sw-table-search sw-table-sort
   sw-table-select sw-table-colvis sw-table-export="csv,copy" sw-table-per="10">...</table> */
(function () {
  'use strict';

  class SWTable {
    static initAll(root = document) {
      SW.$('[sw-table]', root).forEach(el => {
        if (el._swTab) return;
        el._swTab = true;
        new SWTableInst(el);
      });
    }
    static reload(el) { el._swTabInst?.reload(); }
    static search(el, query) { el._swTabInst?.search(query); }
    static export(el, fmt) { el._swTabInst?._export(fmt); }

    static fromData(el, cols, rows, opts = {}) {
      const tbl = typeof el === 'string' ? document.querySelector(el) : el;
      if (!tbl) return null;

      const colDefs = cols.map(c => typeof c === 'string'
        ? { label: c, field: null, nosort: false, novis: false }
        : { label: c.label ?? String(c), field: c.field ?? null, nosort: !!c.nosort, novis: !!c.novis }
      );

      let thead = tbl.querySelector('thead');
      if (!thead) { thead = document.createElement('thead'); tbl.insertBefore(thead, tbl.firstChild); }
      thead.innerHTML = '<tr>' + colDefs.map(c =>
        '<th' + (c.nosort ? ' sw-table-nosort' : '') + (c.novis ? ' sw-table-novis' : '') + '>' + c.label + '</th>'
      ).join('') + '</tr>';

      let tbody = tbl.querySelector('tbody');
      if (!tbody) { tbody = document.createElement('tbody'); tbl.appendChild(tbody); }
      tbody.innerHTML = rows.map(row => {
        const cells = Array.isArray(row)
          ? row.map(v => `<td>${v ?? ''}</td>`).join('')
          : colDefs.map(c => `<td>${c.field != null ? (row[c.field] ?? '') : ''}</td>`).join('');
        return `<tr>${cells}</tr>`;
      }).join('');

      if (tbl._swTabInst) {
        const inst = tbl._swTabInst;
        inst._fromTable(tbl);
        inst.cur = 1;
        inst.query = '';
        const inp = inst.outer?.querySelector('.tbl-srch-inp');
        if (inp) inp.value = '';
        inst._filter();
        return inst;
      }

      if (!tbl.hasAttribute('sw-table')) tbl.setAttribute('sw-table', '');
      Object.entries(opts).forEach(([k, v]) => {
        if (v === false || v === undefined) return;
        tbl.setAttribute(`sw-table-${k}`, v === true ? '' : String(v));
      });
      tbl._swTab = true;
      return new SWTableInst(tbl);
    }
  }

  class SWTableInst {
    constructor(el) {
      this.el = el;
      this.per = parseInt(el.getAttribute('sw-table-per') || 10, 10);
      this.perOpts = (el.getAttribute('sw-table-per-opts') || '10,25,50,100').split(',').map(Number);
      this.doSearch = el.getAttribute('sw-table-search') !== 'false';
      this.doSort = el.getAttribute('sw-table-sort') !== 'false';
      this.doStripe = el.getAttribute('sw-table-stripe') !== 'false';
      this.doSelect = el.hasAttribute('sw-table-select');
      this.doColvis = el.hasAttribute('sw-table-colvis');
      this.doState = el.hasAttribute('sw-table-state');
      this.exports = (el.getAttribute('sw-table-export') || '').split(',').map(s => s.trim()).filter(Boolean);
      this.url = el.getAttribute('sw-table-url');
      this.cur = 1;
      this.sortCol = -1;
      this.sortDir = 'asc';
      this.query = '';
      this._rows = [];
      this._filtered = [];
      this._hidden = new Set();
      this._selected = new Set();
      this._colDefs = [];
      this._stKey = `swtab_${location.pathname}_${el.id || el.className.split(' ')[0] || 'tbl'}`;

      el._swTabInst = this;

      if (this.doState) this._loadState();
      this._buildShell();

      if (this.url) {
        this._buildCtrl();
        this._buildFoot();
        this._loadAjax();
      } else {
        this._fromTable(el);
        this._buildCtrl();
        this._buildFoot();
        this._filter();
      }
    }

    _fromTable(tbl) {
      const ths = Array.from(tbl.querySelectorAll('thead th'));
      this._colDefs = ths.map(th => ({
        label: th.textContent.trim(),
        nosort: th.hasAttribute('sw-table-nosort'),
        novis: th.hasAttribute('sw-table-novis'),
      }));
      this._rows = Array.from(tbl.querySelectorAll('tbody tr')).map(tr =>
        Array.from(tr.querySelectorAll('td')).map(td => ({ html: td.innerHTML, text: td.textContent.trim() }))
      );
      this._filtered = [...this._rows];
      tbl.querySelector('tbody').innerHTML = '';
    }

    _buildShell() {
      const outer = document.createElement('div');
      outer.className = 'tbl-outer';
      this.el.parentNode.insertBefore(outer, this.el);
      this.outer = outer;

      const scroll = document.createElement('div');
      scroll.className = 'tbl-scroll';
      outer.appendChild(scroll);
      scroll.appendChild(this.el);
      this.scroll = scroll;

      this.el.classList.add('tbl-tbl');
      if (this.doStripe) this.el.classList.add('tbl-stripe');
    }

    _buildCtrl() {
      const ctrl = document.createElement('div');
      ctrl.className = 'tbl-ctrl';
      this.outer.insertBefore(ctrl, this.scroll);

      const ctrlL = document.createElement('div');
      ctrlL.className = 'tbl-ctrl-l';
      ctrl.appendChild(ctrlL);

      const perWrap = document.createElement('div');
      perWrap.className = 'tbl-per';
      perWrap.innerHTML = '<label class="tbl-per-lbl">Exibir <select class="tbl-per-sel">' +
        this.perOpts.map(v => '<option value="' + v + '"' + (v === this.per ? ' selected' : '') + '>' + v + '</option>').join('') +
        '</select> por página</label>';
      perWrap.querySelector('.tbl-per-sel').addEventListener('change', e => {
        this.per = parseInt(e.target.value, 10);
        this.cur = 1;
        this.url ? this._loadAjax() : this._filter();
        if (this.doState) this._saveState();
      });
      ctrlL.appendChild(perWrap);

      if (this.exports.length) {
        const _expIco = { csv: '<i class="swi-file-blank"></i>', copy: '<i class="swi-copy"></i>' };
        const _expTip = { csv: 'Exportar CSV', copy: 'Copiar para área de transferência' };
        const expDiv = document.createElement('div');
        expDiv.className = 'tbl-exp';
        this.exports.forEach(fmt => {
          const btn = document.createElement('button');
          btn.type = 'button';
          btn.className = 'tbl-exp-btn';
          btn.title = _expTip[fmt] || fmt;
          btn.innerHTML = _expIco[fmt] || fmt.toUpperCase();
          btn.addEventListener('click', () => this._export(fmt));
          expDiv.appendChild(btn);
        });
        ctrlL.appendChild(expDiv);
      }

      if (this.doColvis) {
        const colvisDiv = document.createElement('div');
        colvisDiv.className = 'tbl-colvis';
        const cvBtn = document.createElement('button');
        cvBtn.type = 'button';
        cvBtn.className = 'tbl-colvis-btn';
        cvBtn.title = 'Visibilidade de colunas';
        cvBtn.innerHTML = '<i class="swi-grid-alt"></i>';
        const drop = document.createElement('div');
        drop.className = 'tbl-colvis-drop';
        this._colDefs.forEach((col, i) => {
          if (col.novis) return;
          const lbl = document.createElement('label');
          lbl.className = 'tbl-colvis-it';
          const chk = document.createElement('input');
          chk.type = 'checkbox';
          chk.checked = !this._hidden.has(i);
          chk.addEventListener('change', () => {
            chk.checked ? this._hidden.delete(i) : this._hidden.add(i);
            this._applyVisibility();
            if (this.doState) this._saveState();
          });
          lbl.appendChild(chk);
          lbl.append(' ' + col.label);
          drop.appendChild(lbl);
        });
        cvBtn.addEventListener('click', e => { e.stopPropagation(); colvisDiv.classList.toggle('is-open'); });
        document.addEventListener('click', () => colvisDiv.classList.remove('is-open'), { passive: true });
        colvisDiv.appendChild(cvBtn);
        colvisDiv.appendChild(drop);
        ctrlL.appendChild(colvisDiv);
      }

      if (this.doSearch) {
        const srchWrap = document.createElement('div');
        srchWrap.className = 'tbl-srch';
        srchWrap.innerHTML = '<i class="swi-search"></i>';
        const inp = document.createElement('input');
        inp.type = 'search';
        inp.placeholder = 'Buscar…';
        inp.className = 'tbl-srch-inp';
        inp.value = this.query;
        let debounce;
        inp.addEventListener('input', () => {
          clearTimeout(debounce);
          debounce = setTimeout(() => {
            this.query = inp.value.toLowerCase();
            this.cur = 1;
            this.url ? this._loadAjax() : this._filter();
            SW.emit(this.el, 'sw:table:search', { query: this.query });
          }, 300);
        });
        srchWrap.appendChild(inp);
        ctrl.appendChild(srchWrap);
      }

      if (this.doSort) {
        this.el.querySelectorAll('thead th').forEach((th, i) => {
          const colIdx = this.doSelect ? i - 1 : i;
          if (colIdx < 0 || this._colDefs[colIdx]?.nosort) return;
          th.classList.add('tbl-sort');
          th.tabIndex = 0;
          th.setAttribute('aria-sort', colIdx === this.sortCol ? (this.sortDir === 'asc' ? 'ascending' : 'descending') : 'none');
          if (colIdx === this.sortCol) th.classList.add(`is-${this.sortDir}`);
          const applySort = () => {
            if (this.sortCol === colIdx) {
              this.sortDir = this.sortDir === 'asc' ? 'desc' : 'asc';
            } else {
              this.sortCol = colIdx;
              this.sortDir = 'asc';
            }
            this.el.querySelectorAll('thead th.tbl-sort').forEach(h => {
              h.classList.remove('is-asc', 'is-desc');
              h.setAttribute('aria-sort', 'none');
            });
            th.classList.add(`is-${this.sortDir}`);
            th.setAttribute('aria-sort', this.sortDir === 'asc' ? 'ascending' : 'descending');
            this.cur = 1;
            this.url ? this._loadAjax() : this._filter();
            SW.emit(this.el, 'sw:table:sort', { col: this.sortCol, dir: this.sortDir });
            if (this.doState) this._saveState();
          };
          th.addEventListener('click', applySort);
          th.addEventListener('keydown', (event) => {
            if (event.key !== 'Enter' && event.key !== ' ') return;
            event.preventDefault();
            applySort();
          });
        });
      }

      if (this.doSelect) {
        const theadTr = this.el.querySelector('thead tr');
        if (theadTr) {
          const th = document.createElement('th');
          th.className = 'tbl-sel-th';
          th.innerHTML = `<input type="checkbox" class="tbl-chk tbl-sel-all" title="Selecionar tudo">`;
          theadTr.insertBefore(th, theadTr.firstChild);
          th.querySelector('.tbl-sel-all').addEventListener('change', e => {
            this.el.querySelectorAll('tbody .tbl-sel-row').forEach(cb => {
              cb.checked = e.target.checked;
              const idx = parseInt(cb.closest('tr').dataset.ridx, 10);
              e.target.checked ? this._selected.add(idx) : this._selected.delete(idx);
            });
            this._updateSelInfo();
            SW.emit(this.el, 'sw:table:select', { selected: [...this._selected] });
          });
        }
      }

      this._applyVisibility();
    }

    _buildFoot() {
      const foot = document.createElement('div');
      foot.className = 'tbl-foot';
      this.outer.appendChild(foot);
      this.foot = foot;
    }

    _filter() {
      let data = [...this._rows];
      if (this.query) {
        data = data.filter(row => row.some(c => c.text.toLowerCase().includes(this.query)));
      }
      if (this.sortCol >= 0) {
        data.sort((a, b) => {
          const va = a[this.sortCol]?.text ?? '';
          const vb = b[this.sortCol]?.text ?? '';
          const na = parseFloat(va), nb = parseFloat(vb);
          const cmp = !isNaN(na) && !isNaN(nb) ? na - nb : va.localeCompare(vb, 'pt-BR', { sensitivity: 'base' });
          return this.sortDir === 'asc' ? cmp : -cmp;
        });
      }
      this._filtered = data;
      this.cur = Math.min(this.cur, Math.ceil(data.length / this.per) || 1);
      this._render();
    }

    _render() {
      const start = (this.cur - 1) * this.per;
      const page = this._filtered.slice(start, start + this.per);
      const tbody = this.el.querySelector('tbody');
      const total = this._rows.length;
      const filt = this._filtered.length;
      const from = filt ? start + 1 : 0;
      const to = Math.min(start + this.per, filt);

      if (!page.length) {
        tbody.innerHTML = `<tr><td class="tbl-empty" colspan="99">Nenhum registro encontrado.</td></tr>`;
      } else {
        tbody.innerHTML = page.map((row, ri) => {
          const absIdx = start + ri;
          const selCell = this.doSelect
            ? `<td class="tbl-sel-td"><input type="checkbox" class="tbl-chk tbl-sel-row"${this._selected.has(absIdx) ? ' checked' : ''}></td>`
            : '';
          const cells = row.map((cell, ci) => {
            const hide = this._hidden.has(ci) ? ' style="display:none"' : '';
            return `<td${hide}>${cell.html}</td>`;
          }).join('');
          return `<tr data-ridx="${absIdx}">${selCell}${cells}</tr>`;
        }).join('');

        if (this.doSelect) {
          tbody.querySelectorAll('.tbl-sel-row').forEach(cb => {
            cb.addEventListener('change', () => {
              const idx = parseInt(cb.closest('tr').dataset.ridx, 10);
              cb.checked ? this._selected.add(idx) : this._selected.delete(idx);
              this._updateSelInfo();
              SW.emit(this.el, 'sw:table:select', { selected: [...this._selected] });
            });
          });
        }
      }

      const pages = Math.ceil(filt / this.per);
      const selInfo = this._selected.size ? `<span class="tbl-sel-info">${this._selected.size} selecionado(s) &bull; </span>` : '';
      const info = filt < total
        ? `${selInfo}Exibindo ${from}–${to} de ${filt} (filtrado de ${total})`
        : `${selInfo}Exibindo ${from}–${to} de ${total} registros`;

      this.foot.innerHTML = `
        <div class="tbl-info">${info}</div>
        <nav class="tbl-pag" aria-label="Paginação">${this._pagHTML(pages)}</nav>`;

      this.foot.querySelectorAll('[data-p]').forEach(btn => {
        btn.addEventListener('click', () => {
          const p = parseInt(btn.dataset.p, 10);
          if (isNaN(p) || p === this.cur) return;
          this.cur = p;
          this.url ? this._loadAjax() : this._render();
          SW.emit(this.el, 'sw:table:page', { page: this.cur });
        });
      });
    }

    _pagHTML(pages) {
      if (pages <= 1) return '';
      const c = this.cur;
      const btn = (p, label, extra = '') => {
        const disabled = p < 1 || p > pages || p === c;
        return `<button class="tbl-pg-it${p === c ? ' is-act' : ''}${extra}" data-p="${p}" ${disabled ? 'disabled' : ''} type="button">${label}</button>`;
      };

      let nums = [];
      if (pages <= 7) {
        nums = Array.from({ length: pages }, (_, i) => i + 1);
      } else {
        nums = [1];
        if (c > 3) nums.push('…');
        for (let i = Math.max(2, c - 1); i <= Math.min(pages - 1, c + 1); i++) nums.push(i);
        if (c < pages - 2) nums.push('…');
        nums.push(pages);
      }

      return (
        btn(c - 1, '‹', ' tbl-pg-nav') +
        nums.map(n => n === '…' ? `<span class="tbl-pg-ell">…</span>` : btn(n, n)).join('') +
        btn(c + 1, '›', ' tbl-pg-nav')
      );
    }

    _loadAjax() {
      const colDefs = JSON.parse(this.el.getAttribute('sw-table-cols') || '[]');
      const tbody = this.el.querySelector('tbody') || this.el.appendChild(document.createElement('tbody'));
      tbody.innerHTML = `<tr><td class="tbl-proc" colspan="99"></td></tr>`;

      const sortField = this.sortCol >= 0 ? (colDefs[this.sortCol]?.field || '') : '';
      const params = new URLSearchParams({
        page: this.cur, per: this.per,
        search: this.query, sort: sortField, dir: this.sortDir,
      });

      fetch(`${this.url}?${params}`, { headers: { 'X-Requested-With': 'XMLHttpRequest' } })
        .then(async r => {
          const contentType = r.headers.get('content-type') || '';
          if (contentType.includes('application/json')) {
            return r.json();
          }
          const text = await r.text();
          try { return JSON.parse(text); } catch (_) { return { html: text }; }
        })
        .then(res => {
          if (res.html !== undefined) {
            const scratch = document.createElement('tbody');
            scratch.innerHTML = res.html;
            const rows = Array.from(scratch.children);
            if (!rows.length) {
              tbody.innerHTML = `<tr><td class="tbl-empty" colspan="99">Nenhum registro encontrado.</td></tr>`;
            } else {
              tbody.replaceChildren(...rows);
            }
            this._rows = Array.from(tbody.rows).map(tr =>
              Array.from(tr.querySelectorAll('td')).map(td => ({ html: td.innerHTML, text: td.textContent.trim() }))
            );
            this._filtered = [...this._rows];
            const total = res.total ?? this._rows.length;
            const filtered = res.filtered ?? total;
            const pages = Math.ceil(filtered / (this.per || 10));
            const start = (this.cur - 1) * (this.per || 10);
            const from = filtered ? start + 1 : 0;
            const to = Math.min(start + (this.per || 10), filtered);
            const info = filtered < total
              ? `Exibindo ${from}–${to} de ${filtered} (filtrado de ${total})`
              : `Exibindo ${from}–${to} de ${total} registros`;

            this.foot.innerHTML = `
              <div class="tbl-info">${info}</div>
              <nav class="tbl-pag">${this._pagHTML(pages)}</nav>`;

            this.foot.querySelectorAll('[data-p]').forEach(btn => {
              btn.addEventListener('click', () => {
                const p = parseInt(btn.dataset.p, 10);
                if (isNaN(p) || p === this.cur) return;
                this.cur = p;
                this._loadAjax();
                SW.emit(this.el, 'sw:table:page', { page: this.cur });
              });
            });
            if (this._hidden.size) this._applyVisibility();
            return;
          }

          const arr = Array.isArray(res) ? res : (res.data || res.rows || []);
          const total = res.total ?? res.count ?? arr.length;
          const filtered = res.filtered ?? total;

          if (!colDefs.length && arr.length) {
            Object.keys(arr[0]).forEach(k => colDefs.push({ label: k, field: k }));
          }

          if (!this.el.querySelector('thead') && colDefs.length) {
            const thead = document.createElement('thead');
            thead.innerHTML = '<tr>' + colDefs.map(c => '<th>' + c.label + '</th>').join('') + '</tr>';
            this.el.insertBefore(thead, this.el.firstChild);
            this._colDefs = colDefs.map(c => ({ label: c.label, nosort: false, novis: false }));
            if (this.doSort) {
              this.el.querySelectorAll('thead th').forEach((th, i) => {
                th.classList.add('tbl-sort');
                th.tabIndex = 0;
                th.setAttribute('aria-sort', 'none');
                const applySort = () => {
                  this.sortCol = i;
                  this.sortDir = this.sortCol === i && this.sortDir === 'asc' ? 'desc' : 'asc';
                  this.el.querySelectorAll('thead th.tbl-sort').forEach(h => {
                    h.classList.remove('is-asc', 'is-desc');
                    h.setAttribute('aria-sort', 'none');
                  });
                  th.classList.add(`is-${this.sortDir}`);
                  th.setAttribute('aria-sort', this.sortDir === 'asc' ? 'ascending' : 'descending');
                  this.cur = 1;
                  this._loadAjax();
                };
                th.addEventListener('click', applySort);
                th.addEventListener('keydown', (event) => {
                  if (event.key !== 'Enter' && event.key !== ' ') return;
                  event.preventDefault();
                  applySort();
                });
              });
            }
          }

          tbody.innerHTML = arr.length
            ? arr.map(row =>
              '<tr>' + colDefs.map(c => '<td>' + (row[c.field] ?? '') + '</td>').join('') + '</tr>'
            ).join('')
            : `<tr><td class="tbl-empty" colspan="99">Nenhum registro encontrado.</td></tr>`;

          const pages = Math.ceil(filtered / this.per);
          const start = (this.cur - 1) * this.per;
          const from = filtered ? start + 1 : 0;
          const to = Math.min(start + this.per, filtered);
          const info = filtered < total
            ? `Exibindo ${from}–${to} de ${filtered} (filtrado de ${total})`
            : `Exibindo ${from}–${to} de ${total} registros`;

          this.foot.innerHTML = `
            <div class="tbl-info">${info}</div>
            <nav class="tbl-pag">${this._pagHTML(pages)}</nav>`;

          this.foot.querySelectorAll('[data-p]').forEach(btn => {
            btn.addEventListener('click', () => {
              const p = parseInt(btn.dataset.p, 10);
              if (isNaN(p) || p === this.cur) return;
              this.cur = p;
              this._loadAjax();
              SW.emit(this.el, 'sw:table:page', { page: this.cur });
            });
          });

          if (this._hidden.size) this._applyVisibility();
        })
        .catch(() => {
          tbody.innerHTML = `<tr><td class="tbl-empty" colspan="99">Erro ao carregar dados.</td></tr>`;
        });
    }

    _applyVisibility() {
      const offset = this.doSelect ? 1 : 0;
      this.el.querySelectorAll('thead th').forEach((th, i) => {
        const ci = i - offset;
        if (ci < 0) return;
        th.style.display = this._hidden.has(ci) ? 'none' : '';
      });
      this.el.querySelectorAll('tbody tr').forEach(tr => {
        tr.querySelectorAll('td').forEach((td, i) => {
          const ci = i - offset;
          if (ci < 0) return;
          td.style.display = this._hidden.has(ci) ? 'none' : '';
        });
      });
    }

    _updateSelInfo() {
      const info = this.foot?.querySelector('.tbl-info');
      if (!info) return;
      const selSpan = info.querySelector('.tbl-sel-info');
      const newTxt = this._selected.size ? `<span class="tbl-sel-info">${this._selected.size} selecionado(s) &bull; </span>` : '';
      if (selSpan) selSpan.outerHTML = newTxt; else info.insertAdjacentHTML('afterbegin', newTxt);
    }

    _export(fmt) {
      const offset = this.doSelect ? 1 : 0;
      const ths = Array.from(this.el.querySelectorAll('thead th'))
        .filter((_, i) => {
          const ci = i - offset;
          return ci >= 0 && !this._hidden.has(ci);
        })
        .map(th => th.textContent.trim().replace(/[⇅↑↓]/g, '').trim());

      const rows = (this._filtered.length ? this._filtered : this._rows).map(row =>
        row.filter((_, ci) => !this._hidden.has(ci)).map(c => c.text)
      );

      if (fmt === 'copy') {
        const txt = [ths, ...rows].map(r => r.join('\t')).join('\n');
        navigator.clipboard?.writeText(txt).then(() => {
          SW.Alert?.ok(`${rows.length} linha(s) copiadas`);
        });
        return;
      }

      if (fmt === 'csv') {
        const esc = v => `"${String(v).replace(/"/g, '""')}"`;
        const csv = [ths, ...rows].map(r => r.map(esc).join(',')).join('\r\n');
        const a = document.createElement('a');
        a.href = URL.createObjectURL(new Blob(['﻿' + csv], { type: 'text/csv;charset=utf-8' }));
        a.download = 'tabela.csv';
        a.click();
        URL.revokeObjectURL(a.href);
      }
    }

    _saveState() {
      try {
        localStorage.setItem(this._stKey, JSON.stringify({
          per: this.per, sortCol: this.sortCol, sortDir: this.sortDir,
          query: this.query, hidden: [...this._hidden],
        }));
      } catch (_) { /* quota exceeded */ }
    }

    _loadState() {
      try {
        const s = JSON.parse(localStorage.getItem(this._stKey) || 'null');
        if (!s) return;
        this.per = s.per ?? this.per;
        this.sortCol = s.sortCol ?? this.sortCol;
        this.sortDir = s.sortDir ?? this.sortDir;
        this.query = s.query ?? this.query;
        this._hidden = new Set(s.hidden || []);
      } catch (_) { /* corrupted */ }
    }

    reload() { this.url ? this._loadAjax() : this._filter(); }
    search(q) {
      this.query = q.toLowerCase();
      this.cur = 1;
      const inp = this.outer?.querySelector('.tbl-srch-inp');
      if (inp) inp.value = q;
      this.reload();
    }
  }

  class SWTableAjax {
    static initAll(root = document) {
      SW.$('[sw-table-ajax]', root).forEach(el => {
        if (el._swTab) return;
        el._swTab = true;
        if (!el.hasAttribute('sw-table-url')) {
          const u = el.getAttribute('sw-table-ajax');
          if (u) el.setAttribute('sw-table-url', u);
        }
        el.setAttribute('sw-table', '');
        new SWTableInst(el);
      });
    }
  }

  window.SW?.register('SWTable', SWTable);
  window.SW?.register('SWTableAjax', SWTableAjax);
  if (window.SW) window.SW.Table = SWTable;
})();

/* SWCore Day — controlador de tema. [sw-day] no botão alterna; [sw-day-set="dark|light"] define
   o tema direto ao clicar, sem alternar (usado pelo par sol/lua do FAB e por qualquer botão
   próprio equivalente, ex. a barra lateral da doc). Sem nenhum botão próprio na página, injeta
   um FAB flutuante preso na borda direita, centralizado verticalmente, com os dois botões
   empilhados (sol/lua). <html sw-day-lock="dark|light"> trava o tema (esconde qualquer toggle).
   Atalho Ctrl+Shift+D. [sw-day-img-{tema}]/[sw-day-bg-{tema}]/[sw-day-font-{tema}] trocam mídia
   conforme o tema. */
(function () {
  'use strict';
  const allowedThemes = new Set(['dark', 'light']);
  const STORAGE_KEY = 'sw-theme';
  let storageBound = false;
  let keyboardBound = false;
  let fabInjected = false;

  function resolveVar(value) {
    if (!value || !value.includes('var(')) return value;
    const match = value.match(/var\((--[^,)]+)(?:,\s*([^)]+))?\)/);
    if (!match) return value;
    return getComputedStyle(document.documentElement).getPropertyValue(match[1]).trim() || match[2] || value;
  }

  function lockedTheme() {
    const raw = document.documentElement.getAttribute('sw-day-lock');
    return allowedThemes.has(raw) ? raw : null;
  }

  const SWDay = {
    get theme() {
      return document.documentElement.getAttribute('sw-theme') || 'dark';
    },
    init() {
      const locked = lockedTheme();
      if (locked) { this.set(locked, false); return; }
      const savedTheme = SW.Utils.storage.get(STORAGE_KEY);
      const systemTheme = window.matchMedia?.('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
      this.set(savedTheme || systemTheme, true);
    },
    set(theme, persist = true) {
      if (!allowedThemes.has(theme)) return false;
      const locked = lockedTheme();
      if (locked && theme !== locked) return false;
      document.documentElement.setAttribute('sw-theme', theme);
      document.documentElement.style.colorScheme = theme;
      if (persist) SW.Utils.storage.set(STORAGE_KEY, theme);
      this._syncMedia(theme);
      this._syncButtons(theme);
      SW.emit(document, 'sw:theme:change', { theme });
      return true;
    },
    setTheme(theme, persist = true) { return this.set(theme, persist); },
    toggle() {
      const current = document.documentElement.getAttribute('sw-theme') || 'dark';
      const next = current === 'light' ? 'dark' : 'light';
      return this.set(next, true);
    },

    _syncMedia(theme) {
      SW.$(`img[sw-day-img-${theme}]`).forEach((image) => {
        image.src = resolveVar(image.getAttribute(`sw-day-img-${theme}`));
      });
      SW.$(`[sw-day-bg-${theme}]`).forEach((element) => {
        const value = resolveVar(element.getAttribute(`sw-day-bg-${theme}`));
        if (value.trim().startsWith('url(')) element.style.backgroundImage = value;
        else element.style.background = value;
      });
      SW.$(`[sw-day-font-${theme}]`).forEach((element) => {
        element.style.fontFamily = resolveVar(element.getAttribute(`sw-day-font-${theme}`));
      });
    },

    _syncButtons(theme) {
      SW.$('[sw-day]').forEach((button) => {
        button.setAttribute('aria-pressed', theme === 'light' ? 'true' : 'false');
        button.setAttribute('title', theme === 'light' ? 'Modo escuro' : 'Modo claro');
        const icon = button.querySelector('.sw-daytg-ico');
        if (icon) icon.className = `sw-daytg-ico ${theme === 'light' ? 'swi-moon' : 'swi-sun'}`;
      });
      SW.$('[sw-day-set]').forEach((button) => {
        const isActive = button.getAttribute('sw-day-set') === theme;
        button.classList.toggle('is-act', isActive);
        button.setAttribute('aria-pressed', isActive ? 'true' : 'false');
      });
    },

    initAll(root = document) {
      const locked = lockedTheme();
      const theme = locked || document.documentElement.getAttribute('sw-theme') || 'dark';

      SW.$('[sw-day]', root).forEach((button) => {
        if (button._swDayBound) return;
        button._swDayBound = true;
        if (locked) { button.hidden = true; button.disabled = true; return; }
        if (button.tagName === 'BUTTON' && !button.hasAttribute('type')) button.setAttribute('type', 'button');
        if (!button.hasAttribute('aria-label') && !button.textContent.trim()) button.setAttribute('aria-label', 'Alternar Tema Claro/Escuro');
        if (!button.querySelector('.sw-daytg-ico') && !button.textContent.trim()) {
          const icon = document.createElement('i');
          icon.className = 'sw-daytg-ico';
          button.appendChild(icon);
        }
        button.addEventListener('click', () => this.toggle());
      });

      SW.$('[sw-day-set]', root).forEach((button) => {
        if (button._swDaySetBound) return;
        button._swDaySetBound = true;
        const target = button.getAttribute('sw-day-set');
        if (locked) { button.hidden = true; button.disabled = true; return; }
        if (button.tagName === 'BUTTON' && !button.hasAttribute('type')) button.setAttribute('type', 'button');
        button.addEventListener('click', () => this.set(target, true));
      });

      const hasRealButton = SW.$('[sw-day], [sw-day-set]').some((el) => !el.hasAttribute('sw-day-demo'));
      if (!locked && root === document && !fabInjected && !hasRealButton && !document.documentElement.hasAttribute('sw-day-no-fab')) {
        fabInjected = true;
        const fab = document.createElement('div');
        fab.className = 'sw-day-fab';
        fab.setAttribute('role', 'group');
        fab.setAttribute('aria-label', 'Alternar Tema Claro/Escuro (Ctrl+Shift+D)');
        fab.innerHTML =
          '<button type="button" sw-day-set="light" class="sw-day-fab-btn" aria-label="Tema claro" title="Tema claro"><i class="sw-day-fab-ico swi-sun"></i></button>' +
          '<button type="button" sw-day-set="dark" class="sw-day-fab-btn" aria-label="Tema escuro" title="Tema escuro"><i class="sw-day-fab-ico swi-moon"></i></button>';
        document.body.appendChild(fab);
        SW.$('[sw-day-set]', fab).forEach((button) => {
          button._swDaySetBound = true;
          const target = button.getAttribute('sw-day-set');
          button.addEventListener('click', () => this.set(target, true));
        });
      }

      this._syncMedia(theme);
      this._syncButtons(theme);

      if (!storageBound) {
        storageBound = true;
        window.addEventListener('storage', (event) => {
          if (event.key === STORAGE_KEY && event.newValue && allowedThemes.has(event.newValue)) {
            this.set(event.newValue, false);
          }
        });
      }
      if (!keyboardBound) {
        keyboardBound = true;
        document.addEventListener('keydown', (event) => {
          if (event.ctrlKey && event.shiftKey && (event.key === 'D' || event.key === 'd')) {
            event.preventDefault();
            this.toggle();
          }
        });
      }
    }
  };
  window.SW.Day = SWDay;
  SW.register('SWDay', SWDay);
})();

/**
 * Smooll v2 — núcleo didático inicial
 * API pública: data-sw + opções data-sw-*
 */
(function sanWebMotionFactory(window, document) {
  "use strict";

  const VERSION = "0.20.0-learning";
  const instances = [];
  const REVEALS = Object.freeze({
    "fade-up": { y: 64, opacity: 0 },
    "fade-down": { y: -64, opacity: 0 },
    "fade-left": { x: 64, opacity: 0 },
    "fade-right": { x: -64, opacity: 0 },
    "slide-up": { y: 96 },
    "slide-down": { y: -96 },
    "slide-left": { x: 96 },
    "slide-right": { x: -96 },
    "zoom-in": { scale: 0.82, opacity: 0 },
    "zoom-out": { scale: 1.18, opacity: 0 },
    "rotate-in": { rotation: -10, scale: 0.9, opacity: 0 },
    "flip-x": { rotationX: 72, opacity: 0 },
    "flip-y": { rotationY: 72, opacity: 0 },
    "reveal-left": { clipPath: "inset(0 100% 0 0)" },
    "reveal-right": { clipPath: "inset(0 0 0 100%)" },
    "reveal-up": { clipPath: "inset(100% 0 0 0)" },
    "reveal-center": { clipPath: "inset(0 50% 0 50%)" }
  });
  const STORY_EFFECTS = Object.freeze({
    slide: { enter: { autoAlpha: 0, y: 56 }, leave: { autoAlpha: 0, y: -56 } },
    fade: { enter: { autoAlpha: 0 }, leave: { autoAlpha: 0 } },
    zoom: { enter: { autoAlpha: 0, scale: 0.84 }, leave: { autoAlpha: 0, scale: 1.12 } },
    left: { enter: { autoAlpha: 0, x: 80 }, leave: { autoAlpha: 0, x: -80 } }
  });
  const STARTS = Object.freeze({ early: "top 94%", normal: "top 86%", late: "top 70%" });
  const EASES = Object.freeze({ soft: "power3.out", smooth: "power2.inOut", snap: "back.out(1.35)", linear: "none" });
  const SECTION_EFFECTS = Object.freeze({
    stack: { from: { yPercent: 100 }, to: { yPercent: 0 } },
    cover: { from: { yPercent: 100 }, to: { yPercent: 0 } },
    "slide-up": { from: { yPercent: 100 }, to: { yPercent: 0 }, exit: { yPercent: -100 } },
    "slide-down": { from: { yPercent: -100 }, to: { yPercent: 0 }, exit: { yPercent: 100 } },
    "slide-left": { from: { xPercent: 100 }, to: { xPercent: 0 }, exit: { xPercent: -100 } },
    "slide-right": { from: { xPercent: -100 }, to: { xPercent: 0 }, exit: { xPercent: 100 } },
    fade: { from: { autoAlpha: 0 }, to: { autoAlpha: 1 }, exit: { autoAlpha: 0 } },
    zoom: { from: { scale: 0.2, autoAlpha: 0 }, to: { scale: 1, autoAlpha: 1 }, exit: { scale: 1.35, autoAlpha: 0 } },
    scale: { from: { scale: 0.78, autoAlpha: 0 }, to: { scale: 1, autoAlpha: 1 }, exit: { scale: 1.16, autoAlpha: 0 } },
    mask: { from: { clipPath: "circle(0% at 50% 50%)" }, to: { clipPath: "circle(150% at 50% 50%)" } },
    skew: {
      from: { xPercent: 100, clipPath: "polygon(18% 0, 100% 0, 100% 100%, 0 100%)" },
      to: { xPercent: 0, clipPath: "polygon(0 0, 100% 0, 100% 100%, 0 100%)" },
      exit: { xPercent: -100, clipPath: "polygon(0 0, 82% 0, 100% 100%, 0 100%)" }
    },
    fold: {
      from: { clipPath: "inset(0 0 100% 0)", scaleY: 0.12, yPercent: -8, transformOrigin: "top center" },
      to: { clipPath: "inset(0 0 0% 0)", scaleY: 1, yPercent: 0, transformOrigin: "top center" }
    }
  });
  const PAGE_EFFECTS = new Set(["fade", "slide", "slide-left", "slide-right", "slide-up", "slide-down", "scale", "scale-up", "scale-down", "flip", "flip-x", "blur", "blur-in", "blur-out", "wipe", "curtain"]);

  const warn = (message, element) => {
    if (window.SWMotionConfig?.debug === false) return;
    console.warn(`[Smooll v2] ${message}`, element || "");
  };

  const normalizeEnd = (rawValue) => {
    const value = String(rawValue || "300%").trim();
    if (!/^\d+(?:\.\d+)?(?:%|px|vh)?$/.test(value)) {
      warn(`data-sw-end inválido: "${value}". Usando 300%.`);
      return "+=300%";
    }
    return `+=${value}`;
  };

  const parseEndRanges = (rawValue) => {
    if (!rawValue) return [];
    return rawValue.split("|").map((part) => {
      const match = part.trim().match(/^(\d+|\*)\s*-\s*(\d+|\*)\s*:\s*(\d+(?:\.\d+)?(?:%|px|vh)?)$/);
      if (!match) return null;
      const min = match[1] === "*" ? 0 : Number(match[1]);
      const max = match[2] === "*" ? Number.POSITIVE_INFINITY : Number(match[2]);
      if (min > max) return null;
      return { min, max, value: match[3], label: `${match[1]}-${match[2]}` };
    }).filter(Boolean);
  };

  const readResponsiveEnd = (element, fallback = "300%", width = window.innerWidth, updateState = true) => {
    const setState = (value, range) => {
      if (!updateState) return;
      element.setAttribute("data-sw-end-active", value);
      element.setAttribute("data-sw-end-range", range);
    };
    const rawRanges = element.getAttribute("data-sw-end-at");
    const ranges = parseEndRanges(rawRanges);
    if (rawRanges && !ranges.length) warn(`data-sw-end-at inválido: "${rawRanges}". Usando as faixas padrão.`, element);
    const matchingRange = ranges.find((range) => width >= range.min && width <= range.max);
    if (matchingRange) {
      setState(matchingRange.value, matchingRange.label);
      return matchingRange.value;
    }
    const attribute = width < 768
      ? "data-sw-end-sm"
      : window.innerWidth < 1200
        ? "data-sw-end-md"
        : "data-sw-end-lg";
    const value = element.getAttribute(attribute) || element.getAttribute("data-sw-end") || fallback;
    setState(value, attribute.slice(-2));
    return value;
  };

  const readResponsiveHeight = (element, fallback = "200%", width = window.innerWidth) => {
    const rawRanges = element.getAttribute("data-sw-h-at");
    const ranges = parseEndRanges(rawRanges);
    const matchingRange = ranges.find((range) => width >= range.min && width <= range.max);
    const value = matchingRange?.value || element.getAttribute("data-sw-h") || fallback;
    element.setAttribute("data-sw-h-active", value);
    element.setAttribute("data-sw-h-range", matchingRange?.label || "default");
    return value;
  };

  const sizeToPixels = (value, reference) => {
    const amount = Number.parseFloat(value);
    if (value.endsWith("%")) return reference * amount / 100;
    if (value.endsWith("vh")) return window.innerHeight * amount / 100;
    return amount;
  };

  const readScrub = (element) => {
    if (!element.hasAttribute("data-sw-scrub")) return false;
    const rawValue = element.getAttribute("data-sw-scrub");
    if (rawValue === "" || rawValue === "true") return true;
    const value = Number.parseFloat(rawValue);
    if (Number.isFinite(value) && value >= 0) return value;
    warn(`data-sw-scrub inválido: "${rawValue}". Usando true.`, element);
    return true;
  };

  const readMotionValue = (element, attribute, fallback) => {
    const rawValue = element.getAttribute(attribute);
    if (rawValue === null) return fallback;
    const value = rawValue.trim();
    if (/^-?\d+(?:\.\d+)?(?:px|%|vw|vh|rem|em)?$/.test(value)) return value;
    warn(`${attribute} inválido: "${value}". Usando ${fallback}.`, element);
    return fallback;
  };

  const readNumber = (element, attribute, fallback, min, max) => {
    const rawValue = element.getAttribute(attribute);
    if (rawValue === null) return fallback;
    const value = Number.parseFloat(rawValue);
    if (Number.isFinite(value) && value >= min && value <= max) return value;
    warn(`${attribute} inválido: "${rawValue}". Usando ${fallback}.`, element);
    return fallback;
  };

  const readChoice = (element, attribute, choices, fallback) => {
    const value = element.getAttribute(attribute) || fallback;
    if (Object.hasOwn(choices, value)) return choices[value];
    warn(`${attribute} inválido: "${value}". Usando ${fallback}.`, element);
    return choices[fallback];
  };

  const shouldReduceMotion = (element) => {
    const configured = window.SWMotionConfig?.reducedMotion;
    const preferred = typeof configured === "boolean"
      ? configured
      : window.matchMedia("(prefers-reduced-motion: reduce)").matches;
    return preferred && !element.hasAttribute("data-sw-force");
  };

  const prepareEffect = (element, effectName) => {
    if (effectName !== "flip-x" && effectName !== "flip-y") return;
    window.gsap.set(element, {
      transformOrigin: "50% 50%",
      backfaceVisibility: "hidden"
    });
    if (element.parentElement) {
      window.gsap.set(element.parentElement, { perspective: 1200 });
    }
  };

  const visibleEffectState = (effect) => ({
    ...(Object.hasOwn(effect, "x") ? { x: 0 } : {}),
    ...(Object.hasOwn(effect, "y") ? { y: 0 } : {}),
    ...(Object.hasOwn(effect, "scale") ? { scale: 1 } : {}),
    ...(Object.hasOwn(effect, "rotation") ? { rotation: 0 } : {}),
    ...(Object.hasOwn(effect, "rotationX") ? { rotationX: 0 } : {}),
    ...(Object.hasOwn(effect, "rotationY") ? { rotationY: 0 } : {}),
    ...(Object.hasOwn(effect, "opacity") ? { opacity: 1 } : {}),
    ...(Object.hasOwn(effect, "clipPath") ? { clipPath: "inset(0 0 0 0)" } : {})
  });

  const destroy = () => {
    while (instances.length) {
      const instance = instances.pop();
      instance.scrollTrigger?.kill();
      instance.kill();
    }
    document.querySelectorAll('[data-sw-ready]').forEach((element) => {
      element.removeAttribute("data-sw-ready");
    });
  };

  const createPin = (element) => {
    const { gsap, ScrollTrigger } = window;
    const reducedMotion = shouldReduceMotion(element);
    const movers = element.querySelectorAll('[data-sw="move"]');

    const timeline = gsap.timeline({
      scrollTrigger: {
        trigger: element,
        start: "top top",
        end: () => normalizeEnd(readResponsiveEnd(element)),
        pin: true,
        scrub: reducedMotion ? false : readScrub(element),
        anticipatePin: 1,
        invalidateOnRefresh: true,
        onUpdate: (self) => {
          element.dispatchEvent(new CustomEvent("sw:progress", {
            detail: { progress: self.progress }
          }));
        }
      }
    });

    movers.forEach((mover) => {
      const motion = { ease: "none" };

      if (mover.hasAttribute("data-sw-x")) {
        motion.x = reducedMotion ? 0 : readMotionValue(mover, "data-sw-x", "0");
      }
      if (mover.hasAttribute("data-sw-y")) {
        motion.y = reducedMotion ? 0 : readMotionValue(mover, "data-sw-y", "0");
      }
      if (mover.hasAttribute("data-sw-scale")) {
        motion.scale = reducedMotion ? 1 : readNumber(mover, "data-sw-scale", 1, 0.1, 5);
      }
      if (mover.hasAttribute("data-sw-rot")) {
        motion.rotation = reducedMotion ? 0 : readNumber(mover, "data-sw-rot", 0, -1080, 1080);
      }
      if (mover.hasAttribute("data-sw-opacity")) {
        motion.opacity = reducedMotion ? 1 : readNumber(mover, "data-sw-opacity", 1, 0, 1);
      }

      if (Object.keys(motion).length > 1) timeline.to(mover, motion, 0);
    });

    element.setAttribute("data-sw-ready", "pin");
    instances.push(timeline);
  };

  const createReveal = (element) => {
    if (element.hasAttribute("data-sw-manual")) return;
    const effectName = element.dataset.sw;
    const effect = REVEALS[effectName];
    if (!effect) return;

    const reducedMotion = shouldReduceMotion(element);

    if (reducedMotion) {
      element.setAttribute("data-sw-ready", effectName);
      return;
    }

    const duration = readNumber(element, "data-sw-dur", 0.8, 0.1, 10);
    const delay = readNumber(element, "data-sw-delay", 0, 0, 10);
    const once = element.hasAttribute("data-sw-once");
    prepareEffect(element, effectName);

    const scrub = readScrub(element);
    const scrollTrigger = scrub
      ? {
          trigger: element,
          start: readChoice(element, "data-sw-start", STARTS, "normal"),
          end: element.getAttribute("data-sw-end") || "bottom center",
          scrub,
          invalidateOnRefresh: true
        }
      : {
          trigger: element,
          start: readChoice(element, "data-sw-start", STARTS, "normal"),
          toggleActions: once ? "play none none none" : "play none none reverse"
        };

    const tween = window.gsap.from(element, {
      ...effect,
      duration,
      delay,
      ease: readChoice(element, "data-sw-ease", EASES, "soft"),
      scrollTrigger
    });

    element.setAttribute("data-sw-ready", effectName);
    instances.push(tween);
  };

  const play = (element) => {
    if (!(element instanceof window.Element)) return false;
    const effectName = element.dataset.sw;

    if (effectName === "cascade") {
      const children = element.querySelectorAll(":scope > [data-sw-item]");
      if (!children.length) {
        warn('data-sw="cascade" precisa de filhos com data-sw-item.', element);
        return false;
      }
      const fxName = element.dataset.swFx || "fade-up";
      const fx = REVEALS[fxName] || REVEALS["fade-up"];

      window.gsap.killTweensOf(children);
      window.gsap.set(children, { clearProps: "transform,opacity,visibility,clipPath" });
      children.forEach((child) => prepareEffect(child, fxName));
      const reducedMotion = shouldReduceMotion(element);
      element.setAttribute("data-sw-ready", "cascade");
      if (reducedMotion) return true;

      window.gsap.fromTo(children, { ...fx }, {
        ...visibleEffectState(fx),
        duration: readNumber(element, "data-sw-dur", 0.72, 0.1, 10),
        stagger: readNumber(element, "data-sw-gap", 0.12, 0.02, 2),
        ease: readChoice(element, "data-sw-ease", EASES, "soft"),
        overwrite: true,
        onComplete: () => window.gsap.set(children, { clearProps: "transform,opacity,visibility,clipPath" })
      });
      return true;
    }

    const effect = REVEALS[effectName];
    if (!effect) {
      warn(`Efeito manual inválido: "${effectName || ""}".`, element);
      return false;
    }

    window.gsap.killTweensOf(element);
    window.gsap.set(element, { clearProps: "transform,opacity,visibility,clipPath" });
    prepareEffect(element, effectName);
    const reducedMotion = shouldReduceMotion(element);
    element.setAttribute("data-sw-ready", effectName);
    if (reducedMotion) return true;

    window.gsap.from(element, {
      ...effect,
      duration: readNumber(element, "data-sw-dur", 0.8, 0.1, 10),
      delay: readNumber(element, "data-sw-delay", 0, 0, 10),
      ease: readChoice(element, "data-sw-ease", EASES, "soft"),
      overwrite: true
    });
    return true;
  };

  const createCascade = (element) => {
    if (element.dataset.sw !== "cascade") return;

    const children = element.querySelectorAll(":scope > [data-sw-item]");
    if (!children.length) {
      warn('data-sw="cascade" precisa de filhos com data-sw-item.', element);
      return;
    }

    const effectName = element.dataset.swFx || "fade-up";
    const effect = REVEALS[effectName];
    if (!effect) {
      warn(`data-sw-fx inválido: "${effectName}". Usando fade-up.`, element);
    }

    const reducedMotion = shouldReduceMotion(element);
    if (reducedMotion) {
      element.setAttribute("data-sw-ready", "cascade");
      return;
    }

    const scrub = readScrub(element);
    const scrollTrigger = scrub
      ? {
          trigger: element,
          start: readChoice(element, "data-sw-start", STARTS, "normal"),
          end: element.getAttribute("data-sw-end") || "bottom center",
          scrub,
          invalidateOnRefresh: true
        }
      : {
          trigger: element,
          start: readChoice(element, "data-sw-start", STARTS, "normal"),
          toggleActions: element.hasAttribute("data-sw-once")
            ? "play none none none"
            : "play none none reverse"
        };

    const initialEffect = effect || REVEALS["fade-up"];
    const tween = window.gsap.fromTo(children, { ...initialEffect }, {
      ...visibleEffectState(initialEffect),
      duration: readNumber(element, "data-sw-dur", 0.72, 0.1, 10),
      stagger: readNumber(element, "data-sw-gap", 0.12, 0.02, 2),
      ease: readChoice(element, "data-sw-ease", EASES, "soft"),
      scrollTrigger,
      onComplete: () => window.gsap.set(children, { clearProps: "transform,opacity,visibility,clipPath" })
    });

    children.forEach((child) => prepareEffect(child, effectName));

    element.setAttribute("data-sw-ready", "cascade");
    instances.push(tween);
  };

  const createParallax = (element) => {
    if (element.dataset.sw !== "parallax") return;

    const reducedMotion = shouldReduceMotion(element);
    if (reducedMotion) {
      element.setAttribute("data-sw-ready", "parallax");
      return;
    }

    const hasX = element.hasAttribute("data-sw-x") || element.hasAttribute("data-sw-from-x");
    const fromVars = { y: readMotionValue(element, "data-sw-from", "-10%") };
    const toVars = {
      y: readMotionValue(element, "data-sw-y", "10%"),
      ease: "none",
      scrollTrigger: {
        trigger: element.parentElement || element,
        scroller: element.closest(".sw-modal-box") || undefined,
        start: "top bottom",
        end: "bottom top",
        scrub: readScrub(element) || 0.6,
        invalidateOnRefresh: true
      }
    };
    if (hasX) {
      fromVars.x = readMotionValue(element, "data-sw-from-x", "-10%");
      toVars.x = readMotionValue(element, "data-sw-x", "10%");
    }

    const tween = window.gsap.fromTo(element, fromVars, toVars);

    element.setAttribute("data-sw-ready", "parallax");
    instances.push(tween);
  };

  const createFixed = (element) => {
    if (element.dataset.sw !== "fixed") return;
    element.setAttribute("data-sw-ready", "fixed");
  };

  const createWindow = (element) => {
    if (element.dataset.sw !== "window") return;

    const image = element.querySelector(":scope > img");
    if (!image) {
      warn('data-sw="window" precisa de uma imagem filha direta.', element);
      return;
    }

    const reducedMotion = shouldReduceMotion(element);

    const positionImage = () => {
      if (reducedMotion) {
        window.gsap.set(image, { xPercent: -50, yPercent: -50, y: element.offsetHeight / 2 });
        return;
      }
      const rect = element.getBoundingClientRect();
      window.gsap.set(image, {
        xPercent: -50,
        yPercent: -50,
        y: (window.innerHeight / 2) - rect.top
      });
    };

    const trigger = window.ScrollTrigger.create({
      trigger: element,
      scroller: element.closest(".sw-modal-box") || undefined,
      start: "top bottom",
      end: "bottom top",
      onUpdate: positionImage,
      onRefresh: positionImage
    });

    positionImage();
    element.setAttribute("data-sw-ready", "window");
    instances.push(trigger);
  };

  const createStory = (element) => {
    if (element.dataset.sw !== "story") return;

    const steps = Array.from(element.querySelectorAll("[data-sw-step]"));
    if (steps.length < 2) {
      warn('data-sw="story" precisa de pelo menos dois data-sw-step.', element);
      return;
    }

    const reducedMotion = shouldReduceMotion(element);
    if (reducedMotion) {
      element.setAttribute("data-sw-ready", "story");
      return;
    }

    const effectName = element.dataset.swFx || "slide";
    const effect = STORY_EFFECTS[effectName] || STORY_EFFECTS.slide;
    if (!STORY_EFFECTS[effectName]) {
      warn(`data-sw-fx inválido para story: "${effectName}". Usando slide.`, element);
    }

    window.gsap.set(steps, { clearProps: "transform,opacity,visibility" });
    window.gsap.set(steps, { ...effect.enter });
    window.gsap.set(steps[0], { autoAlpha: 1, x: 0, y: 0, scale: 1 });

    const timeline = window.gsap.timeline({
      scrollTrigger: {
        trigger: element,
        start: "top top",
        end: () => normalizeEnd(readResponsiveEnd(element, `${steps.length * 100}%`)),
        pin: true,
        scrub: readScrub(element) || 0.8,
        anticipatePin: 1,
        invalidateOnRefresh: true,
        onUpdate: (self) => {
          element.style.setProperty("--sw-progress", self.progress);
          const activeIndex = Math.min(steps.length - 1, Math.round(self.progress * (steps.length - 1)));
          element.dataset.swActive = String(activeIndex + 1);
          element.dispatchEvent(new CustomEvent("sw:progress", {
            detail: { progress: self.progress }
          }));
        }
      }
    });

    timeline.to({}, { duration: 0.45 });
    steps.slice(1).forEach((step, index) => {
      timeline
        .to(steps[index], { ...effect.leave, duration: 0.4 })
        .fromTo(step, { ...effect.enter }, { autoAlpha: 1, x: 0, y: 0, scale: 1, duration: 0.55 }, "<0.1")
        .to({}, { duration: 0.45 });
    });

    element.dataset.swActive = "1";
    element.setAttribute("data-sw-ready", "story");
    instances.push(timeline);
  };

  const createTrack = (element) => {
    if (element.dataset.sw !== "track") return;
    const rail = element.querySelector(":scope > [data-sw-track]");
    const sections = rail ? Array.from(rail.querySelectorAll(":scope > [data-sw-section]")) : [];
    if (!rail || sections.length < 2) {
      warn('data-sw="track" precisa de data-sw-track com pelo menos duas data-sw-section.', element);
      return;
    }
    if (shouldReduceMotion(element)) {
      element.setAttribute("data-sw-ready", "track");
      return;
    }

    window.gsap.set(rail, { x: 0 });
    const tween = window.gsap.to(rail, {
      x: () => -Math.max(0, rail.scrollWidth - element.clientWidth),
      ease: "none",
      scrollTrigger: {
        trigger: element,
        start: "top top",
        end: () => normalizeEnd(readResponsiveEnd(element, `${sections.length * 100}%`)),
        pin: true,
        scrub: readScrub(element) || 0.8,
        anticipatePin: 1,
        invalidateOnRefresh: true,
        onUpdate: (self) => {
          element.style.setProperty("--sw-progress", self.progress);
          element.dataset.swActive = String(Math.min(sections.length, Math.floor(self.progress * sections.length) + 1));
          element.dispatchEvent(new CustomEvent("sw:progress", { detail: { progress: self.progress } }));
        }
      }
    });
    element.dataset.swActive = "1";
    element.setAttribute("data-sw-ready", "track");
    instances.push(tween);
  };

  const createFrames = (element) => {
    if (element.dataset.sw !== "frames") return;
    const stage = element.querySelector(":scope > [data-sw-frame-stage]");
    if (!stage) {
      warn('data-sw="frames" precisa de um palco filho data-sw-frame-stage.', element);
      return;
    }

    const spriteGrid = element.dataset.swFrameSprite;
    const sprite = spriteGrid ? spriteGrid.match(/^(\d+)x(\d+)$/) : null;
    if (spriteGrid && !sprite) {
      warn(`data-sw-frame-sprite inválido: "${spriteGrid}". Use o formato "colunasxlinhas".`, element);
    }

    let frames = [];
    let total = 0;
    let cols = 0;
    let rows = 0;

    if (sprite) {
      cols = Number(sprite[1]);
      rows = Number(sprite[2]);
      total = readNumber(element, "data-sw-frame-count", cols * rows, 2, cols * rows);
      stage.style.backgroundSize = `${cols * 100}% ${rows * 100}%`;
      stage.style.backgroundRepeat = "no-repeat";
    } else {
      frames = Array.from(element.querySelectorAll("[data-sw-frame]"));
      total = frames.length;
      if (total < 2) {
        warn('data-sw="frames" precisa de pelo menos duas imagens data-sw-frame ou de data-sw-frame-sprite.', element);
        return;
      }
    }

    let activeIndex = -1;
    const showFrame = (index) => {
      const next = Math.max(0, Math.min(total - 1, index));
      if (next === activeIndex) return;
      if (sprite) {
        const col = next % cols;
        const row = Math.floor(next / cols);
        stage.style.backgroundPosition = `${cols > 1 ? (col / (cols - 1)) * 100 : 0}% ${rows > 1 ? (row / (rows - 1)) * 100 : 0}%`;
      } else {
        if (activeIndex >= 0) frames[activeIndex].classList.remove("is-active");
        frames[next].classList.add("is-active");
      }
      activeIndex = next;
      element.dataset.swFrameActive = String(next + 1);
      element.style.setProperty("--sw-frame-progress", next / (total - 1));
      element.dispatchEvent(new CustomEvent("sw:frame", { detail: { index: next, total } }));
    };
    showFrame(0);
    if (shouldReduceMotion(element)) {
      element.style.height = "100svh";
      element.setAttribute("data-sw-ready", "frames-static");
      return;
    }
    const setScrollHeight = () => {
      const distance = readResponsiveEnd(element, `${Math.max(3, Math.ceil(total * 0.35)) * 100}%`);
      const amount = Number.parseFloat(distance);
      element.style.height = distance.endsWith("px")
        ? `calc(100svh + ${amount}px)`
        : `${100 + amount}svh`;
    };
    setScrollHeight();
    const trigger = window.ScrollTrigger.create({
      trigger: element,
      start: "top top",
      end: "bottom bottom",
      invalidateOnRefresh: true,
      onRefreshInit: setScrollHeight,
      onUpdate: (self) => showFrame(Math.round(self.progress * (total - 1)))
    });
    element.setAttribute("data-sw-ready", "frames");
    instances.push(trigger);
  };

  const createSections = (element) => {
    if (element.dataset.sw !== "sections") return;
    const sections = Array.from(element.querySelectorAll(":scope > [data-sw-section]"));
    if (sections.length < 2) {
      warn('data-sw="sections" precisa de pelo menos duas sections filhas.', element);
      return;
    }

    const reducedMotion = shouldReduceMotion(element);
    const rawEffects = (element.dataset.swFx || "cover").split("|").map((value) => value.trim()).filter(Boolean);
    const effects = rawEffects.length ? rawEffects : ["cover"];
    const effectFor = (index) => {
      const name = effects[Math.min(index, effects.length - 1)];
      if (!SECTION_EFFECTS[name]) warn(`Efeito de section inválido: "${name}". Usando cover.`, element);
      return { name: SECTION_EFFECTS[name] ? name : "cover", props: SECTION_EFFECTS[name] || SECTION_EFFECTS.cover };
    };
    const requestedBase = effects[0];
    const horizontal = requestedBase === "h-scroll" || requestedBase === "h-scroll-right";
    const baseName = horizontal ? requestedBase : effectFor(0).name;

    const mobileFlow = element.dataset.swMobile === "flow" && window.matchMedia("(max-width: 767px)").matches;
    const contentFlow = sections.some((section) => section.dataset.swSize === "flow");
    if (mobileFlow || contentFlow) {
      element.setAttribute("data-sw-layout", "flow");
      element.setAttribute("data-sw-ready", "sections-flow");
      return;
    }

    if (reducedMotion) {
      element.setAttribute("data-sw-ready", "sections-static");
      return;
    }

    if (horizontal) {
      const reverse = baseName === "h-scroll-right";
      element.setAttribute("data-sw-layout", "horizontal");
      window.gsap.set(sections, { xPercent: reverse ? -100 * (sections.length - 1) : 0 });
      const horizontalTween = window.gsap.to(sections, {
        xPercent: reverse ? 0 : -100 * (sections.length - 1),
        ease: "none",
        scrollTrigger: {
          trigger: element,
          start: "top top",
          end: `+=${sections.length * 100}%`,
          scrub: readScrub(element) || 0.7,
          pin: true,
          anticipatePin: 0,
          invalidateOnRefresh: true
        }
      });
      instances.push(horizontalTween);
      element.setAttribute("data-sw-ready", "sections");
      return;
    }

    element.setAttribute("data-sw-layout", "stack");
    sections.forEach((section, index) => window.gsap.set(section, {
      position: "absolute",
      inset: 0,
      width: "100%",
      zIndex: index + 1
    }));

    let timeline;
    timeline = window.gsap.timeline({
      scrollTrigger: {
        trigger: element,
        start: "top top",
        end: () => `+=${Math.max(1, timeline.duration()) * 100}%`,
        scrub: readScrub(element) || 0.7,
        pin: true,
        anticipatePin: 0,
        invalidateOnRefresh: true
      }
    });

    timeline.to({}, { duration: 0.2 });
    const addSectionScroll = (section) => {
      const content = section.querySelector(":scope > [data-sw-scroll]");
      if (!content) return;
      const heightValue = readResponsiveHeight(section);
      const configuredHeight = sizeToPixels(heightValue, element.clientHeight);
      content.style.minHeight = `${configuredHeight}px`;
      const travel = () => -Math.max(0, content.scrollHeight - section.clientHeight);
      const screens = Math.max(1, configuredHeight / Math.max(1, element.clientHeight));
      timeline.to(content, { y: travel, duration: screens, ease: "none" });
    };
    sections.slice(1).forEach((section, offset) => {
      const index = offset + 1;
      const { props } = effectFor(index);
      const previous = sections[index - 1];
      addSectionScroll(previous);
      timeline.fromTo(section, { ...props.from }, { ...props.to, duration: 1, ease: "none", immediateRender: true });
      if (baseName !== "stack" && props.exit) {
        timeline.fromTo(previous, { ...props.to }, { ...props.exit, duration: 1, ease: "none", immediateRender: false }, "<");
      }
    });
    addSectionScroll(sections[sections.length - 1]);
    timeline.to({}, { duration: 0.2 });
    instances.push(timeline);

    element.setAttribute("data-sw-ready", "sections");
  };

  const createSvgDraw = (element) => {
    if (element.dataset.sw !== "svg-draw") return;
    if (typeof element.getTotalLength !== "function") {
      warn('data-sw="svg-draw" precisa estar num elemento <path> (ou outro com getTotalLength).', element);
      return;
    }
    const length = element.getTotalLength();
    window.gsap.set(element, { strokeDasharray: length, strokeDashoffset: length });

    if (shouldReduceMotion(element)) {
      window.gsap.set(element, { strokeDashoffset: 0 });
      element.setAttribute("data-sw-ready", "svg-draw");
      return;
    }

    const tween = window.gsap.to(element, {
      strokeDashoffset: 0,
      ease: "none",
      scrollTrigger: {
        trigger: element,
        start: "top 85%",
        scrub: readScrub(element) || false,
        toggleActions: readScrub(element) ? undefined : "play none none reverse"
      }
    });
    element.setAttribute("data-sw-ready", "svg-draw");
    instances.push(tween);
  };

  const createMotionPath = (element) => {
    if (element.dataset.sw !== "motion-path") return;
    const curveHost = (element.parentElement && element.parentElement.querySelector("[data-sw-path]"))
      || document.querySelector("[data-sw-path]");
    const curve = curveHost && curveHost.querySelector("path");
    if (!curve) {
      warn('data-sw="motion-path" precisa de um elemento data-sw-path com um <path> dentro, próximo do objeto.', element);
      return;
    }

    const play = element.dataset.swPlay === "auto" ? "auto" : "scroll";
    const repeat = element.dataset.swRepeat === "true";
    const motionPath = { path: curve, align: curve, alignOrigin: [0.5, 0.5] };

    if (shouldReduceMotion(element)) {
      window.gsap.set(element, { motionPath });
      element.setAttribute("data-sw-ready", "motion-path");
      return;
    }

    if (play === "scroll") {
      const tween = window.gsap.to(element, {
        motionPath,
        ease: "none",
        scrollTrigger: {
          trigger: curveHost,
          start: "top bottom",
          end: "bottom top",
          scrub: readScrub(element) || 0.6
        }
      });
      instances.push(tween);
    } else {
      const tween = window.gsap.to(element, {
        motionPath,
        duration: readNumber(element, "data-sw-duration", 2.4, 0.3, 20),
        ease: "power1.inOut",
        repeat: repeat ? -1 : 0,
        yoyo: repeat,
        scrollTrigger: { trigger: curveHost, start: "top 85%", toggleActions: "play none none none" }
      });
      instances.push(tween);
    }
    element.setAttribute("data-sw-ready", "motion-path");
  };

  const createCursor = (element) => {
    if (element.dataset.sw !== "cursor") return;
    if (window.matchMedia && !window.matchMedia("(pointer: fine)").matches) return;
    if (shouldReduceMotion(element)) {
      element.setAttribute("data-sw-ready", "cursor");
      return;
    }

    const mode = element.dataset.swCursor && ["dot", "ring", "image"].includes(element.dataset.swCursor)
      ? element.dataset.swCursor
      : "dot";
    const imgSrc = element.getAttribute("data-sw-cursor-img") || "";
    const size = readNumber(element, "data-sw-cursor-size", 10, 2, 200);
    const color = element.getAttribute("data-sw-cursor-color") || "#fff";
    const mix = element.getAttribute("data-sw-cursor-mix") || "difference";
    const lerp = readNumber(element, "data-sw-cursor-lerp", 0.16, 0.02, 1);
    const hideNative = element.hasAttribute("data-sw-cursor-hide-native");

    const cursor = document.createElement("div");
    cursor.className = "sw-cursor";
    cursor.setAttribute("aria-hidden", "true");
    cursor.style.setProperty("--sw-cursor-size", `${size}px`);
    cursor.style.setProperty("--sw-cursor-hover-size", `${size * 3.6}px`);
    cursor.style.mixBlendMode = imgSrc ? "normal" : mix;
    if (imgSrc || mode === "image") {
      Object.assign(cursor.style, {
        backgroundImage: `url(${imgSrc})`,
        backgroundSize: "contain",
        backgroundRepeat: "no-repeat",
        backgroundPosition: "center",
        backgroundColor: "transparent",
        borderRadius: "0"
      });
    } else if (mode === "ring") {
      Object.assign(cursor.style, { backgroundColor: "transparent", border: `2px solid ${color}` });
    } else {
      cursor.style.backgroundColor = color;
    }
    document.body.append(cursor);
    if (hideNative) document.body.classList.add("sw-cursor-hide-native");

    let tx = window.innerWidth / 2;
    let ty = window.innerHeight / 2;
    let cx = tx;
    let cy = ty;
    let running = true;
    const onMove = (event) => { tx = event.clientX; ty = event.clientY; };
    document.addEventListener("pointermove", onMove);

    (function loop() {
      if (!running) return;
      cx += (tx - cx) * lerp;
      cy += (ty - cy) * lerp;
      cursor.style.transform = `translate(${cx}px, ${cy}px)`;
      window.requestAnimationFrame(loop);
    })();

    const hoverSelector = 'a, button, [data-sw="magnetic"], [data-sw-cursor-hover]';
    const onEnter = (event) => { if (event.target.closest(hoverSelector)) cursor.classList.add("is-hover"); };
    const onLeave = (event) => { if (event.target.closest(hoverSelector)) cursor.classList.remove("is-hover"); };
    document.body.addEventListener("pointerenter", onEnter, true);
    document.body.addEventListener("pointerleave", onLeave, true);

    element.setAttribute("data-sw-ready", "cursor");
  };

  const createMagnetic = (element) => {
    if (element.dataset.sw !== "magnetic") return;
    if (shouldReduceMotion(element)) {
      element.setAttribute("data-sw-ready", "magnetic");
      return;
    }
    const strength = readNumber(element, "data-sw-strength", 0.35, 0, 1);
    const xTo = window.gsap.quickTo(element, "x", { duration: 0.4, ease: "power3.out" });
    const yTo = window.gsap.quickTo(element, "y", { duration: 0.4, ease: "power3.out" });
    const release = () => {
      window.gsap.to(element, { x: 0, y: 0, duration: 0.7, ease: "elastic.out(1, 0.4)", overwrite: "auto" });
    };
    const onMove = (event) => {
      const rect = element.getBoundingClientRect();
      const cx = rect.left + rect.width / 2;
      const cy = rect.top + rect.height / 2;
      const dx = event.clientX - cx;
      const dy = event.clientY - cy;
      const dist = Math.sqrt(dx * dx + dy * dy) || 1;
      const maxDist = Math.max(rect.width, rect.height) * 1.6;
      if (dist < maxDist) {
        const pull = (1 - dist / maxDist) * Math.min(rect.width, rect.height) * strength;
        xTo((dx / dist) * pull);
        yTo((dy / dist) * pull);
      } else {
        release();
      }
    };
    window.addEventListener("pointermove", onMove);
    element.addEventListener("pointerleave", release);
    element.setAttribute("data-sw-ready", "magnetic");
  };

  const createTilt = (element) => {
    if (element.dataset.sw !== "tilt") return;
    if (shouldReduceMotion(element)) {
      element.setAttribute("data-sw-ready", "tilt");
      return;
    }
    const max = readNumber(element, "data-sw-max", 15, 1, 45);
    window.gsap.set(element, { transformPerspective: 600 });
    const rotateXTo = window.gsap.quickTo(element, "rotationX", { duration: 0.4, ease: "power3.out" });
    const rotateYTo = window.gsap.quickTo(element, "rotationY", { duration: 0.4, ease: "power3.out" });
    element.addEventListener("pointermove", (event) => {
      const rect = element.getBoundingClientRect();
      const cx = rect.left + rect.width / 2;
      const cy = rect.top + rect.height / 2;
      rotateXTo(((event.clientY - cy) / (rect.height / 2)) * -max);
      rotateYTo(((event.clientX - cx) / (rect.width / 2)) * max);
    });
    element.addEventListener("pointerleave", () => {
      window.gsap.to(element, { rotationX: 0, rotationY: 0, duration: 0.7, ease: "elastic.out(1, 0.4)", overwrite: "auto" });
    });
    element.setAttribute("data-sw-ready", "tilt");
  };

  const createMarquee = (element) => {
    if (element.dataset.sw !== "marquee") return;
    const origin = element.firstElementChild;
    if (!origin) {
      warn('data-sw="marquee" precisa de algum conteúdo dentro.', element);
      return;
    }
    if (shouldReduceMotion(element)) {
      element.setAttribute("data-sw-ready", "marquee-static");
      return;
    }

    const speed = readNumber(element, "data-sw-speed", 60, 5, 500);
    const reverse = element.dataset.swDirection === "right";
    const rail = document.createElement("div");
    rail.className = "sw-marquee-rail";
    element.classList.add("sw-marquee-viewport");
    element.append(rail);

    const init = () => {
      const item = origin.cloneNode(true);
      item.setAttribute("aria-hidden", "true");
      rail.append(item);
      const itemWidth = item.offsetWidth;
      if (!itemWidth) {
        rail.removeChild(item);
        window.requestAnimationFrame(init);
        return;
      }
      const copies = Math.ceil((window.innerWidth * 2) / itemWidth) + 2;
      for (let i = 1; i < copies; i++) {
        const clone = origin.cloneNode(true);
        clone.setAttribute("aria-hidden", "true");
        rail.append(clone);
      }
      origin.style.display = "none";

      let pos = reverse ? -(copies - 1) * itemWidth : 0;
      let paused = false;
      element.addEventListener("pointerenter", () => { paused = true; });
      element.addEventListener("pointerleave", () => { paused = false; });
      const tick = () => {
        if (paused) return;
        pos += reverse ? speed / 60 : -(speed / 60);
        if (!reverse && pos <= -itemWidth) {
          pos += itemWidth;
          rail.append(rail.firstElementChild);
        } else if (reverse && pos >= 0) {
          pos -= itemWidth;
          rail.insertBefore(rail.lastElementChild, rail.firstElementChild);
        }
        window.gsap.set(rail, { x: pos });
      };
      window.gsap.ticker.add(tick);
      instances.push({ kill: () => window.gsap.ticker.remove(tick) });
    };

    if (document.readyState === "complete") {
      window.requestAnimationFrame(init);
    } else {
      window.addEventListener("load", () => window.requestAnimationFrame(init), { once: true });
    }
    element.setAttribute("data-sw-ready", "marquee");
  };

  const createCountdown = (element) => {
    if (element.dataset.sw !== "countdown") return;
    const targetRaw = element.getAttribute("data-sw-target");
    if (!targetRaw) {
      warn('data-sw="countdown" precisa de data-sw-target com uma data.', element);
      return;
    }
    const target = new Date(targetRaw).getTime();
    if (!Number.isFinite(target)) {
      warn(`data-sw-target inválido: "${targetRaw}".`, element);
      return;
    }
    const format = element.getAttribute("data-sw-format") || "dhms";
    const separator = element.getAttribute("data-sw-separator") || ":";
    const labels = (element.getAttribute("data-sw-labels") || "d,h,m,s").split(",");
    const completeText = element.getAttribute("data-sw-complete");
    const pad = (n) => String(n).padStart(2, "0");

    let intervalId;
    const update = () => {
      const diff = Math.max(0, target - Date.now());
      const days = Math.floor(diff / 86400000);
      const hours = Math.floor((diff % 86400000) / 3600000);
      const minutes = Math.floor((diff % 3600000) / 60000);
      const seconds = Math.floor((diff % 60000) / 1000);
      let display = "";
      if (format.includes("d") && days > 0) {
        display += `${days}${labels[0] || "d"}`;
        if (format.includes("h") || format.includes("m") || format.includes("s")) display += separator;
      }
      if (format.includes("h")) {
        display += `${pad(hours)}${labels[1] || "h"}`;
        if (format.includes("m") || format.includes("s")) display += separator;
      }
      if (format.includes("m")) {
        display += `${pad(minutes)}${labels[2] || "m"}`;
        if (format.includes("s")) display += separator;
      }
      if (format.includes("s")) display += `${pad(seconds)}${labels[3] || "s"}`;
      element.textContent = display || "00s";
      if (diff <= 0) {
        element.setAttribute("data-sw-done", "");
        if (completeText) element.textContent = completeText;
        clearInterval(intervalId);
      }
    };

    update();
    intervalId = setInterval(update, 1000);
    element.setAttribute("data-sw-ready", "countdown");
    instances.push({ kill: () => clearInterval(intervalId) });
  };

  const createGlow = (element) => {
    if (element.dataset.sw !== "glow") return;
    const intensity = readNumber(element, "data-sw-intensity", 20, 0, 200);
    const color = element.getAttribute("data-sw-color") || "currentColor";
    const spread = readNumber(element, "data-sw-spread", 0, 0, 200);

    element.style.textShadow = `0 0 ${intensity}px ${color}, 0 0 ${intensity * 2}px ${color}`;
    if (spread > 0) element.style.boxShadow = `0 0 ${spread}px ${color}`;

    if (element.hasAttribute("data-sw-pulse") && !shouldReduceMotion(element)) {
      const tween = window.gsap.to(element, {
        textShadow: `0 0 ${intensity * 1.5}px ${color}, 0 0 ${intensity * 3}px ${color}`,
        duration: 1.5,
        repeat: -1,
        yoyo: true,
        ease: "sine.inOut"
      });
      instances.push(tween);
    }

    element.setAttribute("data-sw-ready", "glow");
  };

  const createRotate = (element) => {
    if (element.dataset.sw !== "rotate") return;
    if (shouldReduceMotion(element)) {
      element.setAttribute("data-sw-ready", "rotate");
      return;
    }
    const rotation = readNumber(element, "data-sw-rotation", 360, -3600, 3600);
    const start = element.getAttribute("data-sw-start") || "top bottom";
    const end = element.getAttribute("data-sw-end") || "bottom top";
    const tween = window.gsap.fromTo(element, { rotation: 0 }, {
      rotation,
      ease: "none",
      scrollTrigger: {
        trigger: element,
        start,
        end,
        scrub: readScrub(element) || 0.6,
        invalidateOnRefresh: true
      }
    });
    element.setAttribute("data-sw-ready", "rotate");
    instances.push(tween);
  };

  const createScale = (element) => {
    if (element.dataset.sw !== "scale") return;
    if (shouldReduceMotion(element)) {
      element.setAttribute("data-sw-ready", "scale");
      return;
    }
    const from = readNumber(element, "data-sw-from", 0.5, 0, 5);
    const to = readNumber(element, "data-sw-to", 1, 0, 5);
    const start = element.getAttribute("data-sw-start") || "top bottom";
    const end = element.getAttribute("data-sw-end") || "bottom top";
    const tween = window.gsap.fromTo(element, { scale: from }, {
      scale: to,
      ease: "none",
      scrollTrigger: {
        trigger: element,
        start,
        end,
        scrub: readScrub(element) || 0.6,
        invalidateOnRefresh: true
      }
    });
    element.setAttribute("data-sw-ready", "scale");
    instances.push(tween);
  };

  // ── Texto: split/scramble/type — motor próprio via scroll, sem GSAP/ScrollTrigger ──
  const textItems = [];
  let textEngineStarted = false;
  let textUpdateQueued = false;

  const clampProgress = (value) => Math.min(1, Math.max(0, value));

  const textElementProgress = (element) => {
    const rect = element.getBoundingClientRect();
    const viewport = window.innerHeight;
    return clampProgress((viewport * 0.88 - rect.top) / Math.max(viewport * 0.52, rect.height * 0.78));
  };

  const textUpdate = () => {
    textUpdateQueued = false;
    textItems.forEach((item) => {
      const progress = shouldReduceMotion(item.element) ? 1 : textElementProgress(item.element);
      item.render(progress);
    });
  };

  const requestTextUpdate = () => {
    if (textUpdateQueued) return;
    textUpdateQueued = true;
    window.requestAnimationFrame(textUpdate);
  };

  const startTextEngine = () => {
    if (textEngineStarted) return;
    textEngineStarted = true;
    window.addEventListener("scroll", requestTextUpdate, { passive: true });
    window.addEventListener("resize", requestTextUpdate, { passive: true });
    requestTextUpdate();
  };

  const registerTextItem = (element, render) => {
    const item = { element, render };
    textItems.push(item);
    instances.push({
      kill: () => {
        const index = textItems.indexOf(item);
        if (index > -1) textItems.splice(index, 1);
      }
    });
    startTextEngine();
  };

  // ── data-sw-play="auto"/"once": dispara sozinho ao entrar na tela, sem depender da posição contínua do scroll ──
  let textAutoObserver;

  const cancelTextAuto = (item) => {
    if (item.frame) window.cancelAnimationFrame(item.frame);
    item.frame = 0;
  };

  const playTextAuto = (item) => {
    if (item.play === "once" && item.played) return;
    cancelTextAuto(item);
    item.played = true;
    let startedAt;
    const tick = (now) => {
      if (startedAt === undefined) startedAt = now;
      const linear = clampProgress((now - startedAt) / 900);
      const progress = 1 - Math.pow(1 - linear, 3);
      item.render(progress);
      if (linear < 1) item.frame = window.requestAnimationFrame(tick);
      else item.frame = 0;
    };
    item.frame = window.requestAnimationFrame(tick);
  };

  const getTextAutoObserver = () => {
    if (textAutoObserver) return textAutoObserver;
    textAutoObserver = new window.IntersectionObserver((entries) => {
      entries.forEach((entry) => {
        const item = entry.target._swTextItem;
        if (!item) return;
        if (entry.isIntersecting && entry.intersectionRatio >= 0.2) {
          playTextAuto(item);
        } else if (!entry.isIntersecting && item.play === "auto") {
          cancelTextAuto(item);
          item.played = false;
          item.render(0);
        }
      });
    }, { threshold: [0, 0.2, 0.6] });
    return textAutoObserver;
  };

  const registerTextAutoItem = (element, render, play) => {
    const item = { element, render, play, played: false, frame: 0 };
    element._swTextItem = item;
    getTextAutoObserver().observe(element);
    instances.push({
      kill: () => {
        cancelTextAuto(item);
        textAutoObserver?.unobserve(element);
      }
    });
  };

  const registerTextByPlayMode = (element, render) => {
    const play = readChoice(element, "data-sw-play", { scroll: "scroll", auto: "auto", once: "once" }, "scroll");
    if (play === "scroll") registerTextItem(element, render);
    else registerTextAutoItem(element, render, play);
  };

  const textStaggerProgress = (progress, index, count, spread) =>
    clampProgress((progress - (index / Math.max(1, count - 1)) * spread) / (1 - spread));

  const textEaseOutBack = (progress) => {
    const c1 = 1.70158;
    const c3 = c1 + 1;
    return 1 + c3 * Math.pow(progress - 1, 3) + c1 * Math.pow(progress - 1, 2);
  };

  const renderSplitUnits = (units, effect, progress) => {
    const spread = effect === "flash" ? 0.25 : effect === "line-stack" ? 0.5 : 0.58;
    units.forEach((unit, index) => {
      const local = textStaggerProgress(progress, index, units.length, spread);
      const back = textEaseOutBack(local);
      let x = 0, y = 0, scale = 1, rotate = 0, skew = 0, blur = 0;
      if (["fade-up", "blur-up", "line-reveal", "line-body", "line-stack", "flash"].includes(effect)) {
        y = (1 - local) * (effect.startsWith("line") ? 115 : 48);
      }
      if (effect === "blur-in" || effect === "blur-up") blur = (1 - local) * 14;
      if (effect === "zoom-in") scale = 0.18 + back * 0.82;
      if (effect === "zoom-out") scale = 2.3 - local * 1.3;
      if (effect === "fade-right") x = (1 - local) * -58;
      if (effect === "fade-left") x = (1 - local) * 58;
      if (effect === "fade-down") y = (1 - local) * -58;
      if (effect === "rotate-in") rotate = (1 - back) * -55;
      if (effect === "rotate-in-cw") rotate = (1 - back) * 70;
      if (effect === "elastic-up") y = (1 - back) * 82;
      if (effect === "skew-left") { x = (1 - local) * -34; skew = (1 - local) * -24; }
      if (effect === "skew-right") { x = (1 - local) * 34; skew = (1 - local) * 24; }
      if (effect === "wave") y = (1 - local) * 34 + Math.sin(progress * Math.PI * 2 - index * 0.72) * Math.sin(progress * Math.PI) * 18;
      if (effect === "pulse") scale = 0.7 + local * 0.3 + Math.sin(progress * Math.PI * 2 - index * 0.8) * Math.sin(progress * Math.PI) * 0.16;
      unit.style.opacity = String(local);
      unit.style.filter = blur ? `blur(${blur}px)` : "";
      unit.style.transform = `translate3d(${x}px,${y}px,0) rotate(${rotate}deg) skewX(${skew}deg) scale(${scale})`;
    });
  };

  const splitTextIntoUnits = (element, mode) => {
    let pieces;
    if (mode === "lines") {
      pieces = element.innerHTML.split(/<br\s*\/?>/i).map((chunk) => {
        const temp = document.createElement("div");
        temp.innerHTML = chunk;
        return temp.textContent.trim();
      }).filter((line) => line.length);
    } else {
      const text = element.textContent;
      pieces = mode === "words" ? text.split(/(\s+)/) : [...text];
    }
    const fullText = mode === "lines" ? pieces.join(" ") : element.textContent;
    element.setAttribute("aria-label", fullText);
    element.textContent = "";

    const units = [];
    pieces.forEach((piece) => {
      if (mode !== "lines" && /^\s+$/.test(piece)) {
        element.append(document.createTextNode(piece));
        return;
      }
      const clip = document.createElement("span");
      clip.className = `sw-text-${mode === "lines" ? "line" : mode === "words" ? "word" : "char"}-clip`;
      const unit = document.createElement("span");
      unit.className = "sw-text-unit";
      unit.setAttribute("aria-hidden", "true");
      unit.textContent = piece;
      clip.append(unit);
      element.append(clip);
      units.push(unit);
    });
    return units;
  };

  const createTextSplit = (element) => {
    if (element.dataset.sw !== "split") return;
    const mode = element.dataset.swMode === "words" || element.dataset.swMode === "lines" ? element.dataset.swMode : "chars";
    const effect = element.getAttribute("data-sw-effect") || "fade-up";
    const units = splitTextIntoUnits(element, mode);
    const render = (progress) => renderSplitUnits(units, effect, progress);
    if (shouldReduceMotion(element)) {
      render(1);
      element.setAttribute("data-sw-ready", "split");
      return;
    }
    registerTextByPlayMode(element, render);
    element.setAttribute("data-sw-ready", "split");
  };

  const createTextScramble = (element) => {
    if (element.dataset.sw !== "scramble") return;
    const text = element.textContent;
    const chars = element.getAttribute("data-sw-chars") || "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
    const speed = readNumber(element, "data-sw-speed", 30, 1, 200);
    const render = (progress) => {
      const revealed = Math.floor(text.length * progress);
      element.textContent = [...text].map((char, index) => {
        if (char === " " || index < revealed || progress >= 1) return char;
        return chars[(index * 7 + Math.floor(progress * speed)) % chars.length];
      }).join("");
    };
    if (shouldReduceMotion(element)) {
      element.textContent = text;
      element.setAttribute("data-sw-ready", "scramble");
      return;
    }
    registerTextByPlayMode(element, render);
    element.setAttribute("data-sw-ready", "scramble");
  };

  const createTextType = (element) => {
    if (element.dataset.sw !== "type") return;
    const text = element.textContent;
    const variant = element.getAttribute("data-sw-variant") || "default";
    if (!element.hasAttribute("data-sw-cursor")) element.setAttribute("data-sw-cursor", "|");
    const render = (progress) => {
      let typed = progress;
      if (variant === "fast") typed = Math.sqrt(progress);
      else if (variant === "slow") typed = Math.pow(progress, 1.75);
      else if (variant === "repeat") {
        typed = progress < 0.35 ? progress / 0.35 : progress < 0.55 ? 1 : progress < 0.75 ? 1 - (progress - 0.55) / 0.2 : (progress - 0.75) / 0.25;
      } else if (variant === "pause") {
        typed = progress < 0.3 ? progress / 0.3 : progress < 0.7 ? 1 : progress < 0.84 ? 1 - (progress - 0.7) / 0.14 : (progress - 0.84) / 0.16;
      }
      element.textContent = text.slice(0, Math.round(text.length * clampProgress(typed)));
    };
    if (shouldReduceMotion(element)) {
      element.textContent = text;
      element.setAttribute("data-sw-ready", "type");
      return;
    }
    registerTextByPlayMode(element, render);
    element.setAttribute("data-sw-ready", "type");
  };

  const renderWhole = (el, effect, progress) => {
    el.style.opacity = String(clampProgress(progress * 1.5));
    el.style.filter = "none";
    el.style.textShadow = "none";
    el.style.webkitTextStroke = "0 transparent";
    el.style.color = "";
    if (effect === "mask-up") {
      el.style.clipPath = `inset(${(1 - progress) * 100}% 0 0)`;
      el.style.transform = `translateY(${(1 - progress) * 30}px)`;
    } else if (effect === "perspective") {
      el.style.transform = `perspective(500px) rotateX(${(1 - progress) * 76}deg) translateY(${(1 - progress) * 24}px)`;
    } else if (effect === "tracking") {
      el.style.letterSpacing = `${(1 - progress) * 0.3}em`;
      el.style.transform = `scaleX(${0.86 + progress * 0.14})`;
    } else if (effect === "gradient") {
      el.style.backgroundPosition = `${100 - progress * 100}% 50%`;
      el.style.transform = "none";
    } else if (effect === "stroke") {
      el.style.webkitTextStroke = `1.2px rgba(56,189,248,${0.9 - progress * 0.45})`;
      el.style.color = `rgba(56,189,248,${progress})`;
      el.style.transform = `translateY(${(1 - progress) * 12}px)`;
    } else {
      const color = effect === "glow-pink" ? "216,140,255" : effect === "glow-white" ? "255,255,255" : "56,189,248";
      el.style.color = `rgb(${color})`;
      el.style.textShadow = `0 0 ${3 + progress * 6}px rgba(${color},${progress}),0 0 ${progress * 22}px rgba(${color},${progress * 0.78})`;
      el.style.transform = `scale(${0.94 + progress * 0.06})`;
    }
  };

  const createTextWhole = (element) => {
    if (element.dataset.sw !== "whole") return;
    const effect = element.getAttribute("data-sw-effect") || "mask-up";
    const render = (progress) => renderWhole(element, effect, progress);
    if (shouldReduceMotion(element)) {
      render(1);
      element.setAttribute("data-sw-ready", "whole");
      return;
    }
    registerTextByPlayMode(element, render);
    element.setAttribute("data-sw-ready", "whole");
  };

  const createCanvas = (element) => {
    if (element.dataset.swReady === "canvas") return;
    const canvas = element.tagName.toLowerCase() === "canvas" ? element : element.querySelector("canvas");
    if (!canvas) return;

    const context = canvas.getContext("2d");
    if (!context) return;

    const mode = element.getAttribute("data-sw-canvas") || element.getAttribute("data-sw-mode") || "view"; // "once" | "view" | "scroll"
    const preset = element.getAttribute("data-sw-canvas-preset") || "particles";
    const particleCount = readNumber(element, "data-sw-canvas-count", 45, 5, 200);
    const color = element.getAttribute("data-sw-canvas-color") || "#38bdf8";
    const scrub = element.getAttribute("data-sw-scrub") || "true";
    const start = element.getAttribute("data-sw-start") || "top 85%";
    const end = element.getAttribute("data-sw-end") || "bottom 15%";

    let width = 300;
    let height = 200;
    let dpr = 1;
    let animId = null;
    let isVisible = false;
    let progress = 0;
    let startTime = performance.now();

    const updateDimensions = () => {
      const rect = canvas.getBoundingClientRect();
      dpr = Math.min(window.devicePixelRatio || 1, 2);
      const newW = Math.max(1, Math.round(rect.width || canvas.clientWidth || 300));
      const newH = Math.max(1, Math.round(rect.height || canvas.clientHeight || 200));
      if (width !== newW || height !== newH || canvas.width !== Math.round(newW * dpr) || canvas.height !== Math.round(newH * dpr)) {
        width = newW;
        height = newH;
        canvas.width = Math.round(width * dpr);
        canvas.height = Math.round(height * dpr);
        context.setTransform(dpr, 0, 0, dpr, 0, 0);
      }
    };

    updateDimensions();
    window.addEventListener("resize", updateDimensions, { passive: true });

    const particles = Array.from({ length: particleCount }, () => ({
      x: Math.random() * 600,
      y: Math.random() * 300,
      vx: (Math.random() - 0.5) * 0.7,
      vy: (Math.random() - 0.5) * 0.7,
      radius: Math.random() * 2 + 1.2
    }));

    const render = (time = performance.now()) => {
      updateDimensions();
      context.clearRect(0, 0, width, height);

      if (preset === "particles" || preset === "constellation") {
        const maxDist = Math.min(width, height) * 0.35;
        context.save();
        for (let i = 0; i < particles.length; i++) {
          const p = particles[i];
          if (mode !== "scroll") {
            p.x += p.vx;
            p.y += p.vy;
            if (p.x < 0 || p.x > width) p.vx *= -1;
            if (p.y < 0 || p.y > height) p.vy *= -1;
          } else {
            const angle = (i / particles.length) * Math.PI * 2 + progress * Math.PI * 2;
            const dist = (maxDist * 0.9) * Math.sin(progress * Math.PI * 1.5 + i * 0.5);
            p.x = width * 0.5 + Math.cos(angle) * (40 + dist);
            p.y = height * 0.5 + Math.sin(angle) * (30 + dist);
          }

          for (let j = i + 1; j < particles.length; j++) {
            const p2 = particles[j];
            const dx = p.x - p2.x;
            const dy = p.y - p2.y;
            const dist = Math.hypot(dx, dy);
            if (dist < maxDist) {
              const alpha = (1 - dist / maxDist) * 0.35 * (mode === "scroll" ? (0.2 + progress * 0.8) : 1);
              context.strokeStyle = `rgba(56, 189, 248, ${alpha})`;
              context.lineWidth = 1;
              context.beginPath();
              context.moveTo(p.x, p.y);
              context.lineTo(p2.x, p2.y);
              context.stroke();
            }
          }

          context.fillStyle = color;
          context.beginPath();
          context.arc(p.x, p.y, p.radius, 0, Math.PI * 2);
          context.fill();
        }
        context.restore();
      } else if (preset === "rings" || preset === "orbit") {
        context.save();
        const cx = width * 0.5;
        const cy = height * 0.5;
        const baseR = Math.min(width, height) * 0.38;
        const rings = 4;
        const rot = mode === "scroll" ? progress * Math.PI * 2 : (time - startTime) * 0.001;

        // Fundo do núcleo
        const grad = context.createRadialGradient(cx, cy, 2, cx, cy, baseR * 1.1);
        grad.addColorStop(0, "rgba(56, 189, 248, 0.25)");
        grad.addColorStop(0.5, "rgba(56, 189, 248, 0.05)");
        grad.addColorStop(1, "transparent");
        context.fillStyle = grad;
        context.beginPath();
        context.arc(cx, cy, baseR * 1.1, 0, Math.PI * 2);
        context.fill();

        for (let r = 1; r <= rings; r++) {
          const currentR = (baseR / rings) * r * (mode === "scroll" ? (0.35 + progress * 0.65) : 1);
          context.save();
          context.translate(cx, cy);
          context.rotate(rot * (r % 2 === 0 ? 1 : -1) * (1 / r));
          context.strokeStyle = `rgba(56, 189, 248, ${0.2 + (r / rings) * 0.55})`;
          context.lineWidth = 1.5;
          context.setLineDash([8 * r, 6 * r]);
          context.beginPath();
          context.arc(0, 0, currentR, 0, Math.PI * 2);
          context.stroke();

          // Ponto orbital
          const pAngle = rot * 2 * (r % 2 === 0 ? -1 : 1);
          context.fillStyle = "#fff";
          context.shadowColor = "#38bdf8";
          context.shadowBlur = 8;
          context.beginPath();
          context.arc(Math.cos(pAngle) * currentR, Math.sin(pAngle) * currentR, 3.5, 0, Math.PI * 2);
          context.fill();
          context.restore();
        }

        // Núcleo central
        context.fillStyle = "#38bdf8";
        context.shadowColor = "#38bdf8";
        context.shadowBlur = 12;
        context.beginPath();
        context.arc(cx, cy, 6, 0, Math.PI * 2);
        context.fill();
        context.restore();
      } else if (preset === "waves" || preset === "sine") {
        context.save();
        const cy = height * 0.5;
        const waves = 3;
        const waveProgress = mode === "scroll" ? progress * 6 : (time - startTime) * 0.002;

        for (let w = 0; w < waves; w++) {
          context.beginPath();
          context.strokeStyle = `rgba(56, 189, 248, ${0.25 + w * 0.25})`;
          context.lineWidth = 2;
          for (let x = 0; x <= width; x += 4) {
            const freq = 0.01 + w * 0.005;
            const amp = (height * 0.24) * (mode === "scroll" ? (0.3 + progress * 0.7) : 1);
            const y = cy + Math.sin(x * freq + waveProgress + w * 1.5) * amp;
            if (x === 0) context.moveTo(x, y);
            else context.lineTo(x, y);
          }
          context.stroke();
        }
        context.restore();
      } else if (preset === "graph" || preset === "chart") {
        context.save();
        const padding = 28;
        const gw = width - padding * 2;
        const gh = height - padding * 2;
        const pts = [0.15, 0.4, 0.25, 0.65, 0.45, 0.85, 0.7, 0.95];
        const currentProgress = mode === "scroll" ? progress : Math.min(1, (time - startTime) / 1200);

        context.beginPath();
        for (let i = 0; i < pts.length; i++) {
          const x = padding + (gw / (pts.length - 1)) * i;
          const targetY = padding + gh * (1 - pts[i]);
          const startY = padding + gh;
          const y = startY + (targetY - startY) * currentProgress;
          if (i === 0) context.moveTo(x, y);
          else context.lineTo(x, y);
        }
        context.strokeStyle = "#38bdf8";
        context.lineWidth = 3;
        context.stroke();

        for (let i = 0; i < pts.length; i++) {
          const x = padding + (gw / (pts.length - 1)) * i;
          const targetY = padding + gh * (1 - pts[i]);
          const startY = padding + gh;
          const y = startY + (targetY - startY) * currentProgress;
          context.fillStyle = "#fff";
          context.beginPath();
          context.arc(x, y, 4.5, 0, Math.PI * 2);
          context.fill();
        }
        context.restore();
      }
    };

    const loop = (now) => {
      if (!isVisible && mode === "view") {
        animId = null;
        return;
      }
      render(now);
      if (mode === "once") {
        if (now - startTime < 2000) {
          animId = requestAnimationFrame(loop);
        } else {
          animId = null;
        }
        return;
      }
      animId = requestAnimationFrame(loop);
    };

    const startAnimation = () => {
      if (!animId) {
        startTime = performance.now();
        animId = requestAnimationFrame(loop);
      }
    };

    const stopAnimation = () => {
      if (animId) {
        cancelAnimationFrame(animId);
        animId = null;
      }
    };

    if (mode === "scroll") {
      window.ScrollTrigger.create({
        trigger: element,
        start: start,
        end: end,
        scrub: scrub === "false" ? false : (scrub === "true" ? 0.5 : parseFloat(scrub) || 0.5),
        onUpdate: (self) => {
          progress = self.progress;
          render(performance.now());
        }
      });
      // Render inicial imediato
      render(performance.now());
    } else if (mode === "once") {
      window.ScrollTrigger.create({
        trigger: element,
        start: start,
        once: true,
        onEnter: () => {
          startAnimation();
        }
      });
      render(performance.now());
    } else {
      const observer = new IntersectionObserver((entries) => {
        entries.forEach((entry) => {
          isVisible = entry.isIntersecting;
          if (isVisible) {
            startAnimation();
          } else {
            stopAnimation();
          }
        });
      }, { threshold: 0.1 });
      observer.observe(element);
      render(performance.now());
    }

    element.setAttribute("data-sw-ready", "canvas");
  };

  const createPageTransitions = () => {
    const scrollStorageKey = () => `sw-page-scroll:${window.location.pathname}${window.location.search}`;
    const parseEffects = (rawValue) => {
      const parts = String(rawValue || "wipe").split("|").map((value) => value.trim()).filter(Boolean);
      const outgoing = PAGE_EFFECTS.has(parts[0]) ? parts[0] : "wipe";
      const incomingCandidate = parts[1] || outgoing;
      const incoming = PAGE_EFFECTS.has(incomingCandidate) ? incomingCandidate : outgoing;
      return { outgoing, incoming };
    };
    const createOverlay = (effect, state) => {
      const overlay = document.createElement("div");
      const loader = document.createElement("span");
      overlay.className = "sw-page-transition";
      overlay.dataset.effect = effect;
      if (state) overlay.dataset.state = state;
      loader.className = "sw-page-transition__loader";
      loader.setAttribute("role", "status");
      loader.setAttribute("aria-live", "polite");
      const brandTemplate = document.getElementById("sw-page-loader");
      if (brandTemplate instanceof window.HTMLTemplateElement) {
        loader.classList.add("has-brand");
        loader.append(brandTemplate.content.cloneNode(true));
      } else {
        loader.textContent = "Carregando";
      }
      overlay.append(loader);
      return overlay;
    };
    const waitForTransition = (element, timeout = 950) => new Promise((resolve) => {
      let settled = false;
      const finish = () => {
        if (settled) return;
        settled = true;
        element.removeEventListener("transitionend", onEnd);
        resolve();
      };
      const onEnd = (event) => {
        if (event.target === element) finish();
      };
      element.addEventListener("transitionend", onEnd);
      window.setTimeout(finish, timeout);
    });
    const preloadPage = async (destination) => {
      const controller = new AbortController();
      const timeout = window.setTimeout(() => controller.abort(), 4500);
      try {
        await window.fetch(destination.href, {
          method: "GET",
          credentials: "same-origin",
          cache: "force-cache",
          signal: controller.signal,
          headers: { "X-SW-Preload": "page" }
        });
      } catch (error) {
        warn(`Preload da página não concluiu: ${error.name}. A navegação continuará.`);
      } finally {
        window.clearTimeout(timeout);
      }
    };

    const storedEffect = window.sessionStorage.getItem("sw-page-effect");
    if (PAGE_EFFECTS.has(storedEffect)) {
      window.sessionStorage.removeItem("sw-page-effect");
      const overlay = createOverlay(storedEffect, "cover");
      document.body.append(overlay);
      document.documentElement.classList.remove("sw-page-pending");
      window.requestAnimationFrame(() => window.requestAnimationFrame(() => {
          overlay.dataset.state = "leave";
          waitForTransition(overlay).then(() => overlay.remove());
      }));
    } else document.documentElement.classList.remove("sw-page-pending");

    const savedScroll = Number.parseFloat(window.sessionStorage.getItem(scrollStorageKey()));
    if (window.location.hash || Number.isFinite(savedScroll)) {
      window.requestAnimationFrame(() => window.requestAnimationFrame(() => {
        window.ScrollTrigger?.refresh();
        if (window.location.hash) {
          const anchor = document.getElementById(window.location.hash.slice(1));
          anchor?.scrollIntoView({ block: "start" });
        } else {
          window.scrollTo({ top: savedScroll, behavior: "auto" });
        }
      }));
    }

    const playPageTransition = (rawEffect = "wipe") => {
      const effects = parseEffects(rawEffect);
      const overlay = createOverlay(effects.outgoing);
      document.body.append(overlay);
      return new Promise((resolve) => {
        window.requestAnimationFrame(() => window.requestAnimationFrame(() => {
          overlay.dataset.state = "cover";
          waitForTransition(overlay).then(() => {
            window.setTimeout(() => {
              overlay.dataset.effect = effects.incoming;
              overlay.dataset.state = "leave";
              waitForTransition(overlay).then(() => {
                overlay.remove();
                resolve();
              });
            }, 300);
          });
        }));
      });
    };

    document.querySelectorAll("a[data-sw-page], button[data-sw-page]").forEach((link) => {
      if (link.dataset.swPageReady === "true") return;
      const effects = parseEffects(link.dataset.swPage);
      link.dataset.swPageReady = "true";
      const hintPreload = () => {
        const href = link.getAttribute("href");
        if (!href || href === "#" || href.startsWith("#") || href.startsWith("javascript:")) return;
        const destination = new URL(link.href, window.location.href);
        if (destination.origin !== window.location.origin || link.dataset.swPrefetched === "true") return;
        const hint = document.createElement("link");
        hint.rel = "prefetch";
        hint.href = destination.href;
        document.head.append(hint);
        link.dataset.swPrefetched = "true";
      };
      link.addEventListener("pointerenter", hintPreload, { once: true, passive: true });
      link.addEventListener("focus", hintPreload, { once: true });
      link.addEventListener("click", (event) => {
        if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || link.target || link.hasAttribute("download")) return;
        
        const rawHref = link.getAttribute("href");
        const isHashOrEmpty = !rawHref || rawHref === "#" || rawHref.startsWith("#") || rawHref.startsWith("javascript:") || link.hasAttribute("data-sw-preview") || link.tagName.toLowerCase() === "button";

        event.preventDefault();

        // Modo simulação/preview: roda a animação de entrada e saída sem sair da tela
        if (isHashOrEmpty) {
          playPageTransition(link.dataset.swPage);
          return;
        }

        const destination = new URL(link.href, window.location.href);
        if (destination.origin !== window.location.origin) return;

        window.sessionStorage.setItem(scrollStorageKey(), String(window.scrollY));
        const overlay = createOverlay(effects.outgoing);
        overlay.dataset.loading = "true";
        document.body.append(overlay);
        window.requestAnimationFrame(() => window.requestAnimationFrame(() => {
          overlay.dataset.state = "cover";
          Promise.all([waitForTransition(overlay), preloadPage(destination)]).then(() => {
            overlay.dataset.loading = "false";
            window.sessionStorage.setItem("sw-page-effect", effects.incoming);
            window.location.assign(destination.href);
          });
        }));
      });
    });
  };

  const init = () => {
    if (!window.gsap || !window.ScrollTrigger) {
      // Motion agora mora dentro do bundle único (swcore.js) e roda em toda página, mesmo em
      // quem nunca usa a área de Animação. Só avisa no console quando existe de fato algo pra
      // animar (elemento [data-sw] real) — página sem nenhum, fica silenciosa.
      if (document.querySelector("[data-sw]")) {
        warn("GSAP e ScrollTrigger são necessários. O conteúdo continuará visível.");
      }
      return false;
    }

    window.gsap.registerPlugin(window.ScrollTrigger);
    document.querySelectorAll('[data-sw="pin"]').forEach(createPin);
    document.querySelectorAll('[data-sw]').forEach(createReveal);
    document.querySelectorAll('[data-sw="cascade"]').forEach(createCascade);
    document.querySelectorAll('[data-sw="parallax"]').forEach(createParallax);
    document.querySelectorAll('[data-sw="fixed"]').forEach(createFixed);
    document.querySelectorAll('[data-sw="window"]').forEach(createWindow);
    document.querySelectorAll('[data-sw="story"]').forEach(createStory);
    document.querySelectorAll('[data-sw="track"]').forEach(createTrack);
    document.querySelectorAll('[data-sw="frames"]').forEach(createFrames);
    document.querySelectorAll('[data-sw="sections"]').forEach(createSections);
    document.querySelectorAll('[data-sw="svg-draw"]').forEach(createSvgDraw);
    document.querySelectorAll('[data-sw="motion-path"]').forEach(createMotionPath);
    document.querySelectorAll('[data-sw="cursor"]').forEach(createCursor);
    document.querySelectorAll('[data-sw="magnetic"]').forEach(createMagnetic);
    document.querySelectorAll('[data-sw="tilt"]').forEach(createTilt);
    document.querySelectorAll('[data-sw="marquee"]').forEach(createMarquee);
    document.querySelectorAll('[data-sw="countdown"]').forEach(createCountdown);
    document.querySelectorAll('[data-sw="glow"]').forEach(createGlow);
    document.querySelectorAll('[data-sw="rotate"]').forEach(createRotate);
    document.querySelectorAll('[data-sw="scale"]').forEach(createScale);
    document.querySelectorAll('[data-sw="split"]').forEach(createTextSplit);
    document.querySelectorAll('[data-sw="scramble"]').forEach(createTextScramble);
    document.querySelectorAll('[data-sw="type"]').forEach(createTextType);
    document.querySelectorAll('[data-sw="whole"]').forEach(createTextWhole);
    document.querySelectorAll('[data-sw="canvas"]').forEach(createCanvas);
    document.querySelectorAll('[data-sw="video-scroll"], [data-sw-video="scroll"]').forEach(createVideoScroll);
    document.querySelectorAll('[data-sw="adaptive-media"]').forEach(createAdaptiveMedia);
    document.querySelectorAll('[data-sw="lottie"]').forEach(createLottie);
    document.querySelectorAll('[data-sw="rive"]').forEach(createRive);
    document.querySelectorAll('[data-sw="blob"], [data-sw="avatar"]').forEach(createBlob);
    createPageTransitions();
    return true;
  };

  const createVideoScroll = (element) => {
    if (element.dataset.swReady === "video-scroll") return;
    const video = element.tagName.toLowerCase() === "video" ? element : element.querySelector("video");
    if (!video) return;

    video.controls = false;
    video.pause();

    const isContainer = element !== video;
    const scrub = element.getAttribute("data-sw-scrub") || "0.3";
    const start = element.getAttribute("data-sw-start") || (isContainer ? "top top" : "top 75%");
    const end = element.getAttribute("data-sw-end") || (isContainer ? "bottom bottom" : "bottom 25%");
    const pin = element.getAttribute("data-sw-pin") === "true";

    let targetTime = 0;
    let animFrame = null;

    const setTime = () => {
      if (video.readyState >= 1 && Number.isFinite(video.duration) && video.duration > 0) {
        video.pause();
        const maxTime = Math.max(0, video.duration - 0.001);
        const clamped = Math.min(maxTime, Math.max(0, targetTime));
        if (Math.abs(video.currentTime - clamped) > 0.01) {
          video.currentTime = clamped;
        }
      }
      animFrame = null;
    };

    const requestTimeUpdate = (time) => {
      targetTime = time;
      if (!animFrame) {
        animFrame = requestAnimationFrame(setTime);
      }
    };

    const triggerOptions = {
      trigger: element,
      start,
      end,
      scrub: scrub === "false" ? false : (scrub === "true" ? 0.3 : parseFloat(scrub) || 0.3),
      onUpdate: (self) => {
        const duration = Number.isFinite(video.duration) && video.duration > 0 ? video.duration : (parseFloat(element.getAttribute("data-sw-duration")) || 3.83);
        const progress = self.progress;
        requestTimeUpdate(progress * duration);
        element.style.setProperty("--sw-video-progress", progress.toFixed(3));
      }
    };

    if (pin) {
      triggerOptions.pin = video.parentElement || video;
      triggerOptions.anticipatePin = 1;
    }

    window.ScrollTrigger.create(triggerOptions);

    video.addEventListener("loadedmetadata", () => {
      video.currentTime = 0;
      video.pause();
    }, { once: true });

    element.setAttribute("data-sw-ready", "video-scroll");
  };

  const refresh = () => {
    destroy();
    const initialized = init();
    if (initialized) window.ScrollTrigger.refresh();
    return initialized;
  };

  const resolveMedia = (options = {}) => {
    const screenWidth = options.screenWidth || window.innerWidth;
    const breakpoint = options.breakpoint || 768;
    const saveData = options.saveData !== undefined ? options.saveData : Boolean(window.navigator.connection?.saveData);
    const reduceMotion = options.reduceMotion !== undefined ? options.reduceMotion : window.matchMedia("(prefers-reduced-motion: reduce)").matches;

    if (reduceMotion) {
      return { type: "static", src: options.poster || options.static, reason: "Movimento reduzido ativado no sistema" };
    }
    if (saveData) {
      return { type: "static", src: options.poster || options.static, reason: "Economia de dados ativada pelo usuário" };
    }
    if (screenWidth < breakpoint) {
      return { type: "video-mobile", src: options.mobile || options.desktop, reason: `Tela mobile (${screenWidth}px < ${breakpoint}px)` };
    }
    return { type: "video-desktop", src: options.desktop, reason: `Tela desktop (${screenWidth}px >= ${breakpoint}px)` };
  };

  const createAdaptiveMedia = (element) => {
    if (element.dataset.swReady === "adaptive-media") return;
    const desktop = element.getAttribute("data-sw-desktop");
    const mobile = element.getAttribute("data-sw-mobile") || desktop;
    const poster = element.getAttribute("data-sw-poster") || element.getAttribute("data-sw-static");
    const breakpoint = parseInt(element.getAttribute("data-sw-breakpoint") || "768", 10);

    const decision = resolveMedia({ desktop, mobile, poster, breakpoint });

    if (decision.type === "static") {
      const img = document.createElement("img");
      img.src = decision.src || poster;
      img.alt = element.getAttribute("data-sw-alt") || "Mídia estática adaptativa";
      img.style.width = "100%";
      img.style.height = "100%";
      img.style.objectFit = "cover";
      element.replaceChildren(img);
    } else {
      const video = document.createElement("video");
      video.src = decision.src;
      video.poster = poster || "";
      video.playsInline = true;
      if (element.hasAttribute("data-sw-autoplay") || element.getAttribute("data-sw-autoplay") !== "false") {
        video.autoplay = true;
        video.loop = true;
        video.muted = true;
      }
      video.style.width = "100%";
      video.style.height = "100%";
      video.style.objectFit = "cover";
      element.replaceChildren(video);
      if (element.getAttribute("data-sw-mode") === "scroll") {
        video.setAttribute("data-sw", "video-scroll");
        createVideoScroll(video);
      }
    }

    element.setAttribute("data-sw-ready", "adaptive-media");
  };

  const createLottie = (element) => {
    if (element.dataset.swReady === "lottie") return;
    const src = element.getAttribute("data-sw-src") || element.getAttribute("data-sw-lottie");
    if (!src && !element.dataset.swJson) return;

    if (!window.lottie) {
      warn("Lottie player (window.lottie) não encontrado.", element);
      return;
    }

    const autoplay = element.hasAttribute("data-sw-autoplay") && element.getAttribute("data-sw-autoplay") !== "false";
    const loop = element.hasAttribute("data-sw-loop") && element.getAttribute("data-sw-loop") !== "false";
    const speed = parseFloat(element.getAttribute("data-sw-speed")) || 1;
    const hover = element.hasAttribute("data-sw-hover") && element.getAttribute("data-sw-hover") !== "false";
    const click = element.hasAttribute("data-sw-click") && element.getAttribute("data-sw-click") !== "false";
    const mode = element.getAttribute("data-sw-mode") || (autoplay ? "play" : (hover ? "hover" : (click ? "click" : "play")));
    const reducedMotion = shouldReduceMotion(element);

    let animationData = null;
    if (element.dataset.swJson) {
      try {
        animationData = JSON.parse(element.dataset.swJson);
      } catch (e) {}
    }

    const playerOptions = {
      container: element,
      renderer: "svg",
      loop: loop,
      autoplay: false
    };

    if (animationData) {
      playerOptions.animationData = animationData;
    } else {
      playerOptions.path = src;
    }

    const anim = window.lottie.loadAnimation(playerOptions);
    anim.setSpeed(speed);

    anim.addEventListener("DOMLoaded", () => {
      if (reducedMotion) {
        anim.goToAndStop(Math.max(0, anim.totalFrames - 1), true);
        return;
      }

      if (mode === "play" && autoplay) {
        anim.play();
      } else if (mode === "hover" || hover) {
        anim.goToAndStop(0, true);
        element.addEventListener("mouseenter", () => {
          anim.setDirection(1);
          anim.play();
        });
        element.addEventListener("mouseleave", () => {
          if (!loop) {
            anim.setDirection(-1);
            anim.play();
          } else {
            anim.pause();
          }
        });
      } else if (mode === "click" || click) {
        anim.goToAndStop(0, true);
        element.addEventListener("click", () => {
          anim.goToAndPlay(0, true);
        });
      } else if (mode === "scroll" && window.ScrollTrigger) {
        anim.goToAndStop(0, true);
        window.ScrollTrigger.create({
          trigger: element,
          start: element.getAttribute("data-sw-start") || "top 80%",
          end: element.getAttribute("data-sw-end") || "bottom 20%",
          scrub: true,
          onUpdate: (self) => {
            const frame = Math.round(self.progress * (anim.totalFrames - 1));
            anim.goToAndStop(frame, true);
          }
        });
      }
    });

    element._swLottie = anim;
    element.setAttribute("data-sw-ready", "lottie");
    return anim;
  };

  const createRive = (element) => {
    if (element.dataset.swReady === "rive") return;
    const canvas = element.tagName.toLowerCase() === "canvas" ? element : element.querySelector("canvas");
    if (!canvas) return;

    if (!window.rive) {
      warn("Rive runtime (window.rive) não encontrado.", element);
      return;
    }

    const src = element.getAttribute("data-sw-src") || element.getAttribute("data-sw-rive");
    if (!src) return;

    const wasmUrl = element.getAttribute("data-sw-wasm");
    if (wasmUrl && window.rive.RuntimeLoader) {
      window.rive.RuntimeLoader.setWasmUrl(wasmUrl);
    }

    const machine = element.getAttribute("data-sw-machine") || element.getAttribute("data-sw-state-machine");
    const autoplay = element.getAttribute("data-sw-autoplay") !== "false";

    const riveOptions = {
      src,
      canvas,
      autoplay,
      layout: new window.rive.Layout({
        fit: window.rive.Fit.Contain,
        alignment: window.rive.Alignment.Center
      }),
      onLoad: () => {
        player.resizeDrawingSurfaceToCanvas();
        if (typeof element._swRiveOnLoad === "function") {
          element._swRiveOnLoad(player);
        }
      }
    };

    if (machine) {
      riveOptions.stateMachines = machine;
    }

    const player = new window.rive.Rive(riveOptions);

    if ("ResizeObserver" in window) {
      const observer = new ResizeObserver(() => {
        player.resizeDrawingSurfaceToCanvas();
      });
      observer.observe(canvas);
      window.addEventListener("pagehide", () => observer.disconnect(), { once: true });
    }

    element._swRive = player;
    element.setAttribute("data-sw-ready", "rive");
    return player;
  };

  const BLOB_PRESETS = {
    blob: {
      idle: { d: "M 100,20 C 145,20 180,55 180,100 C 180,145 145,180 100,180 C 55,180 20,145 20,100 C 20,55 55,20 100,20 Z", eyeScaleY: 1, eyeY: 90, eyeDistance: 24, eyeRadius: 7, pupilRadius: 3.5, tilt: 0, mouth: "M 92,112 Q 100,116 108,112" },
      happy: { d: "M 100,25 C 155,15 185,60 175,105 C 165,150 145,175 100,175 C 55,175 35,150 25,105 C 15,60 45,15 100,25 Z", eyeScaleY: 0.2, eyeY: 86, eyeDistance: 26, eyeRadius: 8, pupilRadius: 0, tilt: 0, mouth: "M 88,106 Q 100,124 112,106" },
      curious: { d: "M 105,15 C 155,18 185,55 178,102 C 170,148 135,182 92,180 C 50,178 18,142 22,95 C 28,48 55,12 105,15 Z", eyeScaleY: 1.1, eyeY: 82, eyeDistance: 26, eyeRadius: 8.5, pupilRadius: 4, tilt: 8, mouth: "M 94,110 Q 100,114 106,108" },
      surprise: { d: "M 100,10 C 140,10 170,50 170,100 C 170,150 140,190 100,190 C 60,190 30,150 30,100 C 30,50 60,10 100,10 Z", eyeScaleY: 1.3, eyeY: 78, eyeDistance: 28, eyeRadius: 10, pupilRadius: 4.5, tilt: 0, mouth: "M 95,116 A 5,7 0 1,0 105,116 A 5,7 0 1,0 95,116" },
      think: { d: "M 90,22 C 135,15 175,45 182,90 C 188,135 150,178 105,182 C 60,185 22,150 18,105 C 15,60 45,28 90,22 Z", eyeScaleY: 0.8, eyeY: 80, eyeDistance: 24, eyeRadius: 7, pupilRadius: 3.5, tilt: -10, mouth: "M 92,112 L 108,110" },
      sleep: { d: "M 100,45 C 150,45 190,75 190,110 C 190,145 150,175 100,175 C 50,175 10,145 10,110 C 10,75 50,45 100,45 Z", eyeScaleY: 0.1, eyeY: 105, eyeDistance: 22, eyeRadius: 6, pupilRadius: 0, tilt: 0, mouth: "M 94,122 Q 100,125 106,122" }
    },
    ghost: {
      idle: { d: "M 100,20 C 150,20 175,55 175,110 C 175,155 160,180 145,170 C 130,160 115,180 100,170 C 85,160 70,180 55,170 C 40,160 25,155 25,110 C 25,55 50,20 100,20 Z", eyeScaleY: 1, eyeY: 85, eyeDistance: 26, eyeRadius: 8, pupilRadius: 4, tilt: 0, mouth: "M 93,108 Q 100,114 107,108" }
    },
    robot: {
      idle: { d: "M 45,35 C 45,25 55,25 65,25 L 90,25 L 90,10 L 110,10 L 110,25 L 135,25 C 145,25 155,25 155,35 L 155,160 C 155,170 145,170 135,170 L 65,170 C 55,170 45,170 45,160 Z", eyeScaleY: 1, eyeY: 85, eyeDistance: 32, eyeRadius: 9, pupilRadius: 4, tilt: 0, mouth: "M 85,125 L 115,125" }
    },
    cat: {
      idle: { d: "M 100,30 C 115,30 130,15 145,15 C 155,15 165,30 160,55 C 175,70 180,95 180,115 C 180,155 145,180 100,180 C 55,180 20,155 20,115 C 20,95 25,70 40,55 C 35,30 45,15 55,15 C 70,15 85,30 100,30 Z", eyeScaleY: 1, eyeY: 95, eyeDistance: 30, eyeRadius: 8, pupilRadius: 4, tilt: 0, mouth: "M 92,120 Q 100,126 108,120" }
    },
    drop: {
      idle: { d: "M 100,10 C 115,35 175,95 175,130 C 175,165 140,190 100,190 C 60,190 25,165 25,130 C 25,95 85,35 100,10 Z", eyeScaleY: 1, eyeY: 110, eyeDistance: 24, eyeRadius: 7.5, pupilRadius: 3.5, tilt: 0, mouth: "M 93,130 Q 100,135 107,130" }
    },
    star: {
      idle: { d: "M 100,15 C 112,60 140,88 185,100 C 140,112 112,140 100,185 C 88,140 60,112 15,100 C 60,88 88,60 100,15 Z", eyeScaleY: 1, eyeY: 95, eyeDistance: 22, eyeRadius: 7, pupilRadius: 3.5, tilt: 0, mouth: "M 94,115 Q 100,120 106,115" }
    }
  };

  const BLOB_SHAPES = BLOB_PRESETS.blob;

  const createBlob = (element) => {
    if (element.dataset.swReady === "blob") return;

    const size = parseInt(element.getAttribute("data-sw-size") || "200", 10);
    const color = element.getAttribute("data-sw-color") || "#0a0a0c";
    const eyeColor = element.getAttribute("data-sw-eye-color") || "#ffffff";
    const pupilColor = element.getAttribute("data-sw-pupil-color") || "#0a0a0c";
    const preset = element.getAttribute("data-sw-preset") || "blob";
    const customD = element.getAttribute("data-sw-d") || element.getAttribute("data-sw-path");
    const customEyeRad = parseFloat(element.getAttribute("data-sw-eye-size") || element.getAttribute("data-sw-eye-radius") || "0");
    const customPupilRad = parseFloat(element.getAttribute("data-sw-pupil-size") || element.getAttribute("data-sw-pupil-radius") || "0");
    let state = element.getAttribute("data-sw-state") || "idle";
    const track = element.getAttribute("data-sw-track") !== "false";

    const shapesPool = BLOB_PRESETS[preset] || BLOB_SHAPES;
    const initialShape = customD ? { ...shapesPool.idle, d: customD } : (shapesPool[state] || shapesPool.idle || BLOB_SHAPES.idle);

    element.innerHTML = `
      <svg viewBox="0 0 200 200" width="${size}" height="${size}" class="sw-blob-svg" style="display:block; overflow:visible; filter:drop-shadow(0 15px 25px rgba(0,0,0,0.3)); transform-origin: 50% 50%; transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);">
        <g class="sw-blob-group" style="transform-origin: 100px 100px; transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);">
          <path class="sw-blob-body" d="${initialShape.d}" fill="${color}" style="transition: d 0.45s cubic-bezier(0.34, 1.56, 0.64, 1), fill 0.3s ease;"></path>
          <g class="sw-blob-face" style="transform-origin: 100px 100px; transition: transform 0.35s cubic-bezier(0.34, 1.56, 0.64, 1);">
            <!-- Olho Esquerdo -->
            <g class="sw-blob-eye-l" style="transform-origin: 88px 90px; transition: transform 0.3s ease;">
              <ellipse class="sw-blob-sclera-l" cx="88" cy="90" rx="7" ry="7" fill="${eyeColor}"></ellipse>
              <circle class="sw-blob-pupil-l" cx="88" cy="90" r="3.5" fill="${pupilColor}"></circle>
            </g>
            <!-- Olho Direito -->
            <g class="sw-blob-eye-r" style="transform-origin: 112px 90px; transition: transform 0.3s ease;">
              <ellipse class="sw-blob-sclera-r" cx="112" cy="90" rx="7" ry="7" fill="${eyeColor}"></ellipse>
              <circle class="sw-blob-pupil-r" cx="112" cy="90" r="3.5" fill="${pupilColor}"></circle>
            </g>
            <!-- Boca / Expressão -->
            <path class="sw-blob-mouth" d="${initialShape.mouth || 'M 92,112 Q 100,116 108,112'}" stroke="${eyeColor}" stroke-width="2" stroke-linecap="round" fill="none" style="transition: d 0.35s ease;"></path>
          </g>
        </g>
      </svg>
    `;

    const svg = element.querySelector(".sw-blob-svg");
    const group = element.querySelector(".sw-blob-group");
    const body = element.querySelector(".sw-blob-body");
    const eyeL = element.querySelector(".sw-blob-eye-l");
    const eyeR = element.querySelector(".sw-blob-eye-r");
    const pupilL = element.querySelector(".sw-blob-pupil-l");
    const pupilR = element.querySelector(".sw-blob-pupil-r");
    const scleraL = element.querySelector(".sw-blob-sclera-l");
    const scleraR = element.querySelector(".sw-blob-sclera-r");
    const mouth = element.querySelector(".sw-blob-mouth");
    const face = element.querySelector(".sw-blob-face");

    let lookX = 0;
    let lookY = 0;
    let targetLookX = 0;
    let targetLookY = 0;
    let isBlinking = false;
    let animFrame = null;
    let currentCustomConfig = customD || customEyeRad || customPupilRad ? {
      d: customD || initialShape.d,
      eyeRadius: customEyeRad || initialShape.eyeRadius || 7,
      pupilRadius: customPupilRad || (customEyeRad ? customEyeRad * 0.5 : initialShape.pupilRadius) || 3.5
    } : null;

    const setExpression = (newState) => {
      state = newState;
      const activePool = BLOB_PRESETS[element.getAttribute("data-sw-preset") || "blob"] || BLOB_SHAPES;
      const shape = currentCustomConfig || activePool[state] || activePool.idle || BLOB_SHAPES.idle;
      if (shape.d) body.setAttribute("d", shape.d);
      if (shape.mouth) mouth.setAttribute("d", shape.mouth);
      group.style.transform = `rotate(${shape.tilt || 0}deg)`;

      const eRad = shape.eyeRadius || 7;
      const eY = shape.eyeY || 90;
      const eDist = shape.eyeDistance || 24;
      const pRad = shape.pupilRadius !== undefined ? shape.pupilRadius : Math.max(1, eRad * 0.5);

      scleraL.setAttribute("rx", eRad);
      scleraL.setAttribute("ry", eRad * (isBlinking ? 0.1 : (shape.eyeScaleY || 1)));
      scleraL.setAttribute("cx", 100 - eDist / 2);
      scleraL.setAttribute("cy", eY);

      scleraR.setAttribute("rx", eRad);
      scleraR.setAttribute("ry", eRad * (isBlinking ? 0.1 : (shape.eyeScaleY || 1)));
      scleraR.setAttribute("cx", 100 + eDist / 2);
      scleraR.setAttribute("cy", eY);

      pupilL.setAttribute("r", pRad);
      pupilR.setAttribute("r", pRad);
    };

    const updateDesign = (config = {}) => {
      if (config.d) {
        currentCustomConfig = { ...currentCustomConfig, d: config.d };
        body.setAttribute("d", config.d);
      }
      if (config.color) body.setAttribute("fill", config.color);
      if (config.eyeColor) {
        scleraL.setAttribute("fill", config.eyeColor);
        scleraR.setAttribute("fill", config.eyeColor);
        mouth.setAttribute("stroke", config.eyeColor);
      }
      if (config.pupilColor) {
        pupilL.setAttribute("fill", config.pupilColor);
        pupilR.setAttribute("fill", config.pupilColor);
      }
      if (config.eyeY !== undefined || config.eyeDistance !== undefined || config.eyeRadius !== undefined || config.pupilRadius !== undefined || config.mouth) {
        currentCustomConfig = {
          ...currentCustomConfig,
          eyeY: config.eyeY !== undefined ? config.eyeY : (currentCustomConfig?.eyeY || 90),
          eyeDistance: config.eyeDistance !== undefined ? config.eyeDistance : (currentCustomConfig?.eyeDistance || 24),
          eyeRadius: config.eyeRadius !== undefined ? config.eyeRadius : (currentCustomConfig?.eyeRadius || 7),
          pupilRadius: config.pupilRadius !== undefined ? config.pupilRadius : (currentCustomConfig?.pupilRadius !== undefined ? currentCustomConfig.pupilRadius : (config.eyeRadius ? config.eyeRadius * 0.5 : 3.5)),
          mouth: config.mouth || currentCustomConfig?.mouth
        };
        setExpression(state);
      }
    };

    const triggerBlink = () => {
      if (state === "sleep" || isBlinking) return;
      isBlinking = true;
      const activePool = BLOB_PRESETS[element.getAttribute("data-sw-preset") || "blob"] || BLOB_SHAPES;
      const shape = currentCustomConfig || activePool[state] || activePool.idle || BLOB_SHAPES.idle;
      const eRad = shape.eyeRadius || 7;
      scleraL.setAttribute("ry", 1);
      scleraR.setAttribute("ry", 1);
      setTimeout(() => {
        isBlinking = false;
        scleraL.setAttribute("ry", eRad * (shape.eyeScaleY || 1));
        scleraR.setAttribute("ry", eRad * (shape.eyeScaleY || 1));
      }, 140);
    };

    let blinkInterval = setInterval(triggerBlink, 3500 + Math.random() * 2500);

    const onMouseMove = (e) => {
      if (!track || state === "sleep") return;
      const activePool = BLOB_PRESETS[element.getAttribute("data-sw-preset") || "blob"] || BLOB_SHAPES;
      const shape = currentCustomConfig || activePool[state] || activePool.idle || BLOB_SHAPES.idle;
      const eRad = shape.eyeRadius || 7;
      const pRad = shape.pupilRadius !== undefined ? shape.pupilRadius : eRad * 0.5;

      const rect = element.getBoundingClientRect();
      const cx = rect.left + rect.width / 2;
      const cy = rect.top + rect.height / 2;
      const dx = e.clientX - cx;
      const dy = e.clientY - cy;
      const maxDist = Math.max(1.5, (eRad - pRad) * 0.95);
      const dist = Math.min(maxDist, Math.hypot(dx, dy) * 0.04);
      const angle = Math.atan2(dy, dx);
      targetLookX = Math.cos(angle) * dist;
      targetLookY = Math.sin(angle) * dist;
    };

    window.addEventListener("mousemove", onMouseMove, { passive: true });

    const updateLoop = () => {
      lookX += (targetLookX - lookX) * 0.12;
      lookY += (targetLookY - lookY) * 0.12;

      const activePool = BLOB_PRESETS[element.getAttribute("data-sw-preset") || "blob"] || BLOB_SHAPES;
      const shape = currentCustomConfig || activePool[state] || activePool.idle || BLOB_SHAPES.idle;
      const eDist = shape.eyeDistance || 24;
      const eY = shape.eyeY || 90;

      const eyeLx = 100 - eDist / 2 + lookX;
      const eyeLy = eY + lookY;
      const eyeRx = 100 + eDist / 2 + lookX;
      const eyeRy = eY + lookY;

      pupilL.setAttribute("cx", eyeLx);
      pupilL.setAttribute("cy", eyeLy);
      pupilR.setAttribute("cx", eyeRx);
      pupilR.setAttribute("cy", eyeRy);

      face.style.transform = `translate(${lookX * 0.5}px, ${lookY * 0.5}px)`;

      animFrame = requestAnimationFrame(updateLoop);
    };

    animFrame = requestAnimationFrame(updateLoop);

    if (element.hasAttribute("data-sw-hover-state")) {
      const hoverState = element.getAttribute("data-sw-hover-state");
      element.addEventListener("mouseenter", () => setExpression(hoverState));
      element.addEventListener("mouseleave", () => setExpression(element.getAttribute("data-sw-state") || "idle"));
    }

    setExpression(state);

    element.setBlobState = setExpression;
    element.updateBlobDesign = updateDesign;
    element.triggerBlobBlink = triggerBlink;
    element.setAttribute("data-sw-ready", "blob");

    window.addEventListener("pagehide", () => {
      clearInterval(blinkInterval);
      cancelAnimationFrame(animFrame);
      window.removeEventListener("mousemove", onMouseMove);
    }, { once: true });
  };

  const preloadImage = async (src, alt = "") => {
    const img = new Image();
    img.decoding = "async";
    img.src = src;
    if (alt) img.alt = alt;
    if (typeof img.decode === "function") {
      try {
        await img.decode();
        return img;
      } catch (e) {
        // Fallback para decode falho
      }
    }
    if (img.complete && img.naturalWidth > 0) return img;
    await new Promise((resolve, reject) => {
      img.addEventListener("load", () => resolve(img), { once: true });
      img.addEventListener("error", (err) => reject(err), { once: true });
    });
    return img;
  };

  const preloadImages = async (sources = [], onProgress) => {
    let loaded = 0;
    const total = sources.length;
    const promises = sources.map(async (src) => {
      try {
        const img = await preloadImage(src);
        loaded++;
        if (typeof onProgress === "function") onProgress(loaded, total, src, img);
        return img;
      } catch (err) {
        loaded++;
        if (typeof onProgress === "function") onProgress(loaded, total, src, null);
        return null;
      }
    });
    return Promise.all(promises);
  };

  window.SWMotion = Object.freeze({
    VERSION, init, refresh, destroy, play,
    canvas: createCanvas,
    videoScroll: createVideoScroll,
    adaptiveMedia: createAdaptiveMedia,
    resolveMedia,
    lottie: createLottie,
    rive: createRive,
    blob: createBlob,
    avatar: createBlob,
    setBlobState: (el, state) => el?.setBlobState?.(state),
    updateBlobDesign: (el, config) => el?.updateBlobDesign?.(config),
    registerBlobShape: (presetName, config) => {
      BLOB_PRESETS[presetName] = config;
    },
    blobPresets: BLOB_PRESETS,
    preloadImage,
    preloadImages,
    createSurface: (canvas) => {
      const context = canvas.getContext("2d");
      const surface = { canvas, context, width: 1, height: 1, dpr: 1 };
      surface.resize = () => {
        const box = canvas.getBoundingClientRect();
        const dpr = Math.min(window.devicePixelRatio || 1, 2);
        const width = Math.max(1, Math.round(box.width || canvas.clientWidth || 300));
        const height = Math.max(1, Math.round(box.height || canvas.clientHeight || 150));
        if (canvas.width !== Math.round(width * dpr) || canvas.height !== Math.round(height * dpr)) {
          canvas.width = Math.round(width * dpr);
          canvas.height = Math.round(height * dpr);
        }
        context.setTransform(dpr, 0, 0, dpr, 0, 0);
        surface.width = width;
        surface.height = height;
        surface.dpr = dpr;
        return surface;
      };
      return surface.resize();
    },
    transition: (effect) => {
      const parts = String(effect || "wipe").split("|").map((v) => v.trim()).filter(Boolean);
      const outgoing = PAGE_EFFECTS.has(parts[0]) ? parts[0] : "wipe";
      const incoming = parts[1] || outgoing;
      const overlay = document.createElement("div");
      overlay.className = "sw-page-transition";
      overlay.dataset.effect = outgoing;
      document.body.append(overlay);
      return new Promise((resolve) => {
        window.requestAnimationFrame(() => {
          overlay.dataset.state = "cover";
          window.setTimeout(() => {
            overlay.dataset.effect = incoming;
            overlay.dataset.state = "leave";
            window.setTimeout(() => {
              overlay.remove();
              resolve();
            }, 750);
          }, 450);
        });
      });
    },
    resolveEnd: (element, width) => readResponsiveEnd(element, "300%", width, false),
    resolveHeight: (element, width) => {
      const ranges = parseEndRanges(element.getAttribute("data-sw-h-at"));
      return ranges.find((range) => width >= range.min && width <= range.max)?.value || element.getAttribute("data-sw-h") || "200%";
    }
  });

  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", init, { once: true });
  } else {
    init();
  }
})(window, document);
