/** Shopify CDN: Minification failed

Line 16:0 Comments in CSS use "/* ... */" instead of "//"
Line 17:0 Comments in CSS use "/* ... */" instead of "//"
Line 18:0 Comments in CSS use "/* ... */" instead of "//"
Line 19:0 Comments in CSS use "/* ... */" instead of "//"
Line 20:0 Comments in CSS use "/* ... */" instead of "//"
Line 21:0 Comments in CSS use "/* ... */" instead of "//"
Line 23:0 Comments in CSS use "/* ... */" instead of "//"
Line 24:0 Comments in CSS use "/* ... */" instead of "//"
Line 25:0 Comments in CSS use "/* ... */" instead of "//"
Line 26:0 Comments in CSS use "/* ... */" instead of "//"
... and 85 more hidden warnings

**/
// ==UserScript==
// @name         Universal Block Copier v5.1 - Portable Site CSS + Exact Block HTML
// @namespace    local.universal.block.copier
// @version      5.1.0
// @description  Export an origin-isolated portable site CSS package, preserve ancestor selector context, then copy exact block HTML with assets from almost any website without DevTools.
// @match        http://*/*
// @match        https://*/*
// @grant        GM_setClipboard
// @grant        GM_xmlhttpRequest
// @grant        GM_registerMenuCommand
// @grant        GM_getValue
// @grant        GM_setValue
// @connect      *
// @noframes
// @run-at       document-idle
// ==/UserScript==

