/*
 * The shared design system for every Vanessa app site.
 *
 * ONE stylesheet, deployed into every site's bucket under /shared/. It is not
 * copied per site and must not be: two apps that are deliberately a family stop
 * looking like one the first time a rule is fixed in one copy and not the
 * other. What a site may change is the handful of custom properties in its own
 * /theme.css, which loads after this file -- accent colour, and nothing else.
 *
 * The family: navy and gold, from the Sprinter the apps are named after. Each
 * site takes its own accent from that pair -- VanessaCharge an electric gold, a
 * bolt for power. Everything structural is shared.
 *
 * This file is published verbatim into every site's bucket, so a comment here
 * is served to anyone who opens it. Do not name an app that has not been
 * announced: an unreleased name in a stylesheet is as public as one in a
 * heading, and less likely to be noticed before it ships.
 *
 * No framework, no build step, no web fonts. The whole site is a few pages of
 * text whose job is to still be readable in five years and to load before an
 * App Store reviewer loses patience. A font file is another request, another
 * thing that can 404, and another origin to allow in the
 * Content-Security-Policy -- which is `default-src 'none'` and stays that way.
 */

:root {
  /* The family palette. These do not change between sites. */
  --navy-950: #0a1522;
  --navy-900: #0f1e30;
  --navy-700: #223550;

  /* The one theme. A site's theme.css overrides --accent only.

     Dark only, deliberately: these sites are the navy the apps are built in,
     and a light mode was a second palette to keep in step for no one's benefit.
     `color-scheme` below tells the browser, so form controls, scrollbars and
     the flash before first paint are dark too rather than white. */
  --bg: #0a1522;
  --surface: #0f1e30;
  --fg: #e8e6e0;
  --muted: #98a5b4;
  --rule: #22364f;
  --link: #8fbce8;

  /* Fallback accent, in the shared gold. Every site overrides it; this value
     is what a new site looks like before it has a theme.css of its own. */
  --accent: #d9b467;

  /* The width ladder. Two rungs, and everything on a page sits on one of them.

     --measure is the reading column and never changes: 34rem is about 70
     characters in this face, and a line longer than that is harder to come
     back to at the start.

     --frame is the page's own outer edge -- the masthead rule, the footer
     rule, and the widest a screenshot is allowed to go. It opens in two steps
     as the viewport gains room, and because it is a token rather than a
     hard-coded width in three rules, the whole page steps together: the header
     moves out exactly as far as the screenshots do. It is deliberately equal
     to --measure at the bottom of the ladder, so on a phone every rule below
     resolves to the single column this used to be, with no separate mobile
     case to keep in step.

     52rem against 34rem is a ratio of 0.65, which is not a taste: shutterfoot,
     the site this proportion was measured against, holds a 1024px container
     with a 672px text column inside it -- 0.656. Landing on the same number
     from a different measure is the reason to trust it rather than a rounder
     one.

     The alternative -- one width for everything, which is what this was -- is
     what makes a page read as a text file rather than as a page. Nothing was
     wrong with any single element; there was simply only ever one edge. */
  --measure: 34rem;
  --frame: 34rem;

  /* The gap that means "new chapter". Everything else on the page is spaced in
     rem literals; this one is a token because three rules have to agree about
     it or the page loses its rhythm in one place and keeps it in another.

     6rem is measured, not chosen: shutterfoot's sections sit 96-128px apart
     and this is the bottom of that range. It went up when the landing page's
     prose was cut roughly in half -- the page kept its height and gained the
     room. Putting the words back is the edit this token exists to prevent. */
  --space-section: 4rem;
}

/* The ladder opens in two steps, and only here. Every rule that reads --frame
   or --space-section follows automatically, which is the point of writing them
   down once.

   46rem, not 40rem: the frame has to clear the text column by enough to read
   as deliberate. An 8rem-wide viewport margin either side is a frame; a 0.5rem
   one is a rendering fault. */
@media (min-width: 46rem) {
  :root {
    --frame: 40rem;
    --space-section: 6rem;
  }
}

@media (min-width: 60rem) {
  :root {
    --frame: 52rem;
  }
}

*,
*::before,
*::after {
  box-sizing: border-box;
}

html {
  -webkit-text-size-adjust: 100%;
  /* Not decoration: without it the browser paints a white canvas before first
     paint and renders form controls and scrollbars light on a dark page. */
  color-scheme: dark;
}

