/* =========================================================================
   xSERVA — design system
   =========================================================================

   Rebuilt from scratch on the GSAP motion architecture. Three rules govern
   everything below and are worth stating once rather than repeating:

   1. COLOUR MEANS STATE. The chrome is achromatic — near-black surfaces,
      white-alpha borders, grey text. Cyan is the brand's single accent and
      marks interaction and emphasis; green / amber / red mean ok / warn /
      deny and are used for nothing else. A card does not turn green because
      green is pretty.

   2. MOTION IS OWNED BY GSAP, NOT BY CSS. This file sets resting states and
      hover/focus affordances only. Anything scroll-driven, sequenced or
      scrubbed lives in motion.js. Two systems animating the same property is
      how an element ends up fighting itself mid-scroll, and it is invisible
      until someone scrolls fast on a slow phone.

   3. EVERY EFFECT DEGRADES TO READABLE. No rule here may hide content that
      JavaScript is expected to reveal. The one file allowed to set opacity:0
      on real content is motion.css, and only after motion.js has marked the
      document as enhanced.
   ========================================================================= */

/* ---------------------------------------------------------------- fonts --
   Self-hosted: the CSP forbids third-party origins, and a webfont from a CDN
   would be a render-blocking request to a host the policy blocks anyway.
   Variable, so the whole weight range costs one 48 KB file instead of four
   static cuts. font-display:swap — text in a fallback face is worth more than
   no text at all, and the metric overrides below keep the swap from shifting
   layout. */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-display: swap;
  font-weight: 100 900;
  src: url('/assets/fonts/inter-var.woff2') format('woff2');
}
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-display: swap;
  font-weight: 100 800;
  src: url('/assets/fonts/jetbrains-mono-var.woff2') format('woff2');
}

/* The fallback face, described in Inter's metrics. Without this the swap from
   system-ui to Inter reflows every paragraph on the page — the classic
   late-webfont layout shift. With it, the fallback occupies the same vertical
   space and the swap is invisible. */
@font-face {
  font-family: 'Inter Fallback';
  src: local('Arial');
  ascent-override: 90.2%;
  descent-override: 22.48%;
  line-gap-override: 0%;
  size-adjust: 107.4%;
}

/* --------------------------------------------------------------- tokens -- */
:root {
  /* Surfaces. A single flat --bg gave every card the same relationship to the
     page no matter how deep it sat. Four steps is enough to say "this floats
     above that" without inventing a colour per component. The blue undertone
     (hue 222) is what keeps the near-black from reading as dead grey under a
     cyan accent. */
  --bg:            #05060a;
  --surface-1:     #0a0c12;
  --surface-2:     #10131b;
  --surface-3:     #161a24;
  --surface-inset: #07080d;

  /* Borders and glass, as alpha over whatever is beneath, so a card sitting on
     surface-2 and the same card on the page background both read correctly. */
  --glass:          rgba(255, 255, 255, 0.035);
  --glass-strong:   rgba(255, 255, 255, 0.06);
  --glass-border:   rgba(255, 255, 255, 0.09);
  --glass-border-2: rgba(255, 255, 255, 0.14);

  /* Type. Three greys, and they are a hierarchy, not a palette: primary for
     what is being said, secondary for prose, muted for metadata that must be
     available without competing. */
  --text-primary:   #f2f4f8;
  --text-secondary: #a2a9b8;
  --text-muted:     #6f7688;

  /* Brand. Cyan carries interaction and emphasis; violet exists only inside
     gradients and glows, never as a text or state colour — two accents that
     both mean "important" mean neither does. */
  --accent:        #4fd1ff;
  --accent-cyan:   #4fd1ff;   /* retained name: used across 50 pages */
  --accent-strong: #7ae0ff;
  --accent-dim:    rgba(79, 209, 255, 0.14);
  --accent-line:   rgba(79, 209, 255, 0.34);
  --accent-violet: #8b6bff;
  --gradient-accent: linear-gradient(135deg, var(--accent-cyan), var(--accent-violet));

  /* State. These four are the only saturated hues allowed outside the accent,
     and each means exactly one thing everywhere on the site. */
  --state-ok:      #34d399;
  --state-warn:    #fbbf24;
  --state-deny:    #f87171;
  --state-ok-bg:   rgba(52, 211, 153, 0.12);
  --state-warn-bg: rgba(251, 191, 36, 0.12);
  --state-deny-bg: rgba(248, 113, 113, 0.12);

  /* Type scale — fluid, one ratio, no per-page overrides. Every step is
     clamp(min, preferred, max) so the scale compresses on a phone instead of
     each heading needing its own media query. */
  --step--1: clamp(0.82rem, 0.79rem + 0.13vw, 0.89rem);
  --step-0:  clamp(1rem,    0.96rem + 0.18vw, 1.09rem);
  --step-1:  clamp(1.2rem,  1.12rem + 0.38vw, 1.42rem);
  --step-2:  clamp(1.44rem, 1.29rem + 0.72vw, 1.85rem);
  --step-3:  clamp(1.73rem, 1.47rem + 1.25vw, 2.4rem);
  --step-4:  clamp(2.07rem, 1.65rem + 2.05vw, 3.12rem);
  --step-5:  clamp(2.49rem, 1.8rem + 3.3vw,  4.05rem);

  --font-sans: 'Inter', 'Inter Fallback', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  --font-mono: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, Consolas, monospace;

  /* Space — a 4px-based scale. The old names are kept because pages use them
     inline; the values are re-tuned so vertical rhythm is a multiple of one
     number rather than four unrelated ones. */
  --space-1: 8px;
  --space-2: 16px;
  --space-3: 24px;
  --space-4: 40px;
  --space-5: 64px;
  --space-6: clamp(64px, 9vw, 112px);

  --radius-sm: 8px;
  --radius-md: 14px;
  --radius-lg: 24px;
  --radius-xl: 32px;

  --max-width: 1200px;
  --measure: 68ch;

  /* Motion tokens. motion.js reads these same durations so a hover transition
     written here and a GSAP tween written there share one vocabulary — the
     alternative is CSS easing out in 200ms while GSAP eases in over 600ms and
     nobody being able to say why the page feels uneven. */
  --dur-fast: 0.18s;
  --dur-base: 0.32s;
  --dur-slow: 0.6s;
  --ease-out: cubic-bezier(0.22, 1, 0.36, 1);
  --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);

  /* Shadows. Dark UI needs a lifted rim as much as a cast shadow — a pure drop
     shadow on near-black is invisible, so each of these pairs an inset hairline
     with the shadow that does the actual lifting. */
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.04);
  --shadow-md: 0 12px 32px -12px rgba(0, 0, 0, 0.7), inset 0 1px 0 rgba(255, 255, 255, 0.05);
  --shadow-lg: 0 32px 64px -24px rgba(0, 0, 0, 0.85), inset 0 1px 0 rgba(255, 255, 255, 0.06);
  --glow-accent: 0 0 0 1px var(--accent-line), 0 8px 40px -8px rgba(79, 209, 255, 0.28);

  /* The height the sticky header actually occupies. Anything positioning
     against the top of the viewport — an anchor target, a sticky table header,
     a ScrollTrigger start — has to know this number, and hardcoding it in three
     places is how they drift apart. Measured, not guessed. */
  --nav-h: 72px;
}

