/* ============================================================================
   BEFORE THE LIGHT GOES — MOTION EDITION
   global stylesheet & design tokens
   ----------------------------------------------------------------------------
   Single styling control point for the whole site, linked statically from
   Base.astro (same hosting constraint as the original site: no Vite-injected
   CSS). Motion is driven by GSAP/Lenis from /scripts/motion.js; this file
   defines every token, layout, and the no-JS / reduced-motion fallbacks.
   ========================================================================== */

/* ---------------------------------------------------------------------------
   1. FONTS — self-hosted, and ONLY the faces that are actually used. Montserrat
   carries the whole page: Thin (100) for the big display lines, Black (900) for
   the accent words, 300/400/500 for body and labels — the thin × black contrast
   of the aura.build reference. Lemon Milk Light is the nav + footer wordmark.
   ⚠ Cleanup 2026-07-28: Fraunces, Space Grotesk, Inter and Newsreader (14
   files, ~271 KB) were left over from earlier design rounds and referenced by
   nothing; Alex Brush (32 KB) had only a `--font-brand` token that no rule
   used; Lemon Milk Medium (33 KB) was declared AND preloaded but every rule
   asks for weight 300, so the browser fetched it eagerly and never drew a
   glyph with it. All deleted. A test now fails the build if a file in
   public/fonts is not referenced here, so this cannot silently regrow.
--------------------------------------------------------------------------- */
@font-face {
  font-family: "Montserrat";
  src: url("/fonts/montserrat-100.woff2") format("woff2");
  font-weight: 100;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Montserrat";
  src: url("/fonts/montserrat-300.woff2") format("woff2");
  font-weight: 300;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Montserrat";
  src: url("/fonts/montserrat-400.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Montserrat";
  src: url("/fonts/montserrat-500.woff2") format("woff2");
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Montserrat";
  src: url("/fonts/montserrat-900.woff2") format("woff2");
  font-weight: 900;
  font-style: normal;
  font-display: swap;
}
/* LEMON MILK (Marsnev, donationware — donate for commercial use):
   Light = the nav and footer wordmark. That is the only weight the site
   asks for, so it is the only one shipped. */
@font-face {
  font-family: "Lemon Milk";
  src: url("/fonts/lemonmilk-light.otf") format("opentype");
  font-weight: 300;
  font-style: normal;
  font-display: swap;
}

/* ---------------------------------------------------------------------------
   2. DESIGN TOKENS — palette is identical to the original BTLG site.
--------------------------------------------------------------------------- */
:root {
  /* — Font families — */
  --font-display: "Montserrat", system-ui, -apple-system, sans-serif;
  --font-sans: "Montserrat", system-ui, -apple-system, "Segoe UI", sans-serif;
  /* (--font-brand held Alex Brush for the wordmark + preloader + slogan. All
     three are gone; the wordmark uses Lemon Milk directly. Removed 07-28.) */

  /* — Brand palette (unchanged) — */
  --color-granite: #4a4a4a;
  --color-limestone: #e8e4dc;
  --color-ink: #141312;
  --color-ink-2: #1d1c1a;
  --color-near-black: #0c0b0a;
  --color-limestone-dim: #b9b3a8;
  --color-text-on-light: #1b1a18;

  /* — Semantic tokens — */
  --bg: var(--color-ink);
  --bg-elevated: var(--color-ink-2);
  --surface: var(--color-granite);
  --text: var(--color-limestone);
  --text-muted: var(--color-limestone-dim);
  --text-faint: #8d877e;
  --border: color-mix(in srgb, var(--color-limestone) 16%, transparent);
  --border-strong: color-mix(in srgb, var(--color-limestone) 30%, transparent);
  --accent: var(--color-limestone);
  --focus-ring: #d9cfa8;
  --backdrop: rgba(12, 11, 10, 0.96);

  /* — Light-section tokens (the limestone About break) — */
  --light-bg: var(--color-limestone);
  --light-text: var(--color-text-on-light);
  --light-muted: #4a4843;
  --light-border: color-mix(in srgb, var(--color-text-on-light) 14%, transparent);

  /* — Type scale (cinematic: display sizes pushed further than v1) — */
  --fs-display: clamp(3rem, 11vw, 9.5rem);   /* hero name / section displays  */
  --fs-h1: clamp(2.25rem, 6vw, 4.5rem);
  --fs-h2: clamp(1.6rem, 3.4vw, 2.6rem);
  --fs-h3: clamp(1.2rem, 2vw, 1.6rem);
  --fs-lead: clamp(1.1rem, 1.7vw, 1.4rem);
  --fs-body: 1.0625rem;
  --fs-small: 0.875rem;
  --fs-tiny: 0.72rem;
  --leading-body: 1.7;
  --leading-tight: 1.04;
  --measure: 64ch;

  /* — Spacing — */
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-6: 1.5rem;
  --space-8: 2rem;
  --space-12: 3rem;
  --space-16: 4rem;
  --space-24: 6rem;
  --space-32: 8rem;
  --container: 1360px;
  --container-narrow: 760px;
  --gutter: clamp(1.25rem, 4vw, 3.5rem);

  /* — Radii & shadow — */
  --radius-sm: 2px;
  --radius: 4px;
  --shadow-soft: 0 18px 50px -20px rgba(0, 0, 0, 0.7);

  /* — Motion — */
  --dur-micro: 250ms;
  --dur-reveal: 700ms;
  --dur-menu: 650ms;
  --dur-page: 600ms;
  --ease-out: cubic-bezier(0.22, 1, 0.36, 1);
  --ease-inout: cubic-bezier(0.65, 0, 0.35, 1);
  --ease-expo: cubic-bezier(0.16, 1, 0.3, 1);

  /* — z-index scale — */
  --z-nav: 50;
  --z-menu: 80;
  --z-lightbox: 100;
  /* 120 was --z-transition, freed when the page-transition veil was removed;
     140 was --z-preloader, kept as the top of the scale for the skip link. */
  --z-top: 140;

  color-scheme: dark;
}

/* ---------------------------------------------------------------------------
   3. RESET / BASE
--------------------------------------------------------------------------- */
*,
*::before,
*::after { box-sizing: border-box; }

* { margin: 0; }

html {
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
}

/* Lenis drives smooth scrolling when JS runs; this is the no-JS fallback. */
html.no-js { scroll-behavior: smooth; }
html.lenis, html.lenis body { height: auto; }
.lenis.lenis-smooth { scroll-behavior: auto !important; }
.lenis.lenis-stopped { overflow: hidden; }

body {
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-sans);
  font-size: var(--fs-body);
  font-weight: 300;
  line-height: var(--leading-body);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  min-height: 100svh;
  overflow-x: clip;
}

img, picture, svg, video {
  display: block;
  max-width: 100%;
}

h1, h2, h3, h4 {
  font-family: var(--font-display);
  font-weight: 100; /* Thin display; pair with .serif-i (900) accents */
  line-height: var(--leading-tight);
  letter-spacing: 0.02em;
  text-transform: uppercase;
  text-wrap: balance;
}

p { text-wrap: pretty; }

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

button {
  font: inherit;
  color: inherit;
  background: none;
  border: 0;
  cursor: pointer;
}

::selection {
  background: var(--color-limestone);
  color: var(--color-ink);
}

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

.skip-link {
  position: fixed;
  top: -100%;
  left: var(--space-4);
  z-index: calc(var(--z-top) + 1);
  padding: var(--space-2) var(--space-4);
  background: var(--color-limestone);
  color: var(--color-ink);
  font-size: var(--fs-small);
  border-radius: var(--radius);
}
.skip-link:focus { top: var(--space-4); }

/* ---------------------------------------------------------------------------
   4. PRIMITIVES
--------------------------------------------------------------------------- */
.container {
  width: min(100% - 2 * var(--gutter), var(--container));
  margin-inline: auto;
}

.container--narrow {
  width: min(100% - 2 * var(--gutter), var(--container-narrow));
}

.eyebrow {
  font-family: var(--font-sans);
  font-size: var(--fs-tiny);
  font-weight: 500;
  letter-spacing: 0.32em;
  text-transform: uppercase;
  color: var(--text-faint);
}

/* Accent word inside thin display lines: Montserrat Black (was serif italic;
   class name kept so markup doesn't churn). */
.serif-i {
  font-family: var(--font-display);
  font-style: normal;
  font-weight: 900;
  letter-spacing: 0.01em;
}

/* Accent LINE or WORD inside a thin display heading — the block-level sibling
   of .serif-i. Used by the /gallery and /journal page titles (the whole accent
   line), the journal card titles and the article title (the last word).
   ⚠⚠ THIS RULE WAS DELETED BY ACCIDENT IN `4cf97ca` (the Exposure hero
   integration): it lived at the tail of the old §8 HERO block, which was
   removed wholesale, so four titles across three pages silently fell back to
   Thin — on every viewport. `git log -S` confirms it shipped from the initial
   commit until that one. Arturo spotted it the next day. 📌 When deleting a
   block of CSS, check for GENERIC rules that merely happened to live inside
   it. Weight only: letter-spacing is left to inherit, so the display titles
   keep their tight -0.02em and body-scale titles keep theirs. */
