// CrUX Lens — https://cruxlens.dev
// Copyright (C) 2026 Scott Leonard
// Licensed under the GNU Affero General Public License v3.0 or later.
// See the LICENSE file in the project root, or https://www.gnu.org/licenses/agpl-3.0.html

const { useState, useEffect, useRef } = React;

const ORIGIN = "https://www.mywebsite.com";
const TEST_URL = "https://www.mywebsite.com/full-relative-path";

const METRIC_LABELS = {
  largest_contentful_paint: "LCP", interaction_to_next_paint: "INP", cumulative_layout_shift: "CLS",
  first_contentful_paint: "FCP", experimental_time_to_first_byte: "TTFB",
  LARGEST_CONTENTFUL_PAINT: "LCP", INTERACTION_TO_NEXT_PAINT: "INP", CUMULATIVE_LAYOUT_SHIFT: "CLS",
  FIRST_CONTENTFUL_PAINT_MS: "FCP", EXPERIMENTAL_TIME_TO_FIRST_BYTE: "TTFB",
};
const METRIC_UNITS = {
  largest_contentful_paint: "ms", interaction_to_next_paint: "ms", cumulative_layout_shift: "",
  first_contentful_paint: "ms", experimental_time_to_first_byte: "ms",
  LARGEST_CONTENTFUL_PAINT: "ms", INTERACTION_TO_NEXT_PAINT: "ms", CUMULATIVE_LAYOUT_SHIFT: "",
  FIRST_CONTENTFUL_PAINT_MS: "ms", EXPERIMENTAL_TIME_TO_FIRST_BYTE: "ms",
};
const METRIC_THRESHOLDS = {
  largest_contentful_paint: { good: 2500, poor: 4000 }, interaction_to_next_paint: { good: 200, poor: 500 }, cumulative_layout_shift: { good: 0.1, poor: 0.25 },
  first_contentful_paint: { good: 1800, poor: 3000 }, experimental_time_to_first_byte: { good: 800, poor: 1800 },
  LARGEST_CONTENTFUL_PAINT: { good: 2500, poor: 4000 }, INTERACTION_TO_NEXT_PAINT: { good: 200, poor: 500 }, CUMULATIVE_LAYOUT_SHIFT: { good: 0.1, poor: 0.25 },
  FIRST_CONTENTFUL_PAINT_MS: { good: 1800, poor: 3000 }, EXPERIMENTAL_TIME_TO_FIRST_BYTE: { good: 800, poor: 1800 },
};

const psiCurl = (url, strategy) => `# PageSpeed Insights — origin + URL field data (no API key needed)
curl -s "https://www.googleapis.com/pagespeedonline/v5/runPagespeed?url=${encodeURIComponent(url || "https://example.com")}&strategy=${strategy || "mobile"}&category=performance" > psi_result.json

echo "Saved to psi_result.json — paste contents into the dashboard"`;

function generateBqSql(origin, testUrl, months, device) {
  const yyyymmList = months.map(m => m.year * 100 + (m.month + 1));
  const inClause = yyyymmList.join(", ");
  const monthLabels = months.map(m => {
    const d = new Date(m.year, m.month);
    return d.toLocaleDateString("en-US", { month: "short", year: "numeric" });
  });
  const deviceFilter = device === "all" ? "" : `\n  AND device = '${device}'`;
  const deviceLabel = device === "all" ? "all devices" : device;
  const main = `-- CrUX BigQuery: ${origin} CWV — ${monthLabels[0]}–${monthLabels[monthLabels.length - 1]} (${deviceLabel})
-- Run at: https://console.cloud.google.com/bigquery
-- Note: The most recent month's table may not exist yet
-- (CrUX monthly tables release on the 2nd Tuesday after month ends)

SELECT
  yyyymm, device, origin,
  p75_lcp, p75_inp, p75_cls,
  ROUND(fast_lcp / (fast_lcp + avg_lcp + slow_lcp), 3) AS pct_good_lcp,
  ROUND(fast_inp / (fast_inp + avg_inp + slow_inp), 3) AS pct_good_inp,
  ROUND(small_cls / (small_cls + medium_cls + large_cls), 3) AS pct_good_cls,
  ROUND(slow_inp / (fast_inp + avg_inp + slow_inp), 3) AS pct_poor_inp
FROM \`chrome-ux-report.materialized.device_summary\`
WHERE origin = '${origin}'${deviceFilter}
  AND yyyymm IN (${inClause})
ORDER BY yyyymm;`;

  const trimmedUrl = testUrl?.trim();
  if (!trimmedUrl) return main;

  const bonus = `-- BONUS: Compare specific article URL vs origin
SELECT
  yyyymm,
  IF(url = origin, 'ORIGIN', 'ARTICLE') AS scope,
  url,
  p75_lcp, p75_inp, p75_cls
FROM \`chrome-ux-report.materialized.device_summary\`
WHERE (
    origin = '${origin}'
    OR url = '${trimmedUrl}'
  )${deviceFilter}
  AND yyyymm IN (${inClause})
ORDER BY yyyymm, scope;`;

  return `${main}\n\n\n${bonus}`;
}

const BQ_DEVICES = [
  { value: "phone", label: "Phone" },
  { value: "desktop", label: "Desktop" },
  { value: "tablet", label: "Tablet" },
  { value: "all", label: "All" },
];

function fmt(metric, value) {
  const v = parseFloat(value);
  if (metric.toLowerCase().includes("layout_shift") || metric.toLowerCase().includes("cls")) return v.toFixed(2);
  return Math.round(v).toLocaleString();
}

function status(metric, p75) {
  const t = METRIC_THRESHOLDS[metric];
  if (!t) return "unknown";
  const v = parseFloat(p75);
  if (v <= t.good) return "good";
  if (v <= t.poor) return "ni";
  return "poor";
}

const SC = { good: "#0d7c3e", ni: "#d48c00", poor: "#d4230f", unknown: "#888" };
const SB = { good: "rgba(13,124,62,0.06)", ni: "rgba(212,140,0,0.06)", poor: "rgba(212,35,15,0.06)", unknown: "#f5f5f3" };

const FORM_FACTORS_CRUX = [
  { value: "PHONE", label: "Phone" },
  { value: "DESKTOP", label: "Desktop" },
  { value: "TABLET", label: "Tablet" },
  { value: "ALL", label: "All" },
];

const FORM_FACTORS_PSI = [
  { value: "mobile", label: "Mobile" },
  { value: "desktop", label: "Desktop" },
];

const LOOKUP_MODES = [
  { value: "single", label: "Single URL" },
  { value: "sitemap", label: "Sitemap / Feed" },
  { value: "list", label: "URL List" },
];

