/* AcquiCup — leaderboards: Individual, Companies, Beat Acquisit */
const { useState, useEffect, useRef, useMemo } = React;

/* ----------------------------- bump chart ----------------------------- */
// series: [{ id, label, color, ranks:[...], highlight, node }]
//   node — optional React element (an <Avatar>/<CompanyLogo>) rendered at the line's
//   end as the series' logo. Lines are thin + smoothed (no per-point dots); hovering a
//   line reveals that entity's rank at the nearest matchday.

// Smooth an array of [x,y] points into an SVG path `d` using a Catmull-Rom→bezier pass
// (low tension so the curve hugs the data without overshooting). 0–1 points → empty.
function smoothPath(pts) {
  if (!pts.length) return "";
  if (pts.length === 1) return `M ${pts[0][0]},${pts[0][1]}`;
  const t = 0.16;
  let d = `M ${pts[0][0]},${pts[0][1]}`;
  for (let i = 0; i < pts.length - 1; i++) {
    const p0 = pts[i - 1] || pts[i];
    const p1 = pts[i];
    const p2 = pts[i + 1];
    const p3 = pts[i + 2] || p2;
    const c1x = p1[0] + (p2[0] - p0[0]) * t, c1y = p1[1] + (p2[1] - p0[1]) * t;
    const c2x = p2[0] - (p3[0] - p1[0]) * t, c2y = p2[1] - (p3[1] - p1[1]) * t;
    d += ` C ${c1x},${c1y} ${c2x},${c2y} ${p2[0]},${p2[1]}`;
  }
  return d;
}

const VISIBLE = 7; // latest matchdays shown before horizontal scrolling kicks in

function BumpChart({ series, dates, maxRank, viewW, compact }) {
  const [hover, setHover] = useState(null); // { id, i } — hovered series + matchday index
  const D = dates.length;
  // Right padding holds the end-of-line logo + name label. Size it to the longest
  // label actually shown (logo ~24 + ~6.6px/char) instead of a fixed reserve, so short
  // names don't leave a big empty gap on the right. Clamp to a sane min/max.
  const maxLabelLen = series.reduce((m, s) => Math.max(m, (s.label || "").length), 0);
  const padL = 26, padT = 18, padB = 30;
  const padR = Math.round(Math.max(52, Math.min(124, maxLabelLen * 6.6 + 40)));
  // Size each matchday step to the measured viewport: with ≤ VISIBLE days the whole series
  // stretches across the full width (no wasted right-hand space); with more, we cap at
  // VISIBLE days per screen and the earlier days scroll into view (default = latest day).
  const avail = Math.max((viewW || 600) - padL - padR, 240);
  const span = Math.max(D - 1, 1);
  const step = D <= VISIBLE ? avail / span : avail / (VISIBLE - 1);
  const plotW = step * span;
  const W = padL + plotW + padR;
  // Y-axis domain + height. Default: rank 1 (top) → maxRank, a fixed 30px per rank.
  // `compact` (the "Me" view) zooms the axis to the rank range actually drawn — so a lone
  // mid-pack line isn't stranded beneath ~100 empty ranks — and caps the height so the card
  // stays short instead of growing with the viewer's rank number.
  let rankLo, rankHi, plotH;
  if (compact) {
    const rs = series.flatMap((s) => s.ranks).filter((r) => Number.isFinite(r));
    const mn = rs.length ? Math.min(...rs) : 1, mx = rs.length ? Math.max(...rs) : 1;
    const pad = Math.max(1, Math.round((mx - mn) * 0.25));
    rankLo = Math.max(1, mn - pad); rankHi = mx + pad;
    plotH = Math.min((rankHi - rankLo) * 30, 360);
  } else {
    rankLo = 1; rankHi = Math.max(maxRank, 1);
    plotH = (rankHi - 1) * 30;
  }
  const rankSpan = rankHi - rankLo;
  const H = padT + padB + plotH + 14;
  const innerW = plotW, innerH = H - padT - padB;
  const x = (d) => padL + (D === 1 ? 0 : step * d);
  const y = (r) => padT + (rankSpan <= 0 ? 0 : (innerH * (r - rankLo)) / rankSpan);
  const guides = compact
    ? (rankSpan <= 2 ? [rankLo, rankHi].filter((v, i, a) => a.indexOf(v) === i) : [rankLo, Math.round((rankLo + rankHi) / 2), rankHi])
    : (maxRank <= 3 ? [1, maxRank] : [1, Math.ceil(maxRank / 2), maxRank]);
  const ordered = [...series].sort((a, b) => (a.highlight ? 1 : 0) - (b.highlight ? 1 : 0)); // highlight drawn last

  // Map a pointer event to the nearest matchday index (in SVG user space, so it's
  // correct regardless of the responsive scale).
  const nearestIndex = (evt) => {
    const svg = evt.currentTarget.ownerSVGElement;
    const pt = svg.createSVGPoint();
    pt.x = evt.clientX; pt.y = evt.clientY;
    const px = pt.matrixTransform(svg.getScreenCTM().inverse()).x;
    let best = 0, bestD = Infinity;
    for (let i = 0; i < D; i++) { const dd = Math.abs(x(i) - px); if (dd < bestD) { bestD = dd; best = i; } }
    return best;
  };

  const active = hover && series.find((s) => s.id === hover.id);
  return (
    <svg viewBox={`0 0 ${W} ${H}`} width={W} height={H} style={{ display: "block", overflow: "visible" }} preserveAspectRatio="xMidYMid meet">
      {guides.map((r) => (
        <g key={"g" + r}>
          <line x1={padL} x2={padL + innerW} y1={y(r)} y2={y(r)} stroke="var(--line-2)" strokeWidth="1" strokeDasharray="2 5" />
          <text x={padL - 8} y={y(r) + 3.5} textAnchor="end" fontSize="9.5" fontFamily="var(--fm)" fill="var(--ink-soft)">{r}</text>
        </g>
      ))}
      {dates.map((d, i) => (
        <text key={"d" + i} x={x(i)} y={H - 8} textAnchor={i === 0 ? "start" : i === D - 1 ? "end" : "middle"} fontSize="9.5" fontFamily="var(--fm)" fill="var(--ink-soft)">{d}</text>
      ))}
      {/* vertical guide at the hovered matchday */}
      {hover && D > 1 && (
        <line x1={x(hover.i)} x2={x(hover.i)} y1={padT - 4} y2={padT + innerH + 4} stroke="var(--line-2)" strokeWidth="1" />
      )}
      {ordered.map((s) => {
        const pts = s.ranks.map((r, i) => [x(i), y(r)]);
        const isActive = active && s.id === active.id;
        const dim = active && !isActive;
        const lx = x(D - 1), ly = y(s.ranks[D - 1]);
        const logoSize = s.highlight ? 24 : 20;
        return (
          <g key={s.id} opacity={dim ? 0.16 : s.highlight || isActive ? 1 : 0.85}>
            <path d={smoothPath(pts)} fill="none" stroke={s.color}
              strokeWidth={s.highlight || isActive ? 2.6 : 1.5} strokeLinejoin="round" strokeLinecap="round" />
            {/* wide invisible hit area so the thin line is easy to hover */}
            <path d={smoothPath(pts)} fill="none" stroke="transparent" strokeWidth="16"
              style={{ cursor: "pointer", pointerEvents: "stroke" }}
              onMouseMove={(e) => setHover({ id: s.id, i: nearestIndex(e) })}
              onMouseEnter={(e) => setHover({ id: s.id, i: nearestIndex(e) })}
              onMouseLeave={() => setHover(null)} />
            {/* end-of-line logo (reuses Avatar/CompanyLogo via foreignObject) */}
            {s.node
              ? <foreignObject x={lx + 5} y={ly - logoSize / 2} width={logoSize} height={logoSize} style={{ overflow: "visible", pointerEvents: "none" }}>{s.node}</foreignObject>
              : <circle cx={lx + 5 + logoSize / 2} cy={ly} r={logoSize / 2} fill="var(--surface)" stroke={s.color} strokeWidth="2.4" />}
            <text x={lx + logoSize + 11} y={ly + 3.6} fontSize={s.highlight ? 11.5 : 10.5} fontFamily="var(--ff)" fontWeight={s.highlight ? 800 : 600} fill="var(--ink)">{s.label}</text>
          </g>
        );
      })}
      {/* hover marker + rank tooltip (drawn last so it sits above every line) */}
      {active && (() => {
        const i = hover.i, r = active.ranks[i];
        const cx = x(i), cy = y(r);
        const label = `${active.label} · #${r}`;
        const tw = Math.max(54, label.length * 6.2 + 16);
        const above = cy - 34 > padT;
        const ty = above ? cy - 30 : cy + 12;
        const tx = Math.min(Math.max(cx - tw / 2, 2), padL + innerW - tw);
        return (
          <g style={{ pointerEvents: "none" }}>
            <circle cx={cx} cy={cy} r={4.2} fill="var(--surface)" stroke={active.color} strokeWidth="2.6" />
            <rect x={tx} y={ty} width={tw} height={22} rx={6} fill="var(--ink)" opacity="0.92" />
            <text x={tx + tw / 2} y={ty + 11} textAnchor="middle" fontSize="9" fontFamily="var(--fm)" fill="rgba(255,255,255,.7)">{dates[i]}</text>
            <text x={tx + tw / 2} y={ty + 19.5} textAnchor="middle" fontSize="10.5" fontFamily="var(--ff)" fontWeight="700" fill="#fff">{label}</text>
          </g>
        );
      })()}
    </svg>
  );
}

