/* =========================================================================
   Erica Iriarte, Southern California real estate.
   The whole design system. Hand-written, no build step.

   Brand values come from PART 1 of Brett's build spec, which pulled them from
   The Rockwell Group, the site the client named as the aesthetic reference.

   Every rule tagged [CHECKLIST n.n] is an acceptance criterion from PART 2 of
   that spec. Each one is a defect a previous Appomark build shipped. Do not
   "simplify" them away.
   ========================================================================= */

/* [CHECKLIST 2.3] FIRST rule in the sheet, deliberately. Without it iOS Safari
   paints button text in system blue while desktop renders black, so the bug is
   invisible in desktop testing. */
button {
  color: inherit;
  font: inherit;
}

/* ------------------------------------------------------------------ tokens */
:root {
  --paper: #FFFFFF;             /* pure white */
  --cream: #FBF9F7;             /* page background, warm off white */
  --sand: #ECE7E1;              /* warm beige, alternating sections */
  --line: #DDDDDD;              /* hairline rules and borders */
  --ink: #000000;               /* body and heading text on light */
  --ink-soft: rgba(0, 0, 0, .66);
  --ink-faint: rgba(0, 0, 0, .42);
  /* ROUND 7 PART 1: the client could not see the letter E in the serif italic
     lines. The cause is NOT weight, which was already 400 and always has been
     (the font link loads only ital 400, so the 300 italic Brett warns about was
     never available). It is the colour: an italic serif has far thinner strokes
     than Inter at the same opacity, so .66 that reads fine on a label goes
     ghostly on a lede. This token is serif-italic ONLY, and exists precisely so
     that raising it never touches the 36 other rules on --ink-soft, where the
     softer grey is still wanted on labels and captions. */
  --ink-serif: rgba(0, 0, 0, .8);
  /* ROUND 3 PART 1: the deep slate token is DELETED, deliberately, so it
     cannot come back. The site is a two colour world: white and beige with
     black text, and white only on photographs over the neutral scrim. */

  /* [CHECKLIST 2.4] Neutral near black RGB triplet for photo overlays. NEVER a
     brand colour: a tinted scrim colours every photograph on the site and costs
     a full revision round to trace back. */
  --scrim: 10, 9, 8;

  --display: 'Noto Serif Display', 'Times New Roman', serif;
  --body: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;

  --nav-h: 92px;
  --wrap: 1240px;
  --ease: cubic-bezier(.22, .61, .36, 1);

  /* ROUND 3: the vertical rhythm is ONE scale, here and nowhere else. Every
     section pays the gap twice, once as its own bottom padding and once as
     the next section's top, so 56px a side is the 112px desktop budget the
     responsive check enforces. Never hard-code a fat literal padding on a
     section; if a section needs more air, raise this and re-run the check. */
  --gap-section: 56px;
  --gap-tight: 40px;
}

@media (max-width: 860px) {
  :root {
    --gap-section: 36px;
    --gap-tight: 28px;
  }
}

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

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

/* ROUND 9 PART 3: any anchored target lands with its heading clear of the
   fixed nav, not hidden behind it. Covers #reviews (the QR and share links)
   and every in-page anchor the forms use. */
:target,
section[id] {
  scroll-margin-top: 96px;
}

/* ROUND 3 PART 2: the readability lift. Body is 18px at 1.7, and EVERY
   font-size in this sheet is either a plain px value of 13px or more, or a
   clamp() whose floor is at least 13px. Bare vw font sizes are BANNED: they
   rendered the hero lede at 10px and the eyebrows at 8px on a 500px window,
   which is the single measured cause of the "hard to read" complaint. */
body {
  margin: 0;
  background: var(--cream);
  color: var(--ink);
  font-family: var(--body);
  font-size: 18px;
  font-weight: 400;
  line-height: 1.7;
  /* [CHECKLIST 2.5] Belt and braces against sideways scroll on a phone. */
  overflow-x: hidden;
}

/* `img { height: auto }` is load-bearing. Every <img> carries width/height
   attributes to reserve its box against layout shift. Without this the
   attribute becomes the used height and a 2400x1800 photo in a 580px column
   renders 580x1800, badly stretched. */
img {
  max-width: 100%;
  height: auto;
  display: block;
}

a {
  color: inherit;
}

h1, h2, h3, h4 {
  margin: 0;
  font-weight: 300;
}

p {
  margin: 0;
}

ul, ol {
  margin: 0;
  padding: 0;
}

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

.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 200;
  background: var(--ink);
  color: var(--paper);
  padding: 12px 18px;
  font-size: 13px;
}

.skip-link:focus {
  left: 12px;
  top: 12px;
}

.wrap {
  width: 100%;
  max-width: var(--wrap);
  margin: 0 auto;
  padding: 0 40px;
}

@media (max-width: 860px) {
  .wrap { padding: 0 24px; }
}

@media (max-width: 560px) {
  .wrap { padding: 0 20px; }
}

/* ---------------------------------------------------------------- type kit */
/* Display face: Noto Serif Display 300, uppercase, wide letterspacing. */
.display {
  font-family: var(--display);
  font-weight: 300;
  text-transform: uppercase;
  letter-spacing: .05em;
  line-height: 1.14;
  color: var(--ink);
}

/* ROUND 4 PART 2.2: .d1 appears only in page headers, one per interior
   page, so the header H1 scale lives here. Was clamp(44px, 5.5vw, 64px). */
.d1 { font-size: clamp(36px, 4vw, 54px); }
.d2 { font-size: clamp(30px, 3.4vw, 44px); }
.d3 { font-size: clamp(20px, 2.4vw, 26px); }

/* Eyebrows and nav share the small uppercase Inter setting. */
.eyebrow {
  font-size: 13.5px;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 1.4px;
  color: var(--ink-soft);
  margin-bottom: 16px;
}

/* [CHECKLIST 1.2 note] Body paragraphs are SENTENCE CASE. The reference site
   sets some body copy uppercase at 12px; that is not copied here, it hurts
   readability on a consumer site.

   ROUND 3 PART 2: ledes are the style upgrade. Serif italic against the sans
   body, the editorial move. Needs the italic 400 axis in the Google Fonts link
   on every page. */
.lede {
  font-family: var(--display);
  font-style: italic;
  font-weight: 400;
  font-size: clamp(17px, 1.7vw, 20px);
  line-height: 1.65;
  color: var(--ink-serif);
  max-width: 62ch;
}

.prose p + p { margin-top: 18px; }

.prose {
  color: var(--ink-soft);
  max-width: 68ch;
}

.measure { max-width: 68ch; }

/* ----------------------------------------------------------------- buttons */
.btn {
  /* [CHECKLIST 2.1] inline-flex + nowrap, so a button never wraps to two lines
     however tight its column gets. */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  white-space: nowrap;
  gap: 10px;
  padding: 15px 30px;
  border: 1px solid var(--ink);
  border-radius: 0;
  background: none;
  font-family: var(--body);
  font-size: 13px;
  font-weight: 500;
  letter-spacing: 1.4px;
  text-transform: uppercase;
  text-decoration: none;
  cursor: pointer;
  transition: background-color .35s var(--ease), color .35s var(--ease),
              border-color .35s var(--ease), opacity .2s linear;
}

.btn-primary {
  background: var(--ink);
  color: var(--paper);
}

.btn-primary:hover {
  background: transparent;
  color: var(--ink);
}

.btn-ghost {
  background: transparent;
  color: var(--ink);
}

.btn-ghost:hover {
  background: var(--ink);
  color: var(--paper);
}

/* On a photograph the outline has to be white to be seen at all. */
.btn-on-photo {
  border-color: rgba(255, 255, 255, .85);
  color: #fff;
}

.btn-on-photo:hover {
  background: #fff;
  color: var(--ink);
}

.btn[disabled] {
  opacity: .4;
  cursor: not-allowed;
}

.btn-row {
  display: flex;
  flex-wrap: wrap;
  gap: 14px;
  margin-top: 32px;
}

/* A quiet text link with a rule under it, used for "more about" style links. */
.link-more {
  display: inline-block;
  margin-top: 26px;
  padding-bottom: 4px;
  border-bottom: 1px solid var(--ink);
  font-size: 13px;
  font-weight: 500;
  letter-spacing: 1.4px;
  text-transform: uppercase;
  text-decoration: none;
  transition: opacity .25s linear;
}

.link-more:hover { opacity: .6; }

/* -------------------------------------------------------------- navigation */
/* [CHECKLIST 2.1] Flex, never `1fr auto 1fr` with no gap: that collapses the
   bar into a squeezed cluster the moment the link set grows. */
.nav {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 100;
  background: transparent;
  border-bottom: 1px solid transparent;
  /* Background only. No shrink, no hide on scroll, no blur. */
  transition: background-color .35s var(--ease), border-color .35s var(--ease);
}

/* ROUND 3 PART 3: the bar runs the full viewport width. The lockup sits about
   24px from the left edge and the CTA about 24px from the right edge at
   desktop widths, with the links keeping a clear gap from the button. */
/* ROUND 5 PART 3: three zone grid, links genuinely centered in the viewport.
   THE RISE HOLLYWOOD HILLS TRAP LIVES HERE: below the burger breakpoint the
   links and the CTA are display:none, which removes them from the grid, so
   the burger falls into the middle column and renders dead centre. The mobile
   media query MUST redeclare the grid; it does, below. */
