/* ============================================================================
   JOE FINCH — SCROLL INTRO ("FIND YOUR FINCH")
   ----------------------------------------------------------------------------
   Seven shots, one native scroll, one sticky stage at 100dvh. intro.js reads the
   scroll position, interpolates it, and writes progress to CSS custom
   properties; this file owns every crop, transform, opacity and light effect.
   Nothing here can restyle the collection, the product view, the viewer or the
   bag drawer: every selector is scoped under `.intro`, except the two
   `html.intro-live` lines that make the fixed header a compact strip while the
   intro is the thing on screen.

   THE GRAMMAR (landscape)
     copy left ~30% · artwork right ~65% of the stage, header safe area above,
     the full available viewport height used for the work.
     A work is a FLAT PLANE on a stage with perspective: 1000px. Across its shot
     the plane swings rotateY +22deg -> front -> -22deg (direction alternates per
     shot, so the swing carries on instead of restarting), rotateX 5deg -> -3deg,
     translateZ -150px -> 0, scale 0.95 -> 1. It comes forward to the front and
     holds there through the readable middle, then leaves to the other side.
     The plane is sized from the measured slot and the image's own pixel ratio
     (intro.js writes --aw/--ah in px, using both a width and a height limit), so
     it occupies ~86% of the available stage height and is never cropped: no
     inner crop window, no rectangular mask. object-fit: contain inside a box
     that already carries the image ratio means the fit is exact.
     The plane does not rotate away from the audience: at the extreme swing it is
     still ~87% of its front size (perspective 1000px, 22 degrees), so the whole
     work stays inside the stage.

   THE PRINT FAN
     The four unframed prints keep their measured crop boxes (see CROPS below),
     because a print sits inside its paper mount. The sheets are sized in px from
     the measured slot, spread with staggered x and staggered rotation, and the
     fan as a whole spans ~86-90% of the right column.

   WHAT THIS IS NOT
     A CSS transform and a light model over stills. There is no generated
     footage, no video and no film element anywhere in this file. Nothing is
     embossed: no bevel, no relief, no canvas or paper grain is faked. The
     glimmer is one broad softbox (a wide diagonal + a soft radial placed by the
     pointer) and one narrow low-opacity specular streak whose offset is derived
     from the plane's own rotation - a reflection-angle approximation, not a
     render. The streak is clipped to the artwork's own alpha with
     mask-image: var(--mask-src) (the image itself), so the paint catches the
     light and the empty ground around it does not. A frame is never implied to
     be included with a print, and no edition presentation is implied.

   CROPS (measured per work, prints only)
     The uniform 1% inset was wrong: it left the ornamental frames inside the
     crop window. Each print carries its measured painting box as normalised
     x1,y1,x2,y2 against its own source file and the window is derived from it:

       --crop-w:   100 / (x2 - x1)
       --crop-h:   100 / (y2 - y1)
       --crop-x:   -100 * x1 / (x2 - x1)
       --crop-y:   -100 * y1 / (y2 - y1)
       --inner-ar: (sourceW * (x2 - x1)) / (sourceH * (y2 - y1))

     Per work, source size then x1,y1,x2,y2:
       Predestination   725x847    .234 .189 .763 .780
       Mono             816x962    .247 .194 .760 .777
       The Fish Price   1600x1907  .220 .163 .772 .785
       Birdview         1600x1894  .247 .174 .759 .756

     The paintings themselves were not touched on disk: no raster file was
     edited, cropped, recompressed, upscaled or generated. The artworks on the
     planes (Offering, Itch, Jesus My Buddy, Spuiten of Slikken, the logo) are
     shown whole, contain-fit, no crop at all.
   ============================================================================ */

/* The header is a compact strip while the intro owns the screen. The intro's
   own safe area reads the same variable, so the two never disagree; once the
   intro has scrolled away the shop gets its full header back. */
html.intro-live { --header-h: 58px; }
@media (max-width: 700px) { html.intro-live { --header-h: 54px; } }

