/* Gryphus Defence
   Cold blue-grey base with a single warm thermal accent, per the build brief.
   Red is kept for one thing only: the seeker's LOCK and TRACK states, which the
   handover preserves as correct instrument behaviour. The signature element is
   the survey frame, a cartographic reference grid the page is registered to. */

/* ------------------------------------------------------------- tokens */
:root{
  /* build brief palette: cold blue-grey base, one warm thermal accent */
  --canvas:#0D1417;           /* --base */
  --canvas-2:#141D21;         /* --surface */
  --panel:#141D21;
  --line:#253238;
  --line-strong:#35474F;      /* hairlines, hover borders */
  --line-input:#5A7078;       /* 3.57:1, interactive control borders only */
  --accent:#E08A34;           /* thermal amber, 6.97:1 as text on base */
  --accent-dim:#A5652A;       /* brief's --accent-dim: borders and underlines only */
  --accent-hover:#EE9C4C;     /* filled-button hover. --accent-dim behind a label
                                 measures 4.06:1, under AA, so the fill lifts
                                 instead of dimming and the label holds 8.6:1 */
  --accent-muted:rgba(224,138,52,.16);
  --on-accent:#14100A;        /* 7.08:1 on the amber fill */
  --hud-search:#E08A34;
  --hud-lock:#FF5C4A;         /* kept red: lock and track are instrument states */
  --text-primary:#F0F4F5;     /* --text-bright */
  --text-secondary:#C6D1D5;   /* --text, 12:1 */
  --text-tertiary:#7C8C93;    /* --text-dim, 5.4:1 */
  --hot:#F0F4F5;

  --display:"Familjen Grotesk","Helvetica Neue",Arial,sans-serif;
  --body:"Instrument Sans","Helvetica Neue",Arial,sans-serif;

  --ease:cubic-bezier(.22,.61,.36,1);
  --ease-out:cubic-bezier(.16,1,.3,1);
  --tshadow:0 1px 2px rgba(7,11,14,.95),0 3px 12px rgba(7,11,14,.78),0 10px 44px rgba(7,11,14,.8);

  --hero-tail:1200px;      /* see .hero — the terminal effect plays out here */
  --gut:clamp(20px,5vw,72px);
  --maxw:1320px;
}

*,*::before,*::after{box-sizing:border-box}

html,body{overflow-x:hidden;overflow-x:clip}
html{-webkit-text-size-adjust:100%;scroll-behavior:smooth}

body{
  margin:0;
  background:var(--canvas);
  color:var(--text-primary);
  font-family:var(--body);
  font-size:clamp(15px,1.02vw,17px);
  line-height:1.62;
  font-feature-settings:"tnum" 1;
  -webkit-font-smoothing:antialiased;
  /* The page-load fade no longer waits on JavaScript. It used to start at
     opacity 0 and clear only once site.js added a class, so any delay in the
     script left the whole page illegible, and a capture taken during the ramp
     read as "heavily dimmed at scroll zero". This is a pure CSS animation at
     250ms: if it never runs, the page is simply visible. */
  animation:pageIn .25s var(--ease) both;
}
@keyframes pageIn{from{opacity:0}to{opacity:1}}
@media (prefers-reduced-motion:reduce){body{animation:none}}

img{max-width:100%;height:auto;display:block}
::selection{background:var(--accent-muted);color:var(--text-primary)}

/* ------------------------------------------------------ type primitives */
h1,h2,h3{
  font-family:var(--display);
  font-weight:600;
  line-height:1.06;
  letter-spacing:-.018em;
  margin:0;
  text-wrap:balance;
}
h1{font-size:clamp(2.4rem,6.2vw,5.1rem)}
h2{font-size:clamp(1.9rem,4.1vw,3.35rem)}
h3{font-size:clamp(1.14rem,1.62vw,1.44rem);letter-spacing:-.008em;line-height:1.24}
p{margin:0}

.lede{
  font-size:clamp(1.06rem,1.5vw,1.35rem);
  color:var(--text-secondary);max-width:46ch;
}
.ref{
  font-size:clamp(10.5px,.78vw,11.5px);font-weight:500;
  letter-spacing:.19em;text-transform:uppercase;
  color:var(--accent);
  font-variant-numeric:tabular-nums lining-nums;flex:none;
}

.sr-only{
  position:absolute;width:1px;height:1px;padding:0;margin:-1px;
  overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0;
}
.skip{
  position:absolute;left:-9999px;top:0;z-index:200;
  background:var(--accent);color:var(--on-accent);padding:14px 22px;
  font-weight:600;border-radius:0 0 4px 0;
}
.skip:focus{left:0}

a{color:inherit}
:focus-visible{outline:2px solid var(--accent);outline-offset:3px;border-radius:2px}

