/* ==========================================================================
   FRONTGRADE  ·  HOMEPAGE
   Layout and page-specific treatments only. Everything shared lives in
   tokens.css and components.css; nothing here forks a kit component.

   CONTRAST NOTATION
   Ratios follow the same method as tokens.css: sRGB relative luminance, alpha
   composited against the surface first, then measured. Any colour pair
   introduced by this file that is not already ratio-commented in the kit
   carries its computed ratio in the comment above the rule.
   ========================================================================== */

/* Page icon sprite host. Symbols only, nothing renders here. */
.fg-sprite { position: absolute; width: 0; height: 0; overflow: hidden; }

/* ==========================================================================
   1. HERO  ·  ONE MISSION SCENE

   ONE STAGE, AND IT IS THE 16/9 BOX ITSELF.
   .fg-hero__stage is the only aspect-locked element on the page. The plate,
   the four platform artboards, the connection arcs, the four buttons and their
   callouts are all descendants of it, so a percentage of the stage is a
   percentage of the bitmap for every one of them by construction. There is one
   geometry table, no twin boxes to keep in step, and overlay tracking is
   structural rather than arithmetic.

   THE STAGE IS SIZED BY A CONSTANT, NEVER BY THE HERO AND NEVER BY THE COPY.
   At 1440 and up the artwork holds ONE size on screen, --stage-static, and the
   window moves across it:
     stage width  = max(--stage-static, hero width)
     stage height = stage width / --plate-ar
   The constant is 2560px, which is exactly 0.5 of the delivered frame's own
   5120, so one frame pixel is half a CSS pixel and the stage is 2560 x 810 to
   the pixel. Past 2560px of window the stage takes the window's width instead,
   which is the no-gap-on-the-right guard doing its job rather than a second
   sizing policy: the artwork is never narrower than the hero.
   BELOW 1440 THE CONSTANT GIVES WAY TO A MULTIPLE OF THE HERO, because a
   constant that does not fit is not a size, it is a crop. Section 6 of
   cinema.css draws the stage at --stage-fit x the hero's own width there, so
   the whole composition scales with the window and every object stays in it;
   the anchor, the bottom edge and every percentage in the geometry table are
   untouched by the switch, since all of them are expressed against the stage.
   The stage is anchored to the hero's BOTTOM edge, which pins the horizon, the
   sea, the ship (frame y 63.1 to 96.2 percent) and the ground terminal (61.0
   to 95.9) to the bottom of the box at every size. Two things follow from a
   constant scale, and both are intended:
     to the right   the frame's right edge always sits past the viewport's, by
                    a flat 399px below 2560 and by 17.1 percent of the window
                    above it, so the scenery added to the right of the
                    satellite crops off. Nothing in the composition is in it.
     above          the stage is shorter than the hero wherever the hero is
                    taller than stage width / --plate-ar, so a strip of hero
                    ground shows above the artwork. The strip is painted the
                    plate's own top edge and faded into the hero's ground over
                    the 160px above the join; see the sky strip rule.
   TWO HERO BEHAVIOURS, AND THE BREAKPOINT BETWEEN THEM IS 1440. At 1440 and up
   the hero is the full-bleed overlay described above. Below it the hero is a
   scroll catch: one viewport of pin, the copy alone on a dark field, then one
   continuous left-to-right pan across the frame that lands on the composition,
   then release into the page. Section 1c builds it. Its stage is
   bottom-anchored in the same way and reads the same descriptor; what differs
   is which band of the frame fills the width, that the surplus above the
   artwork is sky rather than hero ground, and that the stage moves.
   THE FALLBACK IS THE THIRD LAYOUT AND IT IS THE PLAIN ONE. With no script, or
   with reduced motion asked for, nothing pins: the stage comes back into flow
   at the container's own width, sized so the original painting lands exactly on
   the container's content box, under the copy. Every percentage in the geometry
   table still measures the same bitmap in all three.

   LAYER ORDER. The hero is a stacking context (isolation: isolate). The stage
   is z-index auto, so it creates no context of its own and everything inside
   it competes on the hero's single ladder. That is what lets the text scrim
   sit BETWEEN the artwork and a revealed part:
     .fg-hero (background)         the surface and its one radial lift
     .fg-hero::before          0   hex lattice
     .fg-hero__stage::before   0   the sky strip over the top gap
     .fg-hero__plate           1   the scene plate
     .fg-scene-node__layer   2-5   the four platform artboards, stack order
     .fg-scene__wire           6   the four connection arcs and their signals
     .fg-hero__scrim           7   the text scrim, over all of the artwork
     .fg-scene-node__part      8   a part, over the scrim, drawn or not
     .fg-scene-node__leader    9   callouts, over the scrim, so the ratios
     .fg-scene-node__tag       9   measured on their own backing still hold
     .fg-hero__fade           10   bottom blend into the next band
     .fg-hero__body           11   the copy
   TWO ITEMS SHARE LEVEL 0 AND TREE ORDER SETTLES THEM. The lattice is the
   hero's own ::before and the sky strip is a ::before on the stage, which is a
   later child of the same stacking context, so the strip paints over the
   lattice and both stay under the plate at 1. That is what keeps the lattice
   out of the strip, where 4 percent of #7d89a3 over a near-black sky would put
   a 6-level step along the join the strip exists to remove.
   NOTHING IN THE HERO CHANGES z-index IN RESPONSE TO A POINTER. A platform
   sits under the scrim at rest and stays under it while it is revealed, so its
   opacity fade starts from exactly the appearance it already had and there is
   no brightness step at hover onset. The part sits at 8 whether it is drawn or
   not, which is what puts a revealed part over the scrim at full strength
   without moving anything else.

   ONE GRADIENT OVER THE ARTWORK, AND IT IS VIEWPORT-ANCHORED. .fg-hero__scrim
   is solid ink from the viewport's left edge, full height, continuously
   through the copy column, and then one long eased release into the scene. It
   is the only thing dimming the artwork, and there is deliberately no second,
   scene-anchored wash inside the stage: two gradients on different anchors put
   a second edge in the release band, one that moves with the crop while the
   other does not. The wall's own plateau already covers everything such a wash
   would deepen. The derivation, the stop table and
   the measured ratios are on the rule.

   POINTER-EVENTS DISCIPLINE, and it is load bearing. The stage spans the hero
   and the copy paints on top of it, so left alone one would swallow the
   other's clicks. Both .fg-hero__stage and .fg-hero__body are pointer-events:
   none, and exactly two things switch them back on: the copy's two buttons and
   the four .fg-scene-node__hit boxes. The node buttons themselves do not,
   because each one spans the whole stage; the hit box inside it is the target
   and a click on it activates the button by bubbling. In the scroll catch the
   copy's two buttons stand down once the copy has faded, so a control nobody
   can see cannot take a tap meant for the scene behind it.
   ========================================================================== */

/* THE HERO'S OWN BOX. In the fallback it is an ordinary block: the copy sets
   the height, the stage follows in flow, and the padding clears the header. At
   1440 and up it declares a height and the copy has no say in it; see the
   overlay block. In the scroll catch it is exactly one viewport tall and
   sticky; see section 1c.
   --hero-header TRACKS THE HEADER'S OWN BREAKPOINT rather than the hero's, and
   that is why the two numbers differ. components.css switches to the compact
   header at max-width: 1024px, so the offset switches at exactly min-width:
   1025px and the two can never disagree about how much room the fixed header
   needs. The hero's own overlay switch is at 1440 and has nothing to do with
   it; between 1025 and 1439 the layout is stacked under the full-height
   header, which is the combination that band is checked at. */
.fg-hero {
  --hero-header: var(--header-offset-mobile);

  /* THE FRAME DESCRIPTOR. The delivered artwork is a 5120 x 1620 frame: the
     original 2880 x 1620 painting unchanged, with new scenery added on both
     sides. The stage IS that frame, the plate and the four platform artboards
     are all 5120 x 1620 and lie inset: 0 over it, and every percentage in the
     geometry table is a percentage of it.
       --plate-ar   the frame's aspect, 5120 / 1620
       --comp-x     the original painting's left edge inside the frame, as a
                    fraction of the frame width
       --comp-w     the original painting's width, same fraction
       --sat-rx     the SATELLITE's right edge, same fraction, which is the
                    edge the stage is anchored by: 4242 / 5120, straight off
                    the satellite layer's own alpha bounding box in the node
                    table below. The anchor is an object rather than the
                    painting's border because an object is what a viewer reads
                    the placement against; the painting's own right edge is
                    scenery for most of its last 5 percent.
       --ship-lx    the SHIP's left edge, same fraction, 2099 / 5120, off the
                    same table. It is the left end of the composition, so with
                    --sat-rx it states the span the scroll catch crops to
       --sat-ty     the SATELLITE's top edge as a fraction of the frame HEIGHT,
                    345 / 1620. It is the highest point of the composition and
                    so the one the scroll catch's height cap is solved against
       --frame-pad  how far that edge stops short of the viewport's right
                    edge, the one number the placement exposes for tuning
     --comp-x IS MEASURED, NOT ASSUMED. The original plate was located inside
     the delivered frame by correlation: the old file was slid across the new
     one and the sum of absolute channel differences taken over a 14,950-point
     sample grid at every integer offset. The minimum is at x 1608, y 0, and it
     is sharp: 0.66 mean absolute difference per channel there against 11.1 and
     11.5 at plus and minus one pixel. Correlating the old plate's left and
     right halves independently returns 1608 for both, which is what rules out
     a rescale, and 98.07 percent of the sampled pixels match within 2 of 255
     on every channel. 1608 / 5120 = 0.31406.
     THE COMPOSITION SUB-BOX IS STILL 16/9, which the descriptor states rather
     than assumes: --comp-w x --plate-ar = 0.5625 x 3.160494 = 1.777778. One
     rule uses it, the stacked layout; the overlay anchors on --sat-rx and
     everything else is written in frame percentages directly.
     --stage-static IS THE ONE SIZE THE ARTWORK IS DRAWN AT on the overlay. It
     is derived on the stage rule rather than guessed at here, and it lives in
     the descriptor because it is a property of the delivered frame rather than
     of the layout: 2560 is 0.5 x 5120 exactly, so it is the scale factor the
     frame is drawn at written as a width.
     --stage-fit IS THE SAME IDEA FOR A WINDOW THE CONSTANT WILL NOT FIT IN, and
     it is a multiple rather than a width because that is what the thing it is
     protecting is. Below 1440 the frame is drawn at this multiple of the hero's
     own width, so the artwork scales with the window instead of holding a size
     the window has no room for. Where the two figures come from is on the stage
     rules that use them: 0.5 on the overlay block below, 1.2 in section 6 of
     cinema.css, which is the only place the second one is read. */
  --plate-ar: 3.160494;                /* 5120 / 1620 */
  --comp-x: 0.31406;                   /* 1608 / 5120, measured */
  --comp-w: 0.5625;                    /* 2880 / 5120 */
  --sat-rx: 0.828516;                  /* 4242 / 5120, the satellite's right edge */
  --ship-lx: 0.409961;                 /* 2099 / 5120, the ship's left edge */
  --sat-ty: 0.212963;                  /*  345 / 1620, the satellite's top edge */
  --stage-static: 2560px;              /* 0.5 x 5120; see the stage rule */
  --stage-fit: 1.2;                    /* below 1440; see section 6 of cinema.css */
  --frame-pad: 40px;

  /* THE HEADLINE SIZE, declared here because two rules need the same number:
     the headline itself and the copy budget below. See the note on
     .fg-hero__title for the derivation of the two legs.

     THE CEILING IS ITS OWN PROPERTY, AND IT IS THE ONE KNOB THE OVERLAY TURNS.
     The copy sits ON the picture from 1440 up, so there the headline's size is
     also the width of the wall that hides the scene, and the size the scene
     needs is smaller than the display token's own 100px ceiling. Writing that
     smaller ceiling into the overlay block alone made the scale run BACKWARDS:
     93.3px at 1280, where the token still governed, against 68px at 1920, so
     the widest window on the site carried its smallest display headline and the
     homepage sat under every interior page's 104px.
     SO THE CEILING IS DECLARED ONCE, HERE, AND APPLIES AT EVERY WIDTH. The ramp
     below it is untouched, which is the whole of the range under about 687px of
     viewport, where the steeper hero-local leg is already the smaller of the
     two; from there up the headline is flat at the ceiling. The curve is
     therefore monotonically non-decreasing in viewport width by construction:
     it rises to the ceiling and stays there, and there is no width at which
     growing the window shrinks the type.
     WHERE THE CEILING SITS IS A DESIGN DECISION AND THIS IS WHERE IT IS MADE.
     4.25rem is the figure the scene is solved against; the column that follows
     it is 6.6 times it, stated on --copy-w in the overlay block because a
     calc() in a custom property is not resolved to a length and the script that
     reads the column would take the token stream instead. Moving the ceiling is
     therefore those two declarations and nothing else.

     THE EXPRESSION NO LONGER READS A SITEWIDE TYPE TOKEN, AND THAT IS THE
     POINT OF THIS LINE RATHER THAN A RESIZE. It used to be min() of
     --fs-display and the ramp below. That min() could never bind. --fs-display
     is clamp(3.5rem, 2.479rem + 4.19vw, 6.25rem): its floor is 56px and its
     slope is shallower than this ramp's, while this ramp is floored at 40px
     and ceilinged at --hero-fs-max, so the local leg is the smaller of the two
     at every viewport width there is. The two curves would have crossed at
     697px of viewport, and the ceiling takes this leg flat at 687px, so the
     crossing is never reached and the token leg was inert arithmetic.
     Inert, but not harmless: it tied this headline, and through the em measure
     that sets --copy-w and the scrim that is written off --copy-w, the whole
     scene's clearance over the vessel, to a token whose job is page headlines
     on the other families. A sitewide type decision taken for the mission and
     capability pages could therefore reach into this composition, which is a
     coupling the hero should not have and did not need. Dropping the term
     changes no computed value at any width and leaves the homepage headline
     resolved from the two literals on the next lines and nothing else. */
  --hero-fs-max: 4.25rem;
  --hero-fs: clamp(2.5rem, 0.6rem + 8.5vw, var(--hero-fs-max));

  /* THE COPY BUDGET. The copy column's height, DECLARED rather than measured:
     four lines of the headline plus 242px for the eyebrow, the sub, the
     actions and the gaps between them. The 242px is read off the utility type
     scale rather than chosen: at the 16px body setting the non-headline stack
     measures 200.6px at 1024 and 208.8px from about 1490 up, where the two
     fluid gaps above the sub and the actions have both topped out, and the
     constant is the larger of those plus 33px of slack. Measured against it,
     the real column is 531px at a 82.57px headline (41px of slack) and 579px at
     93.30px (36px), which is enough that a fallback face setting a line or two
     looser still fits. The hero reserves this plus the header allowance, and
     the check matrix asserts the real column fits.
     THE BUDGET IS A FLOOR AND THE STACK UNDER IT HAS SINCE GOT SHORTER. The
     242px was read at a three-line sub; the sub is one line now, so the same
     constant carries the same four headline lines with two lines more slack
     than it was solved for. A floor that is generous is the safe direction: it
     can only reserve height the copy does not need, and size containment means
     the failure it exists to stop is the opposite one.
     --copy-w IS THE COPY COLUMN'S WIDTH, and 660 is the figure for every layout
     in which the copy is NOT over the picture. The overlay block brings it down
     to the width the scene needs; see the note there. */
  --copy-w: 660px;
  --copy-h: calc(4 * var(--hero-fs) + 242px);
  --copy-budget: calc(var(--hero-header) + var(--copy-h));

  /* THE HERO'S DECLARED HEIGHT, used at 1440 and up. THE STAGE NO LONGER READS
     IT. Under the constant scale the artwork's size is --stage-static and the
     hero's height decides only how much of the box the artwork does not fill,
     so this expression sets the height of the dark field the copy sits in and
     nothing about the composition. Its three legs are untouched and so are the
     figures they produce; what changed is which of them can still bind.
       clamp(560px, 88svh, 850px)   the vertical design: a share of the window,
                                    floored so a very short window still gets a
                                    hero and capped so a very tall one does not
                                    open the top strip further than the strip
                                    treatment covers. At the cap the strip is
                                    202px against a 160px fade, so 42px of the
                                    hero's own ground shows above the fade,
                                    which is the intended top of the band.
       --copy-budget                the floor the copy needs. Size containment
                                    means an overflowing column would be
                                    clipped rather than grow the box, so the
                                    budget is taken as a floor. It binds only
                                    on a short window, below about 820px of
                                    viewport height at the 100px headline.
       21.3vw + 130px               the CROP BOUND, and on the 5120 frame it is
                                    a small correction rather than a lever. The
                                    stage is bottom-anchored, so its surplus
                                    height is cropped off the top and the
                                    satellite, whose measured centre sits at
                                    frame y 32.75 percent, is the first control
                                    to go. Keeping that centre at least 130px
                                    below the hero's top edge, clear of the
                                    92px header, needs hero height >= 0.6725 x
                                    stage height + 130, and stage height is
                                    viewport width / 3.160494 whenever the
                                    stage is viewport-wide: 21.28vw + 130px.
                                    UNDER THE CONSTANT SCALE IT ONLY BINDS PAST
                                    2560, because that is the only place the
                                    stage is viewport-wide and so the only
                                    place this expression describes the stage
                                    at all. Below 2560 the stage is a flat
                                    810px tall, the same guarantee reads hero
                                    height >= 675px, and the copy budget's
                                    734px still clears it, now by 59px rather
                                    than by 168. Above 2560 the term is
                                    unchanged and still asks for 862px against
                                    the 792px the clamp gives, a 9 percent lift
                                    and only past about 3100px of viewport
                                    width.
     Between them the clamp governs at every width the check matrix reports up
     to 3100, so the height is a function of viewport height alone there, which
     is what the matrix asserts. */
  --hero-h: max(clamp(560px, 88svh, 850px), var(--copy-budget), calc(21.3vw + 130px));
  position: relative;
  isolation: isolate;
  overflow: hidden;
  padding-top: calc(var(--hero-header) + clamp(24px, 1rem + 2.2vw, 56px));
  padding-bottom: clamp(24px, 1rem + 2vw, 56px);
  background:
    radial-gradient(120% 96% at 72% 38%,
      var(--surface-raised) 0%,
      rgba(14, 18, 32, 0) 62%),
    var(--surface-base);
}

@media (min-width: 1025px) {
  .fg-hero { --hero-header: var(--header-offset); }
}

/* Hex lattice, the kit motif at its own 4 percent. Masked from the right: the
   pattern is texture for the composition side, and behind display type it
   would be noise. The radial lift above it tops out at exactly
   --surface-raised, one step of the existing surface ramp rather than a new
   colour, and reaches its transparent stop at 62 percent of the box, so under
   the copy the ground has already resolved to plain --surface-base and every
   contrast figure the kit measured there holds verbatim. Static, so it needs
   no motion guard. */
