/* ==========================================================================
   Speak It — marketing site
   -------------------------------------------------------------------------
   The app's listening indicator sits in the gap in the headline, a day's worth
   of spoken thoughts scatters out of it and is drawn back in on scroll, and
   from there the page shows rather than tells: a capture demo you can talk to,
   one phone that changes screen as you scroll past it, and the seven ways in.

   The type, palette and component shapes are the app's: system sans throughout
   (the app has no serif anywhere), the palette lifted verbatim from
   SpeakIt/Components/SpeakItTheme.swift, and no brand accent — emphasis comes
   from weight, scale and whitespace. Light only, because that is the app's
   light appearance.
   ========================================================================== */

:root {
  color-scheme: light;

  /* speakBackground / speakSurface / speakInk / speakMuted / speakDivider */
  --bg:       #F8F8F6;
  --bg-rgb:   248 248 246;
  --surface:  #FFFFFF;
  --ink:      #0E0E0E;
  --ink-rgb:  14 14 14;
  --muted:    #6B6B6B;
  --line:     #D6D6D6;

  /* The app is SF and nothing else. No webfont, so no network request and no
     silent fallback to a face the app never uses. */
  --sans: -apple-system, BlinkMacSystemFont, "SF Pro Text", "SF Pro Display",
          "Segoe UI", system-ui, "Helvetica Neue", Arial, sans-serif;

  /* the indicator's diameter drives the hero's gap */
  --u: min(258px, 30vh, 54vw);

  --page:   1280px;

  /* One gutter for the whole page, so there is one place to make the safe area
     count. On a notched iPhone in landscape the inset is on one side only;
     taking the larger of the two keeps the page symmetrical rather than shunted
     off centre, which is what a one-sided padding looks like. */
  --gutter-base: clamp(20px, 4vw, 56px);
  --gutter: max(var(--gutter-base), env(safe-area-inset-left), env(safe-area-inset-right));

  /* The top bar's height, so anything that has to sit clear of it — the sticky
     device in the two-screen section — can say so rather than guess. stage.js
     measures the bar
     and overwrites this; the value here is what that measurement comes to at
     the default type size, and is only used before the first frame. */
  --bar: 76px;

  --ease:   cubic-bezier(.22, .61, .36, 1);
  --swift:  cubic-bezier(.32, .72, 0, 1);
}

/* ------------------------------------------------------------------ reset -- */

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--sans);
  font-size: 17px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

h1, h2, h3 { margin: 0; font-weight: 600; line-height: 1.06; }
p, blockquote, figure { margin: 0; }
ul { margin: 0; padding: 0; list-style: none; }
img, svg { display: block; max-width: 100%; }
a { color: inherit; text-decoration: none; }

/* Several components below set a `display`, which would otherwise beat the
   user-agent rule for `hidden` and leave a "closed" panel on screen. */
[hidden] { display: none !important; }

:focus-visible {
  outline: 1.5px solid var(--ink);
  outline-offset: 4px;
  border-radius: 4px;
}

.u-sr {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 60;
  padding: 12px 18px;
  background: var(--ink);
  color: var(--bg);
}
.skip-link:focus { left: 0; }

/* The app's own small-caps label, used for every section's eyebrow. */
.eyebrow {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--muted);
}

/* ================================ top bar ================================= */

.topbar {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 20;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  /* Safari's own chrome already eats the top inset, so this is 18px in a tab and
     the island's clearance when the page is installed to the Home Screen. */
  padding: max(18px, env(safe-area-inset-top)) var(--gutter) 18px;
  transition: background-color .4s var(--ease), border-color .4s var(--ease);
  border-bottom: 1px solid transparent;
}
/* The opening supplies its own soft top edge; past it the page scrolls plainly
   and the bar has to bring its own backdrop. */
.is-past-opening .topbar {
  background: rgb(var(--bg-rgb) / .86);
  backdrop-filter: saturate(150%) blur(14px);
  -webkit-backdrop-filter: saturate(150%) blur(14px);
  border-bottom-color: var(--line);
}

/* Where you are in the page, as one hairline. */
.topbar__progress {
  position: absolute;
  left: 0; bottom: -1px;
  height: 1px;
  width: calc(var(--sp, 0) * 100%);
  background: var(--ink);
  opacity: .3;
}

.wordmark {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: 14px;
  font-weight: 600;
  letter-spacing: -.01em;
}
.wordmark__glyph { width: 17px; height: 17px; }

.topbar__cta {
  padding: 9px 18px;
  border-radius: 999px;
  background: var(--ink);
  color: var(--bg);
  font-size: 13px;
  font-weight: 500;
  transition: opacity .22s var(--ease), transform .22s var(--swift);
}
.topbar__cta:active { transform: scale(.97); }

/* ========================= the listening indicator ========================= */

/* Fixed to the viewport, then moved so its centre lands on the gap in the
   headline — the type decides where it goes, not the other way round. It
   leaves as the last thought goes into it. */
.stage {
  position: fixed;
  inset: 0;
  z-index: 8;
  display: grid;
  place-items: center;
  pointer-events: none;
  translate: 0 var(--gap-y, 0px);
  opacity: calc(1 - var(--gone, 0));
}

.orb {
  --spread: 0;      /* breath + energy, added to every ring */
  --cs: 1;          /* core scale */
  --level: 0;       /* 0…1, how much is going in */
  --ring-a: .5;     /* opacity of the innermost ring */

  position: relative;
  width: var(--u);
  height: var(--u);
  display: grid;
  place-items: center;
}

/* A disc of paper that travels with the indicator, so headline type can pass
   close to it without ever colliding with it. */
.orb__halo {
  position: absolute;
  width: calc(var(--u) * 1.34);
  aspect-ratio: 1;
  border-radius: 50%;
  background: radial-gradient(circle,
    rgb(var(--bg-rgb)) 0 32%,
    rgb(var(--bg-rgb) / .9) 43%,
    rgb(var(--bg-rgb) / 0) 55%);
}

.orb__ring {
  position: absolute;
  width: calc(var(--u) * .73);
  aspect-ratio: 1;
  border-radius: 50%;
  border: 1px solid rgb(var(--ink-rgb) / calc(var(--ring-a) - var(--i) * .1));
  transform: scale(calc(.84 + var(--i) * .13 + var(--spread) * (1 + var(--i) * .34)));
  transition: border-color .8s var(--ease);
}

.orb__core {
  position: relative;
  width: calc(var(--u) * .313);
  aspect-ratio: 1;
  border-radius: 50%;
  background: var(--ink);
  /* kept inside the rings: a wider shadow spills past the halo and reads as a
     visible disc against the page */
  box-shadow: 0 12px 30px rgb(var(--ink-rgb) / .17);
  display: grid;
  place-items: center;
  transform: scale(var(--cs));
}

.orb__wave {
  width: 48%;
  height: auto;
  overflow: visible;
  transform: scaleY(calc(.72 + var(--level) * .5));
  transition: transform .3s var(--ease);
}
.orb__wave rect {
  transform-box: fill-box;
  transform-origin: 50% 50%;
  animation: bar var(--t) var(--d) ease-in-out infinite alternate;
}
@keyframes bar {
  from { transform: scaleY(.4); }
  to   { transform: scaleY(1); }
}

.orb[data-state="live"] { --ring-a: .5; }
.orb[data-state="calm"] { --ring-a: .26; }

/* ================================ the page ================================ */

main { position: relative; z-index: 2; }

/* ------------------------------------------------------------ opening ----- */

/* A short scroll with one pinned frame. `--p` runs 0 → 1 across it and is set
   by stage.js; the collapse, the fade, the hint and the download block all
   derive from it, so the sequence is a single number and scrubs in both
   directions.

   The scrolling part is the height minus one viewport. 138svh is a little over
   a third of a screen, and every part of the sequence is finished by the time
   the frame lets go — there is no dead scroll to sit through afterwards. */
.opening {
  --p: 0;
  --fade: 1;   /* the thoughts' own opacity, held until the very end */
  --tell: 1;   /* the scroll hint */
  --show: 0;   /* the download block, arriving as the thoughts leave */
  position: relative;
  height: 138svh;
}

.opening__pin {
  position: sticky;
  top: 0;
  height: 100svh;
  display: grid;
  place-items: center;
  padding-inline: var(--gutter);
  text-align: center;
  overflow: clip;
}

/* Every bubble is positioned by stage.js from one transform, so the entrance
   scatter, the idle drift and the scroll collapse compose into a single write
   per frame instead of fighting each other over separate properties. */
.field {
  position: absolute;
  inset: 0;
  z-index: 0;
  display: grid;
  place-items: center;
  pointer-events: none;
  opacity: var(--fade);
}

.bubble {
  grid-area: 1 / 1;
  /* stage.js owns `transform` and `opacity`; nothing else may touch them.
     Hidden until it does, so a page whose script never runs shows the hero on
     its own rather than a pile of bubbles stacked on the headline. */
  opacity: 0;
  will-change: transform;
}

.bubble span {
  position: relative;
  display: block;
  padding: 8px 15px 9px;
  border: 1px solid var(--line);
  border-radius: 16px;
  background: var(--surface);
  font-size: 13.5px;
  line-height: 1.35;
  color: var(--muted);
  white-space: nowrap;
  box-shadow: 0 10px 26px rgb(var(--ink-rgb) / .055);
}

/* The tail. It is what turns a pill into something somebody said — a small
   square of the same surface, rotated into the corner and given only the two
   borders that fall outside the bubble. */
.bubble span::after {
  content: "";
  position: absolute;
  bottom: -5.5px;
  left: 19px;
  width: 10px;
  height: 10px;
  background: var(--surface);
  border-right: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
  border-bottom-right-radius: 3px;
  transform: rotate(52deg) skew(-8deg, -8deg);
}
/* every other one is a different speaker, talking from the other side */
.bubble--r span::after {
  left: auto; right: 19px;
  transform: rotate(38deg) skew(8deg, 8deg);
}

/* The opening's own soft edges. They sit above the field and below the copy,
   so a thought swinging past twelve or six dissolves into the page instead of
   colliding with the eyebrow or the download block. */
