/* ==========================================================================
   SOUTHERN STANDARD SYSTEMS — DESIGN SYSTEM
   Tokens and type scale. Every page links this file.
   No hardcoded color values anywhere else.
   ========================================================================== */

/* FONTS: loaded per page via <link rel="preconnect"> + <link rel="stylesheet"> in
   <head>, not @import — @import blocks rendering until the sheet resolves.
   Every page that links this file MUST also carry these three tags:
     <link rel="preconnect" href="https://fonts.googleapis.com">
     <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
     <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Archivo:wght@400;500;600;700;800;900&display=swap">
   Without them --font falls back to system-ui. */

/* ---- TOKENS -------------------------------------------------------------- */
:root{
  /* grounds */
  --ox:        #4A1219;               /* oxblood — page ground */
  --ox-2:      #5C171F;               /* cards, raised surfaces */
  --ox-deep:   #2B0A0E;               /* CTA sections, footer */
  /* Warmed from #231812 / #2E1F18. The old pair were brown by name and black on
     screen: #231812 is 35/24/18, a 17-point spread across the channels, which
     next to a saturated oxblood ground reads as "the dark one", not as brown.
     Half the bands on this site use them, so the site had a brown half nobody
     could see. Same hue, more chroma and a few points of lift. Still the darkest
     grounds in the palette and still well past AA with --sand on top (11:1). */
  --brown:     #2C1D12;               /* alternating slabs */
  --brown-2:   #3B2718;               /* nested surfaces on brown */

  /* ink */
  --sand:      #F2E7D8;               /* primary ink */
  --sand-2:    rgba(242,231,216,.76); /* body copy */
  /* MUTED LABELS. .58 and not .50, raised 2026-10-09. It was labelled LARGE
     TEXT ONLY at 3.6:1, and then every one of its 15 uses was small text,
     10.5px to 13.5px across 7 sheets, so the label described an intent no rule
     ever honoured and Lighthouse was failing the footer. .58 is the lowest
     value that clears 4.5:1 on all 5 dark grounds this token lands on:
     --ox-deep 5.59, --ox 4.98, --ox-2 4.54, --brown 5.37, --brown-2 4.93.
     --ox-2 is the tight one, so check it first if a ground is ever added. */
  --sand-3:    rgba(242,231,216,.58);
  --line:      rgba(242,231,216,.16); /* hairlines, borders */
  --accent:    #E9C9B4;               /* blush sand — links, small marks, kickers */

  /* INVERTED / LIGHT SURFACE — for slabs that sit on cream instead of oxblood.
     The dark tokens above are unreadable there, so light slabs get their own
     ink. Contrast on --cream: --ink 14.9:1, --ink-2 7.7:1, --ink-3 4.5:1 —
     all clear AA for normal text. The brand red on cream is --ox, the same
     oxblood the hero is built on (12.5:1). */
  --cream:     #F2E7D8;               /* light ground */
  --ink:       #2B0A0E;               /* headings on cream */
  --ink-2:     #5A4038;               /* body copy on cream */
  --ink-3:     #7A6459;               /* labels on cream — 4.5:1, safe at 10px */
  --hair-d:    rgba(43,10,14,.14);    /* hairlines on cream */
  --red:       #C8102E;               /* LEGACY crimson — only the not-yet-rebuilt
                                         pages use it. New work uses --ox. */

  --ease:      cubic-bezier(.22,1,.36,1);

  --font:      'Archivo', system-ui, -apple-system, 'Segoe UI', sans-serif;
  --wrap:      1180px;
  --gut:       clamp(20px, 5vw, 56px);

  /* Every full-bleed photograph band wears this as a mask on the <img>.

     A scrim cannot remove the top edge of a photograph. It can only get close:
     a band whose vertical scrim tops out at 82% ground still leaves 18% of the
     picture showing in the first row of pixels, and where a horizontal pass
     thins out on the right that compounds to a visible step — the photo simply
     begins. It read as a hard line across the top right of the EDGE band on
     all three trade pages and it is the one thing breaking the merge.

     Fixing it by driving the scrim to 100% at the edges would take the picture
     out of the whole band, because the scrim is what protects the copy and it
     cannot be strong at the seam and weak in the middle without visible
     banding. Masking the IMAGE costs nothing in the middle: alpha 0 at the
     seam means there is no edge to see at any scrim strength, which is the
     same reason the hero photo is masked and not its scrim.

     14% of a short wide band is 60-90px, enough to read as a dissolve and far
     short of the copy. */
  --photo-fade: linear-gradient(180deg,transparent 0,#000 14%,#000 86%,transparent 100%);
}

/* ---- RESET --------------------------------------------------------------- */
*,*::before,*::after{box-sizing:border-box}
html{-webkit-text-size-adjust:100%}
body{
  margin:0;
  background:var(--ox);
  color:var(--sand);
  font-family:var(--font);
  font-weight:400;
  font-size:17px;
  line-height:1.68;
  -webkit-font-smoothing:antialiased;
  text-rendering:optimizeLegibility;
}
img,svg,video{max-width:100%;height:auto;display:block}
button,input,select,textarea{font:inherit;color:inherit}

/* ---- TYPE ---------------------------------------------------------------- */
h1,h2,h3,h4{margin:0 0 .5em;text-wrap:balance;color:var(--sand)}
p{margin:0 0 1.1em;text-wrap:pretty;color:var(--sand-2)}
p:last-child{margin-bottom:0}

h1{
  font-weight:900;
  letter-spacing:-.035em;
  line-height:.95;
  font-size:clamp(38px,6.4vw,70px);
}
/* home hero only — larger display setting */
h1.display{font-size:clamp(42px,7.4vw,92px);line-height:.94}

h2{
  font-weight:800;
  letter-spacing:-.03em;
  line-height:1.04;
  font-size:clamp(28px,3.6vw,44px);
}
h3{font-weight:800;letter-spacing:-.02em;line-height:1.16;font-size:clamp(20px,2.1vw,26px)}
h4{font-weight:700;letter-spacing:-.02em;line-height:1.22;font-size:18px}

/* Kicker / label — plain text, used sparingly. Never with an icon or star. */
.kicker{
  font-weight:700;
  font-size:11px;
  letter-spacing:.2em;
  text-transform:uppercase;
  color:var(--accent);
  margin:0 0 18px;
  line-height:1.4;
}

/* Muted label. --sand-3 is 3.6:1 — large text and labels only, never paragraphs. */
.label{
  font-weight:700;
  font-size:11px;
  letter-spacing:.2em;
  text-transform:uppercase;
  color:var(--sand-3);
}

.lede{font-size:clamp(17px,1.5vw,20px);line-height:1.6;color:var(--sand-2)}

a{color:var(--accent);text-decoration-thickness:1px;text-underline-offset:3px}
/* Any anchor wrapping a heading inherits — otherwise the heading renders link-blue. */
a:has(h1),a:has(h2),a:has(h3),a:has(h4),
a h1,a h2,a h3,a h4{color:inherit}

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

hr{border:0;border-top:1px solid var(--line);margin:0}

/* Visually hidden, still read aloud. For the case where the sighted reading
   of something is split across decorative pieces (the rotating hero headline)
   and a screen reader needs the one plain sentence instead.

   NOT display:none and NOT visibility:hidden — both remove the node from the
   accessibility tree, which is the opposite of the point. The 1px clip is the
   only technique that keeps it announced.

   white-space:nowrap matters: without it the text wraps inside a 1px box and
   some engines report it a character per line. */
.vh{
  position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;
  clip-path:inset(50%);white-space:nowrap;border:0;
}

/* ---- LAYOUT -------------------------------------------------------------- */
.wrap{width:100%;max-width:var(--wrap);margin-inline:auto;padding-inline:var(--gut)}

/* ==========================================================================
   BUTTONS — shared, because the nav component depends on them.
   ========================================================================== */
/* A flat sand rectangle with square corners is what a button looks like before
   anybody has decided what the brand is. Four things carry the branding now,
   and none of them are a colour change:

     1. A rounded rectangle, 8px. Enough to read as a made object, not so much
        that it becomes a pill and starts looking like a chip.
     2. A warm vertical gradient rather than one flat fill, so the face has a
        light source. Cream at the top, a step toward --accent at the bottom.
     3. A lit top lip and a cast shadow, the same device the hero card uses.
        The button sits ON the page for the same reason the hero does.
     4. A sweep on hover. A single accent-tinted highlight travelling across
        the face, which is the only motion on the component.

   It lifts 1px on hover and returns to 0 on :active, so a press has somewhere
   to go. Transform and box-shadow only on that transition, both compositor
   properties.                                                               */
.btn{
  position:relative;overflow:hidden;isolation:isolate;
  display:inline-flex;align-items:center;justify-content:center;gap:9px;
  background:linear-gradient(180deg,
    color-mix(in srgb,var(--sand) 94%,#fff) 0%,
    var(--sand) 52%,
    color-mix(in srgb,var(--sand) 88%,var(--accent)) 100%);
  color:var(--ox-deep);
  border:1px solid color-mix(in srgb,var(--sand) 86%,var(--accent));
  border-radius:8px;
  text-decoration:none;font-weight:800;letter-spacing:.03em;
  padding:13px 22px;font-size:13px;white-space:nowrap;
  cursor:pointer;
  /* Inset lip first so it lands on the face, then the two casts. --ink, not
     black: on cream the shadow has to be the brand's dark or it goes grey. */
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,.62),
    0 1px 2px color-mix(in srgb,var(--ink) 22%,transparent),
    0 8px 18px -10px color-mix(in srgb,var(--ink) 58%,transparent);
  transition:transform .22s var(--ease),box-shadow .22s var(--ease),
             border-color .3s var(--ease);
}
/* The sweep. A pseudo-element rather than a moving background-position,
   because a gradient on the element itself would have to fight the face
   gradient above for the same property. Parked off the left edge and driven
   across on hover; z-index -1 keeps it under the label. */
.btn::after{
  content:"";position:absolute;z-index:-1;inset:0;pointer-events:none;
  background:linear-gradient(105deg,transparent 30%,
    color-mix(in srgb,var(--accent) 72%,transparent) 50%,transparent 70%);
  transform:translateX(-100%);
  transition:transform .55s var(--ease);
}
.btn:hover,.btn:focus-visible{
  transform:translateY(-1px);
  border-color:var(--accent);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,.7),
    0 2px 4px color-mix(in srgb,var(--ink) 26%,transparent),
    0 14px 26px -12px color-mix(in srgb,var(--ink) 66%,transparent);
}
.btn:hover::after,.btn:focus-visible::after{transform:translateX(100%)}
.btn:active{
  transform:translateY(0);
  box-shadow:
    inset 0 1px 3px color-mix(in srgb,var(--ink) 26%,transparent),
    0 1px 2px color-mix(in srgb,var(--ink) 22%,transparent);
}
@media (prefers-reduced-motion:reduce){
  .btn,.btn::after{transition:none}
  .btn:hover::after,.btn:focus-visible::after{transform:translateX(-100%)}
}
.btn-lg{font-size:15px;padding:17px 30px;border-radius:10px}
/* The ghost keeps the new radius and the lift, but none of the face: no
   gradient, no lip, no cast. It is the secondary action and it has to stay
   quieter than the thing beside it. */
.btn-ghost{
  background:transparent;color:var(--sand);border-color:var(--line);
  box-shadow:none;
}
.btn-ghost::after{
  background:linear-gradient(105deg,transparent 30%,
    color-mix(in srgb,var(--accent) 16%,transparent) 50%,transparent 70%);
}
.btn-ghost:hover,.btn-ghost:focus-visible{
  background:transparent;border-color:var(--accent);color:var(--sand);
  box-shadow:none;
}
.btn-ghost:active{box-shadow:none}

.textlink{color:var(--accent);font-weight:700;font-size:14px;letter-spacing:.03em}

/* ==========================================================================
   NAV COMPONENT — shared across every page.

   CANONICAL MARKUP (copy verbatim; only change which .nav-link gets aria-current):

     <header class="nav"> … <div class="wrap nav-in"> … </header>
     <div class="navsheet" id="navsheet" hidden> … </div>
     <script src="/js/nav.js" defer></script>

   SPECIFICITY WARNING — READ BEFORE EDITING.
   The "Book a Call" CTA is a SIBLING of <nav class="nav-links">, never a
   descendant. Do NOT write `.nav nav a{…}` or `.nav a{…}` to colour the menu
   links: either rule outranks `.btn` and paints the CTA label sand-on-sand,
   i.e. invisible. Colour links via `.nav-link` only. `.nav .btn` below is a
   second belt on the same bug, which has shipped twice.
   ========================================================================== */
/* Frosted bar. The opaque rule is the fallback and must stay first — Firefox
   below 103 and any browser with backdrop-filter disabled lands on it.

   80% is the floor, not a taste call. The worst backdrop on the site is the
   cream slab scrolling under the bar. The binding constraint is .nav-link and
   the "SYSTEMS" tagline, both --sand-2: over solid --ox they sit at
   6.7:1, and every point of transparency lightens the ground under them. At
   80% they land at ~4.9:1 — still AA. At 72% they drop to ~4.1:1 and fail.
   Do not lower this number without re-measuring against --cream. */
.nav{position:sticky;top:0;z-index:60;background:var(--ox);border-bottom:1px solid var(--line)}
@supports ((backdrop-filter:blur(1px)) or (-webkit-backdrop-filter:blur(1px))){
  .nav{
    background:rgba(74,18,25,.8);            /* --ox at 80% */
    -webkit-backdrop-filter:blur(22px) saturate(1.5);
            backdrop-filter:blur(22px) saturate(1.5);
  }
}
.nav-in{display:flex;align-items:center;gap:clamp(14px,3vw,40px);padding-block:13px}

/* Wordmark. A 2-column grid, not a flex row with a wrapper around the type:
   the badge is column 1 spanning both rows, SOUTHERN STANDARD and SYSTEMS stay
   direct children in column 2. Wrapping the type in a <span> would have been
   the obvious move and it is a trap, because `.mark span` below then matches
   the wrapper as well as SYSTEMS and paints the whole name at 8px. */
.mark{
  display:grid;grid-template-columns:auto auto;
  column-gap:clamp(9px,.8vw,12px);align-items:center;
  color:var(--sand);text-decoration:none;line-height:1;margin-right:auto;flex:0 0 auto;
}
/* The mark. The cowboy on his own, no ring: the round badge was tried here
   first and at 42px the two arcs of type around the rim turned to noise beside
   a wordmark that says the same words in real type.

   The file is cream with the linework knocked out to transparent, so the nav's
   own ground shows through as the drawing. That is why it must not be given a
   background, and why it will look wrong on any ground that is not dark.

   Square file, sized off the type beside it rather than a round number, so it
   stays locked to the wordmark as that scales. It is decoration: the company
   name is right next to it in real text, so alt is empty and it is aria-hidden,
   otherwise a screen reader reads the brand twice. */
