/* ═══════════════════════════════════════════════════════════════════════════
   DRIIF — VIPER page-specific layout only.
   Anything reusable belongs in components, anything numeric in tokens.

   The hero shell, the two-column hero split, the flow strip and the
   picture-tabs pattern are all shared with nexus.html and/or index.html
   and live in components.css. Fold 3 (Mission Layer) reuses
   .problem-resolve — the image-left/copy-right pattern components.css
   already shares with nexus.html's Counter-UAS Challenge and the
   Applications page's own detail folds — rather than the pinned
   points-against-media pattern (.decision-grid) NEXUS's Define and
   Operational Loop use: Mission Layer's own points have no per-point video
   to justify stepping through one at a time, so they sit static instead,
   one after the other. Fold 5 (Edge Autonomy) went through a .decision-grid
   phase of its own for a while (scroll pin-and-walk, one photo per point)
   but is back to its original pattern — a click-to-flip grid (.edge-grid,
   edge-flip.js), each of its three points flipping in place to its own
   photo rather than swapping a shared media slot. What's below is what's
   actually specific to this page: Fold 2's plain-text passage and its
   pinned full-screen scene walk (gap-scroll.js), Fold 3's own divider and
   copy layout, Fold 4's tab-row scroll buttons (NEXUS's Operating Model
   tabs rely on native touch/trackpad scroll alone; this page's six tabs
   get explicit prev/next controls too), Fold 5's own flip grid, and
   Fold 6's comparison table, styled here for the first time.
   ═══════════════════════════════════════════════════════════════════════════ */


/* ── FOLD 1: 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; }


/* ── FOLD 3: MISSION LAYER — the network, drawn over the picture ───────────
   The section is a heading, the two copy paragraphs, then one wide picture:
   a rectangular terrain slab seen straight from the front (road, buildings,
   trees, harbour, pylons) with the ten platforms standing on it. It replaced
   a generated landscape render that read as AI-made and off-style next to the
   Mission Network scene, and it is sized down to a ~1100px panel so it sits
   in the page instead of filling it.

   .mission-net-stage holds the picture and everything drawn on it as one
   registered unit: the SVG (viewBox = the picture's native 1600x735, so line
   coordinates are plain image pixels and track it at any rendered width),
   and the labels (positioned in percentages of that same box). Regenerate
   all three together if the picture is ever re-baked.

   Two tiers of line, same dotted/round-cap/glow technique as the Nexus
   Decision Layer: .mn-line--peer is the link between one asset and the next,
   dimmed right down so it only supports; .mn-line--spoke is the accent-yellow
   link from an asset into the VIPER screen, the one bright tier, since the
   spokes are the point of the picture. All lines are straight — curved ones
   read as scattered over the picture, direct feedback. Every line is dotted
   by stroke-dasharray, which rules out drawing it in with dashoffset — so
   each carries an SVG <mask> whose own solid stroke (.mn-draw) is what
   animates in; the dots appear only where the mask has reached.

   Sequence once .in lands (all delays are authored per element, --dl, in
   viper.html): the picture fades in while the ten asset-to-asset peer links
   start drawing at once, one by one, each over 1.5s; the nine spokes follow
   into the screen the same way, then the VIPER Mission OS label. Each
   asset's name appears with the first line to reach it. No end-point dots —
   lines only, per direct request. Without JS, or if .in never arrives, none
   of the hidden states apply and the finished picture shows.

   The terrain is dimmed and each platform sits in a dark halo (baked into
   the picture) so the platforms never camouflage against the ground.

   The stage is capped at 1100px and so the WHOLE picture fits in one
   viewport height on short windows (width <= (viewport height - --space-9) *
   1600/735, the picture's own ratio). Below 720px the stage keeps that
   minimum width and the wrapper scrolls sideways — eleven names on a phone-
   width picture would sit on top of each other, and the earlier mobile-
   overlap complaints on this site were exactly that. */
.mission-net{ margin:var(--space-8) 0 0; }
.mission-net-scroll{ overflow-x:auto; scrollbar-width:thin; }
.mission-net-stage{ position:relative; width:min(100%, 1100px, calc((100vh - var(--space-9)) * 1600 / 735));
  width:min(100%, 1100px, calc((100svh - var(--space-9)) * 1600 / 735));
  min-width:720px; margin-inline:auto; line-height:0; background:var(--black); }
.mission-net-stage img{ display:block; width:100%; height:auto; }
.mn-lines{ position:absolute; inset:0; width:100%; height:100%; overflow:visible; pointer-events:none; }
.mn-draw{ fill:none; stroke:var(--white); stroke-width:36; stroke-dasharray:1; stroke-dashoffset:0; }
.mn-line{ fill:none; stroke-linecap:round; animation:mn-flow 1.6s linear infinite; }
.mn-line--peer{
  stroke:color-mix(in srgb, var(--white) 62%, transparent); stroke-width:4; stroke-dasharray:.1 12;
  filter:drop-shadow(0 0 2px color-mix(in srgb, var(--white) 40%, transparent));
}
.mn-line--spoke{
  stroke:color-mix(in srgb, var(--color-accent) 85%, transparent); stroke-width:3.4; stroke-dasharray:.1 12;
  filter:
    drop-shadow(0 0 2px color-mix(in srgb, var(--color-accent) 50%, transparent))
    drop-shadow(0 0 6px color-mix(in srgb, var(--color-accent) 25%, transparent));
}
@keyframes mn-flow{ to{ stroke-dashoffset:-24.2; } }

