// /home/frank167/app.ensinoja.com/assets/js/chat.js
// OBS: este arquivo precisa ser carregado como MODULE no HTML:
// <script type="module" src="/assets/js/chat.js"></script>

import { initializeApp } from "https://www.gstatic.com/firebasejs/12.9.0/firebase-app.js";
import { getAuth, onAuthStateChanged, signOut } from "https://www.gstatic.com/firebasejs/12.9.0/firebase-auth.js";

(function boot(){
  const run = () => {
    const firebaseConfig = {
      apiKey: "AIzaSyBSMitLYJaaix3elwf4TZeqlwTl2pjVh50",
      authDomain: "app-ensinoja.firebaseapp.com",
      projectId: "app-ensinoja",
      storageBucket: "app-ensinoja.firebasestorage.app",
      messagingSenderId: "605830529593",
      appId: "1:605830529593:web:079c5a53db042b58c983e8"
    };

    const app = initializeApp(firebaseConfig);
    const auth = getAuth(app);

    /* ✅ Registra o SW (seguro e leve) */
    (function registerSW(){
      try{
        if(!("serviceWorker" in navigator)) return;
        navigator.serviceWorker.register("/sw.js").catch(()=>{});
      }catch(e){}
    })();

    const ROOM = "geral";
    const API_POST = "/chat_api/post.php";
    const API_HISTORY = "/chat_api/history.php";
    const API_STREAM = "/chat_api/stream.php";
    const API_PRESENCE = "/chat_api/presence.php";
    const API_UPLOAD = "/chat_api/upload.php";

    const MAX_IMAGES = 10;
    const WEBP_QUALITY = 0.86;
    const MAX_DIM = 1600;

    const MAX_VIDEOS = 3;

    /* ✅ NOVO: permite selecionar vários PDFs */
    const MAX_PDFS = 5;

    /* ✅ ALTERADO: 100MB (como você pediu) */
    const MAX_VIDEO_BYTES = 100 * 1024 * 1024;

    const ENABLE_VIDEO_COMPRESS = false;

    const PDF_THUMB_MAX_W = 1200;
    const PDF_THUMB_WEBP_Q = 0.86;
    const PDF_THUMB_ASPECT = 2.25;
    const PDF_THUMB_Y_BIAS = 0.18;

    const list = document.getElementById("list");
    const text = document.getElementById("text");
    const roomSub = document.getElementById("roomSub");
    const btnLogout = document.getElementById("btnLogout");
    const btnInstallApp = document.getElementById("btnInstallApp");
    const toBottom = document.getElementById("toBottom");
    const floatDate = document.getElementById("floatDate");
    const floatDateTxt = document.getElementById("floatDateTxt");

    const newMsgs = document.getElementById("newMsgs");
    const newMsgsBtn = document.getElementById("newMsgsBtn");
    const newMsgsTxt = document.getElementById("newMsgsTxt");

    const replyBar = document.getElementById("replyBar");
    const replyBarName = document.getElementById("replyBarName");
    const replyBarPrev = document.getElementById("replyBarPrev");
    const replyBarClose = document.getElementById("replyBarClose");

    // ✅ GLOBAL: deferredPrompt (para botão do topo + fluxo automático)
    window.__ej_pwa_deferredPrompt = window.__ej_pwa_deferredPrompt || null;

    function isStandalone(){
      return (window.matchMedia && window.matchMedia("(display-mode: standalone)").matches)
        || (window.navigator && window.navigator.standalone === true);
    }
    function isIOS(){
      return /iphone|ipad|ipod/i.test(navigator.userAgent || "");
    }

    // ✅ Se estiver instalado: some com o botão e não mostra nada
    (function enforceStandaloneUI(){
      function apply(){
        const st = isStandalone();
        if(btnInstallApp){
          btnInstallApp.style.display = st ? "none" : "";
        }
      }
      apply();

      window.addEventListener("appinstalled", () => {
        try{ window.__ej_pwa_deferredPrompt = null; }catch(e){}
        apply();
      });

      try{
        const mq = window.matchMedia("(display-mode: standalone)");
        mq && mq.addEventListener && mq.addEventListener("change", apply);
      }catch(e){}
    })();

    /* ✅ PWA: Fluxo pedido */
    (function pwaInstallFlow(){
      const AUTO_KEY = "ej_pwa_auto_prompted_v1";

      const back = document.getElementById("pwaBack");
      const btnClose = document.getElementById("pwaClose");
      const stepsBox = document.getElementById("pwaSteps");
      const arrowLayer = document.getElementById("pwaArrowLayer");
      const arrowAndroid = document.getElementById("pwaArrowAndroid");
      const arrowIOS = document.getElementById("pwaArrowIOS");
      const pulseDot = document.getElementById("pwaPulseDot");

      if(!back || !btnClose || !stepsBox || !arrowLayer || !arrowAndroid || !arrowIOS || !pulseDot) return;

      function alreadyAutoPrompted(){
        try{ return localStorage.getItem(AUTO_KEY) === "1"; }catch(e){ return false; }
      }
      function markAutoPrompted(){
        try{ localStorage.setItem(AUTO_KEY, "1"); }catch(e){}
      }

      function hide(){
        back.classList.remove("show");
        back.setAttribute("aria-hidden","true");
      }

      function showOrientation(){
        if(isStandalone()) return;

        arrowAndroid.style.display = "none";
        arrowIOS.style.display = "none";
        pulseDot.style.display = "none";

        if(isIOS()){
          stepsBox.innerHTML = `
            <div class="pwaStep">
              <div class="pwaStepIcon" aria-hidden="true">
                <svg viewBox="0 0 24 24" fill="none">
                  <path d="M12 3v10" stroke="rgba(37,99,235,.92)" stroke-width="2" stroke-linecap="round"/>
                  <path d="M9 6l3-3 3 3" stroke="rgba(37,99,235,.92)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
                  <path d="M5 10v10a1.8 1.8 0 0 0 1.8 1.8h10.4A1.8 1.8 0 0 0 19 20V10" stroke="rgba(17,24,39,.70)" stroke-width="2" stroke-linecap="round"/>
                </svg>
              </div>
              <div class="pwaStepTxt">
                <div class="pwaStepT1">1) Toque em Compartilhar</div>
                <div class="pwaStepT2">Procure o botão de compartilhar do Safari.</div>
              </div>
            </div>
            <div class="pwaStep">
              <div class="pwaStepIcon" aria-hidden="true">
                <svg viewBox="0 0 24 24" fill="none">
                  <path d="M7 3h7l4 4v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2Z" stroke="rgba(17,24,39,.72)" stroke-width="2" stroke-linejoin="round"/>
                  <path d="M14 3v4a2 2 0 0 0 2 2h4" stroke="rgba(17,24,39,.72)" stroke-width="2" stroke-linejoin="round"/>
                  <path d="M8 14h8" stroke="rgba(34,197,94,.92)" stroke-width="2" stroke-linecap="round"/>
                </svg>
              </div>
              <div class="pwaStepTxt">
                <div class="pwaStepT1">2) “Adicionar à Tela de Início”</div>
                <div class="pwaStepT2">Se aparecer como “Adicionar à Tela Inicial”, pode ser a mesma opção. Confirme e pronto.</div>
              </div>
            </div>
          `;
          arrowIOS.style.display = "block";
        }else{
          stepsBox.innerHTML = `
            <div class="pwaStep">
              <div class="pwaStepIcon" aria-hidden="true">
                <svg viewBox="0 0 24 24" fill="none">
                  <path d="M12 5h0" stroke="rgba(17,24,39,.70)" stroke-width="6" stroke-linecap="round"/>
                  <path d="M12 12h0" stroke="rgba(17,24,39,.70)" stroke-width="6" stroke-linecap="round"/>
                  <path d="M12 19h0" stroke="rgba(17,24,39,.70)" stroke-width="6" stroke-linecap="round"/>
                </svg>
              </div>
              <div class="pwaStepTxt">
                <div class="pwaStepT1">1) Abra o menu (⋮)</div>
                <div class="pwaStepT2">Fica no canto superior direito do navegador.</div>
              </div>
            </div>
            <div class="pwaStep">
              <div class="pwaStepIcon" aria-hidden="true">
                <svg viewBox="0 0 24 24" fill="none">
                  <path d="M7 3h10a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2Z" stroke="rgba(17,24,39,.72)" stroke-width="2"/>
                  <path d="M8 12h8" stroke="rgba(37,99,235,.92)" stroke-width="2" stroke-linecap="round"/>
                  <path d="M12 8v8" stroke="rgba(37,99,235,.92)" stroke-width="2" stroke-linecap="round"/>
                </svg>
              </div>
              <div class="pwaStepTxt">
                <div class="pwaStepT1">2) Procure “Instalar” ou “Adicionar à tela inicial”</div>
                <div class="pwaStepT2">Em alguns navegadores pode aparecer como “Instalar app” ou “Adicionar à tela inicial”. Toque na opção disponível.</div>
              </div>
            </div>
          `;
          arrowAndroid.style.display = "block";
          pulseDot.style.display = "block";
        }

        back.classList.add("show");
        back.setAttribute("aria-hidden","false");
      }

      btnClose.addEventListener("click", (e)=>{ e.preventDefault(); hide(); });
      back.addEventListener("click", (e)=>{ if(e.target === back){ e.preventDefault(); hide(); }});
      document.addEventListener("keydown", (e)=>{ if(e.key === "Escape" && back.classList.contains("show")) hide(); });

      let deferredPrompt = null;

      let autoArmed = false;
      function armAutoPromptOnce(){
        if(autoArmed) return;
        if(isStandalone()) return;
        if(isIOS()) return;
        if(alreadyAutoPrompted()) return;

        autoArmed = true;

        const fire = async () => {
          if(!deferredPrompt) return;
          if(isStandalone()) return;

          try{
            deferredPrompt.prompt();
            await deferredPrompt.userChoice;
            markAutoPrompted();
            deferredPrompt = null;
            window.__ej_pwa_deferredPrompt = null;
          }catch(err){
            markAutoPrompted();
          }finally{
            disarm();
          }
        };

        const handler = () => { try{ fire(); }catch(_){ } };

        const disarm = () => {
          document.removeEventListener("click", handler, true);
          document.removeEventListener("touchend", handler, true);
          document.removeEventListener("keydown", handler, true);
        };

        document.addEventListener("click", handler, true);
        document.addEventListener("touchend", handler, true);
        document.addEventListener("keydown", handler, true);
      }

      window.addEventListener("beforeinstallprompt", (e) => {
        try{ e.preventDefault(); }catch(_){}
        deferredPrompt = e;
        window.__ej_pwa_deferredPrompt = deferredPrompt;
        armAutoPromptOnce();
      });

      if(btnInstallApp){
        btnInstallApp.addEventListener("click", async (e) => {
          e.preventDefault();

          if(isStandalone()){
            try{ btnInstallApp.style.display = "none"; }catch(_){}
            return;
          }

          if(isIOS()){
            showOrientation();
            return;
          }

          const dp = deferredPrompt || window.__ej_pwa_deferredPrompt || null;
          if(!dp){
            showOrientation();
            return;
          }

          try{
            dp.prompt();
            await dp.userChoice;
            deferredPrompt = null;
            window.__ej_pwa_deferredPrompt = null;
            markAutoPrompted();
          }catch(err){
            deferredPrompt = null;
            window.__ej_pwa_deferredPrompt = null;
            markAutoPrompted();
            showOrientation();
          }
        });
      }
    })();

    let replyTarget = null;

    let followNew = true;
    let unseenNew = 0;
    const UNSEEN_CAP = 10;

    let me = null;
    let es = null;
    let oldestId = 0;
    let loadingOld = false;
    let reachedTopEnd = false;

    const pendingMap = new Map();
    const MAX_DOM_ROWS = 400;

    let programmaticScroll = false;
    let userScrollArmed = false;
    let userScrollTimer = null;

    let floatHideTimer = null;
    let lastScrollTop = 0;

    let currentLastId = 0;
    let reconnectTimer = null;
    let reconnectAttempt = 0;

    let presenceTimer = null;
    let lastOnlineText = "—";

    let kbOpen = false;
    let kbHeight = 0;

    /* ✅ NOVO: timestamp do último scroll (pra bloquear “tap fantasma” no vídeo) */
    let lastScrollTs = 0;

    function setOnlineCount(n){
      const num = Math.max(0, parseInt(n || "0", 10) || 0);
      const label = (num === 1) ? "1 online" : (num + " online");
      lastOnlineText = label;
      roomSub.textContent = label;
    }
    function keepLastOnlineText(){
      roomSub.textContent = lastOnlineText || "—";
    }

    function clearFloatTimer(){
      if(floatHideTimer){
        clearTimeout(floatHideTimer);
        floatHideTimer = null;
      }
    }
    function hideFloatNow(){
      clearFloatTimer();
      floatDate.classList.remove("show");
    }

    function armUserScroll(){
      userScrollArmed = true;
      if(userScrollTimer) clearTimeout(userScrollTimer);
      userScrollTimer = setTimeout(() => {
        userScrollArmed = false;
      }, 1200);
    }
    list.addEventListener("wheel", armUserScroll, {passive:true});
    list.addEventListener("touchstart", armUserScroll, {passive:true});
    list.addEventListener("touchmove", armUserScroll, {passive:true});
    list.addEventListener("pointerdown", armUserScroll, {passive:true});

    function pad2(n){ return String(n).padStart(2,'0'); }
    function fmtTime(d){ return pad2(d.getHours()) + ":" + pad2(d.getMinutes()); }
    function dayLabel(d){
      const now = new Date();
      const dn = new Date(now.getFullYear(), now.getMonth(), now.getDate());
      const dd = new Date(d.getFullYear(), d.getMonth(), d.getDate());
      const diffDays = Math.round((dn - dd) / 86400000);
      if (diffDays === 0) return "Hoje";
      if (diffDays === 1) return "Ontem";
      const meses = ["janeiro","fevereiro","março","abril","maio","junho","julho","agosto","setembro","outubro","novembro","dezembro"];
      return `${d.getDate()} de ${meses[d.getMonth()]} de ${d.getFullYear()}`;
    }

    function statusSvg(type){
      if(type === "clock"){
        return `<svg viewBox="0 0 24 24" aria-hidden="true">
          <path d="M12 7v6l4 2" stroke="rgba(17,24,39,.55)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
          <path d="M12 22a10 10 0 1 0-10-10 10 10 0 0 0 12 22Z" stroke="rgba(17,24,39,.55)" stroke-width="2" fill="none"/>
        </svg>`;
      }
      return `<svg viewBox="0 0 24 24" aria-hidden="true">
        <path d="M20 6 9 17l-5-5" stroke="rgba(17,24,39,.55)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
      </svg>`;
    }

    function escapeHtml(s){
      return String(s)
        .replaceAll("&","&amp;")
        .replaceAll("<","&lt;")
        .replaceAll(">","&gt;");
    }

    function shortDisplayName(nameOrEmail){
      const raw = String(nameOrEmail || "").trim();
      if(!raw) return "";
      if(raw.includes("@")){
        const pre = raw.split("@")[0];
        return pre.slice(0, 18);
      }
      const parts = raw.split(/\s+/).filter(Boolean);
      return parts.slice(0,2).join(" ") || raw;
    }

    function ensureDayChip(d){
      const key = d.getFullYear()+"-"+pad2(d.getMonth()+1)+"-"+pad2(d.getDate());
      const existing = list.querySelector(`[data-day="${key}"]`);
      if (existing) return;
      const wrap = document.createElement("div");
      wrap.className = "dayChip";
      wrap.dataset.day = key;
      wrap.dataset.label = dayLabel(d);
      wrap.innerHTML = `<span>${wrap.dataset.label}</span>`;
      list.appendChild(wrap);
    }

    function renderAvatar(photoUrl, display){
      const ava = document.createElement("div");
      ava.className = "msgAva";
      if(photoUrl){
        const img = document.createElement("img");
        img.src = photoUrl;
        img.alt = "";
        img.referrerPolicy = "no-referrer";
        img.onerror = () => { ava.textContent = "EJ"; };
        ava.appendChild(img);
      }else{
        const t = (display || "EJ").trim();
        ava.textContent = t.length ? t.slice(0,2).toUpperCase() : "EJ";
      }
      return ava;
    }

    function getLastMsgRow(){
      const rows = list.querySelectorAll(".msgRow");
      return rows.length ? rows[rows.length - 1] : null;
    }

    function applyGroupingClass(row, mine, uid){
      const last = getLastMsgRow();
      if(!last) return;
      const lastMine = last.classList.contains("out");
      const lastUid = last.dataset.uid || "";
      const changedSide = (mine !== lastMine);
      const changedSender = (!mine && uid && lastUid && uid !== lastUid);
      if(changedSide || changedSender) row.classList.add("break");
    }

    function pruneDomIfNeeded(){
      const rows = Array.from(list.querySelectorAll(".msgRow"));
      if(rows.length <= MAX_DOM_ROWS) return;
      const excess = rows.length - MAX_DOM_ROWS;
      for(let i=0;i<excess;i++){
        rows[i].remove();
      }
      const chips = Array.from(list.querySelectorAll(".dayChip"));
      for(const chip of chips){
        const day = chip.dataset.day;
        const stillUsed = list.querySelector(`.msgRow[data-day="${day}"]`);
        if(!stillUsed) chip.remove();
      }
    }

    function setProgrammaticScroll(fn){
      programmaticScroll = true;
      try{ fn(); }
      finally {
        requestAnimationFrame(() => { programmaticScroll = false; });
      }
    }

    function isNearBottom(){
      return (list.scrollHeight - (list.scrollTop + list.clientHeight)) < 220;
    }

    function hideNewMsgs(){
      unseenNew = 0;
      newMsgs.classList.remove("show");
      newMsgs.setAttribute("aria-hidden","true");
    }
    function showNewMsgs(){
      const n = Math.max(1, unseenNew);
      newMsgsTxt.textContent = (n >= UNSEEN_CAP) ? (UNSEEN_CAP + "+") : ("+" + n);
      newMsgs.classList.add("show");
      newMsgs.setAttribute("aria-hidden","false");
    }

    function hideNewMsgsAndSetFollow(){
      followNew = true;
      hideNewMsgs();
      toBottom.classList.remove("show");
    }

    function scrollToBottomInstant(){
      setProgrammaticScroll(() => {
        list.scrollTop = list.scrollHeight;
      });
      hideFloatNow();
      hideNewMsgsAndSetFollow();
    }

    function stickToBottomIfFollowing(){
      if(!followNew) return;
      setProgrammaticScroll(() => { list.scrollTop = list.scrollHeight; });
    }

    function bindMediaStick(el){
      if(!el) return;

      let fired = false;
      const fire = () => {
        if(fired) return;
        fired = true;
        requestAnimationFrame(() => stickToBottomIfFollowing());
        setTimeout(() => stickToBottomIfFollowing(), 60);
        setTimeout(() => stickToBottomIfFollowing(), 260);
      };

      if(el.tagName === "IMG"){
        el.addEventListener("load", fire, {once:true});
        if(el.complete && el.naturalWidth > 0) fire();
        return;
      }

      if(el.tagName === "VIDEO"){
        el.addEventListener("loadedmetadata", fire, {once:true});
        el.addEventListener("loadeddata", fire, {once:true});
        el.addEventListener("canplay", fire, {once:true});
        if(el.readyState >= 1) fire();
        return;
      }
    }

    function triggerDownload(url, filename){
      if(!url) return;
      const a = document.createElement("a");
      a.href = url;
      a.download = filename || "";
      a.style.display = "none";
      document.body.appendChild(a);
      a.click();
      a.remove();
    }

    function downloadPdf(url, filename){
      triggerDownload(url, filename || "Documento.pdf");
    }
    function downloadImage(url, filename){
      triggerDownload(url, filename || "imagem.webp");
    }
    function decodeURIComponentSafe(x){
      try{ return decodeURIComponent(String(x||"")); }catch(e){ return String(x||""); }
    }

    function parseReplyWrapper(body){
      const s = String(body || "");
      if(!s.startsWith("__REPLY__|")) return null;
      const sep = s.indexOf("||");
      if(sep < 0) return null;
      const metaPart = s.slice(0, sep);
      const inner = s.slice(sep + 2);

      const parts = metaPart.split("|").slice(1);
      const obj = {};
      for(const p of parts){
        const i = p.indexOf("=");
        if(i > 0){
          obj[p.slice(0,i)] = p.slice(i+1);
        }
      }
      const mid = parseInt(obj.mid || "0", 10) || 0;
      const name = obj.name ? decodeURIComponentSafe(obj.name) : "";
      const kind = obj.kind ? String(obj.kind) : "text";
      const prev = obj.prev ? decodeURIComponentSafe(obj.prev) : "";
      return { mid, name, kind, prev, inner };
    }

    function buildReplyPrefix(){
      if(!replyTarget || !replyTarget.mid) return "";
      const mid = replyTarget.mid;
      const name = replyTarget.name || "";
      const kind = replyTarget.kind || "text";
      const prev = replyTarget.preview || "";
      return "__REPLY__"
        + "|mid=" + String(mid)
        + "|name=" + encodeURIComponent(name)
        + "|kind=" + encodeURIComponent(kind)
        + "|prev=" + encodeURIComponent(prev)
        + "||";
    }

    function updateReplyOffset(){
      try{
        const shown = replyBar.classList.contains("show");
        if(!shown){
          document.documentElement.style.setProperty("--replyOffset", "0px");
          return;
        }
        const h = Math.ceil(replyBar.getBoundingClientRect().height || 0) + 8;
        document.documentElement.style.setProperty("--replyOffset", h + "px");
      }catch(e){
        document.documentElement.style.setProperty("--replyOffset", "0px");
      }
    }

    function showReplyBar(target){
      replyTarget = target;

      replyBarName.textContent = target?.name || "Respondendo";
      replyBarPrev.textContent = target?.preview || "—";

      if(!replyBar.classList.contains("show")){
        replyBar.classList.add("show");
        replyBar.setAttribute("aria-hidden","false");
      }
      requestAnimationFrame(updateReplyOffset);
    }

    function clearReplyBar(){
      replyTarget = null;
      replyBar.classList.remove("show");
      replyBar.setAttribute("aria-hidden","true");
      replyBarName.textContent = "Respondendo";
      replyBarPrev.textContent = "—";
      updateReplyOffset();
    }

    window.addEventListener("resize", () => requestAnimationFrame(updateReplyOffset));
    if(window.visualViewport){
      window.visualViewport.addEventListener("resize", () => requestAnimationFrame(updateReplyOffset));
    }

    replyBarClose.addEventListener("click", (e)=>{ e.preventDefault(); clearReplyBar(); });
    replyBarClose.addEventListener("keydown", (e)=>{
      if(e.key==="Enter"||e.key===" "){ e.preventDefault(); clearReplyBar(); }
    });

    function getMessagePreviewForReply(rowEl){
      if(!rowEl) return {kind:"text", preview:"Mensagem"};

      const bubble = rowEl.querySelector(".bubble");
      if(!bubble) return {kind:"text", preview:"Mensagem"};

      const wrap = bubble.querySelector(".attachWrap");
      if(wrap){
        const video = wrap.querySelector("video.attachVideo");
        const img = wrap.querySelector("img.attachImg");
        const pdfPrev = wrap.querySelector("img.attachPdfPreview");
        if(img) return {kind:"image", preview:"📷 Imagem"};
        if(video) return {kind:"video", preview:"🎥 Vídeo"};
        if(pdfPrev) return {kind:"pdf", preview:"📄 PDF"};
        return {kind:"file", preview:"📎 Anexo"};
      }

      const t = bubble.querySelector(".bodyText")?.textContent || "";
      const prev = t.trim().replace(/\s+/g," ").slice(0, 90);
      return {kind:"text", preview: prev || "Mensagem"};
    }

    function jumpToMessageId(mid){
      if(!mid) return;
      const el = list.querySelector(`.msgRow[data-msgid="${mid}"]`);
      if(!el) return;
      setProgrammaticScroll(() => {
        el.scrollIntoView({block:"center", behavior:"smooth"});
      });
      el.classList.add("jumpFlash");
      setTimeout(()=> el.classList.remove("jumpFlash"), 850);
    }

    function parseAttachBody(body){
      const s = String(body || "");
      if(!s.startsWith("__ATTACH__|")) return null;
      const parts = s.split("|").slice(1);
      const obj = {};
      for(const p of parts){
        const i = p.indexOf("=")
        if(i > 0){
          obj[p.slice(0,i)] = p.slice(i+1);
        }
      }
      if(!obj.k) return null;
      const urlRaw = obj.url ? obj.url : "";
      let url = urlRaw;
      try{ url = decodeURIComponent(urlRaw); }catch(e){}
      let thumb = "";
      if(obj.thumb){
        try{ thumb = decodeURIComponent(obj.thumb); }catch(e){ thumb = obj.thumb; }
      }
      return {
        kind: obj.k,
        url,
        name: obj.name ? decodeURIComponentSafe(obj.name) : "",
        tid: obj.tid ? String(obj.tid) : "",
        thumb
      };
    }

    function xSvg(){
      return `<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
        <path d="M7 7l10 10" stroke="rgba(17,24,39,.72)" stroke-width="2.6" stroke-linecap="round"/>
        <path d="M17 7L7 17" stroke="rgba(17,24,39,.72)" stroke-width="2.6" stroke-linecap="round"/>
      </svg>`;
    }
    function dlSvg(){
      return `<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
        <path d="M12 3v10" stroke="rgba(17,24,39,.78)" stroke-width="2" stroke-linecap="round"/>
        <path d="M8 11l4 4 4-4" stroke="rgba(17,24,39,.78)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
        <path d="M5 21h14" stroke="rgba(17,24,39,.78)" stroke-width="2" stroke-linecap="round"/>
      </svg>`;
    }

    function makeUploadRing(){
      const wrap = document.createElement("div");
      wrap.className = "upRingBtn";
      wrap.innerHTML = `
        <svg class="upRingSvg" viewBox="0 0 56 56" aria-hidden="true">
          <circle class="upRingTrack" cx="28" cy="28" r="22"></circle>
          <circle class="upRingProg"  cx="28" cy="28" r="22"></circle>
        </svg>
        <div class="upRingCenter" title="Cancelar">${xSvg()}</div>
      `;
      const prog = wrap.querySelector(".upRingProg");
      const center = wrap.querySelector(".upRingCenter");
      const C = 2 * Math.PI * 22;
      function setPct(p){
        const pct = Math.max(0, Math.min(100, p));
        const off = C * (1 - pct/100);
        prog.style.strokeDashoffset = off.toFixed(2);
      }
      setPct(0);
      return {wrap, center, setPct};
    }

    /* ✅ CORRIGIDO (PROFISSIONAL):
       garante play com 1 toque, MAS não toca ao rolar a lista
       (bloqueia “tap fantasma” após scroll e detecta arrasto)
    */
    function ensureVideoOneTapPlay(containerEl, videoEl){
      if(!containerEl || !videoEl) return;
      if(videoEl.dataset.ejOneTap === "1") return;
      videoEl.dataset.ejOneTap = "1";

      // playsinline para iOS e Android
      try{
        videoEl.playsInline = true;
        videoEl.setAttribute("playsinline","");
        videoEl.setAttribute("webkit-playsinline","");
      }catch(e){}

      const tryPlay = async () => {
        try{
          const p = videoEl.play();
          if(p && typeof p.then === "function") await p;
        }catch(err){
          try{
            videoEl.muted = true;
            const p2 = videoEl.play();
            if(p2 && typeof p2.then === "function") await p2;
          }catch(_){}
        }
      };

      const TAP_MOVE_PX = 12;
      const SCROLL_GUARD_MS = 180;

      let down = null;
      function isBlockedByRecentScroll(){
        const dt = Date.now() - (lastScrollTs || 0);
        return dt >= 0 && dt < SCROLL_GUARD_MS;
      }
      function shouldIgnoreTarget(t){
        try{
          if(!t) return false;
          if(t.closest?.(".dlBtn") || t.closest?.(".upRingBtn")) return true;
          if(t.closest?.("a,button,input,textarea,select,label")) return true;
        }catch(_){}
        return false;
      }

      function onPointerDown(e){
        if(shouldIgnoreTarget(e.target)) return;
        if(isBlockedByRecentScroll()) return;

        down = {
          id: e.pointerId,
          x: e.clientX,
          y: e.clientY,
          ts: Date.now(),
          moved: false
        };

        try{ containerEl.setPointerCapture?.(e.pointerId); }catch(_){}
      }

      function onPointerMove(e){
        if(!down) return;
        if(e.pointerId !== down.id) return;
        const dx = e.clientX - down.x;
        const dy = e.clientY - down.y;
        if(Math.abs(dx) > TAP_MOVE_PX || Math.abs(dy) > TAP_MOVE_PX){
          down.moved = true;
        }
      }

      function onPointerUp(e){
        if(!down) return;
        if(e.pointerId !== down.id) return;

        const moved = !!down.moved;
        down = null;

        if(moved) return;
        if(isBlockedByRecentScroll()) return;

        if(videoEl.paused){
          tryPlay();
        }
      }

      function onPointerCancel(){
        down = null;
      }

      containerEl.addEventListener("pointerdown", onPointerDown, {passive:true});
      containerEl.addEventListener("pointermove", onPointerMove, {passive:true});
      containerEl.addEventListener("pointerup", onPointerUp, {passive:true});
      containerEl.addEventListener("pointercancel", onPointerCancel, {passive:true});
      containerEl.addEventListener("lostpointercapture", onPointerCancel, {passive:true});
    }

    function updatePendingAttachmentRow(rowEl, attach){
      if(!rowEl || !attach) return;
      const wrap = rowEl.querySelector(".attachWrap");
      if(!wrap) return;

      wrap.dataset.url = attach.url || "";
      wrap.dataset.thumb = attach.thumb || "";

      if(attach.kind === "image"){
        const img = wrap.querySelector("img.attachImg");
        if(img && attach.url){
          img.src = attach.url;
          bindMediaStick(img);
          stickToBottomIfFollowing();
        }
        return;
      }

      if(attach.kind === "video"){
        const vid = wrap.querySelector("video.attachVideo");
        if(vid && attach.url){
          try{ vid.pause(); }catch(e){}
          try{ vid.currentTime = 0; }catch(e){}
          vid.src = attach.url;
          try{ vid.load(); }catch(e){}
          ensureVideoOneTapPlay(wrap, vid);
          bindMediaStick(vid);
          stickToBottomIfFollowing();
        }
        return;
      }

      if(attach.kind === "pdf"){
        const prev = wrap.querySelector("img.attachPdfPreview");
        if(prev && attach.thumb){
          prev.src = attach.thumb;
          bindMediaStick(prev);
          stickToBottomIfFollowing();
        }
        const nameEl = wrap.querySelector(".attachPdfName");
        if(nameEl && attach.name) nameEl.textContent = attach.name;
        stickToBottomIfFollowing();
        return;
      }
    }

    function renderAttachment(attach, {pending=false, onCancel=null} = {}){
      if(attach.kind === "image"){
        const box = document.createElement("div");
        box.className = "attachWrap";
        box.dataset.url = attach.url || "";
        box.dataset.name = attach.name || "";

        const img = document.createElement("img");
        img.className = "attachImg";
        img.alt = "imagem";
        img.src = attach.url || "";
        box.appendChild(img);
        bindMediaStick(img);

        const dl = document.createElement("div");
        dl.className = "dlBtn";
        dl.innerHTML = dlSvg();
        dl.title = "Baixar imagem";
        dl.addEventListener("click", (e) => {
          e.preventDefault();
          e.stopPropagation();
          const u = box.dataset.url || attach.url || "";
          const name = box.dataset.name || attach.name || "imagem.webp";
          if(u) downloadImage(u, name);
        });
        box.appendChild(dl);

        let overlay = null;
        let ring = null;
        if(pending){
          overlay = document.createElement("div");
          overlay.className = "upOverlay";
          box.appendChild(overlay);

          ring = makeUploadRing();
          box.appendChild(ring.wrap);

          if(onCancel){
            ring.center.addEventListener("click", (e) => {
              e.preventDefault();
              e.stopPropagation();
              onCancel();
            });
          }
        }
        return { el: box, overlay, ringEl: ring ? ring.wrap : null, ringSetPct: ring ? ring.setPct : null };
      }

      if(attach.kind === "video"){
        const box = document.createElement("div");
        box.className = "attachWrap";
        box.dataset.url = attach.url || "";
        box.dataset.name = attach.name || "";

        const vid = document.createElement("video");
        vid.className = "attachVideo";
        vid.src = attach.url || "";
        vid.controls = true;
        vid.playsInline = true;
        vid.setAttribute("playsinline","");
        vid.setAttribute("webkit-playsinline","");
        vid.preload = "metadata";
        box.appendChild(vid);
        ensureVideoOneTapPlay(box, vid);
        bindMediaStick(vid);

        let overlay = null;
        let ring = null;
        if(pending){
          overlay = document.createElement("div");
          overlay.className = "upOverlay";
          box.appendChild(overlay);

          ring = makeUploadRing();
          box.appendChild(ring.wrap);

          if(onCancel){
            ring.center.addEventListener("click", (e) => {
              e.preventDefault();
              e.stopPropagation();
              onCancel();
            });
          }
        }
        return { el: box, overlay, ringEl: ring ? ring.wrap : null, ringSetPct: ring ? ring.setPct : null };
      }

      const box = document.createElement("div");
      box.className = "attachWrap";
      box.dataset.url = attach.url || "";
      box.dataset.thumb = attach.thumb || "";
      box.dataset.name = attach.name || "Documento.pdf";

      box.style.cursor = "pointer";
      box.addEventListener("click", () => {
        const u = box.dataset.url || "";
        const n = box.dataset.name || "Documento.pdf";
        if(u) downloadPdf(u, n);
      });

      const prev = document.createElement("img");
      prev.className = "attachPdfPreview";
      prev.alt = "prévia do PDF";
      prev.src = attach.thumb || "";
      box.appendChild(prev);
      bindMediaStick(prev);

      const row = document.createElement("div");
      row.className = "attachPdf";
      row.innerHTML = `
        <div class="attachPdfIcon" aria-hidden="true">
          <svg viewBox="0 0 24 24" fill="none">
            <path d="M7 3h7l4 4v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2Z"
              stroke="rgba(17,24,39,.72)" stroke-width="2" stroke-linejoin="round"/>
            <path d="M14 3v4a2 2 0 0 0 2 2h4"
              stroke="rgba(17,24,39,.72)" stroke-width="2" stroke-linejoin="round"/>
            <path d="M8 17h8" stroke="rgba(239,68,68,.90)" stroke-width="2" stroke-linecap="round"/>
            <path d="M8 14h6" stroke="rgba(239,68,68,.90)" stroke-width="2" stroke-linecap="round"/>
          </svg>
        </div>
        <div class="attachPdfTxt">
          <div class="attachPdfName">${escapeHtml(attach.name || "Documento.pdf")}</div>
          <div class="attachPdfSub">PDF</div>
        </div>
      `;
      box.appendChild(row);

      let overlay = null;
      let ring = null;
      if(pending){
        overlay = document.createElement("div");
        overlay.className = "upOverlay";
        box.appendChild(overlay);

        ring = makeUploadRing();
        box.appendChild(ring.wrap);

        if(onCancel){
          ring.center.addEventListener("click", (e) => {
            e.preventDefault();
            e.stopPropagation();
            onCancel();
          });
        }
      }
      return { el: box, overlay, ringEl: ring ? ring.wrap : null, ringSetPct: ring ? ring.setPct : null };
    }

    function renderMessage(m, {pending=false, attachLocal=null, onCancel=null} = {}){
      const d = new Date(m.created_at || Date.now());
      ensureDayChip(d);

      const mine = me && m.uid === me.uid;

      const row = document.createElement("div");
      row.className = "msgRow " + (mine ? "out" : "in");
      row.dataset.msgid = String(m.id || "");
      row.dataset.uid = m.uid || "";
      row.dataset.day = d.getFullYear()+"-"+pad2(d.getMonth()+1)+"-"+pad2(d.getDate());
      row.dataset.dayLabel = dayLabel(d);

      applyGroupingClass(row, mine, m.uid || "");

      const display = shortDisplayName(m.name || m.email);
      const nameLine = (!mine && display)
        ? `<div class="sender" title="${escapeHtml(m.name || m.email || "")}">${escapeHtml(display)}</div>`
        : "";

      const status = mine
        ? `<span class="statusIcon" data-status="${pending ? "clock" : "check"}">${statusSvg(pending ? "clock" : "check")}</span>`
        : "";

      if(!mine){
        row.appendChild(renderAvatar(m.photo_url || "", display));
      }

      const bubble = document.createElement("div");
      bubble.className = "bubble";

      let replyMeta = null;
      let bodyInner = m.body || "";
      const wrapReply = parseReplyWrapper(bodyInner);
      if(wrapReply){
        replyMeta = wrapReply;
        bodyInner = wrapReply.inner || "";
      }

      const parsed = attachLocal ? attachLocal : parseAttachBody(bodyInner);

      function makeReplyQuote(){
        if(!replyMeta || !replyMeta.mid) return null;
        const q = document.createElement("div");
        q.className = "replyQuote";
        q.innerHTML = `
          <div class="replyQuoteName">${escapeHtml(replyMeta.name || "Mensagem")}</div>
          <div class="replyQuoteText">${escapeHtml(replyMeta.prev || "—")}</div>
        `;
        q.addEventListener("click", (e)=>{
          e.preventDefault();
          e.stopPropagation();
          jumpToMessageId(replyMeta.mid);
        });
        return q;
      }

      if(parsed){
        const att = renderAttachment(parsed, {pending, onCancel});
        bubble.innerHTML = `${nameLine}`;
        const q = makeReplyQuote();
        if(q) bubble.appendChild(q);

        bubble.appendChild(att.el);

        const meta = document.createElement("div");
        meta.className = "metaLine";
        meta.innerHTML = `<span>${fmtTime(d)}</span>${status}`;
        bubble.appendChild(meta);

        row.appendChild(bubble);
        list.appendChild(row);
        pruneDomIfNeeded();
        stickToBottomIfFollowing();

        bindSwipeReply(row);
        return { row, bubble, attach: att };
      }

      bubble.innerHTML = `${nameLine}`;
      const q = makeReplyQuote();
      if(q) bubble.appendChild(q);

      const bodyDiv = document.createElement("div");
      bodyDiv.className = "bodyText";
      bodyDiv.innerHTML = escapeHtml(bodyInner || "");
      bubble.appendChild(bodyDiv);

      const metaDiv = document.createElement("div");
      metaDiv.className = "metaLine";
      metaDiv.innerHTML = `<span>${fmtTime(d)}</span>${status}`;
      bubble.appendChild(metaDiv);

      row.appendChild(bubble);
      list.appendChild(row);
      pruneDomIfNeeded();

      bindSwipeReply(row);
      return { row, bubble, attach: null };
    }

    (function applyBackgroundPreference(){
      const saved = localStorage.getItem("ej_bg_color");
      if(saved && /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(saved)){
        document.documentElement.style.setProperty("--bgColor", saved);
      }
      const img = new Image();
      img.onload = () => {
        document.documentElement.style.setProperty("--bgImg", `url("/assets/bg/bg_tile.webp?v=${Date.now()}")`);
      };
      img.onerror = () => {
        document.documentElement.style.setProperty("--bgImg", "none");
      };
      img.src = "/assets/bg/bg_tile.webp?v=" + Date.now();
    })();

    (function(){
      function update(){
        const vv = window.visualViewport;
        if(!vv){
          document.documentElement.style.setProperty("--vvb","0px");
          kbOpen = false; kbHeight = 0;
          return;
        }
        const kb = Math.max(0, (window.innerHeight - vv.height - vv.offsetTop));
        document.documentElement.style.setProperty("--vvb", kb + "px");
        kbHeight = kb;
        kbOpen = kb > 40;

        requestAnimationFrame(updateReplyOffset);
      }
      if(window.visualViewport){
        window.visualViewport.addEventListener("resize", update);
        window.visualViewport.addEventListener("scroll", update);
      }
      window.addEventListener("resize", update);
      update();
    })();

    function grow(){
      text.style.height = "auto";
      text.style.height = Math.min(120, text.scrollHeight) + "px";
    }
    text.addEventListener("input", grow);

    const SWIPE_MIN = 58;
    const SWIPE_MAX = 92;

    function bindSwipeReply(row){
      if(!row || row.dataset.swipeBound === "1") return;
      row.dataset.swipeBound = "1";

      let startX=0, startY=0;
      let dx=0, dy=0;
      let active=false;
      let locked=null;
      let trig=false;

      let raf = 0;
      let pendingX = 0;

      let winUp = null, winCancel = null, winBlur = null, winLost = null;

      function applyX(x){
        row.style.transform = `translate3d(${x}px,0,0)`;
      }

      function scheduleApply(x){
        pendingX = x;
        if(raf) return;
        raf = requestAnimationFrame(() => {
          raf = 0;
          applyX(pendingX);
        });
      }

      function cancelRaf(){
        if(raf){
          try{ cancelAnimationFrame(raf); }catch(e){}
          raf = 0;
        }
      }

      function detachGlobalEnds(){
        if(winUp){
          window.removeEventListener("pointerup", winUp, true);
          window.removeEventListener("pointercancel", winCancel, true);
          window.removeEventListener("blur", winBlur, true);
          row.removeEventListener("lostpointercapture", winLost, true);
        }
        winUp = winCancel = winBlur = winLost = null;
      }

      function hardResetTransform(){
        cancelRaf();
        pendingX = 0;
        row.style.transition = "transform .14s ease";
        applyX(0);

        void row.offsetHeight;

        requestAnimationFrame(() => {
          applyX(0);
        });

        setTimeout(()=>{ row.style.transition=""; }, 160);
      }

      function resetAll(){
        active=false; locked=null; trig=false;
        dx=0; dy=0;
        row.classList.remove("dragging");
        hardResetTransform();
        detachGlobalEnds();
      }

      function endGesture(){
        if(!active) return;
        resetAll();
      }

      function attachGlobalEnds(){
        detachGlobalEnds();

        winUp = ()=>{ endGesture(); };
        winCancel = ()=>{ endGesture(); };
        winBlur = ()=>{ endGesture(); };
        winLost = ()=>{ endGesture(); };

        window.addEventListener("pointerup", winUp, true);
        window.addEventListener("pointercancel", winCancel, true);
        window.addEventListener("blur", winBlur, true);
        row.addEventListener("lostpointercapture", winLost, true);
      }

      function pointerDown(e){
        if(e.pointerType === "mouse" && e.button !== 0) return;
        active=true;
        locked=null;
        trig=false;
        startX = e.clientX;
        startY = e.clientY;
        dx=0; dy=0;

        cancelRaf();
        row.classList.add("dragging");

        attachGlobalEnds();

        try{ row.setPointerCapture?.(e.pointerId); }catch(_){}
      }

      function pointerMove(e){
        if(!active) return;
        dx = e.clientX - startX;
        dy = e.clientY - startY;

        if(!locked){
          if(Math.abs(dx) > 10 || Math.abs(dy) > 10){
            locked = (Math.abs(dx) > Math.abs(dy)) ? "h" : "v";
          }else{
            return;
          }
        }
        if(locked === "v") return;

        if(dx < 0) dx = 0;

        e.preventDefault();
        const clamp = Math.max(0, Math.min(SWIPE_MAX, dx));
        scheduleApply(clamp);

        if(!trig && clamp >= SWIPE_MIN){
          trig=true;
          try{ navigator.vibrate && navigator.vibrate(10); }catch(_){}

          const mid = parseInt(row.dataset.msgid||"0",10) || 0;
          if(mid){
            const senderName = row.classList.contains("out")
              ? (shortDisplayName(me?.displayName || me?.email || "Você") || "Você")
              : (shortDisplayName(row.querySelector(".sender")?.textContent || "") || "Usuário");

            const pv = getMessagePreviewForReply(row);
            showReplyBar({ mid, name: senderName, preview: pv.preview, kind: pv.kind });
          }
        }
      }

      row.addEventListener("pointerdown", pointerDown, {passive:true});
      row.addEventListener("pointermove", pointerMove, {passive:false});
      row.addEventListener("pointerup", endGesture, {passive:true});
      row.addEventListener("pointercancel", endGesture, {passive:true});
    }

    async function postToChat(body, temp_id){
      const payload = {
        room: ROOM,
        body,
        temp_id,
        uid: me.uid,
        email: me.email || "",
        name: me.displayName || "",
        photo_url: me.photoURL || ""
      };

      const r = await fetch(API_POST + "?nocache=" + Date.now(), {
        method: "POST",
        headers: {"Content-Type":"application/json"},
        body: JSON.stringify(payload)
      });

      const txt = await r.text();
      if(!r.ok) throw new Error("HTTP " + r.status + " :: " + txt.slice(0,180));
      const j = JSON.parse(txt);
      if(!(j && j.ok)) throw new Error("post_fail");
      return j;
    }

    function findPendingMatch(msg){
      if(!me || msg.uid !== me.uid) return null;
      const now = Date.now();

      const rw = parseReplyWrapper(msg.body || "");
      const innerBody = rw ? rw.inner : (msg.body || "");

      const a = parseAttachBody(innerBody);
      if(a && a.tid){
        const p = pendingMap.get(a.tid);
        if(p && p.el) return { tid: a.tid, attach: a, replyWrap: rw, ...p };
      }

      for(const [tid, p] of pendingMap.entries()){
        if(!p || !p.el) continue;
        const age = now - (p.ts || 0);
        if(age > 15000) continue;
        if((p.body || "").trim() === (msg.body || "").trim()){
          return { tid, attach: a, replyWrap: rw, ...p };
        }
      }
      return null;
    }

    function markPendingAsSent(el, newId){
      if(!el) return;
      if(newId) el.dataset.msgid = String(newId);
      const st = el.querySelector("[data-status]");
      if(st){
        st.setAttribute("data-status","check");
        st.innerHTML = statusSvg("check");
      }
    }

    async function sendMessage(){
      const bodyRaw = (text.value || "").trim();
      if(!bodyRaw) return;

      const temp_id = "t" + Date.now() + "_" + Math.random().toString(16).slice(2);
      const body = buildReplyPrefix() + bodyRaw;

      const local = {
        id: 0,
        uid: me.uid,
        name: me.displayName || "",
        email: me.email || "",
        photo_url: me.photoURL || "",
        body,
        created_at: new Date().toISOString()
      };

      const out = renderMessage(local, {pending:true});
      const el = out.row;
      el.dataset.tempid = temp_id;

      pendingMap.set(temp_id, { el, body, ts: Date.now() });

      text.value = "";
      grow();

      clearReplyBar();
      scrollToBottomInstant();

      try{
        const j = await postToChat(body, temp_id);
        markPendingAsSent(el, j.id || 0);
        setTimeout(() => { pendingMap.delete(temp_id); }, 12000);
      }catch(e){
        console.error(e);
      }
    }

    text.addEventListener("keydown", (e) => {
      if(e.key === "Enter"){
        e.preventDefault();
        sendMessage();
      }
    });

    toBottom.addEventListener("click", () => { scrollToBottomInstant(); });

    newMsgsBtn.addEventListener("click", (e) => {
      e.preventDefault();
      scrollToBottomInstant();
    });
    newMsgsBtn.addEventListener("keydown", (e) => {
      if(e.key === "Enter" || e.key === " "){
        e.preventDefault();
        scrollToBottomInstant();
      }
    });

    async function loadRecent(){
      const r = await fetch(API_HISTORY + "?room=" + encodeURIComponent(ROOM) + "&limit=35&nocache=" + Date.now(), {cache:"no-store"});
      const j = await r.json();
      if(!j.ok) throw new Error("history_fail");

      const items = (j.items || []).slice().reverse();
      list.innerHTML = "";
      oldestId = 0;
      reachedTopEnd = false;

      for(const m of items){
        renderMessage(m);
        oldestId = oldestId ? Math.min(oldestId, m.id) : m.id;
        currentLastId = Math.max(currentLastId, m.id);
      }
      scrollToBottomInstant();
    }

    async function loadOlder(){
      if(loadingOld || reachedTopEnd) return;
      if(!oldestId) return;

      loadingOld = true;
      const prevHeight = list.scrollHeight;

      try{
        const r = await fetch(API_HISTORY + "?room=" + encodeURIComponent(ROOM) + "&limit=35&before_id=" + oldestId + "&nocache=" + Date.now(), {cache:"no-store"});
        const j = await r.json();
        if(!j.ok) throw new Error("older_fail");

        const items = (j.items || []);
        if(items.length === 0){
          reachedTopEnd = true;
          loadingOld = false;
          return;
        }

        const asc = items.slice().reverse();
        const frag = document.createDocumentFragment();
        const temp = document.createElement("div");

        for(const m of asc){
          const d = new Date(m.created_at || Date.now());
          const dayKey = d.getFullYear()+"-"+pad2(d.getMonth()+1)+"-"+pad2(d.getDate());

          if(!list.querySelector(`[data-day="${dayKey}"]`) && !temp.querySelector(`[data-day="${dayKey}"]`)){
            const chip = document.createElement("div");
            chip.className = "dayChip";
            chip.dataset.day = dayKey;
            chip.dataset.label = dayLabel(d);
            chip.innerHTML = `<span>${chip.dataset.label}</span>`;
            temp.appendChild(chip);
          }

          const mine = me && m.uid === me.uid;
          const row = document.createElement("div");
          row.className = "msgRow " + (mine ? "out" : "in");
          row.dataset.msgid = String(m.id || "");
          row.dataset.uid = m.uid || "";
          row.dataset.day = dayKey;
          row.dataset.dayLabel = dayLabel(d);

          const display = shortDisplayName(m.name || m.email);

          if(!mine) row.appendChild(renderAvatar(m.photo_url || "", display));

          const nameLine = (!mine && display)
            ? `<div class="sender" title="${escapeHtml(m.name || m.email || "")}">${escapeHtml(display)}</div>`
            : "";

          const bubble = document.createElement("div");
          bubble.className = "bubble";
          bubble.innerHTML = `${nameLine}`;

          let replyMeta = null;
          let bodyInner = m.body || "";
          const wrapReply = parseReplyWrapper(bodyInner);
          if(wrapReply){
            replyMeta = wrapReply;
            bodyInner = wrapReply.inner || "";
          }

          if(replyMeta && replyMeta.mid){
            const q = document.createElement("div");
            q.className = "replyQuote";
            q.innerHTML = `
              <div class="replyQuoteName">${escapeHtml(replyMeta.name || "Mensagem")}</div>
              <div class="replyQuoteText">${escapeHtml(replyMeta.prev || "—")}</div>
            `;
            q.addEventListener("click", (e)=>{
              e.preventDefault(); e.stopPropagation();
              jumpToMessageId(replyMeta.mid);
            });
            bubble.appendChild(q);
          }

          const parsed = parseAttachBody(bodyInner);
          if(parsed){
            const att = renderAttachment(parsed, {pending:false});
            bubble.appendChild(att.el);
          }else{
            const bd = document.createElement("div");
            bd.className = "bodyText";
            bd.innerHTML = escapeHtml(bodyInner || "");
            bubble.appendChild(bd);
          }

          const meta = document.createElement("div");
          meta.className = "metaLine";
          meta.innerHTML = `<span>${fmtTime(d)}</span>`;
          bubble.appendChild(meta);

          row.appendChild(bubble);
          temp.appendChild(row);
          bindSwipeReply(row);

          oldestId = Math.min(oldestId, m.id);
        }

        while(temp.firstChild) frag.appendChild(temp.firstChild);

        setProgrammaticScroll(() => {
          list.insertBefore(frag, list.firstChild);
          const newHeight = list.scrollHeight;
          list.scrollTop = newHeight - prevHeight + list.scrollTop;
        });

      }catch(e){
        console.error(e);
      }finally{
        loadingOld = false;
      }
    }

    function anyDayChipVisibleInViewport(){
      const chips = list.querySelectorAll(".dayChip");
      const topY = 56;
      const bottomY = window.innerHeight;
      for(const chip of chips){
        const r = chip.getBoundingClientRect();
        const visible = (r.bottom >= topY) && (r.top <= bottomY);
        if(visible) return true;
      }
      return false;
    }

    async function sendPresenceOnce(){
      if(!me) return;
      try{
        await fetch(API_PRESENCE + "?nocache=" + Date.now(), {
          method: "POST",
          headers: {"Content-Type":"application/json"},
          body: JSON.stringify({
            room: ROOM,
            uid: me.uid,
            email: me.email || "",
            name: me.displayName || "",
            photo_url: me.photoURL || ""
          })
        });
      }catch(e){}
    }

    function startPresence(){
      stopPresence();
      sendPresenceOnce();
      presenceTimer = setInterval(() => {
        if(document.visibilityState !== "visible") return;
        sendPresenceOnce();
      }, 25000);
    }
    function stopPresence(){
      if(presenceTimer){
        clearInterval(presenceTimer);
        presenceTimer = null;
      }
    }

    function stopStream(){
      if(reconnectTimer){
        clearTimeout(reconnectTimer);
        reconnectTimer = null;
      }
      if(es){
        try{ es.close(); }catch(e){}
        es = null;
      }
    }

    function scheduleReconnect(){
      if(reconnectTimer) return;
      reconnectAttempt++;
      const base = Math.min(30000, 1000 * Math.pow(2, Math.min(5, reconnectAttempt)));
      const jitter = Math.floor(Math.random() * 350);
      const wait = Math.min(30000, base + jitter);

      reconnectTimer = setTimeout(() => {
        reconnectTimer = null;
        if(!me) return;
        if(document.visibilityState !== "visible") return;
        startStreamClean(currentLastId);
      }, wait);
    }

    function startStreamClean(lastId){
      stopStream();
      keepLastOnlineText();

      const url = API_STREAM + "?room=" + encodeURIComponent(ROOM) + "&last_id=" + (lastId || 0) + "&nocache=" + Date.now();
      es = new EventSource(url);

      es.onopen = () => {
        reconnectAttempt = 0;
        keepLastOnlineText();
      };

      es.onerror = () => {
        keepLastOnlineText();
        try{ es.close(); }catch(e){}
        es = null;
        scheduleReconnect();
      };

      function onMsg(data){
        let m;
        try{ m = JSON.parse(data); }catch(e){ return; }
        if(!m || !m.id) return;

        currentLastId = Math.max(currentLastId, m.id);
        if(list.querySelector(`[data-msgid="${m.id}"]`)) return;

        const match = findPendingMatch(m);
        if(match && match.el){
          const rw = parseReplyWrapper(m.body || "");
          const innerBody = rw ? rw.inner : (m.body || "");
          const a = parseAttachBody(innerBody);
          if(a && a.kind){
            updatePendingAttachmentRow(match.el, a);
          }
          const over = match.el.querySelector(".upOverlay");
          if(over) over.classList.add("done");

          const ring = match.el.querySelector(".upRingBtn");
          if(ring) setTimeout(()=>{ try{ ring.remove(); }catch(e){} }, 240);

          markPendingAsSent(match.el, m.id);
          pendingMap.delete(match.tid);

          stickToBottomIfFollowing();
          return;
        }

        renderMessage(m, {pending:false});
        oldestId = oldestId ? Math.min(oldestId, m.id) : m.id;

        const near = isNearBottom();
        if(followNew && near){
          scrollToBottomInstant();
        }else{
          toBottom.classList.add("show");
          unseenNew = Math.min(UNSEEN_CAP, (unseenNew + 1));
          showNewMsgs();
        }
      }

      function onPing(data){
        try{
          const j = JSON.parse(data);
          if(j && typeof j.online !== "undefined") setOnlineCount(j.online);
        }catch(e){}
      }

      es.onmessage = (ev) => onMsg(ev.data);
      es.addEventListener("message", (ev) => onMsg(ev.data));
      es.addEventListener("ping", (ev) => onPing(ev.data));
    }

    list.addEventListener("scroll", () => {
      lastScrollTs = Date.now();

      const cur = list.scrollTop;
      const delta = cur - lastScrollTop;
      lastScrollTop = cur;

      if(isNearBottom()){
        toBottom.classList.remove("show");
        hideNewMsgsAndSetFollow();
      }else{
        toBottom.classList.add("show");
        if(userScrollArmed && delta < 0){
          followNew = false;
        }
      }

      if(cur < 180) loadOlder();

      if(programmaticScroll) return;
      if(!userScrollArmed) return;

      if(isNearBottom()){
        hideFloatNow();
        return;
      }
      if(delta >= 0){
        hideFloatNow();
        return;
      }
      if(anyDayChipVisibleInViewport()){
        hideFloatNow();
        return;
      }

      const items = list.querySelectorAll(".msgRow, .dayChip");
      let label = "";
      for(const el of items){
        const rect = el.getBoundingClientRect();
        if(rect.bottom >= 78){
          if(el.classList.contains("msgRow")) label = el.dataset.dayLabel || "";
          else label = el.dataset.label || el.textContent.trim();
          break;
        }
      }
      if(label){
        floatDateTxt.textContent = label;
        floatDate.classList.add("show");
        if(floatHideTimer) clearTimeout(floatHideTimer);
        floatHideTimer = setTimeout(() => floatDate.classList.remove("show"), 3000);
      }
    });

    document.addEventListener("visibilitychange", () => {
      if(!me) return;
      if(document.visibilityState === "visible"){
        startPresence();
        if(!es) startStreamClean(currentLastId);
      }else{
        stopPresence();
        stopStream();
      }
    });

    window.addEventListener("pagehide", () => {
      stopPresence();
      stopStream();
    });

    const attachBtn = document.getElementById("attachBtn");
    const attachMenu = document.getElementById("attachMenu");
    const attachOverlay = document.getElementById("attachOverlay");
    const attachClose = document.getElementById("attachClose");
    const optGallery = document.getElementById("optGallery");
    const optVideo = document.getElementById("optVideo");
    const optPdf = document.getElementById("optPdf");

    const fileImages = document.getElementById("fileImages");
    const fileVideos = document.getElementById("fileVideos");
    const filePdf = document.getElementById("filePdf");

    function isMenuOpen(){ return attachMenu.classList.contains("show"); }

    function openMenu(){
      attachOverlay.classList.add("show");
      attachOverlay.setAttribute("aria-hidden","false");
      attachMenu.classList.add("show");
      attachBtn.setAttribute("aria-expanded","true");
    }

    function closeMenu(){
      attachMenu.classList.remove("show");
      attachOverlay.classList.remove("show");
      attachOverlay.setAttribute("aria-hidden","true");
      attachBtn.setAttribute("aria-expanded","false");
    }

    function toggleMenu(){
      if(isMenuOpen()) closeMenu();
      else openMenu();
    }

    attachBtn.addEventListener("click", (e) => {
      e.preventDefault();
      e.stopPropagation();
      toggleMenu();
    });

    attachOverlay.addEventListener("click", (e) => {
      if(e.target === attachOverlay){
        e.preventDefault();
        closeMenu();
      }
    });

    attachClose.addEventListener("click", (e) => {
      e.preventDefault();
      e.stopPropagation();
      closeMenu();
    });

    function pickImages(){ fileImages.value = ""; fileImages.click(); }
    function pickVideos(){ fileVideos.value = ""; fileVideos.click(); }
    function pickPdf(){ filePdf.value = ""; filePdf.click(); }

    function menuItemActivate(fn){
      return (e) => {
        e.preventDefault();
        e.stopPropagation();
        fn();
        closeMenu();
      };
    }

    optGallery.addEventListener("click", menuItemActivate(() => pickImages()));
    optVideo.addEventListener("click", menuItemActivate(() => pickVideos()));
    optPdf.addEventListener("click", menuItemActivate(() => pickPdf())) ;

    document.addEventListener("keydown", (e) => {
      if(e.key === "Escape" && isMenuOpen()) closeMenu();
    });

    function safeBaseName(name){
      return String(name||"arquivo")
        .replace(/\.[a-z0-9]+$/i,"")
        .replace(/[^a-z0-9\-_]+/gi,"_")
        .slice(0, 40) || "arquivo";
    }

    function loadImageFromFile(file){
      return new Promise((resolve, reject) => {
        const url = URL.createObjectURL(file);
        const img = new Image();
        img.onload = () => resolve({img, url});
        img.onerror = () => reject(new Error("Falha ao ler imagem"));
        img.src = url;
      });
    }

    async function fileToWebpBlob(file){
      const {img, url} = await loadImageFromFile(file);
      try{
        const w = img.naturalWidth || img.width;
        const h = img.naturalHeight || img.height;
        const scale = Math.min(1, MAX_DIM / Math.max(w, h));
        const nw = Math.max(1, Math.round(w * scale));
        const nh = Math.max(1, Math.round(h * scale));

        const canvas = document.createElement("canvas");
        canvas.width = nw;
        canvas.height = nh;
        const ctx = canvas.getContext("2d", {alpha:false});
        ctx.drawImage(img, 0, 0, nw, nh);

        const blob = await new Promise((resolve) => canvas.toBlob(resolve, "image/webp", WEBP_QUALITY));
        if(!blob) throw new Error("Falha ao converter para WebP");
        return blob;
      } finally {
        URL.revokeObjectURL(url);
      }
    }

    let _pdfjs = null;
    async function ensurePdfJs(){
      if(_pdfjs) return _pdfjs;
      const mod = await import("https://cdnjs.cloudflare.com/ajax/libs/pdf.js/4.10.38/pdf.min.mjs");
      mod.GlobalWorkerOptions.workerSrc = "https://cdnjs.cloudflare.com/ajax/libs/pdf.js/4.10.38/pdf.worker.min.mjs";
      _pdfjs = mod;
      return _pdfjs;
    }

    async function pdfFileToThumbWebpBlob(file){
      const pdfjsLib = await ensurePdfJs();
      const ab = await file.arrayBuffer();
      const pdf = await pdfjsLib.getDocument({data: ab}).promise;
      const page = await pdf.getPage(1);

      const v0 = page.getViewport({scale: 1});
      const scale = Math.min(2.2, PDF_THUMB_MAX_W / Math.max(1, v0.width));
      const viewport = page.getViewport({scale});

      const canvas = document.createElement("canvas");
      canvas.width = Math.max(1, Math.floor(viewport.width));
      canvas.height = Math.max(1, Math.floor(viewport.height));
      const ctx = canvas.getContext("2d", {alpha:false});

      await page.render({canvasContext: ctx, viewport}).promise;

      const cropW = canvas.width;
      const cropH = Math.min(canvas.height, Math.max(180, Math.floor(cropW / PDF_THUMB_ASPECT)));
      const y = Math.max(0, Math.min(canvas.height - cropH, Math.floor((canvas.height - cropH) * PDF_THUMB_Y_BIAS)));

      const crop = document.createElement("canvas");
      crop.width = cropW;
      crop.height = cropH;

      const cctx = crop.getContext("2d", {alpha:false});
      cctx.drawImage(canvas, 0, y, cropW, cropH, 0, 0, cropW, cropH);

      const blob = await new Promise((resolve) => crop.toBlob(resolve, "image/webp", PDF_THUMB_WEBP_Q));
      if(!blob) throw new Error("Falha ao gerar thumb do PDF");
      return blob;
    }

    async function sendAttachMessage(kind, url, name, temp_id, thumbUrl=""){
      const parts = [
        "__ATTACH__",
        "k=" + kind,
        "url=" + encodeURIComponent(url || ""),
        "name=" + encodeURIComponent(name || ""),
        "tid=" + temp_id
      ];
      if(thumbUrl){
        parts.push("thumb=" + encodeURIComponent(thumbUrl));
      }
      const attachBody = parts.join("|");
      const body = buildReplyPrefix() + attachBody;

      const j = await postToChat(body, temp_id);
      clearReplyBar();
      return j;
    }

    function startOutgoingPending(localAttach, temp_id, cancelFn){
      const attachBody = "__ATTACH__|k=" + localAttach.kind
        + "|url=" + encodeURIComponent(localAttach.url || "")
        + "|name=" + encodeURIComponent(localAttach.name || "")
        + "|tid=" + temp_id
        + (localAttach.thumb ? ("|thumb=" + encodeURIComponent(localAttach.thumb)) : "");

      const body = buildReplyPrefix() + attachBody;

      const local = {
        id: 0,
        uid: me.uid,
        name: me.displayName || "",
        email: me.email || "",
        photo_url: me.photoURL || "",
        body,
        created_at: new Date().toISOString()
      };

      const out = renderMessage(local, {
        pending:true,
        attachLocal: {kind: localAttach.kind, url: localAttach.url, name: localAttach.name, tid: temp_id, thumb: localAttach.thumb || ""},
        onCancel: cancelFn
      });

      out.row.dataset.tempid = temp_id;

      const ringSet = out.attach ? out.attach.ringSetPct : null;
      const overlay = out.row.querySelector(".upOverlay");
      const ringEl = out.row.querySelector(".upRingBtn");

      return {
        el: out.row,
        setPct: (p) => { if(ringSet) ringSet(p); },
        done: () => {
          if(overlay) overlay.classList.add("done");
          if(ringEl) setTimeout(()=>{ try{ ringEl.remove(); }catch(e){} }, 240);
        }
      };
    }

    async function startImageUpload(file){
      const temp_id = "u" + Date.now() + "_" + Math.random().toString(16).slice(2);
      const previewUrl = URL.createObjectURL(file);
      let xhrRef = null;

      const pendingUi = startOutgoingPending(
        {kind:"image", url: previewUrl, name: (file.name || "imagem.webp")},
        temp_id,
        () => {
          try{ if(xhrRef) xhrRef.abort(); }catch(e){}
          const p = pendingMap.get(temp_id);
          if(p && p.el) p.el.remove();
          pendingMap.delete(temp_id);
          try{ URL.revokeObjectURL(previewUrl); }catch(e){}
        }
      );

      pendingMap.set(temp_id, { el: pendingUi.el, body: "__ATTACH__", ts: Date.now(), kind:"image" });
      scrollToBottomInstant();

      try{
        const webpBlob = await fileToWebpBlob(file);
        const outName = safeBaseName(file.name) + ".webp";

        const res = await new Promise((resolve, reject) => {
          const xhr = new XMLHttpRequest();
          xhrRef = xhr;
          xhr.open("POST", API_UPLOAD + "?nocache=" + Date.now(), true);
          xhr.responseType = "json";

          xhr.upload.onprogress = (e) => {
            if(!e.lengthComputable) return;
            const pct = (e.total > 0) ? (e.loaded / e.total * 100) : 0;
            pendingUi.setPct(pct);
          };

          xhr.onload = () => {
            const r = xhr.response;
            if(xhr.status >= 200 && xhr.status < 300 && r && r.ok){
              resolve(r);
            }else{
              let msg = "Falha upload";
              try{ msg = (r && r.error) ? r.error : ("Falha upload (HTTP " + xhr.status + ")"); }catch(e){}
              reject(new Error(msg));
            }
          };
          xhr.onerror = () => reject(new Error("Erro de rede no upload"));
          xhr.onabort = () => reject(new Error("Upload cancelado"));

          const fd = new FormData();
          fd.append("room", ROOM);
          fd.append("kind", "image");
          fd.append("uid", me.uid);
          fd.append("email", me.email || "");
          fd.append("name", me.displayName || "");
          fd.append("photo_url", me.photoURL || "");
          fd.append("file", webpBlob, outName);

          xhr.send(fd);
        });

        pendingUi.setPct(100);
        pendingUi.done();

        const finalUrl = (res && (res.url || res.file_url || res.path)) ? (res.url || res.file_url || res.path) : "";
        if(!finalUrl) throw new Error("upload_sem_url");

        updatePendingAttachmentRow(pendingUi.el, {kind:"image", url: finalUrl, name: outName});

        const j = await sendAttachMessage("image", finalUrl, outName, temp_id);
        markPendingAsSent(pendingUi.el, j.id || 0);

        setTimeout(() => { pendingMap.delete(temp_id); }, 15000);
        try{ URL.revokeObjectURL(previewUrl); }catch(e){}
      }catch(err){
        console.error(err);
        const p = pendingMap.get(temp_id);
        if(p && p.el) p.el.remove();
        pendingMap.delete(temp_id);
        try{ URL.revokeObjectURL(previewUrl); }catch(e){}
        alert((err && err.message) ? err.message : "Falha ao enviar imagem");
      }
    }

    async function startVideoUpload(file){
      if(!file) return;

      if(file.size > MAX_VIDEO_BYTES){
        const mb = Math.round(MAX_VIDEO_BYTES / (1024*1024));
        alert(`Vídeo muito grande. Recomendado até ${mb} MB por vídeo.`);
        return;
      }

      const temp_id = "u" + Date.now() + "_" + Math.random().toString(16).slice(2);
      const previewUrl = URL.createObjectURL(file);
      let xhrRef = null;

      const outName = file.name || ("video_" + Date.now() + ".mp4");

      const pendingUi = startOutgoingPending(
        {kind:"video", url: previewUrl, name: outName},
        temp_id,
        () => {
          try{ if(xhrRef) xhrRef.abort(); }catch(e){}
          const p = pendingMap.get(temp_id);
          if(p && p.el) p.el.remove();
          pendingMap.delete(temp_id);
          try{ URL.revokeObjectURL(previewUrl); }catch(e){}
        }
      );

      pendingMap.set(temp_id, { el: pendingUi.el, body: "__ATTACH__", ts: Date.now(), kind:"video" });
      scrollToBottomInstant();

      try{
        const safeName = safeBaseName(outName) + "." + (String(outName).split(".").pop() || "mp4");

        const res = await new Promise((resolve, reject) => {
          const xhr = new XMLHttpRequest();
          xhrRef = xhr;
          xhr.open("POST", API_UPLOAD + "?nocache=" + Date.now(), true);
          xhr.responseType = "json";

          xhr.upload.onprogress = (e) => {
            if(!e.lengthComputable) return;
            const pct = (e.total > 0) ? (e.loaded / e.total * 100) : 0;
            pendingUi.setPct(pct);
          };

          xhr.onload = () => {
            const r = xhr.response;
            if(xhr.status >= 200 && xhr.status < 300 && r && r.ok){
              resolve(r);
            }else{
              let msg = "Falha upload";
              try{ msg = (r && r.error) ? r.error : ("Falha upload (HTTP " + xhr.status + ")"); }catch(e){}
              reject(new Error(msg));
            }
          };
          xhr.onerror = () => reject(new Error("Erro de rede no upload"));
          xhr.onabort = () => reject(new Error("Upload cancelado"));

          const fd = new FormData();
          fd.append("room", ROOM);
          fd.append("kind", "video");
          fd.append("uid", me.uid);
          fd.append("email", me.email || "");
          fd.append("name", me.displayName || "");
          fd.append("photo_url", me.photoURL || "");
          fd.append("file", file, safeName);

          xhr.send(fd);
        });

        pendingUi.setPct(100);
        pendingUi.done();

        const finalUrl = (res && (res.url || res.file_url || res.path)) ? (res.url || res.file_url || res.path) : "";
        if(!finalUrl) throw new Error("upload_sem_url");

        updatePendingAttachmentRow(pendingUi.el, {kind:"video", url: finalUrl, name: outName});

        const j = await sendAttachMessage("video", finalUrl, outName, temp_id);
        markPendingAsSent(pendingUi.el, j.id || 0);

        setTimeout(() => { pendingMap.delete(temp_id); }, 15000);
        try{ URL.revokeObjectURL(previewUrl); }catch(e){}
      }catch(err){
        console.error(err);
        const p = pendingMap.get(temp_id);
        if(p && p.el) p.el.remove();
        pendingMap.delete(temp_id);
        try{ URL.revokeObjectURL(previewUrl); }catch(e){}
        alert((err && err.message) ? err.message : "Falha ao enviar vídeo");
      }
    }

    async function uploadFileXHR({kind, fileOrBlob, filename, onProgress}){
      return await new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest();
        xhr.open("POST", API_UPLOAD + "?nocache=" + Date.now(), true);
        xhr.responseType = "json";

        if(xhr.upload && onProgress){
          xhr.upload.onprogress = (e) => {
            if(!e.lengthComputable) return;
            const pct = (e.total > 0) ? (e.loaded / e.total * 100) : 0;
            onProgress(pct);
          };
        }

        xhr.onload = () => {
          const r = xhr.response;
          if(xhr.status >= 200 && xhr.status < 300 && r && r.ok){
            resolve(r);
          }else{
            let msg = "Falha upload";
            try{ msg = (r && r.error) ? r.error : ("Falha upload (HTTP " + xhr.status + ")"); }catch(e){}
            reject(new Error(msg));
          }
        };
        xhr.onerror = () => reject(new Error("Erro de rede no upload"));
        xhr.onabort = () => reject(new Error("Upload cancelado"));

        const fd = new FormData();
        fd.append("room", ROOM);
        fd.append("kind", kind);
        fd.append("uid", me.uid);
        fd.append("email", me.email || "");
        fd.append("name", me.displayName || "");
        fd.append("photo_url", me.photoURL || "");
        fd.append("file", fileOrBlob, filename);

        xhr.send(fd);
      });
    }

    async function startPdfUpload(file){
      if(!file) return;

      const temp_id = "u" + Date.now() + "_" + Math.random().toString(16).slice(2);
      let xhrAborters = [];
      let localThumbUrl = "";
      let thumbBlob = null;

      try{
        thumbBlob = await pdfFileToThumbWebpBlob(file);
        localThumbUrl = URL.createObjectURL(thumbBlob);
      }catch(e){
        thumbBlob = null;
        localThumbUrl = "";
      }

      const out = startOutgoingPending(
        {kind:"pdf", url:"", name:file.name || "Documento.pdf", thumb: localThumbUrl},
        temp_id,
        () => {
          for(const fn of xhrAborters){
            try{ fn(); }catch(e){}
          }
          const p = pendingMap.get(temp_id);
          if(p && p.el) p.el.remove();
          pendingMap.delete(temp_id);
          if(localThumbUrl){
            try{ URL.revokeObjectURL(localThumbUrl); }catch(e){}
          }
        }
      );

      pendingMap.set(temp_id, { el: out.el, body: "__ATTACH__", ts: Date.now(), kind:"pdf" });
      scrollToBottomInstant();

      try{
        let remoteThumbUrl = "";
        if(thumbBlob){
          const thumbName = safeBaseName(file.name) + "_preview.webp";
          const resThumb = await uploadFileXHR({
            kind: "image",
            fileOrBlob: thumbBlob,
            filename: thumbName,
            onProgress: (p)=>{}
          });
          remoteThumbUrl = (resThumb && (resThumb.url || resThumb.file_url || resThumb.path)) ? (resThumb.url || resThumb.file_url || resThumb.path) : "";
          if(remoteThumbUrl){
            updatePendingAttachmentRow(out.el, {kind:"pdf", url:"", name:file.name || "Documento.pdf", thumb: remoteThumbUrl});
          }
        }

        const aborter = {xhr:null};
        const resPdf = await new Promise((resolve, reject) => {
          const xhr = new XMLHttpRequest();
          aborter.xhr = xhr;
          xhr.open("POST", API_UPLOAD + "?nocache=" + Date.now(), true);
          xhr.responseType = "json";

          xhr.upload.onprogress = (e) => {
            if(!e.lengthComputable) return;
            const pct = (e.total > 0) ? (e.loaded / e.total * 100) : 0;
            out.setPct(pct);
          };

          xhr.onload = () => {
            const r = xhr.response;
            if(xhr.status >= 200 && xhr.status < 300 && r && r.ok){
              resolve(r);
            }else{
              let msg = "Falha upload";
              try{ msg = (r && r.error) ? r.error : ("Falha upload (HTTP " + xhr.status + ")"); }catch(e){}
              reject(new Error(msg));
            }
          };
          xhr.onerror = () => reject(new Error("Erro de rede no upload"));
          xhr.onabort = () => reject(new Error("Upload cancelado"));

          const fd = new FormData();
          fd.append("room", ROOM);
          fd.append("kind", "pdf");
          fd.append("uid", me.uid);
          fd.append("email", me.email || "");
          fd.append("name", me.displayName || "");
          fd.append("photo_url", me.photoURL || "");
          fd.append("file", file, file.name || "Documento.pdf");

          xhr.send(fd);
        });

        xhrAborters.push(() => { if(aborter.xhr) aborter.xhr.abort(); });

        out.setPct(100);
        out.done();

        const finalUrl = (resPdf && (resPdf.url || resPdf.file_url || resPdf.path)) ? (resPdf.url || resPdf.file_url || resPdf.path) : "";
        if(!finalUrl) throw new Error("upload_sem_url");

        updatePendingAttachmentRow(out.el, {
          kind:"pdf",
          url: finalUrl,
          name: file.name || "Documento.pdf",
          thumb: remoteThumbUrl || localThumbUrl || ""
        });

        const j = await sendAttachMessage("pdf", finalUrl, file.name || "Documento.pdf", temp_id, remoteThumbUrl || "");
        markPendingAsSent(out.el, j.id || 0);

        setTimeout(() => { pendingMap.delete(temp_id); }, 15000);

        if(localThumbUrl){
          try{ URL.revokeObjectURL(localThumbUrl); }catch(e){}
        }

      }catch(err){
        console.error(err);
        const p = pendingMap.get(temp_id);
        if(p && p.el) p.el.remove();
        pendingMap.delete(temp_id);
        if(localThumbUrl){
          try{ URL.revokeObjectURL(localThumbUrl); }catch(e){}
        }
        alert((err && err.message) ? err.message : "Falha ao enviar PDF");
      }
    }

    fileImages.addEventListener("change", () => {
      const files = Array.from(fileImages.files || []).filter(f => f && f.type && f.type.startsWith("image/"));
      if(!files.length) return;

      if(files.length > MAX_IMAGES){
        alert("Você pode selecionar no máximo 10 imagens por vez.");
      }
      const chosen = files.slice(0, MAX_IMAGES);
      for(const f of chosen) startImageUpload(f);
    });

    fileVideos.addEventListener("change", () => {
      const files = Array.from(fileVideos.files || []).filter(f => f && f.type && f.type.startsWith("video/"));
      if(!files.length) return;

      if(files.length > MAX_VIDEOS){
        alert(`Você pode selecionar no máximo ${MAX_VIDEOS} vídeos por vez.`);
      }
      const chosen = files.slice(0, MAX_VIDEOS);
      for(const f of chosen) startVideoUpload(f);
    });

    // ✅ CORRIGIDO: agora envia VÁRIOS PDFs (até MAX_PDFS) e não só 1
    filePdf.addEventListener("change", () => {
      const files = Array.from(filePdf.files || []).filter(f => {
        if(!f) return false;
        const t = String(f.type || "").toLowerCase();
        if(t === "application/pdf") return true;
        const nm = String(f.name || "");
        return /\.pdf$/i.test(nm);
      });
      if(!files.length) return;

      if(files.length > MAX_PDFS){
        alert(`Você pode selecionar no máximo ${MAX_PDFS} PDFs por vez.`);
      }
      const chosen = files.slice(0, MAX_PDFS);
      for(const f of chosen) startPdfUpload(f);
    });

    onAuthStateChanged(auth, async (user) => {
      if(!user){
        location.replace("/login.html?nocache=" + Date.now());
        return;
      }
      me = user;

      await loadRecent();

      const lastRow = Array.from(list.querySelectorAll(".msgRow")).pop();
      currentLastId = lastRow ? (parseInt(lastRow.dataset.msgid||"0",10) || 0) : (currentLastId || 0);

      requestAnimationFrame(updateReplyOffset);

      startPresence();
      startStreamClean(currentLastId);
    });

    btnLogout.addEventListener("click", async () => {
      try{ await signOut(auth); }catch(e){}
      stopPresence();
      stopStream();
      location.replace("/login.html?nocache=" + Date.now());
    });
  };

  if(document.readyState === "loading"){
    document.addEventListener("DOMContentLoaded", run, {once:true});
  }else{
    run();
  }
})();