.opening__edge {
  position: absolute;
  left: 0; right: 0;
  z-index: 1;
  pointer-events: none;
}
.opening__edge--top {
  top: 0;
  height: 18svh;
  background: linear-gradient(to bottom,
    rgb(var(--bg-rgb)) 24%, rgb(var(--bg-rgb) / .84) 56%, rgb(var(--bg-rgb) / 0) 100%);
}
.opening__edge--bottom {
  bottom: 0;
  height: 22svh;
  background: linear-gradient(to top,
    rgb(var(--bg-rgb)) 30%, rgb(var(--bg-rgb) / .84) 60%, rgb(var(--bg-rgb) / 0) 100%);
}

.opening__eyebrow {
  position: absolute;
  /* proportional while there is height to be proportional with, and clear of
     the bar once there is not */
  top: max(9.5vh, calc(var(--bar) + 10px));
  left: 50%;
  translate: -50% 0;
  z-index: 2;
  /* The same paper the headline carries, at label size. The eyebrow sits in
     the top edge's fade, where a thought swinging past twelve is still at
     most of its strength, and eleven letters of tracked small caps over a
     moving bubble was the one place on the page that was hard to read. */
  padding: 8px 40px;
  margin-top: -8px;
  background: radial-gradient(50% 50% at 50% 50%,
    rgb(var(--bg-rgb)) 0 60%,
    rgb(var(--bg-rgb) / .9) 78%,
    rgb(var(--bg-rgb) / 0) 100%);
}

/* One centred stack: headline, standfirst, and the slot the download block
   arrives in. They are all in normal flow, so no amount of type or viewport
   can make one overlap another — the indicator is what moves to meet the gap,
   not the copy that moves to clear the indicator. */
.hero {
  position: relative;
  z-index: 2;
  display: grid;
  justify-items: center;
}

.hero__title {
  /* Above the foot's paper. The foot's disc is 165% of the foot's height and
     centred on it, so its top third reaches up over the second headline line —
     and, painted later in the same stacking context, it was painting over the
     type: "you forget it." arrived with its lower half dissolved into the page
     on every viewport, and almost entirely on a phone. One layer up, and the
     disc still dissolves the thoughts that cross it, just underneath the words. */
  position: relative;
  z-index: 1;
  display: grid;
  justify-items: center;
  font-size: clamp(38px, 6.6vw, 82px);
  font-weight: 600;
  line-height: 1.0;
  letter-spacing: -.035em;
  text-wrap: balance;
}
/* the indicator is moved to sit in this gap */
.hero__gap { height: calc(var(--u) * .98); }

/* There is no arrangement of an ellipse that clears a full-width headline at
   twelve and six o'clock and still clears the copy underneath. So the thoughts
   are allowed to pass behind the words, and every block of type carries a disc
   of paper that dissolves them as they cross — the same halo the indicator
   uses, for the same reason. */
.hero__line,
.hero__foot {
  position: relative;
  isolation: isolate;
}
.hero__line::before,
.hero__foot::before {
  content: "";
  position: absolute;
  z-index: -1;
  left: 50%; top: 50%;
  translate: -50% -50%;
  width: calc(100% + 15vw);
  height: 215%;
  background: radial-gradient(50% 50% at 50% 50%,
    rgb(var(--bg-rgb)) 0 44%,
    rgb(var(--bg-rgb) / .88) 64%,
    rgb(var(--bg-rgb) / 0) 100%);
}
/* The foot is a wide, short block with a bubble able to cross it anywhere, so
   its paper holds full strength further out than the headline's does. */
.hero__foot::before {
  width: calc(100% + 26vw);
  height: 165%;
  background: radial-gradient(50% 50% at 50% 50%,
    rgb(var(--bg-rgb)) 0 58%,
    rgb(var(--bg-rgb) / .94) 78%,
    rgb(var(--bg-rgb) / 0) 100%);
}

.hero__foot {
  margin-top: clamp(22px, 4vh, 42px);
  width: min(680px, 100%);
  display: grid;
  justify-items: center;
  gap: 18px;
}

.hero__sub {
  max-width: 34ch;
  font-size: 16px;
  line-height: 1.6;
  color: var(--muted);
  text-wrap: balance;
}

/* One slot, two occupants. Both are laid into the same grid cell so the
   headline above never moves as they trade places. */
.hero__slot {
  display: grid;
  place-items: center;
  min-height: 92px;
  width: 100%;
}
.hero__slot > * { grid-area: 1 / 1; }

.hint {
  display: grid;
  justify-items: center;
  gap: 10px;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: .2em;
  text-transform: uppercase;
  color: var(--muted);
  opacity: var(--tell);
  pointer-events: none;
}
.hint__rule {
  width: 1px;
  height: 34px;
  background: linear-gradient(to bottom, var(--line), rgb(var(--bg-rgb) / 0));
}

.hero__get {
  opacity: var(--show);
  translate: 0 calc((1 - var(--show)) * 12px);
  pointer-events: none;
}
.hero__get.is-ready { pointer-events: auto; }

/* ------------------------------------------------------------------ get --- */

.get {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: clamp(16px, 3vw, 32px);
  flex-wrap: wrap;
}

.appstore {
  display: inline-flex;
  align-items: center;
  gap: 12px;
  padding: 12px 24px 12px 20px;
  border-radius: 999px;
  background: var(--ink);
  color: var(--bg);
  transition: opacity .22s var(--ease), transform .22s var(--swift);
}
.appstore:active { transform: scale(.98); }
.appstore__glyph { width: 22px; height: 22px; }
.appstore__text  { display: grid; text-align: left; }
.appstore__small { font-size: 10px; letter-spacing: .06em; opacity: .74; }
.appstore__big   { font-size: 16px; font-weight: 600; line-height: 1.2; }

.qr {
  display: flex;
  align-items: center;
  gap: 13px;
}
.qr img {
  width: 74px; height: 74px;
  padding: 7px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 13px;
}
.qr__label {
  font-size: 12px;
  line-height: 1.45;
  text-align: left;
  color: var(--muted);
}
.qr__label b { display: block; font-weight: 500; color: var(--ink); }

/* ============================ shared sections ============================= */

.shell {
  width: 100%;
  max-width: var(--page);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.section {
  position: relative;
  padding-block: clamp(76px, 12vh, 128px);
}
.section--tint {
  background: var(--surface);
  border-block: 1px solid var(--line);
}

.section__head {
  display: grid;
  gap: 12px;
  max-width: 44ch;
  margin-bottom: clamp(38px, 6vh, 60px);
}
.section__head--centre {
  justify-items: center;
  text-align: center;
  margin-inline: auto;
}
.section__title {
  font-size: clamp(28px, 3.6vw, 46px);
  font-weight: 600;
  line-height: 1.08;
  letter-spacing: -.03em;
  text-wrap: balance;
}
.section__sub {
  font-size: 16px;
  line-height: 1.6;
  color: var(--muted);
  text-wrap: pretty;
}

/* ------------------------------------------------------------- controls --- */

.btn {
  font: inherit;
  font-size: 14px;
  font-weight: 500;
  padding: 12px 22px;
  border: 1px solid transparent;
  border-radius: 999px;
  cursor: pointer;
  transition: opacity .2s var(--ease), transform .2s var(--swift);
}
.btn--solid { background: var(--ink); color: var(--bg); }
.btn--solid:active { transform: scale(.98); }

/* The app's own quiet secondary action — a grey capsule, never an outline. */
.ghost {
  font: inherit;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-height: 40px;
  padding: 9px 18px;
  border: 0;
  border-radius: 999px;
  background: rgb(var(--ink-rgb) / .06);
  color: var(--ink);
  font-size: 14px;
  font-weight: 500;
  cursor: pointer;
  transition: background-color .2s var(--ease), transform .2s var(--swift);
}
.ghost:active { transform: scale(.98); }
.ghost svg { width: 17px; height: 17px; }

.chip {
  font: inherit;
  font-size: 13px;
  padding: 8px 15px;
  min-height: 36px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: transparent;
  color: var(--muted);
  cursor: pointer;
  transition: color .2s var(--ease), border-color .2s var(--ease), transform .2s var(--swift);
}
.chip:active { transform: scale(.97); }

/* ============================== the demo ================================== */

/* The app's capture screen, running in the page. Same indicator, same two
   lines of copy, same "Type instead" escape hatch. */
.demo {
  display: grid;
  gap: clamp(26px, 4vh, 40px);
  justify-items: center;
}

.demo__stage {
  width: 100%;
  max-width: 640px;
  display: grid;
  justify-items: center;
  gap: 18px;
}

.mic {
  --u: clamp(150px, 21vw, 186px);
  display: grid;
  place-items: center;
  width: var(--u);
  height: var(--u);
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: none;
  cursor: pointer;
  transition: transform .25s var(--swift);
}
.mic:active:not([disabled]) { transform: scale(.98); }

/* Listening is the one place the page raises its voice, and it does it the way
   the app does: the rings open up and the core holds a soft ring of ink. */
.demo.is-listening .orb__core {
  box-shadow: 0 12px 30px rgb(var(--ink-rgb) / .2),
              0 0 0 6px rgb(var(--ink-rgb) / .07);
}

.demo__state {
  font-size: clamp(22px, 2.6vw, 28px);
  font-weight: 600;
  letter-spacing: -.02em;
  text-align: center;
}
.demo__hint {
  margin-top: -12px;
  font-size: 15px;
  color: var(--muted);
  text-align: center;
  text-wrap: balance;
}

/* What is being heard, as it arrives. It holds its height so the buttons below
   do not hop the moment the first word lands. */
.demo__live {
  min-height: 3.2em;
  max-width: 32ch;
  font-size: clamp(18px, 2vw, 22px);
  line-height: 1.45;
  letter-spacing: -.015em;
  text-align: center;
  text-wrap: balance;
}
.demo__live:empty { min-height: 0; }
.demo__live .is-interim { color: var(--muted); }

.demo__actions {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
  justify-content: center;
}

.demo__form {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
  justify-content: center;
  width: 100%;
  max-width: 520px;
}
.demo__input {
  font: inherit;
  font-size: 15px;
  flex: 1 1 18rem;
  min-width: 0;
  padding: 12px 18px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--surface);
  color: var(--ink);
  transition: border-color .2s var(--ease);
}
.demo__input::placeholder { color: var(--muted); }
.demo__input:focus { border-color: var(--ink); }

