/* ===========================================================================
   TRI-COUNTY COLLISION, SHARED STYLESHEET.

   ---------------------------------------------------------------------------
   THE DESIGN SYSTEM. DECIDED 2026-09-06. PAGES 2 THROUGH 37 INHERIT IT.

   Three directions were built as complete pages and compared by scrolling.
   The pick is PHOTOGRAPHIC, and it is not up for re-deciding on the next
   page. What that commits every page to:

   1. HERO: a full-bleed photograph with an OPAQUE PANEL over it holding the
      copy. Opaque is the rule and it is about the PHOTOGRAPH: no wash, no
      translucency, nothing that lets the image through, because type over a
      photograph is only reliably readable when something solid is behind it.
      The photograph is a real one of this shop, its people or its work.

      THE PANEL'S GROUND IS OXBLOOD AS OF 2026-09-10. It was ink until then.
      It now carries .field-ox, the same ground the two act bands carry,
      including the recorded 104 degree gradient, under the palette law's
      written exception for section backgrounds. It is still opaque; a
      gradient between two solid stops is not a wash.

      WHY IT IS THE SAME CLASS RATHER THAN A COPY OF IT. The act-button rule
      is keyed to .field-ox, so the hero's Call button became ink-filled with
      silver text and the silver hairline on its own, and the Email ghost stayed
      a silver outline, with nothing written for the hero specifically. One
      class, one ground, one button grammar. A hand-copied gradient would
      have needed its own button rule and would have drifted from the bands
      the first time either changed.

      THE SUPPORTING COLOURS WERE RE-MEASURED AGAINST THE GRADIENT'S LIGHTEST
      STOP, which is --ox and therefore the worst case:

          h1, eyebrow        --silver     10.50
          lead, breadcrumb   --silver-2    7.34
          breadcrumb rule    --rule        7.20
          chip border        see .badges, re-tuned from .42 to .50

      Text needs 4.5 and the chip border needs 3 as a shape. Everything
      cleared without changing a token; only the chip border's alpha moved,
      and only to hold the strength it already had.

   2. THE PANEL IS FLUSH TO THE LEFT VIEWPORT EDGE. No gap, no float, no
      rounded corner. Its LEFT PADDING equals the page grid's left margin, so
      the type inside it lines up with every other section's first character
      all the way down the page. At wide viewports the panel GROWS with the
      margin rather than detaching from the edge: it is pinned to the glass,
      and only its content column is centred. See .heroA-panel for the sum.

   3. STAT BAND directly under the hero, on the silver ground. THREE stats,
      big numeral, label, one supporting line. Since 2026-10-12 the founding
      year is one of them (3.98); their order is the band's DOM order, Greg's
      ruling of 2026-10-20 (3.100), the order a visitor asks. 3.98 took the
      band to four across; Greg's eye ruled it out the same day (3.99), and the brands stat left, because the
      brand strip makes it a same-page duplicate. Inventing a statistic to
      balance a row is still exactly the failure the prime law exists to
      stop. Any number that is true only on the day it is written CARRIES
      THE DATE IT WAS TRUE.

   4. TYPE: Archivo Black for display, Source Sans 3 for everything read at
      length. Both self-hosted, both SIL OFL 1.1, licence texts beside them
      in assets/fonts/. Nothing reaches a webfont CDN at runtime. (AMENDED
      2026-10-27, proposed-changes.md 3.142: at cutover exactly two
      third-party scripts load, GA4 and CallRail, written by
      scripts/sync-chrome.py. Nothing else reaches a CDN.) Archivo
      Black has ONE weight and it is enormous, so it takes h1, h2, h3 and the
      step numerals and nothing smaller; every label, subhead and run of body
      copy is Source Sans 3, which is variable and covers 200 to 900 from one
      file. Never ask for a weight Archivo Black does not have: the browser
      will synthesise it and it looks like a mistake.

   5. INK STICKY HEADER, with the slim ink staging bar above it until
      cutover. The call button keeps its oxblood fill and gains a paper
      border. See the contrast note below for why the border is not optional.

   6. THE LIFT, ADOPTED 2026-09-10. A MOLD BEHAVIOUR: every card on every
      page gets it, and nobody re-decides it per page. A card rises 3px
      under a pointer, an ink shadow comes up under it, and the oxblood
      rule along its base sharpens and sweeps to the card's full width.
      The lift is generic on purpose, because feedback to a hand should
      feel familiar; THE MATERIALS ARE WHAT MAKE IT THIS SITE'S. The
      default is a soft grey glow that belongs to nobody. This shadow is
      --ink, so a card casts a shadow the colour of this site's own
      darkest value, and the response is --ox. See the lift section for
      the mechanism and its two constraints. Under a pointer the step
      numeral goes solid oxblood on the same clock, so a card answers as
      one gesture. That colour change is the ONE property outside
      transform, opacity and the rule's width that this file animates.

   7. ARRIVAL MOTION, ADOPTED 2026-09-10. A lane draws, once, the first
      time its section comes into view. See the motion amendment below
      for what that is allowed to mean and what it is not. The stat
      band's counting effect was withdrawn on Greg's ruling (3.85).

   The palette table, its provenance and its measured ratios follow. Read all
   of it before writing a hex, a font-family or a hero.
   ---------------------------------------------------------------------------

   THE PALETTE LIVES HERE AND NOWHERE ELSE. One file defines it, every page
   reads it. A hex typed into a page is a second palette, and a second
   palette drifts. The two sanctioned duplicates are the theme-color meta
   tag, because a meta tag cannot read a custom property, and inline SVG
   attributes, because those cannot either. Both are noted where they sit.

   ---------------------------------------------------------------------------
   WHERE THESE COLORS CAME FROM. They were sampled from the shop's own live
   WordPress site on 2026-09-05, not chosen. The theme declares two of them
   outright in wp-content/themes/tricounty/style.css:

       --primary-color: #121b27      --secondary-color: #691c17

   and the compiled layout CSS uses #691c17 a hundred and forty times against
   ninety-eight whites, which is what makes oxblood the shop's color rather
   than an accent. #E92424 was decoded out of the logo PNG's own palette, so
   the mark's red is the mark's actual red and not a lookalike.

   THE GROUND IS THE LOGO'S SILVER NOW, SAMPLED THE SAME WAY, 2026-09-10.
   40,515 opaque, hueless letterform pixels were read out of the TRI-COUNTY
   wordmark. THE WORDMARK IS CHROME, SO IT IS A RANGE AND NOT A COLOUR:

       darkest #676767   median #ABABAB   p75 #CACACA   highlight #E6E8E8

   WHICH STOP BECOMES THE GROUND IS A JUDGEMENT, AND IT IS RECORDED AS ONE.
   The median is what the eye calls "the silver", and it is unusable as a
   ground: --ink-2 on #ABABAB measures 4.27 and --ox-tx 3.90, so the
   secondary text and the small accent text both fail.

   The first rule taken was THE LIGHTEST STOP THE WORDMARK ACTUALLY
   CONTAINS, #E6E8E8, and the ground shipped at that on 2026-09-10. It was
   lifted the same day because black type did not stand off it hard enough:
   --ink measured 14.09 where the old cream gave 16.21.

   SO THE RULE IS NOW ONE STEP LONGER, and the second step is the honest
   part: TAKE THE WORDMARK'S HIGHLIGHT AND LIFT ITS LIGHTNESS, KEEPING ITS
   CHANNEL RELATIONSHIP EXACTLY. The highlight is rgb(230,232,232), neutral
   with a +2 cool cast in green and blue. The ground is rgb(240,242,242):
   ten lighter, the same +2 cast, the same hue. It is no longer a pixel the
   wordmark contains, and pretending otherwise would be the kind of
   provenance claim this file exists to prevent. What it still is: the
   logo's silver, brighter. --ink reads 15.42 on it.

   --rule COMES OUT OF THE SAME SAMPLE, at the wordmark's p75, #CACACA. It
   had to move: the old warm #DED8D1 measured 1.32 against the cream ground
   and only 1.15 against the silver, which is a hairline nobody can see.
   #CACACA measures 1.33. The same line, on a ground that changed under it.

   THE TEMPERATURE CHANGED ON PURPOSE, AND IT IS A BRAND DECISION RATHER
   THAN A TASTE ONE. #F9F7F4 was a warm cream: red highest, blue lowest.
   #F0F2F2 carries the logo's own neutral-to-cool chrome cast, green and
   blue two above red. The page now stands on the mark's temperature, which
   is the same argument that put #691C17 and #E92424 in this table.

   WHAT IT COSTS, AND IT IS ALMOST NOTHING NOW. The first silver was darker
   than the cream and every ratio on the ground dropped with it. Lifting the
   ground put them back: --ink reads 15.42 against the cream's 16.21, --ink-2
   8.72 against 9.16, --ox-tx 7.96 against 8.37. The temperature moved and
   the contrast very nearly did not.

   | Token     | Hex      | Role                                            |
   |-----------|----------|-------------------------------------------------|
   | --ox      | #691C17  | ACT. Filled buttons, the call bar, the two bands.|
   | --ink     | #121B27  | Body text, headings, the header, the footer.     |
   | --silver   | #F0F2F2  | THE DEFAULT GROUND, and all type on dark.       |
   | --white   | #FFFFFF  | THE HIGHLIGHT GROUND. One band, not the page.   |
   | --mark    | #E92424  | The logo, and nothing else. See the warning.     |
   | --ox-tx   | #8A2419  | Oxblood's shade for small text on the silver ground.         |
   | --ink-2   | #3C4453  | Secondary text on the silver ground.                         |
   | --silver-2 | #C9CCD3  | Secondary text on ink.                           |

   WHITE IS A GROUND, NOT A SHADE OF THE GROUND. Added 2026-09-10.
   --silver is the page and --white is used on exactly ONE band, the stat
   band under the hero, to lift the proof figures off the run of the
   page.

   THE BAND SEPARATES MORE THAN IT USED TO, AND THAT WAS NOT THE PLAN. It
   measured 1.07 against the old cream ground, which was chosen so it would
   read as the same page one shade cleaner rather than as a contrast switch.
   Against the first silver it measured 1.23, which was more separation
   than the band was ever asked for. The ground was lifted on 2026-09-10
   and it is 1.12 now, back within sight of the 1.07 it was tuned to.

   Every text pair gets better on white, and by more than before:

       --ink   on --white  17.33     on --silver  15.42
       --ox    on --white   11.80    on --silver  10.50
       --ink-2 on --white    9.80    on --silver   8.72
       --ox-tx on --white    8.95    on --silver   7.96

   ONE HONEST DUPLICATE: --panel is also #FFFFFF and has been since the
   first commit. It is the ground of a CARD, which sits ON the page; --white
   is the ground of a BAND, which IS the page for its own height. Two names
   for one hex is worth it while the two roles can diverge; if they never do,
   collapsing them is a one-line change.

   GOLD IS GONE, removed 2026-09-06 on the client's call. #E5C549 used to
   carry the certification marks, the small-caps labels and the counters on
   dark grounds. Everything it did is now done by --silver or --ox: a badge on
   ink is silver, a counter on oxblood is a silver disc with oxblood numerals.
   The palette is four colors and two text shades, and it is easier to hold
   the line on four.

   MEASURED CONTRAST, machine-counted against WCAG's formula, never eyeballed:

       --ink    on --silver    15.42        --silver   on --ink     15.42
       --ox     on --silver    10.50        --silver   on --ox      10.50
       --ox-tx  on --silver     7.96        --silver-2 on --ink     10.78
       --ink-2  on --silver     8.72        --silver-2 on --ox       7.34
       --rule   on --silver     1.46        --silver   on the deep  13.68

   AND TWO THAT FAIL ON PURPOSE. THEY ARE THE SAME NUMBER, WHICH IS WHY THE
   RULE IS SYMMETRIC:

       --ox  on --ink   1.47        --ink on --ox   1.47

   An oxblood button on the ink header is all but invisible AS A SHAPE, even
   though its silver label reads at 10.50. An ink button on an oxblood band is
   invisible in exactly the same way and by exactly the same margin, with its
   silver label reading at 15.42.

   SO: THE ACT BUTTON ON ANY DARK GROUND WEARS THE PAPER HAIRLINE. Decided
   2026-09-10, and it is one rule, not two exceptions. The fill changes to
   whichever dark the ground is not, so the button still reads as a solid
   object; the hairline is what gives it an edge. Never put a dark shape on a
   dark ground without one.

       ground        fill      label                 shape   label
       --ink         --ox      --silver                1.47   10.50
       --ox          --ink     --silver                1.47   15.42

   The hover shades are darker still and therefore even less visible as
   shapes, 1.24 and 1.21. THAT IS FINE AND IT IS THE POINT: the border draws
   the shape, the fill only says the pointer is on it, and both hover labels
   stay well clear at 8.72 and above. The ghost button beside the act button
   is unaffected on either ground; it is a border and a label already.

   THE LOGO RED FAILS AS TEXT. #E92424 on the silver ground is 3.94, under the 4.5 that
   normal-size text needs. That is why the mark's red and the text red are two
   separate tokens rather than one "brand red": --mark is for the logo file
   and --ox-tx is for words. Do not set type in --mark, at any size, on any
   background lighter than --ink. This is the palette law's rule that small
   accent text on light backgrounds gets its own darker accessible shade, and
   it is the single easiest thing on this site to get wrong.

   ---------------------------------------------------------------------------
   THE RULES THIS FILE KEEPS.

   SOLID COLORS ONLY. No gradients on text, buttons, cards or accents, and no
   glows.

   ONE SANCTIONED CHROME ACCENT, DATED 2026-09-17. The sticky header carries
   a 4px --ox bottom border, on the client's ruling, matching the brand
   accent the shop's live site wears under its nav. It is recorded here
   because it is oxblood doing a job that is not an act: it is a ground
   element, chrome, like the scrim, and it asks for nothing and cannot be
   clicked. It is --ox and NOT --mark: the logo's red stays mark-only, on
   the rule in the table above. See .nav for what it cost the fold budget,
   which was measured rather than assumed.

   ONE SANCTIONED LABEL GROUND, DATED 2026-09-23. The AFTER chip on the
   Real Repairs pairs fills in --ox with a --silver hairline, on the
   client's ruling: the outcome is the branded moment and the wreck is
   not, so the BEFORE chip keeps its white fill and ink edge. This is the
   THIRD time oxblood has been extended past the act bands, after the
   header's 4px divider and #who-we-are's prose ground, and it is
   recorded here with them rather than argued again at the chip.

   THE TEST FOR A FOURTH CASE: does the element ask the reader to do
   something? A band that asks is an act band and the act-band count
   governs it. A chip does not ask, cannot be clicked and is not a
   target; it names which photograph you are looking at. See .ba-chip for
   the construction and proposed-changes.md 3.33 for the measurements.

   ONE SANCTIONED PAGE-HEADER IDENTITY, DATED 2026-09-28, appended here in
   3.76. The compact ox header, .hero.dark.field-ox, is a PAGE-HEADER
   IDENTITY: chrome, not an in-flow band, on Greg's ruling
   (proposed-changes.md 3.61), extended to the areas tier, the hub and the
   twelve town pages (3.62). /contact-us/, /blog/, the sixteen posts and
   the town pages wear it. The band test above still governs bands in the
   page flow; it does not govern the page header field. Privacy is decided
   when it is built. It is the fourth extension recorded here.

   THE CAP IS AMENDED, DATED 2026-10-02, Greg's ruling (proposed-changes.md
   3.91). The five service pages carry a THIRD in-flow ox band, a
   NAVIGATION band: #related, Related Services, after the FAQ. Its cards
   are links, so by the test above it asks, and the two-band cap would
   have ruled it out; this paragraph is the amendment, in so many words.
   It asks the reader to go somewhere, never to call or write: no CTA row,
   no button, so the CTA bands are still the only ox bands that ask for an
   action and their count of two stands. Same .field-ox, same gradient.
   THE ADJACENCY RULE IS NOT AMENDED: no two adjacent sections share a
   ground, which is why it stands between the silver FAQ and the ink
   footer and not under the ox promise band. It is the fifth extension.

   TWO SANCTIONED SECTION-BACKGROUND GRADIENTS, BOTH DATED 2026-09-10.
   The second was the hero's scrim, the ground its type sat on; see THE
   HERO and the readability amendment. RETIRED 2026-10-20 (3.112) with the
   veil, so ONE remains. The photograph zone's 72px edge fade is a mask on
   the image, not a background gradient: it shows ground, never paints it.

   THE FIRST, DECIDED 2026-09-10. The palette
   law allows exactly this one exception and only as a dated, recorded
   decision, so this is the record. The two oxblood act bands carry a gradient
   (so does every .field-ox band, the service pages' navigation band of 3.91
   included: it is the class's, not a second exception)
   derived from the logo's own swoosh: deep at the ends, --ox exactly through
   the middle, on the mark's angle. IT STAYS INSIDE ONE HUE, so oxblood still
   means act from the first pixel. See .field-ox for the stops, the fallback
   chain and the measured contrast at each stop. Nothing else on this site
   gets a gradient.

   MOTION IS EVIDENCE, NOT COSTUME. That is still the rule, and it is
   still the reason for everything below. NOTHING ANIMATES ON LOAD,
   NOTHING LOOPS, AND NOTHING RE-TRIGGERS. The reader arrived here from a
   collision and is holding a phone; a page that moves while they are
   trying to read a phone number is a page that costs them time.

   ---------------------------------------------------------------------
   THE MOTION AMENDMENT, 2026-09-10. RECORDED AS ONE, IN FULL.

   This file used to say motion was zero except the FAQ disclosure. It is
   not zero any more. Five candidates were built on a sampler, judged,
   and the sampler was deleted once every verdict was in; the build and
   the judging are in the git history around this date if the reasoning
   is ever wanted. Two were cut, and a third, the stat band's counting
   effect, was withdrawn later (3.85). What moves divides into two kinds.

   KIND ONE, WHICH WAS ALWAYS ALLOWED: motion that ANSWERS A READER.
   The FAQ disclosure and its chevron, the lift under a pointer, the
   press under a finger. These move because a person did something to
   them, and they are unremarkable.

   KIND TWO, WHICH IS THE ACTUAL AMENDMENT: MOTION MAY ALSO FIRE ONCE ON
   A SECTION'S FIRST ARRIVAL. A LANE DRAWS. NEVER LOOPING, NEVER
   RE-TRIGGERING.

   WITHDRAWN ON GREG'S RULING, proposed-changes.md 3.85: the stat band's
   counting effect, the rolling digits and the rolling word. It was
   built and proved under this amendment, and his eye ruled it out. The
   figures stand still as the text they are in the markup.

   The lane is still evidence rather than costume, which is the only
   reason it was allowed in:

     THE LANE DRAWS BECAUSE THE SECTION IS A ROAD. Six steps from the
     phone call to the keys, with a dashed centre line down them. It is
     a lane and not a progress bar on purpose: no track, no state change
     once painted, constant speed.

   THE LANE'S AUDIENCE IS A PHONE READER, AND ONLY A PHONE READER. It
   exists only below 720px, because that is the only width where .steps
   is a single column and a line from 01 to 06 is a road rather than a
   stray rule across a grid. A desktop reader never sees it. That is a
   deliberate scope, not an unfinished one.

   UNDER prefers-reduced-motion, IT DOES NOTHING. The lane rests fully
   drawn. Nothing is hidden and nothing is pending, in that case or with
   JavaScript off.

   KIND THREE, ADDED 2026-09-10: ONE FADE ON PAGE LOAD, IN THE HERO
   ONLY. The photograph fades up over 500ms and the copy follows 200ms
   behind it over 500ms. OPACITY ONLY: no translation, no scale, no Ken
   Burns, no parallax, no carousel. Once per load, never on scroll,
   never on a loop.

   This one contradicts a sentence that was written above it: "nothing
   animates on load". That sentence is amended rather than quietly
   broken. The reason it held is still the reason it holds everywhere
   else: a page that moves while a reader is trying to find a phone
   number costs them time. A single fade of the thing they are already
   looking at, finished before the fold is read, does not.

   THE GUARD IS THE WHOLE IMPLEMENTATION. Base opacity is 1 in the
   cascade. The fade exists ONLY as keyframes inside
   prefers-reduced-motion: no-preference, starting from 0 INSIDE the
   keyframes. Reduced motion, an old browser, a stylesheet that half
   loaded, JavaScript off: every one of them rests with everything
   visible. Nothing on this site is ever hidden waiting for an animation
   to reveal it.

   THE LIMIT OF THIS AMENDMENT: once, on first arrival or on load, and
   then nothing watches it again. Anything that would move a second
   time, or move without a reader having arrived, loaded or acted, is a
   different decision and needs its own amendment, dated, here.

   KIND FOUR, ADDED 2026-09-17 ON THE CLIENT'S RULING: ONE CONTINUOUS
   MOTION, THE BRAND STRIP, AND NOTHING ELSE EVER. This is the
   amendment the sentence above asked for, and it is the largest one
   this file carries, because "nothing loops" was written twice above
   and a loop is now allowed.

   IT IS ONE COMPONENT, NOT A PERMISSION. #brands, the twelve
   manufacturer marks, drifting left at about 40 seconds a cycle. No
   second strip, no ticker, no carousel, no autoplaying anything. A
   thing that wants to loop needs its own amendment with its own date,
   here and in CLAUDE.md, and the answer is expected to be no.

   EXTENDED 2026-09-23 ON THE CLIENT'S RULING: A COMPONENT, CAPPED AT
   ONE PER PAGE. This paragraph used to end "on this page or any
   other", which made the strip a property of the home page. It is not:
   it is the one thing on this site that may loop, and a page may carry
   ONE of it. /collision-repair/ now carries it, inside
   #factory-certified, whose subject it is.

   EVERY LIMIT TRAVELS WITH IT, VERBATIM. It is the same markup and the
   same rules below, not a second implementation: transform only,
   pauses on hover and focus-within, rests as a wrapped static row with
   all twelve marks visible under prefers-reduced-motion with that
   resting state the base in the cascade, and no JavaScript at all. A
   copy that dropped one of those would be a new amendment, not this
   one.

   WHY THIS ONE. Twelve marks cannot be shown legibly across a phone,
   and the two alternatives are worse than motion: shrink them until
   nobody recognises them, or hide some behind a control, which is the
   failure the Real Repairs pairs were rebuilt to avoid. The drift
   shows every mark at full size at every width. It is recognition
   rather than decoration, which is the same test kinds two and three
   had to pass: MOTION IS EVIDENCE, NOT COSTUME.

   THE LIMITS ARE THE PERMISSION. Transform only, so nothing fades,
   resizes, recolours, appears or disappears. It pauses under a pointer
   and on focus-within, so a reader who wants to look at one mark can
   stop it. Under prefers-reduced-motion it does not run AT ALL and the
   strip rests as a wrapped static row with every mark visible, which
   is the base state in the cascade rather than a state the media query
   restores. See the brand strip block below for the geometry and for
   why nothing here is ever hidden waiting for an animation.

   ---------------------------------------------------------------------
   THE READABILITY AMENDMENT, 2026-09-10.

   TYPE MAY SIT OVER PHOTOGRAPHY, AND ONLY INSIDE A SCRIM ZONE WHOSE
   COMPOSITE HAS BEEN MEASURED. Design rule 1 used to require an opaque
   panel, which was a way of guaranteeing readability without measuring
   it. The guarantee stays; the method is now the measurement.

   THE METHOD, and it is not eyeballing with a squint:

     1. Render the page at the viewport in question.
     2. Render it again with the overlaid copy set to visibility:hidden,
        which keeps the layout and removes the glyphs, so what is
        sampled is the GROUND and not the type.
     3. Take each overlaid text element's GLYPH RUNS, not its block box:
        Range.getClientRects() over its text nodes. A block box spans the
        whole column and would sample photograph the type never touches.
     4. Sample every composited pixel inside those runs and compute the
        contrast against the element's own computed colour.

   THE FLOOR IS 4.5:1 ANYWHERE UNDER A GLYPH BOX. The target is 7:1 for
   body-size text. IF THE PHOTOGRAPH DEFEATS THE SCRIM, THE SCRIM
   DEEPENS. The standard does not move, the type does not get a shadow,
   and the photograph does not get blurred.

   The hero's measured worst cases are in proposed-changes.md 3.20. They
   were not all passes first time: the phone eyebrow measured 2.13 and
   the 360 H1 measured 4.25 against a scrim that had faded out above the
   copy, and the scrim was deepened until every element cleared 7.

   BUTTON GRAMMAR. A filled button (.btn) means act, and on this site acting
   means calling (215) 322-5350. A text link means read more. Navigation is
   never inflated into a button. .btn-ghost is the same grammar at lower
   emphasis, for the second CTA in a row, never for a nav item.

   THE CALL BAR IS FIXED TO THE BOTTOM OF EVERY PHONE VIEWPORT. It is the
   whole design argument in one element: the reader should never have to
   scroll, hunt or remember to reach the shop. It is hidden above 900px, where
   the header's own call button is always visible instead, and the body gets
   bottom padding to match so it never covers the last line of the footer.

   BACKGROUND RHYTHM. --silver by default, --ox for the two bands that ask the
   reader to act, --ink for the contact band and the footer. That is the whole
   system, and it is deliberately quieter than an alternating dark/light
   rhythm, because every contrast switch costs reading time.

   REAL PHOTOS ONLY. Every image on this site is the shop, its people or its
   work. No stock, no AI imagery, no decorative drawing standing in for a
   photograph.
   =========================================================================== */

