/* ============================================================
   DEVICE PAGE — the Nestify Family Hub, the hardware.

   A standalone page, built the way faq.html is built: the site's own
   header and footer, markup and all, and nothing in here that is not
   already in styles.css. Every colour is a --nes-* token, every curve
   is one of the five --ease-* in styles.css, and the type is the one
   face the site allows — Google Sans, for the reading and the chrome
   alike (it was two until Nunito came out; see the type note there).

   🔴 It does NOT link intro.css, for the same reason faq-page.css
   does not: that file holds the opener, and part of the opener is
   `.site-header { opacity: 0 }` with a 1.6s fade. Inherited onto a
   page with no opener, the bar is invisible for the first second
   and a half.

   🔴 No `html { scroll-behavior: smooth }`. A page this tall would
   scroll a nav jump through every section of it, and — the part that
   actually matters — a CSS smooth value wins over a script's
   `behavior: 'auto'`, so reduced-motion users would get the long
   scroll anyway. Every jump on this page is a scrollIntoView that
   reads the media query first (device.js).

   ORIGIN. This replaces an earlier standalone draft that ran its own
   palette (brass on near-black film, Archivo + EB Garamond, 18–28px
   radii). Nothing of that system survives — it shared no token with
   the site and read as a different company's page. The mapping, for
   anyone holding the old file:
       brass #A3823E  → --nes-blue for anything interactive,
                        --nes-amber for stars and rewards only
       film  #100F0D  → --nes-ink
       paper #FBFAF7  → --nes-white
       panel #F3F1EA  → --nes-sand
       hair  #E4E1D7  → --nes-tan
       EB Garamond italic → --font--primary italic (Google Sans ships
                        a real one; see the note in styles.css)
   ============================================================ */

/* 🔴 The site's ground, not white. Measured: index.html and faq.html both
   sit on rgb(244,242,234) — that IS --page-bg, the one ground styles.css
   names — and this page was the only one on #fff, which is what made it
   read as a different site the moment you crossed from the home page. The
   sections that want to be white still say so; on sand they now read as
   panels, which is how faq-page.css uses white too.

   🔴 It went WHITE for a while — `--page-bg: var(--nes-white)`, scoped to
   .dv-body — and it is back on sand, which is where the paragraph above
   said it should be. The round trip is worth keeping on file: the
   counter-argument was that redefining the token moves every ground on
   the page at once and so moves the page off the site, and that is
   exactly what it did.

   🔴 AND THERE IS AN OVERRIDE AGAIN — --nes-mist, the cool grey lifted
   off apple.com/ipad-air (see the token in styles.css for where the value
   comes from and why it is cyan rather than blue). Everything the white
   era proved is still true and this is not a repeat of it:

     · the white override was WRONG because white is not a ground, it is
       the absence of one, and it made this page read as a different site.
       A near-white with a hue in it is a ground; that is the difference.
     · it moves every surface on THIS page at once. That was the
       counter-argument, and here it is the point: the hero's ramp, the
       stage and the sections below it all have to land on the same value
       or the seams show. One token is what guarantees that.
     · it stops at the body. index.html and faq.html are untouched and
       still stand on sand — checked, they name --nes-sand or --page-bg
       and neither resolves through this rule.

   🔴 So the site does now run two grounds, and that is a real cost being
   accepted rather than an oversight: crossing from the home page to this
   one crosses from warm to cool. It is defensible because this page is a
   product shot and the rest of the site is not — a cool ground is what
   hardware is photographed on — but if a third page ever wants this, the
   answer is to move the site, not to copy this line.

   One thing the white era left behind and that is kept: the surfaces
   that gained their own hairline while the ground was white still have
   it. They do not need it to be told apart, but a hairline on a panel is
   not wrong, and taking them out again is a second change to make on a
   day when the ground is already moving. */
/* 🔴 THE TWO SEMANTIC NAMES HAVE TO BE RE-DECLARED, and finding out why
   cost a wrong render. styles.css says

       :root { --background-color--background-tertiary: var(--page-bg) }

   and that var() is substituted ONCE, on :root, against :root's own
   --page-bg. What every element below inherits is the RESULT — a literal
   sand — not the reference. So overriding --page-bg here moved everything
   that reads --page-bg directly (the hero rail, the stage, this body) and
   moved nothing that reads the semantic name, which is the entire footer
   plus the section shell and the `body` rule in styles.css. Checked in
   isolation before it was believed: two divs, one on each token, under a
   body that redefines --page-bg — the direct one turns, the derived one
   does not.

   Re-declaring them here re-runs the substitution at THIS element, where
   --page-bg is the mist. It is not a duplicate of styles.css; it is the
   same expression evaluated in the only scope that has the new value.

   🔴 --ft-tint-page is the footer's opt-in (see the note on --ft-tint in
   footer.css). --nes-haze, which is the value the hero wash starts from,
   so the page opens and closes on one blue with the product in between.

   🔴 A QUIETER BAND WAS TRIED HERE AND PUT BACK. #ddeaeb: the ground's
   own hue, the same depth, a third of the saturation — which is the
   relationship --nes-tan has to sand, and it is the rule this should
   follow. It read as dirty. The full measurement is in the note where
   that token used to live in styles.css; the short version is that sand
   has enough colour (S 31%) to survive being desaturated and mist does
   not (S 14%), so the "correct" band on this ground lands in grey-teal.
   Between a band that is too blue and one that is grubby, this page takes
   the blue. */
body.is-page.dv-body {
  --page-bg: var(--nes-mist);
  /* 🔴 THE SITE'S OWN BLUE, not apple.com's. This was #0071e3 —
     rgb(0,113,227), the Buy button off apple.com, and the note that used
     to sit here said so outright and scoped it to this page so it could
     not spread. Scoping a borrowed colour keeps it contained; it does not
     stop it being borrowed, and on a page whose hero already leans on
     that company's layout it was the one thing making the resemblance
     look like a decision rather than a reference.

     --bead-blue is the blue on the i of the logo and the blue the home
     page's pills rest on. Same token, same value, no page-local
     override — which is why this is now an alias rather than a colour. */
  --dv-cta-blue: var(--cta-blue);
  --background-color--background-primary: var(--page-bg);
  --background-color--background-tertiary: var(--page-bg);
  /* 🔴 A MIX, not --nes-haze straight, and it is here to hold this band
     STILL while the wash above it got heavier. --nes-haze went #d5ebf3 ->
     #bbe2f0 on re-measurement; handing that to the footer would take the
     band from 6.5 points of lightness under the ground to 12, double what
     the home page's band does and not something anyone asked for. 55% of
     the new value over the ground resolves to rgb(212,235,243) — the old
     value to within a step, which is where this was tuned over two
     passes. The wash is a field you look INTO; this is a shade on a panel
     that has to stay quiet enough for links to sit on it. */
  --ft-tint-page: color-mix(in srgb, var(--nes-haze) 55%, var(--nes-mist));
  background: var(--page-bg);
  color: var(--nes-ink);
}

/* ============================================================
   shared primitives
   ============================================================ */

/* the header's own gutter, so every heading lines up with the logo */
.dv__wrap {
  width: 100%;
  max-width: 77.5rem;
  margin-inline: auto;
  padding-inline: max(5vw, 32px);
}

/* 🔴 The SAME eyebrow the deck uses on the home page (.hero__eyebrow):
   Google Sans at 500, .12em, held-back ink.

   (This note used to argue about WHICH FACE: it was Nunito 700, and the
   case for moving it was that a label above a section headline is
   editorial type rather than chrome. The site runs one family now — see
   the type note in styles.css — so the family is no longer the argument.
   The WEIGHT still is: 500 and .12em is a held-back eyebrow, 700 is a
   small headline, and this wants the first.) */
/* (.dv__eyebrow lived here — the small grey uppercase label over each
   card's headline: --font--primary, clamp(12px, .95vw, 14px), weight
   500, .12em of tracking, --nes-muted. It labelled the four cards THE
   MORNING / VOICE / KIDS / MEALS and it is gone from all of them; the
   headline says which one it is. Nothing else on the page used it.) */

/* the page's section heading. styles.css's h2 rule is the base — this
   only ever changes the size, so the family, weight and tracking stay
   wherever styles.css puts them. */
/* 🔴 --heading--display, NOT a clamp of its own. That token is the
   site's section-opener size and it already has two users on the home
   page — the privacy lockup and the testimonials headline — which is why
   it exists rather than each block picking its own ramp. This block had
   `clamp(2.25rem, 5vw, 4rem)`, which agrees with it at 1440 and runs
   4-7px larger at every width below: same cap, steeper slope. The
   headings read heavier than the home page's for that reason alone —
   they inherit h2's weight 400, exactly as the home page's do.

   line-height comes off too. h2 sets .95 for the whole site; .98 here
   was loosening the one thing that makes a display line read as a
   block. */
.dv__h2 {
  font-size: var(--heading--display);
  text-wrap: balance;
}

/* 🔴 NO ITALIC, and the site already said so: styles.css:296 declares
   `em { font-style: normal; font-weight: inherit; }` globally. The home
   page marks emphasis in headings with <em> too — the hub's "where your
   family lives", the testimonials' "with Nestify" — and neither slants.
   The tag is carrying MEANING there, not a typeface change.

   This rule was turning the site's own reset back off, on this page
   only, so the same markup produced italics here and not there. The
   <em>s stay in the markup; they just stop being drawn differently. */

/* (.dv__lede was the page's section-lead paragraph — --font--body,
   clamp(1rem, 1.3vw, 1.1875rem)/1.65, ink at 68%, text-wrap: pretty.
   The four cards were its only users on this page and they no longer
   carry a sentence, so nothing reads it. Kept as a note because the
   next section that wants a lead paragraph should use these numbers
   rather than invent new ones.) */

/* ---------- buttons ----------
   The header's pill, brought down into the page so the whole site
   answers a cursor in one voice: hollow, 1.5px of the current colour,
   and a disc grown from dead centre so the ENDS of the pill arrive
   last. Same 120%-of-width circle, same --ease-out, same trick of
   delaying the label's colour on the way in and not on the way out
   (see the long note in header.css — the reasoning is identical and
   is not repeated here).

   🔴 The one number to re-check if a label changes: a centred circle
   of diameter 1.2w covers the pill only while h ≤ 0.66w. The tightest
   label on this page is RESERVE at 3.4em tall — fine at every size it
   is used, but a two-word label in the same pill is not automatic. */
.dv__btn {
  position: relative;
  display: inline-flex; align-items: center; justify-content: center;
  min-height: 3.4em; padding: 0 1.9em;
  background: transparent;
  border: 1.5px solid currentColor;
  border-radius: 6.25em;
  color: var(--nes-ink);
  --dv-fill: var(--nes-ink);
  font-family: var(--font--ui);
  font-size: .875rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: .04em;
  text-decoration: none;
  overflow: hidden; cursor: pointer;
  transition: border-color .4s, color .2s, transform 140ms var(--ease-out-quint);
}
.dv__fill {
  position: absolute; z-index: 0;
  /* the pointer's entry point, and 240% to reach the far corner from it —
     see the note on .btn-disc::after in styles.css for both numbers */
  left: var(--disc-x, 50%); top: var(--disc-y, 50%);
  width: 240%; aspect-ratio: 1; height: auto;
  border-radius: 50%;
  background: var(--dv-fill);
  transform: translate(-50%, -50%) scale(0);
  transition: transform .55s var(--ease-out);
  pointer-events: none;
}
.dv__btn > span:not(.dv__fill) { position: relative; z-index: 1; white-space: nowrap; }

@media (hover: hover) and (pointer: fine) {
  .dv__btn:hover .dv__fill { transform: translate(-50%, -50%) scale(1); }
  .dv__btn:hover {
    color: var(--nes-cream);
    border-color: transparent;
    transition-delay: 0s, .18s, 0s;   /* border-color, color, transform */
  }
}

/* solid: the one commitment on each screen. Inverts the pair — the
   disc arrives white so the label can turn dark under it. */
.dv__btn.is-solid {
  background: var(--nes-ink);
  color: var(--nes-white);
  border-color: var(--nes-ink);
  --dv-fill: var(--nes-white);
}
@media (hover: hover) and (pointer: fine) {
  .dv__btn.is-solid:hover { color: var(--nes-ink); border-color: var(--nes-ink); }
}

/* (.on-ink — the inverted pair, for a pill standing on a dark
   section — went with the dark sections. Nothing on this page has a
   ground it needs to invert against any more.) */

/* ---------- press ----------
   🔴 Every pressable on this page answers a press, not only a hover.
   The old draft had a hover rule on six controls and an :active on
   none of them, which on a touch screen is no feedback at all — you
   tap a size chip and the only thing that moves is a 1px border.

   140ms and 3% is deliberately under the threshold where motion reads
   as motion; it reads as the button giving. That is also why it
   SURVIVES prefers-reduced-motion at the foot of this file — a scale
   this small and this brief has no vestibular cost, and taking it
   away would leave reduced-motion users with the one thing they
   actually need least removed. */
.dv__btn:active { transform: scale(.97); }

/* this one lifts on hover, so its press has to come back down through
   the lift rather than scale off it */
.dv__btn.has-lift:active { transform: translateY(0) scale(.98); }

/* ============================================================
   the pad — the device itself

   Drawn, not photographed, exactly as .hub__pad on the home page is
   drawn, and for the same two reasons.

   🔴 The bezel is a box-shadow SPREAD, not padding and not a border.
   Padding would shrink the screen and re-lay-out every row of the UI
   inside it whenever the pad resizes; a spread ring is painted
   outside the box and costs the layout nothing. It also gets the
   outer corner right for free — a shadow's outer radius is the border
   radius plus the spread, which is how a real bezel meets a screen.

   🔴 ONE font-size on .dv__screen drives the entire UI below it, in
   em. The pad appears at five different sizes on this page (hero
   stage, highlight card, feature figure, comparison, size table) and
   not one of them needs its interior re-tuned — they set a screen
   font-size and everything follows.

   Kept here rather than linked from hub.css because hub.css's pad is
   welded to that section's 620svh scroll track and its docking
   transform; importing it would drag the machinery along.
   ============================================================ */
.dv__pad {
  position: relative;
  z-index: 1;
  container-type: inline-size;
  width: 100%;
  aspect-ratio: 16 / 9;   /* measured off the unit: 1920x1080 */
}

/* ---------- the case ----------
   🔴 A real frame element, not a box-shadow spread. The spread was
   fine while the case was ink — one flat colour — but a material has
   a gradient across it, and a shadow spread can only be solid. It is
   ::before rather than padding on .dv__pad for the reason in the
   block above: padding changes the container's inline size, which
   changes the em root, which re-lays-out every row of the UI inside
   the screen. A pseudo-element inset past the pad's edge costs the
   layout nothing.

   🔴 cqw again, and again on a CHILD. .dv__pad establishes the
   container, so nothing written on .dv__pad itself can query it.

   The material is one variable. `data-frame` on <body> switches it —
   see the block below. */
.dv__pad::before {
  content: "";
  position: absolute; z-index: 0;
  /* 🔴 1.9cqw and 3.4cqw — the same numbers nestie3d.js gives BEZEL and
     RADIUS, and back where they started. They were widened to 4.2 and
     then to 6.2/7.2, the last of those measured off the real product
     shot: a family display whose thick soft housing IS the object. That
     is the accurate drawing and it is not the one this page wants. If it
     ever has to be, the numbers are here — bezel 6.2cqw, radius 7.2cqw,
     and the 3D unit grows a sunken well to match.

     The two drawings are one drawing; change these and change those. */
  inset: calc(-1 * clamp(4px, 1.9cqw, 18px));
  border-radius: clamp(9px, 3.4cqw, 2.6rem);
  background: var(--dv-frame, var(--nes-ink));
  box-shadow:
    inset 0 1px 0 var(--dv-frame-lip, rgba(255, 255, 255, .16)),
    /* the silhouette. See the --dv-frame-rim note: a white case on warm
       sand has no outline of its own to lose, so it is given one. */
    inset 0 0 0 1px var(--dv-frame-rim, transparent),
    0 3cqw 5cqw -1.4cqw color-mix(in srgb, var(--nes-ink) 42%, transparent);
}

.dv__screen {
  position: absolute; inset: 0; z-index: 1;
  display: flex;
  border-radius: clamp(6px, 2.2cqw, var(--section-radius--regular));
  /* 🔴 THE GASKET IS BACK, and it comes back with the white frame. It
     was taken out twice: once when the screen carried a dark wallpaper
     that separated itself, and once when the whole front went to black
     glass and there was nothing left for it to separate. The frame is
     white again and the app inside it is white, so this hairline is the
     only thing saying where the screen ends — the same job the 3D
     unit's dark well floor does (see nestie3d.js).

     It is a seam AND a depth cue: the display sits DOWN in the housing,
     so the line at its edge is a shadow, not a border. */
  /* 🔴 NO GASKET. The hairline that used to sit here said where the
     screen ended, back when the white face ran all the way to the
     outside edge and a white app ran straight into it. The housing has a
     raised metal rim now and the white is a band between the picture and
     that lip — the edge reads off the structure, and a drawn line on top
     of it is a black outline around a photograph. Same removal in
     nestie3d.js; the two drawings are one drawing. */
  overflow: hidden;
  font-family: var(--font--body);
  font-size: clamp(6px, 1.5cqw, 15px);
  background: var(--nes-white);
  color: var(--nes-ink);
  line-height: 1.3;
}

