/* ═══════════════════════════════════════════════════════════════════════════
   DRIIF — NEXUS page-specific layout only.
   Anything reusable belongs in components, anything numeric in tokens. Every
   value below resolves to a token; the only literals are small SVG/positioning
   figures too fine-grained to tokenise, the same exception home.css's own
   diagrams (.pd-node, .hatch) already take.

   The hero shell, the problem-resolution "many inputs → one result" diagram,
   the flow strip, the picture-tabs pattern (now also driving the Operating
   Model section), and the Define section's points-list-against-pinned-media
   pattern are all shared with viper.html now and live in components.css —
   see the comment at the top of viper.css for how that split happened.
   What's below is what's actually specific to this page: the loop-back
   caption and the single-line scrolling tabs under Operating Model.
   ═══════════════════════════════════════════════════════════════════════════ */


/* ── THE GAPS COMPOUND: photo on the right, copy stays put ───────────────
   Was #problem's own .problem-resolve block (image right, copy left) —
   moved to its own section/id (#problem-gaps, nexus.html) once #problem
   became VIPER Fold-2's pinned scene walk instead; these rules just moved
   with it; the alignment/column choices are unchanged.

   Went through a fragmented-feeds-diagram version in between (Home's own
   .prob-diagram/.pd-stack motif) — a direct follow-up ("keep only the
   image in this not the text on top") dropped the diagram/labels back to
   a plain photo, so this is back to the same shape .pic-media already
   uses everywhere else on the site: no bespoke rules needed beyond the
   grid placement below.

   .problem-resolve (components.css) is a shared side-by-side grid with
   align-items:center, which is the right call here too. Below 1024px
   this still stacks to a single column (copy, then photo) via the shared
   base rule. */
@media (min-width:1024px){
  /* both explicitly on row 1 — without this, sparse auto-placement puts
     gap-copy on its own row 2 once pic-media's column:2 has advanced the
     placement cursor past column 1 on row 1. */
  #problem-gaps .pic-media{ grid-column:2; grid-row:1; }
  #problem-gaps .gap-copy{ grid-column:1; grid-row:1; }
}
#problem-gaps .gap-copy{ max-width:var(--measure-lead); }


/* ── THE COUNTER-UXV CHALLENGE, PINNED: scroll-scrubbed two-scene reveal ──
   Direct request: "convert [this] to look like viper's 2nd fold, the text
   reveal" — mirrors #gap's own base+pinned split (viper.css) almost
   exactly; see that file's own comments for the fuller reasoning behind
   each piece, this is just the same shape applied here.

   Base rule (always, and the <1024px/reduced-motion/no-GSAP fallback):
   centred, stacked, full-bleed-width-but-measure-capped, no eyebrow, set
   in heading sizes rather than body copy — reads as one deliberate
   statement, same as #gap. problem-scroll.js only ever adds behaviour on
   top via the [data-motion="pinned"] rules below, never removes this. */
#problem .problem-copy{ display:flex; flex-direction:column; gap:var(--space-8);
  max-width:var(--measure-body); margin-inline:auto; text-align:center; }

/* Pinned only (>=1024px, GSAP, motion allowed — problem-scroll.js).
   background-image direct on the SECTION itself, not a separate element —
   deliberately, not by default: an absolutely-positioned sibling backdrop
   with its own opacity fade was tried on VIPER's own #gap-result fold for
   this same "photo behind a pinned scene" job and root-caused two separate
   real bugs before landing on this exact section-background approach (see
   viper.css's own #gap-result[data-motion="pinned"] .gap-result-bg
   comments for the full story) — #problem doesn't need an independently
   fading backdrop (nothing here asks the photo itself to fade in/out
   separately from the scenes), so there's no reason to take on that
   complexity here; the section's own background-image is the simpler,
   already-proven-reliable version of the same trick, same as #gap uses.

   background-attachment:fixed: #problem itself is never visually fixed to
   the viewport (problem-scroll.js pins .problem-copy, not the section) —
   it's an ordinary tall block spanning the whole pin's scroll distance, so
   a plain scroll-attached background would size/centre against that whole
   tall box instead of whatever slice is actually on screen.
   background-size:cover (not contain, unlike #gap-result's close-up
   photo): this is a full-bleed atmospheric backdrop meant to fill the
   frame edge-to-edge behind centred text, not a specific photo whose own
   proportions need preserving. */
