PageSourceSearch

https://www.pollsmax.com/assets/forecast-hub-map-ui.js?v=cd-sec-sync-1

js pollsmax.com collected 2026-09-28 06:37:33 UTC 45,306 bytes, 974 lines download raw bytes

1/**
2 * Senate / Governor forecast hub — bar caption, map key, Show Flips / More Detail.
3 * Requires ET from shared.js. Not used on the site home page maps.
4 */
5(function () {
6    'use strict';
7    var ET = window.ET;
8    if (!ET) return;
9
10    function $(id) { return document.getElementById(id); }
11
12    function syncFlipsToggleRow(checkboxId, rowId, on) {
13        var cb = $(checkboxId);
14        var row = rowId ? $(rowId) : null;
15        if (cb) cb.checked = !!on;
16        if (row) row.classList.toggle('eh-flips-toggle-row--off', !on);
17    }
18
19    var FORECAST_HUB_KEY_RANGE_POLLING = {
20        solid: '> 15%',
21        likely: '5 - 15%',
22        lean: '1 - 5%',
23        tilt: '< 1%'
24    };
25    var FORECAST_HUB_KEY_RANGE_RATINGS = {
26        solid: '>95%',
27        likely: '80 - 95%',
28        lean: '65 - 80%',
29        tilt: '50 - 65%'
30    };
31
32    function isHubKeyRangeCompactMobile() {
33        return window.matchMedia('(max-width: 767px)').matches;
34    }
35
36    function formatHubKeyRangeLabel(text) {
37        if (!text || !isHubKeyRangeCompactMobile()) return text;
38        return String(text).replace(/\s+-\s+/g, '-');
39    }
40
41    function syncLegendKeyRangeLabels(key, ratingsMode) {
42        if (!key) return;
43        var ranges = ratingsMode ? FORECAST_HUB_KEY_RANGE_RATINGS : FORECAST_HUB_KEY_RANGE_POLLING;
44        ['solid', 'likely', 'lean', 'tilt'].forEach(function (tier) {
45            var col = key.querySelector('.forecast-hub-key-col[data-hub-col="' + tier + '"]');
46            if (!col) return;
47            var rangeEl = col.querySelector('.forecast-hub-key-range');
48            if (rangeEl) rangeEl.textContent = formatHubKeyRangeLabel(ranges[tier]);
49        });
50    }
51
52    if (!window._hubKeyRangeResizeBound) {
53        window._hubKeyRangeResizeBound = true;
54        window.addEventListener('resize', function () {
55            document.querySelectorAll('.forecast-hub-map-key').forEach(function (key) {
56                syncLegendKeyRangeLabels(key, key.classList.contains('forecast-hub-map-key--ratings'));
57            });
58        });
59    }
60
61    function syncLegendKey(root, showFlips, showMargins, mapSource) {
62        if (!root) return;
63        var key = root.querySelector('.forecast-hub-map-key');
64        if (!key) return;
65        var ratingsMode = mapSource === 'ratings';
66        key.classList.toggle('forecast-hub-map-key--margins', !!showMargins);
67        key.classList.toggle('forecast-hub-map-key--flips', !!showFlips);
68        key.classList.toggle('forecast-hub-map-key--ratings', ratingsMode);
69        syncLegendKeyRangeLabels(key, ratingsMode);
70    }
71
72    ET.syncForecastHubMapLegendKey = syncLegendKey;
73
74    function syncMapSourceToggle(rowId, mapSource) {
75        var row = rowId ? $(rowId) : null;
76        if (!row) return;
77        var buttons = row.querySelectorAll('[data-map-source]');
78        buttons.forEach(function (btn) {
79            var active = btn.getAttribute('data-map-source') === mapSource;
80            btn.classList.toggle('forecast-hub-map-source__btn--active', active);
81            btn.setAttribute('aria-pressed', active ? 'true' : 'false');
82        });
83        if (typeof ET.syncHubMobileRacesMapSource === 'function') {
84            ET.syncHubMobileRacesMapSource(rowId, mapSource);
85        }
86    }
87
88    function hubAssetUrl(path) {
89        if (typeof window.pollsmaxHubAssetUrl === 'function') return window.pollsmaxHubAssetUrl(path);
90        return path + (path.indexOf('?') >= 0 ? '&' : '?') + 'v=' + (window.POLLSMAX_HUB_UI_REV || '70');
91    }
92
93    function hubHeaderLogoLeft() {
94        var logo = document.querySelector('#mainHeader .sleek-home-header-logo, #mainHeader img[alt*="POLLSMAX"]');
95        return logo ? logo.getBoundingClientRect().left : 0;
96    }
97
98    function bleedLeftFromLogo(hostEl) {
99        if (!hostEl) return '0px';
100        return (hubHeaderLogoLeft() - hostEl.getBoundingClientRect().left) + 'px';
101    }
102
103    function syncForecastHubSearchRuleFromAside() {
104        var searchWrap = document.querySelector('.senate-search-polls-rule-wrap, .gov-search-polls-rule-wrap');
105        var aside = document.querySelector('main .senate-races-aside, main .gov-hub-aside.senate-races-aside');
106        if (!searchWrap) return;
107        var prop = searchWrap.classList.contains('gov-search-polls-rule-wrap')
108            ? '--gov-search-rule-left'
109            : '--senate-search-rule-left';
110        if (window.matchMedia('(min-width: 1024px)').matches && aside) {
111            var ar = aside.getBoundingClientRect();
112            var wr = searchWrap.getBoundingClientRect();
113            /* Aside frame border-right is the column vline (box extends through former +15px gutter). */
114            var vlineLeft = ar.right - 1;
115            var sectionRuleLeft = null;
116            searchWrap.style.setProperty(prop, (vlineLeft - wr.left) + 'px');
117            if (document.body.classList.contains('house-hub-page')) {
118                var grid = forecastHubPageGrid();
119                var buckets = document.getElementById('houseForecastBucketsSection');
120                var rightCol = document.querySelector('.senate-right-content');
121                sectionRuleLeft = rightCol ? (vlineLeft - rightCol.getBoundingClientRect().left) : null;
122                if (grid) {
123                    var gridLeft = grid.getBoundingClientRect().left;
124                    searchWrap.style.setProperty('--house-search-rule-left-bleed', (gridLeft - wr.left) + 'px');
125                }
126                if (sectionRuleLeft != null && isFinite(sectionRuleLeft)) {
127                    var sectionRulePx = Math.round(sectionRuleLeft) + 'px';
128                    if (rightCol) rightCol.style.setProperty('--house-hub-section-rule-left', sectionRulePx);
129                    if (buckets) buckets.style.setProperty('--house-hub-section-rule-left', sectionRulePx);
130                }
131                document.body.classList.add('house-hub-search-rule-synced');
132            } else {
133                searchWrap.style.removeProperty('--house-search-rule-left-bleed');
134                document.body.classList.remove('house-hub-search-rule-synced');
135            }
136            var houseOdds = document.getElementById('houseHubSimulationChartsSection');
137            if (houseOdds) {
138                if (document.body.classList.contains('house-hub-page') && sectionRuleLeft != null && isFinite(sectionRuleLeft)) {
139                    houseOdds.style.setProperty('--house-hub-section-rule-left', Math.round(sectionRuleLeft) + 'px');
140                } else {
141                    houseOdds.style.setProperty('--house-hub-section-rule-left', (vlineLeft - houseOdds.getBoundingClientRect().left) + 'px');
142                }
143            }
144        } else {
145            searchWrap.style.setProperty(prop, '0px');
146            searchWrap.style.removeProperty('--house-search-rule-left-bleed');
147            document.body.classList.remove('house-hub-search-rule-synced');
148            var houseOddsMob = document.getElementById('houseHubSimulationChartsSection');
149            if (houseOddsMob) houseOddsMob.style.setProperty('--house-hub-section-rule-left', '0px');
150        }
151        if (document.body.classList.contains('house-hub-page') && typeof ET.syncHouseAsideScrollHeight === 'function') {
152            ET.syncHouseAsideScrollHeight();
153        }
154        if (document.body.classList.contains('house-hub-page') && typeof ET.syncHouseHubColumnVline === 'function') {
155            ET.syncHouseHubColumnVline();
156        }
157        if (document.body.classList.contains('house-hub-page') && typeof ET.syncHouseHubSearchRuleLayout === 'function') {
158            ET.syncHouseHubSearchRuleLayout();
159        }
160        if (document.body.classList.contains('house-hub-page') && typeof ET.syncHouseHubMapZoomLayout === 'function') {
161            ET.syncHouseHubMapZoomLayout();
162        }
163    }
164
165    function forecastHubPageGrid() {
166        return document.querySelector('main .forecast-hub-page-grid');
167    }
168
169    /** Snapshot collapse-arrow Y and flag-column X while the aside is visible (document scroll, not viewport-fixed). */
170    function captureAsideToggleAnchor() {
171        var grid = forecastHubPageGrid();
172        var chevron = document.querySelector(
173            '.forecast-hub-aside-collapse-toggle--collapse .forecast-hub-aside-collapse-chevron'
174        );
175        var flag = document.querySelector('.senate-races-aside .senate-race-flag');
176        var aside = document.querySelector('main .senate-races-aside, main .gov-hub-aside.senate-races-aside');
177        if (!grid || !chevron) return false;
178
179        var gridRect = grid.getBoundingClientRect();
180        var chRect = chevron.getBoundingClientRect();
181        var top = (chRect.top + chRect.height / 2) - gridRect.top + grid.scrollTop;
182        var left;
183
184        if (flag) {
185            left = flag.getBoundingClientRect().left - gridRect.left + grid.scrollLeft;
186        } else if (aside) {
187            var asideRect = aside.getBoundingClientRect();
188            left = asideRect.left - gridRect.left + grid.scrollLeft;
189        } else {
190            left = 0;
191        }
192
193        grid.style.setProperty('--forecast-hub-aside-toggle-top', top + 'px');
194        grid.style.setProperty('--forecast-hub-aside-toggle-left', left + 'px');
195        return true;
196    }
197
198    function applyAsideToggleAnchor() {
199        var grid = forecastHubPageGrid();
200        if (!grid) return;
201        /* vars already on grid from captureAsideToggleAnchor */
202    }
203
204    function syncForecastHubAsideTogglePosition() {
205        if (document.body.classList.contains('forecast-hub-races-aside-collapsed')) {
206            applyAsideToggleAnchor();
207        } else {
208            captureAsideToggleAnchor();
209        }
210    }
211
212    /** Match Search polls / map rules: aside vertical line, or logo-left full bleed when race list hidden. */
213    function syncForecastHubCollapsedMapRule(shell, ruleEl, divider) {
214        if (!shell || !ruleEl || !divider) return;
215        if (window.matchMedia('(min-width: 1024px)').matches) {
216            if (document.body.classList.contains('forecast-hub-races-aside-collapsed')) {
217                divider.style.removeProperty('--forecast-hub-map-below-rule-left');
218                return;
219            }
220            var aside = document.querySelector('main .senate-races-aside, main .gov-hub-aside.senate-races-aside');
221            if (aside) {
222                var ar = aside.getBoundingClientRect();
223                var rr = ruleEl.getBoundingClientRect();
224                /* Aside frame border-right is the column vline (box extends through former +15px gutter). */
225            var vlineLeft = ar.right - 1;
226                divider.style.setProperty('--forecast-hub-map-below-rule-left', (vlineLeft - rr.left) + 'px');
227                return;
228            }
229        }
230        divider.style.setProperty('--forecast-hub-map-below-rule-left', '0px');
231    }
232
233    /**
234     * Race list hidden (lg+): align hero identity + more blurb to the seat bar rail above the map.
235     * Otherwise clear vars (default full-width head).
236     */
237    function syncForecastHubHeadSeatLabelAlign() {
238        var root = document.documentElement;
239        var lg = window.matchMedia('(min-width: 1024px)').matches;
240        var collapsed = document.body.classList.contains('forecast-hub-races-aside-collapsed');
241        var isHub = document.body.classList.contains('senate-hub-page') ||
242            document.body.classList.contains('gov-hub-page') ||
243            document.body.classList.contains('house-hub-page');
244
245        if (!lg || !isHub) {
246            root.style.removeProperty('--forecast-hub-head-content-left');
247            root.style.removeProperty('--forecast-hub-head-content-width');
248            return;
249        }
250
251        if (!collapsed) {
252            root.style.removeProperty('--forecast-hub-head-content-left');
253            root.style.removeProperty('--forecast-hub-head-content-width');
254            return;
255        }
256
257        if (document.body.classList.contains('senate-hub-page') ||
258            document.body.classList.contains('gov-hub-page')) {
259            root.style.removeProperty('--forecast-hub-head-content-left');
260            root.style.removeProperty('--forecast-hub-head-content-width');
261            requestAnimationFrame(syncForecastHubCollapsedHeadlineWrap);
262            return;
263        }
264
265        var seatBar = document.getElementById('senateHubSeatBar') ||
266            document.getElementById('govHubSeatBar') ||
267            document.getElementById('houseHubSeatBar');
268        var headText = document.querySelector('.forecast-hub-page-head .forecast-hub-page-head-text');
269        if (!seatBar || !headText) {
270            root.style.removeProperty('--forecast-hub-head-content-left');
271            root.style.removeProperty('--forecast-hub-head-content-width');
272            return;
273        }
274
275        var barRail = seatBar.querySelector('.forecast-hub-bar-rail') ||
276            seatBar.querySelector('.pollsmax-hub-seat-bar-rail');
277        if (!barRail) {
278            requestAnimationFrame(syncForecastHubHeadSeatLabelAlign);
279            return;
280        }
281
282        var barRect = barRail.getBoundingClientRect();
283        if (barRect.width < 1) {
284            requestAnimationFrame(syncForecastHubHeadSeatLabelAlign);
285            return;
286        }
287
288        var refRect = headText.getBoundingClientRect();
289        var left = Math.max(0, Math.round(barRect.left - refRect.left));
290        var width = Math.max(0, Math.round(barRect.width));
291        root.style.setProperty('--forecast-hub-head-content-left', left + 'px');
292        root.style.setProperty('--forecast-hub-head-content-width', width + 'px');
293        requestAnimationFrame(syncForecastHubCollapsedHeadlineWrap);
294    }
295
296    var syncForecastHubCollapsedHeadAlign = syncForecastHubHeadSeatLabelAlign;
297
298    function syncForecastHubCollapsedHeadlineWrap() {
299        if (typeof ET.syncStateRaceHeadlineWrap === 'function') {
300            requestAnimationFrame(function () {
301                if (document.getElementById('forecastHubRotatingHeadline')) {
302                    ET.syncStateRaceHeadlineWrap('#forecastHubRotatingHeadline');
303                }
304                if (document.getElementById('forecastGovHubRotatingHeadline')) {
305                    ET.syncStateRaceHeadlineWrap('#forecastGovHubRotatingHeadline');
306                }
307            });
308        }
309        requestAnimationFrame(syncHouseHubForecastTitleFit);
310    }
311
312    function resetHouseHubForecastTitleFit() {
313        document.querySelectorAll('[data-forecast-hub-head="house"] .rf-head-state').forEach(function (h1) {
314            h1.style.removeProperty('max-width');
315            h1.style.removeProperty('white-space');
316            h1.classList.remove('rf-head-state--wrap');
317        });
318        document.querySelectorAll('[data-forecast-hub-head="house"] .rf-head-identity').forEach(function (el) {
319            el.classList.remove('rf-head-identity--title-fit');
320        });
321    }
322
323    function syncHouseHubForecastTitleFit() {
324        var lg = window.matchMedia('(min-width: 1024px)').matches;
325        var collapsed = document.body.classList.contains('forecast-hub-races-aside-collapsed');
326        if (!document.body.classList.contains('house-hub-page') || !lg || !collapsed) {
327            resetHouseHubForecastTitleFit();
328            return;
329        }
330
331        var h1 = document.querySelector('[data-forecast-hub-head="house"] .rf-head-state');
332        var countdownBox = document.querySelector('[data-forecast-hub-head="house"] .forecast-hub-midterm-countdown__boxes') ||
333            document.querySelector('[data-forecast-hub-head="house"] .forecast-hub-head-countdown-slot');
334        var identity = document.querySelector('[data-forecast-hub-head="house"] .rf-head-identity');
335        if (!h1 || !countdownBox || !identity) {
336            resetHouseHubForecastTitleFit();
337            return;
338        }
339
340        identity.classList.add('rf-head-identity--title-fit');
341        h1.classList.remove('rf-head-state--wrap');
342        h1.style.whiteSpace = 'nowrap';
343        h1.style.maxWidth = '';
344
345        var h1Rect = h1.getBoundingClientRect();
346        var cdRect = countdownBox.getBoundingClientRect();
347        var maxW = Math.floor(cdRect.left - h1Rect.left - 5);
348        if (maxW < 48) {
349            h1.style.whiteSpace = '';
350            h1.style.maxWidth = '';
351            h1.classList.add('rf-head-state--wrap');
352            return;
353        }
354
355        h1.style.maxWidth = maxW + 'px';
356        if (h1.scrollWidth > maxW + 1) {
357            h1.style.whiteSpace = '';
358            h1.classList.add('rf-head-state--wrap');
359        }
360    }
361
362    ET.syncHouseHubForecastTitleFit = syncHouseHubForecastTitleFit;
363
364    ET.syncForecastHubHeadSeatLabelAlign = syncForecastHubHeadSeatLabelAlign;
365    ET.syncForecastHubCollapsedHeadAlign = syncForecastHubCollapsedHeadAlign;
366    ET.syncForecastHubCollapsedHeadlineWrap = syncForecastHubCollapsedHeadlineWrap;
367
368    ET.scheduleForecastHubHeadSeatLabelAlign = function () {
369        requestAnimationFrame(function () {
370            syncForecastHubHeadSeatLabelAlign();
371            requestAnimationFrame(function () {
372                syncForecastHubHeadSeatLabelAlign();
373                syncForecastHubCollapsedHeadlineWrap();
374            });
375        });
376    };
377
378    ET.syncForecastHubHubRules = function () {
379        syncForecastHubAsideTogglePosition();
380        syncForecastHubHeadSeatLabelAlign();
381        syncForecastHubCollapsedHeadlineWrap();
382        var collapsed = document.body.classList.contains('forecast-hub-races-aside-collapsed');
383        var lg = window.matchMedia('(min-width: 1024px)').matches;
384
385        if (!collapsed || !lg) {
386            document.querySelectorAll('.senate-map-divider, .senate-right-content > section.senate-right-divider-inset').forEach(function (el) {
387                el.style.removeProperty('--forecast-hub-rule-bleed-left');
388            });
389            var searchWrap = document.querySelector('.senate-search-polls-rule-wrap, .gov-search-polls-rule-wrap');
390            if (searchWrap) {
391                searchWrap.style.removeProperty('--senate-search-rule-left');
392                searchWrap.style.removeProperty('--gov-search-rule-left');
393                searchWrap.style.removeProperty('--house-search-rule-left-bleed');
394            }
395            syncForecastHubSearchRuleFromAside();
396            document.querySelectorAll('.forecast-hub-map-legend-shell--collapsed').forEach(function (shell) {
397                var rule = shell.querySelector('.forecast-hub-map-below-rule');
398                var divider = shell.closest('.senate-map-divider');
399                if (rule && divider) syncForecastHubCollapsedMapRule(shell, rule, divider);
400            });
401            return;
402        }
403
404        document.querySelectorAll('.senate-map-divider, .senate-right-content > section.senate-right-divider-inset').forEach(function (el) {
405            el.style.removeProperty('--forecast-hub-rule-bleed-left');
406        });
407        var searchWrapCollapsed = document.querySelector('.senate-search-polls-rule-wrap, .gov-search-polls-rule-wrap');
408        if (searchWrapCollapsed) {
409            searchWrapCollapsed.style.removeProperty('--senate-search-rule-left');
410            searchWrapCollapsed.style.removeProperty('--gov-search-rule-left');
411            searchWrapCollapsed.style.removeProperty('--house-search-rule-left-bleed');
412        }
413        document.querySelectorAll('.forecast-hub-map-legend-shell--collapsed').forEach(function (shell) {
414            var rule = shell.querySelector('.forecast-hub-map-below-rule');
415            var divider = shell.closest('.senate-map-divider');
416            if (rule && divider) syncForecastHubCollapsedMapRule(shell, rule, divider);
417        });
418        if (document.body.classList.contains('house-hub-page') && typeof ET.syncHouseHubSearchRuleLayout === 'function') {
419            ET.syncHouseHubSearchRuleLayout();
420        }
421    };
422
423    function clearForecastHubCollapsedMapRule(divider) {
424        if (!divider) return;
425        divider.style.removeProperty('--forecast-hub-map-below-rule-left');
426    }
427
428    function ensureHubMapUiCss() {
429        if (!document.getElementById('pollsmax-toggle-css')) {
430            var toggleLink = document.createElement('link');
431            toggleLink.id = 'pollsmax-toggle-css';
432            toggleLink.rel = 'stylesheet';
433            toggleLink.href = '/assets/pollsmax-toggle.css?v=4';
434            document.head.appendChild(toggleLink);
435        }
436        if (!document.getElementById('forecast-hub-map-ui-css')) {
437            var cssPath = '/assets/forecast-hub-map-ui.css';
438            var link = document.createElement('link');
439            link.id = 'forecast-hub-map-ui-css';
440            link.rel = 'stylesheet';
441            link.href = hubAssetUrl(cssPath);
442            document.head.appendChild(link);
443        }
444        /* Keep full-bleed hide after hub CSS so mobile overrides stick. */
445        var hideId = 'pm-mobile-hide-fullbleed-rules';
446        var hide = document.getElementById(hideId);
447        if (!hide) {
448            hide = document.createElement('link');
449            hide.id = hideId;
450            hide.rel = 'stylesheet';
451            hide.href = '/assets/pm-mobile-hide-fullbleed-rules.css?v=3';
452        }
453        document.head.appendChild(hide);
454    }
455
456    ET.initForecastHubMapUi = function (opts) {
457        opts = opts || {};
458        var svgId = opts.svgId;
459        if (!svgId) return;
460
461        ensureHubMapUiCss();
462
463        var ui = ET.getForecastHubMapUi(svgId);
464        if (opts.captionId) {
465            var count = opts.kind === 'gov'
466                ? (typeof ET.countGovSeatsOnBallot === 'function' ? ET.countGovSeatsOnBallot() : 0)
467                : (typeof ET.countSenateSeatsOnBallot === 'function' ? ET.countSenateSeatsOnBallot() : 0);
468            var suffix = opts.captionSuffix || (opts.kind === 'gov'
469                ? (count === 1 ? ' governorship up for election' : ' governorships up for election')
470                : ' seats up for election');
471            if (typeof ET.setForecastHubBarCaption === 'function') {
472                ET.setForecastHubBarCaption(opts.captionId, count, suffix
472);
473            }
474        }
475
476        var toolbar = opts.toolbarId ? $(opts.toolbarId) : null;
477
478        function bindLegendCollapse() {
479            var shell = toolbar && toolbar.closest ? toolbar.closest('.forecast-hub-map-legend-shell') : null;
480            if (!shell) return;
481            var toggle = shell.querySelector('.forecast-hub-map-legend-toggle');
482            if (!toggle || shell.getAttribute('data-hub-legend-collapse-bound')) return;
483            shell.setAttribute('data-hub-legend-collapse-bound', '1');
484
485            var divider = shell.closest('.senate-map-divider');
486            var rule = shell.querySelector('.forecast-hub-map-below-rule');
487
488            function getSectionsBelowMap() {
489                var list = [];
490                if (!divider) return list;
491                var parent = divider.parentElement;
492                if (!parent) return list;
493                var sib = divider.nextElementSibling;
494                while (sib && parent.contains(sib)) {
495                    if (sib.classList && sib.classList.contains('senate-right-divider-inset')) {
496                        list.push(sib);
497                    }
498                    sib = sib.nextElementSibling;
499                }
500                return list;
501            }
502
503            /** Y of the 1px line above the key (rule bottom). */
504            function ruleLineY() {
505                if (!rule) return 0;
506                return rule.getBoundingClientRect().bottom;
507            }
508
509            /** Pull sections up until the top of the first one meets the rule line. */
510            function measureCollapseShift() {
511                var sections = getSectionsBelowMap();
512                if (!sections.length || !rule) return 0;
513                var targetY = ruleLineY();
514                var controlTop = sections[0].getBoundingClientRect().top;
515                var shift = controlTop - targetY;
516                return shift > 0 ? Math.ceil(shift) : 0;
517            }
518
519            function clearSectionShift() {
520                getSectionsBelowMap().forEach(function (sec) {
521                    sec.classList.remove('forecast-hub-legend-collapse-shifted');
522                    sec.classList.remove('forecast-hub-legend-collapse-shifted--lead');
523                    sec.style.removeProperty('--forecast-hub-legend-collapse-shift');
524                });
525            }
526
527            function applyCollapseShift() {
528                var sections = getSectionsBelowMap();
529                if (!sections.length) return;
530                var shiftPx = measureCollapseShift() + 'px';
531                sections.forEach(function (sec, i) {
532                    if (i === 0) {
533                        sec.classList.add('forecast-hub-legend-collapse-shifted');
534                        sec.classList.add('forecast-hub-legend-collapse-shifted--lead');
535                        sec.style.setProperty('--forecast-hub-legend-collapse-shift', shiftPx);
536                    } else {
537                        sec.classList.remove('forecast-hub-legend-collapse-shifted');
538                        sec.classList.remove('forecast-hub-legend-collapse-shifted--lead');
539                        sec.style.removeProperty('--forecast-hub-legend-collapse-shift');
540                    }
541                });
542            }
543
544            function setCollapsed(collapsed) {
545                shell.classList.toggle('forecast-hub-map-legend-shell--collapsed', collapsed);
546                if (divider) {
547                    divider.classList.toggle('forecast-hub-map-divider--legend-collapsed', collapsed);
548                }
549                if (!collapsed) {
550                    clearForecastHubCollapsedMapRule(divider);
551                    clearSectionShift();
552                } else {
553                    syncForecastHubCollapsedMapRule(shell, rule, divider);
554                    void shell.offsetHeight;
555                    applyCollapseShift();
556                }
557                toggle.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
558                toggle.setAttribute('aria-label', collapsed ? 'Show map legend and controls' : 'Hide map legend and controls');
559            }
560
561            if (!shell._hubLegendResizeBound) {
562                shell._hubLegendResizeBound = true;
563                window.addEventListener('resize', function () {
564                    if (!shell.classList.contains('forecast-hub-map-legend-shell--collapsed')) return;
565                    syncForecastHubCollapsedMapRule(shell, rule, divider);
566                    applyCollapseShift();
567                    if (typeof ET.syncForecastHubHubRules === 'function') ET.syncForecastHubHubRules();
568                });
569            }
570
571            toggle.addEventListener('click', function () {
572                setCollapsed(!shell.classList.contains('forecast-hub-map-legend-shell--collapsed'));
573                if (typeof ET.syncForecastHubHubRules === 'function') ET.syncForecastHubHubRules();
574            });
575        }
576
577        bindLegendCollapse();
578        bindRacesAsideCollapse();
579        initForecastHubMidtermCountdown();
580        var urlMapSource = typeof ET.consumeHubMapSourceFromUrl === 'function'
581            ? ET.consumeHubMapSourceFromUrl()
582            : null;
583        if (urlMapSource) {
584            ui.mapSource = urlMapSource;
585        } else if (!ui.mapSource) {
586            ui.mapSource = typeof ET.forecastHubDefaultMapSource === 'function'
587                ? ET.forecastHubDefaultMapSource(svgId)
588                : 'polling';
589        }
590        if (opts.kind === 'house' && ui.mapSource === 'polling') {
591            ui.mapSource = 'ratings';
592        }
593        syncLegendKey(toolbar, ui.showFlips, ui.showMargins, ui.mapSource);
594        syncMapSourceToggle(opts.mapSourceRowId, ui.mapSource);
595        syncFlipsToggleRow(opts.showFlipsId, opts.flipsRowId, ui.showFlips);
596        syncFlipsToggleRow(opts.showMarginsId, opts.marginsRowId, ui.showMargins);
597
598        function applyAll() {
599            var u = ET.getForecastHubMapUi(svgId);
600            syncLegendKey(toolbar, u.showFlips, u.showMargins, u.mapSource);
601            syncMapSourceToggle(opts.mapSourceRowId, u.mapSource);
602            syncFlipsToggleRow(opts.showFlipsId, opts.flipsRowId, u.showFlips);
603            syncFlipsToggleRow(opts.showMarginsId, opts.marginsRowId, u.showMargins);
604            if (opts.kind === 'house') {
605                if (typeof window.PollsmaxEnhanceHouseHubApplyUi === 'function') {
606                    window.PollsmaxEnhanceHouseHubApplyUi(u, { mapToggle: true });
607                } else if (typeof ET.refreshHouseCdMap === 'function') {
608                    ET.refreshHouseCdMap(svgId);
609                } else if (opts.barIds && typeof ET.refreshHouseHubSeatBar === 'function') {
610                    ET.refreshHouseHubSeatBar(opts.barIds, svgId);
611                }
612            } else {
613                if (opts.barIds) {
614                    if (typeof ET.refreshForecastHubSeatBar === 'function') {
615                        ET.refreshForecastHubSeatBar(opts.barIds, opts.kind, svgId);
616                    } else if (typeof ET.syncForecastHubSeatBarFlips === 'function') {
617                        ET.syncForecastHubSeatBarFlips(opts.barIds, u.showFlips);
618                    }
619                }
620                if (opts.kind === 'gov' && typeof ET.refreshForecastHubGovMap === 'function') {
621                    ET.refreshForecastHubGovMap(svgId);
622                } else if (typeof ET.refreshForecastHubSenateMap === 'function') {
623                    ET.refreshForecastHubSenateMap(svgId);
624                }
625            }
626            if (typeof ET.refreshForecastHubAsideRaces === 'function') {
627                ET.refreshForecastHubAsideRaces(svgId);
628            }
629            /* Chamber-hub enhance listens for this to re-paint prerendered maps/bars. */
630            try {
631                document.dispatchEvent(new CustomEvent('pollsmax-forecast-hub-map-ui-changed', {
632                    detail: { svgId: svgId, kind: opts.kind, ui: u }
633                }));
634            } catch (eEv) { /* ignore */ }
635        }
636
637        if (opts.mapSourceRowId) {
638            var mapRow = $(opts.mapSourceRowId);
639            if (mapRow && !mapRow.getAttribute('data-hub-map-source-bound')) {
640                mapRow.setAttribute('data-hub-map-source-bound', '1');
641                mapRow.querySelectorAll('[data-map-source]').forEach(function (btn) {
642                    btn.addEventListener('click', function () {
643                        var src = btn.getAttribute('data-map-source');
644                        if (src !== 'polling' && src !== 'ratings') return;
645                        if (opts.kind === 'house' && src === 'polling') return;
646                        ET.getForecastHubMapUi(svgId).mapSource = src;
647                        applyAll();
648                    });
649                });
650                if (typeof ET.initForecastHubSourceTips === 'function') {
651                    ET.initForecastHubSourceTips(mapRow);
652                }
653            }
654        }
655
656        if (opts.showFlipsId) {
657            var flipsCb = $(opts.showFlipsId);
658            if (flipsCb && !flipsCb.getAttribute('data-hub-bound')) {
659                flipsCb.setAttribute('data-hub-bound', '1');
660                flipsCb.addEventListener('change', function () {
661                    ET.getForecastHubMapUi(svgId).showFlips = flipsCb.checked;
662                    applyAll();
663                });
664            }
665        }
666
667        if (opts.showMarginsId) {
668            var margCb = $(opts.showMarginsId);
669            if (margCb && !margCb.getAttribute('data-hub-bound')) {
670                margCb.setAttribute('data-hub-bound', '1');
671                margCb.addEventListener('change', function () {
672                    ET.getForecastHubMapUi(svgId).showMargins = margCb.checked;
673                    applyAll();
674                });
675            }
676        }
677
678        applyAll();
679        if (typeof ET.initHubMobileRaces === 'function') {
680            ET.initHubMobileRaces();
681        }
682    };
683
684    /** Nov 3, 2026 12:00 AM Eastern — hub map header countdown. */
685    var HUB_MIDTERM_COUNTDOWN_TARGET_MS = new Date('2026-11-03T00:00:00-05:00').getTime();
686    var _hubMidtermCountdownTimer = null;
687
688    function padHubCountdown2(n) {
689        n = Math.max(0, Math.floor(n));
690        return n < 10 ? '0' + n : String(n);
691    }
692
693    function hubMidtermCountdownParts(nowMs, targetMs) {
694        if (targetMs <= nowMs) {
695            return { months: 0, days: 0, hours: 0, minutes: 0, seconds: 0 };
696        }
697        var now = new Date(nowMs);
698        var target = new Date(targetMs);
699        var months = 0;
700        var cursor = new Date(now.getTime());
701        while (true) {
702            var next = new Date(cursor.getFullYear(), cursor.getMonth() + 1, cursor.getDate(),
703                cursor.getHours(), cursor.getMinutes(), cursor.getSeconds());
704            if (next.getTime() > target.getTime()) break;
705            months++;
706            cursor = next;
707        }
708        var remain = target.getTime() - cursor.getTime();
709        var days = Math.floor(remain / 86400000);
710        remain -= days * 86400000;
711        var hours = Math.floor(remain / 3600000);
712        remain -= hours * 3600000;
713        var minutes = Math.floor(remain / 60000);
714        remain -= minutes * 60000;
715        var seconds = Math.floor(remain / 1000);
716        return { months: months, days: days, hours: hours, minutes: minutes, seconds: seconds };
717    }
718
719    function renderHubMidtermCountdown(root) {
720        if (!root) return;
721        var parts = hubMidtermCountdownParts(Date.now(), HUB_MIDTERM_COUNTDOWN_TARGET_MS);
722        ['months', 'days', 'hours', 'minutes', 'seconds'].forEach(function (key) {
723            var el = root.querySelector('[data-hub-cd="' + key + '"]');
724            if (el) el.textContent = padHubCountdown2(parts[key]);
725        });
726    }
727
728    function syncHubMidtermCountdownLayout() {
729        if (typeof ET.syncForecastHubHeroCountdownBoxSize === 'function') {
730            ET.syncForecastHubHeroCountdownBoxSize();
731        }
732        if (typeof ET.syncForecastHubMapHeadSlotAlign === 'function') {
733            ET.syncForecastHubMapHeadSlotAlign();
734        } else {
735            document.querySelectorAll('.forecast-hub-map-head-area').forEach(function (area) {
736                var eyebrow = area.querySelector('.forecast-hub-map-eyebrow');
737                var title = area.querySelector('.forecast-hub-map-title');
738                var slot = area.querySelector('.forecast-hub-map-head-slot');
739                var cd = area.querySelector('[data-forecast-hub-midterm-countdown]');
740                if (!title || !slot) return;
741                if (typeof ET.isMobileLayout === 'function' && ET.isMobileLayout()) {
742                    slot.style.top = '';
743                    return;
744                }
745                var tr = title.getBoundingClientRect();
746                var ar = area.getBoundingClientRect();
747                var centerY = tr.top + tr.height / 2;
748                if (eyebrow) {
749                    var er = eyebrow.getBoundingClientRect();
750                    centerY = (er.top + tr.bottom) / 2;
751                    var size = Math.max(28, Math.round(tr.bottom - er.top));
752                    if (cd && !cd.classList.contains('forecast-hub-midterm-countdown--hidden')) {
753                        cd.style.setProperty('--hub-cd-box-size', size + 'px');
754                    }
755                }
756                slot.style.top = (centerY - ar.top) + 'px';
757            });
758        }
759        if (typeof syncHouseHubForecastTitleFit === 'function') {
760            syncHouseHubForecastTitleFit();
761        }
762    }
763
764    function syncHubHeroCountdownHoverAria(cd, open) {
765        if (!cd) return;
766        var hover = cd.querySelector('.forecast-hub-midterm-countdown__hover');
767        if (hover) hover.setAttribute('aria-hidden', open ? 'false' : 'true');
768    }
769
770    function closeHubHeroCountdownHovers(except) {
771        document.querySelectorAll('.forecast-hub-page-head--forecast-hero .forecast-hub-midterm-countdown--hero.forecast-hub-midterm-countdown--hover-open').forEach(function (cd) {
772            if (cd === except) return;
773            cd.classList.remove('forecast-hub-midterm-countdown--hover-open');
774            syncHubHeroCountdownHoverAria(cd, false);
775        });
776    }
777
778    function bindHubHeroCountdownTap() {
779        if (window._hubHeroCountdownTapBound) return;
780        window._hubHeroCountdownTapBound = true;
781
782        function isSenGovHubMobile() {
783            return typeof ET.isSenGovHubMobilePage === 'function' && ET.isSenGovHubMobilePage();
784        }
785
786        document.addEventListener('click', function (e) {
787            if (!isSenGovHubMobile()) {
788                closeHubHeroCountdownHovers(null);
789                return;
790            }
791
792            var box = e.target.closest('.forecast-hub-page-head--forecast-hero .forecast-hub-midterm-countdown--hero .forecast-hub-midterm-countdown__box');
793            if (box) {
794                var cd = box.closest('.forecast-hub-midterm-countdown--hero');
795                if (!cd) return;
796                var open = !cd.classList.contains('forecast-hub-midterm-countdown--hover-open');
797                closeHubHeroCountdownHovers(null);
798                if (open) {
799                    cd.classList.add('forecast-hub-midterm-countdown--hover-open');
800                    syncHubHeroCountdownHoverAria(cd, true);
801                } else {
802                    cd.classList.remove('forecast-hub-midterm-countdown--hover-open');
803                    syncHubHeroCountdownHoverAria(cd, false);
804                }
805                return;
806            }
807
808            if (!e.target.closest('.forecast-hub-page-head--forecast-hero .forecast-hub-midterm-countdown--hero')) {
809                closeHubHeroCountdownHovers(null);
810            }
811        });
812    }
813
814    function initForecastHubMidtermCountdown() {
815        if (typeof ET.initForecastHubMidtermCountdown === 'function') {
816            ET.initForecastHubMidtermCountdown();
817            syncHubMidtermCountdownLayout();
818            bindHubHeroCountdownTap();
819            return;
820        }
821        function tick() {
822            document.querySelectorAll('[data-forecast-hub-midterm-countdown]').forEach(renderHubMidtermCountdown);
823        }
824        tick();
825        syncHubMidtermCountdownLayout();
826        bindHubHeroCountdownTap();
827        if (_hubMidtermCountdownTimer != null) {
828            clearInterval(_hubMidtermCountdownTimer);
829            _hubMidtermCountdownTimer = null;
830        }
831        if (typeof ET.onAlignedSecondTick === 'function') {
832            ET.onAlignedSecondTick(tick);
833        } else {
834            _hubMidtermCountdownTimer = setInterval(tick, 1000);
835        }
836        if (!window._hubMidtermCountdownResizeBound) {
837            window._hubMidtermCountdownResizeBound = true;
838            window.addEventListener('resize', function () {
839                syncHubMidtermCountdownLayout();
840                if (typeof ET.isSenGovHubMobilePage === 'function' && !ET.isSe
840nGovHubMobilePage()) {
841                    closeHubHeroCountdownHovers(null);
842                }
843            });
844            window.addEventListener('load', syncHubMidtermCountdownLayout);
845        }
846    }
847
848    ET.syncHubMidtermCountdownLayout = syncHubMidtermCountdownLayout;
849
850    function bindRacesAsideCollapse() {
851        var collapseBtn = document.querySelector('.forecast-hub-aside-collapse-toggle--collapse');
852        var expandBtn = document.querySelector('.forecast-hub-aside-collapse-toggle--expand');
853        var expandRow = document.querySelector('.forecast-hub-aside-expand-row');
854        if (!collapseBtn || document.body.getAttribute('data-hub-aside-collapse-bound')) return;
855        document.body.setAttribute('data-hub-aside-collapse-bound', '1');
856
857        function syncAria(collapsed) {
858            collapseBtn.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
859            collapseBtn.setAttribute('aria-label', collapsed ? 'Show race list' : 'Hide race list');
860            if (expandBtn) {
861                expandBtn.setAttribute('aria-expanded', collapsed ? 'true' : 'false');
862                expandBtn.setAttribute('aria-label', collapsed ? 'Show race list' : 'Hide race list');
863            }
864            if (expandRow) expandRow.setAttribute('aria-hidden', collapsed ? 'false' : 'true');
865        }
866
867        function setCollapsed(collapsed, opts) {
868            opts = opts || {};
869            if (opts.fromUser) {
870                if (collapsed) {
871                    document.body.dataset.hubAsideUserCollapsed = '1';
872                } else {
873                    delete document.body.dataset.hubAsideUserCollapsed;
874                }
875            }
876            if (collapsed) captureAsideToggleAnchor();
877            document.body.classList.toggle('forecast-hub-races-aside-collapsed', collapsed);
878            syncAria(collapsed);
879            if (typeof ET.syncForecastHubHubRules === 'function') ET.syncForecastHubHubRules();
880            if (typeof ET.syncAllHubAsideVoteColVisibility === 'function') ET.syncAllHubAsideVoteColVisibility();
881            if (typeof ET.scheduleForecastHubHeadSeatLabelAlign === 'function') {
882                ET.scheduleForecastHubHeadSeatLabelAlign();
883            }
884            if (!collapsed) {
885                captureAsideToggleAnchor();
886                if (typeof ET.clearHubSearchPollsLayoutLocks === 'function') ET.clearHubSearchPollsLayoutLocks();
887                window.dispatchEvent(new Event('resize'));
888            } else if (typeof ET.refreshHubSearchPollsLayout === 'function') {
889                ET.refreshHubSearchPollsLayout();
890            }
891            if (opts.fromUser && typeof ET.syncHubAsideWidthCollapseForPage === 'function') {
892                ET.syncHubAsideWidthCollapseForPage();
893            }
894        }
895
896        ET._forecastHubSetAsideCollapsed = setCollapsed;
897        ET.finishForecastHubAsideInitialCollapse = function () { /* race list defaults to expanded */ };
898
899        function toggle() {
900            setCollapsed(!document.body.classList.contains('forecast-hub-races-aside-collapsed'), { fromUser: true });
901        }
902
903        collapseBtn.addEventListener('click', toggle);
904        if (expandBtn) expandBtn.addEventListener('click', toggle);
905
906        syncAria(document.body.classList.contains('forecast-hub-races-aside-collapsed'));
907        captureAsideToggleAnchor();
908        if (typeof ET.syncForecastHubHubRules === 'function') ET.syncForecastHubHubRules();
909        if (typeof ET.syncHubAsideWidthCollapseForPage === 'function') ET.syncHubAsideWidthCollapseForPage();
910        if (typeof ET.scheduleAllHubAsideVoteColVisibilitySync === 'function') {
911            ET.scheduleAllHubAsideVoteColVisibilitySync();
912        } else if (typeof ET.syncAllHubAsideVoteColVisibility === 'function') {
913            ET.syncAllHubAsideVoteColVisibility();
914        }
915
916        if (!collapseBtn._hubAsideResizeBound) {
917            collapseBtn._hubAsideResizeBound = true;
918            window.addEventListener('resize', function () {
919                if (!window.matchMedia('(min-width: 1024px)').matches) {
920                    if (document.body.classList.contains('forecast-hub-races-aside-collapsed')) {
921                        setCollapsed(false);
922                    }
923                    return;
924                }
925                if (!document.body.classList.contains('forecast-hub-races-aside-collapsed')) {
926                    captureAsideToggleAnchor();
927                }
928                if (typeof ET.syncForecastHubHubRules === 'function') ET.syncForecastHubHubRules();
929                if (typeof ET.syncAllHubAsideVoteColVisibility === 'function') ET.syncAllHubAsideVoteColVisibility();
930            });
931        }
932    }
933
934    function initForecastHubSourceTipsOnLoad() {
935        ET.initForecastHubSourceTips(document);
936    }
937
938    if (document.readyState === 'loading') {
939        document.addEventListener('DOMContentLoaded', initForecastHubMidtermCountdown);
940        document.addEventListener('DOMContentLoaded', initForecastHubSourceTipsOnLoad);
941    } else {
942        initForecastHubMidtermCountdown();
943        initForecastHubSourceTipsOnLoad();
944    }
945
946    function bootHubMobileRacesModule() {
947        if (typeof ET.scheduleHubMobileRacesInit === 'function') {
948            ET.scheduleHubMobileRacesInit();
949        } else if (typeof ET.initHubMobileRaces === 'function') {
950            ET.initHubMobileRaces();
951        }
952    }
953
954    (function loadHubMobileRacesModule() {
955        if (document.querySelector('script[data-hub-mobile-races-js], script[src*="forecast-hub-mobile-races.js"]')) {
956            return;
957        }
958        if (window.__hubMobileRacesScriptLoading) return;
959        window.__hubMobileRacesScriptLoading = true;
960        var src = hubAssetUrl('/assets/forecast-hub-mobile-races.js');
961        var s = document.createElement('script');
962        s.src = src;
963        s.setAttribute('data-hub-mobile-races-js', '1');
964        s.onload = function () {
965            window.__hubMobileRacesScriptLoading = false;
966            bootHubMobileRacesModule();
967        };
968        s.onerror = function () {
969            window.__hubMobileRacesScriptLoading = false;
970            if (console && console.warn) console.warn('[Pollsmax] forecast-hub-mobile-races.js failed to load');
971        };
972        document.head.appendChild(s);
973    })();
974})();

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.