PageSourceSearch

https://www.pollsmax.com/assets/pm-ads-20260815bx.js

js pollsmax.com collected 2026-09-28 06:38:10 UTC 192,966 bytes, 4,343 lines download raw bytes

1/**
2 * Pollsmax manual AdSense placements.
3 * PC: page-top 2-up fixed 250px rectangles (logo→Donate) + home/latest/race/forecast mid duals + footer multiplex — never collapse.
4 * Mobile: existing lazy shells (may collapse empty).
5 */
6(function (global) {
7    'use strict';
8
9    var AD_CLIENT = '';
10    var SLOT_DISPLAY = '7546728775';
11    var SLOT_MULTIPLEX = '6233647108';
12    var SLOT_INFEED = '4495536942';
13    var MOBILE_MQ = '(max-width: 767px)';
14    var PUSHED = 'data-pollsmax-ad-pushed';
15    var ET = global.ET || (global.ET = {});
16    var IO_ROOT_MARGIN = '700px 0px 700px 0px';
17    var lazyObserver = null;
18    var priorityScheduled = false;
19
20    function isMobile() {
21        try {
22            return !!(global.matchMedia && global.matchMedia(MOBILE_MQ).matches);
23        } catch (e) {
24            return false;
25        }
26    }
27
28    function isEmbed() {
29        try {
30            return /[?&]embed=1(?:&|$)/.test(location.search || '');
31        } catch (e2) {
32            return false;
33        }
34    }
35
36    function pathNorm() {
37        var p = String((location && location.pathname) || '');
38        p = p.replace(/\/index\.html$/i, '/').replace(/\/+$/, '') || '/';
39        if (p.indexOf('/dynamic/') === 0) p = p.slice('/dynamic'.length) || '/';
40        return p;
41    }
42
43    function pageKind() {
44        var path = pathNorm();
45        var body = document.body;
46        var isForecast = /\/forecast(?:-premium)?\/?$/i.test((location && location.pathname) || '');
47        var isPrimaryPath =
48            /\/(?:democratic-primary|special-democratic-primary|special-republican-primary|republican-primary|republican-primary-runoff|open-primary)\/?$/i.test(path);
49        return {
50            path: path,
51            home: path === '/' || !!(body && body.classList.contains('pollsmax-home-page')),
52            senateHub: path === '/senate' || path === '/2026-us-senate' || !!(body && body.classList.contains('senate-hub-page')),
53            govHub: path === '/governor' || !!(body && body.classList.contains('gov-hub-page')),
54            houseHub: path === '/2026-us-house' || !!(body && body.classList.contains('house-hub-page')),
55            forecast: isForecast,
56            primaryPoll: isPrimaryPath,
57            gePoll: !isForecast && !isPrimaryPath && (
58                /^\/(?:state|senate|governor)\/[^/]+$/i.test(path) ||
59                /^\/2026-us-house\/[^/]+$/i.test(path)
60            ),
61            approval: path.indexOf('/approval/') === 0 ||
62                !!(body && (
63                    body.classList.contains('national-approval-polls-page') ||
64                    body.classList.contains('generic-ballot-page')
65                )),
66            primary2028: path === '/2028-democratic-primary' ||
67                path === '/2028-republican-primary' ||
68                !!(body && body.classList.contains('primary-2028-approval-format')),
69            latestPolls: path === '/latest-polls' || !!(body && body.classList.contains('all-polls-page')),
70            upcomingElections: path === '/upcoming-elections' ||
71                !!(body && body.classList.contains('upcoming-elections-page'))
72        };
73    }
74
75    function ensureAdsense() {
76        return;
77        if (typeof global.PollsmaxEnsureAdsenseScript === 'function') {
78            global.PollsmaxEnsureAdsenseScript();
79        }
80    }
81
82    function shellOf(ins) {
83        return ins && ins.closest
84            ? ins.closest('.pollsmax-ad-banner, .pollsmax-ad-row, .pollsmax-ad-single, aside.pollsmax-site-multiplex, .pollsmax-ad-shell, #home-after-opening-maps')
85            : null;
86    }
87
88    function measureMobilePanelWidth() {
89        var sels = [
90            '.senate-hub-mob-panel--map',
91            '.senate-hub-mob-panel--chart',
92            '.senate-hub-mob-panel--races',
93            '.senate-hub-mob-panel--search',
94            '.senate-hub-mob-panel',
95            'body.race-forecast-mob-panels .senate-hub-mob-panel--chart',
96            'body.pollsmax-home-page .sleek-maps',
97            'body.pollsmax-home-page .sleek-map-pane',
98            '#homeUpcomingElectionsSection',
99            '#searchAllPollsSection'
100        ];
101        var best = 0;
102        var i;
103        var j;
104        var vw = window.innerWidth || document.documentElement.clientWidth || 0;
105        for (i = 0; i < sels.length; i++) {
106            var list = document.querySelectorAll(sels[i]);
107            for (j = 0; j < list.length; j++) {
108                var el = list[j];
109                if (!el || (el.classList && el.classList.contains('pollsmax-ad-shell'))) continue;
110                var r = el.getBoundingClientRect();
111                var w = r && r.width ? r.width : 0;
112                if (w >= 200 && w <= vw + 1 && w > best) best = w;
113            }
114        }
115        if (best >= 200) return Math.round(best);
116        var root = document.documentElement;
117        var cs = root && window.getComputedStyle ? window.getComputedStyle(root) : null;
118        var stdW = cs ? parseFloat(cs.getPropertyValue('--et-mobile-standard-width')) : NaN;
119        if (stdW >= 200) return Math.round(stdW);
120        var pl = cs ? parseFloat(cs.getPropertyValue('--et-mob-channel-pl')) : NaN;
121        var pr = cs ? parseFloat(cs.getPropertyValue('--et-mob-channel-pr')) : NaN;
122        if (!(pl >= 0)) pl = 16;
123        if (!(pr >= 0)) pr = 16;
124        return Math.max(200, Math.round(vw - pl - pr));
125    }
126
127    function usableWidth(ins) {
128        try {
129            var shell = shellOf(ins);
130            if (isMobile() && shell && (isMobileContentMidShell(shell) ||
131                    (shell.classList && shell.classList.contains('pollsmax-site-multiplex')))) {
132                var panelW = measureMobilePanelWidth();
133                if (panelW > 50) return panelW;
134            }
135            /* Collapsed shells report 0×0 — use parent / channel / viewport width. */
136            if (shell && (
137                shell.getAttribute('data-ad-state') === 'waiting' ||
138                shell.getAttribute('data-ad-state') === 'loading'
139            )) {
140                var ch = measureHeaderAdChannel();
141                if (ch && ch.width > 50) return ch.width;
142                var parent = shell.parentElement;
143                if (parent) {
144                    var pw = parent.getBoundingClientRect().width || parent.clientWidth || 0;
145                    if (pw > 50) return pw;
146                }
147                return document.documentElement.clientWidth || window.innerWidth || 0;
148            }
149            var rect = ins.getBoundingClientRect();
150            var w = rect.width;
151            if (w < 50 && ins.parentElement) {
152                w = ins.parentElement.getBoundingClientRect().width || 0;
153            }
154            if (w < 50 && shell) w = shell.getBoundingClientRect().width || 0;
155            return w;
156        } catch (eW) {
157            return 0;
158        }
159    }
160
161    /* Collapse empty slots once Google is clearly done — not before fills can land. */
162    var EMPTY_SOFT_MS = 1200;
163    var EMPTY_HARD_MS = 6000;
164    var EMPTY_HARD_MS_PAGE_TOP = 15000;
165    var EMPTY_HARD_MS_MULTIPLEX = 20000;
166
167    function clearShellLayoutInline(shell) {
168        if (!shell || !shell.style) return;
169        try {
170            shell.style.removeProperty('height');
171            shell.style.removeProperty('max-height');
172            shell.style.removeProperty('min-height');
173            shell.style.removeProperty('overflow');
174            shell.style.removeProperty('opacity');
175            shell.style.removeProperty('margin');
176            shell.style.removeProperty('padding');
177        } catch (eClr) {
178            shell.style.height = '';
179            shell.style.maxHeight = '';
180            shell.style.minHeight = '';
181            shell.style.overflow = '';
182        }
183    }
184
185    /**
186     * Never collapse / hide:
187     * - Multiplex (PC + mobile) — must stay full-size for AdSense fills
188     * - Desktop page-top 2-up
189     */
190    function isStickyPcAd(shell) {
191        if (!shell || !shell.classList) return false;
192        if (shell.classList.contains('pollsmax-site-multiplex')) return true;
193        if (!isMobile() && shell.classList.contains('pollsmax-ad-row--page-top')) return true;
194        return false;
195    }
196
197    /** Hard-collapse reserved space — CSS alone loses to Google placeholder heights. */
198    function setShellCollapsed(shell) {
199        if (!shell || isStickyPcAd(shell)) return;
200        if (isMobile() && isMobileContentMidShell(shell)) {
201            setShellLoading(shell);
202            return;
203        }
204        try {
205            shell.style.setProperty('height', '1px', 'important');
206            shell.style.setProperty('max-height', '1px', 'important');
207            shell.style.setProperty('min-height', '0', 'important');
208            shell.style.setProperty('margin', '0', 'important');
209            shell.style.setProperty('padding', '0', 'important');
210            shell.style.setProperty('overflow', 'hidden', 'important');
211            shell.style.setProperty('opacity', '0', 'important');
212        } catch (eCol) { /* ignore */ }
213    }
214
215    function isHubMobileMidShell(shell) {
216        if (!shell || !shell.id) return false;
217        return (
218            shell.id === 'senateHubAdAfterMapMobile' ||
219            shell.id === 'senateHubAdBeforeRacesMobile' ||
220            shell.id === 'govHubAdAfterMapMobile' ||
221            shell.id === 'govHubAdBeforeRacesMobile' ||
222            shell.id === 'houseHubAdAfterMapMobile' ||
223            shell.id === 'houseHubAdBeforeRacesMobile'
224        );
225    }
226
227    /**
228     * Mobile in-content singles (hub / home / polls / forecast / approval / lists).
229     * These often sit collapsed (0–1px) until Google marks filled — treat like
230     * page-top for retries / soft-empty so a premature `done` does not kill them.
231     * Legacy #hubAdAfterMapKeyMobile is intentionally excluded (always removed).
232     */
233    function isMobileContentMidShell(shell) {
234        if (!shell) return false;
235        if (isHubMobileMidShell(shell)) return true;
236        if (shell.classList && shell.classList.contains('pollsmax-ad-single--poll-mid-mobile')) {
237            return true;
238        }
239        var id = shell.id || '';
240        return (
241            id === 'homeAdBetweenGovAndSearch' ||
242            id === 'homeAdBetweenGbAndDemPrimary' ||
243            id === 'raceGePollAdBeforeTableMobile' ||
244            id === 'raceGePollAdAfterTableMobile' ||
245            id === 'racePrimaryPollAdBeforeTableMobile' ||
246            id === 'racePrimaryPollAdAfterTableMobile' ||
247            id === 'raceForecastAdTopOfSimMobile' ||
248            id === 'raceForecastAdAfterOddsMobile' ||
249            id === 'raceForecastAdUnderPollsMobile' ||
250            id === 'approvalAdBeforeTableMobile' ||
251            id === 'primary2028AdAfterChartMobile' ||
252            id === 'latestPollsAdUnderListMobile' ||
253            id === 'upcomingElectionsAdUnderListMobile'
254        );
255    }
256
257    function setShellProbing(shell) {
258        if (!shell || isStickyPcAd(shell)) return;
259        /* Hub mids: never reserve 90px — stay collapsed until Google marks filled. */
260        if (isHubMobileMidShell(shell)) {
261            setShellLoading(shell);
262            return;
263        }
264        shell.setAttribute('data-ad-state', 'probing');
265        try {
266            shell.style.setProperty('height', '90px', 'important');
267            shell.style.setProperty('max-height', '90px', 'important');
268            shell.style.setProperty('min-height', '0', 'important');
269            shell.style.setProperty('margin', '0', 'important');
270            shell.style.setProperty('overflow', 'hidden', 'important');
271            shell.style.setProperty('opacity', '0', 'important');
272            void shell.offsetWidth;
273        } catch (eProbe) { /* ignore */ }
274    }
275
276    function setShellLoading(shell) {
277        if (!shell || isStickyPcAd(shell)) return;
278        shell.setAttribute('data-ad-state', 'loading');
279        /* Mobile in-content mids: no height cap — Google sizes the creative. */
280        if (isMobile() && isMobileContentMidShell(shell)) {
281            try {
282                shell.style.removeProperty('display');
283                shell.style.setProperty('min-height', '0', 'important');
284                shell.style.setProperty('height', 'auto', 'important');
285                shell.style.setProperty('max-height', 'none', 'important');
286                shell.style.setProperty('opacity', '1', 'important');
287                shell.style.setProperty('overflow', 'visible', 'important');
288                shell.style.setProperty('pointer-events', 'auto', 'important');
289                shell.style.setProperty('background', 'transparent', 'important');
290            } catch (eHubLoad) { /* ignore */ }
291            return;
292        }
293        setShellCollapsed(shell);
294    }
295
296
297    /** Mobile mock white boxes are off unless ?mockads=1. */
298    function wantMockMobileAds() {
299        try {
300            var q = location.search || '';
301            if (/[?&]mockads=1(?:&|$)/i.test(q)) return true;
302            return false;
303        } catch (eWant) {
304            return false;
305        }
306    }
307
308    function isMockAdShell(shell) {
309        return !!(shell && shell.getAttribute && shell.getAttribute('data-pollsmax-ad-mock') === '1');
310    }
311
312    function isMockableMobileAdShell(shell) {
313        if (!shell || !shell.classList) return false;
314        if (shell.classList.contains('pollsmax-ad-row--page-top')) return false;
315        if (shell.classList.contains('pollsmax-ad-single--page-top')) return false;
316        if (shell.classList.contains('pollsmax-site-multiplex')) return true;
317        if (shell.classList.contains('pollsmax-ad-single')) return true;
318        if (shell.classList.contains('pollsmax-ad-row')) return true;
319        return false;
320    }
321
322    function notifyGePollMidAdChartTableGap(shell) {
323        if (!shell || shell.id !== 'raceGePollAdBeforeTableMobile') return;
324        if (typeof ET.scheduleGePollMidAdChartTableGapSync === 'function') {
325            try { ET.scheduleGePollMidAdChartTableGapSync(); } catch (eGap) { /* ignore */ }
326        } else if (typeof ET.syncStateSenGovPollMobileChartTableLayout === 'function') {
327            try { ET.syncStateSenGovPollMobileChartTableLayout(); } catch (eSync) { /* ignore */ }
328        }
329    }
330
331    function paintMockFilledShell(shell) {
332        if (!shell || !isMockableMobileAdShell(shell)) return;
333        shell.setAttribute('data-pollsmax-ad-mock', '1');
334        shell.setAttribute('data-ad-state', 'filled');
335        try {
336            shell.classList.remove('is-collapsed');
337            shell.classList.remove('is-empty');
338        } catch (eMockCls) { /* ignore */ }
339        shell.removeAttribute('data-pollsmax-ad-empty');
340        shell.setAttribute('data-pollsmax-ad-ready', '1');
341        shell.style.visibility = '';
342        shell.style.position = '';
343        shell.style.left = '';
344        shell.style.top = '';
345        try { shell.style.removeProperty('display'); } catch (eDisp) { /* ignore */ }
346        clearShellLayoutInline(shell);
347        notifyGePollMidAdChartTableGap(shell);
348
349        var nodes = shell.querySelectorAll('ins.adsbygoogle');
350        var i;
351        for (i = 0; i < nodes.length; i++) {
352            nodes[i].setAttribute('data-ad-status', 'filled');
353            try {
354                nodes[i].style.setProperty('display', 'none', 'important');
355                nodes[i].style.setProperty('height', '0', 'important');
356                nodes[i].style.setProperty('min-height', '0', 'important');
357            } catch (eIns) { /* ignore */ }
358        }
359        var cells = shell.querySelectorAll('.pollsmax-ad-cell');
360        for (i = 0; i < cells.length; i++) {
361            cells[i].removeAttribute('data-pollsmax-ad-empty');
362            cells[i].setAttribute('data-ad-state', 'filled');
363        }
364
365        var host = shell.querySelector('.pollsmax-ad-single__inner') ||
366            shell.querySelector('.pollsmax-ad-row__grid') ||
367            shell;
368        var box = shell.querySelector('.pollsmax-ad-mock-box');
369        if (!box) {
370            box = document.createElement('div');
371            box.className = 'pollsmax-ad-mock-box' +
372                (shell.classList.contains('pollsmax-site-multiplex')
373                    ? ' pollsmax-ad-mock-box--multiplex'
374                    : '');
375            box.setAttribute('aria-hidden', 'true');
376            host.appendChild(box);
377        }
378    }
379
380    function applyMockMobileAds() {
381        if (!wantMockMobileAds() || !isMobile()) return;
382        var shells = document.querySelectorAll('.pollsmax-ad-shell, aside.pollsmax-site-multiplex');
383        var i;
384        for (i = 0; i < shells.length; i++) {
385            paintMockFilledShell(shells[i]);
386        }
387    }
388
389    function markShellEmpty(shell) {
390        if (!shell) return;
391        if (isMockAdShell(shell)) return;
392        if (isMobile() && isMobileContentMidShell(shell)) {
393            var midIns = shell.querySelector && shell.querySelector('ins.adsbygoogle');
394            var midSt = midIns && (midIns.getAttribute('data-ad-status') || '');
395            if (midSt !== 'unfilled' && midSt !== 'unfill-optimized') {
396                setShellLoading(shell);
397                return;
398            }
399        }
400        if (isStickyPcAd(shell)) {
401            shell.removeAttribute('data-pollsmax-ad-empty');
402            if (!shell.getAttribute('data-ad-state') ||
403                shell.getAttribute('data-ad-state') === 'unfilled') {
404                shell.setAttribute('data-ad-state', 'live');
405            }
406            clearShellLayoutInline(shell);
407            try { shell.style.removeProperty('display'); } catch (eShow) { /* ignore */ }
408            return;
409        }
410        shell.setAttribute('data-ad-state', 'unfilled');
411        shell.setAttribute('data-pollsmax-ad-empty', '1');
412        shell.removeAttribute('data-pollsmax-ad-ready');
413        try {
414            shell.classList.add('is-collapsed');
415            shell.classList.add('is-empty');
416        } catch (eCls) { /* ignore */ }
417        shell.style.marginLeft = '';
418        shell.style.width = '';
419        shell.style.maxWidth = '';
420        shell.style.visibility = '';
421        shell.style.position = '';
422        shell.style.left = '';
423        shell.style.top = '';
424        clearShellLayoutInline(shell);
425        try {
426            shell.style.setProperty('display', 'none', 'important');
427        } catch (eHide) { /* ignore */ }
428    }
429
430    function syncRowAfterCellCollapse(shell) {
431        if (!shell || !shell.classList.contains('pollsmax-ad-row')) return;
432        var alive = shell.querySelectorAll('.pollsmax-ad-cell:not([data-pollsmax-ad-empty="1"])');
433        if (!alive.length) {
434            markShellEmpty(shell);
435            return;
436        }
437        shell.removeAttribute('data-pollsmax-ad-empty');
438        shell.setAttribute('data-ad-state', 'filled');
439        shell.setAttribute('data-pollsmax-ad-ready', '1');
440        var grid = shell.querySelector('.pollsmax-ad-row__grid');
441        if (grid) {
442            grid.style.gridTemplateColumns = alive.length === 1 ? 'minmax(0, 1fr)' : 'minmax(0, 1fr) minmax(0, 1fr)';
443        }
444        if (shell.classList.contains('pollsmax-ad-row--page-top')) {
445            requestAnimationFrame(function () {
446                try { syncHeaderWidthAds(); } catch (eR) { /* ignore */ }
447            });
448        }
449    }
450
451    /** Collapse one ins (and its cell); only nuke the whole row if every cell is empty. */
452    function markInsEmpty(ins) {
453        if (!ins) return;
454        var shell = shellOf(ins);
455        if (isStickyPcAd(shell)) return;
456        var cell = ins.closest ? ins.closest('.pollsmax-ad-cell') : null;
457        if (cell) {
458            cell.setAttribute('data-ad-state', 'unfilled');
459            cell.setAttribute('data-pollsmax-ad-empty', '1');
460        }
461        if (!shell) return;
462        if (shell.classList.contains('pollsmax-ad-row') && cell) {
463            syncRowAfterCellCollapse(shell);
464            return;
465        }
466        markShellEmpty(shell);
467    }
468
469    function isEmptyAdStatus(status) {
470        return status === 'unfilled' || status === 'unfill-optimized';
471    }
472
473    function insLooksFilled(ins) {
474        if (!ins) return false;
475        if (ins.getAttribute('data-ad-status') === 'filled') return true;
476        if (isEmptyAdStatus(ins.getAttribute('data-ad-status'))) return false;
477        try {
478            var iframe = ins.querySelector('iframe');
479            if (!iframe) return false;
480            var r = iframe.getBoundingClientRect();
481            return !!(r && r.height >= 20 && r.width >= 20);
482        } catch (eF) {
483            return false;
484        }
485    }
486
487    function applyInsStatus(ins) {
488        var shell = shellOf(ins);
489        if (!shell || !ins) return;
490        var status = ins.getAttribute('data-ad-status');
491        /*
492         * Only expand when Google explicitly says filled.
493         * Blank iframes / placeholders look "filled" by size and were causing
494         * huge mobile top gaps with no creative.
495         */
496        if (status === 'filled') {
497            var cell = ins.closest ? ins.closest('.pollsmax-ad-cell') : null;
498            if (cell) {
499                cell.removeAttribute('data-pollsmax-ad-empty');
500                cell.setAttribute('data-ad-state', 'filled');
501            }
502            shell.setAttribute('data-ad-state', 'filled');
503            shell.removeAttribute('data-pollsmax-ad-empty');
504            shell.setAttribute('data-pollsmax-ad-ready', '1');
505            shell.style.visibility = '';
506            shell.style.position = '';
507            shell.style.left = '';
508            shell.style.top = '';
509            try { shell.style.removeProperty('display'); } catch (eDisp) { /* ignore */ }
510            clearShellLayoutInline(shell);
511            if (isMobile() && (isMobileContentMidShell(shell) ||
512                    (shell.classList && shell.classList.contains('pollsmax-site-multiplex')))) {
513                pinMobileAdToStandardWidth(shell);
514                setTimeout(function () { pinMobileAdToStandardWidth(shell); }, 200);
515                setTimeout(function () { pinMobileAdToStandardWidth(shell); }, 800);
516            }
517            notifyGePollMidAdChartTableGap(shell);
518            if (shell.classList && (
519                shell.classList.contains('pollsmax-ad--header-width') ||
520                shell.classList.contains('pollsmax-ad-row--page-top') ||
521                shell.classList.contains('pollsmax-ad-single--page-top')
522            )) {
523                requestAnimationFrame(function () {
524                    try { syncHeaderWidthAds(); } catch (eS) { /* ignore */ }
525                });
526            }
527            return;
528        }
529        var ag = ins.getAttribute('data-adsbygoogle-status');
530        /* Explicit Google no-fill — collapse (unless sticky PC top/multiplex). */
531        if (isEmptyAdStatus(status)) {
532            if (isStickyPcAd(shell)) {
533                shell.setAttribute('data-ad-state', 'live');
534                shell.removeAttribute('data-pollsmax-ad-empty');
535                clearShellLayoutInline(shell);
536                return;
537            }
538            markInsEmpty(ins);
539            return;
540        }
541        /*
542         * `done` without a status is ambiguous — defer to the empty timer so late
543         * fills (Display, multiplex) are not wiped immediately.
544         * iframe-size hints alone must not expand the shell.
545         */
546        if (ag === 'done' && status !== 'filled') {
547            if (isStickyPcAd(shell)) {
548                shell.setAttribute('data-ad-state', 'live');
549                return;
550            }
551            scheduleEmptyTimeout(ins);
552        }
553    }
554
555    function emptyHardMsFor(ins, shell) {
556        shell = shell || shellOf(ins);
557        if (shell && shell.classList && shell.classList.contains('pollsmax-site-multiplex')) {
558            return EMPTY_HARD_MS_MULTIPLEX;
559        }
560        if (shell && (isPageTopShell(shell) || isMobileContentMidShell(shell))) {
561            return EMPTY_HARD_MS_PAGE_TOP;
562        }
563        return EMPTY_HARD_MS;
564    }
565
566    function scheduleEmptyTimeout(ins) {
567        if (!ins || ins.getAttribute('data-pollsmax-empty-timer') === '1') return;
568        var shellEarly = shellOf(ins);
569        if (isStickyPcAd(shellEarly)) return;
570        ins.setAttribute('data-pollsmax-empty-timer', '1');
571        var started = Date.now();
572        var shell = shellEarly;
573        var hardMs = emptyHardMsFor(ins, shell);
574
575        function check(hard) {
576            try {
577                if (ins.getAttribute('data-ad-status') === 'filled') {
578                    applyInsStatus(ins);
579                    return true;
580                }
581                var status = ins.getAttribute('data-ad-status');
582                var ag = ins.getAttribute('data-adsbygoogle-status');
583                /* Explicit Google no-fill — collapse immediately. */
584                if (isEmptyAdStatus(status)) {
585                    markInsEmpty(ins);
586                    return true;
587                }
588                /*
589                 * Ambiguous `done` / no status: wait for hard timeout.
590                 * Placeholder iframes still count as "not settled".
591                 */
592                if (ag === 'done' && !hard) {
593                    return false;
594                }
595                if (hard) {
596                    /*
597                     * Page-top / multiplex / mobile mid singles: leave collapsed
598                     * (loading) on ambiguous done so a late filled status can
599                     * still expand. Only collapse when Google explicitly marks
600                     * empty. Mid singles were dying at 6s while still filling.
601                     */
602                    var softShell = shellOf(ins);
603                    if (
604                        softShell &&
605                        (isPageTopShell(softShell) ||
606                            isMobileContentMidShell(softShell) ||
607                            softShell.classList.contains('pollsmax-site-multiplex'))
608                    ) {
609                        if (
610                            softShell.getAttribute('data-ad-state') === 'probing' ||
611                            softShell.getAttribute('data-ad-state') === 'waiting'
612                        ) {
613                            setShellLoading(softShell);
614                        }
615                        return true;
616                    }
617                    markInsEmpty(ins);
618                    return true;
619                }
620            } catch (eT) { /* ignore */ }
621            return false;
622        }
623
624        setTimeout(function () {
625            if (check(false)) return;
626            var left = Math.max(50, hardMs - (Date.now() - started));
627            setTimeout(function () { check(true); }, left);
628        }, EMPTY_SOFT_MS);
629
630        var polls = 0;
631        var maxPolls = Math.ceil(hardMs / 150) + 2;
632        var pollId = setInterval(function () {
633            polls++;
634            if (check(false) || polls >= maxPolls || Date.now() - started >= hardMs) {
635                clearInterval(pollId);
636            }
637        }, 150);
638    }
639
640    function watchInsStatus(ins) {
641        if (!ins || ins.getAttribute('data-pollsmax-status-watch') === '1') return;
642        if (typeof MutationObserver !== 'function') return;
643        ins.setAttribute('data-pollsmax-status-watch', '1');
644        try {
645            var obs = new MutationObserver(function () {
646                applyInsStatus(ins);
647                var status = ins.getAttribute('data-ad-status');
648                /* Keep watching through ambiguous `done` — status often lands after. */
649                if (status === 'filled' || isEmptyAdStatus(status)) {
650                    try { obs.disconnect(); } catch (eD) { /* ignore */ }
651                }
652            });
653            obs.observe(ins, {
654                attributes: true,
655                attributeFilter: ['data-ad-status', 'data-adsbygoogle-status']
656            });
657        } catch (eObs) { /* ignore */ }
658    }
659
660    /** Replace a dead/stale ins so we can push a fresh unit. */
661    function refreshInsForRetry(ins) {
662        if (!ins || !ins.parentNode) return null;
663        var next = ins.cloneNode(false);
664        next.removeAttribute('data-adsbygoogle-status');
665        next.removeAttribute('data-ad-status');
666        next.removeAttribute('data-pollsmax-empty-timer');
667        next.removeAttribute('data-pollsmax-status-watch');
668        next.removeAttribute(PUSHED);
669        delete next.dataset.pollsmaxInitialized;
670        next.innerHTML = '';
671        ins.parentNode.replaceChild(next, ins);
672        return next;
673    }
674
675    function initializeAdElement(ins) {
676        if (!(ins instanceof HTMLElement)) return false;
677        if (!ins.classList.contains('adsbygoogle')) return false;
678        /* Multiplex is owned exclusively by initializeAdSenseUnit / observe path. */
679        var muxShell = shellOf(ins);
680        if (muxShell && muxShell.classList && muxShell.classList.contains('pollsmax-site-multiplex')) {
681            return false;
682        }
683        if (ins.dataset && ins.dataset.pollsmaxPushed === '1') return false;
684        if (ins.dataset.pollsmaxInitialized === 'true') return false;
685        if (ins.getAttribute('data-ad-status') === 'filled') return false;
686        if (isEmptyAdStatus(ins.getAttribute('data-ad-status'))) return false;
687
688        /*
689         * If Google marked the tag `done` before we pushed (common when the
690         * shell was height:0), refresh the node and push once instead of
691         * permanently collapsing page-top / multiplex / mobile mid singles.
692         */
693        if (ins.getAttribute('data-adsbygoogle-status') === 'done') {
694            if (ins.getAttribute('data-pollsmax-retry') === '1') {
695                if (!isStickyPcAd(shellOf(ins))) markInsEmpty(ins);
696                return false;
697            }
698            var shellDone = shellOf(ins);
699            if (
700                shellDone &&
701                (isPageTopShell(shellDone) ||
702                    isMobileContentMidShell(shellDone) ||
703                    shellDone.classList.contains('pollsmax-site-multiplex'))
704            ) {
705                ins = refreshInsForRetry(ins);
706                if (!ins) return false;
707                ins.setAttribute('data-pollsmax-retry', '1');
708            } else {
709                markInsEmpty(ins);
710                return false;
711            }
712        }
713
714        var pushShell = shellOf(ins);
715        if (isMobile() && pushShell) pinMobileAdToStandardWidth(pushShell);
716        var width = usableWidth(ins);
717        if (width < 50) {
718            /* Page-top / multiplex / mobile mids: use document width as a last resort. */
719            var shellW = shellOf(ins);
720            if (
721                shellW &&
722                (isPageTopShell(shellW) ||
723                    isMobileContentMidShell(shellW) ||
724                    shellW.classList.contains('pollsmax-site-multiplex'))
725            ) {
726                width = Math.max(
727                    width,
728                    measureMobilePanelWidth(),
729                    shellW.clientWidth || 0
730                );
731            }
732        }
733        if (width < 50) return false;
734
735        ins.dataset.pollsmaxInitialized = 'true';
736        ins.setAttribute(PUSHED, '1');
737        var shell = shellOf(ins);
738        var sticky = isStickyPcAd(shell);
739        if (shell) {
740            if (sticky) {
741                /* PC top/multiplex: stay in-flow at full size — never collapse. */
742                shell.setAttribute('data-ad-state', 'live');
743                shell.removeAttribute('data-pollsmax-ad-empty');
744                clearShellLayoutInline(shell);
745                try { shell.style.removeProperty('display'); } catch (eLive) { /* ignore */ }
746            } else {
747                setShellProbing(shell);
748            }
749        }
750
751        try {
752            global.adsbygoogle = global.adsbygoogle || [];
753            void 0;
754            watchInsStatus(ins);
755            if (!sticky) scheduleEmptyTimeout(ins);
756            if (!sticky && shell && shell.getAttribute('data-ad-state') === 'probing') {
757                setShellLoading(shell);
758            }
759            if (sticky && shell && shell.classList.contains('pollsmax-ad-row--page-top')) {
760                requestAnimationFrame(function () {
761                    try { syncHeaderWidthAds(); } catch (eW) { /* ignore */ }
762                });
763            }
764            return true;
765        } catch (error) {
766            delete ins.dataset.pollsmaxInitialized;
767            try { ins.removeAttribute(PUSHED); } catch (eR) { /* ignore */ }
768            if (!sticky) markInsEmpty(ins);
769            if (typeof console !== 'undefined' && console.error) {
770                console.error('Pollsmax AdSense initialization failed', error);
771            }
772            return false;
773        }
774    }
775
776    function initializeShell(shell) {
777        if (!shell) return false;
778        if (shell.getAttribute('data-pollsmax-ad-empty') === '1' && !isStickyPcAd(shell)) return false;
779        var nodes = shell.querySelectorAll('ins.adsbygoogle');
780        var ok = false;
781        var i;
782        for (i = 0; i < nodes.length; i++) {
783            if (initializeAdElement(nodes[i])) ok = true;
784        }
785        return ok;
786    }
787
788    function tryInitializeShell(shell, attemptsLeft) {
789        if (!shell) return;
790        if (initializeShell(shell)) return;
791        if (attemptsLeft <= 0) {
792            /* Multiplex / sticky PC top: keep retrying via later ensure, never nuke. */
793            if (isStickyPcAd(shell)) return;
794            markShellEmpty(shell);
795            return;
796        }
797        requestAnimationFrame(function () {
798            tryInitializeShell(shell, attemptsLeft - 1);
799        });
800    }
801
802    function getLazyObserver() {
803        if (lazyObserver) return lazyObserver;
804        if (typeof IntersectionObserver !== 'function') {
805            lazyObserver = {
806                observe: function (el) { tryInitializeShell(el, 16); },
807                unobserve: function () {},
808                disconnect: function () {}
809            };
810            return lazyObserver;
811        }
812        lazyObserver = new IntersectionObserver(function (entries) {
813            var i;
814            for (i = 0; i < entries.length; i++) {
815                var entry = entries[i];
816                if (!entry.isIntersecting) continue;
817                var shell = entry.target;
818                try { lazyObserver.unobserve(shell); } catch (eU) { /* ignore */ }
819                tryInitializeShell(shell, 16);
820            }
821        }, {
822            root: null,
823            rootMargin: IO_ROOT_MARGIN,
824            threshold: 0.01
825        });
826        return lazyObserver;
827    }
828
829    function schedulePriorityInit(shell) {
830        if (!shell) return;
831        /*
832         * PC mid duals (page-top chrome, not under-header): wait for Multiplex
833         * to claim its push first, otherwise AdSense fills mids and Multiplex
834         * stays empty above the footer.
835         */
836        if (
837            !isMobile() &&
838            isPageTopShell(shell) &&
839            !isTrueHeaderPageTop(shell) &&
840            multiplexPushPending()
841        ) {
842            var deferTries = shell._pmMuxDeferTries || 0;
843            if (deferTries < 40) {
844                shell._pmMuxDeferTries = deferTries + 1;
845                setTimeout(function () {
846                    schedulePriorityInit(shell);
847                }, 120);
848                return;
849            }
850        }
851        var attempts = 0;
852        function tick() {
853            attempts += 1;
854            if (initializeShell(shell)) return;
855            if (attempts < 12) {
856                requestAnimationFrame(tick);
857                return;
858            }
859            /* Layout never gained width — fall back to IO. */
860            shell.setAttribute('data-pollsmax-observed', '1');
861            getLazyObserver().observe(shell);
862        }
863        requestAnimationFrame(function () {
864            requestAnimationFrame(tick);
865        });
866    }
867
868    function observeShell(shell, opts) {
869        opts = opts || {};
870        if (!shell || !shell.querySelector) return;
871        if (!shell.querySelector('ins.adsbygoogle')) return;
872        /* Multiplex: never lazy-IO / priority path — direct snippet push only. */
873        if (shell.classList && shell.classList.contains('pollsmax-site-multiplex')) {
874            shell.setAttribute('data-pollsmax-observed', '1');
875            return;
876        }
877        if (shell.getAttribute('data-pollsmax-observed') === '1') return;
878        if (shell.querySelector('ins.adsbygoogle[data-pollsmax-initialized="true"]')) {
879            shell.setAttribute('data-pollsmax-observed', '1');
880            return;
881        }
882        shell.classList.add('pollsmax-ad-shell');
883        if (!shell.getAttribute('data-ad-state')) shell.setAttribute('data-ad-state', 'waiting');
884
885        if (opts.priority) {
886            shell.setAttribute('data-pollsmax-observed', '1');
887            shell.classList.add('pollsmax-home-ad--priority');
888            schedulePriorityInit(shell);
889            return;
890        }
891
892        /* Above-fold page-top on home: defer observe so maps/LCP settle first. */
893        if (opts.deferIdle) {
894            shell.setAttribute('data-pollsmax-observed', '1');
895            var start = function () { getLazyObserver().observe(shell); };
896            if (typeof global.requestIdleCallback === 'function') {
897                global.requestIdleCallback(start, { timeout: 2200 });
898            } else {
899                setTimeout(start, 1);
900            }
901            return;
902        }
903
904        shell.setAttribute('data-pollsmax-observed', '1');
905        getLazyObserver().observe(shell);
906    }
907
908    function isPriorityShell(shell) {
909        if (!shell) return false;
910        if (shell.id === 'home-after-opening-maps') return true;
911        if (shell.getAttribute('data-ad-place') === 'home-after-opening-maps') return true;
912        /* Mobile mid singles: priority push — lazy IO starves 0-height collapsed shells. */
913        if (isMobile() && isMobileContentMidShell(shell)) return true;
914        return false;
915    }
916
917    function isPageTopShell(shell) {
918        return !!(shell && shell.classList && (
919            shell.classList.contains('pollsmax-ad-row--page-top') ||
920            shell.classList.contains('pollsmax-ad-single--page-top')
921        ));
922    }
923
924    function registerAllShells(root) {
925        var scope = root && root.querySelectorAll ? root : document;
926        var shells = scope.querySelectorAll(
927            '.pollsmax-ad-banner, .pollsmax-ad-row, .pollsmax-ad-single, aside.pollsmax-site-multiplex, #home-after-opening-maps'
928        );
929        var i;
930        for (i = 0; i < shells.length; i++) {
931            var shell = shells[i];
932            if (isPriorityShell(shell)) {
933                observeShell(shell, { priority: true });
934                continue;
935            }
936            if (isPageTopShell(shell)) {
937                /* Page-top is always priority — idle defer was missing fills. */
938                observeShell(shell, { priority: true });
939                continue;
940            }
941            if (shell.classList.contains('pollsmax-site-multiplex')) {
942                observeShell(shell, { priority: true });
943                continue;
944            }
945            observeShell(shell, {});
946        }
947    }
948
949    /* Back-compat: callers that used to push every ins now only register lazy shells. */
950    function initAds(root) {
951        registerAllShells(root);
952    }
953
954    function makeIns(format, opts) {
955        var inner = document.createElement('div');
956        inner.className = 'pollsmax-ad-single__inner pm-ad-slot';
957        return inner;
958    }
959
960    /**
961     * Ads nested in a chart/content column — keep parent width.
962     * Hub-tall units sit under hub charts beside the races list; never
963     * break out to logo→Donate or they overlap the sidebar.
964     */
965    function isChartColumnExtra(extraClass) {
966        var s = String(extraClass || '');
967        return (
968            s.indexOf('in-col') !== -1 ||
969            s.indexOf('forecast-trend') !== -1 ||
970            s.indexOf('hub-tall') !== -1
971        );
972    }
973
974    /**
975     * True under-header page-top dual/single (not mid duals that reuse page-top chrome).
976     * These keep legacy PC placement/spacing — shared PC banner spacing must not touch them.
977     */
978    function isTrueHeaderPageTop(shell, extraClass) {
979        if (!shell || !shell.classList) return false;
980        var extra = String(extraClass || shell.className || '');
981        var looksPageTop =
982            extra.indexOf('page-top') !== -1 ||
983            shell.classList.contains('pollsmax-ad-row--page-top') ||
984            shell.classList.contains('pollsmax-ad-single--page-top');
985        if (!looksPageTop) return false;
986        if (
987            shell.classList.contains('pollsmax-ad-row--home-mid') ||
988            shell.classList.contains('pollsmax-ad-row--race-mid') ||
989            shell.classList.contains('pollsmax-ad-row--forecast-mid') ||
990            shell.classList.contains('pollsmax-ad-row--latest-polls-infeed') ||
991            shell.classList.contains('pollsmax-ad-row--upcoming-elections-infeed') ||
992            extra.indexOf('home-mid') !== -1 ||
993            extra.indexOf('race-mid') !== -1 ||
994            extra.indexOf('forecast-mid') !== -1 ||
995            extra.indexOf('latest-polls-infeed') !== -1 ||
996            extra.indexOf('upcoming-elections-infeed') !== -1
997        ) {
998            return false;
999        }
1000        return true;
1001    }
1002
1003    function clearAdAdjacentContentMarks(shell) {
1004        if (!shell || !shell.parentNode) return;
1005        var prev = shell.previousElementSibling;
1006        var next = shell.nextElementSibling;
1007        if (prev && prev.classList) {
1008            prev.classList.remove('pm-content-before-ad');
1009            prev.classList.remove('pm-content-after-ad');
1010        }
1011        if (next && next.classList) {
1012            next.classList.remove('pm-content-before-ad');
1013            next.classList.remove('pm-content-after-ad');
1014        }
1015    }
1016
1017    /**
1018     * Shared spacing boundary classes (see pm-ad-spacing.css).
1019     * Mobile mids → pm-mobile-ad-boundary; PC mid banners → pm-pc-banner-ad-boundary.
1020     * True page-top under header is never a PC banner boundary.
1021     */
1022    function decorateAdSpacingBoundary(aside, opts) {
1023        if (!aside || !aside.classList) return;
1024        opts = opts || {};
1025        var extra = String(opts.extraClass || aside.className || '');
1026        var mobileMid =
1027            opts.mobileOnly === true ||
1028            extra.indexOf('poll-mid-mobile') !== -1 ||
1029            aside.classList.contains('pollsmax-ad-single--poll-mid-mobile');
1030        var raceOrForecastMid =
1031            extra.indexOf('race-mid') !== -1 ||
1032            extra.indexOf('forecast-mid') !== -1 ||
1033            aside.classList.contains('pollsmax-ad-row--race-mid') ||
1034            aside.classList.contains('pollsmax-ad-row--forecast-mid');
1035        var headerPageTop = isTrueHeaderPageTop(aside, extra);
1036        var isBanner =
1037            aside.classList.contains('pollsmax-ad-banner') ||
1038            raceOrForecastMid ||
1039            aside.classList.contains('pollsmax-ad-row--home-mid') ||
1040            aside.classList.contains('pollsmax-ad-row--latest-polls-infeed') ||
1041            aside.classList.contains('pollsmax-ad-row--upcoming-elections-infeed');
1042
1043        if (mobileMid) {
1044            aside.classList.add('pm-mobile-ad-boundary');
1045        }
1046        /* Forecast PC keeps legacy spacing — do not attach PC boundary class. */
1047        var onForecastPc = false;
1048        try {
1049            onForecastPc = !!(
1050                document.body &&
1051                document.body.classList.contains('race-view-forecast') &&
1052                !isMobile()
1053            );
1054        } catch (eFc) {
1055            onForecastPc = false;
1056        }
1057        if (isBanner && !headerPageTop && !onForecastPc) {
1058            aside.classList.add('pm-pc-banner-ad-boundary');
1059        }
1060        if (headerPageTop) {
1061            aside.classList.remove('pm-pc-banner-ad-boundary');
1062            clearAdAdjacentContentMarks(aside);
1063        }
1064
1065        var slot = null;
1066        var kid = aside.firstElementChild;
1067        while (kid) {
1068            if (
1069                kid.classList &&
1070                (kid.classList.contains('pollsmax-ad-single__inner') ||
1071                    kid.classList.contains('pollsmax-ad-row__grid') ||
1072                    kid.classList.contains('pm-ad-slot'))
1073            ) {
1074                slot = kid;
1075                break;
1076            }
1077            kid = kid.nextElementSibling;
1078        }
1079        if (!slot) {
1080            slot =
1081                aside.querySelector('.pollsmax-ad-single__inner') ||
1082                aside.querySelector('.pollsmax-ad-row__grid');
1083        }
1084        if (slot && slot.classList) {
1085            slot.classList.add('pm-ad-slot');
1086        }
1087    }
1088
1089    function markAdAdjacentContent(shell) {
1090        if (!shell || !shell.parentNode) return;
1091        /* PC/mobile shared spacing must not rewrite gaps around under-header page-top ads. */
1092        if (isTrueHeaderPageTop(shell)) {
1093            clearAdAdjacentContentMarks(shell);
1094            return;
1095        }
1096        var prev = shell.previousElementSibling;
1097        var next = shell.nextElementSibling;
1098        while (prev && prev.nodeType === 1) {
1099            if (prev.classList && prev.classList.contains('pollsmax-ad-shell')) {
1100                prev = prev.previousElementSibling;
1101                continue;
1102            }
1103            if (prev.offsetParent === null && prev.getAttribute &&
1104                (prev.getAttribute('hidden') !== null ||
1105                    (prev.style && prev.style.display === 'none') ||
1106                    (prev.classList && prev.classList.contains('home-key-races-grid')))) {
1107                /* Skip obviously hidden nodes; keep walking for Key Races wrapper. */
1108                if (prev.id === 'homeKeyRacesSection') {
1109                    prev = prev.previousElementSibling;
1110                    continue;
1111                }
1112            }
1113            if (prev.classList) prev.classList.add('pm-content-before-ad');
1114            break;
1115        }
1116        while (next && next.nodeType === 1) {
1117            if (next.classList && next.classList.contains('pollsmax-ad-shell')) {
1118                next = next.nextElementSibling;
1119                continue;
1120            }
1121            if (next.id === 'homeKeyRacesSection') {
1122                next = next.nextElementSibling;
1123                continue;
1124            }
1125            if (next.classList) next.classList.add('pm-content-after-ad');
1126            break;
1127        }
1128    }
1129
1130    function makeBanner(id, opts) {
1131        opts = opts || {};
1132        var aside = document.createElement('aside');
1133        var extra = opts.extraClass || '';
1134        aside.id = id;
1135        aside.className = 'pollsmax-ad-banner pollsmax-ad-shell' +
1136            (extra ? ' ' + extra : '') +
1137            (!isChartColumnExtra(extra) ? ' pollsmax-ad--header-width' : '');
1138        aside.setAttribute('aria-hidden', 'true');
1139        aside.setAttribute('data-ad-state', 'waiting');
1140        var inner = document.createElement('div');
1141        inner.className = 'pollsmax-ad-single__inner pm-ad-slot';
1142        inner.appendChild(makeIns(opts.format || 'auto'));
1143        aside.appendChild(inner);
1144        decorateAdSpacingBoundary(aside, opts);
1145        return aside;
1146    }
1147
1148    function makeRow(id, opts) {
1149        opts = opts || {};
1150        var aside = document.createElement('aside');
1151        var extra = opts.extraClass || '';
1152        var shortTop = extra.indexOf('page-top-short') !== -1;
1153        var isPageTop = extra.indexOf('page-top') !== -1;
1154        aside.id = id;
1155        aside.className = 'pollsmax-ad-row pollsmax-ad-shell' +
1156            (extra ? ' ' + extra : '') +
1157            /* Page-top already syncs; other 2-up rows use header channel width. */
1158            (!isPageTop ? ' pollsmax-ad--header-width' : '');
1159        aside.setAttribute('aria-hidden', 'true');
1160        aside.setAttribute('data-ad-state', 'waiting');
1161        var grid = document.createElement('div');
1162        grid.className = 'pollsmax-ad-row__grid pm-ad-slot';
1163        var i;
1164        for (i = 0; i < 2; i++) {
1165            var cell = document.createElement('div');
1166            cell.className = 'pollsmax-ad-cell' + (shortTop ? ' pollsmax-ad-cell--short' : '');
1167            cell.appendChild(makeIns(
1168                opts.format || 'rectangle',
1169                /*
1170                 * Dual PC rows: fixed 250px rectangle boxes. FWR/auto was still
1171                 * serving short natives into a tall empty (white) shell.
1172                 */
1173                {
1174                    fullWidthResponsive: false,
1175                    slotH: opts.slotH != null ? opts.slotH : 250
1176                }
1177            ));
1178            grid.appendChild(cell);
1179        }
1180        aside.appendChild(grid);
1181        decorateAdSpacingBoundary(aside, opts);
1182        return aside;
1183    }
1184
1185    function makeSingle(id, opts) {
1186        opts = opts || {};
1187        var aside = document.createElement('aside');
1188        aside.id = id;
1189        aside.className = 'pollsmax-ad-single pollsmax-ad-shell' +
1190            (opts.extraClass ? ' ' + opts.extraClass : '');
1191        aside.setAttribute('aria-hidden', 'true');
1192        aside.setAttribute('data-ad-state', 'waiting');
1193        var inner = document.createElement('div');
1194        inner.className = 'pollsmax-ad-single__inner pm-ad-slot';
1195        var midMob = isMobile() && String(opts.extraClass || '').indexOf('poll-mid-mobile') !== -1;
1196        inner.appendChild(makeIns(
1197            opts.format || 'auto',
1198            {
1199                fullWidthResponsive: midMob ? false : opts.fullWidthResponsive,
1200                slotH: opts.slotH
1201            }
1202        ));
1203        aside.appendChild(inner);
1204        decorateAdSpacingBoundary(aside, opts);
1205        return aside;
1206    }
1207
1208    function placeOnce(id, build, insert) {
1209        var existing = document.getElementById(id);
1210        if (existing) {
1211            decorateAdSpacingBoundary(existing, {});
1212            if (typeof insert === 'function') insert(existing);
1213            markAdAdjacentContent(existing);
1214            return existing;
1215        }
1216        var el = build();
1217        if (!el) return null;
1218        if (typeof insert === 'function') insert(el);
1219        else return null;
1220        decorateAdSpacingBoundary(el, {});
1221        markAdAdjacentContent(el);
1222        return el;
1223    }
1224
1225    function insertAfter(anchor, el) {
1226        if (!anchor || !anchor.parentNode || !el) return;
1227        if (el.parentNode === anchor.parentNode && el.previousElementSibling === anchor) return;
1228        anchor.parentNode.insertBefore(el, anchor.nextSibling);
1229    }
1230
1231    /**
1232     * Mobile panel pages: mid-ads must be siblings BETWEEN panels, never nested
1233     * inside .senate-hub-mob-panel / home card panes (that forces in-panel width
1234     * and wrecks the equal gap rhythm).
1235     */
1236    /**
1237     * Match painted panel OUTER width. Full-width-responsive ads ignore the
1238     * parent and size to the viewport — turn that off and lock px width.
1239     */
1240    function pinMobileAdToStandardWidth(el) {
1241        if (!isMobile() || !el) return;
1242        if (
1243            el.getAttribute('data-ad-state') === 'unfilled' ||
1244            el.getAttribute('data-pollsmax-ad-empty') === '1'
1245        ) {
1246            return;
1247        }
1248        if (
1249            el.classList &&
1250            (el.classList.contains('pollsmax-ad-row--page-top') ||
1251                el.classList.contains('pollsmax-ad-single--page-top'))
1252        ) {
1253            return;
1254        }
1255        var isMux = !!(el.classList && el.classList.contains('pollsmax-site-multiplex'));
1256        if (!isMux && !isMobileContentMidShell(el)) return;
1257        var w = measureMobilePanelWidth();
1258        if (!(w > 50)) return;
1259        try {
1260            document.documentElement.style.setProperty('--et-mobile-standard-width', w + 'px');
1261            el.style.setProperty('box-sizing', 'border-box', 'important');
1262            el.style.setProperty('display', 'block', 'important');
1263            el.style.setProperty('width', w + 'px', 'important');
1264            el.style.setProperty('min-width', w + 'px', 'important');
1265            el.style.setProperty('max-width', w + 'px', 'important');
1266            el.style.setProperty('flex', '0 0 ' + w + 'px', 'important');
1267            el.style.setProperty('align-self', 'center', 'important');
1268            el.style.setProperty('margin-left', 'auto', 'important');
1269            el.style.setProperty('margin-right', 'auto', 'important');
1270            el.style.setProperty('padding-left', '0', 'important');
1271            el.style.setProperty('padding-right', '0', 'important');
1272            var ins = el.querySelector('ins.adsbygoogle');
1273            if (ins && !isMux) {
1274                ins.setAttribute('data-full-width-responsive', 'false');
1275                ins.style.setProperty('display', 'block', 'important');
1276                ins.style.setProperty('width', '100%', 'important');
1277                ins.style.setProperty('max-width', '100%', 'important');
1278                ins.style.setProperty('min-width', '0', 'important');
1279                ins.style.setProperty('margin-left', '0', 'important');
1280                ins.style.setProperty('margin-right', '0', 'important');
1281                ins.style.setProperty('left', 'auto', 'important');
1282                ins.style.setProperty('right', 'auto', 'important');
1283                ins.style.setProperty('position', 'relative', 'important');
1284            }
1285            var inner = el.querySelector('.pollsmax-ad-single__inner');
1286            if (inner) {
1287                inner.style.setProperty('width', '100%', 'important');
1288                inner.style.setProperty('max-width', '100%', 'important');
1289                inner.style.setProperty('box-sizing', 'border-box', 'important');
1290            }
1291            var iframe = el.querySelector('iframe');
1292            if (iframe && !isMux) {
1293                iframe.removeAttribute('width');
1294                iframe.style.setProperty('width', '100%', 'important');
1295                iframe.style.setProperty('max-width', '100%', 'important');
1296                iframe.style.setProperty('min-width', '0', 'important');
1297            }
1298        } catch (ePin) { /* ignore */ }
1299    }
1300
1301    function syncMidAdPanelOuterWidths() {
1302        if (!isMobile()) return;
1303        var nodes = document.querySelectorAll(
1304            '.pollsmax-ad-single--poll-mid-mobile, aside.pollsmax-site-multiplex, .pm-mobile-ad-boundary.pollsmax-ad-shell'
1305        );
1306        var ids = [
1307            'homeAdBetweenGovAndSearch',
1308            'homeAdBetweenGbAndDemPrimary',
1309            'senateHubAdAfterMapMobile',
1310            'senateHubAdBeforeRacesMobile',
1311            'govHubAdAfterMapMobile',
1312            'govHubAdBeforeRacesMobile',
1313            'houseHubAdAfterMapMobile',
1314            'houseHubAdBeforeRacesMobile',
1315            'raceGePollAdBeforeTableMobile',
1316            'raceGePollAdAfterTableMobile',
1317            'racePrimaryPollAdBeforeTableMobile',
1318            'racePrimaryPollAdAfterTableMobile',
1319            'raceForecastAdTopOfSimMobile',
1320            'raceForecastAdAfterOddsMobile',
1321            'raceForecastAdUnderPollsMobile',
1322            'approvalAdBeforeTableMobile',
1323            'primary2028AdAfterChartMobile',
1324            'latestPollsAdUnderListMobile',
1325            'upcomingElectionsAdUnderListMobile'
1326        ];
1327        var seen = [];
1328        var i;
1329        for (i = 0; i < nodes.length; i++) {
1330            if (nodes[i] && seen.indexOf(nodes[i]) < 0) seen.push(nodes[i]);
1331        }
1332        for (i = 0; i < ids.length; i++) {
1333            var el = document.getElementById(ids[i]);
1334            if (el && seen.indexOf(el) < 0) seen.push(el);
1335        }
1336        for (i = 0; i < seen.length; i++) pinMobileAdToStandardWidth(seen[i]);
1337    }
1338
1339    function scheduleMidAdPanelOuterWidthSync() {
1340        function run() {
1341            ejectMidAdsFromPanels();
1342            syncMidAdPanelOuterWidths();
1343        }
1344        run();
1345        requestAnimationFrame(run);
1346        setTimeout(run, 120);
1347        setTimeout(run, 500);
1348        setTimeout(run, 1600);
1349    }
1350
1351    function ejectMidAdsFromPanels() {
1352        if (!isMobile()) return;
1353        var i;
1354        var govSearch = document.getElementById('homeAdBetweenGovAndSearch');
1355        if (govSearch) {
1356            var upcoming = document.getElementById('homeUpcomingElectionsSection');
1357            var search = document.getElementById('searchAllPollsSection');
1358            var trappedHome =
1359                (govSearch.closest &&
1360                    (govSearch.closest('#homeUpcomingElectionsSection') ||
1361                        govSearch.closest('#searchAllPollsSection') ||
1362                        govSearch.closest('.sleek-map-pane'))) ||
1363                null;
1364            if (trappedHome || (upcoming && govSearch.previousElementSibling !== upcoming)) {
1365                if (upcoming && upcoming.parentNode) insertAfter(upcoming, govSearch);
1366                else if (search) insertBefore(search, govSearch);
1367            }
1368        }
1369        var gbDem = document.getElementById('homeAdBetweenGbAndDemPrimary');
1370        if (gbDem && gbDem.closest) {
1371            var trappedGb = gbDem.closest('.sleek-map-pane, .senate-hub-mob-panel');
1372            if (trappedGb) {
1373                var approvalRow = document.querySelector('.sleek-maps--home-approval');
1374                if (approvalRow && approvalRow.parentNode) insertAfter(approvalRow, gbDem);
1375                else if (trappedGb.parentNode) insertAfter(trappedGb, gbDem);
1376            }
1377        }
1378        var hubIds = [
1379            'senateHubAdAfterMapMobile',
1380            'senateHubAdBeforeRacesMobile',
1381            'govHubAdAfterMapMobile',
1382            'govHubAdBeforeRacesMobile',
1383            'houseHubAdAfterMapMobile',
1384            'houseHubAdBeforeRacesMobile',
1385            'raceForecastAdTopOfSimMobile',
1386            'raceForecastAdAfterOddsMobile',
1387            'raceForecastAdUnderPollsMobile'
1388        ];
1389        for (i = 0; i < hubIds.length; i++) {
1390            var el = document.getElementById(hubIds[i]);
1391            if (!el || !el.parentNode || !el.closest) continue;
1392            var panel = el.closest('.senate-hub-mob-panel, .sleek-map-pane');
1393            if (!panel || panel === el) continue;
1394            if (panel.parentNode) insertAfter(panel, el);
1395        }
1396    }
1397
1398    function insertBefore(anchor, el) {
1399        if (!anchor || !anchor.parentNode || !el) return;
1400        if (el.parentNode === anchor.parentNode && el.nextElementSibling === anchor) return;
1401        anchor.parentNode.insertBefore(el, anchor);
1402    }
1403
1404    function appendChild(host, el) {
1405        if (!host || !el) return;
1406        if (el.parentNode !== host) host.appendChild(el);
1407    }
1408
1409    /* ---------- Global: top of page ---------- */
1410
1411    /**
1412     * Insert top ads as the first content in <main> — under #mainHeader and
1413     * #site-primary-nav (those sit outside main), above hero / page-head sections.
1414     */
1415    function insertPageTopAd(el) {
1416        if (!el) return;
1417        var main = document.querySelector('main');
1418        if (!main) return;
1419        var parent = main.parentNode;
1420        if (!parent) return;
1421
1422        /*
1423         * National approval (Trump / generic ballot): keep page-top outside <main>,
1424         * after the select bar when present — same slot on both pages so the gap
1425         * under the ad to the H1 matches.
1426         */
1427        if (document.body && document.body.classList.contains('national-approval-polls-page')) {
1428            var bar = document.getElementById('stateRaceSelectBar');
1429            if (bar && bar.parentNode === parent) {
1430                if (el.previousElementSibling !== bar || el.nextElementSibling !== main) {
1431                    parent.insertBefore(el, main);
1432                }
1433                return;
1434            }
1435        }
1436
1437        /* Prefer mounting right after the sitelinks strip when it is a sibling before main. */
1438        var strip = document.getElementById('site-primary-nav') ||
1439            document.querySelector('nav.sleek-sitelinks-strip, .sleek-sitelinks-strip');
1440        if (strip && strip.parentNode === parent) {
1441            if (el.previousElementSibling !== strip || el.nextElementSibling !== main) {
1442                parent.insertBefore(el, main);
1443            }
1444            return;
1445        }
1446
1447        /* Fallback: first child of main, above heroes. */
1448        if (main.firstElementChild !== el) {
1449            main.insertBefore(el, main.firstChild);
1450        }
1451    }
1452
1453    /**
1454     * Content channel for page-top + wide ads: POLLSMAX logo left → Donate right.
1455     * Maps are not used — homepage map columns can diverge from the header chrome.
1456     */
1457    /** Page-top ad host chrome — always site dark canvas (never light/white). */
1458    var PAGE_TOP_AD_BG = '#050608';
1459
1460    /** Live site canvas color — whatever html/body is actually painted with. */
1461    function resolvePageBackgroundColor() {
1462        try {
1463            var root = document.documentElement;
1464            /* Prefer computed body paint (covers page-local overrides like #0b0e13). */
1465            var bodyBg = global.getComputedStyle(document.body).backgroundColor;
1466            if (
1467                bodyBg &&
1468                bodyBg !== 'transparent' &&
1469                bodyBg !== 'rgba(0, 0, 0, 0)'
1470            ) {
1471                return bodyBg;
1472            }
1473            var htmlBg = global.getComputedStyle(root).backgroundColor;
1474            if (
1475                htmlBg &&
1476                htmlBg !== 'transparent' &&
1477                htmlBg !== 'rgba(0, 0, 0, 0)'
1478            ) {
1479                return htmlBg;
1480            }
1481            var token = (
1482                global.getComputedStyle(root).getPropertyValue('--color-page-bg') ||
1483                ''
1484            ).trim();
1485            if (token) return token;
1486        } catch (eBg) { /* ignore */ }
1487        return PAGE_TOP_AD_BG;
1488    }
1489
1490    function paintPageBg(el, pageBg) {
1491        if (!el || !pageBg) return;
1492        try {
1493            el.style.setProperty('background', pageBg, 'important');
1494            el.style.setProperty('background-color', pageBg, 'important');
1495        } catch (ePaint) { /* ignore */ }
1496    }
1497
1498    function measureHeaderAdChannel() {
1499        var vw = document.documentElement.clientWidth || window.innerWidth || 0;
1500        var left = null;
1501        var right = null;
1502
1503        /* Pin Donate first so its live right edge is the ad-channel right. */
1504        if (global.ET && typeof global.ET.syncHeaderDonateToContentRight === 'function') {
1505            try {
1506                global.ET.syncHeaderDonateToContentRight({ skipAds: true, force: true });
1507            } catch (eSyncD) { /* ignore */ }
1508        }
1509
1510        var hdr = document.getElementById('mainHeader');
1511        var donate = null;
1512        if (hdr) {
1513            /* Prefer real logo link — never img[alt*=Pollsmax] (matches tiny social icon). */
1514            var logoImg = hdr.querySelector('img.sleek-home-header-logo');
1515            var logo =
1516                (logoImg && (logoImg.closest('a') || logoImg)) ||
1517                hdr.querySelector('a.sleek-home-header-logo') ||
1518                hdr.querySelector('a[href="/"].flex.items-center') ||
1519                hdr.querySelector('a[href="/"]');
1520            donate =
1521                document.getElementById('pollsmax-header-donate') ||
1522                hdr.querySelector('a.pollsmax-header-donate') ||
1523                hdr.querySelector('a[href*="/donate"]');
1524            if (logo) {
1525                left = logo.getBoundingClientRect().left;
1526            }
1527            /*
1528             * Right edge MUST be Donate’s painted right — never come up short
1529             * of the button (content-target alone was leaving ads too far left
1530             * on some pages).
1531             */
1532            if (donate) {
1533                right = donate.getBoundingClientRect().right;
1534            }
1535            if (!(right > 80) && global.ET && global.ET._headerAlignTarget > 80) {
1536                right = global.ET._headerAlignTarget;
1537            }
1538            if (!(right > 80) &&
1539                global.ET &&
1540                typeof global.ET.measureContentAlignRightEdge === 'function') {
1541                try {
1542                    var contentR = global.ET.measureContentAlignRightEdge();
1543                    if (contentR > 80) right = contentR;
1544                } catch (eCR) { /* ignore */ }
1545            }
1546            if (!(right > left)) {
1547                var wrap = hdr.querySelector('.w-full.px-4') || hdr.firstElementChild;
1548                if (wrap) {
1549                    var wr = wrap.getBoundingClientRect();
1550                    var cs = global.getComputedStyle(wrap);
1551                    if (!(left > 0)) left = wr.left + (parseFloat(cs.paddingLeft) || 0);
1552                    right = wr.right - (parseFloat(cs.paddingRight) || 0);
1553                }
1554            }
1555        }
1556        if (!(right > left) && vw > 0) {
1557            var pad = vw >= 1280 ? 56 : vw >= 1024 ? 40 : vw >= 640 ? 24 : 16;
1558            if (!(left > 0)) left = pad;
1559            right = vw - pad;
1560        }
1561        if (!(vw > 0) || !(right > left)) return null;
1562        var padL = Math.max(0, Math.round(left));
1563        /* Width from logo → Donate; round so painted right never sits left of Donate. */
1564        var width = Math.max(0, Math.round(right - padL));
1565        var padR = Math.max(0, Math.round(vw - padL - width));
1566        /* If rounding still undershoots Donate, steal 1px from padR. */
1567        if (donate) {
1568            var wantR = donate.getBoundingClientRect().right;
1569            var paintedR = padL + width;
1570            if (wantR - paintedR > 0.25) {
1571                width = Math.max(0, Math.ceil(wantR - padL));
1572                padR = Math.max(0, Math.round(vw - padL - width));
1573            }
1574        }
1575        return {
1576            vw: vw,
1577            padL: padL,
1578            padR: padR,
1579            width: width
1580        };
1581    }
1582
1583    /**
1584     * Non–chart-column ads (poll tables, EH, etc.): match page-top width
1585     * even when nested inside narrower padded sections.
1586     */
1587    function syncHeaderWidthAds() {
1588        try {
1589            if (isMobile()) return;
1590            if (syncHeaderWidthAds._lock) return;
1591            syncHeaderWidthAds._lock = true;
1592            var ch = measureHeaderAdChannel();
1593            if (!ch) {
1594                syncHeaderWidthAds._lock = false;
1595                return;
1596            }
1597            var root = document.documentElement;
1598            root.style.setProperty('--pollsmax-top-ad-pad-left', ch.padL + 'px');
1599            root.style.setProperty('--pollsmax-top-ad-pad-right', ch.padR + 'px');
1600            root.style.setProperty('--pollsmax-top-ad-width', ch.width + 'px');
1601
1602            var tops = document.querySelectorAll('.pollsmax-ad-row--page-top');
1603            var ti;
1604            for (ti = 0; ti < tops.length; ti++) {
1605                var top = tops[ti];
1606                if (!top) continue;
1607                /*
1608                 * Nested mid rows (latest in-feed, race polling, forecast) sit inside
1609                 * padded content columns. Break out to logo→Donate — do not also apply
1610                 * padL (that double-insets and leaves a middle gap / short width).
1611                 */
1612                /*
1613                 * Nested breakout: latest in-feed + forecast mids inside padded
1614                 * forecast columns. Above-table race mids use --mid-bleed between
1615                 * <section>s (page-top padL path — same as home mid) so they span
1616                 * logo→Donate without double-inset or clipped breakout.
1617                 */
1618                var isNestedBreakout =
1619                    top.classList.contains('pollsmax-ad-row--latest-polls-infeed') ||
1620                    top.classList.contains('pollsmax-ad-row--forecast-mid') ||
1621                    (top.classList.contains('pollsmax-ad-row--race-mid') &&
1622                        !top.classList.contains('pollsmax-ad-row--mid-bleed'));
1623                /* Upcoming Elections in-feed: fill the date-divider column (not logo→Donate). */
1624                var isUpcomingInfeed =
1625                    top.classList.contains('pollsmax-ad-row--upcoming-elections-infeed');
1626                var spanW = ch.width;
1627                if (isNestedBreakout) {
1628                    top.style.setProperty('padding-left', '0', 'important');
1629                    top.style.setProperty('padding-right', '0', 'important');
1630                    top.style.setProperty('margin-left', '0', 'important');
1631                    top.style.setProperty('margin-right', '0', 'important');
1632                    top.style.setProperty('width', '', 'important');
1633                    top.style.setProperty('max-width', '', 'important');
1634                    void top.offsetWidth;
1635                    var infeedRect = top.getBoundingClientRect();
1636                    var infeedShift = ch.padL - Math.round(infeedRect.left);
1637                    top.style.setProperty('margin-left', infeedShift + 'px', 'important');
1638                    top.style.setProperty('width', ch.width + 'px', 'important');
1639                    top.style.setProperty('max-width', ch.width + 'px', 'important');
1640                    spanW = ch.width;
1641                } else if (isUpcomingInfeed) {
1642                    top.style.setProperty('padding-left', '0', 'important');
1643                    top.style.setProperty('padding-right', '0', 'important');
1644                    top.style.setProperty('margin-left', '0', 'important');
1645                    top.style.setProperty('margin-right', '0', 'important');
1646                    top.style.setProperty('width', '100%', 'important');
1647                    top.style.setProperty('max-width', '100%', 'important');
1648                    void top.offsetWidth;
1649                    spanW = Math.max(0, Math.round(top.getBoundingClientRect().width));
1650                    if (!(spanW > 0)) spanW = ch.width;
1651                } else {
1652                    top.style.setProperty('padding-left', ch.padL + 'px', 'important');
1653                    top.style.setProperty('padding-right', ch.padR + 'px', 'important');
1654                    top.style.setProperty('width', '100%', 'important');
1655                    top.style.setProperty('max-width', '100%', 'important');
1656                    top.style.setProperty('margin-left', '0', 'important');
1657                    top.style.setProperty('margin-right', '0', 'important');
1658                    spanW = ch.width;
1659                }
1660                top.style.setProperty('box-sizing', 'border-box', 'important');
1661                /*
1662                 * Padded dual-ad bands: keep ≥250px creatives; pad above/below so
1663                 * page canvas fills hairline→creative (CSS alone loses to these locks).
1664                 * Home uses 0.75rem; Latest/Upcoming in-feed uses 24px mid-banner gap.
1665                 */
1666                var isHomeBand =
1667                    (document.body && document.body.classList.contains('pollsmax-home-page')) &&
1668                    (top.id === 'pollsmaxPageTopAdDesktop' ||
1669                        top.id === 'homeAdAfterUpcomingElections' ||
1670                        top.id === 'homeAdAfterSearchAllPolls' ||
1671                        top.id === 'homeAdAfterKeyRaces' ||
1672                        top.classList.contains('pollsmax-ad-row--home-mid'));
1673                var isInfeedBand =
1674                    top.classList.contains('pollsmax-ad-row--latest-polls-infeed') ||
1675                    top.classList.contains('pollsmax-ad-row--upcoming-elections-infeed');
1676                if (isHomeBand) {
1677                    top.style.setProperty('height', 'calc(250px + 1.5rem)', 'important');
1678                    top.style.setProperty('max-height', 'calc(250px + 1.5rem)', 'important');
1679                    top.style.setProperty('min-height', 'calc(250px + 1.5rem)', 'important');
1680                    top.style.setProperty('padding-top', '0.75rem', 'important');
1681                    top.style.setProperty('padding-bottom', '0.75rem', 'important');
1682                    top.style.setProperty('margin-top', '0', 'important');
1683                    top.style.setProperty('margin-bottom', '0', 'important');
1684                } else if (isInfeedBand) {
1685                    top.style.setProperty('height', 'calc(250px + 48px)', 'important');
1686                    top.style.setProperty('max-height', 'calc(250px + 48px)', 'important');
1687                    top.style.setProperty('min-height', 'calc(250px + 48px)', 'important');
1688                    top.style.setProperty('padding-top', '24px', 'important');
1689                    top.style.setProperty('padding-bottom', '24px', 'important');
1690                    top.style.setProperty('margin-top', '0', 'important');
1691                    top.style.setProperty('margin-bottom', '0', 'important');
1692                } else {
1693                    /* Exact 250px slot — matches rectangle request; no empty white below. */
1694                    top.style.setProperty('height', '250px', 'important');
1695                    top.style.setProperty('max-height', '250px', 'important');
1696                    top.style.setProperty('min-height', '250px', 'important');
1697                    top.style.setProperty('padding-top', '0', 'important');
1698                    top.style.setProperty('padding-bottom', '0', 'important');
1699                }
1700                top.style.setProperty('overflow', 'hidden', 'important');
1701                top.style.setProperty('border-top', '0', 'important');
1702                top.style.setProperty('border-bottom', '0', 'important');
1703                /* Host chrome is always dark site canvas — not light/white body paint. */
1704                var pageBg = isHomeBand ? '#0b0f13' : PAGE_TOP_AD_BG;
1705                paintPageBg(top, pageBg);
1706                var grid = top.querySelector('.pollsmax-ad-row__grid');
1707                if (grid) {
1708                    /*
1709                     * Exact logo→Donate span. Two flush halves, zero middle gap
1710                     * (no CSS 1rem gap, no space-between). Odd leftover px on
1711                     * the right cell so the row right edge lands on Donate.
1712                     */
1713                    var cellL = Math.max(0, Math.floor(spanW / 2));
1714                    var cellR = Math.max(0, spanW - cellL);
1715                    grid.style.setProperty('display', 'flex', 'important');
1716                    grid.style.setProperty('flex-direction', 'row', 'important');
1717                    grid.style.setProperty('flex-wrap', 'nowrap', 'important');
1718                    grid.style.setProperty('align-items', 'stretch', 'important');
1719                    grid.style.setProperty('justify-content', 'flex-start', 'important');
1720                    grid.style.setProperty('gap', '0', 'important');
1721                    grid.style.setProperty('column-gap', '0', 'important');
1722                    grid.style.setProperty('row-gap', '0', 'important');
1723                    grid.style.setProperty('width', spanW + 'px', 'important');
1724                    grid.style.setProperty('max-width', spanW + 'px', 'important');
1725                    grid.style.setProperty('margin-left', '0', 'important');
1726                    grid.style.setProperty('margin-right', '0', 'important');
1727                    grid.style.setProperty('padding', '0', 'important');
1728                    grid.style.gridTemplateColumns = 'none';
1729                    grid.style.setProperty('height', '250px', 'important');
1730                    grid.style.setProperty('max-height', '250px', 'important');
1731                    grid.style.setProperty('min-height', '250px', 'important');
1732                    grid.style.setProperty('overflow', 'hidden', 'important');
1733                    paintPageBg(grid, pageBg);
1734                    var cells = grid.querySelectorAll('.pollsmax-ad-cell');
1735                    var ci;
1736                    for (ci = 0; ci < cells.length; ci++) {
1737                        var cellW = ci === 0 ? cellL : cellR;
1738                        cells[ci].style.setProperty('flex', '0 0 ' + cellW + 'px', 'important');
1739                        cells[ci].style.setProperty('width', cellW + 'px', 'important');
1740                        cells[ci].style.setProperty('min-width', cellW + 'px', 'important');
1741                        cells[ci].style.setProperty('max-width', cellW + 'px', 'important');
1742                        cells[ci].style.setProperty('box-sizing', 'border-box', 'important');
1743                        cells[ci].style.setProperty('display', 'flex', 'important');
1744                        cells[ci].style.setProperty('align-items', 'stretch', 'important');
1745                        cells[ci].style.setProperty('justify-content', 'flex-start', 'important');
1746                        cells[ci].style.setProperty('margin', '0', 'important');
1747                        cells[ci].style.setProperty('padding', '0', 'important');
1748                        cells[ci].style.setProperty('height', '250px', 'important');
1749                        cells[ci].style.setProperty('max-height', '250px', 'important');
1750                        cells[ci].style.setProperty('min-height', '250px', 'important');
1751                        cells[ci].style.setProperty('overflow', 'hidden', 'important');
1752                        paintPageBg(cells[ci], pageBg);
1753                    }
1754                    var topIns = grid.querySelectorAll('ins.adsbygoogle');
1755                    for (ci = 0; ci < topIns.length; ci++) {
1756                        topIns[ci].style.setProperty('display', 'block', 'important');
1757                        topIns[ci].style.setProperty('width', '100%', 'important');
1758                        topIns[ci].style.setProperty('max-width', '100%', 'important');
1759                        topIns[ci].style.setProperty('min-width', '0', 'important');
1760                        topIns[ci].style.setProperty('box-sizing', 'border-box', 'important');
1761                        topIns[ci].style.setProperty('text-align', 'left', 'important');
1762                        topIns[ci].style.setProperty('height', '250px', 'important');
1763                        topIns[ci].style.setProperty('max-height', '250px', 'important');
1764                        topIns[ci].style.setProperty('min-height', '250px', 'important');
1765                        topIns[ci].style.setProperty('margin', '0', 'important');
1766                        topIns[ci].style.setProperty('padding', '0', 'important');
1767                        paintPageBg(topIns[ci], pageBg);
1768                    }
1769                    var iframes = grid.querySelectorAll('iframe');
1770                    for (ci = 0; ci < iframes.length; ci++) {
1771                        iframes[ci].style.setProperty('width', '100%', 'important');
1772                        iframes[ci].style.setProperty('max-width', '100%', 'important');
1773                        iframes[ci].style.setProperty('min-width', '0', 'important');
1774                        iframes[ci].style.setProperty('margin', '0', 'important');
1775                        iframes[ci].style.setProperty('padding', '0', 'important');
1776                        iframes[ci].style.setProperty('border', '0', 'important');
1777                        iframes[ci].style.setProperty('box-sizing', 'border-box', 'important');
1778                        iframes[ci].style.setProperty('height', '250px', 'important');
1779                        iframes[ci].style.setProperty('max-height', '250px', 'important');
1780                        iframes[ci].style.setProperty('min-height', '250px', 'important');
1781                        paintPageBg(iframes[ci],
1781 pageBg);
1782                        try { iframes[ci].removeAttribute('width'); } catch (eW) { /* ignore */ }
1783                    }
1784
1785                    /* Final paint check: grow the row if it still sits left of Donate. */
1786                    var donateEl =
1787                        document.getElementById('pollsmax-header-donate') ||
1788                        document.querySelector('#mainHeader a.pollsmax-header-donate');
1789                    if (donateEl) {
1790                        var want = donateEl.getBoundingClientRect().right;
1791                        var got = grid.getBoundingClientRect().right;
1792                        var shortBy = Math.round(want - got);
1793                        if (shortBy > 0) {
1794                            ch.width += shortBy;
1795                            ch.padR = Math.max(0, ch.padR - shortBy);
1796                            top.style.setProperty('padding-right', ch.padR + 'px', 'important');
1797                            cellL = Math.max(0, Math.floor(ch.width / 2));
1798                            cellR = Math.max(0, ch.width - cellL);
1799                            grid.style.setProperty('width', ch.width + 'px', 'important');
1800                            grid.style.setProperty('max-width', ch.width + 'px', 'important');
1801                            if (cells[0]) {
1802                                cells[0].style.setProperty('flex', '0 0 ' + cellL + 'px', 'important');
1803                                cells[0].style.setProperty('width', cellL + 'px', 'important');
1804                                cells[0].style.setProperty('min-width', cellL + 'px', 'important');
1805                                cells[0].style.setProperty('max-width', cellL + 'px', 'important');
1806                            }
1807                            if (cells[1]) {
1808                                cells[1].style.setProperty('flex', '0 0 ' + cellR + 'px', 'important');
1809                                cells[1].style.setProperty('width', cellR + 'px', 'important');
1810                                cells[1].style.setProperty('min-width', cellR + 'px', 'important');
1811                                cells[1].style.setProperty('max-width', cellR + 'px', 'important');
1812                            }
1813                            root.style.setProperty('--pollsmax-top-ad-pad-right', ch.padR + 'px');
1814                            root.style.setProperty('--pollsmax-top-ad-width', ch.width + 'px');
1815                        }
1816                    }
1817                }
1818            }
1819
1820            var nodes = document.querySelectorAll('.pollsmax-ad--header-width');
1821            var i;
1822            for (i = 0; i < nodes.length; i++) {
1823                var el = nodes[i];
1824                if (el.classList.contains('pollsmax-ad-row--page-top')) continue;
1825                /* Hub chart ads must stay in the right column — clear any prior breakout. */
1826                if (el.classList.contains('pollsmax-ad-banner--hub-tall')) {
1827                    el.style.marginLeft = '';
1828                    el.style.marginRight = '';
1829                    el.style.width = '';
1830                    el.style.maxWidth = '';
1831                    el.style.paddingLeft = '';
1832                    el.style.paddingRight = '';
1833                    el.classList.remove('pollsmax-ad--header-width');
1834                    continue;
1835                }
1836                /* Reset then measure natural inset so we can break out to header channel. */
1837                el.style.marginLeft = '0';
1838                el.style.marginRight = '0';
1839                el.style.width = '';
1840                el.style.maxWidth = '';
1841                el.style.paddingLeft = '0';
1842                el.style.paddingRight = '0';
1843                var rect = el.getBoundingClientRect();
1844                var shift = ch.padL - Math.round(rect.left);
1845                var nextML = shift + 'px';
1846                var nextW = ch.width + 'px';
1847                if (el.style.marginLeft !== nextML || el.style.width !== nextW) {
1848                    el.style.marginLeft = nextML;
1849                    el.style.width = nextW;
1850                    el.style.maxWidth = nextW;
1851                    el.style.boxSizing = 'border-box';
1852                }
1853            }
1854            syncHeaderWidthAds._lock = false;
1855        } catch (eSync) {
1856            syncHeaderWidthAds._lock = false;
1857        }
1858    }
1859
1860    /* PC top duals keep natural height — no short clamp. */
1861    function fitPageTopCreatives() { /* no-op */ }
1862    function watchPageTopClamp() { /* no-op */ }
1863
1864    function syncTopAdContentBounds() {
1865        syncHeaderWidthAds();
1866    }
1867
1868    /* Back-compat alias */
1869    var syncTopAdHeaderBounds = syncTopAdContentBounds;
1870
1871    function removeShell(el, force) {
1872        if (!el || !el.parentNode) return false;
1873        if (!force && el.querySelector('ins.adsbygoogle[data-pollsmax-initialized="true"]')) return false;
1874        el.parentNode.removeChild(el);
1875        return true;
1876    }
1877
1878    function removeUninitShell(el) {
1879        removeShell(el, false);
1880    }
1881
1882    function placePageTopAds() {
1883        var mobile = isMobile();
1884        var desktopId = 'pollsmaxPageTopAdDesktop';
1885        var mobileId = 'pollsmaxPageTopAdMobile';
1886
1887        /* Mobile: no top banner. Desktop: 2-up fixed 250px rectangles. */
1888        if (mobile) {
1889            removeShell(document.getElementById(desktopId), true);
1890            removeShell(document.getElementById(mobileId), true);
1891            return;
1892        }
1893        removeShell(document.getElementById(mobileId), true);
1894
1895        var staleDesk = document.getElementById(desktopId);
1896        if (staleDesk) {
1897            var deskIns = staleDesk.querySelector('ins.adsbygoogle');
1898            var deskFmt = deskIns && deskIns.getAttribute('data-ad-format');
1899            var deskFwr = deskIns && deskIns.getAttribute('data-full-width-responsive');
1900            /* Remount anything that isn't the fixed 250px rectangle 2
1900-up PC top. */
1901            if (
1902                staleDesk.classList.contains('pollsmax-ad-row--page-top-short') ||
1903                !staleDesk.classList.contains('pollsmax-ad-row--page-top-wide') ||
1904                deskFmt !== 'rectangle' ||
1905                deskFwr === 'true' ||
1906                staleDesk.getAttribute('data-pollsmax-ad-empty') === '1'
1907            ) {
1908                removeShell(staleDesk, true);
1909            }
1910        }
1911
1912        placeOnce(desktopId, function () {
1913            return makeRow(desktopId, {
1914                /* Two fixed 250px rectangles spanning logo → Donate; never collapse. */
1915                extraClass: 'pollsmax-ad-row--page-top pollsmax-ad-row--page-top-wide',
1916                format: 'rectangle',
1917                slotH: 250
1918            });
1919        }, insertPageTopAd);
1920        var desk = document.getElementById(desktopId);
1921        if (desk) {
1922            desk.setAttribute('data-ad-state', 'live');
1923            desk.removeAttribute('data-pollsmax-ad-empty');
1924        }
1925        syncTopAdContentBounds();
1926        requestAnimationFrame(syncTopAdContentBounds);
1927        setTimeout(syncTopAdContentBounds, 120);
1928        setTimeout(syncTopAdContentBounds, 500);
1929    }
1930
1931    /** Homepage Priority 1: first high-value Display after opening Senate/House maps (PC only). */
1932    function placeHomeAfterOpeningMaps(kind) {
1933        if (!kind || !kind.home) return;
1934        /* Mobile: no ad under the opening maps hero. */
1935        if (isMobile()) {
1936            removeShell(document.getElementById('home-after-opening-maps'), true);
1937            return;
1938        }
1939        var maps = document.querySelector('.sleek-maps--home-senate-house');
1940        if (!maps || !maps.parentNode) return;
1941        placeOnce('home-after-opening-maps', function () {
1942            var aside = makeSingle('home-after-opening-maps', {
1943                extraClass: 'pollsmax-home-ad--priority pollsmax-ad--header-width',
1944                format: 'rectangle',
1945                fullWidthResponsive: false,
1946                slotH: 250
1947            });
1948            aside.setAttribute('data-ad-place', 'home-after-opening-maps');
1949            aside.setAttribute('data-ad-priority', '1');
1950            return aside;
1951        }, function (el) { insertAfter(maps, el); });
1952    }
1953
1954    /** Mobile: strip side-by-side under-hero ads (vote/odds pair, 2-up rows, top banners). */
1955    function scrubMobileUnderHeroAds() {
1956        if (!isMobile()) return;
1957        var killIds = [
1958            'pollsmaxPageTopAdMobile',
1959            'pollsmaxPageTopAdDesktop',
1960            'home-after-opening-maps',
1961            'raceForecastAdUnderVote',
1962            'raceForecastAdUnderOdds',
1963            /* Legacy under-map-key unit — would stack a 2nd ad under the hub map. */
1964            'hubAdAfterMapKeyMobile'
1965            /* Never kill intended mobile mids (home/hub/poll/forecast/approval/2028/latest). */
1966        ];
1967        var keepIds = {
1968            homeAdBetweenGovAndSearch: 1,
1969            homeAdBetweenGbAndDemPrimary: 1,
1970            senateHubAdAfterMapMobile: 1,
1971            senateHubAdBeforeRacesMobile: 1,
1972            govHubAdAfterMapMobile: 1,
1973            govHubAdBeforeRacesMobile: 1,
1974            houseHubAdAfterMapMobile: 1,
1975            houseHubAdBeforeRacesMobile: 1,
1976            raceGePollAdBeforeTableMobile: 1,
1977            raceGePollAdAfterTableMobile: 1,
1978            racePrimaryPollAdBeforeTableMobile: 1,
1979            racePrimaryPollAdAfterTableMobile: 1,
1980            raceForecastAdTopOfSimMobile: 1,
1981            raceForecastAdAfterOddsMobile: 1,
1982            raceForecastAdUnderPollsMobile: 1,
1983            approvalAdBeforeTableMobile: 1,
1984            primary2028AdAfterChartMobile: 1,
1985            latestPollsAdUnderListMobile: 1,
1986            upcomingElectionsAdUnderListMobile: 1
1987        };
1988        var i;
1989        for (i = 0; i < killIds.length; i++) {
1990            removeShell(document.getElementById(killIds[i]), true);
1991        }
1992        var rows = document.querySelectorAll('.pollsmax-ad-row');
1993        for (i = 0; i < rows.length; i++) {
1994            var row = rows[i];
1995            if (row && row.id && keepIds[row.id]) continue;
1996            removeShell(row, true);
1997        }
1998        /* Clear stale inline display:none on allowlisted mids (from older CSS path). */
1999        Object.keys(keepIds).forEach(function (id) {
2000            var el = document.getElementById(id);
2001            if (!el || !el.style) return;
2002            try {
2003                if (el.style.getPropertyValue('display') === 'none') {
2004                    el.style.removeProperty('display');
2005                }
2006            } catch (eKeep) { /* ignore */ }
2007        });
2008    }
2009
2010    /* ---------- Global: multiplex above footer ---------- */
2011
2012    function findFooter() {
2013        return document.querySelector('footer[data-et-site-footer="1"]') ||
2014            document.querySelector('footer');
2015    }
2016
2017    function hasOverflowClipAncestor(el) {
2018        var node = el && el.parentElement;
2019        while (node && node !== document.documentElement) {
2020            try {
2021                var cs = global.getComputedStyle(node);
2022                if (cs) {
2023                    var oy = cs.overflowY;
2024                    /*
2025                     * Only vertical clip blocks tall multiplex units.
2026                     * body { overflow-x: hidden } is common and must not count.
2027                     */
2028                    if (oy === 'hidden' || oy === 'clip') return true;
2029                }
2030            } catch (e) { /* ignore */ }
2031            node = node.parentElement;
2032        }
2033        return false;
2034    }
2035
2036    /** Actual mobile scrollport — body/html oy:auto, not the layout viewport. */
2037    function scrollRootForAdsIO() {
2038        try {
2039            var body = document.body;
2040            if (body) {
2041                var bs = global.getComputedStyle(body);
2042                if (bs && (bs.overflowY === 'auto' || bs.overflowY === 'scroll')) return body;
2043            }
2044            var html = document.documentElement;
2045            if (html) {
2046                var hs = global.getComputedStyle(html);
2047                if (hs && (hs.overflowY === 'auto' || hs.overflowY === 'scroll')) return html;
2048            }
2049        } catch (eR) { /* ignore */ }
2050        return null;
2051    }
2052
2053    function isMobileRaceMuxPage() {
2054        if (!isMobile()) return false;
2055        var k = pageKind();
2056        return !!(k.forecast || k.gePoll || k.primaryPoll);
2057    }
2058
2059    function waitForAdSense(timeout) {
2060        timeout = typeof timeout === 'number' ? timeout : 10000;
2061        return new Promise(function (resolve, reject) {
2062            var start = Date.now();
2063            function scriptReady() {
2064                var loader =
2065                    document.getElementById('pollsmax-adsense-loader') ||
2066                    document.querySelector(
2067                        'script[src*="pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"]'
2068                    );
2069                if (!loader) return false;
2070                if (loader.getAttribute('data-pollsmax-loaded') === '1') return true;
2071                try {
2072                    if (loader.dataset && loader.dataset.loaded === '1') return true;
2073                } catch (eD) { /* ignore */ }
2074                /* After Google boots, adsbygoogle is no longer a plain Array. */
2075                try {
2076                    if (
2077                        global.adsbygoogle &&
2078                        typeof global.adsbygoogle.push === 'function' &&
2079                        !Array.isArray(global.adsbygoogle)
2080                    ) {
2081                        return true;
2082                    }
2083                } catch (eAg) { /* ignore */ }
2084                return false;
2085            }
2086            function queueReady() {
2087                try {
2088                    return (
2089                        Array.isArray(global.adsbygoogle) ||
2090                        (global.adsbygoogle && typeof global.adsbygoogle.push === 'function')
2091                    );
2092                } catch (eQ) {
2093                    return false;
2094                }
2095            }
2096            function check() {
2097                if (scriptReady() || (queueReady() && Date.now() - start > 400 && document.readyState === 'complete')) {
2098                    resolve();
2099                    return;
2100                }
2101                if (Date.now() - start >= timeout) {
2102                    if (queueReady()) {
2103                        resolve();
2104                        return;
2105                    }
2106                    reject(new Error('AdSense script did not become ready'));
2107                    return;
2108                }
2109                requestAnimationFrame(check);
2110            }
2111            check();
2112        });
2113    }
2114
2115    function isAdVisibleAndSized(ad) {
2116        if (!ad || !ad.isConnected) return false;
2117        var element = ad;
2118        while (element && element !== document.documentElement) {
2119            try {
2120                var style = global.getComputedStyle(element);
2121                var rect = element.getBoundingClientRect();
2122                if (
2123                    style.display === 'none' ||
2124                    style.visibility === 'hidden' ||
2125                    rect.width <= 0
2126                ) {
2127                    return false;
2128                }
2129                /* Empty <ins> can report height 0 before fill — only reject ancestors. */
2130                if (element !== ad && rect.height <= 0) {
2131                    return false;
2132                }
2133            } catch (eVis) {
2134                return false;
2135            }
2136            element = element.parentElement;
2137        }
2138        var parentRect =
2139            ad.parentElement && ad.parentElement.getBoundingClientRect
2140                ? ad.parentElement.getBoundingClientRect()
2141                : null;
2142        return !!(parentRect && parentRect.width > 0);
2143    }
2144
2145    function logMultiplexDiagnostics(ad, note) {
2146        try {
2147            var loader =
2148                document.getElementById('pollsmax-adsense-loader') ||
2149                document.querySelector(
2150                    'script[src*="pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"]'
2151                );
2152            var cs = ad ? global.getComputedStyle(ad) : null;
2153            var parent = ad && ad.parentElement;
2154            var parentW =
2155                parent && parent.getBoundingClientRect
2156                    ? parent.getBoundingClientRect().width
2157                    : 0;
2158            if (typeof console !== 'undefined' && console.info) {
2159                console.info('[Pollsmax Ads] Multiplex diagnostics' + (note ? ' — ' + note : ''), {
2160                    adsenseScript: !!(loader),
2161                    adsenseLoaded: !!(loader && loader.getAttribute('data-pollsmax-loaded') === '1'),
2162                    adExists: !!ad,
2163                    connected: !!(ad && ad.isConnected),
2164                    parentWidth: Math.round(parentW || 0),
2165                    display: cs ? cs.display : null,
2166                    visibility: cs ? cs.visibility : null,
2167                    adStatus: ad ? ad.getAttribute('data-ad-status') : null,
2168                    adsbygoogleStatus: ad ? ad.getAttribute('data-adsbygoogle-status') : null,
2169                    pollsmaxPushed: ad ? ad.dataset.pollsmaxPushed : null,
2170                    slot: ad ? ad.getAttribute('data-ad-slot') : null,
2171                    format: ad ? ad.getAttribute('data-ad-format') : null
2172                });
2173            }
2174        } catch (eLog) { /* ignore */ }
2175    }
2176
2177    function watchAdStatus(ad) {
2178        if (!ad || ad.dataset.pollsmaxStatusWatch === '1') return;
2179        ad.dataset.pollsmaxStatusWatch = '1';
2180        try {
2181            if (typeof MutationObserver !== 'function') return;
2182            var observer = new MutationObserver(function () {
2183                var status = ad.dataset.adStatus || ad.getAttribute('data-ad-status');
2184                var googleStatus = ad.getAttribute('data-adsbygoogle-status');
2185                if (status || googleStatus === 'done') {
2186                    if (typeof console !== 'undefined' && console.info) {
2187                        console.info('[Pollsmax Ads] Ad processed', {
2188                            slot: ad.dataset.adSlot || ad.getAttribute('data-ad-slot'),
2189                            status: status,
2190                            googleStatus: googleStatus
2191                        });
2192                    }
2193                    if (status === 'filled' || (googleStatus === 'done' && status !== 'unfilled')) {
2194                        settleMultiplexHeight(ad);
2195                        setTimeout(function () { settleMultiplexHeight(ad); }, 300);
2196                        setTimeout(function () { settleMultiplexHeight(ad); }, 1000);
2197                    }
2198                    try { observer.disconnect(); } catch (eD) { /* ignore */ }
2199                    if (status === 'unfilled' || status === 'unfill-optimized') {
2200                        collapseUnfilledAd(ad);
2201                    }
2202                }
2203            });
2204            observer.observe(ad, {
2205                attributes: true,
2206                attributeFilter: ['data-ad-status', 'data-adsbygoogle-status', 'style']
2207            });
2208            try {
2209                var childObs = new MutationObserver(function () {
2210                    if (ad.querySelector('iframe') && ad.getAttribute('data-ad-status') === 'filled') {
2211                        settleMultiplexHeight(ad);
2212                        try { childObs.disconnect(); } catch (eC) { /* ignore */ }
2213                    }
2214                });
2215                childObs.observe(ad, { childList: true, subtree: true });
2216            } catch (eChild) { /* ignore */ }
2217        } catch (eWatch) { /* ignore */ }
2218    }
2219
2220    function collapseUnfilledAd(ad) {
2221        if (!ad) return;
2222        function handleStatus() {
2223            var status = ad.dataset.adStatus || ad.getAttribute('data-ad-status');
2224            if (status !== 'unfilled' && status !== 'unfill-optimized') return;
2225            var shell =
2226                ad.closest('#pollsmaxSiteMultiplex, aside.pollsmax-site-multiplex, .pollsmax-ad-shell') ||
2227                ad.closest('.pollsmax-ad-shell');
2228            if (!shell) return;
2229            /* Multiplex (and PC page-top) must stay reserved for late fills. */
2230            if (isStickyPcAd(shell)) {
2231                shell.setAttribute('data-ad-state', 'live');
2232                shell.removeAttribute('data-pollsmax-ad-empty');
2233                clearShellLayoutInline(shell);
2234                try { shell.style.removeProperty('display'); } catch (eShow) { /* ignore */ }
2235                revealMultiplexShell(shell);
2236                return;
2237            }
2238            shell.setAttribute('data-pollsmax-ad-empty', '1');
2239            shell.setAttribute('data-ad-state', 'unfilled');
2240            try {
2241                shell.style.setProperty('display', 'none', 'important');
2242            } catch (eHide) { /* ignore */ }
2243        }
2244        try {
2245            if (typeof MutationObserver === 'function') {
2246                var observer = new MutationObserver(handleStatus);
2247                observer.observe(ad, {
2248                    attributes: true,
2249                    attributeFilter: ['data-ad-status']
2250                });
2251            }
2252        } catch (eObs) { /* ignore */ }
2253        handleStatus();
2254    }
2255
2256    /**
2257     * Shared push helper — one source of truth for manual unit push.
2258     * Marks data-pollsmax-pushed only after push({}) returns without throwing.
2259     */
2260    function initializeAdSenseUnit(ad) {
2261        if (!ad || !ad.isConnected) return Promise.resolve(false);
2262
2263        if (
2264            ad.dataset.adStatus ||
2265            ad.getAttribute('data-ad-status') ||
2266            ad.getAttribute('data-adsbygoogle-status') === 'done' ||
2267            ad.dataset.pollsmaxPushed === '1'
2268        ) {
2269            return Promise.resolve(false);
2270        }
2271
2272        if (!isAdVisibleAndSized(ad)) {
2273            logMultiplexDiagnostics(ad, 'not visible/sized');
2274            return Promise.resolve(false);
2275        }
2276
2277        return waitForAdSense()
2278            .then(function () {
2279                if (
2280                    ad.dataset.adStatus ||
2281                    ad.getAttribute('data-ad-status') ||
2282                    ad.getAttribute('data-adsbygoogle-status') === 'done' ||
2283                    ad.dataset.pollsmaxPushed === '1'
2284                ) {
2285                    return false;
2286                }
2287                if (!ad.isConnected || !isAdVisibleAndSized(ad)) {
2288                    logMultiplexDiagnostics(ad, 'lost visibility before push');
2289                    return false;
2290                }
2291                var parentWidth =
2292                    (ad.parentElement &&
2293                        ad.parentElement.getBoundingClientRect &&
2294                        ad.parentElement.getBoundingClientRect().width) ||
2295                    0;
2296                if (parentWidth <= 0) {
2297                    logMultiplexDiagnostics(ad, 'parent width 0');
2298                    return false;
2299                }
2300
2301                /*
2302                 * AdSense assigns each push({}) to the next unprocessed ins in
2303                 * document order. Claim earlier manual units first so multiplex
2304                 * is not skipped / stolen.
2305                 */
2306                claimEarlierManualAds(ad);
2307
2308                if (
2309                    ad.getAttribute('data-adsbygoogle-status') === 'done' ||
2310                    ad.dataset.pollsmaxPushed === '1'
2311                ) {
2312                    return false;
2313                }
2314
2315                /* Refresh Multiplex grid/title attrs for this viewport right before push. */
2316                if (ad.getAttribute('data-ad-slot') === SLOT_MULTIPLEX) {
2317                    applyMultiplexLayoutAttrs(ad);
2318                }
2319
2320                watchAdStatus(ad);
2321                global.adsbygoogle = global.adsbygoogle || [];
2322                void 0;
2323                ad.dataset.pollsmaxPushed = '1';
2324                ad.setAttribute(PUSHED, '1');
2325
2326                if (typeof console !== 'undefined' && console.info) {
2327                    console.info('[Pollsmax Ads] Ad pushed', {
2328                        slot: ad.dataset.adSlot || ad.getAttribute('data-ad-slot'),
2329                        format: ad.dataset.adFormat || ad.getAttribute('data-ad-format'),
2330                        uiType: ad.getAttribute('data-matched-content-ui-type'),
2331                        rows: ad.getAttribute('data-matched-content-rows-num'),
2332                        cols: ad.getAttribute('data-matched-content-columns-num'),
2333                        parentWidth: Math.round(parentWidth)
2334                    });
2335                }
2336                logMultiplexDiagnostics(ad, 'push ok');
2337                return true;
2338            })
2339            .catch(function (error) {
2340                try { delete ad.dataset.pollsmaxPushed; } catch (eDel) { /* ignore */ }
2341                try { ad.removeAttribute(PUSHED); } catch (eR) { /* ignore */ }
2342                if (typeof console !== 'undefined' && console.error) {
2343                    console.error('[Pollsmax Ads] Ad push failed', {
2344                        slot: ad ? (ad.dataset.adSlot || ad.getAttribute('data-ad-slot')) : null,
2345                        error: error
2346                    });
2347                }
2348                logMultiplexDiagnostics(ad, 'push failed');
2349                return false;
2350            });
2351    }
2352
2353    function isManualAdIns(ins) {
2354        if (!ins || !ins.getAttribute) return false;
2355        if (ins.closest && ins.closest('.google-auto-placed')) return false;
2356        if (ins.closest && ins.closest('aside.pollsmax-site-multiplex')) return false;
2357        var slot = ins.getAttribute('data-ad-slot');
2358        return !!(slot && String(slot).length > 3);
2359    }
2360
2361    function claimEarlierManualAds(targetIns) {
2362        if (!targetIns || !document.querySelectorAll) return;
2363        var all = document.querySelectorAll('ins.adsbygoogle');
2364        var i;
2365        for (i = 0; i < all.length; i++) {
2366            var el = all[i];
2367            if (el === targetIns) break;
2368            if (!isManualAdIns(el)) continue;
2369            if (
2370                el.dataset.pollsmaxPushed === '1' ||
2371                el.getAttribute('data-adsbygoogle-status') === 'done' ||
2372                el.getAttribute('data-ad-status')
2373            ) {
2374                continue;
2375            }
2376            if (!el.isConnected || !isAdVisibleAndSized(el)) continue;
2377            try {
2378                global.adsbygoogle = global.adsbygoogle || [];
2379                void 0;
2380                el.dataset.pollsmaxPushed = '1';
2381                el.setAttribute(PUSHED, '1');
2382                el.dataset.pollsmaxInitialized = 'true';
2383            } catch (eClaim) { /* ignore */ }
2384        }
2385    }
2386
2387    /**
2388     * Claim only the under-header page-top pair so multiplex can push next.
2389     * Mid duals reuse page-top chrome — claiming them here starves Multiplex
2390     * (AdSense fills document-order display units first).
2391     */
2392    function claimPageTopAdsNow() {
2393        var tops = document.querySelectorAll(
2394            '#pollsmaxPageTopAdDesktop.pollsmax-ad-row--page-top, .pollsmax-ad-row--page-top, .pollsmax-ad-single--page-top'
2395        );
2396        var ti;
2397        for (ti = 0; ti < tops.length; ti++) {
2398            var top = tops[ti];
2399            if (!top || !isTrueHeaderPageTop(top)) continue;
2400            var nodes = top.querySelectorAll('ins.adsbygoogle');
2401            var i;
2402            for (i = 0; i < nodes.length; i++) {
2403                var el = nodes[i];
2404                if (
2405                    el.dataset.pollsmaxPushed === '1' ||
2406                    el.getAttribute('data-adsbygoogle-status') === 'done' ||
2407                    el.getAttribute('data-ad-status')
2408                ) {
2409                    continue;
2410                }
2411                if (!isAdVisibleAndSized(el)) continue;
2412                try {
2413                    global.adsbygoogle = global.adsbygoogle || [];
2414                    void 0;
2415                    el.dataset.pollsmaxPushed = '1';
2416                    el.setAttribute(PUSHED, '1');
2417                    el.dataset.pollsmaxInitialized = 'true';
2418                } catch (eTop) { /* ignore */ }
2419            }
2420        }
2421    }
2422
2423    function multiplexInsIsSettled(ins) {
2424        if (!ins) return true;
2425        return !!(
2426            ins.dataset.pollsmaxPushed === '1' ||
2427            ins.getAttribute('data-adsbygoogle-status') === 'done' ||
2428            ins.getAttribute('data-ad-status')
2429        );
2430    }
2431
2432    /** True while footer Multiplex still needs its first push. */
2433    function multiplexPushPending() {
2434        if (isEmbed()) return false;
2435        try {
2436            if (document.body && document.body.getAttribute('data-pm-no-multiplex') === '1') {
2437                return false;
2438            }
2439        } catch (eNo) { /* ignore */ }
2440        var aside =
2441            document.getElementById('homeAdMultiplex') ||
2442            document.getElementById('pollsmaxSiteMultiplex') ||
2443            document.querySelector('aside.pollsmax-site-multiplex');
2444        /* Shell not mounted yet (footer late) — still treat as pending. */
2445        if (!aside) return true;
2446        var ins =
2447            aside.querySelector('ins.adsbygoogle[data-ad-slot="' + SLOT_MULTIPLEX + '"]') ||
2448            aside.querySelector('ins.adsbygoogle');
2449        if (!ins) return true;
2450        return !multiplexInsIsSettled(ins);
2451    }
2452
2453    function scheduleMultiplexPushRetries(ad) {
2454        if (!ad || ad.getAttribute('data-pollsmax-mux-retry') === '1') return;
2455        ad.setAttribute('data-pollsmax-mux-retry', '1');
2456        var attempts = 0;
2457        function tick() {
2458            if (
2459                !ad.isConnected ||
2460                ad.dataset.pollsmaxPushed === '1' ||
2461                ad.getAttribute('data-adsbygoogle-status') === 'done' ||
2462                ad.getAttribute('data-ad-status')
2463            ) {
2464                return;
2465            }
2466            initializeAdSenseUnit(ad).then(function () {
2467                attempts += 1;
2468                if (
2469                    ad.dataset.pollsmaxPushed === '1' ||
2470                    ad.getAttribute('data-ad-status') ||
2471                    attempts >= 28
2472                ) {
2473                    return;
2474                }
2475                setTimeout(tick, attempts < 10 ? 250 : 500);
2476            }).catch(function () {
2477                attempts += 1;
2478                if (attempts < 28) setTimeout(tick, 400);
2479            });
2480        }
2481        waitForAdSense()
2482            .then(function () {
2483                requestAnimationFrame(function () {
2484                    requestAnimationFrame(tick);
2485                });
2486            })
2487            .catch(function () { /* ignore */ });
2488    }
2489
2490    function observeAdSenseUnit(ad) {
2491        if (!ad || ad.getAttribute('data-pollsmax-mux-observed') === '1') return;
2492        ad.setAttribute('data-pollsmax-mux-observed', '1');
2493
2494        if (
2495            ad.dataset.pollsmaxPushed === '1' ||
2496            ad.getAttribute('data-adsbygoogle-status') === 'done' ||
2497            ad.getAttribute('data-ad-status')
2498        ) {
2499            collapseUnfilledAd(ad);
2500            return;
2501        }
2502
2503        /*
2504         * Always eager-push Multiplex (PC + mobile). Waiting on IO lets mid
2505         * duals claim AdSense first; mobile race pages also need this because
2506         * body is the scrollport and viewport-root IO may never fire.
2507         */
2508        if (ad.getAttribute('data-ad-slot') === SLOT_MULTIPLEX) {
2509            scheduleMultiplexPushRetries(ad);
2510        }
2511
2512        if (typeof IntersectionObserver !== 'function') return;
2513
2514        var observer = new IntersectionObserver(
2515            function (entries) {
2516                var i;
2517                for (i = 0; i < entries.length; i++) {
2518                    if (!entries[i].isIntersecting) continue;
2519                    claimPageTopAdsNow();
2520                    initializeAdSenseUnit(ad).then(function (initialized) {
2521                        if (
2522                            initialized ||
2523                            ad.dataset.adStatus ||
2524                            ad.getAttribute('data-ad-status') ||
2525                            ad.getAttribute('data-adsbygoogle-status') === 'done' ||
2526                            ad.dataset.pollsmaxPushed === '1'
2527                        ) {
2528                            try { observer.disconnect(); } catch (eDisc) { /* ignore */ }
2529                        }
2530                    });
2531                }
2532            },
2533            {
2534                root: scrollRootForAdsIO(),
2535                rootMargin: '900px 0px',
2536                threshold: 0
2537            }
2538        );
2539        observer.observe(ad);
2540    }
2541
2542    function multiplexLayoutSpec() {
2543        /*
2544         * Single-value attrs for the current viewport (comma mobile,desktop pairs
2545         * are often ignored by modern Multiplex). PC: 2×4 = 8 with titles.
2546         * https://support.google.com/adsense/answer/7533385
2547         */
2548        var mobile = isMobile();
2549        return {
2550            uiType: 'image_card_stacked',
2551            rows: mobile ? '4' : '2',
2552            cols: mobile ? '1' : '4'
2553        };
2554    }
2555
2556    function applyMultiplexLayoutAttrs(ins) {
2557        if (!ins) return;
2558        var spec = multiplexLayoutSpec();
2559        ins.setAttribute('data-matched-content-ui-type', spec.uiType);
2560        ins.setAttribute('data-matched-content-rows-num', spec.rows);
2561        ins.setAttribute('data-matched-content-columns-num', spec.cols);
2562        /*
2563         * Google sizes the Multiplex iframe from the <ins> box at push time.
2564         * A short box yields a short iframe that clips titles under the images.
2565         */
2566        /* Mobile: tall min-height so stacked Multiplex is not sized to a short iframe. */
2567        var slotH = isMobile() ? 2200 : 720;
2568        try {
2569            ins.style.display = 'block';
2570            ins.style.setProperty('min-height', slotH + 'px', 'important');
2571            ins.style.setProperty('height', 'auto', 'important');
2572            ins.style.setProperty('max-height', 'none', 'important');
2573            ins.style.setProperty('overflow', 'visible', 'important');
2574        } catch (eH) { /* ignore */ }
2575    }
2576
2577    /**
2578     * After Google fills, drop our prefill height floors.
2579     * Forcing the iframe taller than the 2×4 creatives left an empty white
2580     * "ghost" third row under the real ads.
2581     */
2582    function settleMultiplexHeight(ad) {
2583        if (!ad || ad.getAttribute('data-ad-slot') !== SLOT_MULTIPLEX) return;
2584        var iframe = ad.querySelector('iframe');
2585        var shell =
2586            ad.closest('#pollsmaxSiteMultiplex, aside.pollsmax-site-multiplex') ||
2587            null;
2588        /* Mobile stacked Multiplex is clipped if we hug Google's short iframe. */
2589        if (isMobile()) {
2590            var minH = 2200;
2591            var attrH = iframe ? parseInt(iframe.getAttribute('height'), 10) : 0;
2592            var h = Math.max(minH, attrH || 0, (iframe && iframe.offsetHeight) || 0);
2593            try {
2594                ad.style.setProperty('min-height', h + 'px', 'important');
2595                ad.style.setProperty('height', 'auto', 'important');
2596                ad.style.setProperty('max-height', 'none', 'important');
2597                ad.style.setProperty('overflow', 'visible', 'important');
2598            } catch (eInsM) { /* ignore */ }
2599            if (iframe) {
2600                try {
2601                    iframe.style.setProperty('height', h + 'px', 'important');
2602                    iframe.style.setProperty('min-height', h + 'px', 'important');
2603                    iframe.style.setProperty('max-height', 'none', 'important');
2604                    iframe.style.setProperty('overflow', 'visible', 'important');
2605                } catch (eFrameM) { /* ignore */ }
2606            }
2607            if (shell) {
2608                try {
2609                    shell.style.setProperty('min-height', '0', 'important');
2610                    shell.style.setProperty('height', 'auto', 'important');
2611                    shell.style.setProperty('max-height', 'none', 'important');
2612                    shell.style.setProperty('overflow', 'visible', 'important');
2613                    shell.style.setProperty('background', 'transparent', 'important');
2614                    shell.setAttribute('data-pollsmax-mux-settled', '1');
2615                } catch (eShellM) { /* ignore */ }
2616            }
2617            pinMobileAdToStandardWidth(shell || ad);
2618            return;
2619        }
2620        try {
2621            ad.style.removeProperty('height');
2622            ad.style.removeProperty('min-height');
2623            ad.style.setProperty('min-height', '0', 'important');
2624            ad.style.setProperty('height', 'auto', 'important');
2625        } catch (eIns) { /* ignore */ }
2626        if (iframe) {
2627            try {
2628                iframe.style.removeProperty('min-height');
2629                iframe.style.removeProperty('height');
2630                iframe.style.removeProperty('max-height');
2631            } catch (eFrame) { /* ignore */ }
2632        }
2633        if (shell) {
2634            try {
2635                shell.style.setProperty('min-height', '0', 'important');
2636                shell.style.setProperty('height', 'auto', 'important');
2637                shell.style.setProperty('background', 'transparent', 'important');
2638                shell.setAttribute('data-pollsmax-mux-settled', '1');
2639            } catch (eShell) { /* ignore */ }
2640        }
2641        var inner = ad.closest('.pollsmax-ad-single__inner');
2642        if (inner) {
2643            try {
2644                inner.style.setProperty('min-height', '0', 'important');
2645                inner.style.setProperty('height', 'auto', 'important');
2646            } catch (eInner) { /* ignore */ }
2647        }
2648    }
2649
2650    function buildMultiplexIns() {
2651        var inner = document.createElement('div');
2652        inner.className = 'pollsmax-ad-single__inner pm-ad-slot';
2653        return inner;
2654    }
2655
2656    function multiplexInsNeedsLayoutUpgrade(ins) {
2657        if (!ins) return true;
2658        var spec = multiplexLayoutSpec();
2659        return (
2660            ins.getAttribute('data-ad-slot') !== SLOT_MULTIPLEX ||
2661            ins.getAttribute('data-ad-format') !== 'autorelaxed' ||
2662            ins.getAttribute('data-matched-content-ui-type') !== spec.uiType ||
2663            ins.getAttribute('data-matched-content-rows-num') !== spec.rows ||
2664            ins.getAttribute('data-matched-content-columns-num') !== spec.cols
2665        );
2666    }
2667
2668    function mountFreshMultiplexIns(aside) {
2669        if (!aside) return null;
2670        /* Never wipe after push, Google processing, or an active IO/observer. */
2671        var existing = aside.querySelector('ins.adsbygoogle');
2672        if (
2673            existing &&
2674            (existing.dataset.pollsmaxPushed === '1' ||
2675                existing.getAttribute('data-adsbygoogle-status') === 'done' ||
2676                existing.getAttribute('data-ad-status') ||
2677                existing.getAttribute('data-pollsmax-mux-observed') === '1')
2678        ) {
2679            applyMultiplexLayoutAttrs(existing);
2680            return existing;
2681        }
2682        aside.innerHTML = '';
2683        aside.removeAttribute('data-pollsmax-observed');
2684        aside.removeAttribute('data-pollsmax-ad-empty');
2685        aside.removeAttribute('data-pollsmax-ad-ready');
2686        var inner = document.createElement('div');
2687        inner.className = 'pollsmax-ad-single__inner';
2688        var ins = buildMultiplexIns();
2689        inner.appendChild(ins);
2690        aside.appendChild(inner);
2691        return ins;
2692    }
2693
2694    function revealMultiplexShell(aside) {
2695        if (!aside) return;
2696        aside.classList.add('pollsmax-ad-shell');
2697        aside.classList.add('pollsmax-site-multiplex');
2698        aside.setAttribute('data-ad-priority', '4');
2699        aside.setAttribute('data-ad-state', 'live');
2700        aside.setAttribute('data-pollsmax-observed', '1');
2701        aside.removeAttribute('data-pollsmax-ad-empty');
2702        aside.removeAttribute('aria-hidden');
2703        clearShellLayoutInline(aside);
2704        try {
2705            aside.style.removeProperty('display');
2706            aside.style.setProperty('visibility', 'visible', 'important');
2707            aside.style.setProperty('opacity', '1', 'important');
2708            aside.style.setProperty(
2709                'min-height',
2710                isMobile() ? '0' : '720px',
2711                'important'
2712            );
2713            aside.style.setProperty('height', 'auto', 'important');
2714            aside.style.setProperty('max-height', 'none', 'important');
2715            aside.style.setProperty('overflow', 'visible', 'important');
2716            aside.style.setProperty('contain', 'none', 'important');
2717            aside.style.setProperty(
2718                'width',
2719                isMobile()
2720                    ? 'var(--et-mobile-standard-width, calc(100vw - 2rem))'
2721                    : '100%',
2722                'important'
2723            );
2724            if (isMobile()) {
2725                pinMobileAdToStandardWidth(aside);
2726            }
2727            aside.style.setProperty('pointer-events', 'auto', 'important');
2728            aside.style.setProperty('background', 'transparent', 'important');
2729            aside.style.removeProperty('color');
2730            aside.style.removeProperty('color-scheme');
2731        } catch (eRev) { /* ignore */ }
2732    }
2733
2734    function ensureSiteMultiplex() {
2735        if (isEmbed()) return;
2736        ensureAdsense();
2737
2738        var footer = findFooter();
2739        if (!footer || !footer.parentNode) {
2740            if (!ensureSiteMultiplex._footerRetry) {
2741                ensureSiteMultiplex._footerRetry = setTimeout(function () {
2742                    ensureSiteMultiplex._footerRetry = null;
2743                    ensureSiteMultiplex();
2744                }, 400);
2745            }
2746            return;
2747        }
2748
2749        var home = document.getElementById('homeAdMultiplex');
2750        var aside = home || document.getElementById('pollsmaxSiteMultiplex');
2751        if (!aside) {
2752            aside = document.createElement('aside');
2753            aside.id = 'pollsmaxSiteMultiplex';
2754            aside.className = 'pollsmax-site-multiplex';
2755        }
2756        if (!aside.id) aside.id = 'pollsmaxSiteMultiplex';
2757
2758        var ins = aside.querySelector('ins.adsbygoogle[data-ad-slot="' + SLOT_MULTIPLEX + '"]') ||
2759            aside.querySelector('ins.adsbygoogle');
2760        var alreadyPushed = !!(
2761            ins &&
2762            (ins.dataset.pollsmaxPushed === '1' ||
2763                ins.getAttribute('data-adsbygoogle-status') === 'done' ||
2764                ins.getAttribute('data-ad-status'))
2765        );
2766
2767        /*
2768         * Do not move / recreate the <ins> after it has been pushed or
2769         * processed by Google (requirement #6).
2770         */
2771        if (!alreadyPushed) {
2772            if (!ins || multiplexInsNeedsLayoutUpgrade(ins)) {
2773                ins = mountFreshMultiplexIns(aside);
2774            }
2775            if (
2776                (aside.nextElementSibling !== footer || hasOverflowClipAnce
2776stor(aside)) &&
2777                aside.getAttribute('data-pollsmax-mux-placed') !== '1'
2778            ) {
2779                footer.parentNode.insertBefore(aside, footer);
2780                aside.setAttribute('data-pollsmax-mux-placed', '1');
2781            } else if (!aside.parentNode) {
2782                footer.parentNode.insertBefore(aside, footer);
2783                aside.setAttribute('data-pollsmax-mux-placed', '1');
2784            }
2785        }
2786
2787        revealMultiplexShell(aside);
2788        ins = aside.querySelector('ins.adsbygoogle[data-ad-slot="' + SLOT_MULTIPLEX + '"]');
2789        if (!ins) {
2790            logMultiplexDiagnostics(null, 'missing ins after mount');
2791            return;
2792        }
2793
2794        logMultiplexDiagnostics(ins, 'ensure');
2795        collapseUnfilledAd(ins);
2796
2797        /* IO near-viewport (+ eager retries — always for Multiplex). */
2798        if (ins.getAttribute('data-pollsmax-mux-observed') === '1') {
2799            if (
2800                ins.dataset.pollsmaxPushed !== '1' &&
2801                !ins.getAttribute('data-ad-status')
2802            ) {
2803                /* Clear retry gate so late forecast mount can re-kick push. */
2804                try { ins.removeAttribute('data-pollsmax-mux-retry'); } catch (eRetry) { /* ignore */ }
2805                scheduleMultiplexPushRetries(ins);
2806            }
2807        } else {
2808            observeAdSenseUnit(ins);
2809        }
2810    }
2811
2812    function watchForecastReadyForMultiplex() {
2813        if (!pageKind().forecast) return;
2814        var root = document.documentElement;
2815        function kick() {
2816            ensureSiteMultiplex();
2817        }
2818        if (root.classList.contains('race-view-forecast-ready')) {
2819            kick();
2820            return;
2821        }
2822        if (typeof MutationObserver !== 'function') {
2823            setTimeout(kick, 2000);
2824            return;
2825        }
2826        var mo = new MutationObserver(function () {
2827            if (!root.classList.contains('race-view-forecast-ready')) return;
2828            try { mo.disconnect(); } catch (eMo) { /* ignore */ }
2829            kick();
2830        });
2831        mo.observe(root, { attributes: true, attributeFilter: ['class'] });
2832    }
2833
2834    global.PollsmaxEnsureSiteMultiplex = ensureSiteMultiplex;
2835    global.PollsmaxInitializeAdSenseUnit = initializeAdSenseUnit;
2836    global.PollsmaxWaitForAdSense = waitForAdSense;
2837
2838    /* ---------- Homepage ---------- */
2839
2840    /** PC only: dual horizontals like page-top — after Upcoming / Search / Key Races. */
2841    function placeHomeDesktopDualRows(kind) {
2842        var idUpcoming = 'homeAdAfterUpcomingElections';
2843        var idSearch = 'homeAdAfterSearchAllPolls';
2844        var idKeyRaces = 'homeAdAfterKeyRaces';
2845        if (!kind || !kind.home || isMobile()) {
2846            removeShell(document.getElementById(idUpcoming), true);
2847            removeShell(document.getElementById(idSearch), true);
2848            removeShell(document.getElementById(idKeyRaces), true);
2849            return;
2850        }
2851
2852        function makeHomeMidRow(id) {
2853            return makeRow(id, {
2854                extraClass: 'pollsmax-ad-row--page-top pollsmax-ad-row--page-top-wide pollsmax-ad-row--home-mid',
2855                format: 'rectangle'
2856            });
2857        }
2858
2859        function mountHomeMid(id, beforeEl) {
2860            if (!beforeEl || !beforeEl.parentNode) return;
2861            placeOnce(id, function () {
2862                return makeHomeMidRow(id);
2863            }, function (el) {
2864                if (el.parentNode !== beforeEl.parentNode || el.nextElementSibling !== beforeEl) {
2865                    beforeEl.parentNode.insertBefore(el, beforeEl);
2866                }
2867            });
2868            var mounted = document.getElementById(id);
2869            if (mounted) {
2870                mounted.setAttribute('data-ad-state', 'live');
2871                mounted.removeAttribute('data-pollsmax-ad-empty');
2872            }
2873        }
2874
2875        var upcoming = document.getElementById('homeUpcomingElectionsSection');
2876        var govDonate = document.querySelector('.sleek-maps--home-gov-donate');
2877        if (upcoming && govDonate) mountHomeMid(idUpcoming, govDonate);
2878
2879        var search = document.getElementById('searchAllPollsSection');
2880        var trumpCanvas = document.getElementById('trumpHomeApprovalChart');
2881        var approvalRow = trumpCanvas && trumpCanvas.closest ? trumpCanvas.closest('.sleek-maps') : null;
2882        if (search && approvalRow) mountHomeMid(idSearch, approvalRow);
2883
2884        var keyRaces = document.getElementById('homeKeyRacesSection');
2885        var primaries = document.querySelector('.sleek-maps--home-primaries');
2886        if (keyRaces && primaries) mountHomeMid(idKeyRaces, primaries);
2887    }
2888
2889    function placeHomeAds(kind) {
2890        if (!kind.home) return;
2891        var mobile = isMobile();
2892
2893        if (!mobile) {
2894            [['trumpHomeApprovalChart', 'homeAdTrumpChart'],
2895             ['gbHomeBallotChart', 'homeAdGbChart'],
2896             ['demHomePrimaryChart', 'homeAdDemPrimaryChart'],
2897             ['gopHomePrimaryChart', 'homeAdGopPrimaryChart']].forEac
2897h(function (pair) {
2898                var canvas = document.getElementById(pair[0]);
2899                var stage = canvas && canvas.closest ? canvas.closest('.sleek-chart-stage') : null;
2900                var host = stage && stage.parentElement;
2901                if (!host) return;
2902                placeOnce(pair[1], function () {
2903                    return makeBanner(pair[1], {
2904                        desktopOnly: true,
2905                        extraClass: 'pollsmax-ad-banner--in-col',
2906                        format: 'rectangle'
2907                    });
2908                }, function (el) { appendChild(host, el); });
2909            });
2910        } else {
2911            var govMaps = document.querySelector('.sleek-maps--home-gov-donate');
2912            var search = document.getElementById('searchAllPollsSection');
2913            if (govMaps && search) {
2914                placeOnce('homeAdBetweenGovAndSearch', function () {
2915                    return makeSingle('homeAdBetweenGovAndSearch', {
2916                        mobileOnly: true,
2917                        format: 'auto',
2918                        extraClass: 'pollsmax-ad-single--poll-mid-mobile'
2919                    });
2920                }, function (el) {
2921                    /* Prefer after Upcoming (true between-panels sibling); fallback before Search. */
2922                    var upcoming = document.getElementById('homeUpcomingElectionsSection');
2923                    if (upcoming && upcoming.parentNode) insertAfter(upcoming, el);
2924                    else insertBefore(search, el);
2925                });
2926                var govSearchAd = document.getElementById('homeAdBetweenGovAndSearch');
2927                if (govSearchAd) {
2928                    var upcomingPin = document.getElementById('homeUpcomingElectionsSection');
2929                    if (
2930                        upcomingPin &&
2931                        upcomingPin.parentNode &&
2932                        (govSearchAd.previousElementSibling !== upcomingPin ||
2933                            govSearchAd.parentNode !== upcomingPin.parentNode)
2934                    ) {
2935                        insertAfter(upcomingPin, govSearchAd);
2936                    }
2937                    observeShell(govSearchAd, { priority: true });
2938                }
2939            }
2940
2941            /*
2942             * Between Generic Ballot and 2028 Dem Primary (home mobile).
2943             * Mount immediately after the approval maps row — not after the hidden
2944             * Key Races block — so the shell stays in normal flow for IO + AdSense.
2945             */
2946            var approvalRow = document.querySelector('.sleek-maps--home-approval');
2947            var demRow = document.querySelector('.sleek-maps--home-primaries') ||
2948                (function () {
2949                    var demCanvas = document.getElementById('demHomePrimaryChart');
2950                    return demCanvas && demCanvas.closest ? demCanvas.closest('.sleek-maps') : null;
2951                })();
2952            var gbDemAnchor = approvalRow || demRow;
2953            if (gbDemAnchor) {
2954                placeOnce('homeAdBetweenGbAndDemPrimary', function () {
2955                    var aside = makeSingle('homeAdBetweenGbAndDemPrimary', {
2956                        mobileOnly: true,
2957                        format: 'auto',
2958                        extraClass: 'pollsmax-ad-single--poll-mid-mobile'
2959                    });
2960                    aside.setAttribute('data-ad-priority', '1');
2961                    return aside;
2962                }, function (el) {
2963                    /* Prefer immediately before 2028 Dem row so no hidden Key Races
2964                     * sibling sits between the ad and the next panel (extra margin-top). */
2965                    if (demRow && demRow.parentNode) {
2966                        insertBefore(demRow, el);
2967                    } else if (approvalRow && approvalRow.parentNode) {
2968                        insertAfter(approvalRow, el);
2969                    }
2970                });
2971                var gbDemAd = document.getElementById('homeAdBetweenGbAndDemPrimary');
2972                if (gbDemAd) {
2973                    if (
2974                        demRow &&
2975                        demRow.parentNode &&
2976                        (gbDemAd.nextElementSibling !== demRow ||
2977                            gbDemAd.parentNode !== demRow.parentNode)
2978                    ) {
2979                        insertBefore(demRow, gbDemAd);
2980                    }
2981                    observeShell(gbDemAd, { priority: true });
2982                }
2983            }
2984        }
2985    }
2986
2987    /* ---------- Hub pages ---------- */
2988
2989    function placeHubAds(kind) {
2990        if (!(kind.senateHub || kind.govHub || kind.houseHub)) return;
2991        var mobile = isMobile();
2992
2993        if (mobile) {
2994            removeShell(document.getElementById('hubAdAfterRacesListMobile'), true);
2995            removeShell(document.getElementById('hubAdAfterSearchPollsMobile'), true);
2996
2997            /*
2998             * Senate / Governor / House hub mobile: only two dis
2998play mids —
2999             * 1) directly under the map block
3000             * 2) under last sim chart / before All … Races
3001             * Never above the map. Strip map-key + chart interstitials.
3002             */
3003            if (kind.senateHub || kind.govHub || kind.houseHub) {
3004                var afterMapId = kind.senateHub
3005                    ? 'senateHubAdAfterMapMobile'
3006                    : kind.govHub
3007                        ? 'govHubAdAfterMapMobile'
3008                        : 'houseHubAdAfterMapMobile';
3009                var afterOddsId = kind.senateHub
3010                    ? 'senateHubAdBeforeRacesMobile'
3011                    : kind.govHub
3012                        ? 'govHubAdBeforeRacesMobile'
3013                        : 'houseHubAdBeforeRacesMobile';
3014                var chartsSecId = kind.senateHub
3015                    ? 'senateHubSimulationChartsSection'
3016                    : kind.govHub
3017                        ? 'govHubSimulationChartsSection'
3018                        : 'houseHubSimulationChartsSection';
3019
3020                removeShell(document.getElementById('hubAdAfterMapKeyMobile'), true);
3021                removeShell(document.getElementById('senateHubAdAfterDistribution'), true);
3022                removeShell(document.getElementById('senateHubAdAfterProjected'), true);
3023                removeShell(document.getElementById('senateHubAdAfterControlOdds'), true);
3024                removeShell(document.getElementById('govHubAdAfterDistribution'), true);
3025                removeShell(document.getElementById('govHubAdAfterProjected'), true);
3026                var hubTalls = document.querySelectorAll(
3027                    '.hub-simulation-charts-section .pollsmax-ad-banner--hub-tall, ' +
3028                    '#' + chartsSecId + ' .pollsmax-ad-shell'
3029                );
3030                var ti;
3031                for (ti = 0; ti < hubTalls.length; ti++) {
3032                    var tall = hubTalls[ti];
3033                    if (!tall || !tall.id) {
3034                        removeShell(tall, true);
3035                        continue;
3036                    }
3037                    if (tall.id === afterMapId || tall.id === afterOddsId) {
3038                        continue;
3039                    }
3040                    removeShell(tall, true);
3041                }
3042
3043                /* Collapse only true unfilled stubs — waiting/loading must stay sized. */
3044                function scrubDeadHubMid(id) {
3045                    var dead = document.getElementById(id);
3046                    if (!dead) return;
3047                    if (
3048                        dead.getAttribute('data-pollsmax-ad-empty') === '1' ||
3049                        dead.getAttribute('data-ad-state') === 'unfilled'
3050                    ) {
3051                        try {
3052                            dead.style.setProperty('display', 'none', 'important');
3053                            dead.style.setProperty('height', '0', 'important');
3054                            dead.style.setProperty('max-height', '0', 'important');
3055                            dead.style.setProperty('margin', '0', 'important');
3056                        } catch (eDead) { /* ignore */ }
3057                    }
3058                }
3059                scrubDeadHubMid(afterMapId);
3060                scrubDeadHubMid(afterOddsId);
3061
3062                var chartsSec =
3063                    document.getElementById(chartsSecId) ||
3064                    document.querySelector('.hub-simulation-charts-section');
3065                var mapEl =
3066                    document.querySelector('.senate-right-content > .sleek-maps.senate-map-divider') ||
3067                    document.querySelector('.senate-right-content > .sleek-maps') ||
3068                    document.querySelector('.senate-right-content > .senate-map-divider') ||
3069                    document.querySelector('.sleek-maps.senate-map-divider');
3070
3071                function isHubKeepMid(el) {
3072                    return !!(el && (el.id === afterMapId || el.id === afterOddsId));
3073                }
3074
3075                function hideAutoAd(el) {
3076                    if (!el || !el.style) return;
3077                    try {
3078                        el.style.setProperty('display', 'none', 'important');
3079                        el.style.setProperty('height', '0', 'important');
3080                        el.style.setProperty('max-height', '0', 'important');
3081                        el.style.setProperty('margin', '0', 'important');
3082                        el.style.setProperty('padding', '0', 'important');
3083                        el.style.setProperty('overflow', 'hidden', 'important');
3084                        el.setAttribute('data-pollsmax-hub-ad-scrubbed', '1');
3085                    } catch (eHide) { /* ignore */ }
3086                }
3087
3088                /*
3089                 * Anything that paints above the map (Auto ads, leftover shells,
3090                 * misplaced mids) must go — then we pin afterMap under the map.
3091                 */
3092                function scrubAdsAboveMap(map) {
3093                    if (!map) return;
3094                    var scope = document.querySelector('main') || document.body;
3095                    if (!scope || !scope.querySelectorAll) return;
3096                    var nodes = scope.querySelectorAll(
3097                        '.google-auto-placed, .pollsmax-ad-shell, .pollsmax-ad-single, ' +
3098                        '.pollsmax-ad-banner, .pollsmax-ad-row, aside.pollsmax-site-multiplex'
3099                    );
3100                    var i;
3101                    for (i = 0; i < nodes.length; i++) {
3102                        var el = nodes[i];
3103                        if (!el || el === map) continue;
3104                        if (el.classList && el.classList.contains('pollsmax-site-multiplex')) continue;
3105                        if (isHubKeepMid(el)) continue;
3106                        /* PRECEDING = el is above the map in document order. */
3107                        if (!(map.compareDocumentPosition(el) & Node.DOCUMENT_POSITION_PRECEDING)) {
3108                            continue;
3109                        }
3110                        if (el.classList && el.classList.contains('google-auto-placed')) {
3111                            hideAutoAd(el);
3112                            continue;
3113                        }
3114                        removeShell(el, true);
3115                    }
3116                    /* Also clear previous siblings inside the right column. */
3117                    if (map.parentNode) {
3118                        var pre = map.parentNode.firstElementChild;
3119                        while (pre && pre !== map) {
3120                            var preNext = pre.nextElementSibling;
3121                            if (isHubKeepMid(pre)) {
3122                                /* Misplaced keep-mid — move under map below. */
3123                                pre = preNext;
3124                                continue;
3125                            }
3126                            if (
3127                                pre.classList &&
3128                                (pre.classList.contains('google-auto-placed') ||
3129                                    pre.classList.contains('pollsmax-ad-shell') ||
3130                                    pre.classList.contains('pollsmax-ad-single') ||
3131                                    pre.classList.contains('pollsmax-ad-banner') ||
3132                                    pre.classList.contains('pollsmax-ad-row'))
3133                            ) {
3134                                if (pre.classList.contains('google-auto-placed')) hideAutoAd(pre);
3135                                else removeShell(pre, true);
3136                            }
3137                            pre = preNext;
3138                        }
3139                    }
3140                }
3141
3142                /**
3143                 * Only one unit may sit under the map. Strip Auto ads / leftover
3144                 * shells between map → charts, and never leave the before-races mid
3145                 * as a second under-map sibling when charts aren't mounted yet.
3146                 */
3147                function scrubAdsUnderMap(map, charts) {
3148                    if (!map || !map.parentNode) return;
3149                    var node = map.nextElementSibling;
3150                    while (node) {
3151                        var next = node.nextElementSibling;
3152                        if (
3153                            node === charts ||
3154                            (node.id && node.id === chartsSecId) ||
3155                            (node.classList && node.classList.contains('hub-simulation-charts-section'))
3156                        ) {
3157                            break;
3158                        }
3159                        if (node.id === afterMapId) {
3160                            node = next;
3161                            continue;
3162                        }
3163                        /* Second mid belongs after charts — park it, don't fill here. */
3164                        if (node.id === afterOddsId) {
3165                            if (charts && charts.parentNode) insertAfter(charts, node);
3166                            else {
3167                                setShellCollapsed(node);
3168                                try { node.style.setProperty('display', 'none', 'important'); } catch (ePark) { /* ignore */ }
3169                            }
3170                            node = next;
3171                            continue;
3172                        }
3173                        if (
3174                            node.classList &&
3175                            (node.classList.contains('google-auto-placed') ||
3176                                node.classList.contains('pollsmax-ad-shell') ||
3177                                node.classList.contains('pollsmax-ad-single') ||
3178                                node.classList.contains('pollsmax-ad-banner') ||
3179                                node.classList.contains('pollsmax-ad-row'))
3180                        ) {
3181                            if (node.classList.contains('google-auto-placed')) hideAutoAd(node);
3182                            else if (!node.classList.contains('pollsmax-site-multiplex')) {
3183                                removeShell(node, true);
3184                            }
3185                        }
3186                        node = next;
3187                    }
3188                }
3189
3190                function pinUnderMap(el, map) {
3191                    if (!el || !map || !map.parentNode) return;
3192                    if (el.previousElementSibling !== map || el.parentNode !== map.parentNode) {
3193                        map.parentNode.insertBefore(el, map.nextSibling);
3194                    }
3195                    /* Map uses flex order:1; default order:0 paints ads above it. */
3196                    try { el.style.setProperty('order', '2', 'important'); } catch (eOrd) { /* ignore */ }
3197                }
3198
3199                function kickHubMid(el) {
3200                    if (!el) return;
3201                    var ins = el.querySelector && el.querySelector('ins.adsbygoogle');
3202                    var insStatus = ins && (ins.getAttribute('data-ad-status') || '');
3203                    /* Only Google unfilled kills the slot — never stay collapsed waiting. */
3204                    if (insStatus === 'unfilled' || insStatus === 'unfill-optimized') return;
3205                    if (el.getAttribute('data-ad-state') === 'filled' && insStatus === 'filled') return;
3206                    if (el.querySelector('ins.adsbygoogle[data-pollsmax-initialized="true"]') && insStatus === 'filled') return;
3207                    el.removeAttribute('data-pollsmax-observed');
3208                    el.removeAttribute('data-pollsmax-ad-empty');
3209                    try { el.style.removeProperty('display'); } catch (eKick) { /* ignore */ }
3210                    if (
3211                        !el.getAttribute('data-ad-state') ||
3212                        el.getAttribute('data-ad-state') === 'loading'
3213                    ) {
3214                        el.setAttribute('data-ad-state', 'waiting');
3215                    }
3216                    clearShellLayoutInline(el);
3217                    setShellLoading(el);
3218                    pinMobileAdToStandardWidth(el);
3219                    observeShell(el, { priority: true });
3220                    applyMockMobileAds();
3221                }
3222
3223                function hubChartsReady(sec) {
3224                    if (!sec) return false;
3225                    return !!(
3226                        sec.querySelector('.senate-hub-mob-panel--chart') ||
3227                        sec.querySelector('.sleek-chart-stage') ||
3228                        sec.querySelector('.hub-sim-hist-stage') ||
3229                        sec.querySelector('canvas')
3230                    );
3231                }
3232
3233                scrubAdsAboveMap(mapEl);
3234
3235                /* 1) Directly under the map block — forced next sibling of the map. */
3236                if (mapEl) {
3237                    var afterMap = placeOnce(afterMapId, function () {
3238                        return makeSingle(afterMapId, {
3239                            mobileOnly: true,
3240                            format: 'auto',
3241                            extraClass: 'pollsmax-ad-single--poll-mid-mobile'
3242                        });
3243                    }, function (el) { pinUnderMap(el, mapEl); });
3244                    pinUnderMap(afterMap, mapEl);
3245                    kickHubMid(afterMap);
3246                    scrubAdsUnderMap(mapEl, chartsSec);
3247                }
3248
3249                /*
3250                 * 2) Under the bottom sim chart block:
3251                 *    Senate → Odds of Winning; Governor → Projected / last chart panel.
3252                 *    Wait until charts exist — otherwise this mid stacks under the map.
3253                 */
3254                function lastChartPanelIn(sec) {
3255                    if (!sec) return null;
3256                    var panels = sec.querySelectorAll('.senate-hub-mob-panel--chart');
3257                    if (panels.length) return panels[panels.length - 1];
3258                    var stages = sec.querySelectorAll(
3259                        '.sleek-chart-stage, .hub-sim-hist-stage, .race-forecast-sim-stage'
3260                    );
3261                    return stages.length ? stages[stages.length - 1] : null;
3262                }
3263                var chartCanvasId = kind.senateHub
3264                    ? 'senateHubControlOddsChart'
3265                    : kind.govHub
3266                        ? 'govHubExpectedSeatsChart'
3267                        : 'houseHubControlOddsChart';
3268                var chartCanvas = document.getElementById(chartCanvasId);
3269                var chartAnchor =
3270                    (chartCanvas && chartCanvas.closest('.senate-hub-mob-panel--chart')) ||
3271                    (chartCanvas && chartCanvas.closest('.sleek-chart-stage')) ||
3272                    lastChartPanelIn(chartsSec) ||
3273                    null;
3274                var racesSec =
3275                    document.getElementById('hubMobileRacesSection') ||
3276                    document.querySelector('.hub-mobile-races-section');
3277                var chartsReady = !!(chartAnchor || hubChartsReady(chartsSec));
3278                if (!chartsReady) {
3279                    /* Park — do not destroy. Re-place when charts mount. */
3280                    var parked = document.getElementById(afterOddsId);
3281                    if (parked && racesSec && racesSec.parentNode) {
3282                        insertBefore(racesSec, parked);
3283                        try { parked.style.setProperty('order', '4', 'important'); } catch (eParkOrd) { /* ignore */ }
3284                    }
3285                    if (mapEl) scrubAdsUnderMap(mapEl, chartsSec);
3286                    return;
3287                }
3288                if (chartAnchor || racesSec || chartsSec) {
3289                    var afterChart = placeOnce(afterOddsId, function () {
3290                        return makeSingle(afterOddsId, {
3291                            mobileOnly: true,
3292                            format: 'auto',
3293                            extraClass: 'pollsmax-ad-single--poll-mid-mobile'
3294                        });
3295                    }, function (el) {
3296                        /* Sibling of the charts host (order 4) — never nest inside it. */
3297                        if (racesSec && racesSec.parentNode) {
3298                            insertBefore(racesSec, el);
3299                        } else if (chartsSec && chartsSec.parentNode) {
3300                            insertAfter(chartsSec, el);
3301                        }
3302                    });
3303                    if (afterChart && racesSec && racesSec.parentNode) {
3304                        if (
3305                            afterChart.nextElementSibling !== racesSec ||
3306                            afterChart.parentNode !== racesSec.parentNode
3307                        ) {
3308                            insertBefore(racesSec, afterChart);
3309                        }
3310                        try { afterChart.style.setProperty('order', '4', 'important'); } catch (eOrd2) { /* ignore */ }
3311                    }
3312                    if (mapEl) scrubAdsUnderMap(mapEl, chartsSec);
3313                    /* Guard: never kick a before-races mid that still sits under the map. */
3314                    if (
3315                        afterChart &&
3316                        mapEl &&
3317                        afterChart.parentNode === mapEl.parentNode &&
3318                        (afterChart.previousElementSibling === mapEl ||
3319                            afterChart.previousElementSibling === document.getElementById(afterMapId)) &&
3320                        !(chartsSec && (
3321                            afterChart.compareDocumentPosition(ch
3321artsSec) &
3322                            Node.DOCUMENT_POSITION_PRECEDING
3323                        ))
3324                    ) {
3325                        if (chartsSec && chartsSec.parentNode) insertAfter(chartsSec, afterChart);
3326                        else {
3327                            setShellCollapsed(afterChart);
3328                            return;
3329                        }
3330                    }
3331                    kickHubMid(afterChart);
3332                }
3333                return;
3334            }
3335        }
3336
3337        /* Desktop: chart ads are injected by hub-charts-page.js; house race ratings here. */
3338        if (kind.houseHub) {
3339            var buckets = document.getElementById('houseForecastBucketsSection');
3340            if (buckets) {
3341                placeOnce('houseHubAdAfterRaceRatings', function () {
3342                    return makeBanner('houseHubAdAfterRaceRatings', {
3343                        desktopOnly: true,
3344                        extraClass: 'pollsmax-ad-banner--hub-tall',
3345                        format: 'auto'
3346                    });
3347                }, function (el) { appendChild(buckets, el); });
3348            }
3349        }
3350    }
3351
3352    /** PC only runs page-top + multiplex — no hub chart ads. */
3353    ET.hubChartSlotAdHtml = function () {
3354        return '';
3355    };
3356
3357    ET.initHubChartAds = function (root) {
3358        registerAllShells(root);
3359    };
3360
3361    /* ---------- Race polling / forecast / approval / 2028 ---------- */
3362
3363    function sectionOf(el) {
3364        return el && el.closest ? (el.closest('section') || el) : el;
3365    }
3366
3367    function placeAfterStatsOrChart(adId, mobile) {
3368        var stats =
3369            document.querySelector('.mi-senate-chart-stat-head') ||
3370            document.querySelector('.national-approval-chart-stat-head') ||
3371            document.querySelector('.approval-stat-grid');
3372        var chart =
3373            document.getElementById('avgChart') ||
3374            document.getElementById('senLineViz') ||
3375            document.getElementById('govLineViz') ||
3376            document.querySelector('.sleek-chart-stage');
3377        var anchor = stats || chart;
3378        if (!anchor) return null;
3379        var sec = sectionOf(anchor);
3380        return placeOnce(adId, function () {
3381            return mobile
3382                ? makeSingle(adId, {
3383                    mobileOnly: true,
3384                    format: 'auto',
3385                    extraClass: 'pollsmax-ad-single--poll-mid-mobile'
3386                })
3387                : makeRow(adId, { desktopOnly: true, format: 'rectangle' });
3388        }, function (el) {
3389            if (sec && sec.parentNode) insertAfter(sec, el);
3390            else insertAfter(anchor, el);
3391        });
3392    }
3393
3394    /**
3395     * Mobile primary polling: bar chart (≤2 polls) — no Google ad above the table.
3396     * Uses body.et-state-poll-bar-chart-active when set; falls back to #primaryBarViz visibility.
3397     */
3398    function primaryPollMobileUsesBarChart() {
3399        if (!isMobile()) return false;
3400        if (document.body && document.body.classList.contains('et-state-poll-bar-chart-active')) {
3401            return true;
3402        }
3403        var bar = document.getElementById('primaryBarViz') ||
3404            document.querySelector('[id$="PrimaryBarViz"]');
3405        if (!bar || !bar.isConnected) return false;
3406        if (bar.style && bar.style.display === 'none') return false;
3407        try {
3408            var cs = window.getComputedStyle(bar);
3409            if (cs.display === 'none' || cs.visibility === 'hidden') return false;
3410        } catch (eBar) { /* ignore */ }
3411        var line = document.getElementById('primaryLineViz') ||
3412            document.querySelector('[id$="PrimaryLineViz"]');
3413        if (line && line.isConnected) {
3414            if (line.style && line.style.display === 'none') return true;
3415            try {
3416                var lcs = window.getComputedStyle(line);
3417                if (lcs.display === 'none') return true;
3418            } catch (eLine) { /* ignore */ }
3419        }
3420        return false;
3421    }
3422
3423    /** Remove Google above-table mid on mobile primary bar-chart pages (Mediavine untouched). */
3424    function scrubPrimaryPollMobileBarChartAboveTableAd() {
3425        if (!isMobile()) return;
3426        if (!pageKind().primaryPoll) return;
3427        if (!primaryPollMobileUsesBarChart()) return;
3428        removeShell(document.getElementById('racePrimaryPollAdBeforeTableMobile'), true);
3429    }
3430
3431    /** Chart init can land after ads boot — re-scrub when bar class / viz flips. */
3432    function schedulePrimaryPollMobileBarChartAdScrub() {
3433        if (!isMobile() || !pageKind().primaryPoll) return;
3434        scrubPrimaryPollMobileBarChartAboveTableAd();
3435        setTimeout(scrubPrimaryPollMobileBarChartAboveTableAd, 120);
3436        setTimeout(scrubPrimaryPollMobileBarChartAboveTableAd, 500);
3437        setTimeout(scrubPrimaryPollMobileBarChartAboveTableAd, 1600);
3438        if (schedulePrimaryPollMobileBarChartAdScrub._watching) return;
3439        schedulePrimaryPollMobileBarChartAdScrub._watching = true;
3440        try {
3441            var mo = new MutationObserver(function () {
3442                scrubPrimaryPollMobileBarChartAboveTableAd();
3443            });
3444            if (document.body) {
3445                mo.observe(document.body, { attributes: true, attributeFilter: ['class'] });
3446            }
3447            var bar = document.getElementById('primaryBarViz');
3448            var line = document.getElementById('primaryLineViz');
3449            if (bar) mo.observe(bar, { attributes: true, attributeFilter: ['style', 'class'] });
3450            if (line) mo.observe(line, { attributes: true, attributeFilter: ['style', 'class'] });
3451        } catch (eWatch) { /* ignore */ }
3452    }
3453
3454    /**
3455     * Mobile: single unit immediately above the polls table
3456     * (below GE 4-rectangle stats, or below the chart on primary / 2028 pages).
3457     */
3458    function placeBeforePollsTableMobile(adId) {
3459        if (!isMobile()) return null;
3460        var shell = document.getElementById('pollsTableShell');
3461        if (shell) {
3462            var tableSec = (shell.closest && shell.closest('section')) || pollTableOuterBo
3462x(shell);
3463            if (tableSec && tableSec.parentNode) {
3464                return placeOnce(adId, function () {
3465                    return makeSingle(adId, {
3466                        mobileOnly: true,
3467                        format: 'auto',
3468                        extraClass: 'pollsmax-ad-single--poll-mid-mobile'
3469                    });
3470                }, function (el) {
3471                    insertBefore(tableSec, el);
3472                    notifyGePollMidAdChartTableGap(el);
3473                });
3474            }
3475        }
3476        return placeAfterStatsOrChart(adId, true);
3477    }
3478
3479    /** Outer bordered table chrome — ads go after this, never inside it. */
3480    function pollTableOuterBox(shell) {
3481        if (!shell) return null;
3482        return (shell.closest && (
3483            shell.closest('.state-polls-shell') ||
3484            shell.closest('.race-forecast-polls-table') ||
3485            shell.closest('[class*="polls-shell"]')
3486        )) || shell;
3487    }
3488
3489    /** PC mid-page duals — same chrome as page-top / Latest Polls in-feed. */
3490    function makeDesktopPageTopDualRow(id, extraClass) {
3491        return makeRow(id, {
3492            extraClass: 'pollsmax-ad-row--page-top pollsmax-ad-row--page-top-wide' +
3493                (extraClass ? ' ' + extraClass : ''),
3494            format: 'rectangle'
3495        });
3496    }
3497
3498    function activateDesktopDualRow(el) {
3499        if (!el) return;
3500        el.setAttribute('data-ad-state', 'live');
3501        el.removeAttribute('data-pollsmax-ad-empty');
3502    }
3503
3504    /** Sum of next sibling margin-top + padding-top that sits between ad and content. */
3505    function midAdNextInsetPx(el) {
3506        if (!el) return 0;
3507        var next = el.nextElementSibling;
3508        var total = 0;
3509        var hops = 0;
3510        while (next && hops < 3) {
3511            hops++;
3512            /* Zero-height full-bleed rules don't count as content — skip past them. */
3513            var isRule = next.classList && (
3514                next.classList.contains('race-forecast-trends-rule') ||
3515                next.classList.contains('race-forecast-eh-bottom-rule') ||
3516                next.classList.contains('race-related-races-rule') ||
3517                next.classList.contains('race-forecast-polling-data-rule')
3518            );
3519            try {
3520                var cs = global.getComputedStyle(next);
3521                var mt = parseFloat(cs.marginTop) || 0;
3522                var pt = parseFloat(cs.paddingTop) || 0;
3523                /* Allow negative (e.g. race-polling-page table pull) so mb can compensate. */
3524                total += Math.round(mt + pt);
3525                /*
3526                 * Section wrappers are often 0-inset; the real top inset lives on the
3527                 * first child (pt-7 / margin-top:-20px on poll-table hosts).
3528                 */
3529                if (!isRule && next.firstElementChild) {
3530                    var inner = next.firstElementChild;
3531                    var ics = global.getComputedStyle(inner);
3532                    total += Math.round(
3533                        (parseFloat(ics.marginTop) || 0) + (parseFloat(ics.paddingTop) || 0)
3534                    );
3535                    /* sleek-home .state-polls-shell uses margin-top:-14px — count it too. */
3536                    var shell =
3537                        (inner.classList && inner.classList.contains('state-polls-shell') && inner) ||
3538                        (inner.querySelector && inner.querySelector('.state-polls-shell'));
3539                    if (shell && shell !== inner) {
3540                        var scs = global.getComputedStyle(shell);
3541                        total += Math.round(parseFloat(scs.marginTop) || 0);
3542                    }
3543                }
3544            } catch (eCs) { /* ignore */ }
3545            if (!isRule) break;
3546            next = next.nextElementSibling;
3547        }
3548        /* Related Races lives outside the poll section — include its margin-top. */
3549        if (el.id === 'raceForecastAdUnderPolls') {
3550            var related =
3551                document.getElementById('raceRelatedRaces') ||
3552                document.querySelector('.race-related-races');
3553            if (related && !el.parentNode.contains(related)) {
3554                try {
3555                    var rcs = global.getComputedStyle(related);
3556                    total += Math.max(0, Math.round(parseFloat(rcs.marginTop) || 0));
3557                } catch (eR) { /* ignore */ }
3558            }
3559        }
3560        return total;
3561    }
3562
3563    /**
3564     * PC-only per-placement tweaks vs the canonical mid gap.
3565     * mtDelta / mbDelta adjust space above / below the ad row.
3566     *
3567     * Senate/Gov/House GE + race primary above-table duals: equal +1 so the
3568     * visual gap matches Montana Senate's below-ad spacing (~55px = 1.5rem+30px+1).
3569     */
3570    function midContentAdGapDeltas(el) {
3571        if (!el) return { mt: 0, mb: 0 };
3572        var id = el.id || '';
3573        var kind = pageKind();
3574
3575        /* Forecast: under Projected Vote Share / Odds of Winning. */
3576        if (id === 'raceForecastAdUnderTrends') {
3577            return { mt: -32, mb: 32 };
3578        }
3579        /* Forecast: under polls table (Polling Data section). */
3580        if (id === 'raceForecastAdUnderPolls') {
3581            return { mt: 0, mb: 33 };
3582        }
3583        /* GE + race primary polling: above polls table (Montana below-ad reference). */
3584        if (
3585            id === 'raceGePollAdBeforeTable' ||
3586            id === 'racePrimaryPollAdBeforeTable' ||
3587            ((kind.gePoll || kind.primaryPoll) && el.classList.contains('pollsmax-ad-row--mid-bleed'))
3588        ) {
3589            return { mt: 1, mb: 1 };
3590        }
3591        /* 2028 Dem / GOP primary: above polls table. */
3592        if (id === 'primary2028AdBeforeTable' || (kind.primary2028 && el.classList.contains('pollsmax-ad-row--mid-bleed'))) {
3593            return { mt: 10, mb: 14 };
3594        }
3595        /* Trump approval + generic ballot: above polls table. */
3596        if (id === 'approvalAdBeforeTable' || (kind.approval && el.classList.contains('pollsmax-ad-row--mid-bleed'))) {
3597            return { mt: -85, mb: 14 };
3598        }
3599        return { mt: 0, mb: 0 };
3600    }
3601
3602    /**
3603     * PC: equal visual gap above/below each mid dual, plus page-specific deltas.
3604     * margin-top = GAP + mtDelta; margin-bottom = GAP − next-heading inset + mbDelta
3605     * so Electoral History / Polling Data / Related Races keep their own
3606     * padding and still sit the intended distance from the ad edge.
3607     */
3608    function syncMidContentAdGaps() {
3609        if (isMobile()) return;
3610        var gapPx = 54;
3611        try {
3612            var rootFs = parseFloat(global.getComputedStyle(document.documentElement).fontSize) || 16;
3613            gapPx = Math.round(rootFs * 1.5 + 30);
3614        } catch (eGap) { /* keep fallback */ }
3615
3616        var nodes = document.querySelectorAll(
3617            '.pollsmax-ad-row--race-mid, .pollsmax-ad-row--forecast-mid'
3618        );
3619        var i;
3620        for (i = 0; i < nodes.length; i++) {
3621            var el = nodes[i];
3622            var nextInset = midAdNextInsetPx(el);
3623            var delta = midContentAdGapDeltas(el);
3624            var mt = Math.max(0, gapPx + (delta.mt || 0));
3625            var mb = Math.max(0, gapPx - nextInset + (delta.mb || 0));
3626            el.style.setProperty('margin-top', mt + 'px', 'important');
3627            el.style.setProperty('margin-bottom', mb + 'px', 'important');
3628        }
3629    }
3630
3631    function scheduleMidContentAdGapSync() {
3632        function run() {
3633            syncMidContentAdGaps();
3634            syncTopAdContentBounds();
3635        }
3636        run();
3637        requestAnimationFrame(run);
3638        setTimeout(run, 120);
3639        setTimeout(run, 500);
3640        setTimeout(run, 1600);
3641    }
3642
3643    function placeUnderPollTable(adId, mobile) {
3644        var shell = document.getElementById('pollsTableShell');
3645        if (!shell) return null;
3646        var box = pollTableOuterBox(shell);
3647        if (!box || !box.parentNode) return null;
3648        return placeOnce(adId, function () {
3649            return mobile
3650                ? makeSingle(adId, {
3651                    mobileOnly: true,
3652                    format: 'auto',
3653                    extraClass: 'pollsmax-ad-single--poll-mid-mobile'
3654                })
3655                : makeDesktopPageTopDualRow(adId, 'pollsmax-ad-row--race-mid');
3656        }, function (el) {
3657            /* Sibling under the table shell — not a child inside the bordered box. */
3658            insertAfter(box, el);
3659            if (!mobile) activateDesktopDualRow(el);
3660        });
3661    }
3662
3663    /**
3664     * Walk up from the polls table until the block that is a direct child of
3665     * <main> (or #racePollingBottom). Above-table duals must sit there —
3666     * nesting inside px-* sections double-insets and leaves a middle gap.
3667     */
3668    function abovePollTableAnchor(box) {
3669        if (!box) return null;
3670        var pollingBottom = document.getElementById('racePollingBottom');
3671        var mainEl = document.querySelector('main');
3672        var host =
3673            (pollingBottom && pollingBottom.contains(box) && pollingBottom) ||
3674            (mainEl && mainEl.contains(box) && mainEl) ||
3675            null;
3676        if (!host) {
3677            var section = box.closest ? box.closest('section') : null;
3678            return section && section.parentNode ? section : box;
3679        }
3680        var cur = box;
3681        var hops = 0;
3682        while (cur && cur.parentNode && hops < 12) {
3683            hops++;
3684            if (cur.parentNode === host) return cur;
3685            cur = cur.parentNode;
3686        }
3687        var fallback = box.closest ? box.closest('section') : null;
3688        return fallback && fallback.parentNode ? fallback : box;
3689    }
3690
3691    /**
3692     * PC only: 2-up horizontal above the polls table.
3693     * Sit between chart block and table block (main child) so width sync
3694     * can use logo→Donate padding like home mid — never nest inside
3695     * the table section's px-* pad (that double-insets and clips).
3696     */
3697    function placeAbovePollTable(adId) {
3698        if (isMobile()) return null;
3699        var shell = document.getElementById('pollsTableShell');
3700        if (!shell) return null;
3701        var box = pollTableOuterBox(shell);
3702        if (!box || !box.parentNode) return null;
3703        var anchor = abovePollTableAnchor(box);
3704        if (!anchor || !anchor.parentNode) return null;
3705        return placeOnce(adId, function () {
3706            return makeDesktopPageTopDualRow(
3707                adId,
3708                'pollsmax-ad-row--race-mid pollsmax-ad-row--mid-bleed'
3709            );
3710        }, function (el) {
3711            /* Upgrade legacy nested placements to between-section bleed. */
3712            el.classList.add('pollsmax-ad-row--race-mid', 'pollsmax-ad-row--mid-bleed');
3713            insertBefore(anchor, el);
3714            activateDesktopDualRow(el);
3715        });
3716    }
3717
3718    ET.placeRacePollingPageAds = function () {
3719        if (ET.racePageViewMode && ET.racePageViewMode() === 'forecast') return;
3720        var kind = pageKind();
3721        if (!(kind.gePoll || kind.primaryPoll)) return;
3722        var mobile = isMobile();
3723
3724        if (kind.gePoll) {
3725            if (mobile) {
3726                /* Under the 4 chart rectangles, above the polls table. */
3727                placeBeforePollsTableMobile('raceGePollAdBeforeTableMobile');
3728                placeUnderPollTable('raceGePollAdAfterTableMobile', true);
3729            } else {
3730                /* PC: 2-up horizontal above the polls table only. */
3731                removeShell(document.getElementById('raceGePollAdAfterTable'), true);
3732                placeAbovePollTable('raceGePollAdBeforeTable');
3733            }
3734        } else if (kind.primaryPoll) {
3735            if (mobile) {
3736                /* Under chart, above the polls table — skip when bar chart (≤2 polls). */
3737                if (primaryPollMobileUsesBarChart()) {
3738                    removeShell(document.getElementById('racePrimaryPollAdBeforeTableMobile'), true);
3739                } else {
3740                    placeBeforePollsTableMobile('racePrimaryPollAdBeforeTableMobile');
3741                }
3742                placeUnderPollTable('racePrimaryPollAdAfterTableMobile', true);
3743            } else {
3744                /* PC: 2-up horizontal above the polls table only. */
3745                removeShell(document.getElementById('racePrimaryPollAdAfterChart'), true);
3746                removeShell(document.getElementById('racePrimaryPollAdAfterTable'), true);
3747                placeAbovePollTable('racePrimaryPollAdBeforeTable');
3748            }
3749        }
3750        registerAllShells();
3751        ensureSiteMultiplex();
3752        if (!mobile) scheduleMidContentAdGapSync();
3753    };
3754
3755    function placeForecastDesktopAds() {
3756        /* Retire legacy in-column banners + EH row. */
3757        removeShell(document.getElementById('raceForecastAdUnderVote'), true);
3758        removeShell(document.getElementById('raceForecastAdUnderOdds'), true);
3759        removeShell(document.getElementById('raceForecastAdUnderEh'), true);
3760
3761        /*
3762         * PC: one 2-up horizontal under Projected Vote Share / Odds of Winning,
3763         * still inside the 3-chart block — immediately after the trends grid,
3764         * BEFORE the trends-rule / Electoral History block.
3765         */
3766        var trends =
3767            document.querySelector('.rf-premium-trends') ||
3768            document.querySelector('.race-forecast-trends-grid');
3769        if (trends) {
3770            placeOnce('raceForecastAdUnderTrends', function () {
3771                return makeDesktopPageTopDualRow(
3772                    'raceForecastAdUnderTrends',
3773                    'pollsmax-ad-row--forecast-mid'
3774                );
3775            }, function (el) {
3776                insertAfter(trends, el);
3777                activateDesktopDualRow(el);
3778            });
3779        }
3780
3781        /* Polling Data: 2-up under the polls table when it exists. */
3782        placeUnderPollTable('raceForecastAdUnderPolls', false);
3783        /* House/other forecasts: table can land in #racePollingBottom after mount. */
3784        if (!document.getElementById('raceForecastAdUnderPolls')) {
3785            var pollingBottom = document.getElementById('racePollingBottom');
3786            var bottomShell = pollingBottom && pollingBottom.querySelector('#pollsTableShell, .state-polls-shell');
3787            if (bottomShell) {
3788                var bottomBox = pollTableOuterBox(
3789                    bottomShell.id === 'pollsTableShell' ? bottomShell : bottomShell.querySelector('#pollsTableShell') || bottomShell
3790                );
3791                if (bottomBox && bottomBox.parentNode) {
3792                    placeOnce('raceForecastAdUnderPolls', function () {
3793                        return makeDesktopPageTopDualRow(
3794                            'raceForecastAdUnderPolls',
3795                            'pollsmax-ad-row--forecast-mid'
3796                        );
3797                    }, function (el) {
3798                        insertAfter(bottomBox, el);
3799                        activateDesktopDualRow(el);
3800                    });
3801                }
3802            }
3803        }
3804        /* Nested mid duals — logo→Donate channel + canonical equal gaps. */
3805        scheduleMidContentAdGapSync();
3806    }
3807
3808    function placeForecastMobileAds() {
3809        /* Drop desktop-style in-col trend banners; keep dedicated mobile placements. */
3810        removeShell(document.getElementById('raceForecastAdUnderVote'), true);
3811        removeShell(document.getElementById('raceForecastAdUnderOdds'), true);
3812
3813        var panelsMode = !!(
3814            document.body &&
3815            document.body.classList.contains('race-forecast-mob-panels') &&
3816            !document.body.classList.contains('race-forecast-premium')
3817        );
3818
3819        var simBlock =
3820            document.querySelector('.race-forecast-sim-block') ||
3821            document.querySelector('.rf-premium-panel--sim');
3822        var oddsCanvas = document.getElementById('raceForecastOddsCanvas');
3823        var oddsCard = oddsCanvas && oddsCanvas.closest
3824            ? oddsCanvas.closest('.rf-premium-panel--trend, .race-forecast-trend-card')
3825            : null;
3826        var oddsStage = oddsCard
3827            ? oddsCard.querySelector('.sleek-chart-stage, .race-forecast-chart-stage, .rf-premium-panel__chart')
3828            : null;
3829        var trendsSection =
3830            document.querySelector('.race-forecast-trends-grid') ||
3831            document.querySelector('.rf-premium-trends');
3832        var pollPanel =
3833            document.getElementById('raceForecastPollingData') ||
3834            document.querySelector('.race-forecast-polling-data');
3835        var related =
3836            document.getElementById('raceRelatedRaces') ||
3837            document.querySelector('main > .race-related-races');
3838
3839        /*
3840         * Panels trial (Iowa etc.): place mids as siblings BETWEEN panels
3841         * (channel-centered via CSS) — same rhythm as hub (hero → map → ad):
3842         *   1) Sim → Vote (under first panel, like hub under-map mid)
3843         *   2) Odds → EH
3844         * No Polling Data → Related Races mid on panels mobile.
3845         */
3846        if (panelsMode) {
3847            removeShell(document.getElementById('raceForecastAdUnderPollsMobile'), true);
3848
3849            if (simBlock && simBlock.parentNode) {
3850                var afterSim = placeOnce('raceForecastAdTopOfSimMobile', function () {
3851                    return makeSingle('raceForecastAdTopOfSimMobile', {
3852                        mobileOnly: true,
3853                        format: 'auto',
3854                        extraClass: 'pollsmax-ad-single--poll-mid-mobile'
3855                    });
3856                }, function (el) {
3857                    insertAfter(simBlock, el);
3858                });
3859                if (afterSim) {
3860                    afterSim.setAttribute('data-ad-state', 'waiting');
3861                    setShellLoading(afterSim);
3862                    observeShell(afterSim, { priority: true });
3863                }
3864            }
3865
3866            if (trendsSection && trendsSection.parentNode) {
3867                var afterOdds = placeOnce('raceForecastAdAfterOddsMobile', function () {
3868                    return makeSingle('raceForecastAdAfterOddsMobile', {
3869                        mobileOnly: true,
3870                        format: 'auto',
3871                        extraClass: 'pollsmax-ad-single--poll-mid-mobile'
3872                    });
3873                }, function (el) {
3874                    insertAfter(trendsSection, el);
3875                });
3876                if (afterOdds) {
3877                    afterOdds.setAttribute('data-ad-state', 'waiting');
3878                    setShellLoading(afterOdds);
3879                    observeShell(afterOdds, { priority: true });
3880                }
3881            } else if (oddsCard && oddsCard.parentNode) {
3882                var afterOddsCard = placeOnce('raceForecastAdAfterOddsMobile', function () {
3883                    return makeSingle('raceForecastAdAfterOddsMobile', {
3884                        mobileOnly: true,
3885                        format: 'auto',
3886                        extraClass: 'pollsmax-ad-single--poll-mid-mobile'
3887                    });
3888                }, function (el) {
3889                    insertAfter(oddsCard, el);
3890                });
3891                if (afterOddsCard) {
3892                    afterOddsCard.setAttribute('data-ad-state', 'waiting');
3893                    setShellLoading(afterOddsCard);
3894                    observeShell(afterOddsCard, { priority: true });
3895                }
3896            }
3897            applyMockMobileAds();
3898            return;
3899        }
3900
3901        /*
3902         * Legacy mobile forecast (non-panels):
3903         * 1) Top of Simulated Results box
3904         * 2) Under Odds chart stage
3905         * 3) Under polls table
3906         */
3907        if (simBlock) {
3908            placeOnce('raceForecastAdTopOfSimMobile', function () {
3909                return makeSingle('raceForecastAdTopOfSimMobile', {
3910                    mobileOnly: true,
3911                    format: 'auto',
3912                    extraClass: 'pollsmax-ad-single--poll-mid-mobile'
3913                });
3914            }, function (el) {
3915                if (simBlock.firstElementChild !== el) {
3916                    simBlock.insertBefore(el, simBlock.firstChild);
3917                }
3918            });
3919        }
3920
3921        if (oddsStage || oddsCard || trendsSection) {
3922            placeOnce('raceForecastAdAfterOddsMobile', function () {
3923                return makeSingle('raceForecastAdAfterOddsMobile', {
3924                    mobileOnly: true,
3925                    format: 'auto',
3926                    extraClass: 'pollsmax-ad-single--poll-mid-mobile'
3927                });
3928            }, function (el) {
3929                if (oddsStage && oddsStage.parentNode) insertAfter(oddsStage, el);
3930                else if (oddsCard) appendChild(oddsCard, el);
3931                else if (trendsSection) insertAfter(trendsSection, el);
3932            });
3933        }
3934
3935        placeUnderPollTable('raceForecastAdUnderPollsMobile', true);
3936    }
3937
3938    ET.placeRaceForecastPageAds = function () {
3939        if (ET.racePageViewMode && ET.racePageViewMode() !== 'forecast') return;
3940        if (isMobile()) {
3941            placeForecastMobileAds();
3942        } else {
3943            /* Prefer Multiplex before forecast mid duals when called late (after mount). */
3944            ensureSiteMultiplex();
3945            placeForecastDesktopAds();
3946        }
3947        registerAllShells();
3948        applyMockMobileAds();
3949        ensureSiteMultiplex();
3950    };
3951
3952    /* Back-compat hooks used by older call sites */
3953    ET.placeRaceForecastPollsTableAd = function () {
3954        ET.placeRaceForecastPageAds();
3955    };
3956    ET.placeRaceForecastEhAd = function () {
3957        ET.placeRaceForecastPageAds();
3958    };
3959
3960    function placeApprovalAds(kind) {
3961        if (!kind || !kind.approval) return;
3962        var mobile = isMobile();
3963        if (mobile) {
3964            /* Drop any stale PC dual so it cannot sit between mobile mid-ad and table. */
3965            removeShell(document.getElementById('approvalAdBeforeTable'), true);
3966            /* Under the 4 rectangles / issue buttons, above the polls table. */
3967            placeBeforePollsTableMobile('approvalAdBeforeTableMobile');
3968        } else {
3969            /* PC: same 2-up horizontal above the polls table as race polling pages. */
3970            removeShell(document.getElementById('approvalAdBeforeTableMobile'), true);
3971            removeShell(document.getElementById('approvalAdBeforeTable'), true);
3972            placeAbovePollTable('approvalAdBeforeTable');
3973            scheduleMidContentAdGapSync();
3974        }
3975        registerAllShells();
3976    }
3977
3978    function placeLatestPollsAds(kind) {
3979        if (!kind || !kind.latestPolls) return;
3980        var content = document.getElementById('allPollsContent');
3981        var wrap = document.querySelector('.all-polls-table-wrap') || content;
3982        if (!wrap && !content) return;
3983        var mobile = isMobile();
3984
3985        /* 1-based date ordinals → 0-based indexes among visible date blocks. */
3986        var infeedOrd = [6, 11, 16, 21, 26, 31, 36, 41];
3987        var infeedIds = infeedOrd.map(function (n) { return 'latestPollsAdBeforeDate' + n; });
3988        /* Legacy 3/5/7 slots — always remove. */
3989        ['latestPollsAdBeforeDate3', 'latestPollsAdBeforeDate5', 'latestPollsAdBeforeDate7']
3990            .concat(infeedIds)
3991            .forEach(function (id) {
3992                if (mobile) removeShell(document.getElementById(id), true);
3993            });
3994
3995        if (mobile) {
3996            var host = wrap && wrap.id === 'allPollsContent' && wrap.parentElement ? wrap.parentElement : wrap;
3997            if (!host) return;
3998            placeOnce('latestPollsAdUnderListMobile', function () {
3999                return makeSingle('latestPollsAdUnderListMobile', {
4000                    mobileOnly: true,
4001                    format: 'auto',
4002                    extraClass: 'pollsmax-ad-single--poll-mid-mobile'
4003                });
4004            }, function (el) { insertAfter(host, el); });
4005            registerAllShells();
4006            return;
4007        }
4008
4009        removeShell(document.getElementById('latestPollsAdUnderListMobile'), true);
4010        removeShell(document.getElementById('latestPollsAdUnderList'), true);
4011        /* Clear retired ordinals so they cannot linger between filter changes. */
4012        ['latestPollsAdBeforeDate3', 'latestPollsAdBeforeDate5', 'latestPollsAdBeforeDate7'].forEach(function (id) {
4013            removeShell(document.getElementById(id), true);
4014        });
4015
4016        if (!content) return;
4017        var allBlocks = content.querySelectorAll('.all-polls-date-block');
4018        var blocks = [];
4019        var bi;
4020        for (bi = 0; bi < allBlocks.length; bi++) {
4021            if (allBlocks[bi].style.display === 'none') continue;
4022            blocks.push(allBlocks[bi]);
4023        }
4024
4025        /* Same 2-up horizontal as page-top — before 6th / 11th / 16th / … date blocks. */
4026        var slots = infeedOrd.map(function (n) {
4027            return { id: 'latestPollsAdBeforeDate' + n, index: n - 1 };
4028        });
4029        slots.forEach(function (slot) {
4030            var block = blocks[slot.index];
4031            if (!block || !block.parentNode) {
4032                removeShell(document.getElementById(slot.id), true);
4033                return;
4034            }
4035            placeOnce(slot.id, function () {
4036                return makeRow(slot.id, {
4037                    extraClass: 'pollsmax-ad-row--page-top pollsmax-ad-row--page-top-wide pollsmax-ad-row--latest-polls-infeed',
4038                    format: 'rectangle'
4039                });
4040            }, function (el) {
4041                if (el.parentNode !== block.parentNode || el.nextElementSibling !== block) {
4042                    block.parentNode.insertBefore(el, block);
4043                }
4044                el.setAttribute('data-ad-state', 'live');
4045                el.removeAttribute('data-pollsmax-ad-empty');
4046            });
4047        });
4048        registerAllShells();
4049        syncTopAdContentBounds();
4050        requestAnimationFrame(syncTopAdContentBounds);
4051        setTimeout(syncTopAdContentBounds, 120);
4052    }
4053
4054    function placeUpcomingElectionsAds(kind) {
4055        if (!kind || !kind.upcomingElections) return;
4056        var content = document.getElementById('upcomingElectionsPageRoot');
4057        if (!content) return;
4058        var mobile = isMobile();
4059        /* Every 5 date groups: before 6th, 11th, 16th, … */
4060        var infeedOrd = [6, 11, 16, 21, 26, 31, 36, 41];
4061        var infeedIds = infeedOrd.map(function (n) { return 'upcomingElectionsAdBeforeDate' + n; });
4062
4063        infeedIds.forEach(function (id) {
4064            if (mobile) removeShell(document.getElementById(id), true);
4065        });
4066
4067        if (mobile) {
4068            var wrap = content.parentElement || content;
4069            placeOnce('upcomingElectionsAdUnderListMobile', function () {
4070                return makeSingle('upcomingElectionsAdUnderListMobile', {
4071                    mobileOnly: true,
4072                    format: 'auto',
4073                    extraClass: 'pollsmax-ad-single--poll-mid-mobile'
4074                });
4075            }, function (el) { insertAfter(wrap, el); });
4076            registerAllShells();
4077            return;
4078        }
4079
4080        removeShell(document.getElementById('upcomingElectionsAdUnderListMobile'), true);
4081
4082        var allBlocks = content.querySelectorAll('.upcoming-elections-page__day');
4083        var blocks = [];
4084        var bi;
4085        for (bi = 0; bi < allBlocks.length; bi++) {
4086            if (allBlocks[bi].hidden || allBlocks[bi].style.display === 'none') continue;
4087            blocks.push(allBlocks[bi]);
4088        }
4089
4090        var slots = infeedOrd.map(function (n) {
4091            return { id: 'upcomingElectionsAdBeforeDate' + n, index: n - 1 };
4092        });
4093        slots.forEach(function (slot) {
4094            var block = blocks[slot.index];
4095            if (!block || !block.parentNode) {
4096                removeShell(document.getElementById(slot.id), true);
4097                return;
4098            }
4099            placeOnce(slot.id, function () {
4100                return makeRow(slot.id, {
4101                    extraClass: 'pollsmax-ad-row--page-top pollsmax-ad-row--page-top-wide pollsmax-ad-row--upcoming-elections-infeed',
4102                    format: 'rectangle'
4103                });
4104            }, function (el) {
4105                if (el.parentNode !== block.parentNode || el.nextElementSibling !== block) {
4106                    block.parentNode.insertBefore(el, block);
4107                }
4108                el.setAttribute('data-ad-state', 'live');
4109                el.removeAttribute('data-pollsmax-ad-empty');
4110            });
4111        });
4112        registerAllShells();
4113        syncTopAdContentBounds();
4114        requestAnimationFrame(syncTopAdContentBounds);
4115        setTimeout(syncTopAdContentBounds, 120);
4116    }
4117
4118    function place2028PrimaryAds(kind) {
4119        if (!kind || !kind.primary2028) return;
4120        var mobile = isMobile();
4121        if (mobile) {
4122            /* Under chart box, above the polls table. */
4123            placeBeforePollsTableMobile('primary2028AdAfterChartMobile');
4124        } else {
4125            /* PC: same 2-up horizontal above the polls table as race polling pages. */
4126            removeShell(document.getElementById('primary2028AdAfterChart'), true);
4127            removeShell(document.getElementById('primary2028AdAfterTable'), true);
4128            placeAbovePollTable('primary2028AdBeforeTable');
4129            scheduleMidContentAdGapSync();
4130        }
4131        registerAllShells();
4132    }
4133
4134
4135    /* ---------- Boot ---------- */
4136
4137    function scrubWrongSlots() {
4138        var bad = document.querySelectorAll(
4139            'ins.adsbygoogle[data-ad-slot="5773578144"],' +
4140            'ins.adsbygoogle[data-ad-slot="8152742160"]'
4141        );
4142        var i;
4143        for (i = 0; i < bad.length; i++) {
4144            var shell = bad[i].closest(
4145                '.pollsmax-ad-banner, .pollsmax-ad-row, .pollsmax-ad-single, aside.pollsmax-site-multiplex, .pollsmax-home-ad, #home-after-opening-maps'
4146            );
4147            if (shell && shell.parentNode &&
4148                !shell.querySelector('ins.adsbygoogle[data-pollsmax-initialized="true"]')) {
4149                shell.parentNode.removeChild(shell);
4150            }
4151        }
4152    }
4153
4154    /** Undo logo→Donate breakout on hub chart ads (they sit beside the races list). */
4155    function scrubHubTallBreakout() {
4156        var hubs = document.querySelectorAll('.pollsmax-ad-banner--hub-tall');
4157        var i;
4158        for (i = 0; i < hubs.length; i++) {
4159            var el = hubs[i];
4160            el.classList.remove('pollsmax-ad--header-width');
4161            el.style.marginLeft = '';
4162            el.style.marginRight = '';
4163            el.style.width = '';
4164            el.style.maxWidth = '';
4165            el.style.paddingLeft = '';
4166            el.style.paddingRight = '';
4167            el.style.left = '';
4168            el.style.right = '';
4169            el.style.position = '';
4170        }
4171    }
4172
4173    /** PC: strip ads that aren't page-top duals (incl. mid page-top rows) or multiplex. */
4174    function scrubDesktopExtraAds() {
4175        if (isMobile()) return;
4176        var shells = document.querySelectorAll(
4177            '.pollsmax-ad-banner, .pollsmax-ad-row, .pollsmax-ad-single, aside.pollsmax-site-multiplex, #home-after-opening-maps'
4178        );
4179        var i;
4180        for (i = 0; i < shells.length; i++) {
4181            var el = shells[i];
4182            if (!el || !el.parentNode) continue;
4183            if (el.id === 'pollsmaxPageTopAdDesktop') continue;
4184            if (el.classList && el.classList.contains('pollsmax-ad-row--page-top')) continue;
4185            if (el.classList && el.classList.contains('pollsmax-site-multiplex')) continue;
4186            if (el.id === 'homeAdMultiplex' || el.id === 'pollsmaxSiteMultiplex') continue;
4187            removeShell(el, true);
4188        }
4189    }
4190
4191    function placeShellsOnly() {
4192        if (isEmbed()) return;
4193        ensureAdsense();
4194        scrubWrongSlots();
4195        /* Desktop: under-header dual + Multiplex first; mid duals after mux push. */
4196        if (!isMobile()) {
4197            var deskKind = pageKind();
4198            scrubDesktopExtraAds();
4199            placePageTopAds();
4200            claimPageTopAdsNow();
4201            ensureSiteMultiplex();
4202
4203            function placeDesktopMidContentAds() {
4204                placeHomeDesktopDualRows(deskKind);
4205                placeLatestPollsAds(deskKind);
4206                placeUpcomingElectionsAds(deskKind);
4207                if (deskKind.gePoll || deskKind.primaryPoll) ET.placeRacePollingPageAds();
4208                if (deskKind.forecast) ET.placeRaceForecastPageAds();
4209                if (deskKind.approval) placeApprovalAds(deskKind);
4210                if (deskKind.primary2028) place2028PrimaryAds(deskKind);
4211                ensureSiteMultiplex();
4212                var mux = document.querySelector('aside.pollsmax-site-multiplex');
4213                if (mux) {
4214                    mux.setAttribute('data-ad-state', 'live');
4215                    mux.removeAttribute('data-pollsmax-ad-empty');
4216                    clearShellLayoutInline(mux);
4217                }
4218                registerAllShells();
4219                syncTopAdContentBounds();
4220                scheduleMidContentAdGapSync();
4221            }
4222
4223            /*
4224             * Wait for Multiplex push (or ~2.5s) before mounting mid duals so
4225             * AdSense does not burn the page quota on mids and leave the footer empty.
4226             */
4227            var muxWaitStart = Date.now();
4228            function waitMuxThenMids() {
4229                if (!multiplexPushPending() || Date.now() - muxWaitStart >= 2500) {
4230                    placeDesktopMidContentAds();
4231                    return;
4232                }
4233                setTimeout(waitMuxThenMids, 100);
4234            }
4235            waitMuxThenMids();
4236            return;
4237        }
4238        scrubHubTallBreakout();
4239        scrubMobileUnderHeroAds();
4240        scrubPrimaryPollMobileBarChartAboveTableAd();
4241        var kind = pageKind();
4242        placeHomeDesktopDualRows(kind);
4243        placePageTopAds();
4244        placeHomeAfterOpeningMaps(kind);
4245        placeHomeAds(kind);
4246        placeHubAds(kind);
4247        if (kind.gePoll || kind.primaryPoll) ET.placeRacePollingPageAds();
4248        if (kind.forecast) ET.placeRaceForecastPageAds();
4249        ejectMidAdsFromPanels();
4250        placeApprovalAds(kind);
4251        place2028PrimaryAds(kind);
4252        placeLatestPollsAds(kind);
4253        placeUpcomingElectionsAds(kind);
4254        scrubMobileUnderHeroAds();
4255        scrubPrimaryPollMobileBarChartAboveTableAd();
4256        ensureSiteMultiplex();
4257        registerAllShells();
4258        syncTopAdContentBounds();
4259        applyMockMobileAds();
4260        scheduleMidAdPanelOuterWidthSync();
4261        schedulePrimaryPollMobileBarChartAdScrub();
4262    }
4263
4264    /* Alias kept for race/hub hooks. */
4265    function placeAll() {
4266        placeShellsOnly();
4267    }
4268
4269    function boot() {
4270        placeShellsOnly();
4271        watchForecastReadyForMultiplex();
4272        /* Re-place latest-polls in-feed duals after the date list paints / filters. */
4273        document.addEventListener('pollsmax-all-polls-rendered', function () {
4274            try {
4275                placeLatestPollsAds(pageKind());
4276                claimPageTopAdsNow();
4277                syncTopAdContentBounds();
4278            } catch (eLp) { /* ignore */ }
4279        });
4280        document.addEventListener('pollsmax-upcoming-elections-rendered', function () {
4281            try {
4282                placeUpcomingElectionsAds(pageKind());
4283                claimPageTopAdsNow();
4284                syncTopAdContentBounds();
4285            } catch (eUe) { /* ignore */ }
4286        });
4287        function replaceHubMidsAfterCharts() {
4288            try {
4289                var k = pageKind();
4290                if (!(k.senateHub || k.govHub || k.houseHub) || !isMobile()) return;
4291                placeHubAds(k);
4292                applyMockMobileAds();
4293                scheduleMidAdPanelOuterWidthSync();
4294            } catch (eHubCharts) { /* ignore */ }
4295        }
4296        document.addEventListener('pollsmax-hub-simulation-charts-drawn', replaceHubMidsAfterCharts);
4297        document.addEventListener('pollsmax-house-hub-charts-mounted', replaceHubMidsAfterCharts);
4298        document.addEventListener('pollsmax-hub-charts-ready', replaceHubMidsAfterCharts);
4299        /* Limited retries for late-mounted forecast/hub charts / races — not a long spam loop.
4300         * Hub mobile: shorter schedule so map zoom / races aren't fighting 8s of re-place. */
4301        var kindBoot = pageKind();
4302        var retries = (isMobile() && (kindBoot.houseHub || kindBoot.senateHub || kindBoot.govHub))
4303            ? [500, 2200]
4304            : [400, 1600, 4000, 8000];
4305        retries.forEach(function (ms) {
4306            setTimeout(placeShellsOnly, ms);
4307        });
4308        global.addEventListener('pagehide', function () {
4309            if (lazyObserver && typeof lazyObserver.disconnect === 'function') {
4310                try { lazyObserver.disconnect(); } catch (e) { /* ignore */ }
4311            }
4312        }, { once: true });
4313    }
4314
4315    if (document.readyState === 'loading') {
4316        document.addEventListener('DOMContentLoaded', boot, { once: true });
4317    } else {
4318        boot();
4319    }
4320    global.addEventListener('load', function () {
4321        placeShellsOnly();
4322        syncTopAdHeaderBounds();
4323        scheduleMidAdPanelOuterWidthSync();
4324    }, { once: true });
4325    global.addEventListener('resize', function () {
4326        if (placePageTopAds._resizeT) clearTimeout(placePageTopAds._resizeT);
4327        placePageTopAds._resizeT = setTimeout(function () {
4328            syncTopAdHeaderBounds();
4329            syncMidAdPanelOuterWidths();
4330        }, 80);
4331    });
4332
4333    global.PollsmaxPageAds = {
4334        placeAll: placeAll,
4335        initAds: initAds,
4336        registerAllShells: registerAllShells,
4337        initializeAdElement: initializeAdElement,
4338        ensureSiteMultiplex: ensureSiteMultiplex,
4339        syncTopAdContentBounds: syncTopAdContentBounds,
4340        syncHeaderWidthAds: syncHeaderWidthAds,
4341        fitPageTopCreatives: fitPageTopCreatives
4342    };
4343})(typeof window !== 'undefined' ? window : this);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.