Browsing: Red News

(the “OPINION:” eyebrow) so it doesn’t count
// toward the visible-paragraph budget.
var $bodyParagraphs = $btext.children(‘p’).filter(function () {
return !$(this).hasClass(‘article-category’);
});
var $lastVisible = $bodyParagraphs.eq(paragraphsToKeep – 1);

if ($lastVisible.length) {
// Remove EVERYTHING after the last visible paragraph inside .bigtext —
// ads, outbrain, embeds, Insticator block, anything else interleaved.
// This is more robust than maintaining a removal allowlist.
$lastVisible.nextAll().remove();
}

// Reparent the paywall container into .bigtext so Piano injects the
// offer iframe directly beneath the visible body, not at the bottom of
// the page below the (now-removed) commenting widget.
$container.appendTo($btext).addClass(‘template-shown’);

// Insert a story-text fade divider just above the paywall. This is
// anchored to the article body so it stays put when the paywall is
// sticky on desktop, instead of riding along with the iframe (which is
// what the default `::before` gradient in _piano.scss does). Idempotent:
// remove any prior fade first so re-firing showOffer doesn’t stack them.
$btext.find(‘.piano-content-fade’).remove();
$(‘

‘).insertBefore($container);

// Clean up gateable nodes. Order matters here: wrapper selectors must
// come BEFORE their contents so we don’t leave behind an orphan div with
// just the decorative


elements (which is what was happening with
// the Outbrain block — the .twt-content-only-hide wrapper survived
// after we removed the inner .outbrain-unit-desktop).
var selectorsToRemove = [
// Outbrain: remove the entire .twt-content-only-hide wrapper that
// contains the unit (the wrapper has decorative


s above and below)
// before falling back to the inner unit and its content.
‘.twt-content-only-hide:has(.outbrain-unit-desktop)’,
‘.outbrain-unit-desktop’,
‘.OUTBRAIN’,

// Inline ad slots — also wrapped in .twt-content-only-hide, also
// removed via wrapper-first to avoid orphan striped blocks. We use
// the more specific `.inline-article-ad` selector here instead of
// the generic `.ad-slot` so we don’t accidentally remove the top
// leaderboard banner (which also lives in a `.twt-content-only-hide`
// wrapper containing a generic `.ad-slot`).
‘.twt-content-only-hide:has(.inline-article-ad)’,
‘.inline-article-ad’,

// Editorial recirculation surfaces and bottom-of-article ad.
‘.piano-end-article-reco’, // MyTimes recommendations block
‘.piano-end-article-sale’,
‘#article-btf-ad-container’,

// Insticator commenting + trending. Class-based selectors only —
// we used to have a duplicate `id=”insticator-commenting”` on the
// page

that collided with the widget’s id, which an ID-based
// query would have removed alongside the widget. The H1 id has
// since been removed (see also:

), but
// class selectors remain the safer pattern.
‘.insticator-product-group’,
‘.instiengage-core-commenting’,
‘.instiengage-trending-now’,

// Legacy targets — kept defensively in case any older surface lingers.
‘.piano-in-article-reco’,
‘.connatixcontainer’,
‘.twitter-tweet’,
‘#comment-policy’,
‘#comment-block’
];
selectorsToRemove.forEach(function (sel) {
document.querySelectorAll(sel).forEach(function (el) {
el.remove();
});
});

// Post-pass: any .twt-content-only-hide wrapper that’s left with only
// ornamental content (


elements, whitespace, or nothing) is an
// orphan from removed ad/recirc content. Sweep them up so we don’t show
// empty striped blocks between paragraphs.
document.querySelectorAll(‘.twt-content-only-hide’).forEach(function (el) {
var hasMeaningfulContent = Array.prototype.some.call(el.children, function (child) {
return child.tagName !== ‘HR’ && child.tagName !== ‘SCRIPT’;
});
if (!hasMeaningfulContent) {
el.remove();
}
});

// Drop any height that prior code may have set on .bigtext so it grows
// to fit the visible body + paywall iframe naturally. Also clear the
// base `overflow: hidden` from typography.scss — `overflow: hidden`
// on .bigtext would silently break `position: sticky` on the paywall
// by turning .bigtext into a non-scrolling clip context.
$btext.css({ ‘height’: ‘auto’, ‘overflow’: ‘visible’ });
// If EA already unlocked the article, skip locking entirely
if (window.__gnewsUnlocked) return;

var urlParams = new URLSearchParams(window.location.search);
var isEA = urlParams.has(‘gaa_at’);

document.querySelector(offerParams.containerSelector).classList.add(“template-shown”);

var $btext = $(‘.bigtext’);
var threshold = (offerParams.containerSelector == “.piano-content-lock-upper”) ? 3 : 6;

