:root {
  /* Sampled from img/logo_ambiance_v1.png */
  --bg: #fcf1e3;
  --ink: #5b1220;
  --accent: #c4703a;
  /* The accent on cream is ~3.4:1 — under AA for text. This darker cut reads
     as the same colour but passes; the light one stays for rules and hovers. */
  --accent-text: #94491f;

  /* Off-white for form fields: lighter and less saturated than --bg, so a field
     reads as a surface you can type into without introducing a new colour. */
  --field-bg: #fefaf4;

  /* Nav column: a sand panel that stays a panel all the way down, with a
     denser top so the logo sits in the strongest part of it.
       --nav-top   where the fade starts (12% accent)
       --nav-base  where it settles, and the colour below the fold (7% accent) */
  --nav-top: #f4ddc6;
  --nav-base: #f8e7d5;
  --nav-ink: #5b1220;

  --page-pad: 48px;
  --gap: 8px;
  --row-unit: 1px;

  /* Type scale. The display size is fluid so the contrast with the body text
     holds from a laptop to a wide screen instead of only at one width. It is
     sized in `cqw` — the width of its own block — because a `vw` value ignores
     how wide that block actually is and breaks the lines at some widths. */
  --display: clamp(22px, 7cqw, 42px);
  --body: 17px;
  --label: 12px;

  --content-gap: 40px;

  --ease: cubic-bezier(0.22, 0.61, 0.36, 1);
}

::selection {
  background: var(--accent);
  color: var(--bg);
}

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

* { box-sizing: border-box; }

html, body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: 'Optima', 'Optima nova', Candara, 'Gill Sans', 'Segoe UI', sans-serif;
}

/* Keep the scrollbar's width reserved at all times. The lightbox locks the page
   with `overflow: hidden`, which removes the scrollbar and would otherwise hand
   its width (~15px) back to the layout — the gallery visibly jumped sideways as
   a photo opened. This also steadies the switch between the short Home view and
   the scrolling gallery. */
html { scrollbar-gutter: stable; }

/* Nav column on the left, content on the right, separated by a rule. */
.page {
  display: grid;
  /* `auto`: the column is as wide as the logo, nothing more. */
  grid-template-columns: auto minmax(0, 1fr);
  gap: 0 var(--content-gap);
  /* So the rule reaches the bottom of the window on a short page. */
  min-height: 100vh;
  padding-left: 0;
  padding-right: var(--page-pad);
}

/* The border is on the column itself so the rule runs the full page height,
   while the logo and nav inside it stay put as the content scrolls. */
.sidebar {
  position: relative;
  /* The fade is capped at one viewport: on a long page (the gallery) a gradient
     sized to the element would stretch until it was invisible. Below the fold
     the base colour takes over, so the panel never dissolves into the page. */
  background-color: var(--nav-base);
  background-image: linear-gradient(180deg, var(--nav-top) 0%, var(--nav-base) 100%);
  background-repeat: no-repeat;
  background-size: 100% 100vh;
  color: var(--nav-ink);
  border-right: 0;
  padding: 12px 20px;
}

/* The rule fades out downwards like the panel, so it is a gradient too rather
   than a border that would stay at full strength to the bottom. */
.sidebar::before {
  content: '';
  position: absolute;
  top: 0;
  right: 0;
  width: 1px;
  height: 100%;
  background: linear-gradient(180deg, rgba(196, 112, 58, 0.7), rgba(196, 112, 58, 0.4));
}

/* Sits exactly on the rule and fills it as the page scrolls. */
.sidebar__progress {
  position: absolute;
  top: 0;
  right: 0;
  width: 1px;
  height: 100%;
  background: var(--accent);
  transform: scaleY(0);
  transform-origin: top;
}

.sidebar__inner {
  position: sticky;
  top: 12px;
}

.sidebar__brand {
  display: block;
  margin-bottom: 24px;
}

/* This width is what sets the column width. */
.sidebar__logo {
  width: 300px;
  max-width: 100%;
  height: auto;
  display: block;
}

.content { min-width: 0; padding-top: 24px; }

/* ---------- Menu ---------- */