@media (max-width: 640px) {
  :root { --nav-h: 64px; }
}

/* ----------------------------------------------------------------- base -- */
*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; padding: 0; }

html {
  scroll-behavior: smooth;
  -webkit-text-size-adjust: 100%;
}
/* Smooth in-page jumps are scroll-coupled travel across the whole viewport,
   which is exactly what the preference is for. */
:root[data-motion="reduced"] { scroll-behavior: auto; }

@media (prefers-reduced-motion: reduce) {
  :root:not([data-motion="full"]) { scroll-behavior: auto; }
}

/* Every anchor target on the site sat behind the sticky header once jumped to:
   the skip link landed the first 72px of <main> under the nav, and a deep link
   to a glossary term put the term itself out of sight. scroll-margin-top tells
   the browser to stop short of the target by the height of what is covering it. */
:target,
[id] { scroll-margin-top: calc(var(--nav-h) + var(--space-2)); }

body {
  background: var(--bg);
  color: var(--text-primary);
  font-family: var(--font-sans);
  font-size: var(--step-0);
  line-height: 1.65;
  font-feature-settings: 'cv05' 1, 'cv08' 1, 'ss03' 1, 'tnum' 0;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;

  /* `clip`, NOT `hidden`, and the distinction is load-bearing.
     `overflow-x: hidden` makes the element a scroll container on both axes —
     the spec computes the other axis from visible to auto. On body that traps
     every position:sticky descendant and interferes with ScrollTrigger's
     pinning, which is the single most common reason a pinned section ships
     broken. `clip` contains horizontal overflow identically but creates no
     scroll container, so sticky and pin both keep working. */
  overflow-x: clip;
}

/* The ambient field. Fixed, behind everything, and deliberately not animated:
   a full-viewport gradient repainting on scroll is the most expensive thing a
   page like this can do, and it buys nothing a static field does not. The grain
   is what stops the large flat gradients from banding on an 8-bit panel. */
body::before {
  content: '';
  position: fixed;
  inset: 0;
  z-index: -2;
  pointer-events: none;
  background:
    radial-gradient(ellipse 80% 55% at 15% -5%,  rgba(79, 209, 255, 0.14), transparent 60%),
    radial-gradient(ellipse 70% 50% at 88% 12%,  rgba(139, 107, 255, 0.12), transparent 62%),
    radial-gradient(ellipse 90% 60% at 50% 105%, rgba(79, 209, 255, 0.06), transparent 70%);
}
body::after {
  content: '';
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  opacity: 0.4;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='120' height='120' filter='url(%23n)' opacity='0.035'/%3E%3C/svg%3E");
}

a { color: inherit; text-decoration: none; }
img, svg, video, canvas { max-width: 100%; display: block; }
button, input, select, textarea { font: inherit; color: inherit; }