#problem[data-motion="pinned"]{
  padding-block:var(--section-y-sm);
  background-image:linear-gradient(rgba(6,6,6,.6),rgba(6,6,6,.6)),
    url("../img/nexus-problem.webp");
  background-size:cover; background-position:center; background-repeat:no-repeat;
  background-attachment:fixed;
}
#problem[data-motion="pinned"] .problem-copy{
  position:relative; min-height:92svh; max-width:var(--measure-lead); justify-content:center;
}
#problem[data-motion="pinned"] .problem-scene{
  position:absolute; left:0; right:0; top:50%; transform:translateY(-50%);
  opacity:0; pointer-events:none; transition:none;
}
#problem[data-motion="pinned"] div.problem-scene{ display:flex; flex-direction:column; gap:var(--space-6); }
/* Whole-phrase fade, not letter-by-letter — the one piece of gap-scroll.js
   this deliberately doesn't carry over; that split was a separate, later
   VIPER-specific refinement (see problem-scroll.js's own header), and this
   plainer version is the same mechanic problem-brighten.js already uses
   on Home's own Problem section, just scrubbed by the pin instead of by
   the paragraph's own scroll position. */
#problem[data-motion="pinned"] .brighten{ opacity:.3; transition:none; }
/* Word-by-word reveal (problem-scroll.js's splitWords) — same split-w
   wrapper viper.css's letter-split uses, one level up: inline-block so a
   word can't be torn apart by a line-break (matters more here than it did
   there, since gap-scroll.js's .split-c inline letters relied on .split-w
   for exactly this — this file just never needed that class until now).
   transition:none for the same reason every other scrubbed value in this
   block gets it: nothing here should ease in after scroll stops. */
#problem .split-w{ display:inline-block; transition:none; }


/* ── DECISION LAYER — INTRO DIVIDER ───────────────────────────────────────
   .split (base.css) is shared by every section-intro on the site — eyebrow
   plus heading on the left, lead paragraph on the right — so the line goes
   on this one instance (#define .split) rather than on .split itself,
   which would draw it under every other section's intro too.

   The two columns don't share a baseline (the lead paragraph starts lower
   than the heading and the two run to different lengths), so a line drawn
   right under the text would either cut through the taller column or float
   above the shorter one. Grid rows size to their tallest cell, so a
   border-bottom on the grid itself — not on either column — always lands
   at the true bottom of whichever column runs longer, which keeps it
   level and reads as one deliberate rule under both boxes rather than
   two different lines. padding-bottom holds it off the text itself,
   then .decision-panels' own margin-top (below) picks up the rest of
   the gap down to the cards. */
#define .split{
  padding-bottom:var(--space-6);
  border-bottom:var(--hair) solid var(--color-border-subtle);
}

