/* The product's own steel, on the ground the capsules are lit against.

   Dark only, and that is the brand rather than a preference: the mark is a
   mirror-finish render, and on a paper-white page it has nothing to reflect and
   reads as a grey smudge — the same reason `tools/store/build.py` renders the
   capsules on one palette. */
:root {
  --ground: #0a0c0f;
  --raise: rgba(255, 255, 255, .028);
  --line: rgba(255, 255, 255, .085);
  --line-2: rgba(255, 255, 255, .05);
  --ink: #edf1f6;
  --ink-2: rgba(237, 241, 246, .74);
  --ink-3: rgba(237, 241, 246, .54);
  --steel: #a8bdd8;
  --page: 1180px;
  --ease: cubic-bezier(.16, .84, .28, 1);
}

* { box-sizing: border-box; }
html { scroll-behavior: smooth; }

body {
  margin: 0;
  background: var(--ground);
  color: var(--ink);
  font: 16px/1.65 "Segoe UI Variable Text", "Segoe UI", system-ui, -apple-system,
        "Helvetica Neue", Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

/* ------------------------------------------------------------- the ground

   Three layers, none of them an image: two slow-drifting fields of colour, a
   hairline grid that fades out below the fold, and a film of grain over both.
   The grain is what stops a large area of near-black from banding on an 8-bit
   panel, which is the thing that makes a dark page look cheap. */

.field, .field-2 {
  position: fixed; inset: -30vh -25vw; z-index: 0; pointer-events: none;
  will-change: transform;
}
.field {
  background:
    radial-gradient(46vw 46vw at 72% 6%, rgba(116, 160, 220, .38), transparent 62%),
    radial-gradient(40vw 40vw at 8% 0%, rgba(82, 114, 162, .30), transparent 64%);
  filter: blur(18px);
  animation: drift 46s var(--ease) infinite alternate;
}
/* A second field on a different clock and a different hue. One moving gradient
   reads as a moving gradient; two crossing at unrelated speeds read as light. */
.field-2 {
  background:
    radial-gradient(50vw 38vw at 34% 74%, rgba(96, 122, 168, .20), transparent 66%),
    radial-gradient(30vw 30vw at 88% 52%, rgba(140, 168, 208, .13), transparent 62%);
  filter: blur(26px);
  animation: drift-2 71s linear infinite alternate;
}
/* And the edges go down, so the light has somewhere to fall off to. */
.vignette {
  position: fixed; inset: 0; z-index: 1; pointer-events: none;
  background: radial-gradient(130vw 100vh at 52% 26%, transparent 54%, rgba(4, 5, 7, .5) 100%);
}
@keyframes drift {
  from { transform: translate3d(0, 0, 0) scale(1); }
  to   { transform: translate3d(-4vw, 3vh, 0) scale(1.12); }
}
@keyframes drift-2 {
  from { transform: translate3d(2vw, 1vh, 0) scale(1.06); }
  to   { transform: translate3d(-2vw, -4vh, 0) scale(1); }
}

.grid-lines {
  position: fixed; inset: 0; z-index: 0; pointer-events: none;
  background-image:
    linear-gradient(to right, rgba(255, 255, 255, .045) 1px, transparent 1px);
  background-size: calc(var(--page) / 6) 100%;
  background-position: center;
  -webkit-mask-image: linear-gradient(to bottom, #000 0, transparent 74vh);
  mask-image: linear-gradient(to bottom, #000 0, transparent 74vh);
  opacity: .8;
}

.grain {
  position: fixed; inset: 0; z-index: 1; pointer-events: none; opacity: .035;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* ------------------------------------------------------------------ layout */

.wrap { position: relative; z-index: 2; max-width: var(--page); margin: 0 auto; padding: 0 30px; }

h1, h2, h3, .num {
  font-family: "Segoe UI Variable Display", "Segoe UI", system-ui, sans-serif;
}

h1 {
  font-size: clamp(2.4rem, 5.9vw, 4.15rem);
  line-height: 1.02; letter-spacing: -.028em; font-weight: 660; margin: 0 0 20px;
}
h2 {
  font-size: clamp(1.6rem, 3.1vw, 2.25rem);
  line-height: 1.14; letter-spacing: -.021em; font-weight: 640; margin: 0 0 14px;
}
h3 { font-size: 1.04rem; font-weight: 640; margin: 0 0 6px; letter-spacing: -.006em; }

p { margin: 0 0 16px; color: var(--ink-2); }
.lede { font-size: 1.12rem; color: var(--ink-2); max-width: 60ch; }
strong { color: var(--ink); font-weight: 620; }
em { color: var(--ink-2); }

/* The section's number, set in the margin the way a printed page numbers its
   parts. It is the cheapest thing on the page that says somebody laid it out. */
.num {
  display: block; font-size: .78rem; letter-spacing: .04em; font-weight: 600;
  color: var(--steel); opacity: .74; margin-bottom: 14px;
  font-variant-numeric: tabular-nums;
}
.num::after {
  content: ""; display: inline-block; vertical-align: middle;
  width: 42px; height: 1px; margin-left: 12px; background: currentColor; opacity: .45;
}

/* ---------------------------------------------------------------- masthead */

header.top {
  position: sticky; top: 0; z-index: 20;
  backdrop-filter: blur(14px) saturate(1.2);
  background: linear-gradient(to bottom, rgba(10, 12, 15, .82), rgba(10, 12, 15, .34));
  border-bottom: 1px solid transparent;
  transition: border-color .35s var(--ease), background .35s var(--ease);
}
header.top.stuck { border-bottom-color: var(--line-2); }
header.top .inner {
  max-width: var(--page); margin: 0 auto; padding: 15px 30px;
  display: flex; align-items: center; justify-content: space-between; gap: 20px;
}
header.top .name { display: flex; align-items: center; gap: 11px; font-weight: 620; letter-spacing: -.012em; }
header.top .name img { width: 25px; height: 25px; transition: transform .8s var(--ease); }
header.top .name:hover img { transform: rotate(28deg); }
/* The mark turns as the page is read. It is the same gesture the lock-up in the
   hero makes on a 44-second clock, moved onto the scroll — so once the hero has
   gone, the one piece of it still on screen goes on turning, and it is the
   reader turning it. A quarter of a revolution over the whole page: enough to
   notice on the way back up, not enough to be a spinner.

   `rotate` and not `transform`, because the hover above is a transform and the
   two would overwrite each other. They are separate properties and compose. */
.js header.top .name img { rotate: calc(var(--scroll, 0) * 92deg); }
header.top nav { display: flex; gap: 26px; font-size: .93rem; }
header.top nav a { color: var(--ink-3); text-decoration: none; position: relative; transition: color .25s; }
header.top nav a::after {
  content: ""; position: absolute; left: 0; right: 100%; bottom: -5px; height: 1px;
  background: var(--steel); transition: right .38s var(--ease);
}
header.top nav a:hover { color: var(--ink); }
header.top nav a:hover::after { right: 0; }

#progress {
  position: fixed; top: 0; left: 0; height: 2px; width: 100%; z-index: 30;
  background: linear-gradient(90deg, rgba(168, 189, 216, .9), rgba(237, 241, 246, .9));
  transform: scaleX(0); transform-origin: 0 50%;
}

/* -------------------------------------------------------------------- hero */

.hero { padding: 84px 0 0; text-align: center; }
/* What the thing is, in words a stranger can place, above the line that is a
   slogan. The page never said "Windows" or "monitors" anywhere near the top. */
.kicker {
  color: var(--ink-3); font-size: .88rem; letter-spacing: .01em; margin: 0 0 14px;
}
/* The lock-up, animated the way the game screen and the tour's banner are: the
   mark turns, and a highlight travels along each piece — a moving gradient
   masked by the artwork's own alpha, so the light is inside the letters and
   inside the ribbons rather than wiping across a rectangle.

   Drawn, not filmed. A looping video behind a masthead would be a decoder
   running all day on a page whose whole argument is that nothing here costs
   anything to leave open. */
.lockup {
  display: flex; align-items: center; justify-content: center;
  gap: clamp(14px, 2.4vw, 28px); margin: 0 auto 38px;
}
.lockup .mark, .lockup .word { position: relative; display: block; }
.lockup img { display: block; width: 100%; height: auto; }

.lockup .mark {
  width: clamp(88px, 12vw, 132px); aspect-ratio: 1;
  filter: drop-shadow(0 10px 34px rgba(120, 160, 210, .26));
  animation: turn 44s linear infinite;
}
.lockup .word { width: clamp(210px, 30vw, 330px); }

.lockup .sheen {
  position: absolute; inset: 0; pointer-events: none; mix-blend-mode: screen;
  background: linear-gradient(112deg, transparent 40%, rgba(232, 244, 255, .82) 50%, transparent 60%);
  background-size: 280% 100%; background-repeat: no-repeat;
  -webkit-mask-size: 100% 100%; mask-size: 100% 100%;
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
  animation: sweep 5.5s ease-in-out infinite;
}
/* The mask itself is in masks.css, generated and inlined — see the note there. */
/* A beat behind the mark, so the light reads as one thing crossing the lock-up
   rather than two lamps switched on together. */
.lockup .word .sheen { animation-delay: 1.3s; }

@keyframes turn { to { transform: rotate(360deg); } }
@keyframes sweep {
  0%   { background-position: 150% 0; }
  55%  { background-position: -60% 0; }
  100% { background-position: -60% 0; }
}
.hero .lede { margin: 0 auto 30px; }

.cta { display: flex; align-items: center; justify-content: center; gap: 14px; flex-wrap: wrap; margin-bottom: 14px; }

.button {
  position: relative; overflow: hidden;
  display: inline-block; padding: 13px 28px; border-radius: 7px;
  font-weight: 620; font-size: .98rem; text-decoration: none; color: #0f1319;
  background: linear-gradient(180deg, #e2e8f1 0%, #c6d1de 48%, #b1bfd1 52%, #d0dae6 100%);
  border: 1px solid rgba(255, 255, 255, .24);
  transition: transform .25s var(--ease), box-shadow .3s var(--ease);
}
.button:hover { transform: translateY(-1px); box-shadow: 0 10px 30px -12px rgba(168, 189, 216, .55); }
/* The mark's own highlight, travelling. Drawn rather than filmed, for the same
   reason the tour's banner sweep is drawn: a decoder for a button is absurd. */
.button::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(105deg, transparent 34%, rgba(255, 255, 255, .75) 50%, transparent 66%);
  transform: translateX(-120%);
}
.button:hover::after { animation: sheen 1.1s var(--ease); }
@keyframes sheen { to { transform: translateX(120%); } }

.button.quiet {
  background: transparent; color: var(--ink); border-color: var(--line);
}
.button.quiet:hover { border-color: rgba(255, 255, 255, .26); background: rgba(255, 255, 255, .05); }
.button.quiet::after { display: none; }

.price { color: var(--ink-3); font-size: .94rem; }
.waiting {
  display: inline-flex; align-items: center; gap: 9px;
  color: var(--ink-2); font-size: .95rem; margin: 0 0 6px;
}
.waiting::before {
  content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--steel);
  animation: breathe 2.6s ease-in-out infinite;
}
@keyframes breathe { 50% { opacity: .25; } }
.price b { color: var(--ink); font-weight: 620; }

/* ------------------------------------------------------------ the pictures */

figure { margin: 0; }
figcaption { color: var(--ink-3); font-size: .88rem; margin-top: 11px; }

.shot, .clip {
  width: 100%; height: auto; display: block;
  border-radius: 11px; border: 1px solid var(--line);
  background: #0b0d10;
}
button.frame {
  display: block; width: 100%; padding: 0; border: 0; background: none;
  font: inherit; color: inherit; text-align: inherit;
}
a:focus-visible, button:focus-visible, video:focus-visible {
  outline: 2px solid var(--steel); outline-offset: 4px; border-radius: 4px;
}

.frame {
  position: relative; border-radius: 11px;
  transition: transform .5s var(--ease), box-shadow .5s var(--ease);
  box-shadow: 0 24px 60px -34px rgba(0, 0, 0, .9);
}
.frame:hover { transform: translateY(-3px); box-shadow: 0 34px 80px -34px rgba(0, 0, 0, .95); }

/* A still that opens. The cursor and the corner mark say so before the click,
   because a picture that silently becomes a lightbox is a picture nobody clicks. */
.frame.open { cursor: zoom-in; }
.frame.open::after {
  content: "View full size";
  position: absolute; right: 12px; bottom: 12px;
  padding: 6px 11px; border-radius: 6px; font-size: .78rem; color: var(--ink);
  background: rgba(12, 15, 19, .78); border: 1px solid var(--line);
  backdrop-filter: blur(8px);
  opacity: 0; transform: translateY(6px); transition: opacity .3s var(--ease), transform .3s var(--ease);
}
.frame.open:hover::after { opacity: 1; transform: none; }

.hero .frame { margin-top: 50px; }

/* ---------------------------------------------------------------- sections */

section { padding: 88px 0; position: relative; }
section + section::before {
  content: ""; position: absolute; top: 0; left: 50%; transform: translateX(-50%);
  width: min(var(--page), calc(100% - 60px)); height: 1px;
  background: linear-gradient(90deg, transparent, var(--line), transparent);
}

/* The wide track belongs to the picture in both directions. Reversing with
   `order` instead moved the figure into the *narrow* cell, so the screenshots on
   alternate rows came out 38 % smaller and the paragraphs beside them ran to 78
   characters against 57 on the row above. */
.split { display: grid; grid-template-columns: minmax(0, .84fr) minmax(0, 1.16fr); gap: 58px; align-items: center; }
.split.flip { grid-template-columns: minmax(0, 1.16fr) minmax(0, .84fr); }
.split.flip > figure { order: -1; }

/* The six things a preset does, as a numbered list rather than a wall of equal
   cards. A column of identical boxes is one of the three things that make a
   layout read as machine-made, and this page argues the opposite. */
.things { margin-top: 44px; border-top: 1px solid var(--line-2); }
.things > div {
  display: grid; grid-template-columns: 52px minmax(0, 1fr) minmax(0, 1.5fr); gap: 24px;
  padding: 26px 8px; border-bottom: 1px solid var(--line-2);
  transition: background .4s var(--ease), padding-left .4s var(--ease);
}
.things > div:hover { background: var(--raise); padding-left: 18px; }
.things .n {
  font-variant-numeric: tabular-nums; color: var(--ink-3); font-size: .92rem;
  padding-top: 2px; letter-spacing: .02em;
}
.things h3 { margin: 0; }
.things p { margin: 0; font-size: .96rem; }

.rules { list-style: none; margin: 30px 0 0; padding: 0; display: grid; gap: 16px; max-width: 88ch; }
.rules li { padding-left: 32px; position: relative; color: var(--ink-2); }
.rules li::before {
  content: ""; position: absolute; left: 1px; top: .5em;
  width: 13px; height: 7px; border-left: 1.6px solid var(--steel); border-bottom: 1.6px solid var(--steel);
  transform: rotate(-45deg);
}
.limits { list-style: none; margin: 24px 0 0; padding: 0; display: grid; gap: 13px; max-width: 76ch; }
.limits li { padding-left: 22px; position: relative; color: var(--ink-2); font-size: .97rem; }
.limits li::before { content: ""; position: absolute; left: 2px; top: .72em; width: 9px; height: 1px; background: var(--ink-3); }

/* Four numbers about what the program is, rather than what it does. In a
   category whose reviews are full of what the last tool left behind, this is the
   cheapest paragraph on the page and the one people quote. */
.facts {
  display: grid; grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 1px; background: var(--line-2);
  border: 1px solid var(--line-2); border-radius: 11px; overflow: hidden;
  margin-top: 48px;
}
.facts > div { background: var(--raise); padding: 22px 20px; }
.facts b {
  display: block; font-family: "Segoe UI Variable Display", "Segoe UI", system-ui, sans-serif;
  font-size: 1.32rem; font-weight: 660; letter-spacing: -.02em; margin-bottom: 6px;
}
.facts span { color: var(--ink-2); font-size: .9rem; line-height: 1.5; }

/* ------------------------------------------------------------------- the faq

   Buttons and a height transition rather than <details>, for one reason: a
   closed <details> hides its content outright, so it can be animated open and
   never animated shut. The grid trick animates both ways. */
.faq { margin-top: 40px; border-top: 1px solid var(--line-2); }
.faq .q { border-bottom: 1px solid var(--line-2); }
.faq button {
  display: flex; align-items: center; justify-content: space-between; gap: 24px;
  width: 100%; padding: 22px 6px; border: 0; background: none; cursor: pointer;
  font: 620 1.02rem/1.4 "Segoe UI Variable Display", "Segoe UI", system-ui, sans-serif;
  letter-spacing: -.008em; color: var(--ink); text-align: left;
  transition: padding-left .35s var(--ease), color .25s var(--ease);
}
.faq button:hover { padding-left: 16px; }
.faq button i {
  position: relative; flex: none; width: 13px; height: 13px;
  transition: transform .4s var(--ease);
}
.faq button i::before, .faq button i::after {
  content: ""; position: absolute; inset: 6px 0 auto 0; height: 1.5px; background: var(--steel);
}
.faq button i::after { transform: rotate(90deg); transition: opacity .3s var(--ease); }
.faq button[aria-expanded="true"] i { transform: rotate(180deg); }
.faq button[aria-expanded="true"] i::after { opacity: 0; }

.faq .a { display: grid; grid-template-rows: 0fr; transition: grid-template-rows .42s var(--ease); }
.faq .q.on .a { grid-template-rows: 1fr; }
/* Clipped to nothing is not hidden: a screen reader still reads a closed answer,
   while the button beside it says it is collapsed. `visibility` takes it out of
   the tree and still animates. */
.faq .a > div { overflow: hidden; visibility: hidden; transition: visibility 0s .42s; }
.faq .q.on .a > div { visibility: visible; transition-delay: 0s; }
.faq .a p { margin: 0 0 22px; padding-right: 40px; max-width: 78ch; }

/* A section whose picture is a film gets the whole width. A recording of two
   monitors set into a 615 px column is a grey smear — the windows in it are
   smaller than the type beside them, and they are the proof the page is making.
   Stills stay in the split: they are crops of one window and they open. */
.tell .say { max-width: 64ch; }
.tell figure { margin-top: 38px; }

.wide { margin-top: 46px; }

/* The five demonstrations: one stage, dealt through.

   They were a grid of small cards, which is the shape a landing page reaches for
   and the wrong one here — inside every one of these is a two-monitor desk, and
   half a column wide it is a grey smear with windows smaller than the type
   beside it. They are the proof the whole page is making, so they get the
   screen, one at a time.

   The mechanism is a tall track with a pinned stage inside it. Scrolling the
   track does not move the stage; it moves the *cards*, one out to the left and
   the next in from the right, tied to the scroll position rather than played on
   a timer. That is the difference between an animation and a control: the
   picture is wherever the reader's hand has put it, and it goes backwards when
   they scroll back.

   None of it exists without script, and that is deliberate: the markup is five
   ordinary blocks, and this only takes over once `.js` is on the document. */
.for { margin-top: 40px; }
.for > .pin > .deck > article { padding-bottom: 56px; }
/* 106vh is the width a 16:9 frame has when it is 60 vh tall — which is what is
   left inside the deck once three lines of words have had their room. Sized off
   the window's height as much as its width, because a 16:9 recording at 94vw is
   the whole viewport on a laptop and the sentence explaining it would fall off
   the bottom of the one block whose job is showing one thing at a time. */
.for .stage { width: min(1560px, 94vw, 106vh); margin: 0 auto; }
.for .words { width: min(1560px, 94vw, 106vh); margin: 20px auto 0; }
.for h3 { margin: 0 0 8px; font-size: 1.5rem; letter-spacing: -.01em; }
.for p { margin: 0; color: var(--ink-2); font-size: 1.02rem; max-width: 68ch; }
.for .of { display: block; color: var(--ink-3); font-size: .8rem; letter-spacing: .08em;
           text-transform: uppercase; margin-bottom: 10px; }
.for .rail { display: none; }

/* --- the pinned version, only where there is script to drive it ----------- */

.js .for { position: relative; height: 460vh; }
.js .for > .pin {
  position: sticky; top: 0; height: 100vh;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  /* A card on its way out travels past the edge of the window, and a page that
     scrolls sideways because of it is a page with a bug in it. */
  overflow: hidden;
}
/* Tall enough for the frame *and* the words under it. It was sized to the
   frame alone, and the words — absolutely placed along with their card — spilled
   out of the bottom of the deck and landed on top of the rail. */
.js .for .deck { position: relative; width: min(1560px, 94vw, 106vh); height: 80vh;
                 /* The cards lean as they leave; a little perspective turns that
                    from a skew into a turn. */
                 perspective: 1600px; }
.js .for .deck > article {
  position: absolute; inset: 0; padding: 0;
  display: flex; flex-direction: column; justify-content: center;
  /* Set by script on every frame. Named here so the compositor is told what to
     expect before the first one arrives. */
  will-change: transform, opacity;
  transform-origin: 50% 60%;
}
.js .for .stage, .js .for .words { width: 100%; }

/* Where you are in the five, and what each one is. It doubles as the reason the
   block is worth scrolling through: the titles are visible before you reach
   them, so it reads as a list of five answers rather than an unmarked drop. */
.js .for .rail { display: flex; gap: 18px; list-style: none; margin: 26px 0 0; padding: 0;
                 flex-wrap: wrap; justify-content: center; }
.js .for .rail li { display: flex; align-items: center; gap: 8px;
                    color: var(--ink-3); font-size: .82rem;
                    transition: color .45s var(--ease); }
.js .for .rail li span { width: 22px; height: 2px; background: var(--line-2);
                         transition: background .45s var(--ease), width .45s var(--ease); }
.js .for .rail li.on { color: var(--ink); }
.js .for .rail li.on span { background: var(--steel); width: 38px; }

.centre { text-align: center; }
.centre .lede { margin-left: auto; margin-right: auto; }
.centre .cta { margin-top: 28px; }

/* ------------------------------------------------------------- the reveal

   Everything that scrolls into view arrives from a little below and a little
   transparent. It is one transition on one class, added by an observer, so a
   section added later gets it by writing `reveal` and nothing else. */
/* Hidden only when a script is present to bring it back. Content that CSS hides
   and JavaScript reveals is a blank page the day the script does not run, and a
   marketing page has no business being fragile in that direction. */
.js .reveal { opacity: 0; transform: translate3d(0, 24px, 0) scale(.99); }
.js .reveal.in { opacity: 1; transform: none;
                 transition: opacity .9s var(--ease), transform .9s var(--ease); }
.js .reveal.d1.in { transition-delay: .07s; }
.js .reveal.d2.in { transition-delay: .14s; }
.js .reveal.d3.in { transition-delay: .21s; }
.js .reveal.d4.in { transition-delay: .28s; }

/* --- direction, where direction means something -------------------------

   A picture in a two-column row comes in from its own side of the page, and
   the words stay vertical. That is spatial continuity — the thing arrives from
   where it is going to live — and it is the reason this reads as one page
   moving rather than as a page with effects on it.

   One element travelling sideways per view, no more. Everything else rises.
   The rule that produced that number is the same one this page is built on:
   move one or two things per screen, and only where the movement carries a
   meaning. A block thrown into a corner because a corner was available is the
   thing that makes an expensive page look cheap a second time you scroll it. */
.js .split > figure.reveal { transform: translate3d(44px, 20px, 0) scale(.985); }
.js .split.flip > figure.reveal { transform: translate3d(-44px, 20px, 0) scale(.985); }
.js .split > div > .reveal { transform: translate3d(-16px, 20px, 0); }
.js .split.flip > div > .reveal { transform: translate3d(16px, 20px, 0); }
.js .split > figure.reveal.in, .js .split > div > .reveal.in { transform: none; }

/* A list arrives as a list: each row a beat behind the one above it, which is
   what turns six rows into one gesture. Capped at six — beyond that the last
   row waits so long it reads as broken rather than as staggered. */
.js .things > div.reveal.in:nth-child(1) { transition-delay: .04s; }
.js .things > div.reveal.in:nth-child(2) { transition-delay: .10s; }
.js .things > div.reveal.in:nth-child(3) { transition-delay: .16s; }
.js .things > div.reveal.in:nth-child(4) { transition-delay: .22s; }
.js .things > div.reveal.in:nth-child(5) { transition-delay: .28s; }
.js .things > div.reveal.in:nth-child(n+6) { transition-delay: .34s; }
.js .rules > li.reveal.in:nth-child(2) { transition-delay: .06s; }
.js .rules > li.reveal.in:nth-child(3) { transition-delay: .12s; }
.js .rules > li.reveal.in:nth-child(4) { transition-delay: .18s; }
.js .rules > li.reveal.in:nth-child(n+5) { transition-delay: .24s; }
.js .limits > li.reveal.in:nth-child(2) { transition-delay: .06s; }
.js .limits > li.reveal.in:nth-child(3) { transition-delay: .12s; }
.js .limits > li.reveal.in:nth-child(4) { transition-delay: .18s; }
.js .limits > li.reveal.in:nth-child(n+5) { transition-delay: .24s; }
.js .faq > .q.reveal.in:nth-child(2) { transition-delay: .05s; }
.js .faq > .q.reveal.in:nth-child(3) { transition-delay: .10s; }
.js .faq > .q.reveal.in:nth-child(4) { transition-delay: .15s; }
.js .faq > .q.reveal.in:nth-child(n+5) { transition-delay: .20s; }

/* --- a heading arrives in lines -----------------------------------------

   Not with the paragraph under it, and not as a block fading up: each line is
   clipped to its own box and the type is lifted out from behind its own
   baseline. It is the oldest move in setting type on screens and it is still
   the one that reads as somebody having laid the page out, because it is the
   only entrance here that could not have come from a template — it needs to
   know where the lines are.

   The boxes are written by the script, so a page without one has headings that
   are headings. The clip needs a little room under the baseline or it cuts the
   descenders off a "g"; the negative margin gives it back, so nothing below
   moves by a pixel.

   The forced break in the headline is a break: each half gets its own box and
   its own beat, one behind the other, which is the whole reason to do this
   rather than lift the block. */
.js h1 .ln, .js h2 .ln {
  display: block; overflow: hidden;
  padding-bottom: .12em; margin-bottom: -.12em;
}
.js h1 .ln > i, .js h2 .ln > i {
  display: block; font-style: normal;
  transform: translate3d(0, 108%, 0);
  transition: transform 1.05s var(--ease);
  transition-delay: calc(var(--n, 0) * .085s);
}
/* Same weight as the direction rules above and written after them, so a
   heading in a split row is lifted out of its line rather than pushed in from
   the side: one gesture per element, and this one is stronger. */
.js h1.reveal.lines, .js h2.reveal.lines,
.js .split > div > h2.reveal.lines {
  opacity: 1; transform: none; transition: none;
}
.js .reveal.lines.in .ln > i { transform: none; }

/* --- a tick draws itself -------------------------------------------------

   The rules list is six promises with a check beside each. The check is two
   borders and a rotation, so it can be *drawn*: it comes up out of nothing as
   its line arrives, which is a mark being made rather than a mark appearing.
   The limits list gets the opposite gesture from the same idea — its dash
   extends, because a dash is a qualification and not a promise. */
.js .rules li::before {
  transform: rotate(-45deg) scale(0); opacity: 0;
  transition: transform .5s var(--ease) .12s, opacity .3s var(--ease) .12s;
}
.js .rules li.reveal.in::before { transform: rotate(-45deg) scale(1); opacity: 1; }
.js .limits li::before {
  transform: scaleX(0); transform-origin: 0 50%;
  transition: transform .55s var(--ease) .1s;
}
.js .limits li.reveal.in::before { transform: scaleX(1); }

/* --- the four numbers arrive across --------------------------------------

   Left to right, one cell behind the next, because they are read that way and
   because four boxes appearing together is the column of equal cards this page
   is built to avoid. The cell arrives from *under* its own ground, which is
   what the shared rounded container makes possible: they look like one strip
   filling rather than four things landing. */
.js .facts.reveal { opacity: 1; transform: none; transition: none; }
.js .facts > div {
  opacity: 0; transform: translate3d(0, 16px, 0);
  transition: opacity .7s var(--ease), transform .7s var(--ease);
}
.js .facts.in > div { opacity: 1; transform: none; }
.js .facts.in > div:nth-child(2) { transition-delay: .08s; }
.js .facts.in > div:nth-child(3) { transition-delay: .16s; }
.js .facts.in > div:nth-child(4) { transition-delay: .24s; }

/* ======================================================================
   TRAVEL — the half of the motion that is tied to the scroll position

   Everything above this line arrives once and then holds still, which is what
   words have to do. Everything below it is a function of `--p`, a number
   between 0 and 1 that `app.js` writes from where the element is, on a frame
   the browser asked for. Nothing here is on a timer: the value is wherever the
   reader's hand has put it, it runs backwards when they scroll back up, and it
   stops when they stop.

   That is the whole difference between a page with effects on it and a page
   that feels like an instrument. It is also why this is applied to pictures,
   rules, lists and light, and to no paragraph anywhere: a sentence whose
   opacity depends on the scroll wheel is a sentence you cannot read while
   moving.

   The easing is applied in the script, so every `calc()` below is arithmetic
   on a number that has already been shaped.
   ====================================================================== */

/* --- depth ---------------------------------------------------------------

   Every figure drifts a little slower than the page it is on. It is the one
   continuous movement here and it is deliberately almost invisible: about
   forty pixels across a whole screen of scrolling, which the eye reads as the
   picture being *behind* the text rather than as anything moving. Parallax
   that announces itself is the effect this page is trying not to have. */
.js .depth {
  will-change: transform;
  transform: translate3d(0, calc((.5 - var(--p, .5)) * 46px), 0)
             translateY(var(--hover, 0px));
}
.js .depth:hover { --hover: -3px; }

/* --- the light on the rim ------------------------------------------------

   A band of light travels down the edge of every framed picture as it crosses
   the window. It is the brand's own argument made in one element: the mark is
   a mirror finish, and what tells a mirror from a grey fill is that the light
   on it *moves* when you do. A page of dark rectangles with hairline borders
   is the cheapest place there is to say that, and it costs a gradient on a
   pseudo-element the compositor was going to draw anyway.

   Drawn as a one-pixel ring rather than as a wash over the picture: a sheen
   over a film of somebody's desk is a smear on the thing the page is proving.
   The ring is the standard two-mask trick — paint the padding box, punch the
   content box out of it — because a gradient cannot be a border-color. */
.js .depth::before {
  content: ""; position: absolute; inset: -1px; border-radius: 12px;
  padding: 1px; pointer-events: none; z-index: 3;
  background: linear-gradient(180deg,
      transparent calc(var(--p, 0) * 150% - 42%),
      rgba(206, 226, 250, .62) calc(var(--p, 0) * 150% - 6%),
      rgba(206, 226, 250, .22) calc(var(--p, 0) * 150% + 8%),
      transparent calc(var(--p, 0) * 150% + 44%));
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
          mask-composite: exclude;
}

/* --- the light behind the page -------------------------------------------

   The two blurred fields already drift on clocks of their own. This gives them
   a component that answers the scroll instead, so the light on the page moves
   because somebody is reading it. It is the cheapest thing here and it is the
   one that makes the *whole* page feel awake rather than one section of it.

   Written to `translate` and not into `transform`, because the drift is a
   keyframe animation on `transform` and one would overwrite the other. They
   are separate properties and they compose. */
.js .field   { translate: calc(var(--scroll, 0) * -7vw) calc(var(--scroll, 0) * 13vh); }
.js .field-2 { translate: calc(var(--scroll, 0) * 5vw) calc(var(--scroll, 0) * -11vh); }

/* --- the hero, leaving ---------------------------------------------------

   The first screen recedes as it is scrolled past — slower than the page, a
   little smaller, a little dimmer — so what follows arrives *over* it rather
   than after it. It is the one place on this page where something leaves under
   the reader's hand, and it is allowed here because the masthead carries the
   same two links: nothing goes away that anybody still needs. */
.js .hero .lift {
  will-change: transform, opacity;
  transform: translate3d(0, calc(var(--p, 0) * -58px), 0)
             scale(calc(1 - var(--p, 0) * .045));
  opacity: calc(1 - var(--p, 0) * .78);
}

/* --- the rule beside a section's number ----------------------------------

   It was 42 px of hairline sitting there before the number arrived. Now it is
   drawn, from the number outwards, as the number comes up the screen. Two
   properties and no new element, and it is the detail that says a person set
   this page rather than a template. */
.js .num::after { transform: scaleX(var(--p, 1)); transform-origin: 0 50%; }

/* --- the hairline between two sections ----------------------------------- */
.js section + section::before {
  transform: translateX(-50%) scaleX(var(--se, 1));
}

/* --- the reading meter ---------------------------------------------------

   One hairline in the gutter of each long list, filling as the list is read.
   It is the only mark on this page that answers *where you are* rather than
   where the page is, and it turns a wall of seven rows into something visibly
   being worked through.

   Deliberately in the gutter and not between the rows: a meter inside the
   reading column is a second column of information nobody asked for. */
.js .things, .js .rules, .js .limits, .js .faq { position: relative; }
.js .things::before, .js .rules::before, .js .limits::before, .js .faq::before {
  content: ""; position: absolute; left: -16px; top: 0; bottom: 0; width: 1px;
  background: linear-gradient(180deg, var(--steel), rgba(168, 189, 216, .18));
  transform: scaleY(var(--p, 0)); transform-origin: 50% 0;
  opacity: .5;
}

/* The row under the eye. Everything else here moves because the page moved;
   this is the one thing that answers where the reader is inside a list, and it
   is the same lit ground the pointer already gives on hover — with the padding
   step left out on purpose, because a row that shifts sideways as you scroll
   past it is the page moving text away from somebody reading it. */
.js .things > div.near { background: var(--raise); }
.js .things > div.near .n { color: var(--steel); }

/* ---------------------------------------------------------------- lightbox */

#lightbox {
  position: fixed; inset: 0; z-index: 60; display: none;
  align-items: center; justify-content: center;
  background: rgba(6, 8, 10, 0); backdrop-filter: blur(0px);
  transition: background .38s var(--ease), backdrop-filter .38s var(--ease);
}
#lightbox.on { display: flex; background: rgba(6, 8, 10, .82); backdrop-filter: blur(9px); }
#lightbox img {
  position: absolute; border-radius: 11px; border: 1px solid var(--line);
  box-shadow: 0 50px 120px -40px rgba(0, 0, 0, .95);
  transform-origin: top left; will-change: transform, width;
}
#lightbox .close {
  position: absolute; top: 22px; right: 26px; z-index: 2;
  width: 38px; height: 38px; border-radius: 50%; border: 1px solid var(--line);
  background: rgba(20, 24, 30, .7); color: var(--ink); font-size: 1.1rem; line-height: 1;
  cursor: pointer; opacity: 0; transition: opacity .3s var(--ease) .12s, transform .3s var(--ease);
}
#lightbox.on .close { opacity: 1; }
#lightbox .close:hover { transform: rotate(90deg); }

