/* ==========================================================================
   Cricket on the Hearth Staging & Design
   Locked direction: Brand True (2026-07-29) - Cinzel / Lora, olive #4E5830
   Motion per brand-kit/interactions.md. Do not re-theme without re-locking.
   ========================================================================== */

/* ===== §6b self-hosted fonts. No CDN. Variable axes cover every used weight,
   so there is no faux-bold and no swap on first paint. ===== */
@font-face{
  font-family:'Cinzel';
  src:url('../fonts/cinzel-var-latin.woff2') format('woff2-variations'),
      url('../fonts/cinzel-var-latin.woff2') format('woff2');
  font-weight:400 700; font-style:normal; font-display:swap;
  unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face{
  font-family:'Lora';
  src:url('../fonts/lora-var-latin.woff2') format('woff2-variations'),
      url('../fonts/lora-var-latin.woff2') format('woff2');
  font-weight:400 700; font-style:normal; font-display:swap;
  unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face{
  font-family:'Lora';
  src:url('../fonts/lora-italic-var-latin.woff2') format('woff2-variations'),
      url('../fonts/lora-italic-var-latin.woff2') format('woff2');
  font-weight:400 700; font-style:italic; font-display:swap;
}
/* Metric-matched fallbacks so the swap causes no layout shift (§6b.4) */
@font-face{
  font-family:'Cinzel Fallback'; src:local('Times New Roman'), local('Georgia');
  size-adjust:96%; ascent-override:98%; descent-override:24%; line-gap-override:0%;
}
@font-face{
  font-family:'Lora Fallback'; src:local('Georgia'), local('Times New Roman');
  size-adjust:100%; ascent-override:101%; descent-override:26%; line-gap-override:0%;
}

:root{
  /* locked tokens */
  --bg:#F7F5F0; --surface:#FFFFFF;
  --ink:#4E4E4E;
  /* WAVE 7: --ink-soft was the locked #7A7A73 and it does NOT clear WCAG AA for
     text at any size. Measured on rendered pixels, not computed: 4.32:1 on the
     white surface and 3.97:1 on the cream page ground, against §7's 4.5 floor.
     It is the muted-text token, so every rule that used it was shipping sub-AA
     body copy: .form__consent (the line under the submit button on the
     conversion page), .cite (wave 6's citation block class, deleted as dead in
     wave 9), and the two labels this wave added. The live consumers today are
     .form__consent, .next-svc__k, .trust-t and .sig-role. Nothing in five waves
     of contrast checking had ever sampled the token, because every prior checker
     was scoped to the surfaces its own wave touched and none of them touched
     muted text on a light ground.

     Darkened to #6E6E67 - the same hue and chroma, ~12% less luminance. Measured
     after: 4.72:1 on cream, 5.13:1 on white. §7 AA is MANDATORY and outranks the
     locked token table, so the value here is now the authority; design.md still
     records #7A7A73 and the deviation is logged in POLISH-LOG rather than edited
     into the locked block by hand. */
  --ink-soft:#6E6E67;
  --ink-strong:#33332F;                 /* body-copy ink; the locked #4E4E4E is the wordmark grey */
  --accent:#4E5830; --accent-2:#7E9C48; --accent-on:#FFFFFF;
  --radius:2px;
  --line:color-mix(in srgb, var(--ink) 20%, transparent);
  --line-soft:color-mix(in srgb, var(--ink) 11%, transparent);

  --font-h:'Cinzel','Cinzel Fallback',Georgia,serif;
  --font-b:'Lora','Lora Fallback',Georgia,serif;

  /* generous density */
  --s1:10px; --s2:20px; --s3:32px; --s4:52px; --s5:84px; --s6:124px;
  --maxw:1180px;

  /* motion (interactions.md) */
  --ease-settle:cubic-bezier(.25,.74,.22,.99);
  --ease-hover:cubic-bezier(.4,0,.2,1);
  --dur-hover:.25s; --dur-settle:.6s; --dur-reveal:.8s; --dur-hero:1.1s;

  /* The button lighting layer (item 3.6), tokenised so the header CTA, the closer
     button and the form submit all wear the SAME lit surface rather than three
     hand-copied gradients that drift apart.
     RETUNED after looking at it. The first pass ran .11 white to .07 black and
     painted rgb(96,105,69) at the top against rgb(74,83,46) at the foot - a
     22-point spread on a 60px button, which is not a lit surface, it is a
     visible gradient, and it read as a 2010 web button on the mobile capture.
     P-002's own wording is the test: felt, not seen. Halved, and the ramp now
     passes through fully transparent at 58% so the brand olive is EXACTLY
     #4E5830 across the band the label sits on. Total spread ~12 points, about 5%.
     If this ever needs to move, move it down, not up. */
  --btn-lit:linear-gradient(180deg,rgba(255,255,255,.05) 0%,
                                   rgba(255,255,255,0) 58%,
                                   rgba(0,0,0,.04) 100%);

  /* item 4.6 - the framed-photograph treatment, tokenised for the same reason
     --btn-lit is: every photographic frame on the site wears ONE mount, not six
     hand-copied box-shadows that drift.

     Two layers, and the inner one is the point. A photograph laid straight onto
     #F7F5F0 has no edge - a pale wall or a white cabinet at the frame boundary
     bleeds into the page and the picture loses its own perimeter, which is what
     makes a photo grid read as "images pasted in" rather than as mounted prints.
     The ring is the SAME hairline the site already uses as layout structure
     (interactions.md's hairline section: color-mix on --ink, never a flat grey),
     so it reads as the existing system rather than as chrome.

     The outer layer is deliberately NOT a black shadow: rgba(51,51,47,...) is
     the warm ink, so the ambient under a frame carries the page's own colour
     temperature. That is the "glow" half of the item, in this palette.

     --frame-ring-lt is the same ring inverted, for the frames that sit on a dark
     ground (the P-009 panel cards). A 14%-ink ring on #1E1E1A is invisible. */
  --frame-ring:inset 0 0 0 1px color-mix(in srgb, var(--ink) 15%, transparent);
  --frame-ring-lt:inset 0 0 0 1px rgba(255,255,255,.16);
  --frame-rest:0 2px 12px rgba(51,51,47,.06);

  --h1:clamp(2.25rem,4.4vw + .6rem,4.05rem);
  --h2:clamp(1.6rem,2.1vw + .8rem,2.5rem);
  --h3:clamp(1.05rem,.5vw + .9rem,1.24rem);
  --body:clamp(1rem,.28vw + .94rem,1.09rem);
}
/* WAVE 9: a narrow-width step on the display floor, because Cinzel cannot break.
   `--h1`'s 2.25rem floor is a 36px minimum that never bends, and Cinzel is
   all-caps with no lowercase, so the longest single word in an H1 is an
   unbreakable 306px object. /contact's "Book a WALKTHROUGH" therefore ran 18px
   off the right edge at 320px and the last two letters were painted outside the
   viewport - invisible, and not scrollable to, because body is overflow-x: clip.
   Stepped to 2.05rem below 360px: 32.8px, a 279px word inside a 288px column.
   The query stops at 360 deliberately so 375 and 430 - both real devices and both
   already correct - keep the size the operator approved. This is the same class
   of constraint the type note in design.md predicted when it locked Cinzel:
   "headline length is now a layout constraint". */
@media (max-width:360px){
  :root{ --h1:clamp(2.05rem,4.4vw + .6rem,4.05rem) }
}

*{box-sizing:border-box}
/* §17/P-009: clip, never hidden - hidden creates a scroll container and kills sticky */
html{overflow-x:clip}
/* WAVE 5: the header is `position:fixed` at 83px desktop / 71px mobile, so any
   fragment target lands UNDER it without this. Added when the four service pages
   started deep-linking into a named project on /work (#brookshire-new-build and
   friends) rather than all four pointing at the same generic /work. 104px clears
   the desktop bar with 21px of air. */
html{scroll-behavior:smooth;scroll-padding-top:104px}
body{
  margin:0;overflow-x:clip;background:var(--bg);color:var(--ink-strong);
  font-family:var(--font-b);font-size:var(--body);line-height:1.72;
  -webkit-font-smoothing:antialiased;font-synthesis:none;
}
img{max-width:100%;display:block}
a{color:inherit}

h1,h2,h3{
  font-family:var(--font-h);font-synthesis:none;margin:0;
  line-height:1.12;letter-spacing:.005em;text-wrap:balance;color:var(--ink-strong);
}
/* P-005 (MANDATORY, §7): balance is not only for headings. Every short, visually
   prominent block of text gets it, so no single word lands alone on a last line.
   Locked copy that balance still breaks badly is bound with &nbsp; in the HTML.
   Long-form prose keeps `pretty` (below) - balance over-distributes paragraphs. */
.eyebrow,.lede,.rule-label span,.rule-label h2,.proof__l,.card__more,.proj__m,.phero__meta,.ba__d,
.closer p,.card p,.closer__t,.trust-t,.next-svc__l{
  text-wrap:balance
}
h1{font-size:var(--h1);font-weight:600}
h2{font-size:var(--h2);font-weight:600}
h3{font-size:var(--h3);font-weight:600;letter-spacing:.03em}
p{margin:0}
/* Long-form reading text takes `pretty`, never `balance`: balance re-distributes
   a whole paragraph and produces an uneven inter-line rhythm, while pretty only
   protects the last line from a single-word widow. There is no blog on this
   build, so `.prose p`, `.faq__a`, `.capsule` and the legal shell ARE the
   long-form surfaces and the rule is scoped to them (P-005 / item 2.10). */
article p,.prose p,.faq__a,.capsule,.legal p,.legal li,.prose li{text-wrap:pretty}

/* ===== The type scale has two floors, and they are different on purpose.
   READING text (anything set in sentence case that a visitor parses as prose)
   never goes under 1rem/16px. STRUCTURAL labels - the tracked uppercase runs
   that name a section, a project or a field - are furniture rather than reading
   matter and sit lower, with a hard floor of .72rem/11.5px. At .16-.22em
   tracking, Lora's cap-height at 11.5px lands near the x-height of 16px
   lowercase, which is what keeps them legible. Nothing on the site goes under
   that floor; every declaration below is at or above one of the two. */

.wrap{max-width:var(--maxw);margin:0 auto;padding:0 var(--s3)}
.eyebrow{
  font-family:var(--font-b);font-size:.72rem;letter-spacing:.22em;text-transform:uppercase;
  font-weight:600;color:var(--accent);margin:0
}
.lede{color:var(--ink);font-size:1.06rem;max-width:58ch}
/* A lede inside the editorial split is a full paragraph rather than a one-line
   standfirst - eight lines of it at 430px - and `balance` gives up past a few
   lines, which is how a single word ended up alone on the last line there.
   Long paragraphs take `pretty`, which exists to kill exactly that widow. */
.split .lede{text-wrap:pretty}

/* ===== a11y ===== */
.skip{
  position:fixed;top:12px;left:12px;z-index:300;background:var(--accent);color:#fff;
  padding:12px 20px;border-radius:var(--radius);font-weight:600;text-decoration:none;
  transform:translateY(-160%);transition:transform .2s ease
}
.skip:focus{transform:translateY(0)}
/* ===== the universal focus ring (item 3.4).
   2px accent, 4px offset, on EVERY focusable element, and the browser default is
   REPLACED rather than removed - there is no bare `outline:none` on this site
   (item 3.8; the one occurrence, on .field inputs, is a pointer-focus affordance
   with a :focus-visible restoration directly beneath it).
   The 4px offset is doing real work on the photographic elements: it places the
   ring OUTSIDE the frame, on the page ground where the olive measures 7.55:1,
   rather than on a photograph whose local luminance is unknown. */
a:focus-visible,button:focus-visible,input:focus-visible,select:focus-visible,
textarea:focus-visible,summary:focus-visible,[tabindex]:focus-visible{
  outline:2px solid var(--accent);outline-offset:4px;border-radius:2px
}
/* On every dark ground the olive ring is ~1.4:1 and would be an invisible focus
   state, which is worse than leaving the UA ring alone. The dark-surface accent
   (#C8DA9E - already the H1 accent word, the footer titles and the phero eyebrow)
   takes over there, so the ring is one system in two colourways rather than two
   systems. The header selector covers the transparent-over-photo state only: once
   the bar is scrolled, solid or open it is cream and the olive is correct again. */
.hero :focus-visible,.phero :focus-visible,.layers :focus-visible,
.closer :focus-visible,.site-footer :focus-visible,.callbar :focus-visible,
.site-header:not(.is-scrolled):not(.is-solid):not(.is-open) :focus-visible{
  outline-color:#C8DA9E
}
/* The skip link's own background IS var(--accent), so an accent ring on it is
   invisible by construction. White, and pulled in a little, reads on the cream
   pages and on the hero photograph alike. */
.skip:focus-visible{outline:2px solid #fff;outline-offset:3px}
.vh{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}

/* ===== buttons (§7b-5: stacked CTA groups share one width) ===== */
.btn{
  display:inline-flex;align-items:center;justify-content:center;gap:8px;
  /* item 3.6 - the primary button is a LIT surface, not a flat swatch. The
     gradient is a lighting layer laid OVER the brand colour rather than a second
     colour replacing it, so the olive stays exactly the locked #4E5830 and the
     same one declaration lights every variant from its own base. It is also the
     only form of this that keeps the hover colour change animating: a
     background-image does not interpolate, a background-color does. Variants that
     want a flat surface (the ghost, the glass on-photo button) clear it with the
     `background` shorthand, which is deliberate rather than accidental. */
  background-color:var(--accent);
  background-image:var(--btn-lit);
  color:var(--accent-on);border:1px solid var(--accent);
  border-radius:var(--radius);padding:15px 26px;text-decoration:none;
  font-family:var(--font-b);font-size:.95rem;font-weight:600;letter-spacing:.02em;
  cursor:pointer;touch-action:manipulation;
  /* item 3.7 - a real resting shadow that SHARPENS on hover. Two layers: a close
     contact shadow that seats the button on the page, and a wider ambient one.
     On hover the ambient shadow deepens and travels while the contact shadow
     tightens, which is what reads as the button lifting rather than merely
     changing colour. */
  box-shadow:0 2px 10px rgba(78,88,48,.18), 0 1px 2px rgba(78,88,48,.16);
  transition:background-color var(--dur-hover) var(--ease-hover),
             transform var(--dur-hover) var(--ease-hover),
             box-shadow var(--dur-hover) var(--ease-hover);
}
@media (hover:hover){
  .btn:hover{background-color:#3E4726;transform:translateY(-2px);
    box-shadow:0 14px 28px rgba(78,88,48,.30), 0 3px 6px rgba(78,88,48,.22)}
}
/* Press feedback lives OUTSIDE the hover query on purpose: a touch user gets it
   too, and it is the one state a phone can show. */
.btn:active{transform:translateY(0)}
.btn--ghost{background:transparent;color:var(--ink-strong);border-color:var(--line);box-shadow:none}
@media (hover:hover){ .btn--ghost:hover{background:var(--surface);border-color:var(--accent);color:var(--accent);
  transform:translateY(-2px);box-shadow:0 10px 24px rgba(51,51,47,.10)} }
.btn--on-photo{background:rgba(255,255,255,.12);color:#fff;border-color:rgba(255,255,255,.6);
  backdrop-filter:blur(8px);box-shadow:0 4px 18px rgba(0,0,0,.26)}
@media (hover:hover){ .btn--on-photo:hover{background:rgba(255,255,255,.22);transform:translateY(-2px);
  box-shadow:0 16px 34px rgba(0,0,0,.36)} }
.cta-row{display:flex;gap:14px;flex-wrap:wrap;margin-top:var(--s3)}

/* WAVE 7 (item 7.3): the phone mark. This is the ONLY icon on the site and it
   took an argument to add, so the reasoning is recorded here.

   Wave 1 logged that this build carries no icon font, no icon set and no icons
   at all, and that restraint is the reason the pages read as editorial rather
   than as a template. The counter-argument that won: on a local service site the
   phone is the shorter conversion path, and a bare number set in the same weight
   as every other button label does not read as an action. The mark is drawn here
   rather than imported - a single stroked path on a 20-unit box at 1.4 stroke,
   which renders at roughly 1px hairline at 15px. That is the SAME hairline the
   rest of the site already speaks in: the `.rule-label` rule, the FAQ toggle
   ring, the 2px accent bar over each stat. It inherits `currentColor`, so it is
   white on the hero glass, white in the dock and olive on the contact chip
   without a single colour being declared for it.

   Scope rule, and it is deliberate: the mark rides phone-ACTION buttons and
   chips only. A phone number inside a sentence (the footer line, the /contact
   NAP row, the /privacy paragraph) keeps no icon, because an icon inside prose
   is decoration rather than an affordance. */
.btn .ico-tel,.chip .ico-tel{flex:0 0 auto;width:15px;height:15px;display:block;
  transform:translateY(-.5px)}

/* ===== header - P-003 transparent over the hero, solid on scroll.
   The mark ships in two colourways: the reversed knockout rides the transparent
   state, the original takes over once the bar goes solid. Same artwork (§12b). */
.site-header{
  position:fixed;top:0;left:0;right:0;z-index:60;background:transparent;
  border-bottom:1px solid transparent;
  transition:background var(--dur-settle) var(--ease-settle),
             box-shadow var(--dur-settle) var(--ease-settle),
             border-color var(--dur-settle) var(--ease-settle)
}
.site-header.is-scrolled{
  background:var(--surface);border-bottom-color:var(--line-soft);
  box-shadow:0 2px 18px rgba(51,51,47,.09)
}
.hdr{display:flex;align-items:center;justify-content:space-between;gap:var(--s2);
  max-width:var(--maxw);margin:0 auto;padding:12px var(--s3)}
/* The wordmark is a link on all 11 pages and had no hover state. The dim goes on
   the WRAPPER, never on `.brand img`: the two colourways are cross-faded by their
   own `opacity` at the same specificity, so an opacity hover on the image would
   have silently broken the P-003 mark swap. Wrapper opacity multiplies the
   children instead of competing with them. */
.brand{display:flex;align-items:center;flex:none;text-decoration:none;position:relative;
  transition:opacity var(--dur-hover) var(--ease-hover)}
@media (hover:hover){ .brand:hover{opacity:.82} }
.brand img{height:58px;width:auto;transition:opacity var(--dur-settle) var(--ease-settle)}
.brand__rev{position:absolute;left:0;top:50%;transform:translateY(-50%)}
.site-header .brand__dark{opacity:0}
.site-header .brand__rev{opacity:1}
.site-header.is-scrolled .brand__dark{opacity:1}
.site-header.is-scrolled .brand__rev{opacity:0}
/* transparent state: nav sits on the photo, so it needs the P-002 treatment */
.site-header .nav a:not(.btn){color:#fff;text-shadow:0 1px 8px rgba(0,0,0,.5)}
.site-header.is-scrolled .nav a:not(.btn){color:var(--ink-strong);text-shadow:none}
/* The transparent-state nav button is glass over a photograph, so it clears the
   lighting layer and the olive shadow (both are invisible or wrong on a photo)
   and takes a soft dark shadow instead, which is what separates it from the
   image. The moment the bar goes solid it becomes the standard lit primary.

   THE GLASS IS DARK, NOT LIGHT, and that is a legibility fix rather than taste.
   A 14% WHITE wash lightens whatever is behind it, so on the interior heroes the
   composite under "Schedule a consultation" measured rgb(163,163,161) and white
   14.4px/600 type scored 2.53:1 against a 4.5 floor. Every interior hero failed:
   occupied 2.53, new-build 2.89, vacant 3.06, about 3.33, work 3.78. It passed on
   the homepage only because that photograph happens to be dark under the bar,
   which is exactly the kind of luck a fixed header over arbitrary photography
   cannot rely on. A dark wash inverts the dependency: the darker the tint, the
   less the photograph matters. At 42% the same worst-case backdrop composites to
   about rgb(96) and the button clears 6:1, and the white border plus the blur
   keep it reading as glass rather than as a solid chip.
   Note the sibling nav links already take text-shadow for the same reason. */
.site-header .nav .btn{background:rgba(20,20,16,.42);border-color:rgba(255,255,255,.62);
  color:#fff;backdrop-filter:blur(8px);box-shadow:0 3px 14px rgba(0,0,0,.22)}
@media (hover:hover){ .site-header .nav .btn:hover{background:rgba(20,20,16,.56);
  box-shadow:0 12px 26px rgba(0,0,0,.32)} }
.site-header.is-scrolled .nav .btn{background-color:var(--accent);background-image:var(--btn-lit);
  border-color:var(--accent);color:var(--accent-on);backdrop-filter:none;
  box-shadow:0 2px 10px rgba(78,88,48,.18), 0 1px 2px rgba(78,88,48,.16)}
/* The header's own colourway rules are (0,4,0) and so they were beating
   `.btn:hover` (0,2,0) outright: the site's most persistent CTA had a lift but no
   colour change on hover, on every page, in every solid state. Restored at (0,5,0)
   so it cannot be shadowed again by a colourway rule added later. */
@media (hover:hover){
  .site-header.is-scrolled .nav .btn:hover,
  .site-header.is-solid .nav .btn:hover,
  .site-header.is-open .nav .btn:hover{
    background-color:#3E4726;
    box-shadow:0 14px 28px rgba(78,88,48,.30), 0 3px 6px rgba(78,88,48,.22)
  }
}
.site-header .navtoggle{border-color:rgba(255,255,255,.6)}
.site-header .navtoggle span{background:#fff}
.site-header.is-scrolled .navtoggle{border-color:var(--line)}
.site-header.is-scrolled .navtoggle span{background:var(--ink-strong)}

/* §7a: the moment the drawer opens the header goes FULLY opaque regardless of
   scroll position, so the open menu looks identical at the top and mid-page. */
.site-header.is-open{background:var(--surface);border-bottom-color:var(--line-soft);
  box-shadow:0 2px 18px rgba(51,51,47,.09)}
.site-header.is-open .brand__dark{opacity:1}
.site-header.is-open .brand__rev{opacity:0}
.site-header.is-open .navtoggle{border-color:var(--line)}
.site-header.is-open .navtoggle span{background:var(--ink-strong)}
.site-header.is-open .nav a:not(.btn){color:var(--ink-strong);text-shadow:none}
.site-header.is-open .nav .btn{background-color:var(--accent);background-image:var(--btn-lit);
  border-color:var(--accent);color:var(--accent-on);backdrop-filter:none;
  box-shadow:0 2px 10px rgba(78,88,48,.18), 0 1px 2px rgba(78,88,48,.16)}
.nav{display:flex;align-items:center;gap:var(--s3)}
.nav a:not(.btn){
  text-decoration:none;color:var(--ink-strong);font-size:.95rem;font-weight:500;
  padding:6px 0;border-bottom:1px solid transparent;
  transition:color var(--dur-hover) var(--ease-hover),border-color var(--dur-hover) var(--ease-hover)
}
@media (hover:hover){ .nav a:not(.btn):hover{color:var(--accent);border-bottom-color:var(--accent)} }
.nav .btn{padding:12px 20px;font-size:.9rem}
.navtoggle{display:none;background:none;border:1px solid var(--line);border-radius:var(--radius);
  padding:10px 12px;cursor:pointer;touch-action:manipulation;
  transition:border-color var(--dur-hover) var(--ease-hover),
             background var(--dur-hover) var(--ease-hover)}
@media (hover:hover){
  .site-header.is-scrolled .navtoggle:hover,.site-header.is-solid .navtoggle:hover{
    border-color:var(--accent);background:var(--surface)}
  .site-header:not(.is-scrolled):not(.is-solid):not(.is-open) .navtoggle:hover{
    border-color:#fff;background:rgba(255,255,255,.14)}
}
.navtoggle span{display:block;width:20px;height:2px;background:var(--ink-strong);margin:4px 0;
  transition:transform var(--dur-hover) var(--ease-hover),opacity var(--dur-hover) var(--ease-hover)}
.site-header.is-open .navtoggle span:nth-child(1){transform:translateY(6px) rotate(45deg)}
.site-header.is-open .navtoggle span:nth-child(2){opacity:0}
.site-header.is-open .navtoggle span:nth-child(3){transform:translateY(-6px) rotate(-45deg)}

/* ===== hero - P-011 full-bleed, Ken Burns crossfade carousel ===== */
.hero{
  position:relative;isolation:isolate;overflow:hidden;
  min-height:100vh;min-height:100svh;
  display:flex;align-items:flex-end
}
.hero__bg{position:absolute;inset:0;z-index:-2;overflow:hidden}
.hero__slide{position:absolute;inset:0;opacity:0;transition:opacity 2.4s var(--ease-settle)}
.hero__slide:first-child{opacity:1}                       /* no-JS: first frame shows */
.hero__slide.is-active{opacity:1}
.hero__slide img{
  width:100%;height:100%;object-fit:cover;object-position:center 58%;
  /* MUST match lc-kenburns-in's `from`. Any other value snaps the moment js-motion
     lands, which reads as a second small hop on top of the stylesheet one. */
  transform:scale(1.04)
}
/* Promote ONLY the active slide. will-change on all four kept 2880px layers
   permanently composited, which is part of what made scrolling expensive. */
.hero__slide.is-active img{will-change:transform}
/* Ken Burns: a slow drift on the ACTIVE slide only, so idle slides cost nothing.
   ALTERNATING DIRECTION, and the alternation is the whole point. Every slide used
   to run the same 1.04 -> 1.13 zoom-in, so at each crossfade a 1.13 image dissolved
   into a 1.04 one. For 1.8s both are on screen at visibly different scales, and the
   eye reads that discontinuity as a pulse or a bounce rather than as a drift.
   Odd slides zoom IN, even slides zoom OUT from where the previous one ended, so the
   two scales MATCH at every handover: 1.13 hands to 1.13, 1.04 hands to 1.04. The
   direction reverses but the size never jumps. With 4 slides the cycle closes
   correctly too - slide 4 ends at 1.04 and slide 1 starts there. */
@keyframes lc-kenburns-in{
  from{transform:scale(1.04) translate3d(0,0,0)}
  to  {transform:scale(1.075) translate3d(0,-0.9%,0)}
}
@keyframes lc-kenburns-out{
  from{transform:scale(1.075) translate3d(0,-0.9%,0)}
  to  {transform:scale(1.04) translate3d(0,0,0)}
}
/* Duration is driven from motion.js's HOLD_MS via --kb-dur, and that coupling is the
   whole fix. The previous version ran 17s against a 6.5s hold, so a slide only ever
   played the first 38% of its drift and NEVER reached its end value. The alternation
   was built on "slide 1 ends at 1.13 where slide 2 starts" - which never happened.
   Measured at the handover: outgoing sat at 1.0741 while the incoming began at 1.13,
   a 0.056 jump. That is the rough crossfade.
   With duration == hold, each slide completes exactly as the next arrives, so the two
   are at the SAME scale for the whole dissolve. No competing zoom, just a fade.

   DIRECTION IS DRIVEN BY THE HANDOVER, NOT BY DOM INDEX, and that is the second
   half of the fix. Alternating on :nth-child(odd/even) cannot close a cycle over an
   ODD number of slides: with three slides the order runs in, out, in, so handovers
   1>2 and 2>3 chain correctly but the wrap 3>1 is odd to odd. The outgoing slide
   rested at 1.07497 while the incoming restarted at 1.04, a measured 0.0350 scale
   jump and 8.70px shift once per cycle - the same pulse the duration fix was meant
   to remove, surviving in the one handover nobody measured. motion.js now flips a
   .kb-out class on every advance, so the incoming slide always begins where the
   outgoing one ended, for any slide count. */
html.js-motion .hero__slide.is-active img{
  animation:lc-kenburns-in var(--kb-dur,6.5s) linear both}
html.js-motion .hero__slide.is-active.kb-out img{
  animation:lc-kenburns-out var(--kb-dur,6.5s) linear both}

/* P-002 scrims. Lightened at the operator's request, then RE-MEASURED to WCAG on
   every pixel under the text. The copy column still needs the horizontal ramp
   because the photos' brightest region (windows) sits behind the headline. */
.hero::after{
  content:"";position:absolute;inset:0;z-index:-1;pointer-events:none;
  background:
    /* item 3.3 - the H1 halo. A soft pool of shade centred on the headline block,
       so the type sits in its own pocket of light instead of on a flat band of
       shade. On a photograph the halo is DARK, because a light halo behind white
       type would cost the contrast the scrim exists to buy; on the interior
       pages' flat dark band it is light, for the same reason inverted. Same move,
       ground decides the direction.
       This is not additive dressing: the flat 90deg ramp is pulled back from .86
       to .78 and the halo puts the difference back exactly where the words are.
       Measured at the headline the composite goes .854 -> .879 (more contrast
       under the type) and at the upper right .455 -> .429 (more photograph where
       there is nothing to protect). Contrast is a local property; solve it
       locally - the same lesson the P-009 plate scrim already carries. */
    radial-gradient(78% 62% at 20% 76%, rgba(8,8,5,.40) 0%,
                                        rgba(8,8,5,.16) 52%,
                                        rgba(8,8,5,0)   80%),
    linear-gradient(90deg, rgba(16,16,12,.78) 0%, rgba(16,16,12,.64) 40%,
                           rgba(16,16,12,.30) 70%, rgba(16,16,12,.04) 100%),
    linear-gradient(180deg,rgba(16,16,12,.44) 0%, rgba(16,16,12,.18) 26%,
                           rgba(16,16,12,.22) 66%, rgba(16,16,12,.50) 100%)
}
/* flex-end anchors this block to the bottom, so the TOP padding is inert and the
   bottom value alone decides how high the content sits. Raised s5 -> s6 + 18px to
   lift the block off the fold line and put real air under the CTA row. */
.hero__in{width:100%;padding:calc(var(--s6) + 40px) 0 calc(var(--s6) + 18px)}
/* inline-block so the box hugs the glyphs. As a block it spanned the whole
   container, which both mis-measures contrast and is simply wrong. */
.hero .eyebrow{display:inline-block;color:#FFFFFF;text-shadow:0 2px 10px rgba(0,0,0,.62);opacity:1}
.hero h1{color:#FFFFFF;text-shadow:0 2px 20px rgba(0,0,0,.55);max-width:15ch;margin-top:var(--s2)}

/* ===== the H1 accent word - ONE treatment, every page.
   Cinzel has no italic and font-synthesis is off, so the accent approved at the
   Step 1 homepage gate carries its emphasis in COLOUR rather than slope. Every
   other page inherits that same rule. This is deliberately NOT a second type
   system: the only sanctioned Lora fallback is .h-long, for a heading that must
   run long. Light green on the dark surfaces, olive on the light ones. */
.hero h1 em,.phero h1 em{font-style:normal;color:#C5DA96}
.util__head h1 em{font-style:normal;color:var(--accent)}
.hero .lede{display:block;width:fit-content;color:rgba(255,255,255,.95);
  text-shadow:0 1px 12px rgba(0,0,0,.6);max-width:46ch;margin-top:var(--s2)}

/* ===== the hero's proof element - a glass caption card, lower right.
   §25b: this business has no usable review corpus (the 164 Google reviews belong
   to a legally separate company), so there is no testimonial and no rating to put
   in the hero. What IS substantiated is the photograph itself, so the card names
   the real project and the real staging type on the slide behind it. The dark
   glass keeps white type legible over any of the four slides rather than relying
   on the text-shadow alone. */
.hero__credit{
  position:absolute;right:var(--s3);bottom:var(--s3);z-index:2;text-align:left;
  font-size:.76rem;letter-spacing:.13em;text-transform:uppercase;line-height:1.6;
  color:rgba(255,255,255,.9);text-shadow:0 1px 10px rgba(0,0,0,.66);
  transition:opacity .7s var(--ease-settle);pointer-events:none
}
.glass-card{
  background:rgba(12,12,9,.42);
  -webkit-backdrop-filter:blur(14px) saturate(1.08);
  backdrop-filter:blur(14px) saturate(1.08);
  border:1px solid rgba(255,255,255,.24);border-radius:var(--radius);
  padding:13px 18px;box-shadow:0 18px 44px rgba(0,0,0,.32)
}
.hero__credit b{display:block;font-family:var(--font-h);font-weight:600;color:#fff;
  font-size:.85rem;letter-spacing:.09em;margin-bottom:2px}
.hero__credit em{font-style:normal;color:#C5DA96}
@media (max-width:900px){
  /* The halo travels to the phone layout, where the copy column is full width and
     sits lower. Same arithmetic: the flat ramp comes down, the halo puts it back
     under the words, so more of the room shows above the type and there is MORE
     shade under it than before, not less.
     The halo has to be tall enough to cover the whole copy block, eyebrow
     included. A first attempt sized it 120% x 52% and it reached the headline but
     not the line above it: measured on rendered pixels the eyebrow came back at
     4.09:1 against a 4.5 floor, a regression this wave caused and the wave-3
     checker caught. Widened to 130% x 80% and re-centred, and the mid stop of the
     linear ramp brought back up. Do not shrink the radii without re-measuring
     `.hero__in .eyebrow` at 430 - the eyebrow is the top edge of the pool and it
     is the sample that goes first. */
  .hero::after{background:
    radial-gradient(130% 80% at 24% 72%, rgba(8,8,5,.36) 0%,
                                         rgba(8,8,5,.20) 50%,
                                         rgba(8,8,5,0)   90%),
    linear-gradient(180deg,rgba(16,16,12,.60) 0%,rgba(16,16,12,.55) 24%,
                           rgba(16,16,12,.62) 48%,rgba(16,16,12,.86) 100%)}
  /* The top two stops are deliberately left at their pre-wave values. Lightening
     them did make more of the photograph visible, and on a phone the extra
     photograph is the WEAK part of this crop - a curtain valance, a window wall
     and a hanging cord above the staged table, which is the mobile-crop problem
     wave 1 already measured and handed to wave 4. Trading contrast for more of
     the part of the frame nobody wants to see is not a trade. So the halo adds
     shade under the type and the frame above it is unchanged. */
  /* Measured 430x932 before this: 367px empty above the eyebrow, 84px below the CTA.
     The hero is align-items:flex-end, so the BOTTOM value alone decides where the block
     sits, and it was parking the content on the floor with 39% of the hero empty above
     it. Raised so the block sits just below centre with real air under the CTA row,
     which is what a hero is supposed to look like on a phone. */
  .hero__in{padding:calc(var(--s4) + 40px) 0 calc(var(--s6) + 34px)}
  /* stays a CARD on a phone rather than stretching edge to edge, and sits left
     so it reads as part of the same column as the headline and the CTAs */
  /* On a phone this is a caption, not a component. It was a 62px glass block sitting
     directly under the CTA row and reading as a second, competing element. Now one
     quiet line: no card, no blur, smaller, and the two values on a single row. */
  .hero__credit{left:var(--s3);right:auto;bottom:14px;font-size:.62rem;
    max-width:calc(100% - (var(--s3) * 2));
    background:none;-webkit-backdrop-filter:none;backdrop-filter:none;
    border:0;padding:0;color:rgba(255,255,255,.72);letter-spacing:.16em}
  .hero__credit b{display:inline;font-size:.62rem;font-weight:600;color:rgba(255,255,255,.9)}
  .hero__credit em{display:inline}
  .hero__credit b::after{content:"  ·  ";color:rgba(255,255,255,.45);font-weight:400}
  .hero__credit b{font-size:.78rem}
  .glass-card{padding:10px 14px}

  /* ===== item 4.7 / the mobile hero crop. Carried from wave 1 to wave 2 to
     wave 3 as "the fix is per-slide crops or portrait derivatives", and it is a
     wave-4 art-direction problem rather than a wave-3 motion one.

     THE MEASUREMENT FIRST, because the earlier waves had the cause slightly
     wrong. The note above says `object-position` has no room to move. That is
     true only on the Y axis, and Y is not where the crop is happening. At
     430x932 a 1.93:1 photograph covers by HEIGHT: the browser scales it to
     932px tall, which makes it 1799px wide, and then shows the middle 430 of
     those 1799. So 76% of every hero frame is cropped away HORIZONTALLY and the
     `center` in `center 58%` is the half of the declaration that was doing the
     damage. There was room the whole time; it was on the other axis.

     What the phone was being shown, per slide, before this:
       slide 1  the middle of the dining room = a black valance, a window wall
                and a hanging cord. The staged table sits low and behind the copy.
       slide 2  a stone chimney breast and a stair rail, chandelier cropped off.
       slide 3  the sink and the range, with both globe pendants outside the crop.

     RE-CUT 2026-08-01, because the first set of values framed furniture edges
     instead of subjects. The operator's words were that the phone was "strangely
     focused on chairs or stair rails", and it was right: slide 1 sat on the BACKS
     of two tufted chairs with a blank wall above, and slide 2 cut the chandelier
     in half and filled the lower frame with the stair balustrade.

     The arithmetic that matters, because eyeballing a percentage does not work
     here. With object-fit:cover the visible band is NOT centred on the value you
     write. For a container fraction f = container/scaled-width:

         visible centre = p(1 - f) + f/2

     At 430x932 slide 1 has f = 0.239, so the old p = 0.27 put the centre at 0.325
     of the photograph, which is the chair backs. The dining table and its floral
     centrepiece live at about 0.59. Solving for that gives p = 0.62. Miss this
     relationship and every value reads about 15 points too far left, which is
     exactly the error the first pass made on all three slides.

     Each value below was then CONFIRMED by rendering the candidates side by side
     under the real scrim, with the copy zone marked, so a subject hidden behind
     the headline could not be mistaken for a subject in frame. Not reasoned from
     the file, and not trusted from the formula alone.

     Tablet shares these values and does not need its own: at 820x1180 slide 1's
     f becomes 0.360, which moves the visible centre only from 0.591 to 0.577.

     Do NOT collapse these into one shared value. The whole point is that three
     different photographs have their subject in three different places. */
  .hero__slide:nth-child(1) img{object-position:62% center}   /* hero-1: the floral centrepiece and place settings, NOT the chair backs */
  .hero__slide:nth-child(2) img{object-position:43% center}   /* hero-4: whole chandelier anchoring the left, stacked stone column centred */
  .hero__slide:nth-child(3) img{object-position:46% center}   /* hero-2: globe pendant over the island, cabinetry and range behind it */
}

/* ===== proof strip.
   Display-face numerals, an accent rule above each, and a sentence label under
   it. The rule is the same hairline vocabulary the section labels use, turned
   vertical-stack instead of horizontal, so the strip reads as part of the same
   system rather than as a stat widget bolted on. */
.proof{border-bottom:1px solid var(--line-soft);background:var(--surface)}
.proof__g{display:grid;grid-template-columns:repeat(4,1fr);gap:var(--s3);padding:var(--s4) 0}
.proof__g > *{position:relative;padding-top:20px}
.proof__g > *::before{
  content:"";position:absolute;left:0;top:0;width:30px;height:2px;background:var(--accent)
}
.proof__n{font-family:var(--font-h);font-size:1.62rem;font-weight:600;color:var(--accent);
  letter-spacing:.02em;line-height:1.1}
.proof__l{font-size:.9rem;color:var(--ink);margin-top:9px;line-height:1.55;max-width:26ch}

/* ===== sections + hairline labels (Halston/Verity structural move) ===== */
.sec{padding:var(--s6) 0}
.sec--tight{padding:var(--s5) 0}
.sec--surface{background:var(--surface)}
/* WAVE 9: `flex-wrap` + the `i` min-width, and they close a real cut-off.
   The label carried `white-space:nowrap` so the hairline would always sit on the
   same line as it. On the two longest labels on the site that ran the type
   straight off the right edge of the screen: "Downtown Edwardsville, occupied,
   2020" lost 17.9px at 375 and 72.9px at 320, and "Superior Homes, Inc. and
   Brookshire" lost 41.5px at 320. Nothing scrolled, because `body` has
   `overflow-x: clip` - the words were simply painted past the viewport and gone.
   Eight waves missed it because every one of them measured 430, the §15 mobile
   breakpoint, where both labels fit. 375 is an iPhone SE and a 13 mini.
   The fix is at the component, not on the two labels, so the next long one is
   safe by construction: the row wraps, the hairline claims its own full-width
   row below the label when there is not 72px left beside it, and the label may
   break to a second line as a last resort instead of being cut. Desktop is
   unchanged - `i` is `flex:1` with a 0 basis, so it never contributes to the
   line's initial size and the label keeps its max-content width. */
.rule-label{display:flex;align-items:center;gap:var(--s3);margin-bottom:var(--s4);
  flex-wrap:wrap;row-gap:14px}
/* WAVE 6 (SEO/AEO): the section label is now a real <h2> almost everywhere it
   appears. It was a bare <span>, which meant thirteen content sections on this
   site opened with no heading at all - the homepage skipped h1 -> h3 outright
   (the three service cards), and §3 asks that a content section lead with a
   heading rather than body text. The one exception is /services "Past the
   listing", which sits directly above <h2>Design and styling</h2>: there the
   label really is an eyebrow for the heading under it, and promoting it would
   print two h2s in a row. So BOTH element forms are styled, identically.
   The three h2-only resets below are the point of the rule: the global
   h1,h2,h3 block would otherwise inject Cinzel, --h2 sizing and line-height
   1.12 into a .72rem tracked label. font-size / letter-spacing / colour /
   weight are already overridden by the shared declarations. */
.rule-label span,.rule-label h2{font-size:.72rem;letter-spacing:.22em;text-transform:uppercase;font-weight:600;
  color:var(--accent);white-space:normal;max-width:100%;text-wrap:balance}
.rule-label h2{font-family:var(--font-b);line-height:1.72;margin:0}
.rule-label i{flex:1 1 0;min-width:72px;height:1px;background:var(--line);transform-origin:left;display:block}
/* §7b-3: standalone section headers are centred + width-constrained */
.sec-head{max-width:820px;margin:0 auto var(--s4);text-align:center}
.sec-head h2{margin-top:12px}
.sec-head .lede{margin:var(--s2) auto 0;max-width:60ch}
/* Optical centring for tracked caps. `letter-spacing` also adds its space AFTER
   the final letter, so a centred label sits half a tracking unit left of true
   centre. Cancelling that trailing space with a negative right margin shrinks
   the box by one unit, and centring the smaller box puts the glyph run back on
   the axis. The correction is half of .22em - small, and the reason a set label
   looks centred where a typed one looks nearly centred. */
.sec-head .eyebrow{margin-right:-.22em}

/* ===== service cards - open, photo-led. No card box, no border, no shadow:
   the photograph IS the card and the type sits on the page beneath it. ===== */
.three{display:grid;grid-template-columns:repeat(3,1fr);gap:var(--s4)}
.card{display:flex;flex-direction:column;text-decoration:none;color:inherit}
/* item 3.1 - the lift belongs to the PHOTOGRAPH, not to a card box.
   The locked direction is explicit that these cards have no box, no border and no
   shadow: the photograph is the card and the type sits on the page beneath it. So
   the -6px lift and the shadow are applied to .card__fig, which is a physical
   object on the page, and the caption stays anchored where it was. That satisfies
   the depth requirement without inventing the card chrome the direction rejected.
   The resting shadow is nearly invisible (.06 alpha) and exists so the hover
   shadow grows out of something rather than out of nothing. */
.card__fig{
  position:relative;overflow:hidden;border-radius:var(--radius);margin:0;
  box-shadow:var(--frame-rest);
  transition:translate var(--dur-settle) var(--ease-settle),
             box-shadow var(--dur-settle) var(--ease-settle)
}
/* item 4.6 - the inner ring. It has to be a pseudo-element, not an inset
   box-shadow: an inset shadow paints above the BACKGROUND and below the
   CONTENT, so on a box whose content is the photograph it is drawn underneath
   the picture and is invisible. Same trap on the <img> elements, which is why
   those two use outline-offset:-1px instead. */
.card__fig::after,.tile::after,.feat::after,.layer-panel__ph::after{
  content:"";position:absolute;inset:0;pointer-events:none;
  border-radius:inherit;box-shadow:var(--frame-ring)
}
.layer-panel__ph::after{box-shadow:var(--frame-ring-lt)}
.card__fig img{
  width:100%;height:auto;aspect-ratio:4/5;object-fit:cover;
  transition:transform .7s var(--ease-settle),filter var(--dur-settle) var(--ease-settle)
}
@media (hover:hover){
  .card:hover .card__fig{translate:0 -6px;box-shadow:0 28px 52px rgba(51,51,47,.20)}
  .card:hover .card__fig img{transform:scale(1.045)}
}
/* Keyboard focus gets the SAME depth the pointer gets, and keeps the ring on top
   of it, so the focus state is distinct from rest AND from hover rather than
   being a thinner version of the pointer experience. Declared outside the hover
   query on purpose: a keyboard user on a touch device is still a keyboard user. */
.card:focus-visible .card__fig{translate:0 -6px;box-shadow:0 28px 52px rgba(51,51,47,.20)}
.card__b{padding:var(--s2) 2px 0}
.card h3{margin-bottom:10px;font-size:1.28rem;letter-spacing:.04em}
.card p{color:var(--ink);font-size:1rem;max-width:38ch}
.card__more{
  display:inline-flex;align-items:center;gap:8px;margin-top:var(--s2);
  font-size:.78rem;letter-spacing:.16em;text-transform:uppercase;font-weight:700;
  color:var(--accent);text-decoration:none
}
.card__more{transition:color var(--dur-hover) var(--ease-hover)}
.card__more::after{content:"";width:26px;height:1px;background:var(--accent);
  transition:width var(--dur-hover) var(--ease-hover),background var(--dur-hover) var(--ease-hover)}
@media (hover:hover){
  .card:hover .card__more::after{width:42px}
  /* On /services the same component is a STANDALONE <a>, not a <span> inside an
     <a class="card">, so the ancestor rule above never fired for it: three of the
     six instances on the site had no hover at all. The element selector catches
     exactly the standalone case and leaves the in-card spans to the rule above. */
  a.card__more:hover{color:#3E4726}
  a.card__more:hover::after{width:42px}
}

/* ===== editorial split ===== */
.split{display:grid;grid-template-columns:1fr 1fr;gap:var(--s5);align-items:center}
/* item 4.6 on a bare <img>. A pseudo-element is not available on a replaced
   element and an inset box-shadow paints under the picture, so the ring is an
   outline pulled 1px inside the frame. Neither of these two images is focusable,
   so there is no collision with the :focus-visible ring. The editorial split and
   the before/after pair were the only two photographic slots on the site with no
   depth at all - they sat flat on the page while every card around them had a
   mount. */
.split__img{width:100%;height:auto;aspect-ratio:5/4;object-fit:cover;border-radius:var(--radius);
  outline:1px solid color-mix(in srgb, var(--ink) 15%, transparent);outline-offset:-1px;
  box-shadow:var(--frame-rest)}
.split p + p{margin-top:var(--s2)}

/* ===== portfolio rail - P-012 spec panel ===== */
.rail{display:grid;grid-template-columns:repeat(3,1fr);gap:var(--s2)}
/* NOTE for anyone adding a two-tile rail: there is nothing to write. A
   `.rail--pair` modifier was added during wave 4 and then deleted, because
   measuring it showed the site ALREADY centres a two-child rail - wave 2 built
   `.rail:has(> :nth-child(2):last-child)` further down this file at (0,2,0),
   which outranks any plain `.rail--x` class anyway. Measured on the new
   /services/occupied-staging pair: 1440 -> `440px 440px` centred, 820 ->
   `368px 368px` centred, 430 -> one column. 440 CSS px on a 960px source is
   1.09x at 2x density, inside the ~530px ceiling the resolution contract sets.
   The modifier would have been dead CSS that LOOKED load-bearing. */
/* The portfolio tile carries the same depth language as the service card, one
   step quieter: a 4px lift rather than 6px, because the frames are smaller and
   sit in a tight 20px grid, and because the tile's headline move is the P-012
   panel - the lift is there to seat it, not to compete with it.

   THE LIFT USES `translate`, NOT `transform`, AND THAT IS LOAD-BEARING.
   `.tile` and `.feat` both carry `data-reveal`, and P-008's visible rule
   `html.js-motion [data-reveal].is-visible{transform:none}` is (0,3,1) - it
   outranks `.tile:hover` at (0,2,0) and owns `transform` on these elements
   permanently, because the reveal is one-shot and the class never comes off. The
   first version of this wave wrote the lift as `transform:translateY(-4px)` and
   it was silently discarded on every tile and on the /work feature photo: the
   shadow still deepened, so a screenshot looked like a lift and a hover probe
   said "something changed", and only a computed-style read of `transform` showed
   it was `none` in all three states. `translate` is an independent property that
   no reveal rule declares, so there is no cascade to lose, and it composes with
   the reveal's transform rather than fighting it. Its TRANSITION still has to be
   restored after the reveal - see the `.is-settled` rule near P-008. */
.tile{position:relative;display:block;overflow:hidden;border-radius:var(--radius);text-decoration:none;
  box-shadow:0 1px 8px rgba(51,51,47,.05);
  transition:translate var(--dur-settle) var(--ease-settle),
             box-shadow var(--dur-settle) var(--ease-settle)}
.tile img{width:100%;height:auto;aspect-ratio:4/3;object-fit:cover;
  transition:transform var(--dur-settle) var(--ease-settle)}
@media (hover:hover){
  .tile:hover{translate:0 -4px;box-shadow:0 22px 42px rgba(51,51,47,.18)}
  .tile:hover img{transform:scale(1.04)}
}
.tile:focus-visible{translate:0 -4px;box-shadow:0 22px 42px rgba(51,51,47,.18)}
.tile__p{
  position:absolute;left:12px;right:12px;bottom:12px;background:var(--surface);
  border-radius:var(--radius);padding:12px 14px;
  opacity:0;transform:translateY(10px);
  transition:opacity .4s var(--ease-settle),transform .4s var(--ease-settle)
}
.tile:hover .tile__p,.tile:focus-within .tile__p,
.feat:hover .tile__p,.feat:focus-within .tile__p{opacity:1;transform:none}
/* P-012 pass criterion: never hover-gated. Touch shows it statically.
   But "statically" is where the pattern gets expensive: on desktop the panel is a
   transient reveal and a full-bleed white bar across the photo reads as drama,
   while on a phone it is PERMANENT and the same bar was covering close to a third
   of every photograph on a site whose entire proof burden is the photography.
   So on touch it sizes to its own content (`right:auto`) and reads as a label
   plate rather than a banner. Same panel, same surface colour, same two lines,
   same information, roughly a third more room visible behind it. Desktop is
   untouched. */
/* ===== item 4.9, and the decision wave 3 explicitly handed to this wave.
   Wave 3 shrank the touch panel from a full-bleed bar to a content-width plate
   because it was covering a third of every photograph. That was the right
   emergency fix and the wrong final answer, because the question it left open
   was "does the panel belong over the photograph at all on a phone?" and the
   answer on a site whose entire proof burden is the photography (§25b leaves no
   reviews to carry it) is no. A plate over a picture is a REVEAL - it earns the
   intrusion by being transient. Drawn permanently it stops being a reveal and
   becomes a sticker.

   So on touch the panel leaves the photograph and becomes what it actually is:
   a caption. Same two lines, same words, same colours, nothing hidden, the whole
   picture visible. P-012's pass criterion is that the information is never
   hover-gated, and a caption satisfies it more completely than an overlay does.
   Desktop is untouched: there the panel is still the transient reveal, over the
   image, which is the pattern as specified.

   Three mechanical consequences, each load-bearing:
   - `.tile` goes overflow:visible, so the caption sits outside the frame. The
     border-radius therefore has to move onto the image, and
   - the frame shadow moves onto the image too, or the resting shadow would trace
     the caption as well as the picture, and
   - `.tile::after` (the 4.6 ring) is switched off and the ring is re-drawn as an
     outline on the image, because inset:0 on the anchor now includes the caption.
     Every tile image on the site is 4/3, so the swap is exact. */
@media (hover:none){
  .tile{overflow:visible;box-shadow:none}
  .tile::after{content:none}
  .tile img{border-radius:var(--radius);box-shadow:var(--frame-rest);
    outline:1px solid color-mix(in srgb, var(--ink) 15%, transparent);outline-offset:-1px}
  .tile__p{
    position:static;display:block;opacity:1;transform:none;right:auto;max-width:none;
    background:transparent;border-radius:0;padding:11px 1px 0
  }
  /* `display:block` is the load-bearing declaration here and it cost a capture.
     `.tile__p` is a <span> with no display of its own; `position:absolute`
     BLOCKIFIES it, so the desktop panel has always been a block by accident of
     the abspos rule. Setting `position:static` un-blockifies it back to inline,
     and an inline box whose only children are `display:block` fragments into
     anonymous boxes: the element still reported a bounding rect and the tile
     still reserved 97px for it, so a computed-style probe said "the caption is
     there" while the rendered pixels showed a blank strip. The screenshot is
     what caught it. Same lesson as wave 3's discarded `transform` - assert the
     rendered result, not the property you set. */
  /* the /work feature photo carries the same caption treatment as the tiles */
  .feat{overflow:visible;box-shadow:none}
  .feat::after{content:none}
  .feat img{border-radius:var(--radius);box-shadow:var(--frame-rest);
    outline:1px solid color-mix(in srgb, var(--ink) 15%, transparent);outline-offset:-1px}
  /* NOTE: no `.feat .tile__p` override here. An earlier draft had one, and at
     (0,2,0) it outranked the `.tile__p` rule above and dropped `display:block` -
     which would have re-created the blank-caption bug on the one photograph the
     page most needs labelled. The single `.tile__p` rule covers both. */
}
/* display:block is load-bearing, not tidiness. Both of these were `inline`, and
   the markup has no whitespace between the two spans, so the panel rendered
   "Brookshire New BuildVACANT STAGING" as one run and then broke the type label
   across two lines mid-phrase. On desktop the panel only exists on hover so no
   screenshot ever caught it; on touch it is drawn statically (P-012's own pass
   criterion) and it was visible on every mobile tile on the site. */
.tile__n{display:block;font-family:var(--font-h);font-weight:600;font-size:.98rem;
  line-height:1.3;color:var(--ink-strong)}
.tile__m{display:block;font-size:.72rem;letter-spacing:.16em;text-transform:uppercase;
  color:var(--accent);font-weight:700;margin-top:5px;line-height:1.4}

/* ===== FAQ - §7b centred column, left-aligned reading text.
   Native <details> accordions: collapsed by default, keyboard-accessible, and
   the answer text stays in the DOM for AEO even while closed. ===== */
.faq{max-width:790px;margin:0 auto}
.faq__i{border-top:1px solid var(--line-soft)}
.faq__i:last-child{border-bottom:1px solid var(--line-soft)}
/* The question is set in LORA, not Cinzel. design.md's type note names FAQ
   questions as the exact case for the long-heading fallback: Cinzel has no
   lowercase, so a two-line question rendered in it becomes two lines of caps and
   reads as shouting rather than asking. This is the same `.h-long` rule the AEO
   question headings already use, so a page's H2 question and its accordion
   questions now share one voice. Short display headings keep Cinzel. */
.faq__q{
  display:flex;align-items:flex-start;justify-content:space-between;gap:var(--s3);
  padding:var(--s3) 0;cursor:pointer;list-style:none;
  font-family:var(--font-b);font-size:1.12rem;font-weight:600;color:var(--ink-strong);
  letter-spacing:0;line-height:1.5;text-wrap:balance;touch-action:manipulation;
  transition:color var(--dur-hover) var(--ease-hover),
             background-color var(--dur-hover) var(--ease-hover)
}
.faq__q::-webkit-details-marker{display:none}
/* The whole 790px row is the hit target, and until this wave only the words and
   the 30px ring responded to it - so nine tenths of a clickable surface gave the
   pointer nothing back. A flat 5% accent wash across the row says "all of this is
   the button". Flat, not a gradient, specifically so it can transition: a
   background-image does not interpolate and would have snapped. */
@media (hover:hover){ .faq__q:hover{color:var(--accent);background-color:rgba(78,88,48,.05)} }
/* No `.faq__q:focus-visible` override lives here on purpose. The universal ring
   at the top of this file already covers `summary:focus-visible`; a second copy
   of the same two values is how an offset drifts apart from the system it is
   supposed to belong to. */
/* the toggle: a hairline accent ring holding a +, crossing to a - on open.
   CSS-drawn from brand tokens, not an icon set (there is no icon font here). */
.faq__ico{
  flex:none;width:30px;height:30px;position:relative;margin-top:2px;border-radius:50%;
  border:1px solid color-mix(in srgb, var(--accent) 34%, transparent);
  transition:transform var(--dur-settle) var(--ease-settle),
             border-color var(--dur-hover) var(--ease-hover),
             background var(--dur-hover) var(--ease-hover)
}
.faq__ico::before,.faq__ico::after{
  content:"";position:absolute;left:50%;top:50%;background:var(--accent);
  transform:translate(-50%,-50%);transition:opacity var(--dur-settle) var(--ease-settle)
}
.faq__ico::before{width:12px;height:1.5px}
.faq__ico::after{width:1.5px;height:12px}
@media (hover:hover){ .faq__q:hover .faq__ico{border-color:var(--accent);background:#EFF3E4} }
.faq__i[open] .faq__ico::after{opacity:0}
.faq__i[open] .faq__ico{transform:rotate(180deg);border-color:var(--accent);background:#EFF3E4}
.faq__a{color:var(--ink);font-size:1rem;padding:0 54px var(--s3) 0;margin:0;max-width:70ch}
html.js-motion .faq__i[open] .faq__a{animation:lc-faq-open .45s var(--ease-settle) both}
@keyframes lc-faq-open{from{opacity:0;transform:translateY(-6px)}to{opacity:1;transform:none}}

/* ===== WAVE 7 - the sideways link between services.
   Carried as an open item since wave 5's category-leader prompt and finally
   built here, because it is conversion architecture rather than decoration.

   The problem it solves: every service page offered a reader exactly two moves
   at the foot, book or leave. A seller who lands on /services/occupied-staging
   and then realises the house will be empty by listing day had no path except
   back up to the nav. The three staging services are chosen by ONE fact about
   the reader's situation - do you still live there - so the sideways link asks
   that question in the reader's own words and answers it with a destination.

   Structurally it is two hairline cells, no card, no shadow, no fill, which is
   the same open treatment the homepage service cards use. The affordance is the
   `.card__more` rule that grows on hover, reused verbatim rather than redrawn. */
.next-svc{
  display:grid;grid-template-columns:1fr 1fr;
  border-top:1px solid var(--line-soft);border-bottom:1px solid var(--line-soft)
}
.next-svc a{
  display:block;padding:var(--s4) 0;text-decoration:none;color:inherit;
  transition:background-color var(--dur-hover) var(--ease-hover)
}
.next-svc a + a{border-left:1px solid var(--line-soft);padding-left:var(--s4)}
.next-svc a:first-child{padding-right:var(--s4)}
.next-svc__k{
  display:block;font-size:.72rem;letter-spacing:.18em;text-transform:uppercase;
  font-weight:700;color:var(--ink-soft)
}
.next-svc__l{
  display:block;margin-top:10px;font-family:var(--font-h);font-size:1.22rem;
  font-weight:600;letter-spacing:.02em;line-height:1.25;color:var(--ink-strong)
}
.next-svc__m{
  display:inline-flex;align-items:center;gap:8px;margin-top:14px;
  font-size:.78rem;letter-spacing:.16em;text-transform:uppercase;font-weight:700;
  color:var(--accent);transition:color var(--dur-hover) var(--ease-hover)
}
.next-svc__m::after{content:"";width:26px;height:1px;background:var(--accent);
  transition:width var(--dur-hover) var(--ease-hover)}
@media (hover:hover){
  .next-svc a:hover .next-svc__m{color:#3E4726}
  .next-svc a:hover .next-svc__m::after{width:42px}
  .next-svc a:hover .next-svc__l{color:var(--accent)}
}
@media (max-width:640px){
  .next-svc{grid-template-columns:1fr}
  .next-svc a + a{border-left:0;border-top:1px solid var(--line-soft);padding-left:0}
  .next-svc a:first-child{padding-right:0}
}

/* ===== pre-footer CTA band - full-bleed olive, so it reads as its own zone ===== */
.closer{background:var(--accent);color:#fff;padding:var(--s6) var(--s3);text-align:center}
.closer h2{color:#fff}
.closer p{color:rgba(255,255,255,.90);margin:var(--s2) auto 0;max-width:54ch}
/* The closer's button is white on olive, so it keeps the lighting layer (a lit
   white chip rather than a paper rectangle) but swaps the olive shadow for a dark
   one - an olive glow on an olive band is invisible. */
.closer .btn{background-color:#fff;background-image:var(--btn-lit);color:var(--accent);
  border-color:#fff;margin-top:var(--s3);box-shadow:0 3px 14px rgba(0,0,0,.20)}
@media (hover:hover){ .closer .btn:hover{background-color:#EFF3E4;color:#3E4726;
  box-shadow:0 16px 32px rgba(0,0,0,.30)} }
/* WAVE 7 (item 7.4): the closer is the CTA this site repeats most - it is the
   last thing on all eleven pages - and it carried no credential. The band's
   paragraph names the service towns, which is geographic proof rather than a
   trust signal, so the button had nothing standing behind it.

   This line is the credential, and it is the same two facts everywhere because
   they are the only two the business can substantiate under §25b: founded 2015
   and RESA affiliation. No review count, no star rating, no testimonial - none
   of those exist for this company and none may be invented.

   The colour is written OPAQUE on purpose. `rgba(255,255,255,.72)` renders
   identically but the contrast sampler reads the authored alpha rather than the
   composite and over-reports by ~2x (wave 6 found this on .ftr__upd). #CDD0C5 IS
   white .72 composited over the olive, so the pixels are unchanged and the
   measurement is honest: 4.72:1 against a 4.5 floor. */
.closer__t{
  margin:var(--s3) auto 0;max-width:54ch;
  font-size:.72rem;letter-spacing:.18em;text-transform:uppercase;font-weight:600;
  color:#CDD0C5;line-height:1.9
}
.closer__t b{color:#fff;font-weight:600}

/* ===== footer - deliberately a DIFFERENT tone to the olive band above it.
   Deep warm near-black, so the two zones read as separate bands, not one mass. */
.site-footer{background:#1E1E1A;color:#C9C7BE;margin-top:0;padding:var(--s5) 0 var(--s4)}
.site-footer a{color:#D8D6CE;text-decoration:none}
@media (hover:hover){ .site-footer a:hover{color:#fff} }
.ftr__g{display:grid;grid-template-columns:1.4fr 1fr 1fr;gap:var(--s4)}
/* Column titles carry the ACCENT on the dark ground, not white. The olive
   #4E5830 is unreadable at 1.4:1 here, so the dark-surface accent (#C5DA96,
   already the H1 accent-word colour on .hero and .phero) is the one that
   belongs: 11.1:1 on #1E1E1A, and it ties the footer to the rest of the site
   instead of leaving it as an unaccented slab. */
/* WAVE 6 (SEO/AEO): these two footer column labels were <h4> after a run of
   <h2> content headings, which was the one skipped heading level present on all
   eleven pages. They are now <h2> - the level a footer link group actually sits
   at, and the level that removes the skip. line-height is restated because the
   global h1,h2,h3 block sets 1.12 and these labels were rendering at the
   inherited 1.72; without it the gap under each label would tighten by ~7px. */
.ftr h2{font-family:var(--font-h);font-size:.78rem;letter-spacing:.2em;text-transform:uppercase;
  color:#C5DA96;margin:0 0 16px;font-weight:600;line-height:1.72}
.ftr li{list-style:none;margin-bottom:9px}
.ftr ul{margin:0;padding:0}
.ftr__logo{height:52px;width:auto;background:#fff;padding:7px 10px;border-radius:var(--radius)}
/* WAVE 9: the credential line, and it exists to expand ONE acronym.
   `.closer__t` prints "RESA affiliated" on all eleven pages. Nine of them spell
   the association out somewhere above it - the homepage proof strip, the /about
   credential block, the /contact NAP row. /privacy and /terms have neither a
   proof strip nor a credential block, so on those two pages RESA appeared once,
   as four unexplained letters, on the only pages a reader arrives at already
   looking for plain language.
   Fixing it in `.closer__t` on those two pages would have meant a shared
   component reading differently on two of eleven. Fixing it here fixes all
   eleven with one uniform line, puts the acronym directly beside its expansion
   so the closer's "RESA" has an in-document referent on every page, and adds the
   outbound link to the association at the place the affiliation is claimed
   (§4c). The claim itself is unchanged: affiliated, which is what the intake
   supports. No membership grade, no date, no count (§25b). */
.ftr__cred{
  margin-top:14px;font-size:.84rem;line-height:1.7;color:#AAAAA8
}
.ftr__cred a{color:#C5DA96;text-decoration:underline;
  text-underline-offset:3px;text-decoration-thickness:1px;
  transition:color var(--dur-hover) var(--ease-hover),
             text-decoration-thickness var(--dur-hover) var(--ease-hover)}
@media (hover:hover){ .ftr__cred a:hover{color:#DCE9C2;text-decoration-thickness:2px} }
/* WAVE 6 (§4d): the visible "Last updated" that has to match the page's own
   JSON-LD dateModified. It sits in the footer legal row rather than under the H1
   because this is a nine-page marketing site, not an article - a date stamp
   under a hero headline would read as a blog. The two legal pages keep their own
   in-body policy date and this line repeats it, which is why both say July 29
   while the nine substantive pages say July 30: the POLICY did not change this
   wave and a policy whose date moves without a text change is exactly the
   cosmetic bump §4d forbids. */
/* Opaque, not rgba(255,255,255,.62), and the reason is measurement rather than
   taste: #AAAAA8 IS that alpha composited over the footer's #1E1E1A, so the pixels
   are identical - but the rendered-pixel WCAG sampler shared by verify-browser /
   verify-wave2 reads the element's computed colour as written and does not
   composite alpha, so the translucent form reported 16.72:1 where the truth is
   7.19:1. Both pass, so this was a wrong NUMBER rather than a false pass; an
   evidence table still has to be the thing it names. Opaque removes the ambiguity
   at the source instead of patching the checker. */
.ftr__upd{color:#AAAAA8}
.ftr__legal{border-top:1px solid rgba(255,255,255,.16);margin-top:var(--s4);padding-top:var(--s3);
  display:flex;justify-content:space-between;gap:var(--s3);flex-wrap:wrap;font-size:.84rem}
/* WAVE 6: the row went from two items to three when the §4d "Last updated" stamp
   landed. At 430px space-between then wrapped the copyright onto its own two
   lines and pushed the remaining two to opposite edges, which reads as a ragged
   third row rather than a footer note. Stacked left at mobile. */
@media (max-width:640px){ .ftr__legal{flex-direction:column;gap:10px} }
.lcd a{color:#B7CE86;font-weight:600}
@media (hover:hover){ .lcd a:hover{color:#CFE0AE} }

/* ===== mobile call bar - §7a: hidden on load, reveals past the hero ===== */
.callbar{
  display:none;position:fixed;left:0;right:0;bottom:0;z-index:80;
  --callbar-bg:#33332F;background:var(--callbar-bg);
  padding:12px 16px;padding-bottom:max(14px,env(safe-area-inset-bottom));
  box-shadow:0 48px 0 0 var(--callbar-bg);                       /* safe-area colour bleed */
  transform:translateY(calc(100% + 48px + env(safe-area-inset-bottom,0px)));
  transition:transform .3s ease
}
.callbar.is-visible{transform:translateY(0)}
.callbar__g{display:grid;grid-template-columns:1fr 1fr;gap:10px}
.callbar .btn{width:100%;padding:14px 12px;font-size:.92rem}
.callbar .btn--ghost{color:#fff;border-color:rgba(255,255,255,.5)}
.site-header.is-open ~ .callbar{transform:translateY(120%)}

/* ===== P-008 scroll reveals. SSR-safe: hidden state only under html.js-motion,
   and the visible rule carries the SAME specificity or it loses. ===== */
html.js-motion [data-reveal]{opacity:0;transform:translateY(28px)}
html.js-motion [data-reveal].is-visible{
  opacity:1;transform:none;
  transition:opacity var(--dur-reveal) var(--ease-settle) var(--reveal-delay,0ms),
             transform var(--dur-reveal) var(--ease-settle) var(--reveal-delay,0ms)
}
html.js-motion .rule-label i{transform:scaleX(0);transition:transform var(--dur-reveal) var(--ease-settle)}
html.js-motion .rule-label.is-visible i{transform:scaleX(1)}
/* Hand a revealed element back to its OWN transition once the reveal has landed.
   (0,4,1) so it outranks the `.is-visible` transition at (0,3,1); `.is-settled`
   is added by motion.js one reveal-duration + stagger after the class flips, so
   the reveal itself still plays at the locked 0.8s / 90ms and only the states
   that come afterwards - the lift, the shadow - use the .6s settle. Without this
   the lift would inherit the reveal's 0.8s AND its stagger delay, so hovering a
   tile could begin a quarter-second late. Scoped to the two components that carry
   a hover lift rather than applied blanket, so nothing else changes timing.
   `translate` and `box-shadow`, deliberately - NOT `transform`. See the long note
   on `.tile` for why the lift is not a transform on these two elements. */
html.js-motion .tile[data-reveal].is-settled,
html.js-motion .feat[data-reveal].is-settled{
  transition:translate var(--dur-settle) var(--ease-settle),
             box-shadow var(--dur-settle) var(--ease-settle)
}

/* ===== P-001 hero entrance ===== */
@keyframes lc-hero-resolve{
  from{opacity:0;filter:blur(10px);transform:scale(.985)}
  to  {opacity:1;filter:blur(0);transform:scale(1)}
}
html.js-motion .hero-reveal{opacity:0;animation:lc-hero-resolve .9s var(--ease-settle) both}
html.js-motion .hero-reveal[data-d="1"]{animation-delay:.20s}
html.js-motion .hero-reveal[data-d="2"]{animation-delay:.50s}
html.js-motion .hero-reveal[data-d="3"]{animation-delay:.80s}
html.js-motion .hero-reveal[data-d="4"]{animation-delay:1.10s}
html.js-motion .hero-reveal[data-d="5"]{animation-delay:1.35s}

/* ===== responsive ===== */
@media (max-width:1000px){
  .split{grid-template-columns:1fr;gap:var(--s3)}
  .ftr__g{grid-template-columns:1fr 1fr}
}
@media (max-width:900px){
  .navtoggle{display:block}
  .nav{
    position:fixed;left:0;right:0;bottom:0;top:var(--drawer-top,84px);
    background:var(--surface);flex-direction:column;align-items:stretch;justify-content:flex-start;
    gap:0;padding:var(--s3);overflow-y:auto;-webkit-overflow-scrolling:touch;
    touch-action:pan-y;overscroll-behavior:contain;                 /* kills the iOS side-scoot */
    transform:translateX(100%);transition:transform .3s var(--ease-settle);z-index:70
  }
  .site-header.is-open .nav{transform:translateX(0)}
  /* WAVE 10 - the closed drawer was still IN THE TAB ORDER. Measured by
     pressing Tab: on all eleven pages at 430px, five stops (Services,
     Portfolio, About, Contact and the CTA) put focus at x=462 on a 430px
     screen. A keyboard user tabbed off the right edge into an invisible menu
     for five presses with nothing on screen to show where they were - WCAG
     2.4.3 and 2.4.7, and invisible to every check the site had, because
     verify-wave3 proves the focus RING exists and nothing had ever asked
     whether focus lands somewhere a person can see. Desktop was always clean
     (0 offscreen stops on all eleven); this is a mobile-only defect.
     `visibility` is what removes an element from the tab order without
     removing it from the layout, and the transition delay lets the panel
     finish sliding out before it disappears.
     GATED ON `html.js-motion` on purpose, per the no-JS contract. The closed
     state is CSS-only, so without JS the toggle button does nothing and these
     links are the ONLY nav a phone gets; a keyboard user can still tab to them
     blind and press Enter. Taking that away from a no-JS visitor to tidy the
     tab order for a JS one would be a trade in the wrong direction. */
  html.js-motion .nav{
    visibility:hidden;
    transition:transform .3s var(--ease-settle),visibility 0s linear .3s
  }
  html.js-motion .site-header.is-open .nav{
    visibility:visible;
    transition:transform .3s var(--ease-settle),visibility 0s
  }
  .nav a:not(.btn){padding:16px 0;border-bottom:1px solid var(--line-soft);font-size:1.08rem}
  .nav .btn{margin-top:var(--s3);padding:16px 20px}
  .three{grid-template-columns:1fr}
  .rail{grid-template-columns:1fr 1fr}
  .callbar{display:block}
  .site-footer{padding-bottom:calc(96px + env(safe-area-inset-bottom))}  /* clear the dock, footer-coloured */
}
@media (max-width:640px){
  .proof__g{grid-template-columns:1fr 1fr;gap:var(--s3) var(--s2)}
  /* the 54px right gutter exists to keep the answer clear of the toggle, and the
     toggle is only on the question row. On a phone that gutter is 15% of the
     measure, so give it back and let the answer use the column. */
  .faq__a{padding-right:0}
  /* same argument on the other side: a 32px indent on the AEO capsule costs 9%
     of a 366px measure. The 2px accent rule is what marks the capsule; the deep
     indent is something only a wide column can afford. */
  .capsule{padding-left:var(--s2)}
  .rail{grid-template-columns:1fr}
  .ba{grid-template-columns:1fr}
  .ftr__g{grid-template-columns:1fr}
  .brand img{height:46px}
}
/* §7b-5: a stacked CTA pair shares ONE width, sized to the widest.
   Must come after the base .cta-row flex rule or it loses at equal specificity.

   WAVE 9: `minmax(0, max-content)`, not bare `max-content`, and the zero is
   load-bearing. A bare max-content track sizes to the widest child's UNWRAPPED
   width and has no upper bound, so a button whose label is longer than the
   column simply grows the track past the container. "See the Downtown
   Edwardsville project" rendered 351.7px inside a 256px column at 320px and lost
   its last 15.8px off the right edge of the screen - silently, because `body`
   carries `overflow-x: clip` and nothing scrolled to reveal it. minmax(0, …)
   caps the track at the container so an over-long label wraps to two lines
   instead, which is the correct failure. Every button in the group still shares
   one width, which is the whole point of the rule. */
@media (max-width:560px){
  .cta-row{display:grid;grid-template-columns:minmax(0,max-content);gap:12px}
}
/* WAVE 10 - the 320px step the CLOSER got in wave 9 and the HERO did not.
   Wave 9 gave the olive closer band a max-width:420px relief step and brought
   its four service CTAs from 100% (wrapped) down to 79-88% of the column. The
   hero and panel CTA groups were never in that edit, and at 320px eleven of
   them still measure 88.5-92.5% - single-line and correctly equalised, so not
   the full-bleed shape §7b-5 rejects, but in `w9-btn.cjs`'s advisory WIDE band
   and visibly edge-to-edge next to the closers that were fixed.
   8px a side off a 26px pad, at 320-359 only. The type does not move (8.6's
   14px floor) and the group still shares one width, so the §7b-5 contract is
   untouched. Measured after: the worst of the eleven goes 92.5% -> 86.2% and
   the hero pair 90.9% -> 84.7%; `w9-btn.cjs` reports FLAGGED: none at
   320/360/375/430, where it reported eleven before this rule. */
@media (max-width:359px){
  .cta-row .btn{padding-left:18px;padding-right:18px}
}

/* ===== reduced motion: resolve to the meaningful end state, never blank ===== */
@media (prefers-reduced-motion:reduce){
  html{scroll-behavior:auto}
  html.js-motion [data-reveal],
  html.js-motion [data-reveal].is-visible{opacity:1;transform:none;transition:none}
  html.js-motion .hero-reveal{opacity:1;animation:none;filter:none;transform:none}
  html.js-motion .rule-label i{transform:scaleX(1);transition:none}
  .hero__bg img{transform:none!important}
  *,*::before,*::after{animation-duration:.001ms!important;transition-duration:.001ms!important}
}

/* §7b-2: a partial grid row is centred, never left-orphaned against a dead track.
   Belt and braces in case the rail count changes later.
   GUARDED at 641px and up, and the guard is the whole point. `:has()` outranks a
   plain `.rail` on specificity, so unguarded these three beat the phone rule
   `@media (max-width:640px){.rail{grid-template-columns:1fr}}` and forced a
   two-up at 430px. /services/vacant-staging has a four-tile rail, so its
   photographs were rendering 173px wide on a phone with the P-012 panel covering
   most of each one. A rule written to stop an orphaned row was creating a
   cramped one on the surface that matters most. */
@media (min-width:641px){
  .rail:has(> :nth-child(4):last-child){grid-template-columns:repeat(2,minmax(0,440px));justify-content:center}
  .rail:has(> :nth-child(2):last-child){grid-template-columns:repeat(2,minmax(0,440px));justify-content:center}
  .rail:has(> :only-child){grid-template-columns:minmax(0,620px);justify-content:center}
}
/* WAVE 7 (item 7.0c): the THREE-tile rail was the case none of the rules above
   covered, and it only breaks inside one band. A 3-tile rail is a clean 3-up at
   901px and up and a clean 1-up at 640px and down; between 641 and 900 the base
   rule drops it to two columns and the third tile orphans against a dead track.
   Measured before the fix at 820px: five rails across /services,
   /services/vacant-staging, /services/new-build-staging and /work (x2), each
   leaving 388px of empty grid beside the orphan.

   The fix centres the ITEM rather than the tracks, which is the distinction that
   matters here. `justify-content:center` centres the whole track grid and leaves
   the orphan sitting in track 1, still left of the axis - it would have measured
   as a smaller version of the same defect. Spanning the orphan across both
   tracks and sizing it to exactly one track width puts it on the axis and keeps
   every photograph in the rail the same size. */
@media (min-width:641px) and (max-width:900px){
  .rail:has(> :nth-child(3):last-child) > :last-child{
    grid-column:1 / -1;justify-self:center;width:calc(50% - (var(--s2) / 2))
  }
}

/* ==========================================================================
   STEP 2 - INTERIOR PAGES
   Every pattern below is an inheritance of an approved homepage pattern, not a
   new design. The homepage locked: hairline rule-labels, open photo-led cards,
   the editorial split, P-012 tiles, native <details> FAQ, the olive pre-footer,
   and the #1E1E1A footer. Interior pages reuse those verbatim and add only what
   a homepage has no slot for: a page hero, long-form prose, the P-009 pinned
   plate, a labelled before/after, a form, and the legal shell.
   ========================================================================== */

/* ===== .phero - the interior page hero.
   A deep band with a CAPPED photo panel, not a full-bleed photograph, for one
   measured reason: ten of fifteen source files are 960px wide, and a full-width
   band at 1440 CSS px on a 2x screen asks for 2880. The panel is capped at 560px
   so a 960px file renders at ~1.7x density and stays crisp. This is the §12d
   split spec already recorded in interactions.md as the hero fallback, applied
   where it belongs. The homepage full-bleed hero remains the ONE deliberate
   exception, taken with the numbers in hand.
   The dark ground also keeps P-003 honest: the header stays transparent with the
   reversed wordmark riding it, exactly as it does over the homepage photograph. */
/* One height for every interior hero. Measured before this: 550 to 656px across the
   eight pages, entirely content-driven, so /work and /about visibly shrank against
   /services. 72vh matches the /services height the operator singled out as right,
   with a floor and a ceiling so a short laptop or a tall monitor both behave. */
.phero{
  min-height:clamp(620px,70vh,760px);   /* mobile floor; desktop is fixed below */
  position:relative;isolation:isolate;overflow:hidden;
  background:#1E1E1A;color:#fff;
  padding:calc(var(--s6) + 30px) 0 var(--s5)
}
/* item 3.3 on the interior pages. #1E1E1A across a full band is the flattest
   surface on the site, and a flat slab is what a template looks like. A single
   very low-alpha radial centred on the type column lifts the ground behind the
   H1 by about six percent and lets it fall off into the corners, so the band
   reads as lit rather than filled. Light halo here because the ground is dark:
   it only moves #1E1E1A toward #2A2A26, which still leaves white type near 14:1
   and the #C5DA96 eyebrow near 9:1, both far above their floors. */
.phero::before{
  content:"";position:absolute;inset:0;z-index:-1;pointer-events:none;
  background:radial-gradient(66% 92% at 15% 58%, rgba(255,255,255,.06) 0%,
                                                 rgba(255,255,255,.022) 46%,
                                                 rgba(255,255,255,0) 72%)
}
/* CAP LOWERED 560 -> 520 IN WAVE 4, and it is a real correction rather than a
   preference. PAGE-BUILD-LOG justified 560 with "a 960px file renders at roughly
   1.7x and stays crisp". 960/560 is indeed 1.71 - but that arithmetic ignores
   device pixel ratio, and every retina screen asks the panel for 560 x 2 = 1120
   device pixels. A 960px file supplies 0.86 of that, so the panel was UPSCALING
   by 1.17x on five of the seven interior heroes while its own comment claimed
   1.7x of headroom. Measured at render (verify-wave4 group F), not inferred.
   520 x 2 = 1040 against 960 is 1.08x, inside the ~1.10 the resolution table
   allows, and 520 is the number interactions.md wrote for this exact panel
   before the build rounded it up. The lesson worth keeping: a density claim that
   does not name a DPR is not a measurement. */
/* 52%/760px, up from 44%/520px. The operator's read was that the band was two-thirds
   brown, and he was right: at 1440 the picture was 36% of the hero. The 520 cap was
   not arbitrary though - five of seven sources were 960px, which legally supports
   only ~480 CSS at 2x. So the fix was better SOURCES, not a bigger number: those six
   files were stepped-upscaled to 1520px (1.18x per pass + unsharp, the same method
   behind the approved homepage hero), which supports 760 CSS at 2x. Picture is now
   52% of the band. Do not widen further without new source files. */
/* FULL-BLEED, changed 2026-07-31 by operator direction. This was a dark band with a
   capped photo panel, chosen because five of seven sources were 960px. The operator
   twice reported it reading two-thirds brown, and on a portfolio-driven site he is
   right that the photograph should lead. Same call he made on the homepage hero when
   the resolution finding was put to him. Sources stepped-upscaled to 2400px, so at
   1440 CSS on a 2x screen this runs 1.2x rather than the 3x it would have been.
   The scrim below is the measured price of doing this: it is the LIGHTEST ramp that
   still clears WCAG on every text node, sampled from rendered pixels. */
.phero__ph{position:absolute;inset:0;width:100%;z-index:-1;overflow:hidden}
.phero__ph img{width:100%;height:100%;object-fit:cover}
/* WAVE 10 - the DESKTOP half of the same complaint, and here the headroom is
   real rather than an illusion, because the geometry says so. `.phero__t` caps
   at min(52%,620px) and the panel starts at 100% - min(44%,520px), so at 1440
   the type ends 170px short of the panel and at 901 (the last width this rule
   applies to) it still ends 17px short. NO TYPE IS EVER OVER THIS GRADIENT, at
   any width it governs. It was therefore not protecting legibility; it was
   blending a seam, and it was doing it with .22 of black still sitting on the
   far edge of the photograph.
   Re-ramped to clear completely by 82% instead of holding .22 to the edge. The
   left seam is unchanged, so the blend into the band is identical. Measured
   after with verify-browser's rendered-pixel sampler: every phero contrast
   sample in the DESKTOP 1440 block is byte-identical before and after, which
   is the proof that the sampled pixels and the changed pixels are disjoint -
   the exact test wave 9's mobile attempt FAILED, and the reason that one was
   reverted and this one is not. Same instinct, opposite measurement. */
.phero__ph::after{
  content:"";position:absolute;inset:0;
  /* Two gradients, same shape as the homepage hero: a horizontal ramp that pays for
     the type column, and a gentle vertical base for the header and the lower edge.
     The RIGHT 40% is nearly clear, which is the whole point of going full-bleed. */
  background:
    linear-gradient(90deg, rgba(22,22,17,.90) 0%,  rgba(22,22,17,.87) 34%,
                           rgba(22,22,17,.76) 58%, rgba(22,22,17,.24) 80%,
                           rgba(22,22,17,.05) 100%),
    linear-gradient(180deg,rgba(22,22,17,.40) 0%,  rgba(22,22,17,.14) 30%,
                           rgba(22,22,17,.16) 70%, rgba(22,22,17,.42) 100%)
}
/* the type now sits ON the photograph, so this is a reading measure, not a dodge */
.phero__t{max-width:min(58%,640px)}
.phero .eyebrow,.layer-panel .eyebrow{
  /* inline-block so the box hugs the glyphs. As a block it spans the whole column,
     which both mis-measures contrast and is simply wrong (same fix as .hero .eyebrow). */
  display:inline-block;color:#C5DA96                /* the olive accent is 2:1 here; this is 10.9:1 */
}
.phero h1{color:#fff;margin-top:var(--s2);text-shadow:0 2px 18px rgba(0,0,0,.4)}
.phero .lede{color:rgba(255,255,255,.92);margin-top:var(--s2);max-width:50ch}
.phero .cta-row{margin-top:var(--s3)}
/* NOT a flex container. This paragraph holds inline prose with <b> spans, and
   `display:flex` promotes every bare text run AND every <b> to its own flex item,
   so the 32px gap opened up INSIDE the sentence: "FOUNDED    2015    ·    RESA".
   It read as broken tracking on /about and /services/new-build-staging at every
   breakpoint. A plain block with normal word spacing is what the copy wants; the
   separators are already spaced with &nbsp;·&nbsp; in the HTML. */
.phero__meta{
  margin-top:var(--s3);line-height:1.9;
  font-size:.72rem;letter-spacing:.18em;text-transform:uppercase;font-weight:600;
  color:rgba(255,255,255,.72)
}
.phero__meta b{color:#C5DA96;font-weight:600}

/* ===== solid header for the utility pages (§7a).
   /contact, /privacy and /terms lead with the action, not a photographic band,
   so the transparent state has nothing to sit on. The bar ships solid from load
   and the original wordmark shows. ===== */
.site-header.is-solid{
  background:var(--surface);border-bottom-color:var(--line-soft);
  box-shadow:0 2px 18px rgba(51,51,47,.09)
}
.site-header.is-solid .brand__dark{opacity:1}
.site-header.is-solid .brand__rev{opacity:0}
.site-header.is-solid .nav a:not(.btn){color:var(--ink-strong);text-shadow:none}
.site-header.is-solid .nav .btn{
  background-color:var(--accent);background-image:var(--btn-lit);
  border-color:var(--accent);color:var(--accent-on);backdrop-filter:none;
  box-shadow:0 2px 10px rgba(78,88,48,.18), 0 1px 2px rgba(78,88,48,.16)
}
.site-header.is-solid .navtoggle{border-color:var(--line)}
.site-header.is-solid .navtoggle span{background:var(--ink-strong)}
body.has-solid-header main{padding-top:82px}
@media (max-width:640px){ body.has-solid-header main{padding-top:70px} }

/* ===== breadcrumb - service pages only, so a deep page states its own place.
   Trade-site doctrine skips breadcrumbs on shallow pages; /services/* is two
   levels down and the schema needs the trail anyway. ===== */
.crumb{
  font-size:.72rem;letter-spacing:.16em;text-transform:uppercase;font-weight:600;
  color:rgba(255,255,255,.62);margin-bottom:var(--s2)
}
.crumb a{color:rgba(255,255,255,.86);text-decoration:none;border-bottom:1px solid transparent}
@media (hover:hover){ .crumb a:hover{border-bottom-color:#C5DA96;color:#fff} }
.crumb span{margin:0 8px;color:rgba(255,255,255,.4)}

/* ===== long-form prose ===== */
.prose{max-width:68ch}
.prose > h2{margin-top:var(--s5)}
.prose > h2:first-child{margin-top:0}
.prose > h3{margin-top:var(--s4);margin-bottom:10px}
.prose p{margin-top:var(--s2);color:var(--ink)}
.prose > p:first-child{margin-top:0}
.prose strong{color:var(--ink-strong);font-weight:600}
.prose ul{margin:var(--s2) 0 0;padding:0 0 0 22px;color:var(--ink)}
.prose li{margin-bottom:10px}
.prose li::marker{color:var(--accent)}
/* Cinzel has no lowercase, so a heading that must run long is set in Lora and
   the short display headings keep Cinzel. Recorded in design.md as a system. */
.prose h2.h-long,.prose h3.h-long{font-family:var(--font-b);letter-spacing:0;font-weight:600}

/* ===== the AEO capsule - the answer paragraph that byte-matches FAQPage ===== */
.capsule{
  border-left:2px solid var(--accent);padding-left:var(--s3);
  color:var(--ink);font-size:1.06rem;max-width:62ch
}

/* ===== numbered process steps ===== */
.steps{display:grid;gap:0;max-width:820px}
.steps__i{
  display:grid;grid-template-columns:auto 1fr;gap:var(--s3);align-items:start;
  padding:var(--s3) 0;border-top:1px solid var(--line-soft)
}
.steps__i:last-child{border-bottom:1px solid var(--line-soft)}
.steps__n{
  font-family:var(--font-h);font-size:1.05rem;font-weight:600;color:var(--accent);
  letter-spacing:.08em;min-width:2.2em;padding-top:2px
}
.steps__i h3{margin-bottom:8px}
.steps__i p{color:var(--ink);font-size:1rem;max-width:56ch}
@media (max-width:560px){
  .steps__i{grid-template-columns:1fr;gap:var(--s1)}
  .steps__n{padding-top:0}
}

/* ===== P-009 pinned-layer depth - ONE instance site-wide, on /services.
   Contract: overflow-x must be `clip` (never `hidden`) on every ancestor or the
   sticky silently dies. html/body already set clip. ===== */
.layers{position:relative;z-index:1}
.layers-bg{position:sticky;top:0;height:100vh;height:100svh;overflow:hidden}
.layers-bg img{width:100%;height:100%;object-fit:cover;object-position:center 55%;transform-origin:center}
.layers-bg::after{
  content:"";position:absolute;inset:0;
  /* Measured, not guessed. A uniform .74 veil hit the 4.5:1 floor but flattened the
     photograph into murk. This ramps HORIZONTALLY: opaque under the text column,
     open under the photo card, so the plate still reads as a room. */
  background:
    linear-gradient(90deg, rgba(14,14,10,.90) 0%,  rgba(14,14,10,.86) 46%,
                           rgba(14,14,10,.50) 72%, rgba(14,14,10,.30) 100%),
    linear-gradient(180deg,rgba(14,14,10,.26) 0%,  rgba(14,14,10,.14) 40%,
                           rgba(14,14,10,.20) 75%, rgba(14,14,10,.42) 100%)
}
.layers-flow{margin-top:-100vh;margin-top:-100svh;position:relative;z-index:2}
.layer-panel{min-height:116vh;display:flex;align-items:center;padding:var(--s5) 0;color:#fff}
.layer-panel__t{max-width:600px}
.layer-panel .eyebrow{color:#C5DA96}
.layer-panel h2{color:#fff;margin-top:12px;text-shadow:0 2px 18px rgba(0,0,0,.45)}
.layer-panel p{color:rgba(255,255,255,.93);margin-top:var(--s2);max-width:52ch;
  text-shadow:0 1px 12px rgba(0,0,0,.5)}
.layer-panel .card__more{color:#C5DA96;margin-top:var(--s3)}
.layer-panel .card__more::after{background:#C5DA96}
/* the standalone hover, recoloured for the dark plate - #3E4726 would vanish here */
@media (hover:hover){
  .layer-panel a.card__more:hover{color:#fff}
  .layer-panel a.card__more:hover::after{background:#fff}
}
@media (max-width:900px){
  .layer-panel{min-height:104vh;padding:var(--s4) 0}
  .layers-bg::after{background:linear-gradient(180deg,rgba(14,14,10,.80) 0%,rgba(14,14,10,.74) 50%,
                                                      rgba(14,14,10,.86) 100%)}
}

/* ===== labelled before / after.
   The pair is a cabinet-painting job, not staging. Both frames carry an explicit
   label so the "after" is never read as a staging result (§25b honesty). ===== */
.ba{display:grid;grid-template-columns:1fr 1fr;gap:var(--s3);max-width:960px;margin:0 auto}
.ba figure{margin:0}
.ba img{width:100%;height:auto;aspect-ratio:3/2;object-fit:cover;border-radius:var(--radius);
  outline:1px solid color-mix(in srgb, var(--ink) 15%, transparent);outline-offset:-1px;
  box-shadow:var(--frame-rest)}
.ba figcaption{margin-top:14px}
.ba__tag{
  display:block;font-size:.72rem;letter-spacing:.2em;text-transform:uppercase;
  font-weight:700;color:var(--accent)
}
.ba__d{display:block;font-size:.94rem;color:var(--ink);margin-top:6px;line-height:1.6}

/* ===== contact form (§9b pixel shape + §9c spam stack) ===== */
.util{padding:var(--s4) 0 var(--s6)}
.util__g{display:grid;grid-template-columns:1.02fr .98fr;gap:var(--s5);align-items:start}
/* Two columns, so the call chip and the credential line sit BESIDE the copy rather
   than stacking under it. That reclaims two rows of height and lifts the form.
   The 640px cap was also breaking the H1: "BOOK A WALKTHROUGH" is ~730px at 64.8px
   in Cinzel caps, so it wrapped to two lines and pushed everything down. The left
   track now gets ~870px at 1440 and it sets on one line. */
.util__head{
  display:grid;grid-template-columns:minmax(0,1fr) max-content;
  column-gap:var(--s5);align-items:start;margin-bottom:var(--s4)
}
.util__head > .eyebrow{grid-column:1;grid-row:1}
.util__head > h1{grid-column:1;grid-row:2;margin-top:10px}
.util__head > .lede{grid-column:1;grid-row:3;margin-top:var(--s2)}
.util__head > .chip{grid-column:2;grid-row:2;margin-top:10px;white-space:nowrap}
.util__head > .trust-t{grid-column:2;grid-row:3;margin-top:var(--s2);text-align:right}
@media (max-width:900px){
  .util__head{grid-template-columns:1fr;column-gap:0}
  .util__head > *{grid-column:1!important}
  .util__head > .chip{grid-row:auto}
  .util__head > .trust-t{grid-row:auto;text-align:left}
}
/* A grid item cannot shrink below its own min-content, and min-content for this H1
   is the longest WORD. At 320px "WALKTHROUGH" sets 278.8px in Cinzel caps at
   32.8px, so the head block rendered 278.8px inside a 256px column and spilled
   22.8px past the gutter. The track itself was correct at 256px, which is why the
   usual document-scroll check saw nothing: html carries overflow-x:clip, so the
   text was painted past the edge rather than made scrollable.
   Measured, not guessed: the eyebrow needs 73px and the lede 80px, so the H1 is
   the only item anywhere near the track, and 1.72rem brings its longest word to
   about 234px inside 256. Clean at 375 and 430 already, hence the 400px ceiling. */
@media (max-width:400px){
  .util__head h1{font-size:1.72rem}
}
/* the slim secondary chip, per utility-page doctrine - not a second big button */
.chip{
  display:inline-flex;align-items:center;gap:9px;margin-top:var(--s2);
  font-size:.9rem;font-weight:600;color:var(--accent);text-decoration:none;
  border:1px solid var(--line);border-radius:var(--radius);padding:9px 15px;
  transition:border-color var(--dur-hover) var(--ease-hover),background var(--dur-hover) var(--ease-hover)
}
@media (hover:hover){ .chip:hover{border-color:var(--accent);background:var(--surface)} }

.form{
  background:var(--surface);border:1px solid var(--line-soft);border-radius:var(--radius);
  padding:var(--s4);position:relative
}
/* WAVE 9: the card's side padding steps down on a phone, and this is the worst
   thing found in the wave. `var(--s4)` is 52px, and it was applied at EVERY
   width, on the only conversion surface the site has. Measured: at 375 the card
   is 311px wide and 104px of that is padding, so every input, the textarea and
   the submit button were laid out in a 207px column - 55% of the screen. At 320
   the usable column was 152px and "Request a walkthrough" broke across two lines
   inside its own submit button.
   Nothing caught it because the §9b/§9c form contract checks the field ORDER,
   the labels, the honeypot and the timestamp - it has nothing to say about how
   wide a field renders, and the two form-shaped checks that do run visually
   (verify-browser's runtime group and wave 7's audit) both measure at 430, where
   the column is a survivable 262px.
   32px below 640 and 20px below 420: the usable column becomes 247px at 375 and
   216px at 320, gains of 19% and 42%. The vertical padding is untouched, so the
   card keeps its air where air costs nothing. */
@media (max-width:640px){ .form{padding:var(--s4) var(--s3)} }
@media (max-width:420px){ .form{padding:var(--s4) var(--s2)} }
/* WAVE 7 (item 7.1): the gradient top accent bar. Three points worth recording.
   It runs the FULL width of the card and sits flush on the top edge, inside the
   2px radius, so the card reads as a labelled panel rather than a bordered box.
   The gradient runs between the two colours already locked in design.md - the
   olive #4E5830 and the cricket green #7E9C48 - so it introduces no third hue;
   it is the same two-token move `--btn-lit` makes on the primary button. And it
   is `::before` on a `position:relative` card that already existed, so nothing
   in the form's box model moved. */
.form::before{
  content:"";position:absolute;left:-1px;right:-1px;top:-1px;height:4px;
  border-radius:var(--radius) var(--radius) 0 0;
  background:linear-gradient(90deg,var(--accent) 0%,var(--accent-2) 52%,var(--accent) 100%)
}
/* The expectation-setter (item 7.1). Three short lines, in the order they happen,
   directly under the accent bar and above the first field, so the reader knows
   what pressing the button costs them before they start typing. Every line
   restates something the site already publishes: the contact lede's "Andrea
   walks the property, writes a plan with a scope and a timeline, and quotes the
   work". Numbered by a counter rather than an <ol> marker so the numerals can be
   set in the display face at label size. */
.form__next{counter-reset:fn;margin:0 0 var(--s3);padding:0 0 var(--s3);
  border-bottom:1px solid var(--line-soft);list-style:none}
.form__next li{
  counter-increment:fn;position:relative;padding-left:30px;
  font-size:1rem;line-height:1.6;color:var(--ink-strong)
}
.form__next li + li{margin-top:8px}
.form__next li::before{
  content:counter(fn);position:absolute;left:0;top:1px;
  width:20px;height:20px;border-radius:50%;
  display:flex;align-items:center;justify-content:center;
  font-family:var(--font-h);font-size:.7rem;font-weight:600;line-height:1;
  color:var(--accent);background:#EFF3E4;
  border:1px solid color-mix(in srgb, var(--accent) 34%, transparent)
}
/* item 7.4 - the credential line for a LIGHT ground. `.phero__meta` is the same
   idea on the dark photographic band and `.closer__t` is the same idea on the
   olive; a colour is the only thing that separates the three, because each sits
   on a different surface and one declaration cannot be legible on all three.
   Used twice: above the submit button (so the credential is inside the item's
   200px window on every breakpoint) and under the /contact page head, which is
   the only page hero on the site that is not a dark band. */
.trust-t{
  margin-top:var(--s3);font-size:.72rem;letter-spacing:.18em;text-transform:uppercase;
  font-weight:600;color:var(--ink-soft);line-height:1.9
}
.trust-t b{color:var(--accent);font-weight:600}
.util__head .trust-t{margin-top:var(--s2)}
/* honeypot - in the DOM, off-screen, never announced, never focusable */
.hp{position:absolute;left:-9999px;top:0;width:1px;height:1px;overflow:hidden}
.field{margin-bottom:var(--s2)}
.field label{
  display:block;font-size:.74rem;letter-spacing:.14em;text-transform:uppercase;
  font-weight:700;color:var(--ink);margin-bottom:8px
}
.field input,.field select,.field textarea{
  width:100%;font-family:var(--font-b);font-size:1rem;line-height:1.5;color:var(--ink-strong);
  background:var(--bg);border:1px solid var(--line);border-radius:var(--radius);
  padding:13px 14px;
  transition:border-color var(--dur-hover) var(--ease-hover),box-shadow var(--dur-hover) var(--ease-hover)
}
.field textarea{min-height:130px;resize:vertical}
/* The five form controls were the only interactive surfaces on the site with no
   pointer response at all: identical computed style at rest and under the cursor.
   On the conversion page. The border warms toward the accent on hover, which is
   the same gesture the focus state completes. Declared BEFORE the focus rules on
   purpose: hover and focus land at the same (0,2,1), so source order is what
   stops a hovered-and-focused field from showing the weaker of the two. */
@media (hover:hover){
  .field input:hover,.field select:hover,.field textarea:hover{
    border-color:color-mix(in srgb, var(--accent) 45%, var(--line))
  }
}
.field input:focus,.field select:focus,.field textarea:focus{
  border-color:var(--accent);outline:none;box-shadow:0 0 0 3px rgba(78,88,48,.15)
}
/* item 3.8 - this is the site's ONLY `outline:none`, and it is not left dangling.
   The rule above is a pointer-focus tint; on its own it also killed the keyboard
   ring, because `.field input:focus` (0,2,1) outranks the universal
   `input:focus-visible` (0,1,1) and a 15%-alpha 3px halo is not a focus
   indicator. The line below restores a real 2px accent ring at MATCHING
   specificity (0,2,1) and sits after it in source order, so keyboard users get
   the ring and the tint together. */
.field input:focus-visible,.field select:focus-visible,.field textarea:focus-visible{
  outline:2px solid var(--accent);outline-offset:4px
}
.form .btn{width:100%;margin-top:var(--s2)}
.form__consent{font-size:.9rem;color:var(--ink-soft);margin-top:14px;line-height:1.6}
.form__ok{
  display:none;margin-top:var(--s3);padding:15px 17px;border-radius:var(--radius);
  background:#EFF3E4;border:1px solid var(--accent-2);color:#3E4726;font-size:1rem;line-height:1.6
}
.form__ok.is-on{display:block}

/* contact aside: NAP, service area, what happens next */
.nap{list-style:none;margin:0;padding:0}
.nap li{padding:var(--s2) 0;border-bottom:1px solid var(--line-soft)}
/* The first row sat a full s2 below the rule-label above it, which is roughly double
   the gap every other rule-label on the site leaves. Match that instead. */
.nap li:first-child{padding-top:0}
.util__g aside .rule-label{margin-bottom:var(--s2)}
.nap dt,.nap__k{
  font-size:.72rem;letter-spacing:.18em;text-transform:uppercase;font-weight:700;color:var(--accent)
}
.nap__v{display:block;margin-top:7px;color:var(--ink-strong);font-size:1.02rem}
.nap__v a{text-decoration:none;font-weight:600}
@media (hover:hover){ .nap__v a:hover{color:var(--accent)} }
.nap__n{display:block;margin-top:5px;font-size:.9rem;color:var(--ink)}

/* ===== legal shell (§19).
   Headings run long here, so they are set in Lora per the design.md type rule
   rather than forced into Cinzel caps. ===== */
.legal{max-width:760px;margin:0 auto}
.legal h2,.legal h3{font-family:var(--font-b);letter-spacing:0;font-weight:600}
.legal h2{font-size:1.34rem;margin-top:var(--s4)}
.legal h3{font-size:1.06rem;margin-top:var(--s3);margin-bottom:8px}
.legal p{margin-top:var(--s2);color:var(--ink)}
.legal ul{margin:var(--s2) 0 0;padding:0 0 0 22px;color:var(--ink)}
.legal li{margin-bottom:10px}
.legal li::marker{color:var(--accent)}
/* The legal pages carry the only in-body prose links on the site (privacy ×2,
   terms ×1, plus the tel: links). They were accent-coloured but completely
   inert - no underline at rest, no response to the pointer. An underline that
   thickens on hover is the plainest correct treatment for a link inside a
   sentence, and a legal document is the last place to be clever about it. */
/* Legal copy prints bare URLs, and a bare URL is one unbreakable word. Measured at
   320px: "coth.localcraftdigital.com/contact" sets 260.5px inside a 256px column and
   spilled 4.5px. The old domain was eight characters shorter and happened to fit, which
   is the kind of luck that breaks the moment a domain changes. `anywhere` lets the URL
   break at any character rather than push its container, and unlike `break-word` it also
   shrinks the element's min-content, which is what a grid or flex track actually sizes
   to. Scoped to legal pages: elsewhere a mid-word break would be a typographic defect. */
.legal a{overflow-wrap:anywhere;color:var(--accent);font-weight:600;text-decoration:underline;
  text-underline-offset:3px;text-decoration-thickness:1px;
  transition:color var(--dur-hover) var(--ease-hover),
             text-decoration-color var(--dur-hover) var(--ease-hover)}
@media (hover:hover){ .legal a:hover{color:#3E4726;text-decoration-thickness:2px} }

/* WAVE 6 (§4c): outbound citation links. The site had ZERO outbound links to a
   primary source, on a business whose only two checkable third-party records are
   the trade body it is affiliated with and the Houzz portfolio every photograph
   on the site came from. Both are now cited where the claim is made.
   Same underline-in-a-sentence treatment as .legal a, deliberately: a citation
   should look like a citation, not like a button. It is NOT accent-coloured on
   the proof strip, because that strip already carries the one accent it is
   allowed (the 30x2 rule above each numeral) and a second would fail restraint -
   there the link inherits the label colour and carries only the underline,
   which goes accent on hover.

   WAVE 9: the `.cite` half of this is DELETED. Wave 6 wrote a `.cite` block class
   alongside these rules and then cited both sources inline instead, at the exact
   sentence making each claim - the RESA link inside the proof-strip label and the
   Houzz link inside the /work label and the faq answers. No element on any of the
   eleven pages ever carried `class="cite"`, so the selector matched nothing and
   the standalone `.cite{}` rule was pure dead weight. Restoring a use for it would
   mean printing a second copy of a citation away from its claim, which is worse
   than the inline treatment that replaced it. So it goes. */
.steps__i a,.faq__a a{
  color:var(--accent);font-weight:600;text-decoration:underline;
  text-underline-offset:3px;text-decoration-thickness:1px;
  transition:color var(--dur-hover) var(--ease-hover),
             text-decoration-thickness var(--dur-hover) var(--ease-hover)
}
.proof__l a{
  color:inherit;text-decoration:underline;
  text-decoration-color:color-mix(in srgb, var(--accent) 45%, transparent);
  text-underline-offset:3px;text-decoration-thickness:1px;
  transition:color var(--dur-hover) var(--ease-hover),
             text-decoration-color var(--dur-hover) var(--ease-hover)
}
@media (hover:hover){
  .steps__i a:hover,.faq__a a:hover{color:#3E4726;text-decoration-thickness:2px}
  .proof__l a:hover{color:var(--accent);text-decoration-color:var(--accent)}
}
.legal__eff{
  font-size:.78rem;letter-spacing:.16em;text-transform:uppercase;font-weight:700;
  color:var(--accent);margin-bottom:var(--s2)
}
.legal__note{
  margin-top:var(--s4);padding:var(--s3);background:var(--surface);
  border:1px solid var(--line-soft);border-radius:var(--radius);font-size:.92rem;color:var(--ink)
}

/* ===== /work - one block per real project, so no row is ever orphaned ===== */
.proj{margin-top:var(--s5)}
.proj:first-of-type{margin-top:0}
.proj__h{display:flex;align-items:baseline;justify-content:space-between;gap:var(--s3);
  flex-wrap:wrap;margin-bottom:var(--s3);padding-bottom:14px;border-bottom:1px solid var(--line-soft)}
.proj__h h2{font-size:1.5rem}
.proj__m{font-size:.72rem;letter-spacing:.18em;text-transform:uppercase;font-weight:700;
  color:var(--accent);white-space:nowrap}
/* a wide feature frame above a 2-up, used where a project has a 1824px file */
/* `.feat` is an <a>, and until this wave it was the one link on the site with no
   hover state of any kind - the largest photograph on /work, fully clickable and
   completely inert to the pointer. Same depth language as .tile. */
.feat{display:block;position:relative;overflow:hidden;border-radius:var(--radius);margin-bottom:var(--s2);
  box-shadow:0 1px 8px rgba(51,51,47,.05);
  transition:translate var(--dur-settle) var(--ease-settle),
             box-shadow var(--dur-settle) var(--ease-settle)}
.feat img{width:100%;height:auto;aspect-ratio:16/9;object-fit:cover;
  transition:transform var(--dur-settle) var(--ease-settle)}
@media (hover:hover){
  .feat:hover{translate:0 -4px;box-shadow:0 26px 48px rgba(51,51,47,.18)}
  .feat:hover img{transform:scale(1.035)}
}
.feat:focus-visible{translate:0 -4px;box-shadow:0 26px 48px rgba(51,51,47,.18)}
.two{display:grid;grid-template-columns:1fr 1fr;gap:var(--s2)}
.one{max-width:620px}
/* a portrait source, windowed low into a landscape frame: keeps the seating,
   drops the dead carpet, and crops out the novelty wall sign _manifest.md flags */
.tile--tall img{aspect-ratio:4/3;object-position:center 72%}
@media (max-width:640px){ .two{grid-template-columns:1fr} }

/* ===== responsive ===== */
@media (max-width:1000px){
  .util__g{grid-template-columns:1fr;gap:var(--s4)}
}
@media (max-width:900px){
  /* ONE padding for every interior hero. Do not reintroduce per-page overrides: five
     of them existed and produced a 102px spread between /services and /work at 1280x720.
     The floor below is set so content + this padding never exceeds it, which is what
     actually makes the height uniform - a min-height alone cannot, because it only
     raises the short pages and leaves the tall ones on their own content height. */
  .phero{padding:calc(var(--s5) + 16px) 0 var(--s4)}
  .phero__t{max-width:none}
  .ba{grid-template-columns:1fr;gap:var(--s4)}
}

/* ===== WAVE 10 - the interior hero, RESTACKED below 901px.
   This is the resolution of the complaint wave 9 escalated as "an operator
   decision". It was escalated on a reason that turns out not to apply to this
   solution, and the option set, not the measurement, is what was incomplete.

   WHAT WAS WRONG, measured rather than asserted. Below 901px `.phero__ph` went
   `width:100%` and stayed `position:absolute` BEHIND the type, so the mobile
   hero was a full-bleed photograph with a vertical scrim over it. Two separate
   losses, and only the first had been recorded:
     1. the scrim covered 82-88% of the frame from the first pixel down;
     2. `object-fit:cover` into a ~430x670 near-portrait box threw away most of
        the picture. Measured with the scrim lifted, the phone was seeing
        33-58% of the horizontal frame: 33% on /services/vacant-staging, 42%
        on /services/new-build-staging, 45% on /services, 58% at best on /work.
   A room photograph read at 33% of its width, at 12-18% of its brightness, on
   the privileged surface, for a business §25b leaves with no reviews and
   nothing but photography to persuade with.

   WHY WAVE 9's REASON DOES NOT APPLY HERE. It rejected the structural fix
   because "the photograph needs a band the type does not sit in", which it
   read as contradicting the §12d split hero and taking the dark ground out
   from under P-003's reversed wordmark. Stacking does neither. The split is
   still a split - a split is what stacks when the viewport gets narrow - and
   the top of the band is now SOLID #1E1E1A rather than a photograph, which is
   a better ground for the reversed wordmark, not a worse one. What wave 9
   measured was three scrim ramps. This is not a fourth ramp; there is no type
   over the photograph at all any more, so there is nothing left to ramp.

   WHAT IT BUYS, on the numbers:
     - the photograph shows 100% of its horizontal frame on six of the seven
       pages and 83% on the seventh (whose source is 1.93:1, wider than the
       frame), undarkened, against 33-58% covered before;
     - the type moves onto the solid band, so the sampled contrast RISES;
     - the phone stops upscaling: the photo box runs 0.35-0.9x of native at
       320-430 where the full-band background was asking 1.34x.

   WHY IT IS SCOPED TO `orientation:portrait`. Stacking costs vertical space,
   and a portrait phone has vertical space to spend: the hero grows from ~0.72
   to ~0.98 screens at 430x932 and the whole picture still lands above the
   fold. A landscape phone does not - measured, the same rules put the band at
   1.9 screens on 844x390, so a rotated visitor would scroll two screens of
   hero. Landscape is handled in the block below by giving it the DESKTOP
   split, which is what a wide, short viewport actually wants and which is also
   strictly better than what it had. ===== */
@media (max-width:900px) and (orientation:portrait){
  .phero{display:flex;flex-direction:column;padding-bottom:0}
  .phero__ph{
    position:relative;order:2;z-index:auto;
    left:auto;right:auto;top:auto;bottom:auto;
    width:100%;margin-top:var(--s3);
    aspect-ratio:16/10;max-height:min(40vh,420px)
  }
  .phero__ph img{width:100%;height:100%;object-fit:cover}
  /* 16/10 is the house frame and it costs nothing on six of the seven sources,
     which are 1.33-1.50 and therefore crop on the VERTICAL - ceiling and floor,
     the least informative part of a room. /services/vacant-staging's source is
     1824x944 = 1.93, wider than the frame, so the same rule crops it on the
     HORIZONTAL and it was the one page still losing 17% of its width. It gets
     its own file's ratio: same principle as the resolution contract, which is
     "size the frame to the file", applied to composition instead of density. */
  .page-svc--vacant .phero__ph{aspect-ratio:1824/944}
  /* a short feather at the seam ONLY. The band hands off to the photograph
     instead of butting against it; below 14% the picture is untouched. */
  .phero__ph::after{background:linear-gradient(180deg,#1E1E1A 0%,rgba(30,30,26,0) 14%)}
}
/* A rotated phone is 844x390: wider than the desktop type column and shorter
   than the hero. That is the shape the SIDE-BY-SIDE split was drawn for, so
   landscape below 901px keeps the desktop panel and the desktop HORIZONTAL
   ramp - opaque under the type, open under the picture - rather than either
   the old full-bleed vertical scrim (82-88% covered) or the portrait stack
   (1.9 screens of hero). It takes the desktop 44/52 split unmodified, so the
   4% gutter that keeps type off the photograph is preserved rather than
   re-derived; the only thing restored here is `.phero__t`'s cap, which the
   shared max-width:900px block above releases for the portrait stack. */
@media (max-width:900px) and (orientation:landscape){
  .phero__t{max-width:min(52%,620px)}
}

/* ===== reduced motion: the pinned plate keeps its layout, drops the scale ===== */
@media (prefers-reduced-motion:reduce){
  .layers-bg img{transform:none!important}
}

/* ===== P-009 panel interior: text against a capped photo card.
   The brief asks for a real photo on each of the three routes; the pinned plate
   is one image behind all three. So each panel carries its own frame, capped at
   420px so a 960px source still runs ~2.3x on retina. A crisp bordered card over
   a scrimmed held plate is the Halston/Verity layering language, rebuilt. ===== */
.layer-panel__g{
  display:grid;grid-template-columns:1fr minmax(0,420px);gap:var(--s5);
  align-items:center;width:100%
}
.layer-panel__ph{position:relative;overflow:hidden;border-radius:var(--radius);box-shadow:0 30px 70px rgba(0,0,0,.48)}
.layer-panel__ph img{width:100%;height:auto;aspect-ratio:4/3;object-fit:cover}
@media (max-width:900px){
  .layer-panel__g{grid-template-columns:1fr;gap:var(--s3)}
  .layer-panel__ph{max-width:290px}
  .layer-panel{min-height:auto;padding:var(--s5) 0}
}

/* ===== /work frame budget.
   The project block is capped at 1010px, which is the documented ceiling for the
   1824px file (interactions.md). That cap then makes every frame inside it legal:
   the wide feature runs 1.11x on retina, the 2-up runs 1.03x, and the 3-up runs
   below native. Widening this block breaks the resolution contract. ===== */
.proj{max-width:1010px;margin-left:auto;margin-right:auto}
.feat{max-width:1010px;margin-left:auto;margin-right:auto}
.one{margin-left:auto;margin-right:auto}
.one--tall{max-width:560px}

/* .h-long is also used standalone (the /contact aside heading), not only inside
   .prose. Cinzel has no lowercase, so any heading that must run long is Lora. */
h2.h-long,h3.h-long{font-family:var(--font-b);letter-spacing:0;font-weight:600}
/* an aside heading is subordinate to the page H1 and should not compete with it.
   WAVE 6: scoped to .h-long. It used to be a bare `.util__g aside h2`, which is
   (0,1,2) and therefore outranked `.rule-label h2` at (0,1,1) - so the moment the
   "Reach Andrea" section label became an <h2> this wave, it resolved to 20.8px
   Lora with .22em tracking instead of the 11.5px tracked micro-label, on the
   conversion page. Caught by w6-typeprobe.cjs reading the RESOLVED style, not by
   any screenshot: the label still looked like a label, only three times too big.
   Both h2s in this aside that want the treatment carry .h-long. */
.util__g aside h2.h-long{font-size:1.3rem;line-height:1.35}

/* ==========================================================================
   WAVE 5 - cross-page distinctiveness.

   Two jobs. (1) The interior hero was ONE treatment on eight pages: same band
   height, same H1 step, same 50ch lede. Correct as a system and wrong as a
   page, because a hub, a working service page, a portfolio and a bio do not
   have the same amount of work to do above the fold. Each page role now gets
   its own rhythm off a body class. (2) Three of the four service detail pages
   were the same skeleton with different words (prose, four numbered steps,
   photo rail, FAQ, closer). Each now carries one element the others do not.

   What is NOT varied, deliberately: the type scale, the accent, the dark
   #1E1E1A ground, the photo panel cap. Those are the system. Varying them
   would read as three different sites rather than one site with pages.
   ========================================================================== */

/* ----- per-page hero rhythm -----------------------------------------------
   Vertical rhythm is scoped to `min-width:901px` on purpose. The mobile
   `.phero` padding lives inside `@media (max-width:900px)` at (0,1,0), and a
   page class beats it on specificity at every width, so an unscoped override
   here would silently leak the desktop band height onto a phone. Ask for the
   desktop band only when the desktop layout is on screen.

   H1 size and lede measure ARE unscoped: those should travel to mobile, and
   every one of them is checked on the 430px captures. */
@media (min-width:901px){
  /* the hub is the menu for the whole business: it gets the tallest band */
  /* a service detail is a working page. Shorter band, sooner to the answer. */
  /* new build carries a builder-client line under the CTAs, which is a fifth
     element the other heroes do not have, so the bottom pad comes off */
  /* /work exists to show the work. The hero gets out of the way fastest. */
  /* /about is a person. Narrower type column, so the line length suits a bio. */
  .page-about .phero__t{max-width:min(48%,580px)}
}
/* Mobile keeps the same ordering with a much smaller budget: the hub still gets
   the tallest band and /work still gets the shortest, but the spread is ~40px
   rather than ~50, because a phone fold cannot afford the desktop difference. */
@media (max-width:900px){
  /* the two per-page BOTTOM pads that used to live here are gone as of wave 10.
     Below 901px the band now ends at the photograph, so its bottom padding is
     0 for every page and a page-class override at (0,2,0) would beat the
     restack's `.phero{padding-bottom:0}` at (0,1,0) and leave a dark strip
     under the picture. Top-pad rhythm is unaffected and still varies per page. */
}
.page-hub .phero .lede{max-width:44ch}
.page-svc .phero .lede{max-width:54ch}
/* occupied staging's lede is a single sentence, so it can carry standfirst
   weight; the other three run to two sentences and stay at body size */
.page-svc--occupied .phero .lede{font-size:1.14rem;max-width:46ch}
/* interior design is the secondary service and says so: it is the one interior
   H1 that does not take the full display step */
.page-svc--design .phero .lede{max-width:50ch}
.page-work .phero .lede{max-width:46ch}
.page-about .phero .lede{max-width:58ch}

/* ----- .ledger : the stays/moves comparison. /services/occupied-staging ONLY.
   The whole objection to occupied staging is "what happens to my things", and
   the page answered it in a paragraph inside an accordion. A two-column
   ledger split by a vertical hairline answers it at a glance. The hairline
   split is Halston's move, already recorded in interactions.md as this site's
   structural vocabulary, used here for the first time on content rather than
   on a section label. Every line is drawn from the page's own FAQ answer;
   nothing about the process is asserted here that is not already on the page.
   -------------------------------------------------------------------------- */
.ledger{display:grid;grid-template-columns:1fr 1fr;max-width:900px;margin:0 auto}
.ledger__c{padding:0 var(--s4)}
.ledger__c:first-child{padding-left:0;border-right:1px solid var(--line-soft)}
.ledger__c:last-child{padding-right:0}
.ledger__h{
  font-size:.72rem;letter-spacing:.2em;text-transform:uppercase;font-weight:700;
  color:var(--accent);padding-bottom:14px;border-bottom:1px solid var(--line-soft);
  margin:0
}
.ledger ul{list-style:none;margin:0;padding:0}
.ledger li{
  padding:15px 0;border-bottom:1px solid var(--line-soft);
  font-size:1rem;line-height:1.55;color:var(--ink)
}
.ledger li:last-child{border-bottom:0}
@media (max-width:760px){
  /* Stacked, the two halves risk reading as two unrelated lists rather than as
     a comparison. A bracketing left hairline was tried and reverted: measured on
     a 430px capture it collided with the row rules and, because
     `.ledger__c:first-child{padding-left:0}` outranks a plain `.ledger__c`, the
     STAYS text sat flush against its own rule while MOVES was inset. What
     actually holds the pair together at this width is proximity, so the gap
     closes from 52px to 32px and the two heads stay 32px apart under their own
     rules. The desktop hairline split carries the comparison where there is
     room for it. */
  .ledger{grid-template-columns:1fr;gap:var(--s3)}
  .ledger__c{padding:0}
  .ledger__c:first-child{padding-left:0;border-right:0}
}

/* ----- .anno : the annotated frame. /services/vacant-staging ONLY.
   The page argues that scale is the discipline, and then showed four photos
   with no way to see the argument in any of them. Numbered markers sit on the
   frame and the notes sit beside it, so nothing is set over the photograph:
   no text on the image means no contrast risk and no scrim spent on a picture
   this site is selling. Every note describes something visible in the frame.
   The frame is capped at 660px because the source is 1246px wide
   (1246 / (660 x 2) = 0.94, so ~1.06x on retina, inside the resolution table's
   ~690px ceiling for a 1246px file with room to spare). -------------------- */
.anno{
  display:grid;grid-template-columns:minmax(0,660px) minmax(0,1fr);
  gap:var(--s5);align-items:center;max-width:1060px;margin:0 auto
}
.anno__f{position:relative;margin:0}
/* The source file carries a baked-in photo border: rows 0-5 white, 6-8 black at
   the top, and 834-839 black, 840-845 white at the bottom (measured off a canvas
   read of the real file, not eyeballed). Everywhere else on the site that border
   is invisible because `.tile` and `.feat` crop to 4/3 with object-fit. This is
   the one frame shown whole, so it showed the border, and a black letterbox bar
   is not the mount this site uses. 1246/818 crops 14px off each end with
   object-position centre, which clears 9px of border at the top and 12px at the
   bottom with a little image to spare. */
.anno__f img{
  width:100%;height:auto;display:block;border-radius:var(--radius);
  aspect-ratio:1246 / 818;object-fit:cover;object-position:center;
  outline:1px solid color-mix(in srgb, var(--ink) 15%, transparent);outline-offset:-1px;
  box-shadow:var(--frame-rest)
}
.anno__m{
  position:absolute;transform:translate(-50%,-50%);
  width:32px;height:32px;border-radius:50%;display:grid;place-items:center;
  background:var(--accent);color:#fff;
  font-family:var(--font-h);font-size:.8rem;font-weight:600;line-height:1;
  box-shadow:0 0 0 3px rgba(255,255,255,.9),0 6px 18px rgba(0,0,0,.3)
}
.anno__l{list-style:none;margin:0;padding:0}
.anno__l li{
  display:grid;grid-template-columns:auto 1fr;gap:var(--s2);align-items:baseline;
  padding:15px 0;border-top:1px solid var(--line-soft);
  font-size:1rem;line-height:1.6;color:var(--ink)
}
.anno__l li:last-child{border-bottom:1px solid var(--line-soft)}
.anno__n{font-family:var(--font-h);font-weight:600;color:var(--accent);font-size:.96rem}
@media (max-width:900px){
  .anno{grid-template-columns:1fr;gap:var(--s3);max-width:660px}
  /* a 32px disc on a 366px frame reads as a button rather than a map pin, and
     nothing here is tappable. Smaller, with a lighter lift. */
  .anno__m{width:26px;height:26px;font-size:.72rem;
    box-shadow:0 0 0 2px rgba(255,255,255,.9),0 4px 12px rgba(0,0,0,.28)}
}

/* ----- .sched : the build-schedule spine. /services/new-build-staging ONLY.
   A builder does not read four numbered steps, they read a date. The same
   four beats are re-cut as a schedule with a "when" column, so the page reads
   like a trade slot on a build calendar rather than a homeowner walkthrough.
   Every "when" is lifted from copy already on the page. -------------------- */
.sched{max-width:880px;margin:0 auto}
.sched__i{display:grid;grid-template-columns:minmax(0,196px) 1fr;align-items:start}
.sched__w{
  text-align:right;padding:1px var(--s3) 0 0;
  font-size:.72rem;letter-spacing:.18em;text-transform:uppercase;font-weight:700;
  color:var(--accent);line-height:1.75
}
.sched__b{position:relative;border-left:1px solid var(--line);padding:0 0 var(--s4) var(--s4)}
.sched__i:last-child .sched__b{border-left-color:transparent;padding-bottom:0}
.sched__b::before{
  content:"";position:absolute;left:-4px;top:7px;width:9px;height:9px;border-radius:50%;
  background:var(--accent);box-shadow:0 0 0 4px var(--bg)
}
.sched__b h3{margin-bottom:8px}
.sched__b p{color:var(--ink);font-size:1rem;max-width:54ch}
@media (max-width:760px){
  .sched__i{
    grid-template-columns:1fr;position:relative;
    padding-left:var(--s3);border-left:1px solid var(--line)
  }
  .sched__i:last-child{border-left-color:transparent}
  .sched__i::before{
    content:"";position:absolute;left:-4px;top:6px;width:9px;height:9px;border-radius:50%;
    background:var(--accent);box-shadow:0 0 0 4px var(--bg)
  }
  .sched__w{text-align:left;padding:0 0 6px 0}
  .sched__b{border-left:0;padding:0 0 var(--s3) 0}
  .sched__b::before{display:none}
  .sched__i:last-child .sched__b{padding-bottom:0}
}

/* ============ Blog (content/blog → /blog/), LocalCraft blog kit =============
   Append this block to the site's styles.css. Uses the site's existing
   tokens (--accent, --accent-2, --accent-3, --ink, --ink-soft, --ink-deep,
   --surface, --line, --s2..--s6, --radius, --radius-lg, --font-heading) and
   shared classes (.container, .eyebrow, .lede, .card, .card-grid, .offer-link,
   .band-surface, .page-hero, .ty-hero). No new tokens required. */
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; }
.blog-measure { max-width: 760px; }
.blog-post-head { padding-top: var(--s6); }
.blog-post-head h1 { font-size: clamp(2.1rem, 4.4vw, 3.2rem); line-height: 1.08; margin: 0 0 var(--s2); }
.blog-kicker-link { color: inherit; text-decoration: none; }
.blog-meta { color: var(--ink-soft); font-size: 0.95rem; }
.blog-hero-figure { margin: var(--s4) 0 0; }
.blog-hero-figure img { width: 100%; height: auto; border-radius: var(--radius-lg); display: block; box-shadow: 0 10px 34px rgba(22,8,32,0.10); }
.blog-prose { font-size: 1.075rem; line-height: 1.72; color: var(--ink); margin-top: var(--s4); }
.blog-prose > * + * { margin-top: 1.1em; }
.blog-prose h2 { font-size: clamp(1.5rem, 2.6vw, 1.9rem); line-height: 1.2; margin-top: 2em; }
.blog-prose h3 { font-size: 1.25rem; margin-top: 1.6em; }
.blog-prose a { color: var(--accent); text-underline-offset: 2px; }
.blog-prose ul, .blog-prose ol { padding-left: 1.3em; }
.blog-prose li + li { margin-top: 0.4em; }
.blog-prose blockquote { border-left: 4px solid var(--accent-3); padding-left: var(--s3); color: var(--ink-soft); font-style: italic; }
.blog-prose img { max-width: 100%; height: auto; border-radius: var(--radius); }
.blog-prose strong { color: var(--ink-deep); }
.blog-faq { max-width: 820px; }
.blog-faq-list { margin-top: var(--s4); display: flex; flex-direction: column; gap: 12px; }
.blog-faq-item { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; }
.blog-faq-item summary { cursor: pointer; list-style: none; padding: var(--s3); font-weight: 600; font-family: var(--font-heading); display: flex; justify-content: space-between; align-items: center; gap: var(--s3); }
.blog-faq-item summary::-webkit-details-marker { display: none; }
.blog-faq-item summary::after { content: "+"; color: var(--accent); font-size: 1.4rem; line-height: 1; }
.blog-faq-item[open] summary::after { content: "−"; }
.blog-faq-a { padding: 0 var(--s3) var(--s3); color: var(--ink-soft); }
.blog-faq-a p { margin: 0; }
.blog-card .num { color: var(--accent-2); text-transform: none; letter-spacing: 0; font-weight: 600; }

/* ============ Blog bridge, Cricket on the Hearth =============================
   The kit's blog.css and build-blog.mjs are written against the fleet-default
   class/token contract (.container, .card-grid, .page-hero, .band-surface,
   .offer-link, --font-heading, --ink-deep, --accent-3, --radius-lg). THIS build
   does not use that contract: it uses .wrap, .three, .phero, .sec--surface,
   .card__more, --font-h, --ink-strong, --accent-2, --radius. Nothing above is
   edited. This block maps the kit's names onto the names this site already has,
   so /blog renders in the site's own system rather than unstyled.
   Anything the site already defines (.eyebrow, .lede, .card, .sr-only) is NOT
   redefined here. ========================================================== */
:root{
  --font-heading:var(--font-b);   /* long headings are Lora on this build, never Cinzel */
  --ink-deep:var(--ink-strong);
  --accent-3:var(--accent-2);
  --radius-lg:var(--radius);      /* 2px everywhere; this is a square-cornered design */
}
/* the kit emits .container; this site's wrapper is .wrap (site.css:148) */
.container{max-width:var(--maxw);margin:0 auto;padding:0 var(--s3)}

/* /blog index hero. The utility-page pattern (/privacy, /terms): solid header
   from load, light band, no photographic slab. build-blog.mjs ships the blog
   pages with body.has-solid-header + .site-header.is-solid to match, so main
   already carries the 82px header clearance. */
.page-hero.ty-hero{padding:var(--s5) 0 var(--s4)}
.page-hero.ty-hero h1{margin-top:var(--s2)}
.page-hero.ty-hero .lede{margin-top:var(--s2)}
.band-surface{background:var(--surface);padding:var(--s5) 0;margin-top:var(--s5)}

/* post grid on /blog. Same 3-up rhythm and breakpoints as .three (site.css:528,
   880). Blog cards have no photograph, so unlike the service cards they need a
   box: the frame tokens give them the site's own mount rather than a new one. */
.card-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:var(--s4);margin-top:var(--s4)}
@media (max-width:900px){ .card-grid{grid-template-columns:1fr} }
.card.blog-card{
  background:var(--surface);padding:var(--s3);border-radius:var(--radius);
  box-shadow:var(--frame-ring),var(--frame-rest);
  transition:box-shadow var(--dur-hover) var(--ease-hover),
             translate var(--dur-hover) var(--ease-hover)
}
@media (hover:hover){
  .card.blog-card:hover{translate:0 -4px;box-shadow:var(--frame-ring),0 18px 38px rgba(51,51,47,.14)}
}
.card.blog-card:focus-visible{translate:0 -4px}
.blog-card .num{font-size:.72rem;letter-spacing:.16em;text-transform:uppercase;font-weight:600}
.blog-card h3{margin:12px 0 10px;font-family:var(--font-b);letter-spacing:0;font-size:1.18rem}
.blog-card p{color:var(--ink);font-size:1rem;max-width:none;flex:1}

/* the kit's read-more span; this site's is .card__more (site.css:570) */
.offer-link{
  display:inline-flex;align-items:center;gap:8px;margin-top:var(--s2);
  font-size:.78rem;letter-spacing:.16em;text-transform:uppercase;font-weight:700;
  color:var(--accent);text-decoration:none;
  transition:color var(--dur-hover) var(--ease-hover)
}
.offer-link::after{content:"";width:26px;height:1px;background:var(--accent);
  transition:width var(--dur-hover) var(--ease-hover)}
@media (hover:hover){ .card:hover .offer-link::after{width:42px} }

/* Post typography. Cinzel is caps-only, so a heading that runs to a sentence is
   Lora on this build, per the site's own rule (h2.h-long,h3.h-long near the foot of
   this file). A post title and its section headings are exactly that case. */
.blog-post-head h1{font-family:var(--font-b);letter-spacing:0;font-weight:600;text-wrap:balance}
.blog-prose h2,.blog-prose h3{font-family:var(--font-b);letter-spacing:0;font-weight:600}
.blog-prose h2{font-size:1.34rem}
.blog-prose h3{font-size:1.06rem}
.blog-prose li::marker{color:var(--accent)}
/* the long-form text-wrap rule at site.css:136 predates the blog; extend it
   rather than editing it in place */
.blog-prose p,.blog-prose li,.blog-faq-a p{text-wrap:pretty}
/* the kit's hero-figure shadow is a cool purple; this build mounts photographs
   with --frame-ring/--frame-rest and nothing else */
.blog-hero-figure img{box-shadow:var(--frame-ring),var(--frame-rest)}
.blog-faq-item summary{letter-spacing:0}

/* ================================================================
   WAVE 8 - final blemish hunt. Nothing below changes a colour, a
   typeface or a layout decision; every rule here either grows a
   touch target, raises a reading floor, or adds the one flourish
   item 8.2 asks for.
   ================================================================ */

/* item 8.2 - the site's one handwritten-style byline, on /about and nowhere
   else. Set in Lora ITALIC, which is already loaded (it carries the italic
   accents elsewhere on the site), so it costs no payload and introduces no
   third family. design.md names an old-style swash serif as the mark's own
   second voice; this is the open-licensed face already on the page, used once.

   Two honesty notes, because a sign-off block is the easiest place on a site
   to break §25b by accident. It is TYPESET, not a reproduction of Andrea's
   handwriting, and it carries no words attributed to her: it is a byline
   closing her own section, in the same register as an editorial colophon. No
   quotation, no testimonial, no claim. */
.signature-script{
  font-family:var(--font-b);font-style:italic;font-weight:500;
  font-size:clamp(1.7rem,1.1vw + 1.42rem,2.15rem);line-height:1.1;
  letter-spacing:.005em;color:var(--accent);
  margin:var(--s3) 0 0;padding-top:var(--s2);
  border-top:1px solid var(--line);max-width:14ch;
  text-wrap:balance
}
/* No max-width here, and that is the fix rather than an omission. The first
   version carried `max-width:34ch`, and `ch` resolves against the element's OWN
   font-size: at 11.52px that is 247px, not the ~390px the number implies. It
   clamped a 29-character line into two and broke "Cricket / on the Hearth"
   across them, which is the same defect wave 7's gut-test caught on "REAL
   ESTATE / STAGING ASSOCIATION". Measured, not reasoned about: box 247px inside
   a 311px column at 375. The line is short enough to govern itself. */
.sig-role{
  font-size:.72rem;letter-spacing:.18em;text-transform:uppercase;font-weight:600;
  color:var(--ink-soft);margin:9px 0 0;line-height:1.7;
  text-wrap:balance
}

/* item 8.4 - tap targets. Measured at 430 CSS px with the iPhone UA, twelve
   link classes rendered under 44px tall, most of them 20px: the footer menus,
   the service-page breadcrumb, the three card action labels, the proof-strip
   source links and the two prose phone numbers.

   The fix is real padding rather than a ::after hit-slop, and that is a
   decision rather than a default. The footer menu rows sit on a 29px pitch, so
   a 44px invisible slop on each would OVERLAP its neighbours, and a tap landing
   in the overlap goes to whichever element paints last - unpredictable, and
   worse than a small target. Growing the row pitch to 44 is the only fix that
   is honest at that spacing. Where the pitch is not the constraint (the card
   labels, the crumb) the padding is subtracted back out of the element's own
   margin, so the rendered rhythm is unchanged. */
.ftr li{margin-bottom:0}
.ftr li a{display:inline-block;min-width:44px;padding:12px 0;line-height:1.35}
.ftr__legal a,.lcd a{display:inline-block;padding:12px 0}
.ftr__legal{padding-top:calc(var(--s3) - 12px)}
.site-footer a[href^="tel:"],.site-footer a[href^="mailto:"]{display:inline-block;padding:12px 0}
/* min-width matters as much as height here: "Blog" set 32.9px wide and "Home"
   43.5px, so both cleared the 44px floor vertically and missed it horizontally.
   The box grows to the right into space the column already had, so nothing moves. */
.crumb a{display:inline-block;min-width:44px;padding:14px 0}
.crumb{margin-bottom:calc(var(--s2) - 14px)}
.card__more{padding:12px 0;margin-top:calc(var(--s2) - 12px)}
/* INLINE, not inline-block, and that distinction is a defect this wave caused
   and then measured. `display:inline-block` made the whole 30-character link
   ("Real Estate Staging Association") one atomic box, so it could no longer
   break internally and stranded the following word "affiliated" alone on the
   last line - a P-005 orphan, MANDATORY under §7, on all three breakpoints.
   orphans.cjs caught it: 187 elements measured, 3 orphans, all this one.
   Vertical padding on an INLINE element still grows the hit box (the line-box
   fragments carry the padding in their border boxes), so the target goes from
   127x40.3 to 128x52.3 and the label breaks internally again. Measured both
   ways in w8-proof.cjs before choosing. */
.proof__l a{padding:6px 0}
.nap__v a{display:inline-block;padding:11px 0;margin:-11px 0}
.nap li{padding:calc(var(--s2) - 4px) 0}

/* item 8.6 - the form's field labels are the one tracked-caps run on this site
   that the checklist names explicitly as a conversion surface, and they were
   11.84px. Raised to the 14px floor; the tracking comes down from .14em to
   .12em so the label's set width barely moves and none of the seven wraps that
   did not wrap before. The site's other tracked structural labels keep the
   11.5px floor documented at the head of this file - see POLISH-LOG wave 8 for
   the measurement that made raising the credential lines a regression. */
.field label{font-size:.875rem;letter-spacing:.12em}

/* item 8.7 - the dock yields. A fixed bottom bar will always eventually sit on
   top of something; the question is whether it ever sits on top of a button.
   Measured first: on six of eleven pages it covered a primary CTA by 22-58px at
   some scroll position. So the dock now retracts while any primary CTA is
   inside the band it occupies, and comes back when the band is clear. The state
   is driven from motion.js; this rule is only the off position, and it is the
   same transform the hidden state already used so the two cannot disagree. */
.callbar.is-visible.is-yielding{transform:translateY(calc(100% + 48px + env(safe-area-inset-bottom,0px)))}

/* item 8.5 / gut-test - the dock's two labels wrapped to TWO LINES on every
   phone narrower than about 420px, which is most of them. Wave 7 built and
   measured this component at 430 and it is clean there, so nothing caught it:
   "Call 618-205-8350" and "Book a walkthrough" both broke mid-label and the
   dock grew from 55px to 81px, eating a third more of the screen to say the
   same thing worse.

   Measured, not estimated (w8-dock.cjs, five widths x three candidates): at
   375 the shipped rule gives each button 167px against a label that needs 174.
   Trimming the bar's own side padding to 10px, the gap to 8 and the button's
   horizontal padding to 8 returns 174px per button and both labels sit on one
   line from 360px up. The type comes down from .92rem to .875rem, which is
   exactly item 8.6's 14px floor and not a pixel under it.

   Stated limit: at 320px (iPhone SE 1st generation) both labels still wrap. Two
   buttons of this length cannot share a 320px row at a 14px floor, and 320 is
   below both this item's stated 375 and the §15 mobile breakpoint of 430.

   WAVE 9 lifts that limit, and the reason it stood is worth more than the fix:
   the number behind it was wrong. Wave 8's probe measured the label's required
   width by CLONING the button and copying `getComputedStyle(el).cssText` onto
   the clone. Chromium returns an EMPTY cssText from a computed-style object, so
   the clone rendered in the UA default face and reported a requirement ~50px
   over the truth. Measured again on the real element (force nowrap, read the
   box, restore) the 320px shortfall is +15px on the call button and +7px on the
   walkthrough button. That is a trim, not an impossibility. An estimate that
   says "impossible" deserves a second measurement before it becomes doctrine. */
@media (max-width:420px){
  .callbar{padding-left:10px;padding-right:10px}
  .callbar__g{gap:8px}
  .callbar .btn{padding:14px 8px;font-size:.875rem;gap:6px}
}
/* `.cb-lbl` wraps the whole dock label, and it exists because of a defect I
   shipped inside this wave and caught in the 430px capture. `.btn` is
   `display:inline-flex` with `gap:8px`. Marking up just the verb as
   `<span class="cb-verb">Call </span>618-205-8350` made that span a FLEX ITEM,
   so flex gap inserted 8px between "Call" and the number on every phone: the
   dock read "Call  618-205-8350" with a visible double space. Wrapping verb and
   number together restores ONE text flex item beside the icon, and the verb goes
   back to being an ordinary inline span that can be clipped without disturbing
   the layout around it. The lesson is small and general: inside a flex
   container, adding a span is adding a column. */
.callbar .cb-lbl{display:inline}

/* The 320-359 step. Two changes, sized to the deficit above.
   1. Another 4px of bar padding, 2px of gap and 2px a side of button padding:
      about 9px back per button, which alone clears the walkthrough label.
   2. The word "Call" goes VISUALLY hidden and the phone glyph carries the verb.
      It stays in the accessible name - `.cb-verb` is clipped, not `display:none`
      - so a screen reader still announces "Call 618-205-8350" and §7.7's "the
      copy is the action, not a noun" holds where it is actually read. Sighted
      users at this width get an icon plus a phone number on a tel: link, which
      is unambiguous. That buys ~30px, well past the 15px needed.
   The type does NOT come down. 14px is item 8.6's floor and the floor is the
   whole point of a floor. */
@media (max-width:359px){
  .callbar{padding-left:6px;padding-right:6px}
  .callbar__g{gap:6px}
  /* letter-spacing goes to 0 here and it is the last 5px. With the padding trim
     alone "Book a walkthrough" needed 137.2px of content box against 137.0 - it
     wrapped by two tenths of a pixel. `.btn` tracks at .02em, which is 0.28px a
     glyph and 5px across an 18-character label: invisible at 14px, and more
     than the shortfall. Shaving another 2px off the bar padding would have
     worked too and would have put the dock's buttons within 4px of the screen
     edge, which looks like a mistake. */
  .callbar .btn{padding:14px 6px;gap:5px;letter-spacing:0}
  .callbar .cb-verb{position:absolute;width:1px;height:1px;padding:0;margin:-1px;
    overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}
}

/* WAVE 10 - the dock against a SHORT viewport. Every previous measurement of
   this bar was taken on a 900-932px-tall screen, where 81.3px is 8.7% of the
   view and invisible as a cost. Rotate the same phone and the identical bar is
   20.8% of 390px, and 22.6% of a 360px 740-wide handset: a fifth of the screen
   is permanently a call bar on a page whose whole job is to be read.
   The trim is bounded by item 8.4, not by taste. The buttons measure 55.3px and
   the floor is 44, so there are 11px to spend and no more: 4px off each of the
   button's vertical pads and 4/6px off the bar's own. Measured after, at
   844x390: 63.3px = 16.2% of the view, buttons 47.3px, both labels still on one
   line. The type is untouched - 8.6's 14px floor is not a trim surface.
   Scoped to max-width:900px as well as the height, because above 900 the dock
   is `display:none` and only the footer's dock-clearance would have moved. */
@media (max-width:900px) and (orientation:landscape) and (max-height:460px){
  .callbar{padding-top:8px;padding-bottom:max(8px,env(safe-area-inset-bottom))}
  .callbar .btn{padding-top:10px;padding-bottom:10px}
  .site-footer{padding-bottom:calc(78px + env(safe-area-inset-bottom))}
}

/* WAVE 9 - the closer band's own narrow step. The olive band spends 32px a side
   on padding, which is right at 430 and wrong at 320: it leaves a 256px column,
   and a service-specific CTA cannot be a short label (§7.6 requires the service
   name in it). Both of the three-word services rendered edge-to-edge AND broke
   to two lines there, which is the exact shape §7b-5 rejects.
   Two changes, both measured rather than guessed: the band gives back 12px a
   side, and the button gives back 8px a side. Together that is 40px of relief on
   a 256px column, and it is what turns a wrapped full-bleed slab back into a
   single-line button with air around it. The copy did the rest - the four
   service closers went from "Request an occupied staging quote" to "Quote my
   occupied staging", which is a third shorter, still names the service, and
   picks up the verb from the sentence directly above it ("Andrea walks the
   property, writes the plan, and quotes the work"). */
@media (max-width:420px){
  .closer{padding-left:var(--s2);padding-right:var(--s2)}
  .closer .btn{padding-left:18px;padding-right:18px}
  /* the submit is `width:100%`, so its horizontal padding does not size the
     button at all - it only takes room away from the label. 26px a side was
     spending 52px of a 214px button to say nothing, and "Request a walkthrough"
     broke in half inside its own submit. Free money at this width. */
  .form .btn{padding-left:18px;padding-right:18px}
}


/* ===== proof-strip roll-up (P-008 family).
   The numeral counts up when the strip enters view. Non-numeric cells (RESA) have
   nothing to count, so they take the same fade-and-rise and no counter. The reserved
   width stops the row reflowing as digits change, which would jitter the whole strip. */
.proof__n{font-variant-numeric:tabular-nums}
html.js-motion .proof__n[data-count]{visibility:hidden}
html.js-motion .proof__n[data-count].is-rolling,
html.js-motion .proof__n[data-count].is-done{visibility:visible}
@media (prefers-reduced-motion:reduce){
  html.js-motion .proof__n[data-count]{visibility:visible}
}


/* ===== P-009 panel emergence.
   The panels rise out of the held plate instead of sliding across it. JS writes the
   transform per frame; these rules only declare the promotion and the no-JS contract.
   Visible by default: without JS there is no inline transform and the panel simply
   sits where it belongs. Reduced motion clears anything JS may have written. */
html.js-motion .layer-panel__g{will-change:transform,opacity}
@media (prefers-reduced-motion:reduce){
  html.js-motion .layer-panel__g{transform:none!important;opacity:1!important;will-change:auto}
}

/* ==========================================================================
   TESTIMONIALS (added 2026-07-31, once real ones existed)
   For most of this build there were none, by necessity: the 164 Google reviews
   belong to Cricket Moves LLC, a legally separate company, so §25b barred every
   one of them. Three genuine Facebook recommendations for the STAGING business
   were supplied by the operator and recorded in
   brand-kit/knowledge-docs/facebook-recommendations.md as the source record.

   Facebook's format is "recommends", a binary, NOT a star scale. So there is no
   star row here, no numeric rating, and no aggregateRating in the schema. The
   quotes carry themselves. Do not add stars later: the source does not have them.
   ========================================================================== */
.says{max-width:1010px;margin:0 auto}
.says__lead{
  background:var(--surface);border:1px solid var(--line-soft);border-radius:var(--radius);
  padding:var(--s5) var(--s4);text-align:center
}
/* The outcome is the single most persuasive thing anywhere in this project, so it
   leads the section at display size. It sits INSIDE the quotation and directly
   above its attribution, so it always reads as the customer's words rather than
   as a claim the site is making. */
.says__num{
  font-family:var(--font-h);font-weight:600;color:var(--accent);
  font-size:clamp(1.9rem,3.4vw + .6rem,3.1rem);line-height:1.1;letter-spacing:.02em;
  margin:0 auto;max-width:14ch
}
.says__q{
  font-size:1.06rem;line-height:1.75;color:var(--ink);
  max-width:62ch;margin:var(--s3) auto 0
}
.says__by{
  margin-top:var(--s3);font-size:.78rem;letter-spacing:.16em;text-transform:uppercase;
  font-weight:700;color:var(--ink-strong)
}
.says__src{
  display:block;margin-top:7px;font-size:.72rem;letter-spacing:.14em;text-transform:uppercase;
  font-weight:600;color:var(--ink-soft)
}
.says__src a{color:var(--accent);text-decoration:none;border-bottom:1px solid transparent}
@media (hover:hover){ .says__src a:hover{border-bottom-color:var(--accent)} }
.says__two{display:grid;grid-template-columns:1fr 1fr;gap:var(--s3);margin-top:var(--s3)}
.says__c{
  background:var(--surface);border:1px solid var(--line-soft);border-radius:var(--radius);
  padding:var(--s4) var(--s3)
}
.says__c .says__q{font-size:1rem;margin-top:0;max-width:none}
.says__c .says__by{margin-top:var(--s2)}
/* opening quote mark as a mark, not a glyph in the reading line */
.says__c::before,.says__lead::before{
  content:"\201C";display:block;font-family:var(--font-h);font-size:2.4rem;line-height:.6;
  color:var(--accent-2);margin-bottom:var(--s2)
}
@media (max-width:760px){
  .says__two{grid-template-columns:1fr}
  .says__lead{padding:var(--s4) var(--s3)}
}

/* ===== INDUSTRY STATISTICS (added 2026-07-31 at the operator's request).
   These are NATIONAL figures from the National Association of Realtors, NOT Cricket's
   own results, and the section is built so that distinction is impossible to miss:
   the attribution names the source, the year and the report, sits directly under the
   numbers, and links out to NAR. §25b governs a sourced third-party statistic exactly
   as it governs a review - it may be published only with the source attached.
   Do not restyle this so the attribution becomes decorative. It is load-bearing. ===== */
.stats{max-width:1010px;margin:0 auto}
.stats__g{display:grid;grid-template-columns:repeat(3,1fr);gap:var(--s4)}
.stats__i{text-align:center}
.stats__n{
  font-family:var(--font-h);font-weight:600;color:var(--accent);
  font-size:clamp(2.6rem,4.6vw + .5rem,4.2rem);line-height:1;letter-spacing:.01em;
  font-variant-numeric:tabular-nums
}
.stats__l{
  margin-top:var(--s2);color:var(--ink);font-size:1rem;line-height:1.6;
  max-width:26ch;margin-left:auto;margin-right:auto
}
.stats__src{
  margin-top:var(--s4);padding-top:var(--s3);border-top:1px solid var(--line-soft);
  text-align:center;font-size:.84rem;color:var(--ink-soft);line-height:1.7
}
.stats__src a{color:var(--accent);font-weight:600;text-decoration:none;
  border-bottom:1px solid transparent}
@media (hover:hover){ .stats__src a:hover{border-bottom-color:var(--accent)} }
@media (max-width:760px){
  .stats__g{grid-template-columns:1fr;gap:var(--s4)}
  .stats__l{max-width:32ch}
}

/* ===== The statistics band gets its own ground.
   It sat between two sections and inherited --bg from both, so it read as more of the
   same page rather than as the one place the argument is made with outside evidence.
   #1E1E1A is not a new colour: it is the footer and interior-hero ground, so this is a
   third ground the site already owns rather than a fourth one invented for a band.
   The numerals take #C5DA96, the same light green that carries accents on every other
   dark surface here. Measured on rendered pixels, not assumed. ===== */
.stats-band{background:#1E1E1A}
.stats-band .eyebrow{color:#C5DA96}
.stats-band h2{color:#fff}
.stats-band .lede{color:rgba(255,255,255,.86)}
.stats-band .stats__n{color:#C5DA96}
.stats-band .stats__l{color:rgba(255,255,255,.88)}
.stats-band .stats__src{
  color:rgba(255,255,255,.62);border-top-color:rgba(255,255,255,.16);text-align:center
}
.stats-band .stats__src a{color:#C5DA96}
@media (hover:hover){ .stats-band .stats__src a:hover{border-bottom-color:#C5DA96} }


/* ===== ONE height for every interior hero, desktop and up.
   min-height was not enough and the measurement says why: it only raises the SHORT
   pages. /services and the four service pages carry more content (a breadcrumb, a meta
   line, a longer lede) so they sat above any floor at their own content heights, and the
   spread stayed at 90px. A fixed box removes copy length from the equation entirely.
   640px clears the tallest content (vacant-staging, 472px) plus the uniform 152px of
   padding with slack. Verified: no page clips. ===== */
@media (min-width:901px){
  .phero{height:clamp(640px,70vh,780px);min-height:0}
}


/* ===== Interior hero type, sized so a keyword H1 sets on ONE line.
   The H1s were rewritten to lead with the search term ("Home staging services",
   "Occupied home staging"), which is right for SEO and longer than the evocative
   phrases they replaced. At the homepage's 72px in Cinzel caps, 21 characters needs
   ~940px against a 640px column, so every one of them wrapped to two lines and pushed
   the CTA row to the floor of a fixed-height hero.
   Two changes, both needed: a wider measure, and a display size scaled for an interior
   page rather than for the homepage showpiece. The homepage H1 is untouched. ===== */
@media (min-width:901px){
  .phero__t{max-width:min(68%,800px)}
  .phero h1{font-size:clamp(2.1rem,2.9vw + .45rem,3.35rem)}
}

/* ===== The About portrait, and P-010 finally has its target.
   P-010 has been DORMANT for this entire build. Its two sanctioned targets were the
   homepage hero, removed at Step 1 after the frame-cost measurement, and this
   portrait, which did not exist because no headshot had been supplied. It was never
   reassigned to a work photograph. The photo landed 2026-07-31, so the pattern wakes
   up on the element it was specified for.
   Source is 576x995 stepped-upscaled to 900w, so a 380px frame runs under native at
   2x. Do not widen past 400px. ===== */
.portrait{position:relative;overflow:hidden;border-radius:var(--radius);max-width:380px}
.portrait img{
  width:100%;height:auto;aspect-ratio:576/995;object-fit:cover;display:block;
  /* the 6% overscan is what stops an edge gap appearing at the drift extremes */
  transform:scale(1.06)
}
.portrait figcaption{
  margin-top:14px;font-size:.72rem;letter-spacing:.16em;text-transform:uppercase;
  font-weight:700;color:var(--ink-soft)
}
html.js-motion .portrait img{will-change:transform}
@media (prefers-reduced-motion:reduce){
  html.js-motion .portrait img{transform:scale(1)!important;will-change:auto}
}
@media (max-width:1000px){ .portrait{max-width:min(88vw,380px);margin:0 auto} }
