/* Terrel Construction — the site stylesheet.
 *
 * Strategy is ticket 13's: re-author in plain CSS with custom properties, no
 * build step, icon fonts replaced by inline SVG, Google Fonts self-hosted,
 * jQuery dropped. This is a PORT, not a redesign — every number below was
 * measured off live production at 1440px with tools/probe-live.mjs, not
 * eyeballed. When a value looks arbitrary it is because the Ratio theme's
 * compiled output said so.
 *
 * Breakpoints are 1024 / 900 / 777 (ticket 16). They are deliberately NOT the
 * parent theme's 1024/768/600/480 set, which is why the visual-diff widths
 * were chosen rather than swept: 1024 is the only boundary both share.
 */

/* ==========================================================================
   Fonts — self-hosted (ticket 13). Two variable woff2 files, latin subset,
   48 KB each, against the 198 KB of @font-face CSS the live site requests for
   3 families x every weight and italic (of which Roboto renders nowhere).
   ========================================================================== */

@font-face {
  font-family: 'Open Sans';
  src: url('/fonts/open-sans-latin.woff2') format('woff2');
  font-weight: 300 800;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'Raleway';
  src: url('/fonts/raleway-latin.woff2') format('woff2');
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

/* ==========================================================================
   Tokens
   ========================================================================== */

:root {
  --accent:       #d6ab60;
  --accent-light: #e7bd74;
  --accent-dark:  #c0954b;
  /* Same hue family as --accent, darkened to clear 4.5:1 on white (2.13:1 at
     --accent itself) — for accent-coloured BODY TEXT specifically. --accent
     stays as-is for decorative/large uses where contrast doesn't apply
     (ticket 25). */
  --accent-text:  #8f6526;
  --ink:          #313131;
  /* #757575 (production's value) is 4.30:1 on --panel-grey-2, under WCAG
     AA's 4.5:1 — axe-core hard fails it (ticket 25). This is the sitewide
     default body-text color, so darkened at the token rather than patched
     per selector; passes 4.5:1 against white and both panel greys. */
  --body:         #6b6b6b;
  /* #999 (production's value) is 2.85:1 on white — axe-core hard fails it
     (ticket 25). Landed at the OLD --body value: darker than the new one
     above, but still visibly lighter, keeping the muted/body hierarchy. */
  --muted:        #757575;
  --rule:         #d7d7d7;
  --panel-grey:   #f5f5f5;
  --panel-grey-2: #f7f7f7;

  --font-body:    'Open Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;
  --font-display: 'Raleway', 'Open Sans', Arial, sans-serif;

  /* THE RATIO CONTAINER LADDER. The content column is not fluid — it is a
     stepped set of FIXED widths, centred, taken verbatim from the parent
     theme's `modules-responsive.min.css` (`.edgtf-container-inner, .edgtf-grid,
     .edgtf-grid-section .edgtf-section-inner`) and confirmed against production
     by tools/probe-live.mjs at 1024 / 900 / 390:

         viewport   container   measured x
         >1400      1300        70  (at 1440)
         <=1400     1100
         <=1200      950
         <=1024      768        128 (at 1024) / 66 (at 900)
         <= 768      600
         <= 600      420
         <= 480      300         45 (at 390)
         <= 320      95%

     This is the narrow-width equivalent of the Bootstrap 30px finding below,
     and it is the single largest source of displacement at 1024/900/390: a
     fluid container puts the home intro at x=31 where production has x=128.

     --gutter is DERIVED from the ladder rather than chosen, so every
     `calc(var(--wrap) + 2 * var(--gutter))` and
     `padding-inline: calc(var(--gutter) - 15px)` below keeps working unchanged
     and simply centres the ladder step. Do not give it an independent value. */
  --wrap:   1300px;
  --gutter: max(15px, calc((100vw - var(--wrap)) / 2));

  /* Small fixed inset for things that hang off the viewport edge rather than
     riding the content column — the mobile header bar, the open nav panel, the
     banner caption's right stop. These must NOT track --gutter: on the ladder
     it reaches 128px at 1024 and would shove the hamburger a third of the way
     into the page. */
  --edge: clamp(20px, 3vw, 40px);

  /* --header-h only exists in the <=1024 treatment, where the bar sits ABOVE
     the hero rather than over it, so the hero is 100vh - 60px there. */
}

/* The ladder itself. Steps only — everything derived from --wrap follows. */
@media (max-width: 1400px) { :root { --wrap: 1100px; } }
@media (max-width: 1200px) { :root { --wrap:  950px; } }
@media (max-width: 1024px) { :root { --wrap:  768px; } }
@media (max-width:  768px) { :root { --wrap:  600px; } }
@media (max-width:  600px) { :root { --wrap:  420px; } }
@media (max-width:  480px) { :root { --wrap:  300px; } }
@media (max-width:  320px) { :root { --wrap:   95vw; } }

*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  font-family: var(--font-body);
  font-size: 14px;
  line-height: 26px;
  color: var(--body);
  background: #fff;
  -webkit-font-smoothing: antialiased;
}

/* .two-up is deliberately wider than the viewport at some widths (see its own
   comment — measured live at 968px against a 900px viewport). `overflow-x`
   on body/html does NOT clip this: per spec, a body-level overflow propagates
   to the viewport instead of establishing a clip there, so `scrollWidth` (and
   Playwright's fullPage screenshot width, which reads it) stays at the
   child's full size. Only a non-root ancestor's `overflow` actually clips —
   hence this wrapper around every page's content, confirmed against a
   minimal repro before landing. */
.content-clip { overflow-x: hidden; }

/* --ar is the original photo's ratio, set by the {% img %} shortcode
   (tools/image-render.mjs). Any rule that sets its own aspect-ratio wins. */
img { max-width: 100%; height: auto; display: block; aspect-ratio: var(--ar, auto); }
/* Every photograph is wrapped in a <picture> by the {% img %} shortcode
   (ticket 29). `display: contents` keeps the wrapper out of layout entirely,
   so every `.thing img` rule below goes on selecting and positioning the image
   exactly as it did before the wrapper existed. */
picture { display: contents; }
a { color: inherit; }

h1, h2, h3, h4 {
  margin: 0;
  color: var(--ink);
  font-weight: 300;
  letter-spacing: 1px;
  line-height: 40px;
  text-transform: uppercase;
}
h1 { font-size: 39px; line-height: 47.27px; }
h2 { font-size: 33px; }
h3 { font-size: 17px; font-weight: 400; line-height: 26.4444px; }

p { margin: 0 0 25px; }
p:last-child { margin-bottom: 0; }

.wrap {
  width: 100%;
  max-width: calc(var(--wrap) + 2 * var(--gutter));
  margin-inline: auto;
  padding-inline: var(--gutter);
}

/* NOTE ON MULTI-COLUMN ROWS. The Ratio grid is Bootstrap-derived, so every
   row on this site is 30px WIDER than the 1300px content column and every cell
   carries 15px of side padding — cell content still lands on the 1300px
   column. That is why the rows below all read
   `padding-inline: calc(var(--gutter) - 15px)` with 15px back on the children
   rather than a plain `column-gap`. It is not decoration: it is the difference
   between the home intro heading wrapping to two lines, as production does,
   and not wrapping at all.

   The gold hairline under a heading: 50px of 1px accent-light, inline-block so
   it can be centred by its parent's text-align. */
.rule {
  display: inline-block;
  width: 50px;
  height: 0;
  margin: 16px 0 20px;
  border: 0;
  border-top: 1px solid var(--accent-light);
}
.rule-center { display: block; margin: 31px auto 60px; }

.section-subtitle {
  color: var(--muted);
  font-size: 16px;
  font-weight: 100;
  text-align: center;
  margin-bottom: 40px;
}

/* Ticket 28: the portfolio prev/next arrows are icon-only and their accessible
   names depend on this being a real definition, not display:none. */
.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ==========================================================================
   Buttons
   ========================================================================== */

.btn {
  display: inline-block;
  padding: 17px 43px;
  font-family: var(--font-body);
  font-size: 13px;
  font-weight: 700;
  line-height: 22.1px;
  letter-spacing: 2px;
  text-transform: uppercase;
  text-decoration: none;
  border: 0;
  border-radius: 55px;
  cursor: pointer;
  transition: background-color .15s ease-out, border-color .15s ease-out, color .15s ease-out;
}
.btn::after { content: '\00a0\00a0\2192'; }