.demo__chips {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  justify-content: center;
  /* Allowed past the 640px reading column the rest of the demo is set in, and
     symmetrically, so the row stays centred on the indicator above it. Five
     examples is one row of suggestions; at 640 the fifth dropped to a second
     row on its own and read as an afterthought rather than as one of five.
     They still wrap on a narrow screen, just later. */
  margin-inline: clamp(-60px, calc((100vw - var(--gutter) * 2 - 100%) / -2), 0px);
}

.demo__note {
  max-width: 52ch;
  font-size: 12.5px;
  line-height: 1.6;
  color: var(--muted);
  text-align: center;
}

/* ------------------------------------------------------------- the verdict - */

/* Where it landed, drawn as the app draws it: a real Today row or a real
   Memory entry, beside the reason and the words that were kept. */
.verdict {
  width: 100%;
  max-width: 900px;
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  align-items: start;
  gap: clamp(24px, 4vw, 52px);
  padding: clamp(22px, 3vw, 32px);
  border: 1px solid var(--line);
  border-radius: 22px;
  background: var(--bg);
}

.verdict.is-new { animation: land .5s var(--swift); }
@keyframes land {
  from { opacity: 0; translate: 0 14px; }
  to   { opacity: 1; translate: 0 0; }
}

.verdict__side { display: grid; gap: 8px; align-content: start; }

.verdict__where {
  display: flex;
  align-items: baseline;
  gap: 9px;
  font-size: 13px;
  font-weight: 600;
  color: var(--muted);
}
.verdict__where b {
  font-size: 19px;
  font-weight: 600;
  color: var(--ink);
  letter-spacing: -.02em;
}

.verdict__why {
  font-size: 14.5px;
  line-height: 1.6;
  color: var(--muted);
}
.verdict__said {
  font-size: 15px;
  line-height: 1.55;
  color: var(--ink);
}
.verdict__block { display: grid; gap: 7px; }
.verdict__block + .verdict__block {
  margin-top: 16px;
  padding-top: 16px;
  border-top: 1px solid var(--line);
}

/* --------------------------------------------------------- the app pieces - */

/* One Today section head and one row, at the app's own proportions — and one
   definition of it for the whole page. There is no card here because there is
   no card in the app: `thoughtSection` in `TodayView.swift` is a muted headline
   with a count, a hairline, and rows. The demo used to draw its result inside a
   white bordered panel, which put a shape around the row that the product does
   not have, and left the page with two different ideas of what an app row
   looks like. */
.approw {
  display: grid;
  gap: 9px;
}
.approw__head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 10px;
  padding-bottom: 8px;
  border-bottom: 1px solid var(--line);
  font-size: 13.5px;
  font-weight: 600;
  letter-spacing: -.005em;
  color: var(--muted);
}
/* Top-aligned, like `CapturedItemRow`'s `HStack(alignment: .top)` — a row
   whose title wraps to two lines keeps its control and its trailing detail on
   the first line rather than drifting to the middle of the block. */
.approw__row {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: start;
  gap: 12px;
}
/* `Image(systemName: "circle")` at 24pt in `speakMuted` — a full-strength
   ring, not a hairline. Drawn faint, it read as a disabled control. */
.approw__check {
  width: 21px; height: 21px;
  border: 1.5px solid var(--muted);
  border-radius: 50%;
}
/* The Memory glyph well: `speakSurface` inside a 1px `speakDivider` circle
   with the symbol in `speakInk`, which is what the app draws for a row with no
   completion control. */
