/* ==========================================================================
   Oscar Leo Photography
   Hand-written CSS. No build step, no framework, no runtime dependency.

   Contents
   01 Tokens (light + dark)
   02 Reset & base
   03 Typography
   04 Layout primitives
   05 Header & navigation
   06 Buttons & links
   07 Hero
   08 Editorial blocks
   09 Service index (hover-preview list)
   10 Galleries
   11 Pricing
   12 FAQ
   13 Footer
   14 Lightbox
   15 Motion
   16 Reduced motion & print
   ========================================================================== */

/* ---------- 01 TOKENS ----------------------------------------------------
   Warm neutrals rather than cold zinc: the photography runs olive, sage and
   warm skin tones, and a blue-grey shell visibly fights it. Contrast ratios
   are held at or above the cold palette's.
   ------------------------------------------------------------------------- */

:root {
  /* Read off the work itself: high-contrast monochrome stage photography and
     low-key studio portraiture on warm olive/brass backdrops. Nothing
     saturated — the photographs carry all the colour, and the shell is warm
     paper, warm ink, and brass held back for lines. */
  --bg:            #FAF8F5;   /* paper-100 — warm off-white, not paper-white */
  --bg-elevated:   #FFFFFF;   /* paper-000 */
  --bg-sunken:     #F2EFE9;   /* paper-200 — a step down, still warm */
  --fg:            #0A0A0A;   /* ink-900   18.7:1 on --bg */
  --fg-muted:      #4A4744;   /* ink-500    8.7:1 on --bg */
  --fg-subtle:     #6D6862;   /* ink-400    5.2:1 on --bg, 4.8:1 on the sunken panel */
  --border:        rgba(10, 10, 10, 0.12);   /* hairline rules */
  --border-strong: rgba(10, 10, 10, 0.28);
  /* Real control boundaries need 3:1 (WCAG 1.4.11). The system's own
     --field-border (ink at .22) measures 1.9:1 against paper, so brass carries
     the edge instead — 4.0:1 on paper, 3.7:1 on the sunken panel — which is
     also the colour it uses for focus rings. */
  --border-interactive: #8A7A55;
  --accent:        #0A0A0A;   /* ink still fills; brass is a line colour, never a fill */
  --accent-hover:  #2E2C29;   /* ink-600 */
  --on-accent:     #FAF8F5;

  /* Brass — the single accent, lifted from the studio backdrop wash.
     Rules, focus rings and the occasional underline. It never fills. */
  --brass:         #8A7A55;   /* brass-500 */
  --critical:      #8C2F1E;   /* rust-600 — form errors. Warm, so it belongs to
                                 this palette rather than reading as a browser
                                 default red. 7.4:1 on --bg. */
  --brass-ink:     #6D5F3D;   /* brass-700 — 5.9:1, safe as text on paper */
  --brass-light:   #B7A882;   /* brass-300 — 8.4:1 on ink, for dark bands */
  --overlay:       rgba(10, 10, 10, 0.72);
  --shadow-sm: none;
  --shadow-lg: none;

  /* Two voices. A high-contrast serif carries the statements and Archivo
     carries everything that has to be read or clicked.

     The system named Bodoni Moda, a true didone. On screen its hairlines thin
     out to roughly a pixel and the headings became hard to read, so the display
     face is Playfair Display instead: the same didone lineage and the same
     editorial feel, built with thick enough thins to survive at every size. */
  --font-display: "Playfair Display", Georgia, "Times New Roman", serif;
  --font-body: "Archivo", ui-sans-serif, -apple-system, BlinkMacSystemFont, "Helvetica Neue", Arial, sans-serif;
  --font-ui: "Archivo", ui-sans-serif, -apple-system, BlinkMacSystemFont, "Helvetica Neue", Arial, sans-serif;

  /* Fluid type scale — clamp() so it never needs a breakpoint */
  --step--1: clamp(0.80rem, 0.78rem + 0.12vw, 0.875rem);
  --step-0:  clamp(0.97rem, 0.94rem + 0.16vw, 1.06rem);
  --step-1:  clamp(1.15rem, 1.08rem + 0.36vw, 1.38rem);
  --step-2:  clamp(1.40rem, 1.24rem + 0.80vw, 1.95rem);
  --step-3:  clamp(1.85rem, 1.45rem + 1.90vw, 3.25rem);
  --step-4:  clamp(2.40rem, 1.70rem + 3.40vw, 4.80rem);
  --step-5:  clamp(2.90rem, 1.60rem + 6.20vw, 7.50rem);

  /* Spacing — minimalism is mostly whitespace, so the scale runs generous */
  --space-3xs: 0.25rem;
  --space-2xs: 0.5rem;
  --space-xs:  0.75rem;
  --space-s:   1rem;
  --space-m:   1.5rem;
  --space-l:   2.5rem;
  --space-xl:  4.5rem;
  --space-2xl: 7rem;
  --space-3xl: clamp(6rem, 3.5rem + 10vw, 12rem);

  --measure: 66ch;
  --container: 88rem;
  --container-narrow: 44rem;
  --radius: 0px;               /* square corners read cleaner than soft ones */

  /* Motion */
  --ease-out: cubic-bezier(.22, .61, .36, 1);
  --ease-in-out: cubic-bezier(.65, .05, .36, 1);
  --dur-fast: 160ms;
  --dur: 280ms;
  --dur-slow: 620ms;
  --dur-image: 900ms;

  --header-h: 6.1875rem;  /* 4.5rem base, +25%, then +10% thicker again */
  --header-h-sm: 4.8125rem;  /* the condensed bar, once scrolled */
  --logo-fill: 0.9;          /* how much of the bar the mark occupies */
  color-scheme: light;
}

/* ---------- 02 RESET & BASE ---------------------------------------------- */

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  scroll-padding-top: calc(var(--header-h) + 1rem);
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--fg);
  font-family: var(--font-body);
  font-size: var(--step-0);
  line-height: 1.6;
  font-synthesis-weight: none;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-x: hidden;
  transition: background-color var(--dur) var(--ease-out), color var(--dur) var(--ease-out);
}

img, picture, video, canvas, svg { display: block; max-width: 100%; }
img { height: auto; background-color: var(--bg-sunken); }
input, button, textarea, select { font: inherit; color: inherit; }
p, h1, h2, h3, h4 { overflow-wrap: break-word; }

/* Visible, high-contrast focus for keyboard users — never removed. */
/* Brass carries the focus ring, per the palette. It measures 4.0:1 against
   paper, above the 3:1 that WCAG 1.4.11 asks of a non-text indicator. */
:focus-visible {
  outline: 2px solid var(--brass);
  outline-offset: 3px;
  border-radius: var(--radius);
}
:focus:not(:focus-visible) { outline: none; }

.skip-link {
  position: absolute;
  left: var(--space-s);
  top: var(--space-s);
  z-index: 200;
  padding: var(--space-2xs) var(--space-s);
  background: var(--accent);
  color: var(--on-accent);
  font-family: var(--font-ui);
  font-size: var(--step--1);
  text-decoration: none;
  transform: translateY(-160%);
  transition: transform var(--dur) var(--ease-out);
}
.skip-link:focus { transform: translateY(0); }

.visually-hidden {
  position: absolute !important;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
}

/* ---------- 03 TYPOGRAPHY ------------------------------------------------ */

/* The serif carries the statements — h1 and h2 — tracked lightly (-0.02em,
   where the old grotesque wanted -0.045em; a high-contrast serif comes apart
   when it is tracked in that hard).

   Leading is 1.02 rather than the system's 0.98: Playfair has longer
   descenders and a taller x-height than the didone that number was written
   for, and at 0.98 a "g" on one line touches the "l" below it.

   h3 and h4 stay in Archivo. The display face is specified for 24px and up,
   and those two land at 18-22px and 17px. */
h1, h2, h3, h4 {
  font-family: var(--font-display);
  font-weight: 400;
  line-height: 1.02;
  letter-spacing: -0.02em;
  margin: 0 0 var(--space-m);
  text-wrap: balance;
}

h1 { font-size: var(--step-5); }
h2 { font-size: var(--step-3); }
h3 {
  font-family: var(--font-ui);
  font-size: var(--step-1);
  font-weight: 500;
  line-height: 1.2;
  letter-spacing: -0.02em;
}
h4 {
  font-family: var(--font-ui);
  font-size: var(--step-0);
  font-weight: 600;
  line-height: 1.3;
  letter-spacing: -0.01em;
}

p { margin: 0 0 var(--space-m); max-width: var(--measure); }
p:last-child { margin-bottom: 0; }

.eyebrow {
  font-family: var(--font-ui);
  font-size: var(--step--1);
  font-weight: 500;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--fg-subtle);
  margin: 0 0 var(--space-m);
  display: block;
}

/* The direct-answer paragraph at the top of every page: the passage Google
   snippets and AI answer engines lift verbatim. Deliberately prominent. */
