/* Sarah & Co. Beauty
   Ported from reference/sarah-and-co-homepage-comp-v2.html.
   Tokens live in tokens.css. Comp scaffolding (.switch, .note) and the
   dark/split hero variants are intentionally not carried across — the
   Light hero is the only variant and is baked in below. */

/* ---------- base ---------- */
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
body {
  margin: 0;
  background: var(--ivory);
  color: var(--ink);
  font-family: 'Jost', system-ui, sans-serif;
  font-weight: 300;
  font-size: 17px;
  line-height: 1.65;
  font-variant-numeric: tabular-nums;
}
a { color: inherit; }
img { max-width: 100%; display: block; }

.wrap { max-width: var(--width-page); margin: 0 auto; padding: 0 var(--gutter); }
.narrow { max-width: var(--width-narrow); margin: 0 auto; padding: 0 var(--gutter); }
section { padding: 120px 0; }
.band { background: var(--linen); }

/* ---------- type ---------- */
h1, h2, h3 {
  font-family: 'Cormorant Garamond', Georgia, serif;
  font-weight: 300;
  margin: 0;
  letter-spacing: .01em;
}
h2 { font-size: var(--size-h2); line-height: 1.1; }
p { margin: 0 0 1.1em; max-width: var(--measure); }
.lede { font-size: 1.05rem; color: var(--stone); max-width: var(--measure-lede); }

.rule--accent { width: 52px; height: 1px; background: var(--terracotta); border: 0; margin: 0; }
.meta {
  font-size: .68rem;
  letter-spacing: .22em;
  text-transform: uppercase;
  color: var(--stone);
  font-weight: 400;
}

/* ---------- buttons ---------- */
.btn {
  display: inline-block;
  background: var(--ink);
  color: var(--ivory);
  text-decoration: none;
  font: inherit;
  font-size: .82rem;
  letter-spacing: .14em;
  text-transform: uppercase;
  padding: 16px 34px;
  font-weight: 400;
  cursor: pointer;
  transition: background .2s ease;
  border: 1px solid var(--ink);
}
.btn:hover, .btn:focus-visible { background: var(--terracotta); border-color: var(--terracotta); }
.btn--ghost { background: transparent; color: var(--ivory); border-color: rgba(246, 243, 238, .45); }
.btn--ghost:hover { background: var(--ivory); color: var(--ink); border-color: var(--ivory); }
/* Pending CTA — no destination yet (site.booking.url is null). A native
   disabled <button>, not a dead <a href>: dimmed so it doesn't read as a
   normal, live, ink-solid button, and the hover-to-terracotta rule above is
   cancelled so hovering it doesn't imply it's clickable. */
.btn:disabled { opacity: .45; cursor: not-allowed; }
.btn:disabled:hover { background: var(--ink); border-color: var(--ink); }
:focus-visible { outline: 2px solid var(--terracotta); outline-offset: 3px; }

/* ---------- masthead ---------- */
.masthead {
  position: sticky;
  top: 0;
  z-index: 30;
  background: rgba(246, 243, 238, .94);
  backdrop-filter: blur(6px);
  border-bottom: 1px solid var(--hair);
}
.masthead__in {
  max-width: var(--width-page);
  margin: 0 auto;
  padding: 14px var(--gutter);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
}
.mark {
  font-family: 'Cormorant Garamond', serif;
  font-size: 1.2rem;
  letter-spacing: .16em;
  text-decoration: none;
  white-space: nowrap;
}
.nav { display: flex; align-items: center; gap: 30px; font-size: .78rem; letter-spacing: .12em; text-transform: uppercase; }
/* Padding, not a taller line-height, so the link's own box is the 44px tap
   target (CLAUDE.md accessibility floor) rather than relying on whitespace
   around it that a tap might miss. */
.nav a { display: inline-block; padding: 12px 0; text-decoration: none; color: var(--stone); }
.nav a:hover { color: var(--ink); }
/* Home (see header.njk): desktop stays the existing four items — the
   wordmark is already the desktop home link. Reappears inside the mobile
   overlay only, below. */
.nav a:first-child { display: none; }
/* Book is an <a> inside .nav, so .nav a's own color:stone (and :ink on
   hover) — both more specific than plain .btn's color:ivory — would
   otherwise win and print the button's copy in the wrong, too-dark
   colour on the button's own ink/terracotta background. Restated here at
   higher specificity so it stays the ivory the button was designed with,
   matching the masthead's own background colour, at rest and on hover. */
.masthead .btn { padding: 11px 22px; font-size: .72rem; color: var(--ivory); }
.masthead .btn:hover, .masthead .btn:focus-visible { color: var(--ivory); }

