/* Grand Design Build — the v2 homepage stylesheet.

   ⚠ THE TOKEN BLOCK IS SCOPED TO body.v2, NOT :root, AND MUST STAY THAT WAY.
   The live site's own tokens.css declares --ink-700/600/500/300 on :root as
   a COOL GREY ladder (#3c434a, #54595f, #6b747d, #b6bcc2); this file
   declares the same four names as a WARM ladder (#4e423c, #6e5f58, #72635a,
   #cebeb5). Both are :root at equal specificity, so whichever loads last
   wins — and Astro's bundle loads after this file. On :root, every warm
   colour on the page silently resolved to the live grey: measured, .sec-sub
   came out #3c434a instead of #4e423c.

   body.v2 is (0,1,1) against :root's (0,1,0), so it wins on specificity and
   is immune to load order. Custom properties inherit, so everything inside
   <body class="v2"> gets the warm ladder and the rest of the site is
   untouched. */
body.v2{
  --ink-900:#000;--ink-800:#281f1a;--ink-700:#4e423c;--ink-600:#6e5f58;--ink-500:#72635a;
  --ink-400:#a7968c;--ink-300:#cebeb5;--ink-200:#e8d9d1;--ink-100:#f2e8e1;--ink-50:#f6eee6;--white:#fcf6f2;
  --accent:#f78c1e;--accent-600:#cb7319;--accent-tint:#feeedd;
  --fh:'Inter Tight','Helvetica Neue',Helvetica,Arial,sans-serif;
  --fd:'Lora','Palatino Linotype',Palatino,serif;
  --fb:'Lato','Helvetica Neue',Arial,sans-serif;
  /* Section grounds. One variable each so the palette can be tried by eye.
     Warm neutral ladder: cream -> oyster -> sand -> stone -> greige. */
  --g-hero:#1c1714;        --g-stmt:#ffffff;
  --g-svcs:#faf5f0;     --g-work:#fdfaf7;     --g-proc:#ffffff;
  --g-price:#fdfaf7;    --g-dc:#faf5f0;       --g-revs:#fdfaf7;
  --g-areas:#ffffff;    --g-faq:#faf5f0;      --g-guides:#ffffff;
  --g-close:#fdfaf7;    --g-ft:#211a16;
  /* ⚠ --g-white WAS USED BY FIVE STYLESHEETS AND DEFINED BY NONE.

     17 declarations across v2-portfolio, v2-post, v2-contact, v2-service and
     this file read var(--g-white); only 3 of them carried a fallback. The
     other 14 resolved to nothing, so those elements rendered with NO
     background at all. The one that showed was the portfolio's sticky
     chapter bar: photographs scrolled under it and the labels became
     unreadable (Rashi, 2026-10-29: "the band needs a background otherwise
     cannot read it").

     It is plain white, matching --g-stmt, --g-proc, --g-areas and --g-guides
     which are all #ffffff and are what these sections sit beside.

     It belongs in THIS block, scoped to body.v2, and not on :root -- the
     live site has its own token ladder and a bare :root here would leak into
     it. Same rule as every other token above.

     ⚠ THREE MORE HAD THE SAME GAP, found by auditing every var() the ported
     stylesheets read against everything this site actually serves. The v2
     repo keeps them in src/styles/tokens.css and redesign.css, which are
     compiled into v2's pages and were never ported -- so the stylesheets
     came across and the tokens underneath them did not. Values are v2's own,
     copied exactly:

       --g-beige   7 uses with no fallback (v2-contact, v2-portfolio,
                   v2-post, v2-service) -- all rendering with no background
       --g-mid     3 uses with no fallback
       --ease-draw 7 uses with no fallback -- a timing function, so those
                   transitions were not easing, they were not running

     ⚠ --ease-draw IS THE SAME CURVE AS --draw ABOVE. Two names for one
     value, because v2-home.css and v2's redesign.css were written apart.
     Both are kept: renaming would mean editing six stylesheets to fix
     nothing a visitor can see. If you ever unify them, change the
     DEFINITION here last.

     The audit is worth re-running after any stylesheet is ported:
     every var(--x) in public/v2-*.css must resolve here, in
     src/styles/tokens.css, or carry a fallback. */
  --g-white:#ffffff;    --g-mid:#fdfaf7;    --g-beige:#faf5f0;
  --ease-draw:cubic-bezier(.22,1,.36,1);
  /* Headings and the nav bar share one brown, so the bar reads as part of
     the type rather than a black band laid over the page. */
  --heading:#3a2e26;
  --dark:#211a16;
  --draw:cubic-bezier(.22,1,.36,1);--snap:cubic-bezier(.34,1.56,.64,1);
  --pad:clamp(20px,5vw,80px);
}
*,*::before,*::after{box-sizing:border-box}
html{scroll-behavior:smooth}
body{margin:0;background:var(--white);color:var(--ink-800);font-family:var(--fb);font-weight:300;
  font-size:17px;line-height:1.65;-webkit-font-smoothing:antialiased;overflow-x:hidden}
img,svg,video{display:block;max-width:100%}
img{height:auto}
a{color:inherit;text-decoration:none}
h1,h2,h3{font-family:var(--fh);font-weight:600;letter-spacing:-.035em;line-height:1.02;margin:0;
  color:var(--heading)}