.lead {
  font-size: var(--step-1);
  line-height: 1.5;
  letter-spacing: -0.012em;
  color: var(--fg);
  max-width: 56ch;
}

.prose > * + * { margin-top: var(--space-m); }
.prose ul, .prose ol { padding-left: 1.25em; max-width: var(--measure); }
.prose li { margin-bottom: var(--space-2xs); }
.prose li::marker { color: var(--fg-subtle); }
.prose strong { font-weight: 600; }
.prose em { color: var(--fg-muted); }

.text-muted { color: var(--fg-muted); }
.fine-print {
  font-family: var(--font-ui);
  font-size: var(--step--1);
  color: var(--fg-muted);
  line-height: 1.6;
}

/* ---------- 04 LAYOUT PRIMITIVES ----------------------------------------- */

.container {
  width: min(100% - 2 * var(--space-m), var(--container));
  margin-inline: auto;
}
@media (min-width: 48em) {
  .container { width: min(100% - 2 * var(--space-xl), var(--container)); }
}

.container-narrow {
  width: min(100% - 2 * var(--space-m), var(--container-narrow));
  margin-inline: auto;
}

.section { padding-block: var(--space-3xl); }
.section--tight { padding-block: var(--space-2xl); }
.section--sunken { background: var(--bg-sunken); }

/* Photographs always sit on the page surface, never on a tinted band: a tint
   behind a gallery colours the space between the frames, which reads as a cast
   over the pictures rather than as a background. The separation comes from the
   *next* section being darker instead. Every gallery is authored that way
   already; this is the guard against one being tinted by accident. */
.section--sunken:has(.gallery) { background: var(--bg); }

.section-head {
  display: grid;
  gap: var(--space-m);
  margin-bottom: var(--space-xl);
}
@media (min-width: 60em) {
  .section-head--split {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: var(--space-2xl);
    align-items: end;
  }
  .section-head--split h2 { margin-bottom: 0; }
}

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

.grid-2 { display: grid; gap: var(--space-l); }
@media (min-width: 48em) { .grid-2 { grid-template-columns: repeat(2, minmax(0,1fr)); } }

.grid-3 { display: grid; gap: var(--space-l); }
@media (min-width: 48em) { .grid-3 { grid-template-columns: repeat(2, minmax(0,1fr)); } }
@media (min-width: 68em) { .grid-3 { grid-template-columns: repeat(3, minmax(0,1fr)); } }

/* ---------- 05 HEADER & NAVIGATION --------------------------------------- */

.header {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 100;
  height: var(--header-h);
  display: flex;
  align-items: center;
  background: color-mix(in srgb, var(--bg) 82%, transparent);
  backdrop-filter: saturate(1.4) blur(14px);
  -webkit-backdrop-filter: saturate(1.4) blur(14px);
  border-bottom: 1px solid transparent;
  transition: height var(--dur) var(--ease-out),
              background-color var(--dur) var(--ease-out),
              border-color var(--dur) var(--ease-out);
}
/* Condenses once the visitor leaves the hero. */
.header[data-scrolled="true"] {
  height: var(--header-h-sm);
  background: color-mix(in srgb, var(--bg) 94%, transparent);
  border-bottom-color: var(--border);
}
@supports not (backdrop-filter: blur(1px)) {
  .header { background: var(--bg); }
}

.header__inner {
  width: min(100% - 2 * var(--space-m), var(--container));
  margin-inline: auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-m);
}
@media (min-width: 48em) {
  .header__inner { width: min(100% - 2 * var(--space-xl), var(--container)); }
}

.brand {
  font-family: var(--font-display);
  font-size: calc(var(--step-0) * 1.08);
  font-weight: 600;
  letter-spacing: -0.02em;
  color: var(--fg);
  text-decoration: none;
  white-space: nowrap;
  flex-shrink: 0;
}
.brand__sub { color: var(--fg-subtle); }

.nav { display: none; }
/* 76em, not 62em: at 25% larger the seven nav labels no longer fit on one
   line below ~1200px and wrapped to two rows. Below this the drawer is used. */
@media (min-width: 76em) {
  .nav {
    display: flex;
    align-items: center;
    gap: calc(var(--space-m) * 1.1);
    /* The actions box is empty on desktop now that the theme toggle is gone,
       so space-between would leave the nav floating short of the edge. */
    margin-left: auto;
  }
}
.nav a {
  font-family: var(--font-ui);
  font-size: calc(var(--step--1) * 1.12);
  letter-spacing: 0.02em;
  color: var(--fg-muted);
  text-decoration: none;
  padding: var(--space-2xs) 0;
  position: relative;
  transition: color var(--dur-fast) var(--ease-out);
}
.nav a::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 1px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: right;
  transition: transform var(--dur) var(--ease-out);
}
.nav a:hover { color: var(--fg); }
.nav a:hover::after { transform: scaleX(1); transform-origin: left; }
.nav a[aria-current="page"] { color: var(--fg); }
.nav a[aria-current="page"]::after { transform: scaleX(1); }

.header__actions { display: flex; align-items: center; gap: var(--space-2xs); }

/* 44x44 minimum touch target on every icon control. */
.icon-btn {
  width: 52px; height: 52px;
  flex: none;            /* never let a wide flex sibling crush the target */
  display: grid; place-items: center;
  background: none;
  border: 1px solid transparent;
  border-radius: var(--radius);
  color: var(--fg-muted);
  cursor: pointer;
  transition: color var(--dur-fast) var(--ease-out),
              border-color var(--dur-fast) var(--ease-out),
              background-color var(--dur-fast) var(--ease-out);
}
.icon-btn:hover { color: var(--fg); border-color: var(--border); }
.icon-btn:active { background: var(--bg-sunken); }
.icon-btn svg { width: 25px; height: 25px; stroke-width: 1.4; }

.nav-toggle { display: grid; }
@media (min-width: 76em) { .nav-toggle { display: none; } }

/* Mobile drawer */
.mobile-nav {
  position: fixed;
  inset: 0;
  z-index: 150;
  background: var(--bg);
  display: grid;
  grid-template-rows: var(--header-h) 1fr;
  opacity: 0;
  visibility: hidden;
  transition: opacity var(--dur) var(--ease-out), visibility var(--dur);
}
.mobile-nav[data-open="true"] { opacity: 1; visibility: visible; }
.mobile-nav:focus { outline: none; }
.mobile-nav__head {
  display: flex; align-items: center; justify-content: space-between;
  width: min(100% - 2 * var(--space-m), var(--container));
  margin-inline: auto;
}
.mobile-nav__list {
  list-style: none;
  margin: 0;
  padding: var(--space-l) 0 var(--space-2xl);
  width: min(100% - 2 * var(--space-m), var(--container));
  margin-inline: auto;
  overflow-y: auto;
}
.mobile-nav__list li {
  border-bottom: 1px solid var(--border);
  opacity: 0;
  transform: translateY(12px);
}
.mobile-nav[data-open="true"] .mobile-nav__list li {
  animation: nav-in var(--dur-slow) var(--ease-out) forwards;
  animation-delay: calc(60ms + var(--i, 0) * 45ms);
}
@keyframes nav-in { to { opacity: 1; transform: none; } }
.mobile-nav__list a {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-s);
  padding: var(--space-s) 0;
  min-height: 44px;
  font-family: var(--font-display);
  font-size: calc(var(--step-2) * 0.85);
  font-weight: 500;
  letter-spacing: -0.03em;
  color: var(--fg);
  text-decoration: none;
}
.mobile-nav__list .price {
  font-family: var(--font-ui);
  font-size: var(--step--1);
  color: var(--fg-subtle);
  letter-spacing: 0.04em;
}

/* ---------- 06 BUTTONS & LINKS ------------------------------------------- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2xs);
  min-height: 48px;
  padding: var(--space-xs) var(--space-l);
  font-family: var(--font-ui);
  font-size: var(--step--1);
  font-weight: 500;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  text-decoration: none;
  border: 1px solid var(--accent);
  border-radius: var(--radius);
  background: var(--accent);
  color: var(--on-accent);
  cursor: pointer;
  transition: background-color var(--dur) var(--ease-out),
              border-color var(--dur) var(--ease-out),
              color var(--dur) var(--ease-out),
              transform var(--dur-fast) var(--ease-out);
}
.btn:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
.btn:active { transform: translateY(1px); }

.btn--ghost {
  background: transparent;
  color: var(--fg);
  border-color: var(--border-interactive);
}
.btn--ghost:hover { background: transparent; border-color: var(--fg); color: var(--fg); }

.btn--on-image {
  background: transparent;
  border-color: rgba(250, 248, 245,.55);
  color: #FAF8F5;
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}
.btn--on-image:hover { background: rgba(250, 248, 245,.14); border-color: #FAF8F5; }

.link-arrow {
  display: inline-flex;
  align-items: center;
  gap: 0.5em;
  font-family: var(--font-ui);
  font-size: var(--step--1);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--fg);
  text-decoration: none;
  padding-block: var(--space-3xs);
}
.link-arrow svg { width: 1em; height: 1em; transition: transform var(--dur) var(--ease-out); }
.link-arrow:hover svg { transform: translateX(4px); }

.prose a, .text-link {
  color: var(--accent);
  text-decoration-thickness: 1px;
  text-underline-offset: 0.2em;
  transition: color var(--dur-fast) var(--ease-out);
}
.prose a:hover, .text-link:hover { color: var(--accent-hover); }

/* ---------- 07 HERO ------------------------------------------------------ */

