// ==UserScript== // @name TheVerge Unlimited Reading // @namespace https://github.com/arg0WAK // @version 2026-04-26 // @description Kills paywall overlays and gives you your scroll back on The Verge. // @author github.com/arg0WAK // @match https://www.theverge.com/* // @icon https://www.google.com/s2/favicons?sz=64&domain=theverge.com // @grant none // ==/UserScript== const BLOCKED_SELECTORS = [ ".zephr-zone-footer", ".zephr-footer-container", ".zephr-overlay", "#zephr-zone-footer", "#zephr-inline-container", "#zephr-footer-container", "#zephr-overlay", ]; const ROOT_SELECTORS = [ "#zephr-anchor", "#zepyhr-anchor", '[id*="zephr"][id*="anchor"]', '[id*="zepyhr"][id*="anchor"]', ]; const TARGET_SELECTOR = ".duet--article--article-body-component"; const TARGET_CONTAINER_SELECTOR = ".duet--article--article-body-component-container"; const TARGET_KEY_ATTR = "data-duet-guard-key"; let nextTargetKey = 1; const cacheByRoot = new Map(); const isElement = (node) => node && node.nodeType === Node.ELEMENT_NODE; const getRoots = () => { const roots = new Set(); ROOT_SELECTORS.forEach((selector) => { document.querySelectorAll(selector).forEach((root) => roots.add(root)); }); return [...roots]; }; const isInsideProtectedRoot = (node) => { if (!isElement(node)) return false; return ROOT_SELECTORS.some((selector) => { try { return Boolean(node.closest(selector)); } catch { return false; } }); }; const hasTarget = (node) => { if (!isElement(node)) return false; return ( node.matches(TARGET_SELECTOR) || Boolean(node.querySelector(TARGET_SELECTOR)) ); }; const shouldBlockMutation = (parentNode, candidateNode) => { if (!hasTarget(candidateNode)) return false; return ( isInsideProtectedRoot(candidateNode) || isInsideProtectedRoot(parentNode) ); }; const ensureTargetKey = (node) => { if (!node.getAttribute(TARGET_KEY_ATTR)) { node.setAttribute(TARGET_KEY_ATTR, `duet-guard-${nextTargetKey++}`); } return node.getAttribute(TARGET_KEY_ATTR); }; const snapshotTargets = (root) => { let cache = cacheByRoot.get(root); if (!cache) { cache = new Map(); cacheByRoot.set(root, cache); } root.querySelectorAll(TARGET_SELECTOR).forEach((node) => { cache.set(ensureTargetKey(node), node); }); }; const restoreTargets = (root) => { const cache = cacheByRoot.get(root); if (!cache || !cache.size) return; const container = root.querySelector(TARGET_CONTAINER_SELECTOR) || root; cache.forEach((node) => { const missing = !node || !node.isConnected || !root.contains(node); if (!missing) return; container.appendChild(node); }); }; const pruneDetachedRoots = () => { cacheByRoot.forEach((_, root) => { if (!root.isConnected) cacheByRoot.delete(root); }); }; const enforceTargets = () => { getRoots().forEach((root) => { snapshotTargets(root); restoreTargets(root); }); pruneDetachedRoots(); }; const installDomProtectionPatch = (() => { let installed = false; return () => { if (installed) return; installed = true; const originalRemoveChild = Node.prototype.removeChild; const originalRemove = Element.prototype.remove; const originalReplaceChildren = Element.prototype.replaceChildren; const innerHtmlDescriptor = Object.getOwnPropertyDescriptor( Element.prototype, "innerHTML", ); Node.prototype.removeChild = function patchedRemoveChild(child) { if (shouldBlockMutation(this, child)) return child; return originalRemoveChild.apply(this, arguments); }; Element.prototype.remove = function patchedRemove() { if (shouldBlockMutation(this.parentElement, this)) return; return originalRemove.apply(this, arguments); }; Element.prototype.replaceChildren = function patchedReplaceChildren() { if (shouldBlockMutation(this, this)) return; return originalReplaceChildren.apply(this, arguments); }; if (innerHtmlDescriptor?.set) { Object.defineProperty(Element.prototype, "innerHTML", { configurable: true, enumerable: innerHtmlDescriptor.enumerable, get: innerHtmlDescriptor.get, set(value) { if (shouldBlockMutation(this, this)) return; return innerHtmlDescriptor.set.call(this, value); }, }); } }; })(); const normalizeRootStyles = (node) => { if (node !== document.body && node !== document.documentElement) return; const computed = getComputedStyle(node); if (computed.overflow === "hidden") { node.style.setProperty("overflow", "unset", "important"); } if (computed.position === "fixed") { node.style.cssText += ` position:unset !important; padding:0 !important; top:unset !important; `; } }; const cleanupZephrOverlays = () => { const anchor = document.querySelector("#zephr-anchor"); if (!anchor) return; anchor.querySelectorAll("div[style]").forEach((node) => { const style = node.getAttribute("style") || ""; const isEmpty = node.children.length === 0 && !node.textContent.trim(); const isGradient = style.includes("linear-gradient"); const isAbsoluteFullOverlay = style.includes("position: absolute") && style.includes("width: 100%") && style.includes("height: 100%"); if (isEmpty && (isGradient || isAbsoluteFullOverlay)) { node.remove(); } }); }; const removeBlockedNodes = () => { BLOCKED_SELECTORS.forEach((selector) => { document.querySelectorAll(selector).forEach((node) => node.remove()); }); }; const runCleanup = () => { removeBlockedNodes(); cleanupZephrOverlays(); enforceTargets(); }; new MutationObserver((mutations) => { mutations.forEach((mutation) => { if (mutation.type === "attributes") { normalizeRootStyles(mutation.target); } if (mutation.type === "childList") { runCleanup(); } }); }).observe(document, { subtree: true, childList: true, attributes: true, attributeFilter: ["style"], }); [document.body, document.documentElement].forEach(normalizeRootStyles); installDomProtectionPatch(); runCleanup();