/* =============================================================
   travelet — marketing site

   Glass laid on the app's own sky. The sky itself is painted by
   assets/sky.js; what is here is the glass, and a set of ink tokens
   that script rewrites for whatever hour it is — the palette runs from
   a near-white morning to a near-black night, and no single text colour
   survives both.

   No webfonts, no images, no requests.
   ============================================================= */

/*
   The app's own face, shipped with the page rather than fetched from anyone.
   Be Vietnam Pro Black is what the app sets its wordmark and its arched titles
   in — `BeVietnamPro-Black` in ArchTitle.tsx and in the home screen's
   `traveletTitle` — so it is what the site's name and headings are set in too.
*/
@font-face {
  font-family: 'Be Vietnam Pro';
  /* Subset to the characters the headings actually set and compressed to
     WOFF2: the whole face as shipped in the app is 139 KB, and this is 8. See
     the README for the one command that regenerates it. */
  src: url('fonts/BeVietnamPro-Black.woff2') format('woff2');
  font-weight: 900;
  font-style: normal;
  font-display: swap;
}

:root {
  /*
     Set by assets/sky.js from the palette of the hour, because the sky runs
     from a near-white morning to a near-black night and one fixed ink cannot
     be read on both. These are the values before it gets there — the app's
     midday text colours.
  */
  --ink: #0b1f38;
  --ink-quiet: #3c5878;
  --ink-faint: rgba(11, 31, 56, 0.62);
  --film: rgba(255, 255, 255, 0.44);
  --edge: rgba(20, 30, 55, 0.14);
  --edge-strong: rgba(20, 30, 55, 0.30);

  --shell: min(1180px, 90vw);

  --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, Roboto,
          "Helvetica Neue", Arial, sans-serif;
  /* the app's, for the name and the headings only */
  --title: 'Be Vietnam Pro', var(--font);

  /*
     The app's own material grain, tiled as it is there — same geometry, same
     alphas, same pitch. The one departure: the ordered dither goes on at 0.14
     rather than the app's 0.2, because here the cards are turned a few degrees
     and a Bayer grid at an angle beats against the pixel grid — the moire
     reads as travelling lines rather than as tone. The app's covers hang
     square, so they can afford the heavier hand. In the app each of these is an SVG <Pattern> laid over
     the cover by `components/physical/Texture.tsx`; here each is that pattern's
     tile, repeated by the background. What a cover wears, and how heavily, is
     what it wears in the app.
  */
  --tex-paper: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='26' height='14' viewBox='0 0 26 14'%3E%3Cline x1='1' y1='3' x2='11' y2='3.4' stroke='rgba(0,0,0,0.045)' stroke-width='0.7'/%3E%3Cline x1='14' y1='9' x2='25' y2='8.7' stroke='rgba(0,0,0,0.04)' stroke-width='0.6'/%3E%3Cline x1='5' y1='12' x2='12' y2='12.3' stroke='rgba(0,0,0,0.035)' stroke-width='0.5'/%3E%3Ccircle cx='20' cy='4' r='0.5' fill='rgba(0,0,0,0.05)'/%3E%3Ccircle cx='8' cy='7' r='0.4' fill='rgba(0,0,0,0.04)'/%3E%3C/svg%3E");
  --tex-noise: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='37' height='37' viewBox='0 0 37 37'%3E%3Ccircle cx='3' cy='5' r='0.7' fill='rgba(255,255,255,0.16)'/%3E%3Ccircle cx='11' cy='2' r='0.5' fill='rgba(0,0,0,0.12)'/%3E%3Ccircle cx='19' cy='8' r='0.65' fill='rgba(255,255,255,0.13)'/%3E%3Ccircle cx='27' cy='3' r='0.5' fill='rgba(0,0,0,0.10)'/%3E%3Ccircle cx='33' cy='11' r='0.6' fill='rgba(255,255,255,0.14)'/%3E%3Ccircle cx='6' cy='14' r='0.55' fill='rgba(0,0,0,0.11)'/%3E%3Ccircle cx='15' cy='18' r='0.7' fill='rgba(255,255,255,0.15)'/%3E%3Ccircle cx='24' cy='16' r='0.5' fill='rgba(0,0,0,0.10)'/%3E%3Ccircle cx='31' cy='21' r='0.6' fill='rgba(255,255,255,0.12)'/%3E%3Ccircle cx='2' cy='24' r='0.6' fill='rgba(0,0,0,0.12)'/%3E%3Ccircle cx='9' cy='29' r='0.55' fill='rgba(255,255,255,0.14)'/%3E%3Ccircle cx='18' cy='26' r='0.5' fill='rgba(0,0,0,0.09)'/%3E%3Ccircle cx='26' cy='31' r='0.7' fill='rgba(255,255,255,0.13)'/%3E%3Ccircle cx='34' cy='28' r='0.5' fill='rgba(0,0,0,0.11)'/%3E%3Ccircle cx='13' cy='34' r='0.6' fill='rgba(255,255,255,0.12)'/%3E%3Ccircle cx='22' cy='35' r='0.5' fill='rgba(0,0,0,0.10)'/%3E%3C/svg%3E");
  --tex-linen: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='6' height='6' viewBox='0 0 6 6'%3E%3Cline x1='0' y1='1.5' x2='6' y2='1.5' stroke='rgba(0,0,0,0.035)' stroke-width='0.6'/%3E%3Cline x1='0' y1='4.5' x2='6' y2='4.5' stroke='rgba(0,0,0,0.03)' stroke-width='0.5'/%3E%3Cline x1='1.5' y1='0' x2='1.5' y2='6' stroke='rgba(0,0,0,0.03)' stroke-width='0.5'/%3E%3Cline x1='4.5' y1='0' x2='4.5' y2='6' stroke='rgba(0,0,0,0.025)' stroke-width='0.5'/%3E%3C/svg%3E");
  --tex-halftone: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Crect x='0' y='0' width='1.4' height='1.4' fill='rgba(0,0,0,0.5)'/%3E%3Crect x='6' y='0' width='1.4' height='1.4' fill='rgba(0,0,0,0.5)'/%3E%3Crect x='3' y='3' width='1.4' height='1.4' fill='rgba(0,0,0,0.5)'/%3E%3Crect x='0' y='6' width='1.4' height='1.4' fill='rgba(0,0,0,0.5)'/%3E%3Crect x='6' y='6' width='1.4' height='1.4' fill='rgba(0,0,0,0.5)'/%3E%3Crect x='9' y='9' width='1.4' height='1.4' fill='rgba(0,0,0,0.5)'/%3E%3C/svg%3E");

}

* { box-sizing: border-box; }

html { scroll-behavior: smooth; }

body {
  margin: 0;
  min-height: 100%;
  font-family: var(--font);
  color: var(--ink);
  /*
     The colour of the *canvas*, which is not the same thing as the colour of
     the page. The sky is `position: fixed`, so it covers the viewport and no
     more — and when a phone rubber-bands past the top or the bottom, what
     shows in the gap it opens is this, propagated from body to the canvas.

     It was `#b7dbf9`, a saturated blue left over from before there was a
     photograph, and on a phone that is a bright blue band above and below a
     pale grey page every time you flick past either end.

     Two stops, sampled off the photograph with the wash applied: `#c3d0df` is
     what the top of the sky comes out at and `#ccd3d9` the bottom, so
     overscrolling either end shows the colour that was already there.
  */
  background: linear-gradient(180deg, #c3d0df 0%, #ccd3d9 100%);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-x: hidden;
}

/* ------------------------------------------------------------- sky */

/*
   The sky is painted by assets/sky.js — a port of the app's own background,
   washes of colour laid on paper in the palette of the current hour. The only
   thing here is the frame it is painted into, and a still sky for anyone
   without JavaScript, in the midday palette the app would have drawn at noon.
*/
.sky {
  position: fixed;
  inset: 0;
  /*
     Behind everything, and it has to be said in the negative.

     At `z-index: 0` this is a positioned element, and a positioned element at
     zero paints *after* the in-flow text of everything that is not positioned —
     so the sky went over the hero's heading and its paragraph while the nav,
     the buttons and the phone, all of them positioned, came out on top of it.
     The page looked as though the hero had no words in it. At -1 it paints with
     the negative layer, under every box and every line of type on the page and
     over the canvas colour, which is where a background goes.
  */
  z-index: -1;
  pointer-events: none;
  overflow: hidden;

  /*
     A photograph, laid under everything and held still while the page moves
     over it — the element is fixed, so this costs nothing to scroll.

     The wash over it is not decoration. The page's ink is a dark navy, and the
     cloud in the top right of the photograph bottoms out around a fifth of the
     way up the scale: dark type on it comes to about 3.4:1, which is under the
     4.5 it needs. A white veil at a third takes the darkest of it to roughly
     5.7:1 and leaves the light middle almost untouched. It is heavier at the
     head and the foot, where the nav and the footer sit.

     The colour underneath is what shows before the photograph arrives, and if
     it never does.
  */
  background-color: #cfe3f7;
  /*
     The lower two stops are tinted, not white, and that is about phones.

     `cover` takes the larger of the two scales, and a 3:2 photograph on a 0.46
     screen has to be scaled by height — so a phone shows the *whole* vertical
     range of the picture, warm lower cloud and all, where a wide screen crops
     to the cooler middle. Sampled, the bottom of the photograph is (192,189,181),
     which is a beige, and under a white veil it stays a beige: the page came
     out yellow from the middle down.

     Veiling it in the sky's own blue instead of in white neutralises that and
     costs almost nothing in contrast — the darkest ink goes from 11.9:1 to
     11.0:1 and the quiet ink from 5.3:1 to 4.9:1, both still over the 4.5 they
     need. Re-measure both if the photograph is ever changed.
  */
  background-image:
    linear-gradient(180deg,
      rgba(255, 255, 255, 0.42) 0%,
      rgba(255, 255, 255, 0.28) 38%,
      rgba(214, 230, 247, 0.34) 70%,
      rgba(214, 230, 247, 0.54) 100%),
    url('sky.webp');
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
}

.sky__paint {
  position: absolute;
  inset: 0;
  display: block;
}

/*
   The paper's tooth, over the washes on both skies.

   A 192-pixel image the browser decodes once and repeats, rather than two
   turbulence filters run across the whole viewport and again across the
   phone's screen. It is generated to wrap — every octave's lattice period
   divides the tile, and the finest term is per-pixel — which an SVG filter tile could not do without leaving a
   bright hairline at each edge where the filter region ends. See the README
   for the script.
*/
.sky::after,
.phone__sky::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background-image: url('grain.png');
  background-size: 192px 192px;
  mix-blend-mode: multiply;
}

/* Half as much on the phone: the tile is the same physical size on both, so on
   a screen a third of the width it reads three times as coarse. */
.phone__sky::after { opacity: 0.5; }

/* ------------------------------------------------------------- nav */

