/* ==========================================================================
   Crystal Waterproofing Renovations
   Structure: the page is a vertical section drawing. It opens at ground level
   in daylight, descends into a dark engineered band where the technical
   waterproofing lives, then re-emerges into a lit, finished basement.
   ========================================================================== */

/* --- Tokens --------------------------------------------------------------- */

:root {
  /* Tells the browser to theme native furniture — scrollbars, the select
     dropdown, the date picker — instead of drawing light widgets on a dark
     page. The dark palette itself lives at the foot of this file. */
  color-scheme: light dark;

  /* Ground — the world above the damp-proof line */
  --plaster:      #E8E6E1;   /* lime plaster, cool not cream */
  --plaster-deep: #DAD7D0;
  --paper:        #F4F3F0;

  /* Below ground — saturated earth and wet slate */
  --slate:        #0E1417;
  --slate-raised: #172025;
  --clay:         #2C3A42;
  --clay-light:   #55666F;

  /* Accent — the waterline. Engineered, damp, never cheerful.
     --water is a TEXT colour as much as a fill (.eyebrow, .product__role), and
     at #0F7A72 it measured 4.16:1 on --plaster and 3.61:1 on --plaster-deep —
     both short of AA for text this small. Deepened until it clears 4.5:1 on
     every ground it is actually used on. Same hue, one step further down. */
  --water:        #0B665E;
  --water-bright: #16A99E;
  --water-deep:   #0A4F4A;

  /* What sits ON the accent. White works over the dark teal used in daylight;
     in dark mode the accent brightens, so the label has to darken with it or
     the solid buttons lose their contrast. */
  --on-accent:    #FFFFFF;

  /* Semantic only — damp, warnings, failure states. Not decoration. */
  --rust:         #B4643C;

  /* Text */
  --ink:          #10181C;
  --ink-soft:     #46555D;
  /* Was #74838B, which measured 3.53:1 on --paper — it reads as "quiet" but it
     was quiet past the point of being legible. Deepened to clear 4.5:1 on all
     three light grounds (paper, plaster, plaster-deep). */
  --ink-faint:    #4F5E66;
  --on-dark:      #E8E6E1;
  --on-dark-soft: #A3B2BA;
  --on-dark-faint:#6B7C85;

  --rule:         #C9C5BC;
  --rule-dark:    #263238;

  /* --- Surface tokens ------------------------------------------------------
     These exist so the dark palette at the foot of the file is nothing but a
     list of token values. Before the theme button, dark mode could carry its
     own component rules inside one @media block; now that the same palette has
     to be reachable from a [data-theme] attribute as well, any component rule
     living in that block would have to be written twice. Tokens are written
     once and read from the base rule, so only the values below are duplicated.

     Each of these is a literal that could not follow another token: an rgba()
     fallback for a browser that has no color-mix(), or a filter. */

  /* Pre-color-mix() fallback for the sticky masthead. */
  --masthead-fb:    rgba(232, 230, 225, 0.92);

  /* Hero strata. --strata-line deliberately holds the whole color-mix()
     expression: Safari before 16.2 makes the custom property invalid, drops
     the declaration that uses it, and lands on the --strata-fb line before
     it — which is exactly the fallback behaviour this pair had when it was
     two hard-coded declarations. */
  --strata-opacity: 0.55;
  --strata-fb:      rgba(201, 197, 188, 0.45);
  --strata-line:    color-mix(in srgb, var(--rule) 45%, transparent);

  /* Product cut-outs are photographed on white and stay on a light plate in
     both themes. In daylight they need nothing; after dark they glare. */
  --plate-filter:   none;
  --spec-plate:     var(--plaster);

  /* Type */
  --display: "Newsreader", "Iowan Old Style", Georgia, serif;
  --body:    "Archivo", "Helvetica Neue", Arial, sans-serif;
  --mono:    "JetBrains Mono", "SF Mono", Consolas, monospace;

  /* Scale */
  --step--1: clamp(0.80rem, 0.78rem + 0.10vw, 0.86rem);
  --step-0:  clamp(1.00rem, 0.96rem + 0.20vw, 1.13rem);
  --step-1:  clamp(1.25rem, 1.16rem + 0.44vw, 1.55rem);
  --step-2:  clamp(1.56rem, 1.39rem + 0.85vw, 2.15rem);
  --step-3:  clamp(1.95rem, 1.64rem + 1.55vw, 3.00rem);
  --step-4:  clamp(2.44rem, 1.90rem + 2.68vw, 4.20rem);

  --gutter:  clamp(1.25rem, 5vw, 4rem);
  --measure: 68ch;
  --wide:    1240px;
}

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

html { scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}

body {
  margin: 0;
  background: var(--plaster);
  color: var(--ink);
  font-family: var(--body);
  font-size: var(--step-0);
  line-height: 1.62;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

h1, h2, h3 {
  font-family: var(--display);
  font-weight: 400;
  line-height: 1.08;
  letter-spacing: -0.015em;
  text-wrap: balance;
  margin: 0;
}

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

a { color: inherit; }

:where(a, button, input, textarea, select, summary):focus-visible {
  outline: 2px solid var(--water-bright);
  outline-offset: 3px;
  border-radius: 2px;
}

/* --- Shared primitives ---------------------------------------------------- */

.shell {
  width: min(100% - (var(--gutter) * 2), var(--wide));
  margin-inline: auto;
}

.measure { max-width: var(--measure); }

/* Eyebrow labels use the mono face — this trade runs on specifications
   (BS 8102, Type A/B/C, Grade 1–3), so a technical face is meaningful here. */
.eyebrow {
  font-family: var(--mono);
  font-size: var(--step--1);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--water);
  margin: 0 0 1.25rem;
  display: flex;
  align-items: center;
  gap: 0.75rem;
}
.eyebrow::before {
  content: "";
  width: 1.75rem;
  height: 1px;
  background: currentColor;
  flex: none;
}
.on-dark .eyebrow { color: var(--water-bright); }

.lede {
  font-size: var(--step-1);
  line-height: 1.5;
  color: var(--ink-soft);
  max-width: 58ch;
}
.on-dark .lede { color: var(--on-dark-soft); }

.btn {
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.95rem 1.75rem;
  font-family: var(--body);
  font-size: var(--step--1);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  text-decoration: none;
  border: 1px solid transparent;
  cursor: pointer;
  transition: background-color 0.18s ease, color 0.18s ease, border-color 0.18s ease;
}
.btn--solid  { background: var(--water); color: var(--on-accent); }
.btn--solid:hover  { background: var(--water-deep); }
.btn--ghost  { background: transparent; color: var(--ink); border-color: var(--rule); }
.btn--ghost:hover  { border-color: var(--ink); }
.on-dark .btn--ghost { color: var(--on-dark); border-color: var(--rule-dark); }
.on-dark .btn--ghost:hover { border-color: var(--on-dark-soft); }

/* --- Header --------------------------------------------------------------- */

.masthead {
  position: sticky;
  top: 0;
  z-index: 50;
  /* color-mix() is Safari 16.2+. Without a plain fallback first, older WebKit
     drops the declaration and the sticky masthead goes fully transparent, with
     the page scrolling visibly through the navigation. The fallback is a
     literal and so cannot follow --plaster; it comes from its own token. */
  background: var(--masthead-fb);
  background: color-mix(in srgb, var(--plaster) 92%, transparent);
  -webkit-backdrop-filter: blur(10px);   /* Safari still wants the prefix */
  backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--rule);
}
.masthead__in {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 2rem;
  padding-block: 1rem;
}
.wordmark { display: block; text-decoration: none; line-height: 1; }

/* The whole logo — crystal, name and both straplines — re-laid horizontally
   from the client's artwork (392x104, so 3.769:1). Width and height are both
   declared rather than left to aspect-ratio, which is Safari 15+; at this size
   a wrong guess is a visibly squashed logo, not a subtle reflow.

   A background-image, not an <img>, on purpose: the lettering is near black and
   would vanish on the night masthead, so there are two files, and only the one
   in use is ever fetched. Two <img> tags with one set to display:none would
   download both. The ?v= is manual because CSS cannot call asset() — bump it if
   the artwork is ever replaced, or a returning visitor keeps the old logo for a
   month under the caching rules further down this file. */
.wordmark__lockup {
  display: block;
  width: 12.25rem;
  height: 3.25rem;
  background: url('/assets/brand/logo-nav.png?v=2') left center / contain no-repeat;
}
/* Same three-state pattern as the colour tokens: the bare rule above is light,
   the OS preference is honoured unless an explicit light choice overrides it,
   and an explicit dark choice wins outright. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .wordmark__lockup {
    background-image: url('/assets/brand/logo-nav-dark.png?v=2');
  }
}
:root[data-theme="dark"] .wordmark__lockup {
  background-image: url('/assets/brand/logo-nav-dark.png?v=2');
}

/* 1.5rem, not 2rem. The theme button is a seventh item in a row that had about
   eighty pixels of slack at 1280, and it took all of them — every page in the
   site overflowed by 17px. Six gaps at half a rem less buys back more than the
   button costs, and at this size the rhythm is indistinguishable.

   One thing found while fitting the logo and worth keeping in mind here: the
   theme button's LABEL changes with its state, and "Light" is wider than
   "Auto" and "Dark", so the masthead is a few pixels wider for a visitor who
   has chosen light than for one who has not. A width can therefore pass in one
   theme state and overflow in another — which is why mast-logo.mjs takes the
   state as an argument and is run three times. There is comfortable slack now
   that the wordmark is a 196px logo rather than 380px of live text. */