/* ---------- mobile nav: hamburger + full-screen menu, below 900px ---------- */
/* Desktop is untouched above 900px: the <details> itself takes over .nav's
   old spot as a flex item of .masthead__in (display:flex, same
   align-items), and <summary> stays display:none until the media query
   below turns it on — so with only .nav visible inside it, .nav-toggle's
   box sits exactly where .nav's did before.
   (Not display:contents: it unwraps the box as intended, but collapses
   the unwrapped child to zero width in this flex context — a known
   display:contents/flexbox interaction, not a details-open thing — height
   computes fine, width doesn't. Wrapping instead of unwrapping sidesteps
   it entirely.) */
.nav-toggle { display: flex; align-items: center; }
.nav-toggle > summary {
  display: none;
  list-style: none;
  cursor: pointer;
}
.nav-toggle > summary::-webkit-details-marker { display: none; }

.nav-toggle__icon,
.nav-toggle__icon::before,
.nav-toggle__icon::after {
  display: block;
  width: 22px;
  height: 2px;
  background: var(--ink);
}
.nav-toggle__icon { position: relative; transition: background .2s ease; }
.nav-toggle__icon::before,
.nav-toggle__icon::after {
  content: "";
  position: absolute;
  left: 0;
  transition: transform .2s ease, top .2s ease, opacity .2s ease;
}
.nav-toggle__icon::before { top: -7px; }
.nav-toggle__icon::after { top: 7px; }
/* Bars -> X. The icon stays on the ivory masthead bar in both states —
   opening doesn't move it onto the terracotta overlay — so its colour
   (--ink, 15.71:1 against ivory) doesn't need to change with it. */
.nav-toggle[open] .nav-toggle__icon { background: transparent; }
.nav-toggle[open] .nav-toggle__icon::before { top: 0; transform: rotate(45deg); }
.nav-toggle[open] .nav-toggle__icon::after { top: 0; transform: rotate(-45deg); }

@media (max-width: 900px) {
  .nav-toggle { display: block; }
  .nav-toggle > summary {
    display: flex;
    align-items: center;
    justify-content: center;
    position: relative;
    z-index: 2; /* stays above the open overlay, which is a later sibling */
    width: 44px;
    height: 44px;
  }
  .mark { position: relative; z-index: 2; } /* same reason — stays visible above the overlay */

  .nav-toggle .nav { display: none; }
  .nav a:first-child { display: inline-block; } /* Home, hidden above at desktop */
  .nav-toggle[open] .nav {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 8px;
    position: fixed;
    /* Not inset:0 — .masthead's backdrop-filter makes it the containing
       block for a fixed descendant, so a percentage/inset offset like
       bottom:0 resolves against the ~72px header bar, not the viewport,
       and the menu opens as a short strip instead of full-screen. top/left
       stay 0 since .masthead is already pinned flush with the viewport's
       own top-left; width/height use viewport units specifically because
       they read the true viewport regardless of the containing block. */
    top: 0;
    left: 0;
    width: 100vw;
    height: 100vh;
    z-index: 1;
    background: var(--terracotta);
  }
  /* Terracotta has no existing measured pairing on this site — measured
     for this feature (see the session report): ivory 3.46:1 FAIL, on-ink
     1.70:1 FAIL, ink 4.54:1 PASS. Ink is the only one of the three that
     clears 4.5:1, so it's the only option, not a preference. */
  .nav-toggle[open] .nav a {
    color: var(--ink);
    font-size: 1.4rem;
    letter-spacing: .04em;
    padding: 18px 28px;
  }
  /* The site-wide focus ring is terracotta (:focus-visible, above) —
     invisible against a terracotta background. Swapped to ink here
     only, where that pairing would otherwise disappear. */
  .nav-toggle[open] .nav a:focus-visible { outline-color: var(--ink); }

  /* Book (header.njk) keeps its own button look inside the open overlay.
     .nav-toggle[open] .nav a above is more specific than .btn/.masthead
     .btn and would otherwise win: its color:ink would sit on the
     button's own ink background (invisible text), and its 1.4rem/18px
     28px would replace .masthead .btn's compact sizing. Restores exactly
     that sizing and ivory text; background/hover already come from .btn
     itself and don't need repeating here. */
  .nav-toggle[open] .nav a.btn {
    color: var(--ivory);
    font-size: .72rem;
    letter-spacing: .14em;
    padding: 11px 22px;
  }
  .nav-toggle[open] .nav a.btn:hover,
  .nav-toggle[open] .nav a.btn:focus-visible { color: var(--ivory); }

  html.nav-open { overflow: hidden; }
}

/* ---------- hero (Light variant only) ---------- */
.hero {
  position: relative;
  min-height: min(88vh, 820px);
  display: flex;
  align-items: center;
  overflow: hidden;
  color: var(--ink);
}
.hero__img { position: absolute; inset: 0; }
.hero__img img { width: 100%; height: 100%; object-fit: cover; object-position: 70% 50%; }
.hero__scrim {
  position: absolute;
  inset: 0;
  /* Load-bearing: text directly on the photo fails contrast. Strongest at the left. */
  background: linear-gradient(100deg,
    var(--ivory) 0%,
    var(--ivory) 30%,
    rgba(246, 243, 238, .86) 48%,
    rgba(246, 243, 238, .12) 78%,
    rgba(246, 243, 238, 0) 100%);
}
.hero__in {
  position: relative;
  z-index: 2;
  width: 100%;
  max-width: var(--width-page);
  margin: 0 auto;
  padding: 96px var(--gutter);
}
.hero__col { max-width: none; }
/* Shrink-wraps to the wider of h1 / .hero__sub (always h1) and centres the
   other on it — the same flex approach .hero__sub itself already uses,
   one level up, rather than a new layout method. */
