/* ==========================================================================
   D&A Contractors — Master stylesheet
   Single-file, zero-dependency CSS. Handcrafted with CSS variables,
   Flexbox and Grid. Organised top-to-bottom by component.

   CONTENTS
   01. Design tokens
   02. Reset & base
   03. Typography & helpers
   04. Buttons & badges
   05. Layout (container / section)
   06. Top utility bar
   07. Sticky header & navigation
   08. Mobile drawer
   09. Hero
   10. Trust bar
   11. Service cards
   12. Feature list
   13. Reviews
   14. Quote form
   15. CTA band
   16. Footer
   17. Utilities
   18. Responsive breakpoints
   19. Component additions (breadcrumbs, footer NAP)
   20. Case study (before/after pair, job details)
   21. Share block (case study)
   22. Consent banner (cookies and analytics)

   --------------------------------------------------------------------------
   RESTYLE — "Well-Laid", 18 September 2026
   --------------------------------------------------------------------------
   This is a reskin. The markup, element IDs, data-* hooks and <head> of every
   page are untouched; only the presentation below changed. The previous sheet
   is archived at _archive/css-baseline-2026-09-18/style.css and can be restored
   with a single file copy.

   What changed, and why:
   - Interface red moved from the spec's #CE0000 to #EE1C25, the red actually
     in the logo, on the client's decision. #EE1C25 then turned out to fail
     WCAG AA for normal text (4.35:1 with white, 3.9:1 on bone), so the
     interface red is now #C4161D while the logo red stays on artwork. See the
     note beside --color-primary for the measurements.
   - The palette is now warm. The old sheet paired a cool blue-grey (#F4F5F7 /
     #E2E8F0 / #4A5568) with warm buff-and-charcoal paving photography, which
     read clinical next to the photos. Bone, sand and warm ink sit correctly
     beside them.
   - Three greys that were hardcoded in component rules — #C9CDD4, #B9BEC7 and
     #A8AEb8 — are now tokens (--color-text-on-dark and friends). They were the
     only colour values outside :root and had already drifted apart.
   - Shadows are warmer, softer and layered in two parts (a contact edge plus an
     ambient wash) instead of one neutral blur.
   - Motion is per-property on one easing curve rather than `all .2s ease-in-out`,
     so hovering a card no longer animates properties that cannot change.
   - Type and spacing got an explicit scale. Headings were already 800 weight;
     they are now 700 with tighter tracking, which reads more crafted and less
     shouty at display sizes.
   ========================================================================== */

/* ==========================================================================
   01. DESIGN TOKENS
   ========================================================================== */
:root {
  /* --- Brand palette ------------------------------------------------------ */
  /* NOTE ON THE RED, and why there are two of them. The logo's red is
     #EE1C25. It is kept for artwork (the icon set, the logo files, the
     browser's theme-color) and for thin decorative strokes, because
     tools/make_icons.py reads this value and fails if the icon set, this
     stylesheet and tools/site-facts.json disagree. Do not repoint it.

     It cannot be used for interface text. White on #EE1C25 is 4.35:1 and it
     is 3.9:1 on the bone background, both under the 4.5:1 AA requires for
     normal text. A 1rem/700 button is normal text: WCAG defines large text as
     18.66px bold or 24px regular, and every button here is 16px or smaller.
     An earlier version of this note claimed 1rem counted as large, which is
     how the buttons shipped at 4.35:1 and cost the site its accessibility
     score. --color-primary-deep is therefore the UI red: 6.0:1 with white and
     5.4:1 on bone. Every rule that paints red text, or paints a red field
     carrying white text, uses it; --color-primary is never text. */
  --color-primary: #EE1C25;        /* Logo red — artwork only, never text */
  --color-primary-deep: #C4161D;   /* UI red — 6.0:1 on white, 5.4:1 on bone */
  --color-primary-hover: #A80D14;  /* Pressed/hover UI red — 7.7:1 with white */
  --color-primary-soft: #FDF0F0;   /* 4% red wash — focus rings, tinted panels */

  /* Ink and surfaces. Warm neutrals, not blue-greys. */
  --color-dark: #16130F;           /* Warm near-black — headings, dark bands */
  --color-dark-surface: #211D18;   /* Raised dark surface (trust bar) */
  --color-dark-border: #322C25;    /* Hairline on dark surfaces */
  --color-logo-black: #000000;     /* The supplied logo's exact black; header bar */
  --color-white: #FFFFFF;          /* Crisp white */
  --color-gray-light: #F6F2EC;     /* Bone — section background (was #F4F5F7) */
  --color-gray-border: #E6DFD5;    /* Warm hairline (was #E2E8F0) */
  --color-gray-text: #4B4741;      /* Warm body text — 9.4:1 on bone */

  /* Text on dark surfaces. These replace three hardcoded greys. */
  --color-text-on-dark: #C6BFB5;   /* Body copy on ink — 9.7:1 */
  --color-text-on-dark-soft: #9C948A; /* Secondary/footer links — 5.3:1 */
  --color-text-on-dark-mute: #8C8479; /* Legal + micro text — AA even at 13.6px */

  --color-star-gold: #E0A106;      /* Review gold — darkened for light surfaces */
  --color-badge-bg: #FBF4E4;       /* Warm gold pill */
  --color-badge-border: #EFE1C2;   /* Pill hairline */

  /* Typography. System stack only — the project is strictly zero-dependency,
     so type character comes from scale, weight, case and tracking, not from a
     webfont. */
  --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --font-heading: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;

  /* Spacing scale — a 4px ramp. Existing rem literals in the component rules
     still resolve to the same steps; this just names them.
     The ramp is defined in full so future work has one place to reach for a
     step, but the component rules below still use rem literals: rewriting ~200
     declarations to point at these would be a large diff with no visual change,
     and it would have to be re-verified on all 10 pages. Deliberate, not an
     oversight. */
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-5: 1.25rem;
  --space-6: 1.5rem;
  --space-8: 2rem;
  --space-10: 2.5rem;
  --space-12: 3rem;

  /* Layout */
  --max-width: 1200px;
  --max-width-narrow: 800px;
  --measure: 68ch;                 /* Reading measure — used by .lead, .hero__sub */

  /* Radius — slightly softer than before, still restrained. */
  --border-radius-xs: 3px;
  --border-radius-sm: 6px;
  --border-radius-md: 8px;
  --border-radius-lg: 14px;

  /* Shadow ladder. Each step is a 1px contact edge plus an ambient wash, and
     both are tinted with warm ink rather than neutral black so they sit on the
     bone backgrounds without turning grey. */
  --shadow-sm: 0 1px 2px rgba(22, 19, 15, 0.05), 0 1px 3px rgba(22, 19, 15, 0.06);
  --shadow-md: 0 2px 4px rgba(22, 19, 15, 0.05), 0 6px 16px rgba(22, 19, 15, 0.07);
  --shadow-lg: 0 4px 8px rgba(22, 19, 15, 0.06), 0 14px 34px rgba(22, 19, 15, 0.10);

  /* Motion. --transition is kept for colour-only changes; interactive elements
     name their own properties so they never animate layout. --ease-out is a
     soft decelerating curve used for lifts and reveals. */
  --transition: 200ms cubic-bezier(0.2, 0, 0.2, 1);
  --ease-out: cubic-bezier(0.32, 0.72, 0, 1);
}