.approw__mark {
  width: 23px; height: 23px;
  display: grid;
  place-items: center;
  border: 1px solid var(--line);
  border-radius: 50%;
  background: var(--surface);
  color: var(--ink);
}
.approw__mark svg { width: 11.5px; height: 11.5px; }
.approw__body  { display: grid; gap: 3px; min-width: 0; padding-top: 1px; }
.approw__title {
  font-size: 15px;
  font-weight: 500;
  line-height: 1.32;
  overflow-wrap: anywhere;
}
.approw__meta  { font-size: 12px; line-height: 1.35; color: var(--muted); }
.approw__time  {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  padding-top: 2px;
  font-size: 12px;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
/* A place reminder's trailing text is a place name, and "Home" on its own
   reads as a category. The app puts `mappin.and.ellipse` in front of it for
   exactly that reason, so this does too. */
.approw__pin { width: 11px; height: 11px; flex: none; }

/* ============================== what happens ==============================
   One capture, three thoughts, and the three different things they turn into.

   The rule this section is built on: **nothing here may be a shape the app
   does not have.** Speak It's lists are not cards. A row in Today is a title,
   a category line and a trailing detail sitting straight on the background
   under a hairline — there is no white surface, no border and no radius
   anywhere in `CapturedItemRow.swift`. So the things that come out of the
   phone are not cards either. Each one is a slab of the app's own background,
   lifted off the page by a shadow and nothing else, carrying the app's section
   header, the app's row, and the sentence that produced it.

   That last line is the point of the product, so it is inside the result
   rather than floating above it in a quote bubble: the app keeps your original
   wording underneath the tidied title, and this is that, drawn.

   Everything is authored in its **finished** state and the motion is only the
   order it arrives in. A browser that never starts the sequence — no
   JavaScript, no IntersectionObserver, reduced motion — shows the composition
   already assembled, because the picture has to explain the product on its own
   before it explains it in sequence.
   ======================================================================== */

.emerge { padding-bottom: clamp(56px, 10vh, 110px); }

/* The room. One vanishing point for everything in it, placed over the phone
   rather than in the middle of the box, so the slabs read as turned to face
   somebody standing at the device. `preserve-3d` is what makes the browser
   sort them by depth instead of by source order. */
.scene {
  --beat: .82s;          /* one thought; the pace of the whole section */
  position: relative;
  max-width: 1180px;
  margin-inline: auto;
  aspect-ratio: 1180 / 780;
  perspective: 1500px;
  perspective-origin: 26% 50%;
  transform-style: preserve-3d;
}

/* The soft ground the device stands on. Pushed to the back of the room so a
   slab passing over it is unambiguously in front.

   Two things about it are deliberate. It is **small** — a disc around the
   phone rather than a wash across the whole scene — because a very low-alpha
   gradient stretched over a thousand pixels is the classic recipe for banding,
   and on its own composited layer the bands resolved as a visible grey
   rectangle sitting in the gap between the slabs. And it has **five stops on
   an eased ramp** rather than three on a linear one, which is what actually
   removes the contouring at these alphas; more stops, not more blur. */
.scene__ground {
  position: absolute;
  left: 4%; top: 8%;
  width: 46%; height: 88%;
  transform: translateZ(-160px);
  background: radial-gradient(closest-side circle at 40% 58%,
    rgb(var(--ink-rgb) / .055) 0%,
    rgb(var(--ink-rgb) / .040) 28%,
    rgb(var(--ink-rgb) / .022) 50%,
    rgb(var(--ink-rgb) / .008) 72%,
    rgb(var(--ink-rgb) / 0) 100%);
  pointer-events: none;
}

/* --------------------------------------------------------------- device --- */

/* The left-angle presentation, reproduced rather than exported. The device is
   turned so its left flank recedes and its screen faces the space the thoughts
   arrive in, which is the whole reason for the angle: a straight-on phone puts
   the results beside the product, and this one puts them coming out of it.

   No Rotato or Spline asset is embedded, and that is a constraint rather than
   a preference — the three sentences physically leave this screen and become
   HTML, and a baked render cannot hand one over. Drawn, it also stays sharp at
   any size, costs no bytes, needs no WebGL context on a phone, and follows the
   app's colours automatically.

   The thickness is flat offset shadows rather than a rotated side panel. A
   real panel needs its own corner radii to meet the face's, and the seam where
   they fail to is the thing that makes a CSS device look like a CSS device.

   `container-type` is here so the screen's contents can be sized in `cqw` —
   percentages of the device's own width. Everything inside is then one design
   at every size, and every value below is the app's real point size divided by
   the 420pt width of the screen it is drawn from. */
.tilt {
  container-type: inline-size;
  position: absolute;
  left: 7%;
  top: 50%;
  width: 29.5%;
  transform:
    translate3d(0, -50%, 0)
    rotateY(-16deg) rotateX(3deg) rotateZ(1deg);
  transform-style: preserve-3d;
}
.tilt__body {
  position: relative;
  padding: 2.1cqw;
  border-radius: 13cqw;
  background: linear-gradient(112deg, #38383a 0%, #17171a 40%, #0a0a0c 100%);
  box-shadow:
    /* the flank, three plates receding from the face */
    -5px 1px 0 -1px #2f2f33,
    -10px 2px 0 -4px #17171a,
    -14px 3px 0 -7px #0a0a0c,
    /* contact, then the weight of the thing on the page */
    -22px 18px 30px -14px rgb(var(--ink-rgb) / .22),
    -40px 46px 70px -26px rgb(var(--ink-rgb) / .26),
    0 86px 100px -48px rgb(var(--ink-rgb) / .42);
}
/* The polished rim. At this angle it is what actually reads as titanium —
   a bright, narrow catch down the leading edge, a dimmer one on the far edge,
   and a soft top light. Without it the body is a dark rounded rectangle. */
.tilt__body::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 13cqw;
  background:
    linear-gradient(99deg,
      rgb(255 255 255 / .34) 0 1.1%,
      rgb(255 255 255 / .07) 2.6%,
      rgb(255 255 255 / 0) 12% 84%,
      rgb(255 255 255 / .05) 96%,
      rgb(255 255 255 / .16) 100%),
    linear-gradient(to bottom,
      rgb(255 255 255 / .10) 0%,
      rgb(255 255 255 / 0) 9% 92%,
      rgb(255 255 255 / .05) 100%);
  pointer-events: none;
}

/* The buttons, on the flank we can see. They are what stops the silhouette
   reading as a generic slab — three small plates standing slightly proud of
   the left edge, lit from the same side as the rim. */
.tilt__btn {
  position: absolute;
  left: 0;
  width: 1.5cqw;
  border-radius: .7cqw 0 0 .7cqw;
  background: linear-gradient(to right, #101013, #2e2e32);
  box-shadow: -1px 0 0 rgb(255 255 255 / .16);
}
.tilt__btn--action { top: 20.5%; height: 4.6%; translate: -1.15cqw 0; }
.tilt__btn--up     { top: 28.5%; height: 7.4%; translate: -1.25cqw 0; }
.tilt__btn--down   { top: 38.0%; height: 7.4%; translate: -1.25cqw 0; }

.tilt__screen {
  position: relative;
  aspect-ratio: 1320 / 2868;
  border-radius: 11cqw;
  overflow: hidden;
  background: var(--bg);
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 5.4cqw 4.8cqw 5.6cqw;
  line-height: 1.4;
}

/* The Dynamic Island. On a light screen this one shape is most of what says
   iPhone rather than phone, and it is why the capture chrome below it starts
   lower than the padding alone would put it. */
.tilt__island {
  position: absolute;
  top: 2.4cqw; left: 50%;
  width: 27cqw; height: 8cqw;
  translate: -50% 0;
  border-radius: 999px;
  background: #08080a;
}

/* A single sheet of glass over the whole screen: one weak diagonal catch, and
   a darkening into the far edge where the panel turns away from us. Kept under
   6% — anything stronger stops reading as glass and starts reading as haze. */
.tilt__glass {
  position: absolute;
  inset: 0;
  border-radius: 11cqw;
  background:
    linear-gradient(104deg,
      rgb(255 255 255 / .5) 0%,
      rgb(255 255 255 / 0) 26%),
    linear-gradient(258deg,
      rgb(var(--ink-rgb) / .05) 0%,
      rgb(var(--ink-rgb) / 0) 22%);
  pointer-events: none;
}

/* The app's own capture chrome, at the app's own proportions: close control,
   wordmark, indicator, state, hint, transcript, escape hatch. Every size here
   is `CaptureView.swift`'s point size over the 420pt screen it is drawn from —
   `SPEAK IT` is `.caption` at 12pt, so 12/420 of the screen, and so on. */
.cap__bar {
  position: relative;
  width: 100%;
  margin-top: 7.4cqw;              /* clear of the island */
  display: flex;
  justify-content: center;
  align-items: center;
}
.cap__x {
  position: absolute;
  left: 0;
  width: 8.6cqw; height: 8.6cqw;
  border-radius: 50%;
  background: rgb(var(--ink-rgb) / .08);
}
/* the two strokes of the app's close glyph */
.cap__x::before, .cap__x::after {
  content: "";
  position: absolute;
  inset: 50% 30%;
  height: 1px;
  background: var(--ink);
  opacity: .82;
}
.cap__x::before { transform: rotate(45deg); }
.cap__x::after  { transform: rotate(-45deg); }
/* The name, spelled the way the top bar spells it. It was `SPEAK IT` at half an
   em of tracking here and `Speak It` fifty pixels up the page, so the drawing of
   the app and the page selling it disagreed about the product's own name. The
   app now sets it this way too. */
.cap__word {
  font-size: 3.1cqw;
  font-weight: 600;
  letter-spacing: -.01em;
  color: var(--muted);
}

.cap__orb {
  --u: 52cqw;                      /* ListeningOrb is 230pt on a 420pt screen */
  --spread: .05;
  --cs: 1;
  --level: .34;
  --ring-a: .42;
  margin: 13cqw 0 4.2cqw;
}
/* A breath of light behind the indicator that lifts as each thought leaves it.
   This is what makes the sentences read as coming out of the interface rather
   than out of the air beside it, and it is the only moving thing on the screen
   that is not the indicator itself. */
.cap__flare {
  position: absolute;
  top: 40%; left: 50%;
  width: 78cqw; aspect-ratio: 1;
  translate: -50% -50%;
  border-radius: 50%;
  background: radial-gradient(circle,
    rgb(var(--ink-rgb) / .12) 0%, rgb(var(--ink-rgb) / 0) 62%);
  opacity: 0;
  pointer-events: none;
}

.cap__state {
  font-size: 5.2cqw;               /* .title2.weight(.semibold) */
  font-weight: 600;
  letter-spacing: -.02em;
  color: var(--ink);
}
.cap__hint {
  margin-top: 1.9cqw;              /* the 8pt gap under the title */
  max-width: 69cqw;                /* the app's 290pt frame */
  font-size: 3.4cqw;               /* .subheadline */
  text-align: center;
  color: var(--muted);
}

/* Three sentences in one slot. Each is held for its turn and leaves as its
   thought does, so the screen is always showing the one that is in flight. */
.cap__live {
  position: relative;
  width: 100%;
  min-height: 9.6cqw;
  margin-top: 4.3cqw;
  font-size: 4.5cqw;               /* .title3 */
  text-align: center;
  color: rgb(var(--ink-rgb) / .84);
}
.cap__live span { position: absolute; inset: 0; opacity: 0; }
/* The sentence the screen rests on — the last one, so the phone and the last
   slab agree with each other once everything has landed. It is also the state
   a page with no sequence to run shows. */
.cap__live span:last-child { opacity: 1; }

.cap__type {
  margin-top: auto;
  display: inline-flex;
  align-items: center;
  gap: 1.5cqw;
  padding: 2.6cqw 4.3cqw;
  border-radius: 999px;
  background: rgb(var(--ink-rgb) / .08);
  font-size: 3.4cqw;
  font-weight: 500;
  color: var(--ink);
}
.cap__type svg { width: 4.4cqw; height: 4.4cqw; }

/* ------------------------------------------------------------- the emits -- */

/* One thought, and what it became — as one object.

   `--x`/`--y`/`--w`/`--z` are where the group settles; `--fx`/`--fy`/`--fz`
   are where it flies in from, authored as the offset back to the phone's
   transcript line. Three arrivals then converge on the words they came out of
   without any of them being animated along a path.

   They are in the sheet rather than on the elements because the narrow tier
   has to restate every one of them, and an inline custom property cannot be
   overridden by a media query. */
.emit {
  --face: -6deg;      /* turned with the device, not flat to us */
  --pad-x: 20px;
  --pad-y: 16px;
  /* A container, so the launch offsets below can be written in `cqw` —
     percentages of this group's own width. The sentence that flies is a single
     line of text whose box is a few pixels tall, so expressing its travel as a
     percentage of *itself* made `--fy` come out at 828%, and a sentence that
     wrapped to two lines would then have launched from somewhere else entirely.
     Against the group's width the numbers are stable and legible. */
  container-type: inline-size;
  position: absolute;
  left: var(--x);
  top: var(--y);
  width: var(--w);
  display: grid;
  gap: 12px;
  padding: var(--pad-y) var(--pad-x);
  transform: translate3d(0, 0, var(--z)) rotateY(var(--face));
}

/* The three settle as a descending zigzag that keeps overlapping the device
   rather than standing clear of it — a result that never touches the phone it
   came out of reads as a card placed beside a product shot. A and C lap the
   screen's right edge; B is the far one, and is the only one fully clear.

   `--fx`/`--fy` are short because the launch point is the phone's transcript
   line, which is only a little to the left of where A and C settle. */
/* `--fx`/`--fy` are not guesses. They were solved for: the offset that puts
   the flying sentence's centre exactly on the phone's transcript line at the
   first frame, measured in the page and rounded. Change the device's size or
   position and they have to be re-solved, or the sentences stop coming out of
   the screen and start coming out of the air beside it. */
/* a place, not a time — nearest, and lapping the screen */
.emit--a { --i: 0; --x: 32%; --y:  2%; --w: 41%; --z: 150px; --fx: -81.3cqw; --fy:  60cqw; --face: -7deg; }
/* a time spoken rather than picked — the far one */
.emit--b { --i: 1; --x: 42%; --y: 34%; --w: 41%; --z: -40px; --fx: -108.6cqw; --fy:   4.2cqw; --face: -5deg; }
/* nothing to do, and worth keeping */
.emit--c { --i: 2; --x: 35%; --y: 66%; --w: 41%; --z:  70px; --fx: -89.7cqw; --fy: -52.3cqw; }

/* The paper. It is the app's background rather than a white surface, because
   that is what a piece of a Speak It screen is made of — the shadow is the
   only thing lifting it, and there is no border at all. Drawn as its own
   element rather than as the group's background so it can arrive after the
   sentence it forms around. */
.emit__sheet {
  position: absolute;
  inset: 0;
  border-radius: 20px;
  background: var(--bg);
  box-shadow:
    0 1px 2px rgb(var(--ink-rgb) / .05),
    0 10px 22px -8px rgb(var(--ink-rgb) / .10),
    0 38px 62px -26px rgb(var(--ink-rgb) / .20);
}

.emit__app,
.emit__said { position: relative; }   /* above the sheet */

.emit__app { display: grid; gap: 9px; }

/* The destination is drawn as the app's section header — same rule as
   `.approw__head`, which is what it is. Sentence case and quiet, because that
   is what it is in the app; an uppercase tracked eyebrow here would be website
   vocabulary pretending to be product vocabulary. */
.emit__where {
  padding-bottom: 8px;
  border-bottom: 1px solid var(--line);
  font-size: 13.5px;
  font-weight: 600;
  letter-spacing: -.005em;
  color: var(--muted);
}

/* The original wording, under the row, where the app keeps it. */
.emit__said {
  padding-top: 11px;
  border-top: 1px solid var(--line);
  font-size: 13px;
  line-height: 1.45;
  color: var(--muted);
  text-wrap: pretty;
}


/* ------------------------------------------------------------- sequence --- */

/* Paused rather than absent. Every element carries its animation from the
   first frame and is held on its opening frame — `backwards` fill is what
   makes a paused animation show one — until `.is-live` arrives. A page whose
   script never ran never gets `.js` on the root, so it never gets any of this
   and shows the assembled composition instead. */
.js .emit__said,
.js .emit__sheet,
.js .emit__app,
.js .cap__live span,
.js .cap__flare {
  animation-duration: 1ms;
  animation-fill-mode: both;
  animation-play-state: paused;
}
.js .is-live .emit__said,
.js .is-live .emit__sheet,
.js .is-live .emit__app,
.js .is-live .cap__live span,
.js .is-live .cap__flare { animation-play-state: running; }

/* The order is the argument: I said this → Speak It understood it → this is
   where it went. So the sentence leaves the phone first and alone, still in
   the transcript's own voice; the paper forms around it; the row resolves on
   top of the paper; and the destination is the last thing to appear.

   `--out` is the one easing everything here uses. It is a long decelerating
   curve with no overshoot — the movement should read as weight arriving and
   settling, never as a spring. */
.emerge { --out: cubic-bezier(.16, .84, .28, 1); }

/* 1 · the sentence lifts off the screen. It comes in blurred and small from
   the transcript line's position and resolves as it arrives, which is what
   makes it read as travelling rather than as fading in somewhere else. */
.js .emit__said {
  animation-name: said-fly;
  animation-duration: 1.15s;
  animation-timing-function: var(--out);
  animation-delay: calc(var(--i) * var(--beat) + .42s);
}
/* No Z on this one. `.emit` is transformed but not `preserve-3d`, so a
   `translateZ` on a child of it is flattened and does nothing — the depth cue
   is the scale and the blur, which is also the cheaper thing to composite on a
   phone. */
@keyframes said-fly {
  from {
    opacity: 0;
    transform: translate3d(var(--fx), var(--fy), 0) scale(.72);
    filter: blur(7px);
  }
  38%  { opacity: 1; }
  to {
    opacity: 1;
    transform: translate3d(0, 0, 0) scale(1);
    filter: blur(0);
  }
}

/* 2 · the paper forms around it, growing from the sentence's own line rather
   than from the middle of the group, so the result assembles onto the words
   instead of appearing behind them. */
.js .emit__sheet {
  animation-name: sheet-form;
  animation-duration: .74s;
  animation-timing-function: var(--out);
  animation-delay: calc(var(--i) * var(--beat) + 1.16s);
  transform-origin: 50% 88%;
}
@keyframes sheet-form {
  from { opacity: 0; scale: .96 .74; }
  to   { opacity: 1; scale: 1 1; }
}

/* 3 · the row resolves on top of it, and 4 · the destination last of all. */
.js .emit__app {
  animation-name: app-settle;
  animation-duration: .6s;
  animation-timing-function: var(--out);
  animation-delay: calc(var(--i) * var(--beat) + 1.32s);
}
@keyframes app-settle {
  from { opacity: 0; translate: 0 -7px; }
  to   { opacity: 1; translate: 0 0; }
}
.js .emit__where {
  animation: where-in .5s var(--out) both;
  animation-delay: calc(var(--i) * var(--beat) + 1.5s);
  animation-play-state: inherit;
}
@keyframes where-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* The screen's half of it: the sentence is held while it is being said and
   goes as its thought leaves. Exactly one beat each, back to back, so no two
   are ever legible at the same time. */
.js .cap__live span {
  animation-name: said-hold;
  animation-duration: var(--beat);
  animation-timing-function: linear;
  animation-delay: calc(var(--i) * var(--beat));
}
.js .cap__live span:nth-child(1) { --i: 0; }
.js .cap__live span:nth-child(2) { --i: 1; }
.js .cap__live span:nth-child(3) { --i: 2; }
@keyframes said-hold {
  0%   { opacity: 0; translate: 0 4px; }
  16%  { opacity: 1; translate: 0 0; }
  80%  { opacity: 1; translate: 0 0; }
  100% { opacity: 0; translate: 0 -6px; }
}
/* The last one does not leave. A capture screen that empties itself the moment
   the sequence finishes reads as a bug; leaving the sentence there leaves the
   phone agreeing with the slab it produced. */
.js .cap__live span:last-child { animation-name: said-stay; }
@keyframes said-stay {
  0%   { opacity: 0; translate: 0 4px; }
  16%  { opacity: 1; translate: 0 0; }
  100% { opacity: 1; translate: 0 0; }
}

/* three lifts, one per thought */
.js .cap__flare {
  animation-name: flare;
  animation-duration: calc(var(--beat) * 3 + 1.1s);
  animation-timing-function: linear;
}
/* the three lifts land on the three launches: .42s, 1.24s and 2.06s of a
   3.56s run, which is 12%, 35% and 58% of it */
@keyframes flare {
  0%, 5%    { opacity: 0; }
  12%       { opacity: 1; }
  23%, 28%  { opacity: 0; }
  35%       { opacity: 1; }
  46%, 51%  { opacity: 0; }
  58%       { opacity: 1; }
  69%, 100% { opacity: 0; }
}

/* ============================== two screens ===============================
   The app itself, at size, with one line each and nothing drawn on top. An
   earlier version rang the interesting row with a marker; the rings aged
   worse than the screenshots did, and a screen large enough to read does not
   need one. `--py` is a few pixels of drift written by stage.js, and is 0
   until it is.
   ======================================================================== */

.duo__grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: clamp(32px, 5vw, 76px);
  /* Held well inside the page. Two devices in two half-page columns stand so
     far apart on a wide screen that they stop reading as a pair. */
  max-width: 940px;
  margin-inline: auto;
  align-items: start;
}
.duo__item {
  display: grid;
  justify-items: center;
  gap: clamp(22px, 3vw, 34px);
  translate: 0 var(--py, 0px);
}
.duo__say { max-width: 34ch; text-align: center; }
.duo__name {
  font-size: clamp(24px, 3vw, 34px);
  font-weight: 600;
  letter-spacing: -.03em;
  margin-bottom: 9px;
}
.duo__note {
  font-size: 15.5px;
  line-height: 1.55;
  color: var(--muted);
  text-wrap: pretty;
}

/* Straight on rather than turned, but built from the same parts as the device
   in What happens — the same body gradient, the same polished rim, the same
   buttons on the flank. Before, this was a plain dark rounded rectangle, and
   next to the rebuilt device it was the thing on the page that looked drawn.
   No island is drawn here: these screenshots are real device captures and
   carry their own. */
.phone {
  --pw: clamp(212px, 26vw, 308px);
  position: relative;
  width: var(--pw);
  padding: 8px;
  border-radius: 46px;
  background: linear-gradient(146deg, #38383a 0%, #17171a 42%, #0a0a0c 100%);
  box-shadow:
    0 1px 3px rgb(var(--ink-rgb) / .14),
    0 18px 30px -14px rgb(var(--ink-rgb) / .18),
    0 52px 80px -30px rgb(var(--ink-rgb) / .34);
}
.phone::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 46px;
  background: linear-gradient(101deg,
    rgb(255 255 255 / .26) 0 .8%,
    rgb(255 255 255 / .06) 2.4%,
    rgb(255 255 255 / 0) 11% 88%,
    rgb(255 255 255 / .05) 97%,
    rgb(255 255 255 / .18) 100%);
  pointer-events: none;
  z-index: 1;
}
.phone__btn {
  position: absolute;
  width: 2.5px;
  border-radius: 2px 0 0 2px;
  background: linear-gradient(to right, #101013, #2e2e32);
  box-shadow: -1px 0 0 rgb(255 255 255 / .14);
}
.phone__btn--action { left: -2.5px; top: 20%;   height: 4.4%; }
.phone__btn--up     { left: -2.5px; top: 28%;   height: 7.2%; }
.phone__btn--down   { left: -2.5px; top: 37.5%; height: 7.2%; }
.phone__btn--power  { right: -2.5px; top: 30%;  height: 11%;
                      border-radius: 0 2px 2px 0;
                      background: linear-gradient(to left, #101013, #2e2e32);
                      box-shadow: 1px 0 0 rgb(255 255 255 / .10); }
.phone__screen {
  position: relative;
  aspect-ratio: 1320 / 2868;
  border-radius: 38px;
  overflow: hidden;
  background: var(--bg);
}
.phone__screen img { width: 100%; height: 100%; object-fit: cover; }

/* ------------------------------------------------------------- surfaces --- */

/* Seven does not divide into a grid, and `auto-fit` leaves the remainder
   stranded against the left edge. Flex with a fixed basis puts four on the
   first row and centres the three that are left, so the odd number reads as
   arranged rather than as a row that ran out. */
/* A real grid rather than a centred wrap. Seven items over four columns means
   the second row is short, and wrapping centred it — which left the three
   rules in row two hanging between the four in row one. Aligned columns with
   an empty last cell reads as intended; staggered rules read as an accident. */
.surfaces {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: clamp(22px, 3.2vw, 38px) clamp(20px, 2.6vw, 34px);
}
/* Under a rule, not inside a box. These were seven bordered, rounded, filled
   cards — the most template-like thing on the page, and the one section that
   contradicted both the app (whose lists have no card anywhere in them) and
   the pledge row directly below it, which was already doing it correctly.
   Same treatment as `.pledge__item` now, so the page has one idea of what an
   item in a list of facts looks like. */
.surface {
  display: grid;
  align-content: start;
  gap: 6px;
  padding-top: 18px;
  border-top: 1px solid var(--line);
}
.surface__icon {
  display: block;
  margin-bottom: 7px;
  color: var(--ink);
}
.surface__icon svg { width: 22px; height: 22px; }
.surface__name { font-size: 15.5px; font-weight: 600; letter-spacing: -.01em; }
.surface__note { font-size: 13.5px; line-height: 1.45; color: var(--muted); }

/* --------------------------------------------------------------- pledge --- */

.pledge {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  gap: clamp(28px, 5vw, 64px);
}
.pledge__item {
  display: grid;
  align-content: start;
  gap: 7px;
  padding-top: 22px;
  border-top: 1px solid var(--line);
}
.pledge__icon { margin-bottom: 8px; color: var(--ink); }
.pledge__icon svg { width: 24px; height: 24px; }
.pledge__name {
  font-size: clamp(19px, 2vw, 23px);
  font-weight: 600;
  letter-spacing: -.025em;
}
.pledge__note { font-size: 14.5px; line-height: 1.5; color: var(--muted); }

/* --------------------------------------------------------------- finale --- */

.finale {
  position: relative;
  z-index: 9;
  padding: 12vh var(--gutter) 9vh;
}
.finale__inner {
  max-width: 820px;
  margin-inline: auto;
  display: grid;
  justify-items: center;
  gap: 24px;
  text-align: center;
}
.finale__title {
  font-size: clamp(29px, 4.2vw, 50px);
  font-weight: 600;
  line-height: 1.08;
  letter-spacing: -.03em;
  max-width: 19ch;
  text-wrap: balance;
}
/* The one supporting line under the offer's statement, and the only place the
   discount is said in a sentence. Hidden with every other `[data-sale-only]`
   the moment the sale is switched off. */
.finale__lede {
  margin-top: -8px;
  font-size: clamp(16px, 1.5vw, 18.5px);
  color: var(--muted);
}

.plans {
  width: 100%;
  margin-top: 14px;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: clamp(24px, 5vw, 64px);
  text-align: left;
}
.plan {
  display: grid;
  gap: 11px;
  align-content: start;
  padding-top: 20px;
  border-top: 1px solid var(--ink);
}
.plan__name {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .18em;
  text-transform: uppercase;
}
/* The one filled badge on the page. The sale is the only thing here that is
   temporary, so it is the only thing that raises its voice. */
/* One ink pill on an otherwise monochrome page is enough to carry a discount.
   It used to be uppercase and tracked and read as a banner ad. */
.plan__tag {
  padding: 4px 10px 5px;
  border-radius: 999px;
  background: var(--ink);
  color: var(--bg);
  font-size: 11.5px;
  font-weight: 500;
  letter-spacing: -.01em;
  text-transform: none;
}
.plan__price {
  display: flex;
  align-items: baseline;
  gap: 9px;
  flex-wrap: wrap;
  font-size: 32px;
  font-weight: 600;
  letter-spacing: -.03em;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}
.plan__prices { display: grid; gap: 14px; }
.plan__billing {
  flex-basis: 100%;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--muted);
}
.plan__was {
  font-size: 20px;
  font-weight: 400;
  color: var(--muted);
  text-decoration-thickness: 1.5px;
}
.plan__period { font-size: 15px; font-weight: 400; color: var(--muted); }
.plan__note { font-size: 12.5px; line-height: 1.5; color: var(--muted); }
.plan__list {
  display: grid;
  gap: 7px;
  font-size: 14px;
  line-height: 1.5;
  color: var(--muted);
}

.referral-offer {
  display: grid;
  gap: 10px;
  max-width: 680px;
  margin: 34px auto 0;
  padding: 22px;
  border: 1px solid var(--line);
  border-radius: 22px;
  background: var(--surface);
  text-align: left;
}
.referral-offer h3 {
  font-size: clamp(25px, 4vw, 40px);
  line-height: 1.05;
  letter-spacing: -.04em;
}
.referral-offer > p:not(.eyebrow) {
  max-width: 58ch;
  color: var(--muted);
  line-height: 1.55;
}

.finale .get { margin-top: 26px; }

.finale__foot {
  margin-top: 22px;
  max-width: 46ch;
  font-size: 12px;
  line-height: 1.6;
  color: var(--muted);
}

/* --------------------------------------------------------------- footer --- */

.footer {
  position: relative;
  z-index: 9;
  max-width: var(--page);
  margin-inline: auto;
  padding: 38px var(--gutter) calc(44px + env(safe-area-inset-bottom));
  display: grid;
  justify-items: center;
  gap: 17px;
  border-top: 1px solid var(--line);
  text-align: center;
}
.footer__links {
  display: flex;
  gap: 22px;
  flex-wrap: wrap;
  justify-content: center;
  font-size: 13px;
  color: var(--muted);
}
.footer__legal { font-size: 11px; color: var(--muted); }

/* ========================== the download sheet ============================ */

/* One dimmed, blurred layer over the page with the two ways in laid side by
   side. Ink on the page's own ink rather than a white card: the reader has
   just left the page and this is the one thing in front of them, so it is
   drawn as a layer over it, not as another page. */
.dl {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  max-width: none;
  max-height: none;
  margin: 0;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--bg);
  display: none;
}
.dl[open] { display: grid; place-items: center; }
.dl::backdrop {
  background: rgb(var(--ink-rgb) / .8);
  backdrop-filter: blur(16px) saturate(120%);
  -webkit-backdrop-filter: blur(16px) saturate(120%);
}
.dl__inner {
  position: relative;
  width: min(1100px, 100%);
  padding: 40px var(--gutter);
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  justify-items: center;
  gap: clamp(28px, 6vw, 96px);
  opacity: 0;
  translate: 0 12px;
  transition: opacity .32s var(--ease), translate .32s var(--swift);
}
/* `is-in` is added a frame after showModal(), so the transition has a start
   state to run from — a style that matches on the first painted frame is the
   start state, and nothing would move. */
.dl.is-in .dl__inner { opacity: 1; translate: 0 0; }

.dl__side {
  display: grid;
  justify-items: center;
  gap: 24px;
  text-align: center;
}
.dl__label {
  font-size: 17px;
  font-weight: 500;
  letter-spacing: -.01em;
}
.dl__qr {
  width: min(300px, 62vw);
  height: auto;
  /* the SVG carries its own quiet zone; this is just the card's edge */
  padding: 10px;
  border-radius: 22px;
  background: var(--surface);
}
.dl__rule {
  width: 1px;
  height: min(420px, 60vh);
  background: rgb(var(--bg-rgb) / .28);
}
.dl__btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 56px;
  padding: 14px 34px;
  border-radius: 12px;
  background: var(--surface);
  color: var(--ink);
  font-size: 19px;
  font-weight: 500;
  letter-spacing: -.01em;
  transition: opacity .22s var(--ease), transform .22s var(--swift);
}
.dl__btn:active { transform: scale(.98); }

