/*
Theme Name: South Norfolk Recycling
Theme URI: https://xpose.online
Author: Xpose
Description: Custom theme for South Norfolk Recycling. Static-export friendly — no build step, no framework, one 14KB display font, zero JS.
Version: 0.1.0
Requires at least: 6.0
Tested up to: 7.0
Requires PHP: 8.1
License: Proprietary
Text Domain: snr
*/

/* ============================================================
   TOKENS

   Palette follows the logo: a steel royal blue (--brand) and a
   deep steel navy for the dark surfaces (--steel / --steel-2).
   The logo's own black textured background is a mockup, not part
   of the mark, so it is nowhere here.

   Historical note: this theme was green, hi-vis and copper. The
   token NAMES changed with the colours — --green became --brand,
   --hivis became --spark, --copper became --action — so a value
   never lies about what it is. If you are diffing against an old
   build, that rename is why every line moved.

   --brand  is the structural blue: links, card edges, the mark,
            the "after" tag. #1F5AA8 is 6.8:1 on white, so it
            carries body-weight text, not just graphics.

   --spark  is the accent for dark surfaces and nothing else — the
            eyebrows, step numbers, the rail's top edge, the open
            menu. A light steel blue at 6.8:1 on --steel and ~1.4:1
            on paper: on a light section it is not an accent, it is
            an invisible one. Same rule the old hi-vis lived by,
            new hue.

   --action is the reverse — the warm CTA colour, kept deliberately
            when everything else went blue so "WhatsApp a photo"
            still jumps off a blue page instead of dissolving into
            it. The one colour here that is not in the logo, earning
            its place by being the thing a customer has to find.
   ============================================================ */

/* ============================================================
   THE DISPLAY FACE

   Archivo 800, SIL OFL 1.1 — licence shipped alongside the file
   in assets/fonts/. Self-hosted, so there is no third-party
   request, nothing to consent to, and nothing for the strict CSP
   to allow.

   One weight, one subset, 14KB. Only the headlines use it; body,
   UI and labels stay on the system stack, which is why there is
   no second file. The latin subset covers everything the headings
   actually contain — the arrows and box-drawing the site uses
   elsewhere are all in mono, which is untouched.

   font-display: OPTIONAL, not swap. This theme preloads its LCP
   image and ships width/height on every img specifically so that
   nothing moves while the page loads; letting a headline reflow
   when a font lands would give that back. With `optional` the
   browser uses Archivo when the preload beat the first paint and
   silently keeps the system face when it did not — no swap, no
   shift, and it is cached for the next page either way. On a
   rural 4G first hit the customer gets a fast page in Segoe UI
   rather than a slow one in Archivo, which is the trade this site
   has made everywhere else.

   If the brand face matters more than the last of the CLS, this
   is a one-word change to `swap`.
   ============================================================ */

@font-face {
  font-family: "Archivo";
  font-style: normal;
  font-weight: 800;
  font-display: optional;
  src: url("https://southnorfolkrecycling.co.uk/wp-content/themes/snr/assets/fonts/archivo-800-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}

:root {
  --ink:        #10151C;
  --steel:      #16283F;
  --steel-2:    #21395A;
  --paper:      #F4F6F9;
  --surface:    #FFFFFF;
  --muted:      #58626E;
  /* Quiet label colour, in a light/dark pair like --rule / --rule-dark.
     The old single --faint was #8A928B, which is 3.2:1 on white and 2.98:1 on
     paper — it failed AA everywhere it labelled something on a light surface,
     which is most places it was used. It reads correctly on the dark slabs, so
     that value lives on as --faint-dark rather than being tuned away. */
  --faint:      #64707C;
  --faint-dark: #8A929C;
  --rule:       #DCE0E6;
  --rule-dark:  #2A3B54;
  /* A rule between two paragraphs is decoration and can be as quiet as it
     likes. The edge of a text field is a user interface component: it is the
     only thing telling you where to type, so it owes 3:1 against what is
     either side of it. --rule managed 1.23:1. */
  --field-line: #7B838E;

  --brand:      #1F5AA8;
  --brand-lift: #2E72C8;
  --action:     #B4521F;
  --action-lift:#CA6127;
  --spark:      #86B4EC;

  --font: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --mono: ui-monospace, "Cascadia Mono", "Segoe UI Mono", Consolas, monospace;
  /* Headlines only. The fallback is the same stack the rest of the site runs
     on, so a page that never gets the font is a page set consistently in one
     family rather than a page missing something. */
  --display: "Archivo", system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;

  --wrap: 74rem;
  --measure: 60ch;
  --bar: 4.25rem;    /* sticky mobile action bar height */
  --header: 5.85rem; /* sticky desktop header height — anything that has to
                        clear it measures from here rather than guessing */
}

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

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

/* Keeps the sticky header from landing on top of whatever was jumped to —
   the skip link's own target included. */
@media (min-width: 48rem) {
  html { scroll-padding-top: calc(var(--header) + 1rem); }
}

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font);
  font-size: 17px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  /* The display type is set at 800. On Windows and macOS that lands on a real
     face — Segoe UI Black and SF Pro Heavy, both measurably wider than their
     Bold — but on a platform that ships neither, the browser would smear the
     Bold into a fake. Better to fall back to a clean 700 than to print a
     headline in a synthesised weight. Weight only: real italics still work. */
  font-synthesis-weight: none;
}

/* Mobile: leave room for the sticky action bar */
@media (max-width: 47.99rem) {
  body { padding-bottom: var(--bar); }
}

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

/* Underlines sat on the baseline and cut through every descender in
   "duty of care", "paperwork", "carrier". Dropping them clear and thinning
   them is the difference between a link and a struck-through link. */
a { color: var(--brand); text-underline-offset: .18em; text-decoration-thickness: 1px; }
a:hover { text-decoration-thickness: 2px; }

/* Hi-vis, because it is the site's own colour for "look here" — and it is the
   one highlight that stays legible on the paper sections and the steel ones
   without needing a second rule. */
::selection { background: var(--spark); color: var(--ink); }

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

/* Copper is 2.96:1 on steel and 1.35:1 on --brand — the band, which is where
   the two buttons that matter most on every page live. Those numbers were 3.34
   and 1.86 against the old green palette; the rebrand moved both the wrong way.
   --spark is 3.16:1 on the band, so the focus ring goes hi-vis there. */
.band :focus-visible { outline-color: var(--spark); }

.wrap { max-width: var(--wrap); margin: 0 auto; padding: 0 1.25rem; }

@media (min-width: 60rem) { .wrap { padding: 0 2rem; } }

.skip {
  position: absolute; left: -9999px;
  background: var(--ink); color: #fff; padding: .75rem 1rem; z-index: 100;
}
.skip:focus { left: 1rem; top: 1rem; }

/* ============================================================
   TYPE
   ============================================================ */

/* The rule is simply: everything set at 800 uses the display face.
   That is the headings, the wordmark, the £600 stat, the licence strip's line,
   the four trust figures and the road names — the complete list, and it stays
   complete because 800 is the only weight the font ships and the only weight
   anything here is set at.

   Everything wearing .eyebrow, .rail-title, .menu-head or a footer heading
   overrides this with --mono and is unaffected: those are labels, not
   headlines, and they belong to the mono voice. The 700s — buttons, FAQ
   summaries, table row headers — stay on the system stack, because there is no
   700 Archivo to give them and a faked one is worse than a real Segoe. */