.intro {
  position: relative;
  z-index: 1;

  /* written by intro.js */
  --intro-p: 0;          /* whole journey, 0..1                                  */
  --mx: 0;               /* pointer x, -1..1                                     */
  --my: 0;               /* pointer y, -1..1                                     */

  --intro-safe-top: calc(var(--header-h) + clamp(6px, 2vh, 22px));
  --intro-safe-bottom: clamp(38px, 5.6vh, 70px);
  --intro-col-gap: clamp(20px, 3vw, 64px);

  --intro-paper: #EDE8DF;
  --intro-sleeve-line: rgba(214, 233, 233, .17);
  --intro-fg: rgba(255, 255, 255, .62);
  --intro-fg-dim: rgba(255, 255, 255, .40);
}

/* ── The journey ─────────────────────────────────────────────────────────── */
.intro__track {
  position: relative;
  height: 672svh;                 /* 6 shots, 112svh each                     */
}

.intro__stage {
  position: sticky;
  top: 0;
  box-sizing: border-box;
  height: 100vh;
  height: 100dvh;                 /* the whole available viewport, nothing less */
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: minmax(0, 1fr);
  padding: var(--intro-safe-top) var(--pad-x) var(--intro-safe-bottom);
  background: #0C1C1F;            /* deep petrol, flat, no texture, no panel   */
  overflow: clip;
}

/* ── Scene shell: copy left ~30%, art right ~65% ─────────────────────────── */
.intro-scene {
  grid-area: 1 / 1;
  min-width: 0;
  min-height: 0;
  display: grid;
  grid-template-columns: minmax(0, 30fr) minmax(0, 65fr);
  grid-template-rows: minmax(0, 1fr);
  column-gap: var(--intro-col-gap);

  /* intro.js writes two separate numbers per scene. --art-opacity is the
     dissolve: it ramps over an overlap that spans the shot boundary, so the
     outgoing and incoming works are always at 0.5 together and the total never
     falls away - the journey never fades to black. --copy-opacity is quick and
     separate: the copy is gone well before the boundary and the next copy only
     arrives at 18% of its own shot, so two lines of text are never on screen at
     once. The scene element carries the art number; the copy and the caption
     carry the copy number. */
  opacity: var(--art-opacity, 0);
  will-change: opacity;
}

.intro-scene__copy {
  grid-area: 1 / 1;
  align-self: center;
  min-width: 0;
  max-width: 100%;
  opacity: var(--copy-opacity, 0);
  transform: translateY(calc((1 - var(--copy-opacity, 0)) * 12px));
  will-change: opacity, transform;
}

/* ── The plane stage: perspective lives here, the light lives here ────────── */
.intro-scene__art {
  grid-area: 1 / 2;
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: minmax(0, 1fr);
  place-items: center;
  width: 100%;
  height: 100%;
  min-width: 0;
  min-height: 0;
  overflow: visible;              /* wrappers never clip the work             */
  perspective: 1000px;
  perspective-origin: 50% 50%;
}

.intro-scene__art > * { grid-area: 1 / 1; }

/* ── The work: one flat plane, front and centre ──────────────────────────── */
.intro-plane {
  position: relative;
  width: calc(var(--aw, 400) * 1px);
  height: calc(var(--ah, 500) * 1px);
  max-width: 100%;
  max-height: 100%;
  aspect-ratio: var(--ar);        /* fallback when intro.js is not running    */
  overflow: visible;
  transform-origin: 50% 50%;
  transform:
    translateZ(calc(var(--tz, 0) * 1px))
    rotateY(calc(var(--ry, 0) * 1deg + var(--mx, 0) * 3deg))
    rotateX(calc(var(--rx, 0) * 1deg + var(--my, 0) * -3deg))
    scale(var(--ps, 1));
  will-change: transform;
}

.intro-plane__img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;            /* exact fit: the box already has the ratio */
}