.hero__mark { display: inline-flex; flex-direction: column; align-items: center; }
.hero h1 { font-size: var(--size-hero); line-height: 1; letter-spacing: .09em; margin: 0; }
/* --size-hero-sub's ratio to --size-hero comes from the actual logo SVG's
   letterform proportions, not the comp — see tokens.css for the measurement. */
.hero__sub { display: flex; align-items: center; gap: 16px; margin-top: 16px; }
.hero__sub span {
  font-family: 'Lora', serif;
  font-size: var(--size-hero-sub);
  letter-spacing: .34em;
  white-space: nowrap;
}
.hero__sub i { display: block; width: var(--rule-hero-sub); height: 1px; background: currentColor; opacity: .5; }
.hero__tag {
  font-family: 'Cormorant Garamond', serif;
  font-style: italic;
  font-weight: 300;
  font-size: var(--size-tag);
  line-height: 1.28;
  margin: 44px 0 0;
  /* No max-width: the two sentences should flow as one line wherever
     there's room, wrapping only when the viewport itself is too narrow
     (mobile) — a character-count cap here would force an early wrap
     regardless of available space. */
}
.hero__say { margin: 20px 0 0; max-width: var(--measure-say); font-size: 1rem; color: var(--stone); }
.hero__cta { margin-top: 42px; display: flex; gap: 16px; flex-wrap: wrap; align-items: center; }

@media (max-width: 820px) {
  .hero__in { padding: 72px var(--gutter); }
}
/* Short-landscape phones (rotated portrait phones, not tablets — tied to
   height, not width, and separate from the width breakpoint above). The
   88vh/820px min-height floor was never the problem: content already
   exceeds it well before this point, so .hero just grows to fit content
   regardless of the floor. The actual fix is vertical space allocation —
   .hero__in's padding and the gaps between the tagline/description/button
   — not font-size, which is untouched here. */
@media (max-height: 500px) and (orientation: landscape) {
  /* .hero is a <section>, so it also carries the sitewide
     `section { padding: 120px 0; }` rule on top of .hero__in's own
     padding below — that 240px, not the min-height floor, turned out to
     be the single biggest contributor to the overflow. */
  .hero { min-height: 0; padding: 0; }
  .hero__in { padding: 6px var(--gutter); }
  .hero__tag { margin-top: 10px; }
  .hero__say { margin-top: 6px; }
  .hero__cta { margin-top: 8px; }
}

/* ---------- what we do / category cards ---------- */
/* Was flex/space-between/align-items:flex-end, to sit the old lede
   paragraph beside the heading (removed 2026-09-15, replaced with a
   .rule--accent divider under it instead, matching the consulting/team
   header pattern) — h2 and the hr just stack in normal flow now, so the
   flex layout no longer has a second item to lay out and is dropped
   with it. */
.index__head { margin-bottom: 18px; }
/* Gap widened from 28px to 36px this pass: a 1.05 scale on a ~400px-tall
   card (true at the 900px 2-column breakpoint) grows it by ~20px total,
   ~10px each side — the old 28px row gap left too little clearance and
   adjacent rows started touching at scale. Checked at 1600/900/375. */
.cat-cards { display: grid; grid-template-columns: repeat(5, 1fr); gap: 36px; }
/* Replaces last pass's shadow+lift (box-shadow, padding-bottom to give it
   room): dropped entirely, no box-model side effects to manage. Pure
   transform:scale reads as a clear lift with nothing to reflow. 1.05 —
   inside the requested 1.04-1.06 range — checked against neighbouring
   cards at every grid breakpoint (5-, 2- and 1-column) with the gap
   above, no overlap at any of them. */
.cat-card {
  display: block;
  color: inherit;
  text-decoration: none;
  transition: transform .18s ease;
}
.cat-card:hover, .cat-card:focus-visible {
  transform: scale(1.05);
}
.cat-card__img { margin: 0 0 14px; }
/* 4:5, not the site's usual 3:2 — these five source photos are close crops
   with off-centre detail (a plant at the right edge, a hand sweeping in
   from a top corner), and a tighter 3:4 crop pushed some of that past the
   frame on more of the five than 4:5 did. 4:5 still reads clearly as a
   deliberate portrait card, not a stretched landscape one. */
/* height:auto is load-bearing: the <img> tags carry width="2400"
   height="1600" attributes (native 3:2) for CLS purposes, and without
   overriding height explicitly, the browser uses that attribute-implied
   height instead of the aspect-ratio below, since aspect-ratio only
   takes effect when a dimension resolves to auto. This went unnoticed
   at the old 3:2 crop only because it happened to match the native
   ratio, masking the bug. */