.dl__close {
  position: fixed;
  top: max(18px, env(safe-area-inset-top));
  right: var(--gutter);
  width: 44px;
  height: 44px;
  display: grid;
  place-items: center;
  border: 0;
  border-radius: 999px;
  background: rgb(var(--bg-rgb) / .12);
  color: var(--bg);
  cursor: pointer;
}
.dl__close svg { width: 18px; height: 18px; }
.dl:focus-visible, .dl__close:focus-visible, .dl__btn:focus-visible {
  outline-color: var(--bg);
}
/* the page must not scroll under it */
.is-dl-open { overflow: hidden; }

/* Stacked, and without the QR — a phone cannot scan itself. The rule turns
   sideways-less: it simply goes. */
@media (max-width: 700px) {
  .dl__inner { grid-template-columns: 1fr; }
  .dl__side--qr, .dl__rule { display: none; }
}
@media (hover: hover) {
  .dl__btn:hover { opacity: .88; }
  .dl__close:hover { background: rgb(var(--bg-rgb) / .22); }
}
@media (prefers-reduced-motion: reduce) {
  .dl__inner, .dl__btn { transition: none; }
}

/* ================================ reveals ================================= */

/* Gated on `.js`, like everything else the script has to undo. `.js` is added
   by `stage.js` on its first line, and `is-in` is added by the observer it sets
   up — so ungated, a page whose script 404s, is blocked, or throws showed every
   heading, both plans, the pledge and the download block at `opacity: 0`. The
   sheet only hides what a page has said it can put back. */
