/* ==========================================================================
   INSAME — foundation
   Tokens, reset, typography, layout primitives, motion primitives.

   Palette note: the photography is dark — walnut slat walls, black marble,
   warm LED cove lighting, brass. The page is therefore the *light* element:
   a warm ivory field that lets each image read as a plate dropped onto paper.
   Graphite and near-black are reserved for the InsameVision environment, so
   that shift down into the technical layer actually means something.
   ========================================================================== */

:root {
  /* ---- surface ---- */
  --ivory:      #f5f2eb;
  --bone:       #ede8dd;
  --limestone:  #ded7c7;
  --paper:      #fbfaf7;

  /* ---- ink ---- */
  --near-black: #131210;
  --graphite:   #24231f;
  --graphite-2: #32302b;
  --ink:        #1a1815;
  /* Muted text splits by ground. --stone is for the ivory/bone surfaces and
     is set at 5.0:1 on ivory / 4.6:1 on bone; --stone-dim is its counterpart
     for the near-black surfaces at 7.4:1. The earlier single #8b8578 measured
     3.28:1 on ivory, which fails AA for body-size text. */
  --stone:      #6e6759;   /* muted body / meta — on LIGHT grounds only */
  --stone-dim:  #a9a294;   /* muted body / meta — on DARK grounds only */

  /* ---- accent: warm metallic, used sparingly ---- */
  --bronze:       #856035;   /* 5.05:1 on ivory */
  --bronze-light: #c19c6d;

  /* ---- rules ---- */
  --rule:      rgba(26, 24, 21, .14);
  --rule-soft: rgba(26, 24, 21, .08);
  --rule-dark: rgba(245, 242, 235, .16);

  /* ---- type ---- */
  --serif: "Instrument Serif", "Cormorant Garamond", Georgia, "Times New Roman", serif;
  --sans:  "Instrument Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
  --mono:  "DM Mono", ui-monospace, "SF Mono", Menlo, monospace;

  /* Fluid scale. Min values are the 360px-viewport size, max the 1600px one. */
  --t-mega:  clamp(3.1rem, 11.2vw, 11rem);   /* one-line page statements    */
  --t-xxl:   clamp(2.6rem, 7.4vw, 6.5rem);   /* section statements          */
  --t-xl:    clamp(2.1rem, 5vw, 4rem);       /* project titles              */
  --t-lg:    clamp(1.6rem, 3vw, 2.4rem);     /* sub-statements              */
  --t-md:    clamp(1.05rem, 1.35vw, 1.25rem);/* lead paragraphs             */
  --t-body:  clamp(.95rem, 1.05vw, 1.0625rem);
  --t-sm:    .8125rem;
  --t-label: .6875rem;                       /* eyebrows, mono metadata     */

  /* ---- layout ---- */
  --shell:  1440px;
  --gutter: clamp(1.25rem, 5vw, 5.5rem);
  --sec-y:  clamp(5.5rem, 13vh, 11rem);      /* vertical section rhythm     */

  /* ---- motion ---- */
  --e-out:  cubic-bezier(.16, 1, .3, 1);     /* expo-out: arrivals          */
  --e-io:   cubic-bezier(.76, 0, .24, 1);    /* quart-in-out: A→B moves     */
  --e-soft: cubic-bezier(.33, 1, .68, 1);
  --d-fast: .45s;
  --d-med:  .8s;
  --d-slow: 1.25s;

  --nav-h: 78px;
}

/* --------------------------------------------------------------------------
   Reset
   -------------------------------------------------------------------------- */

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

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

/* Anchor targets must clear the fixed masthead. */
:target { scroll-margin-top: calc(var(--nav-h) + 2rem); }

body {
  margin: 0;
  background: var(--ivory);
  color: var(--ink);
  font-family: var(--sans);
  font-size: var(--t-body);
  line-height: 1.62;
  font-weight: 400;
  letter-spacing: .002em;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overflow-x: clip;
}

h1, h2, h3, h4, p, figure, blockquote, ul, ol, dl { margin: 0; }
ul, ol { padding: 0; list-style: none; }
img, svg, video { display: block; max-width: 100%; }
button { font: inherit; color: inherit; background: none; border: 0; padding: 0; cursor: pointer; }
a { color: inherit; text-decoration: none; }
input, textarea, select { font: inherit; color: inherit; }

:focus-visible {
  outline: 2px solid var(--bronze);
  outline-offset: 3px;
  border-radius: 2px;
}

::selection { background: var(--near-black); color: var(--ivory); }

/* --------------------------------------------------------------------------
   Typography
   -------------------------------------------------------------------------- */

.display {
  font-family: var(--serif);
  font-weight: 400;
  line-height: .94;
  letter-spacing: -.022em;
  text-wrap: balance;
}
.display em { font-style: italic; }

