/* ============================================================
   RG — photography portfolio
   Type and layout: the "Airy Focused" system —
   Cormorant Garamond (headings) / Work Sans (body)
   Inter (nav, collection titles) / Courier Prime (the lightbox counter)
   ============================================================ */

:root {
  /* palette */
  --bg:            #ffffff;
  --heading:       #3E4E5E;   /* deep slate — headings, logo, nav */
  --text:          #24292E;
  --detail:        #D5DBE0;   /* hairlines */
  --accent-bg:     #F2F5F7;   /* the tint a photo frame holds while it loads */

  /* type */
  --serif: 'Cormorant Garamond', Georgia, 'Times New Roman', serif;
  --body:  'Work Sans', -apple-system, BlinkMacSystemFont, 'Helvetica Neue', Arial, sans-serif;
  --mono:  'Courier Prime', 'Courier New', Courier, monospace;
  --nav:   'Inter', -apple-system, system-ui, sans-serif;

  /* layout */
  --gutter: 14px;                              /* grid gap, as on the demo */
  --pad: clamp(20px, 2.36vw, 40px);            /* page side padding */
  --split-gap: clamp(28px, 9.42%, 130px);      /* gap between the two content columns */
  --lb-pad: clamp(16px, 4vw, 64px);            /* lightbox breathing room */

  /* collection covers: blurred, with the title laid over them */
  --cover-blur:  21px;
  --cover-scrim: 0.23;                         /* keeps white type legible */
  --cover-title: 1.25rem;                      /* 20px */
}

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

html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--body);
  font-weight: 500;
  font-size: 1rem;
  line-height: 1.6875;
  -webkit-font-smoothing: antialiased;
}

img { display: block; max-width: 100%; }
a { color: inherit; text-decoration: none; }

/* ---------- type scale ---------- */

.display,
h1, h2, h3 { color: var(--heading); font-family: var(--serif); margin: 0; }

.display {
  font-weight: 400;
  font-size: min(4.6875rem, 1rem + 4.1vw);
  line-height: 1;
  letter-spacing: -0.03em;
}

h1, .h1 {
  font-weight: 400;
  font-size: min(3.9375rem, 1rem + 3.26vw);
  line-height: 1;
  letter-spacing: -0.03em;
}

h2, .h2 {
  font-weight: 500;
  font-size: min(2.375rem, 1rem + 1.53vw);
  line-height: 1.105;
  letter-spacing: -0.03em;
}

h3, .h3 {
  font-weight: 400;
  font-size: min(1.5625rem, 1rem + 0.63vw);
  line-height: 1.16;
  letter-spacing: -0.03em;
}

/* Present to screen readers and search engines, absent on screen. Used for
   the Collections page heading, which the design does without. */
.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

p { margin: 0; }
.lede  { font-size: 1rem;      line-height: 1.6875; }
.small { font-size: 0.9375rem; line-height: 1.73; }

/* ---------- layout ---------- */

.wrap { padding-inline: var(--pad); }

/* two equal content columns — 621px each at a 1440px viewport, as on the original */
.split {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--split-gap);
  align-items: start;
}
.split--middle { align-items: center; }

/* ---------- header ---------- */

.site-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  /* a long site name drops the nav onto its own line rather than colliding */
  flex-wrap: wrap;
  gap: 14px 24px;
  padding-block: 43px 30px;
  background: var(--bg);
}

.logo {
  color: var(--heading);
  text-decoration: none;
  display: inline-flex;
}
/* Drawn geometry, not type: stroke inherits --heading via currentColor, and the
   viewBox is trimmed to the inked bounds so spacing is controlled here, not by
   dead space inside the SVG. Width drives it; height follows the aspect ratio. */
.logo__mark {
  display: block;
  /* Both dimensions are explicit on purpose. Safari/WebKit will not derive an
     inline SVG's height from its viewBox, so `height:auto` collapses the mark
     to zero and it vanishes. viewBox is 141x76, so height = width x 0.539. */
  width: 29px;
  height: 15.63px;
}

.nav { display: flex; gap: 52px; }
.nav a {
  color: var(--heading);
  font-family: var(--nav);
  font-weight: 400;
  font-size: 0.8125rem;
  line-height: 1;
  letter-spacing: 0.2em;
  text-transform: uppercase;
}

/* ---------- hero ---------- */

