/* ---------------------------------------------------------
   Design tokens (pulled from Figma Dev Mode)
--------------------------------------------------------- */
:root {
  --ink: #171717;      /* Primary-Grey — nav, labels, active text */
  --ink-muted: #3D3029; /* secondary text (inactive index rows) */
  --paper: #FDFDFD;    /* page background */

  /* "Söhne Breit" (extended width — nav/headings) and plain "Söhne"
     (normal width — body copy) are TWO DIFFERENT type families, both
     from Klim Type Foundry. Don't conflate them. */
  --font: "Söhne Breit", Helvetica, Arial, sans-serif;
  --font-narrow: "Söhne", Helvetica, Arial, sans-serif;

  --header-h: 72px;

  /* snappy, consistent motion */
  --t-fast: 0.15s;
  --t-med: 0.25s;
}

@font-face {
  font-family: "Söhne Breit";
  src: url("../fonts/soehne-breit-buch.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Söhne Breit";
  src: url("../fonts/soehne-breit-buch-kursiv.woff2") format("woff2");
  font-weight: 400;
  font-style: italic;
  font-display: swap;
}

@font-face {
  font-family: "Söhne Breit";
  src: url("../fonts/soehne-breit-kraftig.woff2") format("woff2");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Söhne";
  src: url("../fonts/soehne-leicht.woff2") format("woff2");
  font-weight: 300;
  font-style: normal;
  font-display: swap;
}

* { box-sizing: border-box; }

/* any element using [hidden] to toggle visibility must actually hide,
   even when it also carries a `display` rule of its own (e.g. .stage's
   display:flex would otherwise beat the browser's default [hidden] rule) */
[hidden] { display: none !important; }

html, body {
  margin: 0;
  padding: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font);
  -webkit-font-smoothing: antialiased;
}

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

img { display: block; max-width: 100%; }

/* ---------------------------------------------------------
   Shared type styles
--------------------------------------------------------- */

/* the small uppercase label used everywhere: nav, "NEXT", "GRID", counters */
.label {
  font-size: 12px;
  font-weight: 400;
  line-height: 28px;
  letter-spacing: 1.2px;
  text-transform: uppercase;
  color: var(--ink);
}

/* the 24px category style used on the Index page */
.cat-label {
  font-size: 24px;
  font-weight: 400;
  line-height: 28px;
  letter-spacing: 1.68px;
  text-transform: uppercase;
  color: #fff;
  mix-blend-mode: difference; /* white text inverts against whatever's beneath it — reads dark on the paper background, light over dark parts of the preview photo */
  transition: transform var(--t-fast) ease, font-style var(--t-fast) ease;
}

/* bold uppercase section heading, used on Info ("Prints / Enquires" etc) — "Kräftig" cut */
.section-heading {
  font-size: 12px;
  font-weight: 700;
  line-height: 26px;
  letter-spacing: 1.2px;
  text-transform: uppercase;
  color: var(--ink);
  display: block;
  margin: 0;
}

/* body copy — plain "Test Söhne" (not Breit), light weight, no tracking
   (bio, contact lines, clients, copyright) */
.body-copy {
  font-family: var(--font-narrow);
  font-size: 14px;
  font-weight: 300;
  line-height: 26px;
  color: var(--ink);
  margin: 0;
}

/* ---------------------------------------------------------
   Header — 4 equal columns: brand | index | info | contact
   Plain grey always (no blend mode, per feedback — reassess later).
   Active page gets a trailing asterisk. No hover state on nav links.
--------------------------------------------------------- */
.site-header {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: var(--header-h);
  display: flex;
  align-items: center;
  padding: 0 32px;
  z-index: 100;
}

.site-header .col {
  flex: 1;
  text-align: left; /* flush-left, matching the content columns below them */
}

/* Contact has no content column of its own — Info only has 3 real
   sections (bio, licensing, clients), not 4 — so it's pulled out of the
   3-way flex split entirely (position:absolute removes it from flow,
   letting the other three share the FULL width equally) and pinned to
   the corner independently, the same way the portrait is. */
.site-header .col:last-child {
  position: absolute;
  right: 32px;
  top: 50%;
  transform: translateY(-50%);
}

.site-header .label { color: var(--ink); font-weight: 400; font-size: 14px; } /* Buch, 14px on desktop/laptop */

/* the asterisk's space is always reserved (content present, just hidden) so
   a link's rendered width never changes when its active state toggles —
   that's what was shifting the other nav items next to it */
.site-header a[data-nav]::after {
  content: "*";
  visibility: hidden;
}

.site-header a.current::after {
  visibility: visible;
}

