/* --------------------------------------------------------------------------
   0. Fraunces, self-hosted.

   Previously loaded from fonts.googleapis.com, which cost every visitor two
   extra DNS + TCP + TLS handshakes (one for the stylesheet, another to
   fonts.gstatic.com for the file) before a single glyph began downloading.
   Serving it from our own origin reuses the connection that already fetched
   the HTML, and lets the roman face be preloaded from <head>.

   The unicode-range split is Google's own: a browser downloads latin-ext only
   if the page actually uses a character from it, so English pages pay for
   latin alone. The vietnamese subset is deliberately not shipped.

   Licensed under the SIL Open Font License -- see fonts/OFL.txt.
   -------------------------------------------------------------------------- */
@font-face {
  font-family: 'Fraunces';
  font-style: normal;
  font-weight: 300 500;
  font-display: swap;
  src: url('../fonts/fraunces-normal-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Fraunces';
  font-style: normal;
  font-weight: 300 500;
  font-display: swap;
  src: url('../fonts/fraunces-normal-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Fraunces';
  font-style: italic;
  font-weight: 300 500;
  font-display: swap;
  src: url('../fonts/fraunces-italic-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Fraunces';
  font-style: italic;
  font-weight: 300 500;
  font-display: swap;
  src: url('../fonts/fraunces-italic-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* --------------------------------------------------------------------------
   0b. Cross-document view transitions.

   This is a multi-page site, so every click previously tore the document down
   and built a new one, flashing white in between -- it read as the whole site
   reloading rather than one section changing. Both the outgoing and incoming
   document must opt in, which is why this lives in the shared stylesheet.

   Naming the chrome makes the browser treat those elements as the *same* thing
   across the navigation, so they hold still while only the content changes.
   Everything unnamed cross-fades under the root transition.
   -------------------------------------------------------------------------- */
@view-transition { navigation: auto; }

/* Only name what genuinely occupies the SAME place on every page. A named
   element is morphed from its old position to its new one, so naming something
   in normal flow makes it fly across the screen: the footer sits at y=1628 on
   the FAQ and y=4802 on the home page, and naming it animated that whole
   3000px journey on every navigation. Both of these are position: fixed at an
   identical offset on all five pages, so they simply hold still. */
.promo-strip { view-transition-name: promo-strip; }
.site-header { view-transition-name: site-header; }

/* The default root animation cross-fades the two pages, which is wrong here:
   the grounds are peach, sage, dark and ink, and the incoming snapshot paints
   ON TOP of the outgoing one, so any overlap shows one ground through the
   other. Between peach and ink that midpoint reads as a flash.

   The fix is to never have both pages visible at once. These two animations
   are SEQUENCED, not overlapping: the outgoing content leaves first, and the
   incoming content only begins at the moment the outgoing one is fully gone
   (in-delay == out-duration). In the handover frame what shows is the backdrop
   -- which is the incoming page's own ground, so the eye reads a clean dip to
   the destination colour instead of a blend of two.

   The named chrome is captured separately, so `root` here is effectively the
   content area: the promo strip, header and footer hold still throughout.

   The small translate is what makes it read as movement rather than a blink;
   it is deliberately tiny, because the chrome is stationary and a large shift
   would fight it. */
@keyframes vt-content-out {
  to { opacity: 0; transform: translateY(-10px); }
}
@keyframes vt-content-in {
  from { opacity: 0; transform: translateY(12px); }
}

/* Kept deliberately short. A cross-document transition does not begin until the
   incoming document is renderable, so its duration is ADDED to however long the
   browser already spent holding the old page frozen. 400ms of animation on top
   of that freeze read as lag. The delay on -new must keep matching the duration
   of -out, or the two pages overlap and the flash returns. */
::view-transition-old(root) {
  animation: vt-content-out 90ms cubic-bezier(0.4, 0, 1, 1) both;
}
::view-transition-new(root) {
  animation: vt-content-in 170ms 90ms cubic-bezier(0, 0, 0.2, 1) both;
}

@media (prefers-reduced-motion: reduce) {
  @view-transition { navigation: none; }
}

/* ==========================================================================
   Alyssa Caples Photography — main.css
   Extracted from prototype.html. Palette + type scale locked per CLAUDE.md.
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. Palette
   -------------------------------------------------------------------------- */
:root {
  --pink: #ffcdb2;         /* Warm peach — page ground, about section */
  --sage: #849678;         /* Offering ground (lifted 4% so ink body copy hits 4.7:1) */
  --sage-deep: #567568;    /* Swirl marks on light grounds, offering CTA hover */
  --sage-ink: #486257;     /* Accent type on LIGHT grounds — 4.6:1 on peach */
  --mauve: #c9b1bd;        /* Reserve — no longer used on the home page */
  --blue-soft: #d5dfe5;    /* ACCENT — small pops only */
  --rose: #b49594;         /* Reserve accent — currently unused */
  --cream: #fefae0;        /* Light type on dark grounds, hero type */
  --ink: #2a2622;          /* Primary text, dark elements */
  --dark: #1e2323;         /* Gallery ground, hero overlay */
  --star: #f08454;         /* Sampled from the hand-drawn star (inner is #f06c60) */
  /* Chrome scales with the viewport instead of sitting at one fixed size —
     at 1920 a 16px nav read as tiny next to a 112px hero title. */
  --promo-h: clamp(46px, 3.1vw, 52px);
  --hdr-h: clamp(5.1rem, 4.488rem + 2.51vw, 7.5rem);  /* Header height; first band clears it */

  /* Content stops widening past this; backgrounds stay full-bleed. Image-led
     sites sit around 1140-1440px — readability doesn't change just because
     the monitor got wider. */
  --wrap: 1440px;
  --wrap-wide: 1620px;    /* photo walls may run wider than text */
  --gutter: clamp(1.25rem, 2.6vw, 2.5rem);
}

/* Fraunces' own personality axes. WONK swaps in the quirky alternates (swashed
   g, curled leg on y); SOFT rounds the sharp serif terminals. Used on buttons
   and section headings so they read playful rather than bridal, without adding
   a second font family. */
.is-wonky {
  font-variation-settings: 'SOFT' 90, 'WONK' 1;
}

/* --------------------------------------------------------------------------
   2. Base
   -------------------------------------------------------------------------- */
* { box-sizing: border-box; margin: 0; padding: 0; }

html { scroll-behavior: smooth; }

body {
  font-family: 'Fraunces', Georgia, serif;
  font-weight: 400;
  color: var(--ink);
  /* Sections fade in from transparent, so the page behind them must match the
     page's own ground — otherwise peach flashes through a sage or dark page. */
  background: var(--page-ground, var(--pink));
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

/* Baseline containment: images never overflow their box, and height follows
   width so the width/height attributes can't force a wrong aspect ratio.
   Rules that need a filled box override block-size explicitly. */
img,
video {
  display: block;
  max-inline-size: 100%;
  block-size: auto;
}

/* Visible keyboard focus on every interactive element */
a:focus-visible,
button:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 3px;
}
footer a:focus-visible {
  outline-color: var(--cream);
}

/* Available to assistive tech, invisible on screen. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* Skip link — visible only on keyboard focus */
.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 100;
  background: var(--ink);
  color: var(--pink);
  padding: 0.75rem 1.25rem;
  font-size: 0.8rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  font-weight: 500;
  text-decoration: none;
}
.skip-link:focus-visible {
  left: 1rem;
  top: 1rem;
  outline-color: var(--pink);
}

/* --------------------------------------------------------------------------
   3. Texture + scroll reveal
   -------------------------------------------------------------------------- */

/* Paper texture overlay on hero + about (URL-encoded SVG noise, not base64) */
.paper-texture { position: relative; }
.paper-texture::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' /%3E%3CfeColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.045 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  mix-blend-mode: multiply;
  opacity: 0.9;
}

/* Fade-up on scroll (IntersectionObserver adds .in-view) */
.fade-up {
  opacity: 0;
  transform: translateY(24px);
  transition: opacity 0.9s ease, transform 0.9s ease;
}
.fade-up.in-view { opacity: 1; transform: translateY(0); }

/* --------------------------------------------------------------------------
   3b. Promo strip — hand-lettered offer, towed across the top like an
       airplane banner. Fixed, so it stays overhead as you scroll.
   -------------------------------------------------------------------------- */
.promo-strip {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 40;
  height: var(--promo-h);
  background: var(--ink);
  border-bottom: 1px solid rgba(240, 132, 84, 0.35);
  overflow: hidden;
  display: flex;
  align-items: center;
}

/* Two identical runs sit end to end; sliding exactly one run's width and
   resetting makes the loop seamless. */
.promo-track {
  display: flex;
  align-items: center;
  flex: none;
  animation: promo-tow 42s linear infinite;
  /* Continuity across navigations. A CSS animation restarts from 0 on every
     document, so the banner snapped back to its start on every page change --
     the most obvious "the whole site reloaded" tell on the page. The inline
     script in <head> sets this to a negative offset taken from the wall clock,
     which starts the loop at the phase it would have reached had it never
     stopped. Keep the 42s here and in that script in sync. */
  animation-delay: var(--promo-phase, 0ms);
}

.promo-run {
  display: flex;
  align-items: center;
  flex: none;
}

@keyframes promo-tow {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(-50%, 0, 0); }
}

/* The whole strip is a link. It has to fill the bar so the tap target is the
   full height, not just the lettering. */
.promo-link {
  display: flex;
  align-items: center;
  width: 100%;
  height: 100%;
  text-decoration: none;
  color: inherit;
}
.promo-link:focus-visible {
  outline: 2px solid var(--star);
  outline-offset: -3px;
}

/* Pause on hover -- but ONLY where hovering is a real thing. A touch tap
   triggers :hover and it sticks, so on a phone this stopped the marquee dead
   and it never restarted. */
@media (hover: hover) and (pointer: fine) {
  .promo-strip:hover .promo-track { animation-play-state: paused; }
}

.promo-item {
  flex: none;
  display: block;
  height: clamp(20px, 1.45vw, 30px);
  width: auto;
  margin: 0 1.6rem;
}
.promo-item--price { height: clamp(23px, 1.68vw, 35px); }

@media (max-width: 700px) {
  .promo-item { height: 19px; margin: 0 1.1rem; }
  .promo-item--price { height: 22px; }
}

@media (prefers-reduced-motion: reduce) {
  .promo-track { animation: none; }
}

/* --------------------------------------------------------------------------
   4. Header + nav (shared across all pages)
   -------------------------------------------------------------------------- */
.site-header {
  position: absolute;
  top: 0; left: 0; right: 0;
  z-index: 20;
  /* The bar spans the viewport; its contents stop at --wrap. */
  padding-block: clamp(1.4rem, 1.7vw, 2rem);
  padding-inline: max(var(--gutter), (100vw - var(--wrap)) / 2);
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 2rem;
}

/* Inner pages have no full-bleed hero, so the header sits in flow. */
.site-header--static {
  position: relative;
  background: var(--pink);
  padding-top: calc(1.75rem + var(--promo-h));
}

/* The header floats transparently over the first band, then resolves into a
   solid bar once you scroll past it. Colours come from two custom properties
   the build sets per page, so the same rule works over a dark hero, a sage
   Services band or a peach FAQ band. Children use currentColor. */
.site-header--overlay {
  position: fixed;
  top: var(--promo-h);
  color: var(--hdr-ink, var(--ink));
  background: transparent;
  transition: background 0.4s ease, color 0.4s ease;
}
.site-header--overlay .brand-mark,
.site-header--overlay .site-nav a,
.site-header--overlay .nav-toggle { color: inherit; }
.site-header--overlay .nav-toggle { border-color: currentColor; }

.site-header--overlay.is-scrolled {
  background: var(--hdr-scrolled-bg, var(--pink));
  color: var(--hdr-scrolled-ink, var(--ink));
  box-shadow: 0 1px 0 rgba(42, 38, 34, 0.08);
}

/* Only the home page has a photo hero worth floating over. Everywhere else the
   first band is text, and a transparent bar let paragraphs scroll THROUGH the
   wordmark -- reported on the FAQ, where the single giant band meant the flip
   to solid never fired at all. Those headers are opaque from the start; the
   colours already match their first band, so nothing looks different at rest.
   (0,2,1) beats both .site-header--overlay (0,1,0) and .is-scrolled (0,2,0). */
body:not([data-page="index"]) .site-header--overlay {
  background: var(--hdr-scrolled-bg, var(--pink));
  color: var(--hdr-scrolled-ink, var(--ink));
}

/* The Book now chip keeps its own colours on every ground. */
.site-header--overlay .site-nav a.book-btn {
  background: var(--star);
  color: var(--ink);          /* 5.8:1 on --star */
  border-color: var(--star);
}
.site-header--overlay .site-nav a.book-btn:hover {
  background: transparent;
  color: var(--star);
}

.site-header--overlay a:focus-visible,
.site-header--overlay button:focus-visible { outline-color: currentColor; }

/* Home only: the hero H1 is already the title, so the WORDMARK waits until
   you scroll past it. The star stays put, so every page has a mark in the
   header at all times. */
.site-header--hide-brand .brand-word {
  opacity: 0;
  transform: translateY(-6px);
  transition: opacity 0.45s ease, transform 0.45s ease;
}
.site-header--hide-brand.is-scrolled .brand-word {
  opacity: 1;
  transform: translateY(0);
}

/* Wordmark + star lockup. Same treatment as the hero H1: light weight,
   sentence case, tight tracking. Colour comes from the header's band.
   (This rule was lost in an earlier refactor of the header block; without
   it the lockup stopped being a flex container and the star collapsed.) */
.brand-mark {
  font-family: 'Fraunces', Georgia, serif;
  font-weight: 300;
  font-size: clamp(1.25rem, 1.66vw, 1.5rem);
  letter-spacing: -0.015em;
  line-height: 1;
  color: inherit;
  text-decoration: none;
  white-space: nowrap;
  display: inline-flex;
  align-items: center;
  gap: 0.55rem;
}

.site-nav ul {
  display: flex;
  gap: 2rem;
  list-style: none;
  align-items: center;
}
.site-nav a {
  font-family: 'Fraunces', Georgia, serif;
  font-weight: 500;
  font-variation-settings: 'SOFT' 90, 'WONK' 1;
  color: var(--ink-on, var(--ink));
  text-decoration: none;
  font-size: clamp(0.95rem, 0.858rem + 0.379vw, 1.313rem);
  letter-spacing: 0.01em;
}
.site-nav a:hover { opacity: 0.6; }

/* Mobile menu button — hidden above the 700px breakpoint */
.nav-toggle {
  display: none;
  align-items: center;
  gap: 0.55rem;
  background: none;
  border: 1.5px solid var(--ink);
  border-radius: 0;
  color: var(--ink);
  padding: 0.6rem 0.85rem;
  cursor: pointer;
  font-family: 'Fraunces', Georgia, serif;
  font-weight: 500;
  font-size: 0.68rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  line-height: 1;
}
.nav-toggle { color: var(--ink-on, var(--ink)); border-color: currentColor; }
.nav-toggle svg { display: block; fill: currentColor; }
.nav-toggle[aria-expanded="true"] { background: var(--ink); color: var(--pink); }
/* Open state inverts to an ink background, so the focus ring must invert too. */
.nav-toggle[aria-expanded="true"]:focus-visible { outline-color: var(--pink); }

/* Current page: no underline (Alyssa's call). aria-current still carries it
   for assistive tech; sighted users get a slight weight/opacity lift. */
.site-nav a[aria-current="page"] { opacity: 1; }
.site-nav a:not([aria-current="page"]) { opacity: 0.78; }

/* Specificity (0,2,1) so it beats `.site-nav a` (0,1,1). */
.site-nav a.book-btn {
  background: var(--star);
  color: var(--ink);          /* 5.8:1 on --star; cream would be 2.5:1 */
  border: 1.5px solid var(--star);
  padding: 0.6rem 1.1rem;
  font-variation-settings: 'SOFT' 90, 'WONK' 1;
  text-transform: none;
  letter-spacing: 0.01em;
  font-size: clamp(0.9rem, 0.811rem + 0.366vw, 1.25rem);
}
/* Specificity (0,2,1) beats .site-nav a (0,1,1) — no !important needed */
.site-nav a.book-btn:hover {
  background: var(--ink);
  color: var(--pink);
  opacity: 1;
}
.site-nav a.book-btn[aria-current="page"] { padding-bottom: 0.6rem; }

@media (max-width: 900px) {
  .site-header { padding: 1.5rem 1.25rem; flex-wrap: wrap; gap: 1rem; }
  .site-nav ul { gap: 1.25rem; }
}
@media (max-width: 700px) {
  /* Centred brand-mark, menu button pinned right. */
  .site-header {
    padding: 1.5rem 1.25rem;
    flex-wrap: nowrap;
    justify-content: center;
  }
  .brand-mark {
    font-size: 0.97rem;
    letter-spacing: -0.01em;
    text-align: center;
    /* Narrow enough that the centred lockup clears the menu button both sides. */
    max-width: 214px;
    gap: 0.4rem;
  }

  /* The shorthand above resets padding-top, so the static header has to
     re-clear the fixed promo strip here. */
  .site-header--static { padding-top: calc(1.5rem + var(--promo-h)); }

  .nav-toggle {
    display: inline-flex;
    position: absolute;
    right: 1.25rem;
    top: calc(50% + var(--promo-h) / 2);
    transform: translateY(-50%);
    gap: 0;
    padding: 0.7rem 0.75rem;
  }
  /* The overlay header already sits below the strip, so no extra offset there. */
  .site-header--overlay .nav-toggle { top: 50%; }
  /* Dots only on mobile so the button stays narrow. The label stays in the
     accessibility tree as the button's accessible name. */
  .nav-toggle-label {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }

  /* The list becomes a panel anchored to the bottom of the header. */
  .site-nav {
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
  }
  .site-nav ul {
    display: none;
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    /* Follows the header's band, so the panel matches an all-dark page. */
    background: var(--ground, var(--pink));
    padding: 0.25rem 1.25rem 1.25rem;
    box-shadow: 0 14px 28px rgba(38, 33, 28, 0.18);
  }
  .site-nav ul[data-open="true"] { display: flex; }
  .site-nav li { width: 100%; }
  .site-nav a {
    display: block;
    padding: 0.9rem 0;
    font-size: 0.8rem;
    border-bottom: 1px solid color-mix(in srgb, currentColor 18%, transparent);
  }
  .site-nav a[aria-current="page"] { padding-bottom: 0.9rem; }
  .site-nav a.book-btn {
    display: inline-block;
    width: auto;
    align-self: flex-start;
    margin-top: 1rem;
    padding: 0.7rem 1.1rem;
    border: 1.5px solid var(--ink);
  }
  .site-nav a.book-btn[aria-current="page"] { padding-bottom: 0.7rem; }
}

/* --------------------------------------------------------------------------
   5. Hero — pink
   -------------------------------------------------------------------------- */
.hero {
  --ground: var(--ink);
  /* Was 110vh, which put the CTA below the fold at every viewport by
     construction. svh (not vh) is the unit that guarantees the whole
     composition is visible on first paint, mobile address bar included. */
  --hero-pad-top: calc(var(--hdr-h) + 0.6rem + var(--promo-h));
  min-height: 100svh;
  padding-block: var(--hero-pad-top) 4rem;
  padding-inline: max(var(--gutter), (100vw - var(--wrap-wide)) / 2);
  position: relative;
  overflow: hidden;
}

/* Full-bleed background photo. Sits under the overlay, the grain and content. */
.hero-bg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center var(--fp, 42%);
  z-index: 0;
}

/* Dark-to-transparent legibility overlay over the hero photo. */
.hero::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background: linear-gradient(100deg, rgba(30, 35, 35, 0.82) 0%, rgba(30, 35, 35, 0.55) 45%, rgba(30, 35, 35, 0.35) 100%);
}

