/* styles.css: page styles. Revision 5.
   Tokens in tokens.css. Rationale in DIRECTION.md.

   Subject:        Merit Plumbing, Inc., union commercial mechanical contractor
   Composition:    editorial column with hanging margin (site-wide)
   Section rhythm: alternating brand navy / sheet, with navy-deep for weight
   Signature move: A13.1 marker band, legend field + uppercase legend + flow arrow

   THE GROUND IS MERIT'S OWN BRAND NAVY. CLAUDE.md section 2 level 1: an
   established client identity outranks any derived palette. Their logo is
   registered and in continuous use. --legend (A13.1 safety yellow) is retained
   as the single accent because it is absent from their identity and so adds a
   conversion color without competing with the mark.

   Standing constraints, pairings that must never appear:
     --legend on --sheet   (1.37:1)  <- the live risk: yellow text on light
     --wave   as body text (3.06:1)  <- BRAND color, but rules only, never type
     --muted  on --navy    (2.36:1)
     --mist   on --sheet   (1.51:1)
*/

/* ---------------------------------------------------------------- fonts --
   Self-hosted, same-origin, no CDN at runtime, so font-src 'self' needs no
   exception. Provenance and licenses in fonts/README.md. */

@font-face {
  font-family: "Space Grotesk";
  src: url("fonts/SpaceGrotesk-500.woff2") format("woff2");
  font-weight: 500; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Space Grotesk";
  src: url("fonts/SpaceGrotesk-700.woff2") format("woff2");
  font-weight: 700; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "IBM Plex Sans";
  src: url("fonts/IBMPlexSans-Regular.woff2") format("woff2");
  font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "IBM Plex Sans";
  src: url("fonts/IBMPlexSans-Medium.woff2") format("woff2");
  font-weight: 500; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "IBM Plex Mono";
  src: url("fonts/IBMPlexMono-Medium.woff2") format("woff2");
  font-weight: 500; font-style: normal; font-display: swap;
}

/* ----------------------------------------------------------------- base -- */

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

html {
  font-size: 100%;
  -webkit-text-size-adjust: 100%;
  /* Declared so browsers in dark mode do not apply their own treatment to
     scrollbars and UI chrome around a page with no light variant. This site is
     a designed dark-ground system; there is no separate dark mode to offer. */
  color-scheme: dark;
}

/* Anchor targets are scrolled to, so give them breathing room under any
   in-page navigation and keep the marker band visible on arrival. */
:target { scroll-margin-top: var(--space-l); }

body {
  margin: 0;
  background: var(--navy);
  color: var(--sheet);
  font-family: var(--font-body);
  font-size: var(--size-body);
  line-height: var(--leading-body);
  overflow-wrap: break-word;
  text-wrap: pretty;
}

h1, h2, h3 { margin: 0; text-wrap: balance; }
h1, h2, h3, .display { overflow-wrap: normal; hyphens: none; }
p, ul, ol, dl, dd, figure { margin: 0; }
ul, ol { padding: 0; list-style: none; }

img { max-width: 100%; height: auto; display: block; }

a { color: var(--legend); text-decoration-thickness: 1px; text-underline-offset: 0.18em; }
a:hover { text-decoration-thickness: 2px; }

/* On light grounds --legend as TEXT is 1.37:1, the standing constraint at the top
   of this file, and the one this palette will keep trying to break. Links there take
   --graphite text (14.75:1) and keep the accent as a thick underline instead, so the
   yellow is still doing the signalling without ever carrying the letterforms.
   The same applies inside .bid, whose ground IS --legend. */
.band--sheet a {
  color: var(--navy-deep);
  text-decoration-color: var(--legend);
  text-decoration-thickness: 2px;
}
/* Inside .bid the ground already IS --legend, so the accent cannot also be the
   underline. Everything there is --navy-deep on yellow at 8.81:1. */
.bid a { color: var(--navy-deep); text-decoration-color: currentColor; }
/* ...except the button, which is a dark fill and needs light text. Without
   this the rule above wins on specificity and the label disappears into its
   own background. */
.bid .btn { color: var(--sheet); }
.band--sheet a:hover, .bid a:hover { text-decoration-thickness: 3px; }

/* ----------------------------------------------------------- skip link -- */

.skip {
  position: absolute; left: var(--space-s); top: -100vh; z-index: 20;
  display: inline-flex; align-items: center; min-height: var(--target-min);
  padding: var(--space-2xs) var(--space-m);
  background: var(--legend); color: var(--navy-deep);
  font-family: var(--font-mono); font-size: var(--size-meta);
  letter-spacing: var(--tracking-meta); text-transform: uppercase; text-decoration: none;
}
.skip:focus { top: var(--space-s); }

/* -------------------------------------------------------------- layout -- */

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

/* The hanging margin. On narrow screens the rail stacks ABOVE its content,
   never below it, a rail pushed below the content it labels reads as a
   footnote and, in the hero, pushed the primary action off the first screen. */
.editorial { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--space-s) 0; }
.editorial__rail { grid-row: 1; }
.editorial__body { grid-row: 2; }

@media (min-width: 64em) {
  .editorial {
    grid-template-columns: var(--rail) minmax(0, 1fr);
    column-gap: var(--rail-gap);
    align-items: start;
  }
  .editorial__rail { grid-row: 1; grid-column: 1; position: sticky; top: var(--space-l); }
  .editorial__body { grid-row: 1; grid-column: 2; }

  /* A rail carrying only its marker band has nothing to hang, so it does not
     earn a column: the grid collapses and the marker sits above the heading.
     CLAUDE.md section 5, vertical emptiness. A hanging margin is only a margin
     while it holds something; empty, it reads as a column that failed to load.
     Degrades safely, without :has() the two-column layout simply persists. */
  .editorial:not(:has(.rail-value)):not(:has(.rail-list)) {
    grid-template-columns: minmax(0, 1fr);
  }
  .editorial:not(:has(.rail-value)):not(:has(.rail-list)) .editorial__rail {
    grid-row: 1; grid-column: 1; position: static;
  }
  .editorial:not(:has(.rail-value)):not(:has(.rail-list)) .editorial__body {
    grid-row: 2; grid-column: 1;
  }

  /* The same collapse, applied by hand where a rail DOES carry a value but
     cannot carry its section's height. A hanging margin is a margin only while
     something sits in it: a 106px rail beside a 1347px body leaves 1241px of
     empty column, which reads as content that failed to load rather than as
     restraint. CLAUDE.md section 5. The rule above cannot detect this, because
     the ratio is a fact about the rendered page, not about the markup. */
  .editorial--stacked { grid-template-columns: minmax(0, 1fr); }
  .editorial--stacked .editorial__rail { grid-row: 1; grid-column: 1; position: static; }
  .editorial--stacked .editorial__body { grid-row: 2; grid-column: 1; }
}

.band { padding-block: var(--space-section); }

/* Alternating rhythm. Each change of ground marks a change of purpose. */
.band--sheet { background: var(--sheet); color: var(--navy-deep); }
.band--deep { background: var(--navy-deep); }

/* ------------------------------------------------ THE SECTION COMPONENT --

   A section is a HEADING and its CONTENT. Everything else is optional and the
   default for all three is ABSENT:

     .editorial__rail   a fact hung in the margin      - optional, default none
     .eyebrow           the family the section is in   - optional, default none
     the intro          a sentence before the content  - optional, default none

   WHY THIS IS WRITTEN DOWN. Each of these is a hole, and across this build
   every pass filled whichever one the previous pass emptied, always with a
   description of the content directly beneath it. The rail acquired counts and
   trade-wide facts. The eyebrow acquired the heading again in smaller type. The
   intro acquired a list of what the grid below it displays. Removing the
   instance never worked, because the slot survived and the next pass refilled
   it.

   THE TEST for anything going in any of the three: does it say something the
   heading and the content below do not already say? If a section has a heading
   and its content and nothing true in between, that is the finished state, not
   an unfinished one.

   MECHANICALLY: no optional slot carries trailing space. Spacing is owned by
   the element that FOLLOWS, so an absent slot costs nothing and cannot leave a
   gap that invites something to fill it. All three collapse when empty. */

