/* orbit: 365 days on a spiral wrapped round a centred block of words.
 *
 * The grid is switched off entirely: orbit.js registers a `place` hook and puts
 * every day at an absolute point on the spiral, so the only thing this file has
 * to agree with the script about is `--dot` (the size of one day) and the two
 * radii it hands over for the January mark.
 *
 * Nothing here uses `transform` on a day: the engine's arrival keyframe ends on
 * `transform: none`, so a day is centred with calc() on left/top instead.
 */

/* ------------------------------------------------------------- the type

   One grotesk, set tight, for everything. Five settings were tried side by
   side at /types and this is the one that was kept; the others are in the
   history if the question ever comes back.

   Nothing is fetched: Helvetica where it exists, its clone where it does not,
   so the page is never waiting on a face to arrive. */

:root {
  --display: "Helvetica Neue", Helvetica, "Nimbus Sans", Arial, sans-serif;
  --body: "Helvetica Neue", Helvetica, "Nimbus Sans", Arial, sans-serif;
  --mono: ui-monospace, "JetBrains Mono", "SFMono-Regular", Menlo,
          "DejaVu Sans Mono", monospace;
  --display-weight: 600;
  --display-track: -.021em;
  --body-track: 0;
}

:root {
  /* The lower bound is a phone, and 6px there was wrong: a day came out 3.8px
     across, so the year read as dust settled on the border rather than as 365
     things somebody has to cover. A day is a unit of work; it has to look like
     one at arm's length. */
  --cell: clamp(11px, 1.05vw, 13px);
  --sweep-span: 700ms;   /* one full revolution of the green front */
  --sweep-dur: 440ms;     /* how long one day holds its green */
  --green: #35d47c;
  /* How far a day leaves its place when it changes hands. The room outside
     the trace (`hold` in the script) is at least 22px, so this cannot throw a
     day into the words or off the page. */
  --swap-out: clamp(15px, 1.9vw, 26px);

  --ink: #070a0e;
  --ink-lift: #0e141b;    /* the lit centre of the page: worst case for text */
  --paper: #eaf1f7;
  /* Body copy and the quieter labels. Both were a step darker, which passed a
     contrast checker against the flat page colour and then had to be read over
     a moving field anyway. Lifted until the second line of a pair is still
     clearly secondary but is not work. */
  --soft: #dbe5ee;
  --mute: #b7c6d3;
  --hair: rgba(174, 190, 204, .16);
}

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

body {
  overflow-x: clip;
  background: var(--ink);
  color: var(--paper);
  font-family: var(--body);
  letter-spacing: var(--body-track, 0);
  -webkit-font-smoothing: antialiased;
}

/* The background the finished year turns into. Fixed, so it stays put behind
   every section while the page scrolls, and behind everything: the sections
   above it are all slightly see-through so it reads through them. */
.drift {
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
}

/* Once the year has been handed over to the canvas, soften it.

   At full sharpness this is several hundred hard-edged coloured circles behind
   every paragraph on the page, and a sharp moving thing behind a sentence wins
   every time. Blurred it is the same field doing the same thing and reads as
   weather.

   Keyed to `.drifting`, which the script adds at the moment of the hand-off,
   and eased in a beat afterwards: the join has to be pixel for pixel, so the
   canvas is sharp on the frame it takes over from the day elements and only
   then goes soft. */
body.drifting .drift {
  filter: blur(2.6px);
  transition: filter 1.4s ease 700ms;
}
@media (prefers-reduced-motion: reduce) {
  body.drifting .drift { transition: none; }
}

/* The field was faded out below 640px for a while, on the grounds that 365
   balls blurred at 2.6px across a 390px screen read as bands of smear rather
   than as days. Put back: the drift is the year that was just built, and a
   phone losing it loses the one thing tying the background to the argument.
   A phone gets the same page as everything else. */

/* The moment of the hand-off. No transition on purpose -- the canvas is
   already drawing these exact circles, so a fade would show as a double
   image. */
.fill.drifting .day { opacity: 0; }

/* The curve and its January mark were the year's frame. Once the year has
   left there is nothing for them to be around, so they go too -- slowly, so
   the hero does not blink. */
.fill.drifting .trace,
.fill.drifting .mark {
  opacity: 0;
  transition: opacity 2.2s ease 600ms;
}

/* ------------------------------------------------------------- the field */

.fill {
  display: flex;
  align-items: center;
  justify-content: center;
  position: relative;
  gap: 0;
  padding: 32px clamp(12px, 3vw, 48px);
  min-height: 100vh;
  height: var(--fill-h, auto);
  overflow-x: clip;
  /* No background of its own. The drift is a fixed canvas behind the whole
     page, and anything painted here would sit on top of it and dim the year
     the moment it was handed over -- which read as the ring going grey. The
     lit centre this used to provide now belongs to the words themselves. */
  background: none;
}

/* The glass the words are read against, carried by the words rather than by
   the section, so it covers the drift only where it has to.

   This was a plain dark gradient, and a gradient does not stop a field of
   sharp little circles from being legible through it: every ball behind the
   lede was still a ball, and the eye went to the moving thing rather than the
   sentence. Frosting the same area throws the balls out of focus instead, so
   what is behind the words is a colour wash and what is beside them is still a
   year being built.

   Masked rather than edged. A rounded rectangle with a border in the middle of
   the orbit reads as a card dropped on the animation; a frosted lens with no
   edge reads as the page being clearer where you are looking. */