/* ── DECISION LAYER — bordered icon-card grid + static visual ────────────
   Replaces the click-to-expand accordion this section used to run
   (.decision-grid/.decision-point*, components.css — still there,
   untouched, for any future section that wants that pattern; this one
   just no longer opts in) with a static two-up layout built from a Figma
   mockup: a bordered 2x2 grid of icon+copy cards on the left, one
   full-height image on the right. Nothing here swaps or expands — every
   card is visible at once, so there's no JS to wire up.

   Went 5fr:7fr (the original .decision-grid split, ~44:56, matching the
   Figma mockup) → 6fr:7fr → 1fr:1fr → 8fr:5fr, each step per direct
   request to give "Airspace Context" (the longest of the four titles)
   enough room to stop wrapping to two lines — 8fr:5fr was the point
   that held it to one line all the way down to the 1024px floor.
   Brought back to 5fr:7fr here, also per direct request, trading that
   back the other way: the image reads bigger again, but measured live
   at this ratio, Airspace Context is back to wrapping to two lines from
   1024px up to somewhere around 1500–1600px wide, and is fine above
   that (the other three titles held one line at every width checked,
   1024 to 2400). If that two-line stretch reads as broken rather than
   intentional at typical laptop/desktop widths, the fix is the same
   trade as before — more width to the cards, less to the image — just
   dial it in less far than 8fr:5fr if the goal is only to clear the
   common range rather than the 1024px floor. */
.decision-panels{
  display:grid; grid-template-columns:1fr; gap:var(--space-7);
  margin-top:var(--space-7);
}
@media (min-width:1024px){
  .decision-panels{
    grid-template-columns:minmax(0,5fr) minmax(0,7fr);
    /* No gutter between the cards and the picture, per direct request: the
       grid's outer hairline runs straight into the image so the two read as
       one seamless block rather than two boxes with a gap. */
    column-gap:0; align-items:stretch;
  }
}

/* The 2x2 "table" look is one outer hairline box plus two interior
   dividers, not four borders glued together at the corners: every card
   draws its own trailing edge (border-bottom always, border-right once
   there are two columns to divide), and the true last row/column just
   has its trailing edge switched back off — cheaper than a border-image
   and it can't show doubled or misaligned lines at the seams the way
   stacking four independent borders can. */
.decision-cards{
  display:grid; grid-template-columns:1fr;
  border:var(--hair) solid var(--color-border-subtle);
}
.decision-card{
  padding:var(--space-6);
  border-bottom:var(--hair) solid var(--color-border-subtle);
}
.decision-card:last-child{ border-bottom:0; }
/* sm (640px), the system's own smallest of its three breakpoints — see
   tokens.css's "three, and only three" note — rather than a one-off
   figure invented just for this grid. */
@media (min-width:640px){
  .decision-cards{ grid-template-columns:1fr 1fr; }
  .decision-card:nth-child(odd){ border-right:var(--hair) solid var(--color-border-subtle); }
  .decision-card:nth-child(3){ border-bottom:0; }
}
/* Fixed height, width auto: the four source icons don't share one
   aspect ratio (a wide terrain plate next to a near-square crosshair),
   so a fixed width would leave them reading as different sizes next to
   each other. Locking height instead keeps all four the same visual
   weight in the grid regardless of their own proportions. */
.decision-card-icon{
  display:block; height:clamp(52px,5vw,76px); width:auto;
  margin-bottom:var(--space-5);
}
.decision-card-title{ margin-bottom:var(--space-3); }

/* Deliberately outside the two-pole+accent system every other surface
   on the site holds to (see tokens.css) — this one panel is meant to
   read as a dark viewport onto the diagram, not as UI chrome, so it
   gets the system's black rather than a new colour. align-items/
   justify-content centre the image if its own ratio ever leaves slack
   at either edge of the stretched panel. */
.decision-visual{
  background:var(--black);
  display:flex; align-items:center; justify-content:center;
  padding:0; /* the artwork's own pure-black edge runs flush to the panel — any padding showed the system black (#0C0E0E) as a grey frame around it */
}
.decision-visual img{ display:block; width:100%; height:auto; }