.nav-inner {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  width: 100%;
  min-height: var(--nav-h);
  margin: 0 auto;
  padding: 0 24px;
}
.nav-inner > .wordmark { justify-self: start; }
.nav-inner > .nav-links { justify-self: center; }
.nav-inner > .nav-cta { justify-self: end; }

/* [CHECKLIST 2.1] Solid cream with a hairline once scrolled past the hero. */
.nav.is-solid {
  background: var(--cream);
  border-bottom-color: var(--line);
}

/* [CHECKLIST 2.2] While the mobile menu is open the bar keeps a solid
   background whatever the scroll position, or the close X sits on a
   transparent bar over a light panel and disappears. */
body.menu-open .nav {
  background: var(--cream);
  border-bottom-color: var(--line);
}

/* The wordmark IS the brand: there is no logo file. Built as markup with one
   class so swapping in a real logo later is a single change. */
.wordmark {
  display: block;
  text-decoration: none;
  color: var(--ink);
  line-height: 1;
  /* ROUND 4 PART 1.3: the bar's flex gap is 32px; this margin adds to it, so
     the lockup never sits closer than 56px to ABOUT when the bar is tight. */
  margin-right: 24px;
}

/* ROUND 3 PART 3 grew the lockup to 24px. ROUND 4 PART 1.4 then took the
   subline to 9px/.24em: at 13px it alone was 272px wide and set the whole
   bar's width budget. The 24px wordmark stays. This is the ONE deliberate
   exception to the Round 3 "nothing below 13px" floor, ordered in Round 4. */
.wordmark-name {
  display: block;
  font-family: var(--display);
  font-weight: 300;
  font-size: 24px;
  letter-spacing: .18em;
  text-transform: uppercase;
  white-space: nowrap;
}

.wordmark-sub {
  display: block;
  margin-top: 5px;
  font-family: var(--body);
  font-size: 9px;
  font-weight: 400;
  letter-spacing: .24em;
  text-transform: uppercase;
  color: var(--ink-soft);
  white-space: nowrap;
}

/* Over the hero photograph, before the bar goes solid.

   [CHECKLIST 2.2] The `body:not(.menu-open)` guard is load-bearing, not tidiness.
   Opening the mobile menu at the TOP of the page makes the bar solid cream while
   the nav still has no `.is-solid` class, so without this guard the wordmark and
   the burger stayed white and vanished into the bar. It is the same defect the
   checklist calls out for the close X, one element over. */