.editorial__rail:empty,
.eyebrow:empty,
.editorial__body > .prose:empty { display: none; }

/* --------------------------------------------------------- eyebrow ----- */

/* THE A13.1 MARKER BAND IS GONE. It sat above every section heading as a
   filled --legend field with a flow arrow, and it was doing two things wrong.

   It restated. SCOPE above a headline containing the word scope, SERVICE above
   "Service, repair and replacement", LICENSES above "Five licenses, two
   states". Nine of eleven were the heading again in smaller type.

   And it was spending the accent. --legend is the one accent this palette
   allows, and eleven filled yellow tabs meant the conversion panel was
   competing with navigation furniture for it. The yellow now belongs to the
   bid panel, the closer number and links, which is where it does work.

   What is left is plain type, and only where a reader could not tell the
   section's family from its heading. Two survive site-wide. */
.eyebrow {
  margin-bottom: var(--space-2xs);
  font-family: var(--font-mono); font-weight: var(--weight-medium);
  font-size: var(--size-meta); letter-spacing: var(--tracking-meta);
  text-transform: uppercase;
  color: var(--mist);                 /* 5.86:1 on --navy */
}
.band--sheet .eyebrow { color: var(--muted); }   /* 5.26:1 on --sheet */

/* ----------------------------------------------------------------- type -- */

.display {
  font-family: var(--font-display); font-weight: var(--weight-bold);
  font-size: var(--size-display); line-height: var(--leading-display);
  letter-spacing: var(--tracking-display);
}
h2 {
  font-family: var(--font-display); font-weight: var(--weight-bold);
  font-size: var(--size-h2); line-height: var(--leading-h2);
  letter-spacing: var(--tracking-h2);
}
h3 {
  font-family: var(--font-display); font-weight: var(--weight-medium);
  font-size: var(--size-h3); line-height: var(--leading-h3);
  letter-spacing: var(--tracking-h3);
}

.lead {
  font-size: var(--size-lead); line-height: var(--leading-lead);
  max-width: var(--measure-lead); margin-top: var(--space-m);
}
.prose { max-width: var(--measure); }
.prose > * + * { margin-top: var(--space-s); }
h2 + .prose, h2 + .lead { margin-top: var(--space-m); }

.small { font-size: var(--size-small); }

/* Secondary text. --mist on navy, --muted on sheet. NOT interchangeable -
   see the standing constraints at the top of this file. */
.muted { color: var(--mist); font-size: var(--size-small); }
.band--sheet .muted { color: var(--muted); }

/* Rail metadata: the reason the hanging margin exists. */
/* THE RAIL COMPONENT.
   Contract, and the reason it is written down: this component has three slots
   and only ONE of them is required.

     .rail-label   a category name
     .rail-value   a TRUE fact, or absent

   There is no description slot. A rail is a label and a value. Prose belongs in
   the body column, and a sentence in the rail was always a restatement of the
   section it labelled.

   The value slot is NOT a layout hole to be filled. If a section has no true
   number, the element is omitted, or left empty and collapses to nothing.
   Never an ordinal (01/02/03), never a zero, never a count invented to give the
   slot something to hold. CLAUDE.md section 5: structural devices must encode
   something true, and 01/02/03 is only permitted where the content is genuinely
   a sequence.

   This kept regressing because the slot looked mandatory. :empty makes an
   unfilled slot render as nothing, so the layout never asks to be fed. */
.rail-label:empty,
.rail-value:empty { display: none; }

/* A rail is also allowed to be nothing but its marker band. */
.editorial__rail:empty { display: none; }

.rail-label {
  font-family: var(--font-mono); font-weight: var(--weight-medium);
  font-size: var(--size-meta); letter-spacing: var(--tracking-meta);
  text-transform: uppercase; color: var(--mist);
}
.band--sheet .rail-label { color: var(--muted); }
.rail-value {
  font-family: var(--font-display); font-weight: var(--weight-bold);
  font-size: 1.5rem; letter-spacing: -0.02em; line-height: 1.1;
  margin-top: var(--space-3xs);
}
.rail-list { margin-top: var(--space-2xs); font-size: var(--size-small); }
.rail-list li + li { margin-top: var(--space-3xs); }

/* --------------------------------------------------------- placeholders --
   Brackets are literal text in the HTML, never generated content, so the
   disclosure survives CSS failing to load. */

.ph {
  font-family: var(--font-mono); font-size: 0.88em;
  color: var(--legend);                                  /* 6.14:1 on navy */
  border-bottom: 1px solid color-mix(in srgb, var(--legend) 40%, var(--navy-deep));
  padding-bottom: 0.08em;
}
/* On the light panel --legend text is 1.37:1 and forbidden. The placeholder
   inverts to --graphite and keeps a legend-yellow rule to stay recognisable. */
.band--sheet .ph {
  color: var(--navy-deep);
  border-bottom-color: var(--legend);
}
.ph--block {
  display: block; font-family: var(--font-mono); font-size: var(--size-meta);
  line-height: 1.65; border-bottom: 0;
  border-left: 3px solid var(--legend);
  padding: var(--space-3xs) 0 var(--space-3xs) var(--space-s);
  margin-top: var(--space-s);
  color: var(--legend);
}
.band--sheet .ph--block { color: var(--navy-deep); }

/* -------------------------------------------------------- concept notice -- */

.concept-notice {
  background: var(--legend); color: var(--navy-deep);
  padding-block: var(--space-s);
}
.concept-notice p { max-width: 62rem; font-size: var(--size-meta); line-height: 1.6; }
.concept-notice strong {
  font-family: var(--font-mono); font-weight: var(--weight-medium);
  text-transform: uppercase; letter-spacing: var(--tracking-meta);
  margin-right: var(--space-2xs);
}

/* --------------------------------------------------------------- header -- */

.site-head { border-bottom: 1px solid color-mix(in srgb, var(--wave) 40%, var(--navy)); }
.head-inner {
  display: flex; flex-wrap: wrap; align-items: center;
  justify-content: space-between; gap: var(--space-2xs) var(--space-m);
  padding-block: var(--space-2xs);
}
/* The registered mark, reversed, sitting directly on the navy.

   The supplied artwork is navy on opaque white with no transparency. A white
   lockup chip was the first treatment, matching their own site, but at header
   scale a white box in a navy bar reads as a broken image rather than as a
   design decision. So the raster is knocked out instead: see
   img/merit/SOURCES.md for how.

   What "reversed" means here: alpha is everything that is NOT white, so the
   background and the MERIT letters both knock out. The background because it
   must not show as a box, and MERIT because the navy page ground showing
   through IS the reverse. The shield inverts from a navy plate carrying white
   letters to a pale plate carrying ground-colored ones. The waves keep the
   existing lighter blue.

   The original navy-on-white pair (logo.webp / logo.png) stays on disk for any
   light-ground section that needs it. No light section carries the mark today. */