/* ── DECISION VISUAL — glow-dot pulse ──────────────────────────────────────
   .decision-visual-wobble wraps the image, line network and glow dots as
   one unit so they stay in registration if it ever gets motion again —
   the ambient rotate/translate wobble this wrapper used to carry was
   removed outright per direct request ("the terrain shaking can stop"),
   not just toned down, so no animation/transform-origin remains on it.
   width:100% here (not on the img directly) is what fills the row; the
   img rule above still matches it as a descendant selector.

   The dots can't be isolated from the flat picture with a filter, so each
   .decision-glow is a small independent element instead: a blurred
   radial-gradient bloom, absolutely positioned by --gx/--gy (percentages,
   so they track the image at any rendered width) sampled directly off the
   current nexus-decision-interceptor.webp at its native 1145x1374 — measured
   computationally (thresholding the artwork's own bright/opaque dot
   pixels, not eyeballed) alongside the line network just below, so both
   layers share exactly the same fourteen coordinates. Re-measure if that
   asset is ever replaced with a differently-composed image.
   mix-blend-mode:screen only brightens what's already under it rather than
   painting a flat circle over the artwork, and --gd staggers each dot's
   pulse so they don't all breathe in lockstep, which reads as more alive
   than something obviously looping.

   var(--color-accent) is used for the glow tint deliberately, not as an
   exception to "no fourth colour, ever" (tokens.css): sampled straight off
   the image, the diagram's own dots already render at ~#F6F91C, close
   enough to #E7FF25 that the token IS the right colour here, not a
   coincidence to work around. */
.decision-visual-wobble{
  position:relative; width:100%; line-height:0;
}
/* A very light flat black wash over the artwork (under the lines, dots and
   labels, which are positioned later in the DOM and paint above it). */
.decision-visual-wobble::before{
  content:""; position:absolute; inset:0; background:rgba(var(--rgb-black),.18); pointer-events:none;
}
.decision-glow{
  position:absolute; left:var(--gx); top:var(--gy);
  width:22px; height:22px; margin:-11px 0 0 -11px;
  border-radius:50%; pointer-events:none; mix-blend-mode:screen;
  background:radial-gradient(circle,
    color-mix(in srgb, var(--color-accent) 65%, white) 0%,
    color-mix(in srgb, var(--color-accent) 35%, transparent) 35%,
    transparent 65%);
  filter:blur(2px);
  animation:decision-glow-pulse 2.6s ease-in-out infinite;
  animation-delay:var(--gd, 0s);
}
@keyframes decision-glow-pulse{
  0%,100%{ opacity:.2; transform:scale(.85); }
  50%    { opacity:.55; transform:scale(1.08); }
}
@media (prefers-reduced-motion:reduce){
  .decision-glow{ animation:none; opacity:.7; }
  .decision-line{ animation:none; stroke-dashoffset:0; }
}

/* ── DECISION VISUAL — item-to-item mesh + on-art labels ───────────────────
   Back to a drawn .decision-lines SVG mesh, per direct follow-up with a
   reference image attached showing the target look: the asset itself
   goes back to its plain, non-duotone render (the terrain's own
   wireframe is native white/grey again, matching the reference) and the
   yellow lives entirely in this drawn layer plus the .decision-glow dots.

   Topology matches the reference: a single unlabeled hub node (no icon —
   just the mesh's own middle convergence point, same as the reference's
   central dot) that most items spoke straight to, plus ISR fanning to
   three points (UGV, Radar, the hub) and UAV linking to the operator and
   to UGV, rather than every item routing through the hub alone.

   .decision-line keeps the faint/dotted/glowing treatment from the
   previous pass (round-capped near-zero dashes read as travelling dots,
   the drop-shadow pair is the glow) — opacity nudged up slightly from
   that pass since the reference's lines read a bit more present than
   fully ambient. var(--color-accent) throughout — tokens.css already
   reserves this exact colour for "own assets (UAV/UGV/USV)" markers in
   these scenes.

   .decision-label is back to var(--white) — "make all text yellow" was
   superseded a message later by "make it white only" — while the lines
   and glow dots stay var(--color-accent), per that same follow-up.
   .t-label's own type (Space Grotesk, the site's display/brand face).
   Anchored bottom-left of its dot by default
   (a small fixed pixel offset); .decision-label--r flips the four labels
   that sit past the image's right edge (ISR Drone, Radar, Recon Drone,
   Tactical Command Center) to anchor bottom-right instead, so their text
   can't run past the panel.

   Command Center's dot (74.24%, 64.41%) sits at almost the same height
   as its neighbour just left of it, and the label text is long enough
   that at the panel's actual rendered width (this column is 7/12 of the
   split, nowhere near the image's native 1145px) it collided with that
   neighbour — direct bug report. .decision-label--low drops Command
   Center's text well below that row instead of fixing a width, since
   the collision is a font-size-vs-image-scale mismatch that a fixed
   offset can't out-scale at every width. Mobile Operator's own label
   text was removed entirely per a later direct follow-up — its dot and
   glow stay, unlabeled, same as the central hub dot. */
