/* Sections: page, title and buttons, cards, zoomed view, captions, intro, privacy page,
   phones (one block), motion (one block per layout), resizing. */

/* ----- Page ----- */

:root {
  /* Apple's light gray page and white cards (apple.com/iphone-duo), with a slight shadow. */
  --page: #f5f5f7;
  --card: #ffffff;
  /* The blank stand-in shown on cards other than the front one. */
  --card-behind: #eaeaea;
  /* The sheets stacked under the front card step down from white to the grey of the card
     behind: the deeper sheet matches it. js/viewer.js reads both. */
  --sheet-near: #f3f3f4;
  --sheet-far: var(--card-behind);
  --card-shadow: 0 0.2rem 1.2rem rgba(0, 0, 0, 0.06), 0 0 0 1px rgba(0, 0, 0, 0.02);
  /* Taglines and captions. Dark enough for 4.5:1 on --page at the 16px floor, still lighter
     than the body copy (#3f3f3f). */
  --quiet: #6e6e72;
  /* Kalam Pink: the strokes in the drawings, and the fill of the download button. */
  --brand: #f0457a;
  /* Ink Pink: the brand as text and focus rings, 5.9:1 on white (Kalam Pink is only 3.6:1). */
  --brand-ink: #c2185b;
  --ease: cubic-bezier(0.32, 0, 0, 1);
  --ease-out: cubic-bezier(0.215, 0.61, 0.355, 1);
  /* The zoom's timing. Keep in sync with ZOOM_MS and ZOOM_EASE in js/viewer.js. */
  --zoom: 1.2s;
  /* 3.2rem and 6.4rem are 32px and 64px at a 1440px window, and they scale with the rem
     layout instead of staying a fixed pixel size. The open frame is twice the card. */
  --card-radius: 3.2rem;
  --frame-radius: 6.4rem;
  /* Space between slides. Keep in sync with SLIDE_GAP in js/viewer.js. */
  --slide-gap: 2rem;
}

/* Everything is sized in rem and scales with the window width, as on the reference site:
   1rem is 1/144 of the width on desktop and 1/37.5 on phones, with the phone layout taking
   over at 768 px. Keep in sync with NARROW_QUERY in js/layout.js. */
html {
  font-size: 0.694445vw;
  scrollbar-width: none;
}

@media (max-width: 768px) {
  html { font-size: 2.66667vw; }
}

body {
  margin: 0;
  background: var(--page);
  color: #1a1a1a;
  font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
  font-weight: 400;
  -webkit-font-smoothing: antialiased;
}

body.home {
  overflow: hidden;
  height: 100vh;
  height: 100dvh;
}

a { color: inherit; }

