Legal Acts Finder

Detect Acts/Bills/Rules/Regulations/Orders/Sections, highlight mentions, show a smooth slide-out sidebar with PDFs (or Google fallback). Dedup by name+year, keep order, smooth scroll & flash, batch-open PDFs. Draggable vertically. Session-persistent per page.

// ==UserScript==
// @name         Legal Acts Finder
// @namespace    http://tampermonkey.net/
// @version      1.9
// @description  Detect Acts/Bills/Rules/Regulations/Orders/Sections, highlight mentions, show a smooth slide-out sidebar with PDFs (or Google fallback). Dedup by name+year, keep order, smooth scroll & flash, batch-open PDFs. Draggable vertically. Session-persistent per page.
// @author       iamnobody
// @license      MIT
// @match        *://*/*
// @grant        GM_xmlhttpRequest
// @icon         https://greasyfork.s3.us-east-2.amazonaws.com/hin0zyjrsudy75bgad4hggtwu2ib
// @banner       https://greasyfork.s3.us-east-2.amazonaws.com/raibg6tl78seouyv8nbjhis9qgju
// ==/UserScript==

(function () {
  'use strict';

  /* ---------- Config ---------- */
  const MIN_WIDTH_PX = 250;
  const MAX_WIDTH_PCT = 30;
  const TRANS_MS = 320;
  const FETCH_CONCURRENCY = 4;
  const OPEN_DELAY_MS = 300;
  const STORAGE_PREFIX = 'laf_v1_'; // session keys prefix

  /* ---------- Regexes ---------- */
  // Matches multi-word names ending with Act/Bill/Rules/Regulation/Order/Code/Law (with optional ", 2005" or "of 2005" or "year 2005")
  const ENTITY_REGEX = /\b([A-Z][A-Za-z0-9&\-\s\.]{2,}?(?:\b(?:Act|Bill|Rules|Regulation|Regulations|Order|Code|Law)\b)(?:[\s,]*(?:of|year)?[\s,]*\d{4})?)\b/gi;
  // Section references like "Section 7 of Companies Act, 1956" (covers Sec. as well)
  const SECTION_REGEX = /\b(Sec(?:tion)?\.?\s+\d+[A-Za-z]?\s+of\s+[A-Z][A-Za-z0-9&\-\s\.]{2,}?(?:\b(?:Act|Bill|Rules|Regulation|Regulations|Order|Code|Law)\b)(?:[\s,]*\d{4})?)\b/gi;
  const YEAR_EXTRACT = /\b(18|19|20)\d{2}\b/;

  /* ---------- Utilities ---------- */
  const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
  const uid = (s) => 'uid_' + Math.random().toString(36).slice(2, 9);
  const escapeHtml = (s) => String(s).replace(/[&<>"']/g, m => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[m]));

  /* ---------- Data structures ---------- */
  // uid -> { name, year|null, key, firstIndex, pdfUrl|null, type: 'pdf'|'search'|'pending' }
  const uniqueMap = new Map();
  const uidOrder = []; // array of uids in first-appearance order
  let mentionCount = 0;

  /* ---------- Fetch queue (limited concurrency) ---------- */
  function createQueue(concurrency = FETCH_CONCURRENCY) {
    const q = [];
    let running = 0;
    const runNext = () => {
      if (running >= concurrency || q.length === 0) return;
      const { query, resolve } = q.shift();
      running++;
      fetchPdf(query).then(res => {
        running--;
        resolve(res);
        runNext();
      }).catch(err => {
        running--;
        resolve({ url: `https://www.google.com/search?q=${encodeURIComponent(query + ' pdf')}`, type: 'search' });
        runNext();
      });
    };
    return (query) => new Promise(res => { q.push({ query, resolve: res }); runNext(); });
  }
  const queuedFetch = createQueue();

  function fetchPdf(query) {
    return new Promise((resolve) => {
      const googleUrl = `https://www.google.com/search?q=${encodeURIComponent(query + ' pdf')}`;
      try {
        GM_xmlhttpRequest({
          method: 'GET',
          url: googleUrl,
          headers: { 'User-Agent': navigator.userAgent },
          onload(response) {
            const html = response && response.responseText ? response.responseText : '';
            const pdfMatch = html.match(/https?:\/\/[^"'>\s]+?\.pdf\b/gi);
            if (pdfMatch && pdfMatch.length) {
              const url = pdfMatch[0].replace(/\\u0026/g, '&');
              resolve({ url, type: 'pdf' });
            } else {
              resolve({ url: googleUrl, type: 'search' });
            }
          },
          onerror() { resolve({ url: googleUrl, type: 'search' }); },
          timeout: 15000
        });
      } catch (e) {
        resolve({ url: googleUrl, type: 'search' });
      }
    });
  }

  /* ---------- DOM walking & replacement ---------- */
  function isIgnorableNode(node) {
    if (!node) return true;
    const tag = node.nodeName;
    return (tag === 'SCRIPT' || tag === 'STYLE' || tag === 'NOSCRIPT' || tag === 'IFRAME' || tag === 'TEXTAREA' || tag === 'INPUT' || tag === 'SELECT');
  }

  function normalizeName(raw) {
    // Trim, collapse spaces, remove leading/trailing punctuation
    let s = raw.replace(/\s+/g, ' ').trim();
    s = s.replace(/^[\W_]+|[\W_]+$/g, '').trim();
    return s;
  }

  function createKey(name, year) {
    return (name.toLowerCase()) + '|' + (year || '__NOYEAR__');
  }

  // walk all text nodes and replace matches with spans
  function scanAndAnnotate(root = document.body) {
    const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null, false);
    const textNodes = [];
    while (walker.nextNode()) {
      const n = walker.currentNode;
      if (!n.nodeValue || !n.nodeValue.trim()) continue;
      const parent = n.parentNode;
      if (parent && isIgnorableNode(parent)) continue;
      textNodes.push(n);
    }

    // process each text node
    for (let i = 0; i < textNodes.length; i++) {
      const tn = textNodes[i];
      const original = tn.nodeValue;
      let frag = document.createDocumentFragment();
      let lastIndex = 0;
      const combinedRegex = new RegExp(`${ENTITY_REGEX.source}|${SECTION_REGEX.source}`, 'gi');
      let m;
      combinedRegex.lastIndex = 0;
      while ((m = combinedRegex.exec(original)) !== null) {
        const matched = m[0];
        const start = m.index;
        const end = combinedRegex.lastIndex;

        if (start > lastIndex) {
          frag.appendChild(document.createTextNode(original.slice(lastIndex, start)));
        }

        const norm = normalizeName(matched);
        const yearMatch = norm.match(YEAR_EXTRACT);
        const year = yearMatch ? yearMatch[0] : null;
        const baseName = year ? norm.replace(YEAR_EXTRACT, '').replace(/\b(of|year)\b/gi, '').trim() : norm;
        const key = createKey(baseName, year);
        let entryUid;
        if (uniqueMap.has(key)) {
          entryUid = uniqueMap.get(key).uid;
        } else {
          entryUid = uid(baseName + (year || ''));
          uniqueMap.set(key, { uid: entryUid, name: baseName, year: year, key, pdfUrl: null, type: 'pending', firstIndex: uidOrder.length });
          uidOrder.push(entryUid);
        }

        // create span
        const span = document.createElement('span');
        span.className = 'la-mention';
        span.dataset.uid = entryUid;
        span.dataset.matchText = matched;
        span.textContent = matched;
        // click handler opens link in new tab (if available), else queue fetch then open
        span.addEventListener('click', async (e) => {
          e.stopPropagation();
          const mapEntry = [...uniqueMap.values()].find(it => it.uid === entryUid);
          if (!mapEntry) return;
          if (mapEntry.type === 'pdf' && mapEntry.pdfUrl) {
            window.open(mapEntry.pdfUrl, '_blank', 'noopener');
            return;
          }
          if (mapEntry.type === 'search' && mapEntry.pdfUrl) {
            window.open(mapEntry.pdfUrl, '_blank', 'noopener');
            return;
          }
          // else fetch now
          const res = await queuedFetch(mapEntry.year ? `${mapEntry.name} ${mapEntry.year}` : mapEntry.name);
          mapEntry.pdfUrl = res.url; mapEntry.type = res.type;
          updateSidebarEntry(mapEntry.uid); // update UI if already built
          window.open(res.url, '_blank', 'noopener');
        });

        frag.appendChild(span);
        mentionCount++;
        lastIndex = end;
      }

      if (lastIndex < original.length) {
        frag.appendChild(document.createTextNode(original.slice(lastIndex)));
      }

      if (frag.childNodes.length > 0) {
        tn.parentNode.replaceChild(frag, tn);
      }
    }
  }

  /* ---------- Styles ---------- */
  function injectStyles() {
    const css = `
.la-mention{border-bottom:2px dotted rgba(200,30,30,0.9);cursor:pointer;transition:background 220ms}
.la-mention.la-flash{animation:laf-flash 900ms ease forwards}
@keyframes laf-flash{0%{background:rgba(255,255,0,0.95)}60%{background:rgba(255,255,0,0.5)}100%{background:transparent}}
/* Sidebar */
#laf-container{position:fixed;right:0;top:50%;transform:translateY(-50%);z-index:2147483646}
#laf-panel{position:fixed;right:0;top:50%;transform:translateX(100%) translateY(-50%);transition:transform ${TRANS_MS}ms cubic-bezier(.2,.9,.2,1),opacity ${TRANS_MS}ms;opacity:0;box-shadow:0 12px 30px rgba(0,0,0,0.18);border-radius:12px 0 0 12px;overflow:hidden;display:flex;flex-direction:column;max-height:80vh;background:var(--laf-bg);color:var(--laf-fg)}
#laf-panel.open{transform:translateX(0) translateY(-50%);opacity:1}
#laf-header{display:flex;align-items:center;justify-content:space-between;padding:12px 14px;cursor:grab;border-bottom:1px solid rgba(0,0,0,0.06)}
#laf-title{font-weight:700;font-size:15px;display:flex;align-items:center;gap:10px}
#laf-controls{display:flex;gap:8px;align-items:center}
#laf-openall{padding:6px 10px;border-radius:8px;border:1px solid rgba(0,0,0,0.06);background:transparent;cursor:pointer}
#laf-openall[disabled]{opacity:0.45;cursor:not-allowed}
#laf-close{background:transparent;border:0;font-size:18px;cursor:pointer;padding:6px 8px}
#laf-list{padding:10px;overflow:auto;flex:1 1 auto}
.laf-item{padding:8px 10px;border-radius:8px;margin-bottom:8px;transition:background 160ms}
.laf-item:hover{background:rgba(0,0,0,0.03)}
.laf-item a{color:var(--laf-accent);text-decoration:none;font-weight:600;display:block}
.laf-meta{font-size:12px;color:rgba(0,0,0,0.55);margin-top:6px}
#laf-accordion{padding:10px;border-top:1px solid rgba(0,0,0,0.04)}
#laf-acc-toggle{display:flex;align-items:center;gap:8px;cursor:pointer;user-select:none}
#laf-acc-arrow{transition:transform ${TRANS_MS}ms}
#laf-acc-content{overflow:hidden;max-height:0;transition:max-height ${TRANS_MS}ms cubic-bezier(.2,.9,.2,1);padding-top:8px}
#laf-footer{padding:8px 12px;border-top:1px solid rgba(0,0,0,0.05);font-size:12px;display:flex;justify-content:space-between;align-items:center}
#laf-tab{position:absolute;right:0;top:50%;transform:translateY(-50%);width:40px;height:84px;border-radius:10px 0 0 10px;background:var(--laf-accent);color:#fff;display:flex;align-items:center;justify-content:center;font-weight:800;cursor:pointer;box-shadow:0 8px 20px rgba(0,0,0,0.18)}
@media (prefers-color-scheme:dark){
  :root{--laf-bg:#07101a;--laf-fg:#e6eef8;--laf-accent:#ff9933}
}
@media (prefers-color-scheme:light){
  :root{--laf-bg:#ffffff;--laf-fg:#0b1220;--laf-accent:#ff9933}
}
@media(max-width:520px){
  #laf-panel{min-width:220px}
  #laf-tab{height:64px;width:36px}
}`;
    const style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);
  }

  /* ---------- Sidebar UI ---------- */
  function buildUI() {
    // container and panel
    const container = document.createElement('div');
    container.id = 'laf-container';
    document.body.appendChild(container);

    const tab = document.createElement('div');
    tab.id = 'laf-tab';
    tab.textContent = '‹';
    container.appendChild(tab);

    const panel = document.createElement('aside');
    panel.id = 'laf-panel';
    // compute width responsive
    const setPanelWidth = () => {
      const vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0);
      const px = Math.max(MIN_WIDTH_PX, Math.floor((MAX_WIDTH_PCT / 100) * vw));
      panel.style.width = Math.min(px + 'px', '100%');
    };
    setPanelWidth();
    window.addEventListener('resize', setPanelWidth);

    // header
    const header = document.createElement('div');
    header.id = 'laf-header';
    const title = document.createElement('div');
    title.id = 'laf-title';
    title.innerHTML = `<span style="width:10px;height:10px;border-radius:50%;background:var(--laf-accent);display:inline-block"></span><span>Acts & Rules</span>`;
    header.appendChild(title);

    const controls = document.createElement('div');
    controls.id = 'laf-controls';

    const openAllBtn = document.createElement('button');
    openAllBtn.id = 'laf-openall';
    openAllBtn.textContent = 'Open All PDFs (0)';
    openAllBtn.disabled = true;
    controls.appendChild(openAllBtn);

    const closeBtn = document.createElement('button');
    closeBtn.id = 'laf-close';
    closeBtn.textContent = '✕';
    controls.appendChild(closeBtn);

    header.appendChild(controls);
    panel.appendChild(header);

    // list
    const listWrap = document.createElement('div');
    listWrap.id = 'laf-list';
    panel.appendChild(listWrap);

    // accordion for individual PDFs
    const accordion = document.createElement('div');
    accordion.id = 'laf-accordion';
    const accToggle = document.createElement('div');
    accToggle.id = 'laf-acc-toggle';
    accToggle.innerHTML = `<span id="laf-acc-arrow">►</span><span>View individual PDFs</span>`;
    const accContent = document.createElement('div');
    accContent.id = 'laf-acc-content';
    accordion.appendChild(accToggle);
    accordion.appendChild(accContent);
    panel.appendChild(accordion);

    // footer
    const footer = document.createElement('div');
    footer.id = 'laf-footer';
    footer.innerHTML = `<div style="opacity:0.85">Tip: Alt+Shift+L toggles</div><div style="opacity:0.6;font-size:11px">Queue fetch & polite</div>`;
    panel.appendChild(footer);

    container.appendChild(panel);

    // session persistence keys
    const pageKey = STORAGE_PREFIX + (location.hostname + location.pathname);
    const topKey = pageKey + '_top';

    // restore state
    const state = sessionStorage.getItem(pageKey);
    if (state === 'open') {
      panel.classList.add('open');
      tab.style.display = 'none';
    } else {
      panel.classList.remove('open');
      tab.style.display = 'flex';
      // center tab
      container.style.top = '50%';
      container.style.transform = 'translateY(-50%)';
    }
    const topPx = sessionStorage.getItem(topKey);
    if (topPx) {
      container.style.top = topPx + 'px';
      container.style.transform = 'none';
    }

    // toggle logic
    function openPanel() {
      panel.classList.add('open');
      tab.style.display = 'none';
      sessionStorage.setItem(pageKey, 'open');
    }
    function closePanel() {
      panel.classList.remove('open');
      tab.style.display = 'flex';
      sessionStorage.setItem(pageKey, 'closed');
      // reset center on close
      container.style.top = '50%';
      container.style.transform = 'translateY(-50%)';
      sessionStorage.removeItem(topKey);
    }

    tab.addEventListener('click', () => openPanel());
    closeBtn.addEventListener('click', () => closePanel());

    // drag vertical when open
    let dragging = false, dragStartY = 0, currentCenterY = null;
    header.addEventListener('mousedown', (e) => {
      if (!panel.classList.contains('open')) return;
      dragging = true;
      dragStartY = e.clientY;
      const rect = container.getBoundingClientRect();
      currentCenterY = rect.top + rect.height / 2;
      container.style.transform = 'none';
      container.style.top = currentCenterY + 'px';
      header.style.cursor = 'grabbing';
      document.body.style.userSelect = 'none';
    });
    window.addEventListener('mousemove', (e) => {
      if (!dragging) return;
      const dy = e.clientY - dragStartY;
      const newCenter = currentCenterY + dy;
      const panelRect = panel.getBoundingClientRect();
      const half = panelRect.height / 2;
      const minCenter = half + 8;
      const maxCenter = window.innerHeight - half - 8;
      const clamped = clamp(newCenter, minCenter, maxCenter);
      currentCenterY = clamped;
      container.style.top = clamped + 'px';
      dragStartY = e.clientY;
    });
    window.addEventListener('mouseup', () => {
      if (!dragging) return;
      dragging = false;
      header.style.cursor = 'grab';
      document.body.style.userSelect = '';
      const pageKeyTop = topKey;
      const val = container.getBoundingClientRect().top + container.getBoundingClientRect().height / 2;
      sessionStorage.setItem(pageKeyTop, String(val));
    });

    // keyboard toggle Alt+Shift+L
    window.addEventListener('keydown', (e) => {
      if (!(e.altKey && e.shiftKey && e.key.toUpperCase() === 'L')) return;
      const active = document.activeElement;
      if (active && (active.tagName === 'INPUT' || active.tagName === 'TEXTAREA' || active.isContentEditable)) return;
      e.preventDefault();
      if (panel.classList.contains('open')) closePanel(); else openPanel();
    });

    // accordion toggle
    let accOpen = false;
    accToggle.addEventListener('click', () => {
      accOpen = !accOpen;
      const arrow = accToggle.querySelector('#laf-acc-arrow');
      if (accOpen) {
        arrow.style.transform = 'rotate(90deg)';
        accContent.style.maxHeight = Math.min(accContent.scrollHeight + 40, window.innerHeight * 0.5) + 'px';
      } else {
        arrow.style.transform = 'rotate(0deg)';
        accContent.style.maxHeight = '0';
      }
    });

    // open all
    openAllBtn.addEventListener('click', async () => {
      const pdfList = uidOrder.map(u => [...uniqueMap.values()].find(it => it.uid === u)).filter(it => it && it.type === 'pdf' && it.pdfUrl);
      const n = pdfList.length;
      if (!n) return;
      if (!confirm(`Open ${n} PDF(s) in new tabs?`)) return;
      for (let i = 0; i < pdfList.length; i++) {
        window.open(pdfList[i].pdfUrl, '_blank', 'noopener');
        // polite delay
        await new Promise(r => setTimeout(r, OPEN_DELAY_MS));
      }
    });

    // expose some helpers
    return {
      panel,
      container,
      listWrap,
      accContent,
      openAllBtn,
      openPanel,
      closePanel,
      updateEntryUI: updateSidebarEntry,
      setPanelWidth
    };
  }

  /* ---------- UI update helpers ---------- */
  function updateSidebarEntry(u) {
    // Called after a fetch resolves to update that entry's UI (if built)
    const listItem = document.querySelector(`.laf-item[data-uid="${u}"]`);
    if (!listItem) return;
    const entry = [...uniqueMap.values()].find(it => it.uid === u);
    if (!entry) return;
    listItem.innerHTML = '';
    const a = document.createElement('a');
    a.href = entry.pdfUrl || (`https://www.google.com/search?q=${encodeURIComponent(entry.name + (entry.year ? ' ' + entry.year : '') + ' pdf')}`);
    a.target = '_blank';
    a.rel = 'noopener noreferrer';
    a.textContent = entry.year ? `${entry.name}, ${entry.year}` : entry.name;
    listItem.appendChild(a);
    const meta = document.createElement('div');
    meta.className = 'laf-meta';
    meta.textContent = entry.type === 'pdf' ? 'Direct PDF found' : 'No direct PDF — Google search';
    listItem.appendChild(meta);
  }

  /* ---------- Build list UI after scanning ---------- */
  function populateSidebar(ui) {
    const listWrap = ui.listWrap;
    listWrap.innerHTML = '';
    if (uidOrder.length === 0) {
      const none = document.createElement('div');
      none.className = 'laf-item';
      none.textContent = 'No Acts/Bills/Rules detected on this page.';
      const hint = document.createElement('div');
      hint.className = 'laf-meta';
      const pageQuery = (document.title || location.hostname || '').trim() + ' law act pdf';
      hint.innerHTML = `Try a web search: <a href="https://www.google.com/search?q=${encodeURIComponent(pageQuery)}" target="_blank" rel="noopener noreferrer">Search</a>`;
      listWrap.appendChild(none); listWrap.appendChild(hint);
      return;
    }

    // For each unique uid in order, create list item
    uidOrder.forEach((u) => {
      const entry = [...uniqueMap.values()].find(it => it.uid === u);
      if (!entry) return;
      const li = document.createElement('div');
      li.className = 'laf-item';
      li.dataset.uid = u;
      // placeholder link text while fetching
      const a = document.createElement('a');
      a.href = '#';
      a.textContent = entry.year ? `${entry.name}, ${entry.year}` : entry.name;
      a.addEventListener('click', (e) => {
        e.preventDefault();
        // find first span with this uid
        const span = document.querySelector(`span.la-mention[data-uid="${u}"]`);
        if (span) {
          const rect = span.getBoundingClientRect();
          const targetY = rect.top + window.scrollY - (window.innerHeight / 2) + (rect.height / 2);
          window.scrollTo({ top: Math.max(0, targetY), behavior: 'smooth' });
          // flash highlight
          span.classList.remove('la-flash');
          // force reflow to restart animation
          void span.offsetWidth;
          span.classList.add('la-flash');
        }
        // open link if available
        const mapEntry = [...uniqueMap.values()].find(it => it.uid === u);
        if (!mapEntry) return;
        if (mapEntry.type === 'pdf' && mapEntry.pdfUrl) {
          window.open(mapEntry.pdfUrl, '_blank', 'noopener');
        } else if (mapEntry.type === 'search' && mapEntry.pdfUrl) {
          window.open(mapEntry.pdfUrl, '_blank', 'noopener');
        } else {
          // trigger fetch and open when ready
          queuedFetch(mapEntry.year ? `${mapEntry.name} ${mapEntry.year}` : mapEntry.name).then(res => {
            mapEntry.pdfUrl = res.url; mapEntry.type = res.type;
            updateSidebarEntry(u);
            window.open(res.url, '_blank', 'noopener');
          });
        }
      });
      li.appendChild(a);

      const meta = document.createElement('div');
      meta.className = 'laf-meta';
      meta.textContent = 'Looking up PDF…';
      li.appendChild(meta);

      listWrap.appendChild(li);
    });
  }

  /* ---------- After scan: start fetching for each unique entry ---------- */
  async function resolveAllEntries(ui) {
    const entries = uidOrder.map(u => [...uniqueMap.values()].find(it => it.uid === u));
    for (const entry of entries) {
      try {
        const q = entry.year ? `${entry.name} ${entry.year}` : entry.name;
        const res = await queuedFetch(q);
        entry.pdfUrl = res.url;
        entry.type = res.type;
        updateSidebarEntry(entry.uid);
      } catch (err) {
        entry.pdfUrl = `https://www.google.com/search?q=${encodeURIComponent(entry.name + (entry.year ? ' ' + entry.year : '') + ' pdf')}`;
        entry.type = 'search';
        updateSidebarEntry(entry.uid);
      }
      updateOpenAllButton(ui);
      buildAccordionContent(ui);
    }
    updateOpenAllButton(ui);
    buildAccordionContent(ui);
  }

  function updateOpenAllButton(ui) {
    const pdfCount = uidOrder.map(u => [...uniqueMap.values()].find(it => it.uid === u)).filter(e => e && e.type === 'pdf' && e.pdfUrl).length;
    ui.openAllBtn.textContent = `Open All PDFs (${pdfCount})`;
    ui.openAllBtn.disabled = pdfCount === 0;
  }

  function buildAccordionContent(ui) {
    const acc = ui.accContent;
    acc.innerHTML = '';
    const pdfEntries = uidOrder.map(u => [...uniqueMap.values()].find(it => it.uid === u)).filter(e => e && e.type === 'pdf' && e.pdfUrl);
    if (pdfEntries.length === 0) {
      const hint = document.createElement('div');
      hint.className = 'laf-meta';
      hint.textContent = 'No direct PDFs found yet.';
      acc.appendChild(hint);
      return;
    }
    pdfEntries.forEach(pe => {
      const row = document.createElement('div');
      row.className = 'la-pdf-item';
      const a = document.createElement('a');
      a.href = pe.pdfUrl;
      a.target = '_blank';
      a.rel = 'noopener noreferrer';
      a.textContent = pe.year ? `${pe.name}, ${pe.year}` : pe.name;
      row.appendChild(a);
      acc.appendChild(row);
    });
  }

  /* ---------- Main init ---------- */
  function init() {
    try {
      injectStyles();
      scanAndAnnotate(document.body);
      // build UI
      const ui = buildUI();
      // populate sidebar with placeholders (we'll update after fetching)
      populateSidebar(ui);
      // kick off fetches for entries
      resolveAllEntries(ui);

      // update sidebar when uniqueMap changes by mapping each uid to sidebar items
      // also update open all button after fetches
      // nothing else needed.

      // Safety: if core scanning found zero items, leave tab present but show helpful message
      if (uidOrder.length === 0) {
        // still keep tab and panel with hint; nothing else to do
        // no extra features added that might hurt detection
      }
    } catch (err) {
      console.error('Legal Acts Finder init error:', err);
    }
  }

  if (document.readyState === 'complete' || document.readyState === 'interactive') {
    setTimeout(init, 300);
  } else {
    window.addEventListener('load', () => setTimeout(init, 300));
  }

})();