:root{
  --paper:#FEF5EE; --paper-deep:#FAEBDF; --ink:#1F1B1A; --ink-soft:#5B534E;
  --accent:#A97E68; --line:rgba(31,27,26,0.14);
  --serif:'Playfair Display',Georgia,serif; --body:'Source Serif 4',Georgia,serif;
  --pad:clamp(64px,9vw,120px);
}
*{box-sizing:border-box}
body{margin:0;background:var(--paper);color:var(--ink);font-family:var(--body);
  line-height:1.65;-webkit-font-smoothing:antialiased}
h1,h2,h3{font-family:var(--serif);line-height:1.15;margin:0 0 0.4em;font-weight:600}
p{margin:0 0 1.1em;color:var(--ink-soft)}
img{max-width:100%;display:block}
a{color:inherit}
.wrap{max-width:1120px;margin:0 auto;padding:0 clamp(24px,4vw,48px)}

/* ---- nav: her real logo, nothing else competing with it ---- */
.nav{display:flex;align-items:center;justify-content:space-between;padding-top:28px;padding-bottom:28px}
.nav img{height:clamp(38px,4.6vw,56px);width:auto}
.nav-links{display:flex;gap:36px;font-size:0.92rem;letter-spacing:0.03em}
.nav-links a{text-decoration:none;opacity:0.75;transition:opacity .2s}
.nav-links a:hover{opacity:1}
.nav-cta{border:1px solid var(--ink);padding:11px 22px;font-size:0.78rem;letter-spacing:0.08em;
  text-transform:uppercase;text-decoration:none;white-space:nowrap;transition:background-color .2s,color .2s}
.nav-cta:hover{background:var(--ink);color:var(--paper)}

/* Mobile menu — a real <button>, JS-driven, not the earlier checkbox+label
   hack. That version worked visually but was a real, live accessibility
   bug caught by independent audit: a <label> is not natively focusable or
   activatable by Enter/Space, so keyboard-only and switch users could not
   open the mobile menu at all, on any page, under 760px — silent to a
   mouse user, total blocker to anyone who doesn't use one. A <button>
   toggled from script.js fixes the operability AND lets aria-expanded be
   set correctly, which the checkbox approach could never do (:checked
   reflects the checkbox's own state, not a real semantic disclosure
   state). State now lives as an .is-open class added to .mobile-panel and
   .menu-btn together, not a sibling-selector trick off a hidden input. */
.menu-btn{display:none;flex-direction:column;gap:5px;background:none;border:none;
  padding:8px;cursor:pointer;z-index:20}
.menu-btn span{display:block;width:24px;height:2px;background:var(--ink);
  transition:transform .25s,opacity .25s;pointer-events:none}
.mobile-panel{display:none}
@media(max-width:760px){
  .nav-links{display:none}
  .menu-btn{display:flex}
  .mobile-panel{
    display:flex;flex-direction:column;gap:0;position:fixed;inset:0;
    background:var(--paper);z-index:15;padding:100px 32px 40px;
    transform:translateX(100%);transition:transform .3s ease;
    visibility:hidden}
  .mobile-panel a{font-family:var(--serif);font-size:1.6rem;text-decoration:none;
    color:var(--ink);padding:18px 0;border-bottom:1px solid var(--line)}
  .mobile-panel .nav-cta{margin-top:28px;text-align:center;font-size:0.9rem;padding:16px}
  .mobile-panel.is-open{transform:translateX(0);visibility:visible}
  .menu-btn.is-open span:nth-child(1){transform:translateY(7px) rotate(45deg)}
  .menu-btn.is-open span:nth-child(2){opacity:0}
  .menu-btn.is-open span:nth-child(3){transform:translateY(-7px) rotate(-45deg)}
}

/* ---- hero: split, a real photo IN the hero, not pushed below it ----
   The first build put her photos in a band under the hero, so the hero
   itself was pure type on flat colour. Compared directly against the
   Astise bespoke build — split layout, a genuine full-height photo of the
   work happening right beside the headline — and that comparison is the
   right one: it's the proven pattern for exactly this kind of bespoke
   build. Rebuilt the same way with Lea's actual strongest photo (the
   marble butler's pantry) rather than leaving it for a proof section
   further down the page. */
