/* The Deck: extra motion.
   Built on the motion tokens in main.css (--d1/--d2/--d3, --ease, --ease-in-out, --rise).
   Every effect needs html.js and data-motion="on", so Still mode, reduced motion
   and a failed script all get the plain, fully visible page.
   Calm mode swaps travel for fades. */

:root { --intro-delay: 0ms; }
html[data-intro="run"] { --intro-delay: 900ms; }
html[data-intro="iris"] { --intro-delay: 1000ms; }
html[data-intro="calm"] { --intro-delay: 1100ms; }

/* ---------- Loading screen: shown once per visit ---------- */
.intro { display: none; }
html.js[data-motion="on"][data-intro] .intro {
  position: fixed; inset: 0; z-index: 200;
  display: grid; place-items: center;
  background: #0b0b0b; color: #f4f4f4;
  cursor: pointer;
}
.intro.is-gone { display: none !important; }
.intro__inner { display: grid; justify-items: center; gap: 22px; }
.intro__logo { width: clamp(96px, 18vw, 148px); height: auto; overflow: visible; fill: currentColor; }
.intro__d, .intro__o { transform-box: fill-box; transform-origin: center; }

/* Iris (the standard intro where the browser supports it):
   1. the D and the O trace themselves as a fine line (0–0.62s),
   2. they fill in solid (0.42–0.72s),
   3. the O becomes a window: it opens out from its own edge and the site shows through (0.76–1.46s),
      while the D drifts back and fades.
   The hole is a radial mask on the black screen whose radius (--iris) starts at the O's exact size,
   so the O seems to grow into the page. Logo maths: the O's centre sits 0.2657 logo-widths right of
   the logo's centre, and its radius is 0.2323 logo-widths. */