.menu__list {
  display: flex;
  flex-direction: column;
  /* Baseline: an SVG's baseline is its bottom edge, so the glyph sits on the
     same line as the letters instead of hanging below it. */
  align-items: baseline;
  gap: 28px;
  margin: 0;
  padding: 0;
  list-style: none;
}

/* Inline-flex so the rule below stops at the word, not at the column edge.
   The nudge offsets the trailing letter-space, which would otherwise leave the
   glyphs looking a hair left of centre. */
.menu__link {
  position: relative;
  display: inline-flex;
  align-items: baseline;
  gap: 0.7em;
  margin-left: 0.11em;
  padding-bottom: 4px;
  color: var(--nav-ink);
  text-decoration: none;
  /* Optima ships a real 550 Medium (the CDN declares 400/550/700), so this is
     an actual cut, not a synthesised bold. */
  font-weight: 550;
  font-size: 18px;
  /* Wide tracking on small caps costs word cohesion; 0.22em was set when the
     nav was a horizontal bar and had room to spare. */
  letter-spacing: 0.14em;
  text-transform: uppercase;
  transition: color 200ms var(--ease);
}

/* Small, set back — an index, not a label. */
.menu__index {
  font-size: 10px;
  letter-spacing: 0.1em;
  color: rgba(196, 112, 58, 0.95);
  transition: color 200ms var(--ease);
}

/* Grows from the left rather than appearing all at once. */
.menu__link::after {
  content: '';
  position: absolute;
  left: 0;
  bottom: 0;
  width: 100%;
  height: 1px;
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 260ms var(--ease);
}

.menu__link:hover,
.menu__link.is-active { color: var(--accent); }

.menu__link:hover::after,
.menu__link.is-active::after { transform: scaleX(1); }

/* ---------- Category list under Galery ----------
   Shown only while Galery is the active view, driven entirely off
   `.menu__link.is-active` — the list is a sibling of the link, so no JS state.

   The wrapper animates `grid-template-rows: 0fr -> 1fr`: transitioning to an
   auto height is otherwise impossible, and a max-height guess would either clip
   the list or ease against a height the content never reaches. */
.submenu {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 340ms var(--ease);
}

.menu__link.is-active + .submenu { grid-template-rows: 1fr; }

.submenu__list {
  overflow: hidden; /* what the collapsing row actually clips */
  list-style: none;
  margin: 0;
  /* Indented past the 01/02 index so the titles hang under the menu label.
     No vertical padding: this is the box the 0fr row collapses, and its own
     padding is a floor it cannot shrink past — the list stayed 14px tall when
     closed. The gap above the first title lives on that title instead. */
  padding: 0 0 0 2.5em;
  /* A grid item's automatic minimum size also stops 0fr reaching zero. */
  min-height: 0;
  /* Collapsed links must leave the tab order, and stay visible long enough to
     animate out — hence the delay on the way closed only. */
  visibility: hidden;
  transition: visibility 0s 340ms;
}

.submenu__list li:first-child .submenu__link { padding-top: 14px; }

.menu__link.is-active + .submenu .submenu__list {
  visibility: visible;
  transition: visibility 0s;
}

.submenu__link {
  display: flex;
  align-items: center;
  gap: 0.7em;
  padding: 5px 0;
  color: var(--nav-ink);
  text-decoration: none;
  font-size: 11px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  opacity: 0;
  transform: translateY(-4px);
  transition: opacity 240ms var(--ease), transform 240ms var(--ease), color 200ms var(--ease);
}

/* Staggered off the index set by script.js, so the list unrolls rather than
   appearing all at once. The offset lets the row animation get underway first. */
.menu__link.is-active + .submenu .submenu__link {
  opacity: 1;
  transform: none;
  transition-delay: calc(var(--i, 0) * 45ms + 90ms);
}

.submenu__link:hover { color: var(--accent-text); }

/* The category's own colour, the same one used for its heading rule and its
   lightbox — it makes the list scannable by colour as well as by name. */
.submenu__link::before {
  content: '';
  flex: none;
  width: 7px;
  height: 7px;
  background: var(--category-color, var(--accent));
}