const parseUrlList = (text) =>
  text.split(/\r?\n/).map(s => s.trim()).filter(s => /^https?:\/\//i.test(s)).slice(0, 100);

const BATCH_METRICS = ["interaction_to_next_paint", "largest_contentful_paint", "cumulative_layout_shift"];

function FormFactorSelect({ value, onChange, options }) {
  return (
    <div style={{ display: "flex", gap: 0, borderRadius: 4, overflow: "hidden", border: "1px solid #d0d0cc" }}>
      {options.map(opt => (
        <button key={opt.value} onClick={() => onChange(opt.value)} style={{
          padding: "7px 14px", fontSize: 12, fontWeight: value === opt.value ? 600 : 400,
          background: value === opt.value ? "#1a1a2e" : "#fff",
          color: value === opt.value ? "#fff" : "#888",
          border: "none", cursor: "pointer", fontFamily: "inherit",
          borderRight: "1px solid #d0d0cc",
        }}>{opt.label}</button>
      ))}
    </div>
  );
}

function ContextStrip({ metrics }) {
  const items = [];

  const rt = metrics?.largest_contentful_paint_resource_type?.fractions;
  if (rt && (rt.image != null || rt.text != null)) {
    const img = Math.round((rt.image || 0) * 100);
    const txt = Math.round((rt.text || 0) * 100);
    items.push({
      label: "LCP resource",
      primary: `${img}% image`,
      secondary: `${txt}% text`,
      bar: null,
    });
  }

  const rtt = metrics?.round_trip_time;
  if (rtt?.percentiles?.p75 != null && Array.isArray(rtt.histogram) && rtt.histogram.length === 3) {
    const h = rtt.histogram;
    items.push({
      label: "Round-trip time (p75)",
      primary: `${rtt.percentiles.p75}ms`,
      secondary: `${Math.round((h[0].density || 0) * 100)}% fast · ${Math.round((h[1].density || 0) * 100)}% ok · ${Math.round((h[2].density || 0) * 100)}% slow`,
      bar: [
        { w: (h[0].density || 0) * 100, c: SC.good },
        { w: (h[1].density || 0) * 100, c: SC.ni },
        { w: (h[2].density || 0) * 100, c: SC.poor },
      ],
    });
  }

  const nt = metrics?.navigation_types?.fractions;
  if (nt) {
    const direct = Math.round(((nt.navigate || 0) + (nt.navigate_cache || 0)) * 100);
    const bf = Math.round(((nt.back_forward || 0) + (nt.back_forward_cache || 0)) * 100);
    const bfcache = Math.round((nt.back_forward_cache || 0) * 100);
    const reload = Math.round((nt.reload || 0) * 100);
    items.push({
      label: "Navigation types",
      primary: `${direct}% direct`,
      secondary: `Back/fwd ${bf}% · Reload ${reload}% · BFCache ${bfcache}%`,
      bar: null,
    });
  }

  if (items.length === 0) return null;
  return (
    <div style={{ marginTop: 12 }}>
      <div style={{ fontSize: 10, fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.08em", color: "#888", marginBottom: 6 }}>Context</div>
      <div style={{ display: "grid", gridTemplateColumns: `repeat(${items.length}, 1fr)`, gap: 10 }}>
        {items.map(it => (
          <div key={it.label} style={{ background: "#fff", border: "1px solid #e0e0dc", borderRadius: 6, padding: "12px 14px" }}>
            <div style={{ fontSize: 10, fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.08em", color: "#888", marginBottom: 4 }}>{it.label}</div>
            <div style={{ fontSize: 18, fontWeight: 700, color: "#1a1a2e", fontFamily: "'DM Mono',monospace" }}>{it.primary}</div>
            {it.bar && (
              <div style={{ display: "flex", height: 4, borderRadius: 2, overflow: "hidden", marginTop: 6 }}>
                {it.bar.map((seg, i) => <div key={i} style={{ width: `${seg.w}%`, background: seg.c }} />)}
              </div>
            )}
            <div style={{ fontSize: 11, color: "#666", marginTop: 6 }}>{it.secondary}</div>
          </div>
        ))}
      </div>
    </div>
  );
}

function LcpBreakdown({ metrics }) {
  const phases = [
    { key: "largest_contentful_paint_image_time_to_first_byte", label: "Server (TTFB)", color: "#1a1a2e" },
    { key: "largest_contentful_paint_image_resource_load_delay", label: "Load delay", color: "#4a5668" },
    { key: "largest_contentful_paint_image_resource_load_duration", label: "Load duration", color: "#8899a8" },
    { key: "largest_contentful_paint_image_render_delay", label: "Render delay", color: "#c8d6e5" },
  ];
  const values = phases.map(p => ({ ...p, p75: metrics?.[p.key]?.percentiles?.p75 }));
  const present = values.filter(v => v.p75 != null);
  if (present.length === 0) return null;
  const total = present.reduce((s, v) => s + (v.p75 || 0), 0);
  const imgFrac = metrics?.largest_contentful_paint_resource_type?.fractions?.image;
  return (
    <div style={{ background: "#fff", border: "1px solid #e0e0dc", borderRadius: 6, padding: "14px 16px", marginTop: 12 }}>
      <div style={{ display: "flex", alignItems: "baseline", gap: 8, marginBottom: 10, flexWrap: "wrap" }}>
        <h4 style={{ fontSize: 10, fontWeight: 600, margin: 0, textTransform: "uppercase", letterSpacing: "0.08em", color: "#555" }}>LCP phase breakdown</h4>
        {imgFrac != null && <span style={{ fontSize: 11, color: "#888" }}>Image LCP on {Math.round(imgFrac * 100)}% of loads</span>}
      </div>
      <div style={{ display: "flex", height: 22, borderRadius: 3, overflow: "hidden", marginBottom: 10 }}>
        {values.map(v => v.p75 == null ? null : (
          <div key={v.key} title={`${v.label}: ${v.p75}ms`} style={{ width: `${(v.p75 / total) * 100}%`, background: v.color }} />
        ))}
      </div>
      <div style={{ display: "grid", gridTemplateColumns: "repeat(4, 1fr)", gap: 10, fontSize: 11 }}>
        {values.map(v => (
          <div key={v.key}>
            <div style={{ display: "flex", alignItems: "center", gap: 6, marginBottom: 3 }}>
              <div style={{ width: 8, height: 8, borderRadius: 2, background: v.color, flexShrink: 0 }} />
              <span style={{ color: "#666", fontSize: 10, textTransform: "uppercase", letterSpacing: "0.06em", fontWeight: 600 }}>{v.label}</span>
            </div>
            <div style={{ fontFamily: "'DM Mono',monospace", fontWeight: 700, color: v.p75 == null ? "#ccc" : "#1a1a2e", fontSize: 13 }}>{v.p75 != null ? `${v.p75}ms` : "—"}</div>
          </div>
        ))}
      </div>
      <div style={{ fontSize: 11, color: "#888", marginTop: 10, lineHeight: 1.5 }}>
        These sub-phases sum to roughly the image LCP (~{total}ms). The largest segment is usually the best fix target — render delay often points to main-thread work, load duration to image size, load delay to blocking resources, TTFB to server response.
      </div>
    </div>
  );
}

function StatusCard({ metric, p75, distributions }) {
  const s = status(metric, p75);
  const label = METRIC_LABELS[metric] || metric;
  const unit = METRIC_UNITS[metric] || "";
  const good = distributions?.[0]?.proportion;
  const ni = distributions?.[1]?.proportion;
  const poor = distributions?.[2]?.proportion;
  return (
    <div style={{ background: SB[s], border: `1px solid ${SC[s]}22`, borderRadius: 6, padding: "14px 16px", borderLeft: `3px solid ${SC[s]}` }}>
      <div style={{ fontSize: 15, fontWeight: 700, letterSpacing: "0.04em", color: "#1a1a2e", marginBottom: 8 }}>{label} <span style={{ fontSize: 11, fontWeight: 500, color: "#888", letterSpacing: 0 }}>(p75)</span></div>
      <div style={{ fontSize: 26, fontWeight: 700, color: SC[s], fontFamily: "'DM Mono', monospace" }}>
        {fmt(metric, p75)}<span style={{ fontSize: 12, fontWeight: 400, marginLeft: 2 }}>{unit}</span>
      </div>
      {good != null && (
        <div style={{ marginTop: 8 }}>
          <div style={{ display: "flex", height: 6, borderRadius: 3, overflow: "hidden" }}>
            <div style={{ width: `${good * 100}%`, background: SC.good }} />
            <div style={{ width: `${ni * 100}%`, background: SC.ni }} />
            <div style={{ width: `${poor * 100}%`, background: SC.poor }} />
          </div>
          <div style={{ display: "flex", justifyContent: "space-between", fontSize: 9, marginTop: 4, fontFamily: "'DM Mono', monospace" }}>
            <span style={{ color: SC.good }}>{(good * 100).toFixed(0)}% good</span>
            <span style={{ color: SC.poor }}>{(poor * 100).toFixed(0)}% poor</span>
          </div>
        </div>
      )}
      <div style={{ fontSize: 10, color: SC[s], marginTop: 6, fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.06em" }}>
        {s === "ni" ? "Needs Improvement" : s === "good" ? "Good" : s === "poor" ? "Poor" : "—"}
      </div>
    </div>
  );
}

function CopyBlock({ text, copied, onCopy, maxHeight = 260 }) {
  const preRef = useRef(null);
  const handleCopy = () => {
    // Try clipboard API first, fall back to selecting text
    if (navigator.clipboard && navigator.clipboard.writeText) {
      navigator.clipboard.writeText(text).then(() => onCopy()).catch(() => selectFallback());
    } else {
      selectFallback();
    }
  };
  const selectFallback = () => {
    if (preRef.current) {
      const range = document.createRange();
      range.selectNodeContents(preRef.current);
      const sel = window.getSelection();
      sel.removeAllRanges();
      sel.addRange(range);
    }
    onCopy();
  };
  return (
    <div style={{ position: "relative", background: "#1a1a2e", borderRadius: 6, overflow: "hidden", marginBottom: 16 }}>
      <button onClick={handleCopy} style={{ position: "absolute", top: 8, right: 8, padding: "4px 12px", background: copied ? "#0d7c3e" : "rgba(255,255,255,0.1)", color: "#fff", border: "none", borderRadius: 3, cursor: "pointer", fontSize: 11, fontWeight: 600, fontFamily: "inherit", zIndex: 1 }}>
        {copied ? "✓ Copied" : "Copy"}
      </button>
      <pre ref={preRef} style={{ color: "#c8d6e5", padding: "16px 20px", margin: 0, fontSize: 11, lineHeight: 1.6, overflow: "auto", maxHeight, fontFamily: "'DM Mono', monospace", userSelect: "text" }}>{text}</pre>
    </div>
  );
}

function InfoTooltip({ children }) {
  const [open, setOpen] = useState(false);
  const timeoutRef = useRef(null);
  const show = () => { clearTimeout(timeoutRef.current); setOpen(true); };
  const hide = () => { timeoutRef.current = setTimeout(() => setOpen(false), 150); };
  return (
    <span style={{ position: "relative", display: "inline-block", marginLeft: 6, marginTop: "-3px", verticalAlign: "top" }}
      onMouseEnter={show} onMouseLeave={hide}>
      <span style={{ display: "inline-flex", alignItems: "center", justifyContent: "center", width: 14, height: 14, borderRadius: "50%", background: "#d0d0cc", color: "#666", fontSize: 10, fontWeight: 700, cursor: "help" }}>?</span>
      {open && (
        <span onMouseEnter={show} onMouseLeave={hide} style={{ position: "absolute", top: "calc(100% + 4px)", left: 0, width: 320, background: "#1a1a2e", color: "#fff", padding: "10px 12px", borderRadius: 4, fontSize: 11, fontWeight: 400, letterSpacing: 0, textTransform: "none", lineHeight: 1.5, zIndex: 10, fontFamily: "'DM Sans',system-ui,sans-serif", boxShadow: "0 2px 8px rgba(0,0,0,0.15)" }}>
          {children}
        </span>
      )}
    </span>
  );
}

function BatchResults({ data }) {
  const [sortKey, setSortKey] = useState(null);
  const [sortDir, setSortDir] = useState("asc");

  const rows = (data.batch || []).map(entry => {
    const metrics = entry.response?.record?.metrics || {};
    const getP75 = m => {
      const v = metrics[m]?.percentiles?.p75;
      return v == null ? null : parseFloat(v);
    };
    return {
      url: entry.url,
      hasData: !!entry.response?.record?.metrics,
      errorMsg: entry.response?.error?.message || null,
      inp: getP75("interaction_to_next_paint"),
      lcp: getP75("largest_contentful_paint"),
      cls: getP75("cumulative_layout_shift"),
    };
  });

  const withData = rows.filter(r => r.hasData);
  const withoutData = rows.length - withData.length;

  const SHORT_KEYS = { interaction_to_next_paint: "inp", largest_contentful_paint: "lcp", cumulative_layout_shift: "cls" };

  const metricStats = BATCH_METRICS.map(mKey => {
    const shortKey = SHORT_KEYS[mKey];
    const vals = withData.map(r => r[shortKey]).filter(v => v != null);
    const count = vals.length;
    if (!count) return { mKey, shortKey, count: 0, good: 0, ni: 0, poor: 0, avg: null, median: null };
    const srt = [...vals].sort((a, b) => a - b);
    const median = srt.length % 2 === 0 ? (srt[srt.length / 2 - 1] + srt[srt.length / 2]) / 2 : srt[Math.floor(srt.length / 2)];
    const avg = vals.reduce((s, v) => s + v, 0) / count;
    let good = 0, ni = 0, poor = 0;
    vals.forEach(v => {
      const s = status(mKey, v);
      if (s === "good") good++; else if (s === "ni") ni++; else if (s === "poor") poor++;
    });
    return { mKey, shortKey, count, good, ni, poor, avg, median };
  });

  const urlStatuses = withData.map(r => {
    const ss = BATCH_METRICS.map(m => r[SHORT_KEYS[m]] == null ? null : status(m, r[SHORT_KEYS[m]])).filter(s => s != null);
    if (!ss.length) return "unknown";
    if (ss.some(s => s === "poor")) return "poor";
    if (ss.some(s => s === "ni")) return "ni";
    return "good";
  });
  const overallGood = urlStatuses.filter(s => s === "good").length;
  const overallNi = urlStatuses.filter(s => s === "ni").length;
  const overallPoor = urlStatuses.filter(s => s === "poor").length;

  const scVerdict = (() => {
    if (withData.length === 0) {
      return { key: "unknown", label: "Not enough data", color: SC.unknown, text: "No URLs returned CrUX data, so Search Console impact can't be predicted from this batch." };
    }
    const passRate = overallGood / withData.length;
    const poorRate = overallPoor / withData.length;
    if (passRate >= 0.75) {
      return { key: "good", label: "Likely passing CWV", color: SC.good, text: `${overallGood} of ${withData.length} URLs (${Math.round(passRate * 100)}%) pass Core Web Vitals on all three metrics. Search Console should report these URLs as Good.` };
    }
    if (poorRate >= 0.25) {
      return { key: "poor", label: "Likely flagged in Search Console", color: SC.poor, text: `${overallPoor} URLs (${Math.round(poorRate * 100)}%) have at least one metric in the Poor zone. Search Console groups those as "Poor URLs" — expect the CWV report to show an issue for these.` };
    }
    return { key: "ni", label: "Mixed — some URLs need improvement", color: SC.ni, text: `${overallNi + overallPoor} URLs (${Math.round((overallNi + overallPoor) / withData.length * 100)}%) fall short of CWV on at least one metric. Search Console will likely show a mix of "Needs improvement" and "Poor" groups.` };
  })();

  const sorted = sortKey ? [...rows].sort((a, b) => {
    const av = a[sortKey], bv = b[sortKey];
    if (av == null && bv == null) return 0;
    if (av == null) return 1;
    if (bv == null) return -1;
    if (sortKey === "url") return sortDir === "asc" ? av.localeCompare(bv) : bv.localeCompare(av);
    return sortDir === "asc" ? av - bv : bv - av;
  }) : rows;

  const toggleSort = k => {
    if (sortKey === k) setSortDir(d => d === "asc" ? "desc" : "asc");
    else { setSortKey(k); setSortDir("asc"); }
  };

  const headerCell = (k, label, align = "left") => (
    <th onClick={() => toggleSort(k)} style={{ textAlign: align, padding: "8px 10px", fontSize: 10, fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.08em", color: "#888", cursor: "pointer", borderBottom: "1px solid #e0e0dc", userSelect: "none", whiteSpace: "nowrap" }}>
      {label}{sortKey === k && <span style={{ marginLeft: 4 }}>{sortDir === "asc" ? "▲" : "▼"}</span>}
    </th>
  );

  const metricCell = (metricKey, val) => {
    if (val == null) return <td style={{ padding: "8px 10px", textAlign: "right", fontFamily: "'DM Mono',monospace", color: "#ccc", fontSize: 12 }}>—</td>;
    const s = status(metricKey, val);
    const display = metricKey.includes("layout") ? val.toFixed(2) : Math.round(val).toLocaleString();
    return <td style={{ padding: "8px 10px", textAlign: "right", fontFamily: "'DM Mono',monospace", background: SB[s], color: SC[s], fontWeight: 600, fontSize: 12 }}>{display}</td>;
  };

  const fmtVal = (mKey, v) => {
    if (v == null) return "—";
    return mKey.includes("layout") ? v.toFixed(2) : Math.round(v).toLocaleString();
  };

  const verdictBg = scVerdict.key === "good" ? SB.good : scVerdict.key === "ni" ? SB.ni : scVerdict.key === "poor" ? SB.poor : "#f5f5f3";

  return (
    <div style={{ marginTop: 16 }}>
      <div style={{ background: verdictBg, border: `1px solid ${scVerdict.color}33`, borderLeft: `3px solid ${scVerdict.color}`, borderRadius: 6, padding: "14px 18px", marginBottom: 12 }}>
        <div style={{ fontSize: 10, fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.08em", color: "#888", marginBottom: 4 }}>Search Console Outlook</div>
        <div style={{ fontSize: 15, fontWeight: 700, color: scVerdict.color, marginBottom: 4 }}>{scVerdict.label}</div>
        <div style={{ fontSize: 12, color: "#555", lineHeight: 1.6 }}>{scVerdict.text}</div>
        {withData.length > 0 && (
          <div style={{ marginTop: 10 }}>
            <div style={{ display: "flex", height: 6, borderRadius: 3, overflow: "hidden" }}>
              <div style={{ width: `${overallGood / withData.length * 100}%`, background: SC.good }} />
              <div style={{ width: `${overallNi / withData.length * 100}%`, background: SC.ni }} />
              <div style={{ width: `${overallPoor / withData.length * 100}%`, background: SC.poor }} />
            </div>
            <div style={{ display: "flex", justifyContent: "space-between", fontSize: 10, marginTop: 4, fontFamily: "'DM Mono',monospace" }}>
              <span style={{ color: SC.good }}>{overallGood} good ({Math.round(overallGood / withData.length * 100)}%)</span>
              <span style={{ color: SC.ni }}>{overallNi} needs improvement ({Math.round(overallNi / withData.length * 100)}%)</span>
              <span style={{ color: SC.poor }}>{overallPoor} poor ({Math.round(overallPoor / withData.length * 100)}%)</span>
            </div>
          </div>
        )}
      </div>

      <div style={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: 10, marginBottom: 12 }}>
        {metricStats.map(st => {
          const label = METRIC_LABELS[st.mKey];
          const unit = METRIC_UNITS[st.mKey];
          const pG = st.count ? st.good / st.count : 0;
          const pN = st.count ? st.ni / st.count : 0;
          const pP = st.count ? st.poor / st.count : 0;
          const cardColor = st.count === 0 ? SC.unknown : pG >= 0.75 ? SC.good : pP >= 0.25 ? SC.poor : SC.ni;
          const cardBg = st.count === 0 ? "#f5f5f3" : pG >= 0.75 ? SB.good : pP >= 0.25 ? SB.poor : SB.ni;
          return (
            <div key={st.mKey} style={{ background: cardBg, border: `1px solid ${cardColor}33`, borderLeft: `3px solid ${cardColor}`, borderRadius: 6, padding: "12px 14px" }}>
              <div style={{ fontSize: 15, fontWeight: 700, letterSpacing: "0.04em", color: "#1a1a2e", marginBottom: 8 }}>
                {label}
                {st.count > 0 && (() => {
                  const verdictPct = pG >= 0.75 ? pG : pP >= 0.25 ? pP : pN;
                  const verdictLabel = pG >= 0.75 ? "Good" : pP >= 0.25 ? "Poor" : "Needs Improvement";
                  return (
                    <span style={{ fontSize: 12, fontWeight: 500, color: cardColor, marginLeft: 6, letterSpacing: 0 }}>- {Math.round(verdictPct * 100)}% {verdictLabel}</span>
                  );
                })()}
              </div>
              {st.count === 0 ? (
                <div style={{ fontSize: 12, color: "#aaa" }}>No data</div>
              ) : (
                <>
                  <div style={{ display: "flex", height: 6, borderRadius: 3, overflow: "hidden", marginBottom: 4 }}>
                    <div style={{ width: `${pG * 100}%`, background: SC.good }} />
                    <div style={{ width: `${pN * 100}%`, background: SC.ni }} />
                    <div style={{ width: `${pP * 100}%`, background: SC.poor }} />
                  </div>
                  <div style={{ display: "flex", justifyContent: "space-between", fontSize: 10, fontFamily: "'DM Mono',monospace", marginBottom: 10 }}>
                    <span style={{ color: SC.good }}>{st.good} ({Math.round(pG * 100)}%)</span>
                    <span style={{ color: SC.ni }}>{st.ni} ({Math.round(pN * 100)}%)</span>
                    <span style={{ color: SC.poor }}>{st.poor} ({Math.round(pP * 100)}%)</span>
                  </div>
                  <div style={{ display: "flex", justifyContent: "space-between", fontSize: 11, color: "#666", borderTop: "1px solid rgba(0,0,0,0.06)", paddingTop: 6 }}>
                    <span>avg <span style={{ fontFamily: "'DM Mono',monospace", fontWeight: 600, color: "#1a1a2e", marginLeft: 4 }}>{fmtVal(st.mKey, st.avg)}{unit}</span></span>
                    <span>median <span style={{ fontFamily: "'DM Mono',monospace", fontWeight: 600, color: "#1a1a2e", marginLeft: 4 }}>{fmtVal(st.mKey, st.median)}{unit}</span></span>
                  </div>
                </>
              )}
            </div>
          );
        })}
      </div>

      <div style={{ fontSize: 11, color: "#888", marginBottom: 8, fontFamily: "'DM Mono',monospace" }}>
        {rows.length} URLs queried · {withData.length} with data · {withoutData} no data
      </div>

      <div style={{ background: "#fff", border: "1px solid #e0e0dc", borderRadius: 6, overflow: "hidden" }}>
        <div style={{ overflowX: "auto" }}>
          <table style={{ width: "100%", borderCollapse: "collapse" }}>
            <thead>
              <tr>
                {headerCell("url", "URL")}
                {headerCell("inp", "INP", "right")}
                {headerCell("lcp", "LCP", "right")}
                {headerCell("cls", "CLS", "right")}
              </tr>
            </thead>
            <tbody>
              {sorted.map((r, i) => (
                <tr key={i} style={{ borderBottom: i === sorted.length - 1 ? "none" : "1px solid #f0f0ec" }}>
                  <td style={{ padding: "8px 10px", fontSize: 11, fontFamily: "'DM Mono',monospace", maxWidth: 420, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
                    <a href={r.url} target="_blank" rel="noopener" style={{ color: r.hasData ? "#1a1a2e" : "#aaa", textDecoration: "none" }} title={r.url}>{r.url}</a>
                    {!r.hasData && r.errorMsg && <span style={{ marginLeft: 6, fontSize: 10, color: "#d48c00" }}>({r.errorMsg.split(":")[0]})</span>}
                  </td>
                  {metricCell("interaction_to_next_paint", r.inp)}
                  {metricCell("largest_contentful_paint", r.lcp)}
                  {metricCell("cumulative_layout_shift", r.cls)}
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      </div>
    </div>
  );
}

function Dashboard() {
  const [tab, setTab] = useState("urlLookup");
  const [psiJson, setPsiJson] = useState("");
  const [psiData, setPsiData] = useState(null);
  const [psiErr, setPsiErr] = useState(null);
  const [psiUrl, setPsiUrl] = useState(TEST_URL);
  const [psiFormFactor, setPsiFormFactor] = useState("mobile");
  const [apiKey, setApiKey] = useState("");
  const [copied, setCopied] = useState({});
  // BigQuery state
  const [bqDevice, setBqDevice] = useState("phone");
  const [bqOrigin, setBqOrigin] = useState(ORIGIN);
  const [bqUrl, setBqUrl] = useState(TEST_URL);
  const [bqMonths, setBqMonths] = useState(() => {
    // Default: past 6 months including current month
    const now = new Date();
    const months = [];
    for (let i = 5; i >= 0; i--) {
      const d = new Date(now.getFullYear(), now.getMonth() - i, 1);
      months.push({ year: d.getFullYear(), month: d.getMonth(), selected: true });
    }
    return months;
  });
  // URL Lookup tab state
  const [urlInput, setUrlInput] = useState(TEST_URL);
  const [urlApiKey, setUrlApiKey] = useState("");
  const [urlFormFactor, setUrlFormFactor] = useState("PHONE");
  const [urlSnapshotJson, setUrlSnapshotJson] = useState("");
  const [urlSnapshotData, setUrlSnapshotData] = useState(null);
  const [urlSnapshotErr, setUrlSnapshotErr] = useState(null);
  const [urlHistoryJson, setUrlHistoryJson] = useState("");
  const [urlHistoryData, setUrlHistoryData] = useState(null);
  const [urlHistoryErr, setUrlHistoryErr] = useState(null);
  const [lookupMode, setLookupMode] = useState("single");
  const [sitemapUrl, setSitemapUrl] = useState("");
  const [pastedUrls, setPastedUrls] = useState("");
  const [batchJson, setBatchJson] = useState("");
  const [batchData, setBatchData] = useState(null);
  const [batchErr, setBatchErr] = useState(null);
  const [batchRunning, setBatchRunning] = useState(false);
  const [batchProgress, setBatchProgress] = useState({ current: 0, total: 0, currentUrl: "" });
  const [showOfflineScript, setShowOfflineScript] = useState(false);
  const [showApiKey, setShowApiKey] = useState(false);
  const [psiRunning, setPsiRunning] = useState(false);
  const [urlSnapRunning, setUrlSnapRunning] = useState(false);
  const [urlHistRunning, setUrlHistRunning] = useState(false);
  const batchAbortRef = useRef(null);
  const urlCanvasRefs = useRef({});

  const copy = (text, id) => { setCopied(p => ({ ...p, [id]: true })); setTimeout(() => setCopied(p => ({ ...p, [id]: false })), 3000); };

  const parsePsi = () => {
    setPsiErr(null);
    try {
      const d = JSON.parse(psiJson);
      if (!d.loadingExperience && !d.originLoadingExperience) throw new Error("No field data found in response. Make sure you're pasting the full PSI JSON.");
      setPsiData(d);
    } catch (e) { setPsiErr(e.message); }
  };

  const parseUrlSnapshot = () => {
    setUrlSnapshotErr(null);
    try {
      const d = JSON.parse(urlSnapshotJson);
      if (!d?.record?.metrics) throw new Error("Invalid CrUX response — missing record.metrics. If you got a NOT_FOUND error, this URL doesn't have enough traffic for CrUX.");
      setUrlSnapshotData(d);
    } catch (e) { setUrlSnapshotErr(e.message); }
  };

  const parseUrlHistory = () => {
    setUrlHistoryErr(null);
    try {
      const d = JSON.parse(urlHistoryJson);
      if (!d?.record?.metrics) throw new Error("Invalid CrUX History response — missing record.metrics. If you got a NOT_FOUND error, this URL doesn't have enough traffic for CrUX.");
      setUrlHistoryData(d);
    } catch (e) { setUrlHistoryErr(e.message); }
  };

  const parseBatch = () => {
    setBatchErr(null);
    try {
      const d = JSON.parse(batchJson);
      if (!Array.isArray(d?.batch)) throw new Error("Invalid batch response — expected a top-level \"batch\" array. Run the generated script and paste the contents of crux_batch.json here.");
      if (d.batch.length === 0) throw new Error("Batch is empty — the script didn't find any URLs in that sitemap/feed.");
      setBatchData(d);
    } catch (e) { setBatchErr(e.message); }
  };

  const batchQueryUrls = async (urls, abort) => {
    const SPACING_MS = 450;
    const batch = [];
    for (let i = 0; i < urls.length; i++) {
      if (abort.signal.aborted) break;
      const url = urls[i];
      setBatchProgress({ current: i + 1, total: urls.length, currentUrl: url });

      const body = urlFormFactor === "ALL" ? { url } : { url, formFactor: urlFormFactor };

      let response;
      try {
        const r = await fetch(
          "https://chromeuxreport.googleapis.com/v1/records:queryRecord",
          { method: "POST", headers: { "content-type": "application/json", "X-goog-api-key": urlApiKey }, body: JSON.stringify(body), signal: abort.signal }
        );
        response = await r.json();
      } catch (e) {
        if (abort.signal.aborted) break;
        response = { error: { code: 0, message: e.message || "Network error" } };
      }

      batch.push({ url, response });

      if (i < urls.length - 1 && !abort.signal.aborted) {
        await new Promise(res => setTimeout(res, SPACING_MS));
      }
    }
    return batch;
  };

  const runBatch = async () => {
    setBatchErr(null);
    setBatchData(null);
    if (!sitemapUrl.trim()) return setBatchErr("Enter a sitemap or feed URL first.");
    if (!urlApiKey.trim()) return setBatchErr("Enter your CrUX API key first.");

    const abort = new AbortController();
    batchAbortRef.current = abort;
    setBatchRunning(true);
    setBatchProgress({ current: 0, total: 0, currentUrl: "Fetching sitemap…" });

    try {
      const smRes = await fetch(`/api/sitemap?url=${encodeURIComponent(sitemapUrl.trim())}`, { signal: abort.signal });
      const smData = await smRes.json();
      if (!smRes.ok) throw new Error(smData.error || `Sitemap proxy returned ${smRes.status}`);
      const urls = smData.urls || [];
      if (!urls.length) throw new Error("No URLs returned from the sitemap.");

      const batch = await batchQueryUrls(urls, abort);
      if (batch.length > 0) {
        const payload = { batch };
        setBatchData(payload);
        setBatchJson(JSON.stringify(payload, null, 2));
      }
    } catch (e) {
      if (e.name !== "AbortError") setBatchErr(e.message || "Batch run failed.");
    } finally {
      setBatchRunning(false);
      batchAbortRef.current = null;
    }
  };

  const runListBatch = async () => {
    setBatchErr(null);
    setBatchData(null);
    if (!urlApiKey.trim()) return setBatchErr("Enter your CrUX API key first.");
    const urls = parseUrlList(pastedUrls);
    if (!urls.length) return setBatchErr("No valid URLs found. Paste one per line, each starting with http:// or https://. Capped at 100.");

    const abort = new AbortController();
    batchAbortRef.current = abort;
    setBatchRunning(true);
    setBatchProgress({ current: 0, total: urls.length, currentUrl: "" });

    try {
      const batch = await batchQueryUrls(urls, abort);
      if (batch.length > 0) {
        const payload = { batch };
        setBatchData(payload);
        setBatchJson(JSON.stringify(payload, null, 2));
      }
    } catch (e) {
      if (e.name !== "AbortError") setBatchErr(e.message || "Batch run failed.");
    } finally {
      setBatchRunning(false);
      batchAbortRef.current = null;
    }
  };

  const cancelBatch = () => {
    if (batchAbortRef.current) batchAbortRef.current.abort();
  };

  const runPsi = async () => {
    setPsiErr(null);
    if (!psiUrl.trim()) return setPsiErr("Enter a URL first.");
    setPsiRunning(true);
    try {
      const u = `https://www.googleapis.com/pagespeedonline/v5/runPagespeed?url=${encodeURIComponent(psiUrl.trim())}&strategy=${psiFormFactor}&category=performance`;
      const r = await fetch(u);
      const d = await r.json();
      if (d.error) throw new Error(d.error.message || "PSI error");
      if (!d.loadingExperience && !d.originLoadingExperience) throw new Error("No field data found in response.");
      setPsiData(d);
      setPsiJson(JSON.stringify(d, null, 2));
    } catch (e) {
      setPsiErr(e.message || "PSI request failed.");
    } finally {
      setPsiRunning(false);
    }
  };

  const runUrlSnapshot = async () => {
    setUrlSnapshotErr(null);
    if (!urlInput.trim()) return setUrlSnapshotErr("Enter a URL first.");
    if (!urlApiKey.trim()) return setUrlSnapshotErr("Enter your CrUX API key first.");
    setUrlSnapRunning(true);
    try {
      const body = urlFormFactor === "ALL" ? { url: urlInput.trim() } : { url: urlInput.trim(), formFactor: urlFormFactor };
      const r = await fetch(
        "https://chromeuxreport.googleapis.com/v1/records:queryRecord",
        { method: "POST", headers: { "content-type": "application/json", "X-goog-api-key": urlApiKey }, body: JSON.stringify(body) }
      );
      const d = await r.json();
      if (d.error) throw new Error(d.error.message || "CrUX error");
      if (!d?.record?.metrics) throw new Error("Invalid CrUX response — missing record.metrics. If you got NOT_FOUND, this URL doesn't have enough traffic for CrUX.");
      setUrlSnapshotData(d);
      setUrlSnapshotJson(JSON.stringify(d, null, 2));
    } catch (e) {
      setUrlSnapshotErr(e.message || "Snapshot request failed.");
    } finally {
      setUrlSnapRunning(false);
    }
  };

  const runUrlHistory = async () => {
    setUrlHistoryErr(null);
    if (!urlInput.trim()) return setUrlHistoryErr("Enter a URL first.");
    if (!urlApiKey.trim()) return setUrlHistoryErr("Enter your CrUX API key first.");
    setUrlHistRunning(true);
    try {
      const body = {
        url: urlInput.trim(),
        metrics: ["largest_contentful_paint", "interaction_to_next_paint", "cumulative_layout_shift"],
      };
      if (urlFormFactor !== "ALL") body.formFactor = urlFormFactor;
      const r = await fetch(
        "https://chromeuxreport.googleapis.com/v1/records:queryHistoryRecord",
        { method: "POST", headers: { "content-type": "application/json", "X-goog-api-key": urlApiKey }, body: JSON.stringify(body) }
      );
      const d = await r.json();
      if (d.error) throw new Error(d.error.message || "CrUX History error");
      if (!d?.record?.metrics) throw new Error("Invalid CrUX History response — missing record.metrics.");
      setUrlHistoryData(d);
      setUrlHistoryJson(JSON.stringify(d, null, 2));
    } catch (e) {
      setUrlHistoryErr(e.message || "History request failed.");
    } finally {
      setUrlHistRunning(false);
    }
  };

  const sitemapScript = (sm, k, ff) => {
    const ffLine = ff === "ALL" ? "" : `,\\\"formFactor\\\":\\\"${ff}\\\"`;
    return `#!/bin/bash
# CrUX Batch Lookup — fetches URLs from a sitemap/feed and queries CrUX for each
# Save as crux_batch.sh, then: chmod +x crux_batch.sh && ./crux_batch.sh
set -e

SITEMAP="${sm || "https://www.example.com/sitemap.xml"}"
API_KEY="${k || "YOUR_API_KEY"}"
MAX_URLS=100
OUTPUT="crux_batch.json"
UA="Mozilla/5.0 (crux-explorer batch lookup)"

echo "Fetching: $SITEMAP"
CONTENT=$(curl -sL -A "$UA" "$SITEMAP")

# Sitemap index? Recurse one level.
if echo "$CONTENT" | grep -q "<sitemapindex"; then
  echo "Sitemap index detected — fetching up to 5 child sitemaps..."
  CHILDREN=$(echo "$CONTENT" | grep -oE '<loc>[^<]+</loc>' | sed -E 's/<\\/?loc>//g' | head -n 5)
  ALL=""
  while IFS= read -r C; do
    [ -z "$C" ] && continue
    echo "  → $C"
    CC=$(curl -sL -A "$UA" "$C")
    ALL="$ALL"$'\\n'"$(echo "$CC" | grep -oE '<loc>[^<]+</loc>' | sed -E 's/<\\/?loc>//g')"
  done <<< "$CHILDREN"
  URLS=$(echo "$ALL" | grep -v '^$' | head -n $MAX_URLS)
else
  # Try <loc> first (sitemap), then <link href="..."> (Atom), then <link>...</link> (RSS).
  URLS=$(echo "$CONTENT" | grep -oE '<loc>[^<]+</loc>' | sed -E 's/<\\/?loc>//g' | head -n $MAX_URLS)
  if [ -z "$URLS" ]; then
    URLS=$(echo "$CONTENT" | grep -oE '<link[^>]+href="[^"]+"' | sed -E 's/.*href="([^"]+)".*/\\1/' | head -n $MAX_URLS)
  fi
  if [ -z "$URLS" ]; then
    URLS=$(echo "$CONTENT" | grep -oE '<link>[^<]+</link>' | sed -E 's/<\\/?link>//g' | head -n $MAX_URLS)
  fi
fi

COUNT=$(echo "$URLS" | grep -vc '^$' || true)
if [ "$COUNT" -eq 0 ]; then
  echo "No URLs found. Check the sitemap/feed URL and try again."
  exit 1
fi
echo "Found $COUNT URLs. Querying CrUX API..."

echo '{"batch":[' > "$OUTPUT"
FIRST=true
i=0
while IFS= read -r URL; do
  [ -z "$URL" ] && continue
  i=$((i+1))
  printf '[%d/%d] %s\\n' "$i" "$COUNT" "$URL"
  [ "$FIRST" = false ] && echo ',' >> "$OUTPUT"
  FIRST=false
  PAYLOAD="{\\"url\\":\\"$URL\\"${ffLine}}"
  RESPONSE=$(curl -s -X POST \\
    "https://chromeuxreport.googleapis.com/v1/records:queryRecord" \\
    -H "X-goog-api-key: $API_KEY" \\
    -H 'Content-Type: application/json' \\
    -d "$PAYLOAD")
  printf '{"url":"%s","response":%s}' "$URL" "$RESPONSE" >> "$OUTPUT"
  sleep 0.3
done <<< "$URLS"
echo ']}' >> "$OUTPUT"

echo ""
echo "✓ Done — paste the contents of $OUTPUT into the dashboard to visualize."`;
  };

  const urlSnapshotCurl = (u, k, ff) => {
    const ffLine = ff === "ALL" ? "" : `\n    "formFactor": "${ff}",`;
    return `# CrUX API — single URL snapshot${ff === "ALL" ? " (all form factors)" : ` (${ff.toLowerCase()})`}
curl -s -X POST \\
  'https://chromeuxreport.googleapis.com/v1/records:queryRecord' \\
  -H 'X-goog-api-key: ${k || "YOUR_API_KEY"}' \\
  -H 'Content-Type: application/json' \\
  -d '{
    "url": "${u || "https://example.com"}"${ff === "ALL" ? "" : ","}${ffLine}
  }' > crux_url_snapshot.json

echo "Saved to crux_url_snapshot.json"`;
  };

  const urlHistoryCurl = (u, k, ff) => {
    const ffLine = ff === "ALL" ? "" : `\n    "formFactor": "${ff}",`;
    return `# CrUX History API — single URL${ff === "ALL" ? " (all form factors)" : ` (${ff.toLowerCase()})`}, 40 weeks
curl -s -X POST \\
  'https://chromeuxreport.googleapis.com/v1/records:queryHistoryRecord' \\
  -H 'X-goog-api-key: ${k || "YOUR_API_KEY"}' \\
  -H 'Content-Type: application/json' \\
  -d '{
    "url": "${u || "https://example.com"}",${ffLine}
    "metrics": ["largest_contentful_paint","interaction_to_next_paint","cumulative_layout_shift"]
  }' > crux_url_history.json

echo "Saved to crux_url_history.json"`;
  };

  // Draw URL History charts
  useEffect(() => {
    if (!urlHistoryData?.record?.metrics) return;
    const metrics = urlHistoryData.record.metrics;
    const periods = urlHistoryData.record.collectionPeriods || [];
    ["interaction_to_next_paint", "largest_contentful_paint", "cumulative_layout_shift"].forEach(mk => {
      const canvas = urlCanvasRefs.current[mk];
      if (!canvas || !metrics[mk]) return;
      const ctx = canvas.getContext("2d");
      const dpr = window.devicePixelRatio || 1;
      const rect = canvas.getBoundingClientRect();
      canvas.width = rect.width * dpr; canvas.height = rect.height * dpr;
      ctx.scale(dpr, dpr);
      const W = rect.width, H = rect.height;
      const p75s = metrics[mk].percentilesTimeseries?.p75s || [];
      const vals = p75s.map(v => v == null ? null : parseFloat(v));
      const valid = vals.filter(v => v !== null);
      if (!valid.length) return;
      const th = METRIC_THRESHOLDS[mk];
      const max = Math.max(...valid, th.poor * 1.15);
      const pT = 32, pB = 48, pL = 56, pR = 20;
      const cW = W - pL - pR, cH = H - pT - pB;
      const y = v => pT + cH - (v / max) * cH;
      ctx.clearRect(0, 0, W, H);
      ctx.fillStyle = "rgba(0,0,0,0.02)"; ctx.fillRect(pL, pT, cW, cH);
      ctx.fillStyle = "rgba(13,124,62,0.05)"; ctx.fillRect(pL, y(th.good), cW, y(0) - y(th.good));
      ctx.fillStyle = "rgba(212,140,0,0.05)"; ctx.fillRect(pL, y(th.poor), cW, y(th.good) - y(th.poor));
      ctx.fillStyle = "rgba(212,35,15,0.05)"; ctx.fillRect(pL, pT, cW, y(th.poor) - pT);
      [th.good, th.poor].forEach((t, i) => {
        const yy = y(t); ctx.strokeStyle = i === 0 ? "#0d7c3e" : "#d4230f"; ctx.lineWidth = 1; ctx.setLineDash([4, 4]);
        ctx.beginPath(); ctx.moveTo(pL, yy); ctx.lineTo(W - pR, yy); ctx.stroke(); ctx.setLineDash([]);
        ctx.fillStyle = ctx.strokeStyle; ctx.font = "600 10px 'DM Sans',system-ui"; ctx.textAlign = "right";
        ctx.fillText(mk.includes("layout") ? t.toFixed(2) : t.toLocaleString(), pL - 6, yy + 3);
      });
      ctx.fillStyle = "#888"; ctx.font = "10px 'DM Sans',system-ui"; ctx.textAlign = "right";
      for (let i = 0; i <= 5; i++) { const v2 = (max * i) / 5; ctx.fillText(mk.includes("layout") ? v2.toFixed(2) : Math.round(v2).toLocaleString(), pL - 6, y(v2) + 3); ctx.strokeStyle = "rgba(0,0,0,0.05)"; ctx.lineWidth = 0.5; ctx.beginPath(); ctx.moveTo(pL, y(v2)); ctx.lineTo(W - pR, y(v2)); ctx.stroke(); }
      const xS = cW / Math.max(vals.length - 1, 1);
      ctx.strokeStyle = "#1a1a2e"; ctx.lineWidth = 2; ctx.lineJoin = "round"; ctx.beginPath();
      let started = false; const pts = [];
      vals.forEach((v2, i) => { if (v2 === null) return; const xx = pL + i * xS, yy = y(v2); pts.push({ x: xx, y: yy, v: v2, i }); if (!started) { ctx.moveTo(xx, yy); started = true; } else ctx.lineTo(xx, yy); });
      ctx.stroke();
      if (pts.length > 1) { ctx.beginPath(); ctx.moveTo(pts[0].x, pts[0].y); pts.forEach(p => ctx.lineTo(p.x, p.y)); ctx.lineTo(pts[pts.length - 1].x, pT + cH); ctx.lineTo(pts[0].x, pT + cH); ctx.closePath(); const g = ctx.createLinearGradient(0, pT, 0, pT + cH); g.addColorStop(0, "rgba(26,26,46,0.12)"); g.addColorStop(1, "rgba(26,26,46,0.01)"); ctx.fillStyle = g; ctx.fill(); }
      let uIdx = -1;
      periods.forEach((p, i) => { const d = p.lastDate; if (!d) return; const dt = new Date(`${d.year}-${String(d.month).padStart(2, "0")}-${String(d.day).padStart(2, "0")}`); if (uIdx < 0 && dt >= new Date("2026-03-27")) uIdx = i; if (dt >= new Date("2026-03-27") && dt <= new Date("2026-04-15")) { ctx.fillStyle = "rgba(212,35,15,0.06)"; ctx.fillRect(pL + i * xS - xS / 2, pT, xS, cH); } });
      pts.forEach(p => { const s2 = status(mk, p.v); if (p.i >= vals.length - 6 || p.i === 0 || p.i === uIdx) { ctx.fillStyle = SC[s2]; ctx.beginPath(); ctx.arc(p.x, p.y, 3.5, 0, Math.PI * 2); ctx.fill(); } });
      if (uIdx >= 0) { const xx = pL + uIdx * xS; ctx.strokeStyle = "rgba(212,35,15,0.5)"; ctx.lineWidth = 1; ctx.setLineDash([3, 3]); ctx.beginPath(); ctx.moveTo(xx, pT); ctx.lineTo(xx, pT + cH); ctx.stroke(); ctx.setLineDash([]); ctx.fillStyle = "#d4230f"; ctx.font = "600 9px 'DM Sans',system-ui"; ctx.textAlign = "left"; ctx.fillText("Mar 27 Core Update", xx + 4, pT + 12); }
      ctx.fillStyle = "#888"; ctx.font = "10px 'DM Sans',system-ui"; ctx.textAlign = "center";
      const ev = Math.max(Math.floor(periods.length / 8), 1);
      periods.forEach((p, i) => { if (i % ev !== 0 && i !== periods.length - 1) return; const d = p.lastDate; if (!d) return; ctx.fillText(`${d.month}/${d.day}`, pL + i * xS, H - pB + 16); });
    });
  }, [urlHistoryData]);

  const tabBtn = (id, label) => (
    <button onClick={() => setTab(id)} style={{ padding: "14px 20px", background: "none", border: "none", borderBottom: tab === id ? "2px solid #1a1a2e" : "2px solid transparent", cursor: "pointer", fontSize: 12, fontWeight: tab === id ? 600 : 400, color: tab === id ? "#1a1a2e" : "#888", fontFamily: "inherit" }}>{label}</button>
  );

  const originMetrics = psiData?.originLoadingExperience?.metrics || {};
  const urlMetrics = psiData?.loadingExperience?.metrics || {};
  const cwvKeys = ["INTERACTION_TO_NEXT_PAINT", "LARGEST_CONTENTFUL_PAINT", "CUMULATIVE_LAYOUT_SHIFT"];

  return (
    <div style={{ fontFamily: "'DM Sans',system-ui,sans-serif", background: "#fafaf8", minHeight: "100vh", color: "#1a1a2e" }}>
      <link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@300;400;500;600;700&family=DM+Mono:wght@400;500&display=swap" rel="stylesheet" />

      <div style={{ background: "#1a1a2e", borderBottom: "3px solid #d4230f" }}>
        <div style={{ maxWidth: 960, margin: "0 auto", padding: "24px 28px 20px", color: "#fff" }}>
          <div style={{ fontSize: 10, letterSpacing: "0.14em", textTransform: "uppercase", color: "#666", marginBottom: 4, fontWeight: 500 }}>Core Web Vitals</div>
          <h1 style={{ fontSize: 20, fontWeight: 700, margin: 0 }}>CrUX Lens</h1>
          <div style={{ fontSize: 12, color: "#888", marginTop: 6, fontFamily: "'DM Mono',monospace" }}>PSI · CrUX API · BigQuery</div>
        </div>
      </div>

      <div style={{ borderBottom: "1px solid #e0e0dc", background: "#fff" }}>
        <div style={{ maxWidth: 960, margin: "0 auto", display: "flex", overflowX: "auto" }}>
          {tabBtn("urlLookup", "① CrUX API Lookup")}
          {tabBtn("psi", "② PageSpeed Insights")}
          {tabBtn("bigquery", "③ BigQuery SQL")}
        </div>
      </div>

      <div style={{ padding: "20px 28px", maxWidth: 960, margin: "0 auto" }}>

        {/* ============ PSI ============ */}
        {tab === "psi" && (
          <div>
            <div style={{ background: "#fff", border: "1px solid #e0e0dc", borderRadius: 6, padding: "16px 20px", marginBottom: 16, fontSize: 13, color: "#555", lineHeight: 1.7 }}>
              <strong>How to use:</strong> Enter a URL, pick a form factor, and click Run. No API key required. Prefer to run offline? Expand the advanced section below for a curl command.
            </div>

            <div style={{ marginBottom: 16 }}>
              <label style={{ display: "block", fontSize: 11, fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.08em", color: "#888", marginBottom: 6 }}>URL to analyze</label>
              <input type="text" value={psiUrl} onChange={e => setPsiUrl(e.target.value)} placeholder="https://www.mywebsite.com/some-article"
                style={{ width: "100%", padding: "10px 14px", border: "1px solid #d0d0cc", borderRadius: 4, fontSize: 12, fontFamily: "'DM Mono',monospace", boxSizing: "border-box" }} />
            </div>

            <div style={{ marginBottom: 16 }}>
              <label style={{ display: "block", fontSize: 11, fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.08em", color: "#888", marginBottom: 6 }}>Form Factor</label>
              <FormFactorSelect value={psiFormFactor} onChange={setPsiFormFactor} options={FORM_FACTORS_PSI} />
            </div>

            <div style={{ display: "flex", gap: 10, alignItems: "center", marginBottom: 12 }}>
              <button onClick={runPsi} disabled={psiRunning} style={{ padding: "10px 22px", background: psiRunning ? "#888" : "#d4230f", color: "#fff", border: "none", borderRadius: 4, cursor: psiRunning ? "default" : "pointer", fontSize: 13, fontWeight: 600, fontFamily: "inherit" }}>
                {psiRunning ? "Running…" : "Run"}
              </button>
              {psiRunning && <span style={{ fontSize: 12, color: "#666", fontFamily: "'DM Mono',monospace" }}>querying PageSpeed Insights…</span>}
            </div>

            {psiErr && <div style={{ background: "rgba(212,35,15,0.06)", border: "1px solid rgba(212,35,15,0.2)", borderRadius: 4, padding: "10px 14px", fontSize: 12, color: "#d4230f", marginBottom: 12 }}>{psiErr}</div>}

            {psiData && (
              <div style={{ marginTop: 24 }}>
                {[
                  { label: "Origin-Level Field Data (CrUX)", subtitle: `All pages on ${ORIGIN}`, metrics: originMetrics, overall: psiData?.originLoadingExperience?.overall_category },
                  { label: "URL-Level Field Data (CrUX)", subtitle: psiUrl, metrics: urlMetrics, overall: psiData?.loadingExperience?.overall_category },
                ].map((sec, si) => (
                  <div key={si} style={{ marginBottom: 24 }}>
                    <div style={{ display: "flex", alignItems: "baseline", gap: 10, marginBottom: 6 }}>
                      <h3 style={{ fontSize: 14, fontWeight: 600, margin: 0 }}>{sec.label}</h3>
                      {sec.overall && <span style={{ fontSize: 10, fontWeight: 600, textTransform: "uppercase", color: SC[sec.overall === "FAST" ? "good" : sec.overall === "AVERAGE" ? "ni" : "poor"], background: SB[sec.overall === "FAST" ? "good" : sec.overall === "AVERAGE" ? "ni" : "poor"], padding: "3px 8px", borderRadius: 3 }}>{sec.overall}</span>}
                    </div>
                    <div style={{ fontSize: 11, color: "#888", marginBottom: 10, fontFamily: "'DM Mono',monospace", wordBreak: "break-all" }}>{sec.subtitle}</div>
                    <div style={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: 10 }}>
                      {cwvKeys.map(m => {
                        const d = sec.metrics[m];
                        if (!d) return <div key={m} style={{ background: "#f5f5f3", borderRadius: 6, padding: "14px 16px", fontSize: 12, color: "#aaa" }}>No {METRIC_LABELS[m]} data</div>;
                        return <StatusCard key={m} metric={m} p75={d.percentile} distributions={d.distributions} />;
                      })}
                    </div>
                    {(() => {
                      const supporting = ["FIRST_CONTENTFUL_PAINT_MS", "EXPERIMENTAL_TIME_TO_FIRST_BYTE"].filter(m => sec.metrics[m]);
                      if (supporting.length === 0) return null;
                      return (
                        <div style={{ marginTop: 12 }}>
                          <div style={{ fontSize: 10, fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.08em", color: "#888", marginBottom: 6 }}>Supporting metrics</div>
                          <div style={{ display: "grid", gridTemplateColumns: `repeat(${supporting.length}, 1fr)`, gap: 10 }}>
                            {supporting.map(m => {
                              const d = sec.metrics[m];
                              return <StatusCard key={m} metric={m} p75={d.percentile} distributions={d.distributions} />;
                            })}
                          </div>
                        </div>
                      );
                    })()}
                  </div>
                ))}

                {psiData?.lighthouseResult?.categories?.performance && (() => {
                  const s = psiData.lighthouseResult.categories.performance.score;
                  const c = s >= 0.9 ? SC.good : s >= 0.5 ? SC.ni : SC.poor;
                  return (
                    <div style={{ display: "flex", alignItems: "center", gap: 16, background: "#fff", border: "1px solid #e0e0dc", borderRadius: 6, padding: "14px 20px" }}>
                      <div style={{ width: 50, height: 50, borderRadius: "50%", border: `4px solid ${c}`, display: "flex", alignItems: "center", justifyContent: "center", fontSize: 20, fontWeight: 700, fontFamily: "'DM Mono',monospace", color: c, flexShrink: 0 }}>{Math.round(s * 100)}</div>
                      <div style={{ fontSize: 12, color: "#888" }}>Lighthouse lab score (simulated mobile). Field data above is what Search Console uses.</div>
                    </div>
                  );
                })()}
              </div>
            )}

            <details style={{ marginTop: 20, background: "#fff", border: "1px solid #e0e0dc", borderRadius: 6, padding: "10px 14px" }}>
              <summary style={{ fontSize: 12, fontWeight: 600, color: "#555", cursor: "pointer", userSelect: "none" }}>
                Advanced: run via curl
              </summary>
              <div style={{ fontSize: 12, color: "#666", margin: "10px 0", lineHeight: 1.6 }}>
                Prefer to run this locally? Copy the command below, save the output as <code style={{ background: "#eee", padding: "1px 5px", borderRadius: 3, fontFamily: "'DM Mono',monospace" }}>psi_result.json</code>, and paste it back here to visualize.
              </div>
              <CopyBlock text={psiCurl(psiUrl, psiFormFactor)} copied={copied.psi} onCopy={() => copy(psiCurl(psiUrl, psiFormFactor), "psi")} />
              <label style={{ display: "block", fontSize: 11, fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.08em", color: "#888", marginBottom: 6 }}>Paste psi_result.json contents here:</label>
              <textarea value={psiJson} onChange={e => setPsiJson(e.target.value)} placeholder='{"lighthouseResult":..., "loadingExperience":..., "originLoadingExperience":...}'
                style={{ width: "100%", height: 100, padding: "10px 14px", border: "1px solid #d0d0cc", borderRadius: 4, fontSize: 11, fontFamily: "'DM Mono',monospace", resize: "vertical", boxSizing: "border-box" }} />
              <button onClick={parsePsi} style={{ marginTop: 8, padding: "8px 20px", background: "#1a1a2e", color: "#fff", border: "none", borderRadius: 4, cursor: "pointer", fontSize: 12, fontWeight: 600, fontFamily: "inherit" }}>Visualize</button>
            </details>
          </div>
        )}

        {/* ============ CrUX API LOOKUP ============ */}
        {tab === "urlLookup" && (
          <div>
            <div style={{ background: "#fff", border: "1px solid #e0e0dc", borderRadius: 6, padding: "16px 20px", marginBottom: 16, fontSize: 13, color: "#555", lineHeight: 1.7 }}>
              <strong>CrUX API Lookup:</strong> Query field data for a single URL, or batch-query up to 100 URLs pulled from a sitemap or feed. Only URLs with sufficient Chrome traffic will have data — URLs without it return NOT_FOUND.
            </div>

            <div style={{ marginBottom: 16 }}>
              <label style={{ display: "block", fontSize: 11, fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.08em", color: "#888", marginBottom: 6 }}>
                CrUX API Key
                <InfoTooltip>
                  You'll need a Google Cloud API key with the CrUX API enabled.{" "}
                  <a href="https://console.cloud.google.com/apis/credentials/wizard?api=chromeuxreport.googleapis.com" target="_blank" rel="noopener" style={{ color: "#7cc4ff" }}>Generate one here ↗</a>
                  <div style={{ marginTop: 8, paddingTop: 8, borderTop: "1px solid rgba(255,255,255,0.1)" }}>
                    <div style={{ fontWeight: 600, marginBottom: 4 }}>Recommended: restrict your key</div>
                    <ul style={{ margin: 0, paddingLeft: 16, fontSize: 11 }}>
                      <li>Limit to the <em>Chrome UX Report API</em> only (API restrictions)</li>
                      <li>Set a daily quota cap in the Cloud Console</li>
                      <li>Add an HTTP referrer restriction once you have a stable domain</li>
                    </ul>
                  </div>
                </InfoTooltip>
              </label>
              <div style={{ position: "relative" }}>
                <input type={showApiKey ? "text" : "password"} value={urlApiKey} onChange={e => setUrlApiKey(e.target.value)} placeholder="AIzaSy..." autoComplete="off" spellCheck="false"
                  style={{ width: "100%", padding: "10px 60px 10px 14px", border: "1px solid #d0d0cc", borderRadius: 4, fontSize: 12, fontFamily: "'DM Mono',monospace", boxSizing: "border-box" }} />
                <button type="button" onClick={() => setShowApiKey(v => !v)} style={{ position: "absolute", right: 6, top: "50%", transform: "translateY(-50%)", padding: "4px 10px", fontSize: 11, fontWeight: 500, background: "#f0f0ec", color: "#555", border: "1px solid #d0d0cc", borderRadius: 3, cursor: "pointer", fontFamily: "inherit" }}>
                  {showApiKey ? "Hide" : "Show"}
                </button>
              </div>
              <div style={{ fontSize: 11, color: "#888", marginTop: 6, lineHeight: 1.5 }}>
                <div>· This server never stores or transmits your key — requests go directly from your browser to Google.</div>
                <div>· Google enforces a default rate limit of <strong style={{ color: "#555", fontWeight: 600 }}>150 queries per minute</strong> per API key. Batch runs are paced to stay under it.</div>
              </div>
            </div>

            <div style={{ marginBottom: 16 }}>
              <label style={{ display: "block", fontSize: 11, fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.08em", color: "#888", marginBottom: 6 }}>Lookup Mode</label>
              <FormFactorSelect value={lookupMode} onChange={setLookupMode} options={LOOKUP_MODES} />
            </div>

            {lookupMode === "single" && (
              <div style={{ marginBottom: 16 }}>
                <label style={{ display: "block", fontSize: 11, fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.08em", color: "#888", marginBottom: 6 }}>URL to look up</label>
                <input type="text" value={urlInput} onChange={e => setUrlInput(e.target.value)} placeholder="https://www.mywebsite.com/some-article"
                  style={{ width: "100%", padding: "10px 14px", border: "1px solid #d0d0cc", borderRadius: 4, fontSize: 12, fontFamily: "'DM Mono',monospace", boxSizing: "border-box" }} />
              </div>
            )}
            {lookupMode === "sitemap" && (
              <div style={{ marginBottom: 16 }}>
                <label style={{ display: "block", fontSize: 11, fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.08em", color: "#888", marginBottom: 6 }}>Sitemap or Feed URL</label>
                <input type="text" value={sitemapUrl} onChange={e => setSitemapUrl(e.target.value)} placeholder="https://www.mywebsite.com/sitemap.xml"
                  style={{ width: "100%", padding: "10px 14px", border: "1px solid #d0d0cc", borderRadius: 4, fontSize: 12, fontFamily: "'DM Mono',monospace", boxSizing: "border-box" }} />
                <div style={{ fontSize: 11, color: "#888", marginTop: 4 }}>Works with XML sitemaps (and sitemap indexes), RSS, and Atom feeds. Capped at 100 URLs.</div>
              </div>
            )}
            {lookupMode === "list" && (() => {
              const detected = parseUrlList(pastedUrls).length;
              const rawLines = pastedUrls.split(/\r?\n/).map(s => s.trim()).filter(Boolean).length;
              const skipped = rawLines - detected;
              return (
                <div style={{ marginBottom: 16 }}>
                  <label style={{ display: "block", fontSize: 11, fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.08em", color: "#888", marginBottom: 6 }}>URL List</label>
                  <textarea value={pastedUrls} onChange={e => setPastedUrls(e.target.value)} placeholder={"https://www.mywebsite.com/page-1\nhttps://www.mywebsite.com/page-2\nhttps://www.mywebsite.com/page-3"}
                    style={{ width: "100%", height: 140, padding: "10px 14px", border: "1px solid #d0d0cc", borderRadius: 4, fontSize: 12, fontFamily: "'DM Mono',monospace", resize: "vertical", boxSizing: "border-box" }} />
                  <div style={{ fontSize: 11, color: "#888", marginTop: 4 }}>
                    One URL per line, each starting with http:// or https://. Capped at 100.
                    {pastedUrls.trim() && <span style={{ marginLeft: 8, fontFamily: "'DM Mono',monospace", color: detected > 0 ? "#555" : "#d4230f" }}>{detected} detected{skipped > 0 ? ` · ${skipped} skipped` : ""}</span>}
                  </div>
                </div>
              );
            })()}

            <div style={{ marginBottom: 16 }}>
              <label style={{ display: "block", fontSize: 11, fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.08em", color: "#888", marginBottom: 6 }}>Form Factor</label>
              <FormFactorSelect value={urlFormFactor} onChange={setUrlFormFactor} options={FORM_FACTORS_CRUX} />
            </div>

            {lookupMode === "single" && (<>
            {/* Snapshot section */}
            <div style={{ paddingBottom: 32, marginBottom: 32, borderBottom: "1px solid #e0e0dc" }}>
              <h3 style={{ fontSize: 15, fontWeight: 700, letterSpacing: "0.04em", color: "#1a1a2e", margin: "0 0 14px", display: "flex", alignItems: "center", gap: 10 }}>
                <span style={{ background: "#1a1a2e", color: "#fff", fontSize: 11, padding: "3px 9px", borderRadius: 3, fontWeight: 700, letterSpacing: 0 }}>A</span>
                Current Snapshot <span style={{ fontSize: 12, fontWeight: 500, color: "#888", letterSpacing: 0 }}>(28-day rolling)</span>
              </h3>

              <div style={{ display: "flex", gap: 10, alignItems: "center", marginBottom: 12 }}>
                <button onClick={runUrlSnapshot} disabled={urlSnapRunning} style={{ padding: "10px 22px", background: urlSnapRunning ? "#888" : "#d4230f", color: "#fff", border: "none", borderRadius: 4, cursor: urlSnapRunning ? "default" : "pointer", fontSize: 13, fontWeight: 600, fontFamily: "inherit" }}>
                  {urlSnapRunning ? "Running…" : "Run Snapshot"}
                </button>
                {urlSnapRunning && <span style={{ fontSize: 12, color: "#666", fontFamily: "'DM Mono',monospace" }}>querying CrUX…</span>}
              </div>

              {urlSnapshotErr && <div style={{ background: "rgba(212,35,15,0.06)", border: "1px solid rgba(212,35,15,0.2)", borderRadius: 4, padding: "10px 14px", fontSize: 12, color: "#d4230f", marginBottom: 12 }}>{urlSnapshotErr}</div>}

              {urlSnapshotData && (
                <div style={{ marginTop: 16 }}>
                  <div style={{ fontSize: 11, color: "#888", marginBottom: 10, fontFamily: "'DM Mono',monospace", wordBreak: "break-all" }}>
                    {urlSnapshotData.record?.key?.url || urlSnapshotData.record?.key?.origin || "—"}
                    {urlSnapshotData.record?.key?.formFactor && <span> · {urlSnapshotData.record.key.formFactor}</span>}
                  </div>
                  <div style={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: 10 }}>
                    {["interaction_to_next_paint", "largest_contentful_paint", "cumulative_layout_shift"].map(m => {
                      const d = urlSnapshotData.record?.metrics?.[m];
                      if (!d) return <div key={m} style={{ background: "#f5f5f3", borderRadius: 6, padding: "14px 16px", fontSize: 12, color: "#aaa" }}>No {METRIC_LABELS[m]} data</div>;
                      const p75 = d.percentiles?.p75;
                      const hist = d.histogram || [];
                      const distributions = hist.map(h => ({ proportion: h.density }));
                      return <StatusCard key={m} metric={m} p75={p75} distributions={distributions} />;
                    })}
                  </div>

                  <LcpBreakdown metrics={urlSnapshotData.record?.metrics} />

                  {(() => {
                    const supporting = ["first_contentful_paint", "experimental_time_to_first_byte"].filter(m => urlSnapshotData.record?.metrics?.[m]);
                    if (supporting.length === 0) return null;
                    return (
                      <div style={{ marginTop: 12 }}>
                        <div style={{ fontSize: 10, fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.08em", color: "#888", marginBottom: 6 }}>Supporting metrics</div>
                        <div style={{ display: "grid", gridTemplateColumns: `repeat(${supporting.length}, 1fr)`, gap: 10 }}>
                          {supporting.map(m => {
                            const d = urlSnapshotData.record.metrics[m];
                            const p75 = d.percentiles?.p75;
                            const hist = d.histogram || [];
                            const distributions = hist.map(h => ({ proportion: h.density }));
                            return <StatusCard key={m} metric={m} p75={p75} distributions={distributions} />;
                          })}
                        </div>
                      </div>
                    );
                  })()}

                  <ContextStrip metrics={urlSnapshotData.record?.metrics} />

                  {urlSnapshotData.record?.collectionPeriod && (
                    <div style={{ fontSize: 11, color: "#888", marginTop: 10 }}>
                      Collection period: {(() => { const f = urlSnapshotData.record.collectionPeriod.firstDate; const l = urlSnapshotData.record.collectionPeriod.lastDate; return f && l ? `${f.month}/${f.day}/${f.year} — ${l.month}/${l.day}/${l.year}` : "—"; })()}
                    </div>
                  )}
                </div>
              )}

              <details style={{ marginTop: 16, background: "#fff", border: "1px solid #e0e0dc", borderRadius: 6, padding: "10px 14px" }}>
                <summary style={{ fontSize: 12, fontWeight: 600, color: "#555", cursor: "pointer", userSelect: "none" }}>
                  Advanced: run via curl
                </summary>
                <div style={{ fontSize: 12, color: "#666", margin: "10px 0", lineHeight: 1.6 }}>
                  Prefer to run this locally? Copy the command below, save the output as <code style={{ background: "#eee", padding: "1px 5px", borderRadius: 3, fontFamily: "'DM Mono',monospace" }}>crux_url_snapshot.json</code>, and paste it back here to visualize.
                </div>
                <CopyBlock text={urlSnapshotCurl(urlInput, urlApiKey, urlFormFactor)} copied={copied.urlSnap} onCopy={() => copy(urlSnapshotCurl(urlInput, urlApiKey, urlFormFactor), "urlSnap")} />
                <label style={{ display: "block", fontSize: 11, fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.08em", color: "#888", marginBottom: 6 }}>Paste crux_url_snapshot.json here:</label>
                <textarea value={urlSnapshotJson} onChange={e => setUrlSnapshotJson(e.target.value)} placeholder='{"record":{"key":{"url":"..."},"metrics":...}}'
                  style={{ width: "100%", height: 80, padding: "10px 14px", border: "1px solid #d0d0cc", borderRadius: 4, fontSize: 11, fontFamily: "'DM Mono',monospace", resize: "vertical", boxSizing: "border-box" }} />
                <button onClick={parseUrlSnapshot} style={{ marginTop: 8, padding: "8px 20px", background: "#1a1a2e", color: "#fff", border: "none", borderRadius: 4, cursor: "pointer", fontSize: 12, fontWeight: 600, fontFamily: "inherit" }}>Visualize Snapshot</button>
              </details>
            </div>

            {/* History section */}
            <div>
              <h3 style={{ fontSize: 15, fontWeight: 700, letterSpacing: "0.04em", color: "#1a1a2e", margin: "0 0 14px", display: "flex", alignItems: "center", gap: 10 }}>
                <span style={{ background: "#1a1a2e", color: "#fff", fontSize: 11, padding: "3px 9px", borderRadius: 3, fontWeight: 700, letterSpacing: 0 }}>B</span>
                40-Week History
              </h3>

              <div style={{ display: "flex", gap: 10, alignItems: "center", marginBottom: 12 }}>
                <button onClick={runUrlHistory} disabled={urlHistRunning} style={{ padding: "10px 22px", background: urlHistRunning ? "#888" : "#d4230f", color: "#fff", border: "none", borderRadius: 4, cursor: urlHistRunning ? "default" : "pointer", fontSize: 13, fontWeight: 600, fontFamily: "inherit" }}>
                  {urlHistRunning ? "Running…" : "Run History"}
                </button>
                {urlHistRunning && <span style={{ fontSize: 12, color: "#666", fontFamily: "'DM Mono',monospace" }}>querying CrUX History…</span>}
              </div>

              {urlHistoryErr && <div style={{ background: "rgba(212,35,15,0.06)", border: "1px solid rgba(212,35,15,0.2)", borderRadius: 4, padding: "10px 14px", fontSize: 12, color: "#d4230f", marginTop: 12 }}>{urlHistoryErr}</div>}

              {urlHistoryData && (
                <div style={{ marginTop: 16 }}>
                  <div style={{ fontSize: 11, color: "#888", marginBottom: 10, fontFamily: "'DM Mono',monospace", wordBreak: "break-all" }}>
                    {urlHistoryData.record?.key?.url || "—"}
                  </div>
                  {["interaction_to_next_paint", "largest_contentful_paint", "cumulative_layout_shift"].map(m => {
                    if (!urlHistoryData.record.metrics[m]) return null;
                    const p75s = urlHistoryData.record.metrics[m].percentilesTimeseries?.p75s || [];
                    const latest = p75s[p75s.length - 1];
                    const s = latest != null ? status(m, parseFloat(latest)) : "unknown";
                    return (
                      <div key={m} style={{ marginBottom: 24 }}>
                        <div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 8, flexWrap: "wrap" }}>
                          <h3 style={{ fontSize: 14, fontWeight: 600, margin: 0 }}>{METRIC_LABELS[m]}</h3>
                          {latest != null && <span style={{ fontSize: 13, fontWeight: 700, color: SC[s], fontFamily: "'DM Mono',monospace" }}>{fmt(m, parseFloat(latest))}{METRIC_UNITS[m]}</span>}
                          <span style={{ fontSize: 10, background: "#eee", padding: "2px 8px", borderRadius: 3, color: "#666" }}>
                            Good ≤ {m.includes("layout") ? METRIC_THRESHOLDS[m].good.toFixed(1) : METRIC_THRESHOLDS[m].good}{METRIC_UNITS[m]} · Poor &gt; {m.includes("layout") ? METRIC_THRESHOLDS[m].poor.toFixed(2) : METRIC_THRESHOLDS[m].poor}{METRIC_UNITS[m]}
                          </span>
                        </div>
                        <div style={{ background: "#fff", border: "1px solid #e0e0dc", borderRadius: 6, overflow: "hidden" }}>
                          <canvas ref={el => (urlCanvasRefs.current[m] = el)} style={{ width: "100%", height: 200, display: "block" }} />
                        </div>
                      </div>
                    );
                  })}
                  <div style={{ fontSize: 11, color: "#888" }}>Red zone = March 27 core update window. {urlHistoryData.record.collectionPeriods?.length} weekly periods shown.</div>
                </div>
              )}

              <details style={{ marginTop: 16, background: "#fff", border: "1px solid #e0e0dc", borderRadius: 6, padding: "10px 14px" }}>
                <summary style={{ fontSize: 12, fontWeight: 600, color: "#555", cursor: "pointer", userSelect: "none" }}>
                  Advanced: run via curl
                </summary>
                <div style={{ fontSize: 12, color: "#666", margin: "10px 0", lineHeight: 1.6 }}>
                  Prefer to run this locally? Copy the command below, save the output as <code style={{ background: "#eee", padding: "1px 5px", borderRadius: 3, fontFamily: "'DM Mono',monospace" }}>crux_url_history.json</code>, and paste it back here to visualize.
                </div>
                <CopyBlock text={urlHistoryCurl(urlInput, urlApiKey, urlFormFactor)} copied={copied.urlHist} onCopy={() => copy(urlHistoryCurl(urlInput, urlApiKey, urlFormFactor), "urlHist")} />
                <label style={{ display: "block", fontSize: 11, fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.08em", color: "#888", marginBottom: 6 }}>Paste crux_url_history.json here:</label>
                <textarea value={urlHistoryJson} onChange={e => setUrlHistoryJson(e.target.value)} placeholder='{"record":{"metrics":...}}'
                  style={{ width: "100%", height: 80, padding: "10px 14px", border: "1px solid #d0d0cc", borderRadius: 4, fontSize: 11, fontFamily: "'DM Mono',monospace", resize: "vertical", boxSizing: "border-box" }} />
                <button onClick={parseUrlHistory} style={{ marginTop: 8, padding: "8px 20px", background: "#1a1a2e", color: "#fff", border: "none", borderRadius: 4, cursor: "pointer", fontSize: 12, fontWeight: 600, fontFamily: "inherit" }}>Visualize History</button>
              </details>
            </div>
            </>)}

            {lookupMode === "sitemap" && (
              <div>
                <h3 style={{ fontSize: 15, fontWeight: 700, letterSpacing: "0.04em", color: "#1a1a2e", margin: "0 0 14px", display: "flex", alignItems: "center", gap: 10 }}>
                  <span style={{ background: "#1a1a2e", color: "#fff", fontSize: 11, padding: "3px 9px", borderRadius: 3, fontWeight: 700, letterSpacing: 0 }}>A</span>
                  Batch Snapshot <span style={{ fontSize: 12, fontWeight: 500, color: "#888", letterSpacing: 0 }}>(from sitemap / feed)</span>
                </h3>
                <div style={{ fontSize: 12, color: "#666", marginBottom: 12, lineHeight: 1.6 }}>
                  Enter a sitemap or feed URL above, then click Run. The app fetches the sitemap via a server proxy, extracts up to 100 URLs, and queries CrUX directly from your browser for each (spaced at ~450ms to stay under the 150 QPM quota).
                </div>

                <div style={{ display: "flex", gap: 8, alignItems: "center", marginBottom: 12 }}>
                  {!batchRunning ? (
                    <button onClick={runBatch} style={{ padding: "10px 22px", background: "#d4230f", color: "#fff", border: "none", borderRadius: 4, cursor: "pointer", fontSize: 13, fontWeight: 600, fontFamily: "inherit" }}>Run Batch</button>
                  ) : (
                    <button onClick={cancelBatch} style={{ padding: "9px 21px", background: "transparent", color: "#d4230f", border: "1px solid #d4230f", borderRadius: 4, cursor: "pointer", fontSize: 13, fontWeight: 600, fontFamily: "inherit" }}>Cancel</button>
                  )}
                  {batchRunning && batchProgress.total > 0 && (
                    <span style={{ fontSize: 12, color: "#666", fontFamily: "'DM Mono',monospace" }}>
                      {batchProgress.current} / {batchProgress.total}
                    </span>
                  )}
                </div>

                {batchRunning && (
                  <div style={{ background: "#fff", border: "1px solid #e0e0dc", borderRadius: 6, padding: "12px 14px", marginBottom: 12 }}>
                    {batchProgress.total > 0 && (
                      <div style={{ height: 6, background: "#f0f0ec", borderRadius: 3, overflow: "hidden", marginBottom: 8 }}>
                        <div style={{ width: `${(batchProgress.current / batchProgress.total) * 100}%`, background: "#1a1a2e", height: "100%", transition: "width 0.2s ease" }} />
                      </div>
                    )}
                    <div style={{ fontSize: 11, color: "#888", fontFamily: "'DM Mono',monospace", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
                      {batchProgress.currentUrl || "…"}
                    </div>
                  </div>
                )}

                {batchErr && <div style={{ background: "rgba(212,35,15,0.06)", border: "1px solid rgba(212,35,15,0.2)", borderRadius: 4, padding: "10px 14px", fontSize: 12, color: "#d4230f", marginBottom: 12 }}>{batchErr}</div>}

                {batchData && <BatchResults data={batchData} />}

                <details style={{ marginTop: 20, background: "#fff", border: "1px solid #e0e0dc", borderRadius: 6, padding: "10px 14px" }} open={showOfflineScript} onToggle={e => setShowOfflineScript(e.target.open)}>
                  <summary style={{ fontSize: 12, fontWeight: 600, color: "#555", cursor: "pointer", userSelect: "none" }}>
                    Advanced: run offline via bash script
                  </summary>
                  <div style={{ fontSize: 12, color: "#666", margin: "10px 0", lineHeight: 1.6 }}>
                    Prefer to run this locally? Copy the script below, save as <code style={{ background: "#eee", padding: "1px 5px", borderRadius: 3, fontFamily: "'DM Mono',monospace" }}>crux_batch.sh</code>, <code style={{ background: "#eee", padding: "1px 5px", borderRadius: 3, fontFamily: "'DM Mono',monospace" }}>chmod +x</code>, and run. Paste the resulting JSON into the box below to visualize.
                  </div>
                  <CopyBlock text={sitemapScript(sitemapUrl, urlApiKey, urlFormFactor)} copied={copied.batch} onCopy={() => copy(sitemapScript(sitemapUrl, urlApiKey, urlFormFactor), "batch")} maxHeight={400} />
                  <label style={{ display: "block", fontSize: 11, fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.08em", color: "#888", marginBottom: 6 }}>Paste crux_batch.json here:</label>
                  <textarea value={batchJson} onChange={e => setBatchJson(e.target.value)} placeholder='{"batch":[{"url":"...","response":{"record":{...}}}, ...]}'
                    style={{ width: "100%", height: 100, padding: "10px 14px", border: "1px solid #d0d0cc", borderRadius: 4, fontSize: 11, fontFamily: "'DM Mono',monospace", resize: "vertical", boxSizing: "border-box" }} />
                  <button onClick={parseBatch} style={{ marginTop: 8, padding: "8px 20px", background: "#1a1a2e", color: "#fff", border: "none", borderRadius: 4, cursor: "pointer", fontSize: 12, fontWeight: 600, fontFamily: "inherit" }}>Visualize Batch</button>
                </details>
              </div>
            )}

            {lookupMode === "list" && (
              <div>
                <h3 style={{ fontSize: 15, fontWeight: 700, letterSpacing: "0.04em", color: "#1a1a2e", margin: "0 0 14px", display: "flex", alignItems: "center", gap: 10 }}>
                  <span style={{ background: "#1a1a2e", color: "#fff", fontSize: 11, padding: "3px 9px", borderRadius: 3, fontWeight: 700, letterSpacing: 0 }}>A</span>
                  Batch Snapshot <span style={{ fontSize: 12, fontWeight: 500, color: "#888", letterSpacing: 0 }}>(from URL list)</span>
                </h3>
                <div style={{ fontSize: 12, color: "#666", marginBottom: 12, lineHeight: 1.6 }}>
                  Paste a list of URLs above (one per line), then click Run. The app queries CrUX directly from your browser for each URL, spaced at ~450ms to stay under the 150 QPM quota.
                </div>

                <div style={{ display: "flex", gap: 8, alignItems: "center", marginBottom: 12 }}>
                  {!batchRunning ? (
                    <button onClick={runListBatch} style={{ padding: "10px 22px", background: "#d4230f", color: "#fff", border: "none", borderRadius: 4, cursor: "pointer", fontSize: 13, fontWeight: 600, fontFamily: "inherit" }}>Run Batch</button>
                  ) : (
                    <button onClick={cancelBatch} style={{ padding: "9px 21px", background: "transparent", color: "#d4230f", border: "1px solid #d4230f", borderRadius: 4, cursor: "pointer", fontSize: 13, fontWeight: 600, fontFamily: "inherit" }}>Cancel</button>
                  )}
                  {batchRunning && batchProgress.total > 0 && (
                    <span style={{ fontSize: 12, color: "#666", fontFamily: "'DM Mono',monospace" }}>
                      {batchProgress.current} / {batchProgress.total}
                    </span>
                  )}
                </div>

                {batchRunning && (
                  <div style={{ background: "#fff", border: "1px solid #e0e0dc", borderRadius: 6, padding: "12px 14px", marginBottom: 12 }}>
                    {batchProgress.total > 0 && (
                      <div style={{ height: 6, background: "#f0f0ec", borderRadius: 3, overflow: "hidden", marginBottom: 8 }}>
                        <div style={{ width: `${(batchProgress.current / batchProgress.total) * 100}%`, background: "#1a1a2e", height: "100%", transition: "width 0.2s ease" }} />
                      </div>
                    )}
                    <div style={{ fontSize: 11, color: "#888", fontFamily: "'DM Mono',monospace", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
                      {batchProgress.currentUrl || "…"}
                    </div>
                  </div>
                )}

                {batchErr && <div style={{ background: "rgba(212,35,15,0.06)", border: "1px solid rgba(212,35,15,0.2)", borderRadius: 4, padding: "10px 14px", fontSize: 12, color: "#d4230f", marginBottom: 12 }}>{batchErr}</div>}

                {batchData && <BatchResults data={batchData} />}
              </div>
            )}

            <div style={{ marginTop: 20, background: "rgba(212,140,0,0.06)", border: "1px solid rgba(212,140,0,0.2)", borderRadius: 6, padding: "12px 16px", fontSize: 12, color: "#666", lineHeight: 1.7 }}>
              <strong style={{ color: "#d48c00" }}>Note:</strong> URL-level CrUX data is only available for pages with sufficient Chrome traffic. If you get a NOT_FOUND error, fall back to the origin-level data in tabs ①–② or use lab tools (DevTools, WebPageTest) for that specific page.
            </div>
          </div>
        )}

        {/* ============ BIGQUERY ============ */}
        {tab === "bigquery" && (() => {
          const selectedMonths = bqMonths.filter(m => m.selected);
          const sql = generateBqSql(bqOrigin, bqUrl, selectedMonths, bqDevice);

          // Generate a wider range of months the user can toggle (12 months back from current)
          const allMonths = [];
          const now = new Date();
          for (let i = 0; i <= 11; i++) {
            const d = new Date(now.getFullYear(), now.getMonth() - i, 1);
            allMonths.push({ year: d.getFullYear(), month: d.getMonth() });
          }

          const isSelected = (y, m) => bqMonths.some(b => b.year === y && b.month === m && b.selected);
          const toggleMonth = (y, m) => {
            const exists = bqMonths.find(b => b.year === y && b.month === m);
            if (exists) {
              setBqMonths(prev => prev.map(b => b.year === y && b.month === m ? { ...b, selected: !b.selected } : b));
            } else {
              setBqMonths(prev => [...prev, { year: y, month: m, selected: true }].sort((a, b) => (a.year * 12 + a.month) - (b.year * 12 + b.month)));
            }
          };

          return (
          <div>
            <div style={{ background: "#fff", border: "1px solid #e0e0dc", borderRadius: 6, padding: "16px 20px", marginBottom: 16, fontSize: 13, color: "#555", lineHeight: 1.7 }}>
              Run in <a href="https://console.cloud.google.com/bigquery" target="_blank" rel="noopener" style={{ color: "#1a1a2e", fontWeight: 600 }}>BigQuery Console</a>. Select months and device type below — the SQL updates automatically. The most recent month's table may not be available yet.
            </div>

            {/* Origin + URL inputs */}
            <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12, marginBottom: 16 }}>
              <div>
                <label style={{ display: "block", fontSize: 11, fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.08em", color: "#888", marginBottom: 6 }}>Origin</label>
                <input type="text" value={bqOrigin} onChange={e => setBqOrigin(e.target.value)} placeholder="https://www.example.com"
                  style={{ width: "100%", padding: "10px 14px", border: "1px solid #d0d0cc", borderRadius: 4, fontSize: 12, fontFamily: "'DM Mono',monospace", boxSizing: "border-box" }} />
              </div>
              <div>
                <label style={{ display: "block", fontSize: 11, fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.08em", color: "#888", marginBottom: 6 }}>URL <span style={{ fontWeight: 400, textTransform: "none", letterSpacing: 0 }}>(for bonus comparison query)</span></label>
                <input type="text" value={bqUrl} onChange={e => setBqUrl(e.target.value)} placeholder="https://www.example.com/page"
                  style={{ width: "100%", padding: "10px 14px", border: "1px solid #d0d0cc", borderRadius: 4, fontSize: 12, fontFamily: "'DM Mono',monospace", boxSizing: "border-box" }} />
              </div>
            </div>

            {/* Month selector */}
            <div style={{ marginBottom: 16 }}>
              <label style={{ display: "block", fontSize: 11, fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.08em", color: "#888", marginBottom: 6 }}>Months to include</label>
              <div style={{ display: "flex", gap: 6, marginBottom: 10 }}>
                {[
                  { label: "Last 3", fn: () => { const s = allMonths.slice(0, 3); setBqMonths(allMonths.map(m => ({ ...m, selected: s.some(l => l.year === m.year && l.month === m.month) }))); } },
                  { label: "Last 6", fn: () => { const s = allMonths.slice(0, 6); setBqMonths(allMonths.map(m => ({ ...m, selected: s.some(l => l.year === m.year && l.month === m.month) }))); } },
                  { label: "All 12", fn: () => setBqMonths(allMonths.map(m => ({ ...m, selected: true }))) },
                  { label: "Clear", fn: () => setBqMonths(allMonths.map(m => ({ ...m, selected: false }))) },
                ].map(preset => (
                  <button key={preset.label} onClick={preset.fn} style={{
                    padding: "5px 12px", fontSize: 11, fontWeight: 500,
                    background: "rgba(26,26,46,0.05)", color: "#1a1a2e",
                    border: "1px dashed #aaa", borderRadius: 12,
                    cursor: "pointer", fontFamily: "inherit",
                  }}>{preset.label}</button>
                ))}
              </div>
              <div style={{ display: "flex", flexWrap: "wrap", gap: 4 }}>
                {allMonths.map(({ year, month }) => {
                  const sel = isSelected(year, month);
                  const label = new Date(year, month).toLocaleDateString("en-US", { month: "short", year: "2-digit" });
                  const isFuture = new Date(year, month) > now;
                  return (
                    <button key={`${year}-${month}`} onClick={() => toggleMonth(year, month)} style={{
                      padding: "6px 10px", fontSize: 11, fontWeight: sel ? 600 : 400,
                      background: sel ? "#1a1a2e" : "#fff",
                      color: sel ? "#fff" : isFuture ? "#ccc" : "#888",
                      border: `1px solid ${sel ? "#1a1a2e" : "#d0d0cc"}`,
                      borderRadius: 3, cursor: "pointer", fontFamily: "'DM Mono', monospace",
                      opacity: isFuture ? 0.5 : 1,
                    }}>{label}</button>
                  );
                })}
              </div>
            </div>

            {/* Device selector */}
            <div style={{ marginBottom: 16 }}>
              <label style={{ display: "block", fontSize: 11, fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.08em", color: "#888", marginBottom: 6 }}>Device</label>
              <FormFactorSelect value={bqDevice} onChange={setBqDevice} options={BQ_DEVICES} />
            </div>

            {selectedMonths.length === 0 ? (
              <div style={{ background: "#f5f5f3", borderRadius: 6, padding: "32px 20px", textAlign: "center", color: "#888", fontSize: 13 }}>Select at least one month to generate the query.</div>
            ) : (
              <CopyBlock text={sql} copied={copied.bq} onCopy={() => copy(sql, "bq")} maxHeight={600} />
            )}

            <div style={{ background: "rgba(212,140,0,0.06)", border: "1px solid rgba(212,140,0,0.2)", borderRadius: 6, padding: "12px 16px", fontSize: 12, color: "#666", lineHeight: 1.7 }}>
              <strong style={{ color: "#d48c00" }}>Comparing sources:</strong> PSI = live 28-day snapshot. CrUX History = weekly over 40 weeks (best for pinpointing the regression). BigQuery = monthly (best for month-over-month + slicing by country/connection). If INP was already poor before March 27 across all sources, the issue predates the core update.
            </div>
          </div>
          );
        })()}
      </div>

      <footer style={{ background: "#1a1a2e", borderTop: "3px solid #d4230f", marginTop: 48, color: "#888" }}>
        <div style={{ maxWidth: 960, margin: "0 auto", padding: "28px", display: "flex", flexWrap: "wrap", gap: 24, justifyContent: "space-between", alignItems: "flex-start" }}>
          <div>
            <div style={{ fontSize: 14, fontWeight: 700, color: "#fff", marginBottom: 4 }}>CrUX Lens</div>
            <div style={{ fontSize: 11, fontFamily: "'DM Mono',monospace" }}>PSI · CrUX API · BigQuery</div>
          </div>
          <div style={{ display: "flex", gap: 28, fontSize: 12 }}>
            <div>
              <div style={{ fontSize: 10, letterSpacing: "0.14em", textTransform: "uppercase", color: "#666", marginBottom: 8, fontWeight: 500 }}>Data sources</div>
              <a href="https://pagespeed.web.dev/" target="_blank" rel="noopener" style={{ display: "block", color: "#c8d6e5", textDecoration: "none", marginBottom: 4 }}>PageSpeed Insights ↗</a>
              <a href="https://developer.chrome.com/docs/crux/api" target="_blank" rel="noopener" style={{ display: "block", color: "#c8d6e5", textDecoration: "none", marginBottom: 4 }}>CrUX API ↗</a>
              <a href="https://developer.chrome.com/docs/crux/bigquery" target="_blank" rel="noopener" style={{ display: "block", color: "#c8d6e5", textDecoration: "none" }}>CrUX on BigQuery ↗</a>
            </div>
            <div>
              <div style={{ fontSize: 10, letterSpacing: "0.14em", textTransform: "uppercase", color: "#666", marginBottom: 8, fontWeight: 500 }}>Project</div>
              <a href="https://web.dev/articles/vitals" target="_blank" rel="noopener" style={{ display: "block", color: "#c8d6e5", textDecoration: "none", marginBottom: 4 }}>About Core Web Vitals ↗</a>
              <a href="https://www.gnu.org/licenses/agpl-3.0.html" target="_blank" rel="noopener" style={{ display: "block", color: "#c8d6e5", textDecoration: "none" }}>License: AGPL-3.0 ↗</a>
            </div>
          </div>
        </div>
        <div style={{ borderTop: "1px solid #2a2a3e" }}>
          <div style={{ maxWidth: 960, margin: "0 auto", padding: "14px 28px", fontSize: 11, fontFamily: "'DM Mono',monospace", color: "#666", display: "flex", justifyContent: "space-between", flexWrap: "wrap", gap: 8 }}>
            <span>Not affiliated with Google. CrUX is a public dataset from the Chrome team.</span>
            <span>© {new Date().getFullYear()}</span>
          </div>
        </div>
      </footer>
    </div>
  );
}
