  // Artifact kinds whose `content` is a full HTML document and must render in
  // an iframe (srcDoc) rather than through the Markdown renderer. Mines reports
  // emit self-contained HTML under their own kind names, so they belong here.
  const HTML_ARTIFACT_KINDS = new Set([
    'html', 'document', 'mines_investor_presentation', 'mines_project_package',
  ]);

  export function isHtmlArtifact(kind) {
    return HTML_ARTIFACT_KINDS.has(kind);
  }

  export function wrapArtifactContent(html) {
    // Default link target = _self so links stay within the artifact iframe
    // instead of escaping to a new tab. Same-origin links (which would load
    // the parent app inside the iframe and crash hydration) are blocked.
    const guard = `<base target="_self"><script>(function(){
  function classify(url){
    // Returns: 'anchor' | 'external' | 'internal' | 'ignore'
    if (!url) return 'ignore';
    var s = String(url).trim();
    if (!s) return 'ignore';
    var lower = s.toLowerCase();
    if (lower === '#' || lower.startsWith('javascript:') || lower.startsWith('data:') || lower.startsWith('blob:')) return 'ignore';
    if (s.startsWith('#')) return 'anchor';
    if (lower.startsWith('mailto:') || lower.startsWith('tel:')) return 'external';
    var parentOrigin = '';
    try { if (window.top && window.top.location) parentOrigin = window.top.location.origin; } catch(e){}
    try {
      var u = new URL(s, window.location.href);
      // Anything resolving to the parent app's origin would load madarX inside the artifact.
      if (parentOrigin && u.origin === parentOrigin) return 'internal';
      if (/^https?:$/i.test(u.protocol)) return 'external';
    } catch(e){}
    // Relative paths like "about" or "/foo" resolve against the iframe (parent origin) — block.
    if (/^(\\/|\\.|[a-z0-9_-]+\\/?$)/i.test(s)) return 'internal';
    return 'ignore';
  }
  function scrollToAnchor(href){
    var id = href.replace(/^#/, '');
    if (!id) return false;
    var el = document.getElementById(id);
    if (!el) {
      try {
        var safeId = window.CSS && CSS.escape ? CSS.escape(id) : id.replace(/["\\\\]/g, '\\\\$&');
        el = document.querySelector('[name="' + safeId + '"]');
      } catch(e){}
    }
    if (el) { el.scrollIntoView({behavior:'smooth'}); return true; }
    return false;
  }

  function postToParent(payload){
    try {
      if (window.parent && window.parent !== window) {
        window.parent.postMessage(payload, '*');
        return true;
      }
    } catch(e){}
    return false;
  }

  // madarX action bridge: elements marked with data-madarx-action="prompt" (and a
  // data-prompt="…" payload) forward to the parent dashboard which sends the prompt
  // into the chat as if the user typed it. Also recognises href="madarx:prompt:…".
  function handleMadarxAction(target){
    if (!target || !target.closest) return false;
    var el = target.closest('[data-madarx-action], a[href^="madarx:"], a[href^="madarx://"]');
    if (!el) return false;
    var action = el.getAttribute('data-madarx-action') || '';
    var payload = el.getAttribute('data-prompt') || el.getAttribute('data-madarx-prompt') || '';
    if (!action) {
      var href = el.getAttribute('href') || '';
      // Accept "madarx:prompt:<text>" or "madarx://prompt?text=<text>"
      var m = href.match(/^madarx:(?:\\/\\/)?([a-z_-]+)[:?](.*)$/i);
      if (m) {
        action = m[1];
        if (m[2].startsWith('text=')) {
          try { payload = decodeURIComponent(m[2].slice(5)); } catch(e){ payload = m[2].slice(5); }
        } else {
          try { payload = decodeURIComponent(m[2]); } catch(e){ payload = m[2]; }
        }
      }
    }
    if (action === 'prompt' && payload) {
      postToParent({ type: 'madarx:prompt', text: String(payload) });
      return true;
    }
    return false;
  }

  // Fallback for artifacts generated before the data-madarx-action contract
  // existed: any CTA-styled button or anchor with no real href (href="#" or
  // empty) is treated as a confirmation of whatever the artifact just proposed.
  // The button's visible text is sent into the chat — the prior turn's
  // proposal is in context, so the LLM knows what "yes, X" means.
  function handleCtaFallback(target){
    if (!target || !target.closest) return false;
    var el = target.closest('button, a, [role="button"]');
    if (!el) return false;
    if (el.hasAttribute('data-madarx-action')) return false;
    if (el.tagName === 'A') {
      var h = (el.getAttribute('href') || '').trim();
      if (h && h !== '#' && !/^javascript:/i.test(h)) return false;
    }
    var cls = ((el.className && el.className.baseVal) || el.className || '').toString().toLowerCase();
    var looksLikeCta = /\\b(cta|btn|button|primary|action|confirm|create|submit)\\b/.test(cls);
    if (!looksLikeCta) return false;
    // Strip trailing arrows/glyphs the LLM tends to add ("→", "›", "»").
    var txt = (el.textContent || '').replace(/[\\u2190-\\u21FF\\u2794\\u279C\\u279E\\u27A1]/g, '').replace(/\\s+/g, ' ').trim();
    if (txt.length < 3 || txt.length > 100) return false;
    postToParent({ type: 'madarx:prompt', text: 'Yes — ' + txt.toLowerCase() });
    return true;
  }

  // 1) Link clicks. madarX action elements forward to the parent; anchors scroll
  // inside the iframe; external links navigate the iframe itself (no new tab);
  // same-origin/relative links are ignored so the parent app is never loaded
  // inside the artifact.
  document.addEventListener('click', function(ev){
    if (handleMadarxAction(ev.target)) {
      ev.preventDefault();
      ev.stopImmediatePropagation();
      return;
    }
    if (handleCtaFallback(ev.target)) {
      ev.preventDefault();
      ev.stopImmediatePropagation();
      return;
    }
    var a = ev.target && ev.target.closest ? ev.target.closest('a[href]') : null;
    if (!a) return;
    var href = a.getAttribute('href') || '';
    var kind = classify(href);
    if (kind === 'anchor') {
      ev.preventDefault();
      ev.stopImmediatePropagation();
      scrollToAnchor(href);
      return;
    }
    if (kind === 'external') {
      ev.preventDefault();
      ev.stopImmediatePropagation();
      try { window.location.href = href; } catch(e){}
      return;
    }
    // 'internal' or 'ignore' — block navigation entirely.
    ev.preventDefault();
    ev.stopImmediatePropagation();
  }, true);

  // 2) Forms — only submit when the action is a real external URL.
  document.addEventListener('submit', function(ev){
    var f = ev.target;
    var act = f && f.getAttribute ? f.getAttribute('action') : '';
    var kind = classify(act || '');
    if (kind === 'external') {
      ev.preventDefault();
      ev.stopImmediatePropagation();
      try { window.location.href = act; } catch(e){}
    } else {
      ev.preventDefault();
      ev.stopImmediatePropagation();
    }
  }, true);

  // 3) Window.open / location overrides — only honor true external URLs.
  try {
    window.open = function(u){
      if (classify(u) === 'external') { try { window.location.href = u; } catch(e){} }
      return null;
    };
  } catch(e){}
})();<\/script>`;
    if (!html) return guard;
    // Inject at the START of <head> so the guard runs before any page script
    // can read window.open / window.location and cache the originals.
    const headOpen = html.match(/<head[^>]*>/i);
    if (headOpen) {
      const pos = headOpen.index + headOpen[0].length;
      return html.slice(0, pos) + guard + html.slice(pos);
    }
    const bodyIdx = html.indexOf('<body');
    if (bodyIdx !== -1) return html.slice(0, bodyIdx) + guard + html.slice(bodyIdx);
    return guard + html;
  }

  export function fixIframe(e) {
    const iframe = e.target;
    try {
      const doc = iframe.contentDocument;
      if (!doc?.body) return;

      // Contain scroll inside the iframe so wheel momentum does not chain to
      // the canvas behind the artifact.
      doc.documentElement.style.overscrollBehavior = 'none';
      doc.body.style.overscrollBehavior = 'contain';

      // Broad CSS override: covers every known scroll-reveal hidden-initial pattern.
      // Targets attribute-based libs (AOS, sal, ScrollReveal) and common class conventions.
      // Scope is class/data-attribute based — never blanket `section > *` or `main > *`,
      // because that breaks slide-deck transforms and active-class toggling.
      const s = doc.createElement('style');
      s.textContent = `
        html, body {
          overscroll-behavior: none !important;
        }
        /* The dashboard artifact window already renders the title in its chrome
           bar, so the document's own title heading would show it twice. Hide the
           in-document title here only — standalone/raw/download views keep it. */
        main.doc > header.doc-header > h1.doc-title {
          display: none !important;
        }
        [data-aos],[data-sal],[data-scroll],[data-animate],[data-animation],
        [data-wow],[data-sr],[data-gsap],
        .fade-in,.fade-up,.fade-down,.fade-left,.fade-right,
        .slide-in,.slide-up,.slide-down,.slide-left,.slide-right,
        .scroll-reveal,.reveal-item,.animate,.animated-item,
        .scroll-hidden,.hidden-until-visible,.will-animate,
        .hero__content,.hero-content,.section-content {
          opacity: 1 !important;
          transform: none !important;
          visibility: visible !important;
          transition: none !important;
          animation: none !important;
        }
      `;
      doc.head?.appendChild(s);

      // Runtime scan: forcibly reveal any element that computed to opacity:0 after load.
      // Runs twice — once immediately, once after any deferred scripts finish.
      // Slide-deck / carousel containers are skipped — those elements legitimately use
      // opacity/visibility to hide inactive slides, and forcing them on stacks every slide.
      const SLIDE_LIKE = '[class*="slide" i],[class*="carousel" i],[class*="deck" i],[class*="presentation" i],[data-slide],[data-page-index],[role="region"][aria-roledescription="slide"],.swiper,.slick,.glide,.flickity,.reveal,.tab-panel,[role="tabpanel"],[role="dialog"]';
      function revealHidden() {
        const view = doc.defaultView;
        if (!view) return;
        doc.querySelectorAll('body *').forEach(el => {
          if (el.closest && el.closest(SLIDE_LIKE)) return;
          const cs = view.getComputedStyle(el);
          if (cs.opacity === '0') el.style.setProperty('opacity', '1', 'important');
          if (cs.visibility === 'hidden' && el.tagName !== 'INPUT' && el.tagName !== 'SELECT') {
            el.style.setProperty('visibility', 'visible', 'important');
          }
        });
      }
      revealHidden();
      setTimeout(revealHidden, 300);

      if (!doc.__madarxWheelContained) {
        doc.__madarxWheelContained = true;
        // passive:true lets the compositor scroll natively without waiting for JS.
        // overscroll-behavior:none (set above on html/body) prevents wheel events
        // from chaining to the canvas-board when the iframe hits its scroll boundary.
        doc.addEventListener('wheel', e => e.stopPropagation(), { capture: true, passive: true });
      }
      // Navigation interception lives in wrapArtifactContent's injected guard
      // so it runs before any page scripts and can override window.open/location.
    } catch { /* cross-origin or not yet ready */ }
  }

  export function containArtifactWheel(e) {
    e.stopPropagation();
  }

  // Capture the iframe's contentWindow synchronously at mount, BEFORE the
  // `load` event — srcdoc inline scripts run during parsing and can call
  // `postMessage` before `load` fires. Registering at ref-attach beats the race.
  export function registerIframeRef(register) {
    return (el) => {
      if (!el || typeof register !== 'function') return;
      try { register(el.contentWindow); } catch { /* ignore */ }
    };
  }

  export function scrollInactiveIframe(e) {
    const iframe = e.currentTarget.parentElement?.querySelector('iframe');
    if (!iframe) return;

    try {
      const { dx, dy } = normalizeWheelDelta(e, iframe);
      const moved = scrollIframeBy(iframe, [], dx, dy);
      if (moved || iframeDocumentCanScroll(iframe)) {
        e.preventDefault();
        e.stopPropagation();
      }
    } catch {
      // If an artifact ever becomes cross-origin, the next wheel after click
      // will fall back to the browser's native iframe scrolling.
    }
  }

  export function normalizeWheelDelta(e, iframe) {
    const modeMultiplier = e.deltaMode === 1
      ? 16
      : e.deltaMode === 2
        ? iframe.clientHeight
        : 1;
    return {
      dx: e.deltaX * modeMultiplier,
      dy: e.deltaY * modeMultiplier,
    };
  }

  export function iframeDocumentCanScroll(iframe) {
    const doc = iframe.contentDocument;
    const root = doc?.scrollingElement || doc?.documentElement || doc?.body;
    return Boolean(root && (root.scrollHeight > root.clientHeight || root.scrollWidth > root.clientWidth));
  }

  export function scrollIframeBy(iframe, path, dx, dy) {
    const doc = iframe.contentDocument;
    const view = doc?.defaultView;
    if (!doc || !view) return false;

    const candidates = [
      ...path.filter(el => el && el.nodeType === 1),
      doc.scrollingElement,
      doc.documentElement,
      doc.body,
    ];

    for (const el of candidates) {
      if (!el || typeof el.scrollTop !== 'number') continue;
      if (!elementCanScroll(view, el, dx, dy, doc)) continue;
      if (scrollElementBy(el, dx, dy)) return true;
    }

    const root = doc.scrollingElement || doc.documentElement || doc.body;
    if (!root) return false;
    const beforeLeft = root.scrollLeft;
    const beforeTop = root.scrollTop;
    view.scrollBy({ left: dx, top: dy, behavior: 'auto' });
    return root.scrollLeft !== beforeLeft || root.scrollTop !== beforeTop;
  }

  export function elementCanScroll(view, el, dx, dy, doc) {
    if (el === doc.scrollingElement || el === doc.documentElement || el === doc.body) {
      return el.scrollHeight > el.clientHeight || el.scrollWidth > el.clientWidth;
    }
    const style = view.getComputedStyle(el);
    const canY = /(auto|scroll|overlay)/.test(style.overflowY) && el.scrollHeight > el.clientHeight;
    const canX = /(auto|scroll|overlay)/.test(style.overflowX) && el.scrollWidth > el.clientWidth;
    const wantsY = dy !== 0 && canY;
    const wantsX = dx !== 0 && canX;
    return wantsY || wantsX;
  }

  export function scrollElementBy(el, dx, dy) {
    const beforeLeft = el.scrollLeft;
    const beforeTop = el.scrollTop;
    el.scrollLeft += dx;
    el.scrollTop += dy;
    return el.scrollLeft !== beforeLeft || el.scrollTop !== beforeTop;

}
