/* Let Us Do It Landscaping — app.css
   Generated from the approved design. Edit here, not in the artifact. */

/* ---------------------------------------------------------------------------
   PALETTE. Sky, 2026-09-22: "make the colors lighter than it is now but not
   white."

   The site was near-black editorial dark mode — the visual language of an
   architecture studio, on a business that sells sunlight and green. This is the
   inversion: a warm limestone ground rather than #FFF, which would glare and
   would also be wrong for the material this company actually works in.

   THE NAMES NO LONGER DESCRIBE THE COLOURS, and that is deliberate — renaming
   46 usages of --bone carries more risk than it removes. Read them as ROLES:

       --ink     the page ground        (now light)
       --ink-2   a raised panel
       --ink-3   a deeper panel
       --bone    primary text           (now dark)
       --sage    secondary text
       --sage-dim  muted / meta text

   Photographic bands stay dark. Light text over a photograph still needs a
   scrim, and a light page with full-bleed dark photo bands is a stronger
   rhythm than either alone. The 28 rgba() scrims are therefore untouched.

   Accents come in pairs. --leaf and --clay are darkened to hold contrast on
   limestone; --leaf-lit and --clay-lit keep the old bright values for the few
   places that sit ON a scrimmed photograph, where the dark versions would
   disappear.
   --------------------------------------------------------------------------- */
:root{
  /* Sky, 2026-09-22, on the first light pass: "a little darker."
     Ground taken down to a mid limestone. NOTE THE SCALE INVERTS: on a mid-tone
     ground the raised panels go LIGHTER, like paper on a desk, rather than
     darker. Solved numerically rather than by eye — every text pair below
     clears WCAG AA (4.5:1) on every one of the three grounds, worst case 4.53,
     and the hierarchy body > secondary > muted holds throughout. */
  --ink:#DED5C1;
  --ink-2:#E5DFD1;
  --ink-3:#EDE8DC;
  --line:#A79A7E;
  --bone:#171E17;
  --sage:#4E5A48;
  --sage-dim:#556050;
  --moss:#4C6146;
  --leaf:#3D7A22;
  --leaf-deep:#2F6B31;
  --clay:#9E5326;
  --clay-dim:#83421D;

  /* For use over photographs and scrims only — see the note above. */
  --leaf-lit:#9BD264;
  --clay-lit:#E08B4F;
  --bone-lit:#EDE7DA;

  --display:"Fraunces",Georgia,"Times New Roman",serif;
  --body:"Karla",-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
  --mono:"IBM Plex Mono",ui-monospace,"SF Mono",Menlo,monospace;

  --gutter:clamp(20px,5vw,72px);
  --maxw:1440px;

  /* parallax depth — see "parallax media layer" below. motion.js reads this. */
  --px:.14;
}

/* ---------------------------------------------------------------------------
   OVER A PHOTOGRAPH, THE TEXT ROLES FLIP BACK.

   Inverting the page to limestone made --bone dark, which is right everywhere
   the ground is limestone and wrong everywhere the ground is a photograph. The
   hero H1 went dark-on-dark and all but vanished — caught by rendering it, not
   by reading it, because every token was "correct" and the suite was green.

   Rather than chase it element by element, the photographic bands REDEFINE the
   tokens for their own subtree. Anything inside them — headings, ledes, ghost
   buttons, figcaptions, plate labels, links added later — inherits the light
   variants automatically and cannot regress.
   --------------------------------------------------------------------------- */
.hero,
.page-hero,
.crew,
.plate,
.nur-shot,
.nurgal-item,
.show-photo,
.fphoto-img{
  /* Setting `color` here is load-bearing, and the reason is worth knowing.
     Custom properties cascade into this subtree, but `color` does NOT
     re-evaluate: the hero h1 declares no colour of its own, so it inherits the
     COMPUTED dark value from body and never looks at the redefined --bone. The
     eyebrow survived only because it happens to declare color:var(--bone)
     explicitly. One declaration here fixes every element that merely inherits.
     Caught by rendering the band; the token was "right" the whole time. */
  color:var(--bone-lit);
  --bone:var(--bone-lit);
  --sage:#C7CEC0;
  --sage-dim:#AAB3A4;
  --leaf:var(--leaf-lit);
  --clay:var(--clay-lit);
  --line:rgba(237,231,218,.26);
}

*{box-sizing:border-box}
body{
  margin:0;
  background:var(--ink);
  color:var(--bone);
  font-family:var(--body);
  font-size:17px;
  line-height:1.6;
  font-weight:300;
  -webkit-font-smoothing:antialiased;
  /* The marquee track is deliberately wider than the screen. overflow-x on <body>
     alone propagates to the viewport, which left the document itself ~29px wider
     than the window on a phone — a real sideways scroll on the homepage, there
     before the page heroes and measured on both. `clip` fixes it without making
     anything a scroll container, which would break the sticky header; the hidden
     line stays as the fallback for browsers that do not know `clip`. */
  overflow-x:hidden;
  overflow-x:clip;
}
html{overflow-x:hidden;overflow-x:clip}
img{max-width:100%;display:block}
a{color:inherit}

.wrap{max-width:var(--maxw);margin:0 auto;padding-inline:var(--gutter)}

/* ---------- type ---------- */
.eyebrow{
  font-family:var(--mono);
  font-size:11px;
  letter-spacing:.18em;
  text-transform:uppercase;
  color:var(--sage);
  display:flex;align-items:center;gap:14px;
  margin:0 0 22px;
}
.eyebrow::before{
  content:"";width:34px;height:1px;background:var(--leaf);flex:none;
  /* When the label wraps to two lines — it does on a phone — a centred rule floats
     between them. Pin it to the first line instead. */
  align-self:flex-start;margin-top:.78em;
}
h1,h2,h3{font-family:var(--display);font-weight:400;text-wrap:balance;margin:0}
h1{
  font-size:clamp(2.6rem,7.2vw,6.4rem);
  line-height:.98;
  letter-spacing:-.025em;
  font-variation-settings:"opsz" 144,"SOFT" 0;
}
h2{
  font-size:clamp(2rem,4.6vw,3.9rem);
  line-height:1.03;
  letter-spacing:-.02em;
  font-variation-settings:"opsz" 96;
}
h3{
  font-size:1.34rem;
  line-height:1.2;
  letter-spacing:-.01em;
  font-variation-settings:"opsz" 24;
}
.lede{
  font-size:clamp(1.05rem,1.7vw,1.32rem);
  color:var(--sage);
  max-width:46ch;
  line-height:1.55;
}
.mono{font-family:var(--mono);font-size:11.5px;letter-spacing:.16em;text-transform:uppercase}

/* ---------- header ---------- */
header.site{
  position:sticky;top:env(safe-area-inset-top,0px);z-index:60;
  background:color-mix(in srgb,var(--ink) 88%,transparent);
  backdrop-filter:blur(14px);
  border-bottom:1px solid var(--line);
  transition:background .22s ease,box-shadow .22s ease;
}
/* Sky, 2026-09-22: "When I scroll the header gets cut in half and only half the number
   shows." The header is 88% opaque, which reads as intentional over the hero at the top
   of the page and reads as damage the moment real content slides under it — the hero
   headline and body copy ghost straight through the bar, and the call button ends up
   sitting on somebody else's letterforms. Sticky and translucent is a combination that
   only works over imagery.

   So it is translucent at rest and solid once it leaves the top, which is also when the
   blur stops earning its cost. Verified by scrolling the real staging page rather than
   by reasoning about it: the first measurement said the header was not sticky at all,
   which turned out to be a bad reading. */
header.site.is-stuck{
  background:var(--ink);
  box-shadow:0 10px 30px rgba(46,38,22,.14);
}
/* Sky, 2026-09-22: "lets make the header bigger so the logo and nav stands out better."
   72px of logo inside 22px of padding is a 116px header, which is the presence a
   landscape contractor's mark should have. But this header is STICKY, and 116px is 15%
   of a laptop viewport to give up permanently — so it shrinks to 72px once you leave the
   top. Padding and logo height are the only things that move, both transform-free and
   both off under prefers-reduced-motion. */
.navrow{
  display:flex;align-items:center;justify-content:space-between;gap:24px;
  padding-block:22px;
  transition:padding-block .22s ease;
}
header.site.is-stuck .navrow{padding-block:12px}
header.site.is-stuck .brand-logo{height:48px}
.brand{display:flex;align-items:center;gap:11px;text-decoration:none}
/* The lockup is 2.09:1 and carries its own wordmark, so it is sized by HEIGHT and the
   width follows. 56px is where "Landscaping, LLC" stops going muddy — measured on the
   header ground, not guessed. Explicit width/height on the <img> keeps the header from
   reflowing while it loads. */
.brand-logo{height:72px;width:auto;display:block;transition:height .22s ease}
@media(max-width:1180px){.brand-logo{height:62px}}
@media(max-width:700px){.brand-logo{height:54px}header.site.is-stuck .brand-logo{height:42px}}
@media(max-width:380px){.brand-logo{height:44px}}
@media (prefers-reduced-motion:reduce){.navrow,.brand-logo{transition:none}}
nav.main{display:flex;gap:32px;align-items:center}
/* Was 14px in --sage, which reads as secondary text rather than as the primary
   navigation. Ink and a little weight is what makes it register as the thing you
   click. */
nav.main a{
  font-size:15.5px;font-weight:500;text-decoration:none;color:var(--bone);letter-spacing:.01em;
  transition:color .2s;
}
nav.main a:hover{color:var(--leaf-deep)}
@media(max-width:1180px){nav.main{gap:24px}nav.main a{font-size:14.5px}}
.callbtn{
  /* Scaled with the nav. At 11.5px against a 15.5px nav the one thing on this bar we
     actually want clicked was the quietest element on it. */
  font-family:var(--mono);font-size:12.5px;letter-spacing:.14em;text-transform:uppercase;
  border:1px solid var(--clay);padding:14px 22px;text-decoration:none;color:var(--bone);
  transition:border-color .2s,background .2s;white-space:nowrap;
}
.callbtn:hover{border-color:var(--clay);background:color-mix(in srgb,var(--clay) 12%,transparent)}
@media(max-width:860px){nav.main{display:none}}
/* "Let Us Do It" broke across two lines on a 390px screen and took the header with it. */

/* ---------- hero ----------
   The homepage headline sits ON the photograph, not above it, so the first screen is
   a picture. Same media layer, same scrim and the same parallax as every inner page —
   one implementation, two places that use it. */
.hero{
  position:relative;overflow:hidden;isolation:isolate;
  min-height:clamp(600px,84vh,920px);
  display:flex;align-items:flex-end;
  border-bottom:1px solid var(--line);
  background:
    radial-gradient(120% 90% at 22% 8%,rgba(200,112,60,.20),transparent 58%),
    linear-gradient(176deg,var(--ink-3) 0%,var(--ink-2) 52%,var(--ink-2) 100%);
}
.hero .hero-copy{
  position:relative;z-index:3;width:100%;
  padding-block:clamp(48px,7vw,86px) clamp(40px,5.5vw,68px);
}
.hero .hero-scrim{
  position:absolute;inset:0;z-index:2;pointer-events:none;
  background:
    linear-gradient(to top,
      rgba(10,14,11,.95) 0%,
      rgba(10,14,11,.88) 20%,
      rgba(10,14,11,.66) 46%,
      rgba(10,14,11,.20) 72%,
      rgba(10,14,11,.44) 100%),
    repeating-linear-gradient(97deg,transparent 0 62px,rgba(157,171,147,.045) 62px 63px);
}
.hero h1,.hero .lede{text-shadow:0 2px 44px rgba(0,0,0,.6)}
/* The eyebrow is 11px mono and lands about halfway up the band, right where the lit
   stonework is brightest. Type that small needs a hard shadow — a 44px blur only
   tints the area around it. */