@property --iris { syntax: "<length>"; inherits: false; initial-value: 0px; }
html[data-intro="iris"] .intro {
  --logo-w: clamp(96px, 18vw, 148px);
  --iris: 0px;
  -webkit-mask-image: radial-gradient(circle at calc(50% + var(--logo-w) * 0.2657) 50%, transparent var(--iris), #000 calc(var(--iris) + 0.5px));
          mask-image: radial-gradient(circle at calc(50% + var(--logo-w) * 0.2657) 50%, transparent var(--iris), #000 calc(var(--iris) + 0.5px));
  animation: intro-iris 1460ms linear both;
}
html[data-intro="iris"] .intro__d,
html[data-intro="iris"] .intro__o {
  stroke: currentColor; stroke-width: 5; stroke-dasharray: 1; stroke-dashoffset: 1; fill-opacity: 0;
}
/* The O's line starts at twelve o'clock and runs clockwise */
html[data-intro="iris"] .intro__o { rotate: -90deg; }
html[data-intro="iris"] .intro__d {
  animation: intro-draw 560ms cubic-bezier(0.65, 0, 0.35, 1) both, intro-fill 300ms cubic-bezier(0.23, 1, 0.32, 1) 420ms both, intro-d-out 420ms cubic-bezier(0.23, 1, 0.32, 1) 820ms both;
}
html[data-intro="iris"] .intro__o {
  animation: intro-draw 520ms cubic-bezier(0.65, 0, 0.35, 1) 100ms both, intro-fill 300ms cubic-bezier(0.23, 1, 0.32, 1) 440ms both, intro-o-out 140ms linear 760ms both;
}
@keyframes intro-draw { to { stroke-dashoffset: 0; } }
@keyframes intro-fill { to { fill-opacity: 1; } }
@keyframes intro-o-out { to { opacity: 0; } }
@keyframes intro-d-out { to { opacity: 0; translate: -14% 0; } }
/* Closed until 0.76s, then it snaps to the O's size (as the O fades) and opens out */
@keyframes intro-iris {
  0%, 52% { --iris: 0px; }
  52.01% { --iris: calc(var(--logo-w) * 0.2323); animation-timing-function: cubic-bezier(0.77, 0, 0.175, 1); }
  100% { --iris: 150vmax; visibility: hidden; }
}

/* Fallback for browsers without @property: the D slides in, the O rolls in, then the screen lifts away */
html[data-intro="run"] .intro { animation: intro-lift 600ms var(--ease-in-out) 700ms both; }
html[data-intro="run"] .intro__d { animation: intro-d 450ms var(--ease) both; }
html[data-intro="run"] .intro__o { animation: intro-o 520ms var(--ease) 80ms both; }
html[data-intro="run"] .intro__inner { animation: intro-inner 600ms var(--ease-in-out) 700ms both; }
@keyframes intro-lift { to { transform: translateY(-100%); visibility: hidden; } }
@keyframes intro-inner { to { transform: translateY(-40px); opacity: 0; } }
@keyframes intro-d { from { transform: translateX(-60%); opacity: 0; } }
@keyframes intro-o { from { transform: translateX(60%) scale(0.4); opacity: 0; } }

/* Calm (and the first-visit reading gate): fades only */
html[data-intro="calm"] .intro { animation: intro-fade var(--d3) var(--ease) 600ms both; }
html[data-intro="calm"] .intro__inner { animation: fade-in var(--d2) var(--ease) both; }
@keyframes intro-fade { to { opacity: 0; visibility: hidden; } }

/* ---------- Hero E: a staged entrance after the loading screen ---------- */
/* The mask that hides the rising lines gets room on every side, so italic tails (the p in "people’s") are never clipped */
.opt-e__title .ln { display: block; overflow: hidden; padding: 0.02em 0.14em 0.24em 0.14em; margin: -0.02em -0.14em -0.24em -0.14em; }
.opt-e__title .ln > span { display: inline-block; }

html.js[data-motion="on"] .opt-e.play .opt-e__img .photo { animation-delay: var(--intro-delay); }
html.js[data-motion="on"] .opt-e.play .opt-e__kicker { animation: rise-in var(--d3) var(--ease) both; animation-delay: calc(var(--intro-delay) + 60ms); }
html.js[data-motion="on"] .opt-e.play .opt-e__title .ln > span {
  animation: line-up calc(var(--d3) * 1.3) var(--ease) both;
  animation-delay: calc(var(--intro-delay) + 140ms + var(--n, 0) * 90ms);
}
html.js[data-motion="on"] .opt-e.play .opt-e__desc { animation: rise-in var(--d3) var(--ease) both; animation-delay: calc(var(--intro-delay) + 420ms); }
html.js[data-motion="on"] .opt-e.play .opt-e__cta { animation: rise-in var(--d3) var(--ease) both; animation-delay: calc(var(--intro-delay) + 520ms); }
@keyframes line-up { from { transform: translateY(105%); } }

/* Calm: the lines fade in instead of rising */
html.js[data-motion="on"][data-mode="calm"] .opt-e.play .opt-e__title .ln > span { animation-name: fade-in; }

/* The photo drifts slower than the page as the hero scrolls away */
/* Only when the words sit over the photo; once they stack beneath it the photo stays put */
@supports (animation-timeline: view()) { @media (min-width: 1100px) {
  html.js[data-motion="on"]:not([data-mode="calm"]) .hero-e .opt-e__img {
    animation: e-drift linear both;
    animation-timeline: view();
    animation-range: exit 0% exit 100%;
  }
  html.js[data-motion="on"]:not([data-mode="calm"]) .hero-e .opt-e__body {
    animation: e-lift linear both;
    animation-timeline: view();
    animation-range: exit 0% exit 100%;
  }
} }
@keyframes e-drift { to { translate: 0 20%; } }
@keyframes e-lift { to { translate: 0 -12%; opacity: 0.2; } }

/* ---------- Reading progress along the top edge ---------- */
.scroll-progress { display: none; }
@supports (animation-timeline: scroll()) {
  html.js[data-motion="on"]:not([data-mode="calm"]) .scroll-progress {
    display: block; position: fixed; top: 0; left: 0; right: 0; height: 3px; z-index: 150;
    background: #fff; mix-blend-mode: difference; pointer-events: none;
    transform-origin: 0 50%;
    animation: progress linear both;
    animation-timeline: scroll(root);
  }
}
@keyframes progress { from { transform: scaleX(0); } to { transform: scaleX(1); } }

/* ---------- Phones: "Change how I read" tucks down to its icon while scrolling down ----------
   It keeps its full name for screen readers, opens the same panel, and expands again
   on scroll up or near the top. This stops it sitting over buttons in the content.
   max-width is animated on this one small element: a deliberate, documented exception. */
@media (max-width: 560px) {
  .reading-card { transition: transform var(--d2) var(--ease), box-shadow var(--d2) var(--ease), padding var(--d2) var(--ease), gap var(--d2) var(--ease); }
  .reading-card__text { max-width: 12em; overflow: hidden; white-space: nowrap; transition: max-width var(--d2) var(--ease), opacity var(--d1) var(--ease); }
  .reading-card.is-tucked { padding-right: 6px; gap: 0; }
  .reading-card.is-tucked .reading-card__text { max-width: 0; opacity: 0; }
}

/* ---------- Thoughts articles: the cover slides up over the title ----------
   The title block is sticky inside .article__intro, so it holds still under the header
   while the cover scrolls up across it, then both leave together once the cover has passed.
   Scroll-linked and position-only (no animation clock), so it moves exactly with the reader.
   Calm, Still and reduced motion keep the plain stacked layout. */
html.js[data-motion="on"]:not([data-mode="calm"]) .article__intro .article__head {
  position: sticky; top: var(--header-h, 0px); z-index: 0;
}
html.js[data-motion="on"]:not([data-mode="calm"]) .article__intro .article__cover { position: relative; z-index: 1; }
@media (max-width: 760px) {
  /* The header scrolls away on phones, so the title pins to the very top */
  html.js[data-motion="on"]:not([data-mode="calm"]) .article__intro .article__head { top: 0; }
}

/* ---------- Headlines rise line by line from behind a mask ----------
   motion.js wraps each word in a clipping span and gives every word on the same line
   the same delay, so whole lines rise together. Once played, the original markup is
   put back so the heading reflows normally. Calm, Still and reduced motion skip it. */
html.js[data-motion="on"]:not([data-mode="calm"]) :is(main h1, main .h2, main .statement, .footer__cta):not(.is-split, .split-done, .opt-e *, .gate *, .deck *, dialog *, .visually-hidden) {
  /* Hidden only until the split runs; shows anyway after 2.5s if the script never does */
  opacity: 0; animation: split-fallback 1ms 2.5s forwards;
}
@keyframes split-fallback { to { opacity: 1; } }
.is-split { opacity: 1; }
.is-split .w { display: inline-block; overflow: hidden; vertical-align: top; padding: 0 0.06em 0.14em 0; margin: 0 -0.06em -0.14em 0; }
.is-split .w > span { display: inline-block; transform: translateY(112%); transition: transform calc(var(--d3) * 1.35) var(--ease-reveal); transition-delay: calc(var(--line, 0) * 120ms); }
.is-split.is-in .w > span { transform: none; }

/* ---------- Images uncover from the bottom and settle from a slight zoom ---------- */
html.js[data-motion="on"]:not([data-mode="calm"]) .img-reveal {
  clip-path: inset(100% 0 0 0);
  transition: clip-path calc(var(--d3) * 1.4) var(--ease-in-out), transform calc(var(--d3) * 2) var(--ease-reveal);
}
html.js[data-motion="on"]:not([data-mode="calm"]) .img-reveal--zoom { transform: scale(1.08); }
html.js[data-motion="on"]:not([data-mode="calm"]) .img-reveal.is-in { clip-path: inset(0 0 0 0); transform: none; }

/* ---------- How I design: the step in the middle of the screen lights up ---------- */
@supports (animation-timeline: view()) {
  html.js[data-motion="on"]:not([data-mode="calm"]) .index .index__num {
    display: inline-block; transform-origin: 0 60%;
    animation: index-focus linear both; animation-timeline: view(); animation-range: cover 25% cover 75%;
  }
}
@keyframes index-focus {
  0%, 100% { color: var(--muted); transform: none; }
  50% { color: var(--fg); transform: scale(1.35); }
}

/* ---------- Cards: the picture eases closer under the pointer ---------- */
@media (hover: hover) and (pointer: fine) {
  :is(.lead__media, .related__card) .frame--image img { transition: transform calc(var(--d3) * 1.2) var(--ease-reveal); }
  :is(.lead:hover, .related__card:hover) .frame--image img.is-in,
  :is(.lead:hover, .related__card:hover) .frame--image img:not(.img-reveal) { transform: scale(1.04); }
}

/* ---------- Little details ---------- */
/* Hidden starting states are set here (not by script) so nothing flashes on load,
   and each one shows anyway after 2.5s if the script never runs. */
@keyframes detail-fallback { to { opacity: 1; transform: none; clip-path: inset(0 0 0 0); } }

/* Tags pop in one after another */
html.js[data-motion="on"]:not([data-mode="calm"]) :is(.label-strip, .related__meta, .facts--hug) > *:not(.opt-e *) {
  opacity: 0; transform: translateY(8px) scale(0.94);
  animation: detail-fallback 1ms 2.5s forwards;
}
html.js[data-motion="on"]:not([data-mode="calm"]) :is(.label-strip, .related__meta, .facts--hug).is-in > * {
  opacity: 1; transform: none; animation: none;
  transition: opacity var(--d2) var(--ease), transform calc(var(--d3) * 0.9) var(--ease-reveal);
  transition-delay: calc(var(--pop, 0) * var(--stagger));
}

/* Eyebrows: the dot grows in, then the label slides out from behind it */
html.js[data-motion="on"]:not([data-mode="calm"]) main .eyebrow:not(.opt-e *, .gate *)::before {
  transform: scale(0); animation: detail-fallback 1ms 2.5s forwards;
}
html.js[data-motion="on"]:not([data-mode="calm"]) main .eyebrow:not(.opt-e *, .gate *) {
  clip-path: inset(0 100% 0 0); animation: detail-fallback 1ms 2.5s forwards;
}
html.js[data-motion="on"]:not([data-mode="calm"]) main .eyebrow.is-in { clip-path: inset(0 0 0 0); animation: none; transition: clip-path calc(var(--d3) * 1.1) var(--ease-reveal) 120ms; }
html.js[data-motion="on"]:not([data-mode="calm"]) main .eyebrow.is-in::before { transform: none; animation: none; transition: transform var(--d3) var(--ease-reveal); }

/* Button labels roll up to a copy of themselves on hover */
.roll { display: inline-block; position: relative; overflow: hidden; vertical-align: top; line-height: 1.25; }
.roll > span { display: inline-block; }
.roll::after { content: attr(data-text) / ""; position: absolute; left: 0; top: 100%; white-space: nowrap; }
@media (hover: hover) and (pointer: fine) {
  .roll > span, .roll::after { transition: transform calc(var(--d3) * 0.85) var(--ease-reveal); }
  :is(.btn:hover, .btn:focus-visible) .roll > span, :is(.btn:hover, .btn:focus-visible) .roll::after { transform: translateY(-100%); }
}

/* Theme button: the new icon turns in when the theme changes */
html.theme-flip[data-motion="on"] .theme-toggle svg { animation: icon-turn var(--d3) var(--ease-reveal); }
@keyframes icon-turn { from { transform: rotate(-120deg) scale(0.5); opacity: 0; } }

/* Footer: a nudging "back to top" arrow */
.footer__top .arrow { display: inline-block; margin-left: 6px; transition: transform var(--d2) var(--ease); }
@media (hover: hover) and (pointer: fine) { .footer__top:hover .arrow { transform: translateY(-3px); } }
.footer__clock { color: color-mix(in srgb, var(--inverse-fg) 72%, transparent); font-variant-numeric: tabular-nums; }
.footer__clock[hidden] { display: none; }

/* ---------- CV: the role cards are dealt in, and peek when hovered ----------
   Each card rises from a slight tilt and settles, one after another across the row,
   like a hand being dealt. Calm keeps the plain fade. */
html.js[data-motion="on"]:not([data-mode="calm"]) #cv .labels > li[data-reveal] {
  opacity: 0;
  transform: translateY(calc(var(--rise) * 3)) rotate(calc(-2.5deg + var(--i, 0) * 2.5deg)) scale(0.95);
  transform-origin: 50% 100%;
  transition: opacity var(--d3) var(--ease), transform calc(var(--d3) * 1.3) var(--ease-reveal);
  transition-delay: calc(var(--i, 0) * 110ms);
}
html.js[data-motion="on"]:not([data-mode="calm"]) #cv .labels > li[data-reveal].is-in { opacity: 1; transform: none; }

/* Hover: the card lifts and turns a few degrees, hinting that it flips */
@media (hover: hover) and (pointer: fine) {
  html.js[data-motion="on"]:not([data-mode="calm"]) #cv .flip:not(.is-flipped) .flip__inner {
    transition: transform var(--d3) var(--ease-in-out);
  }
  html.js[data-motion="on"]:not([data-mode="calm"]) #cv .flip:not(.is-flipped):hover .flip__inner {
    transform: translateY(-6px) rotateY(-9deg);
  }
  html.js[data-motion="on"]:not([data-mode="calm"]) #cv .flip.is-flipped:hover .flip__inner {
    transform: translateY(-6px) rotateY(171deg);
  }
}