/* ==========================================================================
   02. RESET & BASE
   ========================================================================== */
*,
*::before,
*::after {
  box-sizing: border-box;
}

html {
  scroll-behavior: smooth;
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  font-family: var(--font-sans);
  font-size: 17px;
  line-height: 1.65;
  color: var(--color-gray-text);
  background: var(--color-white);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

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

a {
  color: var(--color-primary-deep);
  text-decoration: none;
  transition: color var(--transition);
}

a:hover {
  color: var(--color-primary-hover);
}

/* A link inside running text is underlined. Red against body ink is only
   1.5:1, so colour alone cannot tell it apart from the words around it
   (WCAG 1.4.1, and Lighthouse's link-in-text-block). Buttons and card links
   are excluded: they are already distinguishable by shape or weight. */
p a:not(.btn) {
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.18em;
}

ul {
  margin: 0;
  padding: 0;
  list-style: none;
}

/* Respect users who prefer reduced motion */
@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }
  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    transition-duration: 0.001ms !important;
  }
}

/* ==========================================================================
   03. TYPOGRAPHY & HELPERS
   ========================================================================== */
h1,
h2,
h3,
h4 {
  font-family: var(--font-heading);
  color: var(--color-dark);
  line-height: 1.14;
  margin: 0 0 0.5em;
  font-weight: 700;
  letter-spacing: -0.02em;
  text-wrap: balance;              /* avoids one-word last lines in headlines */
}

h1 { font-size: clamp(2.15rem, 5vw, 3.5rem); }
h2 { font-size: clamp(1.65rem, 3.5vw, 2.4rem); }
h3 { font-size: 1.25rem; letter-spacing: -0.01em; }
h4 { font-size: 1.05rem; letter-spacing: -0.01em; }

p { margin: 0 0 1rem; }

/* NOTE: no global max-width on <p>. It would silently clamp buttons and other
   inline-flex children that sit inside paragraph wrappers on the landing pages,
   and the readability win it buys is already had by .lead and .hero__sub below,
   which are the only paragraphs long enough to need a measure. */

.eyebrow {
  display: inline-block;
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--color-primary-deep);
  margin-bottom: 0.75rem;
}

.lead {
  font-size: 1.18rem;
  line-height: 1.6;
  color: var(--color-gray-text);
  max-width: var(--measure);
}

.text-center { text-align: center; }
.text-center .lead { margin-left: auto; margin-right: auto; }

/* Skip link for keyboard users */
.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 200;
  background: var(--color-primary-deep);
  color: var(--color-white);
  padding: 0.75rem 1.25rem;
  border-radius: 0 0 var(--border-radius-md) 0;
}

.skip-link:focus {
  left: 0;
  color: var(--color-white);
}

/* Visible focus ring for accessibility. Two rings — a white halo then the red —
   so it stays visible against both the white page and the red CTA band. */
:focus-visible {
  outline: 3px solid var(--color-primary);
  outline-offset: 2px;
  box-shadow: 0 0 0 6px rgba(255, 255, 255, 0.85);
}

/* ==========================================================================
   04. BUTTONS & BADGES
   ========================================================================== */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  font-family: var(--font-heading);
  font-size: 1rem;
  font-weight: 700;
  line-height: 1;
  letter-spacing: -0.005em;
  padding: 0.95rem 1.6rem;
  border: 2px solid transparent;
  border-radius: var(--border-radius-md);
  cursor: pointer;
  text-align: center;
  transition: background-color var(--transition),
              border-color var(--transition),
              color var(--transition),
              box-shadow var(--transition),
              transform 180ms var(--ease-out);
  white-space: nowrap;
}

.btn-primary {
  background: var(--color-primary-deep);
  color: var(--color-white);
  box-shadow: var(--shadow-sm);
}

.btn-primary:hover {
  background: var(--color-primary-hover);
  color: var(--color-white);
  transform: translateY(-1px);
  box-shadow: var(--shadow-md);
}

.btn-primary:active {
  transform: translateY(0);
  box-shadow: var(--shadow-sm);
}

.btn-secondary {
  background: var(--color-dark);
  color: var(--color-white);
}

.btn-secondary:hover {
  background: #000;
  color: var(--color-white);
  transform: translateY(-1px);
}

.btn-outline {
  background: transparent;
  color: var(--color-dark);
  border-color: var(--color-dark);
}

.btn-outline:hover {
  background: var(--color-dark);
  color: var(--color-white);
}

/* Sits on the red CTA band — white pill, ink label. */
.btn-light {
  background: var(--color-white);
  color: var(--color-dark);
}

.btn-light:hover {
  background: var(--color-gray-light);
  color: var(--color-dark);
  transform: translateY(-1px);
}

.btn-block { width: 100%; }
.btn-lg { padding: 1.1rem 2rem; font-size: 1.05rem; }

/* Google review CTA — uses Google's brand blue so it reads as a third-party
   action rather than a site action. */
.btn-google {
  /* #4285F4 is Google's blue but white on it is only 3.56:1, so the button
     shipped below AA. #1967D2 keeps the Google cue at 5.4:1; the hover goes
     one step darker again. */
  background: #1967D2;
  color: var(--color-white);
  box-shadow: var(--shadow-sm);
}

.btn-google:hover {
  background: #0B57D0;
  color: var(--color-white);
  transform: translateY(-1px);
  box-shadow: var(--shadow-md);
}

/* Star rating pills */
.badge-rating {
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  background: var(--color-badge-bg);
  color: var(--color-dark);
  border: 1px solid var(--color-badge-border);
  border-radius: 999px;
  padding: 0.5rem 1rem;
  font-size: 0.9rem;
  font-weight: 600;
}

.badge-rating .stars { color: var(--color-star-gold); letter-spacing: 1px; }

.pill {
  display: inline-block;
  background: var(--color-gray-light);
  border: 1px solid var(--color-gray-border);
  border-radius: 999px;
  padding: 0.3rem 0.8rem;
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--color-gray-text);
}

/* ==========================================================================
   05. LAYOUT
   ========================================================================== */
.container {
  width: 100%;
  max-width: var(--max-width);
  margin: 0 auto;
  padding: 0 1.25rem;
}

.container-narrow {
  max-width: var(--max-width-narrow);
}

/* Wider vertical rhythm than before — sections now breathe at up to 7rem,
   which is what separates a "tidy" page from a considered one. */
.section {
  padding: clamp(3.75rem, 8vw, 7rem) 0;
}

.section--alt { background: var(--color-gray-light); }
.section--dark {
  background: var(--color-dark);
  color: var(--color-text-on-dark);
}
.section--dark h2,
.section--dark h3 { color: var(--color-white); }

.section__head {
  max-width: 720px;
  margin: 0 auto 3rem;
  text-align: center;
}

.section__head p { margin-bottom: 0; }

/* ==========================================================================
   06. TOP UTILITY BAR
   ========================================================================== */
