/**
 * meSEO Site Tag — UX signals, RUM, SEO monitor, content intelligence.
 * Rage/dead clicks, hesitation, tab visibility, Core Web Vitals, JS
 * errors, slow/3P resources, SEO snapshot, copy/outbound/search/downloads.
 */
(function () {
  "use strict";
  function init(cfg) {
    if (!cfg) return;
    var pageLoad = Date.now();
    var eventEp = cfg.eventEndpoint, rumEp = cfg.rumEndpoint || cfg.eventEndpoint, seoEp = cfg.seoEndpoint;
    function ctx() { try { return (window.meSEOAttribution && window.meSEOAttribution.getContext()) || {}; } catch (e) { return {}; } }
    function post(url, payload) {
      if (!url) return;
      var c = ctx();
      payload.siteId = cfg.siteId; payload.visitorId = c.visitorId || null; payload.sessionId = c.sessionId || null;
      payload.pageUrl = window.location.href; payload.timestamp = new Date().toISOString();
      var body = JSON.stringify(payload);
      try { if (navigator.sendBeacon) { navigator.sendBeacon(url, new Blob([body], { type: "application/json" })); return; } } catch (e) {}
      try { fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: body, keepalive: true, credentials: "omit" }).catch(function () {}); } catch (e) {}
    }
    // Buffer UX events and flush as a single batched POST every 10 s,
    // on visibilitychange to hidden, and on beforeunload (sendBeacon).
    // This collapses the high-frequency dead_click / rage_click / slow_resource
    // stream from one POST per signal into one POST per flush window.
    var eventBuffer = [];

    function flushEvents(useBeacon) {
      if (!eventBuffer.length) return;
      var batch = eventBuffer.splice(0, eventBuffer.length);
      var c = ctx();
      var payload = {
        siteId: cfg.siteId,
        visitorId: c.visitorId || null,
        sessionId: c.sessionId || null,
        pageUrl: window.location.href,
        timestamp: new Date().toISOString(),
        events: batch,
      };
      var body = JSON.stringify(payload);
      if (useBeacon) {
        try { if (navigator.sendBeacon) { navigator.sendBeacon(eventEp, new Blob([body], { type: "application/json" })); return; } } catch (e) {}
      }
      try { fetch(eventEp, { method: "POST", headers: { "Content-Type": "application/json" }, body: body, keepalive: true, credentials: "omit" }).catch(function () {}); } catch (e) {}
    }

    function event(type, data) {
      eventBuffer.push({ eventType: type, eventData: data });
      // Flush immediately when the buffer is full so a high-activity page
      // never silently drops events waiting for the 30s timer.
      if (eventBuffer.length >= 200) flushEvents(false);
    }

    setInterval(function () { flushEvents(false); }, 30000);

    // ── UX: rage / dead clicks ─────────────────────────────────────────
    // dead_click emits only when 2+ non-interactive clicks land within
    // DEAD_CLICK_RADIUS_PX of each other within DEAD_CLICK_WINDOW_MS.
    // Isolated non-interactive clicks are silently dropped -- they don't
    // indicate frustration, just casual browsing. Each emitted event
    // carries count + cluster centre, making it a richer frustration
    // signal than a raw per-click event at a fraction of the volume.
    var lastClicks = [];
    var DEAD_CLICK_WINDOW_MS = 3000;
    var DEAD_CLICK_RADIUS_PX = 40;
    var deadClickPending = [];
    document.addEventListener("click", function (e) {
      var now = Date.now(), el = e.target;
      lastClicks.push({ t: now, el: el }); if (lastClicks.length > 5) lastClicks.shift();
      if (lastClicks.filter(function (c) { return now - c.t < 500 && c.el === el; }).length >= 3) {
        event("rage_click", { tag: (el.tagName || "").toLowerCase(), id: el.id || null, text: (el.innerText || "").slice(0, 80) });
        lastClicks = [];
      }
      var dTag = (el.tagName || "").toLowerCase();
      if (["div", "span", "p", "img", "section"].indexOf(dTag) >= 0) {
        var p = el, interactive = false;
        while (p && p !== document.body) {
          var pTag = (p.tagName || "").toLowerCase();
          if (pTag === "a" || pTag === "button" || p.onclick || p.getAttribute("role") === "button") { interactive = true; break; }
          p = p.parentElement;
        }
        if (!interactive) {
          var dCx = e.clientX || 0, dCy = e.clientY || 0;
          var dId = el.id || null, dText = (el.innerText || "").slice(0, 80);
          // Prune stale entries outside the rolling window
          var pruned = [], di;
          for (di = 0; di < deadClickPending.length; di++) {
            if (now - deadClickPending[di].t < DEAD_CLICK_WINDOW_MS) pruned.push(deadClickPending[di]);
          }
          deadClickPending = pruned;
          // Find existing pending entries within the dead zone radius
          var nearby = [], ni;
          for (ni = 0; ni < deadClickPending.length; ni++) {
            var ndx = deadClickPending[ni].x - dCx, ndy = deadClickPending[ni].y - dCy;
            if (ndx * ndx + ndy * ndy <= DEAD_CLICK_RADIUS_PX * DEAD_CLICK_RADIUS_PX) nearby.push(deadClickPending[ni]);
          }
          // Always record this click so it can pair with a future click
          deadClickPending.push({ t: now, x: dCx, y: dCy });
          // Emit only when this click forms a 2+ cluster with nearby pending entries
          if (nearby.length >= 1) {
            var clusterSize = nearby.length + 1;
            var sumX = dCx, sumY = dCy, ci;
            for (ci = 0; ci < nearby.length; ci++) { sumX += nearby[ci].x; sumY += nearby[ci].y; }
            event("dead_click", {
              count: clusterSize,
              x: Math.round(sumX / clusterSize),
              y: Math.round(sumY / clusterSize),
              tag: dTag,
              id: dId,
              text: dText,
            });
            // Clear all pending entries within the cluster zone
            var remaining = [], ri;
            for (ri = 0; ri < deadClickPending.length; ri++) {
              var rdx = deadClickPending[ri].x - dCx, rdy = deadClickPending[ri].y - dCy;
              if (rdx * rdx + rdy * rdy > DEAD_CLICK_RADIUS_PX * DEAD_CLICK_RADIUS_PX) remaining.push(deadClickPending[ri]);
            }
            deadClickPending = remaining;
          }
        }
      }
    }, { passive: true, capture: true });

    // ── UX: hesitation (≥3s hover on a/button w/o click) ───────────────
    var hoverTimers = new WeakMap();
    document.addEventListener("mouseenter", function (e) {
      var el = e.target; if (!el.tagName) return;
      var tag = el.tagName.toLowerCase(); if (tag !== "a" && tag !== "button") return;
      hoverTimers.set(el, setTimeout(function () { event("hesitation", { tag: tag, text: (el.innerText || "").slice(0, 80), href: el.href || null }); }, 3000));
    }, true);
    document.addEventListener("mouseleave", function (e) { var tm = hoverTimers.get(e.target); if (tm) { clearTimeout(tm); hoverTimers.delete(e.target); } }, true);

    // ── Tab visibility: actual active time vs background ──────────────
    var activeStart = Date.now(), activeMs = 0;
    document.addEventListener("visibilitychange", function () {
      if (document.visibilityState === "hidden") {
        activeMs += Date.now() - activeStart;
        flushEvents(true);
      } else {
        activeStart = Date.now();
      }
    });

    // ── RUM: Core Web Vitals + errors + slow/3P resources ──────────────
    var rum = { lcp: null, cls: 0, inp: null, ttfb: null };
    try { var nav = performance.getEntriesByType && performance.getEntriesByType("navigation")[0]; if (nav) rum.ttfb = nav.responseStart - nav.requestStart; } catch (e) {}
    function obs(type, cb) { try { new PerformanceObserver(cb).observe({ type: type, buffered: true }); } catch (e) {} }
    obs("largest-contentful-paint", function (l) { var es = l.getEntries(); if (es.length) rum.lcp = es[es.length - 1].startTime; });
    obs("layout-shift", function (l) { l.getEntries().forEach(function (e) { if (!e.hadRecentInput) rum.cls += e.value; }); });
    obs("event", function (l) { l.getEntries().forEach(function (e) { if (e.duration > (rum.inp || 0)) rum.inp = e.duration; }); });

    // ── JS Error dedupe + rate limiter ────────────────────────────────
    // Per-session map keyed by (message::filename::line) tuple.
    // First CAPTURE_BUDGET occurrences are sent normally.
    // Further occurrences are counted in-memory and flushed every
    // FLUSH_INTERVAL_MS as an "error_count_update" event so total
    // volume is still recorded without a SiteEvent row per occurrence.
    // HARD_RATE_LIMIT_PER_SECOND caps total sends across all errors
    // to prevent runaway loops from exhausting the connection pool.
    var CAPTURE_BUDGET = 5;
    var HARD_RATE_PER_SEC = 10;
    var FLUSH_INTERVAL_MS = 30000;
    var errDedupeMap = {};  // { tupleKey: { captureCount, sentCount, msg, file, ln, lastFlushAt } }
    var rateWindow = { ts: 0, count: 0 };

    function errTupleKey(msg, file, ln) {
      return (msg || "").slice(0, 100) + "::" + (file || "") + "::" + (ln || "");
    }

    function rateLimitOk() {
      var now = Date.now();
      if (now - rateWindow.ts > 1000) { rateWindow.ts = now; rateWindow.count = 0; }
      if (rateWindow.count >= HARD_RATE_PER_SEC) return false;
      rateWindow.count++;
      return true;
    }

    function trackJsError(msg, file, ln, isPromise) {
      var key = errTupleKey(msg, file, ln);
      var entry = errDedupeMap[key];
      if (!entry) {
        entry = { captureCount: 1, sentCount: 0, msg: msg, file: file, ln: ln, lastFlushAt: Date.now() };
        errDedupeMap[key] = entry;
      } else {
        entry.captureCount++;
      }
      if (entry.sentCount < CAPTURE_BUDGET && rateLimitOk()) {
        entry.sentCount++;
        var data = { message: msg, filename: file, line: ln, occurrenceCount: entry.captureCount };
        if (isPromise) data.type = "promise";
        event("js_error", data);
      }
    }

    function flushErrCounts(useBeacon) {
      var now = Date.now();
      for (var key in errDedupeMap) {
        var entry = errDedupeMap[key];
        var delta = entry.captureCount - entry.sentCount;
        if (delta <= 0) continue;
        var payload = {
          eventType: "error_count_update",
          eventData: {
            tupleKey: key,
            message: entry.msg,
            filename: entry.file,
            line: entry.ln,
            deltaCount: delta,
            windowMs: now - entry.lastFlushAt
          }
        };
        payload.siteId = cfg.siteId;
        payload.pageUrl = window.location.href;
        payload.timestamp = new Date().toISOString();
        payload.visitorId = null; payload.sessionId = null;
        try { var c = ctx(); payload.visitorId = c.visitorId || null; payload.sessionId = c.sessionId || null; } catch (ex) {}
        var body = JSON.stringify(payload);
        if (useBeacon && navigator.sendBeacon) {
          try { navigator.sendBeacon(eventEp, new Blob([body], { type: "application/json" })); } catch (ex) {}
        } else {
          try { fetch(eventEp, { method: "POST", headers: { "Content-Type": "application/json" }, body: body, keepalive: true, credentials: "omit" }).catch(function () {}); } catch (ex) {}
        }
        entry.sentCount += delta;
        entry.lastFlushAt = now;
      }
    }

    window.addEventListener("error", function (e) {
      trackJsError((e.message || "").slice(0, 300), e.filename || null, e.lineno || null, false);
    });
    window.addEventListener("unhandledrejection", function (e) {
      trackJsError(String((e.reason && e.reason.message) || e.reason || "").slice(0, 300), null, null, true);
    });

    var errFlushTimer = setInterval(function () { flushErrCounts(false); }, FLUSH_INTERVAL_MS);
    document.addEventListener("visibilitychange", function () {
      if (document.visibilityState === "hidden") flushErrCounts(true);
    });
    obs("resource", function (l) {
      var tp = {};
      l.getEntries().forEach(function (e) {
        if (e.duration > 2000) event("slow_resource", { url: e.name, duration: Math.round(e.duration), size: e.transferSize || 0 });
        try { var h = new URL(e.name).hostname; if (h !== window.location.hostname) { tp[h] = tp[h] || { duration: 0, bytes: 0, count: 0 }; tp[h].duration += e.duration; tp[h].bytes += e.transferSize || 0; tp[h].count++; } } catch (err) {}
      });
      for (var h in tp) if (tp[h].duration > 100) event("third_party_impact", { host: h, duration: Math.round(tp[h].duration), bytes: tp[h].bytes, count: tp[h].count });
    });

    // ── SEO monitor — one snapshot per page load ───────────────────────
    function snapshot() {
      var meta = function (q) { var el = document.querySelector(q); return el ? (el.getAttribute("content") || el.getAttribute("href") || "") : null; };
      var h1s = document.querySelectorAll("h1"), og = {}, schemas = [];
      document.querySelectorAll('meta[property^="og:"]').forEach(function (m) { og[m.getAttribute("property")] = m.getAttribute("content"); });
      document.querySelectorAll('script[type="application/ld+json"]').forEach(function (s) { try { var j = JSON.parse(s.textContent); (Array.isArray(j) ? j : [j]).forEach(function (x) { if (x["@type"]) schemas.push(x["@type"]); }); } catch (e) {} });
      var robots = meta('meta[name="robots"]') || "";
      var mixed = location.protocol === "https:" && !!document.querySelector('img[src^="http:"], script[src^="http:"], link[href^="http:"]');
      post(seoEp, {
        title: document.title || null, metaDescription: meta('meta[name="description"]'), canonical: meta('link[rel="canonical"]'), robots: robots || null,
        h1Count: h1s.length, h1Texts: Array.prototype.slice.call(h1s, 0, 5).map(function (h) { return (h.innerText || "").slice(0, 200); }),
        ogTags: og, hasSchema: schemas.length > 0, schemaTypes: schemas,
        hreflang: Array.prototype.slice.call(document.querySelectorAll('link[rel="alternate"][hreflang]')).map(function (l) { return l.getAttribute("hreflang"); }),
        mixedContent: mixed, noindexed: /noindex/i.test(robots),
      });
    }
    if (document.readyState === "complete") snapshot(); else window.addEventListener("load", snapshot, { once: true });

    // ── Content intelligence ───────────────────────────────────────────
    document.addEventListener("copy", function () {
      var sel = window.getSelection && window.getSelection();
      var parent = sel && sel.anchorNode && (sel.anchorNode.parentElement || sel.anchorNode);
      var section = parent && parent.closest ? (parent.closest("section,article,main,nav,footer,header") || {}) : {};
      event("copy_event", { section: (section.tagName || "").toLowerCase(), id: section.id || null });
    }, true);
    // ── Outbound clicks ───────────────────────────────────────────────
    // Delegated click listener that walks up to the nearest <a>, checks
    // whether its href points off-domain, and fires an `outbound_click`
    // event. The per-URL cooldown prevents double-fires when a user
    // double-clicks a link or when React re-renders the anchor under the
    // cursor. post() inside event() already uses sendBeacon / keepalive
    // fetch so the event survives page unloads (external navigations).

    // Subdomain handling: extract a "base domain" from a hostname so
    // www.example.com and portal.example.com share the same base. For
    // standard TLDs (.com, .org, .net) we take the last 2 segments; for
    // country-code TLDs (.co.uk, .com.au, .co.nz, .org.uk, etc.) we
    // take the last 3. The override `meseoConfig.internalDomains` lets
    // site owners explicitly list hostnames considered internal.
    var CC_TLDS = /\.(co|com|org|net|ac|gov|edu)\.[a-z]{2}$/i;
    function baseDomain(hostname) {
      var parts = hostname.split(".");
      if (parts.length <= 2) return hostname.toLowerCase();
      // country-code compound TLDs → keep last 3 segments
      if (CC_TLDS.test(hostname)) return parts.slice(-3).join(".").toLowerCase();
      return parts.slice(-2).join(".").toLowerCase();
    }
    var internalDomains = (window.meseoConfig && Array.isArray(window.meseoConfig.internalDomains))
      ? window.meseoConfig.internalDomains.map(function (d) { return d.toLowerCase(); })
      : null;
    var selfBase = baseDomain(window.location.hostname);

    function isInternalHost(hostname) {
      // Exact match — covers the simplest same-host case.
      if (hostname === window.location.hostname) return true;
      // Explicit override: site owner listed specific internal domains.
      if (internalDomains) {
        var lower = hostname.toLowerCase();
        for (var i = 0; i < internalDomains.length; i++) {
          if (lower === internalDomains[i] || lower.indexOf("." + internalDomains[i]) === lower.length - internalDomains[i].length - 1) return true;
        }
        return false;
      }
      // Default heuristic: same registrable base domain.
      return baseDomain(hostname) === selfBase;
    }

    var outboundCooldown = {}; // url -> lastFiredTs
    document.addEventListener("click", function (e) {
      var a = e.target; while (a && a.tagName !== "A") a = a.parentElement;
      if (!a) return;
      var href = a.getAttribute("href") || "";
      // Exclude empty, fragment, mailto, tel, javascript, and data URIs.
      if (!href) return;
      var lower = href.toLowerCase();
      if (lower.charAt(0) === "#" || lower.indexOf("mailto:") === 0 ||
          lower.indexOf("tel:") === 0 || lower.indexOf("javascript:") === 0 ||
          lower.indexOf("data:") === 0) return;
      // Wrap the entire URL parse + logic in try-catch so a malformed href
      // (e.g. "//weird:///thing") can't break this listener for all future
      // clicks on the page. On error we silently skip the click.
      try {
        var resolved = a.href; // absolute-ified by the DOM
        var u = new URL(resolved);
        if (!u.hostname || isInternalHost(u.hostname)) {
          // Same-domain / internal; still check for resource downloads.
        } else {
          // Debounce per URL for 1 second.
          var now = Date.now();
          if (outboundCooldown[resolved] && now - outboundCooldown[resolved] < 1000) return;
          outboundCooldown[resolved] = now;
          // Prefer visible link text, fall back to aria-label, then title.
          var text = (a.innerText || a.getAttribute("aria-label") || a.getAttribute("title") || "").trim().slice(0, 100);
          event("outbound_click", { url: resolved, text: text, targetDomain: u.hostname });
        }
        if (/\.(pdf|docx?|xlsx?|pptx?|zip|csv|txt|rtf)(\?|#|$)/i.test(resolved)) {
          event("resource_download", { url: resolved, ext: (resolved.match(/\.([a-z]+)(?:\?|#|$)/i) || [])[1] || null });
        }
      } catch (err) { /* malformed URL — skip silently */ }
    }, true);
    document.addEventListener("submit", function (e) {
      var f = e.target; if (!f || f.tagName !== "FORM") return;
      for (var i = 0; i < f.elements.length; i++) {
        var el = f.elements[i];
        if (el.type === "search" || /^(q|query|search|s)$/i.test(el.name || "")) { event("search_query", { query: (el.value || "").slice(0, 120), formAction: f.action || null }); return; }
      }
    }, true);

    // ── Exit Intent (fire once per page) ─────────────────────────────
    var exitFired = false;
    document.addEventListener("mouseout", function (e) {
      if (!exitFired && e.clientY < 10 && e.relatedTarget === null) {
        exitFired = true;
        event("exit_intent", { timeOnPage: Math.round((Date.now() - pageLoad) / 1000), scrollDepth: Math.round(window.scrollY / Math.max(document.body.scrollHeight - window.innerHeight, 1) * 100) });
      }
    });

    // ── Video Engagement ──────────────────────────────────────────────
    var videoTracked = {};
    function trackVideo(type, id, action, ms, pct) {
      var key = type + ":" + id + ":" + action + (pct || "");
      if (videoTracked[key]) return; videoTracked[key] = true;
      event("video_engagement", { videoType: type, videoId: id, action: action, milestone: pct || null, watchTimeSeconds: Math.round((ms || 0) / 1000) });
    }
    // HTML5 <video>
    document.querySelectorAll("video").forEach(function (v, i) {
      var vid = v.id || "video_" + i;
      v.addEventListener("play", function () { trackVideo("html5", vid, "play", 0); });
      v.addEventListener("ended", function () { trackVideo("html5", vid, "complete", v.currentTime * 1000, 100); });
    });
    // Inject API params into YouTube/Vimeo iframes so postMessage works
    function enableVideoApi(iframe) {
      var src = iframe.src || "";
      if (src.indexOf("youtube.com/embed") !== -1 && src.indexOf("enablejsapi") === -1) {
        iframe.src = src + (src.indexOf("?") === -1 ? "?" : "&") + "enablejsapi=1";
      } else if (src.indexOf("player.vimeo.com") !== -1 && src.indexOf("api=") === -1) {
        iframe.src = src + (src.indexOf("?") === -1 ? "?" : "&") + "api=1";
      }
    }
    document.querySelectorAll('iframe[src*="youtube.com/embed"], iframe[src*="player.vimeo.com"]').forEach(enableVideoApi);
    // Watch for dynamically added video iframes
    var vidObs = new MutationObserver(function (muts) {
      muts.forEach(function (m) {
        m.addedNodes.forEach(function (n) {
          if (n.nodeType !== 1) return;
          if (n.tagName === "IFRAME") enableVideoApi(n);
          if (n.querySelectorAll) n.querySelectorAll('iframe[src*="youtube.com/embed"], iframe[src*="player.vimeo.com"]').forEach(enableVideoApi);
        });
      });
    });
    vidObs.observe(document.body, { childList: true, subtree: true });
    // YouTube/Vimeo iframes — listen for postMessage
    window.addEventListener("message", function (e) {
      try {
        var d = typeof e.data === "string" ? JSON.parse(e.data) : e.data;
        if (d && d.event === "onStateChange" && d.info !== undefined) {
          var vid = d.id || "yt"; var st = d.info;
          if (st === 1) trackVideo("youtube", vid, "play", 0);
          if (st === 0) trackVideo("youtube", vid, "complete", 0, 100);
        }
        // Vimeo
        if (d && d.event === "ready" && d.player_id) { /* vimeo ready */ }
        if (d && d.method === "play") trackVideo("vimeo", d.player_id || "vm", "play", 0);
        if (d && d.event === "finish") trackVideo("vimeo", d.player_id || "vm", "complete", 0, 100);
      } catch (ex) {}
    });

    // ── Chat Widget Interaction ──────────────────────────────────────
    var chatOpened = false;
    function chatEvent(platform, action) {
      if (action === "opened" && chatOpened) return;
      if (action === "opened") chatOpened = true;
      event("chat_interaction", { chatPlatform: platform, action: action });
    }
    // Detect common chat widgets
    try { if (window.drift) window.drift.on("chatOpen", function () { chatEvent("drift", "opened"); }); } catch (ex) {}
    try { if (window.Intercom) window.Intercom("onShow", function () { chatEvent("intercom", "opened"); }); } catch (ex) {}
    try { if (window.Tawk_API) { window.Tawk_API.onChatMaximized = function () { chatEvent("tawk", "opened"); }; } } catch (ex) {}
    try { if (window.zE) window.zE("messenger:on", "open", function () { chatEvent("zendesk", "opened"); }); } catch (ex) {}
    // Fallback: observe for common chat containers appearing
    var chatObs = new MutationObserver(function (muts) {
      for (var i = 0; i < muts.length; i++) {
        for (var j = 0; j < muts[i].addedNodes.length; j++) {
          var n = muts[i].addedNodes[j];
          if (n.id && /drift|intercom|tawk|hubspot-messages|livechat/i.test(n.id)) { chatEvent("detected", "opened"); chatObs.disconnect(); return; }
        }
      }
    });
    chatObs.observe(document.body, { childList: true, subtree: false });

    // ── CTA / Element Visibility ─────────────────────────────────────
    var ctaSels = 'a[href^="tel:"],a[href^="mailto:"],form,[class*="cta"],[class*="pricing"],[class*="booking"],[class*="appointment"],[class*="contact"],button[type="submit"],[id*="cta"],[data-cta]';
    try {
      var ctaObs = new IntersectionObserver(function (entries) {
        entries.forEach(function (entry) {
          if (entry.isIntersecting && !entry.target._meSEOSeen) {
            entry.target._meSEOSeen = true;
            var el = entry.target, tag = (el.tagName || "").toLowerCase();
            var type = el.href && el.href.indexOf("tel:") === 0 ? "phone" : el.href && el.href.indexOf("mailto:") === 0 ? "email" : tag === "form" ? "form" : "cta";
            // elementTag = raw HTML tag ("a", "button", "form", "input")
            //   so the UI can prefix "Button:", "Link:", "Form:".
            // elementLabel = aria-label/title if present, for CTAs like
            //   "Phone" icons where textContent is empty.
            var aria = (el.getAttribute && (el.getAttribute("aria-label") || el.getAttribute("title"))) || "";
            event("element_visible", {
              elementType: type,
              elementTag: tag,
              elementText: (el.textContent || "").trim().slice(0, 50),
              elementLabel: String(aria).trim().slice(0, 50),
              elementSelector: el.id ? "#" + el.id : tag + (el.className ? "." + (el.className + "").split(" ")[0] : ""),
              timeToSee: Math.round((Date.now() - pageLoad) / 1000),
            });
          }
        });
      }, { threshold: 0.5 });
      document.querySelectorAll(ctaSels).forEach(function (el) { ctaObs.observe(el); });
    } catch (ex) {}

    // ── HTTP Error Page Detection ──────────────────────────────────
    // Detect when the user has landed on a 404, 5xx, or soft-404 page.
    // Uses Navigation Timing API responseStatus (modern browsers) plus
    // heuristic title/body checks as fallback.
    (function detectHttpErrors() {
      var title = (document.title || "").toLowerCase();
      var statusCode = null;

      // 1. Navigation Timing API — most reliable source
      try {
        var nav = performance.getEntriesByType("navigation")[0];
        if (nav && nav.responseStatus >= 400) statusCode = nav.responseStatus;
      } catch (e) {}

      // 2. Title-based heuristics (fallback for older browsers)
      if (!statusCode) {
        var ERROR_TITLES = /\b(404|not found|page not found|p[aá]gina no encontrada)\b/i;
        var SERVER_TITLES = /\b(500|502|503|504|server error|internal error|service unavailable)\b/i;
        if (ERROR_TITLES.test(title)) statusCode = 404;
        else if (SERVER_TITLES.test(title)) statusCode = parseInt((title.match(/\b(50[0-4])\b/) || [])[1] || "500", 10);
      }

      // 3. Body text heuristics for 404
      if (!statusCode) {
        try {
          var main = document.querySelector("main") || document.body;
          var bodyText = (main.innerText || "").slice(0, 1000).toLowerCase();
          if (/page (doesn.t|does not) exist|page you.re looking for|no longer available|this page (has been|was) removed/.test(bodyText)) {
            statusCode = 404;
          }
        } catch (e) {}
      }

      if (statusCode && statusCode >= 400) {
        event("http_error", {
          statusCode: statusCode,
          referrer: document.referrer || null,
          title: (document.title || "").slice(0, 200)
        });
      }

      // 4. Soft 404 detection: 200 status but tiny content + no nav.
      // Excludes thank-you/confirmation pages which are thin by design.
      if (!statusCode) {
        try {
          var SUCCESS_PAGE_RE = /\b(thank you|thanks|gracias|submission received|appointment confirmed|confirmation|success|submitted|your request has been)\b/i;
          var mainEl = document.querySelector("main") || document.body;
          var text = (mainEl.innerText || "").replace(/\s+/g, " ").trim();
          var wordCount = text.split(/\s+/).length;
          var navLinks = document.querySelectorAll("nav a, .nav a, header a").length;
          var isSuccessPage = SUCCESS_PAGE_RE.test(text) || SUCCESS_PAGE_RE.test(title);
          if (wordCount < 50 && navLinks < 3 && !isSuccessPage) {
            event("soft_404", {
              wordCount: wordCount,
              referrer: document.referrer || null,
              title: (document.title || "").slice(0, 200)
            });
          }
        } catch (e) {}
      }

      // Store detection result for broken-link check below
      window.__meseoHttpStatus = statusCode;
    })();

    // ── Broken Link Detection ───────────────────────────────────────
    // On every internal link click, stash the source→target in
    // sessionStorage. On the next page load, if we landed on an error
    // page, fire a broken_link event that identifies the exact link.
    // All sessionStorage access is wrapped in try-catch because some
    // browsers in private/incognito mode throw SecurityError.
    (function detectBrokenLinks() {
      // Safe sessionStorage helpers — silently return null / no-op on error
      function ssGet(key) { try { return sessionStorage.getItem(key); } catch (e) { return null; } }
      function ssSet(key, val) { try { sessionStorage.setItem(key, val); } catch (e) {} }
      function ssRemove(key) { try { sessionStorage.removeItem(key); } catch (e) {} }

      // Check: did we arrive here via a tracked nav that hit an error?
      try {
        var raw = ssGet("meseo_nav");
        if (raw) {
          ssRemove("meseo_nav");
          var nav = JSON.parse(raw);
          var status = window.__meseoHttpStatus;
          if (status && status >= 400 && nav.to && nav.from) {
            event("broken_link", {
              sourceUrl: nav.from,
              targetUrl: nav.to,
              statusCode: status,
              linkText: (nav.text || "").slice(0, 100)
            });
          }
        }
      } catch (e) {}

      // Track internal link clicks for the NEXT page load
      document.addEventListener("click", function (e) {
        var a = e.target; while (a && a.tagName !== "A") a = a.parentElement;
        if (!a || !a.href) return;
        try {
          var u = new URL(a.href);
          if (u.hostname === window.location.hostname) {
            ssSet("meseo_nav", JSON.stringify({
              from: window.location.href,
              to: a.href,
              text: (a.innerText || a.getAttribute("aria-label") || "").trim().slice(0, 100),
              timestamp: Date.now()
            }));
          }
        } catch (e) {}
      }, true);
    })();

    // ── 301 Redirect Detection ────────────────────────────────────
    // Detect redirect chains users pass through. Two signals:
    // 1. Navigation API redirectCount > 0
    // 2. meseo_nav.to ≠ current location (clicked URL redirected)
    (function detectRedirects() {
      try {
        var nav = performance.getEntriesByType("navigation")[0];
        var redirectCount = (nav && nav.redirectCount) || 0;
        var redirectTime = (nav && nav.redirectEnd > 0 && nav.redirectStart > 0) ? Math.round(nav.redirectEnd - nav.redirectStart) : 0;

        // Check if the user was redirected from the link they clicked
        var raw = null;
        try { raw = sessionStorage.getItem("meseo_nav"); } catch (e) {}
        var navData = null;
        if (raw) {
          try { navData = JSON.parse(raw); } catch (e) {}
        }

        // URL mismatch: clicked one URL, ended up at another
        if (navData && navData.to) {
          try {
            var intended = new URL(navData.to);
            var actual = new URL(window.location.href);
            // Compare pathname (ignore trailing slash + query differences)
            var intendedPath = intended.pathname.replace(/\/$/, "");
            var actualPath = actual.pathname.replace(/\/$/, "");
            if (intended.hostname === actual.hostname && intendedPath !== actualPath) {
              event("redirect_detected", {
                originalUrl: navData.to,
                finalUrl: window.location.href,
                sourcePageUrl: navData.from || null,
                redirectCount: Math.max(redirectCount, 1),
                redirectTimeMs: redirectTime,
                isChain: redirectCount > 1 || redirectTime > 500
              });
              return; // Don't double-fire the generic redirect below
            }
          } catch (e) {}
        }

        // Generic redirect detected via Navigation API (no URL mismatch info)
        if (redirectCount > 0) {
          event("redirect_detected", {
            originalUrl: null,
            finalUrl: window.location.href,
            sourcePageUrl: document.referrer || null,
            redirectCount: redirectCount,
            redirectTimeMs: redirectTime,
            isChain: redirectCount > 1 || redirectTime > 500
          });
        }
      } catch (e) {}
    })();

    // ── Resource Error Detection (broken images, scripts, CSS) ──────
    // Capture phase listener for load errors on <img>, <script>, <link>.
    // Collects all errors during a 3-second window after page load, then
    // fires a single batched "resource_errors" event. This reduces event
    // volume 5-10x on pages with multiple broken resources.
    (function detectResourceErrors() {
      var batch = [];
      var seenUrls = {};
      var flushed = false;

      function flush() {
        if (flushed || batch.length === 0) return;
        flushed = true;
        event("resource_errors", { resources: batch });
      }

      window.addEventListener("error", function (e) {
        var el = e.target;
        if (!el || !el.tagName) return;
        var tag = el.tagName;
        if (tag !== "IMG" && tag !== "SCRIPT" && tag !== "LINK") return;
        var resourceUrl = el.src || el.href || "";
        if (!resourceUrl || seenUrls[resourceUrl]) return;
        seenUrls[resourceUrl] = true;
        batch.push({ type: tag.toLowerCase(), url: resourceUrl.slice(0, 500) });
        // If we've already flushed (late-loading resource), fire immediately
        if (flushed) {
          flushed = false;
          setTimeout(flush, 500);
        }
      }, true);

      // Flush 3 seconds after page load
      setTimeout(flush, 3000);
      // Also flush on page unload in case the timer hasn't fired yet
      window.addEventListener("beforeunload", flush);
    })();

    // ── Session Attribution (UTM + Click IDs, once per session) ──────
    if (!sessionStorage.getItem("meSEO_attributed")) {
      var sp = new URLSearchParams(window.location.search);
      var hasUTM = sp.get("utm_source") || sp.get("gclid") || sp.get("fbclid") || sp.get("msclkid");
      if (hasUTM || !sessionStorage.getItem("meSEO_sid_set")) {
        event("session_attribution", {
          utm_source: sp.get("utm_source"), utm_medium: sp.get("utm_medium"), utm_campaign: sp.get("utm_campaign"),
          utm_term: sp.get("utm_term"), utm_content: sp.get("utm_content"),
          gclid: sp.get("gclid"), fbclid: sp.get("fbclid"), msclkid: sp.get("msclkid"),
          referrer: document.referrer, landingPage: window.location.pathname
        });
        sessionStorage.setItem("meSEO_attributed", "1");
      }
    }

    // ── Final flush on unload: RUM + tab visibility totals ─────────────
    window.addEventListener("beforeunload", function () {
      clearInterval(errFlushTimer);
      flushErrCounts(true);
      if (document.visibilityState !== "hidden") activeMs += Date.now() - activeStart;
      event("tab_visibility", { activeMs: activeMs });
      flushEvents(true); // send buffered events (including tab_visibility) via sendBeacon
      post(rumEp, { eventType: "rum", lcp: rum.lcp, cls: rum.cls, inp: rum.inp, ttfb: rum.ttfb, device: /Mobi|Android/i.test(navigator.userAgent) ? "mobile" : "desktop", connection: (navigator.connection && navigator.connection.effectiveType) || null });
    }, { passive: true });
  }
  window.meSEOSignals = { init: init };
})();