.hero .eyebrow{color:var(--bone);text-shadow:0 1px 3px rgba(0,0,0,.9),0 0 22px rgba(0,0,0,.85)}
/* The project credit moves to the top of the band, where the sky is, because the
   headline now occupies the foot of it. */
.hero .band-tag{
  top:clamp(24px,4vw,42px);bottom:auto;
  left:auto;right:var(--gutter);
  color:var(--bone);text-shadow:0 1px 20px rgba(0,0,0,.7);
  justify-content:flex-end;text-align:right;
}
@media(max-width:900px){.hero .band-tag{display:none}}

.hero-grid{display:grid;grid-template-columns:1.15fr .85fr;gap:clamp(28px,5vw,70px);align-items:end}
@media(max-width:940px){.hero-grid{grid-template-columns:1fr;gap:34px}}
h1 .it{font-style:italic;color:var(--clay)}
.hero-side{padding-bottom:10px;display:flex;flex-direction:column;gap:26px}
.hero-actions{display:flex;flex-wrap:wrap;gap:12px}
.btn{
  font-family:var(--mono);font-size:11.5px;letter-spacing:.14em;text-transform:uppercase;
  padding:14px 24px;text-decoration:none;display:inline-block;transition:transform .18s,background .2s,border-color .2s;
}
.btn-primary{background:var(--clay);color:#160C06;font-weight:500}
.btn-primary:hover{background:#D67E48;transform:translateY(-1px)}
.btn-ghost{border:1px solid var(--line);color:var(--bone)}
.btn-ghost:hover{border-color:var(--sage-dim);transform:translateY(-1px)}

.bandcanvas{position:absolute;inset:0;width:100%;height:100%;display:block}
.band-tag{
  position:absolute;left:var(--gutter);bottom:18px;z-index:3;
  color:var(--sage);display:flex;gap:16px;flex-wrap:wrap;
}
.band-tag b{color:var(--bone);font-weight:500}

/* ---------- stats strip ---------- */
.strip{
  display:grid;grid-template-columns:repeat(var(--strip-cols,4),1fr);
  border-bottom:1px solid var(--line);
}
@media(max-width:760px){.strip{grid-template-columns:repeat(2,1fr)}}
.stat{padding:30px 0 32px;border-right:1px solid var(--line)}
.stat:last-child{border-right:0}
/* Every column after the first clears its own rule. nth-child(2n) padded 2 and 4 but
   not 3, so the third figure sat tight against its divider while the others breathed. */
.stat + .stat{padding-left:22px}
@media(max-width:760px){
  .stat{padding-left:0}
  .stat:nth-child(odd){padding-left:0;border-right:1px solid var(--line)}
  .stat:nth-child(even){border-right:0;padding-left:22px}
  .stat:nth-child(-n+2){border-bottom:1px solid var(--line)}
}
.stat .n{
  font-family:var(--display);font-size:clamp(2.1rem,4vw,3.15rem);line-height:1;
  font-variation-settings:"opsz" 72;letter-spacing:-.02em;display:block;margin-bottom:9px;
  font-variant-numeric:lining-nums tabular-nums;
}
.stat .l{color:var(--sage-dim);font-family:var(--mono);font-size:10.5px;letter-spacing:.16em;text-transform:uppercase;line-height:1.5}

/* ---------- section scaffolding ---------- */
section{padding-block:clamp(64px,9vw,128px)}
.sec-head{display:grid;grid-template-columns:1fr .78fr;gap:clamp(24px,4vw,60px);align-items:end;margin-bottom:clamp(36px,5vw,62px)}
@media(max-width:940px){.sec-head{grid-template-columns:1fr;gap:20px}}

/* ---------- 3D cylinder ---------- */
.cyl-sec{border-top:1px solid var(--line);background:linear-gradient(180deg,var(--ink-2),var(--ink) 40%)}
.stage{
  position:relative;
  height:clamp(330px,52vw,560px);
  perspective:1450px;
  perspective-origin:50% 46%;
  margin-inline:calc(var(--gutter) * -1);
  touch-action:pan-y;
  cursor:grab;
  user-select:none;
}
.stage.dragging{cursor:grabbing}
.ring{
  position:absolute;inset:0;
  transform-style:preserve-3d;
  will-change:transform;
}
.plate{
  position:absolute;top:50%;left:50%;
  width:var(--pw);height:var(--ph);
  margin-left:calc(var(--pw) / -2);
  margin-top:calc(var(--ph) / -2);
  backface-visibility:hidden;
  overflow:hidden;
  border:1px solid rgba(157,171,147,.16);
  background:var(--ink-2);
  transition:border-color .3s;
}
.plate .fill{position:absolute;inset:0;width:100%;height:100%;display:block}
.plate .shade{
  position:absolute;inset:0;
  background:linear-gradient(to top,rgba(8,11,8,.88),rgba(8,11,8,.05) 52%);
  transition:opacity .35s;opacity:.55;
}
.plate.is-active{border-color:rgba(127,184,75,.62)}
.plate.is-active .shade{opacity:.28}
.plate .slot{
  position:absolute;left:14px;bottom:12px;right:14px;
  font-family:var(--mono);font-size:9.5px;letter-spacing:.15em;text-transform:uppercase;
  color:rgba(237,231,218,.72);line-height:1.5;
}
.plate .slot i{display:block;margin-top:3px;color:var(--leaf);font-style:normal;opacity:.9}
.plate .slot b{color:var(--bone);font-weight:500;display:block;letter-spacing:.12em}

.cyl-foot{
  display:flex;justify-content:space-between;align-items:flex-end;gap:24px;flex-wrap:wrap;
  border-top:1px solid var(--line);padding-top:20px;margin-top:26px;
}
.cyl-caption b{
  font-family:var(--display);font-size:1.5rem;font-variation-settings:"opsz" 36;
  letter-spacing:-.015em;display:block;margin-bottom:6px;font-weight:400;
}
.cyl-caption span{color:var(--sage-dim);font-family:var(--mono);font-size:11px;letter-spacing:.15em;text-transform:uppercase}
.dots{display:flex;gap:9px;align-items:center;padding-bottom:6px}
.dot{
  width:7px;height:7px;border-radius:50%;background:var(--line);border:0;padding:0;cursor:pointer;
  transition:background .3s,transform .3s;
}
.dot.on{background:var(--leaf);transform:scale(1.32)}
.dot:focus-visible{outline:2px solid var(--clay);outline-offset:3px}
.hint{color:var(--sage-dim);font-family:var(--mono);font-size:10.5px;letter-spacing:.15em;text-transform:uppercase;margin-top:14px}

/* ---------- services ---------- */
.svc{display:grid;grid-template-columns:repeat(3,1fr);border-top:1px solid var(--line)}
@media(max-width:940px){.svc{grid-template-columns:repeat(2,1fr)}}
@media(max-width:600px){.svc{grid-template-columns:1fr}}
.svc-item{
  padding:26px 26px 34px 0;border-bottom:1px solid var(--line);border-right:1px solid var(--line);
  display:flex;flex-direction:column;gap:11px;
}
.svc-item:nth-child(3n){border-right:0}
@media(max-width:940px){
  .svc-item{padding-right:22px}
  .svc-item:nth-child(3n){border-right:1px solid var(--line)}
  .svc-item:nth-child(2n){border-right:0}
}
@media(max-width:600px){
  .svc-item,.svc-item:nth-child(3n){border-right:0;padding-right:0}
}
.svc-item{text-decoration:none}
.svc-item .num{font-family:var(--mono);font-size:10px;letter-spacing:.18em;color:var(--leaf)}
.svc-item p{margin:0;color:var(--sage);font-size:15.5px;line-height:1.62}
/* Each trade leads with a picture now. 16:10 so ten of them stacked on a phone do not
   turn the section into a mile of scrolling. */
.svc-thumb{
  position:relative;display:block;overflow:hidden;
  aspect-ratio:16/10;border:1px solid var(--line);margin-bottom:7px;
}
.svc-thumb img,.svc-thumb canvas{
  position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;
  transition:transform 1s cubic-bezier(.16,.84,.34,1),filter .4s;
  filter:saturate(1.02) contrast(1.03);
}
.svc-item:hover .svc-thumb img{transform:scale(1.05);filter:saturate(1.1) contrast(1.06)}
.svc-item .svc-thumb + .num{margin-top:4px}

/* ---------- nursery ---------- */
.nursery{border-top:1px solid var(--line);background:var(--ink-2)}
.nur-grid{display:grid;grid-template-columns:.95fr 1.05fr;gap:clamp(28px,5vw,72px);align-items:center}
@media(max-width:940px){.nur-grid{grid-template-columns:1fr;gap:32px}}
.nur-plate{
  aspect-ratio:4/3;max-width:100%;position:relative;overflow:hidden;
  border:1px solid var(--line);
  background:
    radial-gradient(85% 70% at 30% 20%,rgba(76,97,70,.75),transparent 60%),
    radial-gradient(70% 90% at 78% 88%,rgba(200,112,60,.22),transparent 58%),
    linear-gradient(170deg,#1C271B,#0E140E);
}
.nur-plate .slot{
  position:absolute;left:16px;bottom:14px;
  font-family:var(--mono);font-size:9.5px;letter-spacing:.15em;text-transform:uppercase;color:rgba(237,231,218,.7);
}
/* Four frames of the yard, offset so the column reads as a stack of prints rather
   than a grid. The reveal comes from .shot, shared with every other photograph. */
.nur-shots{display:grid;grid-template-columns:repeat(3,1fr);gap:12px}
.nur-shot{margin:0;position:relative;overflow:hidden;border:1px solid var(--line);aspect-ratio:1/1}
/* The establishing frame runs the full width; the three that answer the specific
   doubts sit under it in a row. A 2-column grid left the fourth stranded alone. */
.nur-shot:nth-child(1){grid-column:1/-1;aspect-ratio:16/9}
.nur-shot img{width:100%;height:100%;object-fit:cover;display:block}
/* The label sits on whatever the photograph happens to be, so it carries its own
   scrim rather than relying on a text-shadow over a bright sky. */
.nur-shot figcaption{
  position:absolute;left:0;right:0;bottom:0;z-index:5;padding:26px 10px 8px;
  font-size:9.5px;letter-spacing:.15em;text-transform:uppercase;color:var(--bone);
  background:linear-gradient(to top,rgba(8,12,8,.82),transparent);
}
@media(max-width:560px){.nur-shots{grid-template-columns:1fr 1fr}.nur-shot:nth-child(1){aspect-ratio:4/3}}

/* ---------- nursery gallery (/nursery/) ---------- */
.nurgal-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:clamp(10px,1.4vw,18px)}
@media(max-width:900px){.nurgal-grid{grid-template-columns:repeat(2,1fr)}}
@media(max-width:560px){.nurgal-grid{grid-template-columns:1fr}}
.nurgal-item{margin:0;position:relative;overflow:hidden;border:1px solid var(--line);aspect-ratio:3/2}
.nurgal-item img{width:100%;height:100%;object-fit:cover;display:block}
.nurgal-item figcaption{
  position:absolute;left:0;right:0;bottom:0;z-index:5;padding:30px 12px 11px;
  font-family:var(--mono);font-size:10px;letter-spacing:.11em;color:var(--bone);
  background:linear-gradient(to top,rgba(8,12,8,.86),transparent);
  opacity:0;transform:translateY(6px);transition:opacity .45s ease,transform .45s ease;
}
/* The caption is the evidence, so it must not depend on a pointer: it shows on focus
   and on any device without hover, and only hides behind hover on a mouse. */
.nurgal-item:hover figcaption,.nurgal-item:focus-within figcaption{opacity:1;transform:none}
@media(hover:none){.nurgal-item figcaption{opacity:1;transform:none}}
@media(prefers-reduced-motion:reduce){.nurgal-item figcaption{transition:none}}
/* service-gallery.php reuses .gal-open so lightbox.js's #gal binder picks it up —
   but .gal-open's own sizing (height:auto) is built for the /projects/ page's
   CSS-column layout, where each tile keeps its own aspect ratio. Here the grid is
   fixed 3:2 with object-fit:cover, so restore that inside this context only; the
   gallery page's own .gal-open is untouched. */
.nurgal-item .gal-open{width:100%;height:100%;cursor:zoom-in}
.nurgal-item .gal-open img{height:100%;object-fit:cover}

.nur-list{list-style:none;margin:22px 0 0;padding:0;display:grid;gap:0}
.nur-list li{
  display:flex;justify-content:space-between;gap:18px;align-items:baseline;
  padding:12px 0;border-bottom:1px solid var(--line);
}
.nur-list li span:first-child{font-size:15.5px}
.nur-list li span:last-child{
  font-family:var(--mono);font-size:10.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--sage-dim);
  font-style:italic;text-align:right;
}

/* ---------- areas ---------- */
.areas{border-top:1px solid var(--line)}
.area-links{
  display:flex;flex-wrap:wrap;gap:0;border-top:1px solid var(--line);
}
.area-links a{
  flex:1 1 210px;
  padding:19px 16px 19px 0;
  border-bottom:1px solid var(--line);
  text-decoration:none;display:flex;align-items:baseline;gap:11px;
  transition:color .2s;color:var(--bone);
}
.area-links a:hover{color:var(--leaf)}
.area-links .co{font-family:var(--mono);font-size:9.5px;letter-spacing:.14em;color:var(--sage-dim);text-transform:uppercase}
.area-links .nm{font-size:16.5px}

/* ---------- cta ---------- */
.cta{border-top:1px solid var(--line);background:linear-gradient(180deg,var(--ink),var(--ink-3))}
.cta-inner{display:grid;grid-template-columns:1.1fr .9fr;gap:clamp(26px,5vw,64px);align-items:end}
@media(max-width:940px){.cta-inner{grid-template-columns:1fr;gap:30px}}
.contact-list{list-style:none;margin:0;padding:0;display:grid;gap:14px}
.contact-list li{display:flex;flex-direction:column;gap:3px}
.contact-list .k{font-family:var(--mono);font-size:10px;letter-spacing:.17em;text-transform:uppercase;color:var(--sage-dim)}
.contact-list .v{font-size:17px}
.contact-list a{text-decoration:none;border-bottom:1px solid var(--line);padding-bottom:2px}
.contact-list a:hover{border-color:var(--clay)}

footer.site{border-top:1px solid var(--line);background:var(--ink-3);padding-block:clamp(46px,6vw,76px) 0}
.fnav{
  display:grid;grid-template-columns:repeat(4,1fr);gap:clamp(24px,4vw,52px);
  padding-bottom:clamp(34px,4vw,54px);
}
@media(max-width:940px){.fnav{grid-template-columns:repeat(2,1fr);gap:32px}}
@media(max-width:560px){.fnav{grid-template-columns:1fr;gap:28px}}
/* Footer section labels are h2 so the document outline does not jump h1 -> h4.
   The global h2 rule is display-face, 3.9rem and opsz 96, so every part of it
   that this block does not restate has to be neutralised explicitly. */
.fcol h2{
  font-family:var(--mono);font-size:10px;line-height:1.2;font-variation-settings:normal;
  letter-spacing:.2em;text-transform:uppercase;
  color:var(--leaf);margin:0 0 15px;font-weight:500;
}
.fcol ul{list-style:none;margin:0;padding:0;display:grid;gap:9px}
.fcol a{
  text-decoration:none;color:var(--sage);font-size:14.5px;line-height:1.45;
  transition:color .2s,padding-left .2s;display:inline-block;
}
.fcol a:hover{color:var(--bone);padding-left:5px}

/* The footer panel that fills the space the uneven link columns leave. */
.fphoto{
  display:grid;grid-template-columns:.9fr 1.1fr;gap:clamp(20px,3vw,44px);align-items:center;
  border-top:1px solid var(--line);padding-block:clamp(30px,4vw,48px);
}
@media(max-width:820px){.fphoto{grid-template-columns:1fr;gap:24px}}
.fphoto-img{position:relative;overflow:hidden;border:1px solid var(--line);aspect-ratio:16/10}
.fphoto-img img{width:100%;height:100%;object-fit:cover;display:block}
.fphoto-copy h2{
  font-family:var(--display);font-size:clamp(1.4rem,2.4vw,2rem);line-height:1.12;
  letter-spacing:-.01em;margin:0 0 12px;color:var(--bone);text-transform:none;
}
.fphoto-copy p{margin:0 0 14px;color:var(--sage-dim);font-size:15px;line-height:1.6;max-width:56ch}
.fphoto-tel a{
  font-family:var(--mono);font-size:15px;letter-spacing:.06em;color:var(--bone);text-decoration:none;
}
.fphoto-tel a:hover{color:var(--leaf)}

.fareas{border-top:1px solid var(--line);padding-block:30px}
/* Footer section labels are h2 so the document outline does not jump h1 -> h4.
   The global h2 rule is display-face, 3.9rem and opsz 96, so every part of it
   that this block does not restate has to be neutralised explicitly. */
.fareas h2{
  font-family:var(--mono);font-size:10px;line-height:1.2;font-variation-settings:normal;
  letter-spacing:.2em;text-transform:uppercase;
  color:var(--leaf);margin:0 0 16px;font-weight:500;
}
.fareas-list{display:flex;flex-wrap:wrap;gap:7px 0}
/* A town whose page is still a draft renders as a span, not a link (ludi_area_link).
   Only the anchor carried the separator rule, so with every town page unpublished the
   whole list ran together as one word — "GrafordPossum Kingdom LakeGraham". Caught by
   screenshotting the footer, which is the only way it was ever going to be caught. */
.fareas-list a,
.fareas-list .fareas-soon{
  text-decoration:none;color:var(--sage);font-size:14px;
  padding-right:13px;margin-right:13px;border-right:1px solid var(--line);
  transition:color .2s;white-space:nowrap;
}
.fareas-list a:last-child,
.fareas-list .fareas-soon:last-child{border-right:0}
.fareas-list a:hover{color:var(--leaf)}
/* Dimmer than a link, because it is not one yet. */
.fareas-list .fareas-soon{color:var(--sage-dim);cursor:default}

.fbar{border-top:1px solid var(--line);padding-block:22px 30px}
.foot{display:flex;justify-content:space-between;gap:16px;flex-wrap:wrap;color:var(--sage-dim);font-family:var(--mono);font-size:10px;letter-spacing:.15em;text-transform:uppercase}

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

/* ---------- logo ---------- */

/* ---------- marquee ---------- */
.marquee{
  border-top:1px solid var(--line);border-bottom:1px solid var(--line);
  overflow:hidden;padding-block:15px;background:var(--ink-2);
}
.marquee-track{
  display:flex;gap:0;width:max-content;
  animation:slide 44s linear infinite;
}
.marquee-track span{
  font-family:var(--mono);font-size:11.5px;letter-spacing:.2em;text-transform:uppercase;
  color:var(--sage);padding-inline:26px;display:flex;align-items:center;gap:26px;white-space:nowrap;
}
.marquee-track span::after{content:"";width:5px;height:5px;background:var(--leaf);border-radius:50%;flex:none}
@keyframes slide{from{transform:translateX(0)}to{transform:translateX(-50%)}}
.marquee:hover .marquee-track{animation-play-state:paused}

/* ---------- scroll rise (visible at rest, transform only) ---------- */
.rise{transform:translateY(22px);transition:transform .85s cubic-bezier(.16,.84,.34,1)}
.rise.seen{transform:none}
.rise-d1{transition-delay:.07s}
.rise-d2{transition-delay:.14s}
.rise-d3{transition-delay:.21s}

/* ---------- hover life ---------- */
.svc-item{position:relative}
.svc-item::after{
  content:"";position:absolute;left:0;bottom:-1px;height:1px;width:0;background:var(--leaf);
  transition:width .5s cubic-bezier(.16,.84,.34,1);
}
.svc-item:hover::after{width:100%}
.svc-item:hover h3{color:var(--leaf)}
.svc-item h3{transition:color .3s}
.nur-plate{transition:transform 1.1s cubic-bezier(.16,.84,.34,1)}
.nur-plate.seen{transform:scale(1.015)}
.area-links a{position:relative}
.area-links a::after{
  content:"";position:absolute;left:0;bottom:-1px;height:1px;width:0;background:var(--leaf);
  transition:width .45s cubic-bezier(.16,.84,.34,1);
}
.area-links a:hover::after{width:100%}

@media (prefers-reduced-motion:reduce){
  .rise{transform:none!important}
  .marquee-track{animation:none}

  *{animation-duration:.01ms!important;transition-duration:.01ms!important}
}

/* ---------- document pages ----------
   Everything that is not the homepage: service pages, service-area pages, about,
   contact, 404. The homepage is built from full-bleed bands; these are a single
   measured column, so they need their own rules rather than inheriting band CSS. */

.doc{padding:calc(var(--nav-h,68px) + clamp(56px,9vw,128px)) 0 clamp(64px,9vw,120px)}
.doc > .wrap{max-width:min(var(--maxw),1120px)}

.doc-head{
  border-bottom:1px solid var(--line);
  padding-bottom:clamp(28px,4vw,48px);
  margin-bottom:clamp(32px,4.5vw,56px);
}
.doc-head h1{margin-top:18px}

/* Measured for reading, not for the grid — long-form copy is the ranking asset on
   service and city pages, so it gets a proper measure and vertical rhythm. */
.doc-body{max-width:68ch;font-size:clamp(1rem,1.15vw,1.09rem);line-height:1.7;color:var(--sage)}
.doc-body > * + *{margin-top:1.1em}
.doc-body h2{font-size:clamp(1.7rem,3.1vw,2.5rem);margin-top:2em;color:var(--bone)}
.doc-body h3{font-size:clamp(1.25rem,2vw,1.6rem);margin-top:1.6em;color:var(--bone)}
.doc-body strong{color:var(--bone);font-weight:600}
.doc-body a{color:var(--leaf);text-underline-offset:3px}
.doc-body a:hover{color:var(--bone)}
.doc-body ul,.doc-body ol{padding-left:1.25em}
.doc-body li + li{margin-top:.5em}
.doc-body li::marker{color:var(--leaf)}
.doc-body blockquote{
  border-left:2px solid var(--leaf);
  padding-left:20px;margin-left:0;
  font-family:var(--display);font-size:1.2em;font-style:italic;color:var(--bone);
}
.doc-body img{max-width:100%;height:auto;display:block}
.doc-body table{width:100%;border-collapse:collapse;font-size:.95em}
.doc-body th,.doc-body td{border-bottom:1px solid var(--line);padding:11px 12px;text-align:left}
.doc-body th{font-family:var(--mono);font-size:10.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--bone)}