/* ---------- Views ---------- */

/* .gallery sets display: block, which would beat the UA rule for [hidden]. */
.view[hidden] { display: none !important; }

/* ---------- Home ---------- */

.home,
.poker,
.contact {
  font-size: var(--body);
  line-height: 1.5;
  padding-bottom: 48px;
}

/* One screen, no scroll. Grid rather than flex: the `auto` row always gets the
   band's content height and `minmax(0, 1fr)` lets the hero row take exactly
   what is left — with flex, a late font swap changed the text height and the
   band pushed past the viewport instead of the hero giving way.
   The 24px is .content's top padding, which the hero pulls back over. */
.home {
  display: grid;
  /* Hero first at a fixed share, band takes the rest — the other way round it
     grew to fill the screen and flattened everything below it. */
  grid-template-rows: auto minmax(0, 1fr);
  height: calc(100vh - 24px);
  padding-bottom: 0;
}

.home p,
.poker p,
.contact p { margin: 0 0 1.2em; }

/* `.home p` above is more specific than a bare class, hence `.home` here too. */
.home .home__lead {
  font-size: var(--display);
  line-height: 1.1;
  letter-spacing: -0.015em;
  margin: 0 0 1.4em;
}

/* "Here is" / "or" are joins, not names: small caps, set back, so the eye lands
   on Guillaume, Chapeau, Indiana Jones, The French Cowboy. */
.lead__link {
  font-size: 0.3em;
  font-weight: 550;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  opacity: 0.72;
  /* Holds the small type off the name without touching the line box. */
  margin-right: 0.5em;
}

/* At display size the emoji outweighs the type and pushes itself onto its own
   line; smaller, and glued to the word with a non-breaking space, it stays put. */
.home__emoji { font-size: 0.5em; }

/* Section titles: same treatment as the menu, in the accent, over a hairline
   that echoes the rule beside the nav. */
.home .home__label {
  position: relative;
  color: var(--accent-text);
  font-size: 13px;
  font-weight: 550;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  margin: 0 0 0.8em;
  padding-top: 1.5em;
}

.home .home__label::before {
  content: '';
  position: absolute;
  top: 0;
  left: 50%;
  transform: translateX(-50%);
  width: 28px;
  height: 1px;
  background: rgba(196, 112, 58, 0.55);
}

.home__list {
  list-style: none;
  margin: 0;
  padding: 0;
}

.home__list li { margin: 0; }

/* Text block between the two photos. */
.block {
  padding-top: 4px;
  text-align: center;
}

/* The two opening groups run inline rather than stacked: four short adjectives
   in a column filled a third of the width and left the rest empty. Set on a
   line, they fill the measure and read as a signature. */
.block > .home__list:first-child,
.block > .home__list:first-child + .home__list {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: center;
  gap: 0.1em 0.65em;
}

.block > .home__list:first-child li:not(:last-child)::after,
.block > .home__list:first-child + .home__list li:not(:last-child)::after {
  content: '·';
  margin-left: 0.65em;
  color: var(--accent);
}

.block > .home__list:first-child {
  font-size: 1.3em;
  line-height: 1.4;
  letter-spacing: 0.01em;
}

/* The second group is a quieter aside: same size as the body, set back, and
   separated by the same hairline used above the labels. */
.block > .home__list:first-child + .home__list {
  position: relative;
  color: color-mix(in srgb, var(--ink) 78%, transparent);
  padding-top: 1.5em;
}

.block > .home__list:first-child + .home__list::before {
  content: '';
  position: absolute;
  top: 0;
  left: 50%;
  transform: translateX(-50%);
  width: 28px;
  height: 1px;
  background: rgba(196, 112, 58, 0.55);
}

/* In the two labelled columns each entry gets its own line rule, so they read
   as a list of offers rather than a paragraph broken into pieces. */
.pair .home__list li {
  padding: 0.45em 0;
  border-bottom: 1px solid rgba(196, 112, 58, 0.22);
}

.pair .home__list li:last-child { border-bottom: 0; }