.copy::before {
  content: "";
  position: absolute;
  inset: -20% -30%;
  z-index: -1;
  pointer-events: none;
  background: radial-gradient(ellipse at 50% 50%,
    color-mix(in srgb, var(--ink) 88%, transparent) 0%,
    color-mix(in srgb, var(--ink) 74%, transparent) 52%,
    transparent 78%);
  backdrop-filter: blur(18px) saturate(118%);
  -webkit-backdrop-filter: blur(18px) saturate(118%);
  -webkit-mask-image: radial-gradient(ellipse at 50% 50%,
    #000 0%, #000 46%, transparent 79%);
  mask-image: radial-gradient(ellipse at 50% 50%,
    #000 0%, #000 46%, transparent 79%);
}

.copy {
  position: relative;
  z-index: 4;
  /* 68vw leaves room on either side for the orbit to come round the words.
     Below 700px it does not come round them, it goes behind them (see
     `behind` in orbit.js), so the words take the screen and only keep a
     thumb's margin. */
  width: var(--copy-w, min(560px, 68vw));
  text-align: center;
}

/* One day. Placed by the script; sized here. */
.day {
  position: absolute;
  z-index: 3;
  width: var(--dot, 8px);
  height: var(--dot, 8px);
  border-radius: 50%;
}

/* A halo under the day while the green front is passing over it. The engine
   owns the day's own animation, so the flare lives on a pseudo-element and
   only borrows the same delay. */
.day::after {
  content: "";
  position: absolute;
  inset: -170%;
  border-radius: 50%;
  background: radial-gradient(circle closest-side,
    rgba(53, 212, 124, .55), rgba(53, 212, 124, 0) 78%);
  opacity: 0;
  pointer-events: none;
}
.fill.sweeping .day.settled::after {
  animation: orbit-flare var(--sweep-dur) ease-out both;
  animation-delay: calc(var(--d, 0) * var(--sweep-span));
}
@keyframes orbit-flare {
  0%        { opacity: 0; }
  22%, 40%  { opacity: .9; }
  100%      { opacity: 0; }
}

/* ------------------------------------------- the ring the green runs round */

.trace {
  position: absolute;
  left: 50%;
  /* `--orbit-dy` is how far the year's centre sits from the section's. Zero on
     a desktop, where they share one; negative on a phone, where the year takes
     a band above the words. Set by orbit.js, which offsets the days by the
     same amount. */
  top: calc(50% + var(--orbit-dy, 0px));
  transform: translate(-50%, -50%);
  z-index: 1;
  pointer-events: none;
  overflow: visible;
}
.trace-track {
  fill: none;
  stroke: var(--hair);
  stroke-width: 1;
}
.trace-arc {
  fill: none;
  stroke: var(--green);
  stroke-width: 2;
  stroke-linecap: round;
  stroke-dashoffset: var(--len, 0);
  opacity: 0;
  filter: drop-shadow(0 0 7px rgba(53, 212, 124, .6));
}
.fill.sweeping .trace-arc {
  opacity: 1;
  animation: orbit-draw var(--sweep-span) linear both;
}
.fill.done .trace-arc { opacity: .55; stroke-dashoffset: 0; }
@keyframes orbit-draw {
  from { stroke-dashoffset: var(--len, 0); }
  to   { stroke-dashoffset: 0; }
}

/* Where the year starts, so the direction of the fill is readable. */
.mark {
  position: absolute;
  left: 50%;
  bottom: calc(50% - var(--orbit-dy, 0px) + var(--mark-r, 0px));
  transform: translateX(-50%);
  z-index: 2;
  margin: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  pointer-events: none;
}
.mark-label {
  font-size: 10px;
  font-weight: 500;
  letter-spacing: .34em;
  text-indent: .34em;
  text-transform: uppercase;
  color: var(--mute);
  white-space: nowrap;
}
.mark-line {
  width: 1px;
  height: 10px;
  background: linear-gradient(rgba(174, 190, 204, .05), rgba(174, 190, 204, .5));
}

/* ------------------------------------------------------------- the words */

.eyebrow {
  margin: 0 0 14px;
  font-size: 12px;
  font-weight: 500;
  /* Half a centimetre between letters is a logo, not a label: at .46em this
     read as texture and people skipped it. */
  letter-spacing: .26em;
  text-indent: .13em;
  text-transform: uppercase;
  color: var(--mute);
}

h1 {
  margin: 0 0 12px;
  font-size: clamp(22px, 2.7vw, 36px);
  line-height: 1.18;
  font-weight: 500;
  letter-spacing: .012em;
  color: var(--paper);
  text-wrap: balance;
}
h1 em {
  font-style: italic;
  font-weight: 400;
  font-family: var(--emphasis, ui-serif), "Iowan Old Style", Georgia, serif;
  color: #fff;
}

.lede {
  margin: 0 auto 17px;
  max-width: 46ch;
  font-size: clamp(16.5px, 1.3vw, 19.5px);
  line-height: 1.6;
  color: var(--soft);
  /* One sentence over two even lines, rather than a full line and an orphan. */
  text-wrap: balance;
}

.acts {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  gap: 10px 18px;
  margin: 0 0 17px;
}

.cta {
  font: inherit;
  font-size: 11.5px;
  font-weight: 600;
  letter-spacing: .18em;
  text-indent: .18em;
  text-transform: uppercase;
  color: #05080b;
  background: var(--paper);
  border: 1px solid var(--paper);
  border-radius: 999px;
  padding: 12px 24px;
  cursor: pointer;
  transition: background .18s ease, border-color .18s ease, color .18s ease;
}
.cta:hover:not(:disabled) { background: #fff; border-color: #fff; }
.cta:disabled {
  background: transparent;
  border-color: rgba(174, 190, 204, .38);
  color: var(--soft);
  cursor: default;
}

/* The quiet action. It is a link in most of the page and a button in the
   hero, so it is reset to look like neither. */
.ghost {
  font: inherit;
  background: none;
  border: 0;
  cursor: pointer;
  font-size: 11.5px;
  font-weight: 500;
  letter-spacing: .18em;
  text-indent: .18em;
  text-transform: uppercase;
  color: var(--soft);
  text-decoration: none;
  padding-bottom: 4px;
  border-bottom: 1px solid rgba(174, 190, 204, .42);
  transition: color .18s ease, border-color .18s ease;
}
.ghost:hover:not(:disabled) { color: var(--paper); border-bottom-color: var(--paper); }
.ghost:disabled { cursor: default; color: var(--mute); border-bottom-color: transparent; }

/* The loud action in the hero is a link now, not a button. */
.acts .cta { display: inline-block; text-decoration: none; }

.cta:focus-visible,
.ghost:focus-visible {
  outline: 2px solid var(--green);
  outline-offset: 3px;
  border-radius: 4px;
}

/* ------------------------------------------------------------ the numbers */

.readout {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: flex-start;
  gap: 10px clamp(16px, 2.2vw, 28px);
  padding-top: 14px;
  border-top: 1px solid var(--hair);
}

.r-item {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  max-width: 16ch;
}
.r-item b {
  font-size: 15px;
  font-weight: 500;
  letter-spacing: .03em;
  color: var(--paper);
  font-variant-numeric: tabular-nums;
}
.r-item i {
  font-style: normal;
  font-size: 11px;
  font-weight: 500;
  letter-spacing: .12em;
  text-indent: .08em;
  text-transform: uppercase;
  line-height: 1.5;
  color: var(--mute);
}

/* The stage word changes four times while the solver runs and is much the
   longest thing in the row. On its own line it can say whatever it likes
   without rewrapping the readout, which would drag the whole orbit in and
   out with it, since the orbit is measured off this block. */
.r-item:has(b.stage) {
  flex-basis: 100%;
  max-width: none;
  margin-top: -4px;
}

.r-item b.stage {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  font-size: 12px;
  letter-spacing: .1em;
  color: var(--soft);
  white-space: nowrap;
}
.r-item b.stage::before {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--mute);
  transition: background .3s ease;
}
.fill.done .r-item b.stage { color: var(--green); }
.fill.done .r-item b.stage::before {
  background: var(--green);
  box-shadow: 0 0 8px rgba(53, 212, 124, .8);
}

/* ------------------------------------------------------------------ calm */

@media (prefers-reduced-motion: reduce) {
  .fill.sweeping .day.settled::after { animation: none; opacity: 0; }
  .fill.sweeping .trace-arc { animation: none; opacity: .55; stroke-dashoffset: 0; }
  .cta, .ghost, .r-item b.stage::before { transition: none; }
}

/* ------------------------------------------------ waiting to be a schedule

   Before the button is pressed there is no year yet, so the days are grey and
   unassigned, circling outside the orbit. They are the same 365 elements that
   will land on the curve -- nothing is created or thrown away, they are only
   somewhere else. */

.fill.orbit-idle .day,
.fill.landing .day {
  opacity: 1;
}
.fill.orbit-idle .day {
  /* Bright enough to read as a crowd of days waiting, dim enough that it is
     plainly not the schedule yet. */
  /* Chips, not dots: squared off with a soft corner and set at their own
     angle, so a month reads as a heap of days rather than a smudge of ink. */
  background: var(--waiting, #6d7f93);
  border-radius: 2px;
  box-shadow: none;
}
/* The lane is set per day in the script; this is the lap around it. Started
   part-way through with a negative delay, so the cloud is already spread out
   on the first frame rather than filing out of one point. */
/* Each loose day wanders between three points a few pixels apart, at its own
   pace and its own size. Independent drift is what makes it read as a field
   of things floating rather than a decoration being played. */
.fill.orbit-idle .day {
  animation: orbit-float var(--fly-dur, 18s) ease-in-out infinite alternate;
  animation-delay: var(--fly-delay, 0s);
  opacity: var(--fade, .7);
}
@keyframes orbit-float {
  0% {
    transform: translate(calc(var(--px, 0px) + var(--ax, 0px)),
                         calc(var(--py, 0px) + var(--ay, 0px))) rotate(var(--rot, 0deg)) scale(var(--z, 1));
  }
  50% {
    transform: translate(calc(var(--px, 0px) + var(--bx, 0px)),
                         calc(var(--py, 0px) + var(--by, 0px))) rotate(var(--rot, 0deg)) scale(calc(var(--z, 1) * 1.08));
  }
  100% {
    transform: translate(calc(var(--px, 0px) + var(--cx, 0px)),
                         calc(var(--py, 0px) + var(--cy, 0px))) rotate(var(--rot, 0deg)) scale(var(--z, 1));
  }
}

/* The flight in. Each day carries its own lag, so the year lands a day at a
   time from January round to December rather than all at once.

   Opacity is in the list because on a rebuild each day is handed over from a
   ball in the drifting field and starts at that ball's brightness, which is
   about a third of full. Without a transition on it, every day snapped to
   full on the frame it left the canvas -- 365 small flashes announcing the
   swap the rest of this is built to hide. */
.fill.landing .day {
  transition:
    transform var(--fly-ms, 1100ms) cubic-bezier(.26, .72, .22, 1) var(--fly-lag, 0ms),
    background-color 520ms ease var(--fly-lag, 0ms),
    opacity 520ms ease var(--fly-lag, 0ms),
    box-shadow 520ms ease var(--fly-lag, 0ms);
}

/* Putting them back out for a second run is a move, not a flight: no
   transition, so the next press starts from the cloud again. */
.fill.returning .day { transition: none; }

@media (prefers-reduced-motion: reduce) {
  .fill.orbit-idle .day {
    animation: none;
    transform: translate(var(--px, 0px), var(--py, 0px)) rotate(var(--rot, 0deg)) scale(var(--z, 1));
  }
  .fill.landing .day { transition: none; }
}

/* -------------------------------------------------------------- the story */

/* The page under the orbit. Full bleed, wide rows, a hairline between each
   problem -- the shape of a ledger rather than a stack of marketing cards,
   because every line of it is a complaint somebody actually has. */

.story {
  position: relative;
  z-index: 3;
  /* Was 132/140. Two sections' worth of that met in the middle as 270px of
     black between the last question and the next heading, which reads as the
     page having stopped rather than as air. */
  padding: clamp(56px, 8vh, 100px) clamp(16px, 4.4vw, 76px) clamp(60px, 9vh, 108px);
  background: linear-gradient(
    color-mix(in srgb, var(--ink) 84%, transparent) 0%,
    color-mix(in srgb, #060a0f 90%, transparent) 100%);
  backdrop-filter: blur(20px) saturate(115%);
  -webkit-backdrop-filter: blur(20px) saturate(115%);
  border-top: 1px solid var(--hair);
}

.story > .eyebrow { margin-bottom: 18px; }

.story > h2 {
  margin: 0 0 clamp(34px, 5vw, 60px);
  max-width: 22ch;
  font-size: clamp(26px, 4.1vw, 58px);
  line-height: 1.08;
  font-weight: 500;
  letter-spacing: -.014em;
  color: var(--paper);
  text-wrap: balance;
}

.close {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 28px 48px;
  margin-top: clamp(48px, 7vw, 96px);
}
.close h2 {
  max-width: 20ch;
  font-size: clamp(20px, 2.5vw, 34px);
  line-height: 1.16;
  font-weight: 500;
  letter-spacing: -.01em;
  color: var(--paper);
  text-wrap: balance;
}
.close .cta {
  display: inline-block;
  text-decoration: none;
  flex: none;
}

/* The phone hero. Matches the `behind` threshold in orbit.js: below 700px the
   year passes behind the words instead of around them, so the words stop
   budging for it and take the screen. */
@media (max-width: 699px) {
  .copy { width: var(--copy-w, 92vw); }
  /* The year takes a band above and the words sit under it, so the section
     stops centring them and pins them to the foot instead. orbit.js measures
     what is left and spirals the year into it. */
  .fill {
    /* Vertical padding comes down from 32: on a phone every pixel of it is a
       pixel off the band the year spirals in, and there is nothing above the
       hero for it to breathe against. */
    padding: 16px 10px;
    align-items: flex-end;
  }
  /* A shorter headline is a taller year. At 360px the h1 wrapped to four lines
     instead of three, which cost 70px of band and took the days down to 2.9px.
     Tighter leading and a slightly smaller floor buys the line back. */
  h1 { font-size: clamp(20.5px, 5.6vw, 36px); line-height: 1.14; }
  .lede { line-height: 1.5; margin-bottom: 15px; }
  .acts { margin-bottom: 15px; }
  /* Three counts on one row. At 360px the labels wrapped the readout onto two
     rows, which cost 47px of band on exactly the screen that could least
     afford it. Smaller and less tracked, they fit. */
  .readout { gap: 8px 12px; padding-top: 11px; }
  .r-item { max-width: none; flex: 1 1 0; min-width: 0; }
  .r-item i { font-size: 10px; letter-spacing: .06em; text-indent: 0; }
  /* Nothing crosses the words now, so the lens only has to lift them off the
     drifting field behind the page. Tight, because a phone has no room for it
     to bleed 30% past the edges the way the desktop one does. */
  .copy::before { inset: -8% -10%; }
}

/* There was a scroll-driven `story-rise` here, and on the two blocks below.
   Every row, figure, table and card on the page lifted and faded in as it came
   up the viewport.

   Removed rather than tuned. A view() timeline is scrubbed, not fired: it does
   not play once and finish, it tracks the scroll position, so everything that
   faded in on the way down faded back out on the way up. Reading the page
   normally -- down a bit, up a bit to re-read a line -- made the whole thing
   blink. Nothing on this page needs an entrance; it is dense with real
   material and the one thing worth animating is the year at the top, which
   animates because it is genuinely being computed. */

/* --------------------------------------------------------- the rest of it */

/* Shared shell for every section below the story. Full bleed on purpose:
   there is no centred 900px column anywhere on this page.

   Frosted rather than merely see-through. These were 89% opaque over the
   drift, which let a few hundred sharp coloured dots sit behind every line of
   body copy: readable in a screenshot, tiring to actually read. The panel now
   blurs what is behind it, so the field still colours the page without any
   individual ball competing with a sentence. */
.how, .proof, .rules, .never, .facts, .pricing, .end, .foot {
  position: relative;
  z-index: 3;
  padding: clamp(56px, 7.5vh, 100px) clamp(16px, 4.4vw, 76px);
  background: color-mix(in srgb, #060a0f 90%, transparent);
  backdrop-filter: blur(20px) saturate(115%);
  -webkit-backdrop-filter: blur(20px) saturate(115%);
  border-top: 1px solid var(--hair);
}

.how > h2, .proof h2, .rules > h2, .never h2, .pricing > h2, .end h2 {
  margin: 0 0 clamp(28px, 4vw, 52px);
  max-width: 24ch;
  font-size: clamp(24px, 3.4vw, 48px);
  line-height: 1.1;
  font-weight: 500;
  letter-spacing: -.014em;
  color: var(--paper);
  text-wrap: balance;
}

/* A screenshot is a light rectangle on a dark page, so it is given an edge
   and a shadow rather than being allowed to glare off the background. */
figure img {
  border-radius: 12px;
  border: 1px solid rgba(174, 190, 204, .18);
  box-shadow: 0 26px 70px rgba(0, 0, 0, .55);
}
/* Every screenshot is wrapped in a <picture> so a phone can be handed a
   phone-shaped one. A picture is inline by default, which would leave a
   line-box's worth of dead space under each frame. */
figure picture { display: block; }
figcaption {
  margin-top: 12px;
  font-size: 14px;
  line-height: 1.55;
  color: var(--mute);
}

/* ------------------------------------------------------------ the steps */

.steps { counter-reset: step; display: grid; gap: clamp(48px, 7vw, 104px); }

.steps li {
  counter-increment: step;
  display: grid;
  grid-template-columns: minmax(0, 4fr) minmax(0, 7fr);
  gap: clamp(20px, 3.4vw, 64px);
  align-items: center;
}
/* Alternated so the eye zig-zags down the page instead of running down one
   gutter. The tracks are swapped as well as the order -- moving the picture
   into column one without that put a whole screenshot in the narrow column
   and shrank it to nothing. */
.steps li:nth-child(even) {
  grid-template-columns: minmax(0, 7fr) minmax(0, 4fr);
}
.steps li:nth-child(even) .step-words { order: 2; }

/* A year of call is too dense to read at half width, so that one runs the
   full width of the page with its words above it. */
.steps li.stretch {
  grid-template-columns: minmax(0, 1fr);
  gap: clamp(18px, 2.4vw, 34px);
}
.steps li.stretch .step-words { order: 0; }
.steps li.stretch figure img { width: 100%; }

.steps h3 {
  margin: 0 0 14px;
  font-size: clamp(19px, 2.1vw, 29px);
  line-height: 1.16;
  font-weight: 500;
  letter-spacing: -.008em;
  color: var(--paper);
}
.steps h3::before {
  content: counter(step, decimal-leading-zero);
  display: block;
  margin-bottom: 12px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .2em;
  color: var(--green);
}
.steps p {
  max-width: 44ch;
  font-size: clamp(16.5px, 1.3vw, 19px);
  line-height: 1.68;
  color: var(--soft);
}

/* ----------------------------------------------------------- the proof */

.proof-head { max-width: 62ch; margin-bottom: clamp(28px, 4vw, 48px); }
.proof-head h2 { margin-bottom: 16px; }
.proof .lede { margin: 0; max-width: 62ch; text-align: left; }

/* The fairness table is wider than it is tall and full of small numbers, so
   it gets the whole width of the page. */
.proof .wide img { width: 100%; }

/* The why panel is a tall, narrow portrait -- 672 by 1338 -- and a portrait in
   a half-page column leaves most of that column empty however the column is
   sized. It gets a track the shape of the picture instead of a share of the
   page, and the words take everything else. */
.split {
  display: grid;
  grid-template-columns: minmax(0, 1fr) clamp(300px, 26vw, 400px);
  gap: clamp(24px, 4vw, 72px);
  align-items: center;
  margin-top: clamp(48px, 7vw, 96px);
}
.split h3 {
  margin: 0 0 16px;
  font-size: clamp(20px, 2.2vw, 30px);
  line-height: 1.16;
  font-weight: 500;
  color: var(--paper);
}
.split p {
  max-width: 68ch;
  font-size: clamp(16.5px, 1.3vw, 19px);
  line-height: 1.68;
  color: var(--soft);
}
.split p + p { margin-top: 14px; }

/* Every answer the panel can give, set as the app writes them. Two columns so
   the list stands as tall as the screenshot beside it rather than as a spike
   down one edge. */
.reasons {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0 clamp(16px, 2.4vw, 40px);
  margin: 26px 0 0;
  padding: 0;
  list-style: none;
  max-width: 62ch;
}
.reasons li {
  padding: 11px 0 11px 20px;
  border-top: 1px solid var(--hair);
  font-size: clamp(15px, 1.1vw, 16.5px);
  line-height: 1.45;
  color: var(--soft);
  position: relative;
}
.reasons li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 18px;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--green);
}
@media (max-width: 860px) {
  .reasons { grid-template-columns: minmax(0, 1fr); margin-top: 18px; }
}
/* Fills its track rather than being capped by height and then centred inside
   a column twice its width, which is what left a quarter of the page black. */
.split figure img { width: 100%; height: auto; }

/* ----------------------------------------------------------- the rules */

.rule-cols {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.25fr);
  gap: clamp(28px, 4.5vw, 88px);
  align-items: start;
}
.rule-cols h3 {
  margin: 0 0 20px;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: .2em;
  text-transform: uppercase;
  color: var(--mute);
}
.hard ul li {
  padding: 14px 0 14px 26px;
  border-bottom: 1px solid var(--hair);
  font-size: clamp(16.5px, 1.3vw, 19px);
  color: var(--paper);
  position: relative;
}
.hard ul li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 50%;
  width: 10px;
  height: 10px;
  margin-top: -5px;
  border-radius: 50%;
  border: 2px solid var(--green);
}
.note {
  margin-top: 18px;
  font-size: 14.5px;
  line-height: 1.6;
  color: var(--mute);
  max-width: 46ch;
}

/* The weights are the numbers, so the bar is drawn from the number itself --
   no second set of figures to fall out of step with the code. */
.weights li {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: 12px;
  padding: 11px 0;
  border-bottom: 1px solid var(--hair);
  position: relative;
}
.weights li::before {
  content: "";
  position: absolute;
  left: 0;
  bottom: 0;
  height: 2px;
  width: calc(var(--w) / 150 * 100%);
  background: linear-gradient(90deg,
    color-mix(in srgb, var(--green) 30%, transparent), var(--green));
}
.weights span {
  font-size: clamp(15.5px, 1.15vw, 17.5px);
  color: var(--paper);
}
.weights b {
  font-size: 13px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--mute);
}

/* ----------------------------------------------------------- the rest */

/* A four-word headline and one sentence under it left two thirds of a wide
   screen black. They sit side by side instead: the claim, and what it rests
   on, on one line of the page. */
.never { text-align: left; }
.never .never-row {
  display: grid;
  grid-template-columns: minmax(0, 5fr) minmax(0, 6fr);
  gap: clamp(24px, 4vw, 72px);
  align-items: start;
}
.never h2 { margin: 0; max-width: 16ch; }
.never .lede { margin: 0; max-width: 52ch; text-align: left; }
@media (max-width: 860px) {
  .never .never-row { grid-template-columns: minmax(0, 1fr); gap: 18px; }
}

.fact-grid {
  display: grid;
  /* Three columns, three facts: one row, no empty cells. The count has been
     eight, then five (one card alone beside three lit empty cells, which
     reads as a bug), then six. It is three because the other three -- the
     calendar feed, more than one duty, and the board on a screen -- are all
     stated in the price table now, and saying a thing twice on one page is
     what the whole cut is against. */
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 1px;
  background: var(--hair);
  border: 1px solid var(--hair);
  border-radius: 12px;
  overflow: hidden;
}
.fact-grid li {
  padding: clamp(20px, 2vw, 30px);
  /* A shade more solid than the sections: this is small text in small boxes. */
  background: color-mix(in srgb, #060a0f 93%, transparent);
}
.fact-grid h3 {
  margin: 0 0 10px;
  font-size: clamp(16px, 1.28vw, 19px);
  line-height: 1.28;
  font-weight: 500;
  color: var(--paper);
}
.fact-grid p {
  font-size: 15px;
  line-height: 1.62;
  color: var(--soft);
}

/* ---------------------------------------------------------- the price

   Same hairline-grid construction as the facts: three cells of one panel
   rather than three cards floating on the page, so a price reads as a row in
   a table somebody has published and not as an offer being made at you. */

/* The pricing section is a sentence now rather than three columns, and a
   sentence set to the full width of a 1400px page is not read. */
.pricing-say { max-width: 62ch; margin-top: 6px; }

.tiers {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 1px;
  background: var(--hair);
  border: 1px solid var(--hair);
  border-radius: 12px;
  overflow: hidden;
}
.tiers > li {
  display: flex;
  flex-direction: column;
  padding: clamp(24px, 2.4vw, 36px);
  background: color-mix(in srgb, #060a0f 93%, transparent);
}
/* The middle one is the one most departments land on, marked by being a shade
   lighter rather than by a badge shouting about it. */
.tiers > li.mid { background: color-mix(in srgb, #0b131b 94%, transparent); }
.tiers h3 {
  margin: 0 0 18px;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: .2em;
  text-transform: uppercase;
  color: var(--mute);
}
.fig {
  display: flex;
  align-items: baseline;
  gap: 9px;
  margin: 0 0 6px;
}
.fig b {
  font-family: var(--display);
  font-size: clamp(34px, 3.4vw, 46px);
  font-weight: 500;
  letter-spacing: -.02em;
  color: var(--paper);
  font-variant-numeric: tabular-nums;
}
.fig i {
  font-style: normal;
  font-size: 13px;
  color: var(--mute);
}
.per-year {
  margin: 0 0 22px;
  font-size: 14.5px;
  color: var(--mute);
}
.tiers > li > ul { margin: 0; padding: 0; list-style: none; }
.tiers > li > ul li {
  padding: 11px 0 11px 22px;
  border-top: 1px solid var(--hair);
  font-size: clamp(15px, 1.1vw, 16.5px);
  line-height: 1.5;
  color: var(--soft);
  position: relative;
}
.tiers > li > ul li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 19px;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  border: 1.5px solid var(--green);
}

.end {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 28px 48px;
}
.end h2 { margin: 0 0 12px; }
.end p { color: var(--soft); font-size: clamp(16.5px, 1.3vw, 19px); }
.end .cta { display: inline-block; text-decoration: none; flex: none; }

/* ---------------------------------------------------------- the foot

   The page used to stop at the demo button, so somebody who had decided had
   nowhere to go and no way to ask a question. */

.foot {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 14px 48px;
  padding-block: clamp(28px, 3vh, 40px);
  font-size: 14.5px;
  color: var(--mute);
}
.foot p { margin: 0; }
.foot-brand {
  font-size: 12px;
  font-weight: 600;
  letter-spacing: .34em;
  text-indent: .34em;
  text-transform: uppercase;
  color: var(--paper);
}
.foot-brand span {
  font-weight: 400;
  letter-spacing: .12em;
  text-indent: 0;
  color: var(--mute);
}
.foot-say a { color: var(--paper); text-decoration: none; border-bottom: 1px solid var(--hair); }
.foot-say a:hover { border-bottom-color: var(--paper); }
.foot-nav { display: flex; flex-wrap: wrap; gap: 10px 22px; }
.foot-nav a { color: var(--mute); text-decoration: none; }
.foot-nav a:hover { color: var(--paper); }

@media (max-width: 1180px) {
  .fact-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 980px) {
  .tiers { grid-template-columns: minmax(0, 1fr); }
}

/* A phone is not a small desktop.
 *
 * This block used to keep the desktop screenshots at full size below 860px
 * and let each one scroll sideways inside its own frame, on the reasoning
 * that a 1500px screen scaled down to fit is a grey smudge. It is, but the
 * cure was worse: nobody wants to drag a picture around to read a website.
 *
 * The pictures are shot twice instead -- see scripts/shoot_app_screens.py --
 * and stage.html hands the phone-width ones to phones with <picture>. So
 * everything here is layout only, and every image simply fits. */
@media (max-width: 860px) {
  .steps li, .steps li:nth-child(even), .split, .rule-cols {
    grid-template-columns: minmax(0, 1fr);
  }
  .fact-grid { grid-template-columns: minmax(0, 1fr); }
  .steps li:nth-child(even) .step-words { order: 0; }
  /* The three that were sized or shifted for a two-column page: at one
     column they are all just the width of the page. */
  .steps li.stretch figure img,
  .proof .wide img,
  .split figure img { max-height: none; width: 100%; margin-left: 0; }

  /* Air that reads as generous on a 27in screen reads as a long walk on a
     phone, because one column stacks every gap that used to sit side by
     side. The page was 11,445px on a 390px handset -- thirteen and a half
     screens of scrolling -- and roughly 1,500px of that was section padding
     alone (11vh top and bottom, eight times over). Nothing is removed here;
     the same words are simply set closer together, which is what the reader
     asked for when they said it was overwhelming. */
  .story,
  .how, .proof, .rules, .never, .facts, .pricing, .end { padding-block: 44px; }
  .story > h2, .how > h2, .proof h2, .rules > h2 { margin-bottom: 22px; }
  .steps { gap: 40px; }
  .steps li { gap: 16px; }
  .asked li { padding-block: 18px; }
  .asked .a-new { margin-top: 9px; }
  .proof-head { margin-bottom: 22px; }
  .split { margin-top: 40px; gap: 20px; }
  .rule-cols { gap: 30px; }
}


/* ------------------------------------------------------------- the bar */

.topnav {
  justify-content: space-between;
  padding: 12px clamp(16px, 4.4vw, 76px);
  background: color-mix(in srgb, var(--ink) 82%, transparent);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  border-bottom: 1px solid var(--hair);
}
.topnav .brand {
  font-size: 12px;
  font-weight: 600;
  letter-spacing: .34em;
  text-indent: .34em;
  text-transform: uppercase;
  color: var(--paper);
  text-decoration: none;
}
.topnav nav { flex-wrap: wrap; }
.topnav nav a {
  font-size: 13px;
  color: var(--soft);
  text-decoration: none;
  transition: color .18s ease;
}
.topnav nav a:hover { color: var(--paper); }
.topnav .cta.small {
  padding: 9px 18px;
  font-size: 10.5px;
  text-decoration: none;
}
@media (max-width: 640px) {
  .topnav nav { display: none; }   /* the page is short enough to scroll */
}

/* ------------------------------------------------------ the second layer

   One document, two lengths. Anything marked `long` is the desktop page's
   second layer -- the sentence under the claim, the fourth and fifth example,
   the feature list under the price -- and a phone does not get it.

   One document rather than two: a separate mobile page means two copies of
   every sentence to keep in step, user-agent sniffing that misfires on
   tablets, and -- because Google indexes the mobile version -- a search engine
   that only ever sees the short one. That is a bad trade for a page whose job
   is selling. The words a phone does not render still travel, but they are a
   couple of kilobytes against a 222KB screenshot.

   Marked at the smallest element that still reads as a whole thought when it
   goes: a list item, a paragraph, a section. Never a clause. */
@media (max-width: 860px) {
  /* Prefixed with `body`, which looks redundant and is not.
     A list item here is already given a display by the list it is in --
     `.asked li` and `.steps li` are both grid, both (0,1,1). A bare `.long`
     is (0,1,0) and loses to them outright; `li.long` only ties, and a tie is
     broken by source order, so whether the second layer disappears depended
     on this block sitting below `.asked li` in the file. It did not.
     `body li.long` is (0,1,2) and wins wherever it is written. */
  body .long,
  body li.long { display: none; }
}

/* Anchored jumps land under the bar, not behind it. */
.story, .how, .proof, .rules, .never, .facts, .pricing { scroll-margin-top: 64px; }


/* Which family carries what. Written last on purpose: it is the one place the
   whole page's type is decided, and it overrides the per-element tracking the
   sections were written with so that a setting is a setting rather than five
   headings that each drifted. */
h1, h2, h3,
.steps h3, .split h3, .fact-grid h3, .close h2, .end h2 {
  font-family: var(--display);
  font-weight: var(--display-weight);
  letter-spacing: var(--display-track);
}

/* The small, spaced, upright things: labels, buttons, counts. In most settings
   they stay in the body face; the console setting hands them to the mono. */
.eyebrow, .cta, .ghost, .mark-label, .rule-cols h3,
.readout b, .weights b, .steps h3::before {
  font-family: var(--small, inherit);
}

/* The questions, and whether the schedule can answer them.

   The question was set in italic serif to mark it as somebody speaking. On a
   dark screen at this size that is the hardest thing on the page to read:
   thin strokes, sloped, reversed out. It is the same grotesk as everything
   else now, held apart by weight, size and a green rule instead. */
.asked { list-style: none; margin: 0; padding: 0; }
.asked li {
  display: grid;
  grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
  gap: 4px clamp(14px, 3vw, 56px);
  padding: clamp(22px, 3vw, 34px) 0;
  border-top: 1px solid var(--hair);
  align-items: start;
}
.asked li:last-child { border-bottom: 1px solid var(--hair); }
.asked .q {
  grid-column: 1;
  /* One answer each now, not two, so the question is one row like it. */
  grid-row: 1;
  font-family: var(--display);
  font-weight: 500;
  font-size: clamp(19px, 2vw, 26px);
  line-height: 1.26;
  letter-spacing: -.012em;
  color: #fff;
  max-width: 21ch;
  text-wrap: balance;
}
.asked .a-new {
  grid-column: 2;
  grid-row: 1;
  max-width: 62ch;
  font-size: clamp(16.5px, 1.3vw, 19px);
  line-height: 1.62;
  padding-left: 16px;
  border-left: 2px solid var(--green);
  color: var(--paper);
}

@media (max-width: 860px) {
  .asked li { grid-template-columns: minmax(0, 1fr); }
  .asked .q, .asked .a-new { grid-column: 1; grid-row: auto; }
}

/* ------------------------------------------------- the phone variants

   TEMPORARY, with the `?hero=` layer in orbit.js. Five ways the hero could
   work on a phone, so they can be compared on a real handset. When one is
   chosen it becomes the only one and the rest of this block goes.

   Nothing here applies above 700px: every option is identical on a desktop. */

@media (max-width: 699px) {
  /* a encircle, e field, b snake: the words sit in the middle of the year, so
     the section centres them again rather than pinning them to its foot. */
  body[data-hero="a"] .fill,
  body[data-hero="b"] .fill,
  body[data-hero="e"] .fill,
  body[data-hero="f"] .fill { align-items: center; }

  /* a is the desktop composition and needs the room the desktop leaves: the
     ring has to get round the words, and at 92vw it cannot. */
  body[data-hero="a"] .copy { width: var(--copy-w, 74vw); }

  /* b, d and e are not rings, so there is no curve to draw and no January at
     the top of it. */
  body[data-hero="b"] .trace,
  body[data-hero="d"] .trace,
  body[data-hero="e"] .trace,
  body[data-hero="b"] .mark,
  body[data-hero="d"] .mark,
  body[data-hero="e"] .mark { display: none; }

  /* b and e run days under the whole copy block rather than past its edges,
     so the lens has to be a real one again. */
  body[data-hero="b"] .copy::before,
  body[data-hero="e"] .copy::before,
  body[data-hero="f"] .copy::before {
    inset: -12% -14%;
    backdrop-filter: blur(16px) saturate(115%);
    -webkit-backdrop-filter: blur(16px) saturate(115%);
    background: radial-gradient(ellipse at 50% 50%,
      color-mix(in srgb, var(--ink) 88%, transparent) 0%,
      color-mix(in srgb, var(--ink) 70%, transparent) 54%,
      transparent 80%);
  }
}

/* f only: the canvas the whole build happens on. Over the drift it is about to
   become, under the words, and gone as soon as it has handed over. The day
   elements are still there, still coloured by the engine, still the thing the
   canvas reads every frame; they are simply never painted. */
body[data-hero="f"] .day { opacity: 0 !important; }
/* Outside the phone block: `f` runs at every width, because a sphere is a
   different idea rather than a narrow-screen compromise. There is no ring to
   draw and no January at the top of one. */
body[data-hero="f"] .trace,
body[data-hero="f"] .mark { display: none; }
.globe {
  position: fixed;
  inset: 0;
  z-index: 3;
  pointer-events: none;
}
/* The near wall of the ball, above the copy's z-index of 4, so the days in
   front of the headline pass over it instead of behind it. They are painted
   translucent, so the words stay readable through them. */
.globe.front { z-index: 5; }