.line--black {
  font-weight: 900;
}

.btn {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
  min-height: 44px;
  padding: 0.7rem 1.6rem;
  font-family: var(--font-sans);
  font-size: var(--fs-small);
  font-weight: 500;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  border: 1px solid var(--border-strong);
  border-radius: 999px;
  cursor: pointer;
  transition: background var(--dur-micro) var(--ease-out),
              color var(--dur-micro) var(--ease-out),
              border-color var(--dur-micro) var(--ease-out);
}
.btn:hover {
  background: var(--color-limestone);
  border-color: var(--color-limestone);
  color: var(--color-ink);
}
.btn--solid {
  background: var(--color-limestone);
  border-color: var(--color-limestone);
  color: var(--color-ink);
}
.btn--solid:hover {
  background: transparent;
  color: var(--color-limestone);
}
/* Secondary action standing beside a primary .btn: identical pill geometry so
   the pair sits on one baseline, but a faint border and muted label so the eye
   still lands on the primary first. No hover rules needed — .btn:hover already
   out-specifies both properties, which is what keeps the row feeling like one
   control set rather than two unrelated buttons. */
.btn--quiet {
  border-color: var(--border);
  color: var(--text-muted);
}

/* Light-section button variant */
.on-light .btn { border-color: var(--light-border); color: var(--light-text); }
.on-light .btn:hover {
  background: var(--light-text);
  border-color: var(--light-text);
  color: var(--color-limestone);
}

/* Underline link with sliding line */
.link-line {
  position: relative;
  display: inline-block;
  padding-bottom: 2px;
  cursor: pointer;
}
.link-line::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: 0;
  width: 100%;
  height: 1px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: right;
  transition: transform var(--dur-micro) var(--ease-out);
}
.link-line:hover::after {
  transform: scaleX(1);
  transform-origin: left;
}

/* ---------------------------------------------------------------------------
   5. (removed 2026-07-24) PRELOADER — the intro page. The site now lands
   straight on the hero, and the logo lives there instead. Removed at Arturo's
   request: an intro is a cost paid on arrival, and a photograph is a better
   first impression than a loading counter.
--------------------------------------------------------------------------- */

/* ---------------------------------------------------------------------------
   6. (removed 2026-07-24) PAGE TRANSITION VEIL — a black panel that slid up
   over the page on every internal link click. Removed at Arturo's request:
   it clashed with the menu-close animation and delayed each navigation by
   550ms. Internal links are now plain, instant browser navigation.
--------------------------------------------------------------------------- */

/* ---------------------------------------------------------------------------
   7. NAVIGATION — slim fixed bar + fullscreen overlay menu.
--------------------------------------------------------------------------- */
.nav {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: var(--z-nav);
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--space-4) var(--gutter);
  mix-blend-mode: difference; /* wordmark stays legible over any photo */
  color: #e8e4dc;
}
.nav__brand {
  font-family: "Lemon Milk", var(--font-sans);
  font-weight: 300;
  font-size: 0.95rem;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  line-height: 1;
}
/* ⚠ THE WORDMARK WRAPPED TO TWO LINES ON EVERY PHONE. Measured 2026-08-01:
   it needs 264.6px on one line, and the bar offers 233.1px on a 360px phone
   (202.8px at 320px) once the Menu button is accounted for — the button is
   proportionally WIDER on a small screen, so the squeeze is worst exactly
   where there is least room.
   📌 TRACKING IS GIVEN BACK BEFORE SIZE, deliberately: "Before The Light Goes"
   is 21 characters, so 0.22em of letter-spacing is ~70px = a QUARTER of the
   whole width. Buying the line back out of the letter-space keeps the glyphs
   as large as they can be, which costs less of the wordmark's character than
   shrinking the type would. Lemon Milk stays airy, just less so.
   ⚠ `nowrap` is safe ONLY because the sizes below are measured to fit with
   real headroom — if this wordmark ever gets longer, re-measure rather than
   trusting it. */
@media (max-width: 30rem) {
  .nav__brand {
    font-size: 0.8rem;
    letter-spacing: 0.12em;
    white-space: nowrap;
  }
}
/* The narrow floor (iPhone SE and friends): 320px leaves the bar genuinely
   cramped, so the wordmark steps down once more rather than crowd the button. */
@media (max-width: 21.5rem) {
  .nav__brand {
    font-size: 0.68rem;
  }
}
.nav__menu-btn {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
  min-height: 44px;
  min-width: 44px;
  font-size: var(--fs-tiny);
  font-weight: 500;
  letter-spacing: 0.3em;
  text-transform: uppercase;
  cursor: pointer;
}
.nav__menu-btn .bars {
  display: inline-flex;
  flex-direction: column;
  gap: 5px;
  width: 22px;
}
.nav__menu-btn .bars span {
  height: 1px;
  background: currentColor;
  transition: transform var(--dur-micro) var(--ease-out);
}
.nav__menu-btn:hover .bars span:first-child { transform: translateY(-2px); }
.nav__menu-btn:hover .bars span:last-child { transform: translateY(2px); }

/* — Fullscreen menu overlay — */
.menu {
  position: fixed;
  inset: 0;
  z-index: var(--z-menu);
  background: var(--color-near-black);
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: var(--space-24) var(--gutter) var(--space-12);
  clip-path: inset(0 0 100% 0);
  visibility: hidden;
}
.menu.is-open { visibility: visible; }
html.no-js .menu:target,
html.no-js .menu.is-open { clip-path: none; visibility: visible; }

.menu__close {
  position: absolute;
  top: var(--space-4);
  right: var(--gutter);
  min-height: 44px;
  min-width: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: var(--fs-tiny);
  letter-spacing: 0.3em;
  text-transform: uppercase;
  color: var(--text-muted);
}
.menu__close:hover { color: var(--text); }

.menu__list {
  list-style: none;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: clamp(0.25rem, 1.5vh, 1rem);
}
.menu__link {
  display: flex;
  align-items: baseline;
  gap: var(--space-6);
  font-family: var(--font-display);
  font-size: clamp(2.4rem, 7.5vw, 5.5rem);
  line-height: 1.1;
  color: var(--text-muted);
  transition: color var(--dur-micro) var(--ease-out),
              letter-spacing var(--dur-micro) var(--ease-out);
  overflow: hidden;
}
.menu__link:hover,
.menu__link[aria-current="page"] { color: var(--text); }
.menu__link em {
  font-style: normal;
  font-family: var(--font-sans);
  font-size: var(--fs-tiny);
  font-weight: 500;
  letter-spacing: 0.3em;
  color: var(--text-faint);
}
.menu__link .menu__label { display: inline-block; }

.menu__footer {
  margin-top: var(--space-12);
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-6);
  justify-content: space-between;
  align-items: flex-end;
  font-size: var(--fs-small);
  color: var(--text-faint);
}

/* — (removed 2026-07-24) Menu photo previews. The open menu used to fade in a
   photo per hovered link on wide screens. Removed at Arturo's request: the
   menu should be a clean list with no distractions. — */

/* ---------------------------------------------------------------------------
   8. HERO — "Exposure": the signature over the drifting archive, then a
   scroll-driven shutter curtain wipes top→bottom to one full-bleed
   photograph, picked at random on every visit.

   Shipped 2026-07-28, replacing the "Signature Block" hero (full-bleed
   curated photograph + centred lockup + scroll parallax). That design is
   archived whole, with restore instructions, in
   design-archive/hero-v3-signature-block/. The two CANNOT both be live:
   several class names are reused here for different elements.

   LAYER ORDER IS THE WHOLE TRICK (see index.astro's header for the geometry):
     z1 .hero__opening — archive + lockup, NEVER masked
     z2 .hero__print   — the photograph, masked to what the curtain passed
     z3 .hero__curtain — the mechanism (a soft band, travelling top→bottom)

   Motion lives inline in index.astro, NOT in motion.js: it is a self-contained
   scrubbed timeline, and motion.js's initHero() (the old parallax) was
   retired with the design it belonged to.
--------------------------------------------------------------------------- */
.hero { position: relative; background: var(--bg); }
.hero__stage {
  position: relative;
  /* ⚠ FALLBACK ONLY — the hero script overwrites this with an exact pixel
     height from window.innerHeight (see the note beside setStageHeight() in
     index.astro). `100svh` is the height with every toolbar shown, which an
     iPad in landscape under-reports, leaving a grey band below the pinned
     stage. Kept as the pre-JS value because too SHORT merely shows that band
     for a moment, while too tall (100lvh/100vh) would push the scroll cue off
     the bottom of the screen on a phone. */
  height: 100svh;
  overflow: hidden;
  isolation: isolate;
  user-select: none;
  -webkit-user-select: none;
  background: #0b0b0b;
}