.site-header a:hover,
.site-header a:focus-visible,
.view-toggle:hover,
.view-toggle:focus-visible {
  /* :focus-visible (not :focus) — a mouse click on the toggle otherwise
     leaves it focused with no page reload to clear it, so the italic state
     would stick around after the click instead of just being a hover cue */
  font-style: italic;
}

/* ---------------------------------------------------------
   Fade-in on image load (grid thumbnails, info portrait — NOT the stage)
--------------------------------------------------------- */
/* same speed/curve as the Home/Project stage's image crossfade, so every
   piece of content on the site enters the same way */
.fade-img,
.fade-in {
  opacity: 0;
  transition: opacity 0.25s cubic-bezier(0.54, 0.26, 0.29, 0.74);
}
.fade-img.is-loaded,
.fade-in.is-loaded { opacity: 1; }

/* ---------------------------------------------------------
   Home / Project single-image stage
   Cycles through 5 compositions on click — never crops:
   1. full bleed right   2. landscape, centered (80%)
   3. full bleed left    4. landscape, centered (80%)
   5. portrait, centered with space around it — then repeats.
   No transition here for now (instant swap) while transition timing
   is still being worked out.
--------------------------------------------------------- */
.stage {
  position: relative;
  height: 100vh; /* fallback for browsers without dvh support */
  height: 100dvh; /* actual visible viewport on mobile — 100vh is sized to the browser chrome collapsed, which is taller than what's on screen and was causing the page to scroll */
  width: 100%;
  overflow: hidden;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
}

.stage img {
  display: block;
  width: auto;
  height: auto;
  opacity: 0;
  transition: opacity 0.25s cubic-bezier(0.54, 0.26, 0.29, 0.74); /* same curve as danieldorsa.com, sped up — one shared duration across every image transition on the site */
  /* JS only swaps src/dimensions/layout AFTER this fade-out finishes (see
     initImageStage), so the old photo is always fully invisible before it
     changes shape — never resizing while visible, which is what caused
     the flicker before. */
}
.stage img.is-loaded { opacity: 1; }

/* bleed-right / bleed-left can be wider than the viewport before
   clamping, which sends flexbox's default shrink algorithm into a bad
   interaction with aspect-ratio (it stops preserving ratio). Positioning
   these absolutely instead sidesteps flex sizing entirely.
   width/height are both auto (not a hard height:100vh) so that at narrow
   widths, where 100vh's implied width would exceed 100vw, the browser
   shrinks height along with width to keep the aspect ratio — a fixed
   height combined with a clamped width is what was squishing/distorting
   the image instead of just scaling it down. */
.stage[data-layout="bleed-right"] img,
.stage[data-layout="bleed-left"] img {
  position: absolute;
  top: 0;
  width: auto;
  height: auto;
  max-height: 100vh;
  max-height: 100dvh;
  max-width: 100vw;
}
.stage[data-layout="bleed-right"] img { right: 0; }
.stage[data-layout="bleed-left"] img { left: 0; }

.stage[data-layout="center-landscape"] img {
  max-width: 80vw;
  max-height: 80vh;
  max-height: 80dvh;
}

.stage[data-layout="center-portrait"] img {
  max-width: 56vw;
  max-height: 82vh;
  max-height: 82dvh;
}

.stage-caption {
  position: absolute;
  bottom: 32px;
  left: 50%;
  transform: translateX(-50%);
  text-align: center;
  white-space: nowrap;
}

.stage-counter {
  position: absolute;
  left: 32px;
  bottom: 24px;
}

/* persistent bottom-right view toggle: "Grid" on the stage, "Gallery" on the grid */
.view-toggle {
  position: fixed;
  right: 24px;
  bottom: 24px;
  z-index: 60;
}

/* page count, project title, and Grid/Gallery toggle match the header's
   14px Buch on desktop/laptop (mobile keeps them at the base .label 12px) */
.stage-caption,
.stage-counter,
.view-toggle,
.grid-title {
  font-size: 14px;
}

/* ---------------------------------------------------------
   Index page — list sits at the top, under the header
--------------------------------------------------------- */
.index-list {
  /* no position/z-index here on purpose — that would create its own stacking
     context and wall the category links' mix-blend-mode off from the photo
     behind it. It paints on top simply by coming after .index-image-wrap
     in the markup. */
  margin: 0; /* a plain <ul> otherwise keeps the browser's default 16px top/bottom margin, overflowing the page by that much with nothing actually visible there */
  padding: calc(var(--header-h) + 32px) 32px 32px;
  max-width: 420px; /* keeps the hit area from bleeding into the photo on the right */
}

