/* =========================================================
   alper.at
   Tokens measured from the Figma exports (1440px frame).
   ========================================================= */

@font-face {
  font-family: 'Rethink Sans';
  font-style: normal;
  font-weight: 400 500;
  font-display: swap;
  src: url('/fonts/rethink-sans-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Rethink Sans';
  font-style: normal;
  font-weight: 400 500;
  font-display: swap;
  src: url('/fonts/rethink-sans-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
    U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020,
    U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* Registered so the glow's opacity can be transitioned. Without @property
   support the glow simply appears and disappears instantly. */
@property --glow-a {
  syntax: '<number>';
  initial-value: 0;
  inherits: false;
}

:root {
  /* --- colour ------------------------------------------------ */
  --bg:      #ffffff;
  /* The same white as channels, so gradients can fade it out by alpha. A
     gradient to the `transparent` keyword fades through transparent BLACK and
     leaves a grey cast halfway, which is exactly what we are avoiding here. */
  --bg-rgb:  255, 255, 255;
  --text:    #000000;
  --muted:   #8c8c8c;   /* CLIENT / SCOPE labels, footer legal links */
  --accent:  #9ab22a;   /* current nav item, and every link on hover. Sampled from the exports. */

  /* --- type (fluid 375px -> 1440px, then capped) -------------- */
  --font: 'Rethink Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;

  --fs-logo:        clamp(33px, 28.77px + 1.127vw, 45px);
  --fs-logo-name:   clamp(13px, 11.94px + 0.282vw, 16px);
  --gap-logo:       clamp(14px, 11.18px + 0.751vw, 22px);
  --fs-nav:         clamp(18px, 16.24px + 0.470vw, 23px);
  --fs-statement:   clamp(24px, 20.83px + 0.845vw, 33px);  /* homepage intro + process */
  --fs-contact:     clamp(25px, 21.48px + 0.939vw, 35px);  /* contact lead + socials */
  --fs-body:        clamp(20px, 17.54px + 0.657vw, 27px);  /* bio, page intros */
  --fs-list:        clamp(19px, 16.89px + 0.563vw, 25px);  /* client / press / exhibition lists */
  --fs-detail:      clamp(15px, 14.65px + 0.094vw, 16px);  /* project description - confirm in stage 4 */
  --fs-label:       clamp(13px, 12.30px + 0.188vw, 15px);  /* eyebrows, small labels */
  --fs-fields:      clamp(32px, 24.25px + 2.066vw, 54px);  /* Creative Fields row */
  --fs-detail-title:clamp(26px, 22.83px + 0.845vw, 35px);  /* project title */
  --fs-legal-h:     clamp(18px, 16.59px + 0.376vw, 23px);  /* legal page section headings */
  --fs-footer:      clamp(13px, 12.65px + 0.282vw, 16px);
  --fs-footer-mark: clamp(20px, 17.89px + 0.563vw, 26px);

  /* Tracking. Only the header mark and the nav are tracked out in the design. */
  --ls-logo: 0.13em;
  --ls-nav:  0.045em;

  --lh-statement: 1.15;   /* 38px on 33px */
  --lh-contact:   1.26;   /* 44px on 35px */
  --lh-body:      1.15;   /* 31px on 27px */
  --lh-list:      1.2;    /* 30px on 25px */
  --lh-label:     1.07;   /* 16px on 15px - the eyebrows are set tight */
  --lh-detail:    1.4;

  /* --- layout ------------------------------------------------ */
  --pad-x:    clamp(20px, 15.77px + 1.127vw, 40px);
  --page-max: 1920px;
  --gap-4col: 6.19%;   /* 85px of 1373px content width */
  --gap-3col: 8.52%;   /* 117px of 1373px content width */

  --grid-gap-x:  8.35%;   /* 115px of 1378px content width */
  --gap-work-x:  6.09%;   /* 84px  - the four column work / explore grid */
  --gap-work-y:  clamp(40px, 24.15px + 4.225vw, 85px);
  --gap-series:  0.038;   /* 52px  - between images inside an art series,
                             as a fraction of the page column, see .series-row */
  --series-sm:   0.142;
  --series-md:   0.175;
  --series-lg:   0.214;
  --grid-gap-y:  clamp(48px, 23.70px + 6.479vw, 117px);
  --list-gap-x:  4.06%;   /* 56px between the three editorial list columns */

  /* --- vertical rhythm --------------------------------------- */
  --space-header-top:    clamp(20px, 15.77px + 1.127vw, 32px);
  --space-page-top:      clamp(90px, 54.80px + 9.390vw, 190px);
  --space-hero-top:      clamp(72px, 42.42px + 7.887vw, 156px);
  --space-hero-bottom:   clamp(60px, 34.30px + 6.854vw, 133px);
  --space-section:       clamp(80px, 44.08px + 9.577vw, 182px);
  --space-section-sm:    clamp(68px, 33.14px + 9.296vw, 167px);
  --space-label:         clamp(16px, 12.83px + 0.845vw, 25px);
  --space-list-label:    clamp(22px, 16.72px + 1.408vw, 37px);
  --space-grid-top:      clamp(12px, 10.24px + 0.470vw, 17px);
  --space-contact:       clamp(72px, 47.35px + 6.573vw, 142px);

  /* inner pages */
  --space-intro-series:  clamp(72px, 48.41px + 6.291vw, 139px);
  --space-art-series:    clamp(48px, 32.51px + 4.131vw, 92px);
  --space-art-label:     clamp(16px, 13.18px + 0.751vw, 24px);
  --space-lab-top:       clamp(72px, 41.72px + 8.075vw, 158px);
  --space-portrait-label:clamp(24px, 19.42px + 1.221vw, 37px);
  --space-about-lists:   clamp(96px, 49.52px + 12.394vw, 228px);
  --space-detail-title:  clamp(48px, 35.32px + 3.380vw, 84px);
  --space-detail-gallery:clamp(64px, 42.52px + 5.728vw, 125px);
  --space-gallery-row:   clamp(56px, 35.93px + 5.352vw, 113px);
  --space-explore:       clamp(96px, 57.97px + 10.141vw, 204px);
  --space-explore-grid:  clamp(32px, 21.44px + 2.817vw, 62px);
  --space-social:        clamp(64px, 35.80px + 7.510vw, 144px);
  --space-footer:        clamp(96px, 43.90px + 13.90vw, 244px);
  --space-footer-bottom: clamp(24px, 18.37px + 1.502vw, 40px);
}

/* =========================================================
   Reset
   ========================================================= */
*, *::before, *::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  /* clip, not hidden: it does not create a scroll container, so nothing that
     relies on the document scroller breaks. Absorbs the full-bleed collage. */
  overflow-x: clip;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font);
  font-size: var(--fs-list);
  font-weight: 400;
  line-height: var(--lh-list);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

h1, h2, h3, h4, p, ul, ol, figure { margin: 0; }

/* There is no bold anywhere in the design. Headings carry semantic weight only -
   they inherit the body weight and are sized by their own class. */
h1, h2, h3, h4 { font-size: inherit; font-weight: 400; }
ul, ol { padding: 0; list-style: none; }
img, video { max-width: 100%; height: auto; display: block; }

a { color: inherit; text-decoration: none; }

button {
  font: inherit;
  color: inherit;
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
}

/* Visible keyboard focus, but not on mouse clicks. */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 100;
  padding: 12px 16px;
  background: var(--text);
  color: var(--bg);
  font-size: var(--fs-label);
}
.skip-link:focus { left: 0; }

/* =========================================================
   Page shell
   ========================================================= */
body > header,
.page-body > main,
.page-body > section,
.page-body > footer {
  max-width: var(--page-max);
  margin-inline: auto;
  padding-inline: var(--pad-x);
}

/* Less the header's new bottom padding, so the gap below the header is
   unchanged from the design. */
main { padding-top: calc(var(--space-page-top) - var(--space-header-top)); }

/* Constrained statement column - homepage intro + process statement.
   Measured: starts 10.8% into the content width, runs to ~58% of it. */
.statement {
  margin-left: 10.8%;
  max-width: 58%;
  font-size: var(--fs-statement);
  line-height: var(--lh-statement);
}

/* Inner pages open flush left rather than indented. */
.page-intro {
  max-width: 50%;
  font-size: var(--fs-body);
  line-height: var(--lh-body);
}

/* Small label above a section. */
.eyebrow {
  font-size: var(--fs-label);
  line-height: var(--lh-label);
}

/* =========================================================
   Header
   ========================================================= */
.site-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;

  /* Stays in view while the page scrolls under it. The backdrop that makes it
     readable is on ::before below, so the fade can run past the bottom edge.

     The bottom padding mirrors the top so text never slides up against the
     wordmark. main gives back exactly that much, so the page at rest sits where
     it was designed to sit. */
  position: sticky;
  top: 0;
  z-index: 60;
  padding-top: var(--space-header-top);
  padding-bottom: var(--space-header-top);
}