/*
   Three columns with the nav in the middle one, rather than a row pushed to
   the ends. The outer columns are equal so the pill sits on the centre line of
   the page and not on the centre of what is left over after the wordmark —
   which is what `space-between` gives you, and it is always slightly wrong.
*/
/*
   Everything is in the nav now — the wordmark, the links and the one download
   button — so this is a wrapper and nothing else. Its only child is fixed, so
   it takes no height and needs no rules; the class is kept because a header
   with no class reads like an oversight rather than a decision.

   It was a three-column grid holding the wordmark at one edge and a download
   button at the other, with the nav floating over the middle. That put three
   separate things across the top of a page whose whole argument is that
   scattered things belong in one object.
*/
.bar { position: relative; }

.mark {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  text-decoration: none;
  color: var(--ink);
  /* it sits against the links now, not against the width of the page */
  margin-right: 4px;
  padding-right: 4px;
}

/* Beside the word and a shade smaller than it, as it is in the app's header. */
.mark__plane {
  width: 18px;
  height: 18px;
  display: block;
  margin-top: -2px;
}

.mark__word {
  font-family: var(--title);
  /* Set against the aeroplane rather than against the nav: the app pairs a
     32px word with a 28px plane, and this keeps that ratio. A cap height is
     about seven tenths of the type size, so a word set to match the plane's
     box comes out looking half its size. */
  /* Two thirds of what it was: the wordmark used to stand on its own at the
     page's edge and now shares a pill with four links and a button, where a
     30px word is a banner rather than a mark. The plane comes down with it —
     the app's 32/28 ratio is what is being kept, not the 32. */
  font-size: 20px;
  line-height: 1;
  font-weight: 900;
  letter-spacing: -0.5px;
  opacity: 0.92;
  transition: opacity 0.3s ease;
}

.mark:hover .mark__word { opacity: 1; }

.pill {
  /*
     22px is the bar's own padding, so it sits on the line it would have sat on
     as a grid item and does not move when the page does.

     Centred by `left: 0; right: 0` and an auto margin rather than by
     `left: 50%` and a translate. The translate version looks equivalent and is
     not: an out-of-flow box with `left` set and `right` auto may only be as
     wide as what is left of the containing block, so at `left: 50%` the nav
     could never exceed half the window — which it does not, until a download
     button goes in it and "What it does" starts wrapping onto two lines.
  */
  position: fixed;
  z-index: 21;
  top: 22px;
  left: 0;
  right: 0;
  width: fit-content;
  margin: 0 auto;
  display: flex;
  align-items: center;
  gap: clamp(6px, 1.6vw, 22px);
  padding: 8px 8px 8px 16px;
  border-radius: 999px;
  background: var(--film);
  border: 1px solid var(--edge);
  backdrop-filter: blur(14px) saturate(1.3);
  -webkit-backdrop-filter: blur(14px) saturate(1.3);
}

/*
   The download button, inside the nav.

   Solid where the links are quiet, so it reads as the one thing here that is
   an action rather than a destination — and with the Apple mark set larger
   than the type beside it, which is the convention every App Store badge
   follows and the reason it is recognisable at a glance rather than read.

   Padded a little tighter than a `.btn` elsewhere on the page: this one sits
   inside a pill that is already a container, and the two radii need air
   between them.
*/
.pill__get {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin-left: 4px;
  padding: 9px 18px;
  border-radius: 999px;
  /* Apple's own, from the App Store badge: pure black, white lettering, white
     mark. The page's own solid was a navy, and this button belongs to the App
     Store's identity rather than to this palette — so the navy has gone with
     the buttons that used it. */
  background: #000000;
  color: #ffffff;
  font-size: 14.5px;
  font-weight: 650;
  letter-spacing: -0.1px;
  text-decoration: none;
  white-space: nowrap;
  transition: transform 0.22s ease, box-shadow 0.22s ease;
}

.pill__get:hover {
  transform: translateY(-1px);
  box-shadow: 0 8px 18px rgba(12, 30, 62, 0.26);
}

.pill__get .apple {
  width: 21px;
  height: 21px;
  /* stated, not inherited: the glyph is white on the badge whatever the page's
     ink is doing, and `currentColor` would follow a theme change off a cliff */
  fill: #ffffff;
  /* the glyph sits high in its box, and Apple's own lockups drop it a hair */
  margin-top: -2px;
}

.pill a {
  color: var(--ink-quiet);
  text-decoration: none;
  font-size: 15px;
  letter-spacing: -0.1px;
  padding: 6px 12px;
  border-radius: 999px;
  transition: color 0.22s ease, background 0.22s ease;
}

.pill a:hover { color: var(--ink); }

.pill a.is-here {
  color: var(--ink);
  font-weight: 600;
  background: var(--film);
}

/*
   The wordmark is a link in the nav, and `.pill a` above catches it: it was
   coming out at the quiet ink with a nav link's padding on it. Everything an
   anchor in here is given, the mark is given back — it is a mark that happens
   to be clickable, not the fifth destination.
*/
.pill .mark {
  color: var(--ink);
  padding: 0;
  border-radius: 0;
}

.pill .mark:hover { color: var(--ink); }