/* ----------------------------------------------------------------- footer */

footer { border-top: 1px solid var(--line); padding: 48px 0 64px; color: var(--ink-3); font-size: .9rem; position: relative; z-index: 2; }
footer a { color: var(--ink-2); }
footer .cols { display: flex; justify-content: space-between; gap: 30px; flex-wrap: wrap; }
footer p { color: var(--ink-3); }

/* -------------------------------------------------------------- responsive */

@media (max-width: 900px) {
  .split { grid-template-columns: 1fr; gap: 32px; }
  /* The deck does not survive a portrait screen: a 16:9 film in it fills a
     third of the viewport, so a pinned stage would be two thirds empty, and a
     card thrown sideways under a thumb that is already scrolling is a fight.
     The five go back to being five blocks, which is what the markup says. */
  .js .for { height: auto; }
  .js .for > .pin { position: static; height: auto; display: block; overflow: visible; }
  .js .for .deck { height: auto; width: auto; perspective: none; }
  .js .for .deck > article { position: static; opacity: 1 !important;
                             transform: none !important; padding-bottom: 40px; }
  .js .for .rail { display: none; }
  .for .words { margin-top: 16px; }
  .for h3 { font-size: 1.2rem; }
  .for p { font-size: .96rem; }
  .split.flip > figure { order: 0; }
  section { padding: 60px 0; }
  header.top nav { gap: 16px; font-size: .84rem; }
  header.top .name span, header.top .name { font-size: .92rem; }
  .grid-lines { display: none; }
  .facts { grid-template-columns: 1fr 1fr; }
  .faq button { font-size: .98rem; padding: 18px 4px; }
  .faq .a p { padding-right: 0; }
}