.decision-lines{
  position:absolute; inset:0; width:100%; height:100%;
  overflow:visible; pointer-events:none;
}
.decision-line{
  fill:none; stroke:var(--color-accent); stroke-width:2.5;
  stroke-linecap:round; stroke-dasharray:.1 13;
  opacity:.75;
  filter:
    drop-shadow(0 0 2px color-mix(in srgb, var(--color-accent) 90%, transparent))
    drop-shadow(0 0 6px color-mix(in srgb, var(--color-accent) 50%, transparent));
  animation:decision-line-flow 1.4s linear infinite;
}
@keyframes decision-line-flow{
  to{ stroke-dashoffset:-26; }
}
.decision-label{
  position:absolute; left:var(--lx); top:var(--ly);
  color:var(--white); white-space:nowrap; pointer-events:none;
}
/* Each label is anchored by one point (--lx/--ly, percentages of the image)
   and one of these, which says which corner of the text sits on that point.
   The points are NOT the asset dots: they were picked in the empty
   background beside each asset so no label lands on the soldier or on a
   UxV. The glow dots and lines keep their own coordinates. */
.decision-label--above{ --lt:translate(-50%,-100%); transform:var(--lt); }
.decision-label--below{ --lt:translate(-50%,0); transform:var(--lt); }
.decision-label--left{ --lt:translate(-100%,-50%); transform:var(--lt); }
.decision-label--start{ --lt:translate(0,0); transform:var(--lt); }
.decision-label--above-end{ --lt:translate(-100%,-100%); transform:var(--lt); }
/* On a phone the artwork is ~280px wide, so the labels drop to the smallest
   UI size, and Interceptor / Command Center take their own phone anchors
   (--mx/--my/--mt) to stay clear of the soldier and of each other. */
@media (max-width:639px){
  .decision-label{
    font-size:var(--ui-xs); letter-spacing:.04em; line-height:1;
    left:var(--mx,var(--lx)); top:var(--my,var(--ly)); transform:var(--mt,var(--lt));
  }
}

/* ── DECISION VISUAL — intercept loop ──────────────────────────────────────
   A hostile contact (red X) hangs in the sky between the UAV and the ISR
   Drone; the Interceptor fires a missile 2s after the contact appears, it
   hits, the X vanishes, and the whole thing repeats for as long as the
   section is on screen.

   One 7s cycle, every piece on the same duration so they cannot drift:

      0 – 0.3s    X pops in (this is the moment you arrive — see below)
      2s          muzzle flash; missile leaves the launcher
      2 – 4.16s  missile flies the arc, accelerating (keyframes are spaced on
                  a t^1.7 curve, so the speed-up is baked into the path)
      4.16s        hit — X flashes and goes, a ring and a core burst at its spot
      4.46 – 7s  sky is clear; the cycle restarts and the X reappears

   Because the X appears at the start of every cycle and the missile always
   leaves 2s later, "2s after you reach the section" holds for the first
   launch and for every repeat alike. The cycle starts when .decision-panels
   gets .in (reveals.js adds it as the panel arrives), not at page load.

   All of it is drawn in the .decision-lines svg, so it scales with the
   image exactly like the lines and cannot misregister. The resting state
   (no JS, or reduced motion) is the red X alone — no missile, no burst.

   Colour: the X is var(--color-designate), the system's one "hostile or
   unknown contact" hue, with SHAPE carrying the difference (guidelines:
   "if a scene seems to need a new colour, the answer is a new glyph").
   The guidelines scope that token to .scene; this panel is the same kind of
   tactical scene, so it is used here by direct request and recorded in
   DRIIF-DESIGN-GUIDELINES.md. The missile and burst are var(--color-accent),
   the colour of own assets. */