body {
  margin: 0;
  padding: 0 1.25rem 4rem;
  background: var(--bg);
  color: var(--fg);
  font-family: ui-serif, Georgia, "Iowan Old Style", "Times New Roman", serif;
  font-size: 1.0625rem;
  line-height: 1.6;
}

/* The reading column, centred. Everything a reader reads as a line of text is
   here and nowhere else. */
main {
  max-width: var(--measure);
  margin: 0 auto;
}

/* The masthead and the footer sit on the outer rung instead, so the two
   hairline rules bracket the page at its own edges rather than at the text's.
   Both are centred, so the column stays centred inside them and no left edge
   is thrown off -- the wordmark simply stands further out than the first
   paragraph, which is what tells a reader the column is a column.

   This is the one place the site says how wide it is. Held at --measure, the
   header rule stopped exactly where the prose stopped, and the two screenshots
   that break past it read as images that had escaped rather than as the widest
   rung of a ladder. */
header,
footer {
  max-width: var(--frame);
  margin: 0 auto;
}

header {
  padding: 2.5rem 0 0;
}

.masthead {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem 1rem;
  padding-bottom: 0.75rem;
  border-bottom: 1px solid var(--rule);
}

/* The wordmark is the one place the two sites look different on purpose: same
   type, same size, same position, different glyph and accent. */
.masthead a.wordmark {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  font-family: ui-sans-serif, system-ui, -apple-system, "Helvetica Neue", sans-serif;
  font-weight: 650;
  font-size: 1.0625rem;
  letter-spacing: -0.01em;
  color: var(--fg);
  text-decoration: none;
}

/* Each site ships its own /mark.svg -- a bolt or a pin, in that site's accent.
   An <img> rather than inline SVG so the glyph is one file per site instead of
   one copy per page, which is what keeps four pages from drifting apart. */
.masthead a.wordmark img {
  display: block;
  width: 1.375rem;
  height: 1.375rem;
}

nav {
  margin-left: auto;
  font-family: ui-sans-serif, system-ui, -apple-system, "Helvetica Neue", sans-serif;
  font-size: 0.875rem;
}

nav a {
  color: var(--muted);
  text-decoration: none;
  margin-left: 1rem;
}

nav a:hover,
nav a:focus,
nav a[aria-current="page"] {
  color: var(--fg);
  text-decoration: underline;
  text-decoration-color: var(--accent);
  text-underline-offset: 0.2em;
}

/* Derived from --space-section rather than set on its own, so the air under
   the masthead grows with the gaps between sections instead of drifting out of
   proportion to them the first time one of the two is tuned. */
main {
  padding-top: calc(var(--space-section) * 0.75);
}

h1 {
  font-size: 1.75rem;
  line-height: 1.25;
  letter-spacing: -0.015em;
  margin: 0 0 1rem;
}

/* The space above a heading is doing the work a paragraph used to do. When the
   landing page carried roughly twice as many words, 2.5rem was enough of a gap
   to read as a break; with the prose cut back it read as a list of short
   items instead of a page with chapters in it. The alternative -- putting the
   words back so the page still filled a screen -- is the thing that was wrong
   with it. Empty space beats a column of text nobody reads. */
h2 {
  font-size: 1.1875rem;
  line-height: 1.3;
  margin: var(--space-section) 0 0.75rem;
}

.lede {
  font-size: 1.1875rem;
  color: var(--fg);
}

p {
  margin: 0 0 1rem;
}

/* The indent belongs to the list, not to the prose. It used to be one rule
   covering p, ul and ol, which inset every paragraph on every page 1.25rem to
   the right of the heading above it -- visible on all four pages, and the kind
   of thing that reads as a rendering fault rather than a decision. A list
   still needs it, or the markers hang outside the measure. */
ul,
ol {
  margin: 0 0 1rem;
  padding-left: 1.25rem;
}

li {
  margin-bottom: 0.5rem;
}

/* A string the app itself puts on screen, quoted at display size directly
   under the question it answers.

   These replace paragraphs that described the same strings in longer words.
   "Discharging faster than avg" is three words and is the whole answer to "am
   I gaining or losing battery, and is that unusual?"; the sixty words that
   used to stand here said it worse, and the screenshot underneath had already
   said it better. The app's own interface is the best copy available on this
   page, so the page quotes it rather than paraphrasing it.

   Set in the sans face, not the body serif, because that is what the app is
   set in and the point is that these are the app talking. Coloured with
   --accent rather than --fg for the same reason -- it is a quotation, and a
   run of large serif text would read as the page raising its voice. The
   accent is safe for text at this size: vanessacharge's theme.css records
   #f0b429 as checked at 4.5:1 against --bg for exactly this use.

   No quotation marks. They would suggest a person said it. */
