// op-hub.jsx — Optimum Hub mainnet-readiness dashboard (v4).
// Single polished direction. Fully responsive.

// ────────────────────────────────────────────────────────────────
// Base styles (scoped to .hub-root)
// ────────────────────────────────────────────────────────────────
if (typeof document !== 'undefined' && !document.getElementById('hub-styles')) {
  const s = document.createElement('style');
  s.id = 'hub-styles';
  s.textContent = `
  .hub-root {
    /* Optimum brand — Alabaster / Steel / Plutonium / Platinum / Carbon / Midnight */
    --alabaster: #FFFBFB;
    --steel: #D3DBE2;
    --plutonium: #B0BED1;
    --platinum: #9DB0C2;
    --carbon: #33363C;
    --midnight: #282C34;

    /* Optimum secondary palette */
    --lavender: #B87CFF;
    --peach:    #FEA158;
    --mint:     #64E27F;
    --azure:    #51B1FE;

    /* Surface tokens — light mode (default): greyish Optimum brand field */
    --bg: #F5F7F9;
    --bg-soft: #E8EDF2;
    --bg-card: #FFFFFF;
    --bg-card-2: #EEF2F5;
    --bg-elev: #FAFBFC;
    --bg-nav: rgba(248,250,252,0.84);
    --bg-scrim: rgba(211,219,226,0.58);

    --line: rgba(51,54,60,0.10);
    --line-strong: rgba(51,54,60,0.20);

    --ink: var(--midnight);
    --ink-mute: #596674;
    --ink-dim: #63707E;   /* darkened for AA contrast on light cards (was #8795A6) */
    --ink-faint: var(--plutonium);

    --chrome-hi: var(--midnight);
    --chrome: var(--carbon);
    --chrome-lo: var(--platinum);

    --warm: var(--peach);
    --warn: var(--peach);
    --accent: var(--azure);
    --good: var(--mint);

    /* Ambient gradients tinted with brand accents */
    --bg-grad-a: rgba(81,177,254,0.065);
    --bg-grad-b: rgba(157,176,194,0.32);
    --bg-grad-c: rgba(211,219,226,0.24);
    --bg-grad-base: linear-gradient(180deg, #F8FAFB 0%, #E7EDF2 100%);
    --bg-dots: rgba(51,54,60,0.055);
    --blob-opacity: 0.72;

    /* Validator panel side (intentionally dark in both modes) */
    --side-bg: linear-gradient(180deg, #1F2229 0%, #15171C 100%);
    --side-ink: var(--alabaster);

    /* Validator panel main canvas (light, brand-grey linear) */
    --vp-main-bg: linear-gradient(180deg, #F8FAFB 0%, #E9EEF3 100%);
    --scrollbar:  rgba(40,44,52,0.15);

    font-family: 'Geist', system-ui, sans-serif;
    background: var(--bg);
    color: var(--ink);
    min-height: 100vh;
    width: 100%;
    position: relative;
    overflow-x: clip;
    letter-spacing: -0.005em;
    transition: background-color .25s ease, color .25s ease;
  }

  /* Theme swap: repaint every surface in the SAME frame so the background base,
     the gradient/dot layers and the backdrop-filter panels don't each change on
     their own timing — that mismatch is what reads as the brightness "shaking".
     Added just for the swap frame, then removed so hover transitions still run. */
  html.hub-theme-instant, html.hub-theme-instant *,
  html.hub-theme-instant *::before, html.hub-theme-instant *::after {
    transition: none !important;
  }

  /* ─── DARK MODE — Optimum brand palette · Carbon / Midnight tinted ────── */
  /* Palette anchors: Midnight #282C34 and Carbon #33363C from the brand kit.
     Page bg sits between them so the dossier feels grey-warm, not pure black. */
  .hub-root[data-theme="dark"] {
    --bg:        #232730;   /* page — softer than Midnight, leans Carbon */
    --bg-soft:   #1E222A;
    --bg-card:   #2A2E37;   /* card surface — Carbon-adjacent */
    --bg-card-2: #2F333D;   /* lifted surface */
    --bg-elev:   var(--carbon);  /* highest elevation = brand Carbon */
    --bg-nav:    rgba(42,46,55,0.78);
    --bg-scrim:  rgba(35,39,48,0.62);

    --line:        rgba(255,251,251,0.06);
    --line-strong: rgba(255,251,251,0.14);

    --ink:       var(--alabaster);
    --ink-mute:  var(--plutonium);
    --ink-dim:   var(--platinum);
    --ink-faint: #6E7787;   /* lifted faint so muted Mono labels still read */

    --chrome-hi: var(--alabaster);
    --chrome:    var(--plutonium);
    --chrome-lo: var(--carbon);

    --bg-grad-a:    rgba(184,124,255,0.08);
    --bg-grad-b:    rgba(81,177,254,0.05);
    --bg-grad-c:    rgba(254,161,88,0.04);
    --bg-grad-base: linear-gradient(180deg, #232730 0%, #1B1E25 100%);
    --bg-dots:      rgba(255,251,251,0.04);
    --blob-opacity: 0.28;

    /* Sidebar stays dark in both themes; in dark mode pulled a touch deeper than
       page so it still reads as a distinct rail. */
    --side-bg: linear-gradient(180deg, #1B1E25 0%, #14161B 100%);

    /* In dark mode the validator main canvas drops into Carbon-tinted greys so
       the cards and the canvas share a family — less stark than pure black. */
    --vp-main-bg: linear-gradient(180deg, #232730 0%, #1E222A 100%);
    --scrollbar:  rgba(255,251,251,0.16);
  }
  /* Cards that ship with hardcoded light gradients — swap them for
     midnight-to-carbon equivalents in dark mode so the brand stays coherent. */
  .hub-root[data-theme="dark"] .hub-globe-card {
    background: linear-gradient(180deg, #1F2229, #181A20);
    box-shadow: 0 8px 32px rgba(0,0,0,0.45);
  }
  .hub-root[data-theme="dark"] .hub-globe-card::before,
  .hub-root[data-theme="dark"] .hub-stake-card::before,
  .hub-root[data-theme="dark"] .hub-partner-card::before {
    background: linear-gradient(180deg, rgba(255,255,255,0.06), transparent 45%);
  }
  .hub-root[data-theme="dark"] .hub-metrics-panel {
    background: transparent;
  }
  .hub-root[data-theme="dark"] .hub-stake-card {
    background: linear-gradient(180deg, #1F2229, #181A20);
  }
  .hub-root[data-theme="dark"] .hub-partner-card {
    background: linear-gradient(180deg, rgba(31,34,41,0.6), rgba(22,24,30,0.85));
  }
  /* Active nav link / hover surfaces — keep contrast in dark */
  .hub-root[data-theme="dark"] .hub-nav-links a:hover {
    background: rgba(255,255,255,0.06);
  }
  .hub-root[data-theme="dark"] .hub-nav-links > a:not(.soon),
  .hub-root[data-theme="dark"] .hub-nav-trigger {
    color: var(--ink-mute);
  }
  .hub-root[data-theme="dark"] .hub-nav-links a.active {
    background: rgba(255,255,255,0.10);
  }
  
  .hub-root[data-theme="dark"] .hub-vp-pctile-head .seg {
    background: rgba(47,51,61,0.78);
  }
  .hub-root[data-theme="dark"] .hub-vp-health-card .bar {
    background: rgba(255,251,251,0.07);
  }
  
  .hub-root[data-theme="dark"] .hub-nav-links a:hover,
  .hub-root[data-theme="dark"] .hub-nav-trigger:hover,
  .hub-root[data-theme="dark"] .hub-nav-item:focus-within .hub-nav-trigger {
    color: var(--ink);
  }
  .hub-root, .hub-root *, .hub-root *::before, .hub-root *::after { box-sizing: border-box; }
  .hub-mono { font-family: 'Geist Mono', ui-monospace, monospace; }

  /* ─── ambient bg ─── */
  .hub-bg {
    position: fixed; inset: 0; z-index: -1; pointer-events: none;
    background:
      radial-gradient(1100px 800px at 80% -5%, var(--bg-grad-a), transparent 60%),
      radial-gradient(900px 700px at -10% 110%, var(--bg-grad-b), transparent 65%),
      radial-gradient(700px 500px at 30% 30%, var(--bg-grad-c), transparent 70%),
      var(--bg-grad-base);
  }
  .hub-bg-dots {
    position: fixed; inset: 0; z-index: -1; pointer-events: none;
    background-image: radial-gradient(var(--bg-dots) 1px, transparent 1px);
    background-size: 28px 28px;
    mask-image: radial-gradient(ellipse 70% 60% at 50% 25%, #000 30%, transparent 100%);
  }
  .hub-bg-blob {
    position: fixed; pointer-events: none; z-index: -1;
    opacity: var(--blob-opacity);
    transition: opacity .25s ease;
  }
  .hub-grain {
    position: fixed; inset: 0; z-index: -1; pointer-events: none;
    opacity: 0.02;
    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='200' height='200'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>");
  }
  .hub-shell {
    max-width: 1320px;
    margin: 0 auto;
    padding: 22px 32px 64px;
  }

  /* ─── nav ─── */
  /* z-index is intentionally above the validator overlay (.hub-vp 300 /
     scrim 290) so the navigation chrome stays usable while a validator is
     open. */
  .hub-nav {
    position: sticky; top: 14px; z-index: 320;
    display: flex; align-items: center; gap: 20px;
    padding: 12px 14px 12px 18px;
    border: 1px solid var(--line);
    border-radius: 999px;
    background: var(--bg-nav);
    backdrop-filter: blur(16px) saturate(140%);
    -webkit-backdrop-filter: blur(16px) saturate(140%);
    box-shadow: 0 4px 24px rgba(40,44,52,0.05), 0 1px 0 rgba(255,255,255,0.7) inset;
  }
  /* Opaque backdrop behind the floating nav. Without it, content scrolling up
     shows in the 14px gap above the pill and blurs through the 78% glass, so
     the cards look like they sit "behind" the menu bar.

     Two boxes, because one cannot be both shapes. ::before matches the pill
     exactly and inherits its radius — at inset 0 the 999px clamps to half the
     PILL's height, so the curve tracks the silhouette at every breakpoint. A
     single tall box clamped to half its own height instead, sweeping a 75px arc
     across the pill. ::after covers the gap above, where square is fine: it sits
     over page background, so var(--bg) on var(--bg) is invisible. */
  .hub-nav::before,
  .hub-nav::after {
    content: '';
    position: absolute;
    background: var(--bg);
    z-index: -1;
    pointer-events: none;
  }
  .hub-nav::before {
    inset: 0;
    border-radius: inherit;
  }
  /* Stops at the pill's top edge — overlapping into it filled the notches outside
     the rounded top corners with flat --bg, so the pill read as open-topped with
     its sides running off the screen. Overshoots upward by a little because a
     flush -14px against the 14px sticky offset left no margin and leaked a sliver;
     the surplus sits above the page, where nothing paints. */
  .hub-nav::after {
    left: 0; right: 0; top: -24px; bottom: 100%;
  }
  /* Two dark-only changes. The inset highlight goes: at 0.7 white it is a glass rim
     on a light pill and a bright line on a dark one, and stacked under the border it
     read as a 2px white band. And the border steps up from --line to --line-strong,
     because along the top the backdrop puts --bg on both sides of it, leaving nothing
     for 6% to read against — that edge vanished while the sides and bottom, which sit
     over page content, stayed visible. Both use the palette's own tokens. */
  .hub-root[data-theme="dark"] .hub-nav {
    border-color: var(--line-strong);
    box-shadow: 0 6px 20px rgba(0,0,0,0.34);
  }
  .hub-brand {
    display: flex; align-items: center; gap: 12px;
    font-weight: 500; font-size: 15px; letter-spacing: -0.01em;
    color: var(--ink); text-decoration: none;
  }
  .hub-brand-mark {
    width: 36px; height: 22px;
    -webkit-mask: url("dashboard/assets/optimum-logomark-white-sm.png") center/contain no-repeat;
            mask: url("dashboard/assets/optimum-logomark-white-sm.png") center/contain no-repeat;
    background: var(--ink);
    transition: background-color .25s ease;
  }
  .hub-brand-div { width: 1px; height: 18px; background: var(--line-strong); margin: 0 -4px; }
  .hub-brand-sub { font-family: 'Geist Mono', monospace; font-size: 11.5px; color: var(--ink-mute); }
  .hub-nav-links { display: flex; gap: 2px; margin-left: 6px; flex-wrap: nowrap; }
  .hub-nav-links a,
  .hub-nav-trigger {
    color: var(--ink-mute); font-size: 13px; text-decoration: none;
    padding: 8px 12px; border-radius: 10px;
    transition: color .15s, background .15s;
    display: inline-flex; align-items: center; gap: 6px;
    white-space: nowrap;
    border: 0;
    font-family: inherit;
    background: transparent;
  }
  .hub-nav-links a.active { color: var(--ink); background: rgba(40,44,52,0.07); }
  .hub-nav-links > a:not(.soon),
  .hub-nav-trigger {
    color: var(--ink-mute);
  }
  .hub-nav-links a:hover,
  .hub-nav-trigger:hover,
  .hub-nav-item:focus-within .hub-nav-trigger {
    color: var(--ink); background: rgba(40,44,52,0.04);
  }
  .hub-nav-item { position: relative; display: inline-flex; }
  .hub-nav-trigger { cursor: pointer; }
  .hub-nav-trigger svg { width: 12px; height: 12px; color: var(--ink-dim); }
  .hub-nav-menu {
    position: absolute; left: 0; top: 100%;
    min-width: 190px; z-index: 40;
    padding: 7px;
    border: 1px solid var(--line-strong);
    border-radius: 14px;
    background: var(--bg-nav);
    backdrop-filter: blur(16px) saturate(140%);
    -webkit-backdrop-filter: blur(16px) saturate(140%);
    box-shadow: 0 18px 42px rgba(40,44,52,0.12);
    opacity: 0; transform: translateY(-4px) scale(.98);
    pointer-events: none; visibility: hidden;
    transition: opacity .16s ease, transform .18s ease, visibility 0s linear .18s;
  }
  /* Top level keeps :focus-within: these triggers have no click handler, so it is
     what lets a click or a touch-tap open the dropdown at all. A single dropdown
     staying open after a click is ordinary behaviour — the overlap bug was
     between the category flyouts one level down. */
  .hub-nav-item:hover .hub-nav-menu,
  .hub-nav-item:focus-within .hub-nav-menu {
    opacity: 1; transform: translateY(0) scale(1);
    pointer-events: auto; visibility: visible;
    transition: opacity .16s ease, transform .18s ease, visibility 0s;
  }
  .hub-nav-menu a {
    width: 100%;
    justify-content: flex-start;
    padding: 10px 11px;
    font-size: 12.5px;
  }
  .hub-nav-menu-explore { min-width: 190px; }
  /* Explore → category rows, each opening a flyout submenu to the right. */
  .hub-nav-subitem { position: relative; }
  .hub-nav-subtrigger {
    display: flex; align-items: center; justify-content: space-between; gap: 12px;
    width: 100%; padding: 10px 11px; border-radius: 8px;
    font-family: inherit; font-size: 12.5px; font-weight: 500; text-align: left;
    color: var(--ink-dim); background: none; border: 0; cursor: pointer;
    transition: background .14s ease, color .14s ease;
  }
  .hub-nav-subtrigger svg { width: 11px; height: 11px; color: var(--ink-dim); flex: none; }
  /* :has() selectors are kept in their own blocks throughout this menu. :has() is
     not forgiving, so grouping it with the :hover selector would make a browser
     that lacks :has() drop the whole list — taking hover down with it. Split, the
     hover half always survives and :has() is purely additive. */
  .hub-nav-subitem:hover .hub-nav-subtrigger { color: var(--ink); background: rgba(40,44,52,0.06); }
  .hub-nav-subitem:has(:focus-visible) .hub-nav-subtrigger { color: var(--ink); background: rgba(40,44,52,0.06); }
  .hub-root[data-theme="dark"] .hub-nav-subitem:hover .hub-nav-subtrigger { background: rgba(255,255,255,0.06); }
  .hub-root[data-theme="dark"] .hub-nav-subitem:has(:focus-visible) .hub-nav-subtrigger { background: rgba(255,255,255,0.06); }
  .hub-nav-submenu {
    position: absolute; left: 100%; top: -8px;
    min-width: 180px; padding: 7px; margin-left: 14px;
    border: 1px solid var(--line-strong); border-radius: 14px;
    background: var(--bg-nav);
    backdrop-filter: blur(16px) saturate(140%);
    -webkit-backdrop-filter: blur(16px) saturate(140%);
    box-shadow: 0 18px 42px rgba(40,44,52,0.12);
    opacity: 0; transform: translateX(-4px);
    pointer-events: none; visibility: hidden;
    transition: opacity .16s ease, transform .18s ease, visibility 0s linear .18s;
  }
  /* A transparent bridge over the small gap so moving into the flyout keeps hover. */
  .hub-nav-submenu::before { content: ""; position: absolute; top: 0; bottom: 0; left: -14px; width: 14px; }
  /* Open on hover, or on keyboard focus. Deliberately :focus-visible and not
     :focus-within — a mouse click on a category button focuses it, and
     :focus-within would then pin that flyout open while the pointer moved on to
     another category, leaving two flyouts overlapping. */
  .hub-nav-subitem:hover .hub-nav-submenu {
    opacity: 1; transform: translateX(0);
    pointer-events: auto; visibility: visible;
    transition: opacity .16s ease, transform .18s ease, visibility 0s;
  }
  .hub-nav-subitem:has(:focus-visible) .hub-nav-submenu {
    opacity: 1; transform: translateX(0);
    pointer-events: auto; visibility: visible;
    transition: opacity .16s ease, transform .18s ease, visibility 0s;
  }
  /* One flyout at a time: while the pointer is inside the Explore menu, collapse
     every category it is not on. A category holding keyboard focus is exempt —
     hiding it would pull its links out of the tab order and strand a keyboard
     user (WCAG 2.1.1). A mouse click cannot trigger that exemption, because
     clicking sets :focus, not :focus-visible. */
  .hub-nav-menu-explore:hover .hub-nav-subitem:not(:hover):not(:has(:focus-visible)) .hub-nav-submenu {
    opacity: 0; transform: translateX(-4px);
    pointer-events: none; visibility: hidden;
    transition: opacity .16s ease, transform .18s ease, visibility 0s linear .18s;
  }
  .hub-nav-menu-explore:hover .hub-nav-subitem:not(:hover):not(:has(:focus-visible)) .hub-nav-subtrigger,
  .hub-root[data-theme="dark"] .hub-nav-menu-explore:hover .hub-nav-subitem:not(:hover):not(:has(:focus-visible)) .hub-nav-subtrigger {
    color: var(--ink-dim); background: none;
  }
  .hub-nav-links a.soon { color: var(--ink-dim); cursor: not-allowed; }
  .hub-nav-links a.soon:hover { background: transparent; color: var(--ink-dim); }
  .hub-soon-tag {
    font-family: 'Geist Mono', monospace;
    font-size: 8.5px; color: var(--ink-dim);
    text-transform: uppercase; letter-spacing: 0.16em;
    padding: 2px 5px;
    background: rgba(40,44,52,0.04);
    border-radius: 4px;
    border: 1px solid var(--line);
  }
  .hub-nav-spacer { flex: 1; }
  .hub-pill {
    display: inline-flex; align-items: center; gap: 8px;
    padding: 7px 14px 7px 12px;
    border-radius: 999px;
    border: 1px solid var(--line-strong);
    background: var(--bg-nav);
    font-family: 'Geist Mono', monospace;
    font-size: 11.5px; color: var(--ink);
    white-space: nowrap;
  }
  .hub-dot {
    width: 6px; height: 6px; border-radius: 50%;
    background: var(--accent);
    box-shadow: 0 0 0 3px rgba(81,177,254,0.18), 0 0 8px rgba(81,177,254,0.55);
    animation: hubPulse 2.6s ease-in-out infinite;
  }
  @keyframes hubPulse {
    0%,100% { box-shadow: 0 0 0 3px rgba(81,177,254,0.18), 0 0 8px rgba(81,177,254,0.55); }
    50%     { box-shadow: 0 0 0 6px rgba(81,177,254,0.04), 0 0 16px rgba(81,177,254,0.75); }
  }
  .hub-burger {
    display: none;
    width: 38px; height: 38px;
    align-items: center; justify-content: center;
    border-radius: 10px; cursor: pointer;
    border: 1px solid var(--line);
    background: var(--bg-nav);
    color: var(--ink);
  }
  .hub-mobile-menu {
    display: none;
  }
  .hub-mobile-menu a {
    color: var(--ink-mute);
    text-decoration: none;
  }
  .hub-theme-toggle {
    width: 34px; height: 34px;
    display: grid; place-items: center;
    border-radius: 999px;
    border: 1px solid var(--line-strong);
    background: var(--bg-nav);
    color: var(--ink);
    cursor: pointer;
    transition: background .18s, border-color .18s, color .18s, transform .25s ease;
  }
  .hub-theme-toggle:hover {
    background: var(--bg-card);
    border-color: var(--ink);
    transform: rotate(15deg);
  }
  .hub-theme-toggle:active { transform: rotate(0deg) scale(0.96); }
  .hub-theme-toggle svg { display: block; }

  /* nav chain selector (top-right) */
  .hub-chain-sel { position: relative; }
  .hub-chain-trigger {
    display: inline-flex; align-items: center; gap: 7px; cursor: pointer;
    padding: 7px 11px; border-radius: 999px;
    font-family: 'Geist', sans-serif; font-size: 12.5px; font-weight: 500; color: var(--ink);
    background: var(--bg-card); border: 1px solid var(--line-strong);
    transition: border-color .15s ease, background .15s ease;
  }
  .hub-chain-trigger:hover { border-color: var(--ink); }
  .hub-chain-trigger svg { color: var(--ink-dim); }
  .hub-chain-dot { width: 8px; height: 8px; border-radius: 50%; flex: none; background: #51B1FE; box-shadow: 0 0 0 3px rgba(81,177,254,0.18); }
  .hub-chain-drop {
    position: absolute; top: calc(100% + 8px); right: 0; z-index: 60; min-width: 210px;
    background: var(--bg-card); border: 1px solid var(--line); border-radius: 12px;
    box-shadow: 0 16px 44px rgba(40,44,52,0.20); padding: 6px; animation: hub-help-fade .14s ease-out;
  }
  .hub-chain-drop-label { font-family: 'Geist Mono', monospace; font-size: 9px; letter-spacing: .16em; text-transform: uppercase; color: var(--ink-dim); padding: 6px 10px 5px; }
  .hub-chain-opt {
    display: flex; align-items: center; gap: 9px; width: 100%;
    padding: 9px 10px; border-radius: 9px; border: none; background: transparent;
    font-family: 'Geist', sans-serif; font-size: 13px; color: var(--ink); text-align: left; cursor: pointer;
  }
  .hub-chain-opt.on { background: rgba(81,177,254,0.10); font-weight: 600; }
  .hub-chain-opt.on:hover { background: rgba(81,177,254,0.15); }
  .hub-chain-tick { margin-left: auto; color: var(--op-azure, #51B1FE); }
  .hub-chain-opt.soon { cursor: not-allowed; justify-content: space-between; color: var(--ink-dim); }
  .hub-chain-soon { font-family: 'Geist Mono', monospace; font-size: 8.5px; letter-spacing: .07em; text-transform: uppercase; color: var(--ink-dim); border: 1px solid var(--line-strong); border-radius: 999px; padding: 2px 7px; }
  @media (max-width: 520px) { .hub-chain-name { display: none; } .hub-chain-trigger { padding: 7px 9px; } }

  
  .hub-pill.as-btn {
    cursor: pointer;
    text-decoration: none;
    border-color: var(--line-strong);
    transition: background .18s, border-color .18s, transform .18s;
    padding-right: 10px;
  }
  .hub-pill.as-btn:hover {
    background: var(--bg-nav);
    border-color: rgba(40,44,52,0.28);
  }
  .hub-pill.as-btn.open {
    background: var(--bg-card);
    border-color: rgba(40,44,52,0.28);
    box-shadow: 0 8px 24px rgba(40,44,52,0.08);
  }
  .hub-pill .chev {
    width: 14px; height: 14px;
    display: inline-block;
    transition: transform .35s cubic-bezier(.5,1.6,.4,1);
    color: var(--ink-mute);
  }
  .hub-pill.as-btn.open .chev { transform: rotate(180deg); color: var(--ink); }

  
  .hub-netmenu.open {
    opacity: 1; transform: translateY(0) scale(1);
    pointer-events: auto;
    visibility: visible;
    backdrop-filter: blur(22px) saturate(160%);
    transition: opacity .22s ease, transform .28s cubic-bezier(.4,1.4,.4,1), visibility 0s;
  }
  
  
  .hub-netmenu-head .lbl {
    font-family: 'Geist Mono', monospace;
    font-size: 10px; color: var(--ink-dim);
    text-transform: uppercase; letter-spacing: 0.18em;
  }
  .hub-netmenu-head .val {
    font-family: 'Geist Mono', monospace;
    font-size: 11.5px; color: var(--ink);
    display: inline-flex; align-items: center; gap: 7px;
  }

  
  
  
  
  @keyframes hubMenuIn {
    to { opacity: 1; transform: translateY(0); }
  }
  
  .hub-netopt.disabled { cursor: not-allowed; opacity: 0.55; }
  .hub-netopt.disabled:hover { background: transparent; }
  .hub-netopt .ring {
    width: 28px; height: 28px; flex: none;
    border-radius: 50%;
    border: 1px solid var(--line-strong);
    background: rgba(40,44,52,0.04);
    display: grid; place-items: center;
    position: relative;
    color: var(--ink-mute);
  }
  .hub-netopt.active .ring {
    background: linear-gradient(135deg, var(--midnight), var(--carbon));
    border-color: transparent;
    color: var(--alabaster);
    box-shadow: 0 0 0 3px rgba(40,44,52,0.08), 0 6px 16px rgba(40,44,52,0.25);
  }
  .hub-netopt.warn .ring {
    background: rgba(254,161,88,0.15);
    border-color: rgba(254,161,88,0.45);
    color: var(--warn);
  }
  .hub-netopt .text { flex: 1; line-height: 1.25; }
  .hub-netopt .text .t {
    font-size: 13.5px; color: var(--ink); font-weight: 500;
    letter-spacing: -0.005em;
  }
  .hub-netopt .text .s {
    font-family: 'Geist Mono', monospace;
    font-size: 10.5px; color: var(--ink-dim);
    margin-top: 2px; letter-spacing: 0.04em;
  }
  .hub-netopt .tag {
    font-family: 'Geist Mono', monospace;
    font-size: 9.5px; color: var(--ink-dim);
    text-transform: uppercase; letter-spacing: 0.16em;
    background: rgba(40,44,52,0.04);
    border: 1px solid var(--line);
    padding: 3px 7px; border-radius: 5px;
  }
  .hub-netopt.active .tag { color: var(--ink); border-color: var(--line-strong); background: rgba(40,44,52,0.08); }

  
  .hub-netmenu-foot .k { color: var(--ink-dim); }
  .hub-netmenu-foot .v { color: var(--ink); text-align: right; }

  /* ─── Validator detail dossier (full-page overlay) ─── */
  /* Validator observability owns the viewport so slot comparison charts get
     the same canvas as a dedicated analytics workspace. */
  :root, .hub-root {
    --hub-nav-clearance: 0px;
    --hub-vp-bottom-gap: 0px;
  }
  
  .hub-vp-scrim.open {
    background: var(--bg-scrim);
    backdrop-filter: blur(14px) saturate(120%);
    -webkit-backdrop-filter: blur(14px) saturate(120%);
    pointer-events: auto;
  }
  
  .hub-vp.open { opacity: 1; pointer-events: auto; }
	  
  

  
  
  
  

  
	  
	  
	  
	  
  .hub-vp-crumb .sep { color: rgba(255,255,255,0.22); }
  
  
  
  
  
  
  
  
  
  
  
  .hub-vp-status.onboarding {
    background: rgba(254,161,88,0.10);
    border-color: rgba(254,161,88,0.40);
    color: #FFC18A;
  }
  .hub-vp-status.onboarding::before { background: var(--warn); box-shadow: 0 0 8px rgba(254,161,88,0.6); }
  
  .hub-vp-quickstats .cell {
    background: rgba(20,22,28,0.55);
    padding: 12px 14px;
    display: flex; flex-direction: column; gap: 4px;
  }
  .hub-vp-quickstats .cell .k {
    font-family: 'Geist Mono', monospace; font-size: 9.5px;
    color: rgba(255,255,255,0.46);
    text-transform: uppercase; letter-spacing: 0.16em;
  }
  .hub-vp-quickstats .cell .v {
    font-size: 17px; color: var(--alabaster); font-weight: 500;
    letter-spacing: -0.01em; line-height: 1.1;
  }
  .hub-vp-quickstats .cell .v.unavailable { color: rgba(255,255,255,0.4); font-size: 13px; }

	  
	  
	  
	  .hub-vp-tab .idx { color: rgba(255,255,255,0.36); font-weight: 500; }
	  
	  .hub-vp-tab.active {
	    color: var(--alabaster);
	    border-color: rgba(255,255,255,0.42);
	    background: rgba(255,255,255,0.10);
	    box-shadow: 0 10px 24px rgba(0,0,0,0.18);
	  }
  
	  .hub-vp-tab.active .idx,
	  .hub-vp-tab.active b { color: inherit; font-weight: 500; }

  
  

	  
  
  
	  .hub-vp-btn.primary {
	    background: var(--alabaster);
	    color: var(--midnight);
	    border-color: transparent;
	  }
	  .hub-vp-btn.primary:hover { transform: translateY(-1px); background: var(--bg-card); box-shadow: 0 8px 20px rgba(0,0,0,0.35); }

	  
  
  

  
  .hub-vp-topbar .right { display: flex; align-items: center; gap: 18px; flex-wrap: wrap; }
  
  .hub-vp-topbar .sep { color: var(--ink-faint); margin: 0 6px; }

	  
  
  

  
  .hub-vp-sec-head .idx { color: var(--ink); }
  .hub-vp-sec-head .right { color: var(--ink-mute); }
  .hub-vp-sec-head .right b { color: var(--ink); font-weight: 500; }

  
  
  
  .hub-vp-tm-row .lbl .k {
    font-family: 'Geist Mono', monospace; font-size: 10.5px;
    color: var(--ink-dim);
    text-transform: uppercase; letter-spacing: 0.16em;
  }
  .hub-vp-tm-row .lbl .sub {
    font-family: 'Geist Mono', monospace; font-size: 10.5px;
    color: var(--ink-mute); margin-top: 4px;
  }
  .hub-vp-tm-row .track {
    height: 4px; border-radius: 2px;
    background: rgba(40,44,52,0.06); overflow: hidden;
  }
  .hub-vp-tm-row .track > i {
    display: block; height: 100%; border-radius: 2px;
    background: linear-gradient(90deg, var(--platinum) 0%, var(--carbon) 65%, var(--midnight) 100%);
    transform-origin: left;
    animation: hubBarIn .9s cubic-bezier(.4,1.3,.4,1) both;
  }
  .hub-vp-tm-row .val {
    font-family: 'Geist Mono', monospace;
    font-size: 28px; color: var(--ink);
    text-align: left; font-weight: 500;
    letter-spacing: -0.02em;
  }
  .hub-vp-tm-row .val .u { font-size: 11px; color: var(--ink-dim); margin-left: 2px; }
  .hub-vp-tm-row .spark {
    height: 44px;
    margin-top: 4px;
    position: relative;
  }
  .hub-vp-tm-row .spark svg { width: 100%; height: 100%; display: block; }
  .hub-vp-tm-row .spark .area { fill: rgba(81,177,254,0.10); }
  .hub-vp-tm-row .spark .line { fill: none; stroke: #51B1FE; stroke-width: 1.2; }
  .hub-vp-tm-row .spark .dot  { fill: #51B1FE; }
  .hub-vp-tm-row .meta {
    display: flex; align-items: baseline; justify-content: space-between;
    gap: 12px;
    font-family: 'Geist Mono', monospace; font-size: 9.5px;
    color: var(--ink-mute);
    text-transform: uppercase; letter-spacing: 0.14em;
  }
  .hub-vp-tm-row .meta .delta.up   { color: #2074C9; }
  .hub-vp-tm-row .meta .delta.down { color: #C76A2A; }
  @keyframes hubBarIn { from { transform: scaleX(0); } to { transform: scaleX(1); } }

  
  
  
  .hub-vp-vs-row .lbl .k {
    font-family: 'Geist Mono', monospace; font-size: 10.5px;
    color: var(--ink-dim);
    text-transform: uppercase; letter-spacing: 0.16em;
  }
  .hub-vp-vs-row .lbl .sub {
    font-family: 'Geist Mono', monospace; font-size: 10.5px;
    color: var(--ink-mute); margin-top: 4px;
  }
  
  
  .hub-vp-vs-tb .who {
    font-family: 'Geist Mono', monospace; font-size: 10px;
    color: var(--ink-dim);
    text-transform: uppercase; letter-spacing: 0.14em;
  }
  .hub-vp-vs-tb .who b { color: var(--ink); font-weight: 500; }
  .hub-vp-vs-tb .num {
    font-family: 'Geist Mono', monospace; font-size: 12px;
    color: var(--ink); font-weight: 500;
    text-align: right;
  }
  
  
  .hub-vp-vs-track.solid > i {
    background: linear-gradient(90deg, var(--carbon) 0%, var(--midnight) 100%);
  }
  .hub-vp-vs-track.dashed > i {
    background-color: var(--ink-faint);
    background-image: repeating-linear-gradient(
      45deg,
      rgba(255,255,255,0.55) 0 4px,
      transparent 4px 8px
    );
    opacity: 0.85;
  }
  
  
  .hub-vp-vs-delta .chip {
    display: inline-flex; align-items: center; gap: 4px;
    padding: 5px 9px; border-radius: 999px;
    font-size: 12px; font-weight: 500; letter-spacing: 0.02em;
    border: 1px solid transparent;
  }
  .hub-vp-vs-delta .chip.good {
    color: #1F7A3A;
    background: rgba(100,226,127,0.16);
    border-color: rgba(100,226,127,0.40);
  }
  .hub-vp-vs-delta .chip.flat {
    color: var(--ink-mute);
    background: rgba(40,44,52,0.05);
    border-color: var(--line);
  }
  
  .hub-vp-vs-footer .lead {
    color: var(--ink-dim);
    text-transform: uppercase; letter-spacing: 0.14em;
    font-size: 10px;
  }
  
  .hub-vp-vs-footer .sep { color: var(--ink-faint); }

  
  .hub-vp-rep-meta .pair { display: flex; align-items: center; gap: 6px; }
  .hub-vp-rep-meta .pair .k {
    color: var(--ink-dim);
    text-transform: uppercase; letter-spacing: 0.14em;
    font-size: 9.5px;
  }
  .hub-vp-rep-meta .pair .v { color: var(--ink); font-weight: 500; }
  .hub-vp-rep-meta .sep { color: var(--ink-faint); }

  
  
  
  
  
  

  /* ─── Percentile comparison (mump2p vs libp2p {
    display: flex;
    flex-direction: column;
    gap: 12px;
    padding-top: 2px;
  }
  
  
  .hub-vp-pctile-head .ttl { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
  .hub-vp-pctile-head .ttl .eyebrow {
    font-family: 'Geist Mono', monospace;
    font-size: 10px; font-weight: 500; color: var(--ink-dim);
    letter-spacing: 0.08em; line-height: 1.1;
  }
  .hub-vp-pctile-head .ttl .sub {
    font-family: 'Geist Mono', monospace; font-size: 10.5px;
    color: var(--ink-mute);
    letter-spacing: 0.04em;
  }
  .hub-vp-pctile-head .seg {
    display: inline-flex;
    padding: 3px;
    border: 1px solid rgba(40,44,52,0.08);
    border-radius: 999px;
    background: rgba(238,242,245,0.82);
  }
  .hub-vp-pctile-head .seg button {
    appearance: none; -webkit-appearance: none;
    border: 0; background: transparent;
    padding: 6px 16px;
    border-radius: 999px;
    font-family: 'Geist Mono', monospace; font-size: 11px;
    color: var(--ink-dim); letter-spacing: 0.14em;
    text-transform: uppercase;
    cursor: pointer;
    transition: color .15s, background .15s, box-shadow .15s;
  }
  .hub-vp-pctile-head .seg button:hover { color: var(--ink); }
  .hub-vp-pctile-head .seg button.active {
    background: var(--ink); color: var(--bg);
    box-shadow: none;
  }
  
  
  
  
  
  .hub-vp-pctile-row .role { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
  .hub-vp-pctile-row .role .name {
    font-size: 15px; font-weight: 500; color: var(--ink);
    letter-spacing: -0.005em;
  }
  .hub-vp-pctile-row .role .sub {
    font-family: 'Geist Mono', monospace; font-size: 10px;
    color: var(--ink-mute); letter-spacing: 0.06em;
  }
  .hub-vp-pctile-row .num {
    font-family: 'Geist Mono', monospace;
    font-size: 22px; font-weight: 500;
    color: var(--ink); letter-spacing: -0.01em;
    line-height: 1.05;
  }
  .hub-vp-pctile-row .num.libp2p { color: var(--ink-dim); }
  .hub-vp-pctile-row .num .u {
    margin-left: 3px; font-size: 11px;
    color: var(--ink-mute); font-weight: 500;
  }
  .hub-vp-pctile-row .adv {
    display: flex; align-items: center; gap: 10px;
    min-width: 0;
  }
  .hub-vp-pctile-row .adv .chip {
    display: inline-flex; align-items: baseline; gap: 1px;
    padding: 5px 11px;
    border-radius: 999px;
    background: rgba(31,122,58,0.10);
    border: 1px solid rgba(31,122,58,0.28);
    color: #1F7A3A;
    font-family: 'Geist Mono', monospace;
    font-size: 13px; font-weight: 500;
    letter-spacing: -0.005em;
    white-space: nowrap;
  }
  .hub-vp-pctile-row .adv .chip .u {
    margin-left: 2px; font-size: 10.5px; opacity: 0.85;
  }
  .hub-vp-pctile-row .adv .note {
    font-family: 'Geist Mono', monospace; font-size: 10px;
    color: var(--ink-mute);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  @media (max-width: 760px) {
    
    
    .hub-vp-pctile-row .role { grid-area: role; }
    .hub-vp-pctile-row .num.mump2p { grid-area: mump2p; }
    .hub-vp-pctile-row .num.libp2p { grid-area: libp2p; }
    .hub-vp-pctile-row .adv { grid-area: adv; }
  }

  
  
  .hub-vp-rep-card .role {
    display: flex; align-items: baseline; gap: 10px;
    font-family: 'Geist Mono', monospace;
    text-transform: uppercase; letter-spacing: 0.16em;
  }
  .hub-vp-rep-card .role .k { font-size: 10.5px; color: var(--ink-dim); }
  .hub-vp-rep-card .role .v { font-size: 10.5px; color: var(--ink); font-weight: 500; }
  .hub-vp-rep-card .lede {
    font-size: 12.5px; color: var(--ink-mute); line-height: 1.55;
  }
  .hub-vp-rep-card .lede b { color: var(--ink); font-weight: 500; }
  
  
  .hub-vp-rep-pctile .p {
    font-family: 'Geist Mono', monospace; font-size: 10.5px;
    color: var(--ink-dim);
    text-transform: uppercase; letter-spacing: 0.16em;
  }
  .hub-vp-rep-pctile .ms {
    font-family: 'Geist Mono', monospace;
    font-size: 22px; font-weight: 500; color: var(--ink); margin-top: 8px;
    letter-spacing: -0.005em;
  }
  .hub-vp-rep-pctile .ms .u {
    font-size: 11px; color: var(--ink-mute); margin-left: 2px;
  }
  
  
  .hub-vp-rep-gw-row .gw {
    font-family: 'Geist Mono', monospace; font-size: 11px;
    color: var(--ink); font-weight: 500;
    display: flex; align-items: center; gap: 8px;
  }
  .hub-vp-rep-gw-row .gw::before {
    content: ""; width: 6px; height: 6px; border-radius: 50%;
    background: var(--good); box-shadow: 0 0 6px rgba(100,226,127,0.55);
  }
  .hub-vp-rep-gw-row .body {
    display: flex; flex-direction: column; gap: 4px;
    min-width: 0;
  }
  .hub-vp-rep-gw-row .body .ms {
    font-family: 'Geist Mono', monospace; font-size: 14px;
    color: var(--ink); font-weight: 500;
  }
  .hub-vp-rep-gw-row .body .meta {
    font-family: 'Geist Mono', monospace; font-size: 10.5px;
    color: var(--ink-mute);
  }
  .hub-vp-rep-gw-row .track {
    height: 4px; border-radius: 2px;
    background: rgba(40,44,52,0.06); overflow: hidden;
  }
  .hub-vp-rep-gw-row .track > i {
    display: block; height: 100%;
    background: linear-gradient(90deg, var(--carbon), var(--midnight));
    transform-origin: left;
    animation: hubBarIn .9s cubic-bezier(.4,1.3,.4,1) both;
  }

  
  .hub-vp-rep-chart .head {
    display: flex; justify-content: space-between; align-items: baseline;
    margin-bottom: 12px;
  }
  .hub-vp-rep-chart .head .ttl {
    font-family: 'Geist Mono', monospace; font-size: 10.5px;
    color: var(--ink-dim);
    text-transform: uppercase; letter-spacing: 0.16em;
  }
  .hub-vp-rep-chart .head .ttl b { color: var(--ink); font-weight: 500; }
  .hub-vp-rep-chart .head .sub {
    font-family: 'Geist Mono', monospace; font-size: 10.5px;
    color: var(--ink-mute);
  }
  .hub-vp-rep-chart .area { position: relative; height: 140px; }
  
  .hub-vp-rep-chart .ref-label {
    position: absolute; right: 4px;
    transform: translateY(-50%);
    font-family: 'Geist Mono', monospace; font-size: 10px;
    color: var(--ink-mute);
    background: var(--bg-nav);
    padding: 2px 6px; border-radius: 4px;
    border: 1px solid var(--line);
  }
  .hub-vp-rep-chart .yaxis {
    position: absolute; left: 0; top: 0; bottom: 0;
    display: flex; flex-direction: column; justify-content: space-between;
    font-family: 'Geist Mono', monospace; font-size: 9.5px;
    color: var(--ink-faint);
    pointer-events: none;
  }
  .hub-vp-rep-chart .xaxis {
    display: flex; justify-content: space-between;
    margin-top: 6px;
    font-family: 'Geist Mono', monospace; font-size: 9.5px;
    color: var(--ink-faint);
  }

  
  
  .hub-vp-rep-cov .head {
    font-family: 'Geist Mono', monospace; font-size: 10.5px;
    color: var(--ink-dim);
    text-transform: uppercase; letter-spacing: 0.16em;
    margin-bottom: 10px;
  }
  .hub-vp-rep-cov .head b { color: var(--ink); font-weight: 500; }
  
  
  .hub-vp-rep-cov-cell .gw {
    font-family: 'Geist Mono', monospace; font-size: 11px;
    color: var(--ink); font-weight: 500;
  }
  .hub-vp-rep-cov-cell .pct {
    font-family: 'Geist Mono', monospace; font-size: 22px;
    color: var(--ink); font-weight: 500; letter-spacing: -0.005em;
  }
  
  .hub-vp-rep-vals .head {
    font-family: 'Geist Mono', monospace; font-size: 10.5px;
    color: var(--ink-dim);
    text-transform: uppercase; letter-spacing: 0.16em;
    margin-bottom: 8px;
  }
  .hub-vp-rep-vals .big {
    font-size: 32px; font-weight: 500; color: var(--ink);
    letter-spacing: -0.02em;
  }
  .hub-vp-rep-vals .desc {
    font-family: 'Geist Mono', monospace; font-size: 10.5px;
    color: var(--ink-mute); margin-top: 6px;
  }

  
  
  .hub-vp-snap-card .tag {
    position: absolute; top: 12px; right: 12px;
    font-family: 'Geist Mono', monospace; font-size: 9px;
    color: var(--ink-faint);
    text-transform: uppercase; letter-spacing: 0.14em;
  }
  .hub-vp-snap-card .k {
    font-family: 'Geist Mono', monospace; font-size: 10px;
    color: var(--ink-dim);
    text-transform: uppercase; letter-spacing: 0.16em;
    margin-bottom: 10px;
  }
  .hub-vp-snap-card .v {
    font-size: 22px; font-weight: 560; line-height: 1.05;
    color: var(--ink);
    overflow-wrap: anywhere;
  }
  .hub-vp-snap-card .d {
    font-family: 'Geist Mono', monospace; font-size: 10.5px;
    color: var(--ink-mute); margin-top: 8px;
  }

  
  .hub-vp-note .ico {
    flex: none; width: 16px; height: 16px; border-radius: 50%;
    border: 1px solid var(--line-strong);
    display: grid; place-items: center;
    font-family: 'Geist Mono', monospace; font-size: 10px; color: var(--ink-mute);
    margin-top: 1px;
  }
  

  @media (max-width: 960px) {
    :root, .hub-root { --hub-vp-bottom-gap: 0px; }
    
	    
	    
	    
	    
	    
    
    
    
    
    
    
    
    .hub-validator-overview-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
    .hub-report-panel { grid-template-columns: 1fr; }
  }
	  @media (max-width: 600px) {
	    
	    
	    
	    
	    .hub-validator-overview-grid { grid-template-columns: 1fr 1fr; }
	  }

  /* ─── hero ─── */
  .hub-hero {
    margin-top: 16px;
    display: flex; align-items: flex-end; justify-content: flex-end; gap: 32px;
  }
  .hub-eyebrow {
    display: inline-flex; align-items: center; gap: 12px;
    font-family: 'Geist Mono', monospace;
    font-size: 11px; color: var(--ink-mute);
    text-transform: uppercase; letter-spacing: 0.18em;
    margin-bottom: 10px;
  }
  .hub-eyebrow::before {
    content: ""; width: 28px; height: 1px;
    background: linear-gradient(90deg, var(--chrome), transparent);
  }
  .hub-sub {
    font-size: 16px; line-height: 1.55;
    color: var(--ink-mute);
    max-width: 60ch;
    text-wrap: pretty;
    margin: 0;
  }
  .hub-sr-only {
    position: absolute;
    width: 1px; height: 1px;
    padding: 0; margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
  }
  
  
  
  
  
  
  .hub-flap-tile.blank { color: transparent; }
  @keyframes hubFlipSettle {
    0% { transform: rotateX(-76deg); filter: brightness(1.8) blur(.4px); }
    48% { transform: rotateX(18deg); }
    100% { transform: rotateX(0deg); filter: brightness(1) blur(0); }
  }
  
  .hub-hero-meta {
    display: grid;
    /* Track count MUST match the cell count. This row draws its hairlines by
       painting var(--line) on the container and letting the cells cover it, so a
       track with no cell in it is not whitespace, it is a solid grey slab.
       Measured: a fourth track with three cells left 136px grey at 1440.

       Left coupled rather than made self-sizing because it fails LOUDLY here: the
       slab is a third of the pill at the width everyone develops at. The phone rule
       below is scoped by parity instead, because there the same mistake hides until
       someone narrows the window. */
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 1px;
    border: 1px solid var(--line);
    background: var(--line);
    border-radius: 14px;
    overflow: hidden;
    min-width: min(500px, 100%);
  }
  .hub-hero-meta .cell {
    background: var(--bg-nav);
    padding: 10px 14px;
  }
  .hub-hero-meta .k {
    font-family: 'Geist Mono', monospace;
    font-size: 10px; color: var(--ink-dim);
    text-transform: uppercase; letter-spacing: 0.16em;
    margin-bottom: 6px;
  }
  .hub-hero-meta .v {
    font-family: 'Geist Mono', monospace;
    font-size: 13px; color: var(--ink);
  }
  /* Live indicator, moved off the network-status card so the card can open on
     its headline instead of a status line. */
  .hub-hero-meta .v.live, .hub-hero-meta .v.paused { display: inline-flex; align-items: center; gap: 7px; }
  .hub-hero-meta .v.live i, .hub-hero-meta .v.paused i {
    width: 6px; height: 6px; border-radius: 9999px; flex: none;
    background: var(--op-mint2);
  }
  .hub-hero-meta .v.paused i { background: var(--op-peach); }

  /* ─── Ported design-system tokens + animations (op-*) ─── */
  .hub-root {
    --op-card:#FFFFFF; --op-card2:#FBFCFD; --op-text:#282C34; --op-muted:#616B75; --op-faint:#66737F;
    --op-border:rgba(15,23,32,0.09); --op-hair:rgba(15,23,32,0.06); --op-chip:#F1F4F7; --op-track:rgba(15,23,32,0.06);
    --op-mint:#64E27F; --op-mint2:#2FB765; --op-peach:#D2701E; --op-azure:#51B1FE; --op-green:#1C9A54;
    --op-shadow:0 4px 40px rgba(113,121,129,0.10); --op-glow:rgba(81,177,254,0.10);
  }
  .hub-root[data-theme="dark"] {
    --op-card:#2A2E38; --op-card2:#2E323C; --op-text:#F4F6F8; --op-muted:#96A3B1; --op-faint:#8B96A4;
    --op-border:rgba(255,255,255,0.10); --op-hair:rgba(255,255,255,0.07); --op-chip:#33373F; --op-track:rgba(255,255,255,0.08);
    --op-green:#6EE38A; --op-peach:#FEA158; --op-shadow:0 4px 44px rgba(0,0,0,0.38); --op-glow:rgba(81,177,254,0.16);
  }
  @keyframes op-rise{from{opacity:0;transform:translateY(18px)}to{opacity:1;transform:none}}
  @keyframes op-tipin{from{opacity:0}to{opacity:1}}
  @keyframes op-growx{from{transform:scaleX(0)}to{transform:scaleX(1)}}
  @keyframes op-growy{from{transform:scaleY(0)}to{transform:scaleY(1)}}
  @keyframes op-pulse{0%,100%{opacity:.4;transform:scale(1)}50%{opacity:.95;transform:scale(1.25)}}
  .op-reveal{animation:op-rise .65s cubic-bezier(.22,.61,.36,1) both}
  .op-growx{transform-origin:left center;animation:op-growx 1.05s cubic-bezier(.22,.61,.36,1) both}
  .op-growy{transform-origin:bottom center;animation:op-growy .95s cubic-bezier(.22,.61,.36,1) both}
  .op-livedot{animation:op-pulse 2.2s ease-in-out infinite}
  .op-num{font-variant-numeric:tabular-nums}
  .op-hist-bar-wrap{transition:filter .15s ease}
  .op-hist-bar-wrap:hover{filter:brightness(1.08)}
  .op-hist-bar-wrap:focus-visible{outline:2px solid var(--op-azure);outline-offset:2px;border-radius:6px}
  @media (prefers-reduced-motion:reduce){.op-reveal,.op-growx,.op-growy,.op-livedot{animation:none !important}}

  /* bin-detail modal */
  .op-modal-scrim{position:fixed;inset:0;z-index:1000;background:rgba(15,23,32,0.42);backdrop-filter:blur(3px);-webkit-backdrop-filter:blur(3px);display:grid;place-items:center;padding:24px;animation:op-rise .18s ease both}
  .op-modal{position:relative;width:min(420px,100%);background:var(--op-card);border:1px solid var(--op-border);border-radius:18px;box-shadow:0 24px 70px rgba(0,0,0,0.28);padding:26px 28px;color:var(--op-text);animation:op-rise .28s cubic-bezier(.22,.61,.36,1) both}
  .op-modal-close{position:absolute;top:15px;right:15px;width:28px;height:28px;border-radius:8px;border:1px solid var(--op-border);background:var(--op-chip);color:var(--op-muted);cursor:pointer;font-size:12px;line-height:1}
  .op-modal-close:hover{color:var(--op-text)}
  .op-modal-eyebrow{font-family:'Geist Mono',monospace;font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--op-muted)}
  .op-modal-count{font-size:42px;font-weight:300;letter-spacing:-0.02em;line-height:1;margin:10px 0 4px;font-variant-numeric:tabular-nums}
  .op-modal-count span{font-size:15px;color:var(--op-muted);font-weight:400}
  .op-modal-sub{font-size:12px;color:var(--op-muted);margin-bottom:18px;line-height:1.4}
  .op-modal-rows{display:flex;flex-direction:column;gap:1px;background:var(--op-hair);border:1px solid var(--op-hair);border-radius:12px;overflow:hidden}
  .op-modal-row{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 14px;background:var(--op-card)}
  .op-modal-row .k{font-family:'Geist Mono',monospace;font-size:11px;letter-spacing:.04em;text-transform:uppercase;color:var(--op-muted)}
  .op-modal-row .v{font-size:15px}

  /* ─── section head ─── */
  .hub-section-head {
    display: flex; align-items: baseline; justify-content: space-between;
    margin: 24px 0 12px;
    scroll-margin-top: 96px;
  }
  .hub-section-head h2 {
    font-size: 17px; font-weight: 500; letter-spacing: -0.01em;
    margin: 0;
    display: flex; align-items: center; gap: 12px;
  }
  .hub-section-head h2::before {
    content: ""; width: 6px; height: 6px; border-radius: 2px;
    background: var(--midnight);
    box-shadow: 0 0 10px rgba(40,44,52,0.18);
  }
  .hub-section-head .count {
    font-family: 'Geist Mono', monospace; font-size: 11.5px; color: var(--ink-dim);
    letter-spacing: 0.1em;
  }
  .hub-section-head .actions {
    display: flex; gap: 14px; align-items: center;
    font-family: 'Geist Mono', monospace; font-size: 11px; color: var(--ink-mute);
  }
  .hub-section-head .actions a {
    color: var(--ink-mute); text-decoration: none;
    border-bottom: 1px solid var(--line-strong);
    padding-bottom: 1px;
  }
  .hub-section-head .actions a:hover { color: var(--ink); }
  /* Data-basis chip — tells the reader every stat on the page shares one basis. */
  .hub-basis {
    display: inline-flex; align-items: center; gap: 7px; cursor: default;
    font-family: 'Geist Mono', monospace; font-size: 10px; letter-spacing: .1em; text-transform: uppercase;
    color: var(--ink-mute); padding: 5px 11px; border-radius: 999px;
    border: 1px solid var(--line-strong); background: var(--bg-card);
  }
  .hub-basis .dot {
    width: 6px; height: 6px; border-radius: 50%; flex: none; background: var(--op-mint2, #64E27F);
    box-shadow: 0 0 0 3px rgba(100,226,127,0.20);
  }

  /* Methodology banner above the propagation cards — states, once, what every
     figure in the column is measuring (P80 · all connected validators · 25 slots). */
  .hub-avg-basis {
    background: var(--op-card); border: 1px solid var(--op-border); border-radius: 18px;
    box-shadow: var(--op-shadow); padding: 16px 18px;
  }
  .hub-avg-basis-head { display: flex; align-items: center; gap: 8px; margin-bottom: 13px; flex-wrap: wrap; row-gap: 8px; }
  .hub-avg-basis-head .cad { margin-left: auto; }
  @media (max-width: 560px) { .hub-avg-basis-head .cad { margin-left: 0; flex-basis: 100%; } }
  .hub-avg-basis-head .dot {
    width: 6px; height: 6px; border-radius: 50%; flex: none; background: var(--op-mint2, #64E27F);
    box-shadow: 0 0 0 3px rgba(100,226,127,0.20);
  }
  .hub-avg-basis-head .t { font-family: 'Geist Mono', monospace; font-size: 10.5px; letter-spacing: .13em; text-transform: uppercase; color: var(--op-muted); }
  .hub-avg-basis-head .r { margin-left: auto; font-family: 'Geist Mono', monospace; font-size: 9.5px; letter-spacing: .1em; text-transform: uppercase; color: var(--op-faint); }
  .hub-avg-basis-lede { margin: 0 0 13px; font-size: 12px; line-height: 1.5; color: var(--op-muted); }
  .hub-avg-basis-lede b { color: var(--op-text); font-weight: 600; }
  .hub-avg-basis-grid { display: grid; grid-template-columns: repeat(3, 1fr); border: 1px solid var(--op-hair); border-radius: 12px; overflow: hidden; }
  .hub-avg-basis-grid > div { display: flex; flex-direction: column; gap: 4px; padding: 10px 13px; border-right: 1px solid var(--op-hair); min-width: 0; }
  .hub-avg-basis-grid > div:last-child { border-right: none; }
  .hub-avg-basis-grid .k { font-family: 'Geist Mono', monospace; font-size: 9px; letter-spacing: .11em; text-transform: uppercase; color: var(--op-faint); }
  .hub-avg-basis-grid .v { font-size: 12px; color: var(--op-text); font-weight: 500; }
  @media (max-width: 520px) {
    .hub-avg-basis-grid { grid-template-columns: 1fr; }
    .hub-avg-basis-grid > div { border-right: none; border-bottom: 1px solid var(--op-hair); }
    .hub-avg-basis-grid > div:last-child { border-bottom: none; }
  }

  /* ─── main hero region (map full-width) ─── */
  .hub-network {
    display: block;
  }

  .hub-map-card {
    position: relative;
    border: 1px solid var(--line);
    border-radius: 22px;
    background: linear-gradient(180deg, #FFFFFF, #EEF2F5);
    overflow: hidden;
    isolation: isolate;
    min-height: 540px;
    box-shadow: 0 8px 32px rgba(40,44,52,0.06);
  }
  .hub-map-card::before {
    content: ""; position: absolute; inset: 0; border-radius: inherit;
    padding: 1px;
    background: linear-gradient(180deg, rgba(255,255,255,0.9), transparent 45%);
    -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
            mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
    -webkit-mask-composite: xor; mask-composite: exclude;
    pointer-events: none;
  }

  /* floating metric tile on map */
  .hub-overlay {
    position: absolute;
    border: 1px solid var(--line-strong);
    border-radius: 14px;
    background: var(--bg-nav);
    backdrop-filter: blur(14px) saturate(140%);
    padding: 12px 16px;
    box-shadow: 0 20px 40px rgba(40,44,52,0.08);
    z-index: 2;
  }
  .hub-overlay .k {
    font-family: 'Geist Mono', monospace;
    font-size: 10px; color: var(--ink-dim);
    text-transform: uppercase; letter-spacing: 0.16em;
    margin-bottom: 6px;
  }
  .hub-overlay .v {
    font-size: 26px; letter-spacing: -0.03em; font-weight: 500; color: var(--ink);
    line-height: 1;
  }
  .hub-overlay .v .unit {
    font-size: 13px; font-weight: 400; color: var(--ink-dim);
    margin-left: 3px;
  }
  .hub-overlay .delta {
    font-family: 'Geist Mono', monospace; font-size: 10.5px;
    color: var(--ink-mute); margin-top: 4px;
  }

  /* ─── globe two-pane card ─── */
  .hub-globe-card {
    display: grid;
    /* The brand's signature asymmetric corner pattern. Tracks are deliberately
       uneven: the globe reads fine narrower, and the extra width is what lets the
       lede sit on one line instead of wrapping. Collapses at 900px below. */
    grid-template-columns: minmax(0, 0.8fr) minmax(0, 1.2fr);
    align-items: center;
    --card-pad: clamp(20px, 2.4vw, 34px);
    gap: clamp(24px, 3vw, 48px);
    padding: var(--card-pad);
    min-height: 440px;
    border: 1px solid var(--line);
    border-radius: 20px 80px 20px 80px;
    /* Was white → #EEF2F5, which read as a white card floating on the grey
       page. Nudged so the top sits level with the page and the foot goes a
       little steel — still lighter than the report cards below, so the hero
       stays the lighter of the two surfaces. */
    background: linear-gradient(180deg, #F2F5F8, #E6EBF1);
    /* No overflow:hidden here — the globe is already clipped by .hub-globe-pane's
       own overflow, and clipping the whole card would cut off the upward help
       tooltips (mumP2P connected / average latency). */
    position: relative;
    isolation: isolate;
    box-shadow: 0 8px 32px rgba(40,44,52,0.06);
  }
  .hub-globe-card::before {
    content: ""; position: absolute; inset: 0; border-radius: inherit;
    padding: 1px;
    background: linear-gradient(180deg, rgba(255,255,255,0.9), transparent 45%);
    -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
            mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
    -webkit-mask-composite: xor; mask-composite: exclude;
    pointer-events: none;
  }
  .hub-globe-pane {
    position: relative;
    min-height: 420px;
    overflow: hidden;
    /* Cancel the card's padding on the three outer sides: the globe reads better
       full-bleed, and it puts the expand control at the real corner
       rather than inset by the padding plus their own offset. */
    align-self: stretch;
    margin: calc(-1 * var(--card-pad)) 0 calc(-1 * var(--card-pad)) calc(-1 * var(--card-pad));
    border-radius: 20px 0 0 80px;
  }

  .hub-globe-tag {
    position: absolute; left: 22px; right: 22px; bottom: 18px;
    font-family: 'Geist Mono', monospace; font-size: 10px;
    color: var(--ink-dim); letter-spacing: 0.12em;
    text-transform: uppercase;
    display: flex; align-items: center; justify-content: space-between; gap: 14px;
    background: var(--bg-nav);
    backdrop-filter: blur(10px);
    border: 1px solid var(--line);
    border-radius: 12px;
    padding: 7px 10px;
    z-index: 2;
  }
  .hub-globe-tag > span {
    display: inline-flex; align-items: center; gap: 8px;
    min-width: 0;
  }
  .hub-globe-tag .count {
    color: var(--ink-mute);
    letter-spacing: 0.08em;
    white-space: nowrap;
  }
  .hub-globe-tag .gd {
    width: 6px; height: 6px; border-radius: 50%;
    background: var(--accent);
    box-shadow: 0 0 0 3px rgba(81,177,254,0.18), 0 0 8px rgba(81,177,254,0.55);
    animation: hubPulse 2.6s ease-in-out infinite;
  }
  .hub-metrics-panel {
    position: relative;
    padding: 0;
    display: flex;
    flex-direction: column;
    justify-content: center;
    gap: 16px;
    min-height: 440px;
    background: transparent;
    /* No overflow:hidden — it would clip the upward help tooltips. The glow
       ::before is kept inside the panel bounds so nothing spills; the trusted-by
       marquee clips itself via .op-marquee-viewport. */
  }
  .hub-metrics-panel::before {
    content: ""; position: absolute; right: 20px; top: 20px;
    width: 300px; height: 300px; border-radius: 50%;
    background: radial-gradient(circle, rgba(81,177,254,0.10), transparent 62%);
    pointer-events: none;
  }
  
  
  
  
  /* Both the stats card and the search KPIs span full width, so this is a
     vertical stack. Flex-fill the panel height so the network-status card's
     header pins to the top and its partner marquee pins to the bottom
     (no floating-in-the-middle void on desktop, where the panel matches the
     tall globe). On mobile the panel is content-height, so it just stacks. */
  .hub-globe-metrics-grid {
    position: relative;
    display: flex;
    flex-direction: column;
    gap: 12px;
    flex: 1 1 auto;
    min-height: 0;
  }
  .hub-globe-metrics-grid .hub-kpi {
    min-height: 126px;
    padding: 16px 16px 14px;
    border-radius: 16px;
    box-shadow: 0 4px 14px rgba(40,44,52,0.035);
  }
  .hub-globe-metrics-grid .hub-kpi.wide { grid-column: 1 / -1; }
  .hub-globe-metrics-grid .hub-kpi .k {
    gap: 10px;
    margin-bottom: 12px;
    align-items: flex-start;
  }
  .hub-globe-metrics-grid .hub-kpi .k span:first-child { min-width: 0; }
  .hub-globe-metrics-grid .hub-kpi .k .live {
    flex: none;
    padding-top: 1px;
  }
  .hub-globe-metrics-grid .hub-kpi .v { font-size: 28px; }
  .hub-stake-card .hub-stake-metrics {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 18px;
    align-items: start;
    padding-top: 4px;
  }
  .hub-stake-card .hub-stake-metric {
    min-width: 0;
    display: flex; flex-direction: column;
    gap: 8px;
    padding-right: 12px;
    border-right: 1px dashed rgba(40,44,52,0.10);
  }
  .hub-stake-card .hub-stake-metric:last-child { border-right: none; padding-right: 0; }
  .hub-stake-card .mini-label {
    font-family: 'Geist Mono', monospace;
    font-size: 10px;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--ink-dim);
    min-height: 2.4em;
    display: flex;
    align-items: flex-start;
  }
  .hub-stake-card .mini-value {
    font-size: 30px;
    line-height: 0.98;
    font-weight: 550;
    color: var(--ink);
    letter-spacing: -0.01em;
    overflow-wrap: anywhere;
  }
  .hub-stake-card .mini-value.unavailable {
    font-size: 18px;
    line-height: 1.1;
    color: var(--ink-mute);
  }
  .hub-debug-toggle {
    border: 0;
    background: transparent;
    color: var(--ink);
    border-bottom: 1px solid var(--line-strong);
    font: inherit;
    padding: 0;
    cursor: pointer;
  }
  .hub-rated-debug {
    margin-top: 12px;
    padding-top: 12px;
    border-top: 1px dashed rgba(40,44,52,0.10);
    max-height: 240px;
    overflow: auto;
  }
  .hub-rated-debug table {
    width: 100%;
    border-collapse: collapse;
    font-family: 'Geist Mono', monospace;
    font-size: 10px;
    color: var(--ink-mute);
  }
  .hub-rated-debug th,
  .hub-rated-debug td {
    text-align: left;
    padding: 7px 6px;
    border-bottom: 1px solid rgba(40,44,52,0.08);
    white-space: nowrap;
  }
  .hub-rated-debug th {
    color: var(--ink-dim);
    text-transform: uppercase;
    letter-spacing: 0.12em;
    font-weight: 500;
  }
  
  .hub-vp-impact .confidence {
    display: inline-flex;
    color: var(--ink);
    background: rgba(91,180,255,0.12);
    border: 1px solid rgba(91,180,255,0.26);
    border-radius: 999px;
    padding: 5px 9px;
    font-family: 'Geist Mono', monospace;
    font-size: 9.5px;
    letter-spacing: 0.08em;
    text-transform: uppercase;
  }
  
  .hub-vp-impact .impact {
    font-size: 38px;
    line-height: 0.95;
    font-weight: 560;
    letter-spacing: 0;
    color: var(--ink);
  }
  .hub-vp-impact .assumption {
    font-size: 12px;
    line-height: 1.45;
    color: var(--ink-mute);
  }
  .hub-node-headline {
    display: grid;
    grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr);
    gap: 12px;
    align-items: end;
    padding-bottom: 13px;
    border-bottom: 1px dashed rgba(40,44,52,0.10);
  }
  .hub-node-headline .label,
  .hub-node-evidence .label,
  .hub-node-trust .label {
    font-family: 'Geist Mono', monospace;
    font-size: 9.5px;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--ink-dim);
    margin-bottom: 5px;
  }
  .hub-node-headline .impact {
    font-size: 36px;
    line-height: 0.95;
    font-weight: 550;
    letter-spacing: 0;
    color: var(--ink);
  }
  .hub-node-headline .assumption {
    font-size: 12px;
    line-height: 1.45;
    color: var(--ink-mute);
  }
  .hub-node-evidence,
  .hub-node-trust {
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: 10px;
    margin-top: 13px;
  }
  
  .hub-node-trust {
    grid-template-columns: repeat(5, minmax(0, 1fr));
  }
  .hub-node-evidence .value,
  .hub-node-trust .value {
    font-size: 17px;
    font-weight: 560;
    letter-spacing: 0;
    color: var(--ink);
    white-space: nowrap;
  }
  .hub-node-trust .value {
    font-size: 13px;
  }
  .hub-node-note {
    margin-top: -4px;
    margin-bottom: 16px;
    font-size: 11px;
    line-height: 1.45;
    color: var(--ink-mute);
  }
  
  
  .hub-globe-metrics-grid .hub-kpi .spark { margin-top: 9px; height: 22px; }

  .hub-detail-pane {
    position: relative;
    padding: 26px 28px 22px;
    display: flex; flex-direction: column;
    background: linear-gradient(180deg, rgba(255,255,255,0.65), rgba(232,237,242,0.85));
    min-height: 580px;
  }
  .hub-detail-pane::before {
    content: ""; position: absolute; right: -100px; top: -80px;
    width: 320px; height: 320px; border-radius: 50%;
    background: radial-gradient(circle, rgba(81,177,254,0.10), transparent 60%);
    pointer-events: none;
  }
  .hub-detail-meta {
    display: flex; align-items: center; justify-content: space-between;
    font-family: 'Geist Mono', monospace;
    font-size: 10px; color: var(--ink-dim);
    text-transform: uppercase; letter-spacing: 0.18em;
    margin-bottom: 14px;
    position: relative;
  }
  .hub-detail-meta .crumb {
    display: inline-flex; align-items: center; gap: 8px;
  }
  .hub-detail-meta .crumb .seg + .seg::before {
    content: "/";
    color: var(--ink-faint);
    margin-right: 8px;
  }
  .hub-detail-meta .crumb .here { color: var(--ink); }
  .hub-detail-content {
    flex: 1;
    display: flex; flex-direction: column;
    animation: hubDetailIn .55s cubic-bezier(.3,1.2,.4,1) both;
    position: relative;
  }
  @keyframes hubDetailIn {
    0%   { opacity: 0; transform: translateY(8px); filter: blur(2px); }
    60%  { opacity: 1; filter: blur(0); }
    100% { opacity: 1; transform: translateY(0); }
  }
  .hub-detail-top {
    display: flex; align-items: flex-start; gap: 16px;
    margin-bottom: 14px;
  }
  .hub-detail-avatar {
    width: 54px; height: 54px; flex: none;
    border-radius: 16px;
    background: linear-gradient(135deg, var(--midnight), var(--carbon));
    border: 1px solid var(--line-strong);
    display: grid; place-items: center;
    font-family: 'Geist Mono', monospace;
    font-size: 16px; color: var(--alabaster); font-weight: 500;
    letter-spacing: 0.02em;
    position: relative; overflow: hidden;
  }
  .hub-detail-avatar::after {
    content: ""; position: absolute; inset: 1px; border-radius: 15px;
    background: radial-gradient(circle at 30% 25%, rgba(255,255,255,0.25), transparent 55%);
  }
  .hub-detail-name {
    font-size: 26px; font-weight: 500; letter-spacing: -0.025em;
    line-height: 1.05; margin: 2px 0 4px;
    color: var(--ink);
    text-wrap: balance;
  }
  .hub-detail-region {
    font-family: 'Geist Mono', monospace; font-size: 11.5px;
    color: var(--ink-mute); display: inline-flex; align-items: center; gap: 6px;
  }
  .hub-detail-region::before {
    content: ""; width: 5px; height: 5px; border-radius: 50%;
    background: var(--ink-dim);
  }
  .hub-detail-chips {
    display: flex; gap: 6px; flex-wrap: wrap;
    margin: 14px 0 18px;
  }
  .hub-detail-chip {
    display: inline-flex; align-items: center; gap: 6px;
    padding: 5px 10px;
    border-radius: 999px;
    border: 1px solid var(--line);
    background: var(--bg-nav);
    font-family: 'Geist Mono', monospace; font-size: 10.5px;
    color: var(--ink-mute);
    text-transform: uppercase; letter-spacing: 0.1em;
  }
  .hub-detail-chip b { color: var(--ink); font-weight: 500; }
  .hub-detail-stats {
    display: grid; grid-template-columns: 1fr 1fr 1fr;
    gap: 1px;
    border: 1px solid var(--line);
    background: var(--line);
    border-radius: 14px;
    overflow: hidden;
    margin-bottom: 18px;
  }
  .hub-detail-stat {
    background: var(--bg-nav);
    padding: 12px 14px;
  }
  .hub-detail-stat .k {
    font-family: 'Geist Mono', monospace;
    font-size: 9.5px; color: var(--ink-dim);
    text-transform: uppercase; letter-spacing: 0.16em;
    margin-bottom: 8px;
  }
  .hub-detail-stat .v {
    font-size: 22px; font-weight: 500; letter-spacing: -0.025em;
    line-height: 1;
    color: var(--ink);
  }
  .hub-detail-stat .v .u {
    font-size: 11px; color: var(--ink-dim);
    margin-left: 2px;
  }
  .hub-detail-stat .d {
    font-family: 'Geist Mono', monospace;
    font-size: 10.5px; color: var(--ink-mute);
    margin-top: 6px;
  }
  .hub-detail-section { margin-bottom: 16px; }
  .hub-detail-section .h {
    display: flex; align-items: baseline; justify-content: space-between;
    margin-bottom: 8px;
    font-family: 'Geist Mono', monospace;
    font-size: 10px; color: var(--ink-dim);
    text-transform: uppercase; letter-spacing: 0.16em;
  }
  .hub-detail-section .h .right { color: var(--ink-mute); }
  .hub-detail-bar {
    display: flex; align-items: center; gap: 12px;
    padding: 7px 0;
    font-size: 13px;
  }
  .hub-detail-bar + .hub-detail-bar { border-top: 1px dashed rgba(40,44,52,0.08); }
  .hub-detail-bar .lbl { width: 120px; color: var(--ink-mute); font-size: 12.5px; }
  .hub-detail-bar .track {
    flex: 1; height: 4px; border-radius: 2px;
    background: rgba(40,44,52,0.06);
    overflow: hidden;
  }
  .hub-detail-bar .track > i {
    display: block; height: 100%; border-radius: 2px;
    background: linear-gradient(90deg, var(--platinum) 0%, var(--carbon) 60%, var(--midnight) 100%);
    transform-origin: left;
    animation: hubBarIn .9s cubic-bezier(.4,1.3,.4,1) both;
  }
  .hub-detail-bar .val {
    width: 72px; text-align: right;
    font-family: 'Geist Mono', monospace; font-size: 12px; color: var(--ink);
  }
  .hub-detail-foot {
    margin-top: auto;
    padding-top: 14px;
    border-top: 1px dashed rgba(40,44,52,0.10);
    display: flex; gap: 10px; align-items: center;
  }
  .hub-detail-btn {
    flex: 1;
    padding: 11px 14px;
    border-radius: 12px;
    border: 1px solid transparent;
    background: linear-gradient(180deg, var(--midnight), var(--carbon));
    color: var(--alabaster);
    font-family: 'Geist', system-ui, sans-serif;
    font-size: 12.5px; font-weight: 500;
    cursor: pointer;
    transition: transform .15s, box-shadow .2s;
    display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  }
  .hub-detail-btn:hover { transform: translateY(-1px); box-shadow: 0 8px 24px rgba(40,44,52,0.22); }
  .hub-detail-btn.ghost {
    background: var(--bg-nav);
    border-color: var(--line-strong);
    color: var(--ink);
  }
  .hub-detail-btn.ghost:hover { background: rgba(255,255,255,0.9); box-shadow: 0 4px 14px rgba(40,44,52,0.06); }

  /* map legend */
  .hub-legend {
    position: absolute; bottom: 16px; right: 18px;
    border: 1px solid var(--line-strong); border-radius: 10px;
    background: rgba(255,255,255,0.85); backdrop-filter: blur(14px);
    padding: 8px 12px;
    font-family: 'Geist Mono', monospace; font-size: 10px; color: var(--ink-mute);
    letter-spacing: 0.04em;
    display: flex; gap: 12px; align-items: center;
    z-index: 2;
  }

  /* ─── mainnet readiness ─── */
  .hub-readiness {
    border: 1px solid var(--line);
    border-radius: 22px;
    background: linear-gradient(180deg, rgba(50,55,66,0.55), rgba(36,40,48,0.55));
    backdrop-filter: blur(14px);
    padding: 26px;
    position: relative;
    isolation: isolate;
    overflow: hidden;
    display: flex; flex-direction: column;
  }
  .hub-readiness::before {
    content: ""; position: absolute; inset: 0; border-radius: inherit;
    padding: 1px;
    background: linear-gradient(180deg, rgba(255,255,255,0.10), transparent 45%);
    -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
            mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
    -webkit-mask-composite: xor; mask-composite: exclude;
    pointer-events: none;
  }
  .hub-readiness-blob {
    position: absolute;
    right: -60px; top: -30px;
    width: 200px;
    opacity: 0.22;
    filter: drop-shadow(0 20px 30px rgba(0,0,0,0.4));
    pointer-events: none;
    z-index: 0;
  }
  .hub-readiness-head {
    display: flex; align-items: baseline; justify-content: space-between;
    margin-bottom: 18px;
    position: relative; z-index: 1;
  }
  .hub-readiness-head .tag {
    font-family: 'Geist Mono', monospace;
    font-size: 10.5px; color: var(--ink-dim);
    text-transform: uppercase; letter-spacing: 0.18em;
  }
  .hub-readiness-head .target {
    font-family: 'Geist Mono', monospace;
    font-size: 11px; color: var(--ink-mute);
  }
  .hub-readiness h3 {
    font-size: 22px; letter-spacing: -0.02em; font-weight: 500;
    margin: 0 0 6px;
    position: relative; z-index: 1;
  }
  .hub-readiness .lede {
    font-size: 13.5px; color: var(--ink-mute); line-height: 1.5;
    margin: 0 0 18px;
    position: relative; z-index: 1;
  }
  .hub-readiness-progress {
    position: relative; z-index: 1;
    margin-bottom: 22px;
  }
  .hub-readiness-progress .row1 {
    display: flex; align-items: baseline; justify-content: space-between;
    margin-bottom: 8px;
  }
  .hub-readiness-progress .num {
    font-size: 54px; font-weight: 500; letter-spacing: -0.035em;
    line-height: 1;
    color: var(--ink);
  }
  .hub-readiness-progress .num .pct {
    font-size: 20px; color: var(--ink-dim);
    margin-left: 2px;
  }
  .hub-readiness-progress .meta {
    font-family: 'Geist Mono', monospace; font-size: 11px;
    color: var(--ink-mute); text-align: right;
  }
  .hub-readiness-progress .meta strong { color: var(--ink); font-weight: 500; }
  .hub-readiness-progress .track {
    height: 6px; border-radius: 3px;
    background: var(--bg-nav);
    overflow: hidden;
    position: relative;
  }
  .hub-readiness-progress .track > i {
    display: block; height: 100%;
    background: linear-gradient(90deg, var(--carbon) 0%, var(--platinum) 52%, #FAFBFC 100%);
    border-radius: 3px;
    box-shadow: 0 0 12px rgba(157,176,194,0.32);
  }

  .hub-checklist {
    display: flex; flex-direction: column; gap: 0;
    position: relative; z-index: 1;
    margin-top: auto;
  }
  .hub-check {
    display: flex; align-items: center; gap: 12px;
    padding: 11px 0;
    border-bottom: 1px dashed rgba(255,255,255,0.07);
    font-size: 13px; color: var(--ink);
  }
  .hub-check:last-child { border-bottom: 0; }
  .hub-check .icon {
    width: 18px; height: 18px; flex: none;
    border-radius: 50%;
    display: grid; place-items: center;
    border: 1px solid var(--line-strong);
    background: var(--bg-nav);
  }
  .hub-check.done .icon {
    background: linear-gradient(135deg, #FAFBFC, var(--steel));
    border-color: transparent;
    color: #1E2128;
  }
  .hub-check.wip .icon {
    background: rgba(232,182,143,0.18);
    border-color: rgba(232,182,143,0.4);
  }
  .hub-check.wip .icon::after {
    content: ""; width: 6px; height: 6px; border-radius: 50%;
    background: var(--warn);
    animation: hubPulse 2.2s ease-in-out infinite;
  }
  .hub-check .label {
    flex: 1; line-height: 1.3;
  }
  .hub-check .label small {
    display: block;
    font-family: 'Geist Mono', monospace; font-size: 10.5px;
    color: var(--ink-dim);
    margin-top: 2px; letter-spacing: 0.02em;
  }
  .hub-check .status {
    font-family: 'Geist Mono', monospace;
    font-size: 10.5px; color: var(--ink-mute);
    text-transform: uppercase; letter-spacing: 0.14em;
  }
  .hub-check.done .status { color: var(--chrome-hi); }
  .hub-check.wip .status { color: var(--warn); }

  /* ─── KPI strip ─── */
  .hub-kpi-strip {
    display: grid;
    grid-template-columns: repeat(5, 1fr);
    gap: 16px;
    margin-top: 22px;
  }
  .hub-kpi {
    position: relative;
    border: 1px solid var(--line);
    border-radius: 18px;
    background: var(--bg-card);
    backdrop-filter: blur(14px);
    padding: 18px 20px 16px;
    overflow: hidden;
    isolation: isolate;
    box-shadow: 0 4px 16px rgba(40,44,52,0.04);
  }
  .hub-kpi::before {
    content: ""; position: absolute; inset: 0; border-radius: inherit;
    padding: 1px;
    background: linear-gradient(180deg, rgba(255,255,255,0.9), transparent 45%);
    -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
            mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
    -webkit-mask-composite: xor; mask-composite: exclude;
    pointer-events: none;
  }
  .hub-kpi .k {
    font-family: 'Geist Mono', monospace;
    font-size: 10px; color: var(--ink-dim);
    text-transform: uppercase; letter-spacing: 0.16em;
    margin-bottom: 14px;
    display: flex; align-items: center; justify-content: space-between;
  }
  .hub-kpi .k .live {
    font-size: 9px; color: var(--accent);
  }
  .hub-kpi .v {
    font-size: 30px; line-height: 1;
    letter-spacing: -0.03em; font-weight: 500;
    color: var(--ink);
    margin-bottom: 4px;
  }
  .hub-kpi .v .unit {
    color: var(--ink-dim);
    font-size: 13px; font-weight: 400;
    margin-left: 3px;
  }
  .hub-kpi .delta {
    font-family: 'Geist Mono', monospace; font-size: 11px;
    color: var(--ink-mute);
  }
  .hub-kpi .delta.bad { color: var(--warn); }
  .hub-kpi .cap {
    font-size: 12px; line-height: 1.45;
    color: var(--ink-mute);
    margin-top: 12px; padding-top: 10px;
    border-top: 1px dashed rgba(40,44,52,0.10);
    text-wrap: pretty;
  }
  .hub-kpi .cap a {
    color: var(--ink);
    text-decoration: none;
    border-bottom: 1px solid var(--line-strong);
  }
  .hub-kpi .cap a:hover { color: var(--accent); }
  .hub-kpi .spark { width: 100%; height: 24px; display: block; margin-top: 10px; }

  /* ─── partners table + live feed split ─── */
  .hub-partners {
    border: 1px solid var(--line);
    border-radius: 22px;
    background: var(--bg-card);
    backdrop-filter: blur(14px);
    overflow: hidden;
    isolation: isolate;
    position: relative;
    box-shadow: 0 4px 16px rgba(40,44,52,0.04);
  }
  .hub-partners::before {
    content: ""; position: absolute; inset: 0; border-radius: inherit;
    padding: 1px;
    background: linear-gradient(180deg, rgba(255,255,255,0.10), transparent 45%);
    -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
            mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
    -webkit-mask-composite: xor; mask-composite: exclude;
    pointer-events: none;
  }
  .hub-partners-head {
    display: grid;
    grid-template-columns: 1.1fr minmax(0, 3.4fr);
    gap: 24px;
    padding: 14px 22px;
    font-family: 'Geist Mono', monospace;
    font-size: 10.5px; color: var(--ink-dim);
    text-transform: uppercase; letter-spacing: 0.16em;
    border-bottom: 1px solid var(--line);
  }
  .hub-partner-row {
    display: grid;
    grid-template-columns: 1.1fr minmax(0, 3.4fr);
    gap: 24px; align-items: center;
    padding: 14px 22px;
    border-bottom: 1px dashed rgba(40,44,52,0.08);
    font-size: 13px;
    transition: background .15s;
    cursor: pointer;
  }
  .hub-partner-row:last-child { border-bottom: none; }
  .hub-partner-row:hover { background: rgba(51,54,60,0.04); }

  .hub-partners-filter {
    display: flex;
    align-items: center;
    gap: 18px;
    flex-wrap: wrap;
    margin: 12px 0 14px;
    padding: 10px 14px;
    border: 1px solid var(--line);
    border-radius: 14px;
    background: var(--bg-card);
    box-shadow: 0 2px 8px rgba(40,44,52,0.03);
  }
  .hub-partners-filter .filter-group {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    flex-wrap: wrap;
  }
  .hub-partners-filter .filter-lbl {
    font-family: 'Geist Mono', monospace;
    font-size: 10px;
    color: var(--ink-dim);
    text-transform: uppercase;
    letter-spacing: 0.16em;
    margin-right: 4px;
  }
  .hub-partners-filter .filter-snap {
    padding-right: 14px;
    margin-right: 4px;
    border-right: 1px solid var(--line);
  }
  .hub-partners-filter .filter-snap-meta {
    font-family: 'Geist Mono', monospace;
    font-size: 11px;
    font-weight: 500;
    color: var(--ink);
    font-variant-numeric: tabular-nums;
    letter-spacing: 0;
  }
  .hub-partners-filter .filter-refresh {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 4px 10px 4px 8px;
    border-radius: 999px;
    border: 1px solid var(--line);
    background: transparent;
    color: var(--ink-mute);
    font-family: 'Geist Mono', monospace;
    font-size: 10.5px;
    letter-spacing: 0.04em;
    font-weight: 500;
    cursor: pointer;
    transition: background .15s, border-color .15s, color .15s, transform .2s;
  }
  .hub-partners-filter .filter-refresh:hover {
    border-color: var(--line-strong);
    color: var(--ink);
  }
  .hub-partners-filter .filter-refresh:active svg { transform: rotate(180deg); }
  .hub-partners-filter .filter-refresh svg { transition: transform .35s ease; }
  .hub-root[data-theme="dark"] .hub-partners-filter .filter-refresh {
    border-color: rgba(255,251,251,0.10);
    color: var(--ink-dim);
  }
  .hub-root[data-theme="dark"] .hub-partners-filter .filter-refresh:hover {
    color: var(--ink);
    border-color: rgba(255,251,251,0.22);
  }
  .hub-partners-filter .filter-chip {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 5px 11px;
    border-radius: 999px;
    border: 1px solid var(--line);
    background: transparent;
    color: var(--ink-mute);
    font-family: 'Geist Mono', monospace;
    font-size: 11px;
    letter-spacing: 0.04em;
    font-weight: 500;
    cursor: pointer;
    transition: background .15s, border-color .15s, color .15s;
  }
  .hub-partners-filter .filter-chip:hover {
    border-color: var(--line-strong);
    color: var(--ink);
  }
  .hub-partners-filter .filter-chip.on {
    background: var(--ink);
    border-color: var(--ink);
    color: var(--bg-card);
  }
  .hub-partners-filter .filter-chip .filter-count {
    font-size: 10px;
    color: var(--ink-dim);
    background: rgba(51,54,60,0.06);
    padding: 1px 6px;
    border-radius: 999px;
    font-variant-numeric: tabular-nums;
  }
  .hub-partners-filter .filter-chip.on .filter-count {
    color: var(--bg-card);
    background: rgba(255,255,255,0.18);
  }
  .hub-partners-empty {
    padding: 32px 22px;
    text-align: center;
    color: var(--ink-dim);
    font-family: 'Geist Mono', monospace;
    font-size: 12px;
    letter-spacing: 0.04em;
  }
  /* Used by .hub-chain-drop, the nav chain selector. It was declared inside the
     help-overlay block that used to sit here; kept when that was deleted, because
     nothing about the animation was the overlay's. */
  @keyframes hub-help-fade { from { opacity: 0; } to { opacity: 1; } }
  @keyframes hub-mobile-menu-in { from { opacity: 0; transform: translateY(-6px) scale(.99); } to { opacity: 1; transform: none; } }
  .hub-root[data-theme="dark"] .hub-partners-filter {
    background: rgba(42,46,55,0.62);
    border-color: rgba(255,251,251,0.08);
  }
  .hub-root[data-theme="dark"] .hub-partners-filter .filter-chip {
    color: var(--ink-dim);
  }
  .hub-root[data-theme="dark"] .hub-partners-filter .filter-chip:hover {
    color: var(--ink);
    border-color: rgba(255,251,251,0.22);
  }
  .hub-root[data-theme="dark"] .hub-partners-filter .filter-chip.on {
    background: var(--alabaster);
    color: var(--midnight);
    border-color: var(--alabaster);
  }
  .hub-root[data-theme="dark"] .hub-partners-filter .filter-chip .filter-count {
    background: rgba(255,251,251,0.12);
    color: var(--ink-dim);
  }
  .hub-root[data-theme="dark"] .hub-partners-filter .filter-chip.on .filter-count {
    background: rgba(40,44,52,0.20);
    color: var(--midnight);
  }

  .hub-partner-name {
    display: flex; align-items: center; gap: 12px;
    font-weight: 500;
  }
  .hub-partner-avatar {
    width: 30px; height: 30px; flex: none;
    border-radius: 50%;
    background: var(--bg-card);
    border: 1px solid var(--line-strong);
    display: grid; place-items: center;
    font-family: 'Geist Mono', monospace;
    font-size: 11px; color: var(--ink);
    overflow: hidden;
    position: relative;
    padding: 3px;
  }
  .hub-partner-avatar img {
    width: 100%; height: 100%;
    object-fit: contain;
    display: block;
  }
  .hub-partner-avatar-fb {
    width: 100%; height: 100%;
    background: linear-gradient(135deg, var(--midnight), var(--carbon));
    color: var(--alabaster);
    display: grid; place-items: center;
  }

  
  
  .hub-overview-story .eyebrow {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    width: max-content;
    font-family: 'Geist Mono', monospace;
    font-size: 10.5px;
    color: var(--ink-dim);
    text-transform: uppercase;
    letter-spacing: 0.16em;
  }
  .hub-overview-story .eyebrow .dot {
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: var(--good);
    box-shadow: 0 0 8px rgba(100,226,127,0.55);
  }
  
  
  
  
  
  
  
  .hub-overview-story-metric.primary {
    background: #20242B;
    border-color: rgba(255,255,255,0.08);
    color: var(--alabaster);
  }
  .hub-overview-story-metric .k {
    font-family: 'Geist Mono', monospace;
    font-size: 9.5px;
    color: var(--ink-dim);
    text-transform: uppercase;
    letter-spacing: 0.15em;
    line-height: 1.35;
  }
  .hub-overview-story-metric.primary .k { color: rgba(255,255,255,0.52); }
  .hub-overview-story-metric .v {
    color: var(--ink);
    font-size: 24px;
    font-weight: 560;
    line-height: 1;
    letter-spacing: -0.01em;
    overflow-wrap: anywhere;
  }
  .hub-overview-story-metric.primary .v { color: var(--alabaster); }
  .hub-overview-story-metric .d {
    font-family: 'Geist Mono', monospace;
    font-size: 10px;
    color: var(--ink-mute);
    line-height: 1.35;
  }
  .hub-overview-story-metric.primary .d { color: rgba(255,255,255,0.48); }

  
  
  
  .hub-overview-proof-card .k {
    font-family: 'Geist Mono', monospace;
    font-size: 9.5px;
    color: var(--ink-dim);
    text-transform: uppercase;
    letter-spacing: 0.14em;
    line-height: 1.35;
  }
  .hub-overview-proof-card .v {
    color: var(--ink);
    font-size: 25px;
    font-weight: 560;
    line-height: 1.02;
    letter-spacing: -0.01em;
    overflow-wrap: anywhere;
  }
  .hub-overview-proof-card .d {
    color: var(--ink-mute);
    font-size: 12px;
    line-height: 1.45;
  }
  .hub-overview-proof-card .meta {
    margin-top: auto;
    padding-top: 10px;
    border-top: 1px dashed rgba(40,44,52,0.10);
    color: var(--ink-dim);
    font-family: 'Geist Mono', monospace;
    font-size: 10px;
    line-height: 1.35;
  }

  
  
  
  .hub-vp-report-card .head {
    display: flex; justify-content: space-between; align-items: center;
    padding-bottom: 14px;
    border-bottom: 1px solid var(--line);
  }
  .hub-vp-report-card .head h3 {
    margin: 0; font-size: 17px; font-weight: 500; letter-spacing: -0.01em;
  }
  .hub-vp-report-card .head .mark {
    font-family: 'Geist Mono', monospace; font-size: 9.5px;
    color: var(--ink-dim);
    text-transform: uppercase; letter-spacing: 0.16em;
  }
  
  
  
  .hub-vp-report-card ul li .v {
    font-family: 'Geist Mono', monospace;
    color: var(--ink-dim);
  }
  .hub-vp-report-card .foot {
    margin-top: auto;
    padding-top: 14px;
    border-top: 1px dashed var(--line-strong);
    font-family: 'Geist Mono', monospace; font-size: 10px;
    color: var(--ink-mute);
    letter-spacing: 0.06em; text-transform: uppercase;
  }

  /* ─── Shred Race (horizontal lane race {
    position: relative;
    border: 1px solid var(--line);
    border-radius: 18px;
    background: var(--bg-card);
    padding: 22px 24px 22px;
    box-shadow: 0 4px 16px rgba(40,44,52,0.04);
    margin-bottom: 14px;
    overflow: hidden;
  }
  
  .hub-vp-pipeline-head .titles { display: flex; flex-direction: column; gap: 4px; }
  .hub-vp-pipeline-head .ttl {
    color: var(--ink); font-size: 14px; letter-spacing: -0.005em;
    text-transform: none; font-weight: 500;
  }
  .hub-vp-pipeline-head .legend {
    font-family: 'Geist Mono', monospace; font-size: 11px;
    color: var(--ink-mute);
    text-transform: none; letter-spacing: 0.02em;
    font-weight: 400;
  }
  .hub-vp-pipeline-head .legend b { color: var(--ink); font-weight: 500; }
  .hub-vp-pipeline-head .live {
    color: #2074C9;
    display: inline-flex; align-items: center; gap: 8px;
  }
  .hub-vp-pipeline-head .live::before {
    content: ""; width: 6px; height: 6px; border-radius: 50%;
    background: #51B1FE; box-shadow: 0 0 0 3px rgba(81,177,254,0.15);
    animation: race-live 1.4s ease-in-out infinite;
  }
  @keyframes race-live {
    0%, 100% { box-shadow: 0 0 0 3px rgba(81,177,254,0.15); }
    50%      { box-shadow: 0 0 0 7px rgba(81,177,254,0.0); }
  }

  
  

  
  /* Soft {
    content: "";
    position: absolute; right: -40px; top: 50%; transform: translateY(-50%);
    width: 120px; height: 120px; border-radius: 50%;
    background: radial-gradient(circle, rgba(81,177,254,0.20), transparent 70%);
    animation: source-broadcast 2.4s ease-out infinite;
    pointer-events: none;
  }
  @keyframes source-broadcast {
    0%   { transform: translateY(-50%) scale(0.4); opacity: 0.8; }
    100% { transform: translateY(-50%) scale(1.4); opacity: 0;   }
  }
  
  .hub-vp-pipeline-source .role {
    font-family: 'Geist Mono', monospace; font-size: 9.5px;
    color: rgba(255,255,255,0.45);
    text-transform: uppercase; letter-spacing: 0.18em;
  }
  .hub-vp-pipeline-source .who {
    font-size: 22px; font-weight: 500; letter-spacing: -0.02em;
    color: var(--alabaster);
    line-height: 1.05;
    margin-bottom: 4px;
  }
  .hub-vp-pipeline-source .slot {
    font-family: 'Geist Mono', monospace; font-size: 11px;
    color: rgba(255,255,255,0.55);
    letter-spacing: 0.06em;
  }
  .hub-vp-pipeline-source .hash {
    margin-top: 10px;
    font-family: 'Geist Mono', monospace;
    font-size: 12.5px;
    color: #51B1FE;
    letter-spacing: 0.04em;
    word-break: break-all;
  }
  .hub-vp-pipeline-source .meta {
    margin-top: 14px; padding-top: 12px;
    border-top: 1px solid rgba(255,255,255,0.08);
    display: grid; grid-template-columns: 1fr 1fr; gap: 8px 16px;
  }
  .hub-vp-pipeline-source .meta .k {
    font-family: 'Geist Mono', monospace; font-size: 9px;
    color: rgba(255,255,255,0.45);
    text-transform: uppercase; letter-spacing: 0.14em;
  }
  .hub-vp-pipeline-source .meta .v {
    font-family: 'Geist Mono', monospace; font-size: 12.5px;
    color: var(--alabaster); margin-top: 2px;
  }

  
  
  .hub-vp-race-head .right { text-align: right; }
  
  
  .hub-vp-race-lane .pin {
    width: 6px; height: 6px; border-radius: 50%;
    justify-self: center;
    background: rgba(40,44,52,0.20);
    transition: background .2s ease, box-shadow .2s ease;
  }
  .hub-vp-race-lane.delivered .pin {
    background: #51B1FE;
    box-shadow: 0 0 0 3px rgba(81,177,254,0.16);
  }
  .hub-vp-race-lane .track {
    position: relative; height: 1px;
    background: linear-gradient(90deg, rgba(40,44,52,0.04), rgba(40,44,52,0.14) 50%, rgba(40,44,52,0.04));
  }
  /* Start cap on each lane */
  .hub-vp-race-lane .track::before {
    content: "";
    position: absolute; left: -2px; top: -3px;
    width: 1px; height: 7px;
    background: var(--ink-dim);
    opacity: 0.45;
  }
  .hub-vp-race-lane .packet {
    position: absolute; top: -3.5px; left: 0;
    width: 8px; height: 8px; border-radius: 50%;
    background: #51B1FE;
    box-shadow: 0 0 10px rgba(81,177,254,0.6);
    animation-name: shred-race;
    animation-iteration-count: infinite;
    animation-timing-function: cubic-bezier(0.35, 0.05, 0.55, 0.95);
    will-change: transform, opacity;
  }
  .hub-vp-race-lane.pending .packet {
    background: rgba(40,44,52,0.25);
    box-shadow: none;
  }
  /* Trailing comet — short trace behind the packet */
  .hub-vp-race-lane .packet::before {
    content: "";
    position: absolute; top: 50%; right: 100%;
    width: 22px; height: 1.5px;
    background: linear-gradient(90deg, transparent, rgba(81,177,254,0.7));
    transform: translateY(-50%);
    border-radius: 999px;
  }
  .hub-vp-race-lane.pending .packet::before { background: linear-gradient(90deg, transparent, rgba(40,44,52,0.18)); }
  @keyframes shred-race {
    0%   { left: 0;                  opacity: 0; }
    6%   {                           opacity: 1; }
    94%  {                           opacity: 1; }
    100% { left: calc(100% - 8px);   opacity: 0; }
  }
  .hub-vp-race-lane .city {
    font-family: 'Geist Mono', monospace;
    font-size: 11.5px;
    color: var(--ink);
    letter-spacing: 0.04em;
    text-transform: uppercase;
    text-align: right;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  .hub-vp-race-lane.pending .city { color: var(--ink-mute); }
  .hub-vp-race-lane .ms {
    font-family: 'Geist Mono', monospace;
    font-size: 11px;
    color: var(--ink-dim);
    text-align: right;
  }
  .hub-vp-race-lane.delivered .ms { color: var(--ink); }
  .hub-vp-race-lane.pending .ms { color: var(--ink-mute); }

  
  .hub-vp-pipeline-foot .progress {
    flex: 1; max-width: 340px;
    height: 4px; border-radius: 2px;
    background: rgba(40,44,52,0.08);
    position: relative; overflow: hidden;
  }
  .hub-vp-pipeline-foot .progress > i {
    position: absolute; inset: 0;
    background: linear-gradient(90deg, #51B1FE, #2074C9);
    transform-origin: left;
    transform: scaleX(0.68);
  }
  .hub-vp-pipeline-foot .note { color: var(--ink-mute); }

  
  .hub-vp-headline .main {
    display: flex; align-items: baseline; gap: 14px;
    flex-wrap: wrap;
  }
  .hub-vp-headline .eyebrow {
    font-family: 'Geist Mono', monospace;
    font-size: 10px; color: #2074C9;
    text-transform: uppercase; letter-spacing: 0.18em;
    display: inline-flex; align-items: center; gap: 7px;
    flex: 0 0 auto;
  }
  .hub-vp-headline .eyebrow::before {
    content: ""; width: 6px; height: 6px; border-radius: 50%;
    background: #51B1FE;
    box-shadow: 0 0 0 3px rgba(81,177,254,0.18);
  }
  .hub-vp-headline .delta {
    font-size: 21px; font-weight: 500; letter-spacing: -0.02em;
    color: var(--ink); line-height: 1.15;
  }
  .hub-vp-headline .delta em { font-style: normal; color: #2074C9; }
  .hub-vp-headline .meta {
    font-family: 'Geist Mono', monospace;
    font-size: 10.5px; color: var(--ink-mute);
    letter-spacing: 0.06em;
    text-align: right;
    flex: 0 0 auto;
  }
  @media (max-width: 760px) {
    
    .hub-vp-headline .meta { text-align: left; }
  }

  
  
  
  
  
  .hub-vp-perf-card .k {
    font-family: 'Geist Mono', monospace;
    font-size: 9.5px;
    color: var(--ink-dim);
    text-transform: uppercase; letter-spacing: 0.14em;
    margin-bottom: 4px;
  }
  .hub-vp-perf-card .v {
    color: var(--ink); font-size: 22px; font-weight: 560;
    line-height: 1.05; letter-spacing: -0.01em;
  }
  .hub-vp-perf-card .v .u {
    margin-left: 3px; font-size: 12px; color: var(--ink-dim); font-weight: 500;
  }
  .hub-vp-perf-card .d {
    margin-top: 4px;
    font-family: 'Geist Mono', monospace; font-size: 10px;
    color: var(--ink-mute); line-height: 1.35;
  }
  .hub-vp-perf-card.highlight {
    border-color: rgba(81,177,254,0.45);
    background: linear-gradient(180deg, rgba(81,177,254,0.06), rgba(81,177,254,0.02));
  }
  .hub-vp-perf-card.highlight .v { color: #2074C9; }
  
  

  
  
  
  .hub-vp-health-head .pulse {
    display: inline-flex; align-items: center; gap: 6px;
    color: var(--ink);
  }
  .hub-vp-health-head .pulse::before {
    content: ""; width: 6px; height: 6px; border-radius: 50%;
    background: var(--good); box-shadow: 0 0 8px rgba(100,226,127,0.6);
  }
  
  
  .hub-vp-health-card .k {
    font-family: 'Geist Mono', monospace;
    font-size: 10px; color: var(--ink-dim);
    letter-spacing: 0.08em;
  }
  .hub-vp-health-card .row {
    display: flex; align-items: baseline; gap: 8px;
  }
  .hub-vp-health-card .v {
    color: var(--ink); font-size: 22px; font-weight: 560;
    letter-spacing: -0.01em; line-height: 1.05;
  }
  .hub-vp-health-card .v .u { font-size: 12px; color: var(--ink-dim); margin-left: 2px; }
  .hub-vp-health-card .delta {
    font-family: 'Geist Mono', monospace; font-size: 10px;
    color: var(--ink-mute);
    letter-spacing: 0.02em;
  }
  .hub-vp-health-card .delta.warn { color: var(--warn); }
  .hub-vp-health-card .bar {
    height: 3px; border-radius: 999px; background: rgba(40,44,52,0.06);
    overflow: hidden; margin-top: 2px;
  }
  .hub-vp-health-card .bar > i {
    display: block; height: 100%;
    background: #51B1FE;
    opacity: 0.72;
    border-radius: 999px;
  }
  
  

  /* ─── Validator observability cockpit ─── */
  .hub-obs {
    display: flex;
    flex-direction: column;
    gap: 14px;
  }
  .hub-obs-contextbar {
    position: sticky;
    top: 0;
    z-index: 12;
    display: flex;
    align-items: center;
    gap: 7px;
    flex-wrap: nowrap;
    overflow-x: auto;
    scrollbar-width: none;
    padding: 8px;
    margin: -2px -4px 2px;
    border: 1px solid var(--line);
    border-radius: 13px;
    background: rgba(250,251,252,0.88);
    backdrop-filter: blur(18px) saturate(140%);
    -webkit-backdrop-filter: blur(18px) saturate(140%);
    box-shadow: 0 10px 28px rgba(40,44,52,0.07);
  }
  .hub-obs-contextbar::-webkit-scrollbar { display: none; }
  .hub-obs-context-group {
    min-width: 0;
    display: flex;
    align-items: center;
    gap: 3px;
    padding: 3px;
    border: 1px solid var(--line);
    border-radius: 9px;
    background: var(--bg-card-2);
  }
  .hub-obs-context-group.scroll {
    flex: 0 1 240px;
    max-width: min(100%, 240px);
    overflow-x: auto;
    scrollbar-width: none;
  }
  .hub-obs-context-group.scroll::-webkit-scrollbar { display: none; }
  .hub-obs-chip,
  .hub-obs-scope-chip {
    appearance: none;
    -webkit-appearance: none;
    flex: none;
    height: 28px;
    padding: 0 9px;
    border: 0;
    border-radius: 7px;
    background: transparent;
    color: var(--ink-mute);
    font-family: 'Geist Mono', monospace;
    font-size: 9.5px;
    text-transform: uppercase;
    letter-spacing: 0.11em;
    cursor: pointer;
    white-space: nowrap;
  }
  .hub-obs-chip.active,
  .hub-obs-scope-chip.active {
    background: var(--bg-card);
    color: var(--ink);
    box-shadow: 0 4px 12px rgba(40,44,52,0.08);
  }
  .hub-obs-chip.muted {
    color: var(--ink-dim);
    cursor: default;
  }
  .hub-obs-search {
    flex: 0 0 132px;
    min-width: 132px;
    height: 36px;
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 0 10px;
    border: 1px solid var(--line);
    border-radius: 10px;
    background: var(--bg-card);
  }
  .hub-obs-search span,
  .hub-obs-context-meta span {
    font-family: 'Geist Mono', monospace;
    font-size: 9px;
    color: var(--ink-dim);
    text-transform: uppercase;
    letter-spacing: 0.13em;
  }
  .hub-obs-search input {
    min-width: 0;
    width: 100%;
    border: 0;
    outline: none;
    background: transparent;
    color: var(--ink);
    font-family: 'Geist Mono', monospace;
    font-size: 11px;
  }
  .hub-obs-search:focus-within {
    border-color: rgba(81,177,254,0.55);
    box-shadow: 0 0 0 3px rgba(81,177,254,0.12);
  }
  .hub-obs-context-meta {
    height: 36px;
    display: flex;
    align-items: center;
    gap: 9px;
    padding: 0 10px;
    border: 1px solid var(--line);
    border-radius: 10px;
    background: var(--bg-card);
    font-family: 'Geist Mono', monospace;
    font-size: 9.8px;
    color: var(--ink);
    white-space: nowrap;
    flex: none;
  }
  .hub-obs-filterbar {
    position: sticky;
    top: 0;
    z-index: 12;
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(138px, 1fr));
    gap: 8px;
    align-items: end;
    padding: 10px;
    margin: -2px -4px 2px;
    border: 1px solid var(--line);
    border-radius: 14px;
    background: rgba(250,251,252,0.86);
    backdrop-filter: blur(18px) saturate(140%);
    -webkit-backdrop-filter: blur(18px) saturate(140%);
    box-shadow: 0 10px 28px rgba(40,44,52,0.07);
  }
  .hub-obs-filterbar label {
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 5px;
  }
  .hub-obs-filterbar label.search {
    min-width: 0;
  }
  .hub-obs-filterbar span,
  .hub-obs-updated span {
    font-family: 'Geist Mono', monospace;
    font-size: 9px;
    color: var(--ink-dim);
    text-transform: uppercase;
    letter-spacing: 0.13em;
  }
  .hub-obs-filterbar select,
  .hub-obs-filterbar input {
    width: 100%;
    height: 34px;
    border: 1px solid var(--line);
    border-radius: 8px;
    background: var(--bg-card);
    color: var(--ink);
    font-family: 'Geist Mono', monospace;
    font-size: 11px;
    padding: 0 10px;
    outline: none;
  }
  .hub-obs-filterbar select:focus,
  .hub-obs-filterbar input:focus {
    border-color: rgba(81,177,254,0.55);
    box-shadow: 0 0 0 3px rgba(81,177,254,0.12);
  }
  .hub-obs-refresh,
  .hub-obs-tabs button,
  .hub-obs-table-toolbar button,
  .hub-obs-table th button {
    appearance: none;
    -webkit-appearance: none;
    font-family: 'Geist Mono', monospace;
    cursor: pointer;
  }
  .hub-obs-refresh {
    width: 100%;
    height: 34px;
    padding: 0 13px;
    border: 1px solid var(--line-strong);
    border-radius: 8px;
    background: var(--ink);
    color: var(--bg-card);
    font-size: 10px;
    text-transform: uppercase;
    letter-spacing: 0.12em;
  }
  .hub-obs-contextbar .hub-obs-refresh {
    width: auto;
    height: 36px;
    border-radius: 10px;
    padding: 0 13px;
    flex: none;
  }
  .hub-obs-contextbar .hub-obs-updated {
    height: 36px;
    flex: none;
    width: 72px;
  }
  .hub-obs-updated {
    min-width: 0;
    height: 34px;
    display: flex;
    flex-direction: column;
    justify-content: center;
    gap: 2px;
    padding: 0 10px;
    border: 1px solid var(--line);
    border-radius: 8px;
    background: var(--bg-card-2);
  }
  .hub-obs-updated b {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-family: 'Geist Mono', monospace;
    font-size: 10.5px;
    color: var(--ink);
    font-weight: 500;
  }
  .hub-obs-tabs {
    display: inline-flex;
    width: max-content;
    max-width: 100%;
    gap: 3px;
    padding: 3px;
    border: 1px solid var(--line);
    border-radius: 10px;
    background: rgba(232,237,242,0.72);
  }
  .hub-obs-tabs button {
    height: 31px;
    padding: 0 16px;
    border: 0;
    border-radius: 7px;
    background: transparent;
    color: var(--ink-mute);
    font-size: 10.5px;
    text-transform: uppercase;
    letter-spacing: 0.12em;
  }
  .hub-obs-tabs button.active {
    background: var(--bg-card);
    color: var(--ink);
    box-shadow: 0 4px 12px rgba(40,44,52,0.08);
  }
  .hub-obs-tabs.hidden-tabs { display: none; }
  .hub-obs-hero {
    display: grid;
    grid-template-columns: minmax(0, 1.45fr) minmax(330px, .75fr);
    gap: 12px;
    align-items: stretch;
  }
  .hub-obs-hero-main {
    min-width: 0;
    border: 1px solid rgba(81,177,254,0.20);
    border-radius: 14px;
    background:
      radial-gradient(120% 120% at 0% 0%, rgba(81,177,254,0.12), transparent 64%),
      linear-gradient(180deg, rgba(255,255,255,0.84), rgba(238,242,245,0.72));
    padding: 20px;
  }
  .hub-obs-status {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    padding: 5px 10px;
    border-radius: 999px;
    font-family: 'Geist Mono', monospace;
    font-size: 10px;
    text-transform: uppercase;
    letter-spacing: 0.12em;
    border: 1px solid transparent;
  }
  .hub-obs-status::before {
    content: "";
    width: 6px;
    height: 6px;
    border-radius: 999px;
  }
  .hub-obs-status.good {
    color: #1F7A3A;
    background: rgba(100,226,127,0.12);
    border-color: rgba(100,226,127,0.32);
  }
  .hub-obs-status.good::before { background: var(--good); box-shadow: 0 0 8px rgba(100,226,127,0.6); }
  .hub-obs-status.warn {
    color: #B25F1A;
    background: rgba(254,161,88,0.12);
    border-color: rgba(254,161,88,0.34);
  }
  .hub-obs-status.warn::before { background: var(--warn); }
  .hub-obs-hero h2 {
    margin: 12px 0 8px;
    color: var(--ink);
    font-size: 30px;
    font-weight: 560;
    letter-spacing: -0.026em;
    line-height: 1.04;
    overflow-wrap: anywhere;
  }
  .hub-obs-hero p {
    margin: 0;
    color: var(--ink-mute);
    font-size: 14px;
    line-height: 1.55;
    max-width: 78ch;
  }
  .hub-obs-hero p b { color: #1F7A3A; font-weight: 600; }
  .hub-obs-hero p span { margin-left: 12px; }
  .hub-obs-hero-metrics {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 8px;
  }
  .hub-obs-hero-metrics div,
  .hub-obs-kpi-card,
  .hub-obs-chart,
  .hub-obs-percentiles,
  .hub-obs-compare,
  .hub-obs-outcomes,
  .hub-obs-table-panel {
    border: 1px solid var(--line);
    border-radius: 12px;
    background: var(--bg-card);
    box-shadow: 0 4px 16px rgba(40,44,52,0.035);
  }
  .hub-obs-hero-metrics div {
    min-width: 0;
    padding: 14px;
    display: flex;
    flex-direction: column;
    justify-content: center;
    gap: 6px;
    background: var(--bg-card-2);
  }
  .hub-obs-hero-metrics span,
  .hub-obs-kpi-card .k,
  .hub-obs-group-title,
  .hub-obs-panel-head span,
  .hub-obs-chart-head span,
  .hub-obs-table-toolbar span {
    font-family: 'Geist Mono', monospace;
    font-size: 9.5px;
    color: var(--ink-dim);
    text-transform: uppercase;
    letter-spacing: 0.13em;
  }
  .hub-obs-hero-metrics b {
    color: var(--ink);
    font-family: 'Geist Mono', monospace;
    font-size: 18px;
    font-weight: 560;
  }
  .hub-obs-kpi-groups {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 14px;
  }
  .hub-obs-kpi-group {
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 8px;
  }
  .hub-obs-kpi-grid {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 8px;
  }
  .hub-obs-kpi-card {
    min-width: 0;
    padding: 14px;
    display: flex;
    flex-direction: column;
    gap: 6px;
  }
  .hub-obs-kpi-card .v {
    min-height: 24px;
    color: var(--ink);
    font-family: 'Geist Mono', monospace;
    font-size: 19px;
    font-weight: 560;
    letter-spacing: -0.02em;
    line-height: 1.15;
    overflow-wrap: anywhere;
  }
  .hub-obs-kpi-card .d {
    color: var(--ink-mute);
    font-family: 'Geist Mono', monospace;
    font-size: 10px;
    line-height: 1.35;
  }
  .hub-obs-kpi-card.good .v { color: #1F7A3A; }
  .hub-obs-kpi-card.warn .v { color: #B25F1A; }
  .hub-obs-analytics {
    display: grid;
    grid-template-columns: minmax(0, 1.55fr) minmax(320px, .85fr);
    gap: 12px;
    align-items: stretch;
  }
  .hub-obs-chart,
  .hub-obs-percentiles,
  .hub-obs-compare,
  .hub-obs-outcomes {
    min-width: 0;
    padding: 16px;
  }
  .hub-obs-chart:first-child {
    grid-row: span 2;
  }
  .hub-obs-chart-head,
  .hub-obs-panel-head,
  .hub-obs-table-toolbar {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 12px;
    margin-bottom: 12px;
  }
  .hub-obs-chart-head div,
  .hub-obs-panel-head,
  .hub-obs-table-toolbar > div:first-child {
    display: flex;
    flex-direction: column;
    gap: 4px;
    min-width: 0;
  }
  .hub-obs-chart-head b,
  .hub-obs-panel-head b,
  .hub-obs-table-toolbar b {
    color: var(--ink);
    font-family: 'Geist Mono', monospace;
    font-size: 10.5px;
    font-weight: 500;
    letter-spacing: 0.04em;
  }
  .hub-obs-legend {
    display: inline-flex;
    align-items: center;
    gap: 12px;
    flex: none;
  }
  .hub-obs-legend span {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    font-family: 'Geist Mono', monospace;
    font-size: 10px;
    color: var(--ink-mute);
    text-transform: none;
    letter-spacing: 0.02em;
  }
  .hub-obs-legend span::before {
    content: "";
    width: 8px;
    height: 8px;
    border-radius: 50%;
  }
  .hub-perf-stack .op-race-legend i.mum { background: var(--sc-bar-mum); }
  .hub-perf-stack .op-race-legend i.lib { background: var(--sc-bar-lib); }
  .hub-obs-legend .mum::before { background: #51B1FE; }
  .hub-obs-legend .lib::before { background: #64E27F; }
  .hub-obs-chart svg {
    display: block;
    width: 100%;
    height: auto;
    overflow: visible;
  }
  .hub-obs-chart .grid {
    stroke: rgba(40,44,52,0.08);
    stroke-width: 1;
  }
  .hub-obs-chart .axis,
  .hub-obs-chart .axis-line {
    fill: var(--ink-dim);
    stroke: rgba(40,44,52,0.16);
    font-family: 'Geist Mono', monospace;
    font-size: 10px;
  }
  .hub-obs-chart .line {
    stroke-width: 2.6;
    stroke-linecap: round;
    stroke-linejoin: round;
    vector-effect: non-scaling-stroke;
  }
  .hub-obs-chart .line.mum { stroke: #51B1FE; }
  .hub-obs-chart .line.lib { stroke: #64E27F; }
  .hub-obs-chart .dot {
    cursor: crosshair;
    stroke: var(--bg-card);
    stroke-width: 1.5;
  }
  .hub-obs-chart .dot.mum { fill: #51B1FE; }
  .hub-obs-chart .dot.lib { fill: #64E27F; }
  .hub-obs-chart .pair-stem {
    stroke-width: 5.5;
    stroke-linecap: round;
    opacity: 0.82;
    vector-effect: non-scaling-stroke;
  }
  .hub-obs-chart .pair-stem.positive { stroke: #64E27F; }
  .hub-obs-chart .pair-stem.negative { stroke: #FEA158; }
  .hub-obs-chart .pair-stem.equal { stroke: var(--ink-dim); }
  .hub-obs-chart .pair-stem.na {
    stroke: var(--ink-faint);
    stroke-dasharray: 3 5;
    opacity: 0.48;
  }
  .hub-obs-chart .na-mark {
    fill: none;
    stroke: var(--ink-faint);
    stroke-width: 1.4;
    vector-effect: non-scaling-stroke;
  }
  .hub-obs-chart .latest-line {
    stroke: var(--ink-dim);
    stroke-dasharray: 5 6;
    stroke-width: 1.1;
  }
  .hub-obs-chart .zero-line {
    stroke: rgba(40,44,52,0.30);
    stroke-width: 1.2;
  }
  .hub-obs-chart .bar {
    cursor: crosshair;
  }
  .hub-obs-chart .bar.positive { fill: #64E27F; }
  .hub-obs-chart .bar.negative { fill: #FEA158; }
  .hub-obs-chart .bar.na { fill: var(--ink-faint); opacity: 0.45; }
  .hub-obs-chart .svg-tip rect {
    fill: rgba(31,34,41,0.92);
    stroke: rgba(255,255,255,0.14);
  }
  .hub-obs-chart .svg-tip text {
    fill: #FFFBFB;
    font-family: 'Geist Mono', monospace;
    font-size: 10px;
  }
  .hub-obs-hover {
    margin-top: 8px;
    font-family: 'Geist Mono', monospace;
    font-size: 10.5px;
    color: var(--ink-mute);
  }
  .hub-obs-hover.faster { color: #1F7A3A; }
  .hub-obs-hover.slower { color: #B25F1A; }
  .hub-obs-outcome-hero {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 8px;
    margin-bottom: 10px;
  }
  .hub-obs-outcome-hero div {
    padding: 12px;
    border: 1px solid var(--line);
    border-radius: 10px;
    background: var(--bg-card-2);
  }
  .hub-obs-outcome-hero span,
  .hub-obs-outcome-legend span {
    font-family: 'Geist Mono', monospace;
    font-size: 10px;
    color: var(--ink-mute);
  }
  .hub-obs-outcome-hero strong {
    display: block;
    margin-top: 6px;
    color: var(--ink);
    font-family: 'Geist Mono', monospace;
    font-size: 20px;
    font-weight: 560;
  }
  .hub-obs-outcome-bar {
    display: flex;
    height: 14px;
    overflow: hidden;
    border-radius: 999px;
    background: rgba(40,44,52,0.06);
    border: 1px solid var(--line);
  }
  .hub-obs-outcome-bar i {
    display: block;
    min-width: 2px;
    height: 100%;
  }
  .hub-obs-outcome-bar .faster { background: #64E27F; }
  .hub-obs-outcome-bar .slower { background: #FEA158; }
  .hub-obs-outcome-bar .equal { background: var(--ink-dim); }
  .hub-obs-outcome-bar .na { background: rgba(180,68,68,0.55); }
  .hub-obs-outcome-legend {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 8px;
    margin-top: 10px;
  }
  .hub-obs-outcome-legend span {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    min-width: 0;
  }
  .hub-obs-outcome-legend span::before {
    content: "";
    width: 8px;
    height: 8px;
    border-radius: 50%;
    flex: none;
  }
  .hub-obs-outcome-legend .faster::before { background: #64E27F; }
  .hub-obs-outcome-legend .slower::before { background: #FEA158; }
  .hub-obs-outcome-legend .equal::before { background: var(--ink-dim); }
  .hub-obs-outcome-legend .na::before { background: rgba(180,68,68,0.65); }
  .hub-obs-runway {
    grid-column: 1 / -1;
    min-width: 0;
    padding: 16px;
    border: 1px solid var(--line);
    border-radius: 12px;
    background: var(--bg-card);
    box-shadow: 0 4px 16px rgba(40,44,52,0.035);
  }
  .hub-obs-runway-layout {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 286px;
    gap: 14px;
    align-items: stretch;
  }
  .hub-obs-runway-chart {
    min-width: 0;
    border: 1px solid var(--line);
    border-radius: 10px;
    background:
      linear-gradient(180deg, rgba(100,226,127,0.055), transparent 49%),
      linear-gradient(0deg, rgba(254,161,88,0.055), transparent 48%),
      var(--bg-card-2);
    padding: 12px;
  }
  .hub-obs-runway-svg {
    display: block;
    width: 100%;
    height: auto;
    overflow: visible;
  }
  .hub-obs-runway .grid-line {
    stroke: rgba(40,44,52,0.08);
    stroke-width: 1;
  }
  .hub-obs-runway .zero-line {
    stroke: rgba(40,44,52,0.36);
    stroke-width: 1.4;
    vector-effect: non-scaling-stroke;
  }
  .hub-obs-runway .baseline-label,
  .hub-obs-runway .slot-label,
  .hub-obs-runway .axis-label {
    fill: var(--ink-dim);
    font-family: 'Geist Mono', monospace;
    font-size: 10px;
  }
  .hub-obs-runway .win-zone {
    fill: rgba(100,226,127,0.075);
  }
  .hub-obs-runway .loss-zone {
    fill: rgba(254,161,88,0.075);
  }
  .hub-obs-runway .slot-bar {
    cursor: pointer;
    transition: opacity .15s ease;
    vector-effect: non-scaling-stroke;
  }
  .hub-obs-runway .slot-bar:hover {
    opacity: 0.76;
  }
  .hub-obs-runway .slot-bar.faster { fill: #64E27F; }
  .hub-obs-runway .slot-bar.slower { fill: #FEA158; }
  .hub-obs-runway .slot-bar.equal { fill: #9DB0C2; }
  .hub-obs-runway .slot-bar.na { fill: #CFA2A2; opacity: 0.46; }
  .hub-obs-runway .slot-bar.selected {
    stroke: #51B1FE;
    stroke-width: 2;
  }
  .hub-obs-runway .latest-rail {
    stroke: #51B1FE;
    stroke-width: 1.2;
    stroke-dasharray: 4 5;
    opacity: 0.7;
    vector-effect: non-scaling-stroke;
  }
  .hub-obs-runway-band-label {
    display: flex;
    justify-content: space-between;
    gap: 12px;
    margin-top: 7px;
    color: var(--ink-mute);
    font-family: 'Geist Mono', monospace;
    font-size: 10px;
  }
  .hub-obs-runway-detail {
    border: 1px solid var(--line);
    border-radius: 10px;
    background: var(--bg-card-2);
    padding: 14px;
    display: flex;
    flex-direction: column;
    gap: 12px;
  }
  .hub-obs-runway-detail span,
  .hub-obs-runway-detail dt {
    font-family: 'Geist Mono', monospace;
    font-size: 9px;
    color: var(--ink-dim);
    text-transform: uppercase;
    letter-spacing: 0.12em;
  }
  .hub-obs-runway-result {
    width: max-content;
    max-width: 100%;
    padding: 5px 9px;
    border-radius: 999px;
    font-family: 'Geist Mono', monospace;
    font-size: 9.5px;
    text-transform: uppercase;
    letter-spacing: 0.1em;
  }
  .hub-obs-runway-result.faster {
    color: #1F7A3A;
    background: rgba(100,226,127,0.14);
  }
  .hub-obs-runway-result.slower {
    color: #B25F1A;
    background: rgba(254,161,88,0.16);
  }
  .hub-obs-runway-result.equal,
  .hub-obs-runway-result.na {
    color: var(--ink-mute);
    background: rgba(157,176,194,0.16);
  }
  .hub-obs-runway-detail .adv {
    color: var(--ink);
    font-family: 'Geist Mono', monospace;
    font-size: 38px;
    line-height: 1;
    font-weight: 560;
  }
  .hub-obs-runway-detail .adv.faster { color: #1F7A3A; }
  .hub-obs-runway-detail .adv.slower { color: #B25F1A; }
  .hub-obs-runway-detail p {
    margin: 0;
    color: var(--ink-mute);
    font-size: 12.5px;
    line-height: 1.45;
  }
  .hub-obs-runway-detail dl {
    display: grid;
    gap: 8px;
    margin: 0;
    padding-top: 4px;
    border-top: 1px solid var(--line);
  }
  .hub-obs-runway-detail div {
    display: flex;
    justify-content: space-between;
    gap: 12px;
    align-items: baseline;
  }
  .hub-obs-runway-detail dd {
    margin: 0;
    color: var(--ink);
    font-family: 'Geist Mono', monospace;
    font-size: 11px;
    font-weight: 500;
    text-align: right;
  }
  .hub-obs-runway-legend {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    margin-top: 10px;
    font-family: 'Geist Mono', monospace;
    font-size: 10px;
    color: var(--ink-mute);
  }
  .hub-obs-runway-legend span {
    display: inline-flex;
    align-items: center;
    gap: 6px;
  }
  .hub-obs-runway-legend span::before {
    content: "";
    width: 9px;
    height: 9px;
    border-radius: 999px;
  }
  .hub-obs-runway-legend .faster::before { background: #64E27F; }
  .hub-obs-runway-legend .slower::before { background: #FEA158; }
  .hub-obs-runway-legend .na::before { background: rgba(207,162,162,0.72); }
  .hub-obs-feed {
    grid-column: 1 / -1;
    min-width: 0;
    padding: 16px;
    border: 1px solid var(--line);
    border-radius: 12px;
    background: var(--bg-card);
    box-shadow: 0 4px 16px rgba(40,44,52,0.035);
  }
  .hub-obs-feed-list {
    display: grid;
    gap: 7px;
    max-height: 430px;
    overflow: auto;
    padding-right: 2px;
  }
  .hub-obs-feed-row {
    display: grid;
    grid-template-columns: 96px minmax(0, 1fr) minmax(0, 1fr) 92px 76px;
    gap: 10px;
    align-items: center;
    padding: 10px;
    border: 1px solid var(--line);
    border-radius: 10px;
    background: var(--bg-card-2);
    font-family: 'Geist Mono', monospace;
    font-size: 10px;
    color: var(--ink-mute);
  }
  .hub-obs-feed-row.latest {
    border-color: rgba(81,177,254,0.42);
    background: rgba(81,177,254,0.08);
  }
  .hub-obs-feed-row b {
    display: block;
    color: var(--ink);
    font-size: 12px;
    font-weight: 560;
  }
  .hub-obs-feed-lane {
    min-width: 0;
    display: grid;
    grid-template-columns: 54px minmax(0, 1fr) 58px;
    gap: 8px;
    align-items: center;
  }
  .hub-obs-feed-lane i {
    display: block;
    height: 7px;
    border-radius: 999px;
    background: rgba(40,44,52,0.08);
    overflow: hidden;
  }
  .hub-obs-feed-lane i::before {
    content: "";
    display: block;
    width: var(--w, 0%);
    height: 100%;
    border-radius: 999px;
  }
  .hub-obs-feed-lane.mum i::before { background: #51B1FE; }
  .hub-obs-feed-lane.lib i::before { background: #64E27F; }
  .hub-obs-analytics.hub-obs-compare-grid {
    grid-template-columns: minmax(0, 1.35fr) minmax(330px, .75fr);
  }
  .hub-obs-analytics.hub-obs-advantage-grid {
    grid-template-columns: minmax(0, 1.2fr) minmax(340px, .8fr);
  }
  .hub-obs-analytics.hub-obs-slot-grid {
    grid-template-columns: 1fr;
  }
  .hub-obs-analytics .hub-obs-chart.wide,
  .hub-obs-analytics .hub-obs-table-panel.wide {
    grid-column: 1 / -1;
  }
  .hub-obs-compare-grid .hub-obs-chart:first-child,
  .hub-obs-advantage-grid .hub-obs-chart:first-child,
  .hub-obs-slot-grid .hub-obs-chart:first-child {
    grid-row: auto;
  }
  .hub-obs-compare-grid .hub-obs-percentiles,
  .hub-obs-advantage-grid .hub-obs-chart:last-child {
    grid-column: 1 / -1;
  }
  .hub-obs-percentile-row {
    display: grid;
    grid-template-columns: 112px 1fr;
    gap: 12px;
    align-items: center;
    padding: 10px 0;
    border-top: 1px solid var(--line);
  }
  .hub-obs-percentile-row .label {
    color: var(--ink);
    font-family: 'Geist Mono', monospace;
    font-size: 11px;
    font-weight: 500;
  }
  .hub-obs-percentile-row .bands {
    display: grid;
    gap: 7px;
  }
  .hub-obs-percentile-row .band {
    display: grid;
    grid-template-columns: 34px 1fr 64px;
    gap: 8px;
    align-items: center;
    font-family: 'Geist Mono', monospace;
    font-size: 10px;
    color: var(--ink-mute);
  }
  .hub-obs-percentile-row .band i {
    display: block;
    height: 5px;
    border-radius: 999px;
    background: linear-gradient(90deg, #51B1FE, #64E27F);
    min-width: 3px;
  }
  .hub-obs-percentile-row .band b {
    color: var(--ink);
    font-weight: 500;
    text-align: right;
  }
  .hub-obs-compare-stat {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 8px;
    margin-bottom: 8px;
  }
  .hub-obs-compare-stat div {
    padding: 12px;
    border: 1px solid var(--line);
    border-radius: 10px;
    background: var(--bg-card-2);
  }
  .hub-obs-compare-stat span,
  .hub-obs-extremes span,
  .hub-obs-compare-row span {
    font-family: 'Geist Mono', monospace;
    font-size: 10px;
    color: var(--ink-mute);
  }
  .hub-obs-compare-stat strong {
    display: block;
    margin-top: 6px;
    color: var(--ink);
    font-family: 'Geist Mono', monospace;
    font-size: 18px;
    font-weight: 560;
  }
  .hub-obs-extremes {
    display: flex;
    flex-wrap: wrap;
    gap: 7px;
    margin-bottom: 8px;
  }
  .hub-obs-extremes span {
    padding: 5px 8px;
    border-radius: 999px;
    background: rgba(81,177,254,0.10);
    color: var(--ink);
  }
  .hub-obs-compare-list {
    border-top: 1px solid var(--line);
  }
  .hub-obs-compare-row {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto auto;
    gap: 8px;
    align-items: center;
    padding: 10px 0;
    border-bottom: 1px solid var(--line);
  }
  .hub-obs-compare-row:last-child { border-bottom: 0; }
  .hub-obs-compare-row b {
    color: var(--ink);
    font-family: 'Geist Mono', monospace;
    font-size: 11px;
    font-weight: 500;
    text-align: right;
  }
  .hub-obs-compare-row em {
    grid-column: auto;
    justify-self: end;
    width: max-content;
    max-width: 100%;
    padding: 4px 7px;
    border-radius: 999px;
    font-family: 'Geist Mono', monospace;
    font-size: 9.5px;
    font-style: normal;
  }
  .hub-obs-compare-row em.good {
    color: #1F7A3A;
    background: rgba(100,226,127,0.13);
  }
  .hub-obs-compare-row em.warn {
    color: #B25F1A;
    background: rgba(254,161,88,0.14);
  }
  .hub-obs-compare-row em.neutral {
    color: var(--ink-mute);
    background: rgba(40,44,52,0.06);
  }
  .hub-obs-table-panel {
    padding: 16px;
    overflow: hidden;
  }
  .hub-obs-table-toolbar {
    margin-bottom: 10px;
  }
  .hub-obs-table-toolbar .actions {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    justify-content: flex-end;
  }
  .hub-obs-table-toolbar button {
    height: 29px;
    padding: 0 10px;
    border: 1px solid var(--line);
    border-radius: 999px;
    background: var(--bg-card-2);
    color: var(--ink-mute);
    font-size: 9.5px;
    text-transform: uppercase;
    letter-spacing: 0.11em;
  }
  .hub-obs-table-toolbar button.active {
    background: var(--ink);
    border-color: var(--ink);
    color: var(--bg-card);
  }
  .hub-obs-table-wrap {
    overflow-x: auto;
    border: 1px solid var(--line);
    border-radius: 10px;
  }
  .hub-obs-table {
    width: 100%;
    min-width: 980px;
    border-collapse: collapse;
    font-family: 'Geist Mono', monospace;
    font-size: 11px;
  }
  .hub-obs-table th,
  .hub-obs-table td {
    padding: 11px 12px;
    text-align: left;
    border-bottom: 1px solid var(--line);
    white-space: nowrap;
  }
  .hub-obs-table-wrap.compact th,
  .hub-obs-table-wrap.compact td {
    padding-top: 7px;
    padding-bottom: 7px;
  }
  .hub-obs-table th {
    position: sticky;
    top: 0;
    z-index: 2;
    background: var(--bg-card-2);
    color: var(--ink-dim);
    font-size: 9px;
    text-transform: uppercase;
    letter-spacing: 0.12em;
  }
  .hub-obs-table th button {
    border: 0;
    background: transparent;
    color: inherit;
    padding: 0;
    text-align: left;
    text-transform: inherit;
    letter-spacing: inherit;
    font-size: inherit;
  }
  .hub-obs-table tbody tr {
    background: var(--bg-card);
  }
  .hub-obs-table tbody tr.latest {
    background: rgba(81,177,254,0.08);
  }
  .hub-obs-table tbody tr:hover {
    background: rgba(81,177,254,0.06);
  }
  .hub-obs-table td.faster,
  .hub-obs-table td.equal { color: #1F7A3A; }
  .hub-obs-table td.slower { color: #B25F1A; }
  .hub-obs-table td.na { color: var(--ink-dim); }
  .hub-obs-result {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 58px;
    padding: 4px 8px;
    border-radius: 999px;
    font-family: 'Geist Mono', monospace;
    font-size: 9.5px;
    text-transform: uppercase;
    letter-spacing: 0.08em;
  }
  .hub-obs-result.faster {
    color: #1F7A3A;
    background: rgba(100,226,127,0.15);
  }
  .hub-obs-result.slower {
    color: #B25F1A;
    background: rgba(254,161,88,0.16);
  }
  .hub-obs-result.equal {
    color: var(--ink-mute);
    background: rgba(40,44,52,0.06);
  }
  .hub-obs-result.na {
    color: #B44444;
    background: rgba(255,108,108,0.10);
  }
  .hub-obs-empty {
    display: grid;
    place-items: center;
    min-height: 180px;
    padding: 20px;
    color: var(--ink-mute);
    font-family: 'Geist Mono', monospace;
    font-size: 11px;
    line-height: 1.5;
    text-align: center;
    border: 1px dashed var(--line-strong);
    border-radius: 10px;
    background: var(--bg-card-2);
  }
  .hub-obs-skeleton {
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: 10px;
  }
  .hub-obs-skeleton div {
    height: 90px;
    border-radius: 10px;
    background: linear-gradient(90deg, var(--bg-card), var(--bg-card-2), var(--bg-card));
    background-size: 220% 100%;
    animation: hubObsPulse 1.4s ease-in-out infinite;
  }
  @keyframes hubObsPulse {
    0% { background-position: 0% 50%; }
    100% { background-position: -220% 50%; }
  }

  .hub-root[data-theme="dark"] .hub-obs-filterbar {
    background: rgba(35,39,48,0.86);
    box-shadow: 0 10px 28px rgba(0,0,0,0.25);
  }
  .hub-root[data-theme="dark"] .hub-obs-contextbar {
    background: rgba(35,39,48,0.88);
    box-shadow: 0 10px 28px rgba(0,0,0,0.25);
  }
  .hub-root[data-theme="dark"] .hub-obs-hero-main {
    background:
      radial-gradient(120% 120% at 0% 0%, rgba(81,177,254,0.12), transparent 64%),
      linear-gradient(180deg, rgba(42,46,55,0.88), rgba(31,34,41,0.76));
  }
  .hub-root[data-theme="dark"] .hub-obs-tabs {
    background: rgba(31,34,41,0.74);
  }
  .hub-root[data-theme="dark"] .hub-obs-chart .grid {
    stroke: rgba(255,251,251,0.07);
  }
  .hub-root[data-theme="dark"] .hub-obs-chart .axis-line,
  .hub-root[data-theme="dark"] .hub-obs-chart .zero-line {
    stroke: rgba(255,251,251,0.16);
  }
  .hub-root[data-theme="dark"] .hub-obs-outcome-bar {
    background: rgba(255,251,251,0.06);
  }
  .hub-root[data-theme="dark"] .hub-obs-runway-chart {
    background:
      linear-gradient(180deg, rgba(100,226,127,0.07), transparent 49%),
      linear-gradient(0deg, rgba(254,161,88,0.06), transparent 48%),
      rgba(31,34,41,0.72);
  }
  .hub-root[data-theme="dark"] .hub-obs-runway .grid-line {
    stroke: rgba(255,251,251,0.08);
  }
  .hub-root[data-theme="dark"] .hub-obs-runway .zero-line {
    stroke: rgba(255,251,251,0.24);
  }

  @media (max-width: 1240px) {
    .hub-obs-filterbar {
      grid-template-columns: repeat(3, minmax(0, 1fr));
    }
    .hub-obs-updated { min-width: 0; }
    .hub-obs-hero,
    .hub-obs-analytics {
      grid-template-columns: 1fr;
    }
    .hub-obs-chart:first-child { grid-row: auto; }
    .hub-obs-runway-layout {
      grid-template-columns: 1fr;
    }
  }
  @media (max-width: 980px) {
    .hub-obs-kpi-groups,
    .hub-obs-kpi-grid {
      grid-template-columns: 1fr;
    }
    .hub-obs-hero-metrics {
      grid-template-columns: repeat(3, minmax(0, 1fr));
    }
    .hub-obs-feed-row {
      grid-template-columns: 90px minmax(0, 1fr) 92px;
    }
    .hub-obs-feed-row .hub-obs-feed-lane {
      grid-column: 2 / -1;
    }
  }
  @media (max-width: 640px) {
    .hub-obs-contextbar {
      position: static;
    }
    .hub-obs-context-group.scroll,
    .hub-obs-search {
      flex-basis: 100%;
    }
    .hub-obs-filterbar {
      grid-template-columns: 1fr;
      position: static;
    }
    .hub-obs-tabs {
      width: 100%;
    }
    .hub-obs-tabs button {
      flex: 1;
      padding-left: 8px;
      padding-right: 8px;
    }
    .hub-obs-hero h2 {
      font-size: 24px;
    }
    .hub-obs-hero p span {
      display: block;
      margin-left: 0;
      margin-top: 4px;
    }
    .hub-obs-hero-metrics,
    .hub-obs-compare-stat,
    .hub-obs-outcome-hero,
    .hub-obs-percentile-row {
      grid-template-columns: 1fr;
    }
    .hub-obs-feed-row {
      grid-template-columns: 1fr;
    }
    .hub-obs-feed-row .hub-obs-feed-lane {
      grid-column: auto;
    }
  }

  .hub-validator-overview {
    border: 1px solid var(--line);
    border-radius: 18px;
    background: var(--bg-card);
    padding: 18px;
    box-shadow: 0 4px 16px rgba(40,44,52,0.04);
  }
  .hub-validator-overview-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 14px;
    margin-bottom: 14px;
    font-family: 'Geist Mono', monospace;
    font-size: 10.5px;
    color: var(--ink-dim);
    text-transform: uppercase;
    letter-spacing: 0.16em;
  }
  .hub-validator-overview-head b {
    color: var(--ink);
    font-weight: 500;
  }
  .hub-validator-overview-grid {
    display: grid;
    grid-template-columns: repeat(6, minmax(0, 1fr));
    gap: 10px;
  }
  .hub-validator-overview-card {
    min-width: 0;
    padding: 13px 14px;
    border: 1px solid var(--line);
    border-radius: 12px;
    background: var(--bg-card-2);
  }
  .hub-validator-overview-card .k {
    font-family: 'Geist Mono', monospace;
    font-size: 9.5px;
    color: var(--ink-dim);
    text-transform: uppercase;
    letter-spacing: 0.14em;
    margin-bottom: 8px;
  }
  .hub-validator-overview-card .v {
    color: var(--ink);
    font-size: 22px;
    font-weight: 560;
    line-height: 1.04;
    letter-spacing: -0.01em;
    overflow-wrap: anywhere;
  }
  .hub-validator-overview-card .d {
    margin-top: 6px;
    font-family: 'Geist Mono', monospace;
    font-size: 10px;
    color: var(--ink-mute);
    line-height: 1.35;
  }
  

  .hub-report-panel {
    margin-top: 0;
    border: 1px solid var(--line);
    border-radius: 16px;
    background: var(--bg-card);
    padding: 16px 18px;
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    gap: 16px;
    align-items: center;
    box-shadow: 0 4px 16px rgba(40,44,52,0.04);
  }
  .hub-report-label {
    font-family: 'Geist Mono', monospace;
    font-size: 10px;
    color: var(--ink-dim);
    text-transform: uppercase;
    letter-spacing: 0.16em;
    margin-bottom: 6px;
  }
  .hub-report-title {
    font-size: 16px;
    font-weight: 500;
    letter-spacing: -0.01em;
    margin: 0;
    color: var(--ink);
  }
  .hub-report-meta {
    font-family: 'Geist Mono', monospace;
    font-size: 11px;
    color: var(--ink-mute);
    line-height: 1.45;
  }
  .hub-report-actions {
    display: grid;
    grid-template-columns: repeat(3, minmax(128px, 1fr));
    gap: 10px;
  }
  .hub-report-btn {
    appearance: none;
    border: 1px solid var(--line);
    border-radius: 12px;
    background: var(--bg-card-2);
    color: var(--ink);
    padding: 10px 12px;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    cursor: pointer;
    font-family: inherit;
    text-align: left;
    transition: border-color .15s, background .15s, transform .15s;
  }
  .hub-report-btn:hover {
    border-color: var(--line-strong);
    background: var(--bg-card);
    transform: translateY(-1px);
  }
  .hub-report-btn span {
    display: block;
    font-size: 13px;
    font-weight: 500;
  }
  .hub-report-btn small {
    display: block;
    margin-top: 2px;
    font-family: 'Geist Mono', monospace;
    font-size: 10px;
    color: var(--ink-mute);
    letter-spacing: 0.04em;
  }
  .hub-report-btn svg {
    flex: none;
    color: var(--ink-mute);
  }
  .hub-partner-name .meta-id {
    font-family: 'Geist Mono', monospace;
    font-size: 10.5px; color: var(--ink-dim);
    font-weight: 400;
    display: block; margin-top: 1px;
  }

  .hub-status {
    display: inline-flex; align-items: center; gap: 7px;
    font-family: 'Geist Mono', monospace; font-size: 11px;
    color: var(--ink); text-transform: uppercase; letter-spacing: 0.1em;
    padding: 5px 10px;
    border-radius: 999px;
    border: 1px solid var(--line);
    background: rgba(100,226,127,0.12);
    white-space: nowrap;
  }
  .hub-status .sd { width: 6px; height: 6px; border-radius: 50%; background: var(--good); box-shadow: 0 0 6px rgba(100,226,127,0.55); }
  .hub-status.onboarding { color: #B85F1E; border-color: rgba(254,161,88,0.40); background: rgba(254,161,88,0.12); }
  .hub-status.onboarding .sd { background: var(--warn); box-shadow: 0 0 6px rgba(254,161,88,0.55); }

  .hub-staked {
    display: flex; align-items: center; gap: 10px;
    font-family: 'Geist Mono', monospace;
    font-size: 12px; color: var(--ink);
  }
  .hub-staked .bar {
    flex: 1; max-width: 110px;
    height: 4px; border-radius: 2px;
    background: rgba(40,44,52,0.06);
    overflow: hidden;
  }
  .hub-staked .bar > i {
    display: block; height: 100%;
    background: linear-gradient(90deg, var(--platinum) 0%, var(--carbon) 60%, var(--midnight) 100%);
    border-radius: 2px;
  }
  .hub-staked .amts { display: flex; flex-direction: column; line-height: 1.15; min-width: 0; }
  .hub-staked .amt { white-space: nowrap; font-weight: 500; }
  .hub-staked .amt .eth { color: var(--ink-dim); margin-left: 2px; font-size: 11px; }
  .hub-staked .amt-usd {
    white-space: nowrap;
    font-size: 10.5px;
    color: var(--ink-dim);
    font-weight: 400;
    margin-top: 1px;
  }

  .hub-nodes {
    font-family: 'Geist Mono', monospace; font-size: 12px; color: var(--ink);
  }
  .hub-vcount {
    display: flex; flex-direction: column; gap: 1px;
    font-family: 'Geist Mono', monospace;
    line-height: 1.15;
  }
  .hub-vcount .vc-main {
    font-size: 13px;
    color: var(--ink);
    font-weight: 500;
    font-variant-numeric: tabular-nums;
    letter-spacing: 0;
  }
  .hub-vcount .vc-sub {
    font-size: 10.5px;
    color: var(--ink-dim);
    font-weight: 400;
    letter-spacing: 0.02em;
  }
  .hub-health {
    display: flex; flex-direction: column; gap: 3px;
    line-height: 1.2;
  }
  .hub-health .lbl {
    display: inline-flex; align-items: center; gap: 6px;
    font-family: 'Geist Mono', monospace; font-size: 11px;
    color: #2A8847;
    text-transform: uppercase; letter-spacing: 0.10em;
    font-weight: 500;
  }
  .hub-health .lbl::before {
    content: ""; width: 6px; height: 6px; border-radius: 50%;
    background: var(--good); box-shadow: 0 0 6px rgba(100,226,127,0.5);
  }
  .hub-health.degraded .lbl {
    color: #C76A2A;
  }
  .hub-health.degraded .lbl::before {
    background: var(--warn); box-shadow: 0 0 6px rgba(254,161,88,0.5);
  }
  .hub-health .sub {
    font-family: 'Geist Mono', monospace; font-size: 10.5px;
    color: var(--ink-dim);
    letter-spacing: 0.04em;
  }
  .hub-activity {
    font-family: 'Geist Mono', monospace; font-size: 11.5px; color: var(--ink-mute);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  .hub-activity .ago { color: var(--ink-dim); margin-right: 6px; }

  /* ─── Utility rail ─── */
  .hub-tools {
    display: grid;
    grid-template-columns: repeat(6, 1fr);
    gap: 12px;
  }
  .hub-tool {
    position: relative;
    border: 1px solid var(--line);
    border-radius: 14px;
    background: var(--bg-card);
    backdrop-filter: blur(10px);
    padding: 16px 16px;
    text-decoration: none; color: var(--ink);
    display: flex; flex-direction: column; gap: 6px;
    overflow: hidden;
    transition: transform .25s, border-color .25s, box-shadow .25s;
    box-shadow: 0 2px 8px rgba(40,44,52,0.03);
  }
  .hub-tool:hover { transform: translateY(-2px); border-color: var(--line-strong); box-shadow: 0 8px 24px rgba(40,44,52,0.08); }
  .hub-tool .label {
    font-family: 'Geist Mono', monospace;
    font-size: 10px; color: var(--ink-dim);
    text-transform: uppercase; letter-spacing: 0.14em;
  }
  .hub-tool .name {
    font-size: 14.5px; font-weight: 500; letter-spacing: -0.01em;
    display: flex; align-items: center; justify-content: space-between;
  }
  .hub-tool .name .arr {
    width: 22px; height: 22px; border-radius: 50%;
    border: 1px solid var(--line-strong);
    background: rgba(40,44,52,0.04);
    display: grid; place-items: center;
    transition: background .2s, color .2s, transform .2s;
    color: var(--ink-mute);
  }
  .hub-tool:hover .name .arr {
    background: linear-gradient(180deg, var(--midnight), var(--carbon));
    color: var(--alabaster);
    transform: translateX(2px);
  }
  .hub-tool.disabled { opacity: 0.55; pointer-events: none; }
  .hub-tool .desc {
    font-size: 11.5px; color: var(--ink-mute); line-height: 1.4;
    margin-top: 2px;
  }
  .hub-tool .soon {
    font-family: 'Geist Mono', monospace;
    font-size: 9.5px; color: var(--ink-dim);
    text-transform: uppercase; letter-spacing: 0.16em;
    background: rgba(40,44,52,0.04);
    padding: 3px 7px; border-radius: 5px;
    border: 1px solid var(--line);
    align-self: flex-start;
    margin-top: 6px;
  }

  /* ─── Footer ─── */
  .hub-foot {
    margin-top: 56px;
    padding-top: 22px;
    border-top: 1px solid var(--line);
    display: flex; align-items: center; justify-content: space-between;
    font-family: 'Geist Mono', monospace; font-size: 11.5px; color: var(--ink-dim);
  }
  .hub-foot .mark {
    width: 24px; height: 14px;
    -webkit-mask: url("dashboard/assets/optimum-logomark-white-sm.png") center/contain no-repeat;
            mask: url("dashboard/assets/optimum-logomark-white-sm.png") center/contain no-repeat;
    background: var(--ink);
    opacity: 0.55;
    display: inline-block; margin-right: 8px; vertical-align: -2px;
  }
  .hub-foot-links { display: flex; gap: 22px; }
  .hub-foot-links a { color: var(--ink-mute); text-decoration: none; }
  .hub-foot-links a:hover { color: var(--ink); }

  /* ─── Responsive ─── */
  @media (max-width: 1180px) {
    .hub-nav-links a,
    .hub-nav-trigger { padding: 8px 10px; font-size: 12.5px; }
  }
  @media (max-width: 1080px) {
    .hub-kpi-strip { grid-template-columns: repeat(3, 1fr); }
    .hub-tools { grid-template-columns: repeat(3, 1fr); }
    .hub-hero { flex-wrap: wrap; gap: 22px; }
    
    .hub-hero-meta { width: 100%; }
    .hub-metrics-panel { padding: 22px; }
    .hub-globe-metrics-grid { gap: 10px; }
    /* nav: collapse links to burger here (status pill still fits) */
    .hub-nav-links { display: none; }
    .hub-burger { display: inline-flex; }
    /* Mobile Explore menu — compact grouped list. One surface, hairline row
       separators, an inset well for the open category, Azure only on the open
       chevron and focus ring. Accordion reveal animates grid rows 0fr→1fr inside
       the dropdown, so the page underneath never reflows. */
    .hub-mobile-menu.open {
      display: block;
      position: absolute;
      top: calc(100% + 8px);
      left: 0;
      right: 0;
      z-index: 2;
      max-height: calc(100vh - 112px);
      max-height: calc(100dvh - 112px);
      overflow-y: auto;
      overscroll-behavior: contain;
      -webkit-overflow-scrolling: touch;
      padding: 6px;
      border: 1px solid var(--line-strong);
      border-radius: 22px;
      background: var(--bg-card);
      box-shadow: 0 0 4px rgba(40,44,52,0.06), 0 14px 40px rgba(113,121,129,0.20);
      transform-origin: top center;
      animation: hub-mobile-menu-in .2s cubic-bezier(.2,.8,.2,1);
    }
    .hub-root[data-theme="dark"] .hub-mobile-menu.open {
      box-shadow: 0 0 4px rgba(0,0,0,0.24), 0 14px 40px rgba(0,0,0,0.42);
    }
    .hub-mobile-menu-title {
      padding: 12px 14px 8px;
      font-family: 'Geist Mono', monospace;
      font-size: 10px;
      font-weight: 500;
      letter-spacing: .16em;
      text-transform: uppercase;
      color: var(--ink-dim);
    }
    .hub-mobile-accordion {
      display: flex;
      flex-direction: column;
    }
    .hub-mobile-accordion > * {
      position: relative;
      border-radius: 16px;
    }
    .hub-mobile-accordion > * + *::before {
      content: '';
      position: absolute;
      top: 0;
      left: 14px;
      right: 14px;
      height: 1px;
      background: var(--line);
      pointer-events: none;
      transition: opacity .2s ease;
    }
    .hub-mobile-accordion > .open::before,
    .hub-mobile-accordion > .open + *::before { opacity: 0; }
    .hub-mobile-accordion-item {
      background: transparent;
      transition: background .22s cubic-bezier(.2,.8,.2,1);
    }
    .hub-mobile-accordion-item.open { background: var(--bg-card-2); }
    .hub-mobile-accordion-trigger,
    .hub-mobile-menu .hub-mobile-docs {
      width: 100%;
      min-height: 52px;
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 12px;
      padding: 0 10px 0 14px;
      border: 0;
      border-radius: 16px;
      background: transparent;
      color: var(--ink);
      font-family: 'Geist Mono', monospace;
      font-size: 11px;
      font-weight: 600;
      letter-spacing: .14em;
      text-transform: uppercase;
      text-align: left;
      text-decoration: none;
      cursor: pointer;
      -webkit-tap-highlight-color: transparent;
      transition: background .2s ease;
    }
    .hub-mobile-accordion-trigger:active,
    .hub-mobile-menu .hub-mobile-docs:active { background: var(--bg-card-2); }
    .hub-mobile-accordion-item.open .hub-mobile-accordion-trigger:active { background: transparent; }
    @media (hover: hover) {
      .hub-mobile-accordion-item:not(.open) .hub-mobile-accordion-trigger:hover,
      .hub-mobile-menu .hub-mobile-docs:hover { background: var(--bg-card-2); }
    }
    .hub-mobile-accordion-trigger:focus-visible,
    .hub-mobile-menu a:focus-visible {
      outline: 2px solid var(--accent);
      outline-offset: -2px;
    }
    .hub-mobile-accordion-icon {
      width: 28px;
      height: 28px;
      flex: none;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      border-radius: 999px;
      border: 1px solid var(--line);
      color: var(--ink-dim);
      transition: border-color .22s ease, color .22s ease, background .22s ease;
    }
    .hub-mobile-accordion-icon svg {
      width: 14px;
      height: 14px;
      transition: transform .22s cubic-bezier(.2,.8,.2,1);
    }
    .hub-mobile-accordion-item.open .hub-mobile-accordion-icon {
      border-color: rgba(81,177,254,0.45);
      background: var(--bg-card);
      color: var(--accent);
    }
    .hub-mobile-accordion-item.open .hub-mobile-accordion-icon svg { transform: rotate(180deg); }
    /* visibility (delayed on close) removes collapsed links from tab order and
       the accessibility tree once the collapse has finished. */
    .hub-mobile-accordion-panel {
      display: grid;
      grid-template-rows: 0fr;
      visibility: hidden;
      transition: grid-template-rows .22s cubic-bezier(.2,.8,.2,1), visibility 0s linear .22s;
    }
    .hub-mobile-accordion-item.open .hub-mobile-accordion-panel {
      grid-template-rows: 1fr;
      visibility: visible;
      transition: grid-template-rows .22s cubic-bezier(.2,.8,.2,1), visibility 0s linear 0s;
    }
    .hub-mobile-accordion-clip {
      min-height: 0;
      overflow: hidden;
    }
    .hub-mobile-accordion-links {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(min(100%, 200px), 1fr));
      gap: 4px;
      padding: 0 6px 6px;
      opacity: 0;
      transform: translateY(-4px);
      transition: opacity .16s ease, transform .22s cubic-bezier(.2,.8,.2,1);
    }
    .hub-mobile-accordion-item.open .hub-mobile-accordion-links {
      opacity: 1;
      transform: none;
      transition: opacity .2s ease .04s, transform .22s cubic-bezier(.2,.8,.2,1);
    }
    .hub-mobile-menu .hub-mobile-accordion-links a {
      min-height: 48px;
      min-width: 0;
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 12px;
      padding: 0 14px;
      border-radius: 12px;
      background: var(--bg-card);
      box-shadow: inset 0 0 0 1px var(--line);
      color: var(--ink);
      font-family: 'Geist', system-ui, sans-serif;
      font-size: 14px;
      font-weight: 500;
      letter-spacing: 0;
      text-decoration: none;
      -webkit-tap-highlight-color: transparent;
      transition: box-shadow .2s ease, transform .12s ease;
    }
    .hub-mobile-accordion-links a span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
    .hub-mobile-accordion-links a svg {
      width: 14px;
      height: 14px;
      flex: none;
      color: var(--ink-dim);
    }
    .hub-mobile-menu .hub-mobile-accordion-links a:active { transform: scale(.98); }
    @media (hover: hover) {
      .hub-mobile-menu .hub-mobile-accordion-links a:hover { box-shadow: inset 0 0 0 1px var(--line-strong); }
    }
    @media (prefers-reduced-motion: reduce) {
      .hub-mobile-menu.open { animation: none; }
      .hub-mobile-accordion > *::before,
      .hub-mobile-accordion-item,
      .hub-mobile-accordion-trigger,
      .hub-mobile-menu .hub-mobile-docs,
      .hub-mobile-accordion-icon,
      .hub-mobile-accordion-icon svg,
      .hub-mobile-accordion-panel,
      .hub-mobile-accordion-item.open .hub-mobile-accordion-panel,
      .hub-mobile-accordion-links,
      .hub-mobile-accordion-item.open .hub-mobile-accordion-links,
      .hub-mobile-menu .hub-mobile-accordion-links a { transition: none; }
      .hub-mobile-menu .hub-mobile-accordion-links a:active { transform: none; }
    }
  }
  @media (max-width: 900px) {
    /* minmax(0, 1fr), not 1fr: a bare fr track floors at min-content, and the
       marquee's min-content is its full logo strip — which blew this card out to
       ~4,260px inside a 339px viewport, silently clipped by .hub-root. */
    .hub-globe-card { grid-template-columns: minmax(0, 1fr); min-height: 0; }
    /* Stacked, the well spans the full width and only its top corners are the
       card's — the panel sits directly beneath it, so a rounded bottom would
       cut into that seam. */
    .hub-globe-pane {
      border-right: none; border-bottom: 1px solid var(--line); min-height: 420px;
      margin: calc(-1 * var(--card-pad)) calc(-1 * var(--card-pad)) 0;
      border-radius: 20px 80px 0 0;
    }
    .hub-metrics-panel { min-height: 0; }
    .hub-detail-pane { min-height: 0; padding-top: 22px; }
    .hub-report-panel { grid-template-columns: 1fr; }
    .hub-report-actions { grid-template-columns: repeat(3, minmax(0, 1fr)); }
    
    
    .hub-validator-overview-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  }
  @media (max-width: 760px) {
    .hub-shell { padding: 16px 18px 60px; }
    .hub-nav { padding: 10px 12px; }
    .hub-pill { display: none; }
    .hub-hero { flex-direction: column; align-items: stretch; gap: 18px; }
    .hub-hero-meta { grid-template-columns: repeat(3, minmax(0, 1fr)); }
    .hub-kpi-strip { grid-template-columns: 1fr 1fr; gap: 12px; }
    .hub-tools { grid-template-columns: 1fr 1fr; }
    .hub-report-actions { grid-template-columns: 1fr; }
    
    
    
    .hub-validator-overview-grid { grid-template-columns: 1fr 1fr; }

    /* partners table → card list */
    .hub-partners-head { display: none; }
    .hub-partner-row {
      grid-template-columns: 1fr;
      grid-template-areas: "name" "act";
      gap: 10px;
      padding: 16px 18px;
    }
    .hub-partner-row > :nth-child(1) { grid-area: name; }
    .hub-partner-row > :nth-child(2) { grid-area: act; padding-top: 6px; border-top: 1px dashed rgba(40,44,52,0.08); }

    .hub-map-card { min-height: 380px; }
    .hub-bg-blob { display: none; }
    .hub-globe-pane { min-height: 330px; }
    .hub-metrics-panel { padding: 22px 20px 18px; }
    .hub-detail-pane { padding: 22px 20px 18px; }
    
  }
  @media (max-width: 480px) {
    .hub-kpi-strip { grid-template-columns: 1fr; }
    .hub-globe-metrics-grid { grid-template-columns: 1fr; }
    .hub-globe-metrics-grid .hub-kpi.wide { grid-column: auto; }
    .hub-stake-card .hub-stake-metrics { grid-template-columns: 1fr; gap: 12px; }
    .hub-stake-card .mini-label { min-height: 0; }
    .hub-node-headline,
    .hub-node-evidence,
    .hub-node-trust { grid-template-columns: 1fr; }
    
    .hub-tools { grid-template-columns: 1fr; }
    .hub-validator-overview-grid { grid-template-columns: 1fr; }
    .hub-foot { flex-direction: column; gap: 12px; align-items: flex-start; }
    .hub-foot-links { flex-wrap: wrap; gap: 10px 16px; }
  }

  

  
  
  
  
  
  @keyframes vpub-pulse {
    0%   { transform: scale(0.7); opacity: 1; }
    100% { transform: scale(2.6); opacity: 0; }
  }
  
  
  
  .vpub-meta .sep { color: var(--line-strong); }
  
  
  
  .vpub-segment button.active {
    background: var(--bg-card);
    color: var(--ink);
    box-shadow: 0 4px 12px rgba(40,44,52,0.08);
  }

  
  
  
  @keyframes vpub-glow {
    0%   { opacity: 0; transform: translate(-10%, 4%); }
    100% { opacity: 1; transform: translate(10%, -4%); }
  }
  @keyframes vpub-sheen {
    0%   { transform: translateX(-30%); opacity: 0; }
    35%  { opacity: 1; }
    65%  { opacity: 1; }
    100% { transform: translateX(230%); opacity: 0; }
  }
  
  
  .vpub-hero-eyebrow .chip {
    padding: 3px 8px;
    border-radius: 999px;
    background: rgba(81,177,254,0.12);
    color: #2070C0;
    border: 1px solid rgba(81,177,254,0.25);
  }
  
  
  
  
  
  
  
  .vpub-hero-headline .lbl {
    display: flex;
    align-items: center;
    gap: 8px;
    font-family: 'Geist Mono', monospace;
    font-size: 10px;
    text-transform: uppercase;
    letter-spacing: 0.16em;
    color: var(--ink-dim);
    margin-bottom: 6px;
  }
  .vpub-hero-headline .lbl .pip {
    width: 6px; height: 6px;
    border-radius: 50%;
    background: linear-gradient(135deg, #51B1FE, #B87CFF);
    box-shadow: 0 0 8px rgba(81,177,254,0.55);
  }
  .vpub-hero-headline .big {
    display: flex;
    align-items: baseline;
    gap: 8px;
    font-size: clamp(40px, 6vw, 64px);
    line-height: 1;
    font-weight: 300;
    letter-spacing: -0.028em;
    color: var(--ink);
    font-variant-numeric: tabular-nums;
  }
  .vpub-hero-headline .big .unit {
    font-size: 18px;
    font-weight: 400;
    color: var(--ink-mute);
    letter-spacing: 0;
  }
  .vpub-hero-headline .delta {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    margin-left: auto;
    padding: 5px 10px;
    border-radius: 999px;
    font-family: 'Geist Mono', monospace;
    font-size: 11px;
    background: rgba(100,226,127,0.14);
    color: #1F7A3A;
    border: 1px solid rgba(100,226,127,0.30);
  }
  .vpub-hero-headline .delta svg { width: 10px; height: 10px; }
  .vpub-hero-headline .small {
    margin-top: 12px;
    display: flex;
    flex-wrap: wrap;
    gap: 14px;
    font-family: 'Geist Mono', monospace;
    font-size: 10.5px;
    color: var(--ink-mute);
  }
  .vpub-hero-headline .small b {
    color: var(--ink);
    font-weight: 600;
    font-variant-numeric: tabular-nums;
  }
  .vpub-hero-headline .small span.k {
    color: var(--ink-dim);
    text-transform: uppercase;
    letter-spacing: 0.12em;
    margin-right: 4px;
  }

  
  
  
  
  .vpub-kpi .k {
    font-family: 'Geist Mono', monospace;
    font-size: 9.5px;
    text-transform: uppercase;
    letter-spacing: 0.14em;
    color: var(--ink-dim);
    display: inline-flex;
    align-items: center;
    gap: 8px;
  }
  .vpub-kpi .dot {
    width: 6px;
    height: 6px;
    border-radius: 999px;
    background: var(--ink-faint);
  }
  .vpub-kpi.mum { --kpi-glow: rgba(51,54,60,0.08); }
  .vpub-kpi.lib { --kpi-glow: rgba(254,161,88,0.14); }
  .vpub-kpi.adv { --kpi-glow: rgba(100,226,127,0.14); }
  .vpub-kpi.win { --kpi-glow: rgba(100,226,127,0.14); }
  .vpub-kpi.mum .dot { background: var(--carbon); box-shadow: 0 0 6px rgba(51,54,60,0.35); }
  .vpub-kpi.lib .dot { background: var(--peach); box-shadow: 0 0 6px rgba(254,161,88,0.55); }
  .vpub-kpi.adv .dot { background: #64E27F; box-shadow: 0 0 6px rgba(100,226,127,0.55); }
  .vpub-kpi.win .dot { background: #64E27F; box-shadow: 0 0 6px rgba(100,226,127,0.55); }
  .vpub-kpi .v {
    display: flex;
    align-items: baseline;
    gap: 6px;
    font-size: 30px;
    font-weight: 300;
    letter-spacing: -0.018em;
    font-variant-numeric: tabular-nums;
    color: var(--ink);
    line-height: 1;
  }
  .vpub-kpi .v .unit { font-size: 13px; font-weight: 400; color: var(--ink-mute); }
  .vpub-kpi.adv .v { color: var(--ink); }
  .vpub-kpi.adv .v .num {
    color: #1F7A3A;
  }
  .vpub-kpi .d {
    font-family: 'Geist Mono', monospace;
    font-size: 10px;
    color: var(--ink-mute);
  }
  .vpub-kpi .spark { display: block; margin-top: 6px; width: 100%; height: 30px; }
  .vpub-kpi .bar {
    position: relative;
    height: 6px;
    border-radius: 999px;
    background: rgba(176,190,209,0.18);
    overflow: hidden;
    margin-top: 6px;
  }
  .vpub-kpi .bar i {
    display: block;
    height: 100%;
    background: linear-gradient(90deg, #64E27F, #51B1FE);
    width: 0;
    border-radius: inherit;
    animation: vpub-fill var(--dur, 1400ms) cubic-bezier(.2,.7,.2,1) forwards;
    animation-delay: 200ms;
  }
  @keyframes vpub-fill { to { width: var(--w, 0%); } }

  .vpub-card-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 12px;
    margin-bottom: 14px;
  }
  .vpub-card-head h3 {
    margin: 0;
    font-size: 15px;
    font-weight: 500;
    letter-spacing: -0.01em;
    color: var(--ink);
    display: inline-flex;
    align-items: center;
    gap: 7px;
  }
  .vpub-card-head .sub {
    font-family: 'Geist Mono', monospace;
    font-size: 10px;
    text-transform: uppercase;
    letter-spacing: 0.14em;
    color: var(--ink-dim);
  }
  
  
  
  .vpub-chart .grid line {
    stroke: var(--line);
    stroke-width: 1;
    stroke-dasharray: 2 4;
  }
  .vpub-chart .axis {
    font-family: 'Geist Mono', monospace;
    font-size: 10px;
    fill: var(--ink-dim);
    letter-spacing: 0.04em;
  }
  .vpub-chart .axis-unit {
    fill: var(--ink-faint);
    font-size: 9px;
    letter-spacing: 0.08em;
  }
  .vpub-chart .axis-slot {
    font-size: 10.5px;
    fill: var(--ink-mute);
    letter-spacing: 0.06em;
  }
  .vpub-chart .axis-line {
    stroke: var(--line-strong);
    stroke-width: 1;
  }

  /* trend connectors for both series — softer Carbon for mum, Peach for lib */
  .vpub-chart .trend-mum,
  .vpub-chart .trend-lib {
    fill: none;
    stroke-width: 1.6;
    stroke-linecap: round;
    stroke-linejoin: round;
    stroke-dasharray: 1;
    stroke-dashoffset: 1;
    animation: vpub-draw 1700ms cubic-bezier(.2,.7,.2,1) forwards;
  }
  .vpub-chart .trend-mum {
    stroke: var(--carbon);
    opacity: 0.55;
  }
  .vpub-chart .trend-lib {
    stroke: var(--peach);
    opacity: 0.55;
    animation-delay: 120ms;
  }
  @keyframes vpub-draw { to { stroke-dashoffset: 0; } }

  /* dots — softened Carbon for mumP2P, Peach for libP2P (brand convention) */
  .vpub-chart .dot-mum,
  .vpub-chart .dot-lib {
    opacity: 0;
    animation: vpub-pop 280ms ease-out forwards;
    animation-delay: calc(var(--i, 0) * 10ms + 600ms);
    transition: r .15s ease, stroke-width .15s ease;
  }
  @keyframes vpub-pop { to { opacity: 1; } }
  .vpub-chart .dot-mum {
    fill: var(--carbon);
    fill-opacity: 0.78;
    stroke: var(--bg-card);
    stroke-width: 1.2;
  }
  .vpub-chart .dot-lib {
    fill: var(--peach);
    stroke: var(--bg-card);
    stroke-width: 1.2;
  }
  .vpub-chart .dot-mum.latest,
  .vpub-chart .dot-mum.hover {
    fill-opacity: 1;
    stroke-width: 1.6;
  }
  .vpub-chart .dot-lib.latest,
  .vpub-chart .dot-lib.hover {
    stroke-width: 1.4;
  }

  .vpub-chart .latest-rail,
  .vpub-chart .hover-rail {
    stroke: var(--carbon);
    stroke-width: 1;
    stroke-dasharray: 3 3;
    opacity: 0.45;
  }
  .vpub-chart .hover-rail { opacity: 0.25; }
  .vpub-chart .latest-halo {
    fill: rgba(51,54,60,0.16);
    animation: vpub-halo 2.4s ease-in-out infinite;
  }
  @keyframes vpub-halo {
    0%, 100% { r: 6; opacity: 0.5; }
    50%      { r: 14; opacity: 0; }
  }
  .vpub-chart .hit { fill: transparent; cursor: crosshair; }

  
  .vpub-chart-tip.show { opacity: 1; }
  
  
  
  .vpub-chart-tip-row .dot {
    width: 8px; height: 8px;
    border-radius: 50%;
    flex: none;
  }
  .vpub-chart-tip-row.mum .dot { background: var(--alabaster); }
  .vpub-chart-tip-row.lib .dot {
    background: var(--peach);
    box-shadow: 0 0 0 1px rgba(254,161,88,0.5);
  }
  .vpub-chart-tip-row .lbl { color: var(--plutonium); flex: 1; }
  
  
  
  

  
  
  .vpub-chart-legend .sw {
    width: 10px; height: 10px;
    border-radius: 50%;
    margin-right: 8px;
    display: inline-block;
  }
  .vpub-chart-legend .sw.mum {
    background: var(--carbon);
    opacity: 0.78;
    box-shadow: 0 0 0 1.5px var(--bg-card), 0 0 0 2.5px rgba(51,54,60,0.12);
  }
  .vpub-chart-legend .sw.lib {
    background: var(--peach);
    box-shadow: 0 0 0 1.5px var(--bg-card), 0 0 0 2.5px rgba(254,161,88,0.18);
  }

  
  .vpub-side {
    border: 1px solid var(--line);
    border-radius: 16px;
    background: var(--bg-card);
    padding: 18px 22px;
    display: flex;
    flex-direction: column;
  }
  
  
  
  
  
  
  
  .vpub-feed .vpub-card-head .sub {
    display: inline-flex; align-items: center; gap: 8px;
  }
  
  
  
  
  
  .vpub-feed-toggle button.active {
    background: var(--bg-card);
    color: var(--ink);
    box-shadow: 0 4px 12px rgba(40,44,52,0.08);
  }
  .vpub-feed .vpub-card-head .live-pip {
    width: 6px; height: 6px;
    border-radius: 50%;
    background: var(--carbon);
    box-shadow: 0 0 0 3px rgba(51,54,60,0.08);
    animation: vpub-pulse 1.8s ease-out infinite;
    flex: none;
  }
  
  .vpub-feed-row.clickable { cursor: pointer; }
  .vpub-feed-row.clickable:focus-visible {
    outline: 2px solid rgba(81,177,254,0.55);
    outline-offset: 2px;
  }
  .vpub-feed-row.selected {
    border-color: rgba(89,102,116,0.85);
    background: rgba(89,102,116,0.08);
    box-shadow: 0 0 0 1px rgba(89,102,116,0.45);
  }
  @keyframes vpub-slide { to { transform: translateX(0); opacity: 1; } }
  
  .vpub-feed-row.faster { border-left: 3px solid var(--ink-mute); }
	  .vpub-feed-row.slower { border-left: 3px solid var(--platinum); }
	  .vpub-feed-row.equal,
	  .vpub-feed-row.na { border-left: 3px solid var(--plutonium); }
	  .vpub-feed-row.latest {
	    background: linear-gradient(90deg, rgba(81,177,254,0.10), rgba(255,255,255,0.74));
	    border-color: rgba(81,177,254,0.28);
	  }
  .vpub-feed-row .slot {
    color: var(--ink);
    display: flex; flex-direction: column;
    gap: 2px;
  }
  .vpub-feed-row .slot .tier {
    font-family: 'Geist Mono', monospace;
    font-size: 16px;
    font-weight: 600;
    letter-spacing: -0.01em;
    color: var(--ink-mute);
    line-height: 1;
  }
  .vpub-feed-row .slot .tier-cap {
    font-family: 'Geist Mono', monospace;
    font-size: 9px;
    letter-spacing: 0.10em;
    text-transform: uppercase;
    color: var(--ink-dim);
  }
  .vpub-feed-row .lanes { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
  .vpub-feed-row .lane { display: flex; align-items: center; gap: 8px; }
	  .vpub-feed-row .lane .tag {
	    font-size: 9.5px;
	    color: var(--ink-dim);
	    width: 54px;
	    flex: none;
	    letter-spacing: 0.04em;
	  }
  .vpub-feed-row .lane .bar {
    position: relative;
    flex: 1;
    height: 5px;
    border-radius: 999px;
    background: rgba(176,190,209,0.22);
    overflow: hidden;
  }
  .vpub-feed-row .lane .bar i {
    display: block;
    height: 100%;
    width: 0;
    border-radius: inherit;
    animation: vpub-fill 800ms cubic-bezier(.2,.7,.2,1) forwards;
    animation-delay: calc(var(--i, 0) * 60ms + 200ms);
  }
  .vpub-feed-row .lane .bar.mum i { background: var(--carbon); }
  .vpub-feed-row .lane .bar.lib i { background: var(--platinum); }
	  .vpub-feed-row .lane .ms { color: var(--ink-mute); font-size: 10.5px; flex: none; width: 68px; text-align: right; font-variant-numeric: tabular-nums; }
  .vpub-feed-row .adv {
    color: var(--ink);
    font-weight: 600;
    text-align: right;
    font-variant-numeric: tabular-nums;
    font-size: 13px;
    letter-spacing: -0.01em;
  }
  .vpub-feed-row.slower .adv { color: var(--ink-mute); }
  .vpub-feed-row .pill {
    text-align: center;
    padding: 4px 8px;
    border-radius: 999px;
    font-size: 9px;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    background: var(--bg-card);
    border: 1px solid var(--line);
    color: var(--ink-mute);
    white-space: nowrap;
  }
  .vpub-feed-row.faster .pill { background: var(--ink-mute); color: var(--alabaster); border-color: var(--ink-mute); }
  .vpub-feed-row.slower .pill { background: var(--bg-card); color: var(--ink-mute); border-color: var(--line-strong); }
  .vpub-feed-row.equal .pill { background: var(--bg-card-2); color: var(--ink-mute); }

  
  
  .vpub-legend .row {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: center;
    gap: 10px;
    padding: 7px 10px;
    border-radius: 8px;
    background: rgba(238,242,245,0.5);
    font-family: 'Geist Mono', monospace;
    font-size: 11px;
  }
  .vpub-legend .sw {
    width: 8px; height: 8px;
    border-radius: 50%;
    display: inline-block;
    margin-right: 8px;
  }
  .vpub-legend .row.faster .sw { background: #596674; }
  .vpub-legend .row.slower .sw { background: #FEA158; }
  .vpub-legend .row.equal .sw { background: #B0BED1; }
  .vpub-legend .row.na .sw { background: rgba(176,190,209,0.5); }
  
  .vpub-legend .l { color: var(--ink-mute); }
  .vpub-legend .v {
    display: inline-flex;
    align-items: baseline;
    gap: 8px;
  }
  .vpub-legend .v .pc {
    color: var(--ink-dim);
    font-size: 10px;
    font-variant-numeric: tabular-nums;
    min-width: 30px;
    text-align: right;
  }
  
  .vpub-winrate.faster { border-color: rgba(89,102,116,0.35); }
  .vpub-winrate.slower { border-color: rgba(254,161,88,0.45); background: rgba(254,161,88,0.08); }
  
  .vpub-winrate-value .n {
    font-size: 34px;
    font-weight: 500;
    line-height: 1;
  }
  .vpub-winrate-value .u {
    font-size: 14px;
    color: var(--ink-mute);
    margin-left: 2px;
    font-weight: 400;
  }
  
  
  
  
  
  .vpub-adv-ring .ring {
    position: relative;
    width: 96px;
    height: 96px;
    margin: 0 auto 8px;
  }
  
  
  .vpub-adv-ring .ring-bg {
    stroke: rgba(176,190,209,0.22);
  }
  .vpub-adv-ring .ring-arc {
    stroke-linecap: round;
    transition: stroke-dashoffset 900ms cubic-bezier(.2,.7,.2,1);
  }
  .vpub-adv-ring.faster .ring-arc { stroke: #596674; }
  .vpub-adv-ring.slower .ring-arc { stroke: #FEA158; }
  .vpub-adv-ring.equal .ring-arc,
  .vpub-adv-ring.na .ring-arc { stroke: #B0BED1; }
  .vpub-adv-ring .ring-center {
    position: absolute;
    inset: 0;
    display: grid;
    place-items: center;
    font-size: 22px;
    font-weight: 500;
    font-variant-numeric: tabular-nums;
    color: var(--ink);
    letter-spacing: -0.02em;
  }
  .vpub-adv-ring .ring-center .u {
    font-size: 11px;
    color: var(--ink-mute);
    font-weight: 400;
    margin-left: 1px;
  }
  .vpub-adv-ring .p {
    font-family: 'Geist Mono', monospace;
    font-size: 10px;
    color: var(--ink-dim);
    text-transform: uppercase;
    letter-spacing: 0.12em;
    line-height: 1.2;
  }

  
  .vpub-kpis-head .eyebrow {
    font-size: 10px;
    text-transform: uppercase;
    letter-spacing: 0.16em;
    color: var(--ink-dim);
  }
  .vpub-kpis-head .dot { color: var(--ink-dim); }
  

  
  .vpub-kpis.vpub-kpis-3 .vpub-kpi.adv .v .num { color: #1F7A3A; }
  .hub-root[data-theme="dark"] .vpub-kpis .vpub-kpi.adv .v .num { color: #64E27F; }
  

  .fleet-hist-card .vpub-card-head,
  .fleet-cmp-card .vpub-card-head { margin-bottom: 2px; }
  .fleet-hist-legend {
    display: flex; gap: 18px; margin: 4px 0 16px;
    font-family: 'Geist Mono', monospace; font-size: 10px;
    text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-dim);
  }
  .fleet-hist-legend span { display: inline-flex; align-items: center; gap: 7px; }
  .fleet-hist-legend .sw { width: 11px; height: 11px; border-radius: 3px; flex: none; }
  .fleet-hist-legend .sw.pos { background: #64E27F; }
  .fleet-hist-legend .sw.neg { background: #FEA158; }

  .fleet-hist { position: relative; height: 172px; margin-bottom: 10px; }
  .fleet-hist-bars {
    position: absolute; inset: 0; z-index: 1;
    display: flex; align-items: flex-end; gap: 3px;
  }
  .fleet-hist-slot { flex: 1 1 0; height: 100%; display: flex; align-items: flex-end; min-width: 0; }
  .fleet-hist-bar {
    width: 100%; height: var(--h, 0%); min-height: 1px;
    border-radius: 4px 4px 0 0;
    transform-origin: bottom;
    animation: fleet-hist-grow 620ms cubic-bezier(.2,.7,.2,1) both;
    animation-delay: calc(var(--i, 0) * 26ms);
  }
  .fleet-hist-bar.pos { background: linear-gradient(180deg, #64E27F, rgba(81,177,254,0.72)); }
  .fleet-hist-bar.neg { background: linear-gradient(180deg, #FEA158, rgba(254,161,88,0.55)); }
  @keyframes fleet-hist-grow { from { transform: scaleY(0); } to { transform: scaleY(1); } }
  .fleet-hist-zero, .fleet-hist-median {
    position: absolute; top: -4px; bottom: 0; width: 0; pointer-events: none;
  }
  .fleet-hist-zero { border-left: 1px solid var(--line-strong); z-index: 2; }
  .fleet-hist-median { border-left: 1.5px dashed #1F7A3A; z-index: 3; }
  .hub-root[data-theme="dark"] .fleet-hist-median { border-left-color: #64E27F; }
  .fleet-hist-median .lab {
    position: absolute; top: -6px; left: 7px; white-space: nowrap;
    font-family: 'Geist Mono', monospace; font-size: 9.5px; font-weight: 500;
    color: #1F7A3A; background: var(--bg-card);
    border: 1px solid rgba(100,226,127,0.42); border-radius: 6px;
    padding: 2px 7px;
  }
  .hub-root[data-theme="dark"] .fleet-hist-median .lab { color: #8EE9A5; }
  .fleet-hist-axis {
    display: flex; justify-content: space-between; align-items: baseline;
    font-family: 'Geist Mono', monospace; font-size: 10px; color: var(--ink-dim);
    border-top: 1px solid var(--line); padding-top: 9px;
    font-variant-numeric: tabular-nums;
  }
  .fleet-hist-axis .mid { color: var(--ink-mute); letter-spacing: 0.06em; }
  .fleet-hist-empty { padding: 40px 0; text-align: center; color: var(--ink-dim); font-size: 13px; }

  .fleet-cmp { display: flex; flex-direction: column; gap: 13px; margin: 10px 0 4px; }
  .fleet-cmp-row {
    display: grid; grid-template-columns: 60px minmax(0, 1fr) auto;
    align-items: center; gap: 12px;
  }
  .fleet-cmp-row .lab {
    font-family: 'Geist Mono', monospace; font-size: 11px; color: var(--ink-mute);
  }
  .fleet-cmp-row .track {
    height: 12px; border-radius: 999px;
    background: rgba(176,190,209,0.22); overflow: hidden;
  }
  .fleet-cmp-row .fill {
    display: block; height: 100%; width: 0; border-radius: inherit;
    animation: vpub-fill 1100ms cubic-bezier(.2,.7,.2,1) forwards; animation-delay: 150ms;
  }
  .fleet-cmp-row.mum .fill { background: linear-gradient(90deg, #64E27F, #51B1FE); }
  .fleet-cmp-row.lib .fill { background: linear-gradient(90deg, #FEA158, #F4C77E); }
  .fleet-cmp-row .val {
    font-size: 15px; font-weight: 500; color: var(--ink);
    font-variant-numeric: tabular-nums; letter-spacing: -0.01em; white-space: nowrap;
  }
  .fleet-cmp-row .val .u { font-size: 10.5px; color: var(--ink-mute); font-weight: 400; margin-left: 2px; }
  .fleet-cmp-foot {
    display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
    margin-top: 15px; padding-top: 14px; border-top: 1px solid var(--line);
  }
  .fleet-cmp-foot .chip {
    display: inline-flex; align-items: center; gap: 6px;
    font-family: 'Geist Mono', monospace; font-size: 11px; font-weight: 500;
    color: #1F7A3A; background: rgba(100,226,127,0.12);
    border: 1px solid rgba(100,226,127,0.34); border-radius: 999px; padding: 5px 12px;
  }
  .hub-root[data-theme="dark"] .fleet-cmp-foot .chip {
    color: #8EE9A5; background: rgba(100,226,127,0.14); border-color: rgba(100,226,127,0.3);
  }
  .fleet-cmp-foot .note {
    font-family: 'Geist Mono', monospace; font-size: 10.5px; color: var(--ink-dim);
  }
  

  /* Advantage strip — paired delta per slot with percentile reference lines */
  .vpub-advstrip .vpub-chart-legend .sw.faster { background: #596674; }
  .vpub-advstrip .vpub-chart-legend .sw.slower { background: #FEA158; }
  
  
  .advstrip-bar.faster { fill: #596674; }
  .advstrip-bar.slower { fill: #FEA158; }
  .advstrip-bar.equal { fill: #B0BED1; }
  
  .advstrip-bar.selected {
    stroke: var(--ink);
    stroke-width: 1.8;
  }
  
  
  
  
  
  .advstrip-pline.selected line { stroke-width: 2.2; opacity: 1; }
  
  
  
  
  .advstrip-tip .head {
    color: #FFFBFB;
    font-weight: 600;
    margin-bottom: 5px;
  }
  .advstrip-tip .row {
    display: flex;
    justify-content: space-between;
    gap: 14px;
  }
  .advstrip-tip .row span { color: rgba(255,251,251,0.68); }
  .advstrip-tip .row b { color: #FFFBFB; font-weight: 600; }

  /* Clickable percentile rings */
  .vpub-adv-ring.clickable {
    cursor: pointer;
    border: 1px solid var(--line);
    appearance: none;
    font: inherit;
    color: inherit;
    width: 100%;
    transition: border-color 160ms ease, background 160ms ease, transform 160ms ease;
  }
  .vpub-adv-ring.clickable:hover {
    border-color: rgba(89,102,116,0.55);
    background: rgba(238,242,245,0.7);
    transform: translateY(-1px);
  }
  .vpub-adv-ring.clickable:focus-visible {
    outline: 2px solid rgba(81,177,254,0.55);
    outline-offset: 2px;
  }
  .vpub-adv-ring.selected {
    border-color: rgba(89,102,116,0.85);
    background: rgba(89,102,116,0.08);
    box-shadow: 0 0 0 1px rgba(89,102,116,0.45);
  }

  
  @keyframes vpub-pdetail-in {
    from { opacity: 0; transform: translateY(-4px); }
    to { opacity: 1; transform: translateY(0); }
  }
  
  
  
  
  
  .vpub-pdetail-hist .hist-bar { transition: opacity 200ms ease; }
  .vpub-pdetail-hist .hist-bar.faster { fill: rgba(89,102,116,0.55); }
  .vpub-pdetail-hist .hist-bar.slower { fill: rgba(254,161,88,0.65); }
  .vpub-pdetail-hist .hist-bar.equal { fill: rgba(176,190,209,0.55); }
  .vpub-pdetail-hist .hist-bar.marker { fill: #64E27F; }
  .vpub-pdetail-hist .hist-marker-line { stroke: #64E27F; stroke-width: 1.5; }
  .vpub-pdetail-hist .hist-marker-label {
    font-family: 'Geist Mono', monospace;
    font-size: 10.5px;
    fill: var(--ink);
    font-weight: 500;
  }
  
  
  .vpub-pdetail-row.head {
    background: transparent;
    color: var(--ink-dim);
    font-size: 9.5px;
    text-transform: uppercase;
    letter-spacing: 0.12em;
    padding: 0 10px 4px;
  }
  .vpub-pdetail-row .slot { color: var(--ink); font-weight: 600; }
  .vpub-pdetail-row .delta { text-align: right; font-weight: 600; }
  .vpub-pdetail-row.faster .delta { color: #2F6F45; }
  .vpub-pdetail-row.slower .delta { color: #B26A2A; }

  
  @media (max-width: 640px) {
    
    
    
    
    
    .vpub-feed-row .adv,
    .vpub-feed-row .pill { justify-self: start; }
    
    
  }

  
  
  .hub-root[data-theme="dark"] .vpub-side {
    background: var(--bg-card);
  }
  
  .hub-root[data-theme="dark"] .vpub-hero-headline .delta {
    background: rgba(100,226,127,0.16);
    color: #88EFA0;
    border-color: rgba(100,226,127,0.32);
  }
  
  
  .hub-root[data-theme="dark"] .vpub-feed-row.latest {
    background: linear-gradient(90deg, rgba(81,177,254,0.20), rgba(81,177,254,0.04));
    border-color: rgba(81,177,254,0.40);
  }
  .hub-root[data-theme="dark"] .vpub-legend .row { background: rgba(35,39,48,0.45); }
  
  .hub-root[data-theme="dark"] .vpub-winrate.slower { background: rgba(254,161,88,0.10); border-color: rgba(254,161,88,0.35); }
  .hub-root[data-theme="dark"] .vpub-advstrip .advstrip-bar.faster { fill: var(--alabaster); fill-opacity: 0.78; }
  
  
  
  .hub-root[data-theme="dark"] .vpub-pdetail-hist .hist-bar.faster { fill: rgba(232,232,228,0.6); }
  .hub-root[data-theme="dark"] .vpub-adv-ring.clickable:hover { background: rgba(35,39,48,0.7); border-color: rgba(232,232,228,0.4); }
  .hub-root[data-theme="dark"] .vpub-adv-ring.selected { background: rgba(232,232,228,0.10); border-color: rgba(232,232,228,0.5); box-shadow: 0 0 0 1px rgba(232,232,228,0.35); }
  /* Dark-mode chart — invert mumP2P to Alabaster so it reads on Carbon;
     libP2P keeps its Peach identity in both themes. */
  .hub-root[data-theme="dark"] .vpub-chart .dot-mum {
    fill: var(--alabaster);
    fill-opacity: 0.85;
    stroke: rgba(35,39,48,0.6);
    stroke-width: 1.2;
  }
  .hub-root[data-theme="dark"] .vpub-chart .dot-mum.latest,
  .hub-root[data-theme="dark"] .vpub-chart .dot-mum.hover {
    fill-opacity: 1;
    stroke: var(--bg-card);
    stroke-width: 1.6;
  }
  .hub-root[data-theme="dark"] .vpub-chart .dot-lib {
    fill: var(--peach);
    stroke: rgba(35,39,48,0.6);
    stroke-width: 1.2;
  }
  .hub-root[data-theme="dark"] .vpub-chart .dot-lib.latest,
  .hub-root[data-theme="dark"] .vpub-chart .dot-lib.hover {
    stroke: var(--bg-card);
    stroke-width: 1.4;
  }
  .hub-root[data-theme="dark"] .vpub-chart .trend-mum {
    stroke: var(--alabaster);
    opacity: 0.45;
  }
  .hub-root[data-theme="dark"] .vpub-chart .trend-lib {
    stroke: var(--peach);
    opacity: 0.55;
  }
  .hub-root[data-theme="dark"] .vpub-chart .latest-rail,
  .hub-root[data-theme="dark"] .vpub-chart .hover-rail {
    stroke: var(--alabaster);
    opacity: 0.35;
  }
  .hub-root[data-theme="dark"] .vpub-chart .latest-halo {
    fill: rgba(255,251,251,0.18);
  }
  .hub-root[data-theme="dark"] .vpub-chart-legend .sw.mum {
    background: var(--alabaster);
    opacity: 0.85;
    box-shadow: 0 0 0 1.5px var(--bg-card), 0 0 0 2.5px rgba(255,251,251,0.18);
  }
  .hub-root[data-theme="dark"] .vpub-chart-legend .sw.lib {
    background: var(--peach);
    box-shadow: 0 0 0 1.5px var(--bg-card), 0 0 0 2.5px rgba(254,161,88,0.30);
  }
  .hub-root[data-theme="dark"] .vpub-chart-tip-row.mum .dot {
    background: var(--alabaster);
  }

  /* Dark-mode advantage rings + feed tier badge — invert Carbon to Alabaster
     so mumP2P stays readable on the dark Carbon surface. */
  .hub-root[data-theme="dark"] .vpub-adv-ring.faster .ring-arc {
    stroke: var(--alabaster);
  }
  .hub-root[data-theme="dark"] .vpub-adv-ring .ring-bg {
    stroke: rgba(255,251,251,0.10);
  }
  .hub-root[data-theme="dark"] .vpub-legend .row.faster .sw {
    background: var(--alabaster);
  }
  .hub-root[data-theme="dark"] .vpub-feed-row .slot .tier {
    color: var(--alabaster);
  }
  .hub-root[data-theme="dark"] .vpub-feed-row.faster .pill {
    background: var(--alabaster);
    color: var(--carbon);
    border-color: var(--alabaster);
  }
  .hub-root[data-theme="dark"] .vpub-feed-row.faster .adv { color: #88EFA0; }
  .hub-root[data-theme="dark"] .vpub-feed-row.slower .adv { color: #FEC79C; }
  .hub-root[data-theme="dark"] .vpub-feed-row.faster .pill { background: rgba(81,177,254,0.22); color: #9BD2FF; }
  .hub-root[data-theme="dark"] .vpub-feed-row.slower .pill { background: rgba(254,161,88,0.18); color: #FEC79C; }
  .hub-root[data-theme="dark"] .vpub-hero-eyebrow .chip {
    background: rgba(81,177,254,0.20);
    color: #9BD2FF;
    border-color: rgba(81,177,254,0.40);
  }

  /* ═══════════════════════════════════════════════════════════════
     Main-page live slot stream (Carbon surface, brand accents only)
     ═══════════════════════════════════════════════════════════════ */
  .hub-stream {
    position: relative;
    overflow: hidden;
    border-radius: 20px;
    margin-top: 28px;
    padding: 22px 26px 20px;
    background: linear-gradient(180deg, #1F2229 0%, #15171C 100%);
    color: var(--alabaster);
    border: 1px solid rgba(255,251,251,0.06);
    box-shadow: 0 28px 64px -32px rgba(40,44,52,0.55);
  }
  .hub-stream::before {
    content: ""; position: absolute; inset: 0;
    background:
      radial-gradient(50% 80% at 0% 0%, rgba(81,177,254,0.12), transparent 60%),
      radial-gradient(40% 80% at 100% 100%, rgba(184,124,255,0.10), transparent 60%);
    pointer-events: none;
  }
  .hub-stream::after {
    content: ""; position: absolute;
    top: -40%; left: -10%; width: 50%; height: 180%;
    background: linear-gradient(115deg, transparent 42%, rgba(255,251,251,0.05) 50%, transparent 58%);
    transform: translateX(-30%);
    animation: hub-stream-sheen 9s ease-in-out infinite;
    pointer-events: none;
  }
  @keyframes hub-stream-sheen {
    0%   { transform: translateX(-30%); opacity: 0; }
    30%  { opacity: 1; }
    70%  { opacity: 1; }
    100% { transform: translateX(260%); opacity: 0; }
  }
  .hub-stream-head {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 14px;
    margin-bottom: 16px;
    flex-wrap: wrap;
  }
  .hub-stream-title { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
  .hub-stream-dot {
    position: relative;
    width: 8px; height: 8px;
    border-radius: 50%;
    background: #64E27F;
    box-shadow: 0 0 12px rgba(100,226,127,0.75);
  }
  .hub-stream-dot::after {
    content: ""; position: absolute; inset: -3px;
    border-radius: 50%;
    border: 1.5px solid rgba(100,226,127,0.55);
    animation: vpub-pulse 1.8s ease-out infinite;
  }
  .hub-stream-title h3 {
    margin: 0;
    font-size: 16px;
    font-weight: 500;
    color: var(--alabaster);
    letter-spacing: -0.008em;
  }
  .hub-stream-sub {
    font-family: 'Geist Mono', monospace;
    font-size: 10.5px;
    color: rgba(255,251,251,0.55);
    letter-spacing: 0.08em;
  }
  .hub-stream-meta {
    font-family: 'Geist Mono', monospace;
    font-size: 10px;
    color: rgba(255,251,251,0.55);
    letter-spacing: 0.12em;
    text-transform: uppercase;
    display: flex;
    gap: 10px;
    align-items: center;
  }
  .hub-stream-meta b { color: var(--alabaster); font-weight: 600; }
  .hub-stream-meta .sep { color: rgba(255,251,251,0.18); }
  .hub-stream-meta .grad {
    color: var(--alabaster);
    font-weight: 600;
  }
  .hub-stream-grid {
    position: relative;
    display: grid;
    grid-template-columns: 200px 130px minmax(0, 1fr) 90px 64px;
    gap: 16px;
    padding: 0 14px 8px;
    font-family: 'Geist Mono', monospace;
    font-size: 9.5px;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: rgba(255,251,251,0.45);
    border-bottom: 1px solid rgba(255,251,251,0.06);
    margin-bottom: 8px;
  }
  .hub-stream-grid .right { text-align: right; }
  .hub-stream-grid .center { text-align: center; }
  .hub-stream-list {
    position: relative;
    display: flex;
    flex-direction: column;
    gap: 6px;
  }
  .hub-stream-row {
    position: relative;
    display: grid;
    grid-template-columns: 200px 130px minmax(0, 1fr) 90px 64px;
    align-items: center;
    gap: 16px;
    padding: 11px 14px;
    border-radius: 12px;
    background: rgba(255,251,251,0.03);
    border: 1px solid rgba(255,251,251,0.05);
    color: var(--alabaster);
    font-family: 'Geist Mono', monospace;
    font-size: 11.5px;
    transform: translateY(-10px);
    opacity: 0;
    animation: hub-stream-in 620ms cubic-bezier(.18,.86,.22,1) forwards;
    animation-delay: calc(var(--i, 0) * 34ms);
    transition: background .25s ease, border-color .25s ease, transform .25s ease;
  }
  @keyframes hub-stream-in {
    to { transform: translateY(0); opacity: 1; }
  }
  .hub-stream-row.faster { border-left: 3px solid rgba(81,177,254,0.6); }
  .hub-stream-row.slower { border-left: 3px solid rgba(254,161,88,0.6); }
  .hub-stream-row.equal,
  .hub-stream-row.na { border-left: 3px solid rgba(176,190,209,0.30); }
  .hub-stream-row:hover {
    background: rgba(81,177,254,0.06);
    border-color: rgba(81,177,254,0.20);
    transform: translateY(-1px);
  }
  .hub-stream-row.newest {
    background: linear-gradient(90deg, rgba(81,177,254,0.14), rgba(81,177,254,0.02));
    border-color: rgba(81,177,254,0.30);
    box-shadow: inset 0 0 0 1px rgba(81,177,254,0.10);
  }
  .hub-stream-row .who { display: flex; align-items: center; gap: 10px; min-width: 0; }
  .hub-stream-row .who .badge {
    width: 26px; height: 26px;
    border-radius: 8px;
    background: linear-gradient(135deg, rgba(255,251,251,0.08), rgba(255,251,251,0.02));
    border: 1px solid rgba(255,251,251,0.10);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-family: 'Geist Mono', monospace;
    font-size: 9.5px;
    letter-spacing: 0.06em;
    color: var(--alabaster);
    flex: none;
  }
  .hub-stream-row .who .who-text { min-width: 0; display: flex; flex-direction: column; line-height: 1.25; }
  .hub-stream-row .who .who-text b {
    color: var(--alabaster);
    font-weight: 600;
    font-size: 12px;
    letter-spacing: 0.02em;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .hub-stream-row .who .who-text .region {
    color: rgba(255,251,251,0.42);
    font-size: 10px;
    letter-spacing: 0.06em;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .hub-stream-row .slot {
    font-size: 9.5px;
    color: rgba(255,251,251,0.5);
    letter-spacing: 0.12em;
  }
  .hub-stream-row .slot b {
    display: block;
    color: var(--alabaster);
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    font-size: 12px;
    letter-spacing: 0;
    margin-top: 2px;
  }
  .hub-stream-row .lanes {
    display: flex;
    flex-direction: column;
    gap: 4px;
    min-width: 0;
  }
  .hub-stream-row .lane {
    display: grid;
    grid-template-columns: 32px minmax(0, 1fr) 52px;
    align-items: center;
    gap: 10px;
  }
  .hub-stream-row .lane i {
    font-style: normal;
    font-size: 9.5px;
    letter-spacing: 0.1em;
    color: rgba(255,251,251,0.4);
  }
  .hub-stream-row .lane.mum i { color: rgba(81,177,254,0.85); }
  .hub-stream-row .lane.lib i { color: rgba(176,190,209,0.7); }
  .hub-stream-row .lane .barwrap {
    position: relative;
    height: 4px;
    border-radius: 999px;
    background: rgba(255,251,251,0.06);
    overflow: hidden;
  }
  .hub-stream-row .lane .barwrap span {
    display: block;
    height: 100%;
    width: 0;
    border-radius: inherit;
    animation: vpub-fill 700ms cubic-bezier(.2,.7,.2,1) forwards;
    animation-delay: calc(var(--i, 0) * 34ms + 220ms);
  }
  .hub-stream-row .lane.mum .barwrap span {
    background: linear-gradient(90deg, rgba(81,177,254,0.45), #51B1FE);
    box-shadow: 0 0 8px rgba(81,177,254,0.45);
  }
  .hub-stream-row .lane.lib .barwrap span { background: linear-gradient(90deg, rgba(176,190,209,0.35), #9DB0C2); }
  .hub-stream-row .lane b {
    text-align: right;
    color: rgba(255,251,251,0.85);
    font-weight: 500;
    font-variant-numeric: tabular-nums;
    font-size: 11px;
  }
  .hub-stream-row .adv {
    font-variant-numeric: tabular-nums;
    text-align: right;
    font-weight: 600;
    font-size: 12.5px;
  }
  .hub-stream-row.faster .adv { color: #64E27F; }
  .hub-stream-row.slower .adv { color: #FEA158; }
  .hub-stream-row.equal .adv,
  .hub-stream-row.na .adv { color: rgba(255,251,251,0.65); }
  .hub-stream-row .pill {
    text-align: center;
    padding: 3px 8px;
    border-radius: 999px;
    font-size: 9.5px;
    letter-spacing: 0.14em;
    background: rgba(255,251,251,0.06);
    color: rgba(255,251,251,0.7);
  }
  .hub-stream-row.faster .pill { background: rgba(100,226,127,0.16); color: #88EFA0; border: 1px solid rgba(100,226,127,0.20); }
  .hub-stream-row.slower .pill { background: rgba(254,161,88,0.16); color: #FEC79C; border: 1px solid rgba(254,161,88,0.22); }
  .hub-stream-row.equal .pill { background: rgba(176,190,209,0.14); color: rgba(255,251,251,0.75); }
  .hub-stream-foot {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    margin-top: 14px;
    padding-top: 12px;
    border-top: 1px solid rgba(255,251,251,0.06);
    font-family: 'Geist Mono', monospace;
    font-size: 10px;
    color: rgba(255,251,251,0.55);
    letter-spacing: 0.08em;
    text-transform: uppercase;
  }
  .hub-stream-foot b { color: var(--alabaster); font-variant-numeric: tabular-nums; font-weight: 600; }
  .hub-stream-foot .next {
    display: inline-flex; align-items: center; gap: 8px;
    color: rgba(255,251,251,0.55);
  }
  .hub-stream-foot .next .scan {
    position: relative;
    width: 64px; height: 3px;
    border-radius: 999px;
    background: rgba(255,251,251,0.08);
    overflow: hidden;
  }
  .hub-stream-foot .next .scan::after {
    content: ""; position: absolute; top: 0; left: 0;
    width: 100%; height: 100%;
    background: linear-gradient(90deg, transparent, rgba(51,54,60,0.55), transparent);
    transform: translateX(-100%);
    animation: hub-scan 2.4s linear infinite;
  }
  @keyframes hub-scan { to { transform: translateX(100%); } }

  /* Count-up + gradient treatment for main-page KPIs */
  .hub-kpi .v.grad,
  .hub-kpi .mini-value.grad {
    color: var(--ink);
  }

  @media (max-width: 1000px) {
    .hub-stream-grid,
    .hub-stream-row { grid-template-columns: minmax(0, 1.4fr) 1fr 110px 60px; }
    .hub-stream-grid .col-slot,
    .hub-stream-row .slot { grid-column: 2; }
    .hub-stream-grid .col-lanes,
    .hub-stream-row .lanes { grid-column: 1 / -1; order: 5; }
  }
  @media (max-width: 720px) {
    .hub-stream { padding: 18px; }
    .hub-stream-grid { display: none; }
    .hub-stream-row {
      grid-template-columns: 1fr auto;
      row-gap: 8px;
    }
    .hub-stream-row .slot { grid-column: 2; text-align: right; }
    .hub-stream-row .adv,
    .hub-stream-row .pill { justify-self: end; }
  }

  
  /* Opacity-only fade — keeps layout static when each cell refreshes. */
  @keyframes hub-slotact-in {
    from { opacity: 0; }
    to   { opacity: 1; }
  }
  
  .hub-slotact.faster { border-left: 3px solid rgba(100,226,127,0.65); }
  .hub-slotact.slower { border-left: 3px solid rgba(254,161,88,0.65); }
  .hub-slotact.equal,
  .hub-slotact.na,
  .hub-slotact.onboarding { border-left: 3px solid rgba(176,190,209,0.45); }

  
  .hub-slotact .pct-tag {
    display: inline-flex;
    align-items: center;
    padding: 3px 8px;
    border-radius: 6px;
    background: rgba(51,54,60,0.06);
    color: var(--ink-mute);
    font-family: 'Geist Mono', monospace;
    font-size: 10px;
    font-weight: 600;
    letter-spacing: 0.10em;
    border: 1px solid rgba(51,54,60,0.10);
  }
  .hub-root[data-theme="dark"] .hub-slotact .pct-tag {
    background: rgba(255,251,251,0.06);
    border-color: rgba(255,251,251,0.10);
    color: var(--ink-dim);
  }
  .hub-slotact .ago {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    font-size: 10.5px;
    color: var(--ink-mute);
    letter-spacing: 0.06em;
    font-variant-numeric: tabular-nums;
    flex: none;
  }
  .hub-slotact .ago .dot {
    position: relative;
    width: 6px; height: 6px;
    border-radius: 50%;
    background: #64E27F;
    box-shadow: 0 0 8px rgba(100,226,127,0.65);
  }
  .hub-slotact .ago .dot::after {
    content: ""; position: absolute; inset: -3px;
    border-radius: 50%;
    border: 1.4px solid rgba(100,226,127,0.55);
    animation: vpub-pulse 1.8s ease-out infinite;
  }
  .hub-slotact.slower .ago .dot { background: #FEA158; box-shadow: 0 0 8px rgba(254,161,88,0.55); }
  .hub-slotact.slower .ago .dot::after { border-color: rgba(254,161,88,0.55); }
  .hub-slotact.equal .ago .dot,
  .hub-slotact.na .ago .dot,
  .hub-slotact.onboarding .ago .dot {
    background: #B0BED1; box-shadow: none;
  }
  .hub-slotact.equal .ago .dot::after,
  .hub-slotact.na .ago .dot::after,
  .hub-slotact.onboarding .ago .dot::after { display: none; }
  .hub-slotact .slot {
    font-size: 10px;
    color: var(--ink-dim);
    letter-spacing: 0.16em;
    text-transform: uppercase;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .hub-slotact .slot b {
    color: var(--ink);
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    letter-spacing: 0;
    margin-left: 4px;
    font-size: 12px;
  }
  .hub-slotact .pill {
    padding: 3px 9px;
    border-radius: 999px;
    font-family: 'Geist Mono', monospace;
    font-size: 9.5px;
    letter-spacing: 0.14em;
    font-weight: 600;
    border: 1px solid transparent;
    white-space: nowrap;
    flex: none;
  }
  .hub-slotact .pill.faster {
    background: rgba(100,226,127,0.16);
    color: #1F7A3A;
    border-color: rgba(100,226,127,0.36);
  }
  .hub-slotact .pill.slower {
    background: rgba(254,161,88,0.14);
    color: #B25F1A;
    border-color: rgba(254,161,88,0.30);
  }
  .hub-slotact .pill.equal {
    background: rgba(176,190,209,0.20);
    color: var(--ink-mute);
    border-color: rgba(176,190,209,0.34);
  }
  .hub-slotact .pill.na {
    background: rgba(176,190,209,0.14);
    color: var(--ink-dim);
    border-color: rgba(176,190,209,0.24);
  }
  .hub-slotact.onboarding .pill {
    background: rgba(184,124,255,0.14);
    color: #6F35C2;
    border-color: rgba(184,124,255,0.32);
  }

  
  .hub-slotact .lane {
    display: flex;
    align-items: center;
    gap: 8px;
    min-width: 0;
    flex: 1 1 0;
  }
  .hub-slotact .lane i {
    font-style: normal;
    font-family: 'Geist Mono', monospace;
    font-size: 11px;
    letter-spacing: 0.02em;
    color: var(--ink-dim);
    font-weight: 500;
    white-space: nowrap;
    flex: none;
    width: 46px;
  }
  .hub-slotact .lane.mum i { color: var(--ink); }
  .hub-slotact .lane.lib i { color: #B25F1A; }
  .hub-slotact .lane .barwrap {
    position: relative;
    height: 4px;
    border-radius: 999px;
    background: rgba(176,190,209,0.20);
    overflow: hidden;
    flex: 1 1 auto;
    min-width: 24px;
  }
  .hub-slotact .lane .bar {
    display: block;
    height: 100%;
    width: 0;
    border-radius: inherit;
    animation: vpub-fill 700ms cubic-bezier(.2,.7,.2,1) forwards;
  }
  .hub-slotact .lane.mum .bar {
    background: linear-gradient(90deg, rgba(51,54,60,0.45), var(--carbon));
  }
  .hub-slotact .lane.lib .bar {
    background: linear-gradient(90deg, rgba(254,161,88,0.45), var(--peach));
  }
  .hub-slotact .lane b {
    font-family: 'Geist Mono', monospace;
    font-size: 11px;
    color: var(--ink);
    font-variant-numeric: tabular-nums;
    text-align: right;
    font-weight: 500;
    white-space: nowrap;
    flex: none;
  }
  .hub-root[data-theme="dark"] .hub-slotact .lane.mum i { color: var(--alabaster); }
  .hub-root[data-theme="dark"] .hub-slotact .lane.lib i { color: #FEC79C; }

  
  .hub-slotact-adv .adv-val {
    font-size: 15px;
    font-weight: 500;
    font-variant-numeric: tabular-nums;
    letter-spacing: 0;
  }
  .hub-slotact-adv .adv-sub {
    font-size: 9.5px;
    font-weight: 400;
    color: var(--ink-dim);
    letter-spacing: 0.06em;
    text-transform: uppercase;
  }
  .hub-slotact.faster .hub-slotact-adv .adv-val { color: #1F7A3A; }
  .hub-slotact.faster .hub-slotact-adv .adv-sub { color: #2A8847; }
  .hub-slotact.slower .hub-slotact-adv .adv-val { color: #B25F1A; }
  .hub-slotact.slower .hub-slotact-adv .adv-sub { color: #C76A2A; }
  .hub-slotact.equal .hub-slotact-adv .adv-val,
  .hub-slotact.na .hub-slotact-adv .adv-val,
  .hub-slotact.onboarding .hub-slotact-adv .adv-val { color: var(--ink-mute); }
  .hub-slotact.equal .hub-slotact-adv .adv-sub,
  .hub-slotact.na .hub-slotact-adv .adv-sub,
  .hub-slotact.onboarding .hub-slotact-adv .adv-sub { color: var(--ink-dim); }

  
  
  .hub-root[data-theme="dark"] .hub-slotact .pill.faster { background: rgba(100,226,127,0.20); color: #9BE9AC; border-color: rgba(100,226,127,0.40); }
  .hub-root[data-theme="dark"] .hub-slotact .pill.slower { background: rgba(254,161,88,0.20); color: #FEC79C; border-color: rgba(254,161,88,0.36); }
  .hub-root[data-theme="dark"] .hub-slotact.onboarding .pill { background: rgba(184,124,255,0.22); color: #D4B0FF; border-color: rgba(184,124,255,0.40); }
  .hub-root[data-theme="dark"] .hub-slotact.faster .hub-slotact-adv .adv-val { color: #9BE9AC; }
  .hub-root[data-theme="dark"] .hub-slotact.faster .hub-slotact-adv .adv-sub { color: #7DD693; }
  .hub-root[data-theme="dark"] .hub-slotact.slower .hub-slotact-adv .adv-val { color: #FEC79C; }
  .hub-root[data-theme="dark"] .hub-slotact.slower .hub-slotact-adv .adv-sub { color: #E6A878; }
  .hub-root[data-theme="dark"] .hub-slotact .lane .barwrap { background: rgba(255,251,251,0.10); }
  .hub-root[data-theme="dark"] .hub-slotact .lane.mum .bar { background: linear-gradient(90deg, rgba(255,251,251,0.30), var(--alabaster)); }

  
  @media (max-width: 880px) {
    
    .hub-slotact .lane { width: 100%; }
  }

  /* ════════════════════════════════════════════════════════════════
     FLEET OBSERVABILITY — hero, epoch, rail, heatmap, leaderboard, table
     ════════════════════════════════════════════════════════════════ */

  /* Heat color tokens — share for rail + heatmap + epoch ticks */
  .hub-root {
    --heat-faster-strong: rgba(100,226,127,0.95);
    --heat-faster-mid:    rgba(100,226,127,0.62);
    --heat-faster-soft:   rgba(100,226,127,0.32);
    --heat-equal:         rgba(157,176,194,0.32);
    --heat-slower-soft:   rgba(254,161,88,0.50);
    --heat-slower-strong: rgba(254,72,72,0.78);
    --heat-na:            rgba(157,176,194,0.18);
  }
  .hub-root[data-theme="dark"] {
    --heat-faster-strong: rgba(100,226,127,0.95);
    --heat-faster-mid:    rgba(100,226,127,0.65);
    --heat-faster-soft:   rgba(100,226,127,0.30);
    --heat-equal:         rgba(157,176,194,0.22);
    --heat-slower-soft:   rgba(254,161,88,0.55);
    --heat-slower-strong: rgba(254,72,72,0.78);
    --heat-na:            rgba(255,251,251,0.07);
  }

  /* Window selector / controls row */
  .hub-fleet-controls {
    display: flex; align-items: center; justify-content: flex-end;
    gap: 16px; margin: 6px 0 18px;
    padding: 10px 14px; border-radius: 12px;
    background: var(--bg-card); border: 1px solid var(--line);
    flex-wrap: wrap;
    position: relative; z-index: 30; /* keep the cadence "?" downward tooltip above content below */
  }
  .hub-fleet-controls-group, .hub-fleet-controls-meta {
    display: flex; align-items: center; gap: 8px;
  }
  .hub-fleet-controls .lbl {
    font-family: 'Geist Mono', ui-monospace, monospace;
    font-size: 10px; letter-spacing: 0.08em; text-transform: uppercase;
    color: var(--ink-dim);
  }
  .hub-fleet-time {
    font-family: 'Geist Mono', ui-monospace, monospace;
    font-size: 12px;
    line-height: 1;
    letter-spacing: 0.04em;
    color: var(--ink-mute);
    font-weight: 500;
    font-variant-numeric: tabular-nums;
  }
  .hub-fleet-chip {
    border: 1px solid var(--line-strong); background: transparent;
    color: var(--ink-mute);
    padding: 6px 12px; border-radius: 999px; font-size: 12px; font-weight: 500;
    cursor: pointer; transition: all .15s ease;
    font-family: 'Geist', system-ui, sans-serif;
  }
  .hub-fleet-chip:hover:not(.disabled) { background: var(--bg-card-2); color: var(--ink); }
  .hub-fleet-chip.on { background: var(--ink); color: var(--bg-card); border-color: var(--ink); }
  .hub-fleet-chip.disabled { opacity: 0.4; cursor: not-allowed; }
  .hub-fleet-refresh {
    display: inline-flex; align-items: center; gap: 6px;
    padding: 6px 10px; border-radius: 999px;
    background: var(--bg-card-2); border: 1px solid var(--line);
    color: var(--ink-mute); cursor: pointer; font-size: 12px;
  }
  .hub-fleet-refresh:hover { color: var(--ink); }
  .hub-fleet-refresh:active svg { transform: rotate(180deg); }
  .hub-fleet-refresh svg { transition: transform .35s ease; }

  
  
  
  
  
  
  
  
  
  .hub-fleet-hero-sub .dot { margin: 0 8px; color: var(--ink-faint); }
  
  .hub-fleet-hero-pill .dot {
    width: 6px; height: 6px; border-radius: 50%; background: var(--mint);
    box-shadow: 0 0 8px rgba(100,226,127,0.55);
    animation: hub-fleet-pulse 2.2s ease-in-out infinite;
  }
  @keyframes hub-fleet-pulse {
    0%, 100% { opacity: 1; transform: scale(1); }
    50% { opacity: 0.55; transform: scale(0.85); }
  }
  
  
  .hub-fleet-hero-stat .k {
    font-family: 'Geist Mono', ui-monospace, monospace;
    font-size: 10px; letter-spacing: 0.08em; text-transform: uppercase;
    color: var(--ink-dim);
  }
  .hub-fleet-hero-stat .v {
    margin-top: 8px;
    font-size: 22px; font-weight: 600; letter-spacing: -0.015em;
    color: var(--ink);
    font-family: 'Geist Mono', ui-monospace, monospace;
    line-height: 1.05;
  }
  .hub-fleet-hero-stat .d {
    margin-top: 8px;
    font-size: 11px; color: var(--ink-mute);
  }
  .hub-fleet-hero-stat .tone-good { color: var(--mint); }
  .hub-fleet-hero-stat .tone-warn { color: var(--peach); }
  .hub-fleet-hero-stat .tone-bad  { color: #FE6262; }
  .hub-fleet-hero-stat .tone-muted{ color: var(--ink-mute); }

  /* Flip digit animation — only animates when char changes */
  .hub-fleet-flip-row { display: inline-flex; }
  .hub-fleet-flip {
    position: relative; display: inline-block;
    min-width: 0.55ch;
    overflow: hidden;
    vertical-align: bottom;
  }
  .hub-fleet-flip-now { display: inline-block; }
  .hub-fleet-flip-prev { display: none; }
  .hub-fleet-flip.flipping .hub-fleet-flip-now {
    display: inline-block;
    animation: hub-fleet-flip-in .32s ease both;
  }
  @keyframes hub-fleet-flip-in {
    0%   { transform: translateY(-100%); opacity: 0; }
    60%  { transform: translateY(8%);    opacity: 1; }
    100% { transform: translateY(0);     opacity: 1; }
  }

  /* ── 2. Epoch progress ───────────────────────────────────────── */
  .hub-fleet-epoch {
    background: var(--bg-card); border: 1px solid var(--line);
    border-radius: 12px; padding: 14px 16px; margin-bottom: 14px;
  }
  .hub-fleet-epoch-head {
    display: flex; align-items: baseline; gap: 14px; margin-bottom: 10px; flex-wrap: wrap;
  }
  .hub-fleet-epoch-head .lbl {
    font-family: 'Geist Mono', ui-monospace, monospace;
    font-size: 10px; letter-spacing: 0.10em; text-transform: uppercase; color: var(--ink-dim);
  }
  .hub-fleet-epoch-head .rng { font-size: 12px; color: var(--ink-mute); }
  .hub-fleet-epoch-head .cnt { font-size: 12px; color: var(--ink); margin-left: auto; }
  .hub-fleet-epoch-head .cnt b { font-weight: 600; }
  .hub-fleet-epoch-ticks {
    display: grid; gap: 4px;
    grid-template-columns: repeat(32, 1fr);
  }
  .hub-fleet-epoch-ticks .tick {
    height: 10px; border-radius: 3px;
    background: var(--heat-na);
    transition: background .25s ease, transform .25s ease;
    animation: hub-fleet-tick-in .35s ease both;
    animation-delay: calc(var(--i, 0) * 12ms);
  }
  .hub-fleet-epoch-ticks .tick.empty { background: transparent; border: 1px dashed var(--line); }
  .hub-fleet-epoch-ticks .tick.faster-strong { background: var(--heat-faster-strong); }
  .hub-fleet-epoch-ticks .tick.faster-mid    { background: var(--heat-faster-mid); }
  .hub-fleet-epoch-ticks .tick.faster-soft   { background: var(--heat-faster-soft); }
  .hub-fleet-epoch-ticks .tick.equal         { background: var(--heat-equal); }
  .hub-fleet-epoch-ticks .tick.slower-soft   { background: var(--heat-slower-soft); }
  .hub-fleet-epoch-ticks .tick.slower-strong { background: var(--heat-slower-strong); }
  .hub-fleet-epoch-ticks .tick.na            { background: var(--heat-na); }
  @keyframes hub-fleet-tick-in {
    from { opacity: 0; transform: translateY(4px); }
    to   { opacity: 1; transform: translateY(0); }
  }

  /* ── 3. Split-flap metric strip ──────────────────────────────── */
  .hub-fleet-flap-strip {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
    gap: 1px;
    background: var(--line);
    border: 1px solid var(--line); border-radius: 12px;
    overflow: hidden; margin-bottom: 16px;
  }
  .hub-fleet-flap-strip .seg {
    background: linear-gradient(180deg, var(--bg-card), var(--bg-card-2));
    padding: 12px 14px;
  }
  .hub-fleet-flap-strip .lbl {
    font-family: 'Geist Mono', ui-monospace, monospace;
    font-size: 9.5px; letter-spacing: 0.10em; text-transform: uppercase; color: var(--ink-dim);
  }
  .hub-fleet-flap-strip .val {
    margin-top: 6px;
    font-size: 20px; font-weight: 600; letter-spacing: -0.01em;
    color: var(--ink);
    text-shadow: 0 1px 0 rgba(0,0,0,0.04);
  }
  .hub-fleet-flap-strip .val.good { color: var(--mint); }
  .hub-fleet-flap-strip .val.warn { color: var(--peach); }
  .hub-fleet-flap-strip .sub { font-size: 11px; color: var(--ink-mute); margin-top: 4px; }

  /* ── 4. Last 32 slots rail ───────────────────────────────────── */
  .hub-fleet-rail {
    background: var(--bg-card); border: 1px solid var(--line);
    border-radius: 12px; padding: 14px 16px; margin-bottom: 14px;
    position: relative;
  }
  .hub-fleet-rail-head {
    display: flex; align-items: flex-start; justify-content: space-between; gap: 16px;
    margin-bottom: 12px;
  }
  .hub-fleet-rail-head h3 {
    margin: 0; font-size: 14px; font-weight: 600; color: var(--ink); letter-spacing: -0.01em;
  }
  .hub-fleet-rail-head .sub, .hub-fleet-heat-head .sub {
    margin-top: 4px; font-size: 12px; color: var(--ink-mute);
  }
  .hub-fleet-rail-legend {
    display: flex; gap: 14px; align-items: center;
    font-size: 11px; color: var(--ink-mute);
  }
  .hub-fleet-rail-legend i.dot {
    display: inline-block; width: 9px; height: 9px; border-radius: 3px;
    margin-right: 5px; vertical-align: -1px;
  }
  .hub-fleet-rail-legend .faster { background: var(--heat-faster-strong); }
  .hub-fleet-rail-legend .equal  { background: var(--heat-equal); }
  .hub-fleet-rail-legend .slower { background: var(--heat-slower-strong); }
  .hub-fleet-rail-legend .na     { background: var(--heat-na); }
  .hub-fleet-rail-track {
    display: grid; gap: 4px;
    grid-template-columns: repeat(32, 1fr);
  }
  .hub-fleet-rail-track .rail-cell {
    height: 32px; border-radius: 5px; border: 0;
    cursor: pointer; padding: 0;
    background: var(--heat-na);
    transition: transform .15s ease, box-shadow .15s ease;
    animation: hub-fleet-slot-enter .4s ease both;
  }
  @keyframes hub-fleet-slot-enter {
    from { opacity: 0; transform: translateX(8px); }
    to   { opacity: 1; transform: translateX(0); }
  }
  .hub-fleet-rail-track .rail-cell:hover,
  .hub-fleet-rail-track .rail-cell.on {
    transform: translateY(-2px); box-shadow: 0 6px 14px rgba(0,0,0,0.10);
  }
  .hub-fleet-rail-track .rail-cell.faster-strong { background: var(--heat-faster-strong); }
  .hub-fleet-rail-track .rail-cell.faster-mid    { background: var(--heat-faster-mid); }
  .hub-fleet-rail-track .rail-cell.faster-soft   { background: var(--heat-faster-soft); }
  .hub-fleet-rail-track .rail-cell.equal         { background: var(--heat-equal); }
  .hub-fleet-rail-track .rail-cell.slower-soft   { background: var(--heat-slower-soft); }
  .hub-fleet-rail-track .rail-cell.slower-strong { background: var(--heat-slower-strong); }
  .hub-fleet-rail-track .rail-cell.na            { background: var(--heat-na); }
  .hub-fleet-rail-tip {
    margin-top: 10px; padding: 8px 10px;
    background: var(--bg-card-2); border: 1px dashed var(--line); border-radius: 8px;
    font-size: 12px; color: var(--ink-mute); min-height: 32px;
    display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 4px 16px;
    opacity: 0.6; transition: opacity .15s ease, border-color .15s ease;
  }
  .hub-fleet-rail-tip.show { opacity: 1; border-style: solid; }
  .hub-fleet-rail-tip .row {
    display: flex; gap: 8px; justify-content: space-between;
  }
  .hub-fleet-rail-tip .row b { color: var(--ink); font-weight: 600; }
  .hub-fleet-rail-tip .hint { color: var(--ink-dim); }

  
  
  
  
  
  .hub-fleet-heat-row .rowlbl {
    display: flex; align-items: center; gap: 8px;
    min-width: 0; font-size: 12px; color: var(--ink);
  }
  
  
  .hub-fleet-heat-fb {
    font-family: 'Geist Mono', ui-monospace, monospace;
    font-size: 9px; color: var(--ink-mute); letter-spacing: 0.04em;
  }
  .hub-fleet-heat-row .rowlbl .nm {
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }
  .hub-fleet-heat-row .cells {
    display: grid; grid-template-columns: repeat(32, 1fr); gap: 3px;
  }
  .hub-fleet-heat-row .heat {
    height: 16px; border-radius: 3px;
    background: var(--heat-na); cursor: pointer;
    transition: transform .12s ease, box-shadow .12s ease;
    animation: hub-fleet-tick-in .35s ease both;
    animation-delay: calc(var(--i, 0) * 6ms);
  }
  .hub-fleet-heat-row .heat:hover {
    transform: scale(1.25); z-index: 2;
    box-shadow: 0 0 0 1px var(--ink), 0 4px 10px rgba(0,0,0,0.15);
  }
  .hub-fleet-heat-row .heat.faster-strong { background: var(--heat-faster-strong); }
  .hub-fleet-heat-row .heat.faster-mid    { background: var(--heat-faster-mid); }
  .hub-fleet-heat-row .heat.faster-soft   { background: var(--heat-faster-soft); }
  .hub-fleet-heat-row .heat.equal         { background: var(--heat-equal); }
  .hub-fleet-heat-row .heat.slower-soft   { background: var(--heat-slower-soft); }
  .hub-fleet-heat-row .heat.slower-strong { background: var(--heat-slower-strong); }
  .hub-fleet-heat-row .heat.na            { background: var(--heat-na); }
  
  .hub-fleet-heat-tip.show { opacity: 1; border-style: solid; }
  .hub-fleet-heat-tip .row { display: flex; gap: 8px; justify-content: space-between; }
  .hub-fleet-heat-tip .row b { color: var(--ink); font-weight: 600; }
  .hub-fleet-heat-tip .hint { color: var(--ink-dim); }

  /* ── 7. Leaderboard cards ────────────────────────────────────── */
  .hub-fleet-leader-grid {
    display: grid; gap: 14px; margin-bottom: 14px;
    grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  }
  .hub-fleet-lead {
    background: var(--bg-card); border: 1px solid var(--line);
    border-radius: 12px; padding: 14px 16px;
  }
  .hub-fleet-lead.warn { border-color: rgba(254,161,88,0.30); }
  .hub-fleet-lead.good { border-color: rgba(100,226,127,0.25); }
  .hub-fleet-lead-head { margin-bottom: 10px; }
  .hub-fleet-lead-head h4 { margin: 0; font-size: 13px; font-weight: 600; color: var(--ink); }
  .hub-fleet-lead-head .sub { display: block; font-size: 11px; color: var(--ink-mute); margin-top: 3px; }
  .hub-fleet-lead-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; }
  .hub-fleet-lead-list li {
    display: grid; grid-template-columns: 20px 22px 1fr auto; gap: 8px; align-items: center;
    padding: 6px 6px; border-radius: 8px;
    transition: background .15s ease;
  }
  .hub-fleet-lead-list li:hover { background: var(--bg-card-2); }
  .hub-fleet-lead-list .rank {
    font-family: 'Geist Mono', ui-monospace, monospace;
    font-size: 11px; color: var(--ink-dim); text-align: center;
  }
  .hub-fleet-lead-avatar {
    width: 22px; height: 22px; border-radius: 6px;
    background: var(--bg-card-2); overflow: hidden;
    display: inline-flex; align-items: center; justify-content: center;
    border: 1px solid var(--line);
  }
  .hub-fleet-lead-avatar img { width: 100%; height: 100%; object-fit: cover; }
  .hub-fleet-lead-list .nm {
    font-size: 12.5px; color: var(--ink); font-weight: 500;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }
  .hub-fleet-lead-list .val {
    font-family: 'Geist Mono', ui-monospace, monospace;
    font-size: 12px; color: var(--ink); font-weight: 600;
  }

  /* ── 8. Fleet comparison cards ───────────────────────────────── */
  .hub-fleet-cmp {
    display: grid; gap: 8px; margin-bottom: 16px;
    grid-template-columns: repeat(auto-fit, minmax(155px, 1fr));
  }
  .hub-fleet-cmp-card {
    background: var(--bg-card); border: 1px solid var(--line);
    border-radius: 10px; padding: 10px 12px;
  }
  .hub-fleet-cmp-card .k {
    font-family: 'Geist Mono', ui-monospace, monospace;
    font-size: 9.5px; letter-spacing: 0.10em; text-transform: uppercase;
    color: var(--ink-dim);
  }
  .hub-fleet-cmp-card .v {
    margin-top: 4px; font-size: 17px; font-weight: 600; color: var(--ink);
  }
  .hub-fleet-cmp-card.tone-good { border-color: rgba(100,226,127,0.30); }
  .hub-fleet-cmp-card.tone-good .v { color: var(--mint); }
  .hub-fleet-cmp-card.tone-warn { border-color: rgba(254,161,88,0.32); }
  .hub-fleet-cmp-card.tone-warn .v { color: var(--peach); }
  .hub-fleet-cmp-card.tone-info { border-color: rgba(81,177,254,0.28); }
  .hub-fleet-cmp-card.tone-info .v { color: var(--azure); }
  .hub-fleet-cmp-card.tone-muted .v { color: var(--ink-mute); }

  /* ── 6. Validator ranking table ──────────────────────────────── */
  .hub-fleet-tbl-wrap {
    background: var(--bg-card); border: 1px solid var(--line);
    border-radius: 14px; padding: 14px; margin-bottom: 16px;
    display: flex; flex-direction: column; height: 100%; box-sizing: border-box;
  }
  .hub-fleet-tbl-filters {
    display: flex; gap: 10px; align-items: center; flex-wrap: wrap;
    margin-bottom: 12px;
  }
  .hub-fleet-tbl-search {
    position: relative; flex: 0 1 280px; display: flex; align-items: center;
    background: var(--bg-card-2); border: 1px solid var(--line); border-radius: 8px;
    padding: 6px 10px; gap: 8px; color: var(--ink-dim);
  }
  .hub-fleet-tbl-search input {
    border: 0; background: transparent; outline: none; flex: 1;
    color: var(--ink); font-size: 13px; font-family: inherit;
  }
  .hub-fleet-tbl-search input::placeholder { color: var(--ink-dim); }
  .hub-fleet-tbl-filters select {
    background: var(--bg-card-2); border: 1px solid var(--line); border-radius: 8px;
    padding: 6px 10px; font-size: 12px; color: var(--ink); font-family: inherit;
    cursor: pointer;
  }
  .hub-fleet-tbl-chk {
    display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: var(--ink-mute);
    padding: 6px 10px; border-radius: 8px; background: var(--bg-card-2); border: 1px solid var(--line);
    cursor: pointer;
  }
  .hub-fleet-tbl-chk input { accent-color: var(--mint); }
  .hub-fleet-tbl-count { margin-left: auto; font-size: 11px; color: var(--ink-dim); }

  .hub-fleet-tbl {
    display: grid; gap: 1px;
    background: var(--line); border-radius: 10px; overflow: hidden;
    border: 1px solid var(--line);
  }
  .hub-fleet-tbl-head, .hub-fleet-tbl-row {
    display: grid;
    grid-template-columns:
      minmax(190px, 360px)   /* partner (logo + name + id) */
      minmax(110px, 240px)   /* total staked ETH */
      minmax(130px, 280px)   /* mumP2P staked ETH */
      minmax(0, 0.9fr);      /* trailing spacer — fills wide cards without flinging the numbers to the edge */
    align-items: center;
    background: var(--bg-card);
  }
  .hub-fleet-tbl-head {
    background: var(--bg-card-2);
    font-family: 'Geist Mono', ui-monospace, monospace;
    font-size: 10px; letter-spacing: 0.08em; text-transform: uppercase;
    color: var(--ink-dim);
  }
  .hub-fleet-tbl-head .cell {
    display: flex; align-items: center; gap: 4px;
    padding: 12px 14px; min-width: 0;
  }
  .hub-fleet-tbl-head .cell.left { justify-content: flex-start; text-align: left; }
  .hub-fleet-tbl-head .cell.right { justify-content: flex-end; text-align: right; }
  .hub-fleet-tbl-head .cell.sortable { cursor: pointer; user-select: none; }
  .hub-fleet-tbl-head .cell.sortable:hover { color: var(--ink); }
  .hub-fleet-tbl-head .cell.on { color: var(--ink); }
  /* All header cells carry a title= tooltip — hint at it with a subtle
     dotted underline on hover so users know info is available. */
  .hub-fleet-tbl-head .cell[title]:not(.sortable) { cursor: help; }
  .hub-fleet-tbl-head .cell[title]:hover {
    color: var(--ink);
    text-decoration: underline dotted var(--ink-faint);
    text-underline-offset: 4px;
  }
  .hub-fleet-tbl-row {
    cursor: default; transition: background .15s ease;
  }
  .hub-fleet-tbl-row:hover { background: var(--bg-card-2); }
  .hub-fleet-tbl-row .cell {
    padding: 14px 14px;
    font-size: 13px; color: var(--ink);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
    min-width: 0;
  }
  .hub-fleet-tbl-row .cell.left  { text-align: left; }
  .hub-fleet-tbl-row .cell.right { text-align: right; }
  .hub-fleet-tbl-row .cell.partner {
    display: flex; align-items: center; gap: 12px;
    min-width: 0;
  }
  .hub-fleet-tbl-row .cell.partner .partner-text {
    display: flex; flex-direction: column; min-width: 0; line-height: 1.25;
  }
  .hub-fleet-tbl-row .cell.partner b {
    font-weight: 600; color: var(--ink); font-size: 13.5px;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }
  .hub-fleet-tbl-row .cell.partner .sub {
    font-size: 11px; color: var(--ink-mute); margin-top: 2px;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }
  .hub-fleet-tbl-row .cell.partner .id {
    color: var(--ink-dim); letter-spacing: 0.04em; margin-right: 2px;
  }
  .hub-fleet-tbl-avatar {
    width: 30px; height: 30px; border-radius: 8px;
    background: var(--bg-card-2); overflow: hidden;
    display: inline-flex; align-items: center; justify-content: center;
    border: 1px solid var(--line); flex-shrink: 0;
  }
  .hub-fleet-tbl-avatar img { width: 100%; height: 100%; object-fit: cover; }
  /* curated brand logos: contain so wide marks are never cropped (square tiles still fill) */
  .hub-fleet-tbl-avatar img.op-logo-light, .hub-fleet-tbl-avatar img.op-logo-dark { object-fit: contain; }
  /* theme-aware brand logos: show the dark-background variant only in dark mode */
  .hub-fleet-tbl-avatar img.op-logo-dark { display: none; }
  .hub-root[data-theme="dark"] .hub-fleet-tbl-avatar img.op-logo-light.has-dark { display: none; }
  .hub-root[data-theme="dark"] .hub-fleet-tbl-avatar img.op-logo-dark { display: block; }
  .hub-fleet-tbl-avatar-link { display: inline-flex; border-radius: 8px; text-decoration: none; transition: box-shadow .15s ease, transform .15s ease; }
  .hub-fleet-tbl-avatar-link:hover { box-shadow: 0 0 0 2px var(--accent, #51B1FE); transform: translateY(-1px); }
  .hub-fleet-tbl-avatar-link:focus-visible { outline: 2px solid #51B1FE; outline-offset: 2px; }
  .partner-name-link { text-decoration: none; color: inherit; }
  .partner-name-link:hover { text-decoration: underline; text-underline-offset: 2px; text-decoration-color: var(--ink-dim); }
  .hub-fleet-tbl-row .cell.tone-good { color: var(--mint); }
  .hub-fleet-tbl-row .cell.tone-warn { color: var(--peach); }
  .hub-fleet-tbl-row .cell.tone-bad  { color: #FE6262; }
  .hub-fleet-tbl-row .cell.tone-neutral { color: var(--ink-mute); }
  .hub-fleet-tbl-row .cell.tone-muted { color: var(--ink-dim); }
  .hub-fleet-tbl-row .cell.tone-na { color: var(--ink-dim); }

  /* ── Connected-operators directory (V1: names + logos only) ── */
  .hub-op-dir {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
    gap: 8px;
    padding: 12px;
  }
  .hub-op-item {
    display: flex; align-items: center; gap: 11px;
    padding: 11px 13px;
    border: 1px solid var(--line);
    border-radius: 12px;
    background: var(--bg-card);
    text-decoration: none;
    color: var(--ink);
    min-width: 0;
    transition: background .15s ease, border-color .15s ease, transform .15s ease;
  }
  .hub-op-item:hover { background: var(--bg-card-2); border-color: var(--line-strong); transform: translateY(-1px); }
  .hub-op-name {
    font-size: 13px; font-weight: 500; letter-spacing: -0.005em;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }

  /* Operator LIST variant (Overview): logo · name · connected mumP2P stake.
     One row per operator; the per-operator ETH shares sum to the fleet's
     connected total (the "MUMP2P CONNECTED" headline). */
  .hub-op-list { display: flex; flex-direction: column; padding: 6px; }
  .hub-op-row {
    display: flex; align-items: center; gap: 12px;
    padding: 10px 12px;
    border-radius: 10px;
    text-decoration: none;
    color: var(--ink);
    transition: background .15s ease;
  }
  .hub-op-row + .hub-op-row { box-shadow: 0 -1px 0 var(--op-hair); }
  .hub-op-row:hover { background: var(--bg-card-2); }
  .hub-op-row:hover + .hub-op-row { box-shadow: none; }
  .hub-op-row:focus-visible { outline: 2px solid var(--azure, #51B1FE); outline-offset: 2px; }
  .hub-op-list .hub-op-name { flex: 1 1 auto; }
  .hub-op-val {
    font-family: 'Geist Mono', monospace;
    font-size: 13px; font-weight: 600; letter-spacing: -0.01em;
    color: var(--ink); font-variant-numeric: tabular-nums; white-space: nowrap;
  }
  .hub-op-val em { font-style: normal; font-weight: 500; font-size: 10px; letter-spacing: .04em; color: var(--op-faint); margin-left: 4px; }
  /* Column headers for the operator list — names the two columns
     (operator · connected mumP2P stake). */
  .hub-op-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; padding: 4px 12px 9px; margin-bottom: 2px; border-bottom: 1px solid var(--op-hair); }
  .hub-op-head-k, .hub-op-head-v { font-family: 'Geist Mono', monospace; font-size: 9.5px; letter-spacing: .12em; text-transform: uppercase; color: var(--op-faint); }
  .hub-op-head-v { white-space: nowrap; }

  /* mumP2P KPI inside the Key metrics panel */
  .hub-globe-metrics-grid .hub-kpi-mump2p .hub-kpi-pcts {
    margin-top: 10px;
    display: grid; grid-template-columns: repeat(3, 1fr);
    gap: 8px;
    font-family: 'Geist Mono', ui-monospace, monospace;
  }
  .hub-globe-metrics-grid .hub-kpi-mump2p .hub-kpi-pcts span {
    display: flex; flex-direction: column; align-items: flex-start;
    padding: 6px 8px;
    background: var(--bg-card-2); border-radius: 8px;
    border: 1px solid var(--line);
  }
  .hub-globe-metrics-grid .hub-kpi-mump2p .hub-kpi-pcts i {
    font-style: normal; font-size: 9px; letter-spacing: 0.08em;
    text-transform: uppercase; color: var(--ink-dim);
  }
  .hub-globe-metrics-grid .hub-kpi-mump2p .hub-kpi-pcts b {
    font-size: 13px; font-weight: 600; color: var(--ink); margin-top: 2px;
  }
  .hub-globe-metrics-grid .hub-kpi-mump2p .v.tone-good { color: var(--mint); }
  .hub-globe-metrics-grid .hub-kpi-mump2p .v.tone-warn { color: var(--peach); }
  .hub-globe-metrics-grid .hub-kpi-mump2p .v.tone-bad  { color: #FE6262; }

	  /* ── Hero race card · mumP2P vs libP2P · Carbon palette ─── */
  .hub-globe-metrics-grid .hub-kpi-race {
    position: relative;
    display: flex; flex-direction: column;
    gap: 14px;
    padding: 18px 18px 16px;
    background:
      radial-gradient(640px 220px at 8% 0%, rgba(51,54,60,0.05), transparent 60%),
      radial-gradient(480px 200px at 100% 100%, rgba(157,176,194,0.18), transparent 65%),
      linear-gradient(180deg, var(--alabaster) 0%, rgba(238,242,245,0.85) 100%);
    border: 1px solid var(--line);
    box-shadow: 0 4px 18px rgba(40,44,52,0.045), 0 1px 0 rgba(255,255,255,0.7) inset;
    overflow: hidden;
  }
  /* No vertical rail — keeps the surface clean and natural */
  .hub-race-caption {
    font-family: 'Geist Mono', ui-monospace, monospace;
    font-size: 10.5px;
    letter-spacing: 0.06em;
    color: var(--ink-mute);
  }
  .hub-race-caption b {
    color: var(--ink);
    font-weight: 600;
    font-feature-settings: "tnum" 1;
  }
  .hub-globe-metrics-grid .hub-kpi-race::after {
    content: "";
    position: absolute;
    right: -54px; top: -82px;
    width: 340px; height: 216px;
    background-image: url("dashboard/assets/blob-1.png");
    background-size: contain;
    background-repeat: no-repeat;
    background-position: center;
    opacity: 0.20;
    filter: contrast(1.05) drop-shadow(0 20px 36px rgba(40,44,52,0.12));
    pointer-events: none;
    z-index: 0;
  }
  .hub-globe-metrics-grid .hub-kpi-race > * { position: relative; z-index: 1; }

	  .hub-race-headline {
	    display: flex;
	    align-items: center;
	  }
  .hub-race-big {
    font-family: 'Geist Mono', ui-monospace, monospace;
    font-size: 44px;
    font-weight: 500;
    line-height: 1;
    letter-spacing: -0.025em;
    color: var(--carbon);
    text-shadow: 0 1px 0 rgba(255,255,255,0.6);
    min-width: 96px;
  }
	  .hub-race-bars {
    display: flex; flex-direction: column;
    gap: 9px;
    padding: 12px 14px;
    border-radius: 12px;
    background: rgba(255,251,251,0.55);
    border: 1px solid var(--line);
  }
	  .hub-race-bars .lane {
	    display: grid;
	    grid-template-columns: 68px 1fr 70px;
	    align-items: center;
	    gap: 12px;
	  }
	  .hub-race-bars .lane .tag {
	    font-family: 'Geist Mono', ui-monospace, monospace;
	    font-size: 10px;
	    letter-spacing: 0.04em;
	    text-transform: none;
	    color: var(--ink-mute);
	    font-weight: 600;
	  }
  .hub-race-bars .lane.mum .tag { color: var(--carbon); }
  .hub-race-bars .lane .barwrap {
    position: relative;
    height: 9px;
    border-radius: 999px;
    background: rgba(157,176,194,0.18);
    overflow: hidden;
  }
  .hub-race-bars .lane .bar {
    display: block;
    height: 100%;
    border-radius: 999px;
    transition: width 0.6s cubic-bezier(.2,.7,.2,1);
  }
  .hub-race-bars .lane.mum .bar {
    background: linear-gradient(90deg, var(--platinum) 0%, var(--carbon) 100%);
  }
  .hub-race-bars .lane.lib .bar {
    background: linear-gradient(90deg, var(--steel) 0%, var(--platinum) 100%);
  }
  .hub-race-bars .lane .ms {
    font-size: 12px;
    color: var(--ink);
    text-align: right;
    font-weight: 500;
    font-feature-settings: "tnum" 1;
  }

  .hub-race-footer {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 0;
    padding-top: 4px;
  }
  .hub-race-footer span {
    display: flex; flex-direction: column;
    gap: 4px;
    padding: 4px 0 4px 14px;
    border-left: 1px solid var(--line);
    min-width: 0;
  }
  .hub-race-footer span:first-child {
    border-left: none;
    padding-left: 0;
  }
  .hub-race-footer i {
    font-style: normal;
    font-family: 'Geist Mono', ui-monospace, monospace;
    font-size: 9.5px;
    letter-spacing: 0.10em;
    text-transform: uppercase;
    color: var(--ink-dim);
    white-space: nowrap;
  }
  .hub-race-footer b {
    font-family: 'Geist Mono', ui-monospace, monospace;
    font-size: 14px;
    font-weight: 600;
    color: var(--ink);
    letter-spacing: -0.01em;
    font-feature-settings: "tnum" 1;
  }
  .hub-race-footer b.val.tone-good { color: var(--carbon); }
  .hub-race-footer b.val.tone-warn { color: var(--ink-mute); }
  .hub-race-footer b.val.tone-bad  { color: var(--ink-mute); }

  /* ── Premium connected-stake card ───────────────────────── */
  .hub-globe-metrics-grid .hub-kpi-stake {
    position: relative;
    background:
      radial-gradient(540px 220px at 100% 0%, rgba(157,176,194,0.14), transparent 60%),
      radial-gradient(420px 200px at 0% 100%, rgba(51,54,60,0.04), transparent 65%),
      linear-gradient(180deg, var(--alabaster) 0%, rgba(241,245,248,0.92) 100%);
    border: 1px solid var(--line);
    padding: 20px 22px;
    display: flex;
    flex-direction: column;
    gap: 16px;
    overflow: hidden;
  }
  /* No vertical rail — keeps the surface clean and natural */
  .hub-globe-metrics-grid .hub-kpi-stake::after {
    content: "";
    position: absolute;
    right: 12px; top: -10px;
    width: 246px; height: 206px;
    background-image: url("dashboard/assets/blob-2.png");
    background-size: contain;
    background-repeat: no-repeat;
    background-position: center;
    opacity: 0.52;
    filter: drop-shadow(0 18px 28px rgba(40,44,52,0.13));
    pointer-events: none;
    z-index: 0;
  }
  .hub-globe-metrics-grid .hub-kpi-stake > * { position: relative; z-index: 1; }

  .hub-stake-hero {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 18px;
    align-items: end;
    width: min(470px, 100%);
  }
  .hub-stake-hero-cell {
    display: flex;
    flex-direction: column;
    gap: 6px;
    min-width: 0;
    padding-right: 18px;
    border-right: 1px dashed rgba(40,44,52,0.10);
  }
  .hub-stake-hero-cell:last-child { border-right: none; padding-right: 0; }
  .hub-stake-hero-cell .mini-label {
    font-family: 'Geist Mono', ui-monospace, monospace;
    font-size: 10px;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--ink-dim);
    min-height: 0;
  }
  .hub-stake-hero-cell .mini-value {
    font-size: 34px;
    line-height: 1;
    font-weight: 500;
    color: var(--ink);
    letter-spacing: -0.025em;
    font-feature-settings: "tnum" 1;
    overflow-wrap: anywhere;
  }
  .hub-stake-hero-cell .mini-value.unavailable {
    font-size: 18px;
    line-height: 1.1;
    color: var(--ink-mute);
  }
  .hub-stake-hero-cell .mini-sub {
    margin-top: 2px;
    font-size: 10.5px;
    color: var(--ink-mute);
    letter-spacing: 0.02em;
  }

  /* Distribution bar (top partners by stake share) */
  .hub-stake-dist {
    display: flex;
    flex-direction: column;
    gap: 10px;
    padding: 12px 14px;
    border-radius: 12px;
    background: rgba(255,251,251,0.55);
    border: 1px solid var(--line);
  }
  .hub-stake-dist-bar {
    display: flex;
    height: 10px;
    border-radius: 999px;
    overflow: hidden;
    background: rgba(157,176,194,0.18);
    gap: 2px;
  }
  .hub-stake-dist-bar .seg {
    display: block;
    height: 100%;
    transition: width .6s cubic-bezier(.2,.7,.2,1);
    cursor: help;
  }
  .hub-stake-dist-bar .seg-0 { background: var(--carbon); }
  .hub-stake-dist-bar .seg-1 { background: var(--ink-mute); }
  .hub-stake-dist-bar .seg-2 { background: #7C8896; }
  .hub-stake-dist-bar .seg-3 { background: var(--platinum); }
  .hub-stake-dist-bar .seg-4 { background: var(--plutonium); }
  .hub-stake-dist-bar .seg-rest { background: rgba(157,176,194,0.55); }

  .hub-stake-dist-legend {
    display: flex;
    flex-wrap: wrap;
    gap: 14px;
    font-size: 10.5px;
    color: var(--ink-mute);
  }
  .hub-stake-dist-legend .lg {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    line-height: 1;
  }
  .hub-stake-dist-legend .sw {
    width: 8px; height: 8px;
    border-radius: 2px;
    display: inline-block;
  }
  .hub-stake-dist-legend .sw.seg-0 { background: var(--carbon); }
  .hub-stake-dist-legend .sw.seg-1 { background: var(--ink-mute); }
  .hub-stake-dist-legend .sw.seg-2 { background: #7C8896; }
  .hub-stake-dist-legend .sw.seg-3 { background: var(--platinum); }
  .hub-stake-dist-legend .sw.seg-4 { background: var(--plutonium); }
  .hub-stake-dist-legend .sw.seg-rest { background: rgba(157,176,194,0.55); }
  .hub-stake-dist-legend b {
    color: var(--ink);
    font-weight: 600;
    letter-spacing: 0.04em;
  }
  .hub-stake-dist-legend em {
    color: var(--ink-mute);
    font-style: normal;
  }

  .hub-stake-foot {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
    font-size: 11px;
    color: var(--ink-mute);
    padding-top: 4px;
  }
  .hub-stake-foot b {
    color: var(--ink);
    font-weight: 600;
    font-feature-settings: "tnum" 1;
  }
  .hub-stake-foot .dot { color: var(--ink-faint); }
  .hub-stake-foot .src {
    margin-left: auto;
    font-size: 10.5px;
    color: var(--ink-dim);
    letter-spacing: 0.02em;
  }

	  /* Dark theme overrides for the race + stake hero cards */
  .hub-root[data-theme="dark"] .hub-globe-metrics-grid .hub-kpi-race {
    background:
      radial-gradient(640px 220px at 8% 0%, rgba(255,251,251,0.04), transparent 60%),
      radial-gradient(480px 200px at 100% 100%, rgba(157,176,194,0.08), transparent 65%),
      linear-gradient(180deg, #2A2E37 0%, #232730 100%);
	  }
	  .hub-root[data-theme="dark"] .hub-globe-metrics-grid .hub-kpi-race::after {
	    opacity: 0.32;
	    filter: grayscale(1) invert(1) brightness(1.6) contrast(1.05) drop-shadow(0 20px 36px rgba(0,0,0,0.32));
	  }
  .hub-root[data-theme="dark"] .hub-race-big { color: var(--alabaster); }
  .hub-root[data-theme="dark"] .hub-race-bars {
    background: rgba(255,251,251,0.04);
    border-color: rgba(255,251,251,0.07);
  }
  .hub-root[data-theme="dark"] .hub-race-bars .lane .barwrap {
    background: rgba(255,251,251,0.10);
  }
  .hub-root[data-theme="dark"] .hub-race-bars .lane.mum .bar {
    background: linear-gradient(90deg, var(--platinum) 0%, var(--alabaster) 100%);
  }
  .hub-root[data-theme="dark"] .hub-race-bars .lane.lib .bar {
    background: linear-gradient(90deg, var(--carbon) 0%, var(--platinum) 100%);
  }
  .hub-root[data-theme="dark"] .hub-race-bars .lane.mum .tag { color: var(--alabaster); }
  .hub-root[data-theme="dark"] .hub-race-footer b.val.tone-good { color: var(--alabaster); }
  .hub-root[data-theme="dark"] .hub-globe-metrics-grid .hub-kpi-stake {
    background:
      radial-gradient(540px 220px at 100% 0%, rgba(157,176,194,0.10), transparent 60%),
      linear-gradient(180deg, #2A2E37 0%, #232730 100%);
  }
  .hub-root[data-theme="dark"] .hub-globe-metrics-grid .hub-kpi-stake::after {
    opacity: 0.72;
    filter: drop-shadow(0 18px 30px rgba(0,0,0,0.34));
  }
  .hub-root[data-theme="dark"] .hub-stake-hero-cell {
    border-right-color: rgba(255,251,251,0.10);
  }
  .hub-root[data-theme="dark"] .hub-stake-dist {
    background: rgba(255,251,251,0.04);
    border-color: rgba(255,251,251,0.07);
  }
  .hub-root[data-theme="dark"] .hub-stake-dist-bar { background: rgba(255,251,251,0.10); }
  .hub-root[data-theme="dark"] .hub-stake-dist-bar .seg-0 { background: var(--alabaster); }
  .hub-root[data-theme="dark"] .hub-stake-dist-bar .seg-1 { background: var(--plutonium); }
  .hub-root[data-theme="dark"] .hub-stake-dist-bar .seg-2 { background: var(--platinum); }
  .hub-root[data-theme="dark"] .hub-stake-dist-bar .seg-3 { background: rgba(157,176,194,0.65); }
  .hub-root[data-theme="dark"] .hub-stake-dist-bar .seg-4 { background: rgba(157,176,194,0.45); }
  .hub-root[data-theme="dark"] .hub-stake-dist-bar .seg-rest { background: rgba(255,251,251,0.20); }
  .hub-root[data-theme="dark"] .hub-stake-dist-legend .sw.seg-0 { background: var(--alabaster); }
  .hub-root[data-theme="dark"] .hub-stake-dist-legend .sw.seg-1 { background: var(--plutonium); }
  .hub-root[data-theme="dark"] .hub-stake-dist-legend .sw.seg-2 { background: var(--platinum); }
  .hub-root[data-theme="dark"] .hub-stake-dist-legend .sw.seg-3 { background: rgba(157,176,194,0.65); }
  .hub-root[data-theme="dark"] .hub-stake-dist-legend .sw.seg-4 { background: rgba(157,176,194,0.45); }
  .hub-root[data-theme="dark"] .hub-stake-dist-legend .sw.seg-rest { background: rgba(255,251,251,0.20); }

  /* Race card compact layout */
  @media (max-width: 760px) {
    .hub-globe-metrics-grid .hub-kpi-race { padding: 16px 14px 14px; }
    .hub-globe-metrics-grid .hub-kpi-race::after {
      right: -70px; top: -72px;
      width: 280px; height: 178px;
      opacity: 0.19;
    }
	    .hub-race-headline {
	      align-items: flex-start;
	    }
	    .hub-race-big { font-size: 36px; min-width: 0; }
	    .hub-race-footer { grid-template-columns: repeat(2, 1fr); row-gap: 10px; }
    .hub-race-footer span:nth-child(3) { border-left: none; padding-left: 0; }
  }

  /* Compact vs-fleet chip */
  .hub-fleet-vs-chip {
    display: inline-flex; align-items: center; gap: 4px;
    padding: 3px 9px; border-radius: 999px;
    font-family: 'Geist Mono', ui-monospace, monospace;
    font-size: 11px; font-weight: 600;
    background: var(--bg-card-2); border: 1px solid var(--line);
    color: var(--ink-mute);
  }
  .hub-fleet-vs-chip.tone-good { color: var(--mint);  background: rgba(100,226,127,0.10); border-color: rgba(100,226,127,0.28); }
  .hub-fleet-vs-chip.tone-warn { color: var(--peach); background: rgba(254,161,88,0.10);  border-color: rgba(254,161,88,0.30); }
  .hub-fleet-vs-chip.tone-bad  { color: #FE6262;     background: rgba(254,72,72,0.08);   border-color: rgba(254,72,72,0.28); }
  .hub-fleet-vs-chip.tone-neutral { color: var(--ink-mute); }

  .hub-fleet-status {
    display: inline-flex; align-items: center; gap: 6px;
    padding: 3px 9px; border-radius: 999px;
    font-family: 'Geist Mono', ui-monospace, monospace;
    font-size: 10px; letter-spacing: 0.08em; text-transform: uppercase;
    background: var(--bg-card-2); border: 1px solid var(--line); color: var(--ink-mute);
  }
  .hub-fleet-status::before {
    content: ''; width: 6px; height: 6px; border-radius: 50%; background: currentColor;
  }
  .hub-fleet-status-good  { color: #1F7A3A;       background: rgba(100,226,127,0.14); border-color: rgba(31,122,58,0.30); }
  .hub-fleet-status-warn  { color: var(--peach);  background: rgba(254,161,88,0.10);  border-color: rgba(254,161,88,0.30); }
  .hub-fleet-status-bad   { color: #FE6262;       background: rgba(254,72,72,0.08);   border-color: rgba(254,72,72,0.28); }
  .hub-fleet-status-info  { color: var(--azure);  background: rgba(81,177,254,0.10);  border-color: rgba(81,177,254,0.30); }
  .hub-fleet-status-muted { color: var(--ink-mute); }
  .hub-root[data-theme="dark"] .hub-fleet-status-good {
    color: #9BE9AC;
    background: rgba(100,226,127,0.18);
    border-color: rgba(100,226,127,0.36);
  }

  .hub-fleet-tbl-empty {
    padding: 24px; text-align: center; color: var(--ink-mute); font-size: 13px;
    background: var(--bg-card);
  }
  .hub-fleet-tbl-foot {
    margin-top: auto; padding-top: 12px; font-size: 11px; color: var(--ink-dim);
    display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap;
  }
  .hub-fleet-tbl-foot > span { flex: 1 1 240px; min-width: 0; }
  .hub-fleet-tbl-foot .op-view-details { white-space: nowrap; }

  /* ─── Overview split — validator list (left) + fleet averages (right) ─── */
  .hub-overview-split {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: 16px;
    align-items: stretch;
    margin-top: 4px;
  }
  .hub-fleet-avg-col, .hub-overview-split > div:last-child { min-width: 0; }
  /* Overview: the operator card matches the (taller) averages column, and its
     10 rows grow to share that height evenly — more breathing room per row, and
     no empty gap above "View all". */
  .hub-overview-split > .hub-fleet-tbl-wrap { height: 100%; }
  .hub-overview-split .hub-op-list { flex: 1 1 auto; }
  .hub-overview-split .hub-op-head { flex: none; }
  .hub-overview-split .hub-op-row { flex: 1 1 0; }
  .hub-fleet-avg-col { display: flex; flex-direction: column; gap: 14px; min-width: 0; }
  .hub-fleet-avg { gap: 0; }
  .hub-fleet-avg-p80 {
    margin: 14px 0 4px;
    padding: 16px 18px;
    border: 1px solid rgba(100,226,127,0.30);
    border-radius: 14px;
    background: rgba(100,226,127,0.07);
  }
  .hub-fleet-avg-p80 .k {
    font-family: 'Geist Mono', monospace; font-size: 10px;
    text-transform: uppercase; letter-spacing: 0.14em; color: var(--ink-dim);
  }
  .hub-fleet-avg-p80 .v {
    display: flex; align-items: baseline; gap: 5px; margin-top: 6px;
    font-size: 40px; font-weight: 300; letter-spacing: -0.02em;
    font-variant-numeric: tabular-nums; line-height: 1;
  }
  .hub-fleet-avg-p80 .v .num { color: #1F7A3A; }
  .hub-root[data-theme="dark"] .hub-fleet-avg-p80 .v .num { color: #64E27F; }
  .hub-fleet-avg-p80 .v .u { font-size: 15px; color: var(--ink-mute); font-weight: 400; }
  .hub-fleet-avg-p80 .d {
    margin-top: 8px; font-family: 'Geist Mono', monospace; font-size: 10.5px;
    color: var(--ink-mute); line-height: 1.4;
  }
  .hub-fleet-avg-win { margin-top: 18px; }
  .hub-fleet-avg-winhead {
    display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
    margin-bottom: 10px;
  }
  .hub-fleet-avg-winhead .lbl {
    font-family: 'Geist Mono', monospace; font-size: 10px;
    text-transform: uppercase; letter-spacing: 0.14em; color: var(--ink-dim);
  }
  .hub-fleet-avg-winrate { display: inline-flex; align-items: baseline; gap: 6px; }
  .hub-fleet-avg-winrate b {
    font-size: 26px; font-weight: 500; color: var(--ink);
    font-variant-numeric: tabular-nums; letter-spacing: -0.02em; line-height: 1;
  }
  .hub-fleet-avg-winrate span {
    font-family: 'Geist Mono', monospace; font-size: 10px;
    text-transform: uppercase; letter-spacing: 0.12em; color: var(--ink-mute);
  }
  .hub-fleet-avg-split {
    display: flex; height: 14px; border-radius: 999px; overflow: hidden;
    background: rgba(176,190,209,0.22); gap: 2px;
  }
  .hub-fleet-avg-split .seg { height: 100%; border-radius: 3px; transition: width .5s ease; min-width: 2px; }
  .hub-fleet-avg-split .seg.mum { background: linear-gradient(90deg, #64E27F, #51B1FE); }
  .hub-fleet-avg-split .seg.lib { background: #FEA158; }
  .hub-fleet-avg-split .seg.eq  { background: #B0BED1; }
  .hub-fleet-avg-legend {
    display: flex; flex-wrap: wrap; gap: 8px 18px; margin-top: 12px;
    font-family: 'Geist Mono', monospace; font-size: 11px; color: var(--ink-mute);
  }
  .hub-fleet-avg-legend span { display: inline-flex; align-items: center; gap: 7px; }
  .hub-fleet-avg-legend b { color: var(--ink); font-weight: 600; }
  .hub-fleet-avg-legend .sw { width: 10px; height: 10px; border-radius: 3px; flex: none; }
  .hub-fleet-avg-legend .sw.mum { background: #64E27F; }
  .hub-fleet-avg-legend .sw.lib { background: #FEA158; }

  /* ─── propagation spread — head-to-head comparison ─── */
  .fleet-spread-hero {
    display: flex; align-items: center; gap: 16px;
    margin: 16px 0 18px; padding: 16px 18px;
    border: 1px solid rgba(100,226,127,0.30); border-radius: 14px;
    background: rgba(100,226,127,0.07);
  }
  .fleet-spread-hero .x {
    font-size: 46px; font-weight: 300; line-height: 1; letter-spacing: -0.03em;
    color: #1F7A3A; font-variant-numeric: tabular-nums;
  }
  .hub-root[data-theme="dark"] .fleet-spread-hero .x { color: #64E27F; }
  .fleet-spread-hero .lbl {
    font-family: 'Geist Mono', monospace; font-size: 11px; line-height: 1.35;
    text-transform: uppercase; letter-spacing: 0.12em; color: var(--ink-mute);
  }
  .fleet-spread-bars { display: flex; flex-direction: column; gap: 12px; }
  .fleet-spread-row { display: grid; grid-template-columns: 92px minmax(0,1fr) auto; align-items: center; gap: 12px; }
  .fleet-spread-row .lab {
    font-family: 'Geist Mono', monospace; font-size: 11.5px; color: var(--ink); font-weight: 500;
    display: flex; flex-direction: column; line-height: 1.25;
  }
  .fleet-spread-row .lab em { font-style: normal; font-size: 9px; color: var(--ink-dim); text-transform: uppercase; letter-spacing: 0.12em; }
  .fleet-spread-row .track { height: 16px; border-radius: 999px; background: rgba(176,190,209,0.20); overflow: hidden; }
  .fleet-spread-row .fill { display: block; height: 100%; width: 0; border-radius: 999px; animation: vpub-fill 1000ms cubic-bezier(.2,.7,.2,1) forwards; animation-delay: 120ms; }
  .fleet-spread-row.mum .fill { background: linear-gradient(90deg, #64E27F, #51B1FE); }
  .fleet-spread-row.lib .fill { background: #FEA158; }
  .fleet-spread-row .val { font-size: 15px; font-weight: 600; color: var(--ink); font-variant-numeric: tabular-nums; white-space: nowrap; }
  .fleet-spread-row .val .u { font-size: 10px; color: var(--ink-mute); font-weight: 400; margin-left: 2px; }
  .fleet-spread-note {
    margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--line);
    font-family: 'Geist Mono', monospace; font-size: 10px; color: var(--ink-dim);
    text-transform: uppercase; letter-spacing: 0.08em;
  }

  /* Responsive: tighten cells on smaller viewports */
  @media (max-width: 1080px) {
    .hub-overview-split { grid-template-columns: 1fr; }
  }
  @media (max-width: 1240px) {
    .hub-fleet-tbl-head .cell,
    .hub-fleet-tbl-row .cell { padding: 12px 10px; font-size: 12.5px; }
  }
  @media (max-width: 980px) {
    
    
    .hub-fleet-tbl-wrap { overflow-x: auto; }
    .hub-fleet-tbl-head, .hub-fleet-tbl-row { min-width: 540px; }
  }

  /* Reduced-motion */
  @media (prefers-reduced-motion: reduce) {
    .hub-fleet-flip.flipping .hub-fleet-flip-now,
    .hub-fleet-rail-track .rail-cell,
    .hub-fleet-epoch-ticks .tick,
    .hub-fleet-heat-row .heat,
    .hub-fleet-hero-pill .dot {
      animation: none !important;
      transition: none !important;
    }
  }

  /* ═══ Stats view (op-stats*) — layered telemetry over ambient background ═══ */
  /* Stats view sits directly on the page background (no gridded card wrapper),
     so it matches the Overview and other pages. */
  .op-stats {
    position: relative;
    margin-top: 6px;
    padding: 0 0 36px;
  }
  .op-stats > * { position: relative; z-index: 1; }

  .op-stats-head {
    display: flex; align-items: center; justify-content: flex-end;
    gap: 18px; flex-wrap: wrap; margin-bottom: 22px;
    position: relative; z-index: 30; /* keep the cadence "?" downward tooltip above the body cards */
  }
  .op-chain { display: flex; align-items: center; gap: 5px; padding: 5px; border-radius: 9999px; background: var(--op-card); border: 1px solid var(--op-border); box-shadow: var(--op-shadow); flex-wrap: wrap; }

  /* stats-page refresh control (left of the chain selector) */
  /* ── Snapshot cadence readout (shared: stats header + overview bar) ── */
  .op-cadence { display: inline-flex; align-items: center; gap: 8px; font-family: 'Geist Mono', monospace; }
  .op-cadence .dot {
    width: 7px; height: 7px; border-radius: 50%; flex: none;
    background: var(--op-mint2); box-shadow: 0 0 0 3px color-mix(in srgb, var(--op-mint) 22%, transparent);
    animation: op-cadence-pulse 2.4s ease-in-out infinite;
  }
  .op-cadence .dot.busy { background: var(--op-azure); box-shadow: 0 0 0 3px rgba(81,177,254,0.22); animation-duration: .9s; }
  .op-cadence .lbl { font-size: 10px; letter-spacing: .14em; text-transform: uppercase; color: var(--op-faint); }
  .op-cadence time { font-size: 13.5px; font-weight: 600; letter-spacing: .02em; color: var(--op-text); font-variant-numeric: tabular-nums; }
  .op-cadence .sep { color: var(--op-faint); }
  .op-cadence .next { font-size: 11px; letter-spacing: .03em; color: var(--op-muted); font-variant-numeric: tabular-nums; }
  @keyframes op-cadence-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .45; } }

  /* ── Hoverable "?" metric explainer ── */
  .op-help { position: relative; display: inline-flex; margin-left: 6px; color: var(--op-faint); cursor: pointer; vertical-align: -1px; }
  .op-help:hover, .op-help:focus { color: var(--op-azure); outline: none; }
  .op-help .op-help-tip {
    position: absolute; bottom: calc(100% + 9px); left: 50%; transform: translateX(-50%);
    width: max-content; max-width: 240px; padding: 9px 12px; border-radius: 10px;
    background: var(--op-card); border: 1px solid var(--op-border); box-shadow: var(--op-shadow);
    font-family: 'Geist', sans-serif; font-size: 11.5px; font-weight: 400; line-height: 1.5;
    letter-spacing: 0; text-transform: none; color: var(--op-text); text-align: left; white-space: normal;
    opacity: 0; visibility: hidden; transition: opacity .14s ease; pointer-events: none; z-index: 40;
  }
  .op-help:hover .op-help-tip, .op-help:focus .op-help-tip { opacity: 1; visibility: visible; }
  /* Downward variant — for a "?" near the top of the page where an upward tooltip
     would be clipped by the nav / viewport edge (e.g. the snapshot-cadence indicator). */
  .op-help.op-help-down .op-help-tip { bottom: auto; top: calc(100% + 9px); }
  /* Touch: the glyph is 11x11, under half of WCAG 2.2 SC 2.5.8's 24x24 floor.
     Grow the HIT area with an absolutely-positioned transparent pseudo so
     nothing reflows (padding would shift the tip). 24x32 is deliberate: a 44px
     circle would swallow the leading edge of the adjacent .op-winseg buttons
     and the top of the .op-p80-cell bars across their small gaps. */
  @media (hover: none) {
    .op-help::after {
      content: ""; position: absolute; left: 50%; top: 50%;
      width: 24px; height: 32px; transform: translate(-50%, -50%);
      border-radius: 12px;
    }
  }

  /* Each .op-reveal / .op-panel card is a persistent stacking context (its entrance
     animation leaves a transform), which would otherwise trap a "?" tooltip or a
     pinned bar-detail popover *below* later sibling cards. Lift the active card so its
     overflowing popover wins. position:relative is safe — the lingering transform
     already makes each card the containing block for its absolute descendants. */
  .op-reveal, .op-panel { position: relative; }
  .op-reveal:hover, .op-reveal:focus-within,
  .op-panel:hover, .op-panel:focus-within { z-index: 20; }

  /* Per-slot P80 bars are buttons (hover/focus previews, click pins a slot's detail). */
  .op-p80-cell { appearance: none; -webkit-appearance: none; background: none; border: 0; padding: 0; margin: 0; cursor: pointer; }
  .op-p80-cell:hover .op-growy, .op-p80-cell:focus-visible .op-growy { filter: brightness(1.12); }
  .op-p80-cell:focus-visible { outline: none; } /* no blue ring on click — the bar brightens + popover shows instead */

  /* ── Trusted-by partner marquee (network-status card footer) ── */
  .op-marquee { display: flex; align-items: center; gap: 22px; }
  .op-marquee-viewport {
    position: relative; flex: 1 1 0; min-width: 0; overflow: hidden;
    -webkit-mask-image: linear-gradient(90deg, transparent, #000 7%, #000 93%, transparent);
            mask-image: linear-gradient(90deg, transparent, #000 7%, #000 93%, transparent);
  }
  .op-marquee-track { display: flex; align-items: center; gap: 30px; width: max-content; animation: op-marquee 52s linear infinite; }
  .op-marquee-item { display: inline-flex; align-items: center; gap: 9px; flex: none; }
  .op-marquee-avatar { width: 24px; height: 24px; border-radius: 6px; }
  .op-marquee-name { font-family: 'Geist', sans-serif; font-size: 12.5px; font-weight: 500; color: var(--op-muted); white-space: nowrap; }
  @keyframes op-marquee { from { transform: translateX(0); } to { transform: translateX(-50%); } }
  /* Pause the whole strip on hover or keyboard focus so a name can be read and
     clicked; it resumes the moment the pointer leaves. Pausing on the strip
     rather than the item means moving between logos does not restart it. */
  .op-marquee-item { text-decoration: none; border-radius: 8px; transition: opacity .15s ease; }
  /* Keyboard focus pauses everywhere -- focus is never latched by a tap. */
  .op-marquee:focus-within .op-marquee-track { animation-play-state: paused; }
  /* Hover affordances only where a real hovering pointer exists. iOS Safari
     latches :hover onto a tapped <a> AND its ancestors until the next tap
     elsewhere; a tap on the strip's padding latches .op-marquee:hover with no
     item hovered, freezing the track with every logo dimmed to .55 and no
     navigation to explain it -- it reads as a broken widget. */
  @media (hover: hover) and (pointer: fine) {
    .op-marquee:hover .op-marquee-track { animation-play-state: paused; }
    .op-marquee-item:hover .op-marquee-name { color: var(--op-text); }
    .op-marquee:hover .op-marquee-item:not(:hover) { opacity: .55; }
  }
  .op-marquee-item:focus-visible { outline: 2px solid var(--op-azure); outline-offset: 3px; }
  @media (prefers-reduced-motion: reduce) { .op-marquee-track { animation: none; } }
  /* Loader overlay shown over the stats content while a refresh is in flight */
  .op-refresh-overlay {
    position: absolute; inset: 0; z-index: 30; display: flex; align-items: flex-start; justify-content: center;
    padding-top: 20vh; border-radius: 18px; background: color-mix(in srgb, var(--op-card2) 92%, transparent);
    -webkit-backdrop-filter: blur(2px); backdrop-filter: blur(2px); animation: op-rise .16s ease both;
  }
  /* keep the loader in view even if the card is taller than the viewport */
  .op-refresh-overlay .op-pulse { position: sticky; top: 22vh; }
  /* dismiss control for the manual loading preview — top-right of the overlay */
  .op-refresh-close {
    position: absolute; top: 14px; right: 14px; z-index: 2;
    width: 30px; height: 30px; border-radius: 9px; cursor: pointer;
    display: inline-flex; align-items: center; justify-content: center;
    font-size: 13px; line-height: 1; color: var(--op-muted);
    background: var(--op-card); border: 1px solid var(--op-border);
    box-shadow: var(--op-shadow); transition: color .15s ease, background .15s ease;
  }
  .op-refresh-close:hover { color: var(--op-text); background: var(--op-chip); }
  .op-refresh-close:focus-visible { outline: 2px solid var(--op-azure); outline-offset: 2px; }

  /* ── Validators page ── */
  .op-validators { padding: 6px 0 44px; }
  .op-validators-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 18px; flex-wrap: wrap; margin-bottom: 22px; }
  .op-validators-eyebrow { font-family: 'Geist Mono', monospace; font-size: 10px; letter-spacing: .16em; text-transform: uppercase; color: var(--op-muted); margin-bottom: 8px; }
  .op-validators-head h2 { margin: 0; font-size: 30px; font-weight: 600; letter-spacing: -0.02em; color: var(--op-text); }
  .op-validators-head p { margin: 7px 0 0; font-size: 13.5px; color: var(--op-muted); }
  .op-back-btn {
    display: inline-flex; align-items: center; gap: 7px; cursor: pointer;
    font-family: 'Geist Mono', monospace; font-size: 11px; letter-spacing: .06em; text-transform: uppercase;
    padding: 8px 14px; border-radius: 9999px; color: var(--op-text);
    background: var(--op-card); border: 1px solid var(--op-border); box-shadow: var(--op-shadow);
    transition: color .15s ease, border-color .15s ease, transform .15s ease;
  }
  .op-back-btn:hover { border-color: var(--op-azure); transform: translateY(-1px); }
  .op-back-btn svg { color: var(--op-azure); }
  .op-pager { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
  .op-pager-top { padding: 4px 2px 14px; margin-bottom: 12px; border-bottom: 1px solid var(--op-hair); }
  .op-pager-bottom { padding: 14px 2px 2px; margin-top: 12px; border-top: 1px solid var(--op-hair); }
  .op-pager-info { font-size: 11px; color: var(--op-muted); font-variant-numeric: tabular-nums; }
  .op-pager-btn {
    cursor: pointer; font-family: 'Geist Mono', monospace; font-size: 11px; letter-spacing: .05em;
    padding: 8px 16px; border-radius: 9999px; color: var(--op-text);
    background: var(--op-card); border: 1px solid var(--op-border); box-shadow: var(--op-shadow);
    transition: color .15s ease, border-color .15s ease;
  }
  .op-pager-btn:not(:disabled):hover { border-color: var(--op-azure); color: var(--op-azure); }
  .op-pager-btn:disabled { opacity: .4; cursor: not-allowed; }
  .op-chain-btn {
    display: inline-flex; align-items: center; gap: 7px;
    font-family: 'Geist Mono', monospace; font-size: 11px; letter-spacing: .07em; text-transform: uppercase;
    padding: 8px 15px; border-radius: 9999px; border: 1px solid transparent; background: transparent;
    color: var(--op-muted); cursor: pointer; transition: all .18s ease; white-space: nowrap;
  }
  .op-chain-btn:hover { color: var(--op-text); background: var(--op-chip); }
  .op-chain-btn.on { color: var(--op-text); background: var(--op-chip); border-color: var(--op-border); }
  .op-chain-btn.soon { color: var(--op-faint); cursor: default; }
  .op-chain-btn.soon:hover { color: var(--op-muted); background: transparent; }
  .op-chain-btn:focus-visible { outline: 2px solid var(--op-azure); outline-offset: 2px; }
  .op-chain-soon { font-size: 8px; letter-spacing: .08em; text-transform: uppercase; opacity: .65; border: 1px solid currentColor; border-radius: 9999px; padding: 1px 5px; }

  /* two-up graph grid on the Stats main pane */
  .op-view-details {
    font-family: 'Geist Mono', monospace; font-size: 10px; letter-spacing: .1em; text-transform: uppercase;
    color: var(--op-muted); text-decoration: none; cursor: pointer; transition: color .15s ease; white-space: nowrap;
  }
  .op-view-details:hover { color: var(--op-text); }
  .op-view-details:focus-visible { outline: 2px solid var(--op-azure); outline-offset: 2px; border-radius: 4px; }
  .op-stats-grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; align-items: stretch; }
  /* Both cards match height: each card is a flex column, its tile grid fills the
     leftover space, and the tiles center their content — so the shorter card has
     no empty ("greyed") area at the bottom. */
  .op-stats-grid2 > .op-panel { display: flex; flex-direction: column; }
  .op-stats-grid2 > .op-panel > div:nth-child(2) { flex: 1 1 auto; }
  .op-stats-grid2 > .op-panel > div:nth-child(2) > div { display: flex; flex-direction: column; justify-content: center; }

  /* globe → expand-to-map button */
  .hub-globe-expand {
    position: absolute; top: 16px; left: 16px; z-index: 4;
    display: inline-flex; align-items: center; gap: 5px;
    font-family: 'Geist Mono', monospace; font-size: 9px; letter-spacing: .05em; text-transform: uppercase;
    padding: 5px 9px; border-radius: 9999px; cursor: pointer;
    color: var(--op-muted); background: var(--op-card); border: 1px solid var(--op-border);
    box-shadow: var(--op-shadow); transition: color .15s ease, transform .15s ease;
  }
  .hub-globe-expand svg { width: 11px; height: 11px; }
  .hub-globe-expand:hover { color: var(--op-text); transform: translateY(-1px); }
  .hub-globe-expand svg { color: var(--op-azure); }
  .hub-globe-expand:focus-visible { outline: 2px solid var(--op-azure); outline-offset: 2px; }

  /* full-screen validator atlas modal */
  .op-atlas-overlay {
    position: fixed; inset: 0; z-index: 400;
    display: flex; align-items: center; justify-content: center; padding: clamp(12px, 3vw, 40px);
    background: rgba(15,19,26,0.55); -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px);
    animation: op-rise .2s ease both;
  }
  .op-atlas-modal {
    width: min(1200px, 100%); height: 92vh; max-height: 92vh; display: flex; flex-direction: column;
    border-radius: 22px; border: 1px solid var(--op-border); background: var(--op-card);
    box-shadow: 0 30px 90px rgba(0,0,0,0.4); overflow: hidden;
    transition: border-radius .18s ease;
  }
  /* Maximized: fill the entire viewport (true full-screen within the app). */
  .op-atlas-overlay.op-atlas-overlay--full { padding: 0; }
  .op-atlas-modal.op-atlas-modal--full {
    width: 100%; max-width: 100%; height: 100%; max-height: 100%;
    border-radius: 0; border: none;
  }
  .op-atlas-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; padding: 20px 24px; border-bottom: 1px solid var(--op-hair); }
  .op-atlas-eyebrow { font-family: 'Geist Mono', monospace; font-size: 10px; letter-spacing: .16em; text-transform: uppercase; color: var(--op-muted); }
  .op-atlas-title { margin: 6px 0 0; font-size: 20px; font-weight: 600; color: var(--op-text); font-variant-numeric: tabular-nums; }
  .op-atlas-subhint { margin-top: 5px; font-family: 'Geist Mono', monospace; font-size: 11px; letter-spacing: .02em; color: var(--op-muted); }
  .op-atlas-legend { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
  .op-atlas-max { width: 30px; height: 30px; border-radius: 9px; border: 1px solid var(--op-border); background: var(--op-chip); color: var(--op-muted); cursor: pointer; display: inline-flex; align-items: center; justify-content: center; padding: 0; transition: color .15s ease, background .15s ease; }
  .op-atlas-max:hover { color: var(--op-text); background: var(--op-card); }
  .op-atlas-max svg { display: block; }
  .op-atlas-max:focus-visible { outline: 2px solid var(--op-azure); outline-offset: 2px; }
  .op-atlas-legend span { display: inline-flex; align-items: center; gap: 7px; font-family: 'Geist Mono', monospace; font-size: 10px; letter-spacing: .06em; text-transform: uppercase; color: var(--op-muted); }
  .op-atlas-legend .dot { width: 9px; height: 9px; border-radius: 9999px; display: inline-block; }
  .op-atlas-legend .dot.azure { background: var(--op-azure); }
  .op-atlas-legend .dot.mint { background: var(--op-mint); }
  .op-atlas-close { width: 30px; height: 30px; border-radius: 9px; border: 1px solid var(--op-border); background: var(--op-chip); color: var(--op-muted); cursor: pointer; font-size: 13px; line-height: 1; margin-left: 4px; }
  .op-atlas-close:hover { color: var(--op-text); }
  .op-atlas-canvas { flex: 1; min-height: 0; display: flex; padding: 6px 14px; background: radial-gradient(120% 90% at 50% -10%, var(--op-glow), transparent 60%), var(--op-card2); }
  .op-atlas-foot { display: flex; align-items: center; justify-content: center; padding: 11px 24px; border-top: 1px solid var(--op-hair); }
  .op-atlas-foot .af-note { font-family: 'Geist', sans-serif; font-size: 11px; line-height: 1.45; color: var(--op-faint); text-align: center; }
  .op-atlas-foot .af-note a { color: var(--op-muted); text-decoration: underline; text-underline-offset: 2px; }
  .op-atlas-foot .af-note a:hover { color: var(--op-azure); }
  .op-atlas-zoom { position: absolute; right: 16px; bottom: 16px; display: flex; flex-direction: column; gap: 6px; z-index: 2; }
  .op-atlas-zoom button {
    width: 32px; height: 32px; border-radius: 9px; cursor: pointer;
    display: flex; align-items: center; justify-content: center;
    font-family: 'Geist Mono', monospace; font-size: 16px; line-height: 1;
    color: var(--op-text); background: var(--op-card); border: 1px solid var(--op-border);
    box-shadow: var(--op-shadow); transition: color .15s ease, background .15s ease;
  }
  .op-atlas-zoom button:hover { background: var(--op-chip); color: var(--op-azure); }
  .op-atlas-zoom button.reset { font-size: 13px; color: var(--op-muted); }
  .op-atlas-zoom button:focus-visible { outline: 2px solid var(--op-azure); outline-offset: 2px; }
  /* choropleth hover tooltip — Optimum mark + country + validator count */
  .op-atlas-tip {
    position: absolute; transform: translate(-50%, calc(-100% - 12px)); pointer-events: none; z-index: 5;
    display: inline-flex; align-items: center; gap: 8px; white-space: nowrap;
    padding: 6px 11px; border-radius: 10px;
    background: var(--op-card); border: 1px solid var(--op-border); box-shadow: var(--op-shadow);
    font-family: 'Geist', sans-serif; font-size: 12.5px; color: var(--op-text);
  }
  .op-atlas-tip img { display: block; opacity: .92; }
  .op-atlas-tip .c { font-weight: 600; }
  .op-atlas-tip b { font-family: 'Geist Mono', monospace; color: var(--op-azure); font-weight: 700; font-variant-numeric: tabular-nums; }
  .op-atlas-tip .u { font-family: 'Geist Mono', monospace; font-size: 10px; letter-spacing: .04em; text-transform: uppercase; color: var(--op-muted); }
  /* info caption over the map (top-left) — replaces the old footer strip */
  .op-atlas-caption { position: absolute; top: 14px; left: 16px; z-index: 3; pointer-events: none; }
  .op-atlas-caption .cap-1 { font-family: 'Geist Mono', monospace; font-size: 12px; font-weight: 600; letter-spacing: .02em; color: var(--op-text); }
  .op-atlas-caption .cap-2 { margin-top: 3px; font-family: 'Geist Mono', monospace; font-size: 9.5px; letter-spacing: .04em; color: var(--op-muted); }
  @media (max-width: 640px) { .op-atlas-title { font-size: 17px; } .op-atlas-legend span { display: none; } .op-atlas-subhint { display: none; } }
  @media (max-width: 720px) { .op-stats-grid2 { grid-template-columns: 1fr; } }

  .op-stats-body { display: block; }
  .op-stats-nav {
    position: sticky; top: 78px; display: flex; flex-direction: column; gap: 6px;
    padding: 10px; border-radius: 18px; border: 1px solid var(--op-hair);
    background: color-mix(in srgb, var(--op-card) 84%, transparent);
    -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px);
    /* raise the whole aside above the main panel column so its "?" tooltips
       (which extend rightward over the panels) are never painted behind them —
       the panels are separate stacking contexts via backdrop-filter. */
    z-index: 30;
  }
  .op-stats-navgroup {
    font-family: 'Geist Mono', monospace; font-size: 9.5px; letter-spacing: .16em; text-transform: uppercase;
    color: var(--op-faint); padding: 8px 12px 4px;
  }
  /* left-pane fleet-snapshot card — stat card with window-reactive numbers */
  .op-stats-about-stat { padding: 6px 12px 4px; }
  .op-stats-about-stat b { display: block; font-family: 'Geist Mono', monospace; font-size: 27px; font-weight: 600; letter-spacing: -0.02em; color: var(--op-text); line-height: 1; font-variant-numeric: tabular-nums; }
  .op-stats-about-stat span { display: block; margin-top: 5px; font-family: 'Geist Mono', monospace; font-size: 9.5px; letter-spacing: .1em; text-transform: uppercase; color: var(--op-muted); }
  .op-stats-about-metric { padding: 12px 12px 0; margin-top: 10px; border-top: 1px solid var(--op-hair); }
  .op-stats-about-metric .k { font-family: 'Geist Mono', monospace; font-size: 9px; letter-spacing: .12em; text-transform: uppercase; color: var(--op-faint); }
  .op-stats-about-metric .v { font-family: 'Geist Mono', monospace; font-size: 26px; font-weight: 600; letter-spacing: -0.02em; line-height: 1.05; margin-top: 6px; color: var(--op-text); font-variant-numeric: tabular-nums; }
  .op-stats-about-metric .v.good { color: var(--op-green); }
  .op-stats-about-metric .v em { font-style: normal; font-size: 13px; margin-left: 2px; opacity: .8; }
  .op-stats-about-metric .s { margin-top: 4px; font-family: 'Geist Mono', monospace; font-size: 9px; letter-spacing: .04em; color: var(--op-faint); }
  /* first basis row sits directly under the title — no divider, and the validator
     count reads a touch larger as the panel's headline number. */
  .op-stats-about-first { border-top: none; margin-top: 2px; }
  .op-stats-about-first .v { font-size: 30px; }
  .op-stats-navitem {
    position: relative; text-align: left; display: flex; flex-direction: column; gap: 3px;
    padding: 11px 14px; border-radius: 13px; border: 1px solid transparent; background: transparent;
    color: var(--op-muted); cursor: pointer;
    transition: background .16s ease, border-color .16s ease, color .16s ease;
  }
  .op-stats-navitem .lead {
    display: inline-flex; align-items: center; gap: 9px;
    font-family: 'Geist', sans-serif; font-size: 14px; font-weight: 600; letter-spacing: -0.01em; color: inherit;
  }
  .op-stats-navitem .lead::before {
    content: ''; width: 7px; height: 7px; border-radius: 50%; flex: none;
    background: var(--op-border); transition: background .16s ease, box-shadow .16s ease;
  }
  .op-stats-navitem .sub {
    font-family: 'Geist Mono', monospace; font-size: 9.5px; letter-spacing: .04em;
    color: var(--op-faint); padding-left: 16px;
  }
  .op-stats-navitem:hover { color: var(--op-text); background: var(--op-chip); }
  .op-stats-navitem.on {
    color: var(--op-text);
    background: color-mix(in srgb, var(--op-mint) 13%, transparent);
    border-color: color-mix(in srgb, var(--op-mint) 34%, transparent);
  }
  .op-stats-navitem.on .lead::before {
    background: var(--op-mint2);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--op-mint) 24%, transparent);
  }
  .op-stats-navitem:focus-visible { outline: 2px solid var(--op-azure); outline-offset: 2px; }
  .op-stats-main { display: flex; flex-direction: column; gap: 20px; min-width: 0; }

  /* glass panels */
  .op-panel {
    padding: 22px 24px; border-radius: 18px; border: 1px solid var(--op-border);
    background: var(--op-card);
    background: color-mix(in srgb, var(--op-card) 84%, transparent);
    -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px);
    box-shadow: var(--op-shadow);
  }
  .op-panel-head { display: flex; align-items: baseline; justify-content: space-between; gap: 14px; flex-wrap: wrap; margin-bottom: 16px; }
  .op-panel-head h3 { margin: 0; font-size: 15px; font-weight: 600; letter-spacing: -0.01em; color: var(--op-text); }
  .op-meta { font-family: 'Geist Mono', monospace; font-size: 10.5px; letter-spacing: .04em; color: var(--op-muted); }

  /* 200-slot bar strip */
  .op-strip { display: flex; align-items: flex-end; gap: 1px; height: 92px; padding: 2px 0; }
  .op-strip-cell {
    flex: 1 1 0; min-width: 0; height: 100%; padding: 0; border: none; background: transparent;
    display: flex; align-items: flex-end; cursor: pointer;
  }
  .op-strip-bar {
    width: 100%; min-height: 2px; height: var(--h); border-radius: 2px 2px 0 0;
    overflow: hidden; display: flex; flex-direction: column;
    transform-origin: bottom; animation: op-growy .55s cubic-bezier(.22,.61,.36,1) both;
    animation-delay: calc(var(--i) * 3.5ms); transition: filter .14s ease, opacity .14s ease;
  }
  /* Stacked per-slot bar: peach = full libP2P arrival, green base = mumP2P — the
     green share shows how much faster mumP2P was (matches the legend + Overview). */
  .op-strip-bar > span { width: 100%; flex: none; }
  .op-strip-seg-lib { background: var(--op-peach); }
  .op-strip-seg-mum { background: linear-gradient(180deg, var(--op-mint), var(--op-mint2)); }
  .op-strip-cell:hover .op-strip-bar { filter: brightness(1.12); }
  .op-strip-cell:not(:hover) .op-strip-bar { opacity: .92; }
  .op-strip-cell:focus-visible { outline: none; }
  .op-strip-cell:focus-visible .op-strip-bar { filter: brightness(1.18); }
  .op-strip-axis {
    display: flex; justify-content: space-between; gap: 10px; margin-top: 9px;
    font-family: 'Geist Mono', monospace; font-size: 10px; letter-spacing: .03em; color: var(--op-faint);
  }
  .op-strip-axis .mid { color: var(--op-muted); text-align: center; flex: 1; }

  /* per-slot strip: floating detail tooltip (hover or click a bar) */
  .op-strip-wrap { position: relative; }
  .op-strip-cell.pinned .op-strip-bar { filter: brightness(1.32); border-radius: 2px; }
  .op-strip-tip {
    position: absolute; bottom: 100%; margin-bottom: 10px; transform: translateX(-50%);
    /* Same surface, shadow and corner rhythm as the report cards, scaled down —
       20/8 rather than 80/20, which would swallow a box this size. */
    background: var(--sc-surface, var(--op-card)); border: 0; border-radius: 20px 8px 20px 8px;
    box-shadow: var(--sc-shadow, var(--op-shadow)); padding: 13px 15px; white-space: nowrap; z-index: 50; pointer-events: none;
    /* opacity-only entrance so the inline transform (edge-aware anchoring) is not
       overwritten by the animation's end-state — otherwise tips overflow the right edge. */
    font-family: 'Geist Mono', monospace; animation: op-tipin .14s ease both;
  }
  .op-strip-tip .t1 {
    font-family: 'Geist', sans-serif; font-weight: 700; font-size: 10px;
    letter-spacing: .08em; text-transform: uppercase; color: var(--sc-mute, var(--op-muted));
  }
  .op-strip-tip .t2 { font-size: 11px; color: var(--op-text); margin-top: 5px; }
  .op-strip-tip .t3 { font-size: 12px; font-weight: 600; margin-top: 3px; }
  /* Paired mumP2P/libP2P arrival bars inside the hover tooltip — the visual
     gap between the (short) green bar and the (long) peach bar IS the advantage. */
  .op-tipbars { display: flex; flex-direction: column; gap: 8px; margin-top: 12px; }
  .op-tipbar-row { display: grid; grid-template-columns: 58px 118px auto; align-items: center; gap: 10px; }
  .op-tipbar-k { font-family: 'Geist', sans-serif; font-weight: 700; font-size: 11px; color: var(--sc-fg, var(--op-text)); }
  .op-tipbar-row:last-of-type .op-tipbar-k { color: var(--sc-mute, var(--op-muted)); }
  .op-tipbar-track { position: relative; height: 7px; border-radius: 4px; background: var(--sc-track, var(--op-track)); overflow: hidden; }
  .op-tipbar-track i { position: absolute; top: 0; bottom: 0; left: 0; border-radius: 4px; }
  .op-tipbar-track i.mum { background: var(--sc-bar-mum, var(--op-text)); }
  .op-tipbar-track i.lib { background: var(--sc-bar-lib, var(--op-muted)); }
  .op-tipbar-v { font-family: 'Geist Mono', monospace; font-size: 11px; color: var(--sc-fg, var(--op-text)); font-variant-numeric: tabular-nums; text-align: right; }
  .op-tipbar-sum {
    margin-top: 12px; padding-top: 10px; border-top: 1px solid var(--sc-hair, var(--op-hair));
    font-family: 'Geist', sans-serif; font-size: 12px; font-weight: 500; letter-spacing: 0;
  }
  /* Sample data, like the chart it belongs to — say so rather than let a demo
     multiple sit here contradicting the measured one on the cards above. */
  .op-tipbar-note {
    margin-top: 7px; font-family: 'Geist', sans-serif; font-weight: 700; font-size: 9px;
    letter-spacing: .08em; text-transform: uppercase; color: var(--sc-mute, var(--op-muted)); opacity: .8;
  }

  /* ─── performance hero — the report's four headline results ───
     A single dark slab above the Overview. It is intentionally darker than the
     cards in BOTH themes so it reads as a banner rather than another card. */
  .hub-perf-hero {
    padding: clamp(24px, 2.8vw, 34px) clamp(26px, 3vw, 40px);
    border-radius: 80px 20px 80px 20px;
    background: #282C34; box-shadow: var(--sc-shadow, var(--op-shadow));
  }
  .hub-root[data-theme="dark"] .hub-perf-hero { background: #1A1D24; border: 1px solid var(--op-hair); }
  .hub-perf-hero-meta { display: flex; align-items: baseline; justify-content: space-between; gap: 14px; flex-wrap: wrap; margin-bottom: 24px; }
  .hub-perf-hero-eyebrow, .hub-perf-hero-win {
    font-family: 'Geist Mono', monospace; font-size: 9.5px; font-weight: 500;
    letter-spacing: .14em; text-transform: uppercase;
  }
  .hub-perf-hero-eyebrow { color: rgba(244,246,248,0.50); }
  .hub-perf-hero-win { color: rgba(244,246,248,0.74); }
  .hub-perf-hero-grid { display: grid; grid-template-columns: repeat(var(--hero-cells, 4), minmax(0,1fr)); gap: 26px; }
  .hub-perf-hero-v { display: flex; align-items: baseline; gap: 3px; color: #FFFFFF; }
  .hub-perf-hero-v .op-num { font-size: clamp(29px, 2.9vw, 43px); font-weight: 300; letter-spacing: -0.03em; line-height: 0.95; }
  .hub-perf-hero-v em { font-style: normal; font-size: 19px; font-weight: 300; color: rgba(255,255,255,0.60); }
  .hub-perf-hero-k { margin-top: 13px; font-family: 'Geist Mono', monospace; font-size: 10px; font-weight: 500; letter-spacing: .12em; text-transform: uppercase; color: rgba(244,246,248,0.92); line-height: 1.4; }
  .hub-perf-hero-n { margin-top: 6px; font-size: 11.5px; line-height: 1.45; color: rgba(244,246,248,0.48); }
  @media (max-width: 900px) { .hub-perf-hero-grid { grid-template-columns: repeat(2, minmax(0,1fr)); gap: 24px; } }
  .hub-perf-hero-cell { display: block; width: 100%; text-align: left; }
  /* Only the compact layout needs these; on desktop each cell carries its own. */
  .hub-perf-hero-rule, .hub-perf-hero-caption { display: none; }
  /* Design canvas option 3b. Stacked, the four headlines run to four tall blocks;
     here they sit on one row and a single caption reports whichever is selected. */
  @media (max-width: 520px) {
    .hub-perf-hero { padding: 22px 20px 24px; border-radius: 20px; }
    .hub-perf-hero-grid { grid-template-columns: repeat(var(--hero-cells, 4), minmax(0,1fr)); gap: 8px; }
    .hub-perf-hero-cell {
      appearance: none; background: none; border: 0; padding: 0; margin: 0;
      font: inherit; color: inherit; cursor: pointer;
    }
    .hub-perf-hero-v .op-num { font-size: 26px; }
    .hub-perf-hero-v em { font-size: 12px; }
    .hub-perf-hero-k, .hub-perf-hero-n { display: none; }
    .hub-perf-hero-rule {
      display: block; width: 100%; height: 2px; margin-top: 9px; border-radius: 2px;
      background: rgba(244,246,248,0.18); transition: background .2s ease;
    }
    .hub-perf-hero-cell.is-sel .hub-perf-hero-rule { background: var(--op-mint, #64E27F); }
    .hub-perf-hero-cell:not(.is-sel) .hub-perf-hero-v { color: rgba(255,255,255,0.55); }
    .hub-perf-hero-caption { display: flex; flex-direction: column; gap: 6px; margin-top: 18px; min-height: 52px; }
    .hub-perf-hero-caption .k {
      font-family: 'Geist Mono', monospace; font-size: 10px; font-weight: 500;
      letter-spacing: .12em; text-transform: uppercase; color: rgba(244,246,248,0.92);
    }
    .hub-perf-hero-caption .n { font-size: 12.5px; line-height: 1.45; color: rgba(244,246,248,0.55); }
  }

  /* ─── network-status card — ported from the Optimum Hub design canvas ───
     Card-scoped tokens so the design's palette applies here in both themes.
     The display face is the brand's ABC Diatype with its documented Inter Tight
     fallback (Diatype is licensed and must not be committed). */
  .hub-netcard {
    --sc-fg: #16181A; --sc-mute: #6A757F; --sc-accent: #1C9A54;
    --sc-hair: rgba(0,0,0,0.08); --sc-track: rgba(51,54,60,0.09);
    --sc-bar-mum: #33363C; --sc-bar-lib: #B0BED1; --sc-hatch: rgba(51,54,60,0.22);
    --sc-fill-mum: #7BE692;
    --sc-disp: "ABC Diatype", "Inter Tight", "Helvetica Neue", system-ui, sans-serif;
  }
  .hub-root[data-theme="dark"] .hub-netcard {
    --sc-fg: #FFFBFB; --sc-mute: #B0BED1; --sc-accent: #64E27F;
    --sc-hair: rgba(255,251,251,0.10); --sc-track: rgba(255,251,251,0.10);
    --sc-bar-mum: #FFFBFB; --sc-bar-lib: #8D9CA9; --sc-hatch: rgba(141,156,169,0.30);
    --sc-fill-mum: #64E27F;
  }
  /* Eyebrows are Geist 700 at +8% tracking, per the brand copy rules. */
  .hub-status-eyebrow {
    font-family: 'Geist', sans-serif; font-weight: 700; font-size: 13px;
    letter-spacing: .08em; text-transform: uppercase; color: var(--sc-mute);
  }
  .hub-status-lede {
    margin: 0; font-family: var(--sc-disp); font-weight: 300;
    font-size: clamp(19px, 2vw, 28px); line-height: 1.28; letter-spacing: -0.02em;
    color: var(--sc-fg); text-wrap: balance;
  }
  /* Rotating reason. Every phrase is stacked into ONE grid cell, so the row is
     always as tall as the tallest phrase and swapping never reflows the card.
     This replaced a sizer that picked the longest phrase by character count —
     three of these are 38 characters but differ by ~19px rendered, so at some
     widths the "longest" string still fit while another wrapped, and the card
     jumped. Grid measures what actually renders. */
  /* Inline, not stacked: the name is the sentence's subject, so it reads as one
     line. Weight rather than size sets it apart — a size jump mid-sentence
     fights the baseline. */
  .hub-rot-ghost, .hub-rot-name { font-weight: 500; }
  .hub-rot-ghost { visibility: hidden; }
  .hub-rot-name { position: absolute; left: 0; top: 0; }
  .hub-rot-copy-mobile { display: none; }
  /* The name holds the display scale; the rotating line steps down to body
     scale and the body face, so hierarchy comes from size, not colour. */
  .hub-rot { display: grid; position: relative; }
  .hub-rot-item {
    grid-area: 1 / 1;
    opacity: 0; transform: translateY(0.16em);
    transition: opacity .5s ease, transform .5s cubic-bezier(.22,.61,.36,1);
  }
  .hub-rot-item.on { opacity: 1; transform: none; }
  @media (prefers-reduced-motion: reduce) {
    .hub-rot-item { transition: none; transform: none; }
  }
  @media (max-width: 480px) {
    .hub-status-lede {
      font-size: clamp(12px, 3.7vw, 16px);
      line-height: 1.2;
      letter-spacing: -0.025em;
      white-space: nowrap;
    }
    .hub-rot-copy-full { display: none; }
    .hub-rot-copy-mobile { display: inline; }
  }

  .hub-status-stats {
    margin-top: 30px; padding: 28px 0;
    border-top: 1px solid var(--sc-hair); border-bottom: 1px solid var(--sc-hair);
    display: grid; grid-template-columns: repeat(auto-fit, minmax(min(190px, 100%), 1fr));
    gap: clamp(24px, 2.6vw, 40px);
  }
  .hub-status-stat-k {
    display: flex; align-items: flex-start; gap: 6px;
    font-family: 'Geist', sans-serif; font-weight: 700; font-size: 12.5px;
    letter-spacing: .08em; text-transform: uppercase; color: var(--sc-mute);
    /* Two lines are reserved because one label wraps and the other does not.
       Without it the shorter column's figure sits a line higher than its
       neighbour, and the pair stops reading as one row. */
    line-height: 1.3; min-height: 2.6em;
  }
  /* Ink, not green: at this size a figure reads as a claim, not a status light. */
  .hub-status-stat-v {
    margin-top: 14px; font-family: var(--sc-disp); font-weight: 300;
    font-size: clamp(50px, 5.6vw, 80px); line-height: 1; letter-spacing: -0.03em;
    color: var(--sc-fg); white-space: nowrap;
  }
  .hub-status-stat-v .u { font-size: 0.4em; letter-spacing: -0.01em; }
  /* Budget bar (design canvas option 1d): gossipsub's figure is the whole width,
     mump2p fills its share, and the hatched remainder is the excess. One bar, so
     the comparison is a part-of-whole rather than two lengths to eyeball. */
  /* Query container: the figures are sized from the BAR's width, which does not
     track the viewport's. The stat columns go 2-up from ~840px, so an iPad at
     1024px leaves a narrower bar (228px) than a phone at 375px (~310px). */
  .hub-budget { margin-top: 30px; container-type: inline-size; container-name: budget; }
  .hub-budget-bar {
    display: flex; align-items: stretch; height: 52px; border-radius: 11px; overflow: hidden;
    background: var(--sc-track); border: 1px solid var(--sc-hair);
  }
  /* max-content, not 0: the fill is a data-driven percentage, and on a narrow
     bar that share can be thinner than the figure it holds, which then spills
     across the tick. The fluid type below keeps it inside at every width, so
     this floor is only ever asked for a pixel or two. */
  .hub-budget-bar .fill {
    display: flex; align-items: center; min-width: max-content; background: var(--sc-fill-mum);
    padding: 0 11px;
    padding: 0 clamp(4px, calc(6.034cqw - 8.19px), 11px);
  }
  /* Fixed near-black rather than a theme token: it clears AA on both fills
     (11.8:1 light, 11.0:1 dark), so it needs no inverting pair.
     Size ramps from 13px at a 318px bar to 9px at 202px, where a 320px phone
     lands: the green is a fixed ~25% share, so it shrinks with the bar while a
     fixed figure does not. The px line above is the fallback for browsers
     without container query units. */
  .hub-budget-bar .fill span {
    font-family: 'Geist Mono', monospace; font-weight: 600; color: #10161C; white-space: nowrap;
    font-size: 13px;
    font-size: clamp(9px, calc(3.448cqw + 2.03px), 13px);
  }
  .hub-budget-bar .tick { flex: none; width: 2px; background: var(--sc-fg); }
  /* safe centring: if the labels outgrow this half they spill off the right
     edge, where the bar clips them, rather than back across the tick. */
  .hub-budget-bar .rest {
    flex: 1; min-width: 0; display: flex; align-items: center; gap: 6px;
    justify-content: center;
    justify-content: safe center;
    background: repeating-linear-gradient(115deg, var(--sc-hatch) 0 1.5px, transparent 1.5px 8px);
  }
  /* Same ramp as the fill, so "403.1 ms longer to spread" shrinks in step and
     the two halves always read as one line of type. */
  .hub-budget-bar .rest .v {
    font-family: 'Geist Mono', monospace; color: var(--sc-fg); white-space: nowrap;
    font-size: 13px;
    font-size: clamp(9px, calc(3.448cqw + 2.03px), 13px);
  }
  .hub-budget-bar .rest .u {
    font-family: 'Geist', sans-serif; color: var(--sc-fg); opacity: .78; white-space: nowrap;
    font-size: 12.5px;
    font-size: clamp(8.6px, calc(3.328cqw + 1.92px), 12.5px);
  }
  .hub-budget-legend {
    margin-top: 11px; display: flex; justify-content: space-between; align-items: baseline; gap: 10px;
    font-family: 'Geist', sans-serif; font-size: 12.5px; font-weight: 600;
  }
  /* Not the accent the canvas uses: on the light card that is 3.37:1, under the
     4.38:1 the card's own labels hold. The fill above carries the colour. */
  .hub-budget-legend .mum { color: var(--sc-fg); }
  .hub-budget-legend .lib { color: var(--sc-mute); }
  /* Daily movement under the stake figure. */
  .hub-status-spark { margin-top: 22px; }
  .hub-status-spark-plot { position: relative; }
  .hub-status-spark svg { display: block; width: 100%; height: 76px; overflow: visible; }
  .hub-status-spark-axis {
    display: flex; justify-content: space-between; margin-top: 9px;
    font-family: 'Geist Mono', monospace; font-size: 9.5px;
    letter-spacing: .1em; text-transform: uppercase; color: var(--op-faint);
  }
  .op-spk-cursor {
    position: absolute; top: 0; bottom: 0; width: 0; border-left: 1px dashed var(--op-green);
    opacity: .55; pointer-events: none; transform: translateX(-0.5px);
  }
  /* Inside the plot, not above it: above lands on the stake figure. The series
     rises left to right, so the upper-left is clear. */
  .op-spk-tip {
    position: absolute; top: 2px; z-index: 30; pointer-events: none;
    display: flex; flex-direction: column; gap: 2px; white-space: nowrap;
    background: var(--op-card); border: 1px solid var(--op-border); border-radius: 9px;
    box-shadow: var(--op-shadow); padding: 6px 10px; animation: op-tipin .12s ease both;
  }
  .op-spk-tip-m { font-family: 'Geist Mono', monospace; font-size: 9px; letter-spacing: .12em; text-transform: uppercase; color: var(--op-faint); }
  .op-spk-tip-v { font-family: 'Geist Mono', monospace; font-size: 12.5px; font-weight: 600; color: var(--op-green); font-variant-numeric: tabular-nums; }
  .op-spk-tip-v em { font-style: normal; font-size: 9.5px; color: var(--op-faint); font-weight: 500; margin-left: 2px; }
  /* Below ~290px the stat column is narrower than the nowrap figure, so its ink
     pokes past the section's border rules. Let the unit drop to its own line;
     the multiplier itself stays unbreakable, so "4.1x" never splits. Kept at
     420px rather than 320px because the value is data-driven -- a two-digit
     multiplier widens the ink and would otherwise overhang at 360-420px too. */
  @media (max-width: 420px) {
    .hub-status-stat-v { white-space: normal; }
    .hub-status-stat-v .hi { white-space: nowrap; }
  }
  /* Brand mint reads at ~1.7:1 on a white card, so light mode uses the deepened
     green the hub already ships for accessible text; dark mode takes the mint
     itself, which is legible there. Only the measured figure is accented — the
     staked-ETH number beside it is the fleet total and stays ink. */
  .hub-status-stat-v .hi { color: var(--sc-accent); }
  /* Says on the card face, not only in its tip, that the bars are a fixture. */
  .op-sample-flag {
    font-family: 'Geist Mono', monospace; font-style: normal; font-size: 9px; font-weight: 500;
    letter-spacing: .1em; text-transform: uppercase; color: var(--op-muted);
    border: 1px solid var(--op-border); border-radius: 5px; padding: 2px 5px; vertical-align: 2px;
  }
  .op-stats-samplebar {
    display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
    margin: 0 0 18px; padding: 10px 14px; border-radius: 12px;
    background: var(--op-card); border: 1px solid var(--op-border);
    font-size: 12.5px; color: var(--op-muted); line-height: 1.5;
  }
  .hub-status-partners { margin-top: 64px; }
  .hub-status-partners .op-marquee { margin-top: 18px; }
  /* Larger marks now the card has the room — the strip was sized when this
     block sat tighter under the figures. */
  .hub-status-partners .op-marquee-track { gap: 32px; }
  .hub-status-partners .op-marquee-item { gap: 11px; }
  .hub-status-partners .op-marquee-avatar { width: 28px; height: 28px; border-radius: 8px; }
  .hub-status-partners .op-marquee-name { font-size: 13.5px; color: var(--sc-mute); }

  /* ─── report cards — ported from the Optimum Hub design canvas ───
     Brand palette rather than a traffic-light one: mumP2P is carbon (ink) and
     the gossipsub baseline is plutonium (cool steel), so the comparison reads
     as two materials rather than good-vs-bad. Tokens are stack-scoped so the
     rest of the page keeps its own. */
  .hub-perf-stack,
  .hub-directory {
    /* The design canvas puts its #F0F2F4 cards on alabaster; the hub's ground is
       already grey (#F5F7F9). At #EAEEF2 the card sat only 1.09:1 against the
       page — technically distinct, visually almost floating. Stepping toward the
       brand's steel takes it to ~1.18:1, enough that the card and its corners
       read as a surface without turning heavy. */
    --sc-fg: #16181A; --sc-mute: #6A757F; --sc-surface: #DFE5EC; --sc-accent: #1C9A54;
    --sc-hair: rgba(0,0,0,0.08); --sc-track: rgba(51,54,60,0.09);
    --sc-bar-mum: #33363C; --sc-bar-lib: #B0BED1;
    --sc-tile: rgba(255,255,255,0.62);
    /* Bloom paints over --sc-tile, so it is the overlay that lands on the peak
       rather than the peak itself. The light peak is opaque white. */
    --sc-tile-bloom: #FFFFFF;
    --sc-tile-glow: rgba(90,100,110,0.17);
    --sc-shadow: 0 4px 76px 0 rgba(113,121,129,0.10);
    --sc-disp: "ABC Diatype", "Inter Tight", "Helvetica Neue", system-ui, sans-serif;
  }
  .hub-perf-stack { display: flex; flex-direction: column; gap: clamp(20px, 2.4vw, 32px); }
  .hub-root[data-theme="dark"] .hub-perf-stack,
  .hub-root[data-theme="dark"] .hub-directory {
    --sc-fg: #FFFBFB; --sc-mute: #B0BED1; --sc-surface: #33363C; --sc-accent: #64E27F;
    --sc-hair: rgba(255,251,251,0.10); --sc-track: rgba(255,251,251,0.10);
    --sc-bar-mum: #FFFBFB; --sc-bar-lib: #8D9CA9;
    --sc-tile: rgba(255,255,255,0.045);
    /* Peak is 0.145 white: 0.045 + a(1 - 0.045) = 0.145, so a = 0.105. Layering
       0.145 itself would over-brighten to 0.183. */
    --sc-tile-bloom: rgba(255,255,255,0.105);
    --sc-tile-glow: rgba(0,0,0,0.32);
    --sc-shadow: 0 4px 76px 0 rgba(0,0,0,0.45);
  }
  .hub-perf-pair { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(330px, 100%), 1fr)); gap: clamp(20px, 2.4vw, 32px); }

  /* The brand's asymmetric corner rhythm, alternating card to card. */
  .hub-perf-card {
    background: var(--sc-surface); box-shadow: var(--sc-shadow);
    padding: clamp(26px, 3vw, 44px); border-radius: 80px 20px 80px 20px;
    min-width: 0;
  }
  /* Side by side the two cards share an edge, so the alternating brand pattern
     put an 80px curve on both sides of the gap and flared it into a funnel.
     Large corners go on the pair's OUTER edges, small ones face the gap, so the
     two read as one slab split in half. */
  @media (min-width: 721px) {
    .hub-perf-pair > .hub-perf-card:nth-child(odd) { border-radius: 80px 20px 20px 80px; }
    .hub-perf-pair > .hub-perf-card:nth-child(even) { border-radius: 20px 80px 80px 20px; }
  }
  /* Stacked, each card gets the standard alternating rhythm again. */
  @media (max-width: 720px) {
    .hub-perf-pair > .hub-perf-card:nth-child(even) { border-radius: 20px 80px 20px 80px; }
  }
  .hub-perf-title {
    display: flex; align-items: center; gap: 6px;
    font-family: 'Geist', sans-serif; font-weight: 700; font-size: 12px;
    letter-spacing: .08em; text-transform: uppercase; color: var(--sc-fg);
  }
  /* Sentence case, body face — the definition is prose, not a label. */
  .hub-perf-def { margin-top: 10px; font-size: 14px; line-height: 1.5; color: var(--sc-mute); }
  .hub-perf-bars { margin-top: 34px; display: grid; gap: 26px; }
  .hub-perf-bar-top { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
  .hub-perf-bar-top .k { font-family: 'Geist', sans-serif; font-weight: 700; font-size: 14px; color: var(--sc-fg); }
  .hub-perf-bar-top .k.lib { color: var(--sc-mute); }
  .hub-perf-bar-top .v { font-family: 'Geist Mono', monospace; font-size: 15px; font-variant-numeric: tabular-nums; color: var(--sc-fg); }
  .hub-perf-bar-top .v em { font-style: normal; color: var(--sc-mute); }
  .hub-perf-bar .track { display: block; margin-top: 12px; height: 9px; border-radius: 6px; background: var(--sc-track); overflow: hidden; }
  .hub-perf-bar .track i { display: block; height: 100%; border-radius: 6px; animation: hubBarIn 1s cubic-bezier(.22,.61,.36,1) both; }
  .hub-perf-bar .track i.mum { background: var(--sc-bar-mum); }
  .hub-perf-bar .track i.lib { background: var(--sc-bar-lib); animation-delay: .12s; }
  .hub-perf-stats {
    margin-top: 34px; padding-top: 26px; border-top: 1px solid var(--sc-hair);
    display: grid; grid-template-columns: repeat(auto-fit, minmax(110px, 1fr)); gap: 20px;
  }
  .hub-perf-stats .v { display: block; font-family: var(--sc-disp); font-weight: 300; font-size: 30px; line-height: 1; letter-spacing: -0.03em; color: var(--sc-fg); }
  .hub-perf-stats .v em { font-style: normal; }
  .hub-perf-stats .k { display: block; margin-top: 10px; font-family: 'Geist', sans-serif; font-weight: 700; font-size: 10.5px; letter-spacing: .08em; text-transform: uppercase; color: var(--sc-mute); line-height: 1.4; }

  /* ─── measurement basis — a meta row, not a card ───
     It is methodology, so it must not carry the same weight as a result. Same
     treatment the design canvas uses for its run header: inline label/value
     pairs between two hairlines, no surface, no shadow, no radius. */
  .hub-runmeta {
    display: flex; flex-wrap: wrap; align-items: baseline; gap: 12px 36px;
    padding: 18px 4px; border-top: 1px solid var(--sc-hair); border-bottom: 1px solid var(--sc-hair);
  }
  .hub-runmeta-pair { display: inline-flex; align-items: baseline; gap: 10px; min-width: 0; }
  .hub-runmeta .k {
    font-family: 'Geist Mono', monospace; font-size: 10px; font-weight: 500;
    letter-spacing: .12em; text-transform: uppercase; color: var(--sc-mute); white-space: nowrap;
  }
  .hub-runmeta .v { font-size: 14px; font-weight: 500; color: var(--sc-fg); }
  /* On a phone the wrapped row reads as prose. display:contents lifts each pair's
     label and value into the parent grid, so they line up as a two-column list
     instead of flowing into each other. */
  @media (max-width: 560px) {
    .hub-runmeta {
      display: grid; grid-template-columns: auto minmax(0, 1fr);
      gap: 9px 14px; padding: 16px 2px;
    }
    .hub-runmeta-pair { display: contents; }
    .hub-runmeta .k { font-size: 9.5px; letter-spacing: .1em; }
    .hub-runmeta .v { font-size: 13.5px; }
  }

  /* ─── network advantage — the percentile ladder ─── */
  .hub-netadv {
    background: var(--sc-surface); box-shadow: var(--sc-shadow);
    padding: clamp(28px, 3.4vw, 52px); border-radius: 80px 20px 80px 20px;
    display: grid; grid-template-columns: repeat(auto-fit, minmax(min(460px, 100%), 1fr));
    gap: clamp(28px, 3.4vw, 56px); align-items: center;
  }
  .hub-netadv-eyebrow { font-family: 'Geist', sans-serif; font-weight: 700; font-size: 12px; letter-spacing: .08em; text-transform: uppercase; color: var(--sc-mute); }
  .hub-netadv-lede h3 {
    margin: 16px 0 0; max-width: 20ch; font-family: var(--sc-disp); font-weight: 300;
    font-size: clamp(26px, 2.8vw, 40px); line-height: 1.1; letter-spacing: -0.03em;
    color: var(--sc-fg); text-wrap: pretty;
  }
  .hub-netadv-lede p { margin: 16px 0 0; max-width: 40ch; font-size: 15px; line-height: 1.6; color: var(--sc-mute); }
  .hub-netadv-rows { min-width: 0; display: grid; gap: 16px; align-content: center; }
  .hub-netadv-row { display: grid; grid-template-columns: minmax(52px,auto) minmax(0,1fr) auto; align-items: center; gap: 16px; }
  .hub-netadv-row .p { font-family: 'Geist', sans-serif; font-weight: 700; font-size: 13px; color: var(--sc-fg); }
  .hub-netadv-row .track { display: block; height: 22px; border-radius: 6px; background: var(--sc-track); overflow: hidden; }
  /* Steel into ink: the lead deepens as the percentile climbs. */
  .hub-netadv-row .track i { display: block; height: 100%; border-radius: 6px; background: linear-gradient(90deg, var(--sc-bar-lib), var(--sc-bar-mum)); animation: hubBarIn 1.1s cubic-bezier(.22,.61,.36,1) both; }
  .hub-netadv-row .v { font-family: 'Geist Mono', monospace; font-size: 12.5px; color: var(--sc-fg); white-space: nowrap; }
  .hub-netadv-row .v em { font-style: normal; color: var(--sc-mute); }
  @media (max-width: 620px) { .hub-netadv-row { grid-template-columns: minmax(40px,auto) minmax(0,1fr); } .hub-netadv-row .v { grid-column: 2; } }
  /* Below ~380px the value column can be narrower than the whole nowrap string
     "148.5 ms - 6,582 slots". Let it break, but ONLY between the millisecond
     figure and the slot count: flex makes those two an anonymous item and the
     <em>, each inheriting the nowrap, so no number is ever parted from its
     unit. white-space: normal would instead break after "6,582". */
  @media (max-width: 380px) {
    .hub-netadv-row .v { display: flex; flex-wrap: wrap; }
  }

  /* Public operator directory — the announced-partner roster at the foot of the
     page. Logo and name only: the ticker above is the highlight reel, this is
     the full list, so it carries no figures that would invite ranking. */
  .hub-directory { margin-top: clamp(20px, 2.4vw, 32px); }
  .hub-directory-title {
    margin: 0 0 18px; font-family: 'Geist', sans-serif; font-size: 15px;
    font-weight: 600; letter-spacing: 0; color: var(--sc-fg);
  }
  .hub-directory-search {
    display: flex; align-items: center; gap: 9px;
    height: 42px; padding: 0 14px; margin-bottom: 16px;
    border: 1px solid var(--sc-hair); border-radius: 20px 8px 20px 8px;
    background: var(--sc-tile); transition: border-color .15s ease;
  }
  .hub-directory-search:focus-within { border-color: var(--sc-accent); }
  .hub-directory-search > svg { flex: none; color: var(--sc-mute); }
  .hub-directory-search input {
    flex: 1; min-width: 0; border: 0; outline: none; background: transparent; color: var(--sc-fg);
    font-family: 'Geist Mono', monospace; font-size: 12px;
  }
  .hub-directory-search input::placeholder { color: var(--sc-mute); }
  .hub-directory-search input::-webkit-search-cancel-button { -webkit-appearance: none; }
  .hub-directory-clear {
    flex: none; display: inline-flex; align-items: center; justify-content: center;
    width: 20px; height: 20px; padding: 0; border: 0; border-radius: 6px;
    background: transparent; color: var(--sc-mute); cursor: pointer;
  }
  .hub-directory-clear:hover { background: var(--sc-track); color: var(--sc-fg); }
  /* auto-fill, so the columns thin out on their own down to phone width. */
  .hub-directory-grid { display: grid; gap: 10px; grid-template-columns: repeat(auto-fill, minmax(178px, 1fr)); }
  /* The card's corner rhythm, scaled down so a tile echoes it. */
  .hub-directory-tile {
    display: flex; align-items: center; gap: 10px; min-width: 0;
    padding: 9px 12px; border: 1px solid var(--sc-hair); border-radius: 20px 8px 20px 8px;
    background: var(--sc-tile);
    position: relative; isolation: isolate;
  }
  .hub-directory-tile .hub-fleet-tbl-avatar { width: 26px; height: 26px; border-radius: 8px; }
  .hub-directory-name {
    font-family: 'Geist', sans-serif; font-size: 13px; color: var(--sc-fg);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  .hub-directory-tile.is-link { text-decoration: none; cursor: pointer; transition: border-color .15s ease; }
  .hub-directory-tile.is-link:hover { border-color: var(--sc-bar-lib); }
  .hub-directory-tile.is-link:focus-visible { outline: 2px solid var(--sc-accent); outline-offset: 2px; }
  /* Marks the tile as outbound; appears on hover so the default grid stays quiet. */
  .hub-directory-out { flex: none; margin-left: auto; color: var(--sc-mute); opacity: 0; transition: opacity .15s ease; }
  .hub-directory-tile.is-link:hover .hub-directory-out,
  .hub-directory-tile.is-link:focus-visible .hub-directory-out { opacity: 1; }
  /* Propagation ripple (design canvas option 9b): a wave leaves one operator's
     tile and the rest brighten as it reaches them. Delays are derived from the
     live column count — this grid reflows, and the canvas assumed six.

     The bloom rides a pseudo-element so the keyframes touch only opacity and
     transform, both composited. Keyframing background and box-shadow on the
     tile instead repaints all 22 every frame, which is why this used to be
     disabled on touch; it runs there now. */
  .hub-directory-tile::after {
    content: ''; position: absolute; inset: 0; z-index: -1; pointer-events: none;
    border-radius: inherit; opacity: 0;
    background: var(--sc-tile-bloom);
    box-shadow: 0 7px 20px var(--sc-tile-glow);
  }
  @keyframes hubRippleBloom {
    0%, 100% { opacity: 0; }
    7% { opacity: 1; }
    30% { opacity: 0; }
  }
  @keyframes hubRippleLift {
    0%, 100% { transform: scale(1); }
    7% { transform: scale(1.035); }
    30% { transform: scale(1); }
  }
  .hub-directory-tile { animation: hubRippleLift 2.8s ease-out infinite; animation-delay: var(--d, 0s); }
  .hub-directory-tile::after { animation: hubRippleBloom 2.8s ease-out infinite; animation-delay: var(--d, 0s); }
  /* The wave reads as the roster propagating; a filtered set is not the roster. */
  .hub-directory-wave.is-static .hub-directory-tile,
  .hub-directory-wave.is-static .hub-directory-tile::after { animation: none; }
  @media (prefers-reduced-motion: reduce) {
    .hub-directory-tile,
    .hub-directory-tile::after { animation: none; }
  }
  .hub-directory-empty {
    padding: 26px 2px; font-size: 13px; color: var(--sc-mute);
    font-family: 'Geist Mono', monospace;
  }
  @media (max-width: 520px) {
    .hub-directory-grid { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 8px; }
  }

  /* data tables */
  .op-tbl-wrap { border: 1px solid var(--op-hair); border-radius: 12px; overflow: hidden; }
  .op-scroll { max-height: 340px; overflow: auto; }
  .op-tbl { width: 100%; border-collapse: collapse; font-family: 'Geist Mono', monospace; font-variant-numeric: tabular-nums; }
  .op-tbl th {
    position: sticky; top: 0; z-index: 2; text-align: left; padding: 11px 14px;
    font-size: 10px; font-weight: 500; letter-spacing: .1em; text-transform: uppercase; color: var(--op-muted);
    background: var(--op-chip); border-bottom: 1px solid var(--op-border); white-space: nowrap; vertical-align: bottom;
  }
  .op-tbl th.r, .op-tbl td.r { text-align: right; }
  .op-tbl th.c, .op-tbl td.c { text-align: center; }
  .op-tbl th em { display: block; font-style: normal; font-size: 9px; letter-spacing: .08em; color: var(--op-faint); margin-top: 3px; }
  .op-tbl td { padding: 10px 14px; font-size: 12.5px; color: var(--op-text); border-bottom: 1px solid var(--op-hair); white-space: nowrap; }
  .op-tbl tbody tr:last-child td, .op-tbl tbody tr:last-child td { border-bottom: none; }
  .op-tbl tr:hover td { background: var(--op-chip); }
  .op-tbl td.dim { color: var(--op-muted); }
  .op-tbl td.good { color: var(--op-green); }
  .op-tbl td.warn { color: var(--op-peach); }
  .op-tbl td.bad  { color: #FE6262; }

  /* Per-slot mumP2P-vs-libP2P table — each metric gets its OWN column: a coloured
     value + a mini-bar on a grey track (green = mumP2P, peach = libP2P). Keeping
     each number with its own bar makes "which ms belongs to which bar" obvious. */
  .op-tbl-race td { vertical-align: middle; padding-top: 13px; padding-bottom: 13px; white-space: normal; }
  .op-tbl-race th { white-space: nowrap; }
  /* Compact legend in the panel head: a colour swatch per protocol. */
  .op-race-legend { display: flex; align-items: center; gap: 16px; }
  .op-race-legend span { display: inline-flex; align-items: center; gap: 6px; font-family: 'Geist Mono', monospace; font-size: 10px; letter-spacing: .1em; text-transform: uppercase; color: var(--op-muted); }
  .op-race-legend i { width: 9px; height: 9px; border-radius: 3px; flex: none; }
  .op-race-legend i.mum { background: linear-gradient(90deg, var(--op-mint2), var(--op-mint)); }
  .op-race-legend i.lib { background: var(--op-peach); }
  .op-m-cell { min-width: 150px; }
  .op-m-val { font-family: 'Geist Mono', monospace; font-size: 13px; font-weight: 600; font-variant-numeric: tabular-nums; margin-bottom: 8px; white-space: nowrap; }
  .op-m-val small { font-size: 11px; font-weight: 500; opacity: 0.85; }
  .op-m-track { height: 6px; border-radius: 9999px; background: var(--op-track); overflow: hidden; }
  .op-m-fill { display: block; height: 100%; border-radius: 9999px; }
  .op-m-fill.mum { background: linear-gradient(90deg, var(--op-mint2), var(--op-mint)); }
  .op-m-fill.lib { background: var(--op-peach); }
  .op-accel { display: inline-block; font-size: 11px; padding: 2px 8px; border-radius: 999px; }
  .op-accel.good { color: var(--op-green); background: rgba(100,226,127,0.14); }
  .op-accel.warn { color: var(--op-peach); background: rgba(254,161,88,0.16); }

  /* window segmented control */
  .op-winrow { display: inline-flex; align-items: center; gap: 9px; }
  .op-winseg-lbl { font-family: 'Geist Mono', monospace; font-size: 9.5px; letter-spacing: .11em; text-transform: uppercase; color: var(--op-faint); }
  .op-winseg { display: inline-flex; gap: 3px; padding: 4px; border-radius: 9999px; background: var(--op-chip); border: 1px solid var(--op-hair); }
  .op-winseg button {
    font-family: 'Geist Mono', monospace; font-size: 12px; padding: 6px 13px; border-radius: 9999px;
    border: none; background: transparent; color: var(--op-muted); cursor: pointer; transition: all .16s ease;
  }
  .op-winseg button:hover { color: var(--op-text); }
  .op-winseg button.on { color: var(--op-text); background: var(--op-card); box-shadow: 0 1px 5px rgba(0,0,0,0.08); }

  /* ── Page-level filter / measurement-basis toolbar ──
     One control surface: left communicates the single contextual fact
     (connected operators), right sets percentile + window (the filters that
     govern every figure below). Replaces the old left rail + in-card winrow. */
  .op-filterbar {
    display: flex; align-items: center; justify-content: space-between;
    gap: 16px 28px; flex-wrap: wrap;
    margin-bottom: 22px; padding: 15px 22px;
    border-radius: 18px; border: 1px solid var(--op-border);
    background: var(--op-card); box-shadow: var(--op-shadow);
    position: relative; z-index: 25;
  }
  .op-fb-context { display: inline-flex; align-items: center; gap: 18px; flex-wrap: wrap; min-width: 0; }
  .op-fb-eyebrow {
    font-family: 'Geist Mono', monospace; font-size: 9.5px; letter-spacing: .16em;
    text-transform: uppercase; color: var(--op-faint); white-space: nowrap;
  }
  .op-fb-operators {
    display: inline-flex; align-items: baseline; gap: 10px;
    padding-left: 18px; border-left: 1px solid var(--op-hair);
  }
  .op-fb-op-v {
    font-family: 'Geist Mono', monospace; font-size: 22px; font-weight: 600;
    letter-spacing: -0.02em; line-height: 1; color: var(--op-text);
    font-variant-numeric: tabular-nums;
  }
  .op-fb-op-k {
    display: inline-flex; align-items: center;
    font-family: 'Geist Mono', monospace; font-size: 9.5px; letter-spacing: .1em;
    text-transform: uppercase; color: var(--op-muted); white-space: nowrap;
  }
  .op-fb-controls { display: inline-flex; align-items: center; gap: 22px; flex-wrap: wrap; }
  .op-fb-seg { display: inline-flex; align-items: center; gap: 9px; }

  @media (max-width: 720px) {
    .op-filterbar { flex-direction: column; align-items: stretch; gap: 16px; padding: 15px 16px; }
    .op-fb-controls { justify-content: space-between; gap: 14px 22px; }
  }
  @media (max-width: 460px) {
    .op-fb-context { gap: 12px; }
    .op-fb-operators { padding-left: 12px; }
    .op-fb-seg { gap: 7px; }
  }

  /* averages grid */
  .op-avg-grid { display: flex; flex-direction: column; gap: 1px; border: 1px solid var(--op-hair); border-radius: 12px; overflow: hidden; background: var(--op-hair); }
  .op-avg-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 15px 18px; background: var(--op-card); }
  .op-avg-row .k { font-size: 13px; color: var(--op-muted); }
  .op-avg-row .v { font-family: 'Geist Mono', monospace; font-variant-numeric: tabular-nums; font-size: 21px; font-weight: 500; color: var(--op-text); letter-spacing: -0.01em; }
  .op-avg-row .v.good { color: var(--op-green); }
  .op-avg-row.hl { background: color-mix(in srgb, var(--op-mint) 12%, var(--op-card)); }
  .op-avg-row.hl .k { color: var(--op-text); font-weight: 500; }
  .op-avg-row.hl .v { font-size: 26px; }

  /* coming-soon empty state */
  .op-coming { text-align: center; padding: 60px 30px; }
  .op-coming-badge {
    display: inline-block; font-family: 'Geist Mono', monospace; font-size: 11px; letter-spacing: .1em;
    text-transform: uppercase; padding: 5px 12px; border-radius: 999px; border: 1px solid var(--op-border);
    color: var(--op-muted); background: var(--op-chip); margin-bottom: 16px;
  }
  .op-coming-title { font-size: 22px; font-weight: 600; color: var(--op-text); margin-bottom: 10px; }
  .op-coming-sub { font-size: 13.5px; color: var(--op-muted); line-height: 1.6; max-width: 440px; margin: 0 auto; }

  @media (max-width: 820px) {
    .op-stats-body { grid-template-columns: 1fr; }
    .op-stats-nav { position: static; flex-direction: row; flex-wrap: wrap; align-items: stretch; }
    .op-stats-navgroup { width: 100%; padding: 4px 8px; }
  }

  /* ============================================================
     MOBILE RESPONSIVE PASS — consolidated, desktop-safe.
     Appended at the END of the stylesheet so the fleet-table +
     stat-grid overrides win by source order (equal specificity)
     over the base rules and the <=980px block. Narrow-phone rules
     are capped at 480px; landscape, touch and safe-area hardening
     below use capability-specific queries.
     ============================================================ */
  @media (max-width: 480px) {
    /* ---- Nav / chrome: stop the burger clipping at the right edge ---- */
    .hub-nav { gap: 10px; padding: 8px 10px 8px 12px; }
    .hub-brand { gap: 8px; font-size: 14px; min-height: 42px; }
    .hub-brand-mark { width: 30px; height: 19px; }
    .hub-chain-trigger { min-height: 42px; min-width: 42px; padding: 7px 8px; gap: 6px; justify-content: center; }
    .hub-theme-toggle,
    .hub-burger { width: 42px; height: 42px; flex: 0 0 42px; }

    /* The map affordance was only 24px tall, which is easy to miss with a thumb. */
    .hub-globe-expand { min-height: 40px; padding: 8px 11px; }

    /* The current header has three facts, so keep those three facts on one
       baseline instead of leaving Network Status alone on a second row. */
    .hub-hero-meta { grid-template-columns: repeat(3, minmax(0, 1fr)); min-width: 0; }
    .hub-hero-meta .cell { min-width: 0; padding: 8px 7px; }
    .hub-hero-meta .k { font-size: 8.5px; letter-spacing: .06em; margin-bottom: 3px; }
    .hub-hero-meta .v { font-size: 10px; white-space: nowrap; }


    /* ---- Stat-column labels: reserve a consistent 2-line height so the big
       numbers stay aligned across columns even when one label ("MUMP2P
       CONNECTED") wraps to two lines and another ("TOTAL STAKED") does not.
       Bottom-anchored so single-line labels still sit right above their number. */
    .op-stat-k { min-height: 2.6em; display: flex !important; align-items: flex-end !important; }
    .op-stats-about-metric .k { min-height: 2.4em; display: flex; align-items: flex-end; }

    /* ---- Fleet / top-validators tables (Overview + Validators page) ----
       Release the 540px min-width floor (clipped under overflow:hidden)
       and collapse the dead 4th spacer track to the 3 real fluid cols. */
    .hub-fleet-tbl-head,
    .hub-fleet-tbl-row {
      min-width: 0;
      grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr) minmax(0, 1fr);
    }
    .hub-fleet-tbl-head .cell {
      padding: 11px 9px;
      font-size: 9px;
      letter-spacing: 0.05em;
    }
    .hub-fleet-tbl-row .cell {
      padding: 11px 9px;
      font-size: 12px;
    }
    .hub-fleet-tbl-row .cell.partner { gap: 9px; }
    .hub-fleet-tbl-row .cell.partner b { font-size: 12.5px; }
    .hub-fleet-tbl-avatar { width: 28px; height: 28px; }
    .hub-fleet-tbl-wrap { padding: 10px; }

    /* Filter row: full-width search, count drops to its own line */
    .hub-fleet-tbl-filters { gap: 8px; margin-bottom: 10px; }
    .hub-fleet-tbl-search { flex: 1 1 100%; }
    .hub-fleet-tbl-count { margin-left: 0; }

    /* ---- Stats view: panels, window selector, basis facts, tooltip ---- */
    .op-panel { padding: 16px 14px; border-radius: 14px; }
    .op-panel-head { margin-bottom: 14px; }
    .op-winrow { flex-wrap: wrap; gap: 7px; }
    .op-winseg { flex-wrap: wrap; }
    .op-winseg button { padding: 6px 11px; }
    .op-stats-about-metric { flex: 1 1 92px; padding: 10px 10px 0; margin-top: 0; border-top: none; }
    .op-stats-about-metric .v { font-size: 22px; }
    .op-stats-about-first .v { font-size: 22px; }
    .op-strip-tip { max-width: 78vw; white-space: normal; }

    /* ---- Validators page: title + pager ---- */
    .op-validators-head { margin-bottom: 16px; gap: 10px; }
    .op-validators-head h2 { font-size: 22px; }
    .op-pager { gap: 8px; }
    .op-pager-info { flex: 1 1 100%; order: -1; text-align: center; }
    .op-pager-btn { flex: 1 1 auto; padding: 12px 16px; text-align: center; }

    /* ---- Atlas map modal: near-full-screen + bigger tap targets ---- */
    .op-atlas-overlay { padding: 0; }
    .op-atlas-modal {
      width: 100%; max-width: 100%; height: 100%; max-height: 100vh;
      max-height: 100dvh;
      border-radius: 0; border: none;
    }
    .op-atlas-head { padding: 13px 14px; gap: 10px; }
    .op-atlas-title { font-size: 15px; }
    .op-atlas-max, .op-atlas-close { width: 40px; height: 40px; }
    .op-atlas-canvas { padding: 4px 6px; }
    .op-atlas-zoom { right: 12px; bottom: 12px; }
    .op-atlas-zoom button { width: 40px; height: 40px; }
    .op-atlas-tip {
      max-width: calc(100vw - 20px);
      white-space: normal;
      flex-wrap: wrap;
    }

    /* ---- Overlays: help tooltip, detail modal, help card ---- */
    /* On a phone a 240px tip centred on its marker runs off the screen: the
       markers sit between 41% and 91% of the viewport, so neither centring
       nor a blanket left/right anchor fits them all. Anchoring to the marker
       is the problem, so stop doing it here and span the card instead.
       Dropping .op-help to static hands the containing block to the enclosing
       card, which is already transformed (the reveal animation) and therefore
       already a containing block for absolute descendants -- so the tip
       inherits the card's width and cannot leave the screen. bottom/top:auto
       keeps it vertically at its static position, next to its own marker.
       position:fixed would NOT work here for the same reason it works: the
       transformed ancestor would capture it rather than the viewport. */
    .op-help { position: static; }
    .op-help .op-help-tip {
      left: 12px; right: 12px; width: auto; max-width: none;
      transform: none; bottom: auto; top: auto; margin-top: 9px;
    }
    .op-help.op-help-down .op-help-tip,
    .op-help.op-help-start .op-help-tip {
      left: 12px; right: 12px; top: auto; bottom: auto; transform: none;
    }
    .op-modal-scrim { padding: 16px; }
    .op-modal {
      padding: 22px 20px;
      max-height: calc(100vh - 32px);
      max-height: calc(100dvh - 32px);
      overflow-y: auto;
    }
    .op-modal-close { width: 34px; height: 34px; }
    .op-modal-count { font-size: 34px; }
  }

  @media (max-width: 380px) {
    /* ---- Nav: drop the "Hub" sub-wordmark + divider to free width ---- */
    .hub-nav { gap: 8px; }
    .hub-brand-div,
    .hub-brand-sub { display: none; }

    /* A right-aligned 210px menu starts outside a 320px viewport because the
       compact chain button sits left of the two utility buttons. Centre the
       dropdown on its trigger so both edges stay comfortably on-screen. */
    .hub-chain-drop { left: 50%; right: auto; transform: translateX(-50%); }


    /* ---- Fleet tables: extra squeeze for ~320px ---- */
    .hub-fleet-tbl-head .cell,
    .hub-fleet-tbl-row .cell { padding: 10px 7px; }
    .hub-fleet-tbl-row .cell { font-size: 11.5px; }
    .hub-fleet-tbl-avatar { width: 24px; height: 24px; }

    /* ---- Stats window pill: keep 25/50/100/Loading on one wrap ---- */
    .op-winseg button { padding: 6px 9px; font-size: 11px; }

    /* ---- Validators title ---- */
    .op-validators-head h2 { font-size: 20px; }
  }

  /* ==================================================================
     Phone held in landscape: 800-932px WIDE but only ~360-430px TALL.
     Every breakpoint above is keyed on width, so these handsets clear
     all of them and get the full desktop layout on a 390px-tall screen
     -- a 78px sticky nav eating 20% of the viewport, and a globe well
     measured at 485px, taller than the whole screen. Keyed on height
     and a coarse pointer, never on width: a mouse desktop reports
     pointer:fine, and an iPad in landscape is 768-1024px tall, so
     neither can match this.
     ================================================================== */
  @media (orientation: landscape) and (max-height: 500px) and (max-width: 1100px) and (pointer: coarse) {
    .hub-nav { top: 6px; gap: 12px; padding: 6px 10px 6px 12px; }

    .hub-globe-card { min-height: 0; gap: 20px; --card-pad: 18px; }
    .hub-globe-pane { min-height: 232px; }
    .hub-metrics-panel { min-height: 0; gap: 10px; }
    .hub-globe-metrics-grid { gap: 8px; }
    .hub-globe-metrics-grid .hub-kpi { min-height: 0; padding: 12px 14px 11px; }

    .op-atlas-overlay { padding: 0; }
    .op-atlas-modal {
      width: 100%; max-width: 100%; height: 100%; max-height: 100%;
      border-radius: 0; border: none;
    }
    .op-atlas-head { padding: 8px 14px; gap: 10px; }
    .op-atlas-title { margin-top: 2px; font-size: 15px; }
    .op-atlas-subhint { display: none; }
    .op-atlas-foot { padding: 6px 14px; }
    .op-atlas-foot .af-note { font-size: 10px; line-height: 1.3; }
    .op-atlas-max, .op-atlas-close { width: 40px; height: 40px; }
    .op-atlas-zoom button { width: 40px; height: 40px; }
  }

  /* ==================================================================
     Touch-device hardening.
     Everything here is scoped to (pointer: coarse), which a desktop
     mouse never matches, so none of it can reach the desktop layout.
     ================================================================== */
  @media (pointer: coarse) {
    /* iOS Safari zooms the page in whenever a focused input renders its
       text below 16px, and gives you no way back out. Every search field
       on the page was 13px. */
    .hub-fleet-tbl-search input,
    .hub-obs-search input,
    .hub-obs-filterbar input,
    .hub-directory-search input { font-size: 16px; }
    /* This one is flex:1 with no min-width:0, so the larger text raises its
       intrinsic minimum and would widen the row. */
    .hub-fleet-tbl-search input { min-width: 0; }

    /* The clear glyph is visually small but needs a forgiving thumb target. */
    .hub-directory-clear { width: 32px; height: 32px; margin-right: -6px; }

    /* The default iOS tap flash is a hard black box that reads as
       breakage over our rounded cards. Keep the feedback, brand it. */
    .hub-root a, .hub-root button, .hub-root [role="button"], .hub-root [tabindex] {
      -webkit-tap-highlight-color: rgba(100, 226, 127, 0.18);
    }
  }

  /* Edge-to-edge iOS/iPadOS and Android standalone windows. The ordinary browser
     path resolves every env() value to zero, while notched/rounded displays keep
     navigation, overlays and footer content out of the unsafe edge bands. */
  @supports (padding: max(0px)) {
    @media (max-width: 1080px) {
      .hub-shell {
        padding-top: max(22px, env(safe-area-inset-top));
        padding-right: max(32px, env(safe-area-inset-right));
        padding-bottom: max(64px, calc(env(safe-area-inset-bottom) + 24px));
        padding-left: max(32px, env(safe-area-inset-left));
      }
      .hub-nav { top: max(14px, env(safe-area-inset-top)); }
    }

    @media (max-width: 760px) {
      .hub-shell {
        padding-top: max(16px, env(safe-area-inset-top));
        padding-right: max(18px, env(safe-area-inset-right));
        padding-bottom: max(60px, calc(env(safe-area-inset-bottom) + 24px));
        padding-left: max(18px, env(safe-area-inset-left));
      }
    }

    @media (max-width: 480px) {
      .op-atlas-head {
        padding-top: max(13px, env(safe-area-inset-top));
        padding-right: max(14px, env(safe-area-inset-right));
        padding-left: max(14px, env(safe-area-inset-left));
      }
      .op-atlas-foot {
        padding-right: max(14px, env(safe-area-inset-right));
        padding-bottom: max(11px, env(safe-area-inset-bottom));
        padding-left: max(14px, env(safe-area-inset-left));
      }
      .op-atlas-zoom { right: max(12px, env(safe-area-inset-right)); }
      .op-modal-scrim {
        padding-top: max(16px, env(safe-area-inset-top));
        padding-right: max(16px, env(safe-area-inset-right));
        padding-bottom: max(16px, env(safe-area-inset-bottom));
        padding-left: max(16px, env(safe-area-inset-left));
      }
    }

    @media (orientation: landscape) and (max-height: 500px) and (max-width: 1100px) and (pointer: coarse) {
      .hub-nav { top: max(6px, env(safe-area-inset-top)); }
      .op-atlas-head {
        padding-top: max(8px, env(safe-area-inset-top));
        padding-right: max(14px, env(safe-area-inset-right));
        padding-left: max(14px, env(safe-area-inset-left));
      }
      .op-atlas-foot {
        padding-right: max(14px, env(safe-area-inset-right));
        padding-bottom: max(6px, env(safe-area-inset-bottom));
        padding-left: max(14px, env(safe-area-inset-left));
      }
      .op-atlas-zoom { right: max(12px, env(safe-area-inset-right)); }
    }
  }

  /* Safari inflates body text after a rotation unless told not to. */
  .hub-root { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; }
  `;
  document.head.appendChild(s);
}

// ────────────────────────────────────────────────────────────────
// Data — single source of truth
// ────────────────────────────────────────────────────────────────
// The validator fleet lives in services/validators.js (window.OptimumValidators).
// The hub reads from it and re-derives on updates (see the subscription in Hub),
// so a backend refresh flows to every view. `VAL` is a thin, null-safe handle.
const VAL = (typeof window !== 'undefined' && window.OptimumValidators) ? window.OptimumValidators : {
  getReal: () => [], getSynthetic: () => [], getAll: () => [], getLocated: () => [],
  getCount: () => 0, getCountryCounts: () => ({}), regionCountryToken: () => null,
  subscribe: () => (() => {}), refresh: () => Promise.resolve([]),
};

// (The synthetic test fleet + region→country parsing now live in the single
// source of truth, services/validators.js.)

const PARTNER_LOGO_FALLBACK_DOMAINS = {
  EBU: ['www.ebunker.io', 'ebunker.io'],
  NDM: ['www.node.monster', 'node.monster'],
  OBL: ['obol.org', 'obol.tech'],
  HKC: ['www.hashkey.cloud', 'hashkey.cloud'],
  SIM: ['simplystaking.com'],
  CUP: ['cloud.chainup.com', 'chainup.com'],
};

const cleanDomain = (domain) => String(domain || '').replace(/^https?:\/\//, '').replace(/^www\./, '').replace(/\/.*$/, '');

// Curated per-validator brand assets (researched + verified, bundled locally under
// dashboard/assets/logos/). `light` is a colored/self-contained square mark that reads on both
// themes; `dark` is a dedicated dark-background (white) square variant where the brand
// publishes one — swapped in via CSS in dark mode. `site` is the verified official URL.
const LOGO_V = 20261014;
// How long the directory's propagation wave takes to cross the grid. Must leave
// room for the bloom tail (30% of the 2.8s cycle) inside one cycle.
const DIR_WAVE_TRAVEL_S = 1.1;
const CURATED_LOGOS = {
  EVR: { site: 'https://everstake.one',    light: 'dashboard/assets/logos/EVR.jpg' },
  P2P: { site: 'https://p2p.org',          light: 'dashboard/assets/logos/P2P.svg' },
  EBU: { site: 'https://ebunker.io',       light: 'dashboard/assets/logos/EBU.svg' },
  INF: { site: 'https://infstones.com',    light: 'dashboard/assets/logos/INF.ico' },
  HKC: { site: 'https://hashkey.cloud',    light: 'dashboard/assets/logos/HKC.ico' },
  LUG: { site: 'https://luganodes.com',    light: 'dashboard/assets/logos/LUG.png' },
  BLK: { site: 'https://blockdaemon.com',  light: 'dashboard/assets/logos/BLK.png' },
  GFM: { site: 'https://gateway.fm',       light: 'dashboard/assets/logos/GFM.png' },
  RKX: { site: 'https://rockx.com',        light: 'dashboard/assets/logos/RKX.ico' },
  STK: { site: 'https://stakely.io',       light: 'dashboard/assets/logos/STK.svg',  dark: 'dashboard/assets/logos/STK-dark.svg' },
  SIM: { site: 'https://simplystaking.com', light: 'dashboard/assets/logos/SIM.png' },
  NDM: { site: 'https://node.monster',     light: 'dashboard/assets/logos/NDM.svg',  dark: 'dashboard/assets/logos/NDM-dark.svg' },
  OBL: { site: 'https://obol.org',         light: 'dashboard/assets/logos/OBL.png' },
  EFI: { site: 'https://ether.fi',         light: 'dashboard/assets/logos/EFI.svg' },
  CUP: { site: 'https://cloud.chainup.com', light: 'dashboard/assets/logos/CUP.png' },
  MER: { site: 'https://meria.com',        light: 'dashboard/assets/logos/MER.svg' },
  KLN: { site: 'https://kiln.fi',          light: 'dashboard/assets/logos/KLN.svg' },
  DSR: { site: 'https://dsrv.com',         light: 'dashboard/assets/logos/DSR.svg' },
  FCS: { site: 'https://finoa-fcs.io',     light: 'dashboard/assets/logos/FCS.svg' },
  FPV: { site: 'https://validated.4pillars.io', light: 'dashboard/assets/logos/FPV.png' },
  PUF: { site: 'https://puffer.fi',        light: 'dashboard/assets/logos/PUF.png' },
  VLT: { site: 'https://validatrium.com', light: 'dashboard/assets/logos/VLT.svg' },
  VCL: { site: 'https://validationcloud.io', light: 'dashboard/assets/logos/VCL.svg',  dark: 'dashboard/assets/logos/VCL-dark.svg' },
  NAN: { site: 'https://nansen.ai',        light: 'dashboard/assets/logos/NAN.svg',  dark: 'dashboard/assets/logos/NAN-dark.svg' },
  STF: { site: 'https://stake.fish',       light: 'dashboard/assets/logos/STF.png',  dark: 'dashboard/assets/logos/STF-dark.png' },
  TWS: { site: 'https://twinstake.com',   light: 'dashboard/assets/logos/TWS.png' },
};
const logoAsset = (path) => `${path}?v=${LOGO_V}`;

const REGION_LONLAT = {
  'Kyiv, UA': [30.5, 50.5], 'Singapore, SG': [103.8, 1.35], 'Palo Alto, US': [-122.1, 37.4],
  'Hong Kong': [114.2, 22.3], 'Lugano, CH': [8.95, 46.0], 'Zug, CH': [8.5, 47.2],
  'New York, US': [-74.0, 40.7], 'London, UK': [-0.13, 51.5], 'Barcelona, ES': [2.17, 41.4],
  'Malta': [14.4, 35.9], 'Frankfurt, DE': [8.68, 50.1], 'Seoul, KR': [126.98, 37.57],
  'Paris, FR': [2.35, 48.85], 'Global': [-28, 24], 'Distributed': [-44, 10],
};
function atlasXY(region) {
  const ll = REGION_LONLAT[region] || REGION_LONLAT['Global'];
  return { x: (ll[0] + 180) / 360 * 1000, y: (90 - ll[1]) / 180 * 500 };
}
// Build markers for the flat atlas. Every marker is the Optimum loop mark (white in
// dark mode, navy in light mode); we no longer plot validator brand logos here.
// Markers sit at the validator regions AND at the Hermes production fleet's own
// cities (window.OPTIMUM_PRESENCE), which are real deployments rather than
// decoration, and separate from the operator roster.
// TODO(live-data): both halves are positioned from hardcoded tables, REGION_LONLAT
// here and OPTIMUM_PRESENCE in op-globe-light.jsx. Needs a live deployment feed.
// Currently unrendered: ValidatorAtlas draws from data/network-nodes.json instead.
function buildAtlasNodes(partners, dark) {
  // Navy loop in both themes so the map marker matches the globe exactly
  // (the disc behind it is a theme-independent near-white in op-map-light.jsx).
  const mark = (typeof window !== 'undefined' && window.optimumMarkDataUri) ? window.optimumMarkDataUri('#282C34') : '';
  const nodes = [];
  (partners || []).forEach(p => {
    const { x, y } = atlasXY(p.region);
    nodes.push({ id: p.id, name: 'Optimum', region: p.region, x, y, logo: mark });
  });
  const presence = (typeof window !== 'undefined' && window.OPTIMUM_PRESENCE) || [];
  presence.forEach(pr => {
    nodes.push({
      id: pr.id, name: 'Optimum', region: pr.city + ' · ' + pr.region,
      x: (pr.lon + 180) / 360 * 1000, y: (90 - pr.lat) / 180 * 500, logo: mark,
    });
  });
  const groups = {};
  nodes.forEach(n => { const k = Math.round(n.x / 16) + ',' + Math.round(n.y / 16); (groups[k] = groups[k] || []).push(n); });
  Object.values(groups).forEach(g => {
    if (g.length > 1) {
      const rad = 16;
      g.forEach((n, i) => { const a = (i / g.length) * Math.PI * 2 - Math.PI / 2; n.x += Math.cos(a) * rad; n.y += Math.sin(a) * rad; });
    }
  });
  return nodes;
}

// Footer links. Privacy is the company-wide policy, published by the entity that
// operates this site. Terms have no address yet, so they are skipped, not guessed.
const FOOT_LINKS = [
  { label: 'Discord', href: 'https://discord.com/invite/getoptimum' },
  { label: 'X', href: 'https://x.com/get_optimum' },
  { label: 'Blog', href: 'https://www.getoptimum.xyz/blog' },
  { label: 'Privacy Policy', href: 'https://www.getoptimum.xyz/privacy-policy' },
  { label: 'Terms & Conditions', href: null },
];

const partnerSite = (partner) => {
  const c = partner && CURATED_LOGOS[partner.id];
  if (c && c.site) return c.site;
  const d = cleanDomain(partner && partner.domain);
  return d ? `https://${d}` : undefined;
};

// Candidate logo sources for validators without a curated asset — best-quality first.
// logo.clearbit.com was sunset, so we aggregate: unavatar (real brand logos) +
// Google/DuckDuckGo icons, keeping the sharpest that loads.
function partnerLogoSources(partner) {
  const domains = [
    ...(PARTNER_LOGO_FALLBACK_DOMAINS[partner.id] || []),
    partner.domain,
  ]
    .map(cleanDomain)
    .filter(Boolean)
    .filter((domain, index, arr) => arr.indexOf(domain) === index);

  return domains.flatMap(domain => [
    `https://unavatar.io/${domain}?fallback=false`,
    `https://www.google.com/s2/favicons?domain=${domain}&sz=256`,
    `https://icons.duckduckgo.com/ip3/${domain}.ico`,
    `https://www.google.com/s2/favicons?domain=${domain}&sz=128`,
  ]);
}

function PartnerLogo({ partner, fallbackClassName }) {
  const c = CURATED_LOGOS[partner.id];
  if (c) {
    return (
      <React.Fragment>
        <img className={'op-logo-light' + (c.dark ? ' has-dark' : '')} src={logoAsset(c.light)} alt="" loading="lazy" />
        {c.dark ? <img className="op-logo-dark" src={logoAsset(c.dark)} alt="" loading="lazy" /> : null}
      </React.Fragment>
    );
  }
  return <ProbeLogo partner={partner} fallbackClassName={fallbackClassName} />;
}

// Fallback for any validator without a curated asset: probe several icon sources
// and keep the sharpest that loads (≥32px); else a two-letter monogram.
function ProbeLogo({ partner, fallbackClassName }) {
  const sources = React.useMemo(() => partnerLogoSources(partner), [partner.id, partner.domain]);
  const [best, setBest] = React.useState('');
  React.useEffect(() => {
    let alive = true;
    let bestW = 0;
    setBest('');
    const imgs = sources.map(url => {
      const im = new Image();
      im.onload = () => { if (alive && im.naturalWidth > bestW && im.naturalWidth >= 32) { bestW = im.naturalWidth; setBest(url); } };
      im.src = url;
      return im;
    });
    return () => { alive = false; imgs.forEach(im => { im.onload = im.onerror = null; }); };
  }, [sources]);

  return (
    <React.Fragment>
      {best ? <img key={best} src={best} alt="" /> : null}
      <span className={fallbackClassName} style={{ display: best ? 'none' : undefined }}>{partner.id.slice(0, 2)}</span>
    </React.Fragment>
  );
}

// ────────────────────────────────────────────────────────────────
// Hooks
// ────────────────────────────────────────────────────────────────
function formatNum(n) {
  if (n >= 1e6) return (n / 1e6).toFixed(2) + 'M';
  if (n >= 1e3) return (n / 1e3).toFixed(1) + 'k';
  return Math.round(n).toLocaleString('en-US');
}
function formatEthShort(n) {
  return formatNum(n).toUpperCase();
}
// 'YYYY-MM-DD' -> 'Jun 25'. Split rather than new Date(), which would read the
// string as UTC midnight and render the previous day west of Greenwich.
function fmtSpkDay(day) {
  const M = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
  // Trimmed and truncated so an ISO instant renders as a date rather than being
  // printed raw into a 9.5px axis label.
  const p = String(day || '').trim().slice(0, 10).split('-');
  const mo = p.length === 3 ? Number(p[1]) : NaN;
  const d = p.length === 3 ? Number(p[2]) : NaN;
  // Falls back to the raw string rather than rendering "undefined NaN": `day` is
  // endpoint-controlled and only checked to be a string upstream.
  if (!(mo >= 1 && mo <= 12) || !(d >= 1 && d <= 31)) return String(day || '');
  return M[mo - 1] + ' ' + d;
}
// Delegates to formatEthShort so the headline and the tooltip can never disagree
// about units; the guard is the only thing this adds.
function fmtSpkEth(n) {
  return Number.isFinite(n) ? formatEthShort(n) : '—';
}
function formatUsdShort(n) {
  if (n >= 1e9) return '$' + (n / 1e9).toFixed(2) + 'B';
  if (n >= 1e6) return '$' + (n / 1e6).toFixed(1) + 'M';
  if (n >= 1e3) return '$' + (n / 1e3).toFixed(1) + 'K';
  return '$' + Math.round(n).toLocaleString('en-US');
}
function formatUsd(n) {
  return '$' + n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}

function toMetricNumber(value) {
  if (typeof value === 'number') return Number.isFinite(value) ? value : null;
  if (typeof value === 'string') {
    const n = Number(value.replace(/[,\s%ms]/gi, ''));
    return Number.isFinite(n) ? n : null;
  }
  return null;
}




function classifySlotResult(advantage) {
  const n = toMetricNumber(advantage);
  if (n === null) return 'na';
  if (n > 0) return 'faster';
  if (n < 0) return 'slower';
  return 'equal';
}


function percentile(values, p) {
  const nums = (values || []).filter(Number.isFinite).sort((a, b) => a - b);
  if (!nums.length) return null;
  const idx = (nums.length - 1) * p;
  const lo = Math.floor(idx);
  const hi = Math.ceil(idx);
  if (lo === hi) return Math.round(nums[lo]);
  return Math.round(nums[lo] + (nums[hi] - nums[lo]) * (idx - lo));
}

function calculateBestWorstMedian(slots) {
  const values = (slots || []).map(slot => slot.advantage).filter(Number.isFinite);
  if (!values.length) {
    return { best: null, worst: null, median: null, faster: 0, slower: 0, equal: 0, na: (slots || []).length };
  }
  const classified = (slots || []).reduce((acc, slot) => {
    acc[classifySlotResult(slot.advantage)] += 1;
    return acc;
  }, { faster: 0, slower: 0, equal: 0, na: 0 });
  return {
    best: Math.max(...values),
    worst: Math.min(...values),
    median: percentile(values, 0.5),
    faster: classified.faster,
    slower: classified.slower,
    equal: classified.equal,
    na: classified.na,
  };
}



// Per-slot propagation telemetry now lives in the single source of truth
// (services/validators.js → getObservabilitySlots()), keyed per chain and
// generated from a target speed-up so the headline multiple, the timeline and
// the win-rate stay consistent. Read it via VAL.getObservabilitySlots().

// ────────────────────────────────────────────────────────────────
// Fleet-level metric helpers
// ────────────────────────────────────────────────────────────────
const SLOTS_PER_EPOCH = 32;
const FLEET_WINDOW_OPTIONS = [
  { id: 'epoch', label: '32 slots · 1 epoch', size: 32 },
  { id: '128',   label: '128 slots',           size: 128 },
  { id: 'day',   label: '1 day · 7,200 slots', size: 7200 },
];


// calculatePercentile expects only finite values and returns null for empty input.
// p is a fraction 0..1. N/A values must be filtered out by the caller.
function calculatePercentile(values, p) {
  return percentile(values, p);
}

// calculateAccelerationRate(slots) — % of comparable slots where mumP2P is faster
// (advantage > 0). Missing slots are excluded from the denominator.

// Build a fleet-wide per-validator perturbation of the global slot fixture. Slot
// numbers are SHARED across validators (Ethereum slots are global).
// TODO(live-data): the per-validator offsets are seeded hash noise over the
// TELEMETRY fixture. Needs per-operator slot arrivals, which the public report
// deliberately does not carry: it is fleet-wide only, by design.
function getFleetSlotMatrix(partners, windowSize = SLOTS_PER_EPOCH) {
  const fixture = VAL.getObservabilitySlots();
  const flen = fixture.length;
  const size = Math.max(1, windowSize);
  const latestSlot = fixture[flen - 1][0];
  // Build `size` fleet slots (oldest → newest). When the window exceeds the raw
  // sample, expand it deterministically so a 200-slot window has real variety.
  const fleetSlots = [];
  for (let i = 0; i < size; i++) {
    const base = fixture[i % flen];
    const cycle = Math.floor(i / flen);
    const slot = latestSlot - (size - 1 - i);
    let mum = base[1];
    let lib = base[2];
    if (cycle > 0 && Number.isFinite(mum) && Number.isFinite(lib)) {
      const mumDrift = Math.round(Math.sin(i * 0.57 + cycle) * 34 + (cycle - 1.5) * 7);
      const gapDrift = Math.round(Math.cos(i * 0.41 + cycle * 0.9) * 18 - cycle * 2);
      const gap = lib - mum;
      mum = Math.max(80, mum + mumDrift);
      lib = Math.max(80, mum + gap + gapDrift);
    }
    const advantage = Number.isFinite(mum) && Number.isFinite(lib) ? lib - mum : null;
    fleetSlots.push({ slot, mumP2PArrival: mum, libP2PArrival: lib, advantage, result: classifySlotResult(advantage) });
  }
  const seedFor = (id) => String(id || '').split('').reduce((a, c) => a + c.charCodeAt(0), 0);
  const seededWave = (seed, n) => {
    const x = Math.sin(seed * 12.9898 + n * 78.233) * 43758.5453;
    return x - Math.floor(x);
  };

  const perValidator = partners.map((partner, partnerIdx) => {
    const seed = seedFor(partner.id);
    // Onboarding partners get a degraded signal (more N/A, smaller advantage).
    const isOnboarding = partner.status === 'onboarding';
    const slots = fleetSlots.map((row, i) => {
      if (row.advantage === null) {
        // Fleet-wide N/A → propagate to every validator (network-wide issue).
        return { slot: row.slot, mumP2PArrival: null, libP2PArrival: null, advantage: null, result: 'na' };
      }
      // Per-validator drift on top of fleet — keeps slot numbers identical
      // while differentiating per-validator latency profiles deterministically.
      const drift = (seededWave(seed, i) - 0.5) * 60; // ±30ms
      const baseGap = row.advantage;
      const advGap = Math.round(baseGap + drift + (isOnboarding ? -28 : 0));
      const localMum = Math.max(0, Math.round(row.mumP2PArrival + (seededWave(seed, i + 17) - 0.5) * 40));
      const localLib = Math.max(localMum, localMum + advGap);
      return {
        slot: row.slot,
        mumP2PArrival: localMum,
        libP2PArrival: localLib,
        advantage: advGap,
        result: classifySlotResult(advGap),
      };
    });
    return { partner, slots };
  });

  return { fleetSlots, perValidator };
}

// calculateWindowMetrics — summarize one validator's slots over a window.
function calculateWindowMetrics(slots, windowSize = SLOTS_PER_EPOCH) {
  const rows = (slots || []).slice(-windowSize);
  const comparable = rows.filter(r => Number.isFinite(r.advantage));
  const advValues = comparable.map(r => r.advantage);
  const mumValues = comparable.map(r => r.mumP2PArrival).filter(Number.isFinite);
  const libValues = comparable.map(r => r.libP2PArrival).filter(Number.isFinite);
  const naSlots = rows.length - comparable.length;
  const accelerationRate = comparable.length
    ? +((comparable.filter(r => r.advantage > 0).length / comparable.length) * 100).toFixed(1)
    : null;
  const avgAdvantage = advValues.length
    ? Math.round(advValues.reduce((s, v) => s + v, 0) / advValues.length)
    : null;
  const latestSlot = rows.length ? rows[rows.length - 1].slot : null;
  return {
    windowSize: rows.length,
    sampleSize: comparable.length,
    naSlots,
    accelerationRate,
    avgAdvantage,
    avgMumP2PArrival: mumValues.length ? Math.round(mumValues.reduce((s, v) => s + v, 0) / mumValues.length) : null,
    avgLibP2PArrival: libValues.length ? Math.round(libValues.reduce((s, v) => s + v, 0) / libValues.length) : null,
    p80Advantage: calculatePercentile(advValues, 0.8),
    latestSlot,
  };
}

// calculateFleetAverage — fleet-wide mean of each validator's metrics.
function calculateFleetAverage(validatorRows) {
  const collect = (key) => validatorRows.map(v => v.metrics[key]).filter(Number.isFinite);
  const mean = (arr) => arr.length ? +(arr.reduce((s, v) => s + v, 0) / arr.length).toFixed(1) : null;
  return {
    accelerationRate: mean(collect('accelerationRate')),
    avgAdvantage: mean(collect('avgAdvantage')),
    p80Advantage: mean(collect('p80Advantage')),
    naSlots: mean(collect('naSlots')),
  };
}

// compareAgainstFleet — labelled delta vs fleet for one validator.

// getValidatorStatus(metrics) — simplified: Active for any validator with
// comparable data; Offline only when more than half the window is N/A;
// Onboarding for new partners with too little data; N/A when nothing comparable.
// Fleet-wide N/A (a network property) is intentionally ignored here.
function getValidatorStatus(metrics, partner, fleetNaFloor = 0) {
  if (partner && partner.status === 'onboarding' && (!metrics || metrics.sampleSize < 4)) return 'onboarding';
  if (!metrics || metrics.sampleSize === 0) return 'na';
  if (metrics.naSlots >= Math.floor(metrics.windowSize / 2)) return 'offline';
  return 'healthy';
}

// getSlotHeatColor(result, advantage) — returns a CSS var token name for cell
// background. Mint for faster (mumP2P win), amber for slower, dim for N/A.

// ────────────────────────────────────────────────────────────────
// Animated/flap helpers used by the fleet dashboard
// ────────────────────────────────────────────────────────────────

// FlipDigit — split-flap digit that only animates when its char changes.





























// ────────────────────────────────────────────────────────────────
// Fleet stats — the validator drill-in no longer shows per-validator
// detail charts. The side rail carries that validator's stake + status;
// this panel shows fleet-wide mumP2P-vs-libP2P performance instead.
// ────────────────────────────────────────────────────────────────
function fmtSignedMs(value) {
  const n = toMetricNumber(value);
  if (n === null) return { sign: '', num: '—' };
  return {
    sign: n > 0 ? '+' : n < 0 ? '−' : '',
    num: Math.abs(Math.round(n)).toLocaleString('en-US'),
  };
}

// ── Ported from the Optimum Hub design system (Geist + --op-* tokens, op-* animations) ──
const OP_REDUCED = (typeof window !== 'undefined' && window.matchMedia
  && window.matchMedia('(prefers-reduced-motion: reduce)').matches);

// Count-up number — mirrors the design's data-count reveal.
function OpCountUp({ value, decimals = 0, duration = 1000, fixed = false }) {
  const target = Number(value) || 0;
  const [disp, setDisp] = React.useState(OP_REDUCED ? target : 0);
  // Animate FROM whatever is on screen, not from zero. On mount that is 0, so the
  // count-up is unchanged; on a later change it tweens instead of snapping back to
  // zero and climbing again. That only started mattering when a value could change
  // after mount, which fetched fleet stake now does.
  const fromRef = React.useRef(0);
  React.useEffect(() => {
    if (OP_REDUCED) { setDisp(target); return; }
    const from = fromRef.current;
    let raf, start = null;
    const step = (t) => {
      if (start === null) start = t;
      const p = Math.min(1, (t - start) / duration);
      const eased = 1 - Math.pow(1 - p, 3);
      const v = from + (target - from) * eased;
      fromRef.current = v;
      setDisp(v);
      if (p < 1) raf = requestAnimationFrame(step);
    };
    raf = requestAnimationFrame(step);
    return () => cancelAnimationFrame(raf);
  }, [target, duration]);
  // `fixed` matches formatNum's K/M branches, which use bare toFixed; Intl rounds
  // the shortest repr instead and disagrees at .xx5 midpoints.
  return <>{fixed
    ? disp.toFixed(decimals)
    : disp.toLocaleString('en-US', { minimumFractionDigits: decimals, maximumFractionDigits: decimals })}</>;
}

// Pool every connected validator's slots (each carries advantage + arrivals).
function poolValidatorSlots(validatorRows) {
  const out = [];
  (validatorRows || []).forEach(r => (r.slots || []).forEach(s => {
    if (s && Number.isFinite(s.advantage)) out.push(s);
  }));
  return out;
}

// Two-way mumP2P-vs-libP2P win split — always sums to 100%.
function fleetWinSplit(pooled) {
  let faster = 0, slower = 0;
  pooled.forEach(s => { if (s.advantage > 0) faster++; else if (s.advantage < 0) slower++; });
  const two = faster + slower;
  const mumPct = two > 0 ? +((faster / two) * 100).toFixed(1) : 0;
  return { faster, slower, mumPct, libPct: two > 0 ? +(100 - mumPct).toFixed(1) : 0, total: pooled.length };
}

// Pulse loader — the design's "4B" motion: a row of chrome blobs pops one at a
// time under a fading caption. The .op-pulse CSS lives in index.html so it's
// shared with the pre-React boot screen; the blobs read on both light + dark.
function OpPulseLoader({ caption = 'Loading', className, style }) {
  const v = '20260710load1';
  return (
    <div className={'op-pulse' + (className ? ' ' + className : '')} style={style}>
      <div className="op-pulse-row">
        <img src={`dashboard/assets/stream-1.png?v=${v}`} alt="" />
        <img src={`dashboard/assets/stream-2.png?v=${v}`} alt="" />
        <img src={`dashboard/assets/stream-3.png?v=${v}`} alt="" />
        <img src={`dashboard/assets/stream-4.png?v=${v}`} alt="" />
      </div>
      {caption ? <div className="op-pulse-cap">{caption}</div> : null}
    </div>
  );
}

// ── Top network-status card: headline stats + stake concentration + win split ──
// ── Measured performance report (services/validators.js → getPropagation) ──
// Three views of the same run: the headline strip, the two metric cards, and the
// percentile ladder. All read one object so no figure can drift from another.
function usePropagation() {
  return (typeof window !== 'undefined' && window.OptimumValidators && window.OptimumValidators.getPropagation)
    ? window.OptimumValidators.getPropagation() : null;
}

// A one-decimal figure that keeps trailing zeroes (4.1×, not 4×).
function perfFixed(v, d) {
  return Number(v).toLocaleString('en-US', { minimumFractionDigits: d, maximumFractionDigits: d });
}

// The report's four headline results, as a dark banner. It opens the Performance
// section: the claim first, then the basis it was measured on, then the cards
// that break it down.
function PerfHeroBar({ perf }) {
  // Below 520px the four headlines collapse onto one row with a shared caption,
  // so the cells become real buttons. Above it they are static, and buttons would
  // only add four stops to the tab order for a caption nobody can see.
  const [compact, setCompact] = React.useState(false);
  const [sel, setSel] = React.useState(0);
  React.useEffect(() => {
    if (typeof window === 'undefined' || !window.matchMedia) return undefined;
    const mq = window.matchMedia('(max-width: 520px)');
    const sync = () => setCompact(mq.matches);
    sync();
    mq.addEventListener('change', sync);
    return () => mq.removeEventListener('change', sync);
  }, []);
  if (!perf || !perf.headline) return null;
  const h = perf.headline;
  // The over-4s cell is dropped rather than rendered empty: the endpoint omits the
  // block when the mumP2P arm had no slots over 4s, which is the best week we
  // could have and must not read as "0 slots vs 0 on gossipsub".
  const cells = [
    { v: perfFixed(h.spreadFactor, 1), u: '×', k: 'Tighter propagation spread', n: 'vs gossipsub, mean per slot' },
    { v: h.leadMs.toLocaleString('en-US'), u: 'ms', k: 'Earlier p95 arrival', n: h.leadMs + ' ms mean lead ± ' + h.leadCiMs + ' ms' },
    { v: perfFixed(h.slotsAcceleratedPct, 1), u: '%', k: 'Slots accelerated', n: 'mump2p first vs Xatu reference' },
    Number.isFinite(h.over4sFactor)
      ? { v: perfFixed(h.over4sFactor, 1), u: '×', k: 'Fewer slots over 4s', n: h.over4sMumSlots + ' slots vs ' + h.over4sLibSlots + ' on gossipsub' }
      : null,
  ].filter(Boolean);
  // One clamped index for the class, the caption and aria-pressed alike. Clamped
  // at read time rather than reset in an effect: the cell count shrinks when a
  // live report omits over_4s, and an index left past the end would throw in the
  // caption, during render, and blank the whole hub.
  const selIdx = Math.min(sel, cells.length - 1);
  return (
    <section className="hub-perf-hero op-reveal" aria-label="mump2p performance headlines">
      <div className="hub-perf-hero-meta">
        <span className="hub-perf-hero-eyebrow">mump2p performance report · Xatu reference</span>
        <span className="hub-perf-hero-win">{perf.network} · {perf.windowLabel} · {perf.slotsMeasured.toLocaleString('en-US')} slots</span>
      </div>
      {/* The cell count is data, not layout: the over-4s cell drops out when the
          report omits it, and a fixed four columns would leave a blank track. */}
      <div className="hub-perf-hero-grid" style={{ '--hero-cells': cells.length }}>
        {cells.map((c, i) => {
          const Tag = compact ? 'button' : 'div';
          return (
            <Tag
              key={c.k}
              className={'hub-perf-hero-cell' + (i === selIdx ? ' is-sel' : '')}
              {...(compact ? { type: 'button', onClick: () => setSel(i), 'aria-pressed': i === selIdx } : {})}
            >
              <div className="hub-perf-hero-v"><span className="op-num">{c.v}</span><em>{c.u}</em></div>
              <span className="hub-perf-hero-rule" aria-hidden="true" />
              <div className="hub-perf-hero-k">{c.k}</div>
              <div className="hub-perf-hero-n">{c.n}</div>
            </Tag>
          );
        })}
      </div>
      <div className="hub-perf-hero-caption" aria-live="polite">
        {/* Clamped at read time, not reset in an effect: the cell count shrinks
            when a live report omits over_4s, and a `sel` left pointing past the
            end would throw here, during render, and blank the whole hub. */}
        <span className="k">{cells[selIdx].k}</span>
        <span className="n">{cells[selIdx].n}</span>
      </div>
    </section>
  );
}

// One metric from the report: the paired mumP2P/libP2P bars plus the three
// supporting figures underneath. Shared by propagation spread and slot arrival.
function PerfMetricCard({ title, definition, mumMs, libMs, stats, help }) {
  const max = Math.max(mumMs || 0, libMs || 0, 1);
  const w = (v) => Math.max(4, (v / max) * 100).toFixed(1) + '%';
  return (
    <div className="hub-perf-card op-reveal">
      <div className="hub-perf-title">{title}{help ? <OpHelpTip text={help} /> : null}</div>
      <div className="hub-perf-def">{definition}</div>
      <div className="hub-perf-bars">
        <div className="hub-perf-bar">
          <div className="hub-perf-bar-top">
            <span className="k">mump2p</span>
            <span className="v">{perfFixed(mumMs, 1)}<em> ms</em></span>
          </div>
          <span className="track"><i className="mum" style={{ width: w(mumMs) }} /></span>
        </div>
        <div className="hub-perf-bar">
          <div className="hub-perf-bar-top">
            <span className="k lib">libp2p gossipsub</span>
            <span className="v">{perfFixed(libMs, 1)}<em> ms</em></span>
          </div>
          <span className="track"><i className="lib" style={{ width: w(libMs) }} /></span>
        </div>
      </div>
      {stats && stats.length ? (
        <div className="hub-perf-stats">
          {stats.map(s => (
            <div key={s.k}>
              <span className="v">{s.v}{s.u ? <em>{s.u}</em> : null}</span>
              <span className="k">{s.k}{s.help ? <OpHelpTip text={s.help} placement="start" /> : null}</span>
            </div>
          ))}
        </div>
      ) : null}
    </div>
  );
}

// The percentile ladder — the lead widens toward the tail, which is the whole
// point, so the bars are scaled to the largest (p95) rather than to a round max.
function NetworkAdvantage({ perf }) {
  const rows = (perf && perf.percentileLead) || [];
  if (!rows.length) return null;
  const max = Math.max(...rows.map(r => r.ms), 1);
  // Read off the rows rather than assumed, so neither clause can outlive what it
  // describes. The shared-count line is also gated on the ladder being live: a
  // refreshed static ladder could legitimately share one count, and the two
  // sentences side by side would read as a contradiction.
  const shared = perf.percentileLeadLive
    && rows.length > 1 && rows.every(r => r.slots === rows[0].slots);
  const tip = 'How much earlier mump2p delivers than gossipsub, grouped by how slow the receiving node is. p50 is a typical node; p95 is the slow tail. Each bar is the average per-slot lead in ms, with the number of slots it was measured over.'
    + (perf.percentileLeadLive ? '' : ' These come from an earlier report run, so the slot counts differ from the cards above.')
    + (shared ? ' Every row is measured over the same slots, so they share one count.' : '');
  return (
    <section className="hub-netadv op-reveal" aria-label="Network advantage by arrival percentile">
      <div className="hub-netadv-lede">
        <div className="hub-netadv-eyebrow">Network advantage<OpHelpTip text={tip} /></div>
        <h3>The lead grows where it matters — at the tail.</h3>
        <p>Mean per-slot lead of mump2p over gossipsub, by arrival percentile.</p>
      </div>
      <div className="hub-netadv-rows">
        {rows.map(r => (
          <div className="hub-netadv-row" key={r.p}>
            <span className="p">{r.p}</span>
            <span className="track"><i style={{ width: (r.ms / max * 100).toFixed(1) + '%' }} /></span>
            <span className="v">{perfFixed(r.ms, 1)} ms<em> · {r.slots.toLocaleString('en-US')} slots</em></span>
          </div>
        ))}
      </div>
    </section>
  );
}

// "Trusted by leading validators and staking protocols" — an infinite
// right-to-left logo marquee (getoptimum.xyz style). Built from the real
// operator fleet + their curated brand logos; the set is duplicated so the
// loop is seamless, and edges fade via a CSS mask. Pauses on hover;
// respects reduced-motion.
function TrustedMarquee() {
  const ops = (typeof window !== 'undefined' && window.OptimumValidators && window.OptimumValidators.getReal) ? window.OptimumValidators.getReal() : [];
  const items = ops.filter(o => CURATED_LOGOS[o.id]);
  if (!items.length) return null;
  const loop = items.concat(items); // 2× for a seamless -50% translate
  return (
    <div className="op-marquee">
      <div className="op-marquee-viewport">
        <div className="op-marquee-track">
          {loop.map((o, i) => {
            const dupe = i >= items.length;   // the second pass only exists to make the loop seamless
            return (
              <a
                className="op-marquee-item"
                key={i}
                href={partnerSite(o)}
                target="_blank"
                rel="noopener noreferrer"
                title={o.name + ' ↗'}
                aria-hidden={dupe ? 'true' : undefined}
                tabIndex={dupe ? -1 : undefined}
              >
                <span className="hub-fleet-tbl-avatar op-marquee-avatar"><PartnerLogo partner={o} fallbackClassName="hub-fleet-heat-fb" /></span>
                <span className="op-marquee-name">{o.name}</span>
              </a>
            );
          })}
        </div>
      </div>
    </div>
  );
}

// The lede's second line cycles through why mumP2P is faster. Phrases are
// mechanism, not figures — the stats bar below already carries the numbers, and
// a rotating claim that restated them would just contradict itself on pause.
// setInterval rather than rAF: rAF is throttled to a stop in background tabs,
// which would strand whichever phrase happened to be showing.
const MUMP2P_REASONS = [
  { full: 'sends coded packets, not duplicate data.', mobile: 'codes packets, not copies.' },
  { full: 'rebuilds blocks from whatever arrives.', mobile: 'rebuilds from any packet.' },
  { full: 'recovers without waiting for missing packets.', mobile: 'recovers without waiting.' },
  { full: 'makes redundancy useful, not wasteful.', mobile: 'turns redundancy into speed.' },
];

function RotatingReason({ phrases, intervalMs = 3400 }) {
  const [i, setI] = React.useState(0);
  const reduced = React.useMemo(() => (
    typeof window !== 'undefined' && window.matchMedia
      ? window.matchMedia('(prefers-reduced-motion: reduce)').matches : false
  ), []);
  // Runs for the life of the card. There is deliberately no hover pause: if the
  // pointer left the window while over the line, onMouseLeave could fail to fire
  // and strand one phrase on screen. With constant deps the interval is created
  // once and never torn down by a re-render either.
  React.useEffect(() => {
    if (reduced || phrases.length < 2) return undefined;
    const id = setInterval(() => setI(n => (n + 1) % phrases.length), intervalMs);
    return () => clearInterval(id);
  }, [reduced, phrases.length, intervalMs]);
  return (
    <span className="hub-rot">
      {phrases.map((p, n) => (
        <span key={p.full} className={'hub-rot-item' + (n === i ? ' on' : '')} aria-hidden={n !== i ? 'true' : undefined}>
          <span className="hub-rot-ghost">mump2p</span>{' '}
          <span className="hub-rot-copy-full">{p.full}</span>
          <span className="hub-rot-copy-mobile">{p.mobile}</span>
        </span>
      ))}
      {/* The name never changes, so it is painted once and never fades. The
          ghost above reserves its space inside each phrase so the sentence
          still wraps as a single sentence. */}
      <span className="hub-rot-name" aria-hidden="true">mump2p</span>
    </span>
  );
}

// Network-status card beside the globe — "3A anchored card": a live-status
// header (Optimum branding), two headline stats with sub-captions, and a
// footer marquee of trusted validator partners. The stats are the mumP2P-
// connected stake and the headline propagation-speed advantage (the selling
// point); the per-slot propagation breakdown lives in FleetAveragesPanel below.
function FleetStatsCard({ totalStakeEth, validators, snapshotMs, dark = false, validator = null }) {
  // mumP2P-connected stake comes from the single source of truth, which fetches
  // the console's published daily series and falls back to a static snapshot.
  const VALSVC = (typeof window !== 'undefined' && window.OptimumValidators) || null;
  const mumFleetEth = VALSVC && VALSVC.getMumP2PFleetEth ? VALSVC.getMumP2PFleetEth() : 1316423;
  // Block-propagation advantage, from the measured Xatu-referenced report in the
  // SSOT: mean per-slot spread (p95 arrival − first arrival) for mumP2P vs the
  // libP2P gossipsub baseline. Percentile breakdowns live on the Stats page.
  const perf = (typeof window !== 'undefined' && window.OptimumValidators && window.OptimumValidators.getPropagation)
    ? window.OptimumValidators.getPropagation() : null;
  const spread = perf && perf.spread;
  const speedMult = spread ? spread.speedFactor : 3.0;
  // libP2P sets the scale; mumP2P's mark sits at its share of it.
  const spreadBarPct = spread ? Math.max(4, +(spread.mumP2PMs / spread.libP2PMs * 100).toFixed(1)) : 4;
  // The help tip carries the measurement basis — what the figure means and how
  // many slots / observers it was aggregated over — so the headline multiple is
  // never a bare number. Built from `perf` so the prose can't drift from it.
  const propHelp = perf
    ? 'How long a block takes to reach nearly every node once it first appears. '
      + 'Per slot: the time from the first node seeing the block to the 95th-percentile node seeing it, averaged over '
      + spread.slotsCompared.toLocaleString('en-US') + ' ' + perf.network.toLowerCase() + ' slots. '
      + 'mump2p ' + perfFixed(spread.mumP2PMs, 1) + ' ms vs libp2p gossipsub ' + perfFixed(spread.libP2PMs, 1) + ' ms.'
    : 'How long a block takes to reach nearly every node, mump2p vs the libp2p gossipsub baseline.';
  // Per-operator figures when a specific validator is focused (from the globe search).
  const opStake = validator && Number.isFinite(validator.liveStakeEth) ? validator.liveStakeEth : null;
  const opMump2p = (opStake !== null && totalStakeEth) ? (opStake / totalStakeEth) * mumFleetEth : null;
  // ── Design import: "7C — dashboard density" (Network Status Card · 4 dirs).
  // A lede, then two figures (Total Value Boosted · Propagation spread) are
  // inlined in the render below. We keep the real Optimum logo + trusted-by
  // marquee, and drop 7C's "recent slots" strip per the brief.

  // Daily connected-stake series, oldest first, straight from the console. One
  // point per UTC day: the step-ups are real partner onboardings, so the line is
  // deliberately not smoothed.
  const spkPoints = (VALSVC && VALSVC.getMumP2PStakeSeries ? VALSVC.getMumP2PStakeSeries() : [])
    .filter(p => p && Number.isFinite(p.eth));
  const _spkRaw = spkPoints.length ? spkPoints.map(p => p.eth) : [mumFleetEth];
  // The polyline divides by (n - 1), so a single point renders NaN -- and a single
  // point is the static-fallback path, i.e. exactly when the endpoint is down.
  // Duplicate it into a flat two-point line.
  const spkVals = _spkRaw.length > 1 ? _spkRaw : [_spkRaw[0], _spkRaw[0]];
  // First / middle / last, deduped: 83 ticks would be unreadable in a 320px box,
  // and at two points the midpoint IS the first.
  const spkAxis = spkPoints.length > 1
    ? [...new Set([0, Math.floor((spkPoints.length - 1) / 2), spkPoints.length - 1])]
        .map(i => fmtSpkDay(spkPoints[i].day))
    : [];
  // Below two points there is nothing to compare, and index 0 is the only position
  // any index-derived cursor could take, so hover is disabled rather than pinned.
  // Declared above spkTipText, which reads it eagerly.
  const spkHoverable = spkPoints.length > 1;
  // Matches formatNum's thresholds, so the headline and the hover tooltip never
  // report the same figure in different units.
  const spkUnit = mumFleetEth >= 1e6
    ? { div: 1e6, dp: 2, suffix: 'M' }
    : mumFleetEth >= 1e3
      ? { div: 1e3, dp: 1, suffix: 'K' }
      : { div: 1, dp: 0, suffix: '' };
  const spkLast = spkPoints.length ? spkPoints[spkPoints.length - 1] : null;
  // Says "measured" only when the last point actually was: dating a carried-forward
  // figure as that day's measurement is what `carried` exists to prevent.
  const spkTipText = 'Effective stake run by operators connected to mump2p on Ethereum mainnet'
    + (spkLast ? (spkLast.carried ? ', carried forward to ' : ', measured ') + fmtSpkDay(spkLast.day) : '')
    + (spkHoverable ? '. A day marked · on hover was carried forward rather than measured.' : '.');
  const _spkMin = Math.min(...spkVals), _spkMax = Math.max(...spkVals);
  const _spkRange = (_spkMax - _spkMin) || 1;
  const _spkPts = spkVals.map(v => 20 + ((v - _spkMin) / _spkRange) * 80);
  const _spkN = _spkPts.length, _spkW = 320, _spkH = 52;
  const spkLine = _spkPts.map((v, i) => (i / (_spkN - 1) * _spkW).toFixed(1) + ',' + (_spkH - (v / 100) * (_spkH - 8) - 4).toFixed(1)).join(' ');
  const spkArea = 'M0,' + _spkH + ' L' + spkLine.split(' ').join(' L') + ' L' + _spkW + ',' + _spkH + ' Z';
  const [spkHover, setSpkHover] = React.useState(null);
  const onSpkMove = (e) => {
    if (!spkHoverable) return;
    const r = e.currentTarget.getBoundingClientRect();
    if (!r.width) return;
    const x = (e.clientX - r.left) / r.width;
    setSpkHover(Math.min(spkPoints.length - 1, Math.max(0, Math.round(x * (spkPoints.length - 1)))));
  };
  // Re-clamped at render, not just in the handler: a revalidation can shrink the
  // series with no mouse move, leaving the stored index past the end.
  const spkIdx = spkHoverable && spkHover != null ? Math.min(spkHover, spkPoints.length - 1) : null;
  // Dropped when hover turns off, so a shrink to one point cannot leave an index
  // that reappears on regrow. A shrink to 2+ keeps it, masked by the clamp above.
  React.useEffect(() => { if (!spkHoverable && spkHover != null) setSpkHover(null); }, [spkHoverable, spkHover]);
  const spkHoverPct = spkIdx != null ? (spkIdx / (spkPoints.length - 1)) * 100 : 0;
  const spkTipTx = spkHoverPct < 22 ? 'translateX(0)' : spkHoverPct > 78 ? 'translateX(-100%)' : 'translateX(-50%)';
  return (
    <div className="op-reveal hub-netcard" style={{ position: 'relative', width: '100%', minWidth: 0, gridColumn: '1 / -1' }}>
      {validator ? (
        <div style={{ display: 'flex', alignItems: 'center', gap: 14, marginTop: 20 }}>
          <span className="hub-fleet-tbl-avatar" style={{ width: 44, height: 44, borderRadius: 12, flex: 'none' }}>
            <PartnerLogo partner={validator} fallbackClassName="hub-fleet-heat-fb" />
          </span>
          <div style={{ minWidth: 0 }}>
            <div style={{ fontSize: 18, fontWeight: 600, letterSpacing: '.01em', textTransform: 'uppercase', color: 'var(--op-text)', lineHeight: 1.15 }}>{validator.name}</div>
            <div className="hub-status-eyebrow" style={{ marginTop: 4, fontSize: 10.5 }}>ETH validator · operator</div>
          </div>
        </div>
      ) : null}

      {/* The claim the rest of the page then evidences. The reason rotates; the
          stem does not, so only the second line ever changes. */}
      <h2 className="hub-status-lede">
        <RotatingReason phrases={MUMP2P_REASONS} />
      </h2>

      <div className="hub-status-stats">
        {/* Total Value Boosted — live daily series from the console. */}
        <div style={{ minWidth: 0 }}>
          <div className="hub-status-stat-k">Total Value Boosted (TVB)<OpHelpTip text={spkTipText} /></div>
          <div className="hub-status-stat-v">
            <OpCountUp value={mumFleetEth / spkUnit.div} decimals={spkUnit.dp} fixed={spkUnit.suffix !== ''} />{spkUnit.suffix}
            <span className="u"> ETH</span>
          </div>
          <div className="hub-status-spark">
            {/* The cursor spans this box only, so it stops at the plot and does
                not rule through the date labels below. */}
            <div className="hub-status-spark-plot" onMouseMove={onSpkMove} onMouseLeave={() => setSpkHover(null)}>
            <svg viewBox="0 0 320 52" preserveAspectRatio="none" aria-hidden="true">
              <defs>
                <linearGradient id="opSpkFill" x1="0" y1="0" x2="0" y2="1">
                  <stop offset="0" stopColor="rgba(100,226,127,0.26)" />
                  <stop offset="1" stopColor="rgba(100,226,127,0)" />
                </linearGradient>
              </defs>
              <path d={spkArea} fill="url(#opSpkFill)" />
              <polyline points={spkLine} fill="none" stroke="var(--op-green)" strokeWidth="2.2"
                strokeLinecap="round" strokeLinejoin="round" vectorEffect="non-scaling-stroke" />
            </svg>
            {spkIdx != null && (
              <React.Fragment>
                <span className="op-spk-cursor" style={{ left: spkHoverPct.toFixed(2) + '%' }} />
                <span className="op-spk-tip" style={{ left: Math.min(98, Math.max(2, spkHoverPct)).toFixed(2) + '%', transform: spkTipTx }}>
                  <span className="op-spk-tip-m">{fmtSpkDay(spkPoints[spkIdx].day)}{spkPoints[spkIdx].carried ? ' ·' : ''}</span>
                  <span className="op-spk-tip-v">{fmtSpkEth(spkVals[spkIdx])}<em>ETH</em></span>
                </span>
              </React.Fragment>
            )}
            </div>
            <div className="hub-status-spark-axis">
              {spkAxis.map((d, i) => <span key={d + i}>{d}</span>)}
            </div>
          </div>
        </div>
        {/* The label names the metric, not the outcome, so "faster" cannot be
            read as "blocks arrive sooner" — that is slot arrival, a much
            smaller multiple. */}
        <div style={{ minWidth: 0 }}>
          <div className="hub-status-stat-k">Propagation spread<OpHelpTip text={propHelp} /></div>
          <div className="hub-status-stat-v">
            <span className="hi">≈ <OpCountUp value={speedMult} decimals={1} />×</span><span className="u"> faster</span>
          </div>
          {spread ? (
            <div className="hub-budget">
              <div className="hub-budget-bar">
                <div className="fill" style={{ width: spreadBarPct + '%' }}>
                  <span>{perfFixed(spread.mumP2PMs, 1)} ms</span>
                </div>
                <span className="tick" />
                {/* Not "arrived later": that is slot arrival, measured from slot
                    start, and a different number (436 ms). */}
                <div className="rest">
                  <span className="v">{perfFixed(spread.libP2PMs - spread.mumP2PMs, 1)} ms</span>
                  <span className="u">longer to spread</span>
                </div>
              </div>
              <div className="hub-budget-legend">
                <span className="mum">mump2p</span>
                <span className="lib">libp2p gossipsub</span>
              </div>
            </div>
          ) : null}
        </div>
      </div>

      <div className="hub-status-partners">
        <div className="hub-status-stat-k">Trusted by leading validators and staking protocols</div>
        <TrustedMarquee />
      </div>
    </div>
  );
}

// ── Overview rail: P80 lead · propagation spread · propagation latency ──
// Reusable graph cards — rendered on both the Overview rail and the Stats page.
// Small "View details ↗" link (Overview cards → Stats page); renders only when onClick is given.
function ViewDetailsLink({ onClick }) {
  if (!onClick) return null;
  return (
    <div style={{ marginTop: 14, textAlign: 'right' }}>
      <a href="#stats" className="op-view-details" onClick={(e) => { e.preventDefault(); onClick(); }}>View details ↗</a>
    </div>
  );
}

// (A) Average arrival — mumP2P vs libP2P average arrival, with the gap called out.
function ArrivalCompareCard({ mumMs, libMs, subtitle, title, onViewDetails, wrapClass, wrapStyle }) {
  const has = Number.isFinite(mumMs) && Number.isFinite(libMs);
  return (
    <div className={wrapClass} style={wrapStyle}>
      <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 12, marginBottom: 18 }}>
        <span style={{ fontSize: 15, fontWeight: 600 }}>{title || 'Average arrival at gateway'}</span>
        <span className="op-mono" style={{ fontSize: 9.5, letterSpacing: '.1em', textTransform: 'uppercase', color: 'var(--op-muted)', textAlign: 'right' }}>{subtitle || 'mump2p vs libp2p · lower is faster'}</span>
      </div>
      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
        <div style={{ borderRadius: 12, border: '1px solid rgba(100,226,127,0.28)', background: 'linear-gradient(180deg,rgba(100,226,127,0.10),transparent)', padding: '15px 16px' }}>
          <div className="op-mono op-stat-k" style={{ fontSize: 9.5, letterSpacing: '.08em', textTransform: 'uppercase', color: 'var(--op-muted)' }}>mump2p arrival</div>
          <div style={{ display: 'flex', alignItems: 'baseline', gap: 5, marginTop: 10 }}>
            <span className="op-num" style={{ fontSize: 32, fontWeight: 300, letterSpacing: '-0.02em', lineHeight: 0.9, color: 'var(--op-green)' }}>{has ? <OpCountUp value={Math.round(mumMs)} /> : 'N/A'}</span>
            <span className="op-mono" style={{ fontSize: 12, color: 'var(--op-green)' }}>{has ? 'ms' : ''}</span>
          </div>
        </div>
        <div style={{ borderRadius: 12, border: '1px solid rgba(254,161,88,0.28)', background: 'linear-gradient(180deg,rgba(254,161,88,0.09),transparent)', padding: '15px 16px' }}>
          <div className="op-mono op-stat-k" style={{ fontSize: 9.5, letterSpacing: '.08em', textTransform: 'uppercase', color: 'var(--op-muted)' }}>libp2p arrival</div>
          <div style={{ display: 'flex', alignItems: 'baseline', gap: 5, marginTop: 10 }}>
            <span className="op-num" style={{ fontSize: 32, fontWeight: 300, letterSpacing: '-0.02em', lineHeight: 0.9, color: 'var(--op-peach)' }}>{has ? <OpCountUp value={Math.round(libMs)} /> : 'N/A'}</span>
            <span className="op-mono" style={{ fontSize: 12, color: 'var(--op-peach)' }}>{has ? 'ms' : ''}</span>
          </div>
        </div>
      </div>
      {!has && <div className="op-mono" style={{ fontSize: 11, letterSpacing: '.03em', textAlign: 'center', color: 'var(--op-muted)' }}>No paired samples for this window</div>}
      <ViewDetailsLink onClick={onViewDetails} />
    </div>
  );
}

// mumP2P vs libP2P — the headline result: how often mumP2P delivered first and by
// how much (P80). Reused on the Stats page and the Overview (where it replaces the
// raw "Average arrival" card as the clearer showcase of mumP2P's advantage).
function MumVsLibCard({ winPct, leadMs, windowLabel, pctlLabel = 'P80', title = 'mump2p vs libp2p', wrapClass, wrapStyle }) {
  const hasWin = Number.isFinite(winPct);
  const hasLead = Number.isFinite(leadMs);
  const box = { borderRadius: 12, border: '1px solid rgba(100,226,127,0.28)', background: 'linear-gradient(180deg,rgba(100,226,127,0.10),transparent)', padding: '15px 16px', display: 'flex', flexDirection: 'column', justifyContent: 'center' };
  const label = windowLabel || 'last 25 slots';
  return (
    <div className={wrapClass} style={wrapStyle}>
      <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 12, marginBottom: 18 }}>
        <span style={{ fontSize: 15, fontWeight: 600 }}>{title}<OpHelpTip text={'Left: the share of slots in the window where mump2p delivered the block to the gateway before libp2p gossipsub. Right: how much earlier it arrived, comparing the ' + (pctlLabel && pctlLabel !== 'Avg' ? pctlLabel : 'average') + ' arrival times of the two over the window.'} /></span>
        <span className="op-mono" style={{ fontSize: 9.5, letterSpacing: '.1em', textTransform: 'uppercase', color: 'var(--op-muted)' }}>{label}{pctlLabel ? ' · ' + pctlLabel : ''}</span>
      </div>
      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
        <div style={box}>
          <div className="op-mono op-stat-k" style={{ fontSize: 9.5, letterSpacing: '.08em', textTransform: 'uppercase', color: 'var(--op-muted)' }}>mump2p arrived first</div>
          <div style={{ display: 'flex', alignItems: 'baseline', gap: 4, marginTop: 10 }}>
            <span className="op-num" style={{ fontSize: 32, fontWeight: 300, letterSpacing: '-0.02em', lineHeight: 0.9, color: 'var(--op-green)' }}>{hasWin ? <OpCountUp value={winPct} decimals={0} /> : 'N/A'}</span>
            <span className="op-mono" style={{ fontSize: 14, color: 'var(--op-green)' }}>{hasWin ? '%' : ''}</span>
          </div>
        </div>
        <div style={box}>
          <div className="op-mono op-stat-k" style={{ fontSize: 9.5, letterSpacing: '.08em', textTransform: 'uppercase', color: 'var(--op-muted)' }}>Arrival advantage</div>
          <div style={{ display: 'flex', alignItems: 'baseline', gap: 4, marginTop: 10 }}>
            <span className="op-num" style={{ fontSize: 32, fontWeight: 300, letterSpacing: '-0.02em', lineHeight: 0.9, color: 'var(--op-green)' }}>{hasLead ? <OpCountUp value={leadMs} decimals={0} /> : 'N/A'}</span>
            <span className="op-mono" style={{ fontSize: 14, color: 'var(--op-green)' }}>{hasLead ? 'ms earlier' : ''}</span>
          </div>
        </div>
      </div>
    </div>
  );
}

// Mini paired bars for the per-slot hover tooltip — mumP2P vs libP2P arrival,
// scaled to the slower of the two so the gap (mumP2P's lead) reads at a glance.
function TipArrivalBars({ mum, lib }) {
  if (!Number.isFinite(mum) || !Number.isFinite(lib)) return null;
  const max = Math.max(mum, lib, 1);
  const w = (v) => Math.max(3, (v / max) * 100).toFixed(1) + '%';
  // Summary reads as a comparison ("≈ N× faster"), matching the headline
  // "average latency" card — no "+" (which reads as "extra"), and an approx sign.
  let sum = null;
  if (mum > 0 && lib > 0 && Math.abs(lib - mum) >= 1) {
    sum = lib > mum
      ? { who: 'mump2p', mult: lib / mum, green: true }
      : { who: 'libp2p', mult: mum / lib, green: false };
  }
  return (
    <div className="op-tipbars">
      <div className="op-tipbar-row">
        <span className="op-tipbar-k">mump2p</span>
        <span className="op-tipbar-track"><i className="mum" style={{ width: w(mum) }} /></span>
        <span className="op-tipbar-v">{Math.round(mum).toLocaleString('en-US')}ms</span>
      </div>
      <div className="op-tipbar-row">
        <span className="op-tipbar-k">libp2p</span>
        <span className="op-tipbar-track"><i className="lib" style={{ width: w(lib) }} /></span>
        <span className="op-tipbar-v">{Math.round(lib).toLocaleString('en-US')}ms</span>
      </div>
      {sum
        ? <div className="op-tipbar-sum">{sum.who} <b style={{ fontWeight: 600, color: sum.green ? 'var(--sc-accent, var(--op-green))' : 'var(--op-peach)' }}>≈ {sum.mult.toFixed(1)}× faster</b></div>
        : <div className="op-tipbar-sum" style={{ color: 'var(--sc-mute, var(--op-muted))' }}>even</div>}
      <div className="op-tipbar-note">Sample data</div>
    </div>
  );
}

// Slot counts for one bucket. Not TipArrivalBars, which labels its values "ms"
// and summarises them as a speed multiple.
function DistBucketBars({ mum, lib, peak }) {
  const w = (v) => Math.max(3, (v / Math.max(1, peak)) * 100).toFixed(1) + '%';
  const row = (k, v, cls) => (
    <div className="op-tipbar-row">
      <span className="op-tipbar-k">{k}</span>
      <span className="op-tipbar-track"><i className={cls} style={{ width: w(v) }} /></span>
      <span className="op-tipbar-v">{Number(v).toLocaleString('en-US')}</span>
    </div>
  );
  return (
    <div className="op-tipbars">
      {row('mump2p', mum, 'mum')}
      {row('libp2p', lib, 'lib')}
      <div className="op-tipbar-sum" style={{ color: 'var(--sc-mute, var(--op-muted))' }}>slots in this bucket</div>
    </div>
  );
}

// (B) Fleet arrival distribution: the real replacement for the per-slot fixture.
//
// Two paired 10ms histograms over the same ~50k slots: how often the block had
// reached the fleet by a given point in the slot, mumP2P against the libP2P
// baseline. It carries the same claim the per-slot bars were making (mumP2P
// arrives earlier and in a tighter band) with measured data instead of a fixture.
//
// Not a timeline: there is no first-party source pairing mumP2P and libP2P per
// slot, so a per-slot comparison cannot be drawn honestly at all. This is the
// distribution of the same measurements the hero figures come from.
function ArrivalDistributionCard({ dist, windowLabel, onViewDetails, wrapClass, wrapStyle }) {
  const [hoverI, setHoverI] = React.useState(null);
  const W = 320, H = 96;
  // Memoised on the series identity: hover sets state on every pointer move, and
  // without this each move rebuilt two arrays and two path strings of up to ~2400
  // segments apiece.
  const { lo, span, mum, lib, peak, mumPath, libPath } = React.useMemo(() => {
    // Both arms on one axis so the shift between them is the visual, which means
    // one shared bucket range rather than each series scaled to its own.
    const lo = Math.min(dist.mumP2P[0].bucket, dist.libP2P[0].bucket);
    const hi = Math.max(
      dist.mumP2P[dist.mumP2P.length - 1].bucket,
      dist.libP2P[dist.libP2P.length - 1].bucket
    );
    const span = Math.max(1, hi - lo + 1);
    const series = (pts) => {
      const row = new Array(span).fill(0);
      pts.forEach(pt => { row[pt.bucket - lo] = pt.count; });
      return row;
    };
    const mum = series(dist.mumP2P), lib = series(dist.libP2P);
    // A loop, not Math.max(...mum, ...lib): the span is endpoint-derived, and a
    // spread throws RangeError once it is large.
    let peak = 1;
    for (let i = 0; i < span; i++) { if (mum[i] > peak) peak = mum[i]; if (lib[i] > peak) peak = lib[i]; }
    // Step path: a bucket is a bucket, and smoothing it would draw arrivals
    // between two buckets that nothing measured.
    const path = (row) => {
      let d = 'M0,' + H;
      for (let i = 0; i < row.length; i++) {
        const x = (i / row.length) * W, x2 = ((i + 1) / row.length) * W;
        const y = H - (row[i] / peak) * (H - 4);
        d += ' L' + x.toFixed(1) + ',' + y.toFixed(1) + ' L' + x2.toFixed(1) + ',' + y.toFixed(1);
      }
      return d + ' L' + W + ',' + H + ' Z';
    };
    return { lo, span, mum, lib, peak, mumPath: path(mum), libPath: path(lib) };
  }, [dist]);
  // A revalidation can land with a shorter span while the pointer is parked past
  // its end. `at` is clamped below, and this clears the stored index after.
  React.useEffect(() => { setHoverI(null); }, [span, dist.bucketMs]);

  const msAt = (i) => Math.round((lo + i) * dist.bucketMs);
  const onMove = (e) => {
    const r = e.currentTarget.getBoundingClientRect();
    if (!r.width) return;
    setHoverI(Math.min(span - 1, Math.max(0, Math.floor(((e.clientX - r.left) / r.width) * span))));
  };
  const at = hoverI != null ? Math.min(hoverI, span - 1) : null;
  const tipPct = at != null ? ((at + 0.5) / span) * 100 : 0;
  const tipTx = tipPct < 22 ? 'translateX(0)' : tipPct > 78 ? 'translateX(-100%)' : 'translateX(-50%)';
  return (
    <div className={wrapClass} style={{ ...wrapStyle, position: 'relative', zIndex: at != null ? 40 : (wrapStyle && wrapStyle.zIndex) }}>
      <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 12, marginBottom: 6 }}>
        <span style={{ fontSize: 15, fontWeight: 600 }}>Arrival distribution<OpHelpTip placement="start" text={"How the fleet's block arrivals are spread across the slot, in " + dist.bucketMs + "ms buckets, for mump2p and for the libp2p gossipsub baseline over the same slots. The mump2p curve sits earlier and narrower: blocks reach the fleet sooner and within a tighter band. Measured, from the same report as the figures above."} /></span>
        <span className="op-mono" style={{ fontSize: 9.5, letterSpacing: '.1em', textTransform: 'uppercase', color: 'var(--op-muted)' }}>{windowLabel || 'measured window'}</span>
      </div>
      <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 12, marginBottom: 16, flexWrap: 'wrap' }}>
        <div style={{ display: 'flex', alignItems: 'baseline', gap: 7, flexWrap: 'wrap' }}>
          <span className="op-mono" style={{ fontSize: 10, letterSpacing: '.09em', textTransform: 'uppercase', color: 'var(--op-muted)' }}>bucket</span>
          <span className="op-num" style={{ fontSize: 24, fontWeight: 400, color: 'var(--sc-fg)', lineHeight: 1 }}>{dist.bucketMs}</span>
          <span className="op-mono" style={{ fontSize: 11, color: 'var(--sc-mute)' }}>ms · paired slots</span>
        </div>
        <div className="op-race-legend"><span><i className="mum" />mump2p</span><span><i className="lib" />libp2p</span></div>
      </div>
      <div style={{ position: 'relative' }} onMouseMove={onMove} onMouseLeave={() => setHoverI(null)}>
        <svg viewBox={'0 0 ' + W + ' ' + H} preserveAspectRatio="none"
             style={{ display: 'block', width: '100%', height: 128, borderBottom: '1px solid var(--op-border)' }} aria-hidden="true">
          <path d={libPath} fill="var(--sc-bar-lib)" opacity="0.55" />
          <path d={mumPath} fill="var(--sc-bar-mum)" opacity="0.8" />
        </svg>
        {at != null && (
          <React.Fragment>
            <span className="op-spk-cursor" style={{ left: tipPct.toFixed(2) + '%' }} />
            <div className="op-strip-tip" style={{ left: Math.min(98, Math.max(2, tipPct)).toFixed(2) + '%', transform: tipTx, bottom: 'calc(100% + 8px)' }}>
              <div className="t1">{msAt(at).toLocaleString('en-US')}–{msAt(at + 1).toLocaleString('en-US')} ms</div>
              <DistBucketBars mum={mum[at]} lib={lib[at]} peak={peak} />
            </div>
          </React.Fragment>
        )}
      </div>
      <div style={{ display: 'flex', justifyContent: 'space-between', marginTop: 10 }}>
        <span className="op-mono" style={{ fontSize: 9.5, letterSpacing: '.06em', textTransform: 'uppercase', color: 'var(--op-faint)' }}>{msAt(0).toLocaleString('en-US')} ms</span>
        <span className="op-mono" style={{ fontSize: 9.5, letterSpacing: '.06em', textTransform: 'uppercase', color: 'var(--op-faint)' }}>arrival within slot</span>
        <span className="op-mono" style={{ fontSize: 9.5, letterSpacing: '.06em', textTransform: 'uppercase', color: 'var(--op-faint)' }}>{msAt(span).toLocaleString('en-US')} ms</span>
      </div>
      <ViewDetailsLink onClick={onViewDetails} />
    </div>
  );
}

// (B) Per-slot P80 advantage — one bar per slot over the window, with the average line.
// CAUTION: its bars stack a mumP2P base inside the libP2P total, so the green
// fraction IS the claim, and on the fixture that fraction is ~13% where the
// measured report puts it at ~78%. Only rendered when the live distribution is
// unavailable, and it says "sample data" on the card face, not just in the tip.
// TODO(live-data): bars derived from TELEMETRY, not measured. Needs a per-slot
// arrival series pairing both transports; no first-party source has one.
function P80BarsCard({ bars, avgP80, windowLabel, onViewDetails, wrapClass, wrapStyle }) {
  const list = bars || [];
  const vals = list.map(b => b.p80).filter(Number.isFinite);
  const has = vals.length > 0;
  // Bars are stacked to the slot's libP2P arrival (the full baseline time): a green
  // mumP2P base + a peach remainder, both scaled to the window's slowest libP2P so
  // the mumP2P-vs-libP2P proportion reads directly. The peach remainder = the lead.
  const maxLib = Math.max(1, ...list.map(b => (Number.isFinite(b.libP80) ? b.libP80 : 0)));
  const avg = Number.isFinite(avgP80) ? Math.round(avgP80) : (has ? Math.round(vals.reduce((a, b) => a + b, 0) / vals.length) : null);
  // Hover previews a slot; click pins it (click again / the pinned bar to clear).
  const [hoverI, setHoverI] = React.useState(null);
  const [pinI, setPinI] = React.useState(null);
  // The window can change under us (25/50/100) — indices shift, so drop any selection.
  React.useEffect(() => { setHoverI(null); setPinI(null); }, [list.length, windowLabel]);
  const activeI = hoverI != null ? hoverI : pinI;
  const active = (activeI != null && activeI >= 0 && activeI < list.length) ? list[activeI] : null;
  // Anchor the tip to the bar centre, but flip it near the edges (left-anchor on the
  // first bars, right-anchor on the last) so a wide tip never overflows the card.
  const tipCenter = active ? (activeI + 0.5) / Math.max(1, list.length) * 100 : 50;
  const tipLeft = Math.min(98, Math.max(2, tipCenter)).toFixed(2) + '%';
  const tipTx = tipCenter < 22 ? 'translateX(0)' : tipCenter > 78 ? 'translateX(-100%)' : 'translateX(-50%)';
  const hasArr = active && Number.isFinite(active.mumP80) && Number.isFinite(active.libP80);
  // While a bar is pinned (mouse may leave the card) keep the card lifted so the
  // popover isn't painted under a later sibling card.
  const lifted = (pinI != null || hoverI != null);
  return (
    <div className={wrapClass} style={{ ...wrapStyle, position: 'relative', zIndex: lifted ? 40 : (wrapStyle && wrapStyle.zIndex) }}>
      <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 12, marginBottom: 6 }}>
        <span style={{ fontSize: 15, fontWeight: 600 }}>Per-slot advantage <em className="op-sample-flag">sample data</em><OpHelpTip placement="start" text="One bar per slot for the last 25 slots. The full bar height is when libp2p delivered the block to the gateway. The green base is when mump2p delivered it, so the part above the green is mump2p's lead for that slot. Hover or click a bar for details. Sample data, not from the performance report." /></span>
        <span className="op-mono" style={{ fontSize: 9.5, letterSpacing: '.1em', textTransform: 'uppercase', color: 'var(--op-muted)' }}>{windowLabel || 'last 25 slots'}</span>
      </div>
      <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 12, marginBottom: 16, flexWrap: 'wrap' }}>
        <div style={{ display: 'flex', alignItems: 'baseline', gap: 7, flexWrap: 'wrap' }}>
          <span className="op-mono" style={{ fontSize: 10, letterSpacing: '.09em', textTransform: 'uppercase', color: 'var(--op-muted)' }}>window average</span>
          <span className="op-num" style={{ fontSize: 24, fontWeight: 400, color: 'var(--sc-fg)', lineHeight: 1 }}>{has ? <OpCountUp value={avg} /> : 'N/A'}</span>
          <span className="op-mono" style={{ fontSize: 11, color: 'var(--sc-mute)' }}>{has ? 'ms faster than gossipsub' : ''}</span>
        </div>
        <div className="op-race-legend"><span><i className="mum" />mump2p</span><span><i className="lib" />libp2p</span></div>
      </div>
      <div onMouseLeave={() => { setHoverI(null); setPinI(null); }} style={{ position: 'relative', height: 128, display: 'flex', alignItems: 'flex-end', gap: 3, paddingBottom: 1, borderBottom: '1px solid var(--op-border)' }}>
        {active && (
          <div className="op-strip-tip" style={{ left: tipLeft, transform: tipTx, bottom: 'calc(100% + 8px)' }}>
            <div className="t1">slot {Number(active.slot).toLocaleString('en-US')}{Number.isFinite(active.publishers) ? ` · ${active.publishers} validators` : ''}</div>
            {hasArr && <TipArrivalBars mum={active.mumP80} lib={active.libP80} />}
          </div>
        )}
        {list.map((b, i) => {
          const lib = Number.isFinite(b.libP80) ? b.libP80 : 0;
          const mum = Number.isFinite(b.mumP80) ? Math.max(0, Math.min(lib, b.mumP80)) : 0;
          const barH = (lib / maxLib) * 100;                    // total height ∝ libP2P arrival
          const greenFrac = lib > 0 ? (mum / lib) * 100 : 0;    // mumP2P's share of that time
          const isPinned = pinI === i;
          return (
            <button
              key={i}
              type="button"
              className="op-p80-cell"
              onMouseEnter={() => setHoverI(i)}
              onMouseLeave={() => setHoverI(null)}
              onFocus={() => setHoverI(i)}
              onBlur={() => setHoverI(null)}
              onClick={() => setPinI(p => (p === i ? null : i))}
              aria-pressed={isPinned}
              aria-label={`Slot ${Number(b.slot).toLocaleString('en-US')}: mump2p ${Math.round(mum)} ms vs libp2p ${Math.round(lib)} ms`}
              style={{ flex: 1, display: 'flex', alignItems: 'flex-end', height: '100%' }}
            >
              <span className="op-growy" style={{ width: '100%', minHeight: 3, height: barH.toFixed(1) + '%', borderRadius: '3px 3px 0 0', overflow: 'hidden', display: 'flex', flexDirection: 'column', animationDelay: (i * 0.02).toFixed(3) + 's', filter: isPinned ? 'brightness(1.32)' : undefined, transition: 'filter .12s ease' }}>
                <span style={{ height: (100 - greenFrac).toFixed(1) + '%', background: 'var(--sc-bar-lib)' }} />
                <span style={{ height: greenFrac.toFixed(1) + '%', minHeight: mum > 0 ? 2 : 0, background: 'var(--sc-bar-mum)' }} />
              </span>
            </button>
          );
        })}
      </div>
      <div style={{ display: 'flex', justifyContent: 'space-between', marginTop: 10 }}>
        <span className="op-mono" style={{ fontSize: 9.5, letterSpacing: '.06em', textTransform: 'uppercase', color: 'var(--op-faint)' }}>oldest</span>
        <span className="op-mono" style={{ fontSize: 9.5, letterSpacing: '.06em', textTransform: 'uppercase', color: 'var(--op-faint)' }}>{active ? 'click to pin · move away to clear' : 'per-slot arrival · mump2p vs libp2p'}</span>
        <span className="op-mono" style={{ fontSize: 9.5, letterSpacing: '.06em', textTransform: 'uppercase', color: 'var(--op-faint)' }}>newest</span>
      </div>
      <ViewDetailsLink onClick={onViewDetails} />
    </div>
  );
}

function FleetAveragesPanel({ validatorRows }) {
  const CARD = { background: 'var(--op-card)', border: '1px solid var(--op-border)', borderRadius: 18, boxShadow: 'var(--op-shadow)', padding: 24 };
  const perf = usePropagation();
  // Null on every fallback path, since the static report carries no histograms.
  const dist = (typeof window !== 'undefined' && window.OptimumValidators && window.OptimumValidators.getNetworkDistribution)
    ? window.OptimumValidators.getNetworkDistribution() : null;
  // Per-slot P80 advantage across validators, last 25 slots (chronological).
  // Demo telemetry, not the report. Only reached when the live arrival
  // distribution is unavailable; see ArrivalDistributionCard.
  const p80Bars = React.useMemo(() => {
    const rows = validatorRows || [];
    const n = (rows[0] && rows[0].slots && rows[0].slots.length) || 0;
    const out = [];
    for (let i = Math.max(0, n - 25); i < n; i++) {
      const p = perSlotP80(rows, i);
      out.push({ slot: rows[0] && rows[0].slots[i] && rows[0].slots[i].slot, p80: p.netAdvP80, mumP80: p.mumP80, libP80: p.libP80, publishers: p.publishers });
    }
    return out;
  }, [validatorRows]);
  const p80WindowAvg = React.useMemo(() => { const vals = p80Bars.map(b => b.p80).filter(Number.isFinite); return vals.length ? Math.round(vals.reduce((a, b) => a + b, 0) / vals.length) : null; }, [p80Bars]);

  return (
    <div className="hub-perf-stack">
      {/* The four headline results, ahead of the methodology strip and the cards
          that expand on them. */}
      <PerfHeroBar perf={perf} />

      {/* Measurement basis — the facts every figure below shares: the metric, the
          sample it was measured over, the window, and the last day that window
          carries data for. The full explanation lives in the "?" so the banner
          stays terse.
          
          It used to end with SnapshotCadence, which reads the Hub's TELEMETRY
          clock: "last snapshot" was the page-load time and the countdown ran 24h
          from it. Nothing about that describes this report, which refetches on its
          own 30-minute TTL and changes daily (hourly in the console's live window). A
          countdown promising fresh numbers at an arbitrary moment was worse than no
          countdown.

          SnapshotCadence itself is left alone. Its other call site is StatsView,
          which nothing currently reaches (no setView('stats') anywhere), so it is
          dormant rather than wrong: the clock it reads is the telemetry one, which
          is the clock that view runs on. */}
      <div className="op-reveal hub-runmeta">
        <span className="hub-runmeta-pair">
          <span className="k">Measurement basis</span>
          <OpHelpTip text={(perf
            ? 'Where these numbers come from: one published run of the mump2p performance report, ' + perf.spread.slotsCompared.toLocaleString('en-US') + ' ' + perf.network.toLowerCase() + ' slots over ' + (perf.windowDays == null ? perf.windowSpan : perf.windowDays + (perf.windowDays === 1 ? ' day' : ' days')) + '. mump2p was timed at ' + perf.gateways + (perf.gatewaysAtLeast ? '+' : '') + ' Optimum gateways. The libp2p gossipsub baseline is Xatu\'s first-seen data from ' + perf.xatuSentries + ' sentry nodes.' + (dist ? ' The arrival distribution below is the same measured run, in ' + dist.bucketMs + 'ms buckets.' : ' The per-slot advantage chart is the exception: it shows sample data over a live 25-slot window.')
            : 'Where these numbers come from: mump2p timed at the Optimum gateways, compared with the libp2p gossipsub baseline.')
            + (perf && perf.fromConsole && perf.windowDays != null && perf.network === 'Mainnet'
              && VAL.isPropagationFrozen && VAL.isPropagationFrozen()
              ? ' mump2p propagation is paused, so these figures are from the last whole days before the pause.'
              : '')} />
        </span>
        <span className="hub-runmeta-pair"><span className="k">Metric</span><span className="v">p95 arrival</span></span>
        <span className="hub-runmeta-pair"><span className="k">Sample</span><span className="v">{perf ? perf.gateways + (perf.gatewaysAtLeast ? '+' : '') + ' gateways · ' + perf.xatuSentries + ' Xatu sentries' : 'All connected validators'}</span></span>
        <span className="hub-runmeta-pair"><span className="k">Window</span><span className="v">{perf ? perf.spread.slotsCompared.toLocaleString('en-US') + ' slots · ' + perf.windowSpan : 'Last 25 slots'}</span></span>
        {perf && perf.measuredThrough && (
          <span className="hub-runmeta-pair">
            <span className="k">Measured through</span>
            <span className="v">{perf.measuredThrough} UTC</span>
          </span>
        )}
      </div>
      {/* The two measured report metrics replace the old demo gateway cards
          ("Average arrival at gateway" / "Arrived first at gateway"): spread is
          the window RLNC compresses, arrival is the same advantage end to end. */}
      <div className="hub-perf-pair">
      {perf && (
        <PerfMetricCard
          title="Propagation spread"
          definition={'p95 arrival – first arrival, per slot · mean of ' + perf.spread.slotsCompared.toLocaleString('en-US') + ' slots'}
          help={'Per slot, the time from the first node seeing the block to the 95th-percentile node seeing it. This is how long the block takes to spread once it exists; the proposer\'s build time is not included. Averaged over ' + perf.spread.slotsCompared.toLocaleString('en-US') + ' slots. Lower is better.'}
          mumMs={perf.spread.mumP2PMs}
          libMs={perf.spread.libP2PMs}
        />
      )}

      {perf && (
        <PerfMetricCard
          title="Slot arrival"
          definition={'p95 of arrivals from slot start, per slot · mean of ' + perf.arrival.slotsCompared.toLocaleString('en-US') + ' slots'}
          help={'Per slot, when the 95th-percentile node had the block, timed from the start of the slot. This includes the proposer\'s build time, which no network layer can change, so the gap looks smaller here than in propagation spread. Averaged over ' + perf.arrival.slotsCompared.toLocaleString('en-US') + ' slots. Lower is better.'}
          mumMs={perf.arrival.mumP2PMs}
          libMs={perf.arrival.libP2PMs}
        />
      )}
      </div>

      {/* Measured arrival distribution when the endpoint served one, and the
          per-slot fixture only as the fallback. */}
      {dist
        ? <ArrivalDistributionCard dist={dist} windowLabel={perf ? perf.windowLabel : null} wrapClass="hub-perf-card op-reveal" wrapStyle={{ display: 'flex', flexDirection: 'column', justifyContent: 'center' }} />
        : <P80BarsCard bars={p80Bars} avgP80={p80WindowAvg} windowLabel="last 25 slots · at gateway" wrapClass="hub-perf-card op-reveal" wrapStyle={{ display: 'flex', flexDirection: 'column', justifyContent: 'center' }} />}

      {/* Where the lead actually lands. It closes the section: the ladder is the
          conclusion the cards above have been building toward. */}
      {perf && <NetworkAdvantage perf={perf} />}
    </div>
  );
}



// ────────────────────────────────────────────────────────────────
// Per-validator live activity — each row's "Latest activity" cell
// rotates through slot outcomes (mumP2P vs libP2P) every few seconds,
// with a slide-in animation so the table feels alive.
// ────────────────────────────────────────────────────────────────





// ────────────────────────────────────────────────────────────────
// Fleet observability — new sections
// ────────────────────────────────────────────────────────────────
const STATUS_LABELS = {
  healthy:    { label: 'Active',      tone: 'good',  help: 'Recent comparable mump2p and libp2p samples are available.' },
  offline:    { label: 'Offline',     tone: 'bad',   help: 'More than half of this window is missing comparable telemetry.' },
  onboarding: { label: 'Onboarding',  tone: 'info',  help: 'Validator is new or still building enough comparable telemetry.' },
  na:         { label: 'No data',     tone: 'muted', help: 'No comparable mump2p/libp2p samples are available for this window.' },
};

function StatusPill({ status }) {
  const s = STATUS_LABELS[status] || STATUS_LABELS.na;
  return <span className={'hub-fleet-status hub-fleet-status-' + s.tone} title={s.help}>{s.label}</span>;
}




// Hero / command center — fleet-wide stats

// Epoch progress — 32 ticks; lights up the slots within the current epoch

// Split-flap metric strip — flip-only-on-change

// Last 32 slots rail — fleet-wide per-slot summary

// Validator x slot heat strip

// Leaderboard cards — top-N by metric (no fake movement; only show rank-change if prevRank supplied)

// Fleet comparison cards

// Validator ranking table. It shows the top `limit` by the current sort (search
// still queries the whole fleet) and expands in place to the full fleet — the
// dedicated Validators page it used to link out to is no longer routed.
// Connected-operators directory — logo + name cards (V1: names only, no stake
// or per-operator performance). Shared by the Overview and the full page.
function OperatorDirectory({ rows, minColW, variant, valueOf, unit }) {
  // List variant (Overview): one row per operator with its connected mumP2P
  // stake on the right. `valueOf(r)` returns the ETH number; `unit` its label.
  if (variant === 'list') {
    return (
      <div className="hub-op-list">
        <div className="hub-op-head">
          <span className="hub-op-head-k">Operator</span>
          <span className="hub-op-head-v">mump2p connected{unit ? ' · ' + unit : ''}</span>
        </div>
        {rows.map(r => (
          <a key={r.partner.id} className="hub-op-row" href={partnerSite(r.partner)} target="_blank" rel="noopener noreferrer" title={r.partner.name + ' ↗'}>
            <span className="hub-fleet-tbl-avatar"><PartnerLogo partner={r.partner} fallbackClassName="hub-fleet-heat-fb" /></span>
            <span className="hub-op-name">{r.partner.name}</span>
            <span className="hub-op-val">{formatEthShort(valueOf ? valueOf(r) : 0)}</span>
          </a>
        ))}
      </div>
    );
  }
  return (
    <div className="hub-op-dir" style={minColW ? { gridTemplateColumns: 'repeat(auto-fill, minmax(' + minColW + 'px, 1fr))' } : undefined}>
      {rows.map(r => (
        <a key={r.partner.id} className="hub-op-item" href={partnerSite(r.partner)} target="_blank" rel="noopener noreferrer" title={r.partner.name + ' ↗'}>
          <span className="hub-fleet-tbl-avatar"><PartnerLogo partner={r.partner} fallbackClassName="hub-fleet-heat-fb" /></span>
          <span className="hub-op-name">{r.partner.name}</span>
        </a>
      ))}
    </div>
  );
}

// TODO(live-data): the per-operator mumP2P share is (operator stake / fleet stake)
// x fleet total, which stays a guess even now the fleet total is live. Needs a
// per-operator connected-stake dimension, which fleet-stake deliberately withholds
// because the ETH-per-validator ratio identifies which operator each step was.
function ValidatorRankingTable({ rows, totalStakeEth, limit, totalCount }) {
  const [sortKey, setSortKey] = React.useState('stake');
  const [sortDir, setSortDir] = React.useState('desc');
  const [statusFilter, setStatusFilter] = React.useState('all');
  const [regionFilter, setRegionFilter] = React.useState('all');
  const [query, setQuery] = React.useState('');
  // The full fleet used to live on a dedicated Operators page; it now expands in
  // place, so the top bar's "Operators" anchor still reaches every operator.
  const [expanded, setExpanded] = React.useState(false);

  const regions = React.useMemo(() => {
    const set = new Set(['all']);
    rows.forEach(r => set.add(r.partner.region));
    return Array.from(set);
  }, [rows]);

  // mumP2P-connected ETH per validator = proportional share of the fleet total.
  // Use the fleet-wide total (prop) so shares are correct even when only the top
  // rows are shown. Figure + unit come from the single source of truth.
  const MUMP2P_FLEET_ETH = (VAL.getMumP2PFleetEth && VAL.getMumP2PFleetEth()) || 1316423;
  const unit = (VAL.getActiveChain && VAL.getActiveChain().unit) || 'ETH';
  const totalStake = (Number.isFinite(totalStakeEth) && totalStakeEth > 0)
    ? totalStakeEth
    : rows.reduce((s, r) => s + (Number.isFinite(r.partner.liveStakeEth) ? r.partner.liveStakeEth : 0), 0);
  const mump2pOf = (r) => (totalStake > 0 && Number.isFinite(r.partner.liveStakeEth)) ? (r.partner.liveStakeEth / totalStake) * MUMP2P_FLEET_ETH : 0;

  const sorted = React.useMemo(() => {
    const q = query.trim().toLowerCase();
    const filtered = rows.filter(r => {
      if (statusFilter !== 'all' && r.status !== statusFilter) return false;
      if (regionFilter !== 'all' && r.partner.region !== regionFilter) return false;
      if (q && !(r.partner.name.toLowerCase().includes(q) || r.partner.id.toLowerCase().includes(q) || (r.partner.region || '').toLowerCase().includes(q))) return false;
      return true;
    });
    const get = (r) => {
      if (sortKey === 'partner') return r.partner.name;
      if (sortKey === 'region') return r.partner.region || '';
      if (sortKey === 'status') return r.status;
      if (sortKey === 'stake') return r.partner.liveStakeEth ?? -Infinity;
      if (sortKey === 'mump2p') return mump2pOf(r);
      return r.metrics[sortKey] ?? (sortDir === 'desc' ? -Infinity : Infinity);
    };
    const out = filtered.slice().sort((a, b) => {
      const av = get(a), bv = get(b);
      if (typeof av === 'string') return sortDir === 'desc' ? bv.localeCompare(av) : av.localeCompare(bv);
      return sortDir === 'desc' ? bv - av : av - bv;
    });
    return out;
  }, [rows, sortKey, sortDir, statusFilter, regionFilter, query]);

  // When a search/filter is active, show matches (capped); otherwise show the
  // top `limit`. Search still runs against the whole fleet passed in `rows`.
  const active = query.trim() !== '' || regionFilter !== 'all' || statusFilter !== 'all';
  const cap = active ? 50 : (expanded ? sorted.length : (limit || sorted.length));
  const visible = sorted.slice(0, cap);
  const overflow = sorted.length - visible.length;
  const total = totalCount != null ? totalCount : rows.length;

  const setSort = (k) => {
    if (sortKey === k) setSortDir(d => d === 'desc' ? 'asc' : 'desc');
    else { setSortKey(k); setSortDir('desc'); }
  };
  const arrow = (k) => sortKey === k ? (sortDir === 'desc' ? ' ▼' : ' ▲') : '';

  const cols = [
    { k: 'partner', label: 'Validator',         sortable: true, align: 'left',  help: 'Validator operator, short ID, and region. Click a logo to open its website.' },
    { k: 'stake',   label: 'Total staked ' + unit,  sortable: true, align: 'right', help: 'Total active ' + unit + ' stake from the Rated daily snapshot.' },
    { k: 'mump2p',  label: 'mump2p staked ' + unit, sortable: true, align: 'right', help: 'Active stake connected to mump2p — proportional share of the fleet\'s connected total.' },
  ];

  return (
    <div className="hub-fleet-tbl-wrap">
      <div className="hub-fleet-tbl-filters">
        <div className="hub-fleet-tbl-search">
          <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
            <circle cx="11" cy="11" r="7" />
            <path d="m20 20-3.5-3.5" strokeLinecap="round" />
          </svg>
          <input
            type="text"
            value={query}
            onChange={(e) => setQuery(e.target.value)}
            placeholder="Search operator name…"
          />
        </div>
        <span className="hub-fleet-tbl-count hub-mono">{total.toLocaleString('en-US')} connected operators</span>
      </div>
      {/* Overview shows the top `limit` (10) so this column stays close in height
          to the fleet-averages column beside it; the full set is one click away
          via "View all … operators". Search still matches the whole fleet. */}
      <OperatorDirectory rows={visible} variant="list" valueOf={mump2pOf} unit={unit} />
      {sorted.length === 0 && (
        <div className="hub-fleet-tbl-empty">No operators match the current filter.</div>
      )}
      {!active && overflow > 0 && (
        <div className="hub-fleet-tbl-foot">
          <a href="#" className="op-view-details" onClick={(e) => { e.preventDefault(); setExpanded(true); }}>
            View all {total.toLocaleString('en-US')} operators ↓
          </a>
        </div>
      )}
      {!active && expanded && limit > 0 && (
        <div className="hub-fleet-tbl-foot">
          <a href="#" className="op-view-details" onClick={(e) => { e.preventDefault(); setExpanded(false); }}>
            Show top {limit.toLocaleString('en-US')} operators ↑
          </a>
        </div>
      )}
    </div>
  );
}

// ── Validators page — the full fleet, paginated + searchable ──────
// No longer routed: "Operators" now anchors to the in-page list, which expands
// to the full fleet. Kept here so the page can be restored without a rewrite.
function ValidatorsView({ rows, totalStakeEth, onBack }) {
  const PER_PAGE = 50;
  const [query, setQuery] = React.useState('');
  const [regionFilter, setRegionFilter] = React.useState('all');
  const [sortKey, setSortKey] = React.useState('stake');
  const [sortDir, setSortDir] = React.useState('desc');
  const [page, setPage] = React.useState(0);

  const regions = React.useMemo(() => {
    const set = new Set(['all']);
    rows.forEach(r => set.add(r.partner.region));
    return Array.from(set);
  }, [rows]);

  const MUMP2P_FLEET_ETH = (VAL.getMumP2PFleetEth && VAL.getMumP2PFleetEth()) || 1316423;
  const unit = (VAL.getActiveChain && VAL.getActiveChain().unit) || 'ETH';
  const totalStake = (Number.isFinite(totalStakeEth) && totalStakeEth > 0)
    ? totalStakeEth
    : rows.reduce((s, r) => s + (Number.isFinite(r.partner.liveStakeEth) ? r.partner.liveStakeEth : 0), 0);
  const mump2pOf = (r) => (totalStake > 0 && Number.isFinite(r.partner.liveStakeEth)) ? (r.partner.liveStakeEth / totalStake) * MUMP2P_FLEET_ETH : 0;

  const sorted = React.useMemo(() => {
    const q = query.trim().toLowerCase();
    const filtered = rows.filter(r => {
      if (regionFilter !== 'all' && r.partner.region !== regionFilter) return false;
      if (q && !(r.partner.name.toLowerCase().includes(q) || r.partner.id.toLowerCase().includes(q) || (r.partner.region || '').toLowerCase().includes(q))) return false;
      return true;
    });
    const get = (r) => {
      if (sortKey === 'partner') return r.partner.name;
      if (sortKey === 'region') return r.partner.region || '';
      if (sortKey === 'mump2p') return mump2pOf(r);
      return r.partner.liveStakeEth ?? -Infinity;
    };
    return filtered.slice().sort((a, b) => {
      const av = get(a), bv = get(b);
      if (typeof av === 'string') return sortDir === 'desc' ? bv.localeCompare(av) : av.localeCompare(bv);
      return sortDir === 'desc' ? bv - av : av - bv;
    });
  }, [rows, query, regionFilter, sortKey, sortDir]);

  const pageCount = Math.max(1, Math.ceil(sorted.length / PER_PAGE));
  const curPage = Math.min(page, pageCount - 1);
  const start = curPage * PER_PAGE;
  const pageRows = sorted.slice(start, start + PER_PAGE);
  React.useEffect(() => { setPage(0); }, [query, regionFilter, sortKey, sortDir]);

  const setSort = (k) => { if (sortKey === k) setSortDir(d => d === 'desc' ? 'asc' : 'desc'); else { setSortKey(k); setSortDir('desc'); } };
  const sortInd = (k) => sortKey === k ? (sortDir === 'desc' ? '▼' : '▲') : '↕';
  const cols = [
    { k: 'partner', label: 'Validator', align: 'left' },
    { k: 'stake', label: 'Total staked ' + unit, align: 'right' },
    { k: 'mump2p', label: 'mump2p staked ' + unit, align: 'right' },
  ];
  const renderPager = (pos) => sorted.length === 0 ? null : (
    <div className={'op-pager op-pager-' + pos}>
      <button type="button" className="op-pager-btn" disabled={curPage <= 0} onClick={() => setPage(p => Math.max(0, p - 1))}>← Prev</button>
      <span className="op-pager-info hub-mono">Page {curPage + 1} of {pageCount} · {(start + 1).toLocaleString('en-US')}–{Math.min(start + PER_PAGE, sorted.length).toLocaleString('en-US')} of {sorted.length.toLocaleString('en-US')}</span>
      <button type="button" className="op-pager-btn" disabled={curPage >= pageCount - 1} onClick={() => setPage(p => Math.min(pageCount - 1, p + 1))}>Next →</button>
    </div>
  );

  return (
    <section className="op-validators op-reveal" aria-label="Connected operators">
      <div className="op-validators-head">
        <h2>Connected operators</h2>
      </div>

      <div className="hub-fleet-tbl-wrap">
        <div className="hub-fleet-tbl-filters">
          <div className="hub-fleet-tbl-search">
            <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true"><circle cx="11" cy="11" r="7" /><path d="m20 20-3.5-3.5" strokeLinecap="round" /></svg>
            <input type="text" value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Search operator name…" />
          </div>
          <span className="hub-fleet-tbl-count hub-mono">{sorted.length.toLocaleString('en-US')} of {rows.length.toLocaleString('en-US')} operators</span>
        </div>
        <OperatorDirectory rows={sorted} variant="list" valueOf={mump2pOf} unit={unit} />
        {sorted.length === 0 && <div className="hub-fleet-tbl-empty">No operators match “{query}”.</div>}
      </div>
    </section>
  );
}

// ────────────────────────────────────────────────────────────────
// Hub layout
// ────────────────────────────────────────────────────────────────
// ── Stats view (new) — per-slot tables + averages, chain-selectable, P80-only ──
function opSlotTime(slot, latestSlot, snapMs) {
  const ms = snapMs - (latestSlot - slot) * 12000;
  const d = new Date(ms);
  const p = (n) => String(n).padStart(2, '0');
  return `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
}

// P80 across the fleet's validators for one slot index.
// Per-slot arrival percentile across the fleet for one slot index. `p` selects
// the percentile (0.5 / 0.8 / 0.95); defaults to P80 so existing callers (the
// Overview) are unchanged. The Stats page passes the user-selected percentile.
function perSlotP80(perValidator, i, p = 0.8) {
  const mum = [], lib = [], adv = [];
  (perValidator || []).forEach(pv => {
    const s = pv.slots && pv.slots[i];
    if (s && Number.isFinite(s.advantage)) { mum.push(s.mumP2PArrival); lib.push(s.libP2PArrival); adv.push(s.advantage); }
  });
  const pos = adv.filter(v => v > 0);
  const mumP80 = percentile(mum, p);
  const libP80 = percentile(lib, p);
  return {
    publishers: adv.length,
    mumP80,
    libP80,
    // Percentile of the per-publisher deltas (distribution of individual advantages)
    deltaAllP80: percentile(adv, p),
    deltaPosP80: pos.length ? percentile(pos, p) : null,
    // Network-level advantage: the gap between the aggregate percentile arrivals
    netAdvP80: (Number.isFinite(libP80) && Number.isFinite(mumP80)) ? (libP80 - mumP80) : percentile(adv, p),
    accelPct: adv.length ? Math.round((pos.length / adv.length) * 100) : 0,
  };
}

// Hoverable "?" — explains what a metric means. Keyboard-accessible (focus
// shows the tip too). Theme-aware; inherits nothing from uppercase contexts.
function OpHelpTip({ text, placement }) {
  return (
    <span className={'op-help' + (placement === 'bottom' ? ' op-help-down' : placement === 'start' ? ' op-help-start' : '')} tabIndex={0} role="note" aria-label={text}>
      <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.9" strokeLinecap="round" aria-hidden="true">
        <circle cx="12" cy="12" r="9.4" />
        <path d="M9.4 9.1a2.7 2.7 0 0 1 5.2.9c0 1.8-2.5 2.1-2.5 3.6" />
        <circle cx="12" cy="17.3" r="0.5" fill="currentColor" stroke="none" />
      </svg>
      <span className="op-help-tip" role="tooltip">{text}</span>
    </span>
  );
}

// Chain glyph for the nav selector. Ethereum → the OFFICIAL Ethereum diamond
// (canonical Ethereum_logo, viewBox 0 0 256 417) with its real brand colors —
// never a custom recolor. Per Ethereum's brand kit we use the colored periwinkle
// facets on light backgrounds and the white mono glyph on dark backgrounds
// (the dark facets of the color version lose contrast on a dark UI). Other
// chains fall back to the coloured status dot until a mark is added.
// Registry-driven (keyed by id).
function ChainLogo({ id, size = 15, dark = false }) {
  if (id === 'ethereum') {
    // Official Ethereum "ETH diamond (colored)" facets (ethereum.org brand assets),
    // in path order: upper-R, upper-L, lower-R, lower-L, inner-R, inner-L. We use the
    // real periwinkle facets in BOTH themes — the diamond reads on dark via its lighter
    // facets, and this avoids the flat-white recolor that lost the logo's faceting.
    const f = ['#62688F', '#8A92B2', '#62688F', '#8A92B2', '#454A75', '#62688F'];
    return (
      <svg width={size} height={size} viewBox="0 0 256 417" aria-hidden="true" style={{ flex: 'none', display: 'block' }}>
        <path fill={f[0]} d="M127.96 0 125.17 9.5v275.67l2.79 2.78 127.96-75.63z" />
        <path fill={f[1]} d="M127.96 0 0 212.32l127.96 75.63V154.16z" />
        <path fill={f[2]} d="m127.96 312.19-1.57 1.92v98.2l1.57 4.59L256 236.59z" />
        <path fill={f[3]} d="M127.96 416.9v-104.71L0 236.59z" />
        <path fill={f[4]} d="m127.96 287.95 127.96-75.63-127.96-58.16z" />
        <path fill={f[5]} d="M0 212.32 127.96 287.95V154.16z" />
      </svg>
    );
  }
  return <span className="hub-chain-dot" aria-hidden="true"></span>;
}

// Snapshot cadence readout — "last snapshot HH:MM:SS · next in M:SS". Purely
// presentational: the Hub owns the clock (chain-configurable interval from the
// validator service) and auto-pulls the next snapshot when it lapses, so there
// is no manual refresh anywhere. Ticks locally so only this small component
// re-renders every second.
function SnapshotCadence({ snapshotMs, nextAt, refreshing }) {
  const [now, setNow] = React.useState(() => Date.now());
  React.useEffect(() => {
    const id = setInterval(() => setNow(Date.now()), 1000);
    return () => clearInterval(id);
  }, []);
  const remaining = Math.max(0, (nextAt || 0) - now);
  const hh = Math.floor(remaining / 3600000);
  const mm = Math.floor((remaining % 3600000) / 60000), ss = Math.floor((remaining % 60000) / 1000);
  const pad = (n) => String(n).padStart(2, '0');
  const countdown = hh > 0 ? `${hh}h ${pad(mm)}m` : `${mm}:${pad(ss)}`;
  const updated = new Date(snapshotMs).toLocaleTimeString('en-US', { hour12: false });
  return (
    <div className="op-cadence">
      <span className={'dot' + (refreshing ? ' busy' : '')} aria-hidden="true"></span>
      <span className="lbl">Last snapshot</span>
      <time dateTime={new Date(snapshotMs).toISOString()}>{updated}</time>
      <span className="sep">·</span>
      <span className="next">{refreshing ? 'updating now…' : `next in ${countdown}`}</span>
      <OpHelpTip text="When the data was last refreshed and when the next refresh is due. The page refreshes itself on the chain's snapshot schedule, so no manual reload is needed." placement="bottom" />
    </div>
  );
}

function StatsView({ fleetSnapshot, latestSlot, snapshotMs, nextSnapAt, refreshing, validatorCount = 0 }) {
  const WINDOWS = [25, 50];
  // 'avg' = the plain average (no percentile); the rest are arrival percentiles.
  const PCTLS = [{ k: 'Avg', v: 'avg' }, { k: 'P50', v: 0.5 }, { k: 'P80', v: 0.8 }, { k: 'P95', v: 0.95 }];
  const [win, setWin] = React.useState(25);       // slots across the per-slot view + averages (default 25)
  const [pctl, setPctl] = React.useState('avg');  // metric: Avg (default) / P50 / P80 / P95
  const pctlLabel = (PCTLS.find(p => p.v === pctl) || {}).k || 'Avg';
  const [hoverBar, setHoverBar] = React.useState(null);
  const [pinnedBar, setPinnedBar] = React.useState(null);
  // Window switch shows a brief loader ONLY the first time a window is opened
  // (its data isn't "warm" yet). Once loaded it's cached, so re-selecting is
  // instant. This mirrors how a real backend would stream a wider window on
  // demand rather than shipping every window up front. The default window is
  // already warm. `winLoading` reuses the same pulse overlay as the cadence.
  const [warmed, setWarmed] = React.useState(() => new Set([25]));
  const [winLoading, setWinLoading] = React.useState(false);
  const [previewLoading, setPreviewLoading] = React.useState(false); // manual demo of the loading overlay (the "Loading" test button); closable via its ✕
  const winTimer = React.useRef(null);
  const selectWindow = React.useCallback((w) => {
    if (w === win) return;
    setWin(w);
    if (!warmed.has(w)) {
      setWinLoading(true);
      if (winTimer.current) clearTimeout(winTimer.current);
      winTimer.current = setTimeout(() => {
        setWarmed(prev => { const n = new Set(prev); n.add(w); return n; });
        setWinLoading(false);
      }, 480);
    }
  }, [win, warmed]);
  React.useEffect(() => () => { if (winTimer.current) clearTimeout(winTimer.current); }, []);
  // `refreshing` comes from the Hub's snapshot cadence: while a snapshot is
  // being pulled the pulse loader overlays the content, then the data lands.
  const fleetSlots = (fleetSnapshot && fleetSnapshot.matrix && fleetSnapshot.matrix.fleetSlots) || [];
  const perValidator = (fleetSnapshot && fleetSnapshot.matrix && fleetSnapshot.matrix.perValidator) || [];

  // Per-slot P80 advantage across validators, last `win` slots (chronological).
  const p80Bars = React.useMemo(() => {
    const n = fleetSlots.length;
    const out = [];
    for (let i = Math.max(0, n - win); i < n; i++) {
      const p = perSlotP80(perValidator, i, typeof pctl === 'number' ? pctl : 0.8);
      out.push({ slot: fleetSlots[i].slot, p80: p.netAdvP80, mumP80: p.mumP80, libP80: p.libP80, publishers: p.publishers });
    }
    return out;
  }, [fleetSlots, perValidator, win, pctl]);
  const p80WindowAvg = React.useMemo(() => { const vals = p80Bars.map(b => b.p80).filter(Number.isFinite); return vals.length ? Math.round(vals.reduce((a, b) => a + b, 0) / vals.length) : null; }, [p80Bars]);

  // Per-slot timeline: how many ms earlier mumP2P arrived each slot (the raw
  // per-slot advantage). The percentile selector summarises this into a single
  // headline figure below; the timeline itself shows every slot in the window.
  const strip = React.useMemo(() => fleetSlots.slice(-win).map(s => ({
    slot: s.slot, adv: Number.isFinite(s.advantage) ? s.advantage : 0, mum: s.mumP2PArrival, lib: s.libP2PArrival,
    ts: opSlotTime(s.slot, latestSlot, snapshotMs),
  })), [fleetSlots, latestSlot, snapshotMs, win]);
  // Bars stack to each slot's libP2P arrival (peach) with a green mumP2P base, so
  // scale to the window's slowest libP2P — the same encoding as the Overview card.
  const stripMax = Math.max(1, ...strip.map(b => (Number.isFinite(b.lib) ? b.lib : 0)));

  const tableA = React.useMemo(() => [...fleetSlots].slice(-win).reverse().map(s => ({
    slot: s.slot, mum: s.mumP2PArrival, lib: s.libP2PArrival, adv: s.advantage,
  })), [fleetSlots, win]);
  // Both arrival mini-bars share ONE scale so a tiny green bar against a long peach
  // one shows how much faster mumP2P is at a glance. Scale to the 90th-percentile
  // libP2P (not the raw max) so a single slow-tail slot doesn't shrink every other
  // bar to a dot — the rare outlier just clips at 100%. Advantage bar scaled to the
  // window's best multiple separately.
  const raceScales = React.useMemo(() => {
    const libs = tableA.map(r => (Number.isFinite(r.lib) ? r.lib : 0)).filter(x => x > 0);
    return {
      arrival: libs.length ? Math.max(1, percentile(libs, 0.9)) : 1,
      mult: Math.max(1, ...tableA.map(r => (Number.isFinite(r.mum) && Number.isFinite(r.lib) && r.mum > 0 ? r.lib / r.mum : 0))),
    };
  }, [tableA]);

  // Averages over the same fleet slots the tables use, so every number agrees.
  const avg = React.useMemo(() => {
    const w = fleetSlots.slice(-win);
    const mum = w.map(s => s.mumP2PArrival).filter(Number.isFinite);
    const lib = w.map(s => s.libP2PArrival).filter(Number.isFinite);
    const mean = a => a.length ? Math.round(a.reduce((x, y) => x + y, 0) / a.length) : null;
    return { mum: mean(mum), lib: mean(lib) };
  }, [fleetSlots, win]);

  // Metric-driven mumP2P & libP2P arrival: the plain average (Avg mode) or the
  // P50/P80/P95 percentile across ALL (operator × slot) samples in the window —
  // this is what the metric selector drives. P50 = typical slot, P95 = slow-tail.
  // Both cards read from this so they reflect whatever metric is selected.
  const pctlArr = React.useMemo(() => {
    if (pctl === 'avg') return { mum: avg.mum, lib: avg.lib };
    const mumVals = [], libVals = [];
    const n = fleetSlots.length;
    for (let i = Math.max(0, n - win); i < n; i++) {
      perValidator.forEach(pv => { const s = pv.slots && pv.slots[i]; if (s) { if (Number.isFinite(s.mumP2PArrival)) mumVals.push(s.mumP2PArrival); if (Number.isFinite(s.libP2PArrival)) libVals.push(s.libP2PArrival); } });
    }
    return { mum: mumVals.length ? percentile(mumVals, pctl) : null, lib: libVals.length ? percentile(libVals, pctl) : null };
  }, [fleetSlots, perValidator, win, pctl, avg]);
  // Advantage = the gap between the two shown arrivals, so the cards stay consistent.
  const pctlAdv = (Number.isFinite(pctlArr.mum) && Number.isFinite(pctlArr.lib)) ? pctlArr.lib - pctlArr.mum : null;

  // Window-reactive headline stats for the fleet-snapshot card (animate on change).
  const winPct = strip.length ? Math.round(strip.filter(b => b.adv >= 0).length / strip.length * 100) : 0;
  const leadMs = (avg.mum != null && avg.lib != null) ? Math.round(avg.lib - avg.mum) : 0;

  const activeBar = hoverBar || pinnedBar;
  // Anchor to the bar centre, flipping near the edges so a wide tip never overflows
  // (first bars left-anchor, last bars right-anchor) — clamps within the strip.
  const tipCenter = activeBar ? (activeBar.i + 0.5) / Math.max(1, strip.length) * 100 : 50;
  const tipLeft = Math.min(98, Math.max(2, tipCenter)).toFixed(2) + '%';
  const tipTx = tipCenter < 22 ? 'translateX(0)' : tipCenter > 78 ? 'translateX(-100%)' : 'translateX(-50%)';
  const sign = (v) => v == null ? 'N/A' : (v > 0 ? '+' : v < 0 ? '−' : '') + Math.abs(Math.round(v)) + ' ms';
  const msv = (v) => v == null ? 'N/A' : Math.round(v).toLocaleString('en-US') + ' ms';

  // Clear any hovered/pinned bar when the window changes (indices shift).
  React.useEffect(() => { setHoverBar(null); setPinnedBar(null); }, [win]);

  return (
    <section className="op-stats op-reveal" aria-label="Stats">
      {/* Chain selection lives ONLY in the nav (single source of truth) — the
          old in-page duplicate toggle was removed per the project audit. */}
      <div className="op-stats-head">
        <SnapshotCadence snapshotMs={snapshotMs} nextAt={nextSnapAt} refreshing={refreshing} />
      </div>

      {/* Every figure on this page derives from the TELEMETRY fixture, and until a
          paired per-slot source exists that is a fact a reader needs on the page
          itself rather than buried in a tooltip. The measured numbers are on the
          Overview. */}
      <div className="op-stats-samplebar" role="note">
        <em className="op-sample-flag">sample data</em>
        <span>Per-slot figures on this page run on a demonstration series, not the measured performance report. The measured results are on the Overview.</span>
      </div>

      {/* Single page-level filter/measurement-basis toolbar. Left = the one
          contextual fact (connected operators, count-up preserved); right = the
          real Percentile + Window segmented controls (formerly duplicated in the
          card header AND restated read-only in the left rail). One control
          surface now governs the whole page. */}
      <div className="op-filterbar" role="region" aria-label="Measurement basis and filters">
        <div className="op-fb-context">
          <span className="op-fb-eyebrow">Measurement basis</span>
          <div className="op-fb-operators">
            <span className="op-fb-op-v op-num"><OpCountUp value={validatorCount} decimals={0} /></span>
            <span className="op-fb-op-k">Connected operators<OpHelpTip text="The number of validator operators connected to Optimum over mump2p. Every figure on this page combines all of them, never a single operator." placement="bottom" /></span>
          </div>
        </div>
        <div className="op-fb-controls">
          <div className="op-fb-seg">
            <span className="op-winseg-lbl">Metric<OpHelpTip text="How the slots in the window are summarised. Avg is the plain average. P50 is the typical slot, P80 the upper-typical slot, P95 the slowest 5%. This applies to every figure on the page." placement="bottom" /></span>
            <div className="op-winseg" role="group" aria-label="Metric">
              {PCTLS.map(pp => <button key={pp.k} type="button" className={pctl === pp.v ? 'on' : ''} onClick={() => setPctl(pp.v)} title={pp.v === 'avg' ? 'Plain average — no percentile' : pp.k === 'P50' ? 'Median slot' : pp.k === 'P95' ? 'Slow-tail (worst 5% of slots)' : 'Upper-typical'}>{pp.k}</button>)}
            </div>
          </div>
          <div className="op-fb-seg">
            <span className="op-winseg-lbl">Window<OpHelpTip text="How many of the most recent slots every figure covers. Pick 25 or 50 and the numbers update." placement="bottom" /></span>
            <div className="op-winseg" role="group" aria-label="Window (slots)">
              {WINDOWS.map(w => <button key={w} type="button" className={win === w ? 'on' : ''} onClick={() => selectWindow(w)}>{w}</button>)}
            </div>
          </div>
        </div>
      </div>

      {/* While a strip bar is active, lift the whole body above the filter bar
          (z-index 25) so the hover tooltip escapes it — the panel's own z-index is
          otherwise trapped inside this body's stacking context. Reverts when idle so
          the filter bar's own "?" tooltips still overlay the content below it. */}
      <div className="op-stats-body" style={{ position: 'relative', zIndex: activeBar ? 30 : 1 }}>
        <div className="op-stats-main" style={{ position: 'relative' }}>
          {(refreshing || winLoading || previewLoading) && (
            <div className="op-refresh-overlay">
              {/* Preview mode is user-triggered and would otherwise trap the view
                  (the overlay covers the toggle) — give it a ✕ to dismiss. */}
              {previewLoading && (
                <button type="button" className="op-refresh-close" onClick={() => setPreviewLoading(false)}
                  aria-label="Close loading preview" title="Close preview">✕</button>
              )}
              <OpPulseLoader caption={refreshing ? 'Updating' : 'Loading'} />
            </div>
          )}
          {(
            <React.Fragment>
              {/* per-slot bar strip — hover or click a bar for detail */}
              <div className="op-panel" style={{ zIndex: activeBar ? 40 : undefined }}>
                <div className="op-panel-head">
                  <div>
                    <h3>Arrival advantage · last {strip.length} slots</h3>
                    <span className="op-meta" style={{ display: 'block', marginTop: 5 }}>mump2p vs libp2p arrival · per slot · {pctlLabel}</span>
                  </div>
                  <div className="op-race-legend"><span><i className="mum" />mump2p</span><span><i className="lib" />libp2p</span></div>
                </div>
                <div className="op-strip-wrap" onMouseLeave={() => { setHoverBar(null); setPinnedBar(null); }}>
                  {activeBar && (
                    <div className="op-strip-tip" style={{ left: tipLeft, transform: tipTx }}>
                      <div className="t1">{activeBar.ts} · slot {activeBar.slot.toLocaleString('en-US')}</div>
                      <TipArrivalBars mum={activeBar.mum} lib={activeBar.lib} />
                    </div>
                  )}
                  <div className="op-strip" role="img" aria-label={`Per-slot advantage over the last ${strip.length} slots`}>
                    {strip.map((b, i) => {
                      const lib = Number.isFinite(b.lib) ? b.lib : 0;
                      const mum = Number.isFinite(b.mum) ? Math.max(0, Math.min(lib, b.mum)) : 0;
                      const barH = (lib / stripMax * 100).toFixed(1);
                      const greenFrac = lib > 0 ? (mum / lib * 100) : 0;
                      return (
                        <button key={i} type="button" className={'op-strip-cell' + (pinnedBar && pinnedBar.i === i ? ' pinned' : '')}
                          onMouseEnter={() => setHoverBar({ ...b, i })} onFocus={() => setHoverBar({ ...b, i })} onMouseLeave={() => setHoverBar(null)}
                          onClick={() => setPinnedBar(p => (p && p.i === i) ? null : { ...b, i })}
                          aria-label={`${b.ts} · slot ${b.slot.toLocaleString('en-US')} · ${sign(b.adv)}`}>
                          <span className="op-strip-bar" style={{ '--h': barH + '%', '--i': i }}>
                            <span className="op-strip-seg-lib" style={{ height: (100 - greenFrac).toFixed(1) + '%' }} />
                            <span className="op-strip-seg-mum" style={{ height: greenFrac.toFixed(1) + '%', minHeight: mum > 0 ? 1 : 0 }} />
                          </span>
                        </button>
                      );
                    })}
                  </div>
                </div>
                <div className="op-strip-axis">
                  <span>{strip[0] ? `${strip[0].ts} · oldest` : ''}</span>
                  <span>{strip.length ? `newest · ${strip[strip.length - 1].ts}` : ''}</span>
                </div>
              </div>

              {/* Both cards read the same TELEMETRY fixture as the strip above, not
                  the measured report. The second is the aggregate result: how often
                  mumP2P won and by how much (moved off the Fleet snapshot). */}
              <div className="op-stats-grid2">
                <ArrivalCompareCard title={`${pctlLabel === 'Avg' ? 'Average' : pctlLabel} arrival at gateway`} mumMs={pctlArr.mum} libMs={pctlArr.lib} subtitle={`last ${win} slots · lower is faster`} wrapClass="op-panel op-reveal" />
                <MumVsLibCard winPct={winPct} leadMs={pctlAdv} windowLabel={`last ${win} slots`} pctlLabel={pctlLabel} title={`${pctlLabel === 'Avg' ? '' : pctlLabel + ' '}Arrived first at gateway`} wrapClass="op-panel op-reveal" />
              </div>

              {/* Table A — arrival vs arrival at gateway */}
              <div className="op-panel">
                <div className="op-panel-head">
                  <h3>mump2p arrival vs libp2p arrival</h3>
                  <div className="op-race-legend"><span><i className="mum" />mump2p</span><span><i className="lib" />libp2p</span></div>
                </div>
                <div className="op-tbl-wrap op-scroll">
                  <table className="op-tbl op-tbl-race">
                    <thead><tr><th>Slot</th><th style={{ color: 'var(--op-green)' }}>mump2p arrival</th><th style={{ color: 'var(--op-peach)' }}>libp2p arrival</th><th>mump2p advantage</th></tr></thead>
                    <tbody>
                      {tableA.map(r => {
                        // One column per metric — each a coloured value + a mini-bar on a
                        // grey track, scaled to that column's window max. mumP2P (green)
                        // and libP2P (peach) sit in their own columns, so every ms value
                        // is paired with its own bar. The advantage column states the
                        // exact multiple with a green bar scaled to the window's best.
                        const mult = (Number.isFinite(r.mum) && Number.isFinite(r.lib) && r.mum > 0) ? r.lib / r.mum : null;
                        const mumW = Number.isFinite(r.mum) ? Math.min(100, Math.max(2, r.mum / raceScales.arrival * 100)) : 0;
                        const libW = Number.isFinite(r.lib) ? Math.min(100, Math.max(2, r.lib / raceScales.arrival * 100)) : 0;
                        const advW = mult == null ? 0 : Math.min(100, Math.max(3, mult / raceScales.mult * 100));
                        return (
                          <tr key={r.slot}>
                            <td className="dim">{r.slot.toLocaleString('en-US')}</td>
                            <td className="op-m-cell">
                              <div className="op-m-val" style={{ color: 'var(--op-green)' }}>{msv(r.mum)}</div>
                              <div className="op-m-track"><span className="op-m-fill mum" style={{ width: mumW.toFixed(1) + '%' }} /></div>
                            </td>
                            <td className="op-m-cell">
                              <div className="op-m-val" style={{ color: 'var(--op-peach)' }}>{msv(r.lib)}</div>
                              <div className="op-m-track"><span className="op-m-fill lib" style={{ width: libW.toFixed(1) + '%' }} /></div>
                            </td>
                            <td className="op-m-cell">
                              <div className="op-m-val" style={{ color: 'var(--op-green)' }}>{mult == null ? '—' : <>{mult >= 10 ? Math.round(mult) : mult.toFixed(1)}×<small> faster</small></>}</div>
                              <div className="op-m-track"><span className="op-m-fill mum" style={{ width: advW.toFixed(1) + '%' }} /></div>
                            </td>
                          </tr>
                        );
                      })}
                    </tbody>
                  </table>
                </div>
              </div>
            </React.Fragment>
          )}
        </div>
      </div>
    </section>
  );
}


function Hub() {
  const { GlobeViz, ValidatorAtlas, VALIDATORS = [] } = window;
  // Drives the per-row latest-activity cells so they all rotate in sync.
  // Snapshot is frozen at mount — partner feed reflects the latest scan
  // and only refreshes on page reload (matches other SNAP cards).
  const activityTick = 0;

  // Theme — persisted; defaults to OS preference if no saved choice.
  const [theme, setTheme] = React.useState(() => {
    try {
      const saved = window.localStorage.getItem('optimum-theme');
      if (saved === 'dark' || saved === 'light') return saved;
    } catch (_) {}
    return 'light';  // default to light (matches the pre-React boot); a saved preference still wins
  });
  const themeMounted = React.useRef(false);
  React.useLayoutEffect(() => {
    const isDark = theme === 'dark';
    const bg = isDark ? '#232730' : '#F5F7F9';
    const fg = isDark ? '#FFFBFB' : '#282C34';
    // `only light`, not `light`: the `only` keyword is Chromium's force-dark
    // opt-out. Without it, Chrome Android's Auto Dark Theme and Samsung
    // Internet invert the hand-tuned light theme -- lightening the deliberately
    // dark validator rail and compositing the glass against an inverted ground.
    const scheme = isDark ? 'dark' : 'only light';
    const rootEl = document.documentElement;
    // On a live toggle (not first mount), freeze transitions so the whole page
    // swaps colours in one frame instead of each surface easing on its own clock
    // — that staggered repaint is what looked like the brightness "shaking".
    const swapping = themeMounted.current;
    if (swapping) rootEl.classList.add('hub-theme-instant');
    try { window.localStorage.setItem('optimum-theme', theme); } catch (_) {}
    rootEl.dataset.theme = theme;
    rootEl.style.backgroundColor = bg;
    rootEl.style.colorScheme = scheme;
    document.body.style.backgroundColor = bg;
    document.body.style.color = fg;
    document.body.style.colorScheme = scheme;
    themeMounted.current = true;
    if (!swapping) return undefined;
    // Restore transitions once the new palette has painted (two frames later).
    const raf = requestAnimationFrame(() => requestAnimationFrame(() => rootEl.classList.remove('hub-theme-instant')));
    return () => cancelAnimationFrame(raf);
  }, [theme]);
  const toggleTheme = () => setTheme(t => t === 'dark' ? 'light' : 'dark');

  // (Removed two decorative tickers that re-rendered the whole app every 2-3s
  // for values nothing displayed — flagged by the project audit. Live-feeling
  // motion now comes from leaf components only, e.g. SnapshotCadence.)
  // Re-render whenever the validator service publishes an update (e.g. a backend
  // refresh) so the count, tables, page and choropleth all reflect the new list.
  const [validatorsVersion, setValidatorsVersion] = React.useState(0);
  const [dirQuery, setDirQuery] = React.useState('');
  const dirGridRef = React.useRef(null);
  const [dirCols, setDirCols] = React.useState(6);
  const [dirOrigin, setDirOrigin] = React.useState(0);
  const dirQueueRef = React.useRef([]);
  React.useEffect(() => VAL.subscribe(() => setValidatorsVersion(v => v + 1)), []);
  // The live loads are kicked HERE rather than from the cards that read them.
  // A card unmounts on an Overview/Stats toggle, so its mount effect re-runs on
  // every switch; Hub never unmounts. The services memoise and back off anyway,
  // so this is defence in depth rather than the only guard.
  React.useEffect(() => {
    if (VAL.loadFleetStake) VAL.loadFleetStake();
    if (VAL.loadNetworkPerf) VAL.loadNetworkPerf();
    if (VAL.loadPropagationStatus) VAL.loadPropagationStatus();
  }, []);
  const validators = VAL.getCount();
  const ratedService = window.OptimumRatedMetrics;

  const [activeNode, setActiveNode] = React.useState(() => (VALIDATORS[0] && VALIDATORS[0].id) || 'BLK');
  const [mobileMenuOpen, setMobileMenuOpen] = React.useState(false);
  const [mobileSectionOpen, setMobileSectionOpen] = React.useState(null);
  const mobileNavRef = React.useRef(null);
  const [selectedPartner, setSelectedPartner] = React.useState(null);
  // Fleet observability window (1 epoch = 32 slots by default).
  const [fleetWindowId, setFleetWindowId] = React.useState('epoch');
  const [snapshotNonce, setSnapshotNonce] = React.useState(0);
  const [snapshotTime, setSnapshotTime] = React.useState(() => new Date());
  // ── Snapshot cadence ──────────────────────────────────────────
  // The frontend advances itself on the chain's snapshot interval (from the
  // validator service's chain registry — Ethereum: daily). When the
  // clock lapses: show the pulse loader for ~2s, re-read the snapshot (later:
  // VAL.refresh() against the API), restart the clock. No manual refresh.
  const snapIntervalMs = ((VAL.getActiveChain && VAL.getActiveChain().snapshotIntervalSec) || 240) * 1000;
  const [nextSnapAt, setNextSnapAt] = React.useState(() => Date.now() + snapIntervalMs);
  const [snapRefreshing, setSnapRefreshing] = React.useState(false);
  React.useEffect(() => {
    if (snapRefreshing) return undefined;
    const delay = Math.max(0, nextSnapAt - Date.now());
    const id = setTimeout(() => {
      setSnapRefreshing(true);
      setTimeout(() => {
        VAL.refresh();                       // single source of truth pulls (mock today, API later)
        setSnapshotNonce(n => n + 1);        // re-derive the telemetry snapshot
        setSnapshotTime(new Date());
        setNextSnapAt(Date.now() + snapIntervalMs);
        setSnapRefreshing(false);
      }, 2000);
    }, delay);
    return () => clearTimeout(id);
  }, [nextSnapAt, snapRefreshing, snapIntervalMs]);
  const [view, setView] = React.useState('overview'); // 'overview' | 'stats'
  // The measured performance report — the hero strip and the percentile ladder
  // read it here so both sit outside the Overview's snapshot closure.
  const [mapOpen, setMapOpen] = React.useState(false); // flat atlas modal
  const [mapMax, setMapMax] = React.useState(false);   // atlas maximized to full viewport
  const closeMap = React.useCallback(() => { setMapOpen(false); setMapMax(false); }, []);
  const [chainOpen, setChainOpen] = React.useState(false); // nav chain selector dropdown
  const chainRef = React.useRef(null);

  // Lock page scroll behind the full-screen overlays, the way the validator
  // dossier already does. Without it a touch-drag on the atlas chrome scrolls
  // the document underneath, and on iOS that collapses Safari's toolbars and
  // resizes the position:fixed overlay mid-gesture, so the modal visibly jumps.
  React.useEffect(() => {
    if (!mapOpen) return undefined;
    const prev = document.body.style.overflow;
    document.body.style.overflow = 'hidden';
    return () => { document.body.style.overflow = prev; };
  }, [mapOpen]);
  React.useEffect(() => {
    if (!chainOpen) return undefined;
    const onDoc = (e) => { if (chainRef.current && !chainRef.current.contains(e.target)) setChainOpen(false); };
    const onKey = (e) => { if (e.key === 'Escape') setChainOpen(false); };
    document.addEventListener('mousedown', onDoc);
    document.addEventListener('keydown', onKey);
    return () => { document.removeEventListener('mousedown', onDoc); document.removeEventListener('keydown', onKey); };
  }, [chainOpen]);
  React.useEffect(() => {
    if (!mobileMenuOpen) {
      setMobileSectionOpen(null);
      return undefined;
    }
    const onDoc = (e) => {
      if (mobileNavRef.current && !mobileNavRef.current.contains(e.target)) setMobileMenuOpen(false);
    };
    const onKey = (e) => { if (e.key === 'Escape') setMobileMenuOpen(false); };
    document.addEventListener('pointerdown', onDoc);
    document.addEventListener('keydown', onKey);
    return () => {
      document.removeEventListener('pointerdown', onDoc);
      document.removeEventListener('keydown', onKey);
    };
  }, [mobileMenuOpen]);
  const [ratedMetrics, setRatedMetrics] = React.useState(() => (
    (ratedService && ratedService.getCachedMetrics && ratedService.getCachedMetrics()) || {
      status: 'loading',
      partnerCount: validators,
      matchedPartnerCount: 0,
      totalPartnerStakeEth: null,
      totalPartnerStakeUsd: null,
      ethUsdPrice: null,
      lastUpdated: null,
      partners: [],
      sourceUrl: ratedService ? ratedService.RATED_SOURCE_URL : '',
    }
  ));
  const ratedPartnersById = React.useMemo(() => {
    const map = new Map();
    (ratedMetrics.partners || []).forEach(row => map.set(row.partnerId, row));
    return map;
  }, [ratedMetrics]);
  const maxLiveStakeEth = React.useMemo(() => {
    const values = (ratedMetrics.partners || [])
      .map(row => row.activeStakeEth)
      .filter(value => Number.isFinite(value));
    return values.length ? Math.max(...values) : 1;
  }, [ratedMetrics]);
  const enrichedPartners = React.useMemo(() => {
    const rows = VAL.getReal().map(partner => {
      const rated = ratedPartnersById.get(partner.id) || {};
      return Object.assign({}, partner, {
        liveStakeEth: Number.isFinite(rated.activeStakeEth) ? rated.activeStakeEth : null,
        liveStakeUsd: Number.isFinite(rated.activeStakeUsd) ? rated.activeStakeUsd : null,
        matchedRatedOperatorName: rated.matchedRatedOperatorName || '',
        activeValidators: Number.isFinite(rated.activeValidators) ? rated.activeValidators : null,
        ratedApr: Number.isFinite(rated.apr) ? rated.apr : null,
        ratedRaver: Number.isFinite(rated.raver) ? rated.raver : null,
        ratedSourceUrl: rated.sourceUrl || '',
        estimatedStake: !!rated.estimated,
        ratedLastUpdated: rated.lastUpdated || ratedMetrics.lastUpdated || null,
        liveStakeMaxEth: maxLiveStakeEth,
        liveStakeSharePct: Number.isFinite(rated.activeStakeEth) && ratedMetrics.totalPartnerStakeEth
          ? (rated.activeStakeEth / ratedMetrics.totalPartnerStakeEth) * 100
          : null,
      });
    });
    // Sort by Rated stake descending so the most-staked operators appear first.
    // Rows without a live stake value fall to the bottom but keep their original order.
    rows.sort((a, b) => {
      const av = Number.isFinite(a.liveStakeEth) ? a.liveStakeEth : -Infinity;
      const bv = Number.isFinite(b.liveStakeEth) ? b.liveStakeEth : -Infinity;
      return bv - av;
    });
    return rows;
  }, [ratedPartnersById, ratedMetrics.lastUpdated, ratedMetrics.totalPartnerStakeEth, maxLiveStakeEth, validatorsVersion]);

  // Shared fleet snapshot (32-slot epoch by default) — consumed by the Key
  // Metrics panel above the globe AND the fleet observability section below.
  const fleetSnapshot = React.useMemo(() => {
    const fixtureLen = VAL.getObservabilitySlots().length;
    const availableWindows = FLEET_WINDOW_OPTIONS.map(opt => ({
      ...opt,
      available: opt.size <= fixtureLen,
      effective: Math.min(opt.size, fixtureLen),
    }));
    const activeWindow = availableWindows.find(o => o.id === fleetWindowId) || availableWindows[0];
    const windowSize = 200; // rolling 200-slot snapshot per validator
    const matrix = getFleetSlotMatrix(enrichedPartners, windowSize);
    const fleetNaFloor = matrix.fleetSlots.slice(-windowSize).filter(s => s.advantage === null).length;
    const validatorRows = matrix.perValidator.map(pv => {
      const metrics = calculateWindowMetrics(pv.slots, windowSize);
      return { partner: pv.partner, slots: pv.slots, metrics, status: getValidatorStatus(metrics, pv.partner, fleetNaFloor) };
    });
    const fleetMetrics = calculateFleetAverage(validatorRows);
    const totalNa = validatorRows.reduce((s, r) => s + (r.metrics.naSlots || 0), 0);
    const latestSlot = matrix.fleetSlots[matrix.fleetSlots.length - 1]?.slot ?? null;
    const epochSize = Math.min(SLOTS_PER_EPOCH, windowSize);
    // Slots in the current rolling epoch that already have a non-null result.
    const epochSlots = matrix.fleetSlots.slice(-epochSize);
    const epochProcessed = epochSlots.filter(s => s.advantage !== null).length;
    return {
      windowSize,
      epochSize,
      epochProcessed,
      epochSlots,
      availableWindows,
      activeWindow,
      matrix,
      validatorRows,
      fleetMetrics,
      fleetNaFloor,
      fleetNaCount: totalNa,
      latestSlot,
    };
  }, [enrichedPartners, fleetWindowId, snapshotNonce]);

  // Full fleet for the ranking table + Validators page: real partners (curated
  // logos + live stake) followed by the synthetic test fleet. Lightweight — no
  // per-slot telemetry — so 4.5k rows filter/sort/paginate instantly.
  const allValidatorRows = React.useMemo(() => {
    const real = enrichedPartners.map(ep => ({ partner: ep, status: ep.status || 'active', metrics: { naSlots: 0 } }));
    const synth = VAL.getSynthetic().map(sv => ({ partner: sv, status: sv.status, metrics: { naSlots: 0 } }));
    return real.concat(synth);
  }, [enrichedPartners, validatorsVersion]);
  const allTotalStakeEth = React.useMemo(
    () => allValidatorRows.reduce((s, r) => s + (Number.isFinite(r.partner.liveStakeEth) ? r.partner.liveStakeEth : 0), 0),
    [allValidatorRows]
  );
  // Connected operators (V1): the real, named operators only — names + logos,
  // no per-operator stake/performance. The synthetic scale fleet is not listed.
  const operatorRows = React.useMemo(
    () => enrichedPartners.map(ep => ({ partner: ep, status: ep.status || 'active', metrics: { naSlots: 0 } })),
    [enrichedPartners]
  );

  const openPartnerFromNode = React.useCallback((id) => {
    // Highlight the tapped node. We deliberately do NOT open a validator
    // dossier here: that panel is not currently rendered, and setting
    // selectedPartner only triggers the body scroll-lock effect below —
    // which would silently freeze page scrolling with no panel to show for it.
    setActiveNode(id);
  }, []);

  React.useEffect(() => {
    const el = dirGridRef.current;
    if (!el || typeof ResizeObserver === 'undefined') return undefined;
    const measure = () => {
      const n = getComputedStyle(el).gridTemplateColumns.split(' ').filter(Boolean).length;
      if (n) setDirCols(n);
    };
    measure();
    const ro = new ResizeObserver(measure);
    ro.observe(el);
    return () => ro.disconnect();
  }, []);

  // The announced-partner roster: getReal() only. getAll() would fold in the
  // 4,500 generated names that pad the fleet count, and those are not partners.
  // Alphabetical, because any other order reads as a ranking.
  const directoryOps = React.useMemo(
    () => VAL.getReal().slice().sort((a, b) => a.name.localeCompare(b.name)),
    [validatorsVersion]);
  const directoryShown = React.useMemo(() => {
    const q = dirQuery.trim().toLowerCase();
    return q ? directoryOps.filter(v => v.name.toLowerCase().includes(q)) : directoryOps;
  }, [directoryOps, dirQuery]);
  // Normalised to a fixed travel time, not a fixed step per cell: a fixed step
  // grows the wave with the roster until it outlasts the cycle and the next wave
  // restarts over tiles still mid-bloom. Distances stay proportional.
  const dirDelays = React.useMemo(() => {
    const n = directoryShown.length;
    if (!n) return [];
    const origin = Math.min(Math.max(dirOrigin, 0), n - 1);
    const oCol = origin % dirCols, oRow = Math.floor(origin / dirCols);
    // Rows weighted 1.7×: a row apart reads as further than a column apart.
    const dist = (i) => Math.hypot((i % dirCols) - oCol, (Math.floor(i / dirCols) - oRow) * 1.7);
    let max = 0;
    for (let i = 0; i < n; i++) max = Math.max(max, dist(i));
    return Array.from({ length: n }, (_, i) => (max ? (dist(i) / max) * DIR_WAVE_TRAVEL_S : 0).toFixed(2) + 's');
  }, [directoryShown.length, dirCols, dirOrigin]);

  // Each wave starts from a different operator. A shuffled queue rather than a
  // random pick, so every operator is the origin once before any repeats.
  React.useEffect(() => {
    const n = directoryShown.length;
    if (dirQuery.trim() || n < 2) return undefined;
    dirQueueRef.current = [];
    const id = setInterval(() => {
      if (!dirQueueRef.current.length) {
        const q = Array.from({ length: n }, (_, k) => k);
        for (let k = q.length - 1; k > 0; k--) {
          const j = Math.floor(Math.random() * (k + 1));
          [q[k], q[j]] = [q[j], q[k]];
        }
        dirQueueRef.current = q;
      }
      setDirOrigin(dirQueueRef.current.pop());
    }, 2800);
    return () => clearInterval(id);
  }, [dirQuery, directoryShown.length]);


  // Node fleet totals for the atlas heading, from the snapshot it plots.
  const [nodeStats, setNodeStats] = React.useState(null);
  React.useEffect(() => {
    if (!mapOpen || nodeStats || !window.loadNetworkNodes) return undefined;
    let alive = true;
    window.loadNetworkNodes().then(list => {
      if (!alive || !list || !list.length) return;
      setNodeStats({ nodes: list.reduce((t, n) => t + (n.count || 0), 0), sites: list.length });
    });
    return () => { alive = false; };
  }, [mapOpen, nodeStats]);
  React.useEffect(() => {
    if (!mapOpen) return undefined;
    const onKey = (e) => { if (e.key === 'Escape') closeMap(); };
    document.addEventListener('keydown', onKey);
    return () => document.removeEventListener('keydown', onKey);
  }, [mapOpen]);

  React.useEffect(() => {
    let alive = true;
    if (!ratedService || !ratedService.getRatedMetrics) return undefined;
    ratedService.getRatedMetrics().then(metrics => {
      if (alive) setRatedMetrics(metrics);
    });
    return () => { alive = false; };
  }, [ratedService]);

  // Close the validator dossier and return the user to the partners table
  // so they keep their place in the list rather than landing at the top.
  const closeValidatorPanel = React.useCallback(() => {
    setSelectedPartner(null);
    // Wait for the panel to unlock body scroll before scrolling the page.
    requestAnimationFrame(() => {
      const el = document.getElementById('partners');
      if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' });
    });
  }, []);

  // close validator panel on esc; lock scroll
  React.useEffect(() => {
    if (!selectedPartner) return;
    const onKey = (e) => { if (e.key === 'Escape') closeValidatorPanel(); };
    document.addEventListener('keydown', onKey);
    const prev = document.body.style.overflow;
    document.body.style.overflow = 'hidden';
    return () => {
      document.removeEventListener('keydown', onKey);
      document.body.style.overflow = prev;
    };
  }, [selectedPartner, closeValidatorPanel]);

  return (
    <div className="hub-root" data-theme={theme}>
      {/* Ambient background */}
      <div className="hub-bg"></div>
      <div className="hub-bg-dots"></div>
      <img className="hub-bg-blob" src="dashboard/assets/blob-triangle.png" alt=""
        style={{ top: '4%', right: '-160px', width: 300, opacity: 0.14, transform: 'rotate(-12deg)' }} />
      <img className="hub-bg-blob" src="dashboard/assets/blob-peanut.png" alt=""
        style={{ top: '52%', left: '-180px', width: 280, opacity: 0.12, transform: 'rotate(18deg)' }} />
      <img className="hub-bg-blob" src="dashboard/assets/blob-sphere.png" alt=""
        style={{ bottom: '-80px', right: '8%', width: 180, opacity: 0.10 }} />
      <div className="hub-grain"></div>

      <div className="hub-shell">

        {/* ─── Nav ─── */}
        <nav className="hub-nav" ref={mobileNavRef}>
          <a className="hub-brand" href="#" aria-label="Optimum Hub — back to overview" onClick={(e) => { e.preventDefault(); setSelectedPartner(null); setMobileMenuOpen(false); setView('overview'); window.scrollTo({ top: 0, behavior: 'smooth' }); }}>
            <span className="hub-brand-mark" aria-hidden="true"></span>
            <span>Optimum</span>
            <span className="hub-brand-div" aria-hidden="true"></span>
            <span className="hub-brand-sub">Hub</span>
          </a>
          <div className="hub-nav-links">
            {/* No "Overview" link — the Optimum Hub wordmark (left) already returns home. */}
            {/* Tools, Topologies and Game are exploratory/educational — not live
                dashboard data — so they live together under one "Explore" menu,
                grouped by section, keeping the top bar focused on Stats/Operators. */}
            <div className="hub-nav-item">
              <button type="button" className="hub-nav-trigger" aria-haspopup="true">
                Explore
                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
                  <path d="m6 9 6 6 6-6" strokeLinecap="round" strokeLinejoin="round" />
                </svg>
              </button>
              <div className="hub-nav-menu hub-nav-menu-explore" role="menu">
                {/* Each category is its own hover/focus flyout submenu:
                    Tools ▸ APR estimator · Topologies ▸ Mesh / Tree · Game ▸ Latency Lords */}
                <div className="hub-nav-subitem">
                  <button type="button" className="hub-nav-subtrigger" aria-haspopup="true">
                    Tools
                    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="m9 6 6 6-6 6" strokeLinecap="round" strokeLinejoin="round" /></svg>
                  </button>
                  <div className="hub-nav-submenu" role="menu">
                    <a href="./apr-estimator/" target="_blank" rel="noopener" role="menuitem">APR estimator</a>
                  </div>
                </div>
                <div className="hub-nav-subitem">
                  <button type="button" className="hub-nav-subtrigger" aria-haspopup="true">
                    Topologies
                    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="m9 6 6 6-6 6" strokeLinecap="round" strokeLinejoin="round" /></svg>
                  </button>
                  <div className="hub-nav-submenu" role="menu">
                    <a href="./simulations/mesh/" target="_blank" rel="noopener" role="menuitem">Mesh topology</a>
                    <a href="./simulations/tree/" target="_blank" rel="noopener" role="menuitem">Tree topology</a>
                  </div>
                </div>
                <div className="hub-nav-subitem">
                  <button type="button" className="hub-nav-subtrigger" aria-haspopup="true">
                    Game
                    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="m9 6 6 6-6 6" strokeLinecap="round" strokeLinejoin="round" /></svg>
                  </button>
                  <div className="hub-nav-submenu" role="menu">
                    <a href="./latency-lords/" target="_blank" rel="noopener" role="menuitem">Latency Lords</a>
                  </div>
                </div>
              </div>
            </div>
            <a href="https://console.getoptimum.xyz/" target="_blank" rel="noopener">Console</a>
            <a href="https://docs.getoptimum.xyz/docs/learn/overview/intro" target="_blank" rel="noopener">Docs</a>
          </div>
          <span className="hub-nav-spacer"></span>
          <div className="hub-chain-sel" ref={chainRef}>
            <button type="button" className="hub-chain-trigger" aria-haspopup="true" aria-expanded={chainOpen} onClick={() => setChainOpen(o => !o)} title="Chain">
              <ChainLogo id={(VAL.getActiveChain && VAL.getActiveChain().id) || 'ethereum'} size={16} dark={theme === 'dark'} />
              <span className="hub-chain-name">{(VAL.getActiveChain && VAL.getActiveChain().label) || 'Ethereum'}</span>
              <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" aria-hidden="true"><path d="m6 9 6 6 6-6" strokeLinecap="round" strokeLinejoin="round" /></svg>
            </button>
            {chainOpen && (
              <div className="hub-chain-drop" role="menu">
                <div className="hub-chain-drop-label">Chain</div>
                {/* Chains come from the single source of truth — a new live
                    chain in the registry appears here (and switches the data)
                    with zero component changes. */}
                {(VAL.getChains ? VAL.getChains() : []).filter(c => c.status === 'live').map(c => {
                  const active = VAL.getActiveChain && VAL.getActiveChain().id === c.id;
                  return (
                    <button key={c.id} type="button" className={'hub-chain-opt' + (active ? ' on' : '')} role="menuitemradio" aria-checked={active}
                      onClick={() => { if (VAL.setActiveChain) VAL.setActiveChain(c.id); setChainOpen(false); }}>
                      <ChainLogo id={c.id} size={15} dark={theme === 'dark'} />
                      <span>{c.label}</span>
                      {active && <svg className="hub-chain-tick" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" aria-hidden="true"><path d="M20 6 9 17l-5-5" strokeLinecap="round" strokeLinejoin="round" /></svg>}
                    </button>
                  );
                })}
                <div className="hub-chain-opt soon" aria-disabled="true">
                  <span>More chains</span>
                  <span className="hub-chain-soon">Coming soon</span>
                </div>
              </div>
            )}
          </div>
          <button
            type="button"
            className="hub-theme-toggle"
            aria-label={"Switch to " + (theme === 'dark' ? 'light' : 'dark') + " mode"}
            title={"Switch to " + (theme === 'dark' ? 'light' : 'dark') + " mode"}
            onClick={toggleTheme}
          >
            {theme === 'dark' ? (
              // Sun icon — shown when in dark mode (click to go light)
              <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
                <circle cx="12" cy="12" r="4" />
                <path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41" />
              </svg>
            ) : (
              // Moon icon — shown when in light mode (click to go dark)
              <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
                <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79Z" />
              </svg>
            )}
          </button>
          <button
            className="hub-burger"
            aria-label="Menu"
            aria-expanded={mobileMenuOpen}
            aria-controls="hub-mobile-menu"
            onClick={() => setMobileMenuOpen(open => !open)}
          >
            <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
              <path d="M4 7h16M4 12h16M4 17h16" strokeLinecap="round" />
            </svg>
          </button>
          <div className={"hub-mobile-menu" + (mobileMenuOpen ? " open" : "")} id="hub-mobile-menu" role="region" aria-label="Explore">
            <div className="hub-mobile-menu-title" aria-hidden="true">Explore</div>

            <div className="hub-mobile-accordion">
              {[
                { id: 'tools', label: 'Tools', links: [{ label: 'APR estimator', href: './apr-estimator/' }] },
                { id: 'topologies', label: 'Topologies', links: [{ label: 'Mesh', href: './simulations/mesh/' }, { label: 'Tree', href: './simulations/tree/' }] },
                { id: 'game', label: 'Game', links: [{ label: 'Latency Lords', href: './latency-lords/' }] },
              ].map(section => {
                const open = mobileSectionOpen === section.id;
                const panelId = 'hub-mobile-' + section.id;
                return (
                  <div key={section.id} className={"hub-mobile-accordion-item" + (open ? " open" : "")}>
                    <button
                      type="button"
                      className="hub-mobile-accordion-trigger"
                      id={panelId + '-trigger'}
                      aria-expanded={open}
                      aria-controls={panelId}
                      onClick={() => setMobileSectionOpen(current => current === section.id ? null : section.id)}
                    >
                      <span>{section.label}</span>
                      <span className="hub-mobile-accordion-icon" aria-hidden="true">
                        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
                          <path d="m6 9 6 6 6-6" strokeLinecap="round" strokeLinejoin="round" />
                        </svg>
                      </span>
                    </button>
                    <div className="hub-mobile-accordion-panel" id={panelId} role="region" aria-labelledby={panelId + '-trigger'} aria-hidden={!open}>
                      <div className="hub-mobile-accordion-clip">
                        <div className="hub-mobile-accordion-links">
                          {section.links.map(link => (
                            <a key={link.href} href={link.href} target="_blank" rel="noopener" tabIndex={open ? undefined : -1} onClick={() => setMobileMenuOpen(false)}>
                              <span>{link.label}</span>
                              <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
                                <path d="M7 17 17 7M8 7h9v9" strokeLinecap="round" strokeLinejoin="round" />
                              </svg>
                            </a>
                          ))}
                        </div>
                      </div>
                    </div>
                  </div>
                );
              })}

              <a className="hub-mobile-docs" href="https://console.getoptimum.xyz/" target="_blank" rel="noopener" onClick={() => setMobileMenuOpen(false)}>
                <span>Console</span>
                <span className="hub-mobile-accordion-icon" aria-hidden="true">
                  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
                    <path d="M7 17 17 7M8 7h9v9" strokeLinecap="round" strokeLinejoin="round" />
                  </svg>
                </span>
              </a>
              <a className="hub-mobile-docs" href="https://docs.getoptimum.xyz/docs/learn/overview/intro" target="_blank" rel="noopener" onClick={() => setMobileMenuOpen(false)}>
                <span>Docs</span>
                <span className="hub-mobile-accordion-icon" aria-hidden="true">
                  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
                    <path d="M7 17 17 7M8 7h9v9" strokeLinecap="round" strokeLinejoin="round" />
                  </svg>
                </span>
              </a>
            </div>
          </div>
        </nav>

        {view === 'overview' && (<React.Fragment>
        {/* ─── Hero ─── */}
        <header className="hub-hero">
          <div className="hub-hero-meta" role="group" aria-label="Network meta">
            <div className="cell">
              <div className="k">Protocol</div>
              <div className="v">mump2p · RLNC</div>
            </div>
            <div className="cell">
              <div className="k">Environment</div>
              <div className="v">mainnet</div>
            </div>
            <div className="cell">
              <div className="k">Network status</div>
              {(() => {
                const s = VAL.getPropagationStatus ? VAL.getPropagationStatus() : null;
                if (s === 'off') return <div className="v paused" title="mump2p propagation is paused on mainnet."><i aria-hidden="true" />Paused</div>;
                if (s === 'on') return <div className="v live"><i className="op-livedot" aria-hidden="true" />Live</div>;
                // Unknown: never a guess either way.
                return <div className="v" title="Network status unavailable"><span role="img" aria-label="Unavailable">—</span></div>;
              })()}
            </div>
          </div>
        </header>

        {/* Network status: the map + live-status card. The old "Network status"
            heading was removed — the card's own "NETWORK STATUS · LIVE" header
            already labels this region, so the map sits directly under the header. */}
        <div className="hub-network" style={{ marginTop: 8 }}>
          <div className="hub-globe-card">
            <div className="hub-globe-pane">
              <GlobeViz radiusScale={0.37} activeId={activeNode} onPick={openPartnerFromNode} />

              <button
                type="button"
                className="hub-globe-expand"
                onClick={() => setMapOpen(true)}
                title="Expand to full validator map"
                aria-label="Expand to full validator map"
              >
                <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
                  <path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7" />
                </svg>
                <span>Expand map</span>
              </button>

            </div>

            <aside className="hub-metrics-panel" aria-label="Network position">

              <div className="hub-globe-metrics-grid">
                <FleetStatsCard
                  totalStakeEth={allTotalStakeEth}
                  validators={validators}
                  snapshotMs={snapshotTime.getTime()}
                  dark={theme === 'dark'}
                  validator={null}
                />
              </div>
            </aside>
          </div>
        </div>

        {/* ─── Performance ─── */}
        <div className="hub-section-head" style={{ marginTop: 36 }} id="performance">
          <h2>Performance</h2>
        </div>

        {(() => {
          const { validatorRows } = fleetSnapshot;

          return (
            <React.Fragment>
              {/* The Overview carries no snapshot cadence: the measured report is not on
                  that clock. */}
              {/* Left: top validators by stake · Right: fleet-wide averages */}
              {/* The operator list is temporarily hidden: its per-operator
                  "mumP2P connected ETH" is derived from mock stake, so the table
                  claims a per-validator figure nothing measures. ValidatorRankingTable
                  and OperatorDirectory are left intact — restoring the list is a
                  revert, not a re-implementation. The report now has the full
                  content width to itself. */}
              <FleetAveragesPanel validatorRows={validatorRows} />
            </React.Fragment>
          );
        })()}

        {/* Announced-partner directory — the full roster, searchable. */}
        <section className="hub-directory op-reveal" aria-labelledby="hub-directory-h">
          <div className="hub-perf-card" id="operators">
            <h2 className="hub-directory-title" id="hub-directory-h">Validators and staking protocols using mump2p to propagate faster</h2>
            <label className="hub-directory-search">
              <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
                <circle cx="11" cy="11" r="7" /><path d="m20 20-3.5-3.5" strokeLinecap="round" />
              </svg>
              <input
                type="search"
                value={dirQuery}
                onChange={(e) => setDirQuery(e.target.value)}
                placeholder="Search validators / operators…"
                aria-label="Search validators and operators"
              />
              {dirQuery ? (
                <button type="button" className="hub-directory-clear" aria-label="Clear search" onClick={() => setDirQuery('')}>
                  <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" aria-hidden="true">
                    <path d="M6 6l12 12M18 6 6 18" strokeLinecap="round" />
                  </svg>
                </button>
              ) : null}
            </label>
            {directoryShown.length ? (
              <div className={'hub-directory-wave' + (dirQuery.trim() ? ' is-static' : '')}>
                <div className="hub-directory-grid" ref={dirGridRef}>
                {directoryShown.map((v, i) => {
                  const delay = dirDelays[i] || '0s';
                  const site = partnerSite(v);
                  // Plain tile when an operator has no site on record.
                  const Tag = site ? 'a' : 'div';
                  return (
                    <Tag
                      key={v.id}
                      className={'hub-directory-tile' + (site ? ' is-link' : '')}
                      style={{ '--d': delay }}
                      {...(site ? { href: site, target: '_blank', rel: 'noopener noreferrer', title: v.name + ' ↗' } : {})}
                    >
                      <span className="hub-fleet-tbl-avatar" aria-hidden="true">
                        <PartnerLogo partner={v} fallbackClassName="hub-fleet-heat-fb" />
                      </span>
                      <span className="hub-directory-name">{v.name}</span>
                      {site ? <svg className="hub-directory-out" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="M7 17 17 7M9 7h8v8" /></svg> : null}
                    </Tag>
                  );
                })}
                </div>
              </div>
            ) : (
              <div className="hub-directory-empty" role="status">No validators found</div>
            )}
          </div>
        </section>

        </React.Fragment>)}

        {view === 'stats' && (
          <StatsView
            fleetSnapshot={fleetSnapshot}
            latestSlot={fleetSnapshot.latestSlot}
            snapshotMs={snapshotTime.getTime()}
            validatorCount={operatorRows.length}
            nextSnapAt={nextSnapAt}
            refreshing={snapRefreshing}
          />
        )}

        {mapOpen && ValidatorAtlas && (
          <div className={'op-atlas-overlay' + (mapMax ? ' op-atlas-overlay--full' : '')} role="dialog" aria-modal="true" aria-label="Validator network map"
            onClick={(e) => { if (e.target === e.currentTarget) closeMap(); }}>
            <div className={'op-atlas-modal' + (mapMax ? ' op-atlas-modal--full' : '')}>
              <div className="op-atlas-head">
                <div>
                  <div className="op-atlas-eyebrow">Optimum · Network atlas</div>
                  <h2 className="op-atlas-title">{nodeStats ? nodeStats.nodes.toLocaleString('en-US') + ' nodes · ' + nodeStats.sites + ' sites' : 'Loading node map…'}</h2>
                  <div className="op-atlas-subhint">Hover a country for its nodes or a mark for its site · scroll or drag to explore</div>
                </div>
                <div className="op-atlas-legend">
                  <button type="button" className="op-atlas-max" onClick={() => setMapMax((m) => !m)}
                    aria-label={mapMax ? 'Restore map size' : 'Maximize map'} title={mapMax ? 'Restore' : 'Maximize'}>
                    {mapMax ? (
                      <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="M9 9H4M9 9V4M15 9h5M15 9V4M9 15H4M9 15v5M15 15h5M15 15v5" /></svg>
                    ) : (
                      <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="M8 3H5a2 2 0 0 0-2 2v3M16 3h3a2 2 0 0 1 2 2v3M8 21H5a2 2 0 0 1-2-2v-3M16 21h3a2 2 0 0 0 2-2v-3" /></svg>
                    )}
                  </button>
                  <button type="button" className="op-atlas-close" onClick={closeMap} aria-label="Close map">✕</button>
                </div>
              </div>
              <div className="op-atlas-canvas">
                <ValidatorAtlas
                  dark={theme === 'dark'}
                  height="100%"
                />
              </div>
              {/* Concise, neutral boundary disclaimer with a source link. */}
              <div className="op-atlas-foot">
                <span className="af-note">Borders follow internationally-recognized references and are illustrative — they may be imprecise. Source: <a href="https://www.naturalearthdata.com" target="_blank" rel="noopener">Natural&nbsp;Earth</a>.</span>
              </div>
            </div>
          </div>
        )}

        {/* ─── Footer ─── */}
        <footer className="hub-foot">
          <div><span className="mark" aria-hidden="true"></span>Optimum Hub · © 2026 Spice Solutions Inc.</div>
          <nav className="hub-foot-links" aria-label="Optimum links">
            {FOOT_LINKS.filter(l => l.href).map(l => (
              <a key={l.label} href={l.href} target="_blank" rel="noopener noreferrer">{l.label}</a>
            ))}
          </nav>
        </footer>

      </div>

    </div>
  );
}

// ────────────────────────────────────────────────────────────────
// Inline validator detail for the globe card
// ────────────────────────────────────────────────────────────────

// ────────────────────────────────────────────────────────────────
// Validator detail panel — slides in from right on partner click
// ────────────────────────────────────────────────────────────────

window.Hub = Hub;