.js .reveal {
  opacity: 0;
  translate: 0 20px;
  filter: blur(6px);
  transition: opacity .8s var(--ease), translate .8s var(--swift), filter .8s var(--ease);
}
.js .reveal.is-in {
  opacity: 1;
  translate: 0 0;
  filter: blur(0);
}

/* A row of like things arrives as a row, one after another, rather than as
   seven items that all appeared on the same frame. The steps are short enough
   that the last of seven is still in under half a second, so on a phone —
   where they enter one at a time anyway — nobody waits on it. */
.js .surfaces .reveal:nth-child(2) { transition-delay: .07s; }
.js .surfaces .reveal:nth-child(3) { transition-delay: .14s; }
.js .surfaces .reveal:nth-child(4) { transition-delay: .21s; }
.js .surfaces .reveal:nth-child(5) { transition-delay: .28s; }
.js .surfaces .reveal:nth-child(6) { transition-delay: .35s; }
.js .surfaces .reveal:nth-child(7) { transition-delay: .42s; }
.js .pledge .reveal:nth-child(2),
.js .plans  .reveal:nth-child(2) { transition-delay: .1s; }
.js .pledge .reveal:nth-child(3) { transition-delay: .2s; }

/* Section titles settle a word at a time. stage.js wraps each word of a
   `[data-words]` heading in a span and numbers it in `--w`; each word carries
   the same blur-and-lift the block does, a beat later than the one before it,
   so a title reads in the order it is written rather than arriving whole. The
   spans are inline-block only so that translate and filter apply — the words
   still wrap and balance as one line of text. */
.js .reveal .w {
  display: inline-block;
  opacity: 0;
  translate: 0 .3em;
  filter: blur(5px);
  transition: opacity .7s var(--ease), translate .7s var(--swift), filter .7s var(--ease);
  transition-delay: calc(.06s * var(--w, 0));
}
.js .reveal.is-in .w {
  opacity: 1;
  translate: 0 0;
  filter: blur(0);
}

/* ================================ pointer ================================= */

/* Every hover state on the page lives here, behind a pointer that can actually
   hover. A touch browser resolves `:hover` on tap and then leaves it resolved
   until something else is tapped, so an unguarded hover reads on iPhone as a
   card that stayed lifted, or a chip that stayed dark, after you scrolled on.
   The `:active` states stay where their components are: those are the press
   feedback, and they are right on both. */
@media (hover: hover) {
  .topbar__cta:hover { opacity: .84; }
  .appstore:hover    { opacity: .86; }
  .btn--solid:hover  { opacity: .86; }
  .ghost:hover       { background: rgb(var(--ink-rgb) / .1); }
  .chip:hover        { color: var(--ink); border-color: var(--ink); }
  .mic:hover:not([disabled]) { transform: scale(1.02); }
  .footer__links a:hover { color: var(--ink); }
}