.decision-panels{ --intercept-dur:7s; }
.intercept-x{ fill:none; stroke:var(--color-designate); stroke-width:4.5; stroke-linecap:round;
  filter:drop-shadow(0 0 3px color-mix(in srgb, var(--color-designate) 80%, transparent)); }
.intercept-missile{ opacity:0; transform:translate(245px,450px) rotate(-44.2deg); }
.intercept-head{ fill:var(--color-accent);
  filter:
    drop-shadow(0 0 3px color-mix(in srgb, var(--color-accent) 95%, transparent))
    drop-shadow(0 0 9px color-mix(in srgb, var(--color-accent) 60%, transparent)); }
.intercept-hit-ring{ fill:none; stroke:var(--color-accent); stroke-width:3; opacity:0;
  filter:drop-shadow(0 0 5px color-mix(in srgb, var(--color-accent) 80%, transparent)); }
.intercept-hit-core{ fill:var(--color-accent); opacity:0;
  filter:drop-shadow(0 0 10px color-mix(in srgb, var(--color-accent) 90%, transparent)); }
.intercept-launch{ fill:var(--color-accent); opacity:0;
  filter:drop-shadow(0 0 8px color-mix(in srgb, var(--color-accent) 90%, transparent)); }

.decision-panels.in .intercept-target{ animation:decision-target var(--intercept-dur) linear infinite; }
.decision-panels.in .intercept-missile{ animation:decision-missile var(--intercept-dur) linear infinite; }
.decision-panels.in .intercept-hit-ring{ animation:decision-hit-ring var(--intercept-dur) ease-out infinite; }
.decision-panels.in .intercept-hit-core{ animation:decision-hit-core var(--intercept-dur) ease-out infinite; }
.decision-panels.in .intercept-launch{ animation:decision-launch var(--intercept-dur) ease-out infinite; }

