/* ==========================================================================
   Stainthorpe Electrical — Copper & Slate
   Single token-driven stylesheet. Never hardcode a colour below :root.

   z-index scale:  backdrop -1 · header 100 · progress 150 · overlay 200 · modal 300
   ========================================================================== */

:root {
  /* --- Colour ------------------------------------------------------------
     Two accents, one per pillar, because "Electrical and Security" are
     co-equal and the old single-accent palette said otherwise. Copper is what
     wire is actually made of; signal blue is the colour every alarm keypad,
     PIR and camera LED on the market already uses. They sit near-opposite on
     the wheel (29° vs 199°), so a page reads as one pillar or the other at a
     glance. The light surfaces are tinted toward slate rather than toward
     warmth: a warm near-white is the saturated default for sites like this,
     and it reads as a template.

     ⚠ THE RAMP IS SPACED IN L*, NOT IN CONTRAST RATIO. The old ramp stepped
     3.0 and 3.4 ΔL* between its first three surfaces — at or below the
     just-noticeable difference for a large flat area, which is why four
     "different" backgrounds read as one flat field. WCAG ratio hides this:
     the same ramp spans only 1.39:1, because the +0.05 term in the formula
     swamps everything at the dark end. Ratio is a text-legibility metric and
     is the wrong tool for asking whether two surfaces look different.
     Measured L*: 4.5 · 12.0 · 19.5 · 27.0 — even 7.5 steps, 22.5 total. */
  --slate-900: #0D1012;    /* L* 4.5  — page floor */
  --slate-800: #1B2026;    /* L* 12.0 — raised */
  --slate-700: #293038;    /* L* 19.5 — cards */
  --slate-600: #37414B;    /* L* 27.0 — borders, hover plates */

  /* Measured, not guessed: #6e7b85 came out at 4.0:1 on paper, under the 4.5
     AA floor for body text. Darkened until it cleared. */
  --steel:      #5c6870;   /* light backgrounds — 5.3:1 on paper */
  --steel-soft: #8a959e;   /* dark backgrounds, secondary — 5.8:1 on slate-900 */
  --steel-lift: #9aa5ae;   /* dark backgrounds, body size — 7.1:1 on slate-900,
                              6.2:1 on slate-700. Clears AA on every surface
                              this site uses, which --steel-soft does not once
                              cards go to slate-700. */

  --mist:  #e6e9ec;
  --surface: #f4f5f7;
  --white: #ffffff;
  --ice:   #eef1f4;        /* body text on dark — 15.7:1 on slate-900 */

  /* Electrical. Measured against the substrate's worst-case ceiling, not
     against bare slate — see the canvas-backed note below. */
  --copper:      #b87333;  /* LARGE TEXT AND UI ONLY on any canvas-backed
                              surface: 3.33:1 on the raised ceiling. */
  --copper-deep: #8f5622;  /* solid fills, white text — 5.96:1. Light
                              backgrounds and buttons only; on dark it is
                              2.99:1 and fails even large text. */
  --copper-hover:#a0612a;
  --copper-lit:  #D49556;  /* body-size copper on canvas-backed surfaces —
                              4.95:1 base / 4.77:1 raised. Was #d08a45, which
                              measured 4.29:1 once the raised surface was
                              lifted to L* 12. Nudged +4.2% lightness along
                              its own hue until it cleared AA with margin. */
  --copper-glow: #e0a463;  /* live current, bloom, hover — 5.80:1 on ceiling */

  /* Security. Same three roles, same measurement discipline. */
  --signal:      #4FACD6;  /* body-size signal on canvas-backed surfaces —
                              4.93:1 base / 4.75:1 raised. Solved for, not
                              picked: #46a8d4 came out at 4.53 and had no
                              margin left for a palette change. */
  --signal-deep: #1d6f96;  /* solid fills, white text — 5.58:1. The blue
                              equivalent of --copper-deep, and it exists for
                              the same reason: white on --signal is 2.69:1. */
  --signal-hover:#2a86b0;
  --signal-glow: #7cc8ea;  /* live current, bloom, hover — 6.79:1 on ceiling */

  --alert: #c8452d;        /* emergency only, never decoration. 3.69:1 on
                              slate-900, so LARGE TEXT ONLY on dark. */

  /* --- Semantic: dark-dominant -------------------------------------------
     The page is a circuit, so it runs dark and the accent is the live part.
     Four tonal steps rather than dark-with-light-interruptions: one scheme
     to verify instead of two.

     ⚠ Accents are surface-dependent, and this is the trap in a dark-dominant
     design. Measured against the substrate ceiling: --copper is 4.01:1 on the
     base ceiling and 3.33:1 on the raised one, so body-size --copper is not
     available on a canvas-backed surface at all. Accent text at body size
     must use --copper-lit or --signal. */
  --bg:          var(--slate-900);
  --bg-raised:   var(--slate-800);
  --bg-card:     var(--slate-700);
  --ink:         var(--ice);
  --ink-muted:   var(--steel-lift);
  /* Channel triplets, same reason as --accent-rgb below: rgba(var(--mist), α)
     is not valid CSS. Every ad-hoc alpha rule/veil derives from one of these —
     never write literal channels below :root. */
  --mist-rgb:      230, 233, 236;   /* --mist        */
  --slate-850-rgb: 20, 24, 28;      /* #14181C — header glass and band scrim,
                                       between -900 and -800; matches the
                                       theme-color meta on every page */
  --rule:        rgba(var(--mist-rgb), 0.12);
  --rule-strong: rgba(var(--mist-rgb), 0.24);

  /* --- Pillar accent ------------------------------------------------------
     Every accent below routes through --accent*, and ONE rule swaps the
     pillar (see [data-pillar="security"]). Nothing else in the stylesheet may
     name --copper or --signal directly, or the two pillars drift apart the
     first time someone edits only half of them. */
  --accent:       var(--copper);
  --accent-lit:   var(--copper-lit);
  --accent-deep:  var(--copper-deep);
  --accent-hover: var(--copper-hover);
  --accent-glow:  var(--copper-glow);

  /* Channel triplets, because the glows and rails need the accent AT AN ALPHA
     and `rgba(var(--accent), .26)` is not valid — a hex custom property cannot
     be given an alpha. These were the one place the "never hardcode a colour
     below :root" rule had already been broken: the rail, its head, the junction
     pulse and the emergency band all carried literal rgba(184,115,51). That was
     invisible while copper was the only accent and became a copper rail on a
     blue page the moment it was not. */
  --accent-rgb:      184, 115, 51;    /* --copper      */
  --accent-glow-rgb: 224, 164, 99;    /* --copper-glow */

  /* --slate-800 expressed as a veil rather than a fill. Over --slate-900 this
     composites to (27,32,38) — --slate-800 to the byte — while passing 90% of
     the substrate behind it. The raised tonal step survives; the occlusion does
     not. The source colour looks arbitrary because it is derived, not picked:
     it is whatever lands the composite exactly on the token it replaces.
     Re-derive it if either slate changes: alpha has a hard floor of 0.084 here
     (below that a channel would have to exceed 255), so 0.1 is the working
     value with a little headroom. */
  --veil-raised: rgba(153, 176, 218, 0.1);

  /* --- Current ------------------------------------------------------------
     Scroll-driven. --flow is 0→1 down the page, written by JS each frame;
     --charge is a section-local 0→1 set when a junction ignites. Both have
     static fallbacks so the page is complete with no JS at all. */
  --flow: 0;
  --charge: 0;
  --rail-w: 1px;

  /* --- Type -------------------------------------------------------------- */
  --font-display: "Archivo", "Helvetica Neue", Arial, sans-serif;
  --font-body: "Inter Tight", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;

  --step--1: clamp(0.82rem, 0.79rem + 0.14vw, 0.9rem);
  --step-0:  clamp(1rem, 0.96rem + 0.2vw, 1.12rem);
  --step-1:  clamp(1.2rem, 1.12rem + 0.4vw, 1.45rem);
  --step-2:  clamp(1.5rem, 1.34rem + 0.78vw, 2rem);
  --step-3:  clamp(1.75rem, 1.5rem + 1.2vw, 2.5rem);
  --step-4:  clamp(2.15rem, 1.7rem + 2.1vw, 3.5rem);
  --step-5:  clamp(2.6rem, 2rem + 3vw, 4.5rem);

  /* --- Space ------------------------------------------------------------- */
  --sp-1: 0.25rem;  --sp-2: 0.5rem;   --sp-3: 0.75rem;
  --sp-4: 1rem;     --sp-5: 1.5rem;   --sp-6: 2rem;
  --sp-7: 3rem;     --sp-8: 4rem;     --sp-9: 6rem;    --sp-10: 8rem;

  --gutter: clamp(1.25rem, 4vw, 3rem);
  --measure: 62ch;
  --container: 1200px;

  /* Section rhythm. Tokenised because the junction-node marker has to sit at
     the exact top of the section's content box, so its `top` and the section's
     padding-block are the same number in two places. They had already drifted:
     the node used the base value while .section--tight used a smaller pad, so
     every tight+node section had its node 30px below the content it marks. */
  --section-pad: clamp(2.75rem, 4.6vw, 5rem);
  --section-pad-tight: clamp(1.75rem, 3.2vw, 3.25rem);

  --radius: 3px;      /* near-square: this is a trade, not a startup */
  --radius-lg: 5px;

  --rail-x: clamp(2rem, 5.5vw, 5.5rem);  /* x-position of the copper thread */

  /* ease-out-expo: decisive departure, long settle. No bounce, no elastic. */
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  --ease: cubic-bezier(0.22, 0.61, 0.36, 1);
  --dur: 260ms;

  --z-backdrop: -1;
  --z-header: 100;
  --z-progress: 150;
  --z-overlay: 200;
  --z-modal: 300;
}