/* Narrow specular streak, tied to the plane's rotation and to the pointer.
   Clipped to the artwork's own alpha: light lands on paint, not on air. */
.intro-plane::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(
    100deg,
    rgba(255, 255, 255, 0)    40%,
    rgba(255, 255, 255, .05)  47%,
    rgba(255, 255, 255, .11)  50%,
    rgba(255, 255, 255, .05)  53%,
    rgba(255, 255, 255, 0)    60%
  );
  background-size: 240% 100%;
  background-position: calc((1 - var(--local, 0)) * 100%) 50%;
  background-repeat: no-repeat;
  opacity: .55;
  mix-blend-mode: screen;
  -webkit-mask-image: var(--mask-src);
          mask-image: var(--mask-src);
  -webkit-mask-size: 100% 100%;
          mask-size: 100% 100%;
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
}

/* The logo is a mark, not a painting: no specular pass over it. */
.intro-plane--logo::after { display: none; }

/* The limited edition: the framed original gives way to the painting, both
   whole, both contain-fit, no crop window anywhere. */
/* ── Type: uppercase titles, mono labels, one script line ────────────────── */
.intro-eyebrow {
  margin: 0 0 14px;
  font: 300 10px/1 var(--font-mono);
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--intro-fg-dim);
}

.intro-title {
  margin: 0 0 18px;
  font-family: var(--font-sans);
  font-weight: 600;
  font-size: clamp(1.7rem, 2.4vw, 2.6rem);
  line-height: 1.08;
  letter-spacing: 0.005em;
  text-transform: uppercase;
  color: #FFFFFF;
}

.intro-lede {
  margin: 0 0 12px;
  font-size: clamp(1rem, 1.1vw, 1.12rem);
  line-height: 1.45;
  color: #FFFFFF;
}

.intro-body {
  margin: 0 0 26px;
  max-width: 34ch;
  font-size: clamp(0.95rem, 0.95vw, 1.02rem);
  line-height: 1.5;
  color: var(--intro-fg);
}

/* the Great Vibes line, prints shot */
.intro-lede--script {
  margin: 0 0 20px;
  max-width: 12ch;
  font-family: var(--font-display);
  font-size: clamp(2.1rem, 4vw, 3.6rem);
  line-height: 1;
  color: #FFFFFF;
}

.intro-series {
  margin: 0;
  font-family: var(--font-sans);
  font-weight: 600;
  font-size: clamp(0.9rem, 1.05vw, 1.05rem);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--intro-fg);
}

.intro-caption {
  justify-self: stretch;
  align-self: end;
  z-index: 4;
  margin: 0;
  padding-top: 10px;
  text-align: center;
  font: 300 10px/1.6 var(--font-mono);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--intro-fg-dim);
  opacity: var(--copy-opacity, 0);
  will-change: opacity;
}

.intro-actions { display: flex; flex-wrap: wrap; gap: 14px; margin: 0; }

/* ── The print fan: four sheets, staggered x and staggered rotation ──────── */
.intro-fan {
  grid-area: 1 / 1;
  position: relative;
  width: 100%;
  height: 100%;
  /* 1 when the fan is fully open, 0.30 when the sheets are still stacked. */
  --fan-open: calc(0.30 + 0.70 * var(--lpb, 0));
}

.intro-print {
  position: absolute;
  left: 50%;
  top: 50%;
  width: calc(var(--sheet-w, 340) * 1px);   /* px, measured by intro.js                  */
  transform-origin: 50% 88%;      /* pivot low on the sheet: wide fan, no clip */
  backface-visibility: hidden;
  will-change: transform;
  --rotn: calc(var(--f-r, 0) * var(--fan-open));
  transform:
    translate3d(
      calc(-50% + var(--f-x, 0) * (var(--fan-spread, 0) * 1px) * var(--fan-open)),
      calc(-50% + var(--f-y, 0) * 1%),
      0
    )
    rotate(calc(var(--rotn) * 1deg))
    rotateY(calc(var(--wave, 0) * 1.7deg + var(--mx, 0) * 2deg))
    skewX(calc(var(--wave, 0) * var(--f-k, .4) * 1deg));
  z-index: var(--f-z, 1);
}