/* ── LAYER 1: the opening state ── */
.hero__opening { position: absolute; inset: 0; z-index: 1; }
.hero__space {
  position: absolute;
  inset: 0;
  overflow: hidden;
  perspective: 1000px;
  perspective-origin: 50% 45%;
  pointer-events: none;
  /* The script fades this whole layer out on the same ramp as the lockup.
     A group opacity between 0 and 1 forces an offscreen composite, so the
     hint is worth having here; the script also flips visibility to hidden
     once it reaches 0, which drops the buffer entirely. */
  will-change: opacity;
}
.hero__depth { position: absolute; inset: 0; transform-style: preserve-3d; }
.hero__plane {
  position: absolute;
  opacity: 0;
  will-change: transform, opacity;
  box-shadow: 0 18px 50px rgba(0, 0, 0, 0.5);
}
/* NO cropping and NO grayscale: the photograph keeps its own proportions AND
   its own colour. Width is set on the plane, height follows the ratio. */
.hero__plane img { display: block; width: 100%; height: auto; }

/* The lockup sits over moving photographs, so it gets its own ground.
   ui-ux-pro-max rates contrast failures High — this is the fix that does not
   put a hard panel behind the type. */
.hero__veil {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: radial-gradient(
    ellipse 62% 46% at 50% 50%,
    rgba(11, 11, 11, 0.82) 0%,
    rgba(11, 11, 11, 0.62) 45%,
    rgba(11, 11, 11, 0) 78%
  );
}

/* ── the signature (mirrors §15d) ── */
/* ⚠ STABLE COMPOSITED LAYER — this is the fix for the "vibrations on the
   text and logo". The stage is PINNED and scrubbed against Lenis smooth
   scroll, so its transform lands on fractional pixels every frame; text and
   a bitmap-masked logo inside a fractionally-transformed box get
   re-rasterised each time, which reads as a shimmer. Promoting the signature
   to its own layer (translateZ + backface-visibility) makes the glyphs
   rasterise ONCE and merely move. Nothing here scales.
   ⚠ `will-change: opacity` USED TO BE DECLARED HERE and is now set by the
   hero script only while the wipe is actually in motion. Left on permanently
   it pins the layer's raster for the life of the page — and a raster that has
   outlived a pin transform (or a phone's URL-bar resize) can be STRETCHED
   rather than redrawn, which made the signature come back oversized after
   scrolling down and up again. The shimmer only occurs during the scrub, so
   promoting for exactly that window keeps the fix and drops the side effect.
   See the matching note next to `signature.style.filter` in index.astro. */
.hero__signature {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
  transform: translateZ(0);
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
  -webkit-font-smoothing: antialiased;
  /* The entrance. A CSS animation rather than a GSAP tween, because the
     script writes this element's inline opacity every frame — an animation
     wins the cascade while it runs and then hands control straight back.
     `backwards` holds it hidden through the delay; there is deliberately no
     `forwards`, so afterwards the inline value governs. */
  animation: heroSigIn 0.9s ease-out 0.15s backwards;
}
@keyframes heroSigIn {
  from { opacity: 0; }
}
.hero__lockup {
  display: flex;
  align-items: stretch;
  gap: 0.34em;
  font-size: clamp(1.36rem, 6.8vw, 6.46rem);
  width: max-content;
}
.hero__mark {
  flex: 0 0 auto;
  width: 2.75em;
  background: url("/logo-mark.png") left center / contain no-repeat;
  filter: brightness(0.86);
}
@supports ((-webkit-mask-image: url("")) or (mask-image: url(""))) {
  .hero__mark {
    background: var(--text);
    -webkit-mask: url("/logo-mark.png") left center / contain no-repeat;
    mask: url("/logo-mark.png") left center / contain no-repeat;
    filter: none;
  }
}
/* Name + role column. The mark is align-items:stretch against THIS, so the
   logo keeps matching the full height of the text block. */
.hero__text {
  display: flex;
  flex-direction: column;
  justify-content: center;
  flex: 0 0 auto;
}
.hero__name {
  display: flex;
  flex-direction: column;
  justify-content: center;
  flex: 0 0 auto;
  font-family: var(--font-display);
  /* ⚠ THIN IS THE BASE, the given name overrides to Black (2026-08-01).
     Until then all three lines were 900 — a solid slab that was the ONLY
     display element on the site not using the Thin × Black contrast the rest
     runs on, and the heaviest thing on a phone screen. Weighting the SHORT
     line and letting the two long ones go light also gives the eye a step to
     land on. Both faces are already shipped and preloaded. */
  font-weight: 100;
  font-size: 1em;
  line-height: 0.98;
  letter-spacing: 0.015em;
  text-align: left;
  white-space: nowrap;
  color: var(--text);
  /* Carries the Thin lines over a bright photograph. ⚠ If the surname ever
     reads too faint, deepen .hero__veil — do NOT put a panel behind the text
     or a halo on it (that treatment was rejected on 2026-07-25). */
  text-shadow: 0 2px 24px rgba(0, 0, 0, 0.55);
}
.hero__name .hero__given {
  font-weight: 900;
}
/* "Photography" under the signature — the retired Print Signature hero's
   treatment, brought back on Arturo's ask (2026-07-28). ⚠ Sized on its own
   clamp, NOT in `em`: the lockup's font-size ranges 1.36→6.46rem (4.75×), so
   an em-based label would be 3px on a phone or a headline on a desktop. The
   text is whatever the CMS "Line below the name" field holds — empty hides it. */
.hero__role {
  font-family: var(--font-sans);
  font-size: clamp(0.62rem, 1.15vw, 1.05rem);
  /* ⚠ 300 to match .hero__cue ("Scroll to expose"), per Arturo 2026-08-01.
     He read this as a COLOUR mismatch; measured, both were already the same
     `--text-muted` (#b9b3a8) and always had been — it was WEIGHT (500 vs 300)
     plus a larger size laying down more ink, which reads as brighter. Keep
     these two in step: they are the only two muted labels on the hero.
     ℹ️ The cue's BASE weight is still 300 — only its first word is promoted to
     900 (.hero__cue-lead, 2026-08-01), which is a deliberate exception. */
  font-weight: 300;
  letter-spacing: 0.32em;
  text-transform: uppercase;
  color: var(--text-muted);
  margin-top: 0.5em;
  white-space: nowrap;
}

/* ── LAYER 2: the print. MASKED (not clipped) to the region the curtain has
   passed, so the reveal boundary is soft. JS drives only the two stops:
   opaque up to --e0, gone by --e1. A hard clip-path was the old behaviour;
   this gradient is the "fading effect in the transition". ── */