/* ==========================================================================
   Pillar
   --------------------------------------------------------------------------
   The whole of the second accent is this one block. Put data-pillar="security"
   on <html> for a security page, or on any single section to switch just that
   block — the tokens are inherited, so a nested section re-tints everything
   inside it including the rail, the eyebrow, the markers and the buttons.

   The substrate reads the same attribute in main.js and re-uploads its two
   shader colours, so the circuit board behind a security page runs blue too.

   ⚠ Both hues were audited against the substrate ceiling independently. Do not
   add a third pillar colour without re-running that audit — the ceiling is a
   proof only for the hues that were measured against it.
   ========================================================================== */

[data-pillar="security"] {
  --accent:       var(--signal);
  --accent-lit:   var(--signal);        /* --signal already clears AA at body
                                           size on both ceilings (4.93 / 4.75),
                                           so it needs no lifted twin the way
                                           copper does. */
  --accent-deep:  var(--signal-deep);
  --accent-hover: var(--signal-hover);
  --accent-glow:  var(--signal-glow);
  --accent-rgb:       79, 172, 214;   /* --signal      */
  --accent-glow-rgb: 124, 200, 234;   /* --signal-glow */
}

/* ==========================================================================
   Base
   ========================================================================== */

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

/* The base fill lives on html, not body, and this is load-bearing. The backdrop
   canvas is a negative-z child of body, so it paints in step 2 of the root
   stacking context — above the root background but BELOW body's own background
   box, which paints in step 3. Leave a background on body as well and it
   silently covers the canvas. These two rules are one change, not two. */
html {
  background: var(--bg);
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
}

/* Sticky header is ~68px; keep anchor targets clear of it. */
:target { scroll-margin-top: 92px; }