::selection { background: rgba(79, 209, 255, 0.28); color: #fff; }

/* A visible focus ring on every interactive thing, not only the ones that got
   one by accident. Keyboard users are the cheapest accessibility win there is. */
a:focus-visible,
button:focus-visible,
[tabindex]:focus-visible,
summary:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: var(--radius-sm);
}

/* ------------------------------------------------------------ typography -- */
h1, h2, h3, h4 {
  font-weight: 680;
  line-height: 1.12;
  letter-spacing: -0.022em;
  color: var(--text-primary);
  text-wrap: balance;
}
h1 { font-size: var(--step-5); letter-spacing: -0.032em; }
h2 { font-size: var(--step-3); margin-bottom: var(--space-2); letter-spacing: -0.026em; }
h3 { font-size: var(--step-1); margin-bottom: var(--space-1); }
h4 { font-size: var(--step-0); margin-bottom: var(--space-1); }

/* A heading that must be an h2 for the document outline but must read as a card
   title. Outline level and type size are different questions, and tying them
   together is what pushes authors into skipping a level to get the size they
   want. */
.h-card { font-size: var(--step-1); margin-bottom: var(--space-1); letter-spacing: -0.018em; }

p {
  color: var(--text-secondary);
  max-width: var(--measure);
  text-wrap: pretty;
}
p + p { margin-top: var(--space-2); }

strong, b { color: var(--text-primary); font-weight: 640; }

code, kbd, samp, pre { font-family: var(--font-mono); font-size: 0.92em; }
code {
  background: var(--surface-2);
  border: 1px solid var(--glass-border);
  border-radius: 5px;
  padding: 0.1em 0.4em;
  color: var(--accent-strong);
}
pre { overflow-x: auto; }
pre code { background: none; border: 0; padding: 0; color: inherit; }

.lead {
  font-size: var(--step-1);
  line-height: 1.55;
  color: var(--text-secondary);
  max-width: 62ch;
  font-weight: 380;
}

.eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: 0.76rem;
  font-weight: 620;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--accent);
  margin-bottom: var(--space-2);
  font-feature-settings: 'tnum' 1;
}
/* The rule beside an eyebrow is what makes it read as a section marker rather
   than as small shouty text. Drawn, not typed, so it never reaches a reader. */
.eyebrow::before {
  content: '';
  width: 22px;
  height: 1px;
  background: linear-gradient(90deg, transparent, var(--accent));
  flex: none;
}
/* On a narrow screen a long eyebrow wraps to two lines, and the rule then sits
   beside the first line only — it reads as a stray dash rather than as a
   section marker. Below this width the uppercase tracking is doing the job on
   its own. */
@media (max-width: 520px) {
  .eyebrow::before { display: none; }
}

/* Body links inside prose. Underlined on hover rather than always, so a
   paragraph does not turn into a fence — but never colour-only, because colour
   alone fails anyone who cannot distinguish it. */
main p a:not(.btn):not(.glass-card),
main li a:not(.btn):not(.glass-card):not(.footer-col a) {
  color: var(--accent);
  text-decoration: underline;
  text-decoration-color: rgba(79, 209, 255, 0.3);
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
  transition: text-decoration-color var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out);
}
main p a:not(.btn):not(.glass-card):hover { text-decoration-color: var(--accent); color: var(--accent-strong); }

/* ---------------------------------------------------------------- layout -- */
.container {
  width: 100%;
  max-width: var(--max-width);
  margin-inline: auto;
  padding-inline: var(--space-3);
}

.section { padding-block: var(--space-6); }
.section-head { margin-bottom: var(--space-4); max-width: 78ch; }
.section-head p { max-width: var(--measure); }

/* minmax(0, 1fr) rather than a bare 1fr: a bare 1fr is minmax(auto, 1fr), so a
   track holding a wide table or a long unbroken string cannot shrink below its
   content and forces the whole row wider than the viewport. */
.grid { display: grid; gap: var(--space-3); }
.grid-cols-2 { grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr)); }
.grid-cols-3 { grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr)); }
.grid > * { min-width: 0; }

/* A two-column row where the columns are genuinely unequal — prose beside art,
   or a hero beside a visual. auto-fit would make them the same width, which is
   the wrong answer for a 62ch paragraph next to a 420px diagram. */
@media (min-width: 900px) {
  .grid-split { grid-template-columns: minmax(0, 1.05fr) minmax(0, 0.95fr); gap: var(--space-5); align-items: center; }
}

.stack > * + * { margin-top: var(--space-2); }
.cluster { display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: center; }

/* ----------------------------------------------------------------- cards -- */
.glass-card {
  position: relative;
  background:
    linear-gradient(180deg, rgba(255, 255, 255, 0.045), rgba(255, 255, 255, 0.012));
  border: 1px solid var(--glass-border);
  border-radius: var(--radius-lg);
  padding: var(--space-3);
  box-shadow: var(--shadow-sm);
  isolation: isolate;
  transition: border-color var(--dur-base) var(--ease-out),
              background-color var(--dur-base) var(--ease-out),
              box-shadow var(--dur-base) var(--ease-out),
              transform var(--dur-base) var(--ease-out);
}