.hero__print {
  position: absolute;
  inset: 0;
  z-index: 2;
  background-size: cover;
  background-position: center;
  --e0: 0%;
  --e1: 0%;
  /* 180deg: the photograph is revealed from the TOP down (round 3). */
  -webkit-mask-image: linear-gradient(180deg, #000 0%, #000 var(--e0), transparent var(--e1), transparent 100%);
  mask-image: linear-gradient(180deg, #000 0%, #000 var(--e0), transparent var(--e1), transparent 100%);
}
/* <picture> is an inline box by default and would not fill the layer, so the
   img inside it would have nothing to be 100% of. */
.hero__print picture {
  display: block;
  width: 100%;
  height: 100%;
}
.hero__print img {
  display: block;
  width: 100%;
  height: 100%;
  /* HIS CHOICE: fill the screen. The edges crop; the source is capped at the
     photograph's own largest rendition in JS, so it is never upscaled. */
  object-fit: cover;
  opacity: 0;
  transition: opacity 0.4s ease;
}
.hero__print.is-loaded img { opacity: 1; }

/* The full-bleed, bottom-weighted vignette over the photograph. (The comment
   here used to describe this as "a verbatim copy of .hero__scrim" from a
   preview page — both that page and the rule it copied are gone; this is now
   the only one. Corrected 2026-07-28.)

   It lives INSIDE .hero__print deliberately, so the print's reveal mask
   carries the scrim with it — otherwise the vignette would sit over the
   opening state before the photograph had even arrived. */
.hero__print::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    linear-gradient(to top, rgba(12, 11, 10, 0.72) 0%, rgba(12, 11, 10, 0.28) 32%, rgba(12, 11, 10, 0.06) 58%, rgba(12, 11, 10, 0.22) 100%);
}

/* ── LAYER 3: the mechanism ── */
/* The band travels TOP → BOTTOM. ROUND 4: 56.25% tall — 50% more than round
   3's 37.5. Its own top and bottom edges are masked away over a long ramp
   (38%/62%), so the band has no hard boundary anywhere and the dissolve
   stays smooth. Keep this height in sync with CURTAIN_H in the page script —
   the two must agree or the geometry drifts. */
.hero__curtain {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 59.765625%; /* MUST equal CURTAIN_H in src/pages/index.astro */
  z-index: 3;
  background-color: #161616;
  transform: translateY(-100%); /* starts fully above the frame */
  will-change: transform;
  overflow: hidden;
  pointer-events: none;
  /* ROUND 6 — a SMOOTHSTEP ramp instead of a linear one.
     A plain two-stop gradient rises linearly and then stops dead where it
     meets the solid core. That corner in the rate of change is a crease the
     eye picks up as a hard line even though the tones either side of it are
     nearly identical (Mach banding) — which is what read as "not smooth".
     Smoothstep (3x² − 2x³) has ZERO slope at both of its endpoints, so it
     joins both the transparent end and the solid core tangentially: there
     is no corner anywhere in the band for the eye to find.

     The stops are sampled NON-UNIFORMLY — denser at both ends, where the
     curve flattens. That matters because CSS interpolates linearly BETWEEN
     stops, so an evenly-sampled smoothstep still lands a small corner where
     it meets the solid core; crowding the samples there follows the curve
     into the join. Worst slope change at a join is 0.007 per %, against
     0.026 for the plain two-stop ramp this replaces — 3.8× gentler.
     The grain sitting over it dithers what little banding remains. */
  -webkit-mask-image: var(--hero-band);
  mask-image: var(--hero-band);
  --hero-band: linear-gradient(
    180deg,
    rgba(0, 0, 0, 0) 0%,
    rgba(0, 0, 0, 0.0047) 1.68%,
    rgba(0, 0, 0, 0.0228) 3.78%,
    rgba(0, 0, 0, 0.0686) 6.72%,
    rgba(0, 0, 0, 0.1563) 10.5%,
    rgba(0, 0, 0, 0.2682) 14.28%,
    rgba(0, 0, 0, 0.3957) 18.06%,
    rgba(0, 0, 0, 0.5) 21%,
    rgba(0, 0, 0, 0.6043) 23.94%,
    rgba(0, 0, 0, 0.7318) 27.72%,
    rgba(0, 0, 0, 0.8438) 31.5%,
    rgba(0, 0, 0, 0.9314) 35.28%,
    rgba(0, 0, 0, 0.9772) 38.22%,
    rgba(0, 0, 0, 0.9953) 40.32%,
    rgba(0, 0, 0, 1) 42%,
    rgba(0, 0, 0, 1) 58%,
    rgba(0, 0, 0, 0.9953) 59.68%,
    rgba(0, 0, 0, 0.9772) 61.78%,
    rgba(0, 0, 0, 0.9314) 64.72%,
    rgba(0, 0, 0, 0.8438) 68.5%,
    rgba(0, 0, 0, 0.7318) 72.28%,
    rgba(0, 0, 0, 0.6043) 76.06%,
    rgba(0, 0, 0, 0.5) 79%,
    rgba(0, 0, 0, 0.3957) 81.94%,
    rgba(0, 0, 0, 0.2682) 85.72%,
    rgba(0, 0, 0, 0.1563) 89.5%,
    rgba(0, 0, 0, 0.0686) 93.28%,
    rgba(0, 0, 0, 0.0228) 96.22%,
    rgba(0, 0, 0, 0.0047) 98.32%,
    rgba(0, 0, 0, 0) 100%
  );
}
/* Same baked feTurbulence emulsion as §15e — rasterised once, then only
   translated, instead of a live SVG filter over a full-viewport element. */
.hero__grain {
  position: absolute;
  top: -100px;
  left: -100px;
  right: -100px;
  bottom: -100px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='300' height='300'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='matrix' values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.7 0'/%3E%3C/filter%3E%3Crect width='300' height='300' filter='url(%23g)'/%3E%3C/svg%3E");
  opacity: 0.18;
  pointer-events: none;
  will-change: transform;
}
.hero.is-live .hero__grain { animation: heroShiftGrain 0.4s steps(4) infinite; }

/* NOTE: there is deliberately no flash/light-leak layer here — it was removed
   at Arturo's request during the preview rounds. */

/* ⚠⚠ THIS SPANS THE FULL WIDTH AND CENTRES ITS OWN TEXT. It is NOT
   `left: 50%` + `translateX(-50%)` any more, and putting that back re-breaks
   every phone. WHY: an absolutely positioned box with `left` set and
   `width: auto` shrink-to-fits into "containing block minus left", so
   `left: 50%` gave this label only HALF the viewport to live in — 180px on a
   360px phone, against the ~195px the words actually need. It wrapped to two
   lines, and with no `text-align` those lines sat left-aligned inside that
   half-width box: measured 35px left of centre at 360px. Desktop looked
   perfect only because half of 1440px is still plenty, which is exactly what
   made this a phone-only bug. Reported by Arturo and measured 2026-08-01.
   📌 Deliberately NOT `white-space: nowrap`: at full width the line fits every
   real phone, and if the copy is ever lengthened it should wrap centred rather
   than push the page sideways. */
.hero__cue {
  position: absolute;
  z-index: 5;
  left: 0;
  right: 0;
  bottom: var(--space-16);
  text-align: center;
  font-size: var(--fs-small);
  letter-spacing: 0.24em;
  /* Tracking also adds a space after the LAST letter, which drags centred
     text half a space left; this pays it back so the line optically centres. */
  padding-left: 0.24em;
  text-transform: uppercase;
  color: var(--text-muted);
}
/* "Scroll" in Black against the label's 300 — the same Thin × Black contrast
   the rest of the hero runs on (see .hero__given). ⚠ Only Montserrat
   100/300/400/500/900 are shipped, so 900 is the only true bold available:
   a request for 600 or 700 would resolve up to this very face anyway. */
.hero__cue-lead {
  font-weight: 900;
}

/* Reduced motion / no GSAP: no pin, no scrub, no drift. The photograph is
   simply there with the lockup legible over it, so BOTH halves of the idea
   still land without any scroll-jacking. */
.hero--static .hero__curtain,
.hero--static .hero__space,
.hero--static .hero__cue { display: none; }
.hero--static .hero__print {
  z-index: 1;
  -webkit-mask-image: none !important;
  mask-image: none !important;
}
.hero--static .hero__opening { z-index: 2; background: transparent; }
@media (prefers-reduced-motion: reduce) {
  .hero__grain { animation: none !important; }
}

/* ---------------------------------------------------------------------------
   9. INTRO / STATEMENT SECTION (dark, after hero)
--------------------------------------------------------------------------- */
.statement {
  padding: var(--space-32) 0;
  background: var(--bg);
}
.statement__kicker { margin-bottom: var(--space-8); }
.statement__text {
  font-family: var(--font-display);
  font-weight: 100;
  font-size: clamp(1.6rem, 3.6vw, 3rem);
  line-height: 1.35;
  max-width: 26ch;
}
.statement__text .word { display: inline-block; }
.statement__more {
  margin-top: var(--space-12);
  display: flex;
  flex-wrap: wrap; /* the two pills stack rather than overflow on a phone */
  gap: var(--space-4);
}

/* ---------------------------------------------------------------------------
   10. SHOWCASE — pinned, scroll-scrubbed best-five sequence.
   JS pins .showcase and scrubs through .showcase__slide items.
   No-JS / reduced-motion fallback: slides stack as normal full-height figures.
--------------------------------------------------------------------------- */
.showcase {
  position: relative;
  background: var(--color-near-black);
}
.showcase__pin {
  position: relative;
  min-height: 100svh;
  overflow: clip;
}
.showcase__slide {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: flex-end;
  visibility: hidden;
}
.showcase__slide.is-active { visibility: visible; }
.showcase__media {
  position: absolute;
  inset: 0;
  overflow: clip;
}
.showcase__media img,
.showcase__media picture {
  width: 100%;
  height: 100%;
}
.showcase__media img { object-fit: cover; }
.showcase__media::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(to top, rgba(12, 11, 10, 0.72), transparent 55%);
}
.showcase__caption {
  position: relative;
  z-index: 1;
  width: 100%;
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--space-4) var(--space-8);
  padding: 0 var(--gutter) clamp(2.5rem, 7vh, 5rem);
}
.showcase__title {
  font-family: var(--font-display);
  font-weight: 100;
  font-size: var(--fs-h1);
  text-transform: uppercase;
  letter-spacing: 0.02em;
}
.showcase__title em {
  display: block;
  font-style: normal;
  font-size: var(--fs-tiny);
  font-weight: 500;
  letter-spacing: 0.3em;
  text-transform: uppercase;
  color: var(--text-muted);
  margin-bottom: var(--space-3);
}
.showcase__index {
  font-family: var(--font-sans);
  font-size: var(--fs-small);
  letter-spacing: 0.2em;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}
.showcase__progress {
  position: absolute;
  left: var(--gutter);
  right: var(--gutter);
  bottom: clamp(1.2rem, 3vh, 2rem);
  height: 1px;
  background: var(--border);
  z-index: 1;
}
.showcase__progress-bar {
  height: 100%;
  width: 100%;
  background: var(--color-limestone);
  transform: scaleX(0);
  transform-origin: left;
}
/* Header that introduces the showcase (scrolls before pin) */
.showcase__head {
  padding: var(--space-24) 0 var(--space-16);
}
.showcase__head h2 {
  font-size: var(--fs-h1);
  max-width: 14ch;
}
/* The route into the full gallery, AFTER the pinned sequence releases.
   Arturo, 2026-08-02: readers were finishing the six photographs and never
   reaching /gallery.
   ⚠ CENTRED, his call (2026-08-02) — it was right-aligned for one round, to
   follow the "06 / 06" counter the sequence ends on. Centred, the button is
   the only thing on an otherwise empty band, which is what makes it
   unmissable; it is no longer competing with anything for the eye.
   ⚠ `justify-content` is what does the alignment — do NOT swap it for
   `text-align`. This is a flex row, so text-align would not move the pill at
   all, and the rule would look correct while doing nothing. */