body {
  margin: 0;
  /* No background — html carries it. See the note above. */
  color: var(--ink);
  font-family: var(--font-body);
  font-size: var(--step-0);
  line-height: 1.62;
  font-synthesis-weight: none;
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3, h4 {
  font-family: var(--font-display);
  font-weight: 700;
  line-height: 1.03;
  letter-spacing: -0.022em;
  margin: 0 0 var(--sp-4);
  font-stretch: 112%;              /* Archivo carries a width axis — use it */
  text-wrap: balance;
}

h1 { font-size: var(--step-4); }
h2 { font-size: var(--step-3); }
h3 { font-size: var(--step-1); letter-spacing: -0.014em; font-stretch: 105%; }

p { margin: 0 0 var(--sp-4); max-width: var(--measure); text-wrap: pretty; }

/* --copper-deep was the link colour when the page was light. On slate-900 it
   measures 2.99:1 and fails even the large-text floor.

   --copper was the replacement, at 4.70:1 — clearing AA with almost no headroom.
   The substrate spends that headroom: against the worst-case lit surface copper
   falls to 3.88:1, under the body-text floor. So the base link colour moves up
   again to --copper-lit (5.19:1 worst case), which is what links on raised
   surfaces already used. The two now match, which they arguably should have
   all along. Hover stays --copper-glow (8.21:1 on bare slate). */
a { color: var(--accent-lit); text-decoration-thickness: 1px; text-underline-offset: 3px; }
a:hover { color: var(--accent-glow); }

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

ul { margin: 0 0 var(--sp-4); padding-left: 1.1rem; }
li { margin-bottom: var(--sp-2); }

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

.skip-link {
  position: absolute; left: var(--sp-4); top: -100px;
  background: var(--accent-deep); color: var(--white);
  padding: var(--sp-3) var(--sp-5); z-index: var(--z-modal);
  font-weight: 600; border-radius: var(--radius);
  transition: top var(--dur) var(--ease);
}
.skip-link:focus { top: var(--sp-4); color: var(--white); }

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

/* ==========================================================================
   The substrate — one fixed canvas behind the whole document.

   The page is a circuit, so it sits on a board. A negative tier means every
   other element paints over it without needing a z-index of its own, and the
   canvas never enters the layout: it is injected by JS, so with no script
   there is simply nothing here and the page is the design it always was.
   ========================================================================== */
.backdrop {
  position: fixed;
  inset: 0;
  width: 100%; height: 100%;
  display: block;
  z-index: var(--z-backdrop);
  pointer-events: none;
  opacity: 0;
  transition: opacity 900ms var(--ease-out);
}
.backdrop.is-on { opacity: 1; }

/* ==========================================================================
   Layout primitives
   ========================================================================== */

.container {
  width: 100%;
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

/* Measured 2026-08-09 before changing anything: at 1427px the old
   clamp(3.25rem, 6vw, 6rem) resolved to 85.6px top AND bottom, so every
   boundary between two sections was 171px of empty space. On a 782px viewport
   that is 22% of a screen height, repeated eight times down the homepage. The
   ratio was worst on the short sections: "Quoted, carried out and certified"
   was 488px tall of which 171px (35%) was padding, and the closing quote
   section was 38% padding. It read as sparse rather than spacious, because the
   substrate runs at full strength through the gap so the space is not restful
   either. Roughly a quarter off both steps, keeping the ratio between base and
   tight wide enough that they still read as two distinct densities. */
.section { padding-block: var(--section-pad); position: relative; }
.section--tight { padding-block: var(--section-pad-tight); }

/* Every surface is dark now, so these are tonal steps rather than a light/dark
   switch. The old class names are kept because five pages of markup use them
   and a rename buys nothing.

   .section--dark was painting --bg onto a page that is already --bg: an opaque
   fill doing no visual work, which is exactly what hid the substrate. Dropping
   it changes nothing on screen and lets the board through. The raised step is
   real tonal information, so it survives as a veil rather than a fill. */
.section--dark  { background: transparent; }
.section--slate { background: var(--veil-raised); }
.section--alt   { background: var(--veil-raised); }

h1, h2, h3, h4 { color: var(--white); }

/* :not(.btn) matters — a bare `.section--dark a` is specificity 0,1,1 and beats
   `.btn--call` at 0,1,0, which painted copper text onto a copper button and made
   the primary call-to-action render blank on every dark section. Kept scoped
   for exactly that reason even though the base `a` rule now matches. */
.section--slate a:not(.btn), .section--alt a:not(.btn) { color: var(--accent-lit); }
.section--slate a:not(.btn):hover, .section--alt a:not(.btn):hover { color: var(--accent-glow); }

.section__head { margin-bottom: var(--sp-6); }
.section__head > :last-child { margin-bottom: 0; }

/* Eyebrow — only ever labels what the section *is*, never decoration. */
.eyebrow {
  font-family: var(--font-body);
  font-size: var(--step--1);
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  /* The eyebrow runs at --step--1 / weight 600, which is body size, not large
     text — so it needs the full 4.5:1. --copper cleared that on bare slate-900
     (4.70:1) and nowhere else; with the substrate behind it, it clears nowhere
     at all (3.88:1 worst case). --copper-lit holds on every surface the site
     has, so the raised-surface override below is now redundant — kept only
     because deleting it would make the cascade read as an accident. */
  color: var(--accent-lit);
  margin: 0 0 var(--sp-3);
  display: flex; align-items: center; gap: var(--sp-3);
}
.eyebrow::after {
  content: ""; flex: 1; height: 1px;
  background: rgba(var(--mist-rgb), 0.22); max-width: 5rem;
}
.section--slate .eyebrow, .section--alt .eyebrow { color: var(--accent-lit); }

/* Light type on dark reads lighter than it measures, so the lede gains a little
   line-height over the light-background original. */
.lede { font-size: var(--step-1); color: var(--ink-muted); line-height: 1.58; }

/* ==========================================================================
   The signature: one unbroken copper thread down the page.
   A circuit run — continuous, with a junction at every section.
   ========================================================================== */

/* The conductor in its resting state: cold copper, unlit. This is exactly what
   the page shows with JS disabled, so the no-JS render is the original design
   rather than a broken one. */
.rail {
  position: absolute;
  left: var(--rail-x);
  top: 0; bottom: 0;
  width: var(--rail-w);
  background: linear-gradient(
    to bottom,
    transparent 0,
    var(--accent) 60px,
    var(--accent) calc(100% - 120px),
    transparent 100%
  );
  opacity: 0.5;
  pointer-events: none;
  z-index: 0;
}

/* --- Energised ----------------------------------------------------------
   .js only. --lit is written per-rail each frame by main.js: the fraction of
   this segment the current has reached, 0→1. Scroll drives current down the
   page, so the circuit energises as you read it.

   scaleY rather than height: height is a layout property and this runs every
   frame. transform stays on the compositor. */
.js .rail { opacity: 1; background: none; }

.js .rail::before {
  content: "";
  position: absolute; inset: 0;
  background: linear-gradient(
    to bottom,
    transparent 0,
    rgba(var(--accent-rgb), 0.26) 60px,
    rgba(var(--accent-rgb), 0.26) calc(100% - 120px),
    transparent 100%
  );
}

.js .rail::after {
  content: "";
  position: absolute; left: 0; top: 0;
  width: 100%; height: 100%;
  transform: scaleY(var(--lit, 0));
  transform-origin: top;
  background: linear-gradient(
    to bottom,
    var(--accent) 0%,
    var(--accent-glow) 100%
  );
  box-shadow: 0 0 9px rgba(var(--accent-glow-rgb), 0.5);
}

/* The travelling head. Sits in a full-height sleeve so translateY can be a
   percentage of the rail without JS ever touching a layout property. */
.rail__head {
  position: absolute; left: 50%; top: 0;
  width: 1px; height: 100%;
  transform: translateY(calc(var(--lit, 0) * 100%));
  pointer-events: none;
}
.rail__head::before {
  content: "";
  position: absolute; left: 50%; top: 0;
  width: 5px; height: 5px;
  margin: -2.5px 0 0 -2.5px;
  border-radius: 50%;
  background: var(--accent-glow);
  box-shadow:
    0 0 8px 2px rgba(var(--accent-glow-rgb), 0.75),
    0 0 22px 6px rgba(var(--accent-rgb), 0.35);
  opacity: var(--head-on, 0);
  transition: opacity 400ms var(--ease);
}

.section > .container { position: relative; z-index: 1; }

/* Junction node where each section meets the thread. Cold until the current
   front reaches it, then it ignites once and stays lit — a circuit that has
   been energised, not a decoration that pulses forever. */
.section--node::before {
  content: "";
  position: absolute;
  left: var(--rail-x);
  top: var(--section-pad);
  width: 7px; height: 7px;
  margin-left: -3px;
  background: var(--accent);
  transform: rotate(45deg);
  z-index: 1;
}
/* A tight section pays less padding, so its node has to follow or it floats
   below the heading it is marking. This is the drift the tokens above exist
   to stop. */
.section--tight.section--node::before {
  top: var(--section-pad-tight);
}

.js .section--node::before {
  background: var(--slate-600);
  box-shadow: 0 0 0 0 rgba(var(--accent-glow-rgb), 0);
  transition:
    background 260ms var(--ease),
    box-shadow 900ms var(--ease-out),
    transform 620ms var(--ease-out);
}
.js .section--node.is-live::before {
  background: var(--accent-glow);
  box-shadow: 0 0 14px 3px rgba(var(--accent-glow-rgb), 0.45);
  transform: rotate(45deg) scale(1.28);
}

/* Below 1024px the thread used to be hidden — there was no margin to spare.
   Full effects on mobile was an explicit call, so instead of hiding it the
   gutter widens and the rail tucks into the new margin. */
@media (max-width: 1023px) {
  :root { --rail-x: 11px; --gutter: clamp(1.5rem, 4vw, 3rem); }
  .section--node::before { width: 6px; height: 6px; margin-left: -2.5px; }
}

/* ==========================================================================
   Buttons
   ========================================================================== */

.btn {
  display: inline-flex; align-items: center; gap: var(--sp-3);
  font-family: var(--font-body);
  font-size: var(--step-0);
  font-weight: 600;
  line-height: 1;
  padding: 0.95em 1.5em;
  border: 1px solid transparent;
  border-radius: var(--radius);
  text-decoration: none;
  cursor: pointer;
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease),
              border-color var(--dur) var(--ease), transform var(--dur) var(--ease);
}
.btn:active { transform: translateY(1px); }

/* On a dark page the old --copper-deep fill reads as mud. --copper-lit with
   slate-900 text measures 6.29:1 — better than the white-on-copper-deep it
   replaces (5.96:1) — and it looks like something with power behind it, which
   is the whole point of the build. */
.btn--call {
  background: var(--accent-lit); color: var(--slate-900);
  position: relative;
  box-shadow: 0 0 0 0 rgba(var(--accent-glow-rgb), 0.5);
}
.btn--call:hover {
  background: var(--accent-glow); color: var(--slate-900);
  box-shadow: 0 0 26px -4px rgba(var(--accent-glow-rgb), 0.55);
}

.btn--ghost {
  background: transparent; color: var(--ink);
  border-color: var(--rule-strong);
}
.btn--ghost:hover {
  border-color: var(--accent); color: var(--accent-glow);
  background: rgba(var(--accent-rgb), 0.08);
}

/* nowrap: a phone number broken across two lines is unreadable and unusable. */
.btn__num { font-variant-numeric: tabular-nums; letter-spacing: 0.01em; white-space: nowrap; }

.btn-row { display: flex; flex-wrap: wrap; gap: var(--sp-4); align-items: center; }

/* ==========================================================================
   Header
   ========================================================================== */

/* 0.96 rather than 0.92: the header now blurs a live layer instead of a static
   one, and .brand__sub is --copper at 0.62rem with no contrast headroom at all.
   Halving the substrate bleed under it costs nothing visually. */
.header {
  position: sticky; top: 0; z-index: var(--z-header);
  background: rgba(var(--slate-850-rgb), 0.96);
  backdrop-filter: saturate(140%) blur(10px);
  border-bottom: 1px solid rgba(var(--mist-rgb), 0.1);
}

.header__inner {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--sp-5);
  min-height: 68px;
}

/* His own mark, white monochrome version — reads on the slate header without
   fighting the copper. alt="" is deliberate: the wordmark beside it already
   carries the name, so announcing it twice is noise for a screen reader. */
.brand {
  display: inline-flex; align-items: center; gap: var(--sp-3);
  text-decoration: none; color: var(--white);
}
.brand:hover { color: var(--white); }

.brand__mark { width: 34px; height: auto; flex: none; }

.brand__text {
  display: inline-flex; flex-direction: column; gap: 2px;
  font-family: var(--font-display);
  font-weight: 800;
  font-stretch: 118%;
  font-size: 1.06rem;
  letter-spacing: 0.03em;
  line-height: 1;
}
.brand__sub {
  font-family: var(--font-body);
  font-size: 0.62rem; font-weight: 500;
  letter-spacing: 0.24em; text-transform: uppercase;
  color: var(--accent);
}

