/* ============================================================================
   Partner portal: shared shell.
   Loaded AFTER the design system stylesheets, which supply the light-theme tokens
   (_ds/…/colors_and_type.css). This file adds the dark overrides, the animated
   background, and the page chrome the five partner pages share.
   Every value here is copied from Design/SiberXpose.dc.html rather than re-picked,
   so the portal cannot drift away from the public page.
   ============================================================================ */

/* The design system stylesheet ships no box-sizing reset, so `width:100%` plus padding
   overflowed the viewport by exactly the padding: 407px inside a 375px screen. */
*, *::before, *::after { box-sizing:border-box }

/* ---------- motion + focus tokens ----------
   Built-in CSS easings are too weak to read as intentional. These are the standard strong
   curves: ease-out for anything entering (starts fast, so the user sees movement at the exact
   moment they are watching), ease-in-out for on-screen movement. ease-in is never used on UI 
   it delays the first frame and reads as sluggish at the same duration. */
:root {
  --brand-fill:#B32468; --brand-ink:#B0246A; --ok-fill:#07766A;
  /* The five risk bands, identical to SiberRadar's (Design/sr-format.js) and to BAND_HEX in
     lib/report-model.mjs. One palette across the portal, the PDF, and SiberRadar: a band that
     is #F08A2C in one product must not be a different orange in the next. Defined here rather
     than under [data-theme="dark"] so the light theme gets them too — it had none at all, so
     every score and band label rendered with no colour. */
  --band-veryhigh:#D83483; --band-high:#F08A2C; --band-medium:#C99A00;
  --band-low:#5A9E2F; --band-verylow:#0FA98C;
  /* The warning amber, which was the literal var(--warn-ink) in nine places. That hex is built for a
     navy surface and measures 1.6:1 on the light theme's near-white — the Two-factor banner's
     shield, the pending count, and every hint icon were effectively invisible there. It is a
     token now, dark enough to read on white and unchanged on navy. */
  --warn-ink:#8A6A00;
  --ease-out: cubic-bezier(0.23, 1, 0.32, 1);
  --ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
  --t-press: 140ms;   /* button press feedback */
  --t-pop: 180ms;     /* dropdowns, popovers */
  --t-modal: 240ms;   /* modals, sheets */
}

/* One visible focus ring for the whole portal. There were none at all before, which fails the
   first accessibility rule in every guide: a keyboard user could not see where they were. */
:where(a, button, input, select, [tabindex]):focus-visible {
  outline: 2px solid var(--magenta-500);
  outline-offset: 2px;
  border-radius: 6px;
}

/* ---------- dark token overrides ---------- */
/* The brand magenta #D83483 cannot do both jobs on a dark surface. As a filled button under
   white text it measures 4.43:1, just under the 4.5 body minimum; as text ON that surface it
   measures 3.56:1, well under. So it splits by role rather than getting bent out of shape:
   a darker fill for buttons, a lighter ink for text. Both measured, not eyeballed, and the
   brand hue is unchanged in both. #D83483 itself stays for large marks and non-text fills. */
[data-theme="dark"] {
  --warn-ink:#F6C705;     /* on --bg-canvas: 12.4:1 — the original amber, where it belongs */
  --brand-fill:#C42A75;   /* white on this: 5.34:1 */
  --brand-ink:#F0559C;    /* on --bg-surface: 4.87:1, on --bg-canvas: 5.54:1 */
  --ok-fill:#07766A;      /* white on this: 5.51:1 */
  --bg-canvas:#06172F; --bg-surface:#0C2245; --bg-surface-2:#123156;
  --fg-1:#EAF1FB; --fg-2:#9FB2CE; --fg-3:#8CA3C4;
  --border:rgba(140,175,225,.14); --border-strong:rgba(140,175,225,.24); --divider:rgba(140,175,225,.10);
  --grey-100:rgba(140,175,225,.07); --grey-200:rgba(140,175,225,.15); --accent-soft:rgba(216,52,131,.16);
  --shadow-sm:0 1px 2px rgba(2,10,26,.6); --shadow-md:0 6px 22px rgba(2,10,26,.55); --shadow-lg:0 22px 55px rgba(2,10,26,.7);
}

/* --fg-3 was #647A9C, which measures 3.61:1 on --bg-surface: under the 4.5:1 body minimum,
   and it was carrying every 12px label in the portal. Lifted to #8CA3C4. The main page keeps
   its own value; this override is scoped to the partner pages so nothing there shifts. */
html { min-height:100%; background:var(--bg-canvas); color-scheme:light }
html[data-theme="dark"] { color-scheme:dark }
body {
  margin:0; font-family:var(--font-sans); font-variant-numeric:tabular-nums;
  -webkit-font-smoothing:antialiased; text-rendering:optimizeLegibility;
  background:var(--bg-canvas); color:var(--fg-1);
}
a { color:var(--brand-ink); text-decoration:none }
a:hover { text-decoration:underline }
input { font-family:var(--font-sans) }
input::placeholder { color:var(--fg-3) }
input:focus { outline:none }

/* ---------- layer 1: static atmosphere ----------
   Keyed off <html>, not off .spx-root. SiberXpose.dc.html stamps data-theme on BOTH because
   React owns the root div, but here that second copy is a second source of truth: theme-boot
   sets <html> before paint while the div's attribute is baked into the markup, so a stored
   "light" preference rendered dark panels with dark-on-dark text. One element owns it. */
html[data-theme="dark"] .spx-root::before {
  content:""; position:fixed; inset:0; z-index:0; pointer-events:none;
  background:
    radial-gradient(130% 90% at 50% -14%, rgba(216,52,131,.06), transparent 55%),
    radial-gradient(120% 80% at 82% 8%, rgba(53,102,190,.08), transparent 60%),
    radial-gradient(100% 70% at 50% 116%, rgba(11,40,96,.16), transparent 72%),
    linear-gradient(rgba(140,175,225,.02) 1px, transparent 1px) 0 0/100% 46px,
    linear-gradient(90deg, rgba(140,175,225,.02) 1px, transparent 1px) 0 0/46px 100%;
}

/* The shield is decoration and bleeds past the viewport by design: at 375px its 400px
   minimum overflowed by 29px and made the page scroll sideways. `clip` rather than `hidden`
   because clip creates no scroll container, so the fixed header and scroll anchoring are
   untouched. Decorative layers must never be able to move the page. */
.spx-root { overflow-x:clip }

/* ---------- layer 2: the drifting shield ---------- */
/* top:39% is where the main page puts it. Here the card sits dead centre and would hide it
   completely, so the shield drops to 52% and grows past the card edges: the orbit then
   reads as motion around the card rather than a hidden animation behind an opaque panel. */
.spx-shieldwrap { position:absolute; left:50%; top:52%; transform:translate(-50%,-50%) }
.spx-shield1 {
  display:block; transform-origin:center; will-change:transform,filter;
  animation:spx-orbit 34s ease-in-out infinite, spx-shglow 6s ease-in-out infinite;
}
@keyframes spx-orbit {
  0%{transform:translate(0,0) rotate(-4deg) scale(1)}
  12%{transform:translate(80px,-70px) rotate(5deg) scale(1.05)}
  25%{transform:translate(120px,40px) rotate(8deg) scale(1.02)}
  38%{transform:translate(46px,96px) rotate(3deg) scale(1.08)}
  50%{transform:translate(-70px,80px) rotate(-5deg) scale(1.03)}
  63%{transform:translate(-118px,14px) rotate(-8deg) scale(1.06)}
  76%{transform:translate(-90px,-72px) rotate(-3deg) scale(1.02)}
  88%{transform:translate(-24px,-96px) rotate(-1deg) scale(1.04)}
  100%{transform:translate(0,0) rotate(-4deg) scale(1)}
}
@keyframes spx-shglow {
  0%,100%{filter:drop-shadow(0 0 18px rgba(216,52,131,.16))}
  50%{filter:drop-shadow(0 0 46px rgba(216,52,131,.5))}
}

/* ---------- layer 2b: topographic contours ---------- */
/* Ported from the home page's .spx-topo. Two contour fields cut off by the left edge (upper)
   and the right edge (lower); the centre is left empty for the card and the shield. A mask,
   not a background-image: the SVG strokes in currentColor, which as an image falls to black
   in both themes — the mask only reads its alpha, so the colour follows --fg-2.
   Fixed like the canvas, and the outer box never moves: only ::before/::after drift inside an
   overflow:hidden frame, so the drift cannot grow the page. 34s matches spx-orbit. */