.mark-badge{
  grid-row:1 / 3;
  width:clamp(36px,3.4vw,45px);height:auto;
}
/* THE ROW ALIGNMENT IS THE SPACING. Both lines are align-self'd to the row
   boundary they share, so the gap between them is exactly the margin-top below
   and nothing else.

   Without this the two lines drifted 12.5px apart from a 5px margin, and the
   reason is not obvious: the badge spans both rows, so the grid sizes the rows
   to the BADGE (45px) rather than to the type (30px), then splits the 15px of
   slack between them. The type is the shorter of the two things in the row, so
   the slack lands in the one place it must not, right down the middle of the
   lockup. Centring the badge instead and letting the type set the rows would
   work too, but then the badge size stops being free to change.

   Do not replace these with align-items:center on .mark. That centres each item
   in its OWN row, which is what produced the drift. */
.mark b{
  display:block;align-self:end;
  font-weight:900;font-size:clamp(14px,1.5vw,17px);letter-spacing:.05em;
}
/* One word, not three, so it can afford to be smaller and still read as a
   deliberate mark rather than a cramped line of type. The tracking is what
   carries it: it sits under a 17px wordmark and its job is to finish the name,
   not to be read on its own.

   9.5/.335em, up from 8/.3em. At 8px it was 55px of ink under 227px of wordmark
   and it read as a caption that had slid under the logo. Tracking alone cannot
   close that: matching the full 227px would take 3.37em, which is 7 letters
   scattered across the nav. Size and tracking together get it to 68px, which is
   enough for the block to read as one lockup. */
.mark span{
  display:block;align-self:start;
  font-weight:700;font-size:9.5px;letter-spacing:.335em;
  color:var(--sand-2);margin-top:4px;
}

/* BREAKPOINT: 960px, and it is load-bearing in four places in this file plus
   the resize handler in nav.js — change all five together or the sheet gets
   stranded open with the body scroll-locked.

   960 rather than 860 because "Book an Audit Call" is a 170px button. At 880
   the bar overflowed the viewport by 15px. Any longer CTA label needs this
   number re-checked at the breakpoint itself, not just at 1440. */
.nav-links{display:flex;align-items:center;gap:clamp(14px,2vw,28px)}
@media (max-width:959px){.nav-links{display:none}}

.nav-item{position:relative}
/* STATIC on purpose, and it is what makes the mega panel possible. An abs-
   positioned child resolves against its nearest positioned ancestor; leave
   this `relative` and the panel is trapped in a ~120px-wide trigger and has to
   be dragged out with negative offsets and a hardcoded width. Static hands it
   up to .nav (sticky, therefore positioned), so `left:0;right:0` gives a panel
   exactly as wide as the bar at every viewport, with no magic numbers. */
.nav-item.has-drop{position:static}

/* Anchors and dropdown triggers must render identically. */
.nav-link{
  display:inline-flex;align-items:center;gap:7px;
  background:none;border:0;padding:10px 0;
  color:var(--sand-2);text-decoration:none;
  font-family:inherit;font-weight:600;font-size:12px;
  letter-spacing:.13em;text-transform:uppercase;white-space:nowrap;
  cursor:pointer;transition:color .25s var(--ease);
}
.nav-link:hover,.nav-item.open>.nav-link{color:var(--sand)}
.nav-link[aria-current="page"]{color:var(--sand)}

.car{width:7px;height:7px;border-right:1.5px solid currentColor;border-bottom:1.5px solid currentColor;
  transform:rotate(45deg) translate(-2px,-2px);transition:transform .3s var(--ease);flex:0 0 auto}
.nav-item.open .car{transform:rotate(225deg) translate(-2px,-2px)}

/* ---- MEGA PANEL ----------------------------------------------------------
   Every dropdown on this site is this panel: a full-bar-width band that drops
   under the header carrying picture cards, not a stack of text links. There is
   no second, plainer dropdown variant — one nav component, one panel, so the
   menus cannot drift apart page to page.

   visibility (not display:none) keeps the transition AND removes the links
   from the tab order while closed.

   top:100% with NO gap is deliberate. A 2px gap between the trigger and the
   panel is 2px the pointer can cross on its way down, which closes a
   hover-opened menu halfway to the thing you were reaching for.             */
/* FULLY OPAQUE, unlike the bar above it, and that is not an oversight. The bar
   is a thin strip where a little of the page showing through reads as depth.
   This panel is 300px tall and carries photographs and 13px copy; at 94% the
   hero headline behind it came straight through the cards. There is no alpha
   value that keeps the frosted look and also lets you read the menu, so the
   frost stops at the bar.

   The gradient and the lit top edge are doing the work the transparency used
   to: top edge catches the bar, bottom sinks toward --ox-deep, and the shadow
   separates the whole band from the page under it. */
.drop{
  position:absolute;top:100%;left:0;right:0;
  background:linear-gradient(180deg,var(--ox-2) 0%,var(--ox) 100%);
  border-bottom:1px solid color-mix(in srgb,var(--accent) 34%,transparent);
  box-shadow:
    inset 0 1px 0 color-mix(in srgb,var(--sand) 12%,transparent),
    0 34px 60px -30px var(--ox-deep);
  opacity:0;visibility:hidden;transform:translateY(6px);
  transition:opacity .25s var(--ease),transform .25s var(--ease),visibility .25s;
}

/* Cards left, one aside right, divided by a hairline. The aside is what stops
   the panel reading as a stray three-up of cards floating under the bar — it
   gives the band a right edge and somewhere for the CTA to live. */
.mega-in{
  display:grid;grid-template-columns:minmax(0,1fr) clamp(210px,19vw,280px);
  gap:clamp(22px,3vw,44px);
  padding-block:clamp(22px,2.4vw,32px);
}

/* Columns come from the number of cards, not a fixed repeat(3). Who We Serve
   carries two (HVAC and roofing) and Resources carries three, and a hard
   repeat(3) left the serve panel with a card-shaped hole on the right where
   "All Contractors" used to be. grid-auto-flow:column lays the children out in
   one row and grid-auto-columns sizes each of them equally, so the panel is
   correct at any count and cannot drift again if a menu gains or loses a card.
   minmax(0,1fr) rather than 1fr: the cards contain images and a bare 1fr
   refuses to shrink below their intrinsic width, which overflows the panel. */
.mega-cards{
  display:grid;
  grid-auto-flow:column;grid-auto-columns:minmax(0,1fr);
  gap:clamp(14px,1.6vw,22px);
}

.mega-card{display:block;text-decoration:none;color:inherit}
.mega-img{
  display:block;overflow:hidden;margin-bottom:13px;
  border:1px solid var(--line);
  transition:border-color .3s var(--ease);
}
.mega-img img{
  display:block;width:100%;aspect-ratio:16/10;object-fit:cover;
  /* Portrait source crops: the subject sits above centre in every one of them,
     so a plain `center` lands on belt buckles. */
  object-position:center 38%;
  transition:transform .45s var(--ease);
}
.mega-card:hover .mega-img,
.mega-card:focus-visible .mega-img{border-color:var(--accent)}
.mega-card:hover .mega-img img{transform:scale(1.05)}
@media (prefers-reduced-motion:reduce){.mega-card:hover .mega-img img{transform:none}}

.mega-card b{
  display:block;font-weight:800;font-size:clamp(15px,1.4vw,17.5px);
  letter-spacing:-.01em;line-height:1.2;color:var(--sand);
  transition:color .2s var(--ease);
}
.mega-card:hover b,.mega-card:focus-visible b{color:var(--accent)}
/* Its own class rather than `.mega-card span`, which would also match the
   .mega-img wrapper and push a 6px margin between the photo and nothing. */
.mega-note{
  display:block;margin-top:6px;
  font-size:13px;line-height:1.45;color:var(--sand-2);
  text-transform:none;letter-spacing:0;
}

.mega-aside{
  border-left:1px solid color-mix(in srgb,var(--accent) 26%,transparent);
  padding-left:clamp(20px,2.4vw,36px);
  display:flex;flex-direction:column;align-items:flex-start;
}
.mega-aside>.label{margin-bottom:9px}
.mega-aside p{margin:0 0 18px;font-size:13.5px;line-height:1.5;color:var(--sand-2)}
/* Both asides are the same shape: a label, a short paragraph, one CTA. There
   is no secondary link list in here — a menu that offers cards AND a column of
   text links is the plain dropdown sneaking back in through the side door. */
.mega-aside .btn{margin-top:auto}

/* Below ~1100 the aside squeezes the three cards under ~190px each and the
   descriptions start wrapping to four lines. Drop it; the CTA is still in the
   bar two inches above.

   This block sits BELOW .mega-aside on purpose. A media query adds no
   specificity, so `.mega-aside{display:none}` written above `.mega-aside
   {display:flex}` loses the cascade and the aside stacks under the cards at
   1024 instead of disappearing. */
@media (max-width:1099px){
  .mega-in{grid-template-columns:minmax(0,1fr)}
  .mega-aside{display:none}
}

/* Open on click (.open) and on hover where a real pointer exists. Hover alone
   is unusable on touch, hence both.

   `.drop:focus-within` — NOT `.nav-item:focus-within` — keeps the panel up
   while a keyboard user tabs through its links. Scoping it to the item breaks
   Escape: nav.js returns focus to the trigger on close, the trigger is inside
   the item, and the rule instantly repaints the panel it just closed.
   Keyboard users open it with Enter/Space, which routes through the same
   click handler as the mouse. */
.nav-item.open>.drop,
.drop:focus-within{opacity:1;visibility:visible;transform:none}
@media (hover:hover) and (pointer:fine){
  .nav-item.has-drop:hover>.drop{opacity:1;visibility:visible;transform:none}
}

.nav .btn{color:var(--ox-deep)}     /* specificity belt — keep. see warning above */
.nav-cta{flex:0 0 auto}
@media (max-width:959px){.nav-cta{display:none}}

/* Hamburger */
.burger{display:none;width:44px;height:44px;flex-direction:column;align-items:center;justify-content:center;gap:5px;
  background:none;border:0;cursor:pointer;margin-left:auto;flex:0 0 auto}
@media (max-width:959px){.burger{display:flex}}
.burger i{display:block;width:22px;height:2px;background:var(--sand)}

/* ---- MOBILE SHEET --------------------------------------------------------
   Full-screen overlay. [hidden] is the closed state so the panel is inert for
   assistive tech and keyboard before nav.js ever runs.                       */
.navsheet{
  position:fixed;inset:0;z-index:70;
  background:var(--ox);
  overflow-y:auto;
  display:flex;flex-direction:column;
  padding:18px var(--gut) 34px;
}
.navsheet[hidden]{display:none}
@media (min-width:960px){.navsheet{display:none}}

.navsheet-top{display:flex;align-items:center;justify-content:space-between;gap:16px;margin-bottom:26px}
.sheet-x{width:44px;height:44px;background:none;border:0;cursor:pointer;position:relative;flex:0 0 auto;margin-right:-10px}
.sheet-x::before,.sheet-x::after{content:"";position:absolute;left:11px;top:21px;width:22px;height:2px;background:var(--sand)}
.sheet-x::before{transform:rotate(45deg)}
.sheet-x::after{transform:rotate(-45deg)}

.sheet-nav{display:flex;flex-direction:column}
.sheet-nav>a{
  display:block;padding:13px 0;text-decoration:none;color:var(--sand);
  font-weight:800;font-size:22px;letter-spacing:-.02em;
  border-bottom:1px solid var(--line);
}
.sheet-nav>a:hover{color:var(--accent)}
.sheet-group{border-bottom:1px solid var(--line);padding:13px 0}
.sheet-group>.label{display:block;margin-bottom:2px}
/* Dropdown children render flat — no nested disclosure — and as plain type.
   They used to carry the desktop mega card's photograph at 62x42, which on a
   phone is a picture too small to identify anything in: it read as decoration
   in a place where the only job is to get somebody to the page they want. The
   indent and the rule do the grouping the thumbnails were doing, and a menu
   that used to cost 6 image requests now costs none. */
.sheet-group a{
  display:block;position:relative;
  padding:9px 0 9px 16px;text-decoration:none;color:var(--sand);
  font-weight:700;font-size:18px;letter-spacing:-.01em;
}
/* The rule is the indent made visible — it ties the children to the label
   above them the way the thumbnails' left edge used to. */
.sheet-group a::before{
  content:"";position:absolute;left:0;top:11px;bottom:11px;width:2px;
  background:var(--line);
}
.sheet-group a:hover{color:var(--accent)}
.sheet-group a:hover::before{background:var(--accent)}
.navsheet .btn{width:100%;margin-top:28px}

/* ==========================================================================
   SHARED PAGE FURNITURE — the section rhythm, the closing CTA, and the
   footer. These were page-level on the homepage until a second and third
   page needed them; three copies of a footer is three footers that drift.
   ========================================================================== */

/* The standard vertical rhythm for a full-width band.

   Sized so a section clears a laptop viewport in one screen. The binding case
   is 1024x768, not 1440x900: 768 minus the 77px sticky bar leaves only a 691px
   budget, so the vw coefficient has to stay low enough that a 1024-wide screen
   lands on the 44px floor. Raising either number pushes sections past the fold
   — re-measure every section on the homepage before you do. */
/* --ground is the colour the band is actually sitting on. Anything that has to
   fade INTO the section — a photograph's vertical caps, the ring around a badge
   that punches through a border — reads it instead of naming --ox or --brown
   directly. Those elements get emitted onto both grounds by the same generator
   and the alternation flips whenever a section is added or removed, so a
   hardcoded colour is a bug waiting for the next edit.

   --neighbour is the colour of the bands ABOVE and BELOW this one. Ground
   alternates strictly, so both sides of any band are always the other colour
   and one token covers both. It exists so a band can cap itself in its
   neighbour's colour instead of its own, which is what turns a hard horizontal
   cut into a seam that blends. See "THE SEAMS" in trade.css. */
.sec{padding:clamp(44px,4.2vw,72px) 0;--ground:var(--ox);--neighbour:var(--brown)}

/* Alternating ground. Lives here, not in a per-area sheet: it used to be
   declared in the-system.css and trade.css and simply MISSING from
   resources.css, so `class="sec slab"` on /courses and /content was doing
   nothing and those bands sat on --ox while the header above them faded into
   --brown. One definition, every page. */
