function reportClientError(message, details = {}) {
  if (typeof fetch !== "function" || typeof location === "undefined") return;
  const cleanMessage = String(message || "Client error").replace(/\s+/g, " ").trim().slice(0, 300);
  const payload = {
    page: location.pathname || "unknown",
    message: cleanMessage || "Client error",
    details
  };
  fetch("/api/client-error", {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify(payload),
    keepalive: true
  }).catch(() => {});
}

if (typeof window !== "undefined" && !window.__libertyRaceworksErrorGuard) {
  window.__libertyRaceworksErrorGuard = true;
  window.addEventListener("error", event => {
    reportClientError(event.message, {
      source: event.filename || "",
      line: event.lineno || 0,
      column: event.colno || 0
    });
  });
  window.addEventListener("unhandledrejection", event => {
    const reason = event.reason;
    reportClientError(reason && reason.message ? reason.message : reason || "Unhandled promise rejection", {
      source: "unhandledrejection"
    });
  });
}

async function postJson(url, data) {
  const response = await fetch(url, {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify(data)
  });
  const text = await response.text();
  let result = {};
  try {
    result = text ? JSON.parse(text) : {};
  } catch (_err) {
    result = { ok: false, error: text || "Server returned an unreadable response." };
  }
  if (!result || typeof result !== "object" || Array.isArray(result)) {
    result = { ok: response.ok, data: result };
  }
  if (!("ok" in result)) result.ok = response.ok;
  if (!response.ok && !result.error) result.error = `Request failed (${response.status}).`;
  return result;
}

const STYLE_PROFILE_VERSION = "1";
const STYLE_PROFILE_ALIASES = new Set([STYLE_PROFILE_VERSION, "2026-05-16"]);

const defaultStyleSettings = {
  schemaVersion: 1,
  styleProfileVersion: STYLE_PROFILE_VERSION,
  accentColor: "#e7332f",
  numberColor: "#f4c542",
  nameColor: "#ffffff",
  metricColor: "#cfd4d9",
  graphicBackgroundColor: "#071016",
  graphicPanelColor: "#242929",
  graphicBorderColor: "#050505",
  graphicBadgeColor: "#050505",
  graphicBadgeTextColor: "#ffffff",
  graphicLightTextColor: "#111111",
  graphicLightSubtextColor: "#252525",
  titleSize: 88,
  rowNameSize: 28,
  leaderNameSize: 34,
  boardScale: 0.92,
  boardOffsetY: 72,
  panelOpacity: 0.92,
  competitorNameFormat: "first_initial_last",
  lineupMetricSource: "class",
  footerSource: "class",
  manualTrackName: "",
  manualSession: "",
  bannerDataOpen: true,
  bannerTopFiveOpen: true,
  bannerHeaderLogoVersion: 0,
  bannerSponsorLogoVersion: 0,
  bannerHeaderLogoCount: 0,
  bannerSponsorLogoCount: 0,
  bannerLogoRotateSeconds: 8,
  bannerMetricFields: ["lastLap", "gap"],
  bannerMetricRotateSeconds: 4,
  bannerTemplate: "reference-premium",
  bannerInfoSource: "class",
  bannerManualInfo: "",
  bannerDurationMode: "lapsToGo",
  bannerRaceMode: "auto",
  bannerModeChipOpen: true,
  bannerAccentLinesOpen: true,
  bannerTextureOpen: true,
  bannerReflectionOpen: true,
  bannerBevelOpen: true,
  bannerLogoPanelColor: "#d7dad7",
  bannerDarkCellColor: "#242929",
  bannerLightCellColor: "#e5e9ea",
  bannerFlyoutColor: "#5a6060",
  bannerAccentLineColor: "#e7332f",
  bannerOpacity: 1,
  bannerFlyoutOpacity: 1,
  bannerGlassOpen: true,
  bannerMomentsOpen: true,
  bannerNameFormat: "first_initial_last",
  bannerNameSize: 20,
  bannerMetricSize: 17,
  bannerInfoSize: 22,
  bannerDurationSize: 22,
  top3TitleSource: "class",
  top3Title: "Top 3",
  top3Subtitle: "",
  top3Source: "qualifying",
  top3Layout: "photo",
  top3Drivers: [
    { name: "", number: "" },
    { name: "", number: "" },
    { name: "", number: "" }
  ],
  top3PhotoVersions: [0, 0, 0],
  missingPlaceholder: "dash",
  weatherLocationOverride: "",
  tickerSource: "manual",
  tickerText: "",
  tickerSpeedSeconds: 28,
  raceStoryPosition: "bottom-center",
  raceStoryAutoUpdate: true,
  raceStoryHeldTitle: "",
  raceStoryHeldDetail: "",
  raceStoryHeldTone: "",
  raceStoryHeldType: "",
  raceStoryHeldClass: "",
  raceStoryHeldMode: "",
  raceStoryHeldKey: ""
};