.showcase__more {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--space-4) var(--space-8);
  padding: var(--space-12) 0 var(--space-16);
}
.showcase__more-lead {
  font-size: var(--fs-lead);
  font-weight: 300;
  color: var(--text-muted);
}

/* No-JS / reduced-motion: linear stack, everything visible */
html.no-js .showcase__pin,
html.reduced-motion .showcase__pin { min-height: 0; }
html.no-js .showcase__slide,
html.reduced-motion .showcase__slide {
  position: relative;
  visibility: visible;
  min-height: 88svh;
}
html.no-js .showcase__progress,
html.reduced-motion .showcase__progress { display: none; }

/* ---------------------------------------------------------------------------
   11. ABOUT — the single limestone (light) break.
--------------------------------------------------------------------------- */
.about {
  background: var(--light-bg);
  color: var(--light-text);
  padding: var(--space-32) 0;
}
.about .eyebrow { color: var(--light-muted); }
.about__grid {
  display: grid;
  gap: var(--space-12);
}
@media (min-width: 900px) {
  .about__grid {
    grid-template-columns: 1.1fr 0.9fr;
    gap: clamp(3rem, 7vw, 7rem);
    align-items: start;
  }
}
.about__heading {
  font-size: clamp(2.4rem, 6vw, 5rem);
  margin-top: var(--space-6);
}
.about__heading em {
  font-style: normal;
  font-weight: 900;
}
.about__body p {
  max-width: var(--measure);
  color: var(--light-muted);
  font-size: var(--fs-lead);
  line-height: 1.65;
}
.about__body p + p { margin-top: var(--space-6); }
/* ⚠⚠ BOTH RULES BELONG TO THIS ELEMENT — that is the whole point, do not move
   the lower one onto .about__photo. The quote is ENCAPSULATED between two
   hairlines (Arturo, 2026-08-02), and they must be exactly the same length.
   The first attempt put the second rule on the portrait instead, which gave
   600px against 400px: the widths could not agree, because this box is capped
   in `ch` at the QUOTE's font-size while the portrait is capped in `rem` and
   inherits a smaller one. Border-top and border-bottom on the same box are
   identical by construction and stay identical if the measure is ever
   retuned. */
/* ⚠ THE MEASURE IS TUNED TO A TWO-LINE READ (Arturo, 2026-08-02: "I would
   like the text to cover only two lines"). At 30ch the quote broke over four
   lines and read as a paragraph rather than a line of verse. 44ch is what
   holds his 86-character sentence to two lines from ~760px up, MEASURED at
   1440 / 1280 / 1024 / 768 rather than estimated.
   ⚠⚠ IT IS CONTENT-COUPLED AND NOTHING CAN ENFORCE IT: the quote is a CMS
   field, so a longer sentence WILL take a third line. That is the honest
   trade — a cap tight enough to force two lines on any sentence would clip
   this one. Re-measure if he rewrites it.
   📌 On a phone two lines is physically impossible: 86 characters in ~330px
   needs roughly 9px type. It settles at three there, by arithmetic, not by a
   missing media query.
   📌 `balance` is what stops the second line becoming a two-word orphan; the
   browser evens the two lines out, so the shape reads as deliberate.
   ⚠ NOT italic any more (same ask). The display face carries the quote on
   weight and size; the italic was doing the same job twice and made the
   smaller size look weaker rather than quieter. */
.about__quote {
  margin-top: var(--space-16);
  padding-top: var(--space-8);
  padding-bottom: var(--space-8);
  border-top: 1px solid var(--light-border);
  border-bottom: 1px solid var(--light-border);
  font-family: var(--font-display);
  font-size: clamp(1.15rem, 2vw, 1.55rem);
  line-height: 1.4;
  max-width: 44ch;
  /* CENTRED IN THE COLUMN, to agree with the portrait below it (Arturo,
     2026-08-02). ⚠ `text-align`, NOT `margin-inline: auto` — the box is
     already as wide as the column at every width the two-column grid is on,
     so centring the BOX would move nothing at all. What reads as off-centre
     is the text inside it.
     📌 This pairs with `balance` above: balanced lines centred on each other
     is what makes two lines look composed rather than merely wrapped. */
  text-align: center;
  text-wrap: balance;
}
/* ⚠ CAPPED, and the cap is load-bearing. The portrait sits in the LEFT column
   under the quote (moved there 2026-08-01 to fill the hole that column used to
   leave). His portrait is SQUARE, so at full column width it rendered 675×675
   at 1440px and simply inverted the imbalance: measured left 1122 vs right 658.
   Balancing the two exactly would need a ~210px portrait, which reads as a
   thumbnail rather than a photograph of him, so this settles at 400px —
   measured gap down from 464px to ~190px, which is ordinary white space.
   📌 Re-measure if the bio grows: a longer bio lengthens the RIGHT column and
   the cap could then be raised. */
.about__photo {
  /* No border rule here: the hairline above the portrait is the quote's
     border-bottom, so the two always match. See .about__quote. */
  margin-top: var(--space-16);
  /* CENTRED IN THE COLUMN, not flush left (Arturo, 2026-08-02).
     ⚠ This deliberately breaks the left edge it used to share with the quote
     and the heading. That alignment was not load-bearing — the portrait is the
     one element in this column that is an object rather than text, and it now
     sits centred under the quote's lower hairline, which reads as framed
     rather than as a fourth left-aligned block. */
  margin-inline: auto;
  max-width: 25rem;
  overflow: clip;
}
.about__photo img { width: 100%; height: auto; }

/* ---------------------------------------------------------------------------
   11b. JOURNAL TEASER — the latest entry, between the showcase and contact.
   Added 2026-08-01: the journal's only route in was a quiet button in section
   01, which asks the reader to click with nothing to be curious about.
   ONE entry, shown large. See index.astro for why this is not a card grid.
--------------------------------------------------------------------------- */
.jteaser {
  background: var(--bg);
  padding: var(--space-32) 0;
}
.jteaser__heading {
  font-size: clamp(2.2rem, 5vw, 3.6rem);
  margin-top: var(--space-6);
}
.jteaser__heading em {
  font-style: normal;
  font-weight: 900;
}
/* ⚠ 68ch, NOT 52ch. Arturo asked for this to hold ONE LINE (2026-08-02). At
   52ch a 70-character sentence wrapped with most of the container unused —
   the same mistake as the gallery lead, which was capped at 50ch and fixed on
   2026-08-01. 68ch also keeps it inside the 65–75 measure the site reads at,
   so raising it costs nothing.
   📌 A cap cannot deliver one line on a PHONE: at 390px no useful sentence
   fits. This is a desktop guarantee; keep the wording short and it holds. */
.jteaser__lead {
  margin-top: var(--space-4);
  max-width: 68ch;
  font-size: var(--fs-lead);
  font-weight: 300;
  color: var(--text-muted);
}
/* The list. One row per entry, hairline-separated — chosen by Arturo from
   three options on 2026-08-01 after the single large teaser card was rejected
   ("looks very bad"). No panels, no shadows, no radii: the section is text and
   a small photograph on the page background, which is the whole site's idiom.
   ⚠ NEVER pad this list out by repeating an entry — his explicit restriction.
   index.astro slices, so one post renders exactly one row. */
.jlist {
  list-style: none;
  margin: var(--space-12) 0 0;
  padding: 0;
  border-top: 1px solid var(--border);
}
/* The ROW is the link, so the photograph, title and arrow all react together
   and the whole strip is the tap target — comfortably past 44px on its own. */
.jlist__row {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--space-6);
  padding: var(--space-6) 0;
  border-bottom: 1px solid var(--border);
  cursor: pointer;
}
.jlist__thumb {
  display: block;
  width: clamp(64px, 9vw, 104px);
  aspect-ratio: 1;
  overflow: clip;
}
.jlist__thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform var(--dur-reveal) var(--ease-out);
}
.jlist__row:hover .jlist__thumb img { transform: scale(1.05); }
.jlist__text { display: block; min-width: 0; }
.jlist__date {
  display: block;
  font-size: var(--fs-tiny);
  font-weight: 500;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--text-faint);
}
.jlist__title {
  display: block;
  margin-top: var(--space-2);
  font-family: var(--font-display);
  font-weight: 100;
  font-size: clamp(1.2rem, 2.2vw, 1.75rem);
  line-height: 1.2;
  color: var(--text);
  transition: color var(--dur-micro) var(--ease-out);
}
.jlist__desc {
  display: block;
  margin-top: var(--space-2);
  max-width: 60ch;
  font-size: var(--fs-small);
  font-weight: 300;
  line-height: var(--leading-body);
  color: var(--text-muted);
}
.jlist__arrow {
  font-size: var(--fs-body);
  color: var(--text-faint);
  transition: transform 450ms var(--ease-expo), color var(--dur-micro) var(--ease-out);
}
.jlist__row:hover .jlist__arrow {
  transform: translateX(0.3em);
  color: var(--text);
}
/* On a phone the arrow costs width the description needs, and the whole row
   is tappable anyway, so it goes rather than squeezing the text. */