.hero {
  position: relative;
  min-height: 100svh;
  display: grid;
  align-items: end;
  isolation: isolate;
  overflow: clip;
}
/* Every page opens like the homepage: a full-height photograph. */
.hero--short { min-height: 100svh; }

.hero__media {
  position: absolute;
  inset: 0;
  z-index: -2;
}
.hero__media picture { display: contents; }
.hero__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Tall window: the portrait very nearly fills the box, so this only nudges
     the framing. */
  object-position: center 30%;
}
/* Wide window: the browser has switched to the 16:9 crop, which already has the
   subject placed. Any remaining `cover` trim is taken off the bottom rather
   than the top, so the head is never the thing that goes. */
/* Thresholds mirror the <source> media queries in the markup: a full-height
   hero turns landscape when the window does, a 72svh hero does so earlier. */
@media (min-aspect-ratio: 1/1) {
  .hero:not(.hero--short) .hero__media img { object-position: center 14%; }
}
@media (min-aspect-ratio: 18/25) {
  .hero--short .hero__media img { object-position: center 14%; }
}
/* The BTS hero is an overhead frame of a scene being shot — actors on the
   rug, camera crew ringed around them — and Oscar marked the point he wants
   held in the centre: about two thirds across, just under halfway down,
   where the camera meets the action. `cover` crops around whatever
   object-position names, so pinning it there keeps that point centred from
   a phone to an ultrawide. No -wide- crop is registered for this slug on
   purpose (see build_bts): a baked crop would make this percentage measure
   against the cropped frame instead of the original. */
.hero.hero--bts-focus .hero__media img { object-position: 68% 47%; }
/* The headshots hero mirrors the brand page: subject hard right, the wide
   empty backdrop on the left carrying the headline. The frame is served
   uncropped so that space survives.
   X is 72% — the subject sits at about 66% of the frame, and anchoring past
   him means the window keeps his side of the picture as it narrows, so a
   phone still shows him with backdrop beside him rather than sliding him off
   the edge. Y is 30% because the frame is 1.78:1 against a taller hero box,
   so `cover` trims top and bottom and his head is high in the frame. */
.hero.hero--headshot-focus .hero__media img { object-position: 72% 30%; }
/* The concert hero is nearly square (1320x1292) in a hero box around 1.6:1,
   so `cover` trims roughly a third off the height on a desktop. The singer
   runs almost the full height of the frame — raised foot low, head high — so
   the anchor sits mid-body rather than on his face, which keeps both ends in
   shot instead of cropping through one of them. */
.hero.hero--concert-focus .hero__media img { object-position: 58% 42%; }
.hero__media::after {
  content: "";
  position: absolute;
  inset: 0;
  /* Heavy enough to hold white text over a *bright* photograph, not just a dark
     one — the portrait heroes are high-key and the earlier, lighter scrim put
     the headline and the nav below AA on them. */
  background: linear-gradient(
    to top,
    rgba(6,6,6,.74) 0%,
    rgba(6,6,6,.52) 30%,
    rgba(6,6,6,.30) 58%,
    rgba(6,6,6,.22) 80%,
    rgba(6,6,6,.38) 100%
  );
}

.hero__inner {
  position: relative;
  width: min(100% - 2 * var(--space-m), var(--container));
  margin-inline: auto;
  padding-block: var(--space-2xl) var(--space-xl);
  color: #FAF8F5;
}
@media (min-width: 48em) {
  .hero__inner { width: min(100% - 2 * var(--space-xl), var(--container)); }
}
/* On a phone the hero block carried 112px above and 72px below, which spent
   most of the frame on empty scrim instead of the photograph. */
.hero__inner { padding-block: var(--space-xl) var(--space-l); }
.hero__sub { margin-bottom: var(--space-m); }
.hero h1 { color: #FAF8F5; max-width: 16ch; margin-bottom: var(--space-m); }
/* The eyebrow is the smallest text over the photograph, so it is held to the
   4.5:1 floor rather than the 3:1 one the headline gets. At 96% of the page
   cream it measured 4.48:1 over the wedding portrait on a 375px screen; pure
   white clears the floor without darkening the photograph any further. */
.hero .eyebrow { color: #FFFFFF; }
.hero__sub {
  color: rgba(250, 248, 245,.90);
  font-size: var(--step-1);
  max-width: 44ch;
  margin-bottom: var(--space-l);
  text-shadow: 0 1px 20px rgba(0,0,0,.35);
}
.hero__actions { display: flex; flex-wrap: wrap; gap: var(--space-s); }

.hero__scroll {
  position: absolute;
  right: 0;
  bottom: var(--space-xl);
  display: none;
  align-items: center;
  gap: var(--space-2xs);
  font-family: var(--font-ui);
  font-size: var(--step--1);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: rgba(250, 248, 245,.7);
}

.hero__scroll i {
  display: block;
  width: 1px; height: 3rem;
  background: linear-gradient(to bottom, rgba(250, 248, 245,.7), transparent);
  transform-origin: top;
  animation: scroll-hint 2.4s var(--ease-in-out) infinite;
}
@keyframes scroll-hint {
  0%, 100% { transform: scaleY(.3); opacity: .4; }
  50%      { transform: scaleY(1);  opacity: 1;  }
}

/* ---------- 08 EDITORIAL BLOCKS ------------------------------------------ */

.stat-row {
  display: grid;
  gap: var(--space-l);
  grid-template-columns: repeat(2, minmax(0,1fr));
  padding-block: var(--space-xl);
  border-top: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
}
/* Four figures, all of them numbers a client can act on. The original fourth
   was "1 - Photographer, one consistent style", a claim rather than a figure
   and a sentence already carried by Why clients book below; it is now the 50+
   edited-image count from the concerts pricing. */
@media (min-width: 60em) { .stat-row { grid-template-columns: repeat(4, minmax(0,1fr)); } }
.stat__value {
  font-family: var(--font-display);
  font-size: var(--step-3);
  font-weight: 500;
  letter-spacing: -0.04em;
  line-height: 1;
  display: block;
  margin-bottom: var(--space-2xs);
}
.stat__label {
  font-family: var(--font-ui);
  font-size: var(--step--1);
  color: var(--fg-muted);
  line-height: 1.45;
}

.feature {
  display: grid;
  gap: var(--space-xl);
  align-items: center;
}
@media (min-width: 60em) {
  .feature { grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr); gap: var(--space-3xl); }
  .feature--flip .feature__media { order: 2; }
}
.feature__media { position: relative; overflow: clip; }
.feature__media img { width: 100%; }

.callout {
  border-left: 1px solid var(--brass);
  padding-left: var(--space-m);
  font-family: var(--font-display);
  font-size: var(--step-2);
  font-weight: 400;
  line-height: 1.15;
  letter-spacing: -0.02em;
  color: var(--fg);
  max-width: 22ch;
}

/* ---------- 09 SERVICE INDEX --------------------------------------------- */

.service-index { border-top: 1px solid var(--border); }

.service-row {
  position: relative;
  display: grid;
  gap: var(--space-2xs);
  grid-template-columns: 1fr auto;
  align-items: baseline;
  padding-block: var(--space-m);
  border-bottom: 1px solid var(--border);
  text-decoration: none;
  color: var(--fg);
  transition: color var(--dur) var(--ease-out);
}
@media (min-width: 48em) {
  .service-row { padding-block: var(--space-l); }
}

/* The four-column layout only earns its keep when there is actually room for
   it. It used to start at 48em, but at 768px the container is 624px and the
   description column is sized to its own content, so it took 252px and left
   the title 84px for a 38px face — the names broke mid-word, "Head/shots/&/
   Portr/aits". It now waits for 68em, the width the full nav appears at.
   Below that the row stays two columns, title and price, with the
   description on its own line underneath. */
@media (min-width: 68em) {
  .service-row {
    grid-template-columns: minmax(0,1fr) auto;
    gap: var(--space-2xs) var(--space-m);
  }
}


.service-row__title {
  /* the global break-word is right for prose, but it let service names split
     mid-word when the column got tight; a name should wrap between words or
     overflow visibly so the real problem shows */
  overflow-wrap: normal;
  font-family: var(--font-display);
  font-size: var(--step-3);
  font-weight: 500;
  letter-spacing: -0.04em;
  line-height: 1.05;
  margin: 0;
  transition: transform var(--dur-slow) var(--ease-out);
}
.service-row__desc {
  font-family: var(--font-ui);
  font-size: var(--step--1);
  color: var(--fg-muted);
  grid-column: 1 / -1;
  margin: 0;
}
/* The description stays under the name it belongs to at every width. It used
   to move into a column of its own on wide screens, which put it a long way
   from the service it described. Below 68em it spans the row; above, it sits
   under the title rather than under the number. */
@media (min-width: 68em) {
  .service-row__desc { grid-column: 1 / -1; }
}
.service-row__price {
  font-family: var(--font-ui);
  font-size: var(--step--1);
  letter-spacing: 0.06em;
  color: var(--fg-subtle);
  white-space: nowrap;
}

@media (hover: hover) and (pointer: fine) {
  .service-row:hover .service-row__title { transform: translateX(14px); }
  .service-row:hover { color: var(--accent); }
}

/* The About page closes with the same service table the homepage opens with,
   minus the swapping background photograph — it is a summary at the end of a
   read, not the main event, so it stays quiet: a hairline above the first row
   so the group reads as a table rather than six loose links, and a slightly
   smaller title than the homepage's. The row itself, its hover and its price
   type are inherited from .service-row above, so the two pages can never
   drift apart. */
.service-list {
  border-top: 1px solid var(--border);
}
.service-list .service-row__title {
  font-size: var(--step-2);
}
.service-list__note {
  margin-top: var(--space-l);
  max-width: 52ch;
  color: var(--fg-muted);
  font-size: var(--step--1);
}

/* ---------- 10 GALLERIES -------------------------------------------------- */

/* Masonry via CSS columns. A grid forces every tile in a row to share one
   height, so a 2:3 portrait beside a 3:2 landscape either crops or leaves a
   hole. Columns let each photograph keep its own height, so nothing is cropped
   and nothing is stretched. */
.gallery {
  columns: 2;
  column-gap: var(--space-s);
}
@media (min-width: 48em) {
  .gallery { columns: 3; column-gap: var(--space-m); }
}
/* No four-column variant. It existed for the longer galleries, but a fourth
   column at 78em+ makes each photograph small enough that the work stops
   reading, and it put those pages out of step with the ones that stayed at
   three. Every gallery is three columns on a desktop now. */

.tile {
  position: relative;
  display: block;
  width: 100%;
  margin: 0 0 var(--space-s);
  padding: 0;
  border: 0;
  overflow: hidden;
  background: var(--bg-sunken);
  cursor: zoom-in;
  border-radius: var(--radius);
  /* Keep a photograph from being split across a column break. */
  break-inside: avoid;
  -webkit-column-break-inside: avoid;
  page-break-inside: avoid;
}
@media (min-width: 48em) {
  .tile { margin-bottom: var(--space-m); }
}

/* height:auto — the image's own width/height attributes set the box, so the
   full frame shows at its true aspect ratio and the space is still reserved
   before it loads (no layout shift). */
.tile img {
  width: 100%;
  height: auto;
  transition: transform var(--dur-image) var(--ease-out),
              filter var(--dur) var(--ease-out);
}
@media (hover: hover) {
  .tile:hover img { transform: scale(1.03); }
}
.tile::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(to top, rgba(6,5,4,.42), transparent 45%);
  opacity: 0;
  transition: opacity var(--dur) var(--ease-out);
}
.tile:hover::after, .tile:focus-visible::after { opacity: 1; }

