/* --------------------------------------------------------------------------
   hidewire.org

   Colors come from mobile/src/theme.ts in the app repo. Do not add any here.

   Type: Space Grotesk carries the page, 400 for body and 700 for headings.
   IBM Plex Mono is kept for one job, the letterspaced caps label, which is the
   most recognizable part of the app's look. An earlier version set everything
   in mono to match theme.ts and it was hard to read at paragraph length, which
   is the whole reason a display face and a text face are different things.

   Layout is landscape first: a two column hero, then a scroller where one
   phone stays put and the copy moves past it.
   -------------------------------------------------------------------------- */

@font-face {
  font-family: 'Space Grotesk';
  src: url('/fonts/space-grotesk-400.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'Space Grotesk';
  src: url('/fonts/space-grotesk-500.woff2') format('woff2');
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'Space Grotesk';
  src: url('/fonts/space-grotesk-700.woff2') format('woff2');
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'IBM Plex Mono';
  src: url('/fonts/ibm-plex-mono-600.woff2') format('woff2');
  font-weight: 600;
  font-style: normal;
  font-display: swap;
}

:root {
  --bg: #0A0A0C;
  --surface: #121215;
  --surface2: #1A1A1F;
  --line: #232329;
  --line-bright: #2E2E36;
  --text: #F4F4F2;
  --dim: #9A9AA3;
  --faint: #5C5C66;
  --accent: #C8FF2E;
  --accent-dim: #5E7A0E;
  --danger: #FF4438;
  --warn: #FFB020;
  --on-accent: #0A0A0C;

  --sans: 'Space Grotesk', ui-sans-serif, system-ui, -apple-system, 'Helvetica Neue', sans-serif;
  --mono: 'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;

  --gutter: clamp(1.25rem, 5vw, 4rem);
  --wide: 84rem;
  --phone-w: clamp(12.5rem, 20vw, 16.5rem);
  color-scheme: dark;
}

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

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

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--sans);
  font-size: 1.0625rem;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

a {
  color: var(--text);
  text-underline-offset: 0.25em;
  text-decoration-color: var(--line-bright);
}

a:hover {
  text-decoration-color: var(--accent);
}

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: 2px;
}

h1,
h2,
h3 {
  font-weight: 700;
  letter-spacing: -0.02em;
  line-height: 1.05;
  text-wrap: balance;
  margin: 0;
}

p {
  margin: 0 0 1em;
}

strong {
  font-weight: 700;
}

img {
  max-width: 100%;
  height: auto;
  display: block;
}

.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  background: var(--accent);
  color: var(--on-accent);
  padding: 0.75rem 1rem;
  z-index: 20;
}

.skip-link:focus {
  left: 0;
}

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

/* The one piece of mono left: caps, wide tracking, never long. */
.label {
  font-family: var(--mono);
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--dim);
  margin: 0;
}

.numeral {
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.03em;
  font-weight: 700;
}

/* --------------------------------------------------------------------------
   Lockup: the app mark, then the wordmark.

   The mark is brand/frame-mark.svg from the app repo with its background plate
   removed. The viewfinder brackets are already part of it, so nothing here
   draws a second set around the word.
   -------------------------------------------------------------------------- */

.lockup {
  display: inline-flex;
  align-items: center;
  gap: 0.65rem;
  text-decoration: none;
  color: var(--text);
}

.mark {
  width: 2.125rem;
  height: 2.125rem;
  flex: none;
}

.foot-brand .mark {
  width: 1.75rem;
  height: 1.75rem;
}

.wordmark {
  font-weight: 700;
  font-size: 1.0625rem;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  line-height: 1;
}

/* --------------------------------------------------------------------------
   Header and footer
   -------------------------------------------------------------------------- */

.site-head {
  position: relative;
  z-index: 12;
}

.head-inner {
  max-width: var(--wide);
  margin: 0 auto;
  padding: 1.25rem var(--gutter) 0;
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1.5rem;
  align-items: center;
  justify-content: space-between;
}

.head-status,
.head-back {
  color: var(--dim);
}

.head-back {
  font-size: 0.875rem;
}