p{margin:0}
ul,ol,dl{margin:0;padding:0;list-style:none}
button{font:inherit}
:focus-visible{outline:3px solid var(--accent);outline-offset:3px}
.vh{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
.skip{position:absolute;left:-9999px;top:0;z-index:999;background:var(--accent);color:#000;padding:14px 20px;font-weight:700}
.skip:focus{left:0}
.eyebrow{display:flex;align-items:center;gap:14px;font-size:11px;letter-spacing:.22em;
  font-weight:700;color:var(--ink-600);font-family:var(--fb);text-transform:uppercase}
.eyebrow--light{color:var(--ink-300)}
.eyebrow .dot{color:var(--accent)}
.rule{display:block;width:40px;height:2px;background:var(--accent);flex:none}
.sec-title{font-size:clamp(30px,4.2vw,52px)}
.sec-sub{margin-top:22px;font-size:clamp(15px,1.3vw,17px);line-height:1.65;color:var(--ink-600)}
.on-dark .sec-sub{color:var(--ink-300)}
.on-dark{color:var(--white)}
.split{display:flex;align-items:flex-end;justify-content:space-between;gap:40px;flex-wrap:wrap}
.link{display:inline-flex;align-items:center;gap:11px;font-size:15px;font-weight:700;font-family:var(--fb);
  border-bottom:2px solid var(--accent);padding-bottom:7px;transition:gap .24s var(--snap)}
.link:hover{gap:18px}
.btn{display:inline-flex;align-items:center;justify-content:center;gap:10px;min-height:56px;padding:0 28px;
  background:var(--accent);color:#000;font-weight:700;font-size:16px;font-family:var(--fb);border:0;border-radius:2px;
  cursor:pointer;transition:transform .24s var(--snap),background .24s}
.btn:hover{background:var(--accent-600);transform:translateY(-2px)}
.btn--sm{min-height:44px;padding:0 20px;font-size:14px}
.btn--full{width:100%}
.btn--ghost{background:none;border:1px solid currentColor;color:inherit}
.on-dark .btn--ghost:hover,.btn--ghost:hover{background:var(--accent);border-color:var(--accent);color:#000}
/* ── entry ─────────────────────────────────────────────── */
.loader{position:fixed;inset:0;z-index:900;background:#1c1714;display:grid;place-items:center;
  align-content:center;gap:22px;animation:loaderOut .6s var(--draw) 1.5s forwards}
/* The real wordmark, revealed left to right as though it were being drawn,
   with the orange rule pulling out from under it. Same drafting idea as the
   line that used to be here, done with the actual logo. */
.loader__logo{overflow:hidden;width:min(300px,62vw)}
.loader__logo img{width:100%;height:auto;clip-path:inset(0 100% 0 0);
  animation:logoWipe .85s var(--draw) .25s forwards}
.loader__rule{display:block;width:min(300px,62vw);height:2px;background:var(--accent);
  transform:scaleX(0);transform-origin:left;animation:ruleIn .7s var(--draw) .55s forwards}
@keyframes logoWipe{to{clip-path:inset(0 0 0 0)}}
@keyframes draw{to{stroke-dashoffset:0}}
@keyframes fade{to{opacity:1}}
@keyframes loaderOut{to{opacity:0;visibility:hidden}}
/* ── header ────────────────────────────────────────────── */
.hdr{position:fixed;top:0;left:0;right:0;z-index:100;color:#fcf6f2;
  transition:background .24s ease,backdrop-filter .24s ease,box-shadow .24s ease}
.hdr__in{display:flex;align-items:center;justify-content:space-between;gap:24px;padding:0 var(--pad);
  height:88px;transition:height .24s var(--draw)}
.hdr.is-stuck{background:rgba(33,26,22,.96);backdrop-filter:blur(12px)}
.hdr.is-stuck .hdr__in{height:64px;border-bottom-color:transparent}
.brand{display:flex;align-items:center;gap:12px;font-family:var(--fh);font-weight:700;font-size:13px;letter-spacing:.17em}
.hdr.is-stuck .brand{font-size:11px}
.hdr__nav{display:flex;gap:30px;font-size:14px;color:var(--ink-200)}
.hdr__nav a{position:relative;padding:6px 0}
.hdr__nav a::after{content:"";position:absolute;left:0;right:0;bottom:0;height:2px;background:var(--accent);
  transform:scaleX(0);transform-origin:left;transition:transform .24s var(--draw)}
.hdr__nav a:hover::after{transform:scaleX(1)}
.hdr__act{display:flex;align-items:center;gap:18px}
.hdr__tel{font-size:14px;white-space:nowrap;display:inline-flex;align-items:center;gap:8px}
.hdr__tel-ico{display:none;flex:none}
.burger{display:none;width:44px;height:44px;background:none;border:0;padding:0;cursor:pointer;
  flex-direction:column;align-items:center;justify-content:center;gap:6px}
.burger span{display:block;width:22px;height:1.5px;background:#fcf6f2;transition:transform .24s var(--draw)}
.burger[aria-expanded=true] span:first-child{transform:translateY(3.75px) rotate(45deg)}
.burger[aria-expanded=true] span:last-child{transform:translateY(-3.75px) rotate(-45deg)}
.mnav{position:fixed;inset:64px 0 0;z-index:99;background:var(--dark);padding:30px var(--pad)}
.mnav nav{display:flex;flex-direction:column}
.mnav a{font-family:var(--fh);font-size:28px;font-weight:600;color:#fcf6f2;padding:14px 0}
/* ── hero ──────────────────────────────────────────────── */
.hero{position:relative;min-height:100svh;background:var(--g-hero);overflow:hidden;display:flex;
  flex-direction:column;justify-content:center;padding:120px var(--pad) 150px}
.hero__media{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:.52;
  transform:scale(1.08);animation:heroIn 1.6s var(--draw) .3s forwards;will-change:transform}
@keyframes heroIn{to{transform:scale(1)}}
.hero__scrim{position:absolute;inset:0;background:
  linear-gradient(90deg,rgba(28,23,20,.92),rgba(28,23,20,.7) 42%,rgba(28,23,20,.3)),
  linear-gradient(0deg,rgba(28,23,20,.95),rgba(28,23,20,0) 38%),
  linear-gradient(0deg,rgba(74,56,44,.2),rgba(74,56,44,.2))}
.hero__in{position:relative}
.rule--draw{width:212px;margin-top:16px;transform:scaleX(0);transform-origin:left;
  animation:ruleIn .6s var(--draw) .3s forwards}
@keyframes ruleIn{to{transform:scaleX(1)}}
.hero__h1{margin-top:28px;font-size:clamp(38px,6.4vw,88px);line-height:.98;color:#fcf6f2}
/* line-height .98 makes the line box shorter than the glyphs need, and the
   reveal clips to it — so descenders were cut off: measured 7.9px off the
   "g" and "y" of "average" and 6.9px off "company." at 88px. The padding
   gives the clip box room for them; the equal negative margin keeps the
   leading between lines exactly as it was. */
.hero__h1 .ln{display:block;overflow:hidden;padding-block-end:.2em;margin-block-end:-.2em}
.hero__h1 .ln>span{display:block;transform:translateY(130%);animation:lineUp .9s var(--draw) forwards}
.hero__h1 .ln:nth-child(1)>span{animation-delay:.15s}
.hero__h1 .ln:nth-child(2)>span{animation-delay:.24s}
.hero__h1 .ln:nth-child(3)>span{animation-delay:.33s}
@keyframes lineUp{to{transform:translateY(0)}}
.hero__tag{margin-top:24px;font-family:var(--fd);font-style:italic;font-size:clamp(19px,2vw,26px);color:#fcf6f2}
.hero__lede{margin-top:16px;font-size:clamp(15px,1.4vw,19px);color:var(--ink-200)}
.hero__ctas{display:flex;gap:16px;margin-top:34px;flex-wrap:wrap}
.hero__ctas .btn--ghost{color:#fcf6f2;border-color:rgba(252,246,242,.4)}
.reveal{opacity:0;transform:translateY(16px);animation:revIn .8s var(--draw) forwards;animation-delay:var(--d,.2s)}
@keyframes revIn{to{opacity:1;transform:none}}
.scrollcue{position:absolute;left:var(--pad);bottom:150px;display:flex;flex-direction:column;align-items:center;gap:10px}
.scrollcue span{font-size:10px;letter-spacing:.2em;color:var(--ink-400);writing-mode:vertical-rl}
.scrollcue i{width:1px;height:46px;background:linear-gradient(180deg,rgba(252,246,242,.35),transparent)}
.scrollcue b{width:5px;height:5px;border-radius:50%;background:var(--accent);animation:cue 2s ease-in-out infinite}
@keyframes cue{0%,100%{transform:translateY(-14px);opacity:0}50%{transform:translateY(0);opacity:1}}
/* The band is full-bleed on purpose, but its cells carried their own side
   padding, so the first stat started 34px from the viewport while the
   headline above it started at 89px — the stats read as a separate,
   wider thing. Pad the CONTAINER to the page gutter and space the cells
   with the SAME cap-and-centre formula every other section uses, so the
   row begins and ends exactly where the content does. A plain var(--pad)
   is not enough: past 1320px the shell centres and its gutter grows. */
.hero__proof{position:absolute;left:0;right:0;bottom:0;display:grid;grid-template-columns:repeat(6,minmax(0,1fr));
  padding-inline:max(var(--pad),calc((100% - var(--maxw)) / 2));
  column-gap:clamp(14px,2.4vw,34px);
  background:rgba(28,23,20,.55);backdrop-filter:blur(6px)}
.hero__proof>div{padding:16px 0}
.hero__proof span{display:block;font-family:var(--fd);font-size:11.5px;letter-spacing:.1em;color:var(--accent)}
.hero__proof p{margin-top:5px;font-size:clamp(11px,.85vw,13px);color:#fcf6f2;line-height:1.3}
/* ── statement ─────────────────────────────────────────── */
.stmt{padding:clamp(60px,9vw,100px) var(--pad);background:var(--g-stmt);}
.stmt__h{margin-top:46px;font-size:clamp(28px,4.8vw,62px);font-weight:500;line-height:1.1}
.stmt__h .w{opacity:.16;transition:opacity .4s ease}
.stmt__h .w.on{opacity:1}
/* 1.1em clipped the descenders of every phrase it cycles — "your risk.",
   "a moving target.", "an open question." all end in a y/g/q, measured 1.1
   to 1.8px shaved off. Taller clip box, pulled back down by the same amount
   so the text and its underline stay exactly where they were. */
.swap{display:inline-block;height:1.35em;margin-block-end:-.25em;overflow:hidden;
  vertical-align:bottom;position:relative}
.swap span{display:inline-block;vertical-align:top;color:var(--ink-800);border-bottom:3px solid var(--accent);
  animation:roll .42s var(--draw)}
@keyframes roll{from{transform:translateY(135%);opacity:0}to{transform:none;opacity:1}}
.stmt__sub{margin-top:40px;font-family:var(--fd);font-style:italic;font-size:clamp(17px,1.8vw,24px);
  line-height:1.5;color:var(--ink-700)}
.stmt .link{margin-top:36px}
/* ── services ──────────────────────────────────────────────
   Was a 360vh scroll-jacked pinned track: the page stopped moving and six
   cards travelled sideways while you scrolled. Rashi, 2026-10-27: six boxes
   in three rows beside the heading, in that same space. So the section is an
   ordinary two-column block again — heading left, a 2x3 grid right — and the
   page scrolls the way a page scrolls. */
.svcs{background:var(--g-svcs);position:relative;padding:clamp(60px,9vw,100px) var(--pad)}
.svcs__pin{display:grid;grid-template-columns:clamp(330px,29vw,460px) minmax(0,1fr);
  gap:clamp(28px,4vw,64px);align-items:start}
.svcs__side{position:sticky;top:96px}
.svcs__vp{min-inline-size:0}
.svcs__track{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));
  gap:clamp(16px,1.8vw,26px)}

.svc{color:#fcf6f2;display:flex;flex-direction:column}
.svc__ph{position:relative;block-size:clamp(132px,10vw,164px);overflow:hidden;background:var(--ink-800)}
.svc__ph img{inline-size:100%;block-size:100%;object-fit:cover;opacity:.82;
  transition:transform .6s var(--draw),opacity .4s ease}
.svc:hover .svc__ph img{transform:scale(1.05);opacity:1}
.svc__n{position:absolute;left:14px;top:12px;font-family:var(--fd);font-size:13px;letter-spacing:.1em;
  color:var(--ink-300);transition:color .24s,transform .24s var(--draw)}
.svc:hover .svc__n{color:var(--accent);transform:translateY(-3px)}
/* The gap below the rule lives on the RULE, not on the heading. `.svc h3`
   in the Astro bundle is the same specificity as ours and loads later, and
   it zeroes the margin — so margin-block-start on the heading computed to
   0px and the coloured hover line landed right on the text. Nothing legacy
   targets .svc__rule, so putting the space here simply works. */
.svc__rule{display:block;inline-size:34px;block-size:2px;background:rgba(252,246,242,.3);margin-block-start:10px;margin-block-end:26px;
  transition:inline-size .5s var(--draw),background .3s}
.svc:hover .svc__rule{inline-size:100%;background:var(--accent)}
.svc h3{margin-block-start:0;font-size:clamp(16px,1.2vw,18.5px);line-height:1.2;letter-spacing:-.02em}
.svc p{margin-block-start:8px;font-size:13.5px;line-height:1.58;color:var(--ink-300)}

/* One column once two would leave the boxes too narrow to read. */
@media (max-width: 1100px){
  .svcs__pin{grid-template-columns:1fr;gap:clamp(28px,5vw,44px)}
  .svcs__side{position:static}
}
@media (max-width: 1100px){
  .svcs__track{grid-template-columns:repeat(2,minmax(0,1fr))}
}
@media (max-width: 560px){
  .svcs__track{grid-template-columns:1fr}
}

/* ── work ──────────────────────────────────────────────── */
.work{background:var(--g-work);padding:clamp(60px,9vw,100px) var(--pad)}
.grid4{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:26px;margin-top:38px}
.pj{display:block}
.pj__ph{height:140px;overflow:hidden;background:var(--ink-200)}
.pj--g .pj__ph{height:170px}
.pj__ph img{width:100%;height:100%;object-fit:cover;transition:transform .6s var(--draw)}
.pj:hover .pj__ph img{transform:scale(1.05)}
.pj__m{display:flex;align-items:baseline;justify-content:space-between;gap:12px;margin-top:14px;flex-wrap:wrap}
.pj__m b{min-inline-size:0}
.pj__m span{flex:none}
.pj__m b,.pj>b{color:var(--heading);font-family:var(--fh);font-weight:600;font-size:17px;letter-spacing:-.02em}
.pj__m span{font-size:11px;letter-spacing:.14em;color:var(--ink-500)}
.pj>i,.pj__m+i{display:block;width:34px;height:2px;background:var(--ink-200);margin-top:12px;
  transition:width .5s var(--draw),background .3s}
.pj:hover i{width:100%;background:var(--accent)}
.pj--g .tag{display:inline-block;margin-top:14px;font-size:11px;letter-spacing:.14em;color:var(--ink-500)}
.pj--g b{display:block;margin-top:8px;font-size:18px;line-height:1.25}
.pj--g p{margin-top:8px;font-size:14px;color:var(--ink-600);line-height:1.55}
.reveal-up{opacity:0;clip-path:inset(0 0 100% 0);transition:opacity .9s var(--draw),clip-path .9s var(--draw)}
.reveal-up.on{opacity:1;clip-path:inset(0 0 0 0)}
/* ── process ───────────────────────────────────────────── */
.proc{position:relative;background:var(--g-proc);padding:clamp(60px,9vw,100px) var(--pad);
  display:grid;grid-template-columns:minmax(0,400px) 2px minmax(0,1fr);gap:clamp(24px,5vw,60px)}
.proc__side{position:sticky;top:120px;align-self:start}
.proc__big{display:flex;align-items:flex-start;gap:16px;margin-top:48px}
.proc__big b{font-family:var(--fh);font-size:clamp(70px,10vw,150px);font-weight:600;line-height:.82;
  letter-spacing:-.05em;color:var(--accent)}
.proc__big span{font-size:13px;letter-spacing:.16em;color:var(--ink-400);margin-top:12px}
.proc__name{margin-top:22px;font-family:var(--fd);font-size:23px;color:#fcf6f2}
.proc__side .rule{width:150px;margin-top:18px}
.proc__rail{position:relative;background:rgba(252,246,242,.14)}
.proc__rail i{position:absolute;top:0;left:0;width:2px;height:0;background:var(--accent)}
.proc__list{display:flex;flex-direction:column;gap:30px}
.step{display:flex;gap:26px;opacity:.26;transition:opacity .45s ease}
.step.on{opacity:1}
.step__n{font-family:var(--fd);font-size:14px;letter-spacing:.1em;color:var(--ink-400);width:30px;flex:none;padding-top:7px}
.step.on .step__n{color:var(--accent)}
.step h3{font-size:25px;line-height:1.12;letter-spacing:-.025em;color:#fcf6f2;transition:font-size .3s var(--draw)}
.step.on h3{font-size:30px}
.step p{margin-top:10px;font-size:15px;line-height:1.6;color:var(--ink-300);max-width: none}
/* ── design centre ─────────────────────────────────────── */
.dc{position:relative;background:var(--g-dc);overflow:hidden;padding:clamp(60px,9vw,100px) var(--pad)}
.dc__bg{position:absolute;right:0;top:0;width:60%;height:100%;object-fit:cover;opacity:.17}
.dc__in{position:relative;display:grid;grid-template-columns:minmax(0,452px) minmax(0,1fr);
  gap:clamp(30px,5vw,80px);align-items:center}
.dc__quip{margin-top:28px;font-family:var(--fd);font-style:italic;font-size:17px;color:var(--ink-400)}
.dc__copy .btn--ghost{margin-top:34px;color:#fcf6f2}
/* `align-items:flex-end` hung the cards off a shared BASELINE, so the one
   card whose label wraps to two lines ("WALNUT, FLOATING") was 17px taller
   and grew upward out of the row. Stretch instead: every card takes the
   height of the tallest, the image boxes are a fixed 198px, and the white
   label block absorbs the difference — tops and bottoms both line up. */
.sw{display:flex;gap:18px;align-items:stretch;perspective:900px}
.sw__i{width:142px;flex:none;display:flex;flex-direction:column;background:#2b2320;box-shadow:0 10px 26px rgba(40,31,26,.35);
  transition:transform .35s var(--draw),box-shadow .35s;transform-style:preserve-3d;position:relative;overflow:hidden}
.sw__i i{display:block;height:198px;position:relative;overflow:hidden;background:var(--ink-200)}
.sw__i i img{inline-size:100%;block-size:100%;object-fit:cover;display:block;
  transition:transform .5s var(--draw)}
.sw__i:hover i img{transform:scale(1.06)}
.sw__i i::after{content:"";position:absolute;inset:0;background:linear-gradient(118deg,transparent 32%,rgba(252,246,242,.3) 48%,transparent 62%);
  opacity:0;transition:opacity .35s}
.sw__i:hover i::after{opacity:1}
.sw__i b{display:block;padding:16px 14px 0;font-family:var(--fh);font-size:15px;font-weight:600;color:#fcf6f2}
.sw__i span{display:block;padding:6px 14px 18px;font-size:11px;letter-spacing:.12em;color:var(--ink-400)}
.sw__i::after{content:"";display:block;height:2px;background:rgba(252,246,242,.12);transition:background .3s}
.sw__i:hover::after{background:var(--accent)}
.sw__i:hover{box-shadow:0 26px 54px rgba(40,31,26,.6)}
/* ── reviews ───────────────────────────────────────────── */
.revs{background:var(--g-revs);display:grid;grid-template-columns:minmax(0,320px) minmax(0,1fr);gap:clamp(30px,6vw,88px);
  padding:clamp(60px,9vw,100px) var(--pad)}
.revs__score{color:var(--heading);margin-top:42px;font-family:var(--fh);font-size:clamp(90px,11vw,152px);font-weight:600;
  line-height:.82;letter-spacing:-.05em}
.revs__stars{margin-top:26px}
.revs__stars use{opacity:0;animation:none}
.revs.on .revs__stars use{animation:fade .3s ease forwards}
.revs.on .revs__stars use:nth-child(2){animation-delay:.08s}
.revs.on .revs__stars use:nth-child(3){animation-delay:.16s}
.revs.on .revs__stars use:nth-child(4){animation-delay:.24s}
.revs.on .revs__stars use:nth-child(5){animation-delay:.32s}
.revs__meta{margin-top:20px;font-size:15px;color:var(--ink-600)}
.revs__meta b{color:var(--ink-800)}
.revs__side .link{margin-top:30px}
.revs__main{display:grid}
.revs__q{grid-area:1/1;align-self:start}
.rev{grid-area:1/1;margin:0;padding-top:84px;opacity:0;transform:translateY(12px);pointer-events:none;
  transition:opacity .5s ease,transform .5s var(--draw)}
.rev.is-on{opacity:1;transform:none;pointer-events:auto}
.rev blockquote{color:var(--heading);margin:0;font-family:var(--fd);font-style:italic;font-size:clamp(21px,2.9vw,40px);
  line-height:1.38;letter-spacing:-.015em;max-width: none}
.rev figcaption{display:flex;align-items:center;gap:16px;margin-top:34px;flex-wrap:wrap}
.rev figcaption i{width:34px;height:2px;background:var(--accent)}
.rev figcaption b{font-family:var(--fh);font-weight:600;font-size:17px;letter-spacing:-.015em}
.rev figcaption span{font-size:13px;letter-spacing:.14em;color:var(--ink-500)}
.dots{grid-area:1/1;align-self:end;display:flex;align-items:center;gap:10px;padding-top:20px}
.dots button{width:44px;height:44px;padding:0;border:0;background:none;cursor:pointer;display:grid;place-items:center}
.dots button i{display:block;width:8px;height:8px;border-radius:4px;background:var(--ink-300);
  transition:width .4s var(--draw),background .3s}
.dots button[aria-selected=true] i{width:46px;background:var(--accent)}
/* ── areas ─────────────────────────────────────────────── */
/* ── areas we serve ────────────────────────────────────────
   One treatment at every width (Rashi, 2026-10-28: "make the desktop version
   the same as what you've done for the mobile"). The photograph is the
   panel's own backdrop with a scrim over it, and the names read as light
   type on a photograph — the hero's recipe. It replaced a two-column layout
   whose photo was ~124px wide on a phone, too small to read. */
.areas{position:relative;display:grid;grid-template-columns:1fr;
  padding:clamp(60px,9vw,100px) var(--pad);
  background:var(--g-hero,#1c1714);isolation:isolate}
/* ── THE NEIGHBOURHOODS, ACROSS RATHER THAN DOWN ───────────────────────────
   Rashi, 2026-10-28: "put all the areas we serve with buttons in horizontal
   lines instead of a vertical one, the empty space on the right is looking a
   little weird."

   It was a single column of 23px names, so eleven neighbourhoods made a tall
   ladder down the left and left most of the photograph empty beside it. Now
   it is a wrapping row of pills that uses the full width, which also lets the
   backdrop show through instead of being covered by a column of text.

   font-size no longer changes on hover — growing a pill reflows the whole
   row and every pill after it jumps. Background and border carry the state
   instead, and those cost no layout. */
.alist{margin-top:40px;display:flex;flex-wrap:wrap;gap:12px;list-style:none;padding:0}
.alist a{display:inline-flex;align-items:center;height:auto;
  padding:13px 22px;border-radius:999px;white-space:nowrap;
  border:1px solid rgba(252,246,242,.24);background:rgba(252,246,242,.07);
  font-family:var(--fh);font-weight:500;font-size:17px;letter-spacing:-.01em;
  color:#fcf6f2;
  /* colour is still NOT transitioned: a running colour transition holds its
     old computed value when the rule behind it swaps, and the name stays
     dark-on-dark. */
  transition:background .24s var(--draw),border-color .24s var(--draw),transform .24s var(--draw)}
/* the 40px rule that used to slide out beside each name has no place on a
   pill, and an empty ::before still reserved its box */
.alist a::before{content:none}
.areas__more{margin-top:24px;font-size:14px;color:var(--ink-400)}
.areas__side{position:relative;z-index:1}
.areas__fig{position:absolute;inset:0;margin:0;z-index:0;aspect-ratio:auto;
  overflow:hidden;background:var(--g-hero,#1c1714)}
.areas__fig::after{content:"";position:absolute;inset:0;
  /* Even, not left-heavy: the content now runs the full width, so a scrim
     that faded to .3 on the right left the pills over there sitting on bare
     photograph. */
  background:
    linear-gradient(90deg, rgba(28,23,20,.86), rgba(28,23,20,.66) 55%, rgba(28,23,20,.6)),
    linear-gradient(0deg, rgba(28,23,20,.9), rgba(28,23,20,.2) 62%),
    linear-gradient(0deg, rgba(74,56,44,.2), rgba(74,56,44,.2))}
body.v2 .areas .sec-title{color:#fcf6f2}
body.v2 .areas .sec-sub,body.v2 .areas .eyebrow--light{color:var(--ink-300)}
body.v2 .areas .alist a{color:#fcf6f2}
body.v2 .areas .alist a.on,body.v2 .areas .alist a:hover,
body.v2 .areas .alist a:focus-visible{
  background:var(--accent);border-color:var(--accent);color:#1c1714;
  font-size:17px;transform:translateY(-2px)}
body.v2 .areas .areas__more{color:var(--ink-400)}
.areas__fig img{width:100%;height:100%;object-fit:cover;opacity:.52}
.warn{display:inline-block;margin-top:10px;padding:7px 11px;border:1px dashed rgba(252,246,242,.75);
  color:#fcf6f2;font-size:10px;letter-spacing:.1em}
/* ── faq ───────────────────────────────────────────────── */
.faq{background:var(--g-faq);padding:clamp(60px,9vw,100px) var(--pad)}
.facts{margin-top:44px;padding:30px clamp(20px,3vw,38px);background:#faf5f0;border-radius:3px}
.facts dl{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:20px 40px}
.facts dt{font-family:var(--fh);font-size:12px;font-weight:700;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-600)}
.facts dd{margin:6px 0 0;font-size:15px;line-height:1.55}
.acc{margin-top:40px;max-width: none}
.qa summary{display:flex;align-items:center;justify-content:space-between;gap:24px;padding:20px 0;
  cursor:pointer;list-style:none}
.qa summary::-webkit-details-marker{display:none}
/* line-height 1.3 left the descenders of a wrapped question sitting flush
   with the bottom of its box — measured 0.4px of headroom on a three-line
   question, which is inside the margin font rendering moves by, so it cut
   the tails of g, y and p on some devices and not others. */
.qa h3{font-size:clamp(17px,1.8vw,22px);line-height:1.45;letter-spacing:-.02em;font-weight:600;
  /* leading alone only bought 1.7px under the last line, which is inside the
     range font rendering shifts by. Padding guarantees the room instead —
     it sits below the final line, so it does not open up the wrap. */
  padding-block-end:.16em}
.qa summary i{position:relative;width:18px;height:18px;flex:none}
.qa summary i::before,.qa summary i::after{content:"";position:absolute;background:var(--accent);
  left:50%;top:50%;transition:transform .3s var(--draw)}
.qa summary i::before{width:18px;height:2px;transform:translate(-50%,-50%)}
.qa summary i::after{width:2px;height:18px;transform:translate(-50%,-50%)}
.qa[open] summary i::after{transform:translate(-50%,-50%) scaleY(0)}
.qa__b{padding:0 0 26px;max-width: none}
.qa__b p{color:var(--ink-700);font-size:16px}
.qa[open] .qa__b{animation:revIn .4s var(--draw)}
/* ── guides ────────────────────────────────────────────── */
.guides{background:var(--g-guides);padding:clamp(60px,9vw,100px) var(--pad)}
.guides .grid4{margin-top:38px}
/* ── close ─────────────────────────────────────────────── */
.close{display:grid;grid-template-columns:minmax(0,568px) minmax(0,636px);gap:clamp(30px,5vw,76px);
  justify-content:space-between;padding:clamp(60px,9vw,100px) var(--pad);background:var(--g-close)}
.close__tel{display:flex;align-items:center;gap:14px;margin-top:46px;font-size:15px;color:var(--ink-300)}
.close__tel a{color:#fcf6f2;font-weight:700}
.panel{background:#2a221f;border:1px solid rgba(252,246,242,.1);padding:clamp(24px,3vw,40px);position:relative}
.form h3{font-size:21px;font-weight:600;letter-spacing:-.02em;color:#fcf6f2}
.form__sub{margin-top:9px;font-size:13px;color:var(--ink-400)}
.f{position:relative;margin-top:28px}
.f input,.f select{width:100%;height:46px;background:none;border:0;border-bottom:1px solid rgba(252,246,242,.26);
  color:#fcf6f2;font-size:16px;font-family:var(--fb);padding:0;border-radius:0;transition:border-color .26s}
.f select{color:var(--ink-300)}
.f label{position:absolute;left:0;top:12px;font-size:16px;color:var(--ink-400);pointer-events:none;
  transition:transform .26s var(--draw),font-size .26s var(--draw),color .26s,letter-spacing .26s}
.f input:focus+label,.f input:not(:placeholder-shown)+label,.f--sel label{
  transform:translateY(-22px);font-size:11px;letter-spacing:.14em;color:var(--accent)}
.f input:focus,.f select:focus{outline:none;border-bottom-color:var(--accent);border-bottom-width:2px}
.f::after{content:"";position:absolute;left:0;bottom:0;height:2px;width:0;background:var(--accent);
  transition:width .26s var(--draw)}
.f:focus-within::after{width:100%}
.form .btn{margin-top:32px}
.form__fine{margin-top:16px;font-size:12px;color:var(--ink-400);text-align:center}
.done{text-align:center;padding:40px 0}
.done h3{margin-top:22px;font-size:26px;color:#fcf6f2}
.done p{margin-top:12px;font-size:15px;color:var(--ink-300)}
.done svg{margin:0 auto}
.tick{stroke-dasharray:60;stroke-dashoffset:60;animation:draw .5s var(--draw) .15s forwards}
/* ── footer ────────────────────────────────────────────── */
.ft{background:var(--g-ft);color:#fcf6f2;padding:clamp(50px,7vw,76px) var(--pad) 0;overflow:hidden}
.ft__cols{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:32px;
  padding-bottom:44px}
.ft__h{font-family:var(--fb);font-size:11px;letter-spacing:.2em;color:var(--accent);font-weight:700;
  text-transform:uppercase;margin-bottom:18px}
.ft__cols a,.ft address span{display:block;font-size:14px;color:var(--ink-300);padding:5px 0;font-style:normal}
.ft__cols a:hover{color:#fcf6f2}
.ft address{font-style:normal}
.ft__soc{display:flex;gap:10px;margin-top:16px}
.ft__soc a{font-size:12px;letter-spacing:.1em;text-transform:uppercase}
/* Icon links still need a target you can hit: 40px, which clears the 24px
   minimum for an inline control and matches the dial button in the bar. */
/* No ring. The circle was a 40px box around a 18px glyph, and the glyphs do
   not share a common optical centre — Facebook's f sits high, LinkedIn's
   mark sits low — so each one looked off inside its own circle. Just the
   icon now, with the 40px target kept as invisible padding (Rashi,
   2026-10-28). grid + place-items centres the SVG on both axes exactly. */
/* body.v2 on every one of these: project.css and redesign.css are bundled
   into a <link> that loads after this file, and `.ft__soc a` is MORE
   specific than a bare `.ft__soc-ico`, so the plain selector lost and the
   anchor stayed display:block with the glyph 6px above centre. */
body.v2 .ft__soc-ico{display:grid;place-items:center;flex:none;
  inline-size:40px;block-size:40px;color:var(--ink-300);
  transition:color .22s ease,transform .22s ease}
body.v2 .ft__soc-ico:hover,body.v2 .ft__soc-ico:focus-visible{color:var(--accent);transform:translateY(-2px)}
body.v2 .ft__soc-ico svg{display:block}
.ft__proof{padding:30px 0 26px;font-size:13px;color:var(--ink-400)}
/* A backdrop, not a line of its own. It used to sit in the flow as a band
   under the columns, where on a phone it was clipped mid-word and read as a
   broken element. Now it is absolutely positioned behind the legal line,
   faded out towards the top so it never competes with type, and it is
   allowed to overflow the viewport — it is texture, not something to read
   (Rashi, 2026-10-28). */
.ft__word{position:absolute;inset-inline:0;inset-block-end:0;margin:0;
  overflow:hidden;pointer-events:none;z-index:0;
  -webkit-mask-image:linear-gradient(0deg,#000 10%,transparent 92%);
          mask-image:linear-gradient(0deg,#000 10%,transparent 92%)}
.ft__word span{display:block;font-family:var(--fh);font-weight:700;
  font-size:clamp(54px,11vw,150px);line-height:.9;letter-spacing:-.04em;
  color:rgba(252,246,242,.055);white-space:nowrap;will-change:transform}
.ft{position:relative;isolation:isolate}
.ft__legal{position:relative;z-index:1}
.ft__legal{display:flex;align-items:center;justify-content:space-between;gap:20px;flex-wrap:wrap;
  padding:20px 0 26px;font-size:12px;color:var(--ink-400)}
.ft__legal a{margin-left:26px}
/* ── responsive ────────────────────────────────────────── */
@media (max-width:1100px){
  .hdr__nav{display:none}
  .burger{display:flex}
  .hdr__in{height:64px}
  .svcs{height:auto}
  .svcs__pin{position:static;height:auto;flex-direction:column;align-items:stretch;
    padding:clamp(60px,9vw,100px) 0 clamp(60px,9vw,100px) var(--pad)}
  .svcs__side{width:auto;padding-right:var(--pad)}
  .svcs__vp{overflow-x:auto;scroll-snap-type:x mandatory;margin-top:40px;
    padding-bottom:20px;-webkit-overflow-scrolling:touch}
  .svcs__track{transform:none!important;padding-right:var(--pad)}
  .svc{scroll-snap-align:start}
  .svcs__rail{display:none}
  .proc{grid-template-columns:1fr;gap:34px}
  .proc__side{position:static}
  .proc__rail{display:none}
  .step{opacity:1}
  .step h3,.step.on h3{font-size:22px}
  .dc__in,.revs,.areas,.close{grid-template-columns:1fr}
  .sw{overflow-x:auto;padding-bottom:14px}
  .grid4{grid-template-columns:repeat(2,minmax(0,1fr))}
  .ft__cols{grid-template-columns:repeat(2,minmax(0,1fr))}
  .hero__proof{grid-template-columns:repeat(3,minmax(0,1fr))}
  .scrollcue{display:none}
  .revs__main{display:block}
  .rev{display:none;padding-top:30px}
  .rev.is-on{display:block}
  .dots{padding-top:30px}
}
@media (max-width:640px){
  .grid4{grid-template-columns:1fr}
  .ft__cols{grid-template-columns:1fr}
  .claims{flex-direction:column;gap:18px}
  .hero{padding-bottom:180px}
  .hero__ctas .btn{width:100%}
}
/* ── reduced motion: every reveal becomes a short fade ──── */
@media (prefers-reduced-motion:reduce){
  html{scroll-behavior:auto}
  *,*::before,*::after{animation-duration:.01ms!important;animation-iteration-count:1!important;
    transition-duration:.01ms!important}
  .loader{display:none}
  .hero__h1 .ln>span{transform:none}
  .hero__media{transform:none}
  .reveal{opacity:1;transform:none}
  .stmt__h .w{opacity:1}
  .reveal-up{opacity:1;clip-path:none}
  .revs__stars use{opacity:1}
  .step{opacity:1}
}
/* The header at phone width. website-astro has a live bug here: a long CTA
   label plus the phone number pushes the burger off the bar and the mobile
   nav becomes unreachable. Three rules stop it: the wordmark never wraps,
   the phone number drops, and the CTA carries a short label. */
.lbl-short{display:none}
.brand span{white-space:nowrap}
@media (max-width:760px){
  .brand span{font-size:10px;letter-spacing:.12em}
  .lbl-full{display:none}
  .lbl-short{display:inline}
  .hdr__act{gap:10px}
  .hdr__act .btn--sm{padding:0 16px}
}
/* ───────────────────────────────────────────────────────────────────────────
   GROUND-AWARE SECTIONS.

   Services, Process, Design Centre, Areas and Close have no fixed text
   colour any more: they read four variables, and the variables change with
   how dark their ground is. palette.js measures the luminance of whatever
   ground you pick and adds .is-dark when it is dark, so you can try any tone
   in the bench and the text follows instead of disappearing.

   --t-strong  headings and names        --t-muted  body and captions
   --t-rule    hairlines and dividers    --t-card   cards sitting on the ground
   --t-num     the big display numerals
   ─────────────────────────────────────────────────────────────────────────── */
.svcs,.proc,.dc,.areas,.close{
  --t-strong:var(--heading); --t-muted:var(--ink-700);
  --t-rule:rgba(40,31,26,.22); --t-card:#fdfaf7; --t-num:var(--heading);
  --t-shadow:rgba(40,31,26,.16); --t-shadow-lg:rgba(40,31,26,.28);
}
.svcs.is-dark,.proc.is-dark,.dc.is-dark,.areas.is-dark,.close.is-dark{
  --t-strong:#fcf6f2; --t-muted:var(--ink-300);
  --t-rule:rgba(252,246,242,.3); --t-card:#fdfaf7; --t-num:var(--accent);
  --t-shadow:rgba(28,23,20,.35); --t-shadow-lg:rgba(28,23,20,.6);
}
.svcs .eyebrow--light,.proc .eyebrow--light,.dc .eyebrow--light,
.areas .eyebrow--light,.close .eyebrow--light{color:var(--t-muted)}
.svcs .sec-sub,.proc .sec-sub,.dc .sec-sub,.areas .sec-sub,.close .sec-sub{color:var(--t-muted)}
.svcs .sec-title,.proc .sec-title,.dc .sec-title,.areas .sec-title,.close .sec-title{color:var(--t-strong)}
/* Services */
.svcs .svc{color:var(--t-strong)}
.svcs .svc h3{color:var(--t-strong)}
.svcs .svc p,.svcs .svc__n{color:var(--t-muted)}
.svcs .svc__rule{background:var(--t-rule)}
.svcs .svc:hover .svc__rule{background:var(--accent)}
.svcs .svc:hover .svc__n{color:#fff}
.svcs.is-dark .svc:hover .svc__n{color:var(--accent)}
.svcs__count b{color:var(--t-num)}
.svcs__count span,.svcs__hint{color:var(--t-muted)}
.svcs__rail{background:var(--t-rule)}
/* Process */
.proc__big b{color:var(--t-num)}
.proc__big span{color:var(--t-muted)}
.proc__name{color:var(--t-strong)}
.proc__rail{background:var(--t-rule)}
.proc .step h3{color:var(--t-strong)}
.proc .step p,.proc .step__n{color:var(--t-muted)}
.proc .step.on .step__n{color:var(--heading)}
.proc.is-dark .step.on .step__n{color:var(--accent)}
.proc .step{opacity:.34}
.proc .step.on{opacity:1}
/* Design Centre */
.dc__quip{color:var(--t-muted)}
.dc__copy .btn--ghost{color:var(--t-strong);border-color:var(--t-rule)}
.sw__i{background:var(--t-card);box-shadow:0 10px 26px var(--t-shadow)}
.sw__i:hover{box-shadow:0 26px 54px var(--t-shadow-lg)}
.sw__i b{color:var(--t-strong)}
.sw__i span{color:var(--t-muted)}
.sw__i::after{background:var(--t-rule)}
/* Areas */
.alist a{color:var(--t-strong);border-bottom-color:var(--t-rule)}
.alist a.on,.alist a:hover{color:var(--heading)}
.areas.is-dark .alist a.on,.areas.is-dark .alist a:hover{color:var(--accent)}
.areas__more{color:var(--t-muted)}
/* Close. The form panel is always dark: it frames the one thing the whole
   page is asking for, and it declares its own text colour so nothing inside
   it can ever inherit ink-on-ink. */
.close .claims--light li{color:var(--t-muted)}
.close__tel{border-top-color:var(--t-rule);color:var(--t-muted)}
.close__tel a{color:var(--t-strong)}
.panel{color:#fcf6f2}
.panel .f label{color:var(--ink-400)}
/* The design centre photograph was running under the copy at narrower widths.
   Masked so it only occupies the right of the band, where the swatches are. */
.dc__bg{opacity:.16;-webkit-mask-image:linear-gradient(90deg,transparent 0,#000 40%);
  mask-image:linear-gradient(90deg,transparent 0,#000 40%)}
@media (max-width:1100px){.dc__bg{opacity:.14;-webkit-mask-image:none;mask-image:none}}
/* The service numerals sit ON the photograph, not on the section ground, so
   they cannot take their colour from it — a project photo can be any value.
   A small dark chip guarantees them on every image. */
.svcs .svc__n{color:#fcf6f2;background:rgba(36,30,26,.76);padding:3px 9px;border-radius:2px;
  backdrop-filter:blur(3px)}
.svcs .svc:hover .svc__n{color:#fff;background:rgba(203,115,25,.92)}
/* .alist a is the one flipped element with a transition on `color`. When the
   ground changes in the bench, the running transition keeps the old computed
   value and the var() swap never lands. An explicit, higher-specificity rule
   gives it a fresh value to transition toward. */
.areas.is-dark .alist a{color:#fcf6f2}
.areas:not(.is-dark) .alist a{color:var(--heading)}
/* ── Service page: the permits grid ───────────────────────────────────────
   The thing a service page has that the homepage does not. Set as a list of
   facts rather than prose, because that is what someone checking whether
   their own lot qualifies actually reads. */
.permits { display: grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap: 1px;
  background: var(--t-rule); }
.permits li { background: var(--t-card); padding: 22px 24px; }
.permits b { display: block; font-family: var(--fh); font-weight: 600; font-size: 16px;
  letter-spacing: -.02em; color: var(--t-strong); }
.permits span { display: block; margin-top: 8px; font-size: 14px; line-height: 1.55;
  color: var(--t-muted); }
@media (max-width: 1100px) { .permits { grid-template-columns: 1fr } }
/* ═══════════════════════════════════════════════════════════════════════════
   THE CONTAINER, and one rhythm.

   Measured on the North York page: left edges were consistent and section
   rhythm was a uniform 96px — alignment was never the problem. These were:

   1. EVERY SECTION HAD max-width: none. Content stretched to whatever the
      monitor was, so on a wide screen the page had no shape. The horizontal
      padding grows instead, which caps content at --maxw and centres it
      without any section's internal layout having to change.

   2. SEVENTEEN distinct margin-top values. Nothing repeated, so the eye
      never found a system. One value now sits under every section heading.

   ⚠ These are prefixed `body.v2` ON PURPOSE. The page-specific stylesheets
   load AFTER this one, so an unprefixed `main > section` lost to a plain
   `.fork { padding: … }` and content was still running to 1520px. Raising
   specificity fixes it regardless of load order; reordering the files would
   not, because each page adds its own last.
   ═══════════════════════════════════════════════════════════════════════════ */
:root { --maxw: 1320px; --rhythm: 40px; }
body.v2 main > section,
body.v2 .hero,
body.v2 .ft__cols, body.v2 .ft__proof, body.v2 .ft__word, body.v2 .ft__legal {
  padding-inline: max(var(--pad), calc((100% - var(--maxw)) / 2));
}
/* The services section caps and centres like every other section now, so the
   pin's old zero-padding override and the whole viewport-height card-sizing
   block went with the pinned layout on 2026-10-27. The cards are grid cells;
   they size to their content. */
/* The short rule reads as belonging to the photograph it sits under, so it
   stays close to the image and the gap goes BELOW it, before the heading
   (Rashi, 2026-10-28: "too close to the text, make them closer to image and
   add gap between text and line"). 18/12 became 10/22. */
body.v2 .svc__rule { margin-block-start: 10px; margin-block-end: 26px; }

/* ═══════════════════════════════════════════════════════════════════════════
   COLLISION GUARD — project.css vs the v2 stylesheets.

   Base.astro loads src/styles/project.css on EVERY page, including v2 pages.
   It is the old site's stylesheet, and eleven of its class names are also
   ours: btn, btn--ghost, eyebrow, hero, hero__ctas, is-stuck, on-dark,
   sec-sub, sec-title, svc, tag.

   Most are harmless — we restyle the same properties and win on load order.
   One was not. The old `.svc` is a fixed-height card whose body slides up on
   hover:

       .svc { height: 300px; overflow: hidden }   (260/340/240 at breakpoints)

   Our card is image + rule + title + body, about 500px. It was being capped
   at 240–340px and clipped, so every title and description vanished. Nothing
   in our own CSS opposed `height`, so there was no specificity fight to
   notice — the property was simply unopposed.

   That cost three wrong diagnoses: the pin's clip, the viewport height, and
   a stale cache. The lesson is that a shared class name across two design
   systems fails silently, in whichever property the newer one forgets to set.

   The real fix is to stop loading project.css on v2 pages. Until the old
   templates are migrated off it, this neutralises the collisions explicitly.
   ═══════════════════════════════════════════════════════════════════════════ */
body.v2 .svc {
  block-size: auto;
  overflow: visible;
  position: relative;
  /* flex, not block: the card is a grid cell that stacks its own parts. */
  display: flex;
  flex-direction: column;
  border-radius: 0;
  background: none;
  color: inherit;
}
body.v2 .svc .svc__ph { position: relative; }
/* The legacy PSEUDO-ELEMENTS are the other half of the collision, and they
   are worse than the properties because nothing in our CSS can "win" a
   pseudo-element that we never declared:

     .eyebrow::after  a 32px rule AFTER the label. Our eyebrow draws its own
                      rule BEFORE, so every eyebrow on every v2 page was
                      rendering with a line on both sides.

     .svc::after      a dark gradient at inset:0. While the legacy height
                      capped the card at 300px it only dimmed the image —
                      which is why it looked fine. The moment the card grew
                      to its real height the gradient grew with it and sat
                      on top of the title and the body. The text was never
                      missing; it was under a scrim.
*/
body.v2 .svc::after,
body.v2 .svc::before,
body.v2 .eyebrow::after { content: none; display: none; }

/* ::before as well as ::after. redesign.css draws its own 40px bar BEFORE
   every eyebrow — the "drawn rule" motif — as a flex item with the
   container's 14px gap. It starts at scaleX(0), so it is invisible until a
   section scrolls in, but an invisible element still takes its space: every
   eyebrow label sat exactly 54px (40 + 14) right of the heading under it.
   Hiding our own <i class="rule"> was half the job; this is the other half.

   ⚠ !important is load-bearing, not laziness. Base.astro imports
   redesign.css and Astro bundles it into /_astro/V2.*.css, whose <link>
   comes AFTER v2-home.css. Measured: the identical rule injected last moves
   the glyph from 143px to 89px, so this loses on source order and nothing
   writable from this file reorders those links. ::after does not need it. */
body.v2 .eyebrow::before { content: none !important; display: none !important; }
body.v2 .eyebrow { display: flex; }
body.v2 .hero__ctas { display: flex; }
/* ═══════════════════════════════════════════════════════════════════════════
   HEADER — global corrections.

   1. THE COLOUR BUG. `.hdr` was permanently `color:#fcf6f2` with a background
      only in `.is-stuck`. That is only legible over a dark hero, and three v2
      pages have no dark hero at all — /our-portfolio/, /contact-us/ and the
      blog post. On those the bar was near-white type on a near-white page.

      The default is now a SOLID dark bar, which is correct everywhere. Only a
      page that actually opens with a dark hero gets the transparent treatment,
      detected from the markup itself with :has() rather than a per-page flag
      somebody has to remember. Browsers without :has() keep the solid bar,
      which is the safe direction to fail in.

   2. IT DEPENDED ON JAVASCRIPT. `is-stuck` is toggled by v2-home.js, whose
      scroll listener is registered at the very END of a long IIFE — anything
      throwing above it (as a missing #mqA did) left the bar transparent for
      the life of the page. The solid state is now driven by CSS where
      scroll-timeline exists; the class remains as the fallback.

   3. THE LOGO. The bar carried an inline house glyph and the words GRAND
      DESIGN BUILD; the real artwork was only ever used by the loader.
      Sized by HEIGHT — see the note in Header.astro.
   ═══════════════════════════════════════════════════════════════════════════ */

/* 1 — the bar is solid by default */
body.v2 .hdr { background: rgba(33,26,22,.96); backdrop-filter: blur(12px); }
/* …and transparent only while a dark hero is actually behind it */
@supports selector(:has(*)) {
  body.v2:has(.hero.on-dark) .hdr:not(.is-stuck) { background: transparent; backdrop-filter: none; }
}
/* 2 — solid state without JavaScript, where the browser can do it */
@supports (animation-timeline: scroll()) {
  body.v2:has(.hero.on-dark) .hdr {
    animation: hdrSolid linear both;
    animation-timeline: scroll();
    animation-range: 60px 150px;
  }
  @keyframes hdrSolid {
    to { background: rgba(33,26,22,.96); backdrop-filter: blur(12px); }
  }
}
/* 3 — the real logo, sized by height so it can never set the bar's height */
.brand__mark { display: block; block-size: 42px; inline-size: auto; max-inline-size: none;
  transition: block-size .24s var(--draw); }
.hdr.is-stuck .brand__mark { block-size: 32px; }
/* The bar drops to 64px at 1100px, so the logo steps down there too —
   otherwise 760-1100px ran a 42px mark in a 64px bar, much tighter than
   the 48-50% fill every other width has. */
@media (max-width: 1100px) { .brand__mark { block-size: 32px; } }
/* ── dropdowns ─────────────────────────────────────────────────────────── */
.hdr__grp { position: relative; }
.hdr__grp > a { display: inline-flex; align-items: center; gap: 5px; }
.hdr__chev { transition: transform .3s var(--draw); flex: none; }
.hdr__grp:hover > a .hdr__chev, .hdr__grp:focus-within > a .hdr__chev { transform: rotate(180deg); }
.hdr__box {
  position: absolute; inset-block-start: calc(100% + 10px); inset-inline-start: -14px;
  min-inline-size: 232px; padding: 9px; border-radius: 14px;
  background: rgba(28,23,20,.985); backdrop-filter: blur(14px);
  box-shadow: 0 22px 54px rgba(0,0,0,.42);
  display: grid; gap: 1px;
  opacity: 0; visibility: hidden; transform: translateY(-7px);
  transition: opacity .25s ease, transform .25s var(--draw), visibility .25s;
}
.hdr__grp:hover .hdr__box, .hdr__grp:focus-within .hdr__box { opacity: 1; visibility: visible; transform: none; }
.hdr__box a {
  display: block; padding: 10px 13px; border-radius: 9px; font-size: 13.5px;
  color: var(--ink-200); white-space: nowrap;
  transition: background .22s ease, color .22s ease;
}
.hdr__box a:hover, .hdr__box a:focus-visible { background: rgba(255,255,255,.085); color: #fff; }
.hdr__box a::after { content: none; }           /* the bar's underline must not leak in */
.hdr__nav a[aria-current] { color: #fff; }
.hdr__nav > a[aria-current]::after, .hdr__grp > a[aria-current="true"]::after { transform: scaleX(1); }
/* ── mobile sheet: the dropdowns have to exist here too ────────────────── */
.mnav__sub { display: grid; gap: 2px; margin: 2px 0 10px; padding-inline-start: 14px;
  border-inline-start: 1px solid rgba(255,255,255,.17); }
.mnav__sub a { font-size: 15px !important; opacity: .76; padding: 7px 0 !important; }
.mnav__cta { margin-block-start: 14px; color: var(--accent) !important; }
.mnav__tel { opacity: .76; }
@media (prefers-reduced-motion: reduce) {
  .brand__mark, .hdr__chev, .hdr__box { transition: none; }
  @supports (animation-timeline: scroll()) {
    body.v2:has(.hero.on-dark) .hdr { animation: none; background: rgba(33,26,22,.96); }
  }
}
/* ── The price cards, reordered ───────────────────────────────────────────
   Was: label → chart → statement, with the chart on `flex:1` so it stretched
   to fill the card. The fixed-price line sits at the bottom of its viewBox,
   so that stretch left a large empty band above it and the statement ended up
   a long way from the label it belongs with.

   Now the words sit together at the top and the drawing reads as the
   illustration beneath them, at a fixed height in both cards so the two line
   up. The light card also gets an edge — white on a cream page had none. */

/* ── Measures that were narrower than the row they sit in ─────────────────
   Three sections read as "empty on the right" for the same reason: a cap on
   the content that the block above or beside it does not share.

   .acc was 92ch (907px) directly beneath .facts at the full 1281px, so the
   question list stepped in by 374px for no reason. The ANSWER text keeps its
   own 72ch measure (.qa__b), which is what actually needs to stay readable —
   the question rows can align with the grid above them.

   .rev blockquote was 22ch (525px) in a 900px column, leaving 349px empty
   beside every quote. 30ch is still a comfortable measure at that size. */
.acc { max-width: none; }
.rev blockquote { max-width: none; }
/* The dots were in the SAME grid cell as the rotating reviews
   (`grid-area:1/1; align-self:end`), so on a shorter quote they landed on top
   of the attribution — the name and tag with dots struck through them. Give
   them their own row under the stack. */
.revs__main { grid-template-rows: 1fr auto; }
.revs__q, .rev { grid-row: 1; grid-column: 1; }
.dots { grid-row: 2; grid-column: 1; align-self: start; }
/* The three reviews stack in one grid cell, so the row is as tall as the
   tallest; the longest one still grazed the dots by 12px. Explicit clearance. */
.dots { margin-block-start: 26px; }
/* ── FAQ: one thing to look at ────────────────────────────────────────────
   The section stacked a seven-item key-facts panel directly on top of the
   question list — two dense blocks of small text with equal weight, right
   under the heading. There was no focal point.

   Reading order now: heading → the questions people came for → the key facts
   as a reference panel underneath. The facts block still ships in the same
   place in the markup, so what AI answer engines read is unchanged. */
/* Heading → key facts → questions, the order the current site uses. The facts
   were clutter because seven label/value pairs ran together as loose text in
   one flat panel; each pair is now its own box, so they read as reference
   cards under the heading rather than a wall. */
.faq { display: flex; flex-direction: column; }
.faq > .split { order: 1; }
.faq > .facts { order: 2; margin-top: 40px; }
.faq > .acc   { order: 3; margin-top: 56px; }
/* the panel steps back; the boxes carry the structure */
.facts { background: none; padding: 0; }
.facts dl { gap: 14px; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); }
/* Six boxed facts: auto-fit lands on four columns at desktop, which left
   the last row two-thirds empty. Three columns makes two even rows, and
   the auto-fit below still handles tablet and phone. */
@media (min-width: 1101px) {
  .facts dl { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
.facts dl > div {
  background: #fff;
  border: 1px solid #e7ded4;
  border-radius: 12px;
  padding: 18px 20px;
  box-shadow: 0 6px 18px rgba(33, 26, 22, .05);
  transition: transform .4s cubic-bezier(.2,.7,.2,1), box-shadow .4s ease, border-color .4s ease;
}
.facts dl > div:hover {
  transform: translateY(-3px);
  box-shadow: 0 14px 30px rgba(33, 26, 22, .10);
  border-color: #ddd0c0;
}
.facts dd { margin-top: 8px; }
@media (prefers-reduced-motion: reduce) { .facts dl > div { transition: none; } }
/* The slider-era layout for this section is gone with the slider: .work was
   a two-column grid (slider left, a 320px stack of project cards right), and
   it was still squeezing the new four-up into 966px of a 1320px shell. The
   orphaned brace it left behind went with it. */

/* The three proof points used to sit above the cost-plus comparison charts.
   The charts are gone — they argued against other builders rather than for
   us (Rashi, 2026-10-27) — so the points moved into the statement section,
   where they now carry the argument on their own. Three even columns on
   desktop so the line lengths match; the shared .claims max-width is for a
   flex row, hence the override. */
.claims--stmt { margin-top: 36px; }
@media (min-width: 861px) {
  .claims--stmt { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 28px; }
  .claims--stmt li { max-width: none; }
}
.stmt__cta { margin-top: clamp(28px, 3.5vw, 44px); }
/* ── Content sits UNDER its heading ───────────────────────────────────────
   .split is a heading paired with a short action (a link or a button). When
   a LEDE ends up in one, it must break to its own full-width row instead of
   sitting in the right-hand column. */
.split > .sec-sub { flex: 1 0 100%; max-inline-size: none; }
.sx__top, .pg__top, .sig__top, .ab-top { max-inline-size: none; }
.cband .pg__top { max-inline-size: 60ch; }   /* the centred CTA band keeps its measure */


/* ── One rule, so this stops being a per-section argument ────────────────
   Text that follows a heading runs the full width between the page padding.
   Narrow `ch` measures were scattered across seven stylesheets and put
   every lede, note, FAQ answer and body paragraph in the left half of its
   section with the right half empty. Anything that still wants a measure
   must CENTRE itself (margin-inline:auto), never hug the left edge. */
.qa__b, .prose, .sx__outro, .gantt__note, .pil__out, .pm__sub,
.sproc__b, .rise__b, .rung__b, .sacc__a > div, .route__qs, .ct__claim,
.ct__rest, .pf__lede, .acc, .step p, .founder p, .rev blockquote {
  max-inline-size: none; max-width: none;
}
/* ── mobile fixes, reviewed on a 390px screen — placed last so it wins ─────────────────────────────
   Six separate faults, all of them only visible below 1100px. */
@media (max-width: 1100px) {

  /* 1 · THE SERVICES TRACK WOULD NOT SLIDE. The pin becomes a column here and
     nothing capped its width, so the viewport measured 1974px on a 390px
     screen — as wide as the track inside it. overflow-x had nothing to
     scroll because scrollWidth and clientWidth were equal. Cap the chain. */
  /* 2 · the stars sat on their own line under the 4.9. They now sit beside it,
     and the block stays LEFT aligned — centring it read as odd against the
     testimonial underneath, which is left aligned. */
  .revs__score { display: inline-block; vertical-align: middle;
    font-size: clamp(56px, 17vw, 76px); margin-block-start: 18px; margin-inline-end: 14px; }
  .revs__stars { display: inline-block; vertical-align: middle; margin-block-start: 18px; }
  /* 3 · the area names ran full width with one shared photo underneath.
     Smaller type and a smaller figure put them side by side instead. */
  .alist { margin-top: 26px; gap: 9px; }
  .alist a { height: auto; padding: 10px 16px; font-size: 15px; }
  body.v2 .areas .alist a.on, body.v2 .areas .alist a:hover { font-size: 15px; }
  /* 4 · six full-width fact boxes ran 740px before the questions started */
  body.v2 .facts dl { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
  .facts dl > div { padding: 12px 13px; border-radius: 10px; }
  .facts dt { font-size: 10px; letter-spacing: .1em; }
  .facts dd { margin-top: 5px; font-size: 12.5px; line-height: 1.45; }
  /* 5 · the open menu is 1326px of content in a 780px box, and it could not
     be scrolled — overflow-y was visible, so the rest was unreachable. */
  .mnav { overflow-y: auto; -webkit-overflow-scrolling: touch; overscroll-behavior: contain; }
  /* the 24px gap beside the plus icon cost the question a line of wrap */
  .qa summary { gap: 12px; }
  .qa summary h3 { font-size: 16px; line-height: 1.5; }
  /* 8 · the number was hidden on mobile, so there was no way to call from the
     header. It becomes a dial button beside Book — icon only, because the full
     number does not fit next to a logo and a burger at 390px. And Book itself
     was 16px of padding around four letters, which read as a blob. */
  .hdr__tel { display: inline-flex; inline-size: 44px; block-size: 44px;
    align-items: center; justify-content: center; gap: 0;
    border: 1px solid rgba(252,246,242,.35); border-radius: 999px; color: inherit; }
  .hdr__tel-ico { display: block; }
  .hdr__tel-num { display: none; }
  .hdr__act { gap: 10px; }
  .hdr__act .btn--sm { padding: 0 30px; }
  /* 9 · the footer ran 1286px as a single column */
  body.v2 .ft__cols { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 26px 20px; }
  /* 10 · the wordmark is parallaxed 40px inside an overflow:hidden box, so at
     rest its bottom 40px was clipped and it read as a cut-off logo. Give the
     box the travel distance back as padding. */
  /* Footer links were 32px tall and the phone line 24px — fine to click with a
     mouse, mean with a thumb, and they sit in a dense column where a mis-tap
     lands on the neighbour. */
  /* the address links are inline, so padding alone never changed their
     height — they stayed a 24px line in a dense stack */
  body.v2 .ft__cols a, body.v2 .ft address a { display: block; padding-block: 11px; }
  /* the wordmark link was a 28px strip */
  body.v2 .brand { min-block-size: 44px; align-items: center; }
  /* Four more controls measured under 40px on a phone. Each fix grows the hit
     box without moving anything: padding on an inline element never changes
     the line box, and the arrow links keep their underline because the extra
     room is added above it, not below. */

  /* The arrow links were 33px. Padding alone is unreliable here: the
     accent underline sits on the padding box, so growing the box downward
     would float the rule away from the text — the exact fault Rashi caught
     on the slider. An overlay extends the hit area above and below without
     touching layout or the underline, and it covers the shorter variants
     (the 15px-icon one in the reviews header measured 35px) too. */
  body.v2 .link { padding-block-start: 11px; position: relative; }
  body.v2 .link::after { content: ""; position: absolute;
    inset-block: -7px; inset-inline: 0; }
  /* the closing CTA's phone number was a 24px line — the one number on the
     page a thumb is most likely to go for */
  body.v2 .close__tel a { padding-block: 10px; }
  /* 12px legal links, 15px tall, three in a row */
  body.v2 .ft__legal a { padding-block: 14px; }
  /* the design-centre address link sits inside a sentence, so it stays inline
     and only the hit area grows */
  body.v2 .dcx__meta a { padding-block: 13px; }
  /* same three on the inner templates: the article table of contents (33.6px
     blocks in a dense stack), every link inside body copy, and the service
     card titles, which are the card's only hit area — nothing here stretches
     to cover the card. */
  body.v2 .post__toc a { padding-block: 12px; }
  body.v2 .prose a, body.v2 .doc__in .prose a { padding-block: 12px; }
  body.v2 .sfeat__c h3 a { padding-block: 6px; }
  /* the gallery's closing link sits in a sentence, same inline treatment */
  body.v2 .sx__outro a { padding-block: 13px; }
  /* 10px uppercase micro-labels are below what holds up on a phone screen. */
  body.v2 .facts dt { font-size: 11px; }
}
/* The header carries a logo, a dial button, Book and a burger. At 390px that
   totals 312px against 335px of room; below 390 it did not fit, so everything
   in the bar steps down together rather than one piece being dropped. */
@media (max-width: 380px) {
  body.v2 .brand__mark { block-size: 28px; }
  body.v2 .hdr__act { gap: 8px; }
  body.v2 .hdr__act .btn--sm { padding: 0 16px; font-size: 13px; min-height: 40px; }
  /* the dial button keeps its 44px hit box — only the ring shrinks, drawn
     by the icon's own box, so a thumb still gets the full target */
  body.v2 .hdr__tel { inline-size: 40px; block-size: 44px; }
}
/* Six claims make the band two rows on a phone — 201px at 390 and 233px at
   360, where it used to be one row of four. The hero reserved 180px for it,
   so the CTA ran into the band at 360. Reserve what the band actually needs. */
/* The band is absolutely pinned to the bottom of the hero, so the hero has to
   RESERVE room for it — and with six claims wrapping differently at every
   width, any reserve is wrong somewhere. At 390 it held back 255px for a
   201px band: 54px of dead space under the stats.

   On a phone the band simply sits in the flow at the end of the hero, so the
   hero is exactly as tall as what it contains and the reserve disappears. */
@media (max-width: 1100px) {
  body.v2 .hero { padding-bottom: clamp(36px, 7vw, 56px); }
  body.v2 .hero__proof { position: static; margin-block-start: clamp(26px, 6vw, 42px);
    /* in flow it would sit inside the hero's gutter and stop being a band;
       pull it back out to the edges and re-apply the gutter as padding so the
       stats still line up with the headline above them */
    margin-inline: calc(var(--pad) * -1); padding-inline: var(--pad); }
}
/* ── text-wrap ────────────────────────────────────────────────────────────
   global.css puts `text-wrap: balance` on every heading, which equalises the
   LINE LENGTHS rather than filling the column. On a wide screen that stops a
   short title dropping one orphan word, which is what it is for. In a narrow
   column it does the opposite of what you want: measured on a phone, headings
   were filling as little as 52% of the width available and reading as cramped
   into a box far narrower than the page.

   `pretty` keeps the orphan protection without equalising, so a line runs to
   the edge before it wraps. */
@media (max-width: 1100px) {
  body.v2 h1, body.v2 h2, body.v2 h3, body.v2 h4,
  body.v2 .sec-title, body.v2 .stmt__h { text-wrap: pretty; }
}
/* A FAQ question is a sentence, not a display heading — balance never suits
   it at any width. */
body.v2 .qa h3 { text-wrap: pretty; }
/* ── phone section rhythm ──────────────────────────────────
   Every band carries padding: clamp(60px,9vw,100px), which on a phone
   bottoms out at 60/60 — so 120px of nothing sits between each pair of
   sections. That is one seventh of the screen, repeated. On the custom
   build page, fifteen bands, 1800px of empty scroll. The desktop rhythm
   does not need to survive the trip down; 44px still separates them.
   body.v2 outranks the single-class rules in the per-template sheets,
   whatever order they load in. */
@media (max-width: 760px) {
  /* 44 top + 44 bottom put 88px of nothing between every pair of sections on
     a 375px screen, which reads as the page drifting apart rather than as
     rhythm. 32 and 32: still a clear break, a third less dead scroll, and
     deliberately the SAME number top and bottom so no section looks like it
     is sitting closer to the one above than the one below. */
  body.v2 main > section:not(.hero),
  body.v2 main > div.sx,
  body.v2 main > article > div.post { padding-block: 32px; }
}
/* ── phone hero typography and stat grid ───────────────────
   Two things only a phone shows. The headline is set in three authored
   lines; at 38px the first one needs 384px and the column is 335, so it
   wrapped and left "and" alone on a line of its own. Size the type off
   the viewport so each authored line stays one line down to 320px, and
   keep 38px as the ceiling so nothing above ~437px moves.

   The six claims sat in three 102px columns — too narrow for the body
   line under each label, which broke to three lines and left holes. Two
   columns gives each one ~168px and reads as a list rather than a grid
   of fragments. */
@media (max-width: 560px) {
  body.v2 .hero__h1 { font-size: min(38px, calc(9.6vw - 4px)); }
  body.v2 .hero__proof { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
/* ── phone eyebrows ────────────────────────────────────────
   .eyebrow is a flex row, so "Custom home builder" and "Toronto & the GTA"
   are separate flex items. On a phone neither fits its share of the row,
   so both wrapped to two lines and the dot between them ended up floating
   in the gap. Block layout lets the label read as one sentence that wraps
   where it runs out of room; the leading rule and the dot go inline so
   they keep their place in it. */
@media (max-width: 560px) {
  body.v2 .eyebrow { display: block; }
  body.v2 .eyebrow .dot { margin-inline: 8px; }
}
/* ── phone: controls beside what they drive ────────────────
   Two service-page widgets put a list of choices above the thing the
   choice changes. On a desktop they sit side by side and you see both.
   Stacked on a phone the pair runs 746px (.where) and 654px (.soff)
   against an 812px screen minus a 64px header — so you tap a type and
   the drawing that just changed is off the bottom of the screen.

   These live here rather than in v2-svc.css / v2-service.css because
   body.v2 outranks those files' single-class rules whatever order the
   sheets load in — the same reason the rest of the mobile layer is at
   the end of this file. */
@media (max-width: 760px) {

  /* home-addition: a list of six addition types driving a house drawing
     and a note. The drawing is compact, so it genuinely fits beside the
     list; the note is prose and would be unreadable in a 140px column,
     so `display: contents` lifts the figure and the note out of their
     wrapper and the note spans the full width underneath. */
  body.v2 .where__in {
    grid-template-columns: minmax(0, 1fr) clamp(132px, 22vw, 190px);
    gap: 12px 14px;
    align-items: start;
  }
  body.v2 .where__in > div:not([class]) { display: contents; }
  body.v2 .where__list { grid-column: 1; grid-row: 1; }
  body.v2 .where__fig  { grid-column: 2; grid-row: 1; }
  body.v2 .where__note { grid-column: 1 / -1; grid-row: 2; margin-top: 4px; padding-inline: 0; }
  body.v2 .where__list button { padding: 9px 10px; gap: 9px; font-size: 13px; line-height: 1.3;
    min-block-size: 44px; }
  body.v2 .where__list button em { font-size: 11px; }
  body.v2 .where__key { gap: 7px 12px; margin-top: 10px; font-size: 11px; }
  body.v2 .where__key i { inline-size: 15px; block-size: 9px; }
}
/* v2-service.css stacks .where__in below 52rem, so the side-by-side has to
   reach the same edge or 761-832px keeps the 898px stacked version. */
@media (min-width: 761px) and (max-width: 52rem) {
  body.v2 .where__in {
    grid-template-columns: minmax(0, 1fr) clamp(150px, 22vw, 190px);
    gap: 14px 18px;
    align-items: start;
  }
  body.v2 .where__in > div:not([class]) { display: contents; }
  body.v2 .where__list { grid-column: 1; grid-row: 1; }
  body.v2 .where__fig  { grid-column: 2; grid-row: 1; }
  body.v2 .where__note { grid-column: 1 / -1; grid-row: 2; margin-top: 4px; padding-inline: 0; }
}
@media (max-width: 760px) {

  /* home-renovation and commercial-construction: a list of renovation
     types driving a panel of prose. Prose cannot take a half-width
     column on a phone, so the list becomes a scrolling strip of tabs
     and the panel sits directly under it — 48px of control above the
     thing it changes instead of 376px. It is already role="tablist",
     so the semantics do not change. */
  body.v2 .soff__list {
    grid-template-columns: none;
    grid-auto-flow: column;
    grid-auto-columns: max-content;
    gap: 8px;
    overflow-x: auto;
    scroll-snap-type: x proximity;
    scrollbar-width: none;
    margin-inline: calc(var(--pad) * -1);
    padding: 2px var(--pad) 6px;
  }
  body.v2 .soff__list::-webkit-scrollbar { display: none; }
  body.v2 .soff__list button {
    white-space: nowrap;
    scroll-snap-align: start;
    min-block-size: 44px;
    font-size: 13.5px;
  }
  /* the hover nudge reads as a glitch when a tap triggers it */
  body.v2 .soff__list button:hover { transform: none; }
  body.v2 .soff__panel { min-block-size: 0; }
}
/* ── typical duration on a service card ────────────────────
   Competitors put a number on every service; ours were buried in the FAQ
   at the bottom of each page. A range is the second thing a homeowner
   wants after price, so it goes on the card. Set in the accent face so it
   reads as a fact, not as more body copy. */
/* Sits at the FOOT of the card, not above the title. Two of the six boxes
   have no duration (laneway has no sourced range, project management runs
   the length of whatever it manages), and when the line sat above the title
   those two pushed their headings a line higher than their row neighbour's.
   margin-block-start:auto in a flex column pins it to the bottom instead, so
   every row lines up whether the line is there or not. */
.svc__dur{display:block;order:9;margin-block-start:auto;padding-block-start:14px;
  font-family:var(--fd);font-size:12.5px;letter-spacing:.08em;color:var(--accent)}
/* ── homeowner reviews ─────────────────────────────────────
   Cards, because that is what this section is elsewhere and a bare run of
   text read as unfinished rather than as honest. Kept quiet: a hairline,
   padding and a radius, no shadow and no quote glyph. The long reviews are
   the asset, so the type is sized for reading.

   The rating block had `4.9`, the stars, the count and the link on one
   baseline-aligned flex row; the stars carry line-height:0, so the count
   and the link rode up into them and collided. It stacks now. */
/* Its own section box — the old `.revs` is a two-column grid for the
   carousel the other pages still use. */
.grev{background:var(--g-revs);padding:clamp(60px,9vw,100px) var(--pad)}
.grev__head{display:flex;align-items:flex-end;justify-content:space-between;
  gap:clamp(20px,3vw,40px);flex-wrap:wrap}
.grev__head>div:first-child{flex:1 1 320px;min-inline-size:0}
/* flex:none matters. As a shrinkable item next to a long heading this block
   was compressed to a few pixels wide, and "29 Google reviews" rendered as
   an illegible smear rather than wrapping. */
.grev__rating{display:flex;flex-direction:column;align-items:flex-start;gap:6px;
  flex:none;white-space:nowrap}
.grev__rate{display:flex;align-items:center;gap:12px;margin:0}
.grev__rate b{font-family:var(--fh);font-size:40px;line-height:1;letter-spacing:-.03em;color:var(--heading)}
.grev__stars{display:inline-flex;line-height:0}
.grev__count{margin:0;font-size:13px;color:var(--ink-500)}

/* A moving track rather than a static grid (Rashi, 2026-10-28). Three cards
   at a time on a wide screen, two on a tablet, one on a phone — set by one
   custom property so the JS does not have to know the breakpoints. Underneath
   it is an ordinary scroll-snap list, so it swipes by hand and needs no
   script to be usable. */
.grev__vp{--per:3;--gap:clamp(18px,2vw,26px);
  margin-block-start:clamp(30px,4vw,48px);
  overflow-x:auto;scroll-snap-type:x mandatory;scroll-behavior:smooth;
  scrollbar-width:none;overscroll-behavior-x:contain}
.grev__vp::-webkit-scrollbar{display:none}
.grev__track{display:flex;gap:var(--gap);list-style:none;margin:0;padding:0}
.grev__card{flex:0 0 calc((100% - (var(--per) - 1) * var(--gap)) / var(--per));
  scroll-snap-align:start}
@media (max-width:1100px){ .grev__vp{--per:2} }
@media (max-width:700px){ .grev__vp{--per:1} }

.grev__dots{display:flex;align-items:center;gap:6px;margin-block-start:20px}
.grev__dots button{inline-size:44px;block-size:44px;padding:0;border:0;background:none;
  cursor:pointer;display:grid;place-items:center}
.grev__dots button i{display:block;inline-size:8px;block-size:8px;border-radius:4px;
  background:var(--ink-300);transition:inline-size .3s var(--snap),background .3s ease}
.grev__dots button[aria-selected=true] i{inline-size:38px;background:var(--accent)}
.grev__dots button:focus-visible{outline:2px solid var(--accent-ink);outline-offset:2px}
.grev__card{display:flex;flex-direction:column;background:#fff;
  border:1px solid var(--rule,rgba(40,31,26,.12));border-radius:14px;
  padding:clamp(20px,2.2vw,28px)}
.grev__q{margin:0;font-size:15.5px;line-height:1.68;color:var(--ink-700);
  font-style:normal;text-wrap:pretty}
.grev__by{margin-block-start:auto;padding-block-start:16px;display:flex;
  align-items:baseline;gap:9px;flex-wrap:wrap}
.grev__name{font-family:var(--fh);font-size:15.5px;font-weight:600;color:var(--heading)}
.grev__job{font-family:var(--fd);font-size:11px;letter-spacing:.09em;color:var(--accent-ink,#8a4b06)}
.grev__when{font-size:12px;color:var(--ink-400)}

/* ── recent projects ───────────────────────────────────────
   Three cards across, after Georgian's block, which Rashi picked out. Their
   measurements: ~16:9 image cropped to fill, uppercase serif title sitting
   directly under it with no gap. The note underneath is ours. No carousel —
   three things on a page do not need one, and the one that was here briefly
   hid two thirds of the section at any moment. */
/* Four across now that Cliffcrest is back. At 1512 that is ~300px a card,
   within a whisker of Georgian's 272px, and it keeps the whole set on one
   row the way theirs is. */
.wk{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));
  gap:clamp(18px,2vw,28px);list-style:none;margin:clamp(30px,3.8vw,46px) 0 0;padding:0}
.wk__fig{margin:0}
.wk__img{inline-size:100%;block-size:auto;display:block;object-fit:cover}
.wk__cap{margin-block-start:16px}
/* Uppercase serif, set small — the title is a label for the photograph, not
   a headline competing with the section's own. */
.wk__n{display:block;font-family:var(--fh);font-size:15px;font-weight:500;line-height:1.3;
  text-transform:uppercase;letter-spacing:.03em;color:var(--heading)}
.wk__note{margin:10px 0 0;font-size:14.5px;line-height:1.62;color:var(--ink-600);text-wrap:pretty}

@media (max-width: 1200px){
  .wk{grid-template-columns:repeat(2,minmax(0,1fr));gap:clamp(22px,2.6vw,34px)}
}
@media (max-width: 620px){
  .wk{grid-template-columns:1fr;gap:30px}
}

/* ── project lightbox ──────────────────────────────────────
   The photo links open the full-size file on their own; this is the
   in-page version. <dialog> brings Esc, the backdrop and the focus trap,
   so there is no keyboard handling to write. */
.wk__a{display:block;position:relative;cursor:zoom-in}
.wk__a:focus-visible{outline:2px solid var(--accent-ink);outline-offset:3px}
.wk__a::after{content:"";position:absolute;inset:0;background:rgba(28,23,20,0);
  transition:background .25s ease}
.wk__a:hover::after{background:rgba(28,23,20,.14)}
@media (prefers-reduced-motion: reduce){.wk__a::after{transition:none}}

/* A <dialog> centres itself with the UA's `margin:auto`, which this site's
   `*{margin:0}` reset wipes out — so it pinned to the top-left corner.
   Centre it explicitly rather than relying on the default. */
.lb{position:fixed;inset:0;margin:auto;border:0;padding:0;background:transparent;
  inline-size:fit-content;block-size:fit-content;
  max-inline-size:min(94vw,1200px);max-block-size:92vh;overflow:visible}
.lb::backdrop{background:rgba(20,16,14,.86)}
.lb__img{display:block;inline-size:100%;block-size:auto;max-block-size:82vh;
  object-fit:contain;border-radius:2px;background:var(--ink-800)}
.lb__cap{margin:12px 0 0;font-family:var(--fd);font-size:12px;letter-spacing:.09em;
  text-transform:uppercase;color:#fcf6f2}
.lb__x{position:absolute;inset-block-start:-48px;inset-inline-end:0;
  inline-size:44px;block-size:44px;display:grid;place-items:center;
  border:0;background:none;color:#fcf6f2;cursor:pointer}
.lb__x:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
@media (max-width: 760px){
  .lb{max-inline-size:94vw}
  .lb__x{inset-block-start:-46px}
}

/* ── the claim ticks ───────────────────────────────────────
   ⚠ RESTORED 2026-10-28. The base .claims rules lived inside the price
   comparison block, and went out with it when that section was removed the
   day before — so every tick on the homepage, the service pages and the
   location pages dropped to its own line above the sentence it belonged to.
   Nothing in the markup changed; the layout rule simply stopped existing.

   The tick is beside the text, not above it: it is a 20px mark and a line of
   type, and stacking them reads as two separate things. */
.claims{display:flex;flex-wrap:wrap;gap:clamp(18px,2.4vw,30px);list-style:none;margin:34px 0 0;padding:0}
.claims li{display:flex;align-items:flex-start;gap:11px;max-inline-size:360px;
  font-size:15px;line-height:1.6;color:var(--ink-700)}
.claims li svg{flex:none;inline-size:20px;block-size:20px;margin-block-start:1px}
.claims--light li{color:var(--ink-300)}
@media (max-width: 760px){
  .claims li{max-inline-size:none}
}
/* The restored .claims rule above sits later in the file than .claims--stmt,
   so its `display:flex` was winning and the statement section's three even
   columns had become a flex row. Put the grid back, after it. The LI stays
   flex either way — that is what keeps the tick beside its sentence. */
@media (min-width: 861px) {
  .claims--stmt { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 28px; }
  .claims--stmt li { max-inline-size: none; }
}


/* ── the dash beside every eyebrow is gone ─────────────────
   Rashi, 2026-10-28: "remove these weird line things ... a line under the
   subheading where it's needed is fine, but putting these lines next to it
   is weird."

   It was doing a second kind of damage too. `<i class="rule">` is a 40px
   bar and the eyebrow is a flex row with a 14px gap, so every label sat
   54px to the RIGHT of the heading underneath it — two lines of a single
   title block that never shared a left edge. Hiding the bar is what puts
   them back in the same column; no markup had to change on 17 files.

   Scoped to `.eyebrow .rule` on purpose: `.rule` is used on its own
   elsewhere (the hero's drawn underline, the process column) and those stay. */
.eyebrow .rule { display: none; }

/* The way through to the full areas list, under the "also serving" line. */
.areas__all{margin-block-start:22px}


/* ── phone: the service cards need an edge ─────────────────
   Stacked in one column they were image, rule, heading, paragraph, repeat,
   with nothing marking where one service ended and the next began — Rashi,
   2026-10-28: "you can't tell where design and permit stops and where home
   addition starts ... it's looking cluttered."

   A white card on the section's warm off-white, with a hairline and the
   quietest possible shadow. The photograph stays flush to the card's top
   edge and the text is inset, so the card reads as one object. */
@media (max-width: 760px) {
  body.v2 .svcs__track { gap: 16px; }
  body.v2 .svc {
    background: #fff;
    border: 1px solid rgba(40, 31, 26, .10);
    border-radius: 14px;
    overflow: hidden;
    box-shadow: 0 1px 3px rgba(40, 31, 26, .05);
  }
  body.v2 .svc__ph { border-radius: 0; }
  /* the rule, heading, body and duration are inset; the image is not */
  body.v2 .svc__rule { margin-inline-start: 18px; margin-block-end: 20px; }
  body.v2 .svc h3,
  body.v2 .svc p,
  body.v2 .svc__dur { padding-inline: 18px; }
  body.v2 .svc__dur { padding-block-end: 18px; }
  body.v2 .svc p:last-child { padding-block-end: 18px; }
}

/* ── phone: the FAQ section is the FAQs ────────────────────
   The key-facts list sat above the questions — six term/definition pairs
   before a reader reached the first one. On a phone that is a screen and a
   half of preamble in front of the thing the section is for. Rashi,
   2026-10-28: "remove the cards under the FAQs ... I just want the FAQs to
   be visible."

   Hidden, not deleted: it stays in the markup for its speakable schema and
   on every screen wide enough to carry it. */
@media (max-width: 760px) {
  body.v2 .facts { display: none; }
}

/* The caption plate is gone: the area name now lives in the list itself, and
   a second copy over the photograph duplicated it. */
.areas__fig figcaption { display: none; }

/* ── RECENT PROJECTS: THE LEAD CARD ────────────────────────────────────────
   The Treelawn Parkway lead, added 2026-10-28. .wk is a fixed four-column grid, so a fifth
   ordinary card lands alone on a second row. The lead spans the full track
   and puts its write-up BESIDE the photograph instead of under it, which is
   what makes a five-project section read as one composition. */
body.v2 .wk__item--lead { grid-column: 1 / -1; margin-block-end: clamp(10px,1.4vw,20px); }
body.v2 .wk__item--lead .wk__fig { display: grid; gap: clamp(20px,2.6vw,40px);
  grid-template-columns: minmax(0,1.35fr) minmax(0,1fr); align-items: center; }
body.v2 .wk__item--lead .wk__cap { margin-block-start: 0; }
body.v2 .wk__item--lead .wk__n { font-size: clamp(22px,2.4vw,30px); }
body.v2 .wk__item--lead .wk__note { margin-block-start: 12px; font-size: 16px; }
body.v2 .wk__tag { display: inline-block; margin-block-end: 10px;
  font-family: var(--fb); font-size: 11px; letter-spacing: .18em;
  text-transform: uppercase; color: var(--accent-600,#cb7319); }
@media (max-width: 860px) {
  body.v2 .wk__item--lead .wk__fig { grid-template-columns: 1fr; gap: 16px; }
  body.v2 .wk__item--lead .wk__n { font-size: clamp(20px,5vw,24px); }
}

/* ── "CAN'T FIND YOUR NEIGHBOURHOOD?" ──────────────────────────────────────
   The tail of the areas section, shared by all seven templates that carry
   it. Sits on the dark photographic backdrop, so the type is light and the
   ghost button has a light border rather than the default dark one. */
body.v2 .areas__cta { margin-block-start: clamp(26px,3vw,38px); }
body.v2 .areas__ask { font-family: var(--fh); font-weight: 600;
  font-size: clamp(19px,2vw,24px); letter-spacing: -.02em; color: #fcf6f2; }
body.v2 .areas__askb { margin-block-start: 8px; max-inline-size: 56ch;
  font-size: 15px; line-height: 1.6; color: var(--ink-300); }
body.v2 .areas__btns { display: flex; flex-wrap: wrap; gap: 12px;
  margin-block-start: 20px; }
body.v2 .areas__btns .btn--ghost { color: #fcf6f2; border-color: rgba(252,246,242,.4); }
body.v2 .areas__btns .btn--ghost:hover { background: rgba(252,246,242,.1);
  border-color: #fcf6f2; }

/* ── LICENCES AND MEMBERSHIPS ──────────────────────────────────────────────
   Four credentials, one row. Two carry a real badge image and two carry a
   typographic mark, so .cred__badge is a FIXED-HEIGHT box rather than
   image-sized — otherwise the two typographic cards sat higher than the two
   with badges and the four headings never lined up. */
.creds { background: var(--g-white,#fff); padding: clamp(60px,9vw,100px) var(--pad); }
.creds__note { font-family: var(--fd); font-style: italic; font-size: 15px;
  color: var(--ink-600); max-inline-size: 34ch; }
.creds__grid { display: grid; gap: clamp(22px,3vw,40px); margin-block-start: clamp(30px,4vw,46px);
  grid-template-columns: repeat(4, minmax(0,1fr)); list-style: none; padding: 0; }
@media (max-width: 1000px) { .creds__grid { grid-template-columns: repeat(2, minmax(0,1fr)); } }
@media (max-width: 520px)  { .creds__grid { grid-template-columns: 1fr; gap: 26px; } }

.cred__badge { block-size: 76px; display: flex; align-items: center;
  justify-content: flex-start; margin-block-end: 16px; }
.cred__badge--type { gap: 10px; }
.cred__stars { font-size: 17px; letter-spacing: .08em; color: var(--accent-500,#f78c1e); }
.cred__fig { font-family: var(--fh); font-weight: 600; font-size: 30px;
  letter-spacing: -.03em; color: var(--heading); }
.cred h3 { margin: 0 0 8px; font-family: var(--fh); font-weight: 600;
  font-size: clamp(17px,1.7vw,20px); letter-spacing: -.02em; color: var(--heading); }
.cred p { margin: 0; font-size: 14.5px; line-height: 1.6; color: var(--ink-700); }

/* ── LIVE-SITE COLLISION GUARD ─────────────────────────────────────────────
   This file is a plain <link> in <head>; Astro's bundled global.css +
   project.css load AFTER it, so on any class they BOTH define, the live
   site wins on source order. Nine class names overlap. Most are harmless —
   .btn, .svc and .sec-title measured identical either way — but these did
   not, so they are restated here under body.v2 (0,2,x) to beat project.css
   (0,1,x) regardless of order.

   ⚠ project.css also hangs a decorative bar off .eyebrow::after. That is
   exactly the "weird line in the subheadings" Rashi had removed from the
   redesign, and it would have come straight back on the live homepage. */
/* ⚠ NO `color` HERE. project.css sets display, font, size, tracking, colour
   AND a margin on .eyebrow; only the non-colour half needs restating,
   because the SECTIONS own the colour — .svcs/.proc/.close resolve it from
   --t-muted and .areas/.hero from --ink-300. Declaring colour at this
   specificity overrode that theming and made every light-section eyebrow
   --ink-600 where the approved design is --ink-700. Leave it to the
   cascade. project.css's `margin-block-end` is reset because v2 spaces the
   eyebrow with the rule element beneath it instead. */
body.v2 .eyebrow { display: flex; align-items: center; gap: 14px;
  font-size: 11px; letter-spacing: .22em; font-weight: 700;
  font-family: var(--fb); text-transform: uppercase; margin-block-end: 0; }
body.v2 .eyebrow::after, body.v2 .eyebrow::before { content: none; display: none; }
/* ⚠ DO NOT add a blanket `body.v2 .eyebrow--light` rule here. The sections
   theme themselves light or dark (.svcs/.proc/.dc/.areas/.close swap
   --t-strong/--t-muted), and .eyebrow--light is DARK inside a light section.
   A blanket override at this specificity beats that theming and turned the
   services eyebrow pale-on-cream — measured #cebeb5 where the approved
   design is #4e423c. Only the hero is restated, because project.css's
   `.on-dark .eyebrow` would otherwise claim it. */
body.v2 .hero.on-dark .eyebrow { color: var(--ink-300); }
body.v2 .sec-sub { margin-top: 22px; font-size: clamp(15px,1.3vw,17px);
  line-height: 1.65; color: var(--ink-600); }
body.v2 .sec-title { font-size: clamp(30px,4.2vw,52px); }

/* ── THE TYPOGRAPHY LAYER, PORTED ──────────────────────────────────────────
   ⚠ THE APPROVED DESIGN WAS NEVER v2-home.css ALONE. In the preview this
   file is loaded first and the v2 project.css + redesign.css land on top of
   it, so the heading scale, the heading colour and the .sec-sub colour all
   come from THOSE, not from here. Porting only this stylesheet produced a
   page that was right in structure and wrong in type: measured against the
   preview, section titles were 52px instead of 41.6px, headings came out
   near-black (#1d1f21) instead of the warm #3a2e26, and .sec-sub was
   --ink-600 rather than --ink-700.

   These are the exact rules responsible, scoped to body.v2 so they reach
   this page and no other. Sources:
     website-v2/src/styles/project.css  :90   .sec-title size
     website-v2/src/styles/project.css  :1285 heading colour, light and dark
     website-v2/src/styles/redesign.css :15   heading tracking and leading
     website-v2/src/styles/redesign.css :16   .sec-sub colour
   If the rest of the site moves to this design later, these belong in the
   shared stylesheet and this block should go. */
body.v2 .sec-title { font-size: clamp(1.9rem, 1.6rem + 1.5vw, 2.6rem); text-wrap: balance; }
body.v2 .sec-title, body.v2 h1, body.v2 h2 { letter-spacing: -.035em; line-height: 1.02; }
body.v2 .sec-sub { color: var(--ink-700); }
body.v2 .on-dark .sec-sub { color: var(--ink-300); }
body.v2 h1, body.v2 h2, body.v2 h3,
body.v2 .sec-title, body.v2 .hero__badge { color: var(--heading); }
/* ⚠ EVERY HEADING THAT SITS ON A PHOTOGRAPH BELONGS IN THIS LIST.

   The rule above is `body.v2 h1` — specificity (0,1,2). A page stylesheet
   that colours its own hero heading, like v2-portfolio.css's

       .pf h1 { color: var(--g-white); }

   is only (0,1,1), so it LOSES and the heading paints dark brown. On the
   portfolio that put "Finished rooms, finished houses." in #3a2e26 over a
   sunlit photograph: present, correctly coloured in its own file, and
   unreadable (Rashi, 2026-10-29: "cannot read the h1, colour is wrong").

   .hero and .on-dark were listed because those were the only scrimmed
   sections when this guard was written. .pf is the portfolio's hero and
   carries .pf__scrim, so it is the same situation under a different class
   name.

   ⚠ Adding a new full-bleed hero section means adding it here IN THE SAME
   EDIT. Its own colour rule will not be enough, and the failure is invisible
   until someone looks at the page over a light part of the image. */
body.v2 .on-dark h1, body.v2 .on-dark h2, body.v2 .on-dark h3,
body.v2 .on-dark .sec-title, body.v2 .hero h1,
body.v2 .pf h1,
/* .cband is the dark call-to-action band on /areas-we-serve/, /my-account/
   and /services/. v2-pages.css already sets `.cband .sec-title { color:#fff }`
   — but that is (0,2,0) and `body.v2 .sec-title` above is (0,2,1), so the
   guard won and the heading painted #3a2e26 on a near-black band. Rashi,
   2026-10-29: "text colour of heading to be fixed". Same failure as .pf h1,
   same cause, different section. */
body.v2 .cband .sec-title { color: var(--white); }

/* ⚠ NOT A COLOUR CHANGE — A RESCUE. .ft__h already says
   `color: var(--accent)` in v2-chrome.css, but that is (0,1,0) and
   `body.v2 h2` above is (0,1,2), so the guard won and the four footer column
   headings — What we build, Company, Areas, Contact — rendered #3a2e26 on a
   #211a16 footer. About 1.3:1, which is why they read as muted grey. Rashi,
   2026-10-29 wants them in the brand orange, which is exactly what the
   footer's own stylesheet was asking for. Third time this guard has eaten a
   component's own heading colour; see .pf h1 and .cband above. */
body.v2 .ft__h { color: var(--accent); }

/* ── THE FORM SITS ON THE PAGE, NOT IN A DARK BOX ──────────────────────────
   Rashi, 2026-10-29: "the form, do not put any background behind it."

   .panel was built for v2's OWN form markup — dark card, light type, fields
   that are bottom-ruled and transparent. The live site embeds the
   Buildertrend form instead, which renders its own white inputs and its own
   dark labels. Dark panel + dark labels = the labels were all but invisible,
   and the white fields sat on brown like holes.

   So the panel keeps its place in the layout and gives up its skin: no
   background, no border, no inherited light colour. padding goes too — the
   embed brings its own. */
/* ⚠ EVERY PAGE THAT EMBEDS THE BUILDERTREND FORM IN A .panel BELONGS IN THIS
   SELECTOR LIST. .close is the home page's band; .ct__in is /contact-us/,
   which is where most of the site's calls to action actually land. Add a
   third page and add it here in the same commit, or that page ships the dark
   card with invisible labels and nothing in the build will say so. */
body.v2 .close .panel,
body.v2 .ct__in .panel {
  background: none;
  border: 0;
  padding: 0;
  color: inherit;
}

/* ── LONG REVIEWS CLAMP, WITH A READ MORE ──────────────────────────────────
   Rashi, 2026-10-29: "hide the long ones with read more like google does"
   and "the first 4 or 5 lines should be visible". Five lines, so the clamp
   is the same on every page that shows reviews.

   Once the truncated reviews were completed from the Business Profile, two
   of them ran past 600 characters and one past 750, which in a card three
   across is a wall of text.

   ⚠ NO JAVASCRIPT, DELIBERATELY. The rule in this codebase is that whatever
   hides an element must be the same thing that reveals it: CSS clamped by a
   script that fails to load leaves the review permanently cut off. The
   control is a visually hidden checkbox plus its label, so it works with
   scripting off, it is keyboard operable, and the :checked state does the
   revealing.

   The input must stay BEFORE the blockquote and the label in the markup --
   `~` only reaches forward. Reviews.astro decides which cards get a control
   at all, because CSS cannot measure text. */
/* ⚠ NOT position: absolute. THAT IS WHAT BROKE THE PAGE WIDTH.

   With no inset offsets an absolutely positioned box sits at its static
   position, but its containing block becomes the nearest POSITIONED
   ancestor -- and .grev__card, .grev__track and .grev__vp are all static,
   so that was the page. The checkbox escaped the carousel's overflow clip
   and reported itself at x=2011 on a 1695px viewport, stretching the
   document to 2012px and shifting the whole site sideways. Shipped
   2026-10-29, caught by Rashi within the hour: "why did it shift left?"

   A static, zero-size, appearance:none input cannot escape anything. It
   stays a flex item in the card and contributes nothing to layout. The
   focus ring lives on the label via :focus-visible ~ .grev__more, so
   nothing is lost by the input having no size.

   ⚠ If this ever needs positioning again, give .grev__card
   `position: relative` in the SAME edit. */
.grev__toggle { appearance: none; -webkit-appearance: none;
  position: static; inline-size: 0; block-size: 0;
  margin: 0; border: 0; padding: 0; opacity: 0; pointer-events: none; }
.grev__q--clamp { display: -webkit-box; -webkit-box-orient: vertical;
  -webkit-line-clamp: 5; line-clamp: 5; overflow: hidden; }
.grev__toggle:checked ~ .grev__q--clamp { -webkit-line-clamp: unset;
  line-clamp: unset; overflow: visible; }
.grev__more { align-self: flex-start; margin-block-start: 12px; cursor: pointer;
  font-family: var(--fb); font-size: 13.5px; font-weight: 700;
  color: var(--accent-600); border-block-end: 1px solid currentColor;
  padding-block-end: 2px; }
.grev__more .grev__more-b { display: none; }
.grev__toggle:checked ~ .grev__more .grev__more-a { display: none; }
.grev__toggle:checked ~ .grev__more .grev__more-b { display: inline; }
.grev__toggle:focus-visible ~ .grev__more { outline: 2px solid var(--accent-ink);
  outline-offset: 3px; }