.btn-outline {
  color: #000;
  background: transparent;
  box-shadow: inset 0 0 0 1px var(--rule);
}
.btn-outline:hover { color: #fff; background: var(--accent); box-shadow: inset 0 0 0 1px var(--accent); }

.btn-gradient {
  color: #fff;
  background-image: linear-gradient(to right, var(--accent-dark), var(--accent-light));
}
.btn-gradient:hover { background-image: linear-gradient(to right, var(--accent-light), var(--accent-light)); }

/* ==========================================================================
   Header — two treatments, not one (ticket 14).
   Above 1024px: transparent over the hero, white logo, nav right.
   At 1024px and below: a solid white bar sitting ABOVE the hero, hamburger
   left and colour logo centred.
   ========================================================================== */

.site-header {
  position: absolute;
  inset: 0 0 auto;
  z-index: 20;
  background-color: var(--header-bg, transparent);
  background-image: linear-gradient(to bottom, rgba(0, 0, 0, .5) 0%, rgba(0, 0, 0, 0) 100%);
}
.header-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  min-height: 80px;
  max-width: none;
  /* Ratio pads the bar 23px and offsets the logo 4px more, so the logo sits
     at 27 but the nav ends 23px from the edge (ticket 34). */
  padding-inline: 23px;
}
.logo { display: block; }
@media (min-width: 1025px) {
  .site-header .logo { margin-left: 4px; }
}
.logo img { width: 234px; height: 50px; }
.logo .logo-dark { display: none; }

.nav { display: flex; align-items: center; }
.nav a {
  position: relative;
  padding: 0 20px;
  font-size: 13px;
  font-weight: 500;
  line-height: 80px;
  letter-spacing: 1px;
  text-transform: uppercase;
  text-decoration: none;
  color: #fff;
  transition: color .15s ease-out;
}
/* The main header's item geometry, ported from Ratio (ticket 34): 24px each
   side, and a divider that is an empty inline box with a 1px right border,
   sitting AFTER the item's right padding and taking up that 1px of width.
   Being inline, it is as tall as the font's content area (~18px at 13px) and
   rides the text baseline, as production's `.edgtf-item-outer::after` does.
   None after the last item. The old 20px padding with an overlaid divider made
   every item 9px narrow, which shifted the whole nav 40px right on every page
   above 1024 — not the empty "Customers" <li>, which is 0px wide live. */
@media (min-width: 1025px) {
  /* Ratio centres a 21px line box in the 80px bar (top: 40px, then -50%),
     so it starts at 29.5. An 80px line-height puts the glyphs 0.5px lower. */
  .site-header .nav a { padding: 29.5px 24px; line-height: 21px; }
  .site-header .nav a:not(:last-child) { padding-right: 0; }
  .site-header .nav a:not(:last-child)::after {
    content: '';
    margin-left: 24px;
    border-right: 1px solid rgba(255, 255, 255, .65);
  }
}
/* Ratio's per-page dark header skin (`edgtf-dark-header`), which production
   sets on /company/ only: a white-to-clear gradient, the colour logo, black
   nav and gold dividers. Above 1024 only — below it every page already has
   the solid white bar (ticket 34). */