.tile__caption {
  position: absolute;
  left: var(--space-s);
  right: var(--space-s);
  bottom: var(--space-s);
  z-index: 1;
  font-family: var(--font-ui);
  font-size: var(--step--1);
  color: #FAF8F5;
  text-align: left;
  opacity: 0;
  transform: translateY(6px);
  transition: opacity var(--dur) var(--ease-out), transform var(--dur) var(--ease-out);
}
.tile:hover .tile__caption, .tile:focus-visible .tile__caption { opacity: 1; transform: none; }

/* ---------- 11 PRICING ---------------------------------------------------- */

.price-card {
  display: flex;
  flex-direction: column;
  padding: var(--space-l);
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  transition: border-color var(--dur) var(--ease-out),
              transform var(--dur) var(--ease-out),
              box-shadow var(--dur) var(--ease-out);
}
@media (hover: hover) {
  .price-card:hover { border-color: var(--fg); }
}
.price-card--featured { border-color: var(--accent); }
.price-card__tag {
  font-family: var(--font-ui);
  font-size: var(--step--1);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--accent);
  margin-bottom: var(--space-xs);
}
.price-card h3 { margin-bottom: var(--space-2xs); }
.price-card__amount {
  font-family: var(--font-display);
  font-size: var(--step-2);
  font-weight: 500;
  letter-spacing: -0.035em;
  line-height: 1;
  display: block;
  margin-bottom: var(--space-m);
}
.price-card__amount small {
  font-family: var(--font-ui);
  font-size: var(--step--1);
  color: var(--fg-muted);
  letter-spacing: 0.04em;
}
.price-card ul {
  list-style: none;
  margin: 0 0 var(--space-l);
  padding: 0;
  flex: 1;
  font-size: var(--step--1);
  font-family: var(--font-ui);
  color: var(--fg-muted);
}
.price-card li {
  display: flex;
  gap: var(--space-2xs);
  padding-block: var(--space-3xs);
  line-height: 1.5;
}
.price-card li svg {
  width: 1rem; height: 1rem;
  flex-shrink: 0;
  margin-top: 0.28em;
  color: var(--accent);
}

.rate-table { width: 100%; border-collapse: collapse; font-family: var(--font-ui); }
.rate-table caption { text-align: left; color: var(--fg-muted); font-size: var(--step--1); padding-bottom: var(--space-s); }
.rate-table th, .rate-table td {
  text-align: left;
  padding: var(--space-s) var(--space-xs);
  border-bottom: 1px solid var(--border);
  font-size: var(--step-0);
}
.rate-table th {
  font-size: var(--step--1);
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--fg-subtle);
}
.rate-table td:last-child, .rate-table th:last-child { text-align: right; }
.rate-table tbody tr { transition: background-color var(--dur-fast) var(--ease-out); }
@media (hover: hover) { .rate-table tbody tr:hover { background: var(--bg-sunken); } }
.rate-table .is-best td { color: var(--fg); font-weight: 600; }

.table-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }

/* ---------- 12 FAQ -------------------------------------------------------- */

.faq { border-top: 1px solid var(--border); }
.faq details {
  border-bottom: 1px solid var(--border);
}
.faq summary {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-m);
  padding-block: var(--space-m);
  min-height: 44px;
  cursor: pointer;
  list-style: none;
  font-family: var(--font-display);
  font-size: var(--step-1);
  font-weight: 500;
  letter-spacing: -0.02em;
  line-height: 1.3;
  color: var(--fg);
  transition: color var(--dur-fast) var(--ease-out);
}
.faq summary::-webkit-details-marker { display: none; }
.faq summary:hover { color: var(--accent); }
.faq summary .marker {
  flex-shrink: 0;
  width: 1.25rem; height: 1.25rem;
  position: relative;
  align-self: center;
}
.faq summary .marker::before,
.faq summary .marker::after {
  content: "";
  position: absolute;
  inset: 50% 0 auto 0;
  height: 1px;
  background: currentColor;
  transition: transform var(--dur) var(--ease-out);
}
.faq summary .marker::after { transform: rotate(90deg); }
.faq details[open] summary .marker::after { transform: rotate(0deg); }
.faq__answer { padding-bottom: var(--space-m); }
.faq__answer p { color: var(--fg-muted); max-width: 62ch; }
.faq details[open] .faq__answer { animation: faq-in var(--dur) var(--ease-out); }
@keyframes faq-in { from { opacity: 0; transform: translateY(-6px); } to { opacity: 1; transform: none; } }

/* ---------- 13 FOOTER ----------------------------------------------------- */

.footer {
  background: var(--bg-sunken);
  border-top: 1px solid var(--border);
  padding-block: var(--space-2xl) var(--space-l);
}
.footer__cta { padding-bottom: var(--space-2xl); border-bottom: 1px solid var(--border); margin-bottom: var(--space-xl); }
.footer__cta h2 { max-width: 14ch; }
/* "Request a quote" was breaking onto two lines at every width, making the
   button 236x98 instead of a single row. It is the only button on the site
   that wrapped, so this stays scoped to it rather than applying to .btn
   generally, where longer labels do need to wrap. */
.footer__cta .btn { white-space: nowrap; }
.footer__cta-action { margin-top: var(--space-l); }
.footer__grid {
  display: grid;
  gap: var(--space-xl);
  margin-bottom: var(--space-xl);
}
@media (min-width: 48em) { .footer__grid { grid-template-columns: repeat(2, minmax(0,1fr)); } }
@media (min-width: 68em) { .footer__grid { grid-template-columns: 1.4fr 1fr 1fr 1.2fr; } }

