/* ---------------------------------------------------------------------------
   Pro Plumbing — design tokens
   ---------------------------------------------------------------------------
   Sampled from Greg's logo rather than guessed. The two hero colours are the
   ones the mark is actually built from:

     #29AAE1  the signature blue — the swoosh, the wordmark, the houses
     #F4891F  the orange end of the warm swoosh
     #C9291F  the red end of it
     #0070B8  the deeper blue in the wordmark and the P's stem
     #001D32  the near-black blue the whole mark sits on

   The ramps around them are extended, not sampled, and kept sympathetic: the
   neutrals are tinted toward the brand blue so greys sit with the mark rather
   than against it.

   ⚠ THE LOGO'S OWN COLOURS DO NOT WORK AS TEXT ON WHITE. Measured:
       #29AAE1 on white = 2.65:1      #F4891F on white = 2.49:1
   Both are under even the 3:1 minimum for a border or an icon, let alone the
   4.5:1 text needs. That is not a fault in the logo — it is designed to sit on
   black, where the same colours measure 7.13:1 and 7.59:1. It does mean the
   brand blue and orange are DECORATIVE on a light page: large fills, the mark
   itself, a dark band. Anything that has to be read or perceived uses
   --blue-700 or --flame-600 and darker, which are measured below.
--------------------------------------------------------------------------- */

/* Archivo Narrow, self-hosted, one variable file covering both weights the
   site uses. See src/assets/fonts/README.md for why this face, why headings
   only and why not a Google Fonts <link>. `swap` because a plumbing emergency
   should read the heading in the system font rather than wait for ours. */
@font-face {
  font-family: 'Archivo Narrow';
  src: url('../fonts/archivo-narrow-latin.woff2') format('woff2-variations');
  font-weight: 600 700;
  font-style: normal;
  font-display: swap;
}

:root {
  /* Brand blue. 500 is the logo's own blue; 700 is the functional one. */
  --blue-50: #eaf7fd;
  --blue-100: #cfeefa;
  --blue-200: #a3ddf4;
  --blue-300: #6ac6ec;
  --blue-400: #3fb6e8;
  --blue-500: #29aae1; /* logo */
  --blue-600: #1490ce; /* 3.55:1 on white — borders, large UI, not body text */
  --blue-700: #0070b8; /* logo; 5.24:1 on white — links, buttons */
  --blue-800: #00568c; /* 7.75:1 on white */
  --blue-900: #003a61;
  --blue-950: #001d32; /* logo */

  /* The warm swoosh, orange through to red. */
  --flame-50: #fef3e8;
  --flame-100: #fde2c7;
  --flame-200: #fbc493;
  --flame-300: #f8a55b;
  --flame-400: #f4891f; /* logo — decorative on white (2.49:1), fine on dark */
  --flame-500: #e85c25; /* 3.51:1 on white AND 5.38:1 on dark — the focus ring */
  --flame-600: #d6372a; /* 4.73:1 on white — the accent button */
  /* Not ours: WhatsApp's own dark green, for the one button that goes there.
     Its bright green (#25d366) is 1.98:1 on white, so it is not a fill white
     text can sit on; this darker step from the same brand measures 7.67:1
     and the hover step 9.61:1. Kept as its own token rather than borrowed
     from the blue ramp because a WhatsApp button that is not green is a
     button nobody recognises. */
  --whatsapp-700: #075e54;
  --whatsapp-800: #064e46;
  --flame-700: #c9291f; /* logo */
  --flame-800: #a2211a;

  /* Neutrals, tinted toward the brand blue. */
  --slate-50: #f5f8fa;
  --slate-100: #e8eef3;
  --slate-200: #d2dde6;
  --slate-300: #aebfcd;
  --slate-400: #7e94a6;
  --slate-500: #5a7186; /* 5.07:1 on white */
  --slate-600: #445a6d; /* 7.17:1 on white */
  --slate-700: #334656;
  --slate-800: #22303c; /* 13.50:1 on white — body text */
  --slate-900: #141f28;
  --slate-950: #07121c; /* the ground the logo was drawn for */

  /* Spacing, on a 0.25rem base. The number IS the multiple, so --space-6 is
     6 x 0.25 = 1.5rem and the name carries its own arithmetic.

     This scale was not imposed, it was measured: 54 of the 80 spacing values
     already in this file sat on these seven steps before any token existed.
     Formalising it is a description of what the stylesheet was already doing.

     The off-scale values are gone now, resolved per use rather than by a
     blanket round, because a single rounding rule would have made some of
     them worse. Two rules, and they pull in opposite directions on purpose:

       - Padding and gaps on anything a thumb touches ROUND UP (0.6 and 0.7
         both go to 0.75). The review flags several targets under the 44px
         convention, so on an interactive element the nearer step is not
         automatically the better one.
       - Everything else rounds to the NEAREST step (0.35 to 0.25, 0.4 and
         0.45 to 0.5, 0.85 to 0.75).

     The three heading sizes that were 1.1, 1.15 and 1.125rem are now one
     token. They were three decisions rendering as one thing. */
  --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-12: 3rem;

  /* The thumb, and deliberately not a --space-* step: the scale above measures
     the gaps BETWEEN things, and the smallest square a finger hits reliably is
     a property of the finger rather than of the layout. In px for the same
     reason -- it should not grow with a reader's text size. Three components
     need it now (the footer's icon row, the header's number and menu button,
     the open menu's rows), which is one more than it takes to stop being a
     component's private number. */
  --target: 44px;

  /* Type. Same rule: only the sizes already in use, named rather than
     collapsed. 0.9/0.95 and 1.1/1.125/1.15 are near-duplicates and the
     review reports them; they are not merged here because a 2% type change
     is still a type change. */
  --text-sm: 0.9rem;
  --text-base: 1rem;
  --text-lg: 1.125rem;
  --text-h1: clamp(1.6rem, 4vw, 2.2rem);

  /* Roles. Templates use these, not the ramps, so a rebrand is one block.
     `--heading` exists so a heading follows its band: `.band--dark` redefines
     it and every h1/h2 inside flips without being told. */
  /* Two measures. 68ch is a reading measure and stays the default for prose.
     Photography and grids get the wide one — a hero pinned to a paragraph's
     width is the difference between a page that looks sure of itself and one
     that looks like a document. */
  /* The display face is a role too, with the system stack behind it, so a
     heading still renders in something sensible if the font never arrives. */
  --font-display: 'Archivo Narrow', 'Arial Narrow', system-ui, sans-serif;

  /* Three, and the distinction matters. --measure is the CONTAINER width and is
     deliberately overridable: `.band--wide` raises it so a band can use the
     page. --measure-text is the READING width and is never overridden, because
     a paragraph does not get easier to read inside a wide band — writing a
     prose cap as var(--measure) is how the service descriptions ended up 150
     characters long. */
  --measure: 68ch;
  --measure-wide: 74rem;
  --measure-text: 68ch;

  --heading: var(--slate-900);
  --ink: var(--slate-800);
  --ink-muted: var(--slate-600);
  --surface: #fff;
  --surface-sunk: var(--slate-50);
  --rule: var(--slate-200);
  --link: var(--blue-700);
  --link-hover: var(--blue-800);
  --ring: var(--flame-500);
}

* {
  box-sizing: border-box;
}
body {
  margin: 0;
  font:
    16px/1.65 system-ui,
    -apple-system,
    'Segoe UI',
    sans-serif;
  color: var(--ink);
  background: var(--surface);
}
/* The full-bleed grid. `main` is the whole width and lays out three columns:
   slack, the reading measure, slack. Every direct child lands in the middle
   column without asking, so nothing that existed before this had to change —
   and a section that wants the whole width says `.band` and gets it, with the
   same three columns nested inside so its own content keeps the measure.

   This is what makes the alternating colour bands possible at all. A section
   cannot carry a full-width background while its parent is capped at 68ch. */
main {
  display: grid;
  grid-template-columns: 1fr min(var(--measure-wide), calc(100% - 2 * var(--space-4))) 1fr;
  padding: var(--space-8) 0 var(--space-12);
}
main > * {
  grid-column: 2;
}