.says {
  font-family: ui-sans-serif, system-ui, -apple-system, "Helvetica Neue", sans-serif;
  font-size: 1.4375rem;
  line-height: 1.3;
  letter-spacing: -0.015em;
  color: var(--accent);
  text-wrap: balance;
  margin: 0 0 0.875rem;
}

/* The one thing this site wants a reader to do, placed under the lede.

   It was previously reachable only from the last section, about 86% of the way
   down the page, which asks someone to read the entire argument before finding
   out there is a way in. The link is repeated down there rather than moved,
   because a reader convinced by the first screenshot and a reader convinced by
   the founder's note are different people.

   Borrowed from .contact on the support page -- accent rule, sans face -- so
   this is the site's existing vocabulary for "act on this", not a new button
   component. It is deliberately not a filled button: the whole page argues
   that there is no funnel and nothing being counted, and a button styled to
   look like a conversion target is the one element that would contradict
   that. */
.ask {
  margin: 1.75rem 0 0;
  padding: 0.125rem 0 0.125rem 0.875rem;
  border-left: 3px solid var(--accent);
  font-family: ui-sans-serif, system-ui, -apple-system, "Helvetica Neue", sans-serif;
}

.ask a {
  font-size: 1.0625rem;
  font-weight: 600;
}

.ask .note {
  display: block;
  margin-top: 0.125rem;
  color: var(--muted);
  font-size: 0.9375rem;
}

/* A numbered walkthrough on a support page. The order is the whole content
   here -- the steps cross an app, a website and back again, and a reader who
   reaches the paste prompt without having copied anything is simply stuck --
   so these are an ol rather than the styled headings they could have been.
   More room between items than a plain list gets, because several of them
   carry a screenshot. */
.steps > li {
  margin-bottom: 1.5rem;
}

/* Links wrap mid-word when there is no other way to fit them. The policy pages
   cite outside privacy policies by their full URL, which is one unbreakable
   token; at 320px it hung 11px past the right edge and gave the page a
   horizontal scrollbar. `break-word` rather than `break-all` so ordinary link
   text still breaks at spaces and only a genuinely too-long token is cut. */
a {
  color: var(--link);
  text-underline-offset: 0.15em;
  overflow-wrap: break-word;
}

code {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.9em;
  background: color-mix(in srgb, var(--rule) 55%, transparent);
  padding: 0.1em 0.3em;
  border-radius: 3px;
}

/* FAQ sub-headings on the support page, and sub-sections in the policy. These
   are a class in here rather than a style attribute on the element because the
   Content-Security-Policy is `style-src 'self'`, which blocks inline styles --
   an inline style attribute would silently render unstyled in a browser and
   look fine in a local file preview, which is the worst combination. */
h3 {
  font-family: ui-sans-serif, system-ui, -apple-system, "Helvetica Neue", sans-serif;
  font-size: 1rem;
  line-height: 1.35;
  margin: 1.75rem 0 0.5rem;
}

/* Used for the "last updated" line and the provenance note under the policy. */
.meta {
  color: var(--muted);
  font-size: 0.9375rem;
}

/* The support page's contact block. Deliberately a link, not a form: a form
   needs a backend, and a backend is a thing that can be down when someone needs
   help -- which is the same argument the apps themselves make. */
.contact {
  margin: 1.5rem 0;
  padding: 1.25rem;
  background: var(--surface);
  border: 1px solid var(--rule);
  border-left: 3px solid var(--accent);
  border-radius: 6px;
}

.contact a {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 1.0625rem;
  word-break: break-all;
}

/* A screenshot of the app, sitting in the text directly under the claim it is
   evidence for. No border: the shots are light and the page is not, so they
   already read as objects sitting on it.

   One per section and full measure, rather than the two-up row this replaced.
   A pair of grabs side by side reduced each one to about 14rem, which is wide
   enough to see that a chart exists and not wide enough to read it -- and the
   charts are the whole argument of the section they illustrate.

   The files are cropped to the app's content, not shipped as whole-screen
   grabs, and that is the part worth not undoing. An iPad centres this app in a
   column about 56% of the screen and leaves grey either side; at the measure,
   that margin costs roughly half the linear size of everything inside it, and
   "~303 Wh below clear sky" stops being readable at the exact moment the
   paragraph above quotes it. Crop to the cards, keep the tab bar, drop the iOS
   status bar -- the clock and the battery are not the product.

   loading="lazy" on the images themselves: they sit below the fold, and this
   page's whole promise is that it loads before an App Store reviewer loses
   patience. */
