/* ============================================================
   NEXOVIA — Design System
   ============================================================ */

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

:root {
  /* Surfaces */
  --bg:          #05070a;
  --bg-2:        #0a0e15;
  --surface:     #0c1016;
  --surface-2:   #11171f;
  --surface-3:   #161d27;
  --nx-border:      #1a212b;
  --nx-border-lit:  #29323f;

  /* Text */
  /* Contrast, WCAG 2.1. Two ratios per token, because this site puts copy on
     two grounds: the page --bg #05070a and raised cards --nx-border #1a212b.
     A token rated only against the page silently fails on a card.

                        on #05070a   on #1a212b
     text   #e3e8f0        16.2:1       13.1:1
     text-2 #98a2b1         7.8:1        6.3:1
     text-3 #7e8897         5.6:1        4.5:1   (body + card/legend copy)
     text-4 #6e7887         4.5:1        3.6:1   (micro-labels, page ground only)

     text-3 was #737d8c and text-4 #5c6675, rated 4.8 and 3.5 against the page
     alone. The 2026-08-20 audit measured every live use: text-4 was carrying
     10–14px eyebrows and micro-labels on all ten pages at 3.47:1, never the
     "AA large" its old note reserved it for — not one use qualified. And
     text-3 dropped to 3.89:1 wherever a card sat under it. Both are lifted to
     clear 4.5:1 in the ground each actually sits on. Keep text-4 off cards. */
  --text:        #e3e8f0;
  --text-2:      #98a2b1;
  --text-3:      #7e8897;
  --text-4:      #6e7887;

  /* Headings sit a step brighter than --text on purpose, so they need their own
     token rather than reusing it. Added 2026-08-19: h1-h4 and .spec-v strong
     hardcoded #fff, which meant they could not follow a ground change. Phase
     5.7's inverted band re-grounded everything around them via tokens and left
     every heading white on near-white — measured 1.1:1. */
  --heading:     #fff;

  /* Brand — three tiers.
     Full-strength green marked every structural role at once (78 rules), so it
     read as texture rather than emphasis. It is now reserved for things the user
     can act on; structure and metadata use the dimmer tiers. */
  --nx-accent:      #00ff88;   /* action: buttons, em, live state, ticks, hovers */
  --nx-accent-mid:  #3f9e6e;   /* structure: eyebrows, section numbers, spec keys  */
  --nx-accent-low:  #2c6b4c;   /* metadata: rules, ticks, decorative marks         */
  --nx-accent-deep: #00c46a;
  --nx-accent-glow: rgba(0,255,136,0.14);
  --nx-accent-film: rgba(0,255,136,0.06);
  --cyan:        #22d3ee;
  --violet:      #8b5cf6;
  --danger:      #ff3b4d;
  --warn:        #ffb020;

  /* Type */
  --mono:  'DM Mono', ui-monospace, monospace;
  --serif: 'Instrument Serif', Georgia, serif;
  --sans:  'Syne', system-ui, -apple-system, sans-serif;

  /* Rhythm */
  --gutter: 40px;
  --maxw: 1360px;
  --nx-radius: 4px;
  --nx-radius-lg: 10px;

  /* Motion */
  --ease: cubic-bezier(0.16, 1, 0.3, 1);
}

html {
  scroll-behavior: smooth;
  scroll-padding-top: 90px;
  -webkit-text-size-adjust: 100%;
}

body {
  background: var(--bg);
  color: var(--text);
  font-family: var(--sans);
  font-size: 16px;
  line-height: 1.6;
  overflow-x: hidden;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

/* ---------- Atmosphere grid ----------
   Was `body::before` with `position: fixed`, painting behind the entire
   document. Two problems with that: pinned to the viewport it does not move
   when the page does, so it reads as something laid over the screen rather
   than something the page is built on — and it ran under every band equally,
   which is a lot of texture for 12,000px of page.

   It is now scoped to the STAGE sections (the hero and the two pinned
   chapters) and absolutely positioned inside them, so it scrolls with its
   section and reads as structure. It is also what lets the drawn container
   verticals go without the measure feeling lost — see .band::before below. */
.hero-rig::before,
.scrub::before {
  content: '';
  position: absolute;
  inset: 0;
  background-image:
    linear-gradient(rgba(255,255,255,0.016) 1px, transparent 1px),
    linear-gradient(90deg, rgba(255,255,255,0.016) 1px, transparent 1px);
  background-size: 64px 64px;
  pointer-events: none;
  z-index: 0;
}

/* The green scanline wash that used to live on `body::after` is gone.

   It was `mix-blend-mode: screen` over `position: fixed; inset: 0`, so it sat
   on top of the WHOLE document at z-index 9998 — including every detection
   still and every playing clip. It put a green cast on the one set of assets
   whose job is to prove the product, which is a poor trade for a texture
   nobody was meant to consciously notice. Nothing replaces it. */

img, svg, video { display: block; max-width: 100%; }
a { color: inherit; text-decoration: none; }
button { font: inherit; cursor: pointer; }

::selection { background: var(--nx-accent); color: var(--bg); }

/* ---------- Layout primitives ---------- */

.wrap {
  width: 100%;
  max-width: var(--maxw);
  margin-inline: auto;
  padding-inline: var(--gutter);
  position: relative;
  z-index: 1;
}

.wrap-wide { max-width: 1680px; }

section { position: relative; z-index: 1; }

/* Trimmed 2026-08-22. band-lg at 200px top AND bottom is 400px of air between
   two sections, which is most of how the homepage reached 12,410px and
   /platform 11,460px. Both reference sites (Corum, Verkada) run considerably
   denser than this. The rhythm between the four sizes is preserved. */
.band { padding-block: 92px; }
.band-sm { padding-block: 64px; }
.band-lg { padding-block: 140px; }
.band-xs { padding-block: 52px; }

.rule { border-top: 1px solid var(--nx-border); }

/* ---------- Second surface ----------
   Every section painted rgba(0,0,0,0), so the page was one flat
   9,700px sheet with hairlines on it. Ruled sections now carry a
   ground of their own, which is the only way the eye reads a
   section boundary on near-black. */
.band.rule,
.band-lg.rule,
.band-sm.rule,
.band-xs.rule { background: var(--bg-2); }

/* ---------- Registration ticks ----------
   This replaces the drawn container frame: two 1px verticals that ran the FULL
   HEIGHT of every band on every page. The intent was to make the measure
   visible on a ground where whitespace reads as void, and it did — but a
   continuous line repeated down 12,410px of homepage stops being a mark and
   becomes a fixture you cannot unsee. It was the single most-noticed thing on
   the site.

   The measure is now marked where it actually means something: the band's
   opening corners. A 26px L-tick at each, in --nx-accent-low, and nothing in
   between. Same drafting language, stated once per section instead of
   continuously. The atmosphere grid on stage sections carries the rest.

   Drawn with one gradient stack per corner on two pseudo-elements so this
   still needs no extra markup — ::before takes the left corner, ::after the
   right. `background-repeat: no-repeat` on both is what stops them tiling. */
.band, .band-sm, .band-lg, .band-xs { position: relative; }

.band::before,
.band-sm::before,
.band-lg::before,
.band-xs::before,
.band::after,
.band-sm::after,
.band-lg::after,
.band-xs::after {
  content: '';
  position: absolute;
  top: 0;
  z-index: 0;
  width: 26px;
  height: 26px;
  pointer-events: none;
  background-repeat: no-repeat;
  background-image:
    linear-gradient(var(--nx-accent-low), var(--nx-accent-low)),
    linear-gradient(var(--nx-accent-low), var(--nx-accent-low));
  background-size: 26px 1px, 1px 26px;
}

/* left corner: horizontal arm runs right, vertical arm runs down */
.band::before,
.band-sm::before,
.band-lg::before,
.band-xs::before {
  left: max(var(--gutter), calc(50% - 680px));
  background-position: 0 0, 0 0;
}

/* right corner mirrors it */
.band::after,
.band-sm::after,
.band-lg::after,
.band-xs::after {
  right: max(var(--gutter), calc(50% - 680px));
  background-position: 100% 0, 100% 0;
}

@media (max-width: 760px) {
  .band::before, .band-sm::before, .band-lg::before, .band-xs::before,
  .band::after, .band-sm::after, .band-lg::after, .band-xs::after { display: none; }
}

/* ---------- Typography ---------- */

.eyebrow {
  /* Mono KEEPS this one — it is the single section marker per band, and a
     label rather than language. 0.16em instead of 0.28em: at 12px the wider
     tracking broke the phrase into loose letters. */
  font-family: var(--mono);
  font-size: 12px;
  font-weight: 400;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--nx-accent-mid);
  display: flex;
  align-items: center;
  gap: 11px;
  margin-bottom: 22px;
}

.eyebrow::before {
  content: '';
  width: 7px;
  height: 7px;
  background: var(--nx-accent);
  flex: none;
}

.eyebrow-c { justify-content: center; }
.eyebrow-c::after {
  content: '';
  width: 7px;
  height: 7px;
  background: var(--nx-accent);
  flex: none;
}

h1, .h1 {
  font-family: var(--serif);
  font-weight: 400;
  font-size: clamp(46px, 6.6vw, 104px);
  line-height: 0.96;
  letter-spacing: -0.02em;
  color: var(--heading);
}

h2, .h2 {
  font-family: var(--serif);
  font-weight: 400;
  font-size: clamp(34px, 4.6vw, 68px);
  line-height: 1.03;
  letter-spacing: -0.015em;
  color: var(--heading);
}

h3, .h3 {
  font-family: var(--sans);
  font-weight: 600;
  font-size: clamp(20px, 2vw, 26px);
  line-height: 1.25;
  color: var(--heading);
  letter-spacing: -0.01em;
}

h4, .h4 {
  font-family: var(--sans);
  font-weight: 600;
  font-size: 17px;
  line-height: 1.35;
  color: var(--heading);
}

em, .accent-em {
  font-style: italic;
  color: var(--nx-accent);
}

.lede {
  font-family: var(--sans);
  font-size: clamp(17px, 1.35vw, 21px);
  font-weight: 400;
  line-height: 1.6;
  letter-spacing: -0.005em;
  color: var(--text-2);
  max-width: 52ch;
}

/* Weight 400, not 300. Syne's 300 has thin joins that fill in against a
   near-black ground at body size; 400 holds them open and reads clearer
   without reading heavier. */
.body-copy {
  font-family: var(--sans);
  font-size: 16.5px;
  font-weight: 400;
  line-height: 1.75;
  color: var(--text-2);
}

/* "Sample data shown." under console captures that carry illustrative data.
   Deliberately quiet: it is a disclosure, not a caption to read. */
.sample-note {
  margin-top: 8px;
  font-family: var(--sans);
  font-size: 11px;
  line-height: 1.4;
  letter-spacing: 0.02em;
  opacity: 0.55;
}

/* general-purpose small prose — Syne, not mono. Mono is for values you compare
   character by character; this class carries sentences. */
.small {
  font-family: var(--sans);
  font-size: 15px;
  line-height: 1.7;
  color: var(--text-3);
  /* the /industries table legend ran to ~181 characters per line at 1440px */
  max-width: 78ch;
}

.tag {
  font-family: var(--sans);
  font-size: 13.5px;
  font-weight: 500;
  letter-spacing: 0.01em;
  color: var(--text-3);
}

.center { text-align: center; }
.center .lede, .center .body-copy { margin-inline: auto; }
.center .eyebrow { justify-content: center; }
.center .eyebrow::after {
  content: '';
  width: 7px; height: 7px;
  background: var(--nx-accent); flex: none;
}

/* ---------- Buttons ---------- */

/* Syne, sentence case, 0.01em — not mono caps at 0.16em.

   Part of the 2026-08-22 type pass. DM Mono was carrying the nav, every
   button, every form label, every card caption and the whole footer, all in
   caps at 0.13–0.28em tracking. Mono earns its place on values you compare
   character by character (latencies, confidences, camera IDs, spec figures)
   and on the one eyebrow per section; it does not earn its place on anything
   that carries language. All-caps at wide tracking is also the slowest setting
   on the page to read, which is a real cost on the thing you most want clicked.

   The markup already says "Request a demo" in sentence case — the shouting was
   entirely `text-transform`. */
.btn {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  font-family: var(--sans);
  font-size: 15.5px;
  font-weight: 500;
  letter-spacing: 0.01em;
  padding: 15px 30px;
  border: 1px solid transparent;
  transition: background 0.3s var(--ease), color 0.3s var(--ease), box-shadow 0.3s var(--ease), border-color 0.3s var(--ease), transform 0.3s var(--ease);
  white-space: nowrap;
}

.btn-primary {
  background: var(--nx-accent);
  color: #04120b;
  font-weight: 600;
}

.btn-primary:hover {
  box-shadow: 0 0 0 1px var(--nx-accent), 0 8px 40px rgba(0,255,136,0.28);
  transform: translateY(-2px);
}

