/* GALLERY — local styles. Prefix: g-
   Serves both /gallery/ and the three collection pages under it, which are one
   template (gallery/collection.njk) paginated over _data/gallery.json. */

/* --- Hero ---------------------------------------------------------------- */
/* Same construction as the research, publications and Radio So'o heroes: a
   photograph edge to edge behind the text under a graded dark scrim. This
   section is --paper over a scrimmed image, so both accents take their
   dark-surface counterparts — --petrol measures 1.72:1 here and --saffron is
   1.88:1 on the paper ground it could have had. See CLAUDE.md. */
.g-hero {
  background: var(--ink);
  color: var(--paper);
  padding: 104px 0 88px;
  min-height: clamp(420px, 56vh, 560px);
  display: flex;
  align-items: center;
  position: relative;
  overflow: hidden;
  isolation: isolate;   /* keeps the scrim under the text without z-index on everything */
}
/* The collection pages are shorter. Their hero carries a back link, a title and
   a count — no lead paragraph — and the pictures below it are the page. */
.g-hero--sub {
  padding: 84px 0 68px;
  min-height: clamp(300px, 38vh, 400px);
}
.g-hero__bg {
  position: absolute;
  inset: 0;
  z-index: -1;
}
.g-hero__bg img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Centred, and on this page that is a real choice rather than a default.
     Every other hero on the site has a horizon in it somewhere, so the vertical
     half of this property decides whether the text sits on sky or on ground.
     The logo photograph is an aerial looking straight down at a river running
     corner to corner, with forest either side and the sandbank on the diagonal.
     A diagonal survives any crop: at 1280x722 and at 375x812 the centre keeps
     river, sandbank and both forest walls in frame, which no banded composition
     would do. There is nothing to bias towards. */
  object-position: 50% 50%;
  display: block;
}
/* THE SCRIM.
   Measured on the composited pixels behind the real text boxes — the image
   drawn to a canvas with this element's own object-fit geometry, this gradient
   filled over it, worst-case pixel inside each text box. Both widths, tighter
   of the two shown:

     eyebrow  11px  --saffron-lift  4.98:1  AA yes (small text, needs 4.5)  1.11
     lead     20px  .86 alpha       6.83:1  AA yes                          1.52
     h1       90px  --paper         6.96:1  AA yes, AAA yes (large text)    2.32

   The eyebrow is the binding constraint, as it is on all five of the site's
   other scrimmed heroes — see the correction recorded in CLAUDE.md, which found
   that every one of those files had named the headline's <em> instead. There is
   no <em> in this headline, so this hero has one fewer thing to hold, and the
   headline has the most room of the three by a factor of two.

   DESKTOP IS THE TIGHTER WIDTH HERE — 4.98 against 5.12 at 375x812 — which is
   worth flagging because CLAUDE.md records the opposite for /collective/ and
   says to re-measure at 375 first. That instruction does not generalise, and
   the reason it does not is instructive. Sampling the unscrimmed image behind
   the eyebrow box at each width:

     1280x720   mean rgb(124,119,107)   brightest pixel L 218
      375x812   mean rgb(153,137,130)   brightest pixel L 203

   The phone crop is LIGHTER ON AVERAGE and still the looser of the two, because
   AA is measured on the worst pixel rather than the mean, and the desktop crop
   catches the brighter peak. A first draft of this comment explained
   the gap as forest at one width and sandbank at the other; the means say
   otherwise. Measure both widths, and measure the peak.

   THE WEIGHT WAS SWEPT, NOT GUESSED. Every stop moved together — the shape is
   the research hero's, fitted to a photograph, and only the overall weight was
   ever in question. Eyebrow ratio at 1280, which is the number that decides it:

     0.74/0.62/0.76   4.15  FAILS      0.80/0.68/0.82   4.98  <- shipped
     0.76/0.64/0.78   4.43  FAILS      0.82/0.70/0.84   5.38
     0.78/0.66/0.80   4.66  passes     0.86/0.74/0.88   6.04

   0.78 passes and was the first draft. It is not what shipped, because a
   headroom of 1.04 means one brighter pixel behind eleven-pixel type is the
   whole margin. 0.80/0.68/0.82 is the /news/ hero's shipped weight
   exactly, and lighter than /research/'s 0.82/0.70/0.84.

   Going the other way — lighter, to show more of the picture, which is what the
   19 August 2026 pass across the site was for — costs AA outright below 0.78,
   and the sweep above is the evidence rather than an argument. */