.site-foot {
  position: relative;
  z-index: 12;
  border-top: 1px solid var(--line);
  background: var(--surface);
}

.foot-inner {
  max-width: var(--wide);
  margin: 0 auto;
  padding: 1.75rem var(--gutter) 2.25rem;
  display: flex;
  flex-wrap: wrap;
  gap: 1rem 2rem;
  align-items: center;
  justify-content: space-between;
}

.foot-brand {
  margin: 0;
}

.foot-links {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1.75rem;
  font-size: 0.9375rem;
}

.foot-links a {
  color: var(--dim);
}

.foot-links a:hover {
  color: var(--text);
}

/* --------------------------------------------------------------------------
   Hero
   -------------------------------------------------------------------------- */

.hero {
  position: relative;
  max-width: var(--wide);
  margin: 0 auto;
  padding: clamp(2rem, 5vw, 3rem) var(--gutter) clamp(3rem, 6vw, 4rem);
  display: grid;
  gap: clamp(2.5rem, 5vw, 4rem);
  align-items: center;
}

@media (min-width: 64rem) {
  .hero {
    grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr);
    min-height: min(44rem, calc(100svh - 5.5rem));
    padding-top: 0;
  }
}

h1 {
  font-size: clamp(2.5rem, 6.2vw, 4.75rem);
  letter-spacing: -0.04em;
  max-width: 12ch;
  margin-bottom: 1rem;
}

.lede {
  font-size: clamp(1.0625rem, 1.6vw, 1.3125rem);
  color: var(--dim);
  max-width: 32ch;
  margin-bottom: 2.25rem;
}

.hero-art {
  position: relative;
  display: flex;
  justify-content: center;
  align-items: center;
  /* 844/390 is the screenshot ratio, plus the bezel and the rotation. The
     phones are positioned out of the flow, so without this the section has no
     height of its own and they spill over the copy above. */
  min-height: calc(var(--phone-w) * 2.5);
}

.hero-art .phone {
  position: absolute;
}

/* Depth by brightness rather than opacity: a translucent screenshot on a black
   page is just a darker black. */
.phone-back {
  transform: translate(-34%, -7%) rotate(-7deg) scale(0.9);
  filter: brightness(0.8) saturate(0.95);
}

.phone-front {
  transform: translate(12%, 5%) rotate(3deg);
  z-index: 2;
}

@media (max-width: 63.999rem) {
  .phone-back {
    transform: translate(-26%, -6%) rotate(-8deg) scale(0.85);
  }

  .phone-front {
    transform: translate(14%, 5%) rotate(4deg);
  }
}

.scroll-cue {
  position: absolute;
  left: 50%;
  bottom: 1rem;
  transform: translateX(-50%);
  color: var(--faint);
}

@media (max-width: 63.999rem) {
  .scroll-cue {
    display: none;
  }
}

/* --------------------------------------------------------------------------
   Signup
   -------------------------------------------------------------------------- */

.signup {
  max-width: 34rem;
}

.form {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
  align-items: flex-end;
  margin: 0;
}

.field {
  flex: 1 1 15rem;
  min-width: 0;
}

.field .label {
  display: block;
  margin-bottom: 0.6rem;
  color: var(--text);
}

input[type='email'],
input[type='text'] {
  width: 100%;
  font: inherit;
  font-size: 1.0625rem;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--line-bright);
  border-radius: 8px;
  padding: 0.8rem 0.95rem;
}

input::placeholder {
  color: var(--faint);
}

input[type='email']:focus-visible {
  border-color: var(--accent);
}

.btn {
  font-family: var(--mono);
  font-weight: 600;
  font-size: 0.8125rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  background: var(--accent);
  color: var(--on-accent);
  border: 1px solid var(--accent);
  border-radius: 8px;
  padding: 0.95rem 1.75rem;
  cursor: pointer;
  transition: background 0.15s ease, border-color 0.15s ease, transform 0.15s ease;
}

.btn:hover {
  background: var(--text);
  border-color: var(--text);
  transform: translateY(-1px);
}