.hero{padding:0}
.hero-grid{display:grid;grid-template-columns:1fr 1fr;min-height:min(86vh,860px);align-items:stretch}
.hero-copy{display:flex;flex-direction:column;justify-content:center;
  padding:clamp(40px,6vw,72px) clamp(24px,4vw,48px)}
/* Explicit height, not width/height:100% — percentage heights only resolve
   against a parent with a DEFINITE height, and a grid row sized by
   min-height on the container doesn't reliably count as definite for a
   percentage-height grid item. Confirmed on the live page: 100% still
   let the figure size to the image's raw 640x1136 intrinsic dimensions
   instead of the grid cell, THEN the child img's own width/height:100%
   compounded the error further (rendered even larger than that). Setting
   the same clamp() used on .hero-grid's min-height directly here removes
   every layer of percentage-resolution ambiguity. */
.hero-photo{position:relative;overflow:hidden;background:var(--ink);width:100%;
  height:clamp(420px,86vh,860px)}
.hero-photo img{width:100%;height:100%;object-fit:cover;display:block}
.hero-photo::after{content:'';position:absolute;inset:0;
  background:linear-gradient(120deg, rgba(31,27,26,0.18), transparent 55%)}
@media(max-width:900px){
  .hero-grid{grid-template-columns:1fr;min-height:auto}
  .hero-photo{aspect-ratio:4/3;height:auto;order:-1}
  .hero-copy{padding:clamp(32px,6vw,56px) clamp(24px,4vw,48px)}
}
.eyebrow{font-size:0.8rem;letter-spacing:0.16em;text-transform:uppercase;color:var(--accent);
  font-weight:600;margin-bottom:18px}
.hero h1{font-size:clamp(2.4rem,1.8rem + 2.4vw,3.9rem);letter-spacing:-0.015em;max-width:12ch}
.hero .lede{font-size:clamp(1.02rem,1rem + 0.3vw,1.18rem);max-width:44ch;color:var(--ink-soft)}
.hero-actions{display:flex;gap:16px;align-items:center;margin-top:32px;flex-wrap:wrap}
.btn-solid{background:var(--ink);color:var(--paper);padding:16px 32px;text-decoration:none;
  font-size:0.85rem;letter-spacing:0.06em;text-transform:uppercase;font-weight:500;
  transition:opacity .2s,transform .15s}
.btn-solid:hover{opacity:0.85}
.btn-solid:active{transform:scale(0.97)}
.btn-line{border:1px solid var(--ink);padding:15px 31px;text-decoration:none;font-size:0.85rem;
  letter-spacing:0.06em;text-transform:uppercase;font-weight:500;
  transition:background-color .2s,color .2s,transform .15s}
.btn-line:hover{background:var(--ink);color:var(--paper)}
.btn-line:active{transform:scale(0.97)}
.trust-strip{margin-top:36px;padding-top:28px;border-top:1px solid var(--line);
  display:flex;gap:clamp(20px,4vw,48px);flex-wrap:wrap;font-size:0.82rem;color:var(--ink-soft);
  letter-spacing:0.02em}
.trust-strip span{display:flex;align-items:center;gap:8px}
.trust-strip svg{flex:0 0 auto}