/* One left edge. The container is wide so every band, grid and paragraph
   starts at the same x; the READING measure is then applied to the things
   that are read, rather than to the box around them. Three left edges on one
   page is an accident; this is the fix for it.

   Grids opt out below, because a grid capped at a paragraph's width is a
   paragraph-shaped grid. */
main p,
main h1,
main h2,
main h3,
main dl,
main .callout {
  max-width: var(--measure-text);
}
.work,
.gallery,
.hero-inner {
  max-width: none;
}
/* Inside a box the box is already the measure. */
.callout p,
.callout h2 {
  max-width: none;
}

/* A band: full width, its own vertical rhythm, the measure restored inside. */
.band {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: 1fr min(var(--measure), calc(100% - 2 * var(--space-4))) 1fr;
  padding: var(--space-12) 0;
  margin: var(--space-12) 0;
}
.band > * {
  grid-column: 2;
}
.band > :first-child {
  margin-top: 0;
}
.band > :last-child {
  margin-bottom: 0;
}

/* The dark band. Nothing here is a new colour: --blue-950 is the ground Greg's
   logo was drawn for, and the roles are redefined rather than overridden per
   element — which is the whole reason the stylesheet has role tokens. Anything
   inside a dark band inherits the flip, including partials that know nothing
   about it. */
.band--dark {
  background: var(--blue-950);
  --ink: var(--slate-50);
  --ink-muted: var(--blue-200);
  --heading: var(--slate-50);
  --link: var(--blue-300);
  --link-hover: var(--blue-100);
  --rule: rgba(255, 255, 255, 0.18);
  --surface: var(--blue-900);
  --surface-sunk: var(--blue-900);
  color: var(--ink);
}

/* The hero is a band too, but split: the statement and the call on one side,
   one tall photograph on the other. Split rather than full-bleed because
   Greg's best originals are portrait at 1536x2048 and his best landscape is
   960x720 — which would be upscaled to fill a wide band, and look soft exactly
   where the page is trying to look sure of itself. See planning/design/. */
.hero,
.band--wide {
  --measure: var(--measure-wide);
}
.hero {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: 1fr min(var(--measure), calc(100% - 2 * var(--space-4))) 1fr;
  padding: 0;
  margin: 0 0 var(--space-12);
}
.hero-inner {
  grid-column: 2;
  display: grid;
  gap: var(--space-8);
  align-items: center;
}
.hero-shot {
  margin: 0;
}
.hero-shot img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: 8px;
}
/* The phone treatment the brief asks for (decision 1): the photograph first and
   running edge to edge, which is the reference's own mobile hero and what every
   competitor site does. Only the VISUAL order moves — the markup stays
   text-then-figure, so the page still reads and tabs heading-first and a screen
   reader gets the sentence before the picture. */
@media (max-width: 47.99em) {
  .hero-shot {
    order: -1;
    /* Two gutters to escape, not one. `main` pads --space-4 either side on a
       phone, and the hero's own three-column grid then insets its content by
       another --space-4 — so the figure is exactly 2 x --space-4 in from each
       edge and pulling that back lands it on the viewport, with nothing left
       over to overflow. Derived from the token rather than typed as -32px so
       it cannot quietly stop being right if either gutter changes. */
    margin-inline: calc(-2 * var(--space-4));
    /* And up, flush to the header. This cancels main's own top padding, which
       is `padding: var(--space-6) var(--space-4) var(--space-8)` in the phone
       block near the foot of this file — the SAME --space-6, so if that padding
       is ever retuned this has to move with it and whoever touches one should
       come and look at the other. Left as it was, the page opened with an 18px
       white strip between the dark header and a photograph that is otherwise
       edge to edge, and a gap that size reads as a gap rather than as a break.
       Only the figure is pulled up; the text below it keeps main's rhythm. */
    margin-top: calc(-1 * var(--space-6));
  }
  .hero-shot img {
    /* The original is 900x1200. At its native shape that is 500px tall at 375
       and the h1 starts below the fold, which is the one thing the hero cannot
       do. 4/3 costs the floor and the bottom of the toilet and buys the
       eyebrow, the heading and the lead on the first screen.

       16/10 now, 47px shorter again, and bought deliberately: the first screen
       has to end on a sliver of the first door — its rule and the first line of
       its heading above the call bar — so that a page which is mostly below the
       fold says so. The extra 47px come off the bottom of the frame (the crop
       window moves from source rows 131-806 to 160-722), which costs a little
       more worktop and nothing that identifies the room. Measured at 375x812
       against the call bar's own top edge; if that bar ever changes height,
       re-measure rather than re-derive.

       25% rather than centre: centre would crop symmetrically and take the top
       off the wall units. This holds the window just under the ceiling, so the
       illuminated mirror and the basin — the two things that read as "fitted
       bathroom" at a glance — are both in it. Judged by looking at the render,
       not computed. */
    aspect-ratio: 16 / 10;
    object-fit: cover;
    object-position: center 25%;
    /* Edge to edge means no radius: a rounded corner cut off by the viewport
       edge reads as a rendering fault rather than as a card. */
    border-radius: 0;
  }
  /* --space-6 rather than --space-8 between the photograph and the words. The
     figure is full-bleed here, so its own edge is the viewport and it does not
     need a desktop column's worth of air under it to read as separate. Eight
     of the pixels the first screen is short. */
  .hero-inner {
    gap: var(--space-6);
  }
}
@media (min-width: 48em) {
  .hero-inner {
    grid-template-columns: 1.1fr 0.9fr;
    gap: var(--space-12);
  }
  /* The photograph is capped so a tall portrait does not set the height of the
     whole first screen. */
  .hero-shot img {
    max-height: 34rem;
    object-fit: cover;
  }
}

/* ---------------------------------------------------------------------------
   The confirmation line: a face, a name and one sentence, read as one thing.

   The research (planning/research/2026-09-18-what-customers-actually-ask.md)
   is that under 4% of people find a plumber through a website — the typical
   visitor already has Greg's name from a neighbour and is checking it. So the
   answer to "is this the bloke I was told about?" has to arrive as a single
   glance rather than as a photograph and, separately, a sentence about it.
   Flex row, middle-aligned: the circle and the words are one object, and the
   call button is the next thing under it.
--------------------------------------------------------------------------- */
.who {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  /* Tight to the h1 above it — it is the heading's caption, not a new
     paragraph — and the CTA's own --space-6 top margin carries the gap below,
     so the button reads as the line's consequence. */
  margin: var(--space-3) 0 0;
}
.who span {
  /* 1.45 rather than the body's 1.65. Three short lines beside a 60px circle
     sprawl at the body leading and stop looking like one object; tightened,
     they sit as a block the same height as the photograph. It also buys about
     15px of the first screen on a phone, which is the whole argument for the
     line being here. */
  line-height: 1.45;
}
/* The villages, under the byline: the same tight leading as the byline so
   the two read as one block, muted so the name stays the first thing read,
   and the CTA's own top margin still carries the gap below. */
.where {
  margin: var(--space-3) 0 0;
  line-height: 1.45;
  color: var(--ink-muted);
}
.who-photo {
  flex: none;
  /* 60px, written as 15 steps of the 0.25rem base for the same reason the
     gallery thumbnails are: it is the size of a picture, not a gap, so it is
     not a --space-* value — but it is still on the scale's own base rather
     than a literal the audit has never seen. Big enough to read a face at
     arm's length, small enough that it is a byline and not the hero. */
  width: calc(var(--space-1) * 15);
  height: calc(var(--space-1) * 15);
  border-radius: 50%;
  object-fit: cover;
  /* The source is 600x800 and he is high in it, under the cap. A square crop
     centred vertically would take the top off his head; 20% of the 200px of
     overflow lands the crop on source rows 40-640, which holds the cap, the
     eyes and the beard. Judged by looking at the render, like the hero crop
     above. The horizontal axis needs no help — cover uses the full width. */
  object-position: center 20%;
}