/* Instagram link under the two columns. The glyph is inline SVG on
   `currentColor`, so it takes the link's colour and its hover with it. */
.follow {
  display: inline-flex;
  align-items: center;
  gap: 0.55em;
  margin-top: 1.5em;
  color: var(--accent-text);
  text-decoration: none;
  font-size: 13px;
  font-weight: 550;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  transition: color 200ms var(--ease);
}

.follow:hover { color: var(--accent); }

/* An SVG baseline is its bottom edge, so it sits on the text baseline and its
   extra height goes above the cap line, where the eye expects it. */
/* Sized and shifted in `em`, so both track the label if its size changes.
   The lift puts the glyph's foot on the same line as the letters' feet: at this
   size its stroke reaches the edge of its box, so neither `baseline` nor
   `center` lands it there on its own. */
.follow__icon {
  flex: none;
  width: 1.6em;
  height: 1.6em;
  transform: translateY(-0.46em);
}

/* Names the link for a screen reader without showing the word twice. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* Consecutive lists need a break, or the seven traits read as one list. */
.block .home__list + .home__list { margin-top: 1.1em; }

/* "I cherish" and "What I can do for you": two columns, not a stack. */
.pair {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 32px;
  margin-top: 2.2em;
}

/* The hairline above each label already spaces the columns. */
.pair .home__label { margin-top: 0; }

/* Hero. Bleeds off the right edge of the page, and the intro sits in the photo
   itself — the left two thirds of this frame are empty sky. */
/* Full bleed: up over the content's top padding, left across the nav gutter to
   the rule itself, right past the page padding to the window edge. Shared by
   the Home hero and the Contact banner so the two cannot drift apart. */
.banner {
  position: relative;
  margin: -24px calc(-1 * var(--page-pad)) 0 calc(-1 * var(--content-gap));
  /* Shared, so the two banners cannot end up at different heights. */
  height: min(40vh, 1100px);
  overflow: hidden;
}

.banner__img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: saturate(0.92) contrast(1.03);
  display: block;
}

/* ---------- Contact: two columns ---------- */

/* One screen, no scrolling: the section is exactly the window height and the
   photo column stretches to fill it. */
/* No `overflow: hidden` here: the grid below bleeds out of this box with
   negative margins, and clipping it cut exactly that bleed off — the photo
   started painting at the padding edge while its box still measured as flush.
   Scrolling is prevented by the fixed height, and .contact__col handles its
   own overflow. */
.contact {
  height: calc(100vh - 24px);
  padding-bottom: 0;
}

.contact__grid {
  display: grid;
  /* `auto` for the photo: it is as wide as the whole photo is at full height,
     so nothing is cropped and no empty band appears beside it. The form takes
     whatever is left. */
  grid-template-columns: auto minmax(0, 1fr);
  height: 100%;
  /* Bleeds the same three ways as the banners: up over the content's top
     padding, left to the nav rule, right to the window edge. */
  margin: -24px calc(-1 * var(--page-pad)) 0 calc(-1 * var(--content-gap));
}

/* Photo first: hard against the nav rule and the top of the window, no padding
   of its own. */
.contact__photo {
  position: relative;
  margin: 0;
  height: 100%;
  overflow: hidden;
}

/* Height drives the size and the width follows, so the whole frame is visible.
   `object-fit` is irrelevant here — the box already has the photo's ratio — and
   the mirror makes him face into the page rather than out of it. */
.contact__photo .banner__img {
  width: auto;
  height: 100%;
  transform: scaleX(-1);
}

/* The form sits to its right; the inset is padding on this column, so the photo
   edge stays clean. */
.contact__col {
  display: flex;
  flex-direction: column;
  justify-content: center;
  min-height: 0;
  padding: 24px 48px;
  overflow-y: auto;
}

/* Everything geometric now comes from .banner; .hero adds only the text over it. */
.hero { position: relative; }

/* Keeps the silhouette and the horizon in frame through the crop. */
.hero .banner__img { object-position: center 58%; }

/* A scrim, so the intro holds its contrast whatever the sky does behind it. */
.hero::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(100deg, rgba(43, 8, 15, 0.5) 0%, rgba(43, 8, 15, 0.12) 45%, transparent 70%);
  pointer-events: none;
}