@media (max-width: 33rem) {
  .jlist__row {
    grid-template-columns: auto minmax(0, 1fr);
    gap: var(--space-4);
  }
  .jlist__arrow { display: none; }
}
.jteaser__more { margin-top: var(--space-12); }

/* ---------------------------------------------------------------------------
   12. CONTACT — black closing section.
--------------------------------------------------------------------------- */
.contact {
  background: var(--color-near-black);
  padding: var(--space-32) 0;
}
.contact__heading {
  font-size: clamp(2.6rem, 8vw, 6.5rem);
  max-width: 12ch;
  margin-top: var(--space-6);
}
.contact__heading em { font-style: normal; font-weight: 900; }
.contact__lead {
  margin-top: var(--space-8);
  max-width: 44ch;
  color: var(--text-muted);
  font-size: var(--fs-lead);
}
.contact__grid {
  margin-top: var(--space-16);
  display: grid;
  gap: var(--space-16);
}
@media (min-width: 900px) {
  .contact__grid { grid-template-columns: 1.2fr 0.8fr; gap: var(--space-24); }
}

/* — Form — */
.form { display: grid; gap: var(--space-8); }
.form__row { display: grid; gap: var(--space-8); }
@media (min-width: 640px) { .form__row { grid-template-columns: 1fr 1fr; } }
.form__field { position: relative; }
.form__field label {
  display: block;
  font-size: var(--fs-tiny);
  font-weight: 500;
  letter-spacing: 0.28em;
  text-transform: uppercase;
  color: var(--text-faint);
  margin-bottom: var(--space-2);
}
.form__field input,
.form__field textarea {
  width: 100%;
  background: transparent;
  border: 0;
  border-bottom: 1px solid var(--border-strong);
  color: var(--text);
  font: inherit;
  font-weight: 300;
  padding: var(--space-3) 0;
  transition: border-color var(--dur-micro) var(--ease-out);
}
.form__field input:focus,
.form__field textarea:focus {
  outline: none;
  border-bottom-color: var(--color-limestone);
}
.form__field textarea { resize: vertical; min-height: 7rem; }
.form__status {
  font-size: var(--fs-small);
  color: var(--text-muted);
  min-height: 1.4em;
}
.form__status.is-error { color: #e2a59a; }
.form__status.is-success { color: #b9c9a8; }

.contact__elsewhere .eyebrow { margin-bottom: var(--space-6); }
.social {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}
.social--compact {
  flex-direction: row;
  gap: var(--space-4);
}
.social a {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
  min-height: 44px;
  font-size: var(--fs-body);
  color: var(--text-muted);
  transition: color var(--dur-micro) var(--ease-out);
}
.social a:hover { color: var(--text); }
.social svg { width: 18px; height: 18px; flex: none; }

/* ---------------------------------------------------------------------------
   13. GALLERY — filterable editorial grid + hover meta.
--------------------------------------------------------------------------- */
.page-head {
  padding: calc(var(--space-32) + var(--space-8)) 0 var(--space-12);
}
.page-head h1 {
  font-size: var(--fs-display);
  letter-spacing: -0.02em;
}
.page-head h1 .line {
  display: block;
  overflow: hidden;
  padding-bottom: 0.14em;
  margin-bottom: -0.14em;
}
.page-head h1 .line > span { display: inline-block; }
.page-head p {
  margin-top: var(--space-6);
  /* ⚠ 58ch, raised from 50ch on 2026-08-01. The gallery lead is 69 characters
     and the 50ch cap forced it onto two lines with three words stranded on the
     second — with 1328px of container sitting unused beside it. 50ch was also
     BELOW the 65–75 character measure the rest of the site aims at, so this is
     a readability fix as much as a typographic one. Still narrower than the
     phone viewport, so short viewports are unaffected. */
  max-width: 58ch;
  color: var(--text-muted);
  font-size: var(--fs-lead);
}

.filters {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-6);
  padding: var(--space-6) 0;
  border-top: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
  margin-bottom: var(--space-12);
}
.filters button {
  min-height: 44px;
  padding: 0 var(--space-2);
  font-size: var(--fs-tiny);
  font-weight: 500;
  letter-spacing: 0.26em;
  text-transform: uppercase;
  color: var(--text-faint);
  transition: color var(--dur-micro) var(--ease-out);
  cursor: pointer;
}
.filters button:hover { color: var(--text); }
.filters button[aria-pressed="true"] { color: var(--text); }
.filters button[aria-pressed="true"]::after {
  content: "";
  display: block;
  height: 1px;
  background: var(--color-limestone);
}

/* — "Countries": the one chip that opens a menu of sub-tags (2026-07-28) —
   Deliberately NOT a new visual language. The chip and every sub-tag are the
   same <button> inside .filters, so all the typography, the 44px touch target,
   the hover and the pressed underline above apply to them unchanged; the only
   new paint is the popover panel itself. */
.filters__group {
  position: relative;
  display: flex;
}
.filters__toggle {
  display: inline-flex;
  align-items: center;
  /* Half of the bar's own column gap, so the caret reads as part of the word
     rather than as another chip. The 0.26em tracking already pads the right. */
  gap: var(--space-1);
}
.filters__caret {
  flex: none;
  transition: transform var(--dur-micro) var(--ease-out);
}
.filters__toggle[aria-expanded="true"] .filters__caret { transform: rotate(180deg); }

.filters__menu {
  position: absolute;
  top: 100%;
  left: 0;
  z-index: var(--z-nav);
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  min-width: 100%;
  padding: var(--space-2) var(--space-4);
  background: var(--color-near-black);
  border: 1px solid var(--border);
}
/* [hidden] loses to the display above without this — the menu would sit open
   permanently, and on the no-JS page there is nothing to close it. */
.filters__menu[hidden] { display: none; }
.filters__menu button {
  width: 100%;
  text-align: left;
  white-space: nowrap;
}

/* Masonry columns (aesthetic per 21st.dev efferd/image-gallery): stacked
   mixed-ratio images, soft rounded corners + hairline border, even gaps,
   slow fade-in. Details/meta live in the lightbox; hover shows a whisper. */
.work-grid {
  columns: 1;
  column-gap: var(--space-6);
  padding-bottom: var(--space-32);
}
@media (min-width: 640px) { .work-grid { columns: 2; } }
@media (min-width: 1100px) { .work-grid { columns: 3; } }

.work-card {
  position: relative;
  break-inside: avoid;
  margin-bottom: var(--space-6);
  border-radius: 10px;
  border: 1px solid var(--border);
  overflow: clip;
  background: var(--bg-elevated);
  cursor: pointer;
  transition: border-color 350ms var(--ease-out);
}
.work-card:hover { border-color: var(--border-strong); }
.work-card.is-filtered-out { display: none; }
.work-card__media img {
  width: 100%;
  height: auto;
  transform: scale(1.001);
  transition: transform 1100ms var(--ease-expo);
}
.work-card:hover .work-card__media img { transform: scale(1.04); }

/* Hover whisper: title + location over a soft gradient at the photo's foot */
.work-card__meta {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-4);
  padding: var(--space-8) var(--space-4) var(--space-3);
  background: linear-gradient(to top, rgba(12, 11, 10, 0.72), transparent);
  pointer-events: none;
}
.work-card__title {
  font-family: var(--font-display);
  font-size: 1.1rem;
  letter-spacing: 0;
  color: var(--color-limestone);
}
.work-card__loc {
  font-size: var(--fs-tiny);
  font-weight: 500;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--text-muted);
  white-space: nowrap;
}
@media (hover: hover) {
  .work-card__meta {
    opacity: 0;
    transform: translateY(8px);
    transition: opacity 350ms var(--ease-out), transform 350ms var(--ease-out);
  }
  .work-card:hover .work-card__meta,
  .work-card:focus-within .work-card__meta {
    opacity: 1;
    transform: none;
  }
}

/* ---------------------------------------------------------------------------
   14. LIGHTBOX
--------------------------------------------------------------------------- */
.lightbox {
  position: fixed;
  inset: 0;
  z-index: var(--z-lightbox);
  background: var(--backdrop);
  display: none;
  align-items: center;
  justify-content: center;
  padding: clamp(1rem, 4vw, 3rem);
}
.lightbox.is-open { display: flex; }
.lightbox__figure {
  max-width: min(1500px, 100%);
  max-height: 100%;
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}
.lightbox__img-wrap {
  position: relative; /* anchors .lightbox__shield */
  min-height: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}
.lightbox__img-wrap img {
  max-width: 100%;
  max-height: min(78svh, 1000px);
  width: auto;
  height: auto;
  box-shadow: var(--shadow-soft);
}
/* Transparent overlay so a right-click / drag lands on the shield, not on the
   image. Mirrors .photo__shield on the grid photos. */