.btn-ghost {
  background: transparent;
  color: var(--text);
  border-color: var(--nx-border-lit);
}

.btn-ghost:hover {
  border-color: var(--nx-accent);
  color: var(--nx-accent);
  background: var(--nx-accent-film);
}

.btn-line {
  padding: 0;
  border: none;
  color: var(--nx-accent);
  background: none;
  position: relative;
}
/* 7.4 — the visual stays 25px tall so nothing reflows, but the tappable area
   is expanded to 44px with a pseudo-element. Padding would have pushed every
   layout this link sits in. */
.btn-line::after {
  content: '';
  position: absolute;
  left: 0; right: 0; top: 50%;
  transform: translateY(-50%);
  height: 44px;
}
.btn-line span { border-bottom: 1px solid rgba(0,255,136,0.3); padding-bottom: 3px; }
.btn-line:hover span { border-color: var(--nx-accent); }

.btn-row {
  display: flex;
  flex-wrap: wrap;
  gap: 14px;
  align-items: center;
}


/* ---------- Page header (interior pages) ---------- */

/* 128px, not 190px. Under a 64px fixed nav, 190px put roughly two thirds of
   the opening fold below the headline on a laptop — and this block opens nine
   pages, so the cost was paid nine times. Together with the .band-lg trim
   below this removes about a screen and a half per page without cutting a
   word. Density is part of what reads as sharp. */
.page-head {
  padding-top: 128px;
  padding-bottom: 76px;
  position: relative;
  overflow: hidden;
}

/* The green radial wash that used to sit here is gone.

   It was the identical ellipse behind the headline on all nine interior
   pages. One page in, it is atmosphere; nine pages in, the eye has learned it
   carries no information and it reads as wallpaper. The page openings are
   differentiated by what they actually contain instead. */

/* Two columns. Every interior page opened with the same single left-hand
   column, leaving the right 50% of the fold 63-71% empty. The stat rail
   fills it with the highest-contrast module in the system. */
.page-head .wrap {
  display: grid;
  grid-template-columns: 1.08fr 0.92fr;
  gap: 72px;
  align-items: end;
}

.page-head h1 {
  font-size: clamp(40px, 5.6vw, 84px);
  max-width: 18ch;
}

.page-head .lede { margin-top: 28px; }
.page-head .btn-row { margin-top: 36px; }

/* stat rail — sits in the right column, aligned to the baseline of the copy */
.head-stats {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1px;
  background: var(--nx-border);
  border: 1px solid var(--nx-border);
}

.head-stat {
  background: var(--bg);
  padding: 30px 26px 26px;
  transition: background 0.35s;
}
.head-stat:hover { background: var(--surface); }

.head-stat b {
  display: block;
  font-family: var(--serif);
  font-weight: 400;
  font-size: clamp(38px, 3.9vw, 62px);
  line-height: 0.86;
  letter-spacing: -0.03em;
  color: #fff;
  margin-bottom: 12px;
}
.head-stat b i { font-style: normal; font-size: 0.34em; color: var(--nx-accent); letter-spacing: 0; }

/* 12.5px floor, set 2026-08-20. These four are not decorative micro-type:
   they are what a visitor reads to work out what they are looking at. The
   census found 351 elements under 12px site-wide, and the same 288 stay under
   12px at 390px wide, so they are smallest exactly where they are hardest to
   read. Tracking comes down with the size — 0.18em on an 11px mono label
   stretches a word without making it more legible. Genuinely decorative
   micro-type is left alone. */
.head-stat span {
  font-family: var(--sans);
  font-size: 13.5px;
  font-weight: 500;
  letter-spacing: 0.01em;
  color: var(--text-3);
}

.crumb {
  font-family: var(--sans);
  font-size: 13.5px;
  font-weight: 500;
  letter-spacing: 0.01em;
  color: var(--text-3);
  margin-bottom: 22px;
}
.crumb a:hover { color: var(--nx-accent); }
.crumb span { color: var(--nx-accent); }
/* 7.4 — the breadcrumb link is a standalone control, so it gets a 44px tap
   area via a pseudo-element rather than padding, which would push the 22px
   margin below it out of rhythm.

   Bled 3px past each end on 2026-08-20: the word HOME sets its own box at
   38px, so height alone left the target 38 wide. Nothing else here is
   clickable — the separator and the current-page span are both text — so the
   overhang cannot steal a tap from a neighbour. */
.crumb a { position: relative; }
.crumb a::after {
  content: '';
  position: absolute;
  left: -3px; right: -3px; top: 50%;
  transform: translateY(-50%);
  height: 44px;
}

/* ---------- Cards / grids ---------- */

.grid { display: grid; gap: 1px; background: var(--nx-border); border: 1px solid var(--nx-border); }
.g2 { grid-template-columns: repeat(2, 1fr); }
.g3 { grid-template-columns: repeat(3, 1fr); }
.g4 { grid-template-columns: repeat(4, 1fr); }

.cell {
  background: var(--bg);
  padding: 44px 36px;
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  transition: background 0.4s var(--ease);
}

.cell::before {
  content: '';
  position: absolute;
  top: 0; left: 0;
  width: 100%; height: 2px;
  background: linear-gradient(90deg, var(--nx-accent), transparent);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.5s var(--ease);
}

.cell:hover { background: var(--surface); }

/* Each grid gets one entry point so it is not a uniform hairline mesh. */
.grid > .cell:first-child { background: var(--surface); }
.grid > .cell:first-child::before { transform: scaleX(1); opacity: 0.55; }
/* The lit cell's mark is the grid's entry point, so it runs at full accent. */
.grid > .cell:first-child .cell-icon { color: var(--nx-accent); }

/* ---------- Recessed plate ----------
   This replaces the hairline octagon and its offset ghost twin.

   The doubled outline was reaching for depth, and depth is worth keeping — but
   it was drawn as the same shape printed twice with an offset, which the eye
   reads as misregistration rather than as layers. It was also applied
   inconsistently on purpose: `:nth-child(2n)` hid the ghost and
   `:nth-child(3n)` flipped its direction, so a three-up row showed three
   different marks and the difference read before the icon did.

   Depth now comes from light instead of duplication: a 44px well with a 1px
   highlight along the top edge and an inset shadow at the bottom, so the glyph
   sits IN the surface. It is identical on every cell, which is most of the
   fix, and it survives at 44px on a phone where a 76px octagon could not. */
.cell-icon {
  width: 44px;
  height: 44px;
  background: #070a0e;
  border: 1px solid var(--nx-border);
  border-top-color: var(--nx-border-lit);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.045),
    inset 0 -8px 14px rgba(0, 0, 0, 0.5);
  display: grid;
  place-items: center;
  margin-bottom: 22px;
  position: relative;
  color: var(--nx-accent-mid);
  transition: color 0.35s, border-color 0.35s;
}

.cell:hover .cell-icon {
  color: var(--nx-accent);
  border-color: var(--nx-accent-low);
}

.cell-icon svg { width: 20px; height: 20px; }

.cell h3, .cell .h3 { margin-bottom: 12px; font-size: 20px; }
.cell p { font-family: var(--sans); font-size: 16px; font-weight: 300; line-height: 1.7; color: var(--text-3); }

.cell-foot {
  font-family: var(--sans);
  font-size: 13.5px;
  font-weight: 500;
  letter-spacing: 0.01em;
  color: var(--nx-accent-mid);
  align-self: stretch;
  margin-top: auto;
  padding-top: 26px;
  padding-top: 16px;
  border-top: 1px solid var(--nx-border);
}

/* Honeypot on the contact form.

   Not `display:none` and not `type="hidden"` — the better bots skip both. This
   keeps the field in the layout tree but pushed out of sight, so a script that
   fills every visible-looking input still trips it. aria-hidden and
   tabindex="-1" on the markup keep it away from screen readers and the tab
   order, so nobody using assistive tech can fill it by accident. */