.shot {
  display: block;
  width: 100%;
  height: auto;
  margin: 1.75rem 0 2rem;
  border-radius: 10px;
}

/* The same job as .shot, for a screenshot that arrives already in a device
   frame on a transparent background.

   No border-radius and no background: the frame draws its own corners, and
   rounding the box would clip them. The transparency is the point of using a
   framed shot at all -- a raw grab is a light rectangle punched into a dark
   page, and no amount of cropping fixes that, whereas a frame on alpha sits on
   the navy as an object. It is also why these stay PNG. A JPEG has no alpha,
   and would reintroduce exactly the white plate the frame removes.

   These break out of the measure, in two steps, as the viewport gains room.
   A screenshot pinned to the text column is exactly as wide as a paragraph,
   which reads as a footnote to the prose rather than the thing the prose is
   about. It is also the difference between legible and not: a landscape frame
   held to 34rem renders the app's interface at about half the size the
   portrait one did, and "~303 Wh below clear sky" stops being readable in the
   same breath as the sentence quoting it.

   Sized in rem, not vw. 100vw includes the scrollbar on a desktop browser, and
   the overflow that follows is a horizontal scrollbar on pages that must never
   look broken. Each step is narrower than the viewport it needs, so the image
   never reaches the edge, and below the first breakpoint this is an ordinary
   full-width image -- on a phone there is no room to break out of.

   The width is --frame, so a screenshot is exactly as wide as the masthead
   rule above it and the footer rule below it, and never wider. That is the
   whole reason --frame exists: the breakout used to be two hard-coded widths
   that answered to nothing else on the page, and an image sitting 9rem outside
   every other edge reads as an image that got loose. The negative margin is
   computed rather than written down -- it was a literal with the arithmetic in
   a trailing comment, which is a comment that goes quietly wrong the first
   time either number is tuned.

   One media query rather than two: --frame already steps at 60rem, so this
   rule follows without repeating itself. */
.device {
  display: block;
  width: 100%;
  height: auto;
  margin: 2rem 0 2.25rem;
}

@media (min-width: 46rem) {
  .device {
    width: var(--frame);
    margin-left: calc((var(--measure) - var(--frame)) / 2);
  }
}

/* A screenshot that opens over the page when clicked.

   Every screenshot is served at roughly twice the pixels it is shown at, so
   there is real detail behind them: the app's smaller labels are legible in the
   file and not on the page.

   This overlays the page rather than linking to the image file, and the first
   attempt did link to the file. That version is worth describing because it
   looks correct until you use it: the browser's own image view puts the file on
   a white page, jammed into the top-left corner with the rest of the window
   empty, and the only way back is the back button. Flattening the transparent
   frames onto --bg fixed the picture and not the page around it. An .html page
   per screenshot was the other option, and that is ten more files to keep in
   step with ten images.

   :target is the whole mechanism -- no JavaScript, which `default-src 'none'`
   blocks anyway. The overlay is a link back to the id of the screenshot that
   opened it, so closing returns to the place in the page the reader left rather
   than to the top. The overlay's copy of the image is a second <img> with the
   same src: the browser has it cached, and `display: none` with loading="lazy"
   means the one that is never opened is never fetched.

   The overlay is a separate element rather than the inline image promoting
   itself to fixed position, which was the version with less markup. Taking the
   inline image out of flow collapses the space it occupied, the page below
   jumps up by the height of a screenshot, and closing lands somewhere the
   reader was not. */
.zoom {
  display: block;
  cursor: zoom-in;
}

/* The only thing telling a reader a screenshot opens used to be
   `cursor: zoom-in`, which does not exist on a touch screen. The way this page
   reaches anyone is a link pasted into a Victron thread or a van-life group,
   so most readers are on a phone -- and the readers with the smallest screen
   are exactly the ones the full-size file is for.

   A caption under the image rather than a badge on top of it. The badge is
   furniture over the evidence; this is the page telling a reader what it is
   showing them, in the same muted sans it uses for every other aside. It sits
   inside the link, so the words are a click target too and not just a label
   pointing at one. */