/* ---------------------------------------------------------------------------
   The three doors.

   A customer arrives in one of three situations and each has a question
   underneath it they will not say out loud (planning/design/THREE_DOORS_BRIEF.md).
   One door each, in that order. Three chapters, NOT three cards: no panel, no
   shadow, no radius, no equalised height. A row of identical rounded boxes is
   the named tell of a generated page and it is the same argument that took the
   service cards out and made them a list — except here it matters more,
   because these three are the page.

   What separates them instead is space and a rule. Each door opens on a 3px
   rule in one of the mark's own colours, used as a thick decorative fill,
   which is the one thing those colours are good at on white (see the header of
   this file). The colours are not decoration for its own sake: the signature
   blue for the everyday half-hour work, the near-black the logo sits on for
   the considered job, and the warm flame for the questions, the door that
   answers back. Three weights of the same mark, and nothing else changes
   between the doors. (The flame door was the emergency one, /right-now/,
   until 2026-09-18; Greg does no emergency work, so the questions took its
   colour and its place moved to third.)
--------------------------------------------------------------------------- */
.doors {
  display: grid;
  /* A whole --space-12 between doors on the phone, which is the band rhythm
     rather than the paragraph rhythm: the eye should have to travel to reach
     the next one, so three doors read as three and not as a list. */
  gap: var(--space-12);
  margin: var(--space-12) 0;
}
.door {
  border-top: 3px solid var(--rule);
  padding-top: var(--space-4);
  /* Deliberately NOT capped at --measure-text, though stacked below 62em the
     rule then runs past the paragraph under it. That is the file's own "one
     left edge" rule at the head of this stylesheet: the reading measure is
     applied to the things that are READ, not to the box around them, and the
     rule is the box. In three columns the two coincide anyway — the column is
     373px, well inside the measure — so this only shows between 48em and 62em,
     where it reads as a section rule, which is what it is. */
}
.door--small {
  border-top-color: var(--blue-500);
}
.door--big {
  border-top-color: var(--blue-950);
}
.door--questions {
  border-top-color: var(--flame-600);
}
/* The doors live in the dark band on the home page, where the considered
   job's near-black rule would be the band's own colour and vanish. White
   there: still the quiet one of the three, next to blue and flame. The band
   owns the spacing, so the doors' own margins go. */
.doors-band .doors {
  margin: 0;
}
.doors-band .door--big {
  border-top-color: var(--slate-50);
}
.door h2 {
  margin: 0 0 var(--space-3);
  /* No font-size: the browser's own h2 in the display face at 700 is already
     the strong heading this wants, and a second size here would be a fourth
     type step nobody asked for. 1.2 rather than the global 1.1 because these
     headings run to two and three lines in a column and condensed type at
     1.1 closes up when it wraps. */
  line-height: 1.2;
}
/* The heading is a link to the same page the underlined line beneath it names.
   It is set as a heading rather than as a link on purpose — three underlined
   blue headings down the page is a menu, and the doors are meant to read as
   prose — and that is only safe BECAUSE the door's own link line repeats the
   destination in full: no door is reachable through an unmarked link alone.
   Hover, active and keyboard focus all bring the link appearance back. */
.door h2 a {
  color: var(--heading);
  text-decoration: none;
}
.door h2 a:hover,
.door h2 a:active,
.door h2 a:focus-visible {
  color: var(--link);
  text-decoration: underline;
}
.door p {
  margin: 0;
}
.door p + p {
  margin-top: var(--space-4);
}
.door-links {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  /* Two gaps: the row gap is what separates the button from the text link when
     they wrap onto their own lines at 360px, the column gap what separates
     them side by side. */
  gap: var(--space-2) var(--space-4);
  margin-top: var(--space-5);
}
/* The text link is the door's actual handle, so it is given the thumb's own
   height rather than a sentence's. Not a button — no door offers one now the
   emergency door has gone, the header and the call bar carry the number —
   but a 19px-tall link is
   the review's standing could-improve on this page and it costs nothing to
   fix. The weight is what makes it read as a control next to the body copy. */
.door-links a:not(.btn) {
  display: inline-flex;
  align-items: center;
  min-height: var(--target);
  font-weight: 600;
}
/* The phone, and the one thing the first screen has to do besides confirm who
   he is: say that there is more under it. At 375x812 the sticky call bar's top
   edge is at 741, and with the hero at its old rhythm the first door started at
   832 — so the screen ended on white and nothing said scroll. The gap is pulled
   in here so door 1's coloured rule and the first line of its heading sit above
   that edge, with the second line running under the bar, which is the cue.

   It is a phone-only reduction: the wide screens have no call bar to end on and
   the three-column row wants its air. The total from the call button to the
   rule is still --space-6 (the .cta's own bottom margin) + --space-2 (the hero's
   padding) + --space-6 = 56px, which stays larger than the --space-12 between
   one door and the next, so the break between the doors and the screen above
   them is still the bigger of the two.

   The hero gives up its whole bottom margin rather than both halves giving up
   some: the doors now own the space between the two, which is one number to
   read instead of two that have to be added together. */
@media (max-width: 47.99em) {
  .hero {
    margin-bottom: 0;
  }
  .doors-band {
    margin-top: var(--space-6);
  }
}
/* Three columns only once there is room for the headings to breathe in them.
   Measured: at 768 a third of the page is 224px and "A small job you have been
   putting off" sets to five lines, which is a column of headings rather than
   three doors. At 62em it is 300px and they run to two. Below that they stack,
   which is also the phone. */
@media (min-width: 62em) {
  .doors {
    grid-template-columns: repeat(3, 1fr);
    /* Side by side the rule across the top of each door is what separates
       them, so the gap between columns does not have to do that work as well.
       It stays --space-8 rather than --space-12 so the three read as one row. */
    gap: var(--space-8);
  }
}
h1,
h2,
h3 {
  font-family: var(--font-display);
  font-weight: 700;
  line-height: 1.1;
  /* Condensed type sets tighter than the system stack, so it wants a shade
     more letter-spacing at small sizes and none at large. */
  letter-spacing: 0.005em;
  color: var(--heading);
}
h1 {
  font-weight: 700;
  letter-spacing: -0.005em;
}
h1 {
  font-size: var(--text-h1);
}
.lead {
  font-size: var(--text-lg);
  color: var(--ink-muted);
}
a {
  color: var(--link);
}
/* Every hover state is paired with the same :active state, and the pairing is
   the point rather than a tidy-up: a phone has no hover at all, so a rule that
   only fires on :hover is a desktop decoration and a tap gets no acknowledgement
   from the site whatsoever. The tap is also the slower, less precise input -- a
   finger covers the thing it is pressing -- so it is the one that needs the
   feedback more. Same declarations, so the palette stays closed and there is no
   second appearance to keep in step. */
a:hover,
a:active {
  color: var(--link-hover);
}
:focus-visible {
  outline: 3px solid var(--ring);
  outline-offset: 2px;
  border-radius: 2px;
}

/* The dark band the mark was designed for. */
.site-header {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-4);
  padding: var(--space-3) var(--space-4);
  background: var(--slate-950);
  color: #fff;
}
.site-header a {
  color: #fff;
}
.brand {
  font-family: var(--font-display);
  display: flex;
  align-items: center;
  gap: var(--space-3);
  font-weight: 700;
  font-size: var(--text-lg);
  letter-spacing: 0.02em;
  text-decoration: none;
}
.brand img {
  width: 38px;
  height: 38px;
  display: block;
}
.site-header nav ul {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4);
  margin: 0;
  padding: 0;
}
.site-header .tel {
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
  min-height: var(--target);
  color: #fff;
  text-decoration: none;
  white-space: nowrap;
}
.tel-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--space-8);
  height: var(--space-8);
  border-radius: 50%;
  background: var(--flame-600);
  color: #fff;
  transition: background 0.15s;
}
.tel-text {
  display: flex;
  flex-direction: column;
  line-height: 1.15;
}
.tel-label {
  font-size: var(--text-sm);
  color: var(--slate-300);
}
.tel-number {
  font-weight: 700;
  font-size: var(--text-lg);
  letter-spacing: 0.02em;
  font-variant-numeric: tabular-nums;
}
.site-header .tel:hover .tel-icon,
.site-header .tel:active .tel-icon {
  background: var(--flame-700);
}
.site-header .tel:hover .tel-number,
.site-header .tel:active .tel-number {
  color: var(--blue-100);
}
/* The menu button is hidden here, at every width, and switched on in exactly
   one place: inside the small-screen block, under `.has-menu`. Hidden by
   default rather than shown and then hidden away, because the two states this
   has to survive are a wide screen (where the five links are already in the
   header and a button would be a second way to reach them) and no JavaScript
   at all (where the button would open nothing). Both are the default, so the
   default is off. */
