PageSourceSearch

https://voc.systems/news.js?v=20260814-newsroom-signal-ranking-v1

js voc.systems collected 2026-10-04 03:09:11 UTC 55,302 bytes, 1,164 lines download raw bytes

1function escapeHtml(value) {
2  return String(value ?? "")
3    .replaceAll("&", "&")
4    .replaceAll("<", "&lt;")
5    .replaceAll(">", "&gt;")
6    .replaceAll('"', "&quot;")
7    .replaceAll("'", "&#039;");
8}
9
10function cleanInlineText(value = "") {
11  return String(value || "")
12    .replace(/\s+/g, " ")
13    .trim();
14}
15
16function formatPostedAt(value) {
17  if (!value) return "Posted time pending";
18  const date = new Date(value);
19  if (Number.isNaN(date.getTime())) return "Posted time pending";
20  const formatted = new Intl.DateTimeFormat("en-US", {
21    month: "short",
22    day: "numeric",
23    year: "numeric",
24    hour: "numeric",
25    minute: "2-digit",
26    timeZone: "America/New_York",
27    timeZoneName: "short"
28  }).format(date).replace(/, (?=\d{1,2}:)/, " at ");
29  return `Posted ${formatted}`;
30}
31
32const HISTORY_NOW_LANE_FILTERS = [
33  { key: "artifactsanddocuments", routePrefix: "ArtifactsandDocuments", label: "Artifacts & Documents", shortLabel: "Artifacts" },
34  { key: "sportsbusinessorigins", routePrefix: "SportsBusinessOrigins", label: "Sports Business Origins", shortLabel: "Business Origins" },
35  { key: "institutionaldesign", routePrefix: "InstitutionalDesign", label: "Institutional Design", shortLabel: "Institutional Design" },
36  { key: "fancultureandvenuerituals", routePrefix: "FanCultureandVenueRituals", label: "Fan Culture & Venue Rituals", shortLabel: "Fan Rituals" },
37  { key: "sportsscienceandtraininghistory", routePrefix: "SportsScienceandTrainingHistory", label: "Sports Science & Training History", shortLabel: "Science & Training" },
38  { key: "globalnichesports", routePrefix: "GlobalNicheSports", label: "Global/Niche Sports", shortLabel: "Global/Niche" },
39  { key: "failedfutures", routePrefix: "FailedFutures", label: "Failed Futures", shortLabel: "Failed Futures" }
40];
41
42const ARTICLE_MONTHS = [
43  { value: "01", label: "Jan" },
44  { value: "02", label: "Feb" },
45  { value: "03", label: "Mar" },
46  { value: "04", label: "Apr" },
47  { value: "05", label: "May" },
48  { value: "06", label: "Jun" },
49  { value: "07", label: "Jul" },
50  { value: "08", label: "Aug" },
51  { value: "09", label: "Sep" },
52  { value: "10", label: "Oct" },
53  { value: "11", label: "Nov" },
54  { value: "12", label: "Dec" }
55];
56
57function normalizeLaneKey(value = "") {
58  return cleanInlineText(value)
59    .toLowerCase()
60    .replace(/&/g, "and")
61    .replace(/[^a-z0-9]+/g, "");
62}
63
64function laneFilterFor(value = "") {
65  const key = normalizeLaneKey(value);
66  if (!key) return null;
67  return HISTORY_NOW_LANE_FILTERS.find((lane) =>
68    key === lane.key ||
69    key === normalizeLaneKey(lane.routePrefix) ||
70    key === normalizeLaneKey(lane.label)
71  ) || null;
72}
73
74function activeLane() {
75  return laneFilterFor(new URLSearchParams(location.search).get("lane") || "")?.routePrefix || "";
76}
77
78function activeLaneMeta() {
79  return laneFilterFor(activeLane());
80}
81
82function activeSport() {
83  return new URLSearchParams(location.search).get("sport")?.toUpperCase() || "";
84}
85
86function activeType() {
87  const type = new URLSearchParams(location.search).get("type");
88  if (type === "draft") return "draft";
89  if (type === "evergreen") return "evergreen";
90  if (type === "daily") return "daily";
91  if (type === "archive" || type === "waiver") return "archive";
92  return "all";
93}
94
95function activeSignal() {
96  const signal = new URLSearchParams(location.search).get("signal");
97  return signal === "breaking" || signal === "trending" ? signal : "";
98}
99
100function activeSection() {
101  return cleanInlineText(new URLSearchParams(location.search).get("section") || "").slice(0, 80);
102}
103
104function normalizeArticleSort(value = "") {
105  return cleanInlineText(value).toLowerCase() === "oldest" ? "oldest" : "latest";
106}
107
108function activeArticleSort(url = new URL(location.href)) {
109  return normalizeArticleSort(url.searchParams.get("sort") || "");
110}
111
112function normalizeArticleYear(value = "") {
113  const year = Number.parseInt(String(value || ""), 10);
114  const maxYear = new Date().getFullYear() + 2;
115  return Number.isInteger(year) && year >= 1900 && year <= maxYear ? String(year) : "";
116}
117
118function activeArticleYear(url = new URL(location.href)) {
119  return normalizeArticleYear(url.searchParams.get("year") || "");
120}
121
122function normalizeArticleMonth(value = "") {
123  const raw = cleanInlineText(value).toLowerCase();
124  const labelMatch = ARTICLE_MONTHS.find((month) => month.label.toLowerCase() === raw || month.label.toLowerCase().startsWith(raw));
125  if (labelMatch && raw.length >= 3) return labelMatch.value;
126  const month = Number.parseInt(raw, 10);
127  return Number.isInteger(month) && month >= 1 && month <= 12 ? String(month).padStart(2, "0") : "";
128}
129
130function activeArticleMonth(url = new URL(location.href)) {
131  return normalizeArticleMonth(url.searchParams.get("month") || "");
132}
133
134function currentViewLabel() {
135  const type = activeType();
136  const sport = activeSport();
137  const signal = activeSignal();
138  const section = activeSection();
139  const lane = activeLaneMeta();
140  if (section) return sport ? `${sport} ${section}` : section;
141  if (signal === "breaking") return sport ? `${sport} Breaking` : "Breaking";
142  if (signal === "trending") return sport ? `${sport} Trending` : "Trending";
143  if (lane) return lane.label;
144  if (type === "draft") return sport ? `${sport} Draft Lab` : "Draft Lab";
145  if (type === "evergreen" && sport === "CULTURE") return "Culture";
146  if (type === "evergreen") return sport ? `${sport} History` : "History";
147  if (type === "daily") return sport ? `${sport} Daily Pulse` : "Daily Pulse";
148  if (type === "archive") return sport ? `${sport} Waiver Archive` : "Waiver Archive";
149  return sport ? `${sport} Articles` : "All Articles";
150}
151
152function libraryLabel() {
153  const type = activeType();
154  const sport = activeSport();
155  const signal = activeSignal();
156  const section = activeSection();
157  const lane = activeLaneMeta();
158  if (section) return section.toLowerCase();
159  if (signal === "breaking") return "breaking news";
160  if (signal === "trending") return "trending article";
161  if (lane) return lane.label.toLowerCase();
162  if (type === "evergreen" && sport === "CULTURE") return "sports culture";
163  if (type === "evergreen") return "historical feature";
164  if (type === "draft") return "draft lab";
165  if (type === "daily") return "daily market pulse";
166  if (type === "archive") return "waiver receipt";
167  return "article";
168}
169
170function sportQuery(sport = "") {
171  const value = String(sport || "").trim().toUpperCase();
172  return value ? `?sport=${encodeURIComponent(value)}` : "";
173}
174
175function sportDirectoryHash(sport = "") {
176  const value = String(sport || "").trim().toLowerCase();
177  return value ? `#${encodeURIComponent(value)}-intelligence` : "";
178}
179
180function sportDirectoryHref(sport = "") {
181  return `sports-directory.html${sportQuery(sport)}${sportDirectoryHash(sport)}`;
182}
183
184function newsCardPrimaryLabel(article = {}) {
185  const section = String(article.section || "").trim().toLowerCase();
186  if (section.includes("daily market pulse")) return "Read what changed";
187  if (section.includes("draft")) return "Read the angle";
188  if (articleSignal(article)) return "Read the signal";
189  if (section.includes("culture")) return "Read the argument";
190  if (section.includes("history") || section.includes("evergreen")) return "Read the argument";
191  return "Read the story";
192}
193
194function newsCardHookText(article = {}) {
195  const text = cleanInlineText(article.dek || article.metaDescription || "");
196  if (!text) return "The answer is up front. The consequence is the reason to keep reading.";
197  return text
198    .replace(/^(?:DME call|Desk read|Decision board|Lead question|Why the article works|Where skeptics should push)\s*:\s*/i, "")
199    .replace(/^[-*]\s+/, "")
200    .trim();
201}
202
203function newsCardPayoffLine(article = {}) {
204  const sport = String(article.sport || "").trim().toUpperCase();
205  const sportLabel = sport && sport !== "SPORTS" ? sport : "game";
206  const section = String(article.section || "").toLowerCase();
207  if (section.includes("history") || section.includes("culture") || section.includes("evergreen")) {
208    return "Start with the answer. Stay for the argument that keeps the moment alive.";
209  }
210  if (section.includes("daily market pulse")) {
211    return `Start with what changed. If it moves the read, open the ${sportLabel} board.`;
212  }
213  return `Start with the answer. Open the ${sportLabel} board only when the consequence matters.`;
214}
215
216const localApprovedArticleMedia = new Set([
217  "2026-05-25-mlb-market-pulse",
218  "2026-05-26-mlb-market-pulse",
219  "2026-05-26-nba-market-pulse",
220  "2026-05-26-nfl-market-pulse",
221  "2026-05-26-nhl-market-pulse",
222  "2026-05-26-pga-market-pulse"
223]);
224
225const localArticleThumbnailOverrides = {
226  "1972-olympic-basketball-three-second-reset-market": "/assets/articles/approved/1972-olympic-basketball-three-second-reset-market-hero.png",
227  "2004-nba-finals-pistons-prince-angle-defense-market": "/assets/articles/approved/2004-nba-finals-pistons-prince-angle-defense-market-hero.png",
228  "sports-culture-bleacher-creatures-roll-call-belonging-market": "/assets/articles/approved/sports-culture-bleacher-creatures-roll-call-belonging-market-hero.png",
229  "2011-nba-finals-dallas-zone-miami-hesitation-market": "/assets/articles/approved/2011-nba-finals-dallas-zone-miami-hesitation-market-hero-clean.png",
230  "sports-culture-vuvuzela-sound-ritual-market": "/assets/articles/approved/sports-culture-vuvuzela-sound-ritual-market-hero-clean.png",
231  "derek-redmond-1992-arm-around-lane-six-market": "/assets/articles/approved/derek-redmond-1992-arm-around-lane-six-market-hero-clean.png",
232  "super-bowl-xlii-nascar-front-pressure-market": "/assets/articles/approved/super-bowl-xlii-nascar-front-pressure-market-hero.png",
233  "sports-culture-krzyzewskiville-tenting-belonging-market": "/assets/articles/approved/sports-culture-krzyzewskiville-tenting-belonging-market-hero.png",
234  "2001-world-series-game-7-rivera-bunt-error-market": "/assets/articles/approved/2001-world-series-game-7-rivera-bunt-error-market-hero.png",
235  "steve-bartman-foul-ball-scapegoat-market": "/assets/articles/approved/steve-bartman-foul-ball-scapegoat-market-hero.png",
236  "2019-nba-finals-raptors-box-and-one-curry-market": "/assets/articles/approved/2019-nba-finals-raptors-box-and-one-curry-market-hero.png",
237  "sports-culture-terrible-towel-civic-flag-market": "/assets/articles/approved/sports-culture-terrible-towel-civic-flag-market-hero.png",
238  "super-bowl-xliv-ambush-onside-kick-halftime-market": "/assets/articles/approved/super-bowl-xliv-ambush-onside-kick-halftime-market-hero.png",
239  "sports-culture-portland-victory-log-slab-belonging-market": "/assets/articles/approved/sports-culture-portland-victory-log-slab-belonging-market-hero.png"
240};
241
242const localArticleCardThumbnailOverrides = {
243  "sports-culture-canucks-towel-power-belonging-market": "/assets/articles/approved/sports-culture-canucks-towel-power-belonging-market-card-clean.jpg",
244  "2009-champions-league-final-messi-false-nine-market": "/assets/articles/approved/2009-champions-league-final-messi-false-nine-market-card-clean.jpg",
245  "panenka-1976-penalty-chip-nerve-market": "/assets/articles/approved/panenka-1976-penalty-chip-nerve-market-card-clean.jpg",
246  "2026-06-02-ghana-partey-world-cup-squad-selection-storm": "/assets/articles/approved/2026-06-02-ghana-partey-world-cup-squad-selection-storm-card-clean.jpg",
247  "mankad-non-striker-runout-1947-cricket-law-market": "/assets/articles/approved/mankad-non-striker-runout-1947-cricket-law-market-card-v2.jpg",
248  "world-series-cricket-white-ball-tv-revolution-market": "/assets/articles/approved/world-series-cricket-white-ball-tv-revolution-market-thumb-v5.jpg"
249};
250const NEWS_CARD_CLEAN_THUMB_VERSION = "20260606e";
251
252function versionedNewsCardThumbnail(path = "") {
253  return `${path}?v=${NEWS_CARD_CLEAN_THUMB_VERSION}`;
254}
255
256function articleThumbnailUrl(article = {}) {
257  const slug = String(article.slug || "").trim();
258  if (localArticleThumbnailOverrides[slug]) return localArticleThumbnailOverrides[slug];
259  if (localApprovedArticleMedia.has(slug)) return `/assets/articles/approved/${slug}-01.png`;
260  const thumbnail = article.thumbnailUrl || "";
261  return String(thumbnail).includes(".svg") ? "" : thumbnail;
262}
263
264function articleHologramFallbackUrl(article = {}) {
265  const haystack = [article.sport, article.section, article.title, article.slug].filter(Boolean).join(" ").toUpperCase();
266  if (/(FIFA|WORLD CUP|SOCCER|UEFA|EPL|MLS|USMNT|USWNT|PREMIER LEAGUE)/.test(haystack)) return "/assets/articles/approved/dme-holo-logo-soccer.png";
267  if (/(NASCAR|CUP SERIES|XFINITY)/.test(haystack)) return "/assets/articles/approved/dme-holo-logo-nascar.png";
268  if (/(PGA|GOLF|MASTERS|US OPEN|U\.S\. OPEN|OPEN CHAMPIONSHIP|RYDER CUP)/.test(haystack)) return "/assets/articles/approved/dme-holo-logo-pga.png";
269  if (/(TENNIS|ATP|WTA|WIMBLEDON|ROLAND GARROS|US OPEN|AUSTRALIAN OPEN)/.test(haystack)) return "/assets/articles/approved/dme-holo-logo-tennis.png";
270  if (/(CRICKET|IPL|MLC|ASHES|TEST CRICKET|T20|ODI|ICC|PLAYING XI|WHITE BALL|UNDERARM)/.test(haystack)) return "/assets/articles/approved/dme-holo-logo-cricket-generic-v1.png";
271  if (/(NHL|HOCKEY|STANLEY CUP)/.test(haystack)) return "/assets/articles/approved/dme-holo-logo-nhl.png";
272  if (/(MLB|BASEBALL|WORLD SERIES)/.test(haystack)) return "/assets/articles/approved/dme-holo-logo-mlb.png";
273  if (/(WNBA|NCAAWB)/.test(haystack)) return "/assets/articles/approved/dme-holo-logo-wnba.png";
274  if (/(NBA|BASKETBALL|FINAL FOUR|MARCH MADNESS)/.test(haystack)) return "/assets/articles/approved/dme-holo-logo-nba.png";
275  if (/(NCAAF|COLLEGE FOOTBALL)/.test(haystack)) return "/assets/articles/approved/dme-holo-logo-ncaaf.png";
276  if (/(NFL|FOOTBALL|SUPER BOWL)/.test(haystack)) return "/assets/articles/approved/dme-holo-logo-nfl.png";
277  return "/assets/articles/approved/dme-holo-logo-default.png";
278}
279
280function newsCardCleanThumbnailUrl(thumbnail = "") {
281  const cleanPath = String(thumbnail || "").split("?")[0];
282  if (!cleanPath.endsWith("-thumb-v2.jpg")) return "";
283  return versionedNewsCardThumbnail(`${cleanPath.slice(0, -"-thumb-v2.jpg".length)}-card-clean.jpg`);
284}
285
286function newsThumbnailHasEmbeddedText(path = "") {
287  const cleanPath = String(path || "").split("?")[0].toLowerCase();
288  if (cleanPath.includes("/assets/articles/approved/dme-ultra-raster-")) return false;
289  return cleanPath.includes("/assets/articles/approved/") || /-(thumb-v[0-9]+|card(?:-clean|-v[0-9]+))\.jpg$/.test(cleanPath);
290}
291
292function newsThumbnailUsesDbFallback(path = "") {
293  return String(path || "").split("?")[0].toLowerCase().includes("/assets/articles/approved/dme-ultra-raster-");
294}
295
296function newsThumbnailUsesHologramLogo(path = "") {
297  return String(path || "").split("?")[0].toLowerCase().includes("/assets/articles/approved/dme-holo-logo-");
298}
299
300function newsFallbackVisualAttributes(thumbnail = {}) {
301  if (!newsThumbnailUsesDbFallback(thumbnail.src)) return "";
302  return ` data-visual-placement="news.card.fallback" data-visual-asset-slug="local-3d-visual-repository" data-visual-preserve-alt="true"`;
303}
304
305function articleThumbnailSource(article = {}) {
306  const src = articleThumbnailUrl(article);
307  if (!src) return { src: "", fallback: "", hasEmbeddedText: false };
308  const slug = String(article.slug || "").trim();
309  if (localArticleCardThumbnailOverrides[slug]) {
310    return {
311      src: versionedNewsCardThumbnail(localArticleCardThumbnailOverrides[slug]),
312      fallback: src,
313      hasEmbeddedText: true
314    };
315  }
316  if (newsThumbnailUsesDbFallback(src)) {
317    return {
318      src: articleHologramFallbackUrl(article),
319      fallback: src,
320      hasEmbeddedText: false,
321      isHologramArt: true
322    };
323  }
324  if (newsThumbnailUsesHologramLogo(src)) {
325    return { src, fallback: "", hasEmbeddedText: false, isHologramArt: true };
326  }
327  const cleanSrc = newsCardCleanThumbnailUrl(src);
328  return cleanSrc
329    ? { src: cleanSrc, fallback: src, hasEmbeddedText: true }
330    : { src, fallback: "", hasEmbeddedText: newsThumbnailHasEmbeddedText(src), isHologramArt: false };
331}
332
333function isPlayerSuperstarArticle(article = {}) {
334  return String(article.mediaTemplate || "").trim() === "player-superstar-feature" ||
335    String(article.section || "").trim().toLowerCase() === "superstar fan guide";
336}
337
338function articleOverlayLogoUrl(value = "") {
339  const url = String(value || "").trim();
340  const cleanPath = url.split("?")[0].toLowerCase();
341  if (cleanPath.includes("/teamlogos/soccer/500/364.png")) return "/assets/team-logos/clean/liverpool-fc.png";
342  if (cleanPath.includes("/teamlogos/nhl/500/scoreboard/det.png") || cleanPath.includes("/teamlogos/nhl/500/det.png")) {
343    return "/assets/team-logos/clean/detroit-red-wings.png";
344  }
345  return url;
346}
347
348function localSportLogoUrl(sport = "") {
349  const key = String(sport || "SPORTS").trim().toUpperCase() || "SPORTS";
350  const identityLogo = window.DMLSportIdentity?.get?.(key)?.logoUrl;
351  if (identityLogo) return identityLogo;
352  const localLogos = {
353    CULTURE: "/assets/brand/sport-logos/dme-sport-culture-logo.png?v=20260704",
354    SPORTS: "/assets/brand/sport-logos/dme-sport-general-logo.svg?v=20260704",
355    SPORT: "/assets/brand/sport-logos/dme-sport-general-logo.svg?v=20260704",
356    ALL: "/assets/brand/sport-logos/dme-sport-general-logo.svg?v=20260704",
357    NCAAMB: "/assets/brand/sport-logos/dme-sport-college-basketball-logo.svg?v=20260704",
358    NCAAB: "/assets/brand/sport-logos/dme-sport-college-basketball-logo.svg?v=20260704",
359    NCAAWB: "/assets/brand/sport-logos/dme-sport-college-basketball-logo.svg?v=20260704",
360    NCAAF: "/assets/brand/sport-logos/dme-sport-college-football-logo.svg?v=20260704-football6",
361    NCAA: "/assets/brand/sport-logos/dme-sport-college-logo.svg?v=20260704",
362    "COLLEGE SPORTS": "/assets/brand/sport-logos/dme-sport-college-logo.svg?v=20260704",
363    OLYMPICS: "/assets/brand/sport-logos/dme-sport-olympics-logo.svg?v=20260704",
364    OLYMPIC: "/assets/brand/sport-logos/dme-sport-olympics-logo.svg?v=20260704",
365    CYCLING: "/assets/brand/sport-logos/dme-sport-cycling-logo.svg?v=20260704",
366    RUNNING: "/assets/brand/sport-logos/dme-sport-running-logo.svg?v=20260704",
367    VOLLEYBALL: "/assets/brand/sport-logos/dme-sport-volleyball-logo.svg?v=20260704",
368    BOXING: "/assets/brand/sport-logos/dme-sport-boxing-logo.svg?v=20260704",
369    CRICKET: "/assets/brand/sport-logos/dme-sport-cricket-logo.svg?v=20260704",
370    RUGBY: "/assets/brand/sport-logos/dme-sport-rugby-logo.svg?v=20260704",
371    RACING: "/assets/brand/sport-logos/dme-sport-racing-logo.svg?v=20260704",
372    SOFTBALL: "/assets/brand/sport-logos/dme-sport-softball-logo.svg?v=20260704",
373    BASEBALL: "/assets/brand/sport-logos/dme-sport-baseball-logo.svg?v=20260704",
374    F1: "/assets/brand/sport-logos/dme-sport-f1-logo.svg?v=20260704",
375    UFC: "/assets/brand/sport-logos/dme-sport-combat-logo.svg?v=20260704",
376    MMA: "/assets/brand/sport-logos/dme-sport-combat-logo.svg?v=20260704",
377    "SPORTS MEDIA": "/assets/brand/sport-logos/dme-sport-media-logo.svg?v=20260704",
378    "NBA/NHL": "/assets/brand/sport-logos/dme-sport-multi-league-logo.svg?v=20260704"
379  };
380  return localLogos[key] || localLogos.SPORTS;
381}
382
383function renderNewsTeamLogoBadge(article = {}, options = {}) {
384  if (options.authoredArt) return "";
385  const logos = Array.isArray(article.teamLogos) ? article.teamLogos.filter((item) => item?.logoUrl) : [];
386  const sport = String(article.sport || "SPORTS").trim().toUpperCase() || "SPORTS";
387  const authoredClass = options.authoredArt ? " is-authored-art" : "";
388  const hologramClass = options.hologramBack ? " is-hologram-back" : "";
389  if (!logos.length) {
390    if (options.suppressLeagueFallback) return "";
391    const localLogo = localSportLogoUrl(sport);
392    return `
393      <div class="news-card-team-logos is-league-logo${authoredClass}${hologramClass}" aria-label="${escapeHtml(sport)} league mark" title="${escapeHtml(sport)} league mark">
394        <span class="news-card-team-logo" data-sport-logo-target="${escapeHtml(sport)}">${
395          localLogo ? `<img src="${escapeHtml(localLogo)}" alt="" loading="lazy" decoding="async" referrerpolicy="no-referrer" />` : ""
396        }</span>
397      </div>
398    `;
399  }
400  if (!logos.length) return "";
401  const label = logos.map((item) => item.teamName || item.abbreviation || item.sport).filter(
401Boolean).join(" / ");
402  return `
403    <div class="news-card-team-logos${authoredClass}${hologramClass}" aria-label="Team context: ${escapeHtml(label)}" title="${escapeHtml(label)}">
404      ${logos
405        .slice(0, 3)
406        .map(
407          (item) => `
408            <span class="news-card-team-logo">
409              <img src="${escapeHtml(articleOverlayLogoUrl(item.logoUrl))}" alt="" loading="lazy" decoding="async" referrerpolicy="no-referrer" />
410            </span>
411          `
412        )
413        .join("")}
414    </div>
415  `;
416}
417
418function headlineOverlayText(article = {}) {
419  const title = String(article.title || "").trim();
420  if (!title) return "Market read";
421  const split = title.split(/[:|]/)[0]?.trim() || title;
422  return split.length > 74 ? `${split.slice(0, 71).trim()}...` : split;
423}
424
425function renderNewsMediaOverlay(article = {}, signal = null) {
426  const sport = String(article.sport || "Sports").trim().toUpperCase();
427  const section = String(article.section || "Market Pulse").trim();
428  const label = signal?.label || section;
429  return `
430    <div class="news-card-media-overlay" aria-hidden="true">
431      <span>${escapeHtml(sport)} / ${escapeHtml(label)}</span>
432      <strong>${escapeHtml(headlineOverlayText(article))}</strong>
433    </div>
434  `;
435}
436
437const state = {
438  articles: [],
439  readerFilter: "all",
440  query: cleanInlineText(new URLSearchParams(location.search).get("q") || "").slice(0, 120),
441  sort: activeArticleSort(),
442  year: activeArticleYear(),
443  month: activeArticleMonth(),
444  counts: {},
445  pagination: null,
446  facets: { years: [] },
447  page: 1,
448  pageSize: 12,
449  totalPages: 1
450};
451let newsRequestId = 0;
452let searchDebounceTimer = 0;
453
454function articleLibrary(article = {}) {
455  const section = String(article.section || "").toLowerCase();
456  const sport = String(article.sport || "").toUpperCase();
457  if (section.includes("draft lab")) return "draft";
458  if (section === "sports culture" || sport === "CULTURE") return "culture";
459  if (section.includes("history") || section.includes("evergreen")) return "history";
460  if (section === "trending" || section === "daily market pulse" || section === "breaking market pulse" || section === "world cup 26 preview") return "daily";
461  if (section === "fantasy waiver brief") return "archive";
462  return "other";
463}
464
465function articleLaneKey(article = {}) {
466  const lane = laneFilterFor(article.lane || article.laneRoutePrefix || "");
467  if (lane) return lane.routePrefix;
468  const path = cleanInlineText(article.canonicalPath || "");
469  const [, routePrefix] = path.match(/^\/([^/]+)\//) || [];
470  return laneFilterFor(routePrefix)?.routePrefix || "";
471}
472
473function articleBelongsToCurrentView(article = {}) {
474  const type = activeType();
475  const sport = activeSport();
476  const library = articleLibrary(article);
477  const signal = activeSignal();
478  const section = activeSection();
479  const lane = activeLane();
480  if (section && normalizeSearchText(article.section || "") !== normalizeSearchText(section)) return false;
481  if (lane && articleLaneKey(article) !== lane) return false;
482  if (signal) return library === "daily";
483  if (type === "draft") return library === "draft";
484  if (lane) return library === "history" || library === "culture";
485  if (type === "evergreen" && sport === "CULTURE") return library === "culture";
486  if (type === "evergreen") return library === "history";
487  if (type === "daily") return library === "daily";
488  if (type === "archive") return library === "archive";
489  if (sport && sport !== "CULTURE") return String(article.sport || "").toUpperCase() === sport;
490  return true;
491}
492
493function hoursSince(value) {
494  const date = new Date(value || Date.now());
495  if (Number.isNaN(date.getTime())) return 999;
496  return Math.max(0, (Date.now() - date.getTime()) / 36e5);
497}
498
499function articleTimestamp(article = {}) {
500  const time = new Date(article.publishedAt || article.createdAt || 0).getTime();
501  return Number.isFinite(time) ? time : 0;
502}
503
504function articleYear(article = {}) {
505  const time = articleTimestamp(article);
506  return time ? String(new Date(time).getFullYear()) : "";
507}
508
509function articleMonth(article = {}) {
510  const time = articleTimestamp(article);
511  return time ? String(new Date(time).getMonth() + 1).padStart(2, "0") : "";
512}
513
514function articleMatchesYear(article = {}) {
515  return !state.year || articleYear(article) === state.year;
516}
517
518function articleMatchesMonth(article = {}) {
519  return !state.month || articleMonth(article) === state.month;
520}
521
522function articleSignalText(article = {}) {
523  return [article.title, article.dek, article.section, article.sport, article.metaDescription].join(" ").toLowerCase();
524}
525
526function normalizeSearchText(value) {
527  return String(value ?? "")
528    .normalize("NFKD")
529    .replace(/[\u0300-\u036f]/g, "")
530    .replace(/[’‘]/g, "'")
531    .replace(/[“”]/g, '"')
532    .replace(/[\u2010-\u2015]/g, "-")
533    .toLowerCase()
534    .replace(/[^a-z0-9]+/g, " ")
535    .trim();
536}
537
538function articleSearchText(article = {}) {
539  return normalizeSearchText([
540    article.title,
541    article.seoTitle,
542    article.dek,
543    article.metaDescription,
544    article.section,
545    article.sport,
546    article.author,
547    article.slug,
548    article.canonicalPath,
549    article.imageAlt,
550    article.mediaCaption,
551    article.visualTheme,
552    article.visualPrompt,
553    article.lane,
554    article.laneRoutePrefix
555  ].join(" "));
556}
557
558function articleMatchesSearch(article = {}) {
559  const query = normalizeSearchText(state.query);
560  if (!query) return true;
561  if (state.counts.searchTotal !== null && state.counts.searchTotal !== undefined) return true;
562  const terms = query.split(/\s+/).filter(Boolean);
563  const haystack = articleSearchText(article);
564  return terms.every((term) => haystack.includes(term));
565}
566
567function breakingScore(article = {}) {
568  const text = articleSignalText(article);
569  const hours = hoursSince(article.publishedAt || article.createdAt);
570  let score = Math.max(0, 72 - hours * 2.4);
571  if (/breaking|injury|injured|out|questionable|scratch|scratched|trade|signed|waived|returns|lineup|starter|pitcher|goalie|weather|delay|final|wins|leads|playoff/i.test(text)) score += 18;
572  if (String(article.section || "").toLowerCase().includes("breaking")) score += 24;
573  if (Number(article.wordCount || 0) >= 900) score += 4;
574  return Math.round(Math.min(100, score));
575}
576
577function trendingScore(article = {}) {
578  const text = articleSignalText(article);
579  const hours = hoursSince(article.publishedAt || article.createdAt);
580  let score = Math.max(0, 58 - hours * 0.85);
581  if (/viral|trend|trending|buzz|debate|reaction|sweep|\bfinal\b|finals|championship|game\s*7|draft|mock|waiver|injury|odds|fantasy|playoff|trade|rumor/i.test(text)) score += 18;
582  if (Number(article.wordCount || 0) >= 1200) score += 8;
583  if (Number(article.wordCount || 0) >= 1800) score += 5;
584  if (article.isPremium) score += 3;
585  return Math.round(Math.min(100, score));
586}
587
588function articleSignal(article = {}) {
589  const signal = activeSignal();
590  if (signal === "breaking") return { label: "Breaking Watch", score: breakingScore(article) };
591  if (signal === "trending") return { label: "Trending Score", score: trendingScore(article) };
592  const breakScore = breakingScore(article);
593  const trendScore = trendingScore(article);
594  if (breakScore >= 72) return { label: "Breaking Watch", score: breakScore };
595  if (trendScore >= 72) return { label: "Trending", score: trendScore };
596  return null;
597}
598
599const SOURCE_CONFIDENCE_BADGES = {
600  schedule: { label: "Schedule verified", className: "schedule" },
601  source: { label: "Source depth", className: "source" },
602  injury: { label: "Injury watch", className: "injury" },
603  weather: { label: "Weather risk", className: "weather" },
604  market: { label: "Market move", className: "market" },
605  lineup: { label: "Lineup check", className: "lineup" }
606};
607
608function articleConfidenceBadges(article = {}, signal = null) {
609  const text = articleSignalText(article);
610  const keys = ["schedule"];
611  if (/market|odds|line move|line movement|spread|total|price|pricing|bookmaker|ticket|resale|transfer/.test(text) || signal) keys.push("market");
612  if (/injury|injured|out|questionable|scratch|availability|hamstring|illness|suspended/.test(text)) keys.push("injury");
613  if (/weather|wind|rain|delay|roof|venue|heat|storm|field condition/.test(text)) keys.push("weather");
614  if (/lineup|starter|starting|pitcher|goalie|rotation|minutes|usage|roster|depth chart/.test(text)) keys.push("lineup");
615  if (Number(article.wordCount || 0) >= 900 || article.thumbnailUrl || article.mediaAssets?.length) keys.push("source");
616  if (!keys.includes("source")) keys.push("source");
617  return [...new Set(keys)]
618    .slice(0, 3)
619    .map((key) => SOURCE_CONFIDENCE_BADGES[key])
620    .filter(Boolean);
621}
622
623function renderArticleConfidenceBadges(article = {}, signal = null) {
624  const badges = articleConfidenceBadges(article, signal);
625  return `
626    <div class="article-proof-badges" aria-label="Article source confidence tags">
627      ${badges
628        .map(
629          (badge) => `
630            <span class="article-proof-badge is-${badge.className}">
631              <i aria-hidden="true"></i>
632              <b>${escapeHtml(badge.label)}</b>
633            </span>
634          `
635        )
636        .join("")}
637    </div>
638  `;
639}
640
641function isAutomatedNewsArticle(article = {}) {
642  const section = String(article.section || "");
643  return Boolean(article.isAutoGenerated || /Market Pulse|World Cup 26 Preview/i.test(section));
644}
645
646function newsUpdateStatus(article = {}, signal = null) {
647  const publishedAt = article.publishedAt || article.createdAt;
648  const hours = hoursSince(publishedAt);
649  const fresh = hours <= 24;
650  const automated = isAutomatedNewsArticle(article);
651  if (signal?.label && fresh) {
652    if (/trend/i.test(signal.label)) return "Trend alert updated";
653    return "News alert updated";
654  }
655  if (automated) return "";
656  if (fresh) return "Fresh news update";
657  return "";
658}
659
660function articleMatchesSignal(article = {}) {
661  const signal = activeSignal();
662  // Breaking is a ranked view of the current news feed, not an absolute gate.
663  // The freshness component naturally decays, so a score cutoff can make the
664  // homepage's newsroom link hide every published article between updates.
665  if (signal === "breaking") return true;
666  // Trending is a ranked view of the current news feed, not an absolute gate.
667  // An absolute score cutoff can hide every article as the feed ages, leaving
668  // the homepage's primary CTA pointed at an empty newsroom.
669  if (signal === "trending") return true;
670  return true;
671}
672
673function articleMatches(article) {
674  const readerOk =
675    state.readerFilter === "all" ||
676    (state.readerFilter === "premium" && article.isPremium) ||
677    (state.readerFilter === "free" && !article.isPremium);
678  return articleBelongsToCurrentView(article) && articleMatchesSignal(article) && readerOk && articleMatchesSearch(article) && articleMatchesYear(article) && articleMatchesMonth(article);
679}
680
681function sortArticlesForView(articles = []) {
682  const signal = activeSignal();
683  const list = [...articles];
684  if (signal === "breaking" && state.sort !== "oldest") return list.sort((a, b) => breakingScore(b) - breakingScore(a) || articleTimestamp(b) - articleTimestamp(a));
685  if (signal === "trending" && state.sort !== "oldest") return list.sort((a, b) => trendingScore(b) - trendingScore(a) || articleTimestamp(b) - articleTimestamp(a));
686  const direction = state.sort === "oldest" ? 1 : -1;
687  return list.sort((a, b) => {
688    const timeDiff = articleTimestamp(a) - articleTimestamp(b);
689    if (timeDiff !== 0) return timeDiff * direction;
690    return (Number(a.id || 0) - Number(b.id || 0)) * direction;
691  });
692}
693
694function availableArticleYears(articles = []) {
695  const serverYears = Array.isArray(state.facets?.years) ? state.facets.years.map(String).filter(Boolean) : [];
696  if (serverYears.length) {
697    if (state.year && !serverYears.includes(state.year)) serverYears.push(state.year);
698    return [...new Set(serverYears)].sort((a, b) => Number(b) - Number(a));
699  }
700  const years = new Set();
701  articles.forEach((article) => {
702    if (!articleBelongsToCurrentView(article)) return;
703    const year = articleYear(article);
704    if (year) years.add(year);
705  });
706  if (state.year) years.add(state.year);
707  return Array.from(years).sort((a, b) => Number(b) - Number(a));
708}
709
710function availableArticleMonths(articles = []) {
711  return ARTICLE_MONTHS;
712}
713
714function renderArticleControls(articles = []) {
715  document.querySelectorAll("[data-sort-control]").forEach((button) => {
716    const isActive = normalizeArticleSort(button.dataset.sortControl || "") === state.sort;
717    button.classList.toggle("active", isActive);
718    button.setAttribute("aria-pressed", String(isActive));
719  });
720  const yearSelect = document.querySelector("#articleYear");
721  if (!yearSelect) return;
722  const years = availableArticleYears(articles);
723  const signature = years.join("|");
724  if (yearSelect.dataset.yearSignature !== signature) {
725    yearSelect.innerHTML = [
726      '<option value="">All years</option>',
727      ...years.map((year) => `<option value="${escapeHtml(year)}">${escapeHtml(year)}</option>`)
728    ].join("");
729    yearSelect.dataset.yearSignature = signature;
730  }
731  yearSelect.value = state.year;
732  const monthSelect = document.querySelector("#articleMonth");
733  if (!monthSelect) return;
734  const months = availableArticleMonths(articles);
735  const monthSignature = months.map((month) => month.value).join("|");
736  if (monthSelect.dataset.monthSignature !== monthSignature) {
737    monthSelect.innerHTML = [
738      '<option value="">All months</option>',
739      ...months.map((month) => `<option value="${escapeHtml(month.value)}">${escapeHtml(month.label)}</option>`)
740    ].join("");
741    monthSelect.dataset.monthSignature = monthSignature;
742  }
743  monthSelect.value = state.month;
744}
745
746function renderPagination(totalItems = 0) {
747  const totalPages = Math.max(1, Math.ceil(totalItems / state.pageSize));
748  state.totalPages = totalPages;
749  state.page = Math.min(Math.max(1, state.page), totalPages);
750  const start = totalItems ? (state.page - 1) * state.pageSize + 1 : 0;
751  const end = Math.min(totalItems, state.page * state.pageSize);
752  const controls = document.querySelectorAll(".article-pagination");
753  const shouldDisablePrev = state.page <= 1;
754  const shouldDisableNext = state.page >= totalPages;
755  const markup = `
756    <button type="button" data-page-action="first" ${shouldDisablePrev ? "disabled" : ""}>First</button>
757    <button type="button" data-page-action="prev" ${shouldDisablePrev ? "disabled" : ""}>Prev</button>
758    <span>Page ${state.page} of ${totalPages} | ${start}-${end} of ${totalItems}</span>
759    <button type="button" data-page-action="next" ${shouldDisableNext ? "disabled" : ""}>Next</button>
760    <button type="button" data-page-action="last" ${shouldDisableNext ? "disabled" : ""}>Last</button>
761  `;
762  controls.forEach((control) => {
763    control.innerHTML = totalItems > state.pageSize ? markup : totalItems && control.classList.contains("bottom") ? `<span>${totalItems} articles</span>` : "";
764  });
765}
766
767function renderArticles(articles, errorMessage = "") {
768  const grid = document.querySelector("#newsGrid");
769  const count = document.querySelector("#articleCount");
770  renderArticleControls(articles);
771  if (!articles.length) {
772    if (count) count.textContent = errorMessage ? "Newsroom feed temporarily unavailable" : `No ${libraryLabel()} articles loaded yet`;
773    renderPagination(0);
774    grid.innerHTML = `
775      <article class="news-card empty-state">
776        <span>${errorMessage ? "Newsroom Status" : "Article Library"}</span>
777        <h3>${errorMessage ? "The newsroom feed is temporarily unavailable." : `No ${libraryLabel()} articles are available for this view yet.`}</h3>
778        <p>${
779          errorMessage
780            ? "The page is online, but the article feed did not load. Please check back shortly."
781            : "New market pulse, history, and culture articles will appear here as soon as they are published."
782        }</p>
783        <a class="action-button" href="news.html?type=evergreen">Open History Library</a>
784      </article>
785    `;
786    return;
787  }
788  const scopedArticles = articles.filter(articleBelongsToCurrentView);
789  const visibleArticles = sortArticlesForView(articles.filter(articleMatches));
790  const serverPaginated = Boolean(state.pagination && Number.isFinite(Number(state.pagination.total)));
791  const totalItems = serverPaginated ? Number(state.pagination.total) : visibleArticles.length;
792  const totalPages = Math.max(1, Math.ceil(totalItems / state.pageSize));
793  state.page = serverPaginated
794    ? Number(state.pagination.page || state.page)
795    : Math.min(Math.max(1, state.page), totalPages);
796  const pageStart = serverPaginated ? Number(state.pagination.offset || 0) : (state.page - 1) * state.pageSize;
797  const pageArticles = serverPaginated ? visibleArticles : visibleArticles.slice(pageStart, pageStart + state.pageSize);
798  renderPagination(totalItems);
799  if (count) {
800    const viewLabel = currentViewLabel();
801    const viewTotal = Number(state.counts.viewTotal || scopedArticles.length || 0);
802    const databaseTotal = Number(state.counts.databaseTotal || state.articles.length || 0);
803    const searchActive = Boolean(normalizeSearchText(state.query));
804    const searchTotalValue = Number(state.counts.searchTotal);
805    const hasSearchTotal = searchActive && Number.isFinite(searchTotalValue);
806    const totalLabel = viewTotal === databaseTotal ? `${viewTotal} in DB` : `${viewTotal} in ${viewLabel} | ${databaseTotal} total DB`;
807    const shownStart = totalItems ? pageStart + 1 : 0;
808    const shownEnd = Math.min(totalItems, pageStart + pageArticles.length);
809    const shownLabel = totalItems > state.pageSize ? `${shownStart}-${shownEnd} shown` : `${totalItems} shown`;
810    if (hasSearchTotal) {
811      const matchNoun = searchTotalValue === 1 ? "match" : "matches";
812      count.textContent = `${shownLabel} | ${searchTotalValue} DB ${matchNoun} | ${databaseTotal} total DB`;
813    } else {
814      const loadedLabel = serverPaginated ? "" : state.readerFilter !== "all" || state.query || viewTotal > scopedArticles.length ? ` | ${scopedArticles.length} loaded` : "";
815      count.textContent = `${shownLabel}${loadedLabel} | ${totalLabel}`;
816    }
817  }
818  if (!visibleArticles.length) {
819    renderPagination(0);
820    const hasSearch = Boolean(normalizeSearchText(state.query));
821    const searchTotalValue = Number(state.counts.searchTotal);
822    const searchContext = hasSearch && Number.isFinite(searchTotalValue)
823      ? `Searched the full ${Number(state.counts.databaseTotal || 0).toLocaleString()}-article database for "${escapeHtml(state.query)}".`
824      : "Clear the search or switch the reader-value filter to see the full library.";
825    grid.innerHTML = `
826      <article class="news-card empty-state">
827        <span>Filter Check</span>
828        <h3>${hasSearch ? "No database matches for that search yet." : "No articles match that filter yet."}</h3>
829        <p>${searchContext}</p>
830        <div class="news-empty-actions">
831          ${hasSearch ? `<button type="button" class="action-button" data-clear-search>Clear Search</button>` : `<a class="action-button" href="news.html?type=evergreen">Open History Library</a>`}
832          <a class="action-button secondary-action" href="news.html">All Articles</a>
833          <a class="action-button secondary-action" href="news.html?type=evergreen">History</a>
834        </div>
835      </article>
836    `;
837    bindClearSearch();
838    return;
839  }
840  grid.innerHTML = pageArticles
841    .map(
842      (article) => {
843        const href = article.canonicalPath || `/news/${article.slug}`;
844        const thumbnail = articleThumbnailSource(article);
845        const signal = articleSignal(article);
846        const updateStatus = newsUpdateStatus(article, signal);
847        const sport = String(article.sport || "").trim().toUpperCase();
848        const sportParam = sportQuery(sport);
849        const mediaClasses = [
850          "news-card-media",
851          thumbnail.hasEmbeddedText ? "has-authored-art" : "",
852          thumbnail.isHologramArt ? "has-hologram-art" : "",
853          isPlayerSuperstarArticle(article) ? "has-player-portrait" : ""
854        ].filter(Boolean).join(" ");
855        return `
856        <article class="news-card" data-article-href="${escapeHtml(href)}" data-article-title="${escapeHtml(article.title)}" tabindex="0" role="link" aria-label="Open article: ${escapeHtml(article.title)}">
857          ${
858            thumbnail.src
859              ? `<div class="${mediaClasses}">
860                  <img src="${escapeHtml(thumbnail.src)}" ${
861                    thumbnail.fallback ? `data-fallback-src="${escapeHtml(thumbnail.fallback)}"` : ""
862                  }${newsFallbackVisualAttributes(thumbnail)} alt="${escapeHtml(article.imageAlt || article.title)}" loading="lazy" decoding="async" width="640" height="360" />
863                  ${thumbnail.hasEmbeddedText || thumbnail.isHologramArt ? "" : renderNewsMediaOverlay(article, signal)}
864                  ${renderNewsTeamLogoBadge(article, { authoredArt: thumbnail.hasEmbeddedText, hologramBack: thumbnail.isHologramArt, suppressLeagueFallback: thumbnail.isHologramArt })}
865                </div>`
866              : ""
867          }
868          <div class="news-card-body">
869            <span class="news-card-kicker">${escapeHtml(article.sport || "Sports")} | ${escapeHtml(formatPostedAt(article.publishedAt || article.createdAt))}</span>
870            <div class="news-card-alert-row">
871              ${updateStatus ? `<strong class="article-update-badge">${escapeHtml(updateStatus)}</strong>` : ""}
872              ${signal ? `<strong class="article-signal-badge">${escapeHtml(signal.label)} ${Number(signal.score || 0)}</strong>` : ""}
873            </div>
874            ${renderArticleConfidenceBadges(article, signal)}
875            <h3>${escapeHtml(article.title)}</h3>
876            <p>${escapeHtml(newsCardHookText(article))}</p>
877            <p class="news-card-payoff">${escapeHtml(newsCardPayoffLine(article))}</p>
878            <div class="news-card-meta">
879              <b>${escapeHtml(article.section)}</b>
880              <b>${Number(article.wordCount || 0).toLocaleString()} words</b>
881            </div>
882            <div class="card-action-row">
883              <a class="action-button" href="${escapeHtml(href)}" data-key-event="article_read_start" data-key-target="${escapeHtml(article.slug || article.title || href)}">${escapeHtml(newsCardPrimaryLabel(article))}</a>
884              <a class="action-button secondary-action" href="picks.html${sportParam}" data-key-event="game_brief_open" data-key-target="news-card-brief-${escapeHtml(article.slug || sport || "all")}">Open board</a>
885              <a class="action-button secondary-action" href="${escapeHtml(sportDirectoryHref(sport))}" data-key-event="directory_open" data-key-target="news-card-directory-${escapeHtml(article.slug || sport || "all")}">Sport hub</a>
886            </div>
887          </div>
888        </article>
889      `;
890      }
891    )
892    .join("");
893  bindImageFallbacks();
894  bindArticleCards();
895  window.DMLVisualAssets?.applyAll?.();
896  window.DMLSportIdentity?.applyAll?.();
897  window.DMLSportIdentity?.ready?.then(() => window.DMLSportIdentity?.applyAll?.()).catch(() => {});
898}
899
900function bindClearSearch() {
901  document.querySelectorAll("[data-clear-search]").forEach((button) => {
902    button.addEventListener("click", () => {
903      const search = document.querySelector("#articleSearch");
904      state.query = "";
905      state.page = 1;
906      if (search) search.value = "";
907      syncNewsControlUrl();
908      loadNews();
909      search?.focus();
910    });
911  });
912}
913
914function bindImageFallbacks() {
915  document.querySelectorAll("img[data-fallback-src]").forEach((image) => {
916    image.addEventListener("error", () => {
917      if (image.dataset.fallbackUsed === "true") return;
918      const fallback = image.dataset.fallbackSrc;
919      if (!fallback) return;
920      image.dataset.fallbackUsed = "true";
921      image.src = fallback;
922    });
923  });
924}
925
926function openArticleCard(href, event) {
927  if (!href) return;
928  if (typeof window.trackDmlKeyEvent === "function") {
929    const card = event?.currentTarget || event?.target?.closest?.("[data-article-href]");
930    window.trackDmlKeyEvent("article_read_start", card?.dataset?.articleTitle || href, {
931      href,
932      text: card?.dataset?.articleTitle || "Article card",
933      source: "news-card"
934    });
935  }
936  if (event?.metaKey || event?.ctrlKey) {
937    window.open(href, "_blank", "noopener");
938    return;
939  }
940  window.location.href = href;
941}
942
943function bindArticleCards() {
944  document.querySelectorAll(".news-card[data-article-href]").forEach((card) => {
945    card.addEventListener("click", (event) => {
946      if (event.target.closest("a, button, input, textarea, select, label")) return;
947      openArticleCard(card.dataset.articleHref, event);
948    });
949    card.addEventListener("keydown", (event) => {
950      if (event.target.closest("a, button, input, textarea, select, label") && event.target !== card) return;
951      if (event.key !== "Enter" && event.key !== " ") return;
952      event.preventDefault();
953      openArticleCard(card.dataset.articleHref, event);
954    });
955  });
956}
957
958function setSearchFromUrl(url = new URL(location.href)) {
959  state.query = cleanInlineText(url.searchParams.get("q") || "").slice(0, 120);
960  state.sort = activeArticleSort(url);
961  state.year = activeArticleYear(url);
962  state.month = activeArticleMonth(url);
963  const search = document.querySelector("#articleSearch");
964  if (search) search.value = state.query;
965  renderArticleControls(state.articles);
966}
967
968function syncNewsControlUrl({ replace = true } = {}) {
969  const url = new URL(location.href);
970  if (state.query.trim()) url.searchParams.set("q", state.query.trim());
971  else url.searchParams.delete("q");
972  if (state.sort === "oldest") url.searchParams.set("sort", "oldest");
973  else url.searchParams.delete("sort");
974  if (state.year) url.searchParams.set("year", state.year);
975  else url.searchParams.delete("year");
976  if (state.month) url.searchParams.set("month", state.month);
977  else url.searchParams.delete("month");
978  const next = `${url.pathname}${url.search}${url.hash}`;
979  if (next === `${location.pathname}${location.search}${location.hash}`) return;
980  const method = replace ? "replaceState" : "pushState";
981  history[method]({ newsFilter: true }, "", next);
982}
983
984function scrollNewsResultsIntoView() {
985  const panel = document.querySelector(".news-results-panel");
986  if (!panel) return;
987  const toolbar = document.querySelector(".site-toolbar");
988  const toolbarBottom = Math.max(0, toolbar?.getBoundingClientRect?.().bottom || 0);
989  const top = Math.max(0, window.scrollY + panel.getBoundingClientRect().top - toolbarBottom - 12);
990  window.scrollTo({ top, behavior: "smooth" });
991}
992
993function bindNewsTabLinks() {
994  document.querySelectorAll(".news-tabs a").forEach((link) => {
995    link.addEventListener("click", (event) => {
996      if (event.defaultPrevented || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button !== 0) return;
997      const target = new URL(link.href, location.href);
998      if (target.origin !== location.origin || target.pathname !== location.pathname) return;
999      event.preventDefault();
1000      if (state.sort === "oldest") target.searchParams.set("sort", "oldest");
1001      if (state.year) target.searchParams.set("year", state.year);
1002      if (state.month) target.searchParams.set("month", state.month);
1003      history.pushState({ newsFilter: true }, "", `${target.pathname}${target.search}${target.hash}`);
1004      setSearchFromUrl(target);
1005      state.page = 1;
1006      loadNews();
1007    });
1008  });
1009  window.addEventListener("popstate", () => {
1010    setSearchFromUrl();
1011    state.page = 1;
1012    loadNews();
1013  });
1014}
1015
1016async function loadNews() {
1017  const requestId = ++newsRequestId;
1018  const sport = activeSport();
1019  const type = activeType();
1020  const signal = activeSignal();
1021  const section = activeSection();
1022  const lane = activeLane();
1023  const laneMeta = activeLaneMeta();
1024  const title = document.querySelector("#newsTitle");
1025  if (section) title.textContent = sport ? `${sport} ${section} Articles` : `${section} Articles`;
1026  else if (signal === "breaking") title.textContent = sport ? `${sport} Breaking News` : "Breaking News";
1027  else if (signal === "trending") title.textContent = sport ? `${sport} Trending Articles` : "Trending Articles";
1028  else if (laneMeta) title.textContent = `${laneMeta.label} Articles`;
1029  else if (type === "draft") title.textContent = sport ? `${sport} Draft Lab` : "Draft Lab";
1030  else if (type === "evergreen" && sport === "CULTURE") title.textContent = "Sports Culture Articles";
1031  else if (type === "evergreen") title.textContent = sport ? `${sport} Historical Features` : "Historical Features";
1032  else if (type === "daily") title.textContent = sport ? `${sport} Market Pulse Articles` : "Latest Market Pulse Articles";
1033  else if (type === "archive") title.textContent = sport ? `${sport} Waiver Receipt Archive` : "Waiver Receipt Archive";
1034  else title.textContent = sport ? `All ${sport} Articles` : "All Articles";
1035  let activeNewsTab = null;
1036  document.querySelectorAll(".news-tabs a").forEach((link) => {
1037    const linkSport = new URL(link.href, location.href).searchParams.get("sport")?.toUpperCase() || "";
1038    const rawType = new URL(link.href, location.href).searchParams.get("type");
1039    const linkType = rawType === "draft" ? "draft" : rawType === "evergreen" ? "evergreen" : rawType === "daily" ? "daily" : rawType === "archive" || rawType === "waiver" ? "archive" : "all";
1040    const linkSignal = new URL(link.href, location.href).searchParams.get("signal") || "";
1041    const linkSection = cleanInlineText(new URL(link.href, location.href).searchParams.get("section") || "").slice(0, 80);
1042    const linkLane = laneFilterFor(new URL(link.href, location.href).searchParams.get("lane") || "")?.routePrefix || "";
1043    const isActive = linkSport === sport && linkType === type && linkSignal === signal && linkSection === section && linkLane === lane;
1044    link.classList.toggle("active", isActive);
1045    if (isActive) link.setAttribute("aria-current", "page");
1046    else link.removeAttribute("aria-current");
1047    if (isActive) activeNewsTab = link;
1048  });
1049  const activeGroup = activeNewsTab?.closest(".news-tab-group");
1050  if (activeGroup && activeGroup.scrollWidth > activeGroup.clientWidth) {
1051    activeGroup.scrollLeft = Math.max(0, activeNewsTab.offsetLeft - activeGroup.clientWidth / 2 + activeNewsTab.offsetWidth / 2);
1052  }
1053  try {
1054    const params = new URLSearchParams();
1055    const query = state.query.trim();
1056    if (sport) params.set("sport", sport);
1057    if (type === "evergreen" || type === "draft" || type === "daily" || type === "archive") params.set("type", type);
1058    if (section) params.set("section", section);
1059    if (lane) params.set("lane", lane);
1060    if (state.sort === "oldest") params.set("sort", "oldest");
1061    if (state.year) params.set("year", state.year);
1062    if (state.month) params.set("month", state.month);
1063    if (query) params.set("q", query);
1064    if (state.readerFilter !== "all") params.set("reader", state.readerFilter);
1065    if (signal) {
1066      params.set("limit", "60");
1067    } else {
1068      params.set("page", String(state.page));
1069      params.set("pageSize", String(state.pageSize));
1070    }
1071    const response = await fetch(`/api/news${params.toString() ? `?${params}` : ""}`);
1072    if (!response.ok) throw new Error("News failed");
1073    const { articles, counts, pagination, facets } = await response.json();
1074    if (requestId !== newsRequestId) return;
1075    state.articles = articles || [];
1076    state.counts = counts || {};
1077    state.pagination = pagination || null;
1078    state.facets = facets || { years: [] };
1079    renderArticles(state.articles);
1080  } catch {
1081    if (requestId !== newsRequestId) return;
1082    state.pagination = null;
1083    state.facets = { years: [] };
1084    renderArticles([], "api-error");
1085  }
1086}
1087
1088function bindFilters() {
1089  const search = document.querySelector("#articleSearch");
1090  if (search && state.query) search.value = state.query;
1091  search?.addEventListener("input", () => {
1092    state.query = search.value.trim();
1093    state.page = 1;
1094    const count = document.querySelector("#articleCount");
1095    if (count) count.textContent = state.query ? "Searching full article database..." : "Loading articles";
1096    window.clearTimeout(searchDebounceTimer);
1097    searchDebounceTimer = window.setTimeout(() => {
1098      syncNewsControlUrl();
1099      loadNews();
1100    }, state.query ? 220 : 0);
1101  });
1102  document.querySelectorAll("[data-sort-control]").forEach((button) => {
1103    button.addEventListener("click", () => {
1104      const nextSort = normalizeArticleSort(button.dataset.sortControl || "");
1105      if (state.sort === nextSort) return;
1106      state.sort = nextSort;
1107      state.page = 1;
1108      renderArticleControls(state.articles);
1109      const count = document.querySelector("#articleCount");
1110      if (count) count.textContent = nextSort === "oldest" ? "Loading archive floor..." : "Loading latest articles...";
1111      syncNewsControlUrl();
1112      loadNews();
1113    });
1114  });
1115  const yearSelect = document.querySelector("#articleYear");
1116  yearSelect?.addEventListener("change", () => {
1117    state.year = normalizeArticleYear(yearSelect.value);
1118    state.page = 1;
1119    renderArticleControls(state.articles);
1120    const count = document.querySelector("#articleCount");
1121    if (count) count.textContent = state.year ? `Loading ${state.year} articles...` : "Loading all years...";
1122    syncNewsControlUrl();
1123    loadNews();
1124  });
1125  const monthSelect = document.querySelector("#articleMonth");
1126  monthSelect?.addEventListener("change", () => {
1127    state.month = normalizeArticleMonth(monthSelect.value);
1128    state.page = 1;
1129    renderArticleControls(state.articles);
1130    const monthLabel = ARTICLE_MONTHS.find((month) => month.value === state.month)?.label || "";
1131    const count = document.querySelector("#articleCount");
1132    if (count) count.textContent = state.month ? `Loading ${monthLabel} articles...` : "Loading all months...";
1133    syncNewsControlUrl();
1134    loadNews();
1135  });
1136  document.querySelectorAll("[data-local-filter]").forEach((button) => {
1137    button.addEventListener("click", () => {
1138      state.readerFilter = button.dataset.localFilter || "all";
1139      state.page = 1;
1140      document.querySelectorAll("[data-local-filter]").forEach((item) => {
1141        item.classList.toggle("active", item === button);
1142      });
1143      loadNews();
1144    });
1145    button.classList.toggle("active", button.dataset.localFilter === state.readerFilter);
1146  });
1147  document.querySelectorAll(".article-pagination").forEach((control) 
1147=> {
1148    control.addEventListener("click", async (event) => {
1149      const button = event.target.closest("[data-page-action]");
1150      if (!button) return;
1151      const action = button.dataset.pageAction;
1152      if (action === "first") state.page = 1;
1153      else if (action === "last") state.page = state.totalPages;
1154      else state.page += action === "next" ? 1 : -1;
1155      if (state.pagination) await loadNews();
1156      else renderArticles(state.articles);
1157      scrollNewsResultsIntoView();
1158    });
1159  });
1160  bindNewsTabLinks();
1161}
1162
1163bindFilters();
1164loadNews();

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.