/* The top hairline. A card on a dark page reads as a hole unless its upper edge
   catches light, and a bright 1px line does more for perceived quality here
   than any amount of shadow. */
.glass-card::before {
  content: '';
  position: absolute;
  inset: 0 0 auto 0;
  height: 1px;
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
  background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.16) 22%, rgba(255, 255, 255, 0.16) 78%, transparent);
  pointer-events: none;
  z-index: 2;
}

a.glass-card { display: block; color: inherit; }
a.glass-card:hover,
a.glass-card:focus-visible {
  border-color: var(--accent-line);
  background-color: rgba(255, 255, 255, 0.02);
  box-shadow: var(--shadow-md);
  transform: translateY(-3px);
}
a.glass-card:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
a.glass-card h3 { transition: color var(--dur-fast) var(--ease-out); }
a.glass-card:hover h3 { color: var(--accent); }

/* The 3px hover lift is the one piece of movement in a card. It goes in
   reduced mode; the border warm-up and the shadow stay, so the card still
   answers the pointer. */
:root[data-motion="reduced"] a.glass-card:hover,
:root[data-motion="reduced"] a.glass-card:focus-visible { transform: none; }
@media (prefers-reduced-motion: reduce) {
  :root:not([data-motion="full"]) a.glass-card:hover,
  :root:not([data-motion="full"]) a.glass-card:focus-visible { transform: none; }
}

/* The pointer spotlight. motion.js writes --mx/--my as percentages with a
   quickTo so the follow is damped rather than snapping to the cursor; this is
   the layer it lights. Pure paint, no layout, and it costs nothing when the
   custom properties are unset because the gradient is then centred and
   invisible at zero opacity. */
.spotlight::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: radial-gradient(360px circle at var(--mx, 50%) var(--my, 50%), rgba(79, 209, 255, 0.1), transparent 42%);
  opacity: 0;
  transition: opacity var(--dur-base) var(--ease-out);
  pointer-events: none;
  z-index: 1;
}
.spotlight:hover::after,
.spotlight:focus-within::after { opacity: 1; }

/* Card content must sit above both the hairline and the spotlight wash. */
.glass-card > * { position: relative; z-index: 2; }

.feature-card svg,
.protocol-card svg,
.chan-card > svg {
  width: 30px;
  height: 30px;
  color: var(--accent);
  margin-bottom: var(--space-2);
}

/* --------------------------------------------------------------- buttons -- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-1);
  padding: 12px 22px;
  min-height: 44px;             /* the touch-target floor, not a look */
  border-radius: 999px;
  font-weight: 600;
  font-size: 0.95rem;
  letter-spacing: -0.01em;
  border: 1px solid var(--glass-border-2);
  background: var(--glass);
  color: var(--text-primary);
  cursor: pointer;
  white-space: nowrap;
  transition: border-color var(--dur-fast) var(--ease-out),
              background-color var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out),
              transform var(--dur-fast) var(--ease-out);
}
.btn svg { width: 18px; height: 18px; flex: none; }
.btn:hover { border-color: var(--accent-line); background: var(--glass-strong); color: var(--accent); transform: translateY(-1px); }
.btn:active { transform: translateY(0); }

