/* THE TYPEFACES ARE DECLARED HERE, NOT IN THE PAGE.
   These two @font-face blocks lived in an inline <style> in every page head,
   and this site ships `style-src 'self'` with no 'unsafe-inline' — so in
   production the block was dropped and both faces silently failed to load.
   Every page would have rendered in the system UI font while looking, in dev,
   exactly as intended. The live root pages declare their fonts from their
   stylesheet for the same reason. */
@font-face { font-family: 'Archivo'; src: url('/assets/fonts/archivo-var.woff2') format('woff2-variations');
             font-weight: 100 900; font-display: swap; font-style: normal; }
@font-face { font-family: 'Inter'; src: url('/assets/fonts/inter-var.woff2') format('woff2-variations');
             font-weight: 100 900; font-display: swap; font-style: normal; }

/* ============================================================================
   MODERN 1.0 — DZT Landscaping & Design
   ============================================================================

   THIRD PASS: THE SHAPE AND DEPTH SYSTEM.

   The second pass fixed the content — 18 sections down to 9, only the owner's
   own photographs, no slogans. It did not fix the DESIGN GRAMMAR, and an audit
   of the rendered page said so in numbers: 63 sharp-edged painted boxes, 63
   bordered elements (22 of them <li> chips), four equal-width grids, and
   exactly one border-radius value in the whole stylesheet — 2px. Read as a
   shape, the page was rectangle, line, rectangle, line. A drafting grid.

   The correction is not "round the corners". A page of rounded cards is the
   same failure wearing a different hat. What changed:

     · A CLOSED RADIUS SCALE, five steps, each with a job. Nothing picks a
       radius freehand. Pills are reserved for things that are actually pills.
     · A SURFACE LADDER instead of one black. Five related darks — near-black,
       charcoal, graphite, forest, and a translucent floating tone — so depth
       comes from material rather than from drop shadows.
     · OVERLAP AS THE DEFAULT SECTION JOIN. The proof rail sits over the hero,
       the service canvas crosses its own section edge, surfaces curve into one
       another. Sections stop being stacked components.
     · BORDERS SPENT, NOT SPRAYED. Separation now comes from spacing, surface
       tone and type. A line has to earn its place.
     · UPPERCASE RATIONED. It was on eyebrows, tags, labels, list items and
       chips at .17em tracking, which is the house style of every architecture
       template on the internet.

   MATERIAL, NOT DECORATION. DZT builds in stone, concrete, earth and water.
   The materials are hard; the interface does not have to be. Strong type and
   dark architectural surfaces, soft spatial geometry, organic photography.

   ONE STYLESHEET ON PURPOSE. This page does not load assets/css/styles.css —
   that is the shipping site's cascade, and inheriting it would mean every rule
   here fighting one written for a different design.

   ONLY DZT'S OWN PHOTOGRAPHS APPEAR. Nine frames across four projects; the
   nine Pexels stand-ins are listed in the page's head comment and must never
   be used. Where a photograph does not exist the page asks for it (.slot).

   NO LICENCE, BOND OR INSURANCE CLAIM. Removed from all 21 public pages at the
   owner's direction (dea3661, 6944b0b) and not reintroduced here.
   ========================================================================= */

/* ---------------------------------------------------------------- TOKENS -- */
:root {
  /* ---- SURFACE LADDER -------------------------------------------------
     Five related darks rather than one black. The steps are small on
     purpose: the viewer should feel that a surface sits forward, not
     notice a different colour. Each carries a trace of green so the whole
     page reads as one material family rather than neutral grey. */
  --s-0:  #080A09;   /* page floor, near-black          */
  --s-1:  #0D1110;   /* charcoal — raised ground        */
  --s-2:  #141918;   /* graphite — floating surface     */
  --s-3:  #0C1F17;   /* deep forest — the estimate      */
  /* Intermediate material tones. The page was reading as black/cream/green —
     three values, so every transition was a hard cut between two of them.
     These sit BETWEEN the others and exist to be transitioned through, which
     is what stops the sections looking stacked. */
  --s-05: #0A0E0C;   /* green-black, one step off the floor */
  --s-15: #131916;   /* charcoal forest                     */
  --s-25: #20251F;   /* warm graphite                       */
  --s-air: rgba(243,241,237,.045);  /* translucent, over photography */

  /* One warm light surface, used once, for the project gallery. The site
     floor stays dark — the whole-page light flip was built and rejected
     (design 4.5 in the switcher). This is a surface inside a dark page. */
  --paper:   #E9E4DB;
  --paper-2: #DED8CC;

  --bone:  #F3F1ED;
  --stone: #A7A093;
  --slate: #56514A;
  --coal:  #14100D;

  --forest: #0E4D33;
  /* THE GREEN IS THE LOGO'S OWN NOW, not a neon one. #6EB97E is #285331 — the
     lightest of the four firs in the mark — with its hue (133deg) and
     saturation (35%) carried over exactly and only the lightness raised, 24%
     to 58%, which is what makes it legible as ink without making it neon.
     --volt was hue 147deg at far higher saturation: a tech-brand green on a
     landscaping company.

     Contrast barely moves, which is why this is safe across all 27 use sites:
     the CTA's dark label reads 7.23:1 against it where volt gave 7.39, and as
     an accent on the page floor it is 8.42:1 against volt's 8.61.

     --volt-rgb exists so the glow halos can be driven from the same value.
     They were hardcoded rgba(var(--volt-rgb),...) and would otherwise have stayed
     neon while everything around them changed.

     Still rationed: primary action only. */
  --volt:     #6EB97E;
  --volt-rgb: 110, 185, 126;
  /* THE HOVER GREEN, DERIVED — because hand-typing it is what broke it.
     var(--volt-hi) was hard-coded into .act:hover, .ai__send:hover and
     .ai__contact-btn--call:hover back when --volt was the neon #46C07E. Against
     that green it was a clean lighten: hue 147.5 -> 146.7, lightness +6.1. When
     --volt became the logo's #6EB97E the hover stayed put, so the relationship
     inverted — hovering the primary CTA moved hue +13.9deg and saturation +20
     points and had no lightness change at all. The button did not get brighter
     on hover, it got NEON, jumping back to the palette we had left behind.

     Derived from --volt now, so it cannot be left behind again: same hue, same
     saturation, the same +6 lightness the neon pair had. It also means the
     localdev neon toggle, which only ever set --volt, now gets a matching hover
     instead of a mismatched one.

     The flat hex first so a browser without color-mix still gets the right
     colour rather than falling back to an unstyled hover. */
  --volt-hi: #83C391;
  --volt-hi: color-mix(in srgb, white 14%, var(--volt));

  /* Lines are now a deliberate spend. Two weights, and the page uses very
     few of either — separation comes from spacing and surface tone. */
  --hair-d: rgba(243,241,237,.09);
  --hair-l: rgba(20,16,13,.12);

  /* ---- RADIUS SCALE ---------------------------------------------------
     Closed set. Nothing in this file picks a value outside it. */
  --r-xs:  10px;   /* inputs, small inset surfaces        */
  --r-s:   14px;   /* buttons and interactive controls    */
  --r-m:   20px;   /* nav, proof rail, panels             */
  --r-l:   28px;   /* media                               */
  --r-xl:  40px;   /* hero canvas, service canvas, estimate */
  --r-pill: 999px; /* only genuinely pill-shaped controls */

  /* ---- DEPTH ----------------------------------------------------------
     Environmental, not card. The shadow should never be the thing you
     see; it should be the reason the surface reads as forward. */
  --sh-soft: 0 24px 70px rgba(0,0,0,.36);
  --sh-lift: 0 40px 120px rgba(0,0,0,.48);
  --edge:    inset 0 1px 0 rgba(243,241,237,.055);

  --nav-h: max(66px, 2.4vw);   /* the masthead once it has been scrolled  */
  /* THE LOGO OPENS BIG AND SHRINKS. The roundel is the brand — at 42px it was
     a favicon sitting next to four links. Over the hero it is the size of a
     masthead and the bar has no background behind it, so it reads as a mark
     printed on the photograph; on scroll it draws down into the solid bar.
     The bar's height is derived from the logo rather than set beside it, so
     the two can never disagree. */
  /* THE MARK OPENS BIGGER. It is now the only thing standing between the top
     of the hero photograph and the headline, and at 128px it was reading as a
     favicon next to four links. The stuck size has to clear --nav-h, which is
     max(66px, 2.4vw), so 50px leaves 8px of air top and bottom at the narrow
     end and scales with the bar past 2700px. */
  /* The masthead wordmark is sized by height; --logo-lg/--logo-sm stay because
     the footer badge still uses them. --nav-tall is derived from the wordmark
     now, so the opening bar is as tall as what it actually contains. */
  /* THE MARK OPENS BIG AND DRAWS DOWN. It is the only thing between the top of
     the hero photograph and the headline and it sits outside the nav panel
     now, with the whole left of the masthead to itself — 75px at 1440 was
     sized for a lockup that had to share a bar, which it no longer does.

     THE STUCK SIZE IS NOT BOUND BY THE BAR ANY MORE. It was sized to fit
     inside the panel with air around it — max(66px, 2.4vw) — back when the
     mark lived in there. It does not: it sits beside the panel now, on the
     page, so the only thing limiting it is what looks right next to a 66px bar.
     37px was solving a constraint that no longer exists, and it read as small
     because it was.

     THE COLLAPSE WAS THE PROBLEM, NOT EITHER SIZE ON ITS OWN. 101 to 52 is
     1.94x — the mark loses half of itself, and what lands in the stuck bar
     reads as a smaller, different object rather than as the same mark settling.
     A masthead that shrinks on scroll should give up a quarter to a third, not
     a half, or the two states stop looking like one thing.

     So the gap closes from both ends, and most of it from below. The stuck size
     was pinned at 52 because that is where its bottom edge lands flush with the
     66px bar — but the 12.8px of top margin above it was spare, and handing
     that to the mark buys height for free. 6.5px of margin and 62px of mark
     puts the bottom back on the same line. The open size gives up 7px, which
     nobody sees on its own; what they see is that the mark no longer halves.

     94px open, 62px stuck at 1440 — 1.51x. You still watch it draw down.

     BOTH CAPS HINGE WHERE THE BAR DOES, or the ratio comes undone on a wide
     screen. --nav-h is max(66px, 2.4vw), so the bar is a flat 66px all the way
     out to 2750px — which means the stuck mark cannot grow through that whole
     range no matter what its own clamp says. Left to keep climbing on 6.5vw,
     the open mark hit 104 at 1920 against the same 62, and the collapse was
     back to 1.68x. Wider screen, worse problem: exactly the thing being fixed.

     So the open mark parks at 96 and picks up again on 3.5vw, which crosses 96
     at 2743px; the stuck mark parks at 62 and resumes on 2.25vw, crossing at
     2756. Both within a few pixels of where the bar itself starts growing, so
     the three move as one set and the ratio holds near 1.5 from 1041 to 4K. */
  --mark-h:    clamp(52px, 6.5vw, max(96px, 3.5vw));
  --mark-h-sm: clamp(40px, 4.3vw, max(62px, 2.25vw));

  --logo-lg: clamp(96px, 11vw, max(168px, 6.4vw));
  --logo-sm: max(50px, 1.85vw);
  --nav-tall: calc(var(--mark-h) + 2.8rem);
  --gut:   clamp(1.1rem, 4vw, 4.5rem);
  --wrap:  max(1320px, 46vw);   /* fixed until ~2870px, then keeps proportion */
  --inset: clamp(.6rem, 1vw, 1rem);   /* how far canvases sit off the edge */

  --ff-d: 'Archivo', 'Helvetica Neue', Arial, sans-serif;
  --ff-b: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;

  --ease:  cubic-bezier(.22,.61,.36,1);
  --ease-s: cubic-bezier(.65,.02,.22,1);   /* for surfaces and masks */
}

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

/* THE ROOT SIZE IS THE ONE LEVER THAT REACHES EVERYTHING.

   Almost every dimension in this stylesheet is in rem — type, padding, gaps,
   button sizing, the gutter. So a page on a 3840 monitor was rendering 16px
   body text and .72rem labels inside a frame two and a half times the width it
   was designed for, and no amount of per-component tuning fixes that; the
   whole UI is simply small. `max(100%, .58vw)` leaves the browser default
   alone below ~2760px (so every width signed off so far is untouched, and a
   reader who has raised their own default still wins) and past that scales the
   entire page together instead of leaving it stranded at one size. */
html { -webkit-text-size-adjust: 100%; background: var(--s-0); font-size: max(100%, .58vw); }
/* Smooth is gated. Unconditionally, it animates every programmatic scroll —
   a harness's, and the browser's own scroll restoration — which is how a page
   ends up appearing not to scroll at all. */
@media (prefers-reduced-motion: no-preference) { html { scroll-behavior: smooth; } }

body {
  margin: 0;
  background: var(--s-0);
  color: var(--bone);
  font-family: var(--ff-b);
  font-size: 1rem;
  line-height: 1.62;
  -webkit-font-smoothing: antialiased;
  overflow-x: clip;
}

/* `height: auto` is load-bearing. Every <img> carries width/height attributes
   so the box is reserved before the file lands, but those map to a
   presentational `height: <n>px` that beats `aspect-ratio` outright — without
   this, figures laid out at their attribute height and rows stood 800px tall
   for no visible reason. Anything wanting a real height overrides it below. */
img, svg, video { max-width: 100%; height: auto; display: block; }
a { color: inherit; }
h1, h2, h3, h4 { margin: 0; font-weight: 500; }
p { margin: 0; }
ul, ol { margin: 0; padding: 0; list-style: none; }
button { font: inherit; color: inherit; }

:focus-visible { outline: 2px solid var(--volt); outline-offset: 3px; border-radius: var(--r-xs); }
/* VOLT IS INVISIBLE ON PAPER. #46C07E against --paper measures 1.82:1, so on
   every tan surface — Recent work, Reviews, and the main plate of 22 interior
   pages — a keyboard user got no focus indicator at all. Forest is the same
   green one step down the ladder and reads 4.9:1 on the same ground. Nothing
   on the dark ground changes. */
.on-paper :focus-visible, .plate :focus-visible { outline-color: var(--forest); }

.skip { position: absolute; left: -9999px; top: 0; z-index: 200;
        background: var(--volt); color: var(--s-0); padding: .8rem 1.2rem;
        font-weight: 600; border-radius: var(--r-s); }
.skip:focus { left: var(--inset); top: var(--inset); }

.wrap { width: 100%; max-width: var(--wrap); margin-inline: auto; padding-inline: var(--gut); }
[id] { scroll-margin-top: calc(var(--nav-h) + 2.5rem); }

/* ------------------------------------------------------------ TYPE SCALE -- */
/* More contrast than before, and much less uppercase. Tracking on the small
   label came down from .17em to .06em: wide-tracked caps on every eyebrow,
   tag, label and chip is the single most template-like thing a page can do. */
/* THE UPPER BOUND IS A SECOND VIEWPORT TERM, NOT A DEAD STOP.

   Every size here used to end at a fixed rem cap, which meant the whole page
   stopped growing at about 1600px and then stayed that size for ever. On a
   2560 or 3840 monitor that reads as a small page marooned in a big window —
   in the hero, a 86px headline 994px wide sitting in a 2528px frame with 691px
   of empty photograph above it.

   `max(<old cap>, <slower vw>)` keeps the old cap as the floor of the ceiling,
   so nothing changes at or below roughly 2200px — every width the design was
   actually signed off at renders byte-identical — and past that the second term
   takes over and the page keeps its proportions instead of its pixel sizes. */
.d-xl { font-family: var(--ff-d); font-weight: 600; letter-spacing: -.032em; line-height: 1.0;
        font-size: clamp(2.5rem, 6vw, max(5.4rem, 3.9vw)); }
.d-l  { font-family: var(--ff-d); font-weight: 600; letter-spacing: -.026em; line-height: 1.06;
        font-size: clamp(1.9rem, 3.4vw, max(3.1rem, 2.2vw)); }
.d-m  { font-family: var(--ff-d); font-weight: 600; letter-spacing: -.02em;  line-height: 1.16;
        font-size: clamp(1.25rem, 2vw, max(1.75rem, 1.25vw)); }

.label { display: block; font-size: .72rem; font-weight: 600; letter-spacing: .06em;
         text-transform: uppercase; color: var(--stone); }
.on-paper .label { color: var(--slate); }

.lede  { font-size: clamp(1rem, 1.35vw, max(1.15rem, .78vw)); line-height: 1.62; color: var(--stone); max-width: 54ch; }
.on-paper .lede { color: var(--slate); }
.small { font-size: .9rem; line-height: 1.58; color: var(--stone); }
.on-paper .small { color: var(--slate); }

/* Metadata as editorial text, not as outlined boxes. This one rule replaces
   22 bordered <li> chips — the largest single contributor to the sharp-edged
   feel. Separators are real characters, so they wrap with the words. */
.meta { font-size: .85rem; color: var(--stone); }
.on-paper .meta { color: var(--slate); }
.meta li { display: inline; }
.meta li + li::before { content: ' · '; opacity: .55; }

.num { font-variant-numeric: tabular-nums; }

/* --------------------------------------------------------------- GROUNDS -- */
/* Tonal washes, not gradient backgrounds. Each is a single very low-contrast
   radial that lifts one corner of a section a step or two up the ladder. It
   should be felt, never noticed — if you can see where it starts, it is
   too strong. */
/* THE MOTIF: CONTOUR. The one device that belongs to this company and to no
   other site. A topographic map is concentric rings around a high point, which
   is exactly what repeating-radial-gradient draws — so the survey line is real
   geometry, not a decorative SVG of one, and it stays sharp at any zoom and
   costs no request. It sits at 3.5% on ink and 4% on paper: at that strength
   you cannot see it if you look for it, you only stop reading the background
   as flat. Set --contour-at per section to move the high point, so no two
   scenes carry the same survey.

   The grain is the second half. Flat near-black across four sections merges
   into one field; a trace of tooth keeps each surface reading as a surface. */
:root {
  /* Tuned down twice. At 150% x 95% the ellipse is so much larger than the
     section that only a shallow arc of each ring is on screen, and a shallow
     arc of a huge circle is a straight diagonal — it read as hatching, which is
     the one thing this must not do. Smaller ellipse so the curvature is inside
     the frame, wider spacing, and roughly half the contrast. */
  --contour-ink: repeating-radial-gradient(ellipse 62% 52% at var(--contour-at, 16% 104%),
                 rgba(203,214,207,0) 0 74px, rgba(203,214,207,.019) 74px 75px);
  --contour-pap: repeating-radial-gradient(ellipse 58% 48% at var(--contour-at, 88% -6%),
                 rgba(58,48,38,0) 0 80px, rgba(58,48,38,.022) 80px 81px);
  --grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)' opacity='.42'/%3E%3C/svg%3E");
}

.on-ink {
  background:
    var(--contour-ink),
    radial-gradient(120% 80% at 12% 0%, rgba(20,25,24,.85) 0%, rgba(8,10,9,0) 62%),
    radial-gradient(90% 60% at 88% 104%, rgba(26,35,32,.42) 0%, rgba(8,10,9,0) 70%),
    var(--s-0);
  color: var(--bone);
}
.on-paper {
  background:
    var(--contour-pap),
    radial-gradient(90% 62% at 92% 0%, rgba(255,253,248,.34) 0%, rgba(233,228,219,0) 62%),
    var(--paper);
  color: var(--coal);
}

/* Tooth. One shared overlay rather than a texture per section — fixed
   attachment so it never parallaxes against its own surface, and pinned behind
   content so nothing inherits it. */
.on-ink, .on-paper, .hero__canvas { position: relative; isolation: isolate; }
.on-ink::before, .on-paper::before {
  content: ''; position: absolute; inset: 0; z-index: 0; pointer-events: none;
  background-image: var(--grain); background-size: 140px 140px;
  border-radius: inherit;
  opacity: .022; mix-blend-mode: overlay;
}
.on-ink > *, .on-paper > * { position: relative; z-index: 1; }

.band { padding-block: clamp(4.5rem, 9vw, 8rem); }

/* ---------------------------------------------------------------- ACTION -- */
.act {
  display: inline-flex; align-items: center; gap: .65rem;
  background: var(--volt); color: #06210F;
  font-weight: 600; font-size: .9rem; letter-spacing: 0;
  padding: .95rem 1.5rem; border: 0; border-radius: var(--r-s);
  text-decoration: none; cursor: pointer;
  box-shadow: 0 1px 0 rgba(255,255,255,.22) inset, 0 10px 26px rgba(var(--volt-rgb),.16);
  transition: background .3s var(--ease), transform .3s var(--ease), box-shadow .3s var(--ease);
}
.act:hover { background: var(--volt-hi); transform: translateY(-1px);
             box-shadow: 0 1px 0 rgba(255,255,255,.28) inset, 0 16px 34px rgba(var(--volt-rgb),.22); }
.act:active { transform: translateY(0); }
.act__arw { transition: transform .3s var(--ease); }
.act:hover .act__arw { transform: translate(4px,-1px); }

/* Secondary is text with a growing underline, not another rectangle. */
.act-2 {
  position: relative; display: inline-flex; align-items: center; gap: .5rem;
  background: none; border: 0; padding: .5rem 0; cursor: pointer;
  font-weight: 600; font-size: .9rem; color: inherit; text-decoration: none;
}
.act-2::after {
  content: ''; position: absolute; left: 0; right: 0; bottom: .15rem; height: 1px;
  background: currentColor; opacity: .38; transform-origin: left;
  transition: opacity .3s var(--ease), transform .4s var(--ease);
}
.act-2:hover::after { opacity: .95; transform: scaleX(1.02); }
.act-2 .act__arw { transition: transform .3s var(--ease); }
.act-2:hover .act__arw { transform: translate(3px,-1px); }


