<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <!-- THEME-PREPAINT: apply the cached theme's vars before first paint.
         :root defaults in src/index.css are dark, so Default Light users get
         a dark frame until ThemeContext loads. Lockstep with
         src/contexts/ThemeContext.tsx (cache key/shape, 1h TTL, resolution
         order, applyThemeVars — including the color-scheme inline style and
         the color-scheme / theme-color meta sync). Zero network, no writes,
         no-op on cold cache. -->
    <script>
      (function () {
        try {
          var parsed = JSON.parse(localStorage.getItem('tm_themes_cache_v1') || 'null');
          var themes = parsed && parsed.data;
          if (!Array.isArray(themes) || !themes.length) return;
          if (!(Date.now() - parsed.timestamp <= 3600000)) return;
          var pid = localStorage.getItem('preferred_theme');
          var theme = (pid && themes.filter(function (t) { return t && t.id === pid; })[0])
            || themes.filter(function (t) { return t && t.is_global_default; })[0]
            || themes[0];
          if (!theme || !Array.isArray(theme.vars)) return;
          var root = document.documentElement;
          var pageBg = '';
          theme.vars.forEach(function (v) {
            if (!v || !v.var_name) return;
            root.style.setProperty('--' + v.var_name, v.value);
            if (v.var_name === 'page-bg' && v.value) pageBg = String(v.value);
            var m = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.exec(String(v.value).trim());
            if (!m) return;
            var h = m[1].length === 3 ? m[1][0] + m[1][0] + m[1][1] + m[1][1] + m[1][2] + m[1][2] : m[1];
            root.style.setProperty('--' + v.var_name + '-rgb',
              parseInt(h.slice(0, 2), 16) + ', ' + parseInt(h.slice(2, 4), 16) + ', ' + parseInt(h.slice(4, 6), 16));
          });
          // Browser chrome (status bar, keyboard, native controls) follows the
          // resolved theme — same 'Default Light' name test and page-bg
          // theme-color as applyThemeVars in src/contexts/ThemeContext.tsx.
          // This script runs before the static metas below are parsed, so the
          // copies appended here land first in tree order and win.
          var isLight = theme.name === 'Default Light';
          root.style.colorScheme = isLight ? 'light' : 'dark';
          function setChromeMeta(name, content) {
            if (!content) return;
            var el = document.querySelector('meta[name="' + name + '"]');
            if (!el) {
              el = document.createElement('meta');
              el.setAttribute('name', name);
              document.head.appendChild(el);
            }
            el.setAttribute('content', content);
          }
          setChromeMeta('color-scheme', isLight ? 'light' : 'dark');
          setChromeMeta('theme-color', pageBg);
        } catch (e) { /* cold/broken cache → today's dark first paint, unchanged */ }
      })();
    </script>
    <!-- Favicon: matches the black-bordered app icon (PWA v2 icon set). The
         .ico (16/32/48) is the tab icon browsers prefer; the 32px PNG is a
         HiDPI hint. Both are derived from the same source as icon-512-v2.png. -->
    <link rel="icon" href="/favicon.ico" sizes="any" />
    <link rel="icon" type="image/png" sizes="32x32" href="/favicon.png" />
    <!-- PWA: web app manifest + installability metadata (P2a) -->
    <link rel="manifest" href="/manifest.webmanifest" />
    <meta name="theme-color" content="#0a0a0a" />
    <!-- color-scheme:dark makes browser-native UI (incl. the FedCM Google One
         Tap card) render dark to match the site; see matching :root rule in
         src/index.css. -->
    <meta name="color-scheme" content="dark" />
    <link rel="apple-touch-icon" href="/apple-touch-icon-v2.png" />
    <meta name="mobile-web-app-capable" content="yes" />
    <meta name="apple-mobile-web-app-capable" content="yes" />
    <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
    <meta name="apple-mobile-web-app-title" content="True Mainstream" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />

    <!-- Primary Meta + OG/Twitter block — A21 PRERENDER SWAP SPAN.
         The whole span between TM_META:BEGIN/END is replaced per-route:
         at the edge for crawlers (functions/_lib/og-shell-route.ts) and at
         build time for static routes (scripts/prerender-static.mjs). Real
         users get this default span unchanged; the SPA's useDocumentMeta
         keeps managing title/OG client-side as before. -->
    <!-- TM_META:BEGIN site-default head (replaced per-route by the A21 prerender shell) -->
    <title>True Mainstream: The Truth Unfiltered</title>
    <meta name="description" content="Real news from 1,600+ newsrooms worldwide, in English, around the clock. New headlines appear while you sleep, because the middle of the night where you are is the middle of the day somewhere else. We count newsrooms, not NGOs or press releases. No algorithm. No bias filter. Truth unfiltered." />
    <meta name="keywords" content="unbiased news, curated news aggregator, real-time news, breaking news, conservative news, liberal news, independent news, news without bias" />
    <meta name="author" content="True Mainstream" />
    <meta name="robots" content="index, follow" />
    <link rel="canonical" href="https://truemainstream.com/" />

    <!-- Open Graph / Facebook -->
    <meta property="og:type" content="website" />
    <meta property="og:url" content="https://truemainstream.com/" />
    <meta property="og:title" content="True Mainstream: The Truth Unfiltered" />
    <meta property="og:description" content="Real news from 1,600+ newsrooms worldwide, in English, around the clock. New headlines appear while you sleep, because the middle of the night where you are is the middle of the day somewhere else. We count newsrooms, not NGOs or press releases." />
    <meta property="og:image" content="https://truemainstream.com/og-card.png?v=20260901" />
    <meta property="og:image:width" content="1200" />
    <meta property="og:image:height" content="630" />
    <meta property="og:image:alt" content="True Mainstream — The Truth Unfiltered" />
    <meta property="og:site_name" content="True Mainstream" />
    <meta property="og:locale" content="en_US" />

    <!-- Twitter / X -->
    <meta name="twitter:card" content="summary_large_image" />
    <meta name="twitter:site" content="@TrueMainstream" />
    <meta name="twitter:title" content="True Mainstream: The Truth Unfiltered" />
    <meta name="twitter:description" content="Real news from 1,600+ newsrooms worldwide, in English, 24/7. New headlines appear while you sleep. We count newsrooms, not NGOs or press releases." />
    <meta name="twitter:image" content="https://truemainstream.com/og-card.png?v=20260901" />
    <!-- TM_META:END -->

    <!-- Structured Data: WebSite -->
    <script type="application/ld+json">
    {
      "@context": "https://schema.org",
      "@type": "WebSite",
      "name": "True Mainstream",
      "alternateName": "TrueMainstream",
      "url": "https://truemainstream.com",
      "description": "Real news from 1,600+ newsrooms worldwide, in English, around the clock. We count newsrooms, not NGOs or press releases.",
      "potentialAction": {
        "@type": "SearchAction",
        "target": {
          "@type": "EntryPoint",
          "urlTemplate": "https://truemainstream.com/?q={search_term_string}"
        },
        "query-input": "required name=search_term_string"
      }
    }
    </script>

    <!-- Structured Data: Organization -->
    <script type="application/ld+json">
    {
      "@context": "https://schema.org",
      "@type": "Organization",
      "name": "True Mainstream",
      "url": "https://truemainstream.com",
      "logo": "https://truemainstream.com/true-mainstream-logo.png",
      "description": "An independent news aggregator providing curated, unbiased news from diverse sources."
    }
    </script>

    <!-- DNS prefetch + preconnect for faster Supabase connection -->
    <link rel="dns-prefetch" href="https://ejodhtotxozbilnctety.supabase.co">
    <link rel="preconnect" href="https://ejodhtotxozbilnctety.supabase.co" crossorigin>
    <!-- Static snapshot-kv (hero.json + core.json + img-bytes) — first-byte LCP path -->
    <link rel="dns-prefetch" href="https://archive.truemainstream.com">
    <link rel="preconnect" href="https://archive.truemainstream.com" crossorigin>
    <link rel="dns-prefetch" href="https://snapshot-kv.truemainstream.workers.dev">
    <link rel="preconnect" href="https://snapshot-kv.truemainstream.workers.dev" crossorigin>
    <!-- Preconnect to Cloudflare Worker origin (param-keyed snapshot reads) -->
    <link rel="dns-prefetch" href="https://news-snapshot-worker.truemainstream.workers.dev">
    <link rel="preconnect" href="https://news-snapshot-worker.truemainstream.workers.dev" crossorigin>

    <!--
      Kick off the free-tier snapshot fetch BEFORE the JS bundle parses.
      useLiveFeed picks up window.__TM_SNAPSHOT_PROMISE__ if present so
      War Room can paint hero + first island as soon as the JSON lands,
      not after React, TanStack Query, and the supabase client warm up.

      STAGED BOOT (G1-E + G1-remain + hero-first + BOOT-NO-WAIT): byte 1
      starts ONLY hero.json (~23KB). Fold-image bytes start the moment
      hero.json lands. The promise handed to React (__TM_SNAPSHOT_PROMISE__)
      resolves with that hero slice UNCONDITIONALLY so the LCP card
      can render without waiting for core.json (~1.5MB gzip). core.json
      starts the same tick hero.json lands ('hero-land') and rides
      alongside the photo download; the full payload hydrates via
      __TM_CORE_PROMISE__ / tm-core-ready. hero-empty / hero-fail are
      the only other starts (failure paths). There is no paint gate,
      no text gate, and no fallback timer. Do NOT bring back a 2.5s
      "no text signal" timer (with live document TTFB it fired while
      JS was still arriving, so the 1.3MB parse landed on the
      first-paint critical path — elementRenderDelay ~2.5s → simulated
      LCP ~13s), and do NOT re-gate core.json on the fold image's
      paint (owner rejected that hold 2026-10-11: the photo download
      must never gate the file).

      STATIC EDGE CACHE (Task 1 / G1-A): default the static base so first visit
      (empty localStorage) starts hero.json on byte 1 — do not wait for
      /app-config.json. localStorage / app-config may override (persisted for
      the next visit). Fetch ONLY static core.json; start news-snapshot only
      on miss / stale / empty / timeout. Abort the edge fetch if static 200.
    -->
    <script>
      (function () {
        try {
          // STALE-FLASH FIX: wipe previous session's snapshot BEFORE fetch starts.
          // React reads sessionStorage synchronously on mount; if old data remains
          // it flashes stale headlines for ~1s until the fresh fetch resolves.
          try { sessionStorage.removeItem('tm_snapshot_v1'); } catch (e) { /* ignore */ }
          var SUPABASE_URL = 'https://ejodhtotxozbilnctety.supabase.co';
          var ANON = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImVqb2RodG90eG96YmlsbmN0ZXR5Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjE4OTUxNzUsImV4cCI6MjA3NzQ3MTE3NX0.kFqDd767PwqiippLYJR4pAhRR8xRwrjeuyghxIJVl5c';

          // Known production static base. First-visit LCP cannot wait on
          // /app-config.json. localStorage / app-config may override.
          // 2026-08-15: default + legacy migration OFF snapshot-kv...workers.dev
          // — some carrier networks can't reach *.workers.dev at all, so the
          // static base now lives on archive.truemainstream.com (same worker,
          // custom domain). A localStorage pin to the legacy host is wiped.
          var DEFAULT_STATIC_BASE = 'https://archive.truemainstream.com/v2/free';
          var STATIC_MAX_AGE_MS = 10 * 60 * 1000;
          var staticUrl = '';
          try { staticUrl = localStorage.getItem('tm_static_snapshot_url') || ''; } catch (e) { /* ignore */ }
          if (staticUrl.indexOf('snapshot-kv.truemainstream.workers.dev') !== -1) {
            staticUrl = '';
            try { localStorage.removeItem('tm_static_snapshot_url'); } catch (e) { /* ignore */ }
          }
          var base = staticUrl || DEFAULT_STATIC_BASE;

          function storeResult(json) {
            if (json) {
              try { sessionStorage.setItem('tm_snapshot_v1', JSON.stringify({ data: json, ts: Date.now() })); } catch (e) { /* quota */ }
            }
            return json;
          }

          function fetchFromEdge(externalSignal) {
            var timeout = AbortSignal.timeout(4000);
            var signal = timeout;
            if (externalSignal && typeof AbortSignal.any === 'function') {
              signal = AbortSignal.any([timeout, externalSignal]);
            }
            return fetch(
              SUPABASE_URL + '/functions/v1/news-snapshot?tier=free&_t=' + Date.now(),
              {
                headers: { Authorization: 'Bearer ' + ANON, apikey: ANON },
                // Was unbounded. A stalled edge fetch could hang the preload
                // (and therefore the snapshot handoff) indefinitely. 4s keeps
                // the edge safety net snappy on true static misses; the hero
                // text has already painted by the time this runs (G1-E).
                signal: signal
              }
            ).then(function (r) {
              if (!r.ok) return null;
              // Same one-body rule as the static path (WR-CORE-ONEFETCH):
              // whichever side of the race wins, the app's scanner must find
              // exactly one readable body, never a second transfer.
              window.__TM_CORE_RESPONSE__ = r.clone();
              return r.json();
            }).then(storeResult).catch(function () { return null; });
          }

          function fetchFromStatic(base) {
            // NO _t buster: maximize Smart CDN hit rate. Cache auto-invalidates on upload.
            // 10s ceiling (G1-E, was 4s): core.json is ~1.5MB gzip ≈ 7.5s on a
            // real throttled phone — a 4s timer aborted the BODY mid-download on
            // every healthy static HIT, falsely falling through to the edge fn.
            // It now starts after the text/image paint, so a longer ceiling
            // costs the LCP nothing. True hangs are still bounded; hero.json
            // has already painted the first screen by then.
            return fetch(base.replace(/\/$/, '') + '/core.json', {
              signal: AbortSignal.timeout(10000)
            })
              .then(function (r) {
                if (!r.ok) return null;
                // WR-CORE-ONEFETCH: r.json() consumes this body, and Firefox /
                // WebKit do not coalesce the app scanner's second core.json
                // fetch. Clone BEFORE the consume and publish in this same
                // turn (never across an await) — the scanner reads the shared
                // body instead of re-transferring the file. A non-ok response
                // exposes no body.
                window.__TM_CORE_RESPONSE__ = r.clone();
                return r.json();
              })
              .then(function (json) {
                if (!json) return null;
                // Freshness gate: if the publish cron stalled, fall through to edge.
                var genAt = typeof json.generated_at === 'string' ? new Date(json.generated_at).getTime() : 0;
                if (!genAt || (Date.now() - genAt) > STATIC_MAX_AGE_MS) return null; // stale → edge fallback
                var hasData = (json.articles && json.articles.length > 0) || (json.trending && json.trending.length > 0);
                return hasData ? storeResult(json) : null;
              })
              .catch(function () { return null; });
          }

          // Static-first (G1-A). Previously both static and news-snapshot fired
          // at once, so a healthy CDN HIT still downloaded a second ~2MB copy.
          // Now we start ONLY static core.json. news-snapshot starts only on
          // miss / stale / empty / timeout. If static later returns usable
          // data, any in-flight edge fetch is aborted.
          function raceStaticAndEdge(url) {
            return new Promise(function (resolve) {
              var settled = false;
              var edgeAbort = null;
              function finish(json) {
                if (settled) return;
                settled = true;
                if (edgeAbort) { try { edgeAbort.abort(); } catch (e) { /* ignore */ } }
                resolve(json || null);
              }
              function startEdge() {
                if (settled) return;
                if (typeof AbortController === 'function') edgeAbort = new AbortController();
                fetchFromEdge(edgeAbort ? edgeAbort.signal : null).then(function (json) {
                  finish(json);
                });
              }
              fetchFromStatic(url).then(function (json) {
                if (json) finish(json);
                else startEdge();
              });
            });
          }

          // A4c (LCP) — fetch a TINY hero slice (top trending + sourceCountryMap)
          // from the same static base, on byte 1 (G1-E: core.json is deferred
          // until the first text paint commits). The hero <img> URL lives inside
          // core.json (~1.6MB wire); this ~few-KB hero.json makes it available
          // as soon as it lands so we can (1) preload the hero image with
          // fetchpriority=high and (2) hand the slice to React
          // (window.__TM_HERO_PROMISE__) to paint the hero before core.json
          // starts downloading. Silent no-op on any failure → falls back to the
          // core.json path. Mirrors src/lib/heroSnapshot.ts pickLcpPreloadUrls +
          // imageUtils toCardImageUrl (keep in lockstep). Mobile LCP is the first
          // full-width card AFTER hero_banned + no-safe-image splice. GIFs /
          // Daily Maverick attachments / NYT 3× crops are not preloaded. Max two
          // preloads, media-gated so a phone does not fetch the desktop-only
          // href. Bytes start the moment hero.json lands (G1-remain);
          // the <img> itself still mounts only with React's first hero commit.
          function rewriteCardImg(raw) {
            try {
              var u = new URL(raw);
              var host = u.hostname;
              var path = u.pathname;
              if (host.indexOf('nyt.com') !== -1 || host.indexOf('nytimes') !== -1) {
                u.pathname = path
                  .replace(/-mediumSquareAt3X\./, '-mediumThreeByTwo440.')
                  .replace(/-superJumbo\./, '-articleLarge.')
                  .replace(/-threeByTwoLargeAt2X\./, '-mediumThreeByTwo440.')
                  .replace(/-videoSixteenByNineJumbo1600\./, '-mediumThreeByTwo440.')
                  .replace(/-jumbo\./, '-articleLarge.');
                return u.href;
              }
              if (host.indexOf('nbcnews.com') !== -1 || host.indexOf('s-nbcnews') !== -1) {
                u.pathname = path.replace(/\/t_fit[_-]?\d+w\//, '/t_fit-560w/');
                return u.href;
              }
              var w = u.searchParams.get('w');
              if (w && Number(w) > 800) u.searchParams.set('w', '800');
              var width = u.searchParams.get('width');
              if (width && Number(width) > 800) u.searchParams.set('width', '800');
              // Lockstep with src/lib/imageUtils.ts rewriteCardImageUrl: WP
              // originals without a size token get ?w=800. Without this the
              // boot preload is Foo.jpg and React's <img> is Foo.jpg?w=800 —
              // two GETs, preload never helps LCP, description steals it.
              var decodedPath;
              try { decodedPath = decodeURIComponent(path); } catch (e2) { decodedPath = path; }
              if (/\/wp-content\//i.test(decodedPath) && !u.searchParams.has('w') && !u.searchParams.has('width')) {
                u.searchParams.set('w', '800');
              }
              return u.href;
            } catch (e) { return raw; }
          }
          function cardHeroImg(url) {
            if (!url || typeof url !== 'string') return '';
            if (/feedpress\.me\/link\//i.test(url) || /pixel\./i.test(url) || /\.gif(\?|$)/i.test(url)) return '';
            var rewritten = rewriteCardImg(url);
            if (/\/attachments\//i.test(rewritten)) return '';
            if (/mediumSquareAt3X|superJumbo|threeByTwoLargeAt2X|videoSixteenByNineJumbo/i.test(rewritten)) return '';
            if (/t_fit[_-]?(1[5-9]\d{2}|[2-9]\d{3})w/i.test(rewritten)) return '';
            return rewritten;
          }
          // WR-SLIDE-0 — the event's FACE image: the baked fastest-member
          // slide0 image when present, else the headline image. Lockstep with
          // src/lib/heroSnapshot.ts faceHeroImageUrl + useSlideBuilder slide 0:
          // the preloaded URL must be the one the painted card actually shows.
          function faceImgUrl(ev) {
            if (!ev) return '';
            return (typeof ev.slide0_image_url === 'string' && ev.slide0_image_url)
              ? ev.slide0_image_url
              : ev.headline_image_url || '';
          }
          function heroFreshnessScore(ev, now) {
            var HOUR = 3600000;
            var score = typeof ev.headline_score === 'number' ? ev.headline_score : 0;
            var nsrc = (ev.sources && ev.sources.length) ? ev.sources.length : 1;
            var sourceBonus = Math.sqrt(Math.max(1, nsrc));
            var newest = ev.newest_article_at || ev.last_updated_at;
            var newestMs = newest ? Date.parse(newest) : NaN;
            var ageH = isFinite(newestMs) ? (now - newestMs) / HOUR : 999;
            var decay = ageH <= 6 ? 1 : ageH <= 24 ? Math.max(0.3, 1 - (ageH - 6) * 0.039) : Math.max(0.05, 0.3 - (ageH - 24) * 0.005);
            var firstMs = ev.first_seen_at ? Date.parse(ev.first_seen_at) : NaN;
            var lifeH = isFinite(firstMs) ? (now - firstMs) / HOUR : 0;
            var longevity = lifeH > 48 ? Math.max(0.15, 1 - (lifeH - 48) * 0.01) : lifeH > 24 ? Math.max(0.4, 1 - (lifeH - 24) * 0.025) : 1;
            return (score * decay + Math.max(0, 10 - ageH)) * longevity * sourceBonus;
          }
          function pickDesktopHeroUrl(trending) {
            for (var i = 0; i < trending.length; i++) {
              var d = cardHeroImg(faceImgUrl(trending[i]));
              if (!trending[i].hero_banned && d) return d;
            }
            var ev = trending[0];
            return ev ? cardHeroImg(faceImgUrl(ev)) : '';
          }
          var lastMobileRanked = [];
          function pickMobileLcpUrl(trending) {
            var now = Date.now();
            // Same admission filter as applySnapshotToWarRoom — do not preload
            // / paint an event the first React commit will drop.
            var usable = trending.filter(function (ev) {
              return ev && ev.article_ids && ev.article_ids.length >= 2
                && ev.sources && ev.sources.length >= 2;
            });
            if (usable.length === 0) usable = trending;
            var ranked = usable.slice().sort(function (a, b) {
              return heroFreshnessScore(b, now) - heroFreshnessScore(a, now);
            });
            if (ranked[0] && ranked[0].hero_banned) {
              for (var i = 1; i < ranked.length; i++) {
                if (!ranked[i].hero_banned) {
                  ranked.unshift(ranked.splice(i, 1)[0]);
                  break;
                }
              }
            }
            if (ranked[0] && !cardHeroImg(faceImgUrl(ranked[0]))) {
              for (var s = 1; s < ranked.length; s++) {
                if (cardHeroImg(faceImgUrl(ranked[s]))) {
                  ranked.unshift(ranked.splice(s, 1)[0]);
                  break;
                }
              }
            }
            lastMobileRanked = ranked;
            return ranked[0] ? cardHeroImg(faceImgUrl(ranked[0])) : '';
          }
          // Paint-gate follow-up: the image the fold card falls back to when
          // the #1 pick fails at runtime (publisher 404 — MobileStoryGrid
          // demotes the event and promotes this one). Preloading it keeps
          // the successor's paint off a fresh ~1-4s publisher round trip.
          function pickMobileSuccessorUrl() {
            var ranked = lastMobileRanked;
            if (!ranked || !ranked.length) return '';
            var first = ranked[0] ? cardHeroImg(faceImgUrl(ranked[0])) : '';
            for (var i = 1; i < ranked.length; i++) {
              var img = cardHeroImg(faceImgUrl(ranked[i]));
              if (img && img !== first) return img;
            }
            return '';
          }
          // Live 2026-08-17: #tm-boot-lcp downloaded a 1.1–2.6 MB JPost
          // original (h_5477 / w_8216) and Lighthouse billed 10–17 s LCP.
          // Publisher CDNs do not send CORS, so the browser cannot read
          // Content-Length itself. Our archive worker HEADs the URL (no
          // body) and returns {bytes}. Skip any URL that is unknown or
          // over 400 KB — same cap as MAX_IMAGE_BYTES — BEFORE preload
          // or <img src> starts the GET.
          var MAX_LCP_BYTES = 400 * 1024;
          function imgBytesEndpoint() {
            return (base || DEFAULT_STATIC_BASE).replace(/\/v2\/free\/?$/, '') + '/v2/free/img-bytes?u=';
          }
          function probeImgBytes(href) {
            return fetch(imgBytesEndpoint() + encodeURIComponent(href), { signal: AbortSignal.timeout(2000) })
              .then(function (r) { return r.ok ? r.json() : null; })
              .then(function (j) {
                var n = (!j || typeof j.bytes !== 'number' || !isFinite(j.bytes) || j.bytes < 0) ? null : j.bytes;
                try {
                  window.__TM_IMG_BYTE_CACHE__ = window.__TM_IMG_BYTE_CACHE__ || {};
                  window.__TM_IMG_BYTE_CACHE__[href] = n;
                } catch (e) { /* ignore */ }
                return n;
              })
              .catch(function () { return null; });
          }
          function seedBytesFromHero(json) {
            try {
              window.__TM_IMG_BYTE_CACHE__ = window.__TM_IMG_BYTE_CACHE__ || {};
              var list = (json && json.trending) || [];
              for (var i = 0; i < list.length; i++) {
              var ev = list[i];
              var href = cardHeroImg(ev && faceImgUrl(ev));
              var n = ev && ((typeof ev.slide0_image_url === 'string' && ev.slide0_image_url && typeof ev.slide0_image_bytes === 'number')
                ? ev.slide0_image_bytes
                : ev.headline_image_bytes);
                if (href && typeof n === 'number' && isFinite(n) && n >= 0) {
                  window.__TM_IMG_BYTE_CACHE__[href] = n;
                }
              }
            } catch (e) { /* ignore */ }
          }
          function cachedOkHref(href) {
            try {
              var c = window.__TM_IMG_BYTE_CACHE__;
              if (!c || typeof c[href] !== 'number') return false;
              return c[href] <= MAX_LCP_BYTES;
            } catch (e) { return false; }
          }
          function firstByteOkUrl(hrefs) {
            var seen = {};
            var list = [];
            for (var i = 0; i < hrefs.length; i++) {
              if (!hrefs[i] || seen[hrefs[i]]) continue;
              seen[hrefs[i]] = true;
              list.push(hrefs[i]);
            }
            // Use baked/cached sizes first — no extra RTT. Probe at most TWO
            // unknown URLs so size-checks cannot crowd the fold photo GET.
            for (var c = 0; c < list.length; c++) {
              if (cachedOkHref(list[c])) return Promise.resolve(list[c]);
            }
            var unknown = [];
            for (var u = 0; u < list.length && unknown.length < 2; u++) {
              var have = false;
              try { have = !!(window.__TM_IMG_BYTE_CACHE__ && list[u] in window.__TM_IMG_BYTE_CACHE__); } catch (e2) { have = false; }
              if (!have) unknown.push(list[u]);
            }
            if (unknown.length === 0) return Promise.resolve('');
            return Promise.all(unknown.map(function (href) {
              return probeImgBytes(href).then(function (n) { return { href: href, bytes: n }; });
            })).then(function (rows) {
              for (var r = 0; r < rows.length; r++) {
                if (rows[r].bytes !== null && rows[r].bytes <= MAX_LCP_BYTES) return rows[r].href;
              }
              return '';
            });
          }
          function markLcpImgStarted() {
            try { window.dispatchEvent(new Event('tm-lcp-img-started')); } catch (e) { /* ignore */ }
          }
          var preloadsInjected = false;
          function preloadHeroImage(json) {
            try {
              if (preloadsInjected) return;
              if (!json || !json.trending || !json.trending.length) return;
              preloadsInjected = true;
              pickMobileLcpUrl(json.trending);
              var desktop = pickDesktopHeroUrl(json.trending);
              var isDesktopViewport = true;
              try {
                isDesktopViewport = !window.matchMedia || window.matchMedia('(min-width: 768px)').matches;
              } catch (e) { /* keep desktop default */ }
              function addPreload(href, media, priority) {
                var link = document.createElement('link');
                link.rel = 'preload';
                link.as = 'image';
                link.setAttribute('fetchpriority', priority || 'high');
                if (media) link.setAttribute('media', media);
                link.href = href;
                document.head.appendChild(link);
                if ((priority || 'high') === 'high') markLcpImgStarted();
              }
              var ranked = lastMobileRanked || [];
              var candidates = [];
              for (var i = 0; i < ranked.length; i++) {
                var href = cardHeroImg(faceImgUrl(ranked[i]));
                if (href) candidates.push(href);
              }
              if (desktop) candidates.push(desktop);
              firstByteOkUrl(candidates).then(function (mobile) {
                // Unknown length or >400 KB → do NOT start a GET. That is
                // the 2.6 MB JPost live fail. React can still paint later.
                if (mobile && desktop && mobile !== desktop && isDesktopViewport) {
                  addPreload(mobile, '(max-width: 767px)');
                  addPreload(desktop, '(min-width: 768px)');
                  foldImgUrl = mobile;
                } else if (mobile) {
                  addPreload(mobile, '');
                  foldImgUrl = mobile;
                  if (!isDesktopViewport) {
                    var successor = pickMobileSuccessorUrl();
                    if (successor && successor !== foldImgUrl) {
                      probeImgBytes(successor).then(function (n) {
                        if (n !== null && n <= MAX_LCP_BYTES) {
                          addPreload(successor, '(max-width: 767px)', 'low');
                        }
                      });
                    }
                  }
                }
              });
            } catch (e) { /* preloads are an optimization — never fatal */ }
          }
          var heroJson = null;

          // G1-E — preconnect (DNS+TCP+TLS only, no image bytes) to the origins
          // the above-fold preloads will hit at text-paint time. Connection
          // setup is 2-3 RTT on 4G; paying it during the text-paint wait keeps
          // the image's critical path to a single request.
          function preconnectImageOrigins(json) {
            try {
              if (!json || !json.trending || !json.trending.length) return;
              var urls = [pickMobileLcpUrl(json.trending), pickDesktopHeroUrl(json.trending)];
              var seen = {};
              for (var i = 0; i < urls.length; i++) {
                if (!urls[i]) continue;
                var origin;
                try { origin = new URL(urls[i]).origin; } catch (e) { continue; }
                if (!origin || seen[origin]) continue;
                seen[origin] = true;
                var link = document.createElement('link');
                link.rel = 'preconnect';
                link.crossOrigin = 'anonymous';
                link.href = origin;
                document.head.appendChild(link);
              }
            } catch (e) { /* ignore */ }
          }
          function fetchHero(base) {
            if (!base) return;
            var heroUrl = base.replace(/\/$/, '') + '/hero.json';
            window.__TM_HERO_PROMISE__ = fetch(heroUrl, { signal: AbortSignal.timeout(4000) })
              .then(function (r) { return r.ok ? r.json() : null; })
              .then(function (json) {
                heroJson = json;
                if (!json || !json.trending || !json.trending.length) {
                  // No hero text will ever paint — start core.json now.
                  startCore('hero-empty');
                } else {
                  // HERO-FIRST: unblock React with the 8-event slice NOW.
                  // The LCP <img> must not wait for 1.3MB core.json (live
                  // #195 regression: elementRenderDelay 25ms → 2.5s+).
                  // BOOT-NO-WAIT: core.json starts the same tick React is
                  // unblocked — alongside the photo download, never after
                  // its paint.
                  resolveSnapshot(json);
                  startCore('hero-land');
                  seedBytesFromHero(json);
                  // Warm DNS/TCP/TLS to the above-fold image origins NOW — a
                  // handshake is not content, so this respects the text-first
                  // paint order. Saves ~3 RTT of the image's critical path.
                  preconnectImageOrigins(json);
                  // Fold-image bytes start immediately. Paint order is
                  // already text-first — no <img> exists until React
                  // commits. The old 250ms timer only delayed bytes.
                  preloadHeroImage(json);
                }
                return json;
              })
              .catch(function () { startCore('hero-fail'); return null; });
          }

          // ── STAGED BOOT (BOOT-NO-WAIT): core.json starts at hero-land —
          // the same tick resolveSnapshot unblocks React — and the fold
          // photo preloads in parallel, never gating the file.
          // hero-empty / hero-fail are the only other starts (failure
          // paths); there is no paint or text gate anymore.
          var coreStarted = false;
          var foldImgUrl = '';
          var snapshotResolved = false;
          var resolveSnapshotPromise;
          window.__TM_SNAPSHOT_PROMISE__ = new Promise(function (resolve) {
            resolveSnapshotPromise = resolve;
          });
          function resolveSnapshot(json) {
            if (snapshotResolved) return;
            snapshotResolved = true;
            // Keep the staged hero bytes readable after the promise is
            // consumed, so a late first-visit fetchSnapshot (useRegionData
            // after useLiveFeed deleted the promise, core not started yet)
            // cannot open its own 1.3MB core.json on the LCP path.
            if (json) window.__TM_STAGED_SNAPSHOT__ = json;
            resolveSnapshotPromise(json || null);
          }
          function startCore(reason) {
            if (coreStarted) return;
            coreStarted = true;
            window.__TM_CORE_START_REASON__ = reason;
            var p = raceStaticAndEdge(base);
            window.__TM_CORE_PROMISE__ = p;
            p.then(function (json) {
              resolveSnapshot(json || null);
              try {
                window.dispatchEvent(new CustomEvent('tm-core-ready', { detail: json || null }));
              } catch (e) { /* ignore */ }
            });
          }
          window.__TM_TEXT_PAINTED__ = function () {
            // React's text signal — no longer gates core.json (BOOT-NO-WAIT).
            // bootStage.ts still calls this; keep the preload retry.
            preloadHeroImage(heroJson); // no-op if fetchHero already injected
          };

          // Kick off immediately on the resolved static base. Do not wait
          // for /app-config.json — that waterfall is what held first-visit
          // hero.json hostage. ALWAYS refresh app-config in the background
          // (no-cache: force-cache pinned retired hosts on returning
          // visitors) and write the value through for the next visit.
          fetchHero(base);
          fetch('/app-config.json', { cache: 'no-cache' })
            .then(function (r) { return r.ok ? r.json() : null; })
            .then(function (cfg) {
              var url = cfg && typeof cfg.static_snapshot_url === 'string' ? cfg.static_snapshot_url : '';
              if (url) {
                try { localStorage.setItem('tm_static_snapshot_url', url); } catch (e) { /* ignore */ }
              }
            })
            .catch(function () { /* ignore */ });
        } catch (e) { /* ignore */ }
      })();
    </script>

    <!-- Preconnect to font origins so TCP handshake is ready -->
    <link rel="preconnect" href="https://fonts.googleapis.com" />
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />

    <!--
      Fonts loaded non-blocking with media="print" trick:
      - Browser downloads in background (won't block render)
      - onload switches media to 'all' so fonts apply immediately when ready
      - noscript fallback ensures fonts still load when JS is disabled
      - display=swap prevents invisible text during font swap
      - Primary fonts: Inter (body) + Outfit (headlines) only -- Roboto removed
    -->
    <link
      rel="stylesheet"
      href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&family=Outfit:wght@500;600&display=swap"
      media="print"
      onload="this.media='all'"
    />
    <noscript>
      <link
        rel="stylesheet"
        href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&family=Outfit:wght@500;600&display=swap"
      />
    </noscript>

    <!-- Optional theme fonts: loaded fully async, never block render.
         G1-E: injected at 3s, not byte 1 — these seven families (Roboto alone
         is ~37KB) are cosmetic theme variants, and on byte 1 they pulled real
         font bytes in front of the fold image on the 4G pipe. -->
    <script>
      (function () {
        setTimeout(function () {
          try {
            var l = document.createElement('link');
            l.rel = 'stylesheet';
            l.href = 'https://fonts.googleapis.com/css2?family=Crimson+Text:wght@400;600&family=Merriweather:wght@400;700&family=PT+Serif:wght@400;700&family=Libre+Baskerville:wght@400;700&family=Playfair+Display:wght@400;700&family=Lora:wght@400;700&family=Roboto:wght@400;500&display=swap';
            l.media = 'print';
            l.onload = function () { l.media = 'all'; };
            document.head.appendChild(l);
          } catch (e) { /* ignore */ }
        }, 3000);
      })();
    </script>

    <!--
      Google Analytics 4 (gtag.js) — Consent Mode v2, default-DENIED.

      GA4 uses cookies, so it is gated behind explicit consent (GDPR/PECR/CCPA).
      The inline block MUST run before the async gtag.js tag below so the default
      denied state is in place before the library reads it. We read prior consent
      synchronously here so a returning user who already accepted gets analytics
      on the very first page_view (no lost events); everyone else is denied until
      the <ConsentBanner/> React component calls `gtag('consent','update',...)`.

      send_page_view:false — this is an SPA; src/lib/analytics.ts trackPageView()
      owns every page_view (initial load + route changes) to avoid double-counting.
      ad_* stays permanently 'denied' (we run no ads); only analytics_storage is
      ever granted.

      NOTE: Cloudflare Web Analytics is NOT added here. truemainstream.com is
      proxied through Cloudflare with Web Analytics set to "enable, excluding
      visitor data in the EU", so CF auto-injects its (cookie-free) beacon at the
      edge. A manual beacon would double-track. See PR description for details.
    -->
    <script>
      (function () {
        window.dataLayer = window.dataLayer || [];
        function gtag() { dataLayer.push(arguments); }
        window.gtag = gtag;

        var prior = 'denied';
        try {
          // Grant analytics (no consent needed) for: returning users who
          // accepted, OR any visitor geo-located outside the EU/EEA/UK consent
          // region (cached by src/lib/geoConsent.ts). EU visitors with no prior
          // decision stay 'denied' until the <ConsentBanner/> is accepted.
          if (localStorage.getItem('tm_consent_v1') === 'accepted' ||
              localStorage.getItem('tm_region') === 'non-eu') {
            prior = 'granted';
          }
        } catch (e) { /* private mode / disabled storage */ }

        gtag('consent', 'default', {
          ad_storage: 'denied',
          ad_user_data: 'denied',
          ad_personalization: 'denied',
          analytics_storage: prior,
          functionality_storage: 'granted',
          security_storage: 'granted',
          wait_for_update: 500
        });
        gtag('js', new Date());
        gtag('config', 'G-GKM3VNW8DW', {
          send_page_view: false,
          anonymize_ip: true
        });
      })();
    </script>
    <!-- G1-E: the gtag.js LIBRARY (~100KB) is injected at 3s, not byte 1. The
         inline dataLayer above buffers consent defaults + config + any queued
         events, so nothing is lost — the library processes the queue when it
         arrives. On byte 1 it won the 4G pipe AND the main thread during the
         React mount window, pushing the LCP image back ~1.5s. -->
    <script>
      (function () {
        setTimeout(function () {
          try {
            var s = document.createElement('script');
            s.async = true;
            s.src = 'https://www.googletagmanager.com/gtag/js?id=G-GKM3VNW8DW';
            document.head.appendChild(s);
          } catch (e) { /* ignore */ }
        }, 3000);
      })();
    </script>
    <script type="module" crossorigin src="/assets/index-LgBtHUir.js"></script>
    <link rel="modulepreload" crossorigin href="/assets/vendor-misc-fNjih5lP.js">
    <link rel="modulepreload" crossorigin href="/assets/vendor-react-BhoMx3kk.js">
    <link rel="modulepreload" crossorigin href="/assets/vendor-query-DrtHtUmG.js">
    <link rel="modulepreload" crossorigin href="/assets/vendor-supabase-Bb0LW0xz.js">
    <link rel="modulepreload" crossorigin href="/assets/vendor-icons-17H-ZD-C.js">
    <link rel="stylesheet" crossorigin href="/assets/index-tf4mfo4t.css">
  </head>
  <body>
    <div id="root"></div>
    <!-- A21 prerender body injection point — crawlers get a <noscript> block
         with the real content (article/event/home top stories) here; real
         users' DOM is untouched (noscript is inert when scripting is on). -->
    <!-- TM_BODY:INJECT -->
  </body>
</html>