.wordmark {
  display: inline-flex; align-items: center;
  min-height: var(--target-min);
  text-decoration: none;
}
/* 56px, not 40. At 40 the whole stacked lockup - shield, waves and the second
   line - was compressed into the height of a single nav link and read as a
   shrunken sign. width and height on the img reserve the box before it loads;
   height with width:auto keeps the rendered ratio exactly the intrinsic one,
   because the shield skews visibly if the two are forced to a rounded pair. */
.wordmark img { height: 56px; width: auto; display: block; }

@media (prefers-reduced-motion: no-preference) {
  .wordmark img { transition: opacity var(--duration-fast) var(--easing); }
}
.wordmark:hover img, .wordmark:focus-visible img { opacity: 0.82; }

.navlist { display: flex; flex-wrap: wrap; gap: var(--space-2xs) var(--space-m); }
.navlist a {
  display: inline-flex; align-items: center; min-height: var(--target-min);
  font-family: var(--font-mono); font-size: var(--size-meta);
  letter-spacing: var(--tracking-meta); text-transform: uppercase;
  color: var(--sheet); text-decoration: none;
  border-bottom: 2px solid transparent;
}
.navlist a:hover { color: var(--legend); }
/* Current page is marked by more than color: a rule as well as a hue, so the
   state survives greyscale and forced colors. */
.navlist a[aria-current="page"] { color: var(--legend); border-bottom-color: var(--legend); }

/* ------------------------------------------------------------------ hero -- */

.hero { padding-block: var(--space-2xl) var(--space-xl); }

/* An interior page whose only headline would be its own nav label gets no hero
   band at all. It opens on the first content section, and that section carries
   the h1 and the page rail. --space-section already sets the vertical rhythm;
   the band only needs to sit clear of the nav, so the opening rule is the
   heading measure, not extra air. */
/* Display type takes --measure-display everywhere. The two hand-set ch values
   that used to live here and on .hero were the same idea applied twice with
   different numbers. */
.display { max-width: var(--measure-display); }

/* No .editorial--open rules any more. The opening section's rail was carrying
   Plumbing / Division 22, which earned it a column; that value is gone because
   every plumbing contractor bids under Division 22, so it named the trade
   rather than Merit and a competitor could have carried it unchanged. The rail
   is now its marker alone, which collapses the section to one column like the
   sections below it, and the grid needs no special placement. */
/* The carousel is a separate object from the hero, not a continuation of it.
   At --space-m the two ran together and the first photograph read as part of
   the headline block. */
.hero:has(+ .proj-carousel) { padding-block-end: var(--space-xl); }


/* HOME HERO, two columns: the type block left, the conversion panel right.

   The hero ran one column at 55% of the measure with dead navy beside it, which
   CLAUDE.md section 5 reads as a page that failed to load rather than as a
   margin. The panel is what belongs in that space: it is the conversion point,
   CONTENT-STANDARDS.md section 5 wants it high on the first screen, and moving
   it up beside the lead costs no new content and no second motif. The
   alternative considered was a large reversed shield, which repeats the header
   mark on the same screen and would be a second visual device alongside the
   A13.1 marker band, which section 5 caps at one. */
@media (min-width: 64em) {
  .hero__split {
    display: grid;
    grid-template-columns: minmax(0, 1.06fr) minmax(0, 0.94fr);
    column-gap: var(--space-xl);
    align-items: start;
  }
  /* The column IS the measure now, so the 18ch cap would wrap the headline
     early and leave its own column ragged inside a column that already fits. */
  .hero__split .display { max-width: none; }
  .hero__split .lead { max-width: none; }
  .hero__split .bid { margin-top: 0; max-width: none; }
}


/* Full-container headline. An 18ch measure is right for a hero with a rail
   beside it; with no rail the column is the whole container, and holding a long
   headline at 18ch leaves two thirds of the band empty, which reads as a page
   that failed to load rather than as margin. CLAUDE.md section 5. The headline
   drops to the lower display step so more words fit per line, and the
   supporting copy takes --size-standfirst so the step down is not 77px to 17px
   in one go.

   ONLY for a headline with enough words to fill the measure. Applied to a short
   one it makes things worse, not better: text-wrap: balance splits 44
   characters into two short lines at the smaller size, so the type block ends
   up narrower than it was AND lighter. Tried on About and reverted. */
.hero--wide .display { font-size: var(--size-display-long); }
.hero .display--long, .hero--wide .display { max-width: none; }
.standfirst {
  margin-top: var(--space-s);
  max-width: var(--measure-lead);
  font-size: var(--size-standfirst);
  line-height: var(--leading-standfirst);
  color: var(--mist);
}
.band--sheet .standfirst { color: var(--muted); }
.hero__meta {
  margin-top: var(--space-l); display: flex; flex-wrap: wrap;
  gap: var(--space-2xs) var(--space-l);
  font-family: var(--font-mono); font-size: var(--size-meta);
  letter-spacing: var(--tracking-meta); text-transform: uppercase;
  color: var(--mist);
}

/* ------------------------------------------------------------------ bid -- */

/* The primary action. A solid --legend panel with --navy-deep text at 8.81:1 -
   the loudest object on every page, which CONTENT-STANDARDS.md section 5
   requires of the conversion point. */
.bid {
  background: var(--legend); color: var(--navy-deep);
  padding: var(--space-l);
  max-width: 38rem; margin-top: var(--space-l);
}
/* No .bid__label. It read "Primary action", which is what this component is
   called while building it, not information a visitor lacks. The heading below
   it already says Request a bid. */
.bid h2, .bid h3 {
  font-family: var(--font-display); font-weight: var(--weight-bold);
  color: var(--navy-deep);
}
.bid__list { margin-top: var(--space-m); }
.bid__list li {
  padding-left: var(--space-m); position: relative;
  border-top: 1px solid color-mix(in srgb, var(--navy-deep) 25%, var(--legend));
  padding-top: var(--space-2xs); padding-bottom: var(--space-2xs);
  font-size: var(--size-small);
}
.bid__list li::before {
  content: ""; position: absolute; left: 0; top: 1.05em;
  width: 0.6rem; height: 0.6rem; background: var(--navy-deep);
  clip-path: polygon(0 0, 100% 50%, 0 100%);
}
.bid .ph { color: var(--navy-deep); border-bottom-color: var(--navy-deep); }
/* The gap sits UNDER the real contact, quiet and labelled, so the panel leads
   with something that works rather than opening on a blank. */
.bid__field {
  margin-top: var(--space-s);
  padding-top: var(--space-2xs);
  border-top: 1px solid color-mix(in srgb, var(--navy-deep) 25%, var(--legend));
  font-size: var(--size-meta);
}
.bid__field-label {
  font-family: var(--font-mono); letter-spacing: var(--tracking-meta);
  text-transform: uppercase; color: var(--navy-deep);
  margin-right: var(--space-2xs);
}
.bid .ph { color: var(--navy-deep); border-bottom-color: color-mix(in srgb, var(--navy-deep) 45%, var(--legend)); }

.bid__tel {
  display: inline-flex; align-items: center; min-height: var(--target-min);
  color: var(--navy-deep); font-family: var(--font-display);
  font-weight: var(--weight-bold); font-size: 1.375rem; letter-spacing: -0.02em;
  margin-top: var(--space-s);
}

/* Secondary action, visually subordinate to the bid panel by construction:
   outlined rather than filled, and smaller. */
.service-line { margin-top: var(--space-l); max-width: 38rem; }
.service-line__tel {
  display: inline-flex; align-items: center; min-height: var(--target-min);
  font-family: var(--font-display); font-weight: var(--weight-bold);
  font-size: 1.25rem; letter-spacing: -0.02em;
}

/* ----------------------------------------------------------------- grids -- */

.stack > * + * { margin-top: var(--space-l); }