.hero { padding-block: clamp(4px, 0.5vw, 8px) clamp(38px, 4vw, 57px); }
/* An empty hero would still reserve ~65px, leaving a void between the header
   and the gallery. Collapse it while it holds no elements; putting the <p>
   back in index.html restores the spacing automatically. Keyed on "has no
   element children" rather than on .lede so the Collections hero, which holds
   an <h1>, is unaffected. */
.hero:not(:has(*)) { padding-block: 0; }
.hero .display {
  max-width: 612px;
  font-weight: 500;
  font-size: min(2.375rem, 1rem + 1.53vw);
  line-height: 1.105;
}
.hero p { max-width: 615px; font-size: 0.9375rem; line-height: 1.73; }

/* A collection page leads with its title. It is set to the nav's type rather
   than the serif the other headings use, so the page's one piece of chrome
   type matches the links above it. Every value here is the .nav a rule, and
   the two must be changed together — including the smaller step at 640px. */
.collection-title {
  font-family: var(--nav);
  font-weight: 400;
  font-size: 0.8125rem;
  letter-spacing: 0.2em;
  text-transform: uppercase;
}

/* ---------- gallery ---------- */

.gallery {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--gutter);
}

.shot {
  display: block;
  position: relative;
  overflow: hidden;
  aspect-ratio: 3 / 2;
  background: var(--accent-bg);
  border: 0;
  padding: 0;
  cursor: zoom-in;
}
.shot img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.shot:focus-visible { outline: 2px solid var(--heading); outline-offset: 3px; }

/* ---------- collections index ---------- */

/* Same two-column rhythm and gutter as the gallery, so the two pages read as
   one system; the title beneath is what distinguishes a collection from a
   single photograph. */
.collections {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--gutter);
}

.collection { display: block; position: relative; color: inherit; }
.collection__frame {
  display: block;
  position: relative;
  overflow: hidden;
  aspect-ratio: 3 / 2;
  background: var(--accent-bg);
}
.collection__frame img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: blur(var(--cover-blur));
  /* a blur samples past the edges; the slight overscale keeps the feathered
     result from showing against the page */
  transform: scale(1.08);
}

/* Scrim, not decoration: white type over an arbitrary photograph has no
   guaranteed contrast, and blurring smooths detail without lowering
   luminance — a pale sky would swallow the title on its own. */
.collection__frame::after {
  content: '';
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, var(--cover-scrim));
}

/* The title is a sibling of the frame, lifted out of flow and laid over it,
   so the overlay needs no extra markup. */
.collection__title {
  position: absolute;
  inset: 0;
  z-index: 2;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0 8%;
  color: #fff;
  font-family: var(--nav);
  font-weight: 400;
  font-size: var(--cover-title);
  line-height: 1.2;
  letter-spacing: 0.2em;
  text-align: center;
  text-transform: uppercase;
  text-shadow: 0 1px 24px rgba(0, 0, 0, .34);
}
.collection:focus-visible { outline: 2px solid var(--heading); outline-offset: 3px; }

/* ---------- about / contact page ---------- */

.figure { margin: 0; overflow: hidden; background: var(--accent-bg); }
.figure img { width: 100%; height: 100%; object-fit: cover; }
/* The about photo is shown whole. A fixed frame ratio plus object-fit:cover
   was trimming ~41% off a 2:3 portrait, which read as a zoom-in; letting the
   frame adopt the image's own ratio means nothing is cropped, and a future
   swap of any shape still shows complete. */
.figure--about {
  aspect-ratio: auto;              /* frame takes the image's own ratio */
  max-width: clamp(220px, 30vw, 400px);  /* scale the whole photo down, never crop it */
  justify-self: start;             /* left edge stays put, so the channel to the copy is unchanged */
}
.figure--about img { height: auto; }

.about { padding-block: clamp(28px, 3.5vw, 50px) clamp(64px, 8vw, 128px); }
/* tighter channel between the copy and the photo than the default split gap */
.about { --split-gap: clamp(24px, 2.8vw, 40px); }

/* contact page sits its footer close under the photo */
.contact-page .about { padding-bottom: clamp(14px, 1.6vw, 24px); }
.contact-page .site-footer { padding-block: clamp(18px, 2vw, 30px) clamp(28px, 3.5vw, 50px); }
.about__copy { text-align: right; }
.about__copy > * + * { margin-top: 28px; }
.about__copy p { max-width: 56ch; margin-left: auto; }

