/* Features: a heading fading in centred on the first screen, a line growing from it to item 01, then a numbered timeline played in steps:
   each scroll down (features.js) flies a drone along the line to the next dot, the line growing behind it, and reveals that item, ending
   on the last item's dot; on wide screens the line runs down the middle with items alternating sides. --fly is one flight, from the scroll
   to the landing, and every step's timing derives from it */
.feat {
  --fly: 1.2s;
  --fly-ease: cubic-bezier(.45, 0, .25, 1);
  --rail: 40px;
  /* on phones the items stand well apart, so each takes a good stretch of scrolling; the heading keeps its shorter lead to item 01 */
  --gap: 180px;
  --lead: 120px;
  position: relative;
  z-index: 2;
  overflow-x: clip;
  background: var(--bg);
  /* phones only: the stretch of plain page colour the hero scrolls away into. The section opens on a screen of empty black and the
     heading comes up out of it, so leaving the hero is ordinary scrolling with nothing laid over the page — iPhone Safari leaves the
     foot of a screen-sized gradient layer unpainted, which showed this section through it. Wider screens keep the dark transition
     (core/scroll-dive.js) and set this to 0 below. Every length measured from the section's top adds it (the field map's mask) */
  --dark-lead: 60vh;
  --dark-lead: 60svh;
  /* phones play the heading as a step of its own (features.js): it holds the page and glides item 01 onto the rest line, the way the
     dive's settle used to and the way every later item still does. Wider screens have the dive for that, so it is off there */
  --lead-step: 1;
  --lead-fly: 1.4s; /* the lead step crosses the hero and the dark lead in one move, far further than a step between two items */
  /* under that lead, on phones the heading lands a quarter down the screen, so item 01 and its drone come to rest above the middle;
     wider screens centre it (below) */
  padding: calc(var(--dark-lead) + 25vh - 20px) var(--gutter) 64px;
  padding: calc(var(--dark-lead) + 25svh - 20px) var(--gutter) 64px;
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--lead);
}

/* faint light from the right edge, swaying slowly (light-sway in base.css); sized by the section's height so it fades out well before the
   top and bottom edges, on a layer wider than the page so its sides never show as it moves */
.feat::before {
  content: '';
  position: absolute;
  z-index: -1;
  inset: 0 -10vw;
  pointer-events: none;
  background: radial-gradient(70vw 40% at calc(100% - 10vw) 50%, var(--edge-glow), transparent);
  animation: light-sway 22s ease-in-out infinite;
}

@media (prefers-reduced-motion: reduce) {
  .feat::before { animation: none; }
}

/* background: the field map demo (demos/field-map) on a viewport-wide canvas pinned while the section scrolls by. The scene's camera puts
   the road under the drone's line, --road-x from the canvas's left edge (the rail on the left on phones, the middle on wide screens);
   it is registered so the script reads it computed, as px or %. The map fades in over the first screen and out
   over the last half. A vignette darkens the map toward every screen edge: --vignette wide at the sides (narrow on phones, where the
   road runs by the left edge), the canvas's own top and bottom quarters (--field-fade).
   Under the canvas, pinned in the same box (both stack in the one grid cell, so neither pushes the other down), sits the poster: one frame
   of the map, wide or narrow (scripts/capture-posters.mjs), as tall as the screen and shifted so its road (--poster-road: its x over the
   image's height) lands on --road-x. It shows from the first paint and wherever the scene can't run (fallback, no JS), and crossfades
   to the canvas on its first frame.
   The canvas itself scrolls with the page: it sits in a pinned, masked view (.feat-field-view), --field-band taller than the screen at
   each end, and a scroll-driven translate cancels the view's pinning on the compositor, so the fields stay in step with the dots and the
   line at any scroll speed. The demo (demos/field-map) moves it (--field-top, from the section top) and redraws the map under it only
   once the screen nears its ends; redrawing a pinned canvas every frame left the fields trailing the scroll by a frame or two, so they
   shook against the page. Browsers without view timelines get the cancelling translate from the demo instead (--field-shift) */
@property --road-x {
  syntax: '<length-percentage>';
  inherits: false;
  initial-value: 50%;
}