body:not(.menu-open) .nav:not(.is-solid) .wordmark { color: #fff; }
body:not(.menu-open) .nav:not(.is-solid) .wordmark-sub { color: rgba(255, 255, 255, .8); }
body:not(.menu-open) .nav:not(.is-solid) .nav-link { color: #fff; }
body:not(.menu-open) .nav:not(.is-solid) .nav-link::after { background: #fff; }
body:not(.menu-open) .nav:not(.is-solid) .nav-cta .btn { border-color: rgba(255, 255, 255, .85); color: #fff; }
body:not(.menu-open) .nav:not(.is-solid) .nav-cta .btn:hover { background: #fff; color: var(--ink); }
body:not(.menu-open) .nav:not(.is-solid) .nav-burger { color: #fff; }

/* A page with no hero photograph behind the bar starts solid, so the bar is
   never white text on a cream page. Set on <body> per page. */
body.nav-solid .nav {
  background: var(--cream);
  border-bottom-color: var(--line);
}

.nav-links {
  display: flex;
  align-items: center;
  gap: 28px;
}

.nav-link {
  position: relative;
  font-size: 13px;
  font-weight: 500;
  letter-spacing: 1.2px;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--ink);
  padding: 6px 0;
  /* ROUND 4 PART 1.2: a squeezed bar folds "HOME SEARCH" at the space instead
     of holding the line. No nav element ever renders on two lines. */
  white-space: nowrap;
}

.nav-link::after {
  content: '';
  position: absolute;
  left: 0;
  bottom: 0;
  width: 0;
  height: 1px;
  background: var(--ink);
  transition: width .3s var(--ease);
}

.nav-link:hover::after,
.nav-link.is-current::after { width: 100%; }

.nav-cta { margin-left: 0; }

.nav-cta .btn {
  padding: 13px 22px;
  font-size: 13px;
}

.nav-burger {
  display: none;
  margin-left: auto;
  padding: 8px;
  border: 0;
  background: none;
  color: var(--ink);
  cursor: pointer;
}

/* [CHECKLIST 2.1] Switch to the mobile menu at 1230px, per the Round 4
   PART 1.5 fallback. Measured after the Round 4 type reductions: the bar
   needs 1196px of viewport to hold one line, so a 1150px breakpoint left a
   1151-1196 band where the nowrap links could only collide. Any future nav
   change re-derives this number: sum the lockup, the links, the gaps and the
   CTA, add the bar padding, and keep the breakpoint above it. */
@media (max-width: 1230px) {
  .nav-links,
  .nav-cta { display: none; }
  .nav-burger { display: block; justify-self: end; }
  /* Redeclared, not inherited: with links and CTA out of the grid the burger
     would land in the centre column. minmax(0,1fr) + end pins it hard right. */
  .nav-inner { grid-template-columns: minmax(0, 1fr) auto; padding: 0 24px; }
}

@media (max-width: 560px) {
  :root { --nav-h: 78px; }
  .nav-inner { padding: 0 20px; }
  .wordmark-name { font-size: 20px; letter-spacing: .14em; }
}

/* ------------------------------------------------------------ mobile panel */
/* [CHECKLIST 2.2] No Home item: the wordmark is the home link. No scrolling,
   shrinking or zoom animations inside the panel. */
.mobile-panel {
  position: fixed;
  inset: 0;
  z-index: 99;
  display: none;
  flex-direction: column;
  gap: 6px;
  background: var(--cream);
  /* [CHECKLIST 2.2] Reserve the fixed bar's height so no item can sit behind
     it on a 667px tall phone. */
  padding: calc(var(--nav-h) + 40px) 28px 40px;
  overflow-y: auto;
}

.mobile-panel.is-open { display: flex; }

.mobile-panel a {
  padding: 14px 0;
  border-bottom: 1px solid var(--line);
  font-family: var(--display);
  font-size: 22px;
  font-weight: 300;
  letter-spacing: .05em;
  text-transform: uppercase;
  text-decoration: none;
}

/* ROUND 6 PART 2: the seller call to action, built to the note's end state.
   `.mobile-panel a` above matches EVERY anchor in the panel, the button
   included, so it was rendering as a 22px display-serif slab that ran past the
   panel's own padding. Every inherited property is restored to the site's
   button here, and the rule that separates it from the links is CONTACT's own
   bottom hairline, 20px above: a second border would read as a double line. */
.mobile-panel .btn {
  margin-top: 20px;
  align-self: stretch;
  min-height: 48px;
  /* The label is centred in a full-width box, so this padding is never SEEN: it
     is the minimum inset before a nowrap label would spill. 24px left only 10px
     of slack at 320px, which a failed webfont would eat. */
  padding: 14px 16px;
  border: 1px solid var(--ink);
  background: var(--ink);
  color: var(--cream);
  font-family: var(--body);
  font-size: 13px;
  font-weight: 500;
  letter-spacing: 1.4px;
  text-align: center;
  white-space: nowrap;
}

/* [CHECKLIST 2.2] The burger swaps to a close X while the panel is open, and
   the guard above keeps it dark against the light panel. */
.nav-burger .i-close { display: none; }
body.menu-open .nav-burger .i-open { display: none; }
body.menu-open .nav-burger .i-close { display: block; }

/* ------------------------------------------------------------------- hero */
.hero {
  position: relative;
  display: flex;
  align-items: center;
  /* [CHECKLIST 2.4] 100dvh, NOT 100svh. svh leaves the next section bleeding
     in under the collapsed browser chrome on a phone. */
  min-height: 100dvh;
  padding: calc(var(--nav-h) + 40px) 0 var(--gap-section);
  overflow: hidden;
}

.hero-media {
  position: absolute;
  inset: 0;
}

/* ROUND 6 PART 1: <picture> is an inline wrapper with no box of its own, so it
   is given the media box explicitly. Without this the img sizes itself against
   the inline parent and the hero photograph collapses. */
.hero-media picture {
  display: block;
  width: 100%;
  height: 100%;
}

.hero-media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* [CHECKLIST 2.4] The site is light, so the scrim stays light: roughly .30 to
   .45, only as strong as the text needs. The middle stop sat at .22 in the
   first build and the sub-line disappeared into the sunlit wall of the hero
   photograph, so it is back inside the specified range. */
.hero-media::after {
  content: '';
  position: absolute;
  inset: 0;
  background:
    linear-gradient(to bottom,
      rgba(var(--scrim), .40) 0%,
      rgba(var(--scrim), .34) 45%,
      rgba(var(--scrim), .46) 100%);
}

.hero .wrap {
  position: relative;
  z-index: 1;
  text-align: center;
}

/* ROUND 5 PART 8.2: the timelapse loop. Injected by site.js ONLY on wide
   screens with motion allowed, so phones never download the file; the scrim
   (.hero-media::after) has z-index 0 context above siblings in source order,
   so the video is inserted BEFORE the img and covers the same box. */
.hero-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* ROUND 6 PART 1.3: anchor the portrait's lower frame, so the home sits under
   the text and the sky stays clear above it. On a tall phone the portrait is
   proportionally WIDER than the viewport, so cover crops the sides and shows
   full height: this rule does its work in the 600 to 860px band, where the
   viewport is short enough for the crop to turn vertical. */
@media (max-width: 860px) {
  .hero-media img { object-position: center 62%; }

  /* ROUND 10 PART 4, mobile only. Round 6 pinned this block to the top of the
     frame and it read top heavy, with a dead band of photograph under it. The
     text now floats as ONE group into the upper middle: a column with the
     group centred, biased back up by 12svh of space below it so it sits in the
     sky rather than on the treeline. Eyebrow, name, subline and the button keep
     their own spacing and never split top and bottom.

     The height comes down to 86dvh, the floor the note allows, and that is
     what puts the name at about a third of the screen instead of two fifths.
     It also crops the portrait a little tighter, which lifts the home in
     frame: at a phone ratio the picture is proportionally WIDER than the
     viewport, so cover crops the SIDES and shows full height, and the
     object-position knob above is inert here. Changing the box is the only
     lever that moves the composition on a 390 by 844 screen.

     Desktop is untouched: it keeps 100dvh, the landscape photograph and its
     centred row.

     THE SKY BIAS IS A MARGIN ON THE TEXT, NOT PADDING ON THE SECTION. Written
     as `padding-bottom: 12svh` on `.hero` it composed correctly and broke the
     page rhythm: a section's own bottom padding IS half the gap to the section
     under it, so a 101px pad at 390px made that gap 144px against a 96px budget
     and `scripts/responsive-check` failed the homepage. Carried on the inner
     block the centring is identical, because a centred flex line is shifted up
     by half of whatever space sits below it either way, and the hero keeps the
     one `--gap-section` step every other band uses. The `- var(--gap-section)`
     term is what makes the two forms equal rather than merely similar. */
  .hero {
    flex-direction: column;
    justify-content: center;
    align-items: stretch;
    min-height: 86dvh;
  }

  .hero .wrap { margin-bottom: calc(12svh - var(--gap-section)); }

  /* The scrim's dark zone follows the text down the frame, so contrast holds
     where the words now are and the home below stays open. Neutral, per the
     standing rule: this only moves the stops of the same grey. */
  .hero .hero-media::after {
    background:
      linear-gradient(to bottom,
        rgba(var(--scrim), .28) 0%,
        rgba(var(--scrim), .44) 30%,
        rgba(var(--scrim), .44) 62%,
        rgba(var(--scrim), .30) 82%,
        rgba(var(--scrim), .40) 100%);
  }
}

/* The hero name treatment: the same wordmark at display scale, spanning the
   hero. This is what makes a name work as a brand without a logo. */
.hero-name {
  font-family: var(--display);
  font-weight: 300;
  /* ROUND 5 PART 2.1: bigger again (supersedes Round 4's 42-66 clamp) and
     never wraps, at any width. The 44px floor is Brett's number; verify the
     name holds one line at 390 with the letterspacing below. */
  font-size: clamp(44px, 6vw, 76px);
  white-space: nowrap;
  letter-spacing: .12em;
  line-height: 1.05;
  text-transform: uppercase;
  color: #fff;
  margin: 0;
}

/* ROUND 5 PART 2.2: back to the letterspaced caps subline (supersedes Round
   4's serif italic lede, which lasted one day). Both axes are clamps because
   at a fixed wide letterspacing this 31 character line is the real wrap risk
   at 390px; nowrap makes any overshoot visible instead of folding. */
.hero-sub {
  margin-top: 22px;
  font-size: clamp(11px, 1.3vw, 16px);
  font-weight: 400;
  letter-spacing: clamp(2px, .28vw, 4px);
  text-transform: uppercase;
  white-space: nowrap;
  color: rgba(255, 255, 255, .9);
}

.hero .btn-row { justify-content: center; margin-top: 44px; }

/* [CHECKLIST 2.4] Any scroll cue hides at 860px. */
.scroll-cue {
  position: absolute;
  left: 50%;
  bottom: 34px;
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  z-index: 1;
  font-size: 13px;
  letter-spacing: .26em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, .85);
}

.scroll-cue .line {
  width: 1px;
  height: 46px;
  background: linear-gradient(to bottom, rgba(255, 255, 255, .9), rgba(255, 255, 255, 0));
}

@media (max-width: 860px) {
  .scroll-cue { display: none; }
}

/* ------------------------------------------------------------ page headers */
/* [CHECKLIST 2.4] 44svh with padding-top just clearing the nav and modest
   bottom padding. Never 60vh with flex-end plus 120px top padding: that leaves
   a dead band above the heading on a phone. */
.page-header {
  position: relative;
  display: flex;
  align-items: flex-end;
  min-height: 44svh;
  padding-top: 104px;
  padding-bottom: 52px;
  overflow: hidden;
}

.page-header .hero-media::after {
  background: linear-gradient(to bottom,
    rgba(var(--scrim), .34) 0%,
    rgba(var(--scrim), .26) 50%,
    rgba(var(--scrim), .44) 100%);
}

/* ROUND 3 PART 6.1: ONE shared inner block, so the header text sits at the
   same position on every page. Identical left padding (the .wrap), identical
   bottom padding (the section), and the eyebrow, H1 and lede in the same
   vertical rhythm. No per page overrides. */
.page-header .wrap,
.pagehead-inner {
  position: relative;
  z-index: 1;
}

.page-header .eyebrow {
  color: rgba(255, 255, 255, .82);
  /* ROUND 4 PART 2.4: hero eyebrows 13px. */
  font-size: 13px;
  letter-spacing: 2px;
  margin-bottom: 16px;
}

.page-header .display { color: #fff; }

.page-header .lede {
  color: rgba(255, 255, 255, .92);
  margin-top: 18px;
  /* ROUND 4 PART 2.3: header ledes are 18px flat, capped at 52ch. The body
     .lede clamp (17-20px) stays untouched everywhere else. */
  font-size: 18px;
  max-width: 52ch;
  /* ROUND 5 PART 7: contact's header text sat 30px lower than every other
     page. There was NO per page override; the header bottom-anchors its text,
     so a ONE line lede (contact) hangs the whole block one line lower than a
     two line one (about, buyers). Reserving two lines makes the eyebrow, the
     H1 and the lede land at the same Y on every page. */
  min-height: calc(2 * 1.65em);
}

/* ROUND 3 PART 6.3: the contact header keeps its image with the framing
   raised, so the subject sits higher in the crop. */
.page-header-raised .hero-media img { object-position: center 30%; }

/* ROUND 7 PART 8: the client likes the phone crop and asked for the DESKTOP
   one to come up, framing the door and the plant. Scoped to the wide
   breakpoint precisely so the mobile framing above is left untouched.

   The crop brings the PALE WALL down behind the header text. Measured at 18%,
   the ground under the lede read 146 of 255, which puts white body text at
   about 3.1:1 and fails AA on the very page whose note asks for BETTER
   contrast. So this header, at this breakpoint only, carries a deeper scrim at
   the foot of the gradient. That keeps the framing the client asked for rather
   than nudging the crop away from it. */
@media (min-width: 861px) {
  .page-header-raised .hero-media img { object-position: center 18%; }

  .page-header-raised .hero-media::after {
    background: linear-gradient(to bottom,
      rgba(var(--scrim), .30) 0%,
      rgba(var(--scrim), .34) 42%,
      rgba(var(--scrim), .60) 100%);
  }
}

/* ROUND 4 PART 3.1: the sellers header (the white oak kitchen) frames a touch
   above centre so the counters and the window sit well in the crop. */
.page-header-mid .hero-media img { object-position: center 45%; }

@media (max-width: 560px) {
  .page-header { padding-top: 96px; padding-bottom: 40px; }
}

/* ---------------------------------------------------------------- sections */
.section {
  padding: var(--gap-section) 0;
}

.section-tight { padding: var(--gap-tight) 0; }

.section-cream { background: var(--cream); }
.section-paper { background: var(--paper); }
.section-sand { background: var(--sand); }

/* ROUND 3 PART 1: the deep slate band is GONE. The county band now sits on
   .section-sand with ink text, same layout, warm instead of blue. The site has
   no dark section at all any more, and no .section-dark class either, so a new
   one cannot be reached for by habit. */

/* Two sand moments can now sit adjacent, the county band and the closing CTA.
   A hairline keeps them reading as two sections rather than one long band. */
/* ROUND 5 PART 4: the homepage county band went white, so no section-sand
   sits against the sand cta-band any more and the old hairline rule is gone. */


.section-head {
  max-width: 720px;
  margin-bottom: 48px;
}

/* ROUND 3 PART 4.1: centred section heads breathe at 880px. */
.section-head.is-centred {
  max-width: 880px;
  margin-left: auto;
  margin-right: auto;
  text-align: center;
}

.section-head .lede { margin-top: 18px; }

.section-head.is-centred .lede { margin-left: auto; margin-right: auto; }

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

/* ROUND 3 PART 4.2: the standalone intro strip is REMOVED. Its sentence folded
   into the About preview lede on the homepage. */

/* ----------------------------------------------------------- numbers band */
.stats {
  display: grid;
  gap: 40px;
  grid-template-columns: repeat(4, 1fr);
  padding: var(--gap-section) 0;
}

.stats-3 { grid-template-columns: repeat(3, 1fr); }

.stat {
  position: relative;
  text-align: center;
  /* [CHECKLIST 2.5] A grid cell must be allowed to shrink. */
  min-width: 0;
}

/* Thin vertical hairlines between stats on desktop, hidden on mobile. */
.stat + .stat::before {
  content: '';
  position: absolute;
  left: -20px;
  top: 8%;
  bottom: 8%;
  width: 1px;
  background: var(--line);
}

.stat-value {
  font-family: var(--display);
  font-weight: 300;
  font-size: clamp(40px, 5.5vw, 76px);
  line-height: 1;
  color: var(--ink);
}

.stat-label {
  margin-top: 14px;
  font-size: 13px;
  font-weight: 500;
  letter-spacing: 1.6px;
  text-transform: uppercase;
  color: var(--ink-soft);
}

@media (max-width: 860px) {
  .stats,
  .stats-3 {
    grid-template-columns: repeat(2, 1fr);
    gap: 44px 24px;
    padding: var(--gap-section) 0;
  }
  .stat + .stat::before { display: none; }
}

@media (max-width: 460px) {
  .stats,
  .stats-3 { grid-template-columns: 1fr; }
}

/* ----------------------------------------------------- split image + text */
.split {
  display: grid;
  grid-template-columns: 1fr 1fr;
  align-items: center;
  gap: 72px;
}

.split-body { min-width: 0; }

.split-media { min-width: 0; }

.split-media img {
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
}

/* ROUND 4 PART 3.2: the home value form image (the curb appeal home) sits at
   plain centre. */
.value-split-media img { object-position: center; }

/* [CHECKLIST 2.6] No z-index:-1 decorative frames anywhere. They break inside
   any ancestor with a transform, and the reveal animation creates one. */

@media (max-width: 1024px) {
  .split { gap: 44px; }
}

/* [CHECKLIST 2.6] On mobile, text ALWAYS stacks above its photo. Set order
   explicitly on both children, including in reversed splits: DOM order puts
   the photo first there, and relying on the default would leave two photos
   touching where two splits meet. */
@media (max-width: 860px) {
  .split {
    grid-template-columns: 1fr;
    gap: 32px;
  }
  .split .split-body { order: 1; }
  .split .split-media { order: 2; }
}

/* ------------------------------------------------------------ listing grid */
.listing-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 32px;
}

@media (max-width: 1024px) {
  .listing-grid { grid-template-columns: repeat(2, 1fr); }
}

@media (max-width: 640px) {
  .listing-grid { grid-template-columns: 1fr; }
}

.listing-card {
  display: block;
  min-width: 0;
  background: var(--paper);
  border: 1px solid var(--line);
  text-decoration: none;
  color: var(--ink);
  transition: border-color .35s var(--ease);
}

.listing-card:hover { border-color: var(--ink); }

.listing-media {
  position: relative;
  aspect-ratio: 4 / 3;
  background: var(--sand);
  overflow: hidden;
}

.listing-media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform .6s var(--ease);
}

.listing-card:hover .listing-media img { transform: scale(1.04); }

/* ROUND 7 PART 4: the branded stand-in for a listing with no usable photograph.
   It has to read as a designed part of the site rather than as a failure, so it
   uses the section palette and the display face, not a placeholder graphic. The
   address is capped and clamped because a long one must never push the note out
   of a fixed 4:3 box, and it is inset on the left so it clears the status badge
   sitting in that corner. */
.listing-nophoto {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 14px;
  padding: 46px 24px 24px;
  background: var(--sand);
  text-align: center;
}

.listing-nophoto-addr {
  max-width: 22ch;
  font-family: var(--display);
  font-size: clamp(15px, 1.5vw, 18px);
  line-height: 1.35;
  color: var(--ink);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.listing-nophoto-rule {
  width: 34px;
  height: 1px;
  background: rgba(0, 0, 0, .28);
}

.listing-nophoto-note {
  font-family: var(--body);
  font-size: 13px;
  font-weight: 500;
  letter-spacing: 1.4px;
  text-transform: uppercase;
  color: var(--ink-soft);
}

.listing-status {
  position: absolute;
  left: 0;
  top: 0;
  padding: 8px 14px;
  background: rgba(var(--scrim), .72);
  color: #fff;
  font-size: 13px;
  font-weight: 500;
  letter-spacing: 1.4px;
  text-transform: uppercase;
}

.listing-body { padding: 22px 24px 26px; }

.listing-price {
  font-family: var(--display);
  font-weight: 300;
  font-size: 24px;
  letter-spacing: .02em;
}

.listing-addr {
  margin-top: 8px;
  font-size: 15px;
  line-height: 1.45;
}

.listing-sub {
  margin-top: 4px;
  font-size: 13px;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--ink-soft);
}

.listing-specs {
  display: flex;
  flex-wrap: wrap;
  gap: 18px;
  margin-top: 18px;
  padding-top: 16px;
  border-top: 1px solid var(--line);
  font-size: 13px;
  letter-spacing: 1.1px;
  text-transform: uppercase;
  color: var(--ink-soft);
}

.listing-specs b {
  font-weight: 500;
  color: var(--ink);
}

.listing-empty,
.listing-loading {
  padding: 56px 0;
  text-align: center;
  color: var(--ink-soft);
}

/* ROUND 9 PART 1: the search page answers with a number. */
.listing-count {
  margin: 0 0 22px;
  font-size: 13px;
  font-weight: 500;
  letter-spacing: 1.4px;
  text-transform: uppercase;
  color: var(--ink-soft);
}

.pager {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 18px;
  margin-top: 48px;
}

.pager-info {
  font-size: 13px;
  letter-spacing: 1.4px;
  text-transform: uppercase;
  color: var(--ink-soft);
}

/* The IDX line the shared MLS pool requires. Small, but never hidden. */
.idx-note {
  margin-top: 36px;
  font-size: 13px;
  line-height: 1.7;
  color: var(--ink-faint);
  max-width: 90ch;
}

/* ------------------------------------------------------------ neighborhoods */
.hood-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 28px;
}

@media (max-width: 1024px) {
  .hood-grid { grid-template-columns: repeat(2, 1fr); }
}

@media (max-width: 560px) {
  .hood-grid { grid-template-columns: 1fr; }
}

.hood {
  min-width: 0;
}

.hood-media {
  aspect-ratio: 4 / 3;
  overflow: hidden;
}

.hood-media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Hover scales the photo. No grayscale, ever. */
  transition: transform .6s var(--ease);
}

.hood:hover .hood-media img { transform: scale(1.04); }

/* ROUND 3 PART 1.1: the band is warm now, so the cards read in ink. */
.hood-label {
  margin-top: 18px;
  font-family: var(--display);
  font-weight: 300;
  font-size: 18px;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--ink);
}