.menu-toggle {
  display: none;
}

.hero {
  padding-bottom: var(--space-2);
}
.btn {
  display: inline-block;
  padding: var(--space-3) var(--space-5);
  background: var(--blue-700);
  color: #fff;
  font-weight: 600;
  text-decoration: none;
  border-radius: 6px;
}
.btn:hover,
.btn:active {
  background: var(--blue-800);
  color: #fff;
}
.btn-accent {
  background: var(--flame-600);
}
.btn-accent:hover,
.btn-accent:active {
  background: var(--flame-700);
}
/* The WhatsApp button: the glyph beside the label, in the brand's own dark
   green (see the token). inline-flex so the glyph and the text centre on one
   line, and the gap is the word space the glyph replaces. */
.btn--whatsapp {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  background: var(--whatsapp-700);
}
.btn--whatsapp:hover,
.btn--whatsapp:active {
  background: var(--whatsapp-800);
}
/* The outlined button: the second thing on a screen, never the first. The
   link colour as a 2px edge and as the text, no fill, so it follows a dark
   band as well as white; the fill arrives on hover. The arrow slides a step
   to say "this way", which is the only motion on it. */
.btn--outline {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--target);
  padding: var(--space-2) var(--space-5);
  background: transparent;
  color: var(--link);
  border: 2px solid var(--link);
  /* It wraps to two lines on a phone; at the body leading the two lines
     drift apart and stop reading as one label. */
  line-height: 1.3;
}
.btn--outline:hover,
.btn--outline:active {
  background: var(--link);
  color: var(--surface);
}
.btn--outline svg {
  flex: none;
  transition: transform 0.15s;
}
.btn--outline:hover svg,
.btn--outline:active svg {
  transform: translateX(var(--space-1));
}
.jobs-done .more {
  margin-top: var(--space-6);
}
.cta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-4);
  margin: var(--space-6) 0;
}

.callout {
  margin: var(--space-8) 0;
  padding: var(--space-4) var(--space-5);
  background: var(--surface-sunk);
  /* The brand blue as a large decorative fill, which is what it is good at. */
  border-left: 5px solid var(--blue-500);
  border-radius: 0 6px 6px 0;
}
.callout h2 {
  font-size: var(--text-lg);
  margin-top: 0;
}

/* ---------------------------------------------------------------------------
   The numbered steps: /bigger-jobs/ has four under "How it goes". (The
   retired /right-now/ page had three more, with h2 headings.)

   Deliberately not tied to a heading level: a step heading is whatever the
   page's outline needs, an h3 on the bigger-jobs page, where the steps sit
   inside a section. Styling `li > :first-child` rather than `.steps h2` is what lets
   the same rules serve both without either page lying about its outline to
   get the type it wants.

   The numeral is a counter rather than markup: neither page writes a "1", so
   there is nothing that can fall out of step with the order of the list.
--------------------------------------------------------------------------- */
.steps {
  list-style: none;
  counter-reset: step;
  padding: 0;
  margin: var(--space-6) 0;
  /* The steps are prose, so they take the READING cap rather than the
     container's width -- without it the hairline between two steps runs the
     full 74rem of a wide screen while the sentence above it stops at 68ch,
     which reads as a fault rather than as a rule. --measure-text, not
     --measure, for the reason the token block gives: --measure is raised by a
     wide band and a paragraph inside one is not easier to read. */
  max-width: var(--measure-text);
}
.steps li {
  counter-increment: step;
  /* Two columns: the numeral, then everything the step says. The children are
     pinned to column 2 rather than left to auto-placement, which would put a
     step's second paragraph under the numeral instead of under its heading.
     The numeral column is min-content, so it is exactly one digit wide and
     all three rows line up; a tenth step would widen its own row, which is a
     problem neither page has. */
  display: grid;
  grid-template-columns: min-content 1fr;
  column-gap: var(--space-4);
  padding: var(--space-4) 0;
}
.steps li + li {
  /* Between the steps and nowhere else. A rule above the first and below the
     last would box the list in, and the numerals already do the separating --
     on a page somebody is reading with water coming through a ceiling, the
     less furniture the better. */
  border-top: 1px solid var(--rule);
}
.steps li > * {
  grid-column: 2;
}
.steps li::before {
  content: counter(step);
  grid-column: 1;
  grid-row: 1;
  font-family: var(--font-display);
  font-weight: 700;
  /* The largest size the scale has, which is the h1's. Inventing a fifth size
     for one numeral is exactly what the tokens exist to stop, and a step
     number that reads at arm's length is most of the point of this page.
     --blue-700 rather than the logo's own --blue-500: this is a character
     being read, not a fill, and 500 is 2.65:1 on white. */
  font-size: var(--text-h1);
  line-height: 1.1;
  color: var(--blue-700);
}
.steps li > :first-child {
  font-size: var(--text-lg);
  margin: 0;
}
.steps p {
  /* Quieter than the heading it sits under, which is what makes the three
     headings scannable on their own. */
  margin: var(--space-2) 0 0;
  color: var(--ink-muted);
}
.steps p a:not(.btn) {
  /* The gas line is the one link in these steps that is not the call button,
     and a reader in a hurry has to see that it IS one. It keeps the browser's
     underline and gains the weight. */
  font-weight: 600;
}
.steps a[href^='tel:']:not(.btn) {
  /* A phone number read across a line break is two numbers. */
  white-space: nowrap;
}
.steps .btn {
  /* Full width on a phone, where this is the one thing on the page that has to
     be hit with a thumb by somebody holding a bucket with the other hand. A
     block-level flex box already fills the width it is given, so the phone is
     the default here and the wide screen is the exception below -- the
     opposite way round from `.cta .btn`, which has to say `width: 100%`
     because a `.btn` is inline to start with.

     The min-height is a floor rather than the thing setting the height: the
     button measures 50px as it stands (one line of --text-base plus --space-3
     top and bottom), and the declaration is what keeps it at 44 if that ever
     stops being true. */
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: var(--target);
  text-align: center;
}
@media (max-width: 47.99em) {
  /* On a phone the button breaks out of the step's text column and runs the
     full width of the list, numeral and all. Two reasons, and the second is
     the one that decided it: it should be the widest thing on this screen,
     and the label wrapped to two lines in the ~290px the text column leaves
     it. Measured after this: 311px and one line at 375, 296px and one line at
     360, which is the narrowest width this site is designed down to. The copy
     was cut to "Call Greg on <number>" so that it would sit on one line, and
     a wrapped button label is exactly what the comment in cta.hbs went to the
     trouble of avoiding.

     :has() is the only way to reach the paragraph AROUND the button; where it
     is unsupported the button stays in the text column and wraps, which is a
     narrower button rather than a broken page. */
  .steps li > p:has(> .btn) {
    grid-column: 1 / -1;
  }
}
@media (min-width: 48em) {
  .steps .btn {
    display: inline-flex;
  }
}