/* ====================== TWO LOCKUPS, ONE MARK ============================
   THE MASTHEAD CARRIES THE WORDMARK — assets/img/logo-wordmark.svg, the badge
   with the disc, the bone rim and the four firs removed and the viewBox
   retightened to the type that is left. Derived by tools/build-logo-wordmark.py
   from logo-roundel.svg, which is untouched and is still what the favicons, the
   apple-touch-icon and the OG card are built from.

   THE FOOTER CARRIES THE BADGE, plain. Down there it sits beside "DZT
   Landscaping & Design" written out, so a wordmark would be the same thing
   said twice.

   NO GLASS ON EITHER. A dome was built over the footer badge — a polished rim,
   a hard-edged pane sheen, a trace of dispersion and a glint on hover — and it
   is gone at the owner's call, along with the wrapper it needed. The mark
   renders as drawn, on a page that has no treatment on it. */

/* ------------------------------------------------------------------- NAV -- */
/* Floating and inset, not a full-width bar with a hairline under it. Over the
   hero it is effectively invisible chrome; once scrolled it becomes a
   translucent graphite surface with a faint edge and an environmental shadow.
   Restrained — a surface, not a giant pill. */
/* THE NAV PANEL HANGS FROM THE TOP EDGE; THE MARK STAYS OUT OF IT.

   Two objects, not one bar. The panel drops from y=0 with its bottom corners
   rounded and carries a glass surface of its own. The wordmark sits beside it,
   on the photograph, with nothing behind it — it is the brand, not a control,
   and boxing it in with the navigation made it look like one more button.

   ROUNDED ON THE BOTTOM ONLY. Four rounded corners on something touching the
   top of the screen leaves two slivers of photograph above its top corners,
   which reads as a mistake rather than as a shape.

   THE SURFACE REPLACED THE SCRIM. .nav::before was a 100vw gradient laid over
   the top of the picture whose only job was making links readable on open
   photography; the panel does that properly and the gradient is gone. So are
   the text-shadow halos on the links, the phone and the burger — measured
   against this pane the worst case, bone on the hero's sun flare through .72,
   is about 9:1, and a shadow behind text on a solid surface is a smudge.

   The mark, being outside the panel, keeps a shadow of its own: it is still
   white and green type sitting directly on a photograph. */
.nav {
  position: fixed; z-index: 100;
  top: 0; left: 50%; transform: translateX(-50%);
  /* THE PAGE RUNS UNDER THE STATUS BAR; THE CONTROLS DO NOT. viewport-fit=cover
     lets the hero fill the screen to the very top edge, which is the point —
     but the mark and the burger would then sit under the clock and the battery.
     Padding the masthead by the inset moves only the chrome down; the
     photograph behind it still reaches the top of the display. Zero on every
     device without a notch, so nothing changes on desktop. */
  padding-top: env(safe-area-inset-top, 0px);
  width: min(calc(var(--wrap) + 2 * var(--gut)), calc(100% - 2 * var(--inset)));
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: clamp(.8rem, 2vw, 2rem);
  background: none; box-shadow: none; pointer-events: none;
}
.nav > * { pointer-events: auto; }

.nav__panel {
  display: flex; align-items: center; gap: clamp(.8rem, 2vw, 2rem);
  /* NOT --nav-tall. That token is derived from the wordmark, and the wordmark
     is no longer in here — sizing the panel to it left the links floating
     halfway down 120px of glass. The panel only has to hold the links, the
     number and the button, so it is the collapsed bar height plus a little
     air, which puts the tabs near the top edge where they were asked for. */
  height: calc(var(--nav-h) + 1.1rem);
  padding-inline: clamp(1.1rem, 2vw, 1.9rem);
  border-radius: 0 0 var(--r-xl) var(--r-xl);
  background: rgba(13,17,16,.72);
  -webkit-backdrop-filter: blur(22px) saturate(1.25);
  backdrop-filter: blur(22px) saturate(1.25);
  box-shadow: 0 18px 44px rgba(0,0,0,.34), var(--edge),
              0 0 0 1px rgba(243,241,237,.05);
  transition: height .55s var(--ease-s), background .45s var(--ease-s),
              box-shadow .45s var(--ease-s), backdrop-filter .45s var(--ease-s);
}
/* Scrolled: shorter, and the pane firms up. Same surface either way, so it
   reads as the bar settling rather than as one appearing. */
.nav.is-stuck .nav__panel {
  height: var(--nav-h);
  background: rgba(13,17,16,.88);
  -webkit-backdrop-filter: blur(26px) saturate(1.3);
  backdrop-filter: blur(26px) saturate(1.3);
  box-shadow: var(--sh-soft), var(--edge), 0 0 0 1px rgba(243,241,237,.06);
}

/* The mark hangs clear of the top edge and shrinks with the panel. */
.nav__brand { margin-top: clamp(.9rem, 1.8vw, 1.6rem); transition: margin-top .55s var(--ease-s); }

/* THE MARK GETS A PLATE, AND IT RUNS OFF THE LEFT EDGE.
   A nameplate rather than a floating logo: the mark stops being type laid on a
   photograph and becomes a thing the masthead is built out of. It is the same
   object as the nav panel — both hang from the top of the screen, both are
   rounded only where they end — so the masthead reads as two pieces of one
   structure instead of a logo and a bar.

   IT BLEEDS, IT DOES NOT SIT. Anchored at the left edge of the SCREEN, not of
   the layout. The nav is centred inside a max width, so the brand starts some
   way in from the edge; --nav-left is exactly that gap, computed from the same
   tokens the nav's own width comes from, so the plate can be pulled back to
   x=0 rather than aimed at it with a guessed negative number. The extra 2px
   covers subpixel rounding at fractional widths — a one-pixel sliver of
   photograph down the left edge is the whole effect lost.

   TALLER THAN THE MARK, WHICH IS THE POINT. Flush to the type it would read as
   a tight box around a logo. The top runs off the screen (-6rem is simply
   past it at every size — there is nothing to see up there) and the bottom
   clears the baseline, so what is left on screen is a slab with one corner on
   it. The bottom radius matches the panel's --r-xl; the left, top and
   right-of-screen edges have none, because they are not edges, they are exits.

   THE FILL IS THE PAGE FLOOR, NOT PITCH BLACK. --s-0 is #080A09 — the same
   near-black the rest of the site is built on, and it carries a trace of green
   like every other surface in the ladder, so the plate belongs to the page
   rather than being a hole cut in it. A true #000 was the first instinct and
   it is colder than anything else on the site.

   It is opaque on purpose. The nav panel beside it is glass, and a translucent
   version of this was built — the panel's exact rgba(13,17,16,.72), blur and
   hairline — which makes the two read as one material. That is the next thing
   to look at; solid is what ships first, because it is the version that says
   plainly what the shape is before the material is argued about. */
:root {
  --nav-left: max(var(--inset), calc((100vw - var(--wrap) - 2 * var(--gut)) / 2));
}
/* THE SUFFIX IS WHAT MAKES THE PLATE A SHAPE RATHER THAN A COLUMN.
   With DZT and LANDSCAPING alone the type all sits in the top of the slab and
   the empty half below it has nothing to explain it. A third line puts a foot
   on the lockup: the plate now ends a fixed distance under the last thing in
   it, so its height is set by its contents instead of being a number picked to
   look right.

   SIZED OFF THE MARK, NOT OFF THE ROOT. font-size in rem would hold still
   while the mark scales with the viewport and the two would drift apart at
   the ends of the range. Tied to --mark-h it stays in proportion — and it
   follows the mark down automatically when the masthead collapses, because
   --mark-h-sm is what the stuck rule swaps in. The 8px floor is the point
   below which letterspaced caps stop being readable at all.

   TRACKED WIDE, WHICH IS WHY THE TRAILING SPACE HAS TO COME BACK OFF.
   letter-spacing adds its gap after the last letter too, so the box runs half
   a space wider than the word. The negative margin-right is that gap returned.

   THE LEFT EDGE IS CORRECTED IN TWO PARTS, BOTH MEASURED. Setting the span at
   x=0 put its ink 2.0px left of LANDSCAPING's L — read off a 2x render at
   device px 30 against 34 — and that gap is two separate things that happen to
   point the same way:

     the wordmark's own padding   The SVG viewBox starts at x=154.00 and the L
                                  of LANDSCAPING at 165.70, so 11.70 of the
                                  842-unit box is empty space the <img> still
                                  occupies. Against a 393.54-unit tall viewBox
                                  that is mark-h * 11.70/393.54 = mark-h *
                                  .02973 of real left padding.

     the I's left side bearing    Archivo's I carries about .077em of its own,
                                  so the glyph's ink starts inside its box.

   Subtracting the bearing in em rather than folding it into the ratio is what
   keeps this correct at the ends: font-size is clamped at 8px and 13px, so
   past those points the bearing stops scaling with the mark while the viewBox
   padding does not. Two terms, each in the unit it actually scales with. */
.nav__brand { position: relative; flex-direction: column; align-items: flex-start; }
.nav__brand-suffix {
  font-family: var(--ff-d);
  font-weight: 700;
  font-size: clamp(8px, calc(var(--mark-h) * .108), 13px);
  letter-spacing: .44em;
  margin-right: -.44em;
  margin-left: calc(var(--mark-h) * .02973 - .077em);
  margin-top: .5rem;
  line-height: 1;
  color: var(--bone);
  transition: font-size .55s var(--ease-s), margin-top .55s var(--ease-s);
}
.nav.is-stuck .nav__brand-suffix {
  font-size: clamp(8px, calc(var(--mark-h-sm) * .108), 13px);
  margin-left: calc(var(--mark-h-sm) * .02973 - .077em);
  margin-top: .34rem;
}
.nav__brand::before {
  content: ''; position: absolute; z-index: -1;
  left: calc(-1 * var(--nav-left) - 2px);
  right: -1.5rem;
  top: -6rem;
  bottom: -.9rem;
  background: var(--s-0);
  box-shadow: 0 18px 44px rgba(0,0,0,.34), var(--edge);
  border-radius: 0 0 var(--r-xl) 0;
}
/* Tighter than it looks like it should be, on purpose. The bar is 66px and the
   mark is 62px of that; the 4px left over is the only air there is, and it goes
   above the mark so its baseline still lands on the bar's bottom edge. Giving
   the mark that margin back is what paid for the larger stuck size. */
.nav.is-stuck .nav__brand { margin-top: clamp(.2rem, .45vw, .45rem); }
/* THE STACKED SHADOWS CAME OFF WITH THE PLATE, and this is the same call the
   links got when the panel replaced the scrim.

   Three of them were here — tight, mid and a 20px bloom — and they existed for
   one reason: the mark was white and green type sitting directly on open
   photography, a patch running rgb(5,11,0) to rgb(249,255,255), foliage and
   sky inside the same 133x75 box. Carrying its own ground was the only thing
   holding it together; the worst case was 2.30:1 before they were strengthened
   and 4.80:1 after.

   The plate is that ground now, and it is a real surface rather than a halo.
   A bloom behind type on a surface is not depth, it is fog — the panel's links
   dropped their text-shadows for exactly this and the scrolled mark already
   dropped the bloom for exactly this. One tight shadow stays, which seats the
   mark on the plate instead of letting it float a millimetre above it. */
.nav__brand img {
  filter: drop-shadow(0 1px 2px rgba(4,8,6,.6));
  transition: filter .45s var(--ease);
}

/* Width and height both transition, and both are set: the roundel is square,
   and animating only one leaves the intrinsic ratio fighting the other. */
/* The masthead is the wordmark and nothing else. A descriptor line was tried
   here — "Design & Build" lifted out of the SVG, then as a sentence — and taken
   back out: the mark says the name, the hero says what the company does, and a
   third voice between them was one more thing to read before the page starts.

   The mark is still the wordmark rather than the badge, so "DESIGN & BUILD"
   stays out of the SVG. It was 4px tall up here and was never readable. */
.nav__brand { margin-right: auto; display: inline-flex; text-decoration: none; }
/* The mark is a bare <img> now — the <picture> existed only to swap in the
   DZT-only file below 1040, and the phone gets the full lockup. `block` kills
   the inline baseline gap under it, which otherwise adds a few stray pixels to
   the masthead's height at every width. */
.nav__brand img { display: block; }

.nav__brand img { height: var(--mark-h); width: auto;
                  transition: width .55s var(--ease-s), height .55s var(--ease-s); }
.nav.is-stuck .nav__brand img { height: var(--mark-h-sm); width: auto; }
.nav__links { display: flex; gap: clamp(.9rem, 1.8vw, 1.8rem); }
/* THE MASTHEAD SITS ON OPEN PHOTOGRAPHY AND THE SCRIM CANNOT SAVE IT.

   Measured at 1440: the links sit 23.6% down a 313px scrim that runs .55 to
   .22, so they have about 39% of cover. Against what is actually behind them —
   this hero's sun flare is in the upper right, which is where these links are:

       dark canopy   8.15:1     fine
       mid sky       3.83:1     under
       bright sky    2.63:1     under
       sun flare     2.09:1     effectively invisible

   Darkening the scrim cannot fix it. To bring the flare case to 4.5:1 the
   scrim needs about 78%, which is a near-opaque black band laid across the
   photograph — and the rule two blocks up exists precisely to avoid that:
   "that would flatten the picture to protect four words."

   So the type carries its own backdrop, same as the hero eyebrow: a tight
   shadow for the glyph edges and a soft halo that darkens the photo only
   where the letters are. Flare goes 2.09 -> 7.95, bright sky 3.30 -> 9.27,
   and not one pixel of the picture outside the words changes.

   COLOUR, NOT OPACITY, for the quiet state. `opacity` fades the element's
   whole rendering INCLUDING its text-shadow, so the halo would have been
   dimmed by the same .78 that dims the glyph — the fix fighting itself. An
   alpha on the colour fades only the ink. */
.nav__links a { position: relative; font-size: .84rem; font-weight: 500;
                color: rgba(243,241,237,.86);
                text-decoration: none; padding-block: .4rem;
                transition: color .25s var(--ease); }
.nav__links a::after { content: ''; position: absolute; left: 0; right: 0; bottom: .1rem; height: 1px;
                       background: currentColor; transform: scaleX(0); transform-origin: left;
                       transition: transform .35s var(--ease); }
.nav__links a:hover { opacity: 1; }
.nav__links a:hover::after { transform: scaleX(1); }
/* THE SECTION YOU ARE IN, SAID QUIETLY. No pill, no fill, no colour — the
   current link simply comes up to full opacity while its neighbours stay at
   .78, and its rule sits under it at a third strength. You are not meant to
   notice this happening; you are meant to always know where you are. */
.nav__links a[aria-current="true"] { opacity: 1; }
.nav__links a[aria-current="true"]::after { transform: scaleX(1); opacity: .34; }
.nav__tel { font-weight: 600; font-size: .9rem; text-decoration: none; white-space: nowrap;
             }
.nav .act { padding: .68rem 1.15rem; font-size: .84rem; }
/* min-height, not just padding: the three 2px bars plus padding came to 31px
   against a 44px target, and padding alone would have to be large enough to
   look wrong. */
.nav__burger { display: none; background: none; border: 0; padding: .7rem; cursor: pointer;
               border-radius: var(--r-s); min-width: 44px; min-height: 44px; }
.nav__burger span { display: block; width: 24px; height: 2px; background: var(--bone); border-radius: 2px; }
.nav__burger span + span { margin-top: 6px; }

.sheet {
  position: fixed; inset: var(--inset); z-index: 150; border-radius: var(--r-l);
  background: rgba(13,17,16,.97);
  -webkit-backdrop-filter: blur(24px); backdrop-filter: blur(24px);
  box-shadow: var(--sh-lift), var(--edge);
  display: flex; flex-direction: column; justify-content: center; gap: .2rem;
  padding: clamp(1.5rem, 6vw, 3rem);
  opacity: 0; transform: scale(.98) translateY(-8px); visibility: hidden;
  transition: opacity .35s var(--ease-s), transform .45s var(--ease-s), visibility .45s;
}
.sheet.is-on { opacity: 1; transform: none; visibility: visible; }
.sheet a { font-family: var(--ff-d); font-size: clamp(1.7rem, 8vw, 2.6rem); font-weight: 600;
           letter-spacing: -.028em; text-decoration: none; padding-block: .5rem; }
.sheet__close { position: absolute; top: 1rem; right: 1rem; background: none; border: 0;
                font-size: 1.8rem; line-height: 1; cursor: pointer; padding: .6rem .9rem;
                border-radius: var(--r-s); }

/* ============================ 01 · HERO ================================== */
/* The photograph is a designed surface, not a background behind a text block:
   a canvas inset from every viewport edge with a large radius, and the copy
   floating over its lower-left, crossing the canvas edge rather than sitting
   politely inside it. */
/* CROPPING POLICY — owner's rule, 2026-09-19.

   Cropping is allowed and is the default. THE THREE CONSTRUCTION DETAILS IN
   "HOW WE BUILD" ARE THE EXCEPTION: Grade (project-05b), Base and drainage
   (project-05c) and Retaining (band-hardscape) are shown whole, because they
   are evidence rather than decoration — a poured wall described as running
   "the full width of the yard" is not making that point if the stylesheet has
   removed both ends of it. Those three, and only those three, take their
   aspect-ratio from the file and use object-fit: contain.

   THE HERO IS NOT ONE OF THEM. An earlier pass generalised "do not crop" into
   a rule and rebuilt this section around the file's 16:9 ratio, which threw
   away the framing the owner had already chosen — commit 67b28dc moved the
   16:9 window into that frame from 0.30 to 0.42 of the slack specifically to
   bring the hydrangea bed and the path lighting into shot. A 6% crop that
   somebody deliberately positioned is not damage to be repaired. Restored
   verbatim below; do not "fix" it again.

   The only crop that was genuinely too deep was the lit entry walk at 21:9,
   losing 43% of the frame. That one is now 3:2 — 11% — and the owner's
   objection is answered without touching anything else. */

/* ============================ 01 · HERO ================================== */
/* The photograph is a designed surface, not a background behind a text block:
   a canvas inset from every viewport edge with a large radius, and the copy
   floating over its lower-left, crossing the canvas edge rather than sitting
   politely inside it. */
/* FULL BLEED ACROSS, INSET ONLY AT THE BOTTOM. The hero used to carry --inset
   on all three sides like the tan plates do, which left 14.4px of page ground
   down each edge of the photograph. On a plate that reads as a card floating on
   the page; on a hero it reads as the picture failing to reach the edge of the
   screen — reported exactly that way, as "the sides of the hero image are
   black". The image itself was never short: it is object-fit: cover and was
   measured filling the canvas to the pixel, gap 0 on both sides.
   The bottom inset stays, because the proof rail is pulled up to overlap that
   edge and the bottom corners are what make the canvas read as a surface the
   page continues underneath. */
.hero { position: relative; padding: 0 0 var(--inset); }
.hero__canvas {
  position: relative; overflow: hidden;
  height: clamp(560px, 92svh, 1180px);   /* the cap only bites on tall, wide screens */
  /* ALL FOUR, OWNER'S CALL. I had squared the top pair when the canvas went
     full bleed, on the reasoning that a radius touching the top of the screen
     leaves two slivers of page ground above it. Kept round: the slivers read as
     the page showing through rather than as an error, and the shape is what
     ties the hero to the plates and the masthead panel below it. */
  border-radius: var(--r-xl);
  background: var(--s-1);
}
.hero__img { position: absolute; inset: 0; width: 100%; height: 100%;
             object-fit: cover; object-position: 50% 42%; }

/* Two-axis scrim. A single vertical wash heavy enough to carry bone type over
   this backlit canopy also flattens the tree, the flare and the water, which
   is most of the reason to look at the frame. So a horizontal pass shades only
   the type column and clears by 74% across, and a vertical pass holds the
   bottom band. Neither has to be heavy, because neither does both jobs. */
/* The horizontal pass is anchored to THE COPY COLUMN, not to a percentage of
   the canvas. Percentages were tuned at one width, and once the copy moved onto
   the page's content grid the two came apart: at 2560 and wider the wash still
   hugged the left edge while the headline had travelled to the middle of the
   frame, so the type sat outside its own shadow in an empty dark field.

   `50% - wrap/2 + gut` is the copy's left edge — the same expression .wrap and
   .hero__copy resolve to, because the canvas is centred. The offsets past it
   are the width of the headline, so the wash always clears just after the type
   ends. At 1440 this lands within a few pixels of the old percentage stops, so
   the composition that was signed off there is unchanged. */
