/* Spectrum Impact. Motion layer, CSS half.

   Two jobs only:
   1. the few rules Lenis needs (copied from lenis@1.3.8/dist/lenis.css so the
      page never has to fetch a second stylesheet),
   2. the resting states the reveals animate out of.

   Every resting state is scoped under html.js-motion. That class is added by
   the inline bootstrap in index.html and only when motion is wanted and safe.
   Without it this file changes nothing and the page renders complete. Once an
   element has been revealed it carries .m-in, which takes its resting state
   away for good, so the finished page holds no leftover transforms. */

/* ------------------------------------------------------------------ lenis */

html.lenis, html.lenis body { height: auto; }
.lenis:not(.lenis-autoToggle).lenis-stopped { overflow: clip; }
.lenis.lenis-smooth [data-lenis-prevent] { overscroll-behavior: contain; }
.lenis.lenis-smooth iframe { pointer-events: none; }

/* ---------------------------------------------------------- resting state */

/* Masked headings. The mask is .ln, the mover is .ln-in. */
html.js-motion [data-lines]:not(.m-in) .ln-in { transform: translateY(105%); }

/* Fade plus rise. */
html.js-motion [data-fade]:not(.m-in) { opacity: 0; transform: translateY(18px); }
/* The footer's barcode strip only fades, in place: its rise pushed it 18px down, over the copyright line
   under it, for as long as the reveal was pending. */
html.js-motion .foot-strip[data-fade]:not(.m-in) { transform: none; }

/* Nav entrance. The nav element itself is left alone: app.js owns its
   transform for the hide on scroll, so only its three children move. */
html.js-motion .nav-logo:not(.m-in),
html.js-motion .nav-links:not(.m-in),
html.js-motion .nav-cta:not(.m-in) { opacity: 0; transform: translateY(-10px); }

/* Framed media. */
html.js-motion [data-clip]:not(.m-in) { clip-path: inset(14% 9% 14% 9%); }

/* Portfolio wall. */
html.js-motion #pf-grid .tile:not(.m-in) { opacity: 0; transform: translateY(14px); }

/* Statement paragraph, lit word by word with scroll. */
html.js-motion [data-words] .m-w { opacity: 0.16; }

/* Marquee groups built by motion.js. .marquee-track is styled in site.css. */
html.js-motion .marquee-track { backface-visibility: hidden; }

/* The home ticker. Still, it is a wrapped row (a swiped one on a phone, css/site.css). With motion it is
   one row from the first paint, so nothing jumps when js/motion.js starts to roll it, and its far edge
   fades out so names leave the band and do not hit a wall. */
