/* RADAR1 — rails, info bar, ticker stacks (the L-shaped furniture) */
const DSr = window.Radar1DesignSystem_1f4661;
/* ================= SIDE RAIL — editorial card deck ================= */
/* abstract still-image placeholders (gradient washes, no illustration) */
const RAIL_ART = {
lunar: { base: "linear-gradient(150deg, #1A2436 0%, #0B1220 60%, #060C17 100%)", glow: "radial-gradient(circle at 72% 30%, rgba(203,211,222,0.32), rgba(203,211,222,0.05) 45%, transparent 62%)" },
heat: { base: "linear-gradient(150deg, #3A1420 0%, #1A0F1A 55%, #0A0A14 100%)", glow: "radial-gradient(circle at 30% 35%, rgba(255,140,0,0.4), rgba(228,3,46,0.12) 50%, transparent 68%)" },
harbor: { base: "linear-gradient(170deg, #0E2233 0%, #0A1524 55%, #071019 100%)", glow: "radial-gradient(ellipse at 50% 78%, rgba(31,168,224,0.28), transparent 60%)" },
community: { base: "linear-gradient(150deg, #2A2314 0%, #171507 55%, #0C0B06 100%)", glow: "radial-gradient(circle at 65% 40%, rgba(255,176,0,0.28), transparent 58%)" },
wildlife: { base: "linear-gradient(160deg, #10281E 0%, #0A1A14 55%, #060F0B 100%)", glow: "radial-gradient(circle at 40% 45%, rgba(23,178,106,0.26), transparent 60%)" },
};
const railInk = (accent) => (/cyan|amber|green/.test(accent) ? "#04121C" : "#fff");
function CardKicker({ text, accent }) {
return (
{text}
);
}
function RailPhotoCard({ card }) {
const art = RAIL_ART[card.art] || RAIL_ART.harbor;
/* real image (live /media/ asset) when present + loadable; gradient art otherwise */
const [imgOk, setImgOk] = React.useState(true);
React.useEffect(() => {
setImgOk(true);
if (!card.image) return;
const probe = new Image();
probe.onerror = () => setImgOk(false);
probe.src = card.image;
return () => { probe.onerror = null; };
}, [card.image]);
const photo = !!card.image && imgOk;
return (
{!photo &&
}
{card.credit && (
{card.credit}
)}
{card.loc && (
▸ {card.loc}
)}
{card.headline}
{card.dek}
);
}
function RailChartCard({ card }) {
const pts = card.chart.points;
const max = Math.max(...pts.map((p) => p.v));
return (
{card.headline}
{card.sub}
{pts.map((p, i) => {
const hot = i === pts.length - 1;
return (
);
})}
{card.source}
);
}
function RailQuoteCard({ card }) {
return (
);
}
function RailStatCard({ card }) {
return (
{card.stat}
{card.unit}
{card.headline}
{card.sub}
);
}
const RAIL_CARD_TYPES = { photo: RailPhotoCard, chart: RailChartCard, quote: RailQuoteCard, stat: RailStatCard };
function RailCard({ card }) {
const Body = RAIL_CARD_TYPES[card.type] || RailPhotoCard;
return (
);
}
/* ---- Live cams: rail card holding 1–2 remote feeds (severe weather / watch-and-wait) ---- */
function CamCell({ scene, h24, showTime = true }) {
const def = window.HERO_SCENES[scene] || {};
const now = useNow();
const W = 466, H = Math.round(W * 9 / 16);
return (
{def.locator}
{showTime && {fmtTime(now, def.tz, h24)}}
{/* clean chroma hole — cam video keyed in downstream (magenta, same as hero) */}
);
}
function RailCamsCard({ cams, h24, camTimes = {} }) {
return (
);
}
/* ---- Rail countdown: card addendum — the card appears to extend down past its divider ---- */
function RailCountdown({ label = "TOP OF THE HOUR", target = null, accent = "var(--signal-cyan)" }) {
const now = useNow(500);
/* director-supplied absolute target (RFC3339); default: top of the hour */
let tgt = null;
if (target) {
const d = new Date(target);
if (!isNaN(d.getTime())) tgt = d;
}
if (!tgt) { tgt = new Date(now); tgt.setMinutes(60, 0, 0); }
const s = Math.max(0, Math.floor((tgt - now) / 1000));
const pad2 = (n) => String(n).padStart(2, "0");
const disp = s >= 172800
? `${Math.floor(s / 86400)}D ${pad2(Math.floor((s % 86400) / 3600))}H` /* long-horizon (elections) */
: s >= 3600
? `${pad2(Math.floor(s / 3600))}:${pad2(Math.floor((s % 3600) / 60))}:${pad2(s % 60)}`
: `${pad2(Math.floor(s / 60))}:${pad2(s % 60)}`;
/* flush under the card: shares the card's bottom hairline, paints over its shadow */
return (
{label}
{disp}
);
}
function SideRail({ mode, h24, dwell = 18000, cams = [], camTimes }) {
const cards = window.R1DATA.railCards;
const idx = useCycle(cards.length, dwell, mode === "situation" || cams.length > 0);
if (cams.length > 0) return ;
if (mode === "situation") return ;
if (!cards.length) return null; /* empty rail — show nothing, never crash */
const card = cards[idx % cards.length];
return (
);
}
/* ---- Situation rail: one static card — the timeline of what we know ---- */
function SituationRail() {
const C = window.R1DATA.situation;
if (!C || !(C.timeline || []).length) return null; /* no live situation content */
return (
DEVELOPING
{C.topic}
WHAT WE KNOW · TIMELINE
{C.timeline.map((t, i) => (
{t.t}
{t.text}
))}
RADAR1 VERIFIED · ALL TIMES GMT
);
}
/* ================= INFO BAR — Now/Next · world city feature bar · animated logo ================= */
/* weather-type ambient animations — miniature broadcast weather FX, behind cell content */
function wxSeed(str) { let s = 0; for (let i = 0; i < str.length; i++) s = (s * 31 + str.charCodeAt(i)) % 2147483647; return s || 7; }
function wxRand(seed) { let s = seed % 2147483647; if (s <= 0) s += 2147483646; return () => (s = (s * 16807) % 2147483647) / 2147483647; }
function WxAnim({ type, seed = 7 }) {
const layers = { position: "absolute", inset: 0, pointerEvents: "none" };
const vMask = { WebkitMaskImage: "linear-gradient(to bottom, transparent 0, black 18%, black 80%, transparent 100%)", maskImage: "linear-gradient(to bottom, transparent 0, black 18%, black 80%, transparent 100%)" };
const hMask = { WebkitMaskImage: "linear-gradient(to right, transparent 0, black 12%, black 88%, transparent 100%)", maskImage: "linear-gradient(to right, transparent 0, black 12%, black 88%, transparent 100%)" };
/* deterministic per-city randomization (no re-roll on re-render) */
const R = React.useMemo(() => {
const r = wxRand(seed * 7919 + 13);
return {
drops: Array.from({ length: 16 }, () => ({ left: 4 + r() * 92, dur: 0.65 + r() * 0.75, delay: -r() * 2.2, op: 0.30 + r() * 0.38, h: 12 + r() * 12 })),
clouds: Array.from({ length: 3 }, () => ({ top: 4 + r() * 30, dur: 26 + r() * 22, delay: -r() * 40, sc: 0.8 + r() * 0.7, op: 0.10 + r() * 0.08 })),
};
}, [seed]);
if (type === "sun") return (
{/* rotating rays, radially faded — centered, subdued */}
{/* glowing core */}
);
if (type === "cloud" || type === "fog") {
const tint = type === "fog" ? "203,211,222" : "166,177,194";
return (
{R.clouds.map((c, i) => (
))}
{type === "fog" && }
);
}
if (type === "rain" || type === "storm") {
const col = type === "storm" ? "150,130,255" : "92,196,240";
const drops = type === "storm" ? R.drops.slice(0, 12) : R.drops;
return (
{/* falling stems, per-drop randomized (speed / phase / opacity / length) */}
{drops.map((d, i) => (
))}
{type === "storm" && (
{/* brooding cloud mass along the top */}
{/* lightning bolt + cell flash */}
)}
);
}
return null;
}
/* cycling fixed clock: GMT + US zones, 20s dwell, crossfade */
const CLOCK_ZONES = [
{ l: "GMT", tz: "Etc/GMT" },
{ l: "ET", tz: "America/New_York" },
{ l: "CT", tz: "America/Chicago" },
{ l: "MT", tz: "America/Denver" },
{ l: "PT", tz: "America/Los_Angeles" },
];
/* live 3-letter abbreviation (EDT/EST etc. — DST-aware) */
function tzAbbr(now, tz, fallback) {
try {
const p = new Intl.DateTimeFormat("en-US", { timeZone: tz, timeZoneName: "short" }).formatToParts(now);
const v = (p.find((x) => x.type === "timeZoneName") || {}).value || fallback;
return /^[A-Z]{2,4}$/.test(v) ? v : fallback;
} catch (e) { return fallback; }
}
function CycleClock({ now }) {
const i = useCycle(CLOCK_ZONES.length, 20000);
const cycled = React.useRef(false);
React.useEffect(() => { if (i > 0) cycled.current = true; }, [i]);
const prev = (i + CLOCK_ZONES.length - 1) % CLOCK_ZONES.length;
const cell = (z) => {
let parts;
try { parts = new Intl.DateTimeFormat("en-US", { hour: "numeric", minute: "2-digit", hour12: true, timeZone: z.tz }).formatToParts(now); } catch (e) { parts = null; }
const get = (t) => (parts ? (parts.find((p) => p.type === t) || {}).value || "" : "");
return (
{parts ? get("hour") + ":" + get("minute") : "--:--"}
{get("dayPeriod").toUpperCase()} {tzAbbr(now, z.tz, z.l)}
);
};
return (
{cell(CLOCK_ZONES[i])}
{cycled.current && (
{cell(CLOCK_ZONES[prev])}
)}
);
}
/* fixed-width clock: identical box in 12h and 24h modes (AM/PM as small suffix) */
function CityClock({ now, tz, h24 }) {
const parts = React.useMemo(() => {
try {
return new Intl.DateTimeFormat("en-US", { hour: "2-digit", minute: "2-digit", hour12: !h24, timeZone: tz }).formatToParts(now);
} catch (e) { return null; }
}, [now, tz, h24]);
const get = (t) => (parts ? (parts.find((p) => p.type === t) || {}).value || "" : "");
return (
{parts ? get("hour") + ":" + get("minute") : "--:--"}
{!h24 && {get("dayPeriod").toUpperCase()}}
);
}
/* ---- brand cell with promo rotation: every 5 min the logo fades out for a 10s promo ---- */
const PROMO_MSGS = [
{ pre: "WATCH ONLINE AT", brand: "RADAR1.COM" },
{ pre: "NOW ON MOBILE", brand: "RADAR1.APP" },
];
function BrandPromoCell({ now, size = 26 }) {
/* wall-clock driven so every instance agrees: last 10s of each 5-minute block */
const sec = Math.floor(now.getTime() / 1000);
const promo = sec % 300 >= 290;
const m = PROMO_MSGS[Math.floor(sec / 300) % PROMO_MSGS.length];
return (
);
}
function InfoBar({ h24, mode }) {
const now = useNow();
const prog = window.R1DATA.programs[0];
const regions = window.R1DATA.regions;
/* 3 cities per page; a region shows all its pages before the next region flips in */
const pages = React.useMemo(() => {
const out = [];
regions.forEach((r) => {
for (let i = 0; i < r.cities.length; i += 3) {
out.push({ region: r, cities: r.cities.slice(i, i + 3), page: i / 3, pages: Math.ceil(r.cities.length / 3) });
}
});
return out;
}, [regions]);
const pi = useCycle(pages.length, 7000);
const cur = pages[pi];
const region = cur.region;
const cycledRef = React.useRef(false);
React.useEffect(() => { if (pi > 0) cycledRef.current = true; }, [pi]);
const prevCities = cycledRef.current || pi > 0 ? pages[(pi + pages.length - 1) % pages.length].cities : null;
useLucide([pi]);
const minsToNext = 60 - now.getMinutes();
return (
{/* left: programming — subtle progress fill tracks the hour */}
NOW
{prog.now}
NEXT
{prog.next}
IN {minsToNext} MIN
{/* middle: world city feature bar — 4 fixed segments (region + 3 cities) */}
{/* segment 1: region header — fixed width, never reflows */}
{region.name}
{/* segments 2–4: equal-width city cells fill the remaining bar; staggered crossfade per cell */}
{cur.cities.map((c, ci) => (
{c.city}
{c.hi}°
{c.lo}°
{prevCities && prevCities[ci] && prevCities[ci].city !== c.city && (
{prevCities[ci].city}
{prevCities[ci].hi}°
{prevCities[ci].lo}°
)}
))}
{/* right: animated network logo + cycling zone clock as one unit (with promo rotation) */}
);
}
/* ================= TICKERS ================= */
const TICKER_LABEL_W = 178;
function TickerLabel({ label, accent, ink }) {
return (
{label}
);
}
/* Rotating crawl bar:
- fixed-width label with sweep (right-edge) rounding; unique accent per ticker
- accent also tints the bar itself
- rotation choreography: old content drains off left → beat of empty bar →
incoming label travels across with its content trailing → docks over the old label */
function RotatingCrawl({ items, dur = 46, height = 52, rotateMs = 150000, offsetMs = 0, tint = true }) {
const [active, setActive] = React.useState(0);
const [phase, setPhase] = React.useState("loop"); /* loop → drain → gap → chip → loop */
const barRef = React.useRef(null);
const stripRef = React.useRef(null);
const chipRef = React.useRef(null);
/* items can shrink between renders when a live category empties — stay in range */
const act = items.length ? active % items.length : 0;
const incoming = (act + 1) % items.length;
const cur = items[act], inc = items[incoming];
const curDur = cur.dur || dur;
/* rotate timer */
React.useEffect(() => {
if (items.length < 2) return;
let t;
const arm = (ms) => { t = setTimeout(() => { setPhase("drain"); arm(rotateMs); }, ms); };
arm(rotateMs + offsetMs);
return () => clearTimeout(t);
}, [items.length, rotateMs, offsetMs]);
/* drain: time's up — cut the old ticker with a quick clean fade */
React.useEffect(() => {
if (phase !== "drain") return;
const strip = stripRef.current;
if (!strip) { setPhase("gap"); return; }
const anim = strip.animate([{ opacity: 1 }, { opacity: 0 }], { duration: 350, easing: "ease-out", fill: "forwards" });
anim.onfinish = () => setPhase("gap");
return () => anim.cancel();
}, [phase]);
/* gap: a beat of empty bar */
React.useEffect(() => {
if (phase !== "gap") return;
const t = setTimeout(() => setPhase("chip"), 900);
return () => clearTimeout(t);
}, [phase]);
/* chip: incoming label crosses the bar (content trailing) and docks over the old label */
React.useEffect(() => {
if (phase !== "chip") return;
const bar = barRef.current, chip = chipRef.current;
if (!bar || !chip) { setActive(incoming); setPhase("loop"); return; }
const BW = bar.offsetWidth;
const speed = 260; /* px/s — label glides across, readable but not sluggish */
const anim = chip.animate(
[{ transform: `translateX(${BW}px)` }, { transform: "translateX(0px)" }],
{ duration: (BW / speed) * 1000, easing: "linear", fill: "forwards" }
);
anim.onfinish = () => { setActive(incoming); setPhase("loop"); };
return () => anim.cancel();
}, [phase, incoming]);
return (
{/* crawl layer — spans the FULL bar so items slide beneath the translucent label as they exit */}
{phase !== "chip" && (
{cur.children}
{cur.children}
)}
{/* fixed label — translucent so the crawl fades behind it */}
{/* traveling label chip + trailing content (covers the whole bar, slides over the old label) */}
{phase === "chip" && (
)}
);
}
/* ---- ticker content segments ---- */
function diamondSegs(texts, accent, strong) {
return (
{texts.map((t, i) => (
◆{t}
))}
);
}
function marketSegs(data) {
return (
{data.map((m, i) => (
{m.sym}
{m.px}
))}
);
}
/* Situation: double-height breaking flipper — right column stacks one city cell with the (navy) logo box */
function HeadlineFlipper({ edge = "bottom", h24 = true }) {
const C = window.R1DATA.situation || { topic: "", headlines: [""] };
const i = useCycle(C.headlines.length, 8000);
const now = useNow();
const cityCell = ;
const logoCell = (
);
return (
{C.topic}
{/* right column: logo keeps its normal-mode position (nearest the hero); the city cell takes the outer slot */}
{edge === "bottom" ? (
{logoCell}
{cityCell}
) : (
{cityCell}
{logoCell}
)}
);
}
/* Situation: single city cell — one city at a time, fixed box, crossfade */
function SituationCityCell({ h24 }) {
const cities = React.useMemo(() => window.R1DATA.regions.flatMap((r) => r.cities), []);
const i = useCycle(cities.length, 8000);
const now = useNow();
const prev = (i + cities.length - 1) % cities.length;
const cycled = React.useRef(false);
React.useEffect(() => { if (i > 0) cycled.current = true; }, [i]);
const cell = (c) => (
{c.city}
{c.hi}°
{c.lo}°
);
return (
{cell(cities[i])}
{cycled.current && (
{cell(cities[prev])}
)}
);
}
function TickerStack({ mode, dur = 46, edge = "bottom", h24 = true }) {
const D = window.R1DATA;
const sportsCrawl = (D.sports && D.sports.crawl) || [];
/* top rail: editorial categories */
const newsItem = { key: "news", label: "NEWS", accent: "var(--radar-red)", src: D.tickerNews || [], children: diamondSegs(D.tickerNews || [], "var(--radar-red)"), dur };
const topAll = [
newsItem,
{ key: "sports", label: "SPORTS", accent: "var(--signal-cyan)", ink: "#04121C", src: sportsCrawl, children: diamondSegs(sportsCrawl, "var(--signal-cyan)"), dur: Math.round(dur * 0.9) },
{ key: "ent", label: "ENTERTAINMENT", accent: "var(--signal-violet)", src: D.entertainment || [], children: diamondSegs(D.entertainment || [], "var(--signal-violet)"), dur: Math.round(dur * 0.95) },
{ key: "biz", label: "BUSINESS", accent: "var(--signal-green)", ink: "#03130A", src: D.business || [], children: diamondSegs(D.business || [], "var(--signal-green)"), dur },
{ key: "tech", label: "TECHNOLOGY", accent: "var(--signal-amber)", ink: "#1F1300", src: D.technology || [], children: diamondSegs(D.technology || [], "var(--signal-amber)"), dur: Math.round(dur * 0.95) },
];
/* bottom rail: finance boards */
const cryptoItem = { key: "crypto", label: "CRYPTO", accent: "var(--signal-violet)", src: D.crypto || [], children: marketSegs(D.crypto || []), dur };
const botAll = [
{ key: "stocks", label: "STOCKS", accent: "var(--signal-green)", ink: "#03130A", src: D.markets || [], children: marketSegs((D.markets || []).slice(0, 6)), dur: Math.round(dur * 1.15) },
cryptoItem,
{ key: "forex", label: "FOREX", accent: "var(--signal-cyan)", ink: "#04121C", src: D.forex || [], children: marketSegs(D.forex || []), dur },
{ key: "cmdty", label: "COMMODITIES", accent: "var(--signal-amber)", ink: "#1F1300", src: D.commodities || [], children: marketSegs(D.commodities || []), dur: Math.round(dur * 1.1) },
{ key: "rates", label: "RATES", accent: "var(--grey-500)", ink: "#04121C", src: D.rates || [], children: marketSegs(D.rates || []), dur: Math.round(dur * 0.9) },
];
/* empty categories drop out of rotation; each bar always keeps its anchor item */
let topItems = topAll.filter((it) => it.src.length > 0);
if (!topItems.length) topItems = [newsItem];
let botItems = botAll.filter((it) => it.src.length > 0);
if (!botItems.length) botItems = [cryptoItem];
const rows = mode === "situation"
/* crisis: the full category rotation runs on the OUTER bar; the inner bar is a static headline flipper.
NEWS trades its red for cyan so red stays reserved for the breaking treatment */
? [, it.key !== "news"), ...botItems]} dur={dur} rotateMs={120000} tint={false} />]
: [, ];
if (edge === "top") rows.reverse();
return (
{rows}
);
}
Object.assign(window, { SideRail, SituationRail, InfoBar, TickerStack, RotatingCrawl, HeadlineFlipper, SituationCityCell, RailCamsCard, RailCountdown });