.btn[disabled] {
  background: var(--accent-dim);
  border-color: var(--accent-dim);
  color: var(--bg);
  cursor: progress;
  transform: none;
}

.btn-ghost {
  display: inline-block;
  background: transparent;
  color: var(--text);
  border-color: var(--line-bright);
  text-decoration: none;
}

.btn-ghost:hover {
  background: transparent;
  border-color: var(--accent);
  color: var(--text);
}

/* Off-screen rather than display:none, because some bots skip fields they can
   tell are hidden. Never focusable, never announced. */
.trap {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

.status {
  font-size: 0.9375rem;
  margin: 0.875rem 0 0;
  min-height: 1.4em;
}

.status:empty {
  margin: 0;
  min-height: 0;
}

.status[data-state='ok'] {
  color: var(--accent);
}

.status[data-state='known'] {
  color: var(--warn);
}

.status[data-state='error'] {
  color: var(--danger);
}

.reward {
  position: relative;
  margin: 1.75rem 0 0;
  padding-left: 1.5rem;
  max-width: 46ch;
  font-size: 0.9375rem;
  color: var(--dim);
  text-wrap: pretty;
}

.reward strong {
  color: var(--text);
}

.reward-mark {
  position: absolute;
  left: 0;
  top: 0.5em;
  width: 8px;
  height: 8px;
  background: var(--accent);
  transform: rotate(45deg);
}

/* --------------------------------------------------------------------------
   The phone
   -------------------------------------------------------------------------- */

.phone {
  position: relative;
  margin: 0;
  width: var(--phone-w);
  padding: 7px;
  border-radius: 32px;
  border: 1px solid var(--line-bright);
  background: linear-gradient(160deg, var(--surface2), var(--surface) 60%);
  box-shadow:
    0 1px 0 rgb(255 255 255 / 0.05) inset,
    0 34px 70px -30px rgb(0 0 0 / 0.95);
}

.phone img {
  width: 100%;
  aspect-ratio: 390 / 844;
  object-fit: cover;
  object-position: top center;
  border-radius: 25px;
  border: 1px solid var(--line);
  background: var(--bg);
}

/* --------------------------------------------------------------------------
   The scroller.

   The phone sits in a sticky stage and stays put; the copy panels scroll past
   it and swap the screen underneath. Without JavaScript the stage still
   sticks, the first screen stays up, and all four panels read in order.
   -------------------------------------------------------------------------- */

.scroller {
  position: relative;
  display: grid;
}

.stage,
.panels {
  grid-area: 1 / 1;
}

.stage {
  position: sticky;
  top: 0;
  height: 100svh;
  display: grid;
  place-items: center;
  z-index: 1;
}

/* One wash of accent behind the phone, so it sits on something. */
.stage::before {
  content: '';
  position: absolute;
  inset: 15% 20%;
  background: radial-gradient(50% 40% at 50% 50%, rgb(200 255 46 / 0.1), transparent 70%);
  pointer-events: none;
}

.stage-inner {
  position: relative;
  z-index: 1;
  display: grid;
  justify-items: center;
  gap: 1.25rem;
  transition:
    translate 1.2s cubic-bezier(0.2, 0.7, 0.2, 1),
    scale 1.2s cubic-bezier(0.2, 0.7, 0.2, 1),
    rotate 1.2s cubic-bezier(0.2, 0.7, 0.2, 1);
}

/* The device does not sit in the same spot for six screens. It leans away from
   whichever side the copy is on, which is also what opens the gap up further
   on the side that has words in it. Desktop only: on a narrow screen the copy
   is underneath, so there is nothing to lean away from. */
@media (min-width: 64rem) {
  .stage[data-step='1'] .stage-inner {
    translate: -7% 0;
    rotate: -2deg;
  }

  .stage[data-step='2'] .stage-inner {
    translate: 5% 0;
    scale: 1.06;
  }

  .stage[data-step='3'] .stage-inner {
    translate: -6% 0;
    rotate: 2deg;
  }

  .stage[data-step='4'] .stage-inner {
    translate: 7% 0;
    scale: 0.94;
  }

  .stage[data-step='5'] .stage-inner {
    translate: -5% 0;
    scale: 1.02;
  }
}

/* --------------------------------------------------------------------------
   Ambient layer.

   Cosmetic, behind the phone, and borrowed from the app rather than invented:
   the dashed play zone ring, the diamonds it marks landmarks with, and the
   reveal ping the seeker gets on a timer. Nothing here is above 12 percent
   opacity, because the accent is supposed to mean "this is the thing to act
   on" and there is a live button on the same page.
   -------------------------------------------------------------------------- */

.ambient {
  position: absolute;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
}

.ring,
.ping {
  position: absolute;
  top: 50%;
  left: 50%;
  border-radius: 50%;
  translate: -50% -50%;
}

.ring {
  border: 1px dashed rgb(200 255 46 / 0.11);
  animation: turn 120s linear infinite;
}

.ring-near {
  width: calc(var(--phone-w) * 2.3);
  height: calc(var(--phone-w) * 2.3);
}

/* Counter-rotating, so the two dash patterns never lock into a moire. */
.ring-far {
  width: calc(var(--phone-w) * 3.6);
  height: calc(var(--phone-w) * 3.6);
  border-color: rgb(200 255 46 / 0.06);
  animation-duration: 190s;
  animation-direction: reverse;
}

@keyframes turn {
  to {
    rotate: 360deg;
  }
}

/* One expanding circle per step, fired from motion.js. This is the reveal
   tick: the seeker gets everyone's position for a few seconds, and then it is
   gone again. */
.ping {
  width: calc(var(--phone-w) * 1.6);
  height: calc(var(--phone-w) * 1.6);
  border: 1px solid var(--accent);
  opacity: 0;
}

.ping.is-firing {
  animation: ping 2.6s cubic-bezier(0.1, 0.7, 0.2, 1);
}

@keyframes ping {
  from {
    opacity: 0.34;
    scale: 0.5;
  }
  to {
    opacity: 0;
    scale: 2.3;
  }
}

/* --------------------------------------------------------------------------
   Per step behaviour.

   The rings, the diamonds and the ping are the same three objects throughout,
   but each step puts them somewhere different, so scrolling is not the same
   beat six times over. Everything below eases rather than snapping, and all of
   it is off under reduced motion with the rest of the animation.
   -------------------------------------------------------------------------- */

.ambient {
  transition: opacity 1.1s ease;
}

.ring {
  transition:
    scale 3s cubic-bezier(0.2, 0.7, 0.2, 1),
    border-color 1.2s ease;
}

.ping {
  transition: border-color 0.3s ease;
}

/* 02, the drop: everyone scatters, so the landmarks do. */
.stage[data-step='1'] .mote {
  animation-duration: calc(var(--t) * 0.4);
}

/* 03, the check-in: the ping goes red and the world quiets down, because this
   is the step where the screen behind it says BLACKED OUT. */
.stage[data-step='2'] .ping {
  border-color: var(--danger);
}

.stage[data-step='2'] .ambient {
  opacity: 0.45;
}

/* 04, the hunt: the zone contracts, exactly as it does late in a round. */
.stage[data-step='3'] .ring-near {
  scale: 0.68;
}

.stage[data-step='3'] .ring-far {
  scale: 0.86;
}

/* 05, around you: the landmarks are the subject, so they come forward. */
.stage[data-step='4'] .mote {
  scale: calc(var(--s) * 1.7);
}

/* 06, the ladder: everything settles and brightens a little. */
.stage[data-step='5'] .ring {
  border-color: rgb(200 255 46 / 0.17);
}

/* --------------------------------------------------------------------------
   One signature effect per step.

   The rings and diamonds are the constant; these are what make each step look
   like a different place rather than the same picture with the screen swapped.
   Only one is ever visible, and they cross-fade with the step.
   -------------------------------------------------------------------------- */

.fx {
  position: absolute;
  inset: 0;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.8s ease;
}

.stage[data-step='1'] .fx-scatter,
.stage[data-step='2'] .fx-scan,
.stage[data-step='3'] .fx-radar,
.stage[data-step='4'] .fx-orbit,
.stage[data-step='5'] .fx-bars {
  opacity: 1;
}


/* 02, the drop: twelve trails firing outward from the phone, staggered. */
.fx-scatter i {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 1px;
  height: 46vh;
  margin-top: -46vh;
  transform-origin: 50% 100%;
  background: linear-gradient(to top, rgb(200 255 46 / 0.3), transparent);
  animation: shoot 2.9s cubic-bezier(0.2, 0.6, 0.2, 1) infinite;
  animation-delay: calc(var(--a) / 360deg * -2.4s);
}

@keyframes shoot {
  0% {
    opacity: 0;
    transform: rotate(var(--a)) scaleY(0.08);
  }
  22% {
    opacity: 1;
  }
  100% {
    opacity: 0;
    transform: rotate(var(--a)) scaleY(1);
  }
}

/* 03, the check-in: a red band sweeping down the frame, over and over, the way
   the capture screen scans. */
.fx-scan::before,
.fx-scan::after {
  content: '';
  position: absolute;
  left: 50%;
  top: 0;
  width: calc(var(--phone-w) * 2.6);
  height: 22vh;
  margin-left: calc(var(--phone-w) * -1.3);
  background: linear-gradient(to bottom, transparent, rgb(255 68 56 / 0.16), transparent);
  animation: sweep 3.4s linear infinite;
}

.fx-scan::after {
  animation-delay: -1.7s;
  background: linear-gradient(to bottom, transparent, rgb(255 68 56 / 0.07), transparent);
}

@keyframes sweep {
  from {
    transform: translateY(-30%);
  }
  to {
    transform: translateY(430%);
  }
}

/* 04, the hunt: a radar sweep. This is the seeker looking for you. */
.fx-radar::before {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  width: calc(var(--phone-w) * 3.4);
  aspect-ratio: 1;
  margin: calc(var(--phone-w) * -1.7) 0 0 calc(var(--phone-w) * -1.7);
  border-radius: 50%;
  background: conic-gradient(from 0deg, rgb(200 255 46 / 0.2), rgb(200 255 46 / 0) 62deg);
  -webkit-mask: radial-gradient(circle, #000 34%, rgb(0 0 0 / 0.25) 74%, transparent 78%);
  mask: radial-gradient(circle, #000 34%, rgb(0 0 0 / 0.25) 74%, transparent 78%);
  animation: turn 4.6s linear infinite;
}

/* 05, around you: landmarks in orbit around the phone. */
.fx-orbit {
  animation: turn 30s linear infinite;
}

.fx-orbit i {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 9px;
  height: 9px;
  margin: -4.5px 0 0 -4.5px;
  border: 1px solid rgb(200 255 46 / 0.5);
  transform: rotate(var(--a)) translateY(calc(var(--phone-w) * -1.45)) rotate(45deg);
}

.fx-orbit i:nth-child(2n) {
  border-color: rgb(244 244 242 / 0.4);
  border-radius: 50%;
}

/* 06, the ladder: a bar chart breathing along the floor. */
.fx-bars i {
  position: absolute;
  bottom: 8%;
  left: var(--x);
  width: 2px;
  height: var(--h);
  transform-origin: 50% 100%;
  background: linear-gradient(to top, rgb(200 255 46 / 0.32), transparent);
  animation: rise 3.6s ease-in-out var(--d) infinite alternate;
}

@keyframes rise {
  from {
    transform: scaleY(0.22);
  }
  to {
    transform: scaleY(1);
  }
}

/* An invisible layer still burns frames, and five of the six are invisible at
   any moment, so only the live one runs.

   This has to sit after every `animation:` shorthand above. The shorthand
   resets play-state to running, so the same rules written earlier in the file
   are silently undone by the declarations they were meant to pause. */
.fx,
.fx i,
.fx::before,
.fx::after {
  animation-play-state: paused;
}

.stage[data-step='1'] .fx-scatter i,
.stage[data-step='2'] .fx-scan::before,
.stage[data-step='2'] .fx-scan::after,
.stage[data-step='3'] .fx-radar::before,
.stage[data-step='4'] .fx-orbit,
.stage[data-step='5'] .fx-bars i {
  animation-play-state: running;
}

/* Landmark diamonds, drifting. Same shape the map uses for a POI. */
.mote {
  position: absolute;
  transition: scale 1.4s cubic-bezier(0.2, 0.7, 0.2, 1);
  left: var(--x);
  top: var(--y);
  width: 7px;
  height: 7px;
  border: 1px solid var(--accent);
  opacity: 0;
  scale: var(--s);
  animation: drift var(--t) linear var(--d) infinite;
}

.mote:nth-child(3n) {
  border-color: var(--text);
}

.mote:nth-child(4n) {
  border-radius: 50%;
}

@keyframes drift {
  0% {
    opacity: 0;
    rotate: 45deg;
    translate: 0 26px;
  }
  18%,
  76% {
    opacity: 0.34;
  }
  100% {
    opacity: 0;
    rotate: 405deg;
    translate: 0 -70px;
  }
}

/* Every screen is stacked in one grid cell. The live one is the visible one. */
.phone-stage {
  display: grid;
  grid-template-areas: 'screen';
}

.phone-stage .screen {
  grid-area: screen;
}

.steps {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  gap: 0.4rem;
}

.steps li {
  position: relative;
  width: 1.75rem;
  height: 2px;
  background: var(--line-bright);
  overflow: hidden;
}

/* The live mark fills from the left rather than switching colour, which gives
   the step change a direction. */
.steps li::after {
  content: '';
  position: absolute;
  inset: 0;
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.5s cubic-bezier(0.2, 0.7, 0.2, 1);
}

.steps li.is-live::after {
  transform: scaleX(1);
}

.panels {
  position: relative;
  z-index: 2;
  pointer-events: none;
}

.panel {
  min-height: 100svh;
  display: grid;
  align-items: center;
  padding: 0 var(--gutter);
  max-width: var(--wide);
  margin: 0 auto;
  width: 100%;
}

.panel-copy {
  pointer-events: auto;
  /* Narrower than the column it sits in, so the outer edge takes up the slack
     and the gap beside the phone grows rather than the line length. */
  max-width: 22rem;
}

.panel-copy p + p {
  margin-top: 0.875rem;
}

/* The copy sits at one edge and the phone is centred behind it by the stage.
   Alternating which edge is what stops six identical screens in a row.

   The middle column is empty and exactly the width of the phone. That is the
   whole point of it: with a single column and a centred device, the copy
   reaches the phone at any window narrower than about 1200 and overlaps it
   below 1050, which is most laptops. Reserving the lane makes the clearance a
   property of the grid instead of something that happens to work at the width
   the page was designed on. */
@media (min-width: 64rem) {
  .panel {
    grid-template-columns: minmax(0, 1fr) var(--phone-w) minmax(0, 1fr);
    gap: clamp(3rem, 5.5vw, 6.5rem);
  }

  .panel-copy {
    grid-column: 1;
    justify-self: start;
  }

  .panel-flip .panel-copy {
    grid-column: 3;
    justify-self: end;
  }
}

.panel-n {
  margin-bottom: 0.875rem;
  color: var(--accent);
}

.panel h3 {
  font-size: clamp(1.75rem, 3.4vw, 2.75rem);
  margin-bottom: 0.875rem;
}

.panel p {
  color: var(--dim);
  margin: 0;
}

.tick {
  margin: 1.5rem 0 0;
  display: flex;
  align-items: baseline;
  gap: 0.75rem;
  flex-wrap: wrap;
}

.tick .numeral {
  font-size: clamp(2.25rem, 4vw, 3.25rem);
  color: var(--accent);
  line-height: 1;
}

.tick-note {
  color: var(--dim);
}

/* On a phone the stage pins to the top of the screen and the copy sits in the
   band underneath it. Only one block of copy is ever visible there: on a
   narrow screen the panels pass behind the phone on their way up, and a
   half-faded paragraph crossing a screenshot looks like a rendering fault
   rather than a transition. */
@media (max-width: 63.999rem) {
  /* Smaller than on desktop, deliberately. The copy has to fit in what is left
     of the screen under it, and a phone filling two thirds of an 844 point
     viewport leaves a step that reads as a collision rather than a layout.
     Copy plus a readout is roughly 430 points at these sizes. */
  :root {
    --phone-w: 10.5rem;
  }

  .stage {
    /* Tall enough for the phone and the step marks with slack. Without the
       slack the marks sit on the stage's edge and touch the first line of the
       copy underneath. */
    height: 52svh;
    align-content: center;

    /* The box is 52svh but the sticky range must be a full viewport, like
       desktop's. Sticky pins until the margin box hits the scroller's end, so
       without this the phone stays pinned for 48svh after the last copy has
       already un-stuck, and the copy climbs straight up into it and leaves
       the page frozen behind the device. With the margin both un-stick at the
       same scroll position and exit together, gap intact. */
    margin-bottom: 48svh;

    /* Above the panels, which is the opposite of the desktop order. At rest
       the copy sits fully below the stage, but in flight it passes straight
       through it, and text drawn over the screenshots reads as the page
       breaking. With the stage on top the words slide behind the device
       instead, which reads as intended. The ambient layers ride along, and
       may: they are confined to the stage box, under 12 percent opacity, and
       over the copy only for the moment it is passing. */
    z-index: 3;
  }

  .stage-inner {
    gap: 0.875rem;
  }

  /* The entry has the exit problem in mirror image. The first copy is sticky
     to the bottom band, so it pins the moment its panel starts entering the
     viewport, while the stage is still riding up toward its own pinned spot,
     and the phone descends straight over the words. Holding the panels back
     by exactly the stage's height means no copy can reach the viewport until
     the phone has settled. The pinned reading positions do not move: the
     band sits below 52svh at every height this layout serves. */
  .panels {
    padding-top: 52svh;
  }

  .stage::before {
    inset: 5% 10%;
  }

  .panel {
    min-height: 100svh;
    align-items: end;
    align-content: end;
    padding-bottom: 1.75rem;
    gap: 1rem;
  }

  .panel h3 {
    font-size: 1.625rem;
  }

  .panel p {
    font-size: 1rem;
  }

  /* Park the copy in the band under the phone for as long as its step lasts,
     instead of letting it scroll up across the screenshot. Sticky to the
     bottom does that with no script: the block holds at the foot of the
     viewport while its panel is passing, then leaves with it. */
  .panel-copy {
    position: sticky;
    bottom: 1.75rem;
    max-width: none;
  }

  .tick {
    margin-top: 1rem;
  }

  .tick .numeral {
    font-size: 2rem;
  }


}

/* --------------------------------------------------------------------------
   Closing
   -------------------------------------------------------------------------- */

.closing {
  position: relative;
  z-index: 12;
  background: var(--bg);
  padding: clamp(4rem, 8vw, 7rem) var(--gutter);
  text-align: center;
}

.closing h2 {
  font-size: clamp(2rem, 4.5vw, 3.25rem);
  margin-bottom: 0.75rem;
}

.closing p {
  color: var(--dim);
}

/* --------------------------------------------------------------------------
   Motion.

   Everything that hides something is scoped to .js-motion, which motion.js
   sets on the document before the first paint. No script, an old browser, or
   reduced motion means the class is never set and nothing is ever hidden.
   Getting this backwards makes the page blank rather than static.
   -------------------------------------------------------------------------- */

/* Without the class, only the first screen shows: they are stacked in one grid
   cell, so the rest would otherwise pile up on top of it. */
.phone-stage .screen + .screen {
  visibility: hidden;
}

.js-motion .phone-stage .screen {
  visibility: visible;
  opacity: 0;
  z-index: 1;
  transition:
    opacity 0.5s ease,
    transform 0.75s cubic-bezier(0.2, 0.7, 0.2, 1),
    clip-path 0.75s cubic-bezier(0.2, 0.7, 0.2, 1);
}

/* Three ways in, cycling, so six steps are not six identical cuts. The order
   here matters: these have the same specificity as the .is-live rule below and
   would win on source order if they came after it. */
.js-motion .phone-stage .screen:nth-child(3n + 1) {
  transform: scale(1.04);
}

.js-motion .phone-stage .screen:nth-child(3n + 2) {
  transform: translateY(4%);
}

/* The wipe stays fully opaque and is hidden by its own clip instead, which is
   what makes it read as a reveal rather than a fade. */
.js-motion .phone-stage .screen:nth-child(3n + 3) {
  opacity: 1;
  clip-path: inset(0 0 100% 0);
}

.js-motion .phone-stage .screen.is-live {
  opacity: 1;
  z-index: 2;
  transform: none;
  clip-path: inset(0 0 0 0);
}

.js-motion .panel-copy {
  opacity: 0.2;
  transform: translateY(14px);
  transition: opacity 0.5s ease, transform 0.5s ease;
}

.js-motion .panel.is-live .panel-copy {
  opacity: 1;
  transform: none;
}

/* On a narrow screen the panels pass behind the pinned phone on their way up,
   and a half-faded paragraph crossing a screenshot reads as a rendering fault
   rather than a transition. Only the current one is visible there.
   This has to come after the rule it overrides: same specificity, so source
   order is what decides it. */
@media (max-width: 63.999rem) {
  .js-motion .panel-copy {
    opacity: 0;
  }

  .js-motion .panel.is-live .panel-copy {
    opacity: 1;
  }
}

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

  .js-motion .panel-copy {
    opacity: 1;
    transform: none;
  }

  *,
  *::before,
  *::after {
    animation: none !important;
    transition-duration: 0.001ms !important;
  }
}

/* --------------------------------------------------------------------------
   Legal documents
   -------------------------------------------------------------------------- */

.doc {
  max-width: 46rem;
  margin: 0 auto;
  padding: clamp(2.5rem, 7vw, 4rem) var(--gutter) 2rem;
}

.doc h1 {
  font-size: clamp(2rem, 5vw, 2.75rem);
  max-width: 20ch;
  margin-bottom: 1rem;
}

.doc-intro {
  color: var(--dim);
  max-width: 52ch;
}

.doc-meta {
  color: var(--dim);
  font-size: 0.8125rem;
  display: flex;
  gap: 0.5rem;
  align-items: baseline;
  margin-bottom: 2rem;
}

.doc-meta .label {
  color: var(--dim);
}

.notice {
  border: 1px solid var(--warn);
  border-radius: 10px;
  background: var(--surface);
  padding: 1.125rem 1.25rem;
  margin: 0 0 2.5rem;
}

.notice .label {
  color: var(--warn);
  margin-bottom: 0.5rem;
}

.notice p:last-child {
  margin: 0;
  color: var(--dim);
  font-size: 0.9375rem;
  max-width: 62ch;
}

.toc {
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 1.125rem 1.25rem;
  margin-bottom: 3rem;
  background: var(--surface);
}

.toc .label {
  margin-bottom: 0.75rem;
}

/* No list markers. The Terms number their own headings ("1. Who can use
   Hidewire") and a marker on top of that reads as "1. 1.". */
.toc ul {
  margin: 0;
  padding: 0;
  list-style: none;
  color: var(--dim);
  font-size: 0.9375rem;
  display: grid;
  gap: 0.35rem;
}

.toc a {
  color: var(--dim);
  text-decoration: none;
}

.toc a:hover {
  color: var(--text);
  text-decoration: underline;
}

/* Around 70 characters of measure, per the brief. */
.prose {
  max-width: 68ch;
}

.prose p {
  color: var(--dim);
  margin-bottom: 1.25em;
}

.prose h2 {
  position: relative;
  font-size: 1.375rem;
  margin: 2.75rem 0 1rem;
  scroll-margin-top: 1.5rem;
}

.prose h3 {
  font-size: 1.125rem;
  margin: 2rem 0 0.75rem;
  scroll-margin-top: 1.5rem;
}

.anchor {
  position: absolute;
  left: -1.25rem;
  color: var(--faint);
  text-decoration: none;
  opacity: 0;
}

.anchor::before {
  content: '#';
}

.prose h2:hover .anchor,
.anchor:focus-visible {
  opacity: 1;
}

@media (max-width: 40rem) {
  .anchor {
    display: none;
  }
}