/* ------------------------------------------------------------- evidence --
   Merit's own photographs. Captions describe the work; sourcing lives in the
   footer statement, not under every image. */
.evidence { margin-top: var(--space-l); }

/* One of Merit's own photographs introducing a group of services, where it
   genuinely shows that work. Not decoration, and not used where no real
   photograph of the service exists. */
.group-shot { margin-top: var(--space-l); max-width: 46rem; }
.group-shot img { width: 100%; aspect-ratio: 3 / 2; object-fit: cover; }
.evidence img {
  width: 100%; aspect-ratio: 4 / 3; object-fit: cover;
  border: 1px solid color-mix(in srgb, var(--wave) 45%, var(--navy));
}
.evidence figcaption {
  margin-top: var(--space-2xs);
  font-size: var(--size-small); color: var(--mist);
}
.band--sheet .evidence figcaption { color: var(--muted); }
/* ----------------------------------------------------------- carousel ----
   PROGRESSIVE ENHANCEMENT. The seven projects are a plain grid in the HTML and
   stay a grid unless js/carousel.js runs and GSAP is present, at which point the
   parent gains .is-enhanced and the same markup becomes a scroll-snap track.
   Nothing here hides content from a visitor without JavaScript.

   Momentum swipe is the browser's own scroll-snap, not a reimplementation. */

.proj { position: relative; }

/* The carousel markup IS the no-JS fallback: without JavaScript it renders as
   the plain .proj-grid below, carrying all seven projects with their labels.
   JS only adds .is-enhanced, which turns the same list into a track. */


/* --- enhanced: same list, now a track ---------------------------------- */
.is-enhanced .proj-grid {
  display: flex;
  grid-template-columns: none;
  gap: var(--space-m);
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;
  scroll-padding-inline: var(--space-m);
  padding-block-end: var(--space-s);
  scrollbar-width: none;
}
.is-enhanced .proj-grid::-webkit-scrollbar { display: none; }

/* ONE dominant image centerd in the full width of the band, with a narrow peek
   either side, the geometry of Merit's own slideshow. The carousel deliberately
   sits OUTSIDE the editorial column: squeezed into the text column it cannot
   center, and the left peek disappears entirely.

   The captions are burned into the photographs, so the carousel carries no
   labels; the static grid below is where the project data lives. */
/* CAPPED WIDTH. Merit's slideshow sits in a fixed container, roughly 1230px,
   and the image inside it is about 75% of that with ~12% slivers either side.
   Sizing in vw instead was the mistake: it looks right at 1440 and then keeps
   growing, so on a wide monitor the photograph becomes enormous. Percentages of
   a capped container hold the proportion at every width. */
