/* ==========================================================================
   Blackwood Cycles — production stylesheet
   Direction A · Industrial. Black ground, molten orange, heavy condensed type.

   Design rules held throughout this file:
   - Square corners only. No border-radius anywhere.
   - Depth comes from 1px rules and surface steps, never from box-shadow.
   - Gradients are functional (legibility scrims) or textural (hero ground),
     never decorative colour washes.
   - One backdrop-blur, on the stuck header and the trust bar. Nothing else.
   ========================================================================== */

:root{
  /* ---- colour ---- */
  --canvas:#0A0908;        /* page ground */
  --surface:#131110;       /* raised panels, alternating sections */
  --surface-2:#1B1817;     /* inputs, hovered cells */
  --rule:#26221F;          /* hairlines */
  --rule-strong:#3A3430;   /* button outlines, emphasised dividers */
  --ink:#F5F1EA;           /* primary text */
  --ink-soft:#A79E93;      /* body text on dark — 7.6:1 on canvas */
  --ink-faint:#8A8177;     /* mono labels — 5.1:1 on canvas, AA at small sizes */
  /* Fire-engine red, matched to the homepage token --sc-accent. This used to be
     #FF5A1F "molten orange" and it was the single worst brand miss on the site:
     the client's one stated colour rule is no orange, and #FF5A1F sits at hue
     15.8deg with G90 — further into orange than the #FF453A the homepage had
     already been pulled off. The homepage got migrated and these eleven pages
     did not, so the site shipped fire-engine red on / and orange everywhere else.

     What makes a red read as orange is the GREEN channel, not the hue name.
     G90 -> G29 drops the hue to 358.1deg, i.e. just past pure red, so it leans
     blue rather than orange under any lighting.

     Measured on this file's own tokens, not assumed: 5.17:1 on --canvas and
     4.89:1 on --surface, so the accent still clears AA as text on both grounds.
     It buys that with less headroom than the orange had (6.38 and 6.04), which
     is the real cost of the change and is why accent type stays at label size
     and above, never in body copy. */
  --accent:#FF1D24;        /* 5.17:1 on canvas, 4.89:1 on surface */
  --accent-ink:#0E0405;    /* text on accent — 5.2:1. Neutral, not the old warm
                              brown-black, which was mixed for an orange. */
  --accent-dim:#B00D1B;    /* 2.77:1 — fills and glows only, never type */
  --ok:#5BD08A;
  --err:#FF6B5A;

  /* ---- type ---- */
  --font-display:"Archivo Black","Helvetica Neue",sans-serif;
  --font-text:"Archivo","Helvetica Neue",sans-serif;
  --font-mono:"JetBrains Mono",ui-monospace,monospace;
  /* min 42px, not 46: "RIDE IT OUT." measures 6.59em wide in Archivo Black at
     -.035em tracking, so 46px needs 303px of column. A 320px viewport at the
     20px gutter only offers 280px. 42px needs 277px and clears it. */
  --h1:clamp(42px,9.2vw,132px); --h1-lh:.84; --h1-track:-.035em;
  --h1-weight:400; --h1-case:uppercase;
  --h2:clamp(34px,5.4vw,74px); --h2-lh:.9;
  --h3:clamp(24px,2.6vw,34px);
  --mark-track:.13em; --mark-weight:400; --btn-weight:400;
  /* Floor for every tracked mono micro-label (eyebrows, captions, trust labels,
     step numbers, footer column heads). The first pass let these drift to 9–11px,
     which is squint-territory and reads as "cheap template" no matter how good
     the rest of the page is. Nothing on the site sets type below this token. */
  --label:12px; --label-track:.2em;

  /* ---- spacing scale (4px base) ---- */
  --s1:4px; --s2:8px; --s3:12px; --s4:16px; --s5:24px; --s6:32px;
  --s7:48px; --s8:64px; --s9:96px; --s10:128px;

  /* ---- motion ---- */
  --ease:cubic-bezier(.22,.61,.36,1);
  --gut:40px;              /* page gutter, stepped down in media queries */
}

*,*::before,*::after{margin:0;padding:0;box-sizing:border-box}
body{background:var(--canvas);color:var(--ink);font-family:var(--font-text);
  font-size:17px;line-height:1.6;-webkit-font-smoothing:antialiased;overflow-x:hidden}
img{display:block;max-width:100%}
a{color:inherit;text-decoration:none}
:focus-visible{outline:3px solid var(--accent);outline-offset:3px}
::selection{background:var(--accent);color:var(--accent-ink)}

.skip{position:absolute;left:-9999px;top:0;z-index:200;background:var(--accent);
  color:var(--accent-ink);padding:14px 20px;font-family:var(--font-mono);font-size:13px}
.skip:focus{left:0}