.mn-label{
  position:absolute; left:var(--gx); top:var(--gy);
  color:var(--white); white-space:nowrap; pointer-events:none;
  text-shadow:0 0 var(--space-2) var(--black), 0 0 2px var(--black);
}
.mn-label--c{ transform:translate(-50%,0); }
.mn-label--r{ transform:translate(var(--space-3),-50%); }
.mn-label--l{ transform:translate(calc(-100% - var(--space-3)),-50%); }
.mn-label--core{ color:var(--color-accent);
  padding:var(--space-1) var(--space-2); background:color-mix(in srgb, var(--black) 82%, transparent); }

/* Hidden start state, only while JS is in charge and .in hasn't landed. */
.js .mission-net:not(.in) .mn-draw{ stroke-dashoffset:1; }
.js .mission-net:not(.in) .mn-label{ opacity:0; }
.js .mission-net.in .mn-draw{ animation:mn-draw 1500ms var(--ease-in-out) var(--dl) backwards; }
.js .mission-net.in .mn-label{ animation:mn-fade var(--dur-enter) var(--ease-out) var(--dl) backwards; }
@keyframes mn-draw{ from{ stroke-dashoffset:1; } to{ stroke-dashoffset:0; } }
@keyframes mn-fade{ from{ opacity:0; } to{ opacity:1; } }

@media (prefers-reduced-motion:reduce){
  .js .mission-net .mn-draw,
  .js .mission-net .mn-label{ animation:none !important; }
  .mn-line{ animation:none; }
}

.mission-net-copy{ display:grid; grid-template-columns:1fr; gap:var(--space-8); margin-top:var(--space-7); }
.mission-net-copy .gap-copy{ display:flex; flex-direction:column; gap:var(--space-4); }
@media (min-width:1024px){
  .mission-net-copy{ grid-template-columns:minmax(0,1fr) minmax(0,1fr); column-gap:var(--gutter-lg); }
  .mission-net-copy .gap-copy{ max-width:var(--measure-card); }
  .mission-net-copy .gap-copy:nth-child(2){ margin-left:var(--space-7); }  /* Platform State sits a little further right */
}
@media (min-width:1280px){
  .mission-net-copy .gap-copy:nth-child(2){ margin-left:var(--space-9); }
}

/* FOLD 2 / 2b and the letter split now live in gap-fold.css (shared with mission-os.html). */

/* ── FOLD 4: OPERATING MODEL — tab row scroll buttons ───────────────────────
   Same reasoning as home.css's #mission .pillars-scroll-btn: a bare
   arrowhead (no box, no fill, two rotated border edges — the .nav-caret
   construction), grouped in a controls pair after the row it scrolls
   rather than flanking it, so the row's own left edge always lines up
   with the heading above it regardless of which controls are visible.
   .next dims once nothing is left to reveal; .prev stays invisible until
   the row has scrolled away from its start, so there's always a way back
   to the first tab without hand-scrolling. carousels.js's tabStripNav
   drives both, off the active tab's own index rather than scroll
   position — see the "Side-by-side arrows" note below for why. */
/* .pic-tabs (components.css) carries its own margin-top:section-y-sm, sized
   for when it sits directly as a section child right after .split. Wrapped
   in .pic-tabs-row instead, that margin doesn't disappear — it inflates the
   row's own cross-size (the tallest flex item's margin box sets the line
   height), which then throws off align-items:center for the shorter control
   buttons beside it: they end up centred against the tabs' empty margin
   space, well above the visible tab labels. Moving the gap-from-heading
   onto the row itself and zeroing it on .pic-tabs keeps the same spacing
   without the row growing taller than its visible content. */
/* Side-by-side arrows kept as-is here — no repositioning requested for this
   strip, only the Swiper engine swap: .pic-tabs-row's row layout is
   unchanged, and only .pic-tabs itself needs its manual scroll-snap CSS
   moved into a :not(.swiper-initialized) fallback (carousels.js drives the
   paging now) plus the is-end/is-visible classes swapped for Swiper's own
   .swiper-button-disabled toggle. */
#orchestration .pic-tabs-row{ display:flex; align-items:center; gap:var(--space-4);
  margin-top:var(--section-y-sm); }
/* Below 1024px the tabs are gone and the panels stack (components.css,
   "MOBILE: NO TAB ROWS"). This wrapper has to go with them, and the rule has
   to live HERE rather than beside that block: the display:flex directly above
   uses the same selector at the same specificity and this file loads after
   components.css, so the same rule written there loses and left an empty row
   with its prev/next buttons still sitting in it. The controls are tab
   controls (carousels.js) — with no tabs there is nothing for them to do. */
@media (max-width:1023px){
  #orchestration .pic-tabs-row{ display:none; }
}
/* PHONES AND TABLETS: the eight panels as a swipeable strip, not a stack
   (direct request, "a slider like we did before" = #platforms below). One
   card per capability, 85% of the screen with the next peeking in: the
   video on top, its heading and text under it. scene-in-view.js plays the
   card in view (the peeking one is under its threshold) and warms the next
   one; strip-nav.js drives the prev/next pair. */