.sr {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* ----- Title and buttons ----- */

.copy {
  position: absolute;
  left: 25.7rem;
  top: 0;
  z-index: 5;
  height: 100vh;
  height: 100dvh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: flex-start;
  gap: 5rem;
}

.lockup {
  position: relative;
  display: flex;
  flex-direction: column;
}

.lockup-scale {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  transform-origin: 100% 100%;
}

.line { overflow: hidden; }

.line-title { padding-bottom: 0.25rem; }

.line-role { padding-bottom: 0.5rem; }

h1, .role {
  margin: 0;
  font-size: max(16px, 3.2rem);
  font-weight: 400;
  line-height: 1;
  letter-spacing: normal;
}

h1 { color: #1a1a1a; }

.role { color: var(--quiet); }

/* The logo stands in for the title text; its letters are about 55% of the image height.
   It is 3.75 times the tagline size (12rem to 3.2rem), and the minimums keep that ratio:
   taglines never drop below 16px and the wordmark below 60px. */
.wordmark {
  display: block;
  height: max(60px, 12rem);
  width: auto;
}

/* The second tagline waits until the reel leaves the first card (js/main.js adds .is-shown),
   then drops in like the intro text. */
body .lockup .line-more:not(.is-shown) .role { transform: translateY(-160%); }

/* The tagline under the logo follows the front card (js/main.js): it rolls up and out, then the
   next card's rolls in from below. Only between the intro and an open card, which move it
   themselves. Keep the roll-out time in sync with TAGLINE_OUT_MS in js/main.js. */
@media (prefers-reduced-motion: no-preference) {
  body:not(.intro):not(.viewing) .lockup-scale .line-role .role { transition: transform 0.5s var(--ease); }
}
body:not(.intro):not(.viewing) .lockup-scale .line-role .role.is-out {
  transform: translateY(-160%);
  transition: transform 0.35s cubic-bezier(0.55, 0, 1, 0.45);
}
body:not(.intro):not(.viewing) .lockup-scale .line-role .role.is-below { transform: translateY(160%); transition: none; }

.actions {
  position: relative;
  display: flex;
  gap: 0.4rem;
}

.action { display: flex; }

.square {
  width: 5.4rem;
  height: 5.4rem;
  padding: 0;
  border: 0;
  border-radius: 1.5rem;
  background: var(--card);
  box-shadow: var(--card-shadow);
  color: #1a1a1a;
  display: flex;
  align-items: center;
  justify-content: center;
  text-decoration: none;
  cursor: pointer;
  transition: scale 0.3s cubic-bezier(0.075, 0.82, 0.165, 1);
}

.square svg { width: 1.5rem; height: 1.5rem; display: block; flex: none; }

/* The primary download names itself. Same height as the other square, filled with the ink. */
.square.download {
  width: auto;
  padding: 0 1.6rem 0 1.3rem;
  gap: 0.7rem;
  background: var(--brand);
  color: #fff;
  font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
  font-size: max(16px, 1.5rem);
  /* Bold, so white on Kalam Pink counts as large text for contrast. */
  font-weight: 600;
  line-height: 1;
  white-space: nowrap;
}

.action-label { display: block; }

/* The platform stays in the accessible name. On a phone the word itself is enough, and the
   full phrase would meet the close button. */
@media (max-width: 768px) {
  .action-platform { display: none; }
}

@media (hover: hover) {
  .square:hover { scale: 1.037; }
}

.square:active { scale: 0.97; }

.square:focus-visible,
.panel a:focus-visible,
.panel button:focus-visible,
.dots button:focus-visible {
  outline: 2px solid var(--brand-ink);
  outline-offset: 3px;
}

.panel[hidden] { display: none; }

.panel {
  position: absolute;
  left: 0;
  bottom: calc(100% + 0.8rem);
  min-width: 16rem;
  padding: 1rem 1.1rem;
  background: var(--card);
  box-shadow: var(--card-shadow);
  border-radius: 1.5rem;
  display: flex;
  flex-direction: column;
  gap: 0.7rem;
}

.panel a,
.panel button {
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  /* The layout scales with the window; menu text stays readable on small windows. */
  font-size: max(1rem, 14px);
  line-height: 1.3;
  white-space: nowrap;
  text-align: left;
  text-decoration: none;
  color: #1a1a1a;
  cursor: pointer;
}

/* ----- Cards ----- */

.reel {
  position: absolute;
  right: 25.7rem;
  top: 0;
  width: 33.5rem;
  height: 100vh;
  height: 100dvh;
  perspective: 1500px;
  transform-style: preserve-3d;
  touch-action: pinch-zoom;
}

.card {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 33.5rem;
  height: 34.4rem;
  margin: 0;
  border-radius: var(--card-radius);
  overflow: hidden;
  background: var(--card);
  box-shadow: var(--card-shadow);
  transform-style: preserve-3d;
  backface-visibility: hidden;
  cursor: pointer;
}

.card img,
.card video {
  position: absolute;
  top: 2rem;
  left: 2rem;
  width: calc(100% - 4rem);
  height: calc(100% - 4rem);
  object-fit: contain;
  pointer-events: none;
}

/* Framed screenshots ("framed" in cards.json): size the box to the picture so the rounding
   lands on it. */
.card,
.viewer-slide { container-type: size; }

.card img.rounded,
.viewer-slide img.rounded {
  --w: min(100cqw - 4rem, (100cqh - 4rem) * var(--ar));
  position: absolute;
  top: 50%;
  left: 50%;
  width: var(--w);
  height: auto;
  aspect-ratio: var(--ar);
  margin: 0;
  translate: -50% -50%;
  border-radius: calc(var(--w) * 0.025);
}

/* Text cards, like the privacy policy. */
.policy {
  position: absolute;
  inset: 0;
  box-sizing: border-box;
  padding: 2.6rem;
  display: flex;
  flex-direction: column;
  justify-content: center;
}

.policy h2 {
  margin: 0;
  font-size: 3.2rem;
  font-weight: 400;
  line-height: 1;
}

.policy .role { margin-top: 0.5rem; }

.policy .basic {
  max-width: none;
  margin: 1.5rem 0 0;
}

/* Only the front card shows its picture; the others stand in as plain grey cards.
   A layer over the card rather than a background change, because the intro sets
   inline transitions on the cards. */
.card::after {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--card-behind);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.45s var(--ease-out);
}

.reel .card:not(.is-front)::after { opacity: 1; }

/* The front card sits on a stack, its pictures peeking out below like sheets, so it reads as
   something to open: one sheet for a single picture, two for more. Box shadows rather than
   elements, since the card clips its children. A sheet is a shadow pushed down by --lift and
   narrowed by --tuck, so it shows --lift minus --tuck below whatever sits on it. Each layer casts
   a soft shadow onto the one under it (the card onto sheet 1, sheet 1 onto sheet 2), and each
   sheet has a hairline edge, so the three read apart. Listed top layer first.

   Hovering the card (js/reel.js sets --hover from 0 to 1 with its zoom) fans the sheets out, and
   so does the one bounce after the intro (js/main.js), which plays the same hover. */
.reel .card.is-front {
  --lift-1: calc(1.7rem + 0.8rem * var(--hover, 0));
  --tuck-1: 0.8rem;
  --lift-2: calc(3.2rem + 1.6rem * var(--hover, 0));
  --tuck-2: 1.6rem;
  --cast-card: 0 0.25rem 0.5rem -0.2rem rgba(0, 0, 0, 0.14);
  --sheet-1:
    0 var(--lift-1) 0 calc(-1 * var(--tuck-1)) var(--sheet-near),
    0 calc(var(--lift-1) + 1px) 0 calc(-1 * var(--tuck-1)) rgba(0, 0, 0, 0.1);
  --sheet-1-cast: 0 calc(var(--lift-1) + 0.25rem) 0.5rem calc(-0.2rem - var(--tuck-1)) rgba(0, 0, 0, 0.13);
  box-shadow: var(--cast-card), var(--sheet-1), var(--sheet-1-cast), var(--card-shadow);
}

.reel .card.is-front:not([data-slides="1"]) {
  box-shadow:
    var(--cast-card),
    var(--sheet-1),
    var(--sheet-1-cast),
    0 var(--lift-2) 0 calc(-1 * var(--tuck-2)) var(--sheet-far),
    0 calc(var(--lift-2) + 1px) 0 calc(-1 * var(--tuck-2)) rgba(0, 0, 0, 0.09),
    0 calc(var(--lift-2) + 0.4rem) 0.6rem calc(-1 * var(--tuck-2)) rgba(0, 0, 0, 0.07),
    var(--card-shadow);
}

/* Neighbouring cards fade into the page at the top and bottom edges, above the reel
   (no z-index) and below the title (5) and dots (6). */
.fade {
  position: fixed;
  left: 0;
  right: 0;
  z-index: 4;
  height: 8rem;
  pointer-events: none;
}

/* The fades repeat the page colour and fade it out with a mask, so the cards beneath fade away. */
.fade-top {
  top: 0;
  background: var(--page);
  mask-image: linear-gradient(to bottom, #000, transparent);
}

.fade-bottom {
  bottom: 0;
  background: var(--page);
  mask-image: linear-gradient(to top, #000, transparent);
}

.dots {
  position: absolute;
  top: 50%;
  right: 1.6rem;
  z-index: 6;
  transform: translateY(-50%);
  display: flex;
  flex-direction: column;
}

.dots button {
  width: 1.4rem;
  height: 1rem;
  padding: 0;
  border: 0;
  background: transparent;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
}

.dots i {
  width: 3px;
  height: 3px;
  border-radius: 50%;
  background: #1a1a1a;
  opacity: 0.35;
  display: block;
}

.dots button[aria-pressed="true"] i { opacity: 1; }

/* ----- Zoomed view -----
   body.viewing: the viewer is open. body.zoomed: open or opening (cleared as it closes).
   .viewer.is-settled: the open animation has ended. js/viewer.js animates the frame between
   a card and the box below; at rest the box below places it. */

body.viewing .reel,
body.viewing .dots,
body.viewing .fade {
  visibility: hidden;
  pointer-events: none;
}

.viewer[hidden] { display: none; }

.viewer { touch-action: pinch-zoom; }

/* The open box: a square tile as wide as the right 55% of the window, less a 2rem margin, like
   the reference site's. It depends on the width only, so narrowing the window shrinks it
   without changing its shape, and the next slide shows below it. */
.viewer-frame {
  --side: calc(100vw * 0.55 - 2rem);
  position: fixed;
  top: 2rem;
  right: 2rem;
  z-index: 12;
  width: var(--side);
  height: var(--side);
  border-radius: var(--frame-radius);
  overflow: hidden;
  background: var(--card);
  box-shadow: var(--card-shadow);
}

/* While the frame animates, it alone draws the card: its corners, background and shadow, with
   the slides plain inside it. Once open, each slide draws its own identical card and the frame
   stops clipping, so the handover does not show. */
.viewer:not(.is-settled) .viewer-slide {
  border-radius: 0;
  box-shadow: none;
  background: none;
}

.viewer.is-settled .viewer-frame {
  overflow: visible;
  background: transparent;
  border-radius: 0;
  box-shadow: none;
}

.viewer-scroll {
  will-change: transform;
  position: absolute;
  inset: 0;
}

.viewer-slide {
  position: absolute;
  left: 0;
  top: calc(var(--i, 0) * (100% + var(--slide-gap)));
  width: 100%;
  height: 100%;
  border-radius: var(--frame-radius);
  overflow: hidden;
  background: var(--card);
  box-shadow: var(--card-shadow);
}

.viewer-slide img,
.viewer-slide video {
  display: block;
  width: calc(100% - 4rem);
  height: calc(100% - 4rem);
  margin: 2rem;
  object-fit: contain;
  pointer-events: none;
}

/* Text slides are laid out at card size and scaled to fill the slide (js/viewer.js). */
.viewer-frame .policy {
  inset: auto;
  width: 33.5rem;
  height: 34.4rem;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%) scale(var(--policy-scale, 1));
  transform-origin: center center;
}

/* 2rem left of the open box, which starts 45% of the way across. */
.viewer-close {
  position: fixed;
  top: 50%;
  left: calc(100vw * 0.45 - 7.4rem);
  z-index: 20;
  transform: translateY(-50%);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.45s var(--ease-out);
}

/* Desktop: the close button rides level with the mouse (js/viewer.js sets --close-y, its offset
   from the middle). It eases there, and .is-placing jumps it while it is still hidden. */
@media (min-width: 769px) {
  .viewer-close { translate: 0 var(--close-y, 0px); }
}

@media (min-width: 769px) and (prefers-reduced-motion: no-preference) {
  .viewer-close { transition: opacity 0.45s var(--ease-out), translate 0.45s cubic-bezier(0.22, 1, 0.36, 1); }
  .viewer-close.is-placing { transition: opacity 0.45s var(--ease-out); }
}

body.viewing.zoomed .viewer-close {
  opacity: 1;
  pointer-events: auto;
}

/* The title and buttons slide left to sit centred in the space before the close button.
   That button is 5.4rem wide and ends 2rem before the tile, so the space is 45vw - 7.4rem.
   Half of that, then back by half the column, so a shorter or longer tagline stays centred. */
@media (min-width: 769px) {
  body.zoomed .copy {
    left: calc((45vw - 7.4rem) / 2);
    translate: -50% 0;
  }
}

/* While a picture is open, the Kalam wordmark sits in the top left. js/viewer.js moves a fixed
   copy here from the lockup and back on close; the original hides meanwhile. */
.brand-mark {
  position: fixed;
  left: 2rem;
  top: 2rem;
  z-index: 14;
  display: block;
  height: max(36px, 5rem);
  width: auto;
  transform-origin: 0 0;
  pointer-events: none;
}

.brand-mark[hidden] { display: none; }

body.branded .lockup .wordmark { visibility: hidden; }

/* ----- Captions -----
   While a picture is open, the taglines roll up and out and the slide's title and tagline roll
   in from below; another slide rolls its caption in the same way, and closing reverses it.
   Two .caption layers take turns (js/viewer.js): .is-in is showing, .is-out has just left,
   .is-snap jumps back below without animating. Phones fade instead (below). */

.caption {
  position: absolute;
  top: 50%;
  left: 0;
  transform: translateY(-50%);
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  white-space: nowrap;
  pointer-events: none;
}

.caption-line { overflow: hidden; padding-bottom: 0.5rem; }

.caption p {
  margin: 0;
  font-size: max(16px, 3.2rem);
  line-height: 1;
  color: var(--quiet);
  transform: translateY(160%);
}

.caption .caption-title { color: #1a1a1a; }

body.zoomed .caption.is-in p { transform: none; }

body.zoomed .caption.is-out p { transform: translateY(-160%); }

body.zoomed .lockup-scale .line:not(.line-title) > * { transform: translateY(-160%); }

/* ----- Intro ----- */

body.intro .lockup-scale { transform: scale(0.5); }

body.intro.intro-run .lockup-scale {
  transform: scale(0.72);
  transition: transform 1.08s var(--ease);
}

body.intro.intro-text .lockup-scale {
  transform: scale(1);
  transition: transform 1.08s var(--ease);
}

body.intro .line { opacity: 0; }

body.intro.intro-run .line {
  opacity: 1;
  transition: opacity 1.08s var(--ease);
}

body.intro .line h1,
body.intro .line .role { transform: translateY(-160%); }

body.intro.intro-run .line h1,
body.intro.intro-run .line .role {
  transform: translateY(0);
  transition: transform 1.2s var(--ease);
}

body.intro .action {
  opacity: 0;
  transform: translateY(28px);
  pointer-events: none;
}

body.intro.intro-cta .action {
  opacity: 1;
  transform: none;
  pointer-events: auto;
  transition:
    opacity 0.72s var(--ease),
    transform 0.72s var(--ease);
}

body.intro.intro-cta .action-stagger { transition-delay: 0.08s, 0.08s; }

body.intro .dots {
  opacity: 0;
  pointer-events: none;
}

body.intro.intro-dots .dots {
  opacity: 1;
  pointer-events: auto;
  transition: opacity 0.35s var(--ease-out);
}

body.intro .reel { pointer-events: none; }

/* ----- Privacy page ----- */

.page {
  min-height: 100vh;
  display: flex;
  align-items: center;
  padding: 4rem 8vw;
}

.page h1,
.page .role { font-size: clamp(26px, 3.2rem, 46px); }

.page .role { margin-top: 0.5rem; }

.page .brand {
  display: inline-block;
  margin-bottom: 2.5rem;
}

.page .wordmark { height: clamp(28px, 2.6rem, 40px); }

.page .basic {
  max-width: 36em;
  font-size: clamp(16px, 1.15rem, 18px);
}

.basic {
  max-width: 36rem;
  margin: 1.5rem 0 0;
  font-size: 1.15rem;
  line-height: 1.45;
  color: #3f3f3f;
}

.back {
  display: inline-block;
  margin-top: 2rem;
  font-size: max(1rem, 15px);
  color: #1a1a1a;
}

/* ----- Phones ----- */

@media (max-width: 768px) {
  :root { --frame-radius: 3.2rem; }

  h1, .role, .policy h2 { font-size: max(16px, 1.8rem); }

  /* A two-line title like the reference site: the wordmark over the first tagline; the second
     tagline is desktop only. The wordmark is 25% bigger than the desktop's ratio gives
     (6.75rem to the 1.8rem tagline, so 8.4375rem; minimum 75px). The pair always sits above
     the card: the space for it is the card's top (50% - 13.45rem) less 2.4rem of padding, a
     1.5rem gap and 1.3rem of line spacing, and when that is short of 10.2375rem both shrink
     together, the wordmark taking 82.4% of it and the tagline 17.6%. Once both are at their
     minimums and still do not fit (near-square windows), the reel moves down by the shortfall
     instead, so the card stays below the title at full size and the buttons float over its
     bottom edge. */
  :root {
    --title-space: calc(50vh - 18.65rem);
    --wordmark-height: max(75px, min(8.4375rem, var(--title-space) * 0.8242));
    --tagline-size: max(16px, min(1.8rem, var(--title-space) * 0.1758));
  }

  @supports (height: 1dvh) {
    :root { --title-space: calc(50dvh - 18.65rem); }
  }

  .wordmark { height: var(--wordmark-height); }

  .lockup .role { font-size: var(--tagline-size); }

  .lockup .line-more { display: none; }

  .copy {
    position: fixed;
    inset: 0;
    width: 100%;
    box-sizing: border-box;
    height: auto;
    align-items: center;
    justify-content: space-between;
    gap: 0;
    padding: 2.4rem 1.5rem 4rem;
    text-align: center;
    pointer-events: none;
  }

  .lockup { align-items: center; }

  /* Open, the page colour fills the top behind the Kalam mark and the caption, down to just
     above the tile (top: 8rem below), so slides scrolled up pass under it. Inside .copy, so it
     sits behind the caption and under the mark (.brand-mark, 14). */
  .copy::before {
    content: "";
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    height: 7.5rem;
    z-index: -1;
    background: var(--page);
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.6s var(--ease-out);
  }

  body.zoomed .copy::before { opacity: 1; }

  .lockup-scale { transform-origin: center center; }

  .actions {
    left: 0;
    pointer-events: auto;
  }

  .panel { left: 50%; transform: translateX(-50%); }

  .reel {
    top: max(0px, var(--wordmark-height) + var(--tagline-size) - var(--title-space));
    right: auto;
    left: 50%;
    width: 26.9rem;
    transform: translateX(-50%);
  }

  .card {
    width: 26.9rem;
    height: 26.9rem;
  }

  /* Zoomed: a 33.5rem square like the reference site, so the tile grows without changing
     shape, with the next slide below it. Slides scrolled above it carry on up under the
     header (.copy::before, which sits above the viewer). */
  .viewer-frame {
    --side: 33.5rem;
    left: calc((100vw - var(--side)) / 2);
    right: auto;
    top: 8rem;
  }

  .viewer-frame .policy {
    width: 26.9rem;
    height: 26.9rem;
  }

  /* Zoomed, the buttons float over the slides in one row, 4rem from the edges: the downloads
     slide from the centre to the bottom left and close slides in from the right. The column
     has 1.5rem of padding, so 4rem from the viewport is 2.5rem into the column. translate
     50% puts back half the row, whatever the labelled download measures. */
  body.viewing .copy { z-index: 13; }

  body.zoomed .copy { translate: none; }

  body.zoomed .actions {
    left: calc(2.5rem - 50%);
    translate: 50% 0;
  }

  .viewer-close {
    left: auto;
    right: 4rem;
    top: auto;
    bottom: 4rem;
    transform: none;
    translate: 8rem 0;
  }

  body.zoomed .viewer-close { translate: none; }

  /* Smaller here, where the caption is centred along the top. */
  .brand-mark {
    left: 1.5rem;
    top: 1.5rem;
    height: max(30px, 3rem);
  }

  /* Captions sit along the top and fade rather than roll. */
  .caption {
    top: 0;
    left: 50%;
    transform: translateX(-50%);
    align-items: center;
  }

  .caption p { font-size: max(16px, 1.8rem); }

  .caption p,
  body.zoomed .caption.is-out p {
    transform: none;
    opacity: 0;
  }

  body.zoomed .caption.is-in p { opacity: 1; }

  body.zoomed .lockup-scale .line:not(.line-title) > * {
    transform: none;
    opacity: 0;
  }

  /* The intro fades the title in from the middle of the screen (js/intro.js, --intro-shift). */
  body.intro .lockup { transform: translateY(var(--intro-shift, 0px)); }

  body.intro.intro-text .lockup {
    transform: none;
    transition: transform 1.08s var(--ease);
  }

  body.intro .line h1,
  body.intro .line .role {
    transform: none;
    opacity: 0;
  }

  body.intro.intro-run .line h1,
  body.intro.intro-run .line .role {
    opacity: 1;
    transform: none;
    transition: opacity 0.6s var(--ease);
  }
}

/* ----- Motion -----
   The zoom's moves, timed with the frame. Without these, reduced-motion visitors see each
   state straight away. */

@media (prefers-reduced-motion: no-preference) {
  .line-more.is-shown .role { transition: transform 1.2s var(--ease); }

  .caption p,
  body.viewing .lockup-scale .line:not(.line-title) > * { transition: transform var(--zoom) var(--ease); }

  .caption.is-snap p,
  body:not(.zoomed) .caption.is-out p { transition: none; }
}

@media (min-width: 769px) and (prefers-reduced-motion: no-preference) {
  .copy { transition: translate var(--zoom) var(--ease), left var(--zoom) var(--ease); }
}

/* Phones: the old caption fades out before the new one fades in, in the same place. */
@media (max-width: 768px) and (prefers-reduced-motion: no-preference) {
  .actions { transition: left var(--zoom) var(--ease), translate var(--zoom) var(--ease); }

  .viewer-close {
    transition:
      opacity 0.45s var(--ease-out),
      translate var(--zoom) var(--ease);
  }

  .caption p,
  body.viewing .lockup-scale .line:not(.line-title) > * { transition: opacity 0.5s var(--ease); }

  body.zoomed .caption.is-in p,
  body.viewing:not(.zoomed) .lockup-scale .line:not(.line-title) > * { transition-delay: 0.5s; }

  .caption.is-snap p { transition: none; }
}

/* ----- Resizing -----
   While the window is being resized (js/main.js), nothing animates: crossing the phone
   breakpoint changes how hidden things are hidden, which would otherwise play as motion. */

html.resizing *,
html.resizing *::after {
  transition: none !important;
}