.index-list li { list-style: none; }

/* padding (not margin) creates the vertical spacing, so adjacent rows'
   hoverable boxes touch exactly — no dead gap between list items — and
   the block-level, full-width link enlarges the hit target beyond just
   the glyphs themselves. */
.index-list a {
  display: block;
  padding: 6px 0;
  /* positioned + z-index directly on the blending element, with nothing
     positioned between it and the isolated .index-page ancestor — an
     intermediate positioned wrapper (e.g. on .index-list itself) creates
     its own stacking context and walls the link's mix-blend-mode off from
     seeing the photo behind it */
  position: relative;
  z-index: 2;
}

.index-list a:hover,
.index-list a:focus {
  font-style: italic;
  transform: translateX(16px);
}

/* preview image — centered on the whole page, sitting under the list text
   where they overlap (list has z-index above this). Absolute, not fixed:
   mix-blend-mode only blends within a single stacking context, and
   position:fixed unconditionally creates its own — isolate on .index-page
   instead, so the list's blend-mode text can actually see this image. */
.index-page {
  position: relative;
  isolation: isolate;
  min-height: 100vh;
  min-height: 100dvh;
  background: var(--paper); /* the blend group needs something opaque to blend against outside the photo, or the white text over the page background just disappears */
}

.index-image-wrap {
  position: absolute;
  inset: 0; /* centers against the full page, header included — matches the Home stage */
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
  z-index: 1;
}

/* same sizing as the Home/Project stage's centered compositions, so a
   cover photo previews at the same scale it'd appear at elsewhere on the
   site — landscape matches center-landscape, portrait matches center-portrait */
.index-image-wrap img {
  max-width: 80vw;
  max-height: 80vh;
  max-height: 80dvh;
  width: auto;
  height: auto;
  opacity: 0;
  transition: opacity 0.25s cubic-bezier(0.54, 0.26, 0.29, 0.74); /* same curve/speed as every other image transition on the site */
}
.index-image-wrap img.is-portrait {
  max-width: 56vw;
  max-height: 82vh;
  max-height: 82dvh;
}

.index-image-wrap img.is-active { opacity: 1; }

/* ---------------------------------------------------------
   Grid pages (Home grid + Project grid)
   Uniform square cells, image centered/contained inside with generous
   inset padding — never cropped.
--------------------------------------------------------- */
.grid-page { padding: var(--header-h) 0 96px; } /* grid sits right against the header, and reaches the left/right edges — each figure's own padding creates the inset around its image */

/* stage <-> grid crossfade when the view toggle is clicked — same timing
   as the stage's own image-to-image fade */
.stage,
.grid-page {
  transition: opacity 0.25s cubic-bezier(0.54, 0.26, 0.29, 0.74);
}
.view-fade-out { opacity: 0; }

/* stays pinned in the same on-screen spot as the gallery view's
   stage-caption, so switching views doesn't relocate the title —
   and taking it out of flow lets the grid itself sit right under the header */
.grid-title {
  position: fixed;
  bottom: 32px;
  left: 50%;
  transform: translateX(-50%);
  text-align: center;
  white-space: nowrap;
  z-index: 50;
}

.grid-uniform {
  display: grid;
  grid-template-columns: repeat(4, 1fr); /* laptops, up to ~16" (1728px logical) */
  gap: 16px;
}

/* wider than a 16" laptop — Studio Display / large desktop monitors */
@media (min-width: 1800px) {
  .grid-uniform { grid-template-columns: repeat(6, 1fr); }
}

.grid-uniform figure {
  margin: 0;
  aspect-ratio: 1 / 1;
  padding: 12%;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
}

.grid-uniform img {
  width: 100%;
  height: 100%;
  object-fit: contain; /* not max-height:100% — Safari doesn't reliably resolve a
    percentage max-height against a flex parent whose height comes from
    aspect-ratio, so tall images overflowed the box and got clipped by the
    figure's overflow:hidden. object-fit sidesteps that entirely. */
  /* staggered per-item via transition-delay in JS; duration/curve inherited from .fade-img */
}

/* ---------------------------------------------------------
   Lightbox — translucent scrim over the page, click outside to close
--------------------------------------------------------- */
.lightbox {
  position: fixed;
  inset: 0;
  background: rgba(253, 253, 253, 0.8);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 200;
}

.lightbox img {
  max-width: 90vw;
  max-height: 90vh;
  max-height: 90dvh;
  width: auto;
  height: auto;
  pointer-events: none; /* clicks always fall through to the scrim, which closes */
}