function RankChartCard({ title, sub, series, dates, maxRank, controls, compact }) {
  const scrollRef = useRef(null);
  const [viewW, setViewW] = useState(0);

  // Measure the scroll viewport so the chart sizes its matchdays to the real width.
  useEffect(() => {
    const el = scrollRef.current;
    if (!el) return;
    const measure = () => setViewW(el.clientWidth);
    measure();
    window.addEventListener("resize", measure);
    return () => window.removeEventListener("resize", measure);
  }, []);

  // Pin the view to the latest day on the right (re-runs on data/size change).
  useEffect(() => {
    const el = scrollRef.current;
    if (el) el.scrollLeft = el.scrollWidth;
  }, [series, dates, viewW]);

  const scrollable = dates.length > VISIBLE;
  return (
    <div className="card card-pad col gap-12" style={{ padding: "18px 20px" }}>
      <div className="row between wrap gap-8">
        <div className="col gap-2" style={{ flex: "1 1 260px", minWidth: 0 }}>
          <span className="eyebrow">{title}</span>
          {sub && <span className="muted" style={{ fontSize: 12.5 }}>{sub}</span>}
        </div>
        {scrollable && (
          <span className="badge mono" style={{ fontSize: 10.5, flex: "none" }}>
            scroll ← for earlier days
          </span>
        )}
      </div>
      {controls}
      <div ref={scrollRef} style={{ overflowX: "auto" }}>
        <BumpChart series={series} dates={dates} maxRank={maxRank} viewW={viewW} compact={compact} />
      </div>
    </div>
  );
}

/* Multi-select dropdown for the "Select" chart mode — searchable checklist of entities
   (players or companies), each labelled with its current rank. Closes on outside click. */
function SeriesPicker({ entities, selected, onChange, kindLabel }) {
  const [open, setOpen] = useState(false);
  const [q, setQ] = useState("");
  const ref = useRef(null);
  useEffect(() => {
    if (!open) return;
    const onDoc = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    document.addEventListener("mousedown", onDoc);
    return () => document.removeEventListener("mousedown", onDoc);
  }, [open]);
  const ql = q.trim().toLowerCase();
  const list = ql ? entities.filter((e) => e.name.toLowerCase().includes(ql)) : entities;
  const toggle = (id) => onChange(selected.includes(id) ? selected.filter((x) => x !== id) : [...selected, id]);
  return (
    <div ref={ref} style={{ position: "relative", flex: "none" }}>
      <button className="btn btn-ghost btn-sm" onClick={() => setOpen((o) => !o)}>
        {selected.length ? `${selected.length} ${kindLabel} selected` : `Select ${kindLabel}…`}
        <Icon name="chevron" size={14} />
      </button>
      {open && (
        <div className="card" style={{ position: "absolute", right: 0, top: "calc(100% + 6px)", zIndex: 30, width: 270, maxHeight: 340, display: "flex", flexDirection: "column", boxShadow: "var(--shadow-pop)" }}>
          <div style={{ padding: 8, borderBottom: "1px solid var(--line)" }}>
            <input className="field" style={{ padding: "8px 10px", fontSize: 13 }} placeholder={`Search ${kindLabel}…`} value={q} onChange={(e) => setQ(e.target.value)} autoFocus />
          </div>
          <div className="noscroll" style={{ overflowY: "auto", padding: 4 }}>
            {list.length === 0 && <div className="muted" style={{ fontSize: 12.5, padding: 10 }}>No matches</div>}
            {list.map((e) => (
              <label key={e.id} className="row gap-8 hov" style={{ alignItems: "center", padding: "7px 8px", borderRadius: 8, cursor: "pointer", fontSize: 13 }}>
                <input type="checkbox" checked={selected.includes(e.id)} onChange={() => toggle(e.id)} />
                <span className="rank-num muted" style={{ width: 24, fontSize: 11, flex: "none" }}>#{e.rank}</span>
                <span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{e.name}</span>
              </label>
            ))}
          </div>
          {selected.length > 0 && (
            <div className="row between" style={{ padding: 8, borderTop: "1px solid var(--line)" }}>
              <span className="muted" style={{ fontSize: 12 }}>{selected.length} selected</span>
              <button className="btn btn-ghost btn-sm" onClick={() => onChange([])}>Clear</button>
            </div>
          )}
        </div>
      )}
    </div>
  );
}