/* ---------- Portfolio: lead stories ----------
   The picture wipes up first, then the words follow one after another.
   Starting states live here so nothing flashes; shown anyway after 2.5s without the script. */
html.js[data-motion="on"]:not([data-mode="calm"]) .lead > div:last-child > * {
  opacity: 0; transform: translateY(calc(var(--rise) * 1.5));
  animation: detail-fallback 1ms 2.5s forwards;
}
html.js[data-motion="on"]:not([data-mode="calm"]) .lead.is-in > div:last-child > * {
  opacity: 1; transform: none; animation: none;
  transition: opacity var(--d3) var(--ease), transform calc(var(--d3) * 1.2) var(--ease-reveal);
}
.lead.is-in > div:last-child > :nth-child(1) { transition-delay: 150ms; }
.lead.is-in > div:last-child > :nth-child(2) { transition-delay: 250ms; }
.lead.is-in > div:last-child > :nth-child(3) { transition-delay: 350ms; }
.lead.is-in > div:last-child > :nth-child(4) { transition-delay: 450ms; }
.lead.is-in > div:last-child > :nth-child(5) { transition-delay: 550ms; }
/* Placeholder frames (lettering, no photo) wipe up like the photos do */
html.js[data-motion="on"]:not([data-mode="calm"]) .lead__media .frame:not(.frame--image) {
  clip-path: inset(100% 0 0 0); animation: detail-fallback 1ms 2.5s forwards;
}
html.js[data-motion="on"]:not([data-mode="calm"]) .lead.is-in .lead__media .frame:not(.frame--image) {
  clip-path: inset(0 0 0 0); animation: none;
  transition: clip-path calc(var(--d3) * 1.4) var(--ease-in-out);
}
html.js[data-motion="on"]:not([data-mode="calm"]) .lead__media .frame:not(.frame--image) .frame__big { transform: translateY(18%); transition: transform calc(var(--d3) * 1.8) var(--ease-reveal) 150ms; }
html.js[data-motion="on"]:not([data-mode="calm"]) .lead.is-in .lead__media .frame:not(.frame--image) .frame__big { transform: none; }