/* The backdrop: solid behind the wordmark and the nav, then fading out below
   them so the page dissolves under the header instead of meeting a hard edge.

   It is a pseudo-element rather than a background on the header itself so the
   fade can run past the bottom of the box. Adding that height to the header
   would have pushed the whole page down. */
.site-header::before {
  content: '';
  position: absolute;
  top: 0;
  z-index: -1;
  pointer-events: none;

  /* Full bleed, not the width of the header. The header is capped at --page-max
     and centred, so on a window wider than that it leaves a bare strip down each
     side, and the full-bleed things underneath, the lab collage and the art
     strips, show straight through it. --sbw is the scrollbar width, set by
     main.js; plain 100vw includes it and would overhang. */
  --viewport: calc(100vw - var(--sbw, 0px));
  left: calc(50% - var(--viewport) / 2);
  right: auto;
  width: var(--viewport);

  /* Five stops, each with its own position and its own opacity, so the shape of
     the fade is fully adjustable rather than just its two ends.
     Alpha runs 0 to 1; positions are percentages of the backdrop's height. */
  --hf-extend: 20px;

  --hf-p1:   0%;  --hf-a1: 0.99;
  --hf-p2:  25%;  --hf-a2: 0.87;
  --hf-p3:  42%;  --hf-a3: 0.68;
  --hf-p4:  89%;  --hf-a4: 0.08;
  --hf-p5: 100%;  --hf-a5: 0;

  height: calc(100% + var(--hf-extend));

  /* Stretched over the box, exactly as tuned. */
  background: linear-gradient(
    to bottom,
    rgba(var(--bg-rgb), var(--hf-a1)) var(--hf-p1),
    rgba(var(--bg-rgb), var(--hf-a2)) var(--hf-p2),
    rgba(var(--bg-rgb), var(--hf-a3)) var(--hf-p3),
    rgba(var(--bg-rgb), var(--hf-a4)) var(--hf-p4),
    rgba(var(--bg-rgb), var(--hf-a5)) var(--hf-p5)
  );
}

.logo {
  display: flex;
  align-items: center;
  gap: var(--gap-logo);
}

.logo-mark {
  font-size: var(--fs-logo);
  line-height: 1;
  letter-spacing: var(--ls-logo);
  white-space: nowrap;
}

.logo-name {
  display: flex;
  flex-direction: column;
  font-size: var(--fs-logo-name);
  line-height: 1.19;
}