function migrateStyleSettings(source = {}) {
  const sourceObject = source && typeof source === "object" && !Array.isArray(source) ? source : {};
  const savedVersion = String(sourceObject.styleProfileVersion || "");
  const useSavedValues = !savedVersion || STYLE_PROFILE_ALIASES.has(savedVersion);
  const settings = useSavedValues ? { ...defaultStyleSettings, ...sourceObject } : { ...defaultStyleSettings };
  settings.styleProfileVersion = STYLE_PROFILE_VERSION;
  if (settings.titleSize === 72) settings.titleSize = defaultStyleSettings.titleSize;
  if (settings.top3Subtitle === "Drivers Rolling Out") settings.top3Subtitle = "";
  settings.bannerMetricFields = Array.isArray(settings.bannerMetricFields)
    ? [...settings.bannerMetricFields]
    : [...defaultStyleSettings.bannerMetricFields];
  settings.top3Drivers = Array.isArray(settings.top3Drivers)
    ? settings.top3Drivers.slice(0, 3).map(driver => ({
      name: driver && driver.name ? String(driver.name) : "",
      number: driver && driver.number ? String(driver.number) : ""
    }))
    : defaultStyleSettings.top3Drivers.map(driver => ({ ...driver }));
  while (settings.top3Drivers.length < 3) settings.top3Drivers.push({ name: "", number: "" });
  settings.top3PhotoVersions = Array.isArray(settings.top3PhotoVersions)
    ? [...settings.top3PhotoVersions].slice(0, 3)
    : [...defaultStyleSettings.top3PhotoVersions];
  while (settings.top3PhotoVersions.length < 3) settings.top3PhotoVersions.push(0);
  return settings;
}

function getStyleSettings() {
  try {
    const savedSettings = JSON.parse(localStorage.getItem("raceGraphics.style") || "{}");
    return migrateStyleSettings(savedSettings);
  } catch (_err) {
    return migrateStyleSettings();
  }
}

function saveStyleSettings(settings) {
  const nextSettings = migrateStyleSettings(settings);
  const serialized = JSON.stringify(nextSettings);
  try {
    localStorage.setItem("raceGraphics.style", serialized);
  } catch (_err) {}
  applyStyleSettings(nextSettings);
  try {
    window.dispatchEvent(new StorageEvent("storage", { key: "raceGraphics.style", newValue: serialized }));
  } catch (_err) {}
  return nextSettings;
}

function styleSettingsFromServerResult(result) {
  const candidate = result && result.style && typeof result.style === "object"
    ? result.style
    : result;
  return migrateStyleSettings(candidate);
}