.topbar {
  /* Same #000 as the logo and the header, so the utility bar and the header
     read as one black top area rather than two near-blacks. */
  background: var(--color-logo-black);
  color: var(--color-text-on-dark-soft);
  font-size: 0.85rem;
}

.topbar__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  min-height: 40px;
  flex-wrap: wrap;
}

.topbar a { color: var(--color-text-on-dark-soft); }
.topbar a:hover { color: var(--color-white); }

.topbar__group {
  display: flex;
  align-items: center;
  gap: 1.25rem;
}

.topbar__rating { color: var(--color-star-gold); font-weight: 700; }

/* ==========================================================================
   07. STICKY HEADER & NAVIGATION
   ========================================================================== */
.site-header {
  position: sticky;
  top: 0;
  z-index: 100;
  /* The supplied logo's rectangle is #000. Matching it exactly means the
     black-background logo has no visible tile edge in the header. */
  background: var(--color-logo-black);
  border-bottom: 1px solid var(--color-dark-border);
  box-shadow: var(--shadow-sm);
}

.site-header__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1.5rem;
  min-height: 76px;
}

.brand {
  display: inline-flex;
  align-items: center;
  flex-shrink: 0;
}

.brand__logo {
  /* The client's real lockup (logo-header-black.png, 791x152). The width/height
     attributes give its true pixels so the box is reserved before it loads;
     this rule sets the size it is shown at. */
  width: 210px;
  height: auto;
}

.nav {
  display: flex;
  align-items: center;
  gap: 1.75rem;
}

.nav__link {
  color: var(--color-white);
  font-weight: 600;
  font-size: 0.98rem;
  position: relative;
  padding: 0.25rem 0;
}

.nav__link::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: -2px;
  width: 0;
  height: 2px;
  background: var(--color-primary);
  transition: width 220ms var(--ease-out);
}

/* White text, red rule. The nav label stays white in every state: 15.7px bold
   red on #000 is below the 4.5:1 normal-text threshold, while #EE1C25 makes a
   clear state marker as the underline. */
.nav__link:hover,
.nav__link[aria-current="page"] { color: var(--color-white); }

.nav__link:hover::after,
.nav__link[aria-current="page"]::after { width: 100%; }

.header__actions {
  display: flex;
  align-items: center;
  gap: 0.75rem;
}

/* Hamburger (hidden on desktop). Transparent on the black bar with white
   rules, so it reads as part of the header rather than a light button. */
.nav__toggle {
  display: none;
  flex-direction: column;
  justify-content: center;
  gap: 5px;
  width: 46px;
  height: 46px;
  padding: 0;
  background: transparent;
  border: 1px solid rgba(255, 255, 255, 0.28);
  border-radius: var(--border-radius-md);
  cursor: pointer;
}

.nav__toggle:hover { border-color: rgba(255, 255, 255, 0.6); }

.nav__toggle span {
  display: block;
  width: 22px;
  height: 2px;
  margin: 0 auto;
  background: var(--color-white);
  transition: transform 220ms var(--ease-out), opacity 140ms linear;
}

.nav__toggle[aria-expanded="true"] span:nth-child(1) { transform: translateY(7px) rotate(45deg); }
.nav__toggle[aria-expanded="true"] span:nth-child(2) { opacity: 0; }
.nav__toggle[aria-expanded="true"] span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }

/* ==========================================================================
   08. MOBILE DRAWER
   ========================================================================== */
.mobile-drawer {
  display: none;
  background: var(--color-white);
  border-bottom: 1px solid var(--color-gray-border);
}

.mobile-drawer.is-open { display: block; }

.mobile-drawer__inner {
  padding: 1rem 0 1.5rem;
}

.mobile-drawer__link {
  display: block;
  padding: 0.9rem 0;
  color: var(--color-dark);
  font-weight: 600;
  font-size: 1.1rem;
  border-bottom: 1px solid var(--color-gray-border);
}

.mobile-drawer__link:hover { color: var(--color-primary-deep); }

.mobile-drawer__cta { margin-top: 1.25rem; }

/* ==========================================================================
   09. HERO
   ========================================================================== */
.hero {
  position: relative;
  color: var(--color-white);
  overflow: hidden;
  background: var(--color-dark);
}

.hero__media {
  position: absolute;
  inset: 0;
  z-index: 0;
}

.hero__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Scrim. The ink channels are the warm --color-dark (#16130F = 22,19,15) rather
   than neutral black, so the image tints warm instead of grey. */
.hero__media::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(90deg, rgba(22, 19, 15, 0.92) 0%, rgba(22, 19, 15, 0.72) 45%, rgba(22, 19, 15, 0.35) 100%);
}

.hero__inner {
  position: relative;
  z-index: 1;
  padding-block: clamp(3.5rem, 9vw, 7rem); /* block only: keep .container's side gutter */
  max-width: 720px;
}

.hero h1 { color: var(--color-white); margin-top: 1rem; }

.hero__sub {
  font-size: 1.2rem;
  line-height: 1.6;
  color: var(--color-text-on-dark);
  max-width: 560px;
}

.hero__ctas {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
  margin-top: 2rem;
}

.hero__microcopy {
  margin-top: 1.5rem;
  font-size: 0.9rem;
  color: var(--color-text-on-dark-soft);
}

/* Compact hero used on inner landing pages */
.hero--compact .hero__inner { padding-block: clamp(2.75rem, 6vw, 4.5rem); }

/* ==========================================================================
   10. TRUST BAR
   ========================================================================== */
.trust-bar {
  background: var(--color-dark-surface);
  color: var(--color-text-on-dark);
  padding: 1.5rem 0;
}

.trust-bar__grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 1rem;
  text-align: center;
}

.trust-bar__item strong {
  display: block;
  font-family: var(--font-heading);
  font-size: 1.6rem;
  letter-spacing: -0.02em;
  color: var(--color-white);
}

.trust-bar__item span { font-size: 0.9rem; }

/* ==========================================================================
   11. SERVICE CARDS
   ========================================================================== */
.cards {
  display: grid;
  gap: 1.75rem;
}

.cards--4 { grid-template-columns: repeat(4, 1fr); }
.cards--3 { grid-template-columns: repeat(3, 1fr); }
.cards--2 { grid-template-columns: repeat(2, 1fr); }

/* The team row carries two portraits today. Capping the row keeps each card
   close to the 3-up width the headshots were cropped for (Joe's source is only
   365 px wide, so a full-width two-up would upscale it), and centres the pair
   instead of leaving space on the right that reads as an empty third column. */
.team-grid {
  max-width: 860px;
  margin-inline: auto;
}

.service-card {
  display: flex;
  flex-direction: column;
  background: var(--color-white);
  border: 1px solid var(--color-gray-border);
  border-radius: var(--border-radius-lg);
  overflow: hidden;
  box-shadow: var(--shadow-sm);
  transition: transform 260ms var(--ease-out),
              box-shadow 260ms var(--ease-out),
              border-color var(--transition);
}

/* A 2px lift, not the previous 4px — the card should feel responsive, not
   buoyant. */
.service-card:hover {
  transform: translateY(-2px);
  box-shadow: var(--shadow-lg);
  border-color: #D8CFC2;
}