/* ---------------------------------------------------------------------------
   The small jobs: twelve rows, one line each.

   A scan rather than a document. The whole value of the page is that somebody
   finds their own job in it in about two seconds, so it is a ruled list with
   the title strong and the line under it quiet. No cards -- identical rounded
   boxes with one radius and one shadow each is a named tell of a generated
   page, and this is the argument the services list made before it too. No
   icons and no thumbnails: there are no photographs of these jobs, and a
   stand-in panel was tried once on the services (`.card-photo-none`) and was a
   grey rectangle pretending to be a photograph.

   Each row carries its record's slug as an id, so /small-jobs/#dripping-tap is
   a real address that survived the twelve job pages being folded into this
   one. :target is what makes arriving at one visible.
--------------------------------------------------------------------------- */
.jobs {
  list-style: none;
  padding: 0;
  /* Pulled back by exactly the row's own horizontal padding, so the TITLES
     line up with the page's other left edges while the hairline and the
     :target tint still have room either side of the words. Derived from the
     token rather than typed as -12px, the same way the footer's icon row and
     the open menu panel are, so it cannot quietly stop being right. */
  margin: var(--space-6) calc(-1 * var(--space-3));
}
.jobs li {
  padding: var(--space-3);
  /* Nothing in a row is tappable -- a row is a link TARGET, not a link -- so
     this floor is about the rhythm of a scan rather than about a thumb: it
     stops a short job with a short line making a shallower row than the ones
     either side of it. */
  min-height: var(--target);
  /* Under every row including the last, so the list closes before the
     paragraph that follows it. A top rule on the first row instead would need
     a second one on whichever row starts the right-hand column, and that is a
     count of the records rather than a fact about the layout. */
  border-bottom: 1px solid var(--rule);
}
.jobs li > :first-child {
  font-size: var(--text-lg);
  /* One step of air under the title and none above it: at zero the two lines
     read as one wrapped sentence, which is the one thing a scan cannot
     afford. The same --space-1 the list this replaced used. */
  margin: 0 0 var(--space-1);
}
.jobs p {
  margin: 0;
  color: var(--ink-muted);
}
.jobs li:target {
  /* Quiet, because eleven other rows still have to be readable: the sunken
     surface the callouts use, and the logo's own blue as a bar down the left
     edge -- a large fill, which is the one thing that blue is good at on a
     white page. Inset, so the bar lands in the padding and no word moves. */
  background: var(--surface-sunk);
  box-shadow: inset 4px 0 0 var(--blue-500);
  border-radius: 0 4px 4px 0;
}
@media (min-width: 48em) {
  /* Two columns from the tablet up, at the container's own wide measure, so
     the whole list is on one screen -- which is the difference between a list
     you scan and a list you scroll. The descriptions keep the reading measure
     anyway: `main p` caps them at 68ch, which is narrower than a column.

     Row flow rather than six-and-six columns: the hairlines then line up
     across the gap, and the page is built from a folder of records, so
     nothing here may assume there are exactly twelve of them. */
  .jobs {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    column-gap: var(--space-8);
  }
}

label {
  display: block;
  font-weight: 600;
  margin-bottom: var(--space-1);
}
input,
textarea {
  width: 100%;
  padding: var(--space-3);
  border: 1px solid var(--slate-400);
  border-radius: 4px;
  font: inherit;
}
button[type='submit'] {
  padding: var(--space-3) var(--space-5);
  border: 0;
  border-radius: 6px;
  background: var(--flame-600);
  color: #fff;
  /* Two declarations, not one. `font: inherit, 600` is not valid CSS -- the
     shorthand takes no comma list -- so the whole declaration was dropped and
     the button fell back to the UA's own button font: measured in Chromium as
     Arial at 13.3333px, on the page whose only job is to be filled in. The
     inputs above it were correct, which is what made it survive an eyeball.
     `font: inherit` resets family, size, line-height and weight together, so
     the weight has to be restated after it, not inside it. */
  font: inherit;
  font-weight: 600;
  cursor: pointer;
}
button[type='submit']:hover,
button[type='submit']:active {
  background: var(--flame-700);
}

/* The footer is the last band rather than a light strip with a rule on top:
   it closes the page on the same ground the header opens it on. */
.site-footer {
  background: var(--blue-950);
  padding: var(--space-12) var(--space-4);
  font-size: var(--text-sm);
  --ink: var(--slate-50);
  --ink-muted: var(--blue-200);
  --heading: var(--slate-50);
  --link: var(--blue-300);
  --link-hover: var(--blue-100);
  --rule: rgba(255, 255, 255, 0.18);
  color: var(--ink-muted);
}
.site-footer strong {
  color: var(--ink);
}
/* Two lines: the icon row, then the copyright. The row's own top margin
   would be air above the first thing in the band, which the band's padding
   already supplies. */
.site-footer .social {
  margin-top: 0;
}
.site-footer .footer-legal {
  margin: 0;
}

/* The social row. A list rather than a bare <a> because there will be a second
   account one day and a row of one is still a row. */
.social {
  /* The size the mark reads at, declared here because it is this row's own
     number and because a third value is derived from it and --target below --
     a literal in that calc would go stale the moment either changed. The
     thumb size is not this row's to own and is a root token; the two are still
     not the same number, which is the whole point of the offset further down. */
  --glyph: 22px;
  list-style: none;
  display: flex;
  gap: var(--space-1);
  margin: var(--space-4) 0;
  padding: 0;
  /* Optical alignment, and it is not a nicety. The glyph is centred in a target
     twice its width, so its visible left edge sits half the difference in from
     the link's box -- while every other line in this footer starts flush. The
     icon read as indented against them. Pulling the row back by exactly that
     half-difference puts the MARK in the text column rather than the box that
     contains it, which is what the eye is lining up. Derived rather than typed:
     -11px would be a literal that silently stops being right. */
  margin-left: calc((var(--glyph) - var(--target)) / 2);
}
.social a {
  /* This is the one place on the site where the full 44px is not a compromise.
     An icon link has no words to widen it, so the target IS the padding -- drop
     it and the link becomes a 22px target, which is under even the WCAG floor.
     The nav links elsewhere get to stay at 40px precisely because they have
     text; these have none. */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--target);
  height: var(--target);
  border-radius: 50%;
  color: var(--link);
  /* The icon is the only thing in the link, so an underline would sit under a
     glyph rather than under text. */
  text-decoration: none;
}
/* Sized here as well as on the element, and deliberately: the width/height
   attributes in the markup are what hold the layout before this stylesheet
   arrives, and this is what keeps the glyph tied to --glyph afterwards. */
.social a svg {
  width: var(--glyph);
  height: var(--glyph);
}
.social a:hover,
.social a:active {
  color: var(--link-hover);
  /* Faint rather than a filled circle: the footer is quiet and a solid disc
     appearing under the cursor is louder than the link it belongs to. */
  background: rgba(255, 255, 255, 0.1);
}

/* Text for a screen reader and nothing else. The clip-rect pattern rather than
   `display: none` or `left: -9999px`: display:none is not announced at all, and
   moving text 9999px off-canvas makes some browsers scroll to it when the link
   inside takes focus. This is the only accessible name the social links have,
   so getting it wrong makes them read as "link". */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* ---------------------------------------------------------------------------
   Services: cards, photographs and the "what this covers" list.

   Every photograph on the site is one of Greg's own jobs. They arrived as
   800x800 thumbnails padded with white, which is invisible on a white page and
   made the grids look ragged; the padding is trimmed off, so they are now 4:3
   and 3:4 at their true size. Each record carries its own width/height and the
   markup emits them, so the page does not jump while they load. The grids then
   crop to one landscape box so a row of mixed portraits and landscapes lines
   up.
--------------------------------------------------------------------------- */
/* The list that used to be here — `.stack`, and `.stack--thumbs` with one of
   Greg's photographs per row — went with the pages it was built for. The eight
   service pages and the twelve job pages are now sections on /bigger-jobs/ and
   /small-jobs/, and nothing renders either class. Its two arguments are not
   lost: both the case against cards and the case against a stand-in panel for
   a row with no photograph (`.card-photo-none`, a grey rectangle pretending to
   be a photograph) are restated on `.jobs` above, which is the list that
   inherited the shape. */

/* The photography, bigger. The brief's one departure from the reference: it
   drops cards and has a gallery elsewhere, but Greg's photographs are the best
   material on this site, so they change shape rather than shrink — three large
   images across instead of eight thumbnails. */