.fg-hero::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='34.64' height='60' viewBox='0 0 34.64 60'%3E%3Cpath d='M17.32 0 L0 10 L0 30 L17.32 40 L34.64 30 L34.64 10 Z M17.32 40 L17.32 60' fill='none' stroke='%237d89a3' stroke-width='1'/%3E%3C/svg%3E");
  background-size: 34.64px 60px;
  opacity: 0.04;
  -webkit-mask-image: linear-gradient(90deg, transparent 0%, transparent 38%, #000 72%, #000 100%);
  mask-image: linear-gradient(90deg, transparent 0%, transparent 38%, #000 72%, #000 100%);
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
}

/* THE COPY BLOCK IS POINTER-TRANSPARENT AND ONLY ITS BUTTONS TAKE THE POINTER.
   The body spans the hero at z12, above the whole scene, so an auto block here
   would be an invisible lid over the middle of the composition and would
   swallow the top half of the ship's hit region. Naming the controls instead
   costs the headline and the sub their text selection, which is the cheaper of
   the two: nothing in the hero's copy is interactive except the two buttons,
   and every platform keeps its whole measured silhouette as a target. */
.fg-hero__body {
  position: relative;
  z-index: 11;
  width: 100%;
  pointer-events: none;
}
.fg-hero__copy .fg-btn { pointer-events: auto; }

/* THE HEADLINE IS SET FROM THE HERO'S OWN RAMP AND FROM NOTHING SITEWIDE.
   --hero-fs is a single clamp declared at the top of this section: a 40px
   floor, a 8.5vw slope and the ruled --hero-fs-max ceiling. It used to be
   min() of that ramp and the --fs-display token, and the note on --hero-fs
   carries why the token leg was both inert and worth removing. What the ramp
   itself is for has not changed. At 320px, WCAG reflow's narrow end, the
   column is 280px and the display token's own 56px floor would set lines wider
   than that; here the headline is 40px and every line fits. And on a 390px
   handset the hero has to stay near one screen, where the same ramp takes the
   headline from five lines to four. tokens.css is untouched either way: every
   figure here is scoped to this element.
   THE MEASURE IS IN em SO IT RIDES THE TYPE. 900px is the stacked-mode
   measure, where the copy runs the full container; the overlay block replaces
   it with the em figure that holds the four-line wrap. */
.fg-hero__title {
  font-size: var(--hero-fs);
  max-width: min(100%, 900px);
}

/* THE HIGHLIGHTED COMPOUND STAYS WHOLE, and the floor above is what makes that
   safe rather than a reflow hazard. MISSION-READY measures 5.345em, so it fits
   the column whenever the column is at least 5.345 times the type size:
     viewport   type      compound   column   fits
     320        40.00px   213.8px    280px    yes, 66px spare
     390        42.75px   228.5px    350px    yes
     698        68.90px   368.3px    650px    yes, the token takes over here
     768        71.84px   384.0px    720px    yes
     1024       82.57px   441.3px    900px    yes, the stacked 900px cap here
     1439       99.96px   534.3px    900px    yes
     1440+      capped by the 6.6em measure, and 5.345em is inside it
   Without the floor the token's own 56px minimum puts the compound at 299px in
   a 280px column at 320, which is the WCAG 1.4.10 reflow failure this pair of
   rules exists to remove. Keeping the two together is the reason the wrap
   table further down is a step function of the measure at all: break the
   compound and the headline has a different number of lines at the same
   measure. */
.fg-hero__title .fg-accent-word { white-space: nowrap; }

/* THE SUB'S MEASURE IS A CHARACTER COUNT, NOT A LAYOUT WIDTH, so it is
   re-derived whenever the body token moves rather than left at its old pixel
   figure. 470px held 33.57em of the 14px setting the token used to carry, and
   33.57em of the 16px setting is 537px, so the line length in characters is
   the same before and after the change and the sub still sets three lines
   everywhere the measure binds. It stays in pixels rather than em to match the
   measure caps in tokens.css, which are written the same way. */
.fg-hero__sub {
  margin-top: clamp(20px, 1rem + 1vw, 28px);
  max-width: 537px;
  font-size: var(--fs-body);
  line-height: var(--lh-lede);
  color: var(--text-muted);            /* 5.57:1 on --surface-base */
}

.fg-hero__actions { margin-top: clamp(24px, 1.2rem + 1.4vw, 40px); }

/* THE STAGE, in flow. This is the FALLBACK layout and the base the other two
   override: a plain 16/9 box at the container's own content width, under the
   copy. Container geometry by hand rather than by wrapping the stage
   in a .fg-container, because a wrapper's padding would become part of the
   stage's containing block and every percentage in the geometry table is a
   percentage of that block. The gutter is subtracted from the width instead,
   which lands the box on exactly the container's content width and leaves the
   percentages measuring the bitmap.
   POSITIONED, NOT STATIC. The nodes are absolutely positioned, so a static
   stage would hand their containing block up to .fg-hero and every percentage
   would suddenly measure the whole hero. z-index stays auto so the stage
   creates no stacking context; see the layer table above.
   THE STAGE IS THE WHOLE FRAME, SO IN FLOW IT IS SIZED BY ITS COMPOSITION
   SUB-BOX. A 3.16:1 strip at the container's width would be a 418px letterbox;
   what the stacked layout wants is the original 16/9 painting filling the
   container with the frame's new scenery bleeding out either side. So the
   stage is sized so that its sub-box lands exactly on the container's content
   box: width = containerContent / --comp-w, pulled left by --comp-x of its own
   width. The hero's overflow: hidden takes the overhang, and because the
   overflow is clipped by an ancestor it never reaches the document's own
   scroll width.
   --stage-px IS THE SAME WIDTH AGAIN, WRITTEN WITHOUT A PERCENTAGE, and it has
   exactly one consumer: the leader dash, which is measured in screen pixels
   and so cannot use a percentage of anything. Here it is exact. In the overlay
   block it is max(--stage-static, 100vw) against the box's own
   max(--stage-static, 100%): the two agree exactly below 2560, where the
   constant is the larger term on both sides and no viewport unit is read at
   all, and above it they differ by the width of a classic scrollbar. The dash
   is a shade long as a result, which is the harmless direction. */
.fg-hero__stage {
  --comp-px: min(var(--container-max), calc(100vw - (var(--gutter) * 2)));
  --stage-px: calc(var(--comp-px) / var(--comp-w));
  position: relative;
  z-index: auto;
  width: var(--stage-px);
  max-width: none;
  margin-left: calc(var(--gutter) - (var(--stage-px) * var(--comp-x)));
  margin-right: 0;
  margin-top: clamp(24px, 3vw, 48px);
  aspect-ratio: var(--plate-ar);
  pointer-events: none;
}

/* THE PLATE. A real <img> inside a <picture>, which buys three things over the
   background-image it replaces: a responsive source set can be added later
   without restructuring anything, the download gate is declarative and lives
   next to the source rather than in a media query, and the browser's own
   preload scanner can see it.
   The stage IS the delivered frame, so the plate lies inset: 0 over it and the
   two aspects agree by construction: the box is --plate-ar and so is the file,
   which is what lets the default fill sizing carry it without distortion. */
.fg-hero__plate {
  position: absolute;
  inset: 0;
  z-index: 1;
  display: block;
  pointer-events: none;
  animation: fg-scene-in var(--dur-reveal) var(--ease-entrance) backwards;
}
.fg-hero__plate img { display: block; width: 100%; height: 100%; }

@keyframes fg-scene-in {
  from { opacity: 0; }
}

/* Entrance motion, so the pause control is deliberately given no rule: pausing
   stops loops that run on their own. Reduced motion does drop it, and dropping
   it leaves the plate at its visible base state. */
html[data-motion="reduced"] .fg-hero__plate { animation: none; }

@media (prefers-reduced-motion: reduce) {
  html .fg-hero__plate,
  html[data-motion] .fg-hero__plate { animation: none; }
}

/* The scrim only exists where the copy sits ON the scene at the same height as
   the composition. In the fallback the copy is above the stage in flow, and in
   the scroll catch the frame is not in the window at all while the copy is up;
   the measured ratios are on the scroll catch's copy rule. Neither needs ink. */
.fg-hero__scrim { display: none; }

/* THE BOTTOM BLEND, ABOVE THE ARTWORK. It is the join between the hero and the
   next band, so it has to be over everything the hero paints; at any level
   below the artboards a platform standing on the ground shelf paints straight
   through it and the join reappears. 48px is the collision rule the callouts
   are checked against, restated as a length: nothing may sit below 86 percent
   of the stage, and no tag anchor in the geometry table is below 72 percent. */
.fg-hero__fade {
  position: absolute;
  z-index: 10;
  left: 0; right: 0; bottom: 0;
  height: var(--space-9);
  pointer-events: none;
  background: linear-gradient(180deg, rgba(9, 12, 20, 0) 0%, var(--surface-base) 100%);
}

/* THE SKY RAMP, AND IT IS THE PLATE'S OWN TOP EDGE RESAMPLED AS A GRADIENT.
   Wherever the stage is shorter than the box it sits in, the join between the
   artwork's top edge and whatever is above it is a horizontal line across the
   page. The overlay's strip extends the sky upward with this ramp, which is
   why it is authored here at section scope rather than on the rule.
   THE SCROLL CATCH SAMPLES ITS OWN LINE AND SO CANNOT READ THIS ONE. Its strip
   starts at the crop line rather than at the frame's top edge, so it carries a
   second set of 33 stops measured at that row by the identical method. The two
   ramps are the same technique at two different heights of the same file, and
   each is authored where its one consumer is.
   WHY A RAMP AND NOT ONE COLOUR. The plate's top rows are not flat. Over the
   top 8 rows the whole-frame mean is 1.9 / 2.7 / 4.2 of 255, but 160px windows
   across the frame run from 0.2 / 0.6 / 0.9 at frame x 22 percent to
   5.9 / 9.7 / 17.3 at 69 percent, where a glow off the Earth limb reaches the
   top of the file. A single colour matched to the mean would sit 13 steps of
   blue under the plate at that one place and 3 over it at the darkest, which is
   a soft horizontal edge exactly where the eye is already following the
   satellite. So the ramp carries the same variation the plate does: 33 stops at
   a 160-frame-pixel pitch, each the measured mean of the plate's top 8 rows
   over a 160px window centred on that stop, rounded to the nearest step. Worst
   rounding error over the 33 is 0.5 of 255, so the ramp and the plate meet
   within half a step at every stop and within the ramp's own linear
   interpolation between them.
   THE STOPS ARE IN FRAME SPACE, WHICH IS WHY ONE RAMP SERVES EVERY WIDTH. It
   is painted by a ::before on the STAGE with left: 0 and right: 0, so 0 to 100
   percent maps onto frame x 0 to 5120 by construction and follows the artwork
   whatever the stage's size or placement is doing.
   STARS ARE DELIBERATELY NOT REPRODUCED. The measured means include them, which
   is what makes the AVERAGE level agree; the ramp has no points of its own, so
   the field simply stops having stars above the join, which is what a sky does
   at the top of a frame. */
.fg-hero {
  --sky-ramp: linear-gradient(90deg,
    rgb(2, 3, 3) 0%,
    rgb(1, 2, 1) 3.125%,
    rgb(1, 2, 1) 6.25%,
    rgb(1, 2, 1) 9.375%,
    rgb(1, 2, 2) 12.5%,
    rgb(1, 1, 2) 15.625%,
    rgb(1, 1, 2) 18.75%,
    rgb(1, 1, 2) 21.875%,
    rgb(0, 1, 1) 25%,
    rgb(1, 1, 2) 28.125%,
    rgb(1, 1, 2) 31.25%,
    rgb(1, 2, 4) 34.375%,
    rgb(2, 2, 4) 37.5%,
    rgb(2, 4, 8) 40.625%,
    rgb(3, 4, 8) 43.75%,
    rgb(2, 3, 5) 46.875%,
    rgb(1, 2, 4) 50%,
    rgb(2, 2, 4) 53.125%,
    rgb(3, 4, 7) 56.25%,
    rgb(3, 3, 4) 59.375%,
    rgb(4, 4, 6) 62.5%,
    rgb(4, 5, 7) 65.625%,
    rgb(6, 10, 17) 68.75%,
    rgb(5, 7, 14) 71.875%,
    rgb(3, 4, 7) 75%,
    rgb(3, 4, 5) 78.125%,
    rgb(3, 4, 4) 81.25%,
    rgb(1, 2, 2) 84.375%,
    rgb(2, 2, 2) 87.5%,
    rgb(1, 2, 2) 90.625%,
    rgb(1, 2, 2) 93.75%,
    rgb(1, 2, 1) 96.875%,
    rgb(1, 2, 2) 100%);
}

/* --------------------------------------------------------------------------
   OVERLAY, 1440px and up. The copy is a two-column grid inside the site's own
   container, left column copy and right column the region the scene shows
   through, and the stage is a constant-scale plate behind all of it.
   WHY 1440 AND NOT THE KIT'S 1024. The overlay only works while the copy
   column and the composition can both have their own half of the window. The
   copy column is 450px here and the composition the copy has to clear runs from
   the ship's stern at frame 41.0 percent to the satellite's right edge at 82.9,
   which at the constant scale is 1071px of artwork that has to sit right of the
   scrim's wall. At 1440 the wall ends at 474 and the vessel's own span is 328.5
   to 709.5, so 145.5px of the hull is still under it; at 1728 and above the
   wall clears the whole object. Every width below 1440 pushes the vessel
   further under the wall, so the band that used to run 1024 to 1439 on the
   overlay is the stacked treatment instead, where the scene gets the
   container's full width and no scrim at all. --container-max is 1440, so the
   switch also lands exactly where the copy's own container stops growing.
   THE KIT'S 1024 IS UNTOUCHED. components.css still switches the header there
   and --hero-header still tracks it at 1025; the hero's overlay switch is a
   second, later breakpoint and the two are deliberately independent.
   Resizing across the whole range is continuous: the stage's width is a max()
   of two continuous functions and the copy's column is the container's, so
   there is no width at which the composition jumps.
   -------------------------------------------------------------------------- */
@media (min-width: 1440px) {

  /* THE HERO'S HEIGHT COMES FROM THE VIEWPORT AND NOTHING ELSE, and it is
     --hero-h, derived at the top of this section.
     container-type: size is what makes that a guarantee rather than an
     observation: size containment computes the hero's own size as if it had no
     contents, so the copy cannot lengthen it however the type falls. The floor
     of --hero-h is set above the copy's tallest possible setting with margin,
     and that margin is asserted per width rather than argued, because with
     containment an overflowing copy would be clipped instead of growing the
     box. */
  .fg-hero {
    /* THE COLUMN IS NARROWER ON THE OVERLAY THAN ANYWHERE ELSE, AND THE TYPE IS
       WHAT MAKES IT NARROW. The overlay is the only layout where the copy sits
       ON the picture, so it is the only one where the column's width is also
       the width of the wall that hides the picture. The wall ends at the
       column's right edge by construction, the vessel's own span is fixed by
       the anchor, and at the shipped 660px column the two overlapped so far
       that the object measured full ink across its whole width at every width
       the overlay runs at.
       THE MEASURE IS UNTOUCHED, SO THE WRAP IS UNTOUCHED. The headline's
       max-width is 6.6em and the wrap is a step function of that em figure
       alone, so bringing the type down moves the column and the wall without
       moving a single line break: the same lines set at a smaller size. The
       ceiling is --hero-fs-max at the top of this section, 4.25rem, and 6.6em
       of it is 448.8, so the column is 450. Everything below 1440 keeps the
       660px column, because there the copy is not over the picture and there is
       nothing for a narrower column to uncover.
       THE SIZE IS SET ONCE AND IT IS NOT SET HERE. The ceiling used to be
       written on this rule, which made the type smaller at 1440 than at 1280
       and gave the site's widest window its smallest display headline. It is
       --hero-fs-max now and applies at every width, so this block declares the
       column alone; the note on --hero-fs-max carries the reasoning.
       WHY 68 AND NOT LESS. The vessel spans 381px on screen and its stern sits
       at viewport width less 1111.5, so the wall clears the whole object only
       where 24 + column <= that figure, which no column a display headline can
       live in reaches at 1440. 68px is the size at which the object reads whole
       at 1728 and at 1920, which is the width the defect was reported at:
       measured, it puts 0px of the hull under full ink at both, 4.5 percent of
       it over half ink at 1920 and 229px of bow in clear air. Below that the
       return per pixel of type falls away, at 1440 fastest of all, where the
       column stands on the vessel whatever it is set at: 145.5px of hull under
       the wall there and 73.5 at 1536, which no column a headline can live in
       removes. WHAT MOVING THE CEILING COSTS, in pixels of hull under full ink:
         ceiling   column   1440    1536    1728    1920
         68px       450     145.5    73.5     0       0
         76px       502     197.5   125.5    29.5     0
         86px       568     263.5   191.5    95.5     0
         100px      660     355.5   283.5   187.5    91.5
       The vessel is 381px wide at every one of them, because the anchor fixes
       its span and only the wall moves. */
    --copy-w: 450px;
    container-type: size;
    display: flex;
    align-items: center;
    min-height: var(--hero-h);
    padding-block: 0;
  }

  /* The vertical padding moves off the hero and onto the copy. The stage is
     anchored to the hero's BORDER box, so leaving vertical padding on the hero
     would make the anchored box and the measured box two different rectangles
     and the seam at the top of the artwork would move with the padding.
     Carrying the header allowance here instead keeps the copy clear of the
     fixed header while the flex centring still works on the block as a whole:
     the copy's top lands at (hero height + header offset - copy height) / 2.
     Where the budget is the term that sets the hero height, that reduces to
     the header offset plus half the budget's own slack, so the clearance is
     the header's height plus a positive number by construction and cannot
     fall below it. In the matrix that is 109px, a 734px hero with the copy at
     its 609px maximum under the 92px desktop header, which is the only header
     this block ever runs against: the compact 73px offset is gone by 1025 and
     the overlay does not start until 1440. Where the viewport sets the height
     instead the clearance is larger, 138px at a 792px hero. */
  .fg-hero__body { padding-top: var(--hero-header); }

  /* TWO COLUMNS: the copy, and the region the scene is read in. The second
     column holds nothing; it exists so the split is declared rather than
     implied by a max-width, and so the copy's right edge is the same
     arithmetic the scrim is written off. */
  .fg-hero__body .fg-container {
    display: grid;
    grid-template-columns: minmax(0, var(--copy-w)) minmax(0, 1fr);
  }

  /* THE MEASURE IS IN em, WHICH IS THE WHOLE ANSWER TO THE FIT.
     Measured in Barlow Condensed bold caps at the tracking the kit sets, the
     candidate lines of THE ELECTRONICS BACKBONE BEHIND MISSION-READY SYSTEMS
     are
       THE ELECTRONICS          6.191em
       BACKBONE BEHIND          6.346em   <- the widest, and so the measure
       BEHIND MISSION-READY     8.046em
       MISSION-READY            5.345em
       MISSION-READY SYSTEMS    8.628em
       THE ELECTRONICS BACKBONE 10.021em
     so the wrap is a step function of the measure, not a slope:
       >= 10.021em   three lines
        6.346em      four lines    <- here
        6.191em      five lines
        5.345em      six lines
     Four is the one worth having. Three needs 1002px of column, which pushes
     the copy across most of the frame and leaves nowhere for the scene; five
     costs a whole line for 16px of column; six costs two lines and 200px of
     hero. So the measure is 6.6em: the 6.346em line plus 0.254em of slack for
     a fallback face, written in em so it rides the token instead of being
     re-tuned after it. At the token's 100px ceiling that is 660px, which is
     where the 660px column comes from; below the ceiling the em figure is the
     smaller of the two and governs.
     THE COPY BLOCK CAP IS THE SUB AND THE BUTTONS, not the headline. The sub
     carries its own 537px measure and the actions are a flex row, so 660px
     only stops them spreading to the column; it is the headline's em rule that
     sets the wrap. */
  .fg-hero__copy { max-width: var(--copy-w); }
  .fg-hero__title { max-width: 6.6em; }

  /* THE CONSTANT SCALE. --stage-w is --stage-static or the hero's own width,
     whichever is larger, and aspect-ratio turns that width back into the
     height. The artwork therefore has ONE size on screen across the whole band
     the overlay is designed for, and a window that grows moves the frame
     rather than zooming it. That is the change this block is built around: the
     previous cover crop tied the artwork's size to the hero's height, so a
     taller window silently enlarged every platform, every part and every
     callout, and the composition read at a different size on two machines
     showing the same page.
     WHERE 2560 COMES FROM, AND WHAT IT COSTS. The constant is 0.5 x 5120
     exactly, so one frame pixel is half a CSS pixel, the stage is 2560 x 810 to
     the pixel and every frame-unit figure in this file converts by a clean 0.5.
     It is a size chosen for the picture: at the 2048 it replaces the Earth's
     limb, the satellite and the ground terminal all sat small in a wide dark
     field and the scene read as a backdrop rather than as the subject of the
     hero. A quarter more scale is what puts the four objects at a size the eye
     takes as the point of the page.
     THE FIGURE IT GIVES UP IS THE SHIP'S BOW, and the trade is stated here
     rather than left to be discovered. The bow is frame x 2861 / 5120 = 0.55879,
     the left-most point of the composition the copy was previously kept off. The
     anchor puts the satellite's right edge at the viewport's right edge less
     --frame-pad, so the bow lands at
       bow = W - --frame-pad - (--sat-rx - 0.55879) x --stage-static
           = W - 40 - 0.26973 x --stage-static
     and a larger stage pulls it LEFT: 848px at 1440 with the old constant
     against 709.5px with this one.
     THE WALL CAME BACK TO MEET IT RATHER THAN THE STAGE GOING BACK. Under the
     660px column and the long release the bow sat at 709.5 against a wall
     ending at 684 and a release running to 1122, so the whole object was inside
     the ramp at every width the overlay runs at and none of it read. The column
     is 450 now and the release is a fifth of the room rather than three fifths,
     which puts the wall at 474 and the ramp's end at 686.5 at the same 1440:
     the bow is in clear air there and the whole object is from 1728 up. What is
     still lost is the vessel's STERN at the narrow end of the band, 145.5px of
     hull at 1440 and 73.5 at 1536, and the vessel is neither the anchor nor the
     right-hand content: it is the object furthest into the copy's side of the
     frame, it keeps its label, its hit region and its card, and below 1440 the
     fit rule in section 6 of cinema.css shrinks the frame to the window.
     NOTHING IS CROPPED BY THE CHANGE. The right-hand cut is the anchor's and is
     unmoved in frame terms; the top cut deepens because the stage is 810 tall
     rather than 648, and at the shortest hero the overlay ever draws, 734px at a
     700px window, the satellite's own top edge lands 96px below the hero's top
     against a 92px header. That is the binding case and it clears.
     Percentages resolve against the containing block, which is the hero's
     padding box, so 100% here is the hero's width.

     THE FRAME IS ANCHORED BY THE SATELLITE'S RIGHT EDGE. One left value moves
     the plate and all four platform layers together, and it is set so the
     satellite's own alpha bounding box ends --frame-pad short of the
     viewport's right edge:
       left = max(heroWidth - stageWidth,
                  100% - --frame-pad - stageWidth x --sat-rx)
     Anchoring an OBJECT rather than the painting's border is what makes the
     pad read as a margin: the satellite is the right-most thing in the scene,
     so the eye measures the gap from it, and the 5 percent of painting and 12
     percent of added scenery beyond it are backdrop that may run off-screen
     without anything being lost. Everything the window cannot hold comes off
     the LEFT, which is the added scenery first and then the painting's own
     left edge. The right-hand content is never the thing that is cropped.
     THE max() IS THE NO-GAP GUARD, and under the constant scale it is also the
     sizing policy past 2560. The first term is the placement at which the
     frame's right edge would sit exactly on the viewport's right edge; taking
     the larger of the two makes a gap on the right impossible. It never fires
     as a placement, because the anchor term always leaves the frame's right
     edge past the viewport: by a flat 399px below 2560, where the stage is the
     constant, and by 17.148 percent of the window less 40px above it, where
     --stage-w has become 100% and the two terms are both linear in W.
     A GAP ON THE LEFT IS ALLOWED AND IS INVISIBLE BY CONSTRUCTION. Past
     --stage-static x --sat-rx + --frame-pad = 2161px of viewport the anchor
     term is positive, so the frame's left edge sits inside the window: 399px
     at 2560 and 550px at 3440. That strip is the hero's own
     --surface-base, and it lies entirely inside the scrim's solid plateau,
     which runs to the copy column's right edge at 1220 and 1660 respectively.
     The margin is never close: the widest the gap gets is
     0.17148 x W - 40 against a plateau of 0.5 x W - 60, so the plateau outruns
     the gap by 0.32852 x W - 20 at every width past 61px, and the containment
     cannot fail at any window this page will ever see. Under flat 0.95 ink the
     strip and the frame's own left column, measured at 0.8 / 1.2 / 2.5,
     composite to 0.4 of one step of each other.
     It is written out rather than left to auto margins because an
     over-constrained absolutely positioned box with auto margins does not
     centre when it is wider than its containing block, it goes flush to the
     start edge. A transform would place it in one declaration and is
     deliberately not used, because a transform makes the stage a stacking
     context and seals the parts and callouts out from above the scrim.

     WHAT THE CROP COSTS, from measured geometry at the sizes in the check
     matrix. visible x and visible y are percentages of the FRAME. gap L is the
     hero ground showing left of the stage and gap T the strip showing above
     it, both in CSS pixels; where gap T is 0 the stage is taller than the hero
     and the surplus is cropped off the top instead, which is what moves the
     visible y range off 0.
       viewport      hero   stage          visible x      visible y    gap L  gap T
       1440 x  700    734   2560 x  810   28.2 to 84.4    9.4 to 100      0      0
       1440 x  900    792   2560 x  810   28.2 to 84.4    2.2 to 100      0      0
       1440 x 1440    850   2560 x  810   28.2 to 84.4    0 to 100        0     40
       1600 x  700    734   2560 x  810   21.9 to 84.4    9.4 to 100      0      0
       1600 x  900    792   2560 x  810   21.9 to 84.4    2.2 to 100      0      0
       1600 x 1440    850   2560 x  810   21.9 to 84.4    0 to 100        0     40
       1920 x  700    734   2560 x  810    9.4 to 84.4    9.4 to 100      0      0
       1920 x  900    792   2560 x  810    9.4 to 84.4    2.2 to 100      0      0
       1920 x 1440    850   2560 x  810    9.4 to 84.4    0 to 100        0     40
       2560 x  700    734   2560 x  810    0.0 to 84.4    9.4 to 100    399      0
       2560 x  900    792   2560 x  810    0.0 to 84.4    2.2 to 100    399      0
       2560 x 1440    850   2560 x  810    0.0 to 84.4    0 to 100      399     40
       3440 x  700    863   3440 x 1088    0.0 to 84.0   20.7 to 100    550      0
       3440 x  900    863   3440 x 1088    0.0 to 84.0   20.7 to 100    550      0
       3440 x 1440    863   3440 x 1088    0.0 to 84.0   20.7 to 100    550      0
     WHAT IS OFF-SCREEN, row by row. On the right it is always the same cut and
     it is always scenery: the visible x range ends at --sat-rx + --frame-pad /
     stage width, which is 84.4 percent below 2560 and falls slowly to 84.0 at
     3440 as the pad becomes a smaller share of a wider stage. Frame x 84 to
     100 is the added right-hand extension plus the last 3 percent of the
     original painting, and the right-most object in the composition is the
     satellite at 82.9, so nothing in the scene is ever in the cut.
     On the left, 1440 loses frame x 0 to 28.2, 1600 loses 0 to 21.9 and 1920
     loses 0 to 9.4, all of it added scenery, since the original painting does
     not begin until 31.4 and the left-most object, the vessel, not until 41.0.
     From 2161 up there is nothing off the left at all.
     Off the TOP the taller stage now crops at every viewport height under
     850px, and the deepest cut on the overlay is 9.4 percent at a 700px window,
     which stops 11.9 percent of frame height short of the satellite's own box
     at 21.3. The deepest cut anywhere is still 20.7 percent at 3440, 0.6
     percent short of the same box: what goes is star field either way. Nothing
     is ever cropped off the bottom, because the stage is bottom-anchored.
     SO THE NARROWEST WINDOW THE OVERLAY RUNS AT STILL SHOWS EVERY OBJECT.
     1440 shows frame x 28.2 to 84.4 against four objects that run 41.0 to 82.9,
     with all four wholly inside the window. What 1440 does not show is the
     VESSEL BESIDE THE COPY: its bow lands at 704px against a 684px plateau, so
     the object is behind the scrim's ink rather than off the frame. The
     derivation above says why that trade was taken and where it comes back. */
  .fg-hero__stage {
    --stage-w: max(var(--stage-static), 100%);
    --stage-px: max(var(--stage-static), 100vw);
    position: absolute;
    top: auto;
    bottom: 0;
    left: max(calc(100% - var(--stage-w)),
              calc(100% - var(--frame-pad) - (var(--stage-w) * var(--sat-rx))));
    width: var(--stage-w);
    max-width: none;
    margin: 0;
  }

  /* THE SKY STRIP, AND IT IS THE PLATE'S OWN TOP EDGE RESAMPLED AS A GRADIENT.
     A constant-scale stage is 648px tall against a hero that runs 734 to 850,
     so between 86 and 202px of hero ground shows above the artwork and the
     join between the two is a horizontal line across the top of the page. The
     stage stays bottom-anchored, which is not negotiable: the horizon, the sea
     and the ground never move. So the strip is made to agree with the plate
     instead.
     IT IS A ::before ON THE STAGE, WHICH IS WHAT PUTS IT IN THE RIGHT
     COORDINATE SPACE. left: 0 / right: 0 inside the stage is the frame's own
     x extent, so the 90deg ramp below maps 0 to 100 percent onto frame x 0 to
     5120 by construction and follows the artwork when the anchor moves it.
     bottom: 100% seats it exactly on the stage's top edge, so the join is one
     shared edge rather than two numbers that have to be kept in step.
     THE HORIZONTAL RAMP IS --sky-ramp, authored once above this block with its
     measurement written out there; both layouts that extend the sky read the
     same 33 stops.
     THE VERTICAL FADE IS A MASK, NOT A SECOND GRADIENT, because the ramp is
     already using the background slot and the two axes must not be folded into
     one image. 160px of ease from solid at the join to nothing above it, which
     is the blend; above the fade the hero's own ground, its radial lift and
     the hex lattice are back at full strength. Where the strip is shorter than
     the fade the fade is simply truncated, and that is safe rather than lucky:
     the hero is the first block in the document, so the strip's top edge is
     the page's top edge and there is no surface above it to disagree with.
     IT IS OVERLAY-ONLY. In the stacked layout the copy is above the stage in
     flow and the stage's top edge meets the page's own ground on purpose, so
     there is nothing to blend and this rule does not exist there. */
  .fg-hero__stage::before {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    bottom: 100%;
    height: 160px;
    pointer-events: none;
    background: var(--sky-ramp);
    -webkit-mask-image: linear-gradient(0deg, #000 0%, rgba(0, 0, 0, 0) 100%);
    mask-image: linear-gradient(0deg, #000 0%, rgba(0, 0, 0, 0) 100%);
  }

  /* THE TEXT SCRIM, AND IT IS ONE WALL WITH ONE RELEASE. Solid ink from the
     viewport's left edge, full height, straight through the copy column, then
     a single long eased falloff into the scene. Nothing fades at the left, the
     top or the bottom: the dark side of the hero is dark all the way to its
     edges, and the only transition anywhere is the one horizontal release.

     IT IS ANCHORED TO THE VIEWPORT, NOT TO THE STAGE, so the wall covers the
     text and the release begins at the same relationship to the text at every
     width, whatever the anchor is doing behind it. THAT IS WHAT MAKES THE
     STAGE'S CONSTANT SCALE SAFE. The scrim is a function of W alone and the
     stage's placement is a function of W alone, so the two slide across each
     other on one variable: the plateau ends at max(0.5W - 270, 474) while the
     vessel's stern sits at W - 1111.5, and the gap between them widens by a
     whole pixel of window below 1488 and by 0.5 of one above it. The scene
     therefore comes further out from under the ink as the window grows and
     never goes back under it, which is the property the constant is solved
     against at the 1440 floor: the floor is the worst case, and every wider
     window is strictly better.
     --copy-edge IS THE COPY COLUMN'S RIGHT EDGE, and the plateau runs to it
     exactly: the whole column sits on solid ink. It is derived from the kit's
     container rather than guessed at, so it follows --container-max when that
     token moves, and it is written against --copy-w rather than against a pair
     of literals, so it follows the column when the ceiling moves. .fg-container
     is max-width calc(--container-max + 2 x gutter) centred with gutter
     padding, and the column is --copy-w wide at its left. Above
     --container-max + 2 x gutter of viewport the container is capped and
     centred, so the right edge is (W - 1440)/2 + --copy-w = 0.5W - (720 -
     --copy-w); below it the container is full width, the gutter has already
     reached its own 24px ceiling, and the edge is a flat 24 + --copy-w. The two
     meet at exactly 1488px of viewport for any column, since 0.5W - 720 = 24
     solves to W = 1488 whatever --copy-w is, so the max() has no discontinuity
     at the join. At the 450px column those two legs are 0.5W - 270 and 474, and
     below 1488 the flat leg is the one in force, which is the whole of the 1440
     to 1487 band.
     --release IS 22 PERCENT OF WHAT IS LEFT of the viewport past the plateau,
     lifted to a 200px floor where that fraction would collapse it and the floor
     itself capped at half the room. It used to be 58 percent, and that is what
     hid the vessel: 58 percent ran the ramp 520 to 592px at the widths the
     overlay is read at, which is wider than the 381px object, so the hull sat
     inside the ramp end to end and read at no width at all. A fifth of the room
     is shorter than the object, so the ink is spent BEFORE the hull rather than
     across it. The last three columns are the vessel under the same arithmetic,
     and the stern is the binding end now that the bow is clear everywhere:
       viewport   plateau ends   release ends   clear scene   hull under   stern
                                                              the wall     alpha
       1440        474            686.5          753px        145.5px      0.950
       1600        530            765.4          835px         41.5px      0.950
       1920        690            960.6          959px          0px        0.378
       2560       1010           1351.0         1209px          0px        0
       3440       1450           1887.8         1552px          0px        0
     The bow is past the end of the release at every width from 1440 up, and the
     stern's alpha falls monotonically the whole way.

     THE RELEASE IS NINETEEN STOPS ON AN EASE-OUT, PLACED BY ALPHA RATHER THAN
     BY POSITION, which is what makes it smooth rather than merely gradual.
     alpha(u) = 0.95 x (1 - u)^1.6 is the curve; the stops are the eighteen
     equal alpha steps of 0.0528 along it, each one positioned where the curve
     reaches that value, u = 1 - (alpha / 0.95)^(1/1.6). Equal steps in alpha
     are the thing a gradient's banding is measured in, so placing them that
     way bounds the largest step by construction: no adjacent pair differs by
     more than 0.0528 as authored, or 0.0549 once the compositor quantises the
     alphas to eight bits, against the 0.06 the eye starts to pick up as an
     edge on a large flat area. Placing the same number of stops at equal
     POSITIONS on the curve would put a 0.111 step at the top of the run, which
     is the hard stop this shape exists to avoid.

     THE PLATEAU IS 0.95, AND IT IS SET BY THE SMALLEST TYPE. Composited over a
     hypothetical pure WHITE scene pixel, which is the darkest the ink can ever
     be against its ground whatever the crop or a future plate puts underneath:
       --accent 4.73:1 · --text-muted 5.05:1 · --text-display 16.22:1
     The binding figure is --accent on the eyebrow. At 12px semibold that is
     still normal text, well under the 18.66px bold boundary, so the threshold
     is 4.5:1 rather than the 3:1 the headline is read against: --accent needs
     0.935 of alpha against white and has 0.95. The other runs the scale moves
     are the sub, 16px of --text-muted at 5.05:1, and the two button labels at
     14px: near-black ink on the opaque accent fill at 5.22:1, and --text-primary
     over the plateau at 14.70:1. All three are normal text and all three clear
     4.5:1 with room. Nothing in the column crosses the large-text boundary
     except the headline, which measures 16.22:1 against a 3:1 requirement, so
     the smallest run is still the one that sets the plateau. Every run of copy
     sits on the flat plateau, so there is one alpha for the whole column and
     the ratios move only with what the crop puts under it.
     MEASURED, by compositing the plate and the four platform layers at the
     stage's on-screen geometry, applying the gradient's alpha at each pixel's
     own x and taking the brightest resulting pixel inside every ink rectangle:
     the eyebrow, each headline line box, the accent compound, each sub line
     box and both button labels, over 1440 / 1600 / 1920 / 2560 / 3440 against
     700 / 900 / 1440 of viewport height, which is the whole range this block
     now runs at. Ratios are in the check matrix. */
  .fg-hero__scrim {
    --copy-edge: max(calc(50vw - (720px - var(--copy-w))), calc(24px + var(--copy-w)));
    --release: max(calc((100vw - var(--copy-edge)) * 0.22),
                   min(200px, calc((100vw - var(--copy-edge)) * 0.5)));
    display: block;
    position: absolute;
    inset: 0;
    z-index: 7;
    pointer-events: none;
    background: linear-gradient(90deg,
      rgba(9, 12, 20, 0.95) 0,
      rgba(9, 12, 20, 0.95) var(--copy-edge),
      rgba(9, 12, 20, 0.8972) calc(var(--copy-edge) + (var(--release) * 0.0351)),
      rgba(9, 12, 20, 0.8444) calc(var(--copy-edge) + (var(--release) * 0.071)),
      rgba(9, 12, 20, 0.7917) calc(var(--copy-edge) + (var(--release) * 0.1077)),
      rgba(9, 12, 20, 0.7389) calc(var(--copy-edge) + (var(--release) * 0.1454)),
      rgba(9, 12, 20, 0.6861) calc(var(--copy-edge) + (var(--release) * 0.184)),
      rgba(9, 12, 20, 0.6333) calc(var(--copy-edge) + (var(--release) * 0.2239)),
      rgba(9, 12, 20, 0.5806) calc(var(--copy-edge) + (var(--release) * 0.2649)),
      rgba(9, 12, 20, 0.5278) calc(var(--copy-edge) + (var(--release) * 0.3074)),
      rgba(9, 12, 20, 0.475) calc(var(--copy-edge) + (var(--release) * 0.3516)),
      rgba(9, 12, 20, 0.4222) calc(var(--copy-edge) + (var(--release) * 0.3976)),
      rgba(9, 12, 20, 0.3694) calc(var(--copy-edge) + (var(--release) * 0.4458)),
      rgba(9, 12, 20, 0.3167) calc(var(--copy-edge) + (var(--release) * 0.4967)),
      rgba(9, 12, 20, 0.2639) calc(var(--copy-edge) + (var(--release) * 0.5509)),
      rgba(9, 12, 20, 0.2111) calc(var(--copy-edge) + (var(--release) * 0.6094)),
      rgba(9, 12, 20, 0.1583) calc(var(--copy-edge) + (var(--release) * 0.6737)),
      rgba(9, 12, 20, 0.1056) calc(var(--copy-edge) + (var(--release) * 0.7467)),
      rgba(9, 12, 20, 0.0528) calc(var(--copy-edge) + (var(--release) * 0.8358)),
      rgba(9, 12, 20, 0) calc(var(--copy-edge) + var(--release)));
  }
}

/* ==========================================================================
   1b. THE MISSION SCENE
   Four platforms standing in four domains of one plate.

   WHAT IS LIVE. Each platform is a button that opens its capability drawer,
   and pointing at one runs the reveal (the platform ghosts and grows, its part
   fades up and grows to 3x) and then the callout (the leader draws out of the
   revealed part, the numbered tag lands at its end). Under all of it are four
   independent connections, one arc per platform pair at a single shared
   radius, each with a hairline trace always on and its own signal firing along
   it on its own co-prime cycle.

   ONE SVG SERVES EVERY WIDTH, AND IT DRAWS AT EVERY WIDTH. The connection svg
   is viewBox="0 0 5120 1620", the delivered frame's own pixels, with
   preserveAspectRatio="none", so x / 5120 and y / 1620 land exactly on the
   percentage coordinates everything else in the scene is positioned at and the
   four fitted arcs need no per-viewport arithmetic of any kind. The box is the
   frame's own aspect in all three layouts that draw it, so 1 viewBox unit is
   stage width / 5120 in BOTH axes: a curve authored there cannot come out
   sheared, and a segment with |dx| = |dy| is a true 45 degrees on screen. The
   connections are the one piece of the scene that is never conditional; a
   handset gets the same four arcs and the same four signals the overlay does.

   ---- THE PLATFORMS ARE FULL-ARTBOARD LAYERS -------------------------------
   The four platforms are delivered at the frame's own 5120 x 1620, each one
   WITH its own shadow and transparent everywhere else, so laid inset: 0 over
   the stage they are positioned by construction and registered to the plate by
   construction too. There is no placement table for the artwork and no way for
   the artwork to drift from the composition: the five files ARE the
   composition.
   THEY ARE <img> INSIDE <picture>, AND THERE IS NO LONGER A DOWNLOAD GATE.
   The scene is the hero at every width, so every width fetches the same five
   files: one plate and four artboards. The gate that used to hand a narrow
   screen the tight single-platform cutouts went out with the layout that used
   them, because a cutout cannot compose the scene. It is a crop of one object
   with no registration to the plate and no shadow that belongs to the frame,
   so the five delivered layers are the only set that IS the composition.
   WEIGHT, STATED RATHER THAN SOLVED, and it is the same figure everywhere now:
   the plate is 9.98MB and the four platform layers are 6.24 + 5.64 + 5.82 +
   4.28MB, so the hero is 31.97MB of PNG at every width. The delivered files are
   shipped exactly as they are and nothing here recompresses, resizes or renames
   them. The <picture> structure is kept for exactly one reason: a derivative
   set drops in as extra <source> rules with no change to this file. That is
   where the narrow-screen weight goes when there are derivatives to point at.

   ---- WHAT EACH NODE MEASURES ----------------------------------------------
   The artwork needs no geometry, but the hit region and the growth origin do,
   and both come off ONE measurement per platform, read from the delivered
   layers rather than chosen by eye. Each layer's alpha was thresholded and its
   bounding box taken; the threshold is 200, which is where the box stops
   moving with the threshold and so is the solid silhouette rather than the
   soft shadow around it. At 128 the ship's box grows 73px sideways into its
   own wake and the terminal's 40px down into its base shadow; at 250 the
   satellite's has begun eating its own array tips. 200 sits between the two.
     node          alpha bbox, frame px      frame %, centre and size
     01 satellite  x 3658-4242  y  345-716   77.148, 32.747   11.406 x 22.901
     02 aircraft   x 2595-3398  y  558-778   58.525, 41.235   15.684 x 13.580
     03 ship       x 2099-2861  y 1022-1559  48.438, 79.661   14.883 x 33.148
     04 terminal   x 3339-3847  y  988-1553  70.176, 78.426    9.922 x 34.877
   NO TWO OF THOSE RECTANGLES INTERSECT, so the four hit regions cannot steal
   each other's pointer and no priority rule is needed between them. Two pairs
   do share an x range, the satellite with the terminal and the aircraft with
   the ship, but neither pair shares a y range, so every intersection is empty.
   The smallest box is the aircraft's, and the smallest stage the overlay
   produces is the constant itself, 2560 x 810, which is every window from 1440
   to 2560 and makes that box 400 x 110 CSS pixels. The scroll catch goes
   smaller: its portrait band is 697 x 221 at 390 of viewport, which makes the
   same box 109 x 30, and 30 is under the 44px WCAG 2.1 asks for in both axes.
   So below 1440 the hit box carries a 44px floor in each axis, applied about
   the SAME centre the measured box uses, and the floor is the only thing about
   the target that is not measured. At the floor the four boxes are still
   disjoint at 390: the aircraft grows to 109 x 44 spanning y 73 to 117 against
   the ship's 145 to 222 and the terminal's 140 to 221, and the satellite's x
   range 520 to 603 clears the aircraft's 369 to 483.
   THE MEASUREMENT IS CROSS-CHECKED AGAINST THE REGISTRATION. The previous
   canvas was 2880 x 1620 and its boxes were measured the same way; mapping
   those through the measured registration, x -> (1608 + 1.8x) / 5120, predicts
   where each box should now be. Agreement, as a percentage of the frame:
     node          dx        dy        verdict
     satellite     0.000%   +0.027%    agrees
     aircraft     -0.002%   +0.900%    x agrees, y outside the 0.5% tolerance
     ship         -0.018%   -0.029%    agrees
     terminal     +0.010%   +0.431%    agrees
   Every x lands within 0.02 percent, which is what confirms the offset. The
   aircraft's centre sits 0.9 percent lower than the prediction because its
   delivered layer carries a denser shadow below the airframe than the
   shadow-free set the old box was read from, so alpha 200 reaches 28px further
   down. The MEASURED box governs, here as everywhere: the hit region is 1.7
   percent of frame height taller than the old one at its foot and the growth
   origin sits 14px lower, neither of which is visible, and a box read from the
   file that is actually being drawn cannot be wrong about that file.

   PAINT ORDER IS NOT DOM ORDER. The four objects do not overlap, but the four
   HARMONY LAYERS do: the shadow and bounce each one carries reach well outside
   its own silhouette (the satellite's to x 100%, the aircraft's to y 100%), so
   the stack decides which soft edge sits over which. The export numbering is
   the source stack, top first: 0000 terminal, 0002 satellite, 0003 aircraft,
   0004 ship. DOM order has to be tab order, 01 to 04, so the z-index on each
   LAYER restores that stack underneath it.
   THE STACK LIVES ON THE LAYER AND NOT ON THE BUTTON OR ITS WRAPPER, and that
   is what keeps the ladder global. A z-index on either of those makes it a
   stacking context, and a stacking context is a ceiling: everything inside it,
   part and leader and tag included, would be trapped at that element's own
   level and could never rise over the scrim. With the stack on the layer, the
   button and the artwork wrapper are both z-index auto and create no context,
   so all four layers, all four parts and the eight callout elements are
   siblings on the hero's one ladder.
   ========================================================================== */

/* ---- THE CONNECTIONS  ·  four arcs, one radius, four independent signals ---
   NOT A SEQUENCE AND NOT A LOOP. There are four connections, one per platform
   pair, each one its own arc with its own trace and its own pulse, and nothing
   schedules them against each other.

   ---- THE ARCS ARE FITTED, NOT DRAWN ----------------------------------------
   Every d below came out of the composition source by measurement. The method,
   so it can be re-run if the artwork is redrawn:
     1  difference the source against the delivered plate at their shared
        2880 x 1620, and threshold the WARM residual (R minus B) at 45. That
        isolates what was added from what was already there, which a plain
        chroma threshold cannot do over a photograph with city lights in it.
     2  sample centrelines rather than the glow: scan by row and by column,
        take every run narrower than 70px, and keep its intensity-weighted
        centre. The width filter is what drops the platform glows, which are
        blobs rather than ribbons.
     3  assign each sample to the nearest of the four platform-to-platform
        chords inside a 100-unit corridor, then fit a circle by RANSAC and
        least squares. RANSAC is load bearing on the satellite-to-aircraft
        ribbon, where the descending dish-to-satellite ribbon crosses the same
        corridor and a plain fit lands on neither curve.
     4  choose ONE radius for all four by least squares over the four clouds
        together, which the design calls for: every arc shares a radius.
   WHAT CAME OUT, in the 1600-unit space the fit was done in. Free radii of
   721.6, 571.3, 785.2 and 586.2, and a shared radius of 711.0. Residual
   against the source centrelines at the shared radius:
     connection      samples   free r   rms at r=711
     satellite-jet     434      721.6      0.18u
     jet-ship          484      571.3      0.50u
     ship-dish         475      785.2      0.37u
     dish-satellite    442      586.2      0.36u
   Half a fit unit is about half a pixel at the size the fit was read at: one
   radius fits all four ribbons to inside the width of the hairline that draws
   them.
   THE FIT CARRIES ONTO THE NEW FRAME UNCHANGED, because the mapping between
   the two canvases is a uniform scale and a translation and nothing else:
   x -> 1608 + 1.8x, y -> 1.8y, radius -> 1.8r. A circular arc under a
   similarity transform is still a circular arc with the same relative
   geometry, so the residuals above are the same residuals, the shared radius
   is still shared, and only the printed numbers grow. The shared radius is
   1279.8 frame units and every d in the markup is the fitted d mapped that
   way.
   ENDPOINTS RUN INTO THE OBJECTS, NOT UP TO THEIR BOXES. A bounding box is not
   a silhouette: the satellite's box includes empty sky out at the array tips
   and the ship's includes everything under its mast, so an arc that stopped at
   the box stopped in mid air. Each endpoint is walked along the SAME circle,
   outward from the fit, sampling the CLEAN cutout's alpha (the same set the
   hit-region boxes were read off) until it crosses into alpha 128, then 2.5
   units further so the line makes contact. Walking along the circle is what
   keeps the fit intact: the radius, the centre and every fitted sample are
   untouched, the arc is simply longer at both ends.
   THE CONTACT IS A FLAT 2.5 FIT UNITS because the connections paint OVER the
   artboards (z6). Anything past the silhouette crossing is a line tip drawn ON
   the object; 2.5 fit units is 4.5 frame pixels, enough to seat the line under
   the artwork's own edge and not enough to read as an overshoot.
   HOW FAR EACH END MOVED, in fit units, and what it landed on:
     arc              end     extended   alpha at the terminus
     satellite-jet    sat        10.7      255
     satellite-jet    jet        46.0      252
     jet-ship         jet         2.8      255
     jet-ship         ship        3.9      255
     ship-dish        ship      101.8      255
     ship-dish        dish       81.0      255
     dish-satellite   dish       46.9      251
     dish-satellite   sat       100.6      255
   Arc lengths after the extension: 334.3, 203.2, 449.5 and 327.3 fit units,
   against 277.5, 196.5, 266.8 and 179.9 before. Mapped onto the frame and
   re-measured there with getTotalLength, they are 601.6, 365.8, 809.1 and
   589.1 frame units, which is 1.800x the fit figures to four significant
   places on all four and is the arithmetic cross-check on the mapping.
   ONE FINDING NOT BUILT: the extraction found FIVE ribbons in the source, not
   four. There is an aircraft-to-terminal ribbon that is not one of the four
   named pairs. It is recorded here and deliberately not drawn.

   ---- THE TRACE -------------------------------------------------------------
   Hairline, always on, so the connections read as structure rather than as
   something that only exists while a pulse is on it. --ta is per arc and it is
   measured: the plate's local luminance standard deviation is sampled in a
   25px window along each arc, and the 90th percentile of that maps linearly
   from 10 to 19 onto 0.16 to 0.31 of alpha. A busier backdrop gets more ink:
     arc              backdrop sd (p90)   --ta
     satellite-jet          15.3          0.25   space, Earth limb below
     jet-ship               18.1          0.30   crosses the horizon, busiest
     ship-dish              14.6          0.24   open water
     dish-satellite         10.7          0.17   clear sky, calmest
   -------------------------------------------------------------------------- */

/* z-index 6, ABOVE ALL FOUR ARTBOARDS AND BELOW THE SCRIM AND THE CALLOUTS.
   Under the artboards is where a ribbon belongs relative to the OBJECTS, but
   the artboards are not objects: each is a full 2880 x 1620 layer whose soft
   shadow and bounce reach far outside the silhouette, and a near-transparent
   stretch of one of those over a 1px hairline takes the hairline out of the
   picture. 6 puts every connection over all four artboards, under the scrim
   that dims the whole scene, and under the labels. */
.fg-scene__wire {
  position: absolute;
  inset: 0;
  z-index: 6;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

.fg-conn__arc {
  fill: none;
  stroke: rgba(255, 255, 255, var(--ta));
  stroke-width: 1;
}

/* THE STANDING GLOW. A second, wider instance of the same arc painted UNDER
   the hairline, so the connections sit on a soft warm bed instead of being
   bare white threads on a photograph. stroke-width 7 at 0.22 to 0.30 alpha,
   blurred 5px: a narrower, fainter version of this was invisible, because
   blurring a thin stroke spreads what little ink it has across roughly three
   times its own width.
   IT IS A REAL PATH AND NOT A <use>. A CSS filter on a <use> has to rasterise
   the element's shadow tree; a filter on a plain <path> with its own d has no
   such question to answer. It costs a third copy of every d in the markup,
   which is noted there.
   STATIC, AND THAT IS THE WHOLE OF ITS MOTION STORY. No animation, no
   transition, no data-ambient: there is nothing for the pause control to stop
   and nothing for reduced motion to remove, which is correct, because a
   visitor who has asked for no motion should still get the connections drawn.
   --ga RIDES THE SAME BUSYNESS MEASUREMENT AS --ta, mapped onto its own band:
   the plate's 90th-percentile local luminance standard deviation along each
   arc, from 10.7 to 18.1, onto 0.22 to 0.30 of alpha, so the busiest backdrop
   gets the most bed and the calm sky the least: 0.270, 0.300, 0.262, 0.220. */
.fg-conn__glow {
  fill: none;
  stroke: rgba(227, 85, 48, var(--ga));
  stroke-width: 12.6;
  filter: blur(5px);
}

/* THE BAND. Thirteen concentric layers, every one CENTRED on the same point of
   the path, compositing to a symmetric taper with a soft halo behind it and a
   short bright core on top. It is the Eight Stages rail band's construction
   verbatim; what differs is the normalisation.
   THE LAYERS ARE NORMALISED because there are four paths of four different
   lengths and a signal has to look like the same signal on all of them. Each
   layer carries a FRACTION --sf of its connection's own --sm and the
   arithmetic is done in calc, so one number per connection sets the band
   length and the thirteen fractions are identical everywhere.
   --sm IS THE BAND'S LENGTH AS A FRACTION OF ITS OWN ARC, sized off one figure
   with one cap: 130px of band on screen at the 1600 x 900 reference, which is
   265.9 frame units there, and never more than 60 percent of the arc it runs
   on. The reference matters because a frame unit is not a screen pixel any
   more: at 1600 x 900 the hero is 792px and the stage 2503px across 5120
   units, so one unit is 0.4889px.
     connection        arc      band        --sm
     satellite-jet   601.6u    265.9u     0.4420
     jet-ship        365.8u    219.5u     0.6000   capped
     ship-dish       809.1u    265.9u     0.3286
     dish-satellite  589.1u    265.9u     0.4514
   THE CAP EXISTS FOR THE SHORTEST ARC. Jet-ship is 366 units, and a flat 265.9
   would be 73 percent of it: at that length the pulse is the whole connection
   lit at once, which is a glowing line rather than a signal travelling along
   one. 60 percent is where a head and a tail are still legible as different
   things on the shortest run. The longer band also does cadence work, and that
   is not a coincidence: sweep time is travel divided by a fixed speed, so a
   longer band means a longer sweep, which is lit time bought without ever
   running faster.
   THE DASH PATTERN CENTRES EACH LAYER: a zero-length dash, a gap of half the
   difference between the widest layer and this one, the layer itself, then a
   gap of 2, which is longer than the pathLength of 1 and so guarantees exactly
   one dash on the path. Every layer's centre therefore sits at --sm/2 minus
   the dashoffset, which is what lets one dashoffset animation drive all
   thirteen. */
.fg-conn__band {
  fill: none;
  stroke: rgba(227, 85, 48, var(--sa));
  stroke-width: var(--sw);
  stroke-linecap: butt;
  stroke-dasharray: 0 calc((var(--sm) - (var(--sm) * var(--sf))) / 2)
                    calc(var(--sm) * var(--sf)) 2;
  stroke-dashoffset: calc(var(--sm) * 1px);
  stroke-opacity: 0;
}

/* The widths and alphas are UNIFORM multipliers over all thirteen layers,
   which is the point: the taper is a shape, and multiplying every layer by the
   same number moves the whole profile without bending it. Widths run 5.544 to
   9.072 frame units with a 25.2-unit halo behind them, which is the shipped
   3.08 to 5.04 profile times the 1.8 the frame mapping scales by, so the band
   is the same thickness relative to the artwork as it was; alphas run 0.057 to
   0.900, which puts the bright core at 0.9 and leaves the faintest tail layer
   faint. The trace's own 1px is unaffected, because it is held in screen space
   by vector-effect rather than in user units. */
.fg-conn__band--core { stroke: rgba(236, 100, 70, var(--sa)); }
.fg-conn__band--halo { filter: blur(4px); }

/* ---- THE SIGNALS ----------------------------------------------------------
   THE UNITS ON THE DASHOFFSET ARE LOAD BEARING. A bare custom property
   substitutes as a <number> while a calc() that reaches a length computes as a
   <length>, and the two are not interpolable: given one of each at the two
   ends of a transition the engine falls back to DISCRETE animation and flips
   from the start value to the end value half way through. The opacity ramp
   keeps animating, so the band fades up and down on schedule while its offset
   teleports from off-path-before to off-path-after and is never once drawn on
   the arc. Every dashoffset in this block is therefore written
   `calc(<expression> * 1px)`, at both ends and in the resting rule, so all
   three are the same type. In SVG user space 1px IS one user unit, so the
   rendered geometry is identical to the unitless form.

   SPEED IS BENCHMARKED, NOT CHOSEN. The Eight Stages rail is the pulse the
   rest of the page is measured against, so it sets the pace: the rail is
   1155px wide at a 1600 viewport and its band travels 2.50 rail widths per
   7.6s cycle, which is 379.9 px/s on screen. The signals run at 0.85x that,
   322.9 px/s. STRICTLY LINEAR, no easing at either end: a signal that
   accelerates reads as an object being thrown, and this is a zip, not a glide.

   THE CADENCE TABLE. Sweep time is not a free number: it is the band's travel
   divided by the benchmarked speed, so the only knob is the cycle, and the lit
   share falls out of the two. Travel is arc length x (1 + 1.5 x --sm), which
   is what the dashoffset keyframes sweep through, and the figures below are
   re-derived from the arc lengths MEASURED on the new frame rather than
   carried over: the arcs are longer in frame units and a frame unit is smaller
   on screen, and the two do not cancel.
     connection        arc     cycle    lit     sweep     px/s   x rail  delay
     satellite-jet   601.6u    5.9s   25.67%  1.515s    322.9    0.850  -1.3s
     jet-ship        365.8u    4.1s   25.67%  1.052s    322.9    0.850  -2.9s
     ship-dish       809.1u    7.1s   25.76%  1.829s    322.9    0.850  -5.9s
     dish-satellite  589.1u    5.3s   28.22%  1.496s    322.9    0.850  -3.7s
   The four cycles are untouched, which is what keeps the co-prime arithmetic
   below intact; what moved is the lit share, by about a point and a half.
   ANY-SIGNAL-LIT IS 70.6 PERCENT: 1.05 signals in flight on average, and the
   long-run share of time with nothing moving is the product of the four dark
   shares, 0.743 x 0.743 x 0.742 x 0.718 = 0.294, so the scene is completely
   still for a bit under a third of the time. Phase offsets cannot change that
   average, because co-prime periods drift through every relative phase; what
   they buy is the opening seconds, where four signals all starting at zero
   would fire as one salvo. Each offset is a different, non-obvious fraction of
   its own cycle: 0.220, 0.707, 0.831, 0.698.

   CO-PRIME ON PURPOSE. Written in tenths of a second the four cycles are 59,
   41, 71 and 53, and the rail is 76. All four cycles are prime, 76 is 4x19,
   and no two of the five share a factor. The five loops therefore return to
   the same relative phase once every LCM of those, 691,804,972 tenths of a
   second, a little over 800 days: in practice the firing pattern never repeats
   and never beats against the rail further down the page.
   TWO OF THE FOUR ALTERNATE, and the two shortest arcs were chosen because a
   short run reads as an exchange rather than a delivery: jet-ship and
   dish-satellite answer their own last pass in the other direction.
   animation-direction does that with no second keyframe set, and it also
   reverses the brightening ramp, so the outbound pass lands bright and the
   reply leaves bright. One pass still fires per cycle either way, so the
   co-prime arithmetic above is untouched.

   THE ARRIVAL IS ON THE SIGNAL SIDE, and that is a constraint rather than a
   preference. Two connections meet at every platform, so a platform-side flash
   would need two independent opacity animations on one element, and two CSS
   animations on the same property do not compose: the last one declared wins
   outright and one of the two arrivals would silently never fire. So the band
   fades up as its head enters the arc, reaches full strength at the moment
   that head touches the platform, and is cut at the end of the sweep. The
   head-arrival stop is computed per connection: the head reaches the far end
   at 1 / (1 + 1.5 x --sm) of the sweep, which is 15.44, 13.51, 17.25 and 16.83
   percent of the four cycles.

   GUARDS. Every band layer carries data-ambient, so the kit's global switch in
   components.css freezes these mid-flight on pause and removes them entirely
   on reduced motion, which leaves the traces and their standing glow up and
   the signals off. The media-query twin below is the same statement for a
   visitor whose preference never reaches the attribute.
   -------------------------------------------------------------------------- */

@keyframes fg-signal-sj {
  0%      { stroke-dashoffset: calc(var(--sm) * 1px); stroke-opacity: 0; }
  1.80%   { stroke-opacity: 0.7; }
  15.44%  { stroke-opacity: 1; }
  25.67%  { stroke-dashoffset: calc((-1 - (var(--sm) / 2)) * 1px); stroke-opacity: 0; }
  100%    { stroke-dashoffset: calc((-1 - (var(--sm) / 2)) * 1px); stroke-opacity: 0; }
}
@keyframes fg-signal-js {
  0%      { stroke-dashoffset: calc(var(--sm) * 1px); stroke-opacity: 0; }
  1.80%   { stroke-opacity: 0.7; }
  13.51%  { stroke-opacity: 1; }
  25.67%  { stroke-dashoffset: calc((-1 - (var(--sm) / 2)) * 1px); stroke-opacity: 0; }
  100%    { stroke-dashoffset: calc((-1 - (var(--sm) / 2)) * 1px); stroke-opacity: 0; }
}
@keyframes fg-signal-sd {
  0%      { stroke-dashoffset: calc(var(--sm) * 1px); stroke-opacity: 0; }
  1.80%   { stroke-opacity: 0.7; }
  17.25%  { stroke-opacity: 1; }
  25.76%  { stroke-dashoffset: calc((-1 - (var(--sm) / 2)) * 1px); stroke-opacity: 0; }
  100%    { stroke-dashoffset: calc((-1 - (var(--sm) / 2)) * 1px); stroke-opacity: 0; }
}
@keyframes fg-signal-ds {
  0%      { stroke-dashoffset: calc(var(--sm) * 1px); stroke-opacity: 0; }
  1.98%   { stroke-opacity: 0.7; }
  16.83%  { stroke-opacity: 1; }
  28.22%  { stroke-dashoffset: calc((-1 - (var(--sm) / 2)) * 1px); stroke-opacity: 0; }
  100%    { stroke-dashoffset: calc((-1 - (var(--sm) / 2)) * 1px); stroke-opacity: 0; }
}

.fg-conn--sj .fg-conn__band { animation: fg-signal-sj 5.9s linear -1.3s infinite; }
.fg-conn--js .fg-conn__band { animation: fg-signal-js 4.1s linear -2.9s infinite alternate; }
.fg-conn--sd .fg-conn__band { animation: fg-signal-sd 7.1s linear -5.9s infinite; }
.fg-conn--ds .fg-conn__band { animation: fg-signal-ds 5.3s linear -3.7s infinite alternate; }

/* The compound selector is specificity and not structure: the four rules above
   are (0,2,0) and a bare .fg-conn__band guard is (0,1,0), so it would lose.
   The traces and the standing glow are deliberately NOT in this rule: they do
   not move, so reduced motion has no reason to take them away. */
@media (prefers-reduced-motion: reduce) {
  .fg-conn .fg-conn__band { animation: none; }
}

/* ---- NODE -----------------------------------------------------------------
   The button IS the stage: inset: 0, the same box as the wire and the same box
   as the artboard layer it holds, so every percentage in its style attribute
   is a percentage of the bitmap. It takes no pointer of its own; see the hit
   region below. It carries no z-index either, so it creates no stacking
   context and the layer, the part and the callouts inside it all compete on
   the hero's own ladder.
   -------------------------------------------------------------------------- */
.fg-scene-node {
  position: absolute;
  inset: 0;
  display: block;
  padding: 0;
  background: none;
  border: 0;
  font: inherit;
  color: inherit;
  pointer-events: none;
}

/* The entrance stagger is written as a calc off --i rather than as
   data-reveal-delay, because four literal delay attributes would have to be
   kept in step with a --i the node already carries. It reproduces the kit's
   delay steps 2 through 5 exactly; the kit's own [data-reveal] rule supplies
   the transition and only the delay is restated. */
html[data-fg-js="true"] .fg-scene-node {
  transition-delay: calc((var(--i, 0) + 2) * 70ms);
}

/* THE HIT REGION, and it is the only thing in the scene that takes a pointer.
   Written off the same numbers the growth origin uses, so the target and the
   object cannot drift apart:
     left = --nx - --nw/2      top = --ny - --nh/2
   Invisible on purpose: the silhouette is the target. */
.fg-scene-node__hit {
  position: absolute;
  left: calc(var(--nx) - (var(--nw) / 2));
  top: calc(var(--ny) - (var(--nh) / 2));
  width: var(--nw);
  height: var(--nh);
  pointer-events: auto;
  cursor: pointer;
}

/* THE 44px FLOOR, AND IT IS SCOPED TO THE WIDTHS THAT CAN NEED IT. The measured
   boxes are percentages of the stage, so a narrow stage shrinks them: the
   aircraft's, the smallest of the four, is 109 x 30 CSS pixels on the portrait
   band at 390 of viewport, and 30 is under the 44px WCAG 2.1 asks for in both
   axes. So each axis takes max(measured, 44px), applied about the SAME centre,
   which is why the floor is written into the offsets as well as the sizes: a
   box that grows grows about its own centre rather than downward and rightward
   off its own platform.
   IT STOPS AT 1440 BECAUSE IT IS PROVABLY INERT THERE and a rule that does
   nothing should not be evaluated: the smallest stage the overlay produces is
   the 2560px constant, where the smallest box is 400 x 110. Scoping it also
   keeps the overlay's own declaration the one it has always been, down to the
   order the percentages are resolved in. */
@media (max-width: 1439.98px) {
  .fg-scene-node__hit {
    left: calc(var(--nx) - (max(var(--nw), 44px) / 2));
    top: calc(var(--ny) - (max(var(--nh), 44px) / 2));
    width: max(var(--nw), 44px);
    height: max(var(--nh), 44px);
  }
}

/* THE ARTWORK WRAPPER, holding the platform layer and the part. It carries no
   z-index, no transform and no opacity, so it is not a stacking context and
   its two children keep their own places on the hero's ladder: the layer under
   the scrim, the part over it. It stays an element rather than being dissolved
   into the button because the button is the whole stage and these two are the
   only pieces of it that are artwork.
   display: block is not redundant. This is a <span>, so its default display is
   inline, and while position: absolute blockifies it here, an inline box would
   ignore width, height and aspect-ratio if the position were ever relaxed. */
.fg-scene-node__art {
  position: absolute;
  inset: 0;
  display: block;
  pointer-events: none;
}

/* THE PLATFORM LAYER. The <picture> is the box and the <img> fills it; the box
   is the frame's aspect and so is the artboard, so the default fill sizing
   cannot distort it at any width.
   THE ORIGIN IS THE PLATFORM'S OWN CENTRE, not the artboard's. A scale on a
   2880-wide artboard about its middle would swing a platform standing at 81
   percent of it sideways by a third of the growth; about --nx --ny the
   platform grows in place and everything registered to it grows with it.
   Percentages on transform-origin resolve against this element's own box,
   which IS the stage, so the two numbers are read straight off the geometry
   table. */
.fg-scene-node__layer {
  position: absolute;
  inset: 0;
  display: block;
  transform-origin: var(--nx) var(--ny);
  opacity: 1;
}
.fg-scene-node__layer img { display: block; width: 100%; height: 100%; }

.fg-scene-node--ship .fg-scene-node__layer { z-index: 2; }
.fg-scene-node--jet  .fg-scene-node__layer { z-index: 3; }
.fg-scene-node--sat  .fg-scene-node__layer { z-index: 4; }
.fg-scene-node--dish .fg-scene-node__layer { z-index: 5; }

/* THE PART, AND IT IS A SIBLING OF THE LAYER RATHER THAN A CHILD OF ANYTHING
   THE PLATFORM OWNS. --px / --py place its centre as a percentage of the STAGE
   and --pw sets its width the same way, which is what the full-artboard layers
   force: there is no per-platform art box left to be a percentage of. Because
   the geometry is stage-relative already, the part can sit at its own level on
   the hero's ladder without moving a pixel, and z-index 8 is what puts a
   revealed part over the scrim while its platform stays under it. It holds
   that level whether it is drawn or not, so nothing about the stack changes
   when a reveal starts.
   This rule is the RESTING state, opacity 0; the reveal block raises it. */
.fg-scene-node__part {
  position: absolute;
  left: var(--px);
  top: var(--py);
  z-index: 8;
  width: var(--pw);
  height: auto;
  translate: -50% -50%;
  opacity: 0;
  filter: drop-shadow(0 2px 6px rgba(0, 0, 0, 0.55));
}

/* ---- THE HOVER REVEAL -----------------------------------------------------
   ONE CLOCK, FOUR MOVEMENTS. Point at a platform and it goes translucent and
   grows a little, while the part it carries fades up and grows into place on
   top of it. The four things that move are the platform's fade, the platform's
   growth, the part's fade and the part's growth, and every one of them runs on
   --dur-slower (620ms) with --ease-in-out (cubic-bezier(0.65, 0, 0.35, 1)) and
   no delay at either end. 620ms is the kit's own step above --dur-slow, which
   read as a snap: at 420ms a fade and a growth that are meant to be one
   gesture both arrive before the eye has followed either. Staggering any piece
   of the four would give the composite two arrival moments, and a delay on the
   way out would leave a platform ghosted after the pointer had gone.
   TRANSITIONS AND NOT KEYFRAMES, because a keyframe has to finish. A pointer
   sweeping across the scene reverses these mid-flight constantly, and a
   transition turns round from wherever it has reached.

   HELD BY FOUR STATES, and they are deliberately identical. :hover is the
   pointer, :focus-visible is the keyboard, [aria-expanded="true"] is the
   drawer, which home.js sets on the button it was opened from, and
   [data-revealed="true"] is the first tap on a coarse pointer below 1440, which
   home.js sets in place of the hover a touch screen does not have. All four
   carry exactly the same declarations, so opening a drawer under a hovered or
   tapped platform changes no value and nothing jumps, and the reveal survives
   the focus home.js returns to that button when the drawer closes.
   THE TOUCH STATE IS AN ATTRIBUTE FOR THE SAME REASON THE DRAWER'S IS. It is
   the only one of the four a stylesheet cannot express on its own, and adding
   it to these lists rather than restating their declarations is what keeps one
   set of revealed values. It is written only below 1440 and only on a coarse
   pointer; see section 2 of home.js.

   NOTHING CHANGES LAYER. The platform layer is at its resting z-index at every
   point of the reveal, so it fades from exactly the appearance it already had
   under the scrim, with no brightness step at onset; the part is at z8, over
   the scrim, at every point of the reveal, so it arrives at full strength. The
   arcs keep their full runs and pass behind the revealed part, which is the
   only honest way to draw a connection: a line that stops short of what it
   connects to has a gap in it.

   THE PLATFORM GROWS ABOUT ITS OWN PLACE, NOT THE STAGE'S MIDDLE; see the
   transform-origin on the layer. 1.025 is a lean and not a lunge: on the ship,
   the widest silhouette in the set, it is about 10px of width at a 1920
   viewport.
   NO GLOW, AND IT IS OUT RATHER THAN IDLING. There is deliberately no `filter`
   property on the layer at any state, and that is the point of taking it out
   this way: a filter held at zero alpha still renders, and it costs four
   rasterised filter buffers the size of the stage, kept alive at rest as well
   as on hover, for something that draws nothing. Written out, those buffers
   are released. Putting it back is three lines and no restructuring: a
   `filter` of two zero-alpha, zero-radius drop-shadows on the rest rule, the
   lit pair on the revealed rule, and `filter` added to the layer's transition
   list. Both ends have to carry the same two functions in the same order or
   the whole filter snaps instead of fading, and filter applies BEFORE opacity,
   so at the revealed 0.45 every declared alpha reads at 45 percent of itself.

   THE PART GROWS TO THREE TIMES ITS RESTING BOX. At the resting --pw of about
   3.6 percent of the stage a revealed part is component-sized on a full-bleed
   plate: technically visible, practically not. --part-reveal is 3, which puts
   every part between 10.2 and 10.8 percent of the stage width when it is open:
     node        --pw      revealed   at a 3440 stage   at a 2560 stage
     01 sat      2.025%    6.075%     209px             156px
     02 jet      2.025%    6.075%     209px             156px
     03 ship     1.9688%   5.906%     203px             151px
     04 dish     1.9125%   5.738%     197px             147px
   which are the widest and narrowest stages the OVERLAY produces, the second
   being the constant scale that runs at every window from 1440 to 2560. The
   percentages are smaller than they read on the previous canvas only because
   the frame they are a percentage of is 1.78x wider; the part is the same size
   against the painting it sits in.
   THE RESTING --pw FIGURES ARE UNTOUCHED, and so are --px and --py: the size
   is a scale on top of placed geometry rather than a new set of numbers, so
   the part opens exactly where it has always sat. It overlaps its own ghosted
   platform on the way out, which is the intent: the platform goes to 0.45
   precisely so the part has something to come forward out of. A single node
   can be capped without touching a rule, which is what the per-node variable
   is for:
     .fg-scene-node--ship { --part-reveal: 2.33; }
   0.7 TO 3 IS THE ENTRANCE, and the ratio is the entrance rather than the
   size: the part starts at a fraction of its open size and arrives at full
   size on the same 620ms clock as its own fade, so it reads as an object
   coming forward out of the platform instead of a picture being switched on.
   `scale` and not `transform`, because `translate` already carries the part's
   own centring and the two must not fight. The origin is the default centre,
   and because the property order applies scale before translate the part's
   centre sits on --px --py at every point of the growth: it swells at its
   natural spot rather than sliding into it.
   ITS SEPARATION SHADOW DEEPENS ON THE SAME CLOCK and it rides the scale with
   everything else, because a transform maps the element's filtered output: the
   declared 0 3px 10px renders as 0 9px 30px at the open size. That is the
   right behaviour rather than a rounding error, since the object it is
   separating from the plate is three times the size, and it is why the
   declared figures stay small.

   IT RUNS AT EVERY WIDTH. There is one scene now, so there is one reveal: on a
   pointer device the hover runs it, and on a touch screen the first tap runs it
   through [data-revealed="true"] while the second tap opens the drawer, which
   holds the same reveal through [aria-expanded="true"]. Nothing about the
   reveal itself is width-conditional; only which of the four states asks for
   it.

   THE REVEALED VALUES ARE CUSTOM PROPERTIES, which is a cascade decision. The
   revealed selectors are (0,3,0); a reduced-motion guard written the usual
   way, html[data-motion="reduced"] .fg-scene-node__layer, is (0,2,1) and would
   lose to them. Declaring the growth on the node moves the override to a
   single (0,1,0) declaration site, where a guard cannot lose.
   -------------------------------------------------------------------------- */
.fg-scene-node {
  --plat-grow: 1.025;
  --part-rest: 0.7;
  --part-reveal: 3;
}

.fg-scene-node__layer {
  scale: 1;
  transition: opacity var(--dur-slower) var(--ease-in-out),
              scale   var(--dur-slower) var(--ease-in-out);
}

.fg-scene-node__part {
  scale: var(--part-rest);
  transition: opacity var(--dur-slower) var(--ease-in-out),
              scale   var(--dur-slower) var(--ease-in-out),
              filter  var(--dur-slower) var(--ease-in-out);
}

.fg-scene-node:hover .fg-scene-node__layer,
.fg-scene-node:focus-visible .fg-scene-node__layer,
.fg-scene-node[aria-expanded="true"] .fg-scene-node__layer,
.fg-scene-node[data-revealed="true"] .fg-scene-node__layer {
  opacity: 0.45;
  scale: var(--plat-grow);
}

.fg-scene-node:hover .fg-scene-node__part,
.fg-scene-node:focus-visible .fg-scene-node__part,
.fg-scene-node[aria-expanded="true"] .fg-scene-node__part,
.fg-scene-node[data-revealed="true"] .fg-scene-node__part {
  opacity: 1;
  scale: var(--part-reveal);
  filter: drop-shadow(0 3px 10px rgba(0, 0, 0, 0.72));
}

/* REDUCED MOTION. The ghost stays, because opacity is feedback rather than
   movement and it is the only thing left telling a visitor which platform is
   live. Everything that moves goes, and it goes at the source rather than at
   the rule: the platform's growth is neutralised by making the grown scale
   the same 1 as the rest scale, and the part's by pointing its REST scale at
   its REVEALED one, so it simply appears at full open size instead of
   arriving there. The transition lists then come down to opacity so nothing
   else can animate at all. */
html[data-motion="reduced"] .fg-scene-node {
  --plat-grow: 1;
  --part-rest: var(--part-reveal);
}
html[data-motion="reduced"] .fg-scene-node__layer,
html[data-motion="reduced"] .fg-scene-node__part {
  transition: opacity var(--dur-slower) var(--ease-in-out);
}

/* The media-query twin. The same two neutralisations restated at the same
   selectors, so file order settles it, which is the pattern the Eight Stages
   node lift already uses further down. */
@media (prefers-reduced-motion: reduce) {
  .fg-scene-node {
    --plat-grow: 1;
    --part-rest: var(--part-reveal);
  }
  .fg-scene-node__layer,
  .fg-scene-node__part {
    transition: opacity var(--dur-slower) var(--ease-in-out);
  }
}

/* ---- LEADER AND TAG -------------------------------------------------------
   These rules are the RESTING state; the choreography that drives them is the
   block after the tag. The tag rests at opacity 0 rather than display: none on
   purpose: it holds the button's accessible name, so it has to stay in the
   accessibility tree whether or not it is painted. An opacity: 0 element is
   still named; a display: none one is not, and hiding it that way would leave
   four buttons called nothing.
   The leader svg is authored in the STAGE's own coordinate space,
   viewBox="0 0 5120 1620" over a box that is always the frame's aspect, so a
   segment with |dx| = |dy| is a true 45 degrees on screen rather than a
   sheared diagonal.
   z-index 9, ABOVE ALL FOUR PLATFORMS AND ABOVE THE SCRIM. The layers carry
   the stack at z2 to z5 and each one is a FULL ARTBOARD whose shadow reaches
   far outside its own silhouette, so at any level inside that range a callout
   is painted over by somebody else's shadow. 9 is the top of the
   scene: the labels are the one thing that must never be dimmed, because their
   contrast figures are measured against their own ink backing and nothing
   else.
   -------------------------------------------------------------------------- */
.fg-scene-node__leader {
  position: absolute;
  inset: 0;
  z-index: 9;
  width: 100%;
  height: 100%;
  overflow: visible;
  pointer-events: none;
}

/* THE DASH IS A PER-NODE LITERAL IN --ld, AND pathLength IS DELIBERATELY NOT
   USED. The path carries vector-effect="non-scaling-stroke" so its 1px
   hairline stays 1px at every stage size, and that effect measures the WHOLE
   stroke, dashes included, in screen space. pathLength normalises dash values
   against the USER-space length instead, so the two together scale every dash
   by userLength / screenLength: the draw would finish early or leave the last
   stretch of the line permanently undrawn, and it would do a different one of
   those at every width.
   So each node carries --ld, its own drawn length in the leader svg's own 5120
   x 1620 units, and the dash converts that to screen pixels itself:
     screen length of one viewBox unit = stage width / 5120
   which is what --stage-px carries. dasharray and the resting dashoffset are
   both --ld converted that way: at that value the whole path sits inside the
   leading gap and nothing is drawn, at 0 the whole path sits inside the dash
   and all of it is. A FIXED literal would still be wrong in both directions
   under the constant scale, because the constant is not the only stage width
   this file produces: the overlay holds 2560 up to a 2560px window and then
   takes the window's own width, 3440 on an ultrawide, and the stacked layout
   runs from 1280 at 768 of viewport up to 2473 at 1439. A literal sized for
   one of those would leave part of every leader permanently drawn at rest on a
   wider stage and finish the draw early on a narrower one.
   Converting removes both and makes the draw exactly proportional at every
   width. */
.fg-scene-node__leader-path {
  fill: none;
  stroke: rgba(255, 255, 255, 0.55);
  stroke-width: 1;
  stroke-linecap: butt;
  stroke-dasharray: calc(var(--ld) * var(--stage-px) / 5120);
  stroke-dashoffset: calc(var(--ld) * var(--stage-px) / 5120);
}

/* TAG. An 0.85 ink backing with a hairline-strong edge, a Barlow numeral in
   the accent and the name in display white. Measured against the worst backdrop
   the composition can produce, the plate's warm horizon band at #6a3a20, the
   0.85 backing resolves to #191316:
     --accent 4.89:1 · --text-display 16.78:1   AA
   POSITIONED BY A CORNER, not by a centre. --tgx / --tgy are the far end of the
   leader's horizontal run, and the tag hangs off that point in the direction
   the modifier picks: --tag-e sets its LEFT edge there, --tag-w sets its RIGHT
   edge there, both centre it vertically on the point.
   THE MAX-WIDTH IS A HARD GUARANTEE that never has to fire. A tag is set in
   fixed pixels while the stage is fluid, so each one is given exactly the room
   that exists between its anchor and the stage edge, 3 percent of clearance at
   whichever edge it runs toward, and can never leave the stage at any
   viewport. At the anchors in the markup every tag sets at its natural width
   with budget to spare:
     node   natural   budget at its anchor   headroom
     01     308u      3586u                  11.6x
     02     330u      1601u                   4.9x
     03     285u      2131u                   7.5x
     04     220u      3239u                  14.7x
   in frame units. The budgets grew with the frame while the labels did not,
   so nothing is anywhere near the clamp. What the frame width does NOT
   guarantee any more is that a tag inside the stage is a tag inside the
   VIEWPORT, since the stage runs past the viewport's right edge at every
   overlay width and past its left edge below 1737; that is
   asserted per size in the check matrix instead. That matters because MICROELECTRONICS is a single
   unbreakable run: a clamp would not wrap it, it would spill it past its own
   backing. */
.fg-scene-node__tag {
  position: absolute;
  z-index: 9;                          /* over the artwork AND the scrim; see the leader */
  top: var(--tgy);
  transform: translateY(-50%);
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: 28px;
  padding: 4px var(--space-2);
  background: rgba(9, 12, 20, 0.85);
  border: var(--border-hairline-w) solid var(--border-hairline-strong);
  opacity: 0;
}

.fg-scene-node--tag-e .fg-scene-node__tag {
  left: var(--tgx);
  margin-left: var(--space-2);
  max-width: calc(97% - var(--tgx));
}

.fg-scene-node--tag-w .fg-scene-node__tag {
  right: calc(100% - var(--tgx));
  margin-right: var(--space-2);
  max-width: calc(var(--tgx) - 3%);
}

.fg-scene-node__tag-num {
  flex: none;
  font-family: var(--font-display);
  font-weight: var(--fw-display);
  font-size: var(--fs-2xs);
  line-height: 1;
  letter-spacing: 0.1em;
  color: var(--accent);                /* 4.89:1 on the tag backing */
}

/* min-width: 0 is what makes the max-width above binding at all. A flex item's
   default min-width is auto, which is its longest unbreakable run, and
   MICROELECTRONICS is one such run; without this the tag would push past its
   own max-width rather than respect it. It is a containment rule and not a
   wrapping one: a single unbreakable word has no line to break, so a clamped
   tag spills its label past its own backing. That is why the anchors are
   chosen to leave every label its natural width rather than to rely on this. */
.fg-scene-node__tag-name {
  min-width: 0;
  font-family: var(--font-body);
  font-size: var(--fs-3xs);
  font-weight: var(--fw-bold);
  line-height: 1;
  letter-spacing: var(--ls-badge);
  text-transform: uppercase;
  color: var(--text-display);          /* 16.78:1 on the tag backing */
}

/* ---- CALLOUT SCALE --------------------------------------------------------
   ONE NUMBER, AND IT IS --tag-fs. Everything in the tag is set in em of the
   tag's own font size: the numeral, the name, the padding, the gap and the
   height floor. Change --tag-fs and the whole callout scales with it, in
   proportion, with no second number to keep in step.
   THE CEILING IS 14px. A 10px badge-caps name with 4px of padding is a
   card-corner recipe, sized for a component that already has a border round it
   and a heading above it; a callout floating over a full-bleed photograph has
   none of that context and has to carry itself at a glance from across the
   frame. Padding rides it at 0.6em by 0.9em, which is 8.4px by 12.6px.
   WHY IT IS FLUID AND NOT A FLAT 14px. The tag is fixed pixels over a stage
   that is not: the same label is a far larger share of the frame on a narrow
   band than on the overlay, and four labels that are a fifth of the frame each,
   all of them switched permanently on by the static policy below, would BE the
   scene. The clamp holds 14px from about 1330 up and eases down to its 11px
   floor by 768, which is where the labels are always visible and the stage is
   at its smallest with them on.
     768    11.0px      1280   13.7px
     1024   12.4px      1330+  14.0px
   Below 768 the ramp is already on its floor and 11px is what a tag sets at,
   which is the right size for a label that only appears while its own drawer
   is open.
   THE BACKING AND THE EDGE ARE UNTOUCHED, same rgba(9, 12, 20, 0.85) ink and
   the same hairline, so both measured ratios stand exactly as written above.
   Neither is relying on the large-text threshold to pass; at 14px bold the
   name is normal text by WCAG's measure (large is 18.66px bold) and 16.78:1
   clears the 4.5:1 it is held to with room to spare. --------------------- */
.fg-scene-node__tag {
  --tag-fs: clamp(0.6875rem, 0.431rem + 0.534vw, 0.875rem);
  font-size: var(--tag-fs);
  gap: 0.5em;
  min-height: 2.3em;
  padding: 0.6em 0.9em;
}
.fg-scene-node__tag-num  { font-size: 1em; }
.fg-scene-node__tag-name { font-size: 1em; }

/* ---- THE CALLOUT CHOREOGRAPHY ---------------------------------------------
   THREE MOVEMENTS IN SEQUENCE ON ONE ENTRY, AND ONE MOVEMENT OUT. Point at a
   platform and the reveal runs first: the platform ghosts and grows, its part
   fades up and grows to 3x, both over --dur-slower. The callout is hung off
   the tail of that:
     0ms     platform fade + growth, part fade + growth      620ms  ease-in-out
     250ms   leader draws out of the part                    200ms  out-quart
     430ms   tag fades up and rises 6px into place           140ms  out-quart
     450ms   leader lands on the tag anchor
     570ms   callout settled
     620ms   platform and part settle
   THE TAIL IS COMPRESSED AND ONLY THE TAIL: the 620ms platform and part clock
   is untouched. 250ms is the point the part is recognisably arriving rather
   than still on its way, and the whole callout is done at 570ms, just BEFORE
   the product lands rather than after it. The tag overlaps the last 20ms of
   the leader draw, which reads as the label meeting the line rather than
   queueing behind it. --ease-out-quart on both, because a line that draws and
   a label that settles want a fast start and a long tail, where the platform's
   fade wants the symmetric --ease-in-out it already has.
   EVERY DELAY LIVES IN THE REVEALED RULE, which is what makes the exit right:
   the base rules carry the durations and no delay at all, so releasing the
   pointer starts all four movements at the same instant and they retract
   together, tag first at 200ms, leader at 280, platform and part at 620.
   Nothing hangs on screen waiting for a delay that only entry should have.
   TRANSITIONS AND NOT KEYFRAMES for the same reason as the reveal.
   [aria-expanded="true"] holds the whole state, so a drawer opened from a
   platform keeps its callout up, and [data-revealed="true"] gives a touch
   device the callout on the first tap rather than only after the drawer it
   explains has already opened.
   THE TAG RISES ON `translate`, NOT ON `transform`. The transform slot is
   already carrying translateY(-50%), which is what centres the tag on its
   anchor; the two must not fight. translate is applied before transform, so
   the 6px is a clean offset on top of the centring and the anchor arithmetic
   above is untouched at both ends.
   -------------------------------------------------------------------------- */
.fg-scene-node__leader-path {
  transition: stroke-dashoffset var(--dur-quick) var(--ease-out-quart);
}

.fg-scene-node__tag {
  translate: 0 6px;
  transition: opacity var(--dur-fast) var(--ease-out-quart),
              translate var(--dur-fast) var(--ease-out-quart);
}

.fg-scene-node:hover .fg-scene-node__leader-path,
.fg-scene-node:focus-visible .fg-scene-node__leader-path,
.fg-scene-node[aria-expanded="true"] .fg-scene-node__leader-path,
.fg-scene-node[data-revealed="true"] .fg-scene-node__leader-path {
  stroke-dashoffset: 0;
  transition-delay: 250ms;
}

.fg-scene-node:hover .fg-scene-node__tag,
.fg-scene-node:focus-visible .fg-scene-node__tag,
.fg-scene-node[aria-expanded="true"] .fg-scene-node__tag,
.fg-scene-node[data-revealed="true"] .fg-scene-node__tag {
  opacity: 1;
  translate: 0 0;
  transition-delay: 430ms;
}

/* ---- WHERE THE CALLOUTS ARE STATIC ----------------------------------------
   TWO CASES, ONE RULE, AND IT IS THE TAGS ONLY. Leaders and parts stay
   reveal-driven everywhere; what changes is that the LABEL stops waiting to be
   asked for.
     768 TO 1439. The band is the whole width of the container and every
     platform is drawn at a size that carries a label. A scene of four
     unlabelled silhouettes is a puzzle; the labels are the thing that says
     what the four platforms are.
     TOUCH FROM 768 UP. (hover: none) means there is no pointer to rest, so a
     hover-gated label is a label that only ever appears after a tap has
     already opened the drawer that explains it. The tag has to be there first.
   NEITHER REACHES THE PORTRAIT BAND, and that is the one place the answer goes
   the other way. Below 768 the band is 221px tall at 390 of viewport; four
   permanent labels at 11px over it are most of what is on screen, and the
   aircraft's, which hangs east of a platform already 65 percent across the
   frame, runs past the viewport's right edge and is clipped. Stage 2 of the
   scroll catch is the scene and its four signals, and the name of a platform is
   one tap away in its own drawer, which is where it can be read at full size.
   The other reasonable answer is to open the PARTS statically at these widths
   too, which would make the band read like a fully expanded scene rather than a
   labelled one. That is a one-line change from here.
   transition: none rather than a duration, because a tag that is always on has
   nothing to animate and leaving a live transition on it would only give the
   revealed rule's delay something to do. --------------------------------- */
@media (min-width: 768px) and (max-width: 1439px) {
  .fg-scene-node__tag { opacity: 1; translate: 0 0; transition: none; }

  /* THE AIRCRAFT'S CALLOUT TAKES A LONGER RUN HERE, AND ONLY HERE. Every tag
     is permanently on in this band and each is proportionally far larger than
     on the overlay stage, so the aircraft's label, pulled in tight to its
     product for the overlay, collides with the ship's here. The overlay
     geometry is the one the composition is designed at, so this band
     yields: the aircraft's anchor goes out to 67 percent and its leader is
     re-pointed to match.
     !important IS NOT A SHORTCUT HERE, it is the only mechanism. --tgx is set
     in the button's inline style attribute, and an inline declaration outranks
     every stylesheet rule regardless of selector; !important on a rule is the
     one thing that beats a non-important inline one.
     THE LEADER MOVES WITH IT via the CSS `d` property, which is how an SVG
     path can be re-pointed per breakpoint without a second element or a line
     of script. Both numbers are overridden together, and --ld with them: a tag
     that moves without its leader is a label with no line, and a leader whose
     dash literal no longer matches its length draws short. */
  .fg-scene-node--jet { --tgx: 69.0938% !important; }
  .fg-scene-node--jet .fg-scene-node__leader-path {
    d: path("M3311.52 817.02 L3353.1 858.6 L3537.6 858.6");
    --ld: 247;
  }
}

@media (min-width: 768px) and (hover: none) {
  .fg-scene-node__tag { opacity: 1; translate: 0 0; transition: none; }
}

/* REDUCED MOTION, IN TWO PARTS, because the setting means do not animate and
   not show everything.
   THE FIRST PART RUNS EVERYWHERE. A line that draws itself and a label that
   rises are both motion in the sense the setting means, so the transitions come
   off at every width. What is left is a callout that appears at its finished
   state the instant it is asked for instead of arriving there, which is the
   whole of what the setting is owed.
   THE SECOND PART ONLY RUNS WHERE THE CALLOUTS ARE STATIC ANYWAY, 768 and up,
   and it simply switches them on: every leader drawn and every tag up whether
   or not anything is hovered, which is the state that band is already in. That
   costs the callouts their role as hover feedback at those widths, which is the
   right trade: the platform's own ghost and its part still carry which node is
   live, and a visitor who has asked for no motion gets a scene that is labelled
   rather than one that animates its labels.
   IT STOPS AT 768 FOR THE SAME REASON THE STATIC RULE DOES. Reduced motion
   turns the scroll catch off, so a narrow screen reading this guard is on the
   fallback layout, where the stage is the container's width over --comp-w and
   runs well past the viewport: 622px of stage inside a 390px screen. Four
   permanently-on labels there put the aircraft's, which hangs east of a
   platform already 65 percent across the frame, past the right edge. Left
   reveal-driven it is only ever asked for by a tap, which opens a drawer that
   is full width at that size.
   The guard sets the finished VALUES rather than fighting the revealed rule:
   those selectors are (0,3,0) and a html[data-motion="reduced"] guard is
   (0,2,1), so it can only win against the (0,1,0) base rules, which is exactly
   where the resting state is declared. Both rules agree on 0 and 1 anyway, so
   there is nothing for the cascade to get wrong. */
html[data-motion="reduced"] .fg-scene-node__leader-path,
html[data-motion="reduced"] .fg-scene-node__tag { transition: none; }

@media (prefers-reduced-motion: reduce) {
  .fg-scene-node__leader-path,
  .fg-scene-node__tag { transition: none; }
}

@media (min-width: 768px) {
  html[data-motion="reduced"] .fg-scene-node__leader-path { stroke-dashoffset: 0; }
  html[data-motion="reduced"] .fg-scene-node__tag { opacity: 1; translate: 0 0; }
}

@media (min-width: 768px) and (prefers-reduced-motion: reduce) {
  .fg-scene-node__leader-path { stroke-dashoffset: 0; }
  .fg-scene-node__tag { opacity: 1; translate: 0 0; }
}

/* ==========================================================================
   1c. THE SCROLL CATCH, below 1440px
   One viewport of pin, one camera move, then release.

   WHAT IT IS. The hero holds still for one screen of scroll while the camera
   travels across the artwork under the visitor's own thumb:
     stage 1, at progress 0   the copy alone on a dark field. The frame is not
                              in the window at all: the stage's left edge sits
                              exactly on the viewport's RIGHT edge, so there is
                              no plate pixel, no platform and no arc on screen
     the pan, 0 to 1          the copy fades and lifts away over the first 0.4
                              while the stage slides left, so the window travels
                              rightward across the frame: out of the dark, in
                              over the added scenery, then onto the composition
     stage 2, at progress 1   the tight crop, all four platforms reading, the
                              four independent signals running
   Then the pin releases and the page scrolls on normally. Scrolling back up
   runs every one of those backwards, continuously, because none of it is an
   event and all of it is a function of one number.

   ONE NUMBER DRIVES EVERYTHING AND IT IS --catch. home.js writes it on the
   outer box, 0 to 1, clamped, from one rAF-throttled scroll listener, and it is
   monotone in scroll position by construction: it is the outer box's own top
   edge measured against its runway. Nothing else is scripted. Every visual
   below is a plain CSS consumer of that one property, which is what makes the
   whole thing reversible for free and what keeps the script to a measurement
   rather than a choreography.
   TWO SUB-RAMPS ARE DERIVED FROM IT so the mapping is stated in one place
   rather than repeated in every consumer:
     --catch-copy   0 to 1 across --catch 0 to 0.4      the copy's exit
     --catch-sig    0 to 1 across --catch 0.75 to 1     the signals' arrival
   They do not overlap, and the gap between them is the whole point of the
   choreography: the copy is gone before the first platform pixel reaches the
   window, and the signals come up over the last quarter, by which time the
   composition is most of the way in and there is something for them to run on.

   WHERE THE PROGRESS FIGURES COME FROM. The frame fraction sitting on the
   viewport's right edge is --catch x --catch-rx at every progress, because
   stage 1 puts it on the frame's own left edge, fraction 0, and the landing
   puts it on --catch-rx, and the pan is that one interpolation. So the frame
   fraction at which each platform first appears converts straight into a
   progress figure, and the figures are THE SAME AT EVERY WIDTH, capped or not,
   because --catch-rx is a constant:
     platform     first column          frame fraction   enters at --catch
     ship          2087, measured        0.40762          0.495
     ship          2099, geometry table  0.40996          0.498
     aircraft      2595                  0.50684          0.615
     terminal      3339                  0.65215          0.792
     satellite     3658                  0.71445          0.868
   The ship's measured figure is the first column of the ship layer carrying an
   alpha of 16 or more. Left of it the layer holds only the harmonisation wash
   that lies over the whole painting, whose peak alpha anywhere left of 2087 is
   15 of 255 over a plate whose own brightest column mean there is
   rgb(11, 13, 17).
   The copy's ramp ends at 0.4 against the earliest of those figures, 0.495, so
   the copy is at zero opacity for a tenth of the pan before any part of the
   composition is on screen at all.

   THE MODE IS SWITCHED ON BY SCRIPT, WHICH IS WHAT MAKES THE FALLBACK FREE.
   Every rule in this block is scoped under [data-catch="on"], an attribute
   home.js puts on the outer box only when it is below 1440, has run, and has
   not been asked for reduced motion. No script and reduced motion therefore
   both land on exactly the same thing with no undo rules to write and nothing
   to keep in step: the base layout in section 1, copy above a scene in flow.

   svh THROUGHOUT. The pin is a height, and on a handset the one height that
   does not move when the URL bar does is the small viewport height. Using vh
   here would make the runway grow and shrink under the visitor mid-scroll.
   ========================================================================== */

@media (max-width: 1439.98px) {

  /* THE OUTER BOX IS THE SCROLL BUDGET AND THE HERO IS THE WINDOW ONTO IT.
     Height is one viewport for the hero to stand in plus the runway the pan is
     spent over, and the hero inside is sticky at the top at exactly one
     viewport tall. The hero therefore adds --catch-runway to the page and not
     one pixel more.
     THE RUNWAY IS IN TWO SEGMENTS AND THE KNOBS ARE THE TWO SVH COUNTS.
       --catch-pan    60svh, the pan itself. About two thumb-flicks: long
                      enough that the camera move is something the visitor
                      drives rather than something that snaps past them, short
                      enough that a visitor who wants the page rather than the
                      hero is through it in one gesture
       --catch-hold   40svh, the LANDING HELD. Once the pan reaches 1 the pin
                      does not release; the composition stays on screen, still
                      pinned, for another two thirds of the pan's own distance.
                      Without it the scene arrives and is gone in the same
                      gesture that brought it in, because the flick that
                      finishes the pan carries straight on into the release.
                      40svh is longer than one flick, so a visitor who flicks
                      once at the landing comes to rest still on the scene
     THEY ARE WRITTEN AS UNITLESS COUNTS OF svh, not as lengths, and that is
     what lets one authored pair serve both consumers. The height below turns
     them into a length; home.js reads the same two numbers to work out which
     share of the runway the pan is spent over, and divides by it. A custom
     property holding a plain number is the one kind of value getComputedStyle
     hands back in a form a script can use, so there is no second figure to
     keep in step and no way for the CSS and the script to disagree about where
     the pan ends.
     NOTHING IN THE HOLD IS A NEW STATE. --catch simply reaches 1 at the end of
     the pan segment and stays there, so every rule in this block is already
     written for it and the reverse scroll mirrors it for free. */
  .fg-hero-catch {
    --catch-pan: 60;
    --catch-hold: 40;
    --catch-runway: calc((var(--catch-pan) + var(--catch-hold)) * 1svh);
    --catch: 0;
  }

  [data-catch="on"] {
    /* THE PIN IS ONE WINDOW LESS THE NEXT BAND'S OWN LEAD-IN, and that is the
       whole of the fix for the dark under the artwork. The composition is hung
       from the copy's line and the frame is 3.16:1, so on a handset the hero
       ends with a few hundred pixels of flat field below the scene; the band
       that follows then adds --section-y-lg of its own blank lead-in on top of
       it, and the visitor scrolls the two stacked. Pulling the hero up by
       exactly that lead-in removes the duplication and nothing else: what fills
       the last --section-y-lg of the window during the pin is the next band's
       empty top padding, which is --surface-base, which is exactly what the
       hero's own bottom blend already ends on. Nothing of the next section's
       CONTENT can reach the window while the pin holds, because the pull is
       bounded by the padding that hides it.
       THAT BOUND IS ALSO WHY IT IS NOT PULLED FURTHER. The field is larger
       than the lead-in at every handset size, so more would look like more; it
       would also put the next band's section head on screen underneath the
       composition for the whole of the pan, which is a different page rather
       than a tighter one. Closing the rest of that field needs either a
       tighter crop or a hero that is no longer a full window, and both are
       choices about the composition rather than about spacing.
       EVERY svh IN THIS BLOCK READS THIS PROPERTY, not the window, so the band
       height cap, the stage's offset and the hero's own box all stay solved
       against the box they actually sit in. */
    --catch-hero-h: calc(100svh - var(--section-y-lg));
    height: calc(var(--catch-hero-h) + var(--catch-runway));

    /* The two sub-ramps, resolved here so every consumer reads a plain
       number. clamp() over unitless terms, so a --catch outside 0 to 1 could
       not drive anything past its end state even if one arrived. */
    --catch-copy: clamp(0, calc(var(--catch) / 0.4), 1);
    --catch-sig: clamp(0, calc((var(--catch) - 0.75) / 0.25), 1);
  }

  /* NATIVE STICKY, NOT A SCRIPTED TRANSFORM. The browser owns the pin, so the
     scroll is the platform's own: momentum, rubber-banding, keyboard paging
     and find-in-page all keep working, and there is no frame at which the hero
     is mispositioned because a listener has not run yet.
     The declared block padding comes off: in the pin the hero is the window
     less the next band's lead-in, derived above, and the header allowance
     moves onto the copy, the same trade the overlay makes for the same
     reason. */
  [data-catch="on"] .fg-hero {
    position: sticky;
    top: 0;
    height: var(--catch-hero-h);
    min-height: 0;
    padding-block: 0;

    /* THE CROP IS DESCRIBED BY TWO NUMBERS AND THE REST IS ARITHMETIC.
         --catch-clip-l  how far the window is licensed to cut INTO the ship at
                         the left end, as a fraction of the frame's width
         --catch-clip-r  the same licence at the satellite's end, and it is a
                         quarter of the left one. The derivation is below; the
                         short of it is that a bow and an array do not read the
                         same way when they are trimmed
       Everything else comes off the pair and the frame descriptor at the top of
       this section, so there is no second geometry table:
         --catch-band  the fraction of the frame's width the window holds at
                       the landing, which is the span from the ship's left edge
                       to the satellite's right edge less each end's own clip
         --catch-rx    the frame fraction sitting on the viewport's RIGHT edge
                       at the landing, the satellite's right edge pulled in by
                       the satellite's own clip
         --catch-dark  the same fraction at progress 0, and it is the frame's
                       own left edge. The camera starts one window's width off
                       the artwork, which is what puts zero frame pixels on
                       screen while the copy is up and what makes the platform
                       entry figures above independent of the viewport
       WHY THE CROP CUTS INTO THE OBJECTS. The window has one screen of width
       for a composition 2143 frame pixels wide, and the brief is minimal left
       and right padding. Zero padding alone still reads as a placed picture;
       the clip buys the last of the zoom by taking a little off the two
       objects that END the span. But the two ends are not alike and the clip
       is no longer symmetrical, because what each one can spare is different.
       THE SHIP'S BOW IS A LONG TAPER and it keeps the full 2 percent. 102.4
       frame pixels off its left takes 6.3 percent of the ship's own SOLID
       mass, the alpha carried by columns at 128 or more, and leaves 660 of its
       762 columns. What goes is the tip of the bow, which reads as the frame
       running out rather than as a broken hull.
       THE SATELLITE IS NOT A TAPER, IT IS AN ARRAY WITH AN END, so the same
       cut reads as a broken object rather than as a crop. At 2 percent it lost
       102 of its 585 columns and 10.7 percent of its mass, which is a third of
       one panel. The satellite side is therefore held to the largest clip that
       takes UNDER ONE PERCENT of the object's own solid mass, which is 0.005:
         clip    frame px   columns cut   columns kept   solid mass cut
         0.02      102.4      102           483           10.662 percent
         0.01       51.2       51           534            3.349
         0.0075     38.4       38           547            1.792
         0.005      25.6       26           559            0.851   <- here
         0.004      20.5       20           565            0.506
       25.6 frame pixels is 4.4 percent of the satellite's width and lands in
       the outermost cells of the array, where the profile has already fallen
       to 33 solid rows against the object's own 214-row column mean. The
       silhouette reads whole; what goes is the outer edge of one panel.
       THE AIRCRAFT AND THE TERMINAL are inboard of both ends and are never
       touched at any width.
       WHAT THE RELAXED SIDE COSTS is zoom, and it is stated rather than
       hidden: the window widens from 1938 frame pixels to 2015, which is 4
       percent less magnification at every width the cap does not bind. Where
       the cap does bind the window was already wider than either figure and
       nothing changes at all.

       --catch-w IS THE STAGE'S WIDTH AS A PERCENTAGE and --catch-h its height
       as a length. The pair exists for the same reason --stage-w and
       --stage-px do on the overlay: a percentage is the honest figure for the
       box, because it is a percentage of the hero and so has the scrollbar
       already taken out of it, while the leader's dash arithmetic and the
       bottom blend need a length and can only read the viewport. Below 1440
       they differ by the width of a classic scrollbar and by nothing else. The
       pan itself reads neither: it is a percentage of the stage, so it is
       exact against whichever of the two sized the box.
       THE HEIGHT CAP IS THE SATELLITE, AND IT ONLY BINDS ON A SHORT WIDE
       WINDOW. Nothing in the crop above says anything about height: the stage
       is bottom-anchored and its height follows its width through the frame's
       aspect, so a wide short window would take a band taller than itself,
       crop the top of the frame off, and the first thing off the top is the
       satellite. The cap is that band height stated as the point where the
       satellite's own top edge still has --catch-air of the frame's height
       clear above it, 3 percent:
         cap = --catch-hero-h / (1 - --sat-ty + --catch-air) = 1.22394 heroes
       It binds when the width leg would pass it, which is a window wider than
       1.522 times the HERO's own height, and the hero is one window less the
       next band's lead-in rather than the window itself:
         viewport      hero    band height   window        what the landing shows
          390 x  844    772 px   314 px       2015 frame px  the derived crop
          768 x 1024    941      617          2015           the derived crop
         1280 x  900    795      973, capped  2131            14 px of sea left of the bow
         1439 x  900    788      965, capped  2416           299 px of sea left of the bow
         1280 x  700    595      728, capped  2847           730 px of sea left of the bow
       Where it binds the window gets WIDER, which is less zoom and less clip,
       and every one of those extra frame pixels falls on the LEFT, because
       --catch-rx pins the right edge at every width. That is the overlay's own
       policy restated: the right-hand content is never the thing that gives
       way.
       THE FLOOR IS THE NO-GAP GUARD. max() against 100 percent stops the cap
       from making the stage narrower than the hero, which below about 372px of
       viewport height it otherwise would. */
    --catch-clip-l: 0.02;
    --catch-clip-r: 0.005;
    --catch-air: 0.03;
    --catch-band: calc(var(--sat-rx) - var(--ship-lx)
                       - var(--catch-clip-l) - var(--catch-clip-r));
    --catch-rx: calc(var(--sat-rx) - var(--catch-clip-r));
    --catch-dark: 0;
    --catch-band-max: calc(var(--catch-hero-h) / (1 - var(--sat-ty) + var(--catch-air)));
    --catch-w: max(100%,
                   min(calc(100% / var(--catch-band)),
                       calc(var(--catch-band-max) * var(--plate-ar))));
    --catch-h: max(calc(100vw / var(--plate-ar)),
                   min(calc(100vw / var(--catch-band) / var(--plate-ar)),
                       var(--catch-band-max)));

    /* ONE START LINE, AND BOTH THE COPY AND THE BAND BEGIN ON IT. The copy has
       the pinned window to itself at progress 0 and the band has it at
       progress 1, so the two are given ONE top edge: the composition walks in
       and takes the room the copy has just left, starting exactly where the
       copy started. Nothing is measured at runtime and the pair cannot drift
       apart at a width nobody checked, because both read this one expression.
         --catch-inset  the air under the fixed header, on the spacing scale
                        between --space-9 and --space-11
         --catch-top    the line itself: the header allowance plus that air
       THE HEADER ALLOWANCE IS THE KIT'S OWN TOKEN. --hero-header is set at the
       top of this section and tracks the header's own breakpoint rather than
       the hero's: 73px under the compact header, 92px above 1024. The air on
       top of it is real clearance rather than a hairline, and it is what the
       copy sits on:
         viewport      --hero-header   inset   copy and band start at
          390 x  844    73px            48px    121px, 49px under the header
          768 x 1024    73              48      121, 49px under the header
         1280 x  900    92              57.3    149.3, 58.3px under it
       WHY THE SURPLUS GOES BELOW. The band is the frame and the frame is
       3.16:1, so on a handset or a tablet held upright it is shorter than the
       room under the header and something has to hold the difference. Above the
       artwork that difference reads as a hole punched under the header; below
       it, it is the hero's own ground running out into the next band, which is
       what the bottom blend was already built to carry. So the band is hung
       from the top line and the surplus falls to the bottom.
       --catch-crop TAKES THE FRAME'S DEAD SKY OFF THE TOP, and it is the last
       of the empty space above the composition. The delivered frame carries
       345 rows of star field above the satellite, 21.3 percent of its height,
       and at the landing that is 69px of nothing on a handset sitting between
       the copy's line and the highest thing in the scene. The crop line is the
       satellite's own top edge less the air rule:
         --sat-ty - --catch-air = 0.212963 - 0.03 = 0.182963
       so the composition starts on the line with exactly the headroom
       --catch-air already asks for, 3 percent of the frame's height, and not a
       row more. It is the same figure the height cap is solved against, used
       here as a crop instead of a ceiling, so there is one air rule and not
       two. THE CROP IS A PAINT DECISION, NOT A BOX ONE: the stage stays the
       whole frame, every percentage in the geometry table still measures the
       whole frame, and the sky extension simply covers what is above the line.
       See the extension rule for how, and for the ramp it is covered with. */
    --catch-inset: clamp(var(--space-9), 1.5rem + 2.6vw, var(--space-11));
    --catch-top: calc(var(--hero-header) + var(--catch-inset));
    --catch-crop: calc(var(--sat-ty) - var(--catch-air));

    /* THE HEADLINE IS CAPPED IN THE PIN, and the cap is a composition decision
       rather than a typographic one. The copy has the viewport to itself at
       progress 0, but it has to leave it inside the first 0.4 of the runway,
       so a headline set at the token's full desktop size would still be going
       when the composition arrived. The two extra legs are
         clamp(2.75rem, 1.6rem + 3.4vw, 4.25rem)   the width leg, floored above
                                                   the handset setting so a
                                                   phone is untouched: 42.75px
                                                   at 390 exactly as before,
                                                   51.7 at 768, 68 from 1250 up
         8svh                                      the height leg, which only
                                                   binds on a short window and
                                                   is what stops a landscape
                                                   handset setting a headline
                                                   taller than the room it has
       The first leg is the base expression restated: a custom property cannot
       be written in terms of itself. It carries the 6.25rem ceiling it was
       written with rather than --hero-fs-max, which costs nothing because the
       width leg below caps at 4.25rem and is the binding one from 1250 up.
       THIS EXPRESSION NO LONGER READS --fs-display EITHER, for the reason on
       the base declaration: the token leg could not bind against a 4.25rem cap
       and a 56px floor, and reading it tied the pin to a sitewide type
       decision. NOT A WORD OF THE COPY CHANGES; this is the size it is set at,
       and the wrap follows the same step function the overlay's measure note
       derives. */
    --hero-fs: min(clamp(2.5rem, 0.6rem + 8.5vw, 6.25rem),
                   clamp(2.75rem, 1.6rem + 3.4vw, 4.25rem),
                   8svh);
  }

  /* THE STAGE, HUNG FROM THE COPY'S OWN LINE AND PANNED BY ONE PROPERTY.
     THE VERTICAL PLACEMENT IS --catch-top AND NOTHING ELSE. The band's top
     edge is put on the line the copy starts at, so the composition walks in
     and takes the room the copy has just left instead of arriving at the foot
     of the window with a screen of empty field standing over it. NOTHING
     MOVES VERTICALLY while --catch runs; the offset is a constant and the pan
     is one axis.
     IT IS WRITTEN AS A BOTTOM OFFSET, not a top one, because the clamped end
     of the expression has to be exact. The band IS the frame, and on a wide
     short window the frame is taller than the room under the header, so the
     top line is not reachable and the band falls back to sitting on the
     window's bottom edge. That fallback is the placement the height cap is
     solved against and the one thing here that must land on the pixel: the
     surplus has to go off the TOP, where the sky extension puts it back, and
     never off the bottom, where the horizon, the sea, the ship and the ground
     terminal are.
       100svh - --catch-top - the band's height BELOW THE CROP LINE
     is the offset that seats the crop line on the copy's line, and max()
     against 0 is the fallback. The height term is (1 - --catch-crop) x the
     band, because the rows above the crop line are covered rather than shown
     and so are not part of what the placement has to fit. Read the three cases
     straight off it:
       viewport      band   shown   bottom offset   scene runs   field below
        390 x  844    314    256     395 px          121 to 377   395 px
        768 x 1024    617    504     315             121 to 625   315
       1280 x  900    973    795       0               0 to 795     0
     THE THIRD ROW IS THE CLAMP and it is the only one where the scene starts
     above the copy's line: at that shape the frame is taller than the window
     and fills it outright, so there is no line to hang from and nothing empty
     to close up.
     WHERE IT CLAMPS THERE IS NOTHING TO CORRECT, and that is the point of the
     fallback rather than a limitation of it. The clamp engages only once the
     band is at least as tall as the room under the header, and at that size
     the band already fills the window top to bottom: there is no empty field
     above it to close up and no bottom edge on screen to blend.
     THE SATELLITE'S HEADROOM RULE IS UNCHANGED AND STILL BINDS AT THE CAP.
     --catch-air asks for 3 percent of the frame's height clear above the
     satellite, and the cap solves that against the band sitting on the bottom
     edge, which is exactly the clamped case: lifting the band off that edge
     moves the satellite's top edge DOWN by the same amount and can only add
     headroom. The cap is therefore derived where the clamp lands and every
     placement above it is slack, which is why the air figure needs no second
     derivation here.
     THE HORIZONTAL PLACEMENT IS THE LANDING and the pan is an offset back off
     it. left puts --catch-rx on the viewport's right edge, which IS the
     landing; the translate carries the stage right by the whole span between
     --catch-dark and --catch-rx at progress 0 and gives all of it back by
     progress 1. Both terms are multiples of the stage's own width, so the
     translate is written as a PERCENTAGE OF THE STAGE and the two agree
     exactly with no unit to convert between them.
     translate RATHER THAN left, because the pan runs on every frame of the
     runway: an offset the compositor can apply to a rasterised layer costs a
     transform per frame, where animating left costs a layout and a repaint of
     five full-frame images. The stage becomes a stacking context as a result,
     which is the one thing the overlay's own placement note rules out. It is
     safe HERE and only here: the thing that rule protects is the parts and the
     callouts sitting above .fg-hero__scrim, and the scrim does not exist below
     1440. Everything else in the hero's ladder is a sibling of the stage, so
     the bottom blend at z10 and the copy at z11 still paint over it.
     aspect-ratio, position and the percentage table all come from the base
     rule unchanged, which is the point of deriving one width here rather than
     a second geometry. */
  [data-catch="on"] .fg-hero__stage {
    --stage-px: calc(var(--catch-h) * var(--plate-ar));
    position: absolute;
    top: auto;
    bottom: max(0px, calc(var(--catch-hero-h) - var(--catch-top)
                          - ((1 - var(--catch-crop)) * var(--catch-h))));
    left: calc(100% - (var(--catch-w) * var(--catch-rx)));
    width: var(--catch-w);
    max-width: none;
    margin: 0;
    translate: calc((1 - var(--catch)) * (var(--catch-rx) - var(--catch-dark)) * 100%) 0;
    will-change: transform;
  }

  /* THE SKY EXTENSION, AND HERE IT IS ALSO THE CROP. It does two jobs with one
     box: it covers the frame's dead star field from --catch-crop up to the
     frame's own top edge, which is what takes the empty sky off the top of the
     scene, and it goes on upward from there to fill the window above the
     composition. The two are the same surface and so cannot disagree.
     bottom SEATS IT ON THE CROP LINE. 100% - --catch-crop of the stage's own
     height puts its lower edge exactly on the line the composition starts at,
     so the join is one shared edge rather than two numbers kept in step, the
     same way the overlay's strip is seated on the stage's top edge.
     THE HEIGHT NEEDS NO TUNING. The strip is one viewport tall and the hero is
     one viewport tall, so its own top edge is always at or above the top of
     the window whatever the crop line is doing. There is nothing to fade INTO
     and no mask, which is why the overlay's 160px blend has no counterpart.
     THE RAMP IS SAMPLED AT THE CROP LINE, NOT AT THE FRAME'S TOP, which is why
     it is authored here instead of reading --sky-ramp. Same method, same
     pitch: 33 stops at 160 frame pixels, each the mean of the plate's 8 rows
     starting at row 296, which is --catch-crop x 1620 rounded, over a 160px
     window centred on that stop. Worst rounding error over the 33 is 0.479 of
     255. The line is still sky and still nearly black: the whole strip runs
     under rgb(5, 12, 21) except at frame x 46.9 percent, where the glow off
     the Earth limb reaches rgb(10, 26, 40), and its first stop is rgb(1, 1, 1)
     against the field's rgb(2, 3, 3), so the vertical edge the strip leads
     with as the pan starts is invisible for the same reason the artwork's is.
     IT PAINTS OVER THE ARTWORK, at the level the text scrim holds on the
     overlay and which is empty below 1440. That is what makes the crop a paint
     decision: everything under it, the plate at 1, the four artboards at 2 to
     5 and the arcs at 6, is covered above the line, while a revealed part at 8
     and a callout at 9 still read. The satellite's own tag anchor sits at
     frame y 18.3 percent, which IS the crop line, so it is the one callout the
     order has to get right and it is on the near side of it.
     IT PANS WITH THE ARTWORK, and it has to. The 33 stops are placed in the
     frame's own x, so the strip is a child of the stage and inherits the
     translate: the sky above the line is the sky belonging to the band's own
     part of the frame at every point of the move, and the join never shears. */
  [data-catch="on"] .fg-hero__stage::before {
    content: "";
    position: absolute;
    z-index: 7;
    left: 0;
    right: 0;
    bottom: calc(100% - (var(--catch-crop) * 100%));
    height: 100svh;
    pointer-events: none;
    background: linear-gradient(90deg,
      rgb(1, 1, 1) 0%,
      rgb(1, 1, 2) 3.125%,
      rgb(1, 2, 2) 6.25%,
      rgb(1, 1, 2) 9.375%,
      rgb(1, 1, 2) 12.5%,
      rgb(1, 2, 3) 15.625%,
      rgb(1, 2, 3) 18.75%,
      rgb(1, 2, 3) 21.875%,
      rgb(1, 2, 3) 25%,
      rgb(1, 1, 3) 28.125%,
      rgb(1, 2, 5) 31.25%,
      rgb(2, 2, 6) 34.375%,
      rgb(3, 4, 8) 37.5%,
      rgb(3, 4, 8) 40.625%,
      rgb(4, 8, 15) 43.75%,
      rgb(10, 26, 40) 46.875%,
      rgb(5, 12, 19) 50%,
      rgb(5, 12, 21) 53.125%,
      rgb(5, 11, 19) 56.25%,
      rgb(4, 12, 21) 59.375%,
      rgb(3, 10, 18) 62.5%,
      rgb(3, 6, 13) 65.625%,
      rgb(3, 4, 10) 68.75%,
      rgb(3, 4, 10) 71.875%,
      rgb(4, 7, 14) 75%,
      rgb(4, 5, 9) 78.125%,
      rgb(3, 3, 6) 81.25%,
      rgb(3, 3, 5) 84.375%,
      rgb(2, 3, 4) 87.5%,
      rgb(1, 2, 3) 90.625%,
      rgb(1, 2, 2) 93.75%,
      rgb(1, 1, 1) 96.875%,
      rgb(1, 1, 1) 100%);
  }

  /* THE GROUND RUN-OUT, AND IT IS THE SKY EXTENSION MIRRORED. Once the band is
     hung from the copy's line its bottom edge is off the bottom of the window,
     and that edge is the sea and the shoreline: the brightest thing anywhere
     on the frame's border. Left alone it would meet the flat field as a hard
     horizontal line, which is the one thing the old bottom anchor made
     impossible. So the same technique the top uses is turned upside down: the
     plate's own bottom edge resampled as a horizontal ramp, seated on the
     band's bottom edge at top: 100% so the join is one shared edge, and eased
     out into the field by a vertical mask.
     THE 33 STOPS ARE MEASURED THE SAME WAY THE SKY'S ARE, at the same 160
     frame-pixel pitch, each the mean of the plate's BOTTOM 8 rows over a 160px
     window centred on that stop, rounded to the nearest step. Worst rounding
     error over the 33 is 0.488 of 255, so the ramp and the plate meet within
     half a step at every stop. The bottom 8 rows are plate and only plate: the
     lowest platform is the ship's hull at frame y 96.2 percent, 60 rows above
     the first row sampled, so no artboard is in the mean.
     THE RAMP IS AUTHORED HERE RATHER THAN ON THE HERO because it has exactly
     one consumer. --sky-ramp lives up in section 1 because two layouts read
     it; nothing above 1440 has a bottom edge to blend.
     IT IS BRIGHT AND THAT SETS THE LENGTH. The stops the window holds at the
     landing run frame x 43 to 82 percent and the brightest of them is
     rgb(69, 41, 25) at 65.6 percent, the shoreline under the ground terminal.
     Against the field's rgb(2, 3, 3) the worst channel has 67 levels to lose,
     so at one level per pixel, which is the point a linear ramp stops being
     able to band on an 8-bit screen, the run-out needs 67px. The floor is the
     step above that on the spacing scale. The proportional leg keeps the
     falloff in scale with the artwork on a larger window and takes over from
     the floor at 553px of viewport width, so the floor is the phone's figure
     and the proportion is everything above it.
     IT PANS WITH THE ARTWORK for the same reason the sky does: the stops are
     placed in the frame's own x, so the strip is a child of the stage and
     inherits the translate. The run-out under the band is the run-out
     belonging to the band's own part of the frame at every point of the move.
     WHERE THE BAND IS ON THE BOTTOM EDGE the strip is entirely below the
     window and paints nothing, which is correct: there is no seam there. */
  [data-catch="on"] .fg-hero__stage::after {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    top: 100%;
    height: max(var(--space-11), calc(var(--catch-h) * 0.18));
    pointer-events: none;
    background: linear-gradient(90deg,
      rgb(1, 1, 3) 0%,
      rgb(1, 2, 5) 3.125%,
      rgb(1, 2, 5) 6.25%,
      rgb(1, 1, 2) 9.375%,
      rgb(1, 1, 3) 12.5%,
      rgb(1, 1, 3) 15.625%,
      rgb(1, 2, 4) 18.75%,
      rgb(1, 1, 3) 21.875%,
      rgb(0, 1, 2) 25%,
      rgb(1, 1, 5) 28.125%,
      rgb(1, 1, 3) 31.25%,
      rgb(1, 3, 8) 34.375%,
      rgb(2, 3, 6) 37.5%,
      rgb(1, 2, 7) 40.625%,
      rgb(4, 8, 14) 43.75%,
      rgb(3, 9, 17) 46.875%,
      rgb(2, 6, 12) 50%,
      rgb(7, 17, 26) 53.125%,
      rgb(10, 12, 13) 56.25%,
      rgb(32, 20, 10) 59.375%,
      rgb(27, 18, 8) 62.5%,
      rgb(69, 41, 25) 65.625%,
      rgb(34, 22, 13) 68.75%,
      rgb(18, 14, 10) 71.875%,
      rgb(6, 4, 3) 75%,
      rgb(6, 4, 3) 78.125%,
      rgb(7, 3, 2) 81.25%,
      rgb(0, 0, 0) 84.375%,
      rgb(1, 0, 1) 87.5%,
      rgb(1, 1, 1) 90.625%,
      rgb(0, 1, 1) 93.75%,
      rgb(0, 0, 0) 96.875%,
      rgb(0, 0, 0) 100%);
    -webkit-mask-image: linear-gradient(180deg, #000 0%, rgba(0, 0, 0, 0) 100%);
    mask-image: linear-gradient(180deg, #000 0%, rgba(0, 0, 0, 0) 100%);
  }

  /* THE DARK FIELD, AND IT IS WHAT STAGE 1 IS MADE OF. At progress 0 the
     stage's left edge sits exactly on the viewport's right edge, so the window
     holds the hero's own ground and nothing else, and it goes on holding some
     of it on the left until the frame has walked in. It is painted the sky
     ramp's OWN FIRST STOP rather than a colour of its own, so the seam that
     crosses the screen as the pan starts is a seam between two identical
     values above the band, and between that stop and the plate's own left
     column inside it. Measured over the plate's leftmost 16 columns in 32-row
     bands, that column runs 0.5 to 5.2 red, 0.8 to 4.8 green and 0.8 to 8.4
     blue, so rgb(2, 3, 3) is within 3.2, 2.2 and 5.4 of 255 of the artwork at
     the worst band and within a step of it over most of the frame's height.
     THE RADIAL LIFT AND THE HEX LATTICE COME OFF. Both are hero ground, both
     were invisible while the band filled the width, and both would now be on
     screen for the whole of stage 1: the lift reaches rgb(14, 18, 32), which is
     29 steps of blue above the field, and the lattice is 4 percent of #7d89a3
     masked to the right 62 percent of the box, which is exactly the half of
     the dark field the composition arrives from. */
  [data-catch="on"] .fg-hero { background: rgb(2, 3, 3); }
  [data-catch="on"] .fg-hero::before { display: none; }

  /* THE BOTTOM BLEND IS THE JOIN WITH THE NEXT BAND and it stays on the
     window's own bottom edge, which is where the join is. Two things can be
     under it and it is sized for the harder of them.
     WHERE THE BAND IS CLAMPED TO THAT EDGE the blend is over the artwork, and
     the artwork is the hard case: the plate's bottom 8 rows are not uniform,
     and over a 160-frame-pixel window at frame x 3360 they mean 69 / 41 / 25
     of 255, a warm shoreline running off the bottom edge that a hard cut would
     leave as a step. But 48px of blend is 7 percent of a 648px overlay stage
     and 22 percent of a 220px portrait band, and at 22 percent it takes the
     ship's hull with it. So it is 7 percent of the band's own height, floored
     at 12px, which is the overlay's own proportion restated as a ratio instead
     of a length. Because the band's height is the frame's height, that is also
     a flat 7 percent of the FRAME at every width, and the lowest thing in the
     composition, the ship's hull at frame y 96.2 percent, sits inside it by
     the same margin everywhere.
     WHERE THE BAND IS HUNG FROM THE TOP LINE the blend is over the flat field
     instead, and it has 7 levels of red, 9 of green and 17 of blue to carry
     from rgb(2, 3, 3) to the next band's --surface-base. The same proportion
     is more than enough for that: at its 12px floor it is still under 1.5
     levels per pixel on the widest channel. The run-out under the band's own
     bottom edge is a separate blend and is on the stage, above. */
  [data-catch="on"] .fg-hero__fade { height: max(12px, calc(var(--catch-h) * 0.07)); }

  /* THE COPY SITS ON THE DARK FIELD AND NOTHING ELSE, WHICH IS WHAT MAKES ITS
     CONTRAST ONE TABLE RATHER THAN ONE PER SIZE. The copy is at zero opacity
     by --catch 0.4 and the first platform pixel does not reach the window
     until 0.495, so there is no progress at which any of it is over artwork at
     any viewport. The ground under every run is the flat rgb(2, 3, 3) above,
     whose relative luminance is 0.000846, and the figures follow from the two
     colours alone:
       run                        colour             ratio   threshold
       eyebrow 12px semibold      --accent            5.51      4.5
       headline                   --text-display     18.91      3.0
       headline accent compound   --accent            5.51      3.0
       sub 16px                   --text-muted        5.88      4.5
       secondary CTA label 14px   --text-primary     17.13      4.5
       secondary CTA hairline     --border-strong     4.12      3.0
       primary CTA fill           --accent            5.51      3.0
     The binding figure is --accent on the eyebrow. At 12px semibold it is
     still normal text, well under the 18.66px bold boundary, so it is held to
     4.5:1 and clears it by 1.01. Nothing in the column crosses the large-text
     boundary except the headline, which is measured against 3:1. The primary
     button carries an opaque --accent fill and its own label reads 5.22:1 on
     that fill, unchanged by anything here. Ratios are quoted at full opacity,
     which is the state the copy is read in; below it the copy is leaving.
     NO SCRIM AND NO ASSIST WASH. There is nothing under the copy to hide.
     THE COPY IS TAKEN OUT OF FLOW rather than restructured. It is the same
     block, the same container and the same four text nodes; only its box
     changes, from a block that sets the hero's height to a band pinned across
     its top. The header allowance moves here off the hero for the reason the
     overlay gives: the stage is anchored to the hero's border box, so vertical
     padding on the hero would move the seam at the top of the artwork.
     THE CLEARANCE IS --catch-top AND THE BAND READS THE SAME NUMBER. The copy
     starts on the shared line derived above: the header's own allowance plus
     air from the spacing scale, which is 121px on a handset and 49px of clear
     daylight under a header that measures 72. It is stated once as a token
     sum rather than as a length here, so the copy and the composition cannot
     end up on two different lines. */
  [data-catch="on"] .fg-hero__body {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    padding-top: var(--catch-top);

    /* THE EXIT IS A FADE AND A LIFT AND THEY SHARE ONE RAMP. 6svh of travel
       against a full fade: enough that the copy reads as leaving rather than
       dissolving on the spot, small enough that nothing appears to be thrown.
       Both finish at --catch 0.4, which is what buys the pan a tenth of the
       runway of clear dark before the composition starts to arrive.
       will-change is named rather than implied because both properties move on
       every frame of the runway and the copy is the largest text block on the
       page; without it the block is re-rasterised on each step. */
    opacity: calc(1 - var(--catch-copy));
    transform: translate3d(0, calc(var(--catch-copy) * -6svh), 0);
    will-change: opacity, transform;
  }

  /* THE CONTROLS STAND DOWN ONCE THE COPY HAS GONE. pointer-events is not an
     interpolable property, so this is the one thing the progress number cannot
     drive and the one thing home.js reports as a state instead: it sets
     data-catch-stage="2" past the same 0.4 the fade ends at. Without it the
     two hero buttons stay live at full size over the middle of the scene,
     invisible, swallowing taps meant for the platforms behind them. */
  [data-catch-stage="2"] .fg-hero__copy .fg-btn { pointer-events: none; }

  /* THE SIGNALS ARRIVE OVER THE LAST QUARTER OF THE PAN. Element opacity on
     the band layers only, which composes cleanly with the stroke-opacity their
     own keyframes are already animating and needs no change to the cadence
     table: each signal keeps its own cycle, its own phase and its own
     direction, and what --catch-sig does is decide how much of it is on
     screen. 0.75 is where the terminal is coming into the window and the ship
     and the aircraft are already in it, so the pulses arrive on connections
     that are there to carry them rather than on empty sky.
     THE TRACES AND THEIR STANDING GLOW ARE DELIBERATELY NOT IN THIS RULE. They
     do not move, they are what makes the connections read as structure rather
     than as something that only exists while a pulse is on it, and they belong
     to the artwork the pan is uncovering the same way the platforms do. What
     comes to presence over the last quarter is the motion. */
  [data-catch="on"] .fg-conn__band { opacity: var(--catch-sig); }
}

/* ==========================================================================
   2. THE ELECTRONICS BACKBONE
   Eight numbered nodes on a hairline rail. Numerals are Barlow Condensed in
   the accent; the glyph sits in a hairline square; the rail carries a slow
   orange pulse. Hover lights a node and sweeps a thin orange line through it.
   ========================================================================== */

.fg-backbone-band { overflow: hidden; }

/* --------------------------------------------------------------------------
   COLLAPSED DEFAULT AND THE LEARN MORE UNFOLD (v2, spec 2026-08-01)

   The section opens as four lines of copy and one button at every width. The
   chain, its rail and its hint are inside a closed accordion, and one click
   opens them for good.

   PROGRESSIVE ENHANCEMENT IS THE WHOLE GATE. Every rule that HIDES anything
   here is prefixed html[data-fg-js="true"], the attribute motion.js writes the
   moment it runs. A visitor without scripting therefore gets the finished
   section: no button (it would do nothing), the accordion open at auto height,
   the chain drawn and its ambient loop running. That last part needed the
   draw-in's resting states moved behind the same gate, which is what the two
   rules further down on .fg-backbone__rail and .fg-node do; before this pass a
   no-JS visitor saw an empty band, because opacity: 0 sat unconditionally on
   every node.

   THE ACCORDION IS THE MOBILE NAV'S, VERBATIM. grid-template-rows 0fr to 1fr
   with overflow: hidden on the single grid child. It is the one technique in
   the kit that animates to CONTENT height without a measured pixel value, and
   the chain's height is not knowable in CSS: it is one row at 1024 and up and
   four at 1023 and down, and the labels wrap differently at every width.
   620ms, --dur-slower, which is the same duration the rail draw takes, so the
   box and the line inside it arrive together.

   THE BUTTON COLLAPSES RATHER THAN JUST FADING. A fade alone would leave its
   box behind and the chain would open into a hole; the same 0fr technique run
   backwards takes the space with it. visibility follows the kit's --t-panel
   convention (0s linear, delayed by the duration) so the control leaves the
   tab order and the accessibility tree exactly when it finishes fading, and
   the reveal stays one-way with nothing left to click.
   NO MARGIN ANYWHERE IN THE CTA, and that is the reason the collapse lands on
   nothing. A margin on the grid item is outside the row the accordion is
   shrinking, so it would survive at 0fr and leave a gap where the button was.
   The section head's own margin-bottom already sets the distance to whatever
   follows it, which is the button while it is there and the chain once it is
   not, so there is nothing for a second value to add.
   -------------------------------------------------------------------------- */

.fg-backbone__cta { display: none; }

html[data-fg-js="true"] .fg-backbone__cta {
  display: grid;
  grid-template-rows: 1fr;
  transition: grid-template-rows var(--dur-base) var(--ease-out-expo),
              opacity var(--dur-base) var(--ease-out-expo),
              visibility 0s linear 0s;
}
html[data-fg-js="true"] .fg-backbone__cta > div {
  display: flex;
  justify-content: center;
  overflow: hidden;
}
html[data-fg-js="true"] .fg-backbone__cta[data-open="false"] {
  grid-template-rows: 0fr;
  opacity: 0;
  visibility: hidden;
  transition: grid-template-rows var(--dur-base) var(--ease-out-expo),
              opacity var(--dur-base) var(--ease-out-expo),
              visibility 0s linear var(--dur-base);
}

.fg-backbone__reveal { display: grid; }

html[data-fg-js="true"] .fg-backbone__reveal {
  grid-template-rows: 0fr;
  transition: grid-template-rows var(--dur-slower) var(--ease-out-expo);
}
html[data-fg-js="true"] .fg-backbone__reveal-inner { overflow: hidden; }
html[data-fg-js="true"] .fg-backbone__reveal[data-open="true"] { grid-template-rows: 1fr; }

/* THE AMBIENT LOOP IS GATED ON .is-live, which home.js adds when the unfold
   finishes. Without it the rail sweep, the node bloom, the node ring and the
   serpentine signal would all be part way through their 7.6s and 10.8s cycles
   by the time the chain appeared, and the first thing a visitor saw would be a
   light already half way down a line they had not been shown yet.
   animation-name: none rather than a paused play state, because paused is a
   state the motion control owns and this is not that: there is nothing running
   to freeze, the loop has not started. The gate carries the same
   html[data-fg-js="true"] prefix as everything else in this block, so a no-JS
   visitor, who will never receive .is-live, keeps the loop it always had. */
html[data-fg-js="true"] .fg-backbone:not(.is-live) .fg-backbone__pulse,
html[data-fg-js="true"] .fg-backbone:not(.is-live) .fg-backbone__sig,
html[data-fg-js="true"] .fg-backbone:not(.is-live) .fg-node__mark::before,
html[data-fg-js="true"] .fg-backbone:not(.is-live) .fg-node__mark::after {
  animation-name: none;
  opacity: 0;
}

/* DESKTOP SIZE STEP, roughly 1.25x on every dimension of the component. The
   chain reads too small for the weight it carries in the page, and the fix is
   real size values rather than a transform: a scaled transform would blur the
   numerals and the 10px label, and would leave the grid, the rail and the
   hit areas at their old geometry.
     node square      48 -> 60      numeral row      30 -> 38
     numeral          15 -> 19      icon glyph       21 -> 26
     label            10 -> 12.5    label cap       110 -> 138
     label offset     12 -> 16      node padding   0/4/8 -> 0/5/10
     grid gap          4 -> 5       bloom inset     -18 -> -22
   Two things deliberately do NOT scale. The glyph stroke stays 1.5px: this is
   a thin-line icon system, and a bigger icon drawn at the same weight is the
   point of one. The rail stays a 1px hairline for the same reason; what moves
   there is its vertical position, which is written as a formula in --num-h
   and --mark and so follows on its own.
   Values live in the base rules because the base layout IS the horizontal
   one; the serpentine block at 1023px and down puts the smaller set back.

   FIT AT THE TIGHT END. The horizontal layout starts at 1024px, where the
   gutter is at its 24px ceiling, so the container gives 976px. Eight tracks
   and seven 5px gaps make each track 117.6px, less 10px of node padding is
   107.6px of content. The 60px square clears it outright; the widest word any
   label has to set unbroken is TRANSLATE, about 82px of Inter bold caps at
   12.5px with the 0.06em tracking, so there is roughly 25px of headroom at
   the worst width. No step-down band is needed.
   Note the label cap stops binding on the way up: at 1200px of container the
   column only offers 135.6px, so 138px is a ceiling rather than the active
   constraint. That is intended. The cap grew with the type precisely so the
   labels keep the line counts they had, just larger. */
.fg-backbone {
  --mark: 60px;      /* node square, back to 48 on the serpentine */
  --num-h: 38px;     /* numeral row above the square, 30 on the serpentine */
  position: relative;
  margin-top: clamp(8px, 0.4rem + 1vw, 24px);
}

/* Rail. Spans centre-of-first-node to centre-of-last-node, which is half a
   column in from each edge of the eight-column grid. */
.fg-backbone__rail {
  position: absolute;
  z-index: 0;
  left: 6.25%;
  right: 6.25%;
  top: calc(var(--num-h) + (var(--mark) / 2));
  height: 1px;
  overflow: hidden;
  background: var(--border-hairline);
  /* CENTRE, NOT LEFT. The v2 unfold runs from the inside out: the rail opens
     from the middle towards both ends and the nodes light up in step with it,
     4 and 5 first and 1 and 8 last. The finished state is scaleX(1) either
     way, so the desktop look after the reveal is untouched; what changed is
     the direction of the 620ms that gets there.
     The resting scaleX(0) is on the JS-gated rule below, so a visitor without
     scripting, who will never receive .is-drawn, sees the rail. */
  transform-origin: center;
  transition: transform var(--dur-slower) var(--ease-out-expo);
}
html[data-fg-js="true"] .fg-backbone__rail { transform: scaleX(0); }

/* Slow travelling pulse. Driven by background-position so it stays correct
   at any rail width. Decorative: no contrast requirement.

   Sweep time and gap time are set independently, which needs a word because
   one duration appears to control both. Three numbers do the work:
     background-size S   the gradient image as a multiple of the rail width
     the stop span       how much of that image is lit, so the band width is
                         span x S in rail widths
     the keyframe range  which sets total travel, at 2 x (S - 1) x range
   The band is ON SCREEN for the visible window (1 band width either side of
   the rail, so 1 + band) over that total travel. Duration then only scales
   whatever ratio those produce. So geometry sets the split and duration sets
   the pace:
     first  260%, 42-53, -170% to 170%, 7s     travel 5.44, 23.6% lit
                                               sweep 1.65s, gap 5.35s
     then   260%, 42-53, -100% to 100%, 6.2s   travel 3.20, 40.2% lit
                                               sweep 2.49s, gap 3.71s
     now    200%, 40-54, -125% to 125%, 7.6s   travel 2.50, 51.2% lit
                                               sweep 3.89s, gap 3.71s
   The gap is held to the frame it already had while the sweep itself takes
   1.56x longer to cross: same rhythm of arrival, a much slower glide.

   WHY THE GEOMETRY HAD TO MOVE for this step and not the last one. At 260%
   the band cannot exit the left edge before the loop restarts unless the
   half-travel is at least 0.53 x S, which caps the lit fraction at 46.7%.
   That is short of the 51.2% this pace needs, so pushing the range alone
   would have snapped the sweep off mid-rail. Dropping S to 200% lifts the
   cap; widening the stops from 42-53 to 40-54 holds the lit band at 0.28 of
   a rail width, against 0.286 before, so the sweep looks the same width as
   it always did.

   DIRECTION. The signal runs the chain in reading order, node 01 to node 08:
   left to right along the row, top to bottom down the stack. Both variants
   used to run the other way, and the reason is easy to miss. A background
   position percentage resolves against (container - image), which is
   NEGATIVE whenever the image is larger than the box, so the sign of the
   keyframe value is the opposite of the sign of the travel. Going from -125%
   to 125% put the image at +1.25 rail widths and walked it to -1.25, which
   is right to left. The range is now written 125% to -125% on both axes, and
   the band centre runs -0.31 up to 2.19: it enters at the 01 end and leaves
   past the 08 end. The element carries [data-ambient], so the pause control
   and reduced motion still stop it. */
.fg-backbone__pulse {
  position: absolute;
  inset: 0;
  background-image: linear-gradient(90deg,
    rgba(227, 85, 48, 0) 0%,
    rgba(227, 85, 48, 0) 40%,
    var(--accent) 47%,
    rgba(227, 85, 48, 0) 54%,
    rgba(227, 85, 48, 0) 100%);
  background-size: 200% 100%;
  background-repeat: no-repeat;
  animation: fg-backbone-pulse 7.6s linear infinite;
}

/* Left to right, node 01 to node 08. Because the sign inverts (see above),
   125% is what puts the image LEFT of the rail and -125% is what puts it
   right, so this reads backwards and is correct. The range clears the rail at
   both ends with room to spare: at the start the band centre sits 0.31 rail
   widths short of the left edge, at the end it is 1.19 past the right,
   against a band half-width of 0.14. */
@keyframes fg-backbone-pulse {
  0%   { background-position:  125% 0; }
  100% { background-position: -125% 0; }
}

/* --------------------------------------------------------------------------
   THE SERPENTINE ROUTE, 1023px AND DOWN

   Below 1024 the chain is a 2 x 4 grid and the straight rail cannot serve it,
   so the rail and its background-position sweep are switched off and this SVG
   takes over. It is the hero's connection-band system, unchanged in
   construction and scaled to this rail: one path in a defs route carrying
   pathLength="1", a hairline trace on top of it, and a twelve-layer dash band
   with a blurred halo behind, every layer centred on the same point of the
   path so one dashoffset animation drives all thirteen.

   THE PATH IS A POLYLINE THROUGH THE EIGHT NODE CENTRES, in numeric order, and
   because the grid places 04 and 03 in row 2 reversed and 08 and 07 in row 4
   reversed, that order snakes: across row 1 left to right, down the right
   edge, back across row 2, down the left edge, and so on. Node 01 is where the
   light is born and node 08 is where it dies; the path stops at both centres
   rather than overhanging, which is what makes the arc-length fractions come
   out symmetric (f and 1 - f pair off across the middle).

   RECTILINEAR TURNS. The spec left the corner treatment as a build-time visual
   call with square corners as the stated default, on the grounds that the
   stages are drawn as boxes and an arc through a grid of squares fights them.
   Square it is, with stroke-linejoin: round so the band does not throw a miter
   spike as it comes round a corner.

   THE VIEWBOX IS SET IN PIXELS BY home.js, from the measured grid, so one user
   unit is one CSS pixel and every stroke width below is a real pixel width.
   The literals in the markup are the same geometry at the 520px cap the grid
   takes from 568px of viewport up, so the shape is right before the script
   runs and for a visitor whose script never runs; preserveAspectRatio="none"
   is what lets that fallback stretch to a narrower grid, and the only cost is
   that horizontal and vertical strokes scale by different factors in that one
   ungated case.
   -------------------------------------------------------------------------- */

.fg-backbone__route {
  display: none;
  position: absolute;
  z-index: 0;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
  pointer-events: none;
  /* Band length as a fraction of the whole route. The old vertical rail ran a
     band of 0.40 of a 792px rail, about 317px of light. The serpentine is
     1484px at the cap, so 0.20 of it is 297px: the same amount of line lit,
     which is a little under two node pitches, on a route nearly twice as
     long. */
  --sm: 0.20;
}

/* The line itself, at the 3px the vertical rail used and in the same hairline
   ink. 1px read as almost nothing on a phone and the band can only carry as
   much light as the line it travels down; neither of those facts changed.
   IT DRAWS FROM THE CENTRE OUTWARDS, which is the serpentine's answer to the
   desktop rail's scaleX from centre. A dash of length D centred on the middle
   of the path is dasharray D (with a gap longer than the path so there is
   exactly one dash) at dashoffset D/2 - 0.5, and BOTH of those are linear in
   D, so transitioning the two properties together traces the growth exactly,
   under any easing, with no keyframes and no measured length.
   Both ends of both transitions are written as lengths. A bare number and a
   calc that reaches a length are not interpolable and the engine falls back to
   a discrete flip half way through; the hero's signal block hit that and the
   note there is the long version. In SVG user space 1px is one user unit, and
   pathLength normalises the route to 1, so 1px here means the whole route. */
.fg-backbone__serp {
  fill: none;
  stroke: var(--border-hairline);
  stroke-width: 3;
  stroke-linecap: butt;
  stroke-linejoin: round;
  stroke-dasharray: 1px 2px;
  stroke-dashoffset: 0px;
  transition: stroke-dasharray var(--dur-slower) var(--ease-out-expo),
              stroke-dashoffset var(--dur-slower) var(--ease-out-expo);
}
html[data-fg-js="true"] .fg-backbone__serp {
  stroke-dasharray: 0px 2px;
  stroke-dashoffset: -0.5px;
}

/* The band. Thirteen layers, the hero's profile divided by the 1.8 that
   mapped it onto the enlarged hero frame, which puts it back at the widths it
   was authored at: a long faint 3.08px tail through to a short bright 5.04px
   core, with a 14px blurred halo behind the whole thing. Against a 3px trace
   the core overhangs slightly, which is what makes the light read as sitting
   ON the line rather than as a segment of it.
   The dash pattern is the hero's verbatim: a zero-length dash, a gap of half
   the difference between the widest layer and this one, the layer itself, then
   a gap of 2, which is longer than the normalised path and so guarantees
   exactly one dash. Every layer's centre lands at --sm/2 minus the dashoffset,
   so one animation moves all thirteen as one object. */
.fg-backbone__sig {
  fill: none;
  stroke: rgba(227, 85, 48, var(--sa));
  stroke-width: var(--sw);
  stroke-linecap: butt;
  stroke-linejoin: round;
  stroke-dasharray: 0 calc((var(--sm) - (var(--sm) * var(--sf))) / 2)
                    calc(var(--sm) * var(--sf)) 2;
  stroke-dashoffset: calc(var(--sm) * 1px);
  animation: fg-backbone-serp 10.8s linear infinite;
}
.fg-backbone__sig--core { stroke: rgba(236, 100, 70, var(--sa)); }
.fg-backbone__sig--halo { filter: blur(4px); }

/* THE CYCLE IS 10.8s AND IT IS DERIVED, not chosen. The vertical rail moved
   its band centre 2.50 rail heights, 1980px, per 7.6s cycle: 260.5 px/s on
   screen, and that is the pace a phone was tuned to. The serpentine is 1484px
   at the cap and the dashoffset sweep carries the band centre 1 + 1.5 x --sm
   route lengths, 1.30 of them, so the travel is 1929px. At 260.5 px/s that is
   7.40s of sweep. Holding the vertical variant's 3.34s of dark after it gives
   10.74s, written 10.8s, and the sweep is then 68.5% of the cycle.
   So the light on a longer chain takes longer to walk it, at the speed it
   always walked, and rests for the same beat before starting again.
   No opacity ramp, and it needs none: at 0% the band centre sits at -sm/2, a
   full half-band before the route starts, and at 68.5% it is at 1 + sm, a full
   band past the end. Nothing is drawn at either, so there is no edge to hide.
   The dashoffset is written calc(... * 1px) at every stop for the same
   interpolation reason as the trace above. */
@keyframes fg-backbone-serp {
  0%     { stroke-dashoffset: calc(var(--sm) * 1px); }
  68.5%  { stroke-dashoffset: calc((-1 - (var(--sm) / 2)) * 1px); }
  100%   { stroke-dashoffset: calc((-1 - (var(--sm) / 2)) * 1px); }
}

/* Every layer carries data-ambient in the markup, so the kit's global switch
   freezes the band mid-route on pause and removes it outright on reduced
   motion, leaving the trace drawn. This is the same statement for a visitor
   whose preference never reaches the attribute; the compound selector is
   specificity, matching the hero's guard. */
@media (prefers-reduced-motion: reduce) {
  .fg-backbone .fg-backbone__sig { animation: none; }
}

.fg-backbone__nodes {
  position: relative;
  z-index: 1;
  display: grid;
  grid-template-columns: repeat(8, minmax(0, 1fr));
  gap: 5px;
}

.fg-node {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 0 5px 10px;
  background: none;
  border: 0;
  font: inherit;
  color: inherit;
  text-align: center;
  cursor: pointer;
  transform-origin: center;
  /* The lift is written as the `scale` PROPERTY, not as transform: scale(),
     and that is load bearing. `transform` on this element is already the
     draw-in translate, and .is-drawn resets it to none; a transform: scale()
     hover rule would have to fight both and would inherit the stagger delay
     with them. As its own property scale composes with whatever transform is
     current, and takes its own duration and its own delay slot. transform-origin
     governs it exactly as it governs transform, so the growth is centred.
     220ms is written out because the kit has no token at that value; the
     nearest are 200 and 280. Third slot in the list, third slot in every
     delay list. */
  transition: opacity var(--dur-slow) var(--ease-entrance),
              transform var(--dur-slow) var(--ease-entrance),
              scale 220ms var(--ease-out-quart);
}

/* The resting, undrawn state, behind the progressive-enhancement gate. It used
   to sit unconditionally on .fg-node, which meant a visitor without scripting
   was served eight invisible buttons and an empty band, because .is-drawn is
   added by home.js and by nothing else. The gate is the same one the reveal
   kit uses and the same one the accordion above uses.
   The rise is shared by both layouts now. The vertical rail used to slide its
   nodes in from the left, which suited a stack against a left-hand rail and
   suits nothing on a 2 x 4 grid; a rise reads correctly in both. */
html[data-fg-js="true"] .fg-node {
  opacity: 0;
  transform: translate3d(0, 10px, 0);
}

/* The orange sweep line rides the node's own centre line, so it reads as a
   signal passing through the chain rather than through the label. */
.fg-node.fg-signal-sweep::after { top: calc(var(--num-h) + (var(--mark) / 2)); }

/* Numeral. Barlow Condensed bold in the accent: a display numeral, and
   --accent measures 5.22:1 on --surface-base in any case. */
.fg-node__num {
  display: flex;
  align-items: center;
  justify-content: center;
  height: var(--num-h);
  font-family: var(--font-display);
  font-weight: var(--fw-display);
  font-size: 19px;
  line-height: 1;
  letter-spacing: 0.1em;
  color: var(--accent);                /* 5.22:1 on --surface-base */
  transition: var(--t-color);
}

.fg-node__mark {
  position: relative;
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--mark);
  height: var(--mark);
  background: var(--surface-base);
  border: var(--border-hairline-w) solid var(--border-hairline-strong);
  transition: var(--t-color), box-shadow var(--dur-quick) var(--ease-standard);
}

/* Raised one step so the idle bloom below passes BEHIND the icon rather than
   over it. Nothing else about the glyph changes, so the hover and selected
   colour rules are untouched. */
.fg-node__glyph {
  position: relative;
  z-index: 1;
  width: 26px;
  height: 26px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.5;
  color: var(--stroke-quiet);          /* non-text stroke, 4.50:1 on base */
  transition: var(--t-color);
}

/* Idle bloom. As the rail's sweep band passes behind a node, that node warms
   for a beat: the box face and the air around it lift together, the icon
   reads as a silhouette against the lift, and none of the node's own ink
   moves. The metaphor is light passing behind glass, which is why the layer
   sits under the glyph and never paints on the stroke.

   ITS OWN ELEMENT, DELIBERATELY. box-shadow, border-color and color on the
   mark and the glyph are all owned by the hover, focus and selected rules
   below, and a keyframe animation on any of them would win the cascade
   outright and swallow those states. A dedicated pseudo-element touches none
   of them, so the bloom composes with hover rather than competing with it,
   and it is quieter by construction: peak 0.10 on a soft radial falloff with
   no edge anywhere, against hover's hard 4px 0.10 ring PLUS an accent border
   PLUS a fully orange glyph PLUS a display-white label, and against the
   selected state's 22px 0.35 shadow on a solid accent fill.

   PURELY DECORATIVE, and it stays that way. It lives inside .fg-node__mark,
   which the page template marks aria-hidden, so it is nowhere near the node's
   accessible name. The node's one piece of real text, .fg-node__label, sits
   12px below the mark where this gradient has already fallen to about 0.01:
   --text-muted moves 5.57:1 to 5.53:1 there, still AA. The glyph it sits
   behind is a non-text stroke and the backdrop lift takes it from 4.50:1 to
   4.15:1 at the peak, against a 3:1 minimum for non-text.

   TIMING. The cycle is the sweep's own 7.6s, and the delay puts each node's
   peak on the crossing instant.

   ONE FACT CARRIES THE SCHEME ON THE RAIL. The band CENTRE's trajectory falls
   out of the background-size and the 47 percent mid stop alone, so the centre
   runs -0.31 to 2.19 rail lengths and a crossing instant is a function of node
   POSITION only. Sweep duration never enters a delay; what a wider band
   changes is DWELL, and that is the envelope's business, below.
   BELOW 1024 THIS RULE DOES NOT APPLY AT ALL. The vertical rail it used to
   serve was replaced by the 2 x 4 serpentine, which is an SVG route rather
   than a background-position band, and its delays are a calc over the
   per-node --f fraction instead of a step off --i. The derivation lives in the
   serpentine block at the foot of this section; everything from here to the
   end of this note is the DESKTOP rail and only that.

   Desktop: node i sits at i/7 along the rail, crossing at (i/7 + 0.31) / 2.5
   of the cycle, and the keyframes peak at the halfway mark, so the delay is
   that crossing minus half a cycle:
     node 0  -2.858s      node 4  -1.120s
     node 1  -2.423s      node 5  -0.686s
     node 2  -1.989s      node 6  -0.252s
     node 3  -1.555s      node 7   0.182s
   which is -2.858s stepped up by 0.4343s a node, read off the --i the markup
   already carries. i/7 is an idealisation, because the 5px grid gaps pull the
   real track centres slightly inward. Checked against real track maths at
   976, 1053 and 1200px of container, the worst peak error is 8.1ms against a
   1.06s envelope, so the shared formula stands here. Most delays come out
   negative because the band reaches node 0 early in the cycle; a negative
   delay simply starts that node part-way through its own cycle, which holds
   it in phase from the first pass rather than after a full turn.

   ENVELOPE, AND THE DEFECT THAT WAS FOUND IN IT. The glow has to answer the
   light, so its width has to track how long the band actually DWELLS on a
   node, which is band width over travel: 0.28 / 2.5 of a cycle, 0.851s, on
   desktop. The 43 to 57 envelope is 14 percent, 1.064s, which is 1.25x that
   dwell, and the 0.106s of margin at each end is what makes the glow lead the
   light in and outlast it going out instead of blinking inside it.
   The 1.25x ratio is the part that travels. It is what the serpentine's own
   envelope is built from too, against its own dwell and its own cycle, so the
   glow reads the same on a phone as it does on a desktop even though not one
   number is shared between them. */
.fg-node__mark::before {
  content: "";
  position: absolute;
  inset: -22px;
  pointer-events: none;
  background: radial-gradient(closest-side,
    rgba(227, 85, 48, 0.10) 0%,
    rgba(227, 85, 48, 0.075) 30%,
    rgba(227, 85, 48, 0.035) 60%,
    rgba(227, 85, 48, 0) 100%);
  opacity: 0;
  animation: fg-node-bloom 7.6s var(--ease-in-out) infinite;
  animation-delay: calc((var(--i, 0) * 0.4343s) - 2.858s);
}

@keyframes fg-node-bloom {
  0%,  43%  { opacity: 0; }
  50%       { opacity: 1; }
  57%, 100% { opacity: 0; }
}

/* The pause control and reduced motion reach this layer explicitly, because
   the kit's kill switch cannot. That switch selects [data-ambient] and
   .fg-ambient ELEMENTS, and animation-play-state is not an inherited
   property, so tagging the originating element would not carry to its
   pseudo-element. These rules key off the identical html[data-motion] state
   motion.js writes, and mirror what the kit does to the rail pulse: paused
   freezes the layer where it stands, reduced removes the animation outright.
   Same control, same states, same result. */
html[data-motion="paused"] .fg-node__mark::before { animation-play-state: paused; }
html[data-motion="reduced"] .fg-node__mark::before { animation: none; opacity: 0; }

/* No-JS guard, matching the hover-scale rule further down. The bare selector
   covers a page where motion.js never ran; the second one carries the same
   specificity as the paused rule above and follows it in source order, so a
   reduced-motion visitor can never be left frozen mid-bloom. */
@media (prefers-reduced-motion: reduce) {
  html .fg-node__mark::before,
  html[data-motion] .fg-node__mark::before { animation: none; opacity: 0; }
}

/* Edge ring. The bloom says a node is being passed; this says the band is
   passing THROUGH it. It is a 2px light tracing the mark's perimeter, entering
   at the left edge as the band arrives, threading the hairlines, leaving at the
   right. Brighter and far tighter than the bloom, and the two are meant to be
   read together: a soft swell with a hard line moving inside it.

   THE RING IS A MASK, not a border. ::after is stretched to the mark's border
   box (inset -1px against a 58px padding box gives 60px, the mark's outer
   edge) and given 2px of padding. Two full-coverage mask layers, one clipped
   to the content box and one to the border box, composited xor / exclude,
   cancel everywhere they overlap and leave exactly the 2px perimeter. The
   travelling gradient is painted behind that mask, so only the part of it
   crossing the perimeter is ever seen. This depends on the kit's global
   border-box (components.css line 16); .fg-node__mark sets no box-sizing of
   its own, so it holds. mask-composite must follow the mask shorthand, which
   resets it.

   WINDOW SIZED TO DWELL, NOT TO THE BLOOM. The bloom's envelope is
   deliberately 1.25x the band's dwell so the glow brackets the light. The ring
   is the light, so its window matches the dwell instead:
     desktop    band 0.28 rail  / 2.5 travel = 0.851s dwell = 11.2% of cycle
                full 44 to 56 = 12% = 0.912s, ramps at 43 and 57
     serpentine band 0.20 route / 1.3 travel = 1.138s dwell = 10.5% of cycle
                full 45 to 55 = 10% = 1.080s, ramps at 44 and 56
   Peak still sits at 50 percent, so both variants reuse their bloom delay
   unchanged and the ring's centre crossing lands on the band's.

   The 220% background and the 120 to -20 position range put the bright core
   at -0.34 of the box at the open, dead centre at 50 percent, and +1.34 at
   the close: left to right, matching the sweep.

   INTENSITY, which took a second pass. This was first built at a 0.9 peak
   with a warm-white core and opaque accent flanks, and at 2px on a 60px
   square it stopped being a glint and became a lit band. It is now pulled
   down on all three levers at once rather than one hard one: the peak to 0.3
   (0.35 on mobile, which carries a 3px ring on a heavier rail and can afford
   it), the core off white to a barely-lighter warm accent, and the flanking
   stops to 0.55 alpha. Effective peak is about 0.30 at the core and 0.17
   through the shoulders, against 0.9 and 0.9 before.

   WIDTH AND FALLOFF, which took three passes, and the third found the real
   constraint. Widening only the outer tails did nothing visible: at the
   ring's 0.3 element opacity, gradient alpha below about 0.5 lands under
   0.15 on screen, and a 2px line on this ground cannot show that. The
   visible segment is set by where the gradient alpha stays ABOVE roughly
   0.5, not by where it reaches zero. The profile therefore holds 0.55 or
   more from 34 to 66 percent of the gradient, about seven tenths of the box
   width, doming through 0.75 to the core so the light still has a centre,
   with short feathered tails to zero at 28 and 72. At the centre crossing
   the segment reaches nearly the full edge. Both variants share this
   profile. Peak alpha, line weight, motion, windows, delays and guards are
   untouched throughout.
   Decorative throughout: the mark is aria-hidden in the page template, so nothing
   here carries text. */
.fg-node__mark::after {
  content: "";
  position: absolute;
  inset: -1px;
  padding: 2px;
  pointer-events: none;
  background-image: linear-gradient(90deg,
    rgba(227, 85, 48, 0) 0%,
    rgba(227, 85, 48, 0) 28%,
    rgba(227, 85, 48, 0.25) 31%,
    rgba(227, 85, 48, 0.55) 34%,
    rgba(227, 85, 48, 0.75) 42%,
    #ec6446 50%,
    rgba(227, 85, 48, 0.75) 58%,
    rgba(227, 85, 48, 0.55) 66%,
    rgba(227, 85, 48, 0.25) 69%,
    rgba(227, 85, 48, 0) 72%,
    rgba(227, 85, 48, 0) 100%);
  background-size: 220% 100%;
  background-repeat: no-repeat;
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask-composite: exclude;
  opacity: 0;
  animation: fg-node-ring 7.6s linear infinite;
  animation-delay: calc((var(--i, 0) * 0.4343s) - 2.858s);
}

@keyframes fg-node-ring {
  0%,  43%  { opacity: 0;   background-position: 120% 0; }
  44%       { opacity: 0.3; }
  50%       { background-position: 50% 0; }
  56%       { opacity: 0.3; }
  57%, 100% { opacity: 0;   background-position: -20% 0; }
}

/* GUARDS. The first is the ring's own: a selected node already states itself
   with an accent fill and a 22px shadow, and a light running its edge on top
   of that reads as a glitch. The rest duplicate the bloom's coverage exactly,
   including the html prefix on the bare reduced-motion selector, which is what
   keeps it ahead of the lower-specificity vertical override below.
   Hover and focus are untouched on purpose and need no rule: they own
   border-color and box-shadow on .fg-node__mark itself, and this ring sets
   neither, on a different element. */
.fg-node[aria-expanded="true"] .fg-node__mark::after { animation: none; opacity: 0; }

html[data-motion="paused"] .fg-node__mark::after { animation-play-state: paused; }
html[data-motion="reduced"] .fg-node__mark::after { animation: none; opacity: 0; }

@media (prefers-reduced-motion: reduce) {
  html .fg-node__mark::after,
  html[data-motion] .fg-node__mark::after { animation: none; opacity: 0; }
}

.fg-node__label {
  margin-top: var(--space-4);
  max-width: 138px;
  font-family: var(--font-body);
  font-size: 0.78rem;   /* 12.5, the 1.25 step off the kit's 10px --fs-3xs */
  font-weight: var(--fw-bold);
  line-height: 1.3;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-muted);            /* 5.57:1 on --surface-base */
  transition: var(--t-color);
}

/* Hover and keyboard focus light the node, and lift it a step. The lift is
   interaction feedback rather than ambient motion, so it survives the paused
   state; reduced motion drops it and leaves the colour change to carry the
   state on its own. 1.05 over 220ms is a settle, not a bounce: the easing
   has no overshoot and nothing glows. The z-index bump is what keeps the
   grown node from being clipped by its neighbours, whose grid gap is 4px
   against roughly 7px of growth. */
.fg-node:hover,
.fg-node:focus-visible {
  z-index: 2;                          /* the lifted node stays over its neighbours */
  scale: 1.05;
}
html[data-motion="reduced"] .fg-node:hover,
html[data-motion="reduced"] .fg-node:focus-visible { scale: 1; }

@media (prefers-reduced-motion: reduce) {
  .fg-node:hover,
  .fg-node:focus-visible { scale: 1; }
}

.fg-node:hover .fg-node__mark,
.fg-node:focus-visible .fg-node__mark {
  border-color: var(--border-accent);  /* 5.22:1 on base, clears 3:1 */
  box-shadow: 0 0 0 4px rgba(227, 85, 48, 0.10);
}
.fg-node:hover .fg-node__glyph,
.fg-node:focus-visible .fg-node__glyph { color: var(--accent); }
.fg-node:hover .fg-node__label,
.fg-node:focus-visible .fg-node__label { color: var(--text-display); }

/* Selected. The mark inverts to the accent fill with near-black ink, which
   is the one place the chain reads as "you are here". */
.fg-node[aria-expanded="true"] .fg-node__mark {
  background: var(--accent);
  border-color: var(--accent);
  box-shadow: 0 0 22px rgba(227, 85, 48, 0.35);
}
.fg-node[aria-expanded="true"] .fg-node__glyph { color: var(--surface-ink); }   /* 5.22:1 on --accent */
.fg-node[aria-expanded="true"] .fg-node__label { color: var(--accent); }        /* 5.22:1 on base */

/* Draw-in. The chain assembles rather than appearing, and since v2 it
   assembles FROM THE INSIDE OUT: the rail opens from its middle towards both
   ends over 620ms, and the nodes light up in rings around that middle, 4 and 5
   together, then 3 and 6, then 2 and 7, then 1 and 8.
   --d IS A MARKUP LITERAL, and deliberately not computed. The spec's stagger
   is proportional to |i - 3.5|, which takes the values 3.5, 2.5, 1.5 and 0.5;
   subtracting the constant 0.5 leaves the plain step counts 3, 2, 1, 0 that
   --d carries. CSS abs() would express it in one line and its support baseline
   is too recent to rely on here: a browser that does not parse it drops the
   whole declaration and the stagger disappears silently. One extra number per
   node costs nothing and cannot fail.
   90ms of lead plus 90ms a ring puts the outermost pair in motion at 360ms and
   finished at 780ms, against a rail that is essentially complete by 220ms on
   the expo curve. The line arrives first, the stages fill in behind it.
   Third slot is the hover scale, which never takes the stagger. */
.fg-backbone.is-drawn .fg-backbone__rail { transform: scaleX(1); }
.fg-backbone.is-drawn .fg-backbone__serp {
  stroke-dasharray: 1px 2px;
  stroke-dashoffset: 0px;
}
.fg-backbone.is-drawn .fg-node {
  opacity: 1;
  transform: none;
  transition-delay: calc(90ms + (var(--d, 0) * 90ms)),
                    calc(90ms + (var(--d, 0) * 90ms)),
                    0s;
}

/* Paused or reduced motion: the finished chain, no animation. */
.fg-backbone.is-static .fg-backbone__rail,
.fg-backbone.is-static .fg-node {
  transform: none;
  opacity: 1;
  transition: none;
  transition-delay: 0s;
}
.fg-backbone.is-static .fg-backbone__serp {
  stroke-dasharray: 1px 2px;
  stroke-dashoffset: 0px;
  transition: none;
}
/* One exception to the line above: a paused visitor still gets the hover
   lift, because pausing stops ambient motion, not the interface responding
   to the pointer. Reduced motion is handled separately and drops it. */
.fg-backbone.is-static .fg-node {
  transition: scale 220ms var(--ease-out-quart);
}

.fg-backbone__hint {
  margin-top: clamp(28px, 1.6rem + 1.4vw, 44px);
  text-align: center;
  font-size: var(--fs-2xs);
  letter-spacing: var(--ls-chip);
  color: var(--text-muted);            /* 5.57:1 on --surface-base */
}

/* ==========================================================================
   THE 2 x 4 SERPENTINE, 1023px AND DOWN (v2, spec 2026-08-01)

   The vertical rail is gone. Below 1024 the eight stages sit in a two-column
   grid and the signal snakes through them: across row 1 left to right, down
   the right edge, back across row 2, down the left edge, and so on to node 08.

   DOM ORDER AND TAB ORDER STAY NUMERIC, 01 through 08. Only grid-area moves
   the boxes, so the markup, the roving tab stop and the drawer are untouched;
   what changes is where two of the four rows read from. Rows 2 and 4 run right
   to left visually while their DOM order runs left to right, which is a real
   mismatch and a deliberate one: it is the shape the spec asked for, and the
   toolbar's arrow keys walk the chain in stage order regardless of where a
   stage sits.

   PITCH IS FIXED AT 148px A ROW, and it is fixed rather than derived from
   content for one reason: the serpentine's geometry has to be knowable. A row
   sized by its tallest label would move every node centre whenever a label
   rewrapped, and the route, the arc-length fractions and every bloom delay
   hang off those centres.
   148 is built from the node stack: 30px numeral row, 48px mark, 12px to the
   label, and 31.2px for two lines of the 12px label on 1.3. That is 121.2px,
   so the row carries 26.8px of air under the longest label. The mark centre
   therefore lands 54px below the row top at every row, always.
   The bloom is the other constraint and it is satisfied with room to spare:
   inset -18px on a 48px mark is an 84px box, and at a 148px pitch adjacent
   blooms clear each other by 64px, against the 28px the 112px vertical pitch
   managed. Each stage keeps its own beat.

   THE GRID IS CAPPED AT 520px and centred. Uncapped, the two columns would be
   487px wide at 1023px of viewport and the serpentine would read as a very
   wide, very flat zigzag with the stages marooned at its corners. At the cap
   the columns are 260px, which is a shape rather than a sprawl, and the cap
   binds from about 568px of viewport up, so the route geometry is CONSTANT
   over most of the range this layout covers. The cap sits on .fg-backbone
   rather than on the nodes grid, because the route SVG is inset: 0 against
   .fg-backbone and the two boxes have to stay the same box.
   -------------------------------------------------------------------------- */
@media (max-width: 1023px) {
  .fg-backbone {
    --mark: 48px;
    --num-h: 30px;
    max-width: 520px;
    margin-inline: auto;
  }

  .fg-backbone__nodes {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    grid-auto-rows: 148px;
    gap: 0;
  }

  /* The serpentine placement. Numeric in the DOM, snaking on screen. */
  .fg-node:nth-child(1) { grid-area: 1 / 1; }
  .fg-node:nth-child(2) { grid-area: 1 / 2; }
  .fg-node:nth-child(3) { grid-area: 2 / 2; }
  .fg-node:nth-child(4) { grid-area: 2 / 1; }
  .fg-node:nth-child(5) { grid-area: 3 / 1; }
  .fg-node:nth-child(6) { grid-area: 3 / 2; }
  .fg-node:nth-child(7) { grid-area: 4 / 2; }
  .fg-node:nth-child(8) { grid-area: 4 / 1; }

  /* The node keeps the base rule's column stack, which is what makes a grid
     cell work: numeral over mark over label, centred. align-self: start pins
     it to the row top so the 54px to the mark centre is exact. */
  .fg-node {
    align-self: start;
    padding: 0 8px;
  }

  /* The desktop 1.25x size step is undone, so the phone keeps the sizes it
     already had. --mark and --num-h are both restored on .fg-backbone above,
     and both are load bearing now: 30 + 48/2 = 54 is the mark centre inside
     the row, and 30 + 48/2 is also what the base rule uses to place the hover
     sweep line, so that follows on its own and needs no override here. */
  .fg-node__num { font-size: 15px; }
  .fg-node__glyph { width: 21px; height: 21px; }
  /* 12px of label offset, not the desktop step's 16, and it is one of the four
     numbers the 148px row pitch is built from. 150px of measure holds every
     stage name to two lines at 12px: the longest single token any of them has
     is CONDITION or TRANSLATE, about 84px, and the longest pair that can share
     a line is CONDITION & at about 103px. */
  .fg-node__label { margin-top: var(--space-3); max-width: 150px; font-size: var(--fs-2xs); }

  /* THE BLOOM AND THE RING, RE-DERIVED FOR THE SERPENTINE.

     --f IS THE NODE'S POSITION ALONG THE ROUTE, as a fraction of its total
     arc length, and it replaces the --i step formula the straight rails used.
     A straight rail has a constant pitch, so a node's position is a linear
     function of its index; the serpentine alternates 260px horizontal runs
     with 148px vertical ones, so it is not, and no index formula can express
     it. The markup carries the fractions measured at the 520px cap and home.js
     overwrites them with the real ones on unfold and on resize, which makes
     the timing exact at every width instead of approximate at one.
       node   arc length   --f        node   arc length   --f
       01        0         0          05      1188px      0.5499
       02      260px       0.1752     06      1448px      0.7251
       03      408px       0.2749     07      1596px      0.8248
       04      668px       0.4501     08      1856px      1
     Route total 4 x 260 + 3 x 148 = 1484px. The set is symmetric, f and 1 - f
     pairing across the middle, because the route stops at the two end centres
     and the grid is regular; the midpoint of the route is the midpoint of the
     04 to 05 run, which is where the trace draw starts from.

     TURNING FRACTIONS INTO SECONDS. The band centre covers 1 + 1.5 x --sm =
     1.30 route lengths over the 68.5% of the cycle the sweep occupies, so one
     route length costs 0.685 x 10.8 / 1.30 = 5.6908s. The centre starts at
     -sm/2 = -0.10, so a node at fraction f is crossed at (f + 0.10) x 5.6908s,
     and both keyframe sets peak at 50 percent, so the delay is that crossing
     less half a cycle. That is the whole of --serp-delay, and it runs from
     -4.831s at node 01 to +0.860s at node 08.
     Every delay is a calc over --f rather than a literal, so when home.js
     writes a measured fraction the timing follows with no second table.

     DWELL AND THE TWO WINDOWS. Dwell is band width over travel rate:
     0.20 x 5.6908 = 1.138s, 10.54% of the 10.8s cycle. The bloom keeps the
     1.25x-of-dwell envelope both earlier variants use, 13.17%, written 43.4 to
     56.6, so the glow still leads the light in and outlasts it going out. The
     ring keeps the rule that it IS the light, so its full window matches the
     dwell: 45 to 55 is 10%, 1.08s against 1.138s, with the ramps a point
     outside. Peaks stay at exactly 50 percent in both, so one delay serves. */
  .fg-node {
    --serp-delay: calc(((var(--f, 0) + 0.1) * 5.6908s) - 5.4s);
  }

  .fg-node__mark::before {
    inset: -18px;
    animation-name: fg-node-bloom-serp;
    animation-duration: 10.8s;
    animation-delay: var(--serp-delay);
  }

  /* The ring stays HORIZONTAL here, unlike the vertical rail's rotated one,
     and 3px to sit with the 3px trace. The reason is the route: the horizontal
     runs are 260px against the vertical runs' 148px, so every interior node is
     entered or left along the row it sits in, and a light threading the mark's
     perimeter sideways is answering the longer of its two neighbours in every
     case.
     ROWS 2 AND 4 RUN THE OTHER WAY, and animation-direction: reverse is the
     whole fix. It works because the window is symmetric about 50 percent, in
     both the opacity ramps and the background travel, so reversing leaves the
     peak exactly where the delay expects it and only flips which edge the
     light enters by. The base rule's 90deg gradient and 220% 100% size are
     kept, so nothing but the direction differs across the four rows. */
  .fg-node__mark::after {
    padding: 3px;
    animation-name: fg-node-ring-serp;
    animation-duration: 10.8s;
    animation-delay: var(--serp-delay);
  }
  .fg-node[data-flow="rtl"] .fg-node__mark::after { animation-direction: reverse; }

  /* The straight rail and its background-position sweep have nothing to say
     about a serpentine, so both go and the SVG route takes the layer. */
  .fg-backbone__rail { display: none; }
  .fg-backbone__route { display: block; }

  .fg-backbone__hint { text-align: center; }
}

/* Serpentine bloom envelope: 43.4 to 56.6, so 13.17 percent of the 10.8s
   cycle, 1.423s. The rule is the one both earlier variants follow, an envelope
   1.25x the band's dwell so the glow brackets the light rather than blinking
   inside it, and the dwell here is 0.20 route x 5.6908s = 1.138s. Peak sits at
   exactly 50 percent, which is what lets one delay drive the bloom and the
   ring together. */
@keyframes fg-node-bloom-serp {
  0%,    43.4%  { opacity: 0; }
  50%           { opacity: 1; }
  56.6%, 100%   { opacity: 0; }
}

/* Serpentine ring window: full 45 to 55, so 10 percent, 1.08s, against a
   1.138s dwell, with the ramps a point outside at 44 and 56. The ring is the
   light itself, so its window matches the dwell instead of bracketing it.
   The 220% background and the 120 to -20 travel are the base rule's, so the
   bright core sits at -0.34 of the box at the open, dead centre at 50 percent
   and +1.34 at the close. Rows 2 and 4 play this in reverse; the window is
   symmetric about 50, so their peak lands on the same instant. */
@keyframes fg-node-ring-serp {
  0%,  44%  { opacity: 0;    background-position: 120% 0; }
  45%       { opacity: 0.35; }
  50%       { background-position: 50% 0; }
  55%       { opacity: 0.35; }
  56%, 100% { opacity: 0;    background-position: -20% 0; }
}

/* ==========================================================================
   3. MISSION TILES
   Kit tile promoted to a whole-tile link, with the card hover language.
   ========================================================================== */

/* The mission grid is the one place a tile is the whole message, so it runs
   two sizes up from the kit tile: 32px glyph, 20-25px title, 15-16px
   descriptor. Scoped to .fg-mission, so the kit tile everywhere else is
   untouched. Colours are unchanged, so every ratio already measured for the
   tile still holds.

   PADDING AND GAP ARE DELIBERATELY NOT TRIMMED. The narrow case is 390px,
   where .fg-grid--3 is already a single column (kit, 640px and down): the
   container gives 350px, the 20.3px padding leaves 309px of content, and
   nothing in the tile comes close to needing it. Longest title word is
   PRECISION, about 86px of Barlow Condensed at the 20px floor; longest
   descriptor word is about 89px of Inter at 15px; the top row is 32 + 12 + 18
   = 62px. The tiles get taller, which is the point, and no measure gets
   tighter. */
.fg-mission {
  justify-content: space-between;
  gap: clamp(24px, 1.2rem + 1.7vw, 40px);
  padding: clamp(20px, 1.05rem + 0.9vw, 26px);
  text-decoration: none;
}

.fg-mission .fg-tile__icon { width: 32px; height: 32px; }   /* was 26px, kit tile is 20px */

.fg-mission .fg-tile__text {
  font-size: clamp(0.9375rem, 0.905rem + 0.14vw, 1rem);     /* 15 -> 16, was 13 -> 14 */
  line-height: 1.5;
}

.fg-mission__top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
}

/* Held to the bottom of the same 20 to 30 percent step the tile glyph takes,
   so the top row stays balanced without the hover affordance shouting. */
.fg-mission__arrow {
  width: 18px;
  height: 18px;
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.5;
  color: var(--stroke-quiet);          /* non-text glyph, 4.30:1 on the tile */
  opacity: 0;
  transform: translateX(-4px);
  transition: opacity var(--dur-quick) var(--ease-standard),
              transform var(--dur-quick) var(--ease-out-expo),
              color var(--dur-quick) var(--ease-standard);
}

.fg-mission__title {
  font-family: var(--font-display);
  font-weight: var(--fw-display);
  font-size: clamp(1.25rem, 1.12rem + 0.53vw, 1.5625rem);   /* 20 -> 25, was 18 -> 22 */
  line-height: 1.06;
  letter-spacing: var(--ls-h3);
  text-transform: uppercase;
  color: var(--text-display);          /* 17.08:1 on the tile surface */
  transition: var(--t-color);
}

/* Same hover language as .fg-card--link: brand edge, 5% tint, label to
   orange. On the tinted --surface-raised the orange label measures 4.79:1. */
.fg-mission:hover,
.fg-mission:focus-visible {
  border-color: var(--border-accent);
  background-image: linear-gradient(var(--accent-tint), var(--accent-tint));
}
.fg-mission:hover .fg-mission__title,
.fg-mission:focus-visible .fg-mission__title { color: var(--accent); }
.fg-mission:hover .fg-mission__arrow,
.fg-mission:focus-visible .fg-mission__arrow { opacity: 1; transform: none; color: var(--accent); }

/* ==========================================================================
   4. NEWS CARDS
   ========================================================================== */

/* News cards are flat: badge, date, headline. No media well. */
.fg-news-card .fg-card__body { gap: var(--space-3); padding-block: clamp(20px, 1.1rem + 0.9vw, 26px); }
/* The same card title as the news brief on /news, so it takes the same role
   token. It used to state a clamp of its own with the identical 16 and 19
   endpoints but a different slope, which put the homepage copy of this card at
   19px against the /news copy's 18.22px at 1440, and 16.13 against 16.02 at
   390: one component, two sizes, for no reason the sheet recorded. */
.fg-news-card .fg-card__title {
  font-size: var(--fs-h4);
  line-height: 1.12;
}
.fg-news-card .fg-meta { margin-top: calc(var(--space-1) * -1); }

/* ==========================================================================
   5. DRAWER
   Right-side 440px panel on --surface-raised with a hairline left edge.
   Sits above the fixed motion control (z 800) so nothing floats over a modal.
   Every rule below is class-based and instance-blind, which is what lets the
   homepage run TWO physical drawers off it, #fg-node-panel for the backbone
   stages and #fg-cap-panel for the hero capabilities, at no CSS cost. The
   content cascade keys on .fg-drawer__body > * and so fits either body block
   count without a rule of its own.
   ========================================================================== */

.fg-drawer-scrim {
  position: fixed;
  inset: 0;
  z-index: 860;
  background: rgba(9, 12, 20, 0.72);
  backdrop-filter: blur(2px);
  -webkit-backdrop-filter: blur(2px);
  opacity: 0;
  visibility: hidden;
  transition: opacity var(--dur-base) var(--ease-standard),
              visibility 0s linear var(--dur-base);
}
.fg-drawer-scrim[data-open="true"] {
  opacity: 1;
  visibility: visible;
  transition: opacity var(--dur-base) var(--ease-standard), visibility 0s;
}

.fg-drawer {
  position: fixed;
  z-index: 870;
  top: 0; right: 0; bottom: 0;
  width: 440px;
  max-width: 100%;
  display: flex;
  flex-direction: column;
  background: var(--surface-raised);
  border-left: var(--border-hairline-w) solid var(--border-hairline-strong);
  box-shadow: var(--shadow-panel);
  transform: translate3d(100%, 0, 0);
  visibility: hidden;
  transition: transform var(--dur-slow) var(--ease-out-expo),
              visibility 0s linear var(--dur-slow);
}
.fg-drawer[data-open="true"] {
  transform: none;
  visibility: visible;
  transition: transform var(--dur-slow) var(--ease-out-expo), visibility 0s;
}

/* CONTENT CASCADE.
   The panel's own move is deliberately left alone. --dur-slow on
   --ease-out-expo is already the kit's asymmetric long-tailed deceleration:
   cubic-bezier(0.16, 1, 0.30, 1) holds every control point at or under 1, so
   it cannot overshoot by a pixel and there is nothing elastic in it. Swapping
   it would be churn, not craft.

   What was missing is LAYERING. A panel that slides in with its contents
   welded to it reads as one rectangle arriving. The same panel whose head, its
   four content blocks and its foot lift into place a beat behind the leading
   edge reads as something being assembled, which is the effect worth having.

   SIX SLOTS, 45ms APART, 200ms EACH. Head first, then the four blocks
   home.js writes into the body (lede, capabilities, mission areas, products),
   then the foot. The last slot opens at 225ms and finishes at 425ms, against
   the panel's own 420ms, so the cascade lands WITH the panel rather than after
   it and the perceived open time is the one it always had. The rise is 10px:
   enough to read as a lift at this panel width, small enough that nothing
   appears to drop into place. The axis is deliberately across the panel's own,
   which travels in x while its contents travel in y; that is what keeps the
   two moves legible as two moves rather than one smear.

   AN ANIMATION, NOT A TRANSITION, AND THAT IS LOAD BEARING. home.js empties
   and refills .fg-drawer__body on every open, and it does so BEFORE it flips
   data-open, in the same task. A freshly inserted element has no previous
   computed style to transition FROM, so a transition-delay cascade would fire
   on the head and the foot, which are static markup, and be skipped outright
   by the four body blocks, which are the cascade. An animation carries no such
   requirement: it runs the moment its selector starts matching, on a new node
   and a reused one alike. backwards fill holds the 0 percent state through the
   delay, so nothing paints at full strength and then snaps back to the start.

   THE CLOSE IS NOT STAGGERED, and needs no rule to say so. Every declaration
   below hangs off [data-open="true"], so dropping that attribute takes the
   animation with it and each piece is instantly at its own natural opacity 1
   while the panel slides out as one solid object. There is no fade to sequence
   and no delay to unwind. */
.fg-drawer[data-open="true"] .fg-drawer__head,
.fg-drawer[data-open="true"] .fg-drawer__body > *,
.fg-drawer[data-open="true"] .fg-drawer__foot {
  animation: fg-drawer-rise var(--dur-quick) var(--ease-out-quart) backwards;
}
.fg-drawer[data-open="true"] .fg-drawer__head                 { animation-delay: 0s; }
.fg-drawer[data-open="true"] .fg-drawer__body > :nth-child(1) { animation-delay: 45ms; }
.fg-drawer[data-open="true"] .fg-drawer__body > :nth-child(2) { animation-delay: 90ms; }
.fg-drawer[data-open="true"] .fg-drawer__body > :nth-child(3) { animation-delay: 135ms; }
.fg-drawer[data-open="true"] .fg-drawer__body > :nth-child(4) { animation-delay: 180ms; }
/* render() writes exactly four blocks today. Anything a fifth adds joins the
   fourth slot rather than pushing the tail of the cascade past the panel. */
.fg-drawer[data-open="true"] .fg-drawer__body > :nth-child(n+5) { animation-delay: 180ms; }
.fg-drawer[data-open="true"] .fg-drawer__foot                 { animation-delay: 225ms; }

@keyframes fg-drawer-rise {
  0%   { opacity: 0; transform: translate3d(0, 10px, 0); }
  100% { opacity: 1; transform: none; }
}

/* GUARDS. This is interaction motion, not ambient, so the pause control is
   deliberately NOT given a rule: pausing stops loops that run on their own, it
   does not stop the interface answering a click. That is the same line the
   node hover-lift is held to further up.
   Reduced motion does drop it, and it has to be dropped rather than shortened.
   --dur-quick collapses to 1ms on its own, but the six delays are literals and
   would not, so a reduced-motion visitor would still watch the pieces arrive
   spread across 225ms, each one snapping. animation: none puts every piece at
   its natural state on the first frame, which is the panel and its contents
   arriving composed.
   Two rules, the same shape the node bloom uses, and the doubled selector in
   the media query is doing real work. The bare html prefix outranks the
   grouped animation rule, which is what removes animation-name and so stops
   the animation; the surviving animation-delay is inert without a name. The
   html[data-motion] twin is what outranks the nth-child delay rules
   themselves. The bare one covers a page where motion.js never ran. */
html[data-motion="reduced"] .fg-drawer[data-open="true"] .fg-drawer__head,
html[data-motion="reduced"] .fg-drawer[data-open="true"] .fg-drawer__body > *,
html[data-motion="reduced"] .fg-drawer[data-open="true"] .fg-drawer__foot { animation: none; }

@media (prefers-reduced-motion: reduce) {
  html .fg-drawer[data-open="true"] .fg-drawer__head,
  html .fg-drawer[data-open="true"] .fg-drawer__body > *,
  html .fg-drawer[data-open="true"] .fg-drawer__foot,
  html[data-motion] .fg-drawer[data-open="true"] .fg-drawer__head,
  html[data-motion] .fg-drawer[data-open="true"] .fg-drawer__body > *,
  html[data-motion] .fg-drawer[data-open="true"] .fg-drawer__foot { animation: none; }
}

.fg-drawer__head {
  display: flex;
  align-items: flex-start;
  gap: var(--space-4);
  flex: none;
  padding: clamp(22px, 1.2rem + 1vw, 30px) clamp(20px, 1rem + 1vw, 28px);
  border-bottom: var(--border-hairline-w) solid var(--border-hairline);
}
.fg-drawer__icon { align-self: center; }
.fg-drawer__heading { flex: 1; min-width: 0; align-self: center; }

.fg-drawer__eyebrow {
  display: block;
  margin-bottom: var(--space-2);
  font-family: var(--font-body);
  font-size: var(--fs-3xs);
  font-weight: var(--fw-bold);
  line-height: 1;
  letter-spacing: var(--ls-label);
  text-transform: uppercase;
  color: var(--accent);                /* 4.98:1 on --surface-raised */
}

.fg-drawer__title {
  font-family: var(--font-display);
  font-weight: var(--fw-display);
  font-size: var(--fs-drawer);
  line-height: var(--lh-display);
  letter-spacing: var(--ls-h3);
  text-transform: uppercase;
  color: var(--text-display);          /* 17.08:1 on --surface-raised */
}

.fg-drawer__close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  flex: none;
  margin: -6px -8px 0 0;
  background: none;
  border: var(--border-hairline-w) solid transparent;
  color: var(--text-muted);            /* 5.31:1 on --surface-raised */
  cursor: pointer;
  transition: var(--t-color);
}
.fg-drawer__close svg { width: 16px; height: 16px; }
.fg-drawer__close:hover { color: var(--text-display); border-color: var(--border-ui); }

.fg-drawer__body {
  flex: 1;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: clamp(22px, 1.2rem + 1vw, 30px);
  padding: clamp(22px, 1.2rem + 1vw, 30px) clamp(20px, 1rem + 1vw, 28px);
}

.fg-drawer__lede {
  font-size: var(--fs-body);
  line-height: var(--lh-lede);
  color: var(--text-muted);            /* 5.31:1 on --surface-raised */
}

.fg-drawer__block { display: flex; flex-direction: column; gap: var(--space-3); }
.fg-drawer__rows  { display: flex; flex-direction: column; gap: var(--space-2); }

/* Representative product rows. On --surface-base inside the raised drawer,
   matching the .fg-row-link surface rule. */
.fg-drawer__product {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: 11px var(--space-4);
  background: var(--surface-base);
  border: var(--border-hairline-w) solid var(--border-hairline);
  font-size: var(--fs-xs);
  font-weight: var(--fw-medium);
  color: var(--text-primary);          /* 16.22:1 on --surface-base */
}
.fg-drawer__product::before {
  content: "";
  width: 6px;
  height: 6px;
  flex: none;
  background: var(--accent);           /* decorative marker, no text role */
}

.fg-drawer__foot {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  flex: none;
  padding: clamp(20px, 1rem + 1vw, 28px);
  border-top: var(--border-hairline-w) solid var(--border-hairline);
}

@media (max-width: 560px) {
  .fg-drawer__head { gap: var(--space-3); }
  .fg-drawer__body { gap: var(--space-6); }
}

/* ==========================================================================
   6. SECTION FINE-TUNING
   ========================================================================== */

/* The split opener's quiet link drops below the headline on small screens
   rather than crowding it. */
@media (max-width: 640px) {
  .fg-section-head--split { align-items: flex-start; gap: var(--space-4); }
}

/* ==========================================================================
   7. THE BACKBONE CHAIN STANDS IN THE CONTAINER (2026-08-12)

   Appended block. The Explore Stages gate is gone from the markup, and with
   it the two wrapper divs the chain used to sit inside: .fg-backbone__cta,
   .fg-backbone__reveal and .fg-backbone__reveal-inner have no elements left
   to match, so the rules for them further up this sheet are inert. They are
   left in place rather than cut, because this sheet is carrying other work
   in the same edit window.

   WHY ONE RULE IS NEEDED AT ALL, AND IT IS ABOUT MARGINS RATHER THAN THE
   GATE. .fg-backbone__reveal was display: grid, so the chain's own
   margin-top sat on a grid item and could not collapse with the section
   head's margin-bottom above it; the gap between the two was the sum. Out in
   the container, which is a plain block box, the same two margins are
   adjacent siblings and collapse to the larger of the pair, which is always
   the section head's. That would have quietly closed the band up by the
   whole of the chain's clamp, 8px at the narrow end and 24px at the wide
   one, as a side effect of removing a button.
   A single-column grid on the container restores the sum by putting every
   child back on a grid item. minmax(0, 1fr) rather than a bare auto track,
   which is the kit's habit everywhere else, so a long unbroken string in any
   child can never push the track past the container it is measured in.
   ========================================================================== */

.fg-backbone-band > .fg-container {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
}

/* ==========================================================================
   VESSEL CLEARANCE OPTIONS  ·  LOCAL REVIEW SCAFFOLDING, NOT FOR COMMIT
   Added 2026-08-13 so the three candidate settings can be looked at without
   re-editing the sheet. Delete this whole block, and the option token on the
   hero in page--front.html.twig, once a setting has been chosen and written
   into the sheet proper.

   HOW TO SWITCH: one token, one line, in
   templates/page/page--front.html.twig line 167.
       <section class="fg-hero fg-opt-a" id="fg-hero">     option A  (default)
       <section class="fg-hero fg-opt-b" id="fg-hero">     option B
       <section class="fg-hero fg-opt-c" id="fg-hero">     option C
       <section class="fg-hero" id="fg-hero">              shipped state
   Nothing else changes. Every rule below is one class more specific than the
   rule it settles, so it wins wherever it applies and is inert everywhere
   else.

   WHAT EACH ONE IS
     shared   the copy column comes down to 450px in the 1280 to 1439 band,
              which is the only lever that reaches 1280 without shrinking the
              artwork there. Above 1440 the column is already 450.
     A        the constant scale's floor moves from 1440 to 1728, so the
              1440 to 1727 band draws the artwork at --stage-fit x the hero's
              own width exactly as 1280 to 1439 already does. 1728 and 1920
              are untouched.
     B        one artwork size for the whole overlay, trimmed:
              --stage-static 2560 -> 2304 (0.45 x 5120). Changes 1728 and 1920.
     C        a second constant, 2160px, for the 1440 to 1727 band only.
              1728 and 1920 are untouched.
   ========================================================================== */

/* SHARED: the wall stops standing on the vessel below 1440. */
@media (min-width: 1280px) and (max-width: 1439.98px) {
  .fg-hero.fg-opt-a,
  .fg-hero.fg-opt-b,
  .fg-hero.fg-opt-c { --copy-w: 450px; }
}

/* A: the fit rule's band extended up to 1727.98. */
@media (min-width: 1440px) and (max-width: 1727.98px) {
  .fg-hero.fg-opt-a .fg-hero__stage {
    --stage-w: calc(100% * var(--stage-fit));
    --stage-px: calc(100vw * var(--stage-fit));
  }
}

/* B: the one constant, trimmed, everywhere the overlay runs. */
.fg-hero.fg-opt-b { --stage-static: 2304px; }

/* C: a second constant for the band that cannot hold the first one. */
@media (min-width: 1440px) and (max-width: 1727.98px) {
  .fg-hero.fg-opt-c .fg-hero__stage {
    --stage-w: max(2160px, 100%);
    --stage-px: max(2160px, 100vw);
  }
}


/* ==========================================================================
   THE PLATFORM BUTTON'S FOCUS INDICATOR, MOVED ONTO ITS TARGET

   WHAT WAS MEASURED, at 1440x900 with real Tab presses, on all four nodes:

     .fg-scene-node box            -32,337  1728x547
     .fg-hero box                    0,92   1440x792   overflow: hidden
     outline                       2px solid #ffffff at a 2px offset
     box-shadow                    #090c14 spread 2px, accent 0.38 spread 5px

   TWO SEPARATE THINGS COME OUT OF THAT, and only the first is the contrast
   question the audit left open.

   THE CONTRAST IS SETTLED AND IT PASSES, without sampling a single pixel of
   the artwork. The ring is two-tone by construction: the white outline sits
   at 2 to 4px out and the dark shadow fills 0 to 2px, so the two are adjacent
   and measure 19.55:1 against each other. For any two colours A and B whose
   contrast is R, and any background X, at least one of them contrasts with X
   by at least the square root of R, because the two ratios multiply to R when
   X falls between them and one of them is at least R when it does not. Here
   that floor is 4.42:1, so the indicator clears the 3:1 minimum over every
   possible pixel of the photograph, the wire overlay and the hero gradient
   alike. The same construction is what carries every other control on the
   site and it is stated in section 5 of tokens.css.

   THE INDICATOR DID NOT INDICATE, which is the second thing. The button is
   position: absolute with inset: 0, so its box is the whole stage: 1728px
   wide starting 32px off the left edge, inside a hero that clips its
   overflow. The ring's left and right edges are therefore drawn outside the
   viewport and never painted, the top and bottom edges are drawn as a
   1728px band across the hero, and all four buttons produce an identical
   rectangle, so a keyboard user cannot tell which platform is focused.

   THE FIX IS INDICATOR ONLY. Nothing about the hero at rest changes: the
   composition, the geometry, the artwork and the copy are untouched, and no
   template or script is involved. The ring is moved from the button onto
   .fg-scene-node__hit, which is the target box the pointer already uses,
   measured at 1440 as 197x125, 271x74, 257x181 and 171x191, each one sitting
   on its own platform and each already carrying the 44px floor at narrower
   widths. The declarations are the kit's, taken from the focus block in
   section 4 of components.css rather than restated by value, so the ring
   stays one definition.

   THE z-index IS 9, which is the level the node's own leader and tag already
   use: over the text scrim at 7 and over a revealed part at 8, under the
   bottom fade at 10 and the copy at 11. The hit box is a child of a node that
   creates no stacking context, so it competes on the hero's single ladder in
   the same way everything else in the scene does.

   This changes where the focus ring is drawn, and nothing else. Deleting
   this block returns the ring to the whole stage.
   ========================================================================== */

.fg-scene-node:focus-visible {
  outline: none;
  box-shadow: none;
}

.fg-scene-node:focus-visible .fg-scene-node__hit {
  z-index: 9;
  outline: var(--focus-ring-width) solid var(--focus-ring-inner);
  outline-offset: var(--focus-ring-offset);
  box-shadow:
    0 0 0 var(--focus-ring-offset) var(--focus-ring-outer),
    0 0 0 5px var(--focus-ring-halo);
}