.zoom-hint {
  display: block;
  margin: 0 0 2rem;
  color: var(--muted);
  font-family: ui-sans-serif, system-ui, -apple-system, "Helvetica Neue", sans-serif;
  font-size: 0.875rem;
}

/* The image gives up its bottom margin to the caption, so the pair reads as one
   object instead of as an image and an unrelated line of text. */
.zoom img {
  margin-bottom: 0.5rem;
}

.lightbox {
  display: none;
}

.lightbox:target {
  display: flex;
  position: fixed;
  inset: 0;
  z-index: 10;
  align-items: center;
  justify-content: center;
  padding: 3.5rem 1.25rem 1.25rem;
  background: var(--bg);
  cursor: zoom-out;
}

/* Not .shot or .device: this copy is sized to the window it is filling, and
   must not inherit either one's measure-relative width or breakout margin. */
.lightbox img {
  width: auto;
  max-width: 100%;
  max-height: 100%;
  margin: 0;
}

/* On a narrow screen, fitting the image to the window would show it at exactly
   the size it already is -- the page gives a screenshot the full column there,
   so "fit to the viewport" and "how it looked on the page" are the same
   picture, and tapping it would do nothing visible. Below the first rung the
   overlay therefore shows the file at its own size and lets the reader push it
   around, which is the only way this offers anything on a phone. */
@media (max-width: 46rem) {
  .lightbox:target {
    overflow: auto;
    align-items: flex-start;
    justify-content: flex-start;
  }

  .lightbox img {
    max-width: none;
    max-height: none;
  }
}

/* A word rather than a glyph. An x drawn in a pseudo-element is a character
   that screen readers announce and that means nothing at small sizes; the
   whole overlay is the click target anyway, so this is a label for what
   clicking does, not the control itself. */
.lightbox:target::after {
  content: "Close";
  position: absolute;
  top: 1.25rem;
  right: 1.5rem;
  font-family: ui-sans-serif, system-ui, -apple-system, "Helvetica Neue", sans-serif;
  font-size: 0.875rem;
  color: var(--muted);
}

/* Launch artwork, on a site's landing page only.
   A site may ship a /launch.jpg beside its /mark.svg and /theme.css; this is
   the third and last thing a site is allowed to differ by. Sized in rem and
   capped well below the measure, because the point of it is character, not a
   banner.

   It sits above the footer, where the line-art van used to, and not between
   the opening paragraph and the first section. Up there it is a screen of
   decoration on a phone before the reader has learned anything, and it pushes
   the screenshots -- which are what actually argue for the app -- three
   scrolls further down. Atmosphere signs the page off; evidence carries it.

   The artwork carries its own background at #00102d, a shade off --bg, so it
   settles into the page as a slightly deeper panel rather than sitting on it in
   a frame. That is why it needs no border. */
.launch {
  display: block;
  width: 100%;
  max-width: 19rem;
  height: auto;
  margin: 3.5rem auto 0;
  border-radius: 10px;
}

footer {
  margin-top: var(--space-section);
  padding-top: 1.25rem;
  border-top: 1px solid var(--rule);
  color: var(--muted);
  font-size: 0.875rem;
}

footer a {
  color: var(--muted);
}

/* The other app in the family. Present on both sites, pointing at each other --
   the one piece of cross-site linking, and the reason the footer is shared. */
.family {
  margin-top: 0.75rem;
}

@media (max-width: 32rem) {
  header {
    padding-top: 1.75rem;
  }

  nav {
    margin-left: 0;
    width: 100%;
  }

  nav a {
    margin: 0 1rem 0 0;
  }
}