@keyframes decision-target{
  0%    { opacity:0; transform:scale(.4); }
  4.29% { opacity:1; transform:scale(1); }
  57.29% { opacity:1; transform:scale(1); }
  59.43% { opacity:1; transform:scale(1.25); }
  63.71% { opacity:0; transform:scale(1.6); }
  100%  { opacity:0; transform:scale(1.6); }
}
@keyframes decision-missile{
  0%, 28.56%{ opacity:0; transform:translate(245px,450px) rotate(-44.2deg); }
  28.57%{ opacity:1; transform:translate(245.0px,450.0px) rotate(-44.2deg); }
  30.78%{ opacity:1; transform:translate(248.2px,446.9px) rotate(-44.1deg); }
  32.98%{ opacity:1; transform:translate(255.2px,440.1px) rotate(-43.8deg); }
  35.18%{ opacity:1; transform:translate(265.4px,430.5px) rotate(-43.4deg); }
  37.39%{ opacity:1; transform:translate(278.1px,418.5px) rotate(-42.9deg); }
  39.59%{ opacity:1; transform:translate(293.3px,404.6px) rotate(-42.2deg); }
  41.80%{ opacity:1; transform:translate(310.8px,388.9px) rotate(-41.5deg); }
  44.00%{ opacity:1; transform:translate(330.2px,372.0px) rotate(-40.6deg); }
  46.20%{ opacity:1; transform:translate(351.7px,353.9px) rotate(-39.7deg); }
  48.41%{ opacity:1; transform:translate(374.9px,335.0px) rotate(-38.5deg); }
  50.61%{ opacity:1; transform:translate(399.8px,315.6px) rotate(-37.2deg); }
  52.82%{ opacity:1; transform:translate(426.4px,295.9px) rotate(-35.8deg); }
  55.02%{ opacity:1; transform:translate(454.5px,276.2px) rotate(-34.2deg); }
  57.22%{ opacity:1; transform:translate(484.1px,256.8px) rotate(-32.4deg); }
  59.43%{ opacity:1; transform:translate(515.0px,238.0px) rotate(-30.3deg); }
  59.44%, 100%{ opacity:0; transform:translate(515px,238px) rotate(-30.3deg); }
}
@keyframes decision-hit-ring{
  0%, 59.33%{ opacity:0; transform:scale(.3); }
  59.43%      { opacity:1; transform:scale(.3); }
  72.29%      { opacity:0; transform:scale(3.4); }
  100%     { opacity:0; transform:scale(3.4); }
}
@keyframes decision-hit-core{
  0%, 59.33%{ opacity:0; transform:scale(.4); }
  59.43%      { opacity:1; transform:scale(.4); }
  65.86%      { opacity:0; transform:scale(2.2); }
  100%     { opacity:0; transform:scale(2.2); }
}
@keyframes decision-launch{
  0%, 28.47%{ opacity:0; transform:scale(.3); }
  28.57%      { opacity:.95; transform:scale(.3); }
  37.14%      { opacity:0; transform:scale(1.8); }
  100%     { opacity:0; transform:scale(1.8); }
}
@media (prefers-reduced-motion:reduce){
  .decision-panels.in .intercept-target,
  .decision-panels.in .intercept-missile,
  .decision-panels.in .intercept-hit-ring,
  .decision-panels.in .intercept-hit-core,
  .decision-panels.in .intercept-launch{ animation:none; }
}

/* ── LOOP-BACK NOTE ───────────────────────────────────────────────────────
   The flow strip (.flow-strip/.flow-step, shared with VIPER) closes its
   loop here: Respond feeds back into Sense, so a small centred caption
   states that explicitly rather than leaving the strip looking linear.
   Now a .decision-grid child rather than a sibling after it (see the
   markup comment in nexus.html), so grid-column:1/-1 is what keeps it
   spanning under both columns instead of landing in column one alone —
   below 1024px the grid is single-column anyway and this is a no-op. */
/* A hairline above the note, same device #define's own intro divider
   (.split, above) and VIPER's .mission-set use — draws a deliberate line
   under the row above rather than leaving the gap to it unmarked. Border
   sits at the very top of the margin box; margin-top is the space above
   the line, padding-top the space below it before the note's own text —
   matched to the same token so the line sits dead centre in its gap
   rather than closer to one side. */
.loop-note{ grid-column:1/-1; text-align:center; margin-top:var(--space-7);
  padding-top:var(--space-7); border-top:var(--hair) solid var(--color-border-subtle); }

/* Pinned mode only: the grid's own row-gap (components.css, tightened to
   --space-5 specifically for this pinned second row) already separates
   this from the points+media row above it, so the base rule's margin-top
   would double that spacing on top of it — harmless on the static/mobile
   fallback below 1024px (plenty of scroll room there, and the wider gap
   reads fine in a normal scroll), but at pin width every extra px pushes
   the line closer to the bottom edge of the pin's one-viewport-tall
   visible area. Margin zeroed and padding-top matched to the same
   --space-5 the row-gap uses — same reasoning as the base rule above,
   just at the pinned scale — rather than in the base rule so the
   fallback keeps its own, more generous spacing; the border stays in the
   base rule since both states want the line. */
[data-motion="pinned"] .loop-note{ margin-top:0; padding-top:var(--space-5); }


/* Operational Loop went through two other shapes before settling back on
   Define's own points-against-pinned-media pattern (.decision-grid, shared
   in components.css) — same layout, same pin, same [data-video] swap via
   decision-scroll.js, just this section's own four points/clips. Nothing
   loop-specific left to style here beyond the loop-back note above. */