@media (min-width: 1025px) {
  .site-header[data-skin='dark'] { background-image: linear-gradient(to bottom, #fff 0%, rgba(255, 255, 255, 0) 100%); }
  [data-skin='dark'] .logo .logo-light { display: none; }
  [data-skin='dark'] .logo .logo-dark { display: block; }
  [data-skin='dark'] .nav a { color: #000; }
  [data-skin='dark'] .nav a:not(:last-child)::after { border-right-color: var(--accent); }
}
/* Hover only. Production's main header does not mark the current page; its
   sticky header does (below). Decided by the user, ticket 34. */
.nav a:hover { color: var(--accent-light); }

.nav-toggle {
  display: none;
  background: none;
  border: 0;
  padding: 8px;
  color: #fff;
  cursor: pointer;
}

@media (max-width: 1024px) {
  /* 60px bar + Ratio's 1px #e0e0e0 rule under it = 61, which is where
     production's banner starts at every width <= 1024. Without the rule every
     banner photo sat 1px high and diffed along each horizontal edge (ticket 36). */
  :root { --header-h: 61px; }

  .site-header { position: static; background: #fff; border-bottom: 1px solid #e0e0e0; }
  /* Measured live at 1024/768/390 (ticket 34): the logo is the full 234x50 at
     y 5, centred plus 2px; the hamburger glyph (12x14, black) sits at y 23 on
     the content column's left edge, i.e. at --gutter. Both are placed
     absolutely; the button keeps 13px of padding around the glyph so its tap
     target stays 38x40 rather than production's bare 12x14. */
  .header-inner {
    position: relative;
    display: block;
    height: 60px;
    min-height: 0;
    padding: 0;
  }
  .logo { position: absolute; top: 5px; left: calc(50% - 115px); }
  .logo .logo-light { display: none; }
  .logo .logo-dark { display: block; }

  .nav-toggle {
    display: block;
    position: absolute;
    top: 10px;
    left: calc(var(--gutter) - 13px);
    padding: 13px;
    line-height: 0;
    color: #000;
  }
  .nav {
    display: none;
    position: absolute;
    inset: var(--header-h) 0 auto;
    z-index: 20;
    flex-direction: column;
    align-items: stretch;
    background: #fff;
    padding: 4px var(--edge) 16px;
    box-shadow: 0 12px 24px rgba(0, 0, 0, .12);
  }
  .nav[data-open] { display: flex; }
  .nav a { color: var(--ink); padding: 14px 0; line-height: 26px; }
}

/* Sticky header — desktop only. Fixed, translated off-screen until JS adds
   [data-visible] past the scroll threshold (site.js). */
.sticky-header {
  position: fixed;
  inset: 0 0 auto;
  z-index: 30;
  height: 60px;
  background: #fff;
  box-shadow: 0 1px 10px rgba(0, 0, 0, .1);
  transform: translateY(-100%);
  transition: transform .3s ease;
}
.sticky-header[data-visible] { transform: translateY(0); }
.sticky-header-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  height: 100%;
  max-width: none;
  padding-inline: 27px;
}
.sticky-header .logo img { width: 187px; height: 40px; }
.sticky-header .nav a { color: var(--ink); line-height: 60px; }
.sticky-header .nav a[aria-current='page'] { color: var(--accent); }

@media (max-width: 1024px) {
  .sticky-header { display: none; }
}

/* ==========================================================================
   Banner — the shared hero component (ticket 14).
   One instance per page. A `.banner` with a single slide IS the inner-page
   banner and loads no JavaScript; add slides and `data-rotate` and it is the
   home carousel. The 8 portfolio galleries use it too, which is how one
   component covers all 13 pages and the site still ships no carousel library.
   ========================================================================== */

.banner {
  position: relative;
  height: calc(100vh - var(--header-h, 0px));
  min-height: 340px;
  overflow: hidden;
  background: #1f1f1f;

  /* The caption is laid out in banner units, the way RevSlider lays out its
     grid — see "Caption geometry" below. */
  container-type: size;

  /* A stacking context of its own, so the slides' z-index stays inside the
     banner. Without it the current slide (z-index 1) painted OVER the gallery's
     Residential card, which rides 110px up onto the banner and is meant to sit
     on top of it (ticket 36). */
  isolation: isolate;
}

/* THE INNER-PAGE BANNER IS WIDTH-DRIVEN, NOT VIEWPORT-DRIVEN. The home hero is
   a real 100vh; the five inner-page banners are RevSlider fullwidth sliders,
   whose height is an aspect ratio of the WIDTH, in four bands, capped at 880.
   Swept live across 23 widths at two viewport heights -- the two heights return
   identical values, which is what rules out vh:

       >=1300   880px flat (the 1.4776 band meets the cap at ~1300)
       1024..   w / 1.4776   1230->833  1100->745  1030->697  1024->693
        778..   w / 1.6422   1000->609   900->548   800->487   778->474
        481..   w / 1.0238    770->752   700->684   600->586   481->470
       <=480    w / 0.6667    400->600   390->585   360->540

   The old `calc(100vh - 20px)` was right at 1440x900 by coincidence only
   (900 - 20 = 880 = the cap) and wrong everywhere else: it put 820 where
   production has 693 at 1024, and 880 where production has 812 at 1200.

   The 1023 boundary is deliberately one pixel off the 1024 header breakpoint.
   At exactly 1024 production shows the MOBILE header (banner y=61) but is still
   in the 1.4776 band (h=693) -- and 1024 is a golden width, so the off-by-one
   is load-bearing rather than sloppy. */
.banner:not([data-rotate]) {
  /* An explicit width is load-bearing: with `width: auto` the max-height cap
     feeds BACK through the aspect ratio and shrinks the box to 1300px wide at
     1440, so the banner stopped short of the viewport on all 13 pages above
     ~1300. Pinning the width keeps the cap purely vertical. */
  width: 100%;
  height: auto;
  aspect-ratio: 1.4776;
  max-height: 880px;
  --cap-top: 78%;
}
@media (max-width: 1023px) { .banner:not([data-rotate]) { aspect-ratio: 1.6422; } }
@media (max-width:  777px) { .banner:not([data-rotate]) { aspect-ratio: 1.0238; } }
/* 479, not 480: RevSlider's phone level starts BELOW 480 — live is 469 tall at
   exactly 480 (w / 1.0238) and 719 at 479. (Ticket 36.) */
@media (max-width:  479px) { .banner:not([data-rotate]) { aspect-ratio: 0.6667; } }

/* HARD REQUIREMENT from ticket 20: non-current slides are hidden with
   `display:none`, NOT opacity and NOT visibility. This is load-bearing for
   image bytes, not a style preference — with slides hidden by opacity they
   are all "in viewport" to the lazy-loading heuristic and all 18 Niwot Hills
   photographs load on arrival, putting ~5 MB back on the portfolio pages.
   `[data-enter]` and `[data-leaving]` are the two frames of the crossfade,
   which is the only reason a non-current slide is ever laid out at all. */
.banner-slide {
  position: absolute;
  inset: 0;
  display: none;
  opacity: 0;
  transition: opacity .9s ease, transform .9s cubic-bezier(.16, 1, .3, 1);
}
.banner-slide[data-current],
.banner-slide[data-enter],
.banner-slide[data-leaving] { display: block; }
.banner-slide[data-current] { opacity: 1; transform: none; z-index: 1; }
/* slideleft: the outgoing slide is pushed left as the incoming one fades up */
.banner-slide[data-leaving] { opacity: 0; transform: translateX(-12%); z-index: 0; }

.banner-img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: var(--focus, 50% 50%);
}

/* ---------- Caption geometry (ticket 36) ----------
   Ported from the live rule rather than fitted to the four test widths. Live,
   two systems stack:

   1. RevSlider scales each layer by u = min(1, W / gridW, H / gridH) per
      responsive level, from the sliders' own settings:
                    width    level grid (w x h)        home grid h
        level 0   >= 1024    1300 x 880                 868
        level 1   778-1023   1100 x 670                 768
        level 2   480-777     778 x 760                 960
        level 3   <= 479      480 x 720                 720
      Inner banners are exactly that aspect, so u is just W / gridW there;
      the 100vh home hero is where the H term bites (87px titles at 777x900).
      Title 100/106 (70/72 at weight 200 on phones), subtitle 40/35, both
      1px letter-spaced at full scale only, each box a fixed fraction of the banner width — which
      is what wraps "Photo Gallery" at <= 1000 and nothing else.
   2. The child theme's tc_site.js then overrides position: title BOTTOM and
      subtitle TOP both at topPct of the banner height, left at leftPct of its
      width, with font floors of 75/25 (55/22 below 778). Defaults are
      15% / 85%; per-slide `tcdata` overrides come in as --cl0..3 / --ct0..3
      (see partials/banner.html). RevSlider's own unscaled layer offsets still
      add on top: title 0/0/9/14px, subtitle 6/6/15/18px, subtitle +2px down
      at level 0.

   Measured against production at 18 widths from 1600 to 390 on all five
   banner pages. */
.banner-slide {
  --gw: 1300; --gh: 880;
  --u: min(1px, 100cqw / var(--gw), 100cqh / var(--gh));
  --t-size: 100; --t-lh: 106; --t-min: 75px; --t-wt: 100;
  --s-min: 25px;
  --t-x: 0px;
  --cap-w: 0.65625;
  --cap-left: var(--cl0, 15%);
  --cap-top: var(--ct0, 85%);
  --s-x: var(--sx0, 6px);
  --s-y: var(--sy0, 2px);
}
.banner[data-rotate] .banner-slide { --gh: 868; --cap-ls: 0px; }
@media (max-width: 1023px) {
  .banner-slide {
    --gw: 1100; --gh: 670;
    --cap-left: var(--cl1, 15%); --cap-top: var(--ct1, 85%);
    --s-x: var(--sx1, 6px); --s-y: 0px;
  }
  .banner[data-rotate] .banner-slide { --gh: 768; }
}
@media (max-width: 777px) {
  .banner-slide {
    --gw: 778; --gh: 760;
    --t-size: var(--tsize2, 100); --t-lh: var(--tlh2, 106);
    --t-min: 55px; --s-min: 22px;
    --t-x: 9px; --cap-w: 0.815;
    --cap-left: var(--cl2, 2.5%); --cap-top: var(--ct2, 85%);
    --s-x: var(--sx2, 15px);
  }
  .banner[data-rotate] .banner-slide { --gh: 960; }
}
@media (max-width: 479px) {
  .banner-slide,
  .banner[data-rotate] .banner-slide {
    --gw: 480; --gh: 720;
    --t-size: 70; --t-lh: 72; --t-wt: 200;
    --t-x: 14px; --cap-w: 0.858;
    --cap-left: var(--cl3, 2.5%); --cap-top: var(--ct3, 85%);
    --s-x: var(--sx3, 18px);
  }
}

/* The live slider has no scrim; the text carries a hard shadow instead. Kept
   faithful — adding a scrim would be a design change. The caption is a full
   banner wide so the boxes inside cap themselves at --cap-w, as live, rather
   than at whatever is left of the banner past --cap-left. */
.banner-caption {
  position: absolute;
  top: var(--cap-top);
  left: var(--cap-left);
  width: 100cqw;
  z-index: 2;
  color: #fff;
  text-shadow: rgb(31, 31, 31) 1px 1px 2px;
  pointer-events: none;
}

.banner-title {
  position: absolute;
  bottom: 100%;
  left: var(--t-x);
  max-width: calc(var(--cap-w) * 100cqw);
  margin: 0;
  color: #fff;
  font-family: var(--font-display);
  /* RevSlider rounds every scaled size to whole pixels, ties up (32.5 -> 33,
     Math.round). The .004px stops float error landing 32.4999 on the wrong
     side of a tie. */
  font-size: max(var(--t-min), round(var(--t-size) * var(--u) + .004px, 1px));
  /* tc_site.js sets min + 1 when the floor kicks in. */
  line-height: max(var(--t-min) + 1px, round(var(--t-lh) * var(--u) + .004px, 1px));
  font-weight: var(--t-wt);
  /* RevSlider scales the configured 1px and truncates: 1px at full scale
     (>= 1300 wide), 0 everywhere below — and 0 at every size on the home
     slider, where it writes an inline `letter-spacing: 0px`. */
  letter-spacing: var(--cap-ls, round(down, var(--u), 1px));
  text-transform: uppercase;
}
.banner-sub {
  max-width: calc(var(--cap-w) * 100cqw);
  margin: var(--s-y) 0 0 var(--s-x);
  font-family: var(--font-display);
  font-size: max(var(--s-min), round(40 * var(--u) + .004px, 1px));
  line-height: round(35 * var(--u) + .004px, 1px);
  font-weight: 300;
  letter-spacing: var(--cap-ls, round(down, var(--u), 1px));
}

/* Caption choreography: subtitle in at 600 ms, title at 800 ms, 900 ms each,
   sliding from +50px — the numbers RevSlider was configured with. */
.banner-slide[data-current] .banner-title,
.banner-slide[data-current] .banner-sub {
  animation: caption-in .9s cubic-bezier(.16, 1, .3, 1) backwards;
}
.banner-slide[data-current] .banner-sub { animation-delay: .6s; }
.banner-slide[data-current] .banner-title { animation-delay: .8s; }

@keyframes caption-in {
  from { opacity: 0; transform: translateX(50px); }
  to { opacity: 1; transform: none; }
}

/* ---------- Banner controls ----------
   WCAG 2.2.2 requires a visible pause control of anything that autoplays. The
   live home slider has none, so on / these are real added pixels and the one
   entry in tests/screenshot.css (ticket 16). The live PORTFOLIO slider does
   have dots and arrows, so on those 8 pages the intended diff runs the other
   way — see the note in the ticket. */

.banner-controls {
  position: absolute;
  inset: auto 0 0;
  z-index: 3;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 6px;
  padding: 0 20px 22px;
}
.banner-btn {
  display: grid;
  place-items: center;
  width: 44px; height: 44px;
  border: 0;
  border-radius: 50%;
  background: rgba(0, 0, 0, .35);
  color: rgba(255, 255, 255, .75);
  cursor: pointer;
  transition: background .3s ease, color .3s ease;
}
.banner-btn:hover { background: rgba(0, 0, 0, .6); color: #fff; }
.banner-btn:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }
.banner-btn svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 2.5; }
.banner-btn .icon-play { display: none; }
.banner[data-paused] .banner-btn .icon-play { display: block; }
.banner[data-paused] .banner-btn .icon-pause { display: none; }

/* The live home slider hides its arrows below 1025px and relies on swipe. The
   portfolio slider does not (see Portfolio). */
@media (max-width: 1024px) { .banner:not(.banner-portfolio) .banner-arrow { display: none; } }

.banner-dots { display: flex; gap: 8px; margin-right: 8px; }
.banner-dot {
  width: 10px; height: 10px; padding: 0;
  border: 1px solid rgba(255, 255, 255, .8);
  border-radius: 50%;
  background: transparent;
  cursor: pointer;
  transition: background .3s ease;
}
.banner-dot[aria-current='true'] { background: #fff; }
.banner-dot:focus-visible { outline: 2px solid #fff; outline-offset: 3px; }

/* ---------- Scroll chevron (a separate WPBakery block on the live page) ---- */

/* Production's box, measured live (ticket 34): `position: fixed` at the
   viewport's bottom, so its percentages resolve against the VIEWPORT, which is
   why these are vw/vh. Content-box (the theme's default), height 6vh as well as
   width 6vw, 1.5vw of padding under the chevron, and `margin-left: -2.5vw`
   on a 6vw box: deliberately 0.5vw right of centre, as production has it.
   Sizing off the width alone in border-box drew the chevron 29px wide at
   390x844 instead of 42. The home hero fills the viewport, so absolute at its bottom
   is the same spot on first paint. */
.scrollarrow-wrapper {
  box-sizing: content-box;
  position: absolute;
  left: 50%;
  bottom: 0;
  z-index: 3;
  width: 6vw;
  min-width: 35px;
  height: 6vh;
  min-height: 35px;
  padding-bottom: 1.5vw;
  margin-left: -2.5vw;
  animation: bounce 10s infinite;
  transition: opacity .4s ease;
}
/* The svg is as tall as the wrapper and SQUARE (its viewBox), not as wide as
   the wrapper: on phones 6vh beats the 35px width, and it overflows to the
   right exactly as production's does. */
.scrollarrow { box-sizing: content-box; display: block; width: auto; height: 100%; padding: 10% 10% 0; }
.scrollarrow polyline { stroke: rgba(255, 255, 255, .5); transition: stroke .4s ease; }
.scrollarrow-wrapper:hover polyline { stroke: #fff; }

@keyframes bounce {
  0%, 10%, 25%, 40%, 50%, 100% { transform: translateY(0); }
  20% { transform: translateY(-20px); }
  30% { transform: translateY(-10px); }
}

/* ==========================================================================
   Home
   ========================================================================== */

.intro { padding-block: 113px 110px; }
.intro .wrap {
  display: grid;
  /* minmax(0, …), not a bare `4fr 8fr`: an fr track's automatic minimum is
     min-content, and the 39px heading's longest word is wider than its third
     of the row, so the bare form gave the left cell 328px where production
     gives 266 — and stole 62px from the lead, which then wrapped an extra
     line. */
  grid-template-columns: minmax(0, 4fr) minmax(0, 8fr);
  align-items: start;
  max-width: calc(var(--wrap) + 2 * var(--gutter));
  padding-inline: calc(var(--gutter) - 15px);
}
.intro .wrap > * { padding-inline: 15px; }
/* The base 16/20 is production's separator. `vertical-align: top` kills the
   ~8px of descender space a height-0 inline-block still reserves, which only
   shows once the intro stacks below 992 and its bottom margin is no longer
   absorbed by the taller lead column (ticket 34). */
.intro .rule { vertical-align: top; }
.intro .lead {
  font-family: var(--font-display);
  font-size: 22px;
  line-height: 35px;
  font-weight: 300;
  /* var(--body), not production's own #7d7d7d — see the token's definition
     for why (ticket 25). */
  color: var(--body);
  margin-bottom: 15px;
}
/* flex, so the button is not a baseline-aligned inline-block sitting 15px
   below its own line box — production's button starts at its paragraph's top. */
.intro-body p:last-of-type { display: flex; margin: 24px 0 0; }

/* THE INTRO STACKS AT 992, NOT 777, AND IT IS THE ONLY ROW ON THE SITE THAT
   DOES. Its two cells are WPBakery `vc_col-lg-4` / `vc_col-lg-8`, and `lg`
   collapses at Bootstrap's 992 -- every other row here is `vc_col-sm-*`, which
   collapses at 768. Measured: at 1024 production still runs two columns (lead
   at x=394, w=502); at 900 it is one (lead at x=66, full 768 measure). */
@media (max-width: 991px) {
  /* Production keeps 113/110 all the way down to 320 — an earlier pass
     invented a narrow-width 70/66 and cost the home page 87px. */
  .intro .wrap { grid-template-columns: 1fr; }
}

/* The two half-and-half panels. On production each side is a table-cell with a
   `cover` background and an 11px inset border. The background position is set
   per row and applies to both cells, copy panel included: 50% 55% for the
   gallery row, 45% 35% for Dale's (measured live, ticket 34). Here the photograph
   is a real <img> (so it can carry alt text and a srcset, ticket 19) and the
   decorative line-art panel behind the copy arrives as `--panel`. */
.split {
  --pos: 50% 55%;
  display: grid;
  grid-template-columns: 1fr 1fr;
}
.split-media,
.split-copy {
  position: relative;
  margin: 0;
}
/* The photograph is absolutely positioned so its own aspect ratio never
   drives the row. On production it is a `cover` background on a cell that
   has its own percentage padding (`item_padding`: 27%/30% for the gallery
   photo, 50%/30% for Dale, at every bracket). That padding is a FLOOR on the
   row height: at 1440 and wider Dale's photo cell is taller than his copy and
   sets the row (580.4, not the copy's 575). Stacked, it is the photo band's
   whole height. Reproduced as padding on the figure (ticket 34). */
.split-media { padding-top: calc(22px + (100% - 22px) * var(--media-h, .57)); }
#tc_home_dale { --pos: 45% 35%; }
#tc_home_dale .split-media { --media-h: .8; }
.split-media img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: var(--pos);
}
/* The 11px inset hairline, which on production is the table-cell's padding
   plus `.edgtf-inside-border`. */
.split-media::after,
.split-copy::after {
  content: '';
  position: absolute;
  inset: 11px;
  border: 1px solid rgba(242, 242, 242, .64);
  pointer-events: none;
  z-index: 1;
}
/* Production's padding is a per-panel PERCENTAGE ladder (the WPBakery
   `item_padding*` shortcode attrs, breakpoints from Ratio's
   elements-holder-item-template.php), resolved against the panel minus its
   11px inset, plus that 11px. The row height is whatever the copy plus this
   padding comes to — production has no min-height. An earlier flat 99px read
   at 1440 was 4px short on top and 4px over on the bottom (ticket 34).
   Fractions, not percents, so calc can scale `100% - 22px` by them. */
.split-copy {
  --pt: .132; --pr: .26; --pb: .12; --pl: .123;
  display: flex;
  flex-direction: column;
  justify-content: center;
  background: var(--panel-grey) var(--panel) var(--pos) / cover no-repeat;
  padding:
    calc(11px + (100% - 22px) * var(--pt))
    calc(11px + (100% - 22px) * var(--pr))
    calc(11px + (100% - 22px) * var(--pb))
    calc(11px + (100% - 22px) * var(--pl));
}
.split-reverse .split-copy { --pr: .18; --pl: .203; }
@media (max-width: 1439px) {
  .split-copy { --pr: .14; }
  .split-reverse .split-copy { --pr: .18; --pl: .143; }
}
@media (max-width: 1280px) {
  .split-copy, .split-reverse .split-copy { --pr: .11; --pl: .123; }
}
@media (max-width: 1024px) {
  .split-copy, .split-reverse .split-copy { --pt: .102; --pr: .11; --pb: .11; --pl: .123; }
}
/* Without this the copy's own padding becomes its min-content width and blows
   the 1fr 1fr apart -- measured 430/470 at 900 where production is 450/450. */
.split-media, .split-copy { min-width: 0; }
.split-copy::after { border-color: rgba(194, 196, 200, .6); }
/* Consecutive prose paragraphs keep production's 25px between them (Meet Dale
   has two); the paragraph holding the button is spaced by the button's own
   50px instead. Zeroing every paragraph's margin here had dropped Meet Dale's
   paragraph break entirely (ticket 34). */
.split-copy p { font-size: 18px; line-height: 26px; margin-bottom: 0; }
.split-copy p + p:not(:has(> .btn)) { margin-top: 25px; }
.split-copy h2 + .rule { margin: 24px 0 25px; }
.split-copy .btn { align-self: flex-start; margin-top: 50px; }
/* Production's own spacer above this button is a 24px `vc_empty_space`, not
   the 50px one Project Gallery has — content, not a ladder. */
#tc_home_dale .btn { margin-top: 24px; }

/* Stacked, the cells keep DOM order: gallery photo then copy, Dale's copy
   then photo — matching production. An earlier `order: -1` on the photo put
   Dale's picture above his copy (ticket 34). The copy ladder continues with
   production's 600_768 / 480_600 / ≤480 brackets, the same for both panels. */
@media (max-width: 777px) {
  .split, .split-reverse { grid-template-columns: 1fr; }
  .split-copy, .split-reverse .split-copy { --pt: .102; --pr: .10; --pb: .09; --pl: .103; }
}
@media (max-width: 600px) {
  .split-copy, .split-reverse .split-copy { --pt: .10; --pr: .08; --pb: .07; --pl: .10; }
}
@media (max-width: 480px) {
  .split-copy, .split-reverse .split-copy { --pt: .07; --pr: .06; --pb: .06; --pl: .06; }
}

/* ==========================================================================
   Company
   ========================================================================== */

/* The white card overlaps the banner above it, and by how much is a LADDER,
   not the single -110px an earlier pass read at 1440: Ratio's
   `.edgtf-row-overlap` steps −150 / −110 ≤1500 / −70 ≤1024 / −50 ≤768 / 0 ≤600
   (parent theme `modules-responsive.css`). The card is the Bootstrap column
   plus another 30px each side — --wrap + 60 — with 35px of its own padding, so
   its two 15px-padded cells still land on the --wrap column. Measured live:
   828px wide at x=98 at 1024, 1360 at 1440. */
.two-up {
  position: relative;
  z-index: 2;
  box-sizing: border-box;
  display: grid;
  grid-template-columns: 1fr 1fr;
  --two-up-w: calc(var(--wrap) + 60px);
  width: var(--two-up-w);
  max-width: none;                /* the card is WIDER than .wrap, not capped by it */
  margin-top: -150px;
  /* `margin-inline: auto` does NOT center a box wider than its containing
     block — the auto-margin over-constraint rule resolves both to 0, which
     left-aligns it instead (confirmed against a minimal repro). This box is
     deliberately wider than the viewport at every width down to 390 (see the
     comment below), so it needs a centering method that still bleeds evenly
     when overflowing: `left: 50%` plus a negative half-width margin. */
  left: 50%;
  margin-left: calc(var(--two-up-w) / -2);
  padding: 50px 35px;
  background: #fff;
}
@media (max-width: 1500px) { .two-up { margin-top: -110px; } }
@media (max-width: 1024px) { .two-up { margin-top:  -70px; } }
@media (max-width:  768px) { .two-up { margin-top:  -50px; } }
@media (max-width:  600px) { .two-up { margin-top:      0; } }

/* The card's OVERHANG is a second, independent ladder — Ratio's
   `.edgtf-section .edgtf-row-box` — and it is not the same set of breakpoints
   as the overlap above:
       >=1024   -30px  a side  ->  --wrap +  60,  35px padding
        <1024  -100px  a side  ->  --wrap + 200,  35px padding
        <= 768    -9%  a side  ->  --wrap x 1.18,  7% of --wrap padding
   Measured live: 1360 wide at 1440, 828 at 1024, 968 at 900, 708 at 768,
   354 at 390. */
@media (max-width: 1023px) { .two-up { --two-up-w: calc(var(--wrap) + 200px); width: var(--two-up-w); } }
/* The cells are Bootstrap `md`, so they stack at 991 — not at 768 with the
   rest of the site, and not at 777. At 900 production is already one column
   with a 550px photograph centred over an 868px measure. */
@media (max-width:  991px) { .two-up { grid-template-columns: 1fr; } }
@media (max-width:  768px) {
  .two-up {
    --two-up-w: calc(var(--wrap) * 1.18);
    width: var(--two-up-w);
    padding-inline: calc(var(--wrap) * .07);
  }
}
.two-up-item { padding-inline: 15px; }
.two-up-item img {
  width: 550px;
  max-width: 100%;
  aspect-ratio: 1;
  object-fit: cover;
  margin: 0 auto 29px;
}
.two-up-item h2 { margin-bottom: 15px; }
/* On production the Residential/Commercial prose sits in divs, not <p>, which
   is how a source-parsing extractor lost it (ticket 19). It is body grey like
   the rest of the page's prose, not accent-coloured (measured live, ticket 34). */
.tc_company_description {
  /* 16/28 with 16px between blocks — NOT the page's 14/26 with 25px, which is
     what an earlier pass assumed and which cost this column 13px on the left
     and 45px on the right. */
  font-size: 16px;
  line-height: 28px;
  /* Production follows the prose with a 40px spacer inside the card. */
  margin-bottom: 40px;
}
.tc_company_description p { margin-bottom: 16px; }
.tc_company_description p:last-child { margin-bottom: 0; }



/* The gold "Why Terrel Construction?" band straddles the dark panel below it:
   it starts 37px UP into the white card above (production does that with a
   zero-height spacer carrying margin-top:-37px) and the dark panel then starts
   53px up into the band. flow-root, or those negative margins collapse out of
   the section and take the whole block with them. */
.why { position: relative; display: flow-root; }
.why-banner {
  position: relative;
  z-index: 2;
  box-sizing: border-box;
  width: min(var(--wrap), 100% - 2 * var(--gutter));
  margin: -37px auto -53px;
  /* 38px 15px, and the type is 30px on a 26px line — so the band is 102 tall,
     not the 70 an earlier pass had. */
  padding: 38px 15px;
  background: linear-gradient(to right, #c0954b, #e7bd74);
  color: #fff;
  /* Open Sans 400, measured live (ticket 34). */
  font-family: var(--font-body);
  font-size: 30px;
  font-weight: 400;
  line-height: 26px;
  letter-spacing: normal;
  text-align: center;
  text-transform: none;
}
/* Ratio adds 10px under call-to-action text at 600px and below (meant for a
   button that this band does not have, but it still makes the band 10px
   taller there — ticket 34). */
@media (max-width: 600px) { .why-banner { padding-bottom: 48px; } }
/* The hairline sits 10px inside the band, it does not border it. */
.why-banner::before {
  content: '';
  position: absolute;
  inset: 10px;
  border: 1px solid rgba(255, 255, 255, .7);
}

.why-grid {
  position: relative;
  /* THREE COLUMNS ONLY AT 1200 AND UP. The cells are
     `vc_col-sm-12 vc_col-md-12 vc_col-lg-4`, so Bootstrap's md (≥992) holds
     them full width until lg (≥1200) takes over — at 1024 production stacks
     them, where this stylesheet was still running three columns. */
  display: grid;
  grid-template-columns: 1fr;
  padding-block: 166px 76px;
  padding-inline: calc(var(--gutter) - 15px);
  /* Production paints all three textured bands (this one and the footer's
     two) at native size, tiled, from `0 0` of a row that overhangs the page by
     15px — so the texture starts at x = -15. Not cover/centre, and with no
     darkening overlay: the image is already ~#2c2c2c (ticket 34). */
  background: #262626 url('/img/parallax-7.jpg') -15px 0 repeat;
  color: #fff;
  max-width: none;
}
@media (min-width: 1200px) {
  .why-grid { grid-template-columns: repeat(3, 1fr); }
}
.why-item {
  position: relative;
  display: grid;
  grid-template-columns: 50px 1fr;
  align-content: start;
  /* 15px in from the cell on the left, 11% on the right — production's
     `.vc_column-inner { padding-right: 11% }`, which is what keeps the stacked
     text off the right edge at every width below 1200. */
  padding-left: 15px;
  padding-right: 11%;
  /* The 40px spacer production puts after EVERY item, the last one included —
     so it is the item's own padding, not the grid's row-gap. Dropping it left
     the dark panel 40px short in both the stacked and the three-up mode. */
  padding-bottom: 40px;
}
/* A 30px icon font's em box, white, as production (ticket 34). The handshake
   glyph is 2304/1792 em wide, so its box is 38.57px. */
.why-icon { width: 30px; height: 30px; color: #fff; grid-row: 1; }
.why-icon-wide { width: 38.571px; }
/* The two linea fonts' ascent is 528/512 em, not the 480 the em box assumes,
   so production draws their glyphs 2px lower than FontAwesome's. */
.why-icon:not(.why-icon-wide) { transform: translateY(2px); }
.why-item h3 { color: #fff; margin-bottom: 13px; align-self: center; }
.why-item p { grid-column: 2; font-size: 14px; line-height: 26px; color: #fff; }

/* Heading block, then the photographs. The 60px below the rule is this
   section's padding-bottom on production, and the 70px under the last row of
   photographs is the last photo row's — both are reproduced here because the
   photographs live inside this section rather than in three sibling rows. */
.communities { padding-block: 104px 70px; }
.communities h2 { text-align: center; }
.communities .section-subtitle {
  margin: 15px 0 0;
  padding-inline: 19%;
  /* Production's is ink, not the muted grey of the gallery's subtitle
     (measured live, ticket 34). */
  color: var(--ink);
  font-size: 18px;
  line-height: 27px;
}
.communities .rule { margin: 16px 0 76px; }

/* 11 photographs in rows of 4, 4 and 3 — the last row stretches to thirds on
   production because it is a table with three cells, not a four-column grid
   with an empty slot. A 12-track grid reproduces both widths exactly.
   No column gap: production's cells are table-cells and butt up against each
   other. The 20px row gap and the tile geometry are production's too. */
.community-grid {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  column-gap: 0;
  row-gap: 20px;
  margin: 0;
  padding: 0;
  list-style: none;
}
/* A tile is NOT a fixed aspect ratio. Its height is 10% of its own width top
   and bottom (production's `.edgtf-elements-holder-item-content { padding:10% }`)
   around a name that carries `margin-top: 150px` — so a 192px tile is 214 tall
   and a 256px tile is 227, which no single aspect-ratio can express. */
.community {
  position: relative;
  box-sizing: border-box;
  grid-column: span 3;
  padding: 10%;
  padding-top: calc(10% + 150px);
}
.community:nth-child(n + 9) { grid-column: span 4; }
.community img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Production's tiles are `background-size: cover` at the default
     `background-position: 0 0` — anchored top-LEFT, measured live (ticket 34).
     Centre-top fixed the portrait Niwot photo but shifted the landscape ones
     sideways, which is why an earlier attempt looked tile-specific. */
  object-position: 0 0;
}
.community-name {
  position: relative;
  display: block;
  color: #fff;
  /* Open Sans 400, no tracking — measured live (ticket 34), not Raleway. */
  font-family: var(--font-body);
  font-size: 20px;
  font-weight: 400;
  line-height: 26px;
  text-shadow: 1px 1px 2px #1f1f1f;
}

@media (max-width: 768px) {
  /* Ratio's `edgtf-responsive-mode-768` takes the tiles to one per row here —
     there is no intermediate two-up stage on this site. The 20px gap does NOT
     come with them: production's three rows are three separate sections, so
     stacked there is space after the 4th and 8th tile and nowhere else. A
     uniform row gap added 160px to this page at 390. */
  .community, .community:nth-child(n + 9) { grid-column: span 12; }
  .community-grid { row-gap: 0; }
  .community:nth-child(4), .community:nth-child(8) { margin-bottom: 20px; }
}

/* ==========================================================================
   Experience
   ========================================================================== */

.story {
  display: grid;
  grid-template-columns: 5fr 7fr;
  align-items: start;
  margin-block: 80px;
  padding-inline: calc(var(--gutter) - 15px);
}
.story > * { padding-inline: 15px; }
.story-alt { grid-template-columns: 7fr 5fr; }
/* The two insets are percentages of their own column, not the fixed pixels
   read at 1440: production runs `.tc_textcol { padding: 0 2% }` and
   `.tc_photocol { margin: 0 6% }` on top of the 15px cell padding, and 2% of
   the ROW is 4.8% of a 5/12 column. Fixed 42px/29.7px held at 1440 and cost
   the copy 21px of measure at 1024 — which is a wrapped line per paragraph on
   the page with the most prose on the site. */
/* Geometry below re-measured against production at 1440/1024/900/390
   (ticket 34). The figure is production's whole photo column: 15px cell
   padding, then the <picture> is `.vc_single_image-wrapper` — the photo's
   frame — and the figcaption is the caption block that sits OUTSIDE
   `.tc_photocol`'s 6% inset, so it spans the full cell. */
.story-media { order: 2; margin: 0; text-align: center; }
.story-alt .story-media { order: 0; }
.story-copy { order: 1; padding-inline: calc(15px + 4.8%); }
/* vertical-align: top removes the ~8px baseline/descender strut an
   inline-block with height:0 otherwise inherits from its line box — with
   only `margin` set, the rule measured 8px taller than production on every
   story row. */
.story-copy .rule { margin: 16px 0; vertical-align: top; }
/* The frame is an inline-block with the photo as a block inside it, so no
   line-box strut opens a gap under the photo. `.tc_photocol { margin: 0 6% }`
   only ever narrows the photo, and the frame is centred, so a 88% cap is the
   same thing. It holds stacked too: production keeps the inset at 390. */
.story-media picture,
.milestones picture {
  display: inline-block;
  vertical-align: top;
  max-width: 100%;
}
/* border-box: the inset holds the whole frame, mat included, as
   `.vc_single_image-wrapper` does — not the photo inside it. */
.story-media picture { max-width: 88%; box-sizing: border-box; }
.story-media img,
.milestones img {
  display: block;
  max-width: 100%;
  height: auto;
}
/* Production mats these photos inconsistently (Ratio's vc_box_shadow_border
   vs a bare vc_box_border_grey that adds no visible border at all) — half
   the story/milestone photos are flush, half carry a mat. Matched exactly
   rather than applied uniformly; see ticket 28. The mat is a 12px clear
   (white) frame with a soft shadow, and the photo inside casts the same
   shadow again — not a grey fill (ticket 34). */
.matted picture,
.matted img {
  box-shadow: 0 0 5px rgba(0, 0, 0, .1);
}
.matted picture { padding: 12px; }
/* Ratio's custom-font caption: Open Sans at weight 100, which renders as the
   lightest face loaded (300). 5px above, and a 25px bottom margin that is
   what spaces the caption from whatever follows when stacked (ticket 34). */
.story-media figcaption,
.milestones figcaption {
  margin: 5px 0 25px;
  font-family: var(--font-body);
  font-size: 16px;
  font-weight: 300;
  line-height: 26px;
  color: var(--body);
  text-align: center;
}
.story-copy p { font-size: 16px; line-height: 32px; margin: 20px 0 0; }

.milestones {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  margin-block: 80px;
  padding-inline: calc(var(--gutter) - 15px);
}
.milestones figure { margin: 0; padding-inline: 15px; }

@media (max-width: 768px) {
  /* Rows stay 80px apart stacked; inside a row the caption's own 25px is the
     only gap (ticket 34). */
  .story, .story-alt { grid-template-columns: 1fr; }
  .story-media, .story-alt .story-media { order: -1; }
  /* Stacked, the column IS the row, so 2% is 2% again. */
  .story-copy { padding-inline: calc(15px + 2%); }
  .milestones { grid-template-columns: 1fr; }
}

/* ==========================================================================
   Gallery — two fixed sections, Residential then Commercial. There is no
   filter UI on the live site (both shortcodes carry filter="no"), so there is
   nothing to replace (ticket 19).
   ========================================================================== */

/* SHAPE. Both blocks are a full-bleed #f5f5f5 page band with the content
   riding on a WHITE CARD the width of the Bootstrap column (--wrap + 30px), so
   grey shows as side bands — not, as an earlier pass had it, a grey section
   edge to edge. The Residential card additionally rides UP over the banner:
   that is Ratio's `.edgtf-row-overlap`, whose negative margin steps down its
   own ladder, and its padding is 104px top / 59px bottom. The Commercial card
   has neither. All of it measured live at 1440/1024/900/390. */
.projects {
  background: var(--panel-grey);
  /* flow-root, or the card's negative margin collapses out and drags the grey
     band up over the banner with it — the side bands would then paint over the
     banner's bottom 70px. The band must stay put while the card alone rides. */
  display: flow-root;
}
.projects h2 { text-align: center; }

.projects-card {
  position: relative;             /* paints over the banner it overlaps */
  box-sizing: border-box;
  width: calc(var(--wrap) + 30px);
  margin-inline: auto;
  padding-inline: 15px;           /* so card CONTENT lands on the --wrap column */
  background: #fff;
}
.projects-shaded .projects-card { margin-top: -150px; padding-block: 104px 59px; }
@media (max-width: 1500px) { .projects-shaded .projects-card { margin-top: -110px; } }
@media (max-width: 1024px) { .projects-shaded .projects-card { margin-top:  -70px; } }
@media (max-width:  768px) { .projects-shaded .projects-card { margin-top:  -50px; } }
@media (max-width:  600px) { .projects-shaded .projects-card { margin-top:      0; } }

.project-grid {
  display: grid;
  /* Production's card is `calc(33.33% - 25px * 2 / 3)` of the 1300px holder —
     33.33%, not a third, so 0.12px narrower than 1fr; over three rows of
     square photos that made /gallery/ 0.4px taller and round up a pixel
     (ticket 34). The holder is this grid's content box plus its 16.6px
     padding. At two columns production's calc(50% - 12.5px) equals 1fr. */
  grid-template-columns: repeat(3, calc((100% + 16.6px) * .3333 - 25px * 2 / 3));
  padding-block: 0;
  /* 53px of dead space under the last row, and it is not a design choice:
     production lays the cards out as inline-blocks, so the bottom row keeps
     its own 27px margin-bottom AND the container carries a 26px line-height
     strut. Grid drops both, which shortened every gallery block by 53px. */
  padding-bottom: 53px;
  padding-inline: 8.3px;
  column-gap: 16.6px;
  row-gap: 27px;
  margin: 0;
  list-style: none;
}
.project-card a { display: block; text-decoration: none; }
.project-card img {
  width: 100%;
  aspect-ratio: 1;
  object-fit: cover;
}
.project-title {
  display: block;
  padding-top: 17px;
  color: var(--ink);
  font-size: 17px;
  line-height: 26.4444px;
  letter-spacing: 1px;
  text-transform: uppercase;
}
.project-categories {
  margin: 0;
  color: var(--accent-text);
  font-family: Georgia, serif;
  font-size: 14px;
  font-style: italic;
  line-height: 20px;
}

/* The grid drops to 2 columns at 768 and 1 at 600 — production's boundaries,
   not this stylesheet's 777. The card margins step with them (8.3 → 6.25 → 0),
   which is what keeps three 239px cards on a 768px measure. */
@media (max-width: 768px) {
  .project-grid { grid-template-columns: repeat(2, 1fr); padding-inline: 6.25px; column-gap: 12.5px; }
}
@media (max-width: 600px) {
  /* 79px, not 53: at one column the trailing whitespace text node after the
     last card can no longer share that row's line box and makes one of its
     own, so production gains a second 26px strut. Measured at 600 and 390. */
  .project-grid { grid-template-columns: 1fr; padding-inline: 0; column-gap: 0; padding-bottom: 79px; }
}

/* ==========================================================================
   Portfolio (a single /gallery/<slug>/ page)
   ========================================================================== */

/* The full-bleed carousel IS this page's content (ticket 24). Production
   gives it a fixed height rather than 100vh. */
.banner-portfolio {
  height: 780px;
  min-height: 0;
}

/* The title sits in a translucent plate over the bottom-left of the carousel,
   not in a heading below it. Ratio's `.edgtf-portfolio-slider-content`, as
   measured live (ticket 36): a FIXED height with the title centred in it,
   padding that is a percentage of the banner WIDTH (2% / 2.5%), and a width
   that is the title's own — so "The Farm in Boulder Valley" gets a wide plate
   on one line, not a 360px box it wraps in. */
.portfolio-plate {
  position: absolute;
  left: 0;
  bottom: 0;
  z-index: 4;
  box-sizing: border-box;
  display: flex;
  align-items: center;
  height: 170px;
  padding: 2% 2.5%;
  background: rgba(255, 255, 255, .7);
}
.portfolio-plate h1 {
  margin: 0;
  padding: 0 40px 13px 41px;
  font-size: 33px;
  line-height: 40px;
}

/* The expand control and its panel. Open, the panel replaces the title and
   the plate grows upward to fit it (842 x 250 at 1440, live). */
.portfolio-info summary {
  position: absolute;
  top: 10px;
  right: 10px;
  width: 19px;
  height: 19px;
  list-style: none;
  color: #000;
  cursor: pointer;
}
.portfolio-info summary::-webkit-details-marker { display: none; }
.portfolio-info summary:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
.portfolio-info svg { display: block; width: 19px; height: 19px; fill: currentColor; }
.portfolio-info[open] .icon-expand,
.portfolio-info:not([open]) .icon-condense { display: none; }

.portfolio-plate:has(.portfolio-info[open]) { height: auto; min-height: 170px; }
.portfolio-plate:has(.portfolio-info[open]) h1 { display: none; }
.portfolio-info-body { width: min(770px, 80vw); padding-block: 0 16px; }
.portfolio-facts {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 16px 0;
  margin: 0 0 9px;
}
.portfolio-facts dt { font-size: 17px; line-height: 26px; color: var(--ink); }
.portfolio-facts dd { margin: 0; font-size: 15px; line-height: 26px; color: var(--body); }
.portfolio-info-title { margin: 23px 0 0; font-size: 21px; line-height: 30px; color: var(--ink); }

/* Ratio's portfolio Slick controls, as drawn live (ticket 36): a centred row of
   15px dots 15px + 3% (5% at <= 1024) off the bottom, wrapping on phones, and
   FontAwesome circle arrows 45px in from each side at mid-height — at every
   width, unlike the home hero's. The pause toggle is the WCAG 2.2.2 addition
   (production autoplays too) and stays bottom-right; it is registered in
   tests/screenshot.css. */
.banner-portfolio .banner-controls {
  inset: 0;
  display: block;
  padding: 0;
  pointer-events: none;
}
.banner-portfolio .banner-dots {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 15px;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0;
  margin: 0 0 3%;
}
.banner-portfolio .banner-dot {
  width: 15px;
  height: 15px;
  margin: 3px 8px 8px;              /* 26px rows, as live */
  border: 0;
  border-radius: 15px;
  box-shadow: inset 0 0 0 1px #fff;
  pointer-events: auto;
}
.banner-portfolio .banner-arrow {
  position: absolute;
  top: 50%;
  width: 46px;
  height: 54px;
  margin-top: -27px;
  padding: 0;
  border-radius: 0;
  background: none;
  color: #fff;
  pointer-events: auto;
}
.banner-portfolio .banner-arrow svg { width: 46px; height: 54px; fill: currentColor; stroke: none; opacity: .75; }
.banner-portfolio .banner-arrow:hover { background: none; }
.banner-portfolio .banner-arrow:hover svg { opacity: 1; }
.banner-portfolio .banner-prev { left: 45px; }
.banner-portfolio .banner-next { right: 45px; }
.banner-portfolio .banner-toggle {
  position: absolute;
  right: 20px;
  bottom: 22px;
  pointer-events: auto;
}

/* 40px on top and NOTHING underneath at every width: the grid already carries
   its own 53px of inline-block trailing space, so a padding-bottom here is
   double-counting — it was 60px past production on all 8 portfolio pages. */
.related { padding-block: 40px 0; max-width: none; }
.related h2 { text-align: center; margin-bottom: 46px; }
.related .project-card img { aspect-ratio: 4 / 3; }
/* Ratio insets the related cards' titles 6px a side and their categories 4px
   on the left (ticket 34). */
.related .project-title { padding-inline: 6px; }
.related .project-categories { padding-left: 4px; }

/* The bottom bar: previous project left, back-to-gallery centred, next right. */
.portfolio-nav {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  padding: 39px max(var(--gutter), calc((100% - var(--wrap)) / 2));
  background: var(--panel-grey-2);
}
.portfolio-prev { grid-column: 1; justify-self: start; }
.portfolio-back { grid-column: 2; justify-self: center; margin: 0; }
.portfolio-next { grid-column: 3; justify-self: end; }
.portfolio-nav a { display: block; line-height: 22px; color: #000; text-decoration: none; }
.portfolio-nav a:hover { color: var(--accent); }
/* display:block, or the inline svg sits on a line box and its 6px descender
   makes the whole bar 106 tall against production's 100. */
.portfolio-nav svg { display: block; width: 22px; height: 22px; }
/* linea's arrows at Ratio's 25px; the em box overhangs the 22px row, and
   linea's 516/512 ascent drops the glyph a further 3px (measured, ticket 34). */
.portfolio-nav .portfolio-arrow { width: 25px; height: 25px; margin-block: -1.5px; transform: translateY(3px); }
/* Ratio's back button: nine 6px squares, 1px #989898 outlines, 8px pitch. */
.portfolio-back a { color: #989898; }

/* At and below 1024 the portfolio carousel becomes a real full-viewport
   slider (measured 900 at viewport height 900, 700 at 700 -- it tracks vh,
   unlike the inner-page banner above), and the title plate moves from the
   bottom-left to the TOP-left at roughly half the size. */
@media (max-width: 1024px) {
  .banner-portfolio { height: 100vh; min-height: 360px; }
  .portfolio-plate { top: 0; bottom: auto; height: 100px; }
  .portfolio-plate h1 { padding: 0 16px; font-size: 22px; line-height: 26.6667px; }
  .banner-portfolio .banner-dots { margin-bottom: 5%; }
}

@media (max-width: 777px) {
}

/* ==========================================================================
   Contact
   ========================================================================== */

.contact-band { background: var(--panel-grey-2); }
.contact {
  display: grid;
  grid-template-columns: 4fr 8fr;
  align-items: start;
  padding-block: 30px 50px;
}
/* The column insets are PERCENTAGE-derived, not the fixed 28/73px read at
   1440: production's `.vc_column-inner` carries `margin: 0 3%` of its own
   column plus 15px of padding, and its content is exactly `column - 30` wide,
   overflowing the cell on the right. A grid item's percentages resolve against
   its own GRID AREA, not the container, so 3% is 3% of the column in both the
   two-up and the stacked case. Verified at 1440/1024/900/390. */
.contact-info,
.contact-main {
  padding-left: calc(3% + 15px);
  padding-right: 0;
  margin-right: calc(15px - 3%);
}
/* Production spaces these blocks with WPBakery `vc_empty_space` rows; here it
   is plain margin. The 80px under the hours is two of them — a 40px spacer
   plus the heading's own 40px — which collapse to one 80 rather than to 40. */
/* The hours are held to a 40%% measure (production: `padding-right: 60%%` on
   the text column), which is invisible at 1024 where the block is two lines
   either way and worth 78px at 390 where it is five lines against two. */
.contact-main .hours { margin-top: 10px; margin-bottom: 80px; padding-right: 60%; }
.contact h2 { margin-top: 40px; }
.contact p { margin-block: 17px 25px; }
/* A grid item is its own block formatting context, so the last paragraph's
   bottom margin does NOT collapse out the way it does on production — it made
   both columns 25px taller, which only shows once they stack. */
.contact-info > :last-child, .contact-main > :last-child { margin-bottom: 0; }

/* `.contact .phone`, not `.phone`: production puts 10px between the number
   and the hours, and the generic `.contact p` 25px would otherwise win. */
.contact .phone { display: flex; align-items: center; gap: 14px; margin: 17px 0 0; }
.phone-icon { width: 24px; height: 24px; flex: none; color: var(--accent); }
/* Production's gold (#d6ab60) is 2.1:1 here and fails AA even at 25px;
   --accent-text keeps it reading as the accent (user's call, ticket 34). */
.phone a { font-size: 25px; line-height: 33px; text-decoration: none; color: var(--accent-text); }
.phone a:hover { color: var(--accent); }

@media (max-width: 768px) {
  /* No row gap: stacked, production's second column starts exactly where the
     first ends. */
  /* minmax(0, …): a bare 1fr track grows to the form fields' intrinsic width,
     which made the column 391px wide in a 390px viewport (ticket 34). */
  .contact { grid-template-columns: minmax(0, 1fr); }
}

/* ---------- The form ---------- */

/* Production draws the form in a rounded box (CF7's `.wpcf7` wrapper: 2px
   #d7d7d7, 10px radius, 30px padding). The earlier 32px side inset was that
   box's padding plus border with the box itself never drawn (ticket 34). The
   49px above is likewise re-split: 17px of margin outside, 32 inside. */
.contact-form {
  margin-top: 17px;
  padding: 30px;
  border: 2px solid #d7d7d7;
  border-radius: 10px;
}
.contact-form .field { margin: 0 0 9px; }
.contact-form label {
  display: block;
  /* #aaa (production's value) is 2.32:1 on white — axe-core hard fails it
     (ticket 25). var(--body) clears 4.5:1. */
  color: var(--body);
  font-size: 16px;
  line-height: 26px;
}
.contact-form input,
.contact-form textarea {
  display: block;
  width: 100%;
  padding: 16.5px 15px;
  margin-bottom: 9px;
  background: #fff;
  border: 1px solid var(--rule);
  border-radius: 0;
  font-family: var(--font-display);
  font-size: 16px;
  line-height: 20px;
  color: var(--body);
}
.contact-form textarea { height: 150px; resize: vertical; }
.contact-form input:focus,
.contact-form textarea:focus { outline: 2px solid var(--accent); outline-offset: -1px; }
/* Production's Send sits 48px under the message field. Here that is the
   button's paragraph's 17px top margin (`.contact p`, collapsing with the
   field's 9) plus this 31. Measured with the Turnstile widget hidden, as the
   visual suite sees it — with it shown, its own margin collapses in instead
   (ticket 34). */
.contact-form .btn {
  margin-top: 31px;
  padding: 0 25px;
  height: 50px;
  font-family: var(--font-display);
  font-size: 13px;
  font-weight: 500;
  line-height: 45px;
  text-align: center;
}
.contact-form .btn::after { content: none; }

/* A real field name a bot will want to fill, hidden from people and from
   assistive tech. Deliberately not `display:none` — some bots skip those. */
.honeypot {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

.cf-turnstile { margin-bottom: 20px; }

/* Nothing is stored (ticket 11), so a failed send is a lost lead unless the
   visitor acts — which is why the error state is loud and names the phone. */
.form-status { margin-top: 20px; padding: 14px 18px; font-size: 15px; line-height: 24px; }
.form-status[hidden] { display: none; }
.form-success { background: #eef6ee; color: #2f5d33; }
.form-error { background: #fbeeee; color: #8a2b2b; }
.form-error a { font-weight: 700; }

/* ==========================================================================
   Landing pages — dormant (ticket 09 builds zero of them today), but they
   share this stylesheet so a page re-enabled in 2029 inherits every
   intervening fix.
   ========================================================================== */

.landing {
  display: grid;
  grid-template-columns: 7fr 5fr;
  column-gap: 40px;
  align-items: start;
  padding-block: 80px;
}
.landing h2 { margin-block: 40px 15px; }
.landing h3 { margin-block: 30px 10px; }
.landing .phone a { font-size: 25px; }

@media (max-width: 777px) {
  .landing { grid-template-columns: 1fr; row-gap: 40px; padding-block: 50px; }
}

/* ==========================================================================
   Footer CTA — site-wide, on every page except /contact/.
   ========================================================================== */

.footer-cta-communities {
  margin: 0;
  /* The measure is 64% of the Bootstrap column, not a fixed width: the child
     theme sets `padding: 0 18%` on this band's `.vc_column-inner`, and that
     column is --wrap + 30px (see the grid note above). Reading it at 1440 only
     gives 852px, which then holds three lines at every width — production runs
     three lines at 1440 and *five* at 1024, because the measure shrinks with
     the ladder. Verified live at 1600/1440/1200/1024/900/777/600/480/390.
     The band itself stays full-bleed. */
  padding: 124px max(var(--gutter), calc((100% - (var(--wrap) + 30px) * .64) / 2)) 127px;
  /* Native size, tiled, from x = -15: see .why-grid (ticket 34). */
  background: var(--accent) url('/img/home-10-background-image-small.jpg') -15px 0 repeat;
  color: #fff;
  font-family: var(--font-display);
  font-size: 22px;
  font-weight: 400;
  line-height: 35px;
  text-align: center;
}

.footer-cta-panel {
  position: relative;
  /* Native size, tiled, from x = -15: see .why-grid (ticket 34). */
  background: #262626 var(--cta-image) -15px 0 repeat;
  color: #fff;
}
.footer-cta-inner {
  position: relative;
  display: grid;
  grid-template-columns: 8fr 4fr;
  align-items: start;
  padding-block: 60px;
  padding-inline: calc(var(--gutter) - 15px);
  max-width: calc(var(--wrap) + 2 * var(--gutter));
}
.footer-cta-inner > * { padding-inline: 15px; }
.footer-cta-copy h2 { color: #fff; }
/* display:block, or the inline-block rule sits on a line box and the 26px
   descender under it pushed the copy 8px down on all 12 pages that carry this
   panel. 47px rather than 32 because as a block it now COLLAPSES with the
   paragraph's 15px top margin instead of adding to it — production, with the
   rule in its own holder div, gets 32 + 15. */
.footer-cta-copy .rule { display: block; margin: 25px 0 47px; }
.footer-cta-copy p { margin: 15px 0 0; font-size: 18px; line-height: 26px; }
.footer-cta-action { margin: 65px 0 0; }

@media (max-width: 768px) {
  /* No narrow-width override for .footer-cta-communities: production keeps
     22px/35px and 124/127 padding at every width down to 320, and only the
     measure moves. An earlier guess shrank the type here.

     Stacked, the panel keeps its 60px padding and the button keeps its 65px
     top margin — production simply drops the two cells one under the other
     with no gap. Zeroing the margin and adding a 30px row gap, as this
     stylesheet did, cost 56px on all 12 pages that carry the panel. */
  .footer-cta-inner { grid-template-columns: 1fr; row-gap: 0; padding-block: 60px; }
  .footer-cta-action { text-align: left; }
}

/* ==========================================================================
   Footer
   ========================================================================== */

.site-footer { background: #000; }
.footer-inner { padding-block: 18px; }
.footer-inner .copyright {
  margin: 0 0 25px;
  color: var(--accent);
  font-size: 12px;
  line-height: 26px;
}

/* ==========================================================================
   Motion
   ========================================================================== */

@media (prefers-reduced-motion: reduce) {
  * { transition: none !important; }
  .banner-slide,
  .banner-slide[data-current],
  .banner-slide[data-leaving] { transition: opacity .3s ease; transform: none; }
  .banner-slide[data-current] .banner-title,
  .banner-slide[data-current] .banner-sub { animation: none; }
  .scrollarrow-wrapper { animation: none; }
}