/* Photos drift gently inside their frames as the page scrolls */
@supports (animation-timeline: view()) {
  html.js[data-motion="on"]:not([data-mode="calm"]) :is(.lead__media, .gallery, .article__cover) .frame--image img {
    scale: 1.12;
    animation: frame-drift linear both; animation-timeline: view(); animation-range: cover 0% cover 100%;
  }
}
@keyframes frame-drift { from { translate: 0 -4%; } to { translate: 0 4%; } }

/* Filters: projects that come back fade and rise in, one after another */
html.js[data-motion="on"] .is-entering { animation: filter-in var(--d3) var(--ease-reveal) both; animation-delay: calc(var(--n, 0) * var(--stagger)); }
html.js[data-motion="on"][data-mode="calm"] .is-entering { animation-name: fade-in; }
@keyframes filter-in { from { opacity: 0; transform: translateY(calc(var(--rise) * 1.5)) scale(0.98); } }


/* ---------- Case studies ---------- */
/* Process steps: each number rises into place just after its card */
html.js[data-motion="on"]:not([data-mode="calm"]) .steps li[data-reveal]::before { opacity: 0; transform: translateY(14px); }
html.js[data-motion="on"]:not([data-mode="calm"]) .steps li[data-reveal].is-in::before {
  opacity: 1; transform: none;
  transition: opacity var(--d3) var(--ease) 220ms, transform calc(var(--d3) * 1.2) var(--ease-reveal) 220ms;
}