.service-card__media {
  aspect-ratio: 4 / 3;
  overflow: hidden;
  background: var(--color-gray-light);
}

.service-card__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 500ms var(--ease-out);
}

.service-card:hover .service-card__media img { transform: scale(1.03); }

/* Team cards reuse .service-card, but the photos are portrait headshots rather
   than landscape project shots. The 4:3 frame comes from .service-card__media;
   here we only nudge the crop towards the top of the frame so faces are not
   cut off at the chin, and turn off the hover zoom (odd on a face). */
.team-media img { object-position: center 22%; }
.service-card:hover .team-media img { transform: none; }

/* Reviews page: Dave, Joe and a customer. A portrait photograph beside a short
   note, so the faces stay at a readable size instead of becoming another
   background under the hero copy. Stacked at 1024px, where two columns would
   squeeze the image. */
.review-team {
  display: grid;
  grid-template-columns: minmax(0, 360px) minmax(0, 1fr);
  gap: clamp(2rem, 5vw, 4rem);
  align-items: center;
}
.review-team img {
  width: 100%;
  height: auto;
  border-radius: var(--border-radius-lg);
  box-shadow: var(--shadow-md);
}
.review-team__body h2 { margin-top: 0.5rem; }
.review-team__body p:last-child { margin-bottom: 0; }

.service-card__body {
  display: flex;
  flex-direction: column;
  flex: 1;
  padding: 1.5rem;
}

.service-card__body h3 { margin-bottom: 0.5rem; }
.service-card__body p { flex: 1; }

.service-card__link {
  font-weight: 700;
  margin-top: 0.75rem;
}

/* ==========================================================================
   12. FEATURE LIST
   ========================================================================== */
.features {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 2rem;
}

.feature__icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 54px;
  height: 54px;
  margin-bottom: 1rem;
  border-radius: var(--border-radius-md);
  background: var(--color-badge-bg);
  border: 1px solid var(--color-badge-border);
  color: var(--color-primary-deep);
  font-size: 1.5rem;
  font-weight: 700;
  letter-spacing: -0.02em;
}

.feature h3 { margin-bottom: 0.4rem; }

/* Checklist (used beside the quote form) */
.list-checks { margin: 1.25rem 0; }
.list-checks li {
  position: relative;
  padding-left: 2rem;
  margin-bottom: 0.75rem;
  color: var(--color-dark);
}
.list-checks li::before {
  content: "✓";
  position: absolute;
  left: 0;
  top: 0;
  width: 1.4rem;
  height: 1.4rem;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--color-primary-deep);
  color: var(--color-white);
  border-radius: 50%;
  font-size: 0.8rem;
  font-weight: 700;
}

/* Numbered process steps */
.steps {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 2rem;
  counter-reset: step;
}
.step {
  text-align: center;
  padding: 1.5rem 1rem;
}
.step__num {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 56px;
  height: 56px;
  margin-bottom: 1rem;
  border-radius: 50%;
  background: var(--color-primary-deep);
  color: var(--color-white);
  font-family: var(--font-heading);
  font-size: 1.4rem;
  font-weight: 700;
}
.step h3 { margin-bottom: 0.4rem; }

/* Gallery */
.gallery {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 1.25rem;
}
/* The gallery frames real photographs now, and their sources are not all the
   same shape: the phone originals crop to 4:3, 16:9 and 12:5 depending on what
   had to be left out of each frame (tools/make_web_images.py documents each
   box). Without a fixed frame the grid rows would each be a different height
   and the section would read as ragged. The frame is 4:3 — what the
   placeholders were — and `object-fit: cover` trims the odd shapes to it.
   The written width/height on each <img> is the file's real size, so this is
   presentation only and nothing here is used to reserve layout space. */
.gallery img {
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  border-radius: var(--border-radius-lg);
  box-shadow: var(--shadow-md);
}

/* FAQ accordion (native <details>) */
.faq {
  background: var(--color-white);
  border: 1px solid var(--color-gray-border);
  border-radius: var(--border-radius-md);
  padding: 1rem 1.25rem;
  margin-bottom: 1rem;
}
.faq summary {
  cursor: pointer;
  font-weight: 700;
  color: var(--color-dark);
  font-family: var(--font-heading);
}
.faq summary::marker { color: var(--color-primary); }
.faq p { margin: 0.75rem 0 0; }

/* ==========================================================================
   13. REVIEWS
   ========================================================================== */
.reviews-summary {
  display: flex;
  align-items: center;
  gap: 1.5rem;
  flex-wrap: wrap;
  justify-content: center;
  margin-bottom: 2.5rem;
}

.reviews-summary__score {
  font-family: var(--font-heading);
  font-size: 1.5rem;
  font-weight: 700;
  letter-spacing: -0.02em;
  color: var(--color-dark);
}

.stars {
  color: var(--color-star-gold);
  font-size: 1.05rem;
  letter-spacing: 2px;
  unicode-bidi: plaintext;
}

.reviews-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1.5rem;
}

.review-card {
  display: flex;
  flex-direction: column;
  height: 100%;
  background: var(--color-white);
  border: 1px solid var(--color-gray-border);
  border-radius: var(--border-radius-lg);
  padding: 1.75rem;
  box-shadow: var(--shadow-sm);
}

.review-card__text {
  color: var(--color-dark);
  font-size: 1.02rem;
  line-height: 1.65;
  margin: 0.75rem 0 1.25rem;
}

/* --------------------------------------------------------------------------
   REVIEW TEXT CLAMP — "one paragraph, four lines"
   Reviews in the archive run from one sentence to a full page, and in a grid
   row a single essay stretches every card beside it to the same height. The
   featured blocks on index.html and the four service pages therefore publish
   every review verbatim but SHOW only a four-line teaser, with a Show more
   button to expand it in place.

   .is-clamped is the collapsed state and is added by main.js — never baked into
   the HTML, so a visitor without JavaScript reads every review in full and
   nothing is ever unreachable. Its height is NOT set here: main.js measures the
   line-height and sets an inline max-height of exactly four lines, so the text
   is only ever cut on a line boundary and never through the middle of a row of
   letters. The matching span, which the generator wraps the quotation in, is
   what the script measures against.

   The quotation itself is never trimmed: the words stay in the HTML word for
   word, which is what tools/check_review_claims.py verifies them against and
   what a crawler reads. Truncating the string instead would break both.
   -------------------------------------------------------------------------- */
.review-card__text.is-clamped {
  overflow: hidden;
}

/* The "..." that marks a quotation as cut off. It is a span of its own placed
   AFTER the quotation rather than a character appended to it, so the quoted
   words stay exactly the record (see the generator's render_card docstring and
   tools/check_review_claims.py). It ships hidden and main.js unhides it only on
   cards it has actually clipped — on a short review it would be a lie.

   It CANNOT simply sit inline after the words. The clamp crops the paragraph, so
   a trailing inline mark on a full last line falls outside the crop and is never
   seen — which is exactly what happened the first time. It is therefore pinned
   to the END OF THE LAST VISIBLE LINE instead, the position a reader expects a
   truncation mark to occupy. That is why the paragraph is position: relative and
   the mark is absolute: being out of flow, it cannot add a line and cannot feed
   back into the height measurement main.js makes against the wrapper span. */