/* ---------- accessibility ----------
   The skip link is the first focusable element on every page. Without these rules it
   renders as visible body text above the header on every single page. */

.screen-reader-text{
  position:absolute!important;
  width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;
  clip-path:inset(50%);white-space:nowrap;border:0;
}
.screen-reader-text:focus-visible,
.skip-link:focus-visible{
  position:fixed!important;
  top:14px;left:14px;z-index:9999;
  width:auto;height:auto;margin:0;padding:12px 18px;
  clip-path:none;white-space:normal;
  background:var(--leaf);color:var(--ink);
  font-family:var(--mono);font-size:11.5px;letter-spacing:.14em;text-transform:uppercase;
  outline:2px solid var(--bone);outline-offset:2px;
}

/* ---------- breadcrumbs ---------- */
.crumbs{
  font-family:var(--mono);font-size:10.5px;letter-spacing:.14em;text-transform:uppercase;
  color:var(--sage-dim);margin-bottom:26px;display:flex;flex-wrap:wrap;align-items:center;gap:8px;
}
.crumbs a{color:var(--sage)}
.crumbs a:hover{color:var(--leaf)}
.crumbs .sep{opacity:.45}
.crumbs [aria-current="page"]{color:var(--bone)}

/* ---------- photography ----------
   Photographs sit above the procedural canvas, which stays behind as the paint-in
   layer. If an image is slow or missing the band is still a composed scene rather
   than a blank rectangle. */