.hood-line {
  margin-top: 8px;
  font-size: 14px;
  line-height: 1.6;
  color: var(--ink-soft);
}

/* ROUND 2 PART 6.6: the band carries THREE county cards, not four neighborhood
   cards. Declared after the base rules above so the cascade resolves to three,
   then to two and one at the same breakpoints the four column grid uses. */
.hood-grid-3 { grid-template-columns: repeat(3, 1fr); }

@media (max-width: 1024px) {
  .hood-grid-3 { grid-template-columns: repeat(2, 1fr); }
}

@media (max-width: 560px) {
  .hood-grid-3 { grid-template-columns: 1fr; }
}

/* ROUND 2 PART 5. An in page anchor has to clear the fixed nav, or the heading
   it points at lands underneath the bar. This matters now that the persistent
   "What is my home worth" button in the nav is itself an anchor link. */
[id] { scroll-margin-top: calc(var(--nav-h) + 16px); }

/* The alias that keeps the old /sellers/#valuation link working after the
   section was renamed. It draws nothing and is only a scroll target. */
.anchor-alias {
  display: block;
  height: 0;
}

/* One quiet line under a form, PART 5.4. */
.form-note {
  margin-top: 22px;
  font-size: 15px;
  line-height: 1.65;
  color: var(--ink-soft);
}

/* ----------------------------------------------------------- who she helps */
/* ROUND 2 PART 7. NO BOXES, by instruction: three columns of type on cream.
   Each column is an Inter label over a hairline, then serif items with one
   quiet line under each. The columns collapse to a single stack in the same
   source order on a phone, so the reading order never changes. */
.helps-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 44px;
}

@media (max-width: 900px) {
  .helps-grid {
    grid-template-columns: 1fr;
    gap: 40px;
  }
}

.helps-col { min-width: 0; }

/* ROUND 3 PART 4.3: the full elevation treatment. Column labels carry a 40px
   accent rule above them, each item is a row over a hairline, and the hover
   animates the row's rule to full black while the name eases 6px right. The
   rows themselves carry data-reveal, so they stagger in on scroll. */
.helps-head {
  font-family: var(--body);
  font-size: 13.5px;
  font-weight: 500;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--ink-soft);
  padding-bottom: 14px;
}

.helps-head::before {
  content: '';
  display: block;
  width: 40px;
  height: 2px;
  background: var(--ink);
  margin-bottom: 16px;
}

.helps-list { list-style: none; }

