/* motion.css — arrival and in-view reveals, shared by the preview pages.

   Everything here moves transform and opacity only. Hidden states exist only under
   html.rv-go, which the inline head script sets when JS runs, motion is allowed and the
   visit is not a back/forward; without it every element is simply there. Elements marked
   .rv-h are this page's arrival (the homepage landing, a case study's title), hidden only
   under html.rv-hero.

   Classes: .rv       a block or row rising 16px
            .rv-lines text split into lines, each rising inside its own clipped line
            .rv-img   a picture frame lifting 24px, the picture settling from 1.04
            .rv-fade  opacity only
            .rv-dot   the theme dot
   State:   .rv-in (revealing, then revealed) · .rv-done (finished; transitions dropped so
            hover and active states stay instant) · .rv-split (lines built)
   Return:  html.rv-ret hides the content until motion.js settles in what is in view. */

:root {
  /* ── the easing vocabulary (dossi-motion, 4 Oct). Every transition on the site uses one of
     these by its role; no curve is invented per element.
       fast    content arriving and state changes (a detour, a picture, the dock, the page)
       faster  hovers, underlines, dots, presses, the dim of a list (easeOutExpo)
       stage   one-off stage moves only: hold, whip, soft land (Remora's demo glide)
       exit    short, accelerating exits; exits are always shorter than entrances
     No surface crosses the whole screen, so there is no wipe curve. ── */
  --ease-fast:   cubic-bezier(.17, .67, .1, .99);
  --ease-faster: cubic-bezier(.19, 1, .22, 1);
  --ease-stage:  cubic-bezier(.73, 0, 0, 1);
  --ease-exit:   cubic-bezier(.55, 0, .9, .45);
  --d-micro: 600ms;     /* a hover or an underline: 75% there by ~120ms */
  --d-state: 750ms;     /* something arriving or opening */
  --d-exit:  200ms;     /* something leaving */

  /* the homepage's arrival reveals, on the same vocabulary */
  --rv-ease: var(--ease-fast);
  --rv-line: 800ms;
  --rv-block: 700ms;
  --rv-fade: 500ms;
  --rv-img: 1000ms;
  --rv-lstep: 80ms;
}

/* ── page to page (motion.js, PT). A click on another page of this site plays a JS exit
   first (the chosen row stays, the rest lifts off upward, done by 420ms), then navigates; the
   next page arrives in two waves from html.pt-in, set before first paint by its head script.
   That works in every browser. Where cross-document view transitions exist they only add two
   things on top: the header stays put (its own layer, crossfading in place), and a featured
   row's name morphs into the case study's title (pt-title). The root itself just crossfades
   in 200ms (both images blend plus-lighter, so the white ground never dims): the old page has
   already cleared itself, so there is nothing left to move. Going back, the page you leave
   drops a little instead, so the direction reads. ── */
@view-transition { navigation: none; }   /* 4 Oct: off; the arrival on the next page carries the transition (no snapshot wait, no morph glitch) */
.bar { view-transition-name: kb-bar; }
::view-transition-group(kb-bar) { animation-duration: var(--d-exit); }
::view-transition-old(kb-bar), ::view-transition-new(kb-bar) { animation-duration: var(--d-exit); animation-timing-function: linear; }
::view-transition-old(root) { animation: var(--d-exit) linear both pt-vt-out; }
::view-transition-new(root) { animation: var(--d-exit) linear both pt-vt-in; }
@keyframes pt-vt-out { to { opacity: 0; } }
@keyframes pt-vt-in { from { opacity: 0; } }
html:active-view-transition-type(back)::view-transition-old(root) {
  animation: var(--d-exit) var(--ease-exit) both rv-vt-back;
}
html:active-view-transition-type(back)::view-transition-new(root) { animation: none; }
@keyframes rv-vt-back { to { opacity: 0; transform: translate3d(0, 8px, 0); } }
/* the chosen name becoming the title: on the fast-start curve, 640ms; the two images are the
   same word, so their crossfade is invisible and only the move and the size read */