.g-hero__bg::after {
  content: "";
  position: absolute;
  inset: 0;
  background:
    linear-gradient(180deg,
      rgba(10, 13, 18, 0.80) 0%,
      rgba(10, 13, 18, 0.68) 45%,
      rgba(10, 13, 18, 0.82) 100%);
}
/* A touch heavier than the parent page's, and the reason is the sources. The
   collection heroes reuse an image that already exists elsewhere on the site
   rather than a dedicated 2400-wide cut, so the covers run 900x900, 1600x900
   and 1600x1054 against the parent hero's 2400x1600 — the first of those is
   upscaled well past 1:1 at desktop width. That is soft rather than broken and
   the scrim hides most of it, but there are three different photographs behind
   one gradient here instead of one, so it is fitted to the worst of them.

   MEASURED ON ALL THREE, worst case across 1280x720 and 375x812, at the weight
   below and with .g-back at --paper (see the note on that rule):

                             back    h1     count (req 4.5 / 3.0 / 4.5)
     field-research-photos   8.02   6.85   5.99
     field-research-videos   9.40   8.55   8.08
     events                  7.18   6.74   5.61   <- binding

   The 12px .g-hero__count on the events cover is the tightest thing here at a
   headroom of 1.25, and the whole gradient is fitted to it. .g-back was the
   binding constraint until it went to --paper and is now the loosest.

   Adding a fourth collection to _data/gallery.json means measuring its cover
   against this gradient. Nothing warns you if it comes out short — the events
   cover shipped a 4.49:1 fail past a reading of the other two. */
.g-hero--sub .g-hero__bg::after {
  background:
    linear-gradient(180deg,
      rgba(10, 13, 18, 0.82) 0%,
      rgba(10, 13, 18, 0.72) 45%,
      rgba(10, 13, 18, 0.84) 100%);
}
.g-hero__inner { position: relative; width: 100%; }
/* --saffron-lift, not --saffron: 11px is small text needing 4.5:1, and it is
   the binding constraint on this hero. See the scrim note above. */
.g-hero__eyebrow { margin-bottom: 22px; color: var(--saffron-lift); }
.g-hero__title {
  font-family: var(--display);
  font-size: clamp(56px, 7vw, 104px);
  line-height: 0.94;
  letter-spacing: -0.026em;
  font-weight: 400;
  color: var(--paper);
  margin-bottom: 24px;
  text-wrap: balance;
}
/* Smaller on the collection pages. "Field research photos" is three words
   against the parent page's one, and at 104px it sets to three lines that push
   the pictures off the screen. */
.g-hero__title--sub {
  font-size: clamp(40px, 5vw, 68px);
  max-width: 18ch;
  margin-bottom: 18px;
}
.g-hero__lead {
  font-family: var(--display);
  font-size: clamp(19px, 1.55vw, 22px);
  line-height: 1.42;
  color: rgba(243, 235, 221, 0.86);
  max-width: 46ch;
}
/* Derived from the length of the list in _data/gallery.json, so it cannot go
   stale the way a typed number would. */
.g-hero__count {
  font-family: var(--mono);
  font-size: 12px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: rgba(243, 235, 221, 0.78);
}
/* --paper WITH AN UNDERLINE, not an accent colour, and this was measured rather
   than styled. At --saffron-lift over the events cover at 375x812 this link
   measures 4.49:1 against the 4.5 that 12px text needs — a fail by one
   hundredth, on one of the three covers, at one of the two widths. The other
   two collections passed, which is exactly how a thing like this ships.

   Three ways out were priced. Weighing the sub-hero scrim to 0.84/0.74/0.86
   clears it at 4.83, and costs the picture — the opposite of what the
   19 August 2026 pass across the site was for. --saffron (3.82) and
   --petrol-lift (3.85) are both worse than what is here. --paper is 7.18:1,
   a headroom of 1.60, and needs nothing from the scrim.

   The underline is then doing the work the colour used to, which is the same
   call the publications hero makes for its Google Scholar link and is recorded
   in CLAUDE.md: on a dark ground both accents are spoken for. It also means
   this link is not distinguished from its surroundings by colour alone. */