/* ── OPERATING MODEL: static text-only card grid ──────────────────────────
   Replaces the click-to-switch picture tabs this section used to run
   (.pic-tabs/.pic-panels + tabs.js/tab-panels.js — still there, untouched,
   for viper.html's own picture-tabs section and the Operate Stage section
   further up this same page, which both still opt in) with a static,
   always-visible set of five cards. Per direct request: white section
   like the home page's Mission Layer, same card shape, no icon or image —
   so every card is on screen at once and there's no JS to wire up here.

   Reuses .pillars + .card-notch (components.css) rather than a new card —
   the same pair the home page's Mission Layer (#mission, home.css) uses
   for its own six-card set. This section overrides three things, all
   scoped to this page only:

   1. Column count — was bumped to a 5-across single row at 1600px (the
      old "widen instead of wrap" fix). Direct follow-up ("3x2 grid,
      smaller and compact") drops that: 3 columns from the same 1024px
      desktop breakpoint the rest of the site uses, so five cards read
      as a 3-over-2 grid (a widowed pair, not a widowed single) at every
      desktop width, not just 1600px+.
   2. Card size — min-height and inner padding both pulled in from the
      shared defaults (components.css: 200px baseline, --space-6 padding)
      for the same "smaller and compact" request, now that 3-up cards
      have more width to spare than the old 5-up row did.
   3. Heading/subtext gap — margin-top pulled in from the shared
      --space-5 default (components.css) to --space-3, matching the
      home page's own Mission Layer cards (#mission, home.css) exactly,
      per direct follow-up ("reduce gap between heading and subtext
      inside the boxes like home page mission layer section"). */
#edge-authority .card-notch{ min-height:160px; }
#edge-authority .card-notch .inner{ padding:var(--space-5); }
#edge-authority .card-notch p{ margin-top:var(--space-3); }
@media (min-width:1024px){
  #edge-authority .pillars{ grid-template-columns:repeat(3,1fr); gap:var(--space-5); }
}


/* ── HETEROGENEOUS INTRO LEAD ─────────────────────────────────────────────
   .arch-support (components.css) assumes it follows the stack — this
   section's supporting line comes before it instead, so it gets its own
   centred, auto-margin rule rather than repurposing arch-support's spacing
   for a placement it wasn't built for. */
.hetero-lead{ max-width:var(--measure-lead); margin:0 auto var(--space-8); }


/* ── CAPABILITY COUNT OVERRIDE ────────────────────────────────────────────
   .pillars (components.css) defaults to 3-up at 1600px, tuned for VIPER's
   six cards. NEXUS has four, and three columns would widow a single card
   into its own row — capped at two here instead, scoped to this page only. */
@media (min-width:1600px){
  #capabilities .pillars{ grid-template-columns:repeat(2,1fr); }
}

/* ── HERO POSTER ─────────────────────────────────────────────────────────── */
/* The black floor and sizing live in motion.css; only the image is per page.
   Without this the NEXUS hero has no fallback behind the video. */
.hero .media{ background-image:url("../img/nexus-hero-poster.jpg"); }

/* ── HERO INTRO: lead/CTA pushed later ───────────────────────────────────
   motion.css's shared .intro-d3/.intro-d4 delays (480ms/600ms) were tuned
   for a single-line heading. This hero's own heading is now two lines
   (.intro-line:nth-of-type(2), motion.css), whose rise doesn't finish
   until ~520ms+900ms=1420ms, so the old delays landed the lead and CTA
   arriving before the second line had even started. Pushed back scoped
   to this page only, rather than in the shared file — Applications still
   has a single-line heading and wants the original timing. */
#top .hero-side .intro-d3{ animation-delay:700ms; }
#top .hero-side .intro-d4{ animation-delay:850ms; }