.om-controls{ display:none; }
@media (max-width:1023px){
  #orchestration .pic-panels{
    display:flex; gap:var(--space-4); margin-inline:calc(var(--edge) * -1); padding-inline:var(--edge);
    overflow-x:auto; scroll-snap-type:x mandatory; scroll-padding-inline:var(--edge);
    overscroll-behavior-x:contain; scrollbar-width:none;
  }
  #orchestration .pic-panels::-webkit-scrollbar{ display:none; }
  #orchestration .pic-panels > .tab-panel,
  #orchestration .pic-panels > .tab-panel + .tab-panel{
    display:flex; flex-direction:column; gap:var(--space-5); flex:0 0 85%;
    margin-top:0; padding-top:0; border-top:0; scroll-snap-align:start;
  }
  #orchestration .pic-panels .pic-media{ order:-1; }
  .js .om-controls{ display:flex; justify-content:flex-end; padding-inline:0; margin-top:var(--space-5); }
}
@media (min-width:640px) and (max-width:1023px){
  #orchestration .pic-panels > .tab-panel,
  #orchestration .pic-panels > .tab-panel + .tab-panel{ flex-basis:60%; }
}
#orchestration .pic-tabs{
  margin-top:0; flex:1 1 auto; min-width:0;
}
/* Swiper's own base CSS gives every .swiper-slide (so every .tab here)
   width:100% — meant for a one-slide-per-view carousel, not a compact tab
   bar, so left alone it stretches whichever tab is current across the
   entire row instead of sizing to its own label. slidesPerView:'auto'
   (carousels.js) is what's supposed to size each tab from its own content,
   but that only works if the tab's CSS width is already content-sized
   before Swiper reads it — max-content reclaims that regardless of what
   .swiper-slide set first. */
#orchestration .pic-tabs .tab.swiper-slide{ width:max-content; }
/* FALLBACK: if Swiper's vendor file didn't load, carousels.js never flips
   .swiper-initialized on and this row needs to still scroll on its own —
   the exact scroll-snap rules this used to carry unconditionally, now
   scoped so they only engage in that failure case. */
#orchestration .pic-tabs:not(.swiper-initialized){
  flex-wrap:nowrap; overflow-x:auto;
  scroll-snap-type:x proximity; scroll-behavior:smooth;
  scrollbar-width:none; -ms-overflow-style:none;
}
#orchestration .pic-tabs:not(.swiper-initialized)::-webkit-scrollbar{ display:none; }
#orchestration .pic-tabs:not(.swiper-initialized) .tab{ flex:none; scroll-snap-align:start; }
/* The row itself is .strip-controls in components.css now. This one is NOT
   one of the identical copies that component replaced: these arrows sit
   inline beside the tab row rather than in their own right-aligned row
   below a strip, so the shared justify-content/margin-top are overridden
   back off here. viper.css loads after components.css, so this wins. */
#orchestration .strip-controls{
  justify-content:flex-start; margin-top:0;
  /* padding-inline:0, against the component's default: .strip-controls insets
     itself by --edge so its buttons land on the container edge, but THIS
     instance is already inside a .bleed (section > .bleed > .pic-tabs-row >
     here), which has applied that inset once already. Keeping the default
     added it twice — the buttons stopped 48px short of the container edge and
     sat 64px off the end of the tab row instead of --space-2. Every other
     strip's controls sit outside a .bleed, which is why the padding belongs
     on the component and the exception belongs here. */
  padding-inline:0;
}
/* The prev/next pair is .strip-btn in components.css now — one filled
   component instead of this same block pasted into four page stylesheets.
   Nothing page-specific remains: #orchestration .pic-tabs-scroll-btn carried no rule
   the other three did not carry identically. Positioning stays local (the
   *-controls rule above); the button's own look does not. */

/* ── FOLD 4: OPERATING MODEL — "coming soon" tab ─────────────────────────
   Threat Classification & Detection isn't built yet, but earns a place in
   the row so the roadmap is visible rather than silently absent. .pill
   (components.css) carries the label; --soon fills it solid accent yellow
   instead of the shared bare-outline look — the same "yellow means
   attention" language the CTA buttons already use — so it reads as loud
   on purpose rather than blending in like every shipped tab's plain text
   label. Reused a second time next to the panel's own heading, so the
   label still reads clearly once a visitor has actually clicked through
   to this tab and the row above may have scrolled out of view.
   align-items:center on .tab--soon overrides .tab's own baseline
   alignment (components.css) — baseline put the pill's padded box a few
   px below the tab label's own baseline instead of visually centred
   beside it. */
/* SUBTLE, per direct request: "this coming soon must be subtle, it's too big
   and in the face."

   It was a solid --color-accent fill with accent-ink text — the loudest
   treatment in the system — sitting inside a tab button whose own active
   underline is already accent. That is two accent moments in one row, over
   the one-per-viewport budget (guidelines rule 6), and it made the least
   important thing in the row the first thing the eye landed on.

   Now: no fill, no accent, hairline border, muted text, and tighter padding
   so its box stops competing with the tab label it annotates. It still reads
   as a badge rather than as part of the label, which is the only job it has. */
.pill--soon{
  background:none; color:var(--color-text-muted);
  border-color:var(--color-border-subtle);
  padding:var(--space-1) var(--space-2);
  padding-right:calc(var(--space-2) - var(--track-label));
}
.tab--soon{ align-items:center; }
/* The panel's own "Coming soon" now sits in the middle of the picture instead
   of beside the heading (the tab-row pill is unchanged). The render is 16:9, so
   this panel's frame matches it rather than cropping the corner brackets off
   with the site-wide 16:10 cover crop. */
#orchestration [data-panel="7"] .pic-media{ aspect-ratio:1672 / 941; }
.pic-soon{
  position:absolute; top:50%; left:50%; transform:translate(-50%,-50%);
  padding:var(--space-3) var(--space-6);
  color:var(--white); white-space:nowrap; pointer-events:none;
  background:var(--scrim-82); border:var(--hair) solid var(--color-border);
}