/* ---------- Scroll-linked motion, site-wide ----------
   Tied to the scroll position itself, so it moves exactly with the reader and
   reverses when they scroll back. Uses the individual translate/scale properties,
   so it adds to (never fights) the one-off reveals. Position, scale and opacity only.
   The home hero and article covers keep their own effects. */
@supports (animation-timeline: view()) {
  /* Page titles drift up and fade as they scroll away */
  html.js[data-motion="on"]:not([data-mode="calm"]) :is(main > .hero:first-child, .cs-hero) {
    animation: title-away linear both; animation-timeline: view(); animation-range: exit 10% exit 100%;
  }
  /* Pictures and frames grow into place as they enter */
  html.js[data-motion="on"]:not([data-mode="calm"]) :is(.lead__media, .gallery > figure, .related__frame, .work-item__img, .split > .frame) {
    animation: grow-in linear both; animation-timeline: view(); animation-range: entry 0% cover 40%;
  }
  /* Section headings drift gently against the content around them */
  html.js[data-motion="on"]:not([data-mode="calm"]) :is(.section-head, .section > .h2, .idea .statement, .replies__title) {
    animation: head-drift linear both, split-fallback 1ms 2.5s forwards;
    animation-timeline: view(), auto; animation-range: cover 0% cover 100%, normal;
  }
  /* The About quote and the footer line settle into place as they arrive */
  html.js[data-motion="on"]:not([data-mode="calm"]) :is(.story__quote, .footer__cta) {
    animation: settle-in linear both, split-fallback 1ms 2.5s forwards;
    animation-timeline: view(), auto; animation-range: entry 0% cover 45%, normal;
  }
  /* Card grids: the middle column floats a little on wide screens */
  @media (min-width: 1000px) {
    html.js[data-motion="on"]:not([data-mode="calm"]) :is(.themes, .steps, .work-grid) > li:nth-child(3n+2) {
      animation: column-float linear both; animation-timeline: view(); animation-range: cover 0% cover 100%;
    }
  }
}
@keyframes title-away { to { translate: 0 -18%; opacity: 0.2; } }
@keyframes grow-in { from { scale: 0.9; } to { scale: 1; } }
@keyframes head-drift { from { translate: 0 28px; } to { translate: 0 -28px; } }
@keyframes settle-in { from { scale: 0.94; translate: 0 32px; } to { scale: 1; translate: 0 0; } }
@keyframes column-float { from { translate: 0 36px; } to { translate: 0 -36px; } }