.helps-list li {
  position: relative;
  padding: 20px 0 22px;
  border-bottom: 1px solid var(--line);
}

/* ROUND 7 PART 2.4: "make the three columns read even despite 3, 3, 4". The
   three columns are separate lists, so nothing lines their hairlines up: any
   row that grows in one column pushes every rule below it out of step with its
   neighbours. Measured before the fix, the rules drifted 31px apart at every
   width from 901 to 1030, where "Investment property sales" takes two lines
   while its neighbours take one.

   Reserving two lines on the note is the floor, and it alone is NOT enough,
   because it cannot know about a wrapping item title. Subgrid is what actually
   solves it: the rows are declared once on the grid and both the column and its
   list borrow them, so every row is as tall as its tallest cell across all
   three columns whatever the content does. The short columns then simply stop
   after row 3, which reads as intent rather than as a ragged grid.

   Scoped to the 3-column layout: below 901px the columns stack and each one
   needs its own rhythm, plus the 40px stacking gap the row-gap reset removes. */
.helps-note { min-height: calc(2 * 1.55em); }

@media (min-width: 901px) {
  @supports (grid-template-rows: subgrid) {
    .helps-grid {
      grid-template-rows: auto repeat(4, auto);
      row-gap: 0;
    }

    .helps-col {
      display: grid;
      grid-template-rows: subgrid;
      grid-row: 1 / -1;
    }

    .helps-list {
      display: grid;
      grid-template-rows: subgrid;
      grid-row: 2 / -1;
    }
  }
}

/* The animated rule sits on the hairline and grows to full width on hover. */
.helps-list li::after {
  content: '';
  position: absolute;
  left: 0;
  bottom: -1px;
  width: 0;
  height: 1px;
  background: var(--ink);
  transition: width .3s var(--ease);
}

.helps-list li:hover::after { width: 100%; }

.helps-item {
  display: block;
  font-family: var(--display);
  font-weight: 300;
  font-size: 24px;
  line-height: 1.3;
  color: var(--ink);
  transition: transform .3s var(--ease);
}

.helps-list li:hover .helps-item { transform: translateX(6px); }

.helps-note {
  display: block;
  margin-top: 7px;
  font-family: var(--display);
  font-style: italic;
  font-weight: 400;
  font-size: 17px;
  line-height: 1.55;
  color: var(--ink-serif);
}

/* ------------------------------------------------------------ testimonials */
/* ROUND 10 PART 2: the reviews header is a ROW, not a centred block. The
   heading holds the left of the row and the share button the right, so the
   button reads as an action on this section rather than as a stray link under
   the cards. `.section-head` caps itself at 720px for reading length, which is
   right for the text column and wrong for a row that has to reach both edges,
   so the cap moves inward onto the text half. */
.reviews-head {
  max-width: none;
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 24px;
  text-align: left;
}

.reviews-head-text { max-width: 720px; }

.reviews-share {
  display: flex;
  align-items: center;
  flex-shrink: 0;
}

/* [CHECKLIST 2.1] 44px minimum tap target. The site button already measures
   54px, so this is a floor that records the requirement rather than a change. */
.reviews-share .btn { min-height: 44px; }

/* The confirmation is a live region that is always in the DOM, so a screen
   reader announces it when it fills. `:empty` drops its margin, so an unused
   confirmation takes no width at all next to the button. It sits BEFORE the
   button in the markup, so the group grows to the LEFT when it fills and the
   button does not jump sideways under the pointer. */
.share-confirm {
  margin-right: 14px;
  font-size: 12px;
  letter-spacing: 1.2px;
  text-transform: uppercase;
  color: var(--ink-faint);
}

.share-confirm:empty { margin-right: 0; }

/* Stack on a phone: a 44px button and a heading cannot share a 320px row. */
@media (max-width: 700px) {
  .reviews-head {
    flex-direction: column;
    align-items: flex-start;
    gap: 22px;
  }
}

/* ROUND 10 PART 1: these cards are the FALLBACK, not the reviews section. The
   widget above is. Hidden only while JS is running, so a visitor with JS off
   still reads reviews instead of an empty band; site.js reveals it when the
   widget has not painted within four seconds and hides it again if it does. */
.js .review-fallback { display: none; }
.js .review-fallback.is-shown { display: grid; }

/* The 2026 review runs four times the length of the 2015 pair, so it holds the
   left column at full height while the two short ones stack beside it; three
   equal columns left a ragged hole under the short cards. */
.review-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 32px;
  /* start, not stretch: the columns rarely balance to the pixel, and the
     slack has to land OUTSIDE the cards on the cream band, where it reads as
     air — not inside a white border, where it reads as a missing paragraph. */
  align-items: start;
}

.review-card.is-feature { grid-row: span 2; }

@media (max-width: 860px) {
  .review-grid { grid-template-columns: 1fr; }
  .review-card.is-feature { grid-row: auto; }
}

.review-card {
  display: flex;
  flex-direction: column;
  min-width: 0;
  margin: 0;
  padding: 40px 36px;
  background: var(--paper);
  border: 1px solid var(--line);
}

@media (max-width: 640px) {
  .review-card { padding: 28px 22px; }
}

/* Stars are drawn, never typed: an inline star glyph renders as a hollow
   fallback square on systems whose fonts lack it, and an emoji star arrives
   pre-coloured gold, which is a brand colour this site does not have. */
.review-stars {
  display: flex;
  gap: 5px;
  margin-bottom: 20px;
}

.review-stars svg {
  width: 15px;
  height: 15px;
  fill: var(--ink);
}

.review-card blockquote {
  margin: 0;
  font-size: 17px;
  line-height: 1.7;
  color: var(--ink);
}

.review-card figcaption {
  padding-top: 24px;
}

.review-deal {
  display: block;
  font-size: 13px;
  font-weight: 500;
  letter-spacing: 1.4px;
  text-transform: uppercase;
  color: var(--ink-soft);
}

.review-date {
  display: block;
  margin-top: 6px;
  font-size: 13px;
  letter-spacing: 1.4px;
  text-transform: uppercase;
  color: var(--ink-faint);
}

/* ROUND 10 PART 3: `.review-cta` is gone with the "Review Erica on Zillow"
   button it centred. A review now starts inside the widget's own flow. */

.review-source {
  margin-top: 14px;
  font-size: 13px;
  letter-spacing: 1.4px;
  text-transform: uppercase;
  color: var(--ink-faint);
}

/* ------------------------------------------------------------- blog cards */
.post-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 32px;
}

@media (max-width: 1024px) {
  .post-grid { grid-template-columns: repeat(2, 1fr); }
}

@media (max-width: 640px) {
  .post-grid { grid-template-columns: 1fr; }
}

.post-card {
  display: block;
  min-width: 0;
  text-decoration: none;
  color: var(--ink);
}

.post-media {
  aspect-ratio: 3 / 2;
  background: var(--sand);
  overflow: hidden;
}

.post-media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform .6s var(--ease);
}

.post-card:hover .post-media img { transform: scale(1.04); }

.post-date {
  margin-top: 20px;
  font-size: 13px;
  letter-spacing: 1.4px;
  text-transform: uppercase;
  color: var(--ink-faint);
}

.post-title {
  margin-top: 10px;
  font-family: var(--display);
  font-weight: 300;
  font-size: 20px;
  letter-spacing: .04em;
  text-transform: uppercase;
  line-height: 1.3;
}

.post-excerpt {
  margin-top: 12px;
  font-size: 16px;
  line-height: 1.6;
  color: var(--ink-soft);
}

/* ------------------------------------------------ process, sticky counter */
/* ROUND 8 PART 1. The previous treatment (three-across cards with ghost
   numerals and a connecting rail) was rejected outright; this is the design the
   client chose from rendered options, ported from Brett's
   `process-sticky-counter.html` with its tokens mapped onto the site's own.

   Two tokens have no site equivalent and are declared here rather than promoted
   to :root, because they belong to this component alone: the warm accent and
   the muted step tone are both specified in the notes by hex. Everything else
   resolves to the site scale. */
.process {
  --process-accent: #8A6A3F;
  --process-muted: #B4B2A9;
}

.process-grid {
  display: grid;
  grid-template-columns: 1fr 1.4fr;
  gap: 64px;
  align-items: start;
}

/* LEFT, the column that pins */
.process-left {
  position: sticky;
  top: 120px;
}

.process-eyebrow {
  font-size: 11px;
  letter-spacing: 2.5px;
  text-transform: uppercase;
  color: var(--process-accent);
  margin-bottom: 14px;
  font-weight: 500;
}

.process-counter {
  font-family: var(--display);
  font-size: clamp(48px, 4.5vw, 64px);
  line-height: 1;
  color: var(--ink);
  display: flex;
  align-items: baseline;
  gap: 6px;
}

/* The number swaps inside a fixed-height window, so the digits slide rather
   than reflowing the line. */
.counter-track {
  position: relative;
  display: inline-block;
  height: 1em;
  overflow: hidden;
  min-width: 1.35em;
}

.counter-num {
  display: inline-block;
  transition: transform .2s ease, opacity .2s ease;
}

.counter-num.exit-up { transform: translateY(-110%); opacity: 0; position: absolute; left: 0; top: 0; }
.counter-num.exit-down { transform: translateY(110%); opacity: 0; position: absolute; left: 0; top: 0; }
.counter-num.enter-from-below { transform: translateY(110%); opacity: 0; }
.counter-num.enter-from-above { transform: translateY(-110%); opacity: 0; }
.counter-num.settled { transform: translateY(0); opacity: 1; }