/* ── FOLD 5: EDGE AUTONOMY — product designation ────────────────────────────
   A metadata line naming the agent under the heading. .t-meta carries no
   margin of its own (base.css — it is normally set on a span), and a <p>
   would otherwise take the browser's 1em default on both sides, so the
   offset is declared here rather than inherited. .split's gap only
   separates the two columns, never the stack inside one.
   Scoped to this page because it is the only designation line on the site;
   if a second product ever needs one, this moves to components.css. */
#edge .edge-designation{ margin:var(--space-4) 0 0; }


/* ── FOLD 5: EDGE AUTONOMY — the flip grid ──────────────────────────────────
   Replaces the old three-column .flow-strip. The heading above (.split,
   base.css) is untouched — eyebrow and h2 on the left, the lead paragraph
   beside it, same as every other fold on the site. What changed is what
   sits below it: a dark card (surface-dark + .ground, base.css, so it
   paints the flipped black-on-white palette regardless of this section's
   own surface-light) holding a 4x2 grid of the old flow-strip's three
   points (Edge Execution / Degraded Connectivity / Human Authority). Each
   of those three content cells is its OWN click target (edge-flip.js
   toggles .is-flipped on the cell itself, not a shared grid class;
   role="button" + tabindex on each cell's div since it's a custom
   control, not phrasing content a <button> could legally wrap): click one
   and it does a real 3D card flip — same mechanism as glossary.html's
   .g-card (perspective on the outer cell, preserve-3d + rotateY(180deg)
   on an inner wrapper, backface-visibility:hidden on each face) — from a
   copy face to a photo face, click it again and it flips back. The other
   two cells are untouched either way, and any combination of the three
   can be flipped at once. Nothing here is scroll-linked — this is a
   plain per-cell state toggle, no GSAP involved.

   Both faces are absolutely positioned inside a cell whose row height is
   an explicit fixed value (see --edge-row-h below), not derived from
   content — so unlike an opacity-crossfade approach, neither face's own
   size can ever move the grid: nothing needs measuring, capping, or
   duplicating to hold the row steady across whichever combination of
   01/02/03 happen to be flipped at once.

   The grid is 4 columns x 2 rows, eight equal cells, and that's exactly
   what the static (text) state stays — no spans, plain auto-placement,
   matching the reference as given. Flipping a cell is what reshapes it:
   01 and 02 each grow to swallow the empty cell beside them into a
   two-column tile, and 03 grows to swallow the one below it into a
   two-row tile — a photo gets a tile shaped for it instead of being
   forced into an identical box, but only once you've actually asked to
   see it, and independently of whatever the other two cells are doing.
   The two 1x1 units that are never absorbed (fill-2, fill-3) stay bare in
   every combination — literally empty <div>s, no image, no placeholder
   caption — so an empty cell reads as empty, not as an unfinished one.

   Below 1024px there's no room for any of this, so the grid becomes what
   the old .flow-strip already was on small screens: the three numbered
   points stacked plain, no photos, no click target. The five bare cells
   and the click affordance simply don't render below that width. */
#edge .edge-grid{
  margin-top:var(--section-y-sm); border:var(--hair) solid var(--color-border);
  padding:var(--space-5);
}
#edge .edge-cell{ display:none; }
/* A divider between stacked items, not under every item: :last-of-type
   would look like the obvious tool here, but it's scoped by tag name, not
   class, and every cell — content or filler — is a plain <div>, so it can
   never single out "the last .edge-cell--content" the way it reads. The
   general sibling combinator does what's actually needed instead: give a
   top border to any content cell that has an earlier content cell among
   its siblings, filler <div>s in between or not. */
#edge .edge-cell--content{
  display:flex; flex-direction:column; gap:var(--space-3); padding:var(--space-5) 0;
}
#edge .edge-cell--content ~ .edge-cell--content{ border-top:var(--hair) solid var(--color-border-subtle); }
/* num + copy stack plainly below 1024px, num above heading above body,
   same shape either way the flip apparatus renders — no wrapper needed
   for that, .edge-cell--content's own column flex above already does it. */
#edge .edge-cell-face--front{ display:flex; flex-direction:column; gap:var(--space-3); }
#edge .edge-cell-copy{ display:flex; flex-direction:column; gap:var(--space-2); }