/* ---------- what the case is made of ----------
   🔴 One material, everywhere on this page: the case is WHITE. An
   earlier draft of this file ran it matte black and argued the case
   at length — a dark object against a light room. The argument was
   sound and the premise was wrong; the unit's edge is white, so the
   whole paragraph went with it.

   🔴 A WHITE CASE IS THE HARDER ONE, and every value below is paying
   for that — twice over since the page itself went white. Black would
   separate from this ground for free, because nothing else on it is
   dark. White does not: the case, the ground and the app inside the
   screen are three light surfaces within a few steps of each other,
   and left alone they collapse into one shape with a picture in the
   middle of it. So the case is given three things black never needed —

     · a gradient that actually TRAVELS. Black could sit on two stops
       a few values apart because a dark surface reads as one mass;
       #fbfbf9 → #dcd9d2 is a real fall across the panel and it is what
       makes the thing convex rather than a white rectangle.
     · a HAIRLINE. The case's lightest stop and the page are now four
       steps apart — #fbfbf9 against white — so its top edge has
       nothing of its own to be seen by. The rim is a step darker than
       the case all the way round. Without it the ground and the case
       meet at a seam you cannot see and the object has no silhouette.
     · a much darker GASKET. On black the case was already darker than
       any UI and the seam was free. On white, a white app runs
       straight out into a white bezel — this is now the only thing
       telling you where the screen ends.

   Matte, not gloss: the lip stays soft and broad. A wide bright band
   across it reads as polished plastic, which this is not.

   🔴 EVERYTHING ABOVE DESCRIBES THE CASE, AND THE CASE IS NOT WHAT
   YOU SEE FROM THE FRONT — and everything above is history. The
   short version of a long argument:

     · matte black. Wrong: the unit's edge is white.
     · white, thin frame. This.
     · black glass edge to edge, because "a modern device's front is one
       sheet of glass and a white surround is a picture frame". True of
       a tablet; it turned the case into a black slab.
     · thick warm white, measured off the product shot, with the screen
       sunk into a well. Accurate, and heavier than the page wants.

   So: white, a thin frame, a hairline of a rim to give it a silhouette
   on a warm ground, and a dark gasket where the glass meets it.

   🔴 The GASKET is what lets a white case carry a white app. It is the
   only thing saying where the screen ends, and it has been deleted
   twice — once when the UI carried a dark wallpaper that separated
   itself, once when the whole front went black and there was nothing
   left to separate. Both times it came back with the white case.

   🔴 NOT #ffffff. A diffuse white surface at pure white has nothing
   above it to catch light with: the lit face clips, the gradient
   flattens, and the frame loses its shape. nestie3d.js holds the same
   value for the same reason.

   🔴 These are the only hex values on this page that are not from
   styles.css, and that is deliberate — they describe a physical
   material, not a UI surface. A product's finish is not a design token
   and must not become one. Nothing but the case may cite them. */