function applyStyleSettings(settings = getStyleSettings()) {
  const root = document.documentElement;
  const clampOpacity = value => Math.min(1, Math.max(0.2, Number.isFinite(Number(value)) ? Number(value) : 1));
  const bannerOpacity = clampOpacity(settings.bannerOpacity ?? defaultStyleSettings.bannerOpacity);
  const bannerFlyoutOpacity = clampOpacity(settings.bannerFlyoutOpacity ?? defaultStyleSettings.bannerFlyoutOpacity);
  const panelOpacity = Math.min(1, Math.max(0.35, Number.isFinite(Number(settings.panelOpacity)) ? Number(settings.panelOpacity) : defaultStyleSettings.panelOpacity));
  const lightThemeActive = settings.bannerTemplate === "light-theme";
  const darkThemeActive = settings.bannerTemplate === "reference-premium";
  const raceStoryPosition = settings.raceStoryPosition === "bottom-left" ? "bottom-left" : "bottom-center";
  const effectiveBannerOpacity = lightThemeActive ? 0.86 + bannerOpacity * 0.14 : bannerOpacity;
  const effectiveFlyoutOpacity = lightThemeActive ? 0.86 + bannerFlyoutOpacity * 0.14 : bannerFlyoutOpacity;
  document.body?.classList.toggle("banner-data-hidden", settings.bannerDataOpen === false);
  document.body?.classList.toggle("banner-glass", settings.bannerGlassOpen !== false);
  document.body?.classList.toggle("graphics-theme-dark", darkThemeActive);
  document.body?.classList.toggle("graphics-theme-light", lightThemeActive);
  document.body?.classList.toggle("graphics-theme-classic", !darkThemeActive && !lightThemeActive);
  document.body?.classList.toggle("race-story-position-bottom-left", raceStoryPosition === "bottom-left");
  document.body?.classList.toggle("race-story-position-bottom-center", raceStoryPosition !== "bottom-left");
  if (document.body?.classList.contains("control-body")) return;
  root.style.setProperty("--accent", settings.accentColor);
  root.style.setProperty("--gold", settings.numberColor);
  root.style.setProperty("--name-color", settings.nameColor);
  root.style.setProperty("--metric-color", settings.metricColor);
  root.style.setProperty("--graphic-background-color", settings.graphicBackgroundColor || defaultStyleSettings.graphicBackgroundColor);
  root.style.setProperty("--graphic-panel-color", settings.graphicPanelColor || settings.bannerDarkCellColor || defaultStyleSettings.graphicPanelColor);
  root.style.setProperty("--graphic-border-color", settings.graphicBorderColor || defaultStyleSettings.graphicBorderColor);
  root.style.setProperty("--graphic-badge-color", settings.graphicBadgeColor || defaultStyleSettings.graphicBadgeColor);
  root.style.setProperty("--graphic-badge-text-color", settings.graphicBadgeTextColor || defaultStyleSettings.graphicBadgeTextColor);
  root.style.setProperty("--graphic-light-text-color", settings.graphicLightTextColor || defaultStyleSettings.graphicLightTextColor);
  root.style.setProperty("--graphic-light-subtext-color", settings.graphicLightSubtextColor || defaultStyleSettings.graphicLightSubtextColor);
  root.style.setProperty("--board-title-size", `${settings.titleSize}px`);
  root.style.setProperty("--board-row-name-size", `${settings.rowNameSize}px`);
  root.style.setProperty("--leader-name-size", `${settings.leaderNameSize}px`);
  root.style.setProperty("--board-scale", settings.boardScale);
  root.style.setProperty("--board-offset-y", `${settings.boardOffsetY}px`);
  root.style.setProperty("--panel-alpha", panelOpacity);
  root.style.setProperty("--panel-alpha-percent", `${Math.round(panelOpacity * 1000) / 10}%`);
  root.style.setProperty("--banner-name-size", `${settings.bannerNameSize || defaultStyleSettings.bannerNameSize}px`);
  root.style.setProperty("--banner-metric-size", `${settings.bannerMetricSize || defaultStyleSettings.bannerMetricSize}px`);
  root.style.setProperty("--banner-info-size", `${settings.bannerInfoSize || defaultStyleSettings.bannerInfoSize}px`);
  root.style.setProperty("--banner-duration-size", `${settings.bannerDurationSize || defaultStyleSettings.bannerDurationSize}px`);
  root.style.setProperty("--banner-logo-panel-color", settings.bannerLogoPanelColor || defaultStyleSettings.bannerLogoPanelColor);
  root.style.setProperty("--banner-dark-cell-color", settings.bannerDarkCellColor || defaultStyleSettings.bannerDarkCellColor);
  root.style.setProperty("--banner-light-cell-color", settings.bannerLightCellColor || defaultStyleSettings.bannerLightCellColor);
  root.style.setProperty("--banner-flyout-color", settings.bannerFlyoutColor || defaultStyleSettings.bannerFlyoutColor);
  root.style.setProperty("--banner-accent-line-color", settings.bannerAccentLineColor || settings.accentColor || defaultStyleSettings.bannerAccentLineColor);
  root.style.setProperty("--banner-panel-alpha", effectiveBannerOpacity);
  root.style.setProperty("--banner-flyout-alpha", effectiveFlyoutOpacity);
}

applyStyleSettings();
window.addEventListener("storage", event => {
  if (event.key === "raceGraphics.style") applyStyleSettings();
});

function loadServerStyle() {
  return fetch("/api/style", { cache: "no-store" })
    .then(res => res.json())
    .then(result => {
      const settings = styleSettingsFromServerResult(result);
      saveStyleSettings(settings);
      return settings;
    })
    .catch(() => getStyleSettings());
}

async function saveServerStyle(settings) {
  const result = await postJson("/api/style", settings);
  if (result.ok) saveStyleSettings(styleSettingsFromServerResult(result));
  return result;
}

// One shared /events stream per page. Browsers cap connections per origin at 6,
// so the timing client and the program router must reuse a single connection.
function lrwSharedEventsStream() {
  const current = window.__lrwEventsStream;
  if (current && current.readyState !== 2) return current;
  window.__lrwEventsStream = new EventSource("/events");
  return window.__lrwEventsStream;
}

// Embedded pages (control-center preview iframes, glass-preview panes) must
// not open their own /events streams: Chrome budgets ~6 connections per
// origin across ALL tabs, and a control page full of live previews can
// exhaust it - sends and stylesheet loads then queue behind the streams
// (the 2026-06-11 "stuck program" and flip-flopping theme failures). Each
// top-level page keeps one shared stream and relays every message on a
// BroadcastChannel; embedded pages listen there (answering a ping makes the
// handshake work on an idle server) and only fall back to a direct stream
// when nobody is relaying.
function lrwIsEmbedded() {
  try {
    return window.self !== window.top;
  } catch (_err) {
    return true;
  }
}

function lrwStateRelayChannel() {
  if (typeof BroadcastChannel !== "function") return null;
  if (!window.__lrwStateRelay) window.__lrwStateRelay = new BroadcastChannel("raceGraphics.state");
  return window.__lrwStateRelay;
}