.hero__text {
  position: absolute;
  z-index: 1;
  top: clamp(20px, 5vh, 56px);
  left: clamp(20px, 3.5vw, 56px);
  /* An explicit width, not max-width: `container-type: inline-size` stops the
     box from sizing to its contents, so a shrink-to-fit box collapses to one
     word per line. A definite width gives the container query something real
     to resolve against. */
  width: min(52%, 620px);
  container-type: inline-size;
}

.hero .home__lead {
  color: var(--bg);
  margin: 0;
}

/* Band: a photo either side of the text, bleeding like the hero above it —
   left photo against the nav rule, right photo to the window edge. The text
   column takes the width the bleed frees up, enough for two sub-columns. */
.band {
  display: grid;
  grid-template-columns: 1fr 1.35fr 1fr;
  gap: 44px;
  /* Stretch, so the photos can take the band's full height; the text stays put
     at the top. */
  align-items: stretch;
  min-height: 0;
  margin: 0 calc(-1 * var(--page-pad)) 0 calc(-1 * var(--content-gap));
}

.band .block { align-self: center; }

.shot {
  position: relative;
  margin: 0;
  min-width: 0;
  overflow: hidden;
}

/* The three sources are a green forest, red rock and blue dusk. A shared grade
   plus a thin wash of the page accent makes them read as one series. The wash
   is the palette's own colour, only blended. */
.shot::after {
  content: '';
  position: absolute;
  inset: 0;
  background: var(--accent);
  mix-blend-mode: soft-light;
  opacity: 0.18;
  pointer-events: none;
}

.shot__img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: saturate(0.92) contrast(1.03);
  transition: transform 700ms var(--ease);
}

.shot:hover .shot__img { transform: scale(1.03); }

/* 3:4 rather than the sources' 2:3 — a full portrait beside a text column this
   short would tower over it. The cap stops the band from outgrowing the hero on
   a wide screen, where a ratio-driven height would just keep climbing; past it
   the frame simply crops a little more. */
/* Full column width — that is what keeps the left one against the nav and the
   right one against the window edge. Height comes from the band, not from a
   ratio: an `aspect-ratio` plus a height cap shrinks the width too, and the
   photo drifts away from the edge. */
.shot--left,
.shot--right {
  height: 100%;
  max-height: 60vh;
}

/* Both flush against the hero above them. */
.shot--right { margin-top: 0; }

/* ---------- Contact form ---------- */

/* Centred in its column rather than on the page, now that the column is half
   the width. No bottom padding: the column centres it vertically instead. */
.form {
  width: 100%;
  max-width: 460px;
  margin: 0 auto;
}

.field { margin-top: 20px; }
.field:first-child { margin-top: 0; }

.field__label {
  display: block;
  margin-bottom: 8px;
  color: var(--accent-text);
  font-size: 13px;
  font-weight: 550;
  letter-spacing: 0.16em;
  text-transform: uppercase;
}

/* Ruled rather than boxed, to sit with the rest of the page. */
/* Filled rather than open: the rule underneath stays, it is the page's idiom. */
.field__input {
  width: 100%;
  padding: 11px 13px;
  border: 0;
  border-bottom: 1px solid rgba(196, 112, 58, 0.45);
  background: var(--field-bg);
  color: var(--ink);
  font: inherit;
  transition: border-color 200ms var(--ease), box-shadow 200ms var(--ease);
}

.field__input::placeholder { color: color-mix(in srgb, var(--ink) 40%, transparent); }

.field__input:focus {
  outline: 0;
  border-bottom-color: var(--accent);
  /* Inset rather than an outline, so the field lights up without shifting. */
  box-shadow: inset 0 0 0 1px rgba(196, 112, 58, 0.28);
}

/* Sized in `vh` so four fields plus the button still clear a 768px screen
   without the section scrolling. */
.field__input--area {
  resize: vertical;
  height: clamp(90px, 18vh, 190px);
  line-height: 1.5;
}

