Browsing: Red News

Harrison Berger is a correspondent at The American Conservative. He has contributed to Drop Site News, The Nation, and Responsible Statecraft. Previously, he was a researcher and producer for System Update with Glenn Greenwald. His work focuses on civil liberties and U.S. foreign policy. He studied Political Science and Russian Studies at Union College (NY).

(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);