/* Rank-over-time chart with a view selector: Top 10 / Top 20 / Me / All / Select. Top 10 and
   Top 20 show STRICTLY the top N — the viewer's own line (meId) is highlighted only when it
   falls inside the cut, so the rank axis stays compact even when the viewer ranks far down.
   "Me" (offered only when meId is known) plots the meId line alone, in a compact fixed-height
   chart. `meLabel` renames that tab + caption for profile pages, where the highlighted line is
   the page's subject rather than the viewer (e.g. a player's first name). `entities` is the full
   list sorted best-first ({ id, rank, name }); `buildEntry(entity, {highlight})` maps one to a
   BumpChart series. */
function RankSeriesCard({ title, sub, dates, entities, buildEntry, meId, kindLabel, meLabel = "Me", defaultMode = "top10" }) {
  // Open on `defaultMode`, but fall back to Top 10 if it asks for the "me" tab without a meId.
  const [mode, setMode] = useState(defaultMode === "me" && !meId ? "top10" : defaultMode);
  const [selected, setSelected] = useState(meId ? [meId] : []);

  let chosen;
  if (mode === "select") {
    chosen = entities.filter((e) => selected.includes(e.id));
  } else if (mode === "me") {
    chosen = meId ? entities.filter((e) => e.id === meId) : [];
  } else {
    const n = mode === "top20" ? 20 : mode === "all" ? entities.length : 10;
    chosen = entities.slice(0, n);
  }
  const series = chosen
    .map((e) => buildEntry(e, { highlight: e.id === meId }))
    .filter((s) => Array.isArray(s.ranks) && s.ranks.length);
  const maxRank = series.length ? Math.max(1, ...series.flatMap((s) => s.ranks)) : 1;
  const meShown = !!meId && series.some((s) => s.id === meId);
  const meWord = meLabel === "Me" ? "your line" : meLabel + "'s line";

  const MODES = [
    ["top10", "Top 10"], ["top20", "Top 20"],
    ...(meId ? [["me", meLabel]] : []),
    ["all", "All"], ["select", "Select"],
  ];
  const controls = (
    <div className="row between wrap gap-8" style={{ alignItems: "center" }}>
      <div className="tabs" style={{ flex: "none" }}>
        {MODES.map(([k, lbl]) => (
          <button key={k} className={mode === k ? "on" : ""} onClick={() => setMode(k)}>{lbl}</button>
        ))}
      </div>
      {mode === "select"
        ? <SeriesPicker entities={entities} selected={selected} onChange={setSelected} kindLabel={kindLabel} />
        : <span className="muted" style={{ fontSize: 12 }}>{series.length} shown{meShown ? " · " + meWord + " highlighted" : ""}</span>}
    </div>
  );

  return <RankChartCard title={title} sub={sub} series={series} dates={dates} maxRank={maxRank} controls={controls} compact={mode === "me"} />;
}

// Leaderboard paging: each page holds 20 rows, but only the top 10 show until "Show more"
// expands the page; Pagination navigates between pages of 20.
const PAGE_SIZE = 20;  // rows per page
const COLLAPSED = 10;  // rows shown per page before "Show more"

/* Slice a ranked list to the given page (PAGE_SIZE rows), then to the top COLLAPSED rows
   unless `expanded`, while always keeping the viewer's own row visible (appended below a
   "···" divider if it falls outside the shown rows). Returns the rows to render, whether
   `me` was appended out of sequence, the clamped page index + total page count, and how
   many rows the current page actually holds (so the caller can show/hide "Show more"). */
function paginateWithMe(rows, isMe, page, size, expanded) {
  const pages = Math.max(1, Math.ceil(rows.length / size));
  const p = Math.min(Math.max(page, 0), pages - 1);
  const pageRows = rows.slice(p * size, p * size + size);
  const visible = expanded ? pageRows : pageRows.slice(0, COLLAPSED);
  let meAppended = false;
  if (!visible.some(isMe)) {
    const me = rows.find(isMe);
    if (me) { visible.push(me); meAppended = true; }
  }
  return { visible, meAppended, page: p, pages, pageCount: pageRows.length };
}

/* Centered "Show more / Show less" toggle that expands the current page from the top
   COLLAPSED rows to the full PAGE_SIZE. Hidden when the page holds ≤ COLLAPSED rows. */
function ShowMoreBar({ expanded, onToggle, pageCount }) {
  if (pageCount <= COLLAPSED) return null;
  return (
    <div className="row" style={{ justifyContent: "center" }}>
      <button className="btn btn-ghost btn-sm" onClick={onToggle}>
        {expanded ? "Show less" : `Show more (${pageCount - COLLAPSED} more)`}
        <Icon name="chevron" size={14} style={{ transform: expanded ? "rotate(180deg)" : "none" }} />
      </button>
    </div>
  );
}

/* Centered page selector under a paginated leaderboard table: Prev / numbered pages
   (collapsing to first + last + a window around the current page, with … gaps) / Next. */
function Pagination({ page, pages, onPage }) {
  if (pages <= 1) return null;
  const nums = [];
  for (let i = 0; i < pages; i++) {
    if (i === 0 || i === pages - 1 || Math.abs(i - page) <= 1) nums.push(i);
    else if (nums[nums.length - 1] !== "…") nums.push("…");
  }
  return (
    <div className="row gap-6 wrap" style={{ justifyContent: "center", alignItems: "center" }}>
      <button className="btn btn-ghost btn-sm" disabled={page === 0} onClick={() => onPage(page - 1)}>
        <Icon name="chevron" size={14} style={{ transform: "rotate(90deg)" }} />Prev
      </button>
      {nums.map((n, i) => n === "…"
        ? <span key={"e" + i} className="muted" style={{ padding: "0 4px" }}>…</span>
        : <button key={n} className={"btn btn-sm " + (n === page ? "btn-navy" : "btn-ghost")} onClick={() => onPage(n)}>{n + 1}</button>)}
      <button className="btn btn-ghost btn-sm" disabled={page === pages - 1} onClick={() => onPage(page + 1)}>
        Next<Icon name="chevron" size={14} style={{ transform: "rotate(-90deg)" }} />
      </button>
    </div>
  );
}

/* Footer under a paginated leaderboard table: the "Show more" toggle and the page
   selector sit on the SAME row on desktop (and wrap onto separate centered rows on
   narrow screens). Each child renders null when it isn't needed. */
function LeaderboardFooter({ expanded, onToggle, pageCount, page, pages, onPage }) {
  return (
    <div className="row wrap gap-8" style={{ justifyContent: "center" }}>
      <ShowMoreBar expanded={expanded} onToggle={onToggle} pageCount={pageCount} />
      <Pagination page={page} pages={pages} onPage={onPage} />
    </div>
  );
}