/* Only shown once the address is both non-empty and malformed, so it cannot
   scold someone who has not finished typing. */
.field__hint {
  margin: 6px 0 0;
  font-size: 12px;
  letter-spacing: 0.04em;
  color: var(--accent-text);
}

.field__hint[hidden] { display: none; }

.field__input[aria-invalid='true'] { border-bottom-color: var(--accent-text); }

.field__count {
  margin: 6px 0 0;
  text-align: right;
  font-size: 12px;
  letter-spacing: 0.08em;
  color: color-mix(in srgb, var(--ink) 55%, transparent);
}

/* Turns accent when the message runs close to the 1000-character limit. */
.field__count.is-near { color: var(--accent-text); }

.button {
  margin-top: 32px;
  padding: 12px 30px;
  border: 1px solid var(--accent);
  background: none;
  color: var(--accent-text);
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  cursor: pointer;
  transition: background-color 200ms var(--ease), color 200ms var(--ease), opacity 200ms var(--ease);
}

.button:hover:not(:disabled) {
  background: var(--accent);
  color: var(--bg);
}

/* Disabled until all four fields are valid. */
.button:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

/* The way out if someone would rather use their own mail client.
   `.contact` in the selector because `.contact p` above is more specific than a
   bare class and would otherwise win, zeroing this margin. */
.contact .form__direct {
  margin: 36px 0 0;
  font-size: 14px;
  line-height: 1.5;
  color: color-mix(in srgb, var(--ink) 72%, transparent);
}

.form__mail {
  color: var(--accent-text);
  text-decoration: none;
  border-bottom: 1px solid rgba(196, 112, 58, 0.45);
  transition: color 200ms var(--ease), border-color 200ms var(--ease);
}

.form__mail:hover {
  color: var(--accent);
  border-bottom-color: var(--accent);
}

/* Only shown if the send fails; the form stays filled in behind it. */
.form__error {
  margin: 16px 0 0;
  font-size: 13px;
  line-height: 1.5;
  color: var(--accent-text);
}

.form__error[hidden] { display: none; }

.form__thanks {
  width: 100%;
  max-width: 460px;
  margin: 0 auto;
  font-size: 1.3em;
}

.form__thanks[hidden] { display: none; }

/* ---------- Gallery ---------- */

/* The view is a plain column of categories; each category owns its own 12-column
   grid, so the layout engine runs per category instead of once for everything. */
.gallery {
  display: block;
  padding-bottom: 80px;
}

.category + .category { margin-top: 72px; }

/* The nav's category list scrolls these to the top of the viewport; without a
   margin the heading would sit flush against the edge. */
.category { scroll-margin-top: 24px; }

/* The category colour, set inline by script.js from catalog.json, shows here as
   a rule under the heading and fills the lightbox when a photo is opened. */
.category__title {
  margin: 0 0 22px;
  padding-bottom: 14px;
  border-bottom: 2px solid var(--category-color, var(--accent));
  color: var(--ink);
  /* vw, not cqw: container-type is only set on .hero__text, so a container
     query unit here would resolve against the viewport anyway. */
  font-size: clamp(21px, 2.3vw, 30px);
  font-weight: 400;
  /* Wide tracking reads as a label at 12px but as a gap at 30px. */
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

.category__grid {
  display: grid;
  /* minmax(0, 1fr): plain 1fr can't shrink below the images' min-content width,
     which overflows the page on narrow screens. */
  grid-template-columns: repeat(12, minmax(0, 1fr));
  grid-auto-rows: var(--row-unit);
  gap: var(--gap);
}

/* Centred in the viewport, not just in the flow: with nothing else in the view
   the gallery has no height of its own to centre against. The sidebar's own
   padding is already outside .content, so 100vh minus its top padding lands the
   text on the optical centre of the screen. */
.gallery__empty,
.gallery__error {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: calc(100vh - 24px - 80px);
  margin: 0;
  padding: 0 24px;
  color: var(--ink);
  font-size: var(--body);
  text-align: center;
}

.gallery__empty {
  letter-spacing: 0.06em;
  opacity: 0.85;
}

/* Column and row spans are set by script.js — an aspect-ratio here would fight
   the definite row height and shrink the photo's width, opening gaps. */
.photo {
  margin: 0; /* figure carries a UA margin that would open gutters */
  min-width: 0; /* let the frame follow the grid, not the image's intrinsic size */
  min-height: 0;
}

.photo__frame {
  position: relative;
  height: 100%;
  overflow: hidden;
  background: rgba(91, 18, 32, 0.06);
}

.photo__img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transition: transform 500ms ease;
}