.cat-card__img img { display: block; width: 100%; height: auto; aspect-ratio: 4 / 5; object-fit: cover; }
.cat-card__name {
  display: block;
  font-family: 'Cormorant Garamond', serif;
  font-size: 1.3rem;
  line-height: 1.2;
  transition: color .2s ease;
}
.cat-card:hover .cat-card__name, .cat-card:focus-visible .cat-card__name { color: var(--terracotta); }
.cat-card__tagline { display: block; margin-top: 4px; }
/* .cat__more (main.css above) is stone at rest, terracotta only on its own
   :hover/:focus-visible — right for the hub, where it's a real, independent
   link. Here the whole card is already the link, so this is a <span>, and
   the label needs to read as an affordance with no interaction at all
   (touch has no hover) — hence terracotta by default, overriding .cat__more
   here specifically. The border-bottom still switches hairline -> terracotta
   with the rest of the card's hover/focus state, keeping some of the
   original element's own feedback. */
.cat-card__more { color: var(--terracotta); margin-top: 10px; }
.cat-card:hover .cat-card__more, .cat-card:focus-visible .cat-card__more { border-bottom-color: var(--terracotta); }
/* Same collapse points as .roster below, for the same reason: five/four
   items don't divide evenly at every width, so match the site's existing
   card-grid rhythm rather than pick new breakpoints for this one grid. */