function SubHead({ title, sub, subWidth = "none" }) {
  return (
    <div className="col gap-4">
      <h2 style={{ fontSize: 21 }}>{title}</h2>
      {sub && <span className="muted" style={{ fontSize: 13.5, maxWidth: subWidth }}>{sub}</span>}
    </div>
  );
}

/* Designed hover tooltip for the compact leaderboard/acquiboard stat bars (Accuracy +
   Risk). Rendered through a PORTAL to document.body so the table's horizontal-scroll
   wrapper (overflow:auto clips both axes) never crops it. Fixed-positioned above the
   bar (flips below near the viewport top), caret tracking the bar centre even after the
   tooltip is clamped to the viewport. Shares the .statbar-tip styling of the Dashboard. */
function SegTipCard({ label, headline, segs, total, footer, anchor }) {
  const pct = (v) => (total ? Math.round((v / total) * 100) : 0);
  const vw = window.innerWidth, half = 150, margin = 8;
  const cx = Math.min(Math.max(anchor.cx, half + margin), vw - half - margin);
  // px nudge so the caret still points at the bar after the box is clamped; bounded so
  // it can't slide past the tooltip's own edge when the bar sits near the viewport edge.
  const caretOffset = Math.max(-(half - 22), Math.min(half - 22, anchor.cx - cx));
  const flip = anchor.top < 150;      // not enough room above → drop below the bar
  const style = flip
    ? { position: "fixed", left: cx, top: anchor.bottom + 10, transform: "translateX(-50%)" }
    : { position: "fixed", left: cx, top: anchor.top - 10, transform: "translate(-50%, -100%)" };
  return (
    <div className={"statbar-tip is-fixed" + (flip ? " flip" : "")} style={style}>
      <div className="statbar-tip-head">
        <span className="statbar-tip-title">{label}</span>
        <span className="statbar-tip-pct mono">{headline}%</span>
      </div>
      <div className="statbar-tip-rows">
        {segs.map((s) => (
          <div key={s.label} className="statbar-tip-row">
            <span className="statbar-tip-dot sm" style={{ background: s.color }}></span>
            <span className="statbar-tip-lbl">{s.label}</span>
            <span className="statbar-tip-val mono">{s.value}<span className="statbar-tip-of">/{total}</span></span>
            <span className="statbar-tip-share mono">{pct(s.value)}%</span>
          </div>
        ))}
      </div>
      {footer && <div className="statbar-tip-foot">{footer}</div>}
      <span className="statbar-tip-caret" style={{ left: "calc(50% + " + caretOffset + "px)" }}></span>
    </div>
  );
}

/* Shared compact segmented bar + portal tooltip. `segs` is [{label,value,color}] over
   `total`; `headline` is the trailing % chip and the tooltip's headline number. */
function SegBar({ segs, total, headline, label, footer, width = 120, showPct = true, align = "flex-end" }) {
  const [tip, setTip] = useState(null); // { cx, top, bottom, host } or null
  const ref = useRef(null);
  const pct = (v) => (total ? Math.round((v / total) * 100) : 0);
  const place = () => {
    const el = ref.current;
    if (!el) return;
    const r = el.getBoundingClientRect();
    // Portal into the nearest [data-theme] wrapper (NOT document.body): that element
    // carries the theme CSS vars (--surface/--ink/--line…), so var(--surface) resolves
    // instead of falling back to transparent. It's also above the table's overflow:auto
    // scroll wrapper, and (rootStyle has no transform) position:fixed still tracks the viewport.
    const host = el.closest("[data-theme]") || document.body;
    setTip({ cx: r.left + r.width / 2, top: r.top, bottom: r.bottom, host });
  };
  return (
    <div className="row gap-8" style={{ alignItems: "center", justifyContent: align }}>
      {/* When centered with a trailing % chip, an invisible left spacer mirrors that chip's
          width so the BAR (not the bar+chip group) is centered under a centered column header. */}
      {showPct && align === "center" && <span aria-hidden="true" className="mono" style={{ fontSize: 11.5, minWidth: 30, flex: "none" }}></span>}
      <div ref={ref} onMouseEnter={place} onMouseLeave={() => setTip(null)}
        style={{ display: "flex", width, height: 8, borderRadius: 999, overflow: "hidden", background: "var(--line-2)", flex: "none", cursor: "default" }}>
        {segs.map((s) => s.value > 0 && (
          <div key={s.label} style={{ width: pct(s.value) + "%", background: s.color }}></div>
        ))}
      </div>
      {showPct && <span className="mono muted" style={{ fontSize: 11.5, minWidth: 30, textAlign: "right" }}>{headline}%</span>}
      {tip && ReactDOM.createPortal(
        <SegTipCard label={label} headline={headline} segs={segs} total={total} footer={footer} anchor={tip} />,
        tip.host)}
    </div>
  );
}

/* Compact stacked accuracy bar — exact (navy) / good (blue) / wrong (yellow) over a row's
   SETTLED predictions (settled === exact + good + wrong). Shares the colour language of the
   My-stats donut (screens-core/screens-user). The trailing number is the outcome hit-rate
   (exact + good). Renders a muted dash until at least one pick has settled. */
function AccuracyBar({ exacts, good, settled, width = 120, showPct = true, align = "flex-end" }) {
  const ex = exacts || 0, gd = good || 0, n = settled || 0;
  const wr = Math.max(0, n - ex - gd);
  if (!n) return <span className="muted" style={{ fontSize: 12 }}>–</span>;
  const segs = [
    { label: "Exact", value: ex, color: "var(--brand-navy)" },
    { label: "Good", value: gd, color: "var(--brand-blue)" },
    { label: "Wrong", value: wr, color: "var(--brand-yellow)" },
  ];
  const headline = Math.round(((ex + gd) / n) * 100);
  return <SegBar segs={segs} total={n} headline={headline} label="Accuracy" footer={"of " + n + " settled · " + headline + "% hit rate"} width={width} showPct={showPct} align={align} />;
}

/* Compact stacked SAFETY bar — the inverse framing of the old risk bar. high (navy) /
   medium (blue) / low (yellow) safety over a row's SETTLED, priced picks, split by the
   odds of the OUTCOME the player backed: a favourite (≤20 outcome points ≈ decimal-odds
   × 10, see src/odds.js) is HIGH safety, a long shot (>45) is LOW safety, med 21–45 is in
   between. Shares AccuracyBar's colour language as a bold→safe ramp; the aggregated tier
   counts come from the server (computeStandings → toClientUser/Company/Group) as
   riskLow/riskMed/riskHigh, where a LOW-risk pick maps to HIGH safety. The trailing number
   is a 0–100 safety index (high=1, med=½, low=0, weighted by share = 100 − risk index).
   Muted dash until a priced pick has settled. */