/* THE SEAM. The ground still alternates, but it no longer changes on a single
   pixel row. Both edges of a slab open in the neighbour's colour and resolve
   into their own over ~110px, so scrolling from one band to the next crosses a
   fade rather than a cut. Matt's words: "I want the sections to not be such a
   harsh change between them."

   Layered into the same `background` shorthand rather than added as a
   pseudo-element, deliberately. A ::before would need position:relative on
   every slab and a z-index on every child, and roughly a third of the slabs on
   this site already run their own stacking context. As background layers it is
   one declaration and the override behaviour does not change: a later sheet
   declaring `background` on a specific slab still replaces the whole thing,
   exactly as it did before. Two sections do that and both restate the seam
   themselves (.revs-sec here, .tabs-sec in resources.css).

   Fading to a fully transparent copy of the SAME colour, not to the
   `transparent` keyword. Both are correct in a modern engine because gradient
   interpolation is premultiplied, but writing the colour out means the
   intermediate steps are readable and it cannot go grey if a layer is ever
   moved into a context that interpolates unpremultiplied. */
.slab{
  --ground:var(--brown);--neighbour:var(--ox);
  background:
    linear-gradient(180deg,
      var(--neighbour) 0,
      color-mix(in srgb,var(--neighbour) 46%,transparent) 44px,
      color-mix(in srgb,var(--neighbour) 0%,transparent) 110px),
    linear-gradient(0deg,
      var(--neighbour) 0,
      color-mix(in srgb,var(--neighbour) 46%,transparent) 44px,
      color-mix(in srgb,var(--neighbour) 0%,transparent) 110px),
    var(--brown);
}

/* ==========================================================================
   CONTOUR GROUND — a shared class, not a one-off.

   Add `topo` to any full-width section and it gets the drifting survey-line
   terrain. Three sections use it: THE DIFFERENCE and QUESTIONS on the
   homepage, and QUESTIONS on the three trade pages. It is a class rather
   than a copy-paste because they have to stay identical — the whole effect
   of a repeated ground is that it reads as the same material, and
   hand-maintained copies drift apart on the first tweak.

   It lived inline in index.html until the trade pages needed it. Same reason
   .slab is here: the moment a second page wants a ground, the ground belongs
   to the system, not to a page.

   The base is not flat brown. Three stacked gradients: a warm oxblood bloom
   top-centre where the headline sits, a cooler one at the lower left, and a
   full-bleed vignette dropping the corners into --ox-deep. Flat brown plus
   contour lines reads as a texture swatch; the bloom gives the band a light
   source and the vignette gives it edges.
   ========================================================================== */
.topo{
  position:relative;overflow:hidden;
  background:
    radial-gradient(86% 62% at 50% -6%,color-mix(in srgb,var(--ox-2) 78%,transparent),transparent 68%),
    radial-gradient(72% 58% at 8% 88%,color-mix(in srgb,var(--ox) 46%,transparent),transparent 72%),
    radial-gradient(128% 104% at 50% 46%,transparent 40%,color-mix(in srgb,var(--ox-deep) 82%,transparent) 100%),
    var(--brown);
}
.topo>.wrap{position:relative;z-index:1}

/* /assets/topo.svg is a single 1600x1000 contour field stroked in --sand at
   source, which is why nothing here recolours it.

   ONE layer. This used to stack two copies at different scales, the second
   mirrored, on the theory that two drifting sets read as terrain while one
   reads as sliding wallpaper. They don't: contour lines are heights on a
   surface, so two surfaces laid over each other produce lines that cross, and
   crossed contours are the one thing that cannot happen on a real map. The
   eye reads it as a mistake long before it reads it as depth.

   Animates TRANSFORM ONLY — no background-position, no opacity keyframes.
   Transform runs on the compositor, so a minute-long loop underneath a full
   section costs nothing. Animating background-position repaints the whole
   band every frame and this is a 116KB vector.

   Masked hollow in the middle so the contours are densest at the edges and
   clear behind the headline. The lines are a frame, not a backdrop for body
   copy — at full strength under the lede they cost real legibility.         */