h1, h2, h3,
.brand-name,
.warn .stat,
.licence-title,
.trust-fig,
.road-name { font-family: var(--display); }

h1, h2, h3 { margin: 0; font-weight: 800; text-wrap: balance; }

h1 { font-size: clamp(2.4rem, 7.5vw, 4.6rem); line-height: 1.05; letter-spacing: -0.03em; }
h2 { font-size: clamp(1.85rem, 4.2vw, 2.9rem); line-height: 1.05; letter-spacing: -0.028em; }

/* Optical tracking. Tight letter-spacing is a property of size, not of the
   element: -0.035em flat was slightly loose on a 74px headline and slightly
   pinched on a 38px one. Bigger gets tighter, in step with the clamps above. */
@media (min-width: 48rem) {
  h1 { letter-spacing: -0.038em; }
  h2 { letter-spacing: -0.032em; }
}
@media (min-width: 80rem) {
  h1 { letter-spacing: -0.043em; }
  h2 { letter-spacing: -0.036em; }
}

/* h3 is body-sized, not display-sized, so it does not get the display leading.
   At 1.05 a heading that wrapped would have had its two lines almost touching
   — none of them wrap at today's copy lengths, which is luck rather than
   design. */
h3 { font-size: 1.15rem; line-height: 1.25; letter-spacing: -0.02em; }

p { margin: 0; text-wrap: pretty; }

/* Weight and leading are set explicitly, not inherited, because this class is
   worn by both <p> and <h2> — a section whose only heading is an eyebrow still
   needs a real heading element in the outline, and it must not suddenly render
   at 800 and 1.05 when it gets one. */
.eyebrow {
  font-family: var(--mono);
  font-size: .72rem;
  font-weight: 400;
  line-height: 1.4;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--action);
  margin: 0;
}
.eyebrow.on-dark { color: var(--spark); }

/* 1.5 rather than the body's 1.6: leading wants to tighten as type grows, and
   at 19px the inherited value was opening the deck up into loose stripes. */
.lede { font-size: 1.2rem; line-height: 1.5; color: var(--muted); max-width: var(--measure); }
.lede.on-dark { color: #C3CCD8; }

/* ============================================================
   BUTTONS
   ============================================================ */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: .6rem;
  padding: 1.05rem 1.6rem;
  border-radius: 3px;
  font-weight: 700;
  font-size: 1.02rem;
  text-decoration: none;
  border: 2px solid transparent;
  transition: transform .12s ease, background-color .12s ease;
  min-height: 3.25rem;
}
.btn:active { transform: translateY(1px); }