.g-back {
  display: inline-block;
  font-family: var(--mono);
  font-size: 12px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--paper);
  text-decoration: underline;
  text-underline-offset: 4px;
  text-decoration-thickness: 1px;
  margin-bottom: 24px;
}
.g-back:hover { text-decoration-thickness: 2px; }
.g-back:focus-visible { outline: 2px solid var(--paper); outline-offset: 3px; }

/* --- The three collection boxes ------------------------------------------ */
.g-index {
  background: var(--paper);
  padding: 72px 0 88px;
}
.g-index__grid {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 32px;
}
.g-card {
  background: var(--paper-2);
  border: 1px solid var(--rule);
  border-radius: var(--r-card-lg);
  overflow: hidden;
  /* The stretched link below is positioned against this. */
  position: relative;
  transition: border-color .18s ease, transform .18s ease;
}
.g-card:hover {
  border-color: var(--rule-strong);
  transform: translateY(-2px);
}
@media (prefers-reduced-motion: reduce) {
  .g-card { transition: border-color .18s ease; }
  .g-card:hover { transform: none; }
}
/* ONE link per card, on the title, stretched over the whole tile by a
   pseudo-element against .g-card's `position: relative`. A second link on the
   image would put the same destination in the tab order twice and announce it
   twice. See the note in gallery/index.njk for why the link is on the title
   rather than wrapped round the tile — it is about the accessible name, not
   about layout, and the two structures look identical on screen. */
.g-card__link {
  text-decoration: none;
  color: inherit;
}
.g-card__link::after {
  content: "";
  position: absolute;
  inset: 0;
}
.g-card__link:focus-visible { outline: none; }
.g-card:has(.g-card__link:focus-visible) {
  outline: 2px solid var(--petrol);
  outline-offset: 3px;
}
.g-card__figure {
  display: block;
  aspect-ratio: 4 / 3;
  overflow: hidden;
  background: var(--paper-3);
}
.g-card__figure img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transition: transform .5s ease;
}
.g-card:hover .g-card__figure img { transform: scale(1.03); }
@media (prefers-reduced-motion: reduce) {
  .g-card__figure img,
  .g-card:hover .g-card__figure img { transition: none; transform: none; }
}
.g-card__body {
  display: block;
  padding: 22px 24px 26px;
}
.g-card__index {
  display: block;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.18em;
  /* --petrol, not --saffron: this is a light surface, where --saffron measures
     1.88:1 and fails AA outright. CLAUDE.md, colour section. */
  color: var(--petrol);
  margin-bottom: 10px;
}
/* An h2 now, so it needs the heading defaults reset — the site's global h2 is
   not this size. */
.g-card__title {
  display: block;
  font-family: var(--display);
  font-size: clamp(21px, 1.9vw, 26px);
  font-weight: 600;
  letter-spacing: -0.015em;
  line-height: 1.12;
  color: var(--ink);
  margin-bottom: 8px;
}
.g-card__count {
  display: block;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  /* --steel clears AA on bare --paper (5.38:1); this tile is --paper-2, which is
     darker, so it is checked against that and not assumed: 4.94:1, a headroom of
     1.10. It passes and it is the tightest pair on this page's light surfaces —
     --paper-3 would put it at 4.50, which is the requirement to two decimals and
     no margin at all, so do not darken this tile without re-measuring this
     line. */
  color: var(--steel);
  margin-bottom: 16px;
}
.g-card__cue {
  display: block;
  font-family: var(--mono);
  font-size: 12px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--petrol);
}
.g-card__cue .arrow { transition: transform .18s ease; display: inline-block; }
.g-card:hover .g-card__cue .arrow { transform: translateX(3px); }
@media (prefers-reduced-motion: reduce) {
  .g-card__cue .arrow,
  .g-card:hover .g-card__cue .arrow { transition: none; transform: none; }
}

/* --- The gallery itself -------------------------------------------------- */
.g-collection {
  background: var(--paper);
  padding: 64px 0 88px;
}
/* COLUMNS, NOT GRID, AND THE REASON IS THE PHOTOGRAPHS.
   These images run from 1:1 and 3:2 through to 3.6:1 full-bleed bands, because
   they are the pictures the site already uses rather than a set shot for a
   gallery. A row-based grid would have to crop them to a common box to look
   tidy — and cropping a photograph to fit a layout is the one thing a gallery
   of that photographer's work should not do. Multi-column flow lets every image
   keep its own shape.

   The cost is that reading order goes down each column rather than across, and
   that is accepted: the DOM order is unchanged, so the tab order and a screen
   reader still run in the order _data/gallery.json lists them.

   break-inside: avoid keeps an item whole across a column break. It mattered
   more when the caption sat under each picture; it still matters, because the
   button and its cue are one box. */