.hero__canvas::after {
  /* FLOORED, and the spans capped. Below about 1245px the viewport is narrower
     than --wrap, so 50% - wrap/2 is NEGATIVE and every stop shifts left off the
     canvas — on a 390px phone the anchor lands near -400px and the wash never
     reaches transparent inside the frame, leaving the whole picture flattened
     under it. max() floors the anchor at the gutter; min() caps each span so it
     cannot outrun a narrow canvas. The percentages are the pre-change stops
     verbatim, so above ~1245px this renders byte-identically. */
  --copy-l: max(var(--gut), calc(50% - var(--wrap) / 2 + var(--gut)));
  content: ''; position: absolute; inset: 0; pointer-events: none;
  background:
    linear-gradient(to right,
      rgba(6,8,7,.66) 0,
      rgba(6,8,7,.61) var(--copy-l),
      rgba(6,8,7,.42) calc(var(--copy-l) + min(320px, 30%)),
      rgba(6,8,7,.14) calc(var(--copy-l) + min(690px, 56%)),
      rgba(6,8,7,0)   calc(var(--copy-l) + min(970px, 76%))),
    linear-gradient(to top,   rgba(6,8,7,.88) 0%, rgba(6,8,7,.54) 28%, rgba(6,8,7,.18) 58%, rgba(6,8,7,.06) 84%),
    /* A THIRD PASS, TOP-DOWN, AND IT PAYS TWICE.
       Measured on the actual frame: 15% of this photograph's chromatic pixels
       are sky at hue 210, and the palette contains no blue at any point — it is
       the one genuinely foreign colour in the composition. It also sits at the
       TOP, where the two passes above are at their weakest (.06 to .18), so it
       was surviving at nearly full strength while the bottom of the same
       picture was held at .88.

       The same band is what the white DZT mark sits on, which is the legibility
       problem the masthead was carrying three stacked drop-shadows to solve.
       One gradient answers both: the sky comes down, the mark gets a ground.

       Same rgba(6,8,7) as the other two — hue 150, which is --s-0's own hue, so
       this darkens the frame in the page's colour rather than tinting it. The
       canopy's yellow-green is deliberately left alone; pulling it toward the
       brand's 133 would make a real yard look processed, which is a worse
       trade than a slightly wide green. */
    /* OPAQUE AT THE VERY TOP, AND THAT IS THE POINT ON A PHONE. iOS Safari does
       not paint page content behind the status bar while the page is at rest —
       it fills that band with theme-color, which is #080A09. The seam a person
       sees is therefore not a gap in the photograph: it is flat near-black
       meeting textured foliage, two obviously different surfaces butted
       together. viewport-fit=cover governs the notch and the home indicator; it
       was never going to remove that edge.

       So the first rows of the canvas resolve to --s-0 exactly and ramp away
       over about 12% of the hero. The band and the image become one surface and
       the seam stops existing — which is how this looks right on sites that
       appear to run under the clock. It also still does the job it was added
       for, knocking back the sky, since the sky is in the same band. */
    linear-gradient(to bottom,
      rgb(8,10,9) 0%, rgba(6,8,7,.72) 3%, rgba(6,8,7,.38) 8%,
      rgba(6,8,7,.14) 24%, rgba(6,8,7,0) 52%);
}
/* THE HERO COPY SITS ON THE PAGE'S CONTENT GRID, not on its own inset.

   It used to be padded in from the canvas edge by a clamp, which pins it about
   86px from the left AT EVERY WIDTH while everything below it centres on
   --wrap. Measured: at 1440 the headline was 32px off the section beneath it,
   at 2057 it was 340px off, and at 3200 it was 912px off — a headline stranded
   most of a thousand pixels to the left of the page it introduces. It looked
   deliberate at the one width it was designed at.

   Same three declarations .wrap uses, so the two cannot drift: the canvas is
   centred and inset equally, so max-width + auto margins inside it resolve to
   exactly the page's own left edge at every viewport size. */
.hero__copy {
  position: absolute; z-index: 2; left: 0; right: 0; bottom: clamp(2.5rem, 7vh, 5rem);
  width: 100%; max-width: var(--wrap); margin-inline: auto;
  padding-inline: var(--gut);
}
/* THE ONE LABEL ON THIS SITE THAT SITS ON A PHOTOGRAPH, and it was the default
   --stone, which is a colour chosen to be quiet against a flat dark ground.

   It fails on three axes at once, which is why it and nothing else in the hero
   does. Measured against the canvas: it sits highest in the frame, at 0.70 from
   the bottom, where the vertical scrim has fallen to .125 — every other line
   below it has .18 to .73. It is the smallest text in the hero at 11.5px. And
   it was the only one not in bone.

   Contrast of #A7A093 against the photograph behind it, composited through the
   scrim actually in force there:

       dark foliage   6.37:1     ok
       mid canopy     4.56:1     borderline
       lit lawn       2.78:1     fails
       sun flare      1.77:1     effectively invisible

   Bone alone does not fix it — 4.09:1 on the flare, still under the 4.5 small
   text needs. So the line carries its own backdrop: a tight shadow for the
   glyph edges and a soft halo that darkens the photograph immediately around
   them. With it the same four backdrops read 16.8, 15.4, 13.2 and 11.25 to one.

   Scoped to the hero. Every other .label on the site is on a flat ground where
   --stone is correct and a halo would be grime. */
.hero__copy .label {
  margin-bottom: 1.1rem; letter-spacing: .1em;
  color: var(--bone);
  text-shadow: 0 1px 2px rgba(4,8,6,.72), 0 0 12px rgba(4,8,6,.6);
}
/* The headline sets its own line break, so `balance` has nothing to decide and
   a 15ch cap would only wrap the first line early. The measure is wide enough
   to let the explicit break be the break. */
.hero h1 { max-width: 20ch; }
.hero__sub { margin-top: 1.5rem; max-width: 46ch; font-size: clamp(1rem, 1.3vw, max(1.12rem, .76vw));
             color: rgba(243,241,237,.86); }
.hero__acts { margin-top: 2.2rem; display: flex; flex-wrap: wrap; align-items: center; gap: .9rem 1.8rem; }

/* ============================ 02 · PROOF ================================= */
/* Floats across the seam, overlapping the hero canvas it sits on. Translucent,
   so some of the photograph's atmosphere stays behind the numbers — it reads
   as one composition rather than a band that starts where the picture stops. */
.proof {
  position: relative; z-index: 3;
  width: min(calc(var(--wrap) - 2rem), calc(100% - 2 * var(--gut)));
  margin: clamp(-5.5rem, -7vh, -4rem) auto 0;
  border-radius: var(--r-m);
  /* Less fill and more blur, so the photograph stays legible through it: the
     rail should read as a pane of glass resting on the picture, not a dark
     card placed over it. The lit top edge is what sells the glass. */
  background: linear-gradient(to bottom, rgba(30,38,35,.5), rgba(14,18,17,.62));
  -webkit-backdrop-filter: blur(30px) saturate(1.35); backdrop-filter: blur(30px) saturate(1.35);
  box-shadow: var(--sh-soft), inset 0 1px 0 rgba(243,241,237,.13),
              0 0 0 1px rgba(243,241,237,.055);
  padding: clamp(1.4rem, 2.6vw, 2rem) clamp(1.2rem, 3vw, 2.6rem);
  display: flex; flex-wrap: wrap; gap: clamp(1.2rem, 4vw, 3rem) clamp(1.5rem, 6vw, 4.5rem);
}
/* Deliberately not a four-column grid: the items size to their content and sit
   on one baseline, so the rail reads as a line of facts rather than four
   equal cells with rules between them. */
.proof__i { flex: 0 1 auto; }
/* NUMBER TYPOGRAPHY, not text that happens to be numeric. The figures are the
   only place on the page where four values have to be comparable at a glance,
   so they are set like a table of figures: display face, tabular so the digits
   sit in columns, tighter tracking than running text because numerals are
   already wide, and the "+" demoted to a superior mark at 52% — it qualifies
   the number rather than being part of it, and at full size it was reading as
   another digit. */
.proof__n { font-family: var(--ff-d); font-weight: 600; letter-spacing: -.04em; line-height: .92;
            font-size: clamp(1.9rem, 3.4vw, 2.9rem); font-variant-numeric: tabular-nums;
            display: flex; align-items: flex-start; }
.proof__n i { font-style: normal; font-size: .52em; line-height: 1.15;
              margin-left: .06em; opacity: .55; font-weight: 500; }
.proof__l { margin-top: .5rem; font-size: .76rem; letter-spacing: .012em; color: var(--stone); }

/* ====================== 02b · SERVICE-AREA RAIL ==========================
   The live site's ticker, brought across. Same mechanism, restated in this
   design's terms — the original is a light strip on --paper-2 with its own
   type scale, and dropping that onto a dark page would have been a patch of
   the old site sitting in the middle of this one.

   ONE TRANSFORM ON ONE COMPOSITED ELEMENT. The track holds two identical runs
   and travels exactly -50%, so at the end of a cycle run two sits precisely
   where run one began and the loop has no seam. Sixteen names cost what one
   costs, and nothing reflows.

   60s: fast enough to read as alive, slow enough that a name is legible and a
   link is catchable. The strip stays thin so it never competes with the hero
   above it or the work below it.
   ===================================================================== */
.rail {
  border-block: 1px solid var(--hair-d);
  padding-block: clamp(1.5rem, 3.5vw, 2.6rem);
  overflow: hidden;
}
.rail__inner {
  width: min(var(--wrap), calc(100% - 2 * var(--gut)));
  margin-inline: auto;
}
/* THE PLACE NAME, ABOVE THE TICKER. It sits in the flow as the band's
   heading, not behind the names as a watermark — the watermark version was
   tried and the owner's call is that it belongs over the scroll, where it can
   be read as a statement rather than found as texture.

   OUTLINED, NOT FILLED. Solid at this size it competes with the hero headline
   a screen above it; hollow letterforms carry the scale without the weight,
   and on a dark ground an outline is what makes a very large word feel drawn
   rather than shouted.

   8.2vw with a floor and a ceiling. "Greater Sacramento" is 18 characters, so
   the cap is what keeps it inside the content column on a wide screen instead
   of running out of it — this one is meant to FIT and be read, which is the
   whole difference between a heading and a stamp. It sets on two lines below
   about 1400px and one line above, and both are fine: it is a place name, not
   a sentence with a shape to preserve. */
.rail__place {
  margin: 0 0 clamp(.9rem, 2vw, 1.4rem);
  font-family: var(--ff-d); font-weight: 700;
  font-size: clamp(2rem, 8.2vw, 6.5rem);
  line-height: .92; letter-spacing: -.045em;
  text-transform: uppercase;
  color: transparent;
  -webkit-text-stroke: 1.5px rgba(243,241,237,.22);
}
/* ---- THE PLACE NAME ON A PHONE -----------------------------------------
   8.2vw is 32px at 390, which is barely a heading — against 104px on desktop
   the same element read as an afterthought rather than as the statement the
   section is built around. The desktop version is prominent because it is
   allowed to be large; the phone version was not.

   It sets as a deliberate two-line stack here — GREATER over SACRAMENTO —
   which is what gives a phone the same weight desktop gets from width. The
   ceiling is arithmetic, not taste: SACRAMENTO sets about 6.8x its font-size
   wide, and the content column is ~285px at 320 and ~355px at 390, so 12.5vw
   capped at 3.3rem is the largest that never touches the edge at either end.

   Tighter leading, because two lines of this size with normal leading read as
   two separate words rather than as one block. */
@media (max-width: 680px) {
  /* FULL BLEED. Sized to the COLUMN it was still only filling a column, and a
     word that stops short of both gutters cannot read as the biggest thing on
     the screen no matter how many pixels tall it is. It breaks the gutter with
     a negative inline margin and is sized to run slightly past the viewport on
     every phone — SACRAMENTO sets about 6.8x its font-size, so 15.4vw puts it
     a touch wider than the screen at 320, 390 and 430 alike. .rail already
     clips (overflow: hidden), so the overhang costs no horizontal scroll.
     GREATER sits shorter above it, which is what makes the pair read as a
     stacked block rather than as two centred lines. */
  .rail__place {
    margin-inline: calc(-1 * var(--gut));
    font-size: clamp(3rem, 15.4vw, 4.6rem);
    line-height: .84;
    letter-spacing: -.055em;
    /* THE SAME FADE AS DESKTOP, which means matching the RATIO and not the
       number. Desktop draws 1.5px at 104px — a stroke 1.44% of the cap height.
       Copying 1.5px onto a 60px phone version would be proportionally 70%
       heavier and would read as a bolder, harder outline rather than the same
       one seen smaller. 0.9px at 60px is that same 1.44%, and the alpha is
       desktop's .22 exactly. */
    -webkit-text-stroke-width: .9px;
    -webkit-text-stroke-color: rgba(243,241,237,.22);
  }
}
@supports not (-webkit-text-stroke: 1px red) {
  .rail__place { color: rgba(243,241,237,.13); }
}
.rail__row {
  display: flex; align-items: center; gap: 1.2rem;
}
.rail__head { flex: 0 0 auto; padding-right: 1.4rem; border-right: 1px solid var(--hair-d); }
.rail__label {
  font-family: var(--ff-d); font-weight: 600; font-size: .84rem;
  letter-spacing: -.012em; color: var(--bone); margin: 0;
}
.rail__note {
  margin: .22rem 0 0; font-size: .72rem; line-height: 1.2;
  letter-spacing: .01em; color: var(--stone); white-space: nowrap;
}
.rail__note a { color: var(--stone); text-decoration: none;
                border-bottom: 1px solid rgba(167,160,147,.35); }
.rail__note a:hover { color: var(--bone); border-bottom-color: var(--bone); }

/* `flex: 1 1 0`, NOT `1 1 auto`. The track is thousands of pixels wide, so a
   viewport sized from its content asks for more room than the row has, loses,
   and collapses to zero — the strip renders invisible. `0` makes it take what
   is left rather than bid for what it wants. */