.footer h4 {
  font-family: var(--font-ui);
  font-size: var(--step--1);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--fg-subtle);
  margin-bottom: var(--space-s);
  font-weight: 500;
}
.footer ul { list-style: none; margin: 0; padding: 0; }
.footer li { margin-bottom: var(--space-2xs); }
/* :not(.btn) matters — a bare `.footer a` rule outranks `.btn` on specificity
   and would repaint the call-to-action's label in muted grey on its own
   high-contrast background. */
.footer a:not(.btn) {
  color: var(--fg-muted);
  text-decoration: none;
  font-family: var(--font-ui);
  font-size: var(--step--1);
  display: inline-block;
  padding-block: 2px;
  transition: color var(--dur-fast) var(--ease-out);
}
.footer a:not(.btn):hover { color: var(--fg); }
.footer__bottom {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-s) var(--space-m);
  justify-content: space-between;
  align-items: center;
  padding-top: var(--space-l);
  border-top: 1px solid var(--border);
}
.footer__social { display: flex; gap: var(--space-2xs); }
.footer__social a {
  width: 44px; height: 44px;
  display: grid; place-items: center;
  border: 1px solid var(--border-interactive);
  border-radius: var(--radius);
  color: var(--fg-muted);
}
.footer__social a:hover { color: var(--fg); border-color: var(--fg-muted); }
.footer__social svg { width: 18px; height: 18px; }

/* ---------- 14 LIGHTBOX --------------------------------------------------- */

.lightbox {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: grid;
  grid-template-rows: auto 1fr auto;
  background: var(--overlay);
  backdrop-filter: blur(20px);
  -webkit-backdrop-filter: blur(20px);
  opacity: 0;
  visibility: hidden;
  transition: opacity var(--dur) var(--ease-out), visibility var(--dur);
}
.lightbox[data-open="true"] { opacity: 1; visibility: visible; }
.lightbox__bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--space-s) var(--space-m);
  color: #FAF8F5;
}
.lightbox__count { font-family: var(--font-ui); font-size: var(--step--1); letter-spacing: 0.1em; }
.lightbox__stage {
  position: relative;
  padding: 0 var(--space-s);
  min-height: 0;
}
/* The image is stretched to the stage box and then letterboxed with
   object-fit: contain. A percentage max-height does not resolve reliably
   against a `1fr` grid track — that is what made tall portraits render at
   their full natural height and spill out of the stage, showing a cropped
   horizontal band. Filling a definite box removes the percentage entirely,
   so the whole frame is always visible at its true proportions. */
.lightbox__stage img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  object-position: center;
  /* The base rule tints every <img> with the placeholder grey. Here the element
     is larger than the picture, so that tint would paint pale panels either side
     of a portrait instead of showing the dark backdrop through. */
  background: transparent;
  transform: scale(.96);
  opacity: 0;
  transition: transform var(--dur-slow) var(--ease-out), opacity var(--dur) var(--ease-out);
}
.lightbox[data-open="true"] .lightbox__stage img { transform: none; opacity: 1; }
.lightbox__foot {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-m);
  padding: var(--space-m);
  color: #FAF8F5;
}
.lightbox__caption {
  font-family: var(--font-ui);
  font-size: var(--step--1);
  color: rgba(250, 248, 245,.8);
  text-align: center;
  max-width: 52ch;
}
.lightbox .icon-btn { color: rgba(250, 248, 245,.8); }
.lightbox .icon-btn:hover { color: #FAF8F5; border-color: rgba(250, 248, 245,.4); }
body[data-scroll-locked="true"] { overflow: hidden; }

/* ---------- 15 MOTION ------------------------------------------------------
   Everything animates transform and opacity only — never width, height, top
   or left — so the compositor handles it and nothing triggers layout.
   --------------------------------------------------------------------------- */

/* Scoped to .js — the class is added by the inline <head> script before first
   paint. If scripting is off or the script fails, nothing is ever hidden and the
   page renders as plain static content instead of a blank screen. */
.js [data-reveal] {
  opacity: 0;
  transform: translate3d(0, 22px, 0);
  transition: opacity var(--dur-slow) var(--ease-out),
              transform var(--dur-slow) var(--ease-out);
  transition-delay: calc(var(--i, 0) * 70ms);
}
.js [data-reveal="is-visible"] { opacity: 1; transform: none; }

[data-reveal-img] { overflow: clip; }
.js [data-reveal-img] > img {
  transform: scale(1.07);
  opacity: 0;
  transition: transform 1200ms var(--ease-out), opacity var(--dur-slow) var(--ease-out);
  transition-delay: calc(var(--i, 0) * 70ms);
}
.js [data-reveal-img="is-visible"] > img { transform: none; opacity: 1; }

/* Line-by-line display heading reveal. */
.reveal-lines .line { display: block; overflow: hidden; }
.js .reveal-lines .line > span {
  display: block;
  transform: translate3d(0, 104%, 0);
  transition: transform 900ms var(--ease-out);
  transition-delay: calc(var(--i, 0) * 95ms);
}
.js .reveal-lines[data-reveal="is-visible"] .line > span { transform: none; }

/* Hero image drifts very slowly — no scroll listener, no jank. */
.hero__media img { animation: hero-drift 22s var(--ease-in-out) infinite alternate; }
@keyframes hero-drift {
  from { transform: scale(1.0); }
  to   { transform: scale(1.07); }
}

/* ---------- 16 REDUCED MOTION & PRINT ------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
  html { scroll-behavior: auto; }
  .js [data-reveal], .js [data-reveal-img] > img { opacity: 1 !important; transform: none !important; }
  .js .reveal-lines .line > span { transform: none !important; }
  .hero__media img { animation: none !important; }
  .hero__scroll i { animation: none !important; }
}

@media print {
  .header, .footer__cta, .lightbox, .services__bg, .hero__scroll, .mobile-nav { display: none !important; }
  body { background: #fff; color: #000; }
  .hero { min-height: auto; }
  a[href^="http"]::after { content: " (" attr(href) ")"; font-size: 0.8em; }
}

/* ---------- 17 FORMS ------------------------------------------------------ */

.form { display: grid; gap: var(--space-m); }
/* An explicit display beats the hidden attribute's UA default of display:none,
   so hiding the form after a successful send left it on screen underneath the
   confirmation. Both of these carry an attribute selector, so they outrank the
   display rules above them. */
.form[hidden], [data-form-heading][hidden], .form__done[hidden] { display: none; }

.field { display: grid; gap: var(--space-3xs); }

.field label {
  font-family: var(--font-ui);
  font-size: var(--step--1);
  font-weight: 500;
  letter-spacing: 0.02em;
  color: var(--fg);
}

/* Visible labels, always — never a placeholder standing in for one. */
.field input,
.field select,
.field textarea {
  width: 100%;
  min-height: 48px;
  padding: 0.7rem 0.85rem;
  background: var(--bg);
  color: var(--fg);
  border: 1px solid var(--border-interactive);
  border-radius: var(--radius);
  font-family: var(--font-ui);
  font-size: max(16px, var(--step-0)); /* 16px minimum stops iOS zooming on focus */
  transition: border-color var(--dur-fast) var(--ease-out),
              box-shadow var(--dur-fast) var(--ease-out);
}
.field textarea { min-height: 8rem; resize: vertical; line-height: 1.55; }

/* Enquiry form states. Everything below reuses the existing tokens and type
   scale — no new colours, no layout change to the card itself. */

/* Honeypot: off-screen rather than display:none, because some bots skip
   hidden inputs but not positioned ones. Never focusable, never announced. */
.hp {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

/* One line under the button. Empty and invisible until there is something to
   say, so it never reserves space or shifts the layout. */
.form__status {
  margin: var(--space-2xs) 0 0;
  font-size: var(--step--1);
  line-height: 1.5;
  color: var(--fg-muted);
  min-height: 0;
}
.form__status:empty { display: none; }
.form__status[data-tone="error"] { color: var(--critical); }

/* Per-field validation. The message sits under its own input so it is obvious
   which field it belongs to; the border shift is the same weight the focus
   state already uses, so nothing jumps. */
.field__error {
  margin: 0;
  font-size: var(--step--1);
  color: var(--critical);
}
.field[data-invalid] input,
.field[data-invalid] select,
.field[data-invalid] textarea { border-color: var(--critical); }

/* Submitting: the button stays exactly the same size so the card does not
   reflow mid-submit. */
.btn[aria-busy="true"] { opacity: .65; cursor: progress; }

/* The confirmation. Same rhythm as the prose elsewhere in the card. */
.form__done p + p { margin-top: var(--space-s); }
.form__done > .price-card__tag { margin-bottom: var(--space-s); }
.field input:hover, .field select:hover, .field textarea:hover { border-color: var(--fg-subtle); }
.field input:focus, .field select:focus, .field textarea:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent);
}
.field .hint {
  font-family: var(--font-ui);
  font-size: var(--step--1);
  color: var(--fg-muted);
  line-height: 1.5;
}
.field select { appearance: none; -webkit-appearance: none; padding-right: 2.2rem;
  background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
                    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position: calc(100% - 1.1rem) 55%, calc(100% - 0.75rem) 55%;
  background-size: 6px 6px, 6px 6px;
  background-repeat: no-repeat;
}