/* Grain rides above the photo on the hero. */
.hero.paper-texture::after { z-index: 2; }
.hero-inner {
  position: relative;
  z-index: 3;
  display: grid;
  grid-template-columns: 1fr 1.2fr;
  gap: 4rem;
  min-height: calc(100svh - var(--hero-pad-top) - 4rem);
  align-items: center;
}

.hero-title-block {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  height: 100%;
  min-height: 500px;
  padding: 2rem 0;
}
.hero-title {
  font-family: 'Fraunces', Georgia, serif;
  font-weight: 300;
  font-size: clamp(3.5rem, 1.97rem + 6.275vw, 9.5rem);
  line-height: 0.95;
  letter-spacing: -0.03em;
  color: var(--cream);
}
.hero-title .line { display: block; }

.hero-tagline {
  margin-top: 1.5rem;
  max-width: 32ch;
  color: var(--cream);
  opacity: 0.85;
  font-size: clamp(1.062rem, 0.919rem + 0.588vw, 1.625rem);
  line-height: 1.65;
}

.hero-bottom { margin-top: 3rem; }

.book-link {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  font-family: 'Fraunces', Georgia, serif;
  font-variation-settings: 'SOFT' 90, 'WONK' 1;
  color: var(--cream);
  text-decoration: none;
  border-bottom: 1.5px solid var(--cream);
  padding-bottom: 4px;
  font-size: clamp(1.062rem, 0.951rem + 0.458vw, 1.5rem);
  letter-spacing: 0.005em;
  font-weight: 500;
}
.book-link:hover { opacity: 0.65; }
.hero .book-link:focus-visible { outline-color: var(--cream); }

