/* Shared motion layer for all six pages: smooth scroll, line level arrival,
   block reveal, and the custom cursor. This file is the only place any of
   those four systems is described, so a change to the feel of the site is a
   change to one rule here rather than the same rule in index.html and in
   inner.css. It is linked last on every page, after inner.css and pages.css.

   Everything below defaults to the finished, at rest state. The animated
   state is added by .js and, for anything that moves, inside a
   prefers-reduced-motion: no-preference query, so a visitor with JavaScript
   off or reduced motion on sees the page already arrived. */

:root{
  /* The one curve on the site that crosses 1 on its way to rest, and it has to
     cross it far enough to see. The inherited (0.3,1.18,0.64,1) peaked at
     1.0098, which on the geometry it is spent on is 0.04px past a bar's travel
     and 0.44 degrees past its rotation:an approach shape, not an overshoot.
     This one peaks at 1.0978 at 58% of the duration, which is 0.39px and 4.40
     degrees past on the hamburger and 1.06px past on the chip, two device
     pixels at 2x. It is spent in exactly two places, both of them below.
     Anywhere else it would read as bounce for its own sake. */
  --ease-overshoot:cubic-bezier(0.34,1.56,0.64,1);
}

/* Spend one:the hamburger morph. The two bars travel and rotate on the same
   0.55s the shell always gave them, on the curve that overshoots instead of
   the viscous ease-out that used to swallow the gesture's whole character.
   Spend two: the hero chip image on hover, on Home and on About.

   Both live here rather than beside the rest of their rule in index.html and
   inner.css, which is where they were written twice. The header and the chip
   are shell, and the shell is described in two files; the curve they move on
   is motion, and motion is described in this one. motion.css is linked after
   both on every page, so these win at equal specificity. */
.menu-trigger span{transition:top .55s var(--ease-overshoot),transform .55s var(--ease-overshoot)}
.hero-chip img,.about-hero-chip img{transition:transform .65s var(--ease-overshoot)}

/* The pill press, for the same reason: it is motion, and it was written twice
   and applied never. `.btn:active{transform:scale(.97)}` stood in both
   index.html and inner.css, and motion.js wrote an inline transform for the
   pointer lean on every pointermove. An inline style outranks a rule, so under
   a mouse a pill acknowledged a click with nothing at all. The lean now
   arrives as two custom properties and this is the one place the lean and the
   press compose, so neither has to win. The press also has to land inside the
   click: 0.45s of viscous ease reached .97 at 410ms, and a click is released
   at about 150ms, so the press runs in 120ms and only the release relaxes on
   the slow curve the pill is otherwise transitioned on. */
.btn{transform:translate3d(var(--btn-lean-x,0px),var(--btn-lean-y,0px),0) scale(var(--btn-press,1))}
.btn:active{--btn-press:.97;transition:color .4s var(--ease),transform .12s var(--ease)}

/* ------------------------------------------------------------------ *
 * Smooth scroll
 * ------------------------------------------------------------------ */

/* Lenis writes these classes on <html> while it is running. Without the
   rules the browser keeps its own scrolling underneath the smoothed one and
   the two fight. Lenis is never constructed under reduced motion, so none of
   these classes exist there and the page is on native scroll. */
html.lenis,html.lenis body{height:auto}
.lenis.lenis-smooth{scroll-behavior:auto!important}
/* The works carousel is not fenced off from Lenis. It used to carry
   data-lenis-prevent with an overscroll-behavior:contain rule here, on the
   reasoning that its own horizontal wrap math needed the gesture. It did not:
   the wrap runs on scrollLeft, which Lenis never touches, and Lenis already
   hands a purely horizontal wheel back to the browser untouched. What the pair
   actually did was swallow vertical wheel over the strip. A stationary pointer
   that the strip scrolled under lost every tick while a Lenis animation was in
   flight, and got a native jump instead of the eased curve when it was not. */
.lenis.lenis-stopped{overflow:hidden}
.lenis.lenis-smooth iframe{pointer-events:none}

/* ------------------------------------------------------------------ *
 * Line level arrival
 * ------------------------------------------------------------------ */

/* A display heading arrives one rendered line at a time, each line rising
   out of its own clipped box 90ms after the one above it.

   Two kinds of line exist and they share this mechanism. A heading with an
   image or a link inside a line carries data-reveal-lines="manual" and
   spells its lines out in the markup, because only the author knows which
   line the chip belongs on. Every other display heading carries a bare
   data-reveal-lines and is split by motion.js against the lines the browser
   actually rendered, measured after the fonts land and measured again on
   resize. The split preserves the heading's text exactly, so its accessible
   name is what it always was and no aria is needed to repair it.

   data-lines-ready means the heading is above the fold and waits for the
   loader instead of for a scroll. */