.intro-print__paper {
  position: relative;
  z-index: 1;
  display: block;
  padding: 2.4%;
  background: var(--intro-paper);
  box-shadow:
    0 1px 2px rgba(0, 0, 0, .40),
    0 12px 24px -10px rgba(0, 0, 0, .52),
    0 38px 62px -30px rgba(0, 0, 0, .58);
}

/* The crop window: a print sits inside its mount, so this one box clips on
   purpose. The painting inside never moves; motion is on the sheet wrapper. */
.intro-print__art {
  position: relative;
  display: block;
  overflow: hidden;
  aspect-ratio: var(--inner-ar);
  background: #0C1C1F;
}

.intro-print__art img {
  position: absolute;
  display: block;
  max-width: none;
  max-height: none;
  width: var(--crop-w, 102.0408%);
  height: var(--crop-h, 102.0408%);
  left: var(--crop-x, -1.0204%);
  top: var(--crop-y, -1.0204%);
}

/* The clear sleeve: a broad glimmer that follows the pointer. */
.intro-print__sleeve {
  position: absolute;
  inset: -0.6% -0.7%;
  z-index: 3;
  pointer-events: none;
  border: 1px solid var(--intro-sleeve-line);
  border-radius: 2px;
  overflow: hidden;
  background-image: linear-gradient(
    148deg,
    rgba(255, 255, 255, .055) 0%,
    rgba(255, 255, 255, .012) 26%,
    rgba(255, 255, 255, 0)    46%,
    rgba(255, 255, 255, .022) 78%,
    rgba(255, 255, 255, .05) 100%
  );
  background-size: 180% 180%;
  background-position:
    calc(50% + var(--mx, 0) * 40%) calc(50% + var(--my, 0) * 40%);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .12);
}

/* The narrow streak on the sleeve: position read straight off the fan's scroll
   progress, staggered per sheet, nudged by the pointer. Not an independent
   loop, not a clock. */
.intro-print__sleeve::after {
  content: "";
  position: absolute;
  inset: -14% 0;
  width: 62%;
  pointer-events: none;
  --sh: clamp(0, (var(--lpb, 0) - var(--sheen-index, 0)) * 1.9, 1);
  background: linear-gradient(
    96deg,
    rgba(255, 255, 255, 0)     0%,
    rgba(255, 255, 255, .045) 32%,
    rgba(255, 255, 255, .12)  50%,
    rgba(255, 255, 255, .045) 68%,
    rgba(255, 255, 255, 0)   100%
  );
  transform: translate3d(
    calc(-165% + 430% * var(--sh) + var(--mx, 0) * 20%), 0, 0) rotate(.001deg);
  opacity: calc(var(--sh) * (1 - var(--sh)) * 3.8);
}

/* The low-opacity specular pass on the paint itself, clipped to the artwork's
   alpha. --mask-src is the same file as the img; the crop vars put the mask in
   exactly the same place as the image, so the light lands on paint only. */
.intro-print__art::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(
    98deg,
    rgba(255, 255, 255, 0)    40%,
    rgba(255, 255, 255, .05)  47%,
    rgba(255, 255, 255, .1)   50%,
    rgba(255, 255, 255, .05)  53%,
    rgba(255, 255, 255, 0)    60%
  );
  transform: translateX(calc(var(--rotn) * 1.1% + var(--mx, 0) * 12%));
  mix-blend-mode: screen;
  -webkit-mask-image: var(--mask-src);
          mask-image: var(--mask-src);
  -webkit-mask-size: var(--crop-w, 100%) var(--crop-h, 100%);
          mask-size: var(--crop-w, 100%) var(--crop-h, 100%);
  -webkit-mask-position: var(--crop-x, 0) var(--crop-y, 0);
          mask-position: var(--crop-x, 0) var(--crop-y, 0);
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
}