.site-nav ul {
  display: flex;
  align-items: baseline;
  gap: clamp(16px, 12.48px + 0.939vw, 26px);
  font-size: var(--fs-nav);
  letter-spacing: var(--ls-nav);
  line-height: 1;
}

/* Nav: the accent marks the current page, and every item on hover. */
.site-nav a { transition: color 0.25s ease; }

.site-nav a.is-active,
.site-nav a:hover { color: var(--accent); }

/* Cursor-following glow across the mark and the name. The .has-glow class is
   added by JS only where background-clip:text is supported and the pointer is
   a real one, so the logo stays plain black everywhere else. */
.logo.has-glow {
  --glow-x: -200px;
  --glow-y: -200px;

  /* The glow colour, faded toward the text colour by --glow-a on hover. */
  --glow-tint: color-mix(in srgb, var(--accent) calc(var(--glow-a) * 100%), var(--text));

  /* ONE fully opaque layer, deliberately. Stacking a translucent green over a
     black layer made background-clip apply the glyph mask to each layer in
     turn, so at every antialiased edge the black underneath contributed a
     fraction the green could not cover - a dark hairline around the letters.
     A single green-to-black gradient has nothing to show through. */
  background-image:
    radial-gradient(circle 110px at var(--glow-x) var(--glow-y),
      var(--glow-tint) 0%,
      var(--glow-tint) 34%,
      var(--text) 100%);

  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  transition: --glow-a 0.3s ease;
}

.logo.has-glow:hover { --glow-a: 1; }

/* Menu button. Hidden until the nav collapses; the bars below are styled here
   rather than in the media query so there is one place to look for them. */
.nav-toggle { display: none; }

.nav-toggle {
  position: relative;
  color: var(--text);
  transition: color 0.25s ease;
}
.nav-toggle:hover { color: var(--accent); }

/* A thumb-sized target without a thumb-sized button: the box grows, the bars
   stay where they are and the header layout does not move. */
.nav-toggle::after {
  content: '';
  position: absolute;
  inset: -14px;
}

.nav-toggle-bars {
  display: block;
  position: relative;
  width: 26px;
  height: 16px;
}

.nav-toggle-bars span {
  position: absolute;
  left: 0;
  width: 100%;
  height: 2px;
  background: currentColor;
  transition: transform 0.34s cubic-bezier(0.22, 0.61, 0.36, 1),
              opacity 0.18s ease;
}

.nav-toggle-bars span:nth-child(1) { top: 0; }
.nav-toggle-bars span:nth-child(2) { top: 7px; }
.nav-toggle-bars span:nth-child(3) { top: 14px; }

/* Open: the outer two swing into a cross and the middle one goes. Driven by
   aria-expanded, so the state a screen reader is told is the same state the
   icon is drawn from, and the two cannot drift apart. */
.nav-toggle[aria-expanded="true"] .nav-toggle-bars span:nth-child(1) {
  transform: translateY(7px) rotate(45deg);
}
.nav-toggle[aria-expanded="true"] .nav-toggle-bars span:nth-child(2) {
  opacity: 0;
}
.nav-toggle[aria-expanded="true"] .nav-toggle-bars span:nth-child(3) {
  transform: translateY(-7px) rotate(-45deg);
}

/* =========================================================
   Contact block
   ========================================================= */
.contact {
  display: grid;
  grid-template-columns: 2fr 1fr;
  gap: var(--gap-4col);
  align-items: center;
  margin-top: var(--space-contact);
}

.contact-lead {
  font-size: var(--fs-contact);
  font-weight: 400;
  line-height: var(--lh-contact);
}

.contact-email {
  text-decoration: underline;
  text-underline-offset: 0.12em;
}

.contact-social {
  margin-top: var(--space-social);
  font-size: var(--fs-contact);
  line-height: var(--lh-contact);
}

/* Every text link outside the nav picks up the accent on hover, over the
   same 0.25s fade. The logo is the exception - it has its own glow. */
.contact-email,
.contact-social a,
.about-contact a,
.about-email a,
.footer-legal a,
.list-prose a { transition: color 0.25s ease; }

.contact-email:hover,
.contact-social a:hover,
.about-contact a:hover,
.about-email a:hover,
.footer-legal a:hover,
.list-prose a:hover { color: var(--accent); }

/* Reserved container for the interactive 3D element.
   Deliberately unstyled - no background, no border. */
#contact-3d {
  width: 100%;
  aspect-ratio: 412 / 400;
}

/* =========================================================
   Footer
   ========================================================= */
.site-footer {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 24px;
  margin-top: var(--space-footer);
  padding-bottom: var(--space-footer-bottom);
}

.footer-mark {
  display: block;
  font-size: var(--fs-footer-mark);
  line-height: 1;
  margin-bottom: 10px;
}

.footer-line {
  display: block;
  font-size: var(--fs-footer);
  line-height: 1;
}

.footer-legal {
  display: flex;
  gap: 20px;
  font-size: var(--fs-footer);
  line-height: 1;
  color: var(--muted);
}



/* =========================================================
   Homepage - hero
   ========================================================= */
main.home { padding-top: calc(var(--space-hero-top) - var(--space-header-top)); }

/* The Figma hero slot is 2.21:1 and the reel is 1.80:1, so the video is
   centre-cropped to fit: roughly 71px is trimmed off the top and bottom.
   Change object-position if the framing needs to sit higher or lower. */
.hero-video {
  width: 100%;
  aspect-ratio: 1380 / 625;
  object-fit: cover;
  object-position: center;
  background: #f2f2f2;
}

.statement--intro   { margin-top: var(--space-hero-bottom); }
.statement--process { margin-top: var(--space-section); }

/* =========================================================
   Project grid
   ========================================================= */
.work { margin-top: var(--space-section-sm); }

.project-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--grid-gap-x);
  align-items: start;
  margin-top: var(--space-grid-top);
}