/* ---------- 18 UNWRITTEN COPY MARKERS -------------------------------------
   Placeholder text on the About page. Deliberately loud: this must not ship
   unnoticed. Delete the <mark> wrapper along with the bracketed text.
   -------------------------------------------------------------------------- */

.todo {
  background: var(--bg-sunken);
  color: var(--fg-muted);
  border-bottom: 1px dashed var(--border-interactive);
  padding: 0.1em 0.25em;
}

/* ---------- 19 LOGO MARK -------------------------------------------------
   The wordmark is black line art on transparency. Inverting it in dark mode
   is exact for monochrome art and avoids shipping a second file.
   ------------------------------------------------------------------------- */

/* Two artwork files — black line art for light, white for dark — swapped as a
   CSS background so only the file for the active theme is ever downloaded.
   (An <img> pair would fetch both; a <picture> with a media query would ignore
   the manual theme toggle and follow only the OS setting.) The accessible name
   comes from real, visually-hidden text inside the link, so the studio name is
   still in the DOM for search engines. */
.logo-mark {
  display: block;
  aspect-ratio: 1 / 1;
  background-image: url("../img/logo/oscar-leo-photography-logo-light-480.png");
  background-position: center;
  background-size: contain;
  background-repeat: no-repeat;
}

.footer__mark {
  width: 128px;
  margin-bottom: var(--space-m);
}

/* Footer column headings are h3 (an h2 followed by h4 skips a heading level),
   styled to match the small-caps label they were before. */
.footer .footer__head {
  font-family: var(--font-ui);
  font-size: var(--step--1);
  font-weight: 500;
  line-height: 1.4;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--fg-subtle);
  margin-bottom: var(--space-s);
}

/* The skip link is the first thing a keyboard user meets — give it a full target. */
.skip-link { min-height: 44px; display: inline-flex; align-items: center; }

/* Brand wordmark: comfortable target without changing how it looks. */
.brand { padding-block: 0; }   /* the mark alone far exceeds the touch-target minimum */

/* Inline links sit at the text's own 17px line box. Padding grows the tap
   target past the 24px minimum; the negative margin cancels it so line
   spacing and text flow are untouched. */
.prose a, .text-link {
  padding-block: 5px;
  margin-block: -5px;
}

/* ---------- 20 CONTACT-SHEET HERO -----------------------------------------
   The montage is bright, busy and edge-to-edge, so the standard hero scrim is
   not enough to hold white headline text at AA. This variant darkens the whole
   frame and adds a stronger foot, and it drops the slow drift — a grid of faces
   sliding around reads as a glitch rather than as motion.
   -------------------------------------------------------------------------- */

.hero--grid .hero__media::after {
  /* Lighter than it was for the single montage: with six photographs cycling
     through, a scrim heavy enough for a busy grid buried them. Contrast was
     carrying 1.6-1.9x more margin than it needed, so this spends some of it. */
  background:
    linear-gradient(to top,
      rgba(6, 6, 6, .72) 0%,
      rgba(6, 6, 6, .56) 26%,
      rgba(6, 6, 6, .34) 52%,
      rgba(6, 6, 6, .30) 78%,
      /* the top band carries the header, whose nav links are the smallest text
         over the photograph — it stays darker than the middle, where the
         photograph is most visible */
      rgba(6, 6, 6, .50) 100%);
}
.hero--grid .hero__media img {
  animation: none;
  transform: none;
  filter: saturate(.92) contrast(1.02);
}

/* On a phone the square montage crops to roughly three columns, which is the
   right density — any tighter and the faces stop reading. */
@media (max-width: 47.99em) {
  .hero--grid .hero__media img { object-position: center 28%; }
}

/* ---------- 21 HEADER OVER THE HERO ---------------------------------------
   Every page opens on a full-bleed image behind the fixed header, so a tinted
   bar sits on top of the photograph as a visible band. At rest the header is
   transparent with light text; it only becomes a solid surface once the
   visitor has scrolled past the hero.
   -------------------------------------------------------------------------- */