/* A finger is not a cursor. Everything that can be tapped is brought up to
   Apple's 44pt minimum, and the tap flash is dropped because every control here
   already answers a press by scaling — the grey rectangle is a second, louder
   answer to the same touch. Keyed on the pointer rather than the width, so a
   touch laptop gets it and a narrow desktop window does not. */
@media (pointer: coarse) {
  a, button, input, .chip, .ghost { -webkit-tap-highlight-color: transparent; }

  .topbar__cta { min-height: 44px; display: inline-flex; align-items: center; }
  .wordmark    { padding-block: 11px; margin-block: -11px; }
  .ghost       { min-height: 44px; }
  .chip        { min-height: 44px; padding-inline: 17px; }
  .btn         { min-height: 44px; }
  .appstore    { min-height: 48px; }

  /* iOS zooms the page in when a focused field is under 16px, and then leaves
     the page zoomed — the section the reader was in is off to one side when
     they look up. 16px is the whole fix. */
  .demo__input { font-size: 16px; min-height: 44px; }

  /* The links are set in a row of small type, so they are given their target in
     padding and the row is closed back up with a negative margin — the footer
     keeps its spacing and the taps stop landing between two of them. */
  .footer__links { gap: 8px 14px; margin-block: -12px; }
  .footer__links a { padding: 12px 8px; }
}

/* ============================ short viewports ============================= */

/* The hero stacks a headline, an indicator, a standfirst and the download
   block in one screen. Under about 800px of height it gives up the QR first
   and then some of the indicator, rather than letting anything crowd. */
@media (max-height: 800px) and (min-width: 901px) {
  :root { --u: min(206px, 26vh); }
  .hero__title { font-size: clamp(34px, 5vw, 58px); }
  .opening__eyebrow { top: max(7vh, calc(var(--bar) + 8px)); }
  .hero__foot { margin-top: 20px; gap: 13px; }
  .hero__slot { min-height: 80px; }
  /* Same correction the landscape tier makes, and for the same reason: once the
     bar and the eyebrow are a real share of the height, centring the stack in
     the whole frame puts the headline behind them. */
  .hero { translate: 0 calc((var(--bar) + 34px) / 2); }
}
@media (max-height: 680px) and (min-width: 901px) {
  .hero__foot .qr { display: none; }
}

/* ============================== narrow screens ============================ */

@media (max-width: 900px) {
  :root { --u: min(128px, 19vh); }

  /* The halo stays. It used to be dropped here, on the grounds that the cloud
     never came near the indicator on a phone — but the cloud is drawn close in
     now, so a thought does pass the indicator, and the disc of paper is what
     dissolves it as it goes by. It reaches barely past the rings, so it is only
     ever the indicator it protects; the headline is well outside it. */

  .opening { height: 132svh; }
  .opening__eyebrow { top: max(7.5vh, calc(var(--bar) + 8px)); }
  /* The long ones wrap here. A phone held upright is narrower than the
     sentences on it, and a thought kept to one line has to be either half off
     the screen or sitting on the indicator — there is no radius that avoids
     both. Two lines cost nothing: the cards further down set speech the same
     way, so the page keeps one idea of what something somebody said looks
     like, and the cloud can come in close and still be read end to end. Sideways there is
     width to spare, and that tier keeps them on one line. */
  .bubble span {
    font-size: 12px;
    padding: 7px 12px 8px;
    border-radius: 14px;
    white-space: normal;
    /* Tuned against both ends at once: wider and the longest thought pushes the
       cloud out past the screen again, narrower and short thoughts start
       breaking that had no need to. At 36vw three of the seven take a second
       line and the widest ends 2px past the edge. */
    max-width: 36vw;
  }
  .bubble span::after { width: 8px; height: 8px; bottom: -4.5px; left: 15px; }
  .bubble--r span::after { left: auto; right: 15px; }
  /* half as many, so what is left is still evenly spaced around the ring and a
     375px screen is not wall-to-wall speech */
  .bubble:nth-child(even) { display: none; }

  /* the lines are short here and the cloud is close, so the paper behind each
     one has to reach further past it */
  .hero__line::before { width: calc(100% + 34vw); height: 300%; }
  .hero__foot::before { width: calc(100% + 40vw); }

  .hero__title { font-size: clamp(30px, 8.4vw, 44px); }
  .hero__foot { margin-top: 22px; gap: 14px; }
  .hero__sub { font-size: 15px; }
  /* only the badge lands here — the QR is pointless on the phone that would be
     scanning it */
  .hero__slot { min-height: 76px; }
  .hero__foot .qr { display: none; }
  /* The hint is the only thing that says the opening is a sequence rather than
     a picture, and a phone has no scrollbar to say it instead — so it stays,
     with a shorter rule to suit the height it has. */
  .hint { gap: 8px; font-size: 9.5px; }
  .hint__rule { height: 22px; }

  .demo__form .btn { width: 100%; }
  .verdict { grid-template-columns: 1fr; gap: 22px; padding: 20px; }

  /* The same room, at a phone's size. An earlier version turned the scene
     into a column here — device on top, three cards falling out of the bottom
     of it — on the grounds that a 390pt room has no depth to stand cards in.
     It was legible and it was dull: the whole point of this section is that
     three thoughts physically leave the screen they were spoken into, and a
     stack of cards under a phone is a list with a picture above it.

     So the room stays and the room turns upright. The device moves to the left
     wall and shrinks to a third of the width, the cards keep their descending
     zigzag and lap its right edge exactly as they do on a desktop, and the
     depth is real but shallow — a phone has no room for a 150px `translateZ`
     and does not need one to say which card is nearest. */
  .scene {
    aspect-ratio: 350 / 620;
    perspective: 900px;
    perspective-origin: 22% 46%;
  }
  /* Inside the gutter rather than a share of the width: on a phone a
     percentage that reads as a soft bleed on a desktop is wider than the
     margin the page has, and body's `overflow-x` would be the only thing
     stopping it. */
  .scene__ground {
    left: -10px; right: 40%; top: 12%; bottom: 6%;
    transform: translateZ(-70px);
    background: radial-gradient(closest-side circle at 46% 50%,
      rgb(var(--ink-rgb) / .055) 0%,
      rgb(var(--ink-rgb) / .038) 30%,
      rgb(var(--ink-rgb) / .020) 52%,
      rgb(var(--ink-rgb) / .007) 74%,
      rgb(var(--ink-rgb) / 0) 100%);
  }

  .tilt {
    left: -1%;
    top: 46%;
    width: 31%;
    transform: translate3d(0, -50%, 0) rotateY(-15deg) rotateX(4deg) rotateZ(1deg);
  }

  /* `relative` is not on the table here — the cards are absolute again, which
     is also what keeps `.emit__sheet` (`position: absolute; inset: 0`) sized to
     its own card rather than snapping out to fill `.scene`. */
  .emit {
    max-width: none;
    --face: -5deg;
    --pad-x: 15px;
    --pad-y: 13px;
    gap: 9px;
  }
  /* Wide enough that a row still reads as the app's row: 76% of a 350px scene
     is 266px, and the three columns inside it survive at these sizes. The
     device is only 31% wide, so a card starting at 22% laps it by a third —
     the same overlap the desktop composition has, at a third of the size.

     The cards lap the device but not each other, and that is the one thing
     that had to change from the desktop layout. There a card passing in front
     of another is depth, because they are 41% wide and offset sideways — the
     overlap lands on a corner. Here they are 76% wide and stacked, so the same
     overlap lands on a *sentence*: at 375pt the quote on B wraps to two lines
     and C came down over the second one. The rows are spaced for the tallest
     each one gets, and the depth is carried by the turn, the shadow and `--z`
     instead. */
  .emit--a { --x: 22%; --y:  0%; --w: 76%; --z:  32px; --fx: -56cqw; --fy:  60cqw; }
  .emit--b { --x: 26%; --y: 34%; --w: 72%; --z: -14px; --fx: -68cqw; --fy:  -2cqw; }
  .emit--c { --x: 22%; --y: 68%; --w: 76%; --z:  18px; --fx: -56cqw; --fy: -60cqw; }

  .emit__sheet { border-radius: 16px; }
  .emit__app { gap: 7px; }
  .emit__where { padding-bottom: 6px; font-size: 11.5px; }
  .emit__said { padding-top: 8px; font-size: 12px; }
  .approw__row { gap: 9px; }
  .approw__check { width: 17px; height: 17px; border-width: 1.4px; }
  .approw__mark  { width: 19px; height: 19px; }
  .approw__mark svg { width: 10px; height: 10px; }
  .approw__title { font-size: 13.5px; }
  .approw__meta, .approw__time { font-size: 11px; }
  .approw__pin { width: 11px; height: 11px; }

  /* The time cannot hold a column of its own at this width — "Aug 19, 5:00 PM"
     beside a title inside 266px leaves the title three words to a line. It goes
     under, left-aligned, which is what the app's own row does when it runs out
     of width. */
  .emit .approw__row { grid-template-columns: auto minmax(0, 1fr); }
  .emit .approw__time { grid-column: 2; grid-row: 2; justify-self: start; }

  .duo__grid { grid-template-columns: 1fr; gap: clamp(46px, 9vh, 76px); }
  .phone { --pw: min(238px, 58vw); }

  .surfaces { grid-template-columns: repeat(2, 1fr); gap: 22px 18px; }

  .finale { padding: 10vh var(--gutter) 6vh; }
  .finale__inner { justify-items: start; text-align: left; }
  .finale__title { max-width: none; }
  .plans { grid-template-columns: 1fr; gap: 28px; }
  .finale .get { justify-content: flex-start; }
}

/* Seven of anything is a lot on a phone. Two columns at this width put
   "One press, iPhone 15 Pro and later" on three lines each, and one column of
   the desktop item — icon over name over note, with air around all three — ran
   the section to most of a thousand pixels of scrolling for a list the reader
   is meant to skim.

   So the item turns on its side: the icon moves beside the words instead of
   above them, and each way in becomes one compact row under its hairline. Same
   seven, same notes, a third of the height. */