.hp {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

/* A real product capture sitting inside a copy column, with a caption saying
   what it is. Distinct from .det-video, which is the full-bleed evidence media
   in the other half of a .split. */
.cap-shot { margin: 0; }
.cap-shot img {
  width: 100%;
  height: auto;
  display: block;
  border: 1px solid var(--nx-border-lit);
}

/* A full phone screenshot is roughly 1:1.65, so at the full width of a column
   it renders a metre tall — measured at 644x1062 inside a copy column and
   602x926 in a split, dwarfing the text beside it. Cap the width so it reads
   as a phone rather than a poster. */
.shot-phone img,
img.shot-phone {
  max-width: 320px;
  width: 100%;
  height: auto;
  display: block;
  border: 1px solid var(--nx-border-lit);
}

/* ---- phone frame ----
   App screenshots read as screenshots until you put a device around them. The
   film does this for every phone panel it shows; the site was presenting bare
   screens with a 1px rule, which is why they looked like cropped images rather
   than a product.

   Deliberately no notch or dynamic island. The three screenshots do not start
   at the same place: nx_app_deterrence carries a real status bar (2:06 / 5G /
   87) where a notch would sit correctly, but nx_app_fall starts at
   "Sites / Guard", so a pill there would sit on top of the title. A clean bezel
   reads as a device on all of them and covers nothing.

   The wrapper is what draws the frame — an <img> cannot carry pseudo-elements,
   so the markup needs the extra element. */
/* The screen takes the shape of the capture inside it.

   Earlier on 2026-08-20 this was a fixed 9:17 box with the capture contained
   inside, on the argument that the leftover band would "read as the dark head
   of the screen rather than a gap". Measured, it does not. The three captures
   are three different shapes — nx_app_guard 0.611, nx_app_fall 0.650,
   nx_app_deterrence 0.493 — and one fixed box cannot fit any two of them:

     /            80px of dead band across the top   (13% of the screen)
     /pilot      111px of dead band across the top   (18% of the screen)
     /capabilities  22px down the left AND right edges — a pillarbox, which
                    never reads as a phone at all

   `contain` shows the whole image inside the box, so anything that is not the
   box's shape buys empty space. `cover` fills it but crops the sides, and
   these captures are tight enough that 24px each side clips real interface
   ("Sites" to "ites", "Home" to "ome"). Letting each screen take its own
   capture's shape costs nothing and loses nothing: one phone per page, so the
   slight difference in device proportion between pages is never seen side by
   side.

   The images carry width/height attributes so the box is still reserved
   before a lazy image lands. Still not a substitute for full-height captures
   from the app — that is what makes every phone the same shape again. */
.phone-frame {
  --screen-bg: #0a100e;
  position: relative;
  max-width: 340px;
  width: 100%;
  margin-inline: auto;
  padding: 11px;
  border-radius: 44px;
  background: linear-gradient(158deg, #1b212a 0%, #0d1116 46%, #070a0e 100%);
  box-shadow:
    inset 0 0 0 1px rgba(255, 255, 255, 0.09),
    inset 0 1px 0 rgba(255, 255, 255, 0.13),
    0 26px 54px -22px rgba(0, 0, 0, 0.9);
}
.phone-frame img,
.phone-frame .shot-phone,
.phone-frame img.shot-phone {
  display: block;
  width: 100%;
  max-width: none;        /* the frame owns the width, the capture the height */
  height: auto;
  border: 0;
  border-radius: 33px;
  background: var(--screen-bg);
}

/* nx_app_deterrence and nx_app_fall stop mid-card, because that is all the
   source had. Anchored to the bottom, that severed edge lands on the screen's
   bottom edge, and this fades it out so it reads as a screen continuing past
   the fold rather than a picture cut off. nx_app_guard ends on its own tab bar
   and is complete, so it does not opt in — fading it would hide real UI. */
.phone-frame.is-cut::before {
  content: '';
  position: absolute;
  left: 11px; right: 11px; bottom: 11px;
  height: 78px;
  border-radius: 0 0 33px 33px;
  background: linear-gradient(to bottom, rgba(10, 16, 14, 0) 0%, rgba(10, 16, 14, 0.86) 72%, rgba(10, 16, 14, 0.97) 100%);
  pointer-events: none;
  z-index: 1;
}

/* Side button — the one piece of device detail worth keeping, because it is
   what stops the frame reading as a plain rounded rectangle. Was 3px of
   var(--nx-accent-low): a saturated green stripe standing outside the bezel
   edge, which read as a rendering artefact rather than hardware. A real side
   button is the same material as the case. */
.phone-frame::after {
  content: '';
  position: absolute;
  right: -1px;
  top: 23%;
  width: 2px;
  height: 54px;
  border-radius: 2px;
  background: rgba(255, 255, 255, 0.16);
}
@media (max-width: 720px) {
  .phone-frame { max-width: 290px; border-radius: 38px; padding: 9px; }
  .phone-frame img { border-radius: 29px; }
  .phone-frame.is-cut::before {
    left: 9px; right: 9px; bottom: 9px; border-radius: 0 0 29px 29px;
  }
}

/* For a crop of part of an interface rather than a whole phone. Same border,
   no width cap — .shot-phone put this one at 320px in a 644px column, leaving
   half the column empty, because the aspect is near-square rather than 1:1.65. */
img.shot-ui {
  width: 100%;
  height: auto;
  display: block;
  border: 1px solid var(--nx-border-lit);
}
/* A caption is a sentence about the picture, not a value read off it. */
.cap-shot figcaption {
  margin-top: 12px;
  font-family: var(--sans);
  font-size: 14px;
  font-weight: 300;
  line-height: 1.6;
  color: var(--text-4);
}

/* Bordered panel */
.panel {
  border: 1px solid var(--nx-border);
  background: linear-gradient(180deg, var(--surface) 0%, var(--bg) 100%);
  padding: 40px;
  position: relative;
}

.panel-lit { border-color: var(--nx-border-lit); }

.corner-marks::before,
.corner-marks::after {
  content: '';
  position: absolute;
  width: 14px; height: 14px;
  pointer-events: none;
}
.corner-marks::before {
  top: -1px; left: -1px;
  border-top: 1px solid var(--nx-accent-low);
  border-left: 1px solid var(--nx-accent-low);
}
.corner-marks::after {
  bottom: -1px; right: -1px;
  border-bottom: 1px solid var(--nx-accent);
  border-right: 1px solid var(--nx-accent);
}

/* ---------- Stat bar ---------- */

.stats {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  border-top: 1px solid var(--nx-border);
  border-bottom: 1px solid var(--nx-border);
}

.stat {
  padding: 44px var(--gutter);
  border-right: 1px solid var(--nx-border);
  transition: background 0.35s;
}
.stat:last-child { border-right: none; }
.stat:hover { background: var(--surface); }

/* Bracketed qualifier above the figure. SSTR sets "[ 27 → 14 KN·M ]" over its
   "-48%"; the bracket turns a bare number into a measurement with units and a
   direction, which is most of what makes their stats read as engineering. */
.stat-q {
  display: block;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--nx-accent-mid);
  margin-bottom: 18px;
}
.stat-q::before { content: '[ '; color: var(--nx-accent-low); }
.stat-q::after  { content: ' ]'; color: var(--nx-accent-low); }

.stat-n {
  font-family: var(--serif);
  font-size: clamp(54px, 6.4vw, 116px);
  line-height: 0.84;
  letter-spacing: -0.03em;
  color: #fff;
  margin-bottom: 14px;
}
.stat-n i { font-style: normal; color: var(--nx-accent); font-size: 0.32em; letter-spacing: 0; }

.stat-l {
  font-family: var(--sans);
  font-size: 14px;
  font-weight: 500;
  letter-spacing: 0.01em;
  color: var(--text-3);
}

/* ---------- Reveal animation ---------- */

/* 0.55s, down from 0.9s (and 0.72s from 1.15s in .band-lg).

   The rest of the site answers in 0.2-0.3s — buttons, links, tabs, the nav
   background. A reveal taking three to four times that reads as the page
   still loading rather than as the page arriving, and it is worst on the
   thing it matters most for: 15 of these are the page heading, which is the
   first thing a reader looks for and was taking the best part of a second to
   turn up. The travel comes down with the duration so the movement keeps the
   same speed rather than becoming a lurch.

   The 0.07s stagger ladder below is unchanged; it reads better against a
   shorter reveal, not worse. */
.rv {
  opacity: 0;
  transform: translateY(26px);
  transition: opacity 0.55s var(--ease), transform 0.55s var(--ease);
}
.rv.in { opacity: 1; transform: none; }

.rv-l  { transform: translateX(-24px); }
.rv-r  { transform: translateX(24px); }
.rv-sc { transform: scale(0.972) translateY(16px); }
.band-lg .rv { transform: translateY(40px); transition-duration: 0.72s; }

.d1 { transition-delay: 0.07s; }
.d2 { transition-delay: 0.14s; }
.d3 { transition-delay: 0.21s; }
.d4 { transition-delay: 0.28s; }
.d5 { transition-delay: 0.35s; }
.d6 { transition-delay: 0.42s; }

@media (prefers-reduced-motion: reduce) {
  .rv { opacity: 1; transform: none; transition: none; }
  html { scroll-behavior: auto; }
  *, *::before, *::after { animation-duration: 0.001ms !important; animation-iteration-count: 1 !important; }
}

/* ---------- Footer ---------- */

.foot {
  border-top: 1px solid var(--nx-border);
  background: var(--bg-2);
  padding-top: 80px;
  position: relative;
  z-index: 1;
  overflow: hidden;
}

/* Oversized outlined wordmark, cropped by the page edge. Decorative only — the
   real brand link is at the top of the footer, so this is aria-hidden. */
.foot-word {
  margin: 56px 0 -0.3em;
  font-family: var(--sans);
  font-weight: 800;
  font-size: clamp(76px, 15.2vw, 250px);
  line-height: 0.76;
  letter-spacing: -0.025em;
  text-align: center;
  white-space: nowrap;
  color: transparent;
  -webkit-text-stroke: 1px var(--nx-border-lit);
  user-select: none;
  pointer-events: none;
}

.foot-grid {
  display: grid;
  grid-template-columns: 1.6fr repeat(4, 1fr);
  gap: 48px;
  padding-bottom: 64px;
}

.foot-brand .brand { margin-bottom: 20px; }
.foot-brand p { font-family: var(--sans); font-size: 15px; font-weight: 300; line-height: 1.65; color: var(--text-3); max-width: 34ch; }

.foot-col h2 {
  font-family: var(--sans);
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--nx-accent-mid);
  margin-bottom: 20px;
  font-weight: 400;
}

.foot-col ul { list-style: none; display: flex; flex-direction: column; gap: 12px; }
/* overflow-wrap because the Company and Get started columns now carry the full
   email addresses rather than "Email Prabhjeev". At 37 characters those set a
   min-content floor wider than the column, which is the same thing that pushed
   /privacy past the viewport at 320px and made mobile Safari shrink the page. */
.foot-col a { font-family: var(--sans); font-size: 14.5px; font-weight: 400; color: var(--text-3); transition: color 0.25s; overflow-wrap: anywhere; }
.foot-col a:hover { color: var(--nx-accent); }

.foot-bar {
  border-top: 1px solid var(--nx-border);
  padding-block: 26px;
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 20px;
  flex-wrap: wrap;
  font-family: var(--mono);
  /* on all ten pages, and one of the three items is the CalOPPA privacy link */
  font-size: 12px;
  letter-spacing: 0.12em;
  color: var(--text-3);
}

.foot-bar a:hover { color: var(--nx-accent); }

/* ---------- CTA band ---------- */

.cta-band {
  padding-block: 150px;
  text-align: center;
  position: relative;
  border-top: 1px solid var(--nx-border);
  overflow: hidden;
}

.cta-band::before {
  content: '';
  position: absolute;
  top: -180px; left: 50%;
  transform: translateX(-50%);
  width: 760px; height: 600px;
  background: radial-gradient(circle, var(--nx-accent-glow) 0%, transparent 65%);
  pointer-events: none;
}

.cta-band h2 { max-width: 20ch; margin-inline: auto; }
.cta-band .lede { margin: 26px auto 42px; text-align: center; max-width: 52ch; }

/* ============================================================
   HOME — Hero
   ============================================================ */

/* The hero measured 1024px tall against a 900px viewport, which put the primary
   CTA (bottom 914px) and the marquee below the fold on a standard laptop. */
/* ============================================================
   HERO — full-bleed detection frame
   Ambient.ai and Verkada both lead with a full-bleed image of the
   product in a real place, type laid over it. Ours is a real CCTV
   frame with real bounding boxes and real confidence scores, which
   is the one asset a competitor cannot copy.
   ============================================================ */

.hero {
  position: relative;
  min-height: min(88vh, 820px);
  display: flex;
  align-items: center;
  padding-top: 132px;
  padding-bottom: 96px;
  overflow: hidden;
  text-align: left;
}

.hero-shot { position: absolute; inset: 0; z-index: 0; }

.hero-shot img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 74% 46%;
}

/* Scrim: opaque enough on the left for type, clearing to the right so the
   detection boxes stay readable as evidence. */
.hero-shot::after {
  content: '';
  position: absolute;
  inset: 0;
  background:
    linear-gradient(90deg, rgba(5,7,10,0.96) 0%, rgba(5,7,10,0.88) 30%, rgba(5,7,10,0.42) 58%, rgba(5,7,10,0.12) 100%),
    linear-gradient(180deg, rgba(5,7,10,0.97) 0%, rgba(5,7,10,0.90) 9%, rgba(5,7,10,0.18) 32%, rgba(5,7,10,0.50) 76%, var(--bg) 100%);
}

.hero .wrap { position: relative; z-index: 1; }
.hero-copy { max-width: 760px; }

.hero h1 {
  font-size: clamp(42px, 5.6vw, 82px);
  line-height: 1.0;
  max-width: none;
  margin: 0 0 26px;
}

.hero .lede {
  max-width: 50ch;
  margin: 0 0 38px;
  font-size: clamp(17px, 1.35vw, 21px);
}

.hero .btn-row { justify-content: flex-start; }

.hero-badge {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  font-family: var(--sans);
  font-size: 13.5px;
  font-weight: 600;
  letter-spacing: 0.01em;
  color: var(--text-2);
  border: 1px solid var(--nx-border-lit);
  background: rgba(5,7,10,0.55);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  padding: 9px 16px;
  margin-bottom: 26px;
}

.hero-badge b { color: var(--nx-accent); font-weight: 500; }

.hero-credit {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  bottom: 28px;
  z-index: 1;
  width: 100%;
  max-width: var(--maxw);
  padding-inline: var(--gutter);
  display: flex;
  align-items: center;
  gap: 10px;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--text-3);
}

.hc-dot {
  width: 6px; height: 6px;
  background: var(--nx-accent);
  box-shadow: 0 0 10px var(--nx-accent);
  animation: breathe 1.6s ease infinite;
  flex: none;
}

@media (max-width: 900px) {
  .hero { min-height: 0; padding-top: 116px; padding-bottom: 92px; }
  .hero-shot img { object-position: 66% 50%; }
  .hero-shot::after {
    background:
      linear-gradient(180deg, rgba(5,7,10,0.90) 0%, rgba(5,7,10,0.72) 40%, rgba(5,7,10,0.88) 74%, var(--bg) 100%);
  }
  .hero-credit { font-size: 10px; letter-spacing: 0.12em; bottom: 20px; }
  .hero-badge { font-size: 9.5px; letter-spacing: 0.1em; padding: 8px 12px; }
}

/* Hero live feed mock */
.feed {
  position: relative;
  border: 1px solid var(--nx-border-lit);
  background: #000;
  aspect-ratio: 16/10;
  overflow: hidden;
  box-shadow: 0 40px 120px -30px rgba(0,0,0,0.9), 0 0 0 1px rgba(0,255,136,0.08);
}

.feed video, .feed img {
  position: absolute;
  inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
}

.feed-scrim {
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, rgba(0,0,0,0.55) 0%, transparent 26%, transparent 62%, rgba(0,0,0,0.8) 100%);
  pointer-events: none;
}

.feed-hud {
  position: absolute;
  inset: 0;
  padding: 14px 16px;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  font-family: var(--mono);
  font-size: 10.5px;
  letter-spacing: 0.1em;
  color: rgba(255,255,255,0.82);
  pointer-events: none;
  text-shadow: 0 1px 4px rgba(0,0,0,0.9);
}

.hud-row { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; }

.hud-live {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  color: var(--danger);
  font-weight: 500;
}

.hud-live::before {
  content: '';
  width: 6px; height: 6px;
  border-radius: 50%;
  background: var(--danger);
  box-shadow: 0 0 8px var(--danger);
  animation: breathe 1.1s ease infinite;
}

.hud-log { display: flex; flex-direction: column; gap: 3px; font-size: 10px; }
.hud-log b { color: var(--danger); font-weight: 400; }

/* Corner brackets on feed */
.feed-bracket {
  position: absolute;
  width: 22px; height: 22px;
  border-color: var(--nx-accent);
  opacity: 0.75;
  pointer-events: none;
}
.fb-tl { top: 10px; left: 10px; border-top: 1.5px solid; border-left: 1.5px solid; }
.fb-tr { top: 10px; right: 10px; border-top: 1.5px solid; border-right: 1.5px solid; }
.fb-bl { bottom: 10px; left: 10px; border-bottom: 1.5px solid; border-left: 1.5px solid; }
.fb-br { bottom: 10px; right: 10px; border-bottom: 1.5px solid; border-right: 1.5px solid; }

/* ============================================================
   Workflow SVG diagram
   ============================================================ */