.header:not([data-scrolled="true"]) {
  background: transparent;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  border-bottom-color: transparent;
}
.header:not([data-scrolled="true"]) .brand { color: #FAF8F5; }
.header:not([data-scrolled="true"]) .brand__sub { color: rgba(250, 248, 245, .86); }
.header:not([data-scrolled="true"]) .nav a { color: rgba(250, 248, 245, .92); }
.header:not([data-scrolled="true"]) .nav a:hover,
.header:not([data-scrolled="true"]) .nav a[aria-current="page"] { color: #FAF8F5; }
.header:not([data-scrolled="true"]) .icon-btn { color: rgba(250, 248, 245, .9); }
.header:not([data-scrolled="true"]) .icon-btn:hover {
  color: #FAF8F5;
  border-color: rgba(250, 248, 245, .38);
}
.header:not([data-scrolled="true"]) :focus-visible { outline-color: #FAF8F5; }

/* The drawer is an opaque panel, so its own header keeps the normal colours. */
.mobile-nav .brand { color: var(--fg); }
.mobile-nav .brand__sub { color: var(--fg-subtle); }

/* ---------- 22 LOCAL SCRIM BEHIND HERO TEXT -------------------------------
   The hero copy needs some protection or it drops below AA on a bright
   photograph — measured at 2.6-2.9:1 with no scrim at all.

   This used to be an inset box, which read as a visible rectangle: it stopped
   2rem short of each edge, so it had hard vertical sides sitting over the
   picture. It is now full-bleed horizontally and feathers out over a long
   distance upward, so there is no edge anywhere — it reads as the photograph
   getting darker toward the bottom, not as a slab laid on top of it.

   The sides run 50vw past the copy in both directions and the hero clips them,
   so the darkening reaches the frame edges at every width instead of stopping
   at the container. The text does not move: only the gradient is wider. */
.hero__inner::before {
  content: "";
  position: absolute;
  inset: -8rem -50vw -2rem -50vw;
  z-index: -1;
  pointer-events: none;
  background: linear-gradient(
    to top,
    rgba(0, 0, 0, .46) 0%,
    rgba(0, 0, 0, .40) 40%,
    rgba(0, 0, 0, .24) 70%,
    rgba(0, 0, 0, .10) 88%,
    rgba(0, 0, 0, 0) 100%);
}

/* Header lockup: the mark sits beside the wordmark. Over the hero the header is
   transparent on a dark photograph, so the white artwork is used there whatever
   the page theme is — the theme only decides the mark once the header has
   become a solid surface. The 240px files are plenty for a 40px mark at 3x. */
.brand { display: inline-flex; align-items: center; gap: var(--space-2xs); }

.brand__mark {
  /* Sized from the bar, not a fixed value: the mark fills 90% of the
     header height and keeps that relationship if the bar changes again. */
  width: calc(var(--header-h) * var(--logo-fill));
  flex: none;
  background-image: url("../img/logo/oscar-leo-photography-logo-light-240.png");
  transition: width var(--dur) var(--ease-out);
}
/* Over the hero the header is transparent on a dark photograph, so the white
   artwork is used there. This is not a dark theme — it is white line art on a
   dark image. Once the header becomes a solid white surface it reverts. */
.header:not([data-scrolled="true"]) .brand__mark {
  background-image: url("../img/logo/oscar-leo-photography-logo-dark-240.png");
}
.header[data-scrolled="true"] .brand__mark { width: calc(var(--header-h-sm) * var(--logo-fill)); }
/* Smaller than the page header's mark: the drawer row also has to hold the
   wordmark and the close button, and the full-size mark left 21px for it. */
.mobile-nav .brand__mark { width: 2.75rem; }
.mobile-nav__head .brand { min-width: 0; overflow: hidden; }

/* ---------- 23 SERVICE BACKDROP -------------------------------------------
   Hovering a service row fades a full-bleed photograph in behind the list.

   On contrast: a photograph behind dark-on-white text cannot be made readable
   without fading the photo to near-nothing, which defeats the point. So the
   band commits to a dark treatment in BOTH themes — the six colour tokens are
   redefined for this section only, which flips every component inside it
   without touching their rules — and white text sits on the photograph behind
   a scrim. Contrast is then constant and measurable rather than depending on
   which photograph happens to be showing.
   -------------------------------------------------------------------------- */

.services {
  position: relative;
  isolation: isolate;
  overflow: clip;

  --bg: #0A0A0A;
  --bg-elevated: #141414;
  --bg-sunken: #000000;
  --fg: #FAFAFA;
  --fg-muted: #C6C6CE;
  --fg-subtle: #ABABB4;
  --border: #38383E;
  --border-strong: #4A4A52;
  --accent: #FAFAFA;

  background: var(--bg);
  color: var(--fg);
}

.services__bg {
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
}
.services__bg picture { display: contents; }
.services__bg img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center 30%;
  opacity: 0;
  transform: scale(1.04);
  transition: opacity 620ms var(--ease-out), transform 1400ms var(--ease-out);
}
.services__bg img[data-active="true"] {
  opacity: 1;
  transform: none;
}
/* The scrim sits above the photographs, below the text. Heavy on the left,
   where the type is, and lighter on the right so the photograph still reads. */
.services__bg::after {
  content: "";
  position: absolute;
  inset: 0;
  background:
    linear-gradient(to right,
      rgba(8, 8, 8, .92) 0%,
      rgba(8, 8, 8, .88) 45%,
      rgba(8, 8, 8, .74) 75%,
      rgba(8, 8, 8, .66) 100%),
    linear-gradient(to bottom,
      rgba(8, 8, 8, .55) 0%,
      rgba(8, 8, 8, .30) 40%,
      rgba(8, 8, 8, .55) 100%);
}

/* Rows lift slightly as the photograph arrives. */
.services .service-row__title { transition: transform var(--dur-slow) var(--ease-out); }

@media (hover: hover) and (pointer: fine) {
  .services[data-bg-active="true"] .service-row:not(:hover) {
    color: var(--fg-subtle);
    transition: color var(--dur) var(--ease-out);
  }
}

/* Touch devices cannot hover, so on those the photographs cycle on their own
   instead — see initServiceBackdrop(). They stay lazy, so nothing is fetched
   until the section is actually scrolled to. */

@media (prefers-reduced-motion: reduce) {
  .services__bg img { transition: opacity 1ms; transform: none !important; }
}

/* On a phone the type runs the full width, so the left-heavy desktop scrim just
   buries the photograph — measured at 91% coverage, with contrast far higher
   than it needed to be. Here the scrim is even and much lighter, so the picture
   actually reads while the text stays well clear of AA. Desktop is untouched. */
@media (hover: none) {
  .services {
    /* The small secondary text carries the least contrast, so it is lifted a
       little here — that buys the scrim room to stay light enough to see the
       photograph through. */
    --fg-muted: #DCDCE1;
    --fg-subtle: #CBCBD2;
  }
  .services__bg::after {
    background:
      linear-gradient(to right,
        rgba(8, 8, 8, .68) 0%,
        rgba(8, 8, 8, .63) 60%,
        rgba(8, 8, 8, .60) 100%),
      linear-gradient(to bottom,
        rgba(8, 8, 8, .48) 0%,
        rgba(8, 8, 8, .34) 40%,
        rgba(8, 8, 8, .52) 100%);
  }
}

/* ---------- 24 SMALL-SCREEN HEADER FIT ------------------------------------
   The header row is logo + wordmark on the left and two 52px controls on the
   right. The wordmark does not wrap, so on a narrow screen the row grew wider
   than the viewport and pushed the theme toggle and the menu button off the
   right edge — at 375px they sat at 406px and 466px. With overflow-x hidden
   there was no way to scroll to them, so the menu was unreachable.

   The second word used to be hidden outright to buy that room. It no longer
   has to be: the theme toggle is gone, so the row carries one 52px control
   instead of two, and stacking the wordmark into two lines beside the mark
   fits the full name with room to spare — 198px of brand against 251px of
   space at 375px, and 153px against 196px at 320px.
   -------------------------------------------------------------------------- */

@media (max-width: 30em) {
  .header .brand__name,
  .mobile-nav .brand__name {
    display: flex;
    flex-direction: column;
    line-height: 1.15;
  }
}

@media (max-width: 22em) {
  /* Smallest phones need the mark to give a little too. */
  .header .brand__mark { width: 2.75rem; }
  .header[data-scrolled="true"] .brand__mark { width: 2.5rem; }
}

/* ---------- 25 HERO SLIDESHOW ---------------------------------------------
   Six photographs crossfading behind the headline. Each is cut 16:9 for
   landscape windows and 2:3 for portrait ones (see _build/hero_slides.py), so
   the hero never squeezes a wide frame into a tall box, and every slide shares
   its shape within a breakpoint — none looks more zoomed in than the others.
   -------------------------------------------------------------------------- */

.hero__slides { position: absolute; inset: 0; }
.hero__slides picture { display: contents; }
.hero__slides img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center 32%;
  opacity: 0;
  transition: opacity 1200ms var(--ease-out);
}
.hero__slides img[data-active="true"] { opacity: 1; }

/* The slow drift belongs to a single static hero; with slides crossfading it
   is one motion too many. */
.hero--grid .hero__slides img { animation: none; }

@media (prefers-reduced-motion: reduce) {
  .hero__slides img { transition: opacity 1ms; }
}

/* ---------- 26 DARK FOOTER -------------------------------------------------
   The footer now closes the page on a dark band, which separates it properly
   from the white content above and answers the dark services band earlier in
   the page. The six colour tokens are redefined for this section only, so every
   component inside it flips without touching their own rules.
   -------------------------------------------------------------------------- */

.footer {
  --bg: #0A0A0A;              /* ink-900 */
  --bg-sunken: #000000;
  --bg-elevated: #141414;     /* ink-800 */
  --fg: #FAF8F5;              /* paper-100  18.7:1 */
  --fg-muted: rgba(250, 248, 245, .72);    /*  9.7:1 */
  --fg-subtle: rgba(250, 248, 245, .62);   /*  7.4:1 */
  --border: rgba(250, 248, 245, .16);
  --border-strong: rgba(250, 248, 245, .34);
  --border-interactive: rgba(250, 248, 245, .52);  /* 5.4:1 */
  --accent: #FAF8F5;          /* the call-to-action inverts with the band */
  --accent-hover: #D8D3CB;    /* ink-100 */
  --on-accent: #0A0A0A;
  --brass: #B7A882;           /* brass-300 reads on ink where brass-500 does not */

  background: var(--bg);
  color: var(--fg);
  border-top: 0;
}

/* The mark is black line art, so the dark band needs the white artwork —
   the same file the logo already uses over the hero photograph. */
.footer .logo-mark {
  background-image: url("../img/logo/oscar-leo-photography-logo-dark-480.png");
}

/* ---------- 27 FOOTER ON SMALL SCREENS ------------------------------------
   Stacked in one column the footer ran 1798px — 2.2 screens of scrolling —
   with 72px of gap between each of the four blocks. The two short link lists
   sit side by side instead, and the gaps come in, which removes roughly a
   screen of it. The logo block and the areas list still span the full width,
   since both carry a paragraph.
   -------------------------------------------------------------------------- */

@media (max-width: 47.99em) {
  .footer { padding-block: var(--space-xl) var(--space-m); }

  /* 128px of logo plus its margin was a sixth of the footer on its own. */
  .footer__mark { width: 88px; margin-bottom: var(--space-s); }

  /* The call-to-action carried 206px of text inside 200px of surrounding
     space — a 1:1 ratio — and 112px of it sat in the single gap between the
     button and the first link list. */
  .footer__cta { padding-bottom: var(--space-l); margin-bottom: var(--space-m); }
  .footer__cta > * { margin-bottom: var(--space-s); }
  .footer__cta-action { margin-top: var(--space-m); }

  .footer__grid {
    grid-template-columns: 1fr 1fr;
    gap: var(--space-m);
    margin-bottom: var(--space-m);
  }
  .footer__grid > :first-child,
  .footer__grid > :last-child { grid-column: 1 / -1; }
}

/* ---------- 28 PHOTO BAND --------------------------------------------------
   A band whose background is a slideshow rather than a flat colour. It answers
   the white gallery above it the way the tint did, but with the work itself.

   The six colour tokens are redefined for this section only, the way the footer
   does it, so every component inside — the callout's rule, the link, the
   eyebrows — flips to the light set without touching their own rules.
   -------------------------------------------------------------------------- */

.section--photo {
  --fg: #FAF8F5;              /* paper-100, not paper-white — same warm ramp */
  --fg-muted: rgba(250, 248, 245, .90);
  --fg-subtle: rgba(250, 248, 245, .84);
  --border: rgba(250, 248, 245, .28);
  --border-strong: rgba(250, 248, 245, .40);
  --accent: #FAF8F5;
  --accent-hover: rgba(250, 248, 245, .82);
  --brass: #B7A882;

  position: relative;
  isolation: isolate;
  /* The media box overhangs the band so the photograph can travel. Without
     this it painted those 130px OUTSIDE the section — unscrimmed, over the
     gallery above — and the scrim's own top edge showed as a hard horizontal
     seam across the band. */
  overflow: hidden;
  color: var(--fg);
  background: #0A0A0A;   /* holds the band before the first photograph paints */
}

/* The photograph travels as the band crosses the viewport. It can only do that
   without showing an edge if it is taller than the band it sits in, so the media
   box overhangs by --parallax-overhang top and bottom, and the script moves it
   within exactly that range and no further. */
.section--photo { --parallax-overhang: 12%; }

.section__media {
  position: absolute;
  inset: calc(-1 * var(--parallax-overhang)) 0;
  z-index: 0;
  overflow: hidden;
}
.section__media picture { display: contents; }
.section__media img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
/* Only promoted while it is actually moving — a permanent will-change keeps a
   layer alive for a band most of the page never looks at. */
.section__media[data-parallax-active="true"] img { will-change: transform; }

/* The scrim is deliberately heavier than contrast alone would require.

   With a stage photograph behind it the rule was "lightest step that still
   clears AA", because showing the work mattered and nothing in the frame
   competed with the copy. The band now carries a tight portrait, and a face
   pulls the eye harder than any amount of contrast margin can offset — the
   text sits directly across her eyes. Priority is the text, so the
   photograph is pushed back to atmosphere rather than held at the edge of
   legibility. */
.section--photo::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  background: linear-gradient(
    to bottom,
    rgba(6, 6, 6, .86) 0%,
    rgba(6, 6, 6, .82) 45%,
    rgba(6, 6, 6, .88) 100%);
}