/* Hero photo collage — clean rectangles, no framing, no rotation.
   The container scales with its width and each frame carries a portrait
   aspect-ratio. A fixed height with percentage-sized children turned every
   frame into a landscape box as the viewport widened, cropping portraits to
   a 1.85:1 letterbox at 1920. */
.hero-photos {
  position: relative;
  width: 100%;
  /* Capped by available height as well as width. Without the height term a
     short laptop screen makes the collage taller than the hero and pushes
     the CTA under the fold again. */
  max-width: min(860px, (100svh - var(--hero-pad-top) - 7rem) / 1.06);
  margin-left: auto;
  aspect-ratio: 1 / 1.06;
}
.photo-frame {
  position: absolute;
  overflow: hidden;
  box-shadow: 6px 10px 24px rgba(38, 33, 28, 0.2);
  transition: transform 0.4s ease;
}
.photo-frame img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center var(--fp, 35%);
}
/* Specificity (0,2,1) beats .stamp-1 / .stamp-2 / .stamp-3 (0,1,0) */
.hero-photos .photo-frame:hover {
  transform: translateY(-4px);
  z-index: 20;
}

.featured-frame {
  top: 5%;
  left: 11%;
  width: 66%;
  aspect-ratio: 4/5;
  z-index: 5;
}
.featured-carousel {
  position: relative;
  width: 100%;
  height: 100%;
  overflow: hidden;
}
.featured-slide {
  position: absolute;
  inset: 0;
  opacity: 0;
  transition: opacity 1.4s ease;
}
.featured-slide.active { opacity: 1; }
.featured-slide img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center var(--fp, 35%);
}

.stamp-frame {
  width: 30%;
  aspect-ratio: 3/4;
  z-index: 3;
}
.stamp-1 { top: 0; right: 4%; z-index: 6; }
.stamp-2 { bottom: 6%; left: 0; z-index: 4; }
.stamp-3 { bottom: 0; right: 8%; z-index: 7; }