.review-card__text.is-clamped { position: relative; }

.review-card__ellipsis {
  position: absolute;
  right: 0;
  bottom: 0;
  /* The white backing masks whatever the last line runs on underneath, so the
     mark never sits on top of a half-printed word. Kept small: it is a gap in
     the text, not a chip. */
  padding-left: 0.3rem;
  background: var(--color-white);
  color: var(--color-dark);
}

.review-card__ellipsis[hidden] { display: none; }

/* "Show more" / "See less" is a real <button> — it is a control, not a link to
   anywhere, and a control that changes its own label needs button semantics for
   the keyboard and for screen readers. It is STYLED as a text link, matching the
   site's own `a` rule (brand red, underline on hover and focus), because in this
   position at the end of a quotation a bordered pill competed with the review
   text and with the card border. */
.review-card__reveal {
  order: 1;
  align-self: flex-start;
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  /* Padding rather than margin: it enlarges the tap target without moving the
     control away from the text above it. The bottom margin is the gap to the
     divider that follows. */
  margin: 0.7rem 0 1rem;
  padding: 0.15rem 0;
  border: 0;
  background: none;
  color: var(--color-primary-deep);
  font-family: inherit;
  font-size: 0.9rem;
  font-weight: 700;
  line-height: 1.4;
  text-align: left;
  cursor: pointer;
  transition: color var(--transition);
}

.review-card__reveal:hover,
.review-card__reveal:focus-visible {
  color: var(--color-primary-hover);
  text-decoration: underline;
  text-underline-offset: 3px;
}

/* A small chevron points the way the card will move: down to open, up to close.
   Drawn with borders so it adds no asset and no dependency. */
.review-card__chevron {
  width: 0.42rem;
  height: 0.42rem;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: rotate(45deg) translate(-1px, -1px);
  transition: transform var(--transition);
}

.review-card__reveal[aria-expanded="true"] .review-card__chevron {
  transform: rotate(225deg) translate(-1px, -1px);
}

/* [hidden] has to win over the component's own display value. */
.review-card__reveal[hidden] { display: none; }

@media print {
  .review-card__reveal,
  .review-card__ellipsis { display: none; }
  .review-card__text.is-clamped { overflow: visible; max-height: none !important; }
}

.review-card__meta {
  order: 2;
  /* The card is a column flex box and both this and the Show more control carry
     an order, so this block sits last whatever order the markup puts them in.
     auto on margin-top is what pins the attribution to the bottom of the card
     when a neighbour in the row is taller; the 1.75rem bottom margin holds it
     off the card's padding. The padding-top is the gap between the attribution
     and its divider — the divider is this block's top border, so without it the
     rule sits flush against the Show more link above. */
  margin: auto 0 1.75rem;
  padding-top: 1.1rem;
  border-top: 1px solid var(--color-gray-border);
  font-size: 0.9rem;
}

.review-card__author { font-weight: 700; color: var(--color-dark); }
.review-card__source { color: var(--color-gray-text); }

/* Prominent Google review card (reviews hub) */
.google-cta {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1.5rem;
  flex-wrap: wrap;
  background: var(--color-gray-light);
  border: 1px solid var(--color-gray-border);
  border-radius: var(--border-radius-lg);
  padding: 1.5rem 1.75rem;
  margin-bottom: 2.5rem;
}

.google-cta p { margin: 0.35rem 0 0; }

/* Category filter tabs */
.reviews-toolbar {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  justify-content: center;
  margin-bottom: 2rem;
}

.filter-tab {
  font-family: var(--font-heading);
  font-size: 0.9rem;
  font-weight: 600;
  color: var(--color-dark);
  background: var(--color-white);
  border: 1px solid var(--color-gray-border);
  border-radius: 999px;
  padding: 0.55rem 1.1rem;
  cursor: pointer;
  transition: background-color var(--transition),
              border-color var(--transition),
              color var(--transition);
}

.filter-tab:hover {
  border-color: var(--color-dark);
}

.filter-tab.is-active {
  background: var(--color-primary-deep);
  border-color: var(--color-primary-deep);
  color: var(--color-white);
}

.filter-tab__count {
  margin-left: 0.25rem;
  /* Was 0.75. Against the active tab's red that mixed down to 3.9:1, under
     the 4.5:1 the count needs at 14.4px. 0.9 keeps it visibly secondary. */
  opacity: 0.9;
}

/* Empty / error state inside the grid */
.reviews-empty {
  grid-column: 1 / -1;
  text-align: center;
  color: var(--color-gray-text);
  padding: 2rem 1rem;
}

/* ==========================================================================
   14. QUOTE FORM
   ========================================================================== */
.form-wrap {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 3rem;
  align-items: start;
}

.form-card {
  background: var(--color-white);
  border: 1px solid var(--color-gray-border);
  border-radius: var(--border-radius-lg);
  padding: clamp(1.5rem, 4vw, 2.5rem);
  box-shadow: var(--shadow-md);
}

.form-card--on-dark {
  border: none;
}

.field { margin-bottom: 1.15rem; }

.field label {
  display: block;
  font-weight: 600;
  font-size: 0.9rem;
  color: var(--color-dark);
  margin-bottom: 0.4rem;
}

.field .required { color: var(--color-primary-deep); }

.field input,
.field select,
.field textarea {
  width: 100%;
  font-family: var(--font-sans);
  font-size: 1rem;
  color: var(--color-dark);
  background: var(--color-white);
  border: 1px solid var(--color-gray-border);
  border-radius: var(--border-radius-md);
  padding: 0.8rem 0.9rem;
  transition: border-color var(--transition), box-shadow var(--transition);
}

.field textarea { resize: vertical; min-height: 130px; }

.field input:focus,
.field select:focus,
.field textarea:focus {
  border-color: var(--color-primary);
  outline: none;
  box-shadow: 0 0 0 3px var(--color-primary-soft);
}

.field--row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1rem;
}

.form-note {
  font-size: 0.85rem;
  color: var(--color-gray-text);
  margin-top: 1rem;
}

/* ==========================================================================
   15. CTA BAND
   ========================================================================== */
/* Painted with --color-primary-deep, not --color-primary. The paragraph here is
   body copy, and white on the brand red is 4.45:1 — just under the 4.5:1 that
   body copy needs. The deep red is 5.4:1 and is the same hue family, so the two
   read as one red across the page. Buttons keep the true brand red. */
.cta-band {
  background: var(--color-primary-deep);
  color: var(--color-white);
  padding: clamp(2.5rem, 6vw, 4rem) 0;
}

.cta-band h2 { color: var(--color-white); margin-bottom: 0.5rem; }
.cta-band p { color: rgba(255, 255, 255, 0.92); }

.cta-band__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 2rem;
  flex-wrap: wrap;
}

.cta-band__actions {
  display: flex;
  gap: 1rem;
  flex-wrap: wrap;
}

/* ==========================================================================
   16. FOOTER
   ========================================================================== */