.nav { display: flex; align-items: center; gap: clamp(var(--sp-4), 2vw, var(--sp-6)); }
.nav__link {
  color: var(--mist); text-decoration: none;
  font-size: 0.95rem; font-weight: 500;
  padding: var(--sp-2) 0;
  border-bottom: 1px solid transparent;
  transition: color var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
.nav__link:hover { color: var(--white); border-bottom-color: var(--accent); }
.nav__link[aria-current="page"] { color: var(--white); border-bottom-color: var(--accent); }

.header .btn--call { padding: 0.7em 1.15em; font-size: 0.95rem; }

.nav-toggle {
  display: none;
  background: none; border: 1px solid rgba(var(--mist-rgb), 0.28);
  border-radius: var(--radius);
  color: var(--mist); padding: var(--sp-2) var(--sp-3);
  font: inherit; font-size: 0.9rem; cursor: pointer;
}

@media (max-width: 860px) {
  .nav-toggle { display: inline-flex; align-items: center; gap: var(--sp-2); }
  .nav {
    position: absolute; left: 0; right: 0; top: 100%;
    flex-direction: column; align-items: stretch; gap: 0;
    background: var(--slate-900);
    border-bottom: 1px solid var(--rule);
    padding: var(--sp-3) var(--gutter) var(--sp-5);
    display: none;
  }
  .nav[data-open="true"] { display: flex; }
  .nav__link { padding: var(--sp-4) 0; border-bottom: 1px solid rgba(var(--mist-rgb), 0.09); }
  .nav__link:hover { border-bottom-color: rgba(var(--mist-rgb), 0.09); }
  .header__cta { display: none; }
}

/* Scroll progress */
.progress {
  position: fixed; top: 0; left: 0; height: 2px;
  background: var(--accent);
  width: 0; z-index: var(--z-progress);
  transition: width 90ms linear;
}

/* ==========================================================================
   Hero
   ========================================================================== */

.hero {
  position: relative;
  /* No background: --slate-900 on a --slate-900 page was an opaque fill doing
     no visual work, and it hid the substrate. overflow:hidden is unaffected —
     .backdrop is fixed and not a descendant, so it is never clipped here. */
  color: var(--ink);
  padding-block: clamp(2.75rem, 5vw, 4.5rem);
  overflow: hidden;
}

/* --- Hero supply -------------------------------------------------------
   One layer behind the content, decorative and optional: PCB-style routing that
   runs out from behind the van and joins the thread. The van is the supply; the
   page is what it feeds.

   There used to be a second layer here, a WebGL contour field scoped to the
   hero. It became the site-wide .backdrop — see the substrate block near the
   top of this file — so the hero no longer owns a canvas of its own. */
.hero__traces {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  pointer-events: none;
  z-index: 0;
}

.hero__traces path {
  fill: none;
  stroke: var(--accent);
  stroke-width: 1;
  vector-effect: non-scaling-stroke;
  opacity: 0.42;
}
.js .hero__traces path {
  stroke-dasharray: var(--len) var(--len);
  stroke-dashoffset: var(--len);
}
.js .hero.is-live .hero__traces path {
  stroke-dashoffset: 0;
  transition: stroke-dashoffset 1500ms var(--ease-out);
  transition-delay: calc(var(--i, 0) * 130ms);
}
/* The junction pads the traces terminate on. */
.hero__traces circle {
  fill: var(--accent-glow);
  opacity: 0;
}
.js .hero.is-live .hero__traces circle {
  opacity: 0.9;
  transition: opacity 500ms var(--ease);
  transition-delay: calc(900ms + var(--i, 0) * 130ms);
}

.hero .container { position: relative; z-index: 1; }

/* Deliberate air under the content on the layouts that draw the supply routing.
   The traces live in this margin; without it they would have to cross the trust
   row, and copper through type is not a trade-off worth making. */
@media (min-width: 901px) {
  .hero { padding-bottom: clamp(4.75rem, 7vw, 7.5rem); }
}

/* --- The switchboard ----------------------------------------------------
   Ways on a distribution board. One is live at a time; the supply trace routes
   from the van down to whichever it is. Real links underneath, so this degrades
   to a plain set of service shortcuts with no script. */
.switchboard {
  position: relative;
  z-index: 1;
  margin-top: clamp(var(--sp-6), 4vw, var(--sp-8));
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: clamp(var(--sp-3), 1.6vw, var(--sp-5));
}

/* position: relative is load-bearing. Without it the absolute ::before below
   resolves against .switchboard and the breaker bar spans all four ways. */
.switchboard__way {
  position: relative;
  display: block;
  padding: var(--sp-4) var(--sp-4) var(--sp-3) 0;
  border-top: 1px solid var(--rule-strong);
  text-decoration: none;
  color: var(--ink-muted);
  transition: color var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
/* The breaker: a copper bar that grows along this way's top edge as it goes
   live. Same device as .pillar::after, so the language stays consistent. */
.switchboard__way::before {
  content: "";
  position: absolute;
  top: -1px; left: 0;
  width: 0;
  height: 2px;
  background: var(--accent-glow);
  box-shadow: 0 0 10px rgba(var(--accent-glow-rgb), 0.5);
  transition: width 520ms var(--ease-out);
}

.switchboard__name {
  display: block;
  font-family: var(--font-display);
  font-weight: 700;
  font-stretch: 108%;
  font-size: var(--step-0);
  letter-spacing: -0.014em;
  color: var(--ink-muted);
  transition: color var(--dur) var(--ease);
}
.switchboard__note {
  display: block;
  font-size: var(--step--1);
  color: var(--ink-muted);
  margin-top: 2px;
}

/* Live, or being pointed at. --ink is 15.7:1 on slate-900; the muted step is
   7.1:1, so both states clear AA on their own. */
.switchboard__way.is-live .switchboard__name,
.switchboard__way:hover .switchboard__name,
.switchboard__way:focus-visible .switchboard__name { color: var(--ink); }

.switchboard__way.is-live { border-top-color: transparent; }
.switchboard__way.is-live::before { width: 100%; }
.switchboard__way:hover { color: var(--ink); border-top-color: var(--accent); }

@media (max-width: 860px) {
  .switchboard { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 420px) {
  .switchboard__note { display: none; }   /* the name carries it at this size */
}

/* --- Hero shot stack ----------------------------------------------------
   The markup ships one image, the van, so a no-script hero is the original
   hero. main.js stacks the rest over it and crossfades. */
.hero__shot {
  position: absolute;
  inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  border-radius: var(--radius-lg);
  border: 1px solid rgba(var(--mist-rgb), 0.14);
  opacity: 0;
  transition: opacity 620ms var(--ease-out);
}
.hero__shot.is-live { opacity: 1; }

/* Product shots rather than photographs of his work: contained on a plate so
   they read as deliberate imagery instead of a badly cropped photo. */
.hero__shot--contain {
  object-fit: contain;
  background: var(--bg-raised);
  padding: clamp(var(--sp-5), 4vw, var(--sp-7));
}

/* The live supply trace: brighter than the three mains runs, and it redraws
   whenever the selected way changes. */
.hero__traces path.is-supply {
  stroke: var(--accent-glow);
  opacity: 0.85;
  filter: drop-shadow(0 0 4px rgba(var(--accent-glow-rgb), 0.45));
}

/* The van photo is the only genuinely authentic image on the site, so it gets
   the room. Its source is 739px wide — that's the original Wix upload, not a
   crop — so the column is capped near that to avoid upscaling into softness. */
.hero .container { max-width: 1320px; }

.hero__grid {
  display: grid;
  grid-template-columns: 1fr 1.06fr;
  gap: clamp(var(--sp-5), 3.5vw, var(--sp-7));
  align-items: center;
}

/* Slightly tighter than the global h1 so "One number to call." holds one line
   in the narrower hero column — a break before "call." reads badly. */
.hero h1 { font-size: clamp(2.1rem, 1.6rem + 2vw, 3.3rem); }

.hero h1 { color: var(--white); margin-bottom: var(--sp-5); }
.hero h1 em {
  font-style: normal;
  color: var(--accent);
}

.hero__lede {
  font-size: var(--step-1);
  color: var(--steel-soft);
  max-width: 46ch;
  margin-bottom: var(--sp-6);
}

.hero__cta { margin-bottom: var(--sp-6); }

.hero__figure { position: relative; margin: 0; }
.hero__figure::before {
  content: "";
  position: absolute;
  inset: auto -14px -14px auto;
  width: 62%; height: 62%;
  border-right: 1px solid var(--accent);
  border-bottom: 1px solid var(--accent);
  border-radius: 0 0 var(--radius-lg) 0;
  opacity: 0.55;
  pointer-events: none;
}
.hero__figure img {
  width: 100%;
  border-radius: var(--radius-lg);
  border: 1px solid rgba(var(--mist-rgb), 0.14);
}

/* The one place a caption earns its keep: it says the photo is really him. */
.hero__caption {
  margin-top: var(--sp-3);
  font-size: var(--step--1);
  color: var(--steel-soft);
  display: flex; align-items: center; gap: var(--sp-3);
}
.hero__caption::before {
  content: ""; width: 22px; height: 1px; background: var(--accent); flex: none;
}

.hero__trust {
  display: flex; align-items: center; gap: var(--sp-5);
  flex-wrap: wrap;
  padding-top: var(--sp-5);
  border-top: 1px solid rgba(var(--mist-rgb), 0.14);
}
.hero__badge { height: 58px; width: auto; background: var(--white); padding: 5px 7px; border-radius: var(--radius); }

/* The PSA mark is a wide horizontal lockup (263x54), not a portrait crest like
   the RECI badge, so the shared 58px height would run it out to ~283px and let
   it dominate the security hero. Sized by its own class rather than by position,
   which is how .credentials below does it — and that rule only works because the
   about page happens to carry exactly two badges in exactly one order. */
.hero__badge--wide { height: 52px; }

/* The trust block in a full-width hero, as opposed to the homepage's two-column
   one. --band exists because .hero__trust-text is capped at 30ch, which is right
   beside a hero figure and wrong with a whole container to play with: measured at
   250px over five lines on security.html before this. */
.hero__trust--band { gap: var(--sp-5) var(--sp-6); }
.hero__trust--band .hero__trust-text { max-width: 58ch; }

/* Accreditation marks. White plates because both badges are supplied on white
   and would otherwise sit on a dark section with a hard rectangular edge. */
.credentials { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-4); }
.credentials img {
  background: var(--white);
  padding: 8px 10px;
  border-radius: var(--radius);
  width: auto;
}
.credentials img:first-child { height: 110px; }
.credentials img:last-child { height: 46px; }
.hero__trust-text { font-size: var(--step--1); color: var(--steel-soft); max-width: 30ch; margin: 0; }

@media (max-width: 900px) {
  .hero__grid { grid-template-columns: 1fr; gap: var(--sp-7); }
}

/* ==========================================================================
   Stat strip — tabular figures, copper
   ========================================================================== */

.stats {
  display: grid;
  /* auto-fit, not repeat(4): index dropped to three stats on 2026-08-18 when the
     24/7 one went, while about.html still carries four. A fixed count left a
     quarter-width hole on the homepage. This spreads whatever is there. */
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: var(--sp-5);
  border-top: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
  padding-block: var(--sp-6);
}
.section--dark .stats, .section--slate .stats {
  border-color: rgba(var(--mist-rgb), 0.16);
}

.stat__num {
  font-family: var(--font-display);
  font-size: var(--step-3);
  font-weight: 700;
  font-stretch: 118%;
  font-variant-numeric: tabular-nums;
  color: var(--accent);
  line-height: 1;
  display: block;
  margin-bottom: var(--sp-2);
}
.stat__label {
  font-size: var(--step--1);
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--ink-muted);
  margin: 0;
}
/* .section--slate dropped from this selector deliberately. --steel-soft measures
   4.82:1 against the worst-case lit base but only 4.51:1 against the lit raised
   surface — clearing AA by 0.01. No page pairs them today (the stats live in a
   bare .section--tight), so this was a trap waiting for someone to add a stat
   strip to a raised section and watch it fail silently. Raised falls through to
   --ink-muted at 5.49:1. */
.section--dark .stat__label { color: var(--steel-soft); }

@media (max-width: 720px) {
  .stats { grid-template-columns: repeat(2, 1fr); gap: var(--sp-6) var(--sp-4); }
}

/* ==========================================================================
   Pillars — electrical and security, deliberately equal weight
   ========================================================================== */

.pillars { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-5); }

