/* ==========================================================================
   TA Electrical GC. Free preview site by WVV Studios, 28 July 2026.

   Palette measured by pixel from Tom's own logo (1179x1180, his Facebook
   profile image): black 81.7%, steel #ACACAC 7.8%, white 5.6%,
   amber #F2AE3F 2.8%. Nothing here is inherited from another WVV project.

   Dark by design, for four reasons that all point the same way:
     1. it is his actual brand,
     2. the visitor is often standing in a dark house,
     3. a dark canvas is cheap to send down a mobile connection. This read
        "no photography plus a dark canvas" until 2 August 2026, when Tom sent
        his own job photos and the site went from nought to eleven images. The
        original reason no longer applies as written, so the discipline moved
        into the markup instead: every photograph is WebP with a smaller
        companion file, every one of them below the fold is loading="lazy",
        and every one carries explicit width and height so nothing shifts as
        they arrive. Photography is now the largest thing this site sends, and
        the budget is deliberate rather than absent,
     4. amber #F2AE3F is 10.6:1 on this canvas and 1.93:1 on white, so a dark
        page is the only one where his real brand colour is legible as text
        without us quietly darkening it.
   ========================================================================== */

/* ==== FONTS. Self hosted, latin subset, variable. No external CDN. ==== */
@font-face{
  font-family:'Chivo';src:url('fonts/chivo-var.woff2') format('woff2-variations');
  font-weight:400 900;font-style:normal;font-display:swap;
}
@font-face{
  font-family:'IBM Plex Sans';src:url('fonts/ibmplexsans-var.woff2') format('woff2-variations');
  font-weight:400 600;font-style:normal;font-display:swap;
}

/* ==== TOKENS ==== */
:root{
  /* surfaces, warm neutral rather than the cool near black of other builds */
  --canvas:#0B0B0C;
  --surface:#121214;
  --raised:#17171A;
  --sunk:#080809;

  /* his logo grey, doing structural work */
  --steel:#ACACAC;
  --steel-dim:#8C8C8C;

  /* text */
  --ink:#F4F4F2;
  --ink-soft:#C6C6C2;

  /* the one accent, rationed */
  --amber:#F2AE3F;

  --line:rgba(172,172,172,.16);
  --line-strong:rgba(172,172,172,.30);

  /* industrial shape language: near sharp, one scale, everywhere */
  --radius:3px;
  --radius-lg:5px;

  --ease:cubic-bezier(.2,.7,.3,1);
  --header-h:64px;
  --shear:16deg;                /* the angle the bolt cuts through the T */
  --measure:66ch;
}