.spx-topo { position:fixed; inset:0; z-index:0; overflow:hidden; pointer-events:none; opacity:.09; --s:min(72vw,900px) }
html[data-theme="dark"] .spx-topo { opacity:.13 }
.spx-topo::before, .spx-topo::after {
  content:""; position:absolute; width:var(--s); height:calc(var(--s) * .93);
  background:var(--fg-2);
  mask:url("/assets/topographic.svg") center/100% 100% no-repeat, radial-gradient(closest-side,#000 45%,transparent);
  mask-composite:intersect; animation:spx-topodrift 34s ease-in-out infinite;
}
.spx-topo::before { left:calc(var(--s) / -2); top:calc(30% - var(--s) * .465) }
.spx-topo::after { right:calc(var(--s) / -2); top:calc(72% - var(--s) * .465); animation-direction:reverse }
@keyframes spx-topodrift { 0%,100%{transform:translate(0,0)} 50%{transform:translate(18px,-14px)} }
/* Both shield animations stop while a modal is open, and the glow is the one that matters.
   drop-shadow is a filter, so it is REPAINTED every frame rather than composited — and it runs
   on a 6s infinite loop for as long as the page is open. On its own that is a background cost
   nobody notices. Under a modal it compounds: .spx-modal carries backdrop-filter:blur(3px), and
   a backdrop that is still changing every frame is a backdrop the compositor has to re-blur
   every frame, underneath a panel that is itself being scrolled.
   Nothing is lost by stopping it — the shield sits behind an overlay that is 72% opaque and
   blurred, so it is not visible while this class is on. Toggled by partner-ui.js. */
html.spx-modal-open .spx-shield1 { animation:none }
@media (prefers-reduced-motion: reduce) {
  *{ animation-duration:.01ms !important; transition:none !important }
}

/* ---------- theme-swapped artwork ----------
   The main page swaps the shield PNG per theme via component logic. These pages have no
   component runtime, so both images ship and CSS hides one. Doing it in CSS rather than JS
   means the correct one is right on the FIRST paint: a JS swap would flash the wrong art. */
[data-theme="dark"] .spx-lightonly { display:none !important }
[data-theme="light"] .spx-darkonly { display:none !important }

/* The browser's default `[hidden] { display: none }` is a plain type-less rule, so ANY class
   here that sets `display` beats it and the attribute silently does nothing. That bit three
   elements on the dashboard — an empty movers bar and a dismissed two-factor prompt both
   stayed on screen — and it would bite the next one too, because nothing about writing
   `.spx-thing { display:flex }` warns you. One rule closes the whole class of it. */
[hidden] { display:none !important }

/* ---------- motion parity with the main page ----------
   The background layers alone were not what made the home page feel alive: it also has an
   entrance (spx-up / spx-pop), a rotating gradient glow around its hero panel (spx-glowring
   driving spx-huemove), and a glow on its headline. All four are copied verbatim rather than
   re-invented. Every one respects the reduced-motion block above. */
@keyframes spx-up   { from { opacity:0; transform:translateY(12px) } }
@keyframes spx-pop  { from { opacity:0; transform:scale(.9) } }
@keyframes spx-huemove { 0% { background-position:0% 50% } 100% { background-position:300% 50% } }

.spx-glowring { position:relative; isolation:isolate }
.spx-glowring::before {
  content:""; position:absolute; inset:-2px; border-radius:calc(var(--r-lg) + 2px);
  background:linear-gradient(120deg,#D83483,#3E7BE0,#0FA98C,#D83483);
  background-size:300% 300%; filter:blur(9px); opacity:.28; z-index:-1;
  animation:spx-huemove 9s linear infinite;
}
[data-theme="light"] .spx-glowring::before { opacity:.2 }

.spx-glowtext { text-shadow:0 0 34px rgba(216,52,131,.38) }
[data-theme="light"] .spx-glowtext { text-shadow:none }

/* ---------- tooltips ----------
   The portal's own, not the browser's. `title` waits about a second, cannot be styled, and on
   a touch screen never appears at all — so anything it carries is effectively not on the page.
   Ported from SiberRadar's .sr-ttip: instant, brand surface, small arrow, one transition.
   `title` stays on the same elements where it already was: it is the accessible name, and it
   is the only thing left for a screen reader and for a finger.

   Usage: data-tip="…" on any element. Opens ABOVE by default, flips below when there is no
   room. data-tip-pos="bottom" asks for below first.

   This was ::after on the trigger and is now ONE fixed-position node painted by
   partner-tip.js. The reason is structural, not stylistic: an absolutely positioned tooltip
   inside `.spx-scroll` (overflow:auto) is clipped by it, so every tip on both tables was cut
   off at the panel edge — and the ones that were not clipped counted toward the scroll width
   and put a scrollbar under a table that fits. A fixed node is in no scroll box at all, so
   neither problem has anywhere to come back from. It also lets a tip carry markup, which
   `content:attr()` never could — see the score and band breakdowns. */
.spx-tip {
  position:fixed; top:0; left:0; z-index:200; opacity:0; pointer-events:none;
  max-width:250px; padding:7px 10px;
  background:var(--tip-bg); color:#EAF1FB; border-radius:8px;
  box-shadow:0 12px 28px -10px rgba(2,10,26,.7);
  font-family:var(--font-sans); font-size:11.5px; font-weight:600; line-height:1.45;
  letter-spacing:0; text-transform:none; white-space:pre-line; text-align:center;
  transform:translateY(4px);
  transition:opacity .14s var(--ease-out), transform .14s var(--ease-out);
}
.spx-tip.is-below { transform:translateY(-4px) }
.spx-tip.is-on    { opacity:1; transform:translateY(0) }
/* The arrow tracks the trigger's centre through --ax rather than sitting at 50%: a tip clamped
   to the viewport edge is no longer centred on what it describes, and an arrow pointing at
   nothing is worse than no arrow. */
.spx-tip::after {
  content:""; position:absolute; left:var(--ax,50%); transform:translateX(-50%);
  border:5px solid transparent;
}
.spx-tip::after            { top:100%; border-top-color:var(--tip-bg) }
.spx-tip.is-below::after   { top:auto; bottom:100%; border-top-color:transparent; border-bottom-color:var(--tip-bg) }

/* Rich variant: a panel, not a label. Left-aligned, wider, and its own type scale, because the
   thing inside is a table of numbers rather than a sentence. */
.spx-tip--rich {
  max-width:none; width:296px; padding:13px 14px; text-align:left; white-space:normal;
  font-size:12px; font-weight:500;
}
.spx-tip-k {
  color:#8FA6C8; font-size:10px; font-weight:700; letter-spacing:.09em; text-transform:uppercase;
}
.spx-tip-h { margin:5px 0 11px; color:#fff; font-size:14.5px; font-weight:700 }
.spx-tip-row { margin-bottom:9px }
.spx-tip-row:last-child { margin-bottom:0 }
.spx-tip-line { display:flex; align-items:baseline; justify-content:space-between; gap:10px }
.spx-tip-line b { color:#fff; font-size:12.5px; font-weight:700 }
.spx-tip-line span { color:#B9C9E2; font-variant-numeric:tabular-nums; font-size:11.5px }
.spx-tip-bar { height:3px; margin:5px 0 3px; border-radius:99px; background:rgba(255,255,255,.14); overflow:hidden }
.spx-tip-bar > i { display:block; height:100%; border-radius:99px }
.spx-tip-sub { color:#8FA6C8; font-size:10.5px }
.spx-tip-foot {
  margin-top:11px; padding-top:10px; border-top:1px solid rgba(255,255,255,.13);
  color:#8FA6C8; font-size:10.5px; line-height:1.5;
}
:root { --tip-bg:#12305C }
[data-theme="light"] {
  --tip-bg:#132743;
  /* The design system's --fg-3 measures 3.04:1 on this theme's canvas, and it is not decoration
     here: the stat labels, the page note, the quota line and every date in the table are drawn
     with it. Darkened to clear 4.5:1 for small text. Dark mode is untouched — it already
     measures 7.2:1 there. */
  --fg-3:#6B7078;
}

/* Row entrance. SiberRadar gives every <tr> `animation:srrise`; the same here, re-fired by
   partner-table.js after each sort or page change so the new page arrives rather than
   appearing. The stagger is set per row as --i. */
@keyframes spx-rise { from { opacity:0; transform:translateY(7px) } }
.spx-rt tbody tr.is-in { animation:spx-rise .3s var(--ease-out) both; animation-delay:calc(var(--i,0) * 22ms) }

/* `backwards`, not `both`. With `both` the final transform sticks around as a computed
   matrix, and an element with a transform creates a stacking context: which trapped the
   filter dropdowns inside .spx-scanrow so their z-index could not lift them over the stats
   grid below. `backwards` still hides the pre-animation frame but leaves transform:none
   once it finishes. The home page already uses this variant in one place. */
.spx-enter     { animation:spx-up .42s var(--ease-out) backwards }
.spx-enter--2  { animation:spx-up .42s var(--ease-out) .05s backwards }
.spx-enter--3  { animation:spx-up .42s var(--ease-out) .1s backwards }
.spx-popin     { animation:spx-pop .3s var(--ease-out) .08s backwards }

/* ---------- top chrome on the card pages: brand left, controls right ----------
   The same layout as the main page: the SiberMate wordmark top-left, every control in one
   cluster top-right. Both read the same edge variables (copied from the main page's
   .spx-root), so the wordmark and the cluster sit on one line, the same distance from their
   corners. The 44px in the brand's min-height is the tallest control in the cluster.
   The wordmark scrolls with the page, as the main page's does; only the controls stay fixed.
   It used to be fixed too, and on a phone, where the Clerk card is taller than the screen,
   the card scrolled straight underneath the letters. */
.spx-root { --edge-y:clamp(12px,2.4vw,24px); --edge-x:clamp(16px,3.4vw,44px); --chrome-h:calc(var(--edge-y) * 2 + 44px) }
.spx-pcorner {
  position:absolute; top:0; left:0; z-index:50; display:flex; align-items:center;
  min-height:var(--chrome-h); padding:var(--edge-y) var(--edge-x);
}
/* The wordmark PNG carries ~20.4% transparent margin on all four sides (ink 1994x435 on a
   2293x735 canvas), so a 34px-tall image shows ~20px letters. Scale it up and eat the empty
   margin with a negative one: layout then measures the ink alone, ~30px. Ported from the
   main page's .spx-brandimg. */
.spx-brandimg { --h:52px; height:var(--h); width:auto; display:block; margin:calc(var(--h) * -.204) }
@media (max-width:620px) { .spx-brandimg { --h:42px } }

/* Back to the public scanner. The partner pages are a side door off the main page, and a
   visitor who lands here by mistake had no way out but the browser button. It used to sit
   alone top-left; that corner now belongs to the brand, and the pill joins the theme toggle
   on the right — where the main page keeps its own cross-link, "Partner". */
.spx-pback {
  display:inline-flex; align-items:center; gap:7px; min-height:44px; padding:7px 15px;
  background:var(--bg-surface-2); border:1px solid var(--border); border-radius:var(--r-pill);
  color:var(--fg-1); text-decoration:none; font-size:13px; font-weight:600; white-space:nowrap;
}

@media (max-width:620px) { .spx-pback span { display:none } .spx-pback { padding:7px 11px } }

/* ---------- header: card pages keep the main page's fixed top-right cluster ---------- */
.spx-phead {
  position:fixed; top:0; right:0; z-index:50; background:transparent;
  padding:var(--edge-y) var(--edge-x); display:flex; align-items:center; gap:10px;
}
/* Next to the 44px back pill the 34px toggle read as an afterthought; one height for the
   whole cluster. The board pages' bar keeps its own sizing. A solid fill, like the main page's
   theme button: this cluster floats over the card once a phone scrolls, and with a
   transparent fill the card's text showed through the ring. */
.spx-phead:not(.spx-phead--bar) .spx-iconbtn { width:44px; height:44px; background:var(--bg-surface-2); border-color:var(--border) }

/* ---------- header: the board pages get a real bar ----------
   On /partner-app and /partner-admin the cluster above was wrong twice over, and both faults
   are the same complaint:
   1. Its right edge sat at the viewport (1440), while every block below it stopped at the
      1280 container (1328). Nothing on the page lined up with anything above it.
   2. With no background, the table and the alert banners scrolled underneath the avatar.
   Full-bleed so the rule spans the window, with an inner track on the SAME 1280 container and
   the SAME padding as .spx-pboard: the brand lands on the exact left edge of the content and
   the avatar on its exact right edge. */
.spx-phead--bar {
  left:0; padding:0; display:block;
  background:color-mix(in srgb, var(--bg-canvas) 84%, transparent);
  backdrop-filter:blur(10px); -webkit-backdrop-filter:blur(10px);
  border-bottom:1px solid var(--divider);
}
.spx-phead-in {
  width:100%; max-width:1280px; margin:0 auto;
  padding:11px clamp(18px,2.6vw,32px);
  display:flex; align-items:center; gap:10px;
}
/* The wordmark moves out of the page body and into the bar. In the body it was a third
   left-hand landmark competing with the back pill above it and the h1 below it. */
/* Weight 400 is the brand wordmark's actual weight, measured off Design/assets/mark-navy.png
   rather than eyeballed: stem/cap 13.7% and width/cap 6.240 there, against 13.4% / 6.236 for
   Lexend Deca 400 at normal tracking. 700 — what this was — measures 20.2%. */
.spx-pbrand { display:flex; align-items:center; gap:10px; margin-right:auto; color:var(--fg-1); font-size:16px; font-weight:400 }
.spx-pbrand:hover { text-decoration:none }
.spx-pbrand img { width:26px; height:26px }
/* Matches the wordmark's weight now; the suffix is separated by colour alone, because at 500
   against a 400 wordmark it would read as the heavier of the two. */
.spx-pbrand i { font-style:normal; color:var(--fg-3); font-weight:400 }
@media (max-width:620px) { .spx-pbrand i { display:none } }
/* width:max-content because a pill hugs its buttons. Without it the control is a block-level
   flex and stretches to whatever contains it — which on the report modal's two-item Language
   switch drew a full-width track with EN, ID and then a hand's width of empty pill. The admin
   tabs had already patched this inline; the patch is now here and the inline copy is gone. */
.spx-seg { display:flex; width:max-content; padding:3px; background:var(--bg-surface-2); border:1px solid var(--border); border-radius:var(--r-pill) }
.spx-seg button {
  padding:5px 12px; border:0; border-radius:var(--r-pill); background:transparent;
  color:var(--fg-2); font-family:var(--font-sans); font-size:12px; font-weight:600; cursor:pointer;
}
.spx-seg button[aria-pressed="true"] { background:var(--brand-fill); color:#fff }

/* ---------- centred card (sign-in, sign-up, pending) ---------- */
/* Top and bottom padding are the chrome row's height, so a card taller than the screen starts
   below the wordmark and the controls instead of under them. Equal on both ends, so a card
   that fits stays centred. */
.spx-pwrap {
  position:relative; z-index:1; min-height:100vh;
  display:flex; flex-direction:column; align-items:center; justify-content:center;
  padding:var(--chrome-h) 24px; gap:18px;
}
/* Elevation rule for the whole portal, applied without exception:
   FLOATING surfaces (auth card, dropdown, modal) get a shadow and NO border: the shadow is
   what says "above the page", and a border on top of it is the ghost-card tell.
   INLINE surfaces (panels, tiles) get a border and NO shadow: they sit in the page, they do
   not hover over it. Radius is 14px on cards; 20px reads as a toy. */
.spx-pcard {
  width:100%; max-width:440px; padding:34px clamp(20px,4vw,36px);
  background:var(--bg-surface); border:0;
  border-radius:14px; box-shadow:0 18px 44px rgba(2,10,26,.42); text-align:center;
}
.spx-ppill {
  display:inline-flex; align-items:center; gap:8px; padding:6px 14px;
  font-size:11px; font-weight:700; letter-spacing:.08em; text-transform:uppercase;
  color:var(--magenta-500); background:rgba(216,52,131,.1);
  border:1px solid rgba(216,52,131,.3); border-radius:var(--r-pill);
}
.spx-ppill::before { content:""; width:6px; height:6px; border-radius:50%; background:var(--magenta-500) }
.spx-plogo { display:flex; align-items:center; justify-content:center; gap:10px; margin:22px 0 6px }
.spx-plogo img { width:34px; height:34px }
/* 400 / normal, for the reason spelled out above .spx-pbrand. The tracking matters as much as
   the weight: mark-navy measures width/cap 6.240, and -.02em pulls Lexend Deca to 6.032. */
.spx-plogo span { font-size:23px; font-weight:400; letter-spacing:normal }
.spx-pcard h1 { margin:16px 0 10px; font-size:25px; font-weight:700; letter-spacing:-.01em }
.spx-pcard > p { margin:0 0 24px; color:var(--fg-2); font-size:14px; line-height:1.65 }
.spx-pfoot a { display:inline-flex; align-items:center; min-height:44px; padding:0 10px }
.spx-pfoot { padding-top:10px; margin-top:24px; border-top:1px solid var(--border); color:var(--fg-3); font-size:12.5px }
.spx-pnote { color:var(--fg-3); font-size:12.5px; margin:0 }
/* The main page footer's link, copied so the copyright line reads the same on both. */
.spx-pnote a { color:inherit; text-decoration:underline; text-decoration-color:var(--border-strong); text-underline-offset:3px }

/* Clerk's OTP field submits itself the moment the sixth digit lands, so the Continue button under
   it is a control nobody reaches — and it cost the boxes the height they needed. Hidden on that
   step ONLY: the same button carries the email address forward on the step before it.
   Stated here rather than in the appearance config because that config cannot see which step it
   is on; :has() can. The resend link is deliberately left alone. */
.cl-main:has(.cl-otpCodeField) .cl-formButtonPrimary { display:none }

/* Height, and ONLY height, has to be stated here rather than in the appearance config. Clerk puts
   two classes on each code box — cl-otpCodeFieldInput and cl-input — and its own `.cl-input` rule
   carries a height that ties with anything appearance generates and wins on source order. It was
   measured: font-size:30px from appearance applied, height:92px from the same object did not, and
   the boxes stayed at Clerk's 36px through three attempts to raise them.
   Two classes here beat one there. Width is left alone — six boxes divide the card between them
   and that arithmetic is already at its limit. */
.cl-otpCodeFieldInput.cl-input { height:92px; min-height:92px; padding:0 }


/* Mail shortcuts under the code boxes. Injected by partner-clerk.js and shown on that step only.
   [hidden] is restated because `display:flex` below would otherwise win over the attribute. */
.spx-mailjump { display:flex; justify-content:center; gap:8px; margin-top:18px }
.spx-mailjump[hidden] { display:none }
.spx-mailjump a {
  display:inline-flex; align-items:center; gap:8px;
  min-height:44px; padding:0 14px; border-radius:var(--r-pill);
  color:var(--fg-2); font-size:13px; font-weight:600; text-decoration:none;
  transition:background var(--t-pop) var(--ease-out), color var(--t-pop) var(--ease-out);
}
.spx-mailjump a:hover { background:rgba(255,255,255,.06); color:var(--fg-1) }
.spx-mailjump .ico { width:17px; height:17px }

/* visibility, not display: the page still lays out underneath, so revealing it costs no reflow
   and nothing jumps. See the spx-gate comment in partner-app.dc.html for what it guards. */
.spx-gate { visibility:hidden }

/* The pending card's waiting state. Same 52px ring and same .8s as the scan spinner on the home
   page — a second spinner with its own timing would read as a different product. */
.spx-spinner {
  width:52px; height:52px; margin:26px auto 0; border-radius:50%;
  border:4px solid var(--border); border-top-color:var(--magenta-500);
  animation:spx-spin .8s linear infinite;
}
.spx-loadnote { margin:16px 0 6px; color:var(--fg-3); font-size:13.5px }
@media (prefers-reduced-motion:reduce) { .spx-spinner { animation-duration:2.4s } }

/* ---------- guided application form (pending page, profile state) ----------
   One question per screen. The bar and the counter exist for the one thing a stepped form takes
   away from the partner that a stack of fields gives for free: knowing how much is left. Without
   them "Continue" is an unbounded promise, which is worse than six visible boxes. */
/* margin-top clears the status marker above it: the dot's own bottom margin is 4px, which left
   the track sitting close enough under the circle to read as a rule struck through it. */
.spx-wizbar { height:3px; border-radius:2px; background:var(--border); overflow:hidden; margin:16px 0 14px }
.spx-wizbar span {
  display:block; height:100%; width:0; border-radius:2px; background:var(--brand-fill);
  transition:width .32s var(--ease-out);
}
.spx-wizcount {
  margin:0; color:var(--fg-3); font-size:11.5px; font-weight:700;
  letter-spacing:.08em; text-transform:uppercase;
}
.spx-pcard section[data-state] > h1#pf-q { margin:10px 0 8px }
.spx-step input, .spx-step select {
  width:100%; padding:14px 16px; background:var(--bg-surface-2);
  border:1px solid var(--border); border-radius:10px; color:var(--fg-1); font-size:15px;
}
.spx-step input::placeholder { color:var(--fg-3) }
.spx-step .spx-mono { font-family:ui-monospace,SFMono-Regular,Menlo,monospace }
/* The custom multiselect replaces the <select> with its own button, which carries its own box. */
.spx-step select[data-ms] { border:0; padding:0; background:none }
.spx-wiznav { display:flex; align-items:center; gap:10px; margin-top:16px }
.spx-wiznav .spx-cta { flex:1; border-radius:10px }
/* display:inline-flex is doing the work here, not gap. `.spx-ghost` is a plain inline-block, so
   the arrow and the word sat on the text baseline and `gap` applied to nothing — the icon rode
   high and pressed against the B. Every other place that puts an icon in a ghost button states
   this too (see .spx-pagi .spx-ghost). */
.spx-wiznav .spx-ghost {
  flex:none; display:inline-flex; align-items:center; justify-content:center;
  gap:6px; border-radius:10px; padding:0 14px;
}

/* ---------- status marker + key/value block (pending page states) ---------- */
.spx-statusdot {
  width:54px; height:54px; margin:26px auto 4px; border-radius:50%;
  display:flex; align-items:center; justify-content:center;
}

.spx-statusdot--wait { background:rgba(246,199,5,.14);  border:1px solid rgba(246,199,5,.35);  color:var(--warn-ink) }
.spx-statusdot--ok   { background:rgba(9,174,154,.14);  border:1px solid rgba(9,174,154,.35);  color:#09AE9A }
.spx-statusdot--stop { background:rgba(216,52,131,.14); border:1px solid rgba(216,52,131,.38); color:var(--magenta-500) }
.spx-statusdot--info { background:var(--grey-100);      border:1px solid var(--border-strong); color:var(--fg-2) }

/* Was a bordered, filled box inside the auth card: a card inside a card, which is always
   wrong. Now it is separated by a rule and spacing, with no second surface. */
.spx-kv { padding:16px 0 0; margin-top:4px; text-align:left; font-size:14px; border-top:1px solid var(--divider) }
.spx-kv-label {
  font-size:11px; font-weight:700; letter-spacing:.07em; text-transform:uppercase;
  color:var(--fg-3); margin-bottom:5px;
}
/* Every label but the first opens a new pair, so it needs air above it. As a rule rather than an
   inline style on each one: a list that grows a row should not need the spacing re-remembered. */
.spx-kv .spx-kv-label ~ .spx-kv-label { margin-top:13px }
.spx-kv-mono { font-family:ui-monospace,SFMono-Regular,Menlo,monospace; font-size:13px }

/* Each state owns the card body, so its heading must not keep the card's default top margin. */
.spx-pcard section[data-state] > h1 { margin-top:14px }
.spx-pcard section[data-state] > p  { margin:0 0 22px; color:var(--fg-2); font-size:14px; line-height:1.65 }

/* ---------- icons ----------
   One family (Lucide), one geometry, referenced from a sprite. Stroke and size are set here,
   once, so no glyph can drift from its neighbours the way hand-drawn paths always do. */
.ico {
  width:1em; height:1em; flex:none;
  fill:none; stroke:currentColor; stroke-width:2;
  stroke-linecap:round; stroke-linejoin:round;
}
/* Brand marks (Gmail, Microsoft) carry their own fills and would be flattened by the stroke
   above — so both are reset here rather than per-glyph inside the sprite. */
.ico-brand { fill:currentColor; stroke:none }
.spx-oauth .ico, .spx-rt-report .ico, .spx-rt-btn .ico, .spx-menu button .ico { width:17px; height:17px }
.spx-iconbtn .ico, .spx-iconsm .ico { width:16px; height:16px }
.spx-pback .ico { width:14px; height:14px; color:var(--fg-3) }
.spx-search .ico { width:15px; height:15px }
.spx-ms-btn .ico { width:15px; height:15px; color:var(--fg-3); transition:transform var(--t-pop) var(--ease-out) }
.spx-statusdot .ico { width:24px; height:24px }
.spx-movers .ico, .spx-nudge .ico { width:19px; height:19px }
.spx-h-ico { width:16px; height:16px; color:var(--fg-3) }

/* ---------- press feedback ----------
   Every pressable element scales on :active. Without it the interface gives no sign it heard
   the press, which is the difference between "responsive" and "did that register?".
   0.97 is deliberate: subtle enough not to read as a bounce, visible enough to feel. */
:where(button, .spx-rt-report, .spx-rt-btn, .spx-oauth, .spx-cta, .spx-ghost, .spx-iconsm, .spx-iconbtn,
       .spx-avatar, .spx-ms-btn, .spx-toggle):active {
  transform: scale(0.97);
}
:where(button, .spx-rt-report, .spx-rt-btn, .spx-oauth, .spx-cta, .spx-ghost, .spx-iconsm, .spx-iconbtn,
       .spx-avatar, .spx-ms-btn, .spx-toggle) {
  transition: transform var(--t-press) var(--ease-out),
              background-color var(--t-press) ease,
              border-color var(--t-press) ease;
}

/* Touch devices fire :hover on tap, so an ungated hover state sticks after the finger lifts
   and reads as a stuck selection. Every hover rule below sits inside this gate. */
@media (hover: hover) and (pointer: fine) {
  .spx-ghost:hover      { background:var(--grey-100) }
  .spx-iconbtn:hover, .spx-phead:not(.spx-phead--bar) .spx-iconbtn:hover { background:var(--grey-100) }
  .spx-iconsm:hover    { background:var(--grey-100); color:var(--fg-1) }
  .spx-oauth:hover      { filter:brightness(.96) }
  .spx-cta:hover        { filter:brightness(1.06) }
  .spx-rt-report:hover, .spx-rt-btn:hover { background:var(--grey-200) }
  .spx-rt tbody tr:hover{ background:var(--grey-100) }
  .spx-ms-opt:hover     { background:var(--grey-100) }
  .spx-ms-clear:hover   { color:var(--fg-1) }
  .spx-menu button:hover, .spx-menu a:hover { background:var(--grey-100) }
  .spx-modal-nav:hover  { background:var(--grey-100) }
  .spx-nudge-x:hover    { color:var(--fg-1) }
  .spx-modal-x:hover    { color:var(--fg-1) }
  .spx-pback:hover      { background:var(--grey-100); text-decoration:none }
}

/* Icon buttons are 34px for visual weight but need a 44px hit area. Growing the box would
   wreck the header rhythm, so the target is extended past the visual bounds instead. */
.spx-iconbtn, .spx-avatar, .spx-iconsm, .spx-nudge-x, .spx-modal-x { position:relative }
:where(.spx-iconbtn, .spx-avatar, .spx-iconsm, .spx-nudge-x, .spx-modal-x)::after {
  content:""; position:absolute; left:50%; top:50%; transform:translate(-50%,-50%);
  width:44px; height:44px;
}

/* ---------- buttons ---------- */
.spx-oauth {
  display:flex; align-items:center; justify-content:center; gap:11px;
  width:100%; min-height:48px; padding:14px 18px; margin-bottom:10px;
  background:#fff; color:#1F2328; border:0; border-radius:10px;
  font-family:var(--font-sans); font-size:15px; font-weight:600; cursor:pointer;
}
.spx-oauth img, .spx-oauth > svg:not(.ico) { width:19px; height:19px }
/* Padding is horizontal only, and min-height alone sets the height. With 15px of vertical
   padding on top of a 46px min-height the button measured 49 — against a 46px input and a 44px
   dropdown sitting beside it in the same row. Three heights in one row is what made Scan look
   oversized; it was not wider than it should be, it was taller than its neighbours. */
.spx-cta {
  display:inline-flex; align-items:center; justify-content:center;
  padding:0 26px; min-height:46px; background:var(--brand-fill); color:#fff; border:0;
  border-radius:var(--r-pill); font-family:var(--font-sans); font-size:15px; font-weight:600; cursor:pointer;
}
.spx-ghost {
  padding:11px 16px; min-height:44px; background:transparent; color:var(--fg-1);
  border:1px solid var(--border-strong); border-radius:var(--r-pill);
  font-family:var(--font-sans); font-size:13px; font-weight:600; cursor:pointer;
}
.spx-ok { background:var(--ok-fill); color:#fff; border:0 }
/* Square icon button: matches the 40px language button already in the main page header. */
.spx-iconbtn {
  display:flex; align-items:center; justify-content:center; width:34px; height:34px; padding:0;
  background:transparent; color:var(--fg-1); border:1px solid var(--border-strong);
  border-radius:50%; cursor:pointer; flex:none;
}
.spx-iconbtn svg { width:16px; height:16px }
.spx-divider { display:flex; align-items:center; gap:12px; margin:20px 0; color:var(--fg-3); font-size:12px }
.spx-divider::before, .spx-divider::after { content:""; flex:1; height:1px; background:var(--border) }

/* ---------- account menu ----------
   Stand-in for Clerk's <UserButton userProfileMode="modal">, which ships the avatar, this
   dropdown, and the "Manage account" modal containing the Security tab: 2FA enrolment,
   password change, backup codes. That is why there is no /partner/settings route: building
   one would mean rebuilding what the component already does. */
.spx-acct { position:relative }
.spx-avatar {
  width:34px; height:34px; border-radius:50%; border:1px solid var(--border-strong);
  background:var(--brand-fill); color:#fff; cursor:pointer;
  font-family:var(--font-sans); font-size:13px; font-weight:700;
  display:flex; align-items:center; justify-content:center;
  /* overflow, so a photo is clipped to the circle rather than squaring off the corners.
     padding:0 because the button is 34px and the image fills it edge to edge. */
  overflow:hidden; padding:0;
}
/* Painted by spxAvatar() when the Clerk user actually has a photo. object-fit, because a
   Google avatar is square but a Microsoft one need not be, and `width/height:100%` alone
   would stretch a non-square source into the circle. */
.spx-avatar-img { width:100%; height:100%; border-radius:50%; object-fit:cover; display:block }
.spx-menu {
  position:absolute; top:44px; right:0; width:246px; padding:6px; text-align:left;
  transform-origin:top right;
  transition:opacity var(--t-pop) var(--ease-out), transform var(--t-pop) var(--ease-out);
  background:var(--bg-surface); border:1px solid var(--border);
  border-radius:12px; box-shadow:0 14px 36px rgba(2,10,26,.44); z-index:60;
}
.spx-menu[hidden] { display:block; opacity:0; transform:scale(.97) translateY(-4px); pointer-events:none; visibility:hidden }
.spx-menu-id { padding:11px 12px 12px; border-bottom:1px solid var(--divider); margin-bottom:6px }
.spx-menu-id b { display:block; font-size:13.5px }
.spx-menu-id span { display:block; color:var(--fg-3); font-size:12px; margin-top:2px }
/* Sign out is an <a>, not a <button>: it goes to a page, so it should be middle-clickable and
   show its destination in the status bar like every other navigation on the site. */
.spx-menu button, .spx-menu a {
  display:flex; align-items:center; gap:10px; width:100%; min-height:46px; padding:11px 12px;
  background:transparent; border:0; border-radius:9px; cursor:pointer;
  color:var(--fg-1); font-family:var(--font-sans); font-size:13.5px; text-align:left;
  text-decoration:none; box-sizing:border-box;
}
.spx-menu button .ico, .spx-menu a .ico { color:var(--fg-3) }

/* ---------- scan row ---------- */
.spx-scanrow { display:flex; align-items:center; gap:10px; margin:18px 0 0 }
.spx-scanrow input, .spx-scanrow select {
  min-height:46px; padding:13px 16px; background:var(--bg-surface-2); border:1px solid var(--border);
  border-radius:var(--r-pill); color:var(--fg-1); font-size:14.5px; font-family:var(--font-sans);
}
/* The domain box gets the same clear button the history search has. It is the field on this page
   people actually retype — scan, read, scan someone else — and selecting a domain to delete it is
   the chore the × exists to remove. The wrapper is what the flex row now sizes, so the input can
   fill it and the button can sit inside on the right. */
.spx-scanbox { position:relative; display:flex; flex:1 1 40%; min-width:0 }
.spx-scanbox input { flex:1; min-width:0; padding-right:44px }
.spx-scanbox .spx-search-x { right:12px }
.spx-scanrow > input  { flex:1 1 40% }
.spx-scanrow select { flex:0 1 190px; cursor:pointer }
.spx-scanrow input:focus, .spx-scanrow select:focus { border-color:var(--magenta-500) }

/* ---------- analytics: aggregates only, nothing new persisted ---------- */
.spx-stats { display:grid; grid-template-columns:repeat(3,minmax(120px,170px)) minmax(260px,1fr); margin:28px 0 0 }
/* The wide tile takes the LAST column, whatever the count of tiles before it. Without this it
   flows into whichever cell comes next, and the Suspended tile beside it is `hidden` — so it was
   landing in column 3, a minmax(120px,170px) track, where "Scans today across all partners"
   broke onto three lines and the bar under it had 170px to live in. Auto-placement was reading
   the number of VISIBLE tiles, which is not the number the track list was written for. */
.spx-stat--wide { grid-column:-2 / -1 }
/* The denominator is muted and small, so "41" carries the tile and "of 150" explains it without
   competing. Same treatment .spx-quotacell gives the same shape one screen down, which is what
   makes the two read as one idea rather than two ways of writing a fraction. */
.spx-stat--wide .spx-stat-n span { color:var(--fg-3); font-weight:500; font-size:15px }
/* Full width and 8px, not the 170px/5px the bar has elsewhere: here it has the widest cell on
   the strip to fill, and at 5px across that distance it read as a divider between the tiles
   rather than as a measure of anything. */
.spx-stat--wide .spx-quota-track { width:100%; height:8px; margin-top:12px }
/* Four metrics were four cards in a grid. Cards are the lazy answer: nothing here needs
   elevation, and a row of identical tiles is the most-generated shape in AI dashboards.
   Now it is one bordered strip divided by hairlines: the numbers do the work. */
.spx-stat { padding:16px 20px; border-left:1px solid var(--divider) }
.spx-stat:first-child { border-left:0; padding-left:0 }
.spx-stat-n { font-size:27px; font-weight:700; line-height:1.15 }
.spx-stat-l { color:var(--fg-3); font-size:11.5px; font-weight:600; letter-spacing:.05em; text-transform:uppercase; margin-top:5px }
/* No `overflow:hidden` here. It was rounding the ends of the bar, and it was also clipping the
   segment tooltips to a 9px-tall box — which is why hovering a segment lit it up and showed
   nothing. Each segment rounds itself instead; with a 2px gap they were never one continuous
   shape anyway. */
.spx-mix { display:flex; height:9px; gap:2px }
.spx-mix span { display:block; border-radius:99px; position:relative }
.spx-mixkey { display:flex; flex-wrap:wrap; gap:12px; margin-top:10px; color:var(--fg-2); font-size:12px }
.spx-mixkey span { display:flex; align-items:center; gap:6px }
.spx-mixkey i { width:8px; height:8px; border-radius:50%; flex:none }

.spx-movers {
  display:flex; align-items:flex-start; gap:12px; margin-top:12px; padding:14px 18px;
  background:rgba(216,52,131,.07); border:1px solid rgba(216,52,131,.24); border-radius:var(--r-lg);
}
.spx-movers .ico { color:var(--brand-ink); margin-top:1px }
.spx-movers strong { display:block; font-size:13.5px }
.spx-movers span { display:block; color:var(--fg-2); font-size:12.5px; margin-top:3px }

.spx-nudge {
  display:flex; align-items:center; gap:13px; margin-top:14px; padding:13px 16px;
  background:rgba(246,199,5,.09); border:1px solid rgba(246,199,5,.32); border-radius:var(--r-lg);
}
.spx-nudge .ico { color:var(--warn-ink) }
.spx-nudge > div { flex:1; min-width:0 }
.spx-nudge strong { display:block; font-size:13.5px }
.spx-nudge span { display:block; color:var(--fg-2); font-size:12.5px; margin-top:2px }
.spx-nudge-x {
  background:transparent; border:0; color:var(--fg-3); font-size:19px; line-height:1;
  padding:2px 6px; cursor:pointer; flex:none;
}
@media (max-width:620px) { .spx-nudge { flex-wrap:wrap } .spx-nudge > div { flex:1 1 100% } }

/* Named for what the change MEANS, not which way the number went. `--up`/`--down` was a trap:
   on an exposure score, up is bad. Every reader of the old class had to hold that inversion in
   their head, and the mock had ▲ on rows that had improved. */
.spx-delta { font-size:12px; font-weight:700 }
.spx-delta--worse  { color:var(--band-veryhigh) }
.spx-delta--better { color:var(--band-verylow) }

/* ---------- multi-select ----------
   The native <select> stays in the DOM as the source of truth and is hidden here, not
   removed: the filters keep working if partner-select.js never runs. */
.spx-ms-native { position:absolute; width:1px; height:1px; opacity:0; pointer-events:none }
.spx-ms { position:relative; flex:0 0 auto }
/* In a form field the control is the field, so it fills it — `flex:0 0 auto` above is sized for
   the scan bar, where these sit in a row beside a domain box. Without this the row-edit modal
   had a 150px dropdown under a full-width label and input. */
.spx-field .spx-ms { display:block; width:100% }
.spx-field .spx-ms-btn { width:100% }
.spx-ms-btn {
  display:flex; align-items:center; gap:9px; min-height:46px; padding:0 15px; min-width:150px;
  background:var(--bg-surface-2); border:1px solid var(--border); border-radius:12px;
  color:var(--fg-2); font-family:var(--font-sans); font-size:13.5px; font-weight:600; cursor:pointer;
}
.spx-ms-btn > span { flex:1; text-align:left; white-space:nowrap; overflow:hidden; text-overflow:ellipsis }
.spx-ms.is-open .spx-ms-btn { border-color:var(--magenta-500) }
.spx-ms.is-open .spx-ms-btn .ico { transform:rotate(180deg) }
.spx-ms.has-value .spx-ms-btn { color:var(--fg-1); border-color:var(--border-strong) }
/* The one dropdown that lives inside a form rather than a filter bar. It sits between two text
   inputs, so it borrows their geometry instead of the filter bar's taller, rounder pill. */
.spx-pcard .spx-ms { display:block; margin-bottom:10px }
.spx-pcard .spx-ms-btn {
  width:100%; min-width:0; min-height:0; padding:14px 16px;
  border-radius:10px; font-size:14px; font-weight:500;
}
/* No `.spx-pcard .spx-ms-menu { width:100% }` any more: the menu is not inside the card, so
   that selector matched nothing. place() sets min-width from the button's measured width, which
   is what the rule was reaching for anyway and works wherever the control happens to sit. */

/* The menu is a child of <body>, not of .spx-ms — partner-select.js moves it there so that no
   scrolling ancestor can clip it, which is what .spx-modal-body was doing. Every selector below
   therefore targets the menu directly; anything written as `.spx-ms … .spx-ms-menu` would match
   nothing. position and offsets are set inline at open time. */
.spx-ms-menu {
  /* 210, above .spx-modal's 200. It used to be 70, which was enough while the menu lived inside
     the control and inherited its stacking context; as a sibling of the modal under <body> it
     competes with it directly, and 70 put the open menu BEHIND the overlay — measured with
     elementFromPoint, which returned .spx-modal where the menu should have been. */
  position:fixed; top:0; left:0; z-index:210;
  opacity:0; transform:scale(.97) translateY(-4px); transform-origin:top left;
  pointer-events:none; visibility:hidden;
  transition:opacity var(--t-pop) var(--ease-out),
             transform var(--t-pop) var(--ease-out),
             visibility 0s linear var(--t-pop);
  min-width:100%; max-height:290px; overflow:auto; padding:6px;
  background:var(--bg-surface); border:1px solid var(--border);
  border-radius:12px; box-shadow:var(--shadow-lg);
}
.spx-ms-menu.is-open {
  opacity:1; transform:scale(1) translateY(0); pointer-events:auto; visibility:visible;
  transition:opacity var(--t-pop) var(--ease-out), transform var(--t-pop) var(--ease-out);
}
/* Sticky, because the list it filters is the thing that scrolls: a search box that leaves the
   moment you scroll to check a result is one you have to scroll back up to correct. The negative
   offset cancels the menu's own 6px padding so nothing shows through above it. */
.spx-ms-search {
  position:sticky; top:-6px; z-index:1; display:block;
  width:100%; box-sizing:border-box; margin:-6px -6px 6px; padding:11px 12px; width:calc(100% + 12px);
  background:var(--bg-surface); border:0; border-bottom:1px solid var(--border);
  color:var(--fg-1); font-family:var(--font-sans); font-size:13.5px;
}
.spx-ms-search:focus { outline:none; border-bottom-color:var(--magenta-500) }
.spx-ms-search::placeholder { color:var(--fg-3) }
.spx-ms-empty { margin:0; padding:14px 10px; color:var(--fg-3); font-size:13px; text-align:center }
.spx-ms-empty[hidden] { display:none }
.spx-ms-opt[hidden] { display:none }
.spx-ms-opt {
  display:flex; align-items:center; gap:10px; width:100%; min-height:46px; padding:10px;
  background:transparent; border:0; border-radius:8px; cursor:pointer;
  color:var(--fg-1); font-family:var(--font-sans); font-size:13.5px; text-align:left; white-space:nowrap;
}
.spx-ms-box {
  width:16px; height:16px; flex:none; border-radius:5px;
  border:1.5px solid var(--border-strong); background:transparent;
}
.spx-ms-opt.is-on .spx-ms-box {
  background:var(--magenta-500); border-color:var(--magenta-500);
  background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='3.4' stroke-linecap='round' stroke-linejoin='round'><path d='M20 6L9 17l-5-5'/></svg>");
  background-size:11px; background-position:center; background-repeat:no-repeat;
}
/* single-select variant: a tick on the right instead of a checkbox on the left.
   Rows are tighter than the multi-select's. There the 46px row is carrying a checkbox and is a
   thing you click repeatedly; here it is a four-item list you touch once, and 46px spread over
   four rows made a menu taller than the table footer it belongs to. */
.spx-ms-tick { width:15px; height:15px; margin-left:auto; opacity:0; color:var(--magenta-500) }
.spx-ms-opt.is-on .spx-ms-tick { opacity:1 }
/* The button is still inside .spx-ms, so this one keeps the control-level class. The two below
   style the OPTIONS, which now live in the portaled menu — written against .spx-ms--one they
   silently matched nothing and every single-select row went back to the taller multi geometry. */
.spx-ms--one .spx-ms-btn { min-width:0 }
.spx-ms-menu--one .spx-ms-opt { min-height:36px; padding:7px 10px; gap:12px; font-size:13px }
.spx-ms-menu--one .spx-ms-opt > span:first-child { flex:1 }
.spx-ms-menu--one { min-width:150px; padding:5px }

/* Opens upward: same menu, anchored to the bottom edge, and the pop scales from the bottom so
   it grows out of the button rather than into it. */
/* Upward. Only the origin, so the pop grows from the edge nearest the button — the offsets are
   set inline by place(), which decides the direction from the room actually on screen. */
.spx-ms-menu.is-up { transform-origin:bottom left }

.spx-ms-clear {
  width:100%; min-height:46px; margin-top:4px; padding:8px; border:0; border-top:1px solid var(--divider);
  background:transparent; color:var(--fg-3); font-family:var(--font-sans); font-size:12.5px; cursor:pointer;
}

/* ---------- filter bar ---------- */
.spx-filters { display:flex; gap:10px; flex-wrap:wrap; align-items:center; margin:0 0 14px }
.spx-search { position:relative; flex:1 1 260px; min-width:180px }
.spx-search input {
  width:100%; min-height:46px; padding:11px 15px 11px 40px;
  background:var(--bg-surface-2); border:1px solid var(--border); border-radius:12px;
  color:var(--fg-1); font-size:13.5px;
}
.spx-search input:focus { border-color:var(--magenta-500) }
.spx-search > .ico { position:absolute; left:14px; top:50%; transform:translateY(-50%); color:var(--fg-3) }
/* Clearing a filter by selecting the text and pressing backspace is a chore the box should do
   for you. Right-hand end, only once there is something to clear — an always-on × is a control
   that does nothing most of the time. */
.spx-search input { padding-right:38px }
.spx-search-x {
  position:absolute; right:8px; top:50%; transform:translateY(-50%);
  display:flex; align-items:center; justify-content:center; width:26px; height:26px; padding:0;
  background:transparent; border:0; border-radius:50%; color:var(--fg-3); cursor:pointer;
  transition:background var(--t-press) ease, color var(--t-press) ease;
}
.spx-search-x:hover { background:var(--bg-surface); color:var(--fg-1) }
.spx-search-x .ico { width:14px; height:14px }
.spx-toggle {
  display:flex; align-items:center; gap:9px; min-height:46px; padding:0 15px;
  background:var(--bg-surface-2); border:1px solid var(--border); border-radius:12px;
  color:var(--fg-2); font-family:var(--font-sans); font-size:13.5px; font-weight:600; cursor:pointer;
}
.spx-toggle .spx-ms-box { pointer-events:none }
.spx-toggle.is-on { color:var(--fg-1); border-color:var(--border-strong) }
.spx-toggle.is-on .spx-ms-box {
  background:var(--magenta-500); border-color:var(--magenta-500);
  background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='3.4' stroke-linecap='round' stroke-linejoin='round'><path d='M20 6L9 17l-5-5'/></svg>");
  background-size:11px; background-position:center; background-repeat:no-repeat;
}

/* ---------- results table ----------
   Shaped after SiberRadar's radar table (SiberRadar/Design/SiberRadar.dc.html, the
   .sr-scroll / .sr-table / .sr-actcol / .sr-pagi set), because the two products show the same
   rows to the same people and there is no reason for them to disagree about what a table is.
   What was copied: a header row on its own tinted surface rather than floating on the panel,
   every column a sort control with the arrow in brand magenta, the action column pinned right
   behind a hairline, the domain as a real link chip, and a pagination footer. */
.spx-scroll { overflow-x:auto; border-radius:var(--r-lg) }
/* Thirteen columns wanted 1420px inside a 1214px panel, so the table scrolled sideways at
   1440 and the last two columns were only reachable by dragging. Rather than let it scroll,
   every column was measured and the slack taken out: 7px cell padding instead of 10, the rank
   column dropped, and hard caps on the three text columns that were sizing themselves to their
   longest value. The floor below stays, because under ~1150px there is no slack left to take
   and scrolling is the honest answer. */
.spx-rt { width:100%; border-collapse:collapse; min-width:1150px; table-layout:fixed }

/* The header used to be bare text over the panel, which is why it read as ragged: nothing
   told the eye where the header stopped and the data began except a single hairline.
   SiberRadar puts it on --sr-surface-2 with the rule under it. Same here. */
.spx-rt thead th {
  padding:0; text-align:left; white-space:nowrap;
  background:var(--bg-surface-2); border-bottom:1px solid var(--border);
}
.spx-rt thead th:first-child { border-top-left-radius:var(--r-lg) }

/* Explicit widths, because table-layout:fixed needs them and because "let the browser decide"
   is what produced an 87px Change column next to a 102px Last leak. Sums to 1150. */
.spx-rt th.selcol, .spx-rt td.selcol { width:38px; padding:11px 0 11px 13px }
.spx-rt col-company { }
.spx-rt th:nth-child(2)  { width:198px }   /* Company  */
.spx-rt th:nth-child(3)  { width:94px  }   /* Industry */
.spx-rt th:nth-child(4)  { width:74px  }   /* Score    */
.spx-rt th:nth-child(5)  { width:120px }   /* Band — gauge + the longest label, on one line */
.spx-rt th:nth-child(6)  { width:66px  }   /* Δ        */
.spx-rt th:nth-child(7),
.spx-rt th:nth-child(8)  { width:70px  }   /* People / Logins */
.spx-rt th:nth-child(9)  { width:74px  }   /* Devices  */
.spx-rt th:nth-child(10) { width:80px  }   /* Last leak */
/* 92px, not 84: this column holds "Scanning…" while a re-scan runs, and at 84 that word ran
   straight into the hairline pinning the action column. The 8px came off Band, which had it. */
.spx-rt th:nth-child(11) { width:92px  }   /* Scanned   */
.spx-rt th.act           { width:190px }
/* The action cluster is the one thing that cannot shrink to fit its column — three controls
   with a word in the middle — so it is measured to the column rather than the other way round. */
/* The action cluster was the heaviest thing in the row: a 13px semibold word and three 32px
   boxes, next to 12.5px dim dates. It read as the most important column, which it is not —
   it is the one you use after you have decided, from the numbers. Dialled back to match. */
.spx-rt-act { gap:5px }
/* Report's size lives in the base `.spx-rt-report` rule further down, NOT here. This block used
   to set it and never applied: the base rule sits later in the file at equal specificity and won
   every time. The icon buttons DID shrink, because `.spx-rt .spx-iconsm` outranks the bare
   class — which is how the cluster ended up with 28px icons beside a 44px button. */
.spx-rt .spx-iconsm { width:28px; height:28px; border-radius:8px }
.spx-rt .spx-iconsm .ico { width:14px; height:14px }
.spx-rt-ind { overflow:hidden; text-overflow:ellipsis }
.spx-check {
  accent-color:var(--magenta-500); width:15px; height:15px; display:block; cursor:pointer;
  transition:transform var(--t-press) var(--ease-out);
}
.spx-check:active { transform:scale(.86) }
/* The bulk bar. Appears only with a selection, and its job is to print the COST before the
   press — see the note in partner-app.dc.html for why that sentence is the whole reason this
   control was allowed back. Uses `show()` from partner-scan.js, so display is set inline and
   [hidden] must not be relied on to hide it. */
.spx-bulkbar {
  display:flex; align-items:center; gap:12px; flex-wrap:wrap;
  margin:0 0 14px; padding:11px 14px;
  background:var(--accent-soft); border:1px solid var(--border-strong); border-radius:var(--r-md);
  animation:spx-rise .2s var(--ease-out) backwards;
}
.spx-bulkbar[hidden] { display:none }
.spx-bulkbar-n { color:var(--fg-1); font-size:13px; font-weight:600 }
.spx-bulkbar-n b { font-weight:700 }
.spx-bulkbar-cost { color:var(--fg-2); font-size:12.5px }
/* Pushes the two buttons to the far end without a wrapper element. */
.spx-bulkbar-sp { flex:1 1 auto }
.spx-bulkbar .spx-cta { padding:9px 18px; font-size:13.5px }
.spx-bulkbar .spx-ghost { font-size:13px }
/* Destructive, and it says so only once it is armed. A permanently red button in a bar you open
   by ticking a box reads as a warning about the selection rather than about one control. */
/* `.spx-spin` had NO rule in this file, only the @keyframes further down. Four places already
   put the class on a loader icon — the bulk bar, the View button, and the admin page's radar
   refresh — so every one of those "spinners" has been sitting perfectly still. Found 8 Aug 2026
   while adding the fifth. One rule fixes all of them.
   `.is-spinning .ico` covers the refresh button, whose icon spins while the button itself does
   not, since a rotating square button is a different thing entirely. */
.spx-spin,
#history-refresh.is-spinning .ico { animation:spx-spin 1s linear infinite }
@media (prefers-reduced-motion:reduce) {
  .spx-spin, #history-refresh.is-spinning .ico { animation-duration:2.4s }
}

/* The row's delete. Neutral at rest — a permanently red icon in every row would make the whole
   table read as a warning — and unmistakable on hover, which is the only state it needs now that
   the confirmation is a dialog rather than a second press on this button. */
.spx-rt-del:hover { color:var(--band-veryhigh); border-color:var(--band-veryhigh) }
.spx-danger { color:var(--band-veryhigh) }
.spx-danger:hover { border-color:var(--band-veryhigh) }
/* .spx-cta--danger is NOT set here. It already exists further down this file, and a second rule
   at equal specificity would have been decided by source order — the exact trap .spx-rt-act and
   .cl-input each fell into. Changed in place at its one definition instead. */

/* A checked row is a row about to be acted on, and it should look it. */
.spx-rt tbody tr.is-sel td { background:var(--accent-soft) }
.spx-rt tbody tr.is-sel td.act { background:color-mix(in srgb, var(--accent-soft) 60%, var(--bg-surface)) }

/* The whole header cell is the sort target, not a small label inside it: a 10px word is a
   miserable thing to hit, and the cell is already the column's name. */
.spx-th {
  display:flex; align-items:center; gap:3px; width:100%; min-height:42px; padding:12px 7px;
  background:transparent; border:0; cursor:pointer;
  color:var(--fg-3); font-family:var(--font-sans); font-size:10.5px; font-weight:700;
  letter-spacing:.08em; text-transform:uppercase; text-align:inherit;
}
/* ---------- column alignment, set once for header and cell together ----------
   The header used to be left-aligned everywhere while half the cells under it were right- or
   centre-aligned, so nine of the twelve columns had their label sitting over empty space at
   the far end of their own numbers. One rule per column, applied to <th> and <td> at the same
   time, so the two cannot drift again:
     Company, Industry            left    — text you read from its first letter
     Δ, People, Logins, Devices,
     Last leak, Scanned           centre  — see the reversal below
     Score                        centre  — a number and its bar, one object
     Band                         centre  — a gauge and a word, one object
     Action                       centre  — a control cluster, not a value

   Δ and Scanned were RIGHT until 8 Aug 2026, on the argument that numbers compare down a
   column. Reversed by the product owner, and the argument did not survive the actual columns:
   Δ holds two digits behind an arrow and Scanned holds "2h ago", so neither is a figure anyone
   reads down. What it bought instead was the history table being the only one of the three
   with two right-aligned columns among four centred ones — .spx-rt--partners and .spx-rt--mini
   had both already been centred, one at a time, for exactly this reason. Centring the base is
   what makes the three tables agree; the two modifier blocks below now repeat it rather than
   correct it, and are kept because they also carry the first-column and .act rules.           */
.spx-rt th.num  .spx-th { justify-content:center; flex-direction:row }
.spx-rt th.sc   .spx-th { justify-content:center }
.spx-rt th.bd   .spx-th { justify-content:center }
/* A centred header is a flex row holding the label AND the sort arrow, and the arrow reserves
   its 12px at all times (see the note below it). justify-content:center therefore centres the
   PAIR, which leaves the label itself sitting 7.5px left of true centre — invisible on "SCANNED"
   and obvious on a one-character "Δ", which is how it was spotted.
   A spacer mirroring the arrow on the other side puts the label back in the middle. Width and
   the 3px flex gap match .spx-th-ar exactly, so the two cancel. */
.spx-rt th.num .spx-th::before,
.spx-rt th.sc  .spx-th::before,
.spx-rt th.bd  .spx-th::before { content:""; width:12px; flex:none }
.spx-rt th.act, .spx-rt td.act { text-align:center }
.spx-rt th.sc, .spx-rt td.sc { text-align:center }
.spx-rt th.bd, .spx-rt td.bd { text-align:center }
.spx-rt-band { justify-content:center }
/* The score bar sits under the middle of the number, same as the number's own centre. */
.spx-rt-bar { margin-left:auto; margin-right:auto }
/* `.spx-rt-act { justify-content:center }` used to sit here and never applied: the base rule at
   the bottom of this file is also one class, so it tied on specificity and won on source order
   with flex-end — which is why the Action cluster sat against the right edge under a centred
   ACTION header. The centring now lives in that one rule instead, so there is only one place
   this can be set. Third time this file has been bitten by an equal-specificity later rule. */
/* The arrow holds its space at all times. Showing it only on the sorted column shifts every
   other header sideways the moment you sort, and the whole row appears to twitch. */
.spx-th-ar { width:12px; height:12px; opacity:0; transition:opacity var(--t-press) ease, transform var(--t-press) var(--ease-out) }
.spx-th[data-dir] { color:var(--fg-1) }
.spx-th[data-dir] .spx-th-ar { opacity:1; color:var(--magenta-500) }
.spx-th[data-dir="desc"] .spx-th-ar { transform:rotate(180deg) }

.spx-rt td { padding:11px 7px; border-bottom:1px solid var(--divider); vertical-align:middle; overflow:hidden }
.spx-rt tbody tr:last-child td { border-bottom:0 }

/* The action column had no left edge, so its buttons floated against the numbers beside them.
   SiberRadar pins it: sticky right, its own surface, one hairline. Sticky matters below
   ~1050px, where the table scrolls and the actions must stay reachable. */
.spx-rt th.act, .spx-rt td.act {
  position:sticky; right:0; z-index:2;
  padding:11px 14px; border-left:1px solid var(--border); background:var(--bg-surface);
}
.spx-rt th.act {
  background:var(--bg-surface-2); border-top-right-radius:var(--r-lg);
  color:var(--fg-3); font-size:10.5px; font-weight:700; letter-spacing:.08em; text-transform:uppercase;
  padding:12px 14px;
}
@media (hover: hover) and (pointer: fine) {
  .spx-rt tbody tr:hover td.act { background:var(--bg-surface-2) }
  .spx-th:hover { color:var(--fg-1) }
  .spx-th:hover .spx-th-ar { opacity:.45 }
}
.spx-rt-num { color:var(--fg-3); font-size:13px; font-variant-numeric:tabular-nums }
/* Company was the widest column at 267px, and the only reason was nowrap: "Bank Pembangunan
   Daerah Jatim" was forced onto one line and dragged the whole table 46px past its container.
   Wrapping is the right answer here, not truncation: the company name is what the partner
   scans the row for, so hiding half of it behind an ellipsis defeats the column. */
/* Truncate rather than wrap now that the column has a fixed width: a wrapped label pushed the
   row to two lines and the whole table to a different rhythm depending on which page you were
   on. The tooltip carries the full text, which is what the tooltip is there for. */
/* 13.5px and a 6px gap under it. At 14px with a 4px gap the label and the domain chip below it
   were two dense objects 4px apart in a 67px row — they read as one crushed block rather than
   a name with its address underneath. */
.spx-rt-co {
  font-weight:700; font-size:13.5px; line-height:1.35; margin-bottom:6px;
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
}
.spx-rt-co[hidden] { display:none }
.spx-rt-idline { display:flex; align-items:center; gap:6px; min-width:0 }
.spx-rt-idline .spx-rt-dom { min-width:0; overflow:hidden }
.spx-rt-idline .spx-rt-dom span { overflow:hidden; text-overflow:ellipsis }
/* The domain is now a link, which it should have been from the start: it is the one thing in
   the row that names something the partner can go and look at. Copied from SiberRadar's
   domain chip, including the magenta globe and the border lighting up on hover. */
.spx-rt-dom {
  display:inline-flex; align-items:center; gap:6px; padding:3px 9px;
  background:var(--grey-100); border:1px solid var(--border); border-radius:var(--r-pill);
  color:var(--fg-2); font-size:11.5px; font-family:ui-monospace,SFMono-Regular,Menlo,monospace;
  text-decoration:none;
}
.spx-rt-dom svg { width:11px; height:11px; color:var(--magenta-500) }
@media (hover: hover) and (pointer: fine) {
  .spx-rt-dom:hover { border-color:var(--magenta-500); color:var(--fg-1); text-decoration:none }
}
.spx-cc {
  display:inline-block; padding:2px 7px; border-radius:6px; background:var(--grey-200);
  color:var(--fg-2); font-size:10.5px; font-weight:700; letter-spacing:.04em;
}
.spx-rt-ind { color:var(--fg-2); font-size:13px; white-space:nowrap }
/* The country chip leads the identity line, left of the domain chip — the comp's shape, where
   an "ID" chip sits immediately before what names the company. */
.spx-rt-idline .spx-cc { flex:none }
.spx-rt-score { font-size:21px; font-weight:700; font-variant-numeric:tabular-nums; line-height:1.1 }
.spx-rt-bar { height:3px; border-radius:99px; margin-top:6px; background:currentColor; max-width:72px }
.spx-rt-band { display:flex; align-items:center; gap:8px }
/* 14px, not 13px, and the extra pixel is load-bearing. On the dark canvas the SiberRadar
   veryHigh (#D83483) measures ~3.7:1, which passes WCAG AA for LARGE text (3:1) and fails it
   for small (4.5:1). 14px bold is exactly where WCAG draws the large-text line, so the band
   keeps SiberRadar's colour unchanged and the label is still legible. */
.spx-rt-band b { line-height:1.25; font-size:14px; font-weight:700; white-space:nowrap }
/* Centred since 8 Aug 2026 — see the reversal note in the column-alignment block above. The
   digit-stacking argument that put this at `right` still holds for a column of raw counts, and
   this selector no longer feeds one: the only bare .spx-rt on the site is the history table,
   whose two .num columns are a two-digit delta and a relative time. tabular-nums on .spx-rt-n
   keeps the digits themselves from jittering, which is the half that actually mattered. */
.spx-rt th.num, .spx-rt td.num { text-align:center }
.spx-rt-n { font-variant-numeric:tabular-nums; font-size:13.5px }
.spx-rt-dim { color:var(--fg-3); font-size:12.5px; white-space:nowrap }
/* The one place the Action cluster's alignment is set — see the note further up. Centred, so it
   sits under its own centred header instead of against the cell's right edge. */
.spx-rt-act { display:flex; align-items:center; gap:6px; justify-content:center }
.spx-iconsm {
  display:flex; align-items:center; justify-content:center; width:32px; height:32px; padding:0;
  background:transparent; border:1px solid var(--border-strong); border-radius:9px;
  color:var(--fg-2); cursor:pointer; flex:none;
}

/* 28px, the same box as the two icon buttons beside it. Report is a peer of Rescan, not its
   heading, and three controls on one line that do not share a height read as two groups. */
.spx-rt-report, .spx-rt-btn {
  display:inline-flex; align-items:center; gap:6px; min-height:28px; padding:0 10px;
  background:var(--grey-100); border:1px solid var(--border-strong); border-radius:8px;
  color:var(--fg-1); font-family:var(--font-sans); font-size:12px; font-weight:600; cursor:pointer; white-space:nowrap;
}
/* `.ico`, not `svg`: the glyph carries the class, and the generic `.spx-rt-report .ico` rule up
   in the icon-sizing block outranks an element selector — so `svg` here sized nothing. */
.spx-rt-report .ico, .spx-rt-btn .ico { width:13px; height:13px }

/* ---------- bulk selection bar ----------
   Same shape as SiberRadar's: count and cost on the left, the two selection shortcuts and the
   verb on the right, and the whole bar only exists while something is selected. It slides in
   rather than appearing, because a bar that pops into existence above a table shoves the table
   down by 60px with no explanation of what moved. */
@keyframes spx-bulkin { from { opacity:0; transform:translateY(-8px) } }
.spx-bulk {
  display:flex; align-items:center; gap:12px; flex-wrap:wrap; margin:0 0 14px; padding:13px 16px;
  background:rgba(216,52,131,.09); border:1px solid rgba(216,52,131,.32); border-radius:var(--r-lg);
  animation:spx-bulkin .22s var(--ease-out) both;
}
.spx-bulk[hidden] { display:none }
.spx-bulk-n { display:flex; align-items:center; gap:8px; font-size:13.5px; font-weight:700 }
.spx-bulk-n::before {
  content:""; width:7px; height:7px; border-radius:50%; background:var(--magenta-500);
  animation:spx-bulkdot 1.8s ease-in-out infinite;
}
@keyframes spx-bulkdot { 0%,100% { opacity:1 } 50% { opacity:.3 } }
.spx-bulk-note { color:var(--fg-2); font-size:12.5px }
.spx-bulk-note.is-over { color:var(--band-veryhigh); font-weight:600 }

/* ---------- quota bar ----------
   Was three inline styles in the page. Given a name so it can carry a tooltip and animate its
   own fill on load: a bar that is already full when the page paints tells you nothing about
   which direction it is travelling. */
.spx-quota { display:flex; align-items:center; gap:9px; margin-top:12px; width:max-content; color:var(--fg-3); font-size:12.5px }
.spx-quota-track { flex:0 0 170px; height:5px; background:var(--grey-200); border-radius:99px; overflow:hidden }
.spx-quota-fill { height:100%; background:var(--teal-500); border-radius:99px; transform-origin:left; animation:spx-quotafill .8s var(--ease-out) .25s backwards }
@keyframes spx-quotafill { from { transform:scaleX(0) } }

/* The risk mix segments are now hover targets, so they need to say so and react. */
.spx-mix span { transition:filter var(--t-press) ease, transform var(--t-press) var(--ease-out); transform-origin:center bottom }
.spx-mix span:hover, .spx-mix span:focus-visible { filter:brightness(1.25); transform:scaleY(1.5); outline:none }
.spx-mix { animation:spx-mixgrow .7s var(--ease-out) .2s backwards }
@keyframes spx-mixgrow { from { transform:scaleX(0); opacity:0 } }
.spx-mixkey span { cursor:default; transition:color var(--t-press) ease }
.spx-mixkey span:hover { color:var(--fg-1) }
.spx-stat[data-tip] { cursor:default }

/* The rescan button is the one row action that spends quota, so it is the one that is tinted. */
.spx-iconsm--go { border-color:rgba(216,52,131,.42); color:var(--brand-ink) }
@media (hover: hover) and (pointer: fine) {
  .spx-iconsm--go:hover:not([disabled]) { background:var(--accent-soft); color:var(--brand-ink) }
}
.spx-iconsm--go:not([disabled]):active .ico { animation:spx-spin .5s var(--ease-in-out) }
@keyframes spx-spin { to { transform:rotate(360deg) } }
.spx-bulk-act { display:flex; align-items:center; gap:9px; margin-left:auto; flex-wrap:wrap }
.spx-bulk-act .spx-ghost { min-height:40px; padding:9px 14px }
.spx-bulk-go { display:inline-flex; align-items:center; gap:8px; padding:11px 18px; min-height:40px; font-size:13.5px }
.spx-bulk-go .ico { width:15px; height:15px }
@media (max-width:620px) { .spx-bulk-act { margin-left:0; width:100% } }

/* ---------- pagination ----------
   Shape from SiberRadar's .sr-pagi: the count and the page-size control on the left, the pager
   on the right, and the whole strip sitting BELOW the panel rather than inside it — it is a
   control over the table, not a row of it. */
.spx-pagi {
  display:flex; align-items:center; justify-content:space-between; gap:12px; flex-wrap:wrap;
  margin-top:14px;
}
.spx-pagi-next { display:inline-flex; align-items:center; gap:6px; padding:9px 15px; min-height:40px; font-size:13px }
.spx-pagi-left { display:flex; align-items:center; gap:14px; flex-wrap:wrap; color:var(--fg-3); font-size:12.5px }
.spx-pagi-left b { color:var(--fg-1); font-weight:600 }
.spx-pagi-nav { display:flex; align-items:center; gap:10px }
.spx-pagi-nav span { color:var(--fg-2); font-size:12.5px; font-variant-numeric:tabular-nums }
.spx-pagi .spx-ghost { display:inline-flex; align-items:center; gap:6px; padding:9px 13px; min-height:40px }
.spx-pagi .spx-ghost[disabled] { opacity:.4; cursor:default }
.spx-pagi .ico { width:14px; height:14px }
.spx-pagi select {
  min-height:40px; padding:8px 12px; cursor:pointer;
  background:var(--bg-surface-2); border:1px solid var(--border); border-radius:10px;
  color:var(--fg-1); font-family:var(--font-sans); font-size:12.5px; font-weight:600;
}
@media (max-width:620px) {
  .spx-pagi { flex-direction:column; align-items:stretch }
  .spx-pagi-left, .spx-pagi-nav { justify-content:center }
}

/* ---------- modals ----------
   Two of them, and neither renders its own content in production:
   #acct-modal is Clerk's <UserProfile>, #report-modal frames the very same HTML that
   lib/report-html.mjs produces for the PDF. Framing it rather than rebuilding the on-screen
   result UI means what the partner previews and what they download cannot disagree. */
.spx-modal {
  position:fixed; inset:0; z-index:200; display:flex; align-items:center; justify-content:center;
  padding:28px; background:rgba(2,10,26,.72); backdrop-filter:blur(3px);
  animation:spx-fadein 160ms var(--ease-out) both;
}
@keyframes spx-fadein { from { opacity:0 } }
.spx-modal[hidden] { display:none }
.spx-modal-card {
  position:relative; display:flex; width:100%; max-width:880px; max-height:86vh; overflow:hidden;
  background:var(--bg-surface); border:0;
  border-radius:14px; box-shadow:0 24px 64px rgba(2,10,26,.5);
  animation:spx-pop var(--t-modal) var(--ease-out) both;
}
.spx-modal-x {
  position:absolute; top:12px; right:14px; z-index:2; width:32px; height:32px; padding:0;
  background:var(--grey-100); border:1px solid var(--border); border-radius:9px;
  color:var(--fg-2); font-size:19px; line-height:1; cursor:pointer;
}
.spx-modal-side {
  flex:0 0 190px; padding:22px 14px; background:var(--bg-surface-2); border-right:1px solid var(--border);
}
.spx-modal-title { padding:0 10px 12px; font-size:15px; font-weight:700 }
.spx-modal-nav {
  display:block; width:100%; min-height:44px; padding:11px 10px; margin-bottom:3px; text-align:left;
  background:transparent; border:0; border-radius:8px; cursor:pointer;
  color:var(--fg-2); font-family:var(--font-sans); font-size:13.5px;
}
.spx-modal-nav.is-on { background:var(--accent-soft); color:var(--fg-1); font-weight:600 }
.spx-modal-body { flex:1; padding:24px 26px; overflow:auto }
.spx-modal-body h3 { margin:0 0 16px; font-size:17px; font-weight:700 }

.spx-sec { padding:15px 0; border-top:1px solid var(--divider) }
.spx-sec:first-of-type { border-top:0; padding-top:0 }
.spx-sec-h { display:flex; align-items:center; gap:12px; justify-content:space-between }
.spx-sec-h b { font-size:14px }
.spx-sec-p { color:var(--fg-2); font-size:12.5px; margin-top:5px }

/* ---------- account panes: profile, password, authenticator ----------
   Each editor opens in place of the line it edits, rather than in a modal on top of a modal.
   A dialog stacked on a dialog gives two things to close and two Escape keys to guess at. */
.spx-btn-sm { padding:8px 16px; min-height:38px; font-size:13px }
/* Each of these sets `display`, which outranks the UA's `[hidden] { display:none }`. Without
   this line a closed editor stays on screen next to the value it is meant to be replacing. */
.spx-inline[hidden], .spx-form[hidden], .spx-mfa[hidden] { display:none }
.spx-prof { display:flex; align-items:center; gap:14px; margin-bottom:18px }
.spx-prof-av {
  display:flex; align-items:center; justify-content:center; width:52px; height:52px; flex:none;
  border-radius:50%; background:var(--grad-magenta); color:#fff; font-size:17px; font-weight:700;
}
.spx-prof-n { font-size:15px; font-weight:700; color:var(--fg-1) }
.spx-prof-e { font-size:12.5px; color:var(--fg-3); margin-top:2px }

.spx-inline { display:flex; align-items:center; gap:9px; margin-top:9px; flex-wrap:wrap }
.spx-inline input { flex:1 1 200px }
.spx-form { display:flex; flex-direction:column; gap:11px; margin-top:12px }
.spx-form label { display:flex; flex-direction:column; gap:6px; font-size:12.5px; font-weight:600; color:var(--fg-2) }
.spx-form input, .spx-inline input {
  min-height:42px; padding:10px 13px; background:var(--bg-surface-2);
  border:1px solid var(--border); border-radius:10px;
  color:var(--fg-1); font-family:var(--font-sans); font-size:13.5px; font-weight:400;
}
.spx-form input:focus, .spx-inline input:focus { border-color:var(--magenta-500); outline:none }
.spx-form-note { font-size:11.5px; line-height:1.5; color:var(--fg-3) }
.spx-form-note.is-bad { color:var(--band-veryhigh) }
.spx-form-act { display:flex; gap:9px; flex-wrap:wrap; margin-top:2px }

.spx-mfa { margin-top:12px; padding:14px; border:1px solid var(--border); border-radius:var(--r-md); background:var(--bg-surface-2) }
.spx-mfa-row { display:flex; gap:16px; align-items:flex-start; flex-wrap:wrap }
.spx-mfa-txt { flex:1 1 220px; min-width:0 }
.spx-mfa-txt b { display:block; font-size:12.5px; color:var(--fg-1); margin-top:10px }
.spx-mfa-txt b:first-child { margin-top:0 }
.spx-mfa-txt p { margin:4px 0 0; font-size:12px; line-height:1.5; color:var(--fg-3) }
.spx-key { padding:3px 7px; border-radius:6px; background:var(--bg-surface); border:1px solid var(--border);
           font-family:var(--font-mono, ui-monospace, monospace); font-size:12px; letter-spacing:.06em; color:var(--fg-1) }
/* A drawn QR, not an image: this is a design file with no server to mint a real secret, and a
   grey box labelled "QR" reads as something that failed to load. The pattern is decorative. */
.spx-qr {
  width:116px; height:116px; flex:none; border-radius:10px; background:#fff;
  background-image:
    linear-gradient(90deg, #0b1020 25%, transparent 25% 50%, #0b1020 50% 62%, transparent 62%),
    linear-gradient(#0b1020 12%, transparent 12% 30%, #0b1020 30% 42%, transparent 42% 66%, #0b1020 66% 78%, transparent 78%);
  background-size:16px 16px;
  box-shadow:inset 0 0 0 8px #fff, inset 0 0 0 9px var(--border);
}

/* small single-purpose dialog (rename, revoke, quota) */
.spx-modal--sm .spx-modal-card { max-width:440px; flex-direction:column }
.spx-modal-p { margin:0 0 18px; color:var(--fg-2); font-size:13px; line-height:1.6 }
.spx-modal-p b { color:var(--fg-1); font-family:ui-monospace,SFMono-Regular,Menlo,monospace; font-size:12.5px }
.spx-field { display:block }
.spx-field > span {
  display:block; margin-bottom:6px;
  color:var(--fg-3); font-size:11px; font-weight:700; letter-spacing:.07em; text-transform:uppercase;
}
.spx-field input, .spx-field select, .spx-field textarea {
  width:100%; min-height:46px; padding:12px 14px;
  background:var(--bg-surface-2); border:1px solid var(--border); border-radius:10px;
  color:var(--fg-1); font-family:var(--font-sans); font-size:14px;
}
.spx-field textarea { resize:vertical; line-height:1.55 }
.spx-field input:focus, .spx-field select:focus, .spx-field textarea:focus { border-color:var(--magenta-500) }
.spx-modal-foot { display:flex; align-items:center; justify-content:space-between; gap:10px; margin-top:22px }
.spx-field > span > i { font-style:normal; color:var(--fg-3); font-weight:500; text-transform:none; letter-spacing:0 }
/* The cost of changing the domain, shown while it is being typed rather than after the click. */
.spx-hint {
  display:flex; align-items:flex-start; gap:9px; margin:10px 0 0; padding:10px 12px;
  background:rgba(246,199,5,.09); border:1px solid rgba(246,199,5,.32); border-radius:10px;
  color:var(--fg-2); font-size:12px; line-height:1.5;
  animation:spx-rise .22s var(--ease-out) both;
}
.spx-hint[hidden] { display:none }
.spx-hint .ico { width:15px; height:15px; color:var(--warn-ink); margin-top:1px; flex:none }

/* A row whose numbers are being replaced: they belong to the previous scan until the new one
   lands, so they dim, and a shimmer crosses the row so the wait reads as work rather than as a
   row that has gone wrong. Ported from SiberRadar, which does the same on its queued rows. */
.spx-rt tbody tr.is-rescanning td { opacity:.55 }
.spx-rt tbody tr.is-rescanning td:nth-last-child(2) { opacity:1; color:var(--brand-ink); font-weight:600 }
/* The sweep is the ROW's background, not an absolutely positioned overlay on it. The overlay
   version needed `position:relative` on the <tr>, and a positioned table row becomes a
   containing block — which re-laid out its own cells and visibly shoved the row left of every
   other row while it ran. A background paints under the cells and moves nothing. */
.spx-rt tbody tr.is-rescanning {
  background-image:linear-gradient(100deg, transparent 34%, var(--accent-soft) 50%, transparent 66%);
  background-size:280px 100%; background-repeat:repeat-x;
  animation:spx-shim 1.15s linear infinite;
}
@keyframes spx-shim { from { background-position:-280px 0 } to { background-position:280px 0 } }
/* The rescan icon spins for as long as its row does. */
.spx-rt tbody tr.is-rescanning .spx-iconsm--go .ico { animation:spx-spin .9s linear infinite }

/* Numbers that just changed. One pulse, and only on the cells that moved — flashing the whole
   row would say "everything is new" when three of eleven columns actually are. */
@keyframes spx-flash {
  0%   { background:var(--accent-soft) }
  100% { background:transparent }
}
.spx-rt tbody td.is-fresh { animation:spx-flash 1.5s var(--ease-out) both }

/* ---------- scan status line ----------
   Sits where the quota bar sits, and replaces it while a scan is in flight — the two answer the
   same question ("can I scan, and what is happening") and stacking them puts two competing
   sentences under one input. */
.spx-scanmsg {
  display:flex; align-items:center; gap:9px; margin-top:12px;
  font-size:13px; line-height:1.5; color:var(--fg-2);
  animation:spx-rise .24s var(--ease-out) both;
}
.spx-scanmsg[hidden] { display:none }
.spx-scanmsg .ico { width:16px; height:16px; flex:none }
.spx-scanmsg b { color:var(--fg-1); font-weight:600 }
.spx-scanmsg--work .ico  { color:var(--magenta-500); animation:spx-spin .9s linear infinite }
.spx-scanmsg--stop .ico  { color:var(--band-veryhigh) }
.spx-scanmsg--stop       { color:var(--band-veryhigh) }
.spx-scanmsg--hold .ico  { color:var(--warn-ink) }

/* The Scan button while it works. Width is frozen at the pixel it had, or the row reflows the
   moment the label changes from "Scan" to "Scanning…". */
.spx-cta.is-busy { pointer-events:none; opacity:.85 }
/* "Scan" becomes "⟳ Scanning…" mid-press. The button reserves the wider label's width from the
   start, so the busy state neither overflows the pill nor shoves the input sideways. */
#scan-go { min-width:11.5rem; flex:none }
.spx-cta.is-busy .ico { width:16px; height:16px; margin-right:7px; animation:spx-spin .9s linear infinite }

/* ---------- scan result ----------
   The same instrument the public page shows after a check: gauge and score on the right, the
   facts on the left, then the four pillars and the four counts. A partner who has used the free
   tool must recognise this screen — it is the same product, behind a login. */
.spx-modal--res .spx-modal-card { max-width:840px; flex-direction:column; max-height:92vh }
.spx-res { display:flex; flex-wrap:wrap; gap:18px; align-items:stretch }
.spx-res-facts { flex:1 1 300px; display:flex; flex-direction:column; gap:12px; min-width:0 }
.spx-res-tile { padding:14px 16px; border-radius:var(--r-md); background:var(--bg-surface-2); border:1px solid var(--border) }
.spx-res-k { color:var(--fg-3); font-size:10.5px; font-weight:700; letter-spacing:.05em; text-transform:uppercase }
.spx-res-v { margin-top:7px; font-size:16px; font-weight:700; color:var(--fg-1); word-break:break-all }
.spx-res-two { display:grid; grid-template-columns:1fr 1fr; gap:12px }
.spx-res-score { flex:1 1 250px; display:flex; flex-direction:column; align-items:center; justify-content:center; text-align:center }
.spx-res-n { margin-top:2px; font-size:44px; font-weight:700; letter-spacing:-.03em; line-height:1; color:var(--fg-1) }
.spx-res-n span { font-size:18px; font-weight:600; color:var(--fg-3) }
.spx-res-band { margin-top:11px; padding:5px 14px; border-radius:var(--r-pill); font-size:12.5px; font-weight:700 }
.spx-res-h { margin:26px 0 12px; color:var(--fg-3); font-size:12px; font-weight:700; letter-spacing:.06em; text-transform:uppercase }
.spx-res-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(150px,1fr)); gap:12px }
.spx-res-p { padding:16px; border-radius:var(--r-md); background:var(--bg-surface-2); border:1px solid var(--border) }
.spx-res-p-n { font-size:24px; font-weight:800; line-height:1 }
.spx-res-p-n i { font-style:normal; font-size:15px }
.spx-res-p-l { margin-top:8px; font-size:12.5px; font-weight:700; color:var(--fg-1) }
.spx-res-p-d { margin-top:9px; font-size:11.5px; line-height:1.4; color:var(--fg-2) }
.spx-res-bar { height:4px; margin-top:10px; border-radius:99px; background:var(--grey-200); overflow:hidden }
.spx-res-bar > i { display:block; height:100%; border-radius:99px }
.spx-res-m { padding:18px 16px; border-radius:var(--r-md); background:var(--bg-surface-2); border:1px solid var(--border); text-align:center }
.spx-res-m-n { font-size:28px; font-weight:700; letter-spacing:-.02em; line-height:1; color:var(--fg-1) }
.spx-res-m-l { margin-top:8px; font-size:12.5px; color:var(--fg-2) }
.spx-res-foot {
  display:flex; align-items:center; gap:12px; flex-wrap:wrap;
  margin-top:24px; padding-top:18px; border-top:1px solid var(--divider);
  color:var(--fg-2); font-size:12.5px;
}
.spx-res-foot .spx-res-say { flex:1; min-width:180px }

/* The gauge glow, now copied value-for-value from the public page's .spx-gaugeglow rather than
   approximated. The approximation was the bug: it pulsed between .85 and 1 opacity where the
   original breathes between .32 and .66, and it centred with a margin instead of the keyframe's
   own translate(-50%,-50%) — so it sat low and bright, a permanent blob behind the number
   instead of a glow you mostly notice on hover. Both numbers matter; neither was a judgement
   call to make locally when the source is in this repo. */
.spx-res-glow { position:relative; isolation:isolate; line-height:0 }
.spx-res-glow::before {
  content:""; position:absolute; left:50%; top:60%; width:150px; height:150px;
  border-radius:50%; z-index:-1; pointer-events:none;
  background:radial-gradient(circle, var(--gauge-glow, rgba(216,52,131,.5)), transparent 68%);
  filter:blur(15px); animation:spx-glowpulse 3.2s ease-in-out infinite;
}
@keyframes spx-glowpulse {
  0%,100% { opacity:.32; transform:translate(-50%,-50%) scale(.9) }
  50%     { opacity:.66; transform:translate(-50%,-50%) scale(1.08) }
}
/* Hover is where the glow actually announces itself — the same lift and brighten the public
   page gives the whole score block. */
.spx-res-score { transition:transform .3s cubic-bezier(.2,.7,.3,1); border-radius:var(--r-lg); cursor:default }
.spx-res-score .spx-res-glow,
.spx-res-score .spx-res-glow::before { transition:transform .35s cubic-bezier(.2,.7,.3,1), filter .35s ease }
.spx-res-score:hover { transform:translateY(-4px) }
.spx-res-score:hover .spx-res-glow { transform:scale(1.07) }
.spx-res-score:hover .spx-res-glow::before { filter:blur(20px) brightness(1.25) }

.spx-res-chip {
  display:inline-flex; align-items:center; gap:6px; margin-top:10px; padding:5px 12px;
  border-radius:var(--r-pill); background:var(--accent-soft); color:var(--brand-ink);
  font-size:11px; font-weight:600;
}
.spx-res-chip .ico { width:13px; height:13px }

/* The report tile, ported from .spx-reportbox: the topographic texture screened over the pink,
   the lift-and-glow on hover, and the arrow that bobs continuously and speeds up under the
   cursor. Those three are most of why the original reads as a thing you press. */
.spx-res-cta {
  position:relative; overflow:hidden;
  flex:0 0 190px; display:flex; flex-direction:column; align-items:center; justify-content:center;
  gap:10px; padding:22px 18px; border:0; border-radius:var(--r-md); cursor:pointer;
  background:var(--brand-fill); color:#fff; font-family:var(--font-sans); text-align:center;
  transition:transform .22s cubic-bezier(.2,.7,.3,1), box-shadow .22s ease;
}
.spx-res-cta::after {
  content:""; position:absolute; inset:0; pointer-events:none;
  background:url("/assets/topographic.svg"); background-size:340px;
  opacity:.16; mix-blend-mode:screen;
}
.spx-res-cta > * { position:relative; z-index:1 }
@media (hover: hover) and (pointer: fine) {
  .spx-res-cta:hover { transform:translateY(-4px); box-shadow:0 16px 34px rgba(216,52,131,.5) }
  .spx-res-cta:hover .ico { animation-duration:.85s }
}
@keyframes spx-dlbob { 0%,100% { transform:translateY(-3px) } 50% { transform:translateY(5px) } }
.spx-res-cta .ico { width:26px; height:26px; animation:spx-dlbob 1.9s ease-in-out infinite }
.spx-res-cta-k {
  padding:4px 12px; border-radius:var(--r-pill); background:rgba(255,255,255,.92); color:var(--brand-fill);
  font-size:10.5px; font-weight:700; letter-spacing:.05em; text-transform:uppercase;
}
.spx-res-cta-h { font-size:17px; font-weight:800; letter-spacing:-.01em }
.spx-res-cta-s { font-size:11.5px; opacity:.9 }

/* Metric tiles carry an icon badge, tinted to the thing they count — the same four the public
   page uses, so the two screens are recognisably one product. */
.spx-res-m-i {
  display:inline-flex; align-items:center; justify-content:center; width:56px; height:56px;
  margin-bottom:14px; border-radius:16px;
}
.spx-res-m-i .ico { width:28px; height:28px }

/* ---------- the sections below the fold of the result ----------
   Each is its own panel, matching the public page's section rhythm. */
.spx-res-sec { margin-top:16px; padding:20px 18px; background:var(--bg-surface-2); border:1px solid var(--border); border-radius:var(--r-lg) }
.spx-res-sec-h { color:var(--fg-1); font-size:13px; font-weight:700; letter-spacing:.05em; text-transform:uppercase }
.spx-res-sec-s { margin-top:4px; color:var(--fg-3); font-size:12.5px; line-height:1.5 }

/* Exposure over time */
.spx-res-tl { display:flex; align-items:flex-end; gap:6px; height:96px; margin-top:14px; padding:0 2px }
.spx-res-tl > div { flex:1; display:flex; flex-direction:column; align-items:center; justify-content:flex-end; gap:6px; height:100% }
/* spx-grow, the public page's own bar entrance: scaleY from 0 AND opacity from .35, so the bars
   arrive rather than stretch. */
.spx-res-tl i {
  display:block; width:100%; max-width:34px; border-radius:6px 6px 3px 3px;
  background:var(--grad-magenta, linear-gradient(180deg,#F0559C,#B32468));
  transform-origin:bottom center; animation:spx-grow .6s cubic-bezier(.2,.8,.2,1) backwards;
}
.spx-res-tl span { color:var(--fg-3); font-size:10.5px; font-weight:600 }
@keyframes spx-grow { 0% { transform:scaleY(0); opacity:.35 } 100% { transform:scaleY(1); opacity:1 } }

/* Apps this company leaked most */
.spx-res-apps { display:flex; flex-direction:column; gap:9px; margin-top:14px }
.spx-res-app { display:flex; align-items:center; gap:12px }
.spx-res-app .ico { width:15px; height:15px; color:var(--fg-3); flex:none }
.spx-res-app b {
  width:190px; max-width:42%; font-family:ui-monospace,SFMono-Regular,Menlo,monospace;
  font-size:12.5px; font-weight:600; color:var(--fg-1); word-break:break-all;
}
.spx-res-app-t { flex:1; height:8px; border-radius:var(--r-pill); background:var(--grey-200); overflow:hidden }
.spx-res-app-t > i { display:block; height:100%; border-radius:var(--r-pill); background:var(--grad-magenta, linear-gradient(90deg,#B32468,#F0559C)) }
.spx-res-app > span:last-child { width:34px; text-align:right; font-size:13px; font-weight:700; color:var(--fg-1) }

/* Exposed credentials */
.spx-res-cred { margin-top:14px; border:1px solid var(--border); border-radius:var(--r-md); overflow:hidden }
.spx-res-cred > div {
  display:grid; grid-template-columns:minmax(0,1.7fr) 120px minmax(0,1.2fr) 78px 58px;
  align-items:center; gap:12px; padding:10px 14px; border-top:1px solid var(--divider); font-size:12.5px;
}
.spx-res-cred > div:first-child {
  border-top:0; background:var(--bg-surface); color:var(--fg-3);
  font-size:10.5px; font-weight:700; letter-spacing:.05em; text-transform:uppercase;
}
.spx-res-cred .mono { font-family:ui-monospace,SFMono-Regular,Menlo,monospace; word-break:break-all }
.spx-res-cred .pw { letter-spacing:1.5px; color:var(--fg-2); overflow:hidden; text-overflow:ellipsis; white-space:nowrap }
.spx-res-cred .app { color:var(--fg-3); font-size:11.5px }
.spx-res-cred .yr { text-align:right; color:var(--fg-3); font-weight:600 }
.spx-res-cred-c { margin-top:11px; color:var(--fg-3); font-size:11.5px; text-align:center }
.spx-res-pw { padding:2px 9px; border-radius:var(--r-pill); font-size:10.5px; font-weight:700 }

/* What this means, and how SiberMate helps */
/* Each finding is its own card, as on the public page — a stack of rows with a gap between them,
   not one table, so a row can lift away from its neighbours on hover. */
.spx-res-find { display:flex; flex-direction:column; gap:10px; margin-top:10px }
.spx-res-findrow { display:grid; grid-template-columns:1.1fr 1.1fr 1.2fr; gap:1px;
                   background:var(--border); border:1px solid var(--border); border-radius:var(--r-md); overflow:hidden }
.spx-res-findrow > div { padding:14px 15px; background:var(--bg-surface); font-size:13px; line-height:1.45 }
.spx-res-findrow > div:nth-child(1) { color:var(--fg-1); font-weight:600 }
.spx-res-findrow > div:nth-child(2) { color:var(--fg-2) }
.spx-res-findrow > div:nth-child(3) { background:color-mix(in srgb, var(--magenta-500) 6%, var(--bg-surface)); color:var(--fg-1) }
.spx-res-findhead { display:grid; grid-template-columns:1.1fr 1.1fr 1.2fr; gap:1px; margin-top:14px; padding:0 2px }
.spx-res-findhead > span { padding:0 15px; color:var(--fg-3); font-size:10.5px; font-weight:700; letter-spacing:.05em; text-transform:uppercase }
.spx-res-findhead > span:last-child { color:var(--magenta-500) }

/* Where you stand in your sector */
.spx-res-peer { display:flex; align-items:center; gap:22px; flex-wrap:wrap; margin-top:14px;
                padding:15px 18px; border-radius:var(--r-md); background:var(--bg-surface); border:1px solid var(--border) }
.spx-res-peer-l { flex:1 1 250px; min-width:0 }
.spx-res-peer-lead { font-size:15px; font-weight:700; line-height:1.35; color:var(--fg-1) }
.spx-res-peer-sub { margin-top:6px; font-size:12.5px; line-height:1.5; color:var(--fg-3) }
/* The strip is an SVG built in partner-scan.js — peers as dots, the sector average as a dashed
   line, this prospect as a marker. It is the public card's chart, not a colour scale. */
.spx-res-strip { width:300px; max-width:100% }
.spx-res-axis { position:relative; width:300px; max-width:100%; height:14px; margin-top:4px; color:var(--fg-3); font-size:8.5px; font-weight:600 }
.spx-res-axis > span { position:absolute; transform:translateX(-50%) }
.spx-res-leg { display:flex; align-items:center; justify-content:center; gap:5px;
               width:300px; max-width:100%; margin-top:3px; font-size:9.5px; color:var(--fg-3) }
.spx-res-leg .dash { display:inline-block; width:13px; border-top:1.5px dashed var(--fg-1) }
.spx-res-leg .dot  { display:inline-block; width:8px; height:8px; border-radius:50%; margin-left:7px }
.spx-res-peer-foot { margin-top:10px; font-size:10.5px; line-height:1.45; color:var(--fg-3) }
.spx-res-chips { display:flex; gap:11px; flex-wrap:wrap; margin-top:11px }
.spx-res-chip2 { flex:1 1 150px; display:flex; align-items:center; gap:9px; padding:10px 13px;
                 border-radius:var(--r-md); background:var(--bg-surface); border:1px solid var(--border) }
.spx-res-chip2 i { width:9px; height:9px; border-radius:50%; flex:none }
.spx-res-chip2 b { font-size:11.5px; font-weight:700; color:var(--fg-1); display:block }
.spx-res-chip2 span { font-size:10.5px; color:var(--fg-3) }
.spx-res-chip2 em { margin-left:auto; font-style:normal; font-size:15px; font-weight:800 }

/* ---------- the motion, ported one rule at a time from the public result screen ----------
   The modal had none of this, which is why it read as a printout of the result rather than the
   result. Every value below is the source's, not a new one: .spx-tile, .spx-approw, .spx-row,
   .spx-bar, .spx-find and .spx-peerchip in Design/SiberXpose.dc.html. */
@media (hover: hover) and (pointer: fine) {
  .spx-res-tile, .spx-res-p, .spx-res-m { transition:transform .18s ease, border-color .18s ease, background .18s ease }
  .spx-res-tile:hover, .spx-res-p:hover, .spx-res-m:hover {
    transform:translateY(-3px); border-color:var(--border-strong); background:var(--bg-surface);
  }
  .spx-res-app { transition:transform .16s ease }
  .spx-res-app:hover { transform:translateX(3px) }
  .spx-res-cred > div:not(:first-child) { transition:background .15s ease }
  .spx-res-cred > div:not(:first-child):hover { background:var(--accent-soft) }
  /* The SiberMate column lights up with the row, which is the one piece of persuasion on the
     screen and the reason the original bothers. */
  .spx-res-findrow { transition:transform .18s ease, box-shadow .2s ease }
  .spx-res-findrow:hover { transform:translateY(-2px); box-shadow:var(--shadow-md) }
  .spx-res-findrow > div:nth-child(3) { transition:box-shadow .2s ease, background .2s ease }
  .spx-res-findrow:hover > div:nth-child(3) {
    background:color-mix(in srgb, var(--magenta-500) 11%, var(--bg-surface));
    box-shadow:inset 0 0 0 1px color-mix(in srgb, var(--magenta-500) 55%, transparent),
               inset 0 0 18px -6px color-mix(in srgb, var(--magenta-500) 50%, transparent);
  }
  .spx-res-chip2 { transition:background-color .18s ease, border-color .18s ease, box-shadow .18s ease }
  .spx-res-chip2:hover {
    background:color-mix(in srgb, var(--magenta-500) 7%, var(--bg-surface));
    border-color:color-mix(in srgb, var(--magenta-500) 35%, var(--border));
  }
  .spx-res-tl i { transition:transform .18s cubic-bezier(.2,.8,.2,1), filter .18s ease, box-shadow .18s ease }
  .spx-res-tl > div:hover i {
    transform:translateY(-5px); filter:brightness(1.1);
    box-shadow:0 8px 18px rgba(216,52,131,.55), 0 0 5px rgba(216,52,131,.45);
  }
}
/* The result arrives in the order it should be read: the score block, then how it was made,
   then the evidence. The card itself already pops; without this its contents were simply
   present inside it, which is what made the modal feel like a printout.

   Halved on 8 Aug 2026. The last section used to start at .32s and run for .42s, so the modal
   was not finished assembling itself until 740ms after the click — on top of a token refresh
   and a round trip to fetch the row. Reported as "opening the result is slow", and it was: the
   wait was almost entirely this, not the request. The order still reads, it just arrives.
   Nothing below .16s of stagger survives being noticed as an order at all, which is why the
   delays are halved rather than deleted. */
.spx-modal--res .spx-res            { animation:spx-up .26s var(--ease-out) .02s backwards }
.spx-modal--res .spx-res-h          { animation:spx-up .26s var(--ease-out) .05s backwards }
.spx-modal--res #res-pillars        { animation:spx-up .26s var(--ease-out) .07s backwards }
.spx-modal--res #res-metrics        { animation:spx-up .26s var(--ease-out) .10s backwards }
.spx-modal--res .spx-res-sec        { animation:spx-up .26s var(--ease-out) .13s backwards }
.spx-modal--res .spx-res-foot       { animation:spx-up .26s var(--ease-out) .16s backwards }

/* Findings play in on open, staggered. They used to be driven by `animation-timeline:view()`,
   which ties the animation to the modal's scroll position — so every one of these rows was
   being sampled and re-composited on every scroll frame, inside a panel that also sits under a
   backdrop-filter. Removed 8 Aug 2026 after "scrolling the results popup is laggy": a scroll-
   driven reveal is work done per frame, and the same reveal on open is work done once.
   The base state is visible either way, so nothing is lost if the animation never runs. */
.spx-res-findrow {
  animation:spx-up .5s cubic-bezier(.2,.7,.3,1) backwards;
}
.spx-res-findrow:nth-child(2) { animation-delay:.03s }
.spx-res-findrow:nth-child(3) { animation-delay:.09s }
.spx-res-findrow:nth-child(4) { animation-delay:.15s }
.spx-res-findrow:nth-child(5) { animation-delay:.21s }

@media (max-width:820px) {
  .spx-res-findrow, .spx-res-findhead { grid-template-columns:1fr }
  .spx-res-findhead { display:none }
  .spx-res-cred > div { grid-template-columns:1fr; gap:4px }
  .spx-res-cred > div:first-child { display:none }
  .spx-res-cta { flex:1 1 100% }
}

/* report modal: wider, and its head carries the language choice for the download */
.spx-modal--report .spx-modal-card { max-width:1040px; flex-direction:column; max-height:90vh }
.spx-modal-head {
  display:flex; align-items:center; gap:14px; padding:14px 18px;
  border-bottom:1px solid var(--border); background:var(--bg-surface);
}
.spx-modal-head .spx-h-title { flex:1; font-size:14px; color:var(--fg-2); white-space:nowrap; overflow:hidden; text-overflow:ellipsis }
.spx-modal-head .spx-h-title b { color:var(--fg-1) }

/* The report's pages are a fixed 8.5in wide (lib/report-html.mjs, letter portrait). Stretching
   the frame to the modal's width just adds dead canvas to the right of the last page, so the
   frame is the page's width and the leftover space becomes an even margin either side. */
.spx-modal-frame { flex:1; overflow:auto; background:var(--bg-canvas); display:flex; justify-content:center }
.spx-modal-frame iframe { display:block; width:8.5in; max-width:100%; height:100%; min-height:520px; border:0 }

/* ---------- board (dashboard + admin) ---------- */
/* Third attempt at this, and the two failures either side of it are the useful part.
   1040px was too narrow: the table wants 940px and scrolled sideways at 1440.
   Full-bleed was too wide: eleven short columns stretched across 1430px leaves gaps wide
   enough that the eye loses the row it is reading.
   1280px is the standard app container for a reason. It clears the table's 940px need with
   room to spare, so nothing scrolls, and it keeps columns close enough to scan across. */
.spx-pboard { position:relative; z-index:1; width:100%; max-width:1280px; margin:0 auto; padding:92px clamp(18px,2.6vw,32px) 60px }

/* There used to be a `max-width:820px` on the headings, the scan row and the two banners,
   while the stats strip, the filter bar and the table ran the full 1216px. That is what made
   the page look untidy top to bottom: one left edge, but TWO right edges, alternating down
   the page — 932px, 1328px, 932px, 932px, 1328px. A page reads as tidy when every block
   starts and ends on the same two lines, so the cap is gone and the column is one width. */
.spx-pboard h1 { margin:0 0 6px; font-size:27px; font-weight:700; letter-spacing:-.01em }
.spx-pboard h2 { margin:40px 0 14px; font-size:17px; font-weight:700 }
.spx-panel { background:var(--bg-surface); border:1px solid var(--border); border-radius:14px }
.spx-scanform { display:flex; gap:10px; margin:20px 0 8px }
.spx-scanform input {
  flex:1; padding:14px 18px; background:var(--bg-surface-2);
  border:1px solid var(--border); border-radius:var(--r-pill); color:var(--fg-1); font-size:15px;
}
.spx-scanform input:focus { border-color:var(--magenta-500) }

.spx-table { width:100%; border-collapse:collapse }
.spx-table th {
  padding:11px 14px; text-align:left; color:var(--fg-3);
  font-size:11px; font-weight:700; text-transform:uppercase; letter-spacing:.07em;
}
.spx-table td { padding:15px 14px; border-top:1px solid var(--divider); font-size:14px }
.spx-table tbody tr:hover { background:var(--grey-100) }
.spx-dom { font-weight:600 }
.spx-when { color:var(--fg-2); font-size:13px }
.spx-score { font-size:17px; font-weight:700 }

.spx-band { display:inline-block; padding:3px 11px; border-radius:var(--r-pill); font-size:11.5px; font-weight:700 }
.spx-band--low  { background:rgba(9,174,154,.16); color:#3FD8C4 }
.spx-band--mid  { background:rgba(246,199,5,.15); color:var(--warn-ink) }
.spx-band--high { background:rgba(216,52,131,.16); color:#FF93C0 }

.spx-row-actions { display:flex; gap:8px; justify-content:flex-end }
.spx-empty { padding:34px 14px; text-align:center; color:var(--fg-3); font-size:13.5px }

/* ---------- sign-up steps ----------
   Clerk's card is three screens, and these are the two bits of chrome the steps after the first
   one need: a line that says what went wrong, and a text button for the two escape hatches
   ("use a different email", "resend the code"). */
.spx-suerr {
  margin:-4px 0 10px; color:var(--brand-ink); font-size:12.5px; text-align:left;
}
.spx-suerr[hidden] { display:none }
.spx-suwho { color:var(--fg-1); font-weight:600; overflow-wrap:anywhere }
.spx-sulink {
  padding:0; background:none; border:0; cursor:pointer;
  color:var(--brand-ink); font-family:var(--font-sans); font-size:inherit; font-weight:600;
  text-decoration:underline; text-underline-offset:2px;
}
.spx-sulink--block { display:block; width:100%; margin-top:12px; font-size:13px }

/* ---------- one-time code ---------- */
.spx-otp { display:flex; gap:8px; justify-content:center; margin-bottom:10px }
.spx-otp-box {
  flex:1 1 0; min-width:0; max-width:56px; height:58px; padding:0; text-align:center;
  background:var(--bg-surface-2); border:1px solid var(--border); border-radius:10px;
  color:var(--fg-1); font-size:22px; font-family:ui-monospace,SFMono-Regular,Menlo,monospace;
  transition:border-color var(--t-pop) var(--ease-out), background var(--t-pop) var(--ease-out);
}
.spx-otp-box:focus { outline:none; border-color:var(--magenta-500); background:var(--bg-surface) }
/* A filled box holds its brighter border, so how far along the code is reads at a glance rather
   than by counting digits. */
.spx-otp-box.is-filled { border-color:var(--border-strong) }

/* Verify runs this left to right, one box after another — the stagger is the point. A single
   flash of the whole row says "something happened"; a wave says "these six, in this order, are
   being read", which is the only thing the partner is waiting to know. */
@keyframes spx-otp-pop {
  0%   { transform:none }
  45%  { transform:translateY(-7px) scale(1.07); border-color:var(--magenta-500) }
  100% { transform:none }
}
.spx-otp-box.is-go { animation:spx-otp-pop .4s var(--ease-out) both }

/* Refusal moves the row sideways, never up: the wave already owns the vertical axis, so reusing
   it for the failure would make the two read as the same event at different speeds. */
@keyframes spx-otp-shake {
  0%, 100% { transform:none }
  20% { transform:translateX(-5px) }
  40% { transform:translateX(5px) }
  60% { transform:translateX(-3px) }
  80% { transform:translateX(3px) }
}
.spx-otp.is-bad .spx-otp-box { animation:spx-otp-shake .36s var(--ease-out) both; border-color:var(--brand-ink) }

/* Right-aligned under the password, where the eye is — the two controls that belong to that field
   sit on the same edge instead of one drifting to the centre with the card's text-align. */
.spx-siforgot { margin:-2px 0 12px; text-align:right }
.spx-siforgot a { color:var(--fg-3); font-size:12.5px; text-decoration:none }
.spx-siforgot a:hover { color:var(--brand-ink); text-decoration:underline }

/* Password field with a reveal. The button sits inside the box rather than beside it so the
   control keeps the same footprint as every other field on the card. */
.spx-pwwrap { position:relative; margin-bottom:10px }
.spx-pwwrap input {
  width:100%; box-sizing:border-box; padding:14px 46px 14px 16px;
  background:var(--bg-surface-2); border:1px solid var(--border); border-radius:10px;
  color:var(--fg-1); font-family:var(--font-sans); font-size:14px;
}
.spx-pweye {
  position:absolute; top:50%; right:6px; transform:translateY(-50%);
  display:flex; align-items:center; justify-content:center; width:36px; height:36px;
  background:none; border:0; border-radius:8px; color:var(--fg-3); cursor:pointer;
  transition:color var(--t-pop) var(--ease-out), background var(--t-pop) var(--ease-out);
}
.spx-pweye:hover { color:var(--fg-1); background:var(--grey-100) }
.spx-pweye .ico { width:17px; height:17px }
/* Revealed is a state worth seeing at a glance — this is the one control on the card that
   changes what is on screen rather than what is stored. */
.spx-pweye[aria-pressed="true"] { color:var(--magenta-500) }
.spx-sulink[disabled] { color:var(--fg-3); text-decoration:none; cursor:default }
/* Clerk's own card animates between steps; without this the swap is instant and reads as a
   page navigation rather than as one card changing its mind. */
#clerk-signup > [data-step] { animation:spx-rise .24s var(--ease-out) both }
@media (prefers-reduced-motion: reduce) {
  #clerk-signup > [data-step] { animation:none }
}

/* ---------- admin ---------- */
/* A queue entry is a name, one line of context and two buttons — about 420px of content that was
   being stretched across the full board, so every card carried a dead middle and six of them
   stacked into a block twice as tall as it needed to be. Tiles instead, two across, read
   left-to-right then down: still oldest-first, half the height, no hole. */
#pending {
  background:transparent; border:0; border-radius:0;
  /* min(), or the 420px floor becomes a 420px track on a 339px phone and the tiles hang out
     past the board. */
  display:grid; grid-template-columns:repeat(auto-fill,minmax(min(420px,100%),1fr)); gap:12px;
}
#pending > .spx-appcard {
  border:1px solid var(--border); border-radius:14px; background:var(--bg-surface);
}
#pending > .spx-more, #pending > .spx-empty { grid-column:1/-1 }
#pending > .spx-more { border:1px solid var(--border); border-radius:14px; background:var(--bg-surface) }
.spx-appcard { display:flex; align-items:center; gap:16px; padding:17px 18px; border-top:1px solid var(--divider) }
/* `display:flex` outranks the UA's `[hidden] { display:none }`, so the collapsed tail of the
   queue stayed on screen and "Show the other 3" looked like a button that did nothing. */
.spx-appcard[hidden] { display:none }
.spx-appcard .spx-ghost { min-height:44px }
.spx-appcard:first-child { border-top:0 }
.spx-appmeta { flex:1; min-width:0 }
/* A decided application leaves the same way a revoked row does — sideways, .2s — because it is
   the same event: this thing is no longer something you act on. Height collapses after the
   slide so the cards below do not jump before the eye has followed it out. */
.spx-appcard.is-going {
  animation:spx-rowgo .2s var(--ease-in-out) both, spx-cardclose .16s .2s var(--ease-in-out) both;
  overflow:hidden;
}
/* max-height, not height: `height:auto` has nothing to interpolate from. 140px clears the
   tallest card (two lines plus the free-provider flag) and the curve eats the slack. */
@keyframes spx-cardclose {
  from { max-height:140px }
  to   { max-height:0; padding-top:0; padding-bottom:0; border-top-width:0 }
}
/* The company website, and the only thing on this card an admin can click. Approving is a
   judgement about whether a real company is behind the application, and until now the only way
   to make it was to copy the email domain into a new tab by hand — which is why the rejection
   sitting in the log below reads "no verifiable company website". One click, new tab, and where
   the declared site differs from the sign-up domain it is the only evidence that separates a
   group structure from a stranger borrowing a company's name. */
.spx-appdom {
  display:inline-flex; align-items:center; gap:6px; margin-top:7px; padding:3px 9px;
  background:var(--grey-100); border:1px solid var(--border); border-radius:var(--r-pill);
  color:var(--fg-2); font-size:11.5px; font-family:ui-monospace,SFMono-Regular,Menlo,monospace;
  text-decoration:none; max-width:100%; overflow:hidden;
}
.spx-appdom .ico { width:11px; height:11px; flex:none; color:var(--magenta-500) }
@media (hover: hover) and (pointer: fine) {
  .spx-appdom:hover { border-color:var(--magenta-500); color:var(--fg-1) }
}
/* Where the applicant found us, beside the domain rather than in the sub-line — that line already
   clips, and attribution is not worth pushing the applied date off the end of it. Quieter than the
   domain chip on purpose: the domain is evidence the approver acts on, this is only ever read. */
.spx-appsrc {
  display:inline-flex; align-items:center; margin:7px 0 0 6px; padding:3px 9px;
  border:1px solid var(--divider); border-radius:var(--r-pill);
  color:var(--fg-3); font-size:11.5px; white-space:nowrap;
}
.spx-appname { display:flex; align-items:center; gap:9px; font-weight:600; font-size:14.5px }
/* One line, always. As tiles the cards sit side by side, so a sub-line that wrapped on the one
   long email address made its whole row taller than every other row. The tail that gets clipped
   is the applied date, and an application old enough to have a long name for it is already
   wearing its "N days waiting" flag. */
.spx-appsub { color:var(--fg-2); font-size:12.5px; margin-top:3px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap }
/* The admin list reuses the application card's row, and must not inherit its type. .spx-appname
   is 14.5px/600 for a company name; an email at that size and weight was the heaviest text on
   the page, louder than the section heading above it. Mono at 13px is what an address is set in
   everywhere else here, and .spx-appsub under it carries the provenance line. */
.spx-teamrow .spx-appname { font-size:13px; font-weight:500; color:var(--fg-1) }
.spx-teamrow .spx-appsub { color:var(--fg-3); font-size:12px }
.spx-warn {
  display:flex; align-items:flex-start; gap:11px;
  padding:13px 16px; margin-bottom:20px; border-radius:var(--r-lg); font-size:13px; line-height:1.55;
  background:rgba(246,199,5,.12); border:1px solid rgba(246,199,5,.4); color:var(--fg-1);
}
.spx-warn .ico { width:17px; height:17px; color:var(--warn-ink); margin-top:1px; flex:none }
.spx-warn code { font-family:ui-monospace,SFMono-Regular,Menlo,monospace; font-size:12px; color:var(--fg-2) }

/* Count beside a heading, so "Awaiting review" and "Active partners" carry their own size
   without a second line of text under them. */
.spx-count {
  display:inline-block; margin-left:8px; padding:2px 9px; border-radius:var(--r-pill);
  background:var(--grey-200); color:var(--fg-2); font-size:12px; font-weight:700;
  vertical-align:middle;
}

/* Status pill. One shape, four tones, and the tone is the only thing that changes — a
   suspended partner and an idle one must not differ in weight, only in meaning. */
.spx-pill {
  display:inline-block; padding:4px 11px; border-radius:var(--r-pill);
  font-size:11.5px; font-weight:700; letter-spacing:.02em; cursor:default;
}
.spx-pill--ok   { background:rgba(9,174,154,.16);  color:var(--band-verylow) }
.spx-pill--info { background:var(--grey-200);      color:var(--fg-2) }
.spx-pill--warn { background:rgba(246,199,5,.15);  color:var(--warn-ink) }
.spx-pill--no   { background:rgba(216,52,131,.16); color:var(--brand-ink) }
.spx-flag {
  padding:2px 8px; border-radius:var(--r-pill); font-size:10.5px; font-weight:700;
  background:rgba(246,199,5,.15); color:var(--warn-ink); cursor:default;
}
/* An application older than about four days. Not a different kind of application — the same
   card, wearing how long it has been sitting there. It is the queue's ordering rule stated
   where the ordering is easy to forget. */
.spx-flag--late { background:rgba(216,52,131,.14); color:var(--brand-ink) }

/* The queue's tail, behind one button rather than behind pages. */
.spx-more {
  display:flex; align-items:center; justify-content:center; gap:8px;
  width:100%; min-height:46px; padding:0 16px;
  background:transparent; border:0; border-top:1px solid var(--divider);
  color:var(--fg-2); font-family:var(--font-sans); font-size:13px; font-weight:600; cursor:pointer;
}
.spx-more[hidden] { display:none }
.spx-more b { color:var(--fg-1) }
.spx-more .ico { width:15px; height:15px; transition:transform var(--t-pop) var(--ease-out) }
.spx-more.is-open .ico { transform:rotate(180deg) }
@media (hover: hover) and (pointer: fine) {
  .spx-more:hover { background:var(--grey-100); color:var(--fg-1) }
}

/* Quota in a cell: the fraction, and a bar under it so a partner at 24/25 is visible without
   reading the digits. Right-aligned like every other number in the table. */
.spx-quotacell { display:inline-flex; flex-direction:column; align-items:flex-end; gap:5px; cursor:default }
.spx-quotacell b { font-size:14px; font-weight:700; font-variant-numeric:tabular-nums }
.spx-quotacell b span { color:var(--fg-3); font-weight:500; font-size:12px }
.spx-quotacell-track { width:56px; height:3px; border-radius:99px; background:var(--grey-200); overflow:hidden }
.spx-quotacell-track > div { height:100%; background:var(--teal-500); border-radius:99px; transition:width var(--t-modal) var(--ease-out) }

/* A suspended partner is dimmed but still fully legible: the row is the thing you act on to
   un-suspend them, so it must not read as disabled. */
.spx-rt tbody tr.is-suspended td:not(.act) { opacity:.62 }
@keyframes spx-rowgo { to { opacity:0; transform:translateX(24px) } }
.spx-rt tbody tr.is-going { animation:spx-rowgo .2s var(--ease-in-out) both }

.spx-iconsm--danger { border-color:rgba(216,52,131,.42); color:var(--brand-ink) }
@media (hover: hover) and (pointer: fine) {
  .spx-iconsm--danger:hover { background:var(--accent-soft); color:var(--brand-ink) }
}
/* --band-veryhigh, which is the token this design system already uses to mean "worst" —
   .spx-delta--worse and .spx-rt-del:hover both take it.
   Being honest about what this buys: #D83483 against the brand's #C42A75 is two shades of the
   same magenta, and nobody will read the difference as a warning. The palette has no red, and
   inventing one for a single button would put a colour on this page that appears nowhere else.
   So the safety here is NOT the hue — it is the dialog, which names the domains, counts the
   scans and says the quota is not refunded. The colour only agrees with it. */
.spx-cta--danger { background:var(--band-veryhigh) }
/* One disabled look for every primary button. The pager and the bulk bar each grew their own
   before this rule existed; theirs are gone, because three near-identical opacities is how a
   fourth one ends up slightly different. */
.spx-cta[disabled] { opacity:.45; cursor:default; filter:none }

/* Two alignments, not four. Partner is identity — you read it from its first letter, so it stays
   left. Every other column is a short token in a narrow box (a pill, a quota, a date, a pair of
   buttons), none long enough to need a rail, so all six centre with their headers. One rule per
   pair keeps the <th> and its <td> from drifting apart again.
   The history table has since been centred too (8 Aug 2026), in the base rules rather than
   here, so this block now agrees with .spx-rt instead of overriding it. */
.spx-rt--partners th, .spx-rt--partners td,
.spx-rt--partners th.num, .spx-rt--partners td.num,
.spx-rt--partners th.bd, .spx-rt--partners td.bd { text-align:center }
.spx-rt--partners th .spx-th,
.spx-rt--partners th.num .spx-th { justify-content:center; flex-direction:row }
.spx-rt--partners th:first-child, .spx-rt--partners td:first-child { text-align:left }
.spx-rt--partners th:first-child .spx-th { justify-content:flex-start }
.spx-rt--partners td.act .spx-rt-act { justify-content:center }
.spx-rt--partners .spx-quotacell { align-items:center }

/* The panel's own edges. 7px of cell padding is right BETWEEN columns and wrong at the two
   ends, where it leaves the partner's name almost touching the panel border and the last
   column's buttons doing the same on the right. The gutter matches the header's 14px. */
.spx-rt--partners th:first-child, .spx-rt--partners td:first-child { padding-left:16px }
.spx-rt th:last-child, .spx-rt td:last-child { padding-right:16px }

/* Partner table: seven columns, so they get their own widths rather than the history table's.
   Percentages, not pixels-plus-one-auto. The first column was `width:auto` and therefore ate
   every pixel the six fixed columns did not want — at 1216px that left a 560px name against
   cramped numbers, and the table read as one column with six footnotes. Percentages that sum to
   100 make the slack everybody's, so the proportions hold at any container width. */
.spx-rt--partners { min-width:900px }
   Action gets 18%: it holds a labelled button and three icon boxes, ~200px of content that
   cannot ellipsis, and at 13% it was 158px — the cluster spilled left over "Last active". */
.spx-rt--partners th:nth-child(1) { width:26% }   /* Partner     */
.spx-rt--partners th:nth-child(2) { width:10% }   /* Status      */
.spx-rt--partners th:nth-child(3) { width:10% }   /* Today       */
.spx-rt--partners th:nth-child(4) { width:11% }   /* This month  */
.spx-rt--partners th:nth-child(5) { width:12% }   /* Approved    */
.spx-rt--partners th:nth-child(6) { width:13% }   /* Last active */
.spx-rt--partners th.act          { width:18% }
.spx-rt--partners .spx-rt-dom { font-size:11px }

/* Drill-in table inside the modal: no sorting, no actions, no sticky column. */
.spx-modal-card--tall { flex-direction:column; max-width:680px }
.spx-rt--mini { min-width:0; table-layout:auto }
.spx-rt--mini th {
  padding:10px 8px; background:transparent; border-bottom:1px solid var(--border);
  color:var(--fg-3); font-size:10.5px; font-weight:700; letter-spacing:.08em; text-transform:uppercase;
}
.spx-rt--mini td { padding:11px 8px }
.spx-rt--mini b { font-size:14px; font-variant-numeric:tabular-nums }
/* table-layout:auto gave the domain every spare pixel and squeezed "08 Aug 2026" into a column
   narrow enough to break it onto three lines. Percentages instead, and nowrap on the date: a
   date that wraps is not a date any more, it is three numbers stacked. */
/* Every header centred, and every cell centred under it — except the domain, which is a chip and
   reads from the left edge of the row. One rule per column covering th and td together: they were
   inheriting two different ones (.spx-rt aligns th.num and td.num separately), so no column lined
   up with its own title. */
/* The .num and .bd selectors are repeated, and they have to be. `.spx-rt th.num` (line 948) is
   two classes and `.spx-rt--mini th` is one, so the base right-alignment outranked every override
   written the short way — the When header and its dates stayed right no matter what came after.
   Same trap as .cl-input over .cl-otpCodeFieldInput on the sign-in card: specificity is decided
   before source order, so a later rule with fewer classes never gets a turn.
   This is the shape .spx-rt--partners already uses a few lines down, for the same reason. */
.spx-rt--mini thead th, .spx-rt--mini tbody td,
.spx-rt--mini thead th.num, .spx-rt--mini tbody td.num,
.spx-rt--mini thead th.bd, .spx-rt--mini tbody td.bd,
.spx-rt--mini thead th.act, .spx-rt--mini tbody td.act { text-align:center }
/* Domain centred too, header and chip together. Left was the earlier call — a chip reads from
   the row's left edge — but a centred header over a left-aligned chip is the one combination
   that looks like a mistake rather than a choice, so the two move as a pair. */
.spx-rt--mini th:nth-child(1), .spx-rt--mini td:nth-child(1) { width:34% }
.spx-rt--mini th:nth-child(2), .spx-rt--mini td:nth-child(2) { width:13% }
.spx-rt--mini th:nth-child(3), .spx-rt--mini td:nth-child(3) { width:20% }
.spx-rt--mini th:nth-child(4), .spx-rt--mini td:nth-child(4) { width:21%; white-space:nowrap }
.spx-rt--mini th:nth-child(5), .spx-rt--mini td:nth-child(5) { width:12% }
/* The drill-in's own pager. Not .spx-pagi: that one is bound to the page's single table by
   partner-table.js and owns the #pagi-* ids, and a second set inside a dialog would be two
   controllers fighting over one name. */
.spx-minipagi {
  display:flex; align-items:center; justify-content:space-between; gap:12px;
  margin-top:14px; color:var(--fg-3); font-size:12.5px;
}
.spx-minipagi div { display:flex; align-items:center; gap:8px }
.spx-rt--mini .spx-rt-band { justify-content:center }
.spx-rt--mini td:nth-child(5) .spx-iconsm { display:inline-flex }
/* The hint under the cap field turns red when what is typed will be refused. .spx-form-note had
   this and .spx-hint did not, so the one place that needed to say "no" could only say it in the
   same grey it says everything else in. */
.spx-hint.is-bad { color:var(--band-veryhigh) }
.spx-hint.is-bad .ico { color:var(--band-veryhigh) }

/* One run-on sentence per row put the verb, the partner, the admin, the date and sometimes a
   reason on a single line, so no two rows lined up and the eye had nothing to scan down. Columns
   instead: a fixed verb rail, then who it was done to, then who did it and when on the right.
   The reason drops to its own line because only three of the five verbs carry one, and an
   optional tail mid-sentence is exactly what made the block look ragged. */
.spx-log {
  /* Fixed tracks, not `auto`: the header is its own grid, and an auto column sized to the word
     "By" would not line up with one sized to an email address.
     Country and applied date are columns of their own rather than a run-on under the company.
     Packed into the partner cell they were a 90-character line that ellipsised away, and they
     left the reason column — absent on approve and restore — holding the width on its own. Six
     columns each hold one fact, which is both easier to scan down and, with two more of them
     populated on every row, most of the empty middle gone. */
  display:grid; grid-template-columns:92px minmax(0,1.3fr) minmax(0,1fr) 92px 96px minmax(0,1.1fr) 148px;
  align-items:center; gap:12px;
  padding:11px 16px; border-top:1px solid var(--divider); font-size:13px; color:var(--fg-2);
}
.spx-log:first-child { border-top:0 }
.spx-log--head {
  /* 17px, not 16: the rows sit inside the panel's 1px border and the header sits outside it,
     so matching the rows' padding exactly would leave the two grids 1px out of true. */
  padding:0 17px 6px; border-top:0;
  color:var(--fg-3); font-size:10.5px; font-weight:700; letter-spacing:.08em; text-transform:uppercase;
}
/* The header cells are the same sort buttons the partners table uses, so the log gets its
   arrow, its focus ring and its pressed colour for free. `.spx-th` brings its own padding and
   42px min-height, which are sized for a <th> — here the row above already owns the spacing,
   and left in they would shunt every label 7px right of the column it names. */
.spx-log--head .spx-th { padding:0; min-height:0; letter-spacing:inherit; font-size:inherit }
.spx-log--head > :last-child { text-align:right; justify-content:flex-end }
/* Reason is the one column with no sort: ordering rows by the first letter of a free-text
   sentence answers nothing. It stays a plain label, which also tells you at a glance which
   headers are buttons. */
@media (max-width:860px) { .spx-log--head { display:none } }
/* Who it happened to: the company is what an admin recognises and the email is what they search
   for, so both stay, stacked. The country and the applied date used to trail this line and are
   now their own columns. */
.spx-log-who  { min-width:0; color:var(--fg-1) }
.spx-log-who b { display:block; font-size:13px; font-weight:600; overflow:hidden; text-overflow:ellipsis; white-space:nowrap }
.spx-log-who > span { display:block; margin-top:2px; font-size:11.5px; color:var(--fg-3); overflow:hidden; text-overflow:ellipsis; white-space:nowrap }
/* The two facts an admin weighed when they decided. Quiet: they are context for the row, not
   what the row is about. */
.spx-log-cc, .spx-log-date { min-width:0; color:var(--fg-3); font-size:12.5px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis }
.spx-log-date { font-variant-numeric:tabular-nums }
/* The company website, clickable here for the same reason it is clickable in the queue: the log
   is where you go to ask "why was this one let in", and the answer usually starts with opening
   the site. Same chip as the queue card so the two read as the same object. */
/* justify-self, because a grid item stretches to its track by default and the chips all came
   out the width of the column rather than the width of the domain in them. */
.spx-log-dom {
  display:inline-flex; justify-self:start; align-items:center; gap:6px; min-width:0; max-width:100%; padding:3px 9px;
  background:var(--grey-100); border:1px solid var(--border); border-radius:var(--r-pill);
  color:var(--fg-2); font-size:11.5px; font-family:ui-monospace,SFMono-Regular,Menlo,monospace;
  text-decoration:none; white-space:nowrap; overflow:hidden;
}
.spx-log-dom .ico { width:11px; height:11px; flex:none; color:var(--magenta-500) }
/* A rejected free-mail applicant has no company site — that absence IS the record, so the cell
   says so rather than being blank and looking like data we lost. Not a link, no chip. */
.spx-log-dom--none { background:none; border:0; padding:0; color:var(--fg-3) }
@media (hover: hover) and (pointer: fine) {
  .spx-log-dom:not(.spx-log-dom--none):hover { border-color:var(--magenta-500); color:var(--fg-1) }
}
/* Pinned to the last column: approve and restore emit no reason cell, and without this the
   admin column would slide left into the reason's slot on exactly those rows. */
.spx-log-meta { grid-column:7; text-align:right; color:var(--fg-2); font-size:12px; white-space:nowrap }
.spx-log-meta > span { display:block; margin-top:2px; color:var(--fg-3); font-size:11.5px }
/* The reason is a column, not a second line. Dropping it below the row left the middle of every
   row empty — doubly so on approve and restore, which have no reason at all — and made rows with
   one twice the height of rows without. As a column it fills the gap that was the complaint, and
   every row is the same height whether or not it carries one. */
/* Pinned to column 6. It is the LAST child in the markup — after the admin cell — so without
   this, auto-placement pushed it onto a second grid row and every row carrying a reason came
   out twice the height of one that did not. */
.spx-log-why {
  grid-column:6; grid-row:1;
  min-width:0; color:var(--fg-3); font-size:12.5px; line-height:1.45; font-style:italic;
  display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden;
}
/* Approve and restore carry no reason at all, and an empty cell under a column header reads as a
   value that went missing rather than one that never applied. A dash says which it is, and gives
   the column something to line up on down the whole log. */
.spx-log:not(.spx-log--head):not(:has(.spx-log-why))::after {
  content:"—"; grid-column:6; grid-row:1; color:var(--fg-3); font-size:12.5px;
}
.spx-log-v {
  justify-self:start;
  padding:2px 9px; border-radius:var(--r-pill); font-size:10.5px; font-weight:700;
  letter-spacing:.05em; text-transform:uppercase;
}
@media (max-width:860px) {
  .spx-log { grid-template-columns:1fr; gap:6px; align-items:start }
  .spx-log-meta { grid-column:1; text-align:left; white-space:normal }
  .spx-log-meta > span { display:inline; margin:0 0 0 6px }
  .spx-log-why { grid-column:1; grid-row:auto; -webkit-line-clamp:4 }
  .spx-log:not(.spx-log--head):not(:has(.spx-log-why))::after { grid-column:1; grid-row:auto }
  /* Each cell is its own line here — a grid item is blockified whatever `display` says, so the
     two cannot be rejoined without a wrapper element. The date just says what it is instead,
     since the column header that was carrying that word is hidden at this width. */
  .spx-log-date::before { content:"applied "; }
}
.spx-log-v--ok   { background:rgba(9,174,154,.16);  color:var(--band-verylow) }
.spx-log-v--no   { background:rgba(216,52,131,.16); color:var(--brand-ink) }
.spx-log-v--warn { background:rgba(246,199,5,.15);  color:var(--warn-ink) }
/* A decision just taken arrives rather than appearing: the log is at the far bottom of the
   page, so the entry has to survive the scroll down to it and still read as "this is the one
   you just made". Same rise as every other entrance on these pages. */
.spx-log--new { animation:spx-rise .28s var(--ease-out) both }
.spx-rt tbody tr.is-new { animation:spx-rise .28s var(--ease-out) both }

@media (max-width:620px) {
  /* The header is fixed and right-anchored; four items do not fit 375px. The email is the
     one that can go: it is identity, not navigation, and it is repeated on the page. */
  .spx-phead > .spx-pnote { display:none }
  .spx-phead-in { gap:8px; padding:9px 14px }
  .spx-pboard { padding-top:80px }
  /* Stacked, the row inherited align-items:center from the desktop rule, so each control
     shrank to its own content width and sat centred: four different left edges down a 375px
     screen. Stretch gives them one. */
  .spx-scanform, .spx-scanrow { flex-direction:column; align-items:stretch }
  .spx-scanrow select { flex:none }
  /* …and stretch was not enough, because the control the partner actually sees is not the
     <select> this rule names. partner-select.js hides it and draws .spx-ms-btn over it, and a
     <button> with width:auto is shrink-to-fit whatever `display` it is given — so the wrapper
     stretched to the full column while the button inside stayed at its own text width. Sector
     came out 170px and Country 181px, two ragged edges under a full-width domain box. Same
     `width:100%` the two other places this widget sits inside a form already apply
     (.spx-field, .spx-pcard); the min-width:150px floor goes with it, since a column narrower
     than that would otherwise push the button back out past the box above it.

     The pill radius goes with it. Side by side, 12px dropdowns beside a pill input read as a
     set; stacked in one column, three edges of one radius and two of another read as a slip. */
  .spx-scanrow .spx-ms { display:block; width:100% }
  .spx-scanrow .spx-ms-btn { width:100%; min-width:0; border-radius:var(--r-pill); padding:0 16px }

  /* The quota line was `width:max-content` — a fixed 336px whatever the screen, made of a track
     that cannot shrink (flex:0 0 170px) and a caption that can. Below ~355px it stopped fitting
     the column, and the only thing with any give was the sentence, so "1 of 25 scans used · 24
     left" collapsed into a 30px-wide stack of one word per line beside a full-length bar. It is
     fluid here instead: the track takes what the caption leaves, and if the two genuinely cannot
     share a line the caption drops to its own rather than being crushed onto six. */
  .spx-quota { width:auto; flex-wrap:wrap; gap:6px 10px }
  .spx-quota-track { flex:1 1 130px }
  .spx-quota > span { flex:0 0 auto }
  /* The admin's section tabs. `width:max-content` is right on a desktop row — a segmented
     control stretched across 1280px is a full-width track with four words and a hand's width of
     empty pill — but in a 357px column it left the group short of the search box directly under
     it, one more edge that does not line up with anything. Four labels split the width evenly
     here, and the taller cells are the point as much as the width: 5px of padding is a 26px tap
     target for the control that switches the whole page.

     Scoped to role="tablist" rather than .spx-seg, because the other user of this component is
     the report modal's two-item EN/ID switch — the exact case width:max-content was written for,
     and the one this rule must not reach. */
  .spx-seg[role="tablist"] { width:auto }
  .spx-seg[role="tablist"] button { flex:1; padding:9px 6px }

  /* Filter bars line up with their search box for the same reason. The two dropdowns sat at
     their own text width — "All countries" and "All statuses" are not the same length, so they
     ended at two different points, neither of them the one the box above them ended at. A full
     line each: side by side they are two 170px controls that still would not reach the edge, and
     the flex-basis is what puts each on its own line without turning the whole bar into a column
     (the history bar's Riskier toggle and refresh button share a line quite happily). */
  /* The search box takes the line, explicitly rather than by arithmetic. Its `flex:1 1 260px`
     happens to leave no room for a dropdown beside it on a phone, but "happens to" stops being
     true around 500px, where the basis fits twice and the bar reflowed into something nobody
     designed: search and a dropdown sharing line one, and the refresh button alone on line two. */
  .spx-filters .spx-search { flex:1 1 100% }
  .spx-filters .spx-ms { flex:1 1 100%; display:block }
  .spx-filters .spx-ms-btn { width:100%; min-width:0 }

  /* …except the history bar, which has two controls the admin bar does not: the Riskier toggle
     and the refresh button. Stacked full-width, its dropdown left them alone on a third line —
     a 133px pill and a 34px circle floating under a full-width box, which is the ragged edge
     this whole block exists to remove. They make one row instead: the band dropdown takes what
     the other two leave.

     Matched on :has(.spx-toggle) rather than on a new class, because the toggle IS the
     difference between the two bars — a marker class would be a second thing to keep in step
     with the markup, and this one cannot drift. (:has() is already load-bearing here; .spx-log
     uses it to decide a row's grid.)

     The industry dropdown is `hidden` until the table holds two sectors, and once it appears
     four controls will not share a phone's line. Not by the flex-basis below — by the floor
     under it: a flex item's automatic minimum is its min-content, and .spx-ms-btn's label is
     nowrap, so each dropdown refuses to go under the width of the words inside it. 117 for All
     bands, 154 for All industries, 136 for the toggle, 46 for the button and 30 of gaps is
     483px against 357. Truncating a filter's own name to force one row would be a worse answer
     than a second row.

     So four controls take two rows, deliberately and at every width rather than wherever the
     arithmetic happened to break. Left to itself the break lands in different places: at 393px
     the dropdowns pair off and the other two follow, but at 500px there is room for three and
     the refresh button was orphaned on a line of its own — the exact fault this block keeps
     fixing. The toggle claims a fresh line whenever a second dropdown is showing, so the split
     is always 2 and 2, and both rows end where the search box does. */
  .spx-filters:has(.spx-toggle) .spx-ms { flex:1 1 120px }
  /* Grows, so whichever row it lands on has a right edge. It used to be `0 1 auto`, which was
     right while the band dropdown was its only company — that one took the slack and the row
     was flush anyway. Leading a row, a 136px pill beside a 46px circle left half the width
     empty. */
  .spx-filters .spx-toggle { flex:1 1 auto }
  /* partner-scan.js sets `hidden` on the industry widget's root, so the sibling test is asking
     the only question that matters: are there two dropdowns on screen right now.
     The basis is the line MINUS the refresh button and the gap before it, not 100%. A full-width
     basis starts the new line correctly and then fills it, which pushes the button to a fourth
     row — the orphan again, one line further down. This asks for exactly the room that leaves
     the button its own. */
  .spx-filters:has(.spx-ms:not([hidden]) ~ .spx-ms:not([hidden])) .spx-toggle {
    flex:1 1 calc(100% - 56px);   /* 46px button + the 10px gap before it */
  }
  /* 46, not 34: on its own line a smaller circle reads as a different kind of control, but
     beside a 46px dropdown on the same row it just reads as misaligned. The 44px hit area
     :where(.spx-iconbtn)::after draws was already big enough — this is about the row's edge. */
  .spx-filters .spx-iconbtn { width:46px; height:46px }

  .spx-stats { grid-template-columns:1fr 1fr }
  .spx-stat--wide { grid-column:1 / -1 }
  /* The divider between tiles is vertical in a single row and has to become horizontal once
     they wrap, or the third tile carries a hairline down its left with nothing beside it.
     But "once they wrap" is per ROW, and turning every hairline horizontal missed that: in a
     two-column grid the second tile is still beside the first, so it wore a rule across its top
     that separated it from nothing — a hairline over the right half of the strip and none over
     the left.
     So parity decides, because which tile starts a row is a fact about the grid rather than
     about the tile: column one takes the rule above it, column two the hairline beside it.
     :nth-child counts DOM position, and that survives the admin's Suspended tile in both of its
     states — it sits in an odd slot whether it is `hidden` or showing, and when it shows it
     genuinely does start the second row. The wide tile is named rather than left to the parity:
     it spans both columns, so it always starts a row of its own whatever number it lands on. */
  .spx-stats .spx-stat:nth-child(odd)  { border-left:0; border-top:1px solid var(--divider); padding-left:0 }
  .spx-stats .spx-stat:nth-child(even) { border-left:1px solid var(--divider); border-top:0; padding-left:14px }
  .spx-stats .spx-stat:first-child { border-top:0 }
  .spx-stats .spx-stat.spx-stat--wide { border-left:0; border-top:1px solid var(--divider); padding-left:0 }

  /* The partner dashboard's three counts go across, not two-then-one. They are one sentence —
     how many prospects, how many this month, how many of them are the bad kind — and two above
     one broke it into a pair and an afterthought, with the third number sitting under a full-
     width hairline as if it belonged to the section below.
     Three across, so the dividers go back to being vertical and the strip reads as the single
     row it is on a desktop. The wide tile keeps its own line and the horizontal rule, which is
     correct there: it is a different kind of thing, a distribution rather than a count.

     #stats, not .spx-stats, because the admin strip is the same component with two tiles
     showing (Suspended is `hidden` until there is one). Three columns for two tiles is a third
     of the row left blank, which reads as a tile that failed to load. That one keeps its pair. */
  #stats { grid-template-columns:repeat(3,1fr) }
  #stats .spx-stat { border-top:0; border-left:1px solid var(--divider); padding:0 0 0 12px }
  #stats .spx-stat:first-child { border-left:0; padding-left:0 }
  /* 22px, down from 27. Three columns of ~119px is where a four-digit count would start
     colliding with the divider beside it, and the label under it is the thing that has to stay
     readable — it is carrying two lines now. */
  #stats .spx-stat-n { font-size:22px }
  #stats .spx-stat-l { font-size:10.5px; letter-spacing:.04em; margin-top:4px }
  #stats .spx-stat--wide { border-left:0; border-top:1px solid var(--divider); padding:16px 0 0 }
  .spx-table thead { display:none }
  .spx-table td { display:block; border-top:0; padding:4px 14px }
  .spx-table tbody tr { display:block; border-top:1px solid var(--divider); padding:12px 0 }
  .spx-row-actions { justify-content:flex-start; padding:10px 14px 0 }

  /* ---------- the two real tables become cards ----------
     `.spx-rt` is 940px wide and lives in an overflow:auto box, so on a 375px screen it was an
     eleven-column table you read by dragging sideways — for the primary content of the page.
     The mobile rules that existed targeted `.spx-table`, a class neither table carries, so they
     had never applied to anything.

     Each row becomes a card, and every cell grows the label its column header carried. The
     label is copied from the <th> at load by partner-table.js rather than typed into 154 cells,
     so renaming a column renames its mobile label too and the two cannot drift apart. */
  .spx-scroll { overflow:visible }
  .spx-rt, .spx-rt--partners { min-width:0; display:block }
  .spx-rt thead { display:none }
  .spx-rt tbody { display:block }
  /* 22px, not `auto`: an auto first column is sized by the widest thing in it, and the moment
     anything but the checkbox sat there the identity line started 180px from the card's edge. */
  .spx-rt tbody tr {
    display:grid; grid-template-columns:22px 1fr; gap:2px 10px;
    padding:14px 15px; border-top:1px solid var(--divider);
  }
  .spx-rt tbody tr:first-child { border-top:0 }
  .spx-rt tbody td { display:flex; align-items:center; justify-content:space-between; gap:12px;
                     grid-column:1 / -1; padding:4px 0; border-top:0; text-align:left }
  .spx-rt tbody td::before {
    content:attr(data-l); flex:none;
    color:var(--fg-3); font-size:10.5px; font-weight:700; letter-spacing:.07em; text-transform:uppercase;
  }
  /* The checkbox and the identity share the first line: a full-width row for one 15px box wastes
     the most valuable line on the card. */
  .spx-rt tbody td.selcol { grid-column:1; width:auto; padding:4px 0 10px }
  .spx-rt tbody td.selcol::before { content:none }
  .spx-rt tbody td.selcol + td { grid-column:2; padding-bottom:10px; display:block }
  .spx-rt tbody td.selcol + td::before { content:none }
  /* The partners table has no checkbox, so its identity cell is the first one — and as a normal
     labelled row the name truncated and the email pill ran off the card. It gets the same
     full-width treatment the history table's company cell gets. */
  .spx-rt--partners tbody tr { grid-template-columns:1fr }
  .spx-rt--partners tbody td:first-child { display:block; padding-bottom:10px }
  .spx-rt--partners tbody td:first-child::before { content:none }
  .spx-rt-idline { flex-wrap:wrap }
  .spx-rt-dom { max-width:100%; overflow:hidden; text-overflow:ellipsis }
  /* Score and band each take their own line, labelled, like every other cell on the card. They
     are one fact and it is tempting to set them side by side — but that costs the card its one
     reliable shape, "LABEL … value" down the left edge, and a reader scanning for Devices then
     has to notice that one line reads differently. Consistency is worth more than the pairing. */
  .spx-rt tbody td.sc { text-align:left }
  .spx-rt tbody td.sc .spx-rt-bar { display:none }
  .spx-rt tbody td.bd .spx-rt-band { justify-content:flex-end }
  /* Actions get the full width and sit at the end, where a thumb is. */
  .spx-rt tbody td.act { padding-top:12px }
  .spx-rt tbody td.act::before { content:none }
  .spx-rt tbody td.act .spx-rt-act { width:100%; justify-content:flex-start }
  .spx-rt tbody td.act .spx-rt-report { flex:1; justify-content:center; min-height:38px }
  .spx-rt tbody td.act .spx-iconsm, .spx-rt tbody td.act .spx-rt-btn { min-height:38px }
  .spx-rt .spx-iconsm { width:38px; height:38px }
  /* The sticky action column is a desktop affordance; stuck to nothing here it just shadows.
     Its left hairline and its own surface go with it: a column separator drawn across a card is
     a box around the last line, which is what the drill-in's Radar cell had been wearing. */
  .spx-rt th.act, .spx-rt td.act {
    position:static; box-shadow:none; border-left:0; border-bottom:0;
    padding-left:0; padding-right:0; background:transparent;
  }

  /* ---------- and the drill-in's mini table with them ----------
     The admin's "Scan history: <partner>" dialog was the one table the card rules never reached,
     and it looked broken rather than merely cramped: the domain chip was sliced in half, the
     score showed "82", the band showed "Very H" and the date "09 Au".

     Not a width the cards forgot to set — one they could not undo. .spx-rt--mini sizes its
     columns in percentages (34/13/20/21/12) so the desktop dialog lines up under its own
     headers, and those selectors are two classes deep where `.spx-rt tbody td` is one class and
     two elements. Specificity is settled before source order, so the percentages outlived the
     card rules entirely — and a `width:13%` on a grid item is 13% of the CARD, not of a column
     that no longer exists. Every cell was clipped to the fraction its column used to hold.

     So the widths are released at this width, and the alignment with them: the same rule keeps
     four of the five columns centred, which on a card puts each value in the middle of the row
     instead of at the end of the "LABEL … value" line every other card here reads by. */
  .spx-rt--mini tbody td:nth-child(1), .spx-rt--mini tbody td:nth-child(2),
  .spx-rt--mini tbody td:nth-child(3), .spx-rt--mini tbody td:nth-child(4),
  .spx-rt--mini tbody td:nth-child(5) { width:auto }
  .spx-rt--mini tbody td, .spx-rt--mini tbody td.num,
  .spx-rt--mini tbody td.bd, .spx-rt--mini tbody td.act { text-align:right }
  /* No checkbox column in this table, so the 22px track the history cards reserve for one is
     22px of nothing down the left of every card. */
  .spx-rt--mini tbody tr { grid-template-columns:1fr }
  /* The domain is the card's title, so it gets the whole first line and wraps rather than
     ellipsing: a truncated domain is the one value here an admin cannot act on. */
  .spx-rt--mini tbody td:nth-child(1) { display:block; padding-bottom:8px; text-align:left }
  .spx-rt--mini tbody td:nth-child(1)::before { content:none }
  .spx-rt--mini tbody td:nth-child(1) .spx-rt-dom { max-width:100%; white-space:normal }
  .spx-rt--mini .spx-rt-band { justify-content:flex-end }
  /* The history cards drop the Action label because that cell is a full-width row of verbs that
     says what it is. This one is a single 38px icon, and unlabelled at the end of a card it is a
     button with no name — so it keeps the label and stays on the card's one shape. */
  .spx-rt--mini tbody td.act::before { content:attr(data-l) }
  .spx-rt--mini tbody td.act { justify-content:space-between }
  /* The "Loading…", "No scans yet." and error rows are a single colSpan=5 cell. colSpan means
     nothing to a grid, and they carry no data-l, so they arrive as one unlabelled line — which
     is right, but right-aligned it reads as a stray value. */
  .spx-rt--mini tbody tr:only-child td:only-child { text-align:left; color:var(--fg-2) }

  /* 28px of dialog padding either side of 26px of body padding is a fifth of a 375px screen
     spent on margin. The card also takes the height back: at 86vh a ten-row drill-in ends in a
     scroll box with the pager below the fold. */
  .spx-modal { padding:14px }
  .spx-modal-card { max-height:90vh }
  .spx-modal-body { padding:20px 16px }
  .spx-modal-head { padding:12px 14px; gap:10px }
  /* The pager under it stacks for the same reason .spx-pagi does. */
  .spx-minipagi { flex-direction:column; align-items:stretch; gap:10px }
  .spx-minipagi div { justify-content:space-between }

  /* The pager stacks, and its two halves each take a full line rather than squeezing. */
  .spx-pagi { flex-direction:column; align-items:stretch; gap:12px }
  .spx-pagi-nav { justify-content:space-between }

  /* An application card is three things stacked, not a row: the two verbs need thumb-sized
     targets and they cannot have them side by side with a company name. */
  .spx-appcard { flex-wrap:wrap }
  .spx-appmeta { flex:1 1 100% }
  .spx-appcard .spx-ghost { flex:1 }

  /* Result modal: the gauge and the facts stop competing for one line. */
  .spx-res-score { flex:1 1 100%; order:-1 }
  .spx-res-foot .spx-cta, .spx-res-foot .spx-ghost { flex:1; justify-content:center }
}

/* The history filter bar's three-in-a-row needs 322px and a 320px screen has 284. Measured: at
   360px and up the band dropdown, the Riskier toggle and the refresh button share a line with
   the row ending exactly where the search box above it does; below that the button alone falls
   to a line of its own, which is the orphan the row was arranged to avoid. So the dropdown
   takes the full line here and the two controls that belong together stay together. */
@media (max-width:359px) {
  .spx-filters:has(.spx-toggle) .spx-ms { flex:1 1 100% }
}