.dv-body {
  --dv-frame: linear-gradient(157deg, #fbfbf9 0%, #f0eee9 44%, #dcd9d2 100%);
  --dv-frame-lip: rgba(255, 255, 255, .85);
  --dv-frame-rim: rgba(26, 26, 26, .16);
  --dv-frame-gasket: rgba(0, 0, 0, .62);
  /* 🔴 The anodised rim, and it is the same value nestie-unit.js gives
     rimMat. The 3D housing and every flat drawing of it on this page have
     to agree on the finish or the page shows two products; this is where
     the flat ones read it from. */
  --dv-rim: #aec8de;
}

/* 🔴 cqw, not vw. The pad is drawn at four sizes on this page and one
   of them (the hero stage) is inside a grid column, so its width has
   no fixed relationship to the viewport at all — a vw-keyed root is
   right at exactly one breakpoint and wrong on either side of it. The
   pad is its own container, so the entire UI, the bezel and the corner
   radius all scale off the pad and nothing inside is ever re-tuned.

   1.5cqw is the home page's own proportion: hub.css tops out at a
   980px pad with a 14px root, which is 1.43cqw. The floor is what
   stops the small hero pad becoming texture. */
/* the rail */
.dv__rail {
  flex: none;
  width: 4.4em;
  background: var(--nes-cream);
  border-right: 1px solid color-mix(in srgb, var(--nes-tan) 55%, transparent);
  display: flex; flex-direction: column; align-items: center;
  gap: .35em; padding: 1.5em 0;
}
.dv__rail i {
  width: 2.6em; height: 2.6em;
  display: grid; place-items: center;
  border-radius: .85em;
  color: var(--nes-muted);
  opacity: .55;
}
.dv__rail i svg { width: 1.35em; height: 1.35em; }
.dv__rail i.is-hot {
  opacity: 1;
  color: var(--nes-blue);
  background: color-mix(in srgb, var(--nes-sky) 75%, transparent);
}

.dv__app {
  flex: 1; min-width: 0;
  padding: 1.7em 2em 1.9em;
  display: flex; flex-direction: column;
}
.dv__kicker {
  margin: 0 0 1.05em;
  font-family: var(--font--ui);
  font-size: .8em; font-weight: 700;
  letter-spacing: .13em; text-transform: uppercase;
  color: var(--nes-muted);
  display: flex; align-items: center; justify-content: space-between; gap: 1em;
  flex: none;
}

/* the week */
.dv__week { display: grid; grid-template-columns: repeat(7, 1fr); gap: .5em; flex: 1; min-height: 0; }
.dv__day {
  background: var(--nes-sand);
  border-radius: .8em;
  padding: .65em .55em;
  display: flex; flex-direction: column; gap: .38em;
  min-width: 0;
}
.dv__day > b {
  font-family: var(--font--ui);
  font-size: .78em; font-weight: 700; letter-spacing: .08em;
  text-transform: uppercase; color: var(--nes-muted);
}
.dv__evt {
  background: var(--nes-sky);
  color: var(--nes-sky-up);
  border-radius: .5em;
  padding: .4em .5em;
  font-size: .76em; font-weight: 600; line-height: 1.3;
}
.dv__evt.is-new { background: var(--nes-blue); color: #fff; }
/* the day you are standing in — seven equal columns give the eye
   nowhere to start */
.dv__day.is-today {
  background: var(--nes-white);
  box-shadow: inset 0 0 0 1.5px color-mix(in srgb, var(--nes-blue) 40%, transparent);
}
.dv__day.is-today > b { color: var(--nes-blue); }
/* an empty column has to say so — a blank one reads as unloaded */
.dv__free { font-size: .74em; color: var(--nes-muted); margin: 0; }

/* the spoken line */
.dv__voice {
  flex: none; margin: 1em 0 0;
  display: flex; align-items: center; gap: .7em;
  font-size: .85em; font-weight: 500;
  color: color-mix(in srgb, var(--nes-ink) 70%, transparent);
}
.dv__level { display: flex; align-items: center; gap: .17em; height: 1em; flex: none; }
.dv__level i {
  width: .22em; height: 100%;
  border-radius: 999px;
  background: var(--nes-blue);
  transform: scaleY(.35);
  animation: dv-level .9s ease-in-out infinite alternate;
}
.dv__level i:nth-child(2) { animation-delay: .12s; }
.dv__level i:nth-child(3) { animation-delay: .26s; }
.dv__level i:nth-child(4) { animation-delay: .08s; }
@keyframes dv-level { to { transform: scaleY(1); } }

/* three children, three columns */
.dv__cols { display: grid; grid-template-columns: repeat(3, 1fr); gap: .75em; flex: 1; min-height: 0; }
.dv__col {
  background: var(--nes-sand);
  border-radius: .9em;
  padding: .8em;
  display: flex; flex-direction: column;
}
.dv__who {
  display: flex; align-items: center; gap: .5em;
  font-size: .88em; font-weight: 600; margin-bottom: .55em;
}
.dv__ava {
  width: 1.7em; height: 1.7em; border-radius: 50%;
  display: grid; place-items: center;
  font-size: .82em; font-weight: 700; color: var(--nes-ink);
  background: var(--nes-sky);
  flex: none;
}
.dv__ava.is-amber { background: var(--nes-amber); }
.dv__ava.is-tan   { background: var(--nes-tan); }

.dv__task {
  display: flex; align-items: center; gap: .55em;
  background: var(--nes-white);
  border-radius: .55em;
  padding: .5em .65em;
  margin-top: .4em;
  font-size: .82em; font-weight: 500; line-height: 1.25;
}
.dv__box {
  position: relative; flex: none;
  width: 1.15em; height: 1.15em;
  border: 1.5px solid color-mix(in srgb, var(--nes-blue) 55%, transparent);
  border-radius: .35em;
}
.dv__task.is-checked .dv__box::after {
  content: ""; position: absolute; inset: -1.5px;
  border-radius: .35em;
  background: var(--nes-blue) no-repeat center / .7em .7em;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23fff' stroke-width='3.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 12.5 9.5 18 20 6.5'/%3E%3C/svg%3E");
}
.dv__star { margin-left: auto; color: var(--nes-amber); font-size: .9em; flex: none; }

.dv__tally {
  margin-top: auto; padding-top: .8em;
  display: flex; align-items: center; gap: .55em;
  font-family: var(--font--ui);
  font-size: .74em; font-weight: 700; letter-spacing: .1em;
  text-transform: uppercase; color: var(--nes-muted);
}
.dv__bar {
  flex: 1; height: .38em; border-radius: 999px;
  background: color-mix(in srgb, var(--nes-tan) 70%, transparent);
  overflow: hidden;
}
.dv__bar i { display: block; height: 100%; width: var(--done, 0%); border-radius: 999px; background: var(--nes-blue); }

/* the week's meals */
.dv__meals { display: grid; grid-template-columns: repeat(5, 1fr); gap: .65em; flex: 1; min-height: 0; }
.dv__meal {
  background: var(--nes-sand);
  border-radius: .8em;
  padding: .7em .75em;
}
.dv__meal small {
  font-family: var(--font--ui);
  font-size: .72em; font-weight: 700; letter-spacing: .11em;
  text-transform: uppercase; color: var(--nes-muted);
}
.dv__meal b { display: block; font-size: .92em; font-weight: 600; margin: .25em 0 .14em; line-height: 1.25; }
.dv__meal span { font-size: .76em; color: var(--nes-muted); }
.dv__meal.is-picked { background: var(--nes-sky); box-shadow: inset 0 0 0 1.5px var(--nes-blue); }
.dv__list {
  flex: none; margin-top: .75em;
  background: var(--nes-sand);
  border-radius: .8em;
  padding: .75em .9em;
  display: flex; gap: 1.4em; align-items: center;
  font-size: .82em; color: var(--nes-muted);
}
.dv__list b { color: var(--nes-ink); font-weight: 600; }

/* the morning glance */
.dv__glance { display: grid; grid-template-columns: 1.45fr 1fr; gap: .8em; flex: 1; min-height: 0; }
.dv__gcard {
  background: var(--nes-sand);
  border-radius: .9em;
  padding: .85em 1em;
  display: flex; flex-direction: column; min-width: 0;
}
.dv__gcard > b {
  display: block; margin-bottom: .55em;
  font-family: var(--font--ui);
  font-size: .72em; font-weight: 700; letter-spacing: .12em;
  text-transform: uppercase; color: var(--nes-muted);
}
.dv__grow {
  display: flex; gap: .7em; align-items: baseline;
  padding: .3em 0;
  font-size: .84em; font-weight: 500;
  color: color-mix(in srgb, var(--nes-ink) 72%, transparent);
}
.dv__grow b { font-weight: 600; color: var(--nes-ink); min-width: 4.6em; flex: none; }
.dv__gstack { display: flex; flex-direction: column; gap: .8em; min-height: 0; }

/* ============================================================
   1 · hero — the object, its name, and one thing to do

   🔴 This was a store: a 1.42fr gallery of four cross-fading views
   beside a 1fr configurator with size chips, package radios and a
   running total. It is now a single centred column — name, line,
   object, pill — because the page cannot take money yet and a
   checkout that cannot check out is the loudest possible way to say
   so. Nothing of the two-column grid survives; the sizes it used to
   configure have their own section further down.

   🔴 --page-bg, the ground the whole site sits on — and then it
   LEAVES it, which is the one thing this hero does that the rest of
   the page does not. A drafted version ran on ink from the first
   frame and it was a better photograph of a worse page: the first
   screen belonged to a different company from the one you had just
   left. The difference is arrival. You land on the page's own white,
   the page earns the departure over two and a half screens of your
   own scrolling, and it hands the white back before the next section.
   A page that STARTS somewhere else is a different company; a page
   that GOES somewhere and returns is the same company with a stage.

   🔴 EVERYTHING IN HERE IS ONE NUMBER. --hp is the rail's progress,
   0 → 1, written by device.js and read by the object, the glow and the
   glow, the pill and the 3D object. There is no second timeline to
   fall out of sync with it, and no element animates on its own.
   ============================================================ */
.dv__hero {
  position: relative;
  /* isolation went with the canvas too — see the note on .dv__hero-stage */
  color: var(--nes-ink);

  /* 0 → 1 across the rail (device.js). The default matters: with no JS
     it stays 0, which is the hero at rest, exactly as it was. */
  --hp: 0;

  /* ---------- the ground: sky at the top, sand at the foot ----------
     🔴 It is a FIXED gradient, not a moving one. An earlier hero had a
     wash that rose over the middle of the rail and drained out again,
     keyed to --hp, and it is not coming back: a ground that changes
     colour under you spends the reader's attention on the background.
     This one is painted once and holds for the whole rail. --hp still
     only drives POSITION — the object's turn and its rise.

     🔴 It has to LAND on --page-bg, not merely get close. The section
     below is the page's own sand, so the last stop is that exact token
     and the seam between the two is a value the file cannot get wrong.
     It arrives at 78% rather than 100% so there is a band of settled
     sand under the object before the edge, the way the reference lets
     its own ground go quiet before the section ends.

     🔴 The top is --nes-sky deepened, not a new blue. Mixed toward the
     headline's own --nes-sky-up so the ground and the type are the
     same hue at two strengths; a second, unrelated blue up here is
     what makes a tinted hero look like a stock gradient.

     🔴 And it is painted on the STAGE, not on this element. .dv__hero is
     260svh of rail; a gradient stretched over that only ever shows the
     reader a slice of itself, and the slice moves as they scroll —
     which is the moving ground all over again, arrived at by accident.
     The stage is the one 100svh box that stays put, so a gradient
     there is fixed in the window for as long as the hero is up. */
  background: var(--page-bg);
}

/* ---------- the rail, and the tint lives HERE ----------
   Pure scroll distance otherwise: 100svh of it is the stage standing
   still and the rest is what the reader spends turning the object over.

   🔴 MEASURED OFF THE REFERENCE, not designed. apple.com/ipad-air was
   loaded at 1440×900 and sampled down its own left edge, and what it
   does is the opposite of what "the background changes" sounds like:

     scroll 0    y=200 → rgb(213,235,243)   ← the blue, at the top
                 y=350 → rgb(225,239,244)
                 y=500 → rgb(237,244,245)
                 y=650 → rgb(243,246,246)   ← the page's own ground
     scroll 400  the same values, 400px higher
     scroll 900  every sample is the ground

   So there is no colour animation anywhere on that page. The body is one
   flat value the whole way down (rgb(243,246,246) — note that it is not
   white either), and the blue is a soft gradient occupying the first
   ~650px of the hero that simply SCROLLS AWAY with everything else.

   🔴 Which is why it is on the rail and not on the stage. The stage is
   sticky — a gradient there is pinned to the window and never leaves,
   which is the "hero that changes colour under you" this file has thrown
   out twice already. The rail scrolls normally, so its own background
   scrolls out from behind the pinned stage exactly like the reference's.
   The stage is transparent so this shows through it.

   🔴 It lands on --page-bg by name, not on a value that looks close. The
   last stop has to BE the ground or the seam is a band the eye finds
   immediately.

   🔴 AND THE GROUND IS NOW THEIRS — --page-bg resolves to the same
   rgb(243,246,246) sampled above, so the last stop of this ramp and the
   figure it was measured against are the same colour.

   🔴 AND SO IS THE BLUE, which is the last thing in here that was still
   ours. The stops below are the four samples above, in order, and
   nothing else: the top is --nes-haze (rgb(213,235,243), the token in
   styles.css), then their two middle readings, then the ground. The
   ramp is no longer a straight line between two colours — the
   reference's drains FAST and then holds, which is why a two-stop
   version of it never looked the same however the endpoints were tuned.

   What this replaces, so the reasoning is not lost: the top used to be
   color-mix(--nes-sky 86%, --nes-sky-up) — the site's own blue, pushed a
   little deeper — on the argument that the blue should be the brand's
   even if the ground was theirs. It resolves to rgb(193,220,249), which
   is 20 points darker and 21 points bluer than the thing it was standing
   next to; against a cool grey ground that difference reads as purple.
   The brand blue is still everywhere else on this page (the pill, the
   glow, every tinted panel below). This one gradient is daylight, and
   daylight is measured, not chosen.

   🔴 The stop positions are the sample heights, mapped: the readings run
   y=200 → 650, so 200 is 0%, 350 is 33%, 500 is 67% and 650 is the
   bottom of the box. The box itself is the measured 650px, floored to
   72svh so a short window does not get a wash taller than its own
   content. */
.dv__hero-rail {
  position: relative;
  /* 🔴 AUTO. The rail is not a distance any more — the stage does not
     pin, so there is nothing to buy scroll for. What is left of this
     element is the WASH, which is why it still exists: the tint is
     painted here rather than on the stage so that it scrolls away with
     the page instead of being pinned in the window (the note below the
     measurements is the whole argument, and it still holds).

     It has been 260svh, then 100, then 160. All three were paying for a
     pinned stage. The height of this section is now just the height of
     what is in it. */
  height: auto;
  /* 🔴 NO WASH. This element is the flat ground and nothing else, so the
     hero opens on the same --page-bg the rest of the page stands on.

     🔴 IT IS A ONE-LINE RESTORE, and every piece is written down. What was
     here was a seven-stop 180deg gradient, `top center / 100%
     min(104svh, 940px) no-repeat`, over a second flat --page-bg layer.
     The stops were --nes-haze → --page-bg sampled along t^1.5, evenly
     spaced at 0/16/32/48/64/80/100%:

       #c1e4ec  #c4e5ed  #cae7ee  #d2eaef  #dbedf1  #e5f1f3  #f3f6f6

     🔴 THE SECOND LAYER WAS NOT DECORATIVE, which is the one thing to
     remember if it comes back: the band was `no-repeat` and finite, so a
     flat --page-bg had to sit under it or everything below the band was
     transparent rather than ground.

     🔴 IT HAS BEEN THROUGH FOUR COLOURS — a brand-blue tint, the
     reference's own #bbe2f0, that blue with its blue-over-green gap
     closed to 8 (#c1e4ec), and a grey built out of the ground itself.
     The grey is the right construction for a SHADOW and the wrong one for
     a hero's light; #c1e4ec is the one to rebuild from.

     --nes-haze survives in styles.css — the footer's band still mixes
     from it (see --ft-tint-page above) — so the colour is not lost, only
     the field it was painted on. */
  background: var(--page-bg);
}

/* 🔴 sticky, not fixed, and not a scroll-jack. The stage is a normal
   element that happens to stop at the top of the window until its rail
   runs out — so the scrollbar is honest about the page's length, the
   wheel is never intercepted, and stopping halfway leaves the hero
   halfway rather than snapping it anywhere. */
/* 🔴 NOT PINNED, AND NOT ONE SCREEN. This is the change everything else
   in the hero now depends on, so it is worth being blunt about what it
   replaces: the stage was `position: sticky; top: 0; height: 100svh`,
   and every hard problem in this section came out of that one line. A
   fixed screen means a fixed height budget, which meant the object could
   only be as big as one window minus the type minus the ask; then when
   the object was finally allowed to be big it ran past the foot of the
   window and the lockup landed on top of it, and a fog band had to be
   invented to make the words readable over the calendar.

   None of that is a problem the reader has. The section is simply
   TALLER than a screen now — min-height 100svh so it still opens as a
   full screen, height auto so it grows to fit whatever is in it — and
   the ask sits under the object in the clear, one small scroll down.
   The fog is gone, the height budget is gone, and the object is sized by
   what looks right rather than by what is left over. */
.dv__hero-stage {
  position: static;
  min-height: 100svh;
  /* 🔴 COLUMN, and it has to be said out loud now. This was `display:
     flex; align-items: center` with ONE in-flow child — the column — and
     the ask floating absolutely over it, so the default row direction
     never showed. Put the ask back in the flow and the stage has two
     items in a ROW: the object shrank to make space and the lockup went
     and stood beside it, out at the right-hand edge.

     Column also moves what does the centring: `align-items` is the
     cross axis, so it is now the horizontal one, and the vertical
     centring the note below describes is `justify-content`. Same
     arithmetic, same uneven padding, different property. */
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  /* read twice: once as this box's top padding, once by the name's
     entrance, which has to know how far it is from the top of the
     window to start itself in the middle of it */
  --hero-pad: clamp(3rem, 6vh, 4.5rem);
  --hero-top: var(--hero-pad);
  /* 🔴 CENTRED, AND THE PADDING IS SYMMETRIC. `justify-content` here was
     `flex-start` with a small pad underneath, and the note that used to
     sit in this spot argued for it: centring splits the window's leftover
     height above and below, so a tall screen pushed the hero down by half
     of whatever was spare. That was true and it is worth saying why it is
     no longer the trade it was.

     Flex-start does not remove the leftover height, it MOVES it — all of
     it, into one hole under the ask, where there is nothing to read.
     Measured before this change, the gap under "2189 families waiting":

         1506 x  892     61px      1920 x 1080     64px
         1506 x 1100    229px      1920 x 1300    263px

     Two of those are the height it was tuned at and two of them are what
     happens above it. And the surplus is smaller than it was when the old
     note was written, because the object now scales with the viewport
     instead of stopping at 28rem — the composition grows with the window
     rather than sitting in the middle of a growing one.

     So: centred, with the same padding top and bottom, which makes the
     air over the name and the air under the fine print the same number at
     every window height instead of at one.

     🔴 --hero-pad IS THE MINIMUM, NOT THE POSITION. Once this is centred,
     the padding is only what stops the composition touching the edges on a
     window too short to have any surplus; above that height the gap is
     padding plus half the surplus, on both sides.

     🔴 --hero-top IS NO LONGER THE SAME NUMBER, and this is the part that
     would rot silently. The name's entrance reads --hero-top to work out
     how far it has to travel to start in the MIDDLE of the window, which
     only worked while the name's distance from the top of the window WAS
     this padding — true under flex-start, false under centring by exactly
     half the surplus. device.js measures the name's real offset and writes
     it here; the value below is the fallback for the frame before that
     runs and for the case where the script does not.

     The test this has to keep passing is one sentence: at 1440x900 AND at
     1440x800, both lines of type and the whole device — canvas, not box —
     stand inside the first screen, with the ask under them. */
  /* 🔴 THE NAME SITS ON THE HEADER'S LINE. This is not "a small top
     padding" — it is a measured alignment, and the number is derived
     rather than eyeballed:

       header padding-top   clamp(20px, 2.6vw, 34px)   (header.css)
       header logo height   7.2em × 115/421 = 1.966em, em = the bar's
                            own clamp(1rem, 1vw, 1.5rem)
       this name's height   1.1 × clamp(2rem, 4vw, 3.25rem)

       name top = header pad + (logo height − name height) / 2

     At 1440 that is 34 + (28.3 − 57.2)/2 = 19.5px — and 19.5px was
     tried, on the wordmark's own line, and read as the name having
     fallen INTO the header rather than as a hero starting high. What is
     left of that idea is the direction, walked back twice: 9vh (81px)
     → 1.4vw (20px, on the bar — too high, the name read as having
     fallen into the header) → 7vh (63px). Between the two things that
     were wrong. The clamp ends keep it off the logo
     on a short window and stop it drifting on a tall one. */
  padding: var(--hero-pad) 0;
  overflow: hidden;
  /* 🔴 NO `isolation: isolate` — it was here to keep the WebGL canvas's
     layers to itself, and with the canvas gone it does active harm.
     Isolating this element makes it the backdrop root for anything
     blending inside it, so .dv__hero-shot's `multiply` composited
     against nothing and the render's white background stayed white. The
     blend has to be able to see the rail's tint behind it.

     🔴 TRANSPARENT, and that is what lets the tint scroll away. A
     background here is pinned to the window for the whole rail — which
     is how the sky gradient that used to live on this rule became a
     ground that changed colour under the reader. The tint is on
     .dv__hero-rail now (see the measurements there); this element paints
     nothing and the rail shows through it. */
  background: none;
}

.dv__hero-in {
  position: relative; z-index: 1;
  display: flex; flex-direction: column; align-items: center;
  text-align: center;
  width: 100%;
}

/* ---------- the name, and ONLY the name ----------
   🔴 ONE of the two lines is back. The display line under it is still
   cut — "Family life. Sorted." at up to 8.5rem, ink for the first beat
   and a clipped blue gradient for the second — and it stays cut for the
   reason it was cut: every line over the object is a line the object
   has to survive. The name is not that. The reference puts the product's
   name at the top of its first screen and then gets out of the way, and
   a hero with no name at all is a picture of a thing nobody has been
   told the name of.

   🔴 The logo's own face, and the only type on the site set in it. The
   wordmark is a drawing (assets/nestify-logo.svg) — this is the one
   place the letters are live text, so it is the one place the family has
   to be loaded. The request is in device.html's font link and it is not
   in index.html's, deliberately.

   🔴 SMALLER THAN THE OLD DISPLAY LINE BY A LONG WAY, and bigger than
   the old kicker. It was 1.375→2.125rem when it was a label ABOVE a
   headline; there is no headline to label now, so it carries the top of
   the screen on its own — but the third term of .dv__hero-fig's width is
   the height everything-that-is-not-the-object costs, and every rem here
   comes straight out of the product. This is the largest it can be while
   the pill still clears the fold at 1440×800.

   If it ever needs the display line back, both are recorded above the
   sizes here in git-less form: .dv__hero-tag, clamp(3rem, 9.6vw, 8.5rem),
   two spans, .is-warm ink and .is-cool a clipped gradient of #1668b8 →
   #1e88e5 → #2b8ee8, with an @supports fallback so it does not render as
   transparent text where background-clip is missing. */
/* 🔴 BEHIND THE OBJECT, and it is the type that gives way. The device
   crosses the lockup and the words go under it — the reference does the
   same thing with its own headline and its own hardware, and a product
   page where the product is in front is the right way round.

   🔴 It works because the thing doing the crossing is the CANVAS. The
   overlap is not the still — .dv__hero-3d is 165% of the figure and
   centred on it, so the WebGL object reaches well above this box while
   the picture never does — and a canvas is transparent, so the type is
   genuinely occluded where the device is and untouched everywhere else.

   🔴 THE FALLBACK IS THE ONE THAT MISBEHAVES, and it is worth knowing
   which failure it is. With no WebGL the still shows instead, and the
   still blends with `multiply` (the render has an opaque white
   background — see the note on that rule): it does not occlude what is
   under it, it multiplies with it. White areas leave the type alone,
   dark ones take it away, but a LIGHT screen — the calendar, the
   painting — lets the word read faintly through the glass. The still
   also does not overshoot the figure, so in practice it barely reaches
   the type at all. The real fix, if it ever matters, is a cut-out
   render — a PNG with an alpha channel instead of a JPG on white — and
   then `multiply` comes off that rule entirely. */
.dv__hero-name,
.dv__hero-word { position: relative; z-index: 0; }

/* ============================================================
   THE FIRST SCREEN ARRIVES IN FOUR BEATS

   The reference does not fade its hero in, it PLAYS it: the product's
   name alone in the middle of the window, then the name travels up to
   its place, then the headline, then the object, then the ask. Ours is
   the same score, and every beat below is a transition-delay off the
   ONE class footer.js writes (.is-in). No timeline, no script, nothing
   that can fall out of step with itself — and every element's resting
   state is still its own first frame, so a reader who arrives with
   reduced motion gets the finished screen and nothing else.

     0.10s  name fades up, centred in the window       (lands 1.00)
     1.05s  name shrinks away — gone by 1.85
     1.70s  object rises into the space it left        (lands 2.60)
     2.20s  word: blur/scale off, then the colour      (lands 4.60)
     2.70s  name comes back up into its place          (lands 3.80)
     3.70s  the ask rises                              (lands 5.30)

   🔴 THE OBJECT NOW COMES SECOND, NOT FOURTH, and that single move is
   what makes this a sequence rather than a list. It used to be: name
   arrives, name leaves for the top, word, object — four things appearing
   in four places, none of them aware of each other. The order above has
   the beats acting ON one another instead.

     · the name arrives alone in the middle, so for most of a second it
       is the only thing on the screen and you read it;
     · the object rises into exactly that spot and COVERS it. The name is
       not faded out — it is still there, behind. Nothing has to be
       animated away, which is why this reads as an object arriving
       rather than as a caption being replaced;
     · the word appears above the object, in the space the name has not
       reached yet;
     · and then the name comes UP OUT FROM BEHIND the object to sit over
       the word. It is the same travel it always made, but it now starts
       from concealment, so it reads as being pushed up by the thing that
       covered it rather than as text sliding into place.

   The whole trick is that the name's two beats — arrive, travel — are no
   longer adjacent. Putting the object between them turns a fade and a
   move into cause and effect.

   🔴 IT DEPENDS ON THE OBJECT PAINTING OVER THE TYPE, which is already
   true and is load-bearing now rather than incidental: see the note on
   .dv__hero-word's font-size, where the type went behind the device to
   buy the object its height. If that stacking is ever reversed, this
   sequence silently becomes the old one with odd timings.

   🔴 The beats still OVERLAP on purpose — the object starts while the
   name is still fading up, the name's travel while the word is still
   taking its colour. A hero that waits for each thing to finish reads as
   a slideshow. The numbers above are start times, not slots.
   ============================================================ */

/* 🔴 IT STARTS IN THE MIDDLE OF THE WINDOW. 50svh is the centre, −50%
   is half the name's own height, and −var(--hero-top) is the distance
   this element already sits from the top of the screen — without that
   last term it would start half a header lower than the middle. It is a
   TRANSFORM, so nothing under it moves while it travels: the word and
   the object hold their final places the whole time, invisible. */
/* 🔴 ONE ANIMATION, NOT FOUR TRANSITIONS, and it is the only beat on this
   screen that needs to be. Everything else here happens once: a thing
   appears and stays. This one appears, LEAVES, and comes back somewhere
   else — three states — and a transition can only ever describe two.

   🔴 IT FADES OUT. IT IS NOT COVERED. That was the first attempt: the
   object paints over the type (see .dv__hero-word's font-size note), so
   simply letting it rise into the name ought to hide it. It does not.
   Occlusion only hides what is actually BEHIND something, and the object
   is two pads at an angle with a gap between them and daylight around
   their corners — so the name did not vanish, it got eaten into. The tail
   of "Hub" sat in the gap between the two pads, legible, for a second and
   a half. There is no z-index that fixes that, because the problem is the
   silhouette, not the stacking order.

   So the name yields. It fades as the object arrives, holds at zero while
   the object takes the screen, and comes back UP into its place at the
   top — which is also what makes the return read as a move rather than a
   pop: it starts 2.5rem low and rises as it fades in.

   🔴 The travel happens WHILE IT IS INVISIBLE, between 36.6% and 70.3%,
   and it is a step rather than a tween on purpose. Nothing is on screen
   to see it, so animating across the full drop would only spend a second
   of the timeline moving something nobody can look at, and it would tie
   the return's speed to the length of a journey that is not being shown.
   Cut instead, and the visible part is a short, fast rise of its own.

   🔴 IT SHRINKS AS IT GOES, AND IT GOES BEFORE THE OBJECT ARRIVES.
   The first version of this exit was a 0.35s straight fade timed to the
   object's arrival, and it read as the name being wiped off the object's
   FACE — because that is what was happening. A word losing opacity while
   something opaque grows in underneath it is not a word leaving, it is a
   word being painted over, and the eye reads the surface rather than the
   departure.

   Two changes fix it and they are one idea. The exit takes 0.8s instead
   of 0.35 and carries a scale down to .82, so the name recedes rather
   than dissolves — smaller reads as further away, which is a direction,
   and a thing that has gone somewhere does not need anything to have
   covered it. And it now finishes BEFORE the object has any presence:
   measured against --ease-out over the object's own .9s fade, the name
   is at zero at 1.85s and the object is at 0.10 there. It never sits
   legible on a solid face because the face is not there yet.

   🔴 The opacity LAGS the scale, which is what makes it "shrink, then
   go" rather than "shrink and go". The 38.6% keyframe is the whole
   mechanism: the size is already 60% of the way down while the opacity
   has only given up a fifth. One animation cannot put two curves on two
   properties, so the curve is drawn as a point instead.

   Absolute times, at duration 3.7s + 0.1s delay:
       0.10s  fade up, centred in the window
       1.00s  landed, full size
       1.05s  starts to recede
       1.53s  noticeably smaller, still clearly readable
       1.85s  gone — the object is 10% arrived
       2.70s  starts back, 2.5rem below its place
       3.80s  home */
.dv__hero-name {
  --dv-name-drop: calc(50svh - 50% - var(--hero-top));
}
.dv__hero-name[data-reveal="fade"] {
  transform: translate3d(0, var(--dv-name-drop), 0);
  transition: none;
}
/* 🔴 `forwards`, not `both`. With `both` the backwards fill would apply
   the 0% frame during the delay — which is the same as the resting state
   here, so it would look identical and would silently stop being so the
   moment either changes. The resting state belongs to the rule above; the
   animation only owns what happens after it starts. */
.dv__hero-name.is-in {
  opacity: 1;
  transform: none;
  animation: dv-name-play 3.7s .1s forwards;
}
@keyframes dv-name-play {
  0% {                                    /* 0.10s */
    opacity: 0; transform: translate3d(0, var(--dv-name-drop), 0) scale(1);
    animation-timing-function: ease;
  }
  24.3% {                                 /* 1.00s — landed, full size */
    opacity: 1; transform: translate3d(0, var(--dv-name-drop), 0) scale(1);
    animation-timing-function: linear;
  }
  25.7% {                                 /* 1.05s — starts to recede */
    opacity: 1; transform: translate3d(0, var(--dv-name-drop), 0) scale(1);
    animation-timing-function: cubic-bezier(.3, .1, .7, .5);
  }
  38.6% {                                 /* 1.53s — size 60% gone, opacity 20% */
    opacity: .8; transform: translate3d(0, var(--dv-name-drop), 0) scale(.9);
    animation-timing-function: cubic-bezier(.5, 0, .8, .6);
  }
  47.3% {                                 /* 1.85s — gone */
    opacity: 0; transform: translate3d(0, var(--dv-name-drop), 0) scale(.82);
    animation-timing-function: step-end;
  }
  47.4% {                                 /* the cut, unseen */
    opacity: 0; transform: translate3d(0, 2.5rem, 0) scale(1);
    animation-timing-function: linear;
  }
  70.3% {                                 /* 2.70s — the word has its colour */
    opacity: 0; transform: translate3d(0, 2.5rem, 0) scale(1);
    animation-timing-function: cubic-bezier(.2, .72, .2, 1);
  }
  100% { opacity: 1; transform: none; }   /* 3.80s */
}

.dv__hero-name {
  margin: 0;
  font-family: "Baloo Chettan 2", var(--font--primary);
  /* 🔴 2.35vw / 3.4rem, from 2.2vw / 1.75rem. The old ceiling bound above
     1273px — the eyebrow was the FIRST of the three to freeze, so on a big
     screen it was not just small, it was small relative to the two things
     it is supposed to be an eyebrow to. Moves with them; see .dv__hero-fig. */
  font-size: clamp(1.25rem, 2.35vw, 3.4rem);
  font-weight: 600;
  line-height: 1.1;
  /* 🔴 NOT the display letter-spacing. Baloo is a rounded face with wide
     sidebearings of its own; the -.03em the rest of the site's headings
     carry closes it up into a logotype, which is the one thing this must
     not look like — the wordmark is a drawing and this is a word. */
  letter-spacing: -.01em;
  color: var(--nes-ink);
}

/* ---------- the one word ----------
   🔴 EVERY NUMBER IN HERE IS LIFTED, and the source is worth writing
   down because none of it is guessable. apple.com/ipad-air, hero:

     <h1><img class="logo-eyebrow"></h1>            ← the name, 129×30
     <h2 class="headline-wrap" data-text="Whoosh.">
       <span class="headline text-gradient">Whoosh.</span>

     .headline-wrap { filter: blur(5px); transform: scale(.95); opacity: 0;
       transition: opacity 1.5s ease,
                   filter 2.7s cubic-bezier(.54,0,.43,1),
                   transform 2.7s cubic-bezier(.33,.3,.53,1) }
     .headline-wrap:before { content: attr(data-text)/""; color: #d0eaf4;
                             position: absolute; top: 0 }
     .headline { opacity: 0; transition: opacity 3.5s ease .5s }
     .text-gradient { background-image:
                        linear-gradient(180deg, #004C94 45%, #297BC4 90%);
                      background-clip: text;
                      -webkit-text-fill-color: transparent;
                      padding: 4px; margin: -4px }
     .typography-overview-hero-headline { font-size: 160px;
       line-height: .875; font-weight: 600; letter-spacing: -.04em }

   Three things happen and they are deliberately out of step: the word
   FADES in over 1.5s, it un-blurs and un-shrinks over 2.7s, and the
   colour lands over 3.5s starting half a second late. That last one is
   the whole effect — the word is already standing there, in flat pale
   blue, when the deep blue comes up through it.

   🔴 THE BLUE IS THEIRS, not ours, and that is the answer to "our
   gradient blue is not their blue". #004C94 → #297BC4 is a navy going
   to a mid blue; every blue in styles.css is lighter and brighter than
   both (--nes-blue-3, the darkest, is #1573c9). Ours read as a tint of
   the brand; theirs reads as depth. This is the one place on the site
   that is allowed the borrowed pair — the pill under it is still
   --nes-blue-3, the panels below are still --nes-sky.

   🔴 160px is not on the table. That headline shares its screen with a
   video that is CROPPED by the window; ours shares it with a whole
   object and a pill that both have to survive above the fold. 6rem at
   the top of the clamp is what is left after .dv__hero-fig's height
   budget is paid — see the note there for the sum. */
.dv__hero-word {
  position: relative;
  margin: .12em 0 0;
  font-family: var(--font--primary);
  /* 🔴 THEIR SIZE, near enough: 160px at the top of the clamp against the
     reference's 160, with their line-height (.875) and their tracking
     (-.04em) rather than the site's display defaults. The note above said
     160 was "not on the table" because the word had to hold its own band
     above the object — that stopped being true the moment the type went
     BEHIND the device: the lockup and the object share the same vertical
     space now, so the word costs the figure the part of itself the device
     is not already standing in front of (the negative margin on
     .dv__hero-fig is where that trade is actually made). */
  /* 🔴 10.4vw / 15rem, from 8.8vw / 8rem — and it moved because the OBJECT
     moved. The note on .dv__hero-fig is explicit that the name, the word
     and the pad were tuned as a group and have to move as one; 8rem froze
     the word at 128px above 1454px, which is the same freeze the object
     had. Same ratio to the object as before, at every width now rather
     than at 1440 only. */
  font-size: clamp(2.875rem, 10.4vw, 15rem);
  font-weight: 600;
  line-height: .875;
  letter-spacing: -.04em;
  /* the resting state IS the arrival's first frame */
  opacity: 0;
  filter: blur(5px);
  transform: scale(.95);
  /* 🔴 Their durations, compressed, and all of it held until the name
     has cleared: this is the third beat, so every one of the three
     carries the same 2.2s delay. The reference's 1.5/2.7/2.7 would put
     the colour's landing past six seconds with a beat still to come
     after it. */
  transition:
    opacity 1s ease 2.2s,
    filter 1.9s cubic-bezier(.54, 0, .43, 1) 2.2s,
    transform 1.9s cubic-bezier(.33, .3, .53, 1) 2.2s;
}
/* 🔴 A REAL ELEMENT, not their content: attr(data-text)/"" pseudo. The
   alt-text half of that syntax is what keeps a screen reader from
   reading the word twice, and it is the half with the patchiest
   support — where it is missing the whole declaration is invalid and
   the under copy simply never paints. A span with aria-hidden says the
   same thing in markup that cannot half-work. */
.dv__hero-word-under {
  position: absolute; inset: 0;
  color: #d0eaf4;                      /* their own pale blue, sampled */
}
.dv__hero-word-ink {
  position: relative; z-index: 1;
  opacity: 0;
  transition: opacity 2s ease 2.6s;
  /* 🔴 BACK TO THE ORIGINAL TWO-STOP BLUE, and three replacements were
     tried on the way here. Keeping the record because each one was
     measured, and the measurements are reusable even though the colours
     were not:

       site tokens   --nes-blue-3 → --nes-blue-2. Correct by the palette
                     and flatter than this: the two are close in
                     lightness, so the ramp had almost nowhere to travel.
       a rainbow     blue → gold has to cross the wheel, and sampling
                     every 2% showed exactly where "dirty" comes from —
                     the teal route bottoms out at 21% saturation on an
                     olive #5d8b5b, a single violet straight into gold is
                     worse at 18%, violet + magenta held 46%. If a
                     multi-hue ramp is ever wanted, that is the method:
                     watch the SATURATION floor, not the colours.
       glass         thickness rather than transparency, because on this
                     ground neither of the obvious moves exists — a
                     translucent blue drops under 3:1 at alpha 0.7, and a
                     white specular measures 1.07:1 against a near-white
                     page. It was legible and it was not better.

     #08528f → #3286cf, and getting here took two corrections in opposite
     directions, which between them say something the individual colours
     do not:

       #004C94 → #297BC4   the original. Too heavy — 7.85 falling to 4.09
                           is dark type falling to fairly dark type, and
                           at that depth blue stops being blue and reads
                           as ink.
       #12639f → #3286cf   both stops lifted. Too pale — and the reason
                           is not that they were too light individually,
                           it is that the FALL went with them, 1.92x down
                           to 1.64x. A ramp with nowhere to travel reads
                           as a flat colour that someone has smudged.

     So the answer was never a lightness. It was the RANGE. The head goes
     back down to 7.39 and the foot stays where the lift put it at 3.55,
     which is a 2.08x fall — wider than the original ever was. Deep top,
     light bottom, more distance between them than either previous
     attempt. That is what makes it read as a gradient rather than as a
     shade of blue.

     🔴 THE FOOT IS THE CONSTRAINT, not the head. Display type needs 3:1
     and the bottom stop is the lightest thing here, so it is the only one
     that can actually fail. 3.55 leaves 0.55 of margin; #3d92da, one step
     lighter, measures 3.06, and a stop that clears the line by 0.06 is not
     a margin, it is a coincidence. If this ever wants more range again,
     take it out of the HEAD — there is room down to about 8.0 before it is
     ink — and leave the foot alone.

     🔴 AND THEN IT WENT SKY. #08528f → #3286cf was still reading heavy
     at the head — 7.39 is dark enough that the eye takes it as ink with
     a blue cast rather than as a blue. The pair now is

       #0c66a2 → #108ad5   5.62 falling to 3.44, a 1.63x fall

     which is LESS range than the 2.08 above, and that is a real trade
     made on purpose: what replaces the range is SATURATION. Both stops
     moved to hue 203–204° (from 207–209) and the foot went from 62% to
     86% saturation, so the ramp now travels through chroma where it used
     to travel through lightness. That is what "sky" is — a blue that is
     saturated and light at once — and it is why this does not smudge the
     way the earlier flat attempt (1.64x, both stops desaturated) did.

     The foot still clears display type's 3:1 with 0.44 of margin, which
     is the number that must not be spent. The head has plenty of room in
     both directions now.

     Neither stop is a palette token, which is the one thing about this
     pair worth flagging. But the palette's blues sit within a stop or two
     of each other in lightness, so a token pair has no range to give,
     which is exactly why that version was tried and abandoned above. */
  background-image: linear-gradient(180deg, #0c66a2 45%, #108ad5 90%);
  -webkit-background-clip: text;
          background-clip: text;
  -webkit-text-fill-color: transparent;
          color: transparent;
  /* 🔴 Their 4px of padding pulled back out again, and it is not
     decoration: a gradient clipped to text is clipped to the element's
     BOX, and a box that is tight to the glyphs cuts the overshoot off
     the round letters. Padding grows the box, the negative margin puts
     the layout back. */
  padding-inline: 6px; margin-inline: -6px;
}
/* 🔴 The fallback the old display line carried, kept: a gradient clipped
   to text is transparent type plus a background, so where the clip is
   missing the word renders as NOTHING — the one failure mode that takes
   the headline off the page entirely. One flat blue from the middle of
   the ramp, and the section still says what it says. */
@supports not ((background-clip: text) or (-webkit-background-clip: text)) {
  .dv__hero-word-ink {
    background-image: none;
    color: #14649c;
    -webkit-text-fill-color: currentColor;
  }
}
/* the cue is footer.js's, the same .is-in every other reveal on the
   page waits for — data-reveal="whoosh" only has to be A value, not
   one footer.css knows, so nothing in the shared fade rules applies
   here and this file owns the whole entrance */
.dv__hero-word.is-in { opacity: 1; filter: blur(0); transform: none; }
.dv__hero-word.is-in .dv__hero-word-ink { opacity: 1; }

/* read, not seen — the rest of the heading. The visible word is "Nestie";
   this is what carries "the Nestify Family Hub" for anything that is
   reading the document rather than looking at it. */
.dv__sr {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  overflow: hidden; white-space: nowrap;
  clip-path: inset(50%);
}

/* ---------- the object ----------
   Wider than the type block on purpose: the copy is read at arm's
   length and the device is looked at.

   🔴 The third term in the min() is what keeps the pill above the
   fold: 18rem is everything on this screen that is NOT the figure —
   the section's own padding top and bottom, this element's two margins
   and the ask under it. What is left of the window goes to the device,
   at its own ratio. It was 29rem while the name and the line stood
   above the object; they are gone and the ~11rem they held is the
   object's now.

   The max() is the floor. On a short window the subtraction runs to
   nothing, and a width that resolves to zero is a hero with no
   product in it — better to be taller than the window and say so. */
.dv__hero-fig {
  position: relative;
  /* 🔴 64rem, up from 56. That ceiling existed to keep the object off
     the headline it was overlapping — small enough that the words
     survived it. There are no words. */
  /* 🔴 It is allowed to run off the BOTTOM of the stage, and that is
     what buys the size. Every earlier value here was solving the same
     impossible sum — fit the object, the headline and the ask into one
     screen — and the object always lost. The ask moved above it (see
     `order` below), so the only thing under the object is the edge of
     the stage, which crops it. Cropped is not a compromise here: the
     reference crops its own product at the frame, because an object
     that runs out of the picture reads as bigger than the picture.

     The floor is the second term: never narrower than 26rem, however
     short the window, or a laptop gets a postage stamp. */
  /* 🔴 Down from 64rem / (100svh - 18rem). At that size the object was
     the whole window — it ran off both sides on anything short and the
     stage stopped reading as a frame around it, which is the difference
     between a product shot and a wallpaper. It wants to be large and to
     have air; the second term is what buys the air back. */
  /* 🔴 THE HEIGHT BUDGET IS GONE. This term used to be
     `max(22rem, calc((100svh − Nrem) × 16/9))` — the object sized by
     what was left of ONE WINDOW after the type and the ask had taken
     theirs — and N was retuned four times (18 → 26 → 28 → 34 → 24) as
     things were added to and taken out of the first screen. Every one of
     those numbers was a hand-kept sum of other people's margins, and
     every one of them went stale the moment a line moved.

     The section is taller than a screen now, so there is nothing to
     divide up: the object is sized by what looks right, which is a cap
     and the column it sits in, and the page grows to fit it.

     🔴 28rem = 448px, DOWN FROM 34, and the 34 was itself walked the
     length of its range a screenshot at a time — 50, 56, 60, 64, 54, 46,
     38, 34 — against apple.com/ipad-air rather than against taste: they
     cap their hero media at `--video-max-width: 630px`, 44% of a 1440
     window. Ours is a PAIR of pads in a canvas 165% of this box, so 34rem
     painted ~650px of visible object, which was 45% — their proportion,
     with two devices instead of one.

     🔴 SO THIS IS NOW DELIBERATELY UNDER THE REFERENCE, and that is the
     change: the whole lockup came down about a fifth together — the name
     3.25rem → 2.6, the word 10rem → 8, this 34 → 28 — because the three of
     them at reference scale filled the window rather than sat in it. 28rem
     paints ~535px, about 37% of a 1440 window. If any one of the three
     moves again, move all three: they were tuned as a group and they read
     as a group.

     🔴 THE WORD STILL SURVIVES AT EVERY WIDTH, which is the constraint
     that caps this. The headline is sized in vw and the object in rem, so
     they scale apart: a narrow window shrinks the type and not the pad,
     and each step above 34 had the near corner eating a letter somewhere
     in the range (checked at 1200 and 1440). Coming DOWN only ever helps
     that, so 28 is safe on the same test.

     🔴 THE FIGURE BOX IS NOT THE OBJECT. Every over-large value here was
     read off this box and the object came out bigger than expected: the
     canvas is 165% of it, so 608px of figure is ~1000px of canvas. If
     this number moves, look at the SCREENSHOT, not at the number.

     The svh term that is left is a FLOOR-GUARD, not a budget: on a very
     short window (a laptop at 1440×700, a landscape phone) an object as
     tall as the whole viewport means the name and the word are pushed
     off the top of it before anything has been read. It only binds under
     ~880px of height, and the 22rem max() under that is the point where
     it stops shrinking and the page simply scrolls. */
  /* 🔴 A PROPORTION, NOT A LENGTH. This was a flat `28rem`, and a flat rem
     does not grow — so above about 1280px the object stopped getting bigger
     and the window kept getting bigger around it. Measured, that is the
     whole of the "too small on a big screen" complaint:

         1280   448px   42% of the window
         1440   448px   37%
         1920   448px   28%
         3012   448px   18%

     The `max(18rem, (100svh - 16rem) * 16/9)` term reads like the thing
     that sizes this and it is not: at any normal window height it resolves
     to more than a thousand pixels and never wins. It is a SHORT-WINDOW
     GUARD and nothing else — on a 600px-tall window it takes over and
     stops the object running off the bottom. Left exactly as it was.

     37vw holds the reference's own proportion at every width instead of at
     one: apple.com/ipad-air caps its hero media at 630px of a 1440 window,
     44%, and 37vw x the canvas's 165% overhang paints about that. The
     ceiling is 52rem because a proportion with no ceiling eventually makes
     a 5K monitor a poster; it binds above 2249px, which is where the
     reference's own absolute cap would have.

     🔴 The floor moved 18rem -> 20rem only so `clamp` reads in one glance;
     100% still wins on anything narrow. */
  /* 🔴 THE THIRD TERM IS THE HEIGHT BUDGET AND IT HAD GONE STALE. It was
     `max(18rem, (100svh - 16rem) * 16/9)`, and both numbers in it were
     wrong once this box started scaling with the viewport:

       16rem   stood for "everything else on the first screen" as a
               CONSTANT, from when the type was capped in rem. The type is
               vw now, so everything else grows with the window and 256px
               stopped being true immediately. Measured, it is 372px at
               1280 and 529px at 1999.
       16/9    stood for this box's shape. It is not 16/9 — the picture
               inside is shop.jpg at 1800x1074, so the box is 0.5967 as
               tall as it is wide, measured identically at every width.

     Between them the guard was so generous it never bound, and the
     composition simply overflowed one screen on anything short. Measured
     before this change, total content height against the window:

         1280 x  720    751   OVER by 31       1506 x  892    875   fits
         1400 x  760    806   OVER by 46       1920 x 1080   1070   fits
         1499 x  826    856   OVER by 30       1999 x 1101   1102   OVER by 1

     which is what "the padding is equal top and bottom" stopped being
     true on: centring cannot balance a surplus that does not exist, and
     the foot of the section went under the fold.

     🔴 `22vw + 7rem` IS FITTED, NOT ASSUMED. Everything-but-the-object is
     not a constant and it is not a pure proportion either — the ask has
     fixed-pixel parts, so it is a line rather than a ratio. Measured at
     six sizes it runs 372px at 1280 to 529px at 1999; 22vw + 7rem sits a
     little above that at every one of them, deliberately, because
     overestimating shrinks the object slightly and underestimating puts
     the ask off the screen.

     🔴 `* 1800 / 1074` is shop.jpg's own aspect, written as its pixel
     dimensions so it is checkable against the file rather than being a
     decimal nobody can source. If the still is ever replaced at a
     different shape, this is the second place to change (the first is
     the <img>'s width/height attributes in device.html). */
  width: min(
    100%,
    clamp(20rem, 37vw, 52rem),
    max(18rem, calc((100svh - 2 * var(--hero-pad) - 22vw - 7rem) * 1800 / 1074))
  );
  /* 🔴 Plain margins, both positive. The top one used to be NEGATIVE and
     keyed to the headline's em: the object's near corner crossed the
     last line of type, so the two read as one picture with the object
     in front of the words rather than stacked under them. With no type
     in the section there is nothing to cross — a negative margin now
     would only pull the object off the top of its own stage. What is
     left is air above and below, and the stage centres the rest. */
  /* 🔴 The bottom margin is bigger than it looks like it needs to be, and
     it is not padding for its own sake: the 3D canvas is 168% of the pad
     and centred on it, so the object's lowest corner sits a long way
     BELOW this box. Measured at 1440×900 the pill's top and the device's
     bottom corner were on the same pixel — the button read as stuck to
     the hardware. This clears it. */
  /* 🔴 POSITIVE, AND IT IS THE CANVAS THIS IS PAYING FOR, NOT THE BOX.
     Three values have been through here — negative (the object riding up
     into the word), then most of the way back, now clear of it — and the
     thing that made the first two miss is that this margin moves the
     figure's BOX while the overlap you can see comes from the object
     drawn inside it. .dv__hero-3d is 165% of the figure at 16/10, so at
     a 960px figure the canvas is ~990px tall against a 573px box: it
     hangs ~208px ABOVE the top of the element this margin is nudging.
     Which is why a -40px margin read as "the device has eaten three
     letters" and why undoing it to -10 barely moved the corner.

     3.2vh (29px at 900) is measured off a screenshot rather than
     derived: it is where the near pad's corner sits just under the foot
     of the word — close enough that the two read as one picture, clear
     enough that no letter is touched. It was 1.5vh and the corner was
     on the "up." — a hair, but a hair on the one word the screen is
     for. Every letter is readable and the
     object is still in front of the word, which is the whole point of
     the layering — a word behind a device that touches none of it reads
     as two pictures on one page.

     🔴 IT MOVES WITH THE CAP ABOVE, AND NOT PROPORTIONALLY. 6.5vh at
     60rem, 3.2 at 54, 1.6 at 46, -2.2 at 38, 3.2 at 34 — the last one
     going back UP because the object finally stopped needing to be
     pulled into the type to touch it, and then further up again when
     the word grew from "Sorted." to "Heads up." and reached the corner
     on its own. A narrower figure has a
     shorter canvas and less overshoot, so every step down opens a gap
     this has to close, and past a point it has to go negative to close
     it at all. Not a straight ratio either: the pads are tilted and
     off-centre in the scene, so as the object narrows the corner that
     does the crossing moves INWARD as well as down. Change the cap, then
     look at the word.

     🔴 It is vh, not rem, because what it is spending is the distance
     between the type and the object, and that gap should breathe with
     the window. The clamp ends are guards: 1rem so a short window does
     not close the gap entirely, 4.5rem so a tall one does not open a
     hole between the word and the thing it is about. */
  /* 🔴 UP FROM 1.5vh, AND SHRINKING THE THREE DID NOT DO THIS ON ITS OWN.
     The name, the word and the figure all came down ~20% together, which
     is exactly why the overlap did not change: the canvas is a multiple of
     the figure and the word is sized in vw, so scaling everything scales
     the crossing too. Measured, before and after the shrink —

                     word line box   object top vs word bottom
       34rem / 10rem     139px            34px into it   (24%)
       28rem / 8rem      111px            25px into it   (23%)

     — the same bite, in a smaller mouth. The only thing that changes how
     far the object climbs into the type is this margin, because it is the
     one number that moves the figure relative to the line above it rather
     than scaling both.

     🔴 2vh, AND 3.4 WAS TOO FAR. Measured off a real WebGL render at
     1440x900 — headless with SwiftShader, so this is the 3D PAIR and not
     the still fallback, which is one pad and would have put the crossing
     corner somewhere else entirely:

       word ink, cap to baseline     90px tall
       the "p" descender ends        12px below the baseline
       object's topmost pixel        4px BELOW the descender

     4px of daylight is not "a little overlap", it is a miss: the word and
     the object stop touching and the lockup reads as two pictures stacked,
     which is the failure the note below is about. 2vh brings the object
     back up ~13px so its top corner crosses the descender and nothing
     else — plainly IN FRONT of the word, without taking a letter.

     🔴 MEASURE THE GAP, NOT THE POSITIONS. Two renders of the identical
     stylesheet put the whole lockup 62px apart, because [data-reveal] and
     the 3D entrance are both still settling when the shot is taken. The
     DISTANCE between the word and the object was 4px in both. Any future
     tuning here has to read the difference, not the y values. */
  margin: clamp(.5rem, 2vh, 2rem) 0 clamp(2.5rem, 7vh, 5rem);
  z-index: 1;
}

/* 🔴 THE NAME FIRST, THE OBJECT A BEAT LATER. Everything in this hero is
   on screen at load, so footer.js's observer lights all of it in the same
   frame and the order has to come from a delay, not from the scroll. .28s
   is the whole sequence: it is under the name's own .9s fade, so the two
   overlap rather than queue — the name is still arriving when the device
   starts to — which is what the reference does and what keeps this from
   reading as a slideshow.

   On the wrapper, not on the picture: the still and the canvas inside it
   cross-fade on their own rules and neither should learn about this. */
/* 🔴 1.7s. It was 3s (the last beat, arriving to an empty middle) and
   then 1s, which put it on top of the name's exit — the name lost its
   opacity while this gained its own, and what you saw was a word being
   wiped off a surface rather than a word leaving. 1.7s is measured, not
   chosen: the name is at zero at 1.85s, and over this element's own .9s
   --ease-out fade, 1.7s puts it at 0.10 there. The space is empty before
   the object claims it. See the four-beat ledger above. */
.dv__hero-fig[data-reveal="fade"] { transition-delay: 1.7s; }

/* 🔴 The ask goes ABOVE the object. It was under it, which is the order
   the page reads in — object, then what to do about it — and it is the
   order that kept the object small: anything below the device has to be
   ON the screen, so the device could only ever be as big as the room
   left over. Under the headline the pill costs 45px of a band that was
   empty anyway, and the object below it is free to be as large as the
   stage and then some.

   `order`, not a move in the markup: the reading order stays name →
   line → object → ask for anything that does not do flex layout, which
   is the order the section is written in. */
/* 🔴 THE THIRD ANSWER, and the two above it are why it is the right one.
   The ask was under the object in flow (the object could only be as big
   as the room left over), then briefly above it with `order` (which got
   the size back by making the reading order lie). It is now OUT of the
   flow entirely, at the foot of the stage, arriving on the scroll — the
   markup order is still object → ask, the object gets the whole screen,
   and nothing is cropped that anyone needed to read. The rule that does
   it is further down, next to the lockup's own type. */

/* (the pool under the object lived here — .dv__hero-glow, a soft radial
   ellipse at 185% of the figure, drawn under the pair so the object read
   as floating in light rather than pasted onto a flat page. It went
   through every colour it could: --nes-sky, then --nes-haze, then a
   blue-grey, then the ground's own hue ten points down, then eighteen.
   The last of those was the first that could actually be SEEN — it had
   been hiding behind the object the whole time, at 132% of a figure the
   pair paints past — and seeing it is what settled it. The hero stands
   on flat --page-bg now, with nothing behind the object at all.

   The element went out of device.html with this rule, and --nes-slate
   went out of styles.css with both. /tmp is not a repository: if this
   comes back, it is the four-stop version that is worth rebuilding —
   62/44/18/transparent at 0/50%/74%/90%, on a colour that matches the
   ground's hue and saturation exactly and only drops its lightness.) */

/* ---------- the object, as one picture ----------
   🔴 A render, not a runtime build. What lived here was the sizing for a
   WebGL canvas, the cross-fade that swapped it in over a drawn pad, and
   the `:has()` rules that decided which of three layers won. The hero is
   one <img> now: whatever is in assets/device/shop.jpg is what you see,
   there is no fallback to keep in step with it, and nothing here can
   fail to a different picture than the one that was designed.

   🔴 width: 100%, not a share of it. The render carries its own margin —
   the device does not touch the edges of the file — so a second inset
   here would be air on top of air. If the object wants to be bigger or
   smaller, .dv__hero-fig's own width is the one place to change it. */
.dv__hero-shot {
  position: relative; z-index: 1;
  display: block;
  width: 100%; height: auto;
  /* 🔴 MULTIPLY, because the file is not cut out. shop.jpg has an opaque
     white background — checked, the corner pixels are 255,255,255 at full
     alpha — so dropped straight onto the tinted ground it is a white
     rectangle with a device in the middle of it. Multiply makes white the
     identity: every pixel of that background leaves the ground exactly as
     it was, and the render's own soft shadow survives as a darkening
     instead of being cut away with the background it lives on.

     The cost is that the device takes a little of the tint, which on a
     ground this pale is a few points and reads as the object sitting in
     the same light as the page. If a cut-out with real alpha turns up,
     drop this line and use it — that is strictly better, and this is the
     honest way to ship a white-backed file until then. */
  mix-blend-mode: multiply;
}

/* ---------- the object in three dimensions ----------
   🔴 The canvas covers the still and is LARGER than it. A canvas cropped
   to the picture's own box clips the housing's corners the moment it
   turns, and a turning object whose corners are being cut off by an
   invisible rectangle is the single most obvious way to say "this is a
   WebGL canvas".

   🔴 165% IS DERIVED, NOT PICKED, and it is one number written twice
   because CSS cannot see the camera:

       the front unit's screen projects to 43.6% of the canvas width
       0.72 / 0.436 = 165%   →   the screen lands at 72% of the figure

   The object is deliberately smaller than the figure. It filled it
   exactly while the camera was solved backwards from a frame width;
   72% is a choice, and it is the one number to reach for if the hero
   wants more or less object. Everything else follows it.

   🔴 16:10 because that is the aspect the layout was framed at. At 16:12
   the same composition sits in a letterbox with a fifth of the height
   empty.

   🔴 It fades in OVER the still and the still stays put. No display:none
   on either — the picture is what gives .dv__hero-fig its height, and
   taking it out of the layout would collapse the figure and the canvas,
   which is positioned against it, with it. */
.dv__hero-3d {
  position: absolute; z-index: 2;
  left: 50%; top: 50%;
  width: 165%; aspect-ratio: 16 / 10;
  transform: translate(-50%, -50%);
  opacity: 0;
  transition: opacity .5s var(--ease-out);
  pointer-events: none;
}
.dv__hero-3d.is-ready { opacity: 1; }

/* and the still goes when there is something to replace it with */
.dv__hero-fig:has(.dv__hero-3d.is-ready) .dv__hero-shot {
  opacity: 0;
  transition: opacity .5s var(--ease-out);
}

/* ============================================================
   real media

   Two slots, both optional, both `data-shot` rather than `src` —
   device.js only promotes one to a real request once, and a slot with
   no file behind it costs nothing and shows nothing. Until the
   captures land the drawing underneath is what you see, so the page
   is never half-built while it waits.

   .dv__shot — a capture off the device, dropped into a drawn bezel.
   .dv__unit — a photograph of the whole physical unit, which replaces
               the drawing and its room outright.
   ============================================================ */
.dv__shot {
  position: absolute; inset: 0; z-index: 4;
  display: block;
  width: 100%; height: 100%;
  object-fit: cover;
  border-radius: inherit;
}
.dv__unit {
  position: absolute; inset: 0; z-index: 5;
  display: block;
  width: 100%; height: 100%;
  object-fit: cover;
}

/* ---------- the unit, drawn as an object ----------
   🔴 A placeholder for a PHOTOGRAPH has to read as a thing in a room,
   not as a screenshot with a border. Three cues do almost all of it:
   the case has a lit top edge, it throws a shadow onto the wall
   behind it, and — where it is standing rather than hung — it makes
   contact with a surface. Without the contact shadow the unit floats,
   and a floating device reads as a mockup however good the bezel is.

   All of it is in cqw off the pad, so the same rules hold at any size
   the gallery is drawn. */
/* (the hero's own contact shadow is with the hero, above — it is lit
   differently from the bands and does not share a rule with them.) */

/* (the horizon lived here — .dv__surface, a soft tan wash across the
   bottom third of each band's media half with a hairline along its top.
   It said "this is somewhere" while the device stood in a room on one
   side of a spread. The bands are stacked now and the device HINGES
   OPEN in mid-air, which is not a thing that happens on a floor: a
   surface under it makes the fold read as a device falling over rather
   than opening. The elements went out of the markup with the rule.) */

/* (2 · highlights — the carousel — lived here: the section, the
   scroll-snap track, the six cards, the dot rail and the play/pause
   button. All of it went with the markup. Nothing else on the page uses
   .dv__dots or .dv__pause, so their press state came out of the shared
   :active rule further up as well.) */

/* ============================================================
   3 · the day, four cards, sideways

   🔴 THE CARDS ARE BIG AGAIN AND THEY MOVE ACROSS. Three arrangements
   have been through here and each one was answering the same question —
   how do four features share one section without being four sections:

     a pile   one pinned stage, four cards stacked, the scroll dealing
              them one at a time. Only ever showed one card, and charged
              four screens for the other three.
     a bento  a twelve-column grid, four tiles side by side, nothing
              pinned. Showed all four at once and made each of them
              small.
     this     one pinned stage, four cards in a ROW, and the scroll
              pushes the row sideways.

   The trade this one makes: a card can be as large as the window is
   wide (the height is no longer shared with three other cards), and the
   scroll still only ever costs what a reader spends going through them.
   What it gives up is the comparison the grid had — two cards are on
   screen at a time, not four.

   🔴 Nothing is hijacked. The stage is `position: sticky`, so the page
   scrolls normally the whole way: stop halfway through the rail and the
   row simply stops halfway across. device.js writes ONE transform on
   the track per frame; the cards themselves never move relative to it.
   ============================================================ */
.dv__feat {
  position: relative;
  background: var(--page-bg);
}

/* 🔴 THE RAIL IS THE SCROLL THE DEAL COSTS: one screen per card to turn
   and one at the end so the last is not snatched away as the section
   ends. device.js maps this onto t = 0 → cards − 1, so the number of
   cards and this height have to move together — four cards is 400svh.

   It was 340svh when the same four were a row scrubbed sideways, where
   the mapping was against the row's own width instead. Different
   arithmetic, different number; do not carry one over to the other. */
.dv__feat-rail { position: relative; height: 400svh; }

.dv__feat-stage {
  position: sticky; top: 0;
  height: 100svh;
  display: flex; align-items: center; justify-content: center;
  overflow: hidden;
}

/* 🔴 THE PILE. All four cards are in ONE grid cell, so the stack has a
   real height — the cell is as tall as the tallest card — and the stage
   can centre it without anything being measured in script. The row this
   replaces was `width: max-content` and laid its cards side by side;
   the JS measured that width to know how far to pull. A pile needs no
   measurement at all: every card is a function of one number.

   🔴 The pile's own overhang. The three cards behind the front one are
   TRANSFORMED down, and transforms cost the box no height, so without
   this the cards below would sit on whatever follows the band. */
.dv__pile {
  position: relative;
  display: grid;
  /* 🔴 FULL CONTAINER WIDTH, so the pile's edges are the page's edges.
     It was `min(100%, 62rem)` and 62rem is 992px against a 1096px
     container — 52px of inset on each side that nothing else on the page
     had, so the cards sat narrower than the intel columns directly under
     them and the band read as floating rather than as part of the
     column. Measured: the banner's inner, intel's inner and .dv__wrap
     all run 165..1261 at 1440; the pile now does too.

     🔴 THE SECOND TERM IS A HEIGHT GUARD, NOT A CAP, and it is why the
     62rem could not simply be deleted. The cards are 16/10.8 and fill
     this box, so a full-width pile is 1096 x 740 — which fits a 900px
     window and does not fit a 760px one, and the stage is sticky at
     100svh with `overflow: hidden`, so what does not fit is CUT rather
     than scrolled to. The card's own rule used to carry this guard; it
     lost it when the cards became grid-area and stopped owning a width.

     4.25rem, AND THE WINDOW IS NARROWER THAN IT LOOKS. 6rem was the
     first guess and it bound at a 900px window — 100svh resolved to 813
     there, not 900, so the guard fired 34px early and the pile never
     reached its container. The number is bracketed, not chosen:

       to reach full width   X <= svh − 1096/1.481 = svh − 740
       to avoid being cut    X >= the 4rem overhang

     which at svh 813 leaves 64..73px. 68px sits in the middle of that.
     Checked at three heights: svh 813 → full 1096 and aligned; 713 →
     955, 4px of headroom; 613 → 807. Above the fold it aligns, below it
     shrinks, and it is never clipped. */
  width: min(100%, calc((100svh - 4.25rem) * 16 / 10.8));
  /* 🔴 THE OVERHANG IS SPLIT, AND THE SPLIT IS WHAT CENTRES THE DECK. It
     was 4rem of padding-bottom and nothing on top, which is the honest
     shape of the guard — the cards behind the front one are transformed
     DOWN and transforms cost the box no height, so the room they need is
     room at the FOOT. The trouble is that the stage centres this box, not
     the cards in it, and a box with 64px of empty at one end centres with
     the visible cards riding high. Measured at 1440x900, mid-deal:

       stage        0 .. 900
       cards     48.1 .. 796.0        48 above, 104 below
       off-centre by 28px, and it reads as "the screen sits high"

     Moving 1.75rem of the 4 from the foot to the head makes the box
     symmetric about its own cards without changing what the box COSTS:
     margin-top + padding-bottom is still 4rem, so the width guard above
     — whose 4.25rem is bracketed against exactly that total — is left
     alone. Land: 76 above, 76 below.

     ⚠️ The two values are one number in two places. If the guard's
     4.25rem is ever re-derived, or the deal's transforms change how far
     the back cards hang, re-measure and re-split — do not nudge one of
     these on its own. */
  margin-top: 1.75rem;
  padding-bottom: 2.25rem;
}

/* ---------- the hour being read ----------
   🔴 ONE SLOT, FOUR SENTENCES, all absolute inside it. In flow they
   would each be their own height and the deck below would step up and
   down as the copy changed — the cards are the thing that must not
   move. The slot is as tall as the tallest of them (the relative
   parent is given the height by the one left in flow: see .is-tall). */
/* 🔴 THE COPY IS SPLIT AROUND THE OBJECT NOW: the hour and the headline
   sit ABOVE the screen, the sentence UNDER it. It was one block under
   the device — all three lines below the picture — which put the only
   words that say what you are looking at AFTER the thing they are
   labelling, and left a card that was top-heavy in art and bottom-heavy
   in type.

   Above / below is the shape a captioned figure has always had: the
   title tells you what this is before you look, the sentence tells you
   what it means once you have. It costs the card nothing in height —
   the same three elements are in the same column, in a different order.

   🔴 Two elements, not one, because a flex column cannot put half a
   child above a sibling and half below it. .dv__say--head and
   .dv__say--foot share .dv__say for everything that is common; the
   markup keeps them inside the same card, so the pair still rides the
   deal with no state of its own.

   (The older note here recorded why the block moved UNDER the pile in
   the first place — the leaving card used to fly up THROUGH the copy
   and had to fade to hide the collision. That is still fixed and for
   the same reason: the copy is inside the card, so there is nothing
   for a card to cross on its way out.) */
/* 🔴 IT IS IN THE CARD, so it needs no state of its own. This was
   absolutely positioned in a shared slot at `opacity: 0`, and device.js
   turned exactly one of them on and measured which was longest to hold
   the slot open — machinery that existed only because four sentences had
   to take turns standing in one place. A sentence that rides its own card
   is visible when its card is, which the deal already decides. No .is-on,
   no .is-tall, and 8 lines less JS. */
.dv__say {
  position: relative;
  z-index: 2;
  width: 100%;
  text-align: center;
}
/* 🔴 cqw, NOT vw — the card is the container now (see .dv__card). The
   lede's 40rem max-width went with the move: the card's own padding is
   the measure, and a 40rem column inside a 62rem card would have set the
   longest sentence narrow with air either side of it. */
/* 🔴 THE HOME PAGE'S SECTION WEIGHT, not the base h2's. styles.css sets
   every h2 at 400 / -.03em / .95, and the two openers that actually carry
   a section on the home page — .h2-bigger and .pv__title — override all
   three together at 600 / -.02em / .92 and are kept in step with each
   other by a note in styles.css. These four cards are section openers in
   every sense that matters, and at 400 they read as captions next to
   them: same size, visibly lighter, which is what "not bold enough" is.
   Same three numbers as those two, deliberately — change them there and
   change them here. */
.dv__card .dv__h2 {
  font-size: clamp(1.25rem, 5.2cqw, 3.1rem);
  font-weight: 600;
  letter-spacing: -.02em;
  line-height: .92;
}
/* (the card's sentence lived here — .dv__card .dv__lede, the line under
   the screen, capped to a 70cqw measure and set at clamp(.875rem,
   2.35cqw, 1.25rem)/1.55 in ink at 78%. It is gone from all four cards
   with the eyebrow above it: a card is a headline and a screen now.) */
/* 🔴 The head's own bottom margin is gone with the split — the card's
   `gap` is the space between the three now, and a margin under the
   headline would be that gap stated twice on one side only. Same for the
   lede's old top margin above. */

/* (the fourth card used to flip its type to white — its art ran #171c2c
   to #4b5180 and the ink the other three use would have been dark on
   dark. There is no dark art any more; all four cards are white and all
   four read in ink.) */

/* ---------- a card ----------
   🔴 SIZED OFF THE WINDOW, NOT OFF A SHARE OF A PILE. In the deck the
   card's height was whatever one screen had left after the type, and its
   width followed from that; in the row it is the other way round — the
   card takes a width off the VIEWPORT and its height follows from the
   ratio, because the only thing it now has to fit inside is the stage.

   84vw at the top of the clamp means two cards are never quite both on
   screen: the one being read plus an edge of the next, which is what
   says the row continues. 62rem stops it running away on a wide monitor.
   The height term is the guard for a short window — a card taller than
   the stage would be cropped by the sticky box. */
/* 🔴 GRID-AREA, NOT FLEX. Every card is in the same cell — that IS the
   pile — and device.js writes each one's transform per frame off its
   distance from the front. `flex: none` and a per-card width were the
   row's; in a stack the cards are all the cell's width by definition,
   which is also what keeps them registered on top of each other. */
.dv__card {
  grid-area: 1 / 1;
  position: relative;
  aspect-ratio: 16 / 10.8;
  /* 🔴 CENTRE ORIGIN, and this is the difference between a pile you can
     see and one you cannot. Anchored at the top, a card scaled to .91
     and pushed down 36px lifts its own bottom edge by 9% of its height
     — about the same 36px — and lands exactly behind the card in front:
     four cards, one visible, nothing to say there is more coming. About
     the middle, the same numbers leave ~13px of each card showing under
     the one above it, which is the only reason the stack reads as a
     stack. */
  transition: transform .28s var(--ease-out-quint);
  will-change: transform;
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  gap: clamp(.75rem, 3.4cqw, 2.5rem);
  padding: clamp(1rem, 3.2cqw, 3rem);
  border-radius: clamp(20px, 2.6vw, 48px);
  /* 🔴 THE STRIP'S OWN COLOUR. #E4E9ED was .ph-banner__bar — the empty
     placeholder band these cards were moved into. Keeping it as the
     card surface is what lets the block still read as the band that was
     there, rather than as four white cards that turned up in its place.
     It is also what a stack needs: white cards on a near-white ground
     have nothing but their radius to separate them, and three of the
     four are only ever seen as a sliver under the one in front. */
  background: #E4E9ED;
  container-type: inline-size;
  overflow: hidden;
  isolation: isolate;
  text-align: center;
}

/* 🔴 THE ART IS GONE AND THE CARD IS WHITE. What stood here was the
   full-bleed pair described in the note above — .dv__card-art, four
   blurred gradient "pans" of a room at four hours of the day, and
   .dv__card-shot, the photograph slot that replaced the drawing the
   moment a file landed in assets/device/. Roughly 60 lines of gradient
   stops went with them.

   The argument for them was that a card should be a room the device is
   standing in. The argument against is what four of them look like in a
   pile: four saturated fields behind four screenshots, where the only
   thing that actually differs card to card is the screen and the
   sentence. White gives those two the card, and it is what every other
   surface on this page already is — the size cards, the spec panel, the
   comparison columns.

   Recorded rather than deleted, because the photographs may still be
   shot: the markup note in device.html says exactly which elements come
   back, and the gradients are in git… which this project does not have,
   so here are the four base hues if they are ever wanted again —
   0800 #a9c9d8, 1300 #e2c45f, 1700 #d99a6b, 2100 #3a4066, each a
   repeating-linear-gradient at 96deg under a soft radial highlight,
   blurred 14px and scaled 1.14. */

/* 🔴 THE SCREEN IS THE TILE'S WIDTH NOW. In the pile the pad was a
   SHARE of the card (68%, dropping to 61% on a small one) because the
   card was a room and the device was standing in it, and because the
   card's height was fixed so the words needed a guaranteed share of it.
   Neither is true on a grid: the tile has no room in it and no fixed
   height, so the screen takes the media box whole and the copy under it
   takes what it takes. */
/* 🔴 THE SCREEN TAKES WHAT THE WORDS LEAVE, and it works that out
   itself. The card has a fixed ratio again, so its height is spoken for
   — and at `width: 100%` the pad's height (9/16 of the card's width)
   plus the type came to 787px inside a 670px card: the eyebrow was
   clipped off the top and the last line of the sentence off the bottom.

   In the deck this was solved with a magic share of the card's width
   (68%, 61% on a small one), re-measured by hand every time the type
   moved. This does it by construction: `flex: 1` gives the pad whatever
   height is left after the head and the foot have taken theirs, `width:
   auto` + the 16/9 ratio turns that height into a width, and the card's
   `align-items: center` centres the result. Change the copy, the type
   scale or the gaps and the screen re-fits on its own. */
.dv__card .dv__pad {
  position: relative; z-index: 1;
  flex: 1 1 auto;
  min-height: 0;
  width: auto;
  max-width: 100%;
  height: auto;
  /* 🔴 A CAP ON THE HEIGHT, NOT ON THE WIDTH, and that distinction is the
     whole of this line. `flex: 1` gives the pad the height left over and
     the 16/9 ratio turns that height into a width — so height is the
     input and width is the output. Capping max-width leaves the height
     untouched and squashes the screen: measured, 90% through 70% all
     came out 562px tall, i.e. the same picture at five wrong aspects.
     Capping the height moves both, and the ratio held at 1.78 at every
     step of the same sweep.

     🔴 70%, from that sweep against a 1096 x 740 card:

       uncapped   999 x 562   91% of the card's width
       74%        852 x 479   78%
       70%        806 x 453   74%   <- this
       66%        760 x 427   69%

     At 91% the screen ran nearly edge to edge and the card had stopped
     being a card — it was a bezel around a screenshot. 74% leaves the
     surface visible down both sides, which is what the colour is for. */
  max-height: 70%;
}
/* (the 61% container query that used to sit here went with the share:
   a narrow TILE wants its screen at full width just as much as a wide
   one does — what gives on a small tile is the type, which has its own
   clamps.) */

/* 🔴 ONLY THE FRONT CARD KEEPS ITS FURNITURE. Two of the four devices
   carry things that hang OUTSIDE their own box — the bird straddling
   the lower-right corner and the caption under her — and on a card
   that is 36px lower and 9% smaller than the one in front, that
   furniture is all you see of it: a bird and a speech bubble floating
   under the pile with no device attached. They belong to whichever
   card is being read. device.js marks it. */
.dv__card .dv__nestie,
.dv__card .dv__nestie-say {
  opacity: 0;
  transition: opacity .3s var(--ease-out);
}
.dv__card .dv__nestie,
.dv__card .dv__nestie-say { opacity: 1; }


/* the aura canvas — above the screenshot, below nothing. It is a rim
   light ON the screen, exactly as the device draws it, so it lives
   inside .dv__screen and inherits the corner. */
.dv__aura {
  position: absolute; inset: 0; z-index: 7;
  width: 100%; height: 100%;
  border-radius: inherit;
  pointer-events: none;
}

/* ---------- Nestie ----------
   She sits over the pad's lower-right corner, half on the glass and
   half off it, which is where the device parks her. The bubble is
   behind her and larger than she is, so it reads as the thing she is
   sitting in rather than a halo stuck on. */
.dv__nestie {
  position: absolute; z-index: 8;
  /* straddling the corner: mostly off the glass, enough of her on it
     that she reads as having come out of the screen rather than
     having been dropped next to it */
  right: -5%; bottom: -9%;
  width: clamp(110px, 27%, 200px);
  aspect-ratio: 1;
  pointer-events: none;
}
.dv__nestie-bubble,
.dv__nestie-bird { position: absolute; inset: 0; }
.dv__nestie-bubble { transform: scale(1.45); }   /* the blob overflows her */
.dv__bubble, .dv__bird { width: 100%; height: 100%; display: block; }
.dv__bird { position: relative; z-index: 1; }

/* what she just did with what you said — the caption the device shows
   under her, kept short enough to read at a glance */
/* 🔴 ABOVE HER, NOT UNDER HER. It hung at `top: 100%` — below the bird,
   which is itself below the pad — and that was free space while the card
   was a room with the device floating in it. On a tile the copy starts
   right there, and the caption landed between the eyebrow and the
   headline. Flipped to sit over the glass's lower corner instead, which
   is also the more honest picture: the line is what the device just
   heard, so it belongs on the device. */
.dv__nestie-say {
  position: absolute; bottom: 100%; right: 0;
  margin-bottom: .35rem;
  white-space: nowrap;
  background: var(--nes-white);
  border-radius: 999px;
  padding: .5rem .95rem;
  font-family: var(--font--ui);
  font-size: .8125rem; font-weight: 700;
  color: var(--nes-ink);
  box-shadow: 0 10px 26px -10px color-mix(in srgb, var(--nes-ink) 40%, transparent);
}
.dv__night {
  position: absolute; inset: 0; z-index: 6;
  background: #0b0c0e;
  display: flex; flex-direction: column; justify-content: flex-end;
  padding: 3em 3.4em;
  border-radius: inherit;
}
.dv__night b {
  font-family: var(--font--primary);
  font-size: 6.4em; font-weight: 700;
  letter-spacing: -.03em; line-height: 1;
  color: rgba(255, 255, 255, .6);
  font-variant-numeric: tabular-nums;
}
.dv__night span {
  margin-top: .9em;
  font-family: var(--font--ui);
  font-size: 1.5em; font-weight: 700;
  letter-spacing: .16em; text-transform: uppercase;
  color: rgba(255, 255, 255, .34);
}

/* the real screensaver, once its capture exists — it rides ABOVE the
   drawn clock (z 6) so dropping the file in swaps the screen the same
   way every other capture does, and pulling it back out restores the
   drawing. Shown at full brightness: the screensaver IS the night face
   of the product.

   🔴 `.is-night` is off the fourth card now (the dark art it named went
   with the white ground), so this is keyed to the card's position
   instead. Same one card, same rule, no class to forget to re-add. */
.dv__card[data-card="3"] .dv__shot {
  z-index: 7;
}

/* ============================================================
   5 · privacy
   ============================================================ */
/* 🔴 25px, DOWN FROM 99, AND IT IS NOT THIS SECTION THAT GOT TIGHTER.
   Both of this block's neighbours bring their own air, and neither brings
   it as padding, so the declared number and the seen number were nowhere
   near each other. Measured at 1440x900 with 99 on both ends, from the
   VISIBLE edges — the deck's last card above, the specs heading below:

     deck card edge -> priv card    203    = 104 dead stage + 99
     priv card -> specs heading     207    = 99 + 108 (.dv__specs' own)

   The 104 above is the deck's sticky stage: it is a screen tall with the
   pile centred in it, so the band ends with roughly that much empty stage
   under the last card whatever this section does. Same structural slack
   .intel had to be tuned against when IT sat under the deck, and the same
   answer — subtract it here rather than pretend it is not there.

   .intel's two ends are the page's rhythm now, at 131. A SYMMETRIC value
   cannot hit both of these exactly (one end owes 27, the other 23), but
   25 lands 129 above and 133 below — 4px apart, and it keeps this rule
   the single shorthand it has always been rather than splitting it into
   two compensations that would then have to be maintained apart.

   ⚠️ Re-measure if the deck moves again, or if .dv__specs' padding-top
   changes: this number is the remainder after the neighbours, so it is
   wrong the moment either of them is. */
/* ⚠️ THE TWO ENDS ARE UNEQUAL BECAUSE THE DECK IS HIDDEN. This was one
   symmetric value — `clamp(1rem, 2.8vh, 2.5rem) 0` — and that number was
   never this section's own spacing: it was the REMAINDER left after the
   deck above contributed ~104px of empty sticky stage under its last card.
   The deck is currently wrapped in a template tag in device.html (search
   "THE DECK IS HIDDEN"), so what sits above is the family-fit bento, which
   ends on a hard card edge and brings no slack at all. At 25px the panels
   came within 79px of the cards above, against the ~131 the intel section
   established as this page's rhythm.

   ⚠️ THE NEIGHBOUR ABOVE HAS BEEN TWO DIFFERENT THINGS and this note named
   the wrong one for a while: it was .ph-blocks when the number was derived,
   and .ph-blocks has since been replaced by .ffit--flush. The arithmetic
   survived the swap by luck rather than by design — both bring 54px of
   bottom padding and both end on a card edge — so the 131 still holds. It
   is luck worth knowing about: this padding is a remainder, so it is only
   ever as correct as the neighbour it was subtracted from.

   Measured at 1440x900 with the deck hidden:

                              contributes   total to the eye
     .ffit--flush padding-bottom   54
     .dv__priv padding-top         77              131   <- matches intel
     .dv__priv padding-bottom      25
     .dv__specs padding-top       108              133

   The FOOT was already right and is untouched — .dv__specs brings its own
   108 — which is why only the head moved.

   🔴 RESTORING THE DECK MEANS RESTORING THIS TOO, to
        padding: clamp(1rem, 2.8vh, 2.5rem) 0;
   or the privacy block will sit 104px lower than everything else on the
   page. This is step 2 of the restore note in device.html; the two changes
   are one decision and have to be undone together. */
.dv__priv {
  padding: clamp(3rem, 8.5vh, 6rem) 0 clamp(1rem, 2.8vh, 2.5rem);
  background: var(--page-bg);
}
/* 🔴 THREE PANELS, after apple.com's "Our values lead the way". This was
   ONE wide card — a lock in a left column with two paragraphs beside it —
   and the block it replaces is worth naming, because the row inherits two
   decisions from it rather than re-deciding them: the lock (same classes,
   same keyframes, same trigger) and the row's outer edges.

   🔴 THE ROW KEEPS THE CARD'S ALIGNMENT. The single card had just been
   widened to sit on the deck band's own two edges — .dv__wrap's content
   box is 172..1268 at 1440 and .dv__pile is the same 1096 — and a grid
   across that span holds it, so the three panels still line up with the
   cards above without anything here naming a width. */
/* 🔴 SET TO THE SAME FIVE NUMBERS AS .intel__headline (intel.css), because
   these are the same thing on the page: the sentence that opens a block of
   panels. Both were on .dv__h2, which is --heading--display — a different
   clamp, a different weight and no tracking — and reading from one section
   to the next that difference looked like drift rather than a decision.

   🔴 THE TWO DEVICE HEADINGS SHARE ONE RULE ON PURPOSE. There are three
   places wearing this type now — .intel__headline over in intel.css, and
   these two — and three copies of five numbers is how a "change one,
   change all" note stops being true. Grouping the pair that live in this
   file leaves two copies instead of three; the intel.css one cannot join
   them without one file reaching into the other's block. If a fourth ever
   wants this, that is the point to promote it to a token in styles.css
   rather than paste it again.

   Change one, change all THREE.

     font-size      clamp(1.5rem, 3.4vw, 3rem)
     font-weight    600
     line-height    1.1
     letter-spacing -0.02em
     colour         --nes-ink

   🔴 WHAT IT DOES NOT TAKE IS `white-space: nowrap`, and that is the one
   deliberate departure. Over there the nowrap is load-bearing: intel.css
   derives its whole size ceiling from "the last step that still fits on
   one line", and it carries a 639px override to let go of it on a phone.
   This sentence is 35 characters against that one's 48 — measured 745px at
   the 48px cap, in a 1096px container, so it is nowhere near the edge and
   gains nothing from the rule. It would only inherit the hazard: at the
   clamp's 24px floor the line is 372px, which overflows every phone width
   from 430 down. Copy the type, not the constraint. */
.dv__vals-title,
.dv__specs-title {
  font-family: var(--font--primary);
  font-size: clamp(1.5rem, 3.4vw, 3rem);
  font-weight: 600;
  line-height: 1.1;
  letter-spacing: -0.02em;
  color: var(--nes-ink);
  text-align: center;
  text-wrap: balance;
}
/* 🔴 ALIGNMENT IS SHARED NOW TOO, so the only thing left below is the
   margin. The values title was `text-align: left` — after the reference
   the panel row was built to, whose own values block hangs its title on
   the left edge its cards start from — and the specs title below it has
   always been centred over a centred table. Two centred headings and one
   left one down the same page read as one of them being misaligned rather
   than as two kinds of section, which is the argument the old note here
   made in reverse and lost.

   The margins stay apart: the values title opens a row it has to clear,
   the specs title is followed by its own sub-line which brings its own
   top margin. */
.dv__vals-title  { margin: 0 0 clamp(2rem, 4.5vh, 3.5rem); }
.dv__specs-title { margin: 0; }

/* 🔴 THE EMPHASIS RUN, matching .intel__hl — 700 against the line's 600,
   and no colour. That pairing is the other half of the format being
   followed here: over there it is "family intelligence" and "Nestify",
   here it is "not a data center." and "the last millimeter." — in each
   case the half of the sentence carrying the claim, which is why the run
   is a PHRASE and not the last word: "Down to" is the lead-in, the
   measurement is the claim. On the values title it costs 6px of width
   (738 → 745 at the cap) against 351px of spare. */
.dv__vals-title em,
.dv__specs-title em { font-weight: 700; }
.dv__vals {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  /* the card blocks on this page sit 1.25rem apart; this is the same
     family of gap, one step up because these panels carry text */
  gap: clamp(1rem, 1.6vw, 1.5rem);
  align-items: stretch;
}

/* 🔴 A COLUMN, AND THE LINK IS PUSHED TO THE FLOOR. The three bodies are
   different lengths — 25 words, 12, 19 — so without this the "Learn more"
   lines would sit at three different heights and the row would read as
   three unrelated cards. `margin-top: auto` on the link (below) is what
   levels them; it needs a flex column and `align-items: stretch` on the
   grid to have a floor to sit on. */
.dv__val {
  display: flex;
  flex-direction: column;
  background: var(--nes-white);
  color: color-mix(in srgb, var(--nes-ink) 80%, transparent);
  /* 🔴 --regular, and the token itself says why: styles.css declares
     --section-radius--regular as 2rem and notes beside it that 3rem read
     too soft for a card corner. That decision was made once for the whole
     site. --large is for SECTIONS, not for things sitting on them.

     🔴 DO NOT PASTE THAT NOTE IN HERE. It was quoted verbatim once,
     brackets and all, and CSS comments do not nest — the quoted comment's
     own terminator closed this one early, the rest of the sentence was
     parsed as a declaration, and the parser's recovery swallowed the
     border-radius line underneath it. The corners went square and the
     cause was four lines of prose. */
  border-radius: var(--section-radius--regular);
  padding: clamp(1.5rem, 2.4vw, 2.25rem);
}

/* 🔴 A FLAT SIZE, not a share of the column. The mark is an icon, not a
   picture, and an icon that resizes with its gutter reads as a picture
   that has been scaled. 2.75rem here against the 4.5rem the lock had as a
   single card's left column — it is one of three now, sitting over type
   rather than beside it. */
.dv__val-mark { margin-bottom: clamp(1.25rem, 2.2vh, 1.75rem); }
.dv__val-mark .pv__lock,
.dv__val-ic { --lock-size: 2.75rem; width: 2.75rem; height: 2.75rem; display: block; color: var(--nes-ink); }
.dv__val-ic svg { display: block; width: 100%; height: 100%; overflow: visible; }

/* 🔴 THREE BOXES, THREE SIZES, AND THAT IS THE FIX RATHER THAN THE BUG.
   All three were 2.75rem and the lock read as the runt. It is not — the
   BOXES were identical and the ARTWORK inside them is not. Measured with
   getBBox at the rendered 44px box, stroke included:

     mark      ink in viewBox     renders as      % of box
     lock      17.2 x 19.4        31.5 x 35.7      72 x 81
     camera    21.7 x 21.7        39.8 x 39.8      90 x 90
     people    20.4 x 17.6        37.4 x 32.2      85 x 73

   8px of difference across the row, which is what "the lock is smaller"
   is. The three shapes also have three aspect ratios — the lock is
   portrait, the camera square, the people landscape — so no single scale
   makes all six numbers agree. Matching WIDTH alone would put the lock at
   1.224x and leave it towering over the other two; the geometric mean of
   the width and height corrections is 1.111, which is where 3.05 comes
   from. The camera comes DOWN because at 90% it was the one making the
   lock look small, not the other way round.

   Lands: 35.0 / 37.6 / 37.4 wide — a 2.6px spread, down from 8.3.

   ⚠️ The lock's ARTWORK is not ours to retune — it is the home page's,
   byte for byte, and its 72% is a property of that drawing. If any of
   these three SVGs is redrawn, re-run getBBox and re-derive; do not nudge
   a rem here by eye, that is what produced the row this replaced. */
.dv__val-mark .pv__lock { --lock-size: 3.05rem; width: 3.05rem; height: 3.05rem; }
.dv__val:nth-child(2) .dv__val-ic { width: 2.6rem; height: 2.6rem; }

.dv__val h3 {
  font-family: var(--font--primary);
  font-size: clamp(1.125rem, 1.5vw, 1.375rem);
  font-weight: 600;
  letter-spacing: -.015em;
  line-height: 1.2;
  color: var(--nes-ink);
  margin: 0 0 .6rem;
  text-wrap: balance;
}

/* 🔴 16px, DOWN TWO FROM 18, AND THE CLAMP MOVED AS A SHAPE. This started
   as the single privacy card's type — 18px, itself argued down from 21 on
   the grounds that 21 was display size on a paragraph. Asked down two more
   for the panels, every term shifted by the same 2px rather than the cap
   alone:

     was   clamp(1rem,      1.35vw, 1.125rem)     16 .. 18
     now   clamp(0.875rem,  1.2vw,  1rem)         14 .. 16

   The vw term is re-derived, not left alone: at 1.35vw the old cap bound
   from 1333px up, and 1.2vw puts the new cap in the same place, so the
   breakpoint where the type stops growing has not silently moved. Dropping
   the cap on its own would have left the middle of the range unchanged and
   the clamp doing nothing above 1185px.

   ⚠️ The old floor note said 1rem was chosen because 17px on a phone is
   larger than the page's own body copy. 14px is now BELOW it — that is the
   trade for two sizes down, and it is the number to look at first if the
   panels ever read small on a handset. */
.dv__val p {
  font-family: var(--font--primary);
  font-size: clamp(0.875rem, 1.2vw, 1rem);
  line-height: 1.7;
  /* 🔴 THE ONLY BODY COPY ON THE PAGE THAT IS TRACKED, and it is because
     of the company it keeps rather than because 0 was wrong. The h3 above
     it sits at -.015em and the title above that at -.02em, so untracked
     copy in the same panel read looser than everything around it — the
     drift is visible at this size inside a 350px column in a way it is not
     in a full-width paragraph.

     -.008em, not the heading's -.015: this is running text and the two
     jobs are different. Tightening display type buys back width the eye
     never reads as missing; tightening body type past about -.01em starts
     closing counters and costs legibility, which is the opposite of what
     a paragraph is for.

     Measured after: 5 / 2 / 3 lines — the middle panel's copy pulled up a
     line, which it can afford. The panels are `align-items: stretch` in
     the grid and the link is pushed down by `margin-top: auto`, so all
     three stayed level at 397px whatever the line counts do. That is the
     property to check after any change here, not the counts themselves. */
  letter-spacing: -.008em;
  text-wrap: pretty;
  margin: 0;
}
.dv__val b { color: var(--nes-ink); font-weight: 600; }

/* 🔴 margin-top: auto — see the note on .dv__val. This is the line that
   levels the three cards' feet. */
.dv__val-link {
  margin-top: auto;
  padding-top: clamp(1.25rem, 2.4vh, 1.75rem);
  display: inline-flex;
  align-items: center;
  gap: .3em;
  align-self: flex-start;
  font-family: var(--font--primary);
  font-size: clamp(1rem, 1.35vw, 1.125rem);
  font-weight: 500;
  /* the same blue the intel headline was briefly set in, and the one
     styles.css labels "brand blue (headline)" — as ink on the card's white
     it measures 3.68:1, over the 3:1 large type gets and under the 4.5
     body copy needs.

     🔴 NO UNDERLINE, AND THE CHEVRON IS WHY THAT IS ALLOWED. WCAG 1.4.1 is
     about colour being the ONLY thing that marks a link, which is a real
     hazard for a link buried in a sentence — someone who cannot separate
     the blue from the black has nothing else to go on. Neither applies
     here: this link is on its own line, at the foot of a panel, with a
     chevron after it that no run of body copy carries, and the chevron
     moves on hover. Those are non-colour affordances at rest and on
     interaction both.

     ⚠️ That reasoning is about THIS placement. Put one of these inside a
     paragraph and the underline has to come back. */
  color: var(--nes-blue-2, #1e88e5);
  text-decoration: none;
}
/* the chevron travels, the label does not — the same "goes somewhere
   else" cue the site's arrow buttons use, at link scale */
.dv__val-chev { transition: transform .28s var(--ease-out-quint); }
.dv__val-link:hover .dv__val-chev { transform: translateX(.2em); }

/* 🔴 THE TRIGGER, and it is the only line about the lock this file owns.
   The panel carries [data-reveal], so footer.js puts .is-in on it when it
   arrives — the same arrival the copy fades in on, which is what makes the
   latch read as part of the block landing rather than as a loop running
   underneath it. It was .dv__priv-card before the row existed; the class
   moved, the reasoning did not.

   🔴 The rest state is scoped to the SAME selector's absence, not left on
   .pv__lock-bar: if this stylesheet loads and the reveal never fires — no
   IntersectionObserver, a JS error, print — the lock has to be found
   CLOSED. An open padlock is the wrong picture for a panel promising the
   data stays home, and "the animation did not run" must never be the thing
   that says so. */
.dv__val.is-in .pv__lock-bar {
  animation: pv-latch 1.15s var(--ease-out) .15s backwards;
}

/* 🔴 THE OTHER TWO MARKS GET THE SAME BEAT, or the row animates in thirds.
   The lock's shackle swings; these draw. Same principle as above for the
   rest state: the dash offset sits at 0 — FULLY DRAWN — and the keyframe
   runs `backwards` from the hidden end, so a panel that never receives
   .is-in shows a finished camera-with-a-bar and a finished pair of
   figures rather than a half-drawn one.

   ⚠️ 40 is a length, not a ratio: it has to be at least as long as the
   longest path here (the slash, ~24 units). Redraw either mark longer and
   this stops covering it. */
.dv__val-slash, .dv__val-arc { stroke-dasharray: 40; stroke-dashoffset: 0; }
.dv__val.is-in .dv__val-slash,
.dv__val.is-in .dv__val-arc {
  animation: dv-draw .75s var(--ease-out) .25s backwards;
}
@keyframes dv-draw {
  from { stroke-dashoffset: 40; }
  to   { stroke-dashoffset: 0; }
}

/* ============================================================
   7 · specs
   ============================================================ */
.dv__specs { padding: clamp(5rem, 12vh, 9rem) 0; }
.dv__specs-head { text-align: center; margin-bottom: clamp(3rem, 7vh, 5rem); }
/* (the specs sub-line lived here — "Engineered to earn its place on your
   wall.", the grey line under the section title. Its rule carried the
   page's last note about NOT setting section sub-lines in italic, which
   mattered when there were two of them: this one and the sizes table's.
   Both are gone now, so the note has no subject left on this page.

   The half of that policy still in force is the <em> half, and it is not
   here — styles.css sets `em { font-style: normal }` globally, with the
   reasoning beside it. Look there, not for this.) */
.dv__spec-table { max-width: 48rem; margin: 0 auto; }
.dv__spec-row {
  display: grid; grid-template-columns: 11rem 1fr; gap: 2rem;
  padding: 1.3rem 0;
  border-bottom: 1px solid var(--nes-tan);
  font-size: .9375rem;
}
.dv__spec-row:first-child { border-top: 1px solid var(--nes-tan); }
.dv__spec-row dt { color: var(--nes-muted); }
.dv__spec-row dd { margin: 0; line-height: 1.65; }
.dv__spec-row dd small { display: block; color: var(--nes-muted); font-size: .8125rem; }

/* (the modal sheet, its blurred backdrop, the close button and the
   item list lived here. The pills were the only way in, so they
   went with them.) */

/* ============================================================
   THE WAITLIST CTA — the site's button, on this page

   🔴 Not a device-page button. .btn-ball + .btn-disc in styles.css is
   the same object as the home page's "Meet Nestify": a blue disc that
   opens out into a pill, a filled circle on hover, the same arrow that
   steps out on the way. Someone arriving from the home page has
   already been taught what that shape does, and this page's ask is the
   one place that is worth spending on.

   Two other shapes were built here first and both are gone: a mail
   field with its own submit, and that field fixed to the foot of the
   window for the whole page.

   🔴 It opens onto ITS OWN BLUE and does not change colour at all —
   only the shape does. .btn-ball opens onto --nes-cream because on the
   home page it lands over film; this page's ground is --page-bg,
   rgb(244,242,234), four points off cream, so that pill would be a
   cream shape on a cream field. Holding --bead-blue instead fixes that
   and buys something better: the disc you see first and the pill it
   becomes are one object of one colour, so the opening reads as a
   shape growing rather than as a colour swap wearing a shape change.

   🔴 Resting INK, blue under the pointer — asked for directly, and it
   is the third time this pair has been swapped, so here is the whole
   argument in one place. For blue-at-rest: blue is what this site uses
   for the thing you can press. For ink-at-rest, which is what it does
   now: the pill is the only control on the screen, nothing else on the
   page competes with it for "pressable", and black against a warm
   ground is the heavier object of the two — so the blue is spent on
   the answer to the pointer rather than on the invitation. Both work;
   what must not happen is the two states landing on the same weight.

   🔴 A WHITE LABEL PINS BOTH ENDS, and that is arithmetic, not taste.
   The label is white and never changes, so every colour this button
   can be has to carry white type: ink does at 17.4:1, and the blue the
   disc brings in has to be --nes-blue-3 (4.85:1) rather than
   --bead-blue, which measures 2.71:1 under white — below AA's 4.5 and
   below even the 3:1 large type is allowed, on the one control this
   whole page exists to get pressed. See the note on that token in
   styles.css.

   The cost is that this pill is a shade deeper than the home page's
   "Meet Nestify", which rests on --bead-blue with an ink label. They are
   the same shape and no longer quite the same blue. If they have to
   match exactly, the choice is between white here and light blue
   there — one of the two has to give, and it cannot be the contrast.

   🔴 The hover does not change the label. It is white at both ends —
   on ink at rest and on --nes-blue-3 once the disc has landed — so the
   only thing that moves is the fill.

   🔴 The cue is .is-in, which footer.js already writes onto every
   [data-reveal] on the page. No new observer, and no new class name
   for the same idea — but the wrapper's OWN fade is turned off, or the
   button opens out of its disc while the whole block is still sliding
   up 1.2em underneath it. One entrance, not two.

   🔴 It sits WELL clear of the object. The canvas above it is half
   again as tall as the figure it covers, because the cast shadow needs
   a third of the frame to fade out over — so the figure's own bottom
   margin puts the pill inside the shadow rather than under it. This
   margin is what carries it past the fade and gives the ask its own
   air. It is on the ACT block rather than the figure so that only the
   pill moves; the object stays where the drawn pad is. */
/* 🔴 ANCHORED TO THE STAGE, NOT IN THE COLUMN. Out of flow means out of
   the height budget on .dv__hero-fig — which is where the object's size
   came back from. The stage is sticky, so it is the containing block,
   and it is also the one box that does not move for the whole rail: the
   ask sits at the foot of the WINDOW while the object turns above it.

   🔴 z-index 4, over everything. The figure is 1 and the canvas inside
   it is 2, and this is the one thing on the screen that has to be
   clickable — the type went behind the object on purpose, the button
   cannot. (The canvas is pointer-events: none, so the object never eats
   the click either.) */
/* 🔴 THERE IS NO FOG ANY MORE, and the reason it was here is worth one
   line so it is not re-invented: with a pinned 100svh stage the object
   ran past the foot of the window, the lockup was drawn over the
   calendar, and a band of ground fading up was what made the words
   readable. The section grows now, so nothing is on top of anything and
   there is nothing to hide. A gradient whose job is to cover a layout
   problem is a layout problem. */

/* ---------- the ask, back in the flow ----------
   Under the object, in the clear, one small scroll below the fold — the
   reader sees the whole product first and finds the ask by moving, which
   is the same beat the reference gets by keeping its lockup hidden until
   the hero has been scrolled into. It costs no JavaScript to do it this
   way, which is the better version of the same idea.

   🔴 z-index 4, over the figure (1) and its canvas (2). The canvas is
   165% of the figure and centred on it, so its empty overshoot hangs
   well below the object you can see — without this the pill sits inside
   that box and is unclickable. (The canvas is pointer-events: none as
   well; both, because either one alone has failed before.) */
.dv__hero-act {
  position: relative; z-index: 4;
  margin-top: clamp(1.5rem, 3.5vh, 3rem);
  display: flex; flex-direction: column; align-items: center;
  /* 🔴 .75rem, and the gap is now doing real work: it is the space
     BETWEEN the three parts of the lockup (line, pill, print), not the
     air under an object. The air under the object is the figure's own
     bottom margin and the margin below. */
  gap: .75rem;
}

/* ---------- the blue line over the pill ----------
   The reference's .welcome__lockup-intelligence: 40px / 1.1 / 600 at
   desktop, 24px small, and it wears the SAME .text-gradient as the big
   word at the top of the screen. That repeat is the point — the claim
   at the foot of the hero and the word at its head are one colour, and
   the object sits between them.

   Ours is smaller than 40px because our screen has a name AND a 160px
   word above the object where theirs has only the word; see the height
   budget on .dv__hero-fig for where every rem in this section goes. */
.dv__hero-lede {
  margin: 0;
  font-family: var(--font--primary);
  font-size: clamp(1.25rem, 2.2vw, 1.9rem);
  font-weight: 600;
  line-height: 1.1;
  letter-spacing: -.01em;
}
.dv__hero-lede-ink {
  /* 🔴 BACK TO THE ORIGINAL TWO-STOP BLUE, and three replacements were
     tried on the way here. Keeping the record because each one was
     measured, and the measurements are reusable even though the colours
     were not:

       site tokens   --nes-blue-3 → --nes-blue-2. Correct by the palette
                     and flatter than this: the two are close in
                     lightness, so the ramp had almost nowhere to travel.
       a rainbow     blue → gold has to cross the wheel, and sampling
                     every 2% showed exactly where "dirty" comes from —
                     the teal route bottoms out at 21% saturation on an
                     olive #5d8b5b, a single violet straight into gold is
                     worse at 18%, violet + magenta held 46%. If a
                     multi-hue ramp is ever wanted, that is the method:
                     watch the SATURATION floor, not the colours.
       glass         thickness rather than transparency, because on this
                     ground neither of the obvious moves exists — a
                     translucent blue drops under 3:1 at alpha 0.7, and a
                     white specular measures 1.07:1 against a near-white
                     page. It was legible and it was not better.

     #08528f → #3286cf, and getting here took two corrections in opposite
     directions, which between them say something the individual colours
     do not:

       #004C94 → #297BC4   the original. Too heavy — 7.85 falling to 4.09
                           is dark type falling to fairly dark type, and
                           at that depth blue stops being blue and reads
                           as ink.
       #12639f → #3286cf   both stops lifted. Too pale — and the reason
                           is not that they were too light individually,
                           it is that the FALL went with them, 1.92x down
                           to 1.64x. A ramp with nowhere to travel reads
                           as a flat colour that someone has smudged.

     So the answer was never a lightness. It was the RANGE. The head goes
     back down to 7.39 and the foot stays where the lift put it at 3.55,
     which is a 2.08x fall — wider than the original ever was. Deep top,
     light bottom, more distance between them than either previous
     attempt. That is what makes it read as a gradient rather than as a
     shade of blue.

     🔴 THE FOOT IS THE CONSTRAINT, not the head. Display type needs 3:1
     and the bottom stop is the lightest thing here, so it is the only one
     that can actually fail. 3.55 leaves 0.55 of margin; #3d92da, one step
     lighter, measures 3.06, and a stop that clears the line by 0.06 is not
     a margin, it is a coincidence. If this ever wants more range again,
     take it out of the HEAD — there is room down to about 8.0 before it is
     ink — and leave the foot alone.

     Neither stop is a palette token, which is the one thing about this
     pair worth flagging. But the palette's blues sit within a stop or two
     of each other in lightness, so a token pair has no range to give,
     which is exactly why that version was tried and abandoned above. */
  background-image: linear-gradient(180deg, #0c66a2 45%, #108ad5 90%);
  -webkit-background-clip: text;
          background-clip: text;
  -webkit-text-fill-color: transparent;
          color: transparent;
  padding-inline: 4px; margin-inline: -4px;
}
/* ---------- the NAME inside the line ----------
   🔴 The one word in this sentence that is a product wears the face the
   product's own name wears at the top of the screen — Baloo Chettan 2,
   the logotype's family, and the site's only other use of it. Everything
   around it stays in the display face, so the word reads as a name and
   not as a change of voice.

   🔴 <b>, not <span>. The browser's default bold is turned off again on
   the next line — the weight is the parent's — but the element still
   says "this run of text is different from what is around it", which is
   exactly what is true here and is what a screen reader should hear.

   🔴 The gradient is NOT re-declared. background-clip: text on the
   parent clips the parent's background to ALL the text inside it,
   descendants included, and -webkit-text-fill-color: transparent is
   inherited — so this word is painted by the same ramp as the rest of
   the line and stays in step with it. Give it a background of its own
   and it would clip its own box instead, which is a different slice of
   a different gradient in the middle of one sentence.

   🔴 .96em because Baloo runs large. Its cap height is a bigger share of
   the em than Google Sans's, so at the same font-size the name looked a
   step bolder and taller than the words either side of it; this brings
   the caps back onto the line's own height. */
.dv__hero-lede-name {
  font-family: "Baloo Chettan 2", var(--font--primary);
  font-weight: inherit;
  font-size: .96em;
  letter-spacing: -.005em;
  /* 🔴 IT STEPS OUT OF THE GRADIENT. The line around it is transparent
     type over a clipped ramp; this word paints its own ink instead, so
     both properties have to be said — `color` for the paint and
     -webkit-text-fill-color to override the transparent one it inherits
     from the parent. The ramp still runs under it and simply does not
     show through. */
  color: var(--nes-ink);
  -webkit-text-fill-color: var(--nes-ink);
}

/* ---------- the dot on the i, in the logo's blue ----------
   🔴 THE TITTLE IS PART OF THE GLYPH, so it cannot be recoloured — it is
   covered. A blue disc is drawn over it, which is exactly what the
   wordmark itself does (assets/nestify-logo.svg draws the i's dot twice:
   once in the letterform, once as a blue circle over it — see the note
   in index.html's hub headline).

   🔴 The letter is wrapped so the disc has something to measure against.
   `left: 50%` is the middle of the i's own advance width, which is where
   the tittle sits in this face, and the top and the size are in em so
   the whole thing rides the line's clamp() rather than needing a value
   per breakpoint.

   <i> is doing no italic work here — it is a hook, and the rule below
   turns the browser's default slant off. If it ever needs to be
   semantic, it is not: the letter is a letter. */
.dv__hero-lede-i {
  position: relative;
  font-style: normal;
}
/* 🔴 TWO DISCS, exactly as the wordmark is drawn. A blue dot that is
   SMALLER than the glyph's own tittle cannot hide it — it sits in the
   middle of it and the black shows as a cap above and a sliver below
   (checked at 8× on a screenshot). So the tittle is covered first, in
   the page's own ground, and the bead is drawn on top of the cover at
   whatever size looks right.

   This is the same two-circle trick as assets/nestify-logo.svg, which
   draws the i's dot once in the letterform and once as a blue circle
   with a --page-bg circle between them; the note in index.html's hub
   headline is the long version.

   🔴 The cover has to BE the ground, not something close to it. Here it
   is: the hero's wash has landed on --page-bg well above this line (it
   is a 650px band and this sits under it), so the token is the exact
   colour behind the letter. Move this line up into the tint and the
   cover would need the ramp's value at that height instead, which is
   not a thing CSS can ask for — the answer then is a dotless glyph, not
   a bigger cover. */
.dv__hero-lede-i::before,
.dv__hero-lede-i::after {
  content: '';
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  border-radius: 50%;
  pointer-events: none;
}
/* the cover: a shade wider than the tittle, in the ground */
.dv__hero-lede-i::before {
  top: .33em;
  width: .26em; height: .26em;
  background: var(--page-bg);
}
/* the bead: the logo's own blue, and now free to be any size */
.dv__hero-lede-i::after {
  top: .38em;
  width: .16em; height: .16em;
  background: var(--bead-blue);
}

@supports not ((background-clip: text) or (-webkit-background-clip: text)) {
  .dv__hero-lede-ink {
    background-image: none;
    color: #14649c;
    -webkit-text-fill-color: currentColor;
  }
}

/* ---------- the small print ----------
   Their price line is 17px, weight 400, #6e6e73 — grey, small, and the
   only place a number is allowed to appear on the first screen.

   🔴 The grey is MIXED FROM OUR OWN INK, not lifted. #6e6e73 is a
   neutral that belongs to a page whose type is #1d1d1f; ours is #1a1a1a
   on a faintly cyan ground, and a borrowed grey next to a borrowed blue
   is the point where a page stops being adapted and starts being
   copied. 58% of the ink over the ground lands in the same place
   tonally and is ours. */
.dv__hero-fine {
  margin: .25rem 0 0;
  font-family: var(--font--body);
  font-size: clamp(.8125rem, 1.1vw, 1.0625rem);
  font-weight: 400;
  line-height: 1.3;
  color: color-mix(in srgb, var(--nes-ink) 58%, var(--page-bg));
}

/* ---------- the foot rises, all three of it ----------
   🔴 The wrapper's own fade is off (see .dv__hero-act[data-reveal] below)
   — that is what stopped the pill arriving twice, once sliding and once
   opening — so the two lines have to carry their own entrance or they
   simply appear while the button grows between them.

   Their curve, not the site's: cubic-bezier(0,.35,.23,1) over 2.5s is
   the reference's own rise for this lockup, and it is the slowest thing
   on the screen on purpose — the foot settles last, after the word has
   taken its colour. The travel is 1.2em rather than their 125px; ours
   starts on screen and theirs starts under a video. */
/* ---------- the waitlist number, as wheels ----------
   🔴 THE BOX NEVER MOVES, THE CONTENTS DO. Each column is 1em tall with
   `overflow: hidden` and holds eleven cells of exactly 1em, so the strip
   is 1100% of its own window and count.js scrolls it by a percentage. Any
   height mismatch between the column and its cells shows up as digits
   sitting a pixel or two off their neighbours, which is the one thing an
   odometer may not do — so both are 1em and the line-height is pinned to
   1 on both, away from the paragraph's 1.3.

   🔴 BASELINE COMES FROM THE FIRST CELL. An inline-flex box takes its
   baseline from its first item, so the wheels sit on the paragraph's own
   baseline while their contents scroll past it. That is why this is
   inline-flex and not inline-block with a hand-tuned vertical-align.

   🔴 TABULAR FIGURES, or the columns are different widths and the number
   twitches sideways as it counts. Google Sans has them. */
.dv__count { display: inline-flex; font-variant-numeric: tabular-nums; }
.dv__count-wheels { display: inline-flex; }
.dv__odo {
  display: block;
  height: 1em; overflow: hidden;
  line-height: 1;
}
.dv__odo-strip { display: block; will-change: transform; }
.dv__odo-strip i {
  display: block;
  height: 1em; line-height: 1;
  font-style: normal;
}

.dv__hero-lede,
.dv__hero-fine {
  opacity: 0;
  transform: translate3d(0, 1.2em, 0);
  transition:
    opacity 1.2s ease 3.7s,
    transform 1.6s cubic-bezier(0, .35, .23, 1) 3.7s;
}
/* 🔴 THE CUE IS THE PAGE'S OWN OBSERVER AGAIN — .is-in, from footer.js,
   the same class every other reveal on the site waits for. It was
   briefly .is-asked, written by device.js off the pinned rail's
   progress, because with a 100svh hero the lockup was ON the first
   screen and had to be held back by a script to keep it out of the
   object's way. The section is taller than a screen now: the ask is
   below the fold on its own, so the observer fires when the reader
   actually scrolls to it and there is nothing for JavaScript to decide.

   One class, one cue, and the same one as everywhere else on the page. */
.dv__hero-act.is-in .dv__hero-lede,
.dv__hero-act.is-in .dv__hero-fine { opacity: 1; transform: none; }
.dv__hero-act[data-reveal="fade"] { opacity: 1; transform: none; }

/* the growing disc is the BLUE one here; the label is white at both ends.

   🔴 The CLOSED disc takes the new blue too, not just the opened pill.
   .btn-ball rests on --bead-blue and .is-in was the only rule overriding
   it, so setting the pill alone would have left the little disc opening
   out of one colour and landing on another — and .btn-ball animates
   background-color, so it would have been a visible colour crawl across
   the entrance. Both rules below say ink, for that reason. */
/* 🔴 THE PILL IS BLUE NOW, and it is the reference's blue: rgb(0,113,227)
   — `--sk-button-background` in their button tokens, the same value on
   every Buy button they ship. Measured against white it is 4.70:1, which
   clears AA's 4.5 for the white label it carries. (Our own --nes-blue-3
   exists for exactly this reason and measures 4.85; it is the nearest
   in-house blue if this page ever has to come home.)

   🔴 THE TWO STATES ARE SWAPPED, the same way the home page's hub CTA
   swaps them: it RESTS blue and fills INK on hover. That is forced, not
   chosen — .btn-disc's hover fill has to be a colour the pill is not,
   and filling blue over blue is no feedback at all. The label is white
   at both ends so nothing about it changes as the disc sweeps under it.

   🔴 BOTH RULES SAY THE SAME BLUE, and that is the note this block
   replaces: .btn-ball's closed disc and its opened pill are the same
   element mid-transition, and .btn-ball animates background-color — so
   two different values here is a visible colour crawl across the
   entrance, a disc opening out of one colour and landing on another. */
/* 🔴 THE LABEL WENT BACK TO INK, and it is not a preference — it is what
   the blue costs. White on --bead-blue measures 2.71:1, under AA's 4.5
   and under even the 3:1 large type is allowed, on the one control this
   page exists to get pressed. Ink on the same blue is 6.43:1, and it is
   what the home page's pill does with this exact colour.

   The white label came in when the pill was a deeper blue, and it is why
   --nes-blue-3 was added to the palette. That token stays — a filled
   button that HAS to carry white still needs it — but this pill does not
   have to, and matching the home page means matching both halves. Take
   one without the other and the button is either the wrong blue or
   unreadable. */
/* 🔴 WHITE, AND UNCONDITIONALLY — not `#fff` on :hover. This was the only
   pill on the site whose label was not white, and it was invisible for the
   whole of its hover: --btn-disc fills the pill with --nes-ink and the text
   was --nes-ink too. Ink on ink measures 1.00:1. Not low contrast — none.

   🔴 IT IS DECLARED ONCE, NOT SWITCHED ON HOVER, which is what the home
   page's .hub__btn already does and what the other two do the long way
   round (.intro__cta:hover and .pv__more:hover each re-declare #fff). A
   colour that is the same in both states should not be written as a state
   change: the hover version transitions white → white on every pointer
   enter, and it silently depends on the resting colour being legible too,
   which here it was not going to be.

   🔴 IT CLEARS AT BOTH ENDS NOW, and the history is worth two lines
   because it is why --cta-blue exists at all. Making the label white cost
   contrast at REST while this button was pointed at --bead-blue: white on
   #14a4fd is 2.71:1, under the 3:1 display-size text needs, against the
   6.43 the ink label it replaced had. That was a real trade — an
   unreadable hover (ink on ink, 1.00:1) for a marginal rest.

   It stopped being a trade when the blue moved. --dv-cta-blue now points
   at --cta-blue (#1A80EA), which takes the same white to 3.95:1, and the
   hover is 17.40. Nothing here is under the line any more.

   --nes-blue-3 (#1573c9, 4.85:1) is the next step down if a label ever has
   to clear 4.5 rather than 3. */
.dv__cta {
  /* 🔴 BLUE AT REST, INK ON HOVER — the same pair the home page's two CTAs
     wear (.hub__btn and .pv__more), so every ask on the site answers the
     same way. This was briefly inverted and put back; what the round trip
     bought is that the blue is now one token in styles.css instead of
     three declarations in three files, and that it is --cta-blue rather
     than --bead-blue, which the white label could not stand on. */
  --btn-disc: var(--nes-ink);
  background: var(--dv-cta-blue);
  color: #fff;
}
.dv__hero-act.is-in .dv__cta {
  opacity: 1;
  scale: 1;
  /* 🔴 The open state repeats the resting background on purpose. This
     button arrives by animation (dv-cta-in, `both` fill), and without it
     the pill holds .btn-ball's own closed-disc colour after it lands —
     which is why inverting this pair looked like nothing had changed
     until this line changed too. */
  background: var(--dv-cta-blue);
  clip-path: inset(0 0 0 0 round 100px);
  /* 🔴 AN ANIMATION, NOT A DELAYED TRANSITION, and the reason is the
     hover. This is the last of the four beats, so the pill has to sit
     closed until 3.7s — but .btn-ball's transition is also what runs its
     hover (background-color, colour, the disc), and a transition-delay
     big enough to hold the entrance would put a four-second lag on every
     hover for the rest of the page's life. An animation has its own
     clock: `both` holds the closed frame until the delay is up, plays
     the opening, and leaves the button in exactly the state the
     declarations above already give it. The transitions underneath are
     never touched. */
  animation: dv-cta-in .8s var(--ease-out-expo) 3.7s both;
}
@keyframes dv-cta-in {
  from {
    opacity: 0;
    scale: .55;
    clip-path: inset(0 calc(100% - var(--cta-ball)) 0 0 round 100px);
  }
  to { opacity: 1; scale: 1; clip-path: inset(0 0 0 0 round 100px); }
}
/* reduced motion, and the phone/no-script blocks below, all want the
   pill simply open — an animation with a delay is still four seconds of
   nothing there */
@media (prefers-reduced-motion: reduce) {
  .dv__hero-act.is-in .dv__cta { animation: none; }
  /* the name's three-state entrance is the same kind of thing: without
     this it still plays, and a heading that fades itself out for a second
     is the exact opposite of what the setting asks for. .is-in already
     carries the finished state (opacity 1, transform none), so removing
     the animation leaves the screen assembled. */
  .dv__hero-name.is-in { animation: none; }
}

/* ============================================================
   responsive — the site's own breakpoints (styles.css)
   ============================================================ */
@media screen and (max-width: 991px) {
  /* 🔴 TWO ACROSS, NOT THREE, AND NOT ONE. Three panels of running text in
     a 991px window leaves each about 300px, which is where the headings
     start breaking to three lines. Two-up keeps the measure and lets the
     third panel take the full row under them — it is the "kids" one, and
     being last it is the one that can carry a wider line without the row
     looking ragged. */
  .dv__vals { grid-template-columns: repeat(2, 1fr); }
  .dv__vals > :last-child { grid-column: 1 / -1; }
}

@media screen and (max-width: 767px) {
  /* 🔴 ONE UP. The 991px rule above splits 2 + 1; below this there is not
     enough width for two columns of running text — at 767 each would be
     ~360px minus the panel's own padding, which puts the longest heading
     ("A screen that serves, not one that watches.") on four lines. Stacked,
     each panel gets the full measure and the row becomes a list, which is
     what a phone wants anyway.

     The `:last-child` span from the 991 rule has to be undone here or it
     is a no-op that still overrides — one column makes `1 / -1` the same
     as the default, but leaving it means two rules disagreeing about the
     same element for anyone reading. */
  .dv__vals { grid-template-columns: 1fr; }
  .dv__vals > :last-child { grid-column: auto; }

  /* 🔴 THE RAIL COMES OUT ON A PHONE, and the stage stops sticking.
     Two reasons, and the second is not negotiable:

     1. auto, not 100svh. The name, the line, a 16:9 device and the
        pill do not fit a phone window at once — pinning the section
        would crop the object or bury the pill under the fold.
     2. svh is a MOVING NUMBER on a phone. The address bar collapses on
        the first flick, which is exactly the flick that starts the
        rail, so both the 260svh rail and the 100svh stage inside it
        re-resolve mid-gesture and the sticky element jumps. A pinned
        hero whose pin slips on the first scroll is worse than no pin.

     What survives is --hp itself: device.js still writes it, so the
     wash, the type and the object still cross — just over the section's
     own height instead of a rail's, in one screen of ordinary scroll. */
  .dv__hero-rail { height: auto; }
  .dv__hero-stage {
    position: static;
    height: auto;
    padding-top: clamp(6rem, 13vh, 9rem);
  }
  /* the height budget above is a desktop calculation — on a phone the
     column is as wide as the gutter allows and the page scrolls */
  .dv__hero-fig { width: 100%; margin-block: 2.25rem 2rem; }
  .dv__spec-row { grid-template-columns: 8rem 1fr; gap: 1rem; }
}

/* ============================================================
   reduced motion

   🔴 Gentler, not off. The old draft's `* { transition: none }` took
   the press feedback away with everything else, which is the one
   thing on the page that costs a vestibular system nothing and tells
   a touch user their tap landed. What goes: travel, the ambient mic
   pulse, the level meter, the ring. What stays: opacity, and the 3%
   press.
   ============================================================ */
@media (prefers-reduced-motion: reduce) {
  /* (.dv__mic was listed here too. There is no .dv__mic on this page and
     no @keyframes dv-mic either — both went with the old privacy strip,
     and the selector outlived them. The dot that just came off
     .dv__priv-mark was still asking for that same missing keyframe, which
     is to say it had not animated in a while.) */
  .dv__level i { animation: none; }
  /* 🔴 The lock's bar, not the mark's wrapper — the `i` never carried the
     animation; the shackle does. Turning it off leaves the lock in its
     untransformed state, which is CLOSED, which is the state the panel
     beside it is describing. Reduced motion gets the finished picture, not
     a padlock hanging open forever.

     🔴 And the same for the two marks that draw. Their rest state is
     already the finished stroke (dashoffset 0, see the note by
     .dv__val-slash), so cancelling the keyframe leaves a whole camera-bar
     and a whole pair of figures — not half a line each. */
  .dv__val.is-in .pv__lock-bar,
  .dv__val.is-in .dv__val-slash,
  .dv__val.is-in .dv__val-arc { animation: none; }

  /* 🔴 The hero un-pins, and the wash STAYS. The pin is the vestibular
     problem — a screen held still while the scrollbar keeps moving is
     the exact mismatch this preference is about — but the white-to-sky
     crossing is a colour change, costs nothing, and is most of what the
     section is saying. So the rail collapses to one screen and --hp
     runs over that instead. Same hero, no pin.

     The object still turns, once per progress event rather than sixty
     times a second (see the note in nestie3d.js): the turn carries the
     one fact a flat picture cannot, that the thing has a back. What
     goes is the idle loop and the pointer parallax. */
  .dv__hero-rail { height: auto; }
  .dv__hero-stage { position: static; height: auto; min-height: 100svh; }
  .dv__hero-fig .dv__pad { transform: none; }

  /* 🔴 The word is THERE, finished, in the frame the page loads. Not
     "faster" and not a shorter version of the same entrance: a 2.7s
     blur-and-scale is the exact class of motion this preference asks
     for less of, and a word that assembles itself is nothing anyone is
     missing. footer.js still writes .is-in — it just has nothing left
     to do. The colour is the FINISHED one, so the pale under copy is
     taken out rather than left showing through. */
  .dv__hero-word {
    opacity: 1; filter: none; transform: none; transition: none;
  }
  .dv__hero-word-under { display: none; }
  .dv__hero-word-ink { opacity: 1; transition: none; }
  .dv__hero-fig[data-reveal="fade"] { transition-delay: 0s; }
  /* the foot is standing there, finished: a 2.5s rise is travel, which
     is the thing this preference is asking for less of. footer.js hands
     .is-in over immediately under it, so this only has to take the
     transition off — the opacity would have arrived either way. */
  .dv__hero-lede,
  .dv__hero-fine { opacity: 1; transform: none; transition: none; }
  .dv__cta { opacity: 1; scale: 1; clip-path: inset(0 0 0 0 round 100px); }
}

/* ============================================================
   no script

   🔴 THE ONE STATE NOTHING ELSE COVERS. Every reveal on this page waits
   on footer.js's observer for its .is-in, so with scripting off the
   pill, the line and the print are three elements at opacity 0 — the
   ask is on the page and invisible, which is the one failure here that
   costs something.

   The rest of the hero degrades on its own: .dv__hero-word and the
   figure are held by footer.js's observer, which is the same class of
   dependency the whole page already has, and both are ALSO released by
   this block for the same reason. What cannot be fixed here is the
   canvas — no script, no WebGL object — and it does not need to be:
   .dv__hero-shot is a finished render and it is what stays.
   ============================================================ */
@media (scripting: none) {
  .dv__hero-lede,
  .dv__hero-fine { opacity: 1; transform: none; transition: none; }
  .dv__cta { opacity: 1; scale: 1; clip-path: inset(0 0 0 0 round 100px); }

  /* the word never gets its .is-in either — it stands there finished,
     in the colour it would have arrived at */
  .dv__hero-word { opacity: 1; filter: none; transform: none; }
  .dv__hero-word-under { display: none; }
  .dv__hero-word-ink { opacity: 1; }
  .dv__hero-name,
  .dv__hero-fig[data-reveal="fade"] { opacity: 1; transform: none; }
}