.counter-total {
  font-size: .42em;
  color: var(--process-muted);
}

.process-lede {
  font-family: var(--display);
  font-style: italic;
  font-weight: 400;
  font-size: 18px;
  color: var(--ink-serif);
  margin-top: 16px;
  max-width: 24ch;
}

/* RIGHT, the steps that scroll past it */
.process-steps {
  display: flex;
  flex-direction: column;
  gap: 56px;
}

.step { transition: color .25s var(--ease); }

.step-num { display: none; }

.step-title {
  font-family: var(--display);
  font-weight: 400;
  font-size: 20px;
  margin-bottom: 8px;
  color: var(--process-muted);
  transition: color .25s var(--ease);
}

.step-body {
  font-size: 18px;
  line-height: 1.7;
  color: var(--process-muted);
  max-width: 52ch;
  transition: color .25s var(--ease);
}

.step.is-active .step-title { color: var(--ink); }
.step.is-active .step-body { color: var(--ink-serif); }

/* MOBILE, below 1024: stacked, no sticky, no muted state, no counter. The
   number moves above each title so the sequence still reads. */
@media (max-width: 1023px) {
  .process-grid { grid-template-columns: 1fr; gap: 36px; }
  .process-left { position: static; }
  .process-counter,
  .counter-total { display: none; }
  .process-steps { gap: 40px; }

  .step-num {
    display: block;
    font-size: 12px;
    letter-spacing: 2px;
    color: var(--process-accent);
    margin-bottom: 6px;
    font-weight: 500;
  }

  .step-title { color: var(--ink); }
  .step-body { color: var(--ink-serif); }
}

/* REDUCED MOTION: nothing animates and every step reads at full contrast, so
   the muted state can never strand a step unreadable. */
@media (prefers-reduced-motion: reduce) {
  .counter-num,
  .step,
  .step-title,
  .step-body { transition: none !important; }

  .step-title { color: var(--ink); }
  .step-body { color: var(--ink-serif); }
}

/* ---------------------------------------------------------------- vendors */
.vendor-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 40px 32px;
}

@media (max-width: 1024px) {
  .vendor-grid { grid-template-columns: repeat(2, 1fr); }
}

@media (max-width: 640px) {
  .vendor-grid { grid-template-columns: 1fr; }
}

.vendor-trade { min-width: 0; }

.vendor-trade h3 {
  padding-bottom: 12px;
  border-bottom: 1px solid var(--line);
  font-family: var(--display);
  font-weight: 300;
  font-size: 15px;
  letter-spacing: .1em;
  text-transform: uppercase;
}

.vendor-list {
  list-style: none;
  margin-top: 16px;
}

.vendor-list li + li { margin-top: 14px; }

.vendor-name {
  font-size: 15px;
  font-weight: 500;
}

.vendor-meta {
  margin-top: 3px;
  font-size: 13px;
  color: var(--ink-soft);
}

.vendor-meta a { text-decoration: none; border-bottom: 1px solid var(--line); }

.vendor-note {
  margin-top: 44px;
  font-size: 13px;
  line-height: 1.7;
  color: var(--ink-faint);
  max-width: 80ch;
}

/* ------------------------------------------------------------------ forms */
/* [CHECKLIST 2.5] Every grid field gets min-width:0 and every control gets
   width:100%. Without both, the columns refuse to shrink and the form
   overflows the phone sideways. */
.form {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 22px;
  max-width: 900px;
}

.form-field {
  display: flex;
  flex-direction: column;
  gap: 8px;
  min-width: 0;
}

.form-field.is-wide { grid-column: 1 / -1; }

/* A form that sits inside a half-width split has about 580px to work with, and
   three columns of that leaves an email field 170px wide. Two columns is the
   right density there. */
.form.is-narrow { grid-template-columns: repeat(2, 1fr); }

.form-field label {
  font-size: 13px;
  font-weight: 500;
  letter-spacing: 1.4px;
  text-transform: uppercase;
  color: var(--ink-soft);
}

.form input,
.form select,
.form textarea {
  width: 100%;
  min-width: 0;
  padding: 14px 16px;
  border: 1px solid var(--line);
  border-radius: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--body);
  font-size: 15px;
  transition: border-color .3s var(--ease);
}

.form input:focus,
.form select:focus,
.form textarea:focus {
  border-color: var(--ink);
  outline: none;
}

.form textarea {
  min-height: 132px;
  resize: vertical;
}

/* [CHECKLIST 2.5] The submit gets its own auto-sized cell, never crammed into
   a field column. */
.form-actions {
  grid-column: 1 / -1;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 18px;
}

.form-consent {
  grid-column: 1 / -1;
  display: flex;
  gap: 12px;
  align-items: flex-start;
  font-size: 13px;
  line-height: 1.6;
  color: var(--ink-soft);
}

.form-consent input {
  width: 18px;
  height: 18px;
  flex: 0 0 auto;
  margin-top: 3px;
  padding: 0;
}

.form-check {
  grid-column: 1 / -1;
  display: flex;
  gap: 12px;
  align-items: center;
  font-size: 14px;
  color: var(--ink-soft);
}

.form-check input {
  width: 18px;
  height: 18px;
  flex: 0 0 auto;
  padding: 0;
}