/* --------------------------------------------- the survey frame (signature) */
.survey{
  position:fixed;inset:0;z-index:0;pointer-events:none;
  background-image:
    linear-gradient(to right,var(--line) 1px,transparent 1px),
    linear-gradient(to bottom,var(--line) 1px,transparent 1px);
  background-size:96px 96px;
  opacity:.42;
  mask-image:radial-gradient(ellipse 120% 100% at 50% 30%,#000 20%,rgba(0,0,0,.35) 70%,transparent 100%);
  animation:surveyDrift 96s linear infinite;
  animation-delay:-31s;
}
@keyframes surveyDrift{
  from{transform:translate3d(0,0,0)}
  to{transform:translate3d(-96px,-96px,0)}
}
.bezel{
  position:fixed;left:0;top:0;bottom:0;width:14px;z-index:1;pointer-events:none;
  background-image:repeating-linear-gradient(to bottom,var(--line-strong) 0 1px,transparent 1px 34px);
  opacity:.3;
  mask-image:linear-gradient(to bottom,transparent,#000 18%,#000 82%,transparent);
}
.bezel::after{content:"";position:absolute;left:0;top:0;bottom:0;width:1px;background:var(--line)}
@media (max-width:900px){.bezel{display:none}}

.wrap{
  position:relative;z-index:2;
  width:100%;max-width:var(--maxw);margin:0 auto;padding:0 var(--gut);
}

/* ------------------------------------------------------------ the logo
   The owner's artwork: black ink on transparent, recoloured with the original
   alpha kept, split into three assets so the lockup can be built rather than
   nudged. One width drives everything. GRYPHUS and DEFENCE are 913 and 908
   source pixels wide, so they are the same length in the original; both are set
   to --wordw. The mark's height is derived from GRYPHUS's own aspect ratio
   (913/86), which centres the bird on the cap line by construction. Sizing by
   height instead put the bird a quarter of a lockup low and left DEFENCE 17%
   short, because its aspect ratio is twice as wide. */
.brand{
  --wordw:205px;
  display:grid;
  grid-template-columns:auto auto;
  column-gap:13px;
  align-items:start;
  text-decoration:none;flex:none;min-width:0;
}
.brand .mark     {grid-column:1;grid-row:1;height:calc(var(--wordw) / 10.6163);width:auto}
.brand .word-main{grid-column:2;grid-row:1;width:var(--wordw);height:auto}
.brand .word-sub {grid-column:2;grid-row:2;width:var(--wordw);height:auto;margin-top:5px}

/* The footer lockup used to run larger than the header one. It now inherits
   --wordw from .brand so the two match at every breakpoint, including the
   520px step below — one width still drives every dimension. */

@media (max-width:520px){
  .brand{--wordw:174px;column-gap:10px}
  .brand .word-sub{margin-top:4px}
}

/* --------------------------------------------------------------- nav */
.nav{
  position:fixed;top:0;left:0;right:0;z-index:60;
  display:flex;align-items:center;justify-content:space-between;
  gap:24px;padding:14px var(--gut);
  transition:background .45s var(--ease),border-color .45s var(--ease);
  border-bottom:1px solid transparent;
}
.nav.scrolled{
  background:rgba(13,20,23,.84);
  backdrop-filter:blur(14px);
  border-bottom-color:var(--line);
}
.menu{display:flex;align-items:baseline;gap:clamp(18px,2.2vw,34px)}
.menu a{
  text-decoration:none;color:var(--text-secondary);font-size:.9rem;
  transition:color .3s var(--ease);white-space:nowrap;position:relative;
}
.menu a::after{
  content:"";position:absolute;left:0;right:100%;bottom:-6px;height:1px;
  background:var(--accent);transition:right .38s var(--ease-out);
}
.menu a:hover{color:var(--text-primary)}
.menu a:hover::after{right:0}
.burger{
  display:none;background:none;border:1px solid var(--line-strong);
  color:var(--text-primary);width:44px;height:44px;border-radius:3px;
  align-items:center;justify-content:center;cursor:pointer;
}
.burger span{display:block;width:17px;height:1px;background:currentColor;position:relative}
.burger span::before,.burger span::after{
  content:"";position:absolute;left:0;width:17px;height:1px;background:currentColor;
}
.burger span::before{top:-5px}
.burger span::after{top:5px}

/* --------------------------------------------------------------- buttons */
.btn{
  display:inline-flex;align-items:center;justify-content:center;gap:10px;
  padding:12px 22px;min-height:44px;
  font-family:var(--body);font-size:.9rem;font-weight:500;
  text-decoration:none;border-radius:3px;cursor:pointer;
  border:1px solid transparent;
  transition:background .3s var(--ease),color .3s var(--ease),border-color .3s var(--ease);
  text-shadow:none;
}
.btn-primary{background:var(--accent);color:var(--on-accent);border-color:var(--accent)}
.btn-primary:hover{background:var(--accent-hover);border-color:var(--accent-hover)}
.btn-ghost{border-color:var(--line-strong);color:var(--text-primary);background:transparent}
.btn-ghost:hover{border-color:var(--accent);color:var(--accent)}
.btn .arw{transition:transform .35s var(--ease-out)}
.btn:hover .arw{transform:translateX(3px)}

/* ============================================================== THE HERO
   Review item 9: the sequence used to cost about six viewport-heights of scroll
   to deliver four lines. 400vh leaves 300vh of scroll range. The brief asked for two, but four beats
   cannot each hold five 120px flicks inside two, so the owner chose three. */
/* The hero is 400vh of sequence plus a fixed 1200px tail.
   The tail is dead scroll on purpose: progress is already 1 across it and the
   stage is still pinned, so the terminal effect resolves while the reader is
   still looking at it. It is in PIXELS, not vh, because the effect takes a
   fixed 1.6s regardless of viewport — a vh-based tail shrinks on the short
   windows that need it most, and at 1280x720 the label was landing 570px
   after the hero had already released. */
.hero{position:relative;height:calc(400vh + var(--hero-tail));z-index:2}
.stage{position:sticky;top:0;height:100vh;height:100svh;overflow:hidden;background:#101619}
.poster{
  position:absolute;inset:0;background-size:cover;background-position:50% 50%;
  background-repeat:no-repeat;
  transform-origin:68.8% 51.2%;
  will-change:transform;
}
html:not(.js) .poster{background-image:url('hero-ending.jpg')}
body.static-hero .poster{transform:none !important;transform-origin:50% 50%}
#hero-video{
  position:absolute;inset:0;width:100%;height:100%;
  object-fit:cover;object-position:50% 50%;
  opacity:0;transition:opacity .8s var(--ease);
  will-change:transform;transform:translateZ(0);
}
.stage.video-ready #hero-video{opacity:1}
.stage.video-failed #hero-video{display:none}

/* Review item 8. Measured source: copy lane mean 93, p95 103, so bare white text
   already reads 5.11:1 on the brightest frame. The old flat overlay plus radial
   was crushing a frame that did not need it. This is a directional scrim confined
   to the copy side, dying out by 72% so the thermal signature and the track box
   stay bright. The only other darkening is a short band under the nav. */
.scrim{
  position:absolute;inset:0;pointer-events:none;
  background:
    linear-gradient(to bottom,rgba(13,20,23,.62) 0%,rgba(13,20,23,.10) 11%,rgba(13,20,23,0) 18%),
    linear-gradient(100deg,
      rgba(13,20,23,.88) 0%,
      rgba(13,20,23,.72) 28%,
      rgba(13,20,23,.20) 55%,
      rgba(13,20,23,0)   72%);
}
.edge-fade{
  position:absolute;left:0;right:0;bottom:0;height:34vh;pointer-events:none;
  background:linear-gradient(to bottom,rgba(13,20,23,0) 0%,rgba(13,20,23,.55) 46%,var(--canvas) 100%);
}
.frame-marks{position:absolute;inset:0;width:100%;height:100%;pointer-events:none;overflow:visible}
.frame-marks line{stroke:var(--accent);stroke-width:1;opacity:.34;vector-effect:non-scaling-stroke}

/* --------------------------------------------------------- the tracker */
.track{position:absolute;inset:0;pointer-events:none;--acq:0;--lk:0}
.track-box{
  position:absolute;top:0;left:0;
  opacity:var(--acq);
  transition:opacity .2s linear;
  will-change:transform;
  filter:drop-shadow(0 1px 2px rgba(7,11,14,.92)) drop-shadow(0 0 8px rgba(7,11,14,.55));
}
.track-box .cnr{
  position:absolute;width:15%;height:15%;
  border:1.5px solid var(--hud-search);
  opacity:calc(.55 + .45*var(--lk));
  transition:border-color .22s var(--ease);
}
.track-box .cnr.tl{top:0;left:0;border-right:0;border-bottom:0}
.track-box .cnr.tr{top:0;right:0;border-left:0;border-bottom:0}
.track-box .cnr.bl{bottom:0;left:0;border-right:0;border-top:0}
.track-box .cnr.br{bottom:0;right:0;border-left:0;border-top:0}
.track-box .tick{position:absolute;background:var(--hud-lock);opacity:var(--lk)}
.track-box .tick.t{top:-1px;left:50%;width:1.5px;height:9%;transform:translateX(-50%)}
.track-box .tick.b{bottom:-1px;left:50%;width:1.5px;height:9%;transform:translateX(-50%)}
.track-box .tick.l{left:-1px;top:50%;height:1.5px;width:9%;transform:translateY(-50%)}
.track-box .tick.r{right:-1px;top:50%;height:1.5px;width:9%;transform:translateY(-50%)}
.track.is-lock .track-box .cnr,
.track.is-track .track-box .cnr,
.track.is-hold .track-box .cnr{border-color:var(--hud-lock);border-width:2px}
/* the moment it takes the mark: one ring snaps in and dies. It lives on a
   pseudo-element because the drive writes the box's own transform every frame. */
.track-box::after{
  content:"";position:absolute;inset:0;
  border:2px solid var(--hud-lock);opacity:0;pointer-events:none;
}
.track.is-lock .track-box::after{animation:lockFlash .55s var(--ease-out) 1}
@keyframes lockFlash{
  0%{opacity:.85;transform:scale(1.16)}
  100%{opacity:0;transform:scale(1)}
}

.track-hud{
  position:absolute;right:clamp(18px,3.4vw,54px);top:calc(50% + 4.2rem);
  display:flex;justify-content:flex-end;
}
.chip{
  display:inline-flex;align-items:center;gap:9px;
  background:rgba(10,16,19,.62);border:1px solid rgba(124,140,147,.34);
  border-radius:3px;backdrop-filter:blur(10px);padding:6px 12px;
  font-size:10.5px;font-weight:500;letter-spacing:.19em;text-transform:uppercase;
  color:var(--text-primary);text-shadow:0 1px 3px rgba(7,11,14,.85);
}
.chip .dot{width:5px;height:5px;border-radius:50%;background:var(--text-tertiary);flex:none}
.track.is-acquire .chip .dot{background:var(--hud-search)}
.track.is-lock .chip .dot,
.track.is-track .chip .dot,
.track.is-hold .chip .dot{background:var(--hud-lock)}
.track.is-lock .chip,
.track.is-track .chip,
.track.is-hold .chip{border-color:rgba(255,92,74,.5)}
.track.is-lock .chip .dot{animation:pip 1.6s var(--ease) infinite;animation-delay:-.4s}
@keyframes pip{0%,100%{opacity:1}50%{opacity:.32}}

.track.pinned .track-box{
  left:65.5%;right:auto;top:51.9%;bottom:auto;
  transform:translate(-50%,-50%) !important;
  width:min(15vw,270px) !important;height:min(15vw,270px) !important;
  opacity:1;
}
.track.pinned{--acq:1;--lk:1}
/* no local wash: the directional scrim already owns this side of the frame */

/* ---------------------------------------------------------- the bands */
.bands{position:absolute;inset:0;pointer-events:none}
.band{
  position:absolute;left:var(--gut);top:50%;transform:translateY(-50%);
  width:min(48ch,52vw);opacity:0;--k:0;
  text-shadow:var(--tshadow);
}
/* no per-band wash either. The directional scrim covers the copy lane on every
   frame, and stacking a second darkening on top is what made the footage dead. */
.band p{
  font-family:var(--display);font-weight:500;
  font-size:clamp(1.75rem,3.9vw,3.05rem);
  line-height:1.1;letter-spacing:-.016em;color:var(--text-primary);
  padding-bottom:.14em;margin-bottom:-.14em;
}
.band .sub{
  font-family:var(--body);font-weight:400;
  font-size:clamp(.96rem,1.22vw,1.1rem);line-height:1.6;letter-spacing:0;
  color:var(--text-secondary);margin-top:20px;max-width:40ch;
  opacity:var(--ks,0);
}
.band .cta-row{margin-top:26px;display:flex;gap:12px;flex-wrap:wrap;opacity:var(--kb,0)}
.band.on .cta-row{pointer-events:auto}
.band:not(.on) .cta-row{pointer-events:none}
.band.settle{--ks:clamp(0,(var(--k,0) - .58)*3.4,1);--kb:clamp(0,(var(--k,0) - .74)*4,1)}

.split{display:inline}
.w{display:inline-block;white-space:pre}
.c{display:inline-block;will-change:transform,opacity}

[data-split="rise"] .w{
  --kc:clamp(0,(var(--k,0) - var(--th,0))*2.8,1);
  opacity:var(--kc);transform:translateY(calc((1 - var(--kc))*26px));
}
[data-split="punch"] .w{
  --kc:clamp(0,(var(--k,0) - var(--th,0))*3,1);
  --ks2:clamp(0,(var(--k,0) - var(--th,0) - .1)*3.2,1);
  opacity:var(--kc);
  transform:scale(calc(.66 + .46*var(--kc) - .12*var(--ks2)));
}
[data-split="drift"] .w{
  --kc:clamp(0,(var(--k,0) - var(--th,0))*2.8,1);
  opacity:var(--kc);transform:translateY(calc((1 - var(--kc))*-26px));
}

/* --------------------------------------------------- the loading ring */
.ring{
  position:absolute;right:clamp(18px,3.4vw,54px);bottom:clamp(20px,4vh,42px);
  width:34px;height:34px;color:var(--accent);
  transform:rotate(-90deg);opacity:.85;transition:opacity .5s var(--ease);
}
.ring circle{transition:stroke-dashoffset .12s linear}
.ring.done{opacity:0}
.chev{
  position:absolute;right:clamp(18px,3.4vw,54px);bottom:clamp(20px,4vh,42px);
  width:30px;height:30px;color:var(--text-tertiary);
  animation:chev 2.6s var(--ease) infinite;animation-delay:-.7s;
}
@keyframes chev{0%,100%{transform:translateY(0);opacity:.55}50%{transform:translateY(6px);opacity:1}}

@media (max-height:560px){.track-hud,.ring,.chev{display:none}}

/* ================================================= THE MOBILE HERO
   Review item 10. Not a fallback: the sequence runs as a short loop inside a
   framed screen, the states are named beneath it, and the hero sits in normal
   flow instead of pretending to be a full-bleed cover. */
.mobile-hero{display:none}
body.static-hero .hero{height:auto}
body.static-hero .stage{position:static;height:auto;overflow:visible;background:transparent}
body.static-hero .poster,
body.static-hero #hero-video,
body.static-hero .scrim,
body.static-hero .frame-marks,
body.static-hero .track,
body.static-hero .edge-fade,
body.static-hero .bands,
body.static-hero .ring,
body.static-hero .chev{display:none}

body.static-hero .mobile-hero{
  display:block;padding:clamp(20px,4vh,32px) var(--gut) clamp(28px,6vh,52px);
}

.mh-screen{
  position:relative;aspect-ratio:16/9;width:100%;
  background:#101619;border:1px solid var(--line);border-radius:2px;overflow:hidden;
}
.mh-video,.mh-still{
  position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;
}
.mh-still[hidden]{display:none}
.mh-track{
  position:absolute;left:66%;top:52%;
  width:var(--mw,26%);aspect-ratio:1/1;transform:translate(-50%,-50%);
  pointer-events:none;opacity:var(--mk,0);
  filter:drop-shadow(0 1px 2px rgba(7,11,14,.92));
  transition:width .18s linear,opacity .2s linear;
}
.mh-track .cnr{position:absolute;width:22%;height:22%;border:1.5px solid var(--hud-search)}
.mh-track .cnr.tl{top:0;left:0;border-right:0;border-bottom:0}
.mh-track .cnr.tr{top:0;right:0;border-left:0;border-bottom:0}
.mh-track .cnr.bl{bottom:0;left:0;border-right:0;border-top:0}
.mh-track .cnr.br{bottom:0;right:0;border-left:0;border-top:0}
.mh-track.locked .cnr{border-color:var(--hud-lock);border-width:2px}
.mh-chip{position:absolute;right:10px;bottom:10px}

.mh-captions{
  list-style:none;margin:clamp(16px,3vh,24px) 0 0;padding:0;display:grid;gap:0;
  counter-reset:mh;
}
.mh-captions li{
  position:relative;padding:13px 0 13px 32px;
  border-top:1px solid var(--line);
  font-family:var(--display);font-weight:500;
  font-size:clamp(1rem,4.1vw,1.16rem);line-height:1.3;letter-spacing:-.01em;
  color:var(--text-tertiary);
  transition:color .3s var(--ease);
}
.mh-captions li:last-child{border-bottom:1px solid var(--line)}
.mh-captions li::before{
  counter-increment:mh;content:counter(mh,decimal-leading-zero);
  position:absolute;left:0;top:16px;
  font-family:var(--body);font-size:.72rem;font-weight:500;letter-spacing:.14em;
  color:var(--line-strong);font-variant-numeric:tabular-nums;
  transition:color .3s var(--ease);
}
.mh-captions li.on{color:var(--text-primary)}
.mh-captions li.on::before{color:var(--accent)}
.mobile-hero .cta-row{margin-top:clamp(20px,4vh,30px);display:flex}
.mobile-hero .cta-row .btn{width:100%}

/* reduced motion, or a metered connection: the settled frame and every caption
   readable at once, with the bracket already locked */
body.static-hero.mh-still-mode .mh-video{display:none}
body.static-hero.mh-still-mode .mh-still{display:block}
body.static-hero.mh-still-mode .mh-track{opacity:1;--mw:26%}
body.static-hero.mh-still-mode .mh-captions li{color:var(--text-secondary)}
body.static-hero.mh-still-mode .mh-captions li::before{color:var(--accent)}
/* ========================================================== SECTIONS
   Build brief §2: rhythm set once on .section and never overridden per-section,
   headings in a left column with content in a wider right column, prose capped
   at 34em, radius 2px on interactive elements and 0 everywhere else, no shadows.
   Build brief §3: no scroll reveals and no hover transforms. The hero is the one
   orchestrated moment; everything else transitions colour only, at 150ms. */
.section{
  position:relative;z-index:2;
  padding:clamp(4.5rem,9vw,8rem) 0;
}
.sec-grid{
  display:grid;
  grid-template-columns:1fr 1.6fr;
  gap:clamp(2rem,5vw,5rem);
  align-items:start;
}
/* Sticky only when the heading is alone in its column. On the contact section
   the column also holds the intro and the email line, and a sticky heading slid
   down over its own siblings as the section scrolled. */
.sec-col-head > h2:only-child{position:sticky;top:calc(62px + 2rem)}
.sec-col-head .lede{margin-top:1.1rem}
.prose{max-width:34em}
.prose p + p{margin-top:1.1em}
.prose p{color:var(--text-secondary);font-size:1.0625rem;line-height:1.65}

/* the positioning band: one narrow surface strip under the hero, no heading */
.posband{
  position:relative;z-index:2;
  background:var(--canvas-2);
  border-top:1px solid var(--line);border-bottom:1px solid var(--line);
  padding:clamp(1.6rem,3.5vw,2.6rem) 0;
}
.posband p{
  max-width:44em;color:var(--text-secondary);
  font-size:1.0625rem;line-height:1.65;
}

/* the one kept statement, given a section of its own */
.statement-big{
  font-family:var(--display);font-weight:600;
  font-size:clamp(1.75rem,4vw,3rem);line-height:1.12;letter-spacing:-.018em;
  color:var(--text-bright,var(--text-primary));
  max-width:19ch;padding-bottom:.08em;margin-bottom:-.08em;
}

/* hairline-separated blocks, deliberately not cards */
.stack > article{
  padding:clamp(1.5rem,2.6vw,2.1rem) 0;
  border-top:1px solid var(--line);
  max-width:34em;
}
.stack > article:last-child{border-bottom:1px solid var(--line)}
.stack h3{margin-bottom:.5rem}
.stack p{color:var(--text-secondary);font-size:1.0625rem;line-height:1.65}

/* the Nightjar panel */
.panel{
  background:var(--canvas-2);border:1px solid var(--line);
  padding:clamp(1.8rem,4vw,3.4rem);
}
.product-name{
  font-size:clamp(2.2rem,5vw,3.6rem);letter-spacing:.02em;line-height:1;
  margin-bottom:.5rem;
}
.product-desc{
  color:var(--accent);font-size:1.0625rem;line-height:1.5;
  margin-bottom:clamp(1.8rem,3.5vw,2.8rem);
}
/* build brief wireframe: product image left, body copy over the attribute
   table on the right */
.product-grid{
  display:grid;grid-template-columns:minmax(0,.72fr) minmax(0,1fr);
  gap:clamp(1.8rem,4vw,3.4rem);align-items:start;
}
/* the render is a true cutout, so it sits straight on the panel with no plate
   edge, which is what the build brief asked for. No border, no frame. */
.product-shot{margin:0}
.product-shot img{width:100%;height:auto;display:block}
/* the table spans the panel: the two-column top row is image and body copy only,
   which keeps their heights close and stops a tall table stranding the shot */
.attrs{margin:clamp(1.8rem,4vw,3.2rem) 0 0}
.attrs > div{
  display:grid;grid-template-columns:minmax(9rem,14%) 1fr;
  gap:.4rem 2rem;
  padding:.9rem 0;border-top:1px solid var(--line);
}
.attrs > div:last-child{border-bottom:1px solid var(--line)}
.attrs dt{
  color:var(--text-tertiary);font-size:.8125rem;font-weight:500;
  letter-spacing:.01em;line-height:1.4;padding-top:.15rem;
}
.attrs dd{margin:0;color:var(--text-secondary);font-size:.96rem;line-height:1.55}
.product-close{
  margin-top:clamp(1.8rem,3.5vw,2.8rem);
  padding-top:clamp(1.4rem,2.6vw,2rem);border-top:1px solid var(--line);
  color:var(--text-secondary);max-width:52em;font-size:1.0625rem;line-height:1.65;
}

/* applications: 2x2, hairline-bordered, 2px radius and nothing else */
.cardgrid{display:grid;grid-template-columns:1fr 1fr;gap:1px;background:var(--line);border:1px solid var(--line)}
.card{
  background:var(--canvas);padding:clamp(1.5rem,2.8vw,2.2rem);
  transition:background .15s linear;
}
.card:hover{background:var(--canvas-2)}
.card h3{margin-bottom:.55rem}
.card p{color:var(--text-secondary);font-size:.98rem;line-height:1.6}
.sub-h{
  margin-top:clamp(2.2rem,4vw,3.2rem);margin-bottom:1rem;
  font-size:.8125rem;font-weight:500;letter-spacing:.01em;
  color:var(--text-tertiary);font-family:var(--body);
}

/* --------------------------------------------------------------- form */
.field{display:grid;gap:7px;margin-bottom:16px}
/* sentence case, per the build brief's rule against all-caps labels */
.field label{font-size:.8125rem;letter-spacing:.01em;color:var(--text-tertiary);font-weight:500;line-height:1.4}
.field input,.field textarea,.field select{
  width:100%;background:var(--canvas);color:var(--text-primary);
  border:1px solid var(--line-input);border-radius:2px;
  padding:13px 14px;min-height:46px;
  font-family:var(--body);font-size:.96rem;
  transition:border-color .15s linear;
}
.field select{
  appearance:none;-webkit-appearance:none;cursor:pointer;
  background-image:linear-gradient(45deg,transparent 50%,var(--text-tertiary) 50%),
                   linear-gradient(135deg,var(--text-tertiary) 50%,transparent 50%);
  background-position:calc(100% - 20px) 20px,calc(100% - 14px) 20px;
  background-size:6px 6px,6px 6px;background-repeat:no-repeat;
  padding-right:42px;
}
.field select option{background:var(--canvas-2);color:var(--text-primary)}
.field textarea{min-height:110px;resize:vertical;line-height:1.55}
.field input:focus,.field textarea:focus,.field select:focus{outline:none;border-color:var(--accent)}
.field input:focus-visible,.field textarea:focus-visible,.field select:focus-visible{
  outline:2px solid var(--accent);outline-offset:2px;
}
.field input::placeholder,.field textarea::placeholder{color:var(--text-tertiary);opacity:.75}
.field.invalid input,.field.invalid textarea,.field.invalid select{border-color:var(--hud-lock)}

/* consent, and the honeypot that never reaches a person or a screen reader */
.field.check{
  display:grid;grid-template-columns:auto 1fr;gap:12px;align-items:start;
  margin-top:6px;margin-bottom:22px;
}
.field.check input{
  width:20px;height:20px;min-height:0;margin:2px 0 0;flex:none;
  accent-color:var(--accent);cursor:pointer;padding:0;
}
.field.check label{
  font-size:.9rem;letter-spacing:0;color:var(--text-secondary);
  line-height:1.55;cursor:pointer;
}
.field.check label a{color:var(--accent);text-underline-offset:3px}
.hp{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}
.form-row{display:grid;grid-template-columns:1fr 1fr;gap:16px}
.form-note{font-size:.87rem;color:var(--text-tertiary);margin-top:14px;max-width:52ch}
.success,.formerror{
  margin-top:20px;padding:18px 20px;border-radius:2px;color:var(--text-primary);
}
.success{border:1px solid var(--accent);background:var(--accent-muted)}
.formerror{border:1px solid var(--hud-lock);background:rgba(255,92,74,.12)}
.formerror a{color:var(--accent);text-underline-offset:3px}
form.sent .form-body{display:none}
.direct-alt{margin-top:18px;color:var(--text-tertiary);font-size:.92rem}
.direct-alt a{color:var(--accent);text-underline-offset:3px}

/* the owner-fact marker: visible on purpose, so nothing invented ships silently */
.tbc{
  display:inline-block;padding:1px 7px;border:1px dashed var(--line-strong);
  border-radius:3px;font-size:11px;letter-spacing:.1em;text-transform:uppercase;
  color:var(--text-tertiary);font-weight:500;white-space:nowrap;
}

/* ------------------------------------------------------------- footer */
.foot{position:relative;z-index:2;border-top:1px solid var(--line);padding:clamp(40px,6vh,64px) 0 clamp(28px,4vh,40px)}
.foot-top{
  display:flex;align-items:center;justify-content:space-between;gap:30px;flex-wrap:wrap;
  padding-bottom:clamp(26px,4vh,38px);
}
.foot-brand{flex:none}
.foot-export{
  color:var(--text-tertiary);font-size:.82rem;line-height:1.6;
  max-width:44ch;text-align:right;
}
.foot-base{
  display:flex;justify-content:space-between;gap:20px 40px;flex-wrap:wrap;
  padding-top:clamp(20px,3vh,28px);border-top:1px solid var(--line);
}
.foot-legal{color:var(--text-tertiary);font-size:.82rem;max-width:62ch;line-height:1.7}
@media (max-width:680px){
  .foot-export{text-align:left}
}

/* ======================================= SUBPAGES: careers and privacy
   No hero here, so the nav carries its own ground from the first pixel and the
   page head clears the fixed bar. Same section shell as the home page. */
body.subpage .nav{
  background:rgba(13,20,23,.9);
  backdrop-filter:blur(14px);
  border-bottom-color:var(--line);
}
.page-head{
  position:relative;z-index:2;
  padding:calc(62px + clamp(3rem,7vw,5.5rem)) 0 clamp(1.5rem,4vw,3rem);
}
.page-head h1{
  font-size:clamp(2.4rem,6vw,4.2rem);
  margin-bottom:clamp(1.1rem,2.4vw,1.8rem);
}
.menu a[aria-current="page"]{color:var(--text-primary)}
.menu a[aria-current="page"]::after{right:0}

.role-line{
  font-family:var(--display);font-weight:500;
  font-size:clamp(1.1rem,1.8vw,1.35rem);letter-spacing:-.01em;
  color:var(--accent);margin-top:.35rem;
}
.meta-line{
  margin-top:.8rem;
  font-size:.8125rem;letter-spacing:.01em;color:var(--text-tertiary);
}
.apply-head{margin-top:1.4rem}

.ticks{
  list-style:none;margin:0 0 clamp(1.6rem,3vw,2.4rem);padding:0;
  max-width:34em;
}
.ticks li{
  position:relative;padding:.55rem 0 .55rem 1.6rem;
  color:var(--text-secondary);font-size:1.0625rem;line-height:1.6;
}
.ticks li::before{
  content:"";position:absolute;left:0;top:1.05em;
  width:8px;height:1px;background:var(--accent);
}
.sec-col-body > .sub-h:first-child{margin-top:0}

.closing-note{padding-top:0}
.closing-note .prose{max-width:44em;color:var(--text-secondary)}
.closing-note a,.legal a{color:var(--accent);text-underline-offset:3px}

.legal .sub-h{
  font-family:var(--display);font-weight:600;
  font-size:clamp(1.05rem,1.6vw,1.25rem);letter-spacing:-.008em;
  color:var(--text-primary);text-transform:none;
  margin-top:clamp(2rem,3.6vw,2.8rem);margin-bottom:.7rem;
}
.legal .sub-h:first-child{margin-top:0}
.legal p{margin-bottom:.9em}

/* ============================================================ responsive */
@media (max-width:1040px){
  /* the two-column section shell collapses to stacked, per the build brief */
  .sec-grid{grid-template-columns:1fr;gap:clamp(1.2rem,3vw,2rem)}
  .sec-col-head h2{position:static}
  .product-grid{grid-template-columns:1fr}
}
@media (max-width:760px){
  .cardgrid{grid-template-columns:1fr}
  .form-row{grid-template-columns:1fr}
  .attrs > div{grid-template-columns:1fr;gap:.25rem}
}
@media (max-width:820px){
  .menu,.nav .btn{display:none}
  .burger{display:inline-flex}
  body.menu-open .menu{
    display:flex;position:fixed;inset:62px 0 auto;flex-direction:column;gap:0;
    background:var(--canvas);border-bottom:1px solid var(--line);
    padding:8px var(--gut) 26px;z-index:59;
  }
  body.menu-open .menu a{padding:15px 0;border-bottom:1px solid var(--line);font-size:1.04rem;min-height:44px;display:flex;align-items:center}
  body.menu-open .menu a::after{display:none}
}
@media (pointer:coarse){
  .btn{min-height:44px;display:inline-flex;align-items:center;justify-content:center}
}

/* ======================================================= reduced motion */
@media (prefers-reduced-motion:reduce){
  html{scroll-behavior:auto}
  *,*::before,*::after{
    animation-duration:.001ms !important;
    animation-iteration-count:1 !important;
    transition-duration:.001ms !important;
    transition-delay:0s !important;
    animation-delay:-1ms !important;
  }
  body{opacity:1}
  .survey{animation:none}
  /* re-apply positional transforms a blanket reset would have wiped */
  .track-box .tick.t,.track-box .tick.b{transform:translateX(-50%)}
  .track-box .tick.l,.track-box .tick.r{transform:translateY(-50%)}
  .band{transform:translateY(-50%)}
  .ring{transform:rotate(-90deg)}
  .cap::after{transform:scaleY(0)}
}

body.paused *,body.paused *::before,body.paused *::after{animation-play-state:paused !important}

/* ==========================================================================
   SPLIT PAGES
   Restructure §6. The site went from one long page to seven, and these are the
   components that only exist because of the split: the back link on a role page,
   the job list that replaced two full adverts on careers, the CTA band that
   closes product and company, and the figure caption under a diagram.
   ========================================================================== */

/* --------------------------------------------------- back to the job list */
.backlink{
  margin-bottom:clamp(1rem,2.2vw,1.6rem);
  font-size:.8125rem;letter-spacing:.01em;
}
.backlink a{
  color:var(--text-tertiary);text-decoration:none;
  display:inline-flex;align-items:center;gap:.45rem;
}
.backlink a:hover{color:var(--text-primary)}
.backlink a:hover .back{transform:translateX(-3px)}
.back{display:inline-block;transition:transform .18s var(--ease)}

.role-head h1{margin-bottom:0}

/* ------------------------------------------------------------- job list
   §6.5: title and meta left, button right, hairline between rows, the whole
   row is the hit area. One <a> wrapping everything rather than a nested
   button, so there is one tab stop per role and no nested-interactive. */
.joblist{
  list-style:none;margin:clamp(1.5rem,3vw,2.5rem) 0 0;padding:0;
  border-top:1px solid var(--line);
}
.job{border-bottom:1px solid var(--line)}
.job-hit{
  display:grid;
  grid-template-columns:1fr auto;
  grid-template-areas:"title go" "meta go";
  align-items:center;
  column-gap:clamp(1.5rem,4vw,3rem);
  padding:clamp(1.5rem,3.2vw,2.4rem) 0;
  text-decoration:none;
}
.job-title{
  grid-area:title;
  font-family:var(--display);font-weight:600;
  font-size:clamp(1.25rem,2.4vw,1.7rem);letter-spacing:-.01em;
  color:var(--text-primary);
  transition:color .15s linear;
}
.job-sub{color:var(--text-tertiary);font-weight:500}
.job-hit .meta-line{grid-area:meta;margin-top:.4rem}
.job-go{
  grid-area:go;
  display:inline-flex;align-items:center;gap:.4rem;
  font-size:.9375rem;color:var(--accent);white-space:nowrap;
}
.job-go .arw{display:inline-block;transition:transform .18s var(--ease)}
.job-hit:hover .job-title{color:var(--accent)}
.job-hit:hover .job-go .arw{transform:translateX(4px)}
.job-hit:focus-visible{
  outline:2px solid var(--accent);outline-offset:4px;
}

/* ------------------------------------------------------------- CTA band
   Closes product, company and the role pages. One line and one button. */
.cta-band{border-top:1px solid var(--line)}
.cta-line{
  font-size:clamp(1.5rem,3.4vw,2.4rem);
  line-height:1.15;letter-spacing:-.02em;
  max-width:18em;
  margin-bottom:clamp(1.4rem,3vw,2rem);
}

/* --------------------------------------------------------- figure caption */
.fig-caption{
  margin-top:clamp(1rem,2.2vw,1.5rem);
  max-width:38em;
  color:var(--text-tertiary);font-size:.9375rem;line-height:1.6;
}

/* ---------------------------------------------------- product page layout
   On the home page the product sat in a panel with the shot beside the prose.
   On its own page the prose leads and the shot follows it in the body column. */
.page-head .product-name{margin-bottom:.2rem}
.page-head .product-desc{
  color:var(--text-secondary);
  font-size:clamp(1rem,1.5vw,1.15rem);
}
#overview .product-shot{
  margin:clamp(1.8rem,4vw,2.8rem) 0 0;
  border:1px solid var(--line);background:var(--canvas-2);
}
#overview .product-shot img{display:block;width:100%;height:auto}

@media (max-width:720px){
  .job-hit{
    grid-template-columns:1fr;
    grid-template-areas:"title" "meta" "go";
    row-gap:.5rem;
  }
  .job-go{margin-top:.4rem}
}

/* ------------------------------------------------- the denied-nav section
   §6.1 item 5: the headline, two bullets, and the animation. The two bullets
   are the GNSS and communications rows lifted verbatim from the attribute
   table on the product page, so nothing new is being claimed here. */
.statement-points{
  margin-top:clamp(1.4rem,3vw,2rem);
  max-width:34em;
}
.statement-points li{font-size:1.0625rem}

/* ==========================================================================
   THE TERMINAL EFFECT — restructure brief §4
   Fires once per page load at the end of the sequence, and on demand from the
   Replay control. A flash, a shake and a bloom. No debris, no fire, no sound.

   Everything here is declarative: four keyframe timelines started by two
   classes. Nothing runs per frame, because the hero's drive loop deliberately
   rests the moment the scroll converges, which is exactly when this fires.
   ========================================================================== */
/* --shx/--shy have to be registered, or the keyframe below is ignored: an
   unregistered custom property is not animatable, so the shake silently does
   nothing. Registering them as lengths makes them animatable; steps(1,end) on
   the animation then holds each keyframe rather than interpolating, which is
   what makes it read as a shake and not a slide. Where @property is not
   supported the properties stay at 0px and the frame simply does not shake —
   the flash, the bloom and the label are unaffected. */
@property --shx{syntax:'<length>';inherits:true;initial-value:0px}
@property --shy{syntax:'<length>';inherits:true;initial-value:0px}

.terminal{
  position:absolute;inset:0;pointer-events:none;
  --tx:65.5%;--ty:51.9%;
  z-index:3;
}
.terminal > span{
  position:absolute;left:var(--tx);top:var(--ty);
  transform:translate(-50%,-50%);
  opacity:0;
}

/* 2. the flash: radial, white, peaking at ~120ms and decaying over ~400ms */
.t-flash{
  width:44vmin;height:44vmin;border-radius:50%;
  background:radial-gradient(circle closest-side,
    rgba(255,255,255,.96) 0%, rgba(255,242,225,.62) 34%, rgba(255,214,170,0) 72%);
  mix-blend-mode:screen;
}
.stage.t-shake .t-flash,
.stage.t-flashonly .t-flash{animation:tFlash .52s linear 1 both}
@keyframes tFlash{
  0%{opacity:0;transform:translate(-50%,-50%) scale(.35)}
  23%{opacity:1;transform:translate(-50%,-50%) scale(1)}
  100%{opacity:0;transform:translate(-50%,-50%) scale(1.28)}
}

/* 4. the thermal bloom: slower, warmer, expands out of the flash and fades */
.t-bloom{
  width:30vmin;height:30vmin;border-radius:50%;
  background:radial-gradient(circle closest-side,
    rgba(255,180,120,.55) 0%, rgba(224,138,52,.28) 46%, rgba(224,138,52,0) 78%);
  mix-blend-mode:screen;
}
.stage.t-shake .t-bloom,
.stage.t-flashonly .t-bloom{animation:tBloom .95s var(--ease-out) 1 both}
@keyframes tBloom{
  0%{opacity:0;transform:translate(-50%,-50%) scale(.3)}
  18%{opacity:1}
  100%{opacity:0;transform:translate(-50%,-50%) scale(2.5)}
}

/* 3. the frame shake. --shx/--shy are read by the video and poster transforms,
      which site.js composes as translate(var(--shx),var(--shy)) scale(...).
      Custom properties do not interpolate without @property, and that is the
      point here: each keyframe is a discrete jump, which is what a shake is.
      Amplitude damps from 4px to zero across 500ms. */
.stage{--shx:0px;--shy:0px}
.stage.t-shake{animation:tShake .5s steps(1,end) 1 both}
@keyframes tShake{
  0%  {--shx:0px;--shy:0px}
  8%  {--shx:-4px;--shy:3px}
  18% {--shx:4px;--shy:-3px}
  28% {--shx:-3px;--shy:-3px}
  38% {--shx:3px;--shy:2px}
  50% {--shx:-2px;--shy:2px}
  62% {--shx:2px;--shy:-2px}
  74% {--shx:-1px;--shy:-1px}
  86% {--shx:1px;--shy:1px}
  100%{--shx:0px;--shy:0px}
}

/* 5. the label, centred beneath the impact point, 200ms fade and nothing else.
      Contrast: #D9484A has relative luminance 0.1987; the plate below it sits at
      0.0065, giving 4.40:1. That is under the 4.5:1 AA floor for normal text, so
      the label is set at 19px/600, which clears the 18.66px bold threshold for
      large text where the floor is 3:1. The plate is near-opaque so the ratio is
      the real one and not at the mercy of whatever the video is showing. */
.terminal .t-label{
  /* beneath the impact point, not over it. .terminal > span carries top:var(--ty)
     at 0,1,1, so this needs the same weight or the offset is silently discarded. */
  top:calc(var(--ty) + var(--tly, 13vmin));
  transform:translate(-50%,0);
  white-space:nowrap;
  font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
  font-size:1.1875rem;font-weight:600;letter-spacing:.22em;
  color:#D9484A;
  background:rgba(8,12,15,.88);
  border:1px solid rgba(217,72,74,.38);
  border-radius:2px;
  padding:7px 14px 7px 18px;
}
/* The 200ms fade is the one-time flourish, driven by .t-done.
   Visibility is a different thing and outlives it: once the effect has played
   (.t-seen) the label belongs to the resolved end state, so it is shown
   whenever the sequence is at its end (.settled). Without this split, a reader
   who scrolled quickly past the effect and back never saw the label again, and
   one who flicked through at speed never saw it at all. */
.stage.t-done .t-label{animation:tLabel .2s linear 1 both}
.stage.t-seen.settled .t-label{opacity:1}
@keyframes tLabel{from{opacity:0}to{opacity:1}}

/* Under reduced motion the whole thing collapses to its resting state: §4 asks
   for a cross-fade to the final frame and the label faded in, with no shake and
   no flash. runTerminal() adds both classes at once and skips the timeline. */
@media (prefers-reduced-motion:reduce){
  .stage.t-shake{animation:none}
  .t-flash,.stage.t-shake .t-flash,.stage.t-flashonly .t-flash{animation:none;opacity:0}
  .t-bloom,.stage.t-shake .t-bloom,.stage.t-flashonly .t-bloom{animation:none;opacity:0}
  .stage.t-done .t-label,
  .stage.t-seen.settled .t-label{animation:none;opacity:1;transition:opacity .3s linear}
}

/* the phone gets the label only: the loop repeats, and a flash on every pass
   would be noise rather than a moment */
.mh-label{
  display:none;
  margin-top:clamp(14px,3vh,20px);
  font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
  font-size:.9375rem;font-weight:600;letter-spacing:.2em;
  color:#D9484A;opacity:0;
}
body.static-hero .mh-label{display:block}
body.mh-terminal .mh-label{opacity:1;transition:opacity .25s linear}
@media (prefers-reduced-motion:reduce){
  body.mh-terminal .mh-label{transition:none}
}

/* §3: "After the terminal effect resolves, show a single Talk to us button and
   the scroll cue." The inherited --kb ramp had the button fully visible at about
   80% of the hero — before the effect had fired, let alone resolved. Four classes
   here, so this outranks both .band .cta-row and the .band.on pointer rules. */
.band.settle .cta-row{
  opacity:0;pointer-events:none;
  transition:opacity .3s var(--ease);
}
.stage.t-seen .band.settle .cta-row{opacity:1;pointer-events:auto}
@media (prefers-reduced-motion:reduce){
  .band.settle .cta-row{transition:none}
}


/* ==========================================================================
   SECTION GRAPHICS — restructure brief §5
   Every graphic on this site is drawn in inline SVG or CSS. There are no
   photographs beyond the owner's own hardware shot, and nothing is fetched
   from a third party.
   ========================================================================== */

/* ================================================= 5.1 cost-exchange graphic
   Forty dim drone glyphs against one amber interceptor. Amber is spent on the
   interceptor alone: the swarm, the divider and both labels stay dim, so the
   asymmetry reads before either label does.

   TRIGGER. This page has no entrance observer to hook. The one that used to
   run it was removed on purpose, not disabled (see site.js:602-605, and the
   §SECTIONS note at styles.css:476). So the fill-in is gated on one class,
   .cx-in, on the <svg> root, and the RESTING STATE IS THE FINISHED STATE:
   with no class and no script the graphic is complete and correct. That is the
   same contract the page-load fade already uses at styles.css - if it never
   runs, the graphic is simply there.

   COST. Opacity only, declarative keyframes, no rAF, no stroke-dashoffset,
   nothing that touches layout. Per-frame work is zero. */

.cx-root{
  display:block;
  width:100%;
  height:auto;
  margin-inline:auto;
  overflow:visible;              /* label descenders sit below the viewBox floor */
}

/* the swarm strokes currentColor, so one property dims all forty glyphs and the
   colour reaches into every <use> shadow tree, which stroke alone would not */
.cx-swarm{color:var(--text-tertiary)}

.cx-missile{
  fill:none;
  stroke:var(--accent);
  stroke-width:1.5;
  stroke-linecap:round;
  stroke-linejoin:round;
}

.cx-divide{
  fill:none;
  stroke:var(--line-strong);
  stroke-width:1.5;
  stroke-linecap:round;
}

/* font-size is in user units, so it tracks the viewBox rather than the page.
   The steps hold the rendered label near 14-17px from a full-width desktop
   placement down to a narrow phone, where the SVG is about a third of its
   viewBox and an unstepped 20 would render at roughly 7px. */
.cx-label{
  fill:var(--text-tertiary);
  font-family:var(--body);
  font-size:20px;
  font-weight:400;
  letter-spacing:.005em;
}
@media (max-width:900px){.cx-label{font-size:27px}}
@media (max-width:640px){.cx-label{font-size:38px}}

/* ---------------------------------------------------------- the fill-in */
/* Resting state first. Everything below only takes it away and puts it back,
   so a missing .cx-in leaves the finished graphic rather than an empty one. */
.cx-drone{opacity:1}

.cx-root.cx-in .cx-drone{
  animation:cx-drone-in 220ms cubic-bezier(.22,.61,.36,1) backwards;
  /* 0 to 390ms of stagger, plus the 220ms glyph, is ~610ms end to end */
  animation-delay:calc(var(--i,0) * 10ms);
}

@keyframes cx-drone-in{
  from{opacity:0}
  to{opacity:1}
}

/* ------------------------------------------------------- reduced motion */
/* The final state, rendered flat: all forty glyphs present at full opacity, no
   stagger, no fade, nothing to sit through. The global block in styles.css
   already neutralises animation under this query, but it does so by collapsing
   duration and forcing a negative delay with !important, which only lands
   correctly because this keyframe's end state happens to be right. This states
   the resting state outright instead of relying on that, and carries the
   !important it would otherwise lose to. */
@media (prefers-reduced-motion:reduce){
  .cx-drone,
  .cx-root.cx-in .cx-drone{
    animation:none !important;
    opacity:1 !important;
  }
}

/* ------------------------------------------------- §5.2 capability tiles
   Four links to product.html, in the same hairline idiom as .cardgrid: the
   1px grid gap over a --line background IS the divider, so there are no
   per-tile borders to double up. Each tile is a single <a>, so the whole
   surface is the hit area and there is one tab stop per capability.

   Colour discipline: .ct-ico carries `color`, the icon's dim strokes take
   currentColor from it, and .ct-hot overrides stroke with --accent. That
   means the hover state shifts every dim stroke with one property and never
   touches the amber element, which stays the one thing that matters. */

.ct-head{margin-bottom:clamp(1.6rem,3.4vw,2.6rem)}

.ct-grid{
  display:grid;
  grid-template-columns:repeat(4,1fr);
  gap:1px;
  background:var(--line);
  border:1px solid var(--line);
}

.ct-tile{
  position:relative;
  display:flex;flex-direction:column;align-items:flex-start;
  background:var(--canvas);
  padding:clamp(1.5rem,2.4vw,2rem);
  text-decoration:none;color:inherit;
  transition:background .15s linear;
}
.ct-tile:hover{background:var(--canvas-2)}

/* Overrides the global :focus-visible, which uses outline-offset:3px. A
   positive offset here would sit on the 1px grid gap and be half-eaten by
   the neighbouring tile, so the ring is drawn inside the tile instead and
   the tile is raised so the ring is never overpainted. Background and
   heading colour change too, so focus does not rely on the ring alone. */
.ct-tile:focus-visible{
  outline:2px solid var(--accent);
  outline-offset:-3px;
  border-radius:0;
  background:var(--canvas-2);
  z-index:1;
}

.ct-ico{
  width:64px;height:64px;flex:none;display:block;
  color:var(--text-tertiary);
  margin-bottom:clamp(1.1rem,2vw,1.5rem);
  transition:color .15s linear;
}
.ct-tile:hover .ct-ico,
.ct-tile:focus-visible .ct-ico{color:var(--text-secondary)}

/* the one amber element in each icon */
.ct-hot{stroke:var(--accent)}

.ct-title{
  font-family:var(--display);font-weight:600;
  font-size:clamp(1.06rem,1.45vw,1.24rem);
  letter-spacing:-.008em;line-height:1.22;
  color:var(--text-primary);
  margin:0 0 .55rem;
  transition:color .15s linear;
}
.ct-tile:hover .ct-title,
.ct-tile:focus-visible .ct-title{color:var(--accent)}

.ct-copy{
  color:var(--text-secondary);
  font-size:.98rem;line-height:1.6;
  margin:0;
}

/* margin-top:auto pins the affordance to the bottom of every tile, so the
   four align on a common baseline whatever the copy wraps to. Dim at rest
   rather than amber, because four amber chevrons next to four amber icon
   elements would spend the accent twice in the same row. */
.ct-go{
  margin-top:auto;
  padding-top:clamp(1.1rem,2vw,1.5rem);
  display:inline-flex;align-items:center;gap:.4rem;
  font-size:.875rem;letter-spacing:.01em;
  color:var(--text-tertiary);
  transition:color .15s linear;
}
.ct-tile:hover .ct-go,
.ct-tile:focus-visible .ct-go{color:var(--accent)}
.ct-arw{display:inline-block;transition:transform .18s var(--ease)}
.ct-tile:hover .ct-arw,
.ct-tile:focus-visible .ct-arw{transform:translateX(4px)}

@media (max-width:1040px){.ct-grid{grid-template-columns:1fr 1fr}}
@media (max-width:600px){.ct-grid{grid-template-columns:1fr}}

/* Reduced motion. There is no keyframe animation here, so the resting state
   and the final state are the same thing; what this block removes is the
   150ms colour tweening and the one transform in the component, the 4px
   arrow nudge. Hover and focus still change colour, they just arrive
   instantly instead of easing, and the arrow never moves. */
@media (prefers-reduced-motion:reduce){
  .ct-tile,.ct-ico,.ct-title,.ct-go,.ct-arw{transition:none}
  .ct-tile:hover .ct-arw,
  .ct-tile:focus-visible .ct-arw{transform:none}
}

/* =========================================================== 5.3 satellite
   Two periods run concurrently and deliberately do not resolve against each
   other: the satellite turns on a 12s axis while the narrative runs on an 8s
   loop. They sit on nested groups so neither has to know about the other --
   .sat-spin owns transform, its two children own opacity, and CSS animates
   those independently. Over 24s the pair beat against each other, which is
   what stops the loop looking like a 3-frame GIF.

   Beat map, 8s: 0-3s (0-37.5%) intact  |  3-4s (37.5-50%) failure
                 4-8s (50-100%) handover to the airframe.
   Nothing animates but transform and opacity, and nothing runs per frame:
   the whole thing is declarative keyframes, no rAF, no SMIL. */

.sat-fig{
  display:block;width:100%;height:auto;
  fill:none;stroke:currentColor;stroke-width:1.5;
  stroke-linecap:round;stroke-linejoin:round;
  color:var(--text-tertiary);
}

.sat-ground{color:var(--text-tertiary);stroke:currentColor;stroke-opacity:.7}

/* the cone exists twice: intact, and pre-broken into real segments. Breaking it
   is a cross-fade between the two, not a stroke-dasharray animation. */
.sat-cone{stroke:currentColor}
.sat-cone--solid{color:var(--text-secondary);stroke-opacity:.78;animation:sat-cone-solid 8s linear infinite}
.sat-cone--frag{color:var(--text-tertiary);opacity:0;animation:sat-cone-frag 8s linear infinite}

/* PERIOD ONE, 12s. Only this element rotates. transform-origin:0 0 resolves to
   the local origin set by the parent's transform="translate(300 100)", so the
   satellite spins about its own centre without either animation naming a shared
   coordinate. */
.sat-spin{transform-box:view-box;transform-origin:0 0;animation:sat-turn 12s linear infinite}

/* PERIOD TWO, 8s, on the children of the spinning group. Identical geometry in
   two colours, cross-faded -- so "desaturates to --text-tertiary" is an opacity
   change, never an animated paint property. */
.sat-body{stroke:currentColor}
.sat-body--live{color:var(--text-secondary);animation:sat-alive 8s linear infinite}
.sat-body--dead{color:var(--text-tertiary);opacity:0;animation:sat-dead 8s linear infinite}

/* same trick for the route and the airframe: one geometry, two treatments. */
.sat-route{stroke:currentColor}
.sat-route--dash{color:var(--text-tertiary);stroke-dasharray:5 7;stroke-opacity:.9;animation:sat-hand-out 8s linear infinite}
.sat-route--live{color:var(--accent);opacity:0;animation:sat-hand-in 8s linear infinite}

.sat-carry{animation:sat-carry 8s linear infinite}
.sat-air{stroke:currentColor}
.sat-air--dim{color:var(--text-secondary);animation:sat-hand-out 8s linear infinite}
.sat-air--lit{color:var(--accent);animation:sat-hand-in 8s linear infinite;opacity:0}
.sat-trackbox{
  color:var(--accent);stroke:currentColor;
  transform-box:view-box;transform-origin:0 0;
  opacity:0;animation:sat-lock 8s linear infinite;
}

.sat-say{opacity:0;animation:sat-say 8s linear infinite}
.sat-say .sat-tick{stroke:var(--accent)}
.sat-label{
  fill:var(--accent);stroke:none;
  font-family:var(--body);font-size:15px;font-weight:500;letter-spacing:.05em;
}
/* SVG text scales with the figure. 15 user units renders at ~15.8px when the
   figure is ~758px wide, which is a half-width column at --maxw. If the section
   renders the figure narrower than about 520px, raise this to match. */
@media (max-width:420px){.sat-label{font-size:24px}}

@keyframes sat-turn{from{transform:rotate(0deg)}to{transform:rotate(360deg)}}

/* 3s of 8s is 37.5%, 4s is 50%. The blink just before failure and the stutter
   after it use per-keyframe steps(1,end) so they cut rather than dissolve. */
@keyframes sat-cone-solid{
  0%,33.5%{opacity:1;animation-timing-function:steps(1,end)}
  34.4%{opacity:.35;animation-timing-function:steps(1,end)}
  35.2%{opacity:1;animation-timing-function:steps(1,end)}
  36.4%{opacity:.5;animation-timing-function:steps(1,end)}
  37.3%{opacity:1;animation-timing-function:steps(1,end)}
  37.5%{opacity:0;animation-timing-function:linear}
  98%{opacity:0;animation-timing-function:steps(1,end)}
  100%{opacity:1}
}
@keyframes sat-cone-frag{
  0%,37.4%{opacity:0;transform:none;animation-timing-function:steps(1,end)}
  37.5%{opacity:1;transform:translate(1.5px,-1px);animation-timing-function:steps(1,end)}
  38.8%{opacity:.18;transform:translate(-1.5px,1px);animation-timing-function:steps(1,end)}
  40%{opacity:.95;transform:translate(2px,.5px);animation-timing-function:steps(1,end)}
  41.2%{opacity:.12;transform:translate(-1px,-1.5px);animation-timing-function:steps(1,end)}
  42.5%{opacity:.8;transform:translate(1px,1.5px);animation-timing-function:steps(1,end)}
  43.7%{opacity:.1;transform:translate(-2px,0);animation-timing-function:steps(1,end)}
  45%{opacity:.6;transform:translate(.5px,-.5px);animation-timing-function:steps(1,end)}
  46.2%{opacity:.14;transform:none;animation-timing-function:linear}
  50%,98%{opacity:.7;transform:none;animation-timing-function:steps(1,end)}
  100%{opacity:0}
}
/* alive and dead are complements, so the satellite flickers between two colours
   on its way from one to the other rather than simply dimming. */
@keyframes sat-alive{
  0%,33.5%{opacity:1;animation-timing-function:steps(1,end)}
  34.4%{opacity:.55;animation-timing-function:steps(1,end)}
  35.2%{opacity:1;animation-timing-function:steps(1,end)}
  37.5%{opacity:.35;animation-timing-function:steps(1,end)}
  38.8%{opacity:.9;animation-timing-function:steps(1,end)}
  41.2%{opacity:.25;animation-timing-function:steps(1,end)}
  43.7%{opacity:.5;animation-timing-function:linear}
  47%,98%{opacity:0;animation-timing-function:steps(1,end)}
  100%{opacity:1}
}
@keyframes sat-dead{
  0%,33.5%{opacity:0;animation-timing-function:steps(1,end)}
  34.4%{opacity:.45;animation-timing-function:steps(1,end)}
  35.2%{opacity:0;animation-timing-function:steps(1,end)}
  37.5%{opacity:.65;animation-timing-function:steps(1,end)}
  38.8%{opacity:.1;animation-timing-function:steps(1,end)}
  41.2%{opacity:.75;animation-timing-function:steps(1,end)}
  43.7%{opacity:.5;animation-timing-function:linear}
  47%,98%{opacity:1;animation-timing-function:steps(1,end)}
  100%{opacity:0}
}
/* the handover at 4s: one pair of keyframes drives both the route and the
   airframe, so the amber arrives everywhere on the same frame. */
@keyframes sat-hand-out{0%,50%{opacity:1}60%,98%{opacity:0;animation-timing-function:steps(1,end)}100%{opacity:1}}
@keyframes sat-hand-in{0%,50%{opacity:0}60%,98%{opacity:1;animation-timing-function:steps(1,end)}100%{opacity:0}}

@keyframes sat-carry{from{transform:translate(58px,340px)}to{transform:translate(662px,287px)}}
@keyframes sat-lock{
  0%,50%{opacity:0;transform:scale(1.7);animation-timing-function:cubic-bezier(.2,.85,.3,1)}
  53%,98%{opacity:1;transform:scale(1);animation-timing-function:steps(1,end)}
  100%{opacity:0;transform:scale(1.7)}
}
@keyframes sat-say{
  0%,52%{opacity:0;transform:translateY(8px)}
  62%,96%{opacity:1;transform:translateY(0);animation-timing-function:steps(1,end)}
  100%{opacity:0;transform:translateY(8px)}
}

/* ------------------------------------------------------- reduced motion
   Renders the final state and nothing else: dead satellite, broken cone,
   solid amber route, track box, label. No motion at all.

   This has to beat the site-wide reduce block in styles.css, which sets
   animation-duration/-delay/-iteration-count with !important on the universal
   selector. Those are longhands at specificity (0,0,0); `animation:none` here
   is a shorthand at (0,1,0), so it wins every longhand and the animations are
   removed outright rather than fast-forwarded. That matters: these animations
   use the default fill-mode, so a fast-forward would drop each element back to
   its base style and render the OPENING state, not the closing one. Verified:
   document.getAnimations().length is 0 under this block. */
@media (prefers-reduced-motion:reduce){
  .sat-fig *{animation:none !important}
  .sat-spin{transform:rotate(-24deg) !important}
  .sat-body--live{opacity:0 !important}
  .sat-body--dead{opacity:1 !important}
  .sat-cone--solid{opacity:0 !important}
  .sat-cone--frag{opacity:.7 !important;transform:none !important}
  .sat-route--dash,.sat-air--dim{opacity:0 !important}
  .sat-route--live,.sat-air--lit{opacity:1 !important}
  .sat-carry{transform:translate(571px,295px) !important}
  .sat-trackbox{opacity:1 !important;transform:scale(1) !important}
  .sat-say{opacity:1 !important;transform:none !important}
}

/* ==========================================================================
   5.4b INTEGRATION CHAIN  —  prefix ic-
   Append to assets/styles.css, after the .fig-caption rule in the SPLIT PAGES
   block. .fig-caption on the figcaption is the site's existing class and is
   deliberately reused rather than reinvented.
   ========================================================================== */

.ic-fig{
  margin:clamp(1.8rem,4vw,2.6rem) 0 0;
  max-width:940px;              /* stops the wide chain ballooning in a full-width wrap */
  color:var(--text-tertiary);   /* currentColor is dim by default; amber is opted into */
}

/* the glyph sprite: present in the DOM, never rendered, never announced */
.ic-defs{position:absolute;width:0;height:0;overflow:hidden}

.ic-svg{display:block;width:100%;height:auto}
.ic-wide{display:block}
.ic-stack{display:none;max-width:460px}

/* every stroke in the drawing, one rule */
.ic-block,.ic-conn,.ic-arrow,.ic-bracket,.ic-edge{
  fill:none;
  stroke:currentColor;
  stroke-width:1.5;
  stroke-linecap:round;
  stroke-linejoin:round;
}
/* the software block's top and bottom: held, not housed */
.ic-edge{stroke-dasharray:5 5}

/* the only two colour states in the graphic */
.ic-yours{color:var(--text-tertiary)}   /* 5.34:1 */
.ic-gryphus{color:var(--accent)}        /* 6.95:1 */

/* type. Labels are authored uppercase in the markup rather than transformed,
   because text-transform on SVG text is not dependable. */
.ic-grp{
  font-family:var(--body);font-weight:500;font-size:13px;
  letter-spacing:.2em;fill:currentColor;
}
.ic-name{
  font-family:var(--display);font-weight:600;
  letter-spacing:.05em;fill:currentColor;
}
.ic-sub{
  font-family:var(--body);font-weight:500;
  letter-spacing:.03em;fill:var(--text-tertiary);   /* dim in both groups */
}
.ic-wide .ic-name{font-size:20px}
.ic-wide .ic-sub{font-size:14px}
.ic-stack .ic-name{font-size:21px}
.ic-stack .ic-sub{font-size:14.5px}

/* ---------------------------------------------------------------- stacking
   Base rule: viewport. Guarantees the vertical layout on phones even where
   container queries are unsupported. */
@media (max-width:700px){
  .ic-wide{display:none}
  .ic-stack{display:block}
}
/* Enhancement: container. The product page's .sec-grid is 1fr 1.6fr above
   1040px, so the body column is only ~545px at a 1041px viewport — narrow
   container, wide viewport, which a media query cannot see. */
@supports (container-type:inline-size){
  .ic-fig{container-type:inline-size}
  @container (max-width:560px){
    .ic-wide{display:none}
    .ic-stack{display:block}
  }
}

/* ---------------------------------------------------------- reduced motion
   Nothing in this graphic animates or transitions, so the resting state is
   the drawing itself. This block is a guard: if the figure is ever nested in
   something that animates, it still resolves to that resting state. */
@media (prefers-reduced-motion:reduce){
  .ic-fig,.ic-fig *{animation:none !important;transition:none !important}
}

/* ==========================================================================
   5.7 bench setup — careers.html page intro
   One wide line-art SVG: oscilloscope, board, lens. Texture, not information.
   Decorative (aria-hidden), still (no animation), no numbers anywhere.
   Prefix: bn-
   ========================================================================== */

.bn-bench{
  position:relative;
  display:block;
  width:100%;
  max-width:100%;
  margin:clamp(1.5rem,3.6vw,2.4rem) 0 0;
  /* it must never take a click or a drag away from the intro copy beside it */
  pointer-events:none;
  -webkit-user-select:none;user-select:none;
}
.bn-bench .bn-svg{
  display:block;
  width:100%;          /* no fixed pixel width: the graphic is fluid in its container */
  height:auto;
  max-width:100%;
  overflow:hidden;     /* stated, not assumed: nothing escapes the viewBox */
}

/* 1.5px strokes that stay 1.5px at every rendered size — the same technique the
   hero's .frame-marks already use on this site. vector-effect is not an
   inherited property, so it goes on the shapes and not on the groups. */
.bn-bench .bn-svg path,
.bn-bench .bn-svg rect,
.bn-bench .bn-svg circle,
.bn-bench .bn-svg ellipse,
.bn-bench .bn-svg line{
  vector-effect:non-scaling-stroke;
}

/* Everything that is not the trace.
   opacity on the <g> composites the group once, so overlapping strokes inside
   it do not stack up — which is why .bn-fine is nested inside .bn-dim rather
   than sitting beside it. */
.bn-dim{
  fill:none;
  stroke:var(--text-tertiary);      /* #7C8C93 */
  stroke-width:1.5;
  stroke-linecap:round;
  stroke-linejoin:round;
  opacity:.30;                      /* renders as #2E383C — 1.55:1 on --canvas */
}

/* The single element that matters. */
.bn-trace{
  fill:none;
  stroke:var(--accent);             /* #E08A34 */
  stroke-width:1.5;
  stroke-linecap:round;
  stroke-linejoin:round;
  opacity:.42;                      /* renders as #664623 — 2.18:1 on --canvas */
}

.bn-axis{stroke-dasharray:9 7}

/* Wide: the graphic sits beside the intro, clear to the right of the 34em
   prose column, and behind the type in the stacking order.
   .page-head .wrap is already position:relative in styles.css; restated so this
   block does not silently depend on that staying true.
   Scoped to .page-head so the graphic falls back to the in-flow rule anywhere
   else it is used. */
@media (min-width:1120px){
  .page-head .wrap{position:relative}
  .page-head .bn-bench{
    position:absolute;
    right:var(--gut);
    bottom:0;
    width:min(44%,580px);
    margin:0;
    z-index:0;
  }
  .page-head h1,
  .page-head .prose{position:relative;z-index:1}
}

/* Narrow: drop the finest detail. At 390px the graphic renders 350px wide,
   a 0.36 scale, and a 4-unit feature becomes a pixel and a half. */
@media (max-width:640px){
  .bn-fine{display:none}
}

/* 5.7 is a still. There is no CSS animation, no transition, no SMIL and no
   script anywhere in this graphic, so the resting state IS the default
   rendering. This block is the explicit guarantee the brief asks for: under
   prefers-reduced-motion: reduce the graphic renders byte-identically, and
   nothing added later to .bn-bench can start moving. */
@media (prefers-reduced-motion:reduce){
  .bn-bench,
  .bn-bench *{
    animation:none!important;
    transition:none!important;
  }
}

/* ----------------------------------------------- §5.5 application scenes
   Four small scenes above the four product-page cards. One shared 120x72
   viewBox, one shared stroke weight and a shared horizon at y=58, so they
   read as a set. The svg carries `color` for the dim strokes; .ap-hot
   overrides both stroke and color so the one amber element also picks up
   amber where it is filled rather than stroked. */
.ap-scene{
  display:block;width:100%;max-width:200px;height:auto;
  color:var(--text-tertiary);
  margin-bottom:clamp(.9rem,1.8vw,1.3rem);
}
.ap-scene .ap-hot{stroke:var(--accent);color:var(--accent)}
.scenes .card{display:flex;flex-direction:column}

/* ------------------------------------------------ §5.6 UK supply chain
   Outline in --line-strong so it reads as a container rather than content.
   Amber is spent on the connecting network, not on any one node: the point
   is that the chain closes inside the UK, not that one stage matters most.
   Labels are --text-secondary, 12:1 on --canvas. */
.uk-root{
  display:block;width:100%;max-width:340px;height:auto;
  color:var(--text-tertiary);
}
.uk-land{stroke:var(--line-strong)}
.uk-net{stroke:var(--accent)}
.uk-nodes circle{fill:var(--canvas);stroke:var(--accent);stroke-width:1.5}
.uk-label text{
  fill:var(--text-secondary);
  font-family:var(--body);font-size:11px;letter-spacing:.05em;
}

/* --------------------------------------------------- §5.8 reticle backdrop
   Behind the contact form, bleeding off the right edge.

   Opacity is set by arithmetic, not by eye. The strictest thing on this page
   is not the text but the input borders: --line-input #5A7078 is 3.57:1 on
   bare --canvas, and WCAG 1.4.11 wants 3:1 for a control boundary. Compositing
   --line-strong #35474F over --canvas #0D1417 at alpha a and recomputing:
       a = .50 -> background L 0.0248 -> border ratio 2.69:1   fails
       a = .30 -> background L 0.0158 -> border ratio 3.06:1   passes
   .28 is used, one notch below the threshold value, so the worst case — a
   border falling directly on a reticle stroke — still clears 3:1. Label text
   (--text-secondary) sits at 9.4:1 against the same worst case. */
.contact-sec{position:relative;overflow:hidden}
.rt-root{
  position:absolute;
  right:-14%;top:50%;
  transform:translateY(-50%);
  width:min(680px,62vw);height:auto;
  color:var(--line-strong);
  opacity:.28;
  pointer-events:none;
  z-index:0;
}
.contact-sec .wrap{position:relative;z-index:2}

@media (max-width:900px){
  /* on narrow screens it would sit directly under the fields, so it retreats
     to a corner and drops further rather than tinting the whole form */
  .rt-root{right:-32%;top:12%;transform:none;width:min(520px,88vw);opacity:.2}
}

/* §5.1 asks for a wide SVG. Inside .sec-grid it only ever gets the 1.6fr body
   column, which at 1440 is about 570px for a 1000-unit viewBox — the drone
   glyphs came out too small to read as drones. It gets the full wrap instead. */
.cx-wrap{margin-top:clamp(2.5rem,6vw,4.5rem)}
.cx-wrap .cx-root{width:100%;max-width:none}

/* Two layouts, one figure. The wide one puts the swarm and the interceptor
   side by side; below 720px that either shrinks the drone glyphs into an
   illegible mesh or forces a sideways scroll, and this is the only graphic on
   the site that would scroll horizontally. The portrait variant stacks them
   instead, so the comparison reads down the page like everything else. */
.cx-narrow{display:none}
@media (max-width:720px){
  .cx-wide{display:none}
  .cx-narrow{
    display:block;
    /* deliberately well short of the column: the figure should sit in white
       space, not fill the screen edge to edge */
    max-width:min(320px,82vw);
    margin:clamp(1.5rem,6vw,2.5rem) auto;
  }
  /* 20 user units against a 560-unit box reads far heavier than the same 20
     against the wide 1000-unit box. Brought back to roughly the same share. */
  .cx-narrow .cx-label{font-size:18px}
  .cx-wrap{overflow:visible;margin-top:clamp(1.5rem,4vw,2.5rem)}
}