@media (min-width:1024px){
  /* Explicit row height, not content-driven: the whole point of a real
     flip card (see below) is that both faces sit absolutely inside a box
     whose size nothing about their own content can move. 230px
     comfortably fits the longest static copy (02's four-line body) at
     the compact size set further down, with room to spare in every
     other combination — verified across every flip combination, not
     just guessed. */
  #edge .edge-grid{
    --edge-row-h:230px;
    position:relative; padding:0;
    display:grid; grid-template-columns:repeat(4,1fr); grid-template-rows:repeat(2,var(--edge-row-h));
  }
  /* No grid-column/grid-row here — all eight cells are plain auto-placed
     items, landing in DOM order into the eight units exactly as they
     read in the markup (01, fill-1, fill-2, 03, fill-3, 02, fill-4,
     fill-5 → row1 then row2, left to right). Borders come from position
     alone, same nth-child scheme as any other uniform grid on the site:
     every cell gets a right and bottom rule, the rightmost column drops
     the right one, the bottom row drops the bottom one. */
  #edge .edge-cell{
    display:block; position:relative; overflow:hidden;
    border-right:var(--hair) solid var(--color-border-subtle);
    border-bottom:var(--hair) solid var(--color-border-subtle);
  }
  #edge .edge-cell:nth-child(4n){ border-right:0; }
  #edge .edge-cell:nth-child(n+5){ border-bottom:0; }
  /* Padding moves to each individual face (below) instead of sitting
     here — .edge-cell-flip needs to fill the cell edge to edge for its
     two faces' inset:0 to mean "the whole cell", not "the padded box". */
  #edge .edge-cell--content{ padding:0; cursor:pointer; perspective:1200px; }

  /* Flipping a cell is the only time any of this grid reshapes, and each
     of the three does it on its own — the rules below key off the CELL's
     own .is-flipped (edge-flip.js puts it there directly now, not on a
     shared grid-level class), so 01 can be flipped while 02 and 03 stay
     put, in any combination. 01 and 02 each take the two columns they and
     their neighbour used to occupy separately; 03 takes both rows in its
     own column. fill-1, fill-4 and fill-5 are the three cells that get
     swallowed that way — the general sibling combinator (~) reaches a
     filler from whichever content cell absorbs it, since is-flipped now
     lives on that cell rather than a shared ancestor; display:none removes
     the filler from the grid outright rather than leaving an invisible
     gap. fill-2 and fill-3 (the two that never get absorbed) need no rule
     here at all regardless of which cells are currently flipped: CSS
     Grid's own auto-placement slots every still-auto-positioned item into
     whatever's left once however many explicit spans have claimed their
     space. 03's own border-bottom is explicitly zeroed once it spans into
     the last row — its default border-bottom (from the uniform rule
     above, since position 4 isn't in the bottom row normally) would
     otherwise draw a redundant line right on top of the grid's own outer
     edge. Row height is fixed (above), so none of this spanning ever
     moves the grid regardless of which cells are flipped — a spanned
     cell's two faces just get more width to render into. */
  /* .is-expanded, NOT .is-flipped, and the difference is the whole fix for
     the jagged flip reported here.

     grid-column, grid-row and display are not animatable properties. Both
     classes used to be the same class, so the instant it was applied the
     cell's box doubled in one frame and its filler neighbour vanished in
     the same frame, while the 3D rotation underneath took --dur-enter
     (900ms) to play. Geometry snapping against a running animation is
     exactly what read as jagged.

     Splitting them lets edge-flip.js apply the geometry at the MIDPOINT of
     the rotation, where the card is edge-on at 90deg and occupies no visible
     width — so the resize still happens in one frame, but in the one frame
     where there is nothing to see. Nothing here animates that could not
     animate before; the change is purely WHEN the snap happens.

     Both classes must stay in lockstep: a cell left with .is-expanded but
     not .is-flipped is a double-width cell showing its front face. See the
     paired timers in edge-flip.js, which is also why it cleans up its own
     pending timer if the cell is clicked again mid-flip. */
  #edge .edge-cell--01.is-expanded{ grid-column:1 / 3; grid-row:1; }
  #edge .edge-cell--01.is-expanded ~ .edge-cell--fill-1{ display:none; }
  #edge .edge-cell--03.is-expanded{ grid-column:4; grid-row:1 / 3; border-bottom:0; }
  #edge .edge-cell--03.is-expanded ~ .edge-cell--fill-5{ display:none; }
  #edge .edge-cell--02.is-expanded{ grid-column:2 / 4; grid-row:2; }
  #edge .edge-cell--02.is-expanded ~ .edge-cell--fill-4{ display:none; }
}