/* ------------------------------------------------------------------ phones

   Not the desktop layout with narrower columns. Three things change shape: the
   numbered list stops holding a column open for a two-digit number, the
   demonstrations go edge to edge because they are the smallest thing on the page
   and the page's padding was costing them a tenth of their width, and anything
   that only appeared on hover has to appear anyway — a finger has no hover. */
@media (max-width: 700px) {
  .wrap { padding: 0 20px; }
  header.top nav a:nth-child(2) { display: none; }   /* two fit, three do not */
  section { padding: 56px 0; }
  .hero { padding: 52px 0 0; }
  .lockup { gap: 16px; margin-bottom: 30px; }

  /* The forced break is a typesetting choice for a wide screen; on a phone it
     leaves a line with one word on it. The space around the tag in the markup is
     load-bearing — hide the break without it and the two sentences run together. */
  h1 br { display: none; }
  h1 { font-size: clamp(2.05rem, 8.6vw, 2.6rem); }
  .lede { font-size: 1.04rem; }

  .cta { flex-direction: column; align-items: stretch; gap: 10px; }
  .button { text-align: center; padding: 14px 22px; }

  .things > div { grid-template-columns: 1fr; gap: 7px; padding: 20px 4px; }
  .things > div:hover { padding-left: 4px; }
  .things .n { font-size: .76rem; }
  .things > div > p { grid-column: auto; }

  .facts { grid-template-columns: 1fr; margin-top: 34px; }
  .facts > div { padding: 18px 18px; }

  /* Edge to edge. A 1280-wide film of two monitors is the hardest thing on this
     page to read on a phone, and the 40 px of page padding was a tenth of it. */
  figure .frame { border-radius: 0; border-left: 0; border-right: 0; }
  figure { margin-left: -20px; margin-right: -20px; }
  figcaption { padding: 0 20px; }
  .wide { margin-top: 34px; }

  #lightbox .close { top: 14px; right: 14px; width: 46px; height: 46px; }

  /* No travelling half on a phone — the script never writes `--p` there — so
     the reading meters would be hairlines of zero height sitting in a gutter
     that does not exist at this width anyway. Said outright rather than left
     to fall out of a fallback, because a rule that happens to be invisible is
     a rule somebody will spend an afternoon on. */
  .js .things::before, .js .rules::before,
  .js .limits::before, .js .faq::before { display: none; }
}