:root {
  --ox: #691C17;
  --ox-dk: #55160F;
  --ox-tx: #8A2419;
  --ink: #121B27;
  --ink-2: #3C4453;
  --silver: #F0F2F2;
  /* THE SAME COLOUR AS CHANNELS. Every translucent wash of the ground
     reads from here, so a future ground change cannot leave a stale
     literal behind. It already did once: renaming the token left ten
     rgba(249,247,244,a) washes still warm. */
  --silver-rgb: 240 242 242;
  --ink-rgb: 18 27 39;
  --ox-rgb: 105 28 23;
  --silver-2: #C9CCD3;
  --mark: #E92424;
  --rule: #CACACA;
  --panel: #FFFFFF;
  --white: #FFFFFF;

  --wrap: 1120px;
  --wrap-pad: 20px;
  /* THE HERO'S COPY COLUMN, 3.110: one width, and everything in the hero's
     copy lives inside it at 600px and up, the H1 included. This is its one
     home. Since 3.112 the photograph's zone starts --hero-gutter past its
     right edge and fades in over --hero-fade, so no glyph ever sits over
     the photograph. */
  --hero-col: 620px;
  --hero-gutter: 48px;
  --hero-fade: 72px;
  --pad: clamp(48px, 7vw, 88px);
  /* THE STAT CARD'S OVERLAP, DEFINED ONCE. The home card rides up over
     the hero's bottom seam by this much, and the hero copy's bottom
     padding is DERIVED from it: overlap plus 28px of air. See
     .heroB-copy and .statband.statcard, which are the only two places
     this is read.

     THE LESSON, and it is the same one .statband.statcard carries in a
     different key: TWO VALUES THAT MUST AGREE ARE ONE VALUE PLUS A
     DERIVATION. Until 2026-09-13 the overlap and the padding were two
     independent clamps, tuned to approximately agree, and the air
     between them was whatever was left over after they disagreed: 12px
     at 1440, about 8px near 1300, 6px at 390. That is a white card
     butting into the hero's CTA buttons, and no amount of care on either
     clamp fixes it, because neither one can see the other. Now the
     padding cannot drift, because it is not a second opinion about the
     same number. */
  --statcard-overlap: clamp(44px, 6vw, 78px);
  /* THE AIR BETWEEN THE HERO'S CTA ROW AND THE CARD'S TOP EDGE, and
     it lives here beside the overlap because .heroB-copy states the
     same sum in three places and they must not drift apart. Client
     ruling 2026-09-22 from a rendered comparison; the floor is the
     value that shipped before it, so phones pay nothing for a
     desktop rhythm fix. proposed-changes.md 3.31. */
  --statcard-air: clamp(28px, 3.9vw, 56px);
  /* THE SECTION'S HEAD-TO-BODY GAP, DEFINED ONCE. .sec-head leaves this
     much air under the sub before a section's body begins, flat at every
     width. It is a token from 2026-09-24 because a SECOND place now has
     to agree with it: #factory-certified #brands trims the strip's band
     padding so the marks sit in that same rhythm rather than in a band's.
     Two values that must agree are one value plus a derivation.
     proposed-changes.md 3.43. */
  --sec-gap: 40px;
  --radius: 4px;
  --font: "Source Sans 3", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --display: "Archivo Black", Impact, "Arial Black", sans-serif;
}

/* SELF-HOSTED, both of them. No webfont CDN is contacted at runtime, so no
   font is a third-party request, nothing leaks, and nothing breaks when
   somebody else's CDN changes. (Since cutover, 2026-10-27, 3.142, the only
   third-party requests on a page are the GA4 and CallRail scripts.) These are the latin subsets.
   Source Sans 3 is variable, so one file covers 200 to 900. Archivo Black has
   exactly one weight, 400, and that weight is already very heavy: asking for
   700 or 800 makes the browser fake it. Both are SIL Open Font License 1.1
   and the licence texts sit beside them, unedited, in assets/fonts/. */
@font-face {
  font-family: "Archivo Black";
  src: url("fonts/ArchivoBlack-400.woff2") format("woff2");
  font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Source Sans 3";
  src: url("fonts/SourceSans3-var.woff2") format("woff2");
  font-weight: 200 900; font-style: normal; font-display: swap;
}

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

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  font-family: var(--font);
  font-size: 1.0625rem;
  line-height: 1.65;
  color: var(--ink);
  background: var(--silver);
  /* Room for the fixed call bar, so it never sits on the last line. */
  padding-bottom: 68px;
}

@media (min-width: 900px) { body { padding-bottom: 0; } }

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

a { color: var(--ox-tx); }
a:focus-visible, button:focus-visible, summary:focus-visible {
  outline: 3px solid var(--ox);
  outline-offset: 2px;
}

/* Archivo Black stops at h3. h4 is a small-caps label and Source Sans 3 at
   700 is the right weight for one; Archivo Black at label size is a brick. */
h1, h2, h3 { font-family: var(--display); font-weight: 400; line-height: 1.1; letter-spacing: -0.015em; margin: 0 0 0.5em; }
h1 { font-size: clamp(2.3rem, 6.2vw, 3.9rem); }
h2 { font-size: clamp(1.6rem, 3.6vw, 2.3rem); }
h3 { font-size: 1.2rem; }
p { margin: 0 0 1.1em; }
p:last-child { margin-bottom: 0; }

/* THE EYEBROW IS SITE TYPE, NOT HERO TYPE, and it lives here to say so.
   Small, heavy, tracked, upper case: the line that names the page's
   subject before the H1 makes its claim.

   SWEPT 2026-09-10, RESTORED 2026-09-13, VERBATIM. The commit that
   deleted the old split hero deleted this rule along with it, because
   the rule happened to sit next to the .heroA block rather than because
   it belonged to it. For three days both heroes rendered their eyebrow
   as plain 1rem sentence case: no size, no weight, no tracking, no
   caps. It cost 6px of eyebrow height on a phone and slid the 360x640
   CTA from 575 to 582, past a 580 fold budget, which is how it was
   found.

   THE LESSON, and it is a discipline rather than a mechanism: A SWEEP
   MAY DELETE ONLY RULES SCOPED TO THE THING BEING SWEPT. Before
   deleting a rule in a cleanup, grep its selector across every page. A
   base-class rule that merely lives near a section's block belongs to
   the site, not the section, and nothing but reading the diff or
   measuring the render will tell you which one you are holding.

   The overrides on top of this are untouched and still apply: .heroB
   .eyebrow takes it to silver, the phone block tightens its margin to
   6px, and .dark .eyebrow carries it on any dark ground. */
.eyebrow {
  font-size: .82rem; font-weight: 800; letter-spacing: .12em; text-transform: uppercase;
  color: var(--ox-tx); margin: 0 0 12px;
}

/* THE OTHER TWO RULES FROM THE SAME SWEEP, restored 2026-09-13 with the
   eyebrow and for the same reason: they are site type, they sat beside
   the .heroA block, and they went out with it on 2026-09-10.

   NEITHER SHOWS ON EITHER PAGE TODAY, and they are restored anyway. A
   .lead inside a hero takes its size, colour and measure from .heroB
   .lead, which outranks this on specificity and not on file order; .hero
   is only ever worn alongside .heroB, which zeroes the padding. What
   they are for is the first page that puts a .lead outside a hero, which
   is what templates/service-page-template.html already does. A base rule
   that is invisible today is not a base rule that is unused: it is the
   default every future page inherits, and the sweep took the default
   away. */
.hero { padding-top: clamp(32px, 5vw, 60px); }
.lead { font-size: 1.2rem; color: var(--ink-2); max-width: 60ch; }

.wrap { width: 100%; max-width: var(--wrap); margin: 0 auto; padding: 0 var(--wrap-pad); }

section { padding: var(--pad) 0; }

/* ---------- Buttons: filled means act, and act means call ---------- */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 10px;
  background: var(--ox); color: var(--silver);
  font-size: 1.0625rem; font-weight: 700; text-decoration: none;
  padding: 15px 26px; border: 2px solid var(--ox); border-radius: var(--radius);
  min-height: 52px; transition: background-color .12s ease, border-color .12s ease;
}
.btn:hover { background: var(--ox-dk); border-color: var(--ox-dk); }
.btn-sm { padding: 10px 18px; min-height: 44px; font-size: 1rem; }
.btn-ghost { background: transparent; color: var(--ox); }
.btn-ghost:hover { background: rgba(105, 28, 23, .08); border-color: var(--ox); }
/* THE ACT BUTTON ON A DARK GROUND. One rule, both grounds, and the
   contrast note in the header carries the arithmetic. The fill is
   whichever dark the ground is NOT, and the silver hairline is what gives
   the shape an edge, because dark-on-dark measures 1.47 either way round.

   :not(.btn-ghost) IS LOAD-BEARING. The ghost carries both classes, so an
   unqualified .field-ox .btn would fill it in and destroy it. Excluding it
   here also makes the rule order-independent, which matters more than the
   two extra characters: this block no longer cares whether it sits above
   or below the ghost's own rules. */
.dark .btn:not(.btn-ghost) { color: var(--silver); border-color: var(--silver); }
.field-ox .btn:not(.btn-ghost) { background: var(--ink); }
.field-ox .btn:not(.btn-ghost):hover { background: var(--ink-2); border-color: var(--silver); }
.field-ink .btn:not(.btn-ghost), .heroB .btn:not(.btn-ghost) { background: var(--ox); }
.heroB--ox .btn:not(.btn-ghost) { background: var(--ink); }
.field-ink .btn:not(.btn-ghost):hover,
.heroB .btn:not(.btn-ghost):hover { background: var(--ox-dk); border-color: var(--silver); }
.heroB--ox .btn:not(.btn-ghost):hover { background: var(--ink-2); border-color: var(--silver); }
.dark .btn-ghost { background: transparent; color: var(--silver); border-color: var(--silver); }
.dark .btn-ghost:hover { background: rgb(var(--silver-rgb) / .14); }
.cta-row { display: flex; flex-wrap: wrap; gap: 14px; margin: 26px 0 0; }
/* The two act bands centre their CTA row under a centred heading. The hero
   row is not in a .dark band and stays left, with the copy it belongs to. */
.dark .cta-row { justify-content: center; margin-top: 44px; }
/* A COMPACT HEADER ON A DARK GROUND IS NOT AN ACT BAND, added 2026-09-24
   for /contact-us/'s ox header, proposed-changes.md 3.54. Its call row
   belongs to the left-aligned copy above it, like a hero's, so it keeps
   the base row: left, and the base 26px rather than the band's 44, which
   the fold budget on a phone cannot spare. .hero is worn only by the
   hero and this header, and .heroB is never .dark, so this reaches the
   one header and nothing else.
   WIDENED 2026-09-28, proposed-changes.md 3.61: /blog/ and the sixteen
   posts wear .hero.dark too, and carry no .cta-row, so this still reaches
   /contact-us/ alone. */
.hero.dark .cta-row { justify-content: flex-start; margin-top: 26px; }

/* ---------- Header ---------- */
/* INK HEADER. It sits directly under the staging bar, which is also ink, so
   the two read as one dark band at the top of the page. That is the point:
   the staging notice stops looking like a design element and starts looking
   like chrome. */

/* THE OXBLOOD DIVIDER, ADDED 2026-09-17 ON THE CLIENT'S RULING, matching the
   brand accent the live site carries under its nav. It replaces the 1px
   silver hairline that separated the header from the page.

   IT IS CHROME, NOT AN ACT AND NOT THE MARK, and it is recorded as a
   sanctioned chrome accent in the palette note above. It asks for nothing,
   it is not clickable, and it is the same ground colour rather than the
   logo's red: --mark stays mark-only, because #E92424 is 3.94 on the silver
   ground and is the single easiest thing on this site to get wrong.

   IT RIDES WITH THE HEADER because it is the header's own border and the
   header is sticky, so it is under the nav at every scroll position rather
   than a rule at the top of the document that scrolls away.

   IT COSTS 3px OF FOLD BUDGET ON EVERY PAGE, because the sticky header is in
   flow and everything below it moves down by what the border gained. That is
   measured rather than assumed: the full fold table was re-run at five
   viewports and is in proposed-changes.md 3.24. The tightest phone in the
   record, 360x640, went from clearing by 5px to clearing by 2px, and 2px is
   the floor the ruling set.

   ITS OWN CONTRAST IS MEASURED TOO. --ox on --ink is 1.47, so the divider
   barely reads against the header above it and is not trying to: what it
   separates is the header from the PAGE, and it is measured against what
   sits directly under it, which is the hero photograph. See 3.24. */
.nav {
  position: sticky; top: 0; z-index: 40;
  background: var(--ink); border-bottom: 4px solid var(--ox);
}
/* THE LOGO NEEDS CLEAR SPACE, AND IT HAD NONE. The mark's PNG is cropped
   tight to the artwork, so the <img> box IS the mark: at 1440 the logo
   measured 190x76 inside a 76px bar, which is zero clearance top and
   bottom, and the swoosh touched the ink edge.

   MEASURED, NOT EYEBALLED. The target is ~12px of clear space around the
   mark, which is about a fifth of its own height and reads as deliberate
   rather than as a gap.

       desktop   168 x 66.85 in a 92px bar   ->  12.6px top and bottom
       phone     120 x 47.75 in a 64px bar   ->   8.1px top and bottom

   THE PHONE BAR DOES NOT GROW, AND THAT IS NOT AN AESTHETIC CHOICE. The
   header is sticky, so every pixel it gains comes off the fold budget,
   and the hero's CTA pair clears that budget by 8px. Growing this bar
   would spend the lot. So on a phone the clearance comes out of the
   mark's size instead, and the bar stays exactly where it was.

   THE LEFT EDGE IS THE PAGE GRID'S MARGIN and is left alone on purpose.
   The logo starts where every other first character on the page starts;
   insetting it further would break the alignment the hero panel's whole
   arithmetic exists to hold.

   height is auto so nothing letterboxes inside object-fit. The width and
   height attributes on the <img> still reserve the space, so there is no
   layout shift while it loads. */