if (isEA) {
// EA flow: use CSS-only hiding (reversible by unlockArticle)
$btext.find(“p”).slice(threshold).addClass(‘ea-hidden’);
document.body.classList.add(‘piano-paywall-applied’);
} else {
// Non-EA flow: existing destructive behavior
$btext.find(“p”).slice(threshold).remove();

var elemsToRemove = [];
elemsToRemove.push(document.querySelector(“.piano-in-article-reco”));
elemsToRemove.push(document.querySelector(“.connatixcontainer”));
elemsToRemove.push(document.querySelector(“.twitter-tweet”));
elemsToRemove.push(document.querySelector(‘#comment-policy’));
elemsToRemove.push(document.querySelector(‘#comment-block’));
elemsToRemove.push(document.querySelectorAll(‘.OUTBRAIN’));
elemsToRemove.forEach(function(item){
if (item) {
item.remove();
}
});
}

var newHeight = 0;
var visibleContent = $btext.find(‘p:visible’);
for (var i=0; i 0 ? navHeight : STICKY_TOP_OFFSET_FALLBACK – 10) + 10;
}

function syncStickyContext() {
var $btext = $(‘.bigtext’);
var $container = $(
‘.piano-content-lock-upper.template-shown, .piano-content-lock.template-shown’
).first();
if (!$btext.length || !$container.length) return;

if (window.innerWidth < 800) { // Mobile: sticky off, no min-height stretch (no sidebar pressure). $btext.css('min-height', ''); $container.removeClass('is-sticky'); return; } // Stretch .bigtext to match the right sidebar so the sticky has the // full column to scroll within. Has no effect when sticky is off. // // Critical: we set min-height ONCE per article and never react to // sidebar size changes from a ResizeObserver. The CSS Grid equal-heights // layout creates a feedback loop — our write grows the column, which // stretches the sidebar, which fires the observer, which triggers a // bigger write, and so on. Late-loading ads are caught by a one-shot // setTimeout in ensureStickyHandlersRegistered, not by observation. if (!$btext.data('paywall-min-height-set')) { var sidebar = document.querySelector('#secondary'); if (sidebar) { $btext.css('min-height', sidebar.offsetHeight + 'px'); $btext.data('paywall-min-height-set', true); } } // Engage sticky only if the iframe + offsets fit in the viewport. var topOffset = getStickyTopOffset(); var iframeHeight = $container.outerHeight(); var availableHeight = window.innerHeight - topOffset - STICKY_BOTTOM_BUFFER; if (iframeHeight > 0 && iframeHeight <= availableHeight) { $container.addClass('is-sticky'); } else { $container.removeClass('is-sticky'); } } // requestAnimationFrame-throttled wrapper so resize/observer storms don't // run sync logic dozens of times per second during a window drag. var syncStickyRafId = null; function syncStickyContextThrottled() { if (syncStickyRafId !== null) return; syncStickyRafId = requestAnimationFrame(function () { syncStickyRafId = null; syncStickyContext(); }); } // One-shot registration: keep observers and listeners at module scope so // showOffer firing more than once never stacks them. var stickyHandlersRegistered = false; function ensureStickyHandlersRegistered() { if (stickyHandlersRegistered) return; stickyHandlersRegistered = true; // We deliberately do NOT observe the sidebar (#secondary). The CSS Grid // equal-heights layout creates a feedback loop with our .bigtext // min-height write that runs away into millions of px. Instead, we // capture the sidebar height once and re-measure on a single delayed // timer to catch late-loading ads/recos. if (window.ResizeObserver) { var ro = new ResizeObserver(syncStickyContextThrottled); // Watch the paywall container itself — Piano can resize the iframe // after init when content like login/checkout flows swap in. This is // safe: the paywall's size doesn't depend on .bigtext min-height. var container = document.querySelector( '.piano-content-lock-upper.template-shown, .piano-content-lock.template-shown' ); if (container) ro.observe(container); // Watch the sticky nav so the top-offset stays accurate when it // appears/disappears on scroll. Also safe. var stickyNav = document.querySelector('.header-container'); if (stickyNav) ro.observe(stickyNav); } window.addEventListener('resize', syncStickyContextThrottled); // One-shot delayed re-measure: invalidate the "min-height already set" // flag and re-sync so late-loading sidebar ads/recos get reflected // exactly once. No continuous observation, no feedback loop. setTimeout(function () { $('.bigtext').removeData('paywall-min-height-set'); syncStickyContextThrottled(); }, 3000); } // Fires after Composer finishes evaluating an experience, regardless of // whether an offer was shown. Use it to release the CSS pre-hide for // visitors who were not paywalled. tp.push(["addHandler", "experienceExecute", function (event) { document.body.classList.add('piano-evaluated'); }]); // Safety timeout: if Piano never reports back (network failure, ad // blocker, Composer misconfig), make sure ads aren't hidden forever. setTimeout(function () { document.body.classList.add('piano-evaluated'); }, 5000);