.nav { display: flex; align-items: center; gap: 1.5rem; }
.nav a {
  font-size: var(--step--1);
  font-weight: 500;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--ink-soft);
  transition: color 0.15s ease;
}
.nav a:hover { color: var(--ink); }
.nav__call {
  font-family: var(--mono);
  font-weight: 500 !important;
  color: var(--ink) !important;
  text-transform: none !important;
  letter-spacing: 0 !important;
}
@media (max-width: 860px) {
  .nav a:not(.nav__call):not(.btn) { display: none; }
  .nav { gap: 1rem; }
}

/* --- Hero: ground level --------------------------------------------------- */

.hero {
  position: relative;
  padding-block: clamp(4rem, 11vh, 8rem) clamp(5rem, 13vh, 9rem);
  overflow: hidden;
}
/* Two columns above 900px, stacked below. The copy keeps its own measure rather
   than being stretched by the grid, and the plate is the narrower track — a
   photograph of a job is evidence for the claim, not the claim itself. */
.hero__in {
  display: grid;
  grid-template-columns: minmax(0, 1.15fr) minmax(0, 0.85fr);
  gap: clamp(2rem, 5vw, 4.5rem);
  align-items: center;
}
.hero__copy { min-width: 0; }

.hero__plate { margin: 0; min-width: 0; }
.hero__plate img {
  display: block;
  width: 100%;
  height: auto;
  /* Landscape source now (1600x739, so 2.17:1) in a 16/10 window. The frame is
     wider than the column can ever use, so cover trims the dark wall at the
     left edge and the sheeted stair at the right and keeps the lit run of
     floor, which is the half that carries the picture. Was 4/5 for the earlier
     portrait shot — a landscape source in that window showed a centre sliver. */
  aspect-ratio: 16 / 10;
  object-fit: cover;
  object-position: 50% 50%;
  border: 1px solid var(--rule);
  filter: var(--plate-filter);
}
.hero__plate figcaption {
  margin-top: 0.75rem;
  font-size: var(--step--1);
  color: var(--ink-soft);
  max-width: 34ch;
}

@media (max-width: 900px) {
  /* Stacked, and the photograph goes second — the headline has to be the first
     thing on a phone, not a picture the reader has to scroll past to find out
     what the company does. */
  .hero__in { grid-template-columns: 1fr; }
  .hero__plate { margin-top: 2.5rem; }
  /* The 16/10 override that used to live here is gone: the base rule is now
     16/10 for every width, because the source itself is landscape. */
}

.hero h1 {
  font-size: var(--step-4);
  max-width: 15ch;
  margin-bottom: 1.75rem;
}
.hero h1 em {
  font-style: italic;
  color: var(--water);
}
.hero__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
  margin-top: 2.5rem;
}

/* The section drawing sitting behind the hero copy. Canvas-free, pure CSS —
   horizontal strata that darken with depth. */
.hero__strata {
  position: absolute;
  inset: 0;
  z-index: -1;
  opacity: var(--strata-opacity);
  pointer-events: none;
  /* Fallback first: without it, older Safari drops the whole gradient and the
     hero loses its strata entirely. In dark mode the hairlines have to become
     lighter than the ground rather than darker, or the geology disappears —
     which is now a change of token value, not a second rule. */
  background:
    repeating-linear-gradient(
      to bottom,
      transparent 0 38px,
      var(--strata-fb) 38px 39px
    );
  background:
    repeating-linear-gradient(
      to bottom,
      transparent 0 38px,
      var(--strata-line) 38px 39px
    );
  mask-image: linear-gradient(to bottom, transparent, #000 70%);
  -webkit-mask-image: linear-gradient(to bottom, transparent, #000 70%);
}

/* --- Trust bar: the damp-proof line --------------------------------------- */

.trustbar {
  border-block: 1px solid var(--rule);
  background: var(--paper);
}
.trustbar__in {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 230px), 1fr));
}
.cred {
  display: flex;
  align-items: baseline;
  gap: 1rem;
  padding: 1.5rem 0;
}
.trustbar__in > .cred + .cred { border-left: 1px solid var(--rule); padding-left: 2rem; }
@media (max-width: 760px) {
  .trustbar__in > .cred + .cred { border-left: 0; padding-left: 0; border-top: 1px solid var(--rule); }
}
.cred__abbr {
  font-family: var(--mono);
  font-size: var(--step-0);
  font-weight: 700;
  color: var(--water);
  letter-spacing: 0.02em;
  flex: none;
}
.cred__name { font-size: var(--step--1); font-weight: 600; display: block; }
.cred__note { font-size: var(--step--1); color: var(--ink-faint); }

/* --- The descent: transition into the ground ------------------------------ */

.descent {
  height: clamp(3rem, 8vw, 6rem);
  background: linear-gradient(to bottom, var(--plaster), var(--slate));
}

/* --- Below-ground band ---------------------------------------------------- */

.on-dark {
  background: var(--slate);
  color: var(--on-dark);
}
.on-dark h1, .on-dark h2, .on-dark h3 { color: var(--on-dark); }

.band { padding-block: clamp(4rem, 10vw, 7.5rem); }

.band__head {
  display: grid;
  gap: 1.5rem;
  margin-bottom: clamp(2.5rem, 6vw, 4rem);
}
.band__head h2 { font-size: var(--step-3); max-width: 18ch; }

/* Service cards — a specification sheet, not a marketing tile. The Type
   designation is the organising fact, so it leads. */
/* Hairlines are drawn by the cards themselves (right + bottom) with the
   container supplying top + left. A 1px gap over a coloured background would
   be simpler, but it paints leftover cells whenever the card count doesn't
   fill the last row — this way a short row just ends. */
.specs {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 300px), 1fr));
  gap: 0;
  border: 1px solid var(--rule-dark);
  border-width: 1px 0 0 1px;
}
.spec {
  background: var(--slate);
  border-right: 1px solid var(--rule-dark);
  border-bottom: 1px solid var(--rule-dark);
  padding: clamp(1.75rem, 3vw, 2.5rem);
  display: flex;
  flex-direction: column;
  gap: 1rem;
  transition: background-color 0.2s ease;
}
.spec:hover { background: var(--slate-raised); }
.spec__type {
  font-family: var(--mono);
  font-size: var(--step--1);
  letter-spacing: 0.1em;
  color: var(--water-bright);
}
.spec h3 { font-size: var(--step-1); }
.spec p { color: var(--on-dark-soft); font-size: var(--step--1); line-height: 1.6; }
.spec__note {
  margin-top: auto;
  padding-top: 1rem;
  border-top: 1px solid var(--rule-dark);
  font-family: var(--mono);
  font-size: 0.72rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--on-dark-faint);
}

/* --- Process: a real sequence, so numbering earns its place --------------- */

.steps {
  display: grid;
  gap: 0;
  counter-reset: step;
  margin-top: 2rem;
}
.step {
  display: grid;
  grid-template-columns: 4.5rem 1fr;
  gap: clamp(1rem, 3vw, 2.5rem);
  padding-block: 2rem;
  border-top: 1px solid var(--rule-dark);
  counter-increment: step;
}
.step:last-child { border-bottom: 1px solid var(--rule-dark); }
.step::before {
  content: counter(step, decimal-leading-zero);
  font-family: var(--mono);
  font-size: var(--step-1);
  color: var(--water-bright);
  line-height: 1;
  padding-top: 0.35rem;
}
.step h3 { font-size: var(--step-1); margin-bottom: 0.6rem; }
.step p { color: var(--on-dark-soft); max-width: 62ch; font-size: var(--step--1); }

/* --- Re-emergence: the finished basement ---------------------------------- */

.ascent {
  height: clamp(3rem, 8vw, 6rem);
  background: linear-gradient(to bottom, var(--slate), var(--plaster));
}

.uses {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr));
  gap: 1.5rem;
  margin-top: 2.5rem;
}
.use {
  border: 1px solid var(--rule);
  padding: 1.75rem;
  background: var(--paper);
}
.use h3 { font-size: var(--step-1); margin-bottom: 0.5rem; }
.use p { font-size: var(--step--1); color: var(--ink-soft); }

/* --- Story ---------------------------------------------------------------- */

.story {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 380px), 1fr));
  gap: clamp(2rem, 5vw, 4.5rem);
  align-items: start;
}
.story p + p { margin-top: 1.25rem; }
.story__aside {
  border-left: 2px solid var(--water);
  padding-left: 1.75rem;
}
.story__pull {
  font-family: var(--display);
  font-size: var(--step-2);
  line-height: 1.25;
  font-style: italic;
}

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

.quote { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 340px), 1fr)); gap: clamp(2rem, 5vw, 4.5rem); }

.form { display: grid; gap: 1.25rem; margin-top: 2rem; }
.field { display: grid; gap: 0.45rem; }
.field--pair { display: grid; grid-template-columns: 1fr 1fr; gap: 1.25rem; }
@media (max-width: 560px) { .field--pair { grid-template-columns: 1fr; } }