html.pt-morph .title-block h1 .t { view-transition-name: pt-title; }
::view-transition-group(pt-title) { animation-duration: 640ms; animation-timing-function: var(--ease-fast); }
::view-transition-old(pt-title), ::view-transition-new(pt-title) { animation-duration: 640ms; animation-timing-function: var(--ease-fast); height: 100%; }
@media (prefers-reduced-motion: reduce) {
  @view-transition { navigation: none; }
}

/* the exit: the chosen thing at full ink (a dimmed row lights up at once); what leaves can no
   longer be hovered, so nothing under the pointer changes on the way out */
.pt-keep, .pt-keep a { opacity: 1 !important; transition: opacity 150ms var(--ease-faster) !important; }
.pt-gone { pointer-events: none; }

/* the arrival: until motion.js has set the first screen going, the content waits unseen
   (opacity only, so layout is measured as it will be). The header never hides. With the
   morph, the title is the browser's to move, so it stays. The head script drops pt-in after
   2s whatever happens, and on any script error. */
@media (prefers-reduced-motion: no-preference) {
  html.pt-in :is(.title-block > *, body > :not(.bar, .band, .skip, .title-block, .post), .post > :not(.title-block)) { opacity: 0; }
  html.pt-in.pt-morph .title-block > h1 { opacity: 1; }
}

/* ── blue text links: an underline that passes through. It draws in from the left on hover
   and, on leaving, carries on out to the right instead of rewinding (dossi B1). Painted as a
   1px background stripe, so it follows a link that wraps across lines. Mouse only; the
   colour itself never changes. ── */
@media (hover: hover) {
  :where(.fl-l, .out, .bio a, .years a, .meta dd a, .feat .u, .story p a:not([class])) {
    text-decoration: none;
    background-image: linear-gradient(currentColor, currentColor);
    background-repeat: no-repeat; background-size: 0 1px; background-position: 100% 100%;
  }
  /* (specific enough to beat each page's "links change instantly", which is about colour) */
  :is(.fl-l, .out, .bio a, .years a, .meta dd a, .feat .u), .story p a:not([class]) {
    transition: background-size var(--d-micro) var(--ease-faster);
  }
  :where(.fl-l, .out, .bio a, .years a, .meta dd a, .story p a:not([class])):hover, :where(.feat:hover .u) {
    background-size: 100% 1px; background-position: 0 100%;
  }
  .fl-l.mail.done { background-size: 0 1px; }
}

/* ── the page never scrolls sideways (Kabir, 5 Oct: on iPhone a swipe past the end of the
   Featured row carried the whole page left, leaving an empty strip). clip, not hidden, so
   nothing becomes a scroll container and sticky things keep working ── */
html, body { overflow-x: clip; }
@supports not (overflow: clip) { body { overflow-x: hidden; } }

/* ── the footer's links (CV, LinkedIn, Mail) and the people in a case study's meta (team,
   mentors): ink with a resting underline, not blue (Kabir, 5 Oct). On hover the ink softens.
   Comes after the passing underline above, so it replaces it for these ── */
/* and the homepage's About and Years links (CIID, Fynd, Samarthan; Kabir, 5 Oct) */
.fl-l, .meta dd a, .bio a, .years a, .hello a {
  color: var(--ink); background-image: none;
  text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 3px;
}
@media (hover: hover) { .fl-l:hover, .meta dd a:hover, .bio a:hover, .years a:hover, .hello a:hover { color: var(--quiet); } }
.fl-l.mail.done { text-decoration: none; }

/* ── a return: the content waits unseen for one moment while motion.js puts the scroll back
   and finds what is in view; it then settles in with the Web Animations API (motion.js).
   The chrome and the big name are never hidden. ── */
@media (prefers-reduced-motion: no-preference) {
  html.rv-ret :is(main.content > *, .title-block, [data-rv-kids], .hello, main > section:not(.fold), main > footer) { opacity: 0; }
}

/* an in-page link's target takes focus for screen readers and the keyboard, without a ring
   around a whole section */
.kb-t:focus { outline: none; }

/* ── structure built by motion.js, only while a reveal runs ── */
/* a line is a clip; the padding (given back by the negative margin, so nothing moves) keeps
   descenders and italic overhangs inside it */