.reveal-line{overflow:hidden}
/* Only lines motion.js built. A hand written line brings its own geometry,
   which is usually a flex row holding a chip. */
/* The display type is set tighter than its own glyphs (line-height 0.92 of
   font-size on the page titles), so a clip box drawn at the line box cuts
   the ascenders and the descenders. Each built line pads its box out by
   .06em above and .16em below and pulls the same amount back off as margin,
   so a split heading occupies exactly the height it did unsplit.

   That cancellation only holds if none of those negative margins collapse,
   and in normal flow two of them do. The first and last line's margins
   collapse out through the heading, which made the Work title 138.48px
   against the 111.77px it occupies unsplit. Between two lines the adjacent
   -.16em and -.06em collapse to the more negative of the pair rather than
   summing, which left every gap .06em too tall: Contact's three line title
   measured 342.59px against 328.03px. Flex items collapse no margins at
   all, so the column resolves both at once. It is applied by the class
   motion.js sets when it actually splits a heading, so a heading that is
   never split, under reduced motion or with JavaScript off, stays the plain
   block it was authored as. */
.js [data-reveal-lines].is-split{display:flex;flex-direction:column}
.reveal-line--auto{display:block;padding:.06em 0 .16em;margin:-.06em 0 -.16em}
.reveal-line--auto>.reveal-line-inner{display:block}

@media (prefers-reduced-motion:no-preference){
  .js [data-reveal-lines] .reveal-line-inner{
    transition:transform 1s var(--ease);
    transition-delay:var(--line-delay,0ms);
  }
  .js [data-reveal-lines]:not(.is-lines-in) .reveal-line-inner{transform:translateY(110%)}
}

/* ------------------------------------------------------------------ *
 * Block reveal
 * ------------------------------------------------------------------ */

/* One preset for all six pages, on the numbers the reference site uses.
   The delay is a custom property rather than a fixed value so the observer
   can walk a batch of siblings in 100ms steps, and so a page with its own
   choreography (the work wall pairs its cards at 90ms in pages.css) can
   still say something more specific and win. */
[data-reveal]{opacity:1;transform:none}
@media (prefers-reduced-motion:no-preference){
  .js [data-reveal]{
    opacity:0;
    transform:translateY(70px);
    transition:opacity 1s ease,transform 2s var(--ease);
    transition-delay:calc(var(--reveal-index,0) * 100ms);
  }
  .js [data-reveal].is-visible{opacity:1;transform:none}
}

/* The second preset, for the one line on About the whole page is built
   around: slower, on the decisive ease-in curve, with the closing mark and
   quote drawing in after the line lands. It sat in inner.css until this
   round, which meant the rule that hid the sentence lived in a different
   file from the code that shows it, and one deploy that dropped this file
   left the sentence invisible. Both presets are here now, so a page that
   gets one gets the other.

   The at rest state is plain visible type, as it is above: nothing here
   hides anything until .js and a no-preference motion query both say so. */
[data-reveal-peak]{opacity:1;transform:none}
[data-reveal-peak] .philosophy-mark,[data-reveal-peak] .philosophy-quote--close{display:inline-block}
@media (prefers-reduced-motion:no-preference){
  .js [data-reveal-peak]{opacity:0;transform:translateY(72px);transition:opacity 1.5s var(--ease-in),transform 1.5s var(--ease-in)}
  .js [data-reveal-peak].is-visible{opacity:1;transform:none}
  .js [data-reveal-peak] .philosophy-mark,
  .js [data-reveal-peak] .philosophy-quote--close{
    opacity:0;
    transform:scale(.5);
    transition:opacity .5s var(--ease) 1.05s,transform .65s var(--ease-viscous) 1.05s;
  }
  .js [data-reveal-peak].is-visible .philosophy-mark,
  .js [data-reveal-peak].is-visible .philosophy-quote--close{opacity:1;transform:none}
}

/* ------------------------------------------------------------------ *
 * Cursor
 * ------------------------------------------------------------------ */

/* One vocabulary, six states, all six pages. The native pointer is never
   hidden, so precision is the browser's and this only ever adds a mark
   beside it. It is drawn only where a real pointer exists and only when
   motion is welcome. */