@media (max-width: 900px) {
  .hero-inner { grid-template-columns: 1fr; gap: 3rem; }
  .hero-photos { max-width: 560px; margin: 0 auto; aspect-ratio: 1 / 1.02; }
  .featured-frame { width: 68%; left: 10%; }
  .stamp-frame { width: 34%; }
  .hero-title-block { min-height: auto; padding: 0; }
}
@media (max-width: 700px) {
  .hero { padding: calc(7rem + var(--promo-h)) 1.25rem 3rem; }
  .hero-photos { max-width: 420px; aspect-ratio: 1 / 1.02; }

  /* A phone has no room for the fourth photo. The featured frame is 66% wide
     and a stamp is 34%, so stamp-2 -- which sits BEHIND the featured frame --
     had about 37px of itself showing on a 375px screen: too thin to read as a
     photograph, and it looked instead like the portrait in front of it had
     been cut off. Drop it and pull the featured frame flush left, so the big
     portrait lines up with the H1 above it and the two remaining stamps stack
     down the right. */
  .stamp-2 { display: none; }
  .featured-frame { left: 0; }
}

/* --------------------------------------------------------------------------
   6. Sections + the band system

   A band declares its ground; everything inside reads its colours from the
   band's tokens rather than hardcoding them. Adding a section is therefore
   `<section class="band band--dark">` — type inverts, marks recolour, and the
   accent switches to one that passes contrast on that ground.

   Every pairing below is verified at WCAG AA for body and accent type:
     peach  ink 10.5 : accent 4.6
     sage   ink  4.7 : accent 4.7
     dark   ink 15.1 : accent 11.8
   -------------------------------------------------------------------------- */
section {
  padding-block: clamp(4.5rem, 7vw, 8rem);
  padding-inline: max(var(--gutter), (100vw - var(--wrap)) / 2);
}

.band {
  background: var(--ground);
  color: var(--ink-on);
}

.band--peach {
  --ground: var(--pink);
  --ink-on: var(--ink);
  --accent-on: var(--sage-ink);
  --mark-on: var(--sage-deep);
  --btn-bg: var(--ink);
  --btn-ink: var(--cream);
}
.band--sage {
  --ground: var(--sage);
  --ink-on: var(--ink);
  /* Sage is a mid-tone with no headroom: nothing but near-black passes on it,
     so the label is differentiated by italic and size, not by colour. */
  --accent-on: var(--ink);
  --mark-on: var(--ink);
  --btn-bg: var(--ink);
  --btn-ink: var(--cream);
}
.band--dark {
  --ground: var(--dark);
  --ink-on: var(--cream);
  --accent-on: var(--blue-soft);
  --mark-on: var(--cream);
  /* An ink button on a dark ground has no edge, so dark bands use the star. */
  --btn-bg: var(--star);
  --btn-ink: var(--ink);
}
/* Photo grounds: no flat background, but the ink tokens still apply. */
.band--photo {
  --ink-on: var(--cream);
  --accent-on: var(--cream);
  --mark-on: var(--cream);
  color: var(--ink-on);
}

.section-label {
  font-family: 'Fraunces', Georgia, serif;
  font-weight: 500;
  font-style: italic;
  font-variation-settings: 'SOFT' 90, 'WONK' 1;
  font-size: clamp(1.05rem, 0.983rem + 0.275vw, 1.313rem);
  letter-spacing: 0.01em;
  color: var(--accent-on, var(--ink));
  margin-bottom: 1.5rem;
}

/* --------------------------------------------------------------------------
   7. Offering — sage
   -------------------------------------------------------------------------- */
.offering {
  text-align: center;
  position: relative;
}
.offering-inner {
  max-width: 900px;
  margin: 0 auto;
  position: relative;
  z-index: 2;
}

.offering h2 {
  font-family: 'Fraunces', Georgia, serif;
  font-variation-settings: 'SOFT' 90, 'WONK' 1;
  font-weight: 300;
  font-size: clamp(2.75rem, 1.699rem + 4.314vw, 6.875rem);
  line-height: 1.02;
  letter-spacing: -0.025em;
  margin-bottom: 2rem;
}
.offering h2 em { font-style: italic; }

.offering .section-copy, .block-prose .section-copy {
  font-family: 'Fraunces', Georgia, serif;
  font-size: clamp(1.15rem, 1.074rem + 0.314vw, 1.45rem);
  line-height: 1.65;
  max-width: 52ch;
  margin: 0 auto 2.75rem;
}

/* Offering details as a bulleted list. The section is centred, so the list is a
   block sized to its content and centred with auto margins: the block centres,
   the text stays left-aligned. It must be block-level, not inline-block, or it
   shares a line with the CTA that follows it. */
/* Bulleted list, centred as a block but left-aligned inside. */
.bullet-list {
  font-family: 'Fraunces', Georgia, serif;
  font-size: clamp(1.031rem, 0.973rem + 0.238vw, 1.188rem);
  line-height: 1.65;
  list-style: disc;
  display: block;
  width: fit-content;
  text-align: left;
  max-width: 52ch;
  margin: 0 auto 2.75rem;
  padding-left: 1.25rem;
}
.bullet-list li { margin-bottom: 0.6rem; }
.bullet-list li:last-child { margin-bottom: 0; }

@media (max-width: 700px) {
  .bullet-list { font-size: 1.05rem; }
}

/* Generic CTA. Colours come from the band so it keeps an edge on any ground. */
.btn {
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  font-family: 'Fraunces', Georgia, serif;
  font-weight: 500;
  font-variation-settings: 'SOFT' 90, 'WONK' 1;
  background: var(--btn-bg, var(--ink));
  color: var(--btn-ink, var(--cream));
  padding: 1.05rem 2.4rem;
  text-decoration: none;
  font-size: 1.05rem;
  letter-spacing: 0.005em;
  transition: background 0.2s, transform 0.2s;
}
.btn:hover { filter: brightness(1.15); transform: translateY(-2px); }
.btn:focus-visible { outline-offset: 4px; }

/* Flanking photos — plain rectangles, drop shadow, no rotation */
.offering-photo {
  position: absolute;
  box-shadow: 6px 10px 24px rgba(38, 33, 28, 0.18);
  z-index: 1;
  transition: transform 0.4s ease;
  overflow: hidden;
}
.offering-photo img {
  width: 100%;
  /* height:auto is required — the width/height HTML attributes would otherwise
     override aspect-ratio, which only applies when one dimension is auto. */
  height: auto;
  aspect-ratio: 3/4;
  object-fit: cover;
  object-position: center var(--fp, 30%);
}
.offering-photo:hover { transform: translateY(-4px); z-index: 20; }

/* These are positioned against the section, which is full-bleed, while the
   content stops at --wrap. Anchoring them to a percentage pinned them to the
   screen edges on a wide monitor, far from anything. They now sit just outside
   the content column and scale with the viewport rather than being fixed px. */
.offering-photo { --flank-w: clamp(120px, 11vw, 200px); width: var(--flank-w); }
.off-photo-1 { top: 9%;     left:  var(--flank-out); }
.off-photo-2 { bottom: 11%; left:  calc(var(--flank-out) + 2.5vw); }
.off-photo-3 { top: 13%;    right: var(--flank-out); }
.off-photo-4 { bottom: 9%;  right: calc(var(--flank-out) + 3vw); }

.offering {
  /* Anchored to the offering's own 900px column, not --wrap, so they sit just
     outside the text rather than stranded at the edge of a wide monitor. */
  --flank-out: max(1rem, calc((100vw - 900px) / 2 - clamp(120px, 11vw, 200px) - 2rem));
}

@media (max-width: 1100px) {
  .offering-photo { display: none; }
}

/* --------------------------------------------------------------------------
   8. Gallery mosaic — pink, airy asymmetric grid
   -------------------------------------------------------------------------- */
