/* Live Tennis API — the design system for EVERY public surface this app serves:
   the affiliate portal, the public /program page, the operator console AND the developer blog.

   SOURCE OF TRUTH: the tennis repo's app/services/design_tokens.py. The palette below is that
   module's palette, not a look-alike — bg #0e0e0e, panel #141414, hairline #2a2a2a, text #e4e2e1,
   muted #84967e, accent #00ff41. Hard edges (0 on structural panels, 6px on interactive controls),
   flat fills — no gradients, no glows, no blur, no shadows. Boundaries come from background-value
   shifts, with a #2a2a2a hairline where a shift alone would be ambiguous.
   Space Grotesk (display) / Inter (body) / JetBrains Mono (code), self-hosted below.

   WHY EVERY SURFACE LOADS THIS ONE FILE: the blog used to be two standalone <!doctype html>
   documents that extended nothing, so a base-template migration swept the whole repo except them
   and they drifted into a third palette (#080b11/#3ee07a) with fonts that never loaded. A surface
   that does not inherit cannot be kept in agreement. New pages extend a base template that links
   this stylesheet; they do not ship their own <style> block.

   CONTRAST CONTRACT (do not regress): every text/background pair here is >= 4.5:1, or >= 3.0:1
   where the text is >= 18.66px bold / >= 24px. --dim is 3.30:1 on --bg and is therefore
   LARGE-TEXT ONLY (>= 18px); small dim text uses --faint or --muted. */

/* ------------------------------------------------------------------ fonts ---- */
@font-face{font-family:'Space Grotesk';src:url('/static/fonts/space-grotesk-latin-400.woff2') format('woff2');font-weight:400;font-display:swap}
@font-face{font-family:'Space Grotesk';src:url('/static/fonts/space-grotesk-latin-500.woff2') format('woff2');font-weight:500;font-display:swap}
@font-face{font-family:'Space Grotesk';src:url('/static/fonts/space-grotesk-latin-600.woff2') format('woff2');font-weight:600;font-display:swap}
@font-face{font-family:'Space Grotesk';src:url('/static/fonts/space-grotesk-latin-700.woff2') format('woff2');font-weight:700;font-display:swap}
@font-face{font-family:'Inter';src:url('/static/fonts/inter-latin-400.woff2') format('woff2');font-weight:400;font-display:swap}
@font-face{font-family:'Inter';src:url('/static/fonts/inter-latin-500.woff2') format('woff2');font-weight:500;font-display:swap}
@font-face{font-family:'Inter';src:url('/static/fonts/inter-latin-600.woff2') format('woff2');font-weight:600;font-display:swap}
@font-face{font-family:'Inter';src:url('/static/fonts/inter-latin-700.woff2') format('woff2');font-weight:700;font-display:swap}
@font-face{font-family:'JetBrains Mono';src:url('/static/fonts/jetbrains-mono-latin-400.woff2') format('woff2');font-weight:400;font-display:swap}
@font-face{font-family:'JetBrains Mono';src:url('/static/fonts/jetbrains-mono-latin-500.woff2') format('woff2');font-weight:500;font-display:swap}
@font-face{font-family:'JetBrains Mono';src:url('/static/fonts/jetbrains-mono-latin-600.woff2') format('woff2');font-weight:600;font-display:swap}

/* ------------------------------------------------------------------ tokens ---- */
:root{
  /* surfaces — the board's charcoal ladder; boundaries are value shifts */
  --bg:#0e0e0e; --bg2:#0e0e0e; --surface:#141414; --surface2:#1c1b1b; --surface3:#201f1f;
  --line:#2a2a2a; --line2:#2a2a2a;
  /* input wells are darker than the panel they sit in; their border must clear WCAG 1.4.11 (3:1) */
  --input-bg:#0e0e0e; --input-line:#686868;   /* 3.46:1 on the well, 3.31:1 on --surface, 3.08:1 on --surface2 */

  /* text — every value below is verified against --bg and --surface */
  --ink:#e4e2e1;            /* 14.95:1 on --bg · 14.11:1 on --surface */
  --muted:#84967e;          /*  6.00:1 on --bg ·  5.66:1 on --surface */
  --faint:#7a8a75;          /*  5.20:1 on --bg ·  4.91:1 on --surface — small dim text */
  --dim:#5a6675;            /*  3.30:1 on --bg — LARGE TEXT ONLY (>=18px). Never for body copy. */

  --accent:#00ff41; --accent2:#00ff41; --accent-hover:#72ff70; --accent-ink:#003907;
  --blue:#06b6d4; --amber:#fca300; --danger:#ffb4ab;

  --sans:"Inter",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
  --disp:"Space Grotesk","Inter",sans-serif;
  --mono:"JetBrains Mono",ui-monospace,SFMono-Regular,Menlo,monospace;

  /* hard edges: 6px on interactive controls, 0 on structural panels */
  --r-sm:6px; --r:6px; --r-lg:0; --r-xl:0;
  --tap:44px;               /* minimum touch target */
  --s1:6px; --s2:10px; --s3:14px; --s4:20px; --s5:28px; --s6:40px;
  color-scheme:dark;
}
*{box-sizing:border-box;margin:0;padding:0}
html{scroll-behavior:smooth}
body{background:var(--bg);color:var(--ink);font:400 15.5px/1.62 var(--sans);-webkit-font-smoothing:antialiased;overflow-x:hidden}
a{color:inherit;text-decoration:none}
img,svg{max-width:100%}
/* No border-radius here: the outline follows each element's OWN radius (0 on panels, 6px on
   controls). Setting one here reshapes whatever is focused — e.g. an autofocused input. */