/* The flip itself, at EVERY width (it used to sit inside the >=1024 block,
   which left phones a plain text list with each card's photo hidden; by
   direct request phones now flip text -> photo like desktop). The grid
   layout and the expansion above stay desktop-only; the phone layout is
   the block after this one. */

  /* The flip itself: same construction as glossary.html's .g-card-inner —
     perspective on the outer, non-rotating cell (above); a full-size,
     still non-positioned wrapper here so it fills that cell; two
     absolutely-positioned faces inside it, backface-visibility:hidden so
     only whichever one is currently facing forward ever paints.
     display:block undoes the base rule's display:contents (mobile only,
     in spirit, but written unconditionally there) — a transform has no
     effect on a boxless contents element, so without this override the
     cell would carry .is-flipped but never visibly rotate. */
  #edge .edge-cell-flip{
    display:block; position:relative; width:100%; height:100%;
    transform-style:preserve-3d;
    transition:transform var(--dur-enter) var(--ease-in-out);
  }
  #edge .edge-cell--content.is-flipped .edge-cell-flip{ transform:rotateY(180deg); }
  #edge .edge-cell-face{
    position:absolute; inset:0; overflow:hidden;
    backface-visibility:hidden; padding:var(--space-5);
  }
  #edge .edge-cell-face--front{ gap:var(--space-2); }
  /* display:block overrides the base rule's display:none (mobile only, in
     spirit, but written unconditionally there — same fix as .edge-cell-flip
     above) — without it the back face never generates a box, so the photo
     and duplicated numeral/heading inside it never render even once the
     cell carries .is-flipped and its parent has actually rotated. */
  #edge .edge-cell-face--back{ display:block; transform:rotateY(180deg); }

  /* Static copy, compact: card titles' usual t-h3/t-body pairing (26px
     heading, 18px body) read too big once the row settled to a fixed,
     denser height above — sized down a tier and tightened here instead
     of reusing those classes as given. */
  #edge .edge-cell-copy h3{
    font-family:var(--font-display); font-weight:500;
    font-size:var(--text-lead); line-height:var(--lh-snug); letter-spacing:var(--track-tight);
  }
  #edge .edge-cell-copy p{
    font-family:var(--font-body); font-weight:400;
    font-size:var(--ui-md); line-height:var(--lh-body);
  }

  /* Photo face: the render fills it edge to edge (inset:0 against the
     face's own padding box bleeds under the padding, same as any other
     full-bleed media slot on the site), the duplicated numeral + heading
     sit on top of it inset by that padding. The heading here is set to
     read exactly like the numeral beside it — same .t-meta/.accent
     styling in the markup — rather than the full card-title size the
     static face carries. */
  #edge .edge-cell-img{
    position:absolute; inset:0; width:100%; height:100%; object-fit:cover;
  }
  /* Soft fade: the source photos read brighter than the rest of this dark
     card once dropped in at full brightness — reported directly ("don't
     look so bright"). A gradient tint (darkest along the top, where the
     numeral/heading sit, easing off through the middle so the photo itself
     still reads, settling slightly darker again at the bottom) rather than
     a flat filter, so it stays a "fade" and not a haze over the whole
     image. 12,14,14 is --black (tokens.css) spelled out in rgb() rather
     than referenced — CSS can't tint a var() through alpha without the
     relative-color rgb(from …) syntax, which nothing else on this site
     uses yet, so this stays a plain literal like the .edge-cell-face-head
     text-shadow just below does for the same reason. Sits between the
     photo and .edge-cell-face-head in source order — the head's own
     z-index:1 is what actually keeps it on top, not paint order, since
     this and the <img> are both z-index:auto. */
  #edge .edge-cell-face--back::after{
    content:''; position:absolute; inset:0; pointer-events:none;
    background:linear-gradient(180deg,
      rgba(12,14,14,.58) 0%,
      rgba(12,14,14,.22) 45%,
      rgba(12,14,14,.4) 100%);
  }
  #edge .edge-cell-face-head{
    position:relative; z-index:1;
    display:flex; align-items:baseline; gap:var(--space-3);
    text-shadow:0 1px 4px rgba(0,0,0,.85);
  }

  /* Click affordance: the same bare-arrowhead construction used
     everywhere else on the site (two rotated border edges — see
     .pillars-scroll-btn::before in home.css), rotated diagonal instead
     of left/right, paired with a "TAP" label small enough to read as
     a tag, not a caption. Lives outside .edge-cell-flip entirely,
     directly on the cell, so it never rotates with either face and reads
     identically whichever one is currently showing. */
  #edge .edge-cell-hint{
    display:flex; align-items:center; gap:6px;
    position:absolute; right:var(--space-5); bottom:var(--space-5); z-index:2;
    font-family:var(--font-display); font-weight:500; font-size:10px;
    letter-spacing:var(--track-label); text-transform:uppercase;
    color:var(--color-accent); pointer-events:none;
  }
  #edge .edge-cell-hint-icon{
    width:7px; height:7px; flex:none;
    border-right:2px solid var(--color-accent); border-bottom:2px solid var(--color-accent);
    transform:rotate(-90deg);
  }

/* Phones and tablets: the three cards stacked on the dark panel, each a
   fixed-height cell (the flip's faces are absolutely positioned, so the
   cell has to own its height), flipping text -> photo on tap or on the
   auto-sequence, exactly as on desktop. The four filler cells stay hidden. */
@media (max-width:1023px){
  /* the markup is in desktop-grid order (01, 03, 02); stacked, read 01-02-03 */
  #edge .edge-grid{ padding:0; display:flex; flex-direction:column; }
  #edge .edge-cell--02{ order:2; }
  #edge .edge-cell--03{ order:3; }
  #edge .edge-cell--content{
    display:block; position:relative; overflow:hidden; padding:0; cursor:pointer;
    perspective:1200px; aspect-ratio:5 / 4;
  }
  #edge .edge-cell-face--front{ display:flex; flex-direction:column; }
}
@media (min-width:640px) and (max-width:1023px){
  #edge .edge-cell--content{ aspect-ratio:2 / 1; }
}

@media (prefers-reduced-motion:reduce){
  #edge .edge-cell-flip{ transition:none; }
}

/* ── FOLD 6: COMPARISON TABLE — dull left, bright right ─────────────────────
   First real rules for .compare-table/.compare-row/.compare-answer — the
   markup existed on this page before but had no CSS behind it. Same "thin
   rule, not a filled block" divider language as .tabs and .flow-strip: a
   hairline under the head and under every row, single column below 800px,
   two beside each other above it.

   The emphasis the brief asked for ("left dull, right bright, that's
   Driif") is a colour swap only: the left column stays the section's own
   muted ink, the right column — head label and every row answer — sets
   in plain white, no tinted band, no border rule, no yellow at all. The
   contrast alone (white against the left column's own muted ink) is what
   reads as "the side that advances." */
#comparison .compare-table{ display:flex; flex-direction:column;
  margin-top:var(--space-7); border-top:var(--hair) solid var(--color-border-subtle); }
#comparison .compare-head{ display:grid; grid-template-columns:1fr; gap:var(--space-3);
  padding:var(--space-4) 0; border-bottom:var(--hair) solid var(--color-border-subtle); }
#comparison .compare-head span:last-child{ color:var(--color-accent); }
#comparison .compare-row{ display:grid; grid-template-columns:1fr;
  border-bottom:var(--hair) solid var(--color-border-subtle); }