.gallery-mosaic {
  padding: 6rem 0 8rem;
  position: relative;
}
.gallery-header {
  position: relative;
  z-index: 1;
  max-width: 1500px;
  margin: 0 auto 4rem;
  padding: 0 3rem;
  display: flex;
  justify-content: space-between;
  align-items: flex-end;
  flex-wrap: wrap;
  gap: 1rem;
}
.gallery-header .head-left { display: flex; flex-direction: column; gap: 0.6rem; }
.gallery-header .section-label { margin-bottom: 0; }
.gallery-header h2 {
  font-family: 'Fraunces', Georgia, serif;
  font-variation-settings: 'SOFT' 90, 'WONK' 1;
  font-weight: 300;
  font-size: clamp(2rem, 1.49rem + 2.092vw, 4rem);
  line-height: 1;
  letter-spacing: -0.02em;
  color: var(--ink-on, var(--ink));
}
.view-all {
  color: var(--ink-on, var(--ink));
  text-decoration: none;
  font-family: 'Fraunces', Georgia, serif;
  font-variation-settings: 'SOFT' 90, 'WONK' 1;
  font-size: clamp(1rem, 0.936rem + 0.261vw, 1.25rem);
  letter-spacing: 0.005em;
  border-bottom: 1.5px solid currentColor;
  padding-bottom: 3px;
  font-weight: 500;
}
.view-all:hover { color: var(--accent-on, var(--sage-deep)); }
.gallery-mosaic a:focus-visible { outline-color: var(--cream); }

.mosaic-wall {
  position: relative;
  z-index: 1;
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  gap: 32px 28px;
  max-width: var(--wrap-wide);
  margin: 0 auto;
  padding-inline: var(--gutter);
}
.m-tile {
  overflow: hidden;
  margin: 0;
  box-shadow: 4px 6px 20px rgba(38, 33, 28, 0.12);
  will-change: transform;
}

/* Scroll drift vectors live with each slot above. Peak offset in px; total
   travel is roughly double. JS reads them — see initGalleryDrift in main.js. */

@media (prefers-reduced-motion: reduce) {
  .m-tile { will-change: auto; }
}
.m-tile img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center var(--fp, 35%);
  transition: transform 0.7s ease, filter 0.4s ease;
  filter: saturate(0.95);
}
.m-tile:hover img { transform: scale(1.05); filter: saturate(1.1); }

/* An 8-slot asymmetric rhythm that REPEATS, so a wall can hold any number of
   photos. nth-child(8n+N) rather than fixed .g1-.g8 classes: the home page's
   eight tiles land on exactly the same slots as before, and a full gallery
   cycles the same pattern. Each slot also carries its own scroll-drift vector. */

/* Row 1 — portrait left, landscape right (middle column left empty) */
/* Each slot's ratio is a DEFAULT, not a mandate. A landscape photograph
   dropped into a portrait slot was being cropped to a tall strip and losing
   its subject, so build.py overrides --tile-ar whenever it cannot place a
   photo in a slot matching its own orientation. */
.mosaic-wall > *:nth-child(8n+1) { grid-column: 1 / span var(--tile-cols, 4); aspect-ratio: var(--tile-ar, 4/5); --drift-x: -34; --drift-y: 118; }
.mosaic-wall > *:nth-child(8n+2) { grid-column: 6 / span var(--tile-cols, 7); aspect-ratio: var(--tile-ar, 3/2); --drift-x:  44; --drift-y: -86; }
/* Row 2 — landscape left, portrait right (offset from row 1) */
.mosaic-wall > *:nth-child(8n+3) { grid-column: 2 / span var(--tile-cols, 6); aspect-ratio: var(--tile-ar, 5/3); --drift-x: -52; --drift-y: -64; }
.mosaic-wall > *:nth-child(8n+4) { grid-column: 9 / span var(--tile-cols, 3); aspect-ratio: var(--tile-ar, 3/4); --drift-x:  30; --drift-y: 104; }
/* Row 3 — square, then wide landscape */
.mosaic-wall > *:nth-child(8n+5) { grid-column: 1 / span var(--tile-cols, 3); aspect-ratio: var(--tile-ar, 1/1); --drift-x: -38; --drift-y:  78; }
.mosaic-wall > *:nth-child(8n+6) { grid-column: 5 / span var(--tile-cols, 7); aspect-ratio: var(--tile-ar, 5/3); --drift-x:  56; --drift-y: -98; }
/* Row 4 — portrait left, portrait right (gap in the middle) */
.mosaic-wall > *:nth-child(8n+7) { grid-column: 2 / span var(--tile-cols, 3); aspect-ratio: var(--tile-ar, 3/4); --drift-x: -30; --drift-y: -72; }
.mosaic-wall > *:nth-child(8n+8) { grid-column: 7 / span var(--tile-cols, 4); aspect-ratio: var(--tile-ar, 4/5); --drift-x:  44; --drift-y: 110; }

@media (max-width: 900px) {
  .mosaic-wall {
    grid-template-columns: repeat(6, 1fr);
    gap: 20px 16px;
    padding: 0 1.5rem;
  }
  /* :nth-child(n) matches every tile but carries the same specificity as the
     slot rules above (0,2,0). A plain `.mosaic-wall > *` is (0,1,0) and loses
     to them, which left tiles on their 12-column spans on small screens. */
  .mosaic-wall > *:nth-child(n) { grid-column: 1 / span 6; }
  /* One column, so horizontal drift would read as misalignment — gentle
     vertical stagger only. This must sit AFTER the slot rules: same
     specificity, so source order decides, and it was previously losing. */
  .mosaic-wall > *:nth-child(odd)  { --drift-x: 0; --drift-y:  46; }
  .mosaic-wall > *:nth-child(even) { --drift-x: 0; --drift-y: -46; }

  /* The full photo walls are long single columns on a phone -- 25 tiles on the
     Gallery, 33 on Travel -- and the drift made them feel unsteady to scroll;
     readers reported the tiles fighting the scroll. Amplitudes go to zero on
     both. The home page's four-tile teaser keeps the effect, which is short
     enough that it reads as intended rather than as resistance.
     (0,2,1) beats the (0,2,0) rules above, so order does not matter here. */
  body[data-page="gallery"] .mosaic-wall > *,
  body[data-page="travel"] .mosaic-wall > * {
    --drift-x: 0;
    --drift-y: 0;
  }
  .gallery-header { padding: 0 1.5rem; }
}

/* --------------------------------------------------------------------------
   8b. Swirl brandmark — hand-drawn 15-frame boil loop + static watermark

   Drawn as alpha masks so the colour comes from CSS and the same artwork can
   sit on peach, mauve, sage or a photograph. Everything is wrapped in an
   @supports guard: without mask support these would render as solid blocks,
   so they simply don't render at all.
   -------------------------------------------------------------------------- */
.swirl,
.swirl-mark { display: none; }