/* A finger cannot hover, so the label that says a picture opens has to be there
   from the start — and the play/pause hint on a clip is meaningless. */
@media (hover: none) {
  .frame.open::after { opacity: 1; transform: none; }
  .things > div:hover { background: none; padding-left: 8px; }
  .faq button:hover { padding-left: 6px; }
}

/* Everything above is decoration. Somebody who has asked their system to stop
   moving things has asked for a reason, and the page still has to read. */
@media (prefers-reduced-motion: reduce) {
  /* Nothing travels, nothing drifts, nothing is pinned. The page keeps its
     shape and loses its motion, which is exactly what was asked for. */
  .js .reveal, .js .split > figure.reveal, .js .split > div > .reveal {
    transform: none !important; }
  .js .depth { transform: none !important; }
  /* The travelling half is not disabled here so much as never started: nothing
     writes `--p` or `--scroll` when this setting is on, so every `calc()` above
     falls back to the value that means "arrived". These four are the ones whose
     fallback cannot be the resting state — a rule that has not been drawn is a
     rule of zero width, and a meter that has not been read is a meter of zero
     height — so they are told outright. */
  .js .num::after, .js section + section::before,
  .js .things::before, .js .rules::before, .js .limits::before, .js .faq::before {
    transform: none !important; }
  .js .hero .lift { transform: none !important; opacity: 1 !important; }
  .js .depth::before { display: none; }
  .js .field, .js .field-2 { translate: none; }
  .js header.top .name img { rotate: none; }
  .js h1 .ln > i, .js h2 .ln > i { transform: none !important; transition: none; }
  .js .facts > div, .js .limits li::before {
    opacity: 1; transform: none !important; transition: none; }
  .js .rules li::before { opacity: 1; transition: none;
                          transform: rotate(-45deg) !important; }
  /* The deck is a picture that moves when the page does, which is the whole of
     what this setting asks us not to build. It is not disabled here but in
     `app.js`, which never pins it in the first place — CSS cannot un-write the
     transforms a script has already set. What is left is the markup's own
     shape: five blocks, one after another. */
  html { scroll-behavior: auto; }
  .field, .field-2, .lockup .mark, .lockup .sheen, .waiting::before { animation: none; }
  .js .reveal, .js .reveal.in { opacity: 1; transform: none; transition: none; }
  .frame, .button, .things > div, .faq .a, .faq button { transition: none; }
  .button:hover::after { animation: none; }
}