/* ---------------------------------------------------------
   Info page
--------------------------------------------------------- */
.info-page {
  min-height: 100vh;
  min-height: 100dvh;
  padding: calc(var(--header-h) + 32px) 32px 32px;
  position: relative;
}

/* Mirrors the header's 4 equal flex:1 columns exactly (same page padding,
   same 4-way split, no gap) so column edges line up with MATT HARDY /
   INDEX / INFO / CONTACT above. Column 4 is left empty — the portrait
   sits there, positioned independently since it's bottom-anchored. */
.info-columns {
  display: grid;
  grid-template-columns: repeat(3, 1fr); /* Info has 3 real sections, not 4 */
}

.info-columns > div { padding-right: 32px; }

.info-columns .body-copy { white-space: nowrap; }
.info-columns a:hover { text-decoration: underline; }
.info-columns p.body-copy { white-space: normal; } /* the bio paragraph is the one line meant to wrap */

/* Explicit, non-collapsing vertical rhythm — margins were collapsing in
   ways that made the actual on-screen gaps drift from what the CSS said,
   so every group in the Info columns uses flex + gap instead. */
.info-columns .stack { display: flex; flex-direction: column; }
.info-columns .gap-xs { gap: 6px; }
.info-columns .section-heading { line-height: 16px; }
.info-columns .gap-md { gap: 24px; }
.info-columns .gap-lg { gap: 32px; }

.info-image {
  position: absolute;
  right: 32px;
  bottom: 32px;
  width: 486px;
  max-width: 40vw;
}

.info-image img { width: 100%; height: auto; }

.info-footer {
  position: absolute;
  left: 32px;
  bottom: 24px;
}

/* ---------------------------------------------------------
   404
--------------------------------------------------------- */
.error-page {
  min-height: 100vh;
  min-height: 100dvh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--header-h) 32px 0;
}

.error-message {
  font-family: var(--font);
  font-size: 28px;
  font-weight: 400;
  color: var(--ink);
  margin: 0;
}

/* ---------------------------------------------------------
   Mobile — single column stacking (per the Figma mobile variants)
--------------------------------------------------------- */
@media (max-width: 768px) {
  .site-header { padding: 0 20px; justify-content: space-between; }
  .site-header .col { flex: none; } /* equal flex:1 columns left too little room between short items at mobile widths — space the 3 remaining items across the full width by their own content width instead. Reserved-space asterisk (above) keeps this from shifting when active state toggles. */
  .site-header .col:last-child { display: none; } /* mobile header drops Contact, per Figma's Header - Small */
  .site-header .label { font-size: 12px; } /* still Buch, just smaller on mobile */
  .stage-caption,
  .stage-counter,
  .view-toggle,
  .grid-title { font-size: 12px; }

  /* mobile ignores the desktop bleed-left/bleed-right/center-* compositions
     entirely — every image just centers in the space between the header and
     the counter/toggle row, with a uniform 16px gutter on the sides */
  .stage { padding: var(--header-h) 16px 64px; }
  .stage img {
    position: static !important;
    top: auto !important;
    left: auto !important;
    right: auto !important;
    width: auto !important;
    height: auto !important;
    max-width: 100% !important;
    max-height: 100% !important;
  }
  /* title stays centered, just pulled down to sit on the exact same
     bottom line as the counter/toggle (desktop's 32px/24px gap is
     imperceptible there but reads as two rows at mobile scale) */
  .stage-caption,
  .grid-title { bottom: 16px; }
  .stage-counter { left: 16px; bottom: 16px; }
  .view-toggle { right: 16px; bottom: 16px; }

  /* .index-image-wrap comes first in the markup (needed on desktop so the
     list's mix-blend-mode text can see it), but on mobile they stack in
     flow instead of overlapping — flex + order puts the list back on top
     visually without touching the underlying DOM order. */
  .index-page { display: flex; flex-direction: column; }
  .index-list { order: 1; padding: calc(var(--header-h) + 8px) 20px 24px; }
  .index-image-wrap { order: 2; position: static; width: 100%; height: 50vh; height: 50dvh; margin-top: 0; }
  .index-image-wrap img { opacity: 1; }

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

  .info-page { padding: calc(var(--header-h) + 8px) 20px 24px; }
  .info-columns { grid-template-columns: 1fr; gap: 32px; }
  .info-columns > div { padding-right: 0; }
  .info-image { position: static; width: 100%; max-width: 100%; margin-top: 32px; }
  .info-footer { position: static; display: block; margin-top: 24px; }

  .error-page { padding: calc(var(--header-h) + 8px) 20px 0; }
  .error-message { font-size: 20px; text-align: center; }
}