/* Motion, and the two rules that stop it becoming a liability.
 *
 * The argument for having any of this is specific rather than decorative: this
 * app is about change over time -- gaining or losing, faster than usual, a week
 * measured against its own average -- so a page that is wholly still is
 * mismatched to the thing it is selling. That is a reason to move the two
 * elements that carry the argument. It is not a reason to decorate the page,
 * and nothing else here moves.
 *
 * There is no JavaScript, and there could not be: the Content-Security-Policy
 * is `default-src 'none'` with no script-src, so every scroll-observer library
 * and every hand-rolled IntersectionObserver is blocked before it runs. These
 * are CSS scroll-driven animations, which need no change to that header -- the
 * whole feature costs nothing in infra/main.tf, which a web font would not.
 *
 * RULE ONE: THE STILL PAGE IS THE DEFAULT AND MOTION IS ADDED TO IT.
 * The hidden state lives inside the two guards below, never on the element.
 * The usual way to write a reveal -- `opacity: 0` on the element, 1 via the
 * animation -- means anything that stops the animation leaves the content
 * permanently invisible: a reduced-motion preference, a browser with no
 * `animation-timeline`, a timeline that never resolves. These are the pages an
 * App Store listing points at, so a page that goes blank in a browser nobody
 * has shipped yet is not a hypothetical cost. Delete both guards and every page
 * renders finished.
 *
 * Measured, not assumed: on a document too short to scroll the timeline is
 * inactive, and the element computes to opacity 1 with no transform rather than
 * holding the `from` keyframe. Fail-open is the browser's behaviour here, and
 * the guards above are what make it the behaviour everywhere else too.
 *
 * RULE TWO: NOTHING A READER IS READING MAY MOVE OR FADE.
 * A reader who scrolls quickly must never meet a blank paragraph, so prose is
 * untouched. Only `.device`, a screenshot, which is an object rather than a
 * line, moves at all.
 *
 * RULE THREE: EVERY INTERMEDIATE STATE HAS TO BE A LEGITIMATE STATE.
 * This is the one that took four goes to find, and it is why the only animated
 * property here is `transform`.
 *
 * A scroll-driven animation has no idea whether its subject was already on
 * screen when the page loaded, and CSS cannot ask. Anything visible at
 * scrollY 0 and partway into its range therefore RENDERS partway through, and
 * stays there until the reader scrolls. On a 900px window the first screenshot
 * sits at y=764 and the first quoted line at y=543 -- both above the fold, both
 * mid-animation on first paint.
 *
 * That is fatal to a fade and to a colour change, and harmless to a position.
 * `.says` used to arrive by interpolating --muted to --accent, and a line
 * stuck at 68% of that renders rgb(164, 167, 161): a grey-green that is in
 * neither the palette nor the design, on the line carrying the app's own
 * answer to the question above it. The screenshot stuck at 0.6 opacity is the
 * same failure, more quietly.
 *
 * A translate has no such state. Every frame is the finished element, drawn
 * somewhere else, and "64px lower than it will settle" is not a defect a reader
 * can see even when it never resolves. So the fade is gone, the colour change
 * is gone, and the rise is the whole effect. It is also the only one of the
 * three that answers what was actually missing, which was movement.
 *
 * In practice this is the landing page alone. /privacy and /support are two of
 * the four URLs App Store Connect checks, motion on those is all downside, and
 * neither carries a `.device` -- so nothing here reaches them. The alternative,
 * naming the landing page in a selector, would put a page's identity in the
 * shared stylesheet, which is the one thing this file must not know about.
 *
 * WHERE ON THE SCREEN IT HAPPENS MATTERS MORE THAN HOW LONG IT TAKES.
 * `cover 0` is the instant the subject's top edge touches the BOTTOM of the
 * viewport. A range starting there plays out along the bottom edge of the
 * window, in peripheral vision, and finishes hundreds of pixels of scrolling
 * before the subject reaches the middle of the screen: correctly timed,
 * correctly sized, and staged off-camera.
 *
 * The offsets are in vh, which positions the effect rather than only scheduling
 * it, and vh specifically rather than a percentage of `cover`. The cover phase
 * is as long as the viewport PLUS the subject, so a percentage of it puts a
 * tall element at a different height on screen than a short one -- the same
 * element-height dependence that made `entry 10%` wrong, weakened but not
 * removed. An offset of X vh moves the subject up by exactly X vh whatever its
 * height, so `cover 35vh` means "top edge 65vh down the screen" and nothing
 * else. On a 900px window the rise runs from y=585 to y=270, finishing just
 * before the screenshot is centred at y=145. On a phone the same two numbers
 * put it at y=528 to y=244 against a centre of y=283. That is the property
 * worth keeping: one pair of numbers, the same place on every screen. */
@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {
    .device {
      animation: evidence-arrives linear both;
      animation-timeline: view();
      animation-range: cover 35vh cover 70vh;
    }
  }
}

@keyframes evidence-arrives {
  from {
    transform: translateY(4rem);
  }

  to {
    transform: none;
  }
}