@supports ((-webkit-mask-image: url("#m")) or (mask-image: url("#m"))) {

  /* --- Animated seam mark: straddles the boundary between two sections --- */
  .swirl {
    --swirl-size: 96px;
    display: block;
    position: absolute;
    top: 0;
    left: 50%;
    transform: translate(-50%, -50%);
    z-index: 6;
    width: var(--swirl-size);
    height: var(--swirl-size);
    background-color: var(--swirl-ink, var(--mark-on, var(--sage-deep)));
    -webkit-mask-image: url('../photos/swirl-sprite-mask.png');
    mask-image: url('../photos/swirl-sprite-mask.png');
    -webkit-mask-repeat: no-repeat;
    mask-repeat: no-repeat;
    -webkit-mask-size: calc(var(--swirl-size) * 15) var(--swirl-size);
    mask-size: calc(var(--swirl-size) * 15) var(--swirl-size);
    -webkit-mask-position: 0 0;
    mask-position: 0 0;
    animation: swirl-boil 1.25s steps(15) infinite;
  }

  /* Sits half on the section above and half on the section below, so it needs a
     colour that survives both grounds. */
  .swirl--on-light { --swirl-ink: var(--sage-deep); }
  .swirl--on-dark  { --swirl-ink: var(--cream); }

  /* steps(15) lands on each frame in turn and never reaches the final offset,
     which would wrap onto frame 1 and stall for a beat. */
  @keyframes swirl-boil {
    from { -webkit-mask-position: 0 0; mask-position: 0 0; }
    to   { -webkit-mask-position: calc(var(--swirl-size) * -15) 0;
           mask-position: calc(var(--swirl-size) * -15) 0; }
  }

  /* Clips a bleeding watermark without clipping the section's seam swirl. */
  .swirl-mark-clip {
    position: absolute;
    inset: 0;
    overflow: hidden;
    pointer-events: none;
    z-index: 0;
  }

  /* --- Hand-drawn star, 17-frame boil ---
     Two inks (orange outline + coral inner), so unlike the swirl this stays a
     colour sprite rather than a mask; a mask would flatten it to one colour. */

  /* --- Static faded watermark --- */
  .swirl-mark {
    display: block;
    position: absolute;
    z-index: 0;
    pointer-events: none;
    background-color: var(--swirl-ink, var(--mark-on, var(--sage-deep)));
    -webkit-mask-image: url('../photos/swirl-mark-mask.png');
    mask-image: url('../photos/swirl-mark-mask.png');
    -webkit-mask-repeat: no-repeat;
    mask-repeat: no-repeat;
    -webkit-mask-size: contain;
    mask-size: contain;
  }

  .swirl-mark--hero {
    --swirl-ink: var(--cream);
    width: clamp(360px, 44vw, 660px);
    aspect-ratio: 1;
    top: 46%;
    left: -6%;
    opacity: 0.09;
    z-index: 2;
  }

  /* Star watermark shares the swirl's mask machinery, different artwork. */
  .star-mark {
    display: block;
    position: absolute;
    z-index: 0;
    pointer-events: none;
    background-color: var(--swirl-ink, var(--star));
    -webkit-mask-image: url('../photos/star-mark-mask.png');
    mask-image: url('../photos/star-mark-mask.png');
    -webkit-mask-repeat: no-repeat;
    mask-repeat: no-repeat;
    -webkit-mask-size: contain;
    mask-size: contain;
  }
  .star-mark--gallery {
    --swirl-ink: var(--star);
    width: clamp(300px, 34vw, 520px);
    aspect-ratio: 524 / 520;
    bottom: 4%;
    left: -6%;
    opacity: 0.13;
  }
  /* Sits fully inside the section: clipping it at the top edge read as a seam. */
  .star-mark--about {
    width: clamp(230px, 26vw, 400px);
    aspect-ratio: 524 / 520;
    bottom: 6%;
    right: 4%;
    opacity: 0.12;
  }

  .swirl-mark--gallery {
    width: clamp(340px, 40vw, 620px);
    aspect-ratio: 1;
    top: 18%;
    right: -7%;
    opacity: 0.10;
  }
}

@media (max-width: 700px) {
  .swirl { --swirl-size: 68px; }
  .swirl-mark--hero { top: 58%; opacity: 0.08; }
  .swirl-mark--gallery { opacity: 0.08; }
  .star-mark--gallery, .star-mark--about { opacity: 0.10; }
}

/* Star — colour sprite, 17 frames, same boil technique as the swirl. */
.star {
  --star-w: 180px;
  position: absolute;
  /* Above .hero-inner (z-index 3) so the star sits on top of the photos. */
  z-index: 5;
  width: var(--star-w);
  aspect-ratio: 181 / 180;
  pointer-events: none;
  background-image: url('../photos/star-sprite.png');
  background-repeat: no-repeat;
  background-size: calc(var(--star-w) * 17) 100%;
  background-position: 0 0;
  animation: star-boil 3s steps(17) infinite;
}

@keyframes star-boil {
  from { background-position: 0 0; }
  to   { background-position: calc(var(--star-w) * -17) 0; }
}

/* Logo mark beside the wordmark. Boil only, no spin — a logo that rotates
   reads as a loading spinner. */
.star--logo {
  --star-w: 21px;
  display: inline-block;   /* not reliant on a flex parent to blockify it */
  position: static;
  flex: none;
  animation: star-boil 3s steps(17) infinite;
}

/* Closing mark at the top of the footer. */
.swirl--footer {
  --swirl-size: 54px;
  --swirl-ink: var(--cream);
  position: static;
  transform: none;
  align-self: center;
}

/* Strip star: the same 17-frame boil, shrunk, with a slow spin on top. Two
   animations on one element — background-position for the boil, transform for
   the rotation — so they compose instead of fighting. */
.star--tiny {
  --star-w: 26px;
  display: inline-block;
  position: static;
  flex: none;
  margin: 0 0.15rem;
  animation: star-boil 3s steps(17) infinite,
             star-spin 14s linear infinite;
}
.star--tiny.star--slow { animation-duration: 3s, 19s; animation-direction: normal, reverse; }

@keyframes star-spin {
  from { transform: rotate(0deg); }
  to   { transform: rotate(360deg); }
}

@media (max-width: 700px) {
  .star--tiny { --star-w: 21px; }
  .star--logo { --star-w: 16px; }
}

/* Stars floating in a band's open space. Behind the text, never over it. */
/* Band stars hold a single drawn frame — no boil. The moving stars are the
   hero pair, the promo strip and the logo; these are punctuation, and a page
   full of them wobbling would be noise. */
.star--band-lg,
.star--band-sm,
.star--band-ghost,
.star--band-ghost-sm {
  animation: none;
  background-position: 0 0;
}

/* Solid stars live in the gutters beside the content, so they never sit
   behind text. Below 1100px there is no gutter left and they hide, the same
   way the offering's flanking photos do. */
.star--band-lg {
  --star-w: clamp(110px, 11vw, 190px);
  right: 2.5%;
  top: 12%;
  z-index: 1;
}
.star--band-sm {
  --star-w: clamp(70px, 7.5vw, 120px);
  left: 3%;
  bottom: 14%;
  z-index: 1;
  animation-delay: -1.4s;
}

/* Ghosts are large and faded, so they can sit behind copy. */
.star--band-ghost,
.star--band-ghost-sm {
  z-index: 0;
  opacity: 0.16;
}
.star--band-ghost {
  --star-w: clamp(220px, 30vw, 420px);
  right: 8%;
  bottom: -6%;
  animation-delay: -0.7s;
}
.star--band-ghost-sm {
  --star-w: clamp(140px, 18vw, 260px);
  left: 6%;
  top: 8%;
  animation-delay: -2.1s;
}

/* A title band's heading is left-aligned, so the left gutter isn't free the
   way it is beside a photo. The small star moves right, between the heading
   and the large star. */
.page-head .star--band-sm {
  left: auto;
  right: 21%;
  bottom: 10%;
}

/* On the block a page opens with, the fixed header occupies the top of the
   section, so the upper star drops below it rather than sitting behind the nav. */
.is-first .star--band-lg { top: calc(var(--promo-h) + var(--hdr-h) + 2.5rem); }
.is-first .star--band-ghost-sm { top: calc(var(--promo-h) + var(--hdr-h) + 1rem); }

@media (max-width: 1100px) {
  .star--band-lg, .star--band-sm { display: none; }
}
@media (max-width: 700px) {
  .star--band-ghost { --star-w: clamp(180px, 46vw, 240px); right: 2%; bottom: -3%; }
  .star--band-ghost-sm { --star-w: clamp(120px, 34vw, 180px); left: 2%; }
}
/* Content sits above them. */
.page-head-inner,
.block-inner { position: relative; z-index: 2; }

@media (max-width: 900px) {
}

/* Placed to match the reference: a large one between the headline and the
   collage, a smaller one tucked into the gap on the right. */
/* Sized to the measured gap between the headline (ends 43.5%) and the featured
   photo (starts 54.2%), with a deliberate tuck behind the photo's left edge. */
.star--hero-lg {
  --star-w: clamp(120px, 11.5vw, 230px);
  left: 44.5%;
  top: 27%;
}
.star--hero-sm {
  --star-w: clamp(78px, 8vw, 150px);
  left: 88%;
  top: 49%;
  animation-delay: -1.3s;
}

@media (max-width: 900px) {
  .star--hero-lg { --star-w: clamp(110px, 20vw, 170px); left: 52%; top: 8%; }
  .star--hero-sm { --star-w: clamp(66px, 13vw, 110px); left: 6%; top: 62%; }
}

@media (prefers-reduced-motion: reduce) {
  .star { animation: none; }
}

/* --------------------------------------------------------------------------
   8b2. Prose and numbered-steps blocks
   -------------------------------------------------------------------------- */
.block-prose .block-inner { max-width: 820px; }
.block-prose:has(.block-title) .block-inner { max-width: 1200px; }