/* The honeypot. Off screen rather than display:none, which some bots skip. */
.hp-field {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

.form-status {
  grid-column: 1 / -1;
  display: none;
  padding: 14px 16px;
  border: 1px solid var(--line);
  background: var(--paper);
  font-size: 14px;
}

.form-status.is-shown { display: block; }
.form-status.ok { border-color: var(--ink); }
.form-status.err { border-color: #B4402F; color: #B4402F; }

/* [CHECKLIST 2.5] 2 columns at 860px, 1 column at 560px. The .is-narrow
   variant is included EXPLICITLY at 560: its two-class selector outranks the
   plain .form rule, so without this line a narrow form stays two-column on a
   320px phone and overflows. */
@media (max-width: 860px) {
  .form { grid-template-columns: repeat(2, 1fr); }
}

@media (max-width: 560px) {
  .form,
  .form.is-narrow { grid-template-columns: 1fr; }
}

/* -------------------------------------------------------- search filters */
.filters {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 16px;
  align-items: end;
  margin-bottom: 44px;
}

.filters .form-field { min-width: 0; }

.filters input,
.filters select {
  width: 100%;
  min-width: 0;
  padding: 13px 14px;
  border: 1px solid var(--line);
  border-radius: 0;
  background: var(--paper);
  font-family: var(--body);
  font-size: 14px;
}

/* [CHECKLIST 2.5] The buttons get their own auto-sized row, never crammed into
   a field column where they would squeeze the selects beside them. */
.filters-actions {
  grid-column: 1 / -1;
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  min-width: 0;
}

@media (max-width: 1024px) {
  .filters { grid-template-columns: repeat(2, 1fr); }
}

@media (max-width: 560px) {
  .filters { grid-template-columns: 1fr; }
}

/* ------------------------------------------------------- mortgage calculator */
.calc {
  display: grid;
  grid-template-columns: 1.15fr 1fr;
  gap: 56px;
  align-items: start;
}

@media (max-width: 1024px) {
  .calc { grid-template-columns: 1fr; gap: 40px; }
}

.calc-inputs {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 20px;
}

@media (max-width: 560px) {
  .calc-inputs { grid-template-columns: 1fr; }
}

.calc-result {
  padding: 38px 34px;
  background: var(--paper);
  border: 1px solid var(--line);
}

/* ROUND 7 PART 5.2: the headline number carries the panel. Full ink, never the
   soft grey, because this is the answer the whole component exists to give. */
.calc-total {
  font-family: var(--display);
  font-weight: 300;
  font-size: clamp(36px, 3.6vw, 54px);
  line-height: 1;
  color: var(--ink);
  /* The value is replaced on every keystroke while a tween runs, so the box
     must not resize under the reader as digits change width. */
  font-variant-numeric: tabular-nums;
}

.calc-total-label {
  margin-top: 12px;
  font-size: 13px;
  font-weight: 500;
  letter-spacing: 1.6px;
  text-transform: uppercase;
  color: var(--ink-soft);
}

/* ROUND 7 PART 5.2: the ring sits beside the ledger on desktop and above it on
   a phone, which is the one place the source order is not the reading order, so
   the ring is FIRST in the markup and moved to the right on wide screens. */
.calc-breakdown {
  display: grid;
  grid-template-columns: 120px 1fr;
  align-items: start;
  gap: 26px;
  margin-top: 30px;
  padding-top: 22px;
  border-top: 1px solid var(--line);
}

@media (max-width: 520px) {
  .calc-breakdown {
    grid-template-columns: 1fr;
    justify-items: center;
    gap: 22px;
  }
}

/* The donut itself. The conic-gradient is written by mortgage.js; the hole is
   a paper coloured disc on top, so there is no chart library and no SVG. */
.calc-ring {
  position: relative;
  width: 120px;
  height: 120px;
  border-radius: 50%;
  background: var(--sand);
}

/* The hole has to hold the largest total this ever shows, so the label that
   sat here in the first cut is gone: it overflowed the disc and printed across
   the segments, and "Estimated monthly payment" is already written directly
   above the ring. */
.calc-ring-inner {
  position: absolute;
  inset: 23px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  border-radius: 50%;
  background: var(--paper);
  text-align: center;
}

.calc-ring-total {
  font-family: var(--display);
  font-size: 15px;
  line-height: 1.1;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

.calc-rows { min-width: 0; }

/* Mini ledger rows: a hairline under each one, the amount hard right. */
.calc-row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 16px;
  padding: 9px 0;
  border-bottom: 1px solid var(--line);
  font-size: 14px;
  color: var(--ink-soft);
}

.calc-row:last-child { border-bottom: 0; }

.calc-row b {
  font-weight: 500;
  color: var(--ink);
  text-align: right;
  font-variant-numeric: tabular-nums;
}

/* Each ledger row carries the swatch of its ring segment, so the two read as
   one object rather than as a chart and a list that happen to sit together. */
.calc-row[data-seg] span::before {
  content: '';
  display: inline-block;
  width: 9px;
  height: 9px;
  margin-right: 9px;
  border-radius: 50%;
  vertical-align: baseline;
}

.calc-row[data-seg="1"] span::before { background: #000000; }
.calc-row[data-seg="2"] span::before { background: rgba(0, 0, 0, .58); }
.calc-row[data-seg="3"] span::before { background: rgba(0, 0, 0, .32); }
.calc-row[data-seg="4"] span::before { background: var(--sand); border: 1px solid var(--line); }

/* Amount financed is not part of the monthly payment, so it is set apart from
   the four rows the ring describes rather than reading as a fifth segment. */
.calc-row-aside {
  margin-top: 10px;
  padding-top: 14px;
  border-top: 1px solid var(--line);
  color: var(--ink-faint);
}

.calc-note {
  margin-top: 28px;
  font-size: 13px;
  line-height: 1.7;
  color: var(--ink-faint);
  max-width: 80ch;
}

/* ---------------------------------------------------------------- homebot */
/* The embed is a third-party iframe. It is bounded here so it can never blow
   out the page width on a phone. */
.embed-frame {
  width: 100%;
  max-width: 100%;
  min-width: 0;
  overflow: hidden;
  background: var(--paper);
  border: 1px solid var(--line);
  padding: 28px;
}

.embed-frame iframe {
  width: 100%;
  max-width: 100%;
  border: 0;
  display: block;
}

.embed-placeholder {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 14px;
  padding: 52px 34px;
  text-align: left;
}

/* -------------------------------------------------------------- CTA band */
/* PART 6: the closing band carries NO photograph. One image per slot and no
   reuse, so on a light editorial site a flat beige band reads as more
   confident than a repeated photo. */
.cta-band {
  background: var(--sand);
  padding: var(--gap-section) 0;
  text-align: center;
}

.cta-band .btn-row { justify-content: center; }

/* ----------------------------------------------------------------- footer */
/* ROUND 3 PART 1.2: the footer is warm. Sand background, ink text, hairline
   rules, and the wordmark stays black. */
.footer {
  background: var(--sand);
  color: var(--ink-soft);
  padding: 76px 0 30px;
}

.footer-top {
  display: grid;
  grid-template-columns: 1.4fr 1fr 1fr;
  gap: 48px;
  padding-bottom: 44px;
  border-bottom: 1px solid var(--line);
}

@media (max-width: 860px) {
  .footer-top { grid-template-columns: 1fr; gap: 36px; }
}

.footer-col { min-width: 0; }

.footer .wordmark-name { color: var(--ink); font-size: 22px; }
.footer .wordmark-sub { color: var(--ink-soft); }

.footer-tag {
  margin-top: 16px;
  font-size: 14px;
  line-height: 1.7;
  color: var(--ink-soft);
  max-width: 42ch;
}

.footer-head {
  font-size: 13px;
  font-weight: 500;
  letter-spacing: 1.6px;
  text-transform: uppercase;
  color: var(--ink);
  margin-bottom: 18px;
}

.footer-nav {
  display: flex;
  flex-direction: column;
  gap: 11px;
}

.footer-nav a {
  font-size: 14px;
  text-decoration: none;
  color: var(--ink-soft);
  transition: color .25s linear;
}

.footer-nav a:hover { color: var(--ink); }

.footer-contact {
  font-style: normal;
  font-size: 14px;
  line-height: 1.9;
  color: var(--ink-soft);
}

.footer-contact a {
  text-decoration: none;
  border-bottom: 1px solid rgba(0, 0, 0, .3);
}

.socials {
  display: flex;
  gap: 14px;
  margin-top: 20px;
}

.socials a {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 38px;
  height: 38px;
  border: 1px solid rgba(0, 0, 0, .28);
  color: var(--ink);
  transition: background-color .3s var(--ease), color .3s var(--ease);
}

.socials a:hover { background: var(--ink); color: var(--paper); }

.footer-legal {
  padding: 26px 0;
  border-bottom: 1px solid var(--line);
  font-size: 13px;
  line-height: 1.8;
  color: var(--ink-soft);
}

.footer-bottom {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px 22px;
  padding-top: 26px;
  font-size: 13px;
  color: var(--ink-soft);
}

.footer-bottom a {
  text-decoration: none;
  color: var(--ink-soft);
}

.footer-bottom a:hover { color: var(--ink); }

.footer-bottom .spacer { flex: 1 1 auto; }

.eho {
  display: inline-flex;
  align-items: center;
  gap: 7px;
}

/* ------------------------------------------------------- confirm markers */
/* Nothing the client has not supplied is invented. Every outstanding value
   from PART 8 renders as one of these instead of a plausible-looking number. */
.confirm {
  display: inline-block;
  padding: 2px 8px;
  border: 1px dashed currentColor;
  font-family: var(--body);
  font-size: 13px;
  font-weight: 500;
  letter-spacing: 1px;
  text-transform: uppercase;
  color: #8A6D1F;
  /* Deliberately NOT nowrap. That suits a two-word chip and is catastrophic
     for a sentence: one long inline chip pushed a previous build's document to
     691px on a 390px phone. Long text goes in .confirm-block. */
  white-space: normal;
}

.confirm-block {
  display: block;
  padding: 14px 16px;
  border: 1px dashed rgba(138, 109, 31, .55);
  background: rgba(138, 109, 31, .05);
  font-size: 13px;
  line-height: 1.65;
  color: #8A6D1F;
}

/* ----------------------------------------------- calculator tabs, ROUND 3 */
/* PART 7.2: one component pattern across the three calculators. The tabs are
   real buttons with aria-selected, keyboard reachable, and the active tab
   carries a 2px black underline. Zero third party chrome. */
.calc-tabs {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 28px;
  margin-bottom: 40px;
  border-bottom: 1px solid var(--line);
}

.calc-tab {
  position: relative;
  padding: 12px 2px 14px;
  border: 0;
  background: none;
  cursor: pointer;
  font-size: 13.5px;
  font-weight: 500;
  letter-spacing: 1.4px;
  text-transform: uppercase;
  color: var(--ink-soft);
  transition: color .25s var(--ease);
}

.calc-tab::after {
  content: '';
  position: absolute;
  left: 0;
  bottom: -1px;
  width: 100%;
  height: 2px;
  background: transparent;
  transition: background-color .25s var(--ease);
}

.calc-tab[aria-selected="true"] {
  color: var(--ink);
}

.calc-tab[aria-selected="true"]::after { background: var(--ink); }

.calc-panel[hidden] { display: none; }

/* A helper note under an input, the "what counts as monthly debts" line. */
.field-help {
  font-size: 13px;
  line-height: 1.55;
  color: var(--ink-faint);
}

/* The one sentence answer a calculator leads with. */
.calc-sentence {
  margin-top: 26px;
  font-family: var(--display);
  font-style: italic;
  font-weight: 400;
  font-size: 17px;
  line-height: 1.6;
  color: var(--ink-serif);
}

/* A price RANGE is longer than one figure, so it steps down a size. */
.calc-total-sm { font-size: clamp(24px, 2.8vw, 40px); }

/* Rent vs buy leads with the two five year figures side by side. */
.rb-pair {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 24px;
}

.rb-pair > div { min-width: 0; }

.rb-total { font-size: clamp(24px, 2.6vw, 36px); }

@media (max-width: 460px) {
  .rb-pair { grid-template-columns: 1fr; }
}

/* -------------------------------------- sellers home value split, ROUND 3 */
/* PART 8.2: the kitchen the client loves on the left, the form on cream on
   the right. On mobile the IMAGE stacks above the form, by instruction, which
   is the one split on the site where the media leads. */
.value-split {
  display: grid;
  grid-template-columns: 1fr 1.1fr;
  gap: 64px;
  align-items: stretch;
}

.value-split-media { min-width: 0; }

.value-split-media img {
  width: 100%;
  height: 100%;
  min-height: 420px;
  object-fit: cover;
}

.value-split-body {
  min-width: 0;
  background: var(--cream);
  padding: 48px 44px;
}

@media (max-width: 1024px) {
  .value-split { gap: 40px; }
  .value-split-body { padding: 36px 30px; }
}

@media (max-width: 860px) {
  .value-split { grid-template-columns: 1fr; gap: 28px; }
  .value-split .value-split-media { order: 1; }
  .value-split .value-split-body { order: 2; }
  .value-split-media img { min-height: 0; aspect-ratio: 16 / 10; }
}

/* Floating labels, PART 8.2. The label sits in the field and eases up to 12px
   on focus or fill at 180ms. Present on the sellers form only; the label
   floats via :focus and :not(:placeholder-shown), so every input carries a
   single space placeholder. A select cannot be "placeholder shown", so its
   label holds the floated position permanently. */
.ff-float {
  position: relative;
  gap: 0;
}

.ff-float label {
  position: absolute;
  left: 17px;
  top: 17px;
  margin: 0;
  padding: 0 2px;
  background: transparent;
  font-size: 14px;
  font-weight: 400;
  letter-spacing: .04em;
  text-transform: none;
  color: var(--ink-faint);
  pointer-events: none;
  transform-origin: left top;
  transition: transform .18s var(--ease), color .18s var(--ease),
              background-color .18s var(--ease);
  /* One line, CLIPPED to the field. An absolutely positioned nowrap label
     otherwise runs past a tight column and gives the whole page a sideways
     scroll, which is how /sellers/ failed the sweep at 320px. */
  white-space: nowrap;
  max-width: calc(100% - 34px);
  overflow: hidden;
  text-overflow: ellipsis;
}

.ff-float input,
.ff-float select {
  padding-top: 22px;
  padding-bottom: 8px;
}

.ff-float input:focus + label,
.ff-float input:not(:placeholder-shown) + label,
.ff-float select + label {
  transform: translateY(-13px) scale(.86);
  color: var(--ink-soft);
}

/* PART 8.2: a 2px ink focus ring on the fields of this form. */
.ff-float input:focus,
.ff-float select:focus {
  border-color: var(--ink);
  box-shadow: inset 0 0 0 1px var(--ink);
}

/* PART 8.2: the submit runs the full width of the form column. ROUND 7 PART 7.1
   puts the contact form on the same footing, so the two read as siblings. */
.value-split .form-actions .btn,
.contact-form .form-actions .btn { width: 100%; }

/* ROUND 7 PART 7.1: the textarea needs the same floating label as the inputs.
   It sits taller than a field, so the label rests at the top of the box rather
   than centred on a single line of text. */
.ff-float textarea {
  padding-top: 26px;
  padding-bottom: 10px;
}

.ff-float textarea:focus + label,
.ff-float textarea:not(:placeholder-shown) + label {
  transform: translateY(-13px) scale(.86);
  color: var(--ink-soft);
}

.ff-float textarea:focus {
  border-color: var(--ink);
  box-shadow: inset 0 0 0 1px var(--ink);
}

/* ------------------------------------------------- contact, ROUND 7 PART 7 */
/* The client's note was that the page "looks very plain". The form and the
   details were correct; what was missing was warmth, so the page gains a serif
   line in her voice, a designed panel instead of a paragraph list, and a
   photograph. */
.contact-pull {
  margin-bottom: 30px;
  font-family: var(--display);
  font-style: italic;
  font-weight: 400;
  font-size: clamp(19px, 2vw, 24px);
  line-height: 1.5;
  color: var(--ink-serif);
  max-width: 34ch;
}

.contact-panel {
  padding: 34px 32px 30px;
  background: var(--sand);
}

.contact-group {
  margin-top: 26px;
  padding-top: 18px;
  border-top: 1px solid rgba(0, 0, 0, .12);
}

/* The short rule the note asks for sits under the LABEL, not across the panel,
   so each group reads as its own entry. */
.contact-label {
  font-family: var(--body);
  font-size: 12.5px;
  font-weight: 500;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--ink-soft);
}

.contact-label::after {
  content: '';
  display: block;
  width: 26px;
  height: 1px;
  margin: 10px 0 12px;
  background: var(--ink);
}

.contact-value {
  font-size: 16px;
  line-height: 1.6;
  color: var(--ink-serif);
}

.contact-value a { text-decoration: underline; text-underline-offset: 3px; }

/* 44px tappable rows, which is the minimum comfortable touch target, instead of
   three links run together in a sentence. */
.contact-social { list-style: none; }

.contact-social a {
  display: flex;
  align-items: center;
  /* flex-start, not space-between: the row holds one text node now that the
     trailing rule is gone, and space-between on a single child is a no-op that
     invites the next person to re-add something to the far edge. */
  justify-content: flex-start;
  min-height: 44px;
  padding: 0 2px;
  border-bottom: 1px solid rgba(0, 0, 0, .12);
  font-size: 15px;
  text-decoration: none;
  transition: padding-left .25s var(--ease);
}

.contact-social li:last-child a { border-bottom: 0; }

/* ROUND 8 PART 3: the trailing rule is GONE. It was meant as a hover
   affordance that grew 18px to 30px, but at rest it rendered as a short black
   dash at the far end of every row - `justify-content: space-between` pushed it
   hard right - and read as an empty or broken element rather than as
   decoration. A hover state nobody sees until they hover cannot justify a mark
   that is wrong the rest of the time. Rows are now clean text, and hover
   underlines the text itself, which is what every other link on the site does. */
.contact-social a:hover {
  padding-left: 6px;
  text-decoration: underline;
  text-underline-offset: 3px;
}

/* ROUND 8 PART 2: the contact street photo is removed. It sat as a sibling
   BELOW the details panel in the right column, so on a tall viewport it floated
   away from the text it was meant to sit with and read as a stray image at the
   bottom of the page. The details panel is now the single right-column element
   and the section ends after the form. */

/* PART 7.5, the phone order: pull line, form, details panel, photo. That is
   already the source order, so nothing needs reordering; the panel only needs
   air once it is no longer beside the form. */
@media (max-width: 860px) {
  .contact-panel { margin-top: 8px; }
}

/* --------------------------------------------- About open columns, ROUND 3 */
/* PART 10.1: no boxes. Each item is an open column with a large serif numeral
   behind the title at 15 percent opacity, a 40px rule that extends to full
   width on hover, and the reveal brings the numeral in first. */
.open-steps {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 48px;
}

@media (max-width: 1024px) {
  .open-steps { grid-template-columns: 1fr; gap: 40px; }
}

.open-step {
  position: relative;
  min-width: 0;
}

/* ROUND 5 PART 6.2: the ghost numerals behind the titles are removed. The
   rules, serif titles, reveal and hover extension all stay. */

/* The content follows the numeral by 120ms. The rule keeps `width` in its
   transition list here, or the hover extension would snap instead of easing:
   this selector still matches after .is-in lands, so it owns the transition. */
.js [data-reveal] .open-step-rule,
.js [data-reveal] .open-step-title,
.js [data-reveal] .open-step-body {
  opacity: 0;
  transform: translateY(14px);
  transition: opacity .6s var(--ease) .12s, transform .6s var(--ease) .12s,
              width .3s var(--ease);
}

.js [data-reveal].is-in .open-step-rule,
.js [data-reveal].is-in .open-step-title,
.js [data-reveal].is-in .open-step-body {
  opacity: 1;
  transform: none;
}

.open-step-rule {
  width: 40px;
  height: 2px;
  background: var(--ink);
  transition: width .3s var(--ease);
}

.open-step:hover .open-step-rule { width: 100%; }

.open-step-title {
  position: relative;
  margin-top: 26px;
  font-family: var(--display);
  font-weight: 300;
  font-size: 22px;
  letter-spacing: .06em;
  text-transform: uppercase;
}

.open-step-body {
  margin-top: 12px;
  font-size: 18px;
  line-height: 1.7;
  color: var(--ink-soft);
}

/* ------------------------------------------------------------------ reveal */
/* [CHECKLIST 2.8] Restrained: this is an elegant light site, not a showcase.
   Only armed once JS adds .js to <html>, so with scripts blocked every element
   stays at its default, which is fully visible. */
.js [data-reveal] {
  opacity: 0;
  transform: translateY(24px);
  transition: opacity .7s var(--ease), transform .7s var(--ease);
}

.js [data-reveal].is-in {
  opacity: 1;
  transform: none;
}

/* Photos scale from 1.04 to 1 on reveal. */
.js [data-reveal] .hood-media img,
.js [data-reveal].split-media img {
  transform: scale(1.04);
  transition: transform .9s var(--ease);
}

.js [data-reveal].is-in .hood-media img,
.js [data-reveal].is-in.split-media img {
  transform: none;
}

/* [CHECKLIST 2.8] The reduced motion guard wraps everything. Users who ask for
   less motion see final values immediately, including the stat counters. */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: .001ms !important;
    transition-duration: .001ms !important;
  }
  html { scroll-behavior: auto; }
  .js [data-reveal] {
    opacity: 1 !important;
    transform: none !important;
  }
}

/* ------------------------------------------------------------ legal pages */
.legal {
  padding: calc(var(--nav-h) + 72px) 0 96px;
}

.legal h2 {
  margin-top: 44px;
  font-family: var(--display);
  font-weight: 300;
  font-size: 20px;
  letter-spacing: .06em;
  text-transform: uppercase;
}

.legal h2:first-of-type { margin-top: 32px; }

.legal p,
.legal li {
  color: var(--ink-soft);
  font-size: 16px;
  line-height: 1.75;
}

.legal p { margin-top: 14px; }

.legal ul {
  margin-top: 14px;
  padding-left: 20px;
  list-style: disc;
}

.legal li + li { margin-top: 8px; }

.legal .measure { max-width: 72ch; }