@media (max-width: 900px) { .cat-cards { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 480px) { .cat-cards { grid-template-columns: 1fr; } }
/* Trailing icon (src/scripts/cat-card-trail.js). top/left start at 0 and
   are moved entirely via transform: translate() in JS — never top/left —
   so the browser can animate it on the compositor rather than laying out
   the page on every frame. Positioned by its own top-left corner rather
   than centred on the cursor: simplest to reason about from JS, and the
   script's OFFSET_X/Y already puts it visibly beside the pointer rather
   than under it. 48x64 — double the SVG's original 24x32 viewBox, same
   aspect ratio. JS never runs at all on touch or reduced-motion (see the
   script) — this CSS existing is harmless either way, since nothing ever
   un-hides it there. */
.cat-card-trail {
  position: fixed;
  top: 0;
  left: 0;
  width: 48px;
  height: 64px;
  pointer-events: none;
  z-index: 50;
  opacity: 0;
  transition: opacity .15s ease;
  will-change: transform;
}
.cat-card-trail--visible { opacity: 1; }

/* ---------- category page service listing (also .idx__name above) ---------- */
.idx { border-top: 1px solid var(--ink); }
.idx__row {
  display: grid;
  grid-template-columns: 1fr auto;
  grid-template-areas: "name price" "note price";
  column-gap: 32px;
  align-items: baseline;
  padding: 26px 0;
  border-bottom: 1px solid var(--hair);
  text-decoration: none;
}
/* interactive rows only — the homepage index, where each row links to a category */
a.idx__row { transition: padding-left .25s ease; }
a.idx__row:hover, a.idx__row:focus-visible { padding-left: 12px; }
a.idx__row:hover .idx__name { color: var(--terracotta); }
.idx__name {
  grid-area: name;
  font-family: 'Cormorant Garamond', serif;
  font-size: 1.75rem;
  line-height: 1.15;
  transition: color .2s ease;
}
.idx__svc { grid-area: name; }  /* a service name — Jost, per the brand type rules */
.idx__note { grid-area: note; color: var(--stone); font-size: .9rem; margin-top: 4px; }
.idx__price {
  grid-area: price;
  text-align: right;
  font-size: .88rem;
  color: var(--stone);
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
.idx__price b { display: block; color: var(--terracotta); font-size: 1.1rem; font-weight: 600; }
@media (max-width: 560px) { .idx__name { font-size: 1.35rem; } }

/* ---------- category page — Facials & skin is the built pattern for all five ---------- */
.page-title { font-size: var(--size-h2); margin-bottom: var(--space-group); }
/* Each .subsection is a <section> (see the five treatments/*.njk templates
   — this markup is duplicated across all five, not a shared partial; see
   the session report), so it also carries the sitewide
   `section { padding: 120px 0; }` (main.css:25) with no override, on top
   of the margin-top below. That's the actual source of the oversized
   between-group gap — not the margin-top, which is --space-group, the
   token already meant for exactly this ("gap between major sub-groups in
   a section", tokens.css) and was already the right value underneath the
   redundant 240px of inherited padding. Zeroing this is what does the
   fix; margin-top is untouched. */
.subsection { padding: 0; }
.subsection + .subsection { margin-top: var(--space-group); }
/* Policies page only: the sibling rule above only ever matches a
   .subsection preceded by another .subsection, so the first one (Late
   arrivals) got no top margin at all — just the page header's own
   divider (.rule--accent, 28px bottom margin) with nothing to add to
   it, versus --space-group (64px) between every other pair. Scoped to
   this one heading's own id rather than a structural "header + hr +
   .subsection" selector, which would also catch About's and Image
   Consulting's identical header markup and add space there that
   wasn't asked for and isn't broken. */
#pol-late { margin-top: var(--space-group); }
/* About page only (about.njk): widens the bio paragraph from the
   sitewide default (p's own `max-width: var(--measure)`, 62ch) toward
   the salon photo's full width below it (1112px inside .wrap at
   desktop) — flagged as narrower than the photo. Not widened all the
   way: at this page's actual copy length, the line count only drops
   from 8 to 7 once width passes ~658px, and that 7-line wrap averages
   94 characters per line, past the 75-85 target. 66ch (~650px at this
   page's font-size) is the widest value that stays at 8 lines/~82
   characters average — measured directly in-browser, not estimated.
   A one-off value, not promoted to a tokens.css measure (used exactly
   once) per CLAUDE.md's data-model rule allowing that as a judgement
   call. Image Consulting's own .subsection > p is untouched — this
   selector only matches the page that opts in. */
.subsection--intro p { max-width: 66ch; }
.subsection__intro { margin: .5rem 0 0; color: var(--stone); max-width: var(--measure-lede); }
.subsection > .idx { margin-top: 1.5rem; }
.subsection__note { margin: 1rem 0 0; color: var(--stone); font-size: .9rem; max-width: var(--measure-lede); }

/* Photo band, constrained to the content column — category banners, and the
   About and Image consulting photographs. Same treatment as the comp's
   .team__plate, which stays full-bleed for the homepage team shot. */
.plate { margin: var(--space-group) 0; }
.plate img { width: 100%; height: var(--size-team-plate); object-fit: cover; }

/* ---------- header + hero, Image consulting ---------- */
/* Heading/copy left, photo right — matches the general shape of .consult
   and .book (text beside image) rather than a full-bleed absolutely-
   positioned crop like .consult__img, since that's what forced a
   landscape crop on this portrait photo in the first place (see below).
   38% for the image column: enough width for a 900x1350 portrait to read
   clearly beside a short heading + two-line paragraph, without letting a
   tall photo dominate a light amount of copy. */
.ic-header { display: grid; grid-template-columns: minmax(0, 60%) minmax(0, 38%); gap: 40px; align-items: start; }
/* .plate's fixed band height + object-fit: cover is tuned for landscape
   photos (1600x1067 elsewhere on the site) — tested directly against
   sarah-chata-romano.jpg (900x1350 portrait) and it cropped to a sliver,
   eyes cut off. height:auto here instead: nothing cropped, nothing
   stretched, at any column width. Scoped to this page only — no other
   page has a portrait hero yet. */
.ic-hero { margin: 0; }
.ic-hero img { width: 100%; height: auto; }
@media (max-width: 760px) { .ic-header { grid-template-columns: 1fr; gap: 28px; } }

/* Bulletless content list — credentials on About, hours on Policies.
   The site's only plain prose list; the price pages use .idx instead. */
.list-plain { list-style: none; margin: 1rem 0 0; padding: 0; }
.list-plain li { padding: 4px 0; }

/* ---------- products, About ---------- */
/* retail-wall-wide is portrait 1067x1600, so it sits beside the lists rather
   than in the landscape .plate band. Ratio-preserving, not cropped. */
.products {
  display: grid;
  grid-template-columns: minmax(0, 34%) minmax(0, 66%);
  gap: 40px;
  margin-top: 1.5rem;
  align-items: start;
}
.products__plate { margin: 0; }
.products__plate img { width: 100%; height: auto; }
/* .meta sets size and tracking but not family; h3 would otherwise inherit
   Cormorant, which the brand rules forbid at label size. */
.products__group { display: block; font-family: 'Jost', system-ui, sans-serif; }
.list-plain + .products__group { margin-top: var(--space-group); }
.products__name { font-weight: 400; }
@media (max-width: 760px) { .products { grid-template-columns: 1fr; gap: 24px; } }

/* ---------- testimonials, Image consulting ---------- */
/* Card grid, added 2026-09-17 (previously a photo beside plain quote
   blocks — see image-consulting.njk's own comment on why the photo moved
   and why this uses --hair/--linen rather than a shadow: this site has
   no bordered/shadowed card anywhere to match — cat-cards and the roster
   are both boxless by deliberate choice). Same 900px/480px collapse
   points as .cat-cards and .roster, for the same reason those two share
   them: match the site's existing card-grid rhythm rather than pick new
   breakpoints for this one grid. */
.testimonials__grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 32px;
  margin-top: 1.5rem;
}
/* Reset the browser default blockquote indent/margin — nothing sitewide
   already does this, no other page had quoted material before this. */
.testimonials__item {
  margin: 0;
  padding: 32px;
  border: 1px solid var(--hair);
  background: var(--linen);
}
.testimonials__item p { margin: 0; max-width: none; }
.testimonials__item cite {
  display: block;
  margin-top: 16px;
  font-style: normal;
  font-size: .88rem;
  color: var(--terracotta);
}
@media (max-width: 900px) { .testimonials__grid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 480px) { .testimonials__grid { grid-template-columns: 1fr; } }

/* ---------- photo gallery, Photography ---------- */
/* No existing page holds a same-size photo grid — .cat-cards is five fixed
   category cards with text overlays, .products is one photo beside two
   text lists. This is new, but built from the same materials: --hair,
   --stone and --space-group (all already tokens), and the same 900px/480px
   collapse points .cat-cards and .roster already use, so a 3-6 photo
   gallery still reads as part of the same grid system rather than a new
   one invented for this page alone. 3 columns at desktop (not .cat-cards'
   5) — five was sized for five fixed category cards; a gallery of 3-6
   photos suits fewer, larger columns. */
.photo-gallery { display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px; margin-top: 1.5rem; }
/* Mixed portrait/landscape sources (see photography.njk) forced to the same
   tile shape via a fixed aspect-ratio + cover — the same treatment
   .cat-card__img already uses for its five source images, not a new idea. */
.photo-gallery img { display: block; width: 100%; height: auto; aspect-ratio: 4 / 5; object-fit: cover; }
/* Per-image override, portrait-laughing-2.jpeg only (see photography.njk's
   own comment) — the gallery's default center crop cut too tight against
   her raised elbow on the left at this photo's particular landscape
   proportions (2400x1600 into a 4:5 tile crops horizontally). Shifting the
   window left (a lower X% shows more of the image's own left side) clears
   the elbow without cutting her hair or shoulder on the right. Scoped to
   this one <img> via its own class — the other five tiles keep the plain
   .photo-gallery img rule above untouched. */
.photo-gallery__img--left { object-position: 30% center; }
@media (max-width: 900px) { .photo-gallery { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 480px) { .photo-gallery { grid-template-columns: 1fr; } }

/* ---------- treatments hub ---------- */
/* Hairline-separated editorial blocks, not cards — the system has no boxed
   idiom anywhere, so weight comes from space and rules. Reuses .idx__name for
   the category title and .meta for the tagline and the link. */
.cat {
  display: grid;
  /* head:body kept at the original 38:62 ratio (was minmax(0,38%)
     minmax(0,62%)) — percentages can't mix with the new fixed 120px
     image column (120px + 100% would overflow the row), so the same
     ratio is expressed in fr, which divides the space left over after
     the fixed column instead. */
  grid-template-columns: 120px minmax(0, 38fr) minmax(0, 62fr);
  grid-template-areas: "img head body";
  gap: 40px;
  align-items: start;
  padding: var(--space-group) 0;
  border-top: 1px solid var(--hair);
}
.cat:first-of-type { border-top-color: var(--ink); }
.cat:last-of-type { border-bottom: 1px solid var(--hair); }
/* Sizes the reused .cat-card__img (homepage card image class — same
   aspect-ratio/object-fit rule, not a new one) for this row's much
   smaller, list-like layout. 120px is deliberately modest — this page's
   job is still the list and the links, not another image-forward
   section like the homepage cards. */
.cat__img { grid-area: img; margin: 0; width: 120px; }
.cat__head { grid-area: head; }
.cat__body { grid-area: body; }
.cat__head a { text-decoration: none; }
.cat__head a:hover, .cat__head a:focus-visible { color: var(--terracotta); }
.cat__tagline { display: block; margin-top: 10px; color: var(--terracotta); }
.cat__subs { list-style: none; margin: 0; padding: 0; color: var(--stone); }
.cat__subs li { padding: 4px 0; }
.cat__more {
  display: inline-block;
  margin-top: 20px;
  text-decoration: none;
  padding-bottom: 6px;
  border-bottom: 1px solid var(--hair);
}
.cat__more:hover, .cat__more:focus-visible { color: var(--terracotta); border-bottom-color: var(--terracotta); }
@media (max-width: 760px) {
  /* Image moves above the heading (grid-template-areas order), same
     120px width as desktop — kept modest rather than stretched to the
     column's full width, so it doesn't overwhelm the heading/list
     stacked beneath it on a narrow screen. */
  .cat { grid-template-columns: 1fr; grid-template-areas: "img" "head" "body"; gap: 20px; }
}

/* ---------- consulting ---------- */
.consult { background: var(--ink); color: var(--ivory); padding: 0; overflow: hidden; }
.consult__grid { display: grid; grid-template-columns: minmax(0, 42%) minmax(0, 58%); align-items: stretch; }
.consult__img { position: relative; min-height: 520px; }
.consult__img img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: 50% 22%; }
.consult__body { padding: var(--pad-consult-body); }
.consult h2 { color: var(--ivory); max-width: var(--measure-consult-head); }
.consult p { color: var(--on-ink); max-width: var(--measure-consult-body); }
.consult .meta { color: var(--terracotta); }
.consult__price {
  display: flex;
  gap: 40px;
  flex-wrap: wrap;
  margin: 36px 0 0;
  padding-top: 26px;
  border-top: 1px solid #3A3733;
}
.consult__price div { min-width: 120px; }
.consult__price b {
  display: block;
  font-family: 'Cormorant Garamond', serif;
  font-size: 1.5rem;
  font-weight: 400;
  color: var(--ivory);
  margin-bottom: 2px;
}
.consult__price span { font-size: .82rem; color: var(--on-ink); }
@media (max-width: 900px) {
  .consult__grid { grid-template-columns: 1fr; }
  .consult__img { min-height: 360px; }
}

/* ---------- team ---------- */
/* The sitewide `section { padding: 120px 0; }` (main.css:25) is tuned for
   a two-column section like .consult; #team is three short lines, so the
   same bottom padding on top of .team__intro's own margin-bottom read as
   a large empty gap before the team photo. Top padding is untouched — it
   still reads fine, this is a bottom-only fix. Separation from the photo
   above is still real: .team__intro's own margin-bottom (below) plus this
   section's own top padding. */
#team { padding-bottom: 0; }
/* Two earlier passes here (a repositioned object-position, then
   object-fit: contain below 760px) both worked around a fixed-height
   box fighting this photo's own aspect ratio — first trading which
   person got cropped, then trading crop for letterboxing. This drops
   the fixed height entirely instead: width:100% of .wrap, height:auto,
   no object-fit/object-position/background at any width, so the image's
   own aspect ratio sets its own rendered height and the whole photo is
   always visible with nothing to crop or letterbox. Spacing above/below
   is unaffected by this — .team__intro's own margin-bottom and the
   roster section's ordinary top padding (both flow-based, not tied to
   any assumed image height) already provided the gap on either side. */
.team__plate img { width: 100%; height: auto; }
.team__intro { max-width: 640px; margin: 0 0 var(--space-group); }
.roster { display: grid; grid-template-columns: repeat(4, 1fr); column-gap: 40px; row-gap: 52px; }
.roster__p .n { font-family: 'Cormorant Garamond', serif; font-size: 1.6rem; line-height: 1.12; display: block; }
.roster__p .r { display: block; margin-top: 8px; font-size: .82rem; color: var(--stone); letter-spacing: .02em; }
/* .idx__name .bar reuses this exact rule (Policies page's sub-heading
   accent marks, added 2026-09-15) — .bar itself is scoped to
   .roster__p, so a standalone .bar class matches nothing outside it;
   this adds the one other context that needs it, same declarations,
   not a duplicate rule with its own values. Roster's own .bar is
   untouched — this only adds a second selector, nothing about
   .roster__p .bar's own behaviour changes. */
.roster__p .bar,
.idx__name .bar { display: block; width: 26px; height: 1px; background: var(--terracotta); margin-bottom: 14px; }
@media (max-width: 900px) { .roster { grid-template-columns: repeat(2, 1fr); row-gap: 40px; } }
@media (max-width: 480px) { .roster { grid-template-columns: 1fr; } }

/* ---------- book (homepage only) ---------- */
/* overflow-x:hidden added for the directional reveal (data-reveal-left/
   -right on .book__img/.book__body, added 2026-09-15, matching the
   consulting section's identical image/copy split) — unlike .consult,
   this section had no existing overflow guard to lean on. Measured at
   375px, the worst case: the copy's +32px pre-reveal offset lands its
   right edge at 373.2px against a 375px viewport — only ~2px of
   clearance from the site's own --gutter (34px) minus the 32px slide
   distance. That's real, not theoretical: a slightly different
   scrollbar width or font metrics could tip it into overflow, so this
   contains it explicitly rather than relying on that margin holding. */
#book { overflow-x: hidden; }
.book__grid { display: grid; grid-template-columns: minmax(0, 42%) minmax(0, 58%); align-items: center; gap: 40px; }
/* Same crop as the homepage card for this image (.cat-card__img img,
   "what we do" section) — reused, not a new crop. */
.book__img img { width: 100%; height: auto; aspect-ratio: 4 / 5; object-fit: cover; }
.book__body { text-align: center; }
.book__body p:first-of-type { margin: 20px auto 0; }
.book__body p:last-of-type { margin: 26px auto 0; }
/* Narrower than the sitewide two-column breakpoint (900px, .consult__grid
   and .roster above) so this stays two-column through tablet width and
   only stacks at true mobile — see index.njk's comment on this section
   for why. */
@media (max-width: 760px) {
  .book__grid { grid-template-columns: 1fr; gap: 28px; }
  /* Capped rather than left at the 4:5 crop above: at full mobile
     width, 4:5 runs tall enough to push the button/WhatsApp line well
     down the viewport — the brief calls for the image not to dominate
     here specifically. object-fit:cover already on the rule above
     still applies, so this just crops the same image shorter. */
  .book__img img { aspect-ratio: auto; height: 220px; }
}
@media (min-width: 761px) {
  /* Left-aligned beside a fixed image instead of centered (the brief's
     call — centered text reads oddly next to it here). Without this,
     the auto-centering directly above (needed so the paragraphs' boxes
     land correctly at the centered mobile breakpoint) would pull them
     toward the middle of this wider column instead of flush with the
     heading on its left edge. */
  .book__body { text-align: left; }
  .book__body p:first-of-type,
  .book__body p:last-of-type { margin-left: 0; margin-right: 0; }
}

/* ---------- footer ---------- */
.foot { background: var(--ink); color: var(--ivory); padding: 92px 0 40px; }
.foot a { color: var(--ivory); }
.foot .meta { color: var(--on-ink); }
.foot__grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 36px; }
.foot__grid p { margin: 6px 0 0; font-size: .9rem; max-width: none; }
.foot__grid .meta { display: block; }
.foot__hours-note { color: var(--on-ink); font-size: .78rem; margin-top: 10px; }
.foot__map { display: block; width: 100%; height: 160px; margin-top: 14px; border: 0; }
.foot__links { margin: 36px 0 0; font-size: .9rem; }
.foot__base {
  margin-top: 64px;
  padding-top: 24px;
  border-top: 1px solid #3A3733;
  display: flex;
  justify-content: space-between;
  gap: 24px;
  flex-wrap: wrap;
  font-size: .8rem;
  color: var(--on-ink);
}
@media (max-width: 760px) { .foot__grid { grid-template-columns: 1fr 1fr; gap: 28px; } }