.pillar {
  position: relative;
  display: flex; flex-direction: column;
  min-height: 340px;
  padding: var(--sp-6);
  border: 1px solid var(--rule);
  border-radius: var(--radius-lg);
  background: var(--bg-raised);
  overflow: hidden;
  text-decoration: none;
  color: inherit;
  transition: border-color 300ms var(--ease-out), transform 300ms var(--ease-out);
}
.pillar:hover { border-color: var(--accent); transform: translateY(-3px); color: inherit; }
/* --copper on slate-800 is 4.43:1, under the body floor. The hover state has to
   go brighter, not just warmer. */
.pillar:hover .pillar__more { color: var(--accent-glow); }

/* A photo washed to 16% behind body copy was invisible at every size — noise in
   the markup for no read. The cards carry themselves on type and the copper
   dashes instead; the rule below is the only ornament they get. */
.pillar::after {
  content: "";
  position: absolute; left: 0; right: 0; top: 0; height: 2px;
  background: var(--accent);
  transform: scaleX(0.14);
  transform-origin: left;
  transition: transform 520ms var(--ease-out);
}
.pillar:hover::after, .pillar:focus-visible::after { transform: scaleX(1); }
.pillar > * { position: relative; }

.pillar h3 { margin-bottom: var(--sp-3); }
.pillar p { color: var(--ink-muted); font-size: 0.97rem; }

.pillar__list {
  list-style: none; padding: 0; margin: 0 0 var(--sp-5);
  font-size: 0.95rem;
}
.pillar__list li {
  padding-left: var(--sp-5); position: relative; margin-bottom: var(--sp-2);
}
.pillar__list li::before {
  content: ""; position: absolute; left: 0; top: 0.62em;
  width: 10px; height: 1px; background: var(--accent);
}

.pillar__more {
  margin-top: auto;
  font-weight: 600; font-size: 0.95rem;
  color: var(--accent-lit);   /* 5.92:1 on the slate-800 card */
  display: inline-flex; align-items: center; gap: var(--sp-2);
  transition: gap var(--dur) var(--ease);
}
.pillar:hover .pillar__more { gap: var(--sp-3); }

@media (max-width: 860px) { .pillars { grid-template-columns: 1fr; } }

/* ==========================================================================
   Service cards
   ========================================================================== */

.grid { display: grid; gap: var(--sp-5); }
.grid--3 { grid-template-columns: repeat(3, 1fr); }
.grid--2 { grid-template-columns: repeat(2, 1fr); }

@media (max-width: 900px) { .grid--3 { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 620px) {
  .grid--3, .grid--2 { grid-template-columns: 1fr; }
}

.card {
  padding: var(--sp-6);
  background: var(--bg-raised);
  border: 1px solid var(--rule);
  border-radius: var(--radius-lg);
  border-top: 2px solid var(--accent);
}
.card h3 { margin-bottom: var(--sp-3); }
.card p { color: var(--ink-muted); font-size: 0.97rem; margin-bottom: 0; }

/* A card sitting on a raised section needs the next tonal step down, or the two
   surfaces read as one flat plane. */
.section--slate .card, .section--alt .card {
  background: var(--bg-card);
  border-color: var(--slate-600);
  border-top-color: var(--accent);
}

/* ==========================================================================
   Reviews — real Google reviews, verbatim. Never edit the text.
   ========================================================================== */

.reviews__rating {
  display: flex; align-items: baseline; gap: var(--sp-3);
  margin-bottom: var(--sp-6);
}
.reviews__score {
  font-family: var(--font-display);
  font-size: var(--step-3); font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--accent); line-height: 1;
}
.reviews__meta { font-size: var(--step--1); color: var(--ink-muted); margin: 0; }