.flow-stage {
  border: 1px solid var(--nx-border);
  background:
    radial-gradient(ellipse 80% 60% at 50% 0%, rgba(0,255,136,0.045) 0%, transparent 70%),
    linear-gradient(180deg, var(--surface) 0%, var(--bg) 70%);
  padding: 32px 24px;
  position: relative;
  overflow: hidden;
}

.flow-stage::before {
  content: '';
  position: absolute;
  inset: 0;
  background-image:
    linear-gradient(rgba(255,255,255,0.018) 1px, transparent 1px),
    linear-gradient(90deg, rgba(255,255,255,0.018) 1px, transparent 1px);
  background-size: 32px 32px;
  pointer-events: none;
  mask-image: radial-gradient(ellipse 70% 70% at 50% 50%, #000 20%, transparent 80%);
  -webkit-mask-image: radial-gradient(ellipse 70% 70% at 50% 50%, #000 20%, transparent 80%);
}

.flow-svg { width: 100%; height: auto; position: relative; z-index: 1; }

/* The schematic is authored at 1920 wide. Below ~1200 the smallest labels
   fall under 5px and turn to texture, so scroll it rather than shrink it. */
.flow-hint { display: none; }

@media (max-width: 1200px) {
  .flow-stage {
    overflow-x: auto;
    overscroll-behavior-x: contain;
    padding: 18px 0;
    scrollbar-width: thin;
    scrollbar-color: var(--nx-border-lit) transparent;
  }
  .flow-stage::-webkit-scrollbar { height: 6px; }
  .flow-stage::-webkit-scrollbar-thumb { background: var(--nx-border-lit); }
  .flow-stage > [data-flow] { min-width: 960px; padding-inline: 14px; }
  /* below 1200 the diagram scrolls horizontally, so say so as well */
  .path-hint-swipe { display: inline; }

  .flow-hint {
    display: flex;
    align-items: center;
    gap: 10px;
    font-family: var(--mono);
    font-size: 11px;
    letter-spacing: 0.18em;
    text-transform: uppercase;
    color: var(--text-3);
    padding: 12px 2px 0;
  }
  .flow-hint::before {
    content: '';
    width: 22px; height: 1px;
    background: var(--nx-accent);
    animation: hintslide 1.8s ease-in-out infinite;
  }
}

@keyframes hintslide {
  0%, 100% { transform: translateX(0); opacity: 0.4; }
  50% { transform: translateX(8px); opacity: 1; }
}

/* "Swipe the teardown to read the callouts" is true between 900 and 1200, and
   false below 900.

   Two different breakpoints own this. `.flow-hint` appears under 1200px, but
   hive-explode.js switches the teardown at 900px: it crops the viewBox to the
   stack and re-emits the six callouts as an ordinary <ol class="xl-list">
   underneath. At that point nothing scrolls horizontally — measured at 390 and
   360, document scrollWidth equals clientWidth — so the hint was instructing a
   gesture that does nothing, about callouts already sitting in a list directly
   below it.

   Scoped to #teardown on purpose. /platform's flow schematic keeps the same
   hint and still genuinely scrolls at every width under 1200. */
@media (max-width: 900px) {
  #teardown .flow-hint { display: none; }
}

.flow-caption {
  position: absolute;
  top: 18px; right: 22px;
  font-family: var(--sans);
  font-size: 13px;
  font-weight: 500;
  letter-spacing: 0.01em;
  color: var(--nx-accent);
  opacity: 0.55;
  z-index: 2;
}

/* SVG animation classes */
.dash-flow {
  stroke-dasharray: 9 5;
  animation: dashmove 1.4s linear infinite;
}
@keyframes dashmove { to { stroke-dashoffset: -28; } }

.dash-slow {
  stroke-dasharray: 4 8;
  animation: dashmove 3.2s linear infinite;
}

.pulse-ring { animation: pulsering 3s ease-in-out infinite; transform-box: fill-box; transform-origin: center; }
@keyframes pulsering {
  0%, 100% { opacity: 0.12; }
  50% { opacity: 0.42; }
}

.blink { animation: blink 1.6s steps(1) infinite; }
@keyframes blink { 0%, 60% { opacity: 1; } 61%, 100% { opacity: 0.25; } }

.blink-slow { animation: blink 2.6s steps(1) infinite; }

.node-in {
  opacity: 0;
  animation: nodein 0.7s var(--ease) forwards;
  animation-delay: var(--d, 0s);
}
@keyframes nodein {
  from { opacity: 0; transform: translateY(12px); }
  to { opacity: 1; transform: none; }
}

.scan-line { animation: scansweep 4s ease-in-out infinite; }
@keyframes scansweep {
  0%, 100% { transform: translateY(0); opacity: 0; }
  10% { opacity: 0.8; }
  50% { transform: translateY(86px); opacity: 0.8; }
  90% { opacity: 0; }
}

.bar-grow { transform-box: fill-box; transform-origin: bottom; animation: bargrow 3.4s ease-in-out infinite alternate; }
@keyframes bargrow { from { transform: scaleY(0.45); } to { transform: scaleY(1); } }

/* Flow legend below diagram.
   Superseded by .flow-rail (Phase 5.2) and no longer referenced by any markup —
   kept only because the two responsive overrides further down still name it and
   removing all three is a separate, verifiable cleanup. Do not add new uses. */
.flow-legend {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 1px;
  background: var(--nx-border);
  border: 1px solid var(--nx-border);
  border-top: none;
}


/* ============================================================
   Hive exploded view — scroll-scrubbed teardown
   One --p (0 → 1) is set on .scrub by hive-explode.js; every
   layer derives its travel from it, so there is no per-frame JS
   beyond writing that single custom property.
   ============================================================ */

.scrub {
  --p: 0;
  position: relative;
  height: 340vh;
}

/* While a rig is being scrubbed, stop everything looping inside it.

   The signal path carries 31 infinite SVG animations in the same subtree whose
   --p is rewritten on every scroll frame, so each frame invalidated both and
   the scrub dropped 69% of its frames on a 6x-throttled CPU. Paused for the
   length of the gesture that is 24%. The attribute is set by the rig driver in
   hive-explode.js and cleared 140ms after scrolling stops. Costs nothing on
   /hive, which has nothing animating inside its rig to pause. */
.scrub[data-scrubbing] svg * {
  animation-play-state: paused;
}

.scrub-stage {
  position: sticky;
  top: 0;
  height: 100vh;
  display: grid;
  place-items: center;
  overflow: hidden;
}

/* ============================================================
   Phase 5 — the flow pass (ref: thewatch.60fps.fr)
   ============================================================

   Shared vocabulary for the two pinned chapters, and only those two: the
   signal path on / and the Hive teardown on /hive. The reference pins ONE
   subject for 34,000px, which works because it has one subject. We have nine
   bands and a CTA to reach, so two pinned moments read as deliberate where
   five would read as a gimmick.

   Everything below is inert unless a .scrub opts in, so none of it can leak
   into an ordinary band. */

/* ---- 5.1 chapter counter ---- */
.scrub-counter {
  position: absolute;
  /* Clears the fixed nav. The stage is pinned at top:0, so anything above 64px
     here sits behind the nav bar — the counter was invisible at 28px. */
  top: 88px;
  left: var(--gutter, 40px);
  z-index: 4;
  font-family: var(--mono);
  font-size: 12px;
  letter-spacing: 0.22em;
  color: var(--nx-accent-mid);
  pointer-events: none;
  font-variant-numeric: tabular-nums;
}
.scrub.is-flat .scrub-counter { display: none; }

/* ---- 5.3 registration marks ----
   A centre line, a + tick at the top, and hairlines running off to both
   gutters. This extends the drawn container grid the site already has rather
   than inventing a second language, so it is all one absolutely-positioned
   layer with no markup beyond a single empty div. */
.scrub-reg {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  opacity: 0.55;
}
.scrub-reg::before {
  /* centre line */
  content: '';
  position: absolute;
  left: 50%; top: 0; bottom: 0;
  width: 1px;
  background: linear-gradient(180deg,
    transparent 0%, var(--nx-border) 14%, var(--nx-border) 86%, transparent 100%);
}
.scrub-reg::after {
  /* + tick, drawn as a crosshair from two gradients on one box.
     Below the nav, like the counter. */
  content: '';
  position: absolute;
  left: 50%; top: 92px;
  width: 13px; height: 13px;
  transform: translateX(-50%);
  background:
    linear-gradient(var(--nx-accent-low), var(--nx-accent-low)) 50% 0 / 1px 100% no-repeat,
    linear-gradient(var(--nx-accent-low), var(--nx-accent-low)) 0 50% / 100% 1px no-repeat;
}
/* hairlines to both gutters, sitting on the optical centre of the stage */
.scrub-rule {
  position: absolute;
  top: 50%;
  height: 1px;
  width: calc(var(--gutter, 40px) + 26px);
  background: var(--nx-border);
  pointer-events: none;
  z-index: 1;
}
.scrub-rule.is-l { left: 0; }
.scrub-rule.is-r { right: 0; }

/* ---- 5.5 two-tone stacked headline ----
   New rhythm without a new font: first line recedes, second lands. */
.stack-h {
  font-family: var(--serif);
  font-weight: 400;
  font-size: clamp(30px, 3.4vw, 50px);
  line-height: 1.04;
  letter-spacing: -0.01em;
  margin: 0 0 18px;
  color: var(--text);
}
.stack-h > span {
  display: block;
  color: var(--text-3);
}

/* ---- 5.6 giant corner numeral ----
   One per pinned chapter, unlabelled, deliberately unexplained. Distinct from
   the stat rail, which always labels its numbers. Outline rather than fill so
   it reads as a graphic and never competes with the copy. */
.scrub-numeral {
  position: absolute;
  right: var(--gutter, 40px);
  bottom: 34px;
  z-index: 1;
  margin: 0;
  font-family: var(--sans);
  font-weight: 800;
  font-size: clamp(64px, 9vw, 152px);
  line-height: 0.8;
  letter-spacing: -0.03em;
  color: transparent;
  -webkit-text-stroke: 1px var(--nx-border-lit);
  user-select: none;
  pointer-events: none;
}

/* ---- 5.4 oversized type behind the subject ----
   Reuses .foot-word's outline treatment rather than inventing one. Sits behind
   the artwork and is occluded by it, which is the whole point — it is texture,
   not a heading, so it is aria-hidden in the markup. */
.scrub-word {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  z-index: 0;
  font-family: var(--sans);
  font-weight: 800;
  /* 20vw, not 26vw. At 26vw "HIVE" measured 1453px wide in a 1440px viewport —
     both outer letters ran off the edge, so what was left read as two floating
     rectangles rather than a word. The reference lets the subject occlude the
     MIDDLE of its type; the ends have to survive for that to read. */
  font-size: clamp(90px, 20vw, 300px);
  line-height: 0.76;
  letter-spacing: -0.03em;
  white-space: nowrap;
  color: transparent;
  -webkit-text-stroke: 1px var(--nx-border-lit);
  opacity: 0.62;
  user-select: none;
  pointer-events: none;
}

@media (max-width: 900px) {
  /* The pinned rig is already dropped below 900. Take the decoration with it —
     at this width the marks crowd the artwork and the numeral collides with
     the copy. */
  .scrub-reg, .scrub-rule, .scrub-numeral, .scrub-word { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .scrub-reg, .scrub-rule { display: none; }
}


/* Short viewports: the stage is pinned at top:0 and its contents are centred,
   so on a laptop screen the accumulated rail grows up past the fixed nav and
   "01 — CAPTURE" disappears behind it. Measured at 1024x700 the rail topped out
   at y=14, under a 64px nav.

   Clear the nav, and buy the height back out of the rail's own padding and type
   so the clearance does not just move the clipping to the bottom edge. */
@media (max-height: 860px) {
  #signal-path .scrub-stage { padding-top: 64px; }
  .flow-rail .leg { padding-top: 7px; padding-bottom: 7px; }
  .flow-rail .leg h4, .flow-rail .leg .h4 { font-size: 16px; margin-bottom: 4px; }
  .flow-rail .leg > p { font-size: 13.5px; line-height: 1.55; }
  .flow-rail .leg.is-spent > p { font-size: 12.5px; line-height: 1.45; }
  .leg-lat { margin-top: 7px; }
}

@media (max-width: 1100px) {
  .flow-pin { grid-template-columns: minmax(260px, 34%) 1fr; gap: 30px; }
}

/* Below 900 the rig is already flat. Put the signal path back to a plain
   stacked read: diagram first, then all five legs at full contrast in the
   original five-up rhythm, with no state and no latency reveal to chase. */
@media (max-width: 900px) {
  /* .scrub-stage is `place-items: center`, which is right while pinned but
     stops the flat layout stretching: the grid area sizes to content, so
     `repeat(auto-fit, minmax(240px,1fr))` laid the rail out as THREE 240px
     columns inside a 390px screen. Measured paragraphs at left=289, 536 and
     782, all running off the right edge — three hard failures in
     check:mobile. Stretch the stage and pin the rail to one column. */
  /* minmax(0, 1fr), not the default auto track.

     The diagram sets `.flow-stage > [data-flow] { min-width: 960px }` below
     1200 and scrolls it inside an overflow-x box. An `auto` grid track sizes to
     max-content, so that 960px floor propagated up and made the whole column
     960px wide inside a 390px screen — every paragraph in the rail inherited it
     and ran ~600px off the right edge. minmax(0, …) lets the track shrink below
     its content and hands the overflow back to .flow-stage, which is built to
     scroll. */
  .scrub-stage { justify-items: stretch; grid-template-columns: minmax(0, 1fr); }
  .flow-pin {
    /* flex, not block — `order` below needs a flex or grid parent to mean
       anything. As `display: block` it was silently ignored and the diagram sat
       underneath the five legs, the reverse of the reading order the section
       has always had: show the path, then explain its stages. */
    display: flex;
    flex-direction: column;
    /* width:100% and max-width:100%, NOT width:auto.

       .flow-pin is also a .wrap, and .wrap sets `margin-inline: auto`. Auto
       margins on a grid item beat `justify-items: stretch` and put the box back
       into shrink-to-fit, which resolves to max-content — and max-content here
       is the 960px diagram floor. The track was correctly 390px while the item
       inside it measured 1030px. */
    width: 100%;
    max-width: 100%;
    min-width: 0;
    margin-inline: 0;
    padding-block: 8px 0;
  }
  .flow-art { min-width: 0; max-width: 100%; }
  .flow-rail {
    display: grid;
    grid-template-columns: 1fr;
    gap: 1px;
    background: var(--nx-border);
    border: 1px solid var(--nx-border);
    border-top: none;
    margin-top: 24px;
  }
  .flow-rail .leg {
    background: var(--bg);
    opacity: 1;
    padding: 22px 20px;
    margin-left: 0;
    border-left: none;
  }
  /* max-height, matching the mechanism the pinned rail actually uses. This said
     `grid-template-rows: 1fr` — left over from the first implementation — so it
     reset a property nothing reads any more and the base `max-height: 0` stayed
     in force. Every paragraph in the flat rail was collapsed: five headings and
     no copy. */
  .flow-rail .leg > p,
  .flow-rail .leg.is-spent > p {
    max-height: none;
    opacity: 1;
    font-size: 14px;
    line-height: 1.6;
  }
  .flow-rail .leg .leg-lat { height: 14px; opacity: 1; }
  .flow-art .flow-stage { max-height: none; }
  /* diagram above the legs, matching the old reading order */
  .flow-pin .flow-art { order: -1; }
}

.xl-svg {
  width: 100%;
  height: auto;
  max-height: 88vh;
  display: block;
  /* sealed unit sits closer to camera, pulls back as the stack opens */
  transform: scale(calc(1.22 - var(--p) * 0.22));
  transform-origin: center center;
}

/* layer travel — collapsed stack opens into the exploded stack */
.xl { transition: none; }
.xl-1 { transform: translateY(calc(var(--p) * -355px)); }
.xl-2 { transform: translateY(calc(var(--p) * -213px)); }
.xl-3 { transform: translateY(calc(var(--p) *  -71px)); }
.xl-4 { transform: translateY(calc(var(--p) *   71px)); }
.xl-5 { transform: translateY(calc(var(--p) *  213px)); }
.xl-6 { transform: translateY(calc(var(--p) *  355px)); }

/* callouts only earn their place once the stack has opened */
.xl-label { opacity: clamp(0, calc((var(--p) - 0.34) * 3.4), 1); }

/* the sealed-unit caption fades as it opens */
.xl-closed { opacity: clamp(0, calc(1 - var(--p) * 4), 1); }

.scrub-rail {
  position: absolute;
  left: 50%;
  bottom: 42px;
  transform: translateX(-50%);
  width: min(340px, 60vw);
  height: 2px;
  background: var(--nx-border);
  opacity: clamp(0, calc(1 - var(--p) * 1.6), 1);
}
.scrub-rail::after {
  content: '';
  position: absolute;
  inset: 0;
  background: var(--nx-accent);
  transform: scaleX(var(--p));
  transform-origin: left;
}

/* The callout list only exists for the phone layout; on desktop the labels are
   drawn inside the SVG with leader lines. */
.xl-list { display: none; }

@media (max-width: 900px) {
  /* No sticky scrub on touch: present the teardown already open.

     This used to scroll sideways through a 1040px canvas instead of shrinking,
     on the reasoning that the callouts are unreadable below ~1040px. In
     practice that was worse than either: the artwork is 1500 units wide with
     the labels off to the right, so a phone landed on empty margin, showed a
     third of the stack, no labels at all, and no cue that sideways scrolling
     was even possible.

     So the two halves separate instead. hive-explode.js crops the viewBox to
     the stack alone, which lets the diagram fill the screen at a readable size,
     and re-emits the same six callouts as an ordinary list beneath it. Nothing
     scrolls sideways and nothing is unreadable. */
  .scrub { height: auto; }
  .scrub-stage {
    position: static;
    height: auto;
    padding-block: 32px 0;
  }
  .xl-svg { max-height: none; transform: none; }
  .xl-label { display: none; }
  .scrub-rail { display: none; }

  .xl-list {
    display: grid;
    gap: 2px;
    list-style: none;
    margin: 24px 0 0;
    padding: 0;
    border-top: 1px solid var(--nx-border);
  }
  .xl-list li {
    display: flex;
    gap: 14px;
    align-items: baseline;
    padding: 13px 0;
    border-bottom: 1px solid var(--nx-border);
  }
  .xl-list b {
    font-family: var(--mono);
    font-size: 12px;
    font-weight: 400;
    letter-spacing: 0.16em;
    color: var(--nx-accent);
    flex: none;
  }
  .xl-list span { display: block; }
  .xl-list strong {
    display: block;
    font-family: var(--sans);
    font-size: 16px;
    font-weight: 600;
    color: var(--text);
    margin-bottom: 3px;
  }
  .xl-list span {
    font-family: var(--mono);
    font-size: 12.5px;
    font-weight: 300;
    line-height: 1.5;
    color: var(--text-3);
  }
}

@media (prefers-reduced-motion: reduce) {
  .scrub { height: auto; }
  .scrub-stage { position: static; height: auto; }
  .scrub-rail { display: none; }
}

/* ============================================================
   Detection showcase (video tiles)
   ============================================================ */

.det-tabs {
  display: flex;
  gap: 0;
  border: 1px solid var(--nx-border);
  border-bottom: none;
  overflow-x: auto;
  scrollbar-width: none;
}
.det-tabs::-webkit-scrollbar { display: none; }

/* The strip is 600px of tabs. At 390px the container is 344px, so tabs 3 and
   4 begin off-screen at 473px and 623px — and because the scrollbar above is
   hidden, nothing said they were there. The sibling .flow-stage solves the
   same problem with a visible scrollbar plus a "swipe" line; a strip of tabs
   is small enough that a fade on the trailing edge carries it instead.
   Only below 720px, where the strip actually overflows. */
@media (max-width: 720px) {
  .det-tabs {
    -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 44px), transparent 100%);
    mask-image: linear-gradient(to right, #000 calc(100% - 44px), transparent 100%);
  }
}

.det-tab {
  flex: 1;
  min-width: 150px;
  background: var(--bg);
  border: none;
  border-right: 1px solid var(--nx-border);
  padding: 20px 18px;
  text-align: left;
  color: var(--text-3);
  font-family: var(--sans);
  font-size: 15px;
  font-weight: 600;
  letter-spacing: 0.01em;
  transition: background 0.3s, color 0.3s;
  position: relative;
}

.det-tab:last-child { border-right: none; }
.det-tab:hover { background: var(--surface); color: var(--text); }

.det-tab[aria-selected="true"] {
  background: var(--surface);
  color: var(--nx-accent);
}

.det-tab[aria-selected="true"]::after {
  content: '';
  position: absolute;
  left: 0; bottom: -1px;
  width: 100%; height: 2px;
  background: var(--nx-accent);
  z-index: 2;
}

/* --text-3, not --text-4: these sublabels sit on --surface rather than the
   page ground, where text-4 measures 4.27:1 — the one place the token comment
   warns about. text-3 clears 4.5:1 on both. */
.det-tab i {
  display: block;
  font-style: normal;
  font-size: 10px;
  letter-spacing: 0.1em;
  color: var(--text-3);
  margin-top: 5px;
}

.det-stage {
  display: grid;
  grid-template-columns: 1.55fr 1fr;
  border: 1px solid var(--nx-border);
  background: var(--bg);
}

.det-video {
  position: relative;
  background: #000;
  aspect-ratio: 16/9;
  overflow: hidden;
  border-right: 1px solid var(--nx-border);
}

.det-video video, .det-video img {
  width: 100%; height: 100%;
  object-fit: cover;
}

/* The CSS 180 rotation that lived here is gone, and deliberately so.

   It corrected demo_deterence2.mp4, which came off an inverted camera mount and
   was never rotated at source. The client then supplied "detection new.mp4",
   which IS corrected at source — pool at the top, both people upright, and the
   burned-in overlay mirrored as a consequence. Rotating that would have put it
   back upside down.

   The rule is removed rather than left inert: an unused transform keyed to an
   attribute is exactly the kind of thing that gets re-applied by accident to
   the next clip someone drops in. */

.det-panel { padding: 40px 36px; display: flex; flex-direction: column; }

.det-panel h3 { margin-bottom: 14px; }
.det-panel > p { font-family: var(--sans); font-size: 16px; font-weight: 300; line-height: 1.7; color: var(--text-2); margin-bottom: 28px; }

.det-meta { list-style: none; display: flex; flex-direction: column; gap: 0; margin-top: auto; border-top: 1px solid var(--nx-border); }

.det-meta li {
  display: flex;
  justify-content: space-between;
  gap: 16px;
  padding: 13px 0;
  border-bottom: 1px solid var(--nx-border);
  font-family: var(--mono);
  font-size: 12.5px;
}
.det-meta li:last-child { border-bottom: none; }
.det-meta span { font-family: var(--sans); font-weight: 500; color: var(--text-3); letter-spacing: 0.01em; font-size: 13.5px; }
.det-meta b { color: var(--nx-accent); font-weight: 400; }

.det-pane[hidden] { display: none; }

/* Play badge overlay */
.play-badge {
  position: absolute;
  bottom: 14px; left: 16px;
  z-index: 3;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-family: var(--sans);
  font-size: 12.5px;
  font-weight: 600;
  letter-spacing: 0.01em;
  color: rgba(255,255,255,0.92);
  background: rgba(0,0,0,0.62);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  border: 1px solid rgba(255,255,255,0.14);
  padding: 7px 13px;
  pointer-events: none;
}
.play-badge::before {
  content: '';
  width: 6px; height: 6px;
  background: var(--nx-accent);
  border-radius: 50%;
  box-shadow: 0 0 8px var(--nx-accent);
  animation: breathe 1.8s ease infinite;
}

/* ============================================================
   Split feature rows
   ============================================================ */

.split {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 72px;
  align-items: center;
}
/* Grid items default to min-width:auto, which means they refuse to shrink
   below their content's intrinsic width. On /contact at 360px that let the
   form panel settle at 360.61px inside a 360px column, making the whole page
   horizontally scrollable by 1px — enough to rubber-band on iOS. Every other
   route measured 360 exactly. */
.split > * { min-width: 0; }

.split.flip .split-media { order: -1; }

.split-media {
  position: relative;
  border: 1px solid var(--nx-border);
  background: var(--surface);
  overflow: hidden;
}

.split-media img, .split-media video { width: 100%; height: auto; display: block; }

/* Wide banner art, at the full measure of its section rather than squeezed into
   a split column. See the note in hive.astro. */
.arrival-band {
  margin-top: 48px;
  border: 1px solid var(--nx-border);
  background: var(--surface);
  overflow: hidden;
}
.arrival-band img { width: 100%; height: auto; display: block; }

.split-body h2 { font-size: clamp(30px, 3.4vw, 46px); margin-bottom: 22px; }

.check-list { list-style: none; margin-top: 30px; display: flex; flex-direction: column; gap: 16px; }

.check-list li {
  display: flex;
  gap: 14px;
  font-family: var(--sans);
  font-size: 16px;
  font-weight: 300;
  line-height: 1.65;
  color: var(--text-2);
}

.check-list li::before {
  content: '';
  flex: none;
  width: 6px;
  height: 11px;
  margin: 6px 8px 0 4px;
  border: solid var(--nx-accent);
  border-width: 0 1.6px 1.6px 0;
  transform: rotate(45deg);
}

/* ============================================================
   Table / spec list
   ============================================================ */

.spec-table { border: 1px solid var(--nx-border); }

.spec-row {
  display: grid;
  grid-template-columns: 220px 1fr 150px;
  border-bottom: 1px solid var(--nx-border);
  transition: background 0.3s;
}
.spec-row:last-child { border-bottom: none; }
.spec-row:hover { background: var(--surface); }

.spec-k {
  padding: 26px 30px;
  border-right: 1px solid var(--nx-border);
  font-family: var(--sans);
  font-size: 14.5px;
  font-weight: 600;
  letter-spacing: 0.01em;
  color: var(--nx-accent-mid);
  display: flex;
  align-items: center;
}

/* Block, not flex — unlike .spec-k and .spec-x above.

   This cell is the only one of the three that holds prose with inline markup
   (<strong>, and the "coming soon" chips). As a flex container it blockified
   every inline child into a flex item and dropped the whitespace around it, so
   "Everything marked <strong>shipping</strong> in the catalogue" rendered as
   "markedshippingin". 17 cells across /pilot, /hive and /platform were hit.

   align-self keeps the vertical centring that display:flex was providing.
   .spec-k and .spec-x must stay stretched — they draw the column dividers with
   border-right / border-left, which collapse to nothing if the cell is
   centred rather than filling the row. */
/* Sans, not mono — Phase 7.1 finished 2026-08-19.
   7.1 reported prose-in-mono at 34 -> 0. Re-measured across all ten routes it
   was 51, and .spec-v was 34 of them on its own: it was never in 7.1's list of
   twelve rules, because the name says "value" and the assumption was that it
   held values. It does not. It holds four-line explanatory sentences, up to
   187 characters on /privacy.
   The key (.spec-k) and the tag (.spec-x) stay mono. Those are a label and a
   status, they are compared down the column, and they are the brand signature
   7.1 correctly protected. This cell is prose and reads as prose. */
.spec-v {
  /* ran to ~132 characters per line on /platform, /hive and /privacy at
     desktop width; comfortable reading tops out around 75 */
  max-width: 72ch;
  padding: 26px 30px;
  font-family: var(--sans);
  font-size: 15px;
  font-weight: 300;
  color: var(--text-2);
  line-height: 1.7;
  display: block;
  align-self: center;
}
.spec-v strong { color: var(--heading); font-weight: 400; }

.spec-x {
  padding: 26px 30px;
  border-left: 1px solid var(--nx-border);
  font-family: var(--mono);
  font-size: 12px;
  letter-spacing: 0.12em;
  color: var(--text-3);
  display: flex;
  align-items: center;
}

/* ============================================================
   Pricing
   ============================================================ */

.price-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1px;
  background: var(--nx-border);
  border: 1px solid var(--nx-border);
}

.price-card {
  background: var(--bg);
  padding: 46px 38px;
  display: flex;
  flex-direction: column;
  position: relative;
  transition: background 0.4s;
}

.price-card:hover { background: var(--surface); }

/* The green wash is translucent for its first 42%, and the card sits in a
   1px grid whose gaps are painted --nx-border. With no colour of its own the
   card was a window onto that border colour, and the sub-head landing at 10%
   read 4.39:1 — an AA failure caused by a background nobody intended. The
   opaque layer beneath is the card ground the other two plans already have. */
.price-card.featured {
  background:
    linear-gradient(180deg, rgba(0,255,136,0.05) 0%, var(--surface) 42%),
    var(--surface);
}
/* ---- the beam on the recommended plan ----

   This was a React island (Magic UI's BorderBeam). It worked, but measured on
   the production build it cost 46.3KB gzipped of `motion` — and because it was
   the last React component on the site, it also kept the 55.8KB React runtime
   alive on this page after the nav was converted to plain Astro. 102KB for one
   moving line.

   Same effect in CSS, 0KB. A narrow bright arc in a conic gradient, rotated by
   animating a registered custom property, masked down to the 1.5px border ring.
   Rotating a conic gradient around a rectangle reads as a beam travelling the
   edge, which is what the island was drawing along an offset-path.

   @property is what makes it animate at all — a plain custom property is a
   string to the animation engine and would jump from 0 to 360 with nothing in
   between. Where @property is unsupported the gradient still paints, so the
   fallback is a static green glow on one corner rather than a broken card. */
@property --nx-beam {
  syntax: '<angle>';
  initial-value: 0deg;
  inherits: false;
}

.price-card.featured::before {
  content: '';
  position: absolute;
  inset: 0;
  padding: 1.5px;                 /* the ring thickness */
  background: conic-gradient(
    from var(--nx-beam),
    transparent 0 58%,
    var(--nx-accent-mid) 72%,
    var(--nx-accent) 80%,
    transparent 86% 100%
  );
  /* paint only the ring: fill minus content-box, so the middle is cut out */
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
          mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask-composite: exclude;
  animation: nx-beam-travel 7s linear infinite;
  pointer-events: none;
  z-index: 1;
}
@keyframes nx-beam-travel { to { --nx-beam: 360deg; } }

/* the flag sits in the top-right corner, exactly where the ring passes */
.price-card.featured .price-flag { z-index: 2; }

/* Decoration is precisely what a person with vestibular sensitivity wants
   stopped, so this gets no exemption. The card keeps its green wash, so the
   "most sites land here" emphasis survives without the movement. */
@media (prefers-reduced-motion: reduce) {
  .price-card.featured::before { animation: none; opacity: 0.5; }
}

.price-flag {
  position: absolute;
  top: 0; right: 0;
  font-family: var(--sans);
  font-size: 12.5px;
  font-weight: 600;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  background: var(--nx-accent);
  color: var(--bg);
  padding: 6px 14px;
}

.price-name {
  font-family: var(--sans);
  font-weight: 700;
  font-size: 22px;
  color: #fff;
  margin-bottom: 8px;
  letter-spacing: -0.01em;
}

/* Prose, not a value — see the .spec-v note. Three tier descriptions, two
   lines each, previously 12.5px mono. */
.price-for {
  font-family: var(--sans);
  font-size: 14px;
  font-weight: 300;
  color: var(--text-3);
  line-height: 1.7;
  min-height: 44px;
  margin-bottom: 28px;
}

.price-figure {
  padding-block: 24px;
  border-top: 1px solid var(--nx-border);
  border-bottom: 1px solid var(--nx-border);
  margin-bottom: 30px;
}

.price-cams {
  font-family: var(--serif);
  font-size: 44px;
  line-height: 1;
  color: #fff;
  margin-bottom: 6px;
}
.price-cams i { font-style: normal; font-size: 17px; color: var(--nx-accent); font-family: var(--mono); letter-spacing: 0.1em; margin-left: 4px; }

.price-terms {
  font-family: var(--mono);
  font-size: 11.5px;
  letter-spacing: 0.1em;
  color: var(--text-3);
  line-height: 1.8;
}
.price-terms b { color: var(--nx-accent); font-weight: 400; }

.price-feats { list-style: none; display: flex; flex-direction: column; gap: 13px; margin-bottom: 36px; }

.price-feats li {
  display: flex;
  gap: 11px;
  font-family: var(--sans);
  font-size: 15px;
  font-weight: 300;
  line-height: 1.6;
  color: var(--text-2);
}
.price-feats li::before { content: '+'; color: var(--nx-accent); flex: none; }
.price-feats li.off { color: var(--text-3); }
.price-feats li.off::before { content: '−'; color: var(--text-4); }

.price-card .btn { margin-top: auto; justify-content: center; }

/* Comparison table */
.cmp { width: 100%; border-collapse: collapse; border: 1px solid var(--nx-border); font-family: var(--mono); }
.cmp th, .cmp td { padding: 17px 22px; border-bottom: 1px solid var(--nx-border); text-align: left; font-size: 13px; font-weight: 300; }
.cmp thead th {
  font-size: 12.5px; letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--nx-accent); font-weight: 400; background: var(--surface);
  border-bottom: 1px solid var(--nx-border-lit);
}
.cmp tbody th { color: var(--text-2); font-weight: 400; border-right: 1px solid var(--nx-border); }
.cmp td { color: var(--text-3); text-align: center; }
/* The one prose column in a table of values. /pricing's comparison table is all
   short values and stays mono throughout, which 7.1 was right about;
   /capabilities carries a full sentence per row in the same table shape.
   Replaces a per-cell inline `text-align:left`. */