.cb-cursor{
  position:fixed;
  left:0;
  top:0;
  z-index:500;
  display:none;
  width:64px;
  height:64px;
  margin:-32px 0 0 -32px;
  align-items:center;
  justify-content:center;
  border-radius:50%;
  background:transparent;
  opacity:0;
  pointer-events:none;
  transform:translate3d(-100px,-100px,0);
  contain:layout style size;
  will-change:transform;
  transition:opacity .15s linear;
}
.cb-cursor::before{
  content:"";
  position:absolute;
  left:6px;
  top:6px;
  width:64px;
  height:64px;
  border:1.5px solid transparent;
  border-radius:50%;
  background:var(--ink);
  transform:scale(.09375);
  transition:transform .35s var(--ease),opacity .2s linear,background-color .15s linear,border-color .15s linear;
}
.cb-cursor-label{
  position:relative;
  z-index:1;
  color:var(--paper);
  font-family:var(--mono);
  font-size:14px;
  line-height:1;
  opacity:0;
  pointer-events:none;
  white-space:nowrap;
  transition:opacity .2s ease;
}
.page-leaving .cb-cursor{opacity:0!important}
@media (hover:hover) and (pointer:fine) and (prefers-reduced-motion:no-preference){
  .cb-cursor{display:flex}
}

/* The mark is invisible until it has something to say. */
.cb-cursor.is-link,
.cb-cursor.is-ring,
.cb-cursor.is-drag,
.cb-cursor.is-menu,
.cb-cursor.is-view{opacity:1}

/* Link, the state a visitor meets most. The dot grows from 6px to 7.7px,
   which is enough to feel and small enough to stay out of the way. Its
   centre rides 6px below and right of the pointer, which is the ::before
   offset above with the box centred on the pointer, and when that centre
   lands on ink, a glyph or a mark, the dot drops to a third opacity so the
   letterform reads straight through it. Nothing opaque ever sits on a
   letter. */
.cb-cursor.is-link::before{transform:scale(.12)}
.cb-cursor.is-link.is-on-ink::before{opacity:.34}

/* Ring, on the hero chips. */
.cb-cursor.is-ring::before{left:0;top:0;background:transparent;border-color:var(--ink);transform:scale(1)}
/* Drag, on the works carousel. */
.cb-cursor.is-drag::before{left:0;top:0;background:var(--ink);transform:scale(1)}
.cb-cursor.is-drag .cb-cursor-label{opacity:1}
/* Menu, on the header trigger. */
.cb-cursor.is-menu::before{left:0;top:0;background:transparent;border-color:var(--ink);transform:scale(.78)}
/* View, on a card that opens something. The label is the data-cursor value. */
.cb-cursor.is-view::before{left:0;top:0;background:var(--accent);transform:scale(1)}
.cb-cursor.is-view .cb-cursor-label{color:var(--ink);opacity:1}
/* The accent disc against the Turtle Tuesday artwork's orange ground
   (#e6824f) reads 1.17:1, which is a smudge rather than a disc. On a media
   box marked data-cursor-tone="warm" the disc turns black instead: 7.23:1
   against that ground, with the label at 19.8:1 inside it. */
.cb-cursor.is-view.is-tone-warm::before{background:var(--ink)}
.cb-cursor.is-view.is-tone-warm .cb-cursor-label{color:var(--paper)}

/* Over a dark section the mark flips so it stays visible. */
.cb-cursor.is-inverse:not(.is-view):not(.is-ring):not(.is-menu)::before{background:var(--paper)}
.cb-cursor.is-inverse.is-ring::before{background:transparent;border-color:var(--paper)}
.cb-cursor.is-inverse.is-menu::before{border-color:var(--paper)}

/* Press. Both sizes of mark contract while the pointer is down, in 150ms
   rather than the 350ms it takes to change state, so the acknowledgement lands
   inside the click instead of after it.

   They do not contract by the same ratio, because they are not the same size.
   0.92 takes the 64px disc down 5.1px, which is the acknowledgement it was
   written to be. The same 0.92 took the 7.7px link dot down 0.61px, which at
   1x is not an acknowledgement at all. The dot states take the reference's own
   dot ratio instead, 0.30 of its 0.38, which is 0.79: 7.68px to 6.06px, a
   1.6px contraction, 3.2 device pixels at 2x. */
.cb-cursor.is-active::before{transform:scale(.074);transition-duration:.15s}
.cb-cursor.is-link.is-active::before{transform:scale(.0947)}
.cb-cursor.is-ring.is-active::before,
.cb-cursor.is-drag.is-active::before,
.cb-cursor.is-view.is-active::before{transform:scale(.92)}
.cb-cursor.is-menu.is-active::before{transform:scale(.7176)}