.btn-primary {
  background: var(--gradient-accent);
  color: #05060a;
  border-color: transparent;
  box-shadow: 0 8px 28px -10px rgba(79, 209, 255, 0.55);
}
.btn-primary:hover { color: #05060a; filter: brightness(1.07); box-shadow: 0 12px 34px -10px rgba(79, 209, 255, 0.7); }

/* A link that acts as a forward step in the reading order. The arrow is drawn
   from a pseudo-element and slides on hover, which is the whole affordance —
   no underline needed because the shape already says "onward". */
.link-arrow {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  color: var(--accent);
  font-weight: 560;
  font-size: 0.95rem;
}
.link-arrow::after {
  content: '→';
  transition: transform var(--dur-base) var(--ease-out);
}
.link-arrow:hover::after { transform: translateX(4px); }

/* ------------------------------------------------------------------ nav -- */
.site-nav {
  position: sticky;
  top: 0;
  z-index: 60;
  background: rgba(5, 6, 10, 0.72);
  backdrop-filter: blur(18px) saturate(160%);
  -webkit-backdrop-filter: blur(18px) saturate(160%);
  border-bottom: 1px solid transparent;
  transition: background-color var(--dur-base) var(--ease-out),
              border-color var(--dur-base) var(--ease-out);
}
/* motion.js adds .condensed past the fold. The bar does not change height —
   a header that resizes while you scroll shifts every anchor target under it
   and makes the page feel unstable. It gains a border and more opacity, which
   is the information ("you have left the top") without the movement. */
.site-nav.condensed {
  background: rgba(5, 6, 10, 0.92);
  border-bottom-color: var(--glass-border);
}

.nav-inner {
  max-width: var(--max-width);
  margin-inline: auto;
  padding: 0 var(--space-3);
  min-height: var(--nav-h);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
}

.nav-logo {
  font-weight: 760;
  font-size: 1.22rem;
  letter-spacing: -0.03em;
  color: var(--text-primary);
  flex: none;
}

.nav-links { display: flex; gap: var(--space-1); flex-wrap: wrap; align-items: center; }
.nav-links a {
  position: relative;
  color: var(--text-secondary);
  font-size: 0.92rem;
  font-weight: 500;
  padding: 8px 12px;
  border-radius: var(--radius-sm);
  transition: color var(--dur-fast) var(--ease-out), background-color var(--dur-fast) var(--ease-out);
}
.nav-links a:hover { color: var(--text-primary); background: var(--glass); }

/* The current section. Colour alone would fail anyone who cannot distinguish
   it, so the underline carries the same information — and aria-current carries
   it to a screen reader, which is the part no amount of styling can do. */
.nav-links a[aria-current] { color: var(--text-primary); font-weight: 600; }
.nav-links a[aria-current]::after {
  content: '';
  position: absolute;
  left: 12px;
  right: 12px;
  bottom: 2px;
  height: 2px;
  border-radius: 2px;
  background: var(--accent);
}

.nav-actions { display: flex; align-items: center; gap: var(--space-1); flex: none; }
.nav-login {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 10px 16px;
  min-height: 42px;
  border-radius: 999px;
  border: 1px solid var(--glass-border);
  color: var(--text-primary);
  font-weight: 560;
  font-size: 0.9rem;
  white-space: nowrap;
  transition: border-color var(--dur-fast) var(--ease-out),
              background-color var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out);
}
.nav-login svg { width: 16px; height: 16px; flex: none; }
.nav-login:hover { background: var(--glass); border-color: var(--accent-line); color: var(--accent); }

@media (max-width: 480px) {
  .nav-login { padding: 9px 13px; font-size: 0.85rem; }
  .nav-login svg { display: none; }
}

/* Mobile navigation. Two behaviours, deliberately:
     - No JavaScript: the link row stays on screen and scrolls sideways. Not
       elegant, but every page on the site remains reachable.
     - JavaScript ran (html.js-nav): the row collapses into a panel behind the
       menu button, which is the behaviour worth having.
   What is not here is the third case that used to ship — links set to
   display:none with nothing at all to open them. */
.nav-toggle { display: none; }
.nav-panel-cta { display: none; }

@media (max-width: 860px) {
  .nav-inner { flex-wrap: wrap; gap: var(--space-2); padding-block: 10px; }
  .nav-actions > .whatsapp-cta { display: none; }
  .nav-panel-cta { display: inline-flex; }
  .js-nav .nav-panel-cta { justify-content: center; margin: var(--space-2) 0 var(--space-1); width: 100%; }

  .nav-links {
    order: 3;
    width: 100%;
    flex-wrap: nowrap;
    overflow-x: auto;
    gap: 2px;
    padding-bottom: 4px;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
  }
  .nav-links::-webkit-scrollbar { display: none; }
  .nav-links a { white-space: nowrap; }

  .js-nav .nav-toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    padding: 0;
    border-radius: 999px;
    border: 1px solid var(--glass-border);
    background: transparent;
    color: var(--text-primary);
    cursor: pointer;
  }
  .js-nav .nav-toggle svg { width: 20px; height: 20px; }
  .js-nav .nav-toggle[aria-expanded="true"] { border-color: var(--accent); color: var(--accent); }

  /* Collapsed: a stacked panel, not a squeezed row. Eight destinations read far
     better as a list than as a strip the thumb has to drag. The panel is
     display:none until opened — GSAP animates the items inside it, so it must
     not be opacity-hidden here or the two would fight over the same property. */
  .js-nav .nav-links {
    display: none;
    flex-direction: column;
    flex-wrap: wrap;
    overflow: visible;
    gap: 0;
    margin-top: var(--space-1);
    padding-bottom: var(--space-2);
    border-top: 1px solid var(--glass-border);
  }
  .js-nav .nav-links[data-open="true"] { display: flex; }
  .js-nav .nav-links a:not(.nav-panel-cta) {
    padding: 14px 4px;
    font-size: 1.02rem;
    border-radius: 0;
    border-bottom: 1px solid var(--glass-border);
  }
  .js-nav .nav-links a:not(.nav-panel-cta):last-of-type { border-bottom: 0; }
  .js-nav .nav-links a[aria-current]::after { left: 4px; right: auto; width: 18px; bottom: 8px; }
}

/* Skip link. Fifty pages with an eight-item nav and, before this, no way past it. */
.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 100;
  background: var(--surface-2);
  color: var(--text-primary);
  padding: 12px 20px;
  border-radius: 0 0 var(--radius-md) 0;
  border: 1px solid var(--accent);
  font-weight: 600;
}
.skip-link:focus { left: 0; }

/* Scroll progress. motion.js drives scaleX from a ScrollTrigger on the document,
   so this is only the resting appearance. */
