

:root {
  /* Two typefaces, both the reader's own: the system sans sets the copy, the system serif
     sets the headings. Naming the choice once is what keeps every heading on the site one
     face; nothing is downloaded, so neither costs a request or a moment of invisible text. */
  --sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --heading: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
  --ink: #212529;
  --navy: #14477d;
  /* Dark enough for 4.5:1 on the sand band, not only on white - #5b6673 sat right on the line there. */
  --muted: #4f5a66;
  --brown: #7a3a1a;
  --brown-deep: #43220f;
  --border: #e6e0d9;
  --bg: #ffffff;
  --cream: #fdf3ed;
  /* Cream half-way to white: the opening band's ground. Warm enough not to read as white,
     faint enough that the cream strip under it is still a step down and not a repeat. */
  --ivory: #fef9f6;
  --sand: #ece0d6;
  --star: #e8b53d;
  /* The brown accent's counterpart on a dark ground: a copper that reads against white. */
  --copper: #d9a070;
  /* Nano Banana's output ratios, so generated images need no cropping. */
  --ratio-hero: 3 / 2;
  --ratio-card: 5 / 4;
  --ratio-square: 1 / 1;
  --ratio-portrait: 2 / 3;
  /* The space a band puts above and below its content. One token, because the same-ground
     rule below has to halve it and the phone has to shrink it. */
  --band: 3.5rem;
  --card-pad: 1.25rem;
  --radius-card: 12px;
  --radius-sm: 6px;
  /* Two shadows, and there is no third. --lift is a box raised a hair off the page under
     the pointer; --raised is a box genuinely over something else - a menu over the page, a
     figure over a photograph. Both are the deep brown at a low alpha, never a grey, so a
     shadow reads as the page's own colour dropped rather than as dirt on it. The ring on a
     step's disc is not in here: it is a ring in the band's --ground, not a shadow. */
  --lift: 0 3px 14px color-mix(in srgb, var(--brown-deep) 12%, transparent);
  --raised: 0 12px 28px color-mix(in srgb, var(--brown-deep) 15%, transparent);
  /* The three states an interactive thing can be in, each said once. --t carries its own
     easing, so a rule names it and nothing picks a duration of its own; --focus is the one
     ring; --edge-hover is the edge a box takes under the pointer, and a dark band
     redefines it the way it redefines --ground. */
  --t: 0.15s ease;
  --focus: 2px solid var(--navy);
  --edge-hover: color-mix(in srgb, var(--brown) 45%, var(--border));
  /* On a dark ground the two text colours become white and this. One value, so a band, a
     card and the footer cannot each pick their own alpha for the same role. The lift pair
     is the box that sits on a dark band - a card, a check row, a chip - and it is the same
     fill and edge everywhere one of those appears. */
  --muted-invert: rgba(255, 255, 255, 0.8);
  --lift-fill: rgba(255, 255, 255, 0.07);
  --lift-edge: rgba(255, 255, 255, 0.14);
  --gap-section: 3rem;
  /* The header bar's height. Everything that has to clear it - the sticky offsets, the
     mobile overlay, the anchor scroll padding - reads this rather than repeating a number. */
  --bar: 88px;
  /* The condense: how much of the bar is given up once the page is scrolled - see
     header.site. Only the amount is a decision; the rest is arithmetic on it. */
  /* ⚠ WHAT THE BAR GIVES UP, NOT WHAT IS LEFT. At 24px the condensed bar was 64px holding a
     44px call button, which left seven pixels above and below it and read as a squeeze. 16px
     keeps the condense legible - 88px to 72px is a quarter of the bar - and leaves the
     button, the mark and the burger the air they had. */
  --pinch: 16px;
  --condense: 0;
}
/* Reduced motion, in one place: every transition on the site is timed by --t, so zeroing
   it here stops all of them - and a rule added later is covered without being listed. The
   condense below is not a transition: it is driven by the scroll position, so it moves
   only as far as the reader scrolls it. */
@media (prefers-reduced-motion: reduce) { :root { --t: 0s; } }
* { box-sizing: border-box; margin: 0; }
/* The header is sticky at every width, so an in-page anchor must stop below it rather
   than under it; --bar is the bar's fixed height. */
html { scroll-padding-top: var(--bar); }
/* ── Condense on scroll, without a script ─────────────────────────────── */
/* --condense is the progress, 0 at rest and 1 once the page is scrolled, and it is a
   registered property so the animation interpolates it as a number rather than flipping
   at the halfway keyframe. The plain --condense: 0 above is what a browser without
   @property reads, so every calc() on it still resolves to the resting bar there.
   The progress is tied to the scroll position itself, not to a threshold: over the 160px
   after the topbar has scrolled away the bar shortens by --pinch, and it stays there. No
   snap, so no dead band - the Kirby site needed two thresholds because its condense
   changed the header's height, the document shrank, scroll anchoring corrected for it and
   the correction crossed the threshold again. Here the header's box never changes height
   (see header.site), so there is nothing to anchor against either way. Where the browser
   cannot drive an animation from scroll the block is skipped whole and the bar stays 88px,
   which is the page as it was. Judge it in a visible tab: a backgrounded tab runs no
   animation frames at all, and reads as if nothing happened. */
@property --condense { syntax: '<number>'; inherits: true; initial-value: 0; }
@supports (animation-timeline: scroll()) {
  header.site { animation: condense linear both; animation-timeline: scroll(root);
    animation-range: 40px 200px; }
  @keyframes condense { to { --condense: 1; } }
  /* An anchor jump lands with the bar already condensed, so it clears the condensed height. */
  html { scroll-padding-top: calc(var(--bar) - var(--pinch)); }
}
/* The full-bleed bands are sized from 100vw, which includes the vertical scrollbar where a
   browser draws a classic one - so every band runs ~15px wider than the viewport. Clipping
   at the root (not only on body, whose value only propagates when html is visible) is what
   stops that becoming a horizontal scrollbar. clip, not hidden: hidden would make html a
   scroll container and the sticky header would stop sticking. */
html { overflow-x: clip; }
body {
  overflow-x: clip;
  font-family: var(--sans);
  /* The base body size, which everything that does not name its own size inherits: card
     bodies, prose, list items. Sizes written in rem are anchored to the root and are
     unmoved by it, so this lifts the reading text without touching the headings. */
  font-size: 18px; line-height: 1.6; color: var(--ink); background: var(--bg);
  -webkit-font-smoothing: antialiased;
}
h1, h2, h3 { font-family: var(--heading); color: var(--ink); font-weight: 700;
  letter-spacing: -0.015em; line-height: 1.15; }
/* The floor of each clamp is the phone and the ceiling the desktop, and it is the floor
   that was doing the damage: the body grew to 18px while these stayed in rem, so a phone
   met an h3 at 19.2px over 18px copy - a heading that read as bold body text. Every floor
   goes up by more than its ceiling, and the 1.25 ladder holds at both ends. */
h1 { font-size: clamp(2.2rem, 3.4vw, 2.8rem); }
h2 { font-size: clamp(1.75rem, 2.8vw, 2.25rem); }
/* Under the step down from the h2's floor (1.75 / 1.25 is 1.4): a card's title is the one
   h3 that has to sit four across without wrapping, so it takes the short end of it. */
h3 { font-size: 1.2rem; }
/* Type scale. Anything read as copy is 1rem: body, a card's blurb, a review, the table.
   One-line furniture - a claim in the strip or the why band, a chip, a footer link - is
   0.95rem. Small print is 0.9rem: .fine, the topbar, the rating row, a byline, a caption on
   a photo or a figure. Tracked labels are 0.875rem. The only text smaller than that is a
   label inside a control - the floated field label, the call button's "Call Now", a project
   tag - and, on a phone, the trust strip and the project captions at 0.875rem. */
/* Text colours, and there are only two: ink for anything you are meant to read,
   muted for the standfirst under a heading and the small print beside it. */
.note { color: var(--muted); }
a { color: var(--navy); text-decoration: none; }
a:hover { text-decoration: underline; }
a:focus-visible, button:focus-visible, summary:focus-visible, input:focus-visible,
select:focus-visible, textarea:focus-visible {
  outline: var(--focus); outline-offset: 2px; border-radius: 3px;
}
img { max-width: 100%; }
ul { list-style: none; padding: 0; }
/* Every section sits inside the frame — nothing bleeds to the viewport edge. */
.container { max-width: 1360px; margin: 0 auto; padding: 0 1.25rem; }

/* ── primitives ─────────────────────────────────────────────────────────── */
/* Every band runs edge to edge and they stack flush, so the page reads as a column of
   colour with no white seams between sections. The negative inline margin cancels the
   frame and the matching padding puts the content back on it, so only the background
   escapes — content still lines up with the header above it. */
.panel { background: var(--cream); margin-inline: calc(50% - 50vw);
  padding: var(--band) calc(50vw - 50%); }
/* The opening band runs a little wider than the frame every other band sits on: it is one
   pair of columns rather than a grid, so the picture and the pitch each gain half of the
   extra width, and the page opens on something that reaches past the measure the rest of it
   keeps. The floor is the container's own 1.25rem gutter, not zero: once the viewport is
   narrower than the frame the subtraction goes negative, and a bare max() against 0 would
   run the hero to both edges of a phone while every band under it kept its margin. */
.panel:has(> .hero) { padding-inline: max(1.25rem, calc(50vw - 50% - 6rem)); }
/* Two bands on the same ground are one band as far as the eye is concerned: the colour
   change is what says a section ended, and without one the two paddings meet as a hole with
   nothing in it. So a band that follows a band of its own colour halves its top padding -
   still a gap, no longer a gap doing the work a colour change should be doing. It is a
   fallback, not a licence to stack same-toned bands: the alternation is still the design. */
.panel:not(.pale):not(.tint):not(.dark) + .panel:not(.pale):not(.tint):not(.dark),
.panel.pale + .panel.pale,
.panel.tint + .panel.tint,
.panel.dark + .panel.dark,
/* Pale and plain count as one ground here: ivory is white with a couple of percent of warmth
   in it, which is a difference the eye reads as a printing variation rather than as the end
   of a section. The about page opens pale straight onto plain and needs this. */