/* ---- proof: her real work, asymmetric — not a uniform grid ---- */
.proof{padding:var(--pad) 0;background:var(--paper-deep)}
.proof-head{max-width:60ch;margin-bottom:48px}
.proof-head .eyebrow{margin-bottom:14px}
.proof-grid{display:grid;grid-template-columns:1.4fr 1fr;gap:20px}
.proof-grid figure{margin:0;position:relative;overflow:hidden}
.proof-grid img{width:100%;height:100%;object-fit:cover;display:block}
.proof-main{grid-row:span 2}
.proof-main img{aspect-ratio:4/3}
.proof-side{aspect-ratio:16/10}
.proof-side img{aspect-ratio:16/10}
figcaption{position:absolute;left:0;right:0;bottom:0;padding:18px 20px;
  background:linear-gradient(to top, rgba(31,27,26,0.72), transparent);
  color:#fff;font-size:0.85rem;letter-spacing:0.02em}
@media(max-width:760px){
  .proof-grid{grid-template-columns:1fr}
  .proof-main{grid-row:auto}
}

/* ---- the objection, answered directly — this is the actual sale ---- */
.trust{padding:var(--pad) 0}
.trust-head{max-width:56ch;margin-bottom:56px}
.trust-cols{display:grid;grid-template-columns:repeat(3,1fr);gap:clamp(28px,4vw,56px)}
.trust-card h3{font-family:var(--body);font-size:1.08rem;font-weight:600;margin-bottom:10px}
.trust-card .num{font-family:var(--serif);font-size:0.95rem;color:var(--accent);
  margin-bottom:14px;display:block}
.trust-card p{font-size:0.95rem;margin-bottom:0}
@media(max-width:760px){ .trust-cols{grid-template-columns:1fr;gap:36px} }

/* ---- services: her real list, all of it ---- */
.services{padding:var(--pad) 0;background:var(--paper-deep)}
.services-head{display:flex;justify-content:space-between;align-items:flex-end;
  gap:24px;flex-wrap:wrap;margin-bottom:44px}
.services-head .eyebrow{margin-bottom:14px}
.services-head h2{max-width:14ch;margin-bottom:0}
.services-head p{max-width:40ch;margin:0}
.svc-list{display:grid;grid-template-columns:repeat(2,1fr);border-top:1px solid var(--line)}
.svc-row{padding:22px 0;border-bottom:1px solid var(--line);padding-right:32px}
.svc-row:nth-child(odd){padding-right:32px}
.svc-row h3{font-family:var(--body);font-weight:500;font-size:1.02rem;margin-bottom:5px}
.svc-row p{font-size:0.9rem;margin:0}
.svc-featured h3{color:var(--accent)}
@media(max-width:760px){ .svc-list{grid-template-columns:1fr} }

/* ---- about: the family-owned story, in her voice ---- */
.about{padding:var(--pad) 0}
.about-grid{display:grid;grid-template-columns:0.9fr 1.1fr;gap:clamp(32px,5vw,72px);align-items:center}
.about-grid img{aspect-ratio:3/4;object-fit:cover}
.about h2{max-width:12ch}
.stat-row{display:flex;gap:clamp(28px,4vw,52px);margin-top:32px;flex-wrap:wrap}
.stat-row div strong{font-family:var(--serif);font-size:1.7rem;display:block;font-weight:600}
.stat-row div span{font-size:0.82rem;color:var(--ink-soft);letter-spacing:0.02em}
@media(max-width:760px){ .about-grid{grid-template-columns:1fr} .about-grid img{order:-1} }

/* ---- CTA band ---- */
.cta-band{padding:var(--pad) 0;background:var(--ink);color:var(--paper);text-align:center}
.cta-band h2{color:var(--paper);max-width:16ch;margin:0 auto 16px}
.cta-band p{color:rgba(254,245,238,0.72);max-width:48ch;margin:0 auto 32px}
.cta-band .btn-line{border-color:var(--paper);color:var(--paper)}
.cta-band .btn-line:hover{background:var(--paper);color:var(--ink)}
.contact-methods{display:flex;gap:36px;justify-content:center;margin-top:36px;flex-wrap:wrap;
  font-size:0.9rem}
.contact-methods a{text-decoration:none;border-bottom:1px solid rgba(254,245,238,0.35)}

footer{padding:32px 0;font-size:0.8rem;color:var(--ink-soft);border-top:1px solid var(--line)}
.foot-row{display:flex;justify-content:space-between;flex-wrap:wrap;gap:12px}

/* ---- motion system ----
   Everything above was static — every section just appeared, no entrance,
   no stagger, no response to being looked at. That's the actual gap
   between this and a genuinely current site, more than any single colour
   or spacing decision: STYLE-DIRECTIONS.md's own 2026 trend note names
   micro-interactions as one of the few trends that demonstrably delivered,
   and none were built in here. One system, reused everywhere, rather than
   a different effect per section — consistency is what makes motion read
   as intentional instead of decorative. */
html{scroll-behavior:smooth}
.reveal{opacity:0;transform:translateY(28px);transition:opacity .7s cubic-bezier(.16,1,.3,1),transform .7s cubic-bezier(.16,1,.3,1)}
.reveal.is-visible{opacity:1;transform:translateY(0)}
/* Staggered children get their delay from a CSS custom property set inline
   per item, not a fixed nth-child ladder — works for lists of any length
   (the 10-row services list) without writing 10 rules by hand. */
.stagger > *{opacity:0;transform:translateY(20px);
  transition:opacity .6s cubic-bezier(.16,1,.3,1),transform .6s cubic-bezier(.16,1,.3,1);
  transition-delay:calc(var(--i, 0) * 70ms)}
.stagger.is-visible > *{opacity:1;transform:translateY(0)}

/* Proof grid: a slow, contained zoom on hover — the one continuous-motion
   exception, because it's user-triggered (hover), not scroll-linked, and
   contained by overflow:hidden on the figure so it never disturbs layout. */
.proof-grid figure{cursor:default}
.proof-grid img{transition:transform .6s cubic-bezier(.16,1,.3,1)}
.proof-grid figure:hover img{transform:scale(1.045)}

/* ---- above-the-fold load-in — runs on page load, not scroll ----
   Deliberately NOT wired to the IntersectionObserver the rest of the page
   uses. The hero is visible the instant the page paints, so there is
   nothing to "scroll into view" — gating it behind the same observer as
   everything else would mean it only plays if the observer's first tick
   happens to land before the user notices, which is fragile for exactly
   the one section every visitor sees first. A .loaded class added to
   <body> as soon as the DOM is parseable triggers all of it directly. */
h1{overflow:visible}
.line-mask{display:block;overflow:hidden}
.line-in{display:block;transform:translateY(115%);transition:transform .8s cubic-bezier(.16,1,.3,1)}
body.loaded .line-in{transform:translateY(0);transition-delay:calc(var(--i, 0) * 90ms)}

.load-in{opacity:0;transform:translateY(16px);transition:opacity .7s ease,transform .7s ease}
body.loaded .load-in{opacity:1;transform:translateY(0);transition-delay:calc(var(--i, 0) * 90ms)}

/* The photo uncovers itself rather than fading in — a clip-path wipe reads
   as considerably more deliberate than opacity alone, and it is the one
   place on the page it is used, so it stays a signature moment rather than
   becoming wallpaper. */
.load-in-wipe{clip-path:inset(0 0 100% 0);transition:clip-path 1s cubic-bezier(.16,1,.3,1) .3s}
body.loaded .load-in-wipe{clip-path:inset(0 0 0% 0)}

/* Magnetic buttons — a small cursor-following offset on the primary CTAs,
   applied via inline transform from a tiny JS listener below. Capped
   movement (see the JS) so it reads as a subtle response to the pointer,
   not a toy that flings the button around. */
.magnetic{transition:transform .25s cubic-bezier(.16,1,.3,1)}

@media(prefers-reduced-motion:reduce){
  html{scroll-behavior:auto}
  .reveal, .stagger > *, .proof-grid img, .line-in, .load-in, .load-in-wipe, .magnetic{
    transition:none!important;transform:none!important;opacity:1!important;clip-path:none!important}
}

/* ---- interior page header — a smaller, non-hero band for Services/About/
   Our Work/Contact, so those pages have their own real identity instead
   of borrowing the home hero. Deliberately restrained (no photo) so the
   home page stays the one place a visitor sees the big split-hero
   treatment — repeating it on every page would cheapen it. ---- */
.page-header{padding:clamp(140px,18vw,200px) 0 clamp(48px,6vw,72px)}
.page-header h1{font-size:clamp(2.4rem,1.9rem + 2vw,3.4rem);max-width:16ch}
.page-header p{font-size:1.05rem;max-width:56ch;margin-top:16px}

/* ---- full gallery — Our Work page, all real photos, not the home page's
   condensed 3-tile proof grid ---- */
.gallery-full{padding:0 0 var(--pad);display:grid;grid-template-columns:repeat(2,1fr);gap:20px}
.gallery-full figure{margin:0;position:relative;overflow:hidden;aspect-ratio:4/3}
.gallery-full img{width:100%;height:100%;object-fit:cover;display:block;
  transition:transform .6s cubic-bezier(.16,1,.3,1)}
.gallery-full figure:hover img{transform:scale(1.045)}
@media(max-width:760px){ .gallery-full{grid-template-columns:1fr} }

/* ---- expanded services page — each service gets real space, not a
   one-line row, since this page's whole job is depth the home page
   teaser can't carry ---- */
.svc-full{border-top:1px solid var(--line)}
.svc-full-row{padding:36px 0;border-bottom:1px solid var(--line);
  display:grid;grid-template-columns:0.5fr 1.5fr;gap:32px}
.svc-full-row .svc-num{font-family:var(--serif);color:var(--accent);font-size:1.1rem}
.svc-full-row h3{font-size:1.3rem;margin-bottom:10px}
.svc-full-row p{font-size:1rem;max-width:56ch}
@media(max-width:700px){ .svc-full-row{grid-template-columns:1fr;gap:8px} }

/* ---- simple content page (About, Contact) — a readable single column,
   not stretched full-width, matching Editorial Minimal's own rule that
   long-form text needs a measured line length ---- */
.content-narrow{max-width:70ch}
.content-narrow p{font-size:1.05rem}
.content-narrow h2{margin-top:1.6em}

/* ---- contact page: methods laid out as real, distinct blocks ---- */
.contact-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:24px;margin-top:40px}
.contact-block{border:1px solid var(--line);padding:28px}
.contact-block .eyebrow{margin-bottom:10px}
.contact-block a{font-family:var(--serif);font-size:1.3rem;text-decoration:none;
  display:inline-block;margin-top:4px;border-bottom:1px solid var(--accent)}