.bandphoto,.plate .photo{
  position:absolute;inset:0;width:100%;height:100%;
  object-fit:cover;display:block;z-index:1;
}
.plate .shade{z-index:2}
.band-tag,.plate .slot{z-index:3}
.plate .photo{filter:saturate(1.02) contrast(1.03)}
.plate.is-active .photo{filter:saturate(1.08) contrast(1.05)}
.doc-photo{margin:0 0 clamp(28px,4vw,44px);max-width:min(100%,1120px)}
.doc-photo img{width:100%;height:auto;display:block;border:1px solid var(--line)}

/* ---------- parallax media layer ----------
   One contract, three places. The layer is (1 + 2 × --px) tall and sits --px above
   its band, so a scroll-driven translate of ±--px × height can never expose an edge.
   motion.js reads --px off the root rather than carrying its own copy — if these two
   numbers ever disagree you get a white sliver at the top of the hero on fast scroll,
   which is exactly the bug this comment exists to prevent. */

.hero-media{
  position:absolute;left:0;right:0;
  top:calc(var(--px) * -100%);
  height:calc(100% + var(--px) * 200%);
  z-index:1;
  will-change:transform;
  backface-visibility:hidden;
}
.hero-media .bandcanvas,
.hero-media .bandphoto{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block}
.hero-media .bandcanvas{z-index:0}
.hero-media .bandphoto{z-index:1}

@media (prefers-reduced-motion:reduce){
  .hero-media{transform:none!important}
}

/* ---------- page hero ----------
   Every page that is not the homepage opens on this: a full-bleed band with the
   photograph (or the procedural scene, where no honest photograph exists) running
   behind the H1 rather than beside it. The scrim is not decoration — it is what
   makes 4.5:1 text contrast survive a bright twilight sky. */

.page-hero{
  position:relative;
  min-height:clamp(380px,50vw,620px);
  display:flex;align-items:flex-end;
  overflow:hidden;isolation:isolate;
  border-bottom:1px solid var(--line);
  background:
    radial-gradient(120% 90% at 22% 8%,rgba(200,112,60,.18),transparent 58%),
    linear-gradient(176deg,var(--ink-3) 0%,var(--ink-2) 52%,var(--ink-2) 100%);
}
.page-hero .hero-scrim{
  position:absolute;inset:0;z-index:2;pointer-events:none;
  background:
    linear-gradient(to top,
      rgba(10,14,11,.95) 0%,
      rgba(10,14,11,.80) 28%,
      rgba(10,14,11,.38) 64%,
      rgba(10,14,11,.58) 100%),
    repeating-linear-gradient(97deg,transparent 0 62px,rgba(157,171,147,.045) 62px 63px);
}
/* A painted band carries its own darkness in the lower third — that is where the
   ridgelines are. Washing it with the photograph scrim on top of that turns it to
   mud, so it gets a much lighter one and the sky comes back. */
.page-hero:not(.has-photo) .hero-scrim{
  background:
    linear-gradient(to top,
      rgba(10,14,11,.72) 0%,
      rgba(10,14,11,.38) 28%,
      rgba(10,14,11,0) 58%,
      rgba(10,14,11,.10) 100%),
    repeating-linear-gradient(97deg,transparent 0 62px,rgba(157,171,147,.045) 62px 63px);
}
.page-hero .hero-inner{
  position:relative;z-index:3;width:100%;
  max-width:min(var(--maxw),1120px);
  padding-block:clamp(54px,8vw,96px) clamp(32px,4.5vw,54px);
}
.page-hero .crumbs{margin-bottom:24px}
.page-hero .eyebrow{color:var(--bone);opacity:.82;margin-bottom:18px}
.page-hero h1{
  font-size:clamp(2.35rem,5.6vw,4.7rem);
  max-width:17ch;
  text-shadow:0 2px 44px rgba(0,0,0,.55);
}
.page-hero .lede{
  margin:24px 0 0;max-width:54ch;color:var(--bone);opacity:.88;
  text-shadow:0 1px 30px rgba(0,0,0,.6);
}

/* The band carries the top of the page now, so the column below it no longer needs
   to clear the header on its own. */
.doc.has-hero{padding-top:clamp(44px,6vw,80px)}
.doc.has-hero > .wrap > article > .doc-body{margin-top:0}

/* ---------- work strip ----------
   Three photographs at the foot of a page that would otherwise end on a paragraph,
   each one linked to the service page it actually belongs to. Real internal links,
   not a lightbox gallery. */

.workstrip{padding-block:clamp(48px,7vw,92px);border-top:1px solid var(--line)}
.workstrip .eyebrow{margin-bottom:28px}
.workgrid{display:grid;grid-template-columns:repeat(3,1fr);gap:clamp(12px,1.6vw,22px)}
@media(max-width:820px){.workgrid{grid-template-columns:1fr;gap:16px}}
.workcard{
  position:relative;display:block;overflow:hidden;text-decoration:none;
  border:1px solid var(--line);aspect-ratio:4/3;
}
.workcard img{
  width:100%;height:100%;object-fit:cover;
  transition:transform 1.1s cubic-bezier(.16,.84,.34,1),filter .4s;
  filter:saturate(1.02) contrast(1.03);
}
.workcard:hover img{transform:scale(1.045);filter:saturate(1.09) contrast(1.06)}
.workcard span{
  position:absolute;left:0;right:0;bottom:0;z-index:2;
  padding:34px 16px 14px;color:var(--bone);
  background:linear-gradient(to top,rgba(10,14,11,.92),transparent);
}
.workcard::after{
  content:"";position:absolute;left:0;bottom:0;height:2px;width:0;background:var(--leaf);z-index:3;
  transition:width .5s cubic-bezier(.16,.84,.34,1);
}
.workcard:hover::after{width:100%}

/* ---------- attention: pulse ----------
   Sky, 2026-09-21: "make all the call to action boxes, phone numbers pulse so they
   attract more attention", confirmed after I flagged that an always-on pulse can read
   as discount-retailer on a premium brand.

   So it is deliberately restrained: a slow halo that grows and fades rather than a
   flashing or bouncing button, it does not move the element (no layout shift, no
   jitter next to type), and it stops the moment you hover or focus — it has done its
   job by then and a pulsing button under the cursor reads as broken.

   --pulse is the one number to turn. Lower is louder. */
:root{ --pulse:2.8s }