.t-mega { font-size: var(--t-mega); }
.t-xxl  { font-size: var(--t-xxl); }
.t-xl   { font-size: var(--t-xl); }
.t-lg   { font-size: var(--t-lg); line-height: 1.14; }

/* Lead paragraph — the one place we allow a wider measure. */
.lead {
  font-size: var(--t-md);
  line-height: 1.55;
  color: var(--stone);
  max-width: 46ch;
}
.lead--wide { max-width: 60ch; }

.prose p + p { margin-top: 1.1em; }
.prose { max-width: 62ch; color: var(--stone); }
.prose strong { color: var(--ink); font-weight: 500; }

/* Eyebrow / technical label. Mono is reserved for metadata and the
   InsameVision layer — never for reading copy. */
.label {
  font-family: var(--mono);
  font-size: var(--t-label);
  font-weight: 400;
  letter-spacing: .19em;
  text-transform: uppercase;
  color: var(--stone);
}
.label--bronze { color: var(--bronze); }
.label--dark   { color: var(--stone-dim); }

/* Numbered section marker, e.g. "01". */
.numeral {
  font-family: var(--mono);
  font-size: var(--t-label);
  letter-spacing: .16em;
  color: var(--bronze);
}

/* On the near-black grounds the ivory-tuned bronze drops to 3.3:1 at this
   size. The lighter tone is the same accent, read against the other ground
   (7.4:1). Anything the dark sections tint must be paired here, not patched
   inline on the page that noticed it. */
[data-theme="dark"] .numeral,
.vision .numeral,
.story .numeral,
.close .numeral { color: var(--bronze-light); }

[data-theme="dark"] .label--bronze,
.vision .label--bronze,
.close .label--bronze { color: var(--bronze-light); }

.measure { max-width: 54ch; }
.text-dim { color: var(--stone); }

/* --------------------------------------------------------------------------
   Layout primitives
   -------------------------------------------------------------------------- */

.shell {
  width: 100%;
  max-width: var(--shell);
  margin-inline: auto;
  padding-inline: var(--gutter);
}
.shell--wide { max-width: 1680px; }

.section { padding-block: var(--sec-y); }
.section--tight { padding-block: clamp(3.5rem, 8vh, 6rem); }
.section--flush-b { padding-bottom: 0; }

.rule { height: 1px; background: var(--rule); border: 0; }

/* A 12-column grid is only ever used where the composition is genuinely
   asymmetric; simple stacks use flow + margins. */
.grid12 {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  gap: clamp(1.25rem, 3vw, 2.5rem);
}

/* --------------------------------------------------------------------------
   Figures & image plates

   Every photograph sits in a fixed-ratio mask so mixed source ratios
   (1:1, 3:4, 9:16) never shift the layout. The <img> covers the mask and
   the blur-up placeholder paints underneath until decode finishes.
   -------------------------------------------------------------------------- */

.plate {
  position: relative;
  overflow: hidden;
  background: var(--limestone);
  background-size: cover;
  background-position: center;
}
.plate > img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.r-portrait  { aspect-ratio: 3 / 4; }
.r-tall      { aspect-ratio: 9 / 14; }
.r-square    { aspect-ratio: 1 / 1; }
.r-landscape { aspect-ratio: 4 / 3; }
.r-wide      { aspect-ratio: 16 / 10; }
.r-band      { aspect-ratio: 21 / 9; }

/* Blur-up: the LQIP is painted as a background on .plate, so it is visible
   the instant the element paints and is covered as soon as the real image
   decodes. Fading the <img> in (rather than out of the placeholder) avoids
   a flash when the image is already in cache. */
.plate > img[data-blur] {
  opacity: 0;
  transition: opacity .6s var(--e-soft);
}
.plate > img[data-blur].is-loaded { opacity: 1; }

figcaption {
  margin-top: .9rem;
  display: flex;
  flex-wrap: wrap;
  gap: .5rem 1.25rem;
  align-items: baseline;
}

/* --------------------------------------------------------------------------
   Buttons & links
   -------------------------------------------------------------------------- */

.btn {
  --btn-fg: var(--ivory);
  --btn-bg: var(--near-black);
  display: inline-flex;
  align-items: center;
  gap: .7em;
  padding: 1.05em 1.9em;
  background: var(--btn-bg);
  color: var(--btn-fg);
  font-size: var(--t-sm);
  font-weight: 500;
  letter-spacing: .04em;
  border-radius: 999px;
  position: relative;
  overflow: hidden;
  isolation: isolate;
  transition: color var(--d-fast) var(--e-out);
}
/* Fill wipes up from the base rather than the button changing colour —
   the movement is what reads as considered, not the colour change. */
.btn::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: var(--bronze);
  transform: translateY(101%);
  transition: transform var(--d-fast) var(--e-out);
}
.btn:hover::before, .btn:focus-visible::before { transform: translateY(0); }