.btn-primary { background: var(--action); color: #fff; }
.btn-primary:hover { background: var(--action-lift); }

/* .6, not .5, and originally .35. A ghost button IS its border — that outline
   is the only thing saying "this is a control" — so it has to clear 3:1 against
   whatever it sits on.

   .35 was 3.19 on steel and 2.54 on the old green band. .5 fixed that at 5.14
   and 3.65. Then the band went from green to --brand, which is lighter, and .5
   fell to 2.93 on it — failing again, on the surface carrying the site's final
   call to action. .6 composites to 3.54 on --brand and 6.28 on --steel, so both
   clear with room rather than by hundredths. */
.btn-ghost { background: transparent; color: #fff; border-color: rgba(255,255,255,.6); }
.btn-ghost:hover { border-color: #fff; }

.btn-dark { background: var(--ink); color: #fff; }
.btn-dark:hover { background: var(--steel-2); }

/* Every transition on the site is collected in one reduced-motion rule at the
   end of the file, so a new one can't be added without the guard. */

/* ============================================================
   HEADER
   ============================================================ */

.site-header {
  background: var(--steel);
  color: #fff;
  border-bottom: 1px solid var(--rule-dark);
  /* Anchors the mobile menu panel and keeps it over the hero below. */
  position: relative;
  z-index: 40;
}

/* Sticky from the width where the header's own call button appears.

   On a phone the bottom action bar is already carrying Call and Send a photo,
   and a second fixed bar would eat a third of a small screen for nothing. On a
   desktop there was no persistent way to ring us at all: past the hero the
   number existed only in the footer, on pages that run eight thousand pixels.

   The menu panel is absolutely positioned against this element, so it comes
   along with it. */
@media (min-width: 48rem) {
  .site-header { position: sticky; top: 0; }
}

.header-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding-top: .9rem;
  padding-bottom: .9rem;
}

.brand { display: flex; align-items: center; gap: .7rem; text-decoration: none; color: #fff; }
.brand-mark {
  width: 2.1rem; height: 2.1rem; flex: none; display: block;
  /* mark.svg is a self-contained rounded badge, so this is just sizing. */
}
.brand-name { font-weight: 800; letter-spacing: -.03em; font-size: 1.02rem; line-height: 1.1; }
.brand-name span { display: block; font-family: var(--mono); font-size: .6rem; letter-spacing: .1em; color: var(--spark); font-weight: 400; text-transform: uppercase; white-space: nowrap; }

.header-right { display: flex; align-items: center; gap: .6rem; }

.header-call { display: none; }
@media (min-width: 48rem) {
  .header-call { display: inline-flex; white-space: nowrap; }
}

/* ------------------------------------------------------------------
   The nav bar appears only where it actually fits.

   Measured at full size: brand 229px + nav 504px + call button 196px,
   plus two 1rem flex gaps, is 961px of content — 1025px of viewport
   once the wrap's 2rem gutters are counted. It used to switch on at
   62rem/992px, so between 992 and 1025 every label wrapped onto two
   lines and the header grew a second row. Sticky made that permanent
   rather than something you scrolled past.

   68rem left about 60px of slack, and the note here said a seventh
   item would spend it. A seventh item was added — the four hubs, plus
   Areas, Licensing and Contact — so this is that budget being spent
   and the breakpoint being moved, as promised.

   The seven labels are 56 characters against the old six's 52, and
   there is one more 1.6rem gap. At the measured ~7.2px per character
   for .93rem/600, that is 405px of text plus 154px of gaps: a 559px
   nav where the old one was 504. Content goes to 1016px, viewport to
   1080px. 70rem/1120px is the next stop up and leaves 40px of slack —
   less than before, so the next label added here needs re-measuring
   rather than eyeballing.
   ------------------------------------------------------------------ */
.nav { display: none; }
@media (min-width: 70rem) {
  .nav { display: flex; gap: 1.6rem; }
  .nav a {
    color: #D2D9E2; text-decoration: none; font-size: .93rem; font-weight: 600;
    padding: .3rem 0; border-bottom: 2px solid transparent;
    white-space: nowrap;
  }
  .nav a:hover { color: #fff; border-bottom-color: var(--spark); }
  /* Where you are, not just where you could go. Same hi-vis rule the hover
     state uses, kept on — so the page you are reading is marked the way the
     link you are pointing at is about to be. */
  .nav a[aria-current="page"] { color: #fff; border-bottom-color: var(--spark); }
}

/* The disclosure menu carries the same mark. Below 70rem this is the whole
   navigation, so it is the only place the answer can come from. */
.menu-links a[aria-current="page"] { color: #fff; }
.menu-links a[aria-current="page"]::before {
  content: "";
  display: inline-block;
  width: .5rem; height: .5rem;
  margin-right: .55rem;
  vertical-align: .05em;
  background: var(--spark);
}

/* ============================================================
   MOBILE MENU
   Below 70rem the bar above is hidden, and until this existed
   nothing replaced it — the footer was the only way off a town
   page. Since the town and service pages are the ones search
   traffic lands on, and that traffic is overwhelmingly mobile,
   this was the single biggest hole in the site.

   It now also covers the small-laptop range, where the full nav
   does not fit; the panel carries every link the bar does and
   several it doesn't, so nothing is lost by showing it there.

   <details> because it is a real disclosure widget: keyboard
   operable and correctly announced, for zero JS.
   ============================================================ */

@media (min-width: 70rem) { .menu { display: none; } }

.menu-btn {
  list-style: none;
  cursor: pointer;
  display: inline-flex; align-items: center; gap: .45rem;
  padding: .6rem .8rem;
  min-height: 2.9rem;
  /* --rule-dark on --steel is 1.43:1. That is fine for a hairline between two
     footer blocks and not fine here: below 70rem this button is the only way
     into the navigation, and its border is the whole of its shape. */
  border: 1px solid rgba(255,255,255,.5);
  border-radius: 3px;
  color: #fff;
  font-weight: 700;
  font-size: .9rem;
}
.menu-btn::-webkit-details-marker { display: none; }
.menu-btn:hover { border-color: var(--spark); }

.menu .ico-close { display: none; }
.menu[open] .ico-open { display: none; }
.menu[open] .ico-close { display: block; }
.menu[open] .menu-btn { border-color: var(--spark); }

/* Full-bleed under the header rather than a narrow dropdown: at this width
   there is no reason to squeeze eleven links into a column 12rem wide. */
.menu-panel {
  position: absolute;
  top: 100%; left: 0; right: 0;
  background: var(--steel-2);
  border-bottom: 3px solid var(--spark);
  box-shadow: 0 14px 26px rgba(0,0,0,.3);
}

/* padding-block, not the padding shorthand.
   This element is also a .wrap, and .wrap sets the horizontal gutter — 1.25rem,
   2rem from 60rem up. The shorthand here has equal specificity and comes later
   in the file, so it was quietly resetting left and right to 0 and every link
   in the open menu sat hard against the edge of the phone. */
.menu-panel-inner { display: grid; gap: 1.5rem; padding-block: 1.5rem 1.75rem; }
/* auto-fit, not repeat(2, 1fr).
   This was two fixed columns over one flat list of five services and a "More"
   block. It is now one column per hub plus More — six groups — and a fixed
   two-up would run the open menu three screens down a phone. auto-fit lets the
   count follow the width, and 11rem is the widest label ("Plumbers & heating
   engineers" wraps to two lines below that, which is fine; narrower and it
   goes to three). */
@media (min-width: 34rem) {
  .menu-panel-inner { grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr)); gap: 2rem 1.75rem; }
}

.menu-head {
  font-family: var(--mono);
  font-size: .68rem;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--spark);
  font-weight: 400;
  margin: 0 0 .6rem;
}

.menu-links { list-style: none; margin: 0; padding: 0; display: grid; }
.menu-links a {
  display: block;
  padding: .65rem 0;
  color: #D2D9E2;
  text-decoration: none;
  font-weight: 600;
  border-bottom: 1px solid rgba(255,255,255,.08);
}
.menu-links a:hover { color: #fff; }

/* Redundant once the header's own call button appears. */
.menu-call { justify-content: center; }
@media (min-width: 34rem) { .menu-call { grid-column: 1 / -1; } }
@media (min-width: 48rem) { .menu-call { display: none; } }

/* The 404 reuses the link list on paper, where the dark-panel colours would be
   invisible. */
.menu-links-wide { grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); gap: 0 2.5rem; }
.menu-links-wide a { color: var(--brand); border-bottom-color: var(--rule); }
.menu-links-wide a:hover { color: var(--action); }

/* ============================================================
   HERO
   Every competitor leads with a stock van. This leads with the
   customer's own situation: the packed garage, photographed on
   the job. The headline carries the left, the photograph the
   right, and below 60rem the buttons outrank the picture.
   ============================================================ */

.hero {
  background: var(--steel);
  color: #fff;
  padding: 3.25rem 0 3.5rem;
  position: relative;
  overflow: hidden;
}

/* faint diagonal hazard rake — trade signal, very low contrast */
.hero::after {
  content: "";
  position: absolute; inset: 0;
  background-image: repeating-linear-gradient(
    -45deg,
    rgba(134,180,236,.05) 0 14px,
    transparent 14px 28px
  );
  pointer-events: none;
}

.hero-inner { position: relative; z-index: 1; display: grid; gap: 1.5rem; }
.hero-copy { display: grid; gap: 1.5rem; align-content: start; }

/* Two columns once there is room for the photo to be worth showing. Below that
   it sits under the buttons — the call to action outranks the picture on a
   phone, which is where nearly all of this traffic will be. */
@media (min-width: 60rem) {
  .hero { padding: 4.5rem 0 4.75rem; }
  .hero-inner { grid-template-columns: 1.05fr .95fr; gap: 3.5rem; align-items: center; }
}

.hero-photo img {
  aspect-ratio: 4 / 3;
  object-fit: cover;
  border-radius: 2px;
  /* the hi-vis edge ties the photo to the licence strip below it */
  border-left: 3px solid var(--spark);
}
@media (min-width: 60rem) {
  .hero-photo img { aspect-ratio: 4 / 5; max-height: 30rem; }
}

/* Inner-page hero, same split as the front page when a photo is available. */
.page-hero-inner { display: grid; gap: 1.75rem; }
.page-hero-copy { display: grid; gap: 1rem; align-content: center; }
@media (min-width: 60rem) {
  .page-hero-split .page-hero-inner { grid-template-columns: 1.15fr .85fr; gap: 3.5rem; align-items: center; }
}
.page-hero .hero-photo img {
  aspect-ratio: 4 / 3;
  object-fit: cover;
  border-left: 3px solid var(--spark);
}
@media (min-width: 60rem) {
  .page-hero .hero-photo img { aspect-ratio: 5 / 4; max-height: 22rem; }
}

.hero h1 { color: #fff; max-width: 15ch; }
.hero h1 em { font-style: normal; color: var(--spark); }

.hero-actions { display: grid; gap: .75rem; }
.hero-actions.mt { margin-top: 1.5rem; }
@media (min-width: 30rem) { .hero-actions { grid-template-columns: repeat(2, auto); justify-content: start; } }

.hero-note {
  font-family: var(--mono);
  font-size: .74rem;
  letter-spacing: .04em;
  color: #98A2AE;
}

/* ============================================================
   LICENCE STRIP — the thesis, immediately under the hero.
   ============================================================ */

.licence {
  background: var(--spark);
  color: var(--ink);
  border-top: 3px solid var(--ink);
  border-bottom: 3px solid var(--ink);
}
.licence-inner {
  display: grid;
  gap: 1rem;
  padding: 1.35rem 0;
}
@media (min-width: 56rem) {
  .licence-inner { grid-template-columns: auto 1fr; align-items: center; gap: 2rem; }
}
.licence-title {
  font-weight: 800;
  font-size: 1.05rem;
  letter-spacing: -.02em;
  display: flex; align-items: center; gap: .55rem;
}
.licence-nums { display: flex; flex-wrap: wrap; gap: .5rem 1.75rem; }
.licence-num { font-family: var(--mono); font-size: .78rem; }
.licence-num b { display: block; font-size: .64rem; letter-spacing: .1em; text-transform: uppercase; opacity: .7; font-weight: 400; }

/* ============================================================
   SECTIONS
   ============================================================ */

.section { padding: 3.5rem 0; }
@media (min-width: 60rem) { .section { padding: 5rem 0; } }

.section-dark { background: var(--steel); color: #fff; }
.section-dark h2 { color: #fff; }

/* The third background. With only paper and steel to work with, the run from
   the before/after to the FAQ was three paper sections stacked, which reads as
   one very long undifferentiated section. */
.section-alt {
  background: var(--surface);
  border-top: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
}

/* Visually hidden but still announced — used for table captions and the empty
   corner cell, both of which a screen reader needs and a sighted reader does
   not. */
.vis-hidden, .vs-caption, .vs-th-hidden {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.section-head { display: grid; gap: .85rem; margin-bottom: 2.25rem; max-width: var(--measure); }
.section-head-tight { margin-bottom: 1.1rem; }

/* ------------------------------------------------------------------
   The masthead.

   Every section head on this site was a 60ch column pinned to the left
   of a 74rem wrap, so on any desktop screen the page ran headline,
   lede, then four hundred pixels of nothing — the same shape on the
   home page, both landing templates and the FAQ. It read as a phone
   layout that had been stretched rather than designed.

   A measure is the right constraint for the lede and the wrong one for
   the headline, so they stop sharing a column: headline left at a size
   that can carry the width, deck right, both sitting on the same
   bottom edge. Only where there is a deck to move — a head that is
   just an eyebrow and a title is already the right shape.

   :has() is doing real work here and degrades honestly: a browser
   without it simply keeps today's single-column head.
   ------------------------------------------------------------------ */

@media (min-width: 64rem) {
  .section-head:has(.lede) {
    max-width: none;
    grid-template-columns: minmax(0, 1.15fr) minmax(0, .85fr);
    column-gap: 4rem;
    margin-bottom: 2.75rem;
  }
  .section-head:has(.lede) > .eyebrow { grid-column: 1; grid-row: 1; }
  /* Both hang from the row's bottom edge, so the pairing holds whichever of
     the two happens to be taller — otherwise a four-line deck beside a
     two-line headline leaves the headline floating at the top of the row. */
  .section-head:has(.lede) > h2 { grid-column: 1; grid-row: 2; align-self: end; }
  .section-head:has(.lede) > .lede {
    grid-column: 2;
    grid-row: 2;
    max-width: none;
    align-self: end;
    padding-bottom: .3rem;
  }
}

/* ============================================================
   SERVICES
   ============================================================ */

.cards {
  display: grid;
  gap: 1rem;
  grid-template-columns: 1fr;
}
@media (min-width: 40rem) { .cards { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 64rem) { .cards { grid-template-columns: repeat(3, 1fr); } }

.card {
  background: var(--surface);
  border: 1px solid var(--rule);
  border-top: 3px solid var(--brand);
  display: flex;
  flex-direction: column;
  text-decoration: none;
  color: var(--ink);
  overflow: hidden;
  transition: border-top-color .12s ease, transform .12s ease;
}
/* Photo on top: these cards carry the services, which is the commercial heart
   of the page, and text-only boxes were the weakest thing on it. */
.card-photo img { border-radius: 0; }
.card-body {
  padding: 1.35rem 1.35rem 1.5rem;
  display: flex;
  flex-direction: column;
  gap: .6rem;
  flex: 1;
}
/* Pushes every "see more" onto a shared baseline across the row. */
.card .more { margin-top: auto; padding-top: .4rem; }
.card:hover { border-top-color: var(--action); transform: translateY(-2px); }
.card p { color: var(--muted); font-size: .95rem; }
.card .more { font-family: var(--mono); font-size: .72rem; letter-spacing: .08em; text-transform: uppercase; color: var(--action); }


/* ============================================================
   HOW IT WORKS — 3 steps. Numbered because it IS a sequence.
   ============================================================ */

.steps { display: grid; gap: 1.75rem; counter-reset: step; }
@media (min-width: 52rem) { .steps { grid-template-columns: repeat(3, 1fr); gap: 2rem; } }

.step { counter-increment: step; display: grid; gap: .55rem; align-content: start; }
.step::before {
  content: counter(step, decimal-leading-zero);
  font-family: var(--mono);
  font-size: .8rem;
  color: var(--spark);
  letter-spacing: .08em;
}
.step h3 { color: #fff; }
.step p { color: #AEB9C6; font-size: .96rem; }

/* ============================================================
   DUTY OF CARE — the wedge
   ============================================================ */

.wedge { display: grid; gap: 1.5rem; align-items: start; }
@media (min-width: 60rem) { .wedge { grid-template-columns: 1.15fr .85fr; gap: 3.5rem; } }

.wedge-body { display: grid; gap: 1rem; max-width: var(--measure); }
.wedge-body p { color: var(--muted); }

.warn {
  background: var(--surface);
  border: 1px solid var(--rule);
  border-left: 4px solid var(--action);
  padding: 1.4rem 1.35rem;
  display: grid;
  gap: .7rem;
}
.warn h3 { letter-spacing: -.02em; }
.warn p { font-size: .95rem; color: var(--muted); }
.warn .stat { font-size: 2.4rem; font-weight: 800; letter-spacing: -.04em; color: var(--action); line-height: 1; }

/* ============================================================
   AREAS
   ============================================================ */

.roads { display: grid; gap: 1.75rem; }
@media (min-width: 44rem) { .roads { grid-template-columns: repeat(4, 1fr); gap: 1.25rem; } }

.road-name {
  font-weight: 800; font-size: .92rem; letter-spacing: 0;
  color: var(--brand); padding-bottom: .5rem; border-bottom: 2px solid var(--brand);
  margin-bottom: .85rem;
}
.section-dark .road-name { color: var(--spark); border-bottom-color: var(--spark); }

.stops { list-style: none; margin: 0; padding: 0; display: grid; gap: .45rem; }
.stops li { font-size: .92rem; color: var(--muted); }
.section-dark .stops li { color: #AEB9C6; }

.areas-note { margin-top: 2rem; font-size: .95rem; color: var(--muted); max-width: var(--measure); }

/* ============================================================
   PHOTOGRAPHY
   Rob's own job photos. Every image ships with width/height so
   nothing on the page moves while they load.
   ============================================================ */

.photo { margin: 0; }
.photo img { width: 100%; height: auto; border-radius: 2px; background: #E6EAEF; }
.photo-card img { aspect-ratio: 4 / 3; object-fit: cover; }
.photo figcaption {
  font-family: var(--mono);
  font-size: .68rem;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--faint);
  padding-top: .5rem;
}

/* A lead image sits full-width of the measure, uncropped - these are portrait
   phone photos and cropping them to a banner throws away the subject. */
.photo-lead img { max-height: 34rem; object-fit: cover; object-position: center 40%; }

/* auto-fit with a capped track so a gallery of two doesn't sit stranded in the
   first quarter of a four-column grid, and a gallery of nine still forms a
   proper wall. The max stops two photos ballooning to half the page each. */
.photo-grid {
  display: grid;
  gap: 1rem;
  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
}
@media (min-width: 56rem) {
  .photo-grid { grid-template-columns: repeat(auto-fit, minmax(13rem, 18rem)); }
}

/* ============================================================
   BEFORE / AFTER
   ============================================================ */

.ba { display: grid; gap: 1rem; grid-template-columns: 1fr; }
@media (min-width: 44rem) { .ba { grid-template-columns: repeat(2, 1fr); gap: 1.25rem; } }

.ba-item { margin: 0; position: relative; }
/* Full-size variants rather than the 4:3 card crop — this is the one moment on
   the page where the photograph IS the argument, so it gets the sharp file. */
.ba-photo img { aspect-ratio: 4 / 3; object-fit: cover; border-radius: 2px; }
.ba-item figcaption { padding-top: 0; }

.ba-tag {
  position: absolute;
  top: .75rem; left: .75rem;
  font-family: var(--mono);
  font-size: .68rem;
  letter-spacing: .12em;
  text-transform: uppercase;
  padding: .35rem .7rem;
  color: #fff;
}
.ba-tag-before { background: var(--ink); }
.ba-tag-after  { background: var(--brand); }

/* ============================================================
   RECENT JOBS
   ============================================================ */

.job-cards { display: grid; gap: 1.25rem; grid-template-columns: 1fr; }
@media (min-width: 40rem) { .job-cards { grid-template-columns: repeat(2, 1fr); } }
/* Back to a plain three-up: snr_job_cards() now tops short rows up to the limit,
   so the stranded-single-card case this was working around can't happen, and
   auto-fit was breaking the row to 2+1 at container width. */
@media (min-width: 64rem) { .job-cards { grid-template-columns: repeat(3, 1fr); } }

.job-card {
  display: block;
  background: var(--surface);
  border: 1px solid var(--rule);
  border-top: 3px solid var(--brand);
  color: inherit;
  text-decoration: none;
  transition: transform .15s ease, border-top-color .15s ease;
}
.job-card:hover { border-top-color: var(--action); transform: translateY(-2px); }
.job-card .photo img { border-radius: 0; }
/* Flex column so the "see the job" link sits on the same baseline across a row
   whatever the title wraps to — a grid of cards with the links at four
   different heights is the thing that makes a site look homemade. */
.job-card { display: flex; flex-direction: column; }
.job-card-body { padding: 1.1rem 1.25rem 1.35rem; display: flex; flex-direction: column; gap: .5rem; flex: 1; }
.job-card .more { margin-top: auto; padding-top: .35rem; }
.job-card-body h2, .job-card-body h3 { font-size: 1.12rem; margin: 0; line-height: 1.3; }
.job-town {
  font-family: var(--mono);
  font-size: .68rem;
  letter-spacing: .09em;
  text-transform: uppercase;
  color: var(--action);
  margin: 0;
}
.job-result { color: var(--muted); font-size: .95rem; margin: 0; }
.job-card .more {
  font-family: var(--mono); font-size: .72rem; letter-spacing: .08em;
  text-transform: uppercase; color: var(--action);
}

.band-more { margin-top: 2rem; }
.band-more a { font-family: var(--mono); font-size: .78rem; letter-spacing: .06em; text-transform: uppercase; }

/* ============================================================
   PROSE + RAIL

   Used by the service, town, guide and job templates — it is its
   own thing, not part of Recent Jobs above.

   Prose was running at 60ch down the left with half the page empty
   beside it. The rail fills that column with the things a customer
   actually scans for, plus the action.

   Named for what it does rather than for the one template it started
   on: the service and town pages — the two that take the search
   traffic — now use it as well, and a service page laid out with a
   class called .job-facts is how a stylesheet starts lying to you.
   ============================================================ */
.with-rail { display: grid; gap: 2.5rem; }
@media (min-width: 62rem) {
  .with-rail { grid-template-columns: minmax(0, 1fr) 19rem; gap: 4rem; align-items: start; }
  /* The grid area still spans the full row, so the rail has somewhere to
     travel; align-self keeps the box itself its natural height. Offset clears
     the sticky header. */
  .rail { position: sticky; top: calc(var(--header) + 1.4rem); align-self: start; }
}

.rail {
  background: var(--surface);
  border: 1px solid var(--rule);
  border-top: 3px solid var(--spark);
  padding: 1.5rem 1.4rem;
}
.rail-title {
  font-family: var(--mono);
  font-size: .7rem;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--faint);
  margin: 0 0 1rem;
}
.rail dl { margin: 0 0 1.4rem; display: grid; gap: .1rem; }
.rail dt {
  font-family: var(--mono);
  font-size: .66rem;
  letter-spacing: .09em;
  text-transform: uppercase;
  color: var(--faint);
  margin-top: .85rem;
}
.rail dt:first-of-type { margin-top: 0; }
/* 600 rather than bold, and a notch down from body size: the rail carries a
   couple of sentence-length values as well as one-word ones, and at full size
   and full weight those turned into three lines of shouting in a 19rem
   column. */
.rail dd { margin: 0; font-weight: 600; font-size: .95rem; line-height: 1.45; }
/* A second, quieter action under the primary one. */
.rail .btn + .btn { margin-top: .55rem; }
.rail-note { color: var(--muted); font-size: .95rem; margin: 0 0 1.2rem; }

.btn-block { display: block; text-align: center; width: 100%; }

/* Padding, not just margin: at .72rem this link's box was 18px tall, under the
   24px minimum target size. The padding is what makes it tappable; the margin
   is reduced by the same amount so the spacing above it is unchanged. */
.rail-link {
  display: inline-block; margin-top: .65rem;
  padding: .35rem 0;
  font-family: var(--mono); font-size: .72rem;
  letter-spacing: .08em; text-transform: uppercase;
}
/* The quiet line at the bottom of a rail — "Last checked July 2026". */
.rail-foot {
  margin: 1.4rem 0 0; padding-top: 1rem;
  border-top: 1px solid var(--rule);
  font-family: var(--mono); font-size: .68rem;
  letter-spacing: .06em; text-transform: uppercase; color: var(--faint);
}

.section-tight { padding-top: 0; }
.section-tight:first-of-type { padding-top: 3.5rem; }

.pagination { margin-top: 2.5rem; display: flex; gap: .5rem; flex-wrap: wrap; }
.pagination .page-numbers {
  padding: .5rem .85rem; border: 1px solid var(--rule); background: var(--surface);
  text-decoration: none; font-size: .9rem;
}
.pagination .current { background: var(--brand); color: #fff; border-color: var(--brand); }

/* ============================================================
   CTA BAND
   ============================================================ */

.band { background: var(--brand); color: #fff; }

/* Copper clears 3:1 against paper (4.69) and white (5.04). It does not clear it
   against any of the dark surfaces, so on those the fill cannot be the thing
   that marks it out as a button and it gets an edge instead. The label is 5:1
   on copper throughout; this is about the shape being findable, not the words
   being readable.

   REMEASURED AFTER THE BLUE REBRAND. This rule used to name the green band
   (1.86) and the menu panel (2.82) as the only two exceptions, and put steel at
   a passing 3.34. Both of those numbers belonged to the old palette:

     --action on --steel   2.96   (was quoted as 3.34 — it is not, and this is
                                   the header call button and every dark hero)
     --action on --brand   1.35   (the band is --brand now, not green; worse
                                   than the 1.86 the exception was written for)

   So the exception is not a two-surface special case any more, it is every dark
   surface: the header, the page heroes, the dark sections and the mobile action
   bar as well as the band and the menu. .btn already carries a 2px transparent
   border, so colouring it costs no layout.

   .65 white composites to 3.87 on --brand and comfortably higher on --steel and
   --ink, so one value covers all of them and there is no per-surface tuning to
   keep in step.

   It stays copper rather than going hi-vis: copper means "the action"
   everywhere else on this site, and the final CTA is the wrong place to start
   teaching a second colour. Repointing --action itself would fix this too, but
   that is a branding decision and this is not.

   The selector list is surfaces, not templates, and .hero-actions is on it
   rather than .hero/.page-hero because it is the one class every hero button
   shares — front page, service, town, guide, job, the 404 and the archive —
   and it is also inside .cta-inline, which is a steel slab sitting in an
   otherwise light section. Checked: there is no .hero-actions anywhere on a
   light background, so this cannot paint a white ring onto paper. */
.band .btn-primary,
.menu-panel .btn-primary,
.site-header .btn-primary,
.hero-actions .btn-primary,
.section-dark .btn-primary { border-color: rgba(255,255,255,.65); }
.band-inner { display: grid; gap: 1.5rem; padding: 3rem 0; align-items: center; }
@media (min-width: 56rem) { .band-inner { grid-template-columns: 1fr auto; gap: 3rem; } }
.band h2 { color: #fff; }
/* #B4C4DA was 3.84 on --brand — body copy, so it owes 4.5. It read fine on the
   old green band and was never remeasured against the blue one. #CBD6E2 is
   4.62 and still visibly a step down from the white heading above it, which is
   the only job the dimming was doing. */
.band p { color: #CBD6E2; margin-top: .6rem; max-width: 46ch; }
.band-actions { display: grid; gap: .75rem; }
@media (min-width: 30rem) { .band-actions { grid-auto-flow: column; } }

/* ============================================================
   FOOTER
   ============================================================ */

.site-footer { background: var(--ink); color: #98A2AE; padding: 3rem 0 2rem; font-size: .92rem; }
.footer-grid { display: grid; gap: 2rem; }
@media (min-width: 40rem) { .footer-grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 62rem) { .footer-grid { grid-template-columns: 1.6fr 1fr 1fr 1.2fr; gap: 2.5rem; } }
.site-footer h3 { color: #fff; font-size: .8rem; letter-spacing: .1em; text-transform: uppercase; font-family: var(--mono); font-weight: 400; margin-bottom: .9rem; }
.site-footer a { color: #C3CCD8; text-decoration: none; }
.site-footer a:hover { color: #fff; text-decoration: underline; }
.footer-links { list-style: none; margin: 0; padding: 0; display: grid; gap: .5rem; }

.footer-licence {
  margin-top: 2.5rem; padding-top: 1.5rem; border-top: 1px solid var(--rule-dark);
  display: flex; flex-wrap: wrap; gap: .75rem 2rem;
}
.footer-licence div { font-family: var(--mono); font-size: .72rem; }
.footer-licence b { display: block; font-size: .6rem; letter-spacing: .1em; text-transform: uppercase; color: var(--faint-dark); font-weight: 400; }

.colophon { margin-top: 1.5rem; font-size: .78rem; color: var(--faint-dark); }

/* ============================================================
   STICKY ACTION BAR — mobile only.
   The customer is standing in a garage holding the thing they
   want gone. Never make them hunt for the button.
   ============================================================ */

.actionbar {
  position: fixed;
  bottom: 0; left: 0; right: 0;
  z-index: 50;
  display: grid;
  grid-template-columns: 1fr 1fr;
  border-top: 2px solid var(--ink);
  box-shadow: 0 -6px 20px rgba(0,0,0,.16);
}
@media (min-width: 48rem) { .actionbar { display: none; } }

.actionbar a {
  display: flex; align-items: center; justify-content: center; gap: .5rem;
  padding: 1rem .5rem;
  font-weight: 700; font-size: 1rem; text-decoration: none;
  min-height: var(--bar);
}
.ab-call { background: var(--ink); color: #fff; }
.ab-wa { background: var(--action); color: #fff; }

.ico { width: 1.15em; height: 1.15em; flex: none; fill: currentColor; }

/* ============================================================
   TRUST BAR
   ============================================================ */

/* Sits directly under the hi-vis licence strip. It used to be a second solid
   band — yellow then green, back to back — which made the page look striped and
   spent the hi-vis moment we deliberately reserve for one job. Now it reads as
   a spec strip on paper: quiet, and the yellow above it stays the loud one. */
.trustbar { background: var(--surface); border-bottom: 1px solid var(--rule); }
.trust-inner {
  display: grid; gap: 1.25rem 2rem; padding: 1.5rem 0;
  grid-template-columns: repeat(2, 1fr);
}
@media (min-width: 52rem) { .trust-inner { grid-template-columns: repeat(4, 1fr); } }
.trust-item {
  display: grid; gap: .15rem;
  padding-left: .9rem;
  border-left: 2px solid var(--brand);
}
.trust-fig { font-weight: 800; font-size: 1.05rem; letter-spacing: -.02em; color: var(--ink); }
.trust-lab { font-size: .82rem; color: var(--muted); }

/* ============================================================
   FAQ
   ============================================================ */

.faq { display: grid; gap: 0; max-width: 52rem; border-top: 1px solid var(--rule); }
.faq-item { border-bottom: 1px solid var(--rule); }

/* Nine questions in one column is a very long ladder with half the page empty
   beside it. Two columns halves the scroll and fills the width.

   align-items: start so each rule hugs its own question — with the default
   stretch, opening one answer drags its neighbour's rule down the page and
   leaves it floating under nothing. The top rule moves onto the first item of
   each column, since .faq no longer has a single top edge. */
@media (min-width: 64rem) {
  .faq {
    max-width: none;
    grid-template-columns: 1fr 1fr;
    column-gap: 4rem;
    align-items: start;
    border-top: 0;
  }
  .faq-item:nth-child(-n+2) { border-top: 1px solid var(--rule); }
}
.faq-item summary {
  cursor: pointer; list-style: none;
  padding: 1.15rem 2.5rem 1.15rem 0; position: relative;
  font-weight: 700; font-size: 1.05rem; letter-spacing: -.01em;
}
.faq-item summary::-webkit-details-marker { display: none; }
.faq-item summary::after {
  content: "+"; position: absolute; right: .25rem; top: 50%; transform: translateY(-50%);
  font-weight: 400; font-size: 1.6rem; color: var(--action); line-height: 1;
}
.faq-item[open] summary::after { content: "\2013"; }
.faq-a { padding: 0 2.5rem 1.25rem 0; }
.faq-a p { color: var(--muted); }

/* ============================================================
   WHAT DECIDES THE PRICE
   Four factors on a rule, then the honest note about why there
   is no from-price. The note is the part that does the work, so
   it gets the dark slab and the hi-vis edge rather than sitting
   as a fifth paragraph nobody reaches.
   ============================================================ */

.factors { display: grid; gap: 1.75rem; }
@media (min-width: 40rem) { .factors { grid-template-columns: repeat(2, 1fr); gap: 2rem 2.25rem; } }
@media (min-width: 64rem) { .factors { grid-template-columns: repeat(4, 1fr); gap: 1.75rem; } }

.factor {
  display: grid; gap: .5rem; align-content: start;
  border-top: 2px solid var(--brand);
  padding-top: 1rem;
}
.factor-n {
  font-family: var(--mono);
  font-size: .72rem;
  letter-spacing: .1em;
  color: var(--action);
}
.factor p { color: var(--muted); font-size: .95rem; }

.pricenote {
  margin-top: 2.75rem;
  padding: 1.85rem 1.6rem;
  background: var(--steel);
  color: #fff;
  border-radius: 4px;
  border-left: 4px solid var(--spark);
  display: grid; gap: .85rem;
  max-width: 56rem;
}
.pricenote h3 { color: #fff; font-size: 1.2rem; }

/* Full width once the factors above it are a four-up row: a slab that stops
   two thirds of the way across, under a grid that doesn't, reads as a mistake
   rather than a margin. The title takes its own column so the prose keeps a
   readable measure instead of running the whole 74rem. */
@media (min-width: 64rem) {
  .pricenote {
    max-width: none;
    grid-template-columns: minmax(0, .7fr) minmax(0, 1.3fr);
    column-gap: 3.5rem;
    padding: 2.25rem 2.5rem 2.35rem 2.25rem;
  }
  .pricenote h3 { grid-row: span 2; font-size: 1.35rem; }
}
.pricenote p { color: #AEB9C6; font-size: .97rem; }
.pricenote strong { color: var(--spark); }

/* ============================================================
   SKIP COMPARISON
   A real <table> — this is two things compared across seven
   attributes, which is what tables are for and what makes it
   navigable to a screen reader. It scrolls rather than reflows
   on a phone: a comparison that stacks into one column stops
   being a comparison.
   ============================================================ */

/* Scroll shadows. The table scrolls sideways on a phone and there was nothing
   on screen saying so — the last column simply ended at the edge of the
   viewport looking like the end of the table, which on a comparison is the one
   column you most need to know exists.

   Two pairs of gradients: a solid one attached to the content, which slides
   away as you scroll, uncovering a shadow attached to the element. The effect
   is that a shadow appears on precisely the side that still has content. No
   script, no scroll listener. */
.vs-scroll {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  background:
    linear-gradient(to right, var(--surface) 40%, rgba(255,255,255,0)) left center,
    linear-gradient(to left,  var(--surface) 40%, rgba(255,255,255,0)) right center,
    radial-gradient(farthest-side at 0 50%,   rgba(16,22,20,.26), rgba(16,22,20,0)) left center,
    radial-gradient(farthest-side at 100% 50%, rgba(16,22,20,.26), rgba(16,22,20,0)) right center;
  background-repeat: no-repeat;
  background-size: 2.5rem 100%, 2.5rem 100%, 1.25rem 100%, 1.25rem 100%;
  background-attachment: local, local, scroll, scroll;
}
/* It is focusable, so it must show focus. */
.vs-scroll:focus-visible { outline-offset: 0; }

.vs {
  width: 100%;
  min-width: 32rem;
  border-collapse: collapse;
  font-size: .95rem;
}
.vs th, .vs td {
  text-align: left;
  padding: .9rem 1rem .9rem 0;
  border-bottom: 1px solid var(--rule);
  vertical-align: top;
}
.vs thead th {
  font-family: var(--mono);
  font-size: .68rem;
  letter-spacing: .1em;
  text-transform: uppercase;
  /* --muted rather than the quieter --faint: these two words are the labels
     that tell you which column is which, so they carry more than a caption's
     worth of meaning and get more than a caption's worth of contrast. */
  color: var(--muted);
  font-weight: 400;
  border-bottom: 2px solid var(--rule);
}
.vs tbody th { font-weight: 700; color: var(--ink); width: 11rem; }
.vs td { color: var(--muted); }

/* Our column is tinted and set in the ink colour so the eye lands on it without
   the table shouting. The header cell carries the green rule to mark the edge. */
.vs .vs-us {
  background: rgba(31, 90, 168, .05);
  color: var(--ink);
  font-weight: 600;
  padding-left: 1rem;
}
.vs thead .vs-us { color: var(--brand); font-weight: 700; border-bottom-color: var(--brand); }
.vs tbody tr:last-child th, .vs tbody tr:last-child td { border-bottom: 0; }

.vs-note {
  margin-top: 1.6rem;
  font-size: .92rem;
  color: var(--muted);
  max-width: var(--measure);
}

/* ============================================================
   REVIEWS
   Renders only when there are real ones — see snr_reviews().
   ============================================================ */

.quotes { display: grid; gap: 1.25rem; }
@media (min-width: 46rem) { .quotes { grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr)); } }

.quote {
  margin: 0;
  background: var(--paper);
  border: 1px solid var(--rule);
  border-top: 3px solid var(--spark);
  padding: 1.5rem 1.4rem;
  display: grid; gap: 1.1rem;
  align-content: start;
}
.quote blockquote { margin: 0; }
.quote blockquote p { font-size: 1.04rem; color: var(--ink); }
.quote blockquote p::before { content: "\201C"; }
.quote blockquote p::after  { content: "\201D"; }

.quote figcaption { display: flex; flex-wrap: wrap; gap: .25rem .8rem; align-items: baseline; }
.quote-name { font-weight: 700; font-size: .92rem; }
.quote-town { font-size: .88rem; color: var(--muted); }
.quote-src {
  font-family: var(--mono);
  font-size: .64rem;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--faint);
}

/* ============================================================
   INLINE CTA
   ============================================================ */

.cta-inline {
  background: var(--steel); color: #fff; border-radius: 4px;
  padding: 2.5rem 1.75rem; display: grid; gap: 1.5rem; text-align: center;
  justify-items: center;
}
.cta-inline h2 { color: #fff; }
.cta-inline .hero-actions { justify-content: center; }

.price-tag {
  display: inline-flex; align-items: center; padding: 0 1rem; min-height: 3.25rem;
  font-family: var(--mono); font-size: .9rem; color: var(--spark);
  border: 1px dashed rgba(134,180,236,.5); border-radius: 3px;
}

/* ============================================================
   CONTACT
   ============================================================ */

.contact-grid { display: grid; gap: 2.5rem; }
@media (min-width: 56rem) { .contact-grid { grid-template-columns: 1.3fr 1fr; gap: 3.5rem; } }

.cform { display: grid; gap: 1.1rem; max-width: 34rem; }
.field { display: grid; gap: .4rem; }
.field label { font-weight: 600; font-size: .9rem; }
.field input, .field select, .field textarea {
  font: inherit; font-size: 1rem; padding: .8rem .9rem;
  border: 1px solid var(--field-line); border-radius: 3px; background: var(--surface); color: var(--ink);
  width: 100%;
}
.field input:focus, .field select:focus, .field textarea:focus { outline: 2px solid var(--action); outline-offset: 1px; border-color: var(--action); }
.cform .btn { justify-self: start; }
.cform-note { font-size: .9rem; color: var(--muted); }
.hp { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }

.contact-side { display: grid; gap: 1rem; align-content: start; }
/* The heading here is .rail-title, shared with the rail on the landing
   templates — it was a near-identical duplicate rule under a different tag. */
.contact-big {
  display: block; background: var(--action); color: #fff; text-decoration: none;
  font-weight: 700; font-size: 1.1rem; padding: 1.1rem 1.25rem; border-radius: 3px;
}
.contact-big.alt { background: var(--ink); }
.contact-big:hover { background: var(--action-lift); }
.contact-big.alt:hover { background: var(--steel-2); }
.contact-facts { margin: 1rem 0 0; display: grid; gap: .15rem; }
.contact-facts dt { font-family: var(--mono); font-size: .64rem; letter-spacing: .1em; text-transform: uppercase; color: var(--faint); margin-top: .9rem; }
.contact-facts dd { margin: 0; color: var(--muted); }
.contact-facts dd.mono { font-family: var(--mono); font-size: .85rem; color: var(--ink); }

/* ============================================================
   INNER PAGES
   ============================================================ */

.crumbs { background: var(--steel-2); color: #AEB9C6; font-size: .8rem; }
.crumbs ol { list-style: none; margin: 0; padding: .7rem 0; display: flex; flex-wrap: wrap; gap: .4rem; }
.crumbs li { display: flex; gap: .4rem; align-items: center; }
.crumbs li:not(:last-child)::after { content: "/"; color: var(--faint-dark); margin-left: .4rem; }
/* Padded to clear the 24px minimum target size. The links were 20px tall,
   which is fine for a link inside a sentence and not fine for a row of
   standalone links being tapped with a thumb. */
.crumbs a { color: #AEB9C6; text-decoration: none; padding: .3rem 0; }
.crumbs a:hover { color: #fff; text-decoration: underline; }
.crumbs li[aria-current] { color: #fff; }

.page-hero { background: var(--steel); color: #fff; padding: 2.75rem 0; }
.page-hero h1 { max-width: 20ch; }
.page-hero .lede { margin-top: 1rem; }

.prose { max-width: var(--measure); display: grid; gap: 1.1rem; }
/* A heading inside a 60ch reading column is not a section masthead and should
   not be set like one — at the shared h2 size it came out nearly as large as
   the page title, in a column half its width. These step down to sit above
   their own paragraphs rather than compete with the <h1>. */
.prose h2 { margin-top: 1.5rem; font-size: clamp(1.5rem, 2.4vw, 1.85rem); letter-spacing: -.025em; }
.prose h3 { margin-top: 1rem; font-size: 1.1rem; }
.prose > :first-child { margin-top: 0; }
.prose ul { margin: 0; padding-left: 1.2rem; display: grid; gap: .5rem; color: var(--muted); }
.prose p { color: var(--muted); }
.prose strong { color: var(--ink); }

/* ============================================================
   MOTION

   One rule, at the end, covering everything that moves.

   It used to be three scattered exceptions and .job-card was
   never in any of them — so a reader who had asked their OS to
   stop animations still got cards lifting under the cursor on
   the archive and on every jobs band. Collected here so the next
   transition added to this file is covered by default rather
   than by remembering.

   scroll-behavior lives here too: it makes the skip link and any
   in-page anchor land smoothly under the sticky header, and it
   is exactly the kind of thing reduced-motion is asking about.
   ============================================================ */

@media (prefers-reduced-motion: no-preference) {
  html { scroll-behavior: smooth; }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
    scroll-behavior: auto !important;
  }
  /* Transforms are the part that actually causes trouble, so they go too
     rather than merely happening instantly. */
  .card:hover, .job-card:hover, .btn:active { transform: none; }
}

/* ============================================================
   PRINT
   One page gets printed in this business and it is /licensed-and-legal/,
   usually by someone checking us against another quote. Dark
   slabs cost a fortune in toner and the licence numbers were
   coming out white on white, so the whole thing goes to ink on
   paper and every FAQ answer is forced open.
   ============================================================ */

@media print {
  .site-header, .actionbar, .menu, .crumbs, .skip,
  .band, .cta-inline, .hero-actions, .band-more,
  .btn, .contact-big, .cform { display: none !important; }

  body {
    background: #fff;
    color: #000;
    font-size: 11pt;
    padding-bottom: 0;
  }

  .section { padding: 1.2rem 0; }
  .wrap { max-width: none; padding: 0; }

  .hero, .section-dark, .page-hero, .pricenote, .cta-inline {
    background: #fff !important;
    color: #000 !important;
    border-left: 0;
  }
  .hero h1, .page-hero h1, .section-dark h2, .step h3, .pricenote h3 { color: #000 !important; }
  .lede.on-dark, .step p, .stops li, .pricenote p { color: #333 !important; }
  .eyebrow.on-dark, .hero h1 em, .pricenote strong { color: #000 !important; }

  /* The reason the page is being printed. */
  .licence, .footer-licence { background: #fff; color: #000; border: 1px solid #000; padding: .75rem; }
  .site-footer { background: #fff; color: #000; padding-top: 1rem; }
  .site-footer h3, .site-footer a, .footer-licence b, .colophon { color: #000; }
  .footer-grid, .site-header .nav { display: none; }

  /* Photographs are the argument on screen and a waste of toner on paper. */
  .photo, .hero-photo, .ba, .photo-grid, .card-photo { display: none; }

  /* A closed <details> prints as a headline with no answer, which makes the FAQ
     look broken on paper. */
  .faq-item > .faq-a { display: block !important; }
  .faq-item summary::after { content: ""; }

  .vs-scroll { overflow: visible; background: none; }
  .vs { min-width: 0; }
  .vs .vs-us { background: none; }

  /* The rail is worth printing — it carries the licence number and the hours,
     which is most of why anyone prints a page here. It just must not try to
     stick to a viewport that doesn't exist on paper. */
  .rail { position: static; border: 1px solid #000; }
  .with-rail { display: block; }
  .rail .btn { display: none !important; }

  a { color: #000; text-decoration: underline; }
  /* Printed links are dead ends without the URL. Internal ones are noise. */
  .prose a[href^="http"]::after { content: " (" attr(href) ")"; font-size: .85em; word-break: break-all; }

  .section, .wedge, .warn, .factor, .quote { break-inside: avoid; }
  h1, h2, h3 { break-after: avoid; }
}