function SafetyBar({ high, med, low, width = 120, showPct = true, align = "flex-end" }) {
  const hi = high || 0, md = med || 0, lo = low || 0;
  const n = hi + md + lo;
  if (!n) return <span className="muted" style={{ fontSize: 12 }}>–</span>;
  const segs = [
    { label: "High", value: hi, color: "var(--brand-navy)" },
    { label: "Medium", value: md, color: "var(--brand-blue)" },
    { label: "Low", value: lo, color: "var(--brand-yellow)" },
  ];
  const index = Math.round(((hi + 0.5 * md) / n) * 100);
  return <SegBar segs={segs} total={n} headline={index} label="Safety profile" footer={"of " + n + " priced · safety index " + index} width={width} showPct={showPct} align={align} />;
}

/* Shared colour key for the Accuracy + Safety stat bars — both share the same navy→blue→
   yellow ramp (Accuracy: exact/good/wrong; Safety: high/medium/low), so a single legend
   covers both columns. Sits above the leaderboard tables. */
function StatLegend({ align = "flex-end", style }) {
  const items = [
    { label: "High", color: "var(--brand-navy)" },
    { label: "Medium", color: "var(--brand-blue)" },
    { label: "Low", color: "var(--brand-yellow)" },
  ];
  return (
    <div className="row gap-12 wrap" style={{ alignItems: "center", justifyContent: align, ...style }}>
      {items.map((it) => (
        <span key={it.label} className="row gap-6" style={{ alignItems: "center" }}>
          <span style={{ width: 10, height: 10, borderRadius: 3, background: it.color, display: "inline-block", flex: "none" }}></span>
          <span className="muted" style={{ fontSize: 12 }}>{it.label}</span>
        </span>
      ))}
    </div>
  );
}