/*
   And the button, for the same reason: `.pill a` above is more specific than
   `.pill__get` and comes after it, so the white the button sets for itself was
   being overwritten with the nav's quiet ink and the label was coming out grey
   on black. The mark was caught doing this and the button was not — the same
   bug, twice, because the fix was written against one selector rather than
   against the rule that causes it.
*/
.pill .pill__get,
.pill .pill__get:hover { color: #ffffff; }

/* ------------------------------------------------------------- hero */

/*
   Centred and stacked: headline, sub, call to action, the phone, and the same
   call to action again under it. The phone is the only picture on the page and
   it is not a picture — it is the app's sky and the app's cover, drawn live.
*/
.hero {
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 150px 20px 96px;
  text-align: center;
}

.hero__h {
  margin: 0;
  max-width: 18ch;
  font-family: var(--title);
  font-size: clamp(36px, 6.6vw, 70px);
  line-height: 1.04;
  font-weight: 900;
  letter-spacing: -1.6px;
  color: var(--ink);
  animation: rise 0.45s cubic-bezier(0.2, 0.8, 0.2, 1) both;
}

.hero__p {
  margin: 18px 0 0;
  max-width: 46ch;
  font-size: clamp(16px, 1.7vw, 19px);
  line-height: 1.55;
  color: var(--ink-quiet);
  animation: rise 0.45s 0.05s cubic-bezier(0.2, 0.8, 0.2, 1) both;
}

.hero__meta {
  margin: 14px 0 0;
  font-size: 13px;
  letter-spacing: 0.1px;
  color: var(--ink-faint);
  animation: rise 0.45s 0.14s cubic-bezier(0.2, 0.8, 0.2, 1) both;
}

/* ------------------------------------------------------------ the stage */

/*
   The phone, with the six objects hanging in the air around it.
   They are laid out on the stage's own box rather than in flow, so each one
   sits where it belongs relative to the phone at any width; every position in
   the markup is a percentage of this box for the same reason.
*/
.stage {
  position: relative;
  width: min(1240px, 96vw);
  margin: 54px auto 8px;
  /* Room above and below the phone for the objects to hang in: the air is
     inset to this box, so without the padding the lowest two would fall out
     of the bottom of it. */
  padding: 26px 0 74px;
  display: flex;
  justify-content: center;
}

.phone {
  position: relative;
  z-index: 5;
  /*
     The shadow is cast here and not on the picture itself, because the picture
     is masked. A mask clips to its own box, and a shadow is drawn outside one —
     put the two on the same element and the shadow comes back with straight
     edges where the box ends. Out here it is cast from what the mask has
     already left behind, so it fades away with the phone instead.
  */
  filter: drop-shadow(0 34px 54px rgba(12, 30, 62, 0.34));
  /* `backwards`, so the entrance hides it before its turn and then hands the
     property back — filled forwards it would hold opacity at 1 and the stage
     could never be emptied. */
  animation: rise 0.5s 0.12s cubic-bezier(0.2, 0.8, 0.2, 1) backwards;
}

/*
   The air the objects hang in.
   Absolute over the stage, so nothing here can push the phone around; it takes
   no pointer events, so the whole hero stays clickable through it.
*/
.air {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

/*
   Placed on a ring around the phone: each one carries the point on it where it
   belongs, and is hung from its own middle so the ring stays a ring whatever
   size the object is. The ellipse is wider than it is tall because the stage
   is — and because a true circle would put two of them across the phone's face.
*/
.float {
  position: absolute;
  left: var(--x);
  top: var(--y);
  width: var(--w);
  transform: translate(-50%, -50%) rotate(var(--rot));
  /*
     No entrance. They are drawn where they belong and that is where they start.

     They used to fade up on the shared `rise`, and `rise` ends on
     `transform: none` — which does not mean "no extra transform", it means the
     property is replaced. For the whole second and a half of the stagger every
     object was drawn with its corner on the ring point and no rotation, and
     then jumped into place when the animation released the property. An
     animation on `transform` owns all of it: the placing and the turn as much
     as the movement.
  */
}


/* Where each one hangs on the ring. In the stylesheet rather than on the
   elements: a custom property set inline beats any rule. */
.float--passport { --x: 14.8%; --y: 26.8%; --w: 17%; --rot:  -9deg; }
.float--pass     { --x: 85.2%; --y: 26.8%; --w: 27%; --rot:   8deg; }
.float--visa     { --x: 14.8%; --y: 69.2%; --w: 24%; --rot:   7deg; }
.float--ins      { --x: 85.2%; --y: 69.2%; --w: 23%; --rot:  -7deg; }
.float--dec      { --x: 25.3%; --y: 78.3%; --w: 13%; --rot:  12deg; }
.float--folder   { --x: 74.7%; --y: 78.3%; --w: 14%; --rot: -12deg; }

/* The shadow sits on the float rather than on the cover, so it stays put while
   the cover itself lifts a little under the pointer. */
.float {
  --r: 14px;
  border-radius: var(--r);
  box-shadow: 0 26px 54px rgba(12, 34, 74, 0.26);
  /* the stage above them does not take the pointer; these do, because they
     are pressed */
  pointer-events: auto;

  /*
     A shade sheer, the same as the phone beside them.

     On the object and not on the cover inside it, so the shadow under it goes
     the same amount — a solid shadow beneath a see-through object is the one
     way to make it look cut out rather than present. It is one group, so the
     grain layers and the printing do not show through each other either: the
     whole object is composited and then the sky is let through it.

     0.86, not lower. These carry six-point type at their smallest, and the
     photograph behind them has cloud edges in it; much further and the type is
     being read over something moving.

     Named, because the narrow layout has to put it back after the emptied
     stage puts it to nothing, and two hand-written 0.86s drift apart.
  */
  --sheer: 0.86;
  opacity: var(--sheer);
}

.float--pass   { --r: 16px; }
.float--visa   { --r: 10px; }
.float--ins    { --r: 24px; }
.float--dec    { --r: 6px; }
.float--folder { --r: 10px; }

.float .cover {
  width: 100%;
  box-shadow: none;
}

/*
   Pressing an object brings it to the middle and says what it is for.

   It was a sheen that swept across on hover, which looked like something and
   meant nothing — and on a touch screen it never happened at all. This does
   the job the six objects are on the page to do: you point at the one you
   want and it tells you what it holds.
*/
.float { cursor: pointer; }

/*
   Nothing on the ring moves under the pointer — not the drift, and not this.

   It was a six-pixel lift on the one you point at, which is the ordinary way to
   say a thing can be pressed. But you cannot point at one of six objects
   without crossing two others, so crossing the stage set the papers rising and
   settling behind the cursor, which is the same restlessness the drift had and
   the reason the drift went.

   What it can be pressed is said without moving it: it comes fully solid while
   the other five stay sheer, and its shadow deepens a little as if it had come
   forward. Opacity and colour, both paint — the object does not shift by a
   pixel, and neither does the type on it.
*/
.float:hover,
.float:focus-visible {
  opacity: 1;
  box-shadow: 0 30px 60px rgba(12, 34, 74, 0.32);
}

.float:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 6px;
  border-radius: var(--r);
}

/* ---- the one you pressed ---------------------------------------------- */

/*
   Press one and the stage empties: the phone and all six objects go, and what
   is left is the sentence, in the middle of the space they were using.

   They used to dim to a sixth and the object you pressed came to the centre —
   which meant reading a line of type over five ghosts of covers and a
   half-there phone. There is nothing to look at but the words now, which is
   the point of having pressed it.
*/
/* One transition for both jobs, and it has to be one: a second
   `transition` on `.float` further down would replace this, not add to it,
   and the shadow under a pointed-at object would snap. */
.float,
.phone {
  transition: opacity 0.38s ease, box-shadow 0.38s ease;
}

.stage.is-holding .float,
.stage.is-holding .phone {
  opacity: 0;
  pointer-events: none;
}

/* `is-focused` no longer moves anything — it only remembers which one was
   pressed, so focus can go back to it when the words are put away. */

/*
   The words, in the middle of the emptied stage.

   Plain type on the sky rather than a panel: what is being read is one
   sentence, and a sentence in a bordered box with its own backdrop reads as a
   dialogue that has to be dismissed. This is a caption.
*/
.focus {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  width: min(46ch, 88%);
  z-index: 31;
  text-align: center;
  /* `fade`, not `rise`: the centring translate above has to survive it. */
  animation: fade 0.42s 0.16s ease both;
}

.focus[hidden] { display: none; }

.focus__title {
  margin: 0 0 14px;
  font-family: var(--title);
  font-size: clamp(28px, 3.6vw, 40px);
  font-weight: 900;
  letter-spacing: -1px;
  line-height: 1.05;
}

.focus__body {
  margin: 0;
  font-size: clamp(17px, 1.8vw, 21px);
  line-height: 1.5;
  letter-spacing: -0.15px;
  color: var(--ink-quiet);
}

.focus__shut {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin-top: 26px;
  padding: 8px 16px 8px 13px;
  border: 1px solid var(--edge);
  border-radius: 999px;
  background: transparent;
  color: var(--ink-quiet);
  font: inherit;
  font-size: 13px;
  cursor: pointer;
  transition: border-color 0.2s ease, color 0.2s ease;
}

.focus__shut:hover { border-color: var(--edge-strong); color: var(--ink); }
.focus__shut svg { width: 11px; height: 11px; }

/* the two behind the phone, so the stack has a front and a back */
.float--dec, .float--folder { z-index: 6; }

/*
   The phone is the mockup's device, cut out of the PSD by tools/mockup.py —
   body, glass edge, buttons, camera, and a black screen — with the app's own
   screen laid into the screen's place rather than baked into the picture.

   That is the whole point of doing it this way. A mockup used the ordinary way
   is a photograph of a phone with a screenshot pasted on it, and this page's
   claim is that the phone is *running the app*: the sky behind the page and the
   sky inside the phone are painted by the same code at the same hour, and the
   cover on it is drawn, not photographed. Baking the screen in would have made
   that sentence false.

   Everything below is in two coordinate systems. The frame is the picture's,
   420 CSS pixels wide at the reference size and `--k` of that everywhere else;
   the screen is the app's own, 315 x 684, at the size its type is drawn for.
   That is the mockup's design canvas — 1260 x 2736 — quartered, and it is not
   quite the 9:19.5 a phone's screen is. Assuming the phone's ratio instead was
   wrong by a fraction of a percent, in the one direction that stretches every
   glyph on the screen.

   The matrix takes the second onto the first, in perspective, and none of it is
   measured off the picture: the mockup's smart object records the exact quad it
   places a design into, and that is what this is. All of these numbers come out
   of tools/mockup.py — do not hand-edit one without the rest, they are one
   measurement.
*/
.phone__frame {
  position: relative;
  --k: 1;
  width: 420px;
  aspect-ratio: 2242 / 4267;
  transition: transform 0.6s cubic-bezier(0.2, 0.8, 0.2, 1);

  /*
     A little sheer, so it sits in the sky rather than on top of it — the same
     thing the glass panels and the objects around it do, none of which are
     fully opaque either. It is here rather than on `.phone` because that one's
     opacity is spoken for twice over: the entrance animates it, and pressing an
     object empties the stage by taking it to nothing.

     One number, and the whole phone with it — device, glass and the app's
     screen. Turn it up if the shelf inside starts to read as washed out; the
     screen is painted at midday and the page behind it is not always.
  */
  opacity: 0.88;

  /*
     The foot of it goes into the sky rather than stopping at an edge.

     On the frame and not on the image, so the screen goes with it: fading the
     device while the app stayed solid on top of it would read as a screen
     floating free of the phone. It starts below the object on the shelf — the
     active page's middle sits at about 57% of this box — so what dissolves is
     the phone under it and not the thing it is holding up.
  */
  -webkit-mask-image: linear-gradient(
    180deg,
    #000 0%, #000 66%,
    rgba(0, 0, 0, 0.86) 78%,
    rgba(0, 0, 0, 0.5) 88%,
    rgba(0, 0, 0, 0.16) 95%,
    rgba(0, 0, 0, 0) 100%);
  mask-image: linear-gradient(
    180deg,
    #000 0%, #000 66%,
    rgba(0, 0, 0, 0.86) 78%,
    rgba(0, 0, 0, 0.5) 88%,
    rgba(0, 0, 0, 0.16) 95%,
    rgba(0, 0, 0, 0) 100%);
}

/* The PSD's own shadow layer is not used: it is built to fall on a flat grey
   backdrop, and this phone stands on a watercolour sky. One is cast from the
   silhouette instead, on .phone — see the note there. */
.phone__device {
  display: block;
  width: 100%;
  height: auto;
}

/* from tools/mockup.py — see the note above */
.phone__screen {
  position: absolute;
  top: 0;
  left: 0;
  width: 315px;
  height: 684px;
  /* The placed design is a rectangle and the display is not: without a corner
     the screen stands outside the phone at all four of them, worst down the
     near-vertical left edge where the perspective packs it. */
  border-radius: 44px;
  overflow: hidden;
  background: #cfe0f4;
  /* keeps the grain's multiply against the screen and not the page behind it */
  isolation: isolate;
  transform-origin: 0 0;
  transform: scale(var(--k))
    matrix3d(0.709205, 0.088261, 0, -0.000156,
             -0.239804, 1.050299, 0, -0.000003,
             0, 0, 1, 0,
             161.079967, 6.835022, 0, 1);
}

/*
   The island itself — the hardware, not the button below that turns it on. At
   the proportions it has on the device the mockup is of: 125 x 36 points, 11
   down, on a screen of 393 x 852. It is drawn here rather than left to the
   picture because the picture has a hole where it goes — the mockup expects
   whatever is put on the screen to draw its own.
*/
.phone__notch {
  position: absolute;
  left: 50%;
  top: 9px;
  width: 100px;
  height: 29px;
  margin-left: -50px;
  border-radius: 15px;
  background: #000;
}

.phone__sky {
  position: absolute;
  inset: 0;
}

.phone__sky svg { display: block; width: 100%; height: 100%; }

/*
   What the app puts over the shelf.

   Every number here is the app's own at 315/393 of its size, which is what this
   screen is to the phone the mockup is of: a 32-point wordmark comes out at
   25.6, a 16-point line under it at 12.8, and the header starts a safe area and
   eight points down. The two colours are the midday palette's `text` and
   `subtext`, since that is the hour this screen is fixed at.

   One departure: the app lays its top wash on at full opacity in the sky's own
   top colour, and here it stops at 0.88. The app's sky is a smooth gradient and
   ours is a wash on paper — a solid band of the same colour flattens the paint
   underneath it, and the point of painting the sky properly was to be able to
   see that it is paint.
*/
.phone__top {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  height: 240px;
  padding: 54px 13px 0;
  text-align: center;
  pointer-events: none;
  background: linear-gradient(
    180deg,
    rgba(121, 179, 242, 0.88) 0%,
    rgba(121, 179, 242, 0.88) 50%,
    rgba(121, 179, 242, 0.72) 68%,
    rgba(121, 179, 242, 0.38) 80%,
    rgba(121, 179, 242, 0.08) 90%,
    rgba(121, 179, 242, 0) 100%);
}

.phone__mark {
  margin: 0 0 3px;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  font-family: var(--title);
  font-size: 25.6px;
  font-weight: 900;
  letter-spacing: 0.4px;
  line-height: 1.1;
  color: #0b1f38;
}

.phone__mark img {
  width: 22px;
  height: 22px;
  margin-top: -2px;
}

.phone__say {
  margin: 0;
  font-size: 12.8px;
  line-height: 19px;
  color: #3c5878;
}

/* The button that puts the documents on the island — which is the thing the
   grid further down the page is about. */
.phone__toggle {
  position: absolute;
  top: 54px;
  right: 13px;
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 5px 8px;
  border-radius: 999px;
  border: 1px solid #3c5878;
  background: rgba(255, 255, 255, 0.35);
  font-size: 9.6px;
  font-weight: 700;
  letter-spacing: 0.2px;
  color: #3c5878;
}

.phone__toggle svg {
  width: 10px;
  height: 10px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
}

.phone__foot {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 88px;
  pointer-events: none;
  background: linear-gradient(
    180deg,
    rgba(234, 244, 255, 0) 0%,
    rgba(234, 244, 255, 0.25) 35%,
    rgba(234, 244, 255, 0.6) 70%,
    rgba(234, 244, 255, 0.85) 100%);
}

/* the app's own screen: the shelf of objects, sliding through itself */
.phone__ui {
  position: absolute;
  inset: 0;
  overflow: hidden;
}

/*
   The shelf.

   The app's home screen is a vertical carousel: one object to a page, the
   neighbours showing above and below at nine tenths scale, and it pages when
   you push it. The numbers here are its numbers — `ITEM_HEIGHT_RATIO` is 0.68
   of the screen, `parallaxAdjacentItemScale` is 0.9 — so a slot is 442 of the
   screen's 650, and 104 of the next one shows at each edge.

   This is the one thing on the page that moves without being touched, and it
   is deliberate: a phone in a hero shot with a still screen is a photograph of
   an app, and the point of this one is that it is running. It moves slowly,
   never sharply, and stops entirely for anyone who has asked for less motion —
   at which point it rests on the first slot, which is where a reader without
   JavaScript finds it too.

   The seventh slot is a copy of the first, put there by covers.js. The reel
   slides through six steps onto it and then starts over from the real one,
   which is the same picture in the same place — so the loop has no seam and
   nothing ever slides backwards through five categories to get home.
*/
.phone__reel {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  /*
     Slot 0, centred in the room left under the header rather than in the
     screen: the app's carousel is a flex child with `marginTop: 170`, so the
     middle of the shelf sits at 136 + (684 - 136) / 2 = 410, and a page is
     465 tall — 410 - 232.5.
  */
  transform: translateY(177.5px);
}

.slot {
  height: 465px;
  /* 10 of the app's 393 points, each side. Without it the wide objects — the
     ticket, the visa, the folder — run to the glass on both sides and the
     screen reads as edge to edge; `widthPct` is a fraction of what is left
     after this, not of the screen. */
  padding: 0 2.545%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  transform: scale(1);
}

/* the title hangs just above the cover, whatever height the object is, which
   is what the app's `shelfTitle` box does by pinning the arch to its foot */
.slot .arch { width: 86%; }

/*
   Each object at its own width, which is `widthPct` from the app's theme.ts:
   the passport is a small book and the folder is a full-width wallet, and the
   difference between them is most of what tells the six apart at a glance.

   This screen used to hold one cover at a flat two thirds. That was right for
   one cover centred on a wallpaper and wrong for a shelf — at the app's own
   widths the object fills its page, and the pages above and below show at the
   edges, which is the thing that says there are more of them.
*/
.slot[data-slot="passport"] .cover { width: 60%; }
.slot[data-slot="pass"]     .cover { width: 100%; }
.slot[data-slot="visa"]     .cover { width: 100%; }
.slot[data-slot="ins"]      .cover { width: 96%; }
.slot[data-slot="dec"]      .cover { width: 88%; }
.slot[data-slot="folder"]   .cover { width: 100%; }

/*
   Only while the phone is on screen. Both of these are composited transforms
   and cost nothing to look at, but they cost something to run, and a shelf
   paging away to itself under four sections of text is a battery bill with no
   reader — site.js puts the class on when the stage comes into view and takes
   it off when it leaves.
*/
.phone__reel.is-playing {
  will-change: transform;
  animation: shelf-slide 19.2s ease-in-out infinite;
}

.phone__reel.is-playing .slot {
  animation: shelf-front 19.2s ease-in-out infinite;
}

/* Each slot is at its full size for its own page of the cycle and nine tenths
   for the other five. One set of frames, phase-shifted per slot: the shift is
   negative so every one of them is already where it should be on the first
   pass rather than waiting a cycle out. */
.phone__reel.is-playing .slot:nth-child(1) { animation-delay: -19.2s; }
.phone__reel.is-playing .slot:nth-child(2) { animation-delay: -16s; }
.phone__reel.is-playing .slot:nth-child(3) { animation-delay: -12.8s; }
.phone__reel.is-playing .slot:nth-child(4) { animation-delay: -9.6s; }
.phone__reel.is-playing .slot:nth-child(5) { animation-delay: -6.4s; }
.phone__reel.is-playing .slot:nth-child(6) { animation-delay: -3.2s; }
.phone__reel.is-playing .slot:nth-child(7) { animation-delay: 0s; }

/*
   Six pages, 3.2 seconds each: two and a half still, then three quarters of a
   second moving. The last frame lands on the seventh slot, which is the first
   one over again, so starting the cycle from the top is not a movement.
*/
@keyframes shelf-slide {
  0%,      12.5%   { transform: translateY(177.5px); }
  16.667%, 29.167% { transform: translateY(-287.5px); }
  33.333%, 45.833% { transform: translateY(-752.5px); }
  50%,     62.5%   { transform: translateY(-1217.5px); }
  66.667%, 79.167% { transform: translateY(-1682.5px); }
  83.333%, 95.833% { transform: translateY(-2147.5px); }
  100%             { transform: translateY(-2612.5px); }
}

@keyframes shelf-front {
  0%,      12.5%   { transform: scale(1); }
  16.667%, 95.833% { transform: scale(0.9); }
  100%             { transform: scale(1); }
}

.arch {
  width: 100%;
  height: auto;
  flex: none;
}

/*
   The same title, set a letter at a time, which is what covers.js swaps the
   path version for. Each letter is seated at its own point on the arc and
   turned to sit square on it; the glyph inside the seat is the part that
   moves, so one set of frames does for all of them and the placement is not
   something an animation can throw away.
*/
.arch--live {
  position: relative;
  flex: none;
}

.arch--live i {
  position: absolute;
  left: 50%;
  top: 0;
  width: 27.5px;          /* the app's box: the type size and a quarter */
  margin-left: -13.75px;
  font-style: normal;
  text-align: center;
}

.arch--live b {
  /* inline-block, not block: covers.js measures these to space them, and a
     block fills its seat — every letter would come back the same width and the
     measuring would be measuring the seat */
  display: inline-block;
  font-family: var(--title);
  font-size: 22px;
  font-weight: 900;
  line-height: 1.24;
  color: #020403;
}

/*
   A title arrives when its page does, a letter at a time, and goes when the
   page starts to leave — `active` on the app's own ArchTitle, which is true
   only for the item in the middle. The delay per letter is written on it by
   covers.js: which page it belongs to, plus its own place in the queue.
*/
.phone__reel.is-playing .arch--live b {
  animation: glyph 19.2s ease-out infinite;
}

@keyframes glyph {
  0%            { opacity: 0; transform: translateY(12px) scale(0.86); }
  2.083%, 12.5% { opacity: 1; transform: none; }
  13.5%, 100%   { opacity: 0; transform: none; }
}

/* Set on an arc, as the app sets it — the app steps letter by letter along the
   curve because React Native cannot measure a glyph; a browser can, so this is
   one textPath doing the same job. */
.arch__t {
  font-family: var(--title);
  font-size: 20px;
  font-weight: 900;
  letter-spacing: 0.3px;
  fill: #020403;
}

/* ------------------------------------------------------------ the covers */

/*
   One box for all six, at each object's own proportion and corner radius —
   taken from OBJECT_SPECS in the app's theme.ts, so a cover here is the shape
   it is there.
*/
.cover {
  position: relative;
  overflow: hidden;
  border-radius: 14px;
  box-shadow: 0 22px 44px rgba(14, 34, 70, 0.22);
  /* scales every rule and caption inside with the cover */
  --u: 1;
  color: #1c1c1e;
}

.cover p { margin: 0; }

/*
   Two grain layers per cover, because that is how many the app lays on: a
   material (paper, linen) and something over it (an ordered dither, a fine
   noise). Two pseudo-elements rather than two background layers on one, since
   each has to carry its own opacity and a background layer cannot.

   Over the printing, not under it — grain that the type sits on top of reads as
   a pattern behind glass rather than as the surface the type was printed on.
*/
.cover::before,
.cover::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 4;
  pointer-events: none;
  background-repeat: repeat;
}