function connectState(render) {
  let closed = false;
  let inFlight = false;
  let lastStyleJson = "";
  let fallbackTimer = null;
  let eventSource = null;
  let directStarted = false;
  let channelDataSeen = false;

  function applyState(state) {
    if (!state || typeof state !== "object") return;
    if (state.style) {
      const styleJson = JSON.stringify(state.style);
      if (styleJson !== lastStyleJson) {
        lastStyleJson = styleJson;
        saveStyleSettings(state.style);
      }
    }
    render(state);
  }

  async function refresh() {
    if (closed || inFlight) return;
    inFlight = true;
    try {
      const response = await fetch(`/api/state?t=${Date.now()}`, { cache: "no-store" });
      applyState(await response.json());
    } catch (_err) {
      // Keep the graphics alive if the local server is restarted during a show.
    } finally {
      inFlight = false;
    }
  }

  function startFallbackPolling() {
    if (fallbackTimer || closed) return;
    refresh();
    fallbackTimer = setInterval(refresh, 3000);
  }

  function stopFallbackPolling() {
    if (fallbackTimer) {
      clearInterval(fallbackTimer);
      fallbackTimer = null;
    }
  }

  function startDirectStream() {
    if (directStarted || closed) return;
    directStarted = true;
    if (typeof EventSource === "function") {
      try {
        eventSource = lrwSharedEventsStream();
        const relay = !lrwIsEmbedded() ? lrwStateRelayChannel() : null;
        let lastState = null;
        if (relay && !relay.__lrwPingHandler) {
          // Answer presence pings so embedded pages joining on an idle server
          // still learn someone is relaying (and get the latest state).
          relay.__lrwPingHandler = true;
          relay.addEventListener("message", event => {
            if (event.data === "lrw-state-ping" && lastState) {
              try { relay.postMessage({ kind: "lrw-state", state: lastState }); } catch (_err) {}
            }
          });
        }
        eventSource.addEventListener("message", event => {
          if (closed) return;
          try {
            const state = JSON.parse(event.data);
            applyState(state);
            if (relay) {
              lastState = state;
              try { relay.postMessage({ kind: "lrw-state", state }); } catch (_err) {}
            }
          } catch (_err) {}
        });
        // Leave the stream open so the browser keeps retrying on its own; poll only
        // while it is down, then stop once the stream comes back.
        eventSource.addEventListener("error", () => {
          if (closed) return;
          startFallbackPolling();
        });
        eventSource.addEventListener("open", () => {
          if (closed) return;
          stopFallbackPolling();
        });
      } catch (_err) {
        startFallbackPolling();
      }
    } else {
      startFallbackPolling();
    }
  }

  if (lrwIsEmbedded() && typeof BroadcastChannel === "function") {
    const listenChannel = new BroadcastChannel("raceGraphics.state");
    listenChannel.addEventListener("message", event => {
      if (closed) return;
      const message = event.data;
      if (!message || message.kind !== "lrw-state") return;
      channelDataSeen = true;
      stopFallbackPolling();
      applyState(message.state);
    });
    refresh();
    try { listenChannel.postMessage("lrw-state-ping"); } catch (_err) {}
    setTimeout(() => {
      if (!closed && !channelDataSeen) startDirectStream();
    }, 2500);
    return {
      close() {
        closed = true;
        if (fallbackTimer) clearInterval(fallbackTimer);
        try { listenChannel.close(); } catch (_err) {}
      }
    };
  }

  startDirectStream();

  return {
    close() {
      closed = true;
      // The stream is shared with other subscribers on the page, so detach by
      // flag rather than closing the connection.
      if (fallbackTimer) clearInterval(fallbackTimer);
    }
  };
}

function revealGraphicWhenReady() {
  if (!document.body.classList.contains("graphic-prep")) return;
  requestAnimationFrame(() => {
    document.body.classList.remove("graphic-prep");
    document.body.classList.add("graphic-ready");
  });
}

function isMissingDisplayValue(value) {
  const text = String(value == null ? "" : value).trim();
  if (!text) return true;
  const normalized = text.toLowerCase().replace(/[_-]+/g, " ").replace(/\s+/g, " ");
  return [
    "missing data",
    "no data",
    "n/a",
    "na",
    "null",
    "undefined",
    "none",
    "-",
    "--"
  ].includes(normalized) || /\bmissing data\b/i.test(text);
}

function displayText(value, fallback = "") {
  return isMissingDisplayValue(value) ? fallback : String(value).trim();
}