.cmp td.desc {
  font-family: var(--sans);
  font-size: 14.5px;
  line-height: 1.65;
  text-align: left;
  color: var(--text-2);
}
.cmp td.yes { color: var(--nx-accent); }
.cmp td.no { color: var(--text-3); }
.cmp tbody tr:hover { background: var(--surface); }
.cmp tbody tr:last-child th, .cmp tbody tr:last-child td { border-bottom: none; }

.table-scroll { overflow-x: auto; }

/* ============================================================
   FAQ / accordion
   ============================================================ */

.faq { border: 1px solid var(--nx-border); }

.faq-item { border-bottom: 1px solid var(--nx-border); }
.faq-item:last-child { border-bottom: none; }

.faq-q {
  width: 100%;
  background: none;
  border: none;
  text-align: left;
  padding: 26px 30px;
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 24px;
  color: #fff;
  font-family: var(--sans);
  font-weight: 600;
  font-size: 16.5px;
  transition: background 0.3s, color 0.3s;
}

.faq-q:hover { background: var(--surface); color: var(--nx-accent); }

.faq-q .sign {
  flex: none;
  width: 20px; height: 20px;
  position: relative;
  border: 1px solid var(--nx-border-lit);
}
.faq-q .sign::before, .faq-q .sign::after {
  content: '';
  position: absolute;
  background: var(--nx-accent);
  top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  transition: transform 0.35s var(--ease);
}
.faq-q .sign::before { width: 9px; height: 1px; }
.faq-q .sign::after { width: 1px; height: 9px; }
.faq-q[aria-expanded="true"] .sign::after { transform: translate(-50%, -50%) scaleY(0); }