function Leaderboard({ store, embedded }) {
  const A = window.ACQ;
  const narrow = useNarrow(820);
  const [co, setCo] = useState("all");
  const [page, setPage] = useState(0);
  const [expanded, setExpanded] = useState(false);
  const goPage = (p) => { setPage(p); setExpanded(false); }; // new page resets to top 10
  const coName = (id) => (A.COMPANIES.find((c) => c.id === id) || {}).name || id;
  const meId = store.me && store.me.id;

  const ordered = [...A.USERS].sort((a, b) => a.rank - b.rank);
  let rows = ordered;
  if (co !== "all") rows = rows.filter((u) => u.company === co);

  // Time-series chart: full roster (best-first) is offered via the view selector;
  // the default Top 10 view always includes + highlights your own line.
  const indEntities = ordered.map((u) => ({ id: u.id, rank: u.rank, name: userName(u) }));
  const indBuild = (e, { highlight }) => {
    const u = A.USERS.find((x) => x.id === e.id);
    return {
      id: u.id,
      label: u.you ? "You" : userName(u),
      color: u.you ? "var(--brand-navy)" : companyColor(u.company),
      ranks: A.USER_RANK_SERIES[u.id],
      highlight,
      // company logo at the line's end (AI bots keep their brand avatar — they have no real company)
      node: u.isAi
        ? <Avatar user={u} size={highlight ? 24 : 20} />
        : <CompanyLogo id={u.company} size={highlight ? 24 : 20} />,
    };
  };

  // Table: top 10 per page (your own row always kept visible), "Show more" to 20, paginated.
  const { visible, meAppended, page: pg, pages, pageCount } = paginateWithMe(rows, (u) => u.you, page, PAGE_SIZE, expanded);
  const colSpan = narrow ? 5 : 8;

  return (
    <div className="col gap-20">
      {embedded
        ? <SubHead title="Individual leaderboard" sub="Ranked by total points, then exact scores, then correct outcomes. Movement is vs the last ranking update." />
        : <PageHead eyebrow="Standings" title="Individual leaderboard"
            sub="Ranked by total points, then exact scores, then correct outcomes. Movement is vs the last ranking update." />}
      <div className="row between wrap gap-12" style={{ alignItems: "center" }}>
        <span className="muted" style={{ fontSize: 13 }}>{rows.length} players</span>
      </div>

      <StatLegend />
      <div className="card" style={{ overflow: "hidden" }}>
        <div style={{ overflowX: "auto" }}>
          <table className="lb" style={{ width: "100%", tableLayout: narrow ? "fixed" : undefined }}>
            <thead><tr>
              <th style={{ width: narrow ? 38 : 54 }}>Rank</th>
              <th>Player</th>
              <th style={{ textAlign: "right", width: narrow ? 44 : 72 }}>Points</th>
              {!narrow && <th style={{ textAlign: "center", width: 72 }}>Exact</th>}
              {!narrow && <th style={{ textAlign: "center", width: 72 }}>Correct</th>}
              <th style={{ textAlign: "center", width: narrow ? 70 : undefined }}>Accuracy</th>
              <th style={{ textAlign: "center", width: narrow ? 70 : undefined }}>Safety</th>
              {!narrow && <th style={{ textAlign: "right" }}>Best</th>}
            </tr></thead>
            <tbody>
              {visible.map((u, ri) => (<React.Fragment key={u.id}>
                {meAppended && ri === visible.length - 1 && (
                  <tr className="lb-gap"><td colSpan={colSpan} style={{ textAlign: "center", color: "var(--ink-soft)", padding: "4px 0" }}>···</td></tr>
                )}
                <tr className={"hov " + (u.you ? "me-row" : "")}
                  style={{ cursor: store.openUser ? "pointer" : undefined }}
                  onClick={() => store.openUser && store.openUser(u)}>
                  <td><span className="rank-num">{u.rank}</span></td>
                  <td>
                    <span className="row gap-8" style={{ alignItems: "center", minWidth: 0 }}>
                      <CompanyLogo id={u.company} size={22} />
                      <span style={{ fontWeight: 700, fontSize: 14, minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{userName(u)}{u.you ? " · you" : ""}</span>
                      {u.isAi && <AiBadge />}
                    </span>
                  </td>
                  <td style={{ textAlign: "right" }}><span className="mono" style={{ fontWeight: 700, fontSize: 15 }}>{u.points}</span></td>
                  {!narrow && <td className="mono" style={{ textAlign: "center" }}>{u.exacts}</td>}
                  {!narrow && <td className="mono" style={{ textAlign: "center" }}>{u.correct}</td>}
                  <td style={{ textAlign: "center" }}><AccuracyBar exacts={u.exacts} good={u.good} settled={u.settled} width={narrow ? 48 : 120} showPct={!narrow} align="center" /></td>
                  <td style={{ textAlign: "center" }}><SafetyBar high={u.riskLow} med={u.riskMed} low={u.riskHigh} width={narrow ? 48 : 120} showPct={!narrow} align="center" /></td>
                  {!narrow && <td className="mono" style={{ textAlign: "right" }}>+{u.best}</td>}
                </tr>
              </React.Fragment>))}
            </tbody>
          </table>
        </div>
      </div>
      <LeaderboardFooter expanded={expanded} onToggle={() => setExpanded((x) => !x)} pageCount={pageCount} page={pg} pages={pages} onPage={goPage} />
      <RankSeriesCard title="Player ranking over time" sub="Daily rank across the tournament. Your line is highlighted. Switch views to compare more players."
        dates={A.RANK_DATES} entities={indEntities} buildEntry={indBuild} meId={meId} kindLabel="players" />
    </div>
  );
}

/* ----------------------------- COMPANIES ----------------------------- */
function Companies({ store, go, embedded }) {
  const A = window.ACQ;
  const narrow = useNarrow(820);
  const [page, setPage] = useState(0);
  const [expanded, setExpanded] = useState(false);
  const goPage = (p) => { setPage(p); setExpanded(false); };
  const myCo = store.me && store.me.company;
  // Rank companies (table order) once, attaching the 1-based rank used by both the table and picker.
  const rows = [...A.COMPANY_RANK].sort((a, b) => b.avgTop3 - a.avgTop3).map((c, i) => ({ ...c, _rank: i + 1 }));

  // Time-series chart: full company list (best-first) via the view selector; default Top 10
  // always includes + highlights your company.
  const coEntities = rows.map((c) => ({ id: c.id, rank: c._rank, name: c.name, shortName: c.shortName }));
  const coBuild = (e, { highlight }) => ({
    id: e.id,
    label: e.shortName || e.name.split(" ")[0],
    color: companyColor(e.id),
    ranks: A.COMPANY_RANK_SERIES[e.id],
    highlight,
    node: <CompanyLogo id={e.id} size={highlight ? 24 : 20} />,
  });

  const { visible, meAppended, page: pg, pages, pageCount } = paginateWithMe(rows, (c) => c.id === myCo, page, PAGE_SIZE, expanded);
  const colSpan = narrow ? 5 : 8;

  return (
    <div className="col gap-20">
      {embedded
        ? <SubHead title="Company leaderboard" sub="Minimum 3 players to rank. Company score is the average of each company's top 3 players." />
        : <PageHead eyebrow="Standings" title="Company leaderboard"
            sub="Minimum 3 players to rank. Company score is the average of each company's top 3 players." />}
      <div className="row between wrap gap-12" style={{ alignItems: "center" }}>
        <span className="muted" style={{ fontSize: 12.5 }}>{rows.length} companies ranked</span>
      </div>

      <StatLegend />
      <div className="card" style={{ overflow: "hidden" }}>
        <div style={{ overflowX: "auto" }}>
          <table className="lb" style={{ width: "100%", tableLayout: narrow ? "fixed" : undefined }}>
            <thead><tr>
              <th style={{ width: narrow ? 34 : 54 }}>Rank</th>
              <th>Company</th>
              <th style={{ textAlign: "right", width: narrow ? 56 : undefined }}>{narrow ? "Avg pts" : "Avg top 3"}</th>
              {!narrow && <th style={{ textAlign: "center" }}>Players</th>}
              {!narrow && <th>Top players</th>}
              <th style={{ textAlign: "center", width: narrow ? 58 : undefined }}>Accuracy</th>
              <th style={{ textAlign: "center", width: narrow ? 58 : undefined }}>Safety</th>
              {!narrow && <th style={{ textAlign: "right" }}>Total</th>}
            </tr></thead>
            <tbody>
              {visible.map((c, ri) => {
                const rank = c._rank;
                const mine = c.id === myCo;
                return (
                  <React.Fragment key={c.id}>
                  {meAppended && ri === visible.length - 1 && (
                    <tr className="lb-gap"><td colSpan={colSpan} style={{ textAlign: "center", color: "var(--ink-soft)", padding: "4px 0" }}>···</td></tr>
                  )}
                  <tr className={"hov " + (mine ? "me-row" : "")}
                    style={{ cursor: store.openCompany ? "pointer" : undefined }}
                    onClick={() => store.openCompany && store.openCompany(c)}>
                    <td><span className="rank-num">{rank}</span></td>
                    <td>
                      <div className="row gap-12" style={{ alignItems: "center", minWidth: 0 }}>
                        <CompanyLogo id={c.id} size={narrow ? 22 : 28} />
                        <span style={{ fontWeight: 700, fontSize: 14, minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{companyDisplayName(c.name, narrow)}</span>
                        {!narrow && c.isHost && <span className="badge blue">Host</span>}
                        {!narrow && c.isAi && <AiBadge />}
                      </div>
                    </td>
                    <td style={{ textAlign: "right" }}><span className="mono" style={{ fontWeight: 700, fontSize: 15 }}>{c.avgTop3}</span></td>
                    {!narrow && <td className="mono" style={{ textAlign: "center" }}>{c.count}</td>}
                    {!narrow && <td>
                      {c.isAi
                        ? <span className="muted">–</span>
                        : <div className="col" style={{ gap: 3 }}>
                            {c.members.slice(0, 3).map((m, j) => (
                              <span key={m.id} className="row gap-6" style={{ alignItems: "baseline" }}>
                                <span className="rank-num muted" style={{ width: 12, fontSize: 11 }}>{j + 1}</span>
                                <span style={{ fontSize: 13 }}>{userName(m)}</span>
                                <span className="mono muted" style={{ fontSize: 11.5 }}>{m.points}</span>
                              </span>
                            ))}
                          </div>}
                    </td>}
                    <td style={{ textAlign: "center" }}><AccuracyBar exacts={c.exacts} good={c.good} settled={c.settled} width={narrow ? 48 : 120} showPct={!narrow} align="center" /></td>
                    <td style={{ textAlign: "center" }}><SafetyBar high={c.riskLow} med={c.riskMed} low={c.riskHigh} width={narrow ? 48 : 120} showPct={!narrow} align="center" /></td>
                    {!narrow && <td className="mono" style={{ textAlign: "right" }}>{c.total}</td>}
                  </tr>
                  </React.Fragment>
                );
              })}
            </tbody>
          </table>
        </div>
      </div>
      <LeaderboardFooter expanded={expanded} onToggle={() => setExpanded((x) => !x)} pageCount={pageCount} page={pg} pages={pages} onPage={goPage} />
      <RankSeriesCard title="Company ranking over time" sub="Daily company standings across the tournament. Your company is highlighted. Switch views to compare more companies."
        dates={A.RANK_DATES} entities={coEntities} buildEntry={coBuild} meId={myCo} kindLabel="companies" />
    </div>
  );
}

/* ----------------------------- BEAT ACQUISIT ----------------------------- */
function BeatAcquisit({ store, go, openMatch }) {
  const A = window.ACQ;
  const me = store.me;
  const acq = A.COMPANY_RANK.find((c) => c.id === "acquisit");
  const acqMembers = [...acq.members].sort((a, b) => b.points - a.points);
  const externals = A.USERS.filter((u) => u.company !== "acquisit");
  const beating = externals.filter((u) => u.points > acq.avgTop3);
  const bestExternal = [...externals].sort((a, b) => b.points - a.points)[0];
  const youAhead = me.points > acq.avgTop3;
  const gap = Math.abs(me.points - acq.avgTop3);

  return (
    <div className="col gap-20">
      {/* hero */}
      <div className="card hero-card" style={{ background: "var(--hero-bg)", color: "var(--hero-ink)", border: "none", overflow: "hidden", position: "relative" }}>
        <svg style={{ position: "absolute", inset: 0, opacity: .1, width: "100%", height: "100%" }} viewBox="0 0 600 240" preserveAspectRatio="none" fill="none" stroke="white" strokeWidth="1.2">
          <line x1="300" y1="0" x2="300" y2="240" /><circle cx="300" cy="120" r="70" /><rect x="0" y="60" width="70" height="120" /><rect x="530" y="60" width="70" height="120" />
        </svg>
        <div className="card-pad" style={{ padding: "26px 28px", position: "relative" }}>
          <span className="eyebrow" style={{ color: "color-mix(in srgb,var(--hero-ink) 70%,transparent)" }}>The main event</span>
          <h1 style={{ fontSize: 28, marginTop: 8, fontWeight: 800 }}>Beat the Acquisit team</h1>
          <p style={{ fontSize: 15, opacity: .85, maxWidth: 520, marginTop: 8, marginBottom: 0 }}>
            Everyone competes for the leaderboard, but the real bragging rights come from finishing above the people who built this contest.
          </p>
          <div className="row wrap gap-24" style={{ marginTop: 22 }}>
            <div className="col gap-2"><span className="mono" style={{ fontSize: 32, fontWeight: 700 }}>{acq.avgTop3}</span><span style={{ opacity: .7, fontSize: 12 }}>Acquisit score (avg top 3)</span></div>
            <div className="col gap-2"><span className="mono" style={{ fontSize: 32, fontWeight: 700, color: "var(--brand-blue)" }}>{beating.length}</span><span style={{ opacity: .7, fontSize: 12 }}>players beating Acquisit</span></div>
            <div className="col gap-2"><span className="mono" style={{ fontSize: 32, fontWeight: 700 }}>#{acq.rank}</span><span style={{ opacity: .7, fontSize: 12 }}>company rank</span></div>
          </div>
        </div>
      </div>

      {/* your status message */}
      <div className="card card-pad row between wrap gap-14" style={{ padding: "18px 20px",
        background: youAhead ? "var(--pos-bg)" : "color-mix(in srgb, var(--brand-yellow) 16%, var(--surface))",
        borderColor: youAhead ? "color-mix(in srgb, var(--pos-tx) 30%, var(--line))" : "color-mix(in srgb, var(--brand-yellow) 45%, var(--line))" }}>
        <div className="row gap-14" style={{ flex: "1 1 340px", minWidth: 0 }}>
          <Icon name={youAhead ? "trophy" : "target"} size={24} style={{ color: youAhead ? "var(--pos-tx)" : "var(--gold)", flex: "none" }} />
          <div className="col" style={{ lineHeight: 1.35 }}>
            <span style={{ fontWeight: 800, fontSize: 17 }}>{youAhead ? "You're beating the Acquisit team." : "Acquisit is still ahead. Time to predict some upsets."}</span>
            <span className="soft" style={{ fontSize: 13.5 }}>{youAhead ? `You're ${gap} points clear of their top-3 average. Don't let them catch up.` : `You're ${gap} points behind. One bold upset call could flip it.`}</span>
          </div>
        </div>
        <button className="btn btn-navy" onClick={() => go("matches")}>Find an upset<Icon name="chevron" size={15} /></button>
      </div>

      <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 20, alignItems: "start" }} className="dash-grid">
        {/* acquisit roster */}
        <div className="card card-pad col gap-8" style={{ padding: 18 }}>
          <div className="row between"><span className="eyebrow">Acquisit predictors</span><span className="badge blue">{acqMembers.length} players</span></div>
          {acqMembers.map((u, i) => (
            <div key={u.id} className="row between" style={{ padding: "8px 0", borderTop: i ? "1px solid var(--line-2)" : "none" }}>
              <div className="row gap-10"><span className="rank-num muted" style={{ width: 16 }}>{i + 1}</span><Avatar user={u} size={30} /><div className="col" style={{ lineHeight: 1.15 }}><span style={{ fontWeight: 600, fontSize: 13.5 }}>{userName(u)}</span><span className="muted" style={{ fontSize: 11 }}>#{u.rank} overall</span></div></div>
              <div className="row gap-8">{u.points > me.points ? <span className="badge neg" style={{ fontSize: 10.5 }}>ahead of you</span> : <span className="badge pos" style={{ fontSize: 10.5 }}>behind you</span>}<span className="mono" style={{ fontWeight: 700 }}>{u.points}</span></div>
            </div>
          ))}
        </div>

        {/* challengers */}
        <div className="col gap-16">
          <div className="card card-pad col gap-10" style={{ padding: 18 }}>
            <span className="eyebrow">Top challenger</span>
            <div className="row gap-12"><Avatar user={bestExternal} size={42} /><div className="col" style={{ lineHeight: 1.25 }}><span style={{ fontWeight: 700, fontSize: 15.5 }}>{userName(bestExternal)}</span><span className="muted" style={{ fontSize: 12.5 }}>{(A.COMPANIES.find((c) => c.id === bestExternal.company) || {}).name} · {bestExternal.role}</span></div><div className="grow"></div><span className="mono" style={{ fontWeight: 700, fontSize: 20 }}>{bestExternal.points}</span></div>
            <div className="muted" style={{ fontSize: 12.5 }}>Leads all non-Acquisit players by {bestExternal.points - (externals.sort((a, b) => b.points - a.points)[1] || {}).points} points.</div>
          </div>
          <div className="card card-pad col gap-10" style={{ padding: 18 }}>
            <span className="eyebrow">Players currently beating Acquisit</span>
            {beating.slice(0, 5).map((u, i) => (
              <div key={u.id} className="row between" style={{ padding: "6px 0", borderTop: i ? "1px solid var(--line-2)" : "none" }}>
                <div className="row gap-10"><Avatar user={u} size={28} /><span style={{ fontWeight: u.you ? 700 : 600, fontSize: 13.5 }}>{userName(u)}{u.you ? " · you" : ""}</span></div>
                <span className="mono" style={{ fontWeight: 600 }}>+{u.points - acq.avgTop3}</span>
              </div>
            ))}
            {beating.length === 0 && <span className="muted" style={{ fontSize: 13 }}>Nobody yet. Be the first.</span>}
          </div>
        </div>
      </div>
    </div>
  );
}

/* ----------------------------- MY COMPANY ----------------------------- */
/* The viewer's OWN company board: just their company's players, re-ranked among
   themselves, with a company-internal rank-over-time chart + table. Hidden when the
   viewer has no real company (or the "Independents" catch-all) or when the company has
   fewer than 2 players — a one-person board has nothing to rank. */
function MyCompanyLeaderboard({ store }) {
  const A = window.ACQ;
  const narrow = useNarrow(820);
  const [page, setPage] = useState(0);
  const [expanded, setExpanded] = useState(false);
  const goPage = (p) => { setPage(p); setExpanded(false); };
  const myCo = store.me && store.me.company;
  // "freelance" is the Independents catch-all, not a real team — treat it like no company.
  if (!myCo || myCo === "freelance") return null;

  const members = A.USERS.filter((u) => u.company === myCo).sort((a, b) => a.rank - b.rank);
  if (members.length < 2) return null;

  const coName = (A.COMPANIES.find((c) => c.id === myCo) || {}).name || myCo;
  const meId = store.me && store.me.id;

  // Re-rank members AMONG THEMSELVES. The table uses the 1..N order of their global ranks;
  // the chart re-derives a local rank PER matchday by sorting members by their global rank
  // at that day (USER_RANK_SERIES is gap-filled, so every entry is a real number).
  const D = A.RANK_DATES.length;
  const localSeries = {};
  members.forEach((m) => (localSeries[m.id] = []));
  for (let i = 0; i < D; i++) {
    const order = [...members].sort((a, b) => {
      const ra = (A.USER_RANK_SERIES[a.id] || [])[i];
      const rb = (A.USER_RANK_SERIES[b.id] || [])[i];
      return (ra == null ? Infinity : ra) - (rb == null ? Infinity : rb);
    });
    order.forEach((m, j) => localSeries[m.id].push(j + 1));
  }
  const ranked = members.map((u, i) => ({ ...u, _rank: i + 1 }));
  const entities = ranked.map((u) => ({ id: u.id, rank: u._rank, name: userName(u) }));
  const build = (e, { highlight }) => {
    const u = members.find((x) => x.id === e.id) || {};
    return {
      id: u.id,
      label: u.you ? "You" : userName(u),
      color: u.you ? "var(--brand-navy)" : companyColor(u.company),
      ranks: localSeries[u.id],
      highlight,
      // No avatar/initials — the line's colour + end-of-line name label tell players apart
      // (BumpChart falls back to a coloured rank dot when node is absent).
      node: null,
    };
  };

  const { visible, meAppended, page: pg, pages, pageCount } = paginateWithMe(ranked, (u) => u.you, page, PAGE_SIZE, expanded);
  const colSpan = narrow ? 5 : 8;

  return (
    <>
      <hr className="hr" style={{ margin: "4px 0" }} />
      <div className="col gap-20">
        <SubHead title={`${companyDisplayName(coName, narrow)} leaderboard`}
          sub={`Just your company: how ${companyDisplayName(coName, narrow)}'s ${members.length} players rank against each other. Movement is vs the last ranking update.`} />

        <StatLegend />
        <div className="card" style={{ overflow: "hidden" }}>
          <div style={{ overflowX: "auto" }}>
            <table className="lb" style={{ width: "100%", tableLayout: narrow ? "fixed" : undefined }}>
              <thead><tr>
                <th style={{ width: narrow ? 38 : 54 }}>Rank</th>
                <th>Player</th>
                <th style={{ textAlign: "right", width: narrow ? 44 : 72 }}>Points</th>
                {!narrow && <th style={{ textAlign: "center", width: 72 }}>Exact</th>}
                {!narrow && <th style={{ textAlign: "center", width: 72 }}>Correct</th>}
                <th style={{ textAlign: "center", width: narrow ? 70 : undefined }}>Accuracy</th>
                <th style={{ textAlign: "center", width: narrow ? 70 : undefined }}>Safety</th>
                {!narrow && <th style={{ textAlign: "right" }}>Best</th>}
              </tr></thead>
              <tbody>
                {visible.map((u, ri) => (<React.Fragment key={u.id}>
                  {meAppended && ri === visible.length - 1 && (
                    <tr className="lb-gap"><td colSpan={colSpan} style={{ textAlign: "center", color: "var(--ink-soft)", padding: "4px 0" }}>···</td></tr>
                  )}
                  <tr className={"hov " + (u.you ? "me-row" : "")}
                    style={{ cursor: store.openUser ? "pointer" : undefined }}
                    onClick={() => store.openUser && store.openUser(u)}>
                    <td><span className="rank-num">{u._rank}</span></td>
                    <td>
                      <span className="row gap-8" style={{ alignItems: "center", minWidth: 0 }}>
                        <span style={{ fontWeight: 700, fontSize: 14, minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{userName(u)}{u.you ? " · you" : ""}</span>
                        {u.isAi && <AiBadge />}
                      </span>
                    </td>
                    <td style={{ textAlign: "right" }}><span className="mono" style={{ fontWeight: 700, fontSize: 15 }}>{u.points}</span></td>
                    {!narrow && <td className="mono" style={{ textAlign: "center" }}>{u.exacts}</td>}
                    {!narrow && <td className="mono" style={{ textAlign: "center" }}>{u.correct}</td>}
                    <td style={{ textAlign: "center" }}><AccuracyBar exacts={u.exacts} good={u.good} settled={u.settled} width={narrow ? 48 : 120} showPct={!narrow} align="center" /></td>
                    <td style={{ textAlign: "center" }}><SafetyBar high={u.riskLow} med={u.riskMed} low={u.riskHigh} width={narrow ? 48 : 120} showPct={!narrow} align="center" /></td>
                    {!narrow && <td className="mono" style={{ textAlign: "right" }}>+{u.best}</td>}
                  </tr>
                </React.Fragment>))}
              </tbody>
            </table>
          </div>
        </div>
        <LeaderboardFooter expanded={expanded} onToggle={() => setExpanded((x) => !x)} pageCount={pageCount} page={pg} pages={pages} onPage={goPage} />
        <RankSeriesCard title="Your company ranking over time" sub="Daily rank within your company. Your line is highlighted. Switch views to compare more teammates."
          dates={A.RANK_DATES} entities={entities} buildEntry={build} meId={meId} kindLabel="players" />
      </div>
    </>
  );
}

function Standings({ store, go }) {
  const narrow = useNarrow(820);
  return (
    <div className="col gap-24">
      <PageHead eyebrow="Standings" title="Leaderboards"
        sub={narrow ? undefined : <>The full individual table first, then company standings, plus your own company's board.<br className="desk-br" /> You climb them all at once: every prediction lifts your rank and your company's.</>} />
      <Leaderboard store={store} embedded />
      <hr className="hr" style={{ margin: "4px 0" }} />
      <Companies store={store} go={go} embedded />
      <MyCompanyLeaderboard store={store} />
    </div>
  );
}

Object.assign(window, { Leaderboard, Companies, BeatAcquisit, Standings, MyCompanyLeaderboard, SubHead, RankChartCard, RankSeriesCard, BumpChart, AccuracyBar, SafetyBar, StatLegend });