.field label {
  font-family: var(--mono);
  font-size: 0.74rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--on-dark-soft);
}
.field .req { color: var(--water-bright); }

.field input,
.field select,
.field textarea {
  font-family: var(--body);
  font-size: var(--step-0);
  color: var(--on-dark);
  background: var(--slate-raised);
  border: 1px solid var(--rule-dark);
  padding: 0.85rem 1rem;
  width: 100%;
  transition: border-color 0.15s ease;
}
.field textarea { resize: vertical; min-height: 8rem; line-height: 1.55; }
.field input:hover,
.field select:hover,
.field textarea:hover { border-color: var(--clay-light); }
.field input:focus,
.field select:focus,
.field textarea:focus { border-color: var(--water-bright); outline-offset: 1px; }
.field input::placeholder,
.field textarea::placeholder { color: var(--on-dark-faint); }

/* Honeypot — hidden from people, visible to bots that fill every field. */
.hp {
  position: absolute !important;
  left: -9999px;
  width: 1px; height: 1px;
  overflow: hidden;
}

.form__foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 1.25rem;
  margin-top: 0.5rem;
}
.form__note { font-size: 0.78rem; color: var(--on-dark-faint); max-width: 34ch; }

.notice {
  padding: 1rem 1.25rem;
  border-left: 3px solid;
  font-size: var(--step--1);
}
/* Plain rgba first for Safari < 16.2 — these two carry the "enquiry sent" and
   "enquiry failed" states, so losing the background loses the signal. */
.notice--ok   { border-color: var(--water-bright); background: rgba(15, 122, 114, 0.14); }
.notice--bad  { border-color: var(--rust);         background: rgba(180, 100, 60, 0.14); }
.notice--ok   { background: color-mix(in srgb, var(--water) 14%, transparent); }
.notice--bad  { background: color-mix(in srgb, var(--rust) 14%, transparent); }
.notice ul { margin: 0.5rem 0 0; padding-left: 1.1rem; }

.contact__direct { display: grid; gap: 1.5rem; margin-top: 2rem; }
.direct__item { display: grid; gap: 0.25rem; }
.direct__label {
  font-family: var(--mono);
  font-size: 0.74rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--on-dark-faint);
}
.direct__value { font-size: var(--step-1); font-family: var(--display); }
.direct__value a { text-decoration: none; border-bottom: 1px solid var(--rule-dark); }
.direct__value a:hover { border-color: var(--water-bright); }

/* --- Location pages ------------------------------------------------------- */

.ground-table {
  width: 100%;
  border-collapse: collapse;
  margin-top: 2rem;
  font-size: var(--step--1);
}
.ground-table th,
.ground-table td {
  text-align: left;
  padding: 1rem 1.25rem 1rem 0;
  border-bottom: 1px solid var(--rule);
  vertical-align: top;
}
.ground-table th {
  font-family: var(--mono);
  font-weight: 500;
  font-size: 0.74rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-faint);
  width: 32%;
  white-space: nowrap;
}
@media (max-width: 620px) {
  .ground-table th, .ground-table td { display: block; width: auto; padding-right: 0; }
  .ground-table th { border-bottom: 0; padding-bottom: 0.25rem; }
}

.nearby { display: flex; flex-wrap: wrap; gap: 0.6rem; margin-top: 1.5rem; }
.nearby a {
  font-size: var(--step--1);
  text-decoration: none;
  padding: 0.5rem 1rem;
  border: 1px solid var(--rule);
  color: var(--ink-soft);
  transition: border-color 0.15s ease, color 0.15s ease;
}
.nearby a:hover { border-color: var(--water); color: var(--water); }

/* --- Areas index (linked from footer, keeps location pages crawlable) ----- */

.arealist {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 200px), 1fr));
  gap: 0.5rem 2rem;
  margin-top: 1.5rem;
  padding: 0;
  list-style: none;
}
.arealist a {
  font-size: var(--step--1);
  color: var(--on-dark-soft);
  text-decoration: none;
  display: block;
  padding: 0.35rem 0;
  border-bottom: 1px solid var(--rule-dark);
  transition: color 0.15s ease;
}
.arealist a:hover { color: var(--water-bright); }

/* --- Footer --------------------------------------------------------------- */

.footer { background: var(--slate); color: var(--on-dark-soft); padding-block: clamp(3rem, 7vw, 5rem) 2rem; }
.footer h2 { font-size: var(--step-1); margin-bottom: 0.5rem; }
/* The footer band is var(--slate) in BOTH colour schemes, so the mark only
   ever sits on near-black here and needs no theme swap. This is the one place
   with room to show it at a size where the cut of the crystal reads. */
.footer__mark { display: block; height: 3.25rem; width: auto; margin-bottom: 1rem; }
.footer__strap {
  font-family: var(--display);
  font-style: italic;
  color: var(--on-dark);
  margin-bottom: 0.35rem;
}
.footer__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr));
  gap: 2.5rem;
  padding-bottom: 2.5rem;
  border-bottom: 1px solid var(--rule-dark);
}
.footer a { color: var(--on-dark-soft); }
.footer a:hover { color: var(--water-bright); }
.footer__title {
  font-family: var(--mono);
  font-size: 0.74rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--on-dark-faint);
  margin-bottom: 1rem;
}
.footer__list { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.5rem; font-size: var(--step--1); }
.footer__bar {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 1rem;
  padding-top: 2rem;
  font-size: 0.78rem;
  color: var(--on-dark-faint);
}

.footer__areas { padding-top: 2.5rem; }

/* --- Adjustments for the built templates ---------------------------------- */

/* Contain the strata graphic's negative z-index inside the hero, so it can
   never slip behind the page background. */
.hero { isolation: isolate; }

/* Location and index pages carry longer headings than the homepage. */
.hero--area h1 {
  max-width: 24ch;
  font-size: var(--step-3);
}

/* .band__head already spaces its children with gap. */
.band__head .eyebrow { margin-bottom: 0; }

.notice { margin-bottom: 2rem; }

/* Native selects ignore most of the field styling on Windows, so draw the
   control ourselves and supply the arrow. */
.field select {
  appearance: none;
  -webkit-appearance: none;
  padding-right: 2.75rem;
  background-image: linear-gradient(45deg, transparent 50%, var(--on-dark-soft) 50%),
                    linear-gradient(135deg, var(--on-dark-soft) 50%, transparent 50%);
  background-position: calc(100% - 1.25rem) 55%, calc(100% - 1rem) 55%;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
}
.field select option { background: var(--slate-raised); color: var(--on-dark); }

/* Skip link — visible only once focused. */
.skip {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 100;
  background: var(--water);
  color: var(--on-accent);
  padding: 0.85rem 1.5rem;
  font-size: var(--step--1);
  text-decoration: none;
}
.skip:focus {
  left: 0;
}

/* .nav a sets a colour and beats .btn--solid on specificity, which left the
   header button's label near-invisible on its own teal. Restate it here. */
.nav .btn--solid { color: var(--on-accent); }
.nav .btn--ghost { color: var(--ink); }

/* Below ~560px the wordmark, phone number and CTA together are wider than the
   viewport, which pushed the whole page sideways. The phone number is the
   CTA that matters on a phone, so the button steps aside. */
/* The wordmark used to be live text 33 characters long, and needed a nowrap
   rule here to stop the sticky masthead becoming two rows deep. It is now a
   fixed-size image, so there is nothing left to wrap and that rule has gone. */
.nav__call,
.nav .btn { white-space: nowrap; }

/* Between tablet and full desktop the row is wordmark + four links + phone +
   CTA, and with a 33-character wordmark that no longer fits on one line. The
   nav is allowed to wrap onto a second row rather than being pushed off the
   right edge — nothing in a masthead is optional enough to silently drop.

   Raised from 1180 to 1279 when the theme button arrived. The single row now
   needs about 1250px of content, so 1181–1240 was overflowing the page while
   1280 still fitted — a failure that hides between the round numbers people
   spot-check at, and the reason the masthead is swept across the whole range
   rather than sampled. */
@media (max-width: 1279px) {
  .masthead__in { flex-wrap: wrap; gap: 0.75rem 1.25rem; }
  .nav {
    flex-wrap: wrap;
    justify-content: flex-end;
    gap: 0.75rem 1.25rem;
    margin-left: auto;
  }
}

/* Only 861–1180px is genuinely tight: that is the band where all four text
   links are still shown (they are hidden below 861px by the rule further up)
   AND the long wordmark is competing with the phone number and the CTA. The
   phone is the item to drop, because the CTA beside it, the WhatsApp button
   and the footer all still carry it.
   Scoped with a min-width on purpose — applying it to every width below 1180
   also stripped the phone number off the mobile masthead, where the text links
   are already hidden and it is the single most useful thing in the header. */
@media (min-width: 861px) and (max-width: 1180px) {
  .nav__call { display: none; }
  /* Around 900px the row missed fitting by a couple of pixels and wrapped the
     whole nav onto a second line. Tightening the gap buys that back. */
  .nav { gap: 0.75rem 1rem; }
}

@media (max-width: 560px) {
  .masthead__in { gap: 1rem; }
  .nav .btn { display: none; }
  /* 2.6rem tall keeps the straplines just legible while leaving room for the
     phone number, which is the CTA that matters on a phone. */
  .wordmark__lockup { width: 9.8rem; height: 2.6rem; }
}