.section--photo > .container { position: relative; z-index: 2; }

/* On a phone the band is tall and the text is the only thing in it, so the
   photograph can carry more of the frame: the scrim lifts and the copy is
   pulled tighter so less of it sits over her. */
@media (max-width: 47.99em) {
  .section--photo { padding-block: var(--space-xl); }
  .section--photo::after {
    background: linear-gradient(
      to bottom,
      rgba(6, 6, 6, .78) 0%,
      rgba(6, 6, 6, .60) 42%,
      rgba(6, 6, 6, .80) 100%);
  }
  .section--photo .grid-2 { gap: var(--space-l); }
  .section--photo .prose > * + * { margin-top: var(--space-s); }
  .section--photo h2 { margin-bottom: var(--space-s); }
  .section--photo .eyebrow { margin-bottom: var(--space-2xs); }
}

/* Someone who asked for less motion gets the photograph, held still. The script
   also never starts, so no transform is ever written. */
@media (prefers-reduced-motion: reduce) {
  .section__media img { transform: none !important; }
}

/* A phone gets the short form of each service name; the full name stays in the
   DOM for wider screens and for anything reading the page. */
.service-row__brief { display: none; }
@media (max-width: 47.99em) {
  .service-row__long { display: none; }
  .service-row__brief { display: inline; }
}

/* ---------- 29 SERVICE LIST ON A PHONE -------------------------------------
   The backdrop cycles by itself on touch (there is no hover to drive it), so
   the row whose photograph is showing lifts to meet it. Names are a step
   smaller here than the display scale gives them — at --step-3 they crowded
   the price. Desktop keeps both the scale and the static list.
   -------------------------------------------------------------------------- */
@media (max-width: 47.99em) {
  .service-row__title { font-size: var(--step-2); }

  /* Tighter rows so more of the list lands on one screen. */
  .service-row { padding-block: var(--space-s); gap: var(--space-3xs); }
  .services .section-head { margin-bottom: var(--space-l); }

  .service-row {
    transition: opacity var(--dur) var(--ease-out);
  }
  .services[data-bg-active="true"] .service-row:not([data-current="true"]) {
    opacity: .55;
  }
  .service-row[data-current="true"] .service-row__title {
    transform: translateX(10px);
  }
}

@media (prefers-reduced-motion: reduce) {
  .service-row,
  .service-row[data-current="true"] .service-row__title {
    transition: none;
    transform: none;
  }
}

/* ---------- 30 WORDMARK APPEARS ON SCROLL ----------------------------------
   At the top of a page the header carries the mark alone. The name slides down
   into place once the bar condenses, so the opening frame stays clean.
   -------------------------------------------------------------------------- */
.header .brand__name {
  transition: opacity var(--dur) var(--ease-out), transform var(--dur) var(--ease-out);
}
.header:not([data-scrolled="true"]) .brand__name {
  opacity: 0;
  transform: translateY(-6px);
  pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
  .header .brand__name { transition: opacity var(--dur) var(--ease-out); }
  .header:not([data-scrolled="true"]) .brand__name { transform: none; }
}

/* ---------- 31 SCROLL CUE AND BACK TO TOP (PHONES) -------------------------
   The hero cue used to appear only on wide screens, where there was room for
   it beside the buttons. On a phone it sits centred under them as a single
   drawn line with a chevron, and every page gets one. The footer closes with
   the way back up.
   -------------------------------------------------------------------------- */
@media all {
  /* The twin of the back-to-top control: same circle, same size, same inset
     from the right edge, pointing the other way. */
  .hero__scroll {
    display: flex;
    justify-content: center;
    right: calc(var(--space-s) - var(--space-m));
    left: auto;
    /* Below the buttons, not beside them: the action row reaches the right
       edge on a phone, so at button height the mark sat on top of it. */
    bottom: var(--space-2xs);
    width: 2.25rem;
    height: 2.25rem;
  }
  .hero__scroll span,
  .hero__scroll i { display: none; }   /* just the mark, no label and no line */
  .hero__scroll::after {
    content: "";
    flex: none;
    box-sizing: border-box;
    width: .625rem; height: .625rem;
    margin-top: -.15rem;               /* optical centring of a rotated corner */
    border-right: 1.75px solid rgba(250, 248, 245, .85);
    border-bottom: 1.75px solid rgba(250, 248, 245, .85);
    transform: rotate(45deg);
    animation: scroll-chevron 2s var(--ease-in-out) infinite;
  }
}

@keyframes scroll-chevron {
  0%, 100% { transform: rotate(45deg) translate(-1.5px, -1.5px); opacity: .45; }
  50%      { transform: rotate(45deg) translate(1.5px, 1.5px);   opacity: 1; }
}

/* The same drawn mark as the hero cue, pointing the other way, with the label
   sitting under it. No box — it closes the page rather than asking to be
   clicked like a button. */
.to-top {
  display: none;
  flex-direction: column;
  align-items: center;
  gap: var(--space-2xs);
  color: var(--fg-subtle);
  font-family: var(--font-ui);
  font-size: var(--step--1);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  text-decoration: none;
  transition: color var(--dur-fast) var(--ease-out);
}
.to-top svg {
  width: 1.125rem;
  height: 1.125rem;
  stroke-width: 1.75;
  animation: to-top-hint 2s var(--ease-in-out) infinite;
}
/* The label sits under the circle and only fades up at the very bottom of the
   page; it stays in the accessible name throughout. */
.to-top span {
  position: absolute;
  top: calc(100% + var(--space-2xs));
  right: 0;
  transform: translateY(-4px);
  white-space: nowrap;
  font-size: .5625rem;
  letter-spacing: .14em;
  color: var(--fg-subtle);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--dur) var(--ease-out), transform var(--dur) var(--ease-out);
}
:root[data-at-end="true"] .to-top span {
  opacity: 1;
  transform: none;
}
.to-top:hover { color: var(--fg); }
@media (max-width: 47.99em) {
  /* Floating, and only once a screenful has gone by — it rises into place
     rather than sitting at the bottom of the page waiting to be reached. */
  .to-top {
    display: flex;
    position: fixed;
    right: var(--space-s);
    bottom: var(--space-xl);
    z-index: 60;
    /* Round, not square — the one curve on a site of hard corners, because it
       is a floating control rather than part of the page's grid. */
    width: 2.25rem;
    height: 2.25rem;
    padding: 0;
    justify-content: center;
    color: var(--fg-muted);
    font-size: var(--step--1);
    opacity: 0;
    transform: translateY(10px);
    pointer-events: none;
    transition: opacity var(--dur) var(--ease-out), transform var(--dur) var(--ease-out);
  }
  :root[data-past-fold="true"] .to-top {
    opacity: 1;
    transform: none;
    pointer-events: auto;
  }
}

@keyframes to-top-hint {
  0%, 100% { transform: translateY(2px); opacity: .45; }
  50%      { transform: translateY(-2px); opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .hero__scroll::after,
  .to-top svg { animation: none; }
}

/* ---------- 32 HERO TYPE ON A PHONE ----------------------------------------
   The headline filled most of the frame and left little of the photograph
   showing. A step down on the h1 and the standfirst gives the picture room
   without touching the desktop scale.
   -------------------------------------------------------------------------- */
.hero h1 {
  font-size: clamp(2.1rem, 1.35rem + 3.6vw, 3.4rem);
  line-height: 1.06;
  margin-bottom: var(--space-s);
}
.hero__inner .lead,
.hero__inner > p {
  font-size: var(--step-0);
  line-height: 1.5;
}
.hero .eyebrow { margin-bottom: var(--space-xs); }
.hero__actions { gap: var(--space-2xs); }