/* The four sheets. Each carries its own measured crop box and its own place in
   the spread: Predestination fans far left, Mono inside it, The Fish Price
   right, Birdview holds the front. All four read at rest and open. */
.intro-print--pred {
  --f-z: 2; --f-x: -1; --f-r: -15; --f-y: 1.6; --f-k: .40;
  --sheen-index: 0;
  --mask-src: url("assets/intro/predestination.png");
  --crop-w: 189.0359%; --crop-h: 169.2047%;
  --crop-x: -44.2344%; --crop-y: -31.9797%;
  --inner-ar: 0.76617;
}
.intro-print--mono {
  --f-z: 1; --f-x: -0.36; --f-r: -6; --f-y: 0.5; --f-k: .30;
  --sheen-index: 0.22;
  --mask-src: url("assets/intro/mono.png");
  --crop-w: 194.9318%; --crop-h: 171.5266%;
  --crop-x: -48.1483%; --crop-y: -33.2762%;
  --inner-ar: 0.74639;
}
.intro-print--fish {
  --f-z: 3; --f-x: 0.62; --f-r: 8; --f-y: 1.1; --f-k: .34;
  --sheen-index: 0.44;
  --mask-src: url("assets/intro/the-fish-price.png");
  --crop-w: 181.1594%; --crop-h: 160.7717%;
  --crop-x: -39.8551%; --crop-y: -26.2058%;
  --inner-ar: 0.74459;
}
.intro-print--bird {
  --f-z: 4; --f-x: 0.02; --f-r: 1.5; --f-y: 0; --f-k: .2;
  --sheen-index: 0.66;
  --mask-src: url("assets/intro/birdview.png");
  --crop-w: 195.3125%; --crop-h: 171.8213%;
  --crop-x: -48.2422%; --crop-y: -29.8969%;
  --inner-ar: 0.743168;
}

/* ── Journey furniture ──────────────────────────────────────────────────── */
.intro__progress {
  position: absolute;
  left: var(--pad-x);
  right: var(--pad-x);
  bottom: calc(var(--intro-safe-bottom) * 0.42);
  height: 1px;
  background: rgba(255, 255, 255, .08);
  pointer-events: none;
}
.intro__progress span {
  display: block;
  height: 100%;
  background: rgba(255, 255, 255, .38);
  transform: scaleX(var(--intro-p));
  transform-origin: 0 50%;
}

.intro__cue {
  position: absolute;
  left: var(--pad-x);
  bottom: calc(var(--intro-safe-bottom) * 0.42 + 18px);
  margin: 0;
  font: 300 9.5px/1 var(--font-mono);
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--intro-fg-dim);
  opacity: clamp(0, 1 - var(--intro-p) * 9, 1);
  pointer-events: none;
}