.grid-col {
  display: flex;
  flex-direction: column;
  gap: var(--grid-gap-y);
}

/* The staggered vertical offsets, measured off the export. */
.grid-col-1 { margin-top: clamp(36px, 17.69px + 4.883vw, 88px); }
.grid-col-3 { margin-top: clamp(17px, 8.55px + 2.254vw, 41px); }

.project-card {
  display: block;
  position: relative;
}

/* Cards are cropped to the ratio their slot has in the design rather than
   showing whatever shape the source image happens to be. object-fit does the
   cropping, so no image is ever re-processed for this. */
.project-card img {
  width: 100%;
  aspect-ratio: var(--card-ratio, auto);
  object-fit: cover;
  transition: transform 0.35s cubic-bezier(0.22, 0.61, 0.36, 1);
}

/* Homepage: each of the three masonry columns has its own pair of ratios,
   measured off the export. They repeat if a column gains more cards. */
.grid-col-1 .project-card:nth-child(odd)  { --card-ratio: 383 / 493; }
.grid-col-1 .project-card:nth-child(even) { --card-ratio: 383 / 397; }
.grid-col-2 .project-card:nth-child(odd)  { --card-ratio: 383 / 389; }
.grid-col-2 .project-card:nth-child(even) { --card-ratio: 383 / 527; }
.grid-col-3 .project-card:nth-child(odd)  { --card-ratio: 383 / 625; }
.grid-col-3 .project-card:nth-child(even) { --card-ratio: 383 / 370; }

/* Work and "Explore more": the export alternates tall and near-square in a
   checkerboard across the four columns, so the pattern runs on an 8 cell cycle. */
.work-grid .project-card { --card-ratio: 282 / 292; }
.work-grid .project-card:nth-child(8n + 1),
.work-grid .project-card:nth-child(8n + 3),
.work-grid .project-card:nth-child(8n + 6),
.work-grid .project-card:nth-child(8n + 8) { --card-ratio: 282 / 378; }

.project-card:hover img,
.project-card:focus-visible img { transform: translateY(-6px); }

.project-caption {
  position: absolute;   /* sits below the image without affecting layout */
  top: 100%;
  left: 0;
  right: 0;
  margin-top: 12px;
  display: flex;
  flex-direction: column;
  font-size: var(--fs-label);
  line-height: 1.35;
  opacity: 0;
  transition: opacity 0.25s ease;
}

.project-card:hover .project-caption,
.project-card:focus-visible .project-caption { opacity: 1; }

.project-scope { color: var(--muted); }

/* =========================================================
   Creative Fields
   ========================================================= */
.fields { margin-top: var(--space-section); }

.fields-row {
  display: flex;
  justify-content: space-between;
  gap: 24px;
  margin-top: var(--space-label);
  font-size: var(--fs-fields);
  line-height: 1;
}

/* =========================================================
   Editorial lists
   ========================================================= */
.lists {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--list-gap-x);
  margin-top: var(--space-section);
  align-items: start;
}

.list-col ul { margin-top: var(--space-list-label); }

/* =========================================================
   Scroll reveal - only once JS has confirmed it can run
   ========================================================= */
.js .reveal {
  opacity: 0;
  transform: translateY(24px);
  transition: opacity 0.4s ease, transform 0.4s ease;
  transition-delay: calc(min(var(--i, 0), 5) * 45ms);
}

.js .reveal.is-visible {
  opacity: 1;
  transform: none;
}

/* The lab collage drives transform from physics, 60 times a second. It must
   never be transitioned or every frame gets re-animated over 600ms and the
   images crawl behind the cursor. These fade in without the rise. */
.js .lab-item.reveal {
  transform: none;
  transition: opacity 0.4s ease;
  /* Set separately: the shorthand above resets delay to 0, which would drop the
     stagger and make the whole collage appear at once. */
  transition-delay: calc(min(var(--i, 0), 5) * 45ms);
}

/* =========================================================
   Utilities
   ========================================================= */
.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* =========================================================
   Work grid - four columns, rows top aligned, natural heights
   ========================================================= */
.work-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: var(--gap-work-y) var(--gap-work-x);
  align-items: start;
  margin-top: var(--space-section);
}

/* =========================================================
   Art - one row of images per series
   ========================================================= */
.series { margin-top: var(--space-art-series); }
.series:first-of-type { margin-top: var(--space-intro-series); }

/* One unbroken line per series. The strip spans the full browser width so it
   runs off both edges rather than wrapping, and is dragged sideways like a
   slide. --sbw is the scrollbar width, set by main.js; plain 100vw includes it.

   Widths are still measured against --content, the centred page column, so the
   images keep exactly the size they had when the row wrapped. Only the space
   they can travel through grew. */
.series-row {
  --viewport: calc(100vw - var(--sbw, 0px));
  --ref:      min(var(--viewport), var(--page-max));
  --content:  calc(var(--ref) - var(--pad-x) * 2);
  --edge:     calc((var(--viewport) - var(--ref)) / 2 + var(--pad-x));

  width: var(--viewport);
  margin-left: calc(50% - var(--viewport) / 2);
  margin-top: var(--space-art-label);

  display: flex;
  flex-wrap: nowrap;
  align-items: flex-start;
  gap: calc(var(--content) * var(--gap-series));

  /* The first image lines up with the page text; the last is free to sit under
     the right edge of the window. */
  padding-inline: var(--edge);

  overflow-x: auto;
  overscroll-behavior-x: contain;
  scrollbar-width: none;                      /* the strip is dragged, not scrolled */
  -ms-overflow-style: none;
  cursor: grab;
}
.series-row::-webkit-scrollbar { display: none; }
.series-row.is-dragging { cursor: grabbing; }