.panel.pale + .panel:not(.pale):not(.tint):not(.dark),
.panel:not(.pale):not(.tint):not(.dark) + .panel.pale { padding-top: calc(var(--band) / 2); }
/* --ground is the band's own colour, for anything inside that has to look cut out of it. */
.panel, .panel.plain { --ground: var(--bg); background: var(--ground); }
.panel.pale { --ground: var(--ivory); }
.panel.tint { --ground: var(--sand); }
/* A dark band redefines the tokens that have to change on it and nothing else: its ground,
   and the hover edge, since brown on brown-deep is invisible and copper is its stand-in. */
.panel.dark { --ground: var(--brown-deep); --edge-hover: color-mix(in srgb, var(--copper) 55%, transparent);
  color: #fff; text-align: center; }
.panel.dark h1, .panel.dark h2, .panel.dark a { color: #fff; }
.panel.dark p { color: var(--muted-invert); }
/* On the dark ground the brown eyebrow vanishes, so it becomes copper; and navy on deep
   brown is mud, so the filled button becomes cream. A split inside a dark band reads left. */
.panel.dark .eyebrow { color: var(--copper); }
.panel.dark .btn.solid { background: var(--cream); color: var(--brown-deep); }
.panel.dark .btn.ghost { color: var(--cream); border-color: var(--sand); }
.panel.dark .btn.ghost:hover { background: rgba(255, 255, 255, 0.08); }
.panel.dark .split { text-align: left; }
/* A photograph behind a band, under enough of the deep brown that the band is still a dark
   band - the photo is atmosphere, the text is set as on any dark ground.

   The picture is an <img> in the markup rather than a background: a background can only be
   named in the stylesheet, and the largest image on the page should not wait for the
   stylesheet to be parsed before the browser knows to fetch it. It sits in its own layer
   behind the band's content - isolation makes the panel the stacking context, so the
   negative z-index goes behind the words and no further. */
.panel.photo { position: relative; isolation: isolate; }
.panel.photo > .backdrop { position: absolute; inset: 0; z-index: -1; }
.panel.photo > .backdrop > img { display: block; width: 100%; height: 100%; max-width: none;
  object-fit: cover; }
/* The overlay, on the backdrop rather than the panel, so it is between the picture and the
   words instead of under both. */
.panel.photo > .backdrop::after { content: ''; position: absolute; inset: 0;
  background: linear-gradient(color-mix(in srgb, var(--brown-deep) 86%, transparent),
    color-mix(in srgb, var(--brown-deep) 74%, transparent)); }


/* Any card grid. Column width is the one thing that varies: set --min per use. */
/* min() caps the column minimum at the container's width, so a --min wider than a phone
   (the 360px project cards) collapses to one full-width column instead of overflowing. */
.grid { display: grid; gap: 1rem;
  grid-template-columns: repeat(auto-fit, minmax(min(var(--min), 100%), 1fr)); }
/* A grid whose column count is part of the design rather than a consequence of the card
   width. It still collapses to one column once the columns would be narrower than --min. */
.grid.fixed { grid-template-columns: repeat(var(--columns, 2), minmax(0, 1fr)); }
@media (max-width: 60rem) { .grid.fixed { grid-template-columns: minmax(0, 1fr); } }

/* A column, so a card whose blurb runs short can still push its link to the foot - the
   grid stretches every card to the tallest, and the links then sit on one line. */
.card { display: flex; flex-direction: column; background: var(--bg);
  border: 1px solid var(--border); border-radius: var(--radius-card);
  padding: var(--card-pad); }
/* A card with no heading and body — a town, say — centres its one line in the same box. */
.card.center { display: grid; place-items: center; text-align: center; min-height: 4.5rem; }
/* A picture in a card is full-bleed: negative margins cancel the card's padding on the
   three sides it touches, so it reads as the top of the card rather than as something
   placed inside it. The copy below keeps its padding, and the space under the picture is
   that same padding rather than a number of its own - at 0.85rem the heading sat 14px under
   the picture and 21px in from the sides, which is the one gap in a card the eye can
   compare directly. The card on its side already does this with column-gap.
   Two things this rule has to get right. An img is a replaced element, so its width must
   be stated - auto resolves to the intrinsic size instead of stretching like a block. And
   its radius is the card's less the border it sits inside, otherwise a sliver of that
   border shows through at each top corner where the two curves disagree. */
/* max-width: none, because the global img rule caps images at 100% and would clamp the
   widened width straight back to the content box. */
.card > img { display: block; width: calc(100% + var(--card-pad) * 2);
  max-width: none; height: auto; aspect-ratio: var(--ratio-card); object-fit: cover;
  margin: calc(var(--card-pad) * -1) calc(var(--card-pad) * -1) var(--card-pad);
  border-radius: calc(var(--radius-card) - 1px) calc(var(--radius-card) - 1px) 0 0; }
/* The card on its side: the picture in a column of its own, the words beside it. The same
   card, the same padding, the same link behaviour - only the axis changes, so the blurb
   gets the room a stacked card cannot give it without growing the grid taller than the
   words need. The picture keeps its full bleed, now on the three edges it touches on this
   axis, and takes the card's height rather than the ratio, so a run of them squares up
   however long the words are. Below the grid's own breakpoint the card stacks again, which
   is what a phone wants: picture over words, one column. */
.card.row { display: grid; grid-template-columns: minmax(8rem, 32%) minmax(0, 1fr);
  column-gap: var(--card-pad); }
/* The words set the card's height and the picture takes it, cropping to fit. a zero min-height
   is what makes that work: a replaced element's automatic minimum is its intrinsic height,
   so without it the picture sizes the row and the words spread out to fill what it made. */
.card.row > img { align-self: stretch; width: calc(100% + var(--card-pad));
  height: calc(100% + var(--card-pad) * 2); min-height: 0; aspect-ratio: auto;
  margin: calc(var(--card-pad) * -1) 0 calc(var(--card-pad) * -1) calc(var(--card-pad) * -1);
  border-radius: calc(var(--radius-card) - 1px) 0 0 calc(var(--radius-card) - 1px); }
.card.row > .words { display: flex; flex-direction: column; }
/* The blurb is written to six lines in this column. A longer one is clamped rather than
   allowed to set the row's height, so eight cards in four rows stay square with each other
   whatever a town wrote. The whole sentence is still in the markup, so nothing is lost to a
   reader's find or to a crawler - only the overflow is hidden. */
.card.row .words p { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 6;
  line-clamp: 6; overflow: hidden; }
@media (max-width: 34rem) {
  .card.row { grid-template-columns: minmax(0, 1fr); }
  /* Unset what the row layout added and the base .card > img rule applies again, rather
     than restating its four values here. */
  .card.row > img { width: revert; height: revert; min-height: revert; aspect-ratio: revert;
    margin: revert; border-radius: revert; }
}
.card h3 { margin-bottom: 0.45rem; }
/* The heading is the link on a service card, in the heading's own colour: the card, not
   the underline, is what says it is a link. */
.card h3 > a { color: inherit; }
.card h3 > a:hover { text-decoration: none; }
/* auto takes up whatever slack the card has; the padding keeps a gap when there is none. */
.card .more { align-self: start; display: inline-flex; align-items: center; gap: 0.4rem;
  margin-top: auto; padding-top: 0.9rem; text-decoration: none; color: var(--navy); }
.card:hover .more svg { transform: translateX(2px); }
.card .more svg { transition: transform var(--t); }
/* The whole card is the link. The heading's anchor is stretched over the card it sits in,
   so the card can be clicked anywhere - which is what makes the hover below a promise the
   box can keep rather than a decoration on something inert. The "Learn More" line is a
   span with the same class, so the lift and the hover it keys still apply. */
/* A card carrying a photograph sits off the page from the start: a service card and a
   project card are both a picture in a box, so both wear the resting lift. The plain
   heading-and-body cards stay flat on their band - a shadow under every card is a shadow
   that says nothing. */
.card:has(.more), .card.project { box-shadow: var(--lift); }
.card:has(.more) { position: relative; }
.card h3 > a::after { content: ''; position: absolute; inset: 0; }
/* Under the pointer a card that is a link takes the accent on its edge and rises from the
   resting lift to the shadow a raised thing wears. Only the cards that are links move -
   never a card that is only a card. The boxed rows answer the pointer too; their states
   are with the rule that draws the box. */
.card:has(.more) { transition: border-color var(--t), box-shadow var(--t); }
.card:has(.more):hover, .card:has(.more):focus-within {
  border-color: var(--edge-hover); box-shadow: var(--raised); }
/* The accent card's edge is the page's accent, not the button's: navy is the colour of a
   thing you click, so spending it on a card that does nothing makes the two say the same
   word. Brown on a light band, copper on a dark one, as every other accent is. */
.card.accent { background: rgba(255, 255, 255, 0.55); border: 0;
  border-left: 3px solid var(--brown); }
.panel.dark .card.accent { border-left-color: var(--copper); }
/* A sign over a card's heading: the mark in a tinted square, in the accent colour on cream,
   so a run of cards opens on a picture each rather than a word each. The centred cards -
   the credentials - wear it in a disc, and start their rows from the top so a short card
   beside a tall one does not spread its three lines out to fill the box. */
.card .icon { display: grid; place-items: center; width: 3.5rem; height: 3.5rem;
  border-radius: var(--radius-sm); background: var(--cream); color: var(--brown);
  margin-bottom: 1rem; }
.card.center .icon { border-radius: 50%; }
.card.center:has(.icon) { align-content: start; }
/* A numbered card: the step's disc in a column of its own, the words beside it - the
   stacked shape the steps take on a phone, kept at every width for the guide's stages. */
.card.numbered { display: grid; grid-template-columns: 3.4rem minmax(0, 1fr); column-gap: 1.25rem;
  align-items: start; }
/* The disc is one size wherever it appears: a numbered stage, a homepage step. Only the
   space under it differs, because the stage sets its words beside the disc and the step
   sets them below. */
.card.numbered .badge, .card.step .badge { width: 3.4rem; height: 3.4rem; font-size: 1.5rem;
  line-height: 1; background: var(--brown); }
.card.numbered .badge { margin: 0; }
/* The ways in beside the contact form: three rows in one card, each the mark at a row's
   scale, a label, the thing itself a heading-weight link, and one line under it. The rows
   are ruled apart the way the review's byline is. */
.card.reach h3 { margin-bottom: 0.75rem; }
.card.reach li { display: flex; gap: 1rem; padding: 1rem 0; border-top: 1px solid var(--border); }
/* The last row's own padding would stack on the card's, leaving more air under it than over the first. */
.card.reach li:last-child { padding-bottom: 0; }
.card.reach li > div { min-width: 0; }
.card.reach .icon { flex: none; width: 3rem; height: 3rem; margin: 0; }
.card.reach .eyebrow { margin-bottom: 0.2rem; }
.card.reach .val { display: block; font-family: var(--heading); font-weight: 700;
  font-size: 1.2rem; line-height: 1.25; overflow-wrap: anywhere; }
.card.reach .fine { margin-top: 0.3rem; }
/* The promise under them: the card on the dark ground, the mark a lift of it in copper. */
.card.pledge { background: var(--brown-deep); border: 0; color: #fff; }
.card.pledge h3 { color: #fff; }
.card.pledge p { color: var(--muted-invert); }
.card.pledge .icon { background: rgba(255, 255, 255, 0.1); color: var(--copper); }
/* The person in full: one card, the portrait filling the narrow column and the words the
   wide one. Like the local band's pictures, the portrait is cropped to the height the words
   set - height 0, min-height 100%, so it grows nothing - and the card sits narrower than
   the frame, centred, as the owner band does, so the portrait stays a portrait. */
.card.profile { display: grid; grid-template-columns: minmax(0, 2fr) minmax(0, 3fr);
  padding: 0; overflow: hidden; max-width: 64rem; margin-inline: auto; box-shadow: var(--lift); }
/* The card's own padding is zeroed above so the portrait can fill its column edge to edge,
   so the words carry it - at the shared measure, not a multiple of it: this card sits on the
   same page as the rules, the credentials and the milestones, and a wider inset here read as
   a different kind of card rather than a bigger one. */
.card.profile > div { padding: var(--card-pad); }
.card.profile .shot > img { width: 100%; height: 0; min-height: 100%; object-fit: cover;
  border-radius: 0; box-shadow: none; }
/* The name is a step above a card heading - it is the point of the card - and the
   credentials line closes the head with a rule, the paragraphs starting under it. */
.card.profile h3 { font-size: 1.5rem; margin-bottom: 0.25rem; }
.card.profile .fine { padding-bottom: 1rem; margin-bottom: 1rem; border-bottom: 1px solid var(--border); }
/* The one sentence in their own words: the accent card's box on cream, so it shows against
   the white card, and in italics because it is speech. */
.card.profile blockquote { margin-top: 1.25rem; background: var(--cream); font-style: italic; }
/* A project card is its photograph, with the words laid over the foot of it on a dark
   fade - the same white-on-dark pairing the dark bands use. The image loses the bleed
   maths since it simply fills the card, and takes the 3:2 slot the photographs are made in.
   The tag is the small-caps label on a filled pill. */
.card.project { position: relative; padding: 0; border: 0; overflow: hidden; color: #fff; }
.card.project > img { margin: 0; width: 100%; max-width: 100%;
  aspect-ratio: var(--ratio-hero); border-radius: 0; }
.card.project .caption { position: absolute; inset: auto 0 0 0;
  padding: 4rem var(--card-pad) var(--card-pad);
  background: linear-gradient(to top, rgba(20, 12, 6, 0.88), rgba(20, 12, 6, 0.55) 45%, rgba(20, 12, 6, 0)); }
/* One line, always: a tag that outgrows its card is cut with an ellipsis rather than
   wrapping into a two-line blob - on a phone the cards are two across and the pill has
   about 150px. */
.card.project .tag { display: inline-block; max-width: 100%; white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis; vertical-align: top;
  background: var(--brown); color: #fff;
  font-size: 0.75rem; padding: 0.3rem 0.8rem; border-radius: 999px; margin-bottom: 0.65rem; }
.card.project h3 { color: #fff; font-size: 1.2rem; margin-bottom: 0.3rem; }
.card.project p { color: var(--muted-invert); font-size: 0.9rem; }
/* The heading is the card's link, and the card is a photograph: the anchor takes the
   caption's own white rather than the link colour, which would be unreadable on the
   gradient and would make one card shout among six. The ::after that stretches it over the
   whole card is the shared rule above; here it only has to stop looking like body copy. */
.card.project h3 > a { color: inherit; text-decoration: none; }
.card.project h3 > a:hover { text-decoration: underline; text-underline-offset: 3px; }
/* The card answers the pointer the way every other linked card does - it rises - and the
   focus ring goes on the card, not on the heading's own box, because the hit area is the
   whole card. :has() is already how the sheet asks "is this card a link". */
.card.project:has(h3 > a) { transition: box-shadow var(--t), transform var(--t); }
.card.project:has(h3 > a):hover { box-shadow: var(--raised); }
.card.project:has(h3 > a:focus-visible) { outline: var(--focus); outline-offset: 2px; }
.card.project h3 > a:focus-visible { outline: none; }

/* The disc's line-height is 1 so the digit's box is the glyph's own height and place-items
   centres the glyph, not a taller line box with the digit sitting low in it. */
/* A step is not a card: no fill, no edge, no shadow. The disc is the anchor, and a hairline
   through the four discs is what says they happen in order - each disc wears a ring of the
   band's own ground so the line stops at its edge. */
.card.step { align-items: center; text-align: center; background: none; border: 0; padding: 0 1rem; }
.card.step .badge { margin-bottom: 1.1rem; position: relative;
  box-shadow: 0 0 0 6px var(--ground); }
.grid:has(> .card.step) { position: relative; }
.grid:has(> .card.step)::before { content: ''; position: absolute; left: 12.5%; right: 12.5%;
  top: 1.7rem; border-top: 1px solid color-mix(in srgb, var(--brown-deep) 22%, transparent); }
/* Numbered step: the badge is the only extra element a step card needs. */
/* The digit is set in the sans, not the heading face, and lining-nums is why that matters:
   the serif the heading token resolves to has no lnum feature to ask for, so its figures
   stay old-style and 3, 4, 5, 7 and 9 hang below the baseline. place-items centres the line
   box, not the ink, so a hanging digit sits visibly low in a disc it is meant to be dead
   centre of - measured, the 4 descends 4.3px in the serif and 0 in the sans. */
.card .badge { display: grid; place-items: center; width: 2.3rem; height: 2.3rem;
  border-radius: 50%; background: var(--brown-deep); color: #fff;
  font-family: var(--sans); font-variant-numeric: lining-nums; margin-bottom: 0.85rem; }

.measure { max-width: 64ch; margin-inline: auto; }
.section-head { text-align: center; margin-bottom: 1.75rem; }
/* The margin holds what follows off the heading. A head with nothing under it - the
   contact banner's - would otherwise sit 1.75rem high of the band's centre. */
.section-head:last-child { margin-bottom: 0; }
/* Beside a form or an image the heading is one column of a pair, so it reads from the
   left with the copy under it - the band is still opened by the same component. */

.split .section-head { text-align: left; margin-bottom: 0; }
/* A grid straight under the heading keeps the gap a centred heading would have had. */
.split .section-head + .grid { margin-top: 1.75rem; }
.section-head p { margin-top: 0.75rem; }
/* The standfirst keeps to a measure; the eyebrow is a p too, but it is one short line. */
.section-head p:not(.eyebrow) { max-width: 64ch; margin-inline: auto; }
/* Inside a split the head reads from the left and the column is the measure: the
   standfirst runs the column's full width, level with the paragraphs under it, rather
   than stopping short of them at 64ch. */
.split .section-head p { margin-inline: 0; max-width: none; }
/* The line that closes a section — "questions before you decide?" */
.note { text-align: center; margin-top: 1.5rem; max-width: 64ch; margin-inline: auto; }
/* The reveal, off-screen rather than display:none so it keeps its place in the tab order
   and the label can still be reached by keyboard. Its toggle shows only on a phone.
   Selected through .note, not on its own: the label is also a .btn, and .btn's own
   inline-flex is written later in this sheet at the same specificity, so a bare
   .more-toggle lost the tie and both controls showed at every width. */
.more-check { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }
.note > .more-toggle { display: none; }
.more-check:focus-visible ~ .more-toggle { outline: var(--focus); outline-offset: 3px; }
.split .note { text-align: left; margin-inline: 0; }
/* Small print: the form's consent line, a distance on a chip, a caption under a table. */
.fine { font-size: 0.9rem; color: var(--muted); }

.btn { display: inline-flex; align-items: center; justify-content: center; gap: 0.5rem;
  border: 1px solid transparent; cursor: pointer; font-family: var(--heading);
  font-size: 1.2rem; font-weight: 600; border-radius: var(--radius-sm); padding: 13px 26px; }
.btn { transition: background-color var(--t), filter var(--t); }
.btn:hover { text-decoration: none; filter: brightness(1.08); }
.btn.solid { background: var(--navy); color: #fff; }
.btn svg, li > svg { flex: none; vertical-align: -0.2em; }
/* The outlined second choice. */
/* The outline button carries its whole weight in its border, where the solid one has a
   filled ground to be seen by - so it takes 2px against the solid's 1. The padding drops by
   the same pixel it gains, so the two sit the same height in an .actions row. */
.btn.ghost { background: transparent; color: var(--brown); border-color: var(--brown);
  border-width: 2px; padding: 12px 25px; }
.btn.ghost:hover { background: color-mix(in srgb, var(--brown) 6%, transparent); filter: none; }
/* Buttons follow the band they sit in: centred, because a band's head is centred, and
   left only inside a .split, where the copy column is left-aligned and a centred row would
   float away from the paragraph it closes. Stated here rather than per band so the closing
   CTA, the banner and the hero cannot drift apart. */
.actions { display: flex; flex-wrap: wrap; gap: 0.75rem; justify-content: center; }
.split .actions { justify-content: flex-start; }
/* The phone's fixed call button. Critical, because it is fixed and on the first screen:
   left to the served sheet it would paint as a stray inline icon under the footer for as
   long as that took to land. Off at every width where the header - and its own call
   button - is still on screen; the media query below turns it on. The inset clears the
   home indicator on a phone that has one and falls back to the plain 1rem where env() is
   unknown. */
.call-fab { display: none; position: fixed; z-index: 40; right: 1rem;
  bottom: calc(1rem + env(safe-area-inset-bottom, 0px));
  width: 56px; height: 56px; border-radius: 50%; background: var(--navy); color: #fff;
  box-shadow: var(--lift); }


/* ── chrome ─────────────────────────────────────────────────────────────── */

/* Small print, but only a step under the copy - at 0.9rem it was 14.4px against an 18px
   body, which reads as a disclaimer rather than as the line carrying the phone number. */
.topbar { background: var(--brown-deep); color: #fff; font-size: 1.05rem; }
.topbar .container { display: flex; justify-content: center; align-items: center;
  gap: 0.5rem; padding-block: 0.6rem; }
/* ONE LINE WHERE THERE IS ROOM FOR ONE. On a wide screen the tagline and the number share
   a line and the tagline may shrink below its own content (min-width: 0, which flex does
   not allow by default) rather than wrap and strand the number beside a two-line block. A
   tagline is written per site across hundreds of towns, so nothing here may depend on any
   one of them being short - and on a phone none of them are, which is why the phone block
   stacks the two instead of ellipsising a sentence down to nothing. */
.topbar span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.topbar a { color: #fff; font-weight: 600; text-decoration: underline; white-space: nowrap;
  flex: none; }

/* Sticky at every width, and only the bar itself (--bar tall) sticks: the topbar is a tagline and a
   phone number that scroll away, so the chrome that follows the reader stays one thin
   line. An opaque background is required — the bar overlaps content now, and the page
   would otherwise read through it. The z-index makes this a stacking context; the nav's
   backdrop (25) and dropdown panels (30) live inside the header, so they keep their
   order relative to each other and the whole group paints above the page, which sets
   no z-index anywhere. The existing bottom border is the separation; content sliding
   under a hard line needs no shadow, and no transition means nothing to reduce.
   The condense shortens the visible bar by --pinched without the header's own box ever
   changing height: the bar (.container) gives up that much and the header takes it back as
   top padding, so padding plus bar is always --bar and nothing below the header moves. The
   sticky offset then goes negative by the same amount, so the padding rides above the
   viewport and only the shorter bar is on screen. At rest --pinched is 0 and this is the
   plain sticky header. --pinched is the length the bar is shorter by right now, computed
   here on the element the scroll-driven animation runs on (see --condense above), and
   the mark inherits it. */
header.site { --pinched: calc(var(--pinch) * var(--condense));
  position: sticky; top: calc(-1 * var(--pinched)); z-index: 20;
  padding-top: var(--pinched);
  background: var(--bg); border-bottom: 1px solid var(--border); }
header.site .container { position: relative; display: flex; align-items: center;
  gap: 1.5rem; min-height: calc(var(--bar) - var(--pinched)); }
/* ⚠ THE BAR'S CONTENTS CONDENSE WITH IT, OR THE CONDENSE IS JUST A SQUEEZE. The call button
   is 59px tall against a 88px bar - fifteen pixels above and below it at rest - and taking
   16px off the bar alone left seven. So the button gives up its own share as the bar does,
   the same way the mark already does, and the air around it stays close to what it had. */
header.site .btn { padding-block: calc(13px - 6px * var(--condense)); }
/* The brand is the generated wordmark (see wordmark.ts), an <img> whose width and height
   attributes carry its own proportions. The height is --mark and the width follows; where
   the bar is too narrow for the mark at that height, max-width caps it and object-fit scales
   the drawing down inside the same box, left-aligned, so the bar never changes height.
   The condense takes half of what the bar gives up off the mark, so it tightens with the
   bar rather than filling it. */
.brand { --mark: 48px; display: flex; align-items: center; min-width: 0; }
/* The <picture> around the mark is a switch, not a box: display:contents drops it out of the
   layout so the img stays the brand's own flex child and the rule under this one still sizes
   it. */
.brand picture { display: contents; }
.brand img { display: block; height: calc(var(--mark) - var(--pinched) / 2); width: auto;
  max-width: 100%; object-fit: contain; object-position: left center; }
/* A step above body size: the bar's items are the page's primary controls and read at a
   glance. */
header.site nav { margin-left: auto; display: flex; align-items: center;
  gap: 1.4rem; font-size: 1.0625rem; font-weight: 500; }
header.site nav a { color: var(--ink); }
/* Nav dropdowns, ported from bulk-jobs. A hidden checkbox drives the panel through the
   :checked sibling selector, so it opens on hover, on keyboard focus and on click — a
   pointer leaving the menu is the other half of what people expect it to close on. */
.nav-check { display: none; }
.nav-burger { display: none; }
/* Anchored to its own trigger, so the panel opens under the item you clicked. The
   vertical padding is the gap below the bar: it belongs to the trigger, so hovering
   across it into the panel never breaks the hover. */
.nav-dd { position: relative; padding-block: 0.7rem; }
.dd-check { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }
.dd-summary { display: inline-flex; align-items: center; gap: 0.3rem;
  cursor: pointer; user-select: none; }
.dd-summary svg { transition: transform var(--t); }
.dd-check:checked ~ .dd-summary svg { transform: rotate(180deg); }
.dd-check:focus-visible ~ .dd-summary { outline: var(--focus); outline-offset: 3px;
  border-radius: 3px; }
/* Only present while the menu is open, and only to catch the click that closes it. */
.dd-backdrop { display: none; position: fixed; inset: 0; z-index: 25; }
.dd-check:checked ~ .dd-backdrop { display: block; }
.dd-panel { display: none; }
.nav-dd:hover > .dd-panel,
.nav-dd:focus-within > .dd-panel,
.dd-check:checked ~ .dd-panel {
  display: block; position: absolute; top: 100%; left: 0; z-index: 30;
  columns: 2; column-gap: 1.5rem; width: max-content; max-width: min(88vw, 620px);
  background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius-card);
  padding: 1rem 1.25rem; box-shadow: var(--raised);
}
/* break-inside keeps a wrapped two-line label from splitting across columns. */
.dd-panel a { display: block; padding: 0.25rem 0; font-size: 1rem;
  break-inside: avoid; min-width: 150px; }
/* Both menus are this one panel. The areas menu was a grid of a column per county, which was
   the right answer at 78 towns - a flat run that long has nowhere to look - and the wrong one
   once COVERAGE capped a site at 20. At that length the county heading stopped paying for
   itself: two of five sites cover a single county, so the head named the only thing on offer
   and the per-county cap then hid twelve of twenty towns behind an overflow link. Twenty in
   two columns is four rows more than the services menu already carries. */

footer.site { background: var(--brown-deep); color: var(--muted-invert);
  padding-top: 2.5rem; font-size: 0.95rem; }
footer.site h3 { color: #fff; margin-bottom: 0.9rem; }
footer.site a { color: var(--muted-invert); }
footer.site li { padding: 0.2rem 0; }
/* A long footer list shows its first few and folds the rest into a details element, so
   "view all" needs no script. The summary is ordered last so the toggle stays under the
   list, and reads as a link. */
footer.site details { display: flex; flex-direction: column; }
footer.site summary { order: 1; cursor: pointer; list-style: none; padding: 0.2rem 0; color: var(--copper); font-weight: 600; }
footer.site summary::-webkit-details-marker { display: none; }
footer.site details:not([open]) .open, footer.site details[open] .closed { display: none; }
.foot-bottom { border-top: 1px solid rgba(255, 255, 255, 0.15); margin-top: 2rem;
  padding: 1.25rem 0; text-align: center; }
/* The modified date sits under the copyright rather than beside it: one line of small
   print each, and neither wraps into the other on a phone. */
.foot-updated { display: block; }

/* ── sections ───────────────────────────────────────────────────────────── */

/* Two columns, used by the hero, the estimate block and any copy + art pairing. */
.split { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 2.5rem; align-items: center; }
.split > img, .split > svg { width: 100%; height: auto; aspect-ratio: var(--ratio-hero);
  object-fit: cover; border-radius: var(--radius-card); }
/* The hero's picture is the one on the site with no ratio of its own on the page: it stands
   as tall as the words beside it, whatever they come to, so the two columns end level and
   neither leaves a lip of empty ground under it - but the ratio is its floor, not its shape.
   A 3:2 picture on the 1360 frame is about 427px tall, so a band whose words run taller than
   that stretches the picture to match and object-fit takes the middle; a band whose words are
   shorter - a service hero, which has no rating row under its buttons - is 427 and not the
   690 an upright ratio made of it. The width and height attributes still hold the box in the
   moment before the sheet lands. */
.split.hero { align-items: stretch; }
/* The words are stretched to the picture's height so the two columns end level, which left
   them sitting at the top of a column taller than they are. Centre them inside it: the
   heading now reads against the middle of the photograph rather than its top edge. */
.split.hero > div { display: flex; flex-direction: column; justify-content: center; }
.split.hero > img { aspect-ratio: var(--ratio-hero); height: 100%; }
.shot > img, .shot > svg { display: block; width: 100%; height: auto;
  aspect-ratio: var(--ratio-square); object-fit: cover;
  border-radius: var(--radius-card); }
/* ⚠ THE OWNER BAND'S SQUARE IS A CROP OF THE ABOUT PAGE'S 2:3 PORTRAIT - one photograph of
   one owner rather than two generated faces - so it crops from the top third, where a
   centred crop of a chest-up portrait would take the chin and leave the chest. */
.owner .shot > img { object-position: 50% 22%; }
/* A photograph standing on its own gets the resting lift a photograph in a card gets - the
   pairing's picture, the about and owner portraits, the office map. Stated once, so a
   picture sits off the page the same way wherever it is placed. */
.split > img, .split > svg, .shot > img, .shot > svg, .split.beside .stack > img {
  box-shadow: var(--lift); }
/* The hero's heading is one notch up the scale from an inner page's h1 - the same ~1.25
   step that separates h1 from h2, applied once more, so the heading sizes stay one ladder.
   The phone floor is held to the h1's own ceiling rather than the step's full 2.75rem: the
   title is three lines on a phone already, and a fifth line would cost more than the size
   earns. Only the hero: an index page's h1 sits on a plain band and keeps the base size. */
.hero h1 { font-size: clamp(2rem, 3.1vw, 2.6rem); }
/* The standfirst, wherever it appears: under the h1 in the hero, and under a band's h2 as
   its intro. One class, so the paragraph that leads a section is the same size and weight
   everywhere and only its spacing changes with the place. It takes the accent rather than
   the ink: it is the one paragraph a band is allowed to colour, which is what separates it
   from the body copy under it without a second size or a rule between them. The accent
   follows the ground, brown on a light band and copper on a dark one, and both clear 6:1
   on every ground the site uses. */
.pitch { font-size: 1.2rem; font-weight: 500; color: var(--brown); }
/* A link inside the standfirst keeps the standfirst's colour and says it is a link with an
   underline instead. The paragraph is already the one coloured run on the band, and navy
   inside brown reads as a third colour rather than as an anchor. */
.pitch a { color: inherit; text-decoration: underline; text-underline-offset: 3px;
  text-decoration-thickness: 1px; }
/* The hero's vertical rhythm, in one place, because the eye reads the gaps as a group and
   one odd number in the middle is what makes a stack feel unconsidered. Three steps: the
   pitch sits 1.25rem under the h1, and everything after it - the buttons, then the rule
   over the rating - is held off by 1.75rem. The buttons had 0.9rem, less than the gap above
   the pitch, so they read as attached to the paragraph rather than as the next thing.
   The pitch is the one paragraph on the page set above body size: it is the standfirst the
   h1 hands off to, and at 1.1rem it was within half a pixel of the body it had to lead. */
.hero .pitch { margin: 1.25rem 0 1.75rem; }
/* Brown is invisible on brown-deep, so a dark band's standfirst takes the copper that
   stands in for the accent everywhere else on that ground. */
.panel.dark .pitch { color: var(--copper); }
/* The line above the h1: who this page serves. Small caps in the supporting colour, so it
   reads as a label rather than as the first line of the pitch. */
/* The small-caps label. It appears above a heading as an eyebrow and at the foot of a card
   as its link, so the type is set once for both. */
/* 14px: the usual size for an uppercase tracked label. 12px reads as small print, and
   above 14px it starts competing with the heading it sits over. */
.eyebrow, .card .more, .project .tag { font-size: 0.875rem; font-weight: 700;
  letter-spacing: 0.14em; text-transform: uppercase; color: var(--brown); }
/* .section-head p is more specific than .eyebrow, so the eyebrow is also stated at that
   specificity - otherwise it inherits the standfirst's size and margin. */
.eyebrow, .section-head .eyebrow { margin: 0 0 0.6rem; font-size: 0.875rem; }

/* A chip: one short line in the shared box, an icon at its left - the area grid's towns.
   The why band's rows and the FAQ rows use the same box; this is the standalone form, and
   it keeps the FAQ row's padding and the FAQ list's gap so a stack of towns and a stack of
   questions sit at the same rhythm. */
.chip { display: flex; align-items: center; gap: 0.5rem; padding: 0.9rem 1rem;
  font-size: 0.95rem; font-weight: 600; line-height: 1.35; }
.grid:has(> .chip) { gap: 0.6rem; }
/* A chip is one line: a long town name is cut with an ellipsis (the full name is in the
   title), and the distance never gives way. */
.chip > span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Small print inside a chip - a distance - sits at its right edge. */
.chip .fine { margin-left: auto; flex: none; font-weight: 400; white-space: nowrap; }
/* The three things a caller most wants to know, before the buttons. */
.checks { display: grid; gap: 0.55rem; margin: 0 0 1.75rem; }
/* The why band lays the same ticked rows two across, each in its own box. Its heading is
   the one h2 size, like every other band's. */
.why .checks { grid-template-columns: 1fr 1fr; gap: 0.75rem; margin: 0; }
/* The boxed row. A claim in the why band and a question in the questions band are one
   construct, so the box is stated once for both. */
.why .checks li, .faq li, .chip { background: var(--bg); border: 1px solid var(--border);
  border-radius: var(--radius-sm); color: var(--ink); }
.why .checks li { padding: 0.8rem 0.95rem; font-size: 0.95rem; font-weight: 600;
  line-height: 1.35; }
/* On the dark ground the box is a lift of the ground, not a white card - the same for a
   claim in the why band and a town chip in the areas band. */
.panel.dark .why .checks li, .panel.dark .faq li, .panel.dark .chip { background: var(--lift-fill);
  border-color: var(--lift-edge); color: #fff; }
/* Two of the three boxed rows can be acted on - a town chip goes somewhere, a question
   opens in place - and both answer the pointer with the same accented edge. Only the chip
   also lifts: it takes you off the page, and the question does not. The why band's claims
   are the third, and they stay still, because there is nothing to do to a claim.
   The question is keyed on its summary, so hovering the answer of an open row - which is
   inside the same li - does not light the row back up. */
.chip, .faq li { transition: border-color var(--t), box-shadow var(--t); }
.chip:hover, .chip:focus-visible,
.faq li:has(summary:hover), .faq li:has(summary:focus-visible) {
  border-color: var(--edge-hover); }
.chip:hover, .chip:focus-visible { box-shadow: var(--lift); }
.panel.dark .checks svg, .panel.dark .chip svg { color: var(--sand); }
.panel.dark .fine, .panel.dark .rating .src { color: var(--muted-invert); }
@media (max-width: 560px) { .why .checks { grid-template-columns: 1fr; } }
/* The proof row that closes the hero, above the fold: the platform marks, then the rating
   and its sources stacked beside them. Critical, because it is in the hero and the served
   sheet does not block the first paint - without these rules inlined the marks paint at
   their attribute box before the sheet lands. */

/* One row at every width: the marks keep their size and the text column shrinks and wraps
   inside itself, rather than dropping under the marks on a phone. */
.hero .rating { display: flex; flex-wrap: nowrap; align-items: center; gap: 0.85rem;
  margin-top: 1.75rem; padding-top: 1.25rem; border-top: 1px solid var(--border);
  font-size: 0.9rem; }
/* One square mark per platform, at the ratio a generated image will be. Contained rather
   than cropped, because each mark carries its own shape. */
.hero .rating .marks { display: flex; gap: 0.4rem; flex: none; }
.hero .rating .marks img, .hero .rating .marks svg { width: 40px; height: 40px;
  aspect-ratio: 1; object-fit: contain; }
/* The two lines read as one block, so they sit tighter than body copy. */
.hero .rating .meta { display: grid; gap: 0; line-height: 1.3; min-width: 0; }
.hero .rating .val { display: flex; align-items: center; gap: 0.45rem;
  font-family: var(--heading); font-weight: 600; font-size: 1.05rem; flex-wrap: wrap;}
.hero .rating .src { color: var(--muted); }


/* Copy with two pictures stacked beside it. The copy takes the wider track and sets the
   height; the pictures share the narrower one and split that height between them, so the
   pair always runs exactly as tall as the text. Their source is the square slot - the crop
   is what follows the text, not the photograph's ratio. */
.split.beside { grid-template-columns: minmax(0, 3fr) minmax(0, 2fr); align-items: stretch; }
/* The stretch is for pictures, which fill whatever height the copy sets. The contact page's
   form is a card with a button at its foot, and stretched it would carry a blank foot below
   the button whenever the cards beside it run taller - so it keeps its own height. */
.split.beside > .quote { align-self: start; }
/* The estimate band's .split .prose margins would inset the paragraphs inside their
   column, and the pictures match the column - so here the prose carries no bottom margin,
   and its top is the ordinary paragraph gap, so the standfirst and the paragraphs read as
   one column rather than a head with a block hung under it. */
.split.beside .prose { margin: 1rem 0 0; }
/* minmax(0, 1fr), not 1fr: a bare 1fr row has an auto minimum, which is the picture's own
   height at that width - so the rows would stay square and out-grow the text. */
.split.beside .stack { display: grid; grid-auto-rows: minmax(0, 1fr); gap: 1rem;
  min-height: 0; position: relative; }
/* height 0 with min-height 100%: the picture contributes nothing to the row's height and
   then fills whatever the paragraphs gave it. A plain height 100% still lets its intrinsic
   size grow the row, which is how the stack ended up twice as tall as the text. */
.split.beside .stack > img { width: 100%; height: 0; min-height: 100%; object-fit: cover;
  border-radius: var(--radius-card); display: block; }
/* A button laid on the picture - the map's "open in Maps" - sits in its top corner and
   keeps its own width, phone included. */
/* ⚠ A MATCHING GUTTER ON THE RIGHT, WHICH A LEFT INSET ALONE DOES NOT GIVE. Absolutely
   positioned and shrink-to-fit, the button grows until it runs out of containing block -
   so on a phone, where the label wraps to two lines, it reached the picture's right edge
   and sat flush against it while still holding its 1rem on the left. A max-width caps it
   with the same inset it opens with, and leaves the short desktop case shrink-to-fit. */
.split.beside .stack > .btn { position: absolute; top: 1rem; left: 1rem; width: auto;
  max-width: calc(100% - 2rem); }
/* The name of the place the pin marks, in the corner opposite the button. It is HTML and
   not pixels on purpose - see mapCaption in sections.tsx - so a renamed business is right
   here the moment the row changes, with no file to redraw. */
.split.beside .stack > figcaption { position: absolute; left: 1rem; bottom: 1rem;
  max-width: calc(100% - 2rem); background: var(--brown-deep); color: #fff;
  font-family: var(--heading); font-weight: 600; font-size: 0.95rem; line-height: 1.3;
  padding: 0.45rem 0.8rem; border-radius: var(--radius-sm); }
/* A picture with its figures sitting on it. The figures are ordinary cards - same box,
   same border, same padding - placed on opposite corners so neither covers the middle. */
.shot { position: relative; margin: 0; }
/* The same pair with the picture on the right - the community band, so it faces the story
   band's picture on the left. Order, not markup: the copy still comes first in the source. */
.split.flip > .shot { order: 1; }
.shot .stat { position: absolute; max-width: 60%; padding: 0.8rem 1rem;
  box-shadow: var(--raised); }
/* Each figure runs off one side of the picture, so the corners meeting that side are
   square - a rounded edge there would read as a gap rather than as a label on the image. */
.shot .stat:first-of-type { top: 2rem; right: 0;
  border-radius: var(--radius-card) 0 0 var(--radius-card); }
.shot .stat:last-of-type { bottom: 2rem; left: 0;
  border-radius: 0 var(--radius-card) var(--radius-card) 0; }
/* The figure itself is stated on the card, not on the picture, so the numbers band can
   use the same card standing alone. */
.card.stat b { display: block; font-family: var(--heading); font-size: 1.7rem;
  line-height: 1.1; color: var(--brown); }
/* Block, so the label's own leading sets its box: as an inline it kept the body's 1.6 and
   left more air under the label than the number gets above it. */
.card.stat span { display: block; font-size: 0.9rem; line-height: 1.25; }
/* A figure on its own in the numbers band: centred in its box, the number a size up. On
   the dark band the box is a lift of the ground, like the why band's claims, and the
   number takes the copper the accents take there. */
.card.stat.center { padding: 1.5rem var(--card-pad); gap: 0.35rem; }
.card.stat.center b { font-size: 2.4rem; }
/* Any card on the dark ground is a lift of it, not a white box - the numbers band's
   figures and the guide page's estimate factors alike. */
.panel.dark .card { background: var(--lift-fill);
  border-color: var(--lift-edge); color: #fff; text-align: left; }
.panel.dark .card h3 { color: #fff; }
.panel.dark .card.center { text-align: center; }
.panel.dark .card.stat b { color: var(--copper); }
/* The owner band: the portrait takes the narrow column and the words the wide one, and the
   portrait carries a seal - a check in a disc on its corner - as the about picture carries
   its figures. It sits narrower than the frame, centred, so the portrait stays a portrait. */
.split.owner { grid-template-columns: minmax(0, 2fr) minmax(0, 5fr); max-width: 64rem; margin-inline: auto; }
/* A prose band's one picture, given less of the row than the words.
   Two rules rather than one because the figure is always first in the source and .flip
   only reorders it - so the narrow column is the first or the second depending on which
   side the picture sits, and a single declaration would widen the picture on one of them. */
.split.narrow { grid-template-columns: minmax(0, 2fr) minmax(0, 3fr); }
.split.narrow.flip { grid-template-columns: minmax(0, 3fr) minmax(0, 2fr); }
/* ⚠ THE PICTURE TAKES THE WORDS' HEIGHT, NOT ITS OWN. A square beside three paragraphs sets
   its own height and the two columns end at different places, which reads as a picture and
   some text rather than as a pair. So the row stretches and the image fills it: height 0 with
   min-height 100% is the same trick the profile card uses - it fills the row it is given and
   can never grow it - and object-fit crops the long edges rather than squashing the frame.
   Stacked on a phone it goes back to the square, where there is no second column to match. */
.split.narrow, .split.narrow.flip { align-items: stretch; }
.split.narrow > .shot > img { width: 100%; height: 0; min-height: 100%;
  aspect-ratio: auto; object-fit: cover; }
.shot .seal { position: absolute; right: 1rem; bottom: 1rem; width: 3rem; height: 3rem;
  display: grid; place-items: center; border-radius: 50%; background: var(--brown);
  color: #fff; border: 3px solid var(--bg); }

/* A ticked claim: same layout and same mark whether it is the strip or the hero list.
   The type differs on purpose - the strip is small print, the hero list reads with the
   pitch above it - so only the shared half is shared. */
.trust li, .checks li { display: flex; align-items: center; gap: 0.5rem; }
/* Small icons are two families. A tick - the strip, the hero list, the why band - is navy,
   the link and button colour, because it says yes. A marker - the pin on a town, the plus
   on a question, the disc on a step - is brown, the accent, because it points at
   something. The town chip is a link, but its pin is a marker, not link furniture, so it
   does not take the link's navy. */
.trust svg, .checks svg { color: var(--navy); flex: none; }
.chip svg { color: var(--brown); flex: none; }
/* Above the body size, not under it. These are read at a glance rather than read through,
   and in rem they had quietly fallen behind the base size as that grew. */
.trust li { font-weight: 600; font-size: 1.2rem; }
.checks li { font-size: 1.1rem; font-weight: 500; }
/* Four short claims on one line, wrapping to two on a phone. A strip rather than a band -
   it keeps its own height - but it takes a tint and the same full-bleed trick the panels
   use: cancel the frame with a negative inline margin, put the content back with padding.
   Cream rather than sand, so it reads as its own step between the white hero and the
   tinted band below it. */
.trust { display: flex; flex-wrap: wrap; justify-content: center; gap: 1rem 2rem;
  background: var(--cream); margin-inline: calc(50% - 50vw);
  padding: 1rem calc(50vw - 50%); }

/* Five icons in a row. inline-flex rather than letter-spacing, since the spacing is now
   between elements; the gap is the old tracking. Both places it appears set their own
   surrounding space, so the row carries no margin of its own. */
.stars { display: inline-flex; gap: 2px; color: var(--star); }
/* A review card: the rating and the platform's mark share the top row, the quote fills
   the middle, and the byline - avatar, who and where, then service, platform and year -
   closes it above a rule. Cards are a column, so the quote takes the slack and every
   byline in a row lands on the same line. */
.card.review { gap: 0.9rem; }
.card.review .top { display: flex; align-items: center; justify-content: space-between; }
.card.review .top img { width: 24px; height: 24px; flex: none; }
.card.review > p { flex: 1; }
.card.review .byline { display: flex; align-items: center; gap: 0.75rem; padding-top: 0.9rem;
  border-top: 1px solid var(--border); }
.card.review .byline > .badge { margin: 0; flex: none; }
.card.review .byline .who { margin: 0; }
.card.review .byline .src { display: block; font-size: 0.9rem; color: var(--muted); }
/* The job in the byline links to its page. It is small print beside a name, so it is
   underlined rather than coloured - the accent here would pull the eye off the quote,
   which is the thing the card is for - and it takes the ink the rest of the line does not,
   so the one clickable word in the row is the one that reads darkest. */
.card.review .byline .src a { color: var(--ink); text-decoration: underline;
  text-underline-offset: 2px; text-decoration-color: var(--edge-hover); }
.card.review .byline .src a:hover { text-decoration-color: currentColor; }
.who { display: block; margin-top: 0.75rem; font-weight: 600; font-size: 1rem;
  font-style: normal; }

/* The enquiry form. One component, so the estimate band and the contact page never diverge.
   The coloured head is clipped into a shallow downward chevron: the notch points at the
   first field, which is the next thing you are meant to do. */

.quote { background: var(--bg); border: 1px solid var(--border);
  border-radius: var(--radius-card); overflow: hidden; }
.quote-head { background: var(--brown); color: #fff; text-align: center;
  padding: 1.35rem 1.25rem 1.6rem;
  clip-path: polygon(0 0, 100% 0, 100% calc(100% - 14px), 50% 100%, 0 calc(100% - 14px)); }
.quote-head .title { margin: 0; color: #fff; font-family: var(--heading); font-weight: 700;
  font-size: 1.35rem; line-height: 1.2; text-transform: uppercase; letter-spacing: 0.02em; }
/* Both lines white: the head is the one brown block on the page, and on brown the 80%
   white the dark bands use for small print reads as grey. The size does the softening. */
.quote-head p { margin-top: 0.3rem; font-size: 0.9rem; color: #fff; }
.quote-body { padding: 1.1rem var(--card-pad) var(--card-pad); }
/* The form is a white card and keeps its own colours on a dark band - the hero's photo
   must not turn its consent line, link or button cream. */
.panel.dark .quote { color: var(--ink); }
.panel.dark .quote .fine { color: var(--muted); }
.panel.dark .quote a { color: var(--navy); }
.panel.dark .quote .btn.solid { background: var(--navy); color: #fff; }
/* Its own head too: .panel.dark p is two classes and an element, so it outweighed the
   .quote-head rules above and greyed the one line the form opens on. */
.panel.dark .quote-head .title, .panel.dark .quote-head p { color: #fff; }
/* The name pair uses the site's own .grid; the fields carry the row spacing already. */
.quote .grid { gap: 0 0.75rem; }
/* THE SAME GAP ABOVE AS BELOW, AND BOTH THE FIELDS' OWN. Every .field.float carries
   margin-bottom 0.8rem, and a grid item's margin does not collapse out of the grid - so a
   top margin here was added to that 0.8rem and made the space above this line larger than
   the space below it, against a form whose whole rhythm is one number. It takes none of
   its own above and the fields' number below. */
.quote .fine { margin: 0 0 0.8rem; }

.quote .btn { width: 100%; } /* at every width, not only on a phone */

/* Form: one row shape reused by every field. */
.field span { font-weight: 600; font-size: 0.9rem; color: var(--ink); }
/* One row height across every control: a select falls back to line-height normal, which
   left it 4px shorter than the inputs beside it and under the 44px touch target. */
.field input, .field select, .field textarea {
  font: inherit; line-height: 1.6; width: 100%; padding: 10px 12px; background: var(--bg);
  border: 1px solid var(--border); border-radius: var(--radius-sm); color: var(--ink); }

/* The comparison table. Wide content scrolls in its own box so the page never does. */
.tablewrap { overflow-x: auto; background: var(--bg); border: 1px solid var(--border);
  border-radius: var(--radius-card); }
.compare { border-collapse: collapse; width: 100%; min-width: 40rem; }
.compare th, .compare td { text-align: left; padding: 0.8rem 1rem;
  border-bottom: 1px solid var(--border); vertical-align: top; }
.compare tbody tr:last-child th, .compare tbody tr:last-child td { border-bottom: 0; }
/* The column headings are labels, not readable copy, so they take the small-print colour. */
.compare thead th { font-family: var(--heading); font-size: 0.9rem; color: var(--muted);
  background: var(--cream); white-space: nowrap; }
.compare tbody th { font-family: var(--heading); font-weight: 600; }

/* Floating labels. The control gets room at its top edge; the label starts centred in
   the field as muted text, and moves up and shrinks on focus or once the field holds
   anything (:placeholder-shown, against a single-space placeholder). The select cannot be
   placeholder-shown, so it floats on focus and while an option with a value is chosen
   (option:checked, read through :has); its first option is blank, so until then the label
   is the only text in the field, exactly as on the inputs - not a floated label over a
   placeholder option saying the same thing twice. Same 44px-plus height as before, tighter
   spacing below. */
.field.float { display: block; position: relative; margin-bottom: 0.8rem; }
.field.float > input, .field.float > .select > select, .field.float > textarea {
  padding: 20px 12px 6px; }
.field.float > span:last-child { position: absolute; left: 13px; top: 50%;
  transform: translateY(-50%); font-weight: 400; font-size: 1rem; color: var(--muted);
  pointer-events: none; transition: top var(--t), font-size var(--t), transform var(--t); }
.field.float > :focus + span:last-child,
.field.float > :is(input, textarea):not(:placeholder-shown) + span:last-child,
.field.float > .select:focus-within + span:last-child,
.field.float > .select:not(:has(option[value='']:checked)) + span:last-child {
  top: 6px; transform: none; font-size: 0.75rem; font-weight: 600; letter-spacing: 0.02em; }
/* A textarea has no vertical middle to sit in: its label starts at the top line. */
.field.float.area > span:last-child { top: 14px; transform: none; }
.field.float > .select > select { padding-right: 2.4rem; }

/* One chevron on the site, and the select gets it too rather than the browser's own. */
.field .select { position: relative; display: block; }
.field .select select { appearance: none; -webkit-appearance: none; padding-right: 2.4rem; }
.field .select svg { position: absolute; right: 12px; top: 50%; transform: translateY(-50%);
  pointer-events: none; color: var(--muted); }

/* Address, phone, email, hours — the same list in the footer and the office block. */
.contact li { display: flex; align-items: flex-start; gap: 0.5rem; padding: 0.25rem 0; }
.prose > * + * { margin-top: 1rem; }
/* A titled run inside the prose - a legal page's clauses - opens with more room than a paragraph. */
.prose > * + h3 { margin-top: 1.75rem; }
/* The opening paragraph of a band that pairs copy with a picture: it holds the who,
   the where and the when, so it is weighted above the two that follow it. */
.prose .lead { font-weight: 600; }
.prose + .grid { margin-top: 2rem; }
.split .prose { margin: 1rem 0 1.75rem; }
/* The bottom margin holds a button off the paragraphs. A prose that closes its column has
   nothing to hold off, and stacked on a phone the margin would only double the gap between
   the last paragraph and the picture that follows it - the community band's, which sits
   after the copy. */
.split .prose:last-child { margin-bottom: 0; }

/* The questions band: head and call on the narrow side, the rows on the wide one, held to
   the top so the head does not drift as answers open beneath it. */
.faq-band { grid-template-columns: minmax(0, 2fr) minmax(0, 3fr); align-items: start; }
/* The head, intro and call button stay in view while the rows scroll past. The band is
   already top-aligned, which sticky needs; the offset clears the bar. Desktop only -
   on a phone the split is one column and a stuck head would sit over the rows. */
@media (min-width: 861px) {
  .faq-band > div:first-child { position: sticky; top: calc(var(--bar) + 1.5rem); }
}
.faq-band .btn { margin-top: 1.5rem; }
/* One boxed row per question (the box itself is shared with the why band, above) and the
   plus in its disc turning to a cross when the answer is open - two signals, not three. */
.faq { display: grid; gap: 0.6rem; }
.faq summary { display: flex; align-items: center; justify-content: space-between; gap: 1rem;
  padding: 0.9rem 1rem; cursor: pointer; list-style: none;
  font-family: var(--heading); font-size: 1.05rem; font-weight: 600; }
.faq summary::-webkit-details-marker { display: none; }
.faq summary svg { flex: none; width: 24px; height: 24px; padding: 4px; border-radius: 50%;
  background: var(--brown); color: #fff; transition: transform var(--t); }
.faq details[open] summary svg { transform: rotate(45deg); }
.faq .faq-a { padding: 0 1rem 1rem; }
/* The question is an h3 for the outline's sake; it keeps the summary's own type. */
.faq summary h3 { font: inherit; color: inherit; margin: 0; }
/* The breadcrumb trail wears the eyebrow's type; only the separators are its own. */
.crumbs ol { display: flex; flex-wrap: wrap; gap: 0.35rem; margin: 0; padding: 0; list-style: none; }
.crumbs li + li::before { content: '›'; margin-right: 0.35rem; }
.crumbs a { color: inherit; }
/* The banner: an inner page's opening band with no picture of its own - the trail and the
   h1 centred on the dark ground, with a little more room than a band gets, since it is the
   page's first screen. The h1 takes the hero scale through .hero. */
.banner { padding-block: 1rem; }
.banner .crumbs ol { justify-content: center; }
/* Eight milestones one under another, held narrower than the frame so they read as one
   story rather than a stack of full-width boxes. */
.timeline { max-width: 52rem; margin-inline: auto; }

@media (max-width: 860px) {
  /* The bar is one row, always --bar tall: the menu is an overlay hung off the header rather
     than a second flex line, so opening it cannot re-flow the brand or resize the bar
     under the reader's thumb. Stickiness is now set on the base rule, at every width. */
  header.site .container { gap: 0 0.75rem; flex-wrap: nowrap; }
  /* The mark takes what is left beside the burger and scales down inside it (see .brand img)
     rather than shoving the burger off the bar. */
  header.site .brand { flex: 1; --mark: 42px; }
  .nav-check { display: block; position: absolute; width: 1px; height: 1px; margin: -1px;
    padding: 0; border: 0; overflow: hidden; clip-path: inset(50%); }
  .nav-burger { display: grid; place-items: center; flex: none; margin-left: auto;
    width: 42px; height: 42px; cursor: pointer; background: var(--bg);
    border: 1px solid var(--border); border-radius: var(--radius-sm); }
  .nav-burger span { grid-area: 1 / 1; display: flex; }
  .nav-burger .open, .nav-check:checked ~ .nav-burger .closed { display: none; }
  .nav-check:checked ~ .nav-burger .open { display: flex; }
  .nav-check:focus-visible ~ .nav-burger { outline: var(--focus); outline-offset: 2px; }
  header.site nav { position: absolute; top: 100%; inset-inline: 0; margin-left: 0;
    display: grid; gap: 0; padding: 0 1.25rem; background: var(--bg);
    border-bottom: 1px solid var(--border);
    max-height: 0; overflow: hidden; visibility: hidden; }
  .nav-check:checked ~ nav { visibility: visible; max-height: calc(100vh - var(--bar));
    overflow-y: auto; padding-bottom: 0.9rem; }
  /* Every row is ruled off, the first one included, so the list opens on a line rather than
     running straight out of the bar. The bar's own line stands down while the panel is open
     - the two sit flush at top: 100% and would otherwise read as one 2px rule. */
  header.site:has(.nav-check:checked) { border-bottom-color: transparent; }
  /* A row in the burger panel, not a link in a bar: set at the h3 size in the heading face,
     because it is one of five things on the screen and it is being tapped, not scanned. The
     negative inline margin runs each row's rule to the panel's own edges - inset by the
     panel's padding, the rules stopped short of the border under the bar and read as a
     ragged stack rather than as a list. */
  header.site nav a { margin-inline: -1.25rem; padding: 0.65rem 1.25rem;
    border-top: 1px solid var(--border);
    font-family: var(--heading); font-size: 1.15rem; font-weight: 700; }
  /* The call is the last row and the only filled thing in the panel, so it takes the width
     the rows above it have rather than sitting short of them. */
  header.site nav > .btn { width: 100%; margin-top: 0.75rem; margin-inline: 0;
    border-top: 0; padding: 12px 24px; }
  /* In the burger panel the summary is a row like the links either side of it, so it
     spans the full width — otherwise its separator stops short of theirs and the caret
     floats mid-line instead of sitting at the edge. */
  .nav-dd { width: 100%; padding-block: 0; }
  .dd-summary { display: flex; justify-content: space-between; align-items: center;
    width: auto; margin-inline: -1.25rem; padding: 0.65rem 1.25rem;
    border-top: 1px solid var(--border);
    font-family: var(--heading); font-size: 1.15rem; font-weight: 700; }
  .dd-backdrop { display: none !important; }
  /* THE CHECKBOX IS THE ONLY OPENER HERE. On a bar, hover and focus-within open a dropdown
     because that is what a pointer expects; in the burger panel they are what stopped it
     closing. Tapping the label toggles the checkbox and leaves focus on it - the checkbox
     is inside .nav-dd, so :focus-within stayed true and held the panel open however many
     times it was tapped. Off, both of them, and :checked decides alone. */
  .nav-dd:hover > .dd-panel, .nav-dd:focus-within > .dd-panel { display: none; }
  /* Inside the burger panel the dropdown expands in flow rather than floating - and it is
     tinted, edge to edge, because sixteen services opening inside a list of five rows need
     to read as one block belonging to the row above rather than as eleven more rows of the
     same menu. The tint is the band token, so the panel borrows the page's own quiet
     alternation instead of introducing a colour. */
  .dd-check:checked ~ .dd-panel {
    display: block; position: static; columns: 1; width: auto; max-width: none; left: auto;
    border: 0; border-radius: 0; box-shadow: none; margin-inline: -1.25rem; padding: 0;
    background: var(--sand); }
  /* The links inside an open dropdown take the body face at body weight, a step down from
     the heading face the rows above them are set in, and they are indented past the row's
     text - the two together are what say these belong to Services rather than sit beside
     it. They are ruled off from each other like every other row, because sixteen untapped
     rows of the same colour with no line between them is a wall of text on a phone.
     Selected through header.site nav, not as a bare .dd-panel a: the row rule above is
     header.site nav a, which carries three element selectors to the bare one's one and was
     quietly winning the font-family - the submenu stayed serif however plainly the shorter
     rule asked for the sans. */
  header.site nav .dd-panel a { margin-inline: 0;
    padding: 0.7rem 1.25rem 0.7rem 2rem;
    /* --border is mixed for white and disappears on the sand, so the rule between these
       rows is the ink at low strength instead: the same hairline weight, still visible. */
    border-top: 1px solid color-mix(in srgb, var(--ink) 12%, transparent);
    font-family: var(--sans); font-size: 1rem; font-weight: 400; line-height: 1.4; }
  /* The first row's rule would double with the summary's own, which is already drawn. */
  header.site nav .dd-panel a:first-child { border-top: 0; }
  .split { grid-template-columns: minmax(0, 1fr); gap: 1.75rem; }
  /* Stacked, the hero opens on its photograph: a phone screen is one thing at a time, and
     the picture says what the business does before a word of it is read. It lies down to do
     it - a phone is 400-odd pixels wide, and a tall picture across that is most of the first
     screen spent before the heading is reached. */
  .split.hero > img { order: -1; height: auto; }
  :root { --band: 2.25rem; }
  /* Stacked, the hero ends on its rating row rather than on the picture beside it, and the
     full band under that leaves what follows adrift. Half is enough to separate them. On a
     desktop the picture and the words end level and the band is what squares them off, so
     it keeps the full measure there. "> .hero" and not "> .split.hero", so the about and
     contact banners - which are .hero.banner, the same opening band without a picture - get
     the same treatment as the three that have one. */
  .panel:has(> .hero) { padding-bottom: calc(var(--band) / 2); }
  /* place-items rather than display alone, so the glyph is centred in the disc. */
  .call-fab { display: grid; place-items: center; }
  /* Every call to action in the page body goes full width on a phone - the ones in an
     .actions row stack, and the ones that stand alone (the Why band, About, the FAQ, the
     show-all links) stretch to match. The nav's button has its own rule above. */
  main .btn { width: 100%; }
  /* The trust strip reads from the left here rather than centred, and a claim that needs
     two lines keeps its tick on the first.
     ⚠ FLEX, NOT A TWO-COLUMN GRID, BECAUSE EQUAL COLUMNS COUPLE THE CLAIMS. On a 1fr 1fr
     grid, two by two. The four claims are fixed furniture and all of one length now, so the
     objection to a grid here has gone with the per-site claims that raised it: nothing in
     the set can wrap to two lines and leave the claim beside it sitting in half a column of
     empty space. Auto columns rather than halves, so the pair is as wide as its widest
     claim and the block stays centred instead of spanning the screen. */
  .trust { display: grid; grid-template-columns: repeat(2, minmax(0, auto));
    justify-content: center; gap: 0.7rem 2rem; align-items: start; }
  .trust li { align-items: start; font-size: 1.05rem; }
  .trust svg { margin-top: 0.15rem; }
  /* The why band's button sits under the copy on a desktop and under the claims on a
     phone: the copy column dissolves into the stack, so the button can be ordered last and
     the stack's own gap spaces everything. */
  .why > div:first-child { display: contents; }
  .why .btn { order: 1; }
  .why .prose { margin: 0; }
  /* .split.beside outranks the .split rule above, so it is stacked here on its own; the
     stack then shows one landscape picture under the copy, the split's own ratio. */
  .split.beside, .split.owner, .split.narrow, .split.narrow.flip { grid-template-columns: minmax(0, 1fr); }
  /* Stacked, the profile's portrait gets its own height back, at the square the owner band uses. */
  .card.profile { grid-template-columns: minmax(0, 1fr); }
  .card.profile .shot > img { height: auto; min-height: 0; aspect-ratio: var(--ratio-square); }
  /* Same for the prose pairing: one column, so there is no second one to end level with. */
  .split.narrow > .shot > img { height: auto; min-height: 0; aspect-ratio: var(--ratio-square); }
  /* Stacked, the steps have no line to join them, so they stand further apart instead. */
  .grid:has(> .card.step) { gap: 2rem; }
  .grid:has(> .card.step)::before { display: none; }
  /* Picture cards go two across and drop their blurb, so a run of eight is four short rows
     rather than eight tall ones. A project keeps its caption and goes portrait to hold it. */
  .grid:has(> .card > img) { grid-template-columns: 1fr 1fr; gap: 0.75rem; }
  .grid:has(> .card > img) .card { --card-pad: 0.9rem; }
  .grid:has(> .card > img) .card > p { display: none; }
  .card > img { aspect-ratio: 2 / 1; }
  .card.project > img { aspect-ratio: 2 / 3; }
  .card.project .tag { font-size: 0.7rem; letter-spacing: 0.08em; }
  .card.project h3 { font-size: 1rem; }
  .card.project p { font-size: 0.875rem; }
  /* On a phone every head reads from the left, standfirst and closing note included: a
     centred paragraph is ragged on both edges, and past three lines that is hard work. The
     steps follow, disc and text on the left edge, so the one band with centred content under
     its head does not fight it. */
  .section-head, .note { text-align: left; }
  .section-head p:not(.eyebrow), .note { margin-inline: 0; }
  .banner .crumbs ol { justify-content: flex-start; }
  /* Stacked, a step is a row: the disc on the left, the title level with it and the body
     beneath, and a line running down from disc to disc in place of the line across. */
  .card.step { display: grid; grid-template-columns: 3.4rem minmax(0, 1fr); column-gap: 1rem;
    align-items: start; text-align: left; position: relative; padding: 0; }
  .card.step .badge { grid-row: 1 / span 2; margin: 0; }
  /* ⚠ NO MIN-HEIGHT. The badge spans both rows, so the heading never needed to be as tall
     as it - and a one-line heading centred in 3.4rem opened a gap between itself and the
     sentence it introduces that read as a missing paragraph. It hugs its text; the badge
     goes on spanning. */
  .card.step h3 { margin: 0 0 0.35rem; }
  .card.step::before { content: ''; position: absolute; left: 1.7rem; top: 1.7rem;
    bottom: calc(-2rem - 1.7rem); border-left: 1px solid color-mix(in srgb, var(--brown-deep) 22%, transparent); }
  .card.step:last-child::before { display: none; }
  /* Thumbs, not pointers: the small links get a 44px hit area without moving on the page. */
  .card .more { padding: 0.75rem 0; margin-bottom: -0.75rem; white-space: nowrap; }
  footer.site li { padding: 0; }
  footer.site li a, footer.site summary { display: inline-block; padding: 0.35rem 0; }
  .split.beside .stack { grid-auto-rows: auto; }
  .split.beside .stack > img { height: auto; aspect-ratio: var(--ratio-hero); }
  /* Beside a column of copy the pair dissolves into one column and its two pictures slot
     into the text: the first after the first paragraph, the second after the last. The
     areas pair (no prose) keeps its map as one box under the chips. */
  .split.beside:has(.prose) { gap: 1.25rem; }
  .split.beside:has(.prose) > div, .split.beside:has(.prose) .stack, .split.beside:has(.prose) .prose { display: contents; }
  .split.beside:has(.prose) .prose > p { margin: 0; }
  .split.beside:has(.prose) .prose > p:nth-child(n + 2) { order: 2; }
  .split.beside:has(.prose) .stack > img:first-child { order: 1; }
  .split.beside:has(.prose) .stack > img + img { order: 3; }
  .split.beside:not(:has(.prose)) .stack > img + img { display: none; }
}

/* ── phones ─────────────────────────────────────────────────────────────── */
/* The 860px block above is the tablet layout. These are the things a hand-held width wants
   differently from it, and nothing here is a new component - each one relaxes a rule that
   earns its place on a wider screen and stops earning it on a phone. */
@media (max-width: 34rem) {
  /* STACKED, AND THE TAGLINE SAYS ALL OF ITSELF. Side by side there is room for about
     twenty characters of tagline before the ellipsis starts eating it, and a truncated
     sentence is worse than a second line. The number goes under it on its own line, where
     it is also a wider tap target than it was squeezed against the right edge. */
  .topbar { font-size: 0.95rem; }
  .topbar .container { flex-direction: column; gap: 0; line-height: 1.35;
    padding-inline: 0.9rem; }
  .topbar span { overflow: visible; text-overflow: clip; white-space: normal;
    text-align: center; }

  /* ONE SERVICE PER ROW, WITH ITS SENTENCE. Two across is right on a tablet - sixteen jobs
     read as eight short rows - but on a phone it leaves each card about 150px wide, which
     is why the blurb was hidden: it could not fit. A service whose description cannot be
     read is a link wearing a photograph, not a card, so below this width the grid drops to
     one column and the paragraph comes back with it. Projects keep the two-up treatment on
     purpose: their evidence is the photograph and the figure in the tag, not a paragraph. */
  .grid:has(> .card > img):not(:has(> .card.project)) { grid-template-columns: minmax(0, 1fr); }
  .grid:has(> .card > img):not(:has(> .card.project)) .card > p { display: revert; }
  .grid:has(> .card > img):not(:has(> .card.project)) .card { --card-pad: 1.1rem; }

  /* One field per row. At --min 140px the auto-fill grid still fits two boxes across a
     phone, and a half-width First Name beside a half-width Last Name is two cramped taps
     and two truncated labels on the one form the whole site exists to have filled in. */
  .quote .grid { grid-template-columns: minmax(0, 1fr); }

  /* SIX SERVICES, THEN THE REST IN PLACE. One per row turns sixteen cards into a band the
     reader scrolls past rather than reads, so the band opens on six and the button under
     it uncovers the other ten - they are already in the page, and sending a phone to
     /services to see markup it has downloaded is a page load spent on nothing. The link
     to the full index is what shows at wider widths, where every card is visible anyway. */
  .panel:has(.more-check) .grid > .card:nth-child(n + 7) { display: none; }
  .panel:has(.more-check:checked) .grid > .card { display: revert; }
  .note > .more-toggle { display: inline-flex; }
  .note > a.btn { display: none; }
  .more-check:checked ~ .more-toggle { display: none; }

  /* ONE CLAIM PER LINE. Two by two is a tablet's shape; on a phone the longer two wrap
     inside their column and the block reads as a paragraph rather than as four checkable
     facts. One column, one claim, one tick. */
  .trust { grid-template-columns: minmax(0, auto); gap: 0.6rem; }
  /* Stacked, each claim is one line, so the tick sits level with it rather than at the top
     of a block it no longer has - the top alignment above is for the two-line claims a
     narrow column can still produce. */
  .trust li { align-items: center; text-align: left; }

  /* FOUR PROJECTS, ONE PER ROW. Two across on a phone gives each about 150px, and a
     project's whole argument is the photograph and the figure on it - both unreadable at
     that size. Four full-width cards make the point that six half-width ones did not. */
  .grid:has(> .card.project) { grid-template-columns: minmax(0, 1fr); }
  .grid:has(> .card.project) > .card.project:nth-child(n + 5) { display: none; }
  .card.project > img { aspect-ratio: var(--ratio-card); }
}