@media(max-width:700px){ .contact-grid{grid-template-columns:1fr} }

/* ---- quote form — Contact page. Sits alongside the phone/email links,
   doesn't replace them (per audit brief: "keep the phone/email links
   as-is alongside it"). Submits via fetch to /api/quote.js rather than a
   real page-navigation POST, so a validation error or success message can
   show inline without losing what was typed. ---- */
.quote-form{margin-top:56px;max-width:640px}
.quote-form .field{margin-bottom:20px}
.quote-form label{display:block;font-size:0.85rem;font-weight:600;letter-spacing:0.02em;margin-bottom:6px}
.quote-form input,.quote-form select,.quote-form textarea{
  width:100%;font:inherit;font-size:1rem;padding:13px 14px;border:1px solid var(--line);
  background:var(--paper);color:var(--ink);border-radius:0}
.quote-form input:focus,.quote-form select:focus,.quote-form textarea:focus{
  outline:2px solid var(--accent);outline-offset:1px}
.quote-form textarea{min-height:110px;resize:vertical}
.quote-form .row2{display:grid;grid-template-columns:1fr 1fr;gap:16px}
@media(max-width:600px){ .quote-form .row2{grid-template-columns:1fr} }
/* Honeypot — visually and structurally hidden from a sighted mouse user
   and never in the tab order, but present in the DOM for a bot's form
   scraper to find and fill. left:-9999px rather than display:none, since
   some spam scripts specifically skip display:none fields. */
.quote-form .hp{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}
.quote-form .form-msg{margin-top:14px;font-size:0.92rem}
.quote-form .form-msg.ok{color:#2f6b3a}
.quote-form .form-msg.err{color:#b3261e}
