Skip to main content

HTML Image gallery with shine hover feature (easy for customer to update, pulls alt text) 

1. Create a standard page with all the images you want to feature. Currently works best if divisible by 4. 
2. Add the HTML code below to a page and replace the example URL with the URL of the page you just created.
NOTE: the URL that feeds the photos has to be consistent with the main domain... so youll need to update the URL when launching or updating the main domain. 

A collage of natural landscapes: canyons, arches, mountains, ocean scenes, and a glowing sea.

Put all photos on a separate page and grab the URL, add the URL to the HTML embed code below where indicated. Alt text should pull through, and it will update when customers update that page. leave it published as a "ghost page" linked nowhere.
Cool note: If customers need to crop the photo, crop it on the original page and set it to square and then select the area you want to be focused and it will auto update. 

Scoped Image Gallery
key board navigatable version (not third party tested) 

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <title>Accessible Image Gallery with Keyboard Support</title>
  <style>
    .visually-hidden {
      position: absolute !important;
      width: 1px; height: 1px;
      margin: -1px; padding: 0; border: 0;
      overflow: hidden; clip: rect(0 0 0 0);
      white-space: nowrap;
    }

    .gallery-container * { box-sizing: border-box; margin: 0; padding: 0; }
    .gallery-container { background: #fff; padding: 20px; font-family: Arial, sans-serif; }
    .gallery { display: grid; grid-template-columns: repeat(4, minmax(0,1fr)); gap: 10px; list-style: none; padding: 0; }
    .gallery li { list-style: none; }
    .thumb {
      position: relative; width: 100%; height: 250px;
      overflow: hidden; cursor: pointer; border: 0; background: transparent; padding: 0;
    }
    .thumb img {
      width: 100%; height: 100%; object-fit: cover;
      transition: transform 0.5s ease;
    }
    @media (prefers-reduced-motion: reduce) {
      .thumb img { transition: none; }
    }
    .thumb:focus-visible { outline: 3px solid #005fcc; outline-offset: 2px; }
    .thumb:hover img { transform: scale(1.1); }

    .modal {
      display: none; position: fixed; z-index: 1000; inset: 0;
      background-color: rgba(0,0,0,0.8);
    }
    .modal[open] { display: block; }
    #modalContent { margin: 5% auto; text-align: center; max-width: 1200px; position: relative; }
    #modalImage { max-width: 90%; max-height: 80vh; display: block; margin: 0 auto; }
    .close-button, .nav-button {
      position: absolute; background: transparent; border: none; color: #fff; cursor: pointer;
    }
    .close-button { top: 20px; right: 30px; font-size: 30px; }
    .nav-button { top: 50%; transform: translateY(-50%); font-size: 40px; padding: 10px; }
    .nav-left { left: 20px; }
    .nav-right { right: 20px; }
    .close-button:focus-visible, .nav-button:focus-visible { outline: 3px solid #fff; }

    /* Optional helper for browsers without inert: hide from AT + tab order */
    .aria-hidden { aria-hidden: true; }
  </style>
</head>
<body>
  <div class="gallery-container" id="galleryRoot">
    <h1 class="visually-hidden">Image Gallery</h1>
    <ul class="gallery" id="gallery" role="list"></ul>
  </div>

  <div
    id="modal"
    class="modal"
    role="dialog"
    aria-modal="true"
    aria-labelledby="modalTitle"
    aria-describedby="modalDesc"
  >
    <h2 id="modalTitle" class="visually-hidden">Image preview</h2>

    <div id="modalContent" tabindex="-1">
      <img id="modalImage" alt="" />
      <p id="modalDesc" class="visually-hidden"></p>
      <p id="positionInfo" class="visually-hidden" aria-live="polite"></p>

      <button type="button" class="close-button" id="closeModal" aria-label="Close (Esc)">&times;</button>
      <button type="button" class="nav-button nav-left" id="prevBtn" aria-label="Previous image">&#10094;</button>
      <button type="button" class="nav-button nav-right" id="nextBtn" aria-label="Next image">&#10095;</button>
    </div>
  </div>

  <script>
    document.addEventListener("DOMContentLoaded", () => {
      let lastFocused = null;
      let images = [];
      let currentIndex = 0;

      const galleryRoot = document.getElementById("galleryRoot");
      const gallery = document.getElementById("gallery");
      const modal = document.getElementById("modal");
      const modalContent = document.getElementById("modalContent");
      const modalImage = document.getElementById("modalImage");
      const modalDesc = document.getElementById("modalDesc");
      const positionInfo = document.getElementById("positionInfo");
      const btnClose = document.getElementById("closeModal");
      const btnPrev = document.getElementById("prevBtn");
      const btnNext = document.getElementById("nextBtn");

      // --- Focus trap helpers ---
      const supportsInert = "inert" in HTMLElement.prototype;
      const setBackgroundInteractive = (enabled) => {
        // We only have #galleryRoot as background in this snippet. Adjust as needed.
        if (supportsInert) {
          galleryRoot.inert = !enabled;
        } else {
          // Fallback: remove from tab order & hide from AT while modal is open
          if (!enabled) {
            galleryRoot.classList.add("aria-hidden");
            galleryRoot.setAttribute("aria-hidden", "true");
            galleryRoot.querySelectorAll("[tabindex], a, button, input, select, textarea")
              .forEach(el => el.setAttribute("tabindex", "-1"));
          } else {
            galleryRoot.classList.remove("aria-hidden");
            galleryRoot.removeAttribute("aria-hidden");
            galleryRoot.querySelectorAll("[tabindex='-1']")
              .forEach(el => el.removeAttribute("tabindex"));
          }
        }
      };

      const focusTrapHandler = (e) => {
        if (!modal.hasAttribute("open")) return;
        if (!modal.contains(e.target)) {
          // Redirect stray focus back into modal
          const first = getFocusable()[0];
          first && first.focus();
        }
      };

      const getFocusable = () => {
        return Array.from(modal.querySelectorAll(
          'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
        )).filter(el => !el.hasAttribute("disabled") && el.offsetParent !== null);
      };

      const keyHandler = (e) => {
        if (!modal.hasAttribute("open")) return;

        switch (e.key) {
          case "Escape":
            e.preventDefault();
            closeModal();
            break;
          case "ArrowLeft":
            e.preventDefault();
            showPrev();
            break;
          case "ArrowRight":
            e.preventDefault();
            showNext();
            break;
          case "Home":
            e.preventDefault();
            goTo(0);
            break;
          case "End":
            e.preventDefault();
            goTo(images.length - 1);
            break;
        }
      };

      function openModal(idx) {
        lastFocused = document.activeElement;
        currentIndex = idx;
        updateModal();

        modal.setAttribute("open", "");
        setBackgroundInteractive(false);

        // Move focus into dialog
        modalContent.focus();

        document.addEventListener("keydown", keyHandler);
        document.addEventListener("focusin", focusTrapHandler);
      }

      function closeModal() {
        modal.removeAttribute("open");
        setBackgroundInteractive(true);

        document.removeEventListener("keydown", keyHandler);
        document.removeEventListener("focusin", focusTrapHandler);

        // Restore focus to the thumbnail that opened the dialog
        if (lastFocused && typeof lastFocused.focus === "function") {
          lastFocused.focus();
        }
      }

      function goTo(idx) {
        currentIndex = (idx + images.length) % images.length;
        updateModal();
      }

      function showPrev() { goTo(currentIndex - 1); }
      function showNext() { goTo(currentIndex + 1); }

      function updateModal() {
        const item = images[currentIndex];
        modalImage.src = item.src;
        modalImage.alt = item.alt; // meaningful alt in dialog
        modalDesc.textContent = item.alt; // for aria-describedby
        positionInfo.textContent = `Image ${currentIndex + 1} of ${images.length}`;
      }

      // Clicks
      btnClose.addEventListener("click", closeModal);
      btnPrev.addEventListener("click", showPrev);
      btnNext.addEventListener("click", showNext);

      // Click on backdrop closes (but not on content)
      modal.addEventListener("click", (e) => {
        if (e.target === modal) closeModal();
      });

      // Keyboard on content buttons is handled by default (Enter/Space)

      // --- Build gallery (keeps your remote fetch + alt extraction) ---
      function loadGallery() {
        fetch('https://izzysworld.specialdistrict.org/bucketlist-gallery-attempt')
          .then(r => r.text())
          .then(html => {
            const parser = new DOMParser();
            const doc = parser.parseFromString(html, 'text/html');
            const figs = doc.querySelectorAll('figure');

            images = Array.from(figs).map(f => {
              const img = f.querySelector('img');
              const fallbackAlt =
                f.querySelector('figcaption')?.innerText?.trim() ||
                img?.getAttribute('title') ||
                'Image';
              return img ? { src: img.src, alt: img.alt?.trim() || fallbackAlt } : null;
            }).filter(Boolean);

            // Render as list of buttons
            gallery.innerHTML = '';
            images.forEach((imgData, i) => {
              const li = document.createElement('li');
              li.setAttribute('role', 'listitem');

              const btn = document.createElement('button');
              btn.className = 'thumb';
              btn.type = 'button';
              btn.setAttribute('aria-label', `Open image ${i + 1} of ${images.length}: ${imgData.alt}`);
              btn.dataset.index = i;

              const img = document.createElement('img');
              img.src = imgData.src;
              // Thumbnails are decorative (the button has the accessible name)
              img.alt = '';

              btn.appendChild(img);
              li.appendChild(btn);
              gallery.appendChild(li);
            });

            // Delegate click/keyboard to open
            gallery.addEventListener('click', (e) => {
              const btn = e.target.closest('button.thumb');
              if (!btn) return;
              openModal(parseInt(btn.dataset.index, 10));
            });

            // Optional: allow Arrow navigation between thumbnails when focused
            gallery.addEventListener('keydown', (e) => {
              const current = document.activeElement.closest('button.thumb');
              if (!current) return;
              const idx = parseInt(current.dataset.index, 10);
              if (e.key === 'ArrowRight') {
                e.preventDefault();
                const next = gallery.querySelector(`button.thumb[data-index="${(idx + 1) % images.length}"]`);
                next?.focus();
              } else if (e.key === 'ArrowLeft') {
                e.preventDefault();
                const prev = gallery.querySelector(`button.thumb[data-index="${(idx - 1 + images.length) % images.length}"]`);
                prev?.focus();
              } else if (e.key === 'Home') {
                e.preventDefault();
                gallery.querySelector('button.thumb[data-index="0"]')?.focus();
              } else if (e.key === 'End') {
                e.preventDefault();
                gallery.querySelector(`button.thumb[data-index="${images.length - 1}"]`)?.focus();
              }
            });
          })
          .catch(err => console.error('Gallery load error:', err));
      }

      loadGallery();
    });
  </script>
</body>
</html>

NEWEST EDITION: VALLEY RIVERSIDE

keyboard toggle internal pages, finds all images beneath it and reformats. Screenreader hidden list option. 

<!-- Accessible Inline Gallery (dynamic content safe) -->
<div id="inline-a11y-gallery" data-scope="article#poc, main, body">
  <h2 class="visually-hidden">Image Gallery</h2>
<!-- Add this control and live region just inside #inline-a11y-gallery, above the <ul class="gallery"> -->
<button id="agTogglePlain" class="visually-hidden" aria-controls="inline-a11y-gallery" type="button">
  View plain list of images (toggle)
</button>
<span id="agLive" class="visually-hidden" aria-live="polite"></span>
  <ul class="gallery" role="list"></ul>

  <!-- Modal starts INSIDE so it's easy to copy/paste, but JS will move it to <body> -->
  <div class="modal" role="dialog" aria-modal="true" aria-labelledby="agTitle" aria-describedby="agDesc">
    <h3 id="agTitle" class="visually-hidden">Image preview</h3>
    <div class="modal__content" tabindex="-1">
      <img id="agImg" alt="">
      <p id="agDesc" class="visually-hidden"></p>
      <p id="agPos" class="visually-hidden" aria-live="polite"></p>

      <button type="button" class="modal__close" aria-label="Close (Esc)">&times;</button>
      <button type="button" class="modal__nav modal__nav--prev" aria-label="Previous image">&#10094;</button>
      <button type="button" class="modal__nav modal__nav--next" aria-label="Next image">&#10095;</button>
    </div>

  </div>

  <style>
    .visually-hidden{position:absolute!important;width:1px;height:1px;margin:-1px;padding:0;border:0;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}

    .gallery{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;list-style:none;padding:0;margin:0}
    .thumb{width:100%;height:250px;overflow:hidden;border:0;background:transparent;padding:0;cursor:pointer}
    .thumb img{width:100%;height:100%;object-fit:cover;transition:transform .4s ease}
    .thumb:hover img,.thumb:focus img{transform:scale(1.08)}
    .thumb:focus-visible{outline:3px solid #005fcc;outline-offset:2px}

    /* SINGLE consolidated modal rule: full viewport + opaque backdrop */
    .modal{
      display:none; position:fixed; top:0; left:0; width:100vw; height:100vh;
      background:#000;             /* solid black = no bleed-through */
      z-index:99999;               /* above site chrome */
      opacity:0; transition:opacity .3s ease;
    }
    .modal[open]{display:block; opacity:1}

    .modal__content{margin:5% auto; max-width:1200px; text-align:center; position:relative; outline:none}
    .modal__content img{max-width:90%; max-height:80vh; display:block; margin:0 auto}

    .modal__close,.modal__nav{
      position:absolute; background:rgba(0,0,0,.35); color:#fff; border:0; cursor:pointer;
      border-radius:50%; padding:8px 12px; line-height:1;
      text-shadow:0 0 6px rgba(0,0,0,.7);
    }
    .modal__close{top:20px; right:30px; font-size:30px}
    .modal__nav{top:50%; transform:translateY(-50%); font-size:40px}
    .modal__nav--prev{left:20px}
    .modal__nav--next{right:20px}
    .modal__close:focus-visible,.modal__nav:focus-visible{outline:3px solid #fff}

    /* Respect reduced motion */
    @media (prefers-reduced-motion: reduce){
      .thumb img{transition:none}
      .modal{transition:none}
    }

    /* Your extra rounding overrides */
    .col-md-7 img, .col-md-10 img { border-radius:0!important; }
  </style>

  <script>
  (function(){
    // ===== CONFIG =====
    const WIDGET = document.getElementById('inline-a11y-gallery');
    if(!WIDGET) return;

    const SCOPE_SEL = (WIDGET.getAttribute('data-scope') || 'body')
      .split(',').map(s => s.trim());
    const SCOPE = document.querySelector(SCOPE_SEL.find(s => document.querySelector(s))) || document.body;

    // Match Streamline images (your structure) with a fallback:
    const IMG_SELECTOR = 'figure .poc-editor-image-container > img, figure > img';

    // Nodes (query now; we'll move the modal later)
    const GALLERY = WIDGET.querySelector('.gallery');
    const MODAL = WIDGET.querySelector('.modal');
    const MODAL_CONTENT = WIDGET.querySelector('.modal__content');
    const IMG = WIDGET.querySelector('#agImg');
    const DESC = WIDGET.querySelector('#agDesc');
    const POS = WIDGET.querySelector('#agPos');
    const BTN_CLOSE = WIDGET.querySelector('.modal__close');
    const BTN_PREV  = WIDGET.querySelector('.modal__nav--prev');
    const BTN_NEXT  = WIDGET.querySelector('.modal__nav--next');

    // >>> Move modal to <body> so the black backdrop covers EVERYTHING
    if (MODAL && MODAL.parentElement !== document.body) {
      document.body.appendChild(MODAL);
    }

    // State
    const items = [];                 // [{src, alt}]
    const figures = [];               // matching <figure> we consumed
    const processed = new WeakSet();  // to avoid double-adding
    let current = 0;
    let lastFocused = null;

    const isAfterWidget = (node) =>
      !!(WIDGET.compareDocumentPosition(node) & Node.DOCUMENT_POSITION_FOLLOWING);

    const getAlt = (img, fig) =>
      (img.getAttribute('alt') || '').trim() ||
      (fig?.querySelector('figcaption')?.innerText || '').trim() ||
      img.getAttribute('title') || 'Image';

    const getSrc = (img) =>
      img.getAttribute('src') || img.getAttribute('data-src') || img.getAttribute('data-lazy-src') || '';

    function appendThumb(index){
      const data = items[index];
      const li = document.createElement('li');
      const btn = document.createElement('button');
      btn.className='thumb'; btn.type='button'; btn.dataset.index = String(index);
      btn.setAttribute('aria-label', `Open image ${index+1} of ${items.length}: ${data.alt}`);
      const timg = document.createElement('img'); timg.alt=''; timg.src = data.src;
      btn.appendChild(timg); li.appendChild(btn); GALLERY.appendChild(li);
    }

    function hideFigure(fig){
      if(!fig) return;
      fig.hidden = true;
      fig.setAttribute('aria-hidden','true');
    }

    function syncFromDOM(){
      const found = SCOPE.querySelectorAll(IMG_SELECTOR);
      const toAdd = [];

      found.forEach(img => {
        const fig = img.closest('figure');
        if (!fig || processed.has(fig) || !isAfterWidget(fig)) return;

        const src = getSrc(img);
        if (!src) return;

        const alt = getAlt(img, fig);
        processed.add(fig);
        toAdd.push({ fig, src, alt });
      });

      if (!toAdd.length) return;

      toAdd.sort((a,b) =>
        (a.fig.compareDocumentPosition(b.fig) & Node.DOCUMENT_POSITION_FOLLOWING) ? -1 : 1
      );

      toAdd.forEach(({ fig, src, alt }) => {
        const index = items.length;
        items.push({ src, alt });
        figures.push(fig);
        appendThumb(index);
        hideFigure(fig);
      });

      // Refresh aria-labels after count changes
      GALLERY.querySelectorAll('button.thumb').forEach((btn, i) => {
        const d = items[i];
        btn.dataset.index = String(i);
        btn.setAttribute('aria-label', `Open image ${i+1} of ${items.length}: ${d.alt}`);
      });
    }

    // Initial build
    syncFromDOM();

    // Watch for dynamic content & lazy src changes
    try {
      let debounce;
      const mo = new MutationObserver((mutations) => {
        const relevant = mutations.some(m => {
          if (m.type === 'childList') {
            return Array.from(m.addedNodes).some(n =>
              n.nodeType === 1 && (n.matches?.('figure, .flow-element, .poc-editor-image-container, img') ||
                n.querySelector?.(IMG_SELECTOR)) && isAfterWidget(n));
          }
          if (m.type === 'attributes' && m.target instanceof Element) {
            return isAfterWidget(m.target) && m.target.matches(IMG_SELECTOR);
          }
          return false;
        });
        if (relevant) { clearTimeout(debounce); debounce = setTimeout(syncFromDOM, 50); }
      });
      mo.observe(SCOPE, { childList:true, subtree:true, attributes:true, attributeFilter:['src','data-src','data-lazy-src'] });
    } catch (e) {
      console.warn('[a11y-gallery] MutationObserver issue:', e);
    }

    // ===== Modal (keyboard/ARIA) =====
    function open(i){
      lastFocused = document.activeElement;
      current = (i + items.length) % items.length;
      render();
      MODAL.setAttribute('open','');
      MODAL_CONTENT.focus();
      document.addEventListener('keydown', onKey);
      document.addEventListener('focusin', focusTrap);
    }
    function close(){
      MODAL.removeAttribute('open');
      document.removeEventListener('keydown', onKey);
      document.removeEventListener('focusin', focusTrap);
      lastFocused?.focus?.();
    }
    function go(i){ current = (i + items.length) % items.length; render(); }
    function render(){
      const it = items[current];
      IMG.src = it.src;
      IMG.alt = it.alt;
      DESC.textContent = it.alt;
      POS.textContent = `Image ${current+1} of ${items.length}`;
    }
    function onKey(e){
      if(!MODAL.hasAttribute('open')) return;
      switch(e.key){
        case 'Escape': e.preventDefault(); close(); break;
        case 'ArrowLeft': e.preventDefault(); go(current - 1); break;
        case 'ArrowRight': e.preventDefault(); go(current + 1); break;
        case 'Home': e.preventDefault(); go(0); break;
        case 'End': e.preventDefault(); go(items.length - 1); break;
      }
    }
    function focusTrap(e){
      if(!MODAL.hasAttribute('open')) return;
      if(!MODAL.contains(e.target)) MODAL_CONTENT.focus();
    }

    // Wire up gallery + modal
    GALLERY.addEventListener('click', (e) => {
      const btn = e.target.closest('button.thumb');
      if (btn) open(parseInt(btn.dataset.index, 10));
    });
    GALLERY.addEventListener('keydown', (e) => {
      const btn = e.target.closest('button.thumb');
      if (!btn) return;
      const i = parseInt(btn.dataset.index, 10);
      if (e.key === 'ArrowRight'){ e.preventDefault(); GALLERY.querySelector(`.thumb[data-index="${(i+1)%items.length}"]`)?.focus(); }
      if (e.key === 'ArrowLeft'){  e.preventDefault(); GALLERY.querySelector(`.thumb[data-index="${(i-1+items.length)%items.length}"]`)?.focus(); }
      if (e.key === 'Home'){       e.preventDefault(); GALLERY.querySelector(`.thumb[data-index="0"]`)?.focus(); }
      if (e.key === 'End'){        e.preventDefault(); GALLERY.querySelector(`.thumb[data-index="${items.length-1}"]`)?.focus(); }
    });
    BTN_CLOSE.addEventListener('click', close);
    BTN_PREV.addEventListener('click', () => go(current - 1));
    BTN_NEXT.addEventListener('click', () => go(current + 1));
    MODAL.addEventListener('click', (e) => { if (e.target === MODAL) close(); });
  })();
  </script>
</div>
<script>
(function () {
  const WIDGET = document.getElementById('inline-a11y-gallery');
  if (!WIDGET) return;

  // Reuse nodes you already have:
  const GALLERY = WIDGET.querySelector('.gallery');
  const MODAL   = document.querySelector('body > .modal') || WIDGET.querySelector('.modal'); // moved to <body> earlier
  const TOGGLE  = document.getElementById('agTogglePlain');
  const LIVE    = document.getElementById('agLive');

  // Define where your originals live (same scope you already use):
  const SCOPE = document.querySelector('article#poc') || document.querySelector('main') || document.body;

  // Helper: get all figures AFTER the widget
  const originals = () => Array.from(SCOPE.querySelectorAll('.flow-element figure, figure'))
    .filter(fig => (WIDGET.compareDocumentPosition(fig) & Node.DOCUMENT_POSITION_FOLLOWING) !== 0);

  // Apply hidden collapsing globally (if not already present)
  (function ensureHiddenCSS(){
    const id = 'ag-hidden-style';
    if (document.getElementById(id)) return;
    const style = document.createElement('style');
    style.id = id;
    style.textContent = '[hidden]{display:none !important;}';
    document.head.appendChild(style);
  })();

  function announce(msg){ if (LIVE) LIVE.textContent = msg; }

  function enablePlainMode(){
    // Unhide originals (and their flow-element wrappers if you hid those)
    originals().forEach(fig => {
      fig.hidden = false;
      fig.removeAttribute('aria-hidden');
      const flow = fig.closest('.flow-element');
      if (flow) { flow.hidden = false; flow.removeAttribute('aria-hidden'); }
      // restore tabbables if we had suppressed them earlier
      fig.querySelectorAll('[tabindex="-1"][data-ag-silenced]').forEach(el => {
        el.removeAttribute('tabindex'); el.removeAttribute('aria-hidden'); el.removeAttribute('data-ag-silenced');
      });
    });

    // Disable the gallery completely (so SR/keyboard don’t hit it)
    if (GALLERY) {
      GALLERY.hidden = true;
      GALLERY.setAttribute('aria-hidden', 'true');
      GALLERY.inert = true; // removes from tab order in modern browsers
      // Fallback for older browsers: demote tab stops
      GALLERY.querySelectorAll('a,button,input,select,textarea,[tabindex]').forEach(el => {
        if (el.tabIndex !== -1) { el.setAttribute('data-ag-old-tab', el.getAttribute('tabindex') ?? ''); }
        el.setAttribute('tabindex','-1');
      });
    }
    if (MODAL) { MODAL.hidden = true; MODAL.setAttribute('aria-hidden','true'); }

    localStorage.setItem('agPlainMode','1');
    announce('Plain list mode enabled. Images are shown inline.');
  }

  function enableGalleryMode(){
    // Hide originals again
    originals().forEach(fig => {
      fig.hidden = true;
      fig.setAttribute('aria-hidden','true');
      const flow = fig.closest('.flow-element');
      if (flow) { flow.hidden = true; flow.setAttribute('aria-hidden','true'); }
    });

    // Re-enable gallery
    if (GALLERY) {
      GALLERY.hidden = false;
      GALLERY.removeAttribute('aria-hidden');
      GALLERY.inert = false;
      // Restore any tab stops we demoted
      GALLERY.querySelectorAll('[data-ag-old-tab]').forEach(el => {
        const old = el.getAttribute('data-ag-old-tab');
        if (old === '') { el.removeAttribute('tabindex'); } else { el.setAttribute('tabindex', old); }
        el.removeAttribute('data-ag-old-tab');
      });
    }
    if (MODAL) { MODAL.hidden = false; MODAL.removeAttribute('aria-hidden'); }

    localStorage.setItem('agPlainMode','0');
    announce('Gallery mode enabled.');
  }

  // Toggle button handler
  if (TOGGLE) {
    TOGGLE.addEventListener('click', () => {
      const plain = localStorage.getItem('agPlainMode') === '1';
      plain ? enableGalleryMode() : enablePlainMode();
    });
    // Optional: give SR users a hint when they tab to the control
    TOGGLE.addEventListener('focus', () => {
      announce('Press Enter to switch between gallery and plain image list.');
    });
  }

  // Honor saved preference on load
  if (localStorage.getItem('agPlainMode') === '1') {
    // Wait a tick so your initial hide/show logic has run
    setTimeout(enablePlainMode, 0);
  } else {
    // Default to gallery
    setTimeout(enableGalleryMode, 0);
  }
})();
</script>