.work {
  list-style: none;
  padding: 0;
  margin: var(--space-8) 0 0;
  display: grid;
  gap: var(--space-4);
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
}
.work img {
  display: block;
  width: 100%;
  height: 100%;
  aspect-ratio: 3 / 4;
  object-fit: cover;
  border-radius: 8px;
}

/* The rooms he has done, inside the dark band on /bigger-jobs/. This is the
   only gallery left on the site, and it is the page's argument: the words are
   captions under a heading and the photographs are what is actually being
   said. So the grid is COUNTED rather than auto-fitted — two across on a
   phone, four from 48em — and the controller caps each section at four
   photographs so a section is exactly one full row at either width. A row
   that always fills is what stops eight sections reading as a ragged wall;
   auto-fill would give one, two, three or four depending on the viewport and
   no section would ever be predictably shaped. If the count here changes,
   PHOTOS_PER_ROOM in src/controllers/biggerJobs.js changes with it. */
.gallery {
  list-style: none;
  padding: 0;
  /* No bottom margin: the separator between rooms is .room's own, so the
     gallery never adds a second, different gap under the last row. */
  margin: var(--space-4) 0 0;
  display: grid;
  gap: var(--space-2);
  grid-template-columns: repeat(2, 1fr);
}
@media (min-width: 48em) {
  .gallery {
    grid-template-columns: repeat(4, 1fr);
    gap: var(--space-3);
  }
}
.gallery img {
  display: block;
  width: 100%;
  height: auto;
  /* Square, and it is the kinder crop rather than the tidier one. Fifteen of
     the site's photographs are 3:4 portraits and the rest are 4:3 landscapes;
     cropping that set to 4:3 keeps only the middle 56% of a portrait's height
     and takes the top off every bathroom. A square keeps 75% of either, which
     is the same argument the service-row thumbnails made, and it lines the rows up
     at both widths whatever shape the original was. */
  aspect-ratio: 1 / 1;
  object-fit: cover;
  border-radius: 8px;
  /* Something behind the picture while it loads. Inside .band--dark this is
     --blue-900, so a lazy image below the fold does not flash white. */
  background: var(--surface-sunk);
}

/* One room: a heading, one line, and the photographs. Eight of these run down
   the dark band, and they are separated by a hairline and air rather than by
   a box each — eight bordered panels is a catalogue, and this page is trying
   to be a man showing you what he has done. --rule inside .band--dark is
   already 18% white, so the line is a whisper. Two of the eight records have
   no photograph at all; that is a heading and a line on their own, and the
   gap is the honest reading rather than something to fill. */
.room {
  margin-top: var(--space-6);
}
.room + .room {
  padding-top: var(--space-6);
  border-top: 1px solid var(--rule);
}
.room h3 {
  /* The caption scale the .jobs rows use, so a room title and a small-job
     title are the same size across the site. */
  font-size: var(--text-lg);
  margin: 0 0 var(--space-1);
}
.room p {
  margin: 0;
  color: var(--ink-muted);
}

/* Who is coming: Greg's photograph and Greg's own words, as one block.
   The cap is what makes the float work. `main` is wide (the container is
   --measure-wide) and `.about-photo` floats to the right EDGE of whatever
   contains it, so in the full-width column the photograph would park 600px
   away from a paragraph capped at the reading measure and nothing would wrap
   round it — a picture on its own out in the margin. Capping the section at
   the reading measure puts the photograph and the sentences in one column, so
   the text wraps beside it and the two read as one thing. `main p` caps the
   paragraphs too, which is belt and braces rather than a duplicate: this is
   the cap on the BOX, that is the cap on the line.

   It is also the grid item that contains the float. Every direct child of
   `main` is a grid item and so establishes its own formatting context — the
   same containment `flow-root` would give — which is why the call to action
   after it starts below the photograph rather than beside it, with no
   clearfix anywhere. */
.who-section {
  max-width: var(--measure-text);
  /* Its own movement, after the gas line, so it gets more than a heading's
     worth of air above it. */
  margin-top: var(--space-12);
}

/* Greg's own photograph. The About page it was written for is gone; it now
   lives in the "Who is coming" section of /bigger-jobs/ and nowhere else —
   see the template comment there for what the photograph is and why it is a
   holiday snap. Centred and capped on a phone so it doesn't fill the screen;
   from 48em it floats beside the words instead of sitting above them. */
.about-photo {
  margin: var(--space-6) auto;
  /* --space-12 (3rem) x 6 = 18rem: big enough to read a face, and a third of
     the reading measure .who-section is capped at, so the sentences beside it
     still get two thirds of the column. Written as a multiple of a token
     rather than as 18rem so it is not a literal for the audit to flag. */
  max-width: calc(var(--space-12) * 6);
}
.about-photo img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: 8px;
}
.about-photo figcaption {
  margin-top: var(--space-1);
  font-size: var(--text-sm);
  color: var(--ink-muted);
  text-align: center;
}
@media (min-width: 48em) {
  /* Floats right inside .who-section, which is where the wrapper the old
     About page needed (`.about-intro`) went. That wrapper is not missed:
     .who-section is a direct child of `main`, so it is a grid item, and
     every grid item establishes its own formatting context — the same
     containment `flow-root` would give. The float is contained without a
     clearfix and the call to action after it starts below the photograph.

     What DID have to come across from the old page is the width cap, and
     for the same reason: the float goes to the right edge of its container,
     so unless the container is the reading measure the photograph parks out
     in the margin with nothing wrapping round it. That cap is on
     .who-section, above.

     Greg's own words are two short paragraphs, so the float still runs a
     little deeper than the text beside it. That is a photograph on a page
     rather than a fault, and the alternative — shrinking the portrait until
     the text clears it — buys tidiness by making the one face on the site
     too small to read. */
  .about-photo {
    float: right;
    margin: 0 0 var(--space-4) var(--space-8);
  }
}

/* `.job-photo` was here: one photograph on a single job's own page, keeping its
   real shape because there was no row for it to line up with. The twelve job
   pages are twelve rows of `.jobs` now, and no page has a photograph of its
   own to place. */

/* A named feature and what it does — a definition list because that is what
   the pairs are. Only the shower toilet page uses it. */
.features {
  margin: var(--space-4) 0 var(--space-6);
}
.features dt {
  font-weight: 600;
  margin-top: var(--space-4);
}
.features dd {
  margin: var(--space-1) 0 0;
  color: var(--ink-muted);
}

.note {
  color: var(--ink-muted);
  font-size: var(--text-sm);
}

/* Greg's strapline, above the h1 rather than instead of it. */
.eyebrow {
  margin: 0 0 var(--space-1);
  font-weight: 600;
  font-size: var(--text-sm);
  color: var(--blue-700);
}

/* ---------------------------------------------------------------------------
   Small screens: the phone, and getting to it.

   This stylesheet had no media queries at all. That was survivable because the
   layout is fluid — but the header is a wrapping flex row carrying a brand,
   five links and the telephone number, and at 360px the number wrapped to the
   third line. The number is the most important control on the site and it was
   the last thing in the wrap order.

   Everything below is mechanism rather than claim: it changes how the page
   behaves on a phone and states nothing new about the business.
--------------------------------------------------------------------------- */

/* Invisible until focused, then the first thing in the tab order. Positioned
   rather than display:none, because a hidden element cannot take focus. */
.skip {
  position: absolute;
  left: -9999px;
  top: 0;
  padding: var(--space-3) var(--space-4);
  background: var(--slate-950);
  color: #fff;
  z-index: 20;
}
.skip:focus {
  left: 0;
}

/* The wrapper holding main and the call bar — see src/layouts/layout.hbs. It is
   what gives the sticky bar something to be released by. */
.page {
  display: flex;
  flex-direction: column;
  /* Tall enough that a short page (404, thanks) still puts the bar at the foot
     of the screen rather than floating under two lines of text. */
  min-height: calc(100vh - 12rem);
}
.page > main {
  flex: 1 0 auto;
}