function raceStoryTickerMessage(state = {}) {
  const story = state.raceStory || {};
  const raceState = state.raceState || {};
  const meta = state.meta || {};
  const seen = new Set();
  const parts = [];
  const add = value => {
    const text = displayText(value);
    const key = text.toLowerCase();
    if (!text || seen.has(key)) return;
    seen.add(key);
    parts.push(text);
  };
  (story.items || []).slice(0, 4).forEach(item => {
    const title = displayText(item.title);
    const detail = displayText(item.detail);
    if (title && detail) add(`${title}: ${detail}`);
    else add(title || detail);
  });
  if (!parts.length && story.leader?.name) {
    const number = displayText(story.leader.number);
    add(`Leader: ${number ? `#${number} ` : ""}${story.leader.name}`);
  }
  return parts.join("     |     ");
}

function raceStoryHeldItem(settings = getStyleSettings()) {
  const title = displayText(settings.raceStoryHeldTitle);
  const detail = displayText(settings.raceStoryHeldDetail);
  if (!title && !detail) return null;
  return {
    title: title || "Race Story",
    detail,
    tone: displayText(settings.raceStoryHeldTone, "live"),
    type: displayText(settings.raceStoryHeldType, "story"),
    className: displayText(settings.raceStoryHeldClass),
    modeLabel: displayText(settings.raceStoryHeldMode)
  };
}

function raceStoryLiveSpotlightItem(state = {}) {
  const story = state.raceStory || {};
  const primary = story.primary || (Array.isArray(story.items) ? story.items.find(item => displayText(item?.title) || displayText(item?.detail)) : null);
  if (primary) {
    return {
      title: displayText(primary.title, "Race Story"),
      detail: displayText(primary.detail),
      tone: displayText(primary.tone, "live"),
      type: displayText(primary.type, "story")
    };
  }
  if (story.leader?.name) {
    const number = displayText(story.leader.number);
    return {
      title: "Leader",
      detail: `${number ? `#${number} ` : ""}${displayText(story.leader.name)}`,
      tone: "live",
      type: "leader"
    };
  }
  return null;
}

function raceStorySpotlightItem(state = {}) {
  const settings = state.style || getStyleSettings();
  if (settings.raceStoryAutoUpdate === false) {
    const held = raceStoryHeldItem(settings);
    if (held) return held;
  }
  return raceStoryLiveSpotlightItem(state);
}

function formatLapTime(value) {
  const text = displayText(value);
  if (!text) return "";
  if (/^(?:0+|0+\.0+)$/.test(text)) return "";
  if (/^0{1,2}:0{2}:0{2}(?:\.0+)?$/.test(text)) return "";
  const longClock = text.match(/^(\d{1,2}):(\d{2}):(\d{2})(\.\d+)?$/);
  if (longClock && Number(longClock[1]) > 0) return "";
  if (longClock) {
    const minutes = Number(longClock[2]);
    const seconds = longClock[3];
    const fraction = longClock[4] || "";
    return minutes > 0 ? `${minutes}:${seconds}${fraction}` : `${Number(seconds)}${fraction}`;
  }
  const shortClock = text.match(/^(\d{1,2}):(\d{2})(\.\d+)?$/);
  if (shortClock) {
    const minutes = Number(shortClock[1]);
    const seconds = shortClock[2];
    const fraction = shortClock[3] || "";
    return minutes > 0 ? `${minutes}:${seconds}${fraction}` : `${Number(seconds)}${fraction}`;
  }
  return text;
}

function missingPlaceholderText(settings = getStyleSettings()) {
  if (settings.missingPlaceholder === "blank") return "";
  if (settings.missingPlaceholder === "tbd") return "TBD";
  return "-";
}

function splitNameFallback(value) {
  const parts = displayText(value).split(/\s+/).filter(Boolean);
  if (!parts.length) return { first: "", last: "" };
  if (parts.length === 1) return { first: parts[0], last: "" };
  return { first: parts[0], last: parts.slice(1).join(" ") };
}

function formattedDriverName(driver, format) {
  const supplied = displayText(driver.name);
  const fallback = displayText(driver.fallbackName);
  const split = splitNameFallback(supplied || fallback);
  const first = displayText(driver.firstName) || split.first;
  const last = displayText(driver.lastName) || split.last;
  const full = supplied || [first, last].filter(Boolean).join(" ") || fallback;
  const mode = format || getStyleSettings().competitorNameFormat || defaultStyleSettings.competitorNameFormat;
  if (mode === "first_initial_last" && last) return [first ? `${first.charAt(0)}.` : "", last].filter(Boolean).join(" ");
  if (mode === "full_caps" && full) return full.toUpperCase();
  if (mode === "last") return last || full;
  if (mode === "first") return first || full;
  return full;
}

function withoutClassSuffix(name, driver) {
  const text = displayText(name);
  const className = displayText(driver && driver.className);
  if (!text || !className) return text;
  return text.replace(new RegExp(`\\s+-\\s+${className.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`, "i"), "").trim();
}

function isGeneratedCarName(name, driver) {
  const text = displayText(name).toLowerCase();
  if (!text || !driver) return false;
  const generated = [driver.id, driver.transponder, driver.displayNumber]
    .map(value => displayText(value).toLowerCase())
    .filter(Boolean)
    .map(value => `car ${value}`);
  return generated.includes(text) && !displayText(driver.number);
}

function driverName(driver) {
  if (!driver) return "";
  if (driver.missingData) return missingPlaceholderText();
  const fromName = withoutClassSuffix(formattedDriverName(driver), driver);
  if (isGeneratedCarName(fromName, driver)) return missingPlaceholderText();
  if (fromName) return fromName;
  const number = driverNumber(driver);
  if (number) return `Car ${number}`;
  return driver.position ? missingPlaceholderText() : "";
}

function driverNumber(driver) {
  if (!driver) return "";
  const number = displayText(driver.number);
  const suppliedName = displayText(driver.name) || [driver.firstName, driver.lastName].map(displayText).filter(Boolean).join(" ");
  const id = displayText(driver.id);
  const transponder = displayText(driver.transponder);
  if (number) {
    const displayNumber = displayText(driver.displayNumber) || number;
    if (!suppliedName && id && id !== transponder && id !== displayNumber && id.length > displayNumber.length && id.endsWith(displayNumber)) {
      return id;
    }
    return displayNumber;
  }
  const displayNumber = displayText(driver.displayNumber);
  if (displayNumber && displayNumber !== transponder && displayNumber !== id) return displayNumber;
  return "";
}

function hasDisplayDriver(driver) {
  return Boolean(driver && (driver.position || driverName(driver) || driverNumber(driver)));
}

function hasUsableDriverData(driver) {
  if (!driver || driver.missingData) return false;
  const name = driverName(driver);
  return Boolean(driverNumber(driver) || (name && name !== missingPlaceholderText()));
}

function usableDriverRows(rows = []) {
  return (rows || []).filter(hasUsableDriverData);
}

function timingContextText(state, fallback = "Awaiting Timing Data") {
  const meta = (state && state.meta) || {};
  const session = displayText(meta.className) || displayText(meta.session) || displayText(meta.trackName);
  return session ? `${session} - ${fallback}` : fallback;
}

function renderGraphicNoData(container, state, message = "Awaiting Timing Data") {
  container.innerHTML = "";
  const node = document.createElement("div");
  node.className = "graphic-no-data";
  node.textContent = timingContextText(state, message);
  container.appendChild(node);
  return node;
}

function setOverflowMarquee(element, value, options = {}) {
  if (!element) return;
  const text = displayText(value);
  const marqueeClass = options.marqueeClass || "class-marquee";
  const trackClass = options.trackClass || "class-marquee-track";
  const fitMinSize = Number(options.fitMinSize) || 0;
  element.dataset.marqueeText = text;
  element.classList.remove(marqueeClass);
  if (fitMinSize) {
    element.style.fontSize = "";
    element.style.lineHeight = "";
  }
  element.textContent = text;
  if (!text) return;
  requestAnimationFrame(() => {
    if (!element.isConnected || element.dataset.marqueeText !== text) return;
    if (element.scrollWidth <= element.clientWidth + 4) return;
    if (fitMinSize) {
      // Shrink-to-fit before falling back to the marquee: short-lived chips
      // (8s) die before the 12s marquee loop completes, so scrolled text
      // reads as clipped at both ends for its whole appearance. Pinning the
      // line-height to the full-size value keeps the bar height steady.
      const fullSize = parseFloat(getComputedStyle(element).fontSize) || 0;
      const fitted = fullSize * (element.clientWidth / Math.max(1, element.scrollWidth)) * 0.96;
      if (fitted >= fitMinSize) {
        element.style.lineHeight = `${fullSize}px`;
        element.style.fontSize = `${Math.floor(fitted * 10) / 10}px`;
        return;
      }
    }
    const track = document.createElement("span");
    const first = document.createElement("span");
    const second = document.createElement("span");
    track.className = trackClass;
    first.textContent = text;
    second.textContent = text;
    second.setAttribute("aria-hidden", "true");
    track.append(first, second);
    element.textContent = "";
    element.classList.add(marqueeClass);
    element.appendChild(track);
  });
}

function appendTextCell(parent, className, value) {
  const cell = document.createElement("div");
  cell.className = className;
  cell.textContent = value == null || value === "" ? "" : String(value);
  parent.appendChild(cell);
  if (/\b(name|driver|num|number)\b/.test(className)) {
    fitTextToBox(cell, { minSize: /\b(name|driver)\b/.test(className) ? 16 : 14 });
  }
  return cell;
}

function fitTextToBox(element, options = {}) {
  if (!element) return;
  const minSize = Number.isFinite(Number(options.minSize)) ? Number(options.minSize) : 16;
  const delay = Number.isFinite(Number(options.delay)) ? Number(options.delay) : 0;
  const run = () => {
    if (!element.isConnected) return;
    element.style.fontSize = "";
    const computed = getComputedStyle(element);
    const maxSize = Number.isFinite(Number(options.maxSize)) ? Number(options.maxSize) : parseFloat(computed.fontSize) || minSize;
    if (!element.clientWidth || !element.textContent.trim()) return;
    if (element.scrollWidth <= element.clientWidth + 1) {
      element.style.fontSize = `${maxSize}px`;
      return;
    }
    const ratio = element.clientWidth / Math.max(1, element.scrollWidth);
    const nextSize = Math.max(minSize, Math.min(maxSize, maxSize * ratio * 0.96));
    element.style.fontSize = `${Math.floor(nextSize * 10) / 10}px`;
  };
  const schedule = () => delay ? setTimeout(run, delay) : requestAnimationFrame(run);
  if (document.fonts && document.fonts.ready) document.fonts.ready.then(schedule).catch(schedule);
  else schedule();
}

function fitTextGroup(scope = document, selector = ".lineup-row .name, .lineup-row .num, .leaders-name, .leaders-num, .result-name, .result-number, .podium-name, .podium-number, .lower-third .driver, .name, .num") {
  scope.querySelectorAll(selector).forEach(element => {
    fitTextToBox(element, { minSize: element.classList.contains("num") || element.className.includes("number") ? 14 : 16 });
  });
}

// ---- Live motion helpers ----------------------------------------------------
// Position trends, fastest-lap detection, and FLIP reorder animation shared by
// the board, side tower, and top banner. Graphics rebuild their rows from
// scratch on most timing ticks, so this state lives here and gets re-applied on
// every render; the CSS animations use a negative delay so a rebuilt row picks
// up its fade mid-flight instead of restarting it.

const LRW_TREND_HOLD_MS = 10000;
const LRW_FASTLAP_HOLD_MS = 8000;

// Master switch for the kinetic row layer (trend arrows, fastest-lap flash,
// winner/battle glows, FLIP reorder). Pages set this from style.bannerMotionOpen
// before rendering rows; default on so nothing changes until it is switched off.
let lrwMotionOpen = true;
function setRowMotionOpen(value) {
  lrwMotionOpen = value !== false;
}

const lrwLiveMotion = {
  sessionKey: "",
  positions: new Map(),
  bestLapMs: null,
  fastestFlash: null
};

function lrwDriverKey(driver) {
  if (!driver) return "";
  const key = `${driverNumber(driver)}|${driverName(driver)}`;
  return key === "|" ? "" : key;
}

function lrwLapTimeMs(value) {
  const text = displayText(value);
  if (!text || /^0{1,2}:0{2}:0{2}(?:\.0+)?$/.test(text)) return null;
  const parts = text.split(":");
  const seconds = Number(parts.pop());
  const minutes = parts.length ? Number(parts.pop()) : 0;
  const hours = parts.length ? Number(parts.pop()) : 0;
  if (![hours, minutes, seconds].every(Number.isFinite)) return null;
  const ms = Math.round(((hours * 3600) + (minutes * 60) + seconds) * 1000);
  return ms > 0 ? ms : null;
}

function lrwResetLiveMotionForSession(state) {
  const meta = (state && state.meta) || {};
  const sessionKey = `${displayText(meta.session)}|${displayText(meta.className)}`;
  if (sessionKey === lrwLiveMotion.sessionKey) return;
  lrwLiveMotion.sessionKey = sessionKey;
  lrwLiveMotion.positions.clear();
  lrwLiveMotion.bestLapMs = null;
  lrwLiveMotion.fastestFlash = null;
}

function trackPositionTrends(rows, state) {
  lrwResetLiveMotionForSession(state);
  const now = Date.now();
  (rows || []).forEach((driver, index) => {
    const key = lrwDriverKey(driver);
    if (!key) return;
    const position = Number(driver.position) || index + 1;
    const entry = lrwLiveMotion.positions.get(key);
    if (!entry) {
      lrwLiveMotion.positions.set(key, { position, dir: "", changedAt: 0 });
    } else if (position !== entry.position) {
      entry.dir = position < entry.position ? "up" : "down";
      entry.position = position;
      entry.changedAt = now;
    }
  });
}

function trackFastestLap(state) {
  lrwResetLiveMotionForSession(state);
  let bestMs = null;
  let holder = null;
  ((state && state.competitors) || []).forEach(driver => {
    const ms = lrwLapTimeMs(driver && driver.bestLap);
    if (ms !== null && (bestMs === null || ms < bestMs)) {
      bestMs = ms;
      holder = driver;
    }
  });
  if (bestMs === null) return;
  if (lrwLiveMotion.bestLapMs === null) {
    // The first timed lap sets the benchmark quietly; flashing on session start
    // (or on a page that loads mid-session) would be noise, not news.
    lrwLiveMotion.bestLapMs = bestMs;
    return;
  }
  if (bestMs >= lrwLiveMotion.bestLapMs) return;
  lrwLiveMotion.bestLapMs = bestMs;
  const key = lrwDriverKey(holder);
  if (!key) return;
  lrwLiveMotion.fastestFlash = {
    key,
    name: driverName(holder),
    number: driverNumber(holder),
    timeText: formatLapTime(holder.bestLap),
    at: Date.now()
  };
}

function positionTrend(driver) {
  const entry = lrwLiveMotion.positions.get(lrwDriverKey(driver));
  if (!entry || !entry.dir || !entry.changedAt) return null;
  const elapsed = Date.now() - entry.changedAt;
  return elapsed < LRW_TREND_HOLD_MS ? { dir: entry.dir, elapsed } : null;
}

function fastestLapFlash() {
  const flash = lrwLiveMotion.fastestFlash;
  if (!flash) return null;
  const elapsed = Date.now() - flash.at;
  return elapsed < LRW_FASTLAP_HOLD_MS ? { ...flash, elapsed } : null;
}

function applyPositionTrend(row, driver, positionCellSelector) {
  if (!lrwMotionOpen) return;
  const trend = positionTrend(driver);
  if (!trend) return;
  const cell = positionCellSelector ? row.querySelector(positionCellSelector) : row;
  if (!cell || cell.querySelector(".pos-trend")) return;
  const marker = document.createElement("u");
  marker.className = `pos-trend pos-trend-${trend.dir}`;
  marker.style.animationDelay = `-${trend.elapsed}ms`;
  cell.appendChild(marker);
}

function applyFastestLapFlash(row, driver) {
  if (!lrwMotionOpen) return;
  const flash = fastestLapFlash();
  if (!flash || lrwDriverKey(driver) !== flash.key || row.querySelector(".fastlap-flash")) return;
  const overlay = document.createElement("u");
  overlay.className = "fastlap-flash";
  overlay.style.animationDelay = `-${flash.elapsed}ms`;
  row.appendChild(overlay);
}

// Race moments: white flag and checkered, read from the raw flag text (the
// flagClass helpers fold white and checkered together, so this looks finer).
function raceMomentState(meta) {
  const flag = `${(meta && meta.flagLabel) || ""} ${(meta && meta.flag) || ""}`.toLowerCase();
  if (/checker|finish|complete/.test(flag)) return "checkered";
  if (/\bwhite\b/.test(flag)) return "white";
  return "";
}

// Persistent gold wash for the race winner's row while the checkered flag is
// up. Call sites decide who qualifies; rows rebuild every tick so the overlay
// is simply re-appended.
function applyWinnerGlow(row) {
  if (!lrwMotionOpen) return;
  if (!row || row.querySelector(".winner-glow")) return;
  const overlay = document.createElement("u");
  overlay.className = "winner-glow";
  row.appendChild(overlay);
}

// Amber wash + side bars on a row that is part of the live battle pair.
function applyBattleGlow(row) {
  if (!lrwMotionOpen) return;
  if (!row || row.querySelector(".battle-glow")) return;
  const overlay = document.createElement("u");
  overlay.className = "battle-glow";
  row.appendChild(overlay);
}

// FLIP reorder: capture row offsets before wiping a container, play after the
// rebuild; rows are matched by data-lrw-key.
function captureRowOffsets(container) {
  const map = new Map();
  if (!container) return map;
  container.querySelectorAll("[data-lrw-key]").forEach(row => {
    if (row.dataset.lrwKey) map.set(row.dataset.lrwKey, row.getBoundingClientRect().top);
  });
  return map;
}

function playRowReorder(container, previousOffsets) {
  if (!lrwMotionOpen) return;
  if (!container || !previousOffsets || !previousOffsets.size) return;
  if (typeof Element === "undefined" || typeof Element.prototype.animate !== "function") return;
  container.querySelectorAll("[data-lrw-key]").forEach(row => {
    const previousTop = previousOffsets.get(row.dataset.lrwKey);
    if (previousTop == null) return;
    const delta = previousTop - row.getBoundingClientRect().top;
    if (Math.abs(delta) < 2) return;
    row.animate(
      [{ transform: `translateY(${delta}px)` }, { transform: "translateY(0)" }],
      { duration: 480, easing: "cubic-bezier(0.22, 0.9, 0.24, 1)" }
    );
  });
}

function renderRows(container, rows, limit = 10) {
  container.innerHTML = "";
  rows.slice(0, limit).forEach(driver => {
    const row = document.createElement("div");
    row.className = "row";
    appendTextCell(row, "pos", driver.position || "-");
    appendTextCell(row, "num", driverNumber(driver));
    appendTextCell(row, "name", driverName(driver));
    appendTextCell(row, "metric", formatLapTime(driver.lastLap) || formatLapTime(driver.bestLap));
    container.appendChild(row);
  });
}