.rail__viewport {
  position: relative; flex: 1 1 0; min-width: 0; overflow: hidden;
  -webkit-mask-image: linear-gradient(to right, transparent, #000 3rem, #000 calc(100% - 3rem), transparent);
          mask-image: linear-gradient(to right, transparent, #000 3rem, #000 calc(100% - 3rem), transparent);
}
.rail__track { display: flex; width: max-content; animation: rail-scroll 60s linear infinite; }
.rail__run { display: flex; align-items: center; }
.rail__run > * { position: relative; padding: 0 1.35rem; white-space: nowrap; }
/* The separator is a pseudo-element so the dot is never selectable text and
   never read aloud — punctuation between items, not content. */
.rail__run > *::after {
  content: ''; position: absolute; right: -1px; top: 50%;
  width: 2px; height: 2px; margin-top: -1px;
  border-radius: var(--r-pill); background: var(--hair-d);
}
/* EVERY TOWN LOOKS THE SAME at rest. Eight have pages and eight do not, and a
   visitor cannot tell which is which and should not have to; styling the links
   differently turns the ribbon into a speckle of emphasis that means nothing.
   The link announces itself on hover, where it is useful. */
.rail__run a, .rail__run span {
  font-size: .74rem; font-weight: 500; line-height: 1.7;
  letter-spacing: .04em; color: var(--stone); text-decoration: none;
}
.rail__run a { transition: color .2s var(--ease); }
.rail__run a:hover { color: var(--volt); }
.rail__run a:focus-visible { outline: 2px solid var(--volt); outline-offset: 3px; border-radius: var(--r-xs); }
/* Stops for anyone trying to use it. Without this a link is a moving target. */
.rail:hover .rail__track,
.rail:focus-within .rail__track { animation-play-state: paused; }
@keyframes rail-scroll { from { transform: translateX(0); } to { transform: translateX(-50%); } }

@media (max-width: 700px) {
  /* The head stacks above the rail: 36 characters of caption beside a marquee
     leaves the marquee about 120px, which is one town name and a half. */
  .rail__row { flex-direction: column; align-items: flex-start; gap: .45rem; }
  .rail__head { padding-right: 0; border-right: 0; }
  .rail__viewport { flex: 0 0 auto; width: 100%; }
  .rail__label { font-size: .82rem; }
  .rail__note { font-size: .68rem; white-space: normal; }
  .rail__run > * { padding: 0 .9rem; }
}
/* An endlessly moving strip is an accessibility problem, not a taste one. It
   stops dead and wraps into a static list; the duplicate run is removed so the
   towns are not printed twice, and the edge mask goes with it since there is
   no longer anything scrolling under it. */
@media (prefers-reduced-motion: reduce) {
  .rail__track { animation: none; width: auto; }
  .rail__run:nth-child(2) { display: none; }
  .rail__run { flex-wrap: wrap; }
  .rail__viewport { -webkit-mask-image: none; mask-image: none; }
}

/* ============================ 03 · WORK ================================== */
/* The one warm surface in a dark page, and it is a surface: the paper is
   inset and curved at both ends so the dark ground runs behind it, rather
   than a full-bleed band butting against the sections above and below. */
/* THE JOIN. Dark, then a cream rectangle starting, is the flattest transition
   on the page — two grounds meeting at a line. The plate now reads as a sheet
   physically laid over the dark: it throws a shadow UP onto the section above
   as well as down, and carries a one-pixel lit top edge, so the eye reads one
   surface in front of another instead of two backgrounds taking turns. */
.work {
  position: relative;
  margin-inline: var(--inset);
  border-radius: var(--r-xl);
  padding-block: clamp(4.5rem, 8vw, 7rem);
  margin-top: clamp(3rem, 7vw, 6rem);
  box-shadow: var(--elevated);
}
.head-row { display: flex; flex-wrap: wrap; align-items: flex-end; justify-content: space-between;
            gap: 1rem 2rem; margin-bottom: clamp(2.5rem, 5vw, 4rem); }
.head-row .label { margin-bottom: .7rem; }

/* Three projects, three compositions. Nothing is repeated: the first is media
   left on a two-thirds split, the second is media right and pushed down so the
   pair interlocks, the third is a wide canvas with its metadata floating over
   the lower-left corner on a translucent surface. */
.proj { position: relative; }
.proj + .proj { margin-top: clamp(4rem, 9vw, 8rem); }

/* The figure is now only a positioning context — it has to let the detail
   frame cross its edge — so the clipping, the surface and the shadow move to an
   inner frame, which inherits whatever radius the variant gives the figure. */
.proj__fig { position: relative; margin: 0; overflow: visible; border-radius: var(--r-l); }
.proj__frame {
  overflow: hidden; border-radius: inherit; background: var(--paper-2);
  box-shadow: 0 30px 80px rgba(20,16,13,.16);
}
.proj__frame img { width: 100%; height: auto; object-fit: cover; display: block;
                   transition: transform 1.1s var(--ease-s); }
/* 1.015, not 1.035. Two percent is the difference between a photograph that
   breathes when you approach it and one that visibly zooms. */
.proj:hover .proj__frame img, .proj:focus-within .proj__frame img { transform: scale(1.015); }

/* The project index. Factual — there are three, and this is the second —
   set as a survey tick rather than a page number. */
.proj__ix {
  display: flex; align-items: center; gap: .5rem; margin: 0 0 .8rem;
  font-size: .7rem; font-weight: 600; letter-spacing: .1em;
  font-variant-numeric: tabular-nums; color: var(--slate); opacity: .6;
}
.proj__ix b { font-weight: 700; color: var(--coal); opacity: 1; }
.proj__ix i { display: block; width: clamp(1.4rem, 3vw, 2.6rem); height: 1px;
              background: currentColor; opacity: .5; }



.proj--a { display: grid; grid-template-columns: 1.9fr 1fr; gap: clamp(1.5rem, 3.5vw, 3.5rem); align-items: center; }
.proj--a .proj__frame img { aspect-ratio: 4 / 3; }

/* 1.15fr, not 1fr: at 1fr/2.1fr the text column came out ~400px and the
   description and the dot-separated scope both wrapped badly. The
   composition still reads as media-dominant. */
.proj--b { display: grid; grid-template-columns: 1.15fr 1.85fr; gap: clamp(1.5rem, 3.5vw, 3.5rem); align-items: center; }
.proj--b .proj__fig { order: 2; border-radius: var(--r-xl) var(--r-l) var(--r-l) var(--r-xl); }
.proj--b .proj__frame img { aspect-ratio: 3 / 2; }   /* 11% off a 4:3 source */
.proj--b .proj__meta { order: 1; }

/* THE WIDE ONE IS STILL THE WIDEST, JUST NOT THREE TIMES THE OTHERS.

   Measured at 1440 before this: 1205x803 = 968k square pixels against 429k for
   the two media-left cards and 338k for the interlocked one. Nearly 3x its
   neighbour. It is meant to be the row's biggest moment — a wide canvas with
   its metadata crossing the lower-left — but at 3x it stops reading as
   emphasis and starts reading as a mistake, which is exactly what it was
   called.

   CAPPED BY WIDTH, NOT BY ASPECT. Squeezing the height would mean cropping,
   and this is the lit entry walk — the frame the owner protected by name after
   it went out at 21:9 with 43% of the picture gone. The ratio stays 3:2 and
   the whole photograph stays in it; only the size comes down. 60rem gives
   960x640 = 614k, about 1.4x the cards around it: still first among them, no
   longer a different category of object.

   The space it gives back on the right is not a gap — the metadata panel
   crosses in at the lower left, so the composition keeps its asymmetry. */
.proj--c .proj__fig { border-radius: var(--r-xl); width: min(100%, 60rem); }
.proj--c .proj__frame img { aspect-ratio: 3 / 2; }   /* was 21:9 — 43% of the frame gone */
/* The metadata crosses into the photograph instead of sitting under it. */
.proj--c .proj__meta {
  position: relative; z-index: 2;
  width: min(30rem, 78%);
  margin: clamp(-5rem, -7vw, -3rem) 0 0 clamp(1.5rem, 3vw, 3rem);
  padding: clamp(1.3rem, 2.4vw, 1.9rem) clamp(1.4rem, 2.6vw, 2rem);
  border-radius: var(--r-m);
  /* THE ONE PANEL ON THIS PAGE THAT SITS ON A PHOTOGRAPH, and it was 90%
     opaque — a tinted card over a picture rather than glass on it. Glass needs
     something behind it; this is the only surface here that has any.

     .82 is not a taste call, it is the floor. The small text in this panel is
     --slate #56514A, which needs 4.5:1, and the panel lands on the dusk
     planting in the lower left of the lit-entry-walk frame. Measured through
     the panel onto that exact backdrop:

         .90   slate 5.25:1    10% see-through   (what it was)
         .82   slate 4.55:1    18% see-through   (here)
         .78   slate 4.22:1    22% — fails
         .68   slate 3.45:1    32% — fails badly

     So the transparency roughly doubles and stops. The rest of the glass is
     bought with the EDGE instead, which costs no legibility at all: a polished
     rim and a sheen off the top left, the same two cues that make the mark in
     the masthead read as glass rather than as a dark circle. */
  background: rgba(233,228,219,.82);
  -webkit-backdrop-filter: blur(22px) saturate(1.3);
  backdrop-filter: blur(22px) saturate(1.3);
  background-image: linear-gradient(142deg,
    rgba(255,255,255,.5) 0%, rgba(255,255,255,.14) 26%, rgba(255,255,255,0) 52%);
  box-shadow:
    inset 0 0 0 1px rgba(255,255,255,.4),      /* the polished rim */
    inset 0 1px 0 rgba(255,255,255,.65),       /* light on the top edge */
    var(--sh-soft);
}
.proj__t { margin: .7rem 0 .7rem; }
/* Said quietly, but said. The fourth card carries a reference photograph
   rather than a DZT job, and a portfolio that does not say so is claiming
   someone else's work. Small, recessed and italic — it reads as a footnote on
   the card, not as a disclaimer stuck to it. */
/* No opacity. --slate is already the recessed tone on paper; recessing the
   recessed tone put this at 3.82:1 — the lowest-contrast text on the page, on
   the one line that exists to disclose something. At full strength it is
   6.21:1 and still visibly quieter than the headline and body beside it. */
.proj__note { margin-top: .9rem; font-size: .76rem; font-style: italic;
              color: var(--slate); }
.proj__meta .meta { margin-top: 1.1rem; }

/* The empty slot. All nine of DZT's own frames are already on this page; a
   tenth project needs a tenth photograph, and the honest way to show that is
   to ask for it rather than borrow a stock frame. */
.slot {
  display: flex; flex-direction: column; justify-content: center; gap: .5rem;
  padding: clamp(1.4rem, 3vw, 2.2rem); border-radius: var(--r-l);
  border: 1px dashed rgba(20,16,13,.26); background: rgba(20,16,13,.025);
}
.on-ink .slot { border-color: rgba(243,241,237,.2); background: rgba(243,241,237,.025); }
.slot__k { font-size: .72rem; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: var(--slate); }
.on-ink .slot__k { color: var(--stone); }
.slot p { font-size: .92rem; line-height: 1.55; color: var(--slate); max-width: 62ch; }
.on-ink .slot p { color: var(--stone); }
.slot b { font-weight: 600; color: var(--coal); }
.on-ink .slot b { color: var(--bone); }


/* ====================== THE PAPER IS HELD ABOVE THE PAGE ==================
   YOU CANNOT CAST A SHADOW ON A FLOOR THIS DARK. That is the whole problem,
   and it is arithmetic, not taste.

   --s-0 is #080A09 — rgb(8,10,9), L* 2.61. There are EIGHT code values between
   it and pure black. The shadow that used to be here, two 80px blurs of
   rgba(0,0,0,.42) thrown equally up and down, composites the floor to
   rgb(5,6,5): L* 2.61 -> 1.57, a change of ONE UNIT OF LIGHTNESS spread over
   80px. That is at or under the just-noticeable difference for a soft
   gradient. It was never a weak shadow. It was an unrenderable one, and every
   attempt to fix it by making it darker or bigger was doomed before it was
   drawn — including the one committed yesterday.

   (It was also thrown equally UP and DOWN, which is the shadow cast by a light
   source at infinity in every direction: i.e. by no light source at all.)

   SO THE ELEVATION IS CARRIED BY THE BOUNCE. --paper has relative luminance
   .779 against the floor's .0029 — the sheet is 270x more reflective than the
   ground it hangs in front of. A 78%-albedo surface under any light is a
   second light source, and the backdrop within a few inches of it is
   measurably brighter than the backdrop further out. That term was missing
   entirely, which is why the dark layers had nothing to take away.

   Measured, same cast layer, rgba(2,6,5,.88):
       on the bare floor   rgb(8,10,9) -> rgb(3,6,5)      deltaL*  1.15
       inside the bounce   rgb(38,35,30) -> rgb(7,10,9)   deltaL* 14.17
   Twelve times the work from an identical shadow. The bounce is not a glow
   added for prettiness; it is the surface the shadow needs in order to exist.

   THE PROFILE IS WHAT SELLS IT, not the amplitude. Reading outward from the
   plate's edge: a lit arris on the paper, then a tight dark seam, then a warm
   pool peaking near L* 14, then a slow fall back to L* 2.6. Bright, dark,
   bright, dark — two zero-crossings — is what an eye reads as AIR. A single
   monotonic glow reads as "something bright is behind this rectangle", and two
   flat tones meeting at a rounded rectangle reads as a printed panel.

   THE SHADOWS ARE GREEN-BLACK, NOT BLACK. rgba(2,6,5) is the floor's own hue
   at a third of its value. Pure #000 is COOLER than #080A09, so a black shadow
   here reads as a hole punched through the page rather than as light withheld
   from it. The surface ladder's rule — every dark carries a trace of green so
   the page reads as one material — had simply never been applied to shadows.

   THE LIGHT DIRECTION IS DERIVED, NOT PICKED. The hero scrim and the .on-ink
   washes both light from the upper left (12% 0%). So the cast goes down and
   slightly west, and NOTHING is thrown upward: on a page the ground is a
   vertical backdrop and the plate hangs parallel to it, so a key above-left
   throws down and west. Everything reaching upward is bounce, never cast.

   ORDER MATTERS AND IS NOT COSMETIC. box-shadow paints the FIRST layer on top,
   so the dark layers are listed before the bounce — they have to paint over
   the pool to carve it. Listed the other way round the bounce simply erases
   them and the whole effect collapses back to a glow.

   CLEARANCE: .proof is z-index 3 and paints over .work, so the upward bounce
   has to finish inside .work's own margin-top (48px at the small end). The
   top-side layers are kept under that deliberately. */
:root {
  /* THE BOUNCE IS --paper ITSELF, at a stated alpha. A first pass hand-tuned a
     warmer triple (236,205,172) to fight the floor's green bias; that is a new
     colour in a closed token set, and the page's own rule is that every tone
     carries a trace of the same green so the whole thing reads as one
     material. Derived beats tuned: this is literally the sheet's own colour,
     which is what bounced light off a sheet is.
        at .15 over the floor -> rgb(42,43,40), L* 17.3, a lift of 14.7
        at .075               -> rgb(25,26,25), L*  9.1, a lift of  6.5 */
  --bounce: 233, 228, 219;   /* = --paper */
  --shade:  2, 6, 5;         /* the floor's own hue at a third of its value */

  --elevated:
    /* CONTACT — tight, close, dark. The width of this IS the read of the gap
       height; blurring it further reads as a bigger, vaguer, less convincing
       distance. Listed first so it carves the pool underneath it. */
    0  10px 16px -10px rgba(var(--shade), .95),
    0  -5px 11px  -9px rgba(var(--shade), .7),
    /* CAST — straight down, blur roughly equal to the height it is held at.
       A lateral offset was derived (13 deg west of vertical, ~9px) and then
       dropped: both shadow tokens on this site, --sh-soft and --sh-lift, are
       dead-centre at 0 X, and a single section with a sideways shadow
       contradicts that role system for a cue nobody reads. At a 9.6px inset on
       a phone it reads as a mistake, not as a light source. */
    0 38px 46px -22px rgba(var(--shade), .9),
    /* BOUNCE — the backdrop lit by the sheet, weighted 2:1 downward. The sheet
       is lit from above, so most of what it throws goes down; and down is the
       side that was actually pointed at. Both reaches are inside their budget:
       54px down against .how's 72px top padding, 14px up against .work's own
       48px margin-top — and that upward ceiling is not style, it is clearance,
       because .proof is z-index 3 and paints over anything .work throws up. */
    0  34px 80px -20px rgba(var(--bounce), .15),
    0 -14px 40px -20px rgba(var(--bounce), .075),
    /* THE ARRIS. Warm, not white: pure white on #E9E4DB reads blue at 1px and
       looks like a border rather than like light catching a cut edge. */
    inset 0  1px 0 rgba(255,253,248,.72),
    /* The underside turning away from the key. Deliberately a seam and not an
       extruded lit edge — a 12px band of #AFABA1 under a rounded rectangle is
       the 2009 bevel, and nothing else on this site has one. */
    inset 0 -1px 0 rgba(120,110,96,.28);
}

/* Scoped to the two plates that actually hang on the dark: .work and the
   reviews band. NOT .band.on-paper generally — on the interior pages those
   butt together with a negative margin and squared corners, and giving each
   one its own contact shadow would draw a seam through what is meant to read
   as a single sheet. The reviews band had no shadow at all until now. */
.rev.on-paper { box-shadow: var(--elevated); }

/* ============================ 04 · HOW =================================
   A text column and a cluster of three small placed frames.

   THE FRAMES ARE SMALL BECAUSE THE CAPTIONS LEFT THEM. Each photograph used to
   carry its own heading and sentence, so each needed a caption's worth of
   width, and three of those ate a whole screen — 584px, 481px and 998px wide,
   which is what "way too big and lazy" looked like. With the captions gathered
   into one numbered index, the pictures only have to be pictures: 26%, 19% and
   24% of the row, placed rather than stacked.

   PLACED, NOT GRIDDED. Absolute positions inside a ratio'd box, chosen so no
   two frames share horizontal AND vertical space — the composition reads as
   deliberate because the gaps between them are as considered as the frames.

   Nothing is cropped. Each frame is the photograph's own aspect and there is
   no object-fit anywhere in here.
   ===================================================================== */
.how { padding-block: clamp(4.5rem, 9vw, 8rem); }
.how__grid { display: grid; grid-template-columns: minmax(0, 27rem) minmax(0, 1fr);
             gap: clamp(2.5rem, 6vw, 5.5rem); align-items: center; }
.how__line { font-family: var(--ff-d); font-weight: 500; letter-spacing: -.028em; line-height: 1.14;
             font-size: clamp(1.6rem, 3vw, 2.5rem); margin: 1rem 0 1.2rem; text-wrap: balance; }
.how__text .lede { font-size: 1rem; }

/* The index. Hairline between rows, nothing else — and the rule is doing a
   job, which is the test every line on this page has to pass. */
.layers { margin-top: clamp(1.8rem, 4vw, 2.75rem); }
.layers li { position: relative; display: grid; grid-template-columns: 2.2rem 1fr;
             gap: .1rem .4rem; padding: .95rem 0; cursor: default;
             border-top: 1px solid var(--hair-d);
             transition: opacity .35s var(--ease); }
.layers li:last-child { border-bottom: 1px solid var(--hair-d); }
.layers__n { grid-row: 1 / span 2; font-size: .72rem; font-variant-numeric: tabular-nums;
             color: var(--stone); padding-top: .2em; transition: color .35s var(--ease); }
.layers b { font-family: var(--ff-d); font-weight: 600; font-size: 1.05rem; letter-spacing: -.018em; }
.layers__d { font-size: .88rem; line-height: 1.5; color: var(--stone); max-width: 34ch; }
/* Linked state: the partner lifts, the rest recede. Opacity only — no colour
   change, so nothing flickers on a dark ground. */
.how__grid.is-linked .layers li { opacity: .42; }
.how__grid.is-linked .layers li.is-on { opacity: 1; }
.layers li.is-on .layers__n { color: var(--volt); }

.cluster { position: relative; width: 100%; aspect-ratio: 1.42; }
.lyr { position: absolute; margin: 0; overflow: hidden; background: var(--s-15);
       box-shadow: 0 18px 44px rgba(0,0,0,.34);
       transition: transform .6s var(--ease-s), opacity .45s var(--ease),
                   filter .5s var(--ease), box-shadow .55s var(--ease-s); }
.lyr img { width: 100%; height: auto; display: block; }
/* Three placements. No two overlap in both axes, so the empty space between
   them is composition rather than a gap left over. */
.lyr--1 { width: 46%; left: 0;    top: 0;   border-radius: var(--r-l) var(--r-l) var(--r-l) var(--r-xl); }
.lyr--2 { width: 34%; right: 0;   top: 28%; border-radius: var(--r-xl) var(--r-l) var(--r-l) var(--r-l); }
.lyr--3 { width: 42%; left: 12%;  top: 58%; border-radius: var(--r-l) var(--r-xl) var(--r-xl) var(--r-l); }

/* Depth, not just dimming. The two that are not being talked about sit back a
   little and lose some colour; the one that is comes forward, gains its
   shadow, and goes over the top of them. Reading the section top to bottom you
   are watching the yard get built up in the order it is actually built. */
.how__grid.is-linked .lyr { opacity: .34; transform: scale(.974); filter: saturate(.72); }
.how__grid.is-linked .lyr.is-on { opacity: 1; z-index: 2; filter: none;
                                  transform: translateY(-10px) scale(1.035);
                                  box-shadow: var(--sh-lift); }
/* scale on the FIGURE, not the image inside a clipped box — scaling the image
   under overflow:hidden would crop the very frames that must not be cropped. */

.layers li:focus-visible { outline: 2px solid var(--volt); outline-offset: 4px; border-radius: var(--r-xs); }

@media (max-width: 900px) {
  .how__grid { grid-template-columns: 1fr; }
  .cluster { aspect-ratio: auto; display: grid; gap: 1rem; }
  /* relative, NOT static: the frame's own edge overlay is position:absolute,
     so a static frame would hand it to .cluster and draw one ring around all
     three. top/left are cleared explicitly because the desktop rules set them
     and a relative box would otherwise carry those offsets into the grid. */
  .lyr--1, .lyr--2, .lyr--3 { position: relative; top: auto; left: auto; right: auto;
                              width: 100%; border-radius: var(--r-l); }
  /* Stacked, the recede has to be much gentler. Overlapping on a desktop, a
     third of an opacity reads as depth — one frame in front of two behind it.
     In a vertical column on a dark ground it just reads as two photographs
     that failed to load, which is what it looked like on a phone. The active
     frame carries the state here instead, with its lift and its shadow. */
  .how__grid.is-linked .lyr { opacity: .76; filter: none; transform: none; }
  .how__grid.is-linked .lyr.is-on { opacity: 1; transform: none; box-shadow: var(--sh-lift); }
  .lyr--2 { width: 82%; margin-left: auto; }
  .lyr--3 { width: 92%; }
}
@media (prefers-reduced-motion: reduce) {
  .lyr, .layers li { transition: none; }
  .how__grid.is-linked .lyr { transform: none; filter: none; }
  .how__grid.is-linked .lyr.is-on { transform: none; }
}

/* ============================ 05 · WHAT ================================== */
/* One interactive canvas, not five service cards. The list on the left drives
   a single large photographic surface on the right; choosing a category
   crossfades the image and swaps the description and scope in place. */
.what { position: relative; }
.what__grid { display: grid; grid-template-columns: .85fr 1.15fr; gap: clamp(2rem, 5vw, 4.5rem); align-items: center; }

.what__list { position: relative; }
.what__list button {
  position: relative; width: 100%; text-align: left; background: none; border: 0; cursor: pointer;
  display: grid; grid-template-columns: 2.4rem 1fr; align-items: baseline;
  gap: .4rem; padding: clamp(.75rem, 1.5vw, 1rem) 1rem;
  border-radius: var(--r-s); color: var(--stone);
  transition: color .35s var(--ease), background .35s var(--ease);
}
/* No fill behind the active row. A tinted rounded rectangle under the selected
   item is what makes a list of services read as a software menu; the row earns
   its state from position, weight and the mark instead — it brightens and steps
   right, and the volt rule grows beside it. */
.what__list button { transition: color .35s var(--ease), transform .45s var(--ease-s), opacity .35s var(--ease); }
.what__list button:hover { color: var(--bone); transform: translateX(.2rem); }
.what__list button[aria-selected="true"] { color: var(--bone); transform: translateX(.5rem); }
.what__list button[aria-selected="true"] h3 { font-weight: 600; }
.what__list button:not([aria-selected="true"]) { opacity: .72; }
.what__list .n { font-size: .74rem; font-variant-numeric: tabular-nums; opacity: .5; }
.what__list h3 { font-family: var(--ff-d); font-weight: 600; letter-spacing: -.02em;
                 font-size: clamp(1.05rem, 1.7vw, 1.35rem); }
/* A short mark that grows along the active row — one line, doing a job. */
.what__list button::before {
  content: ''; position: absolute; left: 0; top: 50%; width: 3px; height: 0;
  border-radius: var(--r-pill); background: var(--volt); transform: translateY(-50%);
  transition: height .45s var(--ease-s);
}
.what__list button[aria-selected="true"]::before { height: 58%; }

.what__canvas {
  position: relative; border-radius: var(--r-xl); overflow: hidden;
  background: var(--s-1); box-shadow: var(--sh-lift), var(--edge);
  aspect-ratio: 4 / 3;
}
/* The photograph is wiped in from the side the list sits on as well as faded,
   so the change reads as one surface being drawn across another rather than
   two images dissolving through each other. */
.what__canvas img, .what__canvas .slot {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
  opacity: 0; transform: scale(1.04);
  clip-path: inset(0 0 0 14%);
  transition: opacity .75s var(--ease-s), transform 1.5s var(--ease-s),
              clip-path .9s var(--ease-s);
  border-radius: 0; border: 0; background: var(--s-1);
}
.what__canvas .slot { display: flex; padding: clamp(1.5rem, 4vw, 3rem); }
.what__canvas [data-on="true"] { opacity: 1; transform: none; clip-path: inset(0 0 0 0); }
.what__body { margin-top: 1.6rem; padding-inline: 1rem; min-height: 5.5rem; }
.what__body p { font-size: .95rem; line-height: 1.58; color: var(--stone); max-width: 44ch; }
.what__body .meta { margin-top: .9rem; }
.what__more { margin-top: 1.4rem; padding-inline: 1rem; }

/* ============================ 06 · PROCESS =============================== */
/* A path, not five boxes. One rail runs behind five nodes and fills as the
   section arrives; the labels hang off it. No cells, no rules, no chips. */
.proc { position: relative; }
.proc__rail { position: relative; margin-top: clamp(3rem, 6vw, 4.5rem); padding-top: 2.2rem; }
.proc__rail::before {
  content: ''; position: absolute; left: 0; right: 0; top: 2.2rem; height: 1px;
  background: rgba(243,241,237,.13);
}
.proc__fill {
  position: absolute; left: 0; top: 2.2rem; height: 1px; width: 100%;
  background: linear-gradient(to right, var(--volt), rgba(var(--volt-rgb),.25));
  transform: scaleX(0); transform-origin: left;
  transition: transform 1.8s var(--ease-s);
}
.proc__rail.is-in .proc__fill { transform: scaleX(1); }
.proc__steps { position: relative; display: grid; grid-template-columns: repeat(5, 1fr); }
.proc__i { position: relative; padding-right: clamp(1rem, 2vw, 2rem); }
/* Each node lights as the fill reaches it. The rail takes 1.8s to draw, so the
   delays are that same 1.8s divided across the five — the light arriving at a
   marker IS the line getting there, rather than a separate animation that
   happens to look similar. --i is set per step in the markup. */
.proc__i::before {
  content: ''; position: absolute; left: 0; top: -2.2rem; width: 9px; height: 9px;
  border-radius: var(--r-pill); background: var(--s-0);
  box-shadow: 0 0 0 1px rgba(243,241,237,.28); transform: translateY(-4px);
  transition: background .45s var(--ease), box-shadow .45s var(--ease), transform .45s var(--ease);
}
/* THE STAGGER COMES FROM THE STYLESHEET, NOT FROM style="--i:N".
   --i was set as an inline style attribute on each step, and this site's CSP is
   `style-src 'self'` with no 'unsafe-inline' — so in production every one of
   those attributes is dropped and var(--i, 0) falls back to 0 on all five. The
   nodes would have lit simultaneously instead of running down the rail, which
   is the whole point of the thing, and it would have looked like a design
   choice rather than a blocked attribute. The live root pages carry zero inline
   styles for exactly this reason.
   nth-child gives the same 0.34s cadence with nothing inline. */
.proc__rail.is-in .proc__i::before {
  background: var(--volt); box-shadow: 0 0 0 4px rgba(var(--volt-rgb),.12);
  transition-delay: .12s;
}
.proc__rail.is-in .proc__i:nth-child(2)::before { transition-delay: .46s; }
.proc__rail.is-in .proc__i:nth-child(3)::before { transition-delay: .80s; }
.proc__rail.is-in .proc__i:nth-child(4)::before { transition-delay: 1.14s; }
.proc__rail.is-in .proc__i:nth-child(5)::before { transition-delay: 1.48s; }
.proc__i { transition: opacity .5s var(--ease); }
.proc__rail:hover .proc__i:not(:hover) { opacity: .62; }
.proc__i p { transition: color .4s var(--ease); }
.proc__i:hover p { color: var(--bone); }
.proc__i:hover::before { background: var(--volt); box-shadow: 0 0 0 7px rgba(var(--volt-rgb),.18);
                         transform: translateY(-4px) scale(1.25); transition-delay: 0s; }
.proc__n { font-size: .74rem; font-variant-numeric: tabular-nums; color: var(--stone); opacity: .7; }
.proc__i h3 { margin: .55rem 0 .4rem; font-family: var(--ff-d); font-weight: 600; font-size: 1.05rem; letter-spacing: -.018em; }
.proc__i p { font-size: .87rem; line-height: 1.5; color: var(--stone); }
.proc__foot { margin-top: clamp(2.5rem, 5vw, 3.5rem); display: flex; flex-wrap: wrap; align-items: center; gap: 1rem 2.5rem; }

/* ============================ 07 · REVIEWS =============================
   All three quotes on the page at once, on one rounded surface.

   TWO THINGS WERE TRIED AND BOTH WERE WRONG. Three equal cards in a row read
   as a testimonial widget; a selector that showed one quote and hid two behind
   a control read as a component asking to be operated, in the one place on the
   page where a stranger is supposed to read what other people said without
   doing anything. Three reviews is not enough content to justify hiding two of
   them.

   So: one surface, everything visible, and hierarchy instead of interaction.
   The rating is set as display typography, the first quote runs at headline
   scale, and the other two sit beneath at reading size — a lead voice and two
   corroborating ones rather than three of the same thing.
   ===================================================================== */
.rev__top { max-width: 48rem; }

.rev__stage {
  --stage-pad: clamp(1.75rem, 4.5vw, 3.5rem);
  margin-top: clamp(2rem, 5vw, 3.5rem);
  border-radius: var(--r-xl);
  background: rgba(20,16,13,.045);
  box-shadow: inset 0 0 0 1px rgba(20,16,13,.07);
  padding: var(--stage-pad);
  display: grid; grid-template-columns: auto minmax(0, 1fr);
  column-gap: clamp(2rem, 5vw, 4.5rem); row-gap: clamp(1.5rem, 3vw, 2.25rem);
  align-items: start;
}

.rev__score { text-align: center; }
.rev__num { font-family: var(--ff-d); font-weight: 600; letter-spacing: -.05em; line-height: .85;
            font-size: clamp(3.2rem, 6.5vw, 5.5rem); color: var(--coal); }
.rev__score .rev__stars { margin-top: .55rem; color: var(--forest); letter-spacing: .2em; font-size: .9rem; }
.rev__count { margin-top: .45rem; font-size: .76rem; color: var(--slate); }

.rev__list { display: grid; gap: clamp(1.25rem, 2.5vw, 1.75rem); }
.rev__item blockquote { margin: 0; }
.rev__item p { font-size: 1rem; line-height: 1.55; color: var(--slate); }
.rev__item footer { margin-top: .6rem; font-size: .88rem; font-weight: 600; color: var(--coal); }

/* The lead voice. Display scale, and a rule under it separating it from the
   two that corroborate — one line, doing the only job a line gets here. */
/* An oversized quote mark behind the lead voice, in the paper's own ink at
   almost nothing — it gives the display quote something to sit in instead of
   floating on an empty surface, and at 7% it is texture, not an emoji. */
.rev__item--lead { position: relative; }
.rev__item--lead::before {
  content: '\201C'; position: absolute; z-index: 0; pointer-events: none;
  left: -.06em; top: -.34em;
  font-family: var(--ff-d); font-weight: 600; line-height: 1;
  font-size: clamp(6rem, 13vw, 11rem);
  color: var(--coal); opacity: .07;
}
.rev__item--lead blockquote { position: relative; z-index: 1; }
.rev__item--lead p { font-family: var(--ff-d); font-weight: 500; letter-spacing: -.026em;
                     line-height: 1.2; font-size: clamp(1.3rem, 2.6vw, 2.15rem);
                     color: var(--coal); text-wrap: balance; max-width: 26ch; }
.rev__item--lead { padding-bottom: clamp(1.25rem, 2.5vw, 1.75rem);
                   border-bottom: 1px solid rgba(20,16,13,.1); }
.rev__item--lead footer { margin-top: 1rem; font-size: .95rem; }

/* The two supporting quotes share a row on a wide screen and stack below it. */
.rev__list { grid-template-columns: 1fr; }
@media (min-width: 900px) {
  .rev__list { grid-template-columns: 1fr 1fr; }
  .rev__item--lead { grid-column: 1 / -1; }
}

.rev__areas { margin-top: clamp(3rem, 6vw, 4.5rem); display: flex; flex-wrap: wrap;
              gap: .6rem 1.6rem; align-items: baseline; }
.rev__areas ul { display: flex; flex-wrap: wrap; gap: .2rem .9rem; }
.rev__areas a { position: relative; font-size: .9rem; text-decoration: none; padding: .35rem 0; }
.rev__areas a::after { content: ''; position: absolute; left: 0; right: 0; bottom: .15rem; height: 1px;
                       background: currentColor; opacity: .22; transition: opacity .3s var(--ease); }
.rev__areas a:hover::after { opacity: .8; }

@media (max-width: 860px) {
  .rev__stage { grid-template-columns: 1fr; }
  .rev__score { text-align: left; }

  /* A SWIPE TRACK ON A PHONE. Three quotes stacked is a lot of vertical page
     for something a reader skims, and swiping is the gesture people already
     expect from testimonials. All three are still there — this changes the
     axis, not what is on the page.

     The track cancels the stage's own padding and puts it back as
     scroll-padding, so it bleeds to both edges of the surface while the first
     card still starts on the text margin and the last one can still reach it.
     The trailing spacer does the same job at the end, which padding-right
     alone does not do in every engine. */
  .rev__list {
    display: flex; gap: .8rem; align-items: stretch;
    overflow-x: auto; overscroll-behavior-x: contain;
    scroll-snap-type: x mandatory;
    margin-inline: calc(var(--stage-pad) * -1);
    padding-inline: var(--stage-pad);
    scroll-padding-inline: var(--stage-pad);
    padding-block: .25rem .5rem;
    scrollbar-width: none;
  }
  .rev__list::-webkit-scrollbar { display: none; }
  .rev__list::after { content: ''; flex: 0 0 calc(var(--stage-pad) - .8rem); }

  .rev__item {
    flex: 0 0 80%; scroll-snap-align: start;
    border-radius: var(--r-m);
    padding: clamp(1rem, 4vw, 1.4rem);
    background: rgba(20,16,13,.05);
    box-shadow: inset 0 0 0 1px rgba(20,16,13,.06);
  }
  /* In a row of equal cards the lead no longer needs display size to lead —
     it is simply first. Matching the three keeps the track reading as one set. */
  .rev__item--lead { border-bottom: 0; padding-bottom: clamp(1rem, 4vw, 1.4rem); }
  .rev__item--lead::before { display: none; }   /* no room for it in a swipe card */
  .rev__item--lead p { font-family: var(--ff-b); font-weight: 400; letter-spacing: 0;
                       font-size: 1rem; line-height: 1.55; max-width: none; }
  .rev__item--lead footer { margin-top: .6rem; font-size: .88rem; }
}

/* ============================ 08 · ESTIMATE ============================== */
/* One large forest-tinted surface that the whole step sequence happens inside.
   No boxed form, no outlined inputs in a stack — the surface is the module. */
.est { padding-inline: var(--inset); }
.est__surface {
  position: relative; overflow: hidden; border-radius: var(--r-xl);
  background:
    radial-gradient(90% 120% at 100% 0%, rgba(20,80,55,.55) 0%, rgba(12,31,23,0) 58%),
    var(--s-3);
  box-shadow: var(--sh-lift), var(--edge);
  padding: clamp(2rem, 5vw, 4.5rem);
}
.est__grid { display: grid; grid-template-columns: 1fr 1.05fr; gap: clamp(2rem, 5vw, 4.5rem); align-items: start; }
.est__tel { display: inline-block; margin-top: .3rem; text-decoration: none;
            font-family: var(--ff-d); font-weight: 600; letter-spacing: -.026em;
            font-size: clamp(1.5rem, 2.6vw, 2.1rem); }

.est__bar { display: flex; gap: .4rem; margin-bottom: 1.8rem; }
.est__bar i { flex: 1; height: 2px; border-radius: var(--r-pill); background: rgba(243,241,237,.14);
              transition: background .5s var(--ease); }
.est__bar i.is-on { background: var(--volt); }
.step { display: none; border: 0; padding: 0; margin: 0; }
.step.is-on { display: block; animation: stepIn .5s var(--ease-s) both; }
@keyframes stepIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
.step legend { padding: 0; margin-bottom: 1.2rem; font-family: var(--ff-d); font-weight: 600;
               font-size: clamp(1.15rem, 1.8vw, 1.4rem); letter-spacing: -.02em; }

.opts { display: grid; grid-template-columns: repeat(2, 1fr); gap: .55rem; }
.opt { position: relative; display: block; }
.opt input { position: absolute; opacity: 0; width: 0; height: 0; }
.opt span {
  display: block; padding: .85rem 1rem; font-size: .9rem; cursor: pointer;
  border-radius: var(--r-s); background: rgba(243,241,237,.05);
  box-shadow: inset 0 0 0 1px rgba(243,241,237,.07);
  transition: background .3s var(--ease), box-shadow .3s var(--ease), transform .3s var(--ease);
}
.opt:hover span { background: rgba(243,241,237,.09); transform: translateY(-1px); }
/* A chosen answer should feel pressed, not merely tinted: it sits down into
   the surface with a real inner shadow, keeps the volt edge, and takes a tick
   drawn in the pseudo-element so no glyph or icon file is involved. */
.opt input:checked + span {
  background: rgba(var(--volt-rgb),.16);
  box-shadow: inset 0 0 0 1px rgba(var(--volt-rgb),.6), inset 0 2px 6px rgba(0,0,0,.28);
  color: var(--bone); padding-right: 2.3rem;
}
.opt span { position: relative; transition: background .25s var(--ease), box-shadow .25s var(--ease),
                                            color .25s var(--ease), padding-right .3s var(--ease-s); }
.opt input:checked + span::after {
  content: ''; position: absolute; right: 1.05rem; top: 50%;
  width: 8px; height: 13px; margin-top: -8px;
  border: 2px solid var(--volt); border-top: 0; border-left: 0;
  transform: rotate(42deg);
}
@media (prefers-reduced-motion: reduce) { .opt span { transition: none; } }

/* Form messages. One class for both jobs — "you have missed something" and
   "that did not send" — because they are the same thing to a visitor: the form
   telling them why it has not moved. Amber rather than red for the validation
   case, which is a nudge; red is reserved for the send actually failing. */
.fnote {
  margin: 0 0 1rem; font-size: .86rem; line-height: 1.45;
  color: #E9C784;
}
.fnote--bad { margin: 1rem 0 0; color: #E8A0A0; }

.opt input:focus-visible + span { outline: 2px solid var(--volt); outline-offset: 2px; }

.field { margin-bottom: .9rem; }
.field label { display: block; margin-bottom: .45rem; font-size: .78rem; color: var(--stone); }
/* Inset, not outlined. The surface the input sits in is darker than its
   parent, which reads as a recess without drawing a rectangle around it. */
.field input {
  width: 100%; padding: .95rem 1.05rem; font: inherit; font-size: .95rem; color: var(--bone);
  background: rgba(6,10,8,.45); border: 0; border-radius: var(--r-s);
  box-shadow: inset 0 0 0 1px rgba(243,241,237,.08);
  transition: box-shadow .3s var(--ease), background .3s var(--ease);
}
.field input::placeholder { color: rgba(167,160,147,.6); }
.field input:focus { outline: none; background: rgba(6,10,8,.6);
                     box-shadow: inset 0 0 0 1px rgba(var(--volt-rgb),.6); }
.note { margin-top: 1.2rem; font-size: .77rem; line-height: 1.55; color: rgba(167,160,147,.85); }
.note a { color: inherit; }
.fnav { margin-top: 1.6rem; display: flex; align-items: center; justify-content: space-between; gap: 1rem; }
.fnav:has(.act:only-child) { justify-content: flex-end; }
.done { display: none; }
.done.is-on { display: block; animation: stepIn .5s var(--ease-s) both; }
.done h3 { margin-bottom: .5rem; font-family: var(--ff-d); font-weight: 600; font-size: 1.5rem; letter-spacing: -.026em; }


/* The summary that builds beside the questions. Each line arrives on its own
   rule, indented off a hairline, so the block reads as a docket being filled
   in rather than as a list appearing. */
.est__sum { margin-top: clamp(2rem, 4vw, 2.8rem); }
.est__sum ul { margin-top: .8rem; display: grid; gap: .1rem;
               border-left: 1px solid rgba(var(--volt-rgb),.4); padding-left: 1rem; }
.est__sum li {
  font-family: var(--ff-d); font-weight: 500; letter-spacing: -.016em;
  font-size: clamp(1rem, 1.5vw, 1.2rem); color: var(--bone);
  min-height: 0; height: 0; overflow: hidden; opacity: 0;
  transform: translateY(-.25rem);
  transition: height .45s var(--ease-s), opacity .4s var(--ease),
              transform .45s var(--ease-s);
}
.est__sum li.is-on { height: 1.85em; opacity: 1; transform: none; }
@media (prefers-reduced-motion: reduce) {
  .est__sum li { transition: none; }
}

/* ============================ 09 · FOOTER ================================ */
/* No top rule. The footer separates from the estimate by space and by the
   estimate's own rounded surface ending above it. */
/* THE LAST DETAIL. The footer was the one part of the page with no surface of
   its own — links on flat black, after eight scenes that all had one. It now
   carries the site's own contour, and the name is set once, very large and
   very quiet, across the bottom: the letters are ground rather than content,
   at 3.5% of the bone, clipped by the footer's own box. It is the architectural
   detail at the end of the building, not another headline. */
.foot { position: relative; isolation: isolate; overflow: hidden;
        padding-inline: var(--gut); padding-block: clamp(3.5rem, 7vw, 5.5rem) 0;
        background:
          repeating-radial-gradient(ellipse 60% 50% at 92% 8%,
            rgba(203,214,207,0) 0 78px, rgba(203,214,207,.016) 78px 79px),
          radial-gradient(80% 70% at 8% 0%, rgba(24,32,29,.7) 0%, rgba(8,10,9,0) 68%),
          var(--s-0); }
.foot::before {
  content: 'DZT'; position: absolute; z-index: -1; pointer-events: none;
  left: 50%; bottom: -.3em; transform: translateX(-50%);
  font-family: var(--ff-d); font-weight: 600; letter-spacing: -.05em; line-height: .78;
  font-size: min(34vw, 30rem);
  color: var(--bone); opacity: .035; white-space: nowrap;
}
@media (max-width: 700px) { .foot::before { font-size: 42vw; bottom: -.22em; } }
.foot__top { max-width: var(--wrap); margin-inline: auto;
             display: grid; grid-template-columns: 1.8fr repeat(3, 1fr); gap: clamp(2rem, 4vw, 3.5rem); }
/* THE FOOTER LOCKUP. The roundel was 58px and unlabelled — the mark alone,
   doing brand work at favicon size in the one place a visitor looks for who
   they have been reading. It is now 96px with the name set beside it.

   The mark is untouched: this adds a wordmark next to it, it does not modify
   the logo. "& Design" drops to a second line at a smaller size so the lockup
   reads as one object rather than a long single line, and so "DZT Landscaping"
   — the name people actually say — carries the weight. */
.foot__brand { display: inline-flex; align-items: center; gap: clamp(.8rem, 1.6vw, 1.1rem);
               text-decoration: none; margin-bottom: 1.15rem; }
.foot__brand img { width: clamp(72px, 7vw, 96px); height: auto; flex: 0 0 auto; }
.foot__wordmark { display: grid; font-family: var(--ff-d); font-weight: 600;
                  font-size: clamp(1.15rem, 1.9vw, 1.5rem); letter-spacing: -.022em;
                  line-height: 1.05; color: var(--bone); }
.foot__wordmark em { font-style: normal; font-weight: 500; font-size: .66em;
                     letter-spacing: -.01em; color: var(--stone); margin-top: .12em; }

/* A label, not a smaller link. Same treatment at every width — the mobile
   pass had made these uppercase and left desktop in sentence case, so the two
   breakpoints disagreed about what a footer heading is. */
.foot h4 { margin-bottom: .9rem; font-size: .72rem; font-weight: 600; letter-spacing: .08em;
           text-transform: uppercase; color: rgba(167,160,147,.75); }
.foot ul a { font-size: .92rem; text-decoration: none; color: rgba(243,241,237,.78);
             transition: color .25s var(--ease); }
.foot__blurb { max-width: 32ch; }
.foot ul a:hover { color: var(--bone); }
.foot li + li { margin-top: .5rem; }
.foot__tel { position: relative; display: inline-block;
              font-family: var(--ff-d); font-weight: 600; letter-spacing: -.026em;
              font-size: clamp(1.35rem, 2.1vw, 1.75rem); text-decoration: none;
              transition: color .3s var(--ease); }
.foot__tel::after { content: ''; position: absolute; left: 0; right: 0; bottom: -.1em; height: 1px;
                    background: var(--volt); opacity: 0; transform: scaleX(.4); transform-origin: left;
                    transition: opacity .3s var(--ease), transform .45s var(--ease-s); }
.foot__tel:hover { color: var(--volt); }
.foot__tel:hover::after { opacity: .7; transform: scaleX(1); }

/* Footer links get the same growing rule as every other link on the site,
   instead of being the one place where a hover is a colour change alone. */
.foot ul a { position: relative; display: inline-block; padding-block: .1rem; }
.foot ul a::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 1px;
                    background: currentColor; opacity: 0; transform: scaleX(.3); transform-origin: left;
                    transition: opacity .28s var(--ease), transform .4s var(--ease-s); }
.foot ul a:hover::after { opacity: .5; transform: scaleX(1); }
.foot__bar { max-width: var(--wrap); margin-inline: auto; margin-top: clamp(2.5rem, 5vw, 4rem);
             padding-block: 1.5rem; display: flex; flex-wrap: wrap; justify-content: space-between;
             gap: .5rem 1.5rem; font-size: .8rem; color: rgba(167,160,147,.8); }
.foot__bar a { color: inherit; text-decoration: none; }
.foot__bar a:hover { color: var(--bone); }

/* ========================= THE ASSISTANT ================================
   The "Ask about your yard" bubble, restored. It was on Modern 1.0 at ae0a701
   and my rebuild of index.html dropped it; it stayed dropped for six commits
   because nothing on the page referenced it, so nothing looked broken.

   RESTORING THE SCRIPT ALONE WOULD NOT HAVE WORKED. assistant.js builds its
   own DOM and appends it to <body>, but all 189 of its style rules live in
   assets/css/styles.css — the shipping site's cascade, which this page
   deliberately does not load. The script would have mounted an unstyled stack
   of divs in the corner. So the widget is re-dressed here in this design's own
   terms: the surface ladder, the radius scale, the same blur and environmental
   shadow as the masthead and the proof rail. It should look like part of the
   page rather than a bolted-on product.

   Classes are dictated by the script, not chosen here — all 44 of them. The
   markup is not ours to change, so the names stay ugly and the comments carry
   the intent.

   THE FAB IS ONE OF THE FEW THINGS ON THIS PAGE THAT IS GENUINELY A PILL, so
   it gets --r-pill honestly rather than as decoration.
   ===================================================================== */
/* column-REVERSE, so the launcher stays where the cursor left it. The script
   emits the button before the panel, so a plain column put the button on top
   of the stack and the panel below it — opening the assistant threw the thing
   you just clicked 600px up the screen. Reversing keeps the button pinned to
   the corner and grows the panel upward out of it, which is what every one of
   these is expected to do. */
.ai { position: fixed; z-index: 120; right: var(--inset); bottom: var(--inset);
      display: flex; flex-direction: column-reverse; align-items: flex-end; gap: .75rem; }

.ai__fab {
  display: inline-flex; align-items: center; gap: .6rem;
  padding: .85rem 1.25rem; border: 0; border-radius: var(--r-pill); cursor: pointer;
  font-weight: 600; font-size: .9rem; color: var(--bone);
  background: rgba(20,25,24,.82);
  -webkit-backdrop-filter: blur(18px) saturate(1.3); backdrop-filter: blur(18px) saturate(1.3);
  box-shadow: var(--sh-soft), var(--edge), 0 0 0 1px rgba(243,241,237,.08);
  transition: transform .3s var(--ease), background .3s var(--ease), box-shadow .3s var(--ease);
}
.ai__fab:hover { transform: translateY(-2px); background: rgba(26,33,31,.9);
                 box-shadow: var(--sh-lift), var(--edge), 0 0 0 1px rgba(var(--volt-rgb),.35); }
.ai__fab svg { width: 20px; height: 20px; color: var(--volt); flex: 0 0 auto; }
.ai__fab-label { white-space: nowrap; }

.ai__panel {
  width: min(390px, calc(100vw - 2 * var(--inset)));
  max-height: min(660px, calc(100svh - 7rem));
  display: flex; flex-direction: column; overflow: hidden;
  border-radius: var(--r-xl);
  background: rgba(18,23,22,.94);
  -webkit-backdrop-filter: blur(26px) saturate(1.3); backdrop-filter: blur(26px) saturate(1.3);
  box-shadow: var(--sh-lift), var(--edge), 0 0 0 1px rgba(243,241,237,.07);
}
/* The script toggles the `hidden` attribute, so the open state must not fight
   it — display goes on :not([hidden]) rather than on .is-open. */
.ai__panel[hidden] { display: none; }

.ai__head { display: flex; align-items: flex-start; gap: 1rem;
            padding: 1.25rem 1.35rem 1rem; }
.ai__head > div { margin-right: auto; }
.ai__title { font-family: var(--ff-d); font-weight: 600; font-size: 1.02rem; letter-spacing: -.015em; }
.ai__sub { margin-top: .2rem; font-size: .8rem; line-height: 1.45; color: var(--stone); }
.ai__clear, .ai__close {
  flex: 0 0 auto; background: none; border: 0; cursor: pointer; color: var(--stone);
  font-size: .78rem; padding: .45rem .55rem; border-radius: var(--r-s);
  transition: color .2s var(--ease), background .2s var(--ease);
}
.ai__close { font-size: 1.2rem; line-height: 1; }
.ai__clear:hover, .ai__close:hover { color: var(--bone); background: rgba(243,241,237,.07); }

.ai__log { flex: 1 1 auto; min-width: 0; overflow-y: auto; overscroll-behavior: contain;
           padding: .25rem 1.35rem 1rem; display: flex; flex-direction: column; gap: .7rem; }
/* A backstop: the script can emit arbitrary content into the transcript, and
   one long unbroken string should wrap rather than widen the panel.

   `break-word`, NOT `anywhere`, and the difference is not cosmetic. `anywhere`
   is counted when the browser computes min-content width, so a flex item
   carrying it can be shrunk to the width of a single character. Applied to the
   hand-off row it squeezed the Call and Text buttons to about 35px and stacked
   their labels one letter per line — C / a / l / l. `break-word` wraps the same
   long strings without ever reporting a min-content width that small.

   Scoped to the prose containers too: the button row never needed it. */
.ai__log, .ai__msg { overflow-wrap: break-word; }
.ai__w { display: contents; }

.ai__msg { max-width: 88%; padding: .7rem .9rem; border-radius: var(--r-m);
           font-size: .9rem; line-height: 1.5; }
/* Visitor messages read as the accent; the assistant's read as surface. The
   green is this page's single action colour, so it marks the human's side. */
.ai__msg { align-self: flex-end; background: rgba(var(--volt-rgb),.16);
           box-shadow: inset 0 0 0 1px rgba(var(--volt-rgb),.28); }
.ai__msg--bot, .ai__msg--greet { align-self: flex-start; background: rgba(243,241,237,.055);
                                 box-shadow: inset 0 0 0 1px rgba(243,241,237,.07); }
.ai__msg--wait { color: var(--stone); }
.ai__msg--arriving { animation: aiIn .38s var(--ease-s) both; }
@keyframes aiIn { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
.ai__msg--grow { transition: max-height .4s var(--ease-s); }
.ai__msg p + p { margin-top: .55rem; }
.ai__msg a { color: var(--volt); }

.ai__dots { display: inline-flex; gap: 4px; }
.ai__dots span { width: 5px; height: 5px; border-radius: var(--r-pill); background: var(--stone);
                 animation: aiDot 1.1s infinite ease-in-out; }
.ai__dots span:nth-child(2) { animation-delay: .15s; }
.ai__dots span:nth-child(3) { animation-delay: .3s; }
@keyframes aiDot { 0%,60%,100% { opacity: .3; transform: none; } 30% { opacity: 1; transform: translateY(-3px); } }

.ai__lead { font-size: .9rem; line-height: 1.5; }
.ai__lead--clamped { display: -webkit-box; -webkit-line-clamp: 4; -webkit-box-orient: vertical; overflow: hidden; }
.ai__more { background: none; border: 0; cursor: pointer; color: var(--volt);
            font-size: .82rem; font-weight: 600; padding: .3rem 0; }
.ai__more-p { margin-top: .4rem; }

/* Suggestion chips — pill-shaped controls, so --r-pill is honest here too. */
.ai__chips { display: flex; flex-wrap: wrap; gap: .4rem; padding: 0 1.35rem .9rem; }
.ai__chip { background: rgba(243,241,237,.05); border: 0; cursor: pointer; color: var(--bone);
            font-size: .8rem; padding: .5rem .8rem; border-radius: var(--r-pill);
            box-shadow: inset 0 0 0 1px rgba(243,241,237,.09);
            transition: background .2s var(--ease), box-shadow .2s var(--ease); }
.ai__chip:hover { background: rgba(243,241,237,.1); box-shadow: inset 0 0 0 1px rgba(var(--volt-rgb),.4); }

.ai__form { display: flex; gap: .5rem; padding: .9rem 1.35rem;
            box-shadow: inset 0 1px 0 rgba(243,241,237,.07); }
.ai__input { flex: 1 1 auto; min-width: 0; padding: .8rem .9rem; font: inherit; font-size: .9rem;
             color: var(--bone); background: rgba(6,10,8,.5); border: 0; border-radius: var(--r-s);
             box-shadow: inset 0 0 0 1px rgba(243,241,237,.09);
             transition: box-shadow .25s var(--ease); }
.ai__input::placeholder { color: rgba(167,160,147,.6); }
.ai__input:focus { outline: none; box-shadow: inset 0 0 0 1px rgba(var(--volt-rgb),.6); }
.ai__send { flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center;
            width: 42px; height: 42px; border: 0; border-radius: var(--r-s); cursor: pointer;
            background: var(--volt); color: #06210F;
            transition: background .25s var(--ease), transform .25s var(--ease); }
.ai__send:hover { background: var(--volt-hi); transform: translateY(-1px); }
.ai__send svg { width: 18px; height: 18px; }
.ai__send[disabled] { opacity: .45; cursor: default; transform: none; }

/* The hand-off block: when the assistant reaches the end of what it can
   answer, it offers the phone. Treated as a real action, not a footnote. */
.ai__cta, .ai__contact { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem;
                         margin-top: .6rem; }
.ai__cta-num, .ai__contact-num { font-family: var(--ff-d); font-weight: 600; font-size: 1.05rem;
                                 letter-spacing: -.02em; text-decoration: none; }
.ai__contact-btn { display: inline-flex; align-items: center; gap: .45rem; text-decoration: none;
                   white-space: nowrap; flex: 0 0 auto;
                   font-size: .84rem; font-weight: 600; padding: .6rem .95rem; border-radius: var(--r-s);
                   background: rgba(243,241,237,.06); color: var(--bone);
                   box-shadow: inset 0 0 0 1px rgba(243,241,237,.1);
                   transition: background .2s var(--ease); }
.ai__contact-btn:hover { background: rgba(243,241,237,.11); }
.ai__contact-btn svg { width: 16px; height: 16px; flex: 0 0 auto; }
.ai__contact-btn--call { background: var(--volt); color: #06210F; box-shadow: none; }
.ai__contact-btn--call:hover { background: var(--volt-hi); }

/* ---- THE OPEN PANEL WAS FIVE STRANGERS IN A COLUMN -----------------------
   .ai__log is a flex column with ONE uniform .7rem gap, and on open the script
   drops five siblings straight into it at the same level: the greeting bubble,
   the Call/Text row, the phone number, the callback card and the type hint.
   Every gap equal means no gap means anything — nothing is grouped to anything,
   so the eye has to work out what each row is five separate times. That is the
   clutter. Not one bad element: no relationships between any of them.

   Nothing is removed here. The parts are grouped and re-weighted.

   THE THREE GREENS WERE THE OTHER HALF. Call (a small pill), "Call me" (a
   full-width slab) and the send arrow all carried --volt, which leaves the
   panel with three primaries and therefore none. Worse, the loudest of them —
   a full-width green bar, the single heaviest thing in the panel — belonged to
   the SECONDARY path (leave your details and wait), while dialling now, which
   is the owner's stated priority, was the small one. The hierarchy was
   inverted. Now: one filled green per zone. Call is it above the hairline; the
   send arrow is it below.

   The script owns all 44 class names and the DOM order, so the grouping is done
   with weight, width and spacing rather than by wrapping anything. */

/* Call and Text split the row. Two equal halves read as one control with two
   options; two differently-sized pills against a ragged right edge read as two
   things that happen to be adjacent. Also what "big call or text buttons"
   actually looks like at 390px. */
.ai__contact { gap: .5rem; margin-top: .2rem; flex-wrap: nowrap; }
.ai__contact .ai__contact-btn { flex: 1 1 0; justify-content: center;
                                padding: .82rem .9rem; font-size: .9rem; }

/* THE NUMBER IS THE PAIR'S CAPTION, not a fifth row. It sat a full gap below
   the buttons in the display face at 1.05rem — within a hair of the panel
   title's 1.02rem, so the two competed — and left-aligned under a ragged row it
   read as an orphan line. Pulled tight under the buttons and centred on them,
   it belongs to them. Tabular figures because it is a number to be read aloud
   or written down, and the -.02em tracking that suits a headline crowds digits. */
.ai__contact-num { margin-top: -.25rem; text-align: center;
                   font-size: .96rem; letter-spacing: 0;
                   font-variant-numeric: tabular-nums; color: var(--bone); }

/* The callback form inside the transcript. */
/* `min-width: 0` on BOTH the row and its inputs, and a zero flex-basis.
   An <input> carries an intrinsic width from its size attribute — about 177px
   each — so two of them plus the gap wanted 362px inside a 347px column, and
   neither `flex-shrink` nor `min-width: 0` on the inputs alone was enough
   while the row itself was a grid item free to overflow its track. Measured at
   379px in a 347px parent before this. */
/* ONE OBJECT, NOT FOUR STACKED STRANGERS. The heading, the two fields, the
   button and the note are a single form, and in a flat column with one uniform
   gap they were four peers of everything else in the transcript. A recessed
   surface and a hairline is the whole fix: it says "this is a form, it starts
   here and ends here", and it is the same recess the inputs already sit on so
   the card and its fields read as one material.

   It also has to survive being MOVED — the script relocates this card to the
   end of the log as the conversation grows — so the grouping is the card's own
   box and nothing about its neighbours. */
.ai__cb { margin-top: .85rem; display: grid; gap: .55rem;
          padding: .9rem .9rem .85rem; border-radius: var(--r-m);
          background: rgba(6,10,8,.34);
          box-shadow: inset 0 0 0 1px rgba(243,241,237,.075); }
/* The <h4> had no rule at all, so it rendered at the browser default — bold,
   1em, with block margins — which put a second heading the same size as "Ask
   about your yard" a third of the way down the panel. It is a question put to
   the visitor, so it stays sentence case in the body face; it just stops
   outranking the panel's own title. */
.ai__cb h4 { margin: 0; font-family: var(--ff-b); font-size: .86rem; font-weight: 600;
             letter-spacing: 0; color: var(--bone); }
.ai__cb-row { display: flex; gap: .5rem; min-width: 0; }
.ai__cb-row input { flex: 1 1 0; min-width: 0; padding: .7rem .8rem; font: inherit; font-size: .88rem;
                    color: var(--bone); background: rgba(6,10,8,.5); border: 0; border-radius: var(--r-s);
                    box-shadow: inset 0 0 0 1px rgba(243,241,237,.09); }
.ai__cb-row input:focus { outline: none; box-shadow: inset 0 0 0 1px rgba(var(--volt-rgb),.6); }
/* OUTLINED, NOT FILLED — the third green comes off here. Filled, this was a
   full-width green bar and the heaviest element in the panel, which told the
   visitor that leaving a name and waiting mattered more than the Call button
   above it. Volt type on a volt hairline still reads unmistakably as the
   button of this form (it is the only coloured thing inside the card, and the
   only thing here that is not a field), while the one filled green above the
   hairline stays the panel's single primary. Measured 8.1:1 on this surface.
   Full width is kept: it mirrors the field row above it and closes the card. */
.ai__cb-go { flex: 0 0 auto; border: 0; cursor: pointer;
             background: rgba(var(--volt-rgb), .1); color: var(--volt);
             box-shadow: inset 0 0 0 1px rgba(var(--volt-rgb), .45);
             font-weight: 600; font-size: .84rem; padding: .72rem 1rem; border-radius: var(--r-s);
             transition: background .2s var(--ease), box-shadow .2s var(--ease); }
.ai__cb-go:hover { background: rgba(var(--volt-rgb), .2);
                   box-shadow: inset 0 0 0 1px rgba(var(--volt-rgb), .7); }
.ai__cb-note { font-size: .76rem; color: var(--stone); }
.ai__cb-note--ok { color: var(--volt); }

.ai__assure, .ai__src { font-size: .75rem; line-height: 1.5; color: var(--stone); }
.ai__src { margin-top: .4rem; }
.ai__disclosure { padding: 0 1.35rem 1rem; font-size: .72rem; line-height: 1.5; color: rgba(167,160,147,.8); }
.ai__disclosure a { color: inherit; }

/* Two utilities the script expects and that this page has never needed. */
.muted { color: var(--stone); }
/* OFF-SCREEN, FROM THE STYLESHEET. Both of these were hidden with an inline
   style attribute, which the CSP drops — so in production the bot trap rendered
   as a visible "Company website" label and input inside the estimate form, and
   the polite live region took up space under it. A trap a person can see is not
   a trap, and it is the first thing a visitor would have asked about. */
.hp, [data-step-live] { position: absolute; left: -9999px; }

/* SPACING UTILITIES, because inline style attributes do not survive the CSP.
   These were `style="margin-top:1.2rem"` and friends in the footer and the
   estimate header — dropped in production, so the footer phone number and the
   estimate lede lost their spacing on every page while looking right in dev.
   Five values were in use; these are those five, named rather than repeated. */
.mt-xs { margin-top: .25rem; }
.mt-s  { margin-top: .7rem; }
.mt-m  { margin-top: 1.1rem; }
.mt-l  { margin-top: 1.2rem; }
.mt-xl { margin-top: 1.8rem; }
.pt-0  { padding-top: 0; }
/* A tel: link set in the accent, inside prose. Was an inline colour, which the
   CSP drops — leaving the number styled as ordinary body text with no
   affordance that it dials. */
.tel-accent { color: var(--volt); }

.visually-hidden { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
                   overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }

@media (prefers-reduced-motion: reduce) {
  .ai__fab:hover, .ai__send:hover { transform: none; }
  .ai__msg--arriving { animation: none; }
  .ai__dots span { animation: none; opacity: .6; }
}

/* The vertical fill needs its own reduced-motion stop: the global rule pins
   scaleX(1), which leaves a scaleY(0) rail drawn at zero height — the line
   would simply never appear for anyone who asks for less motion. */
@media (max-width: 1040px) and (prefers-reduced-motion: reduce) {
  .proc__fill { transform: scaleY(1); }
}

@media (max-width: 680px) {
  /* Clear the call bar. It is fixed at the bottom on phones and the bubble
     would otherwise sit on top of the two things most likely to be tapped. */
  .ai { right: var(--inset); align-items: flex-end;
        bottom: calc(var(--inset) + 74px + env(safe-area-inset-bottom, 0px)); }
  .ai__fab-label { display: none; }
  .ai__fab { padding: .9rem; }
  .ai__panel { width: calc(100vw - 2 * var(--inset)); max-height: calc(100svh - 12rem); }
}

/* ============================================================================
   EVERYTHING BELOW "INTERIOR PAGES" HAS BEEN DELETED TWICE, by me, in two
   separate passes — .callout, .pull and .keyrail once, and the whole assistant
   stylesheet the next time. Both times the cause was the same: editing that
   section by replacing everything between its heading and the CALLBAR marker,
   which silently swallowed whatever else happened to live in the range.

   Nothing errored either time. A deleted rule does not fail; the markup simply
   renders unstyled, and "the assistant is 1440x184 at position:static in the
   bottom-left of the footer" is what that looks like from outside.

   So component blocks live ABOVE the interior-pages section now, and anything
   that edits a range should be matching on both of its delimiters rather than
   on "start here, run to the next marker".
   ========================================================================= */

/* ======================= INTERIOR PAGES ==================================
   The homepage earned surfaces, alternation and interaction. These pages had
   none of it — one prose column on one flat ground, twenty-two times, which is
   why the owner called them "static glossaries".

   Same system, same tokens, applied at page scale:
     · grounds ALTERNATE, and the paper ones are inset rounded surfaces with
       the dark floor running behind them, exactly as the project gallery is
     · a page header with a real opening rather than a label and an h1
     · blocks and rows that respond, so a long page has texture under the
       cursor instead of being a wall to read
     · tables, definition lists and callouts already had care; they keep it

   They are still typographic — no photograph appears on an interior page,
   because almost every banner on the old ones was a Pexels stand-in and all
   nine of DZT's own frames are in use on the homepage.
   ========================================================================= */

/* ---- PAGE HEADER --------------------------------------------------------
   Opens on the dark floor with the label, the h1 and the lede on a rule, so
   an interior page starts with a composed moment rather than a heading. */
.phead { position: relative; padding-block: calc(var(--nav-tall) + clamp(2rem, 5vw, 4rem)) clamp(2.5rem, 5vw, 4rem); }
.phead .label { margin-bottom: 1.1rem; }
.phead h1 { max-width: 20ch; text-wrap: balance; }
.phead .lede { margin-top: 1.4rem; }
/* A hairline that stops short of the gutter — a rule doing composition rather
   than fencing the section off. */
.phead::after { content: ''; position: absolute; left: var(--gut); bottom: 0;
                width: min(9rem, 28%); height: 1px; background: var(--volt); opacity: .55; }

/* ---- ALTERNATING GROUNDS ------------------------------------------------
   The paper bands become inset rounded surfaces so the dark floor runs behind
   them and the page stops reading as stacked bands. Consecutive paper sections
   collapse into ONE surface — .band + .band trims the seam — so two adjacent
   light sections do not look like two cards touching. */
.band.on-paper { margin-inline: var(--inset); border-radius: var(--r-xl); }
.band.on-paper + .band.on-paper { margin-top: calc(var(--inset) * -1 - 1px);
                                  border-radius: 0 0 var(--r-xl) var(--r-xl); }
.band.on-paper:has(+ .band.on-paper) { border-radius: var(--r-xl) var(--r-xl) 0 0; }

/* ---- PROSE --------------------------------------------------------------- */
/* One measure for running text, a wider one for tables: capping .prose itself
   also capped its tables, and a nine-column spec table squeezed into a
   68-character measure scrolls sideways to say something that would have fit. */
.prose { max-width: none; }
.prose > p, .prose > h2, .prose > h3, .prose > h4,
.prose > ul, .prose > ol, .prose > dl, .prose > .meta { max-width: 68ch; }
.prose > .tablewrap { max-width: 64rem; }
.prose > * + * { margin-top: 1.1rem; }
.prose h2 { font-family: var(--ff-d); font-weight: 600; letter-spacing: -.024em; line-height: 1.12;
            font-size: clamp(1.5rem, 2.6vw, 2.1rem); margin-top: clamp(2.5rem, 5vw, 3.5rem); }
.prose h3 { font-family: var(--ff-d); font-weight: 600; letter-spacing: -.018em;
            font-size: clamp(1.1rem, 1.7vw, 1.3rem); margin-top: clamp(1.8rem, 3vw, 2.4rem); }
.prose p  { color: var(--stone); }
.on-paper .prose p { color: var(--slate); }
.prose a { color: inherit; text-underline-offset: 3px; }
.prose ul { margin-top: 1rem; display: grid; gap: .55rem; }
.prose ul li { position: relative; padding-left: 1.1rem; color: var(--stone); }
.on-paper .prose ul li { color: var(--slate); }
.prose ul li::before { content: ''; position: absolute; left: 0; top: .62em; width: 5px; height: 5px;
                       border-radius: var(--r-pill); background: var(--volt); opacity: .8; }
.prose strong, .prose b { color: var(--bone); font-weight: 600; }
.on-paper .prose strong, .on-paper .prose b { color: var(--coal); }

/* ---- BLOCK COLUMNS ------------------------------------------------------
   Each block gets a surface and lifts under the cursor. This is where a long
   capability page stops being a list and starts being scannable. */
.pcols { display: grid; grid-template-columns: repeat(2, minmax(0,1fr));
         gap: clamp(1rem, 2vw, 1.5rem); margin-top: 1.4rem; }
.pcols > div {
  padding: clamp(1.1rem, 2.2vw, 1.6rem); border-radius: var(--r-m);
  background: rgba(243,241,237,.035); box-shadow: inset 0 0 0 1px rgba(243,241,237,.055);
  transition: transform .4s var(--ease-s), background .35s var(--ease), box-shadow .35s var(--ease);
}
.on-paper .pcols > div { background: rgba(20,16,13,.035); box-shadow: inset 0 0 0 1px rgba(20,16,13,.06); }
.pcols > div:hover { transform: translateY(-3px); background: rgba(243,241,237,.06);
                     box-shadow: inset 0 0 0 1px rgba(var(--volt-rgb),.3), var(--sh-soft); }
.on-paper .pcols > div:hover { background: rgba(20,16,13,.055);
                               box-shadow: inset 0 0 0 1px rgba(14,77,51,.28), var(--sh-soft); }
.pcols > div > h3 { margin-top: 0; }
.pcols > div > p { margin-top: .5rem; color: var(--stone); font-size: .95rem; line-height: 1.58; }
.on-paper .pcols > div > p { color: var(--slate); }
.pcols > div > ul { margin-top: .7rem; }

/* ---- DEFINITION LISTS ----------------------------------------------------
   The FAQ and spec pairs. A row that responds, and a marker that grows on the
   term rather than a border around the pair. */
.prose dl { margin-top: 1.4rem; max-width: 68ch; }
.prose dt { position: relative; font-family: var(--ff-d); font-weight: 600; font-size: 1.02rem;
            letter-spacing: -.015em; color: var(--bone); margin-top: 1.6rem; padding-left: 1rem;
            transition: color .3s var(--ease); }
.on-paper .prose dt { color: var(--coal); }
.prose dt::before { content: ''; position: absolute; left: 0; top: .25em; bottom: .25em; width: 2px;
                    border-radius: var(--r-pill); background: var(--volt); opacity: .45;
                    transition: opacity .3s var(--ease), transform .35s var(--ease-s); }
.prose dt:hover::before { opacity: 1; transform: scaleY(1.12); }
.prose dd { margin: .4rem 0 0 1rem; color: var(--stone); }
.on-paper .prose dd { color: var(--slate); }

/* ---- TABLES ------------------------------------------------------------- */
.tablewrap { margin-top: 1.6rem; overflow-x: auto; border-radius: var(--r-m);
             background: var(--s-1); box-shadow: var(--edge); -webkit-overflow-scrolling: touch; }
.on-paper .tablewrap { background: rgba(20,16,13,.035); box-shadow: inset 0 0 0 1px rgba(20,16,13,.06); }
.prose table { width: 100%; border-collapse: collapse; font-size: .9rem; min-width: 32rem; }
.prose caption { text-align: left; padding: 1rem 1.15rem .25rem; font-size: .8rem; color: var(--stone); }
/* --stone is the dark-ground tone and lands at 1.9:1 on tan. Every caption in
   the specs lives on a plate, so all 14 of them were effectively unreadable —
   the cells underneath already had this rule, the caption above them did not. */
.on-paper .prose caption { color: var(--slate); }
.prose th { text-align: left; font-weight: 600; color: var(--bone); padding: .95rem 1.15rem;
            background: rgba(243,241,237,.045); white-space: nowrap; }
.on-paper .prose th { color: var(--coal); background: rgba(20,16,13,.05); }
.prose td { padding: .9rem 1.15rem; color: var(--stone); vertical-align: top; }
.on-paper .prose td { color: var(--slate); }
.prose tbody tr { transition: background .25s var(--ease); }
.prose tbody tr:hover { background: rgba(243,241,237,.03); }
.on-paper .prose tbody tr:hover { background: rgba(20,16,13,.03); }
.prose tbody tr + tr th, .prose tbody tr + tr td { border-top: 1px solid rgba(243,241,237,.07); }
.on-paper .prose tbody tr + tr th, .on-paper .prose tbody tr + tr td { border-top-color: rgba(20,16,13,.09); }
.prose table strong, .prose table b { color: var(--bone); }
.on-paper .prose table strong { color: var(--coal); }

/* Scope words keep the homepage's dot-separated idiom — which means undoing
   the generic prose-list treatment AND putting the separator back. `content:
   none` on .meta li::before killed the bullet and the ' · ' with it, and the
   li's inline display had already been overridden to list-item, so on three
   interior pages the scope terms stacked one per line with nothing between
   them. This restores the homepage idiom exactly: inline, dot-separated. */
.prose .meta { margin-top: .9rem; }
.prose .meta li { display: inline; padding-left: 0; }
.prose .meta li::before { content: none; }
.prose .meta li + li::before {
  content: ' · '; position: static; width: auto; height: auto;
  background: none; border-radius: 0; opacity: .55;
}

/* ---- PULL QUOTE ---------------------------------------------------------
   One per page at most, and only over a sentence the source already has. A way
   of SETTING an existing line, never a reason to write one. */
.pull { margin: clamp(2rem, 4vw, 3rem) 0; padding-left: clamp(1.1rem, 2vw, 1.6rem);
        border-left: 2px solid var(--volt); max-width: 40ch; }
.pull p { font-family: var(--ff-d); font-weight: 500; letter-spacing: -.02em; line-height: 1.24;
          font-size: clamp(1.25rem, 2.2vw, 1.75rem); color: var(--bone); text-wrap: balance; }
.on-paper .pull p { color: var(--coal); }

/* ---- CALLOUT: THREE ROLES, THREE TREATMENTS ----------------------------
   These were all one thing — amber, urgent, alarm-shaped — thirteen times
   across the site. The copy is the owner's own and it is good; the design was
   telling the reader to be afraid of it. "Anyone who quotes your yard without
   talking about rock either has not looked or intends to raise the number
   later" is a sales argument and it was in a hazard box. So was "you get a
   weekly update on where it stands".

   A callout inherits its urgency from what it SAYS, so the variants are by
   role rather than by how much attention a section wants:

     --point  expertise. The line a good contractor would say out loud and a
              bad one would not. Green rule, slightly larger, confident.
     --note   precedence and scope. Which document controls, what a summary is
              a summary of, what the company does today. Quiet, secondary,
              authoritative — the tone of a footnote in a spec, not a siren.
     --warn   genuine caution, and there are exactly two on the whole site:
              a page marked not-yet-contractual, and a page whose records are
              not posted yet. Amber, and it means it.
   ===================================================================== */
.callout { margin: 0 0 clamp(2rem, 4vw, 3rem); max-width: 68ch; }
.callout p + p { margin-top: .6rem; }

/* --point — an expert remark. A rule and open space rather than a box: this
   is the page being confident, and a box would make it defensive again. */
.callout--point {
  padding: .3rem 0 .3rem clamp(1.1rem, 2vw, 1.5rem);
  border-left: 2px solid var(--volt);
}
.callout--point p { font-size: 1.02rem; line-height: 1.6; color: var(--bone); }
.callout--point p:first-child { font-weight: 500; }
.on-paper .callout--point, .plate .callout--point, .idx.on-paper .callout--point { border-left-color: var(--forest); }
.on-paper .callout--point p, .plate .callout--point p, .idx.on-paper .callout--point p { color: var(--coal); }

/* --note — precedence and scope. Recessed, smaller, deliberately undramatic. */
.callout--note {
  padding: clamp(.9rem, 1.8vw, 1.2rem) clamp(1rem, 2vw, 1.4rem);
  border-radius: var(--r-m);
  background: rgba(243,241,237,.04);
  box-shadow: inset 0 0 0 1px rgba(243,241,237,.07);
}
.callout--note p { font-size: .88rem; line-height: 1.6; color: var(--stone); }
.callout--note p:first-child { color: rgba(243,241,237,.82); font-weight: 500; }
.on-paper .callout--note, .plate .callout--note, .idx.on-paper .callout--note {
  background: rgba(20,16,13,.045); box-shadow: inset 0 0 0 1px rgba(20,16,13,.08);
}
.on-paper .callout--note p, .plate .callout--note p, .idx.on-paper .callout--note p { color: var(--slate); }
.on-paper .callout--note p:first-child, .plate .callout--note p:first-child,
.idx.on-paper .callout--note p:first-child { color: var(--coal); }

/* --warn — the real thing. Amber, never the brand green: this is a caveat,
   not an accent, and it must not read as decoration. Two on the whole site. */
.callout--warn {
  padding: clamp(1.1rem, 2.2vw, 1.5rem) clamp(1.2rem, 2.4vw, 1.7rem);
  border-radius: var(--r-m);
  background: rgba(214,158,74,.1);
  box-shadow: inset 0 0 0 1px rgba(214,158,74,.42);
}
.callout--warn p { font-size: .95rem; line-height: 1.6; color: #E8C48A; }
.callout--warn p:first-child { font-weight: 600; color: #F2D6A6; letter-spacing: .01em; }
.on-paper .callout--warn, .plate .callout--warn, .idx.on-paper .callout--warn {
  background: rgba(170,112,24,.12); box-shadow: inset 0 0 0 1px rgba(170,112,24,.38);
}
.on-paper .callout--warn p, .plate .callout--warn p, .idx.on-paper .callout--warn p { color: #6B4410; }
.on-paper .callout--warn p:first-child, .plate .callout--warn p:first-child,
.idx.on-paper .callout--warn p:first-child { color: #4A2E08; }

/* ---- KEY FIGURES --------------------------------------------------------
   STRICTLY for numbers the source already presents AS numbers. An editorial
   pass mined figures out of running prose and set them as metrics, turning
   "most of our crews live within twenty minutes" into a flat "20 min" — a
   factual change wearing a design pattern. The rule belongs on the component. */
.keyrail { display: flex; flex-wrap: wrap; gap: clamp(1.2rem, 4vw, 3rem) clamp(1.8rem, 6vw, 4rem);
           margin: clamp(2rem, 4vw, 3rem) 0; padding: clamp(1.3rem, 2.5vw, 1.8rem) clamp(1.4rem, 3vw, 2.2rem);
           border-radius: var(--r-m); background: rgba(243,241,237,.04);
           box-shadow: var(--edge), inset 0 0 0 1px rgba(243,241,237,.05); }
.on-paper .keyrail { background: rgba(20,16,13,.035); box-shadow: inset 0 0 0 1px rgba(20,16,13,.08); }
.keyrail > div { flex: 0 1 auto; }
.keyrail__n { font-family: var(--ff-d); font-weight: 600; letter-spacing: -.026em; line-height: 1;
              font-size: clamp(1.4rem, 2.5vw, 2rem); font-variant-numeric: tabular-nums; }
.keyrail__l { margin-top: .45rem; font-size: .78rem; color: var(--stone); max-width: 20ch; }
.on-paper .keyrail__l { color: var(--slate); }

/* ---- SECTION INDEX ------------------------------------------------------
   Long reference pages become a catalogue, and the catalogue is ONE LARGE
   PAPER CANVAS rather than a list floating in a dark band.

   The first version compacted the page and left it flat: a thin index and
   twelve panes on the page floor, which is the least designed thing on the
   site. The second gave each pane its own alternating ground, which is a
   surface on a surface once the section itself has one.

   This is the page-scale answer. Dark header, one big warm plate, dark closing
   action — and a numbered contents list set on paper is what a printed
   specification actually looks like, which is the thing this page is.

   Inset and rounded like the project gallery, with a barely-there warm
   gradient across it so the plate has tone rather than being a flat fill.
   ===================================================================== */
/* ONE PLATE RULE, shared by the catalogue canvas and by the long documents
   that have no sections to index — privacy, terms, the contractor checklist.
   They were the last pages still rendering as the original flat fallback: a
   single ink band, no surface, no radius. Two ways of drawing the same warm
   plate would have drifted, so there is one. */
.idx.on-paper, .band.on-paper.plate {
  margin-inline: var(--inset);
  border-radius: var(--r-xl);
  /* var(--contour-pap) FIRST, and it has to be restated. `background` is a
     shorthand, so this rule at (0,2,0) replaced .on-paper's entire layer list
     at (0,1,0) — including the contour. The motif reached the homepage's two
     plates and silently missed the single largest tan surface on 15 interior
     pages, which is exactly the inconsistency it was added to remove. */
  background:
    var(--contour-pap),
    radial-gradient(120% 90% at 8% 0%, rgba(255,253,248,.55) 0%, rgba(233,228,219,0) 58%),
    var(--paper);
  box-shadow: var(--sh-soft);
}
/* The accents inside a plate follow it onto paper, same as the catalogue. */
/* .on-paper, not .plate — plain .band.on-paper sections carry .prose too, and
   on those five pages the volt bullets and dt markers were rendering at 1.8:1
   on tan, i.e. not rendering. .on-paper subsumes .plate and .idx.on-paper,
   which both also carry the class, so this one pair replaces three. */
.on-paper .prose ul li::before { background: var(--forest); opacity: .75; }
.on-paper .prose dt::before { background: var(--forest); opacity: .5; }
.plate .pull { border-left-color: var(--forest); }
.idx__grid { display: grid; grid-template-columns: minmax(0, 16rem) minmax(0, 1fr);
             gap: clamp(2rem, 5vw, 4.5rem); align-items: start; }

/* ---- the contents column ------------------------------------------------ */
.idx__nav { position: sticky; top: calc(var(--nav-h) + 2.5rem);
            display: grid; gap: .1rem;
            border-right: 1px solid var(--hair-l); padding-right: clamp(1rem, 2vw, 1.75rem); }
.idx__head { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem;
             margin-bottom: .9rem; padding-bottom: .7rem; border-bottom: 1px solid var(--hair-l);
             font-size: .72rem; font-weight: 600; letter-spacing: .08em; text-transform: uppercase;
             color: var(--slate); }
.idx__count { font-weight: 500; letter-spacing: .02em; text-transform: none; opacity: .7; }

.idx__nav button {
  position: relative; display: grid; grid-template-columns: 1.9rem 1fr; align-items: baseline;
  gap: .3rem; text-align: left; background: none; border: 0; cursor: pointer;
  padding: .62rem .2rem .62rem 0; font-size: .92rem; line-height: 1.35;
  color: var(--slate); opacity: .62;
  transition: opacity .3s var(--ease), color .3s var(--ease);
}
.idx__n { font-size: .7rem; font-variant-numeric: tabular-nums; opacity: .7; }
.idx__nav button:hover { opacity: .9; color: var(--coal); }
.idx__nav button[aria-selected="true"] { opacity: 1; color: var(--coal); }
/* Forest, not volt: the accent green is tuned for a dark ground and goes
   muddy on tan. Every accent inside this canvas makes the same swap. */
.idx__nav button::before {
  content: ''; position: absolute; left: calc(-1 * clamp(1rem, 2vw, 1.75rem) - 1px);
  top: 50%; width: 2px; height: 0; background: var(--forest);
  transform: translateY(-50%); transition: height .35s var(--ease-s);
}
.idx__nav button[aria-selected="true"]::before { height: 74%; }
.idx__nav button:focus-visible { outline: 2px solid var(--forest); outline-offset: 2px; border-radius: var(--r-xs); }

/* ---- the pane ----------------------------------------------------------- */
.idx__panes, .idx__pane { min-width: 0; }
.idx__pane[hidden] { display: none; }
.idx__pane { animation: idxIn .45s var(--ease-s) both; }
@keyframes idxIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
.idx__pane > .label { display: block; margin-bottom: .7rem; }
.idx__pane > h2 { margin-bottom: 1rem; }
/* The accents that live on the canvas follow it onto paper. */
/* (the .idx pair that lived here is now covered by .on-paper above) */
.idx.on-paper .pull { border-left-color: var(--forest); }

@media (max-width: 900px) {
  /* No side column on a phone: the contents become a rail above the pane that
     scrolls sideways and bleeds to the canvas edges. */
  .idx.on-paper { margin-inline: var(--inset); border-radius: var(--r-l); }
  /* minmax(0, 1fr), NOT 1fr. A bare 1fr is minmax(AUTO, 1fr), and auto's
     minimum is min-content — so warranty.html's spec table, which carries
     min-width: 32rem so its columns stay readable, pushed the whole track to
     512px inside a 500px phone and gave the document a horizontal scrollbar.
     Measured: 561 > 500. The desktop rule already had minmax(0, 1fr); the
     mobile override quietly dropped it. */
  .idx__grid { grid-template-columns: minmax(0, 1fr); gap: 1.5rem; }
  .idx__head { margin-bottom: .6rem; }
  .idx__nav {
    position: static; display: flex; gap: .2rem; border-right: 0;
    border-bottom: 1px solid var(--hair-l); padding-right: 0; padding-bottom: .2rem;
    overflow-x: auto; scrollbar-width: none;
    margin-inline: calc(var(--gut) * -1); padding-inline: var(--gut);
    scroll-padding-inline: var(--gut);
  }
  .idx__head { display: none; }
  .idx__nav::-webkit-scrollbar { display: none; }
  .idx__nav button { grid-template-columns: auto auto; white-space: nowrap; padding: .6rem .5rem; }
  .idx__nav button::before { left: .5rem; right: .5rem; top: auto; bottom: -.2rem;
                             width: auto; height: 2px; transform: none; }
  .idx__nav button[aria-selected="true"]::before { height: 2px; }
}
@media (prefers-reduced-motion: reduce) {
  .idx__pane { animation: none; }
  .idx__nav button::before { transition: none; }
}

/* ---- CLOSING ACTION ----------------------------------------------------- */
.pcta { padding-inline: var(--inset); padding-block: clamp(3rem, 6vw, 5rem); }
.pcta__surface {
  position: relative; overflow: hidden; border-radius: var(--r-xl);
  background: radial-gradient(90% 130% at 100% 0%, rgba(20,80,55,.5) 0%, rgba(12,31,23,0) 60%), var(--s-3);
  box-shadow: var(--sh-lift), var(--edge);
  padding: clamp(2rem, 5vw, 3.5rem);
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 1.5rem 3rem;
}
.pcta h2 { font-family: var(--ff-d); font-weight: 600; letter-spacing: -.026em; line-height: 1.1;
           font-size: clamp(1.5rem, 2.8vw, 2.3rem); max-width: 18ch; }
.pcta__acts { display: flex; flex-wrap: wrap; align-items: center; gap: .9rem 1.6rem; }

@media (max-width: 760px) {
  .pcols { grid-template-columns: 1fr; }
  .pcta__surface { flex-direction: column; align-items: flex-start; }
}
@media (prefers-reduced-motion: reduce) {
  .pcols > div, .prose dt::before, .prose tbody tr { transition: none; }
  .pcols > div:hover { transform: none; }
}

/* --------------------------------------------------------------- CALLBAR -- */
.callbar { display: none; }


/* ========================= THE FRAME LANGUAGE ============================
   Every photograph on this site sits in one of four frames, and until now
   each was detailed separately — different shadows, some with an edge and
   some without, the hover on one and not the others. That is what makes
   pictures read as cut out and dropped onto the page: nothing says they
   belong to the same object.

   One treatment, four sizes. Each frame gets the same three things:

     an EDGE      a hairline inside the radius, so the photograph ends on a
                  drawn line instead of just stopping;
     a LIFT       a shadow scaled to the frame's role, warm over paper and
                  cold over ink, so it sits ON the ground rather than in it;
     a SHEEN      one very low-contrast diagonal across the top-left corner —
                  the highlight a sheet of glass would catch. At 4% you never
                  see it; you see that the surface has a front.

   Role sets the radius and nothing else: hero --r-xl, service canvas --r-xl,
   project media --r-l, construction frames --r-l. Same language, one scale.
   ====================================================================== */
/* .lyr IS NOT IN THIS LIST, and leaving it in cost the HOW section its whole
   composition. It is already `position: absolute` in its own rule — the three
   frames are placed at left/top percentages inside an aspect-ratio box — and
   this rule sits ~900 lines further down the file at the SAME specificity
   (0,1,0) as `.lyr--1, .lyr--2, .lyr--3`. Later wins, so `.lyr` went
   `relative` at every width: the desktop `top: 28%` / `top: 58%` stopped being
   absolute placement and became relative NUDGES on in-flow blocks, which shoved
   the photographs down over each other and 138px out the bottom of the cluster.

   Both frames are already positioned where their overlay needs them, so only
   these two need the declaration at all. */
.proj__frame, .what__canvas { position: relative; }

/* The hero is deliberately NOT in this list. Its canvas already carries a
   two-axis scrim and the copy sits inside it at z-index 2; another full-bleed
   overlay there would either paint over the headline or fight the scrim for
   ordering. It is the one frame whose edge is the whole viewport. */
.proj__frame::after, .lyr::after, .what__canvas::before {
  content: ''; position: absolute; inset: 0; z-index: 3; pointer-events: none;
  border-radius: inherit;
  /* One notch toward glass: the rim stays a hairline, but the top edge now
     catches light the way a pane does, and the sheen is a touch stronger.
     Enough that the frames belong to the same material family as the masthead
     mark and the panel on the entry-walk photograph — not enough to read as an
     effect applied to a picture. */
  box-shadow: inset 0 0 0 1px rgba(243,241,237,.13),
              inset 0 1px 0 rgba(255,255,255,.16);
  background: linear-gradient(148deg, rgba(255,255,255,.07) 0%,
                              rgba(255,255,255,0) 36%, rgba(0,0,0,0) 100%);
}
.proj__frame::after { box-shadow: inset 0 0 0 1px rgba(20,16,13,.1),
                                  inset 0 1px 0 rgba(255,255,255,.3); }

/* The lift, per role. */
.proj__frame { box-shadow: 0 28px 60px rgba(20,16,13,.17), 0 4px 12px rgba(20,16,13,.08); }
.lyr { box-shadow: 0 22px 52px rgba(0,0,0,.42), 0 3px 10px rgba(0,0,0,.3); }

/* And every frame answers a pointer the same way: the picture breathes, the
   frame gains depth. Same numbers everywhere, so it reads as one behaviour. */
.lyr img, .what__canvas img {
  transition: transform 1.2s var(--ease-s), opacity .75s var(--ease-s),
              clip-path .9s var(--ease-s);
}
.lyr:hover { box-shadow: 0 34px 74px rgba(0,0,0,.5), 0 4px 12px rgba(0,0,0,.34); }
.lyr:hover img { transform: scale(1.015); }
.what__canvas:hover img[data-on="true"] { transform: scale(1.015); }
@media (prefers-reduced-motion: reduce) {
  .lyr:hover img, .what__canvas:hover img[data-on="true"] { transform: none; }
  /* The canvas SWAP, not just the hover. The block below only neutralised the
     pointer scale; choosing a service still ran an 0.9s clip-path wipe and a
     1.5s scale, which is the largest motion in that section. */
  .what__canvas img, .what__canvas .slot {
    transition: opacity .01s; transform: none; clip-path: none;
  }
  /* And the masthead collapse — the biggest scroll-driven movement on the
     site, 158px of logo and 120px of bar, and nothing had ever covered it. */
  .nav, .nav__brand img { transition: none; }
}

/* ---------------------------------------------------------------- REVEAL -- */
/* Opt-in from script, so with JavaScript off or blocked everything is simply
   visible. See modern.js — a reveal must never be what decides whether the
   page has content on it. */
/* NO clip-path HERE. A "masked rise" was tried and reverted the same day: it
   read well, and it quietly destroyed three things, because clip-path clips to
   the border box exactly and clips box-shadows with it.

     - .cluster is a .reveal whose third frame is placed deliberately outside
       its box — measured 138px past the bottom and 18px past the right. The
       photograph was sliced.
     - Every shadow on a child of a revealed element was cut off at the parent's
       edge, which flattened the frame language the rest of the page depends on.
     - The reviews swipe track bleeds past its surface by design with a negative
       margin, so on a phone the track was cut at both ends.

   Opacity and transform only. They are both compositor properties, they clip
   nothing, and the arrival reads fine without the mask. */
.js-reveal .reveal { opacity: 0; transform: translateY(22px); }
.js-reveal .reveal.is-in { opacity: 1; transform: none;
                           transition: opacity .7s var(--ease-s),
                                       transform .95s var(--ease-s); }
@media (prefers-reduced-motion: reduce) {
  .js-reveal .reveal { opacity: 1; transform: none; transition: none; }
  .act:hover, .opt:hover span { transform: none; }
  .proj:hover .proj__frame img { transform: none; }
  .proc__fill { transition: none; transform: scaleX(1); }
  .step.is-on, .done.is-on { animation: none; }
  .sheet { transition: opacity .01s; }
}

/* ============================== RESPONSIVE =============================== */
@media (max-width: 1040px) {
  :root { --nav-h: 60px; --logo-lg: 70px; --logo-sm: 46px;
          /* SIZED SO LANDSCAPING CAN BE READ, because a phone now gets the full
             lockup rather than DZT on its own.
             The sub-line is 14.8% of the mark's height — measured, not guessed —
             so the old 36px put it at 5.3px, which is why it had been dropped
             from the phone in the first place. 64px puts it at 9.5px and the
             collapsed 54px at 8.0px; at a phone's 2-3x pixel density those are
             19-29 real device pixels of letterspaced caps, which is legible.
             The cost is width, and there is width to spare: 137px of lockup
             against ~226px of free space beside the 62px bubble at 320px. */
          --mark-h: 64px; --mark-h-sm: 54px; }
  .nav__links, .nav__tel, .nav .act { display: none; }
  .nav__burger { display: block; }

  /* ON A PHONE THE PANEL HOLDS ONE BUTTON, SO IT STOPS BEING A BAR.

     The hanging slab is right on a desktop, where it carries four links, a
     phone number and the call to action. With those hidden it was an 80x78
     box of glass wrapped around a single 44px control, still squared to the
     top edge and still rounded at the bottom — a nub protruding from the top
     of the screen, which is exactly as odd as it sounds.

     A control should look like a control. It becomes a round glass button,
     sized to the burger and dropped to the same line as the wordmark, so the
     masthead reads as two objects on one line rather than one object and a
     growth. */
  .nav__panel {
    /* A SQUARE BOX, so the pill radius resolves to a true circle. Padding
       alone gave 64x62 — the burger's own 46x44 plus even padding — which is
       an ellipse, and an ellipse reads as a circle that has gone wrong.

       62px, not the 44px tap target it started at. 44 is the MINIMUM a finger
       needs, and a control sized to its minimum reads as the smallest thing it
       is allowed to be. This is the only control in the masthead on a phone;
       it should look like it. */
    width: 62px; height: 62px; padding: 0;
    display: grid; place-items: center;
    margin-top: clamp(.7rem, 1.4vw, 1.2rem);
    border-radius: var(--r-pill);
    background: rgba(13,17,16,.58);
    -webkit-backdrop-filter: blur(16px) saturate(1.2);
    backdrop-filter: blur(16px) saturate(1.2);
    box-shadow: 0 6px 18px rgba(0,0,0,.34), 0 0 0 1px rgba(243,241,237,.07);
    transition: margin-top .55s var(--ease-s), background .45s var(--ease-s);
  }
  .nav.is-stuck .nav__panel {
    /* 62px, NOT auto. `height: auto` here overrode the fixed square above it and
       collapsed the bubble to the burger's own 44px on scroll — a circle that
       quietly shrinks into a smaller circle, which is not the behaviour and
       measured as a 44px bar where 62 was intended. */
    width: 62px; height: 62px;
    margin-top: clamp(.5rem, 1vw, .8rem);
    background: rgba(13,17,16,.8);
    box-shadow: 0 6px 18px rgba(0,0,0,.4), 0 0 0 1px rgba(243,241,237,.08);
  }
  /* THE MARK CENTRES ON THE BUBBLE, and it never did. Both objects were taking
     the desktop's flex-start margins, which are sized to hang a tall wordmark
     off the top edge — against a 62px circle that left the mark's centre 17px
     above the bubble's on scroll, so the "two objects on one line" this block
     is built around were visibly not on one line.

     Derived, not typed: the same margin the bubble uses, plus half of whatever
     the circle has spare over the mark. The two cannot drift apart if the mark
     size or the bubble size changes later. */
  .nav__brand      { margin-top: calc(clamp(.7rem, 1.4vw, 1.2rem) + (62px - var(--mark-h)) / 2); }
  .nav.is-stuck .nav__brand { margin-top: calc(clamp(.5rem, 1vw, .8rem) + (62px - var(--mark-h-sm)) / 2); }

  .nav { background: transparent; }

  .proj--a, .proj--b, .how__grid, .what__grid, .rev__grid, .est__grid { grid-template-columns: 1fr; }
  .proj--b .proj__fig { order: 0; border-radius: var(--r-l); }
  .proj--b .proj__meta { order: 1; }
  /* Stacked, the canvas has to come FIRST. In source order it follows the
     list, which put the photograph below "See everything we build" — the
     picture the list is driving ended up orphaned past the link out of the
     section. On one column the image leads and the list reads beneath it. */
  .what__grid > .what__canvas { order: -1; margin-bottom: .5rem; }
  /* THE ASPECT HAS TO COME DOWN WITH THE WIDTH. 21:9 is a cinematic band at
     1320px and a letterbox slot at 500px — and the metadata panel's negative
     margin then sat on most of what was left, so the photograph showed as a
     thin strip. This override existed but was written against .proj--wide,
     the class's previous name, so after the rename it matched nothing and
     failed silently. The overlap shrinks with it: enough to still read as
     deliberate, not enough to eat the picture. */
  .proj--c .proj__meta { width: auto; margin-left: 0; margin-top: clamp(-2rem, -3.5vw, -1.25rem); }
  /* ---- THE PROCESS RAIL TURNS VERTICAL, IT DOES NOT SWITCH OFF -----------
     This was three `display: none` rules, which deleted the best piece of
     motion on the site from every phone: the hairline, the volt fill that draws
     across in 1.8s when the section arrives, and the five nodes lighting in
     sequence as the fill reaches each one. What was left on a phone was five
     paragraphs in a grid — the section that shows how the company works,
     showing nothing.

     Nothing here is new. The rail is the same rail on the axis a phone
     actually has: a column has vertical length where it has no horizontal
     length, so the line runs down the left, the fill draws downward on the
     same 1.8s, and the same --i stagger lights the same five nodes in the same
     order. scaleY instead of scaleX is most of the diff.

     One column from 1040 down rather than two, because five steps in a 2-up
     grid have no reading order — the rail's whole point is that the steps
     happen in sequence, and a two-column grid says they happen in pairs. */
  .proc__steps { grid-template-columns: 1fr; gap: 0; }
  .proc__rail { padding-top: 0; padding-left: 1.75rem; }
  /* The hairline: down the left edge, full height of the rail. */
  .proc__rail::before {
    left: 0; right: auto; top: 0; bottom: 0;
    width: 1px; height: auto;
  }
  .proc__fill {
    left: 0; top: 0; width: 1px; height: 100%;
    background: linear-gradient(to bottom, var(--volt), rgba(var(--volt-rgb),.25));
    transform: scaleY(0); transform-origin: top;
  }
  .proc__rail.is-in .proc__fill { transform: scaleY(1); }
  .proc__i { padding-right: 0; padding-bottom: clamp(1.5rem, 4vw, 2.1rem); }
  /* -2rem lands the 9px node centred on the 1px line: the step's content box
     starts at the rail's 1.75rem (28px) padding, so -32px puts the node at
     -4..5 and the line occupies 0..1. Both centre on 0.5. */
  .proc__i::before { left: -2rem; top: .3rem; transform: none; }
  .proc__rail.is-in .proc__i::before { transform: none; }
  .proc__i:hover::before { transform: scale(1.25); }
  .foot__top { grid-template-columns: 1fr 1fr; }
}

@media (max-width: 680px) {
  :root { --r-xl: 26px; --r-l: 20px; }
  /* Restored with the rest of the hero. The phone frame is taller than the
     desktop one relative to its width, and the subject of hero-1 sits left of
     centre, so 38% keeps the lit beds in shot. */
  .hero__canvas { height: clamp(520px, 88svh, 780px); }
  .hero__img { object-position: 38% 45%; }
  .proof { margin-top: clamp(-3.5rem, -5vh, -2.5rem); gap: 1.2rem 2rem; }
  .proof__i { flex: 1 1 38%; }
  .foot__top { grid-template-columns: 1fr; }
  .what__list button { grid-template-columns: 2rem 1fr; padding-inline: .6rem; }
  .proj--c .proj__frame img { aspect-ratio: 4 / 3; }

  /* REVIEWS BECOME A SWIPE TRACK. Three stacked quotes is a lot of vertical
     page for something a reader skims, and swiping is the gesture people
     already expect from testimonials.

     The negative margin cancels the wrap's padding so the track bleeds to both
     screen edges, and scroll-padding puts it back for the snap — so the first
     card starts on the text margin and the last one can still reach it instead
     of stopping short. The trailing spacer does the same job at the end, which
     padding-right alone does not do in every engine. */
  /* NAMED .rev__track AND THE MARKUP SAYS .rev__list — so none of this applied.
     The whole mobile treatment below (snap, bleed, hidden scrollbar, the
     trailing spacer) was written against a class that appears in no HTML file
     in this repository. What survived was .rev__item { flex: 0 0 82% } on its
     own, which laid three cards in a 769px row inside a 336px stage with no
     scroll container: card two was sliced down the middle and card three was
     off-screen, and NEITHER could be reached, because the element that was
     supposed to scroll never became one. Two of the three reviews were simply
     unreachable on every phone. */
  .rev__list {
    display: flex; grid-template-columns: none; align-items: stretch;
    gap: .9rem; overflow-x: auto; overscroll-behavior-x: contain;
    scroll-snap-type: x mandatory;
    /* BLEED PAST THE STAGE'S PADDING TOO, not just the wrap's. The negative
       margin cancelled --gut only, so the track was still boxed inside the
       stage's own 28px inset: 315px of usable width, which made each card 229px
       and pushed the first quote to seven lines. Cancelling both gives the track
       the full screen, the cards room to breathe, and a peek that reads as the
       next card rather than as a column of broken text. --stage-pad is declared
       on .rev__stage and inherits down. */
    padding-inline: calc(var(--gut) + var(--stage-pad));
    scroll-padding-inline: calc(var(--gut) + var(--stage-pad));
    margin-inline: calc((var(--gut) + var(--stage-pad)) * -1);
    padding-block: .25rem 1rem;
    scrollbar-width: none;
  }
  .rev__list::-webkit-scrollbar { display: none; }
  .rev__list::after { content: ''; flex: 0 0 calc(var(--gut) + var(--stage-pad) - .9rem); }

  /* THE SCORE STOPS BEING A TALL EMPTY CARD. On desktop it is the left column
     of a two-column stage and its stacked shape is right. Stacked on a phone it
     became 280px wide and 103px tall holding three short items with the whole
     right-hand side empty — a big box saying very little, directly above the
     thing a reader actually came for. Laid on one line it says the same in a
     third of the height, and the rating reads as a fact about the reviews
     rather than as a panel of its own. */
  .rev__score {
    display: flex; align-items: baseline; gap: .7rem;
    text-align: left; flex-wrap: wrap;
  }
  .rev__score .rev__num { font-size: 2.6rem; line-height: 1; }
  .rev__score .rev__stars { margin-top: 0; }
  .rev__count { margin-top: 0; flex: 1 0 100%; }

  .rev__item, .rev__item--lead {
    /* SIZED AGAINST THE VIEWPORT, NOT THE TRACK. A percentage here resolves
       against the track's CONTENT box, which is the track minus the inset that
       makes the first card line up with the text margin — so widening the track
       widened the padding too and the card barely moved: 82% of 280 is the same
       cramped column 82% of 268 was. 78vw is the card a phone actually has,
       which leaves the first quote at five lines instead of seven and still
       shows a clean 44px of the next card. */
    /* DERIVED FROM THE PEEK, which is the part that has to be right. A card
       sized to look generous left 7px of the next one showing at 390 and -9px
       at 320 — i.e. nothing — and a scroller with no visible next card reads as
       a single cropped testimonial, which is where this started. The card is
       therefore whatever is left after the inset, the gap and a deliberate
       ~42px of card two: enough to be unmistakably another card, not enough to
       be read and misread as broken text. */
    /* The peek lands at ~14px, which is INSIDE the next card's own padding
       (15.6px at 390) — so what shows is its rounded edge and empty inset, never
       a column of sliced letters. A 22px peek put glyph fragments at the screen
       edge, which is a small copy of the bug this section started with. */
    flex: 0 0 calc(100vw - var(--gut) - var(--stage-pad) - 3rem);
    max-width: 24rem; width: auto; margin: 0; transform: none;
    grid-row: auto; scroll-snap-align: start;
    border-radius: var(--r-m);
    padding: clamp(1.1rem, 4vw, 1.5rem) clamp(1.2rem, 4vw, 1.6rem);
    background: rgba(20,16,13,.045);
  }
  /* In a row of equal cards the lead no longer needs display size to lead —
     it is simply first. Dropping it to body scale keeps the three cards the
     same shape, which is what makes the track read as one set. */
  .rev__item--lead blockquote p { font-family: var(--ff-b); font-weight: 400;
                                  font-size: 1.02rem; line-height: 1.55; letter-spacing: 0; }
  .rev__item .rev__by { margin-top: 1rem; }

  /* THE FOOTER WAS 1,400px ON A PHONE — 2.14 screens of it, measured. Fourteen
     links each given a 44px touch target and stacked vertically is most of
     that, and none of it can be deleted: every link is a real page.

     So the groups run inline and wrap instead of stacking. Four links become
     two wrapped rows rather than four tall ones, the dot separators do the
     work the vertical rhythm was doing, and the touch target survives because
     the padding is still there — it is the WIDTH that is being reused, not the
     height that is being cut. Nothing is dropped. */
  /* THE RHYTHM WAS INVERTED, which is most of what "sloppy" was. A heading sat
     47px above the links it labels while the gap BETWEEN groups was 22px — so
     every heading looked detached from its own list and attached to the one
     above. Inside a group is now tight and between groups is loose, which is
     the only arrangement that reads as grouped at all. */
  .foot { padding-block: clamp(2rem, 5.5vw, 2.75rem) 0; }
  .foot__top { grid-template-columns: 1fr; gap: 1.9rem; }
  .foot__brand { margin-bottom: .8rem; }
  .foot__brand img { width: 64px; }
  .foot__blurb { max-width: none; }

  /* Headings read as labels rather than as slightly-smaller links. */
  .foot h4 { margin-bottom: .1rem; }

  .foot__top ul { display: flex; flex-wrap: wrap; gap: 0 .35rem; }
  /* MIN-HEIGHT ON THE ROW, not padding on the link. Padding tall enough for a
     44px target also pushed the heading away from its own list; a min-height
     on the item gives the same target and lets the group close up. */
  .foot__top li { display: flex; align-items: center; gap: .35rem; min-height: 44px; }
  .foot li + li { margin-top: 0; }
  .foot__top li + li::before { content: '\00B7'; color: var(--stone); opacity: .32; }
  .foot ul a { padding-block: 0; font-size: .88rem; }
  .foot__bar { margin-top: 1.5rem; padding-block: .9rem; gap: .2rem 1rem; font-size: .74rem; }
  .foot__bar a { padding-block: .35rem; }

  /* THE WHOLE QUESTIONNAIRE HAS TO FIT ONE PHONE SCREEN, intro included, with
     no scrolling. Measured at 500x653 the section was 1037px against 585px of
     usable height once the call bar is subtracted — so this is a budget, not a
     tidy-up, and every number below was taken off a measurement rather than
     guessed.

     The options list was the whole problem: six choices in one column cost
     347px on their own. Two columns make it three rows and save about 180 of
     that, which is most of the gap closed in one declaration. The rest comes
     off the surface padding, the heading, and the space between the intro and
     the form. */
  .est { padding-block: clamp(1.5rem, 5vw, 2.5rem); }
  .est__surface { padding: 1.15rem 1.1rem; border-radius: var(--r-l); }
  .est__grid { gap: .95rem; }
  .est__grid > div:first-child > h2 { font-size: 1.55rem; margin-top: .45rem !important; }
  .est__grid .lede { font-size: .92rem; line-height: 1.5; margin-top: .6rem !important; }
  .est__grid > div:first-child > p:last-child { margin-top: .9rem !important; }
  .est__tel { font-size: 1.5rem; margin-top: .1rem; padding-block: 0; }

  .est__bar { margin-bottom: .9rem; }
  .step legend { font-size: 1rem; margin-bottom: .7rem; }
  /* Two columns. Six options become three rows. */
  .opts { grid-template-columns: 1fr 1fr; gap: .45rem; }
  .opt span { padding: .7rem .7rem; font-size: .84rem; }
  .field { margin-bottom: .6rem; }
  .field label { margin-bottom: .3rem; font-size: .68rem; }
  .field input { padding: .7rem .8rem; }
  /* The A2P disclosure cannot be shortened — it is the wording that got the
     number registered — so it is set smaller and tighter instead of trimmed. */
  /* Name and phone side by side — worth about 69px, which is most of what
     step 3 is over by.

     FLOAT PLUS A BLOCK FORMATTING CONTEXT, not two inline-blocks. The
     inline-block version measured 465px of content inside a 463px fieldset,
     because two 50% boxes plus their margin plus the WHITESPACE BETWEEN THE
     TAGS came to more than the row, so the second field wrapped and saved
     nothing. A float carries no whitespace, and giving the second field its
     own formatting context makes it take the remainder exactly. */
  .step .field:has(#q-name) { float: left; width: calc(50% - .3rem); margin-right: .6rem; }
  .step .field:has(#q-phone) { overflow: hidden; }
  .step .note, .step .fnav { clear: both; }
  .note { margin-top: .6rem; font-size: .66rem; line-height: 1.42; }

  /* STEP 3 IS THE TIGHT ONE, because the A2P disclosure sits on it and that
     block cannot be shortened — it is the exact wording that got the number
     registered with the carrier, rejected three times before it said all seven
     things it says. So the SELL gets out of the way instead: once you have
     started answering, the lede is a pitch you have already accepted, and the
     heading and the phone number stay. Nothing is removed from the page, it
     just stops being repeated while you are mid-answer. */
  .est.is-deep .est__grid .lede { display: none; }
  .fnav { margin-top: .9rem; }
  .act { padding: .8rem 1.15rem; }

  /* THE HERO'S PHONE LINK GOES, BECAUSE THE CALL BAR IS RIGHT THERE. Below
     680 a fixed bar sits at the bottom of every screen carrying Call and Free
     estimate, so the hero was offering a fourth route to a number that is
     permanently on screen anyway. Scoped to this breakpoint deliberately: it
     is the same breakpoint the bar appears at, so the link is only ever
     removed where something else is already doing its job. The number is
     untouched above 680, where there is no bar. */
  .hero__acts a[href^="tel:"] { display: none; }

  /* ---- THE HEADLINE IS TOO SMALL, AND THE <br> IS WHY ---------------------
     The hero h1 sits at its 2.5rem floor on every phone: 6vw does not overtake
     40px until a 667px viewport, so a 320px phone and a 430px phone get the
     identical 40px. That is the "too small".

     It could not simply be raised, because it is already too BIG at the bottom
     end. The markup hard-breaks the line as "A yard you'll love" / "coming
     home to.", and at 40px that first line measures 288px inside a 266px box
     at 320px wide — so the smallest phones are already overflowing and
     re-wrapping it into a ragged third line. Measured ceilings with the break
     in place: 36.9px at 320, 42.5px at 360, 46.7px at 390. There was nowhere
     to go.

     So the break comes out on phones and the line sets itself. The constraint
     stops being "the longest authored line must fit" — 288px of it — and
     becomes "the longest WORD must fit", which is "you'll" at about 125px. The
     type can then be half as big again and still sit comfortably.

     The two-line lockup is deliberate and is untouched from 681px up, where
     there is width for it. A phone gets three lines of much larger type, which
     is the same headline doing the same job in the shape a phone has. */
  .hero h1 br { display: none; }
  .hero .d-xl { font-size: clamp(3rem, 14.5vw, 3.9rem); }

  .callbar {
    display: grid; grid-template-columns: 1fr 1fr; gap: .5rem;
    /* THE OTHER END OF THE SAME PROBLEM. With viewport-fit=cover the page runs
       under the home indicator too, so a bar pinned to var(--inset) puts Call
       and Free estimate under the bar the user swipes to leave the app. The
       inset is added, not substituted: on a device without one it resolves to
       0px and the spacing is exactly what it was. */
    position: fixed; z-index: 90;
    inset: auto var(--inset) calc(var(--inset) + env(safe-area-inset-bottom, 0px)) var(--inset);
    padding: .5rem; border-radius: var(--r-m);
    background: rgba(18,23,22,.82);
    -webkit-backdrop-filter: blur(18px); backdrop-filter: blur(18px);
    box-shadow: var(--sh-soft), var(--edge);
  }
  .callbar a { padding: .9rem .5rem; text-align: center; font-size: .88rem; font-weight: 600;
               text-decoration: none; border-radius: var(--r-s); color: var(--bone); }
  .callbar a + a { background: var(--volt); color: #06210F; }
  body { padding-bottom: 82px; }
}

/* Touch targets. Gated on width OR pointer: a touchscreen laptop reports
   `fine`, and so does a phone browser in desktop mode. */
@media (pointer: coarse), (max-width: 680px) {
  /* Inline links inside running text measured 20px tall. They cannot become
     44px blocks without wrecking the paragraph they sit in, but padding on an
     INLINE element grows the hit area without touching the line box — the
     paragraph is unchanged and the thumb gets 20px more to aim at. */
  .prose a, .callout a, .idx__pane a, .note a { padding-block: .5rem; }

  /* .72rem, measured rather than guessed: at .45rem these links came out 38px
     tall against a 44px target. Line box is ~20px, so the padding has to carry
     the other 24. */
  /* .foot ul a, .foot__bar a and .est__tel are NOT in these lists. They each
     carry a deliberate padding-block from the mobile footer pass — that pass
     shortened the footer on a phone, and this block sits later in the file at
     equal specificity, so it silently put the height back. Narrowed rather
     than moved: moving it would change coarse-pointer behaviour on wide
     screens, which nobody asked for. */
  .rev__areas a { display: inline-block; padding-block: .72rem; }
  .act-2 { padding-block: .8rem; }
  .foot__tel { display: inline-block; padding-block: .5rem; }
}

@media print {
  .nav, .sheet, .callbar, .est__form { display: none; }
  body { background: #fff; color: #000; }
}