.callbar {
  position: sticky;
  bottom: 0;
  z-index: 10;
  display: flex;
  gap: var(--space-2);
  padding: var(--space-3);
  background: var(--slate-950);
  /* The band is the logo's own ground, so the brand blue is readable on it and
     usable as a large fill here in a way it never is on white. */
  border-top: 3px solid var(--blue-500);
}
.callbar a {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  /* 44px, the smallest target that is reliably hit with a thumb. */
  min-height: var(--target);
  padding: var(--space-2) var(--space-3);
  border-radius: 6px;
  font-weight: 700;
  text-decoration: none;
  text-align: center;
}
.callbar a {
  /* The two labels are short enough to sit on one line at 360px and are kept
     there: a wrapped label made the bar 91px tall, which is a ninth of a phone
     screen spent on two words. Verified against the narrowest width the site is
     designed for rather than assumed — if a label ever grows, re-measure, since
     nowrap on a flex item is what would push the page into horizontal scroll. */
  white-space: nowrap;
}
.callbar-primary {
  /* Two thirds of the bar: calling is the primary action, and the two are not
     offered as equals. */
  flex: 2;
  background: var(--flame-600);
  color: #fff;
}
.callbar-primary:hover,
.callbar-primary:active {
  background: var(--flame-700);
  color: #fff;
}
.callbar-secondary {
  background: transparent;
  border: 1px solid var(--slate-400);
  color: #fff;
}
.callbar-secondary:hover,
.callbar-secondary:active {
  background: var(--slate-800);
  color: #fff;
}

/* Above this width the header's own number is always in view, so the bar is
   only taking space. */
@media (min-width: 48em) {
  .callbar {
    display: none;
  }
  .page {
    min-height: 0;
  }
}

@media (max-width: 47.99em) {
  /* Row one is the brand and the number; the navigation drops to its own row
     underneath. Source order is brand -> button -> nav -> tel, which is the
     right order to read and to tab through -- the button comes before the nav
     it opens; only the visual arrangement moves. With the script running, that
     second row becomes the menu panel and is hidden until the button is
     pressed; without it, it is the row of links it has always been. */
  .site-header {
    gap: var(--space-2) var(--space-3);
    padding: var(--space-3) var(--space-4);
  }
  /* The brand and the number have to share the first row: measured at 360px,
     the header was 190px tall before a word of the page showed, because the
     brand at its desktop size pushed the number onto a row of its own. The mark
     stays legible at 32px and the wordmark at 1rem. */
  .brand {
    font-size: var(--text-base);
    gap: var(--space-2);
  }
  .brand img {
    width: 32px;
    height: 32px;
  }
  .site-header nav {
    order: 1;
    flex-basis: 100%;
  }
  .site-header nav ul {
    /* The row gap is --space-2 rather than --space-1 for one reason: measured at
       375px the five links come to 325px and stay on one row, so the row gap is
       only ever read below the 360px this site is designed down to -- and there
       it is the gap between two 40px targets stacked on top of each other. At
       --space-1 that gap is 4px, under the 8px two adjacent targets need to stop
       a thumb hitting the wrong one. The column gap is already 12px. */
    gap: var(--space-2) var(--space-3);
    font-size: var(--text-sm);
  }
  .site-header nav a {
    display: inline-block;
    /* Not the full 44px — these are secondary to the two bar actions, and a
       44px row of five links would push the page content off the screen. This
       is the compromise the bar exists to make safe. */
    padding: var(--space-2) 0;
  }
  .site-header .tel {
    display: inline-flex;
    align-items: center;
    min-height: var(--target);
    padding: 0 var(--space-3);
    font-size: var(--text-sm);
    border: 1px solid var(--blue-600);
    border-radius: 6px;
  }
  /* The number alone on a phone: the row has no width for the glyph or the
     label, and the call bar says "Call Greg" there anyway. */
  .tel-icon,
  .tel-label {
    display: none;
  }
  .tel-number {
    font-size: var(--text-sm);
  }

  /* -------------------------------------------------------------------------
     The menu, which exists only once the script above the <nav> has marked the
     document. Every rule from here down is behind `.has-menu`, so the header a
     visitor without JavaScript sees is the one in the rules above, unchanged
     and untouched -- that is the whole shape of this enhancement, and it is
     why none of it is written as an override of the two-row header.

     What the owner asked for is a menu button. What the research report asks
     for is the number never behind one. Both, here: the button takes the five
     links off the first screen, and the number keeps its place in the row.
     ------------------------------------------------------------------------- */
  .has-menu .site-header nav {
    display: none;
  }
  /* Three small concessions to fit a fourth thing in the row, and all three
     are here rather than in the rules above so that the header without
     JavaScript is left exactly as it was -- it has no button to make room for.
     Arithmetic, measured at 360px where the row has 328px inside its padding:

       brand 129 + number 146 + button 44 + two 12px gaps  = 343  (over by 15)
       number pill padding --space-3 -> --space-2          = 335  (over by 7)
       column gap --space-3 -> --space-2                   = 327  (fits)

     One pixel of slack is not a fit anybody should rely on, and there is a
     measured reason not to: the wordmark is the only thing in this header set
     in Archivo Narrow, which is `font-display: swap`, so on a cold load it is
     first drawn in the fallback -- and the fallback is only narrow on a device
     that happens to have Arial Narrow. Measured with the woff2 blocked, which
     is what an Android without it renders: the wordmark is 165px, not 129.

       wordmark at --text-sm                               = 318  (10px clear)

     which is what the row is built on. In the fallback face the row is still
     over (153 + 138 + 44 + 16 = 351) and wraps to two. That is the font
     FAILING rather than the font loading: head.hbs preloads the woff2 in the
     same round trip as this stylesheet, for exactly this reason, so the
     wordmark is drawn in Archivo Narrow at first paint. And where it does
     happen the row lands on two rows at 120px, which is the two-row header
     this site has had all along rather than anything broken. Closing that last
     case means a metric-adjusted fallback @font-face, which is a decision
     about the whole display face and not about a menu button. */
  .has-menu .site-header {
    column-gap: var(--space-2);
  }
  .has-menu .site-header .tel {
    padding: 0 var(--space-2);
  }
  .has-menu .brand {
    font-size: var(--text-sm);
  }
  .has-menu .menu-toggle {
    /* Same order as the nav, so the flex tie-break puts it in source order --
       button first, nav after it on its own row. One declaration rather than
       renumbering the nav beside it. */
    order: 1;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    /* Square, and exactly the thumb: with the label out of sight there is no
       text to widen the box, so the target IS the button -- the same argument
       the footer's icon links make, and the same number. */
    width: var(--target);
    height: var(--target);
    /* The bordered pill the number already wears. Two controls sharing the
       right edge of one row should read as a pair rather than as a control
       and a loose glyph. --blue-600 is 5.31:1 on this ground, so the edge of
       the button clears the 3:1 a non-text boundary needs -- BRAND.md measures
       that token against white, and this is the other side of it. */
    padding: 0;
    border: 1px solid var(--blue-600);
    border-radius: 6px;
    background: transparent;
    /* A browser sets a button in the OS font at the OS size, so a button joins
       this page's typography only because a rule puts it there -- and the
       design review reports one that does not. It matters even with the label
       hidden: `.visually-hidden` clips the word, it does not stop it being
       set, and the review measures the control rather than what shows. White
       on --slate-950 is 18.88:1. */
    font: inherit;
    font-size: var(--text-sm);
    font-weight: 700;
    color: #fff;
    cursor: pointer;
  }
  /* Open: a panel under the header row rather than a dropdown over the page.
     There is nothing to drop over -- this is the top of the document -- and a
     panel in the flow pushes the page down instead of covering it, which needs
     no overlay, no scroll lock and no click-outside handler. */
  .has-menu .site-header.is-open nav {
    /* The header redefines no role tokens (it is one ground and white text),
       so the value `.band--dark` gives --rule is named here once and both
       hairlines take it. */
    --rule: rgba(255, 255, 255, 0.18);
    display: block;
    /* The header's own horizontal padding, given back: a menu row is the full
       width of the screen rather than the width of the text in it. Derived
       from the padding rather than typed, so the two cannot drift apart. The
       basis has to grow by the same amount and is not optional -- a negative
       margin moves a flex item's box without widening it, so on its own it
       slid the panel left and left a 32px gutter down the right. Measured, not
       assumed: the rows are 375px at 375px wide. */
    margin: 0 calc(-1 * var(--space-4));
    flex-basis: calc(100% + 2 * var(--space-4));
    border-top: 1px solid var(--rule);
  }
  .has-menu .site-header.is-open nav ul {
    display: block;
    /* Back to body size: these are the only five things on the row now, and
       --text-sm was a concession to fitting them all across. */
    font-size: var(--text-base);
  }
  .has-menu .site-header.is-open nav li + li {
    border-top: 1px solid var(--rule);
  }
  .has-menu .site-header.is-open nav a {
    display: flex;
    align-items: center;
    /* The full 44px, and here it costs nothing: one link per row uses the
       width the five-across row did not have. This is the compromise noted
       above being paid off rather than repeated. */
    min-height: var(--target);
    padding: 0 var(--space-4);
  }
  main {
    padding: var(--space-6) var(--space-4) var(--space-8);
  }
  /* A full-width primary button is easier to hit than an inline one, and on a
     narrow screen there is nothing to sit beside it anyway. */
  .cta {
    gap: var(--space-3);
  }
  .cta .btn {
    display: block;
    width: 100%;
    min-height: var(--target);
    text-align: center;
  }
}