/* ---------- motion ---------- */
@media (prefers-reduced-motion: reduce) {
  * { transition: none !important; scroll-behavior: auto; }
}

/* ---------- scroll reveal (homepage only, src/scripts/reveal.js) ---------- */
/* No rule here hides [data-reveal]/[data-reveal-item] on its own — every
   selector below is gated behind html.js-reveal, a class reveal.js adds
   itself the moment it actually runs. If that script never loads or
   never runs, none of this ever matches and these elements sit in their
   normal, fully visible layout — the no-js/JS-failure fallback lives
   here, not in the JS. [data-reveal-group] (a card grid) is deliberately
   absent from these selectors: the group itself is only an
   IntersectionObserver target in reveal.js, never a visual one — its
   [data-reveal-item] children carry the actual animation, staggered. */
html.js-reveal [data-reveal],
html.js-reveal [data-reveal-item] {
  opacity: 0;
  transform: translateY(20px);
  transition: opacity 1.1s cubic-bezier(.16, 1, .3, 1), transform 1.1s cubic-bezier(.16, 1, .3, 1);
}
html.js-reveal [data-reveal].is-revealed,
html.js-reveal [data-reveal-item].is-revealed {
  opacity: 1;
  transform: none;
}
/* [data-reveal-right]/[data-reveal-left] variants: same trigger
   (reveal.js observes them alongside [data-reveal]/[data-reveal-group]
   with the one shared observer, same threshold/rootMargin, fires once),
   same duration and easing — only the axis and sign differ. Used by the
   consulting section's copy column (slides from the right) and its
   image beside it (slides from the left, the mirror) — index.njk. 32px
   is deliberately short ("a short, subtle distance, not an off-screen
   slide" per the brief) — .consult already carries `overflow: hidden`
   (main.css, "consulting" section) from before this feature existed,
   which happens to also contain both 32px translateX offsets so
   neither can push the page's own scrollWidth out — confirmed
   empirically, not the reason that rule is there, so it's noted rather
   than relied on silently. */