#scrollProgress {
  position: fixed;
  top: 0;
  left: 0;
  height: 2px;
  width: 100%;
  z-index: 70;
  transform: scaleX(0);
  transform-origin: 0 50%;
  background: var(--gradient-accent);
  pointer-events: none;
}

/* ----------------------------------------------------------- breadcrumb -- */
.breadcrumb {
  border-bottom: 1px solid var(--glass-border);
  background: rgba(5, 6, 10, 0.5);
}
.breadcrumb ol {
  max-width: var(--max-width);
  margin-inline: auto;
  padding: 10px var(--space-3);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  list-style: none;
  font-size: 0.82rem;
  color: var(--text-muted);
}
.breadcrumb a { color: var(--text-muted); }
.breadcrumb a:hover { color: var(--accent); }
/* The separator is presentational. Drawn from CSS rather than typed into the
   markup so a screen reader reads "Home, Access Control, PIN" and not a slash
   between every item. */
.breadcrumb li + li::before {
  content: '';
  display: inline-block;
  width: 5px;
  height: 5px;
  margin-right: 6px;
  border-right: 1px solid var(--glass-border-2);
  border-top: 1px solid var(--glass-border-2);
  transform: rotate(45deg);
  vertical-align: middle;
}
.breadcrumb [aria-current="page"] { color: var(--text-secondary); font-weight: 560; }

/* --------------------------------------------------------------- pills --- */
.pill-row { display: flex; flex-wrap: wrap; gap: var(--space-1); margin-top: var(--space-2); }
.pill {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 5px 12px;
  border-radius: 999px;
  border: 1px solid var(--glass-border);
  background: var(--glass);
  font-size: 0.8rem;
  color: var(--text-secondary);
  white-space: nowrap;
}
.pill svg { width: 14px; height: 14px; color: var(--accent); }
.pill-ok   { color: var(--state-ok);   border-color: rgba(52, 211, 153, 0.32);  background: var(--state-ok-bg); }
.pill-warn { color: var(--state-warn); border-color: rgba(251, 191, 36, 0.32);  background: var(--state-warn-bg); }
.pill-deny { color: var(--state-deny); border-color: rgba(248, 113, 113, 0.32); background: var(--state-deny-bg); }

/* A status or caveat that must not read as body copy. */
.callout {
  border-left: 2px solid var(--accent);
  background: linear-gradient(90deg, var(--accent-dim), transparent 60%);
  border-radius: 0 var(--radius-md) var(--radius-md) 0;
  padding: var(--space-3);
  margin-block: var(--space-3);
}
.callout h3 { color: var(--text-primary); }
.callout > :last-child { margin-bottom: 0; }

/* ------------------------------------------------------------------ faq -- */
.faq-item {
  border-bottom: 1px solid var(--glass-border);
  padding: var(--space-3) 0;
}
.faq-item:first-of-type { border-top: 1px solid var(--glass-border); }
.faq-item h3,
.faq-item h4 {
  font-size: var(--step-0);
  font-weight: 620;
  line-height: 1.35;
  margin-bottom: var(--space-1);
  color: var(--text-primary);
}
.faq-item p { max-width: var(--measure); }

/* Glossary A–Z jump strip. Forty definitions in one column need a way in, and
   the letters are the only structure the list has. Letters with no terms are
   never printed, so there are no dead targets. */
.az-index { display: flex; flex-wrap: wrap; gap: 6px; margin-block: var(--space-2) var(--space-4); }
.az-index a {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 36px;
  min-height: 36px;
  padding: 0 8px;
  border: 1px solid var(--glass-border);
  border-radius: 10px;
  background: var(--glass);
  color: var(--text-secondary);
  font-weight: 620;
  font-size: 0.85rem;
  transition: color var(--dur-fast) var(--ease-out), border-color var(--dur-fast) var(--ease-out);
}
.az-index a:hover { color: var(--accent); border-color: var(--accent-line); }
.az-letter {
  margin-top: var(--space-5);
  color: var(--accent);
  font-size: var(--step-2);
  border-bottom: 1px solid var(--glass-border);
  padding-bottom: 8px;
}

/* --------------------------------------------------------------- footer -- */
.site-footer {
  border-top: 1px solid var(--glass-border);
  background: linear-gradient(180deg, transparent, rgba(255, 255, 255, 0.015));
  padding: var(--space-5) 0 var(--space-3);
  margin-top: var(--space-6);
}

/* The closing call to action. It has to out-weigh the link index below it
   without becoming a second hero — hence the panel treatment and a single
   accent glow, rather than a larger type scale. */
.footer-cta {
  display: grid;
  gap: var(--space-4);
  align-items: center;
  padding: var(--space-5) var(--space-4);
  margin-bottom: var(--space-5);
  border: 1px solid var(--glass-border-2);
  border-radius: var(--radius-xl);
  background:
    radial-gradient(ellipse 70% 120% at 8% 0%, rgba(79, 209, 255, 0.13), transparent 62%),
    linear-gradient(180deg, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.012));
  box-shadow: var(--shadow-md);
}
.footer-cta h2 { margin-bottom: var(--space-1); }
.footer-cta p { margin-bottom: 0; }
@media (min-width: 860px) {
  .footer-cta { grid-template-columns: minmax(0, 1fr) auto; gap: var(--space-5); }
}