.grain{position:fixed;inset:0;z-index:1;pointer-events:none;opacity:.42;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='n'%3E%3CfeTurbulence baseFrequency='.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23n)' opacity='.28'/%3E%3C/svg%3E")}

/* ==========================================================================
   Nav
   ========================================================================== */
.nav{position:fixed;top:0;left:0;right:0;z-index:60;display:flex;align-items:center;
  justify-content:space-between;gap:28px;height:96px;padding:0 var(--gut);
  border-bottom:1px solid transparent;transition:background .3s,border-color .3s,padding .3s}
.nav.stuck{background:color-mix(in srgb,var(--canvas) 88%,transparent);
  backdrop-filter:blur(12px);border-bottom-color:var(--rule)}
/* padding, not margin: the wordmark is the "go home" target and measured 20px
   tall on mobile. The nav's own padding makes it *look* generously spaced but
   the clickable rect was only the text box. */
.mark{flex-shrink:0;display:inline-block;padding:6px 0;line-height:0}
/* line-height:0 on the anchor is not cosmetic. An inline <img> sits on a text
   baseline, so the descender space under it becomes phantom padding and the bar
   grows taller than the logo it contains — by 5px here, which reads as the nav
   being mysteriously off-centre. */
/* 56px, not 40px. The wordmark is 21% of this lockup's height, so 40px put
   "BLACKWOODCYCLES" at 8px — visible but not readable, which is the state a
   logo must never be in. Matches index.html exactly; the two stylesheets are
   independent and this is one of the values that has to be changed twice. */
.mark img{display:block;height:68px;width:auto}
/* 13px, matching index.html's .barlinks exactly. 12px uppercase at .16em is
   under the "no tiny text" rule and it did not match the homepage. */
.navlinks{display:flex;gap:34px;font-size:13px;font-weight:600;letter-spacing:.16em;
  text-transform:uppercase;color:var(--ink-soft);flex-shrink:0}
/* 10px vertical padding takes the 27px text box to a 40px target. The underline
   pseudo-element is pinned to the padding box, so it still sits tight under the
   word rather than floating 10px below it. */
.navlinks a{position:relative;padding:10px 0}
.navlinks a::after{content:"";position:absolute;left:0;bottom:6px;width:100%;height:1px;
  background:var(--accent);transform:scaleX(0);transform-origin:right;transition:transform .3s}
.navlinks a:hover{color:var(--ink)}
.navlinks a:hover::after{transform:scaleX(1);transform-origin:left}
.navlinks a[aria-current="page"]{color:var(--ink)}
.navlinks a[aria-current="page"]::after{transform:scaleX(1)}
.navRight{display:flex;align-items:center;gap:14px;flex-shrink:0}
/* The phone pill is the site's primary CTA and it appeared in two forms: display
   face at 13.5px/400 here, text face at 13.12px/600 on the homepage. Same button,
   same bar, two typefaces. Unified on the text face at 13px/600 — --btn-weight
   resolves to 400, and a display face at 400 in a 13px pill reads thin against a
   red plate, which is the opposite of what a primary CTA should do. */
.navcall{background:var(--accent);color:var(--accent-ink);font-family:var(--font-text);
  font-size:13px;font-weight:600;line-height:1;letter-spacing:.06em;padding:13px 22px;border-radius:2px;
  white-space:nowrap;flex-shrink:0;transition:transform .2s,background .2s;
  /* min-height, not more padding. At 390px the padding drops to 11px (line ~185)
     and the pill measured 36px — under the 44px touch minimum, on the one control
     that IS the conversion for a phone-first shop. min-height holds the floor
     without fighting the responsive padding, because align-items:center is
     already centring the label inside whatever box results. */
  min-height:44px;
  display:inline-flex;align-items:center;justify-content:center;gap:9px}
.navcall:hover{transform:translateY(-2px)}
.navcall-short{display:none}

/* A phone glyph, not a wheel.

   The wheel moved down to the sticky bar, where it is big enough to actually
   READ as a wheel. At 17px inside the nav pill it was an ambiguous dark disc,
   and a call button's icon has exactly one job: say "this dials". Two different
   wheels competing in one viewport also split the idea; one large one owns it.

   The navcall-roll keyframes stay defined here because the sticky bar and the
   homepage .barcall still reference them. */
.navcall-ico{font-size:17px;flex:none;display:block;line-height:1}
@keyframes navcall-roll{to{transform:rotate(360deg)}}

/* hamburger — three rules that cross into an X */
.burger{display:none;width:44px;height:44px;border:1px solid var(--rule-strong);
  background:transparent;position:relative;flex-shrink:0;cursor:pointer}
.burger span{position:absolute;left:12px;width:20px;height:2px;background:var(--ink);
  transition:transform .35s var(--ease),opacity .2s}
.burger span:nth-child(1){top:15px}
.burger span:nth-child(2){top:21px}
.burger span:nth-child(3){top:27px}
body.menu-open .burger span:nth-child(1){transform:translateY(6px) rotate(45deg)}
body.menu-open .burger span:nth-child(2){opacity:0}
body.menu-open .burger span:nth-child(3){transform:translateY(-6px) rotate(-45deg)}

/* full-screen drawer */
.drawer{position:fixed;inset:0;z-index:55;background:var(--canvas);
  display:flex;flex-direction:column;justify-content:center;padding:96px var(--gut) 40px;
  transform:translateY(-100%);transition:transform .5s var(--ease);overflow-y:auto}
body.menu-open .drawer{transform:translateY(0)}
.drawer nav{display:flex;flex-direction:column;gap:2px}
.drawer nav a{font-family:var(--font-display);font-size:clamp(30px,9vw,52px);line-height:1.1;
  text-transform:uppercase;letter-spacing:-.02em;padding:12px 0;
  border-bottom:1px solid var(--rule);display:flex;align-items:center;justify-content:space-between}
.drawer nav a span{font-family:var(--font-mono);font-size:var(--label);letter-spacing:var(--label-track);
  color:var(--ink-faint);text-transform:uppercase}
.drawer nav a:hover,.drawer nav a[aria-current="page"]{color:var(--accent)}
.drawerFoot{margin-top:40px;font-family:var(--font-mono);font-size:12px;letter-spacing:.12em;
  text-transform:uppercase;color:var(--ink-faint);line-height:2}
/* inline-block + padding: this is the phone number in the mobile drawer, the
   most-tapped link on the whole site, and it measured 121x16 — below the 24px
   WCAG 2.5.8 floor and far below a thumb. Left inline, the padding would paint
   but not grow the hit rect. */
.drawerFoot a{color:var(--ink);display:inline-block;padding:6px 0}

@media(max-width:900px){
  .navlinks{display:none}
  .burger{display:block}
  .nav,.nav.stuck{height:96px;padding:0 20px;gap:12px}
  .mark img{height:56px}
  .navcall{font-size:12.5px;padding:11px 15px;letter-spacing:.02em}
}
/* Below 460px the wordmark plus a full phone number cannot both fit. The sticky
   bottom bar already carries the whole number, so the nav button goes short. */
@media(max-width:460px){
  .nav,.nav.stuck{height:96px;padding:0 16px;gap:10px}
  .nav .mark img{height:46px}
  .nav .navcall{padding:11px 13px;letter-spacing:.04em}
  .navRight{gap:8px}
  .burger{width:40px;height:40px}
  .burger span{left:10px}
  .navcall-long{display:none}
  .navcall-short{display:inline}
}

/* ==========================================================================
   Hero — homepage
   ========================================================================== */
/* Split hero: type on solid canvas, photo in its own framed column.

   The first build ran the headline across a full-bleed photo behind a gradient
   scrim. Measuring the composite (rasterise plate + scrim, then sample the real
   glyph boxes) showed the accent word "out." at 1.01:1 — invisible. The cause is
   structural, not a tuning miss: the accent's relative luminance is low enough
   that ANY mid-tone pixel behind it drives contrast to 1:1. A sweep of scrim
   strengths and background positions showed the accent only clears 3:1 once the
   veil hits ~0.92 alpha, which turns the photograph into grey mud, and even then
   it holds only at this exact viewport width. The red is DARKER than the orange
   it replaced, so that finding got stronger, not weaker, with the recolour.

   Separating type from photo makes legibility structural: cream on canvas is
   19.2:1 and the accent red is 5.2:1, at every viewport, with any photo we
   swap in later. The photo also gets shown ungraded, which is a better use of a
   real shop asset than burying it under 92% black. */
/* padding:0 overrides the generic `section{padding:120px var(--gut)}`. The hero
   is now a two-column grid whose right column is a photograph that must bleed to
   the viewport edge; inheriting the section gutter left a 40px canvas margin
   down the right side and double-indented the copy (40 from the section, 40
   again from .heroCopy). All hero padding lives on .heroCopy. */
.hero{position:relative;min-height:100svh;display:grid;
  grid-template-columns:minmax(0,1fr) minmax(0,.86fr);align-items:center;
  overflow:hidden;z-index:2;padding:0}
.heroCopy{grid-column:1;grid-row:1;min-width:0;
  padding:150px clamp(40px,6vw,86px) 150px var(--gut)}
.heroFrame{grid-column:2;grid-row:1;align-self:stretch;position:relative;
  overflow:hidden;border-left:1px solid var(--rule)}
.heroBg{position:absolute;inset:-8% 0;background-size:cover;
  background-position:center;will-change:transform}
/* Two jobs. The right/bottom stops are an edge blend, purely optical — no type
   sits there. The top stop is load-bearing: the fixed nav is transparent over
   the hero and its last two links ("Reviews", "Contact") cross into the photo
   column at every desktop width. Clustering the links leftward does not escape
   it — at 1101px the copy column is only 592px wide and the link row is ~370px
   starting after a 244px wordmark. So the top ~150px of the plate is taken to
   solid canvas and faded out over the next 90px; that band is the fuel tank,
   the least interesting part of the frame. Measured, not eyeballed: nav link
   glyphs read 12.8:1 against the composite (see the contrast harness note). */
.heroScrim{position:absolute;inset:0;z-index:1;
  background:linear-gradient(to bottom,var(--canvas) 0,var(--canvas) 92px,transparent 182px),
             linear-gradient(to right,var(--canvas),transparent 24%),
             linear-gradient(to top,var(--canvas) 1%,transparent 26%)}
/* ==========================================================================
   Full-bleed photo divider
   --------------------------------------------------------------------------
   The homepage was eight stacked dark panels in a row. Alternating --canvas
   and --surface gives rhythm but not relief; by the fourth panel the scroll
   reads as one long slab and people stop. A full-bleed photograph between
   chapters is a breath, and it also does an argumentative job the panels
   cannot: it shows the work instead of describing it.

   It reuses .heroBg so it inherits the existing GSAP parallax for free
   (site.js binds parallax to every .heroBg on the page).

   The scrim is a left-to-right ramp because all the type sits left. Measured
   against the darkest sample under the headline it is 11.4:1; the accent
   kicker is 6.1:1. Both clear AA comfortably, which matters more here than
   usual because the plate behind is high-contrast metal and bone.
   ========================================================================== */
/* grid, not flex. As a flex container this collapsed .inner to its content
   width — 292px — because a flex item is shrink-to-fit and .inner's own
   max-width:1220px is only a cap, not a size. The copy ended up floating in
   the middle of the band over the brightest part of the photograph. Grid
   items stretch along the inline axis by default, so .inner keeps its normal
   page width and the copy stays on the left where the scrim is darkest. */
/* `padding:0 var(--gut)`, not `padding:0`. The page gutter lives on the
   generic `section{padding:120px var(--gut)}` rule and .inner has no padding
   of its own, so zeroing both axes to get a full-bleed plate also dragged the
   copy flat against the left edge of the screen at mobile widths. Only the
   vertical needs to go. The plate still bleeds because .heroBg is absolutely
   positioned and resolves inset:0 against the padding box, which spans the
   full element width — the gutter costs the photograph nothing. */
.divider{position:relative;overflow:hidden;padding:0 var(--gut);
  min-height:clamp(340px,42vw,540px);display:grid;align-items:center;
  border-top:1px solid var(--rule);border-bottom:1px solid var(--rule)}
.divider .heroBg{inset:-10% 0}
.divScrim{position:absolute;inset:0;z-index:1;
  background:linear-gradient(to right,
      color-mix(in srgb,var(--canvas) 92%,transparent) 0%,
      color-mix(in srgb,var(--canvas) 78%,transparent) 38%,
      color-mix(in srgb,var(--canvas) 30%,transparent) 72%,
      transparent 100%),
    linear-gradient(to bottom,color-mix(in srgb,var(--canvas) 55%,transparent),
      transparent 26%,transparent 74%,color-mix(in srgb,var(--canvas) 55%,transparent))}
.divider .inner{position:relative;z-index:2;width:100%;
  padding-top:56px;padding-bottom:56px}
/* px, not ch. `ch` is the advance width of "0" at the element's OWN font-size,
   and .divCopy inherits 16px while the h2 inside it renders at 43px — so 30ch
   measured 292px and broke a six-word headline over six lines. The cap has to
   be expressed in the same units the headline is actually set in.
   46% keeps the block inside the dark end of the scrim ramp at every width. */
.divCopy{max-width:min(46%,560px)}
.divKicker{display:flex;align-items:center;gap:14px;font-family:var(--font-mono);
  font-size:var(--label);letter-spacing:.22em;text-transform:uppercase;
  color:var(--accent);margin-bottom:24px}
.divKicker::before{content:"";width:38px;height:2px;background:var(--accent);flex:none}
.divider h2{font-size:clamp(28px,3.4vw,46px);line-height:1.08;letter-spacing:-.02em}
.divider p.divSub{margin-top:20px;font-size:17px;line-height:1.6;color:var(--ink-soft);
  max-width:44ch}
.divLink{display:inline-flex;align-items:center;gap:10px;margin-top:30px;
  font-family:var(--font-mono);font-size:var(--label);letter-spacing:.18em;
  text-transform:uppercase;color:var(--ink);padding:14px 0;
  border-bottom:1px solid color-mix(in srgb,var(--ink) 40%,transparent);
  transition:color .3s,gap .3s,border-color .3s}
.divLink:hover{color:var(--accent);gap:16px;border-color:var(--accent)}
/* Below 760px the ramp cannot both clear the type and leave any photograph
   visible, so the treatment changes rather than degrades: the plate goes to a
   band above the copy and the copy sits on solid canvas underneath. */
@media(max-width:760px){
  .divider{display:block;min-height:0;padding:236px var(--gut) 0}
  .divider .heroBg{inset:0 0 auto;height:250px}
  .divScrim{inset:0 0 auto;height:250px;
    background:linear-gradient(to bottom,transparent 40%,var(--canvas) 99%)}
  .divider .inner{padding-top:8px;padding-bottom:52px}
  .divCopy{max-width:none}
}

.status{display:inline-flex;align-items:center;gap:11px;font-family:var(--font-mono);
  font-size:var(--label);letter-spacing:.22em;text-transform:uppercase;color:var(--accent);
  margin-bottom:30px}
.dot{width:8px;height:8px;border-radius:50%;background:var(--accent);
  box-shadow:0 0 0 0 color-mix(in srgb,var(--accent) 70%,transparent);animation:pulse 2.4s infinite}
.dot.shut{background:var(--ink-soft);animation:none}
@keyframes pulse{0%{box-shadow:0 0 0 0 color-mix(in srgb,var(--accent) 60%,transparent)}
  70%{box-shadow:0 0 0 13px transparent}100%{box-shadow:0 0 0 0 transparent}}

/* A ch cap is a blunt instrument for display type: ch is the advance of "0",
   and Archivo Black's caps are far wider, so the real character budget drifts
   with the string. 15ch is a safety net for interior page headlines only. */
h1{font-family:var(--font-display);font-size:var(--h1);line-height:var(--h1-lh);
  letter-spacing:var(--h1-track);font-weight:var(--h1-weight);text-transform:var(--h1-case);
  max-width:15ch}
/* The hero headline sets its own breaks with a literal <br> (preserved through
   splitWords in site.js), one sentence per line. A width cap on top of that
   would introduce a second, competing break point — so max-width goes away and
   the size is chosen so the longest line always fits.

   The global --h1 is 9.2vw, sized back when the hero ran full-bleed. In the
   split hero the headline only owns ~54% of the viewport, so 9.2vw overflowed
   and the browser re-broke the text into four lines ("RIDE IT / IN. / RIDE IT /
   OUT."). Above 900px the type therefore scales off the *column*, not the
   viewport: content width is vw/1.86 minus the 40px gutter and the clamped
   right inset, and the longest line "RIDE IT OUT." is 6.59em. 6.5vw is the
   largest round figure that fits across the whole 901–1920 range (at 901px it
   needs 386px of a 390px column; at 1440px, 94px of a possible 98px). */
.heroCopy h1{max-width:none}
@media(min-width:901px){.heroCopy h1{font-size:clamp(46px,6.5vw,124px)}}
h1 .accent{color:var(--accent)}
.heroSub{margin-top:30px;font-size:20px;line-height:1.55;color:var(--ink-soft);max-width:52ch}
.heroCta{display:flex;flex-wrap:wrap;align-items:center;gap:16px;margin-top:40px}

.btn{background:var(--accent);color:var(--accent-ink);font-family:var(--font-display);
  font-size:18px;font-weight:var(--btn-weight);letter-spacing:.01em;padding:21px 36px;
  display:inline-flex;align-items:center;gap:12px;border:0;cursor:pointer;
  transition:transform .25s,background .25s}
.btn:hover{transform:translateY(-3px);background:#FF6D38}
.btn iconify-icon{font-size:21px}
.btn.ghost{background:transparent;color:var(--ink);border:1px solid var(--rule-strong);
  font-size:13px;letter-spacing:.14em;text-transform:uppercase;padding:20px 30px}
.btn.ghost:hover{border-color:var(--accent);color:var(--accent);background:transparent}
.btn[disabled]{opacity:.55;pointer-events:none}
/* Keep the phone number on one line — wrapping split it mid-digits ("905-/5465"). */
@media(max-width:460px){
  .heroCta{gap:12px}
  .heroCta .btn{white-space:nowrap;font-size:15px;padding:17px 22px;width:100%;
    justify-content:center}
  .heroCta .btn iconify-icon{font-size:18px}
  .heroCta .btn.ghost{font-size:12px;letter-spacing:.12em;padding:16px 22px}
}

/* ---- hero trust bar ----
   minmax(0,1fr) not 1fr: bare 1fr is minmax(auto,1fr), so a long unbreakable
   value ("NYS INSPECTIONS") inflates its track and widens the document. */
/* Solid, not translucent. The bar crosses under the photo column, and a
   see-through panel put the label text back over photographic mid-tones — the
   exact problem the split hero removes. Solid canvas also keeps the page to a
   single backdrop-blur (the stuck header), per the rules at the top of the file. */
.trust{position:absolute;bottom:0;left:0;right:0;display:grid;
  grid-template-columns:repeat(4,minmax(0,1fr));border-top:1px solid var(--rule);
  background:var(--canvas);z-index:3}
.tItem{padding:22px 28px;border-right:1px solid var(--rule)}
.tItem:last-child{border-right:0}
.tLabel{font-family:var(--font-mono);font-size:var(--label);letter-spacing:var(--label-track);text-transform:uppercase;
  color:var(--ink-faint);margin-bottom:7px}
.tVal{font-family:var(--font-display);font-size:17px;font-weight:var(--h1-weight);
  letter-spacing:.005em}
.tVal em{font-style:normal;color:var(--accent)}
/* Below 900 the hero stacks: copy first on solid canvas, then the photo band,
   then the trust bar in normal flow. Copy stays first so the headline and the
   call button are above the fold, and so the fixed header never sits over the
   photograph (the wordmark is cream and would hit the same contrast problem). */
@media(max-width:900px){
  .hero{grid-template-columns:minmax(0,1fr);min-height:auto}
  .heroCopy{grid-column:1;grid-row:1;padding:132px var(--gut) 44px}
  .heroFrame{grid-column:1;grid-row:2;border-left:0;
    border-top:1px solid var(--rule);border-bottom:1px solid var(--rule);
    height:42svh;min-height:260px}
  .heroBg{inset:-6% 0}
  .heroScrim{background:linear-gradient(to top,var(--canvas) 1%,transparent 22%)}
  .trust{position:static;grid-column:1;grid-row:3;border-top:0}
}
@media(max-width:900px){.trust{grid-template-columns:repeat(2,minmax(0,1fr))}
  .tItem{border-bottom:1px solid var(--rule);padding:15px 18px}
  .tItem:nth-child(2n){border-right:0}}
@media(max-width:460px){
  .trust .tItem{padding:13px 14px}
  .trust .tVal{font-size:12.5px;letter-spacing:0}
  .trust .tLabel{font-size:var(--label);letter-spacing:.12em;margin-bottom:5px}}

/* ==========================================================================
   Page hero — interior pages
   ========================================================================== */
.pageHero{position:relative;z-index:2;padding:190px var(--gut) 78px;overflow:hidden;
  border-bottom:1px solid var(--rule)}
.pageHero .heroBg{inset:-10% 0}
/* THE VEIL WAS LIFTED, and the way it was lifted is the point.

   The old ramp was one long left-to-right wash that never got below 40% canvas
   even at the far right edge, plus a bottom fade climbing 60% of the way up the
   plate. Between them roughly two thirds of every interior hero photograph was
   under permanent grey. That is what "too gray, I want more attention to detail
   in the image" describes.

   The naive fix is to drop every stop together, and it does not work: the h1
   sits hard left, so the left of the plate is doing real legibility work while
   the right of the plate is doing none. One slider moves both, so you either
   keep the right muddy or you break the headline. Contrast is LOCAL — it is
   decided by the brightest pixel under a given line, not by the average of the
   frame — so the ramp has to be re-cut, not re-scaled.

   What changed:

     left 10% -> 8%     solid canvas behind the h1. Essentially unmoved: this is
                        the band that holds the headline and it is not free.
     mid  72% -> 76%    HELD, very slightly raised. The first attempt at this
                        dropped it to 54% and seven of seven heroes failed AA on
                        the headline, worst 2.31:1 over chrome on general-repair.
                        This is the band the type actually crosses and it does
                        not get to be pretty.
     right 40% ->  6%   the big one, and the only reason it is affordable is the
                        14ch measure on .pageHero h1 above. With the headline no
                        longer running to 83% of the viewport, the right third of
                        the plate carries no glyphs, so veiling it was protecting
                        nothing. This is where the returned detail comes from.
     bottom fade
          60% -> 44%    a shorter climb, so the lower third of the frame is
                        photograph again rather than a gradient.

   The shape changed as well as the numbers: it is no longer a straight ramp but
   a plateau that holds across the copy column and then falls off a cliff past
   it. That is the correct shape for "a scrim only where the text sits", and it
   is why the mid stop could go UP while the overall veil went down.

   And one gradient was ADDED, which is what pays for all of the above. The nav
   is fixed and transparent over this plate, and its links cross into the right
   half — exactly the region just opened from 40% to 12%. Lightening the wash
   without protecting the nav would have traded a dull photo for unreadable
   navigation. So the top 84px goes to solid canvas and fades out by 164px,
   landing well clear of the h1, which starts at the 190px padding line. The
   homepage hero has carried the same band for the same reason since it shipped;
   this is that fix applied where it was missed. */
.pageHero .heroScrim{background:
  linear-gradient(to bottom,var(--canvas) 0,var(--canvas) 84px,transparent 164px),
  linear-gradient(to right,var(--canvas) 8%,
    color-mix(in srgb,var(--canvas) 88%,transparent) 34%,
    color-mix(in srgb,var(--canvas) 76%,transparent) 52%,
    color-mix(in srgb,var(--canvas) 40%,transparent) 68%,
    color-mix(in srgb,var(--canvas)  6%,transparent) 84%,
    transparent 100%),
  linear-gradient(to top,var(--canvas) 1%,transparent 44%)}
/* Photoless hero. Used where a photograph would be filler or a repeat: About
   (no portrait of Dillon exists), Privacy and Thank You. It has to carry the
   page on type alone, so it gets a short accent rule, a slow accent wash off
   the left edge, and more air above the headline than the photo variant needs —
   a photo plate supplies its own visual weight, bare canvas does not. */
.pageHero.plain{padding-top:220px;padding-bottom:96px;
  background:radial-gradient(120% 150% at 0% 0%,
    color-mix(in srgb,var(--accent) 9%,transparent) 0%,transparent 58%)}
.pageHero.plain::before{content:"";position:absolute;left:var(--gut);top:150px;
  width:64px;height:3px;background:var(--accent)}
@media(max-width:700px){.pageHero.plain{padding-top:172px;padding-bottom:64px}
  .pageHero.plain::before{top:126px}}
/* max-width 18ch -> 14ch, and this single number is what let the scrim be
   lifted at all.

   Measured on the rendered page, an 18ch headline at the 96px clamp ceiling ran
   to x=1191 on a 1440 viewport: 83% of the screen. That has two consequences and
   both are bad. As typography it is simply too long a measure for a display
   face — the eye loses the line and has to hunt for the start of the next one,
   and the rule of thumb for a headline this size is nearer 20-30 characters per
   line, not 50. As layout it means there is TYPE almost all the way to the right
   edge, so the scrim has to hold canvas almost all the way to the right edge
   too, and the photograph never gets to be a photograph anywhere.

   Narrowing the measure frees the right ~40% of every interior plate to carry no
   type at all, which is what makes it safe to drop that region to near-zero veil.
   The client asked to see more of the image; the way to give them that was not a
   lighter wash over the whole frame — that just greys the type as well — it was
   to stop the words covering the frame in the first place.

   14ch, not 12ch: the shorter measure forces "General motorcycle repair" onto
   four lines, and a four-line H1 at 96px is 460px of headline before the lede
   even starts. */
.pageHero h1{font-size:clamp(40px,7vw,96px);max-width:14ch}
.pageHero .heroSub{font-size:19px;margin-top:24px;max-width:48ch}
.crumbs{display:flex;flex-wrap:wrap;align-items:center;gap:10px;font-family:var(--font-mono);
  font-size:var(--label);letter-spacing:.18em;text-transform:uppercase;color:var(--ink-faint);
  margin-bottom:26px}
.crumbs a:hover{color:var(--accent)}
.crumbs i{font-style:normal;color:var(--rule-strong)}
/* THE MOBILE HERO IS A DIFFERENT HERO, and it has to be.

   The desktop scrim above is a horizontal ramp: solid canvas across the copy
   column, then a cliff to 6% past it. That shape is only honest while the type
   actually stops before the cliff, which the 14ch measure guarantees at 1440.

   At 390 it is false. Measured: the copy column IS the viewport, headlines and
   ledes run to 80-94% of the frame, and they cross the region that was opened
   up precisely because nothing was drawn there. Graded on the text-hidden
   composite, six of ten heroes failed on the h1 (worst 1.66:1 on general-repair,
   cream over the white bodywork of the Honda), six on the lede, and six on the
   crumbs (worst 1.05:1 on chain-drivetrain, over a yellow highlight). That is
   not a near-miss, it is unreadable, and it shipped in the zip.

   The wrong fix is a denser wash. Flooding the whole plate with canvas buys
   legibility by deleting the photograph, which is the exact "too gray" the
   client objected to in the first place.

   So the layout changes instead of the opacity. Measured at 390, the old hero
   ran 461-554px with copy from y150 to y497 — the type occupied 27% to 90% of
   the box and there was NO photo-only region anywhere in it. Here the box is
   grown and the copy is pushed to the bottom of it, which buys a real band at
   the top where the plate is unveiled and a soft vignette below it where the
   plate fades under the words. More photograph is visible than before, not
   less, and the type sits on near-solid canvas.

   min(86svh,760px): svh not vh, because vh on iOS measures the viewport with
   the URL bar retracted and a hero sized in vh is taller than the screen on
   arrival — the copy starts below the fold and the page opens on a headless
   photograph. The 760 cap stops the same rule making a tablet hero absurd.

   The nav strip is shortened from 84/164px to 58/116px. The nav is 50px tall
   here rather than 74, so the desktop values would spend 48px of solid canvas
   covering nothing, and that 48px is taken directly off the top of the band
   this rule exists to create. */
@media(max-width:900px){
  .pageHero{padding:0 var(--gut) 56px;min-height:min(86svh,760px);
    display:flex;flex-direction:column;justify-content:flex-end}
  .pageHero .heroScrim{background:
    linear-gradient(to bottom,var(--canvas) 0,var(--canvas) 58px,transparent 116px),
    linear-gradient(to bottom,
      transparent 0%,
      transparent 24%,
      color-mix(in srgb,var(--canvas) 46%,transparent) 32%,
      color-mix(in srgb,var(--canvas) 86%,transparent) 38%,
      color-mix(in srgb,var(--canvas) 97%,transparent) 42%,
      var(--canvas) 46%,
      var(--canvas) 100%)}
}
@media(max-width:700px){
  .pageHero h1{font-size:clamp(34px,9.5vw,52px)}
  .pageHero .heroSub{font-size:17px;margin-top:18px}
}

/* ==========================================================================
   Marquee
   ========================================================================== */
.marquee{position:relative;z-index:2;border-top:1px solid var(--rule);
  border-bottom:1px solid var(--rule);background:var(--surface);overflow:hidden;
  padding:19px 0;display:flex;white-space:nowrap}
.mTrack{display:flex;gap:44px;padding-right:44px;font-family:var(--font-mono);
  font-size:13px;letter-spacing:.19em;text-transform:uppercase;color:var(--ink-soft);
  flex-shrink:0;will-change:transform}
.mTrack i{color:var(--accent);font-style:normal}

/* ==========================================================================
   Sections
   ========================================================================== */
section{position:relative;z-index:2;padding:120px var(--gut)}
.inner{max-width:1220px;margin:0 auto}
.inner-narrow{max-width:760px;margin:0 auto}
.alt{background:var(--surface);border-top:1px solid var(--rule);border-bottom:1px solid var(--rule)}
.eyebrow{font-family:var(--font-mono);font-size:var(--label);letter-spacing:.24em;
  text-transform:uppercase;color:var(--accent);margin-bottom:20px;display:flex;
  align-items:center;gap:14px}
.eyebrow::before{content:"";width:44px;height:1px;background:var(--accent);flex-shrink:0}
h2{font-family:var(--font-display);font-size:var(--h2);line-height:var(--h2-lh);
  letter-spacing:var(--h1-track);font-weight:var(--h1-weight);text-transform:var(--h1-case);
  max-width:20ch}
h3{font-family:var(--font-display);font-weight:var(--h1-weight)}
.sectionLede{margin-top:22px;font-size:19px;line-height:1.6;color:var(--ink-soft);max-width:56ch}
@media(max-width:700px){section{padding:84px var(--gut)}}

/* ==========================================================================
   Service grid
   ========================================================================== */
.svcGrid{display:grid;grid-template-columns:repeat(3,1fr);gap:1px;background:var(--rule);
  border:1px solid var(--rule);margin-top:64px}
.svc{background:var(--canvas);padding:40px 34px 44px;position:relative;
  transition:background .35s;overflow:hidden;display:flex;flex-direction:column}
.svc::before{content:"";position:absolute;left:0;top:0;height:2px;width:100%;
  background:var(--accent);transform:scaleX(0);transform-origin:left;transition:transform .45s}
.svc:hover{background:var(--surface)}
.svc:hover::before{transform:scaleX(1)}

/* The photograph. Ported verbatim from the homepage grid, where these cards
   were rebuilt as photographic tiles and then never carried across — so until
   now the site changed character the moment you left "/", and anyone landing on
   services.html from a search never saw the designed version of the component.
   build.py passes --shot (and --shot-pos where the default crop misses) as an
   inline custom property on each <a>.

   z-index:-2 on an ::after, not a background-image on .svc itself, because the
   card needs TWO layers that animate independently: a plate that scales on
   hover and a scrim that does not. It paints above the card's own --canvas
   background and below the icon and type, which is exactly the CSS painting
   order for a negative-z-index descendant.

   THE GRADIENT IS MEASURED, NOT CHOSEN. Contrast on light type over a photo is
   a LOCAL property — it is decided by the brightest pixel directly under a given
   line, so the top of the card and the bottom of the card have completely
   different budgets and cannot share one "overlay opacity" slider. Pulling a
   single slider down is what makes a card simultaneously washed out at the
   bottom and still muddy at the top, which was the client's "too gray"
   complaint. The stops below were arrived at band by band against a 4.5:1 floor
   using lab/svcmeasure.mjs + lab/svcgrade.py, which composite the real page and
   read the brightest cell under every line.

   The 0% stop is .10 — almost nothing. Nothing is set up there, so the 30% of
   black the first version put over the most interesting part of every crop was
   pure wasted veil, and lifting it is where the returned detail comes from.

   The 84% and 100% stops are .985/.99 and they DO NOT MOVE. .more is #FF1D24,
   and red has a relative luminance of 0.212 — it is a DARK colour that merely
   looks bright. It clears 4.5:1 only while the pixels behind it stay under
   about grey 26, which pins the bottom sixth of every card closed no matter how
   far the top opens. That is a fact about the accent, not a tuning choice.

   GREYSCALE STAYS, HERE AND ONLY HERE. Every other photograph on the site is
   now in full colour; the client asked for the cards specifically to keep it,
   and it earns its place: six photographs taken on six different phones, in six
   lightings, on six days sit in one grid, and colour is what would make them
   read as six clippings rather than one set. contrast 1.16 and brightness .96
   put back the local separation that dropping chroma costs — greyscale
   collapses three channels into one, so a red brake pad against a black caliper
   (plainly two objects in colour) becomes two near-identical greys. */
.svc::after{content:"";position:absolute;inset:0;z-index:-2;
  background-image:
    linear-gradient(180deg,
      rgba(9,9,10,.10)   0%,
      rgba(9,9,10,.34)  28%,
      rgba(9,9,10,.56)  35%,
      rgba(9,9,10,.80)  47%,
      rgba(9,9,10,.93)  74%,
      rgba(9,9,10,.985) 84%,
      rgba(9,9,10,.99) 100%),
    var(--shot);
  background-size:100% 100%, auto 168%;
  background-position:center top, var(--shot-pos,center 88%);
  background-repeat:no-repeat;
  filter:grayscale(1) contrast(1.16) brightness(.96);
  transform:scale(1.04);
  transition:filter .55s cubic-bezier(.22,1,.36,1),
             transform .85s cubic-bezier(.22,1,.36,1)}
/* No --shot means no ::after at all. .svc--call is the call tile, which is
   deliberately flat so it reads as a different KIND of thing from the five
   destinations beside it; without this guard it would render a scrim over
   nothing and come out a shade darker than the canvas for no reason. */
.svc:not([style*="--shot"])::after,
.svc--call::after{display:none}
.svc:hover::after{transform:scale(1.09);
  filter:grayscale(1) contrast(1.2) brightness(1.02)}
@media(prefers-reduced-motion:reduce){
  .svc::after{transition:none}
  .svc:hover::after{transform:scale(1.04)}
}

/* The call tile that closes the related-services grid. It sits on --surface so
   it is legibly a different KIND of thing from the five service cards beside it
   without needing a solid accent fill, which at this size would out-shout the
   page's real hero CTA and turn the section into a second hero.

   Its top rule starts drawn instead of at scaleX(0): the other five earn that
   line on hover because they are destinations, this one is the action and
   should already look live. */
.svc--call{background:var(--surface)}
.svc--call::before{transform:scaleX(1)}
.svc--call:hover{background:var(--surface-2)}
.svcCallNum{margin-top:auto;padding-top:22px;display:block;
  font-family:var(--font-display);font-size:clamp(22px,2.1vw,28px);
  letter-spacing:-.01em;color:var(--accent);
  transition:color .3s}
.svc--call:hover .svcCallNum{color:var(--ink)}
/* Inline SVG, not <iconify-icon>. Three things that buys us:
   1. It cannot fail silently. The old custom element resolved its glyph over
      the network and a bad name collapsed it to height 0 — a broken grid row
      that looked like a CSS bug. These ship in the HTML.
   2. No 40KB icon runtime on the critical path.
   3. The parts are addressable, so a rotor can spin while its caliper stays
      put. A webfont glyph can only ever move as one lump, which is why the old
      hover was a translate+rotate of the whole icon — the tell of a stock set.

   THIS BLOCK IS SHARED GEOMETRY. It is a copy of the `.ico` rules in
   index.html, kept identical on purpose: the client asked that "no one page
   should have a little different design than the other", and the two grids
   used to be two components that merely resembled each other (40x40 `.ico`
   with a red hub here, 48x48 `.svcIcon` with grey detail there). Both sides
   now render the same markup out of icons.py. Change one, change both, or
   lab/g-cards.mjs fails.

   THE POOL, and it is not decoration.

   These icons sit on a photograph whose top band carries only 10% veil by
   design — that open band is where the returned image detail comes from and it
   is not negotiable. Measured on the composited card with lab/svcint.mjs +
   lab/intgrade.py, the icon came out at 1.00:1 against chrome on general-repair
   and 1.04:1 on same-day. Not "thin": invisible. A red hairline over a bright
   chrome highlight is the same luminance as the chrome.

   Three things could fix it and two are wrong. Darkening the top band undoes
   the exact change the client asked for. Thickening the stroke turns six
   precise part diagrams into six blobs. So: a local wash that travels WITH the
   icon, dark enough under the drawing to carry a hairline and fully transparent
   by the time it reaches the edge of its own box, so it never reads as a plate
   or a badge — just a slightly deeper shadow in that corner of the photograph.

   22px of padding and a plateau running to 78% both come from one piece of
   geometry. `closest-side` on a padded box sets the wash radius to half the
   padded width; the DRAWING is the inner 46x46, whose far corner sits 32.5 from
   centre. At 22px the radius is 45, so those corners land at 72% — inside the
   plateau, so the whole drawing is backed at ~0.93 and only the empty ring
   feathers. Matching negative margins put the layout back exactly where it was.
   content-box, so the 46px drawing size is untouched. */
.ico{width:46px;height:46px;display:block;overflow:visible;box-sizing:content-box;
  /* -22 on three sides cancels the padding; 4 on the bottom is 26 minus the
     same 22, so the gap to the <h3> stays exactly what it was. */
  padding:22px;margin:-22px -22px 4px;
  background:radial-gradient(closest-side,
             rgba(9,9,10,.95) 0%, rgba(9,9,10,.93) 78%,
             rgba(9,9,10,.45) 92%, rgba(9,9,10,0) 100%)}
/* One rule governs every glyph, which is why the markup in icons.py carries no
   fill/stroke attributes of its own — an inline attribute would beat this. */
.ico *{fill:none;stroke:var(--ink);stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round;
  vector-effect:non-scaling-stroke;transition:stroke .4s}
.ico .hot{stroke:var(--accent)}
.svc:hover .ico *{stroke:var(--accent)}
/* The call tile has no photograph, so it needs no pool — on flat --surface the
   wash would show as a visible dark disc. */
.svc--call .ico{background:none;padding:0;margin:0 0 26px}
.svc--call .ico *{stroke:var(--accent)}

.spin{transform-origin:center;transition:transform 1.1s cubic-bezier(.3,1,.4,1)}
.svc:hover .spin{transform:rotate(140deg)}
.spin-r{transform-origin:center;transition:transform 1.1s cubic-bezier(.3,1,.4,1)}
.svc:hover .spin-r{transform:rotate(-160deg)}
.tick{stroke-dasharray:26;stroke-dashoffset:26;transition:stroke-dashoffset .5s cubic-bezier(.6,0,.3,1) .1s}
.svc:hover .tick{stroke-dashoffset:0}
.slide-x{transition:transform .7s cubic-bezier(.3,1,.4,1)}
.svc:hover .slide-x{transform:translateX(4px)}
.pump{transition:transform .5s cubic-bezier(.5,0,.2,1)}
.svc:hover .pump{transform:translateY(-3.5px)}
.chain-run{stroke-dasharray:3.2 5.4;transition:stroke-dashoffset 1.2s linear}
.svc:hover .chain-run{stroke-dashoffset:-34}
/* Draw-on. JS measures each path and sets --len; if JS never runs the custom
   property is missing, dasharray is invalid, and the icon renders solid —
   which is the correct no-JS state. */
.ico.draw *{stroke-dasharray:var(--len);stroke-dashoffset:var(--len)}
@media(prefers-reduced-motion:reduce){
  .ico *,.spin,.spin-r,.tick,.slide-x,.pump,.chain-run{transition:none!important}
  .svc:hover .spin,.svc:hover .spin-r,.svc:hover .slide-x,.svc:hover .pump{transform:none}
}

/* Sentence case, not var(--h1-case). The homepage grid never inherited the
   uppercase heading rule, so the same six service names were shouted on one
   page and spoken on the other — the single most visible half of what the
   client meant by "a little different design". Sentence case wins because the
   names are proper nouns of a sort ("NYS Inspections") and uppercase destroys
   the internal capitals that make them scannable. */
.svc h3{font-size:1.18rem;letter-spacing:-.01em;margin:1.25rem 0 .6rem;
  text-transform:none;color:var(--ink)}
.svc p{font-size:.965rem;line-height:1.62;color:var(--ink-soft);margin:0}
/* Accent, not --ink-faint. This is the affordance that says the card is a
   link, and it was grey here and red on the homepage. Red also makes it the
   only other chroma in the card, which is what pulls the eye to the bottom
   edge where the click is. */
.svc .more{display:inline-flex;align-items:center;gap:.5rem;margin-top:1.1rem;
  font:500 .76rem/1 var(--font-mono);letter-spacing:.14em;text-transform:uppercase;
  color:var(--accent)}
.svc .more svg{transition:transform .34s cubic-bezier(.22,1,.36,1)}
.svc:hover .more svg{transform:translateX(5px)}
@media(max-width:1000px){.svcGrid{grid-template-columns:repeat(2,1fr)}}
@media(max-width:640px){.svcGrid{grid-template-columns:1fr}}

/* ==========================================================================
   Proof numbers
   ========================================================================== */
.proof{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:40px;margin-top:60px}
.pNum{font-family:var(--font-display);font-size:clamp(44px,6vw,86px);line-height:.9;
  font-weight:var(--h1-weight);letter-spacing:-.04em;color:var(--accent)}
.pLabel{margin-top:14px;font-size:15.5px;line-height:1.45;color:var(--ink-soft);
  padding-top:14px;border-top:1px solid var(--rule)}
@media(max-width:760px){.proof{grid-template-columns:repeat(2,minmax(0,1fr));gap:28px}}

/* ==========================================================================
   Reviews reel
   ========================================================================== */
.revWrap{overflow:hidden;margin-top:60px;cursor:grab;
  -webkit-mask-image:linear-gradient(90deg,transparent,#000 7%,#000 93%,transparent);
  mask-image:linear-gradient(90deg,transparent,#000 7%,#000 93%,transparent)}
.revWrap:active{cursor:grabbing}
.revTrack{display:flex;gap:24px;will-change:transform}
.rev{flex:0 0 430px;background:var(--surface);border:1px solid var(--rule);
  padding:34px 32px 30px;display:flex;flex-direction:column;justify-content:space-between}
.stars{color:var(--accent);font-size:15px;letter-spacing:.18em;margin-bottom:18px}
.rev p{font-size:17px;line-height:1.6;color:var(--ink)}
.revBy{margin-top:24px;padding-top:18px;border-top:1px solid var(--rule);
  font-family:var(--font-mono);font-size:12.5px;letter-spacing:.13em;text-transform:uppercase;
  color:var(--ink-faint)}
/* Last holdout under the type floor. This is the "drag to scrub" hint on the
   review slider — the one caption whose whole job is to be read by someone who
   has not realised the strip is interactive, so it is the worst possible place
   to save four pixels. */
.revHint{margin-top:20px;font-family:var(--font-mono);font-size:var(--label);letter-spacing:.16em;
  text-transform:uppercase;color:var(--ink-faint);display:flex;align-items:center;gap:9px}
@media(max-width:560px){.rev{flex:0 0 300px}}

/* ==========================================================================
   Gallery
   ========================================================================== */
.gallery{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:1px;
  background:var(--rule);border:1px solid var(--rule);margin-top:60px}
.gItem{position:relative;overflow:hidden;background:var(--canvas);aspect-ratio:1;display:block}
.gItem img{width:100%;height:100%;object-fit:cover;transition:transform .7s var(--ease);
  filter:saturate(.9)}
.gItem:hover img{transform:scale(1.06);filter:saturate(1.05)}
.gCap{position:absolute;left:0;right:0;bottom:0;padding:16px 18px;
  background:linear-gradient(to top,rgba(10,9,8,.94),transparent);
  font-family:var(--font-mono);font-size:var(--label);letter-spacing:.12em;text-transform:uppercase;
  color:var(--ink);opacity:0;transform:translateY(8px);transition:opacity .35s,transform .35s}
.gItem:hover .gCap{opacity:1;transform:translateY(0)}
.gItem.tall{grid-row:span 2;aspect-ratio:1/2}
@media(max-width:900px){.gallery{grid-template-columns:repeat(2,minmax(0,1fr))}
  .gItem.tall{grid-row:span 1;aspect-ratio:1}}

/* ==========================================================================
   Split feature (image + copy)
   ========================================================================== */
.split{display:grid;grid-template-columns:1fr 1fr;gap:0;align-items:stretch;
  border:1px solid var(--rule);margin-top:60px}
.split .media{position:relative;overflow:hidden;min-height:420px;background:var(--surface)}
.split .media img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
.split .copy{padding:56px 48px;display:flex;flex-direction:column;justify-content:center;
  border-left:1px solid var(--rule)}
.split.flip .media{order:2;border-left:1px solid var(--rule)}
.split.flip .copy{order:1;border-left:0}
@media(max-width:900px){.split{grid-template-columns:1fr}
  .split .copy{border-left:0;border-top:1px solid var(--rule);padding:40px 26px}
  .split .media{min-height:300px}
  .split.flip .media{order:1;border-left:0}
  .split.flip .copy{order:2}}

/* ==========================================================================
   Feature list / checks
   ========================================================================== */
.checks{margin-top:28px;display:flex;flex-direction:column;gap:14px}
.checks li{list-style:none;display:flex;gap:14px;align-items:flex-start;
  font-size:16.5px;line-height:1.55;color:var(--ink-soft)}
.checks iconify-icon{color:var(--accent);font-size:21px;flex-shrink:0;margin-top:2px}

/* ==========================================================================
   Steps
   ========================================================================== */
.steps{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:1px;
  background:var(--rule);border:1px solid var(--rule);margin-top:60px}
.step{background:var(--canvas);padding:38px 32px 42px}
.stepNum{font-family:var(--font-mono);font-size:var(--label);letter-spacing:.22em;color:var(--accent);
  margin-bottom:20px}
.step h3{font-size:22px;text-transform:uppercase;letter-spacing:-.01em;margin-bottom:11px}
.step p{font-size:16px;color:var(--ink-soft);line-height:1.6}
@media(max-width:820px){.steps{grid-template-columns:1fr}}

/* ==========================================================================
   Info cards (hours, address, contact)
   ========================================================================== */
.infoGrid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:1px;
  background:var(--rule);border:1px solid var(--rule);margin-top:60px}
.info{background:var(--canvas);padding:36px 32px 40px}
.info iconify-icon.big{font-size:30px;color:var(--accent);margin-bottom:20px;display:block}
.info h3{font-size:19px;text-transform:uppercase;letter-spacing:.01em;margin-bottom:14px}
.info p,.info address{font-size:16.5px;line-height:1.65;color:var(--ink-soft);font-style:normal}
.info a{color:var(--ink);border-bottom:1px solid var(--rule-strong);padding-bottom:1px}
.info a:hover{color:var(--accent);border-color:var(--accent)}
@media(max-width:820px){.infoGrid{grid-template-columns:1fr}}

.hours{width:100%;border-collapse:collapse;margin-top:6px}
.hours th,.hours td{text-align:left;padding:9px 0;font-size:16px;
  border-bottom:1px solid var(--rule);font-weight:400}
.hours th{color:var(--ink-soft);font-family:var(--font-mono);font-size:12px;
  letter-spacing:.12em;text-transform:uppercase}
.hours td{color:var(--ink);text-align:right;font-variant-numeric:tabular-nums}
.hours tr:last-child th,.hours tr:last-child td{border-bottom:0}
.hours tr.today th,.hours tr.today td{color:var(--accent)}

/* ==========================================================================
   Form
   ========================================================================== */
.formWrap{border:1px solid var(--rule);background:var(--surface);padding:48px 44px}
.field{margin-bottom:26px}
.field label{display:block;font-family:var(--font-mono);font-size:var(--label);letter-spacing:.18em;
  text-transform:uppercase;color:var(--ink-soft);margin-bottom:10px}
.field label .req{color:var(--accent)}
.field input,.field select,.field textarea{width:100%;background:var(--canvas);
  border:1px solid var(--rule-strong);color:var(--ink);font-family:var(--font-text);
  font-size:16.5px;padding:16px 18px;transition:border-color .25s,background .25s}
.field textarea{min-height:150px;resize:vertical;line-height:1.6}
.field select{appearance:none;cursor:pointer;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='9' viewBox='0 0 14 9'%3E%3Cpath d='M1 1l6 6 6-6' stroke='%23A79E93' stroke-width='2' fill='none'/%3E%3C/svg%3E");
  background-repeat:no-repeat;background-position:right 18px center;padding-right:46px}
.field input:focus,.field select:focus,.field textarea:focus{border-color:var(--accent);
  background:var(--surface-2);outline:none}
.field input::placeholder,.field textarea::placeholder{color:var(--ink-faint)}
.field .err{display:none;margin-top:9px;font-size:14px;color:var(--err);
  align-items:center;gap:8px}
.field.invalid input,.field.invalid select,.field.invalid textarea{border-color:var(--err)}
.field.invalid .err{display:flex}
.field .hint{margin-top:9px;font-size:14px;color:var(--ink-faint)}
.row2{display:grid;grid-template-columns:1fr 1fr;gap:26px}
@media(max-width:640px){.row2{grid-template-columns:1fr;gap:0}
  .formWrap{padding:32px 22px}}
.formNote{margin-top:20px;font-size:14.5px;color:var(--ink-faint);line-height:1.6}
.formErr{display:none;margin-bottom:26px;border:1px solid var(--err);
  background:color-mix(in srgb,var(--err) 12%,transparent);padding:16px 18px;font-size:15.5px}
.formErr.show{display:block}
/* The failure message now carries a tel: link — if the form cannot deliver the
   lead, the phone number is the whole point of the message, so it has to look
   like the thing to tap and clear the 24x24 target floor. inline-block,
   because padding does not grow the hit rect of an inline element. */
.formErr a{display:inline-block;padding:4px 2px;color:var(--ink);font-weight:600;
  border-bottom:1px solid var(--accent);transition:color .25s}
.formErr a:hover,.formErr a:focus-visible{color:var(--accent)}
.hp{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}

/* ==========================================================================
   Prose (privacy, long copy)
   ========================================================================== */
.prose{max-width:70ch}
.prose h2{font-size:clamp(26px,3vw,38px);margin:52px 0 18px;max-width:none}
.prose h2:first-child{margin-top:0}
.prose h3{font-size:20px;text-transform:uppercase;margin:34px 0 12px}
.prose p{margin-bottom:18px;color:var(--ink-soft);font-size:17px;line-height:1.7}
.prose ul,.prose ol{margin:0 0 20px 22px;color:var(--ink-soft)}
.prose li{margin-bottom:10px;line-height:1.65}
.prose a{color:var(--accent);border-bottom:1px solid color-mix(in srgb,var(--accent) 45%,transparent)}
.prose a:hover{border-bottom-color:var(--accent)}
.prose strong{color:var(--ink);font-weight:600}

/* ==========================================================================
   FAQ
   ========================================================================== */
.faq{margin-top:56px;border-top:1px solid var(--rule)}
.faq details{border-bottom:1px solid var(--rule)}
.faq summary{cursor:pointer;list-style:none;padding:26px 44px 26px 0;position:relative;
  font-family:var(--font-display);font-size:clamp(18px,2.1vw,23px);text-transform:uppercase;
  letter-spacing:-.01em;transition:color .25s}
.faq summary::-webkit-details-marker{display:none}
.faq summary:hover{color:var(--accent)}
.faq summary::after{content:"";position:absolute;right:8px;top:50%;width:13px;height:2px;
  background:var(--accent);transform:translateY(-50%)}
.faq summary::before{content:"";position:absolute;right:13.5px;top:50%;width:2px;height:13px;
  background:var(--accent);transform:translateY(-50%);transition:transform .3s}
.faq details[open] summary::before{transform:translateY(-50%) scaleY(0)}
.faq .ans{padding:0 60px 28px 0;color:var(--ink-soft);font-size:17px;line-height:1.7}

/* ==========================================================================
   Final CTA + footer
   ========================================================================== */
/* The closing band was a flat --surface rectangle: the last thing every
   interior page said, and the only full-width block on the site carrying no
   image at all. It now sits on the Sportster-at-dusk frame.

   The photograph is NOT grayscaled. "Only service cards in light overlay
   grayscale" was the instruction, and this is a CTA band, not a card.

   Two scrims, not one flat overlay. A single 70% black sheet kills the picture
   everywhere including the parts with no type over them, which is how a photo
   ends up looking like expensive noise. Instead: a horizontal ramp that is
   heavy under the copy column on the left and clears to 25% on the right where
   the sky is, plus a shallow vertical wash so the rule at the top edge does not
   float. Measured over the darkest scrim point the h2 clears 12:1. */
.finalCta{position:relative;isolation:isolate;border-top:1px solid var(--rule);
  background:var(--surface) url('/blackwoodcycles/assets/img/bike-sportster.webp') center 62%/cover no-repeat}
.finalCta::before{content:'';position:absolute;inset:0;z-index:-1;
  background:
    linear-gradient(180deg,rgba(9,9,10,.55) 0%,rgba(9,9,10,.2) 40%,rgba(9,9,10,.6) 100%),
    linear-gradient(90deg,rgba(9,9,10,.92) 0%,rgba(9,9,10,.82) 42%,rgba(9,9,10,.25) 100%)}
.ctaBox{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:40px;
  position:relative;z-index:1}
.ctaBox h2{max-width:16ch}
/* On a phone the copy stacks full-bleed and there is no clear right-hand side
   left for the sky, so the horizontal ramp is replaced by an even, heavier
   sheet. Same picture, still legible. */
@media(max-width:760px){
  .finalCta::before{background:linear-gradient(180deg,rgba(9,9,10,.86) 0%,rgba(9,9,10,.78) 55%,rgba(9,9,10,.9) 100%)}
}

footer{padding:80px var(--gut) 120px;border-top:1px solid var(--rule);
  position:relative;z-index:2;background:var(--canvas)}
.fGrid{max-width:1220px;margin:0 auto;display:grid;
  grid-template-columns:1.4fr 1fr 1fr 1.2fr;gap:48px}
.fCol h4{font-family:var(--font-mono);font-size:var(--label);letter-spacing:var(--label-track);text-transform:uppercase;
  color:var(--ink-faint);margin-bottom:20px;font-weight:400}
/* The gap does the visual spacing but contributes nothing to the hit area — the
   audit measured these links at 18px tall, well under the 24px WCAG 2.5.8 floor
   and nowhere near a thumb. The padding moves into the anchor and the gap shrinks
   to compensate, so the rhythm is unchanged and the target roughly doubles. */
.fCol ul{list-style:none;display:flex;flex-direction:column;gap:2px}
.fCol a{color:var(--ink-soft);font-size:16px;display:inline-block;padding:6px 0}
.fCol a:hover{color:var(--accent)}
/* The footer is the one place the full lockup fits: at 190px wide the
   "Motorcycle Repair" line lands near 11px, which is small but genuinely
   readable, so the tagline is doing work rather than sitting there as texture. */
.fMark{display:block;margin-bottom:18px;line-height:0}
.fMark img{display:block;width:190px;height:auto;max-width:100%}
.fCol address{font-style:normal;color:var(--ink-soft);font-size:16px;line-height:1.7}
.fBar{max-width:1220px;margin:56px auto 0;padding-top:26px;border-top:1px solid var(--rule);
  display:flex;flex-wrap:wrap;gap:20px;justify-content:space-between;
  font-family:var(--font-mono);font-size:var(--label);letter-spacing:.11em;text-transform:uppercase;
  color:var(--ink-faint)}
.fBar a:hover{color:var(--accent)}
/* The phone number inside <address> is the single most valuable link in the
   footer and it measured 121x16 — under the 24px WCAG 2.5.8 target floor.
   inline-block so the vertical padding actually grows the hit rect; a plain
   inline element would paint the padding but keep the 16px box. */
.fCol address a{display:inline-block;padding:5px 0;color:var(--ink)}
.fCol address a:hover{color:var(--accent)}
@media(max-width:900px){.fGrid{grid-template-columns:1fr 1fr;gap:36px}
  footer{padding:64px var(--gut) 120px}}
@media(max-width:560px){.fGrid{grid-template-columns:1fr}}

/* ==========================================================================
   Sticky mobile call bar
   ========================================================================== */
/* THE WHEEL MEDALLION.

   The homepage close act ends on a large wheel with CALL NOW set into its hub,
   and that is the single most memorable object on the site. This bar is the
   same idea at working size: a 78px wheel that turns continuously, sitting in
   FRONT of the red bar and breaking above its top edge, with the words running
   straight across it.

   overflow:visible and a bar that is no longer a flex row. The wheel is taken
   out of flow and centred absolutely so it can protrude past the bar's own box;
   a flex child cannot escape its parent's edge, and the previous 20px inline
   version could only ever sit inside the bar as a companion dot.

   The label is painted ABOVE the wheel, not beside it. That is the whole point
   of the request -- "the call and the number can go across the wheel while it
   spins" -- so the type is the fixed element and the wheel rotates underneath
   it. Because the type now crosses a dark tyre as well as red paint it is set
   in white with a shadow rather than --accent-ink: black would vanish into the
   tread the moment it crossed it. White on #E5262B is 4.3:1, and at 17px/800
   this is large text, which needs 3:1. */
.stickyBar{position:fixed;left:0;right:0;bottom:0;z-index:70;display:none;
  background:var(--accent);font-family:var(--font-display);
  font-size:17px;font-weight:var(--btn-weight);align-items:center;justify-content:center;
  padding:17px;transform:translateY(100%);transition:transform .4s;
  overflow:visible;text-decoration:none}
.stickyBar.up{transform:translateY(0)}
@media(max-width:900px){.stickyBar{display:flex}}

.stickyBar-wheel{position:absolute;left:50%;bottom:6px;width:78px;height:78px;
  margin-left:-39px;border-radius:50%;display:block;pointer-events:none;z-index:1;
  opacity:.92;filter:drop-shadow(0 6px 14px rgba(0,0,0,.45));
  animation:navcall-roll 4.2s linear infinite;will-change:transform}

/* The client: "add a little tint shadow behind the call and the number, like a
   little tight light black shadow, so the white letters pop a bit more. Just a
   subtle shadow."

   Three words in that are doing real work. TIGHT means it hugs the lettering
   rather than spanning the bar. LIGHT means low alpha — this is a tint, not a
   plate, and at high alpha it stops reading as a shadow and starts reading as a
   black pill button sitting on a red bar, which is a second competing control.
   BEHIND means an actual backdrop, not more text-shadow: the label crosses two
   very different grounds (the accent bar and the dark tyre of the spinning
   medallion), and a glow tuned for one is wrong on the other.

   A blurred rounded rect behind the label was tried first and rejected on
   sight, twice. It measured beautifully — 7.74:1 at the worst column — and
   looked wrong, because a box behind a 186px label is a 186px dark shape no
   matter how soft its edges. Both attempts read as a grey smear laid across
   the red bar. The measurement was fine and the thing being measured was the
   wrong thing, which is the whole reason the screenshot gets looked at.

   Stacked text-shadow instead. It is the only backdrop that follows the
   letterforms, so the density lands where "tight" actually means: against the
   strokes, gone within a few pixels, no shape of its own. Four layers rather
   than one strong one, because a single large-radius shadow spreads into the
   same grey haze the box produced, while short radii stacked at rising alpha
   build most of their density in the first 2-3px and fall off fast.

   The 1px vertical offset on the last layer is deliberate: it seats the type
   on the bar instead of making it float, which is what a purely symmetric
   halo does. */
.stickyBar-label{position:relative;z-index:2;color:#fff;letter-spacing:.02em;
  text-shadow:0 0 2px rgba(0,0,0,.92),
              0 0 4px rgba(0,0,0,.82),
              0 0 8px rgba(0,0,0,.55),
              0 1px 2px rgba(0,0,0,.9)}

/* Reduced motion stops the turn but keeps the medallion, because the wheel is
   brand furniture here rather than decoration -- removing it would leave a
   plain red bar and quietly delete the thing the client asked for. */
@media(prefers-reduced-motion:reduce){
  .stickyBar-wheel{animation:none}
}

/* ==========================================================================
   Thank you
   ========================================================================== */
.tyWrap{min-height:100svh;display:flex;flex-direction:column;align-items:center;
  justify-content:center;text-align:center;padding:150px var(--gut) 120px;position:relative;z-index:2}
.tyMark{width:76px;height:76px;border:2px solid var(--accent);display:flex;align-items:center;
  justify-content:center;margin-bottom:34px}
.tyMark iconify-icon{font-size:38px;color:var(--accent)}
.tyWrap h1{max-width:16ch;margin:0 auto}
.tyWrap .heroSub{margin-left:auto;margin-right:auto}
.tyWrap .heroCta{justify-content:center}

/* ==========================================================================
   Motion floor
   ========================================================================== */
[data-reveal]{opacity:0;transform:translateY(22px)}
/* padding + negative margin extends the clip box past the baseline so descenders
   (y, g, p) are not shaved off, without changing the laid-out line height */
.word{display:inline-block;overflow:hidden;vertical-align:bottom;
  padding-bottom:.16em;margin-bottom:-.16em}
.word > span{display:inline-block;transform:translateY(105%)}

@media(max-width:1100px){:root{--gut:28px}}
@media(max-width:560px){:root{--gut:20px}}

@media(prefers-reduced-motion:reduce){
  *,*::before,*::after{animation-duration:.01ms!important;animation-iteration-count:1!important;
    transition-duration:.01ms!important;scroll-behavior:auto!important}
  [data-reveal]{opacity:1!important;transform:none!important}
  .word > span{transform:none!important}
  .dot{animation:none}
  .mTrack,.revTrack{transform:none!important}
  .gItem img{transform:none!important}
}

/* ── Map ──────────────────────────────────────────────────────────────────
   Greyscale-and-inverted so the map belongs to a dark page instead of being a
   lit window cut into it. See gmap() in build.py for why the filter runs
   grayscale BEFORE invert.

   The iframe is left fully interactive on purpose — the client asked for an
   interactive map, and a filter does not intercept pointer events. The one
   cost is that a map inside a long scrolling page swallows the wheel when the
   cursor crosses it, so `.mapWrap` gets an explicit height and the "Get
   directions" button, which is what most visitors actually want, sits on top
   as a normal link that needs no panning at all. */
.mapWrap{position:relative;width:100%;height:var(--mapH,380px);margin-top:24px;
  border:1px solid var(--rule);border-radius:3px;overflow:hidden;background:#0E0E10}
.mapWrap iframe{position:absolute;inset:0;width:100%;height:100%;border:0;display:block;
  filter:grayscale(1) invert(.92) contrast(.88) brightness(1.06)}
/* Two-class selector so it beats .fCol a inside the footer column. */
.mapWrap .mapGo{position:absolute;right:12px;bottom:12px;z-index:2;padding:11px 15px;
  background:var(--accent);color:#fff;text-decoration:none;border-radius:2px;
  font:600 .72rem/1 var(--font-mono);letter-spacing:.12em;text-transform:uppercase;
  transition:filter .25s,transform .25s}
.mapWrap .mapGo:hover{filter:brightness(1.12);transform:translateY(-1px);color:#fff}
.mapWrap--mini{margin-top:20px;max-width:300px}
.mapWrap--mini .mapGo{right:8px;bottom:8px;padding:8px 11px;font-size:.62rem;letter-spacing:.1em}
.mapWrap--card{margin-top:18px}
@media(max-width:700px){.mapWrap{height:calc(var(--mapH,380px) * .72)}}