.faq-a {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 0.4s var(--ease);
}
.faq-a.open { grid-template-rows: 1fr; }
.faq-a > div { overflow: hidden; }
.faq-a p {
  font-family: var(--sans);
  font-size: 16px;
  font-weight: 300;
  line-height: 1.7;
  color: var(--text-3);
  padding: 0 30px 28px;
  max-width: 76ch;
}

/* ============================================================
   Forms
   ============================================================ */

.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 22px; }
.field-full { grid-column: 1 / -1; }

/* The label has to beat the placeholder, and it was losing.

   Measured 2026-08-20: label 11px at 5.32:1, placeholder 14px at 4.27:1. The
   placeholder was 27% larger than the thing naming it, in near enough the same
   contrast, carrying realistic content ("Jane Okafor", "Okafor Auto Parts") —
   so each field read as already filled in, on the one page that asks a visitor
   to do work. The label is now bigger than it was, brighter, and less
   stretched; the placeholder is dimmer than the label and clears AA (see
   ::placeholder below). */
.field label {
  display: block;
  font-family: var(--sans);
  font-size: 14.5px;
  font-weight: 500;
  letter-spacing: 0.01em;
  color: var(--text-2);
  margin-bottom: 10px;
}

.field input, .field select, .field textarea {
  width: 100%;
  background: var(--surface);
  border: 1px solid var(--nx-border);
  color: var(--text);
  font-family: var(--sans);
  font-size: 15.5px;
  font-weight: 400;
  padding: 14px 16px;
  transition: border-color 0.3s, background 0.3s;
  border-radius: 0;
}

.field textarea { resize: vertical; min-height: 130px; line-height: 1.7; }

.field input:focus, .field select:focus, .field textarea:focus {
  outline: none;
  border-color: var(--nx-accent);
  background: var(--surface-2);
}

/* --text-4 measured 4.27:1 on the field fill — below AA, and placeholder text
   is text. The 2026-08-20 contrast probe missed it because ::placeholder is a
   pseudo-element and that probe walks real elements only. --text-3 clears 4.5
   and still sits a clear step below the label above it. */
.field input::placeholder, .field textarea::placeholder { color: var(--text-3); }

.field select {
  appearance: none;
  background-image: linear-gradient(45deg, transparent 50%, var(--nx-accent) 50%), linear-gradient(135deg, var(--nx-accent) 50%, transparent 50%);
  background-position: calc(100% - 20px) center, calc(100% - 14px) center;
  background-size: 6px 6px, 6px 6px;
  background-repeat: no-repeat;
  padding-right: 44px;
}

.field select option { background: var(--surface); color: var(--text); }

/* --text-3, not --text-4: this is 14px prose under the form fields, not a
   micro-label, and on the panel gradient text-4 measures 4.48:1. */
.form-note {
  font-family: var(--sans);
  font-size: 14px;
  color: var(--text-3);
  line-height: 1.7;
}