@keyframes ludi-pulse{
  0%   { box-shadow:0 0 0 0 rgba(200,112,60,.5) }
  70%  { box-shadow:0 0 0 15px rgba(200,112,60,0) }
  100% { box-shadow:0 0 0 0 rgba(200,112,60,0) }
}
@keyframes ludi-breathe{
  0%,100% { color:var(--clay) }
  50%     { color:#E9A06B }
}
.pulse{animation:ludi-pulse var(--pulse) cubic-bezier(.4,0,.2,1) infinite}
.pulse:hover,.pulse:focus-visible{animation:none}
/* Inline phone numbers sit inside sentences — a halo there would be absurd, so they
   breathe in colour instead. */
.pulse-soft{
  color:var(--clay);text-decoration:underline;text-underline-offset:3px;
  animation:ludi-breathe var(--pulse) ease-in-out infinite;
}
.pulse-soft:hover,.pulse-soft:focus-visible{animation:none;color:var(--bone)}
/* The header button is outlined rather than filled, so it gets a border tint too. */
.callbtn.pulse{border-color:var(--clay)}

@media (prefers-reduced-motion:reduce){
  /* The global rule below sets animation-duration to .01ms, which on an INFINITE
     animation means it repeats about a hundred thousand times a second. Kill these
     outright instead. */
  .pulse,.pulse-soft,.callbar-tel{animation:none!important}
  .pulse-soft{color:var(--clay)}
}

/* ---------- see our work ---------- */
.workbox{padding-block:clamp(44px,6vw,80px);border-top:1px solid var(--line)}
.workbox-inner{
  display:grid;grid-template-columns:auto 1fr auto;align-items:center;
  gap:clamp(20px,3vw,44px);
  border:1px solid var(--line);background:var(--ink-2);
  padding:clamp(18px,2.4vw,28px);text-decoration:none;
  transition:border-color .3s,background .3s;
}
.workbox-inner:hover{border-color:var(--moss);background:var(--ink-3)}
/* align-items:center is load-bearing. Flex children default to stretch, which beat
   aspect-ratio and rendered the thumbnails as full-height vertical strips. The height
   is stated outright rather than derived, so there is nothing left to argue with. */
.workbox-thumbs{display:flex;gap:8px;align-items:center}
.workbox-thumbs img{
  width:clamp(72px,8vw,116px);height:clamp(72px,8vw,116px);
  object-fit:cover;display:block;flex:none;
  border:1px solid var(--line);
  transition:transform .8s cubic-bezier(.16,.84,.34,1);
}
.workbox-inner:hover .workbox-thumbs img{transform:scale(1.06)}
.workbox-thumbs img:nth-child(3){display:none}
@media(min-width:1100px){.workbox-thumbs img:nth-child(3){display:block}}
.workbox-copy{display:flex;flex-direction:column;gap:6px;min-width:0}
.workbox-copy .eyebrow{margin:0 0 4px}
.workbox-h{
  font-family:var(--display);font-size:clamp(1.5rem,2.6vw,2.2rem);line-height:1.05;
  letter-spacing:-.02em;color:var(--bone);
}
.workbox-sub{color:var(--sage);font-size:15px;line-height:1.55;max-width:56ch}
.workbox-btn{flex:none;align-self:center}
@media(max-width:860px){
  .workbox-inner{grid-template-columns:1fr;gap:18px}
  .workbox-btn{justify-self:start}
}

/* ---------- gallery ----------
   CSS columns, not a grid. The shoot mixes 3:2 and 16:9 and a fixed-aspect grid has
   to crop one or the other; cropping a drone frame to tidy up a grid throws away the
   part of the picture the client paid for. */
.gal-wrap{max-width:var(--maxw);margin-top:clamp(28px,4vw,52px)}
.gal{columns:3;column-gap:clamp(10px,1.4vw,18px)}
@media(max-width:1000px){.gal{columns:2}}
@media(max-width:620px){.gal{columns:1}}
.gal-item{
  break-inside:avoid;margin:0 0 clamp(10px,1.4vw,18px);
  border:1px solid var(--line);background:var(--ink-2);
}
/* The best frame leads, across the full width, above the columns. */
.gal-lead{column-span:all;margin-bottom:clamp(14px,2vw,26px)}
.gal-open{
  display:block;width:100%;padding:0;border:0;background:none;cursor:zoom-in;
  overflow:hidden;line-height:0;
}
.gal-open img{
  width:100%;height:auto;display:block;
  transition:transform 1.1s cubic-bezier(.16,.84,.34,1),filter .4s;
  filter:saturate(1.02) contrast(1.03);
}
.gal-open:hover img{transform:scale(1.035);filter:saturate(1.09) contrast(1.06)}
.gal-open:focus-visible{outline:2px solid var(--leaf);outline-offset:-2px}
/* Stacked, not side by side. A column is ~340px wide and the service label is set
   nowrap, so justify-content:space-between squeezed the caption down to one word per
   line. The lead frame is full width and can afford the row. */
.gal-item figcaption{
  display:flex;flex-direction:column;align-items:flex-start;gap:8px;
  padding:13px 15px 15px;border-top:1px solid var(--line);
}
.gal-cap{color:var(--sage);font-size:14px;line-height:1.5}
.gal-svc{color:var(--leaf);text-decoration:none}
.gal-svc:hover{color:var(--bone)}
.gal-lead figcaption{
  flex-direction:row;justify-content:space-between;align-items:baseline;gap:20px;
}
.gal-lead .gal-svc{flex:none;white-space:nowrap}
@media(max-width:620px){
  .gal-lead figcaption{flex-direction:column;align-items:flex-start;gap:8px}
  .gal-lead .gal-svc{white-space:normal}
}

/* ---------- lightbox ----------
   <dialog> does the focus trap, the Esc key, the inert page behind and the backdrop.

   The lightbox keeps a near-black backdrop after the palette inversion, because a
   photograph is judged against black — so it flips its own tokens the way the
   photographic bands do rather than inheriting the page's dark-on-light set. Found
   2026-09-22 alongside the services dropdown: the close and arrow glyphs were
   var(--bone) on rgba(12,16,13,.82), which after the inversion is black on black.
   The `color` line is load-bearing; a redefined token alone does not reach an element
   that never declared color. */
.lb{
  border:0;padding:0;background:transparent;max-width:min(96vw,1500px);max-height:94vh;
  width:100%;overflow:visible;
  color:var(--bone-lit);
  --bone:var(--bone-lit);
  --sage:#C7CEC0;
  --sage-dim:#AAB3A4;
  --leaf:var(--leaf-lit);
  --clay:var(--clay-lit);
  --line:rgba(237,231,218,.26);
}
.lb::backdrop{background:rgba(6,9,7,.93)}
.lb figure{margin:0;display:flex;flex-direction:column;gap:12px}
.lb img{
  width:100%;max-height:78vh;object-fit:contain;display:block;
  border:1px solid var(--line);background:#0E120E;
}
.lb figcaption{
  display:flex;justify-content:space-between;align-items:baseline;gap:20px;
  color:var(--sage);font-size:14px;line-height:1.5;
}
.lb-close,.lb-nav{
  position:absolute;z-index:2;border:1px solid var(--line);background:rgba(12,16,13,.82);
  color:var(--bone);cursor:pointer;line-height:1;
  transition:border-color .2s,background .2s;
}
.lb-close:hover,.lb-nav:hover{border-color:var(--clay);background:rgba(237,231,218,.16)}
.lb-close{top:-14px;right:-6px;width:38px;height:38px;font-size:24px}
.lb-nav{top:calc(39vh - 26px);width:44px;height:52px;font-size:28px}
.lb-prev{left:-6px}
.lb-next{right:-6px}
@media(max-width:700px){
  .lb-close{top:-42px;right:0}
  .lb-nav{top:auto;bottom:-46px;height:40px}
  .lb-prev{left:0}
  .lb-next{left:52px;right:auto}
}

/* ---------- enquiry form ---------- */
.lform-sec{
  padding-block:clamp(56px,8vw,110px);
  border-top:1px solid var(--line);background:var(--ink-2);
}
.lform-grid-outer{
  display:grid;grid-template-columns:.85fr 1.15fr;gap:clamp(30px,5vw,76px);align-items:start;
}
@media(max-width:940px){.lform-grid-outer{grid-template-columns:1fr;gap:38px}}
.lform-facts{list-style:none;padding:0;margin:clamp(26px,3.5vw,40px) 0 0;border-top:1px solid var(--line)}
.lform-facts li{display:grid;grid-template-columns:.62fr 1fr;gap:18px;padding:15px 0;border-bottom:1px solid var(--line)}
.lform-facts .k{font-family:var(--mono);font-size:10.5px;letter-spacing:.16em;text-transform:uppercase;color:var(--sage-dim)}
.lform-facts .v{color:var(--bone);font-size:15px;line-height:1.55}

.lform-box{border:1px solid var(--line);background:var(--ink);padding:clamp(20px,3vw,36px)}
.lform-grid{display:grid;grid-template-columns:1fr 1fr;gap:clamp(12px,1.6vw,20px)}
@media(max-width:560px){.lform-grid{grid-template-columns:1fr}}
.lform-row{margin:0;display:flex;flex-direction:column;gap:7px;grid-column:span 2}
.lform-row.is-half{grid-column:span 1}
@media(max-width:560px){.lform-row,.lform-row.is-half{grid-column:span 1}}
.lform-row label{
  font-family:var(--mono);font-size:10.5px;letter-spacing:.14em;text-transform:uppercase;
  color:var(--sage);display:flex;align-items:baseline;gap:9px;
}
.lform-row .opt{color:var(--sage-dim);letter-spacing:.08em;text-transform:none;font-size:10px}
.lform-row input,.lform-row textarea{
  font-family:var(--body);font-size:16px;color:var(--bone);
  background:var(--ink-2);border:1px solid var(--line);
  padding:12px 13px;width:100%;
  transition:border-color .2s,background .2s;
}
.lform-row textarea{resize:vertical;min-height:104px;line-height:1.55}
.lform-row input:focus,.lform-row textarea:focus{
  outline:none;border-color:var(--leaf);background:var(--ink-3);
}
/* :user-invalid, not :invalid. These inputs carry no placeholder attribute, so
   :placeholder-shown never matches, so :not(:placeholder-shown) was always true and
   every required field sat outlined in clay before the visitor had typed a character
   — the form shouting at somebody who has done nothing wrong yet. :user-invalid only
   matches after they have interacted or tried to submit. */
.lform-row input:user-invalid,
.lform-row textarea:user-invalid{border-color:var(--clay)}
/* Honeypot. Off-screen rather than display:none — some bots skip hidden fields. */
.lform-hp{position:absolute!important;left:-9999px;width:1px;height:1px;overflow:hidden}
.lform-foot{display:flex;flex-wrap:wrap;align-items:center;gap:18px;margin-top:clamp(18px,2.4vw,26px)}
.lform-or{color:var(--sage-dim);font-size:14px}
.lform-tel{font-weight:500}
.lform-note{
  margin:0 0 18px;padding:13px 15px;font-size:14.5px;line-height:1.55;border:1px solid var(--line);
}
.lform-ok{border-color:var(--leaf-deep);color:var(--bone);background:rgba(47,107,49,.16)}
.lform-bad{border-color:var(--clay-dim);color:var(--bone);background:rgba(200,112,60,.14)}

/* ---------- sticky call bar, phones only ----------
   On a phone the header scrolls away and the footer is a long way down, so a long
   service page can run a screen and a half with no way to ring the yard. */
.callbar{display:none}
@media(max-width:860px){
  .callbar{
    display:block;position:fixed;left:0;right:0;bottom:0;z-index:70;
    background:color-mix(in srgb,var(--ink) 92%,transparent);
    backdrop-filter:blur(14px);
    border-top:1px solid var(--line);
    padding-bottom:env(safe-area-inset-bottom,0px);
  }
  .callbar-tel{
    display:flex;align-items:center;justify-content:center;gap:10px;
    padding:14px 18px;text-decoration:none;color:var(--bone);
    background:var(--clay);
    font-family:var(--mono);font-size:11.5px;letter-spacing:.13em;text-transform:uppercase;
    animation:ludi-pulse var(--pulse) cubic-bezier(.4,0,.2,1) infinite;
  }
  .callbar-tel strong{font-size:14px;letter-spacing:.04em;font-weight:600}
  .callbar-tel svg{flex:none}
  /* So the bar never covers the last line of the footer. */
  body{padding-bottom:62px}
}

/* ---------- carousel: enlarging a plate ----------
   Only the plates carrying a real photograph open, so only those advertise it.
   A painted stand-in shown full size would invite somebody to read a placeholder as
   a project. */
.plate.has-photo{cursor:zoom-in}
.plate.has-photo:hover .photo{filter:saturate(1.1) contrast(1.06)}
.cyl-enlarge{
  display:inline-block;vertical-align:middle;margin-left:16px;border:1px solid var(--line);background:transparent;color:var(--bone);
  padding:7px 13px;cursor:pointer;font-size:10.5px;
  transition:border-color .2s,background .2s,color .2s;
}
.cyl-enlarge:hover{border-color:var(--clay);background:color-mix(in srgb,var(--clay) 12%,transparent)}
.cyl-enlarge[hidden]{display:none}

/* ---------- rounded corners ----------
   Sky, 2026-09-21: "all call to action boxes and the carousel images need to have
   rounded corners."

   Two tokens rather than a number sprinkled through the file, so the whole site's
   softness is one edit. --r is chrome you click; --r-lg is a card or a photograph.

   Deliberately NOT rounded: anything full-bleed — the page and homepage heroes, the
   marquee, the header, the footer, the stats strip. A band that runs edge to edge has
   no corners to round; giving it some just puts a sliver of page background in each
   corner and reads as a mistake.

   Applied here at the end of the file rather than edited into each rule, so the
   decision is legible in one place and reversible in one deletion. */
:root{
  --r:9px;
  --r-lg:20px;
  --r-full:999px;
  --r-xl:26px;   /* full-bleed photograph blocks */
}

/* calls to action */
.btn,
.callbtn,
.cyl-enlarge,
.callbar-tel,
.lb-close,
.lb-nav{border-radius:var(--r)}

.workbox-inner{border-radius:var(--r-lg)}
.workbox-thumbs img{border-radius:var(--r)}

/* the form is a call to action too */
.lform-box{border-radius:var(--r-lg)}
.lform-row input,
.lform-row textarea,
.lform-note{border-radius:var(--r)}

/* ---- carousel plates ----
   The radius goes on the plate AND on every layer inside it rather than clipping with
   overflow:hidden. The plate lives in a 3D transform, and overflow clipping on a
   transformed element is where Safari drops corners and repaints badly. */
.plate,
.plate .fill,
.plate .photo,
.plate .shade{border-radius:var(--r-lg)}

/* ---- every other photograph, for consistency ----
   Rounding the carousel and leaving the gallery, the service thumbnails and the work
   strip square would read as a bug rather than a decision. */
.gal-item{overflow:hidden;border-radius:var(--r-lg)}
.gal-lead{border-radius:var(--r-lg)}
.workcard{border-radius:var(--r-lg)}
.svc-thumb{border-radius:var(--r)}
.doc-photo img{border-radius:var(--r-lg)}
.nur-plate{border-radius:var(--r-lg)}
.lb img{border-radius:var(--r-lg)}

/* The call bar's button ran edge to edge, so a radius on it left slivers of the bar
   showing in the bottom screen corners — the exact "reads as a mistake" case the note
   above warns about. Inset it and the rounding is obviously a button instead. */
@media(max-width:860px){
  .callbar{padding:8px 12px calc(8px + env(safe-area-inset-bottom,0px))}
  body{padding-bottom:76px}
}

/* ---------------------------------------------------------- services dropdown
   Sky, 2026-09-22: "put a dropdown on Services in nav and add all the services."

   Twelve trades behind one nav item meant a visitor had to already suspect we do
   drainage before they could find the drainage page. The parent stays a real link to
   /services/ — a dropdown parent that only toggles is a dead end on a phone, where
   there is no hover to open it. :focus-within opens it from a keyboard.

   Sky, 2026-09-22: "make the service dropdown serivces brighter. Its hard to read."
   He was reading a panel of black text on a near-black panel. When the palette was
   inverted this morning the page ground went light and --bone flipped from cream to
   #171E17, but this panel kept its old dark background and its `color:var(--bone)` —
   so the token that used to be the light text became the dark text and the contrast
   collapsed to about 1.2:1. Same failure as the hero H1: a custom property changes
   what a rule means without the rule changing.

   Fixed by moving the panel into the palette rather than patching the colour. It is a
   raised surface over a mid-tone ground, so it goes LIGHTER than the header, not
   darker — --ink-3 with --bone text, measured at 13.8:1, and the "all services" row
   at --sage for 6.0:1. Opacity is gone from both: dimming text is how you lose
   contrast you cannot then measure. */
.main .has-sub{position:relative;display:inline-flex;align-items:center}
.main .has-sub>a{display:inline-flex;align-items:center;gap:5px}
.main .caret{transition:transform .18s ease;opacity:.75}
.main .has-sub:hover .caret,
.main .has-sub:focus-within .caret{transform:rotate(180deg)}

.subnav{
  position:absolute;top:100%;left:50%;transform:translateX(-50%) translateY(4px);
  display:grid;grid-template-columns:repeat(2,minmax(232px,1fr));gap:1px 20px;
  padding:14px 18px;margin-top:10px;
  background:var(--ink-3);color:var(--bone);backdrop-filter:blur(14px);
  border:1px solid var(--line);border-radius:var(--r-lg);
  box-shadow:0 18px 44px rgba(46,38,22,.22);
  opacity:0;visibility:hidden;pointer-events:none;transition:opacity .16s ease,transform .16s ease;
  z-index:60;
}
/* The 10px gap between trigger and panel would close the menu as the pointer crosses
   it, so the panel's own hover area reaches up to meet the trigger. */
.subnav::before{content:"";position:absolute;left:0;right:0;top:-12px;height:12px}
.main .has-sub:hover .subnav,
.main .has-sub:focus-within .subnav{opacity:1;visibility:visible;pointer-events:auto;transform:translateX(-50%) translateY(0)}

.subnav a{
  display:block;padding:7px 10px;border-radius:var(--r);
  font-size:.92rem;line-height:1.35;color:var(--bone);
}
.subnav a:hover,.subnav a:focus-visible{background:rgba(23,30,23,.075);color:var(--leaf-deep)}
.subnav .subnav-all{
  grid-column:1/-1;margin-top:6px;padding-top:11px;border-top:1px solid var(--line);
  font-family:var(--mono,ui-monospace,Menlo,monospace);font-size:.74rem;letter-spacing:.08em;
  text-transform:uppercase;color:var(--sage);
}

@media (max-width:900px){
  /* No hover on a phone and no room for a 2-up panel. The parent link still works, so
     Services goes to the index page and the twelve trades are listed there. */
  .subnav{display:none}
  .main .caret{display:none}
}
@media (prefers-reduced-motion:reduce){
  .subnav,.main .caret{transition:none}
}

/* ------------------------------------------------------------- related trades
   Contextual internal links at the foot of a service page. The reason under each
   link is the point — it gives the anchor context and tells a visitor why the two
   trades belong together, which a bare list of service names does not. */
.related{padding:clamp(48px,7vw,86px) 0;border-top:1px solid var(--line)}
.related .eyebrow{margin-bottom:10px}
.related h2{max-width:20ch;margin:0 0 clamp(26px,3.4vw,40px)}
.relgrid{display:grid;grid-template-columns:repeat(3,1fr);gap:clamp(14px,1.8vw,22px)}
.relcard{
  /* Flex column so "See the work" sits on the card's floor rather than wherever the
     paragraph happens to end — one-line and two-line reasons sit side by side. */
  display:flex;flex-direction:column;height:100%;padding:clamp(20px,2.4vw,28px);
  background:rgba(255,255,255,.028);border:1px solid var(--line);border-radius:var(--r-lg);
  transition:background .2s ease,border-color .2s ease,transform .2s ease;
}
.relcard:hover,.relcard:focus-visible{
  background:rgba(255,255,255,.055);border-color:rgba(255,255,255,.17);transform:translateY(-2px);
}
/* The whole card is one <a>, so the theme's link underline lands on the body copy
   and the label too. Caught by screenshotting the block rather than reading it. */
.relcard,.relcard h3,.relcard p,.relcard .mono{text-decoration:none}
.relcard h3{margin:0 0 9px;font-size:clamp(1.02rem,1.35vw,1.2rem);line-height:1.22}
.relcard:hover h3,.relcard:focus-visible h3{text-decoration:underline;text-underline-offset:3px}
.relcard p{margin:0 0 16px;font-size:.93rem;line-height:1.5;opacity:.74}
.relcard .mono{margin-top:auto}
.relcard .mono{font-size:.7rem;letter-spacing:.09em;text-transform:uppercase;opacity:.55}
.relcard:hover .mono{opacity:.85}
@media (max-width:860px){.relgrid{grid-template-columns:1fr}}
@media (prefers-reduced-motion:reduce){.relcard{transition:none}.relcard:hover{transform:none}}

/* ---------------------------------------------------------------- intro loader
   Sky, 2026-09-22: "I also like what you did for Baltz site in the beginning of the
   website how you have his name written before the site opens."

   Ported from didm-baltz-web. Three rails, in order of how much they are trusted:

   1. `html:not(.js-on) .intro{display:none}` — WITHOUT JAVASCRIPT IT NEVER SHOWS.
      The markup is in the HTML; only the inline head script in inc/intro.php reveals
      it. A blocked script or a parse error means no loader, never a black curtain.
   2. The `ludi-intro-exit` keyframe dismisses it at 2.6s with no JS at all.
   3. assets/js/intro.js cuts the hold to 1.2s and exits early on any interaction.

   `html.intro-seen` is set by the same head script from sessionStorage, so the second
   page view of a session never flashes it, and prefers-reduced-motion never sees it.

   Every colour is a token, so this follows the palette rather than pinning the site to
   a dark one — if the site goes light, the curtain goes light with it. */

html:not(.js-on) .intro,
html.intro-seen .intro{display:none}

.intro{
  position:fixed;inset:0;z-index:10000;
  background:var(--ink);
  display:grid;place-items:center;
  clip-path:inset(0 0 0 0);
  transition:clip-path .9s cubic-bezier(.16,.84,.34,1) .05s;
  animation:ludi-intro-exit .9s cubic-bezier(.16,.84,.34,1) 2.6s forwards;
}
.intro.done{clip-path:inset(0 0 100% 0);pointer-events:none}
@keyframes ludi-intro-exit{to{clip-path:inset(0 0 100% 0);pointer-events:none;visibility:hidden}}

.intro-in{text-align:center;padding-inline:24px}

.intro-word{
  margin:0;
  font-family:var(--display);font-weight:400;
  font-size:clamp(2.1rem,6vw,4.2rem);line-height:1.1;letter-spacing:-.025em;
  color:var(--bone);
  overflow:hidden;
}
/* Each half rises from its own clipped line. The second is clay italic, which is the
   same accent the homepage H1 uses on "Texas land." — the entrance and the headline
   should be the same gesture. */
.intro-word span{
  display:inline-block;
  transform:translateY(110%);
  animation:ludi-intro-up .8s cubic-bezier(.16,.84,.34,1) forwards;
}
.intro-word span:nth-child(2){
  animation-delay:.09s;
  color:var(--clay);font-style:italic;
}

.intro-bar{
  width:180px;height:2px;margin:1.5rem auto 0;
  background:var(--line);overflow:hidden;
}
.intro-bar i{
  display:block;height:100%;width:100%;
  background:var(--leaf);
  transform:scaleX(0);transform-origin:left;
  animation:ludi-intro-bar 1.1s cubic-bezier(.16,.84,.34,1) .15s forwards;
}

.intro-sub{
  margin:1rem 0 0;
  /* .26em, matching the live Baltz loader measured at 3.33px on 13px type. */
  font-family:var(--mono);font-size:10.5px;letter-spacing:.26em;text-transform:uppercase;
  color:var(--sage-dim);
}

@keyframes ludi-intro-up{to{transform:none}}
@keyframes ludi-intro-bar{to{transform:scaleX(1)}}

/* The head script already withholds .js-on under reduced motion, so this never
   normally applies. It is here so the rule survives someone adding .js-on by another
   route later — the global reduced-motion block further up sets animation-duration to
   .01ms, and on a `forwards` animation that is fine, but an unhidden curtain is not. */
@media (prefers-reduced-motion:reduce){
  .intro{display:none}
}

/* ------------------------------------------------------------------ crew band
   Sky, 2026-09-22, with a reference screenshot: "Maybe you could put the team photo
   like this and when you scroll the words move off the image".

   The photograph is a plain background layer and does not move. The copy carries
   data-parallax="-.34" — negative, so motion.js translates it AGAINST the scroll and
   it rides up and off the picture as the band passes. Unlike .hero-media, text is not
   inside an oversized clipped parent, so it genuinely leaves the frame; the band is
   padded tall enough that nothing is cut mid-line at rest, and `overflow:hidden`
   keeps the copy from colliding with the sections either side of it.

   Under prefers-reduced-motion motion.js clears the transform and this is simply a
   photograph with copy on it, which is the whole point of doing it with transform. */

.crew{
  position:relative;isolation:isolate;overflow:hidden;
  display:grid;align-items:center;
  min-height:clamp(440px,58vw,660px);
  padding-block:clamp(90px,12vw,150px);
  border-top:1px solid var(--line);border-bottom:1px solid var(--line);
  background:var(--ink-2);
}
.crew-photo{
  position:absolute;inset:0;z-index:0;
  width:100%;height:100%;object-fit:cover;display:block;
  filter:saturate(1.04) contrast(1.03);
}
/* Weighted to the left, where the copy sits, so the right of the photograph stays
   readable. A flat wash over the whole frame kills the picture to protect text that
   only occupies half of it. */
.crew-scrim{
  position:absolute;inset:0;z-index:1;pointer-events:none;
  background:
    linear-gradient(100deg,
      rgba(8,11,8,.90) 0%,
      rgba(8,11,8,.76) 34%,
      rgba(8,11,8,.34) 62%,
      rgba(8,11,8,.30) 100%),
    linear-gradient(to top,rgba(8,11,8,.55),transparent 42%);
}
/* Lighter when the band is actually showing the crew.
   The gradient above was tuned against a bright twilight landscape, where .90 at the
   left edge was needed to hold the headline. The crew photograph is already shaded by
   the tree on that side, so the same scrim buried two or three people at the left edge
   — and burying crew members under a panel that says "one company, one crew" is the
   wrong trade. Held dark only as far as the text actually runs (~28% of the band),
   then released faster. The fallback keeps its original tuning. */
.crew.has-crew .crew-scrim{
  background:
    linear-gradient(100deg,
      rgba(8,11,8,.80) 0%,
      rgba(8,11,8,.62) 28%,
      rgba(8,11,8,.26) 52%,
      rgba(8,11,8,.20) 100%),
    linear-gradient(to top,rgba(8,11,8,.42),transparent 38%);
}
/* width:100% is load-bearing. .wrap carries margin-inline:auto, and inside a grid an
   AUTO MARGIN BEATS STRETCH — so the wrap shrink-wrapped to its content (515px of a
   1440px band) and sat centred, putting the headline over the brightest part of the
   photograph instead of in the scrim built for it. Measured, not guessed: the box was
   left:463 width:515 before this line. */
.crew-wrap{position:relative;z-index:2;width:100%}

.crew-copy{
  max-width:38ch;
  will-change:transform;backface-visibility:hidden;
}
.crew-h{
  font-size:clamp(1.85rem,3.9vw,3.15rem);
  line-height:1.06;color:var(--bone);margin:0;
  text-shadow:0 2px 40px rgba(0,0,0,.6);
}
.crew-yes,.crew-call{
  font-family:var(--display);
  font-size:clamp(1.3rem,2.4vw,2rem);line-height:1.2;
  color:var(--bone);margin:.5em 0 0;
  text-shadow:0 2px 40px rgba(0,0,0,.6);
}
.crew-yes{font-style:italic;color:var(--clay)}

.crew-tel{
  /* flex + fit-content, not inline-flex: the button is its own line below, and an
     inline phone link let the two calls to action share a row at wide viewports. */
  display:flex;width:fit-content;align-items:center;gap:10px;
  margin-top:1.15rem;
  font-family:var(--display);font-size:clamp(1.3rem,2.3vw,1.85rem);
  letter-spacing:-.01em;text-decoration:none;
}
.crew-tel svg{flex:none}

.crew-btn{margin-top:1.6rem;display:inline-flex}

@media(max-width:700px){
  .crew{min-height:0;padding-block:clamp(64px,16vw,96px)}
  .crew-copy{max-width:none}
  /* The travel is halved on a narrow viewport by motion.js rather than here — the
     band is shorter, so the same depth would throw the copy clean out of it before
     the band reached the middle of the screen. See depthOf() in motion.js. */
  .crew-scrim{
    background:
      linear-gradient(to top,rgba(8,11,8,.92) 0%,rgba(8,11,8,.72) 45%,rgba(8,11,8,.48) 100%);
  }
}


/* ======================================================= Harmone detail pass
   Sky, 2026-09-22, having sent the Harmone Framer template: "I want our current dark
   palette, just the layout and how the images move... all the detail," then "you don't
   need to copy it exactly — if things need to move or be added to fit our stuff, make
   those changes."

   So: Harmone's structure and craft detail, our palette, our photographs, our facts.
   What is NOT carried over is its social proof — avatars, "trusted by 500+ clients
   (4.9/5)" and a named testimonial — because ours would all have to be invented. Those
   slots come back the day the client sends real reviews.
   ========================================================================== */

/* ---------- pill eyebrow ----------
   The single highest-leverage detail in the template. Ours was a hairline and caps;
   Harmone's is a bordered pill with a leaf glyph, which reads as a label rather than a
   rule. Opt-in via .eyebrow-pill so existing sections are untouched until moved over. */
.eyebrow-pill{
  display:inline-flex;align-items:center;gap:9px;
  padding:8px 15px 8px 12px;
  border:1px solid var(--line);border-radius:var(--r-full,999px);
  background:rgba(255,255,255,.028);
  color:var(--sage);font-size:10px;letter-spacing:.16em;
  margin:0 0 20px;
}
/* The base .eyebrow draws a 30px rule with ::before. A pill must not have one. */
.eyebrow-pill::before{content:none}
.eyebrow-pill svg{color:var(--leaf);flex:none}

/* ---------- proof: years, chips, reasons ---------- */
.proof{padding-block:clamp(52px,7vw,96px);border-top:1px solid var(--line)}

.proof-stat{
  display:flex;align-items:center;gap:clamp(16px,2.4vw,30px);flex-wrap:wrap;
  padding:clamp(20px,2.6vw,30px) clamp(22px,2.8vw,34px);
  background:var(--ink-2);border:1px solid var(--line);border-radius:var(--r-lg);
}
.proof-n{
  font-family:var(--display);font-size:clamp(2.4rem,5vw,3.9rem);line-height:1;
  letter-spacing:-.03em;color:var(--bone);font-variation-settings:"opsz" 96;
}
.proof-l{color:var(--sage);font-size:clamp(.95rem,1.3vw,1.08rem);line-height:1.5;max-width:44ch}

/* Full-bleed chip marquee. Same mechanic as .marquee — a track duplicated once and
   translated -50%, so the loop has no seam — but chips rather than bare words. */
.chips{
  margin-block:clamp(26px,3.4vw,42px);
  border-top:1px solid var(--line);border-bottom:1px solid var(--line);
  overflow:hidden;padding-block:16px;background:var(--ink-2);
}
.chips-track{display:flex;width:max-content;animation:slide 52s linear infinite}
.chip{
  display:inline-flex;align-items:center;gap:9px;flex:none;
  padding-inline:clamp(18px,2.4vw,34px);
  font-family:var(--body);font-size:14.5px;color:var(--sage);white-space:nowrap;
}
.chip svg{color:var(--leaf);flex:none}
.chips:hover .chips-track{animation-play-state:paused}

.reasons{
  display:grid;grid-template-columns:repeat(2,1fr);
  gap:clamp(12px,1.6vw,20px);
  margin-top:clamp(26px,3.4vw,42px);
}
@media(max-width:820px){.reasons{grid-template-columns:1fr}}
.reason{
  padding:clamp(22px,2.6vw,32px);
  background:rgba(255,255,255,.028);
  border:1px solid var(--line);border-radius:var(--r-lg);
  transition:background .25s ease,border-color .25s ease;
}
.reason:hover{background:rgba(255,255,255,.05);border-color:rgba(255,255,255,.15)}
/* Three dots in an L, lifted from Harmone's card glyph. Cheaper than an icon set and
   it never looks like the wrong icon for the sentence under it. */
.reason-dots{display:flex;flex-wrap:wrap;width:26px;gap:5px;margin-bottom:18px}
.reason-dots i{width:8px;height:8px;border:1.4px solid var(--leaf);border-radius:50%}
.reason-dots i:first-child{background:var(--leaf)}
.reason h3{margin:0 0 9px;font-size:clamp(1.08rem,1.5vw,1.3rem);line-height:1.22;color:var(--bone)}
.reason p{margin:0;color:var(--sage);font-size:.95rem;line-height:1.6}

/* ---------- species cards: Tree Installation stock list + Nursery "what we hold" ----------
   Same grid, same ten cards, called from ludi_species_cards() so the two pages cannot
   drift apart. Each card ships with .rise already in its class list (see the helper in
   inc/content.php), so the sitewide scroll-reveal IntersectionObserver in motion.js
   picks it up with no markup or JS beyond that — the same mechanism every .rise block
   on the site already uses. Hover is CSS-only: a lift plus the brighten treatment
   .relcard and .reason already use, so a card behaves the way every other card on the
   site behaves. */
.species-grid{
  display:grid;grid-template-columns:repeat(3,1fr);
  gap:clamp(12px,1.6vw,20px);
  margin-top:clamp(22px,3vw,34px);
}
@media(max-width:860px){.species-grid{grid-template-columns:1fr 1fr}}
@media(max-width:560px){.species-grid{grid-template-columns:1fr}}
.species-card{
  padding:clamp(18px,2.2vw,26px);
  background:rgba(255,255,255,.028);
  border:1px solid var(--line);border-radius:var(--r-lg);
  transition:background .25s ease,border-color .25s ease,transform .25s ease;
}
.species-card:hover{
  background:rgba(255,255,255,.055);border-color:rgba(255,255,255,.17);transform:translateY(-4px);
}
.species-card h3{margin:0 0 7px;font-size:1.02rem;line-height:1.25;color:var(--bone);transition:color .25s ease}
.species-card:hover h3{color:var(--leaf)}
.species-card p{margin:0;color:var(--sage);font-size:.88rem;line-height:1.55}
@media (prefers-reduced-motion:reduce){.species-card{transition:none}.species-card:hover{transform:none}}

/* ---------- showcase: one project, big ---------- */
.show{padding-block:clamp(56px,8vw,104px);border-top:1px solid var(--line);background:var(--ink-2)}
.show-head{
  display:flex;align-items:flex-end;justify-content:space-between;gap:24px;
  margin-bottom:clamp(26px,3.4vw,42px);flex-wrap:wrap;
}
.show-head h2{max-width:14ch}
.show-nav{display:flex;gap:10px;flex:none}
.show-btn{
  width:46px;height:46px;flex:none;display:grid;place-items:center;
  border:1px solid var(--line);border-radius:50%;background:transparent;color:var(--bone);
  cursor:pointer;transition:border-color .2s,background .2s,transform .2s;
}
.show-btn:hover{border-color:var(--leaf);background:rgba(127,184,75,.12)}
.show-btn:active{transform:scale(.94)}

.show-stage{position:relative;outline:none}
.show-stage:focus-visible{outline:2px solid var(--leaf);outline-offset:6px;border-radius:var(--r-xl)}
.show-card{
  display:grid;grid-template-columns:1.45fr 1fr;
  gap:clamp(18px,2.6vw,40px);align-items:center;
  padding:clamp(14px,1.6vw,20px);
  background:var(--ink-3);border:1px solid var(--line);border-radius:var(--r-xl);
}
@media(max-width:880px){.show-card{grid-template-columns:1fr}}
/* The cross-fade. The card is placed, THEN .is-on lands a frame later, so the
   transition runs instead of being skipped on a freshly unhidden element. */
.show-card{opacity:0;transform:translateY(10px);transition:opacity .5s ease,transform .5s cubic-bezier(.16,.84,.34,1)}
.show-card.is-on{opacity:1;transform:none}
.show-card[hidden]{display:none}

/* 16:11, not 4:3. Every frame in this library is landscape — a drone pass or a wide
   ground shot — and a 4:3 box on a 1.25fr column cropped the sides off and made the
   card taller than the copy beside it. Measured by looking at it. */
.show-photo{
  overflow:hidden;border-radius:var(--r-lg);aspect-ratio:16/9;background:var(--ink-2);
}
.show-photo img{
  width:100%;height:100%;object-fit:cover;display:block;
  transition:transform 1.4s cubic-bezier(.16,.84,.34,1);
  filter:saturate(1.03) contrast(1.02);
}
/* A slow push-in while the card is the live one. This is the "how the images move"
   part: not a scroll parallax, a long ease that keeps a still photograph alive. */
.show-card.is-on .show-photo img{transform:scale(1.045)}

.show-copy{padding-inline:clamp(4px,1.4vw,18px)}
.show-copy h3{margin:0 0 12px;font-size:clamp(1.4rem,2.4vw,2.1rem);line-height:1.1;color:var(--bone)}
.show-copy p{margin:0 0 14px;color:var(--sage);font-size:1rem;line-height:1.6;max-width:42ch}
.show-meta{color:var(--sage-dim);font-size:10.5px;letter-spacing:.15em}
.show-more{margin-top:8px}

.show-dots{display:flex;gap:7px;justify-content:center;margin-top:clamp(20px,2.6vw,30px)}
.show-dots i{width:7px;height:7px;border-radius:50%;background:var(--line);transition:background .25s,width .25s}
.show-dots i.on{background:var(--leaf);width:22px;border-radius:99px}

@media (prefers-reduced-motion:reduce){
  .chips-track{animation:none}
  .show-card{transition:none;opacity:1;transform:none}
  .show-card.is-on .show-photo img{transform:none}
  .show-photo img{transition:none}
}

/* ---------- statement + process ----------
   Sky, 2026-09-22: "Harmone's but with our images in there and dark template."

   These two are the rhythm pieces the earlier pass missed. Harmone's homepage is not a
   stack of feature blocks — it alternates SAY ONE THING / SHOW ONE THING, and the two
   bands that do the saying are a single large sentence under a pill, and a short
   numbered process. Without them the page reads as a list of sections, which is exactly
   what ours was doing. */

.stmt{padding-block:clamp(56px,8vw,104px)}
.stmt-line{
  font-family:var(--display);font-weight:400;
  font-size:clamp(1.5rem,3.2vw,2.75rem);line-height:1.22;letter-spacing:-.018em;
  color:var(--bone);text-wrap:balance;
  max-width:26ch;margin:0;
}
/* Harmone sets this sentence wide and centred; ours runs left because every other
   heading on the site does, and one centred block in a left-aligned page reads as a
   mistake rather than a choice. */
@media(min-width:900px){.stmt-line{max-width:30ch}}

.stmt-photo{
  margin:clamp(34px,4.6vw,62px) 0 0;
  border-radius:var(--r-xl);overflow:hidden;
  aspect-ratio:21/9;background:var(--ink-2);border:1px solid var(--line);
}
@media(max-width:700px){.stmt-photo{aspect-ratio:4/3}}
.stmt-photo img{
  width:100%;height:100%;object-fit:cover;display:block;
  filter:saturate(1.03) contrast(1.02);
  transition:transform 1.6s cubic-bezier(.16,.84,.34,1);
}
.stmt-photo:hover img{transform:scale(1.035)}

.proc{padding-block:clamp(56px,8vw,104px);border-top:1px solid var(--line)}
.proc-list{
  list-style:none;margin:clamp(26px,3.4vw,44px) 0 0;padding:0;
  display:grid;grid-template-columns:repeat(4,1fr);gap:clamp(14px,1.8vw,26px);
  counter-reset:none;
}
@media(max-width:1000px){.proc-list{grid-template-columns:repeat(2,1fr)}}
@media(max-width:620px){.proc-list{grid-template-columns:1fr}}
.proc-step{
  padding-top:22px;border-top:1px solid var(--line);
  transition:border-color .3s ease;
}
.proc-step:hover{border-top-color:var(--leaf)}
.proc-n{display:block;color:var(--leaf);margin-bottom:14px;font-size:10.5px;letter-spacing:.2em}
.proc-step h3{margin:0 0 10px;font-size:clamp(1.05rem,1.45vw,1.28rem);line-height:1.24;color:var(--bone)}
.proc-step p{margin:0;color:var(--sage);font-size:.94rem;line-height:1.6}

@media (prefers-reduced-motion:reduce){
  .stmt-photo img{transition:none}
  .stmt-photo:hover img{transform:none}
}

/* ---------- photograph reveal ----------
   See the matching block in motion.js.

   ⚠️ THE FIRST VERSION OF THIS DEADLOCKED, and it would have made every photograph on
   the site invisible. It put `clip-path: inset(0 0 100%)` on the observed element —
   which clips it to ZERO HEIGHT, so IntersectionObserver reported no intersection, so
   `.seen` was never added, so it stayed clipped. Forever. Caught by scrolling a real
   browser to the services grid and finding the tiles still masked; a manually created
   observer on the same element also never fired, which is what proved it was the CSS
   and not the script.

   So nothing that the observer watches may clip itself. The reveal is now:
     - the IMAGE fades and settles back from a slight over-scale, and
     - a ::after CURTAIN on the container wipes downward.
   Opacity, transform and a pseudo-element are all invisible to IntersectionObserver,
   so the element always reports its true box and the deadlock cannot come back.

   .shot is added by the script, so without JavaScript none of this applies and every
   photograph is simply visible — a CSS-first version would hide the whole library the
   moment a script failed. */

.shot{position:relative}

.shot img,
.shot canvas{
  opacity:0;
  transform:scale(1.14);
  transition:opacity .9s ease, transform 1.5s cubic-bezier(.16,.84,.34,1);
  will-change:opacity,transform;
}
.shot.seen img,
.shot.seen canvas{opacity:1;transform:none}

/* The curtain. A pseudo-element, so it never touches the container's intersection box. */
.shot::after{
  content:"";position:absolute;inset:0;z-index:4;pointer-events:none;
  background:var(--ink-2);
  transform-origin:bottom;
  transition:transform 1s cubic-bezier(.16,.84,.34,1);
}
.shot.seen::after{transform:scaleY(0)}

/* The hover push-in has to beat the reveal's resting state, or a card moused over
   mid-reveal snaps back to 1.14. */
.workcard.seen:hover img,
.gal-open:hover img{transform:scale(1.045)}
.show-card.is-on .show-photo.seen img{transform:scale(1.045)}

@media (prefers-reduced-motion:reduce){
  .shot img,.shot canvas{opacity:1;transform:none;transition:none}
  .shot::after{display:none}
}

/* ---------------------------------------------------------- in-body click-to-call
   The number at a size you can read across a room, in the reading column, on every
   page. Centred on its own band rather than tucked into a corner, because the whole
   point is that a visitor who has just decided to ring does not have to look for it. */
.callcta{padding-block:clamp(44px,6vw,78px);border-top:1px solid var(--line)}
.callcta-inner{
  max-width:var(--measure,720px);margin-inline:auto;text-align:center;
  display:flex;flex-direction:column;align-items:center;gap:0;
}
.callcta-inner h2{margin:10px 0 0;max-width:18ch}
.callcta-inner .lede{margin:16px auto 0;max-width:52ch}
.callcta-tel{
  display:inline-flex;align-items:center;gap:14px;margin-top:clamp(22px,3vw,32px);
  font-family:var(--mono);font-size:clamp(1.25rem,3.4vw,1.9rem);letter-spacing:.06em;
  text-decoration:none;color:var(--bone);
  border:1px solid var(--clay);border-radius:var(--r-lg);
  padding:clamp(14px,2vw,20px) clamp(22px,3.4vw,34px);
  background:var(--ink-2);
  transition:background .2s,border-color .2s,transform .2s;
}
.callcta-tel svg{color:var(--clay);flex:none}
.callcta-tel:hover,.callcta-tel:focus-visible{background:var(--ink-3);border-color:var(--clay-dim);transform:translateY(-1px)}
.callcta-sub{margin-top:16px;color:var(--sage-dim)}
@media (prefers-reduced-motion:reduce){.callcta-tel{transition:none}}

/* A licence number is a legal line, not decoration — small, quiet, and above the
   gallery rather than lost in the footer where the trade it applies to is not. */
.svc-lic{
  margin:0 0 clamp(20px,3vw,34px);padding-top:14px;
  border-top:1px solid var(--line);color:var(--sage-dim);
}

/* Agency credit — the quietest line on the page, below the legal bar. */
.fcredit{justify-content:center;padding-top:4px;color:var(--sage-dim);font-size:12px}
.fcredit a{color:var(--sage-dim);text-decoration:underline;text-underline-offset:2px}
.fcredit a:hover{color:var(--leaf-deep)}

/* ---------------------------------------------------------------- consent banner
   Bottom-left card rather than a full-width bar across the foot of the page: the
   click-to-call CTA and the footer both live down there, and a bar covers them on a
   phone. It declares its own `color` — a scope that only redefines --bone leaves any
   element without an explicit color inheriting the computed value from outside it,
   which is how you get dark text on a dark card. */
.consent{
  position:fixed; z-index:120; left:16px; right:16px; bottom:16px;
  max-width:440px;
  background:var(--ink-3); color:var(--bone);
  /* The card fill sits 1.10:1 against the page, so the fill cannot be what separates
     them — the BORDER has to carry it. #7D7869 measures 3.61:1 on the card and 3.98:1
     on the page, clearing the 3:1 that WCAG 1.4.11 asks of a component boundary.
     rgba(23,30,23,.14) was here first and came out at roughly 1.5:1: invisible, and
     a screenshot will not tell you so because the shadow fakes the edge. */
  border:1px solid #7D7869; border-radius:14px;
  box-shadow:0 18px 50px rgba(46,38,22,.22);
  opacity:0; transform:translateY(14px);
  transition:opacity .28s ease, transform .28s ease;
}
.consent.is-on{opacity:1; transform:none}
.consent-in{padding:20px 22px}
.consent-t{font-size:17px; line-height:1.25; margin:0 0 8px}
.consent-b{font-size:14.5px; line-height:1.55; margin:0 0 16px; color:var(--bone); opacity:.88}
/* --leaf is 4.29:1 on this card — under the 4.5:1 AA needs for text this size.
   --leaf-deep is 5.26:1 and is already in the palette. Measured, not guessed. */
.consent-b a{color:var(--leaf-deep); text-decoration:underline; text-underline-offset:2px}
.consent-acts{display:flex; gap:10px; flex-wrap:wrap}
.consent .btn{font-size:14.5px; padding:10px 20px}

@media (min-width:720px){
  .consent{left:24px; right:auto; bottom:24px}
}
@media (prefers-reduced-motion:reduce){
  .consent{transition:none}
}

/* The standing way back in. Quiet, but never absent — a banner you cannot recall is
   not a choice, it is a one-time toll. */
.consent-recall{
  background:none; border:0; padding:0; margin:0;
  font:inherit; color:inherit; cursor:pointer;
  text-decoration:underline; text-underline-offset:2px;
}
.consent-recall:hover{color:var(--leaf)}