:focus-visible{outline:2px solid var(--accent);outline-offset:3px}

/* ------------------------------------------------------------------ type ---- */
h1,h2,h3{font-family:var(--disp);letter-spacing:-.02em;line-height:1.1;font-weight:700}
h1{font-size:26px}
h2{font-size:13px;color:var(--muted);text-transform:uppercase;letter-spacing:.11em;font-weight:600;font-family:var(--sans);margin:var(--s6) 0 var(--s3)}
.muted{color:var(--muted)} .faint{color:var(--faint)} .small{font-size:13px}
.mono{font-family:var(--mono)}
.g{color:var(--accent)}
.eyebrow{font-family:var(--mono);font-weight:500;font-size:12px;letter-spacing:.18em;text-transform:uppercase;color:var(--accent);display:inline-flex;align-items:center;gap:10px}
.eyebrow::before{content:"";width:22px;height:1px;background:var(--accent)}

/* ------------------------------------------------------------------ buttons ---- */
/* 44px is the touch floor and the site-wide CTA height — min-height AND padding both carry it. */
.btn,button.btn,a.btn{display:inline-flex;align-items:center;justify-content:center;gap:9px;font:600 14.5px/1 var(--sans);
  padding:14px 20px;min-height:var(--tap);border-radius:var(--r);border:1px solid transparent;cursor:pointer;white-space:nowrap;
  transition:transform .14s,border-color .14s,background .14s}
.btn:active{transform:translateY(1px)}
.btn-primary{background:var(--accent);color:var(--accent-ink)}
.btn-primary:hover{background:var(--accent-hover)}
.btn-ghost{background:var(--surface2);border-color:var(--line);color:var(--ink)}
.btn-ghost:hover{border-color:var(--accent)}
.btn-sm{padding:0 14px;min-height:var(--tap);font-size:13px;border-radius:var(--r)}
.btn.full{width:100%}
.btn:disabled,.btn[disabled]{opacity:.45;cursor:not-allowed;transform:none}

/* Default bare <button> = ghost (kills the grey-vs-green per-file drift in the operator console). */
button:not(.btn){display:inline-flex;align-items:center;justify-content:center;gap:8px;font:600 13.5px/1 var(--sans);
  padding:0 14px;min-height:var(--tap);border-radius:var(--r);border:1px solid var(--line);background:var(--surface2);
  color:var(--ink);cursor:pointer;transition:border-color .14s,transform .14s,background .14s}
button:not(.btn):hover{border-color:var(--accent)}
button:not(.btn):active{transform:translateY(1px)}
button:not(.btn):disabled{opacity:.45;cursor:not-allowed}
button.go,button.primary{background:var(--accent);color:var(--accent-ink);border-color:transparent}
button.go:hover,button.primary:hover{background:var(--accent-hover);border-color:transparent}
button.warn{background:var(--surface2);border-color:var(--amber);color:var(--amber)}

/* ------------------------------------------------------------------ cards ---- */
.card{background:var(--surface);border:1px solid var(--line);border-radius:var(--r-lg);padding:22px}
.card--accent{background:var(--surface);border-color:var(--accent)}
.card--flush{padding:6px 18px}
.card--row{display:flex;align-items:center;justify-content:space-between;gap:16px}
.grid{display:grid;gap:var(--s3)}
/* Grid items must be allowed to shrink below their content's intrinsic width, or a long
   unwrappable child (a <pre> line) sizes the track to min-content and blows the page out
   past the viewport — measured 1529px-wide cards on /assets (UIUX-001, WCAG 1.4.10 Reflow). */
.grid>*{min-width:0}

/* ------------------------------------------------------------------ asset kit gallery ---- */
/* /assets downloadable creative kit (UIUX-002). Previews sit in a well that scrolls at natural
   size — a 728x90 banner shown at 312px was unreadable (UIUX-007) — except the >=1000px-wide
   pieces (OG cards, logo), which are legible scaled down and would otherwise force a long
   sideways scroll. The scrollbar stays VISIBLE here on purpose: it is the affordance. */
.asset-grid{display:grid;grid-template-columns:1fr 1fr;gap:var(--s3);margin-bottom:var(--s4)}
.asset-card{background:var(--surface);border:1px solid var(--line);border-radius:var(--r-lg);
  padding:16px;display:flex;flex-direction:column;gap:12px;min-width:0}
.asset-prev{overflow-x:auto;background:var(--bg);border:1px solid var(--line);padding:10px}
.asset-prev img{max-width:none;display:block}
.asset-prev-fit img{max-width:100%;height:auto}
.asset-meta{display:flex;justify-content:space-between;align-items:baseline;gap:10px;
  font-size:13px;color:var(--muted);flex-wrap:wrap}
.asset-actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:auto}
.codeblock.wrap{white-space:pre-wrap;overflow-wrap:anywhere}
@media (max-width:900px){
  .asset-grid{grid-template-columns:1fr}
}

/* ------------------------------------------------------------------ alerts ---- */
.alert{border-radius:var(--r-lg);padding:11px 15px;font-size:14px;display:flex;align-items:center;gap:10px;
  border:1px solid var(--line);background:var(--surface2);color:var(--ink)}