.feat-field {
  --road-x: calc(var(--gutter) + 7.5px);
  --poster-x: var(--road-x);
  --poster-road: .0275;
  --field-fade: linear-gradient(transparent, #000 25%, #000 75%, transparent);
  --vignette: 12vw;
  --vignette-sides: linear-gradient(90deg, transparent, #000 var(--vignette), #000 calc(100% - var(--vignette)), transparent);
  position: absolute;
  z-index: -1;
  inset: 0;
  pointer-events: none;
  mask: linear-gradient(transparent, #000 calc(var(--dark-lead) + 100vh), #000 calc(100% - 50vh), transparent), var(--vignette-sides);
  mask: linear-gradient(transparent, #000 calc(var(--dark-lead) + 100svh), #000 calc(100% - 50svh), transparent), var(--vignette-sides);
  mask-composite: intersect;
  display: grid;
}

.feat-field > * {
  grid-area: 1 / 1;
  align-self: start;
  position: sticky;
  top: 0;
  display: block;
  width: 100%;
  height: 100vh;
  height: 100dvh;
  mask: var(--field-fade);
  transition: opacity 1s ease;
}

.feat-field-poster,
.feat-field-view { overflow: hidden; }

.feat-field-poster img {
  position: absolute;
  top: 0;
  left: calc(var(--poster-x) - var(--poster-road) * 100vh);
  width: auto;
  max-width: none;
  height: 100%;
}

@supports (height: 100dvh) {
  .feat-field-poster img { left: calc(var(--poster-x) - var(--poster-road) * 100dvh); }
}

.feat-field canvas {
  --field-band: 25vh;
  position: absolute;
  left: 0;
  top: var(--field-top, 0px);
  display: block;
  width: 100%;
  height: calc(100% + 2 * var(--field-band));
  height: calc(100lvh + 2 * var(--field-band));
  translate: 0 var(--field-shift, 0px);
  opacity: 0;
  transition: opacity 1s ease;
}

/* the view pins from the section's top reaching the screen's top (contain 0%) until its foot reaches the section's foot, a run of the
   section's height (100cqh) less the view's (100dvh); the canvas moves back up by as much over that run. The view, the run and the
   shift are all the screen's current height (dvh): on iPhones Safari's toolbar changes it as the page scrolls, and with the view in svh
   the run (measured on the current screen) came out shorter than the shift, so the fields outran the text. The canvas is sized on the
   tallest screen (lvh), so the toolbar coming and going never resizes and redraws it */
@supports (animation-timeline: view()) {
  .feat-field {
    container-type: size;
    view-timeline: --feat-field block;
  }

  .feat-field canvas {
    animation: feat-field-pin linear both;
    animation-timeline: --feat-field;
    animation-range: contain;
  }
}

@keyframes feat-field-pin {
  from { translate: 0 0; }
  to { translate: 0 calc(100dvh - 100cqh); }
}

.feat-field[data-state="ready"] canvas { opacity: 1; }
.feat-field[data-state="ready"] .feat-field-poster { opacity: 0; }

.feat h2 {
  justify-self: center;
  text-align: center;
  margin: 0;
  max-width: 16ch;
  font: 800 clamp(28px, 3.4vw, 52px)/1.1 var(--font-display);
  font-synthesis: none;
  letter-spacing: -.01em;
  text-transform: uppercase;
  transition: opacity .6s ease, transform .6s cubic-bezier(.2, .7, .2, 1);
}

.feat h2[data-state="pending"] {
  opacity: 0;
  transform: translateY(24px);
}

.feat-grid {
  position: relative;
  list-style: none;
  margin: 0;
  padding: 0 0 0 var(--rail);
  max-width: 720px;
}

.feat-grid li {
  position: relative;
  padding-bottom: var(--gap);
}

/* an item's content fades and rises on its own, so its dot and line segment keep their place and stay crisp */
.feat-grid li > :where(:not(.feat-drone)) { transition: opacity .6s ease, translate .6s cubic-bezier(.2, .7, .2, 1); }

.feat-grid li:last-child { padding-bottom: 0; }

/* lead line to the first dot, built like the segments below (hairline in the content box, drone centred on its lower edge), starting
   one drone's length under the heading and ending on the dot's centre (8px into item 01), exactly where item 01's own drone sits.
   As the heading fades in, the drone appears there, then flies down to the first dot with the line growing behind it; item 01 then
   fades in with its own drone on the same spot, and this one leaves once that fade is done; keep these timings in step. On phones the
   chain runs in a second, since the page keeps scrolling under it and a longer one let the visitor pass item 01 before it showed; from
   761px up the dive holds the page still, so it keeps its longer timings, ending as the settle glide does (below) */
.feat-grid::before {
  --drone-size: 0px;
  --lead-run: calc(var(--lead) - var(--drone) + 8px);
  content: '';
  position: absolute;
  z-index: 2;
  left: calc(7.5px - var(--drone) / 2);
  top: calc(var(--drone) / 2 - var(--lead));
  width: var(--drone);
  height: var(--lead-run);
  padding-block: calc(var(--drone) / 2);
  box-sizing: content-box;
  pointer-events: none;
  background:
    var(--drone-icon) center bottom / var(--drone-size) no-repeat padding-box,
    linear-gradient(var(--circle-base), var(--circle-base)) center top / 1px 100% no-repeat content-box;
}

.feat h2[data-state="pending"] + .feat-grid::before { height: 0; }

.feat h2[data-state="pending"] + .feat-grid li:first-child { opacity: 0; }
.feat h2[data-state="pending"] + .feat-grid li:first-child > :where(:not(.feat-drone)) { translate: 0 24px; }
.feat h2[data-state="visible"] + .feat-grid::before {
  --drone-size: var(--drone);
  animation: feat-fade .4s ease .3s both, feat-lead .6s ease-in-out .4s both, feat-land 1ms 1.5s forwards;
}
.feat h2[data-state="visible"] + .feat-grid li:first-child { animation: feat-fade .5s ease 1s both; }
.feat h2[data-state="visible"] + .feat-grid li:first-child > :where(:not(.feat-drone)) { animation: feat-rise .5s ease 1s both; }

@keyframes feat-lead {
  from { height: 0; }
  to { height: var(--lead-run); }
}

@keyframes feat-fade {
  from { opacity: 0; }
}

@keyframes feat-land {
  to { background-size: 0 0, 1px 100%; }
}

@keyframes feat-rise {
  from { translate: 0 24px; }
}

/* segment from an item's dot to the next: a solid hairline (the item's ::after) scaled down from its top, with a drone (.feat-drone)
   riding its growing tip, moved down by translate. Both are transforms, so the flight runs on the compositor while features.js glides
   the page after it; growing the line's height redrew the drone every frame on the main thread and it stuttered. The drone is the
   contact map's drone icon (droneIcon in scene/route-map.js, sizes from MAP.drone in
   demos/route-map/config.js) seen from above: square body, crossed white arms, translucent white rotor discs with a blade each; its accent
   is --circle-base like the map's route colour, baked into the image with --ink's white. Its motion matches the map's too (SMIL inside
   the image, MAP.drone): blades spin at rotorRate 30 rad/s (one turn per .21 s) and the drone bobs ±1.5 units at bobRate 4 rad/s
   (1.57 s); under reduced motion a still copy is used */
.feat {
  --drone: 44px;
  --drone-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-23 -23 46 46'%3E%3Cg%3E%3CanimateTransform attributeName='transform' type='translate' values='0 -1.5;0 1.5;0 -1.5' keyTimes='0;.5;1' calcMode='spline' keySplines='.37 0 .63 1;.37 0 .63 1' dur='1.57s' repeatCount='indefinite'/%3E%3Cg fill='%23fff' fill-opacity='.3'%3E%3Ccircle cx='12.7' cy='12.7' r='8'/%3E%3Ccircle cx='-12.7' cy='12.7' r='8'/%3E%3Ccircle cx='12.7' cy='-12.7' r='8'/%3E%3Ccircle cx='-12.7' cy='-12.7' r='8'/%3E%3C/g%3E%3Cg fill='%23fff'%3E%3Crect x='-18' y='-1.2' width='36' height='2.4' transform='rotate(45)'/%3E%3Crect x='-18' y='-1.2' width='36' height='2.4' transform='rotate(-45)'/%3E%3C/g%3E%3Cg fill='%2361bff5'%3E%3Cg transform='translate(12.7 12.7)'%3E%3Crect x='-7.5' y='-.8' width='15' height='1.6'%3E%3CanimateTransform attributeName='transform' type='rotate' from='0' to='360' dur='.21s' repeatCount='indefinite'/%3E%3C/rect%3E%3C/g%3E%3Cg transform='translate(-12.7 12.7)'%3E%3Crect x='-7.5' y='-.8' width='15' height='1.6'%3E%3CanimateTransform attributeName='transform' type='rotate' from='0' to='360' dur='.21s' repeatCount='indefinite'/%3E%3C/rect%3E%3C/g%3E%3Cg transform='translate(12.7 -12.7)'%3E%3Crect x='-7.5' y='-.8' width='15' height='1.6'%3E%3CanimateTransform attributeName='transform' type='rotate' from='0' to='360' dur='.21s' repeatCount='indefinite'/%3E%3C/rect%3E%3C/g%3E%3Cg transform='translate(-12.7 -12.7)'%3E%3Crect x='-7.5' y='-.8' width='15' height='1.6'%3E%3CanimateTransform attributeName='transform' type='rotate' from='0' to='360' dur='.21s' repeatCount='indefinite'/%3E%3C/rect%3E%3C/g%3E%3Crect x='-5.5' y='-5.5' width='11' height='11'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
}

@media (prefers-reduced-motion: reduce) {
  .feat {
    --drone-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-23 -23 46 46'%3E%3Cg%3E%3Cg fill='%23fff' fill-opacity='.3'%3E%3Ccircle cx='12.7' cy='12.7' r='8'/%3E%3Ccircle cx='-12.7' cy='12.7' r='8'/%3E%3Ccircle cx='12.7' cy='-12.7' r='8'/%3E%3Ccircle cx='-12.7' cy='-12.7' r='8'/%3E%3C/g%3E%3Cg fill='%23fff'%3E%3Crect x='-18' y='-1.2' width='36' height='2.4' transform='rotate(45)'/%3E%3Crect x='-18' y='-1.2' width='36' height='2.4' transform='rotate(-45)'/%3E%3C/g%3E%3Cg fill='%2361bff5'%3E%3Cg transform='translate(12.7 12.7)'%3E%3Crect x='-7.5' y='-.8' width='15' height='1.6'%3E%3C/rect%3E%3C/g%3E%3Cg transform='translate(-12.7 12.7)'%3E%3Crect x='-7.5' y='-.8' width='15' height='1.6'%3E%3C/rect%3E%3C/g%3E%3Cg transform='translate(12.7 -12.7)'%3E%3Crect x='-7.5' y='-.8' width='15' height='1.6'%3E%3C/rect%3E%3C/g%3E%3Cg transform='translate(-12.7 -12.7)'%3E%3Crect x='-7.5' y='-.8' width='15' height='1.6'%3E%3C/rect%3E%3C/g%3E%3Crect x='-5.5' y='-5.5' width='11' height='11'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
  }
}

.feat-grid li:not(:last-child)::after {
  content: '';
  position: absolute;
  z-index: 2;
  left: calc(7px - var(--rail));
  top: 8px;
  width: 1px;
  height: 100%;
  pointer-events: none;
  background: var(--circle-base);
  transform-origin: top;
  transition: scale var(--fly) var(--fly-ease);
}

.feat-grid li:has(+ li[data-state="pending"])::after { scale: 1 0; }

/* the drone's box is as tall as the segment with the drone at its top, centred on the item's dot; moving it down by its own height
   (translate 100%) lands the drone on the next dot */
.feat-drone {
  position: absolute;
  z-index: 2;
  left: calc(7.5px - var(--rail) - var(--drone) / 2);
  top: calc(8px - var(--drone) / 2);
  width: var(--drone);
  height: 100%;
  pointer-events: none;
  background: var(--drone-icon) center top / var(--drone) no-repeat;
  translate: 0 100%;
  transition: translate var(--fly) var(--fly-ease), visibility 0s var(--fly);
}

.feat-grid li:has(+ li[data-state="pending"]) > .feat-drone { translate: none; }

/* the drone flies only the segment being drawn: it waits on an item's dot once that item reveals and leaves the segment once the next item
   reveals (the run has landed); on the last item that is the strike, so it is gone as the target explodes */
.feat-grid li[data-state="pending"] > .feat-drone,
.feat-grid li:not(:has(+ li[data-state="pending"])) > .feat-drone { visibility: hidden; }

/* a step: when an item reveals, the drone waiting on the previous dot flies the line down to its dot over --fly, the line growing behind
   it; the dot lights as the drone lands, then the text rises in.
   The text waits for the page to stop: features.js glides it from GLIDE_LAG (.2s) over --fly, and that glide moves the scroll in whole
   pixels, so a rise running under it came out as smooth motion minus a one-pixel staircase and the text shuddered. The delay is that
   glide's end; the dot only changes colour, so it can still light on the drone's landing */
.feat-grid li[data-state="visible"] > :where(:not(.feat-drone)) { transition-delay: calc(var(--fly) + .2s); }
.feat-grid li[data-state="visible"]::before { transition-delay: calc(var(--fly) - .05s); }

.feat-grid li::before {
  content: '';
  position: absolute;
  z-index: 1;
  left: calc(-1 * var(--rail));
  top: 1px;
  width: 15px;
  height: 15px;
  border-radius: 50%;
  border: 1px solid var(--circle-base);
  background: var(--circle-base);
  box-shadow: 0 0 0 6px var(--glow);
  transition: opacity .6s, background .6s, box-shadow .6s, border-color .6s;
}

.feat-grid li[data-state="pending"] > :where(:not(.feat-drone)) {
  opacity: 0;
  translate: 0 40px;
}

.feat-grid li[data-state="pending"]::before {
  opacity: 0;
  background: var(--bg);
  border-color: var(--line-strong);
  box-shadow: none;
}

/* the last item is the target: its dot is the Orients mark in --glow-red (a mask of the header logo's mark paths, index.html), centred
   where the dot's centre was. When the
   drone lands on it the mark bursts in, a flash with a shockwave ring blows out from it and the item's content shakes. --boom-delay keeps
   this in step with the drone's landing: --fly-ease leaves it within a few px of the dot by 85% of the flight and only creeps after, so
   the blast goes off there, not at the flight's very end, and the drone leaves with it; the flash fills forwards only, so nothing shows
   before it */
.feat {
  --boom: 44px;
  --boom-delay: calc(var(--fly) * .85);
}

.feat-grid li:nth-last-child(2) > .feat-drone { transition: translate var(--fly) var(--fly-ease), visibility 0s var(--boom-delay); }

.feat-grid li:last-child::before {
  left: calc(7.5px - var(--rail) - var(--boom) / 2);
  top: calc(8.5px - var(--boom) / 2);
  width: var(--boom);
  height: var(--boom);
  border: 0;
  border-radius: 0;
  box-shadow: none;
  background: var(--glow-red);
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 271 276'%3E%3Cpath d='M4.71802 0L135.558 130.81L221.863 50.0459L135.327 85.5957L4.71802 0Z'/%3E%3Cpath d='M271 266.336L142.692 138.058L221.863 50.0459L187.916 138.173L271 266.336Z'/%3E%3Cpath d='M266.282 276L135.442 145.19L49.1367 226.069L135.558 190.519L266.282 276Z'/%3E%3Cpath d='M0 9.77905L128.308 137.942L49.1367 226.069L82.9686 137.827L0 9.77905Z'/%3E%3C/svg%3E") center / contain no-repeat;
}

.feat-grid li:last-child::after {
  content: '';
  position: absolute;
  left: calc(7.5px - var(--rail) - var(--boom) / 2);
  top: calc(8.5px - var(--boom) / 2);
  width: var(--boom);
  height: var(--boom);
  border-radius: 50%;
  pointer-events: none;
  opacity: 0;
  background: radial-gradient(circle, #fff 0 12%, color-mix(in srgb, var(--glow-red) 85%, transparent) 38%, transparent 70%);
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--glow-red) 70%, transparent);
}

.feat-grid li:last-child[data-state="visible"]::before { animation: feat-boom-mark .6s cubic-bezier(.3, 1.8, .5, 1) var(--boom-delay) both; }
.feat-grid li:last-child[data-state="visible"]::after { animation: feat-boom-blast .9s cubic-bezier(.1, .7, .3, 1) var(--boom-delay) forwards; }
.feat-grid li:last-child[data-state="visible"] > * { animation: feat-boom-shake .45s linear var(--boom-delay) both; }

/* the target itself: a truck seen from above, nose down the line, fleeing the drone and caught on the last dot; it sits under the flying
   drone (z-index 1 < 2). Its box runs --drive (2.6 --gap on phones, where the items' text runs taller, 2.2 wider, so it starts up the
   line, between items 01 and 02) up the line from the dot, and the truck is its ::after, at the box's foot on the dot unless moved up.
   It drives in steps, with the drone's flights (--fly, --fly-ease): waiting at the box's top with item 01, on past the drone at item 02
   as that item reveals, to --drive-left above the dot, and onto the dot with the drone as the last item reveals. It drives by translate,
   on the compositor. When the blast goes off it shakes, flares white and burns out, leaving the mark */
.feat-car {
  --car-w: 24px;
  --car-h: 48px;
  --drive: calc(var(--gap) * 2.6);
  --drive-left: calc(var(--drive) * .45);
  position: absolute;
  z-index: 1;
  left: calc(7.5px - var(--rail) - var(--car-w) / 2);
  top: calc(8.5px - var(--car-h) / 2 - var(--drive));
  width: var(--car-w);
  height: calc(var(--car-h) + var(--drive));
  pointer-events: none;
  transform-origin: 50% calc(100% - var(--car-h) / 2);
}

.feat-car::after {
  content: '';
  position: absolute;
  left: 0;
  bottom: 0;
  width: var(--car-w);
  height: var(--car-h);
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-12 -24 24 48'%3E%3Cg transform='rotate(180)'%3E%3Cg fill='%23fff'%3E%3Crect x='-11.5' y='-19' width='2.5' height='6' rx='1'/%3E%3Crect x='9' y='-19' width='2.5' height='6' rx='1'/%3E%3Crect x='-11.5' y='5' width='2.5' height='6' rx='1'/%3E%3Crect x='9' y='5' width='2.5' height='6' rx='1'/%3E%3Crect x='-11.5' y='13' width='2.5' height='6' rx='1'/%3E%3Crect x='9' y='13' width='2.5' height='6' rx='1'/%3E%3Crect x='-8.5' y='-23' width='17' height='12' rx='3'/%3E%3C/g%3E%3Crect x='-6.5' y='-19.5' width='13' height='4' rx='1' fill='%230b0d10'/%3E%3Crect x='-9.4' y='-8.4' width='18.8' height='30.8' rx='1.5' fill='%23fff' fill-opacity='.3' stroke='%23fff' stroke-width='1.2'/%3E%3Cpath d='M-9 0h18M-9 8h18M-9 16h18' stroke='%23fff' stroke-opacity='.5'/%3E%3C/g%3E%3C/svg%3E") center / 100% no-repeat;
}

.feat-car::after,
.feat-car::before {
  translate: 0 calc(-1 * var(--drive));
  transition: translate var(--fly) var(--fly-ease);
}

.feat-grid li[data-state="visible"] + li[data-state="pending"] > .feat-car::after,
.feat-grid li[data-state="visible"] + li[data-state="pending"] > .feat-car::before { translate: 0 calc(-1 * var(--drive-left)); }

.feat-grid li:last-child[data-state="visible"] > .feat-car::after,
.feat-grid li:last-child[data-state="visible"] > .feat-car::before { translate: none; }

/* target identification: as item 02, target acquisition, reveals (--frame-delay after it, as its drone lands) corner brackets in
   --glow-red lock onto the truck, closing in from wider and blinking twice, and ride with it (the truck's drive, above); they go when
   item 03 reveals and the blast takes the truck, or when the steps reset. The brackets are the frame's border masked down to its four
   corners */
.feat-car::before {
  --frame-pad: 8px;
  --frame-corner: 10px;
  --frame-delay: var(--fly);
  content: '';
  position: absolute;
  left: calc(-1 * var(--frame-pad));
  bottom: calc(-1 * var(--frame-pad));
  width: calc(100% + 2 * var(--frame-pad));
  height: calc(var(--car-h) + 2 * var(--frame-pad));
  box-sizing: border-box;
  border: 2px solid var(--glow-red);
  mask:
    linear-gradient(#000 0 0) 0 0 / var(--frame-corner) var(--frame-corner) no-repeat,
    linear-gradient(#000 0 0) 100% 0 / var(--frame-corner) var(--frame-corner) no-repeat,
    linear-gradient(#000 0 0) 0 100% / var(--frame-corner) var(--frame-corner) no-repeat,
    linear-gradient(#000 0 0) 100% 100% / var(--frame-corner) var(--frame-corner) no-repeat;
  opacity: 0;
}

.feat-grid li[data-state="visible"] + li[data-state="pending"] > .feat-car::before {
  opacity: 1;
  animation: feat-frame-lock .6s ease-out var(--frame-delay) both;
}

@keyframes feat-frame-lock {
  0% { opacity: 0; scale: 1.8; }
  40% { opacity: 1; scale: 1; }
  55% { opacity: .2; }
  70% { opacity: 1; }
  85% { opacity: .2; }
  100% { opacity: 1; scale: 1; }
}

.feat-grid li:not([data-state]) > .feat-car { display: none; }
.feat-grid li[data-state="pending"] > .feat-car { opacity: 1; translate: none; }

/* the truck fades in with the lead drone (feat-fade .4s ease .3s on .feat-grid::before) as the heading shows, and out as it drops back */
.feat h2[data-state="pending"] + .feat-grid li > .feat-car { opacity: 0; }
.feat h2[data-state="visible"] + .feat-grid li > .feat-car { transition: opacity .4s ease .3s; }
.feat-grid li:last-child[data-state="visible"] > .feat-car {
  animation: feat-boom-shake .45s linear var(--boom-delay) both, feat-car-hit .4s ease-in var(--boom-delay) forwards;
}

@keyframes feat-car-hit {
  30% { opacity: 1; scale: 1.25; filter: brightness(3); }
  to { opacity: 0; scale: .8; filter: brightness(.2); }
}

/* the fire after the blast, after the contact map's strike (blast in demos/route-map/config.js, in its colours): a fireball that swells
   and cools from pale yellow to red-orange (::before), sparks flung out from the target that slow and burn out (::after, one box-shadow
   per spark), and a red ember glow on the mark that flickers and dies down about a second after the mark bursts in (the element). Centred
   on the last dot like the blast, screened over the mark so it shows through; shown from --boom-delay once the last item reveals */
.feat-fire {
  position: absolute;
  left: calc(7.5px - var(--rail) - var(--boom) / 2);
  top: calc(8.5px - var(--boom) / 2);
  width: var(--boom);
  height: var(--boom);
  border-radius: 50%;
  pointer-events: none;
  mix-blend-mode: screen;
  opacity: 0;
  background: radial-gradient(circle, rgb(255 106 42 / .6), rgb(255 74 20 / .25) 45%, transparent 70%);
}

.feat-fire::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: 50%;
  opacity: 0;
  background: radial-gradient(circle, #fff0b8 0 18%, #ffb04a 40%, #ff4a14 60%, transparent 72%);
}

.feat-fire::after {
  content: '';
  position: absolute;
  left: calc(50% - 1.5px);
  top: calc(50% - 1.5px);
  width: 3px;
  height: 3px;
  border-radius: 50%;
  opacity: 0;
}

.feat-grid li[data-state="pending"] > .feat-fire { translate: none; }
.feat-grid li:last-child[data-state="visible"] > .feat-fire {
  opacity: 1;
  animation:
    feat-boom-shake .45s linear var(--boom-delay) both,
    feat-fire-glow 1.15s ease-in-out calc(var(--boom-delay) + .25s) both,
    feat-fire-flicker .58s linear calc(var(--boom-delay) + .25s) 2;
}
.feat-grid li:last-child[data-state="visible"] > .feat-fire::before { animation: feat-fire-ball .9s cubic-bezier(.2, .7, .3, 1) var(--boom-delay) both; }
.feat-grid li:last-child[data-state="visible"] > .feat-fire::after { animation: feat-sparks 1.1s cubic-bezier(.15, .7, .3, 1) var(--boom-delay) both; }

@keyframes feat-fire-glow {
  0%, 100% { opacity: 0; }
  25%, 65% { opacity: 1; }
}

@keyframes feat-fire-flicker {
  0%, 100% { scale: 1; filter: brightness(1); }
  25% { scale: 1.12; filter: brightness(1.3); }
  50% { scale: .94; filter: brightness(.9); }
  75% { scale: 1.08; filter: brightness(1.15); }
}

@keyframes feat-fire-ball {
  0% { opacity: 1; scale: .3; filter: brightness(2.2); }
  35% { opacity: 1; }
  100% { opacity: 0; scale: 1.7; filter: brightness(.9); }
}

@keyframes feat-sparks {
  from {
    opacity: 1;
    box-shadow:
      0 0 #ffc070, 0 0 #ff6a2a, 0 0 #ffc070, 0 0 #ff6a2a, 0 0 #ffc070, 0 0 #ff6a2a, 0 0 #ffc070,
      0 0 #ff6a2a, 0 0 #ffc070, 0 0 #ff6a2a, 0 0 #ffc070, 0 0 #ff6a2a, 0 0 #ffc070, 0 0 #ff6a2a;
  }
  70% { opacity: 1; }
  to {
    opacity: 0;
    box-shadow:
      52px 0 #ffc070, 34px 17px #ff6a2a, 38px 47px #ffc070, 10px 43px #ff6a2a, -13px 55px #ffc070, -23px 28px #ff6a2a, -56px 27px #ffc070,
      -46px 0 #ff6a2a, -52px -25px #ffc070, -25px -31px #ff6a2a, -12px -53px #ffc070, 8px -33px #ff6a2a, 38px -47px #ffc070, 38px -18px #ff6a2a;
  }
}

@keyframes feat-boom-mark {
  from { scale: 0; rotate: -40deg; }
}

@keyframes feat-boom-blast {
  from { opacity: 1; scale: .3; }
  to { opacity: 0; scale: 4.5; }
}

/* the shake moves transform, not translate, so it adds to the content's rise (a translate transition) instead of replacing it: Chrome
   dropped the rise and snapped the text up, and iPhone Safari let it run on underneath, so the text sank back as the shake ended and
   rose in a second time */
@keyframes feat-boom-shake {
  0%, 100% { transform: none; }
  15% { transform: translate(5px, -3px); }
  30% { transform: translate(-5px, 3px); }
  45% { transform: translate(4px, 2px); }
  60% { transform: translate(-3px, -2px); }
  80% { transform: translate(1px, 1px); }
}

/* a dark haze sprayed under the text, so it stays legible over the fields: behind each block a soft ellipse in the page colour, reaching
   well past its edges and fading out to its rim, so no box edge shows. Each block is its own stacking context, so the haze sits under
   its text and fades in and out with it */
.feat :is(h3, p) {
  position: relative;
  isolation: isolate;
}

.feat :is(h3, p)::before {
  content: '';
  position: absolute;
  z-index: -1;
  inset: -80px -96px;
  pointer-events: none;
  background: radial-gradient(closest-side, color-mix(in srgb, var(--bg) 65%, transparent) 25%,
    color-mix(in srgb, var(--bg) 30%, transparent) 60%, transparent);
}

/* the paragraph paints after the title, so its haze starts at its own top: reaching up, it would dim the title's last line; the title's
   haze reaching down covers that gap instead */
.feat p::before { inset: 0 -96px -80px; }

.feat h3 {
  margin: 0 0 12px;
  font-weight: 500;
  font-size: var(--text-title); /* their own sizes from tablets up */
  line-height: 1.15;
}

.feat p {
  margin: 0;
  max-width: 38ch;
  color: var(--mute);
  font-size: var(--text-detail);
  line-height: 1.5;
}

@media (min-width: 761px) {
  .feat h3 { font-size: clamp(20px, 1.7vw, 26px); }
  .feat p { font-size: 16px; }
  /* wide enough that the last item's closing word doesn't sit alone on its line; where its half of the grid is narrower, wrapping evens it out */
  .feat-grid li:last-child p { max-width: 42ch; text-wrap: pretty; }
}

@media (min-width: 761px) {
  .feat {
    --rail: 56px;
    --gap: 180px;
    --lead: var(--gap);
    --dark-lead: 0px; /* the dive darkens these widths instead (core/scroll-dive.js), so the section starts where it always did */
    --lead-step: 0;
    padding: calc(var(--dark-lead) + 50vh - 40px) var(--gutter) 120px;
    padding: calc(var(--dark-lead) + 50svh - 40px) var(--gutter) 120px;
  }

  /* the dive holds the page still, then glides it (DIVE.settle in core/scroll-dive.js: delay .8 s, duration .7 s): the line ends as
     that glide ends and item 01 fades in there, on the lead drone's spot */
  .feat h2[data-state="visible"] + .feat-grid::before {
    animation: feat-fade .4s ease .3s both, feat-lead .8s ease-in-out .7s both, feat-land 1ms 2s forwards;
  }

  .feat h2[data-state="visible"] + .feat-grid li:first-child { animation: feat-fade .5s ease 1.5s both; }
  .feat h2[data-state="visible"] + .feat-grid li:first-child > :where(:not(.feat-drone)) { animation: feat-rise .5s ease 1.5s both; }

  .feat-car { --drive: calc(var(--gap) * 2.2); }

  .feat-grid {
    justify-self: center;
    width: 100%;
    max-width: 1040px;
    padding: 0;
  }

  .feat-field { --road-x: 50%; --poster-road: 1.18519; --vignette: 22vw; }

  .feat-grid::before { left: calc(50% - var(--drone) / 2); }

  .feat-grid li { width: 50%; }

  .feat-grid li:nth-child(odd) {
    padding-right: var(--rail);
    text-align: right;
  }

  .feat-grid li:nth-child(even) {
    margin-left: 50%;
    padding-left: var(--rail);
  }

  .feat-grid li:nth-child(odd) p { margin-left: auto; }

  .feat-grid li:nth-child(odd)::before { left: auto; right: -7.5px; }
  .feat-grid li:nth-child(even)::before { left: -7.5px; }
  .feat-grid li:nth-child(odd):last-child::before,
  .feat-grid li:nth-child(odd):last-child::after { left: auto; right: calc(var(--boom) / -2); }
  .feat-grid li:nth-child(even):last-child::before,
  .feat-grid li:nth-child(even):last-child::after { left: calc(var(--boom) / -2); }
  .feat-grid li:nth-child(odd) > .feat-fire { left: auto; right: calc(var(--boom) / -2); }
  .feat-grid li:nth-child(even) > .feat-fire { left: calc(var(--boom) / -2); }
  .feat-grid li:nth-child(odd) > .feat-car { left: auto; right: calc(var(--car-w) / -2); }
  .feat-grid li:nth-child(even) > .feat-car { left: calc(var(--car-w) / -2); }
  .feat-grid li:nth-child(odd):not(:last-child)::after { left: auto; right: -.5px; }
  .feat-grid li:nth-child(even):not(:last-child)::after { left: -.5px; }
  .feat-grid li:nth-child(odd) > .feat-drone { left: auto; right: calc(var(--drone) / -2); }
  .feat-grid li:nth-child(even) > .feat-drone { left: calc(var(--drone) / -2); }
}