/* Contact info blocks */
.contact-line {
  display: flex;
  gap: 16px;
  padding: 22px 0;
  border-bottom: 1px solid var(--nx-border);
}
.contact-line:last-child { border-bottom: none; }
.contact-line .ci { flex: none; width: 34px; height: 34px; border: 1px solid var(--nx-border-lit); display: grid; place-items: center; color: var(--nx-accent); }
.contact-line .ci svg { width: 16px; height: 16px; }
/* The text column, next to the fixed 34px icon. Flex items default to
   min-width:auto, so this refused to shrink below the widest unbreakable run
   it contained — and it contains prabhjeev@nexoviasecuritysolution.com, 37
   characters of mono with no break opportunity. At 360px that pushed the
   column to 288.61px in a 288px space and made the whole page scroll
   horizontally by 1px. Both halves are needed: min-width lets the column
   shrink, overflow-wrap lets the address itself break. */
.contact-line > div:not(.ci) { min-width: 0; }
.contact-line h4, .contact-line .h4 { font-size: 14px; margin-bottom: 4px; }
/* Split 2026-08-19. 7.1 kept .contact-line in mono because it holds "emails,
   phone" — true of the <a>, which is an address you read character by character
   and which stays mono. The <p> beside it is ordinary prose, 105 and 116
   characters, and was only riding on the same selector. */
.contact-line a { font-family: var(--mono); font-size: 13px; font-weight: 300; color: var(--text-3); line-height: 1.7; overflow-wrap: anywhere; }
.contact-line p { font-family: var(--sans); font-size: 15px; font-weight: 300; color: var(--text-3); line-height: 1.7; overflow-wrap: anywhere; }
.contact-line a:hover { color: var(--nx-accent); }

/* ============================================================
   Team (About)
   ============================================================ */

.team-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1px; background: var(--nx-border); border: 1px solid var(--nx-border); }

.member { background: var(--bg); padding: 38px 32px; transition: background 0.35s; }
.member:hover { background: var(--surface); }

.member-av {
  width: 76px; height: 76px;
  border: 1px solid var(--nx-border-lit);
  display: grid;
  place-items: center;
  margin-bottom: 24px;
  font-family: var(--serif);
  font-size: 30px;
  color: var(--nx-accent);
  background: var(--surface);
  position: relative;
  overflow: hidden;
}
.member-av img { width: 100%; height: 100%; object-fit: cover; }

.member h4 { font-size: 18px; margin-bottom: 5px; }
.member .role { font-family: var(--sans); font-size: 14px; font-weight: 500; letter-spacing: 0.01em; color: var(--nx-accent-mid); margin-bottom: 16px; }
.member p { font-family: var(--sans); font-size: 15px; font-weight: 300; line-height: 1.65; color: var(--text-3); }

.placeholder-note {
  border: 1px dashed var(--nx-border-lit);
  background: rgba(255,176,32,0.035);
  padding: 20px 24px;
  font-family: var(--sans);
  font-size: 14px;
  font-weight: 400;
  line-height: 1.75;
  color: var(--warn);
  margin-bottom: 32px;
}
.placeholder-note b { font-weight: 500; letter-spacing: 0.1em; text-transform: uppercase; font-size: 11px; display: block; margin-bottom: 6px; }

/* "Coming soon" — public-facing, unlike .placeholder-note above, which is an
   internal note to the client in amber and must never ship.

   Deliberately quiet: this marks content we have chosen not to invent yet
   (pilot commercial terms, founder bios). It should read as composure, not as
   an unfinished page — so it borrows the accent at low opacity rather than the
   warning colour, and never uses a dashed border. */
.soon-note {
  border: 1px solid var(--nx-border-lit);
  border-left: 2px solid var(--nx-accent-mid, var(--nx-accent));
  background: rgba(0, 255, 136, 0.028);
  padding: 20px 24px;
  font-family: var(--sans);
  font-size: 15px;
  line-height: 1.7;
  color: var(--text-3);
  margin-bottom: 32px;
}
.soon-note b {
  font-family: var(--mono);
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  font-size: 12px;
  color: var(--nx-accent);
  display: block;
  margin-bottom: 8px;
}

/* under a bio */
.soon-inline {
  font-family: var(--sans);
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.01em;
  color: var(--text-3);
  opacity: 0.72;
  margin-top: 14px;
}
.soon-inline::before {
  content: '';
  display: inline-block;
  width: 5px; height: 5px;
  margin-right: 9px;
  vertical-align: 1px;
  background: var(--nx-accent);
  opacity: 0.55;
}

/* inline chip inside a spec table cell */
.soon {
  display: inline-block;
  font-family: var(--sans);
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.01em;
  color: var(--nx-accent);
  border: 1px solid var(--nx-border-lit);
  background: rgba(0, 255, 136, 0.05);
  padding: 3px 9px;
  margin-left: 4px;
  white-space: nowrap;
}

/* Timeline */
.timeline { border-left: 1px solid var(--nx-border); padding-left: 0; margin-left: 8px; }

.tl-item { position: relative; padding: 0 0 44px 40px; }
.tl-item:last-child { padding-bottom: 0; }

.tl-item::before {
  content: '';
  position: absolute;
  left: -5px; top: 6px;
  width: 9px; height: 9px;
  background: var(--bg);
  border: 1.5px solid var(--nx-accent);
  border-radius: 50%;
}

.tl-date { font-family: var(--mono); font-size: 12px; letter-spacing: 0.2em; text-transform: uppercase; color: var(--nx-accent-mid); margin-bottom: 9px; }
.tl-item h4, .tl-item .h4 { font-size: 18px; margin-bottom: 9px; }
.tl-item p { font-family: var(--sans); font-size: 15px; font-weight: 300; line-height: 1.65; color: var(--text-3); max-width: 62ch; }

/* ============================================================
   Industry cards
   ============================================================ */

.ind-card {
  background: var(--bg);
  border: 1px solid var(--nx-border);
  overflow: hidden;
  display: flex;
  flex-direction: column;
  transition: border-color 0.4s, transform 0.4s var(--ease);
}
.ind-card:hover { border-color: var(--nx-border-lit); transform: translateY(-3px); }

.ind-visual {
  aspect-ratio: 16/9;
  background: var(--surface);
  position: relative;
  overflow: hidden;
  border-bottom: 1px solid var(--nx-border);
}
.ind-visual img { width: 100%; height: 100%; object-fit: cover; opacity: 0.72; transition: opacity 0.4s, transform 0.6s var(--ease); }
.ind-card:hover .ind-visual img { opacity: 1; transform: scale(1.04); }

.ind-visual .chip {
  position: absolute;
  top: 12px; right: 12px;
  font-family: var(--sans);
  font-size: 12.5px;
  font-weight: 600;
  letter-spacing: 0.01em;
  background: rgba(0,0,0,0.7);
  backdrop-filter: blur(6px);
  border: 1px solid rgba(0,255,136,0.3);
  color: var(--nx-accent);
  padding: 5px 11px;
}

.ind-body { padding: 30px 28px; flex: 1; display: flex; flex-direction: column; }

/* Sectors without their own footage get a numbered text card rather than an
   empty media well — a 16:9 void reads as a broken image, not as restraint. */
.ind-card-text .ind-body { padding: 34px 28px 30px; }

.ind-num {
  display: flex;
  align-items: center;
  gap: 12px;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.22em;
  color: var(--nx-accent-mid);
  margin-bottom: 18px;
}
.ind-num::after {
  content: '';
  flex: 1;
  height: 1px;
  background: var(--nx-border);
}
.ind-body h3 { font-size: 21px; margin-bottom: 12px; }
.ind-body > p { font-family: var(--sans); font-size: 16px; font-weight: 300; line-height: 1.7; color: var(--text-3); margin-bottom: 22px; }

.chips { display: flex; flex-wrap: wrap; gap: 7px; margin-top: auto; }
.chips span {
  font-family: var(--sans);
  font-size: 13.5px;
  font-weight: 500;
  letter-spacing: 0.01em;
  color: var(--text-3);
  border: 1px solid var(--nx-border);
  padding: 5px 10px;
}

/* ============================================================
   Misc utility
   ============================================================ */

.mt-0 { margin-top: 0; }
.mt-16 { margin-top: 16px; }
.mt-24 { margin-top: 24px; }
.mt-32 { margin-top: 32px; }
.mt-48 { margin-top: 48px; }
.mt-64 { margin-top: 64px; }
.mt-80 { margin-top: 80px; }
.mb-24 { margin-bottom: 24px; }
.mb-48 { margin-bottom: 48px; }
.mb-64 { margin-bottom: 64px; }

.head-block { max-width: 720px; margin-bottom: 60px; }
.head-block.center { margin-inline: auto; }

.two-col-text { display: grid; grid-template-columns: 1fr 1fr; gap: 56px; }

.kicker-num {
  font-family: var(--serif);
  font-size: 64px;
  line-height: 1;
  color: var(--nx-accent-mid);
  opacity: 0.18;
  margin-bottom: 16px;
}

.logo-img { height: 26px; width: auto; }

.marquee {
  border-block: 1px solid var(--nx-border);
  padding-block: 22px;
  overflow: hidden;
  position: relative;
}
.marquee::before, .marquee::after {
  content: '';
  position: absolute; top: 0; bottom: 0;
  width: 140px; z-index: 2; pointer-events: none;
}
.marquee::before { left: 0; background: linear-gradient(90deg, var(--bg), transparent); }
.marquee::after { right: 0; background: linear-gradient(-90deg, var(--bg), transparent); }

.marquee-track {
  display: flex;
  gap: 60px;
  width: max-content;
  animation: slide 34s linear infinite;
}
@keyframes slide { to { transform: translateX(-50%); } }

.marquee-track span {
  font-family: var(--sans);
  font-size: 15px;
  font-weight: 500;
  letter-spacing: 0.02em;
  color: var(--text-4);
  white-space: nowrap;
  display: flex;
  align-items: center;
  gap: 60px;
}
.marquee-track span::after { content: '◆'; color: var(--nx-accent-low); opacity: 0.4; font-size: 8px; }

/* ============================================================
   Responsive
   ============================================================ */

@media (max-width: 1180px) {
  .hero-inner { grid-template-columns: 1fr; gap: 56px; }
  .hero { padding-top: 160px; }
  .foot-grid { grid-template-columns: 1fr 1fr 1fr; gap: 40px; }
  .foot-brand { grid-column: 1 / -1; }
  .det-stage { grid-template-columns: 1fr; }
  .det-video { border-right: none; border-bottom: 1px solid var(--nx-border); }
  .flow-legend { grid-template-columns: repeat(3, 1fr); }
  .g4 { grid-template-columns: repeat(2, 1fr); }
}

@media (max-width: 900px) {
  :root { --gutter: 22px; }

  .band { padding-block: 80px; }
  .band-lg { padding-block: 96px; }
  .band-sm { padding-block: 56px; }

  .page-head { padding-top: 140px; padding-bottom: 56px; }
  .page-head .wrap { grid-template-columns: 1fr; gap: 44px; }
  .page-head .head-stats { grid-column: 1; grid-row: auto; }

  /* minmax(0, 1fr), not 1fr.

     A bare 1fr is minmax(auto, 1fr), and that auto floor is min-content — so
     any single unbreakable string inside a cell sets the track width and the
     track refuses to shrink below it. On /privacy the string was
     "prabhjeev@nexoviasecuritysolution.com": at 320px the grid was 276px wide
     and the cell measured 311, pushing the document to 334 and giving mobile
     Safari a reason to shrink the whole page to fit — which is what "the site
     shows up as desktop" looks like. minmax(0, …) lets the track shrink and
     the wrap rule below breaks the address instead. */
  .g2, .g3, .g4, .grid { grid-template-columns: minmax(0, 1fr); }
  /* long addresses and URLs break rather than widen their container */
  .cell, .cell p, .cell a { overflow-wrap: anywhere; }
  .stats { grid-template-columns: 1fr 1fr; }
  .stat { border-bottom: 1px solid var(--nx-border); }
  .stat:nth-child(2n) { border-right: none; }

  .split { grid-template-columns: 1fr; gap: 40px; }
  .split.flip .split-media { order: 0; }

  .two-col-text { grid-template-columns: 1fr; gap: 32px; }
  .flow-legend { grid-template-columns: 1fr; }
  .price-grid { grid-template-columns: 1fr; }
  .team-grid { grid-template-columns: 1fr; }
  .form-grid { grid-template-columns: 1fr; }

  .spec-row { grid-template-columns: 1fr; }
  .spec-k { border-right: none; border-bottom: 1px solid var(--nx-border); padding: 18px 22px; }
  .spec-v { padding: 18px 22px; }
  .spec-x { border-left: none; border-top: 1px solid var(--nx-border); padding: 14px 22px; }

  .cell { padding: 34px 26px; }
  .panel { padding: 28px 22px; }
  .flow-stage { padding: 20px 12px; }
  .flow-caption { display: none; }

  .cta-band { padding-block: 96px; }
  .det-panel { padding: 30px 24px; }

  /* Tap targets: footer links render ~17px tall on desktop, which is
     well under the 44px touch minimum. Pad them out on small screens. */
  .foot-col ul { gap: 0; }
  .foot-col a {
    display: block;
    padding-block: 12px;
    font-size: 14px;
  }
  .foot-bar { padding-block: 22px; line-height: 2; }
  /* the bottom-bar links are 15px tall, under the 24px WCAG 2.5.8 minimum,
     and are the CalOPPA privacy link — worth hitting first time */
  .foot-bar a { display: inline-flex; align-items: center; min-height: 44px; }
  .crumb a { display: inline-block; padding-block: 8px; }
}