.review {
  padding: var(--sp-6);
  background: var(--bg-raised);
  border: 1px solid var(--rule);
  border-radius: var(--radius-lg);
  display: flex; flex-direction: column;
}
/* 0.9rem is body size, so this needs the full 4.5:1. --copper-deep measures
   2.81:1 on the slate-800 review card — it was legible on the old paper
   background and is not legible here. */
.review__stars { color: var(--accent-lit); letter-spacing: 0.14em; font-size: 0.9rem; margin-bottom: var(--sp-3); }
.review blockquote { margin: 0 0 var(--sp-5); font-size: 0.99rem; }
.review blockquote p { margin: 0; }
.review__who {
  margin-top: auto; font-weight: 600; font-size: 0.93rem;
  padding-top: var(--sp-4); border-top: 1px solid var(--rule);
}
.review__when { font-weight: 400; color: var(--ink-muted); }

/* ==========================================================================
   Process — how a job runs
   Numbered because this genuinely is a sequence. Numbered markers are not used
   as section scaffolding anywhere else on the site; an actual ordered process is
   the exception, and the order carries information the reader needs.
   ========================================================================== */

.process {
  list-style: none;
  padding: 0;
  margin: 0;
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: clamp(var(--sp-5), 2.5vw, var(--sp-6));
  counter-reset: none;
}

.process__step {
  position: relative;
  margin: 0;
  padding-top: var(--sp-5);
  border-top: 1px solid var(--rule);
}
/* The step ticks over to copper as it reveals, so the sequence reads as
   something that runs rather than four boxes. */
.process__step::before {
  content: "";
  position: absolute;
  top: -1px; left: 0;
  width: 0; height: 1px;
  background: var(--accent);
  transition: width 620ms var(--ease-out);
}
.process__step.is-visible::before { width: 100%; }
/* The reveal script sets transitionDelay on the element itself, which does not
   reach a pseudo-element, so the stagger is declared here. */
.process__step:nth-child(2)::before { transition-delay: 90ms; }
.process__step:nth-child(3)::before { transition-delay: 180ms; }
.process__step:nth-child(4)::before { transition-delay: 270ms; }

.process__num {
  display: block;
  font-family: var(--font-display);
  font-size: var(--step-1);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  font-stretch: 112%;
  /* --copper-lit, not --copper: this sits on slate-800 where --copper measures
     4.43:1, under the body floor. */
  color: var(--accent-lit);
  line-height: 1;
  margin-bottom: var(--sp-3);
}
.process__step h3 { margin-bottom: var(--sp-2); }
.process__step p { color: var(--ink-muted); font-size: 0.95rem; margin-bottom: 0; }

@media (max-width: 900px) { .process { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 520px) { .process { grid-template-columns: 1fr; } }

/* ==========================================================================
   Commercial clients — real names he already publishes, set as type rather
   than reproduced trademarks. Confident, and no permission to chase.
   ========================================================================== */

.clients {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-4) 0;
}
.clients__name {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 700;
  font-stretch: 105%;
  font-size: clamp(1rem, 0.9rem + 0.5vw, 1.3rem);
  letter-spacing: -0.012em;
  color: var(--ink);
  padding-inline: clamp(var(--sp-4), 1.8vw, var(--sp-6));
  display: flex;
  align-items: center;
}
/* Copper junction between names — the same square the thread uses, so the
   client list reads as part of the circuit rather than a separate device. */
.clients__name + .clients__name::before {
  content: "";
  width: 5px; height: 5px;
  background: var(--accent);
  transform: rotate(45deg);
  margin-right: clamp(var(--sp-4), 1.8vw, var(--sp-6));
  margin-left: calc(-1 * clamp(var(--sp-4), 1.8vw, var(--sp-6)));
  flex: none;
}
.clients__name:first-child { padding-left: 0; }

/* ==========================================================================
   Emergency band
   ========================================================================== */

.emergency {
  /* No background, for the same reason as .hero. */
  color: var(--mist);
  position: relative;
  overflow: hidden;
}
.emergency__inner {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--sp-6); flex-wrap: wrap;
}
.emergency h2 { margin-bottom: var(--sp-2); }
.emergency p { color: var(--steel-soft); margin: 0; }

.dot {
  display: inline-block; width: 8px; height: 8px; border-radius: 50%;
  background: var(--accent); margin-right: var(--sp-3);
  box-shadow: 0 0 0 0 rgba(var(--accent-rgb), 0.65);
  animation: pulse 2.6s var(--ease) infinite;
}
@keyframes pulse {
  70%  { box-shadow: 0 0 0 11px rgba(var(--accent-rgb), 0); }
  100% { box-shadow: 0 0 0 0 rgba(var(--accent-rgb), 0); }
}

/* ==========================================================================
   Media band (full-bleed photo strip)
   ========================================================================== */

.band {
  min-height: clamp(220px, 30vw, 380px);
  /* The colour is a fallback, not decoration: the text here is white, so if the
     background image fails to load the band would otherwise inherit the paper
     background and the copy would vanish. */
  background-color: var(--slate-900);
  background-size: cover; background-position: center;
  position: relative;
}
.band::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(90deg, rgba(var(--slate-850-rgb),0.86) 0%, rgba(var(--slate-850-rgb),0.45) 55%, rgba(var(--slate-850-rgb),0.2) 100%);
}
.band__inner { position: relative; z-index: 1; padding-block: var(--sp-8); color: var(--mist); }
.band__inner h2 { color: var(--white); }
.band__inner p { color: var(--steel-soft); }

/* ==========================================================================
   Contact
   ========================================================================== */

/* Product shots come from the manufacturer on white, so they need a frame or
   they read as floating cut-outs rather than deliberate imagery. */
.figure--product {
  margin: 0;
  padding: var(--sp-6);
  background: var(--mist);
  border: 1px solid var(--rule);
  border-radius: var(--radius-lg);
  display: grid; place-items: center;
}
.section--alt .figure--product { background: var(--white); }
.figure--product img { max-height: 300px; width: auto; }

/* Photographs of David's own work, added 2026-08-01 from his Instagram. These
   are the opposite case to .figure--product: full-frame photographs rather than
   manufacturer cut-outs on white, so they get no plate and no padding — the
   photograph is the object, and a light plate around it would read as a slide. */
.figure--photo {
  margin: 0;
  border-radius: var(--radius-lg);
  overflow: hidden;
  border: 1px solid var(--rule);
  background: var(--slate-900);
}
.figure--photo img { display: block; width: 100%; height: auto; }

/* Instagram pads a landscape photograph into a square with black bars rather
   than cropping it. solar-own-roof.webp was 1080x810 of picture inside a 1080
   square — 139px of bar at the top, 131 at the bottom. Cropping at 27/20 with
   cover clears both with a pixel to spare, which is why the file shipped as
   downloaded instead of re-encoded.

   ⚠ Currently applied to nothing: the solar section this was written for came
   out on 2026-08-16. Kept deliberately, because every image David sources from
   his own Instagram arrives padded the same way and the next one is already
   requested. The 27/20 is measured against that one file, so RE-MEASURE the
   bars on any new image rather than assuming the ratio transfers. */
.figure--photo--letterboxed img { aspect-ratio: 27 / 20; object-fit: cover; }

.contact-grid { display: grid; grid-template-columns: 1.1fr 0.9fr; gap: clamp(var(--sp-6), 5vw, var(--sp-8)); align-items: center; }
@media (max-width: 860px) { .contact-grid { grid-template-columns: 1fr; } }

.contact-list { list-style: none; padding: 0; margin: 0; }
.contact-list li {
  padding-block: var(--sp-4);
  border-bottom: 1px solid var(--rule);
  display: grid; grid-template-columns: 8rem 1fr; gap: var(--sp-4);
  align-items: baseline;
}
.section--dark .contact-list li { border-color: rgba(var(--mist-rgb), 0.16); }
.contact-list dt, .contact-list__label {
  font-size: var(--step--1); letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--ink-muted);
}
.section--dark .contact-list__label { color: var(--steel-soft); }
/* overflow-wrap: anywhere, not break-word. An email address is one unbreakable
   word to the line breaker, and a 1fr grid track sizes to min-content, so
   break-word would let it break but still force the track wide enough to
   overflow. "anywhere" shrinks the min-content contribution too. Needed from
   2026-08-26, when the published contact address went from 24 characters to 29
   and no longer fitted the narrow footer column. */