.intro__skip {
  position: absolute;
  right: var(--pad-x);
  bottom: calc(var(--intro-safe-bottom) * 0.42 + 14px);
  font: 300 9.5px/1 var(--font-mono);
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--intro-fg-dim);
  border-bottom: 1px solid var(--rule);
  padding-bottom: 3px;
  text-decoration: none;
}
.intro__skip:hover,
.intro__skip:focus-visible { color: #FFFFFF; border-color: #FFFFFF; }

/* Without JS the journey cannot be scrubbed, so the stage holds the first shot
   and nothing else. intro.js adds .intro--js and takes over from there. */
.intro:not(.intro--js) .intro-scene                   { opacity: 0; }
.intro:not(.intro--js) .intro-scene:first-of-type     { opacity: 1; }
.intro:not(.intro--js) .intro-scene__copy,
.intro:not(.intro--js) .intro-caption                 { opacity: 1; }

/* ── Portrait: copy on top ~25%, artwork below ~65% ─────────────────────── */
/* Triggered by the viewport's own shape or by a narrow window, so a 1120x1328
   recording and a phone both land here. */
@media (max-width: 900px), (max-aspect-ratio: 115/100) {
  .intro__stage {
    height: var(--intro-vh, 100dvh);   /* measured from window.innerHeight    */
    padding-top: calc(var(--header-h) + 8px);
    padding-bottom: clamp(28px, 4vh, 52px);
  }
  .intro-scene {
    grid-template-columns: minmax(0, 1fr);
    /* copy 25% · work 65% · 10% breathing room. minmax(0, …) keeps tall
       content from pushing the stage around: the stage itself never grows. */
    grid-template-rows: minmax(0, 25fr) minmax(0, 65fr) minmax(0, 10fr);
    row-gap: clamp(10px, 2.2vh, 24px);
    align-items: stretch;
  }
  .intro-scene__copy {
    grid-area: 1 / 1;
    align-self: start;
    max-width: 44ch;
  }
  .intro-scene__art { grid-area: 2 / 1; }
  .intro-title { font-size: clamp(1.55rem, 5.2vw, 2.4rem); margin-bottom: 12px; }
  .intro-eyebrow { margin-bottom: 10px; }
  .intro-lede { font-size: 1rem; }
  .intro-lede--script { font-size: clamp(1.9rem, 8.4vw, 3rem); margin-bottom: 14px; }
  .intro-body { font-size: 0.95rem; margin-bottom: 16px; }
  .intro-actions { gap: 10px; }
  .intro-caption { text-align: left; padding-top: 6px; }
  .intro__progress { bottom: 18px; }
  .intro__cue { bottom: 34px; }
  .intro__skip { bottom: 32px; }
}

/* A very short window: give the work the room, trim the type. */
@media (min-width: 901px) and (max-height: 700px) {
  .intro-title { font-size: clamp(1.5rem, 2vw, 2.1rem); margin-bottom: 12px; }
  .intro-lede--script { font-size: clamp(1.8rem, 3vw, 2.6rem); }
  .intro-body { margin-bottom: 18px; }
}

/* ── Reduced motion: the shots stack and hold still ─────────────────────── */
@media (prefers-reduced-motion: reduce) {
  .intro__track { height: auto; }
  .intro__stage {
    /* The shots stop overlapping and become a plain, readable stack. */
    display: block;
    position: static;
    height: auto;
    overflow: visible;
    padding-bottom: clamp(40px, 6vh, 80px);
  }
  .intro-scene,
  .intro-scene__copy,
  .intro-caption,
  .intro-plane,
  .intro-plane__img,
  .intro-print,
  .intro-print__art img {
    opacity: 1 !important;
    transform: none !important;
  }
  .intro-scene {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: auto;
    row-gap: clamp(20px, 4vh, 40px);
    padding: clamp(44px, 7vh, 84px) 0;
    border-top: 1px solid var(--rule-soft);
  }
  .intro-scene__copy { grid-area: auto; max-width: 52ch; }
  .intro-scene__art { grid-area: auto; display: block; height: auto; perspective: none; }
  .intro-plane::after,
  .intro-print__sleeve::after { display: none; }
  /* Flat, whole, readable: the image sizes itself and nothing is cut off. */
  .intro-plane {
    display: block;
    width: auto;
    height: auto;
    margin: 0 auto;
  }
  .intro-plane__img {
    width: auto;
    height: auto;
    max-width: 100%;
    max-height: 62svh;
    margin: 0 auto;
  }
  .intro-scene__art--pool .intro-plane { margin: 0 auto 18px; }
  /* Stacked, readable presentations instead of timed shots. */
  .intro-scene__art--fan { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; }
  .intro-fan { display: contents; }
  .intro-print { position: static; width: auto; }
  .intro-print__sleeve { display: none; }
  .intro-caption { text-align: left; }
  .intro__progress,
  .intro__cue { display: none; }
}

/* Print: the intro is screen furniture, the collection is the document. */
@media print {
  .intro { display: none !important; }
}