@media (max-width: 560px) {
  .surfaces { grid-template-columns: 1fr; gap: 0; }

  .surface {
    grid-template-columns: 24px minmax(0, 1fr);
    column-gap: 14px;
    row-gap: 1px;
    align-items: center;
    padding-block: 13px;
  }
  .surface__icon {
    grid-column: 1;
    grid-row: 1 / span 2;
    align-self: center;
    margin-bottom: 0;
  }
  .surface__icon svg { width: 20px; height: 20px; }
  .surface__name { grid-column: 2; grid-row: 1; font-size: 15px; }
  .surface__note { grid-column: 2; grid-row: 2; font-size: 13px; }
}

@media (max-width: 480px) {
  .finale .qr { display: none; }

  /* Below this the time column squeezes the demo's title into a column of
     single words, so that row does what the app's own row does and puts the
     time on its own line. Scoped to the demo: the cards in "what happens"
     carry titles short enough to stay beside their time, and breaking them
     costs the row its shape for no gain. */
  .verdict .approw__row { grid-template-columns: auto minmax(0, 1fr); }
  .verdict .approw__time { grid-column: 2; grid-row: 2; justify-self: start; }
}

/* ============================== in between =============================== */

/* A tablet standing up, and a desktop window dragged narrow. Both come through
   the 900px tier above, which is written for a phone — so on a 768pt iPad the
   page arrives correct but under-scaled, an indicator and a device drawn for a
   screen half the size. The stacked arrangement is right here; only the sizes
   are wrong, so only the sizes are restated. Held off landscape phones, which
   share these widths and need the opposite, by asking for the height. */
@media (min-width: 620px) and (max-width: 900px) and (min-height: 600px) {
  :root { --u: min(196px, 23vh); }

  .hero__title { font-size: clamp(40px, 6.6vw, 60px); }
  .hero__sub { font-size: 17px; max-width: 40ch; }
  .hero__slot { min-height: 88px; }

  .surfaces { grid-template-columns: repeat(3, 1fr); }

  /* The column arrangement is right on a tablet; only its sizes are drawn for
     a phone, so only the sizes are restated. */
  .tilt { width: min(262px, 36vw); }
  .emit { max-width: 500px; }
  .emit__said { font-size: 14px; }
  .duo__grid { grid-template-columns: 1fr 1fr; gap: clamp(26px, 4vw, 44px); }
  .phone { --pw: min(216px, 27vw, 23vh); }
  .duo__note { font-size: 14.5px; }

  /* wide enough for the two plans to stand side by side again, which is the
     comparison the section is for */
  .finale__inner { justify-items: center; text-align: center; }
  .finale__title { max-width: 19ch; }
  .plans { grid-template-columns: 1fr 1fr; gap: clamp(28px, 5vw, 56px); }
  .finale .get { justify-content: center; }
}

/* ============================ a phone on its side ========================= */

/* Every iPhone turned sideways, and nothing else. Landscape is where the two
   halves of the page pull in opposite directions: the widths run from 667 to
   956 — straddling the 900px line the rest of the sheet is organised around —
   while the height collapses to somewhere between 375 and 440. Asking for the
   height alone catches all of them and no phone standing up, whose shortest
   screen is still 667. It is last so that it wins over the width tiers above
   for the widths they overlap on.

   The whole job is the opening: a headline, the indicator, a standfirst and the
   download block have to share about 280pt of height once the bar and the
   eyebrow have taken theirs. Everything below scrolls and only needs to not be
   absurd. */
@media (max-height: 560px) {
  :root { --u: min(78px, 20vh); }

  .topbar { padding-block: 9px; }

  .orb__halo { display: none; }

  /* the same reduction the narrow tier makes, applied by height so that the
     three landscape widths above 900px do not keep the desktop cloud */
  .bubble span { font-size: 12px; padding: 7px 12px 8px; border-radius: 14px; }
  .bubble span::after { width: 8px; height: 8px; bottom: -4.5px; left: 15px; }
  .bubble--r span::after { left: auto; right: 15px; }
  .bubble:nth-child(even) { display: none; }

  /* Clear of the bar rather than a fraction of a height that no longer has the
     room for one — at 390pt, 7.5vh put the eyebrow inside the bar and the
     headline underneath both. */
  .opening__eyebrow { top: calc(var(--bar) + 8px); }

  /* The stack is centred in the frame, and at this height the frame's top is
     already spoken for by the bar and the eyebrow — so centred meant the
     headline started above them both. It is moved down by half the band they
     occupy, which centres it in what is actually left. Half, because the frame
     centres it: shifting the stack by x moves its top edge by x.

     Done on the stack rather than as padding on the frame, because the frame's
     padding box is what the cloud and the two soft edges are laid out in, and
     they still want the whole screen. The indicator follows on its own — it is
     placed by measuring the gap, which moves with this. */
  .hero { translate: 0 calc((var(--bar) + 36px) / 2); }

  .hero__title { font-size: clamp(24px, 6.8vh, 38px); }
  .hero__foot { margin-top: 10px; gap: 8px; }
  .hero__sub { font-size: 13.5px; line-height: 1.5; max-width: 48ch; }
  .hero__slot { min-height: 48px; }
  /* nothing to spare for a cue, and a screen this shape is obviously scrollable */
  .hint { display: none; }
  .get .qr { display: none; }

  .section { padding-block: clamp(48px, 14vh, 90px); }
  .section__head { margin-bottom: clamp(28px, 8vh, 48px); }
  .mic { --u: clamp(112px, 30vh, 150px); }
  .demo__live { min-height: 2.6em; }

  /* Beside the text again. Stacking the phone above the steps costs more height
     than this screen has; sideways there is width to spare instead, and the
     desktop arrangement is the one that fits. */
  /* Sideways there is width to spare and no height at all, so the scene goes
     back to the room — but a shallow one, with the device small enough that it
     and the three cards finish inside 440pt of screen. */
  .scene {
    aspect-ratio: 1180 / 620;
    perspective: 1500px;
    perspective-origin: 20% 48%;
    display: block;
    gap: 0;
  }
  .scene__ground {
    left: -4%; right: -4%; top: 4%; bottom: -8%;
    background: radial-gradient(56% 52% at 26% 52%,
      rgb(var(--ink-rgb) / .06) 0%, rgb(var(--ink-rgb) / 0) 72%);
  }
  .tilt {
    position: absolute;
    left: 1%;
    top: 50%;
    width: 22%;
    margin-bottom: 0;
    transform: translate3d(0, -50%, 0) rotateY(-17deg) rotateX(5deg) rotateZ(1.1deg);
  }
  .emit {
    position: absolute;
    left: var(--x);
    top: var(--y);
    width: var(--w);
    max-width: none;
    /* less turn, because the perspective throws the far corner of a card
       further out here than there is room for at the right-hand edge */
    --face: -4deg;
    transform: translate3d(0, 0, var(--z)) rotateY(var(--face));
  }
  /* The columns do not overlap here. On a desktop a card passing in front of
     another is depth; at this size it is a sentence with its first three words
     behind something else. */
  .emit--a { --x: 27%; --y:  0%; --w: 34%; --z:  46px; --fx: -58cqw; --fy:  24cqw; }
  .emit--b { --x: 62%; --y: 30%; --w: 35%; --z: -14px; --fx: -84cqw; --fy:   1cqw; }
  .emit--c { --x: 27%; --y: 56%; --w: 34%; --z:  20px; --fx: -60cqw; --fy: -23cqw; }
  .emit { --pad-x: 14px; --pad-y: 11px; gap: 8px; }
  .emit__sheet { border-radius: 15px; }
  .emit__app { gap: 7px; }
  .emit__where { padding-bottom: 6px; font-size: 11.5px; }
  .emit__said { padding-top: 8px; font-size: 11.5px; }
  .approw__row { gap: 9px; }
  .approw__check { width: 16px; height: 16px; border-width: 1.4px; }
  .approw__mark  { width: 18px; height: 18px; }
  .approw__mark svg { width: 9px; height: 9px; }
  .approw__title { font-size: 13px; }
  .approw__meta, .approw__time { font-size: 11px; }
  .approw__pin { width: 10px; height: 10px; }
  /* Sideways the cards are wide again, so the time goes back beside the title
     rather than under it — the narrow tier stacks it, and this tier shares those
     widths. */
  .emit .approw__row { grid-template-columns: auto minmax(0, 1fr) auto; }
  .emit .approw__time { grid-column: 3; grid-row: 1; justify-self: end; }

  .duo__grid { grid-template-columns: 1fr 1fr; gap: clamp(24px, 5vw, 60px); }
  .phone { --pw: min(150px, 31vh); }

  .finale { padding: 8vh var(--gutter) 6vh; }
  .footer { padding-block: 28px calc(30px + env(safe-area-inset-bottom)); }
}

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

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }

  /* No scatter and no collapse: the opening becomes a plain hero already in
     its finished state, with the download block present rather than arriving.
     stage.js sets the matching values. */
  .opening { height: 100svh; }
  .field { display: none; }
  .hint { display: none; }

  .orb__wave rect { animation: none; transform: scaleY(.72); }
  .orb__ring, .orb__wave, .btn, .ghost, .chip, .mic, .stage,
  .appstore, .topbar__cta { transition: none; }
  .verdict.is-new { animation: none; }

  /* What happens: the composition, already assembled. Every element's resting
     CSS is its finished state, so cancelling the animations is the whole of
     it — the phone, the three sentences and the three cards are all still
     there, and the section explains the product exactly as well standing
     still as it does arriving. The one thing that has to be said explicitly is
     which sentence the screen is left showing, and it is the first. */
  .emit__said, .emit__sheet, .emit__app, .emit__where, .cap__flare { animation: none; }
  .cap__live span { animation: none; opacity: 0; }
  .cap__live span:last-child { opacity: 1; }
  /* the two devices sit where they are laid out; no drift */
  .duo__item { translate: none; }

  .js .reveal,
  .js .reveal .w {
    opacity: 1;
    translate: none;
    filter: none;
    transition: none;
  }
}
