ABOUT SPLENDID MOLA

// v2.3.1
(function () {
  const style = document.createElement("style");
  style.innerHTML = `
    /* Accessibility Button */
    #accessibility-toggle {
      position: fixed;
      bottom: 25px;
      right: 25px;
      z-index: 9999;
      background: linear-gradient(135deg, #2563eb, #3b82f6);
      color: white;
      border: none;
      border-radius: 16px;
      width: 56px;
      height: 56px;
      display: flex;
      align-items: center;
      justify-content: center;
      box-shadow: 0 4px 20px rgba(37, 99, 235, 0.2);
      cursor: pointer;
      transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
    }
    
    #accessibility-toggle:hover {
      transform: translateY(-2px);
      box-shadow: 0 6px 24px rgba(37, 99, 235, 0.3);
    }

    #accessibility-toggle svg {
      width: 28px;
      height: 28px;
    }

    /* Accessibility Widget */
    #accessibility-widget {
      display: none;
      position: fixed;
      bottom: 90px;
      right: 25px;
      z-index: 10000;
      width: min(90vw, 380px);
      background: #ffffff;
      border-radius: 20px;
      box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
      padding: 24px;
      font-family: system-ui, -apple-system, sans-serif;
      transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
      max-height: calc(100vh - 120px);
      overflow-y: auto;
    }

    #accessibility-widget.open {
      display: block;
      animation: slideIn 0.3s ease-out;
    }

    @keyframes slideIn {
      from {
        opacity: 0;
        transform: translateY(10px);
      }
      to {
        opacity: 1;
        transform: translateY(0);
      }
    }

    #accessibility-widget h2 {
      font-size: 1.5rem;
      font-weight: 600;
      color: #1f2937;
      margin-bottom: 20px;
      padding-bottom: 12px;
      border-bottom: 2px solid #f3f4f6;
    }

    .widget-section {
      margin-bottom: 20px;
    }

    .widget-section-title {
      font-size: 0.875rem;
      font-weight: 600;
      color: #6b7280;
      margin-bottom: 12px;
      text-transform: uppercase;
      letter-spacing: 0.05em;
    }

    #accessibility-widget .button-group {
      display: grid;
      grid-template-columns: repeat(2, 1fr);
      gap: 8px;
      margin-bottom: 8px;
    }

    #accessibility-widget button {
      width: 100%;
      padding: 12px;
      border: none;
      border-radius: 12px;
      background: #f3f4f6;
      color: #374151;
      font-size: 0.875rem;
      font-weight: 500;
      cursor: pointer;
      transition: all 0.2s;
      display: flex;
      align-items: center;
      justify-content: center;
      gap: 8px;
    }

    #accessibility-widget button:hover {
      background: #e5e7eb;
      transform: translateY(-1px);
    }

    /* Feature Groups */
    .text-controls { background: #eef2ff !important; color: #4f46e5 !important; }
    .visual-controls { background: #f0fdf4 !important; color: #16a34a !important; }
    .reading-controls { background: #fff7ed !important; color: #ea580c !important; }
    .navigation-controls { background: #eff6ff !important; color: #2563eb !important; }

    /* Summary Modal */
    .summary-overlay {
      position: fixed;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
      width: min(90vw, 600px);
      max-height: 80vh;
      background: #ffffff;
      border-radius: 24px;
      padding: 32px;
      z-index: 10001;
      overflow-y: auto;
      box-shadow: 0 20px 40px rgba(0, 0, 0, 0.2);
    }

    .summary-overlay p {
      font-size: 1rem;
      line-height: 1.6;
      color: #374151;
      margin-bottom: 16px;
    }

    .summary-actions {
      display: flex;
      gap: 12px;
      margin-top: 24px;
    }

    .summary-actions button {
      padding: 12px 24px;
      border-radius: 12px;
      font-weight: 500;
      transition: all 0.2s;
    }

    .close-button {
      background: #ef4444 !important;
      color: white !important;
    }

    .read-button {
      background: #2563eb !important;
      color: white !important;
    }

    /* Footer */
    #accessibility-widget .footer {
      margin-top: 20px;
      padding-top: 16px;
      border-top: 2px solid #f3f4f6;
      text-align: center;
      font-size: 0.75rem;
      color: #6b7280;
    }

    #accessibility-widget .footer a {
      color: #2563eb;
      text-decoration: none;
      font-weight: 500;
    }

    @font-face {
      font-family: 'OpenDyslexic3';
      src: url("https://website-widgets.pages.dev/fonts/OpenDyslexic3-Regular.woff") format("woff"), url("https://website-widgets.pages.dev/fonts/OpenDyslexic3-Regular.ttf") format("truetype");
    }

    /* Loader Styles */
    .loader-overlay {
      position: fixed;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      background: rgba(0, 0, 0, 0.8);
      display: flex;
      justify-content: center;
      align-items: center;
      z-index: 10001;
    }
    
    .loader {
      width: 48px;
      height: 48px;
      border: 5px solid #FFF;
      border-bottom-color: transparent;
      border-radius: 50%;
      animation: rotation 1s linear infinite;
    }

    @keyframes rotation {
      0% { transform: rotate(0deg); }
      100% { transform: rotate(360deg); }
    }

    .inverted-colors {
      filter: invert(1);
    }

    .highlight-links a {
      padding: 2px 4px;
      background-color: #fef3c7;
      border: 2px solid #f59e0b;
      border-radius: 4px;
      text-decoration: none !important;
      color: #000 !important;
      transition: all 0.2s ease;
    }

    .highlight-links a:hover {
      background-color: #fcd34d;
      border-color: #d97706;
      box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
      transform: translateY(-1px);
    }

    .highlight-links a:focus {
      outline: 3px solid #2563eb;
      outline-offset: 2px;
    }

    .hide-images img {
      display: none;
    }

    .hide-images [style*="background-image"] {
      background-image: none !important;
    }
      
  `;

  document.head.appendChild(style);

  // Create the toggle button
  const toggleButton = document.createElement("div");
  toggleButton.id = "accessibility-toggle";
  toggleButton.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"  fill="#e8eaed"><path d="M480-720q-33 0-56.5-23.5T400-800q0-33 23.5-56.5T480-880q33 0 56.5 23.5T560-800q0 33-23.5 56.5T480-720ZM360-80v-520H120v-80h720v80H600v520h-80v-240h-80v240h-80Z"/></svg>`
  toggleButton.style.cursor = "default";
  toggleButton.style.position = "fixed";
  toggleButton.style.bottom = "20px";
  toggleButton.style.padding = "4px"
  document.body.appendChild(toggleButton);

  // Create the widget
  const widget = document.createElement("div");
  widget.id = "accessibility-widget";
  widget.innerHTML = `
    <h2>Accessibility Options</h2>
    
    <div class="widget-section">
      <div class="widget-section-title">Text Adjustments</div>
      <div class="button-group">
        <button id="increase-text" class="text-controls">Increase Text</button>
        <button id="decrease-text" class="text-controls">Decrease Text</button>
        <button id="line-height" class="text-controls">Line Height</button>
        <button id="letter-spacing" class="text-controls">Letter Spacing</button>
        <button id="dyslexic-font" class="text-controls">Dyslexic Font</button>
      </div>
    </div>

    <div class="widget-section">
      <div class="widget-section-title">Visual Preferences</div>
      <div class="button-group">
        <button id="invert-colors" class="visual-controls">Invert Colors</button>
        <button id="high-contrast" class="visual-controls">High Contrast</button>
        <button id="increase-saturation" class="visual-controls">Increase Saturation</button>
        <button id="decrease-saturation" class="visual-controls">Decrease Saturation</button>
      </div>
    </div>

    <div class="widget-section">
      <div class="widget-section-title">Reading Assistance</div>
      <div class="button-group">
        <button id="toggle-reading" class="reading-controls">Read Screen</button>
        <button id="summarize" class="reading-controls">Summarize</button>
        <button id="check-images" class="reading-controls">Check Images</button>
      </div>
    </div>

    <div class="widget-section">
      <div class="widget-section-title">Navigation</div>
      <div class="button-group">
        <button id="highlight-links" class="navigation-controls">Highlight Links</button>
        <button id="big-cursor" class="navigation-controls">Big Cursor</button>
        <button id="hide-images" class="navigation-controls">Toggle Images</button>
      </div>
    </div>

    <div class="footer">
      Developed by <a href="https://mariancollege.org" target="_blank">mariancollege.org</a> |
      <a href="https://github.com/Jerit-Baiju/a11y-widget/" target="_blank">Contribute</a>
    </div>
  `;
  document.body.appendChild(widget);

  // Utility Functions
  const toggleClassOnBody = (className) => {
    if (className === 'hide-images') {
      document.body.classList.toggle(className);
      document.querySelectorAll('div').forEach(div => div.classList.toggle(className));
    } else {
      document.body.childNodes.forEach(child => {
        if (child.nodeType === 1) {
          child.classList.toggle(className)
        }
      })
    }
  };

  const validateImages = () => {
    const images = document.querySelectorAll("img");
    const missingAltImages = [];

    images.forEach((img, index) => {
      if (!img.hasAttribute("alt") || img.alt.trim() === "") {
        missingAltImages.push({
          index: index + 1,
          src: img.src || "Unknown Source",
        });
      }
    });

    if (missingAltImages.length > 0) {
      const errorMessage = missingAltImages
        .map(
          (img) =>
            `Image ${img.index}: ${img.src.length > 50 ? img.src.substring(0, 50) + "..." : img.src
            }`
        )
        .join("\n");
      alert(
        `Accessibility Issue:\nThe following images are missing 'alt' attributes:\n\n${errorMessage}`
      );
    } else {
      alert("All images have proper 'alt' attributes. Great job!");
    }
  };

  function adjustSaturation(action) {
    const body = document.body;
    let currentSaturation = parseFloat(getComputedStyle(body).getPropertyValue('--saturation') || 1);

    if (action === "increase") {
      currentSaturation += 1;
    } else if (action === "decrease") {
      currentSaturation -= 1;
    }

    body.style.setProperty('--saturation', currentSaturation);
    document.body.childNodes.forEach(child => {
      if (child.nodeType === 1) {
        child.style.filter = `saturate(${currentSaturation})`;
      }
    })
  }

  // Function to toggle widget visibility
  function toggleWidgetVisibility() {
    if (widget.style.display === 'none' || widget.style.display === '') {
      widget.style.display = 'block';
    } else {
      widget.style.display = 'none';
    }
  }

  function enableDyslexicFont(load = false) {
    let isDyslexicFontEnabled = parseInt(localStorage.getItem('isDyslexicFontEnabled')) || 0;
    if (load) {
      isDyslexicFontEnabled = !isDyslexicFontEnabled;
    }
    if (!isDyslexicFontEnabled) {
      document
        .querySelectorAll("*")
        .forEach((el) => {
          if (!el.classList.contains('material-icons')) {
            let orgFontFamily = el.style['font-family']; // Fixed undefined variable
            el.setAttribute('data-asw-orgFontFamily', orgFontFamily);
            el.style['font-family'] = 'OpenDyslexic3';
          }
        });

      localStorage.setItem('isDyslexicFontEnabled', 1);
    } else {
      document
        .querySelectorAll("*")
        .forEach((el) => {
          if (!el.classList.contains('material-icons')) {
            orgFontFamily = el.getAttribute('data-asw-orgFontFamily');
            if (orgFontFamily) {
              el.style['font-family'] = orgFontFamily;
              el.removeAttribute('data-asw-orgFontFamily');
            }
            else {
              el.style.removeProperty('font-family');
            }
          }
        });

      localStorage.setItem('isDyslexicFontEnabled', 0);
    }
  }

  function adjustLetterSpacing(increment = 0) {
    let isLetterSpacingEnabled = parseInt(localStorage.getItem('isLetterSpacingEnabled'));
    if (!increment) {
      isLetterSpacingEnabled = !isLetterSpacingEnabled;
      increment = 0.1;
    }
    if (!isLetterSpacingEnabled) {
      document
        .querySelectorAll("*")
        .forEach((el) => {
          if (!el.classList.contains('material-icons')) {

            let orgLetterSpacing = el.getAttribute('data-asw-orgLetterSpacing');

            if (!orgLetterSpacing) {
              orgLetterSpacing = el.style['letter-spacing'];
              el.setAttribute('data-asw-orgLetterSpacing', orgLetterSpacing);
              if (!(orgLetterSpacing)) {
                orgLetterSpacing = 0;
              }
              orgLetterSpacing = parseFloat(orgLetterSpacing);
              let newLetterSpacing = orgLetterSpacing + increment;
              el.style['letter-spacing'] = newLetterSpacing + 'em';
            }
          }
        });

      localStorage.setItem('isLetterSpacingEnabled', 1);
    } else {
      document
        .querySelectorAll("*")
        .forEach((el) => {
          if (!el.classList.contains('material-icons')) {
            let orgLetterSpacing = el.getAttribute('data-asw-orgLetterSpacing');
            if (orgLetterSpacing) {
              el.style['letter-spacing'] = orgLetterSpacing;
              el.removeAttribute('data-asw-orgLetterSpacing');
            }
            else {
              el.style.removeProperty('letter-spacing');
            }
          }
        });

      localStorage.setItem('isLetterSpacingEnabled', 0);
    }
  }
  function adjustLineHeight(increment = 0) {
    let isLineHeightEnabled = parseInt(localStorage.getItem('isLineHeightEnabled'));
    if (!increment) {
      isLineHeightEnabled = !isLineHeightEnabled;
      increment = 1;
    }
    if (!isLineHeightEnabled) {
      document
        .querySelectorAll("*")
        .forEach((el) => {
          if (!el.classList.contains('material-icons')) {
            let orgLineHeight = el.getAttribute('data-asw-orgLineHeight');

            if (!orgLineHeight) {
              orgLineHeight = el.style['line-height'];
              el.setAttribute('data-asw-orgLineHeight', orgLineHeight);
              if (!orgLineHeight) {
                orgLineHeight = 1.1;
              }
              orgLineHeight = parseFloat(orgLineHeight);
              let newLineHeight = orgLineHeight + increment;
              el.style['line-height'] = newLineHeight;
            }
          }
        });

      localStorage.setItem('isLineHeightEnabled', 1);
    } else {
      document
        .querySelectorAll("*")
        .forEach((el) => {
          if (!el.classList.contains('material-icons')) {
            let orgLineHeight = el.getAttribute('data-asw-orgLineHeight');
            if (orgLineHeight) {
              el.style['line-height'] = orgLineHeight;
              el.removeAttribute('data-asw-orgLineHeight');
            }
            else {
              el.style.removeProperty('line-height');
            }

          }
        });

      localStorage.setItem('isLineHeightEnabled', 0);
    }
  }

  function readText(text, button = null) {
    if (!text || typeof text !== 'string') {
      console.error('Invalid text provided for speech synthesis');
      return;
    }

    if ('speechSynthesis' in window) {
      try {
        // Stop any ongoing speech first
        window.speechSynthesis.cancel();

        // Split text into sentences and filter out lone punctuation marks
        const sentences = text.split(/([.!?]+[\s\n]+|$)/)
          .filter(Boolean)
          .filter(s => !/^[.!?,;:\s]+$/.test(s)); // Skip standalone punctuation
        const chunks = [];

        let currentChunk = '';
        for (const sentence of sentences) {
          const cleanSentence = sentence.trim();
          if (!cleanSentence) continue;

          // If sentence is under 200 chars, try to add it to current chunk
          if (cleanSentence.length <= 200) {
            if ((currentChunk + ' ' + cleanSentence).length <= 200) {
              currentChunk = currentChunk + (currentChunk ? ' ' : '') + cleanSentence;
            } else {
              if (currentChunk) chunks.push(currentChunk);
              currentChunk = cleanSentence;
            }
          } else {
            // Push current chunk if exists
            if (currentChunk) {
              chunks.push(currentChunk);
              currentChunk = '';
            }

            // Split long sentences at word boundaries
            const words = cleanSentence.split(/\s+/);
            let tempChunk = '';

            for (const word of words) {
              if ((tempChunk + ' ' + word).length <= 200) {
                tempChunk = tempChunk + (tempChunk ? ' ' : '') + word;
              } else {
                if (tempChunk) chunks.push(tempChunk);
                tempChunk = word;
              }
            }
            if (tempChunk) chunks.push(tempChunk);
          }
        }

        // Push final chunk if exists
        if (currentChunk) {
          chunks.push(currentChunk);
        }

        // Filter out any remaining empty chunks or punctuation-only chunks
        const validChunks = chunks
          .map(chunk => chunk.trim())
          .filter(chunk => chunk && !/^[.!?,;:\s]+$/.test(chunk));

        let currentChunkIndex = 0;
        let isSpeaking = true;

        function speakNextChunk() {
          if (currentChunkIndex < validChunks.length && isSpeaking) {
            const utterance = new SpeechSynthesisUtterance(validChunks[currentChunkIndex]);
            console.log('Speaking:', validChunks[currentChunkIndex]);

            utterance.onend = () => {
              currentChunkIndex++;
              if (currentChunkIndex >= validChunks.length) {
                // Reset button text when all chunks are read
                if (button) {
                  button.textContent = 'Read Screen';
                }
                window.a11yWidget.isReading = false;
              } else {
                speakNextChunk();
              }
            };

            utterance.onerror = (event) => {
              console.error('Speech synthesis error:', event);
              currentChunkIndex++;
              speakNextChunk();
            };

            window.speechSynthesis.speak(utterance);
          }
        }

        // Store the control functions and state in window for global access
        window.a11yWidget = window.a11yWidget || {};
        window.a11yWidget.isReading = true;
        window.a11yWidget.stopSpeaking = () => {
          isSpeaking = false;
          window.speechSynthesis.cancel();
          if (button) {
            button.textContent = 'Read Screen';
          }
          window.a11yWidget.isReading = false;
        };

        speakNextChunk();
      } catch (error) {
        console.error('Speech synthesis failed:', error);
        alert('Speech synthesis failed. Please try again.');
        if (button) {
          button.textContent = 'Read Screen';
        }
        window.a11yWidget.isReading = false;
      }
    } else {
      alert('Speech synthesis is not supported in your browser.');
      if (button) {
        button.textContent = 'Read Screen';
      }
      window.a11yWidget.isReading = false;
    }
  }

  function stopReading() {
    if (window.a11yWidget && window.a11yWidget.stopSpeaking) {
      window.a11yWidget.stopSpeaking();
    }
    window.speechSynthesis.cancel();
  }

  function adjustContrast(load = false) {
    let isContrastEnabled = parseInt(localStorage.getItem('isContrastEnabled'));
    if (load) {
      isContrastEnabled = !isContrastEnabled;
    }
    if (!isContrastEnabled) {
      document
        .querySelectorAll("*")
        .forEach((el) => {
          let orgColor = el.getAttribute('data-asw-orgContrastColor');
          let orgBgColor = el.getAttribute('data-asw-orgContrastBgColor');

          if (!orgColor) {
            orgColor = el.style.color;
            el.setAttribute('data-asw-orgContrastColor', orgColor);
          }
          if (!orgBgColor) {
            orgBgColor = window.getComputedStyle(el).getPropertyValue('background-color');
            el.setAttribute('data-asw-orgContrastBgColor', orgBgColor);
          }

          el.style["color"] = '#ffff00';
          el.style["background-color"] = '#0000ff';
        });

      localStorage.setItem('isContrastEnabled', 1);
    } else {
      document
        .querySelectorAll("*")
        .forEach((el) => {
          let orgContrastColor = el.getAttribute('data-asw-orgContrastColor');
          let orgContrastBgColor = el.getAttribute('data-asw-orgContrastBgColor');
          if (orgContrastColor) {
            el.style.color = orgContrastColor;
          } else {
            el.style.removeProperty('color');
          }
          if (orgContrastBgColor) {
            el.style.backgroundColor = orgContrastBgColor;
          } else {
            el.style.removeProperty('background-color');
          }
          el.removeAttribute('data-asw-orgContrastColor');
          el.removeAttribute('data-asw-orgContrastBgColor');
        });
      localStorage.setItem('isContrastEnabled', 0);
    }
  }

  function adjustFontSize(step) {
    // Save the updated font size step in local storage
    let currentStep = parseFloat(localStorage.getItem("fontSizeStep")) || 0;
    currentStep += step;
    localStorage.setItem("fontSizeStep", currentStep);

    // Get all elements in the document
    const elements = document.querySelectorAll("*");

    elements.forEach(element => {
      // Get the computed style of the element
      const computedStyle = window.getComputedStyle(element);
      const fontSize = parseFloat(computedStyle.fontSize);

      // Adjust font size
      if (!isNaN(fontSize)) {
        element.style.fontSize = `${fontSize + step}px`;
      }
    });
  }


  function restoreFontSize() {
    const savedStep = parseFloat(localStorage.getItem("fontSizeStep"));
    if (!isNaN(savedStep) && savedStep !== 0) {
      try {
        const elements = document.querySelectorAll("*");
        elements.forEach(element => {
          const computedStyle = window.getComputedStyle(element);
          const fontSize = parseFloat(computedStyle.fontSize);
          if (!isNaN(fontSize)) {
            const newSize = Math.max(fontSize + savedStep, 8); // Prevent too small fonts
            element.style.fontSize = `${newSize}px`;
          }
        });
      } catch (error) {
        console.error('Error restoring font size:', error);
        localStorage.removeItem("fontSizeStep"); // Reset on error
      }
    }
  }
  restoreFontSize()

  function enableHighlightLinks(load = false) {
    let isHighlightLinks = parseInt(localStorage.getItem('isHighlightLinks'));
    if (load) {
      isHighlightLinks = !isHighlightLinks;
    }
    if (!isHighlightLinks) {
      document.body.classList.add('highlight-links');

      localStorage.setItem('isHighlightLinks', 1);
    } else {
      document.body.classList.remove('highlight-links');

      localStorage.setItem('isHighlightLinks', 0);
    }
  }

  function showOverlay(paragraphs) {
    const overlay = document.createElement('div');
    overlay.className = 'summary-overlay';

    paragraphs.forEach(text => {
      const para = document.createElement('p');
      para.textContent = text;
      overlay.appendChild(para);
    });

    const actions = document.createElement('div');
    actions.className = 'summary-actions';

    const closeButton = document.createElement('button');
    closeButton.textContent = 'Close';
    closeButton.className = 'close-button';
    closeButton.onclick = () => document.body.removeChild(overlay);

    const readButton = document.createElement('button');
    readButton.textContent = 'Read Summary';
    readButton.className = 'read-button';
    readButton.onclick = () => {
      if (window.speechSynthesis.speaking) {
        stopReading();
        readButton.textContent = 'Read Summary';
      } else {
        const summaryText = paragraphs.join(' ');
        readText(summaryText, readButton);
        readButton.textContent = 'Stop Reading';
      }
    };

    actions.appendChild(closeButton);
    actions.appendChild(readButton);
    overlay.appendChild(actions);
    document.body.appendChild(overlay);
  }

  // Event Listeners
  toggleButton.addEventListener("click", toggleWidgetVisibility);

  document.addEventListener('click', function (event) {
    if (!widget.contains(event.target) && !toggleButton.contains(event.target)) {
      widget.style.display = 'none';
    }
  });

  function initializeEventListeners() {
    const elements = {
      "increase-text": () => adjustFontSize(2),
      "decrease-text": () => adjustFontSize(-2),
      "line-height": () => adjustLineHeight(1),
      "dyslexic-font": () => enableDyslexicFont(),
      "invert-colors": () => toggleClassOnBody("inverted-colors"),
      "high-contrast": () => adjustContrast(),
      "check-images": validateImages,
      "highlight-links": () => enableHighlightLinks(),
      "hide-images": () => toggleClassOnBody("hide-images"),
      "increase-saturation": () => adjustSaturation("increase"),
      "decrease-saturation": () => adjustSaturation("decrease"),
      "letter-spacing": () => adjustLetterSpacing(0.1),
      "summarize": () => {
        widget.style.display = 'none'; // Hide the widget before showing summary
        summarizeText(extractUniqueDocumentText()).then(summary => {
          showOverlay([summary]);
        })
      },
      "toggle-reading": () => {
        const button = document.getElementById('toggle-reading');
        if (window.speechSynthesis.speaking) {
          stopReading();
          button.textContent = 'Read Screen';
        } else {
          const text = extractUniqueDocumentText();
          readText(text, button);
          button.textContent = 'Stop Reading';
        }
      },
      "big-cursor": () => enableBigCursor(),
    };

    Object.entries(elements).forEach(([id, handler]) => {
      const element = document.getElementById(id);
      if (element) {
        element.addEventListener("click", handler);
      }
    });
  }

  initializeEventListeners();

  function extractUniqueDocumentText() {
    const uniqueTexts = new Set();
    const elements = document.body.querySelectorAll(
      "*:not(#accessibility-widget):not(#accessibility-widget *)"
    );

    // Process each element
    elements.forEach(element => {
      if (element.tagName.toLowerCase() === "img") {
        // Handle images - add alt text if available
        const altText = element.getAttribute("alt");
        if (altText && altText.trim()) {
          uniqueTexts.add(`[Image: ${altText.trim()}]`);
        } else {
          uniqueTexts.add("[Image without description]");
        }
      } else {
        // Handle text elements
        const text = element.innerText;
        if (text && text.trim() &&
          !Array.from(uniqueTexts).some(t => t.includes(text.trim()))) {
          uniqueTexts.add(text.trim());
        }
      }
    });

    // Convert Set to string
    return Array.from(uniqueTexts)
      .filter(text => text.length > 0)
      .join('\n');
  }

  function enableBigCursor(load = false) {
    let isBigCursorEnabled = parseInt(localStorage.getItem('isBigCursorEnabled'));
    if (load) {
      isBigCursorEnabled = !isBigCursorEnabled;
    }
    if (!isBigCursorEnabled) {
      document
        .querySelectorAll("*")
        .forEach((el) => {
          el.style.cursor = `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='64' height='64' viewBox='0 0 512 512'%3E%3Cpath d='M429.742 319.31L82.49 0l-.231 471.744 105.375-100.826 61.89 141.083 96.559-42.358-61.89-141.083 145.549-9.25zM306.563 454.222l-41.62 18.259-67.066-152.879-85.589 81.894.164-333.193 245.264 225.529-118.219 7.512 67.066 152.878z' xmlns='http://www.w3.org/2000/svg'/%3E%3C/svg%3E"), default`;;
        });
      localStorage.setItem('isBigCursorEnabled', 1);
    } else {
      document
        .querySelectorAll("*")
        .forEach((el) => {
          el.style.cursor = 'default';
        });

      localStorage.setItem('isBigCursorEnabled', 0);
    }
  }

  function showLoader() {
    const loaderOverlay = document.createElement('div');
    loaderOverlay.className = 'loader-overlay';
    loaderOverlay.innerHTML = '<div class="loader"></div>';
    document.body.appendChild(loaderOverlay);
    return loaderOverlay;
  }

  async function summarizeText(text) {
    const loader = showLoader();
    try {
      const formData = new FormData();
      formData.append('data', text)
      const response = await fetch('https://a11y-widget.jerit.in/summarize', {
        method: 'POST',
        body: formData
      });

      if (!response.ok) {
        throw new Error('Network response was not ok');
      }

      const data = await response.json();
      return data.summary;
    } catch (error) {
      console.error('Error:', error);
      throw error;
    } finally {
      loader.remove();
    }
  }

  function getTemporaryAltText(img) {
    // Try to generate meaningful temporary alt text from various sources
    const fileName = img.src?.split('/')?.pop()?.split('?')[0]?.split('#')[0] || '';
    const nameWithoutExt = fileName.replace(/\.[^/.]+$/, "");
    const cleanName = nameWithoutExt
      .replace(/[-_]/g, ' ')
      .replace(/([A-Z])/g, ' $1')
      .trim()
      .split(' ')
      .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
      .join(' ');

    // Look for nearby text content
    const parentText = img.parentElement?.textContent?.trim();
    const prevSibling = img.previousElementSibling?.textContent?.trim();
    const nextSibling = img.nextElementSibling?.textContent?.trim();

    // Try to use the most relevant text source
    if (cleanName && cleanName.length > 3 && !/^[0-9]+$/.test(cleanName)) {
      return `Image of ${cleanName}`;
    } else if (parentText && parentText.length < 100) {
      return `Image related to: ${parentText}`;
    } else if (prevSibling && prevSibling.length < 100) {
      return `Image for: ${prevSibling}`;
    } else if (nextSibling && nextSibling.length < 100) {
      return `Image illustrating: ${nextSibling}`;
    }

    // Size-based description as last resort
    const width = img.width || img.naturalWidth;
    const height = img.height || img.naturalHeight;
    if (width && height) {
      const size = width > height ? 'Wide' : height > width ? 'Tall' : 'Square';
      return `${size} image - Loading description...`;
    }

    return 'Image - Loading description...';
  }

  (async function handleImagesWithoutAlt() {
    try {
      const images = document.querySelectorAll('img:not([alt]), img[alt=""]');
      const imageProcessingPromises = [];

      // First pass: Set temporary alt text immediately
      images.forEach((img) => {
        if (!img.src || img.src === '') return;
        const tempAlt = getTemporaryAltText(img);
        img.setAttribute('alt', tempAlt);
        img.setAttribute('data-temp-alt', 'true');
      });

      // Second pass: Process images with API
      images.forEach((img) => {
        if (!img.src || img.src === '') return;

        const processImage = async () => {
          try {
            const isExternal = new URL(img.src, location.href).origin !== location.origin;
            if (isExternal) {
              img.crossOrigin = 'anonymous';
            }

            // Wait for image to load
            await Promise.race([
              new Promise((resolve, reject) => {
                if (img.complete && img.naturalHeight !== 0) {
                  resolve();
                } else {
                  img.onload = resolve;
                  img.onerror = reject;
                }
              }),
              new Promise((_, reject) =>
                setTimeout(() => reject(new Error('Image load timeout')), 5000)
              )
            ]);

            const canvas = document.createElement('canvas');
            const ctx = canvas.getContext('2d');
            canvas.width = img.naturalWidth;
            canvas.height = img.naturalHeight;
            ctx.drawImage(img, 0, 0);

            const imageBlob = await new Promise((resolve, reject) =>
              canvas.toBlob((blob) => (blob ? resolve(blob) : reject(new Error('Blob conversion failed'))), 'image/jpeg')
            );

            const formData = new FormData();
            formData.append('image', imageBlob, 'image.jpg');

            const response = await fetch('https://a11y-widget.jerit.in/upload', {
              method: 'POST',
              body: formData,
            });

            if (!response.ok) {
              throw new Error(`HTTP error! status: ${response.status}`);
            }

            const altText = await response.json();

            // Only update if the current alt text is temporary
            if (img.hasAttribute('data-temp-alt')) {
              img.setAttribute('alt', altText['alt']);
              img.removeAttribute('data-temp-alt');
            }
          } catch (error) {
            console.error(`Error processing image ${img.src}:`, error);
            // Keep the temporary alt text if API fails
            if (!img.hasAttribute('alt') || img.getAttribute('alt').includes('Loading description')) {
              const fallbackAlt = getTemporaryAltText(img).replace('Loading description...', 'Description unavailable');
              img.setAttribute('alt', fallbackAlt);
            }
          }
        };

        imageProcessingPromises.push(processImage());
      });

      // Wait for all images to be processed
      await Promise.allSettled(imageProcessingPromises);

    } catch (error) {
      console.error('Error handling images:', error);
    }
  })();

  window.addEventListener("beforeunload", () => {
    if ('speechSynthesis' in window) {
      window.speechSynthesis.cancel();
    }
  });

  // Cleanup function for page unload
  function cleanup() {
    stopReading();
    delete window.a11yWidget;
  }

  // Handle page unload/reload
  window.addEventListener("beforeunload", cleanup);
  window.addEventListener("unload", cleanup);

  // Handle visibility change (tab switching/minimizing)
  document.addEventListener("visibilitychange", () => {
    if (document.hidden) {
      stopReading();
    }
  });

  function fixHeadingOrder() {
    const headings = Array.from(document.querySelectorAll('h1, h2, h3, h4, h5, h6'));
    let lastActualLevel = 0;

    headings.forEach((heading) => {
      const currentLevel = parseInt(heading.tagName.substring(1));

      // Skip the first heading
      if (lastActualLevel === 0) {
        lastActualLevel = currentLevel;
        return;
      }

      // If heading level skips more than one level
      if (currentLevel > lastActualLevel + 1) {
        // Create new element with correct level
        const newHeading = document.createElement(`h${lastActualLevel + 1}`);
        newHeading.innerHTML = heading.innerHTML;
        newHeading.className = heading.className;

        // Copy all attributes
        Array.from(heading.attributes).forEach(attr => {
          if (attr.name !== 'class') {
            newHeading.setAttribute(attr.name, attr.value);
          }
        });

        // Replace old heading with new one
        heading.parentNode.replaceChild(newHeading, heading);
        lastActualLevel = lastActualLevel + 1;
      } else {
        lastActualLevel = currentLevel;
      }
    });
  }

  // Call immediately after DOM is ready
  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", fixHeadingOrder);
  } else {
    fixHeadingOrder();
  }

  // Add observer to handle dynamically added content
  const observer = new MutationObserver((mutations) => {
    let shouldFix = false;
    mutations.forEach((mutation) => {
      if (mutation.addedNodes.length) {
        mutation.addedNodes.forEach((node) => {
          if (node.nodeName.match(/^H[1-6]$/)) {
            shouldFix = true;
          }
        });
      }
    });
    if (shouldFix) {
      fixHeadingOrder();
    }
  });

  observer.observe(document.body, {
    childList: true,
    subtree: true
  });

  function fixLinkNames() {
    const links = document.querySelectorAll('a');

    links.forEach((link) => {
      // Skip links that are part of the accessibility widget
      if (link.closest('#accessibility-widget')) return;

      // Check if link has text content
      const visibleText = link.textContent.trim();

      // Check if link has aria-label
      const ariaLabel = link.getAttribute('aria-label');

      // Check if link has title
      const title = link.getAttribute('title');

      // Check if link contains an image
      const image = link.querySelector('img');
      const imageAlt = image ? image.getAttribute('alt') : null;

      // If link has no discernible name
      if (!visibleText && !ariaLabel && !title && !imageAlt) {
        // Try to generate a name from the URL
        let urlText = '';
        try {
          const url = new URL(link.href);
          urlText = url.pathname.split('/').pop().replace(/[-_]/g, ' ').replace(/\.[^/.]+$/, '');
          if (!urlText) {
            urlText = url.hostname.replace('www.', '');
          }
        } catch (e) {
          urlText = link.href;
        }

        // Clean up the URL text
        urlText = urlText
          .replace(/([A-Z])/g, ' $1') // Add spaces before capital letters
          .replace(/\s+/g, ' ') // Remove extra spaces
          .trim();

        // Capitalize first letter of each word
        urlText = urlText
          .split(' ')
          .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
          .join(' ');

        // Set aria-label if we generated a name
        if (urlText) {
          link.setAttribute('aria-label', `Link to ${urlText}`);
        }

        // If link is empty (no text or images), add span with generated text
        if (!link.textContent.trim() && !link.querySelector('img')) {
          const span = document.createElement('span');
          span.textContent = urlText;
          span.style.cssText = 'position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;';
          link.appendChild(span);
        }
      }

      // If link contains an image without alt text
      if (image && (!image.hasAttribute('alt') || !image.getAttribute('alt').trim())) {
        let imageAltText = '';

        // Try to generate alt text from parent link's text/aria-label/title
        imageAltText = visibleText || ariaLabel || title || 'Image';

        image.setAttribute('alt', imageAltText);
      }
    });
  }

  // Run the fix immediately
  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", fixLinkNames);
  } else {
    fixLinkNames();
  }

  // Set up observer for dynamically added links
  const linkObserver = new MutationObserver((mutations) => {
    let shouldFix = false;
    mutations.forEach((mutation) => {
      if (mutation.addedNodes.length) {
        mutation.addedNodes.forEach((node) => {
          if (node.nodeName === 'A' || (node.nodeType === 1 && node.querySelector('a'))) {
            shouldFix = true;
          }
        });
      }
    });
    if (shouldFix) {
      fixLinkNames();
    }
  });

  linkObserver.observe(document.body, {
    childList: true,
    subtree: true
  });

  function fixButtonAccessibility() {
    const buttons = document.querySelectorAll('button:not([aria-label]):not([title]), input[type="button"]:not([aria-label]):not([title]), input[type="submit"]:not([aria-label]):not([title]), input[type="reset"]:not([aria-label]):not([title])');

    buttons.forEach((button) => {
      // Skip buttons that are part of the accessibility widget
      if (button.closest('#accessibility-widget')) return;

      const buttonText = button.textContent?.trim() || '';
      const buttonValue = button.value?.trim() || '';
      const buttonImage = button.querySelector('img');
      const buttonIcon = button.querySelector('i, .icon, [class*="icon-"]');

      // If button has no text content
      if (!buttonText && !buttonValue) {
        let accessibleName = '';

        // Check for image alt text
        if (buttonImage && buttonImage.alt) {
          accessibleName = buttonImage.alt;
        }
        // Check for icon aria-label
        else if (buttonIcon && buttonIcon.getAttribute('aria-label')) {
          accessibleName = buttonIcon.getAttribute('aria-label');
        }
        // Try to generate name from class names
        else if (button.className) {
          const classNames = button.className.split(' ')
            .filter(cls => cls.includes('btn-') || cls.includes('button-') || cls.includes('-btn'))
            .map(cls => cls.replace(/(btn-|button-|-btn)/g, ''))
            .map(cls => cls.replace(/[_-]/g, ' '))
            .map(cls => cls.replace(/([A-Z])/g, ' $1').trim())
            .map(cls => cls.charAt(0).toUpperCase() + cls.slice(1).toLowerCase());

          if (classNames.length > 0) {
            accessibleName = `${classNames[0]} button`;
          }
        }

        // If we still don't have a name, try to infer from siblings or context
        if (!accessibleName) {
          // Check for adjacent label or heading
          const prevSibling = button.previousElementSibling;
          const nextSibling = button.nextElementSibling;
          if (prevSibling && (prevSibling.tagName === 'LABEL' || /^H[1-6]$/.test(prevSibling.tagName))) {
            accessibleName = prevSibling.textContent.trim();
          } else if (nextSibling && (nextSibling.tagName === 'LABEL' || /^H[1-6]$/.test(nextSibling.tagName))) {
            accessibleName = nextSibling.textContent.trim();
          }
        }

        // If we still don't have a name, use a generic one based on position
        if (!accessibleName) {
          const buttonIndex = Array.from(document.querySelectorAll('button')).indexOf(button);
          accessibleName = `Button ${buttonIndex + 1}`;
        }

        // Apply the accessible name
        button.setAttribute('aria-label', accessibleName);

        // If it's a completely empty button, add a visually hidden span
        if (!buttonImage && !buttonIcon && !button.innerHTML.trim()) {
          const span = document.createElement('span');
          span.textContent = accessibleName;
          span.style.cssText = 'position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;';
          button.appendChild(span);
        }
      }

      // Ensure button is focusable
      if (!button.getAttribute('tabindex')) {
        button.setAttribute('tabindex', '0');
      }

      // Add role="button" for non-button elements that act as buttons
      if (button.tagName !== 'BUTTON' && !button.getAttribute('role')) {
        button.setAttribute('role', 'button');
      }
    });
  }

  // Run the fix immediately
  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", fixButtonAccessibility);
  } else {
    fixButtonAccessibility();
  }

  // Set up observer for dynamically added buttons
  const buttonObserver = new MutationObserver((mutations) => {
    let shouldFix = false;
    mutations.forEach((mutation) => {
      if (mutation.addedNodes.length) {
        mutation.addedNodes.forEach((node) => {
          if (node.nodeName === 'BUTTON' ||
            (node.nodeType === 1 && (
              node.querySelector('button') ||
              node.querySelector('input[type="button"]') ||
              node.querySelector('input[type="submit"]') ||
              node.querySelector('input[type="reset"]')
            ))
          ) {
            shouldFix = true;
          }
        });
      }
    });
    if (shouldFix) {
      fixButtonAccessibility();
    }
  });

  buttonObserver.observe(document.body, {
    childList: true,
    subtree: true
  });

  function fixIframeAccessibility() {
    const frames = document.querySelectorAll('frame:not([title]), iframe:not([title])');

    frames.forEach((frame) => {
      let frameTitle = '';

      // Try to get title from various sources
      try {
        // Check if frame has a source
        if (frame.src) {
          const url = new URL(frame.src);

          // Try to generate title from URL
          if (url.pathname !== '/') {
            frameTitle = url.pathname
              .split('/')
              .pop()
              .replace(/[-_]/g, ' ')
              .replace(/\.[^/.]+$/, '') // Remove file extension
              .replace(/([A-Z])/g, ' $1') // Add spaces before capital letters
              .trim();
          }

          // If no pathname, use hostname
          if (!frameTitle && url.hostname) {
            frameTitle = url.hostname
              .replace('www.', '')
              .replace(/\.[^.]+$/, ''); // Remove TLD
          }

          // Capitalize words
          frameTitle = frameTitle
            .split(' ')
            .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
            .join(' ');

          frameTitle += ' content';
        }

        // Check for nearby context
        if (!frameTitle) {
          // Look for preceding heading or label
          const prevElement = frame.previousElementSibling;
          if (prevElement && (prevElement.matches('h1, h2, h3, h4, h5, h6, label'))) {
            frameTitle = prevElement.textContent.trim();
          }
        }

        // If still no title, try to get it from the frame's content
        if (!frameTitle && frame.contentDocument) {
          const frameDocument = frame.contentDocument;
          frameTitle = frameDocument.title ||
            frameDocument.querySelector('h1')?.textContent ||
            frameDocument.querySelector('h2')?.textContent;
        }

        // Default fallback
        if (!frameTitle) {
          const frameIndex = Array.from(document.querySelectorAll('iframe, frame')).indexOf(frame);
          frameTitle = `Embedded content ${frameIndex + 1}`;
        }

        // Set the title attribute
        frame.setAttribute('title', frameTitle);

        // Also set aria-label for better screen reader support
        frame.setAttribute('aria-label', frameTitle);

        // Ensure frame is keyboard accessible if interactive
        if (frame.contentDocument &&
          frame.contentDocument.querySelector('button, a, input, select, textarea')) {
          frame.setAttribute('tabindex', '0');
        }

This writers’ happiness movement came about because I was so disturbed the fact that so many of the ways we as writers have access to writing time, get to meet each other, and are able to take care of our bodies and minds require money. This includes things such as conferences, retreats, workshops, and so forth — all wonderful, but all requiring money. And so I decided to try and build a new system of support for writers, where those kinds of things were available to any writer, anywhere, regardless of financial situation.

With this in mind, most of what is offered here for writers is free — online retreats, yoga, salons, tiny grants, and more. Instead of being supported by each individual paying to attend, it’s supported by the Mola Fund, a communal pot made up of mostly-small donations from any writer who wants to and can support this vision. 


This kind of structure isn’t a standard one-to-one exchange for goods or services. It isn't dependent on hierarchy, scarcity, greed, or fear of missing out—all things that are common in the economic world, but aren’t useful when the true end goals are more joy, creativity, and community for all of us. Instead, this is all of us taking care of all of us.  


There are also paid offerings here, such as in-person retreats and live writing containers. These are so I can support myself and Splendid Mola as it grows. To assure access, every paid offering has as many fellowships as possible covering the cost to attend. 


This is a grand experiment. I have no idea if it will work…but I think it will. Because there are so many of us who care about all of us. So many of us are actively choosing love over fear. So many of us who understand how much writing matters, and how important it is to have space to do it — even and especially when it feels like the world might be falling apart. 


Whether you’ve been writing for decades, are just starting to think you might like to write, or are somewhere in between, Splendid Mola is here to remind you — all of you — how important what you do actually is. And to help you do it, as best we can. 

:

WHAT THE HECK IS A MOLA?

splen·did

– ˈsplendəd/

– adjective

• magnificent; very impressive.

mo·la

– ˈmōlə/

– noun

Mola molas, or ocean sunfish, are my favorite fish (I was once a marine biologist; a favorite fish is a requirement). They are the heaviest bony fish in the world, weighing up to 5,000 pounds and reaching up to 15 feet in length and 10 feet from top to bottom. They look somewhat like gigantic smushed blobs, and they often float sideways at the surface of the ocean for reasons that are still mostly unknown. They are delightful, strange, and unwieldy. They tilt to the side. They are often really, REALLY slow, and not even remotely graceful. No one is quite sure why they do some of the things they do, and they often seem to serve no apparent purpose. They are mysterious, goofy, majestic, ponderous, and incredibly, weirdly unique.

In other words, they are somewhat like the creative process.

:

Wisdom Council

The Splendid Mola Wisdom Advisory Council is made up of writers who are part of the community and have volunteered to meet with Lori every couple of months to give feedback on new ideas, help figure out challenges, and generally offer their brilliance, opinions, and thoughts on all of this. The council is currently at full capacity; join the mailing list to know when spaces open up.  

:

HOW DOES THIS WORK, FINANCIALLY?

Splendid Mola is founded on the values of community, art, access, kindness, inclusion, and equity, and it is important to me that the economic system that supports the programs here reflects these values. Hence the Mola Fund, the communal pot that supports all the offerings and fellowships.

The way it works is this: Anyone who wants to support the vision of Splendid Mola can become a patron at $5/month, or can give a one-time donation to the Mola Fund. Then, this communal pot is what covers the programs, fellowships, and grants, so all of us are supporting all of us. But please note! You do not have to “pay in” to attend programs; they are free to attend for any writer, anywhere, any time. “Joining the movement” simply means signing up for the mailing list.

The main amount for the patronage is $5/month, although more is of course welcome. I set it at $5/month because this is small enough that it might seem do-able for many people, but big enough that it can add up quickly.

When we come together with small amounts like this, we can make a huge impact in the lives of writers everywhere. And here’s the big, audacious goal: 100,000 writers supporting the Mola Fund at $5/month or more. Because here’s the grand vision: enough communal support that the Mola Fund is used to:

• Send writers on fully-paid residencies

• Build and manage all-expense-paid co-living spaces for writers

• Give no-strings-attached grants to writers who are also good, kind people.

• And maybe build awesome, creativity-based retirement homes for writers!

Can you imagine all the writers we could help? The books, poems, screenplays, articles, stories, plays, and songs that might be born because all of us supported all of us?

We can change the world with as little as five dollars a month. The power (and love, and community) comes from all of us.

:

GET INVOLVED >

Sign-up for news and updates