.g-grid {
  list-style: none;
  margin: 0 0 56px;
  padding: 0;
  column-count: 3;
  column-gap: 24px;
}
/* The closed <dialog> is a child of this <li> and is display: none until it
   opens, so it takes no space in the column flow. */
.g-item {
  break-inside: avoid;
  margin: 0 0 24px;
}
.g-item__figure { margin: 0; position: relative; }
.g-item__media {
  width: 100%;
  height: auto;
  display: block;
  border-radius: var(--r-card);
  background: var(--paper-3);
}

/* --- The picture as a button, and the caption behind it ------------------- */
/* The caption is not on the page — it is in the <dialog> below, opened by this
   button. See the long note in gallery/collection.njk for why the reveal is a
   click rather than a hover. This button has to disappear as a button: no
   border, no padding, no background, and `display: block` so the image inside
   it is not sitting on a text baseline with a few pixels of descender space
   under it. */
.g-item__open {
  display: block;
  width: 100%;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  position: relative;
  border-radius: var(--r-card);
  /* The cue is positioned against this and would otherwise escape the corner
     radius. */
  overflow: hidden;
  color: inherit;
  font: inherit;
}
.g-item__open .g-item__media {
  border-radius: 0;   /* the button carries the radius now */
  transition: opacity .18s ease, transform .5s ease;
}
.g-item__open:hover .g-item__media { opacity: 0.88; transform: scale(1.02); }
.g-item__open:focus-visible {
  outline: 2px solid var(--petrol);
  outline-offset: 3px;
}
@media (prefers-reduced-motion: reduce) {
  .g-item__open .g-item__media,
  .g-item__open:hover .g-item__media { transition: none; transform: none; }
}

/* THE CUE IS ALWAYS VISIBLE, and that is the decision rather than an oversight.
   Fading it in on hover looks better on a laptop and makes the whole feature
   invisible on a phone, where there is no hover state to enter — the pictures
   would read as flat images and nobody would learn there is anything behind
   them. It sits at 0.82 opacity and goes to 1 on hover, so pointer devices
   still get the feedback.

   --paper on rgba(10,13,18,0.78) was measured against the region of every one
   of the 23 photographs that actually sits under the pill: worst 8.51:1 on
   puente-bioceanica-2400.jpg, best 12.31, against the 4.5 that 10px text needs.
   The spread is narrow because the ground is nearly opaque rather than a wash,
   which is the point — a lighter pill would take its contrast from whatever
   photograph happened to be behind it, and a gallery is a page where that
   changes 23 times. */
.g-item__cue {
  position: absolute;
  left: 12px;
  bottom: 12px;
  font-family: var(--mono);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--paper);
  background: rgba(10, 13, 18, 0.78);
  border: 1px solid rgba(243, 235, 221, 0.22);
  border-radius: var(--r-pill);
  padding: 6px 12px;
  opacity: 0.82;
  transition: opacity .18s ease;
  pointer-events: none;   /* the button under it is the target */
}
.g-item__open:hover .g-item__cue,
.g-item__open:focus-visible .g-item__cue { opacity: 1; }

/* The videos' caption button. It cannot be an overlay on the picture the way
   the photographs' is — a <video> fills its box with its own transport
   controls, and anything laid over the lower edge of it covers the scrubber.
   So this one sits under the video as an ordinary small button. */
.g-item__vbtn {
  display: inline-block;
  margin-top: 12px;
  font-family: var(--mono);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  /* --petrol, not --saffron: this one is on --paper, where --saffron is 1.88:1
     and fails outright. 8.28:1 against the pill's ground. */
  color: var(--petrol);
  background: var(--paper-2);
  border: 1px solid var(--rule);
  border-radius: var(--r-pill);
  padding: 8px 14px;
  cursor: pointer;
  transition: background .18s ease, border-color .18s ease;
}
.g-item__vbtn:hover { background: var(--paper-3); border-color: var(--rule-strong); }
.g-item__vbtn:focus-visible { outline: 2px solid var(--petrol); outline-offset: 2px; }