/* Safety net for the crop that survives capBand. A centred crop on a standing
   figure takes the same amount off the head and the feet, and the head is what
   matters — so bias the frame upward. Portrait only: landscape cells keep ~99%
   of the photo, so re-centring them would move the subject for no reason. */
.photo[data-format="portrait"] .photo__img { object-position: 50% 30%; }

.photo:hover .photo__img { transform: scale(1.03); }

/* Revealed on hover: a gradient rather than a flat band, and the words rise
   into place. Focus counts too, so the caption is reachable by keyboard. */
.photo__title {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  margin: 0;
  padding: 34px 18px 14px;
  background: linear-gradient(to top, rgba(43, 8, 15, 0.62), rgba(43, 8, 15, 0));
  color: #fff;
  font-size: var(--label);
  font-weight: 400;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  opacity: 0;
  transform: translateY(6px);
  transition: opacity 300ms var(--ease), transform 300ms var(--ease);
}

.photo:hover .photo__title,
.photo:focus-within .photo__title {
  opacity: 1;
  transform: none;
}

/* --- Reveal on scroll (script.js adds .is-in) ------------------------------ */

.reveal {
  opacity: 0;
  transform: translateY(16px);
  transition: opacity 700ms var(--ease), transform 700ms var(--ease);
  transition-delay: var(--reveal-delay, 0ms);
}

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

/* Photos fade in as they decode, so they stop popping into place. */
.shot__img:not(.is-loaded),
.photo__img:not(.is-loaded) { opacity: 0; }

.shot__img,
.photo__img { transition: opacity 600ms var(--ease), transform 700ms var(--ease); }

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

  .shot__img,
  .photo__img,
  .photo__title,
  .menu__link::after { transition: none; }

  /* The category list still opens and closes, it just snaps. The 0s visibility
     delay has to go too, or the closed list would linger in the tab order. */
  .submenu,
  .submenu__link,
  .submenu__list { transition: none; }

  .menu__link.is-active + .submenu .submenu__link { transition-delay: 0s; }

  .shot__img:not(.is-loaded),
  .photo__img:not(.is-loaded) { opacity: 1; }
}

/* ---------- Responsive ---------- */

/* Short screens: one step of tightening, on the spacing rather than the type,
   so the block still fits under a hero fixed at 40% of the height. */
@media (max-height: 820px) {
  .home { --body: 16px; }
  .home .pair { margin-top: 1em; }
  .home .pair .home__list li { padding: 0.12em 0; }
  .home .follow { margin-top: 1.1em; }
  .home .home__label { padding-top: 1em; }
}

/* 720p, the smallest desktop worth targeting. */
@media (max-height: 660px) {
  .home .pair { margin-top: 0.5em; }
  .home .block > .home__list:first-child + .home__list { padding-top: 1em; }
  .home .pair .home__list li { padding: 0.08em 0; }
  .home .follow { margin-top: 0.6em; }
  .home .home__label { padding-top: 0.8em; }
}

/* Below this the two sub-columns are too narrow for "Be The future John Wayne"
   and it wraps, so they stack instead. */
@media (max-width: 1200px) {
  .pair {
    grid-template-columns: minmax(0, 1fr);
    gap: 0;
  }

  .pair__col + .pair__col { margin-top: 2.2em; }
}