.footer-tagline { color: var(--text-muted); }

/* The motion switch. Deliberately quiet — it is a preference, not a call to
   action — but a real 40px target, because the people most likely to need it
   are the ones least well served by a 12px link. */
.motion-toggle {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-height: 40px;
  padding: 8px 14px;
  border-radius: 999px;
  border: 1px solid var(--glass-border);
  background: transparent;
  color: var(--text-muted);
  font-size: 0.82rem;
  font-weight: 560;
  cursor: pointer;
  white-space: nowrap;
  transition: color var(--dur-fast) var(--ease-out),
              border-color var(--dur-fast) var(--ease-out);
}
.motion-toggle:hover { color: var(--text-primary); border-color: var(--glass-border-2); }

/* State is carried by the dot AND by the label text, never by colour alone —
   an accessibility control that fails an accessibility guideline would be a
   poor joke. */
.motion-toggle-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  flex: none;
  background: var(--text-muted);
  transition: background-color var(--dur-fast) var(--ease-out),
              box-shadow var(--dur-fast) var(--ease-out);
}
.motion-toggle[data-mode="full"] { color: var(--text-secondary); }
.motion-toggle[data-mode="full"] .motion-toggle-dot {
  background: var(--accent);
  box-shadow: 0 0 8px var(--accent);
}
@media (max-width: 640px) {
  .footer-tagline { display: none; }   /* the one line that adds nothing on a phone */
}
.footer-grid {
  max-width: var(--max-width);
  margin-inline: auto;
  padding-inline: var(--space-3);
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
  gap: var(--space-4) var(--space-3);
}
.footer-col h3,
.footer-col h4 {
  font-size: 0.76rem;
  font-weight: 620;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-muted);
  margin-bottom: var(--space-1);
}
.footer-col ul { list-style: none; }
/* Forty-odd links, the densest cluster of targets on the site, and each one was
   a 20px-high box — under the 24px floor, on the surface most likely to be
   touched rather than clicked. inline-block is what lets the padding count:
   vertical padding on an inline element does not grow its hit area. */
.footer-col a {
  display: inline-block;
  color: var(--text-secondary);
  font-size: 0.9rem;
  padding-block: 6px;
  transition: color var(--dur-fast) var(--ease-out);
}
.footer-col a:hover { color: var(--accent); }
.footer-bottom {
  max-width: var(--max-width);
  margin: var(--space-4) auto 0;
  padding: var(--space-3) var(--space-3) 0;
  border-top: 1px solid var(--glass-border);
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-2);
  color: var(--text-muted);
  font-size: 0.85rem;
}

/* --------------------------------------------------------------- tables -- */
/* A table too wide for a phone scrolls inside this box rather than pushing the
   page sideways.

   The max-height is not cosmetic. overflow-x:auto makes this a scroll container
   on BOTH axes — the spec computes the other axis from visible to auto — and a
   sticky <thead> sticks to its nearest scroll container, not to the viewport.
   With the box free to grow to the full height of the table there is nothing
   for the header to stick to, which is why it scrolled away on an 87-row table.
   Bounding the box is what gives the header something to stick to. */
.table-scroll {
  overflow: auto;
  max-height: min(78vh, 760px);
  overscroll-behavior: contain;
  border-radius: var(--radius-md);
  border: 1px solid var(--glass-border);
}

.comparison-table {
  width: 100%;
  border-collapse: collapse;
  font-variant-numeric: tabular-nums;
}
.comparison-table thead {
  background: var(--surface-2);
  border-bottom: 1px solid var(--glass-border);
  position: sticky;
  top: var(--nav-h);
  z-index: 2;
}
.table-scroll .comparison-table thead { top: 0; }
.comparison-table th {
  text-align: left;
  font-weight: 620;
  color: var(--text-primary);
  padding: var(--space-2);
  font-size: 0.9rem;
  white-space: nowrap;
}
.comparison-table td {
  color: var(--text-secondary);
  padding: var(--space-2);
  font-size: 0.9rem;
  border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.comparison-table tbody th { color: var(--accent); font-weight: 600; }
/* Zebra striping only past a handful of rows — on a five-row table it is noise;
   on an eighty-row one it is the difference between reading across and losing
   your place. */
.comparison-table tbody tr:nth-child(even) { background: rgba(255, 255, 255, 0.02); }
.comparison-table tbody tr { transition: background-color var(--dur-fast) var(--ease-out); }
.comparison-table tbody tr:hover { background: rgba(79, 209, 255, 0.055); }
.comparison-table tbody tr:last-child td { border-bottom: none; }

@media (max-width: 768px) {
  .comparison-table th,
  .comparison-table td { padding: 10px var(--space-2); font-size: 0.85rem; }
}

/* -------------------------------------------------------- illustrations -- */
/* width/height stay on the <img> so the browser reserves the right box before
   decode (no layout shift), and height:auto lets the intrinsic ratio govern
   once it has one. Without this the declared pixel height wins on narrow
   screens and the art is stretched. */
.art {
  display: block;
  width: 100%;
  max-width: 760px;
  height: auto;
  margin-inline: auto;
}
figure { margin: 0; }

/* A caption is an argument, not a label. Left-aligned under a rule reads as
   editorial; centred italic under a picture reads as a stock-photo credit. */
figure figcaption {
  color: var(--text-muted);
  font-size: 0.86rem;
  line-height: 1.55;
  max-width: var(--measure);
  margin: var(--space-2) auto 0;
  padding-left: var(--space-2);
  border-left: 2px solid var(--accent-line);
}

/* Wide diagrams carry ~10px labels. Scaled to a 327px phone they land at four
   pixels and become decoration. Same answer as .table-scroll: let the figure
   scroll inside itself at a legible minimum width, rather than shrinking the
   art until it says nothing. The page body never scrolls horizontally. */
.art-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }
.art-scroll .art { min-width: 640px; }
.art-scroll:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; }
@media (min-width: 800px) { .art-scroll .art { min-width: 0; } }