/* --- The caption dialog --------------------------------------------------- */
/* Same construction and the same script as .r-modal on /research/ and the
   person cards on /collective/ — see assets/js/peer-cards.js. Wider than that
   one, because this dialog leads with the photograph: .r-modal holds a title
   and a paragraph and is capped at 620px, where a picture wants the room. */
.g-modal {
  width: min(880px, calc(100vw - 40px));
  max-height: calc(100vh - 60px);
  padding: 0;
  border: 1px solid rgba(243, 235, 221, 0.16);
  border-radius: var(--r-card-lg);
  background: #171c24;
  color: var(--paper);
  overflow: auto;
  /* The FLIP transition scales the whole dialog, so the origin has to be the
     centre for the movement to read as travel rather than stretch. */
  transform-origin: center center;
}
.g-modal::backdrop {
  background: rgba(10, 13, 18, 0.72);
  backdrop-filter: blur(3px);
}
.g-modal__inner { padding: 0 0 26px; position: relative; }
.g-modal__img {
  width: 100%;
  height: auto;
  display: block;
  /* The dialog's own top corners; the image is flush to three edges. */
  border-radius: var(--r-card-lg) var(--r-card-lg) 0 0;
  background: #0e1218;
}
.g-modal__cap {
  font-family: var(--display);
  font-size: clamp(16px, 1.5vw, 19px);
  line-height: 1.45;
  color: var(--paper);
  margin: 24px 32px 0;
}
.g-modal__meta {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.06em;
  /* .78 alpha on #171c24 measures 9.22:1 — this is small text and needs 4.5.
     The caption above it is --paper at 14.49:1. */
  color: rgba(243, 235, 221, 0.78);
  margin: 10px 32px 0;
}
.g-modal__meta .dot { margin: 0 5px; }
/* Over the photograph rather than beside it, because the photograph reaches the
   top edge. Its own near-opaque disc, for the same reason .g-item__cue has one:
   what is behind it is a different picture every time. */
.g-modal__close {
  position: absolute;
  top: 14px; right: 14px;
  width: 34px; height: 34px;
  border-radius: 50%;
  border: 1px solid rgba(243, 235, 221, 0.3);
  background: rgba(10, 13, 18, 0.72);
  color: var(--paper);
  font-size: 20px;
  line-height: 1;
  cursor: pointer;
  transition: background .18s ease, border-color .18s ease;
}
.g-modal__close:hover { background: rgba(10, 13, 18, 0.9); border-color: rgba(243, 235, 221, 0.55); }
.g-modal__close:focus-visible { outline: 2px solid var(--petrol-lift); outline-offset: 2px; }

/* Two videos, and they are the content rather than texture, so they get room
   rather than a column. A <video> has no intrinsic size until its metadata
   arrives, so the box is declared here instead — 16/9 is chaco-road-1600.mp4
   exactly and hero-canopy-1280.mp4 to within 1.7%, which object-fit absorbs. */
.g-grid--video {
  column-count: 1;
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 40px 28px;
}
.g-grid--video .g-item__media {
  aspect-ratio: 16 / 9;
  object-fit: cover;
  background: var(--ink);
}

/* --- Sideways to the other collections ----------------------------------- */
.g-more {
  border-top: 1px solid var(--rule);
  padding-top: 28px;
}
.g-more__label {
  display: block;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--steel);
  margin-bottom: 14px;
}
.g-more__list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 14px 32px;
}
.g-more__list a {
  font-family: var(--display);
  font-size: clamp(18px, 1.6vw, 22px);
  font-weight: 600;
  letter-spacing: -0.012em;
  color: var(--petrol);
  text-decoration: none;
}
.g-more__list a:hover { text-decoration: underline; }
.g-more__list a:focus-visible { outline: 2px solid var(--petrol); outline-offset: 3px; }

/* --- Narrower viewports -------------------------------------------------- */
@media (max-width: 1000px) {
  .g-index__grid { grid-template-columns: repeat(2, 1fr); }
  .g-grid { column-count: 2; }
}
@media (max-width: 720px) {
  .g-hero { padding: 80px 0 64px; }
  .g-hero--sub { padding: 64px 0 52px; }
  .g-index__grid { grid-template-columns: 1fr; }
  /* One column on a phone. Two columns of photographs at 375px puts each one
     under 170px wide, which is a thumbnail rather than a gallery. */
  .g-grid { column-count: 1; }
  .g-grid--video { grid-template-columns: 1fr; }
}