/* A page title living inside a content band rather than in its own band.
   Same scale as the one a band-head renders, so Services reads like Gallery. */
.block-title-wrap {
  max-width: 1200px;
  margin: 0 auto 3.5rem;
  position: relative;
  z-index: 2;
}
.block-title {
  font-family: 'Fraunces', Georgia, serif;
  font-weight: 300;
  font-size: clamp(2.75rem, 1.699rem + 4.314vw, 6.875rem);
  line-height: 1.02;
  letter-spacing: -0.025em;
  color: var(--ink-on, var(--ink));
}

@media (max-width: 700px) {
  .block-title-wrap { margin-bottom: 2.25rem; }
}
.block-prose h2,
.block-steps h2 {
  font-family: 'Fraunces', Georgia, serif;
  font-weight: 300;
  font-variation-settings: 'SOFT' 90, 'WONK' 1;
  font-size: clamp(2rem, 1.49rem + 2.092vw, 4rem);
  line-height: 1;
  letter-spacing: -0.02em;
  color: var(--ink-on, var(--ink));
  margin-bottom: 1.75rem;
}
.block-prose .bullet-list { margin-left: 0; margin-right: 0; }
.block-prose .btn { margin-top: 1rem; }

.block-steps .block-inner { max-width: 900px; }

/* Photo beside the content. The figure keeps a portrait crop so a section
   never depends on having a landscape frame. */
.block-prose,
.block-steps,
.block-qa { position: relative; }

/* Whatever a page opens with sits under the fixed header. `.page-head`
   already carries this; any other block used first needs it too. */
.is-first:not(.page-head) {
  padding-top: calc(4rem + var(--promo-h) + var(--hdr-h));
}
@media (max-width: 700px) {
  .is-first:not(.page-head) {
    padding-top: calc(2.5rem + var(--promo-h) + var(--hdr-h));
  }
}

.block-split {
  display: grid;
  grid-template-columns: 1fr 1.15fr;
  gap: 4rem;
  align-items: center;
  max-width: 1200px;
}
.block-figure {
  margin: 0;
  aspect-ratio: 4/5;
  overflow: hidden;
  box-shadow: 6px 10px 24px rgba(38, 33, 28, 0.18);
}
.block-figure img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center var(--fp, 30%);
  transition: transform 0.8s ease;
}
.block-figure:hover img { transform: scale(1.03); }
.block-figure--right { order: 2; }
/* A landscape crop, for a section that wants one wide image rather than a grid. */
.block-figure--wide { aspect-ratio: 3/2; }
.block-split:has(.block-figure--wide) { grid-template-columns: 1.3fr 1fr; }

/* One long question list beside a single photo. `align-items: start` matters
   twice: it stops the figure stretching (a stretched grid item is already full
   height, so sticky would never engage) and it anchors the photo at the top
   instead of floating it in the middle of a very tall column. Sticky then
   keeps it in view down all 15 questions rather than stranding several hundred
   pixels of empty space. Declared before the 900px block so that override,
   which matches this specificity, still wins on source order. */
.block-qa .block-split {
  grid-template-columns: 1.55fr 1fr;
  align-items: start;
}
.block-qa .block-figure {
  position: sticky;
  top: calc(var(--promo-h) + var(--hdr-h) + 1.5rem);
}

/* A title inside a mosaic band sits above the wall. The gap has to clear the
   scroll drift: the first row's largest vertical amplitude is 118px, so a
   smaller margin lets a tile ride up over the title as you scroll. */
.gallery-mosaic .block-title-wrap {
  padding-inline: var(--gutter);
  max-width: var(--wrap-wide);
  margin-bottom: 9.5rem;
}
@media (max-width: 900px) {
  /* Drift drops to 46px on one column, so the clearance can too. */
  .gallery-mosaic .block-title-wrap { padding: 0 1.5rem; margin-bottom: 4.5rem; }
}

/* Matches .section-label exactly — 1.15rem was a one-off size that existed
   nowhere else and made the label above it read as the smaller of the two. */
.block-sub {
  font-family: 'Fraunces', Georgia, serif;
  font-weight: 500;
  font-style: italic;
  font-variation-settings: 'SOFT' 90, 'WONK' 1;
  font-size: 1.05rem;
  letter-spacing: 0.01em;
  color: var(--accent-on, var(--ink));
  margin: 0 0 1rem;
}
.block-body > .bullet-list { margin-top: 0; }

@media (max-width: 900px) {
  /* The :has() variant above is (0,2,0); a plain .block-split is (0,1,0) and
     loses to it, so the wide-photo section never stacked. Match specificity. */
  .block-split,
  .block-qa .block-split,
  .block-split:has(.block-figure--wide) { grid-template-columns: 1fr; gap: 2.5rem; }
  .block-qa .block-figure { position: static; }
  .block-figure { max-width: 420px; }
  .block-figure--right { order: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .block-figure img { transition: none; }
  .block-figure:hover img { transform: none; }
}
.steps { list-style: none; counter-reset: step; }
.step {
  counter-increment: step;
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 1.5rem;
  padding: 1.9rem 0;
  border-top: 1px solid color-mix(in srgb, currentColor 20%, transparent);
}
.step:last-child { border-bottom: 1px solid color-mix(in srgb, currentColor 20%, transparent); }
.step-n {
  font-family: 'Fraunces', Georgia, serif;
  font-weight: 300;
  font-style: italic;
  font-size: 2rem;
  line-height: 1;
  color: var(--accent-on, var(--ink));
  min-width: 2ch;
}
.step h3 {
  font-family: 'Fraunces', Georgia, serif;
  font-weight: 500;
  font-variation-settings: 'SOFT' 90, 'WONK' 1;
  font-size: 1.25rem;
  line-height: 1.3;
  margin-bottom: 0.5rem;
  color: var(--ink-on, var(--ink));
}

@media (max-width: 700px) {
  .step { grid-template-columns: 1fr; gap: 0.5rem; padding: 1.4rem 0; }
  .step-n { font-size: 1.5rem; }
}

/* --------------------------------------------------------------------------
   8c. Q&A accordion

   Built on native <details>/<summary>: keyboard support, screen-reader
   expanded/collapsed announcement, and find-in-page all come free, and it
   still works with JavaScript disabled.
   -------------------------------------------------------------------------- */
/* The narrow measure is only right when the questions stand alone. It also
   has to opt out of the generic `margin: 0 auto`, or an 820px column sits
   centred inside the 1200px wrap and reads as indented against the page
   title, which spans the full width. */
.block-qa .block-inner:not(.block-split) {
  max-width: 820px;
  margin-inline: 0 auto;
}

.block-qa h2 {
  font-family: 'Fraunces', Georgia, serif;
  font-weight: 300;
  font-variation-settings: 'SOFT' 90, 'WONK' 1;
  font-size: clamp(2rem, 1.49rem + 2.092vw, 4rem);
  line-height: 1;
  letter-spacing: -0.02em;
  color: var(--ink-on, var(--ink));
  margin-bottom: 2.5rem;
}

.qa-item {
  border-top: 1px solid color-mix(in srgb, currentColor 20%, transparent);
}
.qa-item:last-of-type {
  border-bottom: 1px solid color-mix(in srgb, currentColor 20%, transparent);
}

.qa-item > summary {
  list-style: none;              /* hide the default disclosure triangle */
  cursor: pointer;
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 2rem;
  padding: 1.6rem 0;
  font-family: 'Fraunces', Georgia, serif;
  font-weight: 500;
  font-variation-settings: 'SOFT' 90, 'WONK' 1;
  font-size: clamp(1.125rem, 1.055rem + 0.286vw, 1.312rem);
  line-height: 1.35;
  color: var(--ink-on, var(--ink));
}
.qa-item > summary::-webkit-details-marker { display: none; }
.qa-item > summary:hover { opacity: 0.7; }
.qa-item > summary:focus-visible {
  outline: 2px solid currentColor;
  outline-offset: 4px;
}

/* Chevron drawn from borders rather than a "+" character, so no stray glyph
   lands in the accessibility tree — <details> already announces its state. */
.qa-item > summary::after {
  content: '';
  flex: none;
  width: 11px;
  height: 11px;
  margin-top: 0.5em;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: rotate(45deg);
  transition: transform 0.3s ease;
}
.qa-item[open] > summary::after { transform: rotate(-135deg); }

/* Links inside body copy: underlined, inheriting the band's ink. */
.section-copy a {
  color: inherit;
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-thickness: 1px;
}
.section-copy a:hover { text-decoration-thickness: 2px; }
.bullet-list a { color: inherit; text-underline-offset: 3px; }

/* A list inside an answer is left-aligned, not centred like the offering's. */
.qa-item .bullet-list,
.block-prose .bullet-list {
  margin-left: 0;
  margin-right: 0;
  width: auto;
  max-width: 54ch;
}
.qa-item .bullet-list { margin-bottom: 1.9rem; }

.qa-item .section-copy {
  /* ~70 characters a line. `ch` measures "0", which is wider than average in
     Fraunces, so 62ch was rendering closer to 80. */
  max-width: 54ch;
  padding-bottom: 1.9rem;
}
.qa-item .section-copy + .section-copy { margin-top: 1rem; padding-bottom: 1.9rem; }

@media (prefers-reduced-motion: reduce) {
  .qa-item > summary::after { transition: none; }
}

@media (max-width: 700px) {
  .qa-item > summary { font-size: 1.1rem; gap: 1.25rem; padding: 1.3rem 0; }
}

/* --------------------------------------------------------------------------
   9. Banner — full-width photo band with italic overlay
   -------------------------------------------------------------------------- */
.banner {
  height: 60vh;
  min-height: 400px;
  position: relative;
  overflow: hidden;
  --ground: var(--sage-deep);
  padding: 0;
}
.banner img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center var(--fp, 32%);
  opacity: 0.85;
  transition: transform 8s ease-out;
}
.banner:hover img { transform: scale(1.05); }
.banner::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, rgba(0,0,0,0.25) 0%, rgba(0,0,0,0.15) 50%, rgba(0,0,0,0.4) 100%);
  pointer-events: none;
}
.banner-text {
  position: relative;
  z-index: 2;
  height: 100%;
  display: flex;
  /* Row was fine while this held a single line. With an attribution added it
     put the name alongside the quote instead of under it. */
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 3rem 2.5rem;
}
.banner-text blockquote p {
  font-family: 'Fraunces', Georgia, serif;
  font-weight: 300;
  font-style: italic;
  font-size: clamp(2.5rem, 6vw, 5rem);
  line-height: 1.1;
  letter-spacing: -0.02em;
  color: var(--cream);
  text-align: center;
  max-width: 20ch;
  text-shadow: 0 2px 20px rgba(0,0,0,0.4);
}