.btn__arrow { transition: transform var(--d-fast) var(--e-out); }
.btn:hover .btn__arrow { transform: translateX(4px); }

.btn--ghost {
  --btn-bg: transparent;
  --btn-fg: var(--ink);
  box-shadow: inset 0 0 0 1px var(--rule);
}
.btn--ghost:hover, .btn--ghost:focus-visible { --btn-fg: var(--ivory); }

.btn--on-dark {
  --btn-bg: var(--ivory);
  --btn-fg: var(--near-black);
}
.btn--ghost-dark {
  --btn-bg: transparent;
  --btn-fg: var(--ivory);
  box-shadow: inset 0 0 0 1px var(--rule-dark);
}

/* Underline that draws from the leading edge on hover. */
.link {
  display: inline-flex;
  align-items: center;
  gap: .55em;
  font-size: var(--t-sm);
  font-weight: 500;
  letter-spacing: .03em;
  padding-bottom: .35em;
  background-image: linear-gradient(currentColor, currentColor);
  background-size: 0% 1px;
  background-position: 0 100%;
  background-repeat: no-repeat;
  transition: background-size var(--d-fast) var(--e-out);
}
.link:hover, .link:focus-visible { background-size: 100% 1px; }
.link svg { transition: transform var(--d-fast) var(--e-out); }
.link:hover svg { transform: translateX(4px); }

/* --------------------------------------------------------------------------
   Motion primitives

   Reveal state is driven by a single IntersectionObserver in motion.js which
   sets .is-in. Stagger comes from --i on the child, so a group needs no JS
   beyond the observer.
   -------------------------------------------------------------------------- */

/* Rise: the default arrival for blocks and images. */
[data-rise] {
  opacity: 0;
  transform: translate3d(0, 26px, 0);
  transition:
    opacity var(--d-med) var(--e-out),
    transform var(--d-med) var(--e-out);
  transition-delay: calc(var(--i, 0) * 90ms);
}
[data-rise].is-in { opacity: 1; transform: none; }

/* Masked baseline reveal for display type. Each line is wrapped in a
   .line > .line__i pair by motion.js; the inner element slides up out of the
   overflow-clipped outer one. This is the site's signature type entrance. */
.line { display: block; overflow: hidden; padding-bottom: .08em; margin-bottom: -.08em; }
.line__i {
  display: block;
  transform: translate3d(0, 105%, 0);
  transition: transform 1.05s var(--e-out);
  transition-delay: calc(var(--i, 0) * 105ms);
}
.is-in .line__i, .line.is-in .line__i { transform: none; }

/* Image mask reveal: the photograph wipes down onto the plate's own ground
   while counter-scaling, so it settles rather than slides.

   The clip lives on the CHILD, never on [data-mask] itself. Chrome clips an
   element's IntersectionObserver rect by that element's own clip-path, so an
   element hidden with `clip-path: inset(0 0 100% 0)` reports an intersection
   height of 0 forever — it can never observe its way back into view, and the
   reveal deadlocks with every photo left as an empty rectangle. Verified in
   Chrome: clipped -> isIntersecting false / ratio 0; clip removed -> ratio
   0.72 for the same element in the same position.

   Do not move the clip back up onto [data-mask]. */
[data-mask] > img {
  clip-path: inset(0 0 100% 0);
  transform: scale(1.12);
  transition: clip-path 1.15s var(--e-io), transform 1.6s var(--e-out);
}
[data-mask].is-in > img { clip-path: inset(0 0 0 0); transform: scale(1); }

/* Thin rule that draws across on entry — used under section headings. */
[data-draw] { transform: scaleX(0); transform-origin: 0 50%; transition: transform 1.1s var(--e-io); }
[data-draw].is-in { transform: scaleX(1); }

/* Parallax targets are moved by motion.js via --py; keeping the transform in
   CSS means the JS only ever writes a single custom property. */
[data-parallax] { transform: translate3d(0, var(--py, 0px), 0); will-change: transform; }

.u-clip { overflow: hidden; }
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

.skip-link {
  position: absolute; left: 1rem; top: -100%; z-index: 200;
  background: var(--near-black); color: var(--ivory);
  padding: .8rem 1.2rem; border-radius: 4px; font-size: var(--t-sm);
  transition: top .2s var(--e-out);
}
.skip-link:focus { top: 1rem; }

/* --------------------------------------------------------------------------
   Reduced motion

   Everything still arrives — it simply arrives already in place. Scroll-linked
   scenes fall back to static states handled in motion.js.
   -------------------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
    transition-delay: 0ms !important;
  }
  [data-rise], .line__i, [data-mask], [data-mask] > img, [data-draw] {
    opacity: 1 !important;
    transform: none !important;
    clip-path: none !important;
  }
  [data-parallax] { transform: none !important; }
}