.contact-list__value { font-size: var(--step-1); font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }

@media (max-width: 520px) {
  .contact-list li { grid-template-columns: 1fr; gap: var(--sp-1); }
}

.coverage {
  display: flex; flex-wrap: wrap; gap: var(--sp-2);
  list-style: none; padding: 0; margin: var(--sp-4) 0 0;
}
.coverage li {
  font-size: 0.9rem; margin: 0;
  padding: var(--sp-2) var(--sp-4);
  border: 1px solid var(--rule-strong);
  border-radius: 100px;
}
.section--dark .coverage li { border-color: var(--rule-strong); }

/* ==========================================================================
   Footer
   ========================================================================== */

.footer {
  background: var(--slate-900);
  color: var(--steel-soft);
  padding-block: var(--sp-8) var(--sp-6);
  font-size: 0.94rem;
}
.footer a { color: var(--mist); text-decoration: none; }
.footer a:hover { color: var(--accent-lit); text-decoration: underline; }

.footer__grid {
  display: grid; grid-template-columns: 1.4fr 1fr 1fr;
  gap: var(--sp-7); margin-bottom: var(--sp-7);
}
@media (max-width: 760px) { .footer__grid { grid-template-columns: 1fr; gap: var(--sp-6); } }

.footer h4 {
  font-size: var(--step--1); letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--white); margin-bottom: var(--sp-4); font-stretch: 100%;
}
.footer ul { list-style: none; padding: 0; margin: 0; }
.footer li { margin-bottom: var(--sp-3); overflow-wrap: anywhere; }

/* The services list runs to eight items against a four-item contact column, so
   the footer sat visibly lopsided with a tall stack on the left and empty space
   on the right. Setting it in two columns evens the heights. Done structurally
   rather than with a class because the footer markup is duplicated across all
   five pages and this keeps the change in one place. Only above the 760px
   breakpoint: below it the grid is already a single stack and two narrow
   columns of link text would be worse than one. */
@media (min-width: 761px) {
  .footer__grid > div:nth-child(2) ul {
    columns: 2;
    column-gap: var(--sp-6);
  }
  .footer__grid > div:nth-child(2) li { break-inside: avoid; }
}

.footer__bottom {
  padding-top: var(--sp-5);
  border-top: 1px solid rgba(var(--mist-rgb), 0.14);
  display: flex; justify-content: space-between; gap: var(--sp-4); flex-wrap: wrap;
  font-size: 0.86rem;
}

.footer__note {
  margin-top: var(--sp-4); font-size: 0.8rem; color: var(--steel-soft);
  max-width: var(--measure);
}

/* ==========================================================================
   Sticky mobile call bar — the whole site is phone-first
   ========================================================================== */

.callbar {
  position: fixed; left: 0; right: 0; bottom: 0;
  z-index: var(--z-overlay);
  display: none;
  align-items: center; justify-content: space-between; gap: var(--sp-4);
  padding: var(--sp-3) var(--gutter);
  background: var(--slate-900);
  border-top: 1px solid rgba(var(--mist-rgb), 0.16);
}
.callbar__label { font-size: 0.74rem; color: var(--steel-soft); margin: 0; line-height: 1.32; min-width: 0; }
.callbar__label strong { color: var(--white); display: block; font-size: 0.9rem; }
.callbar .btn { flex: none; padding: 0.8em 1.1em; }

@media (max-width: 860px) {
  .callbar { display: flex; }
  body { padding-bottom: 76px; }
}

/* ==========================================================================
   Motion — felt, not noticed. Everything here has a reduced-motion escape.
   ========================================================================== */

/* Gated on .js, set by main.js on the <html> element before paint. Without it
   the reveal state never applies, so a failed script, a headless renderer or a
   background tab shows finished content instead of a blank page. */
.js .reveal {
  opacity: 0;
  transform: translateY(16px);
  transition: opacity 620ms var(--ease-out), transform 620ms var(--ease-out);
}
.js .reveal.is-visible { opacity: 1; transform: none; }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
  .reveal { opacity: 1; transform: none; }
  .dot { animation: none; }
  .progress { display: none; }

  /* The circuit resolves to its finished state rather than its empty one: the
     conductor reads fully energised, every junction lit, traces drawn. Nothing
     moves, and nothing is missing. A reduced-motion visitor sees the design,
     not a stripped-back version of it.

     The substrate follows the same rule and is deliberately NOT hidden here.
     A board is a static object; it was only ever the drift and the current
     packets that were motion. main.js renders it once and stops, so the page
     keeps its background and simply holds still. */
  .js .rail::after { transform: scaleY(1); }
  .rail__head { display: none; }
  .js .section--node::before {
    background: var(--accent-glow);
    box-shadow: 0 0 14px 3px rgba(var(--accent-glow-rgb), 0.45);
    transform: rotate(45deg);
  }
  .js .hero__traces path { stroke-dashoffset: 0; }
  .js .hero__traces circle { opacity: 0.9; }
}

/* ==========================================================================
   Scroll-scrubbed showpiece
   --------------------------------------------------------------------------
   A tall section is the scroll runway; a sticky child pins one viewport of it;
   the canvas fills that. scrubber.js turns distance travelled into a frame index.

   These styles live here rather than in a scrubber.css because this site has one
   stylesheet by convention — and because the overlay has to inherit --accent*, so
   the same markup tints itself copper on electrical.html and blue on security.html
   with nothing duplicated.
   ========================================================================== */

.scrub {
  --scrub-screens: 4;               /* 4 = 400vh. Below 3 the animation whips past. */
  position: relative;
  height: calc(var(--scrub-screens) * 100vh);
}

.scrub__sticky { position: sticky; top: 0; height: 100vh; overflow: hidden; }

/* The canvas is the only thing on the page allowed to be full-bleed.

   There IS a seam to hide, contrary to what this comment used to claim. An mp4 carries no
   alpha, so every extracted frame is fully opaque, and `contain` stops it partway across the
   element — so the art punched a hard-edged rectangle out of the substrate animating behind
   the page. Two straight vertical lines, the full height of the viewport.

   Feather the image's own box, not the element's: showpiece.js publishes --fit-* because
   only JS knows where `contain` landed the image at this viewport aspect. The fallbacks let
   the rule degrade to an element-box feather if the script has not run yet.

   The four numbers are measured, not taste, and they are per-side because the art is not
   centred. Scanning every 8th frame of both sequences for the bounding box of drawn ink, the
   tightest margin on each side is:

       left 20.42%   right 3.47%   top 2.47%   bottom 5.68%

   (both minima on the right and top belong to the alarm, frames 241 and 257.) Each feather
   sits inside its own figure, so the ramp is fully opaque before it reaches any ink.

   Do not derive these from CONTENT/FILL_* in the renderers — that was the first attempt and
   it was wrong. Those constants bound the *layout* box, and the alarm draws past it, so the
   arithmetic said 5.66% everywhere while the pixels said 2.47%. Re-measure from the frames
   if the art changes; raising a feather past its figure dissolves the drawing, and the worst
   offender is the last frame, which is the one the page holds at the bottom of the scroll. */
.scrub__canvas {
  display: block; width: 100%; height: 100%;

  --feather-l: calc(var(--fit-w, 100%) * 0.14);
  --feather-r: calc(var(--fit-w, 100%) * 0.03);
  --feather-t: calc(var(--fit-h, 100%) * 0.02);
  --feather-b: calc(var(--fit-h, 100%) * 0.05);

  -webkit-mask-image:
    linear-gradient(to right,
      transparent var(--fit-x, 0px),
      #000 calc(var(--fit-x, 0px) + var(--feather-l)),
      #000 calc(var(--fit-x, 0px) + var(--fit-w, 100%) - var(--feather-r)),
      transparent calc(var(--fit-x, 0px) + var(--fit-w, 100%))),
    linear-gradient(to bottom,
      transparent var(--fit-y, 0px),
      #000 calc(var(--fit-y, 0px) + var(--feather-t)),
      #000 calc(var(--fit-y, 0px) + var(--fit-h, 100%) - var(--feather-b)),
      transparent calc(var(--fit-y, 0px) + var(--fit-h, 100%)));
  mask-image:
    linear-gradient(to right,
      transparent var(--fit-x, 0px),
      #000 calc(var(--fit-x, 0px) + var(--feather-l)),
      #000 calc(var(--fit-x, 0px) + var(--fit-w, 100%) - var(--feather-r)),
      transparent calc(var(--fit-x, 0px) + var(--fit-w, 100%))),
    linear-gradient(to bottom,
      transparent var(--fit-y, 0px),
      #000 calc(var(--fit-y, 0px) + var(--feather-t)),
      #000 calc(var(--fit-y, 0px) + var(--fit-h, 100%) - var(--feather-b)),
      transparent calc(var(--fit-y, 0px) + var(--fit-h, 100%)));

  /* Both gradients must apply at once — the corners need the intersection of the two, or
     they stay square. Safari spells intersect "source-in" on the prefixed property. */
  -webkit-mask-composite: source-in;
  mask-composite: intersect;
}