#comparison .compare-row p{ margin:0; padding:var(--space-6) 0; }
#comparison .compare-answer{ color:var(--color-text); font-weight:500; }
/* PHONES (one column, below 800): the head row's two labels sat once at the
   top, so by the second row nothing said which line was which and it no
   longer read as a comparison (direct report). Each row now carries its
   own pair: the current approach under a muted label, then the VIPER answer
   under an accent label with an accent rule down its edge, so every row
   reads as before -> after on its own. The labels repeat the head's two
   strings (.compare-head, viper.html); keep them in step. */
@media (max-width:799px){
  #comparison .compare-head{ display:none; }
  #comparison .compare-row{ gap:var(--space-4); padding:var(--space-6) 0; }
  #comparison .compare-row p{ padding:0; }
  #comparison .compare-row p::before{
    display:block; margin-bottom:var(--space-2);
    font-family:var(--font-display); font-weight:500; font-size:var(--ui-xs);
    line-height:var(--lh-flat); letter-spacing:var(--track-label); text-transform:uppercase;
  }
  #comparison .compare-row p:first-child::before{ content:"The approach now"; color:var(--color-text-muted); }
  #comparison .compare-answer{ padding-left:var(--space-4) !important; border-left:2px solid var(--color-accent); }
  #comparison .compare-answer::before{ content:"VIPER Mission OS"; color:var(--color-accent); }
}
@media (min-width:800px){
  #comparison{ --cmp-gap:clamp(32px, 5vw, 80px); }
  #comparison .compare-head, #comparison .compare-row{
    grid-template-columns:minmax(0,1fr) minmax(0,1fr); column-gap:var(--cmp-gap);
  }
  #comparison .compare-row p:first-child{ padding-right:0; }
}
/* Column alignment with the intro above, and a much narrower gutter.
   The intro .split used to be the site-wide 11fr / 6fr pair with the full
   --gutter-lg between, and the table copied it, so the Driif column began
   ~400px to the right of where the left text actually ended — "too much gap
   between the two columns" (direct report, 2026-11-13). Both are now two
   equal columns with one modest shared gutter (--cmp-gap), so "The Approach
   Now" sits under the heading and "VIPER Mission OS" under the lead exactly
   as before, just closer. The eyebrow now sits above the split instead of
   inside the left cell, so the lead's top lines up with the heading's top
   rather than with the eyebrow. Both text boxes wrap at the same measure. */
@media (min-width:1024px){
  #comparison .split{ grid-template-columns:minmax(0,1fr) minmax(0,1fr); column-gap:var(--cmp-gap); }
  #comparison .eyebrow{ margin-bottom:var(--space-5); }
  #comparison .split > .t-lead{ max-width:44ch; }
  #comparison .compare-row p{ max-width:56ch; }
}

/* ── FOLD 7: EVERY PLATFORM — photo tiles, classes on hover / tap ─────────
   Replaced the hairline domain list (Oct 2026: it ran ~950px tall). Same
   panel as #edge's grid above: a bordered dark ground, hairline dividers,
   a fixed row height. Each tile is a photo, desaturated like the home
   page's product renders, under a flat scrim (rule 3), with its numeral
   and domain name top-left. Its platform classes fade up over a heavier
   scrim when the tile is hovered (fine pointers only) or opened by click,
   tap, Enter or Space (.is-open + aria-expanded, platform-tiles.js). The
   whole tile is its button. Four across from 1024, two below; the eighth
   cell holds the note. No JS: every list shows, over the heavy scrim. */
#platforms .pf-grid{
  --pf-row-h:232px;
  margin-top:var(--section-y-sm); border:var(--hair) solid var(--color-border);
  display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); grid-auto-rows:var(--pf-row-h);
}
@media (min-width:640px){ #platforms .pf-grid{ --pf-row-h:240px; } }
@media (min-width:1024px){ #platforms .pf-grid{ --pf-row-h:220px; grid-template-columns:repeat(4,minmax(0,1fr)); } }

/* dividers from position alone, same scheme as #edge's grid */
#platforms .pf-tile, #platforms .pf-note{
  position:relative; overflow:hidden;
  border-right:var(--hair) solid var(--color-border-subtle);
  border-bottom:var(--hair) solid var(--color-border-subtle);
}
#platforms .pf-grid > :nth-child(2n){ border-right:0; }
#platforms .pf-grid > :nth-child(n+7){ border-bottom:0; }
@media (min-width:1024px){
  #platforms .pf-grid > :nth-child(2n){ border-right:var(--hair) solid var(--color-border-subtle); }
  #platforms .pf-grid > :nth-child(4n){ border-right:0; }
  #platforms .pf-grid > :nth-child(n+5){ border-bottom:0; }
}

#platforms .pf-img{
  position:absolute; inset:0; width:100%; height:100%; object-fit:cover;
  filter:grayscale(1) contrast(1.06);
  transition:transform var(--dur-enter) var(--ease-out);
}
#platforms .pf-tile::before{
  content:''; position:absolute; inset:0; z-index:1; pointer-events:none;
  background:var(--scrim-45); transition:background-color var(--dur-ui) var(--ease-out);
}
/* the button is the whole tile: one target, one focus ring */
#platforms .pf-head{
  position:absolute; inset:0; z-index:3;
  display:flex; align-items:baseline; gap:var(--space-3); padding:var(--space-5);
  padding-right:calc(var(--space-5) + 12px + var(--space-3)); /* clear of the + mark */
  background:none; border:0; color:var(--color-text); text-align:left; cursor:pointer;
  text-shadow:0 1px 4px rgba(0,0,0,.85);
}
#platforms .pf-head:focus-visible{ outline:2px solid var(--color-accent); outline-offset:-4px; }
#platforms .pf-name{
  font-family:var(--font-display); font-weight:500;
  font-size:var(--text-lead); line-height:var(--lh-snug); letter-spacing:var(--track-tight);
}
#platforms .pf-list{
  position:absolute; left:0; right:0; bottom:0; z-index:2; margin:0; list-style:none;
  padding:var(--space-5); display:flex; flex-direction:column; gap:var(--space-2);
  font-family:var(--font-display); font-weight:500; font-size:var(--ui-xs);
  letter-spacing:var(--track-label); text-transform:uppercase; line-height:1.3;
  color:var(--color-text);
}
@media (max-width:639px){
  #platforms .pf-list{ gap:var(--space-1); padding:var(--space-4); }
  #platforms .pf-head{ padding:var(--space-4); padding-right:calc(var(--space-4) + 12px + var(--space-3)); }
  #platforms .pf-hint{ top:calc(var(--space-4) + 4px); right:var(--space-4); }
}
/* from 1024 the classes run in two columns, so the longest (Payloads,
   seven) still clears the name at a 220px row */