.nav .inner { display: flex; align-items: center; gap: 18px; min-height: 92px; }
.nav-home { display: block; flex: 0 0 auto; }
.nav-home img { width: 168px; height: auto; object-fit: contain; }
/* Oxblood fill, because oxblood means act, plus a silver edge because
   --ox on --ink is 1.47 and the shape would otherwise disappear. */
.nav .btn { flex: 0 0 auto; border-color: var(--silver); }
.nav .btn:hover { background: var(--ox-dk); border-color: var(--silver); }

/* ---------- THE NAV, 2026-09-29, proposed-changes.md 3.83 ----------
   THE LIVE SITE'S NAVIGATION IS THE GUIDE, on Greg's ruling, minus Home:
   Collision Services and Areas We Serve as dropdowns, then Authorization
   Forms, Contact Us and Blog, in the live site's own words. The markup is
   written by scripts/sync-chrome.py and is the same on every page.

   ONE BREAKPOINT, MEASURED: 1100px. The row needs 979.6px for the logo,
   the five items and Call, so it fits from a 1020px viewport, but at 1024
   that is 4.4px of slack, and a font that renders half a percent wider on
   another platform would break it. At 1100 the slack is 80px, about 8%.
   Below 1100 the Menu button takes over IN THE SAME ROW. 3.83.

   THE PHONE HEADER DID NOT GROW, and that is the hard constraint. Its
   single text link became the Menu button, the same row at the same
   height, and the menu opens as an overlay positioned out of flow. While
   closed it is display: none, so it takes no space, no focus and no place
   in the accessibility tree. The fold table in 3.83 proves the header's
   height moved by nothing.

   NO MOTION AT ALL. A dropdown appears and disappears; the chevron flips
   without a transition. So there is nothing for prefers-reduced-motion to
   turn off, which is the simplest way to respect it.

   JAVASCRIPT OFF: site.js marks the page .js. Without it, a dropdown opens
   on hover and on focus-within, so every item is still reachable by
   keyboard; the Menu button is not shown, and a phone reader reaches every
   page from the footer's Explore column and the call bar. With it, the
   dropdowns are true disclosures: aria-expanded, Escape closes and returns
   focus to the button, and a click outside closes. */
.nav-main { margin-left: auto; }
.nav-list { display: flex; align-items: center; gap: 22px; list-style: none; margin: 0; padding: 0; }
.nav-item { position: relative; display: flex; align-items: center; }
.nav-main a, .nav-sub-toggle { color: var(--silver); font-weight: 600; text-decoration: none; font-size: 1rem; }
.nav-main a:not(.btn):hover { text-decoration: underline; text-underline-offset: 5px; }
/* The current page: a heavier, further underline, --silver because --ox on
   --ink is 1.47. Colour and weight only. */
.nav-main a[aria-current="page"] { text-decoration: underline; text-decoration-thickness: 2px; text-underline-offset: 6px; }
.nav-main .nav-item > a { padding: 12px 0; }
.nav-sub-toggle {
  display: inline-flex; align-items: center; gap: 6px;
  background: none; border: 0; margin: 0; padding: 12px 0;
  font-family: inherit; line-height: inherit; cursor: pointer;
}
.nav-sub-icon { padding: 12px 6px; margin-left: 2px; }
.nav-chev { width: 12px; height: 12px; fill: none; stroke: currentColor; stroke-width: 1.8; }
.nav-sub-toggle[aria-expanded="true"] .nav-chev { transform: rotate(180deg); }
.nav-sub { display: none; list-style: none; margin: 0; padding: 0; }
.nav-sub-toggle[aria-expanded="true"] + .nav-sub { display: block; }
/* The towns run DOWN two columns, so the alphabet reads the way a list
   does, and the column count, not a town count, is all this knows. */
.nav-sub-towns { columns: 2; column-gap: 16px; }
.nav-sub-towns li { break-inside: avoid; }
.navtoggle { display: none; }

@media (min-width: 1100px) {
  .nav-sub {
    position: absolute; top: 100%; left: -18px; z-index: 5; min-width: 270px; padding: 8px 0;
    background: var(--ink); border: 1px solid rgb(var(--silver-rgb) / .5); border-top: 4px solid var(--ox);
  }
  .nav-sub-towns { min-width: 440px; }
  .nav-sub a { display: block; padding: 10px 18px; white-space: nowrap; }
  html:not(.js) .nav-item:hover > .nav-sub, html:not(.js) .nav-item:focus-within > .nav-sub { display: block; }
}

@media (max-width: 1099px) {
  /* From 900 to 1099 the header's Call and the Menu button sit together
     at the right; below 900 the call bar carries the call and Menu takes
     the right edge alone. */
  .nav .nav-call { margin-left: auto; }
  html.js .navtoggle {
    display: inline-flex; align-items: center; gap: 8px; flex: 0 0 auto;
    min-height: 44px; padding: 8px 12px; background: none; color: var(--silver);
    border: 1px solid rgb(var(--silver-rgb) / .5); border-radius: var(--radius);
    font: inherit; font-weight: 600; cursor: pointer;
  }
  .navtoggle svg { width: 22px; height: 22px; fill: none; stroke: currentColor; stroke-width: 2; }
  .navtoggle .ico-close, .navtoggle[aria-expanded="true"] .ico-open { display: none; }
  .navtoggle[aria-expanded="true"] .ico-close { display: block; }
  /* THE MENU IS AN OVERLAY under the header's oxblood border, out of flow,
     scrolling inside itself if a short screen needs it. */
  .nav-main {
    display: none; position: absolute; top: calc(100% + 4px); left: 0; right: 0; margin: 0;
    max-height: calc(100vh - 100px); overflow-y: auto;
    padding: 8px 20px 24px; background: var(--ink); border-bottom: 4px solid var(--ox);
  }
  .nav.is-open .nav-main { display: block; }
  .nav-list { display: block; }
  .nav-item { flex-wrap: wrap; border-bottom: 1px solid rgb(var(--silver-rgb) / .18); }
  .nav-main .nav-item > a, .nav-sub-toggle { flex: 1 1 auto; padding: 14px 0; }
  .nav-sub-toggle { justify-content: space-between; text-align: left; }
  .nav-sub-icon { flex: 0 0 auto; padding: 14px 16px; }
  .nav-sub { flex: 0 0 100%; padding: 0 0 12px 16px; }
  .nav-sub a { display: block; padding: 10px 0; }
}

@media (max-width: 899px) {
  .nav .inner { min-height: 64px; }
  .nav-home img { width: 120px; height: auto; }
  /* The call bar carries the call on a phone; the header's Call steps aside. */
  .nav .nav-call { display: none; }
  html.js .navtoggle { margin-left: auto; }
  /* The call bar covers the foot of the screen, so the menu leaves room. */
  .nav-main { max-height: calc(100vh - 68px); padding-bottom: 84px; }
}

/* ---------- The call bar. The one element the whole design turns on. ---------- */
.callbar {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 60;
  display: flex; align-items: center; justify-content: center; gap: 10px;
  min-height: 60px; padding: 10px 16px;
  background: var(--ox); color: var(--silver);
  font-size: 1.15rem; font-weight: 800; text-decoration: none;
  border-top: 2px solid var(--ox-dk);
}
.callbar svg { width: 22px; height: 22px; fill: none; stroke: currentColor; stroke-width: 2; }
@media (min-width: 900px) { .callbar { display: none; } }

/* ---------- The split hero: REMOVED 2026-09-10 ----------
   .heroA and its panel, the flush-left arrangement with the photograph
   beside it, are gone. They were the service-page signature for four
   days; the architecture amendment made every hero text-over-photograph
   and /collision-repair/ was their only user.

   WHAT WENT WITH THEM, so a future reader knows what to look for in the
   history rather than wondering what the gap was: .heroA, .heroA-photo,
   .heroA-panel and the panel-width arithmetic (--edge + --col + --pad-r),
   which is the sum that kept the panel welded to the left glass and grew
   it with the grid. That arithmetic was the design system's one real
   piece of geometry and it is worth reading before anything like it is
   built again: git show f13adbf:docs/assets/site.css.

   Deleted rather than commented out, per the rule that a second
   direction left in the repo is a second direction somebody builds from.
   =========================================================================== */

/* ---------- Reassurance strip: certifications and rating ---------- */
.badges { display: flex; flex-wrap: wrap; gap: 10px; margin: 22px 0 0; padding: 0; list-style: none; }
.badges li {
  display: inline-flex; align-items: center; gap: 8px;
  background: var(--panel); border: 1px solid var(--rule); border-radius: 999px;
  padding: 8px 15px; font-size: .95rem; font-weight: 700; color: var(--ink);
}
.badges svg { width: 17px; height: 17px; fill: none; stroke: var(--ox); stroke-width: 2.1; }

/* ---------- The directions card, 3.65 ----------
   Greg's ruling of 2026-09-28: the directions become a CARD on the town
   pages and on /contact-us/, the Ambler page's assembly, ours. Map on top,
   a visible "Open in Google Maps" button, the drive-time chip, the address,
   the steps, and Call closing it. One column at every width, so the map
   sits over what it shows. The town pages drew their route and contact
   embedded Google's map until 3.117 (Greg's ruling of 2026-10-21), when
   the town cards took contact's embed too: one map across the site. Each
   rule names what it serves.
   The button reads "Get directions" since 3.144 (Greg, 2026-10-28).
   SIDE BY SIDE FROM 900px, 3.70, Greg's ruling of 2026-09-29, which
   supersedes the one-column reading above from that width and strikes
   nothing: below 900px the card stacks exactly as 3.65 built it. */
/* The card: white, the site's hairline and radius, clipped so the map's
   corners follow the card's. Narrow enough that a portrait route map is
   not a wall at 1440. */
.dir-card { max-width: 520px; margin: 0 auto; background: var(--panel); border: 1px solid var(--rule); border-radius: var(--radius); overflow: hidden; }

/* The map fills the card's top edge to edge, so its own frame comes off. */
.dir-card .map-box > a { border: 0; border-radius: 0; }
.dir-card .map-credit { margin: 8px 22px 0; }
.dir-embed { display: block; width: 100%; aspect-ratio: 4 / 3; border: 0; }
/* THE BODY BREATHES, 3.73 (Greg's ruling of 2026-09-29). Stacked, it
   takes the site's own card interior, .card's 26px by 24px, so the
   directions read as a card like every other on the page. */
.dir-body { padding: 26px 24px; }
/* The button-and-chip row. 3.74 put the chip alone at the top of the
   body; 3.75 (Greg's ruling of 2026-09-29) superseded that and moved the
   row, button and chip as first built, into the getaway kit at the
   card's foot. It wraps on a phone. Its 20px margin meets the address's
   20px and collapses to the one 3.73 step. */
.dir-top { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin: 0 0 20px; }
/* The chip is the vetted chip's pill, so it is named beside .badges li
   rather than restyled; it is not in .badges because it is per-town text,
   not the pattern row the variance gate leaves out. */
.dir-chip { display: inline-flex; align-items: center; gap: 8px; background: var(--panel); border: 1px solid var(--rule); border-radius: 999px; padding: 8px 15px; font-size: .95rem; font-weight: 700; color: var(--ink); }
.dir-chip svg { width: 17px; height: 17px; fill: none; stroke: var(--ox); stroke-width: 2.1; stroke-linecap: round; }
/* The address, prominent, with the pin mark: the one line a driver types
   into their own phone. */
/* ONE STEP BETWEEN THE BODY'S TEXT BLOCKS, 3.73: 20px after the top row,
   the address and contact's lines alike, a pixel over the prose's own
   1.1em paragraph gap, so the blocks above the steps keep the rhythm of
   the paragraphs under them. The Call keeps the ask's 26px of air. */
.dir-address { display: flex; align-items: center; gap: 10px; margin: 0 0 20px; font-size: 1.15rem; font-weight: 700; line-height: 1.4; }
.dir-address svg { flex: 0 0 auto; width: 22px; height: 22px; fill: none; stroke: var(--ox); stroke-width: 2; }
.dir-address a { color: var(--ink); }
.dir-card .prose { max-width: none; }
.dir-card .contact-lines { margin-top: 0; }
/* THE GETAWAY KIT, 3.74 (Greg's ruling of 2026-09-29). "Open in Google
   Maps" and, on the town card, the drive-time chip on the same row (since
   3.75) and the address beneath close the card as its utilities, below
   the Call: Open in Google Maps is an EXIT action and
   may not outrank the conversion action. Content, then the ask, then the
   kit. Contact's lines lose their top margin because they now open that
   body, where the button used to.
   (The button reads "Get directions" since 3.144.)
   36px BELOW THE CALL, from the render: the split's body inset, so the
   kit starts as far from the ask as the body starts from the card's edge
   and reads as the card's closing tier. 26, the ask's own air above,
   paired the ghost button with the Call as if it were a second ask; 44
   began to detach the kit from the card. One value at every width. */
.dir-utils { margin-top: 36px; }
.dir-utils .dir-address { margin: 20px 0 0; }
/* THE CARD'S CALL SITS LEFT, 3.76 (Greg's ruling, option B), closing
   3.73's open item: the card's content and its kit are left-aligned, and
   a centred button between them read adrift. One rule, both cards. It
   has to outrank .prose + .cta-row, which centres a row following a
   .prose anywhere and is what centred the town card's Call; three classes
   beat that rule's two wherever either sits in the file. */
.dir-card .dir-body > .cta-row { justify-content: flex-start; }
/* THE SPLIT. Two equal columns, the map first. 1010px IS THE RENDER'S
   NUMBER, not a round one: at equal columns it is the narrowest card at
   which Jamison's steps rewrap short enough that the body ends no lower
   than the portrait map and its credit (map 16px the longer; at 1005 the
   body runs 15px past it). Measured in proposed-changes.md 3.70.
   SUPERSEDED BY 3.73: THE CARD SPANS THE WRAP. The respace's 36px inset
   narrows the text, and at 1010 the body then runs 52px past the map. By
   the same balance test the card takes the wrap's full column, 1080 at
   1440, where the body ended 3px short of the map and its credit. The map
   stops growing there, because the column does.
   NO LONGER BALANCED SINCE 3.74: the reorder gave the getaway kit its own
   tier below the Call, and the town body ran 76px past the map at 1440.
   3.75 put the chip back on the button's row and it runs 13px past. The
   card is already at the full column, so width cannot restore it;
   recorded in proposed-changes.md 3.74 and 3.75, not re-tuned.
   Contact's landscape embed FILLS ITS COLUMN'S HEIGHT rather than keeping
   4:3: its details, hours and Call stand 475px tall at every card width,
   and a 4:3 embed never reaches that, so it would leave a white gap
   beside them. It is 504 by 475 at the full card, still wider than tall.
   The column stretches, so the town's figure does too, and its map keeps
   its own ratio at the top, which is where it was. SINCE 3.117 the town
   card carries the same embed, so it fills its column the same way:
   539 by 629 on Warminster and 539 by 760 on Jamison at 1440, the card's
   height unchanged because the body sets it.
   AFTER THE CARD'S BASE RULES ON PURPOSE: it overrides .dir-embed's
   4:3 at equal specificity, so it has to come later in the file. */