.site-footer {
  background: var(--color-dark);
  color: var(--color-text-on-dark-soft);
  padding: 4rem 0 2rem;
  font-size: 0.92rem;
}

.footer__grid {
  display: grid;
  grid-template-columns: 1.4fr 1fr 1fr 1.2fr;
  gap: 2.5rem;
  padding-bottom: 2.5rem;
  border-bottom: 1px solid var(--color-dark-border);
}

.site-footer h3 {
  color: var(--color-white);
  font-size: 1rem;
  margin-bottom: 1rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

.site-footer a { color: var(--color-text-on-dark-soft); }
.site-footer a:hover { color: var(--color-white); }

.footer__list li { margin-bottom: 0.6rem; }

/* The header uses the black-background lockup so its rectangle disappears into
   the #000 header. The footer keeps the transparent cut: it already sits on
   near-black, and a pure-black tile would show a seam there. */
.footer__logo {
  width: 190px;
  height: auto;
  margin-bottom: 1rem;
}

.footer__areas { line-height: 1.9; }

.footer__bottom {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
  padding-top: 1.75rem;
  font-size: 0.85rem;
  color: var(--color-text-on-dark-mute);
}

/* ==========================================================================
   17. UTILITIES
   ========================================================================== */
.mt-0 { margin-top: 0; }
.mt-2 { margin-top: 2rem; }
.mb-0 { margin-bottom: 0; }
.visually-hidden {
  position: absolute !important;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.breadcrumbs {
  font-size: 0.85rem;
  color: var(--color-gray-text);
  margin-bottom: 0.75rem;
}
.breadcrumbs a { color: var(--color-gray-text); }
.breadcrumbs a:hover { color: var(--color-primary-hover); }

/* ==========================================================================
   18. RESPONSIVE BREAKPOINTS
   ========================================================================== */

/* Tablet */
@media (max-width: 1024px) {
  .cards--4 { grid-template-columns: repeat(2, 1fr); }
  .reviews-grid { grid-template-columns: repeat(2, 1fr); }
  .footer__grid { grid-template-columns: 1fr 1fr; }
  .trust-bar__grid { grid-template-columns: repeat(2, 1fr); gap: 1.5rem; }
  .steps { grid-template-columns: repeat(2, 1fr); }
  .review-team { grid-template-columns: 1fr; }
  .review-team img { max-width: 420px; }
}

/* Mobile navigation switch */
@media (max-width: 900px) {
  .nav,
  .header__actions .btn-desktop { display: none; }
  .nav__toggle { display: flex; }
  .form-wrap { grid-template-columns: 1fr; gap: 2rem; }
  .features { grid-template-columns: 1fr; gap: 1.5rem; }
  .cards--3 { grid-template-columns: 1fr; }
}

/* Small screens */
@media (max-width: 640px) {
  body { font-size: 16px; }
  .cards--4 { grid-template-columns: 1fr; }
  .cards--2 { grid-template-columns: 1fr; }
  .reviews-grid { grid-template-columns: 1fr; }
  .footer__grid { grid-template-columns: 1fr; gap: 1.75rem; }
  .topbar__inner { justify-content: center; text-align: center; }
  .topbar__group--right { display: none; }
  .field--row { grid-template-columns: 1fr; }
  .steps { grid-template-columns: 1fr; gap: 1rem; }
  .gallery { grid-template-columns: 1fr; }
  .hero__ctas .btn,
  .cta-band__actions .btn { width: 100%; }
  .cta-band__inner { flex-direction: column; align-items: flex-start; }
  .google-cta { flex-direction: column; align-items: flex-start; }

}

/* Header fit. The header row must never be wider than the viewport:
   - 901-1100px: seven nav links plus the desktop call button do not fit at
     full size, so tighten the gaps and type until the drawer takes over.
   - phones: see "Phone header" below. As a safety net, any part of the row
     may give way (min-width: 0) except the menu toggle. */
.site-header__inner > * { min-width: 0; }
.nav__toggle { flex-shrink: 0; }

@media (min-width: 901px) and (max-width: 1100px) {
  .site-header__inner { gap: 1rem; }
  .brand__logo { width: 160px; }
  .nav { gap: 1rem; }
  .nav__link { font-size: 0.9rem; }
  .header__actions .btn-desktop { padding: 0.8rem 1rem; font-size: 0.92rem; }
}

/* Nav CTA needs to stay visible on mobile as the click-to-call button */
@media (max-width: 900px) {
  .header__actions .btn-call-mobile { display: inline-flex; }
}
@media (min-width: 901px) {
  .header__actions .btn-call-mobile { display: none; }
}

/* Phone header: logo + call button + menu toggle must fit a 375px screen.
   At full button size they needed ~450px, so the page scrolled sideways and
   the menu toggle sat off-screen. */
@media (max-width: 640px) {
  /* Long labels ("Read All 200+ Customer Reviews →") may wrap rather than
     push the page wider than the phone. */
  .btn { white-space: normal; max-width: 100%; line-height: 1.2; }
  .site-header__inner { gap: 0.75rem; }
  .header__actions { gap: 0.5rem; }
  .brand__logo { width: 124px; }
  .header__actions .btn-call-mobile {
    padding: 0.75rem 0.65rem;
    font-size: 0.9rem;
    white-space: nowrap;
  }
}
/* The narrowest phones show just "Call"; the number stays in the link for
   screen readers, and the hero's full-width call button sits right below. */
@media (max-width: 360px) {
  .btn-call-mobile__number {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
  }
}

/* ==========================================================================
   19. COMPONENT ADDITIONS
   These two blocks arrived after the original sheet — breadcrumbs replaced
   inline style="" attributes on five service pages, and the footer NAP block is
   a local-SEO requirement. They keep their original relative order so the
   cascade behaves exactly as before.
   ========================================================================== */

/* --------------------------------------------------------------------------
   BREADCRUMBS
   Previously these carried inline style="color:..." attributes on five
   service pages — which broke the "one master stylesheet" rule and hardcoded a
   colour that was not in the token set. One rule replaces all of them, and it
   defaults to inheriting the surrounding text colour so it works both in the
   dark hero (light text) and on a plain white content page (dark text).
   BreadcrumbList JSON-LD accompanies this markup on each service page.
   -------------------------------------------------------------------------- */
.breadcrumbs {
  font-size: 0.9rem;
  color: inherit;
  margin-bottom: 0.75rem;
}

.breadcrumbs a {
  color: inherit;
  text-decoration-color: currentColor;
}

/* On the dark hero the breadcrumb sits over a scrim, so lift it slightly. */
.hero__inner .breadcrumbs {
  color: rgba(255, 255, 255, 0.82);
}

/* --------------------------------------------------------------------------
   FOOTER NAP BLOCK
   Postal address, phone and email in the footer. This is a local-SEO
   requirement as much as a design one: the name, address and phone number must
   be readable text, present on every page, and identical to the Google
   Business Profile and every directory citation. Styled to stay legible against
   the dark footer surface without competing with the main footer copy.
   -------------------------------------------------------------------------- */
.footer__address {
  font-style: normal;               /* <address> italicises by default — undo it */
  line-height: 1.9;
  margin-top: 0.75rem;
}

.footer__address a {
  color: var(--color-white);
  text-decoration-color: rgba(255, 255, 255, 0.35);
}

.footer__legal {
  display: block;
  margin-top: 0.5rem;
}

.footer__legal a { color: inherit; }

/* ==========================================================================
   20. CASE STUDY
   Added 16 Oct 2026 with case-studies.html, the first case study page
   (docs/CASE-STUDY-RETAINER.md). Two components are genuinely new here; the
   page reuses the hero, badge, cards, review card, quote form and CTA band
   unchanged, so this block is deliberately the only addition.

   The before/after was first a side-by-side pair and is now a drag slider
   (21 Oct 2026, see .compare below). It still applies no crop of its own:
   both files are generated at 1200x900 from boxes chosen to match, so they
   arrive the same shape and are shown as they are.
   ========================================================================== */
/* BEFORE / AFTER SLIDER (21 Oct 2026, replacing the side-by-side pair).
   Both photographs sit in one 4:3 frame: the after fills it, and the before is
   clipped to everything left of --pos with clip-path, so dragging the handle
   wipes one into the other. --pos is set by assets/js/main.js section 7 from
   a real <input type="range"> laid invisibly over the frame; the browser gives
   that input dragging, touch, arrow keys and screen-reader support for free.
   The 4:3 frame is not a crop: both files are generated at 1200x900 from boxes
   chosen to match, so the frame is exactly their shape. Without JavaScript the
   default 50% shows a still half-and-half split. No motion plays on its own —
   the slider moves only when the visitor moves it. */
.compare { margin: 0; }

.compare__frame {
  --pos: 50%;
  position: relative;
  overflow: hidden;
  aspect-ratio: 4 / 3;
  max-width: 64rem;
  margin: 0 auto;
  border-radius: var(--border-radius-lg);
  box-shadow: var(--shadow-md);
  background: var(--color-dark);
  user-select: none;
  /* Vertical swipes still scroll the page on a phone; horizontal ones drag. */
  touch-action: pan-y;
}

.compare__img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

.compare__img--before { clip-path: inset(0 calc(100% - var(--pos)) 0 0); }

/* The handle: a red line with a round grip, drawn in CSS (no asset). It is
   decoration only; the input underneath is the control. */
.compare__handle {
  position: absolute;
  top: 0;
  bottom: 0;
  left: var(--pos);
  width: 4px;
  margin-left: -2px;
  background: var(--color-primary);
  pointer-events: none;
}

.compare__handle::after {
  content: "\2039\00a0\203A";
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  display: grid;
  place-items: center;
  width: 3.25rem;
  height: 3.25rem;
  border-radius: 50%;
  background: var(--color-primary-deep);
  color: var(--color-white);
  font-size: 1.5rem;
  font-weight: 800;
  line-height: 1;
  box-shadow: 0 0 0 4px rgba(0, 0, 0, 0.3);
}

/* The real control: full-frame and transparent, so a press anywhere on the
   photo moves the handle there. */
.compare__range {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  opacity: 0;
  cursor: ew-resize;
  -webkit-appearance: none;
  appearance: none;
}

/* The input is invisible, so its keyboard focus is shown on the grip instead. */
.compare__frame:has(.compare__range:focus-visible) .compare__handle::after {
  outline: 3px solid var(--color-white);
  outline-offset: 3px;
}

.compare__tag {
  position: absolute;
  top: 1rem;
  padding: 0.3rem 0.7rem;
  border-radius: var(--border-radius-xs);
  font-size: 0.85rem;
  font-weight: 700;
  pointer-events: none;
}

.compare__tag--before { left: 1rem; background: var(--color-dark); color: var(--color-white); }
.compare__tag--after { right: 1rem; background: var(--color-white); color: var(--color-dark); }

.compare figcaption {
  max-width: 64rem;
  margin: 0.9rem auto 0;
  font-size: 0.9rem;
  color: var(--color-gray-text);
}

/* The job's own facts, as a <dl>: location, job type, what was installed and
   when. A definition list rather than a table because there are no columns or
   figures to compare — it is a label and its value. The row rule is a hairline
   rather than a box, so the block reads as a job sheet and not as a card. */
.job-details {
  margin: 2rem 0;
  border-top: 1px solid var(--color-gray-border);
}

.job-details__row {
  display: grid;
  grid-template-columns: 12rem 1fr;
  gap: 0.5rem 1.5rem;
  padding: 0.9rem 0;
  border-bottom: 1px solid var(--color-gray-border);
}

.job-details dt {
  font-family: var(--font-heading);
  font-weight: 700;
  color: var(--color-dark);
}

.job-details dd { margin: 0; }

/* One review card alone in the grid.
   A single card in a three-column `.reviews-grid` sits in the left third with
   two empty columns beside it — which is exactly what the first render of this
   page showed. It is not a defect any static check could see: the card is the
   right width for a column, there are simply no other columns.
   Spanning the row is the honest fix rather than centring the card, because the
   card is the only thing in the section and should read as a full-width
   testimonial rather than as a stranded excerpt. The measure is capped so the
   quotation does not run the whole 1200 px, and the clamp in main.js still
   measures the paragraph, so the Show more control is unaffected. */
.review-card--wide {
  grid-column: 1 / -1;
  max-width: 52rem;
  margin: 0 auto;
}

@media (max-width: 640px) {
  .job-details__row { grid-template-columns: 1fr; gap: 0.15rem; }
}

/* ==========================================================================
   21. SHARE BLOCK
   Added 16 Oct 2026 with the share controls on case-studies.html (client
   request). Three anchors — Facebook, WhatsApp and X — plus a copy-link control
   and a native share button, both of which assets/js/main.js reveals.

   Two decisions worth keeping:

   * The links are ordinary anchors and this block needs no JavaScript at all.
     That is the project's standing rule (nothing may depend on script), and it
     is also the honest reading of what a share button can do: the platform reads
     the TARGET PAGE's Open Graph tags, so the picture in the preview comes from
     case-studies.html's own og:image, never from anything a button can send.

   * They are styled as buttons, not as links, and that is not a contradiction.
     Each one is a real navigation to a third-party sharing endpoint, so <a> is
     the correct element (a <button> would need script to navigate, and would not
     offer "open in a new tab"). Styling an anchor as a button is the treatment
     the job wants: a small row of peer actions that look tappable.

   Reuse rather than invention: the vertical rhythm and the two-column stack come
   from the `.job-details` pattern in block 20, the pill radius is `999px` as
   `.badge-rating` and `.pill` already use, and hover/focus work on the global
   `:focus-visible` ring plus a border-colour change. Nothing here is a new
   colour, size or shadow. */
.share {
  margin-top: 2.5rem;
  padding-top: 1.75rem;
  border-top: 1px solid var(--color-gray-border);
}

.share__title {
  margin: 0 0 0.35rem;
  font-size: 1.125rem;
}

.share__lead {
  margin: 0 0 1rem;
  color: var(--color-gray-text);
}

.share__list {
  display: flex;
  flex-wrap: wrap;
  gap: 0.625rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

.share-button {
  display: inline-flex;
  align-items: center;
  gap: 0.55rem;
  padding: 0.6rem 1rem;
  background: var(--color-white);
  border: 1px solid var(--color-gray-border);
  border-radius: 999px;
  color: var(--color-dark);
  font-family: var(--font-heading);
  font-size: 0.93rem;
  font-weight: 700;
  line-height: 1;
  white-space: nowrap;
  transition: border-color var(--transition),
              color var(--transition),
              box-shadow var(--transition);
}

.share-button:hover {
  border-color: var(--color-dark);
  color: var(--color-dark);
  box-shadow: var(--shadow-md);
}

/* The icon states the brand; the label states the action. Kept to currentColor
   so an icon can never disagree with its own link's colour. */
.share-button__icon {
  flex-shrink: 0;
  width: 20px;
  height: 20px;
}

/* The two script-only controls. `hidden` is the default state in the markup, so
   without JavaScript there is no navigator.share() to call and no clipboard to
   write to, and neither control is offered. JS removes the attribute. */
.share__native {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 1rem;
  margin-top: 1.25rem;
}

.share__copy {
  padding: 0;
  background: none;
  border: 0;
  color: var(--color-primary-deep);
  font-family: var(--font-heading);
  font-size: 0.93rem;
  font-weight: 700;
  text-decoration: underline;
  text-underline-offset: 3px;
  cursor: pointer;
}

.share__copy:hover { color: var(--color-primary-hover); }

/* [hidden] has to win over the component's own display value — the same rule,
   and the same reason, as .review-card__reveal[hidden] in block 13. `.btn`
   sets `display: inline-flex`, and a class selector beats the user agent's
   `[hidden] { display: none }`, so the button would render for a visitor whose
   script never ran: a control that calls navigator.share() where there is no
   navigator.share to call.
   This was found by rendering the block with main.js deliberately not loaded —
   NOT by reading the markup, which is correct, and not by any check, because
   every check reads the attribute and the attribute was right. It is the same
   failure mode as the review control's, which is why the fix is the same. */
.share__native-btn[hidden] { display: none; }

/* The polite live region that reports "Link copied". Empty in the markup and
   written by JS, so it is never in the page for a screen reader to read out on
   load, and it takes up no room until there is something to say. */
.share__status {
  margin: 0;
  font-size: 0.9rem;
  font-weight: 600;
  color: var(--color-gray-text);
}

@media (max-width: 640px) {
  /* One per row: three pills side by side on a 390 px screen either wrap
     unevenly or squeeze the labels down to an unreadable width. */
  .share__list { flex-direction: column; }
  .share-button { justify-content: center; width: 100%; }
}

/* ==========================================================================
   22. CONSENT BANNER
   Added 17 Oct 2026 (SEO-PLAN.md section 7 item 2). The first-party banner asks
   permission for Google Analytics and Google Ads cookies before either is set;
   there is no third-party CMP because a CMP would be a runtime dependency and
   this project has none.

   Three decisions worth keeping:

   * It is a bar fixed to the bottom of the viewport, in ink, with the brand red
     as a 3px cap. A modal that covers the page would be the wrong trade on a
     site whose whole job is to get a homeowner to read a price and call: the
     banner has to be answerable, and then be gone.

   * "Reject optional" carries the same size and the same position in the row
     as "Accept all". Consent must be as easy to refuse as to give, so the
     refusal is a real button and not a link in the corner - and its border is
     white rather than the ink `.btn-outline` would give, which is invisible on
     this background.

   * There is no entrance animation. An element with the `hidden` attribute is
     not rendered at all, so there is nothing to animate from, and a bar that
     slides in at the bottom of a phone screen is exactly the motion a
     `prefers-reduced-motion` visitor did not ask for. */
.consent {
  position: fixed;
  right: 0;
  bottom: 0;
  left: 0;
  z-index: 150;                      /* above the page, below the skip link (200) */
  padding: 1.15rem clamp(1rem, 4vw, 2rem);
  padding-bottom: max(1.15rem, env(safe-area-inset-bottom));
  background: var(--color-dark);
  border-top: 3px solid var(--color-primary);
  box-shadow: var(--shadow-lg);
  color: var(--color-text-on-dark);
}

.consent__inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 1rem 1.75rem;
  max-width: var(--max-width);
  margin: 0 auto;
}

.consent__copy {
  flex: 1 1 32rem;
  min-width: 0;
}

.consent__title {
  margin: 0 0 0.3rem;
  color: var(--color-white);
  font-size: 1.05rem;
}

.consent__text {
  margin: 0;
  font-size: 0.95rem;
  line-height: 1.55;
}

/* The policy link sits on ink, so it takes the white text colour rather than
   the brand red the base anchor rule gives (which is 4.45:1 on white and much
   worse on ink). Underlined, because a link inside a paragraph has to look
   like one. */
.consent__text a {
  color: var(--color-white);
  text-decoration: underline;
  text-underline-offset: 3px;
}

.consent__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
  align-items: center;
}

.consent__actions .btn {
  padding: 0.8rem 1.35rem;
  font-size: 0.95rem;
}

.consent__reject {
  background: transparent;
  border-color: rgba(255, 255, 255, 0.55);
  color: var(--color-white);
}

.consent__reject:hover {
  background: rgba(255, 255, 255, 0.12);
  border-color: var(--color-white);
  color: var(--color-white);
}

/* The footer control that reopens the banner. Styled as a link because that is
   what it looks like in that line, but it is a <button> because it acts on this
   page rather than navigating anywhere. */
.footer__legal-btn {
  padding: 0;
  background: none;
  border: 0;
  color: inherit;
  font: inherit;
  text-decoration: underline;
  text-underline-offset: 3px;
  cursor: pointer;
}

/* The separator belongs to the control, not to the line: the legal row reads
   "Privacy Policy · Accessibility · Cookie settings", and a dot typed into the
   footer would still be there in the run where the button ships hidden. A
   pseudo-element on the button disappears with it. */
.footer__legal-btn::before {
  content: "·";
  display: inline-block;
  margin: 0 0.4rem 0 0.15rem;
  color: var(--color-text-on-dark-mute);
  text-decoration: none;
}

.footer__legal-btn:hover { color: var(--color-white); }

/* [hidden] is stated explicitly rather than left to the user agent. Today these
   two rules are belt-and-braces - neither element carries an author `display`
   value, so `[hidden] { display: none }` already wins - but this sheet has
   shipped that fault twice already (.review-card__reveal[hidden] in block 13,
   .share__native-btn[hidden] in block 21), and each time the markup was correct
   and no check could see the rendering. The difference here is the blast radius:
   a failed `.share__native-btn[hidden]` shows one broken control on one page,
   while a failed `.consent[hidden]` asks every visitor on a site that sets no
   cookie to accept a cookie. Two lines is a cheap way to make that impossible. */
.consent[hidden],
.footer__legal-btn[hidden] { display: none; }

@media (max-width: 640px) {
  /* The bar has to stay answerable on a phone without covering the whole
     viewport, so the buttons go full width and the copy keeps its own measure. */
  .consent__actions { width: 100%; }
  .consent__actions .btn { flex: 1 1 auto; }
}