/* Scaled up ~1.46x from the export - the artwork was undersold at Figma size. */
.series-item     { margin: 0; flex: 0 0 auto; }
.series-item--sm { width: calc(var(--content) * var(--series-sm)); }
.series-item--md { width: calc(var(--content) * var(--series-md)); }
.series-item--lg { width: calc(var(--content) * var(--series-lg)); }
.series-item img { width: 100%; -webkit-user-drag: none; user-select: none; }

/* The row is what the observer watches, not the individual images. Staggering
   each image on its own would mean an image parked off to the right sat out its
   whole delay before appearing the moment it was dragged into view; this way the
   series has already run its cascade by the time you reach the far end.

   A horizontal scroller cannot let overflow show on the cross axis either, so
   the usual 24px rise would be clipped along the bottom. These fade only. */
/* The row itself is not revealed, its images are, so opacity and transform stay
   put. filter is the exception: the hover effect below animates it. */
.js .series-row.reveal { opacity: 1; transform: none; transition: filter 400ms ease-out; }

.js .series-row.reveal .series-item {
  opacity: 0;
  transition: opacity 0.4s ease;
  transition-delay: calc(min(var(--i, 0), 5) * 45ms);
}

.js .series-row.reveal.is-visible .series-item { opacity: 1; }

/* =========================================================
   Art page - the series you are looking at, the rest recede
   ========================================================= */

/* Driven by scroll position rather than the cursor, on every device. Hover
   meant holding the mouse still on a row to keep it in focus, which fights with
   actually reading the page; the row in the middle of the screen is a better
   guess at what someone is looking at, and it works the same on a phone.

   art.js marks the row; the reduced-motion guard is here so that preference
   turns the whole thing off rather than leaving it to the script. */
@media (prefers-reduced-motion: no-preference) {

  /* On the row, not on each image, for two reasons.

     The row is the unit: the series in the middle of the screen stays sharp and
     the others recede together.

     And it is the only way the blur does not get cut off. The row is a
     horizontal scroller, so it clips its own contents; blurring the images
     inside means every halo is sliced at that boundary. Filtering the row
     happens after the clip, so the softness spreads outward instead. */
  .series-row {
    transition: filter 400ms ease-out;
  }

  /* Only while a row actually holds focus, rather than parking a layer per row
     for the whole visit. */
  main:has(.series-row.is-focused) .series-row {
    will-change: filter;
  }

  /* Fade and blur are both written into filter so they move as one, and so
     neither collides with the opacity the scroll reveal already owns. */
  main:has(.series-row.is-focused) .series-row:not(.is-focused) {
    filter: blur(6px) opacity(0.55);
  }
}

/* =========================================================
   Homepage entrance - once per session
   ========================================================= */

/* The header settles down from above, the page below rises to meet it. Small
   distances on purpose: enough to read as arriving, not as a slide.

   `both` matters. Without it the elements paint at their natural position for
   one frame before the animation takes hold, which is the flash this is meant
   to avoid. */
@media (prefers-reduced-motion: no-preference) {
  .intro .site-header {
    animation: intro-settle 520ms cubic-bezier(0.22, 0.61, 0.36, 1) both;
  }

  /* Slightly longer and a touch behind, so the two are not one block moving. */
  .intro main.home {
    animation: intro-rise 640ms cubic-bezier(0.22, 0.61, 0.36, 1) 80ms both;
  }
}

@keyframes intro-settle {
  from { opacity: 0; transform: translateY(-10px); }
  to   { opacity: 1; transform: none; }
}

@keyframes intro-rise {
  from { opacity: 0; transform: translateY(18px); }
  to   { opacity: 1; transform: none; }
}

/* =========================================================
   Lab - free placement collage, driven entirely by lab.json
   ========================================================= */
.lab-head {
  display: flex;
  align-items: flex-start;
  gap: 18px;
}

/* Only surfaces once the collage has been disturbed, then fades out again the
   moment it is pressed. Nothing announces it beforehand. */
.lab-reset {
  flex: none;
  margin-top: 0.15em;
  padding: 4px;
  color: var(--muted);
  line-height: 0;
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.3s ease, color 0.25s ease, transform 0.35s ease;
}
.lab-reset.is-visible { opacity: 1; visibility: visible; }
.lab-reset:hover  { color: var(--accent); }
.lab-reset:active { transform: rotate(-140deg); }
.lab-reset[hidden] { display: none; }

.lab-collage {
  position: relative;
  margin-top: var(--space-lab-top);

  /* The container spans the full browser width so images can be dragged all the
     way to the edges, but the composition itself is laid out against --ref, a
     centred band capped at the page width. So the images keep exactly the size
     and position they had before; only the space around them grows.
     --sbw is the scrollbar width, set by main.js; plain 100vw includes it and
     would force a horizontal scroll on Windows. */
  --viewport: calc(100vw - var(--sbw, 0px));
  --ref: min(var(--viewport), var(--page-max));

  width: var(--viewport);
  margin-left: calc(50% - var(--viewport) / 2);
  margin-right: 0;
  height: calc(var(--ref) * 1780 / 1440);   /* replaces aspect-ratio: tied to --ref, not the viewport */
}

.lab-item {
  position: absolute;
  /* --left and --w are plain numbers (percentages of --ref), so the band stays
     centred in the full width container. */
  left: calc((var(--viewport) - var(--ref)) / 2 + var(--left) * var(--ref) / 100);
  top: var(--top);                         /* percentage of the --ref based height */
  width: calc(var(--w) * var(--ref) / 100);
  margin: 0;
}