.topo::before{
  content:"";position:absolute;z-index:0;pointer-events:none;
  background:url(/assets/topo.svg) center/cover no-repeat;
  inset:-16% -14%;opacity:.26;
  -webkit-mask-image:radial-gradient(112% 86% at 50% 48%,transparent 14%,#000 70%);
          mask-image:radial-gradient(112% 86% at 50% 48%,transparent 14%,#000 70%);
  animation:topoA 64s ease-in-out infinite alternate;
}
@keyframes topoA{
  from{transform:translate3d(-1.5%,-1.1%,0) scale(1.02)}
  to  {transform:translate3d(1.5%,1.1%,0) scale(1.09)}
}
@media (prefers-reduced-motion:reduce){
  .topo::before{animation:none}
}

/* ==========================================================================
   THE RAISED HERO CARD — .hero-card + .hero-join

   The hero is a card lying ON the page, not a stripe above it. Put `hero-card`
   on the hero section and `hero-join` on the section immediately after it.
   That is the whole contract. The two are a pair and neither works alone: the
   card's radius is the exact distance the join pulls up, so they read the same
   value from --card-r.

   Three things were tried at this edge before this one, and they are worth
   knowing so they are not tried again:

     1. A 1px hairline. A knife cut across the page at the exact point the eye
        leaves the headline.
     2. Dissolving the hero down into the next band's photograph. That removed
        the cut, but it removed the edge along with it, and cream fading into
        oxblood travels through mud on the way.
     3. The dark band as a panel overlapping the hero. Right amount of depth,
        wrong object on top: it made the hero look like the thing being covered
        up, when the hero is the thing the page is about.

   So the hero is on top. Rounded at the bottom two corners, square everywhere
   else, casting a shadow down onto the section below. The two colours never
   blend at any pixel, so there is no mud, but the boundary is a shape with a
   light source implied above it and the hero reads as a physical object.

   This belongs to the system, not to trade.css, because it is the site's hero
   treatment. The direction of the overlap is the entire point of it: do not
   reverse it, and do not round the top corners.
   ========================================================================== */
.hero-card{
  /* Big enough to read as a corner at a glance, small enough that the card
     still reads as full-bleed rather than as a rounded box floating in a
     margin. It is also the exact distance the band below is pulled up. */
  --card-r:clamp(20px,2.6vw,38px);
  position:relative;
  z-index:2;
  border-radius:0 0 var(--card-r) var(--card-r);
  /* The 3D. Two casts: a tight one that anchors the card to the surface and a
     wide soft one that gives it height. --ink, not black and not --ox — the
     shadow has to be darker than the ground it falls on or it is invisible, and
     --ox is only a few steps off black already.

     The inset hairline is the card's own bottom lip. Without it a light card
     runs straight into its own shadow and the edge goes soft in exactly the
     place that has to stay crisp. */
  box-shadow:
    0 4px 10px -6px color-mix(in srgb,var(--ink) 74%,transparent),
    0 26px 52px -22px color-mix(in srgb,var(--ink) 88%,transparent),
    inset 0 -1px 0 color-mix(in srgb,var(--ox) 16%,transparent);
}

/* The band below slides up UNDER the card by exactly the radius, so only the
   curved corners overlap and the straight run of the edge lands where the
   section would have started anyway. The padding puts that distance back so the
   kicker does not end up behind the card.

   The padding-top assumes the .sec rhythm above. A section with its own top
   padding should restate it in the same calc rather than dropping .hero-join,
   or the first line lands under the corner. */
.hero-join{
  position:relative;z-index:1;
  margin-top:calc(-1 * clamp(20px,2.6vw,38px));
  padding-top:calc(clamp(44px,4.2vw,72px) + clamp(20px,2.6vw,38px));
}

/* There was an .is-dark variant here, for the four pages whose heroes were
   dark bands over a dark page: a drop shadow alone cannot separate dark from
   dark, so it added a lit bottom lip. Every one of those pages is a cream
   .photo-hero now and nothing carries the class, so it is gone. If a dark hero
   ever comes back, that is the problem to solve and the lip is the answer. */

/* ==========================================================================
   THE CREAM PHOTO HERO — .photo-hero

   The one light band above the fold: cream ground, oxblood lettering, and a
   photograph dissolving into the cream from the right. Everything below it
   goes back to the dark palette. It is the top of /hvac, /roofing, /blog,
   /work, /courses and /resources, and it is always worn together with
   .hero-card — the card is the EDGE, this is the SURFACE.

   It started on the trade pages and lived in trade.css. It is here now for
   the same reason .topo moved here: six copies of a hero is six heroes that
   drift. Nothing about it changed in the move.

   It is inverted for one reason, the photograph. On a near-black ground the
   scrim had to reach 84% opacity before body copy cleared 4.5:1, and at 84%
   the man in the shot was a silhouette. On cream the maths runs the other
   way: the scrim is the SAME colour as the ground it protects, so lightening
   it helps the copy and the picture at once instead of trading one against
   the other. The picture is finally legible.

   The tokens are the ones .cta uses, the site's other light band: --cream
   ground, --ox for headings and the kicker, --ink-2 for running copy.
   Measured on cream: --ox 12.3:1, --ink-2 7.7:1.

   Two layers under the copy, the <img> (z-index -3) and a directional cream
   scrim (-2). The photo is a real <img> and not a CSS background so it keeps
   width/height (the box is reserved before the file lands and the headline
   does not jump), so it can carry alt text, and so it can be eager-loaded as
   the LCP element.

   No contour field on this band. The photograph is the texture.

   Markup contract:
     <section class="photo-hero hero-card">
       <img class="photo-hero-bg" src="" alt="" width="" height="" fetchpriority="high">
       <div class="wrap photo-hero-in"> kicker / h1 / lede / actions </div>
     </section>
   ========================================================================== */
.photo-hero{
  position:relative;isolation:isolate;overflow:hidden;
  color:var(--ink-2);
  background:
    /* A warm bloom off the top-left so the cream is a lit surface rather than
       a flat fill. --accent is the warm tan; on cream it reads as sunlight. */
    radial-gradient(74% 112% at 8% 14%,
      color-mix(in srgb,var(--accent) 52%,transparent),
      transparent 72%),
    var(--cream);
  /* Ordinary padding. This used to reserve a photograph strip above the
     kicker, which opened every page on a phone with 232px of pale nothing
     before a word of copy. The picture is the band's background at every
     width now, so the band starts where the content starts. */
  padding:clamp(44px,11vw,84px) 0 clamp(56px,6.4vw,96px);
}

/* MOBILE FIRST: the whole band, not a strip across the top.

   object-position is the "bring the picture up" control and it reads
   backwards: a HIGHER percentage slides the image content upward inside the
   box, revealing more of the bottom of the file. Per-photo tuning belongs on
   the page, as --hero-pos; this is only the portrait-shaped default, which
   keeps a face in the upper third behind the headline rather than behind the
   button. */
.photo-hero-bg{
  position:absolute;z-index:-3;inset:0;
  width:100%;height:100%;
  object-fit:cover;object-position:var(--hero-pos,50% 26%);
  /* Tuned for a light ground, which is the opposite of what this was. The
     picture has to sit UP against cream, so brightness goes over 1 and
     contrast comes down slightly — a dark contrasty photo under a cream veil
     goes muddy rather than soft. */
  filter:saturate(.6) contrast(1) brightness(1.08);
}

/* THE SCRIM. Copy sits ON the photograph at this breakpoint, so the veil does
   real work rather than just joining a strip to the ground: it has to hold
   --ox headings and --ink-2 body copy at AA over whatever the top of the
   frame happens to be.

   A flat wash plus a downward ramp. The wash is what makes the contrast a
   property of the ground instead of a property of the crop, so the same
   numbers hold on every photograph put behind it; the ramp reaches solid
   cream by the buttons, where a textlink underline over foliage would be the
   first thing to break. */
.photo-hero::before{
  content:"";position:absolute;z-index:-2;inset:0;pointer-events:none;
  background:
    linear-gradient(180deg,
      color-mix(in srgb,var(--cream) 52%,transparent) 0%,
      color-mix(in srgb,var(--cream) 66%,transparent) 26%,
      color-mix(in srgb,var(--cream) 88%,transparent) 62%,
      var(--cream) 88%),
    linear-gradient(0deg,
      color-mix(in srgb,var(--cream) 30%,transparent),
      color-mix(in srgb,var(--cream) 30%,transparent));
}

/* WIDE: the photo moves to the right two-thirds and dissolves leftward into
   the cream, copy sits in the clear third. The mask is on the IMAGE, not on
   the scrim — fading the scrim alone leaves a hard vertical edge where the
   file starts, which is exactly the seam this layout exists to avoid.

   left:38% is also why the picture is not upscaled: the trade hero photos are
   900px wide and 62% of a 1440px band is 893px. Bleeding it the full width
   would stretch a 900px file across 1440 and the grain would show. A wider
   file can opt out with --hero-left:0%;--hero-w:100%. */
@media (min-width:940px){
  .photo-hero{padding-top:clamp(56px,6.6vw,100px)}
  .photo-hero-bg{
    left:var(--hero-left,38%);width:var(--hero-w,62%);height:100%;
    object-position:var(--hero-pos-w,50% 46%);
    -webkit-mask-image:linear-gradient(90deg,transparent 0,#000 40%);
            mask-image:linear-gradient(90deg,transparent 0,#000 40%);
  }
  /* THE HOLD, THEN THE DROP.
     This ramp used to start giving the photograph away at 36% and was down to
     56% cream by 58%, which at 1440 is x=835. The copy column ends at x=786,
     so the last 200px of it sat on a ground that was visibly sliding from
     cream into picture: the hero bar's third claim in particular ran straight
     across the muddiest part of the ramp, and the same 3 claims did not read
     on the same background as each other.

     So: hold near-solid cream across the whole copy column (94% out to 56%,
     which is x=806), then drop fast. Measured with the photo at its normal
     left:38%, the picture now contributes under 4% behind the third claim,
     versus roughly a quarter before.

     The photograph did not move and did not need to. It has less room on the
     left and a steeper ramp, so it reads harder where it reads at all, and it
     is still fully itself from about x=1240 to the right edge. Do not soften
     this back out to buy the photo more width: it costs the copy its ground.
     If a future crop needs more room, move the IMAGE with --hero-left. */
  .photo-hero::before{
    background:
      linear-gradient(90deg,
        var(--cream) 0%,
        color-mix(in srgb,var(--cream) 94%,transparent) 56%,
        color-mix(in srgb,var(--cream) 44%,transparent) 72%,
        color-mix(in srgb,var(--cream) 12%,transparent) 86%,
        transparent 100%),
      linear-gradient(0deg,
        color-mix(in srgb,var(--cream) 16%,transparent),
        color-mix(in srgb,var(--cream) 16%,transparent));
  }
}
/* No hairline under this band. There was one, on the reasoning that cream
   cannot dissolve into oxblood without going through mud so a clean cut was
   the honest option. The mud was real; a border was the wrong answer to it.
   The hero is a raised card sitting ON the page, so the edge is already drawn
   — by a shape with a shadow under it rather than by a 1px rule. A border
   here would run straight across its rounded corners. See THE RAISED HERO
   CARD above. */

/* One column, capped. The cap goes on the CHILDREN, not on .photo-hero-in
   itself. That element is a .wrap, and .wrap is max-width plus auto inline
   margins — narrowing it re-centres the whole column in the viewport, which
   walked the trade headline 260px right and straight onto the technician's
   face. Leaving the wrap at its normal width keeps the left edge on the same
   rail as every other section on the page. */
.photo-hero-in{position:relative;z-index:1}
@media (min-width:940px){
  .photo-hero-in>*{max-width:min(100%,600px)}
}

/* THE INVERTED PALETTE. Every one of these overrides a rule written for a
   dark ground, and each would be invisible without it: .kicker is --accent
   (pale tan on cream), h1 and .lede are --sand (which IS --cream), and .btn
   is a --sand fill.

   The :hover rules are not optional. .btn:hover elsewhere in this file is
   (0,2,0) and so is .photo-hero .btn, so whichever is declared later wins —
   restate the hover or it stops painting entirely. Same trap documented on
   .cta. */
.photo-hero .kicker,
.photo-hero .label{color:var(--ox)}
.photo-hero h1,
.photo-hero h2{color:var(--ox)}
.photo-hero p,
.photo-hero .lede{color:var(--ink-2)}
.photo-hero .btn{background:var(--ox);border-color:var(--ox);color:var(--cream)}
.photo-hero .btn:hover{background:var(--ox-deep);border-color:var(--ox-deep);color:var(--cream)}
.photo-hero .textlink{color:var(--ox)}
.photo-hero .textlink:hover{color:var(--ox-deep)}

/* A notch under the global h1 scale, and well under h1.display, which is the
   home hero only. These headlines name a service and an outcome, so they run
   longer than a slogan; at the global size they came out five lines deep and
   pushed the button off the first screen. */
.photo-hero h1{max-width:16ch;margin:0;font-size:clamp(33px,4.05vw,57px)}
.photo-hero .lede{margin:20px 0 0;max-width:50ch}

.photo-hero-actions{
  display:flex;flex-wrap:wrap;align-items:center;
  gap:16px clamp(18px,2.4vw,28px);
  margin-top:clamp(24px,2.8vw,34px);
}

/* ==========================================================================
   THE HERO BANNER — .hero-bar

   A full-bleed plate across the very bottom of a hero carrying the 4 things
   that are actually different about this shop, one star each, no sub-copy:
   1 client per metro, the 90 day guarantee, founder led, US based. This is the
   slot a site normally fills with client logos or review counts. There are none
   to show yet, so it carries the differentiators instead. Swap it for real proof
   when there is real proof.

   IT IS A PLATE, NOT A ROW. The earlier version was 3 columns of small text on a
   hairline, sitting inside the copy column. It was accurate and nobody saw it.
   This has its own ground, runs the full width of the hero, and is the last
   thing in the band, so it reads as the hero's foot rather than a footnote.

   FOUR SHORT LABELS, NOT 3 SENTENCES. The claims used to be written out in the
   first person ("You get me, not an account manager"). That reads better in
   isolation and it does not survive 4 across: a sentence in a 295px column wraps
   to 3 lines, and 4 wrapped paragraphs is a block of text, not a banner. These
   are labels on purpose. The argument behind each one is made at length further
   down every page that carries this plate.

   THE STAR is the same on all 4 and it is lifted off the badge, which has
   5-point stars in its ring and in the flag. It is the one shape on this site
   that is already brand. Four different glyphs would be 4 shapes competing with
   the wordmark; one repeated star reads as a rating strip, which is the exact
   connotation wanted under a hero. Do not "fix" the repetition.

   MARKUP — the ground is full-bleed, the content is not:

     <div class="hero-bar">
       <div class="wrap hero-bar-in">
         <div class="hero-bar-item"><svg class="hero-bar-ic">…</svg><b>…</b></div>
         ×4
       </div>
     </div>

   It must be a direct child of the hero SECTION, after the .wrap, not inside
   it. Inside the wrap it cannot reach the edges, and on the trade pages it
   would also inherit the 600px cap `.photo-hero-in>*` puts on every child.

   No <p> and no bare <span> for the claim: `.photo-hero p` is (0,1,1) and
   would beat a bare class here. <b> is the element the old row used for the
   same reason.

   The markup lives in 3 files and is written by /tmp/mkbar.py. Edit the claims
   there, not in one page.

   Palette is 5 variables so ONE component serves both grounds. Do not restate
   the layout to change a colour.
   ========================================================================== */
.hero-bar{
  /* BROWN, on every page. This was --ox-deep with a --brown override for the
     homepage only, which meant the single warmest surface in the palette showed
     up on one page out of three and the trade pages got near-black.

     Brown works under both heroes: the homepage hero is --ox with an --ox-deep
     logo band under it, and the trade heroes are cream photo cards over a run of
     flat --ox (see ONE GROUND in trade.css). Brown is a different hue from all
     three, so the plate is a distinct object above and below on every page. */
  --bar-bg:var(--brown);
  --bar-k:var(--sand);
  --bar-ic:var(--accent);
  --bar-rule:rgba(242,231,216,.15);
  /* The disc behind each star, and the ring around it. Sand at 7% was the first
     try and it disappeared: over a near-black ground that is a lift of about 12
     RGB points, which is a smudge, not an object. 11% plus an accent ring is the
     point where the disc reads as a disc from a normal viewing distance. The
     ring is accent rather than sand on purpose, so the disc belongs to the same
     warm light as the bloom on the left instead of being neutral hardware. */
  --bar-chip:rgba(242,231,216,.11);
  --bar-ring:color-mix(in srgb,var(--accent) 26%,transparent);

  position:relative;
  /* The gap between the hero copy and the plate. The hero's own padding-bottom
     goes to 0 wherever this is used, so this margin IS the bottom padding and
     the plate finishes the band flush. */
  margin-top:clamp(38px,5vw,72px);
  background:var(--bar-bg);
  /* Three lights, not one. An oxblood bloom off the left, so the plate is lit
     from where the page is lit and the brown reads as brown against something
     redder rather than as undifferentiated dark. A small accent bloom off the
     right so the row does not fall away at the end. Then a short fade down from
     the top edge, which is what stops the fill reading as one flat swatch: a
     real surface is brighter where it turns to meet the light. */
  background-image:
    radial-gradient(78% 240% at 2% 50%,
      color-mix(in srgb,var(--ox) 72%,transparent),transparent 60%),
    radial-gradient(60% 200% at 99% 50%,
      color-mix(in srgb,var(--accent) 11%,transparent),transparent 62%),
    linear-gradient(180deg,rgba(242,231,216,.07),transparent 46%);
  /* Cast UP, onto the band above. The plate is the nearest surface to the
     viewer at the foot of the hero, so it has to sit ON the hero rather than be
     a hole cut in it. Inside .hero-card the card's overflow:hidden keeps this
     contained, which is what we want. */
  box-shadow:0 -20px 44px -26px rgba(0,0,0,.6);
}
/* THE LIT EDGE. A plain 1px --bar-rule across the top was the single biggest
   reason this read as flat: the same dull hairline used everywhere else on the
   site, so the plate announced itself as another divider. This is the same 1px,
   but it runs accent at the left where the bloom is and decays to nothing at
   the right, so the top of the plate is a machined lip catching light. Not a
   border: a border would also have to be declared on the phone layout where the
   plate meets the page edge differently. */
.hero-bar::before{
  content:"";position:absolute;inset:0 0 auto;height:1px;pointer-events:none;
  background:linear-gradient(90deg,
    color-mix(in srgb,var(--accent) 60%,transparent),
    var(--bar-rule) 46%,
    color-mix(in srgb,var(--accent) 18%,transparent));
}
.hero-bar-in{
  display:grid;grid-template-columns:repeat(4,minmax(0,1fr));
  align-items:center;
  /* Air. The first pass ran 16px and the plate came out 68px tall, which is a
     cookie notice. These 4 claims are the only proof on the page, so the plate
     gets the height of a real band and the row sits in it rather than on it. */
  padding-block:clamp(20px,2.1vw,29px);
}
/* Dividers, not boxes. 4 bordered panels here would be a card grid inside a
   hero that is itself a card, which is one plate too many. The discs below give
   each item its own object to hang on, so the rule only has to mark the column
   edge and can stay a hairline. */
.hero-bar-item{
  position:relative;
  display:flex;align-items:center;gap:clamp(11px,.9vw,14px);
  padding-left:clamp(14px,1.5vw,22px);
}
/* The divider is a pseudo-element, not a border-left, because a border runs the
   full height of the grid row and a 1px line that hits the top and bottom edges
   of the plate cuts it into 4 panels. This one is 60% tall, centred, and fades
   out at both ends, so it separates the claims without slicing the surface. */
.hero-bar-item::before{
  content:"";position:absolute;left:0;top:50%;translate:0 -50%;
  width:1px;height:60%;
  background:linear-gradient(180deg,
    transparent,var(--bar-rule) 24%,var(--bar-rule) 76%,transparent);
}
.hero-bar-item:first-child{padding-left:0}
.hero-bar-item:first-child::before{display:none}

/* THE STAR DISC. The icon used to be a bare 23px stroke sitting at the same
   visual weight as the sentence beside it, which is why a row of them read as
   punctuation instead of as a set. It is the SVG itself that is the disc, so
   there is no wrapper element to add to markup that exists in 3 files: padding
   on the box plus the viewBox does the inset, and box-sizing is border-box
   globally so the declared size is the disc.
   The ring is an inset shadow rather than a border, because a border would eat
   into the padding box and shrink the glyph.
   flex:none or the disc is a flex item that shrinks when the claim beside it
   wraps, and 4 discs at 4 different sizes down one row is worse than a wrap. */
.hero-bar-ic{
  flex:none;
  width:clamp(38px,2.95vw,45px);height:clamp(38px,2.95vw,45px);
  padding:clamp(9px,.73vw,11px);
  border-radius:50%;
  background:var(--bar-chip);
  /* 3 shadows, outside in: a wide soft halo that separates the disc from the
     plate, the accent ring, and a top-inside highlight so the disc is domed
     rather than punched. The halo is what does most of the work, and it is the
     thing to turn down first if this ever looks like a button. */
  box-shadow:
    0 0 0 5px color-mix(in srgb,var(--accent) 6%,transparent),
    inset 0 0 0 1px var(--bar-ring),
    inset 0 1px 0 rgba(242,231,216,.14);
  color:var(--bar-ic);
}
/* Uppercase and tracked, which is the .label treatment used for kickers all
   over the site. These are 4 specifications, not 4 sentences, and small caps on
   a stamped plate is how a spec is written on a real object. Weight 800 and the
   full --sand, not --sand-2: tracked-out uppercase loses a lot of apparent
   weight, and at 13px in a dull tan it would be fine print. */
.hero-bar-item b{
  font-size:clamp(12px,1vw,13.5px);
  font-weight:800;letter-spacing:.085em;text-transform:uppercase;
  line-height:1.25;color:var(--bar-k);
}

/* THE TRADE HEROES. The plate is inside a card with rounded corners and
   overflow:hidden, so it is clipped to the card's bottom radius for free — do
   not add a radius here, it would round the top corners too.

   It also has to paint OVER the hero photograph and its scrim, which sit at
   z-index -3 and -2. Normal flow with an opaque background already does that;
   the z-index is here so a future change to the stacking inside .photo-hero
   cannot drop the plate behind the picture. */
.photo-hero .hero-bar{z-index:1}

/* Wherever the plate is used, it IS the bottom of the band. */
.hero:has(.hero-bar),
.photo-hero:has(.hero-bar){padding-bottom:0}

/* BELOW 940: 2 up, 2 down. Not 4 stacked rows — 4 full-width rows is 210px of
   plate on a phone, which is a third of the screen spent on a strip. The labels
   are short enough to hold a half-width column all the way down, so the grid
   just halves once and stops.

   Column 1 loses its divider and picks up the flush left edge; row 2 gets a
   horizontal rule instead, because a vertical rule beside a wrapped row reads as
   a tick rather than a divider. */
@media (max-width:939px){
  .hero-bar-in{grid-template-columns:repeat(2,minmax(0,1fr));gap:0}
  .hero-bar-item{padding-block:clamp(11px,1.7vw,15px)}
  .hero-bar-item:nth-child(odd){padding-left:0}
  .hero-bar-item:nth-child(odd)::before{display:none}
  .hero-bar-item:nth-child(n+3){border-top:1px solid var(--bar-rule)}
}

/* PHONE: same 2×2, tighter. At 390 a half column is about 175px and the disc at
   45px was taking a quarter of it, which pushed the longer labels to 2 lines.

   The type goes down to 10.5px, which is smaller than anything else on the site
   and is the right call here: these are 4 short labels with an icon each, read
   at a glance, not copy. The alternative was 2 of the 4 wrapping, and a 2×2 with
   a ragged first row looks broken in a way that 10.5px does not. */
@media (max-width:560px){
  .hero-bar-item{gap:9px}
  .hero-bar-item:nth-child(even){padding-left:12px}
  .hero-bar-ic{width:32px;height:32px;padding:7px;box-shadow:
    0 0 0 4px color-mix(in srgb,var(--accent) 6%,transparent),
    inset 0 0 0 1px var(--bar-ring),
    inset 0 1px 0 rgba(242,231,216,.14)}
  .hero-bar-item b{font-size:10.5px;letter-spacing:.04em}
}

/* ---- CLOSING CTA --------------------------------------------------------
   The one centred band every page ends on, and the only inverted ground
   outside the homepage story slab.

   It used to be --ox-deep, which is also the footer colour: the last two
   blocks of every page were the same shade and the ask disappeared into the
   sitemap underneath it. Flipping it to cream puts the single hardest edge on
   the page right where the single most important control is, and it costs
   nothing but a re-declared palette.

   INVERTED PALETTE. Every colour in this block is set against --cream.
   Inherit the dark defaults and you paint sand-on-cream and it vanishes —
   same rule that governs .light on the homepage.                           */
.cta{
  position:relative;overflow:hidden;isolation:isolate;
  background:var(--cream);color:var(--ink);text-align:center;
  padding-block:clamp(66px,7.2vw,108px);
}
/* DRAWN GROUND, NOT A PHOTOGRAPH.
   This was a washed-out /images/hero.jpg under a heavy cream wash until
   2026-09-26. Two things were wrong with it and they pulled in the same
   direction. The wash had to be near-opaque to protect body-copy contrast, so
   what survived of the photo was a grey smear with no subject — the cost of a
   full-bleed image with none of the benefit. And the image was the one with
   another company's truck and phone number in the frame, sitting directly
   behind the only ask on the page.

   Two pseudo-elements now, painted bottom-to-bottom:

     ::before  the cream field and its vignette
     ::after   the contour lines

   The vignette does the work the photo was supposed to do. Cream is flat out
   to 46% of the radius, so the whole copy column keeps its 4.5:1, then turns
   down toward ox at the corners. That is what stops the band reading as an
   empty rectangle: the edges are darker than the middle, so it has a centre.

   z-index:-2 under an isolation:isolate root keeps both layers behind the copy
   without giving the copy its own stacking context. */
.cta::before{
  content:"";position:absolute;z-index:-2;inset:0;pointer-events:none;
  background:
    radial-gradient(86% 128% at 50% 40%,
      color-mix(in srgb,var(--cream) 96%,var(--accent)) 0%,
      var(--cream) 46%,
      color-mix(in srgb,var(--cream) 90%,var(--ox)) 78%,
      color-mix(in srgb,var(--cream) 74%,var(--ox)) 100%),
    var(--cream);
}
/* CONTOURS. Same /assets/topo.svg the dark bands use, so the CTA is built from
   the site's own motif rather than a one-off texture. The file is stroked in
   --sand, which is invisible on cream, so it is used as a MASK here and --ox
   is painted through it. Alpha in the source is the strokes and nothing else,
   which is exactly what a mask wants.

   Hollow centre, same as .topo: the lines are a frame around the ask, not a
   texture under it. At .09 they are barely there on purpose — this sits under
   14.9:1 headline type and the job is to keep the band from feeling printed on
   blank stock, not to be noticed.

   Drift is transform-only and 78s, slower than the .topo bands. A CTA is where
   someone stops to read, and motion in peripheral vision is more noticeable
   when the eye is still. */
.cta::after{
  content:"";position:absolute;z-index:-1;inset:-18% -15%;pointer-events:none;
  background:var(--ox);
  -webkit-mask:url(/assets/topo.svg) center/cover no-repeat;
          mask:url(/assets/topo.svg) center/cover no-repeat;
  opacity:.09;
  animation:ctaDrift 78s ease-in-out infinite alternate;
}
@keyframes ctaDrift{
  from{transform:translate3d(-1.2%,-.9%,0) scale(1.02)}
  to  {transform:translate3d(1.2%,.9%,0) scale(1.08)}
}
@media (prefers-reduced-motion:reduce){.cta::after{animation:none}}
/* Hairline at the seam. The band above the CTA is dark on every page, and
   without this the cream simply starts — the rule reads as a deliberate edge
   instead of a missing one. */
.cta{border-top:1px solid color-mix(in srgb,var(--ox) 26%,transparent)}
/* 840, not 780: the copy is capped at 58ch and centred regardless, so this
   number only sets how wide .cta-vals gets — and at 780 a three-item row
   wraps its last value onto a line of its own. */
.cta .wrap{position:relative;max-width:840px}
/* One step up from the global h2. This is the last thing on the page and the
   only headline that has a whole band to itself; at the shared 44px it read
   as just another section title. */
.cta h2{
  color:var(--ink);
  font-weight:900;letter-spacing:-.038em;line-height:1.02;
  font-size:clamp(33px,4.6vw,58px);
  max-width:16ch;margin-inline:auto;
}
.cta h2 em{font-style:normal;color:var(--ox)}
.cta p{margin-inline:auto;max-width:58ch;color:var(--ink-2);font-size:clamp(15.5px,1.4vw,17px);line-height:1.62}
.cta .kicker,.cta .label{color:var(--ink-3)}
.cta strong{color:var(--ink);font-weight:800}
/* The shared .btn is sand on dark, which here is cream on cream. The closing
   button inverts to oxblood — the only dark fill on a light ground anywhere on
   the site, which is exactly what makes it the loudest control.

   It inherits the radius, the lift, the press and the sweep from .btn and only
   restates the palette: the same gradient face, lip and cast, read in oxblood
   instead of sand. This used to be a flat `background:var(--ox)` with one
   shadow, which threw away every one of those and left the page's single most
   important control as the plainest object on it.

   The :hover rule is required: .cta .btn (0,2,0) sits LATER in this file than
   .btn:hover (0,2,0), so without it the hover state never paints. */
.cta .btn{
  margin-top:clamp(26px,3vw,34px);
  background:linear-gradient(180deg,
    color-mix(in srgb,var(--ox) 88%,var(--ox-2)) 0%,
    var(--ox) 54%,
    color-mix(in srgb,var(--ox) 86%,var(--ink)) 100%);
  border-color:color-mix(in srgb,var(--ox) 80%,var(--ink));
  color:var(--cream);
  box-shadow:
    inset 0 1px 0 color-mix(in srgb,var(--accent) 34%,transparent),
    0 2px 5px -2px color-mix(in srgb,var(--ink) 46%,transparent),
    0 16px 30px -14px color-mix(in srgb,var(--ox) 72%,transparent);
}
/* The sweep is accent on sand by default, which is invisible on oxblood. Here
   it is a cream highlight instead, at the strength a dark face needs. */
.cta .btn::after{
  background:linear-gradient(105deg,transparent 30%,
    color-mix(in srgb,var(--cream) 22%,transparent) 50%,transparent 70%);
}
.cta .btn:hover,.cta .btn:focus-visible{
  background:linear-gradient(180deg,
    color-mix(in srgb,var(--ox) 94%,var(--ox-2)) 0%,
    color-mix(in srgb,var(--ox) 88%,var(--ink)) 54%,
    var(--ox-deep) 100%);
  border-color:var(--ox-deep);color:var(--cream);
  box-shadow:
    inset 0 1px 0 color-mix(in srgb,var(--accent) 44%,transparent),
    0 3px 7px -2px color-mix(in srgb,var(--ink) 52%,transparent),
    0 24px 42px -16px color-mix(in srgb,var(--ox) 80%,transparent);
}
.cta .btn:active{
  box-shadow:
    inset 0 2px 5px color-mix(in srgb,var(--ink) 54%,transparent),
    0 1px 2px color-mix(in srgb,var(--ink) 40%,transparent);
}
/* The value row under the CTA copy. Lives here rather than in a page sheet
   because the CTA is on every page — left in the homepage's <style> it simply
   doesn't exist anywhere else, and the list renders as default bullets.
   A drawn tick, not a character, so it can't fall back to a system emoji. */
/* A three-up GRID above 760, not a wrapping flex row.

   Flex-wrap kept putting the third value on a line of its own the moment any
   one string grew, and a centred orphan under two values reads as a mistake
   rather than as a list. It also made the layout a hostage to copy length —
   every edit to a value risked re-breaking the row. Equal columns hold three
   abreast at any length; a long value wraps to two lines inside its own cell,
   which is a shape the eye accepts. Stacked below 760 where three columns
   would be ~14 characters wide. */
.cta-vals{
  list-style:none;margin:clamp(20px,2.3vw,28px) auto 0;padding:0;
  display:grid;gap:11px clamp(14px,2vw,24px);justify-content:center;
}
@media (min-width:760px){.cta-vals{grid-template-columns:repeat(3,minmax(0,1fr))}}
.cta-vals li{position:relative;padding-left:23px;font-size:14.5px;line-height:1.45;color:var(--ink-2);text-align:left}
.cta-vals li::after{
  content:"";position:absolute;left:4px;top:.28em;width:5px;height:9px;
  border:solid var(--ox);border-width:0 1.8px 1.8px 0;transform:rotate(45deg);
}

/* ---- SCREENSHOT WALL -----------------------------------------------------
   The unpolished proof: texts, DMs and emails, dropped in as pictures. This
   is the block Hook's reviews page leads with and it is the most convincing
   thing on a page like this precisely because it is ugly. A formatted quote
   is something a copywriter could have produced; a screenshot of a 6am text
   saying "phone hasn't stopped" is not.

   MASONRY BY COLUMNS, NOT BY GRID. CSS columns because the images are
   whatever shape the phone screenshotted them at: a 3-line text and a long
   email thread cannot share a grid row without one of them being cropped or
   letterboxed. Columns let each one keep its own height. The trade is that
   reading order goes down column 1 before column 2, which does not matter
   here because there is no order to these.

   Each shot needs `width` and `height` on the <img>. Without them the column
   heights are computed before the images load and the wall reflows.        */
.shots{columns:3;column-gap:clamp(12px,1.3vw,18px)}
@media (max-width:900px){.shots{columns:2}}
@media (max-width:560px){.shots{columns:1;max-width:420px;margin-inline:auto}}

.shot{
  break-inside:avoid;margin:0 0 clamp(12px,1.3vw,18px);
  border-radius:12px;overflow:hidden;background:var(--ox-2);
  border:1px solid color-mix(in srgb,var(--sand) 16%,transparent);
  box-shadow:0 16px 36px -22px var(--ox-deep);
}
.shot img{display:block;width:100%;height:auto}
.shot figcaption{
  padding:9px 13px 11px;font-size:11.5px;line-height:1.45;color:var(--sand-3);
  border-top:1px solid color-mix(in srgb,var(--sand) 12%,transparent);
}
/* Empty state. These stay until real screenshots land, and they are sized by
   aspect-ratio rather than by a height so the wall looks like the wall. */
.shot.is-slot{
  border-style:dashed;display:grid;place-items:center;align-content:center;
  gap:6px;text-align:center;padding:20px;aspect-ratio:var(--ar,4/5);
}
.shot.is-slot span{
  font-weight:800;font-size:10.5px;letter-spacing:.17em;text-transform:uppercase;
  color:var(--accent);
}
.shot.is-slot small{font-size:12px;line-height:1.5;color:var(--sand-2);max-width:24ch}

/* ---- PROOF CARDS ---------------------------------------------------------
   Three cards showing what the deliverable looks like: the local pack, an AI
   Mode answer, and a finished Google Business Profile. Homepage only.

   THESE ARE ILLUSTRATIONS, NOT SCREENSHOTS. Every business named in them is
   "Your Company" or a generic "Competitor ...". That is not a style choice: a
   mockup showing a named real business ranking #1 is a performance claim about
   that business, and a claim has to be substantiated (16 CFR 465). Addressed
   to the reader it cannot be mistaken for a claim about anyone. Never paste a
   real contractor's name, phone number or address in here.

   DARK SCREEN, drawn from the 3 reference screenshots Matt supplied on
   2026-09-26: the local finder, AI Mode, and a business profile panel, all in
   Google's dark theme. They were a light UI before. Dark is both closer to
   what he sent and a better neighbour for an oxblood ground, where a white
   rectangle punched a hole in the band.

   Built in markup rather than exported as images. At 3 cards x 3 breakpoints a
   raster set is 9 files to re-cut every time a word changes, and text in a PNG
   is invisible to search and unreadable when zoomed.
   -------------------------------------------------------------------------- */

/* Google's dark theme, scoped to the screens so none of it can leak into the
   page. Named for what they are in that UI, not for their hex. */
.scr{
  --g-bg:#1f2022; --g-sur:#2b2c2f; --g-sur2:#35363a;
  --g-line:#3f4043; --g-txt:#e3e5e8; --g-dim:#9aa0a6;
  --g-blue:#8ab4f8; --g-green:#81c995; --g-gold:#fdd663; --g-red:#ea4335;
}

/* LIT GROUND. The cards had depth and the band behind them did not, so 3
   raised objects sat on a flat rectangle and the section read flat anyway.

   The fix is light, not texture. Two layers and no pattern:

     ::before  an overhead wash, brightest at top centre and gone by the middle
     ::after   the floor the cards stand on

   The wash is aimed at the same place the cards say their light comes from.
   Every .proof carries `inset 0 1px 0` on its top edge, which is a claim about
   a source above and in front; without a matching gradient on the band that
   highlight is just a pale line. Now the two agree and the cards look lit
   rather than drawn.

   The floor is the other half of the perspective. .proofs already vanishes to
   50% 120% and the entrance rises off it, but nothing was ever visible there.
   An elliptical pool plus one hairline is the least that reads as a surface,
   and it lands under the cards where their long shadows already fall, so the
   shadows now terminate on something.

   Deliberately NOT contours, a grid, or a second photograph. The band's job is
   to hold 3 busy dark screens full of small type, and anything with structure
   in it competes with them. This is only light.                              */
.proof-sec{position:relative;overflow:hidden;isolation:isolate}
.proof-sec::before,
.proof-sec::after{content:"";position:absolute;z-index:-1;pointer-events:none}
.proof-sec::before{
  inset:0;
  background:radial-gradient(112% 62% at 50% -4%,
    color-mix(in srgb,var(--ox-2) 82%,transparent),
    transparent 64%);
}
/* A halo around the card cluster, NOT a floor under it. A floor line was the
   first attempt and it was geometrically impossible here: measured at 1440,
   the grid runs from 32% to 93% of the band, so there are 57px of section
   below the cards and anything drawn as a surface lands behind them, where 3
   opaque cards hide it.

   So the pool is centred ON the cluster instead. What shows is the part the
   cards do not cover: the 2 gutters and the page margins outside the 1180
   wrap. That turns the gaps between the cards into light rather than into
   holes, which is what makes them look like separate objects in one space
   instead of 3 tiles on a sheet.

   --accent at 7% is not a colour, it is a warmth. Anything stronger and the
   band starts competing with the cream CTA further down the page for which
   light source the page believes in. */
.proof-sec::after{
  left:0;right:0;top:26%;bottom:-8%;
  background:radial-gradient(58% 72% at 50% 50%,
    color-mix(in srgb,var(--accent) 7%,transparent),
    transparent 70%);
}
/* THE FLOOR SEAM. .slab's own seam opens the TOP of the band below this one;
   it cannot reach up into this one. This is the other half of the same join:
   the bottom third of the proof band resolves to --brown, so the two fades
   meet and the boundary between the cards and the reviews below stops being a
   line. Matt: the cards should be "more blended into the next section".

   On the section's own `background` rather than a third pseudo-element,
   because .proof-sec has no background of its own — it sits on the page's --ox
   — so the layer is free and there is nothing to stack against. The cards keep
   casting their shadows over the top of it, which is the point: the shadows
   now run out onto ground that is already the next section's colour.        */
.proof-sec{
  background:linear-gradient(180deg,
    color-mix(in srgb,var(--brown) 0%,transparent) 66%,
    color-mix(in srgb,var(--brown) 46%,transparent) 86%,
    var(--brown) 100%);
}

/* The perspective lives on the grid, not on the cards. One shared vanishing
   point is what makes 3 cards read as 3 objects on one floor; per-card
   perspective gives each its own horizon and they fan apart at the edges. */
.proofs{
  display:grid;grid-template-columns:repeat(3,minmax(0,1fr));
  gap:clamp(14px,1.6vw,22px);margin-top:clamp(28px,3vw,40px);
  align-items:start;
  perspective:1500px;perspective-origin:50% 120%;
}
@media (max-width:980px){
  .proofs{grid-template-columns:1fr;max-width:520px;margin-inline:auto}
}

.proof{
  position:relative;
  display:flex;flex-direction:column;
  padding:clamp(16px,1.5vw,20px);border-radius:14px;
  background:linear-gradient(180deg,
    color-mix(in srgb,var(--ox-2) 88%,var(--sand)) 0%,
    var(--ox-2) 66%);
  border:1px solid color-mix(in srgb,var(--sand) 18%,transparent);
  /* Four layers, and each one is doing a different job. The inset top
     highlight is the lit edge. The hairline ring keeps the card from bleeding
     into the ground where the shadow is darkest. Then a tight contact shadow
     and a long soft one: the tight one says the card is a solid object, the
     long one says it is well above the floor. Drop either and it flattens. */
  box-shadow:
    inset 0 1px 0 color-mix(in srgb,var(--sand) 26%,transparent),
    0 0 0 1px color-mix(in srgb,var(--ox-deep) 55%,transparent),
    0 2px 6px -1px color-mix(in srgb,var(--ox-deep) 70%,transparent),
    0 34px 60px -28px var(--ox-deep);
  transition:transform .45s var(--ease),box-shadow .45s var(--ease);
}
/* Lift on hover, and lift the shadow with it. Raising the card without
   lengthening the shadow reads as the card growing, not rising. */
.proof:hover{
  transform:translateY(-6px);
  box-shadow:
    inset 0 1px 0 color-mix(in srgb,var(--sand) 30%,transparent),
    0 0 0 1px color-mix(in srgb,var(--ox-deep) 55%,transparent),
    0 2px 6px -1px color-mix(in srgb,var(--ox-deep) 70%,transparent),
    0 48px 78px -30px var(--ox-deep);
}

/* THE RISE.
   Matt asked for the section to "animate and look 3d as if its coming from the
   bottom section up". The cards start below the line, pitched back on the X
   axis so you are looking at the underside, and swing upright as they arrive.
   transform-origin is the BOTTOM edge, which is the whole trick: hinging on
   the centre looks like a card flipping in place, hinging on the bottom looks
   like one standing up off the floor.

   The start state is gated behind .is-ready, which /js/reveal.js sets. Putting
   the hidden state in the CSS unconditionally means a failed script leaves the
   section permanently blank, and a blank section is a worse bug than an
   unanimated one. Reduced motion never gets .is-ready at all. */
.proofs.is-ready .proof{
  opacity:0;
  transform:translateY(70px) rotateX(20deg) scale(.95);
  transform-origin:50% 100%;
  transition:opacity .75s var(--ease),transform .95s var(--ease);
}
.proofs.is-ready.is-in .proof{opacity:1;transform:none}
.proofs.is-ready.is-in .proof:nth-child(2){transition-delay:.14s}
.proofs.is-ready.is-in .proof:nth-child(3){transition-delay:.28s}

.proof-k{
  display:flex;align-items:center;gap:8px;margin:0 0 12px;
  font-weight:800;font-size:10.5px;letter-spacing:.17em;text-transform:uppercase;
  color:var(--accent);
}
.proof-k svg{width:14px;height:14px;fill:currentColor;flex:none}

/* The screen. FIXED height, not min-height: the 3 mocks inside are different
   shapes and on min-height the tallest one pushes its card's headline out of
   line with the other two. A fixed box makes the 3 screens and the 3 headlines
   share a baseline. overflow:hidden so a future edit fails by cropping rather
   than by breaking the row. */
.scr{
  background:var(--g-bg);border-radius:10px;padding:10px;
  /* 300, set by the tallest of the 3 mocks (the local pack) with its map strip
     in. Measure with scrollHeight against clientHeight after any content edit:
     overflow here crops silently and the first thing to go is the bottom of
     whichever mock grew. */
  height:300px;overflow:hidden;
  display:flex;flex-direction:column;gap:6px;
  box-shadow:inset 0 0 0 1px var(--g-line),0 6px 16px -6px rgba(0,0,0,.7);
  /* Google's UI face, not the site's. A grotesque here would read as part of
     the page rather than as a picture of a browser. */
  font-family:Roboto,Arial,-apple-system,system-ui,sans-serif;
  font-size:10.5px;line-height:1.35;color:var(--g-txt);
}
.scr-bar{
  display:flex;align-items:center;gap:6px;flex:none;
  padding:5px 9px;background:var(--g-sur);border-radius:99px;
  font-size:10.5px;color:var(--g-dim);
}
.scr-bar svg{width:11px;height:11px;fill:var(--g-dim);flex:none}
.scr-h{margin:1px 0 0;font-size:11.5px;font-weight:500;color:var(--g-txt)}
.scr-star{color:var(--g-gold);letter-spacing:.4px;font-style:normal}

/* ---- MOCK 1: the local pack ----------------------------------------------
   Your row carries the full anatomy from the reference shot (rating, count,
   category, years, open state, and the action buttons). The 2 competitors
   under it are deliberately thinner and dimmer: that IS the pack, the first
   result is the one Google fills in. */
.pack{display:flex;gap:9px;padding:6px 5px;border-radius:7px;align-items:flex-start}
.pack+.pack{border-top:1px solid var(--g-line)}
.pack-main{min-width:0;flex:1 1 auto}
.pack b{display:block;font-size:11.5px;font-weight:500;line-height:1.3;color:var(--g-blue)}
.pack i{display:block;font-style:normal;margin-top:2px;font-size:9.5px;color:var(--g-dim)}
.pack .open{color:var(--g-green)}
/* Action column, the Website / Directions circles. Icon over a caption, which
   is what makes them read as Google's buttons rather than as links. */
.pack-act{display:flex;gap:7px;flex:none;padding-top:1px}
.pack-act span{display:grid;justify-items:center;gap:3px;font-size:8px;color:var(--g-blue)}
.pack-act svg{width:21px;height:21px;padding:5px;border-radius:50%;background:var(--g-sur2);fill:var(--g-blue);box-sizing:border-box}
.pack.is-you{background:color-mix(in srgb,var(--g-blue) 13%,transparent);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--g-blue) 34%,transparent)}
.pack.is-dim b{color:var(--g-txt);font-size:11px}
.pack.is-dim i{opacity:.8}

/* The map strip. A real map is the single most recognisable thing in that
   screenshot, so the mock gets one even at 44px tall: dark ground, 2 road
   strokes, and pins with yours the only one at full size. */
.pack-map{
  position:relative;flex:none;margin-top:auto;height:38px;border-radius:7px;overflow:hidden;
  background:#20343d;
}
.pack-map svg{position:absolute;inset:0;width:100%;height:100%}

/* ---- MOCK 2: AI Mode ------------------------------------------------------ */
.ai-tabs{display:flex;gap:10px;flex:none;font-size:9.5px;color:var(--g-dim);border-bottom:1px solid var(--g-line);padding-bottom:6px}
.ai-tabs b{font-weight:500;color:var(--g-txt);box-shadow:0 6px 0 -5px var(--g-txt)}
/* The query sits right, in a bubble, like a message you sent. That alignment
   is the one cue that separates AI Mode from a results page. */
.ai-q{align-self:flex-end;flex:none;padding:5px 11px;border-radius:99px;background:var(--g-sur);font-size:10px;color:var(--g-txt)}
.ai-a{margin:0;font-size:10px;line-height:1.55;color:var(--g-txt)}
.ai-a mark{background:none;color:var(--g-txt);font-weight:700;box-shadow:inset 0 -6px 0 color-mix(in srgb,var(--g-blue) 30%,transparent)}
.ai-biz{display:flex;gap:8px;align-items:flex-start;flex:none}
.ai-biz img{width:42px;height:42px;flex:none;border-radius:6px;object-fit:cover;display:block}
.ai-biz u{display:block;font-size:11px;color:var(--g-blue);text-decoration:underline;text-underline-offset:2px}
.ai-biz i{display:block;font-style:normal;margin-top:2px;font-size:9.5px;color:var(--g-dim)}
.ai-biz .open{color:var(--g-green)}
.ai-pills{display:flex;gap:5px;flex:none;flex-wrap:wrap}
.ai-pills span{display:inline-flex;align-items:center;gap:4px;padding:3px 9px;border:1px solid var(--g-line);border-radius:99px;font-size:9px;color:var(--g-blue)}
.ai-pills svg{width:10px;height:10px;fill:var(--g-blue)}
.ai-bul{margin:auto 0 0;font-size:9.5px;line-height:1.5;color:var(--g-dim)}
.ai-bul b{color:var(--g-txt);font-weight:700}

/* ---- MOCK 3: the business profile panel ----------------------------------- */
.gbp-head{flex:none}
.gbp-head b{display:block;font-size:12.5px;font-weight:500;color:var(--g-txt)}
.gbp-head i{display:block;font-style:normal;margin-top:2px;font-size:9.5px;color:var(--g-dim)}
.gbp-tabs{display:flex;gap:5px;flex:none}
.gbp-tabs span,.gbp-tabs b{padding:3px 9px;border:1px solid var(--g-line);border-radius:99px;font-size:9px;font-weight:400;color:var(--g-dim)}
.gbp-tabs b{border-color:var(--g-txt);color:var(--g-txt)}
.gbp-pics{display:grid;grid-template-columns:1fr 1fr;gap:3px;flex:none}
.gbp-pics img{width:100%;height:68px;object-fit:cover;display:block;border-radius:3px}
.gbp-pills{display:flex;gap:5px;flex:none;flex-wrap:wrap}
.gbp-pills span{padding:3px 9px;border:1px solid var(--g-line);border-radius:99px;font-size:9px;color:var(--g-blue)}
.gbp-rows{display:flex;flex-direction:column;margin-top:auto}
.gbp-row{display:flex;align-items:center;gap:8px;padding:5px 0;font-size:9.5px;color:var(--g-txt);border-top:1px solid var(--g-line)}
.gbp-row svg{width:13px;height:13px;flex:none;fill:var(--g-dim)}
.gbp-row .open{color:var(--g-green)}

.proof h3{
  margin:14px 0 0;font-size:clamp(16px,1.35vw,18px);line-height:1.25;color:var(--sand);
}
.proof p{margin:7px 0 0;font-size:13.5px;line-height:1.55;color:var(--sand-2)}

/* ---- REVIEWS ------------------------------------------------------------
   Three equal cards, centred head. This is the one place on the page where a
   matched three-up is the right call: the whole point of a review wall is that
   the quotes are peers, and ranking one above the others reads as cherry-
   picking. Every other list section on this page is deliberately asymmetric so
   that this one still looks like its own thing.                              */

/* The band sits on real job photos instead of flat brown — this is the
   section about actual customers, so it is the one place a job-site ground
   earns its keep. Three layers, bottom to top:
     .rev-montage  the photos, cross-fading one every three seconds
     ::after       a scrim that is solid --brown top and bottom and lets the
                   photos through only in the middle band, so the section blends
                   into the flat sections above and below instead of cutting a
                   rectangle
     .wrap         the content, lifted to z-index 1
   .revs-sec has to come AFTER .slab in this sheet: both are one class, so the
   background only wins on source order.

   SHARED. Homepage and /reviews both mount this. It lived in the homepage's
   inline <style> until 2026-09-26, when /reviews needed the same ground;
   copying it would have left two copies to drift apart, which is the exact
   trap .topo already documents. */
/* This declares `background`, so it replaces .slab's whole layer stack and has
   to restate the seam itself. See the note on .slab. */
.revs-sec{
  position:relative;overflow:hidden;
  background:
    linear-gradient(180deg,
      var(--neighbour) 0,
      color-mix(in srgb,var(--neighbour) 46%,transparent) 44px,
      color-mix(in srgb,var(--neighbour) 0%,transparent) 110px),
    linear-gradient(0deg,
      var(--neighbour) 0,
      color-mix(in srgb,var(--neighbour) 46%,transparent) 44px,
      color-mix(in srgb,var(--neighbour) 0%,transparent) 110px),
    var(--brown);
}
/* The veil over the montage. It ends in --neighbour rather than --brown at
   both extremes, which is the same seam the background under it draws — the
   veil is opaque at 0% and 100%, so a --brown end stop here would paint the
   seam out and the whole thing would have been wasted. */
.revs-sec::after{
  content:"";position:absolute;inset:0;z-index:0;pointer-events:none;
  background:linear-gradient(180deg,
    var(--neighbour) 0%,
    color-mix(in srgb,var(--brown) 58%,transparent) 30%,
    color-mix(in srgb,var(--ox-deep) 46%,transparent) 58%,
    var(--neighbour) 100%);
}
.revs-sec>.wrap{position:relative;z-index:1}

/* PHOTO MONTAGE. Real <img> elements rather than a CSS background so the
   browser can decode all six up front — a background-image swapped on a timer
   fetches on first paint of that frame, which shows as a flash of empty brown
   the first time round the loop.

   The whole stack is held at 20% by the wrapper, so the individual frames
   animate a clean 0→1 and nobody has to reason about two multiplied opacities.
   One shared 18s timeline, each frame offset by a negative delay: frame n is
   opaque for its own three seconds and transparent for the other fifteen. Add
   or remove a photo and you must retune BOTH the keyframe percentages and the
   delays — 100/n is the frame width, and the fade is ~15% of that.

   All six frames are HVAC or roofing, alternating, and they have to stay that
   way. This band used to open on a junk removal crew and also carried
   plumbing, pressure washing and hardscaping, from when the shop took every
   trade. Two photos of the same trade back to back reads as a stall in the
   cross-fade rather than a change of picture, hence the alternation. */
.rev-montage{position:absolute;inset:0;z-index:0;pointer-events:none;opacity:.2}
.rev-montage img{
  position:absolute;inset:0;width:100%;height:100%;
  object-fit:cover;object-position:center 42%;
  opacity:0;animation:revshot 18s linear infinite;
}
.rev-montage img:nth-child(1){animation-delay:0s}
.rev-montage img:nth-child(2){animation-delay:-15s}
.rev-montage img:nth-child(3){animation-delay:-12s}
.rev-montage img:nth-child(4){animation-delay:-9s}
.rev-montage img:nth-child(5){animation-delay:-6s}
.rev-montage img:nth-child(6){animation-delay:-3s}
/* The hold runs to 16.7% — the full width of the frame — and the fade-out
   happens AFTER it, overlapping the next frame's fade-in. Ending the hold
   early instead would fade one photo out before the next came in, and the
   brown ground would flash through between every pair. */
@keyframes revshot{
  0%{opacity:0}
  2.8%{opacity:1}
  16.7%{opacity:1}
  19.5%{opacity:0}
  100%{opacity:0}
}
/* Frozen on the first frame rather than hidden: the band is designed around
   having a photograph under it, and an empty one reads as a broken image. */
@media (prefers-reduced-motion:reduce){
  .rev-montage img{animation:none}
  .rev-montage img:nth-child(1){opacity:1}
}

/* The card component itself (.rev-head, .revs, .rev, .rev-top, .rev-face,
   .rev-stars, .rev-job) is immediately below. */

/* ---- REVIEW CARDS --------------------------------------------------------
   Used on the homepage (as a marquee) and on /reviews (as a grid).

   SITE PALETTE. These were Google's white card in Google's greys until
   2026-09-26, on the argument that a review should look lifted from somewhere
   the site cannot edit. Matt asked for branded instead: *"Lets just make it
   branded for now instead of looking like google."* The white rectangles were
   also the loudest thing on the page, which is the wrong ranking for a band
   that sits under the proof cards.

   No Google mark and no timestamp. The mark was the strongest "this came from
   Google" signal and it has to go with the rest of it. The timestamp went
   because Matt asked for it to.
   -------------------------------------------------------------------------- */
.rev-head{text-align:center;max-width:none}
/* 34ch, not the 22ch a centred head usually wants. At 22ch "Contractors who
   made the switch." wrapped to 2 lines and put the testimonials band 28px past
   the bottom of a 900px screen. The cap is here to stop a long h2 running the
   full 1180, not to force a break into a short one. */
.rev-head h2{max-width:34ch;margin:0 auto}
.rev-head .lede{max-width:56ch;margin:16px auto 0}

/* Grid form, used on /reviews where the page exists to show all of them. */
.revs{
  display:grid;grid-template-columns:repeat(3,minmax(0,1fr));
  gap:clamp(14px,1.5vw,20px);margin-top:clamp(28px,3vw,40px);
  align-items:start;
}
@media (max-width:1040px){.revs{grid-template-columns:repeat(2,minmax(0,1fr))}}
@media (max-width:680px){.revs{grid-template-columns:1fr;max-width:460px;margin-inline:auto}}

/* ---- MARQUEE FORM (homepage) ---------------------------------------------
   One line, moving left to right, running full bleed past both gutters.

   The rail is the window and the track is the belt. The track holds the 6
   cards TWICE and slides by exactly -50%, which puts copy 2 where copy 1
   started at the moment the animation restarts, so the loop has no seam. Any
   other distance and the cards jump. The duplicate is aria-hidden in the
   markup for the obvious reason.

   Direction: the keyframe runs -50% -> 0, so the belt travels to the RIGHT.
   0 -> -50% would be the more usual ticker and is the wrong way round here.

   The mask fades both ends into the band instead of letting cards get guillotined
   at the viewport edge. Two properties, unprefixed and -webkit-, because Safari
   still wants the prefix for mask-image on a composited layer. */
.revs-sec>.rev-rail{position:relative;z-index:1}
.rev-rail{
  margin-top:clamp(28px,3vw,40px);overflow:hidden;
  mask-image:linear-gradient(90deg,transparent,#000 7%,#000 93%,transparent);
  -webkit-mask-image:linear-gradient(90deg,transparent,#000 7%,#000 93%,transparent);
}
.rev-track{
  display:flex;align-items:stretch;gap:clamp(14px,1.5vw,20px);
  width:max-content;
  animation:rev-slide 68s linear infinite;
}
/* Hovering a card you are reading and having it keep moving is the one thing
   that makes a marquee feel broken, so the belt stops. */
.rev-rail:hover .rev-track{animation-play-state:paused}
@keyframes rev-slide{
  from{transform:translate3d(-50%,0,0)}
  to{transform:translate3d(0,0,0)}
}
/* No animation at all under reduced motion. The rail becomes a normal
   horizontal scroller so the content is still reachable, and the duplicate
   half is hidden outright so nobody scrolls into 6 repeats. */
@media (prefers-reduced-motion:reduce){
  .rev-track{animation:none}
  .rev-rail{overflow-x:auto;mask-image:none;-webkit-mask-image:none}
  .rev-track .rev[aria-hidden="true"]{display:none}
}
/* Fixed width in the rail, because flex would otherwise size every card to its
   own quote and the belt would read as ragged rather than as a row. */
.rev-rail .rev{width:clamp(280px,25vw,340px);flex:none}

/* One place defines the card material. The /audit proof cards (.pf-sms,
   .pf-rank, .pf-stat) rode along on this selector until 2026-10-11, when
   that page swapped its 2 invented belts for 4 real client results and
   the whole .pf family went with them. .rev is the only caller again. */
.rev{
  margin:0;display:flex;flex-direction:column;
  padding:20px;border-radius:12px;
  background:linear-gradient(180deg,
    color-mix(in srgb,var(--ox-2) 88%,var(--sand)) 0%,
    var(--ox-2) 70%);
  border:1px solid color-mix(in srgb,var(--sand) 16%,transparent);
  /* Same 4 layer recipe as .proof so the 2 bands read as the same material:
     lit top edge, containing ring, contact shadow, long soft shadow. */
  box-shadow:
    inset 0 1px 0 color-mix(in srgb,var(--sand) 24%,transparent),
    0 2px 6px -1px color-mix(in srgb,var(--ox-deep) 70%,transparent),
    0 28px 52px -26px var(--ox-deep);
  transition:transform .25s var(--ease),box-shadow .25s var(--ease),border-color .25s var(--ease);
}
.rev:hover{
  transform:translateY(-4px);
  border-color:color-mix(in srgb,var(--accent) 50%,transparent);
  box-shadow:
    inset 0 1px 0 color-mix(in srgb,var(--sand) 28%,transparent),
    0 2px 6px -1px color-mix(in srgb,var(--ox-deep) 70%,transparent),
    0 40px 68px -28px var(--ox-deep);
}
@media (prefers-reduced-motion:reduce){.rev,.rev:hover{transform:none;transition:none}}

/* Header row: initial, then name over company. */
.rev-top{display:flex;align-items:center;gap:12px;margin-bottom:12px}
.rev-face{
  flex:0 0 auto;width:40px;height:40px;border-radius:50%;overflow:hidden;
  display:grid;place-items:center;
  /* One accent disc for every card now. The per-reviewer hues were Google's
     convention and they went with the rest of it; 6 different colours in the
     site's own palette would just look like 6 mistakes. */
  background:var(--accent);
  font-weight:800;font-size:17px;line-height:1;color:var(--ink);
}
.rev-face img{width:100%;height:100%;object-fit:cover;display:block}
.rev-id{min-width:0}
.rev-id b{
  display:block;font-family:inherit;font-weight:700;font-size:14.5px;
  line-height:1.3;letter-spacing:0;color:var(--sand);
}
.rev-id span{display:block;margin-top:1px;font-size:12.5px;color:var(--sand-2)}

.rev-stars{display:flex;gap:1px;margin-bottom:9px}
.rev-stars svg{width:16px;height:16px;fill:var(--accent);display:block}

.rev blockquote{
  margin:0;font-size:14px;line-height:1.58;color:var(--sand-2);
  /* Normal weight and normal quotes. A review is not a pull quote: italics,
     hanging punctuation or a display face all make it read as the site
     talking about itself in a nicer font. */
  font-style:normal;
}
/* margin-top:auto, so in the marquee (where the cards are stretched to a
   common height but the quotes are not a common length) the tag line sits on
   one baseline across the whole belt instead of floating up under short
   quotes. In the grid the cards are not stretched, so it does nothing. */
.rev-job{
  margin:auto 0 0;padding-top:11px;border-top:1px solid var(--line);
  font-size:11.5px;font-weight:700;letter-spacing:.09em;text-transform:uppercase;
  color:var(--accent);
}

/* PLACEHOLDER STATE — see the banner comment above the reviews markup.
   Sample copy is styled IDENTICALLY to a real review on purpose, because the
   point of the sample is to judge the design. The marker is the data
   attribute in the HTML, not a visual treatment. `node /tmp/…` is not needed:
   grep for data-sample to find every one of them. */

/* ---- FOOTER -------------------------------------------------------------- */
/* background-COLOR, not the `background` shorthand, and the distinction is
   load bearing for exactly 1 page. .foot is declared at line ~1936 and .topo
   at ~667, same specificity, so whichever sets `background` last wins the
   whole property. As a shorthand this line was resetting background-image to
   none, which meant `class="foot topo"` painted a flat --ox-deep strip with
   the contour field silently gone and nothing logged. /audit's footer needs
   both: topo's gradients, this colour under them.

   Identical rendering on every other footer in the repo. All of them carry
   `class="foot"` on its own with no other background source, and for those
   the shorthand and the longhand produce the same pixels. */
.foot{background-color:var(--ox-deep);border-top:1px solid var(--line);padding:46px 0 38px}
.foot-g{display:grid;grid-template-columns:1fr;gap:28px}
@media (min-width:760px){.foot-g{grid-template-columns:1.5fr 1fr 1fr;gap:40px}}
/* h5, not h2 — an h2 here would inherit the 44px display setting and need a
   full override, and these are column labels, not section headings. */
.foot h5{font-size:11px;letter-spacing:.18em;text-transform:uppercase;font-weight:700;color:var(--sand-2);margin:0 0 12px;line-height:1.4}
.foot p{font-size:13.5px;margin:0;max-width:46ch;line-height:1.7}
.foot a{font-size:13.5px;font-weight:600}
.foot .mark{margin:0 0 12px;display:inline-block}
/* --sand-3 at 12.5px is 5.59:1 on the --ox-deep footer. This line is why the
   token was raised from .50, where it sat at 4.44 and Lighthouse failed it. */
.foot-legal{margin-top:30px;padding-top:20px;border-top:1px solid var(--line);font-size:12.5px;color:var(--sand-3);
  display:flex;flex-wrap:wrap;gap:8px 22px;justify-content:space-between}

/* ==========================================================================
   SHARED SECTION COMPONENTS: WORKS ALONGSIDE, AND THE NINETY DAYS.

   BOTH OF THESE LIVED IN THE <style> BLOCK IN index.html until 2026-10-09,
   when /audit needed them too. They moved rather than being copied, which is
   the same call that was made for the review montage on 2026-09-26 and for
   .topo before that. Edit them HERE. A second copy in a page-level sheet is
   how 2 pages that are supposed to look identical quietly stop.

   Neither one is modified by the move. The only edits are to the prose in
   these comments, where some em-dashes were cleaned up on the way across.

   WHAT EACH ONE NEEDS FROM THE PAGE USING IT:
     .along-band   2 identical .along-track children. The loop depends on it.
     .phase-sec    the ground is /images/sections/how-this-goes.jpg, set in
                   CSS, so the page needs no <img>. The rail draws itself only
                   if js/reveal.js is loaded and .phases carries [data-reveal];
                   without that script the rail and all 5 nodes simply render
                   in their finished state, which is a correct static diagram.
   ========================================================================== */

/* ==========================================================================
   WORKS ALONGSIDE : its own band between the hero and the cream slab.

   This used to sit INSIDE the light slab with the marks desaturated to 42%
   on cream, and it read as a washed-out afterthought. Two things fixed it:
   a dark ground of its own, and white marks instead of grey ones.

   It also replaces the elevation rule that used to divide these two:
   hairline / centred label / hairline, which is now the label row inside the
   band. The page still gets its divider; it just does a job as well.
   ========================================================================== */
.along-band{
  position:relative;overflow:hidden;
  background:
    radial-gradient(130% 190% at 50% 0%,color-mix(in srgb,var(--accent) 8%,transparent),transparent 72%),
    var(--ox-deep);
  border-block:1px solid var(--line);
}
/* A lit top edge. One hairline is what separates "a darker rectangle" from
   "a machined strip": it catches the eye at the seam with the hero. */
.along-band::before{
  content:"";position:absolute;inset:0 0 auto;height:1px;z-index:2;
  background:linear-gradient(90deg,transparent,color-mix(in srgb,var(--accent) 50%,transparent),transparent);
}
.along{position:relative;z-index:1;padding-block:clamp(26px,3.2vw,40px) clamp(24px,2.9vw,36px)}

/* The old elevation rule, kept as the band's header. */
.along-lab{display:flex;align-items:center;gap:clamp(14px,3vw,26px);margin-bottom:clamp(20px,2.6vw,30px)}
.along-lab::before,.along-lab::after{content:"";flex:1;height:1px;background:var(--line)}
.along-lab .label{flex:0 0 auto;color:var(--sand-3)}

/* Marquee. TWO sibling tracks, each holding an identical set and each
   min-width:100%. Together they span 200% of the band, so sliding both by
   exactly -100% lands track two where track one started, seamless at any
   viewport width. A single track with a -50% keyframe breaks the moment the
   set is narrower than the screen: you get a visible gap every loop.
   ADD OR REMOVE A LOGO IN BOTH TRACKS OR THE LOOP JUMPS. */
.along-marquee{
  display:flex;overflow:hidden;
  -webkit-mask-image:linear-gradient(90deg,transparent,#000 11%,#000 89%,transparent);
          mask-image:linear-gradient(90deg,transparent,#000 11%,#000 89%,transparent);
}
/* NO `gap` here on purpose. space-around already gives every item a half-unit
   of space on each side, so the join between the two tracks (last item's right
   half + first item's left half) equals one full internal gap and the loop
   reads as evenly spaced. Adding `gap` on top widens the internal gaps but not
   the seam, and you get a visible hitch once a loop. */
.along-track{
  flex:0 0 auto;min-width:100%;
  display:flex;align-items:flex-end;justify-content:space-around;
  animation:alongScroll 38s linear infinite;will-change:transform;
}
@keyframes alongScroll{from{transform:translateX(0)}to{transform:translateX(-100%)}}
.along-marquee:hover .along-track,
.along-marquee:focus-within .along-track{animation-play-state:paused}

/* system.css only clamps animation-duration under reduced motion, which would
   snap the track to its -100% end state and hide every logo. Kill the
   animation outright and let the band scroll by hand instead. */
@media (prefers-reduced-motion:reduce){
  .along-track{animation:none;transform:none}
  .along-marquee{overflow-x:auto;-webkit-mask-image:none;mask-image:none}
}

/* Every item is the SAME width, which is what makes the row read as evenly
   spaced. The art runs from a square Jobber mark to a wide Meta wordmark;
   sized to their own aspect ratios they sit at wildly uneven centres and the
   band looks clumped even though the gaps are mathematically equal. */
.along-item{
  display:flex;flex-direction:column;align-items:center;justify-content:flex-end;
  gap:11px;flex:0 0 auto;
  width:clamp(124px,13.5vw,176px);padding-inline:12px;
}
/* Flattened to white, NOT desaturated. grayscale() leaves Jobber at mid-grey
   and the ServiceTitan mark at near-black, which on an ox-deep band means one
   logo glows and the next one vanishes. brightness(0) invert(1) crushes every
   source colour to the same flat white and the row reads as one material.
   (The old cream band needed the opposite treatment. Do not copy that back.) */
.along-item img{
  height:clamp(22px,2.4vw,30px);width:auto;max-width:100%;object-fit:contain;
  filter:brightness(0) invert(1);
  opacity:.72;transition:opacity .3s var(--ease);
}
/* Square marks get their own height. Matched to a single height the wordmarks
   run ~150px wide and the marks ~33px, and the row reads as three logos and
   three specks. Optical balance beats a shared number here: sized up, a square
   mark occupies about the same AREA as a wordmark, which is what the eye is
   actually comparing. Add a logo, decide which of the two it is. */
.along-item img.along-mark{height:clamp(32px,3.6vw,45px)}
.along-item:hover img{opacity:1}
/* Half the set is marks-only (ServiceTitan, Google Ads), so the captions have
   to stay and have to be readable. --sand-2, not --sand-3: at 9.5px this is
   the smallest type on the page and --sand-3 is 3.6:1. */
.along-cap{font-weight:700;font-size:9.5px;letter-spacing:.18em;text-transform:uppercase;color:var(--sand-2)}

/* ---- THE NINETY DAYS ----------------------------------------------------
   Header beside a photo, then the timeline runs left-to-right underneath.

   Horizontal on purpose: the claim is that this is a sequence with a
   duration, and left-to-right says that. A stacked list just says "five
   things," and it also ran the section past a laptop fold.

   The rail and ticks are neutral --line/--sand, never accent. FIVE STEPS
   below owns the only accent markers on this page, and the three list
   sections have to stay distinct: OUTCOMES is numbered, FIVE STEPS is
   EL-coded, this one is a timeline.                                        */
.phase-top{
  /* Copy only now. The right-hand column held the roofer photograph; the
     photograph is the ground. The reserved width stays, because the headline
     has to keep clearing the part of the frame that is still at full strength
     on the right. */
  display:grid;grid-template-columns:minmax(0,1.2fr) minmax(0,.85fr);
  gap:clamp(24px,4vw,56px);align-items:center;
  min-height:clamp(200px,20vw,286px);
}
@media (max-width:860px){.phase-top{grid-template-columns:1fr;gap:26px;min-height:0}}
.phase-top h2{max-width:19ch;margin-bottom:.45em}
.phase-top .lede{max-width:50ch;margin:0}

/* ---- THE GROUND ---------------------------------------------------------
   The same roofer frame that used to be a boxed <img> beside the copy, run
   full bleed and dissolved left to right so the headline sits on clean
   oxblood and the picture is only ever at strength out past the last word.

   Two pseudo-elements rather than one, because the fade has to run in two
   directions at once and mask-composite is the sharp edge of browser support.
   ::before does the horizontal with a mask; ::after does the vertical with an
   ordinary gradient painted over the top. Same result, nothing exotic.

   No <img> means no alt text, which is correct: the picture is not carrying
   information any more, the five steps under it are.                        */
.phase-sec{position:relative;overflow:hidden;isolation:isolate}
.phase-sec>.wrap{position:relative;z-index:1}
/* The frame is DARKENED at the source, not faded. Fading a photograph with a
   bright sky in it over a dark ground raises the black point and the whole
   band goes milky: the picture stops being a picture and becomes fog. So the
   image runs at full opacity with brightness pulled down below the ground's
   own value, and the mask does the dissolving. The result reads as a
   photograph lit from behind the panel rather than a transparency laid on it.

   The frame is mirrored. The roofer faces right in the original, which points
   the one piece of implied direction in the section off the edge of the page;
   flipped, he faces back into the headline. */
.phase-sec::before{
  content:"";position:absolute;z-index:-2;pointer-events:none;
  inset:0 0 auto auto;width:min(62%,940px);height:100%;
  background:url(/images/sections/how-this-goes.jpg) center right/cover no-repeat;
  transform:scaleX(-1);
  filter:brightness(.52) saturate(.4) contrast(1.14) sepia(.34);
  /* 270deg, NOT 90deg. The transform above flips the element and everything
     painted into it, the mask included, so a mask written left-to-right comes
     out right-to-left and the photo ends up solid at the headline and gone at
     the page edge: a hard vertical seam straight down the middle of the band.
     The gradient is written backwards so that the flip lands it forwards.
     Change the transform and this has to change with it. */
  -webkit-mask:linear-gradient(270deg,transparent 0%,
    color-mix(in srgb,#000 18%,transparent) 34%,
    color-mix(in srgb,#000 62%,transparent) 64%,#000 100%);
          mask:linear-gradient(270deg,transparent 0%,
    color-mix(in srgb,#000 18%,transparent) 34%,
    color-mix(in srgb,#000 62%,transparent) 64%,#000 100%);
}
/* The wash. Oxblood at full strength top and bottom and thinnest across the
   headline row, so the photo is at its strongest exactly where the copy is
   not. It closes back up before the timeline, which is what lets that row run
   the full width on clean ground instead of across a picture. */
.phase-sec::after{
  content:"";position:absolute;z-index:-1;inset:0;pointer-events:none;
  background:linear-gradient(180deg,
    var(--ox) 0%,
    color-mix(in srgb,var(--ox) 34%,transparent) 26%,
    color-mix(in srgb,var(--ox) 54%,transparent) 50%,
    color-mix(in srgb,var(--ox) 92%,transparent) 72%,
    var(--ox) 100%);
}
/* Under 860 the copy is full width and there is no empty column for the photo
   to live in, so it drops to a hint rather than sitting behind the headline. */
@media (max-width:860px){
  .phase-sec::before{width:100%;filter:brightness(.26) saturate(.4) contrast(1.14) sepia(.34)}
}

/* The rail is a GRADIENT, not a flat rule: it leaves day zero at --line and
   arrives at --accent on day ninety, so the row reads as progress rather than
   as five columns that happen to sit under a line. The nodes track the same
   run, ending on a filled dot with a soft halo so the eye lands on the payoff
   instead of trailing off the right edge. */
.phases{
  position:relative;
  display:grid;grid-template-columns:repeat(5,minmax(0,1fr));
  gap:clamp(12px,1.6vw,24px);
  margin-top:clamp(28px,3.4vw,42px);padding-top:30px;
}
.phases::before{
  content:"";position:absolute;top:5px;left:0;right:0;height:1px;
  background:linear-gradient(90deg,var(--line),var(--accent));
}
.phase{position:relative}
/* -30px, not 0. That 30px is the padding-top on .phases, which is the strip the
   rail sits in, so the grid items themselves begin BELOW the line. Pulling the
   node back up by exactly that padding is what lands it ON the rail instead of
   on top of the day label. Change .phases padding-top, change this too. */
.phase::before{
  content:"";position:absolute;top:-30px;left:0;width:11px;height:11px;
  border-radius:50%;background:var(--sand-3);
}
.phase:nth-child(4)::before{background:color-mix(in srgb,var(--accent) 65%,var(--sand-3))}
.phase:nth-child(5)::before{
  background:var(--accent);
  box-shadow:0 0 0 5px color-mix(in srgb,var(--accent) 15%,transparent);
}

/* THE RAIL DRAWS ITSELF, day zero to day ninety.
   Same [data-reveal] hook the cards use, so the arming class is set by
   js/reveal.js and never by the stylesheet. A section that says "here is how
   90 days goes" and then paints the 90 days in one frame is a diagram; drawing
   it left to right is the claim itself.

   scaleX on the existing gradient, not a width transition: width relayouts the
   band on every frame, transform runs on the compositor. The gradient scales
   with it, which is the good kind of accident: the line is --line at its
   origin and reaches --accent only as it arrives at day ninety.

   TIMING IS LOAD-BEARING HERE. reveal.js strips both classes at 1500ms, so
   everything has to be finished before then or it snaps. The rail takes 1.1s,
   and the nodes are offset to land just behind the line rather than on it:
   the last one starts at .87s and ends at 1.32s, 180ms of headroom. Lengthen
   any of these and raise DONE in js/reveal.js to match. */
.phases.is-ready::before{transform:scaleX(0);transform-origin:0 50%;
  transition:transform 1.1s var(--ease)}
.phases.is-ready.is-in::before{transform:scaleX(1)}

.phases.is-ready .phase::before{opacity:0;transform:scale(.3);
  transition:opacity .45s var(--ease),transform .45s var(--ease)}
.phases.is-ready.is-in .phase::before{opacity:1;transform:scale(1)}
.phases.is-ready.is-in .phase:nth-child(1)::before{transition-delay:.15s}
.phases.is-ready.is-in .phase:nth-child(2)::before{transition-delay:.33s}
.phases.is-ready.is-in .phase:nth-child(3)::before{transition-delay:.51s}
.phases.is-ready.is-in .phase:nth-child(4)::before{transition-delay:.69s}
.phases.is-ready.is-in .phase:nth-child(5)::before{transition-delay:.87s}

.phase-when{
  display:block;font-weight:800;font-size:11px;letter-spacing:.18em;
  text-transform:uppercase;color:var(--sand-3);margin-bottom:9px;
  font-variant-numeric:tabular-nums;
}
/* The last column is the promise the section is selling, so its label gets the
   accent the rail has been building toward. */
.phase:nth-child(5) .phase-when{color:var(--accent)}
.phase h3{font-size:clamp(15px,1.35vw,18.5px);line-height:1.22;margin-bottom:.45em}
.phase p{font-size:14px;line-height:1.55;margin:0;color:var(--sand-2)}

/* Five columns stop being readable well before the nav breaks, and the rail
   only works as a rail while the phases are a single row: wrap them onto two
   or three rows and four of the five nodes have no line under them. So there is
   no intermediate two-up state: it goes straight from five-across to one
   column with the rail stood on its end. */
@media (max-width:900px){
  /* The rail turns vertical: it runs down the left edge and the nodes sit on
     it, which keeps the timeline reading as a timeline instead of collapsing
     into a plain stacked list. */
  .phases{grid-template-columns:1fr;gap:0;padding:0 0 0 26px}
  .phases::before{top:6px;bottom:6px;left:5px;right:auto;width:1px;height:auto;
    background:linear-gradient(180deg,var(--line),var(--accent))}
  .phase{padding:0 0 22px}
  .phase:last-child{padding-bottom:0}
  .phase::before{top:5px;left:-26px}
  /* The rail is vertical here, so it has to draw downward. scaleX(0) would
     leave a full-height line squashed to nothing horizontally, which on a 1px
     rule means it vanishes and then appears, not a draw at all. */
  .phases.is-ready::before{transform:scaleY(0);transform-origin:50% 0}
  .phases.is-ready.is-in::before{transform:scaleY(1)}
}

/* ==========================================================================
   THE VIDEO FACADE: .vid-thumb / .vid-play / .vid-len / .vid h3
   --------------------------------------------------------------------------
   MOVED HERE FROM /css/resources.css ON 2026-10-09. Not copied. /audit now
   carries a VSL above its booking calendar, using the same markup the /content
   video row uses, and 2 copies of a component is how they drift apart.

   NOT THE HOMEPAGE. A version of this went onto the homepage hero first and
   was reverted the same day (see ebd7340 and f632035). The homepage hero keeps
   its rotating photographs. The VSL belongs on the YouTube landing page, which
   is where the people who have already watched something arrive.

   js/video.js is what makes it a facade: the markup ships as a plain <a> with
   a local still, and the YouTube iframe is only injected on the first click.
   That script is loaded per page, and any page using these classes needs it.

   .vid-row, the 3-up grid, stayed in resources.css. It is a layout for that
   page's video listing and nothing else uses it.

   Only comment prose changed in the move. The rules are byte-identical.
   ========================================================================== */

.vid-thumb{
  position:relative;display:block;overflow:hidden;
  aspect-ratio:16/9;
  background:var(--ox-deep);
  border:1px solid var(--line);
  transition:border-color .25s var(--ease);
}
.vid-thumb img{width:100%;height:100%;object-fit:cover;display:block}
.vid-thumb iframe{width:100%;height:100%;border:0;display:block}
.vid:hover .vid-thumb{border-color:color-mix(in srgb,var(--accent) 62%,transparent)}

/* Play button. A filled disc rather than an outline. This sits on a photo
   that could be any colour, and an outline glyph disappears on half of them. */
.vid-play{
  position:absolute;left:50%;top:50%;translate:-50% -50%;
  width:clamp(46px,4.2vw,58px);aspect-ratio:1;border-radius:50%;
  display:grid;place-items:center;
  background:var(--sand);
  box-shadow:0 10px 30px -8px var(--ox-deep);
  transition:background .2s var(--ease),scale .25s var(--ease);
}
/* The triangle is a bordered box with three transparent sides: no SVG file
   and no icon font for one glyph. Nudged right because a triangle's optical
   centre sits left of its bounding box. */
.vid-play::after{
  content:"";margin-left:15%;
  border-style:solid;border-width:8px 0 8px 13px;
  border-color:transparent transparent transparent var(--ink);
}
.vid:hover .vid-play{background:var(--accent);scale:1.06}

/* Runtime, bottom-right, over its own scrim so it stays legible on a bright
   still. Real numbers, because it is the main thing that decides whether somebody
   presses play, so it is worth the pixels. */
.vid-len{
  position:absolute;right:9px;bottom:9px;
  padding:3px 8px;border-radius:4px;
  background:color-mix(in srgb,var(--ink) 82%,transparent);
  color:var(--cream);
  font-weight:700;font-size:11.5px;letter-spacing:.04em;font-variant-numeric:tabular-nums;
}

/* Title under the still and nothing else: no description, no meta row, no
   read-more. The thumbnail is the pitch. */
.vid h3{margin:13px 0 0;font-size:15.5px;line-height:1.34;font-weight:700}
/* Sibling of the thumb link, never nested inside it: a link inside a link is
   invalid, and these are 2 real destinations: play here, or open YouTube. */
.vid h3 a{color:inherit;text-decoration:none;transition:color .2s var(--ease)}
.vid:hover h3 a{color:var(--accent)}

@media (prefers-reduced-motion:reduce){
  .vid-play{transition:none}
  .vid:hover .vid-play{scale:1}
}

@media (prefers-reduced-motion:reduce){
  *,*::before,*::after{animation-duration:.01ms !important;transition-duration:.01ms !important}
}