(function () {
  'use strict';

  // ============================================================
  // CONFIG
  // ============================================================

  const VERSION = '5.1.0';
  const WRAPPER_CLASS = 'ubc-source-design';
  const PANEL_ID = '__ubc_v51_panel__';
  const OVERLAY_ID = '__ubc_v51_overlay__';
  const MARKER_PREFIX = 'ubc-site-css-exported::';

  const MAX_IMPORT_DEPTH = 8;
  const MAX_FETCHED_STYLESHEETS = 400;
  const MAX_SITE_CSS_BYTES = 60 * 1024 * 1024;
  const MAX_CONTEXT_ANCESTORS = 14;
  const MAX_CONTEXT_ATTRIBUTE_VALUE = 800;
  const MAX_COMPUTED_CUSTOM_PROPERTIES = 1800;


  if (document.getElementById(PANEL_ID)) return;

  let selected = null;
  let picking = false;
  let cssJobRunning = false;
  let siteCSSCache = null;

  // ============================================================
  // GENERIC HELPERS
  // ============================================================

  const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));

  function byteSize(text) {
    try {
      return new Blob([text]).size;
    } catch {
      return String(text || '').length;
    }
  }

  function formatBytes(bytes) {
    if (bytes < 1024) return `${bytes} B`;
    if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
    return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
  }

  function safeName(value) {
    return String(value || 'site')
      .replace(/^www\./i, '')
      .replace(/[^a-z0-9._-]+/gi, '-')
      .replace(/-+/g, '-')
      .replace(/^-|-$/g, '') || 'site';
  }

  function stableHash(value) {
    // Small deterministic FNV-1a style hash. Used only to namespace CSS packages,
    // never for security.
    let h = 0x811c9dc5;
    const s = String(value || '');

    for (let i = 0; i < s.length; i++) {
      h ^= s.charCodeAt(i);
      h = Math.imul(h, 0x01000193);
    }

    return (h >>> 0).toString(36);
  }

  const SOURCE_KEY = `ubc-src-${stableHash(location.origin)}`;
  const SCOPE_SELECTOR = `.${WRAPPER_CLASS}.${SOURCE_KEY}`;
  const KEYFRAME_PREFIX = `ubc_${stableHash(location.origin)}_`;

  function nowStamp() {
    const d = new Date();
    const pad = n => String(n).padStart(2, '0');
    return (
      d.getFullYear() +
      pad(d.getMonth() + 1) +
      pad(d.getDate()) + '-' +
      pad(d.getHours()) +
      pad(d.getMinutes()) +
      pad(d.getSeconds())
    );
  }

  function absoluteURL(value, base = location.href) {
    if (!value) return value;
    const v = String(value).trim();

    if (
      v.startsWith('data:') ||
      v.startsWith('blob:') ||
      v.startsWith('#') ||
      v.startsWith('mailto:') ||
      v.startsWith('tel:')
    ) {
      return v;
    }

    if (v.startsWith('javascript:')) return '';

    try {
      return new URL(v, base).href;
    } catch {
      return value;
    }
  }

  function absoluteSrcset(srcset, base = location.href) {
    if (!srcset) return srcset;

    return String(srcset)
      .split(',')
      .map(part => {
        const pieces = part.trim().split(/\s+/);
        const url = pieces.shift();
        return [absoluteURL(url, base), ...pieces].join(' ');
      })
      .join(', ');
  }

  function absoluteCSSURLs(cssText, baseURL) {
    if (!cssText) return cssText;

    return String(cssText).replace(
      /url\(\s*(['"]?)(.*?)\1\s*\)/gi,
      function (_, quote, url) {
        if (
          !url ||
          url.startsWith('data:') ||
          url.startsWith('blob:') ||
          url.startsWith('#')
        ) {
          return `url(${quote}${url}${quote})`;
        }

        return `url("${absoluteURL(url, baseURL)}")`;
      }
    );
  }

  function copyText(text) {
    try {
      if (typeof GM_setClipboard === 'function') {
        GM_setClipboard(text, 'text');
        return true;
      }
    } catch {}

    try {
      navigator.clipboard.writeText(text);
      return true;
    } catch {}

    return false;
  }

  function downloadText(filename, text, mime = 'text/plain;charset=utf-8') {
    const blob = new Blob([text], { type: mime });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');

    a.href = url;
    a.download = filename;
    a.style.display = 'none';

    document.documentElement.appendChild(a);
    a.click();
    a.remove();

    setTimeout(() => URL.revokeObjectURL(url), 10000);
  }

  function describeElement(el) {
    if (!el) return 'none';

    let result = el.tagName ? el.tagName.toLowerCase() : 'node';

    if (el.id) result += `#${el.id}`;

    if (el.classList && el.classList.length) {
      result += '.' + Array.from(el.classList).slice(0, 4).join('.');
    }

    return result;
  }

  function markerKey() {
    return MARKER_PREFIX + location.origin;
  }

  function readExportMarker() {
    try {
      return GM_getValue(markerKey(), null);
    } catch {
      return null;
    }
  }

  function writeExportMarker(stats) {
    try {
      GM_setValue(markerKey(), {
        origin: location.origin,
        page: location.href,
        exportedAt: Date.now(),
        version: VERSION,
        ...stats
      });
    } catch {}
  }

  // ============================================================
  // NETWORK
  // ============================================================

  function decodeDataCSS(url) {
    try {
      if (!/^data:text\/css/i.test(url)) return '';
      const comma = url.indexOf(',');
      if (comma < 0) return '';

      const meta = url.slice(0, comma);
      const body = url.slice(comma + 1);
      return /;base64/i.test(meta) ? atob(body) : decodeURIComponent(body);
    } catch {
      return '';
    }
  }

  function requestText(url) {
    if (/^data:text\/css/i.test(url)) {
      return Promise.resolve({
        ok: true,
        status: 200,
        text: decodeDataCSS(url),
        finalUrl: url,
        error: ''
      });
    }

    return new Promise(resolve => {
      try {
        GM_xmlhttpRequest({
          method: 'GET',
          url,
          timeout: 30000,
          anonymous: false,
          overrideMimeType: 'text/css; charset=utf-8',

          onload(response) {
            const ok = response.status >= 200 && response.status < 400;
            resolve({
              ok,
              status: response.status,
              text: response.responseText || '',
              finalUrl: response.finalUrl || url,
              error: ok ? '' : `HTTP ${response.status}`
            });
          },

          onerror() {
            resolve({ ok: false, status: 0, text: '', finalUrl: url, error: 'network error' });
          },

          ontimeout() {
            resolve({ ok: false, status: 0, text: '', finalUrl: url, error: 'timeout' });
          }
        });
      } catch (error) {
        resolve({ ok: false, status: 0, text: '', finalUrl: url, error: String(error) });
      }
    });
  }

  // ============================================================
  // UI OVERLAY
  // ============================================================

  const overlay = document.createElement('div');
  overlay.id = OVERLAY_ID;
  overlay.style.cssText = `
    all: initial !important;
    position: fixed !important;
    display: none !important;
    pointer-events: none !important;
    z-index: 2147483646 !important;
    border: 3px solid #1787ff !important;
    background: rgba(23,135,255,.10) !important;
    box-sizing: border-box !important;
  `;
  document.documentElement.appendChild(overlay);

  function hideOverlay() {
    overlay.style.setProperty('display', 'none', 'important');
  }

  function drawOverlay(el) {
    if (!el || !el.getBoundingClientRect) {
      hideOverlay();
      return;
    }

    const r = el.getBoundingClientRect();

    overlay.style.setProperty('display', 'block', 'important');
    overlay.style.setProperty('left', `${r.left}px`, 'important');
    overlay.style.setProperty('top', `${r.top}px`, 'important');
    overlay.style.setProperty('width', `${r.width}px`, 'important');
    overlay.style.setProperty('height', `${r.height}px`, 'important');
  }

  // ============================================================
  // UI PANEL
  // ============================================================

  const panel = document.createElement('div');
  panel.id = PANEL_ID;
  panel.style.cssText = `
    all: initial !important;
    position: fixed !important;
    right: 18px !important;
    bottom: 18px !important;
    width: 330px !important;
    padding: 12px !important;
    z-index: 2147483647 !important;
    display: block !important;
    box-sizing: border-box !important;
    background: #151515 !important;
    color: #fff !important;
    border: 1px solid #444 !important;
    border-radius: 10px !important;
    box-shadow: 0 8px 30px rgba(0,0,0,.45) !important;
    font-family: Arial, sans-serif !important;
    font-size: 13px !important;
    line-height: 1.4 !important;
    direction: rtl !important;
  `;
  document.documentElement.appendChild(panel);

  function makeButton(text) {
    const button = document.createElement('button');
    button.textContent = text;
    button.style.cssText = `
      all: initial !important;
      display: block !important;
      box-sizing: border-box !important;
      width: 100% !important;
      padding: 8px 6px !important;
      margin: 0 !important;
      background: #303030 !important;
      color: #fff !important;
      border: 1px solid #4d4d4d !important;
      border-radius: 6px !important;
      font-family: Arial, sans-serif !important;
      font-size: 12px !important;
      text-align: center !important;
      cursor: pointer !important;
    `;

    button.addEventListener('mouseenter', () => {
      button.style.setProperty('filter', 'brightness(1.22)', 'important');
    });

    button.addEventListener('mouseleave', () => {
      button.style.setProperty('filter', 'none', 'important');
    });

    return button;
  }

  const title = document.createElement('div');
  title.textContent = 'Universal Block Copier v5.1';
  title.style.cssText = `
    all: initial !important;
    display: block !important;
    color: #fff !important;
    font: 700 14px/1.3 Arial, sans-serif !important;
    margin-bottom: 7px !important;
    direction: ltr !important;
  `;

  const workflow = document.createElement('div');
  workflow.textContent = '① Site CSS مرة واحدة  →  ② HTML لكل بلوك';
  workflow.style.cssText = `
    all: initial !important;
    display: block !important;
    color: #b9c1cc !important;
    font: 11px/1.4 Arial, sans-serif !important;
    margin-bottom: 8px !important;
    direction: rtl !important;
  `;

  const status = document.createElement('div');
  status.textContent = 'لم يتم اختيار بلوك';
  status.style.cssText = `
    all: initial !important;
    display: block !important;
    box-sizing: border-box !important;
    background: #222 !important;
    color: #ddd !important;
    padding: 7px !important;
    margin-bottom: 8px !important;
    border-radius: 6px !important;
    font-family: monospace !important;
    font-size: 11px !important;
    white-space: nowrap !important;
    overflow: hidden !important;
    text-overflow: ellipsis !important;
    direction: ltr !important;
  `;

  const grid = document.createElement('div');
  grid.style.cssText = `
    all: initial !important;
    display: grid !important;
    grid-template-columns: 1fr 1fr !important;
    gap: 6px !important;
  `;

  const message = document.createElement('div');
  message.style.cssText = `
    all: initial !important;
    display: block !important;
    min-height: 18px !important;
    margin-top: 8px !important;
    color: #9fd3ff !important;
    font: 11px/1.4 Arial, sans-serif !important;
    direction: rtl !important;
  `;

  panel.append(title, workflow, status, grid, message);

  const btnExportCSS = makeButton('① Export Site Design CSS');
  const btnPick = makeButton('🎯 اختيار بلوك');
  const btnSmart = makeButton('🧠 Smart Block ↑');
  const btnParent = makeButton('⬆ Parent');
  const btnCopyHTML = makeButton('② Copy Exact Block HTML');
  const btnText = makeButton('Text');
  const btnAssets = makeButton('Assets');
  const btnRawCSS = makeButton('Raw Full CSS');
  const btnCancel = makeButton('إلغاء التحديد');

  btnExportCSS.style.setProperty('grid-column', '1 / -1', 'important');
  btnExportCSS.style.setProperty('background', '#5c3b8a', 'important');
  btnPick.style.setProperty('grid-column', '1 / -1', 'important');
  btnPick.style.setProperty('background', '#1268b3', 'important');
  btnCopyHTML.style.setProperty('grid-column', '1 / -1', 'important');
  btnCopyHTML.style.setProperty('background', '#157347', 'important');

  grid.append(
    btnExportCSS,
    btnPick,
    btnSmart,
    btnParent,
    btnCopyHTML,
    btnText,
    btnAssets,
    btnRawCSS,
    btnCancel
  );

  function say(text, ms = 4500) {
    message.textContent = text;
    setTimeout(() => {
      if (message.textContent === text) message.textContent = '';
    }, ms);
  }

  function setBusy(button, busy, busyText) {
    if (!button) return;

    if (busy) {
      if (!button.dataset.ubcOldText) button.dataset.ubcOldText = button.textContent;
      button.textContent = busyText || 'Working...';
      button.disabled = true;
      button.style.setProperty('opacity', '.75', 'important');
      button.style.setProperty('cursor', 'wait', 'important');
    } else {
      button.textContent = button.dataset.ubcOldText || button.textContent;
      delete button.dataset.ubcOldText;
      button.disabled = false;
      button.style.setProperty('opacity', '1', 'important');
      button.style.setProperty('cursor', 'pointer', 'important');
    }
  }

  ['click', 'mousedown', 'mouseup', 'pointerdown', 'pointerup', 'contextmenu'].forEach(type => {
    panel.addEventListener(type, event => event.stopPropagation());
  });

  // ============================================================
  // SELECTION
  // ============================================================

  function selectElement(el) {
    if (!el || !(el instanceof Element)) return;
    if (el === panel || panel.contains(el) || el === overlay) return;
    if (el === document.documentElement || el === document.body) return;

    selected = el;

    const count = selected.querySelectorAll('*').length + 1;
    status.textContent = `${describeElement(selected)} | ${count} elements`;
    drawOverlay(selected);
  }

  btnPick.addEventListener('click', event => {
    event.preventDefault();
    event.stopPropagation();

    picking = !picking;

    if (picking) {
      btnPick.textContent = '✋ اضغط على البلوك المطلوب';
      say('مرّر الماوس فوق البلوك ثم اضغط عليه');
    } else {
      btnPick.textContent = '🎯 اختيار بلوك';
      if (selected) drawOverlay(selected);
    }
  });

  document.addEventListener('mousemove', event => {
    if (!picking) return;
    const el = event.target;
    if (!(el instanceof Element)) return;
    if (el === panel || panel.contains(el) || el === overlay) return;
    drawOverlay(el);
  }, true);

  document.addEventListener('click', event => {
    if (!picking) return;
    const el = event.target;
    if (!(el instanceof Element)) return;
    if (el === panel || panel.contains(el) || el === overlay) return;

    event.preventDefault();
    event.stopPropagation();
    event.stopImmediatePropagation();

    selectElement(el);
    picking = false;
    btnPick.textContent = '🎯 اختيار بلوك';
    say('تم اختيار العنصر');
  }, true);

  btnParent.addEventListener('click', () => {
    if (!selected) return say('اختر عنصرًا أولًا');

    const p = selected.parentElement;
    if (p && p !== document.body && p !== document.documentElement) {
      selectElement(p);
    }
  });

  function smartBlockFrom(el) {
    if (!el) return null;

    // E-commerce / CMS sections first.
    const cms = el.closest(
      '[id^="shopify-section-"], .shopify-section, [data-section-id], [data-section-type], ' +
      '[data-component], [data-block], [data-widget], [data-module]'
    );

    if (cms && cms !== document.body && cms !== document.documentElement) return cms;

    // Semantic block containers.
    const semantic = el.closest('section, article, [role="region"], main > div, aside');
    if (semantic && semantic !== document.body && semantic !== document.documentElement) return semantic;

    // Class/name heuristic.
    let node = el;
    let best = null;
    let bestScore = -Infinity;

    for (let depth = 0; depth < 8 && node && node !== document.body; depth++, node = node.parentElement) {
      const rect = node.getBoundingClientRect();
      if (!rect.width || !rect.height) continue;

      const identity = `${node.id || ''} ${Array.from(node.classList || []).join(' ')}`.toLowerCase();
      let score = 0;

      if (/section|component|module|widget|block|slider|carousel|review|testimonial|gallery|feature|benefit|faq|product/.test(identity)) score += 8;
      if (/wrapper|container|inner|content/.test(identity)) score += 2;
      if (node.id) score += 2;
      if (node.tagName === 'SECTION' || node.tagName === 'ARTICLE') score += 5;

      const viewportRatio = rect.width / Math.max(window.innerWidth, 1);
      if (viewportRatio > 0.45) score += 2;
      if (viewportRatio > 0.95) score -= 1;

      const descendants = node.querySelectorAll('*').length;
      if (descendants >= 4) score += 1;
      if (descendants > 1200) score -= 5;

      // Prefer a useful ancestor, not the exact tiny clicked element.
      score += Math.min(depth, 4) * 0.6;

      if (score > bestScore) {
        best = node;
        bestScore = score;
      }
    }

    return best || el.parentElement || el;
  }

  btnSmart.addEventListener('click', () => {
    if (!selected) return say('اختر عنصرًا أولًا');
    const smart = smartBlockFrom(selected);
    if (smart) {
      selectElement(smart);
      say('تم توسيع التحديد إلى أقرب بلوك منطقي');
    }
  });

  btnCancel.addEventListener('click', () => {
    selected = null;
    picking = false;
    hideOverlay();
    status.textContent = 'لم يتم اختيار بلوك';
    btnPick.textContent = '🎯 اختيار بلوك';
    say('تم إلغاء التحديد');
  });

  window.addEventListener('scroll', () => {
    if (selected && !picking) drawOverlay(selected);
  }, true);

  window.addEventListener('resize', () => {
    if (selected && !picking) drawOverlay(selected);
  });

  // ============================================================
  // BLOCK HTML EXPORT
  // ============================================================

  function sourceContextClasses() {
    // The origin-specific key prevents CSS exported from different source sites
    // from styling each other's copied blocks on the destination store.
    const classes = new Set([WRAPPER_CLASS, SOURCE_KEY]);

    for (const cls of document.documentElement.classList || []) {
      if (cls && cls.length < 100) classes.add(cls);
    }

    if (document.body) {
      for (const cls of document.body.classList || []) {
        if (cls && cls.length < 100) classes.add(cls);
      }
    }

    return Array.from(classes);
  }

  function copyInlineCustomProperties(source, target) {
    try {
      for (const prop of source.style || []) {
        if (!prop.startsWith('--')) continue;

        const value = source.style.getPropertyValue(prop);
        const priority = source.style.getPropertyPriority(prop);

        if (value && value.length < 4000) {
          target.style.setProperty(prop, value, priority);
        }
      }
    } catch {}
  }

  function applySourceContextToWrapper(wrapper, root) {
    const roots = [document.documentElement, document.body].filter(Boolean);

    // Preserve only selector-relevant root metadata. Page layout styles from
    // html/body are intentionally NOT copied onto the portable wrapper.
    for (const el of roots) {
      for (const attr of Array.from(el.attributes || [])) {
        const name = attr.name.toLowerCase();

        if (name === 'class' || name === 'id' || name === 'style') continue;

        if (
          name === 'lang' ||
          name === 'dir' ||
          name.startsWith('data-')
        ) {
          if (
            !wrapper.hasAttribute(attr.name) &&
            attr.value.length < MAX_CONTEXT_ATTRIBUTE_VALUE
          ) {
            wrapper.setAttribute(attr.name, attr.value);
          }
        }
      }

      copyInlineCustomProperties(el, wrapper);
    }

    // Snapshot inherited CSS custom properties as seen by the selected block.
    // This makes components far less dependent on the destination theme's :root.
    try {
      const style = getComputedStyle(root);
      let copied = 0;

      for (const prop of style) {
        if (!prop.startsWith('--')) continue;
        if (copied >= MAX_COMPUTED_CUSTOM_PROPERTIES) break;

        const value = style.getPropertyValue(prop).trim();
        if (!value || value.length >= 4000) continue;

        wrapper.style.setProperty(prop, value);
        copied++;
      }
    } catch {}

    try {
      wrapper.style.setProperty(
        '--ubc-source-root-font-size',
        getComputedStyle(document.documentElement).fontSize || '16px'
      );
    } catch {}
  }

  function collectAncestorContext(root) {
    const nearest = [];
    let node = root?.parentElement || null;

    while (
      node &&
      node !== document.body &&
      node !== document.documentElement &&
      nearest.length < MAX_CONTEXT_ANCESTORS
    ) {
      nearest.push(node);
      node = node.parentElement;
    }

    return nearest.reverse();
  }

  function contextTagName(source) {
    const tag = String(source?.tagName || 'DIV').toLowerCase();

    // Keep common structural/custom-element tags so tag-based ancestor
    // selectors still have a chance to match. Avoid special table/list/form
    // elements whose HTML parsing semantics could rearrange the copied DOM.
    if (
      ['div', 'section', 'main', 'article', 'header', 'footer', 'aside', 'nav'].includes(tag) ||
      tag.includes('-')
    ) {
      return tag;
    }

    return 'div';
  }

  function createContextShell(source, depth) {
    const shell = document.createElement(contextTagName(source));

    shell.setAttribute('data-ubc-context', String(depth));
    // display:contents keeps the ancestor selector context without recreating
    // the source page's outer layout/padding/grid around the selected block.
    shell.style.setProperty('display', 'contents', 'important');

    if (source.id && source.id.length < 220) {
      shell.id = source.id;
      shell.setAttribute('data-ubc-context-id', source.id);
    }

    for (const cls of source.classList || []) {
      if (cls && cls.length < 140) shell.classList.add(cls);
    }

    for (const attr of Array.from(source.attributes || [])) {
      const name = attr.name.toLowerCase();

      if (
        name === 'id' ||
        name === 'class' ||
        name === 'style' ||
        name === 'hidden' ||
        name === 'open'
      ) {
        continue;
      }

      if (
        name === 'lang' ||
        name === 'dir' ||
        name === 'role' ||
        name.startsWith('data-')
      ) {
        if (attr.value.length < MAX_CONTEXT_ATTRIBUTE_VALUE) {
          try {
            shell.setAttribute(attr.name, attr.value);
          } catch {}
        }
      }
    }

    copyInlineCustomProperties(source, shell);
    return shell;
  }

  function appendWithAncestorContext(wrapper, root, clone) {
    const ancestors = collectAncestorContext(root);
    let parent = wrapper;

    ancestors.forEach((ancestor, index) => {
      const shell = createContextShell(ancestor, index + 1);
      parent.appendChild(shell);
      parent = shell;
    });

    parent.appendChild(clone);
    return ancestors.length;
  }

  function detectBlockDependencies(root) {
    const found = new Set();
    const q = selector => {
      try {
        return root.matches?.(selector) || !!root.querySelector?.(selector);
      } catch {
        return false;
      }
    };

    if (q('.splide, [class*="splide__"]')) found.add('Splide JS/CSS');
    if (q('.swiper, .swiper-wrapper, .swiper-slide')) found.add('Swiper JS/CSS');
    if (q('.slick-slider, .slick-track, .slick-slide')) found.add('Slick JS/CSS');
    if (q('.flickity-enabled, .flickity-slider')) found.add('Flickity JS/CSS');
    if (q('.glide, [class*="glide__"]')) found.add('Glide JS/CSS');
    if (q('[class*="embla"], [data-embla]')) found.add('Embla/custom JS');

    const animationNames = new Set();
    const nodes = [root, ...Array.from(root.querySelectorAll('*')).slice(0, 1800)];

    for (const el of nodes) {
      try {
        const name = getComputedStyle(el).animationName;
        if (name && name !== 'none') {
          name.split(',').map(x => x.trim()).filter(Boolean).forEach(x => animationNames.add(x));
        }
      } catch {}
    }

    if (animationNames.size) {
      found.add(`CSS animation: ${Array.from(animationNames).slice(0, 8).join(', ')}`);
    }

    if (q('canvas')) found.add('Canvas snapshot');
    if (q('iframe')) found.add('iframe content may remain external');

    return Array.from(found);
  }

  function prepareBlockClone(root) {
    const clone = root.cloneNode(true);
    const originals = [root, ...root.querySelectorAll('*')];
    const copies = [clone, ...clone.querySelectorAll('*')];

    originals.forEach((src, index) => {
      const dst = copies[index];
      if (!dst) return;

      // Remove executable inline JS events only.
      Array.from(dst.attributes || []).forEach(attr => {
        if (/^on/i.test(attr.name)) dst.removeAttribute(attr.name);
      });

      // Preserve the actual image currently rendered.
      if (src instanceof HTMLImageElement && src.currentSrc) {
        dst.setAttribute('src', src.currentSrc);
        dst.setAttribute('loading', 'eager');
      }

      // Preserve current media source.
      if (
        (src instanceof HTMLVideoElement || src instanceof HTMLAudioElement) &&
        src.currentSrc
      ) {
        dst.setAttribute('src', src.currentSrc);
      }

      // Convert common relative URLs to absolute URLs.
      ['src', 'href', 'poster', 'action', 'formaction'].forEach(attr => {
        if (!dst.hasAttribute?.(attr)) return;
        const value = dst.getAttribute(attr);
        if (!value) return;

        const fixed = absoluteURL(value);
        if (fixed) dst.setAttribute(attr, fixed);
        else dst.removeAttribute(attr);
      });

      if (dst.hasAttribute?.('srcset')) {
        dst.setAttribute('srcset', absoluteSrcset(dst.getAttribute('srcset')));
      }

      // Common lazy-load attributes: keep them, but absolutize likely URLs.
      Array.from(dst.attributes || []).forEach(attr => {
        const name = attr.name.toLowerCase();
        if (!/^data-(src|lazy-src|original|image|bg|background|poster)/.test(name)) return;

        const value = attr.value;
        if (!value || /\s/.test(value) && !/^https?:/i.test(value)) return;

        const fixed = absoluteURL(value);
        if (fixed) dst.setAttribute(attr.name, fixed);
      });

      // Preserve live form state.
      if (src instanceof HTMLInputElement) {
        if (src.type !== 'file') dst.setAttribute('value', src.value);
        if (src.checked) dst.setAttribute('checked', '');
        else dst.removeAttribute('checked');
      }

      if (src instanceof HTMLTextAreaElement) dst.textContent = src.value;

      if (src instanceof HTMLSelectElement) {
        Array.from(dst.options).forEach((opt, i) => {
          if (src.options[i]?.selected) opt.setAttribute('selected', '');
          else opt.removeAttribute('selected');
        });
      }

      // Canvas cannot be cloned as pixels through outerHTML. Preserve a snapshot when possible.
      if (src instanceof HTMLCanvasElement && dst instanceof HTMLCanvasElement) {
        try {
          const img = document.createElement('img');
          img.src = src.toDataURL('image/png');
          img.width = src.width;
          img.height = src.height;
          img.alt = 'Canvas snapshot';
          dst.replaceWith(img);
        } catch {}
      }
    });

    // Site CSS is exported separately, so do not carry unscoped page CSS or source scripts
    // inside every block. This avoids leaking source-site rules into the destination theme.
    clone.querySelectorAll('script, style, link[rel~="stylesheet"]').forEach(node => node.remove());

    return clone;
  }

  function buildBlockHTML(root) {
    const clone = prepareBlockClone(root);
    const contextClasses = sourceContextClasses();
    const wrapper = document.createElement('div');

    wrapper.className = contextClasses.join(' ');
    wrapper.setAttribute('data-ubc-source-key', SOURCE_KEY);
    wrapper.setAttribute('data-ubc-source-origin', location.origin);
    wrapper.setAttribute('data-ubc-source-url', location.href);
    wrapper.setAttribute('data-ubc-version', VERSION);

    applySourceContextToWrapper(wrapper, root);

    const contextDepth = appendWithAncestorContext(wrapper, root, clone);
    const marker = readExportMarker();
    const dependencies = detectBlockDependencies(root);

    return `<!--
Universal Block Copier v${VERSION}
SOURCE: ${location.href}
SOURCE PACKAGE KEY: ${SOURCE_KEY}
WORKFLOW: Use with the v${VERSION} Site Design CSS exported from the same source site.
SITE CSS EXPORTED BEFORE: ${marker ? 'YES' : 'UNKNOWN / NO MARKER'}
ANCESTOR CONTEXT SHELLS: ${contextDepth}
DEPENDENCIES DETECTED: ${dependencies.length ? dependencies.join(' | ') : 'No known JS library detected'}
TEXT, SVG, INLINE STYLES AND CURRENT DOM STATE ARE PRESERVED.
SOURCE SCRIPTS ARE INTENTIONALLY NOT COPIED.
-->

${wrapper.outerHTML}`;
  }

  btnCopyHTML.addEventListener('click', () => {
    if (!selected) return say('اختر بلوكًا أولًا');

    try {
      const output = buildBlockHTML(selected);
      copyText(output);

      const marker = readExportMarker();
      const note = marker ? '' : ' | Site CSS غير معلَّم كمصدّر';
      say(`تم نسخ البلوك — ${formatBytes(byteSize(output))}${note}`, 6500);
    } catch (error) {
      console.error('[UBC block export]', error);
      say('تعذر نسخ البلوك', 6500);
    }
  });

  btnText.addEventListener('click', () => {
    if (!selected) return say('اختر بلوكًا أولًا');
    const text = (selected.innerText || selected.textContent || '').trim();
    copyText(text);
    say(`تم نسخ النصوص — ${formatBytes(byteSize(text))}`);
  });

  function collectAssets(root) {
    const urls = new Set();
    const elements = [root, ...root.querySelectorAll('*')];

    for (const el of elements) {
      if (el instanceof HTMLImageElement && el.currentSrc) urls.add(el.currentSrc);

      ['src', 'poster'].forEach(attr => {
        if (!el.hasAttribute?.(attr)) return;
        const u = absoluteURL(el.getAttribute(attr));
        if (u) urls.add(u);
      });

      if (el.hasAttribute?.('srcset')) {
        absoluteSrcset(el.getAttribute('srcset'))
          .split(',')
          .forEach(part => {
            const u = part.trim().split(/\s+/)[0];
            if (u) urls.add(u);
          });
      }

      try {
        for (const pseudo of [null, '::before', '::after']) {
          const style = pseudo ? getComputedStyle(el, pseudo) : getComputedStyle(el);
          const values = [
            style.backgroundImage,
            style.getPropertyValue('mask-image'),
            style.getPropertyValue('-webkit-mask-image')
          ];

          for (const value of values) {
            if (!value || value === 'none') continue;
            for (const match of value.matchAll(/url\(\s*['"]?(.*?)['"]?\s*\)/gi)) {
              const u = absoluteURL(match[1]);
              if (u) urls.add(u);
            }
          }
        }
      } catch {}
    }

    return Array.from(urls).filter(Boolean);
  }

  btnAssets.addEventListener('click', () => {
    if (!selected) return say('اختر بلوكًا أولًا');
    const urls = collectAssets(selected);
    copyText(urls.join('\n'));
    say(`تم نسخ ${urls.length} رابط أصل`);
  });

  // ============================================================
  // CSS SCOPING ENGINE
  // ============================================================

  function splitSelectorList(selectorText) {
    const out = [];
    let current = '';
    let paren = 0;
    let bracket = 0;
    let quote = '';
    let escaped = false;

    for (const ch of String(selectorText || '')) {
      if (escaped) {
        current += ch;
        escaped = false;
        continue;
      }

      if (ch === '\\') {
        current += ch;
        escaped = true;
        continue;
      }

      if (quote) {
        current += ch;
        if (ch === quote) quote = '';
        continue;
      }

      if (ch === '"' || ch === "'") {
        quote = ch;
        current += ch;
        continue;
      }

      if (ch === '(') paren++;
      if (ch === ')') paren = Math.max(0, paren - 1);
      if (ch === '[') bracket++;
      if (ch === ']') bracket = Math.max(0, bracket - 1);

      if (ch === ',' && paren === 0 && bracket === 0) {
        if (current.trim()) out.push(current.trim());
        current = '';
        continue;
      }

      current += ch;
    }

    if (current.trim()) out.push(current.trim());
    return out;
  }

  function readLeadingCompound(selector, tagName) {
    const re = new RegExp(`^${tagName}(?=$|[.#\\[:\\s>+~])`, 'i');
    const match = selector.match(re);
    if (!match) return null;

    let i = match[0].length;
    let paren = 0;
    let bracket = 0;
    let quote = '';
    let escaped = false;

    while (i < selector.length) {
      const ch = selector[i];

      if (escaped) {
        escaped = false;
        i++;
        continue;
      }

      if (ch === '\\') {
        escaped = true;
        i++;
        continue;
      }

      if (quote) {
        if (ch === quote) quote = '';
        i++;
        continue;
      }

      if (ch === '"' || ch === "'") {
        quote = ch;
        i++;
        continue;
      }

      if (ch === '[') bracket++;
      else if (ch === ']') bracket = Math.max(0, bracket - 1);
      else if (ch === '(') paren++;
      else if (ch === ')') paren = Math.max(0, paren - 1);

      if (paren === 0 && bracket === 0 && /\s|>|\+|~/.test(ch)) break;
      i++;
    }

    return {
      compound: selector.slice(0, i),
      rest: selector.slice(i),
      qualifiers: selector.slice(match[0].length, i)
    };
  }

  function readLeadingRootPseudo(selector) {
    const s = String(selector || '');
    if (!s.toLowerCase().startsWith(':root')) return null;

    let i = ':root'.length;
    let paren = 0;
    let bracket = 0;
    let quote = '';
    let escaped = false;

    while (i < s.length) {
      const ch = s[i];

      if (escaped) {
        escaped = false;
        i++;
        continue;
      }

      if (ch === '\\') {
        escaped = true;
        i++;
        continue;
      }

      if (quote) {
        if (ch === quote) quote = '';
        i++;
        continue;
      }

      if (ch === '"' || ch === "'") {
        quote = ch;
        i++;
        continue;
      }

      if (ch === '[') bracket++;
      else if (ch === ']') bracket = Math.max(0, bracket - 1);
      else if (ch === '(') paren++;
      else if (ch === ')') paren = Math.max(0, paren - 1);

      if (paren === 0 && bracket === 0 && /\s|>|\+|~/.test(ch)) break;
      i++;
    }

    return {
      compound: s.slice(0, i),
      rest: s.slice(i),
      qualifiers: s.slice(':root'.length, i)
    };
  }

  function rootSelectorInfo(selector) {
    let remaining = String(selector || '').trim();
    if (!remaining) return null;

    let qualifiers = '';
    let consumed = false;

    const rootPseudo = readLeadingRootPseudo(remaining);

    if (rootPseudo) {
      consumed = true;
      qualifiers += rootPseudo.qualifiers;
      remaining = rootPseudo.rest.trimStart();
    } else {
      const html = readLeadingCompound(remaining, 'html');

      if (html) {
        consumed = true;
        qualifiers += html.qualifiers;
        remaining = html.rest.trimStart();

        const bodyAfterHtml = readLeadingCompound(remaining, 'body');

        if (bodyAfterHtml) {
          qualifiers += bodyAfterHtml.qualifiers;
          remaining = bodyAfterHtml.rest.trimStart();
        }
      } else {
        const body = readLeadingCompound(remaining, 'body');

        if (body) {
          consumed = true;
          qualifiers += body.qualifiers;
          remaining = body.rest.trimStart();
        }
      }
    }

    if (!consumed) return null;

    return {
      remaining,
      scopedRoot: `${SCOPE_SELECTOR}${qualifiers}`,
      pureRoot: !remaining
    };
  }

  function isSafePageRootSelector(selector) {
    const info = rootSelectorInfo(selector);
    if (!info?.pureRoot) return false;

    // Relational/dynamic root states can be component behavior (for example
    // body:has(.modal){overflow:hidden}); keep those as full component rules.
    if (
      /:has\(|:(hover|active|focus|focus-visible|focus-within|target|checked|open)\b/i
        .test(String(selector || ''))
    ) {
      return false;
    }

    return true;
  }

  function scopeOneSelector(selector) {
    const s = String(selector || '').trim();
    if (!s) return s;

    // Avoid double-scoping exported CSS.
    if (s.includes(SCOPE_SELECTOR)) return s;

    const rootInfo = rootSelectorInfo(s);

    if (rootInfo) {
      return rootInfo.remaining
        ? `${rootInfo.scopedRoot} ${rootInfo.remaining}`
        : rootInfo.scopedRoot;
    }

    return `${SCOPE_SELECTOR} ${s}`;
  }

  function scopeSelectorList(selectorText) {
    return splitSelectorList(selectorText)
      .map(scopeOneSelector)
      .filter(Boolean)
      .join(', ');
  }

  function groupHeader(rule) {
    const text = rule.cssText || '';
    const index = text.indexOf('{');
    return index >= 0 ? text.slice(0, index).trim() : '';
  }

  function isFontFaceRule(rule) {
    return rule.type === 5 || rule.constructor?.name === 'CSSFontFaceRule' || /^@font-face\b/i.test(rule.cssText || '');
  }

  function isKeyframesRule(rule) {
    return rule.type === 7 || rule.constructor?.name === 'CSSKeyframesRule' || /^@(?:-webkit-)?keyframes\b/i.test(rule.cssText || '');
  }

  function isImportRule(rule) {
    return rule.type === 3 || rule.constructor?.name === 'CSSImportRule' || /^@import\b/i.test(rule.cssText || '');
  }

  function isStyleRule(rule) {
    return rule.type === 1 || rule.constructor?.name === 'CSSStyleRule';
  }

  function shouldPreserveRawAtRule(rule) {
    const text = rule.cssText || '';
    return /^@(font-face|property|counter-style|font-feature-values|font-palette-values|namespace|custom-media)\b/i.test(text);
  }

  const ROOT_SAFE_PROPERTIES = new Set([
    'color',
    'font',
    'font-family',
    'font-size',
    'font-style',
    'font-weight',
    'font-stretch',
    'font-kerning',
    'font-optical-sizing',
    'font-synthesis',
    'font-variant',
    'font-feature-settings',
    'font-variation-settings',
    'line-height',
    'letter-spacing',
    'word-spacing',
    'text-align',
    'text-transform',
    'text-rendering',
    'text-size-adjust',
    '-webkit-text-size-adjust',
    '-webkit-font-smoothing',
    '-moz-osx-font-smoothing',
    'direction',
    'writing-mode',
    'color-scheme',
    'accent-color',
    'hyphens',
    'tab-size',
    'box-sizing'
  ]);

  function originalStyleDeclarations(rule) {
    const text = String(rule?.cssText || '');
    const first = text.indexOf('{');
    const last = text.lastIndexOf('}');

    if (first >= 0 && last > first) {
      return text.slice(first + 1, last).trim();
    }

    return String(rule?.style?.cssText || '').trim();
  }

  function filteredRootDeclarations(rule) {
    const out = [];

    try {
      for (const prop of rule.style || []) {
        const normalized = String(prop || '').toLowerCase();

        if (
          !normalized.startsWith('--') &&
          !ROOT_SAFE_PROPERTIES.has(normalized)
        ) {
          continue;
        }

        const value = rule.style.getPropertyValue(prop);
        if (!value) continue;

        const priority = rule.style.getPropertyPriority(prop);
        out.push(`${prop}:${value}${priority ? ' !important' : ''};`);
      }
    } catch {}

    return out.join('');
  }

  function cssNumber(value) {
    const n = Number(value);
    if (!Number.isFinite(n)) return value;

    return n
      .toFixed(4)
      .replace(/\.0+$/, '')
      .replace(/(\.\d*?)0+$/, '$1');
  }

  function normalizeRemUnits(text, rootFontPx) {
    if (!text || !Number.isFinite(rootFontPx) || rootFontPx <= 0) return text;

    return String(text).replace(
      /(-?(?:\d+\.?\d*|\.\d+))rem\b/gi,
      (_, number) => `${cssNumber(parseFloat(number) * rootFontPx)}px`
    );
  }

  function escapeRegExp(value) {
    return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  }

  function keyframeName(rule) {
    if (rule?.name) return String(rule.name);

    const match = String(rule?.cssText || '').match(
      /^@(?:-webkit-)?keyframes\s+(?:"([^"]+)"|'([^']+)'|([^\s{]+))/i
    );

    return match ? (match[1] || match[2] || match[3] || '') : '';
  }

  function namespaceKeyframeText(text, oldName, newName) {
    if (!text || !oldName || !newName) return text;

    return String(text).replace(
      /^(@(?:-webkit-)?keyframes\s+)(?:"[^"]+"|'[^']+'|[^\s{]+)/i,
      `$1${newName}`
    );
  }

  function rewriteAnimationReferences(cssText, keyframeMap) {
    if (!cssText || !keyframeMap?.size) return cssText;

    return String(cssText).replace(
      /((?:-webkit-)?animation(?:-name)?\s*:\s*)([^;}]+)/gi,
      (full, prefix, value) => {
        let updated = value;

        for (const [oldName, newName] of keyframeMap) {
          if (!oldName || !newName || oldName === newName) continue;

          const re = new RegExp(
            `(^|[^a-zA-Z0-9_-])${escapeRegExp(oldName)}(?=$|[^a-zA-Z0-9_-])`,
            'g'
          );

          updated = updated.replace(re, `$1${newName}`);
        }

        return prefix + updated;
      }
    );
  }

  // ============================================================
  // SITE CSS COLLECTOR + SCOPER
  // ============================================================

  async function buildSiteDesignCSS({ scoped = true, onStatus = null } = {}) {
    if (siteCSSCache && scoped && siteCSSCache.scopedCSS) return siteCSSCache;

    const processedSheetObjects = new WeakSet();
    const processedURLs = new Set();
    const errors = [];
    const keyframeMap = new Map();

    let rootFontPx = 16;

    try {
      rootFontPx =
        parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
    } catch {}

    let fetchedStylesheets = 0;
    let scannedStylesheets = 0;
    let ruleCount = 0;
    let fontFaceCount = 0;
    let keyframeCount = 0;

    function emitRule(text) {
      if (!text || !text.trim()) return '';
      return text.trim() + '\n';
    }

    async function serializeRuleList(ruleList, baseURL, depth) {
      let out = '';
      if (!ruleList) return out;

      for (const rule of Array.from(ruleList)) {
        if (!rule) continue;

        if (isImportRule(rule)) {
          const href = absoluteURL(rule.href || '', baseURL);
          if (!href || depth >= MAX_IMPORT_DEPTH) continue;

          let imported = '';
          try {
            if (rule.styleSheet?.cssRules) {
              imported = await serializeRuleList(rule.styleSheet.cssRules, href, depth + 1);
            } else {
              imported = await fetchAndSerializeStylesheet(href, depth + 1);
            }
          } catch {
            imported = await fetchAndSerializeStylesheet(href, depth + 1);
          }

          if (imported.trim()) {
            const mediaText = rule.media?.mediaText?.trim?.() || '';
            const supportsText = rule.supportsText || '';
            const layerName = rule.layerName || '';

            if (mediaText && mediaText.toLowerCase() !== 'all') {
              const portableMedia = scoped
                ? normalizeRemUnits(mediaText, rootFontPx)
                : mediaText;

              imported = `@media ${portableMedia}{\n${imported}}\n`;
            }
            if (supportsText) {
              imported = `@supports ${supportsText}{\n${imported}}\n`;
            }
            if (layerName) {
              imported = `@layer ${layerName}{\n${imported}}\n`;
            }

            out += imported;
          }
          continue;
        }

        if (isStyleRule(rule)) {
          const rawSelectorText = rule.selectorText || '';
          if (!rawSelectorText.trim()) continue;

          let declarations = absoluteCSSURLs(
            originalStyleDeclarations(rule),
            baseURL
          );

          if (scoped) {
            declarations = normalizeRemUnits(declarations, rootFontPx);
          }

          if (!declarations.trim()) continue;

          if (!scoped) {
            ruleCount++;
            out += emitRule(`${rawSelectorText}{${declarations}}`);
            continue;
          }

          const rawSelectors = splitSelectorList(rawSelectorText);
          const rootSelectors = [];
          const componentSelectors = [];

          for (const selector of rawSelectors) {
            if (isSafePageRootSelector(selector)) rootSelectors.push(selector);
            else componentSelectors.push(selector);
          }

          if (componentSelectors.length) {
            const selector = componentSelectors
              .map(scopeOneSelector)
              .filter(Boolean)
              .join(', ');

            if (selector) {
              ruleCount++;
              out += emitRule(`${selector}{${declarations}}`);
            }
          }

          if (rootSelectors.length) {
            let rootDeclarations = absoluteCSSURLs(
              filteredRootDeclarations(rule),
              baseURL
            );

            rootDeclarations = normalizeRemUnits(
              rootDeclarations,
              rootFontPx
            );

            if (rootDeclarations.trim()) {
              const selector = rootSelectors
                .map(scopeOneSelector)
                .filter(Boolean)
                .join(', ');

              if (selector) {
                ruleCount++;
                out += emitRule(`${selector}{${rootDeclarations}}`);
              }
            }
          }

          continue;
        }

        if (isFontFaceRule(rule)) {
          fontFaceCount++;

          let text = absoluteCSSURLs(rule.cssText || '', baseURL);

          if (scoped) {
            text = normalizeRemUnits(text, rootFontPx);
          }

          out += emitRule(text);
          continue;
        }

        if (isKeyframesRule(rule)) {
          keyframeCount++;

          let text = absoluteCSSURLs(rule.cssText || '', baseURL);

          if (scoped) {
            const oldName = keyframeName(rule);

            if (oldName) {
              const newName =
                KEYFRAME_PREFIX +
                oldName.replace(/[^a-zA-Z0-9_-]/g, '_');

              keyframeMap.set(oldName, newName);
              text = namespaceKeyframeText(text, oldName, newName);
            }

            text = normalizeRemUnits(text, rootFontPx);
          }

          out += emitRule(text);
          continue;
        }

        if (shouldPreserveRawAtRule(rule)) {
          let text = absoluteCSSURLs(rule.cssText || '', baseURL);

          if (scoped) {
            text = normalizeRemUnits(text, rootFontPx);
          }

          out += emitRule(text);
          continue;
        }

        // Generic grouping rules: @media, @supports, @container, @layer,
        // @scope, @starting-style and future CSS grouping rules exposed by CSSOM.
        if (rule.cssRules) {
          let header = groupHeader(rule);

          if (scoped) {
            header = normalizeRemUnits(header, rootFontPx);
          }

          const inner = await serializeRuleList(rule.cssRules, baseURL, depth);

          if (header && inner.trim()) {
            out += emitRule(`${header}{\n${inner}}`);
          }

          continue;
        }

        // Keep harmless non-page-global rules we do not understand.
        let raw = absoluteCSSURLs(rule.cssText || '', baseURL);

        if (scoped) {
          raw = normalizeRemUnits(raw, rootFontPx);
        }

        if (/^@(charset|page)\b/i.test(raw)) continue;
        if (raw.trim()) out += emitRule(raw);
      }

      return out;
    }

    async function serializeSheetObject(sheet, fallbackSource, depth = 0) {
      if (!sheet) return '';

      try {
        if (processedSheetObjects.has(sheet)) return '';
        processedSheetObjects.add(sheet);
      } catch {}

      scannedStylesheets++;
      const baseURL = sheet.href || fallbackSource || location.href;

      try {
        const rules = sheet.cssRules;
        if (sheet.href) processedURLs.add(absoluteURL(sheet.href));
        return await serializeRuleList(rules, baseURL, depth);
      } catch (error) {
        if (sheet.href) return await fetchAndSerializeStylesheet(sheet.href, depth);
        errors.push(`Unreadable stylesheet: ${fallbackSource || 'inline'} :: ${String(error)}`);
        return '';
      }
    }

    async function fetchAndSerializeStylesheet(url, depth = 0) {
      const absolute = absoluteURL(url);
      if (!absolute || processedURLs.has(absolute) || depth > MAX_IMPORT_DEPTH) return '';
      if (fetchedStylesheets >= MAX_FETCHED_STYLESHEETS) return '';

      processedURLs.add(absolute);
      fetchedStylesheets++;
      onStatus?.(`CSS: قراءة ملف ${fetchedStylesheets}…`);

      const response = await requestText(absolute);
      if (!response.ok || !response.text) {
        errors.push(`Fetch failed: ${absolute} (${response.error || response.status})`);
        return '';
      }

      const finalURL = response.finalUrl || absolute;
      let cssText = absoluteCSSURLs(response.text, finalURL);
      cssText = cssText.replace(/@charset\s+["'][^"']+["']\s*;/gi, '');

      const temp = document.createElement('style');
      temp.media = 'not all';
      temp.setAttribute('data-ubc-temp-style', '');
      temp.textContent = cssText;

      try {
        document.documentElement.appendChild(temp);
        await sleep(0);

        if (!temp.sheet?.cssRules) {
          errors.push(`Could not parse stylesheet: ${finalURL}`);
          return '';
        }

        return await serializeRuleList(temp.sheet.cssRules, finalURL, depth);
      } catch (error) {
        errors.push(`Parse failed: ${finalURL} :: ${String(error)}`);
        return '';
      } finally {
        temp.remove();
      }
    }

    let cssBody = '';

    const sheets = Array.from(document.styleSheets || []);
    onStatus?.(`CSS: ${sheets.length} stylesheet…`);

    for (let i = 0; i < sheets.length; i++) {
      const sheet = sheets[i];
      onStatus?.(`CSS: stylesheet ${i + 1}/${sheets.length}`);
      cssBody += await serializeSheetObject(sheet, sheet.href || `inline-${i}`, 0);

      if (i % 8 === 0) await sleep(10);
    }

    // adoptedStyleSheets are not always present in document.styleSheets.
    try {
      for (const [index, sheet] of Array.from(document.adoptedStyleSheets || []).entries()) {
        cssBody += await serializeSheetObject(sheet, `document.adoptedStyleSheets[${index}]`, 0);
      }
    } catch {}

    // Stylesheet links that CSSOM did not expose/read.
    const knownHrefs = new Set(sheets.map(s => s.href).filter(Boolean).map(h => absoluteURL(h)));

    const candidateLinks = Array.from(document.querySelectorAll('link[href]')).filter(link => {
      const rel = String(link.rel || '').toLowerCase();
      const as = String(link.as || '').toLowerCase();
      return rel.includes('stylesheet') || as === 'style';
    });

    for (const link of candidateLinks) {
      const href = absoluteURL(link.href);
      if (!href) continue;
      if (!knownHrefs.has(href)) cssBody += await fetchAndSerializeStylesheet(href, 0);
    }

    // Performance entries can reveal CSS loaded dynamically by JS.
    try {
      for (const entry of performance.getEntriesByType('resource') || []) {
        const name = entry.name || '';
        const init = String(entry.initiatorType || '').toLowerCase();
        if (/\.css(?:[?#]|$)/i.test(name) || init === 'css') {
          cssBody += await fetchAndSerializeStylesheet(name, 0);
        }
      }
    } catch {}

    if (scoped) {
      cssBody = rewriteAnimationReferences(cssBody, keyframeMap);

      cssBody =
`${SCOPE_SELECTOR}{
  --ubc-package-version: "${VERSION}";
  --ubc-source-key: "${SOURCE_KEY}";
  --ubc-source-root-font-size: ${cssNumber(rootFontPx)}px;
}

` + cssBody;
    }

    const header = `/*
Universal Block Copier v${VERSION} — ${scoped ? 'SCOPED PORTABLE SITE DESIGN CSS' : 'RAW SITE CSS'}
SOURCE: ${location.href}
ORIGIN: ${location.origin}
COLLECTED: ${new Date().toISOString()}

HOW TO USE:
- Keep each copied block wrapped in ${SCOPE_SELECTOR}
- Load this CSS once on the destination site, preferably after the destination theme's base CSS.
- Re-export this package when the source page loads materially different styles.
- Then Copy Exact Block HTML can be used repeatedly without embedding full site CSS in every block.

PORTABILITY:
- Source package key: ${SOURCE_KEY}
- Source root font size: ${cssNumber(rootFontPx)}px
- rem units are converted to source-equivalent px values in the scoped package.
- Bare html/body/:root page-layout declarations are NOT copied onto the component wrapper.
- Ancestor selector context is recreated by lightweight display:contents shells in copied HTML.
- Keyframes are namespaced to avoid collisions with the destination theme or other source sites.

STATS:
- CSSOM stylesheets scanned: ${scannedStylesheets}
- External stylesheets fetched: ${fetchedStylesheets}
- Style rules exported: ${ruleCount}
- Font-face rules: ${fontFaceCount}
- Keyframes: ${keyframeCount}
- Namespaced keyframe names: ${keyframeMap.size}
- Collector errors: ${errors.length}

LIMITS:
- Closed Shadow DOM and inaccessible cross-origin iframe internals cannot be exported.
- Server-side source such as Shopify Liquid does not exist in the browser DOM.
- External font/image servers may independently restrict cross-origin reuse.
*/

`;

    const finalCSS = header + cssBody;
    const finalBytes = byteSize(finalCSS);

    if (finalBytes > MAX_SITE_CSS_BYTES) {
      throw new Error(`Site CSS exceeded safety limit ${formatBytes(MAX_SITE_CSS_BYTES)}`);
    }

    const result = {
      scopedCSS: scoped ? finalCSS : null,
      rawCSS: scoped ? null : finalCSS,
      bytes: finalBytes,
      scannedStylesheets,
      fetchedStylesheets,
      ruleCount,
      fontFaceCount,
      keyframeCount,
      sourceKey: SOURCE_KEY,
      rootFontPx,
      errors
    };

    if (scoped) siteCSSCache = result;
    return result;
  }

  // ============================================================
  // SITE CSS BUTTONS
  // ============================================================

  btnExportCSS.addEventListener('click', async () => {
    if (cssJobRunning) return say('هناك عملية CSS قيد التنفيذ');

    cssJobRunning = true;
    setBusy(btnExportCSS, true, 'CSS: scanning…');

    try {
      const result = await buildSiteDesignCSS({
        scoped: true,
        onStatus(text) {
          btnExportCSS.textContent = text;
          message.textContent = text;
        }
      });

      const filename = `ubc-site-design-${safeName(location.hostname)}-${nowStamp()}.css`;
      downloadText(filename, result.scopedCSS, 'text/css;charset=utf-8');

      writeExportMarker({
        bytes: result.bytes,
        rules: result.ruleCount,
        files: result.scannedStylesheets + result.fetchedStylesheets
      });

      say(
        `تم حفظ Site CSS — ${formatBytes(result.bytes)} | ${result.ruleCount} rules | ${result.errors.length} errors`,
        9000
      );
    } catch (error) {
      console.error('[UBC Site CSS]', error);
      say(`فشل تصدير Site CSS: ${error.message || error}`, 9000);
    } finally {
      cssJobRunning = false;
      setBusy(btnExportCSS, false);
    }
  });

  btnRawCSS.addEventListener('click', async () => {
    if (cssJobRunning) return say('هناك عملية CSS قيد التنفيذ');

    cssJobRunning = true;
    setBusy(btnRawCSS, true, 'Raw CSS…');

    try {
      const result = await buildSiteDesignCSS({
        scoped: false,
        onStatus(text) {
          btnRawCSS.textContent = text;
          message.textContent = text;
        }
      });

      const filename = `ubc-raw-full-css-${safeName(location.hostname)}-${nowStamp()}.css`;
      downloadText(filename, result.rawCSS, 'text/css;charset=utf-8');
      say(`تم حفظ Raw CSS — ${formatBytes(result.bytes)}`, 8000);
    } catch (error) {
      console.error('[UBC Raw CSS]', error);
      say(`فشل Raw CSS: ${error.message || error}`, 8000);
    } finally {
      cssJobRunning = false;
      setBusy(btnRawCSS, false);
    }
  });

  // ============================================================
  // MENU COMMANDS
  // ============================================================

  try {
    GM_registerMenuCommand('UBC: Export Site Design CSS', () => btnExportCSS.click());
    GM_registerMenuCommand('UBC: Start Block Picker', () => btnPick.click());
  } catch {}

  // ============================================================
  // READY
  // ============================================================

  const marker = readExportMarker();

  if (marker) {
    say('جاهز — يوجد سجل سابق لتصدير Site CSS لهذا الدومين', 6500);
  } else {
    say('جاهز — ابدأ بـ ① Export Site Design CSS', 6500);
  }
})();
.ubc-source-design {
  outline: 6px solid #ff00ff !important;
}