.lab-item img,
.lab-item video { width: 100%; height: auto; display: block; }
.lab-item video { background: #f2f2f2; }

/* Drag affordances. Scoped above 768px because below that the collage is a
   plain column, and touch-action:none there would break page scrolling. */
@media (min-width: 769px) {
  /* The header sits 15px closer to the top edge on desktop. main takes the same
     15px back, so only the header moves: everything below stays exactly where
     the design puts it. */
  .site-header { padding-top: calc(var(--space-header-top) - 15px); }

  main      { padding-top: calc(var(--space-page-top) - var(--space-header-top) + 15px); }
  main.home { padding-top: calc(var(--space-hero-top) - var(--space-header-top) + 15px); }

  .lab-collage.is-interactive .lab-item {
    cursor: grab;
    touch-action: none;
  }
  /* Promote to their own layers only while something is moving, rather than
     parking eight large textures on the GPU for the whole visit. */
  .lab-collage.is-dragging .lab-item { will-change: transform; }
  .lab-collage.is-interactive .lab-item.is-held {
    cursor: grabbing;
    z-index: 10;
  }
  .lab-collage.is-interactive .lab-item img,
  .lab-collage.is-interactive .lab-item video {
    user-select: none;
    -webkit-user-drag: none;
    pointer-events: none;   /* the figure receives the pointer, not the media */
  }
}

/* =========================================================
   About
   ========================================================= */
.about {
  display: grid;
  grid-template-columns: 24.3% 1fr;
  gap: 3.3%;
  align-items: start;
}

.about-portrait { width: 100%; }

.about-contact-label { margin-top: var(--space-portrait-label); }
.about-contact       { margin-top: var(--space-art-label); font-size: var(--fs-list); line-height: var(--lh-list); }

.about-bio        { max-width: 85.2%; }
.about-bio p + p  { margin-top: 1.2em; }
.about-email      { margin-top: 1.2em; }
.about-email a    { text-decoration: underline; text-underline-offset: 0.12em; }

.lists--four {
  grid-template-columns: repeat(4, 1fr);
  margin-top: var(--space-about-lists);
}

.list-prose { margin-top: var(--space-list-label); }
.list-prose p + p { margin-top: 1.2em; }

/* =========================================================
   Legal pages - the About layout with the image column removed
   ========================================================= */
.legal-body {
  max-width: 62%;               /* the same measure as the About bio */
  margin-top: var(--space-section-sm);
  font-size: var(--fs-detail);
  line-height: 1.65;
}

.legal-lead {
  font-size: var(--fs-list);
  line-height: var(--lh-list);
  margin-bottom: 1.6em;
}

.legal-body section + section { margin-top: 2.4em; }

.legal-body h2 {
  font-size: var(--fs-legal-h);
  line-height: 1.25;
  margin-bottom: 0.45em;
}

.legal-body p + p { margin-top: 1em; }

.legal-body a {
  text-decoration: underline;
  text-underline-offset: 0.12em;
  transition: color 0.25s ease;
}
.legal-body a:hover { color: var(--accent); }

/* =========================================================
   Project detail
   ========================================================= */
.detail-hero {
  display: grid;
  grid-template-columns: 2.5fr 1fr;
  gap: 4.07%;
  align-items: start;
}

.detail-hero-main,
.detail-hero-side { width: 100%; }

/* The pair at the top of a project page fades in as one, once both pictures
   have arrived. Revealing them as they land made the smaller one look like an
   afterthought; this way the top of the page settles in a single movement.
   Without JS the .js class is absent and they are simply visible. */
.js .detail-hero img {
  opacity: 0;
  transition: opacity 0.6s ease;
}
.js .detail-hero.is-loaded img { opacity: 1; }

.detail-meta {
  display: grid;
  grid-template-columns: 34.9% 37% 28.1%;
  align-items: start;
  margin-top: var(--space-detail-title);
}

.detail-title {
  font-size: var(--fs-detail-title);
  font-weight: 400;
  line-height: 1.1;
}

.detail-description {
  /* Wraps where the hero image ends rather than where its column does: the hero
     is 68.5% of the content width, and this column starts at 34.9%. */
  max-width: 90.9%;
  font-size: var(--fs-detail);
  line-height: var(--lh-detail);

  /* Descriptions are written as plain text in the admin. A line break typed
     there is meant, so it survives instead of collapsing into the sentence
     before it. Runs of spaces still collapse as normal. */
  white-space: pre-line;
}

.detail-facts {
  margin: 0;
  font-size: var(--fs-detail);
  line-height: var(--lh-detail);
}

/* Set like the section labels elsewhere on the site ("Selected Client
   Projects"), just in the muted grey. */
.detail-facts dt {
  color: var(--muted);
  font-size: var(--fs-label);
  line-height: var(--lh-label);
  margin-bottom: 0.25em;
}

.detail-facts dd { margin: 0; }
.detail-facts dd + dt { margin-top: 1.6em; }

.detail-gallery { margin-top: var(--space-detail-gallery); }

.gallery-row {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  margin-top: var(--space-gallery-row);
}
.gallery-row:first-child { margin-top: 0; }

.gallery-item {
  position: relative;          /* only so the reduced-motion play button can sit on it */
  margin: 0;
  width: var(--w);
  margin-top: var(--offset);   /* vertical offset, as a % of the content width */
}
.gallery-item img,
.gallery-item .gallery-video { width: 100%; height: auto; display: block; }

/* Videos sit in the gallery exactly like images: same width and offset controls,
   muted and looping, with the poster carrying the layout until they load. */
.gallery-video { background: #f2f2f2; }

/* Play control, added by JS only where the visitor has asked for reduced motion.
   Nothing autoplays there, so the poster needs a way to be started deliberately. */
.video-play {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  width: 64px;
  height: 64px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: rgba(var(--bg-rgb), 0.86);
  box-shadow: 0 2px 14px rgba(0, 0, 0, 0.18);
  cursor: pointer;
  color: var(--text);
}
/* The triangle is drawn rather than set, so no glyph or icon file is involved. */
.video-play::before {
  content: '';
  display: block;
  width: 0;
  height: 0;
  margin: 0 auto;
  border-style: solid;
  border-width: 10px 0 10px 17px;
  border-color: transparent transparent transparent currentColor;
  transform: translateX(2px);
}
.video-play:hover,
.video-play:focus-visible { background: rgba(var(--bg-rgb), 1); }

.explore { margin-top: var(--space-explore); }
.explore .work-grid { margin-top: var(--space-explore-grid); }

/* =========================================================
   Page transitions
   ========================================================= */

/* Cross-document view transitions where the browser has them: the page
   cross-fades on navigation, with no JavaScript and no risk of a click being
   left half-faded. Browsers without support just navigate normally. */
@view-transition { navigation: auto; }

/* A straight cross-fade between two mostly white pages reads as nothing
   happening, so the outgoing page fades out first and the new one comes in just
   behind it, through the white background. mix-blend-mode has to be normal:
   the UA default of plus-lighter would brighten the overlap. */
/* The whole sequence lands at 200ms: the new page starts at 65 and finishes at
   200. It was 340, which read as lag rather than polish. Every value is the old
   one scaled by the same 0.588, so the overlap between the two halves is kept in
   proportion and the transition still looks like itself, just quicker. */
::view-transition-old(root) {
  animation: page-out 75ms ease-in both;
  mix-blend-mode: normal;
}

::view-transition-new(root) {
  animation: page-in 135ms ease-out 65ms both;
  mix-blend-mode: normal;
}

@keyframes page-out { to   { opacity: 0; } }
@keyframes page-in  { from { opacity: 0; } }

/* Naming the header lifts it out of the page snapshot, so the logo and nav sit
   completely still while everything below them fades. */
.site-header { view-transition-name: site-header; }

::view-transition-group(site-header),
::view-transition-old(site-header),
::view-transition-new(site-header) {
  animation: none;
  mix-blend-mode: normal;
}

/* Without view transitions there is no fade out, but the fade in still works
   and cannot leave the page stuck invisible. */
@supports not (view-transition-name: none) {
  main,
  .page-body > .contact,
  .site-footer {
    animation: page-fade-in 200ms ease both;
  }
}

@keyframes page-fade-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* Reduced motion is handled by removing the animations, NOT by a conditional
   @view-transition rule: Chrome applies the last @view-transition in document
   order whether or not its media query matches, so `navigation: none` in here
   silently disables transitions for everyone. */
@media (prefers-reduced-motion: reduce) {
  ::view-transition-old(root),
  ::view-transition-new(root) {
    animation: none;
  }
}

/* =========================================================
   Responsive
   ========================================================= */
@media (max-width: 1024px) {
  .statement  { margin-left: 0; max-width: 72%; }
  .page-intro { max-width: 62%; }
  .legal-body { max-width: 78%; }

  /* Masonry columns flatten into a plain two-up grid in reading order. */
  .project-grid {
    grid-template-columns: repeat(2, 1fr);
    gap: var(--grid-gap-y) var(--grid-gap-x);
  }
  .grid-col   { display: contents; }
  .project-card { order: var(--o, 0); }
  .grid-col-1, .grid-col-3 { margin-top: 0; }

  .fields-row {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 24px 5%;
  }

  .lists { grid-template-columns: repeat(2, 1fr); row-gap: var(--space-section-sm); }
  .lists--four { grid-template-columns: repeat(2, 1fr); }

  .work-grid { grid-template-columns: repeat(2, 1fr); }

  /* Fewer, larger images per series row. */
  :root { --series-sm: 0.213; --series-md: 0.263; --series-lg: 0.321; }

  .detail-meta {
    grid-template-columns: 1fr 1fr;
    gap: var(--space-art-label) var(--gap-work-x);
  }
  .detail-title { grid-column: 1 / -1; margin-bottom: 0.4em; }
}

@media (max-width: 768px) {
  .statement,
  .page-intro { max-width: none; }

  /* Optional line breaks that only hold at desktop. */
  br.line-break { display: none; }

  .site-header {
    flex-wrap: wrap;
    align-items: flex-start;

    /* No row gap. The nav is a flex item at all times now, not just when open,
       so a row gap would sit under the header permanently as empty space. The
       spacing above the open menu comes from the nav's own padding instead,
       which has the advantage of animating with it. */
    row-gap: 0;
  }

  /* With the menu open, the page behind it goes soft. Mobile only: on desktop
     the nav is always on show, so there is nothing to open and nothing to blur.

     A plain filter on the content, not backdrop-filter on the menu. Backdrop
     filters make the browser sample and blur everything underneath on every
     frame, which is heavy on the phones most likely to be reading this. */
  /* One element, so the whole page recedes as a piece. The origin is the top
     edge, so it steps away from the menu rather than drifting down out of it. */
  .page-body {
    transform-origin: center top;
    transition: filter 0.34s cubic-bezier(0.22, 0.61, 0.36, 1),
                transform 0.34s cubic-bezier(0.22, 0.61, 0.36, 1);
  }

  body.nav-open .page-body {
    filter: blur(7px);
    transform: scale(0.94);
  }

  /* With the menu open the header is three times as tall, and a gradient
     stretched over the whole of it spreads the fade across the menu itself: by
     the time it reaches the content it has already run out, leaving white above
     and blurred content below with a hard line between.

     So while the menu is open the fade becomes a fixed length pinned to the
     bottom edge, with flat colour above it, and the backdrop reaches further
     down to give it room to dissolve into the page. The closed header keeps the
     stretched gradient above, untouched. */
  body.nav-open .site-header::before {
    --hf-extend: 90px;
    --hf-length: 150px;

    background:
      linear-gradient(
        to bottom,
        rgba(var(--bg-rgb), var(--hf-a1)) var(--hf-p1),
        rgba(var(--bg-rgb), var(--hf-a2)) var(--hf-p2),
        rgba(var(--bg-rgb), var(--hf-a3)) var(--hf-p3),
        rgba(var(--bg-rgb), var(--hf-a4)) var(--hf-p4),
        rgba(var(--bg-rgb), var(--hf-a5)) var(--hf-p5)
      ) 0 100% / 100% var(--hf-length) no-repeat,
      linear-gradient(
        rgba(var(--bg-rgb), var(--hf-a1)),
        rgba(var(--bg-rgb), var(--hf-a1))
      ) 0 0 / 100% calc(100% - var(--hf-length)) no-repeat;
  }

  /* Set for one frame while navigating away, so the menu is already shut by the
     time the outgoing page is photographed for the view transition. Nothing
     eases, because there is no time for it to finish. */
  body.nav-instant .site-nav,
  body.nav-instant .page-body,
  body.nav-instant .nav-toggle-bars span {
    transition: none;
  }

  .nav-toggle {
    display: block;

    /* The header aligns its items to the top so the menu wraps flush below, but
       that leaves a 16px icon hanging at the top of a 34px logo. Centring this
       one item lines the bars up with the wordmark, and keeps them lined up if
       the logo's type ever changes size. */
    align-self: center;
  }

  /* Slides open to whatever height the list happens to be.
     display:none cannot be transitioned, which is why this used to snap. A
     grid row going from 0fr to 1fr animates to the content's natural height
     without anyone having to guess a pixel value up front, so adding a nav item
     later needs no change here.

     visibility is what keeps the closed menu out of the tab order and away from
     screen readers, since the links are still in the DOM at zero height. It is
     held back until the collapse has finished, so the menu does not vanish
     halfway through closing. */
  .site-nav {
    flex-basis: 100%;
    display: grid;
    grid-template-rows: 0fr;
    opacity: 0;
    visibility: hidden;
    padding-top: 0;
    transition: grid-template-rows 0.34s cubic-bezier(0.22, 0.61, 0.36, 1),
                padding-top 0.34s cubic-bezier(0.22, 0.61, 0.36, 1),
                opacity 0.22s ease,
                visibility 0s linear 0.34s;
  }

  .site-nav.is-open {
    grid-template-rows: 1fr;
    opacity: 1;
    visibility: visible;
    padding-top: 52px;
    transition: grid-template-rows 0.34s cubic-bezier(0.22, 0.61, 0.36, 1),
                padding-top 0.34s cubic-bezier(0.22, 0.61, 0.36, 1),
                opacity 0.28s ease 0.06s,
                visibility 0s linear 0s;
  }

  /* The grid row can only collapse if its child is allowed to be clipped and to
     shrink below its content height. */
  .site-nav ul {
    flex-direction: column;
    align-items: flex-start;
    gap: 14px;
    line-height: 1.2;
    overflow: hidden;
    min-height: 0;
  }

  /* Fill the screen instead of sitting as a thin 2.21:1 band on a phone.
     object-fit:cover means the landscape source is cropped at the sides. */
  .hero-video {
    aspect-ratio: auto;
    height: calc(100vh - 170px);
    height: calc(100svh - 170px);
    min-height: 360px;
  }

  .contact {
    grid-template-columns: 1fr;
    gap: 48px;
  }

  .project-grid { grid-template-columns: 1fr; }

  .lists { grid-template-columns: 1fr; }

  .fields-row { grid-template-columns: 1fr; gap: 12px; }

  /* No hover on touch, so the captions simply stay visible. */
  .project-caption {
    position: static;
    opacity: 1;
    margin-top: 12px;
  }

  .work-grid { grid-template-columns: 1fr; }

  .lists--four { grid-template-columns: 1fr; }

  :root { --series-sm: 0.47; --series-md: 0.47; --series-lg: 0.72; }

  /* The collage stops being a collage and becomes a plain column. */
  .lab-collage {
    position: static;
    width: auto;
    height: auto;
    margin-inline: 0;
    display: flex;
    flex-direction: column;
    gap: 48px;
  }
  /* Relative rather than static, so the reduced-motion play button has
     something to position against. The collage offsets have to be cleared
     along with it: static ignored left and top, relative applies them, and
     each item would slide out of the column by its collage coordinates. */
  .lab-item { position: relative; left: auto; top: auto; width: 100%; }

  .about {
    grid-template-columns: 1fr;
    gap: 48px;
  }
  .about-portrait { max-width: 60%; }
  .about-bio { max-width: none; }
  .legal-body { max-width: none; }

  .detail-hero { grid-template-columns: 1fr; gap: 32px; }
  .detail-hero-side { width: 60%; }

  .detail-meta { grid-template-columns: 1fr; gap: 24px; }
  .detail-title { margin-bottom: 0; }

  .gallery-row { flex-direction: column; gap: 48px; }
  .gallery-item { width: 100% !important; margin-top: 0 !important; }

  .site-footer {
    flex-direction: column;
    align-items: flex-start;
    gap: 32px;
  }
}

@media (prefers-reduced-motion: reduce) {
  /* Never leave a revealed element stuck at opacity 0. */
  .js .reveal { opacity: 1; transform: none; transition: none; }
  .js .detail-hero img { opacity: 1; transition: none; }

  /* The menu still opens and closes, it just does not travel to get there. */
  .site-nav, .site-nav.is-open { transition: none; }
  .nav-toggle-bars span { transition: none; }

  /* The page behind still softens, it just arrives there at once. Easing a
     blur is the part that can unsettle people who asked for less motion. */
  .page-body { transition: none; }
  .js .series-row.reveal .series-item { opacity: 1; transition: none; }

  .project-card:hover img,
  .project-card:focus-visible img { transform: none; }

  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}