/* ==== RESET AND BASE ==== */
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
html{
  color-scheme:dark;
  scroll-behavior:smooth;
  scroll-padding-top:calc(var(--header-h) + 16px);
  -webkit-text-size-adjust:100%;
}
body{
  background:var(--canvas);
  color:var(--ink);
  font:400 1rem/1.65 'IBM Plex Sans',sans-serif;
  font-synthesis-weight:none;
  overflow-x:hidden;
  padding-bottom:env(safe-area-inset-bottom);
}
img{max-width:100%;display:block;height:auto}
a{color:inherit}
button,input,select,textarea{font:inherit;color:inherit}
h1,h2,h3,h4,.num,.brand-word{font-family:'Chivo',sans-serif;letter-spacing:-.02em;line-height:1.08}
h1{font-weight:900;font-size:clamp(2.3rem,6.4vw,4rem)}
h2{font-weight:900;font-size:clamp(1.7rem,3.9vw,2.5rem)}
h3{font-weight:700;font-size:clamp(1.08rem,1.7vw,1.2rem);letter-spacing:-.01em}
p{max-width:var(--measure)}
:focus-visible{outline:3px solid var(--amber);outline-offset:3px;border-radius:1px}
::selection{background:var(--amber);color:#0B0B0C}
@media (prefers-reduced-motion:reduce){
  html{scroll-behavior:auto}
  *,*::before,*::after{animation-duration:.01ms!important;transition-duration:.01ms!important}
}

.wrap{max-width:1120px;margin-inline:auto;padding-inline:clamp(20px,5vw,44px)}
.wrap-narrow{max-width:760px;margin-inline:auto;padding-inline:clamp(20px,5vw,44px)}
section{padding-block:clamp(3.5rem,8vw,6.5rem)}
.band{background:var(--surface);border-block:1px solid var(--line)}
.lede{font-size:clamp(1.06rem,1.5vw,1.16rem);color:var(--ink-soft);max-width:58ch}
.tnum{font-variant-numeric:tabular-nums}

.skip{
  position:absolute;left:12px;top:-56px;z-index:200;
  background:var(--amber);color:#0B0B0C;font-weight:600;
  padding:.7rem 1.1rem;border-radius:var(--radius);transition:top .18s var(--ease);
}
.skip:focus{top:12px}

/* ==== THE SHEAR. The one geometry, taken from the bolt through the T. ==== */
.label{
  display:flex;align-items:center;gap:.7rem;
  font-family:'Chivo',sans-serif;font-weight:400;font-size:.72rem;
  letter-spacing:.2em;text-transform:uppercase;color:var(--steel);
  margin-bottom:1.1rem;
}
.label::before{
  content:"";width:16px;height:11px;flex:none;background:var(--amber);
  transform:skewX(calc(var(--shear) * -1));
}
.rule{height:1px;background:var(--line);border:0}

/* ==== SECTION HEAD. Built for the safety page, 3 August 2026 ====
   Will: "it looks so random and clunky", and the measurements agreed. That
   page ran 1554 words over eight sections in which only TWO opened the same
   way. Six of them began with a bare h2 and nothing else, so there was no
   repeating signal that a new section had started and the eye had to work it
   out from spacing alone. One section carried four separate ideas under a
   single h2, which spacing cannot fix at all.

   THE REPEATING SIGNAL IS: hard rule, amber shear chip, section name, h2. It
   fires identically at the top of every content section, so the page reads as
   a guide with parts rather than as a run of essays. The rule does as much
   work as the eyebrow, because padding alone is ambiguous at a glance and a
   line is not.

   A NUMBERED INDEX (01 / 02 / 03) WAS BUILT FIRST AND THROWN AWAY. It read
   as the generic editorial scaffold it is: it would have looked identical on
   any site, and this build's whole premise is that it is drawn from Tom's
   own logo. The existing .label chip is the shear of the bolt through the T,
   so using it here means the section markers are made of the same geometry as
   the buttons and the active nav item. The structural fixes (splitting the
   overloaded section, one content width, an eyebrow on EVERY section) are
   what actually cure the clunkiness; the numbers were decoration on top of
   the real repair. Do not reintroduce them.

   So .sec-head deliberately does NOT touch .label::before. The chip is the
   point. */
/* THE EYEBROW CAME OFF ON 3 AUG 2026, hours after it went on. The critique
   counted SEVEN tracked-caps eyebrows on this page against one or two on
   every other, which is the eyebrow-as-section-grammar tell: an eyebrow above
   every section is AI scaffolding, and a page-type label in the hero is
   voice. It was also plainly redundant, because the h2 sat directly beneath
   it saying the same thing in more words.

   THE RULE IS WHAT ACTUALLY FIXED THE CLUNKINESS, not the eyebrow, so the
   rule stays and the structure survives intact. The topic words are not lost
   either: they moved into the .contents strip at the top of the page, where
   six section names in one place are a navigation aid rather than a
   repeating decoration.

   The deterministic scanner does NOT catch this (it flags numbered markers,
   01/02/03, but not un-numbered eyebrows), so it has to be held by hand.
   Do not reintroduce a .label inside .sec-head. */
.sec-head{
  border-top:1px solid var(--line-strong);
  padding-top:1.15rem;margin-bottom:1.7rem;
}
/* Long compliance headings wrap into a shape rather than a ragged run-on.
   20ch is measured against the longest one on the page, "Ninety days, and the
   bit most websites get wrong", which lands on two lines at every width. */
.sec-head h2{max-width:20ch;text-wrap:balance}
/* Set-piece bands (the countdown, the pull quote) deliberately get NO
   .sec-head and stay centred. They are the punctuation between sections, and
   giving them the same opening would flatten the rhythm this creates. */

/* ==== ON THIS PAGE. Added 3 August 2026 from the critique ====
   The safety page is 6547px over nine sections and had no way to jump. It is
   also the page most likely to be arrived at from a search ("smoke alarm
   rules qld"), and its highest-value content, the ninety day correction and
   the which-situation-am-I router, sat mid-scroll and last respectively. A
   visitor who came for one answer had to read all of them.

   Deliberately NOT a boxed card and NOT a sidebar. It is one rule, one label
   and a wrapped row of links, so it costs about 90px and reads as a table of
   contents rather than as another component. It also gives the six section
   names a home now that they are not above each section.

   Anchor offset is already handled globally: html carries
   scroll-padding-top:calc(var(--header-h) + 16px), so a jump never lands
   under the sticky header. */
.contents{
  border-top:1px solid var(--line-strong);
  border-bottom:1px solid var(--line);
  padding-block:1.1rem;
}
.contents h2{
  font-family:'Chivo',sans-serif;font-weight:400;font-size:.72rem;
  letter-spacing:.2em;text-transform:uppercase;color:var(--steel);
  margin-bottom:.85rem;
}
.contents ul{
  list-style:none;display:flex;flex-wrap:wrap;gap:.4rem 1.6rem;
}
.contents a{
  display:inline-block;padding-block:.3rem;
  color:var(--ink-soft);text-decoration:none;font-size:.95rem;
  border-bottom:1px solid transparent;
  transition:color .15s var(--ease),border-color .15s var(--ease);
}
.contents a:hover{color:var(--ink);border-bottom-color:var(--amber)}

/* ==== GUIDE PAGES: TIGHTER VERTICAL RHYTHM ====
   Splitting the safety page's overloaded sections MADE IT TALLER, which is
   the exact opposite of what was asked for. Measured: 6730px before, 7470px
   after the split, on less copy. The site's default section padding is
   clamp(3.5rem,8vw,6.5rem), which is tuned for a home page of five or six
   long sections, and this page now has nine short ones paying up to 104px top
   AND bottom each.

   The padding can come in precisely BECAUSE .sec-head exists: separation is
   now being done by a hard rule and a repeated eyebrow, which is unambiguous
   at any distance, so it no longer has to be done by a gap big enough to read
   as one. Trading whitespace for a line is the whole point of the head.

   Scoped to .guide, set on <main>, so nothing else on the site moves. */
.guide > section{padding-block:clamp(2.3rem,4.4vw,3.3rem)}
/* The two centred set-pieces keep more room around them. They are supposed to
   feel like a pause, and a pause that is the same size as the rhythm it
   interrupts is not one. */
.guide > .deadline,
.guide > .band,
.guide > .cta{padding-block:clamp(3rem,6vw,4.6rem)}

/* ==== BUTTONS ==== */
.btn{
  --btn-bg:var(--amber);--btn-fg:#0B0B0C;
  position:relative;display:inline-flex;align-items:center;justify-content:center;gap:.55rem;
  min-height:52px;padding:.85rem 1.6rem;border:0;border-radius:var(--radius);
  background:var(--btn-bg);color:var(--btn-fg);
  font-family:'Chivo',sans-serif;font-weight:700;font-size:1.02rem;letter-spacing:-.01em;
  text-decoration:none;cursor:pointer;white-space:nowrap;
  touch-action:manipulation;-webkit-tap-highlight-color:transparent;
  transition:background-color .16s var(--ease),transform .1s var(--ease);
}
/* the sheared leading edge, drawn as a pseudo element so the focus ring
   stays a clean rectangle around the real control */
.btn::before{
  content:"";position:absolute;left:-7px;top:0;bottom:0;width:14px;
  background:inherit;transform:skewX(calc(var(--shear) * -1));border-radius:1px;
}
.btn:hover{background:#FFC463}
.btn:active{transform:scale(.985)}
.btn-ghost{
  --btn-bg:transparent;--btn-fg:var(--ink);
  border:1px solid var(--line-strong);
}
.btn-ghost::before{display:none}
.btn-ghost:hover{background:rgba(172,172,172,.09);border-color:var(--steel)}
.btn-lg{min-height:60px;font-size:1.18rem;padding:1rem 2rem}

.phone-link{
  font-family:'Chivo',sans-serif;font-weight:900;letter-spacing:-.02em;
  font-variant-numeric:tabular-nums;color:var(--amber);text-decoration:none;
  border-bottom:2px solid transparent;transition:border-color .16s var(--ease);
}
.phone-link:hover{border-bottom-color:var(--amber)}

/* ==== HEADER ==== */
.site-header{
  position:sticky;top:0;z-index:100;height:var(--header-h);
  background:rgba(11,11,12,.9);backdrop-filter:blur(12px);
  border-bottom:1px solid transparent;transition:border-color .2s var(--ease);
}
.site-header.is-stuck{border-bottom-color:var(--line)}
.header-in{display:flex;align-items:center;gap:1.5rem;height:var(--header-h)}
.brand{display:flex;align-items:center;gap:.6rem;text-decoration:none;margin-right:auto}
.brand img{width:34px;height:34px}
.brand-word{font-weight:900;font-size:1.05rem;letter-spacing:-.01em}
.brand-word span{color:var(--amber)}
/* ---- MAIN NAV. The shadcn/Radix NavigationMenu pattern, 3 Aug 2026 ----
   Will's brief: "this type of navigation bar ... do not implement the
   dropdown feature." Everything below is that component's anatomy with the
   menu machinery stripped out, which is also what Radix itself degrades to
   when an item has no <NavigationMenuContent>: the trigger stops being a
   button and becomes a plain link carrying navigationMenuTriggerStyle().

   So there is NO JavaScript here and none is needed. No viewport, no
   indicator, no popover, no aria-expanded, no focus trap. Six links in a
   list. That is the whole component at this size, and it is the reason the
   markup could stay static across all seven pages.

   WHAT CAME ACROSS VERBATIM from navigationMenuTriggerStyle():
     h-9 (36px), px-4, text-sm, font-medium, inline-flex centred
     gap-1 (4px) between items, list-none, semantic ul/li
     a FILLED hover and focus background, not a colour change
     a half-strength filled background for the active item
     transition on colour and box-shadow ONLY, so the fill snaps in
       instantly on hover exactly as it does in the original
     the layered focus treatment: ring-4 plus outline-1, a hairline edge
       inside a soft halo

   TWO DELIBERATE DEVIATIONS, both because the component is a neutral kit and
   this site is not:

   1. RADIUS IS 3px, NOT rounded-md's 6px. The token block at the top of this
      file commits to "one scale, everywhere" and every button, card and input
      on the site is on var(--radius). A 6px nav inside a 3px site reads as a
      part borrowed from somewhere else, which is the one thing a bought-in
      component must not do.

   2. THE ACTIVE ITEM KEEPS ITS AMBER SHEAR. shadcn marks the current page
      with bg-accent/50 alone, and on this canvas that is a mistake: the
      active fill would be FAINTER than the hover fill, so the moment the
      visitor's cursor is anywhere in the bar, "where I am" is outvoted by
      "where my mouse is". The amber bar was already the site's answer to
      that and it is the bolt-through-the-T shear, which is the one piece of
      geometry the whole build is drawn from. It moved inside the pill rather
      than being replaced by it.
      To go back to stock shadcn, delete the ::after rule. Nothing else
      depends on it. */
.nav{display:flex;align-items:center}
.nav-list{
  display:flex;align-items:center;justify-content:center;
  gap:4px;list-style:none;
}
.nav-item{position:relative}
.nav-link{
  position:relative;display:inline-flex;align-items:center;justify-content:center;
  height:36px;padding-inline:1rem;border-radius:var(--radius);
  font-size:.875rem;font-weight:500;line-height:1;letter-spacing:0;
  color:var(--ink-soft);text-decoration:none;white-space:nowrap;
  touch-action:manipulation;-webkit-tap-highlight-color:transparent;
  transition:color .15s var(--ease),box-shadow .15s var(--ease);
}
/* ORDER MATTERS BELOW AND IT IS NOT THE COMPONENT'S ORDER. Where you ARE is
   declared first, what you are DOING to it second, because ".nav-link:hover"
   and '.nav-link[aria-current="page"]' are both one class plus one qualifier
   and therefore the same specificity, so the later rule simply wins. Written
   in the original's order, with active last, the current page's own link is
   the one link in the bar that does not respond to the mouse: the .07 fill
   overrides the .14 one and the pill sits there looking dead under the
   cursor. Location is a fact about the page, hover and focus are facts about
   the visitor, and the visitor has to win. */
.nav-link[aria-current="page"]{background:rgba(172,172,172,.07);color:var(--ink)}
.nav-link[aria-current="page"]::after{
  content:"";position:absolute;left:1rem;right:1rem;bottom:6px;height:3px;
  background:var(--amber);transform:skewX(calc(var(--shear) * -1));
}
.nav-link:hover{background:rgba(172,172,172,.14);color:var(--ink)}
/* focus keeps the hover fill AND adds the ring, matching the component's own
   focus:bg-accent. The 2px amber edge is the accessible part (10.6:1 on this
   canvas); the wide 22% halo is the soft ring and measures only 1.52:1, so it
   is decoration and the indicator deliberately does not depend on it.
   Overriding the global :focus-visible offset and its border-radius:1px so
   the ring hugs the pill instead of squaring it off. */
.nav-link:focus-visible{
  background:rgba(172,172,172,.14);color:var(--ink);
  outline:2px solid var(--amber);outline-offset:0;
  border-radius:var(--radius);
  box-shadow:0 0 0 5px rgba(242,174,63,.22);
}
.nav-call{display:inline-flex}
.menu-btn{
  display:none;align-items:center;justify-content:center;
  width:46px;height:46px;background:none;border:1px solid var(--line-strong);
  border-radius:var(--radius);cursor:pointer;touch-action:manipulation;
}
.menu-btn svg{width:20px;height:20px;stroke:var(--ink);stroke-width:2;fill:none}
/* ---- THE MOBILE PANEL IS FIXED TO THE VIEWPORT, NOT LEFT IN FLOW ----
   Fixed 4 August 2026, Tom's bug report on the walkthrough call: the menu
   "does not drop down where you are, it goes to the top of the page".

   THE CAUSE, because it is not obvious from the markup. .site-header is
   position:sticky, so it detaches and floats at the top of the viewport as
   you scroll. This panel is the very next element in the document, but it was
   a plain static block, so it never left its place at the TOP OF THE
   DOCUMENT. Open the menu anywhere below the fold and it duly expanded, a
   full screen or more above you, where nobody could see it. The button
   reported aria-expanded="true" and the visitor saw nothing happen, which is
   the worst version of this bug: it looks like a dead button.

   Fixing it to the viewport under the header is what ties the panel to the
   header it belongs to, at any scroll position. --header-h is the same token
   the header's own height uses, so the two can never drift apart.

   THE BACKGROUND MUST STAY OPAQUE. --surface is #121214, solid. The header
   above it can afford rgba + backdrop-filter because it is a thin bar; this
   panel now has page content running underneath it, and a translucent one
   would put the nav links over whatever happened to be behind them.

   max-height + overflow-y: six links fit any phone in portrait, but not a
   short landscape window, and a menu you cannot scroll to the bottom of is a
   trap. overscroll-behavior:contain stops that scroll chaining to the page
   underneath once it bottoms out. */
.mobile-nav{
  display:none;
  position:fixed;top:var(--header-h);left:0;right:0;z-index:99;
  border-bottom:1px solid var(--line);background:var(--surface);
  max-height:calc(100dvh - var(--header-h));
  overflow-y:auto;overscroll-behavior:contain;
}
/* .mobile-nav.open{display:block} RETIRED 5 Aug 2026. The drawer is no longer
   toggled by a class on the panel and is never display:none on mobile: it is
   visibility + clip-path, driven by html.nav-open, so a wipe is possible at
   all (display cannot transition). See the mobile block appended at the end
   of this file. */
.mobile-nav a{
  display:block;padding:1rem 0;border-bottom:1px solid var(--line);
  text-decoration:none;font-size:1.05rem;
}
.mobile-nav a:last-of-type{border-bottom:0}

/* THE PILLS COST WIDTH AND THE HEADER HAD NONE SPARE, so two things moved
   with them. Measured at 1440, where the wrap is capped at 1120: brand 172 +
   nav 486 + call button 189 + two 24px gaps = 895 inside a 1032 content box.
   Comfortable. But the wrap keeps shrinking below 1120 while that 895 does
   not, and the old bare-text nav was 70px narrower, so it cleared widths the
   pill version does not: at 940 the row needs 895 against 852 available.
   Nothing would have looked broken either, which is the dangerous part.
   body{overflow-x:hidden} would have silently clipped Contact rather than
   scrolled to it.

   1. Below 1120 the pill padding and the header gap tighten. Text size and
      the 36px height do not move, so the hit target is untouched.
   2. The hamburger breakpoint moved 900 to 1000. Even tightened, six pills
      plus a brand plus a 189px phone button in a 813px box at 901 is 1px of
      slack, which is not a margin, it is a coincidence. A window between 900
      and 1000 is rare (iPad portrait is 768 and landscape is 1024, so both
      land exactly where they did before) and the mobile menu is the better
      answer there anyway. */
@media (max-width:1120px){
  .header-in{gap:1rem}
  .nav-link{padding-inline:.7rem}
  .nav-link[aria-current="page"]::after{left:.7rem;right:.7rem}
}
@media (max-width:1000px){
  .nav{display:none}
  .menu-btn{display:inline-flex}
}
@media (max-width:520px){
  .nav-call{display:none}
}

/* ==== HERO ====
   Inner pages (.is-inner) stay typographic on the flat canvas. The home page
   became a photographic hero on 3 August 2026; everything under .has-photo
   below governs that and nothing else. The old two-column layout that put the
   logo mark in a right-hand column is gone with it, and so is .hero-mark,
   which nothing references any more. */
.hero{
  position:relative;padding-block:clamp(3rem,7vw,5.5rem) clamp(3rem,6vw,4.5rem);
  border-bottom:1px solid var(--line);overflow:hidden;
}
.hero::after{ /* a single sheared amber edge, the bolt angle at page scale */
  content:"";position:absolute;right:-14%;top:-30%;width:52%;height:160%;
  background:linear-gradient(180deg,rgba(242,174,63,.075),rgba(242,174,63,0) 70%);
  transform:skewX(calc(var(--shear) * -1));pointer-events:none;
}
.hero-in{position:relative;z-index:1;display:grid;gap:clamp(1.6rem,3vw,2.4rem)}
.hero h1{max-width:15ch}

/* ==== PHOTOGRAPHIC HERO ==== DORMANT SINCE 3 AUGUST 2026, KEPT ON PURPOSE.
   ---------------------------------------------------------------------------
   NOTHING ON THIS SITE CARRIES .has-photo ANY MORE, so every rule in this
   block is currently inert. Tom asked, on the second revision of 3 August, for
   the main page to stay black with the vehicle photograph placed on the page
   instead of behind the writing. The photo moved to .van-band further down and
   the home page hero went back to the flat typographic treatment above.

   It is kept rather than deleted for one concrete reason: Tom is arranging a
   photograph of himself and Sam actually on a job, and if that becomes a hero
   later, the measured veil audit below is expensive to recreate and easy to
   get wrong. The numbers in it were solved against a luminance map of the
   actual frame, not chosen by eye.

   TO REACTIVATE: add has-photo to the hero section in index.html, restore the
   .hero-bg img and the .hero-veil div inside it, restore the <head> preload,
   and RE-RUN THE CONTRAST AUDIT against whatever the new frame is. The stops
   below are specific to vans-wide.webp and are not transferable to a different
   photograph.
   ---------------------------------------------------------------------------
   Original reasoning follows, unchanged.

   Will's brief: the two-vehicle photo as a backdrop taking about 65% of the
   landing screen. The header is sticky and 64px, so subtracting it makes
   header + hero exactly 65vh and the credentials strip is deliberately visible
   underneath on load. Clamped top and bottom so a very short laptop window
   still gets a usable hero and a tall monitor does not get a billboard.

   THE PHOTO IS TREATED AS A GROUND, NOT A PICTURE, and the numbers below come
   from a measured luminance map of vans-wide.webp rather than taste. That map,
   in eighths across the frame, runs 25/21/15/11/22/35/37/37 along the top and
   43/34/29/32/7/2/10/6 lower down: the white van on the LEFT peaks at 52%
   relative luminance and the dark ute on the RIGHT bottoms out at 2%. So the
   frame is bright exactly where a left-aligned headline wants to sit, on a page
   whose entire concept is a dark house.
   The answer is a directional veil rather than a flat wash. It is near-opaque
   at the left edge where the type lands, and thins to almost nothing by the
   right edge so the ute and its TA ELEC plate stay legible as the subject.
   A flat wash heavy enough for the van would have turned the ute to mud. */
.hero.has-photo{
  padding:0;border-bottom:0;
  min-height:clamp(430px,calc(65vh - var(--header-h)),660px);
  /* The explicit minmax(0,1fr) column is load-bearing, not decoration.
     .hero-in is .wrap, which carries margin-inline:auto, and auto margins on a
     grid item defeat the default stretch and collapse it to fit-content. With
     an implicit auto column that fed straight back into the 58% inner column
     below and settled at a 342px text block, which wrapped the h1 to four lines
     and pushed the hero to 826px, 99% of the screen instead of 65%. */
  display:grid;grid-template-columns:minmax(0,1fr);align-items:end;
}
.hero.has-photo::after{content:none} /* the amber shear reads as haze over a photo */

.hero-bg{
  position:absolute;inset:0;z-index:0;width:100%;height:100%;
  /* 40% down, not centred. A 1440x521 box is 2.76:1 against a 1.5:1 source, so
     cover throws away 45% of the height and the choice of which 55% to keep is
     the difference between reading as two vehicles and reading as one dark car.
     Crops were rendered at 30/40/52/62% and compared: 52% (the obvious centre)
     cropped the white van down to its grille and lost the two-vehicle point
     Tom actually asked for. 40% keeps the van's body, its perforated grille and
     the TA ELEC2 plate on the left, and the ute's bonnet and grille on the
     right, with no dead showroom ceiling at the top. */
  object-fit:cover;object-position:50% 40%;
  /* Graded to night. This is the single most important number on the page and
     it is not a taste call: at the showroom's own exposure the white van peaks
     at 52% relative luminance, and no veil light enough to leave the ute
     visible can also carry body text over that. Dropping the whole frame to
     62% brightness first means the veil only has to do local work, which is
     what keeps the right-hand side of the photograph readable instead of mud.
     Saturation comes down with it so the fluorescent cast stops fighting the
     amber. Raising brightness here without re-running the contrast audit will
     silently break AA on the hero paragraph. */
  filter:saturate(.80) brightness(.62);
}
.hero-veil{position:absolute;inset:0;z-index:1;pointer-events:none}
.hero-veil::before,.hero-veil::after{content:"";position:absolute;inset:0}
/* Horizontal. This is the one carrying the text, and every stop below was
   solved against the measured frame rather than chosen. Required alpha across
   the desktop text zone came out between 0.29 and 0.54 for 5:1 on --ink-soft;
   these stops sit above that the whole way, then fall away past 74% so the ute
   and its TA ELEC plate stay legible as the subject of the shot. */
.hero-veil::before{
  background:linear-gradient(100deg,
    rgba(8,8,9,.72) 0%,  rgba(8,8,9,.68) 22%, rgba(8,8,9,.64) 42%,
    rgba(8,8,9,.56) 58%, rgba(8,8,9,.24) 74%, rgba(8,8,9,.05) 100%);
}
/* Vertical. Seats the photo under the sticky header at the top, and dissolves
   it into the credentials strip at the bottom. The final stop is #080809,
   which is exactly var(--sunk), so the photo does not end at an edge, it
   becomes the strip of verifiable facts underneath it. */
.hero-veil::after{
  background:linear-gradient(180deg,
    rgba(8,8,9,.78) 0%, rgba(8,8,9,.20) 16%, rgba(8,8,9,0) 42%,
    rgba(8,8,9,.42) 80%, #080809 100%);
}
/* Tighter than the typographic hero on purpose, and the tightening is what
   keeps the 65%. A photo hero wants its type as one block rather than three
   spaced ones, and at the original gap and padding the content measured 576px
   against a 521px min-height, so the content, not the brief, was setting the
   height. These values bring it under so min-height governs again. */
/* TIGHTENED AGAIN 3 Aug 2026, when Tom's plain logo went in above the h1.
   The note below is the original reasoning and still holds; the numbers moved
   because the stack got a whole extra element. Measured at 1440x900: the hero
   was 592px before the logo and 656px after it, which took header plus hero
   from roughly 65% of the screen to 72.9%. Trimming the gap and the block
   padding here, plus clamping the logo by height rather than width, buys most
   of that back. It does not buy all of it, and that is a deliberate trade:
   Tom asked for the mark and the functional half of the 65% rule, the
   credentials strip being on screen at load, is still true at every viewport
   size tested down to 1280x720. */
.hero.has-photo .hero-in{
  position:relative;z-index:2;
  /* width:100% is required, not tidying. .hero-in is .wrap, which sets
     margin-inline:auto, and auto margins on a grid item beat the default
     stretch and shrink it to fit-content. It settled at 687px inside a 1425px
     column, so the 58% inner track below resolved against 599px and produced a
     347px text block with the h1 on four lines. Explicit width restores the
     stretch; max-width:1120px on .wrap still caps it and the auto margins
     still centre it. */
  width:100%;
  gap:clamp(.95rem,1.7vw,1.25rem);
  padding-block:clamp(1.3rem,2.9vw,1.95rem);
}
.hero.has-photo .hero-sub{max-width:52ch}
@media (min-width:900px){
  /* Hold the block inside the dark end of the horizontal veil. Past about 58%
     the veil is too thin to carry body text over the white van. */
  .hero.has-photo .hero-in{grid-template-columns:minmax(0,min(58%,660px))}
}
/* THE BREAKPOINT HERE MUST STAY AT 899.98px, MATCHING THE min-width:900px RULE
   ABOVE. An earlier version put the veil switch at 760px while the text-column
   constraint switched at 900px, which left the 760 to 899 band using the
   desktop veil with FULL-WIDTH text. Measured at 768px, the hero paragraph
   landed on 1.81:1. Move one of these two breakpoints and you have to move the
   other. Below 900px the text runs the whole wrap width and the crop lands on
   the join between the two vehicles, so there is no dark column to aim at and
   the veil goes to an even wash instead of a directional one. */
@media (max-width:899.98px){
  /* A portrait viewport keeps only the middle ~47% of the frame. Centred, that
     lands on the join between the two vehicles; 35% puts the white van and its
     signage in shot alongside the ute's grille and TA ELEC plate, so both
     vehicles still read on a phone. */
  .hero-bg{object-position:35% 50%}
  /* Lighter than the first pass. At .62/.58/.52 the photograph was measurably
     fine and visually gone: on a phone it read as flat dark texture, which
     defeats the point of putting it there. Re-audited at these values and the
     hero paragraph still sits at 5.5:1 with amber at 4.9:1, both clear of AA,
     while the vehicles are actually visible. */
  .hero-veil::before{
    background:linear-gradient(100deg,
      rgba(8,8,9,.50) 0%, rgba(8,8,9,.45) 50%, rgba(8,8,9,.40) 100%);
  }
  .hero-veil::after{
    background:linear-gradient(180deg,
      rgba(8,8,9,.72) 0%, rgba(8,8,9,.22) 18%, rgba(8,8,9,.30) 40%,
      rgba(8,8,9,.80) 72%, #080809 100%);
  }
}
/* ==== THE PLAIN LOGO IN THE HERO. Tom's request, 3 August 2026. ====
   His words: put the plain mark above "Your local sparky on the Gold Coast",
   and use the same one in the footer.

   THIS IS HIS REAL ARTWORK, not the old reconstruction. assets/logo-plain.png
   is his supplied file trimmed to its own alpha bounding box and nothing else:
   no redraw, no upscale, no hand-fixing. It arrived already transparent, so
   the flood-fill step that other logos on this account have needed did not
   apply, and the white letterforms inside ELECTRICAL were never at risk. The
   bolt tail is complete on this one, which the badge in the header is not.

   SIZED BY HEIGHT, NOT WIDTH, and that is the whole trick. The hero is tuned
   so header plus hero is 65% of the landing screen with the credentials strip
   showing underneath, and the CSS below already records one occasion where
   content rather than the brief set that height. Height is the axis that
   threatens it, so height is the axis that is clamped; the width follows the
   mark's own 534x609 ratio. Re-measure the hero if you change this. */
/* SIZED FOR THE WORDMARK, NOT THE MONOGRAM, and that is why it is not smaller.
   This mark is a lockup: TA and the bolt, plus ELECTRICAL and GC set small
   underneath. At 94px tall the monogram read fine and the wordmark turned to
   mush, which is a poor way to show someone the logo they just asked you to
   put up. The floor here is the height at which ELECTRICAL is still legible.
   Most of the mark's height is the bolt tail hanging below the wordmark, so it
   costs more vertical room than its visual weight suggests. */
.hero-logo{
  display:block;width:auto;
  height:clamp(78px,9vw,118px);
  /* Pulled tight to the headline because a logo and the sentence under it read
     as one lockup, not two blocks.

     THIS MOVED OUT OF .hero.has-photo ON 3 AUGUST 2026 AND THAT WAS A REAL
     BUG, not tidying. The margin only existed under .has-photo, because the
     logo and the photographic hero landed in the same pass and no other hero
     had ever carried a logo. Taking the photo away took the spacing with it
     and butted the mark straight onto the h1, since the logo and the headline
     share one grid cell and .hero-in's gap therefore does not separate them.
     It belongs on the element, not on one of its backgrounds. */
  margin-bottom:clamp(.6rem,1.2vw,.85rem);
}

.hero h1 b{color:var(--amber);font-weight:inherit}
/* text-wrap:pretty added 3 Aug 2026. Tom's new hero paragraph is roughly twice
   the length of the one it replaced and runs to six lines on a 390px phone,
   which left a one-word orphan ("repairs.") on the last line. His copy is
   published verbatim, so the fix is typographic, not editorial. Same reasoning
   as the review quotes further down: pretty, not balance, because this is a
   left-aligned multi-sentence paragraph and balance would ragged the right
   edge. Degrades to normal wrapping on browsers that do not support it. */
.hero-sub{font-size:clamp(1.06rem,1.6vw,1.2rem);color:var(--ink-soft);max-width:44ch;text-wrap:pretty}
.hero-actions{display:flex;flex-wrap:wrap;gap:.85rem;align-items:center}
.hero-num{display:block;font-size:.8rem;color:var(--steel);margin-top:.55rem}

/* credentials sit BELOW the hero, never inside it */
.creds{border-bottom:1px solid var(--line);background:var(--sunk)}
.creds ul{
  display:flex;flex-wrap:wrap;gap:.6rem 2rem;list-style:none;
  padding-block:1rem;font-size:.88rem;color:var(--ink-soft);
}
.creds li{display:flex;align-items:center;gap:.5rem}
.creds li::before{
  content:"";width:9px;height:9px;flex:none;background:var(--steel);
  transform:skewX(calc(var(--shear) * -1));
}

/* ==== TRIAGE. Nobody in this category publishes this. ==== */
.triage{display:grid;grid-template-columns:1.05fr .95fr;gap:clamp(1.5rem,4vw,3.5rem);align-items:start}
/* marked with the page's own shear rather than a coloured side stripe */
.triage-card{
  position:relative;background:var(--raised);border:1px solid var(--line);
  border-radius:var(--radius);padding:clamp(1.3rem,2.6vw,1.9rem);
}
.triage-card::before{
  content:"";position:absolute;left:clamp(1.3rem,2.6vw,1.9rem);top:-2px;
  width:30px;height:4px;background:var(--amber);
  transform:skewX(calc(var(--shear) * -1));
}
.triage-card h3{margin-bottom:.6rem}
.triage-card p{font-size:.97rem;color:var(--ink-soft)}
.fault-list{list-style:none;display:grid;gap:.9rem;margin-top:1.1rem}
.fault-list li{display:grid;grid-template-columns:auto 1fr;gap:.9rem;align-items:baseline;padding-top:.9rem;border-top:1px solid var(--line)}
.fault-num{font-family:'Chivo',sans-serif;font-weight:900;font-size:1.15rem;color:var(--amber);font-variant-numeric:tabular-nums;white-space:nowrap}
.fault-list span{font-size:.94rem;color:var(--ink-soft)}
@media (max-width:820px){.triage{grid-template-columns:1fr}}

/* ==== SERVICES. Heading left, copy right. Deliberately neither a three
       card grid nor a numbered list, both of which read as scaffold. ==== */
.svc-list{display:grid;gap:0;margin-top:2rem;border-top:1px solid var(--line)}
.svc-row{
  display:grid;grid-template-columns:minmax(150px,.3fr) 1fr;gap:clamp(.6rem,3vw,2.6rem);
  padding-block:clamp(1.6rem,3vw,2.2rem);border-bottom:1px solid var(--line);
  align-items:baseline;
}
.svc-row h3{margin:0;font-size:clamp(1.15rem,2vw,1.4rem)}
.svc-row p{color:var(--ink-soft);font-size:.99rem}
@media (max-width:640px){
  .svc-row{grid-template-columns:1fr;gap:.5rem}
}

/* ==== ICONED VARIANT OF THE LIST. 4 August 2026, Tom's call ====
   An amber mark and a white label in the left column, the copy in the right.
   Scoped to .is-iconed so the bare .svc-list above is untouched.

   ALIGN-ITEMS GOES TO START ON THIS VARIANT ONLY. The base row aligns on the
   baseline, which is right when the left column is a lone heading: the h3 and
   the paragraph then sit on the same line. Put a 34px icon beside that heading
   and the flex row's baseline is still the h3's text, so the icon hangs above
   the line and drags the whole left column up out of alignment with the copy.
   Aligning both columns to the top makes the icon and the first line of copy
   start together, which is the relationship you actually see. */
.svc-list.is-iconed .svc-row{align-items:start}
.svc-head{display:flex;align-items:center;gap:.65rem}
.svc-head h3{margin:0}
/* Sheared like everything else on this site that marks a thing, so the icons
   read as part of the page rather than as a downloaded set. The shear is on
   the box and unwound on the glyph, so the SQUARE tilts and the drawing inside
   it stays upright: a tilted house reads as a mistake, a tilted plate does
   not. Same trick as .label::before and the nav's current-page underline. */
.svc-icon{
  flex:none;display:grid;place-items:center;
  width:38px;height:38px;
  border:1px solid var(--line-strong);
  background:var(--sunk);
  transform:skewX(calc(var(--shear) * -1));
}
/* STROKE WIDTH IS IN viewBox UNITS, SO IT HAD TO BE REDONE when the icons
   moved from a 32 grid to a 24 one on 4 Aug 2026. The old pair was 1.7 units
   on a 32 grid drawn at 24px, which is 1.7 x (24/32) = 1.28 real pixels.
   Carrying 1.7 straight over to a 24 grid would have rendered 1.84px, a 44%
   jump, and the icons would have looked bolder than everything around them.
   1.5 units at 26px lands at 1.63px: a little heavier than before on purpose,
   because the shopfront's scallops need the extra weight to hold at size. */
.svc-icon svg{
  width:26px;height:26px;display:block;
  fill:none;stroke:var(--amber);
  stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round;
  transform:skewX(var(--shear));
}
@media (max-width:640px){
  /* The heading row keeps its own gap from the copy once the columns stack,
     otherwise the .5rem row gap puts the paragraph tight under the icon. */
  .svc-list.is-iconed .svc-head{margin-bottom:.25rem}
}

/* ==== A NAMED JOB NESTED INSIDE A CATEGORY ROW. 5 August 2026 ====
   One specific job (the generator changeover switch) that belongs to Domestic
   rather than beside it. See the comment at its markup in index.html.

   grid-column:2 IS THE WHOLE MECHANISM AND IT IS NOT DECORATION. .svc-row is
   a two column grid whose children are placed in source order, so a third
   child would otherwise land in column 1, directly under the category icon,
   which is precisely where a reader has learned to expect a category. Forcing
   it into column 2 lines it up under the paragraph it belongs to instead.

   It carries the SAME .svc-icon as the categories, at full size, deliberately:
   the indent already says "this is inside Domestic", and shrinking the box
   would thin the glyph relative to the rest of the set, because stroke-width
   is in viewBox units and scales with the icon. */
.svc-sub{
  grid-column:2;
  display:flex;gap:.65rem;align-items:start;
  margin-top:1.15rem;padding-top:1.15rem;
  border-top:1px solid var(--line);
}
.svc-sub p{margin:0;color:var(--ink-soft);font-size:.99rem}
.svc-sub strong{color:var(--ink);font-weight:600}
@media (max-width:640px){
  /* The row is one column down here, so column 2 does not exist. Without this
     the sub-item is placed into a column the grid never created.

     THE INDENT REPLACES THE COLUMN. On desktop, sitting in column 2 under
     Domestic's paragraph is what says "this belongs to that". Stacked, every
     row is in column 1 and that signal is gone, so the item reads as a fourth
     category again, which is the exact thing this markup exists to avoid.
     margin-left rather than padding-left on purpose: it insets the hairline
     rule as well, so the rule above this item is visibly shorter than the
     ones separating the categories, and the hierarchy survives the
     breakpoint. */
  .svc-sub{grid-column:1;margin-left:1.4rem}
}

/* screen reader only */
.vh{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}

/* ==== SMALL UTILITIES, so pages stay free of inline style ==== */
.p-body{margin-top:1.2rem;color:var(--ink-soft)}
.center{text-align:center}
.mt{margin-top:1.2rem}
.mt-lg{margin-top:2rem}
.link-amber{color:var(--amber)}
.pull{
  font-family:'Chivo',sans-serif;font-weight:900;letter-spacing:-.025em;
  font-size:clamp(1.55rem,3.8vw,2.35rem);line-height:1.22;
  margin-inline:auto;max-width:26ch;text-wrap:balance;
}
.pull-src{margin:1.1rem auto 0;color:var(--steel);font-size:.9rem}
.hero.is-inner{padding-block:clamp(2.4rem,5vw,3.6rem)}
.center .preview-flag{text-align:left}
.foot-blurb{color:var(--ink-soft);font-size:.9rem}
.callbar-in{padding-inline:0}
.opt{color:var(--steel);font-weight:400}
.center-block{text-align:center;margin:2rem auto 0}
.link-steel{color:var(--steel)}

/* ==== WHY. Four short items under top hairlines. ==== */
.why{display:grid;grid-template-columns:repeat(4,1fr);gap:clamp(1.2rem,2.6vw,2rem);margin-top:2rem}
/* .why is SHARED: the home page "Why TA Electrical GC" grid and the About page
   "How Tom works" grid both use it. The home page dropped to three cards on
   3 Aug 2026 when "Free quotes" came out, About still has four, so the three-up
   case is a modifier rather than a change to the base rule. Changing .why
   itself would have rendered About as 3+1. If Tom's public liability cover
   fills the empty slot, delete the modifier from index.html and it is four
   again with no CSS change. */
.why.is-three{grid-template-columns:repeat(3,1fr)}
.why-item{border-top:2px solid var(--line-strong);padding-top:1.1rem}
.why-item h3{margin-bottom:.45rem}
.why-item p{font-size:.93rem;color:var(--ink-soft)}
/* Both selectors are listed in each query on purpose. .why.is-three is a
   two-class selector and outranks a bare .why, so without naming it here the
   modifier would win at every width and the three-card grid would still be
   three columns on a phone. Measured before this line existed: 104px columns
   and 14 characters per line at 390px. Any future .why modifier has to be
   added to both queries for the same reason. */
@media (max-width:900px){.why,.why.is-three{grid-template-columns:repeat(2,1fr)}}
@media (max-width:520px){.why,.why.is-three{grid-template-columns:1fr}}

/* ==== QUOTE ==== */
.quote{max-width:44ch;margin-inline:auto;text-align:center}
.quote blockquote{
  font-family:'Chivo',sans-serif;font-weight:700;letter-spacing:-.02em;
  font-size:clamp(1.3rem,2.8vw,1.85rem);line-height:1.3;text-wrap:balance;
}
.quote figcaption{margin-top:1.3rem;font-size:.9rem;color:var(--steel)}
.quote figcaption b{color:var(--ink);font-weight:600;display:block;margin-bottom:.15rem}
.quote-mark{
  width:26px;height:18px;background:var(--amber);margin:0 auto 1.6rem;
  transform:skewX(calc(var(--shear) * -1));
}

/* ==== REVIEW LIST ====
   Added 2 August 2026, when Tom sent four more recommendations and the page
   went from one review to five.

   The .quote treatment above is a centred 44ch pull-quote at up to 1.85rem.
   That is exactly right for ONE review carrying a whole section, and it
   becomes several screens of scrolling at five. So the same elements step
   down inside .review-list rather than a new card component appearing: left
   aligned, wider measure, smaller display size, and the amber mark shrinks
   to a list marker. Same figure, same blockquote, same figcaption.

   text-wrap goes from balance to pretty here. Balance is for short centred
   display lines; on a left-aligned paragraph of four sentences it produces a
   visibly ragged right edge. */
.review-list{margin-top:2rem;border-top:1px solid var(--line)}
.review-list .quote{
  max-width:none;margin-inline:0;text-align:left;
  padding-block:clamp(1.5rem,3vw,2.1rem);
  border-bottom:1px solid var(--line);
}
.review-list .quote-mark{width:18px;height:12px;margin:0 0 1rem}
.review-list blockquote{
  font-size:clamp(1.08rem,1.9vw,1.32rem);line-height:1.38;
  max-width:56ch;text-wrap:pretty;
}
.review-list figcaption{margin-top:1rem}

/* ==== AREAS ==== */
.area-grid{display:flex;flex-wrap:wrap;gap:.7rem;margin-top:1.8rem;list-style:none}
.area-grid li{
  border:1px solid var(--line-strong);border-radius:var(--radius);
  padding:.65rem 1.1rem;font-size:.95rem;
}
.area-grid li.base{border-color:var(--amber);color:var(--amber);font-weight:600}

/* ==== ABOUT ==== */
/* DORMANT SINCE 3 AUG 2026, kept rather than deleted. Both rules below were
   used by exactly one thing, Sam's bio beside the van portrait, and that
   section is now a .person card with a placeholder instead. Nothing on the
   site references either class today. .about-grid is a generic two-column
   text-and-figure layout and is the obvious thing to reach for the next time
   a page needs one, which is why it is still here. */
.about-grid{display:grid;grid-template-columns:.95fr 1.05fr;gap:clamp(1.8rem,5vw,4rem);align-items:center}
.about-grid figure{margin:0}
.about-grid img{border-radius:var(--radius-lg);border:1px solid var(--line)}
.about-grid figcaption{margin-top:.8rem;font-size:.86rem;color:var(--steel);max-width:46ch}
@media (max-width:860px){.about-grid{grid-template-columns:1fr}}
/* ALTERNATING PHOTO SIDE, added 4 Aug 2026 when both men got a portrait.
   Tom's row puts his picture on the left and Sam's keeps his on the right, so
   the two read as a pair working down the page rather than the same row twice.

   DONE WITH `order`, NOT DOM ORDER, and that is the whole reason this rule
   exists rather than just swapping the markup. Below 860px the grid collapses
   to one column, and if the flip lived in the HTML then Tom's face would land
   above his own name and role while Sam's sat below his. Keeping the text
   first in the document and flipping only on the wide layout means the reading
   order is name, role, story, photograph for both of them at every width. */
@media (min-width:861px){
  .about-grid.is-flipped > div{order:2}
  .about-grid.is-flipped > figure{order:1}
}

/* ==== THE TWO PEOPLE, one box each. Added 3 August 2026 ====
   Will's brief: a box per person carrying their story, with the photo slot
   drawn as a black box with a yellow border rather than filled. Sam's van
   portrait came OUT as part of this ("I don't want to use that photo again
   for Sam"), so both people are now on placeholders and the two cards match.

   THE AMBER EDGE IS DOING A JOB, not just decoration. Every real photograph
   on this site carries the same 1px hairline in --line, per the photography
   rule further up. Drawing the empty slots in 2px amber instead means the
   two treatments can never be confused: hairline steel is a photo, amber is
   a hole where a photo goes. That also stops a black rectangle reading as a
   broken image on a page Tom is being shown.

   AND THEY SAY SO IN WORDS. The box carries "Photo of Tom to come" as real
   text, not a background image and not aria-hidden, so it is honest in the
   design, honest to a screen reader, and honest in view-source. The site's
   flag-every-placeholder rule is the whole reason this page can be shown to
   a client at all. Delete the <p> and you have an unexplained black box.

   No image file and no <img>: the slot is an aspect-ratio box, so it costs
   zero requests and zero layout shift. When the real photos arrive, swap the
   div for an <img> with explicit width and height and drop .person-photo. */
/* ALIGN-ITEMS:START, NOT THE GRID DEFAULT OF STRETCH. Measured before this
   line existed: Tom's bio is two paragraphs and Sam's is one, so stretch made
   both cards 900px tall and left 269px of empty black inside Sam's border. A
   bordered box with a quarter of it visibly blank reads as a loading state or
   a bug. Letting each card end where its content ends is also the honest
   answer, since the apprentice genuinely has a shorter story than the owner.
   The two photo slots are a fixed ratio, so they still match each other
   exactly and the tops of both cards align; only the bottom edges differ.

   .wrap, NOT .wrap-narrow. At the narrow measure the cards were 323px wide
   and the bios ran at about 36 characters a line, which is card-blurb width
   being asked to carry 90 words of prose. .wrap gives ~500px cards and ~56
   characters. It also costs the page nothing in rhythm: this section now
   lines up with the hero above it and "How Tom works" below it, where
   .wrap-narrow would have introduced a third left edge. */
/* ==== ONE PERSON PER ROW, TOM ON TOP. 4 August 2026, Tom's call ====
   His instruction was "move Tom's bio back to the top and Sam below". The two
   were side-by-side columns, where neither is above the other, so what he is
   describing is this: a single column, Tom first, Sam under him.

   THE CARD TURNS ON ITS SIDE RATHER THAN JUST GETTING WIDER, and that is the
   whole reason this is more than a one-line change. A stacked card that kept
   the photo on top would put a 1:1 slot across the full .wrap: a portrait more
   than a thousand pixels tall, and you would scroll past Tom's face to reach
   the first word about him. Photo left at a fixed 240px, words right, keeps
   the picture subordinate to the writing, which is the same judgement the
   square crop was made on in the first place (see the note above).

   THE 240px COLUMN IS A max, NOT A fixed width: minmax(0,240px) lets it shrink
   with the page instead of forcing an overflow on the way down to the
   breakpoint. Below 720px the row becomes one column again and the photo goes
   full width, which is exactly what it already did and needed no change.

   .person IS NOW A GRID, so the bios are wrapped in .person-body in about.html
   rather than being loose children. Without that wrapper the role, the h3 and
   each paragraph would take a grid cell each and land in a column apiece. */
.people{
  display:grid;grid-template-columns:1fr;
  align-items:start;
  gap:clamp(1.2rem,2.5vw,1.8rem);margin-top:2.2rem;
}
.person{
  display:grid;
  grid-template-columns:minmax(0,240px) minmax(0,1fr);
  gap:clamp(1.1rem,3vw,2rem);
  align-items:start;
  background:var(--surface);border:1px solid var(--line);
  border-radius:var(--radius-lg);padding:clamp(1rem,2.2vw,1.4rem);
}
/* The grid gap owns the spacing between photo and words now, in both
   directions, so the photo's own bottom margin would double it up.
   TWO CLASSES DELIBERATELY: .person-photo sets margin-bottom:1.3rem further
   down this file, so a bare .person-photo override here would tie on
   specificity and lose on source order. */
.person .person-photo{margin-bottom:0}
@media (max-width:720px){
  .person{grid-template-columns:1fr}
}
/* Square, not the 4:5 a person photo naturally wants. At .wrap width a 4:5
   slot is 456x570 and the black box becomes the loudest thing on the page,
   above the two people it is standing in for. 1:1 is the standard team-card
   crop and keeps the slot subordinate to the writing. */
.person-photo{
  aspect-ratio:1/1;width:100%;
  background:#000;border:2px solid var(--amber);border-radius:var(--radius);
  display:flex;align-items:center;justify-content:center;
  padding:1rem;margin-bottom:1.3rem;
}
.person-photo p{
  margin:0;max-width:none;text-align:center;text-wrap:balance;
  font-family:'Chivo',sans-serif;font-weight:400;font-size:.72rem;
  letter-spacing:.2em;text-transform:uppercase;color:var(--steel);line-height:1.7;
}
/* FILLED, 4 AUGUST 2026, AND THE BORDER HAD TO CHANGE WITH IT.

   The rule this file already set is that the two treatments can never be
   confused: hairline steel is a photograph, amber is a hole where a photograph
   goes. Dropping a real portrait into the amber box would have broken exactly
   that rule and left the page claiming a gap it no longer has, so .is-real
   swaps the 2px amber for the same 1px steel every other photograph on the site
   carries, drops the padding the placeholder text needed, and clips the image
   to the box. The amber version stays untouched above for any slot still
   genuinely empty. */
.person-photo.is-real{
  border:1px solid var(--line);
  padding:0;
  overflow:hidden;
  background:var(--sunk);
  /* STAYS SQUARE ON A PHONE, and this one is load bearing. There is a
     `@media (max-width:720px){ .person-photo{aspect-ratio:3/2} }` further down
     which was right for an EMPTY box (a tall empty rectangle wastes a phone
     screen) and is wrong the moment the box holds a person: object-fit:cover
     would centre-crop a 1:1 portrait into 3:2 and take about 17% off the top,
     which is where both their heads are. Caught by measuring the rendered box
     at 390px, not by looking at the desktop and assuming.
     The .is-real selector outranks it, so this holds at every width. */
  aspect-ratio:1/1;
}
.person-photo.is-real img{
  width:100%;height:100%;object-fit:cover;display:block;
  transition:transform .6s var(--ease-out-quint);
}
@media (hover:hover) and (pointer:fine){
  .person:hover .person-photo.is-real img{transform:scale(1.03)}
}
.person-role{
  font-family:'Chivo',sans-serif;font-weight:400;font-size:.72rem;
  letter-spacing:.2em;text-transform:uppercase;color:var(--steel);
}
.person h3{margin:.35rem 0 0;font-size:clamp(1.3rem,2.4vw,1.6rem)}
.person p:not(.person-role){color:var(--ink-soft);font-size:.96rem}
.person h3 + p{margin-top:.9rem}
.person p + p{margin-top:.9rem}
/* One column early. Two 4:5 slots plus their stories side by side stop being
   two cards and start being two narrow strips well before the usual 640
   breakpoint: at 620 the boxes are 210px wide and Tom's bio is running at
   about 26 characters a line. */
@media (max-width:720px){
  .people{grid-template-columns:1fr;gap:1.2rem}
  .person-photo{aspect-ratio:3/2}
}

/* ==== PHOTOGRAPHY ====
   Added 2 August 2026. Tom sent through his own job photos and two of the van,
   which is why the "no photographs exist" note that used to sit in the hero
   block below is gone.

   One rule governs all of it. This is a #0B0B0C canvas and these are bright
   daylight photographs, so every image carries the same hairline var(--line)
   border. Without it a lit photo reads as a hole punched through the page
   rather than an object sitting on it. Nine bright squares on one page already
   ask for plenty of attention, so there is no coloured border, no drop shadow
   and no hover zoom anywhere in here. The photographs are the interest. */

/* .van-band was REMOVED on 3 Aug 2026. It showed vans-wide.webp as a band under
   the credentials strip; that frame is now the hero backdrop above, and running
   it twice on one page weakened both. Nothing else used the class. The frame
   still does the same job it always did, proving the plates read TA ELEC and
   TA ELEC2, just at hero scale. Recoverable from
   styles.css.pre-tom-revisions-2026-08-03.bak if it is ever wanted back. */

/* Both work vehicles, home page, directly under the credentials strip. It
   continues the same --sunk plinth the credentials sit on, because it is doing
   the same job: verifiable proof rather than decoration. No caption. The
   plates read TA ELEC and TA ELEC2, which is the entire point of the frame.

   REMOVED and then RESTORED on 3 August 2026. It came out when the same frame
   became the hero backdrop, and went back in a few hours later when Tom asked
   for the vehicle pictures to be on the page rather than behind the writing.
   No rule here changed in between; this is the original block. */
.van-band{background:var(--sunk);border-bottom:1px solid var(--line)}
.van-band .wrap{padding-block:clamp(1.6rem,3.5vw,2.6rem)}
.van-band img{
  width:100%;border:1px solid var(--line);border-radius:var(--radius);
  background:var(--sunk);
}

/* Three job photos under each service section. Below 640px three squares are
   too small to make out what is in them, so it drops to two columns and the
   third spans the full row rather than shrinking all three to thumbnails. */
.job-grid{
  display:grid;grid-template-columns:repeat(3,1fr);
  gap:clamp(.6rem,1.6vw,1rem);margin-top:2rem;
}
.job-grid img{
  width:100%;aspect-ratio:1;object-fit:cover;
  border:1px solid var(--line);border-radius:var(--radius);background:var(--sunk);
}
@media (max-width:640px){
  .job-grid{grid-template-columns:1fr 1fr}
  .job-grid>*:nth-child(3){grid-column:1/-1}
}

/* The van portrait on the About page, in an .about-grid beside Sam's bio.
   Capped because a 3:4 crop at full column width towers over the short
   paragraph beside it. */
.about-portrait{max-width:420px;justify-self:center}

/* The Q1 photo on the About page. Tom asked on 3 Aug 2026 for the paragraph
   first and the picture underneath it, which took it out of the .about-grid it
   used to share with his bio. Capped and centred because a 1:1 crop at the full
   .wrap-narrow measure is a 700px square, which swamps the short paragraphs
   above it. Same hairline border as every other photo on the site, no shadow,
   no hover, per the photography rule above. */
.q1-photo{margin:2.2rem auto 0;max-width:520px}
.q1-photo img{
  width:100%;height:auto;border:1px solid var(--line);
  border-radius:var(--radius-lg);background:var(--sunk);
}
.q1-photo figcaption{margin-top:.8rem;font-size:.86rem;color:var(--steel);text-align:center}

/* ==== LONG FORM, used by the safety page ==== */
.prose h2{margin-top:2.8rem;margin-bottom:.9rem}
.prose h3{margin-top:1.8rem;margin-bottom:.5rem}
.prose p{margin-bottom:1rem;color:var(--ink-soft)}
.prose ul{margin:0 0 1.2rem 1.1rem;color:var(--ink-soft)}
.prose li{margin-bottom:.45rem;padding-left:.3rem}
.prose strong{color:var(--ink);font-weight:600}
.callout{
  position:relative;background:var(--raised);border:1px solid var(--line);
  border-radius:var(--radius);padding:1.2rem 1.4rem;margin:1.6rem 0;
}
.callout::before{
  content:"";position:absolute;left:1.4rem;top:-2px;width:30px;height:4px;
  background:var(--steel);transform:skewX(calc(var(--shear) * -1));
}
.callout p{margin:0;font-size:.95rem}
.callout.is-key::before{background:var(--amber)}

/* ==== FORM ==== */
.contact-grid{display:grid;grid-template-columns:1.1fr .9fr;gap:clamp(2rem,5vw,4rem);align-items:start}
@media (max-width:900px){.contact-grid{grid-template-columns:1fr}}
.field{display:grid;gap:.4rem;margin-bottom:1.1rem}
.field label{font-size:.88rem;font-weight:500;color:var(--ink)}
.field input,.field textarea{
  width:100%;font-size:16px;                 /* 16px stops iOS zooming on focus */
  background:var(--sunk);border:1px solid var(--line-strong);border-radius:var(--radius);
  padding:.8rem .9rem;transition:border-color .15s var(--ease);
}
.field textarea{min-height:132px;resize:vertical}
.field input:focus,.field textarea:focus{border-color:var(--steel)}
.field input::placeholder,.field textarea::placeholder{color:#9A9A96}
.field .err{font-size:.85rem;color:#FFA39B;min-height:1.2em}
.field input[aria-invalid="true"],.field textarea[aria-invalid="true"]{border-color:#FFA39B}
.form-note{
  font-size:.88rem;color:var(--steel);margin-top:1rem;
  padding-top:1rem;border-top:1px solid var(--line);
}
.info-list{list-style:none;display:grid;gap:1.15rem}
.info-list li{display:grid;gap:.2rem;padding-bottom:1.15rem;border-bottom:1px solid var(--line)}
.info-list dt,.info-list .k{font-size:.76rem;letter-spacing:.16em;text-transform:uppercase;color:var(--steel);font-family:'Chivo',sans-serif}
.info-list .v{font-size:1.02rem}
/* The phone number and the email in this list measured 21px and 22px tall,
   just under the 24px target floor, and they are the two most important
   things to tap on the whole site. They are standalone values in a list, not
   links inside a sentence, so padding them costs nothing: the row already
   owns that vertical space via the li's own gap.

   NOT applied to the inline "Facebook" links in the review figcaptions,
   which measure 18px and are deliberately left alone. Those sit inside a
   running sentence ("Recommended on Facebook, 11 May"), where WCAG 2.5.8
   explicitly excepts inline targets, and padding them would overlap the line
   above. Expanding a target is right; breaking a paragraph's leading to do
   it is not. */
.info-list .v a{display:inline-block;padding-block:.15rem}

/* ==== PLACEHOLDER. Must be unmistakably a gap, never mistaken for a fact. ====
   The .tbc inline chip was REMOVED on 3 Aug 2026. Both of its two uses are gone:
   Tom supplied his years in the trade (About) and his opening hours (Contact),
   so there is no placeholder left on the site to style. The rule is preserved in
   styles.css.pre-tom-revisions-2026-08-03.bak if a gap ever needs one again.
   .preview-flag stays: it is still doing its job on the services page and on the
   unwired enquiry form. */
.preview-flag{
  background:rgba(242,174,63,.09);border:1px dashed rgba(242,174,63,.5);
  border-radius:var(--radius);padding:1rem 1.2rem;margin-top:1.6rem;
  font-size:.92rem;color:var(--ink-soft);
}
.preview-flag b{color:var(--amber);font-weight:600}

/* ==== CTA STRIP ==== */
.cta{background:var(--surface);border-top:1px solid var(--line);text-align:center}
.cta h2{margin-bottom:.8rem}
.cta p{margin-inline:auto;margin-bottom:1.8rem;color:var(--ink-soft)}
.cta .btn{margin-inline:auto}

/* ==== FOOTER ==== */
.site-footer{background:var(--sunk);border-top:1px solid var(--line);padding-block:3rem 2rem;font-size:.9rem}
.foot-grid{display:grid;grid-template-columns:1.3fr 1fr 1fr;gap:2.5rem}
.foot-grid img{width:120px;margin-bottom:1rem}
.foot-grid h3{font-size:.76rem;letter-spacing:.16em;text-transform:uppercase;color:var(--steel);margin-bottom:.9rem;font-weight:400}
/* THE FOOTER LINKS WERE 18px TALL. Measured in the 3 Aug 2026 critique: 11
   targets per page under the 24px minimum, and they are the ones that matter
   most, because the phone number and the email repeat down here and this is
   the block a thumb reaches for on a phone.

   The fix trades list gap for anchor padding so the hit area grows without
   the footer spreading out. THE LINE-HEIGHT IS PART OF THE FIX, not a tweak:
   padding alone took the link to 30px and the row pitch from 27px to 33px, so
   the footer visibly loosened. Dropping the inherited 1.65 body line-height to
   1.35 on these one-line links pulls the box back to ~26px, which still clears
   the 24px floor, and a .1rem gap lands the pitch back at ~27px. Measured
   before and after, not estimated. inline-block is required for the vertical
   padding to apply at all. */
.foot-grid ul{list-style:none;display:grid;gap:.1rem}
.foot-grid a{
  color:var(--ink-soft);text-decoration:none;
  display:inline-block;padding-block:.22rem;line-height:1.35;
}
.foot-grid a:hover{color:var(--ink)}
/* THE FOOTER PHONE NUMBER IS A CALL ACTION AND WAS RENDERING AS A NAV LINK.
   Added 5 Aug 2026. .phone-link{color:var(--amber)} is (0,1,0) and .foot-grid a
   is (0,1,1), so the amber lost and the number came out ink-soft. The same
   class renders correctly amber in the contact page's detail list, so one page
   was showing the number in two different colours. Amber on --sunk is 10.9:1.
   The :hover pair is restated because .foot-grid a:hover is (0,2,1) and would
   otherwise turn the number ink on hover; the layout half of .foot-grid a
   (the 44px tap target) is deliberately left to win. */
.foot-grid a.phone-link,
.foot-grid a.phone-link:hover{color:var(--amber)}
.foot-meta{
  margin-top:2.5rem;padding-top:1.4rem;border-top:1px solid var(--line);
  display:flex;flex-wrap:wrap;gap:.6rem 1.6rem;color:var(--steel);font-size:.84rem;
}
@media (max-width:760px){.foot-grid{grid-template-columns:1fr 1fr}.foot-grid>div:first-child{grid-column:1/-1}}

/* ==========================================================================
   DEPTH PASS, 3 AUGUST 2026. Components for safety.html and
   service-areas.html, which were the two thinnest pages on the site: one was
   a single 760px column of prose, the other was three short paragraphs.

   NOTHING NEW WAS INVENTED HERE. Every component below is built from the four
   things this site already had: the 16deg shear taken from the bolt through
   the T, the near-sharp 3px radius, the four dark surfaces, and amber as the
   single rationed accent. No new font, no new colour, no new radius.

   WHERE THE AMBER GOES, because rationing it is the whole discipline. On these
   two pages it means exactly three things and nothing else:
     1. a deadline that has a date on it,
     2. the thing that protects a person rather than equipment,
     3. Merrimac, the one place on the map that is his.
   If a fourth meaning appears here, the accent has stopped meaning anything.

   CONTRAST, computed rather than eyeballed, against the three surfaces these
   components sit on (--surface #121214, --raised #17171A, --sunk #080809).
   The worst case of each pair is quoted, which is always the lightest
   surface, --raised:
     --ink      on --raised  16.2:1
     --ink-soft on --raised  10.4:1
     --amber    on --raised   9.3:1
     --steel    on --raised   7.9:1
   All clear AA comfortably. Amber is never used as small text on anything
   lighter than these, and never as a fill behind light text.
   ========================================================================== */

/* ==== THE DEADLINE BAND ====
   The single most valuable thing on the safety page: Queensland's smoke alarm
   standard reaches every home on 1 January 2027, and nobody in this category
   publishes a countdown to it.

   THE COUNTER IS PROGRESSIVE ENHANCEMENT, AND THAT IS DELIBERATE. The number
   ships hidden and JS reveals it. With no JS the visitor still gets the date
   and the rule, which are true forever; what they never get is a stale number
   baked into the HTML, which on a site whose entire pitch is that nothing is
   invented would be the one dishonest element on it. The same reasoning
   governs the other end: once the date passes, site.js simply leaves the
   counter hidden rather than counting up, and the static sentence underneath
   goes on reading correctly with no edit. */
.deadline{background:var(--sunk);border-block:1px solid var(--line);position:relative;overflow:hidden}
/* the hero's amber shear, mirrored to the left so the two do not read as the
   same element repeated down the page */
.deadline::after{
  content:"";position:absolute;left:-16%;top:-40%;width:46%;height:180%;
  background:linear-gradient(180deg,rgba(242,174,63,.07),rgba(242,174,63,0) 72%);
  transform:skewX(calc(var(--shear) * -1));pointer-events:none;
}
.deadline-in{
  position:relative;z-index:1;
  display:grid;grid-template-columns:auto 1fr;
  gap:clamp(1.4rem,4vw,3.4rem);align-items:center;
}
.deadline-count{
  display:grid;justify-items:start;
  padding-right:clamp(1.4rem,4vw,3.4rem);border-right:1px solid var(--line-strong);
}
.deadline-num{
  font-family:'Chivo',sans-serif;font-weight:900;
  font-size:clamp(3.6rem,11vw,6.8rem);line-height:.86;letter-spacing:-.045em;
  color:var(--amber);font-variant-numeric:tabular-nums;
}
.deadline-unit{
  font-family:'Chivo',sans-serif;font-size:.76rem;letter-spacing:.2em;
  text-transform:uppercase;color:var(--steel);margin-top:.7rem;
}
.deadline-body h2{margin-bottom:.7rem}
.deadline-body p{color:var(--ink-soft);max-width:52ch}
.deadline-body p+p{margin-top:.8rem}
@media (max-width:820px){
  .deadline-in{grid-template-columns:1fr;gap:1.6rem}
  /* the rule moves from the side to underneath, so the number still reads as
     a separate object rather than as a headline above the paragraph */
  .deadline-count{
    padding-right:0;border-right:0;
    padding-bottom:1.4rem;border-bottom:1px solid var(--line-strong);
  }
}

/* ==== TWO PANEL CONTRAST. Safety switch against circuit breaker. ====
   This distinction is the most misunderstood thing in domestic electrical
   work and it spent the last week as one subordinate clause in a paragraph.
   Amber marks the one that protects a person; steel marks the one that
   protects equipment. That is the accent carrying meaning rather than
   decorating a card. */
.contrast{
  display:grid;grid-template-columns:1fr 1fr;
  gap:clamp(1rem,2.4vw,1.6rem);margin-top:2.2rem;
}
.contrast-panel{
  position:relative;background:var(--raised);border:1px solid var(--line);
  border-radius:var(--radius);padding:clamp(1.3rem,2.6vw,1.9rem);
}
.contrast-panel::before{
  content:"";position:absolute;left:clamp(1.3rem,2.6vw,1.9rem);top:-2px;
  width:34px;height:4px;background:var(--steel);
  transform:skewX(calc(var(--shear) * -1));
}
.contrast-panel.is-you::before{background:var(--amber)}
.contrast-role{
  font-family:'Chivo',sans-serif;font-size:.72rem;letter-spacing:.2em;
  text-transform:uppercase;color:var(--steel);margin-bottom:.5rem;
}
.contrast-panel.is-you .contrast-role{color:var(--amber)}
.contrast-panel h3{margin-bottom:.7rem;font-size:clamp(1.15rem,2vw,1.35rem)}
/* :not(.contrast-role) IS THE FIX, NOT A FLOURISH. Added 5 Aug 2026.
   The role IS a <p>, so this rule matched it, and at (0,1,1) against the
   .contrast-role rule's (0,1,0) it won both declarations: the 11.52px label
   rendered at 15.52px, and steel became ink-soft. That broke the panel's whole
   premise, which is that amber marks what protects a person and steel marks
   what protects equipment: "Protects the wiring" was rendering in neither.
   Same shape as .person p:not(.person-role) further down this file. */
.contrast-panel p:not(.contrast-role){color:var(--ink-soft);font-size:.97rem}
.contrast-panel p+p{margin-top:.7rem}
@media (max-width:720px){.contrast{grid-template-columns:1fr}}

/* ==== THE 90. Asymmetric split: one enormous numeral, two triggers. ====
   Both deadlines in the Regulation are ninety days, and the number doing the
   work at display scale is the correction: plenty of Queensland trade sites
   publish three months for a sale and six for a rental, which is wrong in
   both halves and matters when you are counting from a settlement date. */
.ninety{
  display:grid;grid-template-columns:auto 1fr;
  gap:clamp(1.5rem,4vw,3.4rem);align-items:start;margin-top:2.2rem;
}
.ninety-num{
  font-family:'Chivo',sans-serif;font-weight:900;
  font-size:clamp(4.6rem,14vw,8.5rem);line-height:.82;letter-spacing:-.05em;
  color:var(--amber);font-variant-numeric:tabular-nums;
}
.ninety-label{
  display:block;font-family:'Chivo',sans-serif;font-weight:400;
  font-size:.74rem;letter-spacing:.2em;text-transform:uppercase;
  color:var(--steel);margin-top:.8rem;
}
.ninety-list{list-style:none;display:grid;gap:0}
.ninety-list li{
  display:grid;gap:.3rem;
  padding-block:clamp(.9rem,2vw,1.15rem);border-top:1px solid var(--line);
}
.ninety-list li:first-child{border-top:0;padding-top:0}
.ninety-when{
  font-family:'Chivo',sans-serif;font-size:.74rem;letter-spacing:.16em;
  text-transform:uppercase;color:var(--steel);
}
.ninety-what{font-family:'Chivo',sans-serif;font-weight:700;font-size:1.05rem}
.ninety-list p{font-size:.93rem;color:var(--ink-soft);margin-top:.15rem}
@media (max-width:640px){
  .ninety{grid-template-columns:1fr;gap:1.4rem}
}

/* ==== PHASE IN. Horizontal timeline, smoke alarm law. ====
   Three dates from the Fire Services Act 1990, and the reason the third one
   matters: it is the first that needs no sale, no new lease and no
   renovation to bite. Amber marks 2027 and stays on it after the date
   passes, because it is still the rule that governs every home. */
.phase{
  display:grid;grid-template-columns:repeat(3,1fr);
  gap:clamp(1rem,2.4vw,2rem);margin-top:2.4rem;
  border-top:1px solid var(--line-strong);
}
.phase-step{position:relative;padding-top:1.4rem}
.phase-step::before{
  content:"";position:absolute;top:-2px;left:0;width:34px;height:4px;
  background:var(--steel);transform:skewX(calc(var(--shear) * -1));
}
.phase-step.is-key::before{background:var(--amber)}
.phase-year{
  font-family:'Chivo',sans-serif;font-weight:900;
  font-size:clamp(1.3rem,2.7vw,1.8rem);letter-spacing:-.02em;
  font-variant-numeric:tabular-nums;
}
.phase-step.is-key .phase-year{color:var(--amber)}
/* :not(.phase-year), same repair as .contrast-panel p above. The year is a <p>,
   so this rule was overriding it at (0,1,1) and the three dates rendered at
   14.88px, the same size as their own explanation, with an unwanted .5rem
   pushing them off the shear marker. A timeline whose years do not outrank
   their captions has no hierarchy left to read. Restored to the authored
   clamp(1.3rem,2.7vw,1.8rem): 20.8px on a phone, 28.8px at 1440. */
.phase-step p:not(.phase-year){font-size:.93rem;color:var(--ink-soft);margin-top:.5rem}
@media (max-width:700px){
  .phase{grid-template-columns:1fr;gap:0}
  .phase-step{padding-block:1.3rem;border-bottom:1px solid var(--line)}
  .phase-step:last-child{border-bottom:0;padding-bottom:0}
  .phase-step::before{top:-2px}
  .phase-step+.phase-step::before{top:-2px}
}

/* ==== THE ALARM PLAN. A drawn cross section, not a bullet list. ====
   WHY THIS IS HAND DRAWN SVG AND NOT A PHOTOGRAPH OR AN ICON SET. The rule
   is spatial: every bedroom, every storey, and the hallways between the
   bedrooms and the rest of the house, all wired to each other. Three bullets
   describe that. A drawing shows it, and shows in particular the one part
   people miss, which is that the link crosses between floors. No photograph
   of Tom's could show a whole house in section, and no icon library ships a
   two storey plan, so this is a genuine information graphic rather than
   decoration. It carries no invented facts: it is a generic house, labelled
   as an example, and every alarm on it is one the standard actually asks for.

   The living room deliberately has NO alarm on it. That is not an oversight,
   it is the teaching point: the diagram shows what is required and stops. */
.alarm-plan{
  margin-top:2.4rem;background:var(--surface);border:1px solid var(--line);
  border-radius:var(--radius-lg);padding:clamp(1.1rem,2.6vw,1.9rem);
}
.alarm-plan svg{width:100%;height:auto;display:block;max-width:660px;margin-inline:auto}
.alarm-key{
  display:flex;flex-wrap:wrap;gap:.8rem 2rem;
  margin-top:1.4rem;padding-top:1.2rem;border-top:1px solid var(--line);
  list-style:none;font-size:.88rem;color:var(--ink-soft);
}
.alarm-key li{display:flex;align-items:center;gap:.6rem}
.alarm-key .k-dot{
  width:11px;height:11px;flex:none;border-radius:50%;background:var(--amber);
}
.alarm-key .k-line{
  width:22px;height:0;flex:none;
  border-top:2px dashed var(--amber);
}
.alarm-caption{margin-top:1rem;font-size:.84rem;color:var(--steel);max-width:60ch}

/* ==== YOUR SITUATION. Native disclosure, no JS, no library. ====
   Four situations, because which rules bite depends entirely on whether you
   own the place, are buying it, are selling it or are letting it. <details>
   is used rather than a scripted accordion so it keeps working with JS off,
   is keyboard operable for free, and adds nothing to the page weight. */
.situations{margin-top:2.2rem;border-top:1px solid var(--line-strong)}
.situations details{border-bottom:1px solid var(--line)}
.situations summary{
  display:flex;align-items:center;gap:1rem;
  padding-block:1.2rem;min-height:52px;cursor:pointer;
  font-family:'Chivo',sans-serif;font-weight:700;
  font-size:clamp(1.02rem,1.9vw,1.18rem);letter-spacing:-.01em;
  list-style:none;touch-action:manipulation;-webkit-tap-highlight-color:transparent;
}
/* both are needed: the pseudo element covers WebKit, list-style covers the rest */
.situations summary::-webkit-details-marker{display:none}
.situations summary::marker{content:""}
.situations summary:hover{color:var(--amber)}
.situations .chev{
  margin-left:auto;flex:none;width:13px;height:13px;
  border-right:2px solid var(--amber);border-bottom:2px solid var(--amber);
  transform:translateY(-3px) rotate(45deg);transform-origin:center;
  transition:transform .2s var(--ease);
}
.situations details[open] .chev{transform:translateY(2px) rotate(225deg)}
.situations .sit-body{padding-bottom:1.4rem;max-width:64ch}
.situations .sit-body p{color:var(--ink-soft);font-size:.97rem}
.situations .sit-body p+p{margin-top:.7rem}
.situations .sit-body strong{color:var(--ink);font-weight:600}

/* ==== THE LAW THIS COMES FROM. ====
   The page's actual differentiator, and until now it was invisible: every
   rule above was read against the Regulation and the Act rather than against
   another electrician's blog, and a visitor had no way of knowing that. Two
   instruments get a panel each because they are the two doing the work; the
   standards sit underneath on one line because they are referenced, not
   argued from. Deliberately not three equal cards. */
.sources{
  display:grid;grid-template-columns:1fr 1fr;
  gap:clamp(1rem,2.4vw,1.6rem);margin-top:2.2rem;
}
.source{
  background:var(--surface);border:1px solid var(--line);
  border-radius:var(--radius);padding:clamp(1.2rem,2.4vw,1.7rem);
}
.source-kind{
  font-family:'Chivo',sans-serif;font-size:.7rem;letter-spacing:.2em;
  text-transform:uppercase;color:var(--steel);margin-bottom:.6rem;
}
.source h3{font-size:clamp(1.02rem,1.9vw,1.15rem)}
.source ul{list-style:none;display:grid;gap:.75rem;margin-top:1.1rem}
.source li{
  display:grid;grid-template-columns:auto 1fr;gap:.85rem;
  font-size:.91rem;color:var(--ink-soft);line-height:1.5;
}
.source li b{
  font-family:'Chivo',sans-serif;font-weight:700;color:var(--amber);
  font-variant-numeric:tabular-nums;white-space:nowrap;
}
.standards{
  margin-top:clamp(1rem,2.4vw,1.6rem);
  background:var(--sunk);border:1px solid var(--line);border-radius:var(--radius);
  padding:clamp(1rem,2.2vw,1.3rem) clamp(1.2rem,2.4vw,1.7rem);
  display:flex;flex-wrap:wrap;gap:.5rem 2rem;align-items:baseline;
}
.standards dt{
  font-family:'Chivo',sans-serif;font-size:.7rem;letter-spacing:.2em;
  text-transform:uppercase;color:var(--steel);
}
.standards dd{font-size:.91rem;color:var(--ink-soft)}
.standards dd b{color:var(--ink);font-weight:600}
@media (max-width:760px){.sources{grid-template-columns:1fr}}

/* ==========================================================================
   SERVICE AREAS. The page had no image of any kind and three paragraphs.
   ========================================================================== */

/* ==== THE MAP ====
   WHY IT IS DRAWN AND NOT A MAP TILE. A tile from any provider is a third
   party request on a site that currently makes none, needs a key, and would
   drag its own light grey cartography onto a #0B0B0C page. Drawn, it costs
   nothing to send, matches the page, and stays inside the honesty rule that
   governs this whole build.

   AND THE HONESTY RULE IS WHY IT NAMES ONLY TWO PLACES. Tom told us on 3
   August that he does not work off a suburb list. So the map carries the two
   places he named himself, Merrimac and Yatala, plus geography that belongs
   to nobody: the coast, the M1, and the state border. It is not a coverage
   polygon and does not pretend to be one, which is also why it is captioned
   as schematic rather than drawn to scale.

   THE BORDER IS ON IT ON PURPOSE. He also said he travels as far south as the
   Tweed Coast, which is New South Wales, and a Queensland contractor licence
   does not by itself authorise work there. Drawing the border makes the page
   stop where the published claim stops, visibly, instead of going quiet. */
.coverage{
  display:grid;grid-template-columns:minmax(0,.92fr) minmax(0,1fr);
  gap:clamp(1.8rem,4.5vw,3.4rem);align-items:center;margin-top:2.4rem;
}
.map-frame{
  background:var(--sunk);border:1px solid var(--line);
  border-radius:var(--radius-lg);padding:clamp(.9rem,2vw,1.3rem);
}
.map-frame svg{width:100%;height:auto;display:block}
.map-note{
  margin-top:.9rem;padding-top:.9rem;border-top:1px solid var(--line);
  font-size:.82rem;color:var(--steel);
}
.reach{list-style:none;display:grid;gap:0;border-top:1px solid var(--line-strong)}
.reach li{
  display:grid;gap:.25rem;
  padding-block:clamp(1.1rem,2.4vw,1.4rem);border-bottom:1px solid var(--line);
}
.reach .k{
  font-family:'Chivo',sans-serif;font-size:.74rem;letter-spacing:.16em;
  text-transform:uppercase;color:var(--steel);
}
.reach .v{
  font-family:'Chivo',sans-serif;font-weight:900;letter-spacing:-.02em;
  font-size:clamp(1.15rem,2.4vw,1.5rem);
}
.reach li.is-base .v{color:var(--amber)}
.reach p{font-size:.93rem;color:var(--ink-soft);margin-top:.2rem}
@media (max-width:880px){
  .coverage{grid-template-columns:1fr;gap:2rem}
  .map-frame{max-width:420px;margin-inline:auto}
}

/* ==== HOUSING ERAS. Vertical timeline with the shear as its node. ====
   The strongest genuinely local thing this page can say, and it was one
   subordinate clause: the age of the street tells you what is likely behind
   the switchboard before anyone opens it. It also gives the two pages a real
   reason to link to each other, because the pre 1992 case is exactly the
   safety switch case. */
/* FULL WIDTH ROWS, and both the things this is NOT are deliberate.

   It is NOT the sticky two-column version that shipped earlier on 3 Aug 2026.
   That pinned the heading while the timeline scrolled past it, which Will
   flagged: a heading that refuses to leave reads as stuck rather than as
   helpful, and it is a heavy device for three short items.

   It is also NOT a narrow ribbon down the left of a 1120px section, which is
   what the vertical spine underneath it was, and which left half the width
   empty. Each era now takes the full measure: the period in the left column,
   the finding and its explanation in the right, hairlines between. Nothing is
   pinned, nothing is empty, and the three periods still read top to bottom as
   the chronology they are. */
.eras{margin-top:2.4rem;border-top:1px solid var(--line-strong)}
.era{
  display:grid;grid-template-columns:minmax(180px,.28fr) 1fr;
  gap:clamp(.6rem,3.5vw,3rem);
  padding-block:clamp(1.5rem,3.2vw,2.3rem);
  border-bottom:1px solid var(--line);
  align-items:start;
}
.era-when{
  font-family:'Chivo',sans-serif;font-weight:900;letter-spacing:-.02em;
  font-size:clamp(1.12rem,2.2vw,1.45rem);font-variant-numeric:tabular-nums;
  line-height:1.16;
  /* HANGING INDENT, because the period can be two lines and one of them is.
     Added 5 Aug 2026 alongside the :not(.era-when) repair below. Restoring the
     authored 23.2px at 1440 makes "Mid 1992 onward" too wide for the
     minmax(180px,.28fr) column, and the node below is an inline-block that only
     occupies the FIRST line, so the wrapped word fell back to the container
     edge and sat 25px left of the line above it.
     The two values are the node's own width and margin, so they cannot drift
     from it: first line starts at padding+indent = 0 and still leads with the
     node, every later line starts at the padding edge, which is exactly where
     the first line's text begins. Widening the column instead would have taken
     the space off the finding, which is the part people read. */
  padding-left:calc(15px + .65rem);
  text-indent:calc((15px + .65rem) * -1);
}
/* The node is inline-block rather than a flex child on purpose: a 10px box has
   no text baseline, so baseline alignment against the year put it visibly off,
   and vertical-align:middle on an inline-block is the one that behaves. */
.era-when::before{
  content:"";display:inline-block;vertical-align:middle;
  width:15px;height:10px;margin-right:.65rem;
  background:var(--steel);transform:skewX(calc(var(--shear) * -1));
}
.era.is-key .era-when{color:var(--amber)}
.era.is-key .era-when::before{background:var(--amber)}
.era h3{color:var(--ink);font-weight:700;font-size:clamp(1.02rem,1.9vw,1.2rem)}
/* :not(.era-when), the third instance of the same override on this site.
   The period is a <p>, so this rule beat .era-when at (0,1,1) and every era
   rendered at 15.36px, SMALLER than the 19.2px h3 beside it, which inverts the
   row: the period is meant to anchor the row and was reading as its footnote.
   The stray margin-top also dropped it ~11px below the h3 it should line up
   with, defeating the align-items:start on .era. */
.era p:not(.era-when){margin-top:.7rem;color:var(--ink-soft);font-size:.96rem}
@media (max-width:760px){
  /* stacked, the year becomes a label above its own row rather than a column */
  .era{grid-template-columns:1fr;gap:.7rem}
}

/* ==== THE CREW BAND. One photograph, on a page that had none. ==== */
.crew{
  display:grid;grid-template-columns:1.15fr .85fr;
  gap:clamp(1.6rem,4.5vw,3.4rem);align-items:center;
}
.crew figure{margin:0}
.crew img{
  width:100%;height:auto;border:1px solid var(--line);
  border-radius:var(--radius-lg);background:var(--sunk);
}
.crew figcaption{margin-top:.8rem;font-size:.84rem;color:var(--steel);max-width:46ch}
.crew h2{margin-bottom:.9rem}
@media (max-width:860px){.crew{grid-template-columns:1fr}}

/* ==== STICKY CALL BAR. Mobile only. The highest value element on the site. ==== */
/* Fail safe on purpose: the bar is SHOWN by default and JS hides it only
   while the hero call button is on screen. If the observer never runs, the
   visitor still gets a phone number, which is the whole point of the site. */
.callbar{
  position:fixed;left:0;right:0;bottom:0;z-index:120;display:none;
  padding:.6rem clamp(12px,4vw,20px) calc(.6rem + env(safe-area-inset-bottom));
  background:rgba(11,11,12,.94);backdrop-filter:blur(12px);
  border-top:1px solid var(--line);
  transform:translateY(0);transition:transform .28s var(--ease);
}
.callbar.is-hidden{transform:translateY(110%)}
.callbar .btn{width:100%}
@media (max-width:900px){
  .callbar{display:block}
  body{padding-bottom:84px}
}

/* ==========================================================================
   THE CURRENT LAYER. Added 4 August 2026, the morning of the walkthrough.

   WHAT THIS IS AND WHY IT IS NOT DECORATION. Everything above this line was
   built to be read. This block is built to be watched, because the site is now
   shown on a screen share to the man whose business it is before it is ever
   shown to a customer. Two different jobs, and the second one needs motion the
   first one never asked for.

   THE ONE RULE THAT KEEPS IT HONEST: every animation here is either the shear
   (the bolt through the T, --shear, already the site's only geometry) or it is
   electricity behaving like electricity. Nothing bounces, nothing floats,
   nothing slides in from the left because slides look nice. If an effect could
   sit unchanged on a dentist's website it does not belong on a sparky's.

   THE MOVES, in order of how much they carry:
     1. .interconnect  a signal travels the dashed line between smoke alarms.
                       The law is about alarms that TALK to each other, and the
                       diagram now shows them doing it.
     2. [data-draw]    the two hand drawn diagrams draw themselves in.
     3. [data-tilt]    cards take a small perspective tilt under the cursor
                       with an amber spotlight, the current under the surface.
     4. [data-magnet]  the call buttons lean toward the cursor. It is the
                       highest value element on the site, so it is the one that
                       reaches back.
     5. [data-reveal]  sections arrive rather than appear.

   HOW IT CANNOT BREAK THE PAGE. Every reveal is an enhancement on top of an
   already visible default: the hidden state lives under html.js-motion, which
   JavaScript adds as its first act and a 2.4s failsafe removes if anything
   throws. No JS, dead JS, a headless renderer or a background tab, and the
   page is exactly what it was yesterday, fully readable. That is the single
   most important line in this block.
   ========================================================================== */

:root{
  /* one exponential ease family for everything that moves. No bounce. */
  --ease-out-quint:cubic-bezier(.22,1,.36,1);
  --ease-out-expo:cubic-bezier(.16,1,.3,1);
}

/* ==== 1. REVEAL ====
   The default is VISIBLE. .js-motion is what arms the hidden state, and only
   JavaScript can set it, so the failure mode of every path through this code
   is a fully readable page. */
/* REVEALS MOVE ON `translate`, NOT ON `transform`, AND THAT IS LOAD BEARING.
   Several of the things that arrive are also things that tilt or lean under the
   cursor, and those use `transform`. Written both ways round, one silently
   cancels the other: the reveal rule is the more specific of the two, so its
   `transform:none` would win after arrival and the tilt would be dead on
   exactly the cards it was written for. `translate` is an independent property
   that composes with `transform` instead of replacing it, so the two systems
   never touch. Found by reading the cascade, not by watching it fail. */
html.js-motion [data-reveal]{
  opacity:0;
  translate:0 14px;
}
html.js-motion [data-reveal].is-in{
  opacity:1;translate:0 0;
  transition:opacity .62s var(--ease-out-quint),translate .62s var(--ease-out-quint);
  transition-delay:var(--reveal-delay,0ms);
}
/* Children stagger inside one parent rather than every section firing the same
   single fade, which is the uniform reflex this is meant to avoid. */
html.js-motion [data-reveal-group] > *{
  opacity:0;translate:0 16px;
}
html.js-motion [data-reveal-group].is-in > *{
  opacity:1;translate:0 0;
  transition:opacity .58s var(--ease-out-quint),translate .58s var(--ease-out-quint);
}
html.js-motion [data-reveal-group].is-in > *:nth-child(1){transition-delay:0ms}
html.js-motion [data-reveal-group].is-in > *:nth-child(2){transition-delay:70ms}
html.js-motion [data-reveal-group].is-in > *:nth-child(3){transition-delay:140ms}
html.js-motion [data-reveal-group].is-in > *:nth-child(4){transition-delay:210ms}
html.js-motion [data-reveal-group].is-in > *:nth-child(5){transition-delay:280ms}
html.js-motion [data-reveal-group].is-in > *:nth-child(n+6){transition-delay:350ms}

/* THE HERO IS TIMED, NOT OBSERVED. Its parts are above the fold on every page,
   so an IntersectionObserver would fire for all of them in the same frame and
   the stagger would be invented rather than sequenced. A plain keyframe with
   delays is both simpler and the only way to control the order. */
html.js-motion .hero-logo{animation:current-rise .7s var(--ease-out-expo) both}
html.js-motion .hero h1{animation:current-rise .8s var(--ease-out-expo) .06s both}
html.js-motion .hero-sub{animation:current-rise .8s var(--ease-out-expo) .14s both}
html.js-motion .hero-actions{animation:current-rise .8s var(--ease-out-expo) .22s both}
@keyframes current-rise{
  from{opacity:0;transform:translate3d(0,18px,0)}
  to{opacity:1;transform:none}
}

/* ==== 2. THE AMBER SHEAR SWEEP ON BUTTONS ====
   The existing .btn::before is the sheared leading edge and it stays exactly as
   it was. This adds a second pseudo element that runs the same angle across the
   face on hover, so the button reads as a contact closing rather than as a
   colour change. Clipped to the button so it cannot leak over the focus ring. */
/* ==========================================================================
   THE BUTTON IS ONE SHAPE NOW. Rebuilt 4 August 2026, Will's call.

   THE DEFECT HE SPOTTED, stated exactly. The filled button was TWO shapes
   pretending to be one: a rectangle at border-radius 3px, plus a separate
   14px pseudo element parked at left:-7px, skewed 16 degrees, at border-radius
   1px. Two different radii, two different outlines, butted against each other
   down a vertical seam. At the top-left and bottom-left corners the two shapes
   do not meet cleanly and leave a visible notch, so the control reads as a
   badge stuck onto a box rather than as one object. It was on the hero, the
   header, every CTA strip and the mobile call bar, which is to say everywhere
   the site asks for the phone call.

   WHY IT IS NOT A GENERIC PREMIUM BUTTON. Reviewed the 21st.dev catalog for a
   drop-in replacement (Anti Metal Button, kokonutd's Button Shiny, Great UI
   Minimal Buttons). Every good one is a pill or a rounded rect, and swapping to
   one would have deleted the shear, which is not decoration on this site: it is
   the bolt through the T from Tom's own logo, and it is the same geometry as
   the section chips, the credential bullets and the nav marker. Replacing his
   one motif to fix a seam is a worse outcome than the seam.

   So what came across from those components is their CONSTRUCTION, which is the
   part that actually reads as expensive, and every one of them obeys the same
   rule: one continuous shape, never two glued together. Applied here as:

     1. ONE clipped layer. The whole face, shear included, is a single polygon
        on ::before. There is no seam because there is no second shape.
     2. The clip lives on ::before, NOT on .btn, and that is deliberate:
        clip-path clips an element's outline too, so clipping the button itself
        would cut the focus ring off. The button box stays unclipped and keeps
        a clean rectangular ring, which is what the original code wanted and
        was using two shapes to get.
     3. drop-shadow, not box-shadow. A box-shadow on a clipped element is
        clipped away with it; a filter drop-shadow follows the real silhouette,
        so the shadow is sheared too.
     4. A bevel: one hairline of light along the top edge, which is what stops
        a flat colour fill reading as flat.
   ========================================================================== */
/* THE RUN IS DERIVED FROM --shear, NOT TYPED IN, and the first attempt at this
   is why. Hardcoding "15px" looked right and measured 15.07 degrees against a
   motif that is 16 everywhere else on the site, because the button renders 54px
   tall rather than the 52px min-height (padding plus a 1.65 line-height), and a
   fixed run against a different height is a different angle. One pixel, and
   invisible, but the whole discipline here is one angle everywhere, measured.

   tan() reads the same --shear token as the section chips, the credential
   bullets and the nav marker, so the button's angle can never drift from them:
   change --shear once and all five follow. The plain value on the line above is
   the fallback for an engine without CSS trig functions, which drops the calc
   as invalid and keeps the literal.

   --btn-h is the REAL rendered height, not min-height. If the padding or the
   type scale ever changes, remeasure it; that is the one number here that has
   to be observed rather than reasoned. */
.btn{
  --btn-h:54px;
  --btn-run:15.5px;
  --btn-run:calc(var(--btn-h) * tan(var(--shear)));
  background-color:transparent;         /* the face is painted by ::before */
  background-image:none;
  border-radius:0;                      /* the polygon owns the shape now */
  padding-left:calc(1.6rem + var(--btn-run) / 2);   /* optical centring against the slant */
  transition:transform .18s var(--ease-out-quint),filter .22s var(--ease);
  /* THE BUTTON MUST OWN A STACKING CONTEXT, AND THIS IS LOAD BEARING.
     Fixed 5 Aug 2026. The face lives on `.btn::before` at z-index:-1 (below),
     and .btn is position:relative with z-index:auto, which is NOT a stacking
     context. A negative-z-index pseudo therefore joins the nearest ancestor
     context (the root) and paints at step 3 of the painting order, BELOW every
     block-level descendant background at step 4. Any section that paints its
     own background paints straight over the amber, so the button renders as
     ink-on-ink and disappears.

     It was disappearing. Every filled CTA on all seven pages measured
     rgb(18,18,20) (--surface) at 390px, and rgb(8,8,9) (--sunk) on contact.
     Desktop only escaped it by accident: the reveal layer leaves `translate:0px`
     on the revealed <section>, which happens to make a stacking context and
     trap the pseudo inside it. That is a coincidence, not a mechanism, and the
     mobile pass legitimately cleared `translate` on phones, so the accident
     stopped happening and the buttons went dark.

     `isolation:isolate` makes the button its own stacking context with no
     layout, paint or geometry side effects: the pseudo is now trapped inside
     .btn at every width, on every page, whatever ancestors do. It does not clip
     (only overflow/clip-path clip), so ::before's drop-shadow still overhangs
     the box and the unclipped focus ring is untouched. */
  isolation:isolate;
}
.btn-lg{
  --btn-h:63px;
  --btn-run:18px;
  --btn-run:calc(var(--btn-h) * tan(var(--shear)));
  padding-left:calc(2rem + var(--btn-run) / 2);
}

.btn::before{
  content:"";position:absolute;inset:0;z-index:-1;
  left:0;top:0;right:0;bottom:0;width:auto;   /* unset the old 14px wedge geometry */
  transform:none;border-radius:0;
  background-color:var(--btn-bg);
  background-image:
    /* the sweep, which needs no clipping because a background layer is painted
       inside the box by definition */
    linear-gradient(100deg,rgba(255,255,255,0) 38%,rgba(255,255,255,.42) 50%,rgba(255,255,255,0) 62%),
    /* the bevel: light from above, one hairline, not a gradient wash */
    linear-gradient(180deg,rgba(255,255,255,.22),rgba(255,255,255,0) 3px);
  background-repeat:no-repeat,no-repeat;
  background-size:260% 100%,100% 100%;
  background-position:150% 0,0 0;
  clip-path:polygon(var(--btn-run) 0,100% 0,100% 100%,0 100%);
  filter:drop-shadow(0 1px 1px rgba(0,0,0,.5)) drop-shadow(0 8px 18px rgba(0,0,0,.35));
  transition:background-color .16s var(--ease),filter .22s var(--ease);
}
@media (hover:hover) and (pointer:fine){
  .btn:hover::before,.btn:focus-visible::before{
    animation:btn-sweep .68s var(--ease-out-quint);
  }
  /* the glow is on the shadow, so it follows the sheared silhouette rather than
     drawing a rectangular halo around a slanted object */
  .btn:hover::before{
    filter:drop-shadow(0 1px 1px rgba(0,0,0,.5)) drop-shadow(0 8px 22px rgba(242,174,63,.42));
  }
}
@keyframes btn-sweep{
  from{background-position:150% 0,0 0}
  to{background-position:-90% 0,0 0}
}
.btn:hover::before{background-color:#FFC463}
.btn:hover{background-color:transparent}   /* the old rule painted the box, not the face */

/* THE GHOST IS UNCHANGED AND WAS NEVER BROKEN. It carries no shear at all
   (.btn-ghost::before is display:none, and has been since the build), so it is
   a plain bordered rectangle with nothing to seam. It keeps its own radius, its
   own border and its own steel sweep on the element itself. Leaving it alone is
   the point: only the filled button had two shapes. */
.btn-ghost{
  border-radius:var(--radius);
  padding-left:1.6rem;
  background-image:linear-gradient(100deg,
    rgba(172,172,172,0) 38%,rgba(172,172,172,.20) 50%,rgba(172,172,172,0) 62%);
  background-repeat:no-repeat;background-size:260% 100%;background-position:150% 0;
}
.btn-ghost.btn-lg{padding-left:2rem}
@media (hover:hover) and (pointer:fine){
  .btn-ghost:hover,.btn-ghost:focus-visible{animation:btn-sweep-ghost .68s var(--ease-out-quint)}
}
@keyframes btn-sweep-ghost{
  from{background-position:150% 0}
  to{background-position:-90% 0}
}
.btn-ghost:hover{box-shadow:none;background-color:rgba(172,172,172,.09)}

/* ==== 3. MAGNETIC CALL BUTTONS ====
   Nested, which is the detail that makes it feel machined rather than rubbery:
   the button leans, and the label inside leans a little further, so the two
   separate by a pixel or two under the cursor and close back up on exit.
   Pointer driven only, never on touch, never under reduced motion. */
[data-magnet]{
  transform:translate3d(var(--mag-x,0px),var(--mag-y,0px),0);
  transition:transform .5s var(--ease-out-quint);
}
[data-magnet].is-pulling{transition:transform .1s linear}
/* The press feedback moves to the independent `scale` property for the same
   reason the reveals moved to `translate`. The original .btn:active is
   `transform:scale(.985)` at higher specificity than [data-magnet], so a
   magnetised button would snap back to dead centre on mousedown, which is the
   one frame a person is actually looking at it. */
.btn[data-magnet]:active{transform:translate3d(var(--mag-x,0px),var(--mag-y,0px),0);scale:.985}
[data-magnet-label]{
  display:inline-block;
  transform:translate3d(var(--mag-x,0px),var(--mag-y,0px),0);
  transition:transform .5s var(--ease-out-quint);
}
[data-magnet-label].is-pulling{transition:transform .1s linear}

/* ==== 4. TILT AND SPOTLIGHT ====
   Ported from the 21st.dev tilt-card pointer maths (px and py normalised
   against the bounding box, rotation scaled off centre) and then changed in the
   two ways that matter here. The published component tilts 15 degrees and
   scales to 1.05; both are far too much for a compliance page and read as a
   toy, so this runs about 5 degrees with no scale at all. Its spotlight is
   white, which on this canvas is a grey smudge; this one is his amber, at the
   alpha where it registers as a glow rather than as a colour. */
@media (hover:hover) and (pointer:fine){
  [data-tilt]{
    transform-style:preserve-3d;
    transform:
      perspective(1000px)
      rotateX(var(--tilt-x,0deg))
      rotateY(var(--tilt-y,0deg))
      translate3d(0,var(--tilt-lift,0px),0);
    transition:transform .5s var(--ease-out-quint),border-color .3s var(--ease);
  }
  [data-tilt].is-tilting{transition:transform .12s linear,border-color .3s var(--ease)}
  /* the spotlight paints UNDER the content, never over it, so no text can lose
     contrast to it at any cursor position */
  [data-tilt]::after{
    content:"";position:absolute;inset:0;z-index:0;pointer-events:none;
    border-radius:inherit;
    background:radial-gradient(
      circle 220px at var(--spot-x,50%) var(--spot-y,50%),
      rgba(242,174,63,.13),
      rgba(242,174,63,.04) 38%,
      transparent 62%
    );
    opacity:0;transition:opacity .32s var(--ease);
  }
  [data-tilt].is-tilting::after{opacity:1}
  [data-tilt] > *{position:relative;z-index:1}
  [data-tilt].is-tilting{border-color:var(--line-strong)}

  /* TILT WITHOUT THE SPOTLIGHT. Will's call, 4 August 2026, for the two people
     cards on the About page: keep the tilt, drop the glow.

     He is right, and the reason generalises. The spotlight reads as current
     moving under a surface, which is what the why-cards and the compliance
     panels are: statements on a plate. The people cards are a portrait slot, a
     name and a paragraph about a person, and lighting that from the cursor
     makes a man's bio look like a product tile. The tilt still gives them the
     physicality; the glow was the part that made them merchandise.

     Written as an opt-out attribute rather than a .person selector so the next
     card that wants weight without shine does not need a new CSS rule.
     content:none stops the pseudo element being generated at all, so there is
     nothing left for .is-tilting to fade in. */
  [data-tilt][data-tilt-spot="off"]::after{content:none}
}
[data-tilt]{position:relative}

/* Photographs lift and sharpen rather than tilt. A photo on a card that tilts
   reads as a sticker, and these are Tom's own job photos. */
.job-grid img{
  transition:transform .55s var(--ease-out-quint),filter .55s var(--ease);
}
@media (hover:hover) and (pointer:fine){
  .job-grid figure,.job-grid a{overflow:hidden;border-radius:var(--radius-lg)}
  .job-grid figure:hover img,.job-grid a:hover img{
    transform:scale(1.035);filter:saturate(1.06) contrast(1.03);
  }
}

/* ==== 5. THE CREDENTIALS STRIP IS A LIVE CIRCUIT ====
   A single amber charge crosses the strip's bottom edge every eight seconds. It
   is the one piece of ambient motion on the site and it is here rather than
   anywhere else on purpose: this strip carries the licence number and the ABN,
   the two facts the whole page rests on, so the thing that quietly draws the
   eye back to them is worth its eight seconds. */
.creds{position:relative;overflow:hidden}
.creds::after{
  content:"";position:absolute;left:0;bottom:0;height:1px;width:18%;
  background:linear-gradient(90deg,transparent,var(--amber),transparent);
  opacity:.9;
  animation:creds-charge 8s linear infinite;
}
@keyframes creds-charge{
  0%{transform:translateX(-110%)}
  50%,100%{transform:translateX(670%)}
}
.creds li::before{transition:background-color .3s var(--ease)}
.creds:hover li::before{background:var(--amber)}

/* ==== 6. THE INTERCONNECT. The best idea in this block. ====
   Queensland's 2027 rule is not "have smoke alarms", it is "have smoke alarms
   that are INTERCONNECTED", which is the part nobody pictures. That dashed
   amber line in the cross section IS the interconnection, and it now carries a
   signal along it: the dashes travel, and each alarm's outer ring pulses in
   turn, so the drawing demonstrates the requirement instead of labelling it.

   The dashes were already in the markup (stroke-dasharray "5 4", so one cycle
   is 9 units and -27 is three whole dashes, which is what makes the loop
   seamless). This is a small addition to a diagram that was drawn correctly to
   begin with. */
.alarm-plan .interconnect path{
  animation:signal-travel 2.4s linear infinite;
}
@keyframes signal-travel{ to{stroke-dashoffset:-27} }
.alarm-plan .alarm-ring{
  transform-box:fill-box;transform-origin:center;
  animation:alarm-sound 2.4s var(--ease-out-quint) infinite;
  animation-delay:var(--ring-delay,0s);
}
@keyframes alarm-sound{
  0%,64%{opacity:.2;transform:scale(1)}
  74%{opacity:.8;transform:scale(1.16)}
  100%{opacity:.2;transform:scale(1)}
}

/* ==== 7. THE DRAWN DIAGRAMS DRAW THEMSELVES ====
   A stroke-dasharray and dashoffset reveal, armed by JS which measures each
   path's own length first. A single hardcoded dash length would make the short
   lines snap and the long ones crawl. Fills come up separately behind the
   strokes so the house does not appear as an empty outline waiting to be
   coloured in. */
html.js-motion [data-draw] [data-draw-stroke]{
  stroke-dasharray:var(--len);
  stroke-dashoffset:var(--len);
}
html.js-motion [data-draw].is-in [data-draw-stroke]{
  animation:draw-in 1.15s var(--ease-out-expo) forwards;
  animation-delay:var(--draw-delay,0ms);
}
@keyframes draw-in{ to{stroke-dashoffset:0} }
html.js-motion [data-draw] [data-draw-fade]{opacity:0}
html.js-motion [data-draw].is-in [data-draw-fade]{
  animation:draw-fade .7s var(--ease-out-quint) forwards;
  animation-delay:var(--draw-delay,0ms);
}
@keyframes draw-fade{ to{opacity:1} }

/* ==== 8. THE COUNTDOWN COUNTS ====
   The number is the loudest thing on the safety page and it was arriving fully
   formed. It now runs up to its value once, on first sight, over about a
   second. The markup is unchanged and the inline script still writes the true
   figure first, so a visitor with no JS animation still reads the correct
   number of days.

   tabular-nums was already on this element, which is what stops the width
   jittering as it counts. It was put there for a different reason and it
   happens to be exactly what this needs. */
.deadline-num{text-shadow:0 0 38px rgba(242,174,63,.18)}
html.js-motion .deadline-count.is-in .deadline-num{animation:count-settle .9s var(--ease-out-expo) both}
@keyframes count-settle{
  from{opacity:0;transform:translate3d(0,10px,0)}
  to{opacity:1;transform:none}
}

/* ==== 9. NAV: ONE AMBER CONTACT THAT SLIDES ====
   The nav had a per-link fill and, on the current page, a sheared amber
   underline as wide as that link. The underline is now a single shared element
   that travels to whichever link the cursor is on and returns to the current
   page on exit, so the bar reads as one control with a moving contact rather
   than as seven independent buttons. The [aria-current] underline stays in the
   CSS as the no-JS truth and is hidden only while the slider is actually live.

   IT IS A FIXED WIDTH CHIP, NOT A STRETCHING BAR, and that is a design decision
   before it is a performance one. A bar that resizes to each link has to
   animate width and left, which are layout properties and thrash on every
   frame. A contact of constant width that slides along the row animates on
   transform alone, costs nothing, and is the better idea anyway: it is the same
   16x11 sheared amber chip as .label::before, so the nav marker is made of the
   site's existing geometry, and a fixed contact travelling a busbar is what the
   electrical metaphor actually describes. */
.nav-list{position:relative}
.nav-slider{
  position:absolute;bottom:6px;left:0;width:18px;height:3px;
  background:var(--amber);
  transform:translate3d(var(--nav-x,0px),0,0) skewX(calc(var(--shear) * -1));
  opacity:0;pointer-events:none;
  transition:transform .38s var(--ease-out-quint),opacity .2s var(--ease);
}
.nav-slider.is-on{opacity:1}
html.js-motion .nav-list.has-slider .nav-link[aria-current="page"]::after{opacity:0}

/* ==== 10. SCROLL PROGRESS, GUIDE PAGES ONLY ====
   The safety page is 6547px of legislation. A hairline that says how much is
   left is a reading aid, not an ornament, which is why it is on that page and
   not on the four short ones.

   Zero JavaScript: a CSS scroll driven animation, behind @supports so browsers
   without it get nothing at all rather than a bar that never moves. No scroll
   listener means no main thread work and no cost to the page's TBT. */
.read-progress{display:none}
@supports (animation-timeline:scroll()){
  .guide-page .read-progress{
    display:block;position:fixed;left:0;top:0;height:2px;width:100%;
    transform-origin:0 50%;transform:scaleX(0);
    background:linear-gradient(90deg,var(--amber),#FFC463);
    z-index:150;pointer-events:none;
    animation:read-progress linear both;
    animation-timeline:scroll(root block);
  }
  @keyframes read-progress{ to{transform:scaleX(1)} }
}

/* ==== 11. LINKS AND SMALL PARTS ====
   The site's links were a colour change or a border colour change. Underlines
   now wipe in from the left, which costs nothing and is the difference between
   a page that responds and a page that recolours. */
.link-amber,.contents a{position:relative}
.link-amber::after,.contents a::after{
  content:"";position:absolute;left:0;right:0;bottom:-2px;height:1.5px;
  background:currentColor;transform:scaleX(0);transform-origin:0 50%;
  transition:transform .32s var(--ease-out-quint);
}
.link-amber:hover::after,.link-amber:focus-visible::after,
.contents a:hover::after,.contents a:focus-visible::after{transform:scaleX(1)}
.contents a:hover{border-bottom-color:transparent}

/* section labels: the amber chip charges up as its section arrives */
html.js-motion [data-reveal] .label::before,
html.js-motion [data-reveal-group] .label::before{
  transform:skewX(calc(var(--shear) * -1)) scaleX(0);transform-origin:0 50%;
}
html.js-motion [data-reveal].is-in .label::before,
html.js-motion [data-reveal-group].is-in .label::before{
  transform:skewX(calc(var(--shear) * -1)) scaleX(1);
  transition:transform .5s var(--ease-out-expo) .12s;
}

/* The service rows get a sheared amber edge that grows on hover: the same
   gesture as the button's leading edge, at row scale.

   The row shifts on TRANSFORM, not on padding. Padding was the obvious way to
   write this and it is the wrong one: padding is a layout property, so every
   frame of the hover would reflow the row and everything under it. translateX
   moves the same distance on the compositor for nothing. */
.svc-row{position:relative;transition:transform .38s var(--ease-out-quint)}
.svc-row::before{
  content:"";position:absolute;left:-14px;top:14%;bottom:14%;width:3px;
  background:var(--amber);transform:skewX(calc(var(--shear) * -1)) scaleY(0);
  transform-origin:50% 0;transition:transform .42s var(--ease-out-quint);
}
@media (hover:hover) and (pointer:fine){
  .svc-row:hover{transform:translate3d(14px,0,0)}
  .svc-row:hover::before{transform:skewX(calc(var(--shear) * -1)) scaleY(1)}
}

/* ==== 12. REDUCED MOTION ====
   The global rule near the top of this file already clamps every duration to
   0.01ms, which handles the transitions. Infinite animations have to be
   switched off outright rather than sped up, or they become a strobe. */
@media (prefers-reduced-motion:reduce){
  .creds::after,
  .alarm-plan .interconnect path,
  .alarm-plan .alarm-ring,
  html.js-motion .hero-logo,
  html.js-motion .hero h1,
  html.js-motion .hero-sub,
  html.js-motion .hero-actions{animation:none!important}
  .alarm-plan .alarm-ring{opacity:.2}
  html.js-motion [data-reveal],
  html.js-motion [data-reveal-group] > *{opacity:1!important;transform:none!important}
  html.js-motion [data-draw] [data-draw-stroke]{stroke-dashoffset:0!important;stroke-dasharray:none!important}
  html.js-motion [data-draw] [data-draw-fade]{opacity:1!important}
  html.js-motion [data-reveal] .label::before,
  html.js-motion [data-reveal-group] .label::before{transform:skewX(calc(var(--shear) * -1))!important}
  [data-magnet],[data-magnet-label]{transform:none!important}
  [data-tilt]{transform:none!important}
  [data-tilt]::after{opacity:0!important}
}

/* ==========================================================================
   THE SPLIT HERO. Added 4 August 2026 from a reference Will supplied.

   ONE THING WAS TAKEN FROM THAT REFERENCE and it is worth writing down what was
   not, because the reference was a mockup carrying a lot the site cannot have.
   Taken: the photograph in its own panel on the right, cut away from the copy
   by a lightning bolt edge. NOT taken, each for a reason the build already
   holds: the stock photograph of a man (the brief bans stock people and AI
   imagery, and there is no photograph of Tom yet, which is why the About page
   still says "portrait to come"); the trust badges reading FREE QUOTES, 20+
   YEARS and 100% RECOMMENDED as icon lockups; the strip reading FULLY LICENCED
   & INSURED and WORK GUARANTEED, none of which is verified and two of which the
   3 August pass deliberately removed; and the .com.au domain on the van, which
   is not his (taelectricalgc.com, no .au). The headline stays his.

   WHY A SPLIT AND NOT A BACKDROP. The photographic hero of 3 August put type
   over a bright dealership shot and needed a measured directional veil to hold
   contrast; Tom then asked for the page to stay black with the vehicles ON it
   rather than behind the writing, and it was reverted. A split panel answers
   both: the photograph is above the fold at full strength, and the type is on
   flat --sunk in its own column at exactly the ratios every other page uses.
   No veil, no contrast audit, nothing to regress.

   THE EDGE IS ONE SHAPE CUT TWICE. .hero-media::before is the amber, clipped by
   the outer polygon; the img is clipped by the same polygon shifted right by
   --bolt-w. The amber that shows is the difference between them, so the line
   can never come adrift from the photograph the way two independently drawn
   shapes would. Same lesson as the button.
   ========================================================================== */
@media (min-width:900px){
  .hero.is-split{
    /* the bolt, as four x positions down the right panel. The kink between
       --bolt-x2 and --bolt-x3 is what makes it read as a bolt rather than as a
       plain diagonal, and it is the same silhouette as the tail through the T
       in his logo. */
    /* THE BOLT WAS EATING THE VAN. Will spotted it: the white van has two faces
       in this photograph, its side and its front, and at the first values the
       edge cut straight down through the front so only the side survived. The
       van read as a white shape behind the ute rather than as a vehicle, and
       the whole point of the panel is that there are two of them.

       The edge is pulled about 8 points further left across all four stops,
       which is enough to clear the van's nose (full perforated grille, the
       headlight, the badge) while keeping the kink obvious. The silhouette is
       unchanged; it is the same bolt, sitting further out. */
    /* AND PULLED LEFT AGAIN, 5 August 2026, Tom's note: he wanted the whole
       front BUMPER, not just the nose. The pass above cleared the grille and
       the headlight but the edge still landed across the bumper's left end,
       so the van's front stopped in mid air instead of ending in a corner.

       WHY THIS IS THE ONLY LEVER. object-position is already 0%, meaning the
       leftmost pixel of the photograph is on screen, so there is nothing
       further left to pan to. Nor can the panel be widened: the photograph is
       scaled to fill the panel's HEIGHT and anchored to its left edge, so a
       wider panel reveals more on the RIGHT and leaves the bumper exactly
       where it was, still under the same percentage of bolt. Moving the bolt
       is the only thing that uncovers it. Four positions were rendered and
       compared at the bumper rather than reasoned about; this is the first
       one where the body's left corner, the end of the grille and the floor
       behind it are all in frame.

       WHAT IT COSTS, so nobody "restores" it by accident: the black wedge is
       now mostly gone below the kink and the lower edge runs nearly straight
       down the column boundary. The bolt reads from its top wedge and its
       kink, which both survive. That is a deliberate trade of motif for
       subject, made by the client, and the previous values are one line up
       if it ever needs reversing. */
    --bolt-x1:11%;      /* where the edge meets the top */
    --bolt-x2:0%;       /* the point, jutting left into the black */
    --bolt-x3:3%;       /* the step back */
    --bolt-x4:0%;       /* where the edge meets the bottom */
    /* THE KINK'S HEIGHT IS A VARIABLE BECAUSE OF WHERE IT LANDED. At 46/54 the
       widest part of the bolt sat exactly across the van's number plate and
       masked it, which is the one thing on that vehicle worth reading. Moving
       the kink up puts the intrusion over the windscreen, where there is
       nothing to lose, and leaves the lower half running almost straight so the
       plate clears. */
    --bolt-y1:24%;      /* the point */
    --bolt-y2:32%;      /* the step back */
    --bolt-w:8px;       /* thickness of the amber line */
    --bolt:polygon(
      var(--bolt-x1) 0, 100% 0, 100% 100%, var(--bolt-x4) 100%,
      var(--bolt-x3) var(--bolt-y2), var(--bolt-x2) var(--bolt-y1)
    );

    position:relative;
    display:grid;
    grid-template-columns:minmax(0,1fr) minmax(0,52%);
    align-items:center;
    padding-block:0;
    min-height:clamp(30rem,62vh,38rem);
    overflow:hidden;
  }
  /* the flat amber wedge that used to fill the right of the hero is the
     photograph's job now, and two amber shapes in one corner is one too many */
  .hero.is-split::after{content:none}

  .hero.is-split .hero-in{
    grid-column:1;
    padding-block:clamp(2.6rem,6vw,4rem);
    /* the wrap centres the page, but this column is only the left half of it,
       so the copy keeps the page's left margin and loses the right one */
    max-width:calc((1120px / 2) + clamp(20px,5vw,44px));
    margin-inline:auto 0;
    padding-right:clamp(1.5rem,3vw,2.5rem);
  }
  .hero.is-split .hero-sub{max-width:36ch}

  .hero-media{
    grid-column:2;grid-row:1;
    position:relative;align-self:stretch;
    /* bleed to the viewport edge rather than stopping at the 1120px wrap */
    margin-right:calc((100% - 100vw) / 2 * 0);
    background:var(--sunk);
  }
  /* the amber, cut by the outer bolt */
  .hero-media::before{
    content:"";position:absolute;inset:0;z-index:1;
    background:linear-gradient(160deg,#FFC463,var(--amber) 45%,#D9922B);
    clip-path:var(--bolt);
  }
  /* the photograph, cut by the same bolt shifted right by the line thickness */
  /* OBJECT-POSITION IS MEASURED, NOT GUESSED, and it took two passes.

     The panel is roughly square and the photograph is 4:3, so cover throws away
     about 30% of the width, and where it throws it away from decides whether
     this is a picture of two vehicles or a picture of a grille. The first guess
     of 62% put the ute in the frame and left the van as a sliver. 44% fixed
     that but still cut the van's FRONT, so it read as a white shape parked
     behind the ute rather than as the second vehicle.

     12% is the value where the van's whole nose clears the bolt: the full
     perforated grille, the headlight and the badge, alongside its side panel
     with his phone number and email. Both plates' vehicles are complete, and
     the ute keeps its TA ELEC plate anchoring the bottom right. Six
     combinations of this and the bolt stops were rendered and compared rather
     than reasoned about. */
  .hero-media img{
    position:absolute;inset:0;z-index:2;
    width:100%;height:100%;object-fit:cover;object-position:0% 55%;
    clip-path:polygon(
      calc(var(--bolt-x1) + var(--bolt-w)) 0, 100% 0, 100% 100%,
      calc(var(--bolt-x4) + var(--bolt-w)) 100%,
      calc(var(--bolt-x3) + var(--bolt-w)) var(--bolt-y2),
      calc(var(--bolt-x2) + var(--bolt-w)) var(--bolt-y1)
    );
  }
  /* A darkening only along the cut, so the amber line has something to sit
     against and the busy showroom does not crowd the headline. It stops well
     before the middle of the photograph: this is not a veil, nothing is read
     over it, and the vehicles are at full strength where they matter. */
  .hero-media::after{
    content:"";position:absolute;inset:0;z-index:3;pointer-events:none;
    background:linear-gradient(100deg,rgba(8,8,9,.85) 0,rgba(8,8,9,.35) 18%,rgba(8,8,9,0) 38%);
    clip-path:polygon(
      calc(var(--bolt-x1) + var(--bolt-w)) 0, 100% 0, 100% 100%,
      calc(var(--bolt-x4) + var(--bolt-w)) 100%,
      calc(var(--bolt-x3) + var(--bolt-w)) var(--bolt-y2),
      calc(var(--bolt-x2) + var(--bolt-w)) var(--bolt-y1)
    );
  }
  /* THE PANEL DOES NOT FADE IN, AND THIS IS A PERFORMANCE RULE BEFORE IT IS A
     TASTE ONE. It had `current-rise .9s .1s both` here, matching the rest of
     the hero, and that single line cost a whole second of Largest Contentful
     Paint: the photograph IS the LCP element on this page, `both` holds it at
     opacity 0 until the animation runs, and LCP does not count an element while
     it is invisible. Measured on a preview deployment: the file finished
     downloading at 1.48s and did not register as painted until 2.8s.

     It is also the exact rule the rest of this stylesheet already follows, in
     the reveal block: a reveal must enhance an already visible default, never
     gate content on a class. The hero copy still staggers around it. */
}

/* ==== BELOW 900px: STACKED, AND THE BOLT TURNS ====
   A vertical split on a phone gives two columns of nothing. The panel becomes a
   full width band under the buttons with the bolt running across its top edge
   instead of down its side, so the motif survives the breakpoint rather than
   being dropped at it. */
@media (max-width:899.98px){
  .hero.is-split{position:relative;overflow:hidden;padding-bottom:0}
  .hero-media{
    --bolt-w:7px;
    position:relative;display:block;width:100%;
    margin-top:clamp(1.8rem,5vw,2.6rem);
    aspect-ratio:16/10;background:var(--sunk);
  }
  .hero-media::before{
    content:"";position:absolute;inset:0;z-index:1;
    background:linear-gradient(160deg,#FFC463,var(--amber) 45%,#D9922B);
    clip-path:polygon(0 14%,46% 4%,54% 12%,100% 0,100% 100%,0 100%);
  }
  /* The stacked panel is 16:10 from a 4:3 source, so here cover crops the
     HEIGHT rather than the width and the framing problem is the opposite one:
     centred is right, nudged down slightly so the bolt edge eats ceiling rather
     than number plates. */
  .hero-media img{
    position:absolute;inset:0;z-index:2;
    width:100%;height:100%;object-fit:cover;object-position:50% 58%;
    clip-path:polygon(
      0 calc(14% + var(--bolt-w)),46% calc(4% + var(--bolt-w)),
      54% calc(12% + var(--bolt-w)),100% calc(0% + var(--bolt-w)),
      100% 100%,0 100%);
  }
  .hero-media::after{content:none}
  /* no entrance here either, and on a phone it matters more: this is where the
     photograph is the LCP element on the narrowest connection. See the note in
     the desktop block above. */
}

/* Belt and braces. Nothing should be animating .hero-media after the two rules
   above, but if a later pass ever adds one, this keeps the LCP element painted
   for anyone who has asked for less motion. */
@media (prefers-reduced-motion:reduce){
  html.js-motion .hero-media{animation:none!important;opacity:1!important;transform:none!important}
}

/* ==== WHAT WE DO: THREE COLUMNS WITH A SQUARE PHOTOGRAPH ON TOP ====
   Rebuilt 4 August 2026 from Will's sketch. The old .svc-list was a stacked
   run of heading-left / paragraph-right rows: correct, readable, and reading as
   a table of contents rather than as three kinds of work. It was also the only
   section on the home page with no photograph in it.

   THE SQUARE IS NOT A STYLE CHOICE, IT IS WHAT THE SOURCE FILES ARE. Tom's job
   photos were already exported 1:1 for the services page, so a square slot
   crops nothing: the frame you see is the frame he sent. aspect-ratio holds the
   box before the file arrives, which is what keeps CLS at zero on a lazy image.

   .svc-list IS DELIBERATELY LEFT IN THE STYLESHEET. It is four rules, the
   services page still uses that pattern, and it is the revert path. */
.svc-cards{
  display:grid;
  grid-template-columns:repeat(3,minmax(0,1fr));
  gap:clamp(1.2rem,2.6vw,2rem);
  margin-top:clamp(1.8rem,3.5vw,2.6rem);
  list-style:none;
}
.svc-card{
  position:relative;
  display:flex;flex-direction:column;
  /* a hairline above each column rather than a box around it: the site's own
     idiom, the same top rule the .why-item cards use, so this reads as part of
     the page and not as a widget bolted onto it */
  border-top:2px solid var(--line-strong);
  padding-top:1.15rem;
  transition:border-color .35s var(--ease);
}
.svc-shot{
  margin:0 0 1.15rem;
  aspect-ratio:1/1;
  overflow:hidden;
  border-radius:var(--radius-lg);
  background:var(--sunk);
  border:1px solid var(--line);
  position:relative;
}
.svc-shot img{
  width:100%;height:100%;object-fit:cover;display:block;
  transition:transform .6s var(--ease-out-quint),filter .5s var(--ease);
}
.svc-card h3{margin-bottom:.5rem}
.svc-card p{color:var(--ink-soft);font-size:.96rem}

/* The amber shear rides the top rule, growing from the left on hover: the same
   gesture the old .svc-row had, moved to the edge that now separates the
   columns. Sheared, because everything on this site that marks a thing is. */
.svc-card::before{
  content:"";position:absolute;left:0;top:-2px;height:2px;width:38%;
  background:var(--amber);
  transform:skewX(calc(var(--shear) * -1)) scaleX(0);transform-origin:0 50%;
  transition:transform .42s var(--ease-out-quint);
}
@media (hover:hover) and (pointer:fine){
  .svc-card:hover::before{transform:skewX(calc(var(--shear) * -1)) scaleX(1)}
  .svc-card:hover .svc-shot img{transform:scale(1.04);filter:saturate(1.05) contrast(1.02)}
}

/* Two up, then one. The third card centring itself on a two-column row would
   read as a mistake, so below 1000px it goes two-then-one with the last card
   spanning, and below 720px they simply stack. */
@media (max-width:1000px){
  .svc-cards{grid-template-columns:repeat(2,minmax(0,1fr))}
  .svc-cards > :last-child{grid-column:1 / -1}
  .svc-cards > :last-child .svc-shot{aspect-ratio:16/9}
}
@media (max-width:720px){
  .svc-cards{grid-template-columns:1fr;gap:2rem}
  .svc-cards > :last-child{grid-column:auto}
  .svc-cards > :last-child .svc-shot{aspect-ratio:1/1}
}

/* ==========================================================================
   MOBILE PASS, 5 August 2026. Appended block, mobile only.
   ==========================================================================
   Everything below this line is inside a max-width media query, mechanically
   checked, so the desktop rendering is provably unchanged. Nine fragments,
   concatenated in cascade order: broadest first, then components, then motion,
   then interaction states, then the call bar, then the drawer last so it wins
   over everything including html.nav-open .callbar over .callbar.is-hidden.

   Rules marked ORCHESTRATOR CORRECTION were changed at merge time; each one
   carries the measurement that justified it at the declaration site.
   ========================================================================== */

/* ---------- frag-10-shell ---------- */
/* frag-10-shell.css: page shell on a phone: safe-area gutters, no permanent header blur, the 14px readability floor, and the contact page's two most valuable taps.
   APPEND AFTER: nothing. This fragment is FIRST in the concatenation, so it is
                 the broadest layer and every later fragment overrides it.
   OWNS: .wrap, .wrap-narrow, .site-header, .field .err, .info-list .v a
   TOUCHES NOTHING ELSE. */

/* ---- 1. THE PAGE GUTTER RESPECTS THE NOTCH ----------------------------------
   The counterpart to viewport-fit=cover. Until that meta landed, every
   env(safe-area-inset-*) in this stylesheet resolved to 0, so this gutter has
   never once been tested against a real inset. Measured under a notched-iPhone
   landscape emulation (844x390, inset 59px): the gutter computes to 42.2px
   (5vw) against a 59px inset, so the first 16.8px of every line of text sits
   under the notch. In portrait the inset is 0 and max() returns the clamp
   unchanged, which is why this costs nothing on the common case.

   max() around the WHOLE clamp, not clamp(max(...)): the inset is a floor on
   the finished gutter, not a floor on the preferred term.

   padding-left / padding-right longhands deliberately. The shipped rule
   (styles.css:98-99) is the padding-inline SHORTHAND, and a shorthand later in
   the cascade would reset both longhands. Appended after it, these two win. */
@media (max-width:900px){
  .wrap,
  .wrap-narrow{
    padding-left: max(clamp(20px,5vw,44px), env(safe-area-inset-left));
    padding-right:max(clamp(20px,5vw,44px), env(safe-area-inset-right));
  }
}

/* ---- 2. THE HEADER STOPS BLURRING ON A PHONE --------------------------------
   backdrop-filter:blur(12px) on a full-width sticky bar is a compositor layer
   that is resampled on every frame of every scroll, for the entire session. On
   a mid-range Android that is real battery, and this audience is defined by
   "mobile data, dying battery".

   It also buys nothing here. The page behind is #0B0B0C and the header was
   #0B0B0C at 90%, so the blur was sampling near-black and returning near-black:
   a 10% bleed of an almost-black page through an almost-black bar. Going fully
   opaque removes the layer and leaves the same colour on screen.

   .is-stuck, its border-colour transition and the JS sentinel are deliberately
   untouched. The header is seamless against the page until you scroll, exactly
   as today, and then the hairline appears, exactly as today. Desktop keeps the
   blur: on a large window the bar is a smaller share of the frame, the cost is
   paid by a machine on mains power, and nothing about that trade is the same. */
@media (max-width:900px){
  .site-header{
    background:var(--canvas);
    -webkit-backdrop-filter:none;
    backdrop-filter:none;
  }
}

/* ---- 3. THE 14px FLOOR, WHERE IT IS AN ERROR MESSAGE ------------------------
   .85rem is 13.6px. This is the one piece of sentence-case running text on the
   site that a visitor is only ever shown at the exact moment something has gone
   wrong, in #FFA39B, on a phone, at arm's length. 0.9rem is 14.4px.

   The tracked uppercase labels around it (.info-list dt at .76rem, .label at
   .72rem) are deliberately NOT raised: they are the logo's own wide-tracked
   caps and heavy tracking materially raises legibility at small sizes. The
   floor applies to sentence-case running text, not to fine print.

   min-height:1.2em is untouched and still reserves the line, so the field does
   not jump when the message appears. It simply reserves a slightly taller one. */
@media (max-width:900px){
  .field .err{ font-size:.9rem; }
}

/* ---- 4. THE PHONE NUMBER AND THE EMAIL ON THE CONTACT PAGE ------------------
   Measured at 390px: 33.7px and 31.7px tall. These are the two most valuable
   taps on the page and both sat under the 44px floor.

   This supersedes the .15rem padding compromise at styles.css:1146. That
   comment reasoned it out correctly and then stopped short of the floor,
   because it was aiming at 24px. The row already owns this vertical space via
   the li's own 1.15rem gap, so paying for it costs the layout nothing it was
   not already spending.

   flex + min-height, per the binding mechanism for vertical link lists: it is
   deterministic and it cannot overlap a neighbour. NOT an invisible ::after
   expander, which on a list with this pitch would have the later sibling
   painting over the earlier one and stealing its taps. padding-block:0 removes
   the superseded .15rem so the box is exactly the 44px it claims to be. */
@media (max-width:900px){
  .info-list .v a{
    display:flex;
    align-items:center;
    min-height:44px;
    padding-block:0;
  }
}

/* ---- REDUCED MOTION ---------------------------------------------------------
   Nothing. Deliberately, and after checking rather than by omission.

   This fragment declares no transition, no animation, no delay and no
   transform. The only animated property anywhere in P10's namespace is the
   shipped .skip{transition:top .18s var(--ease)}, which this fragment does not
   touch and which the global *{transition-duration:.01ms!important} at
   styles.css:95 already clamps. There is no transition-delay anywhere in the
   namespace, so there is nothing for a reduce block to clamp that is not
   already clamped. A rule here would be a rule with no work to do. */

/* ---------- frag-06-targets ---------- */
/* frag-06-targets.css: mobile tap-target floor for the header brand and the footer link lists.
   APPEND AFTER: frag-10-shell
   OWNS: .brand, .foot-grid, .foot-grid ul, .foot-grid a, .foot-meta, .site-footer
   TOUCHES NOTHING ELSE. */

/* ==========================================================================
   TAP TARGETS: THE HEADER BRAND AND THE FOOTER. Mobile only.
   Every declaration below lives inside a max-width media query, so nothing
   here can reach the desktop layout. Verified: the 1440x900 render of all
   seven pages is byte-identical with and without this file, and the computed
   values at 1440 are unchanged (.brand 34px, .foot-grid a 26.47px,
   display inline-block, padding-block 3.52px, ul gap 1.6px).

   MECHANISM, and the only one permitted for a vertical list of links:
   display:flex + align-items:center + min-height:44px on the anchor, with the
   list's gap set to 0. That gives a deterministic 44px row pitch where each
   row exactly abuts the next: measured gap between every adjacent pair is
   0.00px, so there is no overlap anywhere and no row can steal a neighbour's
   tap. 56 adjacent pairs checked across all seven pages, at 390 and at 320.

   WHAT IS DELIBERATELY NOT DONE HERE, and why:
   1. No invisible ::after hit-expanders. The footer rows sit at ~28px pitch,
      so a 44px expander on each would overlap its neighbours by ~16px, and
      the later sibling paints over the earlier one: a tap just under
      "Services" would fire "Safety & compliance". Hit expansion is only safe
      on isolated targets, and there are none here worth it.
   2. No change to .foot-meta. Copyright, ABN and licence number at .84rem
      (13.44px measured) is legitimate fine print, not a readability defect.
   3. No change to .link-steel inside a review figcaption. That is the
      documented WCAG 2.2 SC 2.5.8 inline exception at styles.css:1136-1144,
      reasoned out by hand there, and it is correct.
   4. No :active rule. Press feedback is a separate layer.
   5. This fragment introduces no transition and no animation, so it has no
      prefers-reduced-motion override to write. The global reduce block at
      styles.css:93-96 stays the only one that applies.
   ========================================================================== */

/* 1. THE BRAND LOCKUP, measured 172x34 at 390px. It is the header's home
   link, and the one target up there that is neither the 44x44 menu button
   nor the call button. .brand is already display:flex + align-items:center,
   so a min-height grows the hit box symmetrically about the 34px logo and
   nothing moves optically. The header is a fixed 64px on mobile
   (--header-h has no mobile override), which leaves 10px of clearance above
   and below. .brand img (34x34) and .brand-word are untouched: the lockup
   renders identically, only the hit area changes. Measured after: 44x171.7.

   ORCHESTRATOR CORRECTION (merge time): this rule alone moves to the 1000px
   hamburger breakpoint, not 900px. P06 followed its spec literally and then
   flagged the gap rather than deciding: .menu-btn renders from 1000px down
   (styles.css:435), so between 901 and 1000 the header is ALREADY in its
   mobile hamburger arrangement while .brand stayed 34px. Matching the
   breakpoint that governs the header is the correct scope for a header target.
   Desktop above 1000px is untouched. The footer rules stay at 900px. */
@media (max-width:1000px){
  .brand{ min-height:44px; }
}

@media (max-width:900px){

  /* 2. THE FOOTER LINK LISTS. Ten links per page (six pages, the phone
     number, the email, Facebook, Instagram), measured 26.47px tall and
     between 38.89 and 156.13px wide, and this is the block a thumb reaches
     for on a phone.

     This supersedes the measured 26px compromise documented at
     styles.css:1175-1188. That comment is honest about its own trade: it
     capped the fix at padding plus a 1.35 line-height specifically to stop
     the footer loosening, and the result landed at 26.47px, under the 44px
     floor. On mobile, loosening the footer is the correct trade. Row pitch
     becomes exactly 44px, the six-link column grows from 168px to 264px, and
     the footer as a whole goes from 664.72px to 761.94px at 390px. Nothing
     else on the page has to move to pay for it, because the footer is the
     last block on it.

     padding-block:0 is load-bearing: it retires the .22rem from the base rule
     so min-height is the sole sizer, which is what makes the pitch exactly
     44px instead of 44px-or-more-depending-on-the-label. line-height:1.3
     keeps a one-line label optically centred and lets a two-line label (which
     happens to "Safety & compliance" and "Service areas" once the column
     narrows to 83.9px at 320px) still fit inside the 44px box: 2 x 18.72px
     = 37.44px of text in a 44px band.

     .phone-link and the Facebook / Instagram links are all .foot-grid a
     descendants, so this one rule fixes every one of the ten. A second
     selector for them would be a second thing to keep in sync for no gain. */
  .foot-grid ul{ gap:0; }
  .foot-grid a{
    display:flex; align-items:center;
    min-height:44px; padding-block:0; line-height:1.3;
  }

  /* 3. .foot-grid layout itself is deliberately unchanged. Verified at 390px
     and 320px on all seven pages: the two-column arrangement from the
     existing @media (max-width:760px) rule still holds, computed tracks are
     153.88/156.13px at 390 and 83.88/156.13px at 320, nothing escapes its
     column, and document.scrollWidth stays equal to the viewport width at
     both. Also checked at 768px, where the three-column arrangement is still
     in force and reads correctly with 44px rows. */

}

/* ---------- frag-09-pages ---------- */
/* frag-09-pages.css: home, services, about and reviews on a phone
   APPEND AFTER: frag-10-shell, frag-06-targets
   OWNS: .hero-actions, .hero-actions .btn, .creds::after,
         .about-grid figcaption, .q1-photo figcaption, .crew figcaption
   TOUCHES NOTHING ELSE. */

/* ==========================================================================
   1. THE HERO ACTIONS BECOME TWO FULL-WIDTH SLABS.

   Measured at 390px before this rule: "Call 0410 055 089" renders 232.6px
   wide and "Get a free quote" 210.3px, both already on their own line because
   .hero-actions wraps. Two ragged left-aligned widths, one under the other,
   for the only two decisions on a triage page. They should be two slabs the
   width of the column, under the thumb.

   THE ::before OVERHANG WAS CHECKED, NOT ASSUMED. The old .btn::before at
   styles.css:250 was a 14px wedge hung at left:-7px, which WOULD have escaped
   a width:100% box and pushed the page 7px wider. It is superseded at
   styles.css:1786, which resets the geometry to inset:0 / left:0 / width:auto /
   transform:none and draws the shear with a clip-path INSIDE the box instead.
   Probed live at 390 and 320 on all four pages: computed left 0px, right 0px,
   inset 0px, transform none, clip-path polygon(18.065px 0, 100% 0, 100% 100%,
   0 100%). The slant cuts in; nothing hangs out. width:100% is safe.

   flex-wrap goes to nowrap deliberately. align-items:stretch stretches an item
   to its FLEX LINE's cross size, not the container's, so a column container
   that ever wrapped into two columns would give half-width slabs. The
   container is content-height in normal flow so it cannot wrap today; nowrap
   makes that a guarantee rather than a coincidence. */
@media (max-width:640px){
  .hero-actions{
    flex-direction:column;
    flex-wrap:nowrap;
    align-items:stretch;
    gap:.85rem;
  }
  .hero-actions .btn{ width:100%; }
}

/* ==========================================================================
   2. THE CREDENTIALS CHARGE STOPS AFTER FOUR PASSES ON MOBILE.

   THIS IS THE ONE PLACE IN THIS FRAGMENT WHERE MOBILE DELIBERATELY DIVERGES
   FROM DESKTOP, and it is a battery decision, not a taste one. .creds::after
   (styles.css:1939-1948) is an 8s linear INFINITE compositor animation. On a
   desktop that is ambient. On this audience, "mobile data, possibly on a low
   battery", standing in a dark house, a page that never stops compositing is
   a cost with no ceiling. Four passes is 32 seconds: long enough that anyone
   reading the licence number and the ABN sees the charge cross them, short
   enough that the page eventually goes quiet. The idea survives; the drain
   does not. Desktop keeps `infinite`, unchanged.

   FILL-MODE IS NOT OPTIONAL HERE, and the plan did not call for it. The
   keyframes end at 50%,100%{translateX(670%)}, which parks the 18%-wide amber
   gradient past the right edge where .creds{overflow:hidden} clips it away.
   With iteration-count alone and no fill mode, the element reverts to its
   un-animated transform at the end of the fourth pass, translateX(0), which
   leaves a static amber smear sitting at the bottom-LEFT of the strip
   permanently. Capping the loop without `forwards` trades a battery cost for a
   visual defect. Verified by driving animation-delay:-33s and reading the
   computed transform in both configurations. */
@media (max-width:900px){
  .creds::after{
    animation-iteration-count:4;
    animation-fill-mode:forwards;
  }
}

/* ==========================================================================
   3. THE 14px READABILITY FLOOR ON THIS PACKAGE'S FIGCAPTIONS.

   Sentence-case running text below 14px, per the binding small-text policy.
   Measured at 390px: .about-grid figcaption and .q1-photo figcaption both
   render 13.76px (.86rem) on about.html, "Tom, beside the TA Electrical GC
   ute.", "Sam, beside the TA Electrical GC van.", "Tom and Sam at the top of
   Q1...". Colour is unchanged (--steel, already 8.8:1); only the size moves.

   NOT TOUCHED, deliberately: the review figcaptions on index.html and
   reviews.html already measure 14.4px, and the .link-steel "Facebook" inside
   them is the documented WCAG 2.2 SC 2.5.8 inline exception at
   styles.css:1136-1144. The tracked uppercase .label at 11.52px is legitimate
   fine print and belongs to another package besides.

   .crew figcaption is included because the policy names it, but it currently
   matches nothing: .crew appears in zero shipped HTML files. Inert today,
   correct the day a crew block ships.

   ORCHESTRATOR CORRECTION (merge time): breakpoint moved 640 -> 900 to
   harmonise the readability floor across packages. P06, P08 and P10 all apply
   their floors at max-width:900px; P09's stated package default was 640px, so
   as written these two captions sat at 13.76px between 641 and 899 while every
   other caption on the site was already at 14px there. One number, nothing
   else in this fragment moves. */
@media (max-width:900px){
  .about-grid figcaption,
  .q1-photo figcaption,
  .crew figcaption{ font-size:.875rem; }
}

/* ==========================================================================
   4. NO REDUCED-MOTION BLOCK IN THIS FRAGMENT, BY MEASUREMENT.

   Checked rather than duplicated. styles.css:2112-2119 already carries
   `.creds::after{animation:none!important}` in an unscoped
   @media (prefers-reduced-motion:reduce) block, so it applies at every width
   including this one, and an !important shorthand resets iteration-count and
   fill-mode along with everything else, my un-important declarations above
   cannot resurrect the loop or strand a filled end state. Nothing else this
   fragment writes is motion: a flex direction, a width and a font size have
   no reduced-motion counterpart. Adding a block here would be dead CSS.

   5. NOTHING ELSE MEASURED WRONG. The audit at 390 and 320 across index,
   services, about and reviews is in the report: document.scrollWidth equals
   innerWidth on all eight combinations before and after, .creds ul wraps
   cleanly to three rows at 390 and four at 320, .why / .svc-row / .svc-list /
   .about-grid are all already single-column, .job-grid holds its 2-up at both
   widths, and [data-tilt] on .why-item computes transform:none. .triage,
   .fault-list, .svc-cards, .svc-card, .svc-shot, .person, .people, .crew and
   .van-band render on none of these four pages. */

/* ---------- frag-08-guide ---------- */
/* frag-08-guide.css: the two guide pages on a phone: the jump nav becomes a real control, the section drawing becomes readable, the reading hairline becomes visible.
   APPEND AFTER: frag-10-shell, frag-06-targets, frag-09-pages
   OWNS: .contents, .contents ul, .contents a, .alarm-plan, .alarm-plan svg, .alarm-caption, .guide-page .read-progress
   TOUCHES NOTHING ELSE. */

/* ==========================================================================
   P08. safety.html and service-areas.html on mobile.
   Every declaration below sits inside a max-width media query, so desktop is
   byte-identical: proved by md5 on 1440x900 full-page captures of both pages.
   ========================================================================== */

@media (max-width:900px){

  /* ---- 1. THE JUMP NAV IS THE PAGE'S MOST IMPORTANT CONTROL AND IT READ AS
     PROSE. safety.html is 8285 CSS px tall at 390px (16,570 device px at the
     2x capture the brief measured), and it is the page a visitor arrives at
     from a search with exactly one question. The six links were the mitigation
     and they were 35.7px tall, unbordered, unpadded, sitting in a wrapped row
     at the same size and weight as body copy. Measured in the render they read
     as a sentence, not as six things you can press.

     They become chips: bordered, padded, 44px minimum. Same colour, same
     size, same words. No amber, because a jump link is not the call action,
     not an active state and not the bolt motif, which are the only three
     things amber is allowed to be.

     Row gap goes to .6rem so the wrap reads as a deliberate grid of controls
     rather than as a paragraph that happens to have gaps in it. The column
     gap comes DOWN from 1.6rem, because the border is now doing the
     separating that the whitespace used to have to do on its own.

     .contents a::after (the underline wipe at styles.css:2070) is deliberately
     left alone: it only fires on :hover and :focus-visible, so it is inert on
     a touch device and still correct for a keyboard.

     ONE NUMBER FOR WHOEVER MERGES THIS. --line composites to rgb(36.8,36.8,37.6)
     on --canvas, which is 1.281:1. It reads as six chips in the 390px render
     and it is what the plan specifies, so it is what ships. --line-strong is
     1.766:1 and the edges read materially crisper in bright ambient light,
     which is the condition this audience is in. It is an existing token, it is
     the token .contents already uses for its own top rule, and swapping it is
     two words. Both renders are in pkg-08/shots/chip{A,B}-contents-390.png.
     Flagged, not taken: deviating from the contract inside a parallel run is
     how a ten-way merge goes wrong. */
  .contents ul{ gap:.6rem .6rem; }
  .contents a{
    display:flex; align-items:center; min-height:44px;
    padding:.55rem .8rem;
    /* ORCHESTRATOR CORRECTION (merge time): --line composites to 1.281:1 on
       --canvas, which made the chips FAINTER than the .contents container
       around them, and that container already uses --line-strong (1.766:1) for
       its own top rule. P08 measured both and flagged it. Same token family,
       no new colour. */
    border:1px solid var(--line-strong);
    border-radius:var(--radius);
    border-bottom-color:var(--line-strong);
  }

  /* ---- 2. THE TWO STOREY SECTION DRAWING. MEASURED, THEN DECIDED.
     The room labels are set at 17 user units inside a 520x320 viewBox. At
     390px the svg renders 312.8px wide, a scale of 0.6016, so those labels
     land at 10.23 effective px. At 320px the scale is 0.4669 and they land at
     7.94px. Both are under the 11px floor, so the horizontal scroll container
     applies. (getComputedStyle reports 17px at every viewport, because that is
     the user-unit value and it never changes; the number that decides
     legibility is the rendered one, and it is the one measured here.)

     A technical section drawing is the one legitimate case for a horizontally
     scrollable container on a phone. The container clips, so page level
     horizontal overflow stays at zero: verified at 390 and 320 on both pages.

     ONE DELIBERATE DEVIATION FROM THE PLAN, AND THE EVIDENCE FOR IT.
     The plan writes min-width:520px, the viewBox width, so the drawing renders
     1:1. It was built and looked at, and it produces two defects the plan did
     not foresee:

       - 520px leaves 348 of 555 scrollable px on screen, so 62.7% of the
         drawing is visible at 390px. The third room is off screen, the roof
         apex is cut, and the single teaching point the drawing exists for,
         that the LIVING room deliberately has NO alarm, cannot be seen without
         panning. There is no persistent scrollbar on a touch device, so
         nothing signals that panning is possible at all.
       - .alarm-plan is the scroll container, so .alarm-key and .alarm-caption
         pan with the drawing. At full pan (207px) the caption is sliced mid
         word: "ounding sets off all", "orey, and in the". That reads as broken,
         not as scrollable. See pkg-08/shots/after-alarm-panned-390.png.

     340px is the value that satisfies the plan's OWN stated rule, which is
     "clear 11px", with the least collateral damage. Measured: labels at 11.11
     effective px, 92.8% of the drawing on screen at 390px, 27px of pan instead
     of 207px, and every room, the roof and the cross floor interconnect stay
     visible without touching anything. At 320px it is 11.11px and 74.1%
     visible, against 46.7% at 520px. Page level horizontal overflow is zero
     either way, at both widths, on both pages.

     Compare pkg-08/shots/after-alarm-390.png (520) with
     pkg-08/shots/alarm340-390.png (340). To restore the plan's literal value,
     change 340px to 520px on the line below. Nothing else depends on it.

     This engages below roughly a 420px viewport, which is where the container
     first becomes narrower than 340px. From 420px up the drawing already fits
     and nothing scrolls at all. */
  .alarm-plan{
    overflow-x:auto;
    overscroll-behavior-x:contain;
    -webkit-overflow-scrolling:touch;
  }
  .alarm-plan svg{ min-width:340px; }

  /* ---- 3. THE READING HAIRLINE. 2px is invisible on a small screen in bright
     ambient light, and on a page this long it is a genuine reading aid rather
     than an ornament. 3px. z-index:150 and the @supports (animation-timeline)
     gate are untouched; the selector matches the original exactly so source
     order decides it, not a specificity bump. */
  .guide-page .read-progress{ height:3px; }

  /* ---- 4. THE 14px FLOOR, this package's one instance. Sentence case running
     text below 14px gets raised to 14px on mobile; tracked uppercase labels do
     not, because that is the logo's own move. .alarm-caption is a sentence
     ("An example house, not a plan of yours...") and was 13.44px. Colour
     unchanged. */
  .alarm-caption{ font-size:.875rem; }

  /* ---- 5. REDUCED MOTION. Nothing to write, and that is a finding rather
     than an omission. This package adds no transition, no animation and no
     transition-delay: the chips are static, the scroll container is a scroll
     container (scroll-behavior is not inherited and is set only on html, so
     the drawing pans instantly), and the only motion inside this namespace is
     .contents a's existing colour/border transition plus the .alarm-plan
     interconnect and alarm-ring animations, all of which are already covered
     by the global reduce rule at styles.css:93-96 and the explicit
     .alarm-plan rules at styles.css:2113-2120. Verified with
     Emulation.setEmulatedMedia, not assumed. */

}

/* ==========================================================================
   AUDITED AND DELIBERATELY LEFT ALONE (measured at 390 and 320, both pages).
   Written down because a decision that is not recorded reads as an omission.

   .sec-head              rhythm holds. Section padding 36.8px top and bottom;
                          measured gap above each of the six heads is 55.4 /
                          73.6 / 111.4 / 73.6 / 85.8 / 85.8px. No collision, no
                          change.
   .situations summary    65.3px tall, already past 44. The chevron's box
                          measures 18.4px and sits inside that row. No change,
                          and no :active here: that belongs to frag-05.
   .sources / .standards  single column below 760px already; source li 14.56px,
                          standards dd 14.56px, both above the floor.
   .source-kind,
   .standards dt          11.2px tracked uppercase. Legitimate fine print,
                          left at size on purpose.
   .coverage / .map-frame single column below 880px, frame capped at 420px and
                          centred. Zero overflow at 390 and 320.
   the drawn map          load-bearing labels pass: MERRIMAC 14.44px effective
                          at 390, YATALA 12.92px. The sub-10px labels
                          (QUEENSLAND / NEW SOUTH WALES 8.36px, PACIFIC OCEAN
                          9.12px, N 9.88px, M1 10.64px) are geographic
                          furniture whose content is restated in real text
                          immediately below in .map-note and .reach, so nothing
                          is lost at size. Enlarging hand placed labels inside
                          a hand composed drawing risks collisions the drawing
                          was tuned to avoid. Left alone.
   .reach                 170.9px rows, k 11.84px tracked caps, v 18.4px, p
                          14.88px. Nothing under the floor.
   .era / .eras           stacked below 760px, h3 16.32px, p 15.36px. Fine.
   suburb list            there isn't one, by design: "There is no suburb list
                          on this page, because Tom does not work off one."
   ========================================================================== */

/* ---------- frag-07-reveal ---------- */
/* frag-07-reveal.css: the reveal layer retuned for mobile: shorter reveals, nothing staggers, a hero that finishes in 0.64s.
   APPEND AFTER: frag-08-guide   (§3 concat position. No true selector dependency, every namespace touched here is disjoint from the other seven fragments; what matters is only that it lands after the shipped rules at styles.css:1672-1704 and :1984-1997, which every appended fragment does.)
   OWNS: html.js-motion [data-reveal], html.js-motion [data-reveal-group], html.js-motion [data-draw], html.js-motion .hero-logo / .hero h1 / .hero-sub / .hero-actions, and the bare [data-reveal] / [data-reveal-group] / [data-draw] / [data-tilt] / [data-magnet] / [data-magnet-label] attribute selectors
   TOUCHES NOTHING ELSE. */

/* ==========================================================================
   THE TWO DEFECTS THIS FIXES, both mobile-only.

   1. The hero choreography is 1.02s long and the call button is LAST. The
      element carrying the phone number (.hero-actions, styles.css:1704) does
      not finish arriving until 1.02 seconds after paint. On a triage site for
      someone standing in a dark house holding a phone, that is a defect and
      not a flourish. Retimed to 0.64s total, call button complete at 640ms.

   2. [data-reveal-group]'s 70ms-per-child stagger (styles.css:1690-1695) is
      written for a multi-column grid. At 390px every grid on this site is one
      column, so the ladder degrades into six sequential fades down a single
      column, the saturated default that reads as a template. On mobile,
      nothing staggers. A group arrives as one block.

   WHAT IS DELIBERATELY NOT TOUCHED:
   - @keyframes current-rise's 18px translate. It is shared with desktop.
   - The 55ms per-path [data-draw] step. It is set by site.js as --draw-delay.
   - .hero-media. html.js-motion .hero-media at styles.css:2324 is a documented
     LCP guard.
   - [data-tilt] / [data-magnet]. Already inert on mobile, twice over. See §4.
   ========================================================================== */

@media (max-width:640px){

  /* ---- 1. SECTION REVEALS, shorter, smaller, same idea ----
     620ms -> 340ms, 14px -> 8px. On a phone a reveal should read as the page
     settling, not as an event; a 14px lift over 620ms on a 390px column is a
     performance, and the visitor is here to find a phone number.

     Both rules are EQUAL SPECIFICITY to the originals at styles.css:1672-1680,
     so source order alone decides and this fragment is appended after them.
     The move is on `translate`, never `transform`, styles.css:1664-1671
     documents by name why writing it the other way round silently kills the
     tilt layer, and that reasoning holds identically here. */
  html.js-motion [data-reveal]{
    translate:0 8px;
  }
  html.js-motion [data-reveal].is-in{
    transition:opacity 340ms var(--ease-out-quint),translate 340ms var(--ease-out-quint);
    transition-delay:var(--reveal-delay,0ms);
  }

  /* ---- 2. NOTHING STAGGERS ON MOBILE ----
     ALL SIX nth-child SELECTORS ARE RESTATED EXPLICITLY AND THAT IS THE WHOLE
     TRICK. The originals at styles.css:1690-1695 are (0,4,1); the group rule
     immediately above is (0,3,1), so a single `> *` override would be one
     class short and every child would keep its shipped delay. Only an exact
     selector match at equal specificity, landing later in source order, wins.
     Do not collapse these six lines into one. */
  html.js-motion [data-reveal-group] > *{
    translate:0 8px;
  }
  html.js-motion [data-reveal-group].is-in > *{
    transition:opacity 340ms var(--ease-out-quint),translate 340ms var(--ease-out-quint);
  }
  html.js-motion [data-reveal-group].is-in > *:nth-child(1),
  html.js-motion [data-reveal-group].is-in > *:nth-child(2),
  html.js-motion [data-reveal-group].is-in > *:nth-child(3),
  html.js-motion [data-reveal-group].is-in > *:nth-child(4),
  html.js-motion [data-reveal-group].is-in > *:nth-child(5),
  html.js-motion [data-reveal-group].is-in > *:nth-child(n+6){
    transition-delay:0ms;
  }

  /* ---- 3. THE HERO, RETIMED ----
     1.02s -> 0.64s total. The call button completes at 640ms instead of
     1020ms, which is the entire point of this package.

       logo   460ms + 0ms   = 460ms
       h1     500ms + 40ms  = 540ms
       sub    500ms + 90ms  = 590ms
       actions 500ms + 140ms = 640ms   <- the phone number

     ONLY duration and delay are overridden. The `animation` shorthand at
     styles.css:1701-1704 keeps supplying `current-rise`, `--ease-out-expo`
     and `both`, so the keyframe is untouched and the fill mode still makes
     this an enhancement rather than a gate: no JS, dead JS or a failed
     observer and the hero is simply there, exactly as it is today. */
  html.js-motion .hero-logo{animation-duration:460ms;animation-delay:0ms}
  html.js-motion .hero h1{animation-duration:500ms;animation-delay:40ms}
  html.js-motion .hero-sub{animation-duration:500ms;animation-delay:90ms}
  html.js-motion .hero-actions{animation-duration:500ms;animation-delay:140ms}

  /* ---- 4. [data-tilt] / [data-magnet]: NOTHING IS ADDED, deliberately ----
     Both are already inert on a phone twice over: the CSS lives inside
     @media (hover:hover) and (pointer:fine) (styles.css:1874) and site.js
     gates the JS on the same matchMedia. Verified at 390px rather than
     assumed, every [data-tilt] computes transform:none, and no [data-magnet]
     takes .is-pulling under a synthetic pointer move. The mobile answer to
     hover is P05's press feedback, not a substitute gesture invented here. */

  /* ---- 5. THE DRAWN DIAGRAMS, SHORTER ----
     11 draw targets on safety.html and 13 on service-areas.html, each stroke
     running 1.15s. On a phone that is a long sequence for a diagram the
     visitor may well scroll straight past before it finishes.

     DURATION ONLY. The per-path step (55ms for strokes, 380ms + 70ms for
     fades) is set by site.js as --draw-delay and is not this package's to
     touch, so the sequence keeps its rhythm and only each stroke gets
     quicker. Equal specificity to styles.css:1988-1997, later in source. */
  html.js-motion [data-draw].is-in [data-draw-stroke]{animation-duration:780ms}
  html.js-motion [data-draw].is-in [data-draw-fade]{animation-duration:480ms}

}

/* ---- 6. REDUCED MOTION ----
   The block at styles.css:2112-2130 was checked property by property against
   everything this package touches, and it covers almost all of it. Measured
   under Emulation.setEmulatedMedia prefers-reduced-motion:reduce at 390px:

     hero (:2116-2119)  animation-name none, opacity 1, transform none   COVERED
     [data-draw] stroke (:2123)  dash-offset 0px, dash-array none        COVERED
     [data-draw] fade (:2124)    opacity 1                               COVERED
     [data-tilt]/[data-magnet] (:2127-2129)  transform none              COVERED
     [data-reveal] (:2121-2122)  opacity 1  COVERED / transform none  COVERED
                                 translate  NOT COVERED  <- the one gap

   THE GAP IS REAL AND IT PREDATES THIS PASS. :2122 resets `transform`, but
   the reveal layer deliberately moves on `translate`, styles.css:1664-1671
   spells out why, and it is right. So the reduce rule names a property the
   reveal does not use, and every unobserved [data-reveal] sits at its full
   offset with reduced motion on. Measured on the UNTOUCHED stylesheet before
   this fragment existed: translate 0px 14px on [data-reveal] and 0px 16px on
   [data-reveal-group] > *, at 390px AND at 1440px.

   WHY IT MATTERS RATHER THAN BEING COSMETIC. The global rule at :95 clamps
   every transition to 0.01ms. It does not remove the transition, so with
   reduce on, an element does not fade up, it JUMPS its full offset the
   instant the observer reaches it. A sudden positional snap on every section
   as you scroll is the exact thing prefers-reduced-motion exists to prevent,
   and it is worse than the motion it replaced. :2122's `transform:none` was
   plainly meant to stop this; it just names the wrong property.

   Fixed here for mobile only, because §2.6 rule 1 requires every declaration
   in this fragment to sit inside a max-width query. THE DESKTOP HALF OF THIS
   BUG IS STILL LIVE and is not in any package's namespace, see the note to
   the orchestrator in the P07 report. */
@media (max-width:640px) and (prefers-reduced-motion:reduce){
  html.js-motion [data-reveal],
  html.js-motion [data-reveal-group] > *{translate:none!important}
}

/* ---------- frag-11-hoverfix ---------- */
/* frag-11-hoverfix.css. Neutralise sticky :hover on touch, and the parked amber under reduce.
   APPEND AFTER: frag-10, frag-06, frag-09, frag-08, frag-07   (and BEFORE frag-05, see below)
   OWNS: the four ungated :hover rules at styles.css 269, 1193, 1427, 1818, plus
         .creds::after under prefers-reduced-motion, on mobile only.
   TOUCHES NOTHING ELSE. */

/* WHY THIS FILE EXISTS, and why it is not in any of the ten packages.
   Four :hover rules ship OUTSIDE any (hover:hover) gate. Chrome leaves :hover stuck on the last
   tapped element on a touch screen, so on a phone the RELEASE state is the hover state until you
   tap somewhere else. Measured: the call button gets LIGHTER on release (#FFC463) and stays that
   way. That is the exact inverse of press feedback, and it silently undercuts the whole touch
   layer. P05 owns :active and was forbidden from writing :hover, so it correctly left them alone
   and flagged them instead. P10 then widened .phone-link's tap target to the full column, which
   turns its stuck-hover underline from 103px into 350px, so this got more visible, not less.

   ORDERING IS LOAD BEARING. This fragment must be appended BEFORE frag-05-touch.
   .btn:hover::before and P05's .btn:active::before are both specificity (0,2,1). During a press
   BOTH match, so source order decides: frag-05 must come later or the press colour is lost. After
   release only :hover still matches, so these resets take over and the control returns to rest.

   PROVABLY DESKTOP INERT TWICE OVER: max-width:900px, and (hover:none), which a desktop pointer
   never satisfies. */

@media (max-width:900px) and (hover:none){

  /* Rest value is var(--amber) (styles.css:1786 paints the face). The shipped hover lightens it
     to #FFC463, which is the wrong direction for a press and wrong to leave stuck after one. */
  .btn:hover::before{ background-color:var(--amber); }

  /* Rest value is var(--ink-soft) (styles.css:1189-1192). */
  .foot-grid a:hover{ color:var(--ink-soft); }

  /* Rest value is var(--ink), inherited (styles.css:1417-1426 sets no colour). */
  .situations summary:hover{ color:var(--ink); }

  /* Rest value is a transparent 2px bottom border (styles.css:264-268). P10's .info-list and
     .foot-grid flex rules make this underline span the full column, so a stuck amber rule reads
     as a selected row rather than as a link that was tapped. */
  .phone-link:hover{ border-bottom-color:transparent; }
}

/* Under prefers-reduced-motion the shipped styles.css:2113 kills .creds::after's animation, which
   leaves its 18%-wide amber gradient PARKED at the strip's bottom-left with transform:none, in
   full view. Same artefact P09 fixed on the normal path with animation-fill-mode:forwards; that
   fix cannot reach the reduce path because :2113 removes the animation entirely. Hiding the layer
   is correct here: it is decorative, it carries no information, and nothing else references it.
   The desktop half of this is left alone deliberately, per the mobile-only scope. */
@media (max-width:900px) and (prefers-reduced-motion:reduce){
  .creds::after{ opacity:0; }
}

/* ---------- frag-05-touch ---------- */
/* frag-05-touch.css: the mobile touch feedback layer, the answer to hover.
   APPEND AFTER: frag-07-reveal   (and BEFORE frag-04-callbar, frag-01-nav)
   OWNS: every :active rule on mobile except inside .mobile-nav / .menu-btn,
         plus the bare a, button, summary, [role="button"] rules for
         touch-action and -webkit-tap-highlight-color.
   TOUCHES NOTHING ELSE. */

/* ==========================================================================
   WHY THIS BLOCK EXISTS. Every interactive affordance on this site is gated
   behind `@media (hover:hover) and (pointer:fine)` in CSS and `fine.matches`
   in JavaScript: [data-tilt], [data-magnet], btn-sweep, btn-sweep-ghost,
   .svc-row:hover, .svc-card:hover, .job-grid img:hover, .contents a::after,
   .situations summary:hover. Every single one of them.

   What is left on a phone is `.btn:active{transform:scale(.985)}`, which is
   0.8px of travel on a 52px button, sitting next to
   `-webkit-tap-highlight-color:transparent`, which deletes the browser's own
   flash. The net result is that tapping the phone number, the most tapped
   control on the site, on a site whose entire job is to be phoned, produces
   literally no acknowledgement at all. This block is the whole mobile
   feedback layer.

   THE PRESS RIDES `scale`, NOT `transform`, AND THAT IS LOAD BEARING.
   styles.css:1853-1857 documents this hazard by name for exactly these
   elements: the call buttons carry [data-magnet], whose lean lives on
   `transform`, and a press written as `transform:scale()` at higher
   specificity replaces the lean instead of composing with it. `scale` is an
   independent property, so the two systems never touch. Same reasoning as
   the reveals moving to `translate` at styles.css:1664-1671.
   ========================================================================== */

@media (max-width:900px){

  /* ---- 1. FILLED BUTTONS ----------------------------------------------
     .97 on a 52px button is 1.6px of travel, which is felt through a
     fingertip. .985 is 0.8px, which is not. #D9922B is the amber's own dark
     stop, already on the page in the mobile hero bolt gradient
     (styles.css:2299), no new colour enters the palette.

     Since the 4 Aug rebuild the filled button's face is painted by
     `.btn::before` (inset:0, z-index:-1, background-color:var(--btn-bg),
     styles.css:1786-1803), so the press colour has to land on the pseudo
     element. Setting it on .btn itself would paint a box behind a
     transparent face and do nothing visible. */
  .btn{
    transition:background-color .16s var(--ease),
               scale 160ms var(--ease-out-quint),
               filter .22s var(--ease);
  }
  .btn:active{
    transform:none;          /* neutralises .btn:active{transform:scale(.985)} at styles.css:255 */
    scale:.97;
    transition-duration:90ms;
  }
  /* SPECIFICITY REPAIR, and the reason this rule is not redundant.
     `.btn[data-magnet]:active` at styles.css:1858 is (0,3,0) and hard-codes
     `scale:.985`. A press rule written only as `.btn:active` is (0,2,0) and
     LOSES to it no matter how late it is appended, on precisely the four
     elements that matter most, because [data-magnet] is only ever on the
     tel: call buttons and the quote CTA beside them. Matching the selector
     is what makes .97 real on the hero call button. The transform is
     restated rather than nulled so the lean still composes on any device
     that reports a fine pointer under 900px. */
  .btn[data-magnet]:active{
    transform:translate3d(var(--mag-x,0px),var(--mag-y,0px),0);
    scale:.97;
    transition-duration:90ms;
  }
  .btn:active::before{
    background-color:#D9922B;
    transition-duration:90ms;
  }

  /* ---- 2. GHOST BUTTONS ------------------------------------------------
     The ghost carries no ::before (display:none since the build), so its
     press colour goes on the element. .14 alpha is one step past the .09
     hover fill it already uses, and the border firming to --steel is the
     second, non-colour cue. */
  .btn-ghost:active{
    transform:none;
    scale:.97;
    background-color:rgba(172,172,172,.14);
    border-color:var(--steel);
  }

  /* ---- 3. TEXT LINKS ---------------------------------------------------
     :active is an active state, which is one of the three sanctioned uses of
     amber. Zero-duration on the way in: a press is an acknowledgement, not
     an animation, and any ramp at all reads as lag on a control the visitor
     is already touching.

     `.mobile-nav a` is excluded because P01 owns every :active inside the
     drawer. `.skip` is excluded because it is a filled amber slab
     (styles.css:105-110), amber text on an amber background is a 1:1
     contrast hole for the length of the press, and the skip link is the one
     control on the site that exists purely for accessibility. */
  a:not(.btn):not(.skip):not(.mobile-nav a):active{
    color:var(--amber);
    transition:color 0s;
  }
  /* Links that are ALREADY amber press to 70% instead, so the accent is
     never doubled and the press is still unmistakable. */
  .link-amber:active,
  .phone-link:active,
  .info-list .v a:active{
    color:var(--amber);
    opacity:.7;
  }

  /* ---- 4. CONSISTENCY --------------------------------------------------
     Today only .btn, .nav-link, .menu-btn and .situations summary carry
     these (styles.css:245, 335, 369, 1422). Everything else, every footer
     link, every inline link, every jump chip, the brand, still eats the
     300ms double-tap delay and the browser's own inconsistent grey flash,
     which now competes with the feedback above instead of substituting for
     it. One grouped rule, no exceptions. */
  a,button,summary,[role="button"]{
    touch-action:manipulation;
    -webkit-tap-highlight-color:transparent;
  }

  /* ---- 5. DISCLOSURE ---------------------------------------------------
     The four <summary> rows on safety.html. The chevron already answers
     "what happened" via [open]; this answers "did my tap land". */
  .situations summary:active{color:var(--amber)}
}

/* ---- 6. REDUCED MOTION -------------------------------------------------
   Colour feedback survives, because colour is not motion. The travel does
   not. The global reduce rule at styles.css:93-96 clamps DURATIONS only, so
   without this the scale still happens, just instantly, which is the one
   thing a person who asked for less motion did not ask for.
   `.btn[data-magnet]:active` is restated for the same specificity reason as
   above: (0,2,0) alone would lose to it and the magnetised call buttons
   would keep scaling. */
@media (max-width:900px) and (prefers-reduced-motion:reduce){
  .btn:active,
  .btn[data-magnet]:active,
  .btn-ghost:active{scale:1}
}

/* ---------- frag-04-callbar ---------- */
/* frag-04-callbar.css: the sticky call bar stops sliding and starts striking
   APPEND AFTER: frag-05-touch   (and before frag-01-nav, which owns html.nav-open .callbar)
   OWNS: .callbar, .callbar-in, .callbar .btn, and the two arithmetic rules below
   TOUCHES NOTHING ELSE.

   NAMESPACE EXEMPTIONS, declared per the plan (section 2.5, P04 items 4 and 5):
   this is the ONLY fragment permitted to write a bare html{...} rule
   (scroll-padding-bottom) and the ONLY one permitted to write
   body{padding-bottom}. Both are arithmetic on the call bar's own measured
   height, which is why they live here and not in frag-10-shell.

   MEASURED, NOT GUESSED. Rendered .callbar height at 390x844 with
   env(safe-area-inset-bottom) = 0 is 83.328125px (9.6 pad + 63.14 button
   + 9.6 pad + 1 border), and it is the same 83.328125px at 320 and at 899,
   because the bar is one full-width button. Under a notched-iPhone emulation
   (bottom inset 34px) it measures 117.328125px, i.e. exactly base + inset,
   which is what makes the calc() below correct rather than approximate.
   84px is the ceiling of the measured base. */

@media (max-width:900px){

  /* ---- 1. THE RISE. -------------------------------------------------------
     Not a slab sliding up. 380ms on --ease-out-expo, which spends most of its
     travel in the first third and then settles, so the bar arrives rather than
     glides. Exit is 285ms, 75% of the enter, per the contract. The face closes
     with it (240ms, no delay) but is off screen almost immediately, so leaving
     reads as one movement rather than as a second event.

     transform only. z-index:120, display, and the fail-safe direction (the bar
     is VISIBLE by default and .is-hidden is the exception) are untouched, which
     is the whole reason a dead observer still leaves a phone number on screen. */
  .callbar{ transition: transform 380ms var(--ease-out-expo); }
  .callbar.is-hidden{ transition-duration: 285ms; }

  /* ---- 2. THE STRIKE. -----------------------------------------------------
     90ms into the rise, the amber face wipes in left-to-right over 320ms along
     the button's OWN shear. No new geometry: the open polygon is the shipped
     clip-path at styles.css:1800, and the closed one collapses it onto the
     sheared edge itself. Four vertices both ways, same order, so it
     interpolates as a sweep instead of snapping (measured: the wipe passes
     through 0%, 43.1%, 77.6%, 95.8% and 99.98% on its way open).

     The OPEN clip stays the default and is never restated here: if the
     IntersectionObserver never runs, the class never lands, and the face is
     fully drawn. background-color and filter keep their shipped --ease timings
     so the press feedback and the drop-shadow are not silently re-timed. */
  .callbar .btn::before{
    transition: clip-path 320ms var(--ease-out-expo) 90ms,
                background-color .16s var(--ease),
                filter .22s var(--ease);
  }
  .callbar.is-hidden .btn::before{
    clip-path: polygon(var(--btn-run) 0, var(--btn-run) 0, 0 100%, 0 100%);
    transition-duration: 240ms;
    transition-delay: 0s;
  }

  /* ORDERING NOTE, one line, safe to delete if the orchestrator disagrees.
     frag-04 lands after frag-05 on purpose, so this package wins on
     .callbar .btn. The side effect the plan did not price in: P05's
     .btn:active::before{transition-duration:90ms} has specificity (0,2,1),
     exactly equal to .callbar .btn::before, so the shorthand above would
     otherwise stretch the call bar button's press feedback from 90ms back to
     160ms. This restores P05's intent inside P04's own namespace. Inert if
     frag-05 never lands. */
  .callbar .btn:active::before{ transition-duration: 90ms; }

  /* ---- 3. SAFE AREA. ------------------------------------------------------
     Nothing to add. .callbar's padding already reads
     calc(.6rem + env(safe-area-inset-bottom)) (styles.css:1607); it has simply
     never resolved to anything but 0 because no page carried viewport-fit=cover.
     Verified live: with the meta present and a 34px bottom inset the bar's
     padding-bottom computes to 43.6px and its height to 117.33px. */

  /* ---- 4. THE ARITHMETIC. -------------------------------------------------
     body{padding-bottom:84px} (styles.css:1616) is a flat number that does not
     know about the notch, so the bar grows by the inset and the padding does
     not. Measured at maximum scroll with a 34px inset: the last footer line
     goes from 33px of clearance to 1px UNDERNEATH the bar. The base height is
     the constant and the inset is added, never baked in: baking the notched
     measurement into the number would double-count the inset on the phones
     that have one and over-pad every phone that does not. */
  body{ padding-bottom: calc(84px + env(safe-area-inset-bottom)); }

  /* An anchor jump or a scrollIntoView must not land its target under the bar.
     84 + 16, the same 16px breathing room the shipped
     scroll-padding-top:calc(var(--header-h) + 16px) uses at the other end. */
  html{ scroll-padding-bottom: calc(100px + env(safe-area-inset-bottom)); }
}

/* ---- 5. REDUCED MOTION. ---------------------------------------------------
   The bar still appears and disappears; it just does not travel, and the face
   is never clipped. The global *{transition-duration:.01ms!important} at
   styles.css:95 clamps durations but not delays, and a 90ms delay on a 0.01ms
   wipe is still a visible stutter, so this kills the transitions outright. */
@media (max-width:900px) and (prefers-reduced-motion:reduce){
  .callbar{ transition: none !important; }
  .callbar .btn::before{
    clip-path: polygon(var(--btn-run) 0, 100% 0, 100% 100%, 0 100%) !important;
    transition: none !important;
  }
}

/* ---------- frag-01-nav ---------- */
/* frag-01-nav.css: the mobile navigation drawer: bolt-edged clip-path wipe, scrim, no-JS fallback, close icon
   APPEND AFTER: frag-10, frag-06, frag-09, frag-08, frag-07, frag-05, frag-04   (this fragment is LAST)
   OWNS: .mobile-nav, .mobile-nav a, .mobile-nav > nav, .nav-scrim, .menu-btn, .menu-ico, .menu-ico-t/-m/-b,
         and every rule whose selector begins html.nav-open (html.nav-open body, html.nav-open .callbar)
   TOUCHES NOTHING ELSE. */

/* ==========================================================================
   THE MOBILE MENU IS A TOP DRAWER WHOSE BOTTOM EDGE IS THE LIGHTNING BOLT.

   Everything below is mobile-only by construction: the whole file lives inside
   @media (max-width:1000px), which is the hamburger breakpoint (styles.css:435,
   where .menu-btn stops being display:none). Above it the base rule
   .mobile-nav{display:none} is untouched, so desktop cannot see any of this.

   WHY A DRAWER AND NOT A FULL-HEIGHT SHEET. A sheet would be the safe choice
   and it deletes the one thing a person triaging in a dark house needs: the
   sense that the page they came from is still there behind them. It would also
   cover the sticky call bar, which is the site's persistent promise. So the
   drawer is sized to its own content, sits on a scrim, and the call bar is
   deliberately left on top of it (html.nav-open .callbar, below).

   THE EDGE IS ONE SHAPE CUT TWICE, exactly as the hero panel does it
   (styles.css:2155-2159): an amber layer clipped by the outer polygon, and a
   --surface layer clipped by the same polygon pulled up by the 6px line
   thickness. The amber that shows is the difference between them, so the bolt
   can never come adrift from the panel the way two drawn shapes would. The
   profile itself is the mobile hero panel's own edge (styles.css:2300)
   mirrored to a bottom edge, expressed in px so it does not stretch or flatten
   as the drawer's height changes.

   THE ENTER ANIMATION IS THE WIPE, AND IT REPLACES A PER-LINK STAGGER. The six
   links are painted at full opacity behind the clip and are revealed
   physically, top to bottom, by the descending edge. Because that edge is a
   bolt rather than a level line, the two halves of each link arrive a few
   milliseconds apart on their own. There is no --i, no nth-child delay chain,
   no fade-and-rise. A 390px column turns any sibling stagger into a queue.

   AND THE PANEL IS NEVER display:none. visibility + clip-path is what makes
   the wipe possible at all (display cannot transition), and visibility:hidden
   already removes the panel from the tab order and the accessibility tree, so
   no inert, no <dialog>, no popover API is needed on it.
   ========================================================================== */

@media (max-width:1000px){

  /* ==== 1. THE NO-JS FALLBACK. This comes first on purpose. ================
     With JavaScript off, the six links are a plain visible list under the
     header. That is the honest reading of "fully usable with JS disabled":
     today a phone visitor with no JS can only navigate via the footer, which
     on the 16,570px safety page is not navigation, it is an expedition.
     Above 1000px the base display:none still wins, so desktop is unchanged. */
  .mobile-nav{
    display:block;
    position:static;
    border-bottom:1px solid var(--line);
  }
  /* ORCHESTRATOR ADDITION (merge time), the loose end P01 flagged and correctly
     declined to take unilaterally. With JS off the list above is already open,
     so the hamburger is a control that does nothing. Hide it rather than leave
     a dead affordance sitting over a visible list. Inert whenever JS runs,
     because the head script sets has-js before first paint. */
  html:not(.has-js) .menu-btn{ display:none; }

  /* ==== 2. THE DRAWER ====================================================
     Everything in this section is prefixed html.has-js, which the inline head
     script sets before first paint. So the drawer only ever becomes a closed
     drawer once something is running that can open it again. */
  html.has-js .mobile-nav{
    --nav-bolt-w:6px;              /* the amber line thickness the two polygons differ by */
    position:fixed;top:var(--header-h);left:0;right:0;z-index:99;
    /* the straight hairline goes; the bolt is the bottom edge now */
    border-bottom:0;
    /* THE AMBER LAYER. It is only ever visible along the bolt, because the
       --surface layer on ::before covers everything except the 6px band. */
    background:linear-gradient(160deg,#FFC463,var(--amber) 45%,#D9922B);
    clip-path:polygon(0 0, 100% 0, 100% 6px, 54% 0px, 46% 6px, 0 0px);
    visibility:hidden;
    /* THE PANEL MUST NOT BE THE SCROLL CONTAINER. The base rule makes it one
       (max-height + overflow-y:auto, styles.css:402-403). An absolutely
       positioned ::before inside a scroller scrolls away with the content and
       the two halves of the shape come apart; and a max-height smaller than
       the content would put the clip's 100% at the wrong place and cut the
       last links off. The scroller is the <nav> inside instead. */
    max-height:none;
    overflow:visible;
    transition:clip-path 315ms var(--ease-out-quint),visibility 0s linear 315ms;
  }
  /* the --surface layer: the same shape pulled up by --nav-bolt-w */
  html.has-js .mobile-nav::before{
    content:"";position:absolute;inset:0;z-index:0;
    background:var(--surface);
    clip-path:polygon(0 0, 100% 0, 100% 0px, 54% 0px, 46% 0px, 0 0px);
    transition:clip-path 225ms var(--ease-out-quint);
  }
  /* THE SCROLLER. 104px is reserved for the call bar so the last link can
     never sit under it on a short landscape viewport; the 34px of bottom
     padding is what keeps that link clear of the bolt at maximum scroll.
     100vh first as the fallback for anything without dvh.

     ORCHESTRATOR CORRECTION (merge time). The flat 104px was too small once
     P03's viewport-fit=cover went live. P04 and P10 independently measured the
     call bar at 83.33px with no inset but 117.33px with a 34px bottom inset,
     and 117.33 > 104, so on a notched phone in landscape the last link would
     have sat under the bar. The clearance is 291.5px at 390x844 but only
     20.7px at 390x390, so landscape is the case with no slack. The inset is
     added rather than baked in, matching P04's own body{padding-bottom}
     arithmetic: the base height is the constant, env() is the variable. */
  html.has-js .mobile-nav > nav{
    position:relative;z-index:1;
    max-height:calc(100vh - var(--header-h) - 104px - env(safe-area-inset-bottom));
    max-height:calc(100dvh - var(--header-h) - 104px - env(safe-area-inset-bottom));
    overflow-y:auto;overscroll-behavior:contain;
    padding-block:8px 34px;
    /* THE DESCENT, added 5 Aug 2026 on Will's note that the menu "just appears".
       The clip-path wipe alone uncovers links that never move, so the bolt edge
       travels but the menu does not: at 150ms of a 300ms ease-out-quint the wipe
       was already 79% done, which is why two screenshots 150ms apart looked
       identical. The panel now also DROPS. The links start 18px high and settle
       as the bolt edge descends past them, so the reveal and the travel are one
       gesture instead of a mask sliding over static text.

       Reference: 21st.dev "Animated Dropdown" (Shatlyk1011), whose Framer recipe
       is y:-10 -> 0 with opacity and scale over 200ms easeOut, plus a 30ms child
       stagger. The descent is taken. The opacity and scale are not: the clip is
       already doing the appearing, and fading content while uncovering it reads
       as two competing ideas. The stagger is not: a 390px column has exactly one
       item per row, so a sibling stagger degrades into a queue, which is the
       documented mobile policy in this file.

       translate, not transform, per the convention at styles.css:1664-1671, so
       it composes with anything the motion layer sets rather than replacing it. */
    translate:0 -18px;
    transition:translate 315ms var(--ease-out-quint);
  }
  /* OPEN. The visibility 0s linear 225ms on the closed rule and the plain
     visibility 0s here is the whole trick: on the way in the panel is visible
     immediately and the clip wipes it open; on the way out the clip wipes shut
     for 225ms and only then does visibility drop. */
  html.has-js.nav-open .mobile-nav{
    visibility:visible;
    clip-path:polygon(0 0, 100% 0, 100% 100%, 54% calc(100% - 29px), 46% calc(100% - 10px), 0 calc(100% - 34px));
    transition:clip-path 420ms var(--ease-out-quint),visibility 0s;
  }
  html.has-js.nav-open .mobile-nav::before{
    clip-path:polygon(0 0, 100% 0, 100% calc(100% - 6px), 54% calc(100% - 35px), 46% calc(100% - 16px), 0 calc(100% - 40px));
    transition:clip-path 420ms var(--ease-out-quint);
  }
  /* The links ride the same curve as the edge that reveals them, so the two
     read as one gesture. 420ms open / 315ms close keeps the exit at 75% of the
     entrance, the rule every other moment in this file follows. 300ms was
     inside the spec band but too quick to register on a phone: it is the
     difference between a menu that drops and a menu that is simply there. */
  html.has-js.nav-open .mobile-nav > nav{
    translate:0 0;
    transition:translate 420ms var(--ease-out-quint);
  }

  /* ==== 3. THE LINKS =====================================================
     padding:1rem 0 and font-size:1.05rem are inherited from the base rule and
     deliberately unchanged (measured 60.7px today). min-height is a floor for
     the case where the variable font never arrives, not a new size.
     Not prefixed html.has-js: a 44px+ target and a marked current page are
     right in the no-JS list too. */
  .mobile-nav a{
    position:relative;
    display:flex;align-items:center;
    min-height:56px;
    color:var(--ink-soft);
    touch-action:manipulation;-webkit-tap-highlight-color:transparent;
    transition:background-color 160ms var(--ease-out-quint);
  }
  /* the release eases out over 160ms; the press itself must be instant */
  .mobile-nav a:active{
    background:rgba(172,172,172,.10);
    transition-duration:0s;
  }
  /* YOU ARE HERE, said twice: weight and shape, never colour alone.
     --ink #F4F4F2 on --surface #121214 is 18.6:1 against --ink-soft's 11.0:1,
     and the sheared amber tab is the same construction as .svc-row::before
     (styles.css:2098) and the same answer the desktop .nav-link gives
     (styles.css:347), so the two navs agree about what "current" looks like.
     The negative left offset is the .wrap's own gutter, so the tab sits flush
     to the screen edge rather than to the text column. */
  .mobile-nav a[aria-current="page"]{color:var(--ink)}
  .mobile-nav a[aria-current="page"]::before{
    content:"";position:absolute;
    left:calc(-1 * clamp(20px,5vw,44px));
    top:12px;bottom:12px;width:4px;
    background:var(--amber);
    transform:skewX(calc(var(--shear) * -1));
  }

  /* ==== 4. THE SCRIM =====================================================
     rgba(8,8,9,.72) is --sunk at 72%. NO BLUR, and that is a decision:
     a full-viewport backdrop-filter is the most expensive thing you can put on
     a mid-range Android, this audience is defined by mobile data and a dying
     battery, and the page behind is already near-black so a blur buys almost
     nothing. It starts below the header because the close button lives there
     and the header must stay crisp. Created once by site.js. */
  .nav-scrim{
    position:fixed;top:var(--header-h);left:0;right:0;bottom:0;z-index:98;
    background:rgba(8,8,9,.72);
    opacity:0;visibility:hidden;
    touch-action:none;-webkit-tap-highlight-color:transparent;
    transition:opacity 180ms var(--ease-out-quint),visibility 0s linear 180ms;
  }
  html.nav-open .nav-scrim{
    opacity:1;visibility:visible;
    transition:opacity 240ms var(--ease-out-quint),visibility 0s;
  }

  /* ==== 5. WHAT BEING OPEN DOES TO THE PAGE ==============================
     overflow:hidden on the scroll container, and nothing else. NOT
     position:fixed on body: body{overflow-x:hidden} propagates to the viewport
     (html's overflow is visible), so the whole-page scroll IS the viewport
     scroll and .site-header{position:sticky} sticks against it. Make body
     fixed and the header's scrollport disappears, so the sticky header drops
     to the document top and vanishes off-screen the moment the menu opens.
     overflow:hidden preserves scrollTop by definition, so there is also
     nothing to save and nothing to restore.

     THE CALL BAR STAYS ON TOP OF THE OPEN MENU, DELIBERATELY. Someone who
     opens the menu and then realises they just want to ring him must not have
     to close it first. Forcing it visible matters most at the top of the page,
     where the observer has hidden the bar because the hero call button is on
     screen, behind an open menu that button is not reachable, so without this
     line there would be no call action at all. Specificity 0,2,1 beats
     .callbar.is-hidden at 0,2,0, so no !important is needed.

     ORCHESTRATOR CORRECTION (merge time). This rule was specified, and written,
     as `html.nav-open, html.nav-open body`. The html half BREAKS THE STICKY
     HEADER and P02 proved it by variant test. body{overflow-x:hidden} only
     propagates to the viewport while the ROOT's overflow is visible; the moment
     html goes overflow:hidden, propagation stops, body becomes its own scroll
     container at scrollTop 0, and the sticky header re-anchors to the document
     top, 1200px above the fold. Measured at scrollY 1200: header rect.top was
     -1200 with the html half, 0 without it, and the page stayed locked at 1200
     either way under a real wheel and a real touch drag. So the header, the
     close button and the X all vanished and only the scrim could close the
     menu. Invisible at scrollY 0, which is why it survived P01's own probe.
     The reasoning above for rejecting position:fixed is right; the conclusion
     that overflow:hidden on BOTH is safe was not. */
  html.nav-open body{overflow:hidden}
  html.nav-open .callbar{transform:translateY(0)}

  /* ORCHESTRATOR ADDITION (merge time), from P04's seam test. Forcing the bar
     visible is not enough: it still carries .is-hidden, so P04's closed face
     clip still applies and the forced-visible bar renders with NO amber face
     and near-black text on a near-black bar. P04 photographed a call bar you
     cannot read. That is precisely the failure this site exists to prevent.
     Restoring the open polygon here, in P01's own html.nav-open namespace,
     is P04's verified fix. Inert if frag-04 never lands. */
  html.nav-open .callbar.is-hidden .btn::before{
    clip-path:polygon(var(--btn-run) 0, 100% 0, 100% 100%, 0 100%);
  }

  /* ==== 6. THE HAMBURGER BECOMES A CLOSE ICON ============================
     Three <path> elements with the coordinates unchanged from the single path
     they replaced, so the closed state is byte-identical to what shipped. The
     outer two translate to the centre and rotate +/-45deg about it; the middle
     collapses on scaleX and opacity.

     THE MIDDLE BAR IS SEQUENCED, NOT SIMULTANEOUS: it collapses first on the
     way in (no delay, while the outer bars wait 70ms) and returns last on the
     way out (80ms delay, after the outer bars have already unfolded). Three
     bars becoming an X in one frame reads as a swap; done in order it reads as
     one thing folding. */
  .menu-ico path{
    transform-box:view-box;
    transform-origin:12px 12px;
  }
  .menu-ico-t,.menu-ico-b{
    transition:transform 175ms var(--ease-out-quint) 0s;
  }
  .menu-ico-m{
    transition:opacity 150ms linear 80ms,transform 150ms var(--ease-out-quint) 80ms;
  }
  html.nav-open .menu-ico-t{
    transform:rotate(45deg) translateY(6px);
    transition:transform 230ms var(--ease-out-quint) 70ms;
  }
  html.nav-open .menu-ico-b{
    transform:rotate(-45deg) translateY(-6px);
    transition:transform 230ms var(--ease-out-quint) 70ms;
  }
  html.nav-open .menu-ico-m{
    opacity:0;transform:scaleX(0);
    transition:opacity 120ms linear 0s,transform 150ms var(--ease-out-quint) 0s;
  }
  /* The button that owns all of the above is the one control on the mobile
     header with no press feedback at all, and P05's touch layer is forbidden
     from writing inside this namespace. Same treatment as the links. */
  .menu-btn{
    -webkit-tap-highlight-color:transparent;
    transition:background-color 160ms var(--ease-out-quint);
  }
  .menu-btn:active{
    background:rgba(172,172,172,.10);
    transition-duration:0s;
  }
}

/* ==== 7. REDUCED MOTION ==================================================
   The global *{transition-duration:.01ms!important} at styles.css:95 already
   clamps every duration here, but it does NOT clamp delays. Without this the
   drawer would sit fully visible for 225ms after its clip had already snapped
   shut, and the middle bar of the icon would come back 80ms after the X had
   already gone. Delays are the only thing this needs to say. */
@media (max-width:1000px) and (prefers-reduced-motion:reduce){
  .mobile-nav,
  .mobile-nav::before,
  .nav-scrim,
  .menu-ico path{
    transition-delay:0s !important;
  }
}

/* ---------- orchestrator: one miss caught by the post-merge sweep ---------- */
/* .map-note on service-areas.html is sentence-case running text ("Schematic, not
   drawn to scale...") rendering at 13.12px (.82rem). The readability policy fixes
   sentence-case running text below 14px on mobile and leaves tracked uppercase
   labels and .foot-meta fine print alone. .map-note is neither: it is a real
   sentence, and it is the text that carries the map's sub-10px geographic labels
   in readable form, so it is the last thing that should be small. It sat outside
   every package's declared namespace, which is why nobody owned it. */
@media (max-width:900px){
  .map-note{ font-size:.875rem; }
}