/* Keep the label and its value in the same eyeline instead of throwing them
   to opposite ends of a 1240px row. */
.ground-table { max-width: 78ch; }

/* --- Breadcrumbs ----------------------------------------------------------
   Sits directly under the sticky masthead, above the hero. Kept quiet — it is
   orientation and structured data, not a feature of the page. */

.crumbs {
  border-bottom: 1px solid var(--rule);
  background: var(--paper);
  font-size: var(--step--1);
}
.crumbs ol {
  list-style: none;
  margin: 0;
  padding: 0.85rem 0;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem 0.75rem;
}
.crumbs li { display: flex; align-items: center; gap: 0.75rem; }
.crumbs li + li::before {
  content: "";
  width: 5px;
  height: 5px;
  border-top: 1px solid var(--ink-faint);
  border-right: 1px solid var(--ink-faint);
  transform: rotate(45deg);
  flex: none;
}
.crumbs a { color: var(--ink-soft); text-decoration: none; }
.crumbs a:hover { color: var(--water); }
.crumbs [aria-current="page"] { color: var(--ink-faint); }

/* --- Long-form prose (service pages) -------------------------------------- */

.prose { max-width: var(--measure); }
.prose--next { margin-top: clamp(2.5rem, 6vw, 4rem); }
.prose h2 { font-size: var(--step-2); margin-bottom: 1.5rem; }
.prose p + p { margin-top: 1.25rem; }
.prose strong { font-weight: 600; }

/* --- FAQs ------------------------------------------------------------------
   Rendered as real, visible content because the FAQPage schema claims it is. */

.faqs { display: grid; gap: 0; border-top: 1px solid var(--rule-dark); }
.faq { border-bottom: 1px solid var(--rule-dark); }
.faq summary {
  cursor: pointer;
  list-style: none;
  padding: 1.4rem 3rem 1.4rem 0;
  position: relative;
  font-family: var(--display);
  font-size: var(--step-1);
  line-height: 1.3;
  color: var(--on-dark);
}
.faq summary::-webkit-details-marker { display: none; }
.faq summary::after {
  content: "";
  position: absolute;
  right: 0.5rem;
  top: 1.9rem;
  width: 9px;
  height: 9px;
  border-right: 1.5px solid var(--water-bright);
  border-bottom: 1.5px solid var(--water-bright);
  transform: rotate(45deg);
  transition: transform 0.2s ease;
}
.faq[open] summary::after { transform: rotate(-135deg); }
.faq summary:hover { color: var(--water-bright); }
.faq__a { padding: 0 3rem 1.6rem 0; }
.faq__a p { color: var(--on-dark-soft); max-width: 68ch; font-size: var(--step--1); }

/* --- Link treatments inside cards ----------------------------------------- */

.spec h3 a,
.use h3 a { color: inherit; text-decoration: none; }
.spec h3 a:hover { color: var(--water-bright); }
.spec__note a { color: var(--water-bright); text-decoration: none; }
.spec__note a:hover { text-decoration: underline; }

/* Area page's link back up to its region hub. */
.uplink {
  margin-top: 2rem;
  font-size: var(--step--1);
  color: var(--ink-soft);
}
.uplink a { color: var(--water); }

/* --- Area list variants ---------------------------------------------------- */

.band--tight { padding-block: clamp(2.5rem, 6vw, 4rem); }
.band__head h2 a { color: inherit; text-decoration: none; }
.band__head h2 a:hover { color: var(--water); }

.arealist--regions a { font-size: var(--step-0); }
.arealist__pc {
  display: block;
  font-family: var(--mono);
  font-size: 0.72rem;
  color: var(--on-dark-faint);
  letter-spacing: 0.04em;
}
.arealist-wrap { margin-top: 1rem; }

.visually-hidden {
  position: absolute !important;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* --- Product plates --------------------------------------------------------
   Manufacturer product shots are photographed on white. Dropped straight into
   the dark specs grid they read as glaring holes, so each one sits on its own
   light plate — a spec-sheet convention rather than a failed attempt to blend. */

/* The plate has a fixed height and clips, and the image fills it with
   object-fit rather than relying on max-height: 100% — inside a grid item that
   percentage does not always resolve, and when it fails the image spills out
   over the card's heading. */
.spec__shot {
  /* Stays light in both themes. The shot is merged into the plate with
     mix-blend-mode: multiply below, and multiplying against a dark plate drives
     every channel to near-black and the product disappears. */
  background: var(--spec-plate);
  filter: var(--plate-filter);
  border: 1px solid var(--rule-dark);
  height: 150px;
  overflow: hidden;
  padding: 0.7rem;
  margin-bottom: 0.25rem;
}
.spec__shot img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
  mix-blend-mode: multiply;   /* merges the shot's white ground into the plate */
}

/* --- Product cards ---------------------------------------------------------- */

.products {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 420px), 1fr));
  gap: 0;
  margin-top: 2.5rem;
  border: 1px solid var(--rule);
  border-width: 1px 0 0 1px;
}
.product {
  border-right: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
  padding: clamp(1.5rem, 3vw, 2rem);
  display: grid;
  grid-template-columns: 132px 1fr;
  gap: 1.5rem;
  align-items: start;
  background: var(--paper);
}
.product__shot {
  background: #fff;
  /* A white plate on a dark page glares. Taking the brightness off it is
     better than pretending a cut-out photographed on white can be re-themed. */
  filter: var(--plate-filter);
  border: 1px solid var(--rule);
  aspect-ratio: 1;
  overflow: hidden;
  padding: 0.5rem;
}
.product__shot img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
}
.product__role {
  font-family: var(--mono);
  font-size: 0.7rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--water);
  margin-bottom: 0.4rem;
}
.product h3 { font-size: var(--step-1); margin-bottom: 0.6rem; line-height: 1.25; }
/* text-decoration rather than a border, because these headings wrap to two
   lines and a border-bottom draws a full-width rule under each of them. */
.product h3 a {
  color: inherit;
  text-decoration: underline;
  text-decoration-color: var(--rule);
  text-decoration-thickness: 1px;
  text-underline-offset: 4px;
}
.product h3 a:hover { color: var(--water); text-decoration-color: var(--water); }
.product__blurb { font-size: var(--step--1); color: var(--ink-soft); }
.product__specs {
  list-style: none;
  padding: 0;
  margin: 1rem 0 0;
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
}
.product__specs li {
  font-family: var(--mono);
  font-size: 0.68rem;
  letter-spacing: 0.03em;
  color: var(--ink-faint);
  border: 1px solid var(--rule);
  padding: 0.2rem 0.5rem;
}

/* --- Downloads -------------------------------------------------------------- */

.docs { list-style: none; padding: 0; margin: 1.25rem 0 0; display: grid; gap: 0; }
.docs--wide { margin-top: 2rem; border-top: 1px solid var(--rule); }
.doc { border-bottom: 1px solid var(--rule); }
.doc > a {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.5rem 1rem;
  padding: 0.7rem 0;
  text-decoration: none;
  color: var(--ink);
}
.doc > a:hover .doc__label { color: var(--water); }
.doc__label { font-size: var(--step--1); border-bottom: 1px solid transparent; }
.doc__meta {
  font-family: var(--mono);
  font-size: 0.68rem;
  letter-spacing: 0.06em;
  color: var(--ink-faint);
  white-space: nowrap;
}
.doc__note {
  font-size: var(--step--1);
  color: var(--ink-soft);
  max-width: 62ch;
  padding-bottom: 0.9rem;
  margin-top: -0.2rem;
}
.doc--lead > a { padding-block: 1.1rem 0.5rem; }
.doc--lead .doc__label { font-family: var(--display); font-size: var(--step-1); }

/* On dark sections the same list has to invert. */
.on-dark .docs, .on-dark .docs--wide { border-color: var(--rule-dark); }
.on-dark .doc { border-color: var(--rule-dark); }
.on-dark .doc > a { color: var(--on-dark); }
.on-dark .doc > a:hover .doc__label { color: var(--water-bright); }
.on-dark .doc__meta { color: var(--on-dark-faint); }
.on-dark .doc__note { color: var(--on-dark-soft); }

/* --- Notices ---------------------------------------------------------------- */

.notice {
  border-left: 2px solid var(--rust);
  padding: 0.25rem 0 0.25rem 1.5rem;
  margin-top: 2.5rem;
  max-width: var(--measure);
}
.notice p { font-size: var(--step--1); color: var(--ink-soft); }
.notice p + p { margin-top: 0.75rem; }
.notice strong { color: var(--ink); }

/* --- Section drawings -------------------------------------------------------
   Inline SVG rather than images: the callouts are real text, it stays sharp at
   any size, and the whole drawing is a couple of KB. The sheet is always light,
   including inside .on-dark sections, where it reads as a drawing pinned up
   rather than a diagram that failed to theme. */

.diagram { margin: clamp(2rem, 5vw, 3rem) 0 0; }
/* A drawing is on paper, in daylight and after dark alike — so the sheet does
   not follow the page down, and restating the light token values here re-lights
   everything inside it in one place, the numbered legend included. Written
   unconditionally rather than inside the dark block: these ARE the light values,
   so in daylight the rule changes nothing, and in dark mode it holds the sheet
   light without needing a second copy for the theme button to reach. */