@media (max-width: 560px) {
  .det-meta li { flex-direction: column; gap: 6px; align-items: flex-start; }
  .stats { grid-template-columns: 1fr; }
  .stat { border-right: none; }
  .foot-grid { grid-template-columns: 1fr 1fr; }
  .btn { width: 100%; justify-content: center; }
  .btn-row { flex-direction: column; align-items: stretch; }
  .btn-line { width: auto; }
}

/* ═══════════════ DEPTH: RIM LIGHT + GRAIN ═══════════════
   Two things sell depth on a near-black page, and neither is a glow. A 1px
   top highlight reads as a physical edge catching light, and a faint grain
   stops large flat fields from banding on cheap panels. Both are applied at
   the threshold of visibility on purpose — you should feel them, not see them. */

.panel,
.panel-lit,
.flow-stage,
.price-card.featured,
.grid > .cell:first-child {
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.055);
}

body::after {
  content: '';
  position: fixed;
  inset: 0;
  z-index: 9999;
  pointer-events: none;
  background: url("data:image/svg+xml,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 width=%27180%27 height=%27180%27%3E%3Cfilter id=%27n%27%3E%3CfeTurbulence type=%27fractalNoise%27 baseFrequency=%270.85%27 numOctaves=%274%27 stitchTiles=%27stitch%27/%3E%3C/filter%3E%3Crect width=%27180%27 height=%27180%27 filter=%27url(%23n)%27/%3E%3C/svg%3E") repeat;
  opacity: 0.028;
  mix-blend-mode: overlay;
}

@media (prefers-reduced-motion: reduce) { body::after { display: none; } }

/* ============================================================
   THE SIGNAL PATH — clicked, not scrubbed  (2026-08-22)
   ============================================================

   Replaces the pinned five-chapter scrub on the homepage. The rationale is in
   the markup comment in src/pages/index.astro; this is only the dressing.

   The one idea worth protecting if this gets refactored: the segment widths
   are the latency budget, set inline as --w (12/18/46/8/16 = 6/9/23/4/8 ms of
   50). The picker is a data graphic that happens to be clickable, not a row of
   tabs that happens to sit above a chart. Do not equalise them. */

.path-pick { margin-top: 52px; }

.path-bar {
  display: flex;
  gap: 2px;
  /* 88px, measured not guessed: the three stacked labels come to 82.6px at
     the body line-height, so 76px clipped every latency figure — the one
     number in the component that has to be readable. Line-heights below are
     pinned for the same reason, so a change to the body leading cannot
     silently re-clip this. */
  height: 88px;
}

.path-seg {
  width: var(--w, 20%);
  min-width: 0;
  background: var(--bg-2);
  border: none;
  border-top: 2px solid var(--nx-border-lit);
  padding: 13px 15px;
  text-align: left;
  overflow: hidden;
  cursor: pointer;
  transition: background 0.3s var(--ease), border-color 0.3s var(--ease);
}

.path-seg:hover { background: var(--surface); }
.path-seg:focus-visible { outline: 2px solid var(--nx-accent); outline-offset: -2px; }

.path-seg.is-on {
  background: var(--surface-2);
  border-top-color: var(--nx-accent);
}

.path-n {
  display: block;
  font-family: var(--mono);
  font-size: 10.5px;
  line-height: 1.2;
  letter-spacing: 0.16em;
  color: var(--text-4);
  white-space: nowrap;
}
.path-seg.is-on .path-n { color: var(--nx-accent-mid); }

.path-t {
  display: block;
  font-size: 14px;
  line-height: 1.25;
  font-weight: 600;
  color: var(--text-3);
  margin-top: 8px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.path-seg.is-on .path-t { color: var(--heading); }

.path-ms {
  display: block;
  font-family: var(--mono);
  font-size: 11.5px;
  line-height: 1.2;
  color: var(--text-4);
  margin-top: 7px;
}
.path-seg.is-on .path-ms { color: var(--nx-accent); }

.path-scale {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  margin-top: 10px;
  font-size: 13px;
  color: var(--text-4);
}
.path-total { font-family: var(--mono); font-size: 12.5px; color: var(--text-3); }
.path-total b { color: var(--nx-accent); font-weight: 400; }

.path-body {
  display: grid;
  grid-template-columns: 340px minmax(0, 1fr);
  gap: 56px;
  margin-top: 44px;
  align-items: start;
}

.path-leg[hidden] { display: none; }
.path-leg h3, .path-leg .h3 { margin-bottom: 14px; }
.path-leg > p {
  font-size: 16px;
  line-height: 1.7;
  color: var(--text-2);
  margin-bottom: 26px;
}

/* key/value rows — the keys are language and set in Syne, the values are
   compared character by character and stay mono */
.path-spec { border-top: 1px solid var(--nx-border); }
.path-spec > div {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 16px;
  padding: 12px 0;
  border-bottom: 1px solid var(--nx-border);
}
.path-spec dt { font-size: 13.5px; color: var(--text-3); }
.path-spec dd {
  margin: 0;
  font-family: var(--mono);
  font-size: 12.5px;
  color: var(--text);
  text-align: right;
}

.path-art { position: relative; min-width: 0; }

/* Says out loud that the diagram is clickable. An SVG has no affordance of
   its own beyond the cursor, and the cursor only helps once you are already
   hovering the right place. */
.path-hint {
  display: flex;
  align-items: center;
  gap: 9px;
  margin: 0;
  padding-top: 14px;
  font-size: 13.5px;
  color: var(--text-4);
}
.path-hint::before {
  content: '';
  width: 6px;
  height: 6px;
  background: var(--nx-accent-low);
  flex: none;
}
.path-hint-swipe { display: none; }

/* The oversized outline numeral stays — kept deliberately. It moved off
   .scrub-numeral when this section stopped being a scrub, but the treatment is
   unchanged: outline rather than fill so it reads as a graphic and never
   competes with the copy, unlabelled and deliberately unexplained. Its
   siblings on /hive (.scrub-numeral, .scrub-word) are untouched. */
.path-figure {
  position: absolute;
  right: 0;
  bottom: -22px;
  z-index: 0;
  margin: 0;
  font-family: var(--sans);
  font-weight: 800;
  font-size: clamp(64px, 9vw, 152px);
  line-height: 0.8;
  letter-spacing: -0.03em;
  color: transparent;
  -webkit-text-stroke: 1px var(--nx-border-lit);
  user-select: none;
  pointer-events: none;
}

/* ---------- stage emphasis ----------
   The whole path stays fully lit. An earlier version dimmed the four stages
   you were not on to 0.24 and lit one — which made the diagram read as a
   slideshow with four stages switched off, when the point of drawing the
   whole path is that it is one continuous journey you can see at once.

   Selection is now marked, not spotlit: the picked stage gets its label in
   the accent and a hairline underline beneath it. Clicking a stage explains
   what it does in the copy beside the diagram; it does not turn the rest of
   the picture off. */
.flow-svg .fx { transition: opacity 0.3s var(--ease); }
[data-flow][data-active] .fx { cursor: pointer; }

/* Hover no longer touches opacity. With every stage at full strength there is
   nothing to lift TO — a hover opacity below 1 reads as the stage dimming,
   which is the opposite of "this is clickable". The affordance is the faint
   wash on the hit rect plus the pointer cursor. */
[data-flow][data-active] .fx:hover .fx-hit { fill-opacity: 0.03; }
.flow-svg .fx-hit { fill: #fff; transition: fill-opacity 0.25s var(--ease); }

/* the picked stage names itself in the accent, the way the eyebrow does */
[data-flow][data-active="1"] .fx[data-fx="1"] .fx-label,
[data-flow][data-active="2"] .fx[data-fx="2"] .fx-label,
[data-flow][data-active="3"] .fx[data-fx="3"] .fx-label,
[data-flow][data-active="4"] .fx[data-fx="4"] .fx-label,
[data-flow][data-active="5"] .fx[data-fx="5"] .fx-label { fill: var(--nx-accent); }

/* and carries a rule under that label, so the selection is legible without
   anything else having to go dark */
.fx-rule { opacity: 0; transition: opacity 0.3s var(--ease); }
[data-flow][data-active="1"] .fx[data-fx="1"] .fx-rule,
[data-flow][data-active="2"] .fx[data-fx="2"] .fx-rule,
[data-flow][data-active="3"] .fx[data-fx="3"] .fx-rule,
[data-flow][data-active="4"] .fx[data-fx="4"] .fx-rule,
[data-flow][data-active="5"] .fx[data-fx="5"] .fx-rule { opacity: 1; }

@media (prefers-reduced-motion: reduce) {
  .flow-svg .fx { transition: none; }
}

@media (max-width: 1100px) {
  .path-body { grid-template-columns: minmax(260px, 34%) minmax(0, 1fr); gap: 32px; }
}

@media (max-width: 900px) {
  /* Diagram first, then the picker, then the copy — same reading order the
     section has always had: show the path, then explain its stages.

     align-items MUST be reset to stretch here. The base rule sets
     align-items:start, which is right for the two-column grid (top-align
     the copy against the diagram) — but the moment this becomes a flex
     COLUMN the cross axis is width, and start shrink-wraps every item to
     max-content. The diagram carries min-width:960px below 1200, so
     .path-art measured 986px inside a 346px parent and pushed the whole
     document to 1008px wide; the fixed nav then overflowed on every phone
     size. Measured, not guessed — scripts/mobile-check.mjs caught it on
     all three viewports. */
  .path-body { display: flex; flex-direction: column; align-items: stretch; gap: 28px; }
  .path-art { order: -1; min-width: 0; max-width: 100%; }
  .path-bar { height: 68px; gap: 1px; }
  /* Every segment clears 44px even at 8% of a 390px screen. */
  .path-seg { min-width: 56px; padding: 9px 8px; }
  .path-t { display: none; }
  .path-n { font-size: 11px; }
  .path-ms { margin-top: 6px; font-size: 12px; }
  .path-scale { font-size: 12px; }
  .path-pick { margin-top: 36px; }
  /* same call the pinned rig made: below 900 the numeral collides with copy */
  .path-figure { display: none; }
}

/* ============================================================
   AI detection overlay on a clip  (2026-08-22)
   ============================================================

   Three of the four detection clips carry the model's boxes burned into the
   export. The fall capture does not — its box only exists on the poster still —
   so it is drawn here instead, over the <video>, which is what the product
   actually does to a live feed anyway. Burning it in was not an option: the
   ffmpeg on this machine (the one inside the Remotion package) is built with
   --disable-filters and has no drawbox, drawtext or overlay.

   Geometry is declared per clip in the markup as --l/--t/--w/--h percentages,
   and `data-at` is the second the detection lands. site.js adds .is-on then, so
   the box snaps in on the fall rather than sitting over an empty floor for the
   first three seconds. */
.ai-box {
  position: absolute;
  left: var(--l);
  top: var(--t);
  width: var(--w);
  height: var(--h);
  border: 2px solid var(--nx-accent);
  z-index: 2;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.12s linear;
}
.ai-box.is-on { opacity: 1; }

/* the label chip sits on the box's top edge, like the burned-in ones do */
.ai-box b {
  position: absolute;
  left: -2px;
  bottom: 100%;
  display: block;
  font-family: var(--mono);
  font-size: 12px;
  font-weight: 400;
  letter-spacing: 0.04em;
  line-height: 1;
  padding: 4px 7px;
  background: var(--nx-accent);
  color: #03080c;
  white-space: nowrap;
}

/* Corner ticks, so it reads as an instrument rather than a plain rectangle —
   the same registration language as the section ticks. */
.ai-box i {
  position: absolute;
  width: 9px;
  height: 9px;
  border: 0 solid #fff;
}
.ai-box i:nth-child(1) { top: -2px;    left: -2px;    border-top-width: 2px;    border-left-width: 2px; }
.ai-box i:nth-child(2) { top: -2px;    right: -2px;   border-top-width: 2px;    border-right-width: 2px; }
.ai-box i:nth-child(3) { bottom: -2px; left: -2px;    border-bottom-width: 2px; border-left-width: 2px; }
.ai-box i:nth-child(4) { bottom: -2px; right: -2px;   border-bottom-width: 2px; border-right-width: 2px; }

@media (prefers-reduced-motion: reduce) {
  .ai-box { transition: none; }
}

@media (max-width: 760px) {
  .ai-box b { font-size: 10.5px; padding: 3px 5px; }
  .ai-box i { width: 7px; height: 7px; }
}