/* A quotation, not a headline: the words carry the weight, the name sits under
   them at a smaller size so it reads as attribution rather than as part of the
   sentence. */
.banner-text blockquote { margin: 0; }
.banner-text cite {
  display: block;
  margin-top: 1.4rem;
  font-style: normal;
  font-size: clamp(0.95rem, 0.88rem + 0.29vw, 1.2rem);
  letter-spacing: 0.02em;
  opacity: 0.82;
}

/* --------------------------------------------------------------------------
   10. About — mauve
   -------------------------------------------------------------------------- */
.about { position: relative; }

.about-inner {
  /* Above the star watermark, which is a positioned element and would
     otherwise paint over this unpositioned content. */
  position: relative;
  z-index: 1;
  max-width: 1200px;
  margin: 0 auto;
  display: grid;
  grid-template-columns: 1fr 1.2fr;
  gap: 5rem;
  align-items: center;
}
.about-photo {
  aspect-ratio: 4/5;
  overflow: hidden;
  margin: 0;
}
.about-photo img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center var(--fp, 30%);
  transition: transform 0.8s ease;
}
.about-photo:hover img { transform: scale(1.03); }

.about-text h2 {
  font-family: 'Fraunces', Georgia, serif;
  font-variation-settings: 'SOFT' 90, 'WONK' 1;
  font-weight: 300;
  font-size: clamp(2.5rem, 5vw, 4rem);
  line-height: 1.02;
  letter-spacing: -0.02em;
  margin-bottom: 1.5rem;
}
.about-text h2 em { font-style: italic; }
.about-text .section-copy {
  font-family: 'Fraunces', Georgia, serif;
  font-size: 1.1rem;
  line-height: 1.7;
  max-width: 46ch;
  margin-bottom: 1.25rem;
}

@media (max-width: 900px) {
  .about-inner { grid-template-columns: 1fr; gap: 2.5rem; }
  .about-photo { max-width: 420px; margin: 0 auto; }
  .about, .offering { padding: 5rem 1.5rem; }
}

/* --------------------------------------------------------------------------
   11. Inner-page scaffold (gallery / services / faq)
   -------------------------------------------------------------------------- */
/* The header is fixed, so the first band has to clear it and the promo
   strip above it. */
.page-head {
  padding: calc(3rem + var(--promo-h) + var(--hdr-h)) 2.5rem 6rem;
  position: relative;
}
.page-head-inner {
  max-width: 1500px;
  margin: 0 auto;
}
.page-head h1 {
  font-family: 'Fraunces', Georgia, serif;
  font-weight: 300;
  font-size: clamp(2.75rem, 1.699rem + 4.314vw, 6.875rem);
  line-height: 1.02;
  letter-spacing: -0.025em;
  color: var(--ink-on, var(--ink));
}
.page-head h1 em { font-style: italic; }

/* Standfirst under a page title. Also the base style any block's body copy
   inherits — colour comes from the band, so it inverts with the ground. */
.section-copy {
  font-family: 'Fraunces', Georgia, serif;
  font-size: clamp(1rem, 0.93rem + 0.286vw, 1.188rem);
  line-height: 1.7;
  color: var(--ink-on, var(--ink));
}
.page-head-intro {
  margin-top: 1.5rem;
  max-width: 60ch;
}
.page-head-intro .section-copy + .section-copy { margin-top: 1rem; }

/* Generic wrapper for the reusable blocks (prose / steps / qa). */
.block-inner {
  max-width: 1200px;
  margin: 0 auto;
}

@media (max-width: 700px) {
  :root { --hdr-h: 5.4rem; }
  .page-head { padding: calc(2rem + var(--promo-h) + var(--hdr-h)) 1.25rem 4rem; }
  .page-head-intro { margin-top: 1.1rem; }
  .section-copy { font-size: 1.05rem; }
}

/* --------------------------------------------------------------------------
   12. Footer
   -------------------------------------------------------------------------- */
.site-footer {
  padding-block: clamp(2.5rem, 4vw, 3.25rem) clamp(2.75rem, 4vw, 3.5rem);
  padding-inline: max(var(--gutter), (100vw - var(--wrap)) / 2);
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 2.25rem;
  font-family: 'Fraunces', Georgia, serif;
  font-size: 0.9rem;
}
.footer-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  flex-wrap: wrap;
  gap: 1.5rem;
}
.site-footer a {
  color: var(--cream);
  text-decoration: none;
  border-bottom: 1px solid rgba(254, 250, 224, 0.45);
  margin-right: 1.5rem;
  padding-bottom: 3px;
}
.site-footer a:hover { border-color: var(--cream); }
.footer-links a:last-child { margin-right: 0; }

@media (max-width: 700px) {
  .site-footer { padding: 2.5rem 1.5rem; gap: 1.75rem; }
  .swirl--footer { --swirl-size: 44px; }
  .footer-row { flex-direction: column; align-items: flex-start; gap: 1rem; }
}

/* --------------------------------------------------------------------------
   13. Reduced motion
   -------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  .fade-up { opacity: 1; transform: none; transition: none; }
  .featured-slide { transition: none; }
  .photo-frame,
  .offering-photo,
  .btn,
  .m-tile img,
  .banner img,
  .about-photo img { transition: none; }
  .swirl { animation: none; }
  .banner:hover img { transform: none; }
  .m-tile:hover img { transform: none; }
  .about-photo:hover img { transform: none; }
}