html.js-motion .tk-view { overflow: hidden; -webkit-mask-image: linear-gradient(90deg, #000 calc(100% - 96px), transparent); mask-image: linear-gradient(90deg, #000 calc(100% - 96px), transparent); }
html.js-motion .tk-track { flex-wrap: nowrap; width: max-content; backface-visibility: hidden; will-change: transform; }
html.js-motion .tk-item { flex: none; border-bottom: 0; margin-bottom: 0; }

/* The shared journal hover image, created by motion.js. .m-fit is the wrapper
   js/motion.js scales when a picture brings a black mat of its own, so the
   photograph fills the frame and not the mat. */
html.js-motion .follow { will-change: transform, opacity; }
html.js-motion .follow img { will-change: transform; }
.follow .m-fit { display: block; width: 100%; height: 100%; transform-origin: 0 0; }
/* Docked on the press ledger with no room left under the year: the picture stays away (js/motion.js). */
.follow.is-off { visibility: hidden; }

/* ------------------------------------------------------------------ guard */

/* The bootstrap never sets js-motion under reduced motion, so this only
   matters if the preference changes while the page is open. Content stays
   put, the word paragraph stays readable, nothing drifts. */
@media (prefers-reduced-motion: reduce) {
  html.js-motion [data-lines] .ln-in,
  html.js-motion [data-fade],
  html.js-motion [data-clip],
  html.js-motion .nav-logo,
  html.js-motion .nav-links,
  html.js-motion .nav-cta,
  html.js-motion #pf-grid .tile,
  html.js-motion [data-parallax] { opacity: 1 !important; transform: none !important; clip-path: none !important; }
  html.js-motion [data-words] .m-w { opacity: 1 !important; }
  html.js-motion .marquee-track { transform: none !important; }
  html.js-motion .tk-view { overflow-x: auto; -webkit-mask-image: none; mask-image: none; }
  html.js-motion .tk-track { transform: none !important; }
  html.js-motion .tk-item[aria-hidden] { display: none; }
  html.js-motion .follow { display: none !important; }
}

/* Hint the compositor only while the motion layer is live. js/motion.js clears it per line once revealed. */
html.js-motion .ln-in { will-change: transform; }

/* ------------------------------------------------------------------ touch */

/* The phone's share of the delight. On a desktop the page answers the
   pointer: magnetic buttons, tile wipes, rows that dim around the one you
   are on, doors that fill. None of that exists without hover, so js/motion.js
   hands out classes instead (.is-lit, .is-peek, .is-press and friends) and
   the looks live here. Everything sits under html.js-motion, so reduced
   motion, ?static and the panic path switch the lot off at once, and inside
   (hover: none) wherever it mirrors a hover rule, so a desktop never sees it.
   Transform, opacity and clip-path only. */

:root { --spring: cubic-bezier(0.34, 1.56, 0.64, 1); }
@supports (transition-timing-function: linear(0, 1)) {
  /* A damped spring, ratio 0.5, sampled. One overshoot, then rest. */
  :root { --spring: linear(0, 0.145, 0.449, 0.76, 0.994, 1.124, 1.163, 1.141, 1.092, 1.041, 1.002, 0.98, 0.973, 0.977, 0.985, 0.993, 0.999, 1.003, 1.004, 1.004, 1.003, 1.001, 1); }
}

@keyframes m-press { to { scale: var(--press, 0.97); } }
@keyframes m-release { from { scale: var(--press, 0.97); } to { scale: 1; } }

@media (hover: none) {

  /* Scroll spotlight. The row, journal line or door crossing the middle of
     the screen takes the look a mouse would give it, its neighbours step
     back. Three things differ from the (hover: hover) rules in site.css, all
     learned on a real phone. The headline is never nudged sideways: on a
     narrow column it breaks the left edge it shares with its copy. The step
     back is gentler, 0.62, because the dimmed copy still has to be read. And
     it is applied to the big type and the picture only, never to the row as a
     whole, so the small mono lines (date, outlet, count) keep their full
     contrast. js/motion.js leaves .has-lit off long lists altogether. */

  html.js-motion .row-word, html.js-motion .row-text, html.js-motion .row-go,
  html.js-motion .jr-title, html.js-motion .jr-arrow { transition: opacity 0.35s linear, transform 0.6s var(--ease); }
  html.js-motion .rows.has-lit .row:not(.is-lit) :is(.row-word, .row-text, .row-go),
  html.js-motion .jr-list.has-lit .jr:not(.is-lit) :is(.jr-title, .jr-arrow) { opacity: 0.62; }
  html.js-motion .row.is-lit::after { transform: scaleX(1); }
  html.js-motion .row.is-lit .row-go::after { transform: translateX(5px); }
  /* The count rides inside the headline, so it would dim with it. It is lent
     the headline's white for as long as the row steps back, which through
     0.62 lands within a shade of its own grey: the label holds its contrast. */
  html.js-motion .row-word sup { transition: color 0.35s linear; }
  html.js-motion .rows.has-lit .row:not(.is-lit) .row-word sup { color: var(--fg); }

  html.js-motion .jr.is-lit::after { transform: scaleX(1); }
  html.js-motion .jr.is-lit .jr-arrow { transform: translate(3px, -3px); }

  /* The ticker has no hover on a phone either: while the band crosses the middle of the screen (data-spot)
     the marks take their own colors, as they would under a pointer. White artwork stays ink. */
  html.js-motion .tk-view.is-lit .tk-item:not([data-bg="dark"]) .tk-logo img { filter: none; }

  /* The door never fills. Crossing the middle of the screen it draws its spectrum hairline once and keeps it
     (.was-lit stays after the light has moved on), its subline comes up to white and the arrow leans out. */
  html.js-motion .door:is(.is-lit, .was-lit)::after { transform: none; }
  html.js-motion .door.is-lit .door-s { color: var(--fg); }
  html.js-motion .door.is-lit .door-a { transform: translate(8px, -8px); }

  /* Living wall. The same wipe a mouse gets, minus the line that only makes
     sense after a tap. A touch closes it faster than it opened. */

  html.js-motion .tile.is-peek .tile-over { clip-path: inset(0); }
  html.js-motion .tile.is-peek img { transform: scale(calc(var(--s, 1) * 0.94)); }
  html.js-motion .tile.is-peek .tile-mark.is-trim { transform: scale(0.94); }
  html.js-motion .tile:is(.is-peek, .is-peek-out):not(.is-open) .tile-over small { display: none; }
  html.js-motion .tile.is-peek-fast .tile-over { transition-duration: 0.24s; }

  /* Press physics. Small things give more, big surfaces give less. */

  html.js-motion .is-press { animation: m-press 0.14s cubic-bezier(0.25, 1, 0.5, 1) forwards; }
  html.js-motion .is-release { animation: m-release 0.56s var(--spring); }
  html.js-motion .is-tap { animation: m-press 0.1s cubic-bezier(0.25, 1, 0.5, 1) forwards, m-release 0.56s var(--spring) 0.1s forwards; }
  html.js-motion .tile, html.js-motion .door, html.js-motion .row, html.js-motion .jr { --press: 0.985; }
  html.js-motion .link-arrow, html.js-motion .foot-top a, html.js-motion .dlg-x { --press: 0.95; }
  html.js-motion .burger { --press: 0.86; }
  html.js-motion .sheet > a { --press: 0.975; transform-origin: 0 60%; }

  /* A pressed button draws its spectrum hairline, the way a hovered one does. It never changes colour. */
  html.js-motion .pill:is(.is-press, .is-tap, .is-release):not([disabled])::before { transform: none; }
  html.js-motion .pill-arrow:is(.is-press, .is-tap, .is-release) span::after { transform: translateX(4px); }
  html.js-motion .pill-ghost:is(.is-press, .is-tap, .is-release):not([disabled]) { box-shadow: inset 0 0 0 1px var(--fg); }
  html.js-motion .link-arrow:is(.is-press, .is-tap, .is-release)::after { transform: translateX(5px); }
  html.js-motion a.link-arrow[target="_blank"]:is(.is-press, .is-tap, .is-release)::after { transform: translate(3px, -3px); }

  /* Closing beat, the door. There is one now, "Get in touch". If it starts below the
     screen it is parked off stage by js/motion.js (.m-wait, --dir is -1, the left;
     a second door would come from the right) and gets one timed slide when it comes
     up (.m-go): headline from its side, subline a shorter way behind it, arrow from the other side. The
     .m-go lists repeat the transitions site.css gives these three, so the lit
     look still eases while a door is arriving. */

  /* WebKit's scroll anchoring follows the sliding headline and walks the page
     backward by up to 400px while the doors arrive. Nothing in here may be
     picked as the anchor, and neither may the footer pinned underneath. */
  html.js-motion #doors, html.js-motion footer.foot { overflow-anchor: none; }

  html.js-motion .door.m-wait .door-t { translate: calc(var(--dir, -1) * 46vw) 0; }
  html.js-motion .door.m-wait .door-s { translate: calc(var(--dir, -1) * 28vw) 0; opacity: 0; }
  html.js-motion .door.m-wait .door-a { translate: calc(var(--dir, -1) * -18vw) 0; opacity: 0; }
  html.js-motion .door.m-go .door-t { transition: translate 0.52s cubic-bezier(0.16, 1, 0.3, 1), transform 0.7s var(--ease); }
  html.js-motion .door.m-go .door-s { transition: translate 0.52s cubic-bezier(0.16, 1, 0.3, 1) 0.04s, opacity 0.36s linear 0.04s, color 0.35s var(--ease-q); }
  html.js-motion .door.m-go .door-a { transition: translate 0.52s cubic-bezier(0.16, 1, 0.3, 1) 0.06s, opacity 0.36s linear 0.06s, transform 0.6s var(--ease); }

  /* Closing beat, the footer. It is pinned under main, so the last section
     lifts off it like a curtain and the browser does the work, not a script.
     Black lifting off black shows no edge, and the footer's type looked cut in
     half under it. So the lifting edge is drawn: a one pixel rule in the logo
     gradient, and under it a short scrim in the page black that the type
     passes through instead of meeting a hard line. js/motion.js also eases the
     footer's type into place as it is uncovered. --foot-tuck is 0, or negative
     when the footer is taller than the screen: js/motion.js measures it so the
     top of a tall footer is never out of reach. main is painted the page black
     so the curtain stays opaque even where a page leaves a gap between
     sections. */

  html.js-motion.m-reveal main { position: relative; z-index: 1; background: var(--black); }
  html.js-motion.m-reveal main::after {
    content: ""; position: absolute; left: 0; right: 0; top: 100%; height: 36px; pointer-events: none;
    background: var(--spectrum) 0 0 / 100% 1px no-repeat, linear-gradient(to bottom, color-mix(in oklch, var(--black) 94%, transparent), color-mix(in oklch, var(--black) 60%, transparent) 45%, transparent);
    transition: opacity 0.4s linear;
  }
  /* Fully lifted, the edge has done its work: the footer rests exactly as it
     does without motion, with no rule over its logo. */
  html.js-motion.m-lifted main::after { opacity: 0; }
  html.js-motion.m-reveal footer.foot { position: sticky; bottom: var(--foot-tuck, 0px); z-index: 0; }
}

/* ------------------------------------------------------------- menu sheet */

/* Not a hover matter: the sheet exists on every narrow screen. A hairline in
   the logo gradient sits between the links and the two buttons, with or
   without motion. */

.sheet .sheet-cta { position: relative; }
.sheet .sheet-cta::before { content: ""; position: absolute; left: 0; right: 0; top: -28px; height: 1px; background: var(--spectrum); transform-origin: left; }

/* Closed look first, then .is-on. js/motion.js adds .m-sheet only once it is
   watching the sheet, so an unwatched sheet is never left clipped shut. The
   closed rules carry the quick exit timings, the open rules the entrance. */

html.js-motion .sheet.m-sheet { clip-path: inset(0 0 100% 0); transition: clip-path 0.44s var(--ease-q) 0.06s; }
html.js-motion .sheet.m-sheet.is-on { clip-path: inset(0 0 0 0); transition: clip-path 0.8s var(--ease); }
html.js-motion .sheet.m-sheet[hidden].is-closing { display: flex !important; pointer-events: none; }

html.js-motion .m-sheet .m-mask { display: block; overflow: hidden; padding: 0.06em 0.08em 0.14em 0; margin: -0.06em 0 -0.14em 0; }
html.js-motion .m-sheet .m-rise { display: block; transform: translateY(115%); transition: transform 0.3s var(--ease-q); }
html.js-motion .m-sheet.is-on .m-rise { transform: none; transition: transform 0.95s var(--ease) calc(0.1s + var(--i, 0) * 0.06s); }

html.js-motion .m-sheet .sheet-cta::before { transform: scaleX(0); transform-origin: right; transition: transform 0.3s var(--ease-q); }
html.js-motion .m-sheet.is-on .sheet-cta::before { transform: scaleX(1); transform-origin: left; transition: transform 1.1s var(--ease) 0.3s; }

html.js-motion .m-sheet .sheet-cta .cta { opacity: 0; transform: translateY(14px); transition: opacity 0.2s linear, transform 0.3s var(--ease-q); }
html.js-motion .m-sheet.is-on .sheet-cta .cta { opacity: 1; transform: none; transition: opacity 0.6s linear calc(0.42s + var(--i, 0) * 0.08s), transform 0.9s var(--ease) calc(0.42s + var(--i, 0) * 0.08s); }

/* While the sheet retracts the nav keeps its dark face, then returns to the
   tone of the section underneath. */
html.js-motion .nav.m-sheet-out { --bg: var(--black); --fg: var(--on-dark); --fg2: var(--on-dark-2); }
html.js-motion .nav.m-sheet-out::before { opacity: 0.94; }

/* Burger to close mark in two moves: the bars meet, then cross. Closing
   uncrosses first, then parts them. Same end state as site.css. */
html.js-motion .burger i { transition: translate 0.24s var(--ease-q) 0.2s, rotate 0.3s var(--ease-q); }
html.js-motion .burger[aria-expanded="true"] i { transform: none; transition: translate 0.2s var(--ease-q), rotate 0.55s var(--ease) 0.16s; }
html.js-motion .burger[aria-expanded="true"] i:first-child { translate: 0 3.5px; rotate: 45deg; }
html.js-motion .burger[aria-expanded="true"] i:last-child { translate: 0 -3.5px; rotate: -45deg; }

/* ------------------------------------------------------------ touch guard */

/* Same promise as the guard above, for the touch layer: if the preference
   flips while the page is open, nothing keeps moving. */
@media (prefers-reduced-motion: reduce) {
  html.js-motion .is-press, html.js-motion .is-release, html.js-motion .is-tap { animation: none !important; }
  html.js-motion .door-t, html.js-motion .door-s, html.js-motion .door-a { translate: none !important; opacity: 1 !important; }
  html.js-motion .foot-strip { clip-path: none !important; }
  html.js-motion footer.foot > .w { translate: none !important; opacity: 1 !important; }
  html.js-motion .follow .m-fit { transform: none !important; }
  html.js-motion.m-reveal footer.foot { position: static !important; }
  html.js-motion .sheet.m-sheet, html.js-motion .m-sheet .m-rise, html.js-motion .m-sheet .sheet-cta .cta, html.js-motion .m-sheet .sheet-cta::before { clip-path: none !important; transform: none !important; opacity: 1 !important; }
  html.js-motion .sheet.m-sheet[hidden].is-closing { display: none !important; }
}

/* Keyboard focus never waits for an entrance: whatever holds the focused element shows at once, even mid rise
   (js/motion.js lands anything that has not started). */
html.js-motion :is([data-fade], [data-stagger] > *, #pf-grid .tile):focus-within { opacity: 1 !important; transform: none !important; }
html.js-motion [data-lines]:focus-within .ln-in { transform: none !important; }