html.js-reveal [data-reveal-right] {
  opacity: 0;
  transform: translateX(32px);
  transition: opacity 1.1s cubic-bezier(.16, 1, .3, 1), transform 1.1s cubic-bezier(.16, 1, .3, 1);
}
html.js-reveal [data-reveal-left] {
  opacity: 0;
  transform: translateX(-32px);
  transition: opacity 1.1s cubic-bezier(.16, 1, .3, 1), transform 1.1s cubic-bezier(.16, 1, .3, 1);
}
html.js-reveal [data-reveal-right].is-revealed,
html.js-reveal [data-reveal-left].is-revealed {
  opacity: 1;
  transform: none;
}
/* Belt and suspenders with reveal.js's own prefers-reduced-motion check
   (which skips the observer entirely, so .is-revealed is never added):
   the sitewide `* { transition: none }` rule above silences the
   transition but doesn't touch the opacity:0/translateY (or translateX,
   for [data-reveal-right]/[data-reveal-left]) starting values the rules
   above introduce, so on its own it would leave these elements
   permanently invisible for a reduced-motion visitor who never scrolls
   them into view. This forces the same elements to their final, fully
   visible state unconditionally instead — immediately, not "instantly
   once triggered". */
@media (prefers-reduced-motion: reduce) {
  html.js-reveal [data-reveal],
  html.js-reveal [data-reveal-item],
  html.js-reveal [data-reveal-right],
  html.js-reveal [data-reveal-left] {
    opacity: 1;
    transform: none;
    transition: none;
  }
}