.diagram__frame {
  --ink:      #10181C;
  --ink-soft: #46555D;
  --ink-faint:#4F5E66;
  --rule:     #C9C5BC;
  --water:    #0B665E;
  background: #F4F3F0;
  border: 1px solid #C9C5BC;
  padding: clamp(0.75rem, 2vw, 1.5rem);
  overflow-x: auto;          /* the drawing scrolls, never the page */
  -webkit-overflow-scrolling: touch;
}
.diagram figcaption {
  font-size: var(--step--1);
  color: var(--ink-soft);
  max-width: var(--measure);
  margin-top: 1rem;
}
.on-dark .diagram figcaption { color: var(--on-dark-soft); }

.dg { display: block; width: 100%; min-width: 620px; height: auto; }

/* A raster drawing inside the same frame. Only one of these exists — Wykamol's
   Wp012 detail — and it gets the SAME min-width as the SVG rather than
   max-width: 100%, deliberately: shrinking a drawing whose labels are pixels
   until it fits a 360px phone makes it unreadable, whereas .diagram__frame
   already scrolls horizontally and the SVG drawings already rely on that. */
.dg-raster { display: block; width: 100%; min-width: 620px; height: auto; }

/* Materials */
.dg-earth      { fill: #D6D0C6; }
.dg-wall       { fill: #E4E1DA; }
.dg-concrete   { fill: #DEDBD4; }
.dg-screed     { fill: #E4E1DA; }
.dg-finish     { fill: #FBFAF8; stroke: #10181C; stroke-width: 1.2; }
.dg-floorfinish{ fill: #2C3A42; }
.dg-channel    { fill: #2C3A42; }
.dg-void       { fill: #9FD8D2; }
.dg-sump       { fill: #EFEDE9; }
.dg-sumpwater  { fill: #9FD8D2; }
.dg-pump       { fill: #2C3A42; }
.dg-alarm      { fill: #B4643C; }
.dg-fillet     { fill: #C7C1B5; stroke: #10181C; stroke-width: 1.2; }
.dg-barrier    { fill: none; stroke: #0A4F4A; stroke-width: 7; stroke-linejoin: round; stroke-linecap: square; }
.dg-spray      { fill: #16A99E; fill-opacity: 0.16; }
.dg-plant      { fill: #2C3A42; }
.dg-waterstop  { fill: #0F7A72; }
.dg-waterstop-fin { stroke: #0F7A72; stroke-width: 3; }
.dg-joint      { stroke: #10181C; stroke-width: 2; stroke-dasharray: 7 4; }
.dg-rebar      { stroke: #8B9AA2; stroke-width: 1; stroke-dasharray: 4 4; }
.dg-rebar-dot  { fill: #55666F; }
.dg-pipe-fill  { fill: #EFEDE9; }
.dg-float      { fill: #0F7A72; }
.dg-valve      { fill: #F4F3F0; stroke: #10181C; stroke-width: 2; }
.dg-valve-mark { stroke: #10181C; stroke-width: 2.4; }
.dg-panel      { fill: #EFEDE9; }
.dg-level      { stroke: #16A99E; stroke-width: 1.6; stroke-dasharray: 8 5; }
.dg-level--alarm { stroke: #B4643C; }
.dg-gateway    { fill: #2C3A42; }
.dg-phone      { fill: #EFEDE9; }
.dg-sensor     { fill: #0F7A72; stroke: #F4F3F0; stroke-width: 2; }
.dg-signal     { fill: none; stroke: #16A99E; stroke-width: 2; stroke-linecap: round; }

/* Lines */
.dg-hatch      { stroke: #A9A296; stroke-width: 1; }
.dg-hatch--faint { stroke: #BDB7AC; }
.dg-hair       { stroke: #B4AEA3; stroke-width: 1; }
.dg-outline    { fill: none; stroke: #10181C; stroke-width: 1.6; }
.dg-ground     { stroke: #10181C; stroke-width: 2.2; }
.dg-membrane   { fill: none; stroke: #0F7A72; stroke-width: 2.6; stroke-linejoin: miter; }
.dg-pipe       { fill: none; stroke: #10181C; stroke-width: 5; stroke-linejoin: round; }
/* Detection cable. Teal because it is a water device, dashed and thin so it
   reads as a run of sensing rather than as a pipe carrying anything. */
.dg-cable      { fill: none; stroke: #0F7A72; stroke-width: 2.2; stroke-dasharray: 3 4; stroke-linecap: round; }
/* Ductwork. Ink rather than teal, because on every other drawing here teal is
   water and rust is damp — air has no colour of its own and should not borrow
   one that already means something. Supply solid, extract dashed, which is the
   convention a real ventilation drawing uses, and the arrowhead is an open
   chevron rather than the filled teal triangle so a duct never reads as a pipe
   carrying water. */
.dg-duct          { fill: none; stroke: #10181C; stroke-width: 2.4; stroke-linejoin: round; stroke-linecap: round; }
.dg-duct--extract { stroke-dasharray: 9 5; }
.dg-air-head      { fill: none; stroke: #10181C; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }
/* Condensate is water, so it is allowed the waterline teal — but thin, because
   it is a trickle rather than the drained system's discharge. */
.dg-cond          { fill: none; stroke: #0F7A72; stroke-width: 1.8; stroke-linejoin: round; }
.dg-unit          { fill: #DDD9D1; }
.dg-term          { fill: #C9C5BC; }
/* The sealed collar. Solid ink so it reads as something deliberately formed,
   not as a gap where the duct passes through. */
.dg-collar        { fill: #10181C; }
.dg-watertable { stroke: #16A99E; stroke-width: 1.6; stroke-dasharray: 10 6; }
.dg-flow       { stroke: #16A99E; stroke-width: 2.2; }
.dg-flow-head  { fill: #16A99E; }
.dg-leader     { stroke: #74838B; stroke-width: 1; }
.dg-marker     { fill: #F4F3F0; stroke: #10181C; stroke-width: 1.5; }

/* Text inside the drawing */
.dg text { font-family: var(--mono); }
.dg-num  { font-size: 13px; font-weight: 700; fill: #10181C; text-anchor: middle; }
.dg-note { font-size: 11px; letter-spacing: 0.09em; fill: #74838B; }
.dg-note--water { fill: #0F7A72; }
.dg-room { font-size: 12.5px; letter-spacing: 0.14em; fill: #46555D; }
.dg-tiny { font-size: 10px; letter-spacing: 0.06em; fill: #E8E6E1; text-anchor: middle; }
.dg-tiny--ink { fill: #46555D; }
.dg-dim  { font-size: 10.5px; letter-spacing: 0.06em; fill: #46555D; text-anchor: middle; }
.dg-dim--alarm { fill: #B4643C; }

/* Legend — numbered to match the callouts, and real text so it reflows. */
.dg-legend {
  list-style: none;
  counter-reset: dgitem;
  padding: 0;
  margin: 1.5rem 0 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 320px), 1fr));
  gap: 0.9rem 2.5rem;
}
.dg-legend li {
  counter-increment: dgitem;
  position: relative;
  padding-left: 2.2rem;
  font-size: var(--step--1);
  color: var(--ink-soft);
  line-height: 1.55;
}
.dg-legend li::before {
  content: counter(dgitem);
  position: absolute;
  left: 0;
  top: 0.05em;
  width: 1.55rem;
  height: 1.55rem;
  border: 1px solid var(--rule);
  border-radius: 50%;
  display: grid;
  place-items: center;
  font-family: var(--mono);
  font-size: 0.7rem;
  font-weight: 700;
  color: var(--ink);
}
.dg-legend strong { color: var(--ink); font-weight: 600; }

/* No .on-dark override for the legend: it lives inside .diagram__frame, which
   is always the light drawing sheet. Inverting it there paints light text on a
   light ground. Only the figcaption sits on the section background, and it is
   handled above. */

/* --- Library groupings and section links ------------------------------------ */

.on-plaster { background: var(--plaster-deep); }

.libgroup + .libgroup { margin-top: clamp(2.5rem, 6vw, 4rem); }
.libgroup h3 { font-size: var(--step-2); }
.libgroup__note {
  font-size: var(--step--1);
  color: var(--ink-soft);
  max-width: var(--measure);
  margin-top: 0.5rem;
}

.band__more { margin-top: 2rem; font-size: var(--step--1); }
.band__more a { color: var(--water); text-decoration: none; border-bottom: 1px solid currentColor; }
.on-dark .band__more a { color: var(--water-bright); }

/* --- WhatsApp ---------------------------------------------------------------
   The glyph carries WhatsApp's green so it is recognised instantly, but it sits
   on the site's own slate rather than being a green blob — this is a firm
   quoting six-figure basement work, and the button has to look like it belongs
   to them rather than bolted on.

   No JavaScript anywhere on this site, so the float is always present rather
   than appearing on scroll. */

.wa-float {
  position: fixed;
  right: 1.25rem;
  /* Plain value first. clamp() nested inside calc() has a history of parsing
     badly in WebKit, and a dropped `bottom` on a fixed element is the kind of
     failure that leaves the button somewhere nobody can see it. */
  bottom: 1.25rem;
  bottom: calc(1.25rem + env(safe-area-inset-bottom, 0px));
  z-index: 60;
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.8rem 1.15rem;
  background: var(--slate);
  color: var(--on-dark);
  border: 1px solid var(--rule-dark);
  text-decoration: none;
  font-size: var(--step--1);
  line-height: 1;
  box-shadow: 0 6px 22px rgba(14, 20, 23, 0.28);
  transition: background-color 0.18s ease, transform 0.18s ease;
}
.wa-float:hover,
.wa-float:focus-visible {
  background: var(--slate-raised);
  color: var(--on-dark);
  transform: translateY(-2px);
}
.wa-float__icon { color: #25D366; flex: none; }
.wa-float__label { white-space: nowrap; }

/* Below the point where the masthead CTA is already hidden, drop the label and
   leave a circular icon button — a pill wide enough to read is a pill wide
   enough to sit on top of the content underneath it. */
@media (min-width: 561px) {
  .wa-float { right: 1.75rem; bottom: 1.75rem; }
  .wa-float { bottom: calc(1.75rem + env(safe-area-inset-bottom, 0px)); }
}

@media (max-width: 560px) {
  .wa-float { padding: 0.85rem; border-radius: 50%; gap: 0; }
  /* iOS Safari puts its address bar at the BOTTOM of the screen and lets it
     overlay the layout viewport, so anything pinned near bottom:0 sits behind
     the browser's own chrome and looks like it was never rendered. Sitting the
     button clear of that band is the difference between "there is no WhatsApp
     icon" and a button people can actually tap. */
  .wa-float { bottom: 4.5rem; }
  .wa-float { bottom: calc(4.5rem + env(safe-area-inset-bottom, 0px)); }
  .wa-float__label {
    position: absolute;
    width: 1px; height: 1px;
    margin: -1px; padding: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
  }
}

/* The alternative offered under the enquiry form. */
.wa-alt { margin-top: 1.25rem; font-size: var(--step--1); }
.wa-alt a {
  display: inline-flex;
  align-items: center;
  gap: 0.55rem;
  color: var(--on-dark-soft);
  text-decoration: none;
  border-bottom: 1px solid transparent;
}
.wa-alt a:hover { color: var(--on-dark); border-bottom-color: var(--water-bright); }
.wa-alt .wa__icon { color: #25D366; flex: none; }

/* The form sits inside .on-dark on every page that uses it, but keep the light
   case correct in case it is ever moved. */
.wa-alt a { color: var(--ink-soft); }
.on-dark .wa-alt a { color: var(--on-dark-soft); }
.on-dark .wa-alt a:hover { color: var(--on-dark); }

@media (max-width: 560px) {
  .product { grid-template-columns: 1fr; }
  .product__shot { width: 132px; }
}

/* --- The monitoring panel ----------------------------------------------------
   A working illustration of the dashboard, built from tokens rather than shown
   as a screenshot. Everything here therefore follows the page into dark mode on
   its own, which is the main reason it exists: a photograph of somebody's white
   interface needs a brightness filter to sit on a dark page, and a filter is a
   patch over a picture we cannot re-theme.

   It is instrument panel, not chrome — mono throughout, hard corners, one hair
   rule per tile. See includes/monitor-panel.php for the honesty rule. */

.monitor { margin: clamp(2rem, 5vw, 3rem) 0 0; }
.monitor__eyebrow {
  font-family: var(--mono);
  font-size: 0.72rem;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin: 0 0 0.7rem;
}
.monitor__frame {
  background: var(--paper);
  border: 1px solid var(--rule);
}

/* The header strip. The badge lives here rather than under the figure so that
   it stays beside the readings — including in anybody's screenshot of it. */
.monitor__bar {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1rem;
  align-items: baseline;
  justify-content: space-between;
  padding: 0.85rem clamp(0.9rem, 2.5vw, 1.3rem);
  border-bottom: 1px solid var(--rule);
}
.monitor__site {
  margin: 0;
  font-family: var(--mono);
  font-size: 0.78rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink);
}
.monitor__badge {
  margin: 0;
  font-family: var(--mono);
  font-size: 0.68rem;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--rust);
  border: 1px solid currentColor;
  padding: 0.15rem 0.45rem;
}

/* Explicit column counts, not auto-fit.
   There are six reading tiles, and auto-fit resolved to five columns at desktop
   — which left one tile alone on the second row beside four empty cells showing
   through as a grey void. Six divides evenly by 1, 2 and 3, so the breakpoints
   step through exactly those and every row is always full. Adding a seventh
   tile means revisiting this, which is the correct amount of friction. */
.monitor__grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 1px;
  /* The gap is the rule. One background behind a 1px grid gap draws every
     divider between tiles without a border on each one meeting its neighbour
     and doubling. */
  background: var(--rule);
  border-top: 0;
}
@media (min-width: 560px) { .monitor__grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 900px) { .monitor__grid { grid-template-columns: repeat(3, 1fr); } }

.tile {
  background: var(--paper);
  padding: clamp(0.9rem, 2.5vw, 1.25rem);
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
}
.tile--wide { grid-column: 1 / -1; }

.tile__label {
  margin: 0;
  font-family: var(--mono);
  font-size: 0.68rem;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--ink-faint);
  /* The status is carried by the dot AND by the words in the tile, never by
     colour alone — a red tile means nothing to a third of colourblind readers
     and nothing at all to a screen reader. */
  display: flex;
  align-items: center;
  gap: 0.45rem;
}
.tile__label::before {
  content: "";
  width: 7px; height: 7px;
  border-radius: 50%;
  background: var(--water);
  flex: none;
}
.tile--watch .tile__label::before { background: var(--rust); }

.tile__value {
  margin: 0;
  font-family: var(--mono);
  font-size: clamp(1.35rem, 1.1rem + 1vw, 1.85rem);
  line-height: 1.1;
  color: var(--ink);
  /* Digits that change every four seconds must not reflow the tile around
     them. Tabular figures keep every glyph the same width. */
  font-variant-numeric: tabular-nums;
}
.tile--watch .tile__value { color: var(--rust); }
.tile__unit {
  font-size: 0.62em;
  color: var(--ink-faint);
  margin-left: 0.15em;
}
.tile__note {
  margin: 0;
  font-size: var(--step--1);
  color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
}

/* The gateway tile reads as the header of the set rather than one more
   reading, so it gets the accent ground and loses the status dot. */
.tile--wide {
  background: var(--plaster);
  flex-direction: row;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.4rem 1.1rem;
}
.tile--wide .tile__label::before { display: none; }
.tile--wide .tile__value { font-size: clamp(1.1rem, 1rem + 0.5vw, 1.35rem); color: var(--water); }

.monitor figcaption {
  font-size: var(--step--1);
  color: var(--ink-soft);
  max-width: var(--measure);
  margin-top: 1rem;
}
.monitor figcaption strong { color: var(--ink); font-weight: 600; }
/* The credit is a requirement, not a footnote, so it sits on its own line
   where it cannot be skimmed past. */
.monitor__credit {
  display: block;
  margin-top: 0.6rem;
  font-family: var(--mono);
  font-size: 0.72rem;
  letter-spacing: 0.06em;
  /* --ink-soft, not --ink-faint: this sits on --plaster-deep, the darkest of
     the light grounds, and a credit line is the one thing on the figure that
     must stay readable. */
  color: var(--ink-soft);
}
.monitor__credit a { color: inherit; text-underline-offset: 3px; }
.on-dark .monitor figcaption { color: var(--on-dark-soft); }
.on-dark .monitor figcaption strong { color: var(--on-dark); }
.on-dark .monitor__credit { color: var(--on-dark-faint); }
.on-dark .monitor__eyebrow { color: var(--on-dark-faint); }

/* --- The sensor schedule -----------------------------------------------------
   A drawing set lists its components in a schedule, so this one does too: mark,
   device, purpose, position. Same construction as the monitor panel above it —
   one background showing through 1px gaps — so the two read as pages from the
   same set rather than as two different widgets that happen to be adjacent.

   Deliberately NOT a <table>. Twelve rows of prose in three columns cannot be
   read across on a 390px screen, and a real table there can only be scrolled
   sideways. Below 720px each row becomes a record: mark, name, what it catches,
   then the position as a labelled line. See includes/sensor-schedule.php. */

.schedule { margin: clamp(2rem, 5vw, 3rem) 0 0; }
.schedule__intro {
  max-width: var(--measure);
  color: var(--ink-soft);
  margin: 0 0 clamp(1.5rem, 4vw, 2.25rem);
}
.schedule__intro strong { color: var(--ink); font-weight: 600; }

.schedule__frame {
  display: flex;
  flex-direction: column;
  gap: 1px;
  background: var(--rule);
  border: 1px solid var(--rule);
}

.srow {
  background: var(--paper);
  display: grid;
  grid-template-columns: 1fr;
  gap: 0.5rem 1.4rem;
  padding: clamp(0.95rem, 2.5vw, 1.25rem);
  align-items: start;
}

/* The mark is a specification reference — it is what a sensor gets called on
   the survey sheet — so it is boxed and monospaced rather than set as a
   heading number that could be mistaken for reading order. */
.srow__mark {
  margin: 0;
  font-family: var(--mono);
  font-size: 0.78rem;
  font-weight: 500;
  letter-spacing: 0.06em;
  color: var(--water);
  border: 1px solid var(--water);
  padding: 0.2rem 0;
  text-align: center;
  width: 3.4rem;
  font-variant-numeric: tabular-nums;
}
/* The gateway and the routers measure nothing. Giving them the accent would
   say they are sensors, which is the one thing the schedule is trying not to
   imply — so they get the neutral rule instead. */
.srow--infra .srow__mark { color: var(--ink-faint); border-color: var(--rule); }

.srow__body { min-width: 0; }
.srow__name {
  font-family: var(--body);
  font-size: var(--step-0);
  font-weight: 600;
  color: var(--ink);
  margin: 0 0 0.3rem;
  line-height: 1.3;
}
.srow__catches {
  margin: 0;
  font-size: var(--step--1);
  color: var(--ink-soft);
  line-height: 1.6;
  max-width: 52ch;
}
.srow__catches strong { color: var(--ink); font-weight: 600; }

/* Below the breakpoint the columns stack, so "Position" needs saying again on
   every row — a column heading sitting over a stack is simply wrong. Above it
   the header row carries the word and this label is redundant, so it goes. */
.srow__where {
  margin: 0;
  font-family: var(--mono);
  font-size: 0.72rem;
  line-height: 1.55;
  color: var(--ink-faint);
}
.srow__where::before {
  content: "Position — ";
  letter-spacing: 0.06em;
  text-transform: uppercase;
  font-size: 0.9em;
}

.srow--head { display: none; }

@media (min-width: 720px) {
  .srow {
    grid-template-columns: 3.4rem minmax(0, 1fr) 13rem;
    align-items: start;
  }
  .srow__mark { width: auto; }
  .srow__where::before { content: none; }

  .srow--head {
    display: grid;
    background: var(--plaster);
    padding-block: 0.6rem;
  }
  .srow--head p {
    margin: 0;
    font-family: var(--mono);
    font-size: 0.66rem;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--ink-faint);
    border: 0;
    padding: 0;
    text-align: left;
    font-weight: 500;
  }
}

/* --- The two notes under the schedule -------------------------------------- */

.schedule__notes {
  display: grid;
  grid-template-columns: 1fr;
  gap: 1.5rem;
  margin-top: clamp(1.75rem, 4vw, 2.5rem);
}
@media (min-width: 900px) {
  .schedule__notes { grid-template-columns: repeat(2, 1fr); gap: 2rem; }
}

.snote {
  border-left: 2px solid var(--water);
  padding-left: clamp(1rem, 2.5vw, 1.4rem);
}
.snote h3 {
  font-family: var(--display);
  font-size: var(--step-1);
  font-weight: 400;
  color: var(--ink);
  margin: 0 0 0.6rem;
  line-height: 1.25;
}
.snote p {
  margin: 0 0 0.8rem;
  font-size: var(--step--1);
  color: var(--ink-soft);
  line-height: 1.65;
}
.snote p:last-child { margin-bottom: 0; }
.snote strong { color: var(--ink); font-weight: 600; }

/* The caution is marked by the rule colour AND by its heading reading as a
   caution, never by colour on its own. */
.snote--caution { border-left-color: var(--rust); }

/* --- Photographs of our own work ---------------------------------------------
   Same construction as the schedule and the monitor panel: a 1px gap over the
   rule colour, so every divider is drawn once and no two borders meet and
   double up.

   No dark-mode block. A photograph is a plate laid on the page rather than a
   surface of it — the same argument that keeps .diagram__frame on paper in both
   themes — so it borrows the two tokens the product shots already use.
   --plate-filter takes the glare off a bright photo on a dark ground, which is
   the whole of what these need after dark. */

.gallery__intro {
  max-width: var(--measure);
  color: var(--ink-soft);
  margin: 0 0 clamp(1.5rem, 4vw, 2.25rem);
}
.gallery__intro strong { color: var(--ink); font-weight: 600; }

.gallery__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 20rem), 1fr));
  /* Real gaps, NOT the 1px-over-the-rule-colour trick used by the schedule and
     the monitor panel. Two reasons, both found by looking at it rather than by
     reasoning about it: a divided grid has to stretch every figure to the row
     height, which left a block of dead ground under the short captions; and it
     forces portrait and landscape frames to a common height, which crops a
     1600x739 shot down to its middle third. Photographs bring their own hard
     edges and do not need drawn ones. */
  gap: clamp(1.25rem, 2.5vw, 1.75rem);
  align-items: start;
}

.shot {
  margin: 0;
  display: flex;
  flex-direction: column;
}
.shot__frame {
  background: var(--spec-plate);
  filter: var(--plate-filter);
  border: 1px solid var(--rule);
  overflow: hidden;
  /* A consistent window on to inconsistent source material. object-fit: cover
     crops rather than distorts, and a portrait shot gets a taller window so it
     is not reduced to a letterbox of its own middle. */
  aspect-ratio: 4 / 3;
}
.shot--tall .shot__frame { aspect-ratio: 3 / 4; }
.shot__frame img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.shot figcaption {
  padding-top: 0.75rem;
  font-size: var(--step--1);
  line-height: 1.6;
  color: var(--ink-soft);
}
.shot figcaption strong { color: var(--ink); font-weight: 600; }

.gallery__note {
  margin: clamp(1.25rem, 3vw, 1.75rem) 0 0;
  max-width: var(--measure);
  font-size: var(--step--1);
  color: var(--ink-faint);
}

/* On the darkest light ground the caption needs the stronger ink, for the same
   reason the monitor credit does. */
.on-plaster .shot figcaption { color: var(--ink-soft); }

/* Declared BEFORE the media query that turns it on. The other way round, this
   base rule wins at equal specificity and the hint never appears — which is
   invisible in the markup and only shows up if you measure the computed style. */
.gallery__hint {
  display: none;
  margin: 0.85rem 0 0;
  font-family: var(--mono);
  font-size: 0.7rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

/* On a phone the grid becomes a swipeable row.
   ---------------------------------------------------------------------------
   Stacked one-up, six portrait photographs and their captions run to roughly
   four thousand pixels, and everything below the gallery falls off the end of
   the page. A snap-scrolling row fixes that without the two things a normal
   carousel costs you: nothing is hidden from the DOM, so every caption is still
   real text for a search engine and a screen reader, and it needs NO
   JavaScript — it is scroll-snap and overflow, which is the same technique
   .diagram__frame already uses. The page body still never scrolls sideways;
   only this container does.

   Desktop keeps the grid. There the whole point is being able to take in the
   sequence at once. */
@media (max-width: 700px) {
  .gallery__grid {
    display: flex;
    grid-template-columns: none;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    -webkit-overflow-scrolling: touch;
    scroll-padding-left: 0;
    /* Room for the shadow of the next frame, so it is obvious there is more. */
    gap: 0.85rem;
    padding-bottom: 0.5rem;
  }
  .gallery__grid .shot {
    flex: 0 0 82%;
    scroll-snap-align: start;
  }
  .gallery__grid .shot figcaption { min-height: 7.5em; }
  .gallery__hint { display: block; }
}

/* ==========================================================================
   Dark mode
   --------------------------------------------------------------------------
   Three states, not two. A visitor is in exactly one of them:

     no [data-theme] stamp   → follow the device. The default, and what
                               everyone gets who never touches the button —
                               including anyone with JavaScript off, since the
                               stamp is only ever written by the button.
     [data-theme="light"]    → light, whatever the device says.
     [data-theme="dark"]     → dark, whatever the device says.

   Hence the two blocks below. The media query is guarded with
   :not([data-theme="light"]) so an explicit light choice beats a dark OS, and
   the attribute block repeats the same values so an explicit dark choice beats
   a light OS. THE TWO LISTS MUST STAY IN SYNC — changing a colour in one and
   not the other produces a bug that only appears for people who have pressed
   the button, which is nobody testing it locally.

   Nothing but token values may go in either block. Anything a component needs
   for dark mode gets a token in :root instead (see "Surface tokens" at the top
   of the file); a component rule written here would have to be written twice
   and eventually would be written once.

   The page is a section drawing that descends from daylight into the ground,
   and that rhythm is what has to survive. So the two grounds move together
   rather than inverting: the daylight bands become a lit room after dark, and
   the .on-dark technical bands go DEEPER still, staying the darker of the two.
   Inverting them would have put the "below ground" band above the "above
   ground" one in brightness, which reads backwards.

   Two things stay light on purpose, and it is the same reason in both cases —
   they are plates laid on the page, not surfaces of it: the section drawings
   (a drawing is on paper) and the product shots (cut-outs photographed on
   white, which a dark plate would frame with a white rectangle).
   ========================================================================== */

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    /* Ground — the room above, at night */
    --plaster:      #151C20;
    --plaster-deep: #101619;
    --paper:        #1B2429;   /* cards and frames, lifted off the ground */

    /* Below ground — deeper than the ground above it, as in daylight */
    --slate:        #090D10;
    --slate-raised: #121A1E;
    --clay:         #34444D;
    --clay-light:   #6B7C85;

    /* The waterline brightens: #0F7A72 is a text colour for paper, and on a
       dark ground it falls to about 1.8:1 — unreadable. */
    --water:        #2BB3A6;
    --water-bright: #46C9BC;
    --water-deep:   #1B8A80;
    --on-accent:    #06201D;   /* labels on the brightened accent */

    --rust:         #D07A4E;

    /* Text. --ink is now light; the --on-dark set is unchanged, so every
       existing .on-dark rule keeps working untouched. */
    --ink:          #E8E6E1;
    --ink-soft:     #A9B6BD;
    --ink-faint:    #7E8D95;

    --rule:         #2A353B;
    --rule-dark:    #1E282D;

    /* Surfaces. The masthead fallback keeps Safari before 16.2 from painting a
       near-white bar across a dark page; the strata hairlines have to become
       lighter than the ground rather than darker, or the geology disappears;
       and the white product plates need the glare taken off them. */
    --masthead-fb:    rgba(21, 28, 32, 0.92);
    --strata-opacity: 0.4;
    --strata-fb:      rgba(169, 182, 189, 0.30);
    --strata-line:    color-mix(in srgb, var(--ink-faint) 34%, transparent);
    --plate-filter:   brightness(0.94);
    --spec-plate:     #E8E6E1;
  }
}

/* Keep in sync with the block above. */
:root[data-theme="dark"] {
  --plaster:      #151C20;
  --plaster-deep: #101619;
  --paper:        #1B2429;

  --slate:        #090D10;
  --slate-raised: #121A1E;
  --clay:         #34444D;
  --clay-light:   #6B7C85;

  --water:        #2BB3A6;
  --water-bright: #46C9BC;
  --water-deep:   #1B8A80;
  --on-accent:    #06201D;

  --rust:         #D07A4E;

  --ink:          #E8E6E1;
  --ink-soft:     #A9B6BD;
  --ink-faint:    #7E8D95;

  --rule:         #2A353B;
  --rule-dark:    #1E282D;

  --masthead-fb:    rgba(21, 28, 32, 0.92);
  --strata-opacity: 0.4;
  --strata-fb:      rgba(169, 182, 189, 0.30);
  --strata-line:    color-mix(in srgb, var(--ink-faint) 34%, transparent);
  --plate-filter:   brightness(0.94);
  --spec-plate:     #E8E6E1;
}

/* The tokens above only paint what this stylesheet draws. Native furniture —
   scrollbars, the select dropdown in the enquiry form, the focus ring — is the
   browser's, and it follows color-scheme. Left at "light dark" it would keep
   following the OS after somebody had chosen otherwise, so an explicit choice
   pins it too. */
:root[data-theme="light"] { color-scheme: only light; }
:root[data-theme="dark"]  { color-scheme: only dark; }

/* --- The theme button -------------------------------------------------------
   Icon plus a mono word, because the third state is the problem: sun and moon
   are universal, and there is no glyph that reliably says "follow the device"
   at 32px. Below 861px — where the nav links are already hidden — the word goes
   to .visually-hidden rather than display:none, so it stops taking space but
   stays the button's accessible name. */

.themetoggle {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  flex: none;
  background: none;
  border: 1px solid var(--rule);
  color: var(--ink-soft);
  font-family: var(--mono);
  font-size: 0.66rem;
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  padding: 0.42rem 0.6rem;
  cursor: pointer;
  transition: color 0.15s ease, border-color 0.15s ease;
}
.themetoggle:hover { color: var(--ink); border-color: var(--ink-faint); }
.themetoggle:focus-visible {
  outline: 2px solid var(--water);
  outline-offset: 2px;
}
.themetoggle svg {
  width: 15px;
  height: 15px;
  flex: none;
  /* currentColor throughout, so the icon is one more thing that needs no dark
     variant. */
  fill: none;
  stroke: currentColor;
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}
/* The three icons are all in the DOM; the script sets data-state on the button
   and CSS decides which one is drawn. Cheaper and less breakable than building
   SVG in JavaScript, and it means the button is already correct in the markup
   before any script runs. */
.themetoggle [data-icon] { display: none; }
.themetoggle[data-state="auto"]  [data-icon="auto"],
.themetoggle[data-state="light"] [data-icon="light"],
.themetoggle[data-state="dark"]  [data-icon="dark"] { display: block; }

@media (max-width: 860px) {
  .themetoggle__label {
    position: absolute !important;
    width: 1px; height: 1px;
    margin: -1px; padding: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
    border: 0;
  }
  .themetoggle { gap: 0; padding: 0.42rem 0.5rem; }
}

/* --- Site-wide notice --------------------------------------------------------
   A temporary message the owner writes in the admin panel — closed for a week,
   away on site, phone diverted. It sits directly under the masthead and scrolls
   away with the page rather than following the reader down it: this is
   information, not an alert, and a bar that cannot be scrolled past reads as
   the second kind however politely it is worded.

   Two levels only. --water for ordinary news, --rust for something that changes
   what a visitor should do (we are closed, use WhatsApp instead). A third level
   would only invite the first two to be used carelessly.

   Each background is declared twice on purpose: a browser without color-mix()
   drops the second line and lands on a real token rather than on nothing, which
   is the same fallback pattern the masthead and the hero strata use. */
.sitenotice {
  background: var(--plaster-deep);
  background: color-mix(in srgb, var(--water) 13%, var(--paper));
  border-bottom: 1px solid var(--rule);
  color: var(--ink);
  padding-block: 0.7rem;
}
.sitenotice--alert {
  background: var(--plaster-deep);
  background: color-mix(in srgb, var(--rust) 14%, var(--paper));
  border-bottom-color: color-mix(in srgb, var(--rust) 40%, var(--rule));
}
.sitenotice__text {
  width: min(100% - (var(--gutter) * 2), var(--wide));
  margin-inline: auto;
  font-size: var(--step--1);
  line-height: 1.55;
}
.sitenotice__text strong { color: var(--ink); }
.sitenotice a { color: var(--water-deep); }
.sitenotice--alert a { color: var(--rust); }

/* --- News and advice: the listing -------------------------------------------
   Rows with a rule between them rather than cards. A card grid implies a lot of
   posts and looks broken with two; a list reads correctly at any length, which
   matters on a section that starts with one. */
.postlist { list-style: none; margin: 0; padding: 0; }
.postlist li + li { border-top: 1px solid var(--rule); }

.post-link {
  display: block;
  padding: 1.75rem 0;
  text-decoration: none;
  color: inherit;
}
.post-link:hover .post-title { color: var(--water); }

.post-meta {
  display: block;
  font-family: var(--mono);
  font-size: var(--step--1);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
.post-title {
  display: block;
  margin-top: 0.5rem;
  font-size: var(--step-1);
  line-height: 1.25;
  color: var(--ink);
  transition: color 0.15s ease;
}
.post-excerpt {
  display: block;
  margin-top: 0.6rem;
  max-width: var(--measure);
  color: var(--ink-soft);
}

.postlist__note {
  margin-top: 2.5rem;
  max-width: var(--measure);
  font-size: var(--step--1);
  color: var(--ink-faint);
}

/* --- News and advice: one post -----------------------------------------------
   Hung on .post-body and never on .prose. A post is the only prose on this site
   that can contain lists, sub-subheadings and quotes, and none of these rules
   may be able to change a service or an area page. */
.post-hero { margin: 0 0 2.5rem; }
.post-hero img { display: block; width: 100%; height: auto; }

.post-body > * + h2 { margin-top: 3rem; }
.post-body > * + h3 { margin-top: 2.25rem; }
.post-body h3 { font-size: var(--step-1); margin-bottom: 1rem; }
.post-body ul,
.post-body ol { margin: 1.25rem 0; padding-left: 1.4rem; max-width: var(--measure); }
.post-body li { color: var(--ink-soft); }
.post-body li + li { margin-top: 0.5rem; }
.post-body blockquote {
  margin: 2rem 0;
  padding-left: 1.5rem;
  border-left: 2px solid var(--water);
  max-width: var(--measure);
  color: var(--ink-soft);
}
.post-body a { color: var(--water); }

/* --- Cookie consent ---------------------------------------------------------
   Rendered ONLY when cfg('analytics_cookie') is switched on, which it is not by
   default — see the note above render_cookie_banner() in includes/analytics.php.
   The site counts visitors without cookies precisely so that this bar does not
   have to exist.

   It sits at the bottom rather than the top: on a phone the top of the page is
   the heading somebody arrived to read, and a bar over it is the reason consent
   banners get dismissed without being read. Above the WhatsApp float's
   z-index so the two cannot overlap.

   No JavaScript — the two choices are ordinary links, so the bar is dismissible
   for everybody rather than only for visitors running script. */
.consent {
  position: fixed;
  inset: auto 0 0 0;
  z-index: 90;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: clamp(0.75rem, 3vw, 2rem);
  flex-wrap: wrap;
  padding: 1rem clamp(1rem, 5vw, 2.5rem);
  background: var(--slate-raised);
  border-top: 1px solid var(--rule-dark);
  color: var(--on-dark-soft);
}
.consent__text {
  margin: 0;
  font-size: var(--step--1);
  line-height: 1.55;
  max-width: 62ch;
}
.consent__actions { display: flex; gap: 0.6rem; flex-wrap: wrap; }
.consent .btn { font-size: var(--step--1); padding: 0.5rem 1rem; }
.consent .btn--ghost { color: var(--on-dark); border-color: var(--on-dark-faint); }
.consent .btn--ghost:hover { border-color: var(--on-dark); }

@media (max-width: 560px) {
  /* Clear of the iOS bottom address bar, for the same reason the WhatsApp
     float sits at 4.5rem. */
  .consent { padding-bottom: 4.5rem; }
}