.lightbox__shield {
  position: absolute;
  inset: 0;
  background: transparent;
}
.lightbox__caption {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: var(--space-2) var(--space-8);
  font-size: var(--fs-small);
  color: var(--text-muted);
}
.lightbox__caption strong {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: 1.1rem;
  color: var(--text);
}
.lightbox__story {
  max-width: 70ch;
  font-size: var(--fs-small);
  color: var(--text-faint);
}
.lightbox__close,
.lightbox__prev,
.lightbox__next {
  position: absolute;
  min-width: 44px;
  min-height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text-muted);
  font-size: var(--fs-small);
  letter-spacing: 0.2em;
  text-transform: uppercase;
  transition: color var(--dur-micro) var(--ease-out);
  cursor: pointer;
}
.lightbox__close:hover,
.lightbox__prev:hover,
.lightbox__next:hover { color: var(--text); }
.lightbox__close { top: var(--space-4); right: var(--gutter); }
.lightbox__prev { left: var(--space-2); top: 50%; transform: translateY(-50%); }
.lightbox__next { right: var(--space-2); top: 50%; transform: translateY(-50%); }

/* ---------------------------------------------------------------------------
   15. JOURNAL — shared card details. The index itself is the "Scroll Stack"
   further down this section; these two rules are all that is left of the
   fanned JournalCard, which was REMOVED on 2026-07-28 along with the article
   pages' "More from the journal" strip: with a short journal that strip only
   ever repeated the entry the prev/next bar links directly above it, and on a
   phone it cost a full screen to do so (Arturo). Both rules are still used by
   the scroll-stack cards. The removed component and its fan CSS are in git at
   d440b73 if a future, longer journal wants a discovery strip back — the plan
   then is a MINIMAL LIST (small cover + date + title), not the fanned stack.
--------------------------------------------------------------------------- */
/* Same transparent overlay the gallery photos carry (deterrence bundle) —
   inside the <a>, so clicks still navigate. */
.jcard__shield {
  position: absolute;
  inset: 0;
  z-index: 5;
}

.jcard__eyebrow {
  font-size: var(--fs-tiny);
  font-weight: 500;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--text-faint);
}
/* "In English" / "En español" on an index row whose article is not available
   in the page's language. Brighter than the rest of the eyebrow on purpose:
   it is the one thing in that line that changes what the reader is about to
   get, so it must not read as another tag. */
.jcard__lang {
  margin-left: var(--space-2);
  color: var(--text-muted);
}

/* — Article navigation (prev · Journal · next), above and below the post.
     Three slots on a 1fr auto 1fr grid so "Journal" stays optically centred
     even when an end is missing (first/last entry render an empty slot).
     Every target clears 44px. — */
.artnav {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: var(--space-4);
}
.artnav--top {
  padding-bottom: var(--space-6);
  border-bottom: 1px solid var(--border);
}
.artnav--bottom {
  margin-top: var(--space-16);
  padding-top: var(--space-8);
  border-top: 1px solid var(--border);
  align-items: start;
}
.artnav__side,
.artnav__home {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: var(--space-2);
  min-height: 44px;
}
a.artnav__side,
.artnav__home { cursor: pointer; }
.artnav__side--next { text-align: right; }
.artnav__home {
  justify-self: center;
  text-align: center;
}
.artnav__label,
.artnav__home {
  font-size: var(--fs-tiny);
  font-weight: 500;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--text-faint);
  transition: color var(--dur-micro) var(--ease-out);
}
a.artnav__side:hover .artnav__label,
.artnav__home:hover { color: var(--text); }
.artnav__label span[aria-hidden] {
  display: inline-block;
  transition: transform 450ms var(--ease-expo);
}
a.artnav__side--prev:hover .artnav__label span[aria-hidden] { transform: translateX(-0.3em); }
a.artnav__side--next:hover .artnav__label span[aria-hidden] { transform: translateX(0.3em); }
.artnav__title {
  font-family: var(--font-display);
  font-size: var(--fs-body);
  line-height: 1.35;
  color: var(--text-muted);
  transition: color var(--dur-micro) var(--ease-out);
}
a.artnav__side:hover .artnav__title { color: var(--text); }

/* ⚠⚠ READ BEFORE CHANGING — THIS HAS BEEN WRONG TWICE.
   Below 640px three equal thirds squeeze the titles unreadably, so the bar
   re-flows. The FIRST attempt stacked all three as full-width rows and flipped
   "Next" to the left for one tidy left edge; Arturo rejected that, because an
   arrow that points right while sitting on the left contradicts itself. The
   SECOND attempt kept the stack and only right-aligned Next, which produced
   three rows aligned left / left / right — his words, "a staircase, very
   horrible", and he was right.
   ✅ THE FIX IS TO STOP STACKING ALL THREE. Previous and Next share ONE row, so
   they sit on a common baseline and keep their left/right meaning; "Back to
   the Journal" spans the full width beneath them, centred as it is on desktop.
   📌 Placed by :nth-child, NOT by the modifier classes: when an end of the
   chain is missing the component renders an EMPTY <span> in that slot, so
   position must come from DOM order or a lone "Next" would drift left. */
@media (max-width: 639px) {
  .artnav--bottom {
    grid-template-columns: 1fr 1fr;
    gap: var(--space-8) var(--space-6);
    align-items: start;
  }
  .artnav--bottom > :nth-child(1) {
    grid-column: 1;
    grid-row: 1;
    text-align: left;
  }
  .artnav--bottom > :nth-child(3) {
    grid-column: 2;
    grid-row: 1;
    text-align: right;
  }
  .artnav--bottom > :nth-child(2) {
    grid-column: 1 / -1;
    grid-row: 2;
    justify-self: center;
    text-align: center;
  }
  /* A narrower column needs a slightly smaller title, or a three-word entry
     becomes a four-line block beside a one-line one. */
  .artnav--bottom .artnav__title { font-size: var(--fs-small); }
}

/* — Scroll stack (the /journal index — CHOSEN 2026-07-26 from three live
     candidates): full-width sticky cards that overlap while scrolling.
     Pure CSS: each card pins below the nav (cascading a little per index via
     --k, capped at 3 in the markup so a long journal never pushes cards down
     the viewport) and the next entry, later in the DOM, simply slides over
     it. The card surface must stay OPAQUE (--bg-elevated) or the covered
     card would ghost through. The media pane is 16:9 — wide, cinematic,
     kind to panos — and it is what sets the card's height. — */
.jstack {
  display: flex;
  flex-direction: column;
  gap: var(--space-12);
  padding-bottom: var(--space-32);
}
.jstack__card {
  position: sticky;
  top: calc(5.5rem + var(--k, 0) * 0.85rem);
}
.jstack__card a {
  display: grid;
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-soft);
  overflow: hidden;
  cursor: pointer;
  transition: border-color var(--dur-micro) var(--ease-out);
}
.jstack__card a:hover { border-color: var(--border-strong); }
@media (min-width: 900px) {
  .jstack__card a { grid-template-columns: 5fr 7fr; }
}
.jstack__media {
  position: relative;
  aspect-ratio: 16 / 9; /* wide + cinematic; also drives the card height */
  order: -1; /* photo first on mobile */
}
@media (min-width: 900px) { .jstack__media { order: 1; } }
.jstack__media picture {
  position: absolute;
  inset: 0;
}
.jstack__media img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
  transition: transform 900ms var(--ease-out);
}
.jstack__card a:hover .jstack__media img { transform: scale(1.03); }
.jstack__text {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: var(--space-4);
  padding: clamp(1.5rem, 4vw, 3.5rem);
}
.jstack__title {
  font-size: clamp(1.7rem, 3.4vw, 3rem);
  line-height: var(--leading-tight);
  transition: transform 450ms var(--ease-expo);
}
.jstack__card a:hover .jstack__title { transform: translateX(0.35em); }
.jstack__desc {
  max-width: 48ch;
  font-size: var(--fs-small);
  line-height: 1.65;
  color: var(--text-muted);
}
.jstack__cta {
  margin-top: var(--space-2);
  font-size: var(--fs-tiny);
  font-weight: 500;
  letter-spacing: 0.26em;
  text-transform: uppercase;
  color: var(--text-faint);
}


/* — Post (reading) layout — */
.post {
  padding: calc(var(--space-32) + var(--space-8)) 0 var(--space-32);
}
.post__head { margin-bottom: var(--space-16); }
.post__head h1 {
  font-size: clamp(2.4rem, 6vw, 4.5rem);
  max-width: 16ch;
  margin-top: var(--space-4);
}
.post__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-6);
  margin-top: var(--space-6);
  font-size: var(--fs-tiny);
  font-weight: 500;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--text-faint);
}
/* ── LANGUAGE SWITCH (bilingual articles, 2026-08-02) ──
   Rendered only when the article exists in two languages, so it is never a
   control that does nothing. `margin-left: auto` puts it at the far right of
   the meta line, opposite the date — a language choice is not metadata about
   the article, so it should not read as another item in that list.
   ⚠⚠ THESE ARE <button>s, NOT LINKS. The swap happens inside the page; there
   is no second URL. That is why the reset below is needed at all — a button
   inherits none of the page's type and brings its own chrome.
   ⚠ The CURRENT language keeps the hover treatment OFF via `:not(.is-current)`
   rather than by being a different element: both are buttons now, so the
   distinction has to be a state, not a tag. */