/* ---------------------------------------------------------------------------
   The enquiry form.

   It asks for a telephone number rather than an email address, because that is
   how this business works, and it asks what the problem is and where you are,
   because those are the two things that decide whether the job is Greg's. The
   styling has one job beyond looking tidy: on a phone, every control has to be
   big enough to hit and the keyboard that comes up has to be the right one.
--------------------------------------------------------------------------- */
.contact-routes {
  list-style: none;
  padding: 0;
  margin: var(--space-6) 0 var(--space-8);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3) var(--space-5);
  color: var(--ink-muted);
}

.enquiry {
  max-width: 34rem;
  margin-bottom: var(--space-8);
}
.enquiry p {
  margin: 0 0 var(--space-5);
}
.enquiry select {
  width: 100%;
  padding: var(--space-3);
  border: 1px solid var(--slate-400);
  border-radius: 4px;
  font: inherit;
  background: var(--surface);
  color: var(--ink);
}
/* 44px on every control, not just the button: a select that is 34px tall is one
   a thumb misses. */
.enquiry input,
.enquiry select,
.enquiry button {
  min-height: var(--target);
}
.enquiry input[type='file'] {
  padding: var(--space-2);
  background: var(--surface-sunk);
}
.field-note {
  display: block;
  margin-top: var(--space-1);
  font-size: var(--text-sm);
  color: var(--ink-muted);
}
.optional {
  font-weight: 400;
  color: var(--ink-muted);
}
.enquiry button[type='submit'] {
  width: 100%;
}
@media (min-width: 30em) {
  .enquiry button[type='submit'] {
    width: auto;
    min-width: 12rem;
  }
}

/* The honeypot. Never shown, never announced, never in the tab order — the
   markup carries aria-hidden and tabindex="-1" as well, because display:none
   alone is what a competent bot checks for first. */
.hp {
  display: none;
}

/* The problem grid that stood here is gone with src/partials/problems.hbs.
   It was the home page's tile menu of eleven symptoms; the three doors replaced
   it, and the one surviving use of the word — contact.json's `problems` array —
   is the enquiry form's <select>, which is styled as a form control and never
   carried the class. Grepped for before deleting: no markup references
   `.problems` anywhere in src/. */

/* ---------------------------------------------------------------------------
   The questions page, /faq/.

   Grouped questions, each an <article> with an id so /faq/#boiler is an
   address a WhatsApp reply can send. The rows borrow the .jobs list's
   grammar on purpose: a hairline under each, the caption scale for the
   question, muted text for the answer, and the same quiet :target tint, so
   arriving at one question from a link on another page shows you where you
   landed. One column even on a wide screen, unlike .jobs: an answer is prose
   to be read, not a line to be scanned, and `main p` already holds it to the
   reading measure.

   The topic list at the top wraps as a row of plain links, each a full
   thumb target, because on a phone twenty-odd questions is a long scroll and
   the jump is the point of it.
--------------------------------------------------------------------------- */
.faq-index ul {
  display: flex;
  flex-wrap: wrap;
  gap: 0 var(--space-5);
  list-style: none;
  padding: 0;
  margin: var(--space-6) 0;
}
.faq-index a {
  display: inline-flex;
  align-items: center;
  min-height: var(--target);
  font-weight: 600;
}
.faq-group {
  margin-top: var(--space-12);
}
.faq {
  margin: var(--space-4) calc(-1 * var(--space-3)) 0;
}
.faq article {
  padding: var(--space-4) var(--space-3);
  border-bottom: 1px solid var(--rule);
}
.faq h3 {
  font-size: var(--text-lg);
  margin: 0 0 var(--space-2);
}
.faq p {
  margin: 0 0 var(--space-3);
}
.faq p:last-child {
  margin-bottom: 0;
}
.faq-more a {
  font-weight: 600;
}
.faq article:target {
  background: var(--surface-sunk);
  box-shadow: inset 4px 0 0 var(--blue-500);
  border-radius: 0 4px 4px 0;
}

/* A before-and-after pair: on /faq/ (the coal shed) and in the "Before and
   after" section of /bigger-jobs/. The .gallery grid, held at two across on
   every width and to the reading measure, so a pair reads as one comparison
   rather than as a strip of four with two missing. The pictures keep their
   own shape -- a pair is two portraits or two landscapes taken from the same
   spot, and cropping them square would cut away the ceiling or the floor
   that is half of what changed. */
.pair {
  max-width: var(--measure-text);
  margin-bottom: var(--space-3);
}
@media (min-width: 48em) {
  .pair {
    grid-template-columns: repeat(2, 1fr);
  }
}
.pair img {
  aspect-ratio: auto;
  object-fit: fill;
}
.pair figure {
  margin: 0;
}
.pair figcaption {
  margin-top: var(--space-1);
  color: var(--ink-muted);
}
.before-after + .before-after {
  margin-top: var(--space-8);
}
.before-after h3 {
  font-size: var(--text-lg);
  margin-bottom: var(--space-1);
}
.before-after .year {
  font-weight: 400;
  color: var(--ink-muted);
}

/* ---------------------------------------------------------------------------
   The Google reviews card, under the call to action on the home page.

   Shaped the way Google's own review badges are -- a white card with a thin
   edge, the four-colour G, a bold line and a muted one -- because the point
   is recognition: somebody should know what it is before they read it. The
   G keeps its own colours (src/partials/google-reviews.hbs says why); every
   other colour here is a role token, so the card follows its surroundings.
   A link the whole way across, at least a thumb tall.
--------------------------------------------------------------------------- */
.google-reviews {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
  min-height: var(--target);
  padding: var(--space-2) var(--space-4) var(--space-2) var(--space-3);
  margin-top: var(--space-2);
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: 8px;
  color: var(--ink);
  text-decoration: none;
  transition: border-color 0.15s, box-shadow 0.15s;
}
.google-reviews:hover,
.google-reviews:active {
  color: var(--ink);
  border-color: var(--slate-300);
  box-shadow: 0 2px 8px rgba(0, 29, 50, 0.08);
}
.google-reviews-g {
  flex: none;
}
.google-reviews-text {
  display: flex;
  flex-direction: column;
  line-height: 1.3;
}
.google-reviews-title {
  font-weight: 600;
}
.google-reviews-sub {
  font-size: var(--text-sm);
  color: var(--ink-muted);
}
.google-reviews-stars {
  /* Google's own star gold, the fourth colour of its mark. */
  color: #fbbc05;
  letter-spacing: 0.05em;
}