/* --- loading --- */
.scrub__loading {
  position: absolute; left: 50%; bottom: 3rem; translate: -50% 0;
  width: min(220px, 40vw); height: 2px;
  background: var(--rule);
  transition: opacity 0.5s ease 0.2s;
}
.scrub__loading > i {
  --p: 0;
  display: block; height: 100%; width: 100%;
  background: var(--accent-glow);
  transform-origin: left; scale: var(--p) 1;
  transition: scale 0.25s var(--ease-out);
}
.scrub__loading[data-done] { opacity: 0; pointer-events: none; }

/* --- spec rail --- */
.scrub__rail {
  position: absolute; inset: 0;
  pointer-events: none;             /* never eat scroll, never block the call button */
  display: grid; align-content: center;
  padding-inline: var(--gutter);
  padding-block: var(--sp-6);
}

.scrub__beat {
  grid-area: 1 / 1;                 /* all beats stack in one cell and cross-fade */
  max-width: 32ch;
  opacity: 0; translate: 0 1.25rem;
  transition: opacity 0.6s ease, translate 0.6s var(--ease-out);
}
.scrub__beat[data-active] { opacity: 1; translate: 0 0; }

/* The step number is set as a counter, not a heading: tabular figures so 01→05 does
   not shift the baseline as it changes, and a hairline rule tying it to the beat. */
.scrub__step {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  font-family: var(--font-display);
  font-size: var(--step--1);
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--accent-lit);
  margin-bottom: var(--sp-4);
}
.scrub__step::after {
  content: ""; flex: 1; height: 1px; max-width: 4rem;
  background: rgba(var(--accent-rgb), 0.45);
}

.scrub__beat h3 {
  font-size: var(--step-3);
  line-height: 1.1;
  margin: 0 0 var(--sp-4);
  text-wrap: balance;
}
.scrub__beat p { color: var(--ink-muted); margin: 0; line-height: 1.6; }

/* --steel-soft, NOT --steel. --steel is documented light-backgrounds-only and measured
   2.57:1 here — the audit caught it. --steel-soft is base-surface-only, and a scrub
   section is a base surface, so it holds: 4.98:1 on the copper ceiling, 4.83 on signal. */
.scrub__caption {
  position: absolute; right: var(--gutter); bottom: var(--sp-6);
  max-width: 24ch; text-align: right;
  font-size: var(--step--1); color: var(--steel-soft);
  margin: 0; line-height: 1.5;
}

/* --- anchor marker ---
   The contract says a beat must name a part AND the frame must make that part
   findable. Focal colour does most of that inside the render; this pin puts a mark
   on it. Position comes from anchors.json, which the renderer emits from the same
   beat table it draws with, through the same fit transform — so it cannot drift.

   Drawn as a crosshair rather than a plain ring: a bare circle floating over a
   schematic reads as a UI affordance you are meant to click. Ticks read as a
   drawing annotation, which is what this is. */
.scrub__pin {
  position: absolute;
  width: 34px; height: 34px;
  margin: -17px 0 0 -17px;
  opacity: 0;
  scale: 0.7;
  transition: opacity 0.5s ease, scale 0.5s var(--ease-out),
              left 0.55s var(--ease), top 0.55s var(--ease);
}
.scrub__pin::before,
.scrub__pin::after {
  content: "";
  position: absolute; inset: 0;
  border: 1px solid rgba(var(--accent-glow-rgb), 0.85);
}
/* horizontal ticks */
.scrub__pin::before {
  border-block: 0; border-inline-width: 7px;
  border-inline-style: solid;
  inset-block: 50%; margin-block: -0.5px;
}
/* vertical ticks + the ring */
.scrub__pin::after {
  border-radius: 50%;
  border-color: rgba(var(--accent-glow-rgb), 0.5);
  box-shadow: 0 0 14px -3px rgba(var(--accent-glow-rgb), 0.6);
}
.scrub__pin[data-on] { opacity: 1; scale: 1; }

/* --- progress track ---
   Sits under the copy rather than spanning the viewport: a full-width hairline at
   the bottom of the screen reads as a browser chrome artefact, and it competed with
   the loader bar for the same edge. */
.scrub__track {
  position: absolute; left: var(--gutter); bottom: var(--sp-6);
  width: min(18rem, 34vw);
  height: 2px; background: var(--rule);
  overflow: hidden;
}
.scrub__track > i {
  --p: 0;
  display: block; height: 100%; width: 100%;
  background: linear-gradient(to right, var(--accent), var(--accent-glow));
  transform-origin: left; scale: var(--p) 1;
}

/* Below 900px the rail would sit on top of the drawing. Move it out of the way:
   the canvas takes the upper half and the copy sits beneath it, which also stops
   30ch of text competing with a 16:9 schematic on a phone. */
@media (max-width: 900px) {
  .scrub { --scrub-screens: 3.4; }
  .scrub__sticky { display: grid; grid-template-rows: 52vh 1fr; }
  .scrub__canvas { height: 52vh; }
  .scrub__rail { position: static; align-content: start; padding-top: var(--sp-5); }
  .scrub__caption { position: static; text-align: left; max-width: none; margin-top: var(--sp-4); }
  .scrub__pin { display: none; }          /* too small to aim at, and it crowds the art */
  .scrub__loading { bottom: auto; top: calc(52vh - 1rem); }
}

/* --- reduced motion ---
   scrubber.js sets data-scrub-static: the runway collapses to one screen and every
   beat shows at once as a list. The copy is written to survive that. */
.scrub[data-scrub-static] { height: auto; }
.scrub[data-scrub-static] .scrub__sticky { position: static; height: auto; }
.scrub[data-scrub-static] .scrub__canvas { height: min(56vh, 420px); }
.scrub[data-scrub-static] .scrub__rail {
  position: static; display: grid; gap: var(--sp-6);
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  align-content: start;
}
.scrub[data-scrub-static] .scrub__beat {
  grid-area: auto; opacity: 1; translate: none; transition: none;
}
.scrub[data-scrub-static] .scrub__pin,
.scrub[data-scrub-static] .scrub__track { display: none; }

@media (prefers-reduced-motion: reduce) {
  .scrub__loading > i, .scrub__beat, .scrub__pin { transition: none; }
}

/* ==========================================================================
   Print
   ========================================================================== */

@media print {
  .header, .callbar, .progress, .rail, .backdrop { display: none !important; }

  /* A 400vh runway prints as three blank pages, and a canvas prints as nothing.
     Collapse the showpiece to its beats, which are written to read as a plain list
     anyway — the same guarantee the reduced-motion path relies on. */
  .scrub { height: auto !important; }
  .scrub__sticky { position: static !important; height: auto !important; }
  .scrub__canvas, .scrub__loading, .scrub__pin, .scrub__track { display: none !important; }
  .scrub__rail { position: static !important; display: block !important; }
  .scrub__beat {
    opacity: 1 !important; translate: none !important;
    break-inside: avoid; margin-bottom: 1rem; max-width: none;
  }
  /* html as well as body: the base fill moved up to the root element, so
     whitening body alone leaves a slate page behind it. */
  html, body { background: #fff; color: #000; }
  .section--dark, .section--slate, .section--alt, .hero, .emergency {
    background: #fff !important; color: #000 !important;
  }
}