.rv-l {
  display: block; overflow: hidden; overflow: clip;
  padding: .06em .12em .2em; margin: -.06em -.12em -.2em;
}
/* a column of lines whose margins add up rather than collapse, so the split paragraph is
   exactly as tall as the one it replaced */
.rv-split { display: flex; flex-direction: column; align-items: stretch; }
.rv-t { display: block; white-space: nowrap; }
/* a clip for a bare picture that has no frame of its own */
.rv-clip { display: block; position: relative; overflow: hidden; }

@media (prefers-reduced-motion: no-preference) {

  /* blocks and rows */
  html.rv-go .rv:not(.rv-h):not(.rv-in),
  html.rv-hero .rv.rv-h:not(.rv-in) {
    opacity: 0; transform: translate3d(0, var(--rv-y, 16px), 0);
  }
  html.rv-go .rv.rv-in:not(.rv-done) {
    transition: transform var(--rv-block) var(--rv-ease) var(--rd, 0ms),
                opacity var(--rv-fade) var(--ease-fast) var(--rd, 0ms);
  }

  /* text lines: the element waits unseen until it is split, then each line rises in its mask */
  html.rv-go .rv-lines:not(.rv-h):not(.rv-in):not(.rv-split),
  html.rv-hero .rv-lines.rv-h:not(.rv-in):not(.rv-split) { opacity: 0; }
  html.rv-go .rv-lines:not(.rv-in) .rv-t { transform: translate3d(0, calc(100% + .3em), 0); }
  html.rv-go .rv-lines.rv-in:not(.rv-done) .rv-t {
    transition: transform var(--rv-line) var(--rv-ease) calc(var(--rd, 0ms) + var(--l, 0) * var(--rv-lstep));
  }

  /* pictures and clips: the frame lifts and fades, the picture settles inside it */
  html.rv-go .rv-img:not(.rv-h):not(.rv-in),
  html.rv-hero .rv-img.rv-h:not(.rv-in) {
    opacity: 0; transform: translate3d(0, 24px, 0);
  }
  html.rv-go .rv-img:not(.rv-h):not(.rv-in) > :is(img, video),
  html.rv-hero .rv-img.rv-h:not(.rv-in) > :is(img, video) { transform: scale(1.04); }
  html.rv-go .rv-img.rv-in:not(.rv-done) {
    transition: transform var(--rv-img) var(--rv-ease) var(--rd, 0ms),
                opacity var(--rv-fade) var(--ease-fast) var(--rd, 0ms);
  }
  html.rv-go .rv-img.rv-in:not(.rv-done) > :is(img, video) {
    transition: transform var(--rv-img) var(--rv-ease) var(--rd, 0ms);
  }

  /* opacity only: the case-study meta row */
  html.rv-go .rv-fade:not(.rv-h):not(.rv-in),
  html.rv-hero .rv-fade.rv-h:not(.rv-in) { opacity: 0; }
  html.rv-go .rv-fade.rv-in:not(.rv-done) { transition: opacity 600ms var(--ease-fast) var(--rd, 0ms); }

  /* the theme dot, last: it grows from a point */
  html.rv-go .rv-dot:not(.rv-h):not(.rv-in),
  html.rv-hero .rv-dot.rv-h:not(.rv-in) { opacity: 0; transform: scale(.25); }
  html.rv-go .rv-dot.rv-in:not(.rv-done) {
    transition: transform 700ms var(--ease-faster) var(--rd, 0ms), opacity 300ms var(--ease-faster) var(--rd, 0ms);
  }

  /* snap: arrive at rest with no motion (input during the landing, came in from above) */
  html.rv-now :is(.rv, .rv-lines, .rv-t, .rv-img, .rv-img > img, .rv-img > video, .rv-fade, .rv-dot),
  .rv-snap, .rv-snap :is(.rv-t, img, video) { transition: none !important; }
}

/* the header words rise a little less than content; lists and the foot are gentler still */
.nav .rv { --rv-y: 12px; }
.tbl > .rv, .about > .rv { --rv-y: 12px; }