@media (max-width: 900px) {
  .page {
    grid-template-columns: minmax(0, 1fr);
    gap: 0;
  }

  .sidebar {
    border-right: 0;
    border-bottom: 1px solid rgba(196, 112, 58, 0.45);
    padding: 16px 0;
    background-size: 100% 100%;
  }

  .sidebar::before { display: none; }

  .sidebar__inner {
    position: static;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 24px;
  }

  .sidebar__brand { margin: 0; }
  .sidebar__logo { width: 160px; }

  .menu__list {
    flex-direction: row;
    flex-wrap: wrap;
    gap: 24px;
  }

  /* Hero: shorter, and the intro gets the full width of it. */
  .hero {
    height: 56vh;
    margin: 0 0 40px;
  }

  .hero__text { max-width: 82%; }

  /* One column: photo, text, photo. */
  .band {
    grid-template-columns: minmax(0, 1fr);
    gap: 32px;
  }

  .shot--left,
  .shot--right { aspect-ratio: auto; }

  .shot--right { margin-top: 0; }

  .shot__img { height: auto; }
}

@media (max-width: 560px) {
  :root { --page-pad: 20px; }

  .hero { height: 62vh; }
  .hero__text { max-width: 100%; }
}

/* ---------- Lightbox ---------- */

.lightbox {
  position: fixed;
  inset: 0;
  z-index: 10;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 18px;
  padding: 5vmin;
  /* script.js sets --lightbox-bg to the category colour, already carrying the
     alpha, so the gallery stays faintly visible behind the open photo. */
  background: var(--lightbox-bg, rgba(43, 8, 15, 0.92));
  cursor: zoom-out;
  opacity: 0;
  transition: opacity 320ms var(--ease);
}

/* .is-open is added a frame after [hidden] is dropped — a display: none box
   cannot transition, so the class has to land on an already-painted element. */
.lightbox.is-open { opacity: 1; }

.lightbox[hidden] { display: none; }

/* No resting transform here: script.js drives it, starting the photo at the
   clicked thumbnail's box and animating to identity (and back on close). A
   declared scale would fight the inline one and jump on the first frame. */
.lightbox__img {
  max-width: 100%;
  max-height: 84vh;
  object-fit: contain;
  display: block;
  transition: transform 420ms var(--ease);
}

@media (prefers-reduced-motion: reduce) {
  .lightbox,
  .lightbox__img {
    transition: none;
  }

  .lightbox__img { transform: none; }
}

.lightbox__caption {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 7px;
  max-width: 100%;
}

.lightbox__title {
  margin: 0;
  color: var(--lightbox-ink, var(--bg));
  font-size: var(--label);
  letter-spacing: 0.2em;
  text-transform: uppercase;
}

/* The photo's own title, or its filename when it has none. Set as data rather
   than as a label: a filename carries its own case and underscores, so
   uppercasing or tracking it out only makes it harder to read. */
.lightbox__name {
  margin: 0;
  color: var(--lightbox-ink, var(--bg));
  font-size: 11px;
  letter-spacing: 0.02em;
  opacity: 0.7;
  /* A long key must wrap inside the caption instead of widening the overlay. */
  overflow-wrap: anywhere;
  text-align: center;
}

/* An attribution names a person, so it has to be legible: it is set apart from
   the title by case and tracking, not by being faded out. At the old 0.55 opacity
   it sat near 2:1 against the translucent backdrop — invisible in practice, and
   under any readable contrast threshold. */
.lightbox__credit {
  margin: 0;
  color: var(--lightbox-ink, var(--bg));
  font-size: 11px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  text-align: center;
  overflow-wrap: anywhere;
}

.lightbox__credit[hidden] { display: none; }

.lightbox__creditLink {
  color: inherit;
  text-decoration-line: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 4px;
  text-decoration-color: color-mix(in srgb, currentColor 45%, transparent);
  /* The overlay is zoom-out everywhere else; this is the one thing to click. */
  cursor: pointer;
  transition: text-decoration-color 200ms var(--ease);
}

.lightbox__creditLink:hover { text-decoration-color: currentColor; }

.lightbox__close {
  position: absolute;
  top: 3vmin;
  right: 3vmin;
  width: 44px;
  height: 44px;
  border: 0;
  background: none;
  color: var(--lightbox-ink, var(--bg));
  font-size: 30px;
  line-height: 1;
  cursor: pointer;
}

.photo { cursor: zoom-in; }