.proj-carousel {
  margin-top: var(--space-l);
  max-width: var(--carousel-max);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.is-enhanced .proj-grid {
  gap: 12px;
  margin-top: 0;
  scroll-padding-inline: 0;
  padding-inline: 12.5%;     /* (100 - 75) / 2 */
}

/* 100% of the PADDED content box, not of the container: with 12.5% padding
   either side that resolves to 75% of the container, which is the proportion
   wanted. Setting 75% here would compound the padding and give 56%. */
.is-enhanced .proj-card {
  flex: 0 0 100%;
  scroll-snap-align: center;
  scroll-margin-inline: 0;
  display: block;
}

/* The photograph is the object: no frame, no border. */
.is-enhanced .proj-card img { aspect-ratio: 3 / 2; border: 0; }

/* Labels stay, they carry unit counts and locations the photographs do not -
   but only under the center slide, so the peeks read as pure image. */
.is-enhanced .proj-card .proj-card__sector, .is-enhanced .proj-card .proj-card__name { opacity: 0; }
.is-enhanced .proj-card.is-current .proj-card__sector, .is-enhanced .proj-card.is-current .proj-card__name { opacity: 1; }
@media (prefers-reduced-motion: no-preference) {
  .is-enhanced .proj-card .proj-card__sector, .is-enhanced .proj-card .proj-card__name { transition: opacity 0.4s var(--easing); }
}
@media (prefers-reduced-motion: reduce) {
  .is-enhanced .proj-card .proj-card__sector, .is-enhanced .proj-card .proj-card__name { opacity: 1; }
}

/* Peeks sit well back so the center image is unambiguously the subject. */
.is-enhanced .proj-card { opacity: 0.3; }
.is-enhanced .proj-card.is-current { opacity: 1; }
@media (prefers-reduced-motion: no-preference) {
  .is-enhanced .proj-card { transition: opacity 0.5s var(--easing); }
}
@media (prefers-reduced-motion: reduce) {
  .is-enhanced .proj-card { opacity: 1; }
}

/* --- controls, injected by JS ------------------------------------------ */

/* Arrows straddle the edges of the CENTER image, framing it, rather than
   floating loose on the peeks. Solid light disc on a navy ground: reads at any
   size, sits clear of whatever photograph is behind it, and does not compete
   with the yellow, which belongs to the conversion panel alone. */
.proj-carousel { position: relative; }

.car-arrow {
  position: absolute; top: 50%; z-index: 3;
  width: 56px; height: 56px; margin-top: -28px;
  display: inline-flex; align-items: center; justify-content: center;
  padding: 0; cursor: pointer; border: 0; border-radius: 50%;
  background: var(--sheet); color: var(--navy-deep);
  box-shadow: 0 4px 20px rgb(0 0 0 / 0.35);
}
.car-arrow svg { width: 22px; height: 22px; }

/* Center image is 60vw wide at base, 46/36/32vw up the breakpoints; each arrow
   sits astride its edge, half on the image and half on the peek. */
.car-arrow--prev { left: calc(12.5% - 28px); }
.car-arrow--next { right: calc(12.5% - 28px); }
@media (min-width: 48em) {
}
@media (min-width: 64em) {
  .car-arrow { width: 64px; height: 64px; margin-top: -32px; }
  .car-arrow svg { width: 24px; height: 24px; }
  .car-arrow--prev { left: calc(12.5% - 32px); }
  .car-arrow--next { right: calc(12.5% - 32px); }
}
@media (min-width: 96em) {
}

.car-arrow--prev svg { transform: rotate(180deg); }


.car-arrow:hover { background: var(--legend); }
.car-arrow:active { transform: scale(0.94); }
.car-arrow[disabled] { opacity: 0; pointer-events: none; }

@media (prefers-reduced-motion: no-preference) {
  .car-arrow {
    transition: background-color var(--duration-fast) var(--easing),
                opacity var(--duration-fast) var(--easing),
                transform var(--duration-fast) var(--easing);
  }
}



/* Visually hidden, still announced. */
.vh { position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden; clip-path:inset(50%); white-space:nowrap; }

/* -------------------------------------------------------- project cards --
   Merit's own photographs of completed buildings. Each image carries its own
   burned-in name and location, so the heading repeats the name rather than
   captioning over the top of it. */
.proj-grid { display: grid; gap: var(--space-l); margin-top: var(--space-l); }
@media (min-width: 40em) { .proj-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 64em) { .proj-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
.proj-card img {
  width: 100%; aspect-ratio: 3 / 2; object-fit: cover;
  border: 1px solid color-mix(in srgb, var(--wave) 45%, var(--navy));
}
.band--sheet .proj-card img { border-color: color-mix(in srgb, var(--muted) 35%, var(--sheet)); }
.proj-card__sector {
  margin-top: var(--space-s);
  font-family: var(--font-mono); font-size: var(--size-meta);
  letter-spacing: var(--tracking-meta); text-transform: uppercase;
  color: var(--mist);
}
.band--sheet .proj-card__sector { color: var(--muted); }
.proj-card__name {
  margin-top: var(--space-3xs);
  font-family: var(--font-display); font-weight: var(--weight-bold);
  font-size: var(--size-h3); letter-spacing: -0.02em; line-height: 1.2;
}

/* --------------------------------------------------------- service cards --
   Image, name, description, the arrangement Merit's own services page uses.
   Every one of these images is Getty stock from their current site, recorded in
   img/merit/SOURCES.md and flagged for replacement before production. */
.svc-grid { display: grid; gap: var(--space-l); margin-top: var(--space-l); }
@media (min-width: 40em) { .svc-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 64em) { .svc-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
/* Every card carries an image. Four are Merit's own, where the photograph
   genuinely shows that work. The remaining ten are the stock their current site
   uses, reproduced on instruction and recorded in img/merit/SOURCES.md as
   REMOVE-before-production. */
.svc-card { display: flex; flex-direction: column; }

/* Fixed ratio on every image so the row of cards shares one baseline whatever
   the source photograph's proportions are. */
.svc-card img {
  width: 100%; aspect-ratio: 3 / 2; object-fit: cover;
  margin-bottom: var(--space-s);
}

/* Headings run to one or two lines depending on the service name, which left
   the descriptions starting at different heights across a row. A two-line floor
   puts every description on the same baseline without truncating the long ones.
   min-height, not height, so a three-line name still grows. */
/* On the page that opens on its first content section, that section's heading is
   the h1, so its cards sit one level below it and are h2 in the markup. They are
   the same object as every other card and take the same treatment: the heading
   level is structure, not size. */
.svc-card h3, .svc-card__name {
  font-family: var(--font-display); font-weight: var(--weight-bold);
  font-size: var(--size-h3); line-height: var(--leading-h3); letter-spacing: -0.02em;
  min-height: calc(2 * var(--leading-h3) * 1em);
  overflow-wrap: break-word;
  margin-bottom: var(--space-2xs);
}
.svc-card p { overflow-wrap: break-word; hyphens: none; }
.svc-card:not(:has(img)) { border-top: 2px solid var(--legend); padding-top: var(--space-s); }
.svc-card p { font-size: var(--size-small); }

/* Photograph paired with the copy it belongs to, alternating down the section -
   the arrangement Merit's own About page uses. The work and the claim sit
   together, which is the point: the photographs are the evidence for the text. */
.duo-row { display: grid; gap: var(--space-m); margin-top: var(--space-xl); align-items: center; }
.duo-row__text > * + * { margin-top: var(--space-s); }
.duo-row__text h3 { margin-bottom: var(--space-2xs); }

@media (min-width: 56em) {
  .duo-row { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-2xl); }
  /* NO `order` FLIP. The photograph used to alternate sides on desktop, which
     meant the visual order and the DOM order disagreed on every second row.
     CLAUDE.md section 7 requires reading order and focus order to stay matched,
     and a decided stack order on mobile. Both are satisfied the same way:
     photograph first in the DOM on every row, so mobile stacks image then text
     every time and desktop shows image left, text right, every time. The
     alternation was rhythm bought with a mismatch. */
  .duo-row--single { grid-template-columns: minmax(0, 1fr); max-width: 34rem; }
}

/* --------------------------------------------------------------- pillars --
   Merit's own About Us copy, in their words. The design work here is
   presentation, not rewriting: their three blocks get a proper hierarchy, a
   readable measure, and the accent rule that ties them to the rest of the page. */
.pillars { display: grid; gap: var(--space-l); margin-top: var(--space-l); }
@media (min-width: 48em) { .pillars { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.pillar { border-top: 2px solid var(--legend); padding-top: var(--space-s); }
.pillar h3 { margin-bottom: var(--space-2xs); }
.pillar p { font-size: var(--size-small); }

/* ---------------------------------------------------------- testimonials --
   Reproduced verbatim from Merit's own testimonials page. PERMISSION-PENDING:
   the words belong to named third parties, and several carry objective pricing
   and response-time claims that need substantiating before any public build.
   Included here on instruction for a private demo. See img/merit/SOURCES.md. */
.quotes { display: grid; gap: var(--space-l); margin-top: var(--space-l); }
@media (min-width: 56em) { .quotes { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.quote {
  border-left: 3px solid var(--legend);
  padding-left: var(--space-m);
}
.quote blockquote { margin: 0; }
.quote p { font-size: var(--size-small); }
.quote p + p { margin-top: var(--space-2xs); }
.quote figcaption {
  margin-top: var(--space-2xs);
  font-family: var(--font-mono); font-size: var(--size-meta);
  letter-spacing: 0.04em; color: var(--mist);
}
.band--sheet .quote figcaption { color: var(--muted); }

/* Utility spacing classes rather than style="" attributes: PRODUCTION-STANDARDS.md
   section 2 keeps styles in same-origin stylesheets, and the style-src 'self' CSP
   baseline blocks inline style attributes outright. */
.push-l { margin-top: var(--space-l); }

/* Standalone links need a real target. WCAG 2.2 target size exempts links inline
   in a sentence, but these are not that: a "more" link is the entire content of
   its paragraph, and a phone number in an address block is a primary contact
   action. Both are standalone targets, so both take the house 44px. */
.push-l a,
address a {
  display: inline-flex; align-items: center;
  min-height: var(--target-min);
}

.scope-list { margin-top: var(--space-l); display: grid; gap: var(--space-m); }
.scope-item {
  border-top: 1px solid color-mix(in srgb, var(--wave) 40%, var(--navy));
  padding-top: var(--space-s);
}
.band--sheet .scope-item { border-top-color: color-mix(in srgb, var(--muted) 35%, var(--sheet)); }
.scope-item h3 { margin-bottom: var(--space-2xs); }
.scope-item__note { font-size: var(--size-small); color: var(--mist); }
.band--sheet .scope-item__note { color: var(--muted); }


/* Qualification ledger, a plain labelled list of what must be verified.
   Deliberately NOT a badge strip: every badge would be an unverified claim in
   exactly the shape CLAUDE.md section 1 bans. */
.ledger { margin-top: var(--space-l); display: grid; gap: var(--space-m); }
.ledger > div {
  border-top: 1px solid color-mix(in srgb, var(--wave) 40%, var(--navy)); padding-top: var(--space-2xs);
}
.band--sheet .ledger > div { border-top-color: color-mix(in srgb, var(--muted) 35%, var(--sheet)); }
.ledger dt {
  font-family: var(--font-mono); font-size: var(--size-meta);
  letter-spacing: var(--tracking-meta); text-transform: uppercase;
  color: var(--mist); margin-bottom: var(--space-2xs);
}
.band--sheet .ledger dt { color: var(--muted); }


/* ------------------------------------------------------------- closer -- */

/* One line and the number. The full bid panel is the conversion point on
   Contact and in the home hero; repeating it on all six pages spent a screen
   each time and read as a template rather than a system. Every other page
   closes on a line about what that page just showed, with the number as the
   loudest object in the band, which is what CONTENT-STANDARDS.md section 5
   asks of a conversion point. */
.closer .wrap {
  display: flex; flex-wrap: wrap;
  align-items: baseline; justify-content: space-between;
  gap: var(--space-s) var(--space-l);
}
.closer__line {
  font-size: var(--size-standfirst); line-height: var(--leading-standfirst);
  max-width: 34ch;
}
.closer__tel {
  display: inline-flex; align-items: center; min-height: var(--target-min);
  font-family: var(--font-display); font-weight: var(--weight-bold);
  font-size: var(--size-h2); letter-spacing: -0.02em; line-height: 1;
  color: var(--legend); text-decoration: none;   /* 10.75:1 on --navy-deep */
}
.closer__tel:hover, .closer__tel:focus-visible {
  text-decoration: underline; text-decoration-thickness: 3px;
  text-underline-offset: 0.14em;
}


/* ----------------------------------------------------------- coverage -- */

/* Real county boundaries, from US Census cartographic boundary files
   (cb_2023_us_county_500k, public domain, retrieved 2026-08-01), projected and
   emitted as inline SVG. The cb_ series is clipped to the shoreline, which is
   why Lake Erie is a shape here: TIGER/Line legal boundaries run the lakeshore
   counties out into the water to the state line, and a map built from those
   has no coast in it.

   Inline, so no third party, no iframe, and no CSP directive opened. A maps
   embed would need frame-src, script-src, connect-src and img-src all widened,
   and PRODUCTION-STANDARDS.md section 5 requires each third-party script to be
   justified individually. */
/* A graphic, not prose: the prose measure was never the right cap for it. */
.coverage { margin-top: var(--space-l); max-width: var(--measure-lead); }
.coverage__fig { width: 100%; height: auto; display: block; }

.coverage__water { fill: color-mix(in srgb, var(--wave) 22%, var(--sheet)); }

/* Context counties: enough tone to read as land against the water, not enough
   to compete with the six. */
.coverage__ctx path {
  fill: color-mix(in srgb, var(--navy) 7%, var(--sheet));
  stroke: color-mix(in srgb, var(--navy) 22%, var(--sheet));
  stroke-width: 1.4;
}
/* The six, in Merit's brand navy. Hairline --sheet strokes keep them separable
   as counties rather than fusing into one blob. */
.coverage__cov path { fill: var(--navy); stroke: var(--sheet); stroke-width: 1.6; }
.coverage__state path {
  fill: none;
  stroke: color-mix(in srgb, var(--navy) 55%, var(--sheet));
  stroke-width: 3; stroke-linejoin: round;
}

/* Names sit on the navy fill, so --sheet. Same pairing as body text on the
   dark bands. */
.coverage__n text {
  fill: var(--sheet);
  font-family: var(--font-display); font-weight: var(--weight-bold);
  font-size: 26px; text-anchor: middle;
}
/* --legend on --navy, and only there. As letterforms on the light ground it is
   1.37:1, the standing constraint at the top of this file. */
.coverage__pin { fill: var(--legend); stroke: var(--navy-deep); stroke-width: 3; }
.coverage__pin-n {
  fill: var(--legend);
  font-family: var(--font-mono); font-weight: var(--weight-medium);
  font-size: 24px; text-anchor: start; letter-spacing: 0.04em;
}
.coverage__water-n {
  fill: color-mix(in srgb, var(--navy) 55%, var(--sheet));
  font-family: var(--font-mono); font-weight: var(--weight-medium);
  font-size: 22px; letter-spacing: var(--tracking-meta);
  text-transform: uppercase; text-anchor: end;
}

/* Standalone link in an address block, so it is a target, not inline text. */
.directions {
  display: inline-flex; align-items: center;
  min-height: var(--target-min);
  margin-top: var(--space-xs);
}


/* ------------------------------------------------------- past clients -- */

/* Merit's own past-client sheets, one per sector, reproduced as they publish
   them: their site serves each as a PDF behind an embedded viewer, and these
   are page one of each rendered to an image.

   Run wide rather than at --measure. They are dense logo walls, and a reader
   checking whether their building type is on the list needs to be able to read
   the names; held to the text measure they would be unreadable. CLAUDE.md
   section 5, the strongest evidence on the page is the largest thing on it. */
.client-set { margin-top: var(--space-xl); }
.client-set:first-of-type { margin-top: var(--space-l); }
.client-set h2 { margin-bottom: var(--space-s); }
.client-set img {
  width: 100%; height: auto; display: block;
  background: #fff;
  border: 1px solid color-mix(in srgb, var(--navy) 20%, var(--sheet));
}


/* ------------------------------------------------------- affiliations -- */

/* Merit's five association marks, from their own Affiliations page.

   White chips, one per mark, for the same reason their logo needed one and
   unlike their logo it is not optional here: these are five THIRD-PARTY marks
   with five different backgrounds - BBB is black and teal, UA is black, PITC is
   white and green - and knocking any of them out would be five unauthorised
   derivatives rather than one. The chip is the only treatment that puts them on
   a navy band without altering artwork nobody has licensed to this build.

   Widths differ because the marks differ. Forcing a common width would either
   distort them or letterbox them; a shared height and shared padding is what
   makes the row read as one object. */
.affil {
  display: flex; flex-wrap: wrap; align-items: stretch;
  gap: var(--space-s);
  margin-top: var(--space-l);
}
.affil li { display: flex; }
/* The chip IS the link, as it is on Merit's own affiliations page. The whole
   chip is the target rather than just the artwork, so it clears 44px without a
   separate hit area, and the focus ring lands on the chip edge where it reads
   against the white. */
.affil a {
  background: #fff;
  padding: var(--space-2xs) var(--space-s);
  display: flex; align-items: center;
  text-decoration: none;
}
/* height with width:auto so each mark keeps its exact intrinsic ratio.
   64, not 48: the MCA of N.W. Ohio and Piping Industry Training Center marks
   both carry a second line of small type, and at 48 it was unreadable. */
.affil img { height: 64px; width: auto; display: block; }


/* ------------------------------------------------------ hours, rating -- */

.hours { margin-top: var(--space-s); font-size: var(--size-small); }

/* The rating is a fact about their Google profile, not a review reproduced from
   it. CONTENT-STANDARDS.md section 4: where republication rights are uncertain,
   link to the profile. The link also handles the thing a static page cannot,
   which is that the number moves. */
.rating {
  margin-top: var(--space-2xs);
  font-size: var(--size-small); color: var(--mist);
}
.band--sheet .rating { color: var(--muted); }
.rating strong {
  font-family: var(--font-display); font-weight: var(--weight-bold);
  font-size: var(--size-h3); color: var(--legend);   /* on --navy-deep only */
  margin-right: 0.15em;
}
.rating a {
  display: inline-flex; align-items: center;
  min-height: var(--target-min);
}


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

/* Bid invitation form. Lives on a --sheet band, so every control is
   --navy-deep on white and every border clears 3:1 against its own ground,
   which is what WCAG asks of a meaningful UI boundary rather than the 4.5:1
   that applies to the text inside it.

   TWO RULES, both from CLAUDE.md section 5.

   A field is as wide as what it holds. A phone number is not as wide as an
   email address and neither is as wide as a project name; equal-width boxes are
   the default HTML form showing through, and they make the eye measure every
   field instead of reading down the labels.

   Fields are grouped, and the groups are real <fieldset>/<legend>, so the
   grouping a sighted reader gets from the rules and spacing is the same
   grouping a screen reader announces on entering each field. */

.form { margin-top: var(--space-l); max-width: 48rem; }

.fieldset {
  margin: 0 0 var(--space-xl);
  padding: var(--space-m) 0 0;
  border: 0; border-top: 2px solid color-mix(in srgb, var(--navy) 30%, var(--sheet));
}
.fieldset legend {
  padding: 0 var(--space-xs) 0 0;
  font-family: var(--font-display); font-weight: var(--weight-bold);
  font-size: var(--size-h3); color: var(--navy-deep);
}

/* Sized to content, wrapping when the row runs out. */
.fields { display: flex; flex-wrap: wrap; gap: var(--space-m) var(--space-l); }
.field { flex: 1 1 100%; min-width: 0; }
@media (min-width: 44em) {
  .field--name    { flex: 0 1 19rem; }
  .field--company { flex: 1 1 22rem; }
  .field--email   { flex: 1 1 22rem; }
  .field--phone   { flex: 0 1 12rem; }
  .field--project { flex: 1 1 30rem; }
  .field--date    { flex: 0 1 11rem; }
  .field--full    { flex: 1 1 100%; }
}

/* ONE TYPE SYSTEM for the three kinds of text around a control:
     label  - mono, uppercase, meta size. It names data, like every other label
              on this site
     hint   - body face, small, --muted. It is a sentence, so it is set as one
     error  - the same size and face as the hint, differing only by weight,
              colour and the --legend rule. A reader tells them apart by what
              they say and where they sit, not by a change of typeface */
.field label {
  display: block; margin-bottom: var(--space-3xs);
  font-family: var(--font-mono); font-weight: var(--weight-medium);
  font-size: var(--size-meta); letter-spacing: var(--tracking-meta);
  text-transform: uppercase; color: var(--navy-deep);
}
/* aria-hidden and decorative; `required` and aria-required carry it for
   assistive technology, so this is never read out as "star". */
.req { color: var(--navy-deep); }

.form input, .form textarea {
  width: 100%; min-height: var(--target-min);
  padding: var(--space-2xs) var(--space-xs);
  background: #fff; color: var(--navy-deep);
  border: 2px solid color-mix(in srgb, var(--navy) 45%, var(--sheet));   /* 3.2:1 on --sheet */
  font-family: var(--font-body); font-size: var(--size-body);
}
.form textarea { resize: vertical; line-height: var(--leading-body); }
.form input:hover, .form textarea:hover { border-color: var(--navy); }

/* Invalid state carries a border, a heavier weight AND a text message under the
   field. Colour is never the only signal. PRODUCTION-STANDARDS.md section 1. */
.form .is-invalid { border-color: var(--navy-deep); border-width: 3px; }

.field__error {
  margin-top: var(--space-3xs);
  font-size: var(--size-small); font-weight: var(--weight-medium);
  color: var(--navy-deep);
  border-left: 3px solid var(--legend); padding-left: var(--space-2xs);
}
.field__hint {
  margin-top: var(--space-3xs);
  font-size: var(--size-small); color: var(--muted);   /* 5.26:1 on --sheet */
}

/* A REAL file input, restyled. ::file-selector-button dresses the browser's own
   control rather than hiding it behind a label, so keyboard activation, the
   file picker, the chosen-filename readout and assistive-technology support are
   all the native ones. */
.file {
  padding: var(--space-3xs);
  font-size: var(--size-small);
  cursor: pointer;
}
.file::file-selector-button {
  margin-right: var(--space-s);
  min-height: 40px; padding: 0 var(--space-m);
  background: var(--navy-deep); color: var(--sheet);
  border: 0;
  font-family: var(--font-mono); font-weight: var(--weight-medium);
  font-size: var(--size-meta); letter-spacing: var(--tracking-meta);
  text-transform: uppercase; cursor: pointer;
}
.file:hover::file-selector-button { background: var(--navy); }

/* Error summary. Takes focus on a failed submit, so it is the first thing read
   and the user keeps their place instead of being dropped at the top. */
.form__summary {
  margin-bottom: var(--space-m); padding: var(--space-s) var(--space-m);
  background: var(--legend); color: var(--navy-deep);   /* 8.81:1 */
  border-left: 6px solid var(--navy-deep);
}
.form__summary ul { margin-top: var(--space-2xs); }
.form__summary li + li { margin-top: var(--space-3xs); }
.form__summary a {
  color: var(--navy-deep); text-decoration-color: currentColor;
  display: inline-flex; align-items: center; min-height: var(--target-min);
}

.btn {
  display: inline-flex; align-items: center; justify-content: center;
  min-height: var(--target-min); margin-top: var(--space-l);
  padding: var(--space-2xs) var(--space-l);
  background: var(--navy-deep); color: var(--sheet);   /* 12.6:1 */
  border: 2px solid var(--navy-deep);
  font-family: var(--font-display); font-weight: var(--weight-bold);
  font-size: var(--size-h3); letter-spacing: -0.01em;
  text-decoration: none; cursor: pointer;
}
.btn:hover { background: var(--navy); border-color: var(--navy); }
.btn[disabled] { opacity: 0.55; cursor: progress; }

/* Inside the bid panel the ground is already --legend, so the button inverts
   to the dark fill rather than competing with its own background. */
.btn--on-legend { margin-top: var(--space-m); }

.form__status {
  margin-top: var(--space-s);
  font-size: var(--size-small); font-weight: var(--weight-medium);
  color: var(--navy-deep);
}
.form__status:empty { display: none; }

.form__privacy { margin-top: var(--space-m); color: var(--muted); }

/* NOT CONNECTED. Deliberately not styled like the success panel: --sheet on
   --navy-deep rather than dark-on-yellow, so a reviewer cannot mistake it for a
   sent confirmation at a glance. It replaces the form and takes focus. */
.form__notice {
  margin-top: var(--space-l); max-width: 44rem;
  padding: var(--space-l);
  background: var(--navy-deep); color: var(--sheet);   /* 12.6:1 */
  border-left: 6px solid var(--legend);
}
.form__notice h3 {
  font-family: var(--font-display); font-weight: var(--weight-bold);
  font-size: var(--size-h2); margin-bottom: var(--space-2xs);
}
.form__notice p + p { margin-top: var(--space-s); }
.form__notice a { color: var(--legend); }              /* 10.4:1 on --navy-deep */
.form__notice code {
  font-family: var(--font-mono); font-size: 0.9em;
}

/* Success replaces the form and takes focus. */
.form__done {
  margin-top: var(--space-l); max-width: 44rem;
  padding: var(--space-l);
  background: var(--legend); color: var(--navy-deep);
  border-left: 6px solid var(--navy-deep);
}
.form__done h3 {
  font-family: var(--font-display); font-weight: var(--weight-bold);
  font-size: var(--size-h2); margin-bottom: var(--space-2xs);
}
.form__done a { color: var(--navy-deep); text-decoration-color: currentColor; }

/* Honeypot. Offscreen rather than display:none, which some bots detect and
   skip. Out of the tab order and hidden from assistive technology, so only a
   scripted filler ever reaches it. */
.hp {
  position: absolute; width: 1px; height: 1px;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}


/* The hero goes straight into the form, so the only thing it carries besides the
   headline is the number, for anyone who would rather talk than type. */
.hero__call {
  margin-top: var(--space-m);
  font-size: var(--size-standfirst); color: var(--mist);
}
.hero__call a {
  display: inline-flex; align-items: center; min-height: var(--target-min);
  font-family: var(--font-display); font-weight: var(--weight-bold);
  letter-spacing: -0.02em;
}

/* -------------------------------------------------- contact two columns -- */

/* A contact page has two things to say and they are independent: how to send a
   bid, and where the office is and what it covers. Stacked, each ran at the
   form's measure and left the right of the page empty from the header down.
   Side by side they use the measure and the page halves in height. */
.contact-cols { display: grid; gap: var(--space-xl); }

@media (min-width: 68em) {
  .contact-cols {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    column-gap: var(--space-2xl);
    align-items: start;
  }
  /* The form no longer needs its own cap: the column is the measure. */
  .contact-cols__main .form { max-width: none; }
  /* Nor does the map. --measure held it at 34rem, which is why it read as a
     thumbnail beside a full-height form; in its own column it fills it. */
  .contact-cols__aside .coverage { max-width: none; }
  .contact-cols__aside { position: sticky; top: var(--space-l); }
}

.contact-cols__aside .coverage { margin-top: var(--space-l); }

/* --------------------------------------------------------------- footer -- */

.site-foot { border-top: 1px solid color-mix(in srgb, var(--wave) 40%, var(--navy)); padding-block: var(--space-xl); }
.site-foot .prose > * + * { margin-top: var(--space-s); }
.foot-grid { display: grid; gap: var(--space-l); }
.foot-grid h2 {
  font-size: var(--size-meta); font-family: var(--font-mono);
  font-weight: var(--weight-medium); letter-spacing: var(--tracking-meta);
  text-transform: uppercase; color: var(--mist);
  margin-bottom: var(--space-2xs);
}
.site-foot address { font-style: normal; }

/* ---------------------------------------------------------------- focus -- */

:focus-visible { outline: var(--focus-width) solid var(--legend); outline-offset: 2px; }
.band--sheet :focus-visible, .concept-notice :focus-visible, .bid :focus-visible {
  outline-color: var(--navy-deep);
}

/* Fragment-navigation targets carry tabindex="-1" so activating an in-page link
   moves reading position into the section. They are never in the tab order and
   are not interactive controls; Chrome matches :focus-visible on them anyway,
   which would draw a rule around an entire section and read as a rendering
   fault. The designed replacement is the navigation itself, the section is
   scrolled into view and, being a labelled region, announced on arrival.
   Real controls cannot be reached by this selector. */
main[tabindex="-1"]:focus,
section[tabindex="-1"]:focus { outline: none; }

/* ------------------------------------------------------------- movement --
   Extended tier permits one orchestrated decorative moment. It is the home
   hero only: the marker band wipes in as a marker is applied to a pipe, and
   the headline follows in a short staggered rise. Nothing else on any page
   animates on entry.

   Every rule lives INSIDE the no-preference query, so the reduced-motion
   default is the finished state, content is never hidden waiting for an
   animation that will not run. */
@media (prefers-reduced-motion: no-preference) {
  a, .navlist a, .wordmark {
    transition: color var(--duration-fast) var(--easing),
                text-decoration-thickness var(--duration-fast) var(--easing);
  }

  .anim-rise { animation: rise var(--duration-slow) var(--easing-out) both; }
  .anim-rise--2 { animation-delay: 110ms; }
  .anim-rise--3 { animation-delay: 200ms; }
}

@keyframes rise {
  from { opacity: 0; transform: translateY(0.6rem); }
  to { opacity: 1; transform: none; }
}

/* ------------------------------------------------------- forced colors --
   In Windows High Contrast the browser discards background colors, so the
   solid --legend panels lose their fill. Text stays readable, but the bid
   panel's entire visual job is being the loudest object on the page, so it is
   given an explicit border to keep an identifiable boundary when the fill goes.
   The same applies to the concept notice, which is a legal disclosure sitting
   on a yellow band. */
@media (forced-colors: active) {
  .bid, .concept-notice { border: 2px solid CanvasText; }
  .wordmark, .affil li { border: 1px solid CanvasText; }
  .navlist a[aria-current="page"] { border-bottom-color: Highlight; }
  .bid__list li::before { display: none; }
  .bid__list li { padding-left: 0; }
}

/* ------------------------------------------------------------ responsive -- */

@media (min-width: 40em) {
  .scope-list { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-l); }
  .ledger { grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: var(--space-xl); }
  .foot-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (min-width: 64em) {
  .scope-list { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .ledger { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .foot-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

@media print {
  body { background: #fff; color: #000; }
  .band--sheet.band--deep { background: none; color: #000; }
  .concept-notice { border: 2px solid #000; }
}


/* =============================================================== MOBILE ==
   CLAUDE.md section 7. Mobile is a designed layout, not a narrower copy. Six
   things on this build fail a scale-down and get a different component rather
   than a smaller one. Each is recorded in DIRECTION.md. */

/* ---- 1. Display type gets its own scale ----------------------------------
   The desktop ratios do not survive the trip. --leading-display 0.95 and
   --tracking-display -0.03em are set for 80px type read across a room; at 36px
   on a phone held at arm's length they close the counters and jam the lines
   together. Mobile gets looser leading, near-neutral tracking, and a size set
   for the screen rather than taken from the bottom of the desktop clamp. */
@media (max-width: 47.99em) {
  :root {
    --size-display: 2.25rem;        /* 36px */
    --leading-display: 1.04;
    --tracking-display: -0.015em;

    --size-display-long: 2rem;      /* 32px */

    --size-h2: 1.5rem;              /* 24px */
    --leading-h2: 1.2;
    --tracking-h2: -0.005em;

    --size-standfirst: 1.1875rem;   /* 19px */
    --leading-standfirst: 1.45;
  }
}

/* ---- 2. The county map: a different crop, not a smaller one --------------
   The wide map is 1000 x 593. At 360px its county names render around 9px and
   the six counties are a third of the frame. The narrow variant is a separate
   SVG on a tighter extent: less surrounding context, labels at nearly twice the
   relative size. Only one is in the accessibility tree at a time, so a screen
   reader never meets the same map twice. */
.coverage__fig--narrow { display: block; }
.coverage__fig--wide { display: none; }
@media (min-width: 48em) {
  .coverage__fig--narrow { display: none; }
  .coverage__fig--wide { display: block; }
}

/* ---- 3. Past-client sheets: a document, not an image ---------------------
   Dense logo walls, 2000px wide, several hundred names. Scaled to 360 they are
   unreadable, and an unreadable image is worse than no image because it looks
   like the page is broken. Below 48em the sheet becomes what it actually is: a
   document you open. The sector heading carries the information; the link opens
   the sheet full size.

   The production answer is different again, and it is recorded in HANDOFF.md:
   replace the sheets with the client names as text, which reads at every width
   and carries the argument without reproducing several hundred third-party
   marks. */
/* The count published on the sheet itself, as text. On a phone the sheet is a
   link rather than an image, so without this the sector would be a heading and
   nothing else; on desktop it surfaces a number that is set in 9px on the
   artwork and easy to miss. */
.client-set__count {
  margin-top: var(--space-3xs);
  font-family: var(--font-display); font-weight: var(--weight-bold);
  font-size: var(--size-h3); color: var(--navy);
}

.client-set__open {
  display: inline-flex; align-items: center;
  min-height: var(--target-min);
  margin-top: var(--space-2xs);
  font-size: var(--size-small);
}
@media (max-width: 47.99em) {
  .client-set img { display: none; }
}
@media (min-width: 48em) {
  .client-set__open { display: none; }
}

/* ---- 4. Carousel: one-up with swipe --------------------------------------
   The desktop peek shows 12.5% of each neighbour, which at 360 is 45px: too
   little to read as a card and enough to read as a rendering fault. On a phone
   the card takes the full width and the neighbours are off-screen entirely,
   which is what "cropped at the viewport edge looks intentional" means here.
   Momentum swipe is the browser's own scroll-snap, so it needs nothing added. */
@media (max-width: 47.99em) {
  .is-enhanced .proj-grid { padding-inline: 0; gap: var(--space-s); }
  .proj-carousel { padding-inline: 0; }
  .car-arrow { display: none; }   /* swipe is the control; arrows need a cursor */
}

/* ---- 5. Thumb reach -------------------------------------------------------
   CLAUDE.md section 7: a conversion point at the top of a long phone page is a
   conversion point nobody taps. The closer already puts the number at the foot
   of every page; on mobile it gets the full width so it is a thumb target
   rather than a line of text. */
@media (max-width: 47.99em) {
  .closer .wrap { flex-direction: column; align-items: flex-start; }
  .closer__tel { font-size: var(--size-h2); }
}