@media (min-width: 900px) {
  .dir-card { max-width: none; display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
  /* At the split the body is a column beside a picture, so it takes a
     wider inset than the stacked card: 36px, from the render, the most
     the text can take while the body still ends level with the map. */
  .dir-body { padding: 36px; }
  .dir-embed { aspect-ratio: auto; height: 100%; }
}

/* ===========================================================================
   THE LIFT. ADOPTED 2026-09-10, FROM CANDIDATE C OF drafts/motion/.
   A MOLD BEHAVIOUR: every .card, .step and .svc on every page gets it,
   and nobody re-decides it per page.

   WHAT HAPPENS. The card rises 3px. An ink shadow comes up underneath
   it. The oxblood rule along its base sharpens from quiet to solid and
   sweeps from a short mark to the card's full width. That sweep is the
   branded half: the lift says "this is a thing", the rule says whose.

   WHAT MAY MOVE, AND IT IS A SHORT LIST: transform, opacity, and the
   rule's width. Nothing else. No colour animates, no shadow animates,
   no height animates. The shadow is painted ONCE on a layer that sits
   at opacity 0 and only its opacity moves, because animating box-shadow
   is expensive and lands on the main thread.

   NO BLUR ON THE RULE AND NO GLOW ANYWHERE. The rule is a solid 2px bar
   of --ox with no shadow of its own. The palette law allows solid
   colours and this is one.

   TWO PSEUDO-ELEMENTS PER CARD, WHICH IS WHY THE STEP NUMERAL MOVED.
   ::before is the shadow, ::after is the rule. .step::before used to
   carry the "01" and now .step h3::after does. If a future component
   wants the lift, it needs both of its own pseudo-elements free.

   HOVER ONLY, AND ONLY WHERE HOVERING IS REAL. Everything below is
   inside @media (hover: hover), so a touch device never gets a hover
   state welded on by a tap and left there.

   THE PRESS IS NOT A HOVER and sits outside that query on purpose. On a
   phone it is the only confirmation a finger gets that the button took
   the tap.
   =========================================================================== */
.card, .svc, a.svc-card { position: relative; }

.card::before, .step::before, .svc::before, a.svc-card::before {
  content: ""; position: absolute; inset: 0; border-radius: inherit;
  box-shadow: 0 12px 26px -10px rgba(18, 27, 39, .34);
  opacity: 0; pointer-events: none;
}
/* At rest the rule is present but quiet: a short mark at the base, not a
   full underline. A card at rest should still read as flat. */
.card::after, .step::after, .svc::after, a.svc-card::after {
  content: ""; position: absolute; left: 0; bottom: 0;
  width: 28px; height: 2px; background: var(--ox); opacity: .4;
  pointer-events: none;
}
/* On ink and oxblood grounds the rule would be invisible against its own
   band, so it answers in silver instead. Same behaviour, readable ground. */
.dark .card::after { background: var(--silver); }

@media (hover: hover) {
  .card, .step, .svc, a.svc-card { transition: transform 160ms cubic-bezier(.2, .8, .3, 1); }
  .card::before, .step::before, .svc::before, a.svc-card::before {
    transition: opacity 160ms cubic-bezier(.2, .8, .3, 1);
  }
  .card::after, .step::after, .svc::after, a.svc-card::after {
    transition: width 260ms cubic-bezier(.2, .8, .3, 1),
                opacity 160ms cubic-bezier(.2, .8, .3, 1);
  }
  .card:hover, .step:hover, .svc:hover, a.svc-card:hover { transform: translateY(-3px); }
  .card:hover::before, .step:hover::before, .svc:hover::before, a.svc-card:hover::before { opacity: 1; }
  .card:hover::after, .step:hover::after, .svc:hover::after, a.svc-card:hover::after {
    width: 100%; opacity: 1;
  }
  /* THE NUMERAL ANSWERS TOO, added 2026-09-10. At rest "01" is --rule,
     structural and quiet. Under a pointer it goes solid oxblood on the
     same clock as the sweep, so the whole card responds as one gesture
     rather than two. COLOUR ONLY: no blur, no glow, no movement. This is
     the one property outside transform, opacity and the rule's width
     that this stylesheet animates, and it is listed as such above. */
  .step h3::after { transition: color 260ms cubic-bezier(.2, .8, .3, 1); }
  .step:hover h3::after { color: var(--ox); }
}

.btn, .callbar { transition: transform 90ms cubic-bezier(.3, .7, .4, 1); }
.btn:active, .callbar:active { transform: translateY(1px); }

/* REDUCED MOTION. The hover STATE still changes, because that is
   feedback and a reader who asked for less motion still needs to know
   the pointer is on something. What goes is the movement: no rise, no
   sweep, no travel. The shadow and the rule simply are, or are not. */
@media (prefers-reduced-motion: reduce) {
  .card, .step, .svc, a.svc-card,
  .card::before, .step::before, .svc::before, a.svc-card::before,
  .card::after, .step::after, .svc::after, a.svc-card::after,
  .step h3::after,
  .btn, .callbar { transition: none; }
  .card:hover, .step:hover, .svc:hover, a.svc-card:hover { transform: none; }
  .btn:active, .callbar:active { transform: none; }
}

/* ===========================================================================
   ARRIVAL MOTION. ADOPTED 2026-09-10. ONE EFFECT, ONE RULE.

   The lane is the only thing on this site that moves on arrival without
   a reader doing anything to it, and the motion law was amended for it.
   See the amendment in the header of this file. The rule it lives under:
   ONCE, ON A SECTION'S FIRST ARRIVAL. Never looping, never re-triggering,
   never on load. The stat band's counting effect lived here too, and was
   withdrawn on Greg's ruling (3.85) with its rules and its prose.

   The mechanism: site.js adds .motion-armed to a section when the
   effect is actually going to run, and .motion-in when the section
   first crosses into view. Then the observer stops watching it, which is
   what makes "never re-triggering" a property of the code rather than a
   promise in a comment.

   THE "FROM" STATE IS NEVER IN CSS ALONE. The lane is armed by site.js
   and only when motion is allowed, so with JavaScript off, or
   with prefers-reduced-motion set, nothing is ever hidden and nothing is
   ever pending. This is the rule that keeps an entrance effect from
   turning into a blank page when a script fails.
   =========================================================================== */

/* Read by a screen reader, not drawn: /blog/'s "All posts" heading. */
.sr-only {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}

/* ---------- The lane: a road drawn down the process ----------
   The six process steps gain a dashed centre line down the gaps between
   them, painting once, top to bottom. The road to road-ready.

   IT MUST READ AS A LANE, NOT A PROGRESS BAR. No track behind the
   dashes, so nothing is being filled. No state change once painted, so
   nothing is being completed. Constant speed, because easing toward a
   stop is what a progress bar does and a road has no finish, only a next
   dash.

   THE COLOUR IS --rule, NOT OXBLOOD, AND THAT IS THE PALETTE LAW
   TALKING. Oxblood means act, and since the lift shipped it also means
   "you are pointing at this". A decorative line that answers neither
   would be a third meaning, which is how a palette stops meaning
   anything. --rule is what the step numerals already use at rest.

   IT EXISTS ONLY BELOW 720px, WHICH MAKES IT A PHONE-READER'S MOMENT.
   .steps is one column below 720, two to 1040, three above, and a line
   from 01 to 06 only exists in the first of those. Above 720px it draws
   nothing rather than drawing something that is not a road. Whoever
   changes the .steps breakpoints owns this too. */
.lane-host { position: relative; }
.lane {
  position: absolute; left: 50%; top: 0; width: 3px; margin-left: -1.5px;
  pointer-events: none;
}
.lane-dash {
  position: absolute; left: 0; width: 3px; background: var(--rule);
  border-radius: 1.5px; transform: scaleY(0); transform-origin: top center;
}
.motion-in .lane-dash {
  transform: scaleY(1);
  transition: transform 220ms linear;
  transition-delay: var(--dash-delay, 0ms);
}
@media (min-width: 720px) { .lane { display: none; } }

/* REDUCED MOTION. THE LANE DOES NOTHING: it rests fully drawn. site.js
   is the first guard and never arms it; this is the second, and it also
   covers the FAQ chevron and its answer. */
@media (prefers-reduced-motion: reduce) {
  .lane-dash, .faq-ico { transition: none; }
  .lane-dash { transform: scaleY(1); }
  #faq details[open] > p { animation: none; }
}

/* ===========================================================================
   THE HERO ARCHITECTURE. AMENDED 2026-09-10.

   EVERY HERO IS TEXT OVER PHOTOGRAPH, inside a measured scrim, with the
   one-time entrance. THE PAGE TYPE LIVES IN THE SCRIM'S COLOUR: INK on
   the front door, OX on a service page.

   EXCEPT WHERE THE PAGE'S SUBJECT IS INVISIBLE TO A CAMERA, amended
   2026-10-01 (proposed-changes.md 3.86). There a licensed, provenance-
   clean, human-made illustration may serve, recorded per page with its
   reason. ONE PAGE: /adas-calibration/, whose subject is sensor fields
   and camera aim. A second earns its own ruling, and
   scripts/prepare-hero-photo.py refuses one without it. Same .heroB, same
   scrim, measured the same way.

   THIS SUPERSEDES "HOME IS FULL-BLEED, SERVICE PAGES ARE SPLIT", which
   stood for four days. The split hero is gone; see the tombstone above
   .heroB for what went with it. The distinction it was drawing is worth
   keeping and is now drawn in colour instead of in shape, which costs
   one variable rather than two hero implementations.

   ONE IMPLEMENTATION, NOT A COPY. .heroB is the hero; .heroB--ox is a
   modifier that sets --scrim-rgb and --scrim-ground. Both pages ride the
   same structure, the same scrim geometry, the same button grammar, and
   THE ENTRANCE KEYFRAMES EXIST ONCE.

   THE OX GROUND IS WEAKER THAN INK AND EVERYTHING FOLLOWS FROM THAT.
   Measured over a white photo pixel: silver needs alpha .758 on ink to
   clear 7:1 and .849 on ox. --silver-2, the secondary tone, needs .858
   on ink and .981 on ox, because --silver-2 on solid ox is only 7.34 to
   begin with. SO THERE IS NO SECONDARY TEXT TONE ON AN OX HERO: the lead
   and the breadcrumb are --silver there. On ink, where --silver-2 reads
   10.78, the hierarchy stays.

   WHAT STILL DIVERGES BETWEEN THE TWO PAGE TYPES, and it is only shape:

     scrim       ink on the front door, ox on a service page.
     .statcard   the home stats worn as a white card overlapping the
                 hero's seam. Service pages keep the band, which is why
                 the ox hero's foot can come in: no card to clear.
     .svc-cards  a photographic router grid, which is what a front door
                 is for.
     .proofstrip the hero's chips, on a phone, below the hero.
     rhythm      home carries ONE oxblood act band, not two.

   WHAT DOES NOT DIVERGE, and this list is the reason the rule is
   written down: the palette, the type, the motion laws, the lift, the
   arrival motion, the button grammar, the NAP, the staging exception,
   and every check that enforces them. A different shape is not a
   different site.

   TWO DELIBERATE DEPARTURES FROM THE RULES ABOVE, recorded rather than
   left to be discovered:

   1. THE HERO PANEL IS INK HERE, NOT OXBLOOD. Design rule 1 says the
      panel's ground is oxblood, and on a split hero it is. Here the
      panel runs the full width under a photograph, and a full-width
      oxblood band at the top of the page spends the act colour before
      the page has asked for anything. Ink is structural, which is what
      a panel under a photograph is.

   2. HOME CARRIES ONE OXBLOOD ACT BAND, NOT TWO. Two is right on a long
      service page a reader scrolls through deciding. The front door
      routes rather than argues: it asks once, after the diagram, and
      its other asks are the services grid and the call bar.

      THE WORD "ACT" IN THAT SENTENCE IS LOAD-BEARING SINCE 2026-09-17.
      Home now carries a second oxblood GROUND, #who-we-are, which asks
      for nothing and carries no button. The act-band count is one on
      this page and it did not move. See .dark for the amendment.

   ON A PHONE THE HERO IS SOLID GROUND AT CONTENT HEIGHT, AMENDED
   2026-10-20 (proposed-changes.md 3.106), Greg's ruling from his phone
   review. Below 600px: no photograph, no veil, no minimum height; the
   ground is --scrim-ground, ink on the front door and ox on a service
   page, which .heroB already paints. Measured at 390x844 on
   /collision-repair/: 116px between the header and the breadcrumb, over
   a photograph the phone scrim (.94 to .97 alpha up to 95%) left
   invisible but for a sliver at the top edge, and a 270KB JPEG fetched
   at high priority for it. The rule was buying height and bytes for
   nothing. All six .heroB pages, the ADAS illustration included, because
   the hero is one implementation; on a phone it now matches the compact
   ox page header. At 600px and up nothing changed: the photograph sits
   right of the copy there and stays. Each hero <img> sits in a <picture>
   whose phone <source> is a 1x1 transparent GIF, so a phone does not
   fetch the photograph; the <img> itself is unchanged.

   ONE COPY COLUMN, AND THE SCRIM HUGS IT, AMENDED 2026-10-20
   (proposed-changes.md 3.110), Greg's ruling. At 600px and up the hero's
   copy lives inside --hero-col, the H1 included, and the scrim ends just
   past that column with a short fixed fade, so the photograph shows from
   there to the frame's edge. See .heroB-veil at 600px and up.
   HISTORY since 3.112, below: the veil is retired.

   TEXT ON GROUND, PHOTOGRAPH IN ITS OWN ZONE, AMENDED 2026-10-20
   (proposed-changes.md 3.112), Greg's ruling from his localhost review.
   Full-bleed behind the scrim, a hero showed about a quarter of its
   photograph: the 3:2 frame was cropped to the hero's ~2.4:1 shape and
   the scrim covered the left half or more, so the subjects read cut off.
   THE SPLIT ARRANGEMENT RETIRED ON 2026-09-10 RETURNS IN A NEW FORM:
   the copy sits on solid --scrim-ground, and the photograph takes its own
   zone from --hero-gutter past the copy column's right edge to the
   viewport's right edge, at full hero height, object-fit: cover, CENTRED
   ON ITS SUBJECT (--focus-x and --focus-y on each <img>, written by
   scripts/sync-hero-focus.py from the frame's measured subject box, never
   by hand). Its left edge fades into the ground over --hero-fade, so it
   reads as one hero, not two panels. NO GLYPH SITS OVER THE PHOTOGRAPH,
   by construction and measured, so the readability law has nothing to
   measure on a hero any more and the veil is retired; its 600 to 820
   failure (3.108) is resolved by construction. BELOW 1050px THE HERO IS
   SOLID GROUND AT CONTENT HEIGHT, 3.106's phone rule extended upward:
   there the zone is under 362px wide and no subject survives it (measured
   in 3.112), and the <picture>'s empty source covers the same widths, so
   they never fetch the photograph.

   THE PANEL IS OPAQUE, WHICH IS WHY IT IS A PANEL AND NOT A SCRIM. The
   readability half of design rule 1 is not shape and does not diverge:
   type over a photograph needs something solid behind it. So the
   photograph runs full-bleed and undimmed ABOVE the panel, and the
   panel is --ink at full strength with --silver on it at 15.42. A
   translucent wash over the image would have read as the same idea and
   would have been the one part of rule 1 that is not negotiable.
   =========================================================================== */
.heroB {
  /* THE PAGE TYPE LIVES IN THIS ONE VARIABLE. Ink on the front door, ox
     on a service page. Everything else about the hero is shared: the
     structure, the scrim geometry, the entrance keyframes, the button
     grammar. One implementation, a modifier, not a copy. */
  --scrim-rgb: var(--ink-rgb);
  --scrim-ground: var(--ink);
  /* The H1's size and its own 17ch cap, held once (3.108). At 600px and
     up the H1 is capped at the smaller of this and --hero-col (3.110). */
  --hero-h1-size: clamp(2.5rem, 5.2vw, 4rem);
  --hero-h1-measure: 17ch;
  position: relative; overflow: hidden; background: var(--scrim-ground); padding: 0;
  display: flex; align-items: flex-end;
  min-height: clamp(420px, 84vh, 600px);
}
/* OX IS A WEAKER GROUND THAN INK AND THE SCRIM IS DEEPER FOR IT. Measured:
   over a white photo pixel, silver needs alpha .758 on ink to clear 7:1 and
   .849 on ox; the lead in --silver-2 needs .858 on ink and .981 on ox,
   because --silver-2 on solid ox is only 7.34 to begin with. So the ox
   scrim runs near-solid where the copy sits and does its fading further
   out. The standard did not move; the scrim did. */
.heroB--ox { --scrim-rgb: var(--ox-rgb); --scrim-ground: var(--ox); }
/* THE PHOTOGRAPH'S ZONE, 3.112. Below 1050px there is none: the <picture>
   is not rendered and its source is an empty GIF, so nothing is fetched.
   From 1050 the <picture> IS the zone: from --hero-gutter past the copy
   column's right edge (the same edge the 3.110 scrim measured from) to the
   hero's right edge, full height. It is a size container so the <img> can
   centre its focus point in it: the cover-fitted image is
   max(100cqw, 150cqh) wide (every hero is the 1200x800 contract, 3:2),
   and the offset that puts --focus-x at the zone's centre is clamped so
   the image never leaves an edge uncovered. The plain percentages first
   are the fallback where container units are unknown. The mask is the
   short soft edge into the ground. */
.heroB > picture { display: none; }
@media (min-width: 1050px) {
  .heroB > picture {
    display: block; position: absolute; top: 0; bottom: 0; right: 0;
    left: calc(max(var(--wrap-pad), (100% - var(--wrap)) / 2 + var(--wrap-pad))
               + min(var(--hero-col), 100% - 2 * var(--wrap-pad))
               + var(--hero-gutter));
    container-type: size;
    -webkit-mask-image: linear-gradient(90deg, transparent, #000 var(--hero-fade));
    mask-image: linear-gradient(90deg, transparent, #000 var(--hero-fade));
  }
  .heroB-photo {
    --fit-w: max(100cqw, 150cqh);
    --fit-h: max(100cqw / 1.5, 100cqh);
    position: absolute; inset: 0; width: 100%; height: 100%;
    object-fit: cover;
    object-position: calc(var(--focus-x, .5) * 100%) calc(var(--focus-y, .5) * 100%);
    object-position:
      clamp(100cqw - var(--fit-w), 50cqw - var(--focus-x, .5) * var(--fit-w), 0px)
      clamp(100cqh - var(--fit-h), 50cqh - var(--focus-y, .5) * var(--fit-h), 0px);
  }
}

/* THE SCRIM IS A SECOND SANCTIONED SECTION-BACKGROUND GRADIENT, dated
   2026-09-10, and it is recorded in the palette note with the first one.
   It is not a gradient on text, a button, a card or an accent; it is the
   ground a section's type sits on, which is the one exception the
   palette law carves out.

   PARTIALLY OVER THE IMAGE, NOT A FULL-FRAME DARKENING. At 600px and up
   it anchors LEFT and fades rightward, so the right of the frame stays
   clear photograph doing work.
   HISTORY, 2026-10-20 (3.106): until then, on a phone, it anchored to the
   BOTTOM edge and faded upward, so the top of the frame was meant to stay
   clear photograph. At .94 to .97 alpha up to 95% it left only a sliver
   visible, so a phone hero now carries no photograph and no scrim.

   THE STOPS ARE MEASURED, NOT CHOSEN. See the readability amendment in
   this file's header: every overlaid text element's composited pixels
   are sampled out of a real render and the worst case under any glyph
   box has to clear 4.5, with 7 the target for body-size type. If the
   photograph defeats the scrim, the scrim deepens. The standard does
   not move.

   RETIRED 2026-10-20 (proposed-changes.md 3.112). The photograph left the
   copy's ground for its own zone, so no glyph sits over it and there is
   nothing for a scrim to do. The .heroB-veil element and its gradients,
   this bottom-anchored one and the 3.108/3.110 left-anchored one, were
   deleted from the six pages and from this file; git history keeps them.
   The palette note's second sanctioned gradient goes with it. */

/* THE BOTTOM PADDING IS THE STAT CARD'S OVERLAP PLUS AIR, and since
   2026-09-13 it says so in arithmetic rather than in a comment. The card
   is opaque white and rides up over the hero's bottom seam by
   --statcard-overlap; without this padding it sits ON the CTA buttons.
   The contrast probe found that before the eye did: the ghost button's
   glyph run measured 1.12 against silver, which is silver on white,
   which is the card.

   THE AIR IS --statcard-air AND NO NUMBER IS TYPED HERE. It is a clamp,
   it lives in :root beside --statcard-overlap, and that token is its one
   home; repeating its figures in this comment would be a copy that
   nothing updates, the same convention ICO_BOX follows. Everything else
   still comes from the overlap, so the gap between the buttons and the
   card's top edge is the air by construction at every width where the
   hero is content driven, and more where min-height governs. It used to
   be whatever two independent clamps happened to leave: 12px at 1440,
   6px at 390. See --statcard-overlap for the whole lesson.

   THE AIR'S FLOOR IS THE VALUE THAT SHIPPED BEFORE IT, so a phone's hero
   is exactly as tall as it was. The 390 hero already runs about two and
   a half screens and it does not pay for a rhythm that only reads on a
   desktop.

   AND THE AIR IS SPENT BELOW THE CTA ROW, NEVER INSIDE THE HERO. Interior
   spacing was considered on 2026-09-22 and declined: the copy cluster and
   the buttons are one unit, and air between them pushes the CTA row DOWN,
   which is the element the fold is measured against on a phone. Bottom
   padding moves the card away from the buttons without moving the buttons,
   so the fold arithmetic in scripts/mobile-check.md is untouched by
   construction rather than by re-measurement. */
.heroB-copy {
  position: relative; z-index: 1; width: 100%;
  padding: clamp(10px, 4vh, 44px) 0 calc(var(--statcard-overlap) + var(--statcard-air));
}
.heroB h1 { font-size: var(--hero-h1-size); color: var(--silver); margin-bottom: .26em; }
/* BALANCE IS POLISH ONLY, 3.110. It never widens a line or adds one, and a
   browser without it falls back to the greedy wrap inside the same cap, so
   the scrim's geometry depends on the cap and never on this. */
.heroB h1 { text-wrap: balance; }
.heroB .eyebrow { color: var(--silver); }
.heroB .lead { color: var(--silver-2); font-size: 1.12rem; max-width: 46ch; }
/* ON OX THE LEAD IS --silver, NOT --silver-2, AND THAT IS ARITHMETIC.
   --silver-2 on SOLID ox is 7.34, so against a 7:1 target it has 0.34 of
   headroom and any photograph showing through at all takes it under; it
   measured 6.57. Deepening the scrim to opaque would just about reach 7.34
   and would put no photograph under the lead at all, which is the thing
   this hero exists to do. --silver on solid ox is 10.50, so the scrim can
   stay short of opaque and the picture still reads. The standard did not
   move: the type colour with headroom on this ground did.
   THE BREADCRUMB GOES WITH IT for the same arithmetic: it measured 6.34
   to 7.07 in --silver-2 and clears comfortably in --silver. ON AN OX
   GROUND OVER A PHOTOGRAPH THERE IS NO SECONDARY TEXT TONE, because
   --silver-2 tops out at 7.34 there and a target of 7 leaves nothing for a
   photograph. On ink, where --silver-2 reads 10.78, the hierarchy stays.

   SCOPED 2026-09-17, NOT RELAXED. That sentence used to say "on an ox
   ground", full stop, and it was read as a law about the colour when it is
   an argument about the photograph: 0.34 of headroom is spent by the first
   image pixel that shows through a scrim, and it measured 6.57 here. A BAND
   HAS NO PHOTOGRAPH UNDER IT, so on the oxblood prose band --silver-2 is
   7.34 and stays 7.34, which was measured on the render rather than assumed.
   See .dark .prose p and proposed-changes.md 3.25. */
.heroB--ox .lead, .field-ox .lead { color: var(--silver); }
.heroB .btn { border-color: var(--silver); }
.heroB .btn-ghost { color: var(--silver); }
.heroB .cta-row { margin-top: 24px; }
.heroB .crumb ol { color: var(--silver-2); }
/* THE CRUMB'S LINK WAS INHERITING THE PAGE LINK COLOUR, which is oxblood,
   and on the ox hero that measured 1.42: oxblood on oxblood. Caught by the
   probe, not the eye. */
.heroB .crumb a { color: var(--silver); }
/* SOURCE ORDER, NOT SPECIFICITY, decides this one: .heroB .crumb ol and
   .heroB--ox .crumb ol are both (0,2,1), so the override has to sit after
   the rule it overrides. It did not, the first time, and the probe read
   the same 6.34 twice, which is how the mistake announced itself. */
.heroB--ox .crumb ol, .field-ox .crumb ol { color: var(--silver); }
/* THE TWO .field-ox SELECTORS ABOVE, added 2026-09-24 for /contact-us/'s
   compact ox header, proposed-changes.md 3.54. They are the ox hero's own
   declarations extended to a band, not new values: the lead and the
   breadcrumb take --silver on ox for the arithmetic the note above gives,
   and a band has no photograph, so the numbers only get better. Measured
   on the render against the band's brightest stop, --ox: lead and crumb
   text in --silver in 3.54. The crumb's link needs nothing, because .dark a
   already gives it --silver; its separator keeps the base --rule, measured
   there too. Without these two, .crumb ol and .lead print --ink-2 on ox,
   which is dark on dark. */
.heroB .crumb li + li::before { color: rgb(var(--silver-rgb) / .5); }
/* THE TOWN HEADER'S CHIPS ARE GONE, 3.67: the extensions 3.63 added here
   (.hero.field-ox beside .heroB) served only the town header, and Greg's
   ruling of 2026-09-29 took the chip row out of it. The service heroes'
   rules below are untouched. */
.heroB .badges { margin-top: 22px; }
.heroB .badges li { background: transparent; border-color: rgb(var(--silver-rgb) / .55); color: var(--silver); }
.heroB .badges svg { stroke: var(--silver); }
/* CHIPS IN THE HERO ARE A DESKTOP ARRANGEMENT ONLY. Four rows of them
   over a phone photograph costs about 210px, which fails the fold and
   buries the picture. Below 600px the strip below the hero carries them
   instead. See .proofstrip. */
@media (max-width: 599px) { .heroB .badges { display: none; } }

/* THE PHONE COPY BLOCK IS TIGHTENED, because on a phone the block's own
   height is what decides whether the CTA clears the fold, not the hero's
   min-height. Measured at 360x640 the block came to 600px on its own.
   H1 stays above a service page's 2.3rem floor, which is the rule; the
   lead, the gaps and the bottom padding come in around it. */
@media (max-width: 599px) {
  /* SOLID GROUND AT CONTENT HEIGHT, 3.106: no photograph, no veil, no
     minimum height on a phone. See the hero architecture note. The
     <picture> around each hero <img> stops the phone fetching it. */
  .heroB { min-height: 0; }
  .heroB h1 { font-size: 2.4rem; margin-bottom: .16em; }
  .heroB .eyebrow { margin-bottom: 6px; }
  .heroB .lead { font-size: .98rem; line-height: 1.46; margin-bottom: 0; }
  .heroB .cta-row { margin-top: 12px; gap: 12px; }
  .heroB-copy { padding: clamp(10px, 4vh, 44px) 0 calc(var(--statcard-overlap) + var(--statcard-air)); }
  /* THE OX HERO SITS ABOVE A BAND, NOT THE CARD, so its foot is its own
     value and derives from nothing. This is the phone half of the pair;
     the desktop half is in the min-600 block below, and it is unchanged.
     The value is what a service page has always had on a phone, stated
     here rather than inherited from the rule above it, because that rule
     now belongs to the page that carries the card. */
  .heroB--ox .heroB-copy { padding-bottom: clamp(50px, 7vh, 58px); }
}

@media (min-width: 600px) {
  /* SOLID GROUND AT CONTENT HEIGHT UNTIL THE ZONE OPENS, 3.112: no
     minimum height below 1050, 3.106's phone rule extended upward. */
  .heroB { align-items: center; min-height: 0; }
  /* THE SCRIM HUGGED THE COPY COLUMN, AMENDED 2026-10-20 TWICE, AND IS
     RETIRED THE SAME DAY (3.112). History:
     3.108: the strong stop used to be 52% of the viewport, which is where
     the copy ends at 1440 and nowhere else; between 600 and about 820 the
     copy runs to 66 to 91% of the frame and every service hero failed
     there (an H1 at 1.69, leads at 1.75 to 3.72, chips at 1.13 to 2.99).
     3.108 derived the stop from the copy's geometry. 3.110 put all the
     copy inside --hero-col and held the scrim .97 flat to one --wrap-pad
     past the column, then a fixed 112px fade. 3.112 moved the photograph
     out from behind the copy altogether, which resolves the 600 to 820
     failure by construction: there is no photograph under any glyph. */
  .heroB-copy { padding: clamp(18px, 1.8vw, 24px) 0 calc(var(--statcard-overlap) + var(--statcard-air)); }
  /* THE CHIP ROW IS HELD INSIDE THE COPY COLUMN (written in 3.110 as "inside
     the scrim's strong zone"; since 3.112, left of the photograph's zone). Unconstrained,
     four chips run 917px and reach 76% of a 1440 frame, which is inside
     the fade: the rightmost glyphs measured 1.06. Capped, they wrap to
     two rows that end at 46% and clear comfortably. */
  .heroB .badges { max-width: var(--hero-col); }
  /* The ox hero sits above a stat BAND, not the home page's floating
     card, so there is no overlap to clear and the foot can come in. That
     is what buys the second chip row without passing 600px of depth. */
  .heroB--ox .heroB-copy { padding-bottom: clamp(36px, 3.4vw, 48px); }
  .heroB h1 { max-width: min(var(--hero-col), var(--hero-h1-measure)); }
  .heroB .lead { max-width: min(var(--hero-col), 50ch); }
}
@media (min-width: 1050px) {
  .heroB { min-height: clamp(480px, 58vh, 560px); }
}
@media (min-width: 1100px) {
  .heroB .lead { max-width: min(var(--hero-col), 52ch); }
}

/* THE ENTRANCE. One-time, on load, CSS only, opacity only.
   THE GUARD IS THE POINT: base opacity is 1 in the cascade and the fade
   exists ONLY as keyframes inside prefers-reduced-motion: no-preference,
   starting from 0 INSIDE the keyframes. So reduced motion, an old
   browser, a failed stylesheet or any no-animation context all rest with
   everything visible. Nothing is ever hidden waiting for an animation to
   un-hide it, which is the same rule the arrival motion follows.
   Fires once per load. Never on scroll, never on a loop. */
.heroB-photo, .heroB-copy { opacity: 1; }
@media (prefers-reduced-motion: no-preference) {
  @keyframes heroB-in { from { opacity: 0; } to { opacity: 1; } }
  .heroB-photo { animation: heroB-in 500ms ease-out both; }
  .heroB-copy { animation: heroB-in 500ms ease-out 200ms both; }
}

/* ---------- The proof strip: the hero's chips, on a phone ----------
   ONE LIST, TWO PLACES, AND ONLY ONE OF THEM RENDERED AT A TIME. The
   chips are a desktop hero arrangement and a phone band, which is two
   arrangements of the same four claims. A single element cannot occupy
   two DOM positions, so there are two lists and exactly one is displayed
   at any width; display:none keeps the hidden one out of the
   accessibility tree as well as off the screen.

   THE STRIP IS PAGE, NOT HERO, so it does not animate. The entrance
   belongs to the hero and stops at its edge.
   ITS FOOT MATCHES ITS HEAD, 20px both, 3.107: the 4px foot had no
   recorded reason and sat the chips tight on the stat band. */
.proofstrip { padding: 20px 0; }
.proofstrip .badges { margin: 0; }
@media (min-width: 600px) { .proofstrip { display: none; } }

/* ---------- The stat band, worn as jewellery ----------
   SAME .statband CLASS ON PURPOSE: the same three stats in the same
   type as a service page's band. .statcard only changes where the white
   sits.

   COMPOUNDED, SO IT WINS ON SPECIFICITY AND NOT ON FILE ORDER. .statband
   declares its own background and border-bottom about 300 lines below
   this block, and .statcard alone ties it at (0,1,0), so until
   2026-09-13 the base won on source order: the section painted white
   edge to edge with a full-width hairline, the negative margin pulled it
   up over a photograph it then erased, and the card never floated over
   anything. THE LESSON, one line: an override that wins only because of
   where it sits in the file is a trap waiting to re-arm, so a modifier
   compounds with the class it modifies. Every property the base sets is
   answered here by name. */
.statband.statcard {
  background: transparent; border-bottom: 0; padding: 0;
  /* THE OVERLAP IS NOT WRITTEN HERE. It is --statcard-overlap, because
     the hero copy's foot has to be the same number plus air, and the
     two used to be separate clamps that drifted. */
  margin-top: calc(-1 * var(--statcard-overlap)); position: relative; z-index: 2;
}
.statcard-inner {
  background: var(--white); border-radius: var(--radius);
  padding: clamp(26px, 3.4vw, 42px) clamp(18px, 2.6vw, 36px);
  box-shadow: 0 18px 44px -20px rgba(18, 27, 39, .40);
}

/* ---------- The services router: photographs, not labels ----------
   THE WHOLE CARD IS THE LINK where there is a page to link to, and where
   there is not the card is a div with data-pending-href and NO "learn
   more" text. A label that looks like a link and is not one is worse
   than no label, and three of these four had one until this rebuild.
   THE LIFT IS ON THE LINKED CARD ONLY, for the same reason: a card that
   rises under a pointer and then does nothing is a promise the markup
   cannot keep. */
.svc-cards { display: grid; gap: 24px; }
/* A LONE LAST CARD SITS CENTRED, 3.87, by 3.55's ruling for the two-up
   grid ("pinned left it reads as a card that lost its partner"), applied
   here when the fifth service made the router odd. The same construction
   as .grid2: twice the tracks, every card spanning two, so the gap stays
   the one value above. Two-up a lone card starts at track 2 of 4.
   THE WIDE ROUTER IS THREE AND TWO, 3.88, Greg's ruling: four across left
   the fifth service alone beneath, an afterthought, so 3.55's principle
   reaches from centring the odd card to balancing the rows. Six tracks,
   three cards a row, and a two-card last row starts at track 2 of 6, so
   the pair sits centred. The card order does not change. It catches the
   two-card remainder only, which is the only remainder the router has. */
@media (min-width: 760px) {
  .svc-cards { grid-template-columns: repeat(4, 1fr); }
  .svc-cards > * { grid-column: span 2; }
}
@media (min-width: 760px) and (max-width: 1119px) {
  .svc-cards > :last-child:nth-child(odd) { grid-column: 2 / span 2; }
}
@media (min-width: 1120px) {
  .svc-cards { grid-template-columns: repeat(6, 1fr); }
  .svc-cards > :nth-last-child(2):nth-child(3n+1) { grid-column: 2 / span 2; }
  .svc-cards > :last-child:nth-child(3n+2) { grid-column: 4 / span 2; }
}
.svc-card {
  display: flex; flex-direction: column; text-decoration: none; color: inherit;
  background: var(--white); border: 1px solid var(--rule); border-radius: var(--radius);
}
.svc-card img {
  display: block; width: 100%; aspect-ratio: 4 / 3; object-fit: cover;
  border-radius: var(--radius) var(--radius) 0 0;
}
.svc-card-body { padding: 18px 20px 22px; }
.svc-card h3 { margin-bottom: .35em; }
.svc-card p { font-size: 1rem; color: var(--ink-2); margin: 0; }
a.svc-card h3 { color: var(--ox-tx); }
/* SERVICE CARDS ON A DARK GROUND, 3.91: the Related Services band on the
   five service pages is .dark.field-ox. The cards take the dark-ground
   card grammar (.dark .card): a .08 silver wash and silver type. MEASURED
   ON THE RENDERED BAND, every pixel under every glyph run with the glyphs
   transparent, at 360, 390 and 1440: the card headings and lines 8.81
   worst, past the 7 body target; the band's H2 10.50.
   THE BORDER RISES TO .50, NOT THE .28 .dark .card SHIPS. These cards are
   LINKS, and a link card's edge is what tells a reader where the target
   is, so it meets the 3:1 floor the law sets for a shape. .28 renders
   1.98 against the wash and 2.35 against the band; .50 renders 3.34 and
   3.99, worst case. The ink precedent, .dark .card at .28, is not a link.
   The rule along the base answers in silver, as on .dark .card, because
   --ox on --ox is invisible. */
.dark .svc-card { background: rgb(var(--silver-rgb) / .08); border-color: rgb(var(--silver-rgb) / .50); }
.dark a.svc-card h3, .dark .svc-card p { color: var(--silver); }
.dark a.svc-card::after { background: var(--silver); }

/* ---------- We Fix It All: the licensed render and the eight ----------
   Replaced the drawn intake diagram on 2026-09-17, on the client's
   ruling. What went with it: .diagram-svg, its three .dg line weights,
   .dg-lead, .dg-label, the .dg-notes hide below 900px, and the
   .diagram-list visually-hidden rule above it. Every one of those
   selectors was scoped to .diagram-svg or .diagram-list and grepped
   across every page and the template before deletion.

   .ticks WAS NOT TOUCHED. It sat in the same section and is still worn
   by /collision-repair/, which is exactly the trap the 2026-09-13 note
   in CLAUDE.md is about: a base class that merely lives near the block
   being swept belongs to the site, not the section.

   THE RENDER IS A LICENSED WIREFRAME, not a drawing of ours and not a
   photograph. AdobeStock_1604222224, provenance verified before use.
   It is greyscale line art on a ground the asset pipeline pulls to true
   black, so `mix-blend-mode: screen` composites it onto the ink band
   with no alpha channel and no cutout: screen leaves a ground of zero
   exactly as it found it. scripts/prepare-car-render.py holds the crop
   box, the black point and the measured contrast, and prints all three.

   MEASURED ON THE INK GROUND, screened, against the 3:1 a non-text
   graphic needs: the median line reads 8.51:1 on the 2x file and 4.01:1
   on the 1x, and the JPEG's ringing around a line reads 1.05:1, which
   is below sight rather than a glow. No level lift was applied, because
   the median already cleared; the pipeline still solves one if a future
   size or ground pushes it under.

   THE ITEMS TAKE THE RULE SWEEP AND NOT THE LIFT. Kind-one motion only:
   the ::after rule sweeps from its quiet 28px mark to full width on
   hover, on the same clock and easing as every card on the site. The
   lift's 3px rise and its ink shadow are deliberately absent, because
   the brief bans the float. Hover-only, and the state still answers
   under prefers-reduced-motion since only width and opacity move.

   FOUR ACROSS, TWO ROWS, which is the client's sizing. Two across from
   600 to 999 and one column below that. Eight items in one column on a
   phone is the longest this band gets, and the render costs almost
   nothing there: at 2.74:1 a 350px column renders it 128px tall, so the
   height cap the brief asked for turned out to be unnecessary rather
   than omitted. */
.carx {
  display: block; width: 100%; height: auto;
  mix-blend-mode: screen;
}

.fixgrid {
  display: grid; gap: 34px 28px; grid-template-columns: repeat(4, 1fr);
  list-style: none; margin: 40px 0 0; padding: 0;
}
.fix { position: relative; padding-bottom: 16px; }
/* THE MARKS ARE A LICENSED SET NOW, AdobeStock_1964340052, and they are
   FILLED OUTLINES rather than strokes: the file carries 253 fill
   operators and ZERO stroke operators, so `fill: none; stroke: ...`
   renders them as nothing at all. Line weight is therefore a property of
   the artwork and of the scale it is drawn at, and not something this
   stylesheet can set. Adopted 2026-09-22, proposed-changes.md 3.28.

   THE SCALE IS DERIVED AND NOT CHOSEN. The set carries ONE stroke,
   measured at 9.61 to 10.08 across three marks and taken as 9.8 set
   units. The chips are 17px at stroke-width 2.1 on a 24 viewBox, which
   is 1.4875 device px, and the set's own stroke equals that at 54.64px
   per 360 set units. The band ships 56 per 360, so every mark's line is
   1.5244 device px, 2.5% off the chips.

   THIS RETIRES THE BAND'S OLD 2.5px GLYPHS. Those were 30px at
   stroke-width 2, the heaviest icons on the site and a recorded
   deviation from the 17px chip scale. The band now joins the chip
   grammar instead of being the outlier, which is the argument for the
   size rather than a taste for bigger icons.

   NATURAL PROPORTIONS AT ONE SCALE. Every mark keeps its own aspect, so
   the two-car compositions are simply wider than tall. A square box was
   tried and rejected: it shrinks the wide marks to fit and drops their
   stroke to 0.58 and 0.68 of everyone else's, which is visible in a row
   that stands side by side. scripts/prepare-damage-icons.py holds the
   arithmetic and writes each mark's width and height onto its own svg.

   The box is 46px and BOTTOM-ALIGNED, so the cars share a ground line
   and the headings below them stay level. It has to clear the tallest
   mark shipping, and the pipeline FAILS rather than let a re-mapping
   grow one past it. The number is not repeated here on purpose: which
   marks ship is a mapping decision that changes, and a comment quoting
   today's tallest is a copy that nothing updates. ICO_BOX in
   scripts/prepare-damage-icons.py is the one place it lives. */
/* ONE IMPLEMENTATION, TWO GROUNDS, 3.104. The box and the fill/stroke
   grammar live here once, on .ico, and the colour is set per ground
   through --ico-ink: --mark on home's ink band, --ox on a light card
   (/collision-repair/'s Minor and Major cards carry two of these marks,
   byte for byte). A ground that wants a third colour sets --ico-ink; it
   does not copy these rules. */
.ico { --ico-ink: var(--ox); display: flex; align-items: flex-end; height: 46px; }
.fix .ico { --ico-ink: var(--mark); }
.ico svg { display: block; }
/* The set's own marks are fill. The three matched redraws are authored as
   strokes at the set's own 9.8 units, in the same set-unit viewBox, so the
   two render at one weight by construction rather than by anybody having
   matched them up by eye. */
.ico svg .f { fill: var(--ico-ink); stroke: none; }
.ico svg .s {
  fill: none; stroke: var(--ico-ink); stroke-width: 9.8;
  stroke-linecap: round; stroke-linejoin: round;
}
.fix h3 { font-size: 1.06rem; margin: 14px 0 .35em; color: var(--silver); }
.fix p { font-size: .97rem; color: var(--silver-2); margin: 0; }

/* The quiet mark at rest, the same 28px by 2px every card wears, in
   silver because oxblood is invisible against this band. */
.fix::after {
  content: ""; position: absolute; left: 0; bottom: 0;
  width: 28px; height: 2px; background: var(--silver); opacity: .4;
  pointer-events: none;
}
@media (hover: hover) {
  .fix::after {
    transition: width 260ms cubic-bezier(.2, .8, .3, 1),
                opacity 160ms cubic-bezier(.2, .8, .3, 1);
  }
  .fix:hover::after { width: 100%; opacity: 1; }
}

@media (max-width: 999px) {
  .fixgrid { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 599px) {
  .fixgrid { grid-template-columns: 1fr; gap: 26px; }
}

/* ---------- Real Repairs: five before and after pairs ----------
   Added 2026-09-17, with the first real photographs on this build.

   REVISED 2026-09-17 ON THE CLIENT'S RULING: A PAIR SHOWS BOTH FRAMES
   AT ONCE, before beside after, and the tap-to-crossfade is retired.
   The comparison is now made by the layout rather than by a reader
   finding a control.

   THERE IS NO MOTION HERE AND NO STATE. This band used to carry
   kind-one motion, a 400ms dissolve that fired on a tap and never on
   its own; now nothing fires at all. There is no hover state either:
   these are photographs, not links, and a shape that answers a pointer
   is telling a reader it can be clicked.

   NOTHING LEAVES NORMAL FLOW, so nothing needs a JS-off fallback. The
   ten frames are ten images in the flow of the page, each with its own
   chip and each lazy-loaded. The fallback the crossfade needed retired
   with it, because there is no enhanced state left for a reader to
   miss.

   ONE PAIR PER ROW, WHICH IS WHAT THE COMPARISON COSTS. Two pairs
   across a desktop row would be four frames across, and halving every
   photograph buries the thing the section exists to show. Five rows:
   before left, after right, equal widths, the caption under the row it
   describes.

   EQUAL HEIGHTS COME FREE, and that is not luck. Both frames of every
   pair ship at identical pixel dimensions, which
   scripts/prepare-repair-photos.py enforces and fails on, so equal
   widths give equal heights with no aspect-ratio box, no object-fit
   and no crop at render time. Each row is as tall as its own
   photographs are.

   THE PHONE STACKS THE PAIR RATHER THAN SHRINKING IT. Two frames side
   by side in a 350px column are about 165px wide each, which buries a
   crease in a quarter panel. Below 600px the pair reads top to bottom
   instead, before above after, both chips, one caption. */
#real-repairs .repairs-note {
  max-width: 62ch; margin: -18px auto 34px; text-align: center;
  font-size: 1rem; color: var(--ink-2);
}

/* THE SECTION-BOTTOM ASK, CENTRED. One mold, 2026-09-24.

   THE TRIGGER 3.30 PREDICTED HAS FIRED, AND THIS IS THE ANSWER IT ASKED
   FOR. That record centred a single light section with
   #real-repairs .cta-row and said, in as many words, that if a SECOND
   light section ever wanted the same thing, that would be the moment to
   generalise and this was not it. On 2026-09-24 the client put the ask
   at the foot of #process and #factory-certified on /collision-repair/
   as well. Second and third. So the id-scoped rule is gone and this is
   what replaced it. proposed-changes.md 3.44.

   WHAT THE SELECTOR MEANS: a .cta-row that CLOSES a section's .wrap is
   a section-bottom ask, and section-bottom asks centre. It is position,
   not identity, so no page and no section is named and a fourth one
   needs nothing written for it.

   THE BASE .cta-row IS STILL LEFT-ALIGNED AND STILL MUST BE. The hero's
   copy is anchored left inside the scrim and the whole flush-left hero
   reads off that edge, so centring the base rule would move the one
   thing the design turns on.

   THE HERO IS EXCLUDED BY STRUCTURE RATHER THAN BY AN EXEMPTION, which
   is why there is no :not() here and nothing to keep in sync. Both
   heroes nest their copy as .heroB > .heroB-copy > .wrap, so the hero's
   row is not a child of a wrap that a SECTION owns, and the child
   combinator cannot reach it. That was measured on both pages before
   this rule was written, because the home hero's row IS the last child
   of its wrap and a positional selector without the section > step
   would have centred it. See 3.44.

   .dark .cta-row STILL CARRIES THE GROUND, and the two do not collide:
   it sets justify-content AND margin-top 44px, this sets only
   justify-content, so a dark section-bottom row keeps its deeper air
   and a light one keeps the base 26px.

   .prose + .cta-row IS NOW REDUNDANT AND IS DELIBERATELY LEFT STANDING.
   Measured by DOM matching on both pages: it matches 2 rows on
   /collision-repair/ and 0 on /, and the number it matches that this
   mold does NOT is ZERO. So it changes nothing today. It is not swept
   because it is not this rule's to sweep: it centres a row that follows
   a .prose ANYWHERE, including mid-section, and the service-page
   template can still produce that shape. A sweep deletes only what it
   owns. If it is ever removed, re-measure that number first. */
section > .wrap > .cta-row:last-child { justify-content: center; }

.repairs-grid { display: grid; gap: 44px; }

.ba { margin: 0; }
.ba-stack { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.ba-frame { position: relative; }
/* THE PRINTS REST ON A SHADOW, AND IT IS THE LIFT'S SHADOW STANDING STILL.
   Client ruling 2026-09-23, proposed-changes.md 3.33.

   WHY A SHADOW RESTS HERE WHEN EVERYWHERE ELSE IT ANSWERS A POINTER. The
   lift exists to tell a hand that a card is under it: a .card, .step or
   .svc is quiet until a pointer arrives, and the ink shadow is the
   answer. These frames are not cards. Nothing here is hoverable, nothing
   is a target, and there is no gesture to answer. What they are is the
   evidence the whole page argues from, and the client's argument is that
   evidence should read as a physical print lying on the page rather than
   as a picture pasted into it. So the same ink the cards cast under a
   pointer is cast here at rest, softer and at lower alpha, because it is
   describing weight rather than reacting to one.

   It is --ink by its channels and not a grey. A shadow the colour of the
   site's own darkest value is the material argument the lift already
   makes; a default grey glow belongs to nobody. No geometry is invented:
   the radius stays var(--radius), so no new number enters this file.

   Shadows paint OUTSIDE the box, so nothing reflows and no section grows.
   Measured both ways in 3.33.

   ON A DARK GROUND THIS SHADOW IS INK ON INK AND EFFECTIVELY INVISIBLE,
   which is true on /collision-repair/ since 3.38. THIS RULE IS STILL
   DELIBERATELY LEFT ALONE: scoping it off would fork a shared component
   into two implementations to save nothing. If you came here to fix the
   invisible shadow by editing THIS rule: it is not broken.

   THE SANCTIONED ANSWER IS .dark .ba img, further down this file, added
   2026-09-23 on the client's ruling. It paints a true black shadow OVER
   this one on a dark ground rather than replacing it, so the base keeps
   its --ink and nothing forks. proposed-changes.md 3.42. */
.ba img {
  display: block; width: 100%; height: auto;
  border-radius: var(--radius); background: var(--rule);
  box-shadow: 0 12px 28px rgb(var(--ink-rgb) / .16),
              0 3px 8px rgb(var(--ink-rgb) / .10);
}

/* THE CHIPS ARE TWO CONSTRUCTIONS OF ONE ARGUMENT, and the argument is
   that the ground under a chip is a photograph nobody controls.

   NEITHER CHIP TAKES --rule. The established chip wears a --rule
   hairline, which measures 1.46 on its own white fill and cannot edge a
   shape sitting on an unknown picture. A pair of tones can: give the
   chip a light one and a dark one, and whichever way the photograph
   goes, one of them stands off it. That is the same symmetric argument
   that put the silver hairline on the act button, and it is why each
   chip below carries a fill and an edge that sit at opposite ends.

   BEFORE  white fill, ink edge      unchanged since 3.23
   AFTER   ox fill, silver hairline  added 2026-09-23, 3.33

   THE ASYMMETRY IS THE POINT, not an oversight. The client's ruling is
   that the outcome is the branded moment and the wreck is not, so only
   the AFTER chip fills in oxblood. The BEFORE chip is the same argument
   inverted and is deliberately left alone.

   The AFTER chip's own two tones read 10.50 against each other, which is
   --silver on --ox, the same pair the act button carries on a dark
   ground and for the same reason. Measured per frame, at every position
   around every chip's perimeter, in proposed-changes.md 3.23 and
   re-measured in 3.33 when the AFTER chip changed. */
.ba-chip {
  position: absolute; top: 12px; left: 12px;
  background: var(--panel); border: 1px solid var(--ink); border-radius: 999px;
  padding: 5px 12px; font-size: .74rem; font-weight: 800;
  letter-spacing: .1em; text-transform: uppercase; color: var(--ink);
}
/* The outcome wears the brand. Fill and edge only: the geometry, the type
   and the position are the BEFORE chip's, so the two read as one family
   with one of them coloured. */
.ba-frame--after .ba-chip {
  background: var(--ox); border-color: var(--silver); color: var(--silver);
}

.ba figcaption { margin-top: 14px; font-size: .97rem; color: var(--ink-2); }
.ba figcaption strong { display: block; color: var(--ink); font-size: 1.04rem; }

/* THE .ba TEXT GRAMMAR ON A DARK GROUND. Added 2026-09-23 when
   /collision-repair/ put its Real Repairs on ink, proposed-changes.md 3.38.

   SCOPED THROUGH .dark, SO IT IS THE COMPONENT'S OWN RULE AND NOT A PAGE
   ONE-OFF. Any page that gives this section a dark ground gets readable
   captions with nothing written for it, which is what made the section a
   component in 3.34. Measured on --ink: --silver-2 is 10.78 and --silver
   is 15.42, against a 4.5 floor and a 7 target. The tones they replace
   are the reason the rule exists at all: --ink-2 on ink measures 1.77 and
   --ink on ink measures 1.00.

   THE PRINT SHADOW SPEAKS DARK-GROUND HERE, added 2026-09-23 on the
   client's ruling, proposed-changes.md 3.42. The base shadow is --ink by
   its channels, so on the ink ground it is ink on ink and does not read.
   The answer is the same idea in the ground's own language: a TRUE BLACK
   shadow, which reads because black is darker than ink and ink is the
   darkest ground this section can sit on. BLACK IS THE FLOOR, and a
   shadow has to be darker than whatever it lies on.

   IT PAINTS OVER THE BASE RULE RATHER THAN SCOPING IT OFF. .ba img keeps
   its --ink shadow untouched; this rule simply wins the cascade on a dark
   ground. Nothing forks, and a page that never darkens the section never
   sees this rule.

   THE GEOMETRY RUNS LARGER AND THE ALPHA DEEPER THAN THE LIGHT-GROUND
   SHADOW, deliberately. Dark grounds eat shadow contrast: the same
   numbers that describe weight on silver describe nothing on ink. The
   client picked the assertive strength from rendered comparisons for
   exactly that reason, and it is his pick rather than a derivation.

   THE HEADING AND THE KICKER NEED NOTHING. .dark gives the sec-title
   --silver and .dark .sec-sub gives the kicker the same, exactly as
   #what-we-fix gets them on the home page.

   THE INTRO LINE NAMES THE ID, and only because the rule it has to beat
   does. #real-repairs .repairs-note carries an explicit --ink-2 at a
   specificity of 110, so a plain .dark .repairs-note would lose to it.
   Rewriting that selector would have been the tidier fix and a wider
   change than this one needed.

   AND THE TWO CLASSES SIT ON THE SAME ELEMENT, hence #real-repairs.dark
   and not .dark #real-repairs. The descendant form was written first and
   matched nothing: .dark is ON the section, not around it, so there is no
   ancestor for it to describe. The measurement caught it, still showing
   --ink-2 on the intro line while the captions had already turned. */
.dark .ba figcaption { color: var(--silver-2); }
.dark .ba figcaption strong { color: var(--silver); }
.dark .ba img {
  box-shadow: 0 14px 32px rgb(0 0 0 / .62),
              0 4px 10px rgb(0 0 0 / .50);
}
#real-repairs.dark .repairs-note { color: var(--silver-2); }

@media (max-width: 760px) {
  .repairs-grid { gap: 36px; }
  #real-repairs .repairs-note { margin-bottom: 26px; }
}

@media (max-width: 599px) {
  .ba-stack { grid-template-columns: 1fr; gap: 12px; }
}

/* ---------- The brand strip: twelve marks, drifting ----------
   Added 2026-09-17 on the client's ruling, migrating the live
   homepage's manufacturer carousel. It sits between the ink
   testimonials band and the oxblood act band, on silver.

   THIS IS THE MOTION LAW'S THIRD KIND AND IT IS AN AMENDMENT. Kind one
   answers a reader, kind two fires once on arrival, and both are
   recorded in this file's header. CONTINUOUS MOTION WAS FORBIDDEN:
   "nothing loops" is written twice up there. It is now allowed in
   exactly one place, this strip, and the amendment is dated in the
   header and in CLAUDE.md. Anything else that wants to loop needs its
   own amendment, dated, in both places.

   WHY THIS ONE EARNS IT. A row of twelve marks is a row of twelve
   claims a reader recognises rather than reads, and there is no honest
   way to fit twelve legible marks across a phone. The alternatives are
   worse: shrink them until they are unreadable, or hide some behind a
   control, which fails the same test the Real Repairs pairs failed.
   The drift shows every mark at full size on every width, and it is
   slow enough to read rather than to watch.

   THE LIMITS, and they are the whole reason this is allowed:

     one strip     this section only, never a second one
     ~40s          a cycle, which is a drift and not a carousel
     pauses        under a pointer, and on focus-within
     stops         entirely under prefers-reduced-motion
     transform     the only property that animates. No opacity, no
                   colour, no size, and nothing appears or disappears.

   THE RESTING STATE IS THE BASE AND THE MOTION IS THE ADDITION, which
   is the same construction the hero entrance uses and for the same
   reason. The rules outside the media query lay the twelve marks out
   as a wrapped, centred, static row with the duplicate tracks
   DISPLAYED NONE. So reduced motion, an old browser, a stylesheet that
   half loaded, or a browser that never heard of @keyframes all rest
   with all twelve marks visible and nothing moving. NOTHING HERE IS
   EVER HIDDEN WAITING FOR AN ANIMATION.

   NO JAVASCRIPT AT ALL. Not a fallback, not an enhancement: there is
   no script, so a page with scripting off renders exactly this.

   THE GEOMETRY, and it is arithmetic rather than taste. The marquee
   holds THREE identical tracks and translates by exactly one third of
   its own width, which is one track plus one gap, so the second track
   lands precisely where the first began and the loop has no seam. The
   gap lives on the track's own padding-right rather than on the flex
   gap between tracks, because -33.333% has to equal ONE WHOLE UNIT and
   a gap that sits between units is not inside that unit.

   WHY THREE TRACKS AND NOT TWO. What is on screen at the end of a
   cycle is the total width less one unit, so two tracks cover a
   viewport only while one unit is wider than it. A unit is 654px of
   marks plus twelve gaps: 1866px at 1440 and 2094px at 1920, which
   covers those but not a 2560 monitor. THREE tracks cover twice a unit,
   4188px, which covers any screen this site will meet. The third track
   costs twelve <img> elements that the browser has already downloaded.

   THE MARKS CARRY NO GROUND AND NO TONE. They are black alpha masks at
   60px tall, 2x of the 30px they render at, written by
   scripts/prepare-brand-logos.py. How dark they sit on the page is
   --brand-ink here and nowhere else: one number, not twelve baked
   greys in twelve files. MEASURED ON THE RENDER, not predicted: at .72
   over --silver the darkest composited pixel in the strip is
   rgb(67,68,68) and reads 8.70:1, well past the 3:1 a graphic needs,
   and the marks read as the live site's grayscale strip rather than as
   twelve black stamps.

   THE GAP IS FLUID because the unit has to stay wider than the
   viewport at the top of the range and has to not be absurd at the
   bottom: 40px on a phone, 7vw through the middle, 120px at 1714 and
   up. */
#brands {
  padding: calc(var(--pad) * .55) 0;
  --brand-gap: clamp(40px, 7vw, 120px);
  --brand-ink: .72;
}

/* THE STRIP INSIDE A SECTION IS NOT A BAND, added 2026-09-24 on the
   client's ruling, proposed-changes.md 3.43.

   The padding above is BAND padding, and it is right where the strip is
   a band: on the home page #brands is a standalone section between two
   others, and calc(--pad * .55) is what makes it read as one. On
   /collision-repair/ the strip sits INSIDE #factory-certified, between
   the sec-head and the prose, and there the same padding doubles the
   section's own gaps and the marks read as a stripe with moats.

   SCOPED TO THE NESTED CONTEXT ON PURPOSE. The home page's standalone
   band keeps its band padding untouched; only a #brands nested inside a
   section is trimmed, and since 2026-09-24 that is two contexts,
   #factory-certified on /collision-repair/ and #fleet on
   /commercial-collision-repair/ (3.51). This is the section-within-a-section
   answer and it is the same genre as the compound-selector lesson: the
   rule describes WHERE the element is, not what it is.

   THE NUMBERS ARE DERIVED, NOT PICKED. The target is the section's own
   head-to-body grammar, --sec-gap, which .sec-head leaves under the sub
   before body text. Above the strip .sec-head already supplies the whole
   of it, so the strip adds NOTHING; below it nothing else contributes,
   because p has margin-top 0, so the strip supplies all of it. Hence 0
   and --sec-gap rather than one symmetric value: the padding is whatever
   makes each gap equal the grammar, and the two sides do not start from
   the same place. Measured before and after in 3.43. */
#factory-certified #brands, #fleet #brands { padding-top: 0; padding-bottom: var(--sec-gap); }

.brandstrip { overflow: hidden; }

/* THE RESTING STATE. A wrapped, centred, static row of all twelve. */
.brandmarquee { display: flex; justify-content: center; }
.brandtrack {
  display: flex; flex-wrap: wrap; justify-content: center; align-items: center;
  gap: 22px var(--brand-gap);
  margin: 0; padding: 0; list-style: none;
}
.brandtrack[aria-hidden="true"] { display: none; }
.brandtrack img {
  display: block; height: 30px; width: auto;
  opacity: var(--brand-ink);
}

@media (prefers-reduced-motion: no-preference) {
  .brandmarquee {
    width: max-content; justify-content: flex-start;
    animation: brand-drift 40s linear infinite;
  }
  .brandtrack, .brandtrack[aria-hidden="true"] {
    display: flex; flex-wrap: nowrap; flex: 0 0 auto;
    gap: var(--brand-gap); padding-right: var(--brand-gap);
  }
  /* PAUSE UNDER A POINTER, and only where a pointer exists. A tap on a
     touch screen can weld :hover on, which would leave the strip
     stopped for the rest of the visit; the lift is behind the same
     query for the same reason. Both halves were verified on the
     rendered page: pointer on pauses, pointer off resumes, and a
     focusable element planted inside pauses it too. */
  @media (hover: hover) {
    .brandstrip:hover .brandmarquee { animation-play-state: paused; }
  }
  /* AND ON FOCUS-WITHIN. Nothing in the strip is focusable today, so
     this fires only if something ever becomes focusable inside it.
     That is the point: it is the rule, not a description of the
     current markup. */
  .brandstrip:focus-within .brandmarquee { animation-play-state: paused; }
}

@keyframes brand-drift {
  from { transform: translateX(0); }
  to { transform: translateX(-33.3333%); }
}

/* ---------- Breadcrumb ---------- */
.crumb { margin: 0 0 22px; font-size: .95rem; }
.crumb ol { display: flex; flex-wrap: nowrap; gap: 8px; list-style: none; margin: 0; padding: 0; color: var(--ink-2); }
.crumb li { flex-shrink: 0; }
.crumb li + li::before { content: "/"; margin-right: 8px; color: var(--rule); }
/* THE LAST CRUMB IS ONE LINE, added 2026-09-28, proposed-changes.md 3.60,
   on Greg's ruling that the first screen of a post is paid for in the
   crumb and not the headline. It serves the first screen: a post's last
   crumb repeats its H1, and on a phone it wrapped to two lines of its own.

   CLIPPED, NEVER HIDDEN. The whole title stays in the DOM, visible and in
   the accessibility tree, so a screen reader still reads it in full and it
   still mirrors the BreadcrumbList word for word; only the painted line
   ends in an ellipsis. min-width: 0 is what lets a flex item shrink below
   its text, without which the ellipsis never fires.

   ONE RULE AT EVERY WIDTH. At 1440 no crumb on the site is long enough to
   trigger it, measured, and on a phone it fires only on the posts whose
   title wrapped: 25px back on each.

   CRUMBS NEVER WRAP, added 2026-09-29, proposed-changes.md 3.82, on
   Greg's ruling. Under flex-wrap the last crumb moved to a line of its own
   before it could shrink, so the ellipsis above only ever clipped a second
   line and never saved the first. With nowrap, every crumb but the last
   holds its width and the last one alone shrinks into what is left: one
   line, always. It took Northeast Philadelphia's call row back above the
   call bar at 360, and it gives each of the sixteen posts a line back at
   360 and 390. Measured: nothing else on the site moved. */
.crumb li:last-child { flex-shrink: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* A PLACE NAME DOES NOT CLIP, 3.94. The ellipsis is for a post title that
   repeats its H1. At 360 "Northeast Philadelphia", the longest town crumb,
   needed 159px and had 156, so a town's own name ended "Philadelp...".
   Each separator spends the gap plus its own margin, 16px, twice in a
   town's three-crumb trail; at 400 and under both drop to 6px, which hands
   the last crumb 8px back and clears it by 5. One line still, never a
   wrap: the crumbs-never-wrap law is untouched. */
@media (max-width: 400px) {
  .crumb ol { gap: 6px; }
  .crumb li + li::before { margin-right: 6px; }
}

/* ---------- Sections ---------- */
.sec-head { max-width: 840px; margin: 0 auto var(--sec-gap); text-align: center; }
.sec-title { margin-bottom: .35em; }
.sec-sub {
  font-size: .84rem; font-weight: 800; letter-spacing: .1em; text-transform: uppercase;
  color: var(--ox-tx); margin: 0;
}
/* .dark is the BEHAVIOR: light type on a dark ground. The ground itself is
   .field-ox or .field-ink, so the two never get confused.

   OXBLOOD MEANS ACT, AND THE RULE IS ABOUT ACT BANDS. AMENDED 2026-09-17 on
   the client's ruling. It used to read "a .field-ox band always carries the
   CTA row; there are exactly two on a page", and that sentence was written
   about the bands that ASK FOR SOMETHING. It still governs them: the CTA
   band is the only oxblood band on a page that asks for an action, one on
   the front door and two on a long service page.
   AMENDED 2026-10-02 (3.91): a service page also carries #related, a
   third in-flow ox band that asks the reader to go somewhere, not to act.
   See the palette note for the amendment in full.

   WHAT IS NEW IS OXBLOOD AS A PROSE-SECTION GROUND. #who-we-are carries it
   and carries no button, no CTA row and no link. It joins the recorded
   section-background gradient exception in the palette note rather than
   creating a new one: same class, same 104-degree stops, same button
   grammar if a button ever lands in one.

   THE TEST, IF A THIRD EVER COMES UP: does the band ask the reader to do
   something? If it does, it is an act band and the act-band count governs
   it. If it only says something, it is a ground.

   Ink is structural: it breaks the silver run under proof and quotes, where
   there is nothing to click. */
.dark { color: var(--silver); }
/* THE DARK-GROUND BODY TONE, ADDED 2026-09-17 with the oxblood prose band.
   Headings and kickers take --silver from .dark above; a run of body copy
   takes --silver-2, which is the same hierarchy the ink bands already use.

   MEASURED ON BOTH DARK GROUNDS, worst case first: --silver-2 on --ox is
   7.34 and on --ink is 10.78, against a 4.5 floor and a 7 target for
   body-size type. The ox band's gradient is brightest at --ox exactly, so
   7.34 IS the worst pixel on it and the ends only get better: 12.39 on
   --ox-dk and 13.68 at the deep end.

   THIS IS NOT THE HERO'S RULE AND THE DIFFERENCE IS A PHOTOGRAPH. The hero
   note says there is no secondary text tone on an ox ground, and it is
   right about the hero: --silver-2's 0.34 of headroom over a 7 target is
   spent by the first photograph pixel that shows through a scrim, and it
   measured 6.57 there. A band has no photograph under it, so the 7.34 is
   the number and nothing takes it down. The rule was scoped on 2026-09-17,
   not relaxed: on ox OVER A PHOTOGRAPH there is still no secondary tone. */
.dark .prose p { color: var(--silver-2); }
/* THE ONE SANCTIONED GRADIENT ON THIS SITE. Decided 2026-09-10, and the
   palette law's own exception: section backgrounds may carry a gradient
   when it is a dated, recorded decision. This is that record. Everything
   else stays solid: no gradient on text, on a button, on a card or on an
   accent, and no glows anywhere.

   IT IS THE LOGO'S OWN GESTURE. The mark's red swoosh runs dark, bright,
   dark across its length, and the chrome swoosh under it does the same
   in grey. At band scale that reads as light falling across a painted
   panel, which is a fair thing for a shop that paints panels to have
   behind its words.

   IT STAYS INSIDE ONE HUE, WHICH IS THE POINT. The brightest place in
   the band is --ox exactly, and the ends are a deeper oxblood. Nothing
   new enters the band, so OXBLOOD STILL MEANS ACT and it means it from
   the first pixel. A gradient that started in --ink was drawn and
   rejected for that reason: an act band would have spent its first
   screen looking structural.

   THE DEEP END IS DERIVED, NOT INVENTED. color-mix() takes it from
   --ox-dk and --ink, both already in the palette, so no fifth colour is
   typed into this file and the palette still lives in one place. The
   plain --ox-dk gradient above it is the fallback: a browser without
   color-mix drops the second declaration and gets the softer version,
   and one without either gets the flat --ox background-color. All three
   are readable.

   MEASURED, at every stop:

       ground        silver text     ink button AS A SHAPE
       --ox              10.50                       1.47
       --ox-dk          12.39                       1.24
       deep end         13.68                       1.13

   Text gets MORE readable toward the ends, not less. The act button sits
   mid-band on --ox, and its silver hairline is what draws its shape in
   any case, which is the rule recorded in the contrast note above. */
.field-ox {
  background: var(--ox);
  background-image: linear-gradient(104deg, var(--ox-dk) 0%, var(--ox) 46%, var(--ox-dk) 100%);
  background-image: linear-gradient(104deg,
      color-mix(in srgb, var(--ox-dk) 72%, var(--ink)) 0%,
      var(--ox) 46%,
      color-mix(in srgb, var(--ox-dk) 72%, var(--ink)) 100%);
}
.field-ink { background: var(--ink); }
.dark .sec-sub, .dark .eyebrow { color: var(--silver); }
.dark a { color: var(--silver); }
/* THE FOCUS RING ON A DARK GROUND, added 2026-09-28, proposed-changes.md
   3.59, queued since 3.54. It serves a keyboard reader, who has to see
   where they are. The base ring is --ox, which on an ox band is oxblood on
   oxblood, 1.00, and on an ink band is 1.47: both invisible. --silver is
   the one tone that reads on both grounds, measured against the worst
   pixel of each: 10.50 on --ox, the gradient's brightest stop, and 15.42
   on --ink, against a 3:1 floor for a focus indicator.

   ONLY THE COLOUR CHANGES. Width, style and offset stay the base rule's,
   so a ring is the same shape on every ground. Scoped to .dark like every
   other dark-ground answer in this file, so it reaches the ox and ink
   bands and nothing on silver or white, where --ox already reads. */
.dark a:focus-visible, .dark button:focus-visible, .dark summary:focus-visible { outline-color: var(--silver); }
/* THE SAME RING ON THE DARK SURFACES .dark DOES NOT REACH, added
   2026-09-28, proposed-changes.md 3.60. The ink header, the ink footer and
   the photo heroes are dark without wearing .dark, and each drew the --ox
   ring at 1.00 to 1.47. The header is the first tab stop on every page.

   MEASURED UNDER THE RING'S OWN FOOTPRINT, worst pixel: 15.42 in the header
   and footer, and on the heroes, over the photograph through the scrim,
   6.65 at the worst stop and 8.24 or better at every other, against a 3:1
   floor. Colour only, as on .dark. */
.nav a:focus-visible, .nav button:focus-visible,
footer.site a:focus-visible,
.heroB a:focus-visible { outline-color: var(--silver); }
/* THE CALL BAR'S RING SITS INSIDE IT, on Greg's ruling, 3.60. The bar is
   fixed to the viewport's foot, so an outside ring lands on whatever has
   scrolled up behind it: silver on the silver page is 1.00, and --ox on an
   ox band is 1.00 too. No colour survives every ground, so the ring moves
   onto the one ground that never changes, the bar's own --ox, where silver
   is 10.50 at every scroll position. */
.callbar:focus-visible { outline-color: var(--silver); outline-offset: -6px; }
.band-panel { background: var(--panel); border-top: 1px solid var(--rule); border-bottom: 1px solid var(--rule); }

/* THE MEASURE IS CENTERED, not pinned left. At 1440 a 68ch column inside an
   1120px wrap left half the band empty and made the page read as a narrow
   strip of text with nothing beside it. Prose is now a centered column and
   every other section carries real structure across the full width. */
.prose { max-width: 70ch; margin-left: auto; margin-right: auto; }
.prose h3 { margin-top: 1.6em; }
.prose + .cta-row { justify-content: center; }
/* A CENTERED OPENING, Greg's ruling 2026-10-20, proposed-changes.md 3.114.
   The commercial page's #intro sits under a centered H2 and over a centered
   CTA row, so its two short paragraphs center with them. A modifier, not a
   change to .prose: every other prose block keeps its left-aligned text. */
.prose--center { text-align: center; }
/* THE LAST THING IN A PROSE BLOCK CARRIES NO MARGIN, added 2026-09-28,
   proposed-changes.md 3.59. It serves the section seam. p:last-child
   already zeroes a closing paragraph, and a closing list or heading kept
   its margin, which pushed the deer post's body-to-FAQ seam 17px past the
   3.46 baseline. A block's spacing is the section's to set, not its last
   child's.

   IT REACHES EVERY .prose ON THE SITE, and was measured on all of them
   before it landed: three blocks end on a child with a margin, and only
   the deer post's seam moves. The other two end above a .cta-row, whose
   26px top margin wins the collapse either way. */
.prose > :last-child { margin-bottom: 0; }

/* POST HEADLINES ARE SENTENCES, added 2026-09-28, proposed-changes.md
   3.59. It serves the first screen of a post. The site's H1 scale is sized
   for short service titles; a post headline runs 60 to 114 characters,
   took up to nine lines on a phone and pushed the first paragraph under
   the call bar. Scoped to the post header, so no service page, the home
   page and the blog index keep their H1 exactly. */
#post-head h1 { font-size: clamp(1.9rem, 4.6vw, 2.9rem); }
/* THE DATE LINE ON OX, added 2026-09-28, proposed-changes.md 3.61, when
   the post header took the compact ox header on Greg's ruling. It serves
   the one line in that header with no tone of its own: a plain paragraph,
   which would otherwise only inherit one from .dark. It takes --silver,
   the tone .field-ox .crumb ol and .field-ox .lead already take, so the
   header's text is one family. Measured on the band's brightest stop,
   --ox, in the record. Scoped to the ox header, so a post header on any
   other ground would not inherit a light tone meant for a dark one. */
#post-head.field-ox > .wrap > p { color: var(--silver); }

.grid2, .grid3, .grid4 { display: grid; gap: 22px; }
@media (min-width: 700px) { .grid4 { grid-template-columns: repeat(2, 1fr); } }
/* THE TWO-COLUMN CARD GRID CENTRES A LONE LAST CARD, 2026-09-24 on Greg's
   ruling, proposed-changes.md 3.55. An odd count leaves the last card
   alone in its row, and pinned left it reads as a card that lost its
   partner. So it keeps its siblings' width and sits in the middle.

   FOUR TRACKS, EVERY CARD SPANNING TWO, which is exactly two columns: a
   card is two tracks plus one gap, which is (width - gap) / 2, the same as
   before to the pixel (3.55 measures it). A lone last card starts at
   track 2 and so lands centred. Written this way so the gap stays the one
   value above and nothing here re-derives it: a calc() with the gap typed
   in again would be a second copy of it.

   WHAT IT REACHES TODAY, grepped across every page: one grid, the five
   action cards on /contact-us/, and every .grid2 the service-page
   template builds. No other page uses .grid2. .grid3 was left out here
   as "a separate decision"; Greg made it on 2026-10-21 (proposed-changes.md
   3.128), and .grid3 now takes this same construction, below. .payoff goes
   from one column straight to three and .repairs-grid is always one, so
   neither ever has a lone card in two columns; .payoff centres a short
   last row of three since 3.139, at its own block. */
@media (min-width: 700px) {
  .grid2 { grid-template-columns: repeat(4, 1fr); }
  .grid2 > * { grid-column: span 2; }
  .grid2 > :last-child:nth-child(odd) { grid-column: 2 / span 2; }
}
/* THE THREE-ACROSS CARD GRID CENTRES ITS INCOMPLETE LAST ROW, 2026-10-21
   on Greg's ruling from his review of /areas-served/, proposed-changes.md
   3.128: pinned left, the hub's Montgomery grid ended on two cards hugging
   the left edge and Philadelphia's one card sat alone at the left. .grid2's
   construction, carried to three across, as .svc-cards did for the router
   (3.88): twice the tracks, every card spanning two, so a card is exactly
   as wide as before to the pixel and the gap stays the one value above.
   Two across (700 to 999px) a lone last card starts at track 2 of 4, which
   is .grid2's rule and centres glass's third Repair or Replace card. Three
   across (1000px up) a last row of two starts at track 2 of 6 and a last
   row of one at track 3. The DOM order and the card order do not change;
   below 700 it is one column, as it was. */
@media (min-width: 700px) {
  .grid3 { grid-template-columns: repeat(4, 1fr); }
  .grid3 > * { grid-column: span 2; }
}
@media (min-width: 700px) and (max-width: 999px) {
  .grid3 > :last-child:nth-child(odd) { grid-column: 2 / span 2; }
}
@media (min-width: 1000px) {
  .grid3 { grid-template-columns: repeat(6, 1fr); }
  .grid3 > :nth-last-child(2):nth-child(3n+1) { grid-column: 2 / span 2; }
  .grid3 > :last-child:nth-child(3n+2) { grid-column: 4 / span 2; }
  .grid3 > :last-child:nth-child(3n+1) { grid-column: 3 / span 2; }
}
@media (min-width: 1000px) { .grid4 { grid-template-columns: repeat(4, 1fr); } }

.card {
  background: var(--panel); border: 1px solid var(--rule); border-radius: var(--radius);
  padding: 26px 24px;
}
.card h3 { margin-bottom: .45em; }
.card p { font-size: 1rem; color: var(--ink-2); }
.dark .card { background: rgb(var(--silver-rgb) / .08); border-color: rgb(var(--silver-rgb) / .28); }
.dark .card p { color: var(--silver); }

.tile { display: inline-flex; align-items: center; justify-content: center; width: 44px; height: 44px; margin-bottom: 16px; border: 2px solid var(--ox); border-radius: var(--radius); }
.tile svg { width: 24px; height: 24px; fill: none; stroke: var(--ox); stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.tile-dark { border-color: rgb(var(--silver-rgb) / .5); }
.tile-dark svg { stroke: var(--silver); }
.tile-dark svg .ico-accent { stroke: var(--silver); }

/* ---------- Numbered process steps ---------- */
.step { counter-increment: step; }
.steps { counter-reset: step; }
.step .num {
  display: block; font-size: .82rem; font-weight: 800; letter-spacing: .12em;
  color: var(--ox-tx); margin-bottom: 10px;
}

/* ---------- A plain checked list, used for the what-you-get bullets ---------- */
.ticks { list-style: none; margin: 0; padding: 0; display: grid; gap: 14px; }
@media (min-width: 800px) { .ticks { grid-template-columns: repeat(2, 1fr); gap: 14px 34px; } }
/* PROSE AFTER A CHECKED LIST, 3.125 (Greg's ruling of 2026-10-21). The
   ADAS page's #why closes on its paragraph, directly under the list, and
   the two had no air between them: the list carries no margin and a
   .prose none on top. 44px is .dark .cta-row's margin-top, the air the
   band's own ask had in the same place before the paragraph replaced it. */
.ticks + .prose { margin-top: 44px; }
.ticks li { position: relative; padding-left: 32px; }
.ticks li::before {
  content: ""; position: absolute; left: 0; top: .45em; width: 16px; height: 9px;
  border-left: 3px solid var(--ox); border-bottom: 3px solid var(--ox);
  transform: rotate(-45deg);
}
.ticks strong { display: block; }
/* A LIST INSIDE A RUN OF PROSE, 3.86. /adas-calibration/'s "required after:"
   list is one sentence broken into items, so it reads down one column, sits
   in the prose's own 70ch measure, and keeps a paragraph's spacing on both
   sides. Two columns would read a sentence across a gutter. A MODIFIER, NOT
   .prose .ticks: /commercial-collision-repair/'s #fleet already nests a
   .ticks list of vehicle types in prose, two-up at desktop, and that is
   its own decision to keep. */
.prose .ticks--run { grid-template-columns: 1fr; margin: 0 0 1.1em; }

/* ---------- Ordered steps in prose, the what-to-do-next list ---------- */
.numbered { counter-reset: n; list-style: none; margin: 0; padding: 0; display: grid; gap: 18px; }
@media (min-width: 900px) { .numbered { grid-template-columns: repeat(2, 1fr); gap: 22px 44px; } }
.numbered li { counter-increment: n; position: relative; padding-left: 52px; }
.numbered li::before {
  content: counter(n); position: absolute; left: 0; top: 0;
  width: 36px; height: 36px; border-radius: 50%;
  background: var(--ox); color: var(--silver);
  display: flex; align-items: center; justify-content: center;
  font-weight: 800; font-size: 1rem;
}
/* On an oxblood band an oxblood counter would vanish, so it inverts: a paper
   disc with oxblood numerals. Same idea for the tick marks in .ticks. */
.dark .numbered li::before { background: var(--silver); color: var(--ox); }
/* A PARAGRAPH AFTER THE LIST GETS ITS GAP BACK, added 2026-09-28,
   proposed-changes.md 3.62. .numbered zeroes its own margin, which is right
   where the list closes a block and a CTA row follows it with its own
   margin, and wrong where prose carries on beneath it: the Jamison page's
   second route sat flush against the last step. The gap is a paragraph's
   own, so the list reads as one more block in the run of prose. Grepped:
   the town page's directions are the only list followed by a paragraph. */
.numbered + p { margin-top: 1.1em; }
/* STEPS IN THE DIRECTIONS CARD STAY ONE COLUMN. Added 2026-09-28 for the
   town page's steps beside its map in a .split (3.63), and moved with them
   into the card in 3.65, which left the .split form with no user: it was
   3.63's own rule, so it goes rather than standing. .numbered goes two
   across at 900px, which suits a full-width list; in a card this narrow
   two columns of steps would read out of order. */
@media (min-width: 900px) { .dir-card .numbered { grid-template-columns: 1fr; } }
.dark .ticks li::before { border-color: var(--silver); }

/* ---------- Figures ---------- */
.figure { margin: 34px 0 0; }
.figure img { border-radius: var(--radius); width: 100%; }
.split { display: grid; gap: 30px; align-items: center; }
@media (min-width: 900px) { .split { grid-template-columns: 1fr 1fr; gap: 44px; } .split.flip .split-media { order: 2; } }

/* ---------- FAQ ---------- */
#faq details {
  border-top: 1px solid var(--rule); padding: 6px 0;
}
#faq details:last-of-type { border-bottom: 1px solid var(--rule); }
#faq summary {
  display: flex; align-items: flex-start; gap: 14px; justify-content: space-between;
  cursor: pointer; list-style: none; padding: 18px 0;
  font-size: 1.12rem; font-weight: 700; color: var(--ink);
}
#faq summary::-webkit-details-marker { display: none; }
/* A CHEVRON, NOT A PLUS. Adopted 2026-09-10 from candidate E of the
   motion sampler. Two bars at 45 degrees, and the whole mark turns over
   when the answer opens, so the control says which way it is going
   rather than swapping one symbol for another. */
.faq-ico { flex: 0 0 auto; width: 20px; height: 20px; position: relative; margin-top: 5px; }
.faq-ico::before, .faq-ico::after {
  content: ""; position: absolute; background: var(--ox); left: 50%; top: 50%;
  width: 11px; height: 3px; border-radius: 2px;
}
.faq-ico::before { transform: translate(-84%, -50%) rotate(45deg); }
.faq-ico::after { transform: translate(-16%, -50%) rotate(-45deg); }
.faq-ico { transition: transform 200ms cubic-bezier(.2, .8, .3, 1); }
#faq details[open] .faq-ico { transform: rotate(180deg); }

/* The answer's text arrives rather than appearing. THE PANEL'S HEIGHT
   STILL SNAPS, and that is not an oversight: animating it is animating a
   height, which this stylesheet does not do. The text fades and slides
   four pixels; the box does not glide. A one-shot animation, never a
   loop, and it runs because a person opened the disclosure. */
@keyframes faq-answer-in {
  from { opacity: 0; transform: translateY(-4px); }
  to   { opacity: 1; transform: none; }
}
#faq details[open] > p { animation: faq-answer-in 180ms ease-out both; }
#faq details > p { margin: 0 0 22px; max-width: 74ch; color: var(--ink-2); }

/* ---------- Contact band ---------- */
.contact { background: var(--ink); color: var(--silver); }
.contact .sec-sub { color: var(--silver); }
.contact a { color: var(--silver); }
.contact-grid { display: grid; gap: 26px; margin-top: 34px; }
@media (min-width: 800px) { .contact-grid { grid-template-columns: repeat(3, 1fr); } }
.contact-grid h3 { font-size: .84rem; letter-spacing: .1em; text-transform: uppercase; color: var(--silver); margin-bottom: .6em; }
.contact-alt { margin-top: 30px; }

/* ---------- Details and directions: /contact-us/'s #find-us ----------
   ADDED 2026-09-24 on Greg's ruling, proposed-changes.md 3.54, for the one
   section that carries them. The two columns are the existing .split and
   the heading is the existing .eyebrow and .sec-title, so what is here is
   only what the system lacked. Each rule names what it serves. */

/* The address, phone and email as three tappable lines. A list, because
   they are three separate actions; unstyled, because they are lines and
   not bullets. */
.contact-lines { list-style: none; margin: 18px 0 26px; padding: 0; display: grid; gap: 12px; font-size: 1.1rem; line-height: 1.5; }

/* The hours box. Silver on the white band, with the site's hairline, so
   it reads as a box without being a card: it is not a link and does not
   lift. Its label is .sec-sub on an h3, which gives it the kicker's
   face, size and tracking with no rule of its own. */
.hours-box { background: var(--silver); border: 1px solid var(--rule); border-radius: var(--radius); padding: 20px 22px; }
.hours-box .sec-sub { margin-bottom: 8px; }

/* The map box. A figure, so its credit line is its caption. The link
   wraps the whole drawing, so the frame is on the link, and the drawing
   scales to its column at every width. */
.map-box { margin: 0; }
.map-box > a { display: block; border: 1px solid var(--rule); border-radius: var(--radius); overflow: hidden; }
.map-box svg { display: block; width: 100%; height: auto; }

/* THE CREDIT IS THE LICENCE. OpenStreetMap's data is ODbL, and a map
   drawn from it must credit its contributors visibly and say the data is
   available under that licence. Small, but set in --ink-2 at full
   strength, because a credit nobody can read is not a credit. */
.map-credit { margin-top: 10px; font-size: .85rem; line-height: 1.5; color: var(--ink-2); }
.map-credit a { color: var(--ink-2); }

/* THE MAP'S OWN INK. Inline SVG reads these tokens, which is why the map
   is inline: a standalone file would have to type hex outside this file.
   Silver ground; roads white with a hairline casing, the through roads
   wider with an --ink-2 casing; labels --ink-2 over a white halo; route
   numbers in a white box edged in ink; and ONE oxblood element, the pin,
   because the map asks the reader to go somewhere and oxblood means act. */
.map-ground { fill: var(--silver); }
.map-case { fill: none; stroke: var(--rule); stroke-linecap: round; stroke-linejoin: round; }
.map-case--major { stroke: var(--ink-2); }
.map-road { fill: none; stroke: var(--white); stroke-linecap: round; stroke-linejoin: round; }
.map-label { fill: var(--ink-2); font-family: var(--font); font-weight: 600; paint-order: stroke; stroke: var(--white); stroke-width: 4px; stroke-linejoin: round; }
.map-shield { fill: var(--white); stroke: var(--ink); stroke-width: 1.5px; }
.map-shield-text { fill: var(--ink); font-family: var(--font); font-weight: 800; }
.map-pin { fill: var(--ox); stroke: var(--silver); stroke-width: 2px; }
.map-pin-dot { fill: var(--silver); }
.map-pin-label { fill: var(--ink); font-family: var(--font); font-weight: 800; paint-order: stroke; stroke: var(--silver); stroke-width: 5px; stroke-linejoin: round; }
/* THE TOWN'S CORNER, added 2026-09-28 for the town-page map, proposed-
   changes.md 3.63: where the route starts, marked in the map's structural
   ink with a silver core, so it reads as a place and not as the goal. The
   pin stays the one oxblood mark, because the shop is where the reader is
   going. Its name takes .map-pin-label, the same face and halo. */
.map-corner { fill: var(--silver); stroke: var(--ink); stroke-width: 3px; }

/* ---------- Footer ---------- */
footer.site { background: var(--ink); color: var(--silver); padding: var(--pad) 0 34px; }
footer.site a { color: var(--silver); }
.foot-grid { display: grid; gap: 34px; }
/* FOUR TRACKS, NOT THREE, since Hours was promoted out of the small
   print on 2026-09-22 (proposed-changes.md 3.29). The grid gained a
   child, so it gained a track.

   TWO STEPS RATHER THAN ONE, AND THE PHONE NUMBER IS THE REASON. Four
   across from 760 gives the three link columns 131px each, measured, and
   at that width "Call (215) 322-5350" breaks between the exchange and
   the line number. A phone number split across two lines is not a
   cramped layout, it is an unreadable fact, and this is the one fact the
   whole page is for. Two columns carry 760 to 899 at 343px each; four
   arrive at 900, where the link columns measure 177px and the number
   sits on one line.

   The identity column resolves WIDER than its 1.4fr share because the
   logo is 200px and min-content wins: 226px at 760 against the 197px the
   ratio alone would give. That is the track doing its job, not a bug,
   and it is why nothing overflows at the narrow end. */
/* AREAS WE SERVE IS A FIFTH COLUMN, 3.89, Greg's ruling, and the track
   count did NOT grow to five. Five across gives each link column 175px
   at 1440, and "Commercial Collision Repair", "Glass Repair &
   Replacement" and "Send us your details online" all wrap there,
   measured. So Get Started sits under Hours in ONE column, and the
   markup carries it in that order: on a phone the stack reads identity,
   Hours, Get Started, Explore, Areas, and tab order matches the columns
   on a desktop. The link columns keep their 222px at 1440 and nothing
   wraps from 1119 up. Below 900 the identity column spans the Hours and
   Get Started rows, so no column stands alone on a row. "auto 1fr" puts
   every spanning column's extra height into the last row, which keeps
   Get Started one gap under Hours rather than pushed down by the towns. */
.foot-id { grid-area: id; }
.foot-hours { grid-area: hours; }
.foot-start { grid-area: start; }
.foot-explore { grid-area: explore; }
.foot-areas { grid-area: areas; }
.foot-grid { grid-template-areas: "id" "hours" "start" "explore" "areas"; }
@media (min-width: 760px) {
  .foot-grid {
    grid-template-columns: 1fr 1fr; grid-template-rows: auto 1fr auto;
    grid-template-areas: "id hours" "id start" "explore areas";
  }
}
@media (min-width: 900px) {
  .foot-grid {
    grid-template-columns: 1.4fr 1fr 1fr 1fr; grid-template-rows: auto 1fr;
    grid-template-areas: "id hours explore areas" "id start explore areas";
  }
}
.foot-home img { width: 200px; height: 80px; object-fit: contain; }
/* THE FOOTER LOGO GETS A BOX OF ITS OWN, added 2026-09-28 on Greg's
   ruling, proposed-changes.md 3.60. It serves the focus ring. The link was
   inline, and an inline link wrapped around a block image has an empty
   inline box: the image sits outside it, so the outline had no box to be
   drawn around and no ring painted at all, in any colour, on every page.
   display: block gives the link a box; width: fit-content keeps that box,
   and so the focus target, the logo's size rather than the column's.

   MEASURED: the link's own box is the only box that changes, column width
   down to 200x80, on all 23 pages at 1440, 390 and 360. The header's
   .nav-home did not share the defect: it is display: block already and a
   flex item besides, one box exactly the logo's size. */
.foot-home { display: block; width: fit-content; }
.foot-tag { margin-top: 16px; max-width: 38ch; color: var(--silver-2); }
.foot-nap { margin-top: 18px; line-height: 1.8; }
.foot-head { font-size: .84rem; letter-spacing: .1em; text-transform: uppercase; color: var(--silver); margin: 0 0 14px; }
.foot-links { list-style: none; margin: 0; padding: 0; display: grid; gap: 11px; }
.foot-bottom { margin-top: 40px; padding-top: 24px; border-top: 1px solid rgb(var(--silver-rgb) / .18); font-size: .93rem; color: var(--silver-2); }
.foot-bottom a { color: var(--silver-2); }

/* The staging banner's rule came out at cutover with the banner. */

@media (prefers-reduced-motion: reduce) {
  * { animation: none !important; transition: none !important; scroll-behavior: auto !important; }
}


/* ===========================================================================
   FURNITURE, added 2026-09-06.

   The page was right about its words and wrong about its shape: correct,
   plain, and a wall of text below the fold. The stress-reader logic still
   governs the top of the page, the call bar, the hero and the proof band.
   Below the fold the reader is comparison shopping from a couch, and
   structure that rewards skimming wins.

   EVERY ICON ANCHORS A REAL THING AND NEVER REPLACES ITS SENTENCE. The
   sentence is always there; the icon is a handle for finding it again.
   One style throughout, extended from the hero badge set: a 24x24 viewBox,
   no fill, 2px strokes, round caps. No manufacturer logos anywhere, because
   a brand's logo is that brand's asset and using one needs their program
   art and their permission.

   MOTION IS STILL ZERO. Nothing here animates. The FAQ accordion is the
   only moving part on the page and it moves because a person opened it.
   =========================================================================== */

/* ---------- Stat band: three numbers, directly under the hero ----------
   THREE, AGAIN, Greg's eye ruling of 2026-10-12 (proposed-changes.md 3.99).
   It was THREE, NEVER FOUR while a fourth number would have had to be
   invented. 3.98 found a confirmed fourth, the founding year, and took the
   band four across; built and measured, it was ruled out by eye the same
   day, the odometer's pattern (3.85). The brands stat left rather than the
   year, because the brand strip already says it on the same page. So the
   third honest number replaced the word-stat era's arithmetic: one stat is
   still a word, and none was invented. A FOURTH needs its own ruling.
   THE ORDER IS THE BAND'S DOM ORDER, Greg's ruling of 2026-10-20 (3.100),
   and nothing here reorders it.
   ANY NUMBER TRUE ONLY ON THE DAY IT WAS WRITTEN CARRIES ITS DATE, in .stat-s.
   The review count is the one on this page. */
/* THE HIGHLIGHT GROUND. This is the one band on the site that is --white
   rather than --silver. See the palette note for why 1.12 is the point. */
/* ANYTHING ADDED HERE HAS TO BE ANSWERED in .statband.statcard above, which
   is the home page wearing this band as a floating card. */
.statband { background: var(--white); border-bottom: 1px solid var(--rule); }
.stats { display: grid; gap: 34px; }
@media (min-width: 820px) { .stats { grid-template-columns: repeat(3, 1fr); gap: 0; } }
/* CENTRED AT EVERY WIDTH, numeral, label and supporting line together,
   including the stacked phone layout. The padding is SYMMETRIC on purpose:
   the old rule put 44px on one side only, which would pull centred text
   off its own centre by 22px and look like a mistake nobody could find. */
.stat { padding: 8px 0; text-align: center; }
@media (min-width: 820px) {
  .stat { padding: 8px 22px; }
  .stat + .stat { border-left: 1px solid var(--rule); }
}
/* .fig-n, 3.68: .stat-n's look for a figure that is not one of the band's
   own. The counting effect it was split off from is withdrawn (3.85);
   the class stays as named. */
.stat-n, .fig-n {
  display: block; font-family: var(--display); font-weight: 400;
  font-size: clamp(3.1rem, 6.4vw, 4.7rem); line-height: .88;
  letter-spacing: -.025em; color: var(--ox);
}
.stat-l { display: block; margin-top: 15px; font-size: 1.06rem; font-weight: 700; }
.stat-s { display: block; margin-top: 7px; font-size: .95rem; color: var(--ink-2); }
/* THE FIGURES AS CARDS, added 2026-09-29, proposed-changes.md 3.67: the
   town page's proof section carries three figures in cards (two of them
   the band's, and the brands figure the band carried until 3.99), beside three claim cards, so Lifetime, the review count and 12
   still land at a glance. Centred, as .stat is. The band's size would run
   "Lifetime" about 320px wide into a card with about 297px inside it at
   1440, so a card sets its own; the line-height stays the band's .88em. */
.card-figure { text-align: center; }
.card-figure .stat-n, .card-figure .fig-n { font-size: clamp(2.6rem, 4.4vw, 3.6rem); }

/* ---------- The six-step process rail ---------- */
.steps { display: grid; gap: 22px; counter-reset: step; }
@media (min-width: 720px) { .steps { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1040px) { .steps { grid-template-columns: repeat(3, 1fr); } }
.step {
  counter-increment: step; position: relative;
  background: var(--panel); border: 1px solid var(--rule); border-radius: var(--radius);
  padding: 26px 24px 24px;
}
/* THE NUMERAL HANGS OFF THE HEADING, NOT OFF .step, and that is not a
   style choice. The lift needs both of .step's pseudo-elements, one for
   the shadow and one for the rule, and the numeral was sitting on
   ::before. h3 is static, so its containing block is .step, which is
   positioned: the numeral lands in exactly the same place it always
   did. The counter increments on .step and descendants can read it.
   A step without an h3 would lose its numeral. There is no such step,
   and a step without a heading is a bug worth seeing. */
.step h3::after {
  content: "0" counter(step);
  position: absolute; top: 20px; right: 22px;
  font-family: var(--display); font-size: 1.45rem; color: var(--rule); letter-spacing: -.02em;
}
.step h3 { font-size: 1.1rem; margin-bottom: .5em; padding-right: 48px; }
.step p { font-size: 1rem; color: var(--ink-2); margin: 0; }

/* ---------- Minor and Major, side by side ---------- */
.svc-grid { display: grid; gap: 26px; align-items: start; }
@media (min-width: 900px) { .svc-grid { grid-template-columns: repeat(2, 1fr); gap: 32px; } }
/* NO overflow: hidden HERE ANY MORE. It used to clip the photo to the
   card's corners, and it would clip the lift's shadow layer just as
   happily, which is the one adoption cost the sampler flagged. The
   rounding moved onto the image instead, which is where it can stay. */
.svc {
  background: var(--silver); border: 1px solid var(--rule); border-radius: var(--radius);
}
.svc-photo {
  display: block; width: 100%; aspect-ratio: 3 / 2; object-fit: cover;
  border-radius: var(--radius) var(--radius) 0 0;
}
.svc-body { padding: 28px 26px 30px; }
.svc-body > h3 { margin-bottom: .6em; }
/* The card mark sits 14px above its heading, the band's own gap (.fix h3). */
.svc-body > .ico { margin-bottom: 14px; }
.svc-body h4, .sec-sub, .eyebrow, .foot-head, .contact-grid h3 { font-family: var(--font); }
.svc-body h4 {
  font-size: .82rem; font-weight: 800; letter-spacing: .1em; text-transform: uppercase;
  color: var(--ox-tx); margin: 26px 0 8px;
}
.svc-body h4:first-of-type { margin-top: 20px; }
.svc-body p { font-size: 1rem; color: var(--ink-2); margin: 0; }

/* ---------- Quote cards ---------- */
.quote-grid { display: grid; gap: 22px; align-items: start; }
@media (min-width: 760px) { .quote-grid { grid-template-columns: repeat(2, 1fr); } }
.quote {
  background: rgb(var(--silver-rgb) / .07); border: 1px solid rgb(var(--silver-rgb) / .2);
  border-radius: var(--radius); padding: 26px 24px; margin: 0;
}
.quote svg { width: 26px; height: 26px; fill: none; stroke: var(--silver); stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; margin-bottom: 12px; }
.quote p { font-size: 1rem; line-height: 1.6; }
.quote figcaption { margin-top: 14px; font-weight: 700; font-size: .96rem; color: var(--silver-2); }

/* ---------- Three-card stack for the factory-certification payoff ----------
   3.128'S CENTRED LAST ROW, carried here on 2026-10-22 (proposed-changes.md
   3.139): /collision-repair/'s payoff lost its warranty card (R1 L3) and
   is two cards, which pinned left left an empty third column. Six tracks,
   every card spanning two, so a card is exactly as wide as before to the
   pixel; a last row of two starts at track 2, a last row of one at track 3.
   /paintless-dent-repair/'s three are unchanged. */
.payoff { display: grid; gap: 20px; margin-top: 34px; }
@media (min-width: 820px) {
  .payoff { grid-template-columns: repeat(6, 1fr); }
  .payoff > * { grid-column: span 2; }
  .payoff > :nth-last-child(2):nth-child(3n+1) { grid-column: 2 / span 2; }
  .payoff > :last-child:nth-child(3n+2) { grid-column: 4 / span 2; }
  .payoff > :last-child:nth-child(3n+1) { grid-column: 3 / span 2; }
}
.payoff .card p { color: var(--ink-2); font-size: 1.02rem; }