.art-inline { max-width: 420px; }

/* ---------------------------------------------------------- photography -- */
/* Photos sit on a dark page, so they carry a soft edge and a slight darkening
   at the base: a bright photo butting straight against the near-black
   background reads as a pasted rectangle. */
.photo {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 3 / 2;
  object-fit: cover;
  border-radius: var(--radius-lg);
  border: 1px solid var(--glass-border);
}
.photo-figure { position: relative; margin: 0; }
/* The scrim sits on the image box only. It used to cover the figure, which put
   a gradient over the caption too on any figure whose caption wrapped. */
.photo-figure::after {
  content: '';
  position: absolute;
  inset: auto 0 0 0;
  height: 38%;
  border-radius: 0 0 var(--radius-lg) var(--radius-lg);
  background: linear-gradient(to bottom, transparent, rgba(5, 6, 10, 0.55));
  pointer-events: none;
}
.photo-figure:has(figcaption)::after { display: none; }
.photo-figure figcaption { position: relative; }

/* A photo behind hero text needs to lose contrast, or the text is unreadable.
   Handled with a scrim rather than by dimming the file, so the same asset can
   be reused somewhere it is the subject. */
.photo-hero { position: relative; overflow: hidden; border-radius: var(--radius-xl); }
.photo-hero .photo { aspect-ratio: 21 / 9; border-radius: var(--radius-xl); }
.photo-hero::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(105deg, rgba(5, 6, 10, 0.9) 0%, rgba(5, 6, 10, 0.62) 45%, rgba(5, 6, 10, 0.22) 100%);
  pointer-events: none;
}
@media (max-width: 700px) {
  .photo-hero .photo { aspect-ratio: 4 / 3; }
  .photo-hero::after { background: linear-gradient(to top, rgba(5, 6, 10, 0.93) 25%, rgba(5, 6, 10, 0.45) 100%); }
}

/* ------------------------------------------------------- feature band ---- */
/* A photograph and the argument it supports, as one object rather than as two
   consecutive full-width scrolls. The copy sits over the lower half of the
   image on desktop and below it on a phone — text over a photograph needs a
   scrim, and on a narrow screen there is not enough image left under the text
   for the scrim to work without swallowing the picture entirely. */
.feature-band {
  position: relative;
  border-radius: var(--radius-xl);
  overflow: hidden;
  border: 1px solid var(--glass-border-2);
  box-shadow: var(--shadow-lg);
  isolation: isolate;
}
.feature-band-media { position: absolute; inset: -8% 0; z-index: 0; }
.feature-band-media .photo-figure,
.feature-band-media picture { height: 100%; }
.feature-band-media .photo {
  height: 100%;
  aspect-ratio: auto;
  border: 0;
  border-radius: 0;
  object-fit: cover;
}
.feature-band-media .photo-figure::after { display: none; }

.feature-band-copy {
  position: relative;
  z-index: 1;
  padding: clamp(var(--space-4), 6vw, var(--space-6));
  padding-top: clamp(var(--space-6), 22vw, 220px);
  background: linear-gradient(180deg, rgba(5, 6, 10, 0.15) 0%, rgba(5, 6, 10, 0.82) 46%, rgba(5, 6, 10, 0.95) 100%);
}
.feature-band-copy p { color: var(--text-secondary); }
.feature-band-copy > :last-child { margin-bottom: 0; }

@media (max-width: 700px) {
  /* No overlay on a phone: the band becomes a stacked figure and paragraph,
     which is legible where 15-point type over a night photograph is not. */
  .feature-band-media { position: relative; inset: auto; aspect-ratio: 3 / 2; }
  .feature-band-copy { padding-top: var(--space-4); background: none; }
}

/* ------------------------------------------------------------ utilities -- */
.visually-hidden {
  position: absolute !important;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.text-center { text-align: center; }
.text-center p,
.text-center .lead { margin-inline: auto; }
.mono { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