.alert--success{background:#122012;border-color:var(--accent);color:var(--accent)}
.alert--warn{background:#201802;border-color:var(--amber);color:var(--amber)}
.alert--info{background:#0a1c20;border-color:var(--blue);color:var(--blue)}
.alert .grow{flex:1}

/* ------------------------------------------------------------------ stats ---- */
.stats{display:grid;grid-template-columns:repeat(4,1fr);gap:var(--s3)}
.stat{background:var(--surface);border:1px solid var(--line);border-radius:var(--r-lg);padding:16px 18px}
.stat b{display:block;font-family:var(--disp);font-size:23px;letter-spacing:-.02em;line-height:1.15}
.stat.acc b{color:var(--accent)}
.stat span{color:var(--muted);font-size:12.5px}

/* ------------------------------------------------------------------ pills ---- */
.pill{font:500 11px/1 var(--mono);padding:5px 9px;border-radius:0;border:1px solid var(--line);color:var(--muted);
  display:inline-flex;align-items:center;text-transform:uppercase;letter-spacing:.04em}
.pill.g,.pill--active,.pill--paid,.pill--ok{color:var(--accent);border-color:var(--accent)}
.pill--held,.pill--flagged,.pill--restricted,.pill--canceled,.pill--clawed_back{color:var(--amber);border-color:var(--amber)}
.pill--suspended,.pill--failed{color:var(--danger);border-color:var(--danger)}
/* Listing-coverage states: live is the win (accent); submitted/ready are in-flight (blue);
   needs-account is blocked-on-us (amber); queued stays neutral. One calm signal per state. */
.pill--live{color:var(--accent);border-color:var(--accent)}
.pill--submitted,.pill--ready{color:var(--blue);border-color:var(--blue)}
.pill--needs_account{color:var(--amber);border-color:var(--amber)}

/* ------------------------------------------------------------------ tables ---- */
.table-wrap{border:1px solid var(--line);border-radius:var(--r-lg);overflow-x:auto;background:var(--surface)}
table{width:100%;border-collapse:collapse;font-size:13.8px}
thead th{position:sticky;top:0}
th{text-align:left;color:var(--muted);font-weight:600;font-size:12.5px;background:var(--surface2)}
th,td{padding:10px 14px;border-bottom:1px solid var(--line2);white-space:nowrap}
tbody tr:last-child td{border-bottom:none}
tbody tr:hover td{background:var(--surface2)}
td.num,th.num{text-align:right;font-family:var(--mono)}
td.pos{color:var(--accent)} td.neg{color:var(--amber)}
.empty{color:var(--muted);text-align:center;padding:22px}
code{font-family:var(--mono);color:var(--blue);font-size:.92em}

/* ------------------------------------------------------------------ forms ---- */
label{display:block;font-size:12.5px;color:var(--muted);margin:0 0 6px;font-weight:500}
.field{margin-top:var(--s3)}
/* 16px is load-bearing: iOS Safari zooms the viewport on focus of any input under 16px and,
   with no maximum-scale set (deliberate — pinch-zoom must stay available), never zooms back. */
input,select,textarea{width:100%;padding:11px 13px;background:var(--input-bg);border:1px solid var(--input-line);border-radius:var(--r);
  color:var(--ink);font:400 16px/1.3 var(--sans)}
input::placeholder{color:var(--muted)}
input:focus,select:focus,textarea:focus{outline:2px solid var(--accent);outline-offset:1px;border-color:transparent}
input:disabled,select:disabled{opacity:.55;cursor:not-allowed;background:var(--surface)}
/* flex:none is load-bearing — as a flex item inside .check the box was being SHRUNK below its
   declared size (measured 13x17), taking the only control that gates account creation under the
   24px WCAG 2.5.8 floor. The clickable target is the whole <label> row (>=44px tall), which is
   what a user actually presses. */
input[type=checkbox]{width:24px;height:24px;flex:none;accent-color:var(--accent)}
.check{display:flex;align-items:center;gap:12px;margin-top:var(--s3);font-weight:400;
  min-height:var(--tap);padding:8px 0;cursor:pointer}
.check span{color:var(--muted);font-size:13px;line-height:1.5}
.inline-form{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
.inline-form input,.inline-form select{width:auto;min-width:120px}

/* copy box + code block */
.copy{display:flex;gap:8px}
.copy input{font-family:var(--mono);color:var(--accent);font-size:16px}
.codeblock{overflow:auto;background:#0a0a0a;border:1px solid var(--line);
  border-radius:var(--r-lg);padding:13px 15px;font:400 12.8px/1.6 var(--mono);color:var(--blue)}
.copied{color:var(--accent)!important;border-color:var(--accent)!important}

/* ------------------------------------------------------------------ app chrome ---- */
/* Skip link (UIUX-009): the first focusable element on every public page. Parked off-screen
   until keyboard focus, then a high-contrast block ABOVE the sticky header (z-index 30 > 20). */
.skip-link{position:absolute;left:-9999px;top:0;z-index:30;background:var(--accent);
  color:var(--accent-ink);font:600 14px/1 var(--sans);padding:15px 18px;border-radius:0}
.skip-link:focus{left:0}

.app-header{position:sticky;top:0;z-index:20;background:var(--bg);border-bottom:1px solid var(--line)}
.app-header .in{max-width:960px;margin:0 auto;padding:0 20px;height:62px;display:flex;align-items:center;gap:16px}
.brand{display:flex;align-items:center;gap:10px;font-family:var(--disp);font-weight:700;font-size:16px;color:var(--ink);white-space:nowrap;flex:none}
.brand .dot{width:24px;height:24px;border-radius:0;background:url("/static/logo-mark.svg") center/contain no-repeat;flex:none}
.nav{margin-left:auto;display:flex;gap:2px;align-items:center;overflow-x:auto;scrollbar-width:none}
.nav::-webkit-scrollbar{display:none}
.nav a{color:var(--muted);font-size:14px;font-weight:500;padding:8px 12px;border-radius:0;white-space:nowrap;transition:color .12s,background .12s}
.nav a:hover{color:var(--ink);background:var(--surface2)}
.nav a[aria-current=page]{color:var(--accent);background:var(--surface2)}
.nav form{display:inline-flex}

/* Public site chrome (blog + landing pages) reuses .app-header/.brand/.nav. The sign-in CTA sits
   OUTSIDE .nav so it stays pinned right whatever the nav does on a narrow phone. */
.site-signin{flex:none;margin-left:4px}
/* Public nav links are primary navigation → full 44px touch rows (UIUX-007). Scoped to
   .site-header so the operator console's fixed 62px header is untouched. */
.site-header .nav a{display:inline-flex;align-items:center;min-height:var(--tap)}
/* ≤860px the public header wraps to two rows: brand + Sign-in on row 1, the nav full-width on
   row 2, WRAPPING onto as many lines as it needs (UIUX-001). At 390px the old single-row layout
   squeezed .nav to 66px against 524px of links — the active tab was off-screen behind a hidden
   scrollbar with zero affordance that more existed. Wrapping keeps every destination visible;
   a scroll+fade edge was rejected because this design system is flat-fill only (no gradients). */
@media(max-width:860px){
  .site-header .in{height:auto;min-height:62px;flex-wrap:wrap;padding-top:9px;padding-bottom:9px}
  .site-header .nav{order:3;flex-basis:100%;margin-left:0;flex-wrap:wrap;overflow-x:visible}
  .site-header .site-signin{margin-left:auto}
}

/* The logged-in AFFILIATE portal header (.portal-header, aff_base.html) gets the same treatment
   as .site-header above (UIUX-004): 44px touch rows, and on a narrow phone the nav WRAPS instead
   of hiding 6 of its 8 destinations behind a scrollbar-less horizontal scroller (measured at
   390px: nav scrollWidth 690 vs 190 visible — Links/Assets/Payouts/Leaderboard/Settings/Sign-out
   all undiscoverable). Scoped to .portal-header so the operator console's fixed 62px header and
   the 3-item customer portal header stay untouched. */
.portal-header .nav a{display:inline-flex;align-items:center;min-height:var(--tap)}
@media(max-width:860px){
  .portal-header .in{height:auto;min-height:62px;flex-wrap:wrap;padding-top:9px;padding-bottom:9px}
  .portal-header .nav{order:3;flex-basis:100%;margin-left:0;flex-wrap:wrap;overflow-x:visible}
  /* Wrapped, the 8-row nav makes the header ~197px tall (measured at 390px). Sticky, that would
     permanently consume 23% of an 844px phone — so on mobile this header scrolls away with the
     page instead. Desktop keeps .app-header's sticky single row. */
  .portal-header{position:static}
}

/* Public site footer — links every product/surface from every public page. Tokens only. */
.site-foot{border-top:1px solid var(--line);background:var(--bg)}
.site-foot .in{max-width:960px;margin:0 auto;padding:var(--s6) 20px var(--s5)}
.site-foot-cols{display:grid;grid-template-columns:repeat(4,1fr);gap:var(--s5) var(--s4)}
.site-foot-col{display:flex;flex-direction:column}
.site-foot-h{font-size:12px;text-transform:uppercase;letter-spacing:.1em;color:var(--muted);
  font-weight:600;font-family:var(--sans);margin-bottom:6px}
.site-foot-col a{color:var(--faint);font-size:14px;min-height:var(--tap);display:inline-flex;
  align-items:center;transition:color .12s}
.site-foot-col a:hover{color:var(--accent)}
.site-foot-base{color:var(--muted);font-size:13px;line-height:1.6;margin-top:var(--s5);
  padding-top:var(--s3);border-top:1px solid var(--line)}

.app-main{max-width:960px;margin:0 auto;padding:32px 20px 90px}
.app-main.narrow{max-width:820px}
.page-head{margin-bottom:var(--s4)}
.page-head .eyebrow{margin-bottom:10px}
.page-head p{color:var(--muted);margin-top:6px;font-size:14.5px}
.flash-stack{display:grid;gap:10px;margin-bottom:var(--s4)}

/* auth shell (no nav) */
.auth-shell{min-height:100vh;display:flex;flex-direction:column;align-items:center;padding:5vh 20px 40px}
/* min-height carries the 44px touch floor: a bare link is a target too, and these are the only
   navigation a logged-out visitor has. Same reason the .auth-foot links below are 44px rows. */
.auth-brand{display:inline-flex;align-items:center;gap:10px;font-family:var(--disp);font-weight:700;font-size:17px;
  margin-bottom:var(--s5);min-height:var(--tap);padding:0 4px}
.auth-brand .dot{width:26px;height:26px;border-radius:0;background:url("/static/logo-mark.svg") center/contain no-repeat;flex:none}
.authcard{width:100%;max-width:400px}
.authcard h1{font-size:24px;text-align:left}
/* The card's own heading is an <h2> (one <h1> per document — the pitch owns it on /signup) but
   must keep reading as the form's title. */
.authcard .form-title{font-family:var(--disp);font-weight:700;letter-spacing:-.02em;line-height:1.2;
  font-size:20px;color:var(--ink);text-transform:none}
.auth-foot{color:var(--muted);margin-top:var(--s4);font-size:14px;
  display:flex;flex-wrap:wrap;align-items:center;justify-content:center;gap:0 8px}
.auth-foot a{color:var(--accent);text-decoration:underline;text-underline-offset:2px;
  display:inline-flex;align-items:center;min-height:var(--tap);padding:0 2px}
/* 'Continue with Google' — a neutral board-charcoal button carrying the OFFICIAL 4-colour G mark
   (inline SVG; CSP img-src is 'self' data: only). Sits above the email form with an 'or' rule. */
.btn-google{background:var(--surface2);border-color:var(--line);color:var(--ink)}
.btn-google:hover{border-color:var(--accent)}
.btn-google .g-mark{flex:none}
/* 'Continue with GitHub' — same neutral board-charcoal button; the OFFICIAL Octocat mark is inline
   SVG rendered monochrome via currentColor so it reads cleanly on the dark board kit. */
.btn-github{background:var(--surface2);border-color:var(--line);color:var(--ink)}
.btn-github:hover{border-color:var(--accent)}
.btn-github .gh-mark{flex:none}
.auth-or{display:flex;align-items:center;gap:12px;margin:16px 0;
  color:var(--muted);font-size:13px;text-transform:lowercase}
.auth-or::before,.auth-or::after{content:"";flex:1;height:1px;background:var(--line)}

/* signup conversion landing (two-column on desktop) */
.signup-grid{width:100%;max-width:1000px;display:grid;grid-template-columns:1.05fr .95fr;gap:44px;align-items:center}
.signup-pitch h1{font-size:clamp(30px,4vw,42px);margin-top:16px;text-wrap:balance}
.signup-pitch p{color:var(--muted);margin-top:16px;font-size:16px;max-width:44ch}
.trust{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:26px}
.trust div{background:var(--surface);border:1px solid var(--line);border-radius:var(--r-lg);padding:14px 16px}
.trust b{display:block;font-family:var(--disp);font-size:20px;color:var(--accent);letter-spacing:-.02em}
.trust span{color:var(--muted);font-size:13px}

/* /program — the PRODUCT card ("what you'd promote"). Describes the API, not the offer. */
/* .auth-shell is a centred flex COLUMN, so a flex item is sized to its content, not to the shell.
   Without width:100% the endpoint table's min-content width dragged the whole pitch to 447px inside
   a 390px phone. Pinning the item to the line box makes the table (not the page) do the scrolling. */
.signup-pitch{width:100%;min-width:0}
.signup-pitch .card h3{font-size:17.5px;line-height:1.25}
.prod-points{margin:12px 0 2px;padding-left:20px;font-size:14.5px}
.prod-points li{margin-bottom:5px}
/* display:block + overflow-x keeps the endpoint paths (which must not wrap mid-path) inside the
   card on a 390px phone instead of widening the page. */
.prod-endpoints{display:block;width:auto;max-width:100%;overflow-x:auto;margin-top:14px;font-size:13px}
.prod-endpoints caption{caption-side:top;text-align:left;padding:0 0 6px}
.prod-endpoints td{border-bottom:1px solid var(--line);padding:8px 14px 8px 0}
.prod-endpoints tr:last-child td{border-bottom:none}
.prod-links{display:flex;flex-wrap:wrap;gap:10px;margin-top:16px}
/* A row of CTAs in running copy (e.g. the history one-off packages) — was an inline style. */
.cta-row{display:flex;flex-wrap:wrap;align-items:center;gap:10px}

/* ------------------------------------------------------------------ operator layout ---- */
.ops-wrap{max-width:1080px;margin:0 auto;padding:26px 22px 80px}
.ops-top{display:flex;align-items:center;justify-content:space-between;gap:12px 14px;
  flex-wrap:wrap;margin-bottom:var(--s3)}
.ops-top h1{font-size:21px}
.ops-top h1 .hint{font-family:var(--sans);font-size:14px;font-weight:500;color:var(--muted);letter-spacing:0}
.ctl{display:grid;grid-template-columns:1.2fr 1fr .7fr .7fr .8fr .9fr 1.3fr;align-items:center;gap:10px;padding:11px 14px}
.ctl-head{color:var(--muted);font-size:11.5px;text-transform:uppercase;letter-spacing:.08em;background:var(--surface2);
  border:1px solid var(--line);border-bottom:none;border-radius:0}
.ctl-row{border:1px solid var(--line);border-top:none;background:var(--surface)}
.ctl-row:last-of-type{border-radius:0}
.kv-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:12px}
.kv{background:var(--surface);border:1px solid var(--line);border-radius:var(--r-lg);padding:13px 15px}
.kv b{display:block;font-family:var(--disp);font-size:17px}
.kv span{color:var(--muted);font-size:11.5px}
.actions-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:14px}

/* ------------------------------------------------------------------ operator console kit ---- */
/* ONE shared chrome + component set for the Growth Console, spanning BOTH blueprints that
   extend ops_base.html (dashboard + admin). Before this the admin templates leaned on a
   parallel vocabulary — .cards/.big/.tbl/.ok/.subnav — that portal.css never defined, so
   they rendered as unstyled full-width blocks and bare tables that overflowed a 390px phone
   by up to ~1200px (WCAG 1.4.10 Reflow). Everything the console needs now lives here in tokens. */

/* Operator header: like .site-header/.portal-header, the nav WRAPS on a phone instead of
   scrolling "Sign out" off-screen behind a hidden scrollbar (measured: nav clipped at 390px). */
.ops-header .nav a{display:inline-flex;align-items:center;min-height:var(--tap)}

/* Section header — a display-weight heading with a light rule, the console's section rhythm.
   (The global h2 is a small uppercase form label; ops sections want a touch more presence.) */
.ops-h{font-family:var(--sans);font-size:12.5px;font-weight:600;text-transform:uppercase;
  letter-spacing:.1em;color:var(--muted);margin:var(--s5) 0 var(--s3);display:flex;
  align-items:baseline;gap:10px}
.ops-h .count{font-family:var(--mono);color:var(--faint);font-size:12px;letter-spacing:.04em}
.ops-h .hint{margin-left:auto;font-weight:400;text-transform:none;letter-spacing:0;font-size:12.5px;color:var(--faint)}
.ops-h-action{margin-left:auto}

/* Admin sub-nav (Finance · Customers · Usage · Analytics · Affiliates) — real tab targets. */
.subnav{display:flex;flex-wrap:wrap;gap:2px;margin:0 0 var(--s4);border-bottom:1px solid var(--line)}
.subnav a{display:inline-flex;align-items:center;min-height:var(--tap);padding:0 14px;color:var(--muted);
  font-size:14px;font-weight:500;border-bottom:2px solid transparent;margin-bottom:-1px}
.subnav a:hover{color:var(--ink)}
.subnav a[aria-current=page]{color:var(--accent);border-bottom-color:var(--accent)}

/* Back link above a page title (was a bare small <a> in the admin templates). */
.ops-back{display:inline-flex;align-items:center;gap:6px;min-height:36px;color:var(--muted);font-size:13.5px}
.ops-back:hover{color:var(--accent)}

/* Secondary caption — the "excluded/coverage" honesty notes. Muted, small, readable width. */
.caption{color:var(--muted);font-size:12.8px;line-height:1.55;margin-top:8px;max-width:82ch}
.caption em{color:var(--faint);font-style:italic}

/* Stat cards — THE stat-card component (supersedes .cards/.card+.big and .kv/.stat):
   label = tiny uppercase mono muted · value = large mono number · sub = muted line.
   Equal heights from the grid; a tight rhythm, not a stretched full-width block. */
.statgrid{display:grid;grid-template-columns:repeat(4,1fr);gap:var(--s3)}
.statgrid.cols-3{grid-template-columns:repeat(3,1fr)}
.statgrid.cols-2{grid-template-columns:repeat(2,1fr)}
.statgrid.cols-6{grid-template-columns:repeat(6,1fr)}
@media(max-width:920px){.statgrid.cols-6{grid-template-columns:repeat(3,1fr)}}
.statcard{background:var(--surface);border:1px solid var(--line);border-radius:var(--r-lg);
  padding:15px 16px;display:flex;flex-direction:column;gap:6px;min-width:0}
.statcard .label{font:600 11px/1.1 var(--mono);letter-spacing:.09em;text-transform:uppercase;color:var(--muted)}
.statcard .val{font:500 25px/1.05 var(--mono);letter-spacing:-.01em;color:var(--ink);
  font-variant-numeric:tabular-nums}
.statcard.acc .val{color:var(--accent)}
.statcard .val.off{color:var(--muted);font-size:16px;letter-spacing:0}
.statcard .sub{font-size:12.5px;color:var(--muted);line-height:1.45}
.statcard .val .unit{font-size:.5em;color:var(--muted);margin-left:2px;letter-spacing:0}
.statgrid+.table-wrap{margin-top:var(--s3)}

/* Status pill extensions (reuse .pill). "not connected" is a limitation, not an error → muted;
   n/a → faint. Keeps channel/health states in the same chip language as the rest of the console. */
.pill--off{color:var(--muted);border-color:var(--line)}
.pill--na{color:var(--faint);border-color:var(--line)}

/* Table refinements shared by every console table. Wrap long free-text cells instead of forcing
   the table (and the page) wide; truncate long ids with a hover title. */
td.wrap,th.wrap{white-space:normal;min-width:16ch}
.trunc{display:inline-block;max-width:20ch;overflow:hidden;text-overflow:ellipsis;
  white-space:nowrap;vertical-align:bottom}

/* Vertical list of items separated by hairlines (research packets, manual queue). */
.stack{display:flex;flex-direction:column}
.stack-item{padding:12px 0;border-top:1px solid var(--line)}
.stack-item:first-child{border-top:none;padding-top:2px}
.stack-item b{font-weight:600}
.stack-item .pill{margin-left:8px}

/* Listings per-channel action cluster — url input + status select + Set, aligned on one line;
   the packet disclosure drops tidy onto its own full-width row beneath (was sprawling). */
.listing-ctl{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
.listing-ctl input[name=submitted_url]{flex:1 1 130px;min-width:120px;max-width:190px}
.listing-ctl select{width:auto;min-width:104px;flex:none}
.listing-ctl button{flex:none}
.packet{flex-basis:100%;margin-top:2px}
.packet>summary{cursor:pointer;color:var(--accent);font-size:13px;min-height:36px;
  display:inline-flex;align-items:center;gap:6px;list-style:none}
.packet>summary::-webkit-details-marker{display:none}
.packet>summary::before{content:"▸";display:inline-block;transition:transform .12s}
.packet[open]>summary::before{transform:rotate(90deg)}
.packet .codeblock{margin-top:6px;white-space:pre-wrap}

/* Recruitment funnel — proportional horizontal bars, hard edges, tokenised (was inline styles
   with a 6px radius that broke the flat/hard-edge rule). The bar WIDTH stays an inline % since
   it is per-row data; everything else is here. */
.funnel{display:flex;flex-direction:column;gap:10px}
.funnel-row{display:grid;grid-template-columns:118px 1fr 40px;align-items:center;gap:12px}
.funnel-row .stage{font-size:13.5px;text-transform:capitalize;color:var(--ink)}
.funnel-track{height:12px;background:var(--surface2);border:1px solid var(--line);overflow:hidden}
.funnel-bar{height:100%;background:var(--accent);min-width:2px}
.funnel-row .count{font-family:var(--mono);font-size:13.5px;text-align:right;color:var(--muted)}
.funnel-row.hot .count{color:var(--accent)}

/* Analytics "Refresh now" — the button and its explanatory caption on one aligned row. */
.refresh-row{display:flex;align-items:center;gap:14px;flex-wrap:wrap;margin:var(--s3) 0 0}
.refresh-row .caption{margin-top:0;flex:1 1 240px}

/* ------------------------------------------------------------------ responsive ---- */
@media(max-width:720px){
  .signup-grid{grid-template-columns:1fr;gap:30px;max-width:440px}
  .kv-grid{grid-template-columns:1fr 1fr}
  .actions-grid{grid-template-columns:1fr}
  .ctl,.ctl-head{grid-template-columns:1fr 1fr;gap:8px}
  /* Operator header wraps its nav onto a second full-width row (mirrors .portal-header): the
     single-row scroller hid "Sign out" past the right edge at 390px. Non-sticky on mobile so
     the wrapped nav doesn't permanently eat the top of a short phone viewport. */
  .ops-header .in{height:auto;min-height:62px;flex-wrap:wrap;padding-top:9px;padding-bottom:9px}
  .ops-header .nav{order:3;flex-basis:100%;margin-left:0;flex-wrap:wrap;overflow-x:visible}
  .ops-header{position:static}
  .statgrid,.statgrid.cols-3,.statgrid.cols-2{grid-template-columns:1fr 1fr}
  /* Narrower cards: shrink the big number so a grouped figure (1,626,483) stays on one line
     instead of breaking across digits. */
  .statcard .val{font-size:20px}
}
@media(max-width:420px){
  .funnel-row{grid-template-columns:96px 1fr 34px;gap:8px}
}
@media(max-width:640px){
  .site-foot-cols{grid-template-columns:1fr 1fr}
}
@media(max-width:560px){
  .stats{grid-template-columns:1fr 1fr}
  .trust{grid-template-columns:1fr}
  .app-main{padding:24px 16px 90px}
}
/* ------------------------------------------------------------------ developer blog ---- */
/* The long-form reading surface (blog.livetennisapi.com). Tokens only — see the header comment
   for why this lives here and not in a per-template <style> block.

   MEASURE: 580px of wrap gives ~75 characters per line at the desktop body size. The previous
   720px ran ~95 CPL, well past the 45-75 comfortable range for continuous prose.
   FLUID TYPE: every level is a clamp, so a 34px desktop headline is a 26px phone headline
   instead of one byte-identical size at both. */
.blog-wrap{max-width:580px;margin:0 auto;padding:40px 22px 88px}
.blog-wrap--index{max-width:700px}
.blog-wrap h1{font-family:var(--disp);font-weight:700;letter-spacing:-.02em;line-height:1.15;
  font-size:clamp(26px,calc(21.5px + 1.15vw),34px)}
.blog-back{display:inline-flex;align-items:center;gap:8px;min-height:var(--tap);
  color:var(--muted);font-size:14px}
.blog-back:hover{color:var(--accent)}
.blog-sub{color:var(--muted);margin-top:10px;font-size:clamp(15px,calc(14.2px + .2vw),17px);line-height:1.55}

/* index */
/* Product cross-link strip on the blog index — wraps on narrow screens. */
.prod-strip{display:flex;flex-wrap:wrap;gap:10px;margin-top:var(--s4)}
.post-list{margin-top:var(--s5)}
.post-item{border-bottom:1px solid var(--line);padding:4px 0 16px}
.post-item h2{font-family:var(--disp);font-weight:700;text-transform:none;letter-spacing:-.01em;
  color:var(--ink);line-height:1.25;margin:0;font-size:clamp(18px,calc(17.1px + .23vw),21px)}
.post-item h2 a{display:flex;align-items:center;min-height:var(--tap);color:var(--ink)}
.post-item h2 a:hover{color:var(--accent)}
.post-item p{color:var(--muted);margin:2px 0 0;font-size:15px}

/* article body (markdown-rendered) */
.blog-article{margin-top:var(--s4);font-size:clamp(16px,calc(15.4px + .11vw),17px);line-height:1.7}
.blog-article h2,.blog-article h3{font-family:var(--disp);font-weight:700;color:var(--ink);
  text-transform:none;letter-spacing:-.01em;line-height:1.25}
.blog-article h2{font-size:clamp(20px,calc(17.8px + .56vw),24px);margin:var(--s5) 0 var(--s2)}
.blog-article h3{font-size:clamp(17px,calc(15.9px + .28vw),19px);margin:var(--s4) 0 var(--s1)}
.blog-article p,.blog-article li{color:var(--ink);margin:0 0 var(--s3)}
.blog-article ul,.blog-article ol{padding-left:22px;margin:0 0 var(--s3)}
.blog-article li{margin-bottom:4px}
/* overflow-wrap is the fix for a bare URL in prose: with the default `normal` a long unbroken
   href pushed the whole document ~7px wider than a 390px viewport. */
.blog-article a{color:var(--accent);text-decoration:underline;text-underline-offset:2px;
  overflow-wrap:anywhere}
/* A .btn inside an article keeps its BUTTON styling, not the article link color/underline. Without
   this, a landing-page CTA (.btn-primary) inherits green text on a green fill = invisible label. */
.blog-article a.btn{text-decoration:none}
.blog-article a.btn-primary{color:var(--accent-ink)}
.blog-article a.btn-ghost{color:var(--ink)}
.blog-article pre{background:var(--surface);border:1px solid var(--line);border-radius:var(--r-lg);
  padding:14px 16px;overflow-x:auto;margin:0 0 var(--s3)}
.blog-article code{font-family:var(--mono);font-size:13.5px;line-height:1.6}
.blog-article :not(pre)>code{color:var(--blue)}
/* display:block + overflow-x lets a wide table scroll INSIDE the column instead of widening the
   page (a 3-column table measured 378.7px inside a 346px phone column). */
.blog-article table{display:block;width:auto;max-width:100%;overflow-x:auto;border-collapse:collapse;
  margin:0 0 var(--s3);font-size:14px}
.blog-article th,.blog-article td{border:1px solid var(--line);padding:7px 11px;white-space:normal}
.blog-article th{background:var(--surface2);color:var(--muted)}
.blog-article img{height:auto;border-radius:var(--r-lg)}
.blog-article blockquote{border-left:2px solid var(--line);padding-left:14px;color:var(--muted);margin:0 0 var(--s3)}

/* article footer */
.post-foot{margin-top:var(--s6);padding-top:var(--s4);border-top:1px solid var(--line);
  color:var(--muted);font-size:14.5px;line-height:1.65}
.post-foot a{color:var(--accent);text-decoration:underline;text-underline-offset:2px}
.post-foot-links{display:flex;flex-wrap:wrap;align-items:center;gap:0 18px;margin-top:2px}
.post-foot-links a{display:inline-flex;align-items:center;min-height:var(--tap)}

/* ------------------------------------------------------------------ products hub ---- */
/* /products — the discoverability hub. Wider wrap than the reading surfaces so the card grid has
   room; tokens only, cards reuse the board's flat-fill + hairline + hover-to-accent language. */
.blog-wrap--hub{max-width:960px}
.prod-hub{text-align:center;padding:var(--s4) 0 var(--s6);border-bottom:1px solid var(--line)}
.prod-hub .eyebrow{justify-content:center}
.prod-hub h1{font-size:clamp(28px,calc(23px + 1.6vw),40px);margin-top:14px;text-wrap:balance}
/* Hub section headings ("What people build on it"): a display step below the h1 (UIUX-004) —
   without this they fell through to the global h2, a 13px uppercase muted form label. */
.prod-hub h2{font-family:var(--disp);font-weight:700;letter-spacing:-.02em;line-height:1.15;
  font-size:clamp(22px,calc(20px + .6vw),26px);color:var(--ink);text-transform:none;
  margin:14px 0 0;text-wrap:balance}
.prod-hub p{color:var(--muted);margin:14px auto 0;line-height:1.55;max-width:60ch;
  font-size:clamp(15px,calc(14.2px + .2vw),17px)}
/* A hub section that FOLLOWS a card grid (e.g. "What people build on it"): separator above
   instead of below — was an inline style on the section (UIUX-012). */
.prod-hub--follow{border-top:1px solid var(--line);border-bottom:none;margin-top:var(--s6)}
.prod-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:var(--s3);margin-top:var(--s5)}
.prod-card{display:flex;flex-direction:column;background:var(--surface);border:1px solid var(--line);
  border-radius:var(--r-lg);padding:22px 22px 24px;transition:border-color .14s}
.prod-card:hover{border-color:var(--accent)}
/* the flagship product reads first: a persistent accent hairline (hover language, made permanent). */
.prod-card--featured{border-color:var(--accent)}
/* h2 for the product cards (sections under the page h1), h3 for the integration-example cards
   (they sit under the "What people build on it" h2) — same look, honest outline (UIUX-011). */
.prod-card h2,.prod-card h3{font-family:var(--disp);font-weight:700;text-transform:none;letter-spacing:-.02em;
  color:var(--ink);font-size:19px;line-height:1.2;margin:12px 0 0}
.prod-card p{color:var(--muted);font-size:14.5px;line-height:1.55;margin:8px 0 0}
.prod-card-cta{display:flex;flex-wrap:wrap;gap:10px;margin-top:auto;padding-top:18px}
@media(max-width:640px){.prod-grid{grid-template-columns:1fr}}

/* ------------------------------------------------------------------ pricing tiers ---- */
/* History pricing on a phone (UIUX-002): ≤640px the 3-column comparison table restacks as
   per-tier cards (name, price, key rows, Buy CTA) — at 390px the table kept "Get Business"
   past the right edge of a hidden horizontal scrollbar. Desktop keeps the table; only one
   rendering is in the layout (and the accessibility tree) at a time. Tokens only. */
.tier-cards{display:none}
@media(max-width:640px){
  .pricing-table{display:none}
  .tier-cards{display:grid;gap:var(--s3);margin:0 0 var(--s3)}
  .tier-card{background:var(--surface);border:1px solid var(--line);border-radius:var(--r-lg);
    padding:16px 18px 18px}
  .tier-card h3{display:flex;align-items:baseline;justify-content:space-between;gap:10px;margin:0}
  .tier-price{font-family:var(--disp);font-weight:700;font-size:20px;letter-spacing:-.02em;
    color:var(--accent);white-space:nowrap}
  .tier-card .prod-points{margin:10px 0 14px}
  .tier-card .prod-points li{color:var(--muted);font-size:14px;margin-bottom:4px}
  .tier-card .btn,.tier-card form{width:100%}
}

@media(prefers-reduced-motion:reduce){html{scroll-behavior:auto}*{transition:none!important}}