@media (min-width:1024px){
  #platforms .pf-list{ display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:var(--space-2) var(--space-4); }
}
/* open/close mark, top-right: a plus that turns to a cross when open */
#platforms .pf-hint{
  position:absolute; top:calc(var(--space-5) + 4px); right:var(--space-5); z-index:3;
  width:12px; height:12px; pointer-events:none;
  transition:transform var(--dur-ui) var(--ease-out);
}
#platforms .pf-hint::before, #platforms .pf-hint::after{
  content:''; position:absolute; left:0; top:5px; width:12px; height:2px; background:var(--color-accent);
}
#platforms .pf-hint::after{ transform:rotate(90deg); }
#platforms .pf-tile.is-open .pf-hint{ transform:rotate(45deg); }

#platforms .pf-note{ display:flex; align-items:flex-end; padding:var(--space-5); }
#platforms .pf-note p{ margin:0; line-height:1.4; }

/* PHONES AND TABLETS (below 1024): a swipeable strip instead of a cramped
   two-column grid (direct request, "mobile can be better"). Each domain is
   a large card, 85% of the screen with the next one peeking in, its name
   on top and its platform classes always shown over a fade at the bottom,
   since touch has no hover to reveal them. The note card closes the strip.
   The shared prev/next pair steps it (platform-tiles.js). */
.pf-controls{ display:none; }
@media (max-width:1023px){
  #platforms .pf-grid{
    display:flex; gap:var(--space-3); border:0; background:none;
    margin-inline:calc(var(--edge) * -1); padding-inline:var(--edge);
    overflow-x:auto; scroll-snap-type:x mandatory; scroll-padding-inline:var(--edge);
    overscroll-behavior-x:contain; scrollbar-width:none;
  }
  #platforms .pf-grid::-webkit-scrollbar{ display:none; }
  #platforms .pf-grid > *, #platforms .pf-grid > :nth-child(2n){
    flex:0 0 85%; aspect-ratio:4 / 5; scroll-snap-align:start;
    border:var(--hair) solid var(--color-border); background:var(--color-bg);
  }
  #platforms .pf-tile::before{ background:linear-gradient(180deg, var(--scrim-45), transparent 35%, var(--scrim-82) 72%); }
  .js #platforms .pf-grid .pf-list{ opacity:1; visibility:visible; transform:none; }
  #platforms .pf-list{ padding:var(--space-5); gap:var(--space-2); }
  #platforms .pf-head{ padding:var(--space-5); }
  #platforms .pf-hint{ display:none; }
  #platforms .pf-tile.is-open .pf-img{ transform:none; }
  .js .pf-controls{ display:flex; padding-inline:0; }
}
@media (min-width:640px) and (max-width:1023px){
  #platforms .pf-grid > *, #platforms .pf-grid > :nth-child(2n){ flex-basis:45%; }
}

/* without JS every list is simply shown */
html:not(.js) #platforms .pf-tile::before{ background:var(--scrim-82); }
/* with JS: hidden until hovered or opened */
.js #platforms .pf-list{
  opacity:0; visibility:hidden; transform:translateY(var(--space-3));
  transition:opacity var(--dur-ui) var(--ease-out), transform var(--dur-ui) var(--ease-out),
             visibility 0s linear var(--dur-ui);
}
.js #platforms .pf-tile.is-open::before{ background:var(--scrim-82); }
.js #platforms .pf-tile.is-open .pf-list{
  opacity:1; visibility:visible; transform:none;
  transition:opacity var(--dur-ui) var(--ease-out), transform var(--dur-ui) var(--ease-out);
}
.js #platforms .pf-tile.is-open .pf-img{ transform:scale(1.04); }
@media (hover:hover) and (pointer:fine){
  .js #platforms .pf-tile:hover::before{ background:var(--scrim-82); }
  .js #platforms .pf-tile:hover .pf-list{
    opacity:1; visibility:visible; transform:none;
    transition:opacity var(--dur-ui) var(--ease-out), transform var(--dur-ui) var(--ease-out);
  }
  .js #platforms .pf-tile:hover .pf-img{ transform:scale(1.04); }
}
@media (prefers-reduced-motion:reduce){
  #platforms .pf-img, #platforms .pf-hint{ transition:none; }
  .js #platforms .pf-list{ transform:none; }
  .js #platforms .pf-tile:hover .pf-img, .js #platforms .pf-tile.is-open .pf-img{ transform:none; }
}