/* inline links in body copy need a visible affordance */
.about__copy a {
  border-bottom: 1px solid var(--detail);
  transition: border-color .25s ease;
}
.about__copy a:hover { border-bottom-color: currentColor; }

/* ---------- 404 ---------- */

/* Sized to hold the footer down the page rather than leaving it under three
   lines of text: the header and footer together are roughly 190px, so this
   takes the rest of the viewport and centres the copy in it. */
.notfound {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: center;
  min-height: max(320px, calc(100dvh - 190px));
  max-width: 46ch;
  padding-block: clamp(28px, 4vw, 56px);
}
.notfound__code {
  margin-bottom: 18px;
  color: var(--heading);
  font-family: var(--mono);
  font-size: 0.6875rem;
  letter-spacing: 0.2em;
  opacity: .75;
}
.notfound h1 { margin-bottom: 18px; }
.notfound a {
  border-bottom: 1px solid var(--detail);
  transition: border-color .25s ease;
}
.notfound a:hover { border-bottom-color: currentColor; }

/* ---------- footer ---------- */

.site-footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  padding-block: clamp(18px, 2vw, 28px) clamp(14px, 1.6vw, 22px);
}
.site-footer p {
  margin: 0;
  /* same treatment as the nav links */
  color: var(--heading);
  font-family: var(--nav);
  font-weight: 400;
  font-size: 0.625rem;   /* 10px — a step below the nav */
  line-height: 1;
  letter-spacing: 0.2em;
  text-transform: uppercase;
}
.to-top {
  display: grid;
  place-items: center;
  width: 34px; height: 34px;
  background: none;
  border: 0;
  color: var(--heading);
  cursor: pointer;
  transition: transform .3s ease;
}
.to-top:hover { transform: translateY(-4px); }

/* ---------- lightbox ---------- */

.lightbox {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--lb-pad);
  background: rgba(20, 16, 14, .94);
  opacity: 0;
  visibility: hidden;
  transition: opacity .35s ease, visibility .35s;
}
.lightbox.is-open { opacity: 1; visibility: visible; }
.lightbox[hidden] { display: none; }
.lightbox img {
  /* both dimensions capped against the viewport, so tall frames stay centred */
  max-width: 100%;
  max-height: calc(100dvh - 2 * var(--lb-pad));
  width: auto;
  height: auto;
  object-fit: contain;
}
.lightbox__btn {
  position: absolute;
  display: grid;
  place-items: center;
  width: 48px; height: 48px;
  background: none;
  border: 0;
  color: #fff;
  cursor: pointer;
  opacity: .75;
  transition: opacity .2s ease;
}
.lightbox__btn:hover { opacity: 1; }
.lightbox__close { top: 18px; right: 18px; }
.lightbox__prev  { left: 12px;  top: 50%; transform: translateY(-50%); }
.lightbox__next  { right: 12px; top: 50%; transform: translateY(-50%); }
.lightbox__count {
  position: absolute;
  bottom: 20px;
  left: 50%;
  transform: translateX(-50%);
  color: #fff;
  font-family: var(--mono);
  font-size: 0.6875rem;
  letter-spacing: 0.2em;
  opacity: .7;
}
body.is-locked { overflow: hidden; }

/* ---------- entrance animation ---------- */

.reveal {
  opacity: 0;
  transform: translateY(22px);
  transition: opacity .9s cubic-bezier(.16,1,.3,1), transform .9s cubic-bezier(.16,1,.3,1);
}
.reveal.is-in { opacity: 1; transform: none; }

@media (prefers-reduced-motion: reduce) {
  .reveal { opacity: 1; transform: none; transition: none; }
  .to-top { transition: none; }
}

/* ---------- responsive ---------- */