.cover::before {
  background-image: var(--tex-1);
  background-size: var(--tex-1-size);
  opacity: var(--tex-1-o, 0);
}

.cover::after {
  background-image: var(--tex-2);
  background-size: var(--tex-2-size);
  opacity: var(--tex-2-o, 0);
}

/* ---- passport: grey uncoated board, the world in halftone --------------- */

.cover--passport {
  --tex-1: var(--tex-noise); --tex-1-size: 37px 37px; --tex-1-o: 0.75;
  aspect-ratio: 0.72;
  border-radius: calc(14px * var(--u));
  background: linear-gradient(150deg, #94948f 0%, #8a8a85 50%, #7e7e7a 100%);
}

.world {
  position: absolute;
  left: 0;
  right: 0;
  top: 30%;
  bottom: 29%;
}

.world svg { display: block; width: 100%; height: 100%; }

.pp__titles {
  position: absolute;
  left: 12%;
  right: 8%;
  top: 11%;
}

.pp__titles p {
  font-size: calc(12px * var(--u));
  line-height: calc(15px * var(--u));
  font-weight: 500;
  letter-spacing: -0.1px;
  color: #ffffff;
}

.pp__foot {
  position: absolute;
  left: 12%;
  right: 8%;
  bottom: 8%;
  display: flex;
  gap: calc(14px * var(--u));
  text-align: left;
}

.pp__foot > div { flex: 1; }

.pp__line {
  font-size: calc(6.5px * var(--u));
  line-height: calc(9px * var(--u));
  color: rgba(255, 255, 255, 0.82);
}

.pp__strong {
  font-size: calc(6.5px * var(--u));
  line-height: calc(9px * var(--u));
  font-weight: 700;
  color: #ffffff;
}

/* ---- boarding pass ------------------------------------------------------ */

.cover--pass {
  --tex-1: var(--tex-paper); --tex-1-size: 26px 14px; --tex-1-o: 0.45;
  --tex-2: var(--tex-halftone); --tex-2-size: 12px 12px; --tex-2-o: 0.14;
  aspect-ratio: 2.1;
  border-radius: 16px;
  background: rgba(255, 255, 255, 0.96);
  padding: 12px 16px;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
}

.bp__head { display: flex; align-items: center; gap: 7px; }

.bp__logo {
  width: 18px;
  height: 18px;
  border-radius: 9px;
  background: #34c759;
  display: grid;
  place-items: center;
}

.bp__logo svg { width: 11px; height: 11px; fill: #fff; }
.bp__airline { font-size: 12px; font-weight: 600; }
.bp__type { margin-left: auto; font-size: 10px; color: #8e8e93; }
.bp__rule { height: 1px; background: rgba(60, 60, 67, 0.16); margin-top: 8px; }

.bp__route { display: flex; align-items: center; justify-content: space-between; }
.bp__to { text-align: right; }
.bp__code { font-size: 28px; font-weight: 700; letter-spacing: 0.2px; }
.bp__city { font-size: 9px; color: #8e8e93; margin-top: 1px; }
.bp__plane { width: 15px; height: 15px; fill: #8e8e93; }

.bp__fields {
  display: flex;
  justify-content: space-between;
  padding-right: 12px;
  /* The route's city names sit right above these captions, and at this width
     the two lines of small caps ran together. */
  margin-top: 4px;
}
.bp__k {
  font-size: 8px;
  letter-spacing: 0.6px;
  font-weight: 600;
  color: #aeaeb2;
  margin-bottom: 2px;
}
.bp__v { font-size: 12px; font-weight: 600; }

.bp__dashed { border-top: 1px dashed rgba(60, 60, 67, 0.28); }

.bp__barcode { display: flex; gap: 1.5px; height: 26px; justify-content: center; }
.bp__barcode i { display: block; background: #1c1c1e; }

/* ---- visa: near-white security stock ------------------------------------ */

.cover--visa {
  --tex-1: var(--tex-linen); --tex-1-size: 6px 6px; --tex-1-o: 0.6;
  --tex-2: var(--tex-halftone); --tex-2-size: 12px 12px; --tex-2-o: 0.14;
  aspect-ratio: 1.45;
  border-radius: 10px;
  background: linear-gradient(140deg, #f4fbff 0%, #eef4fd 34%, #fdf7ef 68%, #fdeff5 100%);
}

/* Security printing drifts through several hues rather than shading one. */
.vs__band {
  position: absolute;
  left: -30px;
  right: -30px;
  height: 30px;
  background: rgba(59, 130, 246, 0.05);
  transform: rotate(-8deg);
}

.vs__band--a { top: 30%; }
.vs__band--b { top: 58%; }

.vs__ticks {
  position: absolute;
  top: 6px;
  left: 14px;
  right: 14px;
  height: 5px;
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
}

.vs__ticks i { width: 1px; height: 2.5px; background: rgba(39, 70, 144, 0.32); }
.vs__ticks i.tall { height: 5px; background: rgba(39, 70, 144, 0.5); }

/* Letter under letter rather than turned on its side: rotated type has to be
   given a width to turn within, and it lies about its bounds. */
.vs__edge {
  position: absolute;
  right: 5px;
  top: 34%;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 5px;
  line-height: 6.5px;
  text-align: center;
  color: rgba(39, 70, 144, 0.3);
}

.vs__body {
  position: absolute;
  inset: 0;
  padding: 18px 16px 16px;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
}

.vs__head { display: flex; justify-content: space-between; align-items: flex-start; }
.vs__country {
  font-size: 9px;
  letter-spacing: 1.2px;
  font-weight: 600;
  color: #6b8ec9;
  margin-bottom: 3px;
}
.vs__word { font-size: 26px; font-weight: 700; letter-spacing: 3px; color: #1e3a6e; }
.vs__alt { font-size: 7.5px; color: #7d92b8; margin-top: 2px; }

.vs__holo {
  width: 34px;
  height: 34px;
  border-radius: 6px;
  background: linear-gradient(135deg, #fda4af, #fde68a, #86efac, #93c5fd, #c4b5fd);
  /* Caught at the angle the app's own shimmer passes through, and left there.
     Nothing on this page moves while you are not touching it. */
  opacity: 0.8;
  transform: rotate(-4deg);
}

.vs__rule { height: 1px; background: rgba(39, 70, 144, 0.18); }

.vs__fields { display: flex; gap: 10px; }
.vs__fields > div { flex: 1; min-width: 0; }
.vs__k {
  font-size: 6px;
  color: #8aa0c4;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.vs__v { font-size: 11px; font-weight: 600; color: #1e3a6e; margin-top: 1px; }

.vs__mrz {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 8px;
  line-height: 11px;
  letter-spacing: 0.6px;
  color: #33456b;
  white-space: nowrap;
  overflow: hidden;
}

/* ---- insurance: a letterpress tag on uncoated ivory --------------------- */

.cover--ins {
  --tex-1: var(--tex-paper); --tex-1-size: 26px 14px; --tex-1-o: 0.6;
  --tex-2: var(--tex-noise); --tex-2-size: 37px 37px; --tex-2-o: 0.22;
  aspect-ratio: 1.586;
  border-radius: 24px;
  background: linear-gradient(150deg, #f4f0e5 0%, #eee9db 50%, #e7e1d2 100%);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  color: #3b3830;
}

/* Sized by width and ratio so the whole drawing lands inside its box, and set
   far enough back that the rules read over it. */
.ins__mark {
  position: absolute;
  right: 7%;
  top: 6%;
  width: 10%;
  height: auto;
}

.ins__rules { position: absolute; inset: 0; width: 100%; height: 100%; }

.ins__head { position: absolute; left: 6%; top: 8%; }
.ins__head p { font-size: 7px; line-height: 10px; letter-spacing: 1.9px; }

.ins__slashes {
  position: absolute;
  left: 6%;
  /* Clear of the first ruled line and of the field under it: the descenders in
     "Repatriation" were landing on the rule. */
  top: 25%;
  font-size: 6.5px;
  letter-spacing: 0.5px;
  color: rgba(59, 56, 48, 0.7);
}

.ins__stub {
  position: absolute;
  left: 6%;
  font-size: 6px;
  letter-spacing: 0.4px;
  color: rgba(59, 56, 48, 0.62);
}

.ins__field {
  position: absolute;
  left: 23%;
  font-size: 7px;
  letter-spacing: 0.4px;
}

.ins__foot {
  position: absolute;
  left: 6%;
  right: 6%;
  bottom: 6%;
  display: flex;
  align-items: baseline;
  gap: 10px;
}

.ins__label { font-size: 7.5px; letter-spacing: 1.6px; }
.ins__note {
  font-size: 6px;
  letter-spacing: 0.2px;
  color: rgba(59, 56, 48, 0.6);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ---- declarations: continuous stationery -------------------------------- */

.cover--dec {
  --tex-1: var(--tex-paper); --tex-1-size: 26px 14px; --tex-1-o: 0.5;
  aspect-ratio: 0.74;
  border-radius: 6px;
  background: #fbfcf6;
  display: flex;
  color: #22301c;
}

.dec__sprocket {
  position: relative;
  width: 22px;
  flex: none;
  padding: 5px 0;
  display: flex;
  flex-direction: column;
}

.dec__hole {
  flex: 1;
  display: block;
  align-self: center;
  width: 7px;
  background:
    radial-gradient(circle at 50% 50%, #15170f 0 3.5px, transparent 3.5px);
}

.dec__tear {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 1px;
  padding: 3px 0;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
}

.dec__tear--left { left: 0; }
.dec__tear--right { right: 0; }
.dec__tear i { display: block; width: 1px; height: 3px; background: rgba(32, 48, 28, 0.3); }

.dec__body { flex: 1; display: flex; flex-direction: column; overflow: hidden; }

.dec__row {
  flex: 1;
  display: flex;
  align-items: center;
  padding: 0 4px;
  min-height: 0;
}

.dec__row.is-bar { background: #dcead2; }

.dec__no {
  width: 12px;
  margin-right: 5px;
  text-align: right;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 5px;
  color: rgba(40, 62, 38, 0.5);
}

.dec__line {
  flex: 1;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 6.4px;
  line-height: 9px;
  white-space: pre;
  overflow: hidden;
}

/* ---- folder: dyed card stock -------------------------------------------- */

.cover--folder {
  --tex-1: var(--tex-paper); --tex-1-size: 26px 14px; --tex-1-o: 0.5;
  --tex-2: var(--tex-noise); --tex-2-size: 37px 37px; --tex-2-o: 0.35;
  aspect-ratio: 0.84;
  border-radius: 10px;
  background: linear-gradient(150deg, #7a2f3a 0%, #6d2933 100%);
  color: #f4e6e8;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}

.fd__print { position: absolute; inset: 0; width: 100%; height: 100%; }

.fd__row {
  position: absolute;
  left: 53%;
  right: 9%;
  display: flex;
  align-items: center;
  gap: 6px;
}

.fd__k { font-size: 5.4px; letter-spacing: 0.7px; color: rgba(255, 255, 255, 0.5); }
.fd__v {
  font-size: 6.6px;
  color: #ffffff;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

.fd__contents {
  position: absolute;
  left: 7%;
  top: 38%;
  font-size: 5.4px;
  letter-spacing: 1.4px;
  color: rgba(255, 255, 255, 0.5);
}

/* ------------------------------------------------------------ what it does */

/*
   Three screens of the app, one sentence each, the picture running off the
   bottom of its card.

   This row used to be six tiles with the thing drawn in each — the shelf, a
   card being assembled, five stamps coming down — every one of them built out
   of this stylesheet and faithful to the app's own constants. They are gone,
   and it is worth saying why, because the drawings were the better piece of
   work and the screenshots are the better page.

   A drawing of a shelf argues that the app has a shelf. A screenshot of the
   shelf *is* the shelf: the real type, the real watercolour behind it, the
   real amount of air around a boarding pass. Anyone deciding whether to
   install this is asking what it looks like, and there is exactly one honest
   answer to that question.

   What the drawings still do better is the island, which has no screenshot at
   all — it happens on the outside of the phone, above the app — so that one
   stayed, in the band underneath.
*/

/*
   The heading and its sentence on one line, sitting on a shared baseline at
   the foot rather than stacked.

   Scoped to a wrapper and not to `.sheet h2` + `.lede`, which every other
   section on the page also uses: this row is three columns, and a heading over
   a paragraph over three cards is four things all starting at the same left
   edge. The other sheets are one column of prose and want the stack.

   `align-items: end` is what makes it read as one line — the two have very
   different line heights, so aligning their tops leaves the paragraph
   floating, and aligning their middles leaves it nowhere.
*/
.lead {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  align-items: end;
  gap: 10px clamp(24px, 4vw, 56px);
}

.lead h2 { margin: 0; max-width: 14ch; }

/* A shade off the bottom: the heading's descenders sit below its last
   baseline, and matching the box edges puts the paragraph visibly low. */
.lead .lede { margin: 0 0 5px; max-width: 44ch; }

.shots {
  margin: 40px 0 0;
  display: grid;
  gap: 16px;
  grid-template-columns: repeat(3, 1fr);
}

/*
   `overflow: hidden` is the whole trick: the picture is taller than the space
   left for it and the card simply cuts it. Nothing is faded, nothing is
   masked — a screen carries on past the bottom edge the way it carries on past
   the bottom of a phone you are holding.
*/
.shot {
  position: relative;
  display: flex;
  flex-direction: column;
  padding: 30px 28px 0;
  border-radius: 22px;
  background: var(--film);
  border: 1px solid var(--edge);
  backdrop-filter: blur(12px) saturate(1.2);
  -webkit-backdrop-filter: blur(12px) saturate(1.2);
  overflow: hidden;
  transition: border-color 0.3s ease;
}

.shot:hover { border-color: var(--edge-strong); }

/*
   A wash of the screen's own colour, rising to meet it.

   Each is the accent the app gives that category in theme.ts — the sky the
   onboarding opens on, the boarding pass's green, the stamp page's cream —
   so the card is tinted by the thing inside it rather than by a palette
   invented for this page.
*/
.shot::before {
  content: '';
  position: absolute;
  inset: auto 0 0;
  height: 62%;
  pointer-events: none;
  background: linear-gradient(180deg, rgba(255, 255, 255, 0) 0%, var(--wash) 100%);
}

.shot--objects { --wash: rgba(151, 190, 236, 0.34); }
.shot--passes  { --wash: rgba(122, 197, 156, 0.34); }
.shot--places  { --wash: rgba(226, 205, 160, 0.40); }

/* Over the wash, or the last line of the paragraph reads through it. */
.shot h3,
.shot p,
.shot__phone { position: relative; z-index: 1; }

.shot h3 {
  margin: 0 0 10px;
  font-family: var(--title);
  font-size: clamp(27px, 2.5vw, 36px);
  font-weight: 900;
  letter-spacing: -1px;
  line-height: 1.05;
}

.shot p {
  /*
     The gap under the paragraph is stated, not left over. The picture is
     bottom-aligned with an `auto` top margin, which absorbs whatever room the
     card has spare — and once the three paragraphs were trimmed to the same
     three lines there was none spare, so the phone came up hard against the
     last line of type. This is the floor; the auto margin still takes anything
     above it, so the three pictures stay on one line as the text lengths drift.
  */
  margin: 0 0 clamp(24px, 3vw, 40px);
  font-size: 15px;
  line-height: 1.56;
  color: var(--ink-quiet);
}

/*
   Bottom-aligned, so three cards with paragraphs of different lengths still
   start their pictures on the same line. The height is the window, not the
   picture: the image inside is taller and overflows it.
*/
.shot__phone {
  margin: auto auto 0;
  width: 82%;
  /*
     A ratio, not a height.

     How much of a screen you can see is the window's height *over the phone's
     width*, so a fixed height crops more of the screen the wider the phone
     gets — and the phone gets wider as the row loses columns. Written as a
     height in px this was fine at three across and cut the boarding pass in
     half at two, which is the one thing the middle card is about.

     1.26 is set by the lowest subject of the three: the app's shelf centres a
     cover well down the screen. One ratio for all three, set by the worst
     case, or they stop being a row.
  */
  aspect-ratio: 1 / 1.26;
  overflow: hidden;
  /* the device, drawn — the screenshot has the island in it but no bezel */
  padding: 6px 6px 0;
  border-radius: 40px 40px 0 0;
  background: linear-gradient(165deg, #46536b 0%, #1d2634 42%, #0d131d 100%);
  box-shadow:
    0 -1px 0 rgba(255, 255, 255, 0.35) inset,
    0 18px 40px rgba(12, 30, 62, 0.3);
}

.shot__phone img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: 34px 34px 0 0;
}

/* ---- the island, on the outside of the phone ---------------------------- */

/*
   Full width under the three, because it is the one thing here that is not a
   screen in the app — it is the app on the outside of a locked phone, and a
   third of a row would have read as a fourth feature rather than as the
   difference it is.
*/
.band {
  margin: 16px 0 0;
  display: grid;
  grid-template-columns: 1fr 1fr;
  align-items: center;
  gap: 8px 30px;
  padding: 30px 28px 0;
  border-radius: 22px;
  background: var(--film);
  border: 1px solid var(--edge);
  backdrop-filter: blur(12px) saturate(1.2);
  -webkit-backdrop-filter: blur(12px) saturate(1.2);
  overflow: hidden;
  transition: border-color 0.3s ease;
}

.band:hover { border-color: var(--edge-strong); }

.band__say { padding-bottom: 30px; }

.band__mark {
  display: grid;
  place-items: center;
  width: 30px;
  height: 30px;
  margin-bottom: 12px;
  border-radius: 9px;
  border: 1px solid var(--edge);
  background: rgba(255, 255, 255, 0.3);
}

.band__mark svg {
  width: 17px;
  height: 17px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
  opacity: 0.82;
}

.band h3 {
  margin: 0 0 10px;
  font-family: var(--title);
  font-size: clamp(24px, 2.2vw, 32px);
  font-weight: 900;
  letter-spacing: -0.8px;
  line-height: 1.08;
}

.band p {
  margin: 0;
  font-size: 15px;
  line-height: 1.56;
  color: var(--ink-quiet);
}

/* Same rule as the screenshots: it runs off the bottom edge. */
.band__art {
  position: relative;
  align-self: end;
}

/* ---- the island --------------------------------------------------------- */

/*
   A photograph of the top of a phone, with the activity opening on the real
   Dynamic Island in it.

   The phone used to be drawn too — a dark rounded panel with a status bar of
   its own. It is a screenshot of the simulator's home screen now, taken with
   the app installed and its badge on the icon, and the only thing still drawn
   is the activity itself, laid over the real island. Which is the honest
   division: the opened activity cannot be photographed here, because the pass
   it would show is built from a barcode and Vision does not detect barcodes on
   a simulator — but everything around it can be, so it is.

   THE PICTURE. Percentages taken off it with numpy, not eyeballed. The crop
   (`assets/shot-island.webp`, the capture boxed at 128,200,919,820) is the
   device's silhouette and nothing else — 790 x 620 of capture, written out at
   840 x 658. Against it:

       island  left   31.61%      top      8.06%
               width  35.65%      height   9.84%
               centre 49.37% across — the fifth of a percent it sits left of
                                      the middle is the phone's, not the crop's

       corner  133px of the device's 790 = 16.81% of the width, which is
               21.45% of the height — the two have to be given separately or a
               percentage radius comes out elliptical on a box that is not
               square

       glass   736px of the device's 790, and an iPhone 17 Pro Max screen is
               440pt across — so the picture carries 1.6727 capture pixels to
               the point. Two checks on that: the island comes out 168.6pt
               wide, which is a compact island carrying content, and 36.5pt
               tall against Apple's 37.33. Both land.

   THE ACTIVITY. Because that scale is known, nothing below is invented — every
   size is the app's own number in points, multiplied by `--pt`. From
   `targets/island/TraveletLiveActivity.swift`:

       maxCodeHeight  108      the expanded island's, not the Lock Screen's 120
       BarcodeView    +4 padding all round, corner 8
       StepButton     44, Apple's minimum target, with a 17 chevron
       HStack spacing 8
       sensor band    37, which is the island's own height — the leading and
                      trailing labels share it with the camera

   which makes the bottom row 44 + 8 + 116 + 8 + details + 8 + 44 across, with
   10 of inset either side of it, and 37 + 116 down. The details column is the
   only free number: 132pt, which is what two lines of the 13pt document name
   need. 380 x 153 in total.

   Change the crop and every number in the first group is wrong; re-measure,
   do not nudge. Change the app and the second group is wrong; read it off the
   Swift again.
*/
.isle {
  --tint: #16a34a;
  position: relative;
  /* a phone's width, centred. Without the cap the island stretches the width
     of the grid and the code comes out larger than any phone shows it. */
  width: min(100%, 420px);
  margin: 0 auto;
  aspect-ratio: 840 / 658;

  /*
     Everything drawn on the photograph is sized in `cqw` — hundredths of this
     box's width — so it scales with the picture instead of staying 44px wide
     on a 260px phone.

     `--pt` is one point of the phone's screen expressed in those: 1pt is
     1.6727 capture pixels, the picture is 790 capture pixels across, so
     1pt = 100 * 1.6727 / 790 = 0.21173cqw. It is what lets the rules below be
     the app's own numbers rather than numbers that look about right.
  */
  container-type: inline-size;
  --pt: 0.21173cqw;
}

.isle__shot {
  display: block;
  width: 100%;
  height: auto;
  /*
     The device's own corner. The capture is a rectangle and the phone in it is
     not, so without this the black the simulator renders outside the frame
     stays behind as four hard corners — which is exactly what it looks like: a
     photograph of a phone on a black card, beside three phones with rounded
     tops. Only the top two are given; the bottom is masked away below.
  */
  border-radius: 16.81% 16.81% 0 0 / 21.45% 21.45% 0 0;
  /* the phone runs off the bottom of the band rather than ending in a line,
     which would read as a picture of a phone cut in half */
  -webkit-mask-image: linear-gradient(#000 74%, rgba(0, 0, 0, 0));
  mask-image: linear-gradient(#000 74%, rgba(0, 0, 0, 0));
}

/* ---- the invitation ----------------------------------------------------- */

/*
   A button the exact size and shape of the island in the photograph, sitting
   on it, pulsing.

   It is a `<button>` and not a div because it is the one thing in this band
   that does something: it has to be reachable by keyboard and it has to say
   what it does. It is also the only thing that takes the pointer — hovering
   the band's paragraph should not open an island three hundred pixels away.

   Nothing is drawn in its resting state. The ring is a pseudo-element that
   scales out and fades, which is a transform and an opacity, so the pulse
   costs a composite and not a paint.
*/
.isle__ping {
  position: absolute;
  left: 31.61%;
  top: 8.06%;
  width: 35.65%;
  height: 9.84%;
  padding: 0;
  border: 0;
  border-radius: 999px;
  background: none;
  cursor: pointer;
  z-index: 2;
}

.isle__ping::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: 999px;
  border: 0.36cqw solid var(--tint);
  opacity: 0;
}

.is-idle .isle__ping::after {
  animation: ping 2.6s cubic-bezier(0.2, 0.7, 0.3, 1) infinite;
}

/* Not shown once the thing it was pointing at has happened. */
.isle__ping:focus-visible {
  outline: 2px solid var(--tint);
  outline-offset: 4px;
}

@keyframes ping {
  0%        { transform: scale(1);    opacity: 0.7; }
  60%, 100% { transform: scale(1.5);  opacity: 0; }
}

/* ---- the activity ------------------------------------------------------- */

/*
   Opened, at rest, sitting exactly on the island in the photograph.

   Open is the resting state on purpose. With no JavaScript, and for a reader
   who has asked for less motion, the card is simply there — the same rule the
   rest of the page follows, that the resting state is the finished picture.
   What the script does is add `is-idle`, which closes it and starts the pulse;
   pointing at the island takes `is-idle` off again.

   Closing and opening is a transition on `clip-path` rather than a keyframe,
   so it runs in both directions from wherever it happens to be — a keyframe
   would snap back when the pointer left. A clip is not a layout either, so the
   code and the caption inside are never measured twice.
*/
.isle__pill {
  position: absolute;
  left: 49.37%;
  top: 8.06%;
  transform: translateX(-50%);
  width: calc(380 * var(--pt));
  /*
     37 of sensor band, 116 of code, and 14 of air under it. Without the last
     the code sits on the card's bottom edge, inside its corner radius — which
     the system never does, and which reads as a card that ran out of room.
     PassBody's own cap is 160 and the band is not part of it: 116 + 14 is
     well inside that.
  */
  height: calc(167 * var(--pt));
  padding-bottom: calc(14 * var(--pt));
  background: #000;
  color: #fff;
  clip-path: inset(0px 0px 0px 0px round calc(28 * var(--pt)));
  box-shadow: 0 12px 26px rgba(0, 0, 0, 0.45);
  transition:
    clip-path 520ms cubic-bezier(0.2, 0.8, 0.2, 1),
    opacity 180ms ease;
}

/*
   Shut, which is the island's own box expressed against the card's:

     in from each side   (380 - 168.6) / 2 / 380 = 27.82%
     up from the bottom  (167 - 36.5) / 167      = 78.14%

   — the island's 168.6 x 36.5pt being the measurement off the photograph, so
   the two edges of this animation are the same island.

   And invisible with it. The compact island is *in the photograph*, complete
   with the app's aeroplane and its count, so there is nothing to draw: this
   used to redraw it on top of itself, which is work done to arrive back at the
   picture. What is left is a card that fades in over the real island as it
   grows out of it — and since both are black, the fade is invisible and what
   you see is the island opening.
*/
.is-idle .isle__pill {
  clip-path: inset(0% 27.82% 78.14% 27.82% round 999px);
  opacity: 0;
}

/* the sensor band: labels only, because the camera is in the middle of it */
.isle__top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  height: calc(37 * var(--pt));
  padding: 0 calc(14 * var(--pt));
}

.isle__cat {
  font-size: calc(11 * var(--pt));
  font-weight: 700;
  color: var(--tint);
}

.isle__pos { font-size: calc(11 * var(--pt)); color: rgba(255, 255, 255, 0.6); }

/* Both states of a line sit in one grid cell so they cross-fade in place
   rather than nudging it as they swap. */
.isle__pos,
.isle__name,
.isle__meta { display: grid; }

.isle__pos i,
.isle__name i,
.isle__meta i { grid-area: 1 / 1; font-style: normal; }

.isle__pos i { justify-self: end; }

/* the bottom region: arrow, code, details, arrow */
.isle__row {
  display: flex;
  align-items: center;
  gap: calc(8 * var(--pt));
  padding: 0 calc(10 * var(--pt));
}

/* 44pt in the app, for the reason given there: a near-miss on a smaller target
   falls through to the activity itself and opens the app. */
.isle__step {
  flex: none;
  width: calc(44 * var(--pt));
  height: calc(44 * var(--pt));
  border-radius: 50%;
  display: grid;
  place-items: center;
  background: rgba(22, 163, 74, 0.18);
}

.isle__step svg {
  width: calc(17 * var(--pt));
  height: calc(17 * var(--pt));
  fill: none;
  stroke: var(--tint);
  stroke-width: 2.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* 108pt of code with BarcodeView's 4pt all round, and its corner of 8 */
.isle__code {
  position: relative;
  flex: none;
  display: grid;
  width: calc(116 * var(--pt));
  height: calc(116 * var(--pt));
  padding: calc(4 * var(--pt));
  border-radius: calc(8 * var(--pt));
  background: #fff;
}

.isle__code i { grid-area: 1 / 1; display: block; }
.isle__code svg { display: block; width: 100%; height: 100%; }

/* what a square code leaves room for, which is the whole point of the layout */
.isle__det {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: calc(2 * var(--pt));
  overflow: hidden;
}

.isle__kind {
  font-size: calc(11 * var(--pt));
  font-weight: 700;
  color: var(--tint);
}

.isle__name {
  font-size: calc(13 * var(--pt));
  font-weight: 700;
  color: #fff;
}

.isle__who {
  font-size: calc(11 * var(--pt));
  color: rgba(255, 255, 255, 0.7);
}

/* pushed to the foot of the column by the app's Spacer */
.isle__meta {
  margin-top: auto;
  font-size: calc(10 * var(--pt));
  color: rgba(255, 255, 255, 0.5);
}

.isle__kind,
.isle__who,
.isle__meta i {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Two lines, as `lineLimit(2)` gives it in the app — this is the line a
   square code makes room for, so cutting it to one would be cutting the point
   of the layout. */
.isle__name i {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
  line-height: 1.2;
}

/* ---- stepping from one document to the next ----------------------------- */

/*
   THE ONE ON SHOW AT REST IS `is-a`, and it has to be.

   It was the other way round, and that is what made the plate blink empty
   before anything happened. `is-b` was the resting state, so the card opened
   showing it; then the animation started, and an animation with `both` fill
   applies its *first* frame through its own delay — which had `is-a` at zero
   and `is-b` not yet arrived. Both layers were transparent for the whole
   620ms, so a code that was already on screen vanished, waited, came back as
   the other document, and then swapped. Three events where there should have
   been one.

   Now `is-a` is what the card carries at rest and what it opens holding, the
   step takes it to `is-b`, and `both` holds `is-b` afterwards. Shutting the
   card drops the class and it is back on `is-a`, ready to do it again.
*/
.isle__pos .is-b,
.isle__name .is-b,
.isle__meta .is-b,
.isle__code .is-b { opacity: 0; }

/*
   Only while it is open. Nothing in the band moves otherwise — the pulse is
   the whole of the invitation, and a card stepping through documents behind a
   pulsing ring is two things asking to be looked at.

   THE HANDOVER DOES NOT OVERLAP. Cross-dissolving the two codes was the
   obvious way to write it and the wrong one: halfway through, both symbols are
   half-opaque and on top of each other, which is not a code fading into a code
   — it is grey mush, and it flickers, because the two patterns disagree module
   by module. The outgoing one clears before the incoming one starts, so what
   passes between them is the white plate the code sits on.

   What keeps that instant from reading as a blink is that the codes also
   *move*: the old one settles back a little as it goes and the new one comes
   up from slightly under size. A transform and an opacity, both composited,
   and enough continuity that the eye follows one code to the next instead of
   seeing two states cut together. The text does not scale — sliding type is a
   different and worse idea — so it has its own pair of keyframes.
*/
.is-open .isle__pos .is-a,
.is-open .isle__name .is-a,
.is-open .isle__meta .is-a {
  animation: isle-a 2600ms cubic-bezier(0.4, 0, 0.2, 1) 500ms both;
}

.is-open .isle__pos .is-b,
.is-open .isle__name .is-b,
.is-open .isle__meta .is-b {
  animation: isle-b 2600ms cubic-bezier(0.4, 0, 0.2, 1) 500ms both;
}

.is-open .isle__code .is-a {
  animation: isle-code-a 2600ms cubic-bezier(0.4, 0, 0.2, 1) 500ms both;
}

.is-open .isle__code .is-b {
  animation: isle-code-b 2600ms cubic-bezier(0.4, 0, 0.2, 1) 500ms both;
}

.is-open .isle__step--next {
  animation: isle-press 2600ms cubic-bezier(0.4, 0, 0.2, 1) 500ms both;
}

/*
   Each code is a path of a few hundred modules, and fading one by opacity
   without this repaints all of them every frame. Promoted only while the card
   is open, and the class goes on well over a second before the fade starts, so
   the layers exist long before they are needed rather than being made
   mid-animation.
*/
.is-open .isle__code i { will-change: opacity, transform; }

/*
   The order is the order a finger makes it happen in: the button goes down at
   42%, the old code clears between 46% and 60%, the new one arrives between
   60% and 78%. At 2600ms that is 364ms out and 468ms in, meeting at a single
   instant of empty plate — and the card is showing a code from the moment it
   opens, so nothing is waiting to appear.
*/
@keyframes isle-a {
  0%, 46%   { opacity: 1; }
  60%, 100% { opacity: 0; }
}

@keyframes isle-b {
  0%, 60%   { opacity: 0; }
  78%, 100% { opacity: 1; }
}

@keyframes isle-code-a {
  0%, 46%   { opacity: 1; transform: scale(1); }
  60%, 100% { opacity: 0; transform: scale(0.96); }
}

@keyframes isle-code-b {
  0%, 60%   { opacity: 0; transform: scale(0.94); }
  78%, 100% { opacity: 1; transform: scale(1); }
}

@keyframes isle-press {
  0%, 36%   { transform: none; background: rgba(22, 163, 74, 0.18); }
  42%       { transform: scale(0.88); background: rgba(22, 163, 74, 0.34); }
  50%, 100% { transform: none; background: rgba(22, 163, 74, 0.18); }
}

/* ---- the six kinds of paper --------------------------------------------- */

/*
   One line each, and the only copy of these sentences: the hero's caption
   quotes them out of this markup when an object is pressed.

   Which is why the whole block goes away the moment the script is running —
   `has-press` is put on the root element by site.js once the press handlers
   are attached, and with the press working this strip is the same six
   paragraphs a second time, at the foot of a section that has just shown three
   photographs of the app instead. Without the script it is the only place the
   words exist, and it stays. One copy either way; only its presentation moves.
*/
.has-press .roll { display: none; }

.roll__h {
  margin: 30px 0 12px;
  font-family: var(--title);
  font-size: 18px;
  font-weight: 900;
  letter-spacing: -0.4px;
}

.tells {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 8px;
  /* Wide enough that six of them come out three and three at the page's full
     width rather than four and two, which reads as a row with a gap in it. */
  grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
}

.tell {
  padding: 11px 14px;
  border-radius: 12px;
  border: 1px solid transparent;
  transition: border-color 0.3s ease, background-color 0.3s ease;
}

.tell:hover {
  border-color: var(--edge);
  background: rgba(255, 255, 255, 0.24);
}

.tell h4 {
  margin: 0 0 3px;
  font-size: 14px;
  font-weight: 700;
  letter-spacing: -0.2px;
}

.tell p {
  margin: 0;
  font-size: 13px;
  line-height: 1.5;
  color: var(--ink-quiet);
}

/*
   Two entrances, and which one to use is not a matter of taste.

   `rise` moves, so it owns `transform` for as long as it runs and — filled
   forwards — for ever after. Anything that has a transform of its own to keep,
   a centring translate or a rotation, must use `fade` instead or the animation
   will quietly throw that transform away. It has cost this page three bugs.
*/
@keyframes rise {
  from { opacity: 0; transform: translateY(26px); }
  to   { opacity: 1; transform: none; }
}

@keyframes fade {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* ------------------------------------------------------------ sections */

/*
   Nothing below the hero is laid out or painted until it is scrolled near.

   The reserved height is a guess at each section's size, so the scrollbar is
   honest before anything down there has been measured; being a little wrong
   costs a small jump in the bar, where leaving it out costs a jump in the page.
*/
.sheet {
  width: var(--shell);
  margin: 0 auto;
  padding: clamp(60px, 9vw, 120px) 0;
  content-visibility: auto;
  contain-intrinsic-size: auto 900px;
}

.sheet h2 {
  margin: 0 0 16px;
  font-family: var(--title);
  font-size: clamp(26px, 4.1vw, 43px);
  font-weight: 900;
  letter-spacing: -0.8px;
  line-height: 1.08;
  max-width: 16ch;
}

.lede {
  margin: 0 0 14px;
  max-width: 54ch;
  font-size: clamp(16px, 1.5vw, 18px);
  line-height: 1.62;
  color: var(--ink-quiet);
}

.lede--wide { max-width: 64ch; }

.fine {
  margin: 18px 0 0;
  max-width: 60ch;
  font-size: 14px;
  line-height: 1.6;
  color: var(--ink-faint);
}











.sheet--split {
  display: grid;
  gap: clamp(32px, 5vw, 72px);
  grid-template-columns: 1fr 1fr;
  align-items: center;
}

.alerts { list-style: none; margin: 0; padding: 0; display: grid; gap: 12px; }

/* built to read as notifications, which is where these actually appear */
.alert {
  padding: 14px 16px;
  border-radius: 16px;
  background: rgba(255, 255, 255, 0.9);
  color: #16202e;
  box-shadow: 0 18px 40px rgba(10, 44, 100, 0.22);
}

.alert__t { margin: 0; font-size: 14px; font-weight: 650; letter-spacing: -0.2px; }
.alert__b { margin: 3px 0 0; font-size: 13.5px; color: #55606f; }

.sheet--quiet { text-align: center; }
.sheet--quiet h2 { max-width: none; }
.sheet--quiet .lede,
.sheet--quiet .fine { margin-left: auto; margin-right: auto; }

.acts {
  margin-top: 34px;
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  justify-content: center;
}

.btn {
  display: inline-flex;
  align-items: center;
  padding: 13px 26px;
  border-radius: 999px;
  font-size: 15px;
  font-weight: 550;
  letter-spacing: -0.1px;
  text-decoration: none;
  color: var(--ink);
  background: var(--film);
  border: 1px solid var(--edge);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  transition: background 0.25s ease, transform 0.25s ease;
}

.btn:hover { border-color: var(--edge-strong); transform: translateY(-2px); }

/* `.btn--solid` and `.btn--app` were here, and the App Store button they
   dressed is now `.pill__get` up in the nav. The remaining `.btn`s are the
   quiet pair at the foot — privacy policy, support. */

.apple {
  width: 17px;
  height: 17px;
  fill: currentColor;
  /* the mark sits optically high in its box */
  margin-top: -2px;
  flex: none;
}

.foot {
  position: relative;
  z-index: 10;
  width: var(--shell);
  margin: 0 auto;
  padding: 30px 0 46px;
  display: flex;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
  border-top: 1px solid var(--edge);
  font-size: 13.5px;
  color: var(--ink-faint);
}

.foot p { margin: 0; }
.foot a { color: inherit; }

/* ------------------------------------------------------------ small screens */

/*
   The phone steps down with the page.

   It is a picture at a fixed size rather than a box that stretches, so it
   cannot simply be told a percentage: the screen laid into it is positioned in
   its own pixels by a matrix, and only `--k` — the frame's width over the
   reference 420 — carries that down with it. The two have to be changed
   together or the app's screen slides off the glass.

   The sizes are chosen against the ring of objects rather than against the
   phone: they hang on an ellipse measured in percentages of the stage, so a
   phone that keeps its pixels while the stage loses them grows into them.
*/
@media (max-width: 1100px) {
  .phone__frame { width: 380px; --k: 0.904762; }
}

@media (max-width: 900px) {
  .phone__frame { width: 340px; --k: 0.809524; }
}

/*
   Three phones stop being three phones somewhere around here. A screenshot has
   a width below which its own type stops being type, and that floor is what
   the breakpoints are set by — not by the card, which would go on shrinking
   happily long after the picture in it had turned to texture.
*/
@media (max-width: 1000px) {
  .shots { grid-template-columns: repeat(2, 1fr); }
  /* Third card on its own row: full width would make a phone the height of the
     viewport, so it keeps a column and takes the picture wider instead. */
  .shot:last-child { grid-column: span 2; }
  .shot:last-child .shot__phone { width: 46%; }

  .band { grid-template-columns: 1fr; gap: 0; }
  .band__say { padding-bottom: 4px; }
  .band__art { max-width: 420px; margin: 0 auto; }
}

@media (max-width: 860px) {
  /* Two columns of type is a column of type and a column of orphans at this
     width. Back to the stack. */
  .lead { grid-template-columns: 1fr; }
  .lead h2 { max-width: 16ch; }
  .lead .lede { margin: 4px 0 0; max-width: 54ch; }
}

@media (max-width: 620px) {
  .shots { grid-template-columns: 1fr; }
  .shot:last-child { grid-column: span 1; }
  .shot:last-child .shot__phone,
  .shot__phone { width: 74%; }
}

@media (max-width: 760px) {
  /*
     Centred and wide, rather than shrink-wrapped and pushed into a corner.

     It went to the right when the download button was still out in the bar and
     the nav had to make room for it. Nothing is out in the bar now, so there is
     nothing to make room for — and a pill that hugs its two contents and hangs
     off one edge reads as a leftover, where one that spans the screen with the
     mark at one end and the button at the other reads as the header it is.

     Stated width, because the base rule is `fit-content`. `space-between` is
     what puts the two at the ends; the slack in the middle is the point.
  */
  .pill {
    left: 0;
    right: 0;
    width: min(430px, calc(100% - 32px));
    margin: 0 auto;
    justify-content: space-between;
    padding: 9px 9px 9px 18px;
    gap: 0;
  }

  /* Bigger with it: at 40px tall in the middle of a phone's width this was a
     desktop nav that had been left on. */
  .pill .mark__word { font-size: 21px; }
  .pill .mark__plane { width: 19px; height: 19px; }

  .pill a {
    display: none;
    width: 100%;
    padding: 11px 16px;
    border-radius: 12px;
  }

  /*
     Two things, and no menu to open.

     There was a hamburger here that stacked the four section links under the
     nav. It has gone, and with it the only way to reach them on a phone —
     which is the trade, stated: this is one page that scrolls, the four links
     are anchors into it, and a menu button whose whole content is four places
     you will pass anyway is a control that exists to be tidy.

     What is left is the mark and the button. Neither is a link to hide: one is
     the nav's identity, the other is the reason the nav follows you down the
     page at all.
  */
  .pill a.mark {
    display: inline-flex;
    width: auto;
    padding: 0;
    margin-right: 2px;
  }

  .pill a.pill__get {
    display: inline-flex;
    width: auto;
    padding: 11px 20px;
    border-radius: 999px;
    font-size: 15px;
  }

  .pill a.pill__get .apple { width: 22px; height: 22px; }

  .sheet--split { grid-template-columns: 1fr; }
  .foot { justify-content: center; text-align: center; }

  /* only the wordmark is left in the bar at any width */
  .bar { grid-template-columns: 1fr; }

  /*
     No backdrop filters on a phone, and the reason is the hard edge they draw.

     `backdrop-filter: saturate(1.2)` makes the sky *inside* a card more
     saturated than the sky outside it, and on a phone a card is the full width
     of the screen — so its top and bottom edges are two hard horizontal lines
     ruled across the page where the colour steps. The blur has the same shape
     of problem on iOS, which samples a `position: fixed` backdrop a fraction
     out and leaves a seam at the boundary.

     Both come off, and `--film` goes from 0.44 to 0.58 so the panels still
     read as panels against the sky without the glass. It is also the cheapest
     thing on the page to stop asking a phone to do.
  */
  .shot,
  .band,
  .btn {
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    background: rgba(255, 255, 255, 0.58);
  }

  .phone__frame { width: 320px; --k: 0.761905; }

  /*
     The hero's foot. 96px of it was room under a ring of objects that is a row
     here, and with the section below carrying its own 60 it came to a screen
     and a half of sky between the last cover and the next heading.
  */
  .hero { padding: 118px 20px 40px; }

  /* one line at 390: at 13px the last two words fell to a second line, which
     turns a footnote into a paragraph */
  .hero__meta { font-size: 12px; }

  /*
     There is no room to hang six objects around a phone on a phone. They come
     out of the air and lie down in a row you can push along instead — the same
     six, at a size where their small print is still print, which it would not
     be at a sixth of this width.
  */
  .stage {
    display: block;
    width: 100%;
    /* the ring's headroom was for objects hanging above and below the phone */
    padding: 0 0 12px;
    margin-top: 26px;
  }

  .stage .phone { display: flex; justify-content: center; }

  /*
     The six objects, in a row you push along.

     They are sized near the width the covers were *drawn* for, and that is the
     constraint everything else here bends to. The printing inside a cover is
     laid out in fixed pixels against `--u`, and `--u` is 1 everywhere: at the
     280-330px the ring gives them on a wide screen the small print sits where
     it belongs, and at much under 200 the insurance cover's rules run through
     each other. A grid of all six was tried and this is what killed it — two
     columns of a phone is 160px a cover, and 160px is a cover with its type on
     top of itself.

     So: one at a time, at nearly full size, with the next one showing at the
     edge. The peek is the whole affordance — a row that ends flush with the
     screen looks like a picture that has been cut off, which is what it was.
  */
  .air {
    position: static;
    margin: 26px 0 0;
    padding: 6px 5vw 18px;
    pointer-events: auto;
    display: flex;
    align-items: center;
    gap: 16px;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    scroll-padding-inline: 5vw;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    /*
       No mask. It faded the last cover against the right edge, which was the
       nicer ending — and a mask on a scrolling container is a composited layer
       over a fixed background, which is the same seam the cards were drawing.
       The peek of the next cover was always the real affordance; the fade was
       manners.
    */
  }

  .air::-webkit-scrollbar { display: none; }

  /* The row stays where it is: the objects are already one to a screen here,
     and emptying the strip would leave a caption with nothing to explain.
     Pressing one opens its words under the row, where they can be read. */
  .stage.is-holding .float,
  .stage.is-holding .phone {
    pointer-events: auto;
  }

  .stage.is-holding .float { opacity: var(--sheer); }
  .stage.is-holding .phone { opacity: 1; }

  .focus {
    position: static;
    transform: none;
    width: auto;
    margin: 18px 5vw 0;
  }

  .float {
    position: static;
    inset: auto;
    left: auto;
    top: auto;
    /* 300 is the middle of the range the covers are drawn for; 78vw keeps a
       peek of the next one on any phone. Flat, not tilted — a tilt reads as
       thrown down on a wide stage and as a mistake in a queue. */
    width: min(300px, 78vw) !important;
    flex: none;
    transform: none;
    animation: none;
    scroll-snap-align: center;
    align-self: center;
  }
}

@media (max-width: 420px) {
  .hero { padding-top: 128px; }
  .phone__frame { width: 280px; --k: 0.666667; }

  /* The nav's two halves come to about 310px, and the pill is the screen less
     32. Below about 350 of screen those meet, so both come back down — the
     wordmark by a point, the button by the padding it can spare. */
  .pill { padding: 8px 8px 8px 15px; }
  .pill .mark__word { font-size: 19px; }
  .pill .mark__plane { width: 17px; height: 17px; }
  .pill a.pill__get { padding: 9px 15px; font-size: 14px; }
  .pill a.pill__get .apple { width: 20px; height: 20px; }
  /* The landscape covers get very small type at this width; a shade wider
     stops the machine-readable zone turning into a grey stripe. */
  .object__art .cover { max-width: none; }
}

/* ------------------------------------------------------------ preferences */

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}