.post__lang {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  margin-left: auto;
  letter-spacing: 0.18em;
}
.post__lang-item {
  display: inline-flex;
  align-items: center;
  /* 44px is the touch-target floor these chips would otherwise miss badly —
     the meta type is --fs-tiny, which on its own gives a ~16px tall target. */
  min-height: 44px;
  padding: 0 0.75rem;
  /* the button reset: inherit the meta line's type rather than the UA's */
  font: inherit;
  letter-spacing: inherit;
  background: none;
  cursor: pointer;
  border: 1px solid var(--border);
  border-radius: 999px;
  color: var(--text-muted);
  transition: background var(--dur-micro) var(--ease-out),
              color var(--dur-micro) var(--ease-out),
              border-color var(--dur-micro) var(--ease-out);
}
.post__lang-item:not(.is-current):hover {
  background: var(--color-limestone);
  border-color: var(--color-limestone);
  color: var(--color-ink);
}
.post__lang-item.is-current {
  border-color: var(--border-strong);
  color: var(--text);
  cursor: default;
}

/* The two language bodies. Only one is ever shown; `hidden` does the hiding,
   and this makes that explicit so no future `display` rule on .post__text can
   quietly out-specify the attribute and reveal both at once. */
.post__text[hidden],
[data-lang-part][hidden] { display: none; }
/* ── End-of-article invitation to get in touch ──
   Constrained to the same `--measure` as .prose so it reads as the last beat
   of the article rather than a banner bolted underneath it, and centred so it
   is clearly no longer body copy. The rule above it does the separating —
   a filled panel would have made it look like an ad. */
.post__invite {
  max-width: var(--measure);
  margin-top: var(--space-16);
  padding-top: var(--space-12);
  border-top: 1px solid var(--border);
  text-align: center;
}
.post__invite-lead {
  font-family: var(--font-display);
  font-weight: 100;
  font-size: var(--fs-h3);
  color: var(--text);
}
.post__invite-text {
  margin: var(--space-4) auto var(--space-8);
  max-width: 46ch;
  font-size: var(--fs-body);
  font-weight: 300;
  line-height: var(--leading-body);
  color: var(--text-muted);
}

.prose {
  max-width: var(--measure);
  font-size: var(--fs-lead);
  font-weight: 300;
  line-height: 1.75;
  color: var(--text-muted);
}
.prose h2, .prose h3 {
  color: var(--text);
  margin: var(--space-12) 0 var(--space-4);
}
.prose p + p { margin-top: var(--space-6); }
.prose a {
  color: var(--text);
  text-decoration: underline;
  text-underline-offset: 0.2em;
  text-decoration-color: var(--border-strong);
}
.prose a:hover { text-decoration-color: var(--text); }
.prose img { margin: var(--space-12) 0; }
/* ⚠ …but NEVER on a <Photo>. Its <img> fills a wrapper that has a fixed
   aspect-ratio + overflow:clip, so a margin here pushes the photograph down
   INSIDE its own frame — the placeholder shows as a band across the top and
   the bottom edge is clipped off (reported on "Form, Without a Name",
   2026-07-26). Space the WRAPPER instead; the photo keeps its own ratio. */
.prose .photo__img { margin: 0; }
.prose .photo { margin: var(--space-12) 0; }
.inline-photo { margin: var(--space-12) 0; }
.inline-photo img { width: 100%; height: auto; }
.inline-photo figcaption {
  margin-top: var(--space-3);
  font-size: var(--fs-small);
  color: var(--text-faint);
}
.prose blockquote {
  margin: var(--space-12) 0;
  padding-left: var(--space-6);
  border-left: 1px solid var(--border-strong);
  font-family: var(--font-display);
  font-style: italic;
  font-size: 1.3em;
  color: var(--text);
}
/* Drop cap on the opening paragraph only. Ghost/thin treatment: an
   oversized, low-contrast Montserrat Thin letter — the same Thin ×
   Black display contrast the rest of the site runs on (see FONTS
   above), turned all the way down so the letter almost dissolves.
   `:first-of-type` targets the first <p> among .prose's children, so
   a <Photo>/<Figure> or blockquote before it does not break this.
   ⚠ OPT-IN via `prose--article`. This was plain `.prose` until 2026-08-01,
   and `.prose` has exactly TWO users — the journal article and /privacy — so
   the drop cap was also blowing up the "B" of "Before The Light Goes" at the
   top of the privacy policy (Arturo spotted it). A drop cap is an editorial
   flourish for a story; a legal page has to read as plain text. The article
   now asks for the treatment instead of every .prose inheriting it. */
/* ⚠⚠ THE SELECTOR REACHES ONE LEVEL DEEPER SINCE 2026-08-02. An article body
   is now wrapped in a `[data-lang-part]` element (one per language, all but
   one `hidden`), so `.prose--article > p` matches NOTHING at all. The wrapper
   is emitted even for a single-language article precisely so this stays one
   rule rather than two that can disagree. */
.prose--article > [data-lang-part] > p:first-of-type::first-letter {
  font-family: var(--font-sans);
  font-weight: 100;
  color: color-mix(in srgb, var(--text) 40%, transparent);
  float: left;
  font-size: 4.5em;
  line-height: 0.72;
  padding-right: 0.06em;
  margin-top: -0.03em;
}

/* ---------------------------------------------------------------------------
   16. FOOTER
--------------------------------------------------------------------------- */
.footer {
  border-top: 1px solid var(--border);
  background: var(--color-near-black);
  padding: var(--space-12) 0;
}
.footer__inner {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-6) var(--space-12);
  justify-content: space-between;
  align-items: baseline;
}
/* Footer wordmark mirrors the nav brand exactly (Lemon Milk Light). */
.footer__brand {
  font-family: "Lemon Milk", var(--font-sans);
  font-weight: 300;
  font-size: 0.95rem;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  line-height: 1;
}
.footer__links {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-6);
  font-size: var(--fs-small);
  color: var(--text-muted);
}
.footer__links a:hover { color: var(--text); }
.footer__fine {
  width: 100%;
  font-size: var(--fs-tiny);
  letter-spacing: 0.1em;
  color: var(--text-faint);
}

/* ---------------------------------------------------------------------------
   17. REVEALS — elements enter as they scroll into view.
   JS (GSAP) animates [data-reveal]; these are the resting/fallback states.
--------------------------------------------------------------------------- */
[data-reveal] { opacity: 0; transform: translateY(28px); }
/* "fade" variant: pure slow opacity, no movement (masonry gallery) */
[data-reveal="fade"] { transform: none; }
html.no-js [data-reveal],
html.reduced-motion [data-reveal] {
  opacity: 1;
  transform: none;
}
[data-reveal].is-revealed { opacity: 1; transform: none; }

/* Split-line reveal targets (hero name, page heads) */
[data-split] .line { overflow: hidden; }
[data-split] .line > span {
  display: inline-block;
  transform: translateY(110%);
}
html.no-js [data-split] .line > span,
html.reduced-motion [data-split] .line > span { transform: none; }
[data-split].is-revealed .line > span { transform: none; }

/* ---------------------------------------------------------------------------
   18. PHOTO PRIMITIVE — responsive <picture> wrapper (lightbox-ready).
--------------------------------------------------------------------------- */
.photo {
  position: relative;
  display: block;
  width: 100%;
  padding: 0;
  border: 0;
  overflow: clip;
  background: var(--bg-elevated);
  text-align: left;
}
button.photo { cursor: pointer; }
.photo__img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.photo__shield {
  position: absolute;
  inset: 0;
  background: transparent;
}
/* Long-press deterrent — the mobile half of the bundle. Without this, iOS and
   Android still offer "Save Image"/"Copy" on a long press, which is the most
   common casual-copy route of all; every other deterrent here is desktop-only.
   Scoped to <img> ON PURPOSE: it never touches text, so captions, the bio and
   the journal stay fully selectable and copyable. Every <img> on this site is
   a photograph (the logo mark is a CSS background), so this needs no upkeep
   when new images are added.
   ⚠ Deterrence, not prevention — screenshots, devtools and view-source remain,
   as accepted in decision 0.2. */
img {
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
}
.photo--missing {
  display: grid;
  place-items: center;
  color: var(--text-faint);
  font-size: var(--fs-small);
  border: 1px dashed var(--border);
}

/* — Form field validation states — */
.form__field .field__error {
  display: none;
  font-size: var(--fs-small);
  color: #e2a59a;
  margin-top: var(--space-2);
}
.form__field[data-invalid="true"] .field__error { display: block; }
.form__field[data-invalid="true"] input,
.form__field[data-invalid="true"] textarea { border-bottom-color: #e2a59a; }
.form__field .hint {
  display: block;
  font-size: var(--fs-tiny);
  letter-spacing: 0.04em;
  color: var(--text-faint);
  margin-top: var(--space-2);
}
.form__status[data-state="error"] { color: #e2a59a; }
.form__status[data-state="success"] { color: #b9c9a8; }

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

/* ---------------------------------------------------------------------------
   19. REDUCED MOTION — the hard rule: everything readable, nothing required.
--------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
  html { scroll-behavior: auto; }
}