@media (max-width: 900px) {
  .split { grid-template-columns: 1fr; gap: clamp(28px, 6vw, 48px); }
  .hero .display { max-width: none; order: -1; }  /* headline leads once stacked */
  .hero p { max-width: none; }

  /* The contact page is the exception: copy left, portrait right, at every
     width down to the narrowest phone. The composition — a right-aligned
     column of type reading into the photograph beside it — is the page, and
     stacking it dissolved that into an unrelated photo with a caption. So the
     two columns stay and the proportions give instead: the photo drops its
     desktop cap and takes a share of the row, and the type is sized from the
     viewport so the headline holds its two lines in the column it is left. */
  .about.split {
    grid-template-columns: 1fr minmax(0, 38%);
    gap: clamp(12px, 3.4vw, 40px);
  }
  .about__copy p { max-width: none; }
  .figure--about { max-width: none; justify-self: stretch; }
  /* Both scales are drawn to meet the page-wide ones at 900px, so nothing
     jumps across the breakpoint; they simply fall away faster below it. The
     headline is sized so its two hand-broken lines still hold at 320px. */
  .contact-page h1 { font-size: min(3.9375rem, 0.8rem + 3.6vw); }
  .contact-page .lede { font-size: clamp(0.8125rem, 0.45rem + 1.6vw, 1rem); }
  .contact-page .about__copy > * + * { margin-top: clamp(14px, 3.6vw, 28px); }
}

@media (max-width: 640px) {
  .gallery { grid-template-columns: 1fr; }
  /* Two photographs side by side on a phone are two small photographs. Rows
     collapse; the ratios and the order survive. */
  .gallery--justified .jrow { display: grid; grid-template-columns: 1fr; gap: var(--gutter); }
  .gallery--justified .shot { flex: none; }
  .collections { grid-template-columns: 1fr; }
  .site-header { padding-block: 24px 20px; }
  .nav { gap: 28px; }
  .nav a { font-size: 0.6875rem; letter-spacing: 0.15em; }
  .collection-title { font-size: 0.6875rem; letter-spacing: 0.15em; }
  .logo__mark { width: 22px; height: 11.86px; }
  .lightbox__prev { left: 2px; }
  .lightbox__next { right: 2px; }
}

/* ---------- gallery layouts ----------

   The grid above crops every photo into a uniform 3:2 cell. These two size
   the cell from the photograph instead, for sets that are not shot to one
   shape. Which one a page uses is a class stamped by build.py from the
   'layouts' map in photos.json; see the README.

   Both read --r off each .shot — the photograph's width over its height,
   written out by build.py.                                                */

/* --- justified rows -------------------------------------------------- */
/* build.py groups the photographs into rows and stamps each row with --span,
   the sum of its aspect ratios, and --n, how many it holds. A row is always
   exactly full: each photo takes --r/--span of the width left after the
   gutters, so the widths are proportional to the ratios and every photo in
   the row therefore lands on the same height. Nothing here measures anything,
   so it renders with JavaScript off, and the arithmetic is exact rather than
   nearly exact.

   Row height is the width over --span, which is why --span is also the
   density control: see DENSITIES in build.py. */
.gallery--justified {
  display: grid;
  grid-template-columns: minmax(0, 1fr);   /* one row per row, not the base grid's two */
  gap: var(--gutter);
}
/* Tops align and each photo keeps its own height. The arithmetic makes those
   heights identical anyway; not stretching means a rounding error shows up as
   a hairline rather than by quietly distorting a photograph. */
.gallery--justified .jrow { display: flex; align-items: flex-start; gap: var(--gutter); }
.gallery--justified .shot {
  aspect-ratio: var(--r, 1.5);
  flex: 0 1 calc((100% - (var(--n) - 1) * var(--gutter)) * var(--r, 1.5) / var(--span));
  min-width: 0;
}

/* --- pinned rows -------------------------------------------- */
/* The grid has no element standing for a row, so a pinned row is marked on
   the photograph it starts with: one on its own takes both columns, and a
   pair starts a fresh row rather than being split across two. In a justified
   row build.py has already stamped --n and --span on the row itself, and in
   flow every photograph has a row already — so these classes ride along in
   those layouts doing nothing, and switching layout stays a class change
   rather than a re-render.

   Below 640px the gallery is one column, where every photograph already has
   a row to itself and both rules are moot. */
.shot--solo { grid-column: 1 / -1; }
.shot--rowstart { grid-column: 1; }

/* --- flow --------------------------------------------------- */
/* One photograph at a time, each capped by height rather than width, so a
   panorama and a portrait carry the same visual weight. */
.gallery--flow {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  justify-items: center;
  gap: clamp(40px, 6vw, 104px);
  --cap-h: 78vh;
}
.gallery--flow .shot {
  aspect-ratio: var(--r, 1.5);
  width: min(100%, calc(var(--cap-h) * var(--r, 